Skip to content

docs(checklist): rule 42 — a shared refusal code makes an arm's witness ambiguous - #2008

Merged
lilyshen0722 merged 2 commits into
mainfrom
kai/checklist-refusal-code-attribution
Sep 29, 2026
Merged

lilyshen0722 merged 2 commits into
mainfrom
kai/checklist-refusal-code-attribution

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

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 same issuer_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 an issuer).

Measured as one instrument, same suite (30 tests), both directions — the survivor is not taken from the report:

arm fixture endpoint guard deleted result
no issuer (pre-repair, f0354405) yes 30/30 green — the guard is unwitnessed
issuer present (post-repair) yes that arm alone reds — on the code assertion: with the guard gone nothing is thrown, so thrown.code is a TypeError

That second row is also why the first revision of this rule was wrong, and the docs gate refused it at 568df1c2 with 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 — vendorFailureStatus in routes/hostedMcpConnect.ts does exactly this — two defects sharing a code are indistinguishable to the operator: a 502 reading issuer_metadata_incomplete does 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

node scripts/verify-numbered-rules.js docs/development/review-checklist.md
✓ 42 rules, numbers 1..42 ascending with no gap, 20 citation(s) all resolve.

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.

…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.
@lilyshen0722
lilyshen0722 added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 8caf4e6 Sep 29, 2026
22 checks passed
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.
lilyshen0722 added a commit to thantiklermcirony/commonly that referenced this pull request Oct 9, 2026
…ork point is measured (TASK-190) (Team-Commonly#2013)

* docs(checklist): rule 32's fidelity test is patch-id, and rule 44's fork point is measured (TASK-190)

Four corrections carried out of the Team-Commonly#2000 landing, each a defect in text now on
main rather than in an open branch.

1. RULE 44, a false statement of fact. The earned clause called `58298156` "a
   sibling of `1a811d84`". Measured on the real commits:
   `git merge-base 5829815 1a811d8` = `bbab5c31`, and
   `git merge-base 7f17163 1a811d8` = the same commit — so `7f171637` is no
   ancestor of `1a811d84` either, and the two heads are separate takes on the
   same TASK-180 change. The sentence now says "diverge at `bbab5c31`".

2. RULE 32, a wrong instrument. Its headline and its proof sentence compared
   `rev-parse <merge>^{tree}` against `<head>^{tree}`. Both failures were
   measured: it is BASE-RELATIVE (any base move under the PR makes the trees
   differ necessarily while the landing is faithful — Team-Commonly#1988/Team-Commonly#1989/Team-Commonly#1992 all
   landed faithfully with differing trees) and SQUASH-SHAPED ONLY (`a20ea9b3`,
   Team-Commonly#1994's landing, is a two-parent merge-queue commit whose `^{tree}` is
   main's tree, so the comparison cannot be run there at all). The test is now
   patch-id of `<merge>^..<merge>` against `<base>..<head>`, which is what held
   for all three of those landings (`81ecc062`, `b9feb00c`, `efcec768`) and is
   the instrument the AX audit reached independently for a refresh carry
   (entry 65).

3. THE REHEARSAL RIDER on rule 32: a guard arm, mutation run or probe binds to
   a head exactly as a clearance does. On Team-Commonly#1994's hold, three arms were quoted
   after a third head move, and ARM 1 had measured a file that no longer existed
   at the then-current head.

4. THE RENUMBER CLAUSE in rule 43: its own squash subject announces "rule 42
   … and rule 43" while the file it landed carries those as 43 and 44, because
   Team-Commonly#2008 merged its own rule 42 between the gate and the press. A rule number is
   a queue position; a reader citing the log gets both wrong.

TWO RECONCILIATIONS the instrument change required, beyond the row's scope and
deliberately in the same diff: rule 32's earned text and rule 43's body both
still asserted "the tree comparison", which the new headline contradicts. An
internal contradiction is worse than either sentence alone, and the change of
instrument is what made them false.

Guard: `node scripts/verify-numbered-rules.js` -> 44 rules, numbers 1..44
ascending with no gap, 28 citations all resolve. Docs only; no other file in the
tree quotes rule 32's tree test or its headline (checked by grep).

* docs(checklist): correct rule 32's test in the body, not in its lead (TASK-190)

The guard caught this, and it was right to. `node scripts/verify-numbered-rules.js`
without `--previous` is the weaker check — CI runs it with `--previous <main>`,
and that mode pins every rule's LEAD sentence as its name: a rewritten lead is
byte-indistinguishable from a foreign rule claiming the same number, so the job
failed with

  rule 32 on main is "Verify a merge at the consumer: for a squash merge, the
  merg…" and this version's rule 32 is "Verify a merge at the consumer: the
  change that landed must …" (line 105) — one number, two rules.

The script documents that as deliberate and without bypass, and its reason is
sound: ADR-028, ADR-019, REVIEW.md and nine citations inside the file refer to
rules by NUMBER, and the lead is how a reader verifies the citation landed on
the right text. So the correction moves to the body, where the script's own
docstring says in-place edits belong.

Rule 32 now keeps main's lead verbatim and opens its body by scoping it: the
tree equality in the lead is the SPECIAL CASE, not the test — it holds only
while the base has not moved under the pull request — with the general test
being patch-id of the merge's own range against the gated range. The lead is
therefore not left asserting a general rule it does not hold as.

That leaves a real gap, filed as TASK-198 rather than papered over: a rule whose
lead is ITSELF false has no in-place repair under this guard, only (a) scope the
falsehood in the body as done here, or (b) delete and re-append, which renumbers
every rule after it and re-points every citation. The proposed mechanism is a
declared amendment — the body naming the lead it replaces — which keeps the
property the guard actually wants, namely telling "same rule, retitled" from
"foreign rule at this number".

Everything else from the previous head stands: rule 44's fork point measured as
bbab5c3, the rehearsal rider, the renumber clause, and the two sentences that
had to stop saying "the tree comparison".

Guard, both modes: 44 rules, numbers 1..44 ascending with no gap, 28 citations
all resolve, and no rule changed its number or its name.

* docs(checklist): rule 32's second failure cause was false — four base-moved witnesses (TASK-190)

sprint-review's re-gate finding, reproduced and it holds. Rule 32 claimed tree
equality fails on Team-Commonly#1994 for a SECOND reason: "squash-shaped only: a20ea9b is a
two-parent merge-queue commit whose ^{tree} is main's tree rather than the
head's, so the comparison cannot be run there at all". Measured:

  git rev-list --parents -n 1 a20ea9b  ->  one parent (95356c5)

So it is not a merge of any shape. And with the base taken as the merge base,
the patch-id agrees on both sides exactly like the other three:

  95356c5..a20ea9b  391ca7f451da
  4e39c99..b2da9bf  391ca7f451da

Trees for the record: parent 0de9aac, landed commit 3a24f1a, carried head
683663d — a base move, the same cause as Team-Commonly#1988/Team-Commonly#1989/Team-Commonly#1992.

The clause is deleted rather than patched, and Team-Commonly#1994 joins the other three as a
fourth witness: one cause with four measurements beats two causes where one is
refuted by `rev-list`. On the shape claim itself, measured over main: all 200 of
its most recent commits have exactly one parent, and the newest two-parent
commit is dc9d849 (2026-04-07, 1766 commits back), so "the merge queue's shape"
describes something this repo stopped producing in April.

The rule's earned text now carries the correction, because the false claim was
published in the rule that exists to catch names and tests that do not measure
what they say they measure.

Guard, both modes: 44 rules, numbers 1..44 ascending with no gap, 28 citations
all resolve, no rule changed its number or its name.

* docs(checklist): the 60-character name pin is a window, not a wall (TASK-190)

sprint-review measured that fingerprint() ends .slice(0, 60), so the guard
binds only the first 60 characters of a rule's lead — and rule 32's claim
that such a correction has no in-place mechanism was therefore false.

Reproduced with a paired control on the real script: editing rule 32's lead
from character 61 stays green (44 rules, no rule changed its name, exit 0);
editing it at character 3 reds with the collision error. 40 of the file's 44
leads exceed 60 characters; 2,948 characters of lead text sit outside the pin.

States the boundary and rescopes TASK-198 to the question it actually raises.

* docs(checklist): name the base, and say what the two heads actually were (TASK-190)

Two findings from sprint-review's gate, both open in the text rather than
refuted by it.

(b) rule 32 prescribed the patch-id proof without naming the base, and the
only guidance for it sat three sentences away framed around a different trap.
The base is now named where the proof is prescribed:
BASE=$(git merge-base <carried-head> <merge>), the value actually run on all
four of today's landings.

(a) rule 44 called 5829815 and 1a811d8 'separate takes on the same change'.
They are stronger than that: both single-commit diffs return patch-id
81ecc06228fb, the patch-id the landing itself was verified by — the same
change on two bases, which is precisely why the stale stamp was seductive.

* docs(checklist): the pin counts normalised characters, so name the raw offset (TASK-190)

sprint-review's non-blocking addition, folded while the stamp is unspent rather
than deferred: fingerprint() strips ** and `*_ and collapses whitespace BEFORE
it slices, so the 60 are counted on the normalised lead and the boundary lands
at a different raw offset in every lead carrying markup.

Measured: rule 35's pin ends at raw character 64, rule 41's at 62, rule 13's at
61. Counting raw lengths gives 2,978 where the pin's own measure gives 2,948 --
the re-derivation error this sentence closes.

* docs(checklist): the pin can skip over 60 — rule 41's raw offset does not exist

Reviewer's finding: the sentence gave rule 41's raw offset as 62, computed as
the last offset before the boundary while rules 35 and 13 were given as the
offset at which the normalised count reaches 60 — one sentence, two quantities.

Reproduced here on the file's own parseRules/fingerprint, and it is worse than
a wrong number: fingerprint() ends .trim(), so a prefix that ends on a space
has that space discounted and the next raw character makes it interior and
counted. The normalised length can therefore step 59 -> 61 straight over the
boundary, and four leads have NO raw offset that normalises to exactly 60:
rules 3, 23 and 42 (raw 60 -> 61) and rule 41 (raw 62 -> 63). Exact landings:
34 leads at 60, rule 13 at 61, rule 35 at 64.

Guard, CI's mode: 44 rules, 1..44, 38 citations resolve, no rule changed its
number or its name.

* docs(checklist): state the pin's raw offsets as a set, and the renumber 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 Team-Commonly#2008 merging between
the gate and the press. It did not: Team-Commonly#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.

* docs(checklist): the mutation direction was inverted, and a re-derived count is not a control

sprint-review's second gate found one sentence false: the method this
paragraph hands a reader. "An edit to the last counted character leaves it
green, and one character earlier reds it" is backwards. The pin's last
counted character is inside the pin, so editing it reds the guard, and a
character later is outside it and green. Measured on rule 41, whose pin's
last counted character is the space at raw 62: raw 61 and 62 red, 63 and 64
green. A method given with its direction inverted lands the reader one
character past the boundary it is being used to find, which is how the wrong
number got published in the first place.

The paragraph's other numbers stand and are re-measured here from the lead
text: the pin ends at raw 60 in 37 of the 40 long leads, 61 in rule 13, 62
in rule 41, 64 in rule 35, and the prefix-by-prefix steps are at raw 60->61
in rules 3, 23 and 42 and at raw 62->63 in rule 41.

Also stated, because the retraction is the more useful finding: a count
re-derived with the instrument under test is not a control. The
prefix-by-prefix re-derivation was gated twice and passed, confirming the
error rather than the boundary. The arm that refutes a boundary claim is the
mutation in the direction the claim asserts, and the check on a
re-derivation is a differently-derived number.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant