Skip to content

fix(metadata-protocol): refusal messages stop restating their own code in a bracketed opener - #19683

Merged
os-support-ai merged 14 commits into
mainfrom
claude/issue-16245-bracketed-tag-refusal-openers
Sep 22, 2026
Merged

os-support-ai merged 14 commits into
mainfrom
claude/issue-16245-bracketed-tag-refusal-openers

Conversation

@os-support-ai

@os-support-ai os-support-ai commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #16245

Clause-②: yes

Every refusal ObjectStackProtocolImplementation and SysMetadataRepository raised opened with a bracketed tag that restates the code the same throw declares. The 2026-08-29 maintainer ruling is 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. These met that condition by construction, so they are gone; code and status are untouched on every one of them.

27 files, 116 derived gate families, head 5e294644b2.

What was measured, not assumed

The inventory is 38, not the 30 in the card's title. Re-derived by pairing every throw site in the two named files with the code it declares, rather than by grepping literal tags:

reading count
literal [tag] message openers 37
of those, restating their own declared code 37 (mismatches: 0)
openers spelled by interpolation 1
total 38

The card measured 31 tagged sites / 30 restatements on 2026-09-06; the tree moved 16 days, and [unanswerable_target] — the one mismatch, which #16145 already removed — is correctly absent from this re-derivation. Firing controls on the same command and scope: a nonexistent tag returns 0, unanswerable_target returns only its pin and a comment, and the pattern finds the idiom in 20+ files repo-wide.

The 38th is the one no grep could find. assertOverlayAllowed's shared emitter opened its message with an interpolated [${code}], built from the same variable it assigns to err.code three lines down — the most redundant member of the family and invisible to every literal-tag probe, which is why it is absent from the card's inventory.

Reachability re-verified. withoutDeclaredCodePrefix returns the message unchanged unless message.startsWith(error.code) AND a colon separator follows. [no_draft] starts with neither, so it was never stripped and reached the caller in error.message. This is a published-output change, which is why Clause-② is yes.

Nothing consumed the tag — the open question triage left on the card. The only consumers found anywhere are strippers: @object-ui/react's extractWriteErrorMessage and two plugin-detail call sites each remove a leading bracketed prefix before showing the sentence to a user, beside the SCREAMING_SNAKE: strip. They independently confirm the tag was arriving, and they cannot break on its absence — the regex simply matches nothing.

Two bracketed vocabularies are deliberately kept, because neither restates a declared code: the path [zod code] locators inside a validation headline, and the [rule] locators the author-time gate composes. The [Protocol] and [SysMetadataRepository] logger prefixes are likewise untouched and declared by name in the new pin.

The published docs this change falsified are repaired here

Removing the openers made documented payloads wrong. Repairing what this round makes false is part of the round.

  • content/docs/api/error-catalog.mdx (:609, :660) — both documented INVALID_REQUEST payloads showed a message opening with the tag beside a code field already carrying the token. Hand-written page, edited directly.
  • packages/spec/src/api/protocol.zod.ts:815 — the promotion describe() said the lookup answers 404 [no_draft]; it now names NO_DRAFT, the axis that still carries it.
  • content/docs/references/api/protocol.mdx — AUTO-GENERATED from that source, so it was ⛔ never hand-edited: regenerated with check:generated --fix, which proved exactly 1 of 15 artifacts stale and rewrote only that one. The page moved exactly 1 line.

⛔ The three carriers in content/docs/releases/v17/ are deliberately left alone. Release notes record what shipped in the version they document; v17.1 and v17.2 did emit those tags, so those pages are historically accurate and are not a defect to repair.

The blast radius, closed

A message-text change breaks every pin asserting that text. Thirteen pins in packages/metadata-protocol, two in packages/runtime, six in packages/objectql, and two in packages/cli.

⚠️ The objectql and cli consumers surfaced on successive CI runs rather than together, because the shard stopped at the first failure and reported @objectstack/cli was scheduled but never reached. A green shard is only evidence about what it reached — the run that finally reported 11 of 11 scheduled package(s) reported, 0 never reached is the one that measured the whole set. The consumer scan is therefore repo-wide and not CI-led: the only remaining assertion on a removed opener anywhere in the tree is a packages/rest test that constructs its own error, which is fixture drift, not a consumer.

The six objectql assertions split into two families, and only one is this card's to retire:

  • Five pinned the opener itself. They move to the ENVELOPE (code + status), which is strictly stronger than what they had — two were bare rejects.toThrow(/regex/), which a plain Error would have satisfied. In each case the pre-change producer string carried the lowercase token ONLY inside the brackets, with zero occurrences elsewhere in the message.
  • One is not that. protocol-object-overlay-layer.test.ts asserts captured console.warn output, and a log line has no envelope beside it — the code axis a caller reads does not exist there, so removing the opener took the only machine-readable token with it and left nothing to fall back on. Genuine collateral. ⇒ Fixed at the producer's log site, not in the assertion: the boot warning now prints the declared code itself, as the sibling branch twelve lines above already does. ⛔ The opener was NOT restored at the producer — that would reintroduce the defect for every caller in order to serve one log site.

Three pins elsewhere were only ever green because the tag happened to spell the token; they now read err.code.

os meta delete — two surfaces, two different answers

The CLI carries this refusal on both a human and a machine surface, and the two assertions do not move the same way.

  • stdout is the human surface. printError prints the message and nothing else, so the token never belonged there. That assertion now reads the sentence, and the structural controls already in the test — exit code, the reset request's parentVersion, the if-match header — are what keep it from degrading into "something went wrong".
  • --format json is the machine surface, and it already carries the token. Measured rather than assumed, by dumping the real envelope through a temporary probe with a proven restore:
{ "success": false,
  "error": "view/json_probe has been modified since you loaded it. Expected parent sha256:… but current is sha256:….",
  "code": "METADATA_CONFLICT",
  "httpStatus": 409 }

