fix(prompts): rule each key-teaching section, and widen check:prompt-keys to read them - #9143
Conversation
…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
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 keyReviewed against the diff, ⛔ not the report. Head ⛔ Correction 1 — my premise table said 「
|
| 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 file — expect(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.
⭐ ⛔ 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.mdteachesatom:buttontoo. 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
typestring 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.
⇒ ACCEPT. Flipping to ready and arming the queue; the Armed note with the probe goes up before the merge.
Generated by Claude Code
Armed —
|
| 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 | |
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
Fixes #9098
.github/prompts/component.prompt.mdtaught 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 onlyPROTOCOL_COMPONENTSregisters it. Universe on1e0e46af9: 650 keys = 536 authorable + 114 placeholder-only, derivation findings 0.Standard Components Library:nav:breadcrumbplaceholder-only;user:profileregistered by nothinguser:profileremoved to a tombstone;nav:breadcrumbmoved to aProtocol Placeholders:listRequired Library:atom:*registered;atom:appears in no registration site anywhere in the repoatom:key spelling is gone from the fileRequired Components:(action:*)packages/components/src/renderers/action/Required Components:(ai:*)ai:input/ai:suggestion/ai:feedbackplaceholder-only;ai:chat_windowregistered by nothingProtocol Placeholders:;ai:chat_windowremoved to a tombstoneRequired Types (Ref: ...):typevocabulary → ⛔ deliberately outside the gateThe 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 onlyai:*. Section G'saction:button/action:group/action:menu/action:iconwere unflagged and unmeasured, and a label-based widening pulls them in. Partitioned throughderiveRegistryKeys:⇒ 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.tsxcarries the four commented out under// 14. Smart Actions (implemented in ./action/).3. ⛔
Required Typesstays out, and that exclusion is load-bearingobjectui#8929 landed the answer in the file: a
Keysentry is a REGISTRY key; aRequired Typesentry is a spectypevalue. 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 anyRequired Typeslabel as a gated vocabulary):⇒ 23 correct lines would have gone red. The card's label list invites exactly that mistake; the label set in the gate names
Required Typesin 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: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:
user:profile,ai:chat_window, and objectui#8929's own note namingview: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.pnpm check:prompt-keys,objectui checkare 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 === 0catches theKeys: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 reasonnot-a-placeholder-keyfires when a listed key gains a real renderer;unregistered-keyfires 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-objectbefore/after), each restore proved bygit diff HEADempty and a blob-hash match, under atrap ... EXIT INT TERM.Must FIRE — an unregistered key in a NEWLY-scanned section (§G's block):
Must FIRE — the inverted leg, an authorable key inside
Protocol Placeholders::Must NOT fire — the same two bad keys, in PROSE:
Must NOT fire — the
Required Typessections (§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 --porcelainempty, gate exit 0.7. Gate verdicts, by required-context name
Lintpnpm lint(farm → CI); locallyeslintover the root set aslint:rootconfigures it4cd3d5fcf. 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) andcheck-entry-guard.mjs(89 files)Type Checkpnpm type-check:scriptspnpm check:esm-specifiers0,pnpm check:doc-example-readers0Test (shard 1..4/4)vitest runoverscripts/__tests__/check-prompt-component-keys.test.tsChangeset Declarationnode scripts/check-changeset-presence.mjsBuild & E2E,Build DocsDoc Component Type Check, the gate's own home — not a required context)node scripts/check-prompt-component-keys.mjsThe narrowing on
Lintis a measurement, not a skip: the universe is read from eslint's own flat config (which targets**/*.{ts,tsx}plus default JS — no.mdfile 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 (noproject,projectServiceorparserOptions.projectineslint.config.js), so this diff cannot move a verdict on any file it does not touch.8. Changeset — quoted verbatim
⇒ exit 0, none owed, none added.
9.⚠️ One bounded in-place fix, declared
.github/prompts/engine.prompt.md:147read "All atoms (atom:button) consume these variables" — the same unregisteredatom: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 theButtoncomponent and records thatatom:is not a registry namespace. Measured after: 0 backtickedatom:spellings remain anywhere under.github/prompts/.10. What the card, triage and the PM claim got wrong
Keys:anchor reads nothing. The testreads NOTHING under an ungated labelpins the control.Required Typesreds on 23 spec-valid values (§3).Required Typessections is wrong: 3 vs the measured 4. They arecomponent.prompt.mdlines 54 (§A field), 91 (§B list), 107 (§B form), 143 (§E dashboard) on1e0e46af9. The likely cause is that the claim grepsRequired 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.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 atypestring 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.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.node scripts/check-prompt-component-keys.mjsruns inDoc 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, insideTest (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'sbullets === 0throw 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 twoKeys:bullets still read the old way with their exact 8 keys, 0 keys judged under anyRequired Typeslabel, and each of the four tombstones present in the file yet absent from the scan.Fences honoured
Keys:bullets and itsRequired Typesnote are byte-for-byte untouched — pinned by a sha256 + count survival leg in the post-merge probe, and by a test..claude/**,docs/adr/**,skills/**,AGENTS.mdorCLAUDE.md.scripts/markdown-test-inputs.mjsand.github/workflows/ci.ymluntouched (objectui#9096 is concurrent there). Verified: nothing outside this gate reads.github/prompts/**.user:profileon 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