Skip to content

feat(qa-checklist): flag an area-level recipe no item references (#11506) - #11847

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11506-unreferenced-recipe-direction
Aug 24, 2026
Merged

feat(qa-checklist): flag an area-level recipe no item references (#11506)#11847
os-steve merged 1 commit into
mainfrom
claude/issue-11506-unreferenced-recipe-direction

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11506

check:platform-checklist resolved fixtures.provisioning.use in one direction only. This adds the other: every area-level recipe must be named by some item's use. A recipe nobody opts into is dead text a runner may still replay.

The card's load-bearing claim, verified first

The card rests on "the reason it was deferred has expired". It has — established before any code was written, not assumed.

The reason. While cross-area reuse had no spelling, a recipe whose only consumer lived in another area could be referenced only from that item's knownGaps prose, invisible to the gate. Flagging an unreferenced recipe then could not distinguish dead text from a recipe whose consumer could not say so, and reddening it would have settled the open cross-area convention by accident, toward "recipes are area-local".

What discharged it. The maintainer ruled option A on 2026-08-22 (#10593 gap 2), landed by #11508. Three independent confirmations in this tree, not recollection:

evidencewhere
the qualified spelling is implemented (parseUse, two-level lookup)scripts/check-platform-checklist.mjs
the worked instance carries it: "use": "search:qa-contributor-bound-member"areas/records-forms.json:23
the recipe's own note records the ruling and both machine-resolved consumersareas/search.json:52

So every legitimate consumer can now express itself as a use, from any area. A recipe with no use pointing at it is no longer possibly-referenced-from-prose. The gate's own comment and the README already said the reason had expired and pointed here; both are now rewritten to describe a direction that exists.

The population, and what that dictated

Four recipes, seven referencing items, zero unreferenced — and all seven referencing items are active:

attachments-storage:qa-scratch-authz 3 items
attachments-storage:qa-media-constraints 1 item
automation:qa-flow-status-doors 1 item
search:qa-contributor-bound-member 2 items (one cross-area)

The subject population is zero and stays zero on a healthy ledger. That did not argue for a self-test instead of a check — the invariant is real going forward, and the card names the trigger precisely: a recipe goes unreferenced when its last consumer is retired, which is an edit in a different area file. What zero dictated is that a check alone would be worthless: its output is permanently empty, so its green cannot distinguish "working" from "deleted". So this ships both — the direction, and a 19-assertion positive control that runs inline on every invocation (the pattern the trap-vocabulary and resolve controls beside it already use). Here the control is not a safeguard on top of the real subject; it is the only subject.

Is "unreferenced" crisply definable? Yes — with zero exceptions needed

Measured rather than assumed. Grepping all four recipe names across the whole repo returns only the gate, the README and four area files — no run records, no skills, no other surface. Three edges, each decided in the gate's comment rather than left to the reader:

  1. What counts as a reference — a resolved fixtures.provisioning.use, nothing else. Recipe names do appear in prose (automation.json and search.json both open with "Shape copied from qa-scratch-authz"), and that is provenance, not consumption. Counting prose would restore the invisible pointer the qualified spelling exists to retire.

  2. Retired items count. "Referenced" means referenced by any item. This is mechanical, not a convention call, and the forward direction forces it: provisioningProblems runs on every item regardless of status, so a retired item's use must still resolve, so its recipe must still exist. Under an active-only reading, a recipe whose last consumer retired would be flagged here, and deleting it to clear that flag would dangle the retired item's use and red the forward direction — two checks in one gate made mutually unsatisfiable, escapable only by editing a retired item, which the append-only lifecycle forbids. Pinned as R11.

  3. Suppressed while any use dangles. An unresolved reference means the consumer graph is incomplete, so "nobody references this recipe" is not yet worth saying — the intended consumer may be the broken reference. Without this, one typo prints twice: once against the item that has it, and once against the perfectly correct recipe it meant to name. Pinned as R12/R13, and proven on real data in ablation B below.

Exception-list size: zero. No waiver spelling is invented, deliberately — the population needing one is zero, and a waiver field guessed ahead of its first real case guesses what that case wants. The failure names the two remedies that exist today: give the recipe a consumer, or delete it. A genuine keep-it-anyway case is the moment to decide the spelling, on that case's evidence; #10885 (recipes carry no revision/history) may well answer it as a retired recipe rather than a waived one, and nothing here forecloses that.

Non-vacuity — both readings on the record

All ablations restore under trap … EXIT INT TERM, assert the anchor match count in-process (a zero-hit edit aborts loudly instead of reading as a clean ablation), prove the mutation landed by sha change, and prove the restore byte-identical.

A — does it fire? Injected one genuinely unreferenced recipe into areas/automation.json.

sha256 ce5f1eda… -> 71f39dfe… inject count 0 -> 1 (json still parses)
GATE_EXIT=1
✗ automation.json: area-level recipe "qa-ablation-orphan-recipe" is referenced by
no item — dead text. …
restore: sha256 71f39dfe… -> ce5f1eda… BYTE-IDENTICAL GATE_EXIT=0

B — Zone 1 guard: a typo must not accuse the correct recipe. Misspelt one real use (qa-media-constraints to qa-media-constraint).

GATE_EXIT=1, problem count = 1
✗ …field-accept-maxsize-server-enforced: "fixtures.provisioning.use" names
"qa-media-constraint" … did you mean `qa-media-constraints`?

Only the dangling reference is reported. qa-media-constraints — a correct recipe — was not accused. Suppression held on real data. Restore byte-identical.

C — cross-area accounting on real data, two stages.qa-contributor-bound-member has two consumers, one in another area.

  • C1 — repointed the cross-area consumer (records-forms.crud-roundtrip) elsewhere: GATE_EXIT=0, still green, because search.rls-both-personas still references it.
  • C2 — repointed that one too: GATE_EXIT=1, reported against search.json, the area that owns the recipe, though the triggering edits were in other files. That is exactly the card's motivating scenario — a recipe going unreferenced by an edit in a different area file — reproduced end to end.

All three files restored byte-identical; gate green again.

The existing direction is unchanged — by its verdict line, not by inspection

Baseline captured on this same tree at e47d5ef61 before any edit, compared field by field against the branch:

fieldbaselinebranch
areas / items / active15 / 207 / 20715 / 207 / 207unchanged
coverage kinds mapped / waived31 / 031 / 0unchanged
traps documented / in use19 / 1919 / 19unchanged
area recipes44unchanged
item references resolved77unchanged
area-qualified refs11unchanged
self-check: trap-vocabulary2222unchanged
self-check: provisioning-resolve3434unchanged
self-check: unreferenced-recipeabsent19added
recipes referencedabsent4/4added

Every pre-existing figure is untouched; the new figures are additions only. The 4/4 is counted from the reference map rather than restated from recipeTotal — a line that restates a constant reports nothing, and this direction's whole risk is a green that looks the same whether it ran or not.

Verification

Union re-run at final head ee7ade76d, all through scripts/pm/os-verify-lock.sh. Exit codes captured by redirect-then-capture, never through a pipe.

EXIT=0 pnpm check:nul-bytes
EXIT=0 pnpm check:platform-checklist
EXIT=0 pnpm check:agent-test-spelling
EXIT=0 pnpm check:cross-package-test-inputs
EXIT=0 pnpm check:doc-authoring
EXIT=0 pnpm --filter @objectstack/lint run check:doc-formula-expressions
EXIT=0 pnpm check:entry-guard
EXIT=0 pnpm check:parse-guard
EXIT=0 pnpm check:pnpm-filter-targets
EXIT=0 node scripts/check-cross-package-test-inputs.mjs

Gate family re-derived from the committed diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (it reads its own change set; no hand-built path list) — same 8 families, no additions. check:doc-formula-expressions first reported PREREQUISITE NOT MET for @objectstack/formula, then for @objectstack/lint; both built as instructed and re-run green. Per #11824 that message is a prerequisite, not a finding.

Repo-wide pnpm lint narrowed, declared as a narrowing with its three measurements: (1) population read from eslint's own config — isPathIgnored reports README.md ignored ("File ignored because no matching configuration was supplied") and the .mjs in scope, so one of the two files is eslint's whole population here; (2) file count from --format json — 2 entries, 0 errors; (3) config invariance — eslint.config.mjs states in its own words that this repo "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", measured there with a positive control, so this diff cannot move the verdict on any untouched file. CI runs the full farm regardless.

Scope notes


Generated by Claude Code

`check:platform-checklist` resolved `fixtures.provisioning.use` forwards only.
The reverse direction — a recipe nobody opts into — was deliberately deferred
while cross-area reuse had no spelling: flagging it then would have settled that
open convention by accident, in the direction of "recipes are area-local".
The maintainer's option-A ruling of 2026-08-22 (#10593 gap 2, landed in #11508)
discharged that reason. Every legitimate consumer can now express itself as a
`use` from any area, so a recipe with no `use` pointing at it is unambiguously
dead text rather than possibly-referenced-from-prose.
Three definitional edges are decided rather than left to the reader:
- a reference from a `retired` item still counts. The forward direction runs on
every item regardless of status, so a retired item's `use` must resolve and its
recipe must exist; an active-only reading would make the two directions
mutually unsatisfiable the moment a recipe's last consumer retired;
- the direction is suppressed while any `use` dangles, so one typo cannot also
accuse the correct recipe it was aiming at;
- no waiver spelling is invented — the population needing one is zero, and the
failure names the two remedies that exist (give it a consumer, or delete it).
All four recipes on the ledger are referenced by seven items, so this direction's
subject population is zero and stays that way on a healthy ledger. That makes its
green uninformative on its own, so the 19-assertion positive control runs inline
on every invocation, like the trap-vocabulary and resolve controls beside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Aug 24, 2026
@os-steveClaude

Copy link
Copy Markdown
Collaborator

ACCEPT. Verified by content on origin/main.

⭐ You verified the premise first, and it holds

I made the expired-deferral check the lead requirement because "a deferral whose stated cause is gone is a different thing from a deferral someone forgot about." You established the cause, and its discharge is in the history:

d10e21462 feat(qa-checklist): resolve an area-qualified `provisioning.use` (#11508)

Plus the three in-tree confirmations, all of which I reproduced: parseUse present in the gate, records-forms.json:23 carrying "use": "search:qa-contributor-bound-member" (the cross-area worked instance), and search.json:52 recording "maintainer ruling 2026-08-22 — option A". Not recollection — the tree.

⭐ And you found a second expired warning the card carried: it said the gate was red on main for #11421, which closed via PR #11524. You recorded the gate green on your base before any edit. A card that has gone stale in two places is worth knowing about as a card, not just as a task.

The zero is the interesting part, and you drew the right conclusion from it

Population: 4 recipes / 7 references / 0 unreferenced. A lesser answer would have shipped the check and called the green a pass. Yours:

because the subject population is zero and stays zero on a healthy ledger, a check alone would have a permanently empty output whose green cannot distinguish 'working' from 'deleted'

So you shipped the direction and a 19-assertion inline positive control, matching the file's existing 22 trap-vocabulary and 34 provisioning-resolve controls. That is the correct response to an empty population, and it is the difference between a gate and a decoration.

The three definitional edges

The retired-item one is the best reasoning in the report. You did not decide it by preference; you showed an active-only reading makes the two directions mutually unsatisfiable the moment a recipe's last consumer retires — escapable only by editing a retired item, which the append-only lifecycle forbids. A definition that can be forced by an impossibility is settled, not chosen.

And the suppression edge is the Zone 1 guard I set, met by construction: while any use dangles, the direction is suppressed, so one typo cannot also accuse the correct recipe it was aiming at.

Ablations

B is the one I asked for: misspelling a real use produced exactly one problem — the dangling reference — and the correct recipe was not accused. Suppression held on real data, not on a fixture.

C reproduced the card's motivating scenario end to end: repointing consumers one at a time, green while one remained, then red reported against the area that owns the recipe though the triggering edits were in different files. That is cross-area accounting demonstrated, not asserted.

Existing direction proven unchanged by the gate's own verdict line field-by-field, with both pre-existing self-check counts intact and only additions.

⭐ Two self-corrections, both worth more than they cost

my first attempt to compare directions ran origin/main's gate from a copied file in the scratchpad; it died with ERR_MODULE_NOT_FOUND … and my parser would have reported the pre-existing fields as "CHANGED"

That is #11824's defect biting inside a verification harness — a broken run misparsed into a false finding — caught by reading the real output rather than your own summary table. Two hours after that fix landed, here is the exact failure it exists to prevent, in the wild.

And the ablation harness's shell-quoting bug in an echo banner: it touched no mutation, and you knew that because every mutation is proven independently by an in-process 1-match assertion and a sha256 change. You recorded it anyway because a silent no-op from that class was the trap flagged at dispatch. Reporting a harness defect that provably changed nothing is how the readings stay trustworthy.

⛔ Zone 1 held

lint.yml untouched — the PR is two files. I checked the wiring myself and found platform-checklist in lint.yml three times, all comment lines recording the maintainer decision; the control (run: pnpm check:doc-anchors) shows what real wiring looks like and there is no run: for this gate. #11730 stays the maintainer's.

(My raw count of 3 did not distinguish prose from wiring — a reminder that a grep count is not a reading until you look at what it counted.)

Ruling on your open question: A, ship with no waiver.

Your reasoning is complete and I am confirming rather than merely accepting it. The decisive point is the one about reviewability: a waiver with no real case has no standard to judge it against, so the first use of it would set the standard by accident — which is exactly how this card's original deferral happened. And a waiver field is the surface an author reaches for to silence a gate, so adding one before a case exists makes the gate weaker for no measured benefit.

B is a guess at what the first real case will want, written before that case exists to constrain it. C leaves the gate's own comment documenting an expired deferral indefinitely — the shape that created this card. Nothing here forecloses #10885 relocating the concept later.

Flipping to ready. Arming once every check is green.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude