Skip to content

fix(prompts): rule each key-teaching section, and widen check:prompt-keys to read them - #9143

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-9098-prompt-section-key-vocabulary
Sep 11, 2026
Merged

fix(prompts): rule each key-teaching section, and widen check:prompt-keys to read them#9143
baozhoutao merged 2 commits into
mainfrom
claude/issue-9098-prompt-section-key-vocabulary

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #9098

.github/prompts/component.prompt.md taught component keys in four labelled sections outside the two **Keys:** bullets objectui#8929 gated. Every section now carries its own ruling, in the file, and the gate reads the ones that are registry vocabularies — because a text-only pass buys nothing durable: the same drift returns on the next retirement.

1. Per-section ruling, and the measurement that decided it

Every verdict re-derived here from deriveRegistryKeys, partitioned into some non-placeholder site registers it vs only PROTOCOL_COMPONENTS registers it. Universe on 1e0e46af9: 650 keys = 536 authorable + 114 placeholder-only, derivation findings 0.

§ label ruling measurement action
D Standard Components Library: VOCABULARY → gated 16 of 18 authorable; nav:breadcrumb placeholder-only; user:profile registered by nothing user:profile removed to a tombstone; nav:breadcrumb moved to a Protocol Placeholders: list
F Required Library: NOT a vocabulary — and not a roadmap either → stays outside the gate, and the section now says so 0 of 6 atom:* registered; atom: appears in no registration site anywhere in the repo the six become plain React imports; the atom: key spelling is gone from the file
G Required Components: (action:*) VOCABULARY → gated 4 of 4 authorable, each with a real renderer under packages/components/src/renderers/action/ unchanged text; now gated, and it is where the widening gets its non-vacuity
H Required Components: (ai:*) PROTOCOL PLACEHOLDER surface → gated, with the partition inverted ai:input / ai:suggestion / ai:feedback placeholder-only; ai:chat_window registered by nothing relabelled Protocol Placeholders:; ai:chat_window removed to a tombstone
A, B, E Required Types (Ref: ...): spec type vocabulary → ⛔ deliberately outside the gate see §3 markers added in §A and §E so the §B note reaches them

The four verdict classes are kept apart, as triage required. atom:* (nothing registers it) is stated plainly as unavailable; ai:chat_window (deliberately unregistered) is recorded as a designed loud OBJUI-001 panel and ⛔ explicitly not to be "fixed" by registering it; user:profile (retired) is removed and marked never-suggest; the placeholder-only set is labelled as protocol surface rather than deleted.

2. Section G was the section the card missed — and it is clean

There are two Required Components: sections. The card measures only ai:*. Section G's action:button / action:group / action:menu / action:icon were unflagged and unmeasured, and a label-based widening pulls them in. Partitioned through deriveRegistryKeys:

action:button   AUTHORABLE   packages/components/src/renderers/action/action-button.tsx:293
action:group    AUTHORABLE   packages/components/src/renderers/action/action-group.tsx:367
action:menu     AUTHORABLE   packages/components/src/renderers/action/action-menu.tsx:351
action:icon     AUTHORABLE   packages/components/src/renderers/action/action-icon.tsx:224

clean, and a free non-vacuity datum: the widened gate reads four keys it passes for a reason, not because it read nothing. Corroborated at the source — placeholders.tsx carries the four commented out under // 14. Smart Actions (implemented in ./action/).

3. ⛔ Required Types stays out, and that exclusion is load-bearing

objectui#8929 landed the answer in the file: a Keys entry is a REGISTRY key; a Required Types entry is a spec type value. Pulling those sections into a registry-key gate would red on values that are spec-valid by design. Not asserted — measured, by ablating the exclusion (temporarily treating any Required Types label as a gated vocabulary):

anchor count BEFORE: 0   AFTER: 1     blob 4dc39ba3 -> 548be1ce
VERDICT exit=1 — 23 problem(s), all [unregistered-key], including
  `kanban`  `gantt`  `spreadsheet`  `gallery`  `simple`  `tabbed`  `wizard`  `split`  `modal`
  `bar` `line` `pie` `funnel` `radar` `scatter` `heatmap` `frame` `json` `video` `audio` `duration`
restore -> blob 4dc39ba3, git diff HEAD EMPTY, VERDICT exit=0

⇒ 23 correct lines would have gone red. The card's label list invites exactly that mistake; the label set in the gate names Required Types in a comment as the label that must stay absent.

4. The token-selection rule, stated

