Skip to content

feat(devx,qa): refuse a folded /meta plural spelling in any checklist call string - #13248

Queued
os-elon wants to merge 1 commit into
mainfrom
claude/issue-13010-checklist-plural-call-guard
Queued

feat(devx,qa): refuse a folded /meta plural spelling in any checklist call string#13248
os-elon wants to merge 1 commit into
mainfrom
claude/issue-13010-checklist-plural-call-guard

Conversation

@os-elon

@os-elon os-elon commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Fixes #13010

A QA checklist call is not prose. RUNNER.md has an operator replay it against a live boot, so it is the one field in that ledger that is an executable instruction — and areas/records-forms.json told its operator to send PUT /api/v1/meta/objects/qa_nofeeds, a plural item write on the metadata write door. The /meta type segment is always singular; that step answers 200 only because the boundary folds the spelling.

The point is not the site — the population GREW after it was measured

The card's argument, re-measured on this branch's base 5a9b7a0b10:

#11042's four sites, measured at 2866d5f97e all in areas/attachments-storage.jsonsince repaired by 9598f0ee7a (PR #13011), so they no longer appear in the population
areas/records-forms.json at that same ref grep -c 'meta/objects'0
the same file four days later, 5737222b89 (PR #12382) 1 — authored, reviewed and merged with nothing in the tree able to see it
current population, measured here, not recalled 1 — this fifth site, and no sixth

So this is not a site a sweep missed; it is a site that did not exist yet. The class is still being newly introduced, one honest PR at a time, which makes the deliverable what stops the next one rather than are there more. A sweep — however complete on the day it runs — is by construction the thing that already failed here.

What lands

A refusal in scripts/check-platform-checklist.mjs. Every call string in every area document, at any depth, is scanned for a /meta or /metadata path segment, and the segment is refused when it is a key of META_URL_TO_SINGULAR.

The vocabulary is read from the contract, never copied. META_URL_TO_SINGULAR lives in packages/spec/src/meta-spelling/meta-url-data.generated.ts — derived by build-meta-url-spelling.ts from PLURAL_TO_SINGULAR and DEFAULT_METADATA_TYPE_REGISTRY, and re-derived from those live sources on every CI lap by check:meta-url-spelling (lint.yml, "Check the meta-url-spelling data module is current and spellings agree" — green on this tree: 34 spellings, 27 registry-declared types; manifest/derived agreement holds). Both halves confirmed: it is the right closed set and it is gate-held, so this guard cannot drift from the contract it enforces. packages/spec is read only — nothing there is edited.

Keying on the closed set rather than a trailing-s heuristic matters in both directions: it catches the camelCase folds a heuristic would miss (sharingRules, analyticsCubes, ragPipelines) and it leaves alone anything that merely looks plural.

Read as source text, not imported, because this gate is a zero-dependency node script on an unbuilt tree (README "Operating cadence"). Same refusal discipline as the trap-vocabulary extractor beside it: a literal it cannot parse — missing declaration, no object literal, unclosed, zero pairs, a partial parse under the floor, or a map that has lost the objects anchor — is a hard failure, never an empty vocabulary that would validate every call against nothing and print a green identical to a working parse.

The fifth site is repaired in the same commit, so the ratchet lands at a zero baseline with no grandfather record. Nothing is parked, so there is nowhere for the next one to hide. The shape of the repair is not invented here: 9598f0ee7a made exactly this edit on the four sibling sites.

Two fences, both load-bearing

The fold is not narrowed. /meta/objects/:name and /meta/object/:name answer identically today and must keep doing so. Nothing in this PR reaches the wire, the router, or any accept or reject decision — the refusal is about the repo not instructing a spelling it merely tolerates. The sibling gate check-doc-route-spelling.mjs carries the same fence in its own header.

call only. The neighbouring fields legitimately contain plural spellings, because their job is to narrate the fold or the defect: a why explaining why the plural door was a hole, an expect describing what the old spelling answered, a source citing a registration that was retired, and attachments-storage.json's requires prose correctly naming the parameterized PUT /api/v1/meta/:type/:name — a segment-shaped literal, not a plural. That is also why this could not be a root expansion of check-doc-route-spelling.mjs instead: that gate scans a file's text against the route ledgers, and text-scanning these files would flag the narration along with the instruction. Field-addressed collection is what makes the ruling mechanical rather than a matter of care.

Depth rather than a curated path list, for the same reason the gate exists at all: every call on the ledger today sits in an area-level fixtures.RECIPE.sequence[], but a path list that matches where they happen to live now can never match the one added tomorrow.

Verification

Union run at a7864a0161, the final commit on this branch.

Baseline (positive control that the gate runs at all), on the unmodified base:

check-platform-checklist: OK — 15 areas, 221 items (221 active); coverage: 31 kinds mapped, 0 waived; ...

Exit 0 with the plural site present — confirming the card's claim that nothing in the tree could see it. Grepping the checker's source for call returns only prose matches.

After, at a7864a0161:

check-platform-checklist: OK — 15 areas, 221 items (221 active); ... meta-URL spelling: 19 `call` strings scanned
against 34 folded spellings; (self-checks: 22 trap-vocabulary + 34 provisioning-resolve + 19 unreferenced-recipe
+ 53 meta-call-spelling assertions).

--self-test: 128 assertions, exit 0.

53 new assertions run on every invocation, not only behind --self-test. This gate is not CI-wired by maintainer decision (README "Operating cadence"), so a --self-test leg here would execute nowhere — the file's existing batteries are inline for exactly that reason and this one follows them. The battery pins the false-positive direction as hard as the firing direction, because a gate that reds a correct row costs more than the defect it catches.

Four ablations, each with its prediction named before the run, mutations proven on disk by decomposed grep counts plus git hash-object against the HEAD blob, restores proven by an empty git diff HEAD under an absolute-path trap. No rebuild leg applies: this gate is a plain .mjs that node executes from source, so nothing resolves through a package exports or dist that could go stale.

mutation predicted observed
A1 the repaired site back to plural exit 1, one finding naming sequence[1].call and objects exactly that
A2 a different file, a different fold (flow to flows) exit 1, one finding naming flows — the refusal is not hardcoded to the anchor exactly that
A3 a plural-looking segment that is not in the closed set (widgets) exit 0 — membership decides, not a trailing s exactly that
A4 the plural moved into a narration field (expect) of the real ledger exit 0 — the call-only ruling, on the real corpus exactly that

False positives, tested against the real corpus rather than fixtures only. The guard ran over all 15 live area files and their 19 call strings and flagged exactly one — the measured site. The corpus already exercises every shape worth worrying about: a query parameter (?package=com.objectstack.qa.feeds), the canonical singular (/meta/object/, /meta/flow/), parameterized prose (/meta/:type/:name), an address wrapped in prose, and calls that are not URLs at all. A3 and A4 add the two the corpus does not carry.

Derived gate familynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, exit 0, derived at a7864a0161; 14 families matched. All green, with two exceptions that are refusals rather than findings, each captured before any pipe:

  • check-test-completeness.mjs exits 3 with NOT MEASURED — it needs a saved turbo run test log. Not a red.
  • check-doc-formula-expressions first exited 1 with PREREQUISITE NOT MET. After building @objectstack/formula and @objectstack/lint it was really measured: exit 0.

pnpm lint (the full repo scan, eslint . --no-inline-config) exits 0 — run whole, so no narrowing is being declared. pnpm check:nul-bytes: OK (scanned 7342 text file(s) ... no raw ASCII control bytes).

Scope

#11042 is untouched here and stays open: its triage fences it to attachments-storage.json, this is a different file, and the deliverable is a guard rather than a site repair. No changeset — a QA ledger edit plus a CI-internal gate script releases nothing, which lint.yml calls the textbook skip-changeset case, and the 9598f0ee7a precedent carried none either.

One honest limitation, stated rather than buried. check:platform-checklist is deliberately out of per-PR CI by maintainer decision, so this refusal catches the next plural at the next manual run — the run the README already prescribes "whenever you touch the checklist", which is exactly the author who would introduce it — not at the moment it merges. Re-wiring that cadence is a maintainer decision and is not taken here.

Generated by Claude Code


Generated by Claude Code

…list `call` string

A QA checklist `call` is the one field in that ledger an operator REPLAYS, so
a step reading `PUT /api/v1/meta/objects/:name` instructs a plural item write
on the metadata write door. It answers 200 only because the boundary folds the
spelling; the `/meta` type segment is always singular.

The population is why this is a gate and not a sweep. The four sites measured
at `2866d5f97e` were all in `areas/attachments-storage.json` and have since
been repaired; `areas/records-forms.json` held ZERO occurrences at that ref and
gained one four days later in `5737222b89`, authored, reviewed and merged with
nothing in the tree able to see it. The class is still being newly introduced,
so the deliverable is what stops the next one.

`check-platform-checklist.mjs` now scans every `call` string, at any depth, and
refuses a `/meta` or `/metadata` path segment that is a key of
`META_URL_TO_SINGULAR` — the closed set of folded spellings generated in
`packages/spec/src/meta-spelling/meta-url-data.generated.ts` and re-derived from
its live sources on every CI lap by `check:meta-url-spelling`, so the guard
cannot drift from the contract it enforces. Read as source text, with the same
refusal discipline the trap-vocabulary extractor beside it already has: a
literal it cannot parse is a hard failure, never an empty vocabulary.

`call` ONLY. The neighbouring `why` / `expect` / `source` fields legitimately
quote plural spellings to narrate the fold, and `attachments-storage.json`'s
`requires` prose correctly names the parameterized `PUT /api/v1/meta/:type/:name`.
That is also why this could not be a root expansion of the sibling
`check-doc-route-spelling.mjs`: that gate scans a file's text, and text-scanning
these files would flag the narration along with the instruction.

The fold itself is untouched — no wire behaviour, no router, no accept/reject
decision changes. The refusal is about the repo not INSTRUCTING a spelling it
merely tolerates.

The fifth site is repaired in the same commit, so the ratchet lands at a zero
baseline with no grandfather record. Finding filed as #13010.

53 new assertions run on every invocation, not only behind `--self-test`: this
gate is not CI-wired by maintainer decision, so a `--self-test` leg here would
execute nowhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants