← Blog

Claim boundaries you can run: gating public copy in CI

A written claim boundary decays unless something executes it. How RAP scans prose and rendered pages, keeps a must-fail control, and documents where the gate is unsound.

Nissan Dookeran8 min readCIClaim HygieneReddi Agent ProtocolField Notes

RAP Assurance (Reddi Agent Protocol Assurance) is an open, integration-first receipt and conformance layer for paid MCP/API and agent work.

Local/offline SDK primitives; not production/mainnet/live settlement evidence.

Payments prove transfer; RAP Assurance proves paid work.

Primary link: agent-protocol.reddi.tech

Field note, as of 2026-10-14 (UTC). This post is about the check that guards RAP's public copy, not about RAP's maturity. Checked by the author against RAP's public repository at a pinned commit.

What changed

A claim boundary written in a document is a promise that only holds on the day someone last read it. On 2026-09-30 RAP merged a pull request that did two things at once: it rewrote the public copy to a written boundary, and it added two checks that run that boundary.

Both checks share one list of forbidden claims, 11 patterns in lib/public-claims/public-claim-boundary-terms.ts. The static half, npm run check:claims:public, scans an enumerated set of owned repository prose and package manifests, discovers Markdown under public/docs and public/downloads, and reads the caption tracks declared by shipped onboarding guides; it does not recursively scan all repository prose or manifests. The rendered half is a Playwright spec that loads 16 routes and scans the DOM a visitor actually sees, with text this repository did not author excluded. The static half runs in a workflow that lists the paths that start it and installs no dependencies. The rendered half is documented as part of a blocking browser lane.

Then there is the part we think matters most: a negative control. node scripts/check-public-claim-boundaries.mjs --negative-control appends all 11 affirmative examples in memory to each of the 24 active prose, served-Markdown and caption files in its claim-scan list, and must exit 1. The separate historical-disclaimer and package-manifest checks are not injected by this control. The workflow runs the control after the real gate and fails if the control passes, so a gate that has stopped catching overclaims turns the build red instead of silently passing.

Why it mattered

The pull request is itself the evidence that a written boundary decays. It changed 121 files: the landing page, navigation, about 30 app routes, the README, the whitepaper, package READMEs and manifests. That count includes the new gate and its tests, but it is still a measure of how much public wording had to change in one go. Rewriting it once produces a snapshot. Without a check, the next edit to any of those pages can put an overclaim back, and nobody notices until a reader does.

What failed

RAP's own documentation is unusually direct about where the automation is unsound, and that is the part of the design we would copy first.

The qualifier problem has no clean answer. A boundary sentence has to mention the forbidden thing in order to deny it ("not a custody provider or a custody service"). So the gate has to tell a denial from a claim. A leading negation qualifies every same-family item in the enumeration it governs. But the gate cannot tell which noun a negation governs across independent clauses, so a claim joined to an unrelated negation by ", and" or ", or" passes. The documentation gives the shape: "custody is not claimed, and AUDD settlement is live today". The term list records two attempts to close the gap. One bound a form to the asserted predicate; the other split the clause before a finite verb. Each rejected truthful boundary sentences the repository actually ships while still admitting the same overclaim under a verb they did not anticipate. The code comment says the gate "does not pretend to" decide it: prose of that shape is a reviewer's call. The pull request removed the splitter and documented the limit instead.

Coverage is partial, and the document says how partial. app/ has 45 page routes. 16 are scanned in the rendered DOM. The other 29 are listed with a reason each: copy behind a wallet connection or multi-step state, a body that depends on a live network read, a primary body that is a third-party or user-entered record, one known false positive, and one redirect. Two routes were gated and then removed again because gating them put a live RPC read on a blocking lane. The coverage statement is checked: the gate derives the route partition from the filesystem, so the document cannot quietly stop matching the tree. That check accounts for every route; it does not verify that each exemption reason is right or establish semantic coverage.

One exclusion mechanism is only half tested. The rendered scan removes subtrees marked as external text. A control proves the mechanism works: a marked node is not scanned, and identical unmarked text is. But nothing asserts that the component carrying the marker marks the right fields, and the home route fills its specialist list from a network read, so a run often renders no such card and exercises the marker zero times. The documentation's answer is to review edits to that component by hand.

The gates cannot see everything that is public. The documentation says Markdown elsewhere under public/ is not scanned and 29 routes are not gated. By construction the checks read repository files and rendered routes, so anything outside those is outside them. GitHub repository descriptions and homepage fields are external metadata, not repository files or rendered routes; these two gates do not read or validate them, so they require separate editorial review.

Evidence

  • The checks exist and run. RAP's claim boundary, "Executable checks" describes both halves, the 16 gated routes and the 29 routes that are not.
  • The shared list and its known limit. The term list holds the 11 patterns, an expected-verdict table asserted by an always-on self-test, and the KNOWN LIMIT comment on the qualifier function.
  • The control that must fail. The workflow runs the real gate, then the negative control, and fails if the control passes.
  • The rendered half. The spec scans the gated routes, steps through every /tour caption and tests the exclusion mechanism with synthesised nodes.
  • How it was validated. Pull request 654 records, in its Testing section, a static gate over 40 surfaces, a control that exited 1 with 264 failures (24 active claim-scan files by 11 claims), 18 adversarial mutations of a scratch copy and 13 of 14 scenarios exercised locally. It also records that the unrelated-negation conjunction passes by design and needs reviewer judgment.

Claim boundary

  • What we claim. A written claim boundary is more reliable when something executes it, when the check scans what readers see and not only source files, when a must-fail control proves the check still bites, and when the document names where the check is unsound.
  • What we do not claim. That RAP's public copy is free of overclaims. By RAP's documentation neither check is complete semantic enforcement. A term list is a lexical aid, and every sentence about RAP still needs a human reader.
  • RAP. Local/offline SDK primitives and devnet-bounded demos as of 2026-10-14. No production, mainnet, live-settlement, security-audit, compliance or release claim. RAP's boundary document says public and contributor-facing claims should use it unless a later audited release note supersedes it.

How to inspect or reproduce

The static gate reads prose and imports one dependency-free module, so it needs no install. We ran it with Node 26; RAP's workflow uses Node 24.

git clone https://github.com/nissan/reddi-agent-protocol
cd reddi-agent-protocol
git checkout cf0807688c3ef90457f69ac29c50f9519d3f40a0   # the commit this post was checked against
node scripts/check-public-claim-boundaries.mjs                    # expect: OK: checked 40 public claim surface(s), exit 0
node scripts/check-public-claim-boundaries.mjs --negative-control # expect: exit 1, 264 failure lines across 24 files and 11 claim patterns

Node may print a warning about module type; it did not change either exit code when we ran it. To see the unsound case, read the KNOWN LIMIT comment in the term list and write the sentence it describes into a scratch copy of a scanned file. We did not run the rendered half for this post; it needs the app and a browser.

What we learned

  • Make the boundary executable, then make the executor prove it can fail. A gate with no negative control can pass forever for the wrong reason.
  • Write down where the check is unsound instead of papering over it. Two attempts to close the qualifier gap made the gate worse, and recording that was more useful than a third attempt.
  • Scan what the reader sees, and list what you do not scan. The most honest line in a coverage document is the one that names the 29 routes you chose not to gate, and why.