docs(checklist): rule 42 — a shared refusal code makes an arm's witness ambiguous - #2008
Merged
Merged
Conversation
…ss ambiguous Two guards that refuse with the same code make one arm ambiguous, and a guard added ABOVE an existing one silently transfers the old guard's only witness to the new one. The arm does not change, the test file shows nothing, the suite is green — which is why the review check is not "is this arm good" but "how many guards emit this code". Earned today on #2007/TASK-172. The new RFC 8414 §3.3 issuer check landed above an established endpoint guard; the neighbouring arm sent a document naming no issuer, kept refusing with the same `issuer_metadata_incomplete`, and so was pinning the new guard rather than the one it was written for. Deleting the endpoint guard changed no test in the neighbourhood. Measured on both sides of the repair, in one instrument: pre-repair arm + guard deleted = 30/30 green (the survivor), post-repair arm + guard deleted = that arm alone goes red, by name. Rider: where a route answers `{ error: code }` with no message, the shared code is client-visible too, so "same code" is a contract decision rather than a test detail. Guard: scripts/verify-numbered-rules.js → 42 rules, 1..42, 20 citations resolve.
…ssertion shape Corrected per the docs gate's HOLD at 568df1c. The first draft claimed a shared refusal code obliges every arm to assert the message; the rule's own incident disproves it, and I measured the counter-example before rewriting: arm fixture endpoint guard deleted result no `issuer` (pre) yes 30/30 green (guard unwitnessed) `issuer` present yes that arm ALONE reds — on the CODE assertion: with the guard gone nothing is thrown, so `thrown.code` is a TypeError So re-aiming the fixture is the fix and it is local; pinning the message is an option that additionally pins WHICH refusal, not a requirement. The review move: counting a code's emitters says where attribution is at risk (rule 25); the per-arm question is which guards can fire on its input, and the witness is a mutation of the guard that arm NAMES. The rider (a shared code is client-visible, so "same code" is a contract decision) is unchanged. Guard: 42 rules, 1..42, 20 citations resolve.
samxu01
pushed a commit
that referenced
this pull request
Sep 29, 2026
#2008 landed rule 42 (refusal codes as witnesses) at EOF first. This merge appends #2000's two sections after it and renumbers them 43 and 44, including the three in-text references to rule 43 (now 44). No other wording changed; verify-numbered-rules.js reports 1..44 with all 25 citations resolving. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NDyNbwmCco62PAAviSL1k
samxu01
pushed a commit
that referenced
this pull request
Sep 29, 2026
…er clause's real cause ux-lead's docs gate on the previous head measured three false sentences. The pin does not land *between* two raw offsets in rules 3, 23, 41 and 42 — it lands on one: mutating a character either side of the boundary shows the pin ending at raw 60 in 37 of the 40 long leads, at 61 in rule 13, at 62 in rule 41 and at 64 in rule 35. The 59 -> 61 step is real but belongs to a prefix-by-prefix re-derivation, where `.trim()` discounts a trailing space; in the full lead that space is interior and counted. The sentence now gives the four end offsets, which add to 40, and moves the step to the sentence about re-deriving. "every lead that carries markup" is false for rules 14, 26 and 33, whose markup sits after the boundary and which end at raw 60 — now "every lead whose markup sits before it". Rule 43 attributed the 42/43 -> 43/44 discrepancy to #2008 merging between the gate and the press. It did not: #2008 merged at 15:35:29Z, both clearing gates are at `b32428b9` (15:39 and 15:41), and that head's file already reads 43 and 44. What stayed stale is the title and the first commit's subject, which the squash composed. Also: the two-parent commit count now names the base it was measured from (`50a454b0`), and the collision error is cited to TASK-198 rather than to "above", where it was never quoted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
One rule appended to
docs/development/review-checklist.md(rule 42, new section Refusal codes as witnesses). Docs-only: no version bump, no source change.A guard added above an existing one can invalidate the arms below it without changing a line of them — the arm still passes, for the new guard's reason. What makes such an arm a witness is that its FIXTURE reaches the guard the arm NAMES, and the instrument for that is a mutation of that guard, not the shape of the assertion.
Two guards sharing a refusal code make that code non-discriminating for a given input, so counting a code's emitters says only where attribution is at risk (rule 25); the per-arm question is which guards can fire on its input. Re-aiming the fixture is the fix, and it is local and cheap. Asserting the exception message is an option that additionally pins which refusal — never a requirement. (An earlier revision of this rule claimed the opposite; see Earned.)
Earned
Today, on #2007/TASK-172. My RFC 8414 §3.3 issuer check landed above an established endpoint guard in
backend/services/hostedMcpIntakeService.ts. The neighbour arm — "refuses an incomplete document rather than guessing an endpoint" — was written when only the endpoint guard existed and sent a document naming no issuer, so it was answered by the newer guard, under the sameissuer_metadata_incomplete. Deleting the endpoint guard changed no test in the neighbourhood: the arm named a guard that could no longer answer its input.Found by the gate, not the author: vera measured that deleting
if (!body.authorization_endpoint || !body.token_endpoint)left the neighbourhood green, and her repair was one line (give the arm anissuer).Measured as one instrument, same suite (30 tests), both directions — the survivor is not taken from the report:
issuer(pre-repair,f0354405)issuerpresent (post-repair)thrown.codeis aTypeErrorThat second row is also why the first revision of this rule was wrong, and the docs gate refused it at
568df1c2with exactly this counter-example. The rule was rewritten against the measurement: a checklist line that over-prescribes the assertion shape sends every future reader to add a second assertion instead of asking which guard answers their input.Rider in the rule
Where a route answers
{ error: code }with no message —vendorFailureStatusinroutes/hostedMcpConnect.tsdoes exactly this — two defects sharing a code are indistinguishable to the operator: a 502 readingissuer_metadata_incompletedoes not say whether the document named no issuer or named no endpoint. So "same code" is a client-visible contract decision, not a test detail. Splitting the vocabulary is deliberately not in #2007; it is recorded on the TASK-172 row for whoever takes it.Guard
Gate: rhea — cleared at
3ec8034b. Numbering passes against current main; #2000 (rules 42 + 43) remains open, so whichever of the two merges second renumbers — mine, if it is mine.