fix(metadata-protocol): the author-time gate's 422 stops opening with a bracketed restatement of its own code - #19830
Conversation
… a bracketed restatement of its own code The runtime authoring gate's refusal opened with `[invalid_metadata]` in front of the `INVALID_METADATA` / 422 the same throw declares. The message now opens with the sentence; `code`, `status`, `issues` and `rulesRun` are unchanged, and the `[rule]` locators inside the headline stay. The absence pin widens to this third producer, with a per-file refusal floor and the gate's advisory-log `[rule]` locator declared by name. Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
…tem quotes; changeset The studio-authoring item quoted a bracketed `[invalid_metadata]` opener and a `: Required` tail that saveMetaItem's spec-validation refusal no longer emits. The clause now quotes the measured headline, the dispatcher source pointer says what that test pins, and the producer's per-face renderer is cited beside it. Revision 2 -> 3 with its history entry. Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a859fc789e3f52a941611f1093b40f08bc365bc0 && git checkout a859fc789e3f52a941611f1093b40f08bc365bc0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1cacfe4a425dc0c2cdd4eb5d0088d5b3f5a16c92 58b526dbdd7124e2161e7a379f13a3780385f05b && git checkout -B drift-repro 1cacfe4a425dc0c2cdd4eb5d0088d5b3f5a16c92 && git merge --no-ff 58b526dbdd7124e2161e7a379f13a3780385f05b
node scripts/docs-audit/affected-docs.mjs --json 1cacfe4a425dc0c2cdd4eb5d0088d5b3f5a16c92 |
Contract reviewServed-tier: Rendered by an isolated at-tier reviewer subagent that was fed the card, its rulings and this PR only, and adopted by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19709
Clause-②: no
The runtime authoring gate's
422 INVALID_METADATArefusal no longer opens its message with[invalid_metadata], a bracketed restatement of thecodethe same throw declares. The 2026-08-29 maintainer ruling defines one envelope semantics: «erroris HUMAN LANGUAGE,codeis the MACHINE TOKEN, and a prefix is removed because the same fact already rides thecodeaxis». This is limb ① of the card only. Thepackages/restandpackages/runtimelimbs belong to another lane and are untouched here.5 files, head
58b526dbd. Precedent followed:7a25a3ee9c(PR #19683).What changed
packages/metadata-protocol/src/runtime-authoring-gate.ts: the thrown message now opens with the sentence:flow/leave_approval failed author-time validation: 1 issue — flows[0].nodes[1].config.approvers[0].value [approval-expression-invalid].code,status,issuesandrulesRunare byte-identical. The[rule]locators inside the headline stay: they say which rule produced each finding, and no other field on the message carries that.protocol.bracketed-refusal-opener-absence.test.ts(the existing pin):PRODUCERSwidens to this file. There is no second pin. Two structural changes were needed to widen it, and each was measured before it was written:new Error(per producer file. This producer constructs exactly one, so a naive widening reds for being small rather than for being wrong.PRODUCERSis now a file-to-floor map (protocol.ts5,sys-metadata-repository.ts5,runtime-authoring-gate.ts1). The whole-family floor of 30 is unchanged.advisory.rule. Line 715 of the producer is the advisory log line's[${advisory.rule}]locator, a continuation literal after the[Protocol]prefix. It belongs to the[rule]-locator vocabulary the card says to leave alone. The line-based scan cannot tell a mid-line continuation from an opener, so the exemption is spelled out rather than inferred, and it is keyed on the exact expression, never on "anything interpolated".evaluateRuntimeAuthoringGatewith the gate-local schedule-org refusal assertsINVALID_METADATA, 422, no opener, the opening sentence, and the[rule]locator.protocol.runtime-authoring-gate.test.ts:214: flipped (A3 class i, below).docs/qa/platform-checklist/areas/studio-authoring.json: two lines corrected (A2, below), revision 2 to 3..changeset/19709-authoring-gate-bracketed-opener.md:patch,@objectstack/metadata-protocol.Measured, not assumed
A1: the opener census for this producer
On
c11852406the file has onenew Error(and one bracketed opener::774,[invalid_metadata], besidecode = 'INVALID_METADATA'/status = 422. That count comes from running the pin's own scanner over the file, not from a grep. The otherinvalid_metadatamention (:40) is doc prose about the envelope. The remaining bracketed literals are not this family::714and:759are[Protocol]logger prefixes, and:715is the[rule]locator above. Reachability is not re-derived:withoutDeclaredCodePrefixstrips only theCODE:spelling, as the precedent measured.A2: which producer the two checklist lines describe
The PM's reading holds and triage's does not. Both lines describe
saveMetaItem's Zod spec-validation refusal inprotocol.ts(failed spec validation). Neither describes this card's producer (failed author-time validation). The item saves with?mode=draft, and the author-time gate never judges a draft.:477clause: false today on two counts. That producer lost its bracketed opener in7a25a3ee9c. On the REST and dispatcher doors its message is also apath [zod code]headline, with the per-issue prose carried onissues[]. Measured onc11852406with a temporary probe (removed, never committed), for a field missing its type:object/qa_invalid_probe failed spec validation: 1 issue — fields.amount.type [invalid_value],INVALID_METADATA, 422. The wordRequiredappears nowhere in it; Zod 4 reportsInvalid option: …. The clause now quotes the headline, with the placeholders written the way the file already writes them.:514source pointer. It citedpackages/runtime/src/http-dispatcher.test.ts#errorand quoted that test's hand-built mock message as the producer's shape. The mock stays untouched (class ii). The pointer now says what the test actually pins: the 422,INVALID_METADATA, anddetails.issuesthreading.packages/metadata-protocol/src/protocol.ts#specValidationFindingsis added beside it. The gate's anchor sweep resolves the new anchor: 619/637 before, 620/638 after.A3: every test outside the producer that carries the removed bytes
Scanned across the whole tree, not by following CI:
git grepfor[invalid_metadata], for lowercaseinvalid_metadata, forfailed author-time, and for opener-shaped message assertions.(i) Asserts on this producer's real output
packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts:214rejects.toThrow(/invalid_metadata/), green only because the tag spelled the token. Updated. It now assertscode+statusplus the opening sentence and the rule locator, so it cannot pass on an empty or generic message.packages/objectql/src/publish-package-drafts-response-conformance.test.ts:590-615publishPackageDraftsanddist. It asserts only retained bytes (failed author-time validation,[approval-expression-invalid],: 1 issue —) and keys onf.code === 'INVALID_METADATA'. Not edited. Re-run after rebuilding: 15/15.(ii) Builds its own string: a mock or fixture, left untouched
packages/plugins/plugin-security/src/packaged-permission-set-lock.test.ts:216saveMetaItemimitating the spec-validation producerpackages/plugins/plugin-security/src/packaged-permission-set-restore-leg.test.ts:220packages/plugins/plugin-security/src/permission-set-duplicate-name-refusal.test.ts:151packages/plugins/plugin-security/src/permission-set-projection.test.ts:163packages/rest/src/rest-4xx-message-truncation.test.ts:297,319packages/runtime/src/domains/meta-put-falsy-body.test.ts:67packages/runtime/src/http-dispatcher.test.ts:255saveMetaItemrejectionpackages/spec/src/api/protocol.test.ts:1254,1478,1559:1478imitates this producer's old text, and the schema accepts any stringpackages/runtime/src/domains/packages-seed-apply-read-decorations.test.ts:406not.toContain('invalid_metadata')), unaffectedA4: log sites that print only the message
Nothing to restore at the producer. The producer's own two log lines never interpolate its
err.message. One is the advisory warn, which is not about the refusal. The other is the migration-hatch warn, which composes its text from the issues and never carried the tag.The refusal does reach generic consumer sites that print any error's
e.messageand nothing else:codeelsewhere?protocol.ts,publishPackageDraftsrollbackconsole.warnfailed[].code); the log line does notprotocol.ts,duplicatePackagecopy-failureconsole.warnandfailed[].errorDuplicatePackageResponseSchema.failed[]has nocodefieldpackages/runtime/src/domains/packages.ts, visibility-fliplogger.error(Cause: e.message)plugin-securitypermission-set projection, untyped-hostwarnfallbacklogger.errorAfter this PR those lines keep the fixed sentence
failed author-time validationand the[rule]locators, but not theINVALID_METADATAtoken. All four have carried the precedent family's refusals without a token since7a25a3ee9c; this PR adds one more code to that set. None is in the producer.The bounded in-place exemption does not hold, on its "no other claim holds the file" condition:
protocol.tsis reserved to a concurrent card this round, andpackages.tsis limb ③'s lane. The precedent's remedy would be one line per site: print the declared code,(code=…), at the log site. It is reported to the seat rather than done here. ⛔ The opener was not re-added.Reverse verifications
Each was committed first, mutated through
scripts/ablation-replace.mjs(the anchor must hit, and blob counts are checked on disk), then restored with proof: blob equals the HEAD blob andgit diff HEADis empty. No ablation needed a rebuild, because every test involved resolves the producer through a relativesrcimport.:782(blobc3698c313409to669df99b04ed): red, 3 failures. The pin namedruntime-authoring-gate.ts:782, its behavioural case failed, and the door test at:214failed. Restored toc3698c313409.[${args.type}]at the same anchor: red, 2 failures. The capture-group change kept the interpolated arm alive, and the exemption is narrow. Restored.advisory.ruleexemption emptied in the pin (blobfc4a3d904d03toac342c7f8dcc): red, 1 failure, namingruntime-authoring-gate.ts:715. The exemption is load-bearing. Restored.Tests
pnpm --filter @objectstack/metadata-protocol test: 187 files / 2660 tests pass; 3 files and 19 tests skipped (pre-existing).typecheckexit 0.tsc --listFilesholds 190 test files, including both edited ones.packages/objectqlpublish-package-drafts-response-conformance.test.ts: 15/15, run after rebuilding this package.dist/index.jscarries the new sentence (positive control: 1). The old literal reads 0 in bothdist/index.jsanddist/index.cjs.58b526dbd.eslint.config.mjsenables no type-aware linting: noparserOptions.project, and its rules are per-file. The only file inputs it reads are two baseline JSONs this diff does not touch, so no untouched file's verdict can move. The three touched.tsfiles were linted with--no-inline-config --format json: 3 files, 0 errors, 0 warnings. The.jsonand.mdfiles match nofilesglob.Gates
node scripts/pm/dispatch-gates.mjs --commands, run on58b526dbd, derived 62 commands. All 62 ran, plus the 4 roster families it flagged as sharing a directory with these paths (check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity). All 66 exit 0. The PM-named families are among them:check:platform-checklist,check:dispatcher-error-vocabularyandcheck:engine-double-contract.check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debtfirst answeredPREREQUISITE NOT MET(exit 3, explicitly not a pass). The whole-workspace build they ask for was paid (73/73 tasks), and all three then read 0.--ranreconciliation: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero because every line carries its exit code.Changeset
patchon@objectstack/metadata-protocolonly, following the claim'sClause-②: no: every accept/reject verdict is byte-identical and no export moved. The package shipsdist, and the message bytes are there, so the change does publish.Clause-②: yes/minorfor the same shape of change. This PR follows the claim's reasoning, recorded on the card, and does not re-decide it.Acceptance notes
docs/adr/0005-metadata-customization-overlay.md:348shows an example payload with the retired opener on the spec-validation producer. It is an ADR (a governed surface) recording a decision at its date, so it was not touched. Carrier: none.packages/metadata-protocol/src/protocol-publish-drafts-closure.test.ts:15quotes a dated measurement (2026-08-21) of the rollback log line, including the old opener. It is a historical record, left as is. The same goes for the narrative comment atplugin-security/src/permission-set-projection.ts:361and theCHANGELOG.mdentries, which are release-owned.7a25a3ee9ctext. They are drift, not consumers, and are left as the dispatch instructed. Carrier: none.runtime-authoring-gate.ts:40says "invalid_metadata-shaped envelope". That is a lowercase spelling of the code in doc prose, not an opener. Left.origin/mainmoved one commit since the base (c1dfa5241b, a spec fix plus its changeset). It overlaps none of these paths and was not merged. CI builds the merge ref.Generated by Claude Code