Skip to content

fix(metadata-protocol): the author-time gate's 422 stops opening with a bracketed restatement of its own code - #19830

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19709-authoring-gate-bracketed-opener
Sep 23, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19709-authoring-gate-bracketed-opener

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #19709

Clause-②: no

The runtime authoring gate's 422 INVALID_METADATA refusal no longer opens its message with [invalid_metadata], a bracketed restatement of the code the same throw declares. The 2026-08-29 maintainer ruling defines one envelope semantics: «error is HUMAN LANGUAGE, code is the MACHINE TOKEN, and a prefix is removed because the same fact already rides the code axis». This is limb ① of the card only. The packages/rest and packages/runtime limbs 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, issues and rulesRun are 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): PRODUCERS widens to this file. There is no second pin. Two structural changes were needed to widen it, and each was measured before it was written:
    • Per-file refusal floor. The pin required at least 5 new Error( per producer file. This producer constructs exactly one, so a naive widening reds for being small rather than for being wrong. PRODUCERS is now a file-to-floor map (protocol.ts 5, sys-metadata-repository.ts 5, runtime-authoring-gate.ts 1). The whole-family floor of 30 is unchanged.
    • The interpolated arm now captures its expression, and one expression is declared by name: 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".
    • A behavioural case is added for this producer: evaluateRuntimeAuthoringGate with the gate-local schedule-org refusal asserts INVALID_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 c11852406 the file has one new Error( and one bracketed opener: :774, [invalid_metadata], beside code = 'INVALID_METADATA' / status = 422. That count comes from running the pin's own scanner over the file, not from a grep. The other invalid_metadata mention (:40) is doc prose about the envelope. The remaining bracketed literals are not this family: :714 and :759 are [Protocol] logger prefixes, and :715 is the [rule] locator above. Reachability is not re-derived: withoutDeclaredCodePrefix strips only the CODE: 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 in protocol.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.

  • :477 clause: false today on two counts. That producer lost its bracketed opener in 7a25a3ee9c. On the REST and dispatcher doors its message is also a path [zod code] headline, with the per-issue prose carried on issues[]. Measured on c11852406 with 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 word Required appears nowhere in it; Zod 4 reports Invalid option: …. The clause now quotes the headline, with the placeholders written the way the file already writes them.
  • :514 source pointer. It cited packages/runtime/src/http-dispatcher.test.ts#error and 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, and details.issues threading. packages/metadata-protocol/src/protocol.ts#specValidationFindings is 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 grep for [invalid_metadata], for lowercase invalid_metadata, for failed author-time, and for opener-shaped message assertions.

(i) Asserts on this producer's real output

file disposition
packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts:214 rejects.toThrow(/invalid_metadata/), green only because the tag spelled the token. Updated. It now asserts code + status plus 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-615 Reads this producer's real output through publishPackageDrafts and dist. It asserts only retained bytes (failed author-time validation, [approval-expression-invalid], : 1 issue — ) and keys on f.code === 'INVALID_METADATA'. Not edited. Re-run after rebuilding: 15/15.

(ii) Builds its own string: a mock or fixture, left untouched

file what it is
packages/plugins/plugin-security/src/packaged-permission-set-lock.test.ts:216 a mock saveMetaItem imitating the spec-validation producer
packages/plugins/plugin-security/src/packaged-permission-set-restore-leg.test.ts:220 same
packages/plugins/plugin-security/src/permission-set-duplicate-name-refusal.test.ts:151 same
packages/plugins/plugin-security/src/permission-set-projection.test.ts:163 same
packages/rest/src/rest-4xx-message-truncation.test.ts:297,319 constructs its own error and asserts the REST layer passes it through
packages/runtime/src/domains/meta-put-falsy-body.test.ts:67 constructs its own error
packages/runtime/src/http-dispatcher.test.ts:255 a mock saveMetaItem rejection
packages/spec/src/api/protocol.test.ts:1254,1478,1559 response-schema parse fixtures; :1478 imitates this producer's old text, and the schema accepts any string
packages/runtime/src/domains/packages-seed-apply-read-decorations.test.ts:406 an absence assertion (not.toContain('invalid_metadata')), unaffected

A4: 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.message and nothing else:

site face carries code elsewhere?
protocol.ts, publishPackageDrafts rollback console.warn operator log the response row does (failed[].code); the log line does not
protocol.ts, duplicatePackage copy-failure console.warn and failed[].error log + response data no: DuplicatePackageResponseSchema.failed[] has no code field
packages/runtime/src/domains/packages.ts, visibility-flip logger.error (Cause: e.message) log no
plugin-security permission-set projection, untyped-host warn fallback log the primary path hands the Error object to logger.error

After this PR those lines keep the fixed sentence failed author-time validation and the [rule] locators, but not the INVALID_METADATA token. All four have carried the precedent family's refusals without a token since 7a25a3ee9c; 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.ts is reserved to a concurrent card this round, and packages.ts is 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 and git diff HEAD is empty. No ablation needed a rebuild, because every test involved resolves the producer through a relative src import.

  1. Opener re-inserted at :782 (blob c3698c313409 to 669df99b04ed): red, 3 failures. The pin named runtime-authoring-gate.ts:782, its behavioural case failed, and the door test at :214 failed. Restored to c3698c313409.
  2. Interpolated opener [${args.type}] at the same anchor: red, 2 failures. The capture-group change kept the interpolated arm alive, and the exemption is narrow. Restored.
  3. advisory.rule exemption emptied in the pin (blob fc4a3d904d03 to ac342c7f8dcc): red, 1 failure, naming runtime-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). typecheck exit 0. tsc --listFiles holds 190 test files, including both edited ones.
  • packages/objectql publish-package-drafts-response-conformance.test.ts: 15/15, run after rebuilding this package. dist/index.js carries the new sentence (positive control: 1). The old literal reads 0 in both dist/index.js and dist/index.cjs.
  • Lint, narrowed and proven at 58b526dbd. eslint.config.mjs enables no type-aware linting: no parserOptions.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 .ts files were linted with --no-inline-config --format json: 3 files, 0 errors, 0 warnings. The .json and .md files match no files glob.

Gates

node scripts/pm/dispatch-gates.mjs --commands, run on 58b526dbd, 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-vocabulary and check:engine-double-contract.

check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first answered PREREQUISITE 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.

--ran reconciliation: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero because every line carries its exit code.

Changeset

patch on @objectstack/metadata-protocol only, following the claim's Clause-②: no: every accept/reject verdict is byte-identical and no export moved. The package ships dist, and the message bytes are there, so the change does publish. ⚠️ For the contract reviewer: the precedent declared Clause-②: yes / minor for 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:348 shows 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:15 quotes 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 at plugin-security/src/permission-set-projection.ts:361 and the CHANGELOG.md entries, which are release-owned.
  • The class (ii) mocks imitate the spec-validation producer's pre-7a25a3ee9c text. They are drift, not consumers, and are left as the dispatch instructed. Carrier: none.
  • runtime-authoring-gate.ts:40 says "invalid_metadata-shaped envelope". That is a lowercase spelling of the code in doc prose, not an opener. Left.
  • origin/main moved 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

… 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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1cacfe4a425dc0c2cdd4eb5d0088d5b3f5a16c92packageMentionDocs.

Which tree this was computed on

This run read content/docs from a859fc789e3f52a941611f1093b40f08bc365bc0 — the merge of head 58b526dbdd7124e2161e7a379f13a3780385f05b into base 1cacfe4a425dc0c2cdd4eb5d0088d5b3f5a16c92, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 58b526dbdd7124e2161e7a379f13a3780385f05b

Rendered by an isolated at-tier reviewer subagent that was fed the card, its rulings and this PR only, and adopted by the domain:engine#1 seat. PR head unchanged while reviewed; merge base c118524061a902880c6dbf9cb68c2df97c1a7694; five files, +132/-16.

① Derived judgments

  • Message bytes of the author-time 422 (runtime-authoring-gate.ts evaluateRuntimeAuthoringGate): the leading [invalid_metadata] is dropped, and the message opens TYPE/NAME failed author-time validation: HEADLINEright under the ruling. code = 'INVALID_METADATA' is assigned on the same throw, so the opener restated a fact already riding the code axis. Reachability was re-verified rather than inherited: packages/rest/src/error-response.ts withoutDeclaredCodePrefix strips only when the message starts with error.code followed by a colon, so the lowercase bracketed spelling reached every caller's error.message.
  • Accept/reject verdictsbyte-identical. The producer hunk is one template-literal edit plus a comment. The early return, the hatch branch and the code / status / issues / rulesRun assignments sit outside the hunk.
  • Public surfaceunchanged. No export is added, removed or renamed. The pin's new imports (evaluateRuntimeAuthoringGate, PLATFORM_SCHEDULE_CREATE_RECORD_ORG_MISSING) are already exported and are reached by relative path. Only one dist string moves.
  • Vocabularies that are NOT this familyintact: the PATH [RULE] headline, the advisory log's [${advisory.rule}] locator, the [Protocol] logger prefixes, and the path [zod code] vocabulary in protocol.ts metadataIssueHeadline, which the diff never touches.
  • The widened pinreally fails on a re-introduced opener in this file. PRODUCERS becomes a file-to-floor map (runtime-authoring-gate.ts 1, equal to its total new Error( count). The interpolated arm only adds a capture: the match set is identical. NON_REFUSAL_LOCATORS exempts advisory.rule by exact expression text. Replicated statically on base and head: a literal opener at the throw is flagged, [${args.type}] is flagged, and [INVALID_METADATA] is flagged. Limits judged not defects: an opener spelled [${advisory.rule}] at the throw is out of scope there, and it would be a rule locator, not a restatement. A double-quoted literal is covered for this file by the two behavioural cases asserting ^flow/… failed author-time validation:.
  • Tests assert the envelopeyes. protocol.runtime-authoring-gate.test.ts replaces rejects.toThrow(/invalid_metadata/), which was green only through the tag, with status === 422, code === 'INVALID_METADATA', the opening-sentence match and the rule locator. The new pin case provokes a real gate-local refusal (findPlatformScheduleOrgGaps), so it cannot pass vacuously.
  • Blast radius re-derived over the head tree: the only assertion on this producer's real output outside the package is packages/objectql/src/publish-package-drafts-response-conformance.test.ts, which keys on code. Every other hit is a hand-built mock or a schema-parse fixture. No content/docs/** page quotes this message.
  • Checks on this head: 32 runs, all completed, every one success except three path/opt-in skips (Build Docs, Console Pin Gate, Packed-tarball smoke). Every required context and Check Changeset is success.

② Semver level

patch on @objectstack/metadata-protocol with Clause-②: noright for this diff. The PR body's line agrees with the changeset and with the claim (5791778305). By references/lanes/spec.md, a verdict that is byte-identical is the auxiliary face, and an auxiliary-face changeset is patch. Clause-② asks whether the accept set or the public surface widens, and nothing widens here. The precedent 7a25a3ee9c declared minor / yes on two grounds, neither present here: a when-unsure self-declaration, and a real contract-face edit (a published packages/spec .describe() plus a regenerated reference page). An over-declared yes was permitted there, not mandated; a false no is the auditable failure, and this no is true by the criterion.

③ Boundary flags

Implemented-by: claude/issue-19709-authoring-gate-bracketed-opener
Reviewed-by: session_01TEhopqrWQYBycZzyJHpAZr

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 23, 2026 09:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 71ef221 Sep 23, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19709-authoring-gate-bracketed-opener branch September 23, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] Three more producers still write bracketed openers that restate their own declared code — the #16245 family beyond its two named files

1 participant