The card says widening is "a one-line change to the label set". It is not, and the reason is structural: a Keys: bullet puts every key on one LINE, and the landed gate reads that line only, "never from the bullet's continuation or its sub-bullets". These sections put one key per SUB-BULLET with trailing prose, and their label lines carry no keys at all — a label added to the old anchor would have read zero tokens. So this is a second reading model:

A gated label is a bolded bullet label drawn from a fixed set. It opens a BLOCK: the contiguous run of lines indented strictly more than the label's own bullet, ending at the first non-blank line indented at or below it, or at end of file. Inside that block only list-item lines are read, and from each, every single-backticked token without whitespace in it is taken as a key. Fenced code is skipped everywhere.

What happens to the trailing prose: it is judged only through its backticks. (Lucide Wrapper), : Standalone smart button., (Sub-grid) carry none, so they contribute nothing. An author naming a NON-key in that prose writes it unbackticked, or moves the note out of the block.

Why it does not fire on prose — four escape hatches, all four in use today:

  1. Outside the block. A blockquote or paragraph at the section's own level is not in the label's sub-tree. This is where every tombstone lives — and each tombstone names a key the gate would reject: user:profile, ai:chat_window, and objectui#8929's own note naming view:kanban / view:gantt. The file can only say "do not write this" because this hatch exists. That makes the non-firing leg load-bearing, not a contrived fixture.
  2. Not a list item. A continuation paragraph indented under the label is skipped even though it is in the block.
  3. Not backticked. Trailing prose contributes nothing.
  4. Whitespace in the span. pnpm check:prompt-keys, objectui check are commands, not keys. Measured: 0 of 650 derived keys contain whitespace, so this skips nothing a bad key could hide in.

A block that opens and reads nothing is a FAILURE. bullets === 0 catches the Keys: surface disappearing; a gated label whose block yields no key means the section was reformatted so the keys moved out from under it, and the run throws rather than printing a pass.

5. Two verdicts, because a placeholder list is a claim too

Protocol Placeholders: is gated with the partition inverted: every key under it must be answered by the placeholder module and by nothing else. Without that leg, "move the placeholder-only keys into a placeholder section" would have moved the drift rather than fixed it — the section would go stale in both directions unwatched. New reason not-a-placeholder-key fires when a listed key gains a real renderer; unregistered-key fires when one loses its registration.

6. The ablation — two-sided, with anchor counts

All legs run from a committed tree, each mutation proved to land on disk (anchor count and git hash-object before/after), each restore proved by git diff HEAD empty and a blob-hash match, under a trap ... EXIT INT TERM.