So the assertion reads code and httpStatus, which a prose match over error only ever approximated. ⛔ No producer change was needed and the opener was not re-added — errorCodeFields has carried this since #13347. Had the envelope carried no code, that would have been a real regression on a documented machine-readable surface and the fix would have belonged at the producer, exactly as it did for the boot log line above.

Tests

  • pnpm --filter @objectstack/metadata-protocol test — 186 files / 2650 tests pass, 3 files and 19 tests skipped. typecheck exit 0, and --listFiles shows all 189 test files inside the tsc program, so the new pin is type-checked.
  • packages/objectql — the 5 files, 159 tests, exit 0; typecheck exit 0. packages/runtime and packages/rest targeted — exit 0.
  • pnpm lint — the whole repo, eslint . --no-inline-config, exit 0, zero findings. No narrowing was used.
  • All 113 derived gate families run, 0 NOT MEASURED, 0 UNRUN, every exit code recorded and none is 3 (dispatch-gates --ran reconciliation). check:dual-build-cjs-loads 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 and both then read 0.

Two reverse verifications, each with on-disk proof and a proven restore:

  1. One opener re-inserted at a named anchor (anchor 1 to 0, blob ce0ae6aafeb6 to 734afa23b233). The new absence pin went red, naming protocol.ts:8204. Restored; worktree blob equals the HEAD blob and git diff HEAD empty.
  2. The log-site fix ablated to prove it is load-bearing rather than cosmetic. Because objectql resolves this package through dist, the mutation was rebuilt and confirmed present in both dist/index.js and dist/index.cjs before any verdict was read — an unbuilt ablation here would have stayed green and vouched for nothing. The test then went red. Restored, rebuilt, marker count in dist back to 0, whole-tree git status --porcelain empty.

Changeset

minor on both @objectstack/metadata-protocol and @objectstack/spec, measured rather than pattern-matched. Both are public, non-private packages. metadata-protocol ships files: ["dist", …] and the new message bytes appear twice each in dist/index.js and dist/index.cjs, while both controls (the old literal opener, the interpolated opener) read 0. spec ships dist and src/**/*.zod.ts, and the new describe bytes appear in dist/api/index.js and the browser builds. content/docs/** is shipped by no package, so the mdx edits publish nothing on their own. Clause-②: yes takes at least minor; no (narrowing) arm, because nothing an author can write is removed, renamed or narrowed.

Acceptance notes

Noted, not filed, and ⛔ not ridden along:

  1. packages/metadata-protocol/src/runtime-authoring-gate.ts still writes [invalid_metadata] in front of code = 'INVALID_METADATA' / status = 422. Same package, same family, same ruling, third producer — outside the two files this card named, and it reaches dist/index.js. The nearest same-class remainder and the obvious next increment.
  2. packages/rest/src/rest-server.ts raises its own [invalid_request] opener, and packages/runtime/src/domains/packages.ts its own [writable_package_required] one. The idiom is repo-wide well beyond this card's two files.
  3. packages/spec/src/api/protocol.zod.ts WAS taken, not merely flagged — the describe correction at :815 is in this diff, and triage's boundary comment 5571629394 names exactly that correction as part of the deliverable, which is what makes Fixes #16245 honest. packages/spec/src/api/plugin-rest-api.zod.ts:898 was taken too, on the at-tier review's reading: it described the same publishMetaItem door as answering 404 [no_draft] while protocol.zod.ts now says NO_DRAFT, and BOTH ship in this same @objectstack/spec minor — twice over, as published .zod.ts source and in dist/api/index.{js,mjs}. One release carrying two contradictory descriptions of one door is a defect in this change, not a follow-up. Nothing regenerates from it (all 15 spec artifacts re-checked clean). metadata_conflict on that line stays lowercase deliberately — it is the shared spelling in protocol.zod.ts too, so "correcting" it would introduce the very inconsistency this removes. ⚠️ PR feat(spec)!: split the translation bundle type — settings is a platform group, not a per-app one (#15178) #19600 also holds protocol.zod.ts, at @@ -27,7 and @@ -3167,9, while this diff's single hunk is @@ -812,7 — roughly 2,350 lines clear, so it is not a region collision. It goes to the merge queue, ⛔ never hand-sequenced.
  4. Current-tense claims about current behaviour, outside this card's lane — found by git grep over the whole tracked tree at this head, with a dark control ([no_draft_xyzzy]) returning 0 so the probe discriminates. ⛔ Not edited:
    • packages/client/src/index.ts:2151 — JSDoc "404 [no_draft] when there is nothing to publish" (domain:cli).
    • docs/qa/platform-checklist/areas/access-security.json at :1816, :1818, :1875 (domain:devx).
    • docs/qa/platform-checklist/areas/studio-authoring.json at :477 and :514 — the same probe finds two more checklist carriers in a second area file (domain:devx).
  5. Governed-surface carriers, ⛔ untouched (docs/adr/** is Tier H, and triage forbade touching it on this card): docs/adr/0005-metadata-customization-overlay.md:348 and docs/adr/0010-metadata-protection-model.md:480 and :485 each show a wire payload whose error opens with the retired tag. ⚠️ Those same ADR payloads also spell code in lowercase ("code": "not_overridable"), which disagrees with the SCREAMING_SNAKE rule and with what the runtime actually emits — a pre-existing inaccuracy this change does not cause but does sit beside.
  6. Fixture drift, harmless but self-propagating: hand-written envelope fixtures in packages/spec/src/api/protocol.test.ts and mock refusals in plugin-security, cli, service-package and packages/client/src/client.test.ts still fabricate the bracketed spelling. They stay green because they construct their own errors, which is exactly how the idiom reaches the next author who copies one.
  7. One byte changed beyond the tags — the seed-body refusal opened the published seed bodies failed… once its tag was removed, and now opens The published….

⛔ packages/*/CHANGELOG.md entries that quote the old tags are release-owned history in the past tense, describing what the code did when they were written. Not a falsified claim, and not touched.

