Repository navigation
feat(metadata-protocol): mark an erased authoring arm in the /meta/types derivation - #19399
Conversation
…pes derivation
The served output derivation of a `ZodPipe` describes the transform's result,
so a predicate slot's bare-string authoring arm is emitted as the anonymous
`{}` — indistinguishable on the wire from a member that admits every instance.
Annotate exactly those husks with one vendor-prefixed keyword,
`x-objectstack-erased-authoring-input`, carrying a shape version and the
authoring type that was erased. The predicate is structural: the node admits
everything, the zod node behind it is a pipe, and the pipe's input side names a
type. A `z.unknown()` husk (the envelope's own `ast`) is not a pipe and stays
bare, and a type served from the `io: 'input'` retry has erased nothing and is
marked nowhere.
No keyword zod emitted is rewritten, so what each document accepts is
unchanged and the refused blanket `io: 'input'` widening stays refused. The
degeneracy pin's baseline derives through the same hook, the way #17502
established for the unauthorable strip, so its blast radius still reads one.
Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
Co-authored-by: Claude <noreply@anthropic.com>
…h the same hook `protocol.meta-types-unauthorable-columns.test.ts` asks whether the served payload is a derivation MINUS unsatisfiable nodes and nothing else. The erased-authoring mark is an addition, so eight types read as unexplained against a baseline derived without it — red for a reason that pin does not own, and blind meanwhile to the over-drop it exists to catch. Same treatment #17502 gave the strip: the baseline derives with the emitter's own `override`. Any other addition or rewrite still lands in `other`, and every removal is still judged by `acceptsNothing`. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
…erased-authoring mark The new suite stands up a fake engine over the producer's own write-verb dispatch assertions, so `check:engine-double-contract` wants its three rows in the retained ledger or the file is never protected by it. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6e471b76653c06d04525bc497b7531e12560caad && git checkout 6e471b76653c06d04525bc497b7531e12560caad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5d3d1d8b69e84497373c5232a654f0bbffe2b76 03809f0972ed908aaf65625af58395f91e086ca3 && git checkout -B drift-repro c5d3d1d8b69e84497373c5232a654f0bbffe2b76 && git merge --no-ff 03809f0972ed908aaf65625af58395f91e086ca3
node scripts/docs-audit/affected-docs.mjs --json c5d3d1d8b69e84497373c5232a654f0bbffe2b76
|
Part of #19295
Clause-②: no
What this lands
GET /meta/typesnow annotates a served member whose AUTHORING arm the output derivation erased, so a consumer can tell an erased authoring type from a member that genuinely admits anything.Every predicate slot the platform serves composes the expression-input family — a two-arm union whose string arm is a
ZodPipe. The served derivation is zod's defaultio: 'output', which describes what comes OUT of the transform, so the arm's own input type is erased and the member is served with an empty first arm. On the wire that empty schema means "admits everything", so a metadata designer could not tell the husk from a member that really does accept any instance.Each such husk arm now carries one vendor-prefixed keyword:
{ "x-objectstack-erased-authoring-input": { "version": 1, "type": "string" } }packages/metadata-protocol/src/erased-authoring-mark.ts, with the keyword, the shape version and the reader a consumer mirrors.hook.condition/visibleWhen/ the rest.Clause-② — how it was measured
The claim comment told this seat to measure the declaration rather than carrying one, and the card's Acceptance sets the rule: a new declared key on the ADR-0089 envelope is
yes; an annotation on the served projection only is measured.The shape chosen is the annotation, and the diff is the measurement. Seven files, all in
packages/metadata-protocol/src/,.changeset/andscripts/engine-double-contract.pinned.json.packages/specis not touched: no Zod object gained a key, no closed set gained a member, no published entry point's export listing grew, and no registry gained a row.packages/metadata-protocol/src/index.tsis deliberately unchanged — the mark travels on the WIRE, so objectui reads it from the payload and no new published export exists for it to import.Why the envelope key was the wrong shape here, and not merely the more expensive one. A declared key on the envelope describes the OBJECT arm, while the thing that needs describing is the sibling arm the derivation erased. It would also have been carried by every consumer of that envelope everywhere it is embedded, including the persistence contract where nothing was erased at all. The annotation sits on the exact node whose meaning is ambiguous, and it is emitted by the projection that created the ambiguity.
The refused widening is untouched
The blanket
io: 'input'repair stays refused and its pin stays armed. JSON Schema ignores an unrecognised keyword, so every served document accepts exactly what it accepted before — byte-different, semantically identical.adds no constraint — every marked arm still admits every instanceasserts that structurally: read with its mark removed, every marked node is still the empty schema zod emitted.Two pin baselines now derive through the emitter's OWN
override, which is the treatment #17502 established for the unauthorable strip rather than a new one:protocol.meta-types-degenerate-derivation.test.ts— blast radius still reads exactly['action'].protocol.meta-types-unauthorable-columns.test.ts— the served payload is again a derivation minus deletions and nothing else.Reverse verification, both legs restored and proven against HEAD. Adding
io: 'input'to the served derivation and re-running the degeneracy pin moves 24 types instead of one and reds it — the refusal is still armed, not laundered into a baseline. Mutation and restore went throughscripts/ablation-replace.mjs, so the anchor count, the blob change and the post-restoregit diff HEADare on-disk evidence rather than an exit code.The negative control
A mark that appeared on every empty schema would be exactly as uninformative as the husk it replaces, so both directions are pinned and the negative half is the load-bearing one.
Measured over the served surface: 78 marked arms across 8 types, and 187 empty-schema nodes left unmarked. Named instances are pinned rather than only counted — the ADR-0089 envelope's own
ast(az.unknown()sitting one level BELOW a marked arm, inside the very envelope that arm is a sibling of),field.defaultValue, and the open record values ontool.parameters.actionis the second control, from the other direction: it is the one type served from theio: 'input'retry, where a pipe derives from its input side, nothing is erased, and the predicate slot already publishes its real string arm. It carries zero marks, and the test also asserts the string arm is really there — so the absence reads as "nothing was erased", never as "the sweep missed it".Reverse verification of the control. Making the emitter treat every husk as an erased pipe arm reds BOTH negative assertions — the
astpin and theactionpin — so neither is vacuously true.Premise re-derived on this tree, with two corrections
The card's measurement came from objectui's dev reports, seat-verified there. Re-derived here against
origin/main, and it holds — with two things the card states differently:EvaluatedExpressionInputSchema, notExpressionInputSchema. The card names the latter. On this treehook.condition,field.visibleWhen/readonlyWhen/requiredWhenand the flowedge.conditionare allEvaluatedExpressionInputSchemasince spec/automation:FlowEdgeSchema.conditionstill accepts an envelope the engine cannot evaluate (ast-only, whitespace-onlysource) — the evaluated-slot rule of #15430 has not reached the edge condition #15807 / spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every otherExpressionInputSchemaslot an engine evaluates (formulaexpression, validation / hook / sharingcondition,visibleWhen…) still accepts anast-only or blank-sourceenvelope #15811;ExpressionInputSchemaand its aliasPredicateInputSchemaremain the PERSISTENCE contract. It changes nothing structural — both are two-arm unions whose first arm is aZodPipeover a string, both erase identically — but the mark is emitted by a structural predicate rather than by naming a schema, so it covers the whole family and a third sibling added later.sharing_ruleresolves no schema in the registry-only harness, so that member is covered by the structural sweep wherever it IS served rather than pinned by name.One consequence worth naming because it is wider than the card's list: the same predicate also marks
flow.nodes.items, which derives an empty schema today and whose erased authoring type is an object. That is the same defect one level up, and the mark now says so instead of serving an anonymous husk. Nothing else about that member moves.Acceptance notes
content/docs/references/is auto-generated frompackages/specTSDoc and may not be hand-edited; the hand-writtencontent/docs/api/metadata-api.mdxdoes not document the/meta/typesschema derivation at all. Both homes are outside this claim's declared file surface (packages/metadata-protocol/src/), so the seat routes that half rather than this lane taking it uncleared.flow.nodes.itemsserving an empty schema is a whole-member husk of the metadata-protocol:/meta/typesserves an EMPTY JSON Schema foraction— the output-mode derivation of itsZodPipehas no properties, and the hand-crafted fallback never fires #17501 class, one level below the type. It is now labelled by the mark; making it derive is a different change with a different blast radius. Carrier: no queued PR touches it today.Verification
pnpm --filter @objectstack/metadata-protocol test— 183 files, 2608 passed, 19 skipped, 0 failed.pnpm --filter @objectstack/metadata-protocol typecheck— clean.pnpm --filter '@objectstack/metadata-protocol^...' build— dependency closure green.scripts/pm/dispatch-gates.mjsand reconciled with--ran: 68 derived, 65 run green, 3 NOT MEASURED (check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt— each exits 3 refusing its own prerequisite, a fullpnpm build; CI's Build Core provides it).pnpm lint— the whole repo,eslint . --no-inline-config, exit 0 at03809f097.Generated by Claude Code