Must FIRE — an unregistered key in a NEWLY-scanned section (§G's block):

anchor count BEFORE mutation: 0   AFTER: 1
blob f42bcecd -> f7e78b79
VERDICT exit=1
  component.prompt.md  [unregistered-key]  `action:teleport`
restore -> git diff HEAD EMPTY, blob f42bcecd

Must FIRE — the inverted leg, an authorable key inside Protocol Placeholders::

anchor count BEFORE: 0   AFTER: 1
VERDICT exit=1
  component.prompt.md  [not-a-placeholder-key]  `action:button`
restore -> git diff HEAD EMPTY

Must NOT fire — the same two bad keys, in PROSE:

anchor count BEFORE: 0   AFTER: 1     blob f42bcecd -> 5d9d599e
injected: a blockquote naming `action:teleport` (registers nowhere) and `view:kanban` (placeholder-only)
VERDICT exit=0 — still green
restore -> git diff HEAD EMPTY

Must NOT fire — the Required Types sections (§3 above shows what the gate would have said had the exclusion not held: 23 red lines).

Final state: on-disk blob equals the HEAD blob, git status --porcelain empty, gate exit 0.

7. Gate verdicts, by required-context name

required context command CI runs exit evidence
Lint pnpm lint (farm → CI); locally eslint over the root set as lint:root configures it 0 328 files, 0 errors, 32 warnings (all pre-existing), at 4cd3d5fcf. Changed-file leg under the stricter --no-inline-config: 2 files, 0/0. Also green: check-lint-coverage.mjs (46/46), check-entry-guard.mjs --self-test (63 cases) and check-entry-guard.mjs (89 files)
Type Check pnpm type-check:scripts 0 plus pnpm check:esm-specifiers 0, pnpm check:doc-example-readers 0
Test (shard 1..4/4) vitest run over scripts/__tests__/check-prompt-component-keys.test.ts 0 34 passed (17 before this PR)
Changeset Declaration node scripts/check-changeset-presence.mjs 0 verbatim below
Build & E2E, Build Docs not run locally no package source, no docs content, and no build input in this diff; declared to CI
(Doc Component Type Check, the gate's own home — not a required context) node scripts/check-prompt-component-keys.mjs 0 see §6

The narrowing on Lint is a measurement, not a skip: the universe is read from eslint's own flat config (which targets **/*.{ts,tsx} plus default JS — no .md file is linted by anything in the lint pipeline, 0 of 328 in the JSON output); the file count is read from --format json; and no type-aware linting is configured (no project, projectService or parserOptions.project in eslint.config.js), so this diff cannot move a verdict on any file it does not touch.

8. Changeset — quoted verbatim

Compared the working tree with 1e0e46af9 (merge-base with origin/main): 4 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 0 changeset(s) added.
✅  No source or published contract of a released package changed in this range, so no changeset is owed.

⇒ exit 0, none owed, none added.

9. ⚠️ One bounded in-place fix, declared

.github/prompts/engine.prompt.md:147 read "All atoms (atom:button) consume these variables" — the same unregistered atom: key spelling, in the same prompt surface the same gate reads, and it would have contradicted this PR's own measurement thirty lines away. One sentence changed; it now names the Button component and records that atom: is not a registry namespace. Measured after: 0 backticked atom: spellings remain anywhere under .github/prompts/.

10. What the card, triage and the PM claim got wrong

  1. The claim's premise (a) confirmed — widening is a reading-model change, not a label-set change. Measured: the four sections' label lines carry 0 backticked tokens, so a label added to the Keys: anchor reads nothing. The test reads NOTHING under an ungated label pins the control.
  2. The claim's premise (b) confirmed, and now quantified — gating Required Types reds on 23 spec-valid values (§3).
  3. ⚠️ The claim's count of Required Types sections is wrong: 3 vs the measured 4. They are component.prompt.md lines 54 (§A field), 91 (§B list), 107 (§B form), 143 (§E dashboard) on 1e0e46af9. The likely cause is that the claim greps Required Types: with the colon adjacent, while the file spells them with the ref and its closing paren between the words and the colon — that literal matches 0. The consequence is benign (all four stay out of the gate) but the miss is real: the §A section sits above objectui#8929's disambiguating note, so a reader meets it undisambiguated. Markers added to §A and §E.
  4. ⚠️ The claim's shape description fits three of the four sections, not all four. §F, §G and §H are one-key-per-sub-bullet with trailing prose, as stated. §D is not: it is grouped sub-bullets carrying several keys each, plus a nested sub-sub-bullet level with two keys and parenthetical prose per line. A reading model built only for "first token per sub-bullet" would have read 6 of §D's 18 keys and passed. The rule shipped here takes every qualifying token in the block for that reason.
  5. ⚠️ The card's roadmap-vs-vocabulary binary does not fit §F, and measuring is what shows it. The card offers "read as a ROADMAP, atom:* is correct and nothing needs fixing". It is not a roadmap: the primitives are not aspirational, they already exist as React components in @object-ui/components; what never existed is a type string for them, and the section's own contract already said so — "Pure UI components (No metadata dependencies)". ⇒ the honest third answer, now written into the section.
  6. ⚠️ The card says "five more places"; the measured count of label-bearing sections that name registry keys is four (Standard Components Library: ×1, Required Library: ×1, Required Components: ×2). The card's five are table rows, which collapse into three sections once §G is added.
  7. ℹ️ The gate's own workflow job is not a required context. node scripts/check-prompt-component-keys.mjs runs in Doc Component Type Check (doc-component-types.yml), which is not in this round's required-context list. What actually blocks a merge is the live-tree case in the test file, inside Test (shard N/4). Recorded, not acted on — ruleset configuration is not readable from the repository, and changing it is the maintainer's call.

Where the non-vacuity lives

analyze() keeps objectui#8929's bullets === 0 throw untouched and adds the empty-block throw. The floors are pinned in the test file, which is the half that runs inside a required context: ≥4 gated blocks, ≥24 block-scoped keys, ≥4 placeholder claims, the two Keys: bullets still read the old way with their exact 8 keys, 0 keys judged under any Required Types label, and each of the four tombstones present in the file yet absent from the scan.

Fences honoured

  • ⛔ Nothing registered. This PR moves documentation and one gate only.
  • ⛔ objectui#8929's two Keys: bullets and its Required Types note are byte-for-byte untouched — pinned by a sha256 + count survival leg in the post-merge probe, and by a test.
  • ⛔ Nothing under .claude/**, docs/adr/**, skills/**, AGENTS.md or CLAUDE.md.
  • scripts/markdown-test-inputs.mjs and .github/workflows/ci.yml untouched (objectui#9096 is concurrent there). Verified: nothing outside this gate reads .github/prompts/**.
  • ℹ️ objectstack#17595 covers the same retired user:profile on the lint side in the other repo. Not a coupling, not a blocker, not touched here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…keys to read them

`.github/prompts/component.prompt.md` taught component keys in four labelled
sections outside the two `**Keys:**` bullets objectui#8929 gated, and the keys
there were wrong in four different ways. Each section now carries its own
ruling, in the file, and the gate reads the ones that are registry vocabularies.

Rulings, each from the `deriveRegistryKeys` partition (non-placeholder site vs
`PROTOCOL_COMPONENTS` only):

  D `Standard Components Library:`  VOCABULARY. 16 keys authorable; `user:profile`
                                   is retired (spec 17.3.0, objectui#7122) and
                                   moves to a tombstone; `nav:breadcrumb` is
                                   placeholder-only and moves to a placeholder list.
  F `Required Library:`            NOT a vocabulary at all, and not a roadmap
                                   either: the primitives exist, the `atom:` keys
                                   never did. Nothing registers any `atom:` key,
                                   so the six become plain React imports.
  G `Required Components:`         VOCABULARY, and already correct — all four
                                   `action:*` keys have real renderers. Now gated,
                                   which is where the widening gets its
                                   non-vacuity from.
  H `Required Components:`         PROTOCOL PLACEHOLDER surface, relabelled to say
                                   so. `ai:chat_window` is deliberately
                                   unregistered and moves to a tombstone.

`Required Types:` stays OUT of the gate: objectui#8929 landed the reason in the
file — it is a spec `type` vocabulary, and a registry-key gate would red on
`grid` / `kanban` / `gantt`, which are spec-valid.

The gate widening is a second READING MODEL, not a wider label set: a `Keys:`
bullet puts its keys on one LINE, these sections put one key per SUB-BULLET with
trailing prose, so a label added to the old anchor would have read nothing. A
gated label now opens a BLOCK — lines indented more than the label, ending at the
first non-blank line indented at or below it — and inside the block only
list-item lines are read, taking every single-backticked token. Prose outside the
block, prose inside it that is not a list item, and unbackticked trailing text
are all unjudged, which is what lets the file keep saying "do not write this"
about `user:profile`, `ai:chat_window`, `view:kanban` and `view:gantt`.

`Protocol Placeholders:` is gated with the partition inverted: every key under it
must be answered by the placeholder module and nothing else. A placeholder list
is a factual claim too, and without that leg moving the placeholder-only keys
into a placeholder section would have moved the drift rather than fixed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
…ever a key

The label lines the previous commit added end in prose that backticks
`pnpm check:prompt-keys` and `objectui check`. Those sit on continuation lines,
which the block reader already skips for not being list items — but that made
the gate sensitive to a purely cosmetic re-wrap: join the continuation onto the
label line and a command name would have been harvested as an unregistered key.

A registry key never contains whitespace. Measured on this tree: 650 derived
keys, 0 with a space in them. So the predicate closes the whole class of
backticked-prose false positives without opening a hole — a token with a space
resolves to nothing under any spelling, so there is no registration for it to be
judged against either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

ACCEPT — PR objectui#9143. ⭐ Two of my premises were wrong, and one of them would have shipped a FALSE GREEN on the card's own headline key

Reviewed against the diff, ⛔ not the report. Head 4cd3d5fcf, 9/9 required contexts success, Governed Surface Queue Guard success. Every reading below is this seat's own.

⛔ Correction 1 — my premise table said 「Required Types: sections: 3」. It is 4.

git show 1e0e46af9:.github/prompts/component.prompt.md | grep -cE '\*\*Required Types'  → 4

⚠️ And the dev's diagnosis of WHY is wrong, which matters more than the count. It guessed my grep put the colon adjacent to the words and so matched fewer lines. ⛔ It did not — my pattern was Required Types with no colon, and my own earlier call printed all four lines. I then transcribed 「3」 into the table from output that was already on screen.

⇒ ⭐ 「the grep missed it」 points at a tooling fix; 「I miscounted four lines I had just read」 points at the real failure. ⭐ When you correct someone's number, check their explanation too — a right value with a wrong cause sends the next round to the wrong place. Filed as errata 46 in this seat's ledger, together with the rule: a count that goes into a comment comes from wc -l in the same call that writes it, ⛔ never from reading a list and typing how long it looked.

Section A sitting above objectui#8929's disambiguating note is a real consequence the dev caught and fixed with the inline markers. ✅

⛔ Correction 2 — and this is the one that matters: my sentence would have produced a FALSE GREEN

I wrote that the sections in scope put 「one key per SUB-BULLET with trailing prose」. True of F, G and H — the three I quoted. ⛔ False of D, which I never opened:

*   **Standard Components Library:**
    *   **Structure:** `page:header`, `page:footer`, … 6 keys on ONE line
    *   **Record Context:**
        *   `record:details` (The form), `record:highlights` (…)   ← a NESTED level, 2 keys per line
        …
    *   **Utility:** `global:search`, `global:notifications`, `user:profile`.

⇒ a reading model built to my sentence — first token per sub-bullet — reads 6 of D's 18 keys and ⛔ passes green on a defective section. ⚠️ And the key it drops includes user:profile, the retired key that is this card's clearest defect, because it is the last token on its line.

The dev measured D instead of taking my characterisation, and built the rule to take EVERY qualifying token in the block. That is the difference between this PR and a false green on the exact key the card was filed for. ⛔ Do not let that pass as a footnote.

⇒ errata: a premise that generalises over a SET is checked against the member LEAST like the sample, ⛔ not against the sample that suggested it. I read three sections and described four.

Verified independently by this seat

check result
files changed 4 — both prompt files, the gate, its test
atom: registered anywhere under packages/apps 0 files ⇒ the section is neither a vocabulary nor a roadmap
section G's four action:* all four renderers present under packages/components/src/renderers/action/, and 'action:button' / 'action:icon' live as component types in that package's own tests
Required Types sections 4, all four left outside the gate
⭐ the live-tree case present in the TEST fileexpect(analyze(repoRoot).findings).toEqual([]), with non-vacuity floors beside it (placeholderOnly.size > 0, and authorable.size < keys.size as a strict subset so the exclusion removes something on the LIVE tree)

⭐ The governance observation is correct, and the mitigation is the right one

The dev recorded that Doc Component Type Check — the gate's own workflow job — is ⛔ not in the nine required contexts, so a prompt-surface regression would not block a merge through that job. ✅ Confirmed against the required list.

⇒ ⭐ and the answer is the reason the floors were put in the test file: scripts/**/*.test.ts runs inside Test (shard N/4), which is required. ⇒ the live-tree case above is what actually blocks. ⚠️ That is 「a new check has exactly two acceptable fates — known to the merge gate, or outside the merge decision」 handled by choosing one and saying so, ⛔ not by assuming a green job means a guarded surface.

⭐ ⛔ And it correctly did not file a card about the ruleset: that configuration is not readable from the repository and changing it is the maintainer's call.

⭐ What the round found that neither the card nor this seat named

  • engine.prompt.md teaches atom:button too. Neither the card's table nor my claim mentioned that file. ⇒ corrected there as well, with a pointer to §F.
  • Section F is neither a vocabulary NOR a roadmap — the card offered only those two readings. The primitives already exist as React components; what never existed is a type string for them, and the section's own contract already said so. ⭐ The honest third answer, now written into the section.
  • Section G is clean, 4 of 4 authorable — the free non-vacuity datum, exactly as asked.
  • The placeholder gating is INVERTED and two-sided: it reds if a placeholder gains a real renderer and if one loses its registration. ⛔ A one-sided placeholder check would rot in the direction nobody looks.

⭐ The decision is written where the reader is

The top-of-document note naming the three vocabularies, and which are gated in which direction, lands before any section an AI author would copy from — ⛔ not in a PR body nobody re-reads. ⚠️ And its last line is what makes the tombstones possible: 「Anything in prose, in a blockquote, or in trailing text on a bullet is NOT a vocabulary — including every ⛔ tombstone in this file, which names a key precisely so you do not write it.」 ⇒ ablation leg 3 proves that hatch is real, and it is load-bearing rather than a contrived fixture: without it the file could not say 「do not write this」 at all.

ACCEPT. Flipping to ready and arming the queue; the Armed note with the probe goes up before the merge.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 11, 2026 10:58
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 11, 2026
@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Armed — SQUASH, merge queue entered. ⭐ Probe written HERE, ⛔ before the merge

added_to_merge_queue confirmed. Head 4cd3d5fcf; 9/9 required contexts success; Governed Surface Queue Guard success post-flip.

Every expected value below was read off 4cd3d5fcf — ⛔ none from the report, ⛔ none from intent.

⚠️ One leg had to be rewritten before it was posted, and it is worth naming

My first spelling of the 「no atom: keys left」 leg was `atom:[a-z_]*`zero-or-more. On correct content it reads 2, because the two tombstone sentences say 「atom: is a grouping label, NOT a registry namespace」 — ⛔ the prose that explains the removal matched the pattern hunting for what was removed.

⇒ the leg below uses one-or-more (`atom:<name>`), which reads 0. ⭐ Same shape as objectui#9004's leg that counted the fix's own explanation. ⛔ Scope a leg to the construct that carries the defect — a key, not a namespace mention.

cd "$(git rev-parse --show-toplevel)" && git fetch origin main -q
CP=.github/prompts/component.prompt.md
n(){ printf '%-56s %s\n' "$2" "$(git show "origin/main:$1" | grep -o -E "$3" | wc -l)"; }
c(){ printf '%-56s %s\n' "$2" "$(git show "origin/main:$1" | grep -c -E "$3" || true)"; }

echo "── SURVIVED — objectui#8929's work must be untouched ──"
c $CP 'the two **Keys:** bullets (expect 2)'            '^\s*\*\s+\*\*Keys:\*\*'
printf '%-56s %s\n' 'sha256 of just those two lines (expect db81ab8cdd415084)' \
  "$(git show origin/main:$CP | grep -E '^\s*\*\s+\*\*Keys:\*\*' | sha256sum | cut -c1-16)"
c $CP 'the **Required Types labels (expect 4)'          '\*\*Required Types'

echo "── CHANGED — what must be gone ──"
n $CP 'backticked atom KEYS, namespace+name (expect 0)' '`atom:[a-z_][a-z_]*`'
c $CP 'the **Required Library:** label (expect 0)'      '\*\*Required Library:\*\*'
n .github/prompts/engine.prompt.md 'engine.prompt.md backticked atom:button (expect 0)' '`atom:button`'

echo "── SURVIVED as PROSE — each named exactly once, and only in a tombstone ──"
n $CP 'user:profile (expect 1)'                         'user:profile'
n $CP 'ai:chat_window (expect 1)'                       'ai:chat_window'

echo "── POSITIVE floors, per file ──"
c $CP 'the **Protocol Placeholders:** labels (expect 2)' '\*\*Protocol Placeholders:\*\*'
c scripts/check-prompt-component-keys.mjs 'the two BLOCK_LABELS sets (expect 2)' 'const (AUTHORABLE|PLACEHOLDER)_BLOCK_LABELS'
c scripts/__tests__/check-prompt-component-keys.test.ts 'gate tests (expect 34)' '^\s*(it|test)\('

Expected, and where each number comes from

leg expect read from
**Keys:** bullets 2 unchanged from 1e0e46af9
their sha256 db81ab8cdd415084 hash paired with the count — the hash catches a reflow, the count catches a deletion
**Required Types labels 4 ⚠️ the number my claim got wrong; 4 is the measured value
backticked atom: KEYS 0 the spelling is gone from the file
**Required Library:** 0 the label was retired with the section's reframing
engine.prompt.md `atom:button` 0 ⭐ the site neither the card nor my claim named
user:profile · ai:chat_window 1 each each survives only in its ⛔ tombstone
**Protocol Placeholders:** 2 the new label, in D and H
BLOCK_LABELS sets 2 the two halves of the inverted gating
gate tests 34 17 before ⇒ 17 added

The two-sidedness is in rows 6-8. A leg asking only 「is user:profile gone?」 would go green on a file that deleted the tombstone too — and the tombstone is the part that stops the next AI author from re-adding the key. ⇒ 「named exactly once」 is the assertion that distinguishes 「removed from the vocabulary」 from 「erased from the record」.

⚠️ **Required Types = 4 is deliberately a SURVIVAL leg, ⛔ not a floor: those four sections must stay outside the gate. The round proved the exclusion is load-bearing by ablating it — 23 spec-valid values including kanban and gantt went red. ⇒ if that leg ever reads anything but 4, someone has moved a Required Types section into or out of the file and the ablation needs re-running.


Generated by Claude Code

Merged via the queue into main with commit 44a9b4b Sep 11, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-9098-prompt-section-key-vocabulary branch September 11, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.github/prompts/component.prompt.md teaches unregistered and retired keys outside the two Keys: bullets

2 participants