🤖 Generated with Claude Code

https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr


Generated by Claude Code

…hrow's own code

Every refusal `ObjectStackProtocolImplementation` and `SysMetadataRepository`
raise opened with a lowercase `[tag]` that is the restatement of the `code` the
same throw declares. `withoutDeclaredCodePrefix` strips only `CODE: ` casing and
separator, so the bracketed spelling was never stripped and reached the caller in
`error.message` — the duplication the 2026-08-29 ruling removes, because the same
fact already rides the `code` axis.

37 openers removed across the two files; in-file prose that named a tag now names
the code, which is the axis that still carries it.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
…ence

The per-door pins asserted the bracketed opener as part of the refusal text;
they now assert the prose that opens instead, so each still proves the declared
4xx sentence is quoted verbatim. Mock refusals that fabricated a producer
spelling were repointed too — a fixture that no longer resembles its subject is
how the idiom spreads.

Adds `protocol.bracketed-refusal-opener-absence.test.ts`: nothing else notices a
tag coming back, since a newly-written refusal reds no existing pin. Scans both
producers for the shape and carries a `new Error(` floor, so a scan that matches
nothing reds instead of passing for free.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
`assertOverlayAllowed`'s shared emitter opened its message with `[${code}]`,
built from the same variable it assigns to `err.code` three lines down. Same
family, most redundant member, and invisible to every grep for a literal tag —
which is why the card's inventory did not carry it.

The absence pin now detects the interpolated spelling as well, and declares the
two bracketed vocabularies that are NOT this family (the `[Protocol]` and
`[SysMetadataRepository]` logger prefixes) by name rather than by heuristic.

Pins that read the code off the PROSE now read it off `err.code`: three of them
were only ever green because the tag happened to spell the token.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
…ast stale prose

`packages/runtime`'s two publish-drafts pins asserted the bracketed opener on the
seed-apply refusal, which `metadata-protocol` produces — the only two
cross-package assertions the change moves. They now assert the sentence; the
class still rides `code`.

Also corrects three in-file comments that still spelled a tag the producer no
longer writes.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
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 22, 2026
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/spec, touching 41 documentable anchor(s).

