From 549936fe473d5e44ffac81c857875ffb9a151fee Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 19:53:50 +0000 Subject: [PATCH] ci(docs-drift): state the same-name-different-shape trap where the hand-grep remedy is prescribed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The advisory's fold already prescribes a hand re-read for the rule-carrying blind spot. A hand re-read is a name-based search, and a name does not identify a key: the same spelling is authorable on one governed type and a `[REMOVED]` tombstone on another for `active`, `aria`, `joins`, `objects`, `template`, `tools` and `version`. Nothing in a search result tells the two apart, so a grep hit on a live example reads as evidence about the dead key — measured on #19059, where content/docs/ai/agents.mdx was reported as contradicting the `agent.tools` tombstone over an example inside a `defineSkill({` block. The remedy's own failure mode now renders beside the remedy, unconditionally, with the censused names as the payload and the discriminating parse as the fix. Pinned byte-exact on every case in check-drift-comment.mjs, the same shape the neighbouring unconditional line takes. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- .github/workflows/docs-drift-check.yml | 43 ++++++++++++++++++++++ scripts/docs-audit/check-drift-comment.mjs | 38 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/.github/workflows/docs-drift-check.yml b/.github/workflows/docs-drift-check.yml index 47175bbc9d3..f050c7c2394 100644 --- a/.github/workflows/docs-drift-check.yml +++ b/.github/workflows/docs-drift-check.yml @@ -355,6 +355,49 @@ jobs: // run by construction, exactly like the SDK bridge line above, and stating it // only sometimes would be the lie. limits.push('a page that states a rule by its **inputs** shares no identifier with the **emitter** that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on #11430: `content/docs/protocol/objectql/types.mdx` documents the text-family column mapping by the ObjectQL type names it maps FROM (`text` / `textarea` / `html`) while the diff changed `createColumn`; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.'); + // ── A KEY NAME IS NOT A KEY — THE SECOND BOUNDARY OF THE HAND-GREP REMEDY (#19093) + // + // The line above ends by prescribing a remedy: re-read the rule by hand in the + // pages that restate it. This one states that remedy's OWN failure mode, because + // the remedy is a name-based search and a name does not identify a key. + // + // The same spelling can be authorable on one schema and a `[REMOVED]` tombstone + // on another, and a search result carries nothing that tells the two apart. So a + // hand-grep that lands on a page's LIVE example reads it as evidence about the + // DEAD key — a false alarm that is indistinguishable from a real find until + // someone parses the value. That is not hypothetical: on #19059 a reviewer + // grepping `content/docs` for `tools` reported `content/docs/ai/agents.mdx` as + // contradicting the `agent.tools` tombstone, citing an example at `:161` that is + // inside a `defineSkill({` block opened at `:155`. The page was already correct; + // the cost was one stop-the-line alarm and a round of the implementing dev's + // attention. The grep was the RIGHT method — it is what found the genuinely + // falsified page on #19080, in exactly the blind spot the line above names. + // + // The names are the payload, and they are a census rather than the one instance: + // measured on #19093 by walking the liveness ledger's governed types and reading + // each top-level key's `[REMOVED]` marker. ⛔ A COUNT IS NOT A READING HERE — the + // whole defect is that a count cannot tell two identically-named keys apart, so + // this line prints the names and any re-measure must do the same. + // + // ⛔ Its radius is stated because it is NOT the whole class. Top-level keys of + // governed types only: a key retired through the `.strict()` guidance-map route + // instead of `retiredKey()` leaves NO tombstone in the walked shape and so cannot + // appear here at all — `tool.category` is retired that way while + // `AIToolDefinition.category` stays live, the identical trap one radius out. + // + // ⛔ Sited here, not in the liveness ledger's `_note`, and that was a choice + // between two candidates. The ledger records the verdict; this comment reaches + // the reader AT THE MOMENT THE METHOD IS PRESCRIBED, on every run, in the same + // fold as the prescription. A reader alarmed about `agent.tools` opens the AGENT + // ledger — so a discriminator written on the SKILL ledger sits on the half they + // have no reason to open, which is the failure this card exists to stop. + // + // Unconditional by construction, like the line above and for the same reason: + // the collision is a property of the schemas, not of any diff. Pinned byte-exact + // on every case in `scripts/docs-audit/check-drift-comment.mjs` + // (`SAME_NAME_DIFFERENT_SHAPE`) — an unconditional line nothing pins is one that + // can vanish silently, which is the defect that file was written for. + limits.push('a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a `[REMOVED]` tombstone on another for each of `active`, `aria`, `joins`, `objects`, `template`, `tools` and `version` (censused on #19093 over the liveness ledger\'s governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on #19059: `content/docs/ai/agents.mdx` was reported as contradicting the `agent.tools` tombstone over its `tools:` example at `:161`, which is inside the `defineSkill({` block opened at `:155` — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES `SkillSchema`, and as an `AgentSchema` it FAILS at `tools` with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a `.strict()` guidance map leaves no tombstone in the walked shape and none of them here (`tool.category`, live as `AIToolDefinition.category`).'); // A broken scan is not a coverage number and must not read like one — see // `bridgeCoverageFrom`. Rendered outside the collapsed section below, because // "the bridge could not read the ledgers" is not a limit of this run, it is a diff --git a/scripts/docs-audit/check-drift-comment.mjs b/scripts/docs-audit/check-drift-comment.mjs index 959fc85e58b..2063410edc4 100755 --- a/scripts/docs-audit/check-drift-comment.mjs +++ b/scripts/docs-audit/check-drift-comment.mjs @@ -230,6 +230,37 @@ const RULE_CARRYING_BLIND_SPOT = + ' detect this on, so a rule your change carries has to be re-read by hand in the pages' + ' that restate it.'; +/** + * The #19093 boundary line, byte-exact — the SECOND fold entry that is not a report about + * the run, and the second asserted on every case. + * + * The class: the line above prescribes a hand re-read, and a hand re-read is a name-based + * search. A name does not identify a key — `tools` is authorable on `SkillSchema` and a + * `[REMOVED]` tombstone on `AgentSchema` — so a grep hit on a live example is + * indistinguishable from evidence about the dead key. Measured on #19059, where + * `content/docs/ai/agents.mdx` was reported as contradicting the `agent.tools` tombstone + * over an example that sits inside a `defineSkill({` block. + * + * ⛔ The payload is the NAMES, never a count: the defect is precisely that a count cannot + * tell two identically-named keys apart. A re-measure that replaces them with a number has + * removed the reading, and this pin is byte-exact so it cannot happen quietly. + */ +const SAME_NAME_DIFFERENT_SHAPE = + 'a key NAME is not a key, so the hand re-read the line above prescribes can land on the' + + ' wrong schema. The same spelling is authorable on one governed type and a `[REMOVED]`' + + ' tombstone on another for each of `active`, `aria`, `joins`, `objects`, `template`,' + + ' `tools` and `version` (censused on #19093 over the liveness ledger\'s governed types,' + + ' top-level keys); nothing in a search result distinguishes the two, so a grep hit on a' + + ' LIVE example reads as evidence about the DEAD key. Measured on #19059:' + + ' `content/docs/ai/agents.mdx` was reported as contradicting the `agent.tools` tombstone' + + ' over its `tools:` example at `:161`, which is inside the `defineSkill({` block opened' + + ' at `:155` — the page was already correct. Settle ownership by PARSING the value against' + + ' both schemas, never by the name: that literal PASSES `SkillSchema`, and as an' + + ' `AgentSchema` it FAILS at `tools` with the tombstone prescription. ⛔ These names are' + + ' not the whole class — a key retired through a `.strict()` guidance map leaves no' + + ' tombstone in the walked shape and none of them here (`tool.category`, live as' + + ' `AIToolDefinition.category`).'; + /** * `want` is the mapper contract each case rides on — asserted before any text is, so a * case that silently stopped exercising its branch fails here instead of passing there. @@ -348,6 +379,13 @@ try { true, body.includes(`- ${RULE_CARRYING_BLIND_SPOT}`)); check(c.id, 'and it stays out of the headline, which reports this run only', false, headline.includes('shares no identifier with the **emitter**')); + // #19093 — the same opposite pin, for the same reason, on the line that states the + // hand-grep remedy's own failure mode. Byte-exact so the NAMES cannot be quietly + // replaced by a count: on this class a count is structurally not a reading. + check(c.id, 'the same-name-different-shape boundary is stated in the fold, byte-exact', + true, body.includes(`- ${SAME_NAME_DIFFERENT_SHAPE}`)); + check(c.id, 'and it stays out of the headline, which reports this run only', + false, headline.includes('a key NAME is not a key')); c.expect(headline, body); } } finally {