fix(runtime): strip read-time decorations before the route-level seed apply's closed parse - #16162
fix(runtime): strip read-time decorations before the route-level seed apply's closed parse#16162os-litant wants to merge 2 commits into
Conversation
… apply's closed parse `POST /packages/:id/publish-drafts` reads each just-published `seed` body back through `protocol.getMetaItem` and hands it to `SeedLoaderRequestSchema`. That read exits through `decorateMetadataItem`, which stamps `_diagnostics` on every body whose type has a registered schema, and `SeedSchema` is closed — so the door refused the document it had just served, on a 200, as `seedApplied.error`: zero rows loaded and the author told their seed body failed spec validation. The direction is settled by the contract, not by judgement. `METADATA_READ_DECORATIONS` declares `_diagnostics` a key the read path derives and attaches to the response, and its module names "any re-parse of a served document" as a consumer that must strip. `_packageId` is deliberately NOT a member and `SeedSchema` accepts it via `MetadataProtectionFields` — measured on the real producer, the served body carries both and the schema refuses exactly one. So the fix is the declared helper, not a widened schema and not the export path's blanket underscore strip, which would drop provenance this schema allowlists on purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…engine double Both are mechanical regenerations the gates asked for by name, not hand edits: - `check:check-system-context-census` reported pure LINE ROT — the three `domains/packages.ts` anchors on `content/docs/permissions/system-context.mdx` each moved by exactly the +9 lines this branch's import block added. Repaired with the gate's own `--fix`; it now reports OK over 105 sites and 140 anchors. - `check:engine-double-contract` reported the new test file's engine double as RETAINED-but-unrecorded on all three scanned verbs. Regenerated with `--write` (736 rows, 3 added, 0 lost), so the pin protects the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift Check
What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract review (clause ②) — PASS on content · NOT LANDABLE (dirty) — PR #16162 at head
|
Fixes #15591
The package-publish door's route-level seed apply could not consume the shipping protocol's own read-back envelope.
POST /packages/:id/publish-draftsreads each just-publishedseedbody back throughprotocol.getMetaItem, unwraps it, and hands it toSeedLoaderRequestSchema.safeParse. That read exits throughdecorateMetadataItem, which stamps_diagnosticson every body whose metadata type has a registered schema —seedhas one — andSeedSchemahas been closed since protocol 17. So the door refused the document it had just served, minted the refusal as a 422 and delivered it on a 200 asseedApplied.error: zero rows loaded, and the author told their seed body failed spec validation when nothing about it was wrong.Which side was wrong — decided by measurement, not by judgement
The card left the direction open (strip at the consumer vs. stop decorating at the producer) and warned that the two underscore keys are not one population. Driven against the real producer — a
state:'draft'seed row promoted by the shippingpublishPackageDraftson a realObjectStackProtocolImplementation, read back through the same protocol over the same engine — they are not, andspecnames which is which:_diagnosticsis a member ofMETADATA_READ_DECORATIONS, whose module states the rule this door was missing: such a key "belongs to the response, never to the document — a served body is therefore NOT a valid input to the schema that produced it until these are removed", and it names "any re-parse of a served document" as a consumer class that must strip. ⇒ the producer is correct; the consumer was skipping a declared step._packageIdis "deliberately NOT" a member — ADR-0010 envelope state, which "the closed metadata schemas allowlist … precisely so a served document keeps its provenance on re-parse".SeedSchemaspreadsMetadataProtectionFieldson purpose, and the measurement above confirms it parses clean.So the repair is
stripReadDecorationsat the unwrap — the same helper, for the same reason, thatrest-server.tsalready calls before parsing a serveddataset("A SERVED document is not a valid input to the schema that produced it"), thatservice-automation's cold-boot flow bind calls, and thatsaveMetaItemcalls before its verbatim persist. This makes the seed apply the fourth consumer of one declared mechanism rather than a new accommodation.⛔ Not a widened schema.
SeedLoaderRequestSchemaandSeedSchemaare untouched; no.passthrough(), no alias, no??fallback. Prime Directive #12 pushes back on a tolerant consumer, and this is not one: it removes an annotation the READ path added, not a key an author wrote.⛔ Not the blanket
startsWith('_')stripassemblePackageManifestruns 300 lines up in the same file. Triage was right that the precedent sits nearby and right that it must be answered rather than copied: a portable manifest must shed provenance, a re-parse must keep it. Reusing that rule here would drop_packageId/_provenancethis schema allowlists on purpose — measured, in §3's second ablation below.Is the fallback still live? Yes, and the spec says so
The card's first open question was whether to repair the fallback or delete it as unreachable.
PublishPackageDraftsResponseSchema's own note answers it without a judgement call: the REST door "back-fillsseedAppliedfor custom protocols that do not self-apply", declared as part of the wire face. Measured here too — the shipping protocol'spublishPackageDraftsdoes answerseedApplied(§0 asserts it), which is exactly why this path is invisible in the shipping composition and why the harness has to withhold that field to reach it at all. Deleting a declared behaviour was not available; hardening it was.Every consumer of the read-back envelope
Enumerated rather than fixed one call site (
stripReadDecorationscall sites plus everySeedSchema/SeedLoaderRequestSchemaconsumer):rest-server.ts— serveddatasetbeforeDatasetSchema.parseservice-automation— cold-boot flow bindmetadata-protocol—saveMetaItem's verbatim persistmetadata-diagnostics— re-parse before computing a verdictruntime/domains/packages.ts— the route-level seed applyplugin-email— effective template readThe other
SeedLoaderRequestSchemaparses (app-plugin.ts×3,marketplace-install-local-plugin.ts,metadata-protocol's ownapplySeedBodies) all read authored config, a manifest already cleaned on export, or a raw storeddraft.body— none is a served document, so none is affected.Tests
packages/runtime/src/domains/packages-seed-apply-read-decorations.test.ts, 6 cases. The publish and the read-back are both real; onlypublishPackageDraftsis a facade, and only because the fallback runs exclusively for protocols that do not self-apply.SeedLoaderService.prototype.loadis spied through, not replaced, so §3 observes the exact body the shipping loader received.The whole
src/domains/packages*set runs green beside it — 11 files / 169 tests passed, which includes the two suites that own this call site from the other side (packages-seed-apply-disclosure.test.tsandpackages-seed-apply-org-scope.test.ts), andpnpm --filter @objectstack/runtime typecheckpasses both halves. That second half is not vacuous here:packages/runtime/tsconfig.jsonexcludes**/*.test.ts, so the reading rests on the siblingcheck:test-typecheck --project tsconfig.test.json, andtsc --listFileson that project confirms the new file is in the program.Reverse verification, direction predicted in writing before running, mutation confirmed on disk both ways (anchored text counts plus blob hash) and restore proven by observed state (
git diff HEADempty, blob hash back to HEAD's) — never by an exit code:expected '[invalid_metadata] the published seed…' to be undefined.expected { object: 'project', …(4) } to have property "_packageId". This is the run that makes §3 load-bearing; without it the file would be satisfied by a strip that silently deletes provenance.No rebuild was required for either ablation and that is a property of the resolution, not an omission: the subject (
packages/runtime/src/domains/packages.ts) is reached from the suite through a relative specifier inside its own package, and@objectstack/specis aliased to source inpackages/runtime/vitest.config.ts. The imports that do resolve throughdist(@objectstack/metadata-protocol,@objectstack/metadata-core) are not mutated by either ablation, and the dependency closure was built before any of these readings.Gates
Union derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsagainst the delivered diff and asserted against its ownReconciliation — 91 famil(ies)line (91 commands emitted). The whole union was re-run after the final commit, at73cdcb34a76. Exit codes captured before any pipe, in every case by redirecting first (cmd > log 2>&1; EXIT=$?) rather than reading$?after a pipe. The Artifact rosters block (36 further families) was run separately, also at73cdcb34a76. Two gates asked for a mechanical regeneration and got one, in their own second commit:check:system-context-census --fix(pure line rot — the threedomains/packages.tsanchors each moved by the +9 lines this branch's import block added) andcheck:engine-double-contract --write(the new test file's engine double, 736 rows, 3 added, 0 lost).NOT MEASURED, by name, each with its own verdict line and none read as a pass:
check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/.check:type-check-debt— exit 3, same class; it says explicitly that it is "NOT evidence that a recorded number went up".check:published-readme-exports— exit 3, same class.check:react-declaration-parity— needs a browser manifest dump; refuses rather than compute a false green.check:single-claim-paths— exit 2,NOT WIRED(noPR_NUMBERoutside CI). Its pnpm spelling resolves to--self-testonly, so that green grades the checker's fixtures, not this diff (Two artifact-roster gates report a green that is not PR clearance — the pnpm script names resolve to --self-test only #16030).check:partof-closing-keywordwas in the same bucket and was then measured for real: re-run with this body inPR_BODY, it exits 0 — "this PR carries no Part-of/closing-keyword contradiction". Its pnpm spelling is--self-testonly and is not what that reading rests on.Everything else in both blocks passed.
check:skill-examplesinitially refused for the same prerequisite reason and was measured after building@objectstack/client-react's closure: 257 prose examples type-check across 3 surfaces.Changeset
patchon@objectstack/runtime. A bug fix on a published package: no API is added, removed or changed, no spec surface moves, and no stored shape migrates — so neitherminornor a**BREAKING**banner with an ADR-0087 disposition marker applies (andcheck-changeset-no-majorrefusesmajorin the launch window regardless).🤖 Generated with Claude Code
https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Generated by Claude Code