Uh oh!
There was an error while loading. Please reload this page.
feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191
Conversation
… read (ADR-0130 D4)
`ObjectStackDefinitionSchema` gains an optional `packages` key carrying package
entries, so one release artifact can deliver a product split into modules with
zero object renames. `manifest` (singular) is retained and both shapes are read:
`packages` present -> iterate; absent -> `manifest` is a single-element list.
Each entry is a wrapper object (`{ manifest }`), never a flattened manifest body,
so a future `{ ref, integrity }` external segment is an additive key rather than
a reshape. Segmented loading itself is an ADR-0130 Non-goal and is not built.
`os compile` / `os build` needed no source change: normalize, lower and write all
shallow-clone the top level and validation parses with this schema. Verified by
compiling real projects rather than read off the source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015adLit3ZYASJiXwxKG78Wi…refresh baselines `check:spec-parsed-alias` (lint.yml "Spec type-alias convention gate") refused `ArtifactPackageEntry` for having no name for its post-parse shape: ManifestSchema applies defaults, so `z.input` and `z.infer` genuinely differ and a consumer holding a parse result had no type to hold it in. Declares `ArtifactPackageEntryParsed`. Also types the two `warnSpy.mock.calls` callbacks — `tsconfig.test.json` compiles the test files this package's plain `typecheck` skips, and `check:test-typecheck` flagged both as TS7006. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015adLit3ZYASJiXwxKG78Wi
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 128 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 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd && git checkout 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9dd022aea05251a8bc8ee2161bd93931abd031b9 a1683952929e20211d7c3294fa84d8ee46f7fdfe && git checkout -B drift-repro 9dd022aea05251a8bc8ee2161bd93931abd031b9 && git merge --no-ff a1683952929e20211d7c3294fa84d8ee46f7fdfe
node scripts/docs-audit/affected-docs.mjs --json 9dd022aea05251a8bc8ee2161bd93931abd031b9
|
os-support-ai
commented
Sep 1, 2026
Independent contract re-review (Clause ②) — PASS (second, independent verdict)Reviewed at head Both limbs YES (path:
Landing: non-governed surface, clear-and-land path. CI is green at head — proceeding ready → merge queue, tracked to MERGED by this seat. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14161 — epic #14122, implementing ADR-0130 D4 (
docs/adr/0130-release-artifact-as-co-ownership-boundary.md).ObjectStackDefinitionSchemagains an optionalpackageskey so one release artifact can carry N package manifests, and a product can be split into modules with zero object renames.manifest(singular) is retained, and both shapes are read:packagespresent → iterate;packagesabsent → treatmanifestas a single-element list. A replacement would break every artifact already built and on disk at every customer — the schema shape IS the compatibility mechanism, which is why ADR-0130 states the read-both rule as the schema decision rather than an implementation note.Clause-② : YES
Path limb:
packages/spec/src/**. Content limb: a new optional public key is a widening of the contract's accept set.needs:contract-reviewis attached and this PR stays draft pending that review.The structural reservation — the load-bearing shape decision
Each
packages[]entry is a wrapper object whose manifest body sits undermanifest:, never the manifest body inlined flat as the array element. ADR-0130 D4 reserves that position deliberately, at schema time, and the reason is worth restating: when a future external-segment form lands it is{ ref, integrity }— an additive key on an existing object. Flatten the manifest into the element instead, and that same future is a reshape:ref/integritywould have to be bolted onto the sharedManifestSchemaand every required manifest field would have to go optional, because a segment reference carries no manifest content at all.The ablation below turned up a second, unplanned argument for the wrapper:
ManifestSchemais not strict. Under the flattened shape,{ ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is astrictObject, so the same input is refused by name — which is exactly D4's forward-compatibility posture (an older runtime "refuses it cleanly rather than mis-parsing it into a half-registered install"), riding the existingmanifest.engines.protocolmechanism (ADR-0025). ⛔ No new version-negotiation mechanism.⛔ Explicitly not in this PR, per ADR-0130's Non-goals and the card split: segmented loading itself; the load path that iterates the list in topological order through
resolvePluginOrder(D5, #14162); theinstallPackageco-ownership gate with its install-time object-name uniqueness check (D1/D3, #14163 — ADR-0130 requires those two to land as one inseparable change, and nothing here touches either). Until those land, a multi-package artifact parses and carries its list and nothing downstream iterates it.What the diff contains
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema(+ its…Parsedalias, ADR-0122) and the optionalpackageskey; aCOMPOSE_KEY_DISPOSITIONSentrypackages/spec/src/stack-artifact-packages.test.tspackages/cli/test/compile-artifact-packages.e2e.test.tsos buildactually writesscripts/check-stack-collection-maps.mjspackagesis not a metadata collection — see belowpackages/spec/api-surface/root.json,export-origins/root.json.changeset/…@objectstack/spec: minoros compile/os build: no source change was needed, and that was measuredThe card names
compile.ts:271(parse) and:596(write). Both anchors verified. No CLI source change is required:normalizeStackInput,lowerCallablesand the artifact write each shallow-clone the top level, and the validation step parses with this very schema, so the key flows through end to end. "It works by construction" is precisely the claim that stops being true the day someone adds a whitelist to one of those three, so it is pinned by compiling real projects and reading the artifact off disk — including the exact top-level key set for a single-package project, which is where a.default([])on the new key (the obvious near-miss) would have shown up as every customer's artifact being rewritten on its next build.Two mechanical consequences a new top-level key carries
COMPOSE_KEY_DISPOSITIONS(packages: 'concat'). That table is a TOTALRecordkeyed on everyObjectStackDefinitionkey, so the key does not compile until composition is declared for it — declared here, in the change that declares the key, as the table's own docblock requires. Concat is the array-collection rule for the array-collection reason: composing two stacks that each carry entries must yield both.manifest:'s deliberate pick-one semantics — acomposeStackspreserve mode is ADR-0130's follow-up row 3, its own additive card, and a pin here keeps that follow-up visible rather than silent.scripts/check-stack-collection-maps.mjs. Its extractor treats every top-levelz.array(SomeSchema)as a metadata collection and reconciles it against eight enumerating sites.packagesmatches that shape and is not a metadata collection: no singular metadata-type name, noPLURAL_TO_SINGULARentry, no artifact subdirectory, noregisterInMemorykind, no map/record authoring form. Left alone it would manufacture eight simultaneous deviations and drive eight waiver rows each asserting the opposite of the truth. So the gate gains a declared, reasonedNON_COLLECTION_ARRAY_KEYSexception (a list of names, deliberately not a second heuristic) plus a self-test assertion — ⛔ rather than dodging the regex by renaming or inlining, which would leave the next author's harmless reformat reddening eight sites for no visible reason. The gate's reference set is unchanged at 31.Reverse verification (ablation)
Mutated
packages: z.array(ArtifactPackageEntrySchema)→z.array(ManifestSchema)— the flattened shape D4 forbids — and re-ran the pins.'./stack.zod', a package-relative path, so it resolves to source, not throughexports→dist. No rebuild leg is required for this ablation to be real, and a staledistcannot make it falsely green.0(expected 0), flattened form present1(expected 1), plus a non-emptygit diff --stat.engines.protocolcarrier, refuses a manifest body inlined flat, refuses an entry with no manifest, and refuses the future{ ref, integrity }segment. That last one is the finding recorded above — under the flattened shape the segment form parses green.git checkout HEAD -- ABSOLUTE-PATH(namingHEAD, never a bare--), thengit diff HEADempty andgit hash-objectequal to the HEAD blob — both374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried atrap … EXIT INT TERM.Verification — all on the final commit
a16839529Lint & Repo Gatesjob, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caughtcheck:spec-parsed-alias(ADR-0122), which the path derivation did not name — the new alias needed anArtifactPackageEntryParsedbecauseManifestSchemaapplies defaults, soz.inputandz.infergenuinely differ. Fixed in the second commit.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands— all 56 derived commands: 55 green, 1 NOT MEASURED.check-test-completenessexits 3 by design without a CI test-run log to parse ("⛔ It is not a red, and there is nothing here to fix"). Four others initially reported exit 3 /PREREQUISITE NOT METagainst a partially built tree; afterturbo run build --filter='./packages/*' --filter='./packages/*/*'(what lint.yml does),check-dev-prereqs,check:dual-build-cjs-loads,check:type-check-debtandcheck:type-check-coverageall re-ran green.packages/specfull suite: 449 files / 12,072 passed, 1 skipped.pnpm --filter @objectstack/spec typecheck(source + scripts +tsconfig.test.json): green.packages/cli:compile-artifact-packages.e2e+ the three sibling compile-path suites — 4 files / 25 passed.pnpm --filter @objectstack/cli typecheck: 0 errors.TS7016/TS7006cascade from declarations my ownOS_SKIP_DTS=1closure build had skipped, zero naming any file in this diff. Recorded because it read exactly like contract drift and was not.node scripts/pm/check-governed-merges.mjs --test FINAL-FILE-LIST: NOT governed — 0 of 7 paths hit the register; ordinary queue landing applies. Re-run on the final list, not recalled from earlier in the session.pnpm --filter @objectstack/spec check:generatedgreen (api-surface/andexport-origins/regenerated from a fulldtsbuild, +1 line each).Changeset grade
@objectstack/spec: minor. A pure widening — the new key is optional, no existing key changed shape, nothing that parsed before is refused now. No@objectstack/cligrade, and that is a measurement rather than an omission: the CLI ships no changed line and takes the new accept set entirely through its@objectstack/specbump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registrationgreen).ADR anchor re-verification
Every anchor ADR-0130 D4 and the card cite was re-checked against this branch's base (
682d03ba7) and all held exactly:stack.zod.ts:240ismanifest: ManifestSchema.optional();compile.ts:271is theObjectStackDefinitionSchema.safeParse;compile.ts:596isJSON.stringify(finalBundle, null, 2)with the write on:597;build.tsis a six-line subclass ofCompile;ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.Generated by Claude Code
Generated by Claude Code