15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via getReferences (sdk, the bare tail of client method meta.getReferences, bound to GET /api/v1/meta/:type/:name/references), meta.getReferences (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/references, selected by route anchor /:type/:name/references), meta.rollbackItem (sdk, the route ledger binds it to POST /api/v1/meta/:type/:name/rollback, selected by route anchor /:type/:name/rollback), rollbackItem (sdk, the bare tail of client method meta.rollbackItem, bound to POST /api/v1/meta/:type/:name/rollback))
  • content/docs/api/data-api.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), SysMetadataRepository (symbol, a top-level class), migrateStoredMetadata (symbol, a method of class ObjectStackProtocolImplementation), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/data-modeling/drivers.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/data-modeling/schema-design.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
  • content/docs/deployment/validating-metadata.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/cluster.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/contracts/metadata-service.mdx (via /:type/:name/rollback (route, bridged from symbol rollbackMetaItem — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via deleteMetaItem (symbol, a method of class ObjectStackProtocolImplementation), getMetaItem (symbol, a method of class ObjectStackProtocolImplementation), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/authorization.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/permission-sets.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
  • content/docs/protocol/objectui/actions.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
  • content/docs/ui/actions.mdx (via /:object/export (route, bridged from symbol getMetaItem — its route source's handler names it))
  • content/docs/ui/react-pages.mdx (via /:type/:name/references (route, bridged from symbol findReferencesToMeta — its route source's handler names it))
  • content/docs/upgrading.mdx (via toVersion (literal, a string literal in rollbackMetaItem))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), deletePackage (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/releases/v17/17-1.mdx (via findReferencesToMeta (symbol, a method of class ObjectStackProtocolImplementation))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 137 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 e2c55edb1ecde1136c1b137f00d021fd8a840b25 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from abf6d9455143e9f7b0014253657634fd37f2402b — the merge of head 5e294644b26155abef6c6595ec0e80c68bb40537 into base e2c55edb1ecde1136c1b137f00d021fd8a840b25, 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 abf6d9455143e9f7b0014253657634fd37f2402b && git checkout abf6d9455143e9f7b0014253657634fd37f2402b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e2c55edb1ecde1136c1b137f00d021fd8a840b25 5e294644b26155abef6c6595ec0e80c68bb40537 && git checkout -B drift-repro e2c55edb1ecde1136c1b137f00d021fd8a840b25 && git merge --no-ff 5e294644b26155abef6c6595ec0e80c68bb40537

node scripts/docs-audit/affected-docs.mjs --json e2c55edb1ecde1136c1b137f00d021fd8a840b25

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e2c55edb1ecde1136c1b137f00d021fd8a840b25 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Removing the bracketed openers made four published carriers wrong. The three
that are editable are repaired here, because a change that makes a published
claim false owns the repair.

- `ProtocolSchema`'s promotion `describe()` said the lookup answers 404
  `[no_draft]`; it now names `NO_DRAFT`, the axis that still carries it.
- `content/docs/references/api/protocol.mdx` regenerated from that source via
  `check:generated --fix` — AUTO-GEN, never hand-edited. One line moved.
- The error catalog's two documented `INVALID_REQUEST` payloads showed a
  `message` opening with the tag beside a `code` field already carrying the
  token; they now show what the platform emits.

⛔ The three carriers in `content/docs/releases/v17/` are deliberately left
untouched: release pages record what shipped, and a code PR does not edit them.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

CI triage — Test Core (5/6) is this PR's failure

Measured 2026-09-22T08:04Z on head ea92663819b6.

Failing check: Test Core (5/6) — run 35701250713, job 106659953132. It is the only failure: every other completed check on this head is success or skipped; 4 Test Core shards and Type Check · workspace were still running at the time of reading.

Ruled out as not-this-PR's: the base is green. main's push run at 1f53b0b685 — this PR's base commit — concluded success. So these are not pre-existing failures, and not a flake.

Why it is this PR's: all 6 failures are in @objectstack/objectql, a package this diff does not touch. The diff updates 12 sibling test files under packages/metadata-protocol and 2 under packages/runtime for exactly this refusal-message shape change, and does not reach the objectql consumers of the same messages. The blast radius is incomplete.

# Test Pinned Now emitted
1 protocol-commit-history.test.ts:659 [NOT_OVERRIDABLE] opener opener absent, guidance appended
2 protocol-commit-history.test.ts:855 same same
3 protocol-meta-types-rich.test.ts:133 /not_(overridable|creatable)/ Metadata type 'agent' is code-only: …
4 protocol-meta.test.ts:634 /invalid_metadata/ view/bad_view failed spec validation: …
5 protocol-object-overlay-layer.test.ts:573 object_overlay_package_mismatch [Protocol] Failed to hydrate object/myapp_invoice: …
6 protocol-writepath-object-ownership.test.ts:321 /not_overridable/ Metadata item 'object/myapp_invoice' is provided by a code package …

All paths are under packages/objectql/src/.

These are two families, not one. (1) and (2) pin the uppercase bracketed opener that #16245 exists to retire — updating those assertions is in scope. (3)–(6) pin a lowercase snake token, which is a different carrier from the uppercase bracket; if those messages have lost every machine-readable code token as collateral, the correction belongs in packages/metadata-protocol/src/protocol.ts, not in the assertions. Case 4 is the sharpest: line 632's expect(caught.code).toBe('INVALID_METADATA') still passes, so only the message text changed. Deciding that fork is the next step here; the six assertions will not be blanket-rewritten to match whatever the code currently emits.

Cross-lane note. packages/objectql is domain:engine, outside this card's domain:spec lane. Per the seat charter the card does not move and the fix stays in this PR, with the widened file face declared on the card. Separately, packages/spec/src/api/protocol.zod.ts (+1/-1 here) is also held by open PR #19600 — that overlap is left to the merge queue, not resolved in either PR.

The failed check has not been re-run: a re-run is only warranted for a failure that is not this PR's, and this one is.


Generated by Claude Code

…he boot log its own code

Six objectql assertions read the retired opener. Five are the same family as the
producer change and move to the envelope, which is strictly stronger than a
regex over prose: `code` plus `status` instead of a lowercase token that only
ever appeared inside the bracket.

The sixth is NOT that. `protocol-object-overlay-layer.test.ts` asserts captured
`console.warn` output, and a log line has no envelope beside it — the `code`
axis a caller reads does not exist there, so removing the opener took the only
machine-readable token with it and left nothing to fall back on. Fixed at the
PRODUCER's log site rather than in the assertion: the boot warning now prints
the declared code itself, as the sibling branch twelve lines above already does.
The caller-facing message stays prose.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
… owns

`os meta delete` carries the refusal on two surfaces and they are not the same
surface, so the two assertions do not move the same way.

stdout is the HUMAN one: `printError` prints the message and nothing else, so
the token never belonged there. That assertion now reads the sentence, and the
structural controls already in the test — exit code, the reset request's
parentVersion, the `if-match` header — are what keep it from degrading into
"something went wrong".

`--format json` is the MACHINE one, and it already carries the token on its own
axis. Measured rather than assumed, by dumping the real envelope:

  { success: false, error: '…has been modified…',
    code: 'METADATA_CONFLICT', httpStatus: 409 }

So the assertion reads `code` and `httpStatus`, which a prose match over `error`
only ever approximated. ⛔ No producer change was needed and the opener was not
re-added: `errorCodeFields` has carried this since #13347.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 108/108 CONTRACT_REVIEW_TIER
Head-sha: 3a05af7bf9f50973caefe964aeec1ebc93199dc5

Rendered by an isolated review subagent, adopted by the seat after per-request transcript verification (contract-review.md:54 — 子代理档只取其转录 harness 逐请求 model 盖章). Verified by the seat: 108 of 108 stamped requests carry the constant's current value and no other model appears; the three keyword hits for fallback/overload in that transcript are tool-schema prose, ⛔ not notices. The subagent's own self-report read 17/17, its turn count — the transcript stamp count is the reading that governs.

① Derived judgments

  • Accept set unchanged. No input newly accepted or refused; all 38 sites still throw the same code and status. Zero non-test +/- lines touch a .code =, .status = or httpStatus assignment; the error-code ledger is not in the diff and no code is minted.
  • Public surface not enlarged. 38 published error.message strings lose a leading [tag] prefix. No new export, no new payload key. The added (code=X) on the hydrate console.warn is stderr, ⛔ not a contract surface.
  • Clause-②: yes is CORRECT, reached independently of the card — and ⛔ not because the accept set widens, which it does not. It is because the diff changes bytes on a published output surface whose published text at merge-base AFFIRMED the old bytes: content/docs/api/error-catalog.mdx:609 and :660 documented message: "[invalid_request] …" beside code, and protocol.mdx:2277 said the door answers 404 [no_draft]. A no under SKILL.md:530 needs a Contract-text: citation negating the tag; none existed, so no would be refused under :531. Conformance class, which contract-review.md:13 forbids treating as mechanical.
  • Consumers: none, measured. objectui's four sites (extractWriteErrorMessage, InlineEditSaveBar:155, DetailView:776, AssignedUsersSection:237) all STRIP a leading bracketed prefix; objectstack has no reader outside tests. Under the ruling no consumer is entitled to parse error.
  • Inventory sound. 37 literal tagged non-comment lines at merge-base plus the interpolated opener at sys-metadata-repository.ts:1644 = 38; head = 0; dark control 0. The card's 30 was an undercount at its own head.

② Semver level

minor on both packages, verified against each files[] after a REAL build with positive and negative byte controls.

  • @objectstack/metadata-protocol — positive in both bundles: No pending draft exists for 2/2, is locked (_lock= 2/2, is not draftable 1/1, (code= 1/1. Negative, both bundles: [no_draft] [item_locked] [invalid_request] [not_overridable] [metadata_conflict] [commit_not_found] [destructive_change] [writable_package_required] all 0. ⚠️ Its DTS step did not complete in the isolated worktree (unbuilt sibling types) — an environment chain, ⛔ not a PR defect; CI Build Core is green at head.
  • @objectstack/spec — positive: answers 404 NO_DRAFT present in both dist api bundles. Negative: the bracketed spelling absent.
  • content/docs/** ships in no package.

③ Boundary flags

No pin was weakened — all 23 repointed assertions were checked one by one; seven moved from a prose token to code plus status, which is strictly stronger. The new absence pin carries a floor (≥5 new Error( per file, ≥30 combined) and a by-name allow-list of the two logger prefixes. No gate weakening, no security or permission boundary, no migration shape. Kept vocabularies (path [zod code], [rule]) name WHICH finding mid-sentence and never the envelope code ⇒ consistent with the ruling. PR #19600 overlap: @@ -27,7 / @@ -3167,9 versus this diff's @@ -812,7 ⇒ ⛔ not a region collision, merge queue arbitrates.

⛔ Two owed changes, both inside the reviewed face and both one line:

  1. The changeset headline is contradicted by measured shipped bytes. It states that refusal messages no longer open with a bracketed tag restating the code the same throw declares — but the same package's third producer, runtime-authoring-gate.ts:774, still throws [invalid_metadata] with code = 'INVALID_METADATA', and it SHIPS: that opener is present 1/1 in both dist/index.js and dist/index.cjs. The PR body discloses the producer; the consumer-facing changeset does not. ⇒ scope the headline to the two producers, or otherwise make it true. ⛔ Extending the removal to that third file is beyond triage's boundary and is NOT demanded.
  2. Two contradictory describes for ONE door ship in the same @objectstack/spec minor. packages/spec/src/api/plugin-rest-api.zod.ts:898 still describes publishMetaItem as answering 404 [no_draft] while protocol.zod.ts:815 now says NO_DRAFT. Both ship twice over — as published .zod.ts source and in dist/api/index.{js,mjs}. It reaches no generated page and DEFAULT_DISCOVERY_ROUTES has no runtime consumer, which is why this is a one-line correction and ⛔ not a wire defect.

Recorded, ⛔ not blocking: seedApplied.error and duplicatePackage.failed[].error carry no code axis at all, so on those faces the removed tag was the only in-prose machine token — no published contract promised it and no consumer reads it, but a follow-up card may be wanted. Stale carriers outside the dev's inventory: migrations/registry.ts:4828,5395, migrations/entries/semantic/18.admin-export-wildcard-removed.ts:26, identity/eval-user.zod.ts:124. Construction sat at the judgment tier per 强制条款② and the constant moved to its current value at 2026-09-22T08:04Z, after the 06:21Z claim — ⛔ not a verdict input; this at-tier review is the compensating control.

Not measured: whether plugin-rest-api.zod.ts is held by another open PR's single-writer surface — the on-push gate answers it. The dev's own local gate-family and test runs were taken as reported, ⛔ not re-run (contract-review.md:34).

Implemented-by: claude/issue-16245-bracketed-tag-refusal-openers
Reviewed-by: session_013RDBh5DqXd2xnLwvHLgLFr

VERDICT: FAIL


Generated by Claude Code

…t it changed

Two corrections from the at-tier review, one line each.

`plugin-rest-api.zod.ts` still described `publishMetaItem` as answering
404 `[no_draft]` while `protocol.zod.ts` now says `NO_DRAFT`. Both ship in the
SAME `@objectstack/spec` minor, twice over — as published `.zod.ts` source and
in `dist/api/index.{js,mjs}` — so one release would have carried two
contradictory descriptions of one door. That is not a follow-up; it is a defect
in this change. ⛔ Nothing regenerates from it: all 15 spec artifacts re-checked
clean, `content/docs`, `json-schema` and `api-surface` included.

The changeset headline claimed refusal messages no longer open with a bracketed
tag restating their own code. Measured in the built package, that is false: the
third producer, `runtime-authoring-gate.ts`, still throws `[invalid_metadata]`
and the opener is present 1/1 in both `dist/index.js` and `dist/index.cjs`. The
headline is now scoped to the two producers this change actually touched.
⛔ The removal is NOT extended into that third producer — beyond triage's
boundary, and the review does not ask for it.

`metadata_conflict` on the same line is deliberately left lowercase: it is the
shared spelling in `protocol.zod.ts` too, so changing it here would create the
inconsistency this commit is removing.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
Discharges the `os-regen-pending` deferral the merge driver recorded. The merge
brought an `enableOnInstall` description that git's textual merge dropped from
the generated page; only a regeneration on the committed merge restores it,
which is why the driver defers instead of trusting the text merge.

Not this card's content — regenerated, never hand-edited.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 64/64 CONTRACT_REVIEW_TIER (seat-measured, 2026-09-22T12:47Z)
Head-sha: 5e294644b26155abef6c6595ec0e80c68bb40537

Rendered by an isolated review subagent on the head above, which supersedes the FAIL record #19683 (comment) on 3a05af7bf9. The PR's own contribution moved by exactly 13 content lines between the two reviewed heads (diff of the two merge-base-to-head patches, hunk headers excluded): the changeset headline swap and the one-line plugin-rest-api.zod.ts hunk. Every other file of the 27 is byte-identical in what the PR contributes, so measurements on those are carried from the superseded record and said so below; everything the two new commits and the merge touched was re-measured.

① Derived judgments

  • Accept set unchanged — re-measured. Zero +/- lines in the two producers' merge-base-to-head diff touch a .code =, .status = or httpStatus assignment; the error-code ledger is not among the 27 files; no code minted. main moved protocol.ts by 62 lines (A stopped or rolled-back bulk batch now names a causal row that did not fail, and calls the real error "unknown" — live on main since #19432 #19452) between the two merge-bases and the merge was conflict-free; it added no opener (see inventory).
  • Inventory re-derived with the pin's own detector, not by literal grep: merge-base 38 openers (protocol.ts 32, sys-metadata-repository.ts 6 — 17 distinct literal tags plus the interpolated [${code}] site, whose two codes NOT_CREATABLE/NOT_OVERRIDABLE are already in the set), head 0/0, new Error( floor 86 against the pin's 30, dark control [no_draft_xyzzy] 0. The changeset's "NO_DRAFT, ITEM_LOCKED, NOT_OVERRIDABLE and the 14 others" is therefore exactly 17 and true; the one - line carrying [unanswerable_target] is a comment, not a throw.
  • Public surface not enlarged — carried. No new export or payload key; the producers' contributed diff is message text only, byte-identical to the previously reviewed head.
  • Clause-②: yes is CORRECT — re-measured from the diff itself. The - lines of this PR show the published docs AFFIRMED the old bytes at merge-base (content/docs/.../error-catalog.mdx:609 and :660 documented message: "[invalid_request] …" beside code; protocol.mdx:2277 said the door answers 404 [no_draft]), so a no under the skill's clause-② rule would have had no negating Contract-text: to cite. Conformance class, not mechanical. Carried on all three carriers: PR body line 3, the claim comment, the changeset.
  • The documented payloads now match the producers — re-measured. error-catalog.mdx's two INVALID_REQUEST messages are the templates at protocol.ts:292 and protocol.ts:15438, neither with an opener; the three FROM → TO rows resolve to protocol.ts:8204, :14141/:14209 and sys-metadata-repository.ts:1642, none opening with a bracket.
  • Consumers: none — carried, spot-checked. objectui's extractWriteErrorMessage still strips the leading bracketed tag at packages/react/src/utils/error-message.ts:86; it cannot break on the tag's absence.

② Semver level

minor on both packages, consistent with the changeset, verified against each files[] and a REAL build in an isolated worktree at this head (tsup, JS bundles), positive and negative controls:

  • @objectstack/metadata-protocol (files: dist, README, CHANGELOG) — dist/index.js and dist/index.cjs: all 17 retired literal openers 0/0 each, interpolated [${code}] 0/0; positives No pending draft exists for 2/2, is locked (_lock= 2/2, (code= 1/1. Owed change 1 verified in tree and bytes: runtime-authoring-gate.ts:774 still throws [invalid_metadata] and it ships 1/1 in both bundles — which is exactly why the headline is now scoped to ObjectStackProtocolImplementation and SysMetadataRepository, the two classes whose bytes read 0. The scoped headline is true; the previous one was not.
  • @objectstack/spec (files: includes dist and src/**/*.zod.ts) — owed change 2 verified in tree and bytes: [no_draft] is 0 across the whole of packages/spec/src and 0 files across the whole built dist; 404 `NO_DRAFT` reads 2/2 in dist/api/index.js and dist/api/index.mjs (both describes of the one publishMetaItem door), and the two shipped .zod.ts sources carry it 1+1. One door, one description.
  • content/docs/** ships in no package — re-read off both manifests.

③ Boundary flags

Dev open_questions: []. Flags raised, each answered:

  • Third producer left out (runtime-authoring-gate.ts) — correct: outside the two files the triage comment bounded, and the changeset no longer claims it. Remains a same-class follow-up, ⛔ not owed here.
  • metadata_conflict left lowercase on the corrected line — correct, and the seat's unjudged observation is resolved as NOT a finding of this change. plugin-rest-api.zod.ts:898 is the POST /:type/:name/publish route entry and protocol.zod.ts:895 is PublishMetaItemResponseSchema.version — the SAME door, both spelling its 409 metadata_conflict; flipping :898 alone would recreate one-door-two-spellings, the defect class the superseded FAIL record named as its item 2. The lowercase-code prose class is pre-existing (18 such lines across packages/spec/src/**/*.zod.ts at merge-base and at head, count unchanged by this PR; the ledger's own comment at error-code-ledger.zod.ts:612 records the residue as left as-is; ADR-0112 lineage Two error-code vocabularies are both live: StandardErrorCode is lowercase snake_case, the servers emit SCREAMING_SNAKE #3841/ADR-0112 batch 2: sweep the ~100 lowercase snake_case error-code emitters to the SCREAMING catalog #4003 closed on the emitters). A spec-wide prose normalisation would be its own card; ⛔ not this PR's to carry.
  • PR feat(spec)!: split the translation bundle type — settings is a platform group, not a per-app one (#15178) #19600 also holds protocol.zod.ts — carried: this diff's single hunk @@ -812,7 is unchanged since the prior head; merge queue arbitrates. The prior record's one unmeasured item — whether plugin-rest-api.zod.ts is held by another open PR's single-writer path — is now answered by the head's green No other open PR may claim the same single-writer path check.
  • Regen commit 5e294644b2 — the committed tree is the criterion: protocol.mdx differs from merge-base by exactly the ONE organizationId row that the one-line source change derives, and the restored enableOnInstall row equals main's. ⛔ check:generated was not re-run by the reviewer (derived gate family).
  • No pin weakened — carried. None of the 23 repointed assertions' files changed between the two heads. The absence pin at head re-read: floor ≥5 new Error( per producer and ≥30 combined (measured 86), logger prefixes Protocol/SysMetadataRepository allow-listed by name, literal AND interpolated arms.
  • Recorded, ⛔ not blocking, carried: seedApplied.error and duplicatePackage.failed[].error carry no code axis; stale bracketed spellings survive in comments (error-code-ledger.zod.ts:612, identity/eval-user.zod.ts:124, migrations/registry.ts:4828,5395, 18.admin-export-wildcard-removed.ts:26) and in the three read-only content/docs/releases/v17/ pages, which are historically accurate.

CI at this head: 42 check runs, all completed, every one success or skipped, 0 failures.

Nothing in this record derives from a dispatch instruction; the prior record was used to choose what to re-measure, not what to conclude.

Implemented-by: claude/issue-16245-bracketed-tag-refusal-openers
Reviewed-by: session_013RDBh5DqXd2xnLwvHLgLFr

VERDICT: PASS


Tier control — measured by the seat, not reported by the reviewer

The subagent self-reported 11/11 by counting its own turns. That is not the reading the protocol takes: the governing control is the transcript's per-request harness model stamp, and get_session is ⛔ not admissible as corroboration. So the seat counted the reviewer's transcript itself:

  • 64 assistant requests, 64 stamped claude-fable-5-1 — every per-request "message":{"model":…} in the transcript, no exceptions.
  • 74/74 counting every model field in the transcript, per-request stamps and advisorModel alike.
  • Dark control: 0. Every "model"/"advisorModel" occurrence in the transcript, filtered for anything other than claude-fable-5-1, returns empty — no other model name appears anywhere in 692 KB of transcript.
  • The three keyword hits for fallback/overload were read in context and are all tool-schema prose, ⛔ not fallback notices.
  • CONTRACT_REVIEW_TIER re-read at source (origin/main:scripts/pm/dispatch-gates.mjs) = claude-fable-5-1, matching the stamps.

So the reviewer was served at tier for every request it made, and the Served-tier: line above carries the measured count in place of the reported one.

The reviewer also flagged, for anyone running it from the shared checkout: dispatch-gates.mjs --tier printed tier names from a STALE TREE (checkout 14+ commits behind) — read the constant from origin/main, not from that output.


Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review September 22, 2026 12:49
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit 7a25a3e Sep 22, 2026
44 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-16245-bracketed-tag-refusal-openers branch September 22, 2026 13:12
os-litant pushed a commit that referenced this pull request Sep 27, 2026
… axis, not the retired [no_draft] opener

`publishItem`'s docstring (packages/client/src/index.ts, ships into dist/index.d.ts)
still documented the refusal as `404 [no_draft]`, the bracketed lowercase message
opener PR #19683 (#16245) retired from every metadata-protocol producer on the
2026-08-29 maintainer ruling that `error` is human language and `code` is the
machine token. The runtime has emitted `status: 404, code: 'NO_DRAFT'` with no
bracketed opener since that PR landed; this corrects the docstring to match,
following the same `404 `NO_DRAFT`` convention already used in
packages/spec/src/api/plugin-rest-api.zod.ts. No behaviour change.

Card relation carried in the pull request body only.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… a bracketed restatement of its own code (objectstack-ai#19830)

Fixes objectstack-ai#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 objectstack-ai#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](https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… not a retired message spelling (objectstack-ai#19843)

Fixes objectstack-ai#19705
Clause-②: no

## What

`docs/qa/platform-checklist/areas/access-security.json`, item
`access-security.permission-matrix-edit-loop` (P1): clause 2 and its
`verify` quoted the nothing-to-publish refusal by a bracketed lowercase
opener inside the message text. That opener was retired from every
metadata-protocol refusal in `7a25a3ee9c` (PR objectstack-ai#19683), so a checker
following the clause verbatim against a live answer recorded a false
failure.

Per the card's fix shape (adopted by triage): the clause and verify now
key on the **code axis**: HTTP 404 whose body carries `code:
'NO_DRAFT'`, and say explicitly to judge the refusal on status + `code`,
never on message prose. Pass/fail semantics are unchanged.

- `revision` 2 -> 3, with a new append-only `history` entry (revision 3,
ref objectstack-ai#19705). The revision-2 history entry that quotes the old spelling
is untouched (history is append-only).
- New `source` row:
`packages/metadata-protocol/src/sys-metadata-repository.ts#promoteDraft`,
the producer.

## Runtime site (verified on `origin/main` 71ef221)

- Producer: `SysMetadataRepository.promoteDraft`
(`packages/metadata-protocol/src/sys-metadata-repository.ts`) throws
message `No pending draft exists for TYPE/NAME — nothing to publish.`
with `code = 'NO_DRAFT'`, `status = 404`. No bracketed opener.
- Route: `POST /meta/:type/:name/publish` in
`packages/rest/src/rest-server.ts` routes the throw through
`handleRouteError`; the wire body shape `{ error, code: 'NO_DRAFT' }` is
pinned in `rest-4xx-message-truncation.test.ts` and
`rest-5xx-message-sanitization.test.ts`.
- Status and code match the card. Absence of the opener family is pinned
by `protocol.bracketed-refusal-opener-absence.test.ts`.

## Carrier re-derivation over `docs/qa/**`

Instrument: `git grep -ni no_draft -- docs/qa` plus a family census `git
grep -noE '[0-9]{3} \[[a-z][a-z_]+\]' -- docs/qa`, at 71ef221 (base).
Radius: every tracked file under `docs/qa/`. Lit control: `git grep -nic
invalid_metadata -- docs/qa` answers 3 hits in each of two area files,
so the instrument reaches both files.

- Before: `no_draft` 3 lines, all in `access-security.json` (clause,
verify, revision-2 history).
- After (db81a89): 1 line, the immutable revision-2 history entry. No
other `docs/qa/**` carrier of the retired `no_draft` spelling exists.

## Gates (all at `db81a89b`)

`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 13 commands; all 13 run, reconciled with `--ran` (13
derived, 13 run, 0 NOT-MEASURED, derived from recorded exit codes). All
exit 0. `check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT
MET, formula/lint unbuilt, nothing measured); after `turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint` it re-ran exit
0.

`check:platform-checklist` anchor counts: symbol anchors before 620/638
resolved against 310 cited sources, after 621/639 against 311 (the one
new source row resolves); residual 18 unchanged; 17 file floors held;
line citations 0 before and after. No floor or ledger touched.

## Changeset

None. Docs/QA-only: `docs/qa/**` ships in no package's `files[]` (`git
grep -l docs/qa -- '**/package.json'` = 0 hits; positive control: the
`files` arrays exist and list `dist`). This is `skip-changeset`
territory; per the dispatch, no label write was attempted from this
seat.

## Acceptance notes

- `docs/qa/platform-checklist/areas/studio-authoring.json` is off-limits
for this card by the triage ruling. Its `[invalid_metadata]` lines have
already been re-spelled on `main` (its item revision 3, dated
2026-09-23). It still carries `422 [writable_package_required]` in one
clause and one source row, for the metadata-protocol write door, which
no longer emits that opener (`SysMetadataRepository` throws
`WRITABLE_PACKAGE_REQUIRED` / 422 with plain prose). Same false-failure
class as this card; reported for the seat to file or attach to objectstack-ai#19709,
not edited here.
- `packages/runtime/src/domains/packages.ts` still opens two refusals
with a bracketed restatement of their declared code
(`[writable_package_required]` and `[duplicate_source_not_a_base]`). The
absence pin covers only `metadata-protocol`. Reported, not edited here
(outside `docs/qa/**`).
- `packages/rest/src/rest-server.ts`: the comment above the publish
route still reads `404 [no_draft] when nothing to publish`. This is a
code comment, not a refusal, so it goes here as a note only.
- `packages/client` JSDoc carrying the same spelling is objectstack-ai#19704 (a
`domain:cli` card). objectstack-ai#19704 is not addressed here.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…aft] opener (objectstack-ai#20187)

Fixes objectstack-ai#19704

Clause-②: no

`publishItem`'s JSDoc in `packages/client/src/index.ts` (ships into
`dist/index.d.ts`) documented the publish-refusal spelling as `404
[no_draft]` — the bracketed lowercase message opener the 2026-08-29
maintainer ruling retired from every `@objectstack/metadata-protocol`
producer via objectstack-ai#16245 / PR objectstack-ai#19683 (`7a25a3ee9c`). The runtime has answered
`status: 404, code: 'NO_DRAFT'` with no bracketed opener since that PR
landed. This corrects the docstring to name `NO_DRAFT` on the `code`
axis, following the same 404 `NO_DRAFT` convention already used in
`packages/spec/src/api/plugin-rest-api.zod.ts`. No behaviour change.

The card's own `Blocked-by: objectstack-ai#16245` is satisfied — that PR merged on
2026-09-22 (`7a25a3ee9c`). Triage originally planned to fold this card
into objectstack-ai#19705's round; per the claim's serial-constraints note, that round
closed via a PR touching only QA files and never picked this line up, so
this card proceeded on its own.

## Measured

- **Site confirmed**: `packages/client/src/index.ts:2152` read `404
[no_draft] when there is nothing to publish.` on `origin/main`
`455dcc06`.
- **Tree-wide sweep**: `git grep -n --fixed-strings "[no_draft]"` over
the tracked tree (excluding `content/docs/releases/` and
`CHANGELOG.md`s) found no other present-tense claim of this spelling
inside `packages/client/src/`. Control words in the same file —
`NO_DRAFT` and `[item_locked]` are zero there while `publish` hits 55
times — show the grep discriminates rather than being silently broken.
- **Published-surface question (triage's UNMEASURED item)**: built
`@objectstack/client` and grepped the emitted `dist/index.d.ts` — the
docstring **does ship** (with positive controls, two neighbouring JSDoc
lines, confirmed present too). This is a published-surface correction,
so it carries `.changeset/19704-client-no-draft-jsdoc.md` (`patch`), not
the `skip-changeset` route.
- **Runtime producer**: `SysMetadataRepository.promoteDraft`
(`packages/metadata-protocol/src/sys-metadata-repository.ts:946`) throws
`status: 404`, `code: 'NO_DRAFT'`, message `"No pending draft exists for
{type}/{name} — nothing to publish."` — no bracketed opener. The new
docstring names exactly that.
- No test pins the old `[no_draft]` text (zero `git grep` hits across
`packages/client/`'s test files).

## Acceptance notes

Two more present-tense claims of the same *retired bracketed-opener*
shape turned up while measuring the above, both outside this card's file
surface (packages/client/src/ + the [no_draft] spelling specifically) —
reported rather than fixed here:

- `packages/rest/src/rest-server.ts:8427` — a source comment (`404
[no_draft] when nothing to publish.`) on the same `POST
/meta/:type/:name/publish` route this card's producer serves. Same
defect family as objectstack-ai#19704 and objectstack-ai#16245, different file.
- `packages/client/src/index.ts:6297` — a sibling docstring (`Get a
sharing rule by id or name. 404 [RULE_NOT_FOUND] when absent.`) for a
*different* code. Its producer
(`packages/plugins/plugin-sharing/src/sharing-rule-service.ts:818`, bare
`throw new Error('RULE_NOT_FOUND')`) and REST mapping
(`packages/rest/src/rest-server.ts:12807-12808`, `:12920`) never emit a
bracketed message either — the objectstack-ai#16245 shape recurring on a code that
ruling never touched.

Neither is the `[no_draft]` spelling this card claims, and the second
isn't in scope by spelling; both are named in the round report for the
dispatching seat to route.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

2 participants