feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema
Sep 1, 2026
Merged

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4)#14191
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#14161 — epic #14122, implementing ADR-0130 D4 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md).

ObjectStackDefinitionSchema gains an optionalpackages key 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: packages present → iterate; packages absent → treat manifest as 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-review is 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 under manifest:, 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/integrity would have to be bolted onto the shared ManifestSchema and 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: ManifestSchema is not strict. Under the flattened shape, { ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is a strictObject, 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 existing manifest.engines.protocol mechanism (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); the installPackage co-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

FileWhy
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema (+ its …Parsed alias, ADR-0122) and the optional packages key; a COMPOSE_KEY_DISPOSITIONS entry
packages/spec/src/stack-artifact-packages.test.ts17 pins — both read branches, the reservation, the negative cases, composition
packages/cli/test/compile-artifact-packages.e2e.test.ts4 pins on the artifact os build actually writes
scripts/check-stack-collection-maps.mjspackages is not a metadata collection — see below
packages/spec/api-surface/root.json, export-origins/root.jsonregenerated (additive: two entries)
.changeset/…@objectstack/spec: minor

os compile / os build: no source change was needed, and that was measured

The card names compile.ts:271 (parse) and :596 (write). Both anchors verified. No CLI source change is required: normalizeStackInput, lowerCallables and 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 TOTAL Record keyed on every ObjectStackDefinition key, 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. ⚠️ This does not repair manifest:'s deliberate pick-one semantics — a composeStacks preserve 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-level z.array(SomeSchema) as a metadata collection and reconciles it against eight enumerating sites. packages matches that shape and is not a metadata collection: no singular metadata-type name, no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no registerInMemory kind, 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, reasoned NON_COLLECTION_ARRAY_KEYS exception (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.

  • Resolution: the subject test imports './stack.zod', a package-relative path, so it resolves to source, not through exportsdist. No rebuild leg is required for this ablation to be real, and a stale dist cannot make it falsely green.
  • Mutation confirmed on disk, both directions counted before reading anything: wrapper form remaining 0 (expected 0), flattened form present 1 (expected 1), plus a non-empty git diff --stat.
  • Result: exit 1 — 6 failed / 11 passed of 17. Red: both read-branch acceptors, the engines.protocol carrier, 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.
  • Restore confirmed by bytes, not by exit code:git checkout HEAD -- ABSOLUTE-PATH (naming HEAD, never a bare --), then git diff HEAD empty and git hash-object equal to the HEAD blob — both 374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried a trap … EXIT INT TERM.

Verification — all on the final commit a16839529

  • Every runnable step of lint.yml's Lint & Repo Gates job, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caught check:spec-parsed-alias (ADR-0122), which the path derivation did not name — the new alias needed an ArtifactPackageEntryParsed because ManifestSchema applies defaults, so z.input and z.infer genuinely 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-completeness exits 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 MET against a partially built tree; after turbo run build --filter='./packages/*' --filter='./packages/*/*' (what lint.yml does), check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt and check:type-check-coverage all re-ran green.
  • packages/spec full 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. ⚠️ An earlier reading showed 66 — every one a TS7016/TS7006 cascade from declarations my own OS_SKIP_DTS=1 closure 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.
  • Generated artifacts fresh: pnpm --filter @objectstack/spec check:generated green (api-surface/ and export-origins/ regenerated from a full dts build, +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/cli grade, 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/spec bump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registration green).

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:240 is manifest: ManifestSchema.optional(); compile.ts:271 is the ObjectStackDefinitionSchema.safeParse; compile.ts:596 is JSON.stringify(finalBundle, null, 2) with the write on :597; build.ts is a six-line subclass of Compile; ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.

Generated by Claude Code


Generated by Claude Code

… 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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/environment-routing.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx(via com.example.crm (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/concepts/north-star.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/kernel/services-checklist.mdx(via com.example.crm (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/releases/v17.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 9dd022aea05251a8bc8ee2161bd93931abd031b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd — the merge of head a1683952929e20211d7c3294fa84d8ee46f7fdfe into base 9dd022aea05251a8bc8ee2161bd93931abd031b9, 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 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

⚠️ 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 9dd022aea05251a8bc8ee2161bd93931abd031b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Independent contract re-review (Clause ②) — PASS (second, independent verdict)

Reviewed at head a1683952929e20211d7c3294fa84d8ee46f7fdfe by the director seat under the maintainer's direct summons of 2026-09-01, in-session at tier (machine-read fuse: session_context.model and last_served_model both equal CONTRACT_REVIEW_TIER). This seat wrote none of this diff and read neither the dispatch order nor the prior verdict's reasoning before deriving its own — the findings below are independently derived from the diff, the tests, ADR-0130 on origin/main, and the CI record.

Both limbs YES (path: packages/spec/src/stack.zod.ts; content: public-surface widening). Derived judgments, each verified against the artifact itself:

  1. ADR-0130 D4 fidelity, clause by clause: optional packages with manifest retained and the read-both rule stated at the schema (§D4 verbatim); the wrapper reservation exactly as the ADR words it ("each entry … is an object, never a bare inlined manifest body"), enforced by strictObject with a teaching history message; forward compatibility riding manifest.engines.protocol with a pin, no new negotiation mechanism; segmented loading, D5 ordering (ADR-0130 D5+D7:装载路径按拓扑序注册物内 N 包(复用 resolvePluginOrder)+ 存量产物逐位相同测试 #14162), D1/D3 gate (ADR-0130 D1+D3:installPackage 命名空间闸认同物共同所有者 + 安装期对象名唯一性检查(同 PR 机器不可拆) #14163) and D8 owner-field all untouched — every Non-goal held.
  2. Zero-regression argument is structural, not asserted: the top-level schema was already strict, so no existing artifact can carry a packages key — nothing that parsed before parses differently now. The .default([]) near-miss is pinned twice (schema-level exact key set; e2e on the artifact os build writes).
  3. Composition: COMPOSE_KEY_DISPOSITIONS packages: 'concat' is the right rule for the right reason; manifest's pick-one semantics deliberately untouched with a pin keeping ADR-0130 follow-up row 3 visible.
  4. check-stack-collection-maps exception is a classification fix, not a gate weakening: packages genuinely is not a metadata collection (no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no register kind); the exception is a named list with its own self-test pin, and the gate's reference set is unchanged at 31.
  5. Changeset: @objectstack/specminor, no BREAKING banner, no ADR-0087 disposition owed — correct for a pure widening; the absent @objectstack/cli grade is backed by the measured zero-source-line pass-through (e2e on real compiles).
  6. Docs drift advisory audited: none of the six flagged pages makes a closed top-level-key enumeration claim; nothing is falsified by an additive optional key.
  7. CI at head: 39 check runs, all success or conditional-skip; check-governed-merges --test 0 of 7 paths governed. The ablation's collateral finding (ManifestSchema strips unknown keys) is correctly filed as ManifestSchema strips unknown keys silently — a namespace typo inside manifest: parses green and the namespace comes out undefined #14192, not folded in.

⚠️ Procedural record — why this second review exists. The PASS at comment 5493145799 on #14161 was rendered by the session that implemented this diff, reviewing its own product, and it cleared the carrier label single-sided. The in-seat review path (2026-08-31 ruling) covers a tier seat reviewing a lower-tier implementer's increment — "审的是低档实现者的契约增量,非自身产物" — and the independence requirement exists precisely for this case. The substance of that verdict is confirmed correct by this independent pass; the process gap is recorded separately as a finding card, and the label is not re-hung since the two verdicts agree and this review completes the independence the gate wanted.

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

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 12:57
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit e621291Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-14161-artifact-packages-schema branch September 1, 2026 13:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130 D4:产物 schema 增加可选 packages[] 列表,两种形态都读,分段加载键位结构性预留

3 participants

@os-sam@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema
Sep 1, 2026
Merged

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4)#14191
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#14161 — epic #14122, implementing ADR-0130 D4 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md).

ObjectStackDefinitionSchema gains an optionalpackages key 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: packages present → iterate; packages absent → treat manifest as 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-review is 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 under manifest:, 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/integrity would have to be bolted onto the shared ManifestSchema and 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: ManifestSchema is not strict. Under the flattened shape, { ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is a strictObject, 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 existing manifest.engines.protocol mechanism (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); the installPackage co-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

FileWhy
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema (+ its …Parsed alias, ADR-0122) and the optional packages key; a COMPOSE_KEY_DISPOSITIONS entry
packages/spec/src/stack-artifact-packages.test.ts17 pins — both read branches, the reservation, the negative cases, composition
packages/cli/test/compile-artifact-packages.e2e.test.ts4 pins on the artifact os build actually writes
scripts/check-stack-collection-maps.mjspackages is not a metadata collection — see below
packages/spec/api-surface/root.json, export-origins/root.jsonregenerated (additive: two entries)
.changeset/…@objectstack/spec: minor

os compile / os build: no source change was needed, and that was measured

The card names compile.ts:271 (parse) and :596 (write). Both anchors verified. No CLI source change is required: normalizeStackInput, lowerCallables and 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 TOTAL Record keyed on every ObjectStackDefinition key, 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. ⚠️ This does not repair manifest:'s deliberate pick-one semantics — a composeStacks preserve 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-level z.array(SomeSchema) as a metadata collection and reconciles it against eight enumerating sites. packages matches that shape and is not a metadata collection: no singular metadata-type name, no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no registerInMemory kind, 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, reasoned NON_COLLECTION_ARRAY_KEYS exception (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.

  • Resolution: the subject test imports './stack.zod', a package-relative path, so it resolves to source, not through exportsdist. No rebuild leg is required for this ablation to be real, and a stale dist cannot make it falsely green.
  • Mutation confirmed on disk, both directions counted before reading anything: wrapper form remaining 0 (expected 0), flattened form present 1 (expected 1), plus a non-empty git diff --stat.
  • Result: exit 1 — 6 failed / 11 passed of 17. Red: both read-branch acceptors, the engines.protocol carrier, 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.
  • Restore confirmed by bytes, not by exit code:git checkout HEAD -- ABSOLUTE-PATH (naming HEAD, never a bare --), then git diff HEAD empty and git hash-object equal to the HEAD blob — both 374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried a trap … EXIT INT TERM.

Verification — all on the final commit a16839529

  • Every runnable step of lint.yml's Lint & Repo Gates job, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caught check:spec-parsed-alias (ADR-0122), which the path derivation did not name — the new alias needed an ArtifactPackageEntryParsed because ManifestSchema applies defaults, so z.input and z.infer genuinely 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-completeness exits 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 MET against a partially built tree; after turbo run build --filter='./packages/*' --filter='./packages/*/*' (what lint.yml does), check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt and check:type-check-coverage all re-ran green.
  • packages/spec full 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. ⚠️ An earlier reading showed 66 — every one a TS7016/TS7006 cascade from declarations my own OS_SKIP_DTS=1 closure 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.
  • Generated artifacts fresh: pnpm --filter @objectstack/spec check:generated green (api-surface/ and export-origins/ regenerated from a full dts build, +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/cli grade, 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/spec bump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registration green).

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:240 is manifest: ManifestSchema.optional(); compile.ts:271 is the ObjectStackDefinitionSchema.safeParse; compile.ts:596 is JSON.stringify(finalBundle, null, 2) with the write on :597; build.ts is a six-line subclass of Compile; ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.

Generated by Claude Code


Generated by Claude Code

… 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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/environment-routing.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx(via com.example.crm (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/concepts/north-star.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/kernel/services-checklist.mdx(via com.example.crm (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/releases/v17.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 9dd022aea05251a8bc8ee2161bd93931abd031b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd — the merge of head a1683952929e20211d7c3294fa84d8ee46f7fdfe into base 9dd022aea05251a8bc8ee2161bd93931abd031b9, 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 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

⚠️ 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 9dd022aea05251a8bc8ee2161bd93931abd031b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Independent contract re-review (Clause ②) — PASS (second, independent verdict)

Reviewed at head a1683952929e20211d7c3294fa84d8ee46f7fdfe by the director seat under the maintainer's direct summons of 2026-09-01, in-session at tier (machine-read fuse: session_context.model and last_served_model both equal CONTRACT_REVIEW_TIER). This seat wrote none of this diff and read neither the dispatch order nor the prior verdict's reasoning before deriving its own — the findings below are independently derived from the diff, the tests, ADR-0130 on origin/main, and the CI record.

Both limbs YES (path: packages/spec/src/stack.zod.ts; content: public-surface widening). Derived judgments, each verified against the artifact itself:

  1. ADR-0130 D4 fidelity, clause by clause: optional packages with manifest retained and the read-both rule stated at the schema (§D4 verbatim); the wrapper reservation exactly as the ADR words it ("each entry … is an object, never a bare inlined manifest body"), enforced by strictObject with a teaching history message; forward compatibility riding manifest.engines.protocol with a pin, no new negotiation mechanism; segmented loading, D5 ordering (ADR-0130 D5+D7:装载路径按拓扑序注册物内 N 包(复用 resolvePluginOrder)+ 存量产物逐位相同测试 #14162), D1/D3 gate (ADR-0130 D1+D3:installPackage 命名空间闸认同物共同所有者 + 安装期对象名唯一性检查(同 PR 机器不可拆) #14163) and D8 owner-field all untouched — every Non-goal held.
  2. Zero-regression argument is structural, not asserted: the top-level schema was already strict, so no existing artifact can carry a packages key — nothing that parsed before parses differently now. The .default([]) near-miss is pinned twice (schema-level exact key set; e2e on the artifact os build writes).
  3. Composition: COMPOSE_KEY_DISPOSITIONS packages: 'concat' is the right rule for the right reason; manifest's pick-one semantics deliberately untouched with a pin keeping ADR-0130 follow-up row 3 visible.
  4. check-stack-collection-maps exception is a classification fix, not a gate weakening: packages genuinely is not a metadata collection (no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no register kind); the exception is a named list with its own self-test pin, and the gate's reference set is unchanged at 31.
  5. Changeset: @objectstack/specminor, no BREAKING banner, no ADR-0087 disposition owed — correct for a pure widening; the absent @objectstack/cli grade is backed by the measured zero-source-line pass-through (e2e on real compiles).
  6. Docs drift advisory audited: none of the six flagged pages makes a closed top-level-key enumeration claim; nothing is falsified by an additive optional key.
  7. CI at head: 39 check runs, all success or conditional-skip; check-governed-merges --test 0 of 7 paths governed. The ablation's collateral finding (ManifestSchema strips unknown keys) is correctly filed as ManifestSchema strips unknown keys silently — a namespace typo inside manifest: parses green and the namespace comes out undefined #14192, not folded in.

⚠️ Procedural record — why this second review exists. The PASS at comment 5493145799 on #14161 was rendered by the session that implemented this diff, reviewing its own product, and it cleared the carrier label single-sided. The in-seat review path (2026-08-31 ruling) covers a tier seat reviewing a lower-tier implementer's increment — "审的是低档实现者的契约增量,非自身产物" — and the independence requirement exists precisely for this case. The substance of that verdict is confirmed correct by this independent pass; the process gap is recorded separately as a finding card, and the label is not re-hung since the two verdicts agree and this review completes the independence the gate wanted.

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

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 12:57
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit e621291Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-14161-artifact-packages-schema branch September 1, 2026 13:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130 D4:产物 schema 增加可选 packages[] 列表,两种形态都读,分段加载键位结构性预留

3 participants

@os-sam@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema
Sep 1, 2026
Merged

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4)#14191
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#14161 — epic #14122, implementing ADR-0130 D4 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md).

ObjectStackDefinitionSchema gains an optionalpackages key 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: packages present → iterate; packages absent → treat manifest as 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-review is 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 under manifest:, 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/integrity would have to be bolted onto the shared ManifestSchema and 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: ManifestSchema is not strict. Under the flattened shape, { ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is a strictObject, 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 existing manifest.engines.protocol mechanism (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); the installPackage co-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

FileWhy
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema (+ its …Parsed alias, ADR-0122) and the optional packages key; a COMPOSE_KEY_DISPOSITIONS entry
packages/spec/src/stack-artifact-packages.test.ts17 pins — both read branches, the reservation, the negative cases, composition
packages/cli/test/compile-artifact-packages.e2e.test.ts4 pins on the artifact os build actually writes
scripts/check-stack-collection-maps.mjspackages is not a metadata collection — see below
packages/spec/api-surface/root.json, export-origins/root.jsonregenerated (additive: two entries)
.changeset/…@objectstack/spec: minor

os compile / os build: no source change was needed, and that was measured

The card names compile.ts:271 (parse) and :596 (write). Both anchors verified. No CLI source change is required: normalizeStackInput, lowerCallables and 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 TOTAL Record keyed on every ObjectStackDefinition key, 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. ⚠️ This does not repair manifest:'s deliberate pick-one semantics — a composeStacks preserve 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-level z.array(SomeSchema) as a metadata collection and reconciles it against eight enumerating sites. packages matches that shape and is not a metadata collection: no singular metadata-type name, no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no registerInMemory kind, 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, reasoned NON_COLLECTION_ARRAY_KEYS exception (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.

  • Resolution: the subject test imports './stack.zod', a package-relative path, so it resolves to source, not through exportsdist. No rebuild leg is required for this ablation to be real, and a stale dist cannot make it falsely green.
  • Mutation confirmed on disk, both directions counted before reading anything: wrapper form remaining 0 (expected 0), flattened form present 1 (expected 1), plus a non-empty git diff --stat.
  • Result: exit 1 — 6 failed / 11 passed of 17. Red: both read-branch acceptors, the engines.protocol carrier, 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.
  • Restore confirmed by bytes, not by exit code:git checkout HEAD -- ABSOLUTE-PATH (naming HEAD, never a bare --), then git diff HEAD empty and git hash-object equal to the HEAD blob — both 374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried a trap … EXIT INT TERM.

Verification — all on the final commit a16839529

  • Every runnable step of lint.yml's Lint & Repo Gates job, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caught check:spec-parsed-alias (ADR-0122), which the path derivation did not name — the new alias needed an ArtifactPackageEntryParsed because ManifestSchema applies defaults, so z.input and z.infer genuinely 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-completeness exits 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 MET against a partially built tree; after turbo run build --filter='./packages/*' --filter='./packages/*/*' (what lint.yml does), check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt and check:type-check-coverage all re-ran green.
  • packages/spec full 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. ⚠️ An earlier reading showed 66 — every one a TS7016/TS7006 cascade from declarations my own OS_SKIP_DTS=1 closure 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.
  • Generated artifacts fresh: pnpm --filter @objectstack/spec check:generated green (api-surface/ and export-origins/ regenerated from a full dts build, +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/cli grade, 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/spec bump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registration green).

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:240 is manifest: ManifestSchema.optional(); compile.ts:271 is the ObjectStackDefinitionSchema.safeParse; compile.ts:596 is JSON.stringify(finalBundle, null, 2) with the write on :597; build.ts is a six-line subclass of Compile; ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.

Generated by Claude Code


Generated by Claude Code

… 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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/environment-routing.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx(via com.example.crm (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/concepts/north-star.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/kernel/services-checklist.mdx(via com.example.crm (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/releases/v17.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 9dd022aea05251a8bc8ee2161bd93931abd031b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd — the merge of head a1683952929e20211d7c3294fa84d8ee46f7fdfe into base 9dd022aea05251a8bc8ee2161bd93931abd031b9, 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 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

⚠️ 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 9dd022aea05251a8bc8ee2161bd93931abd031b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Independent contract re-review (Clause ②) — PASS (second, independent verdict)

Reviewed at head a1683952929e20211d7c3294fa84d8ee46f7fdfe by the director seat under the maintainer's direct summons of 2026-09-01, in-session at tier (machine-read fuse: session_context.model and last_served_model both equal CONTRACT_REVIEW_TIER). This seat wrote none of this diff and read neither the dispatch order nor the prior verdict's reasoning before deriving its own — the findings below are independently derived from the diff, the tests, ADR-0130 on origin/main, and the CI record.

Both limbs YES (path: packages/spec/src/stack.zod.ts; content: public-surface widening). Derived judgments, each verified against the artifact itself:

  1. ADR-0130 D4 fidelity, clause by clause: optional packages with manifest retained and the read-both rule stated at the schema (§D4 verbatim); the wrapper reservation exactly as the ADR words it ("each entry … is an object, never a bare inlined manifest body"), enforced by strictObject with a teaching history message; forward compatibility riding manifest.engines.protocol with a pin, no new negotiation mechanism; segmented loading, D5 ordering (ADR-0130 D5+D7:装载路径按拓扑序注册物内 N 包(复用 resolvePluginOrder)+ 存量产物逐位相同测试 #14162), D1/D3 gate (ADR-0130 D1+D3:installPackage 命名空间闸认同物共同所有者 + 安装期对象名唯一性检查(同 PR 机器不可拆) #14163) and D8 owner-field all untouched — every Non-goal held.
  2. Zero-regression argument is structural, not asserted: the top-level schema was already strict, so no existing artifact can carry a packages key — nothing that parsed before parses differently now. The .default([]) near-miss is pinned twice (schema-level exact key set; e2e on the artifact os build writes).
  3. Composition: COMPOSE_KEY_DISPOSITIONS packages: 'concat' is the right rule for the right reason; manifest's pick-one semantics deliberately untouched with a pin keeping ADR-0130 follow-up row 3 visible.
  4. check-stack-collection-maps exception is a classification fix, not a gate weakening: packages genuinely is not a metadata collection (no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no register kind); the exception is a named list with its own self-test pin, and the gate's reference set is unchanged at 31.
  5. Changeset: @objectstack/specminor, no BREAKING banner, no ADR-0087 disposition owed — correct for a pure widening; the absent @objectstack/cli grade is backed by the measured zero-source-line pass-through (e2e on real compiles).
  6. Docs drift advisory audited: none of the six flagged pages makes a closed top-level-key enumeration claim; nothing is falsified by an additive optional key.
  7. CI at head: 39 check runs, all success or conditional-skip; check-governed-merges --test 0 of 7 paths governed. The ablation's collateral finding (ManifestSchema strips unknown keys) is correctly filed as ManifestSchema strips unknown keys silently — a namespace typo inside manifest: parses green and the namespace comes out undefined #14192, not folded in.

⚠️ Procedural record — why this second review exists. The PASS at comment 5493145799 on #14161 was rendered by the session that implemented this diff, reviewing its own product, and it cleared the carrier label single-sided. The in-seat review path (2026-08-31 ruling) covers a tier seat reviewing a lower-tier implementer's increment — "审的是低档实现者的契约增量,非自身产物" — and the independence requirement exists precisely for this case. The substance of that verdict is confirmed correct by this independent pass; the process gap is recorded separately as a finding card, and the label is not re-hung since the two verdicts agree and this review completes the independence the gate wanted.

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

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 12:57
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit e621291Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-14161-artifact-packages-schema branch September 1, 2026 13:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130 D4:产物 schema 增加可选 packages[] 列表,两种形态都读,分段加载键位结构性预留

3 participants

@os-sam@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema
Sep 1, 2026
Merged

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4)#14191
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#14161 — epic #14122, implementing ADR-0130 D4 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md).

ObjectStackDefinitionSchema gains an optionalpackages key 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: packages present → iterate; packages absent → treat manifest as 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-review is 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 under manifest:, 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/integrity would have to be bolted onto the shared ManifestSchema and 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: ManifestSchema is not strict. Under the flattened shape, { ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is a strictObject, 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 existing manifest.engines.protocol mechanism (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); the installPackage co-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

FileWhy
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema (+ its …Parsed alias, ADR-0122) and the optional packages key; a COMPOSE_KEY_DISPOSITIONS entry
packages/spec/src/stack-artifact-packages.test.ts17 pins — both read branches, the reservation, the negative cases, composition
packages/cli/test/compile-artifact-packages.e2e.test.ts4 pins on the artifact os build actually writes
scripts/check-stack-collection-maps.mjspackages is not a metadata collection — see below
packages/spec/api-surface/root.json, export-origins/root.jsonregenerated (additive: two entries)
.changeset/…@objectstack/spec: minor

os compile / os build: no source change was needed, and that was measured

The card names compile.ts:271 (parse) and :596 (write). Both anchors verified. No CLI source change is required: normalizeStackInput, lowerCallables and 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 TOTAL Record keyed on every ObjectStackDefinition key, 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. ⚠️ This does not repair manifest:'s deliberate pick-one semantics — a composeStacks preserve 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-level z.array(SomeSchema) as a metadata collection and reconciles it against eight enumerating sites. packages matches that shape and is not a metadata collection: no singular metadata-type name, no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no registerInMemory kind, 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, reasoned NON_COLLECTION_ARRAY_KEYS exception (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.

  • Resolution: the subject test imports './stack.zod', a package-relative path, so it resolves to source, not through exportsdist. No rebuild leg is required for this ablation to be real, and a stale dist cannot make it falsely green.
  • Mutation confirmed on disk, both directions counted before reading anything: wrapper form remaining 0 (expected 0), flattened form present 1 (expected 1), plus a non-empty git diff --stat.
  • Result: exit 1 — 6 failed / 11 passed of 17. Red: both read-branch acceptors, the engines.protocol carrier, 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.
  • Restore confirmed by bytes, not by exit code:git checkout HEAD -- ABSOLUTE-PATH (naming HEAD, never a bare --), then git diff HEAD empty and git hash-object equal to the HEAD blob — both 374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried a trap … EXIT INT TERM.

Verification — all on the final commit a16839529

  • Every runnable step of lint.yml's Lint & Repo Gates job, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caught check:spec-parsed-alias (ADR-0122), which the path derivation did not name — the new alias needed an ArtifactPackageEntryParsed because ManifestSchema applies defaults, so z.input and z.infer genuinely 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-completeness exits 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 MET against a partially built tree; after turbo run build --filter='./packages/*' --filter='./packages/*/*' (what lint.yml does), check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt and check:type-check-coverage all re-ran green.
  • packages/spec full 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. ⚠️ An earlier reading showed 66 — every one a TS7016/TS7006 cascade from declarations my own OS_SKIP_DTS=1 closure 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.
  • Generated artifacts fresh: pnpm --filter @objectstack/spec check:generated green (api-surface/ and export-origins/ regenerated from a full dts build, +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/cli grade, 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/spec bump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registration green).

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:240 is manifest: ManifestSchema.optional(); compile.ts:271 is the ObjectStackDefinitionSchema.safeParse; compile.ts:596 is JSON.stringify(finalBundle, null, 2) with the write on :597; build.ts is a six-line subclass of Compile; ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.

Generated by Claude Code


Generated by Claude Code

… 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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/environment-routing.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx(via com.example.crm (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/concepts/north-star.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/kernel/services-checklist.mdx(via com.example.crm (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/releases/v17.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 9dd022aea05251a8bc8ee2161bd93931abd031b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd — the merge of head a1683952929e20211d7c3294fa84d8ee46f7fdfe into base 9dd022aea05251a8bc8ee2161bd93931abd031b9, 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 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

⚠️ 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 9dd022aea05251a8bc8ee2161bd93931abd031b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Independent contract re-review (Clause ②) — PASS (second, independent verdict)

Reviewed at head a1683952929e20211d7c3294fa84d8ee46f7fdfe by the director seat under the maintainer's direct summons of 2026-09-01, in-session at tier (machine-read fuse: session_context.model and last_served_model both equal CONTRACT_REVIEW_TIER). This seat wrote none of this diff and read neither the dispatch order nor the prior verdict's reasoning before deriving its own — the findings below are independently derived from the diff, the tests, ADR-0130 on origin/main, and the CI record.

Both limbs YES (path: packages/spec/src/stack.zod.ts; content: public-surface widening). Derived judgments, each verified against the artifact itself:

  1. ADR-0130 D4 fidelity, clause by clause: optional packages with manifest retained and the read-both rule stated at the schema (§D4 verbatim); the wrapper reservation exactly as the ADR words it ("each entry … is an object, never a bare inlined manifest body"), enforced by strictObject with a teaching history message; forward compatibility riding manifest.engines.protocol with a pin, no new negotiation mechanism; segmented loading, D5 ordering (ADR-0130 D5+D7:装载路径按拓扑序注册物内 N 包(复用 resolvePluginOrder)+ 存量产物逐位相同测试 #14162), D1/D3 gate (ADR-0130 D1+D3:installPackage 命名空间闸认同物共同所有者 + 安装期对象名唯一性检查(同 PR 机器不可拆) #14163) and D8 owner-field all untouched — every Non-goal held.
  2. Zero-regression argument is structural, not asserted: the top-level schema was already strict, so no existing artifact can carry a packages key — nothing that parsed before parses differently now. The .default([]) near-miss is pinned twice (schema-level exact key set; e2e on the artifact os build writes).
  3. Composition: COMPOSE_KEY_DISPOSITIONS packages: 'concat' is the right rule for the right reason; manifest's pick-one semantics deliberately untouched with a pin keeping ADR-0130 follow-up row 3 visible.
  4. check-stack-collection-maps exception is a classification fix, not a gate weakening: packages genuinely is not a metadata collection (no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no register kind); the exception is a named list with its own self-test pin, and the gate's reference set is unchanged at 31.
  5. Changeset: @objectstack/specminor, no BREAKING banner, no ADR-0087 disposition owed — correct for a pure widening; the absent @objectstack/cli grade is backed by the measured zero-source-line pass-through (e2e on real compiles).
  6. Docs drift advisory audited: none of the six flagged pages makes a closed top-level-key enumeration claim; nothing is falsified by an additive optional key.
  7. CI at head: 39 check runs, all success or conditional-skip; check-governed-merges --test 0 of 7 paths governed. The ablation's collateral finding (ManifestSchema strips unknown keys) is correctly filed as ManifestSchema strips unknown keys silently — a namespace typo inside manifest: parses green and the namespace comes out undefined #14192, not folded in.

⚠️ Procedural record — why this second review exists. The PASS at comment 5493145799 on #14161 was rendered by the session that implemented this diff, reviewing its own product, and it cleared the carrier label single-sided. The in-seat review path (2026-08-31 ruling) covers a tier seat reviewing a lower-tier implementer's increment — "审的是低档实现者的契约增量,非自身产物" — and the independence requirement exists precisely for this case. The substance of that verdict is confirmed correct by this independent pass; the process gap is recorded separately as a finding card, and the label is not re-hung since the two verdicts agree and this review completes the independence the gate wanted.

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

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 12:57
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit e621291Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-14161-artifact-packages-schema branch September 1, 2026 13:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130 D4:产物 schema 增加可选 packages[] 列表,两种形态都读,分段加载键位结构性预留

3 participants

@os-sam@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema
Sep 1, 2026
Merged

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4)#14191
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#14161 — epic #14122, implementing ADR-0130 D4 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md).

ObjectStackDefinitionSchema gains an optionalpackages key 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: packages present → iterate; packages absent → treat manifest as 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-review is 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 under manifest:, 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/integrity would have to be bolted onto the shared ManifestSchema and 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: ManifestSchema is not strict. Under the flattened shape, { ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is a strictObject, 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 existing manifest.engines.protocol mechanism (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); the installPackage co-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

FileWhy
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema (+ its …Parsed alias, ADR-0122) and the optional packages key; a COMPOSE_KEY_DISPOSITIONS entry
packages/spec/src/stack-artifact-packages.test.ts17 pins — both read branches, the reservation, the negative cases, composition
packages/cli/test/compile-artifact-packages.e2e.test.ts4 pins on the artifact os build actually writes
scripts/check-stack-collection-maps.mjspackages is not a metadata collection — see below
packages/spec/api-surface/root.json, export-origins/root.jsonregenerated (additive: two entries)
.changeset/…@objectstack/spec: minor

os compile / os build: no source change was needed, and that was measured

The card names compile.ts:271 (parse) and :596 (write). Both anchors verified. No CLI source change is required: normalizeStackInput, lowerCallables and 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 TOTAL Record keyed on every ObjectStackDefinition key, 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. ⚠️ This does not repair manifest:'s deliberate pick-one semantics — a composeStacks preserve 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-level z.array(SomeSchema) as a metadata collection and reconciles it against eight enumerating sites. packages matches that shape and is not a metadata collection: no singular metadata-type name, no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no registerInMemory kind, 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, reasoned NON_COLLECTION_ARRAY_KEYS exception (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.

  • Resolution: the subject test imports './stack.zod', a package-relative path, so it resolves to source, not through exportsdist. No rebuild leg is required for this ablation to be real, and a stale dist cannot make it falsely green.
  • Mutation confirmed on disk, both directions counted before reading anything: wrapper form remaining 0 (expected 0), flattened form present 1 (expected 1), plus a non-empty git diff --stat.
  • Result: exit 1 — 6 failed / 11 passed of 17. Red: both read-branch acceptors, the engines.protocol carrier, 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.
  • Restore confirmed by bytes, not by exit code:git checkout HEAD -- ABSOLUTE-PATH (naming HEAD, never a bare --), then git diff HEAD empty and git hash-object equal to the HEAD blob — both 374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried a trap … EXIT INT TERM.

Verification — all on the final commit a16839529

  • Every runnable step of lint.yml's Lint & Repo Gates job, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caught check:spec-parsed-alias (ADR-0122), which the path derivation did not name — the new alias needed an ArtifactPackageEntryParsed because ManifestSchema applies defaults, so z.input and z.infer genuinely 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-completeness exits 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 MET against a partially built tree; after turbo run build --filter='./packages/*' --filter='./packages/*/*' (what lint.yml does), check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt and check:type-check-coverage all re-ran green.
  • packages/spec full 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. ⚠️ An earlier reading showed 66 — every one a TS7016/TS7006 cascade from declarations my own OS_SKIP_DTS=1 closure 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.
  • Generated artifacts fresh: pnpm --filter @objectstack/spec check:generated green (api-surface/ and export-origins/ regenerated from a full dts build, +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/cli grade, 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/spec bump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registration green).

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:240 is manifest: ManifestSchema.optional(); compile.ts:271 is the ObjectStackDefinitionSchema.safeParse; compile.ts:596 is JSON.stringify(finalBundle, null, 2) with the write on :597; build.ts is a six-line subclass of Compile; ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.

Generated by Claude Code


Generated by Claude Code

… 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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/environment-routing.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx(via com.example.crm (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/concepts/north-star.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/kernel/services-checklist.mdx(via com.example.crm (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/releases/v17.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 9dd022aea05251a8bc8ee2161bd93931abd031b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd — the merge of head a1683952929e20211d7c3294fa84d8ee46f7fdfe into base 9dd022aea05251a8bc8ee2161bd93931abd031b9, 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 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

⚠️ 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 9dd022aea05251a8bc8ee2161bd93931abd031b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Independent contract re-review (Clause ②) — PASS (second, independent verdict)

Reviewed at head a1683952929e20211d7c3294fa84d8ee46f7fdfe by the director seat under the maintainer's direct summons of 2026-09-01, in-session at tier (machine-read fuse: session_context.model and last_served_model both equal CONTRACT_REVIEW_TIER). This seat wrote none of this diff and read neither the dispatch order nor the prior verdict's reasoning before deriving its own — the findings below are independently derived from the diff, the tests, ADR-0130 on origin/main, and the CI record.

Both limbs YES (path: packages/spec/src/stack.zod.ts; content: public-surface widening). Derived judgments, each verified against the artifact itself:

  1. ADR-0130 D4 fidelity, clause by clause: optional packages with manifest retained and the read-both rule stated at the schema (§D4 verbatim); the wrapper reservation exactly as the ADR words it ("each entry … is an object, never a bare inlined manifest body"), enforced by strictObject with a teaching history message; forward compatibility riding manifest.engines.protocol with a pin, no new negotiation mechanism; segmented loading, D5 ordering (ADR-0130 D5+D7:装载路径按拓扑序注册物内 N 包(复用 resolvePluginOrder)+ 存量产物逐位相同测试 #14162), D1/D3 gate (ADR-0130 D1+D3:installPackage 命名空间闸认同物共同所有者 + 安装期对象名唯一性检查(同 PR 机器不可拆) #14163) and D8 owner-field all untouched — every Non-goal held.
  2. Zero-regression argument is structural, not asserted: the top-level schema was already strict, so no existing artifact can carry a packages key — nothing that parsed before parses differently now. The .default([]) near-miss is pinned twice (schema-level exact key set; e2e on the artifact os build writes).
  3. Composition: COMPOSE_KEY_DISPOSITIONS packages: 'concat' is the right rule for the right reason; manifest's pick-one semantics deliberately untouched with a pin keeping ADR-0130 follow-up row 3 visible.
  4. check-stack-collection-maps exception is a classification fix, not a gate weakening: packages genuinely is not a metadata collection (no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no register kind); the exception is a named list with its own self-test pin, and the gate's reference set is unchanged at 31.
  5. Changeset: @objectstack/specminor, no BREAKING banner, no ADR-0087 disposition owed — correct for a pure widening; the absent @objectstack/cli grade is backed by the measured zero-source-line pass-through (e2e on real compiles).
  6. Docs drift advisory audited: none of the six flagged pages makes a closed top-level-key enumeration claim; nothing is falsified by an additive optional key.
  7. CI at head: 39 check runs, all success or conditional-skip; check-governed-merges --test 0 of 7 paths governed. The ablation's collateral finding (ManifestSchema strips unknown keys) is correctly filed as ManifestSchema strips unknown keys silently — a namespace typo inside manifest: parses green and the namespace comes out undefined #14192, not folded in.

⚠️ Procedural record — why this second review exists. The PASS at comment 5493145799 on #14161 was rendered by the session that implemented this diff, reviewing its own product, and it cleared the carrier label single-sided. The in-seat review path (2026-08-31 ruling) covers a tier seat reviewing a lower-tier implementer's increment — "审的是低档实现者的契约增量,非自身产物" — and the independence requirement exists precisely for this case. The substance of that verdict is confirmed correct by this independent pass; the process gap is recorded separately as a finding card, and the label is not re-hung since the two verdicts agree and this review completes the independence the gate wanted.

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

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 12:57
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit e621291Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-14161-artifact-packages-schema branch September 1, 2026 13:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130 D4:产物 schema 增加可选 packages[] 列表,两种形态都读,分段加载键位结构性预留

3 participants

@os-sam@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema
Sep 1, 2026
Merged

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4)#14191
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#14161 — epic #14122, implementing ADR-0130 D4 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md).

ObjectStackDefinitionSchema gains an optionalpackages key 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: packages present → iterate; packages absent → treat manifest as 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-review is 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 under manifest:, 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/integrity would have to be bolted onto the shared ManifestSchema and 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: ManifestSchema is not strict. Under the flattened shape, { ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is a strictObject, 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 existing manifest.engines.protocol mechanism (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); the installPackage co-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

FileWhy
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema (+ its …Parsed alias, ADR-0122) and the optional packages key; a COMPOSE_KEY_DISPOSITIONS entry
packages/spec/src/stack-artifact-packages.test.ts17 pins — both read branches, the reservation, the negative cases, composition
packages/cli/test/compile-artifact-packages.e2e.test.ts4 pins on the artifact os build actually writes
scripts/check-stack-collection-maps.mjspackages is not a metadata collection — see below
packages/spec/api-surface/root.json, export-origins/root.jsonregenerated (additive: two entries)
.changeset/…@objectstack/spec: minor

os compile / os build: no source change was needed, and that was measured

The card names compile.ts:271 (parse) and :596 (write). Both anchors verified. No CLI source change is required: normalizeStackInput, lowerCallables and 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 TOTAL Record keyed on every ObjectStackDefinition key, 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. ⚠️ This does not repair manifest:'s deliberate pick-one semantics — a composeStacks preserve 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-level z.array(SomeSchema) as a metadata collection and reconciles it against eight enumerating sites. packages matches that shape and is not a metadata collection: no singular metadata-type name, no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no registerInMemory kind, 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, reasoned NON_COLLECTION_ARRAY_KEYS exception (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.

  • Resolution: the subject test imports './stack.zod', a package-relative path, so it resolves to source, not through exportsdist. No rebuild leg is required for this ablation to be real, and a stale dist cannot make it falsely green.
  • Mutation confirmed on disk, both directions counted before reading anything: wrapper form remaining 0 (expected 0), flattened form present 1 (expected 1), plus a non-empty git diff --stat.
  • Result: exit 1 — 6 failed / 11 passed of 17. Red: both read-branch acceptors, the engines.protocol carrier, 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.
  • Restore confirmed by bytes, not by exit code:git checkout HEAD -- ABSOLUTE-PATH (naming HEAD, never a bare --), then git diff HEAD empty and git hash-object equal to the HEAD blob — both 374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried a trap … EXIT INT TERM.

Verification — all on the final commit a16839529

  • Every runnable step of lint.yml's Lint & Repo Gates job, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caught check:spec-parsed-alias (ADR-0122), which the path derivation did not name — the new alias needed an ArtifactPackageEntryParsed because ManifestSchema applies defaults, so z.input and z.infer genuinely 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-completeness exits 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 MET against a partially built tree; after turbo run build --filter='./packages/*' --filter='./packages/*/*' (what lint.yml does), check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt and check:type-check-coverage all re-ran green.
  • packages/spec full 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. ⚠️ An earlier reading showed 66 — every one a TS7016/TS7006 cascade from declarations my own OS_SKIP_DTS=1 closure 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.
  • Generated artifacts fresh: pnpm --filter @objectstack/spec check:generated green (api-surface/ and export-origins/ regenerated from a full dts build, +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/cli grade, 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/spec bump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registration green).

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:240 is manifest: ManifestSchema.optional(); compile.ts:271 is the ObjectStackDefinitionSchema.safeParse; compile.ts:596 is JSON.stringify(finalBundle, null, 2) with the write on :597; build.ts is a six-line subclass of Compile; ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.

Generated by Claude Code


Generated by Claude Code

… 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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/environment-routing.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx(via com.example.crm (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/concepts/north-star.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/kernel/services-checklist.mdx(via com.example.crm (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/releases/v17.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 9dd022aea05251a8bc8ee2161bd93931abd031b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd — the merge of head a1683952929e20211d7c3294fa84d8ee46f7fdfe into base 9dd022aea05251a8bc8ee2161bd93931abd031b9, 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 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

⚠️ 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 9dd022aea05251a8bc8ee2161bd93931abd031b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Independent contract re-review (Clause ②) — PASS (second, independent verdict)

Reviewed at head a1683952929e20211d7c3294fa84d8ee46f7fdfe by the director seat under the maintainer's direct summons of 2026-09-01, in-session at tier (machine-read fuse: session_context.model and last_served_model both equal CONTRACT_REVIEW_TIER). This seat wrote none of this diff and read neither the dispatch order nor the prior verdict's reasoning before deriving its own — the findings below are independently derived from the diff, the tests, ADR-0130 on origin/main, and the CI record.

Both limbs YES (path: packages/spec/src/stack.zod.ts; content: public-surface widening). Derived judgments, each verified against the artifact itself:

  1. ADR-0130 D4 fidelity, clause by clause: optional packages with manifest retained and the read-both rule stated at the schema (§D4 verbatim); the wrapper reservation exactly as the ADR words it ("each entry … is an object, never a bare inlined manifest body"), enforced by strictObject with a teaching history message; forward compatibility riding manifest.engines.protocol with a pin, no new negotiation mechanism; segmented loading, D5 ordering (ADR-0130 D5+D7:装载路径按拓扑序注册物内 N 包(复用 resolvePluginOrder)+ 存量产物逐位相同测试 #14162), D1/D3 gate (ADR-0130 D1+D3:installPackage 命名空间闸认同物共同所有者 + 安装期对象名唯一性检查(同 PR 机器不可拆) #14163) and D8 owner-field all untouched — every Non-goal held.
  2. Zero-regression argument is structural, not asserted: the top-level schema was already strict, so no existing artifact can carry a packages key — nothing that parsed before parses differently now. The .default([]) near-miss is pinned twice (schema-level exact key set; e2e on the artifact os build writes).
  3. Composition: COMPOSE_KEY_DISPOSITIONS packages: 'concat' is the right rule for the right reason; manifest's pick-one semantics deliberately untouched with a pin keeping ADR-0130 follow-up row 3 visible.
  4. check-stack-collection-maps exception is a classification fix, not a gate weakening: packages genuinely is not a metadata collection (no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no register kind); the exception is a named list with its own self-test pin, and the gate's reference set is unchanged at 31.
  5. Changeset: @objectstack/specminor, no BREAKING banner, no ADR-0087 disposition owed — correct for a pure widening; the absent @objectstack/cli grade is backed by the measured zero-source-line pass-through (e2e on real compiles).
  6. Docs drift advisory audited: none of the six flagged pages makes a closed top-level-key enumeration claim; nothing is falsified by an additive optional key.
  7. CI at head: 39 check runs, all success or conditional-skip; check-governed-merges --test 0 of 7 paths governed. The ablation's collateral finding (ManifestSchema strips unknown keys) is correctly filed as ManifestSchema strips unknown keys silently — a namespace typo inside manifest: parses green and the namespace comes out undefined #14192, not folded in.

⚠️ Procedural record — why this second review exists. The PASS at comment 5493145799 on #14161 was rendered by the session that implemented this diff, reviewing its own product, and it cleared the carrier label single-sided. The in-seat review path (2026-08-31 ruling) covers a tier seat reviewing a lower-tier implementer's increment — "审的是低档实现者的契约增量,非自身产物" — and the independence requirement exists precisely for this case. The substance of that verdict is confirmed correct by this independent pass; the process gap is recorded separately as a finding card, and the label is not re-hung since the two verdicts agree and this review completes the independence the gate wanted.

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

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 12:57
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit e621291Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-14161-artifact-packages-schema branch September 1, 2026 13:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130 D4:产物 schema 增加可选 packages[] 列表,两种形态都读,分段加载键位结构性预留

3 participants

@os-sam@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema
Sep 1, 2026
Merged

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4)#14191
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#14161 — epic #14122, implementing ADR-0130 D4 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md).

ObjectStackDefinitionSchema gains an optionalpackages key 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: packages present → iterate; packages absent → treat manifest as 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-review is 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 under manifest:, 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/integrity would have to be bolted onto the shared ManifestSchema and 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: ManifestSchema is not strict. Under the flattened shape, { ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is a strictObject, 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 existing manifest.engines.protocol mechanism (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); the installPackage co-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

FileWhy
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema (+ its …Parsed alias, ADR-0122) and the optional packages key; a COMPOSE_KEY_DISPOSITIONS entry
packages/spec/src/stack-artifact-packages.test.ts17 pins — both read branches, the reservation, the negative cases, composition
packages/cli/test/compile-artifact-packages.e2e.test.ts4 pins on the artifact os build actually writes
scripts/check-stack-collection-maps.mjspackages is not a metadata collection — see below
packages/spec/api-surface/root.json, export-origins/root.jsonregenerated (additive: two entries)
.changeset/…@objectstack/spec: minor

os compile / os build: no source change was needed, and that was measured

The card names compile.ts:271 (parse) and :596 (write). Both anchors verified. No CLI source change is required: normalizeStackInput, lowerCallables and 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 TOTAL Record keyed on every ObjectStackDefinition key, 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. ⚠️ This does not repair manifest:'s deliberate pick-one semantics — a composeStacks preserve 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-level z.array(SomeSchema) as a metadata collection and reconciles it against eight enumerating sites. packages matches that shape and is not a metadata collection: no singular metadata-type name, no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no registerInMemory kind, 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, reasoned NON_COLLECTION_ARRAY_KEYS exception (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.

  • Resolution: the subject test imports './stack.zod', a package-relative path, so it resolves to source, not through exportsdist. No rebuild leg is required for this ablation to be real, and a stale dist cannot make it falsely green.
  • Mutation confirmed on disk, both directions counted before reading anything: wrapper form remaining 0 (expected 0), flattened form present 1 (expected 1), plus a non-empty git diff --stat.
  • Result: exit 1 — 6 failed / 11 passed of 17. Red: both read-branch acceptors, the engines.protocol carrier, 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.
  • Restore confirmed by bytes, not by exit code:git checkout HEAD -- ABSOLUTE-PATH (naming HEAD, never a bare --), then git diff HEAD empty and git hash-object equal to the HEAD blob — both 374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried a trap … EXIT INT TERM.

Verification — all on the final commit a16839529

  • Every runnable step of lint.yml's Lint & Repo Gates job, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caught check:spec-parsed-alias (ADR-0122), which the path derivation did not name — the new alias needed an ArtifactPackageEntryParsed because ManifestSchema applies defaults, so z.input and z.infer genuinely 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-completeness exits 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 MET against a partially built tree; after turbo run build --filter='./packages/*' --filter='./packages/*/*' (what lint.yml does), check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt and check:type-check-coverage all re-ran green.
  • packages/spec full 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. ⚠️ An earlier reading showed 66 — every one a TS7016/TS7006 cascade from declarations my own OS_SKIP_DTS=1 closure 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.
  • Generated artifacts fresh: pnpm --filter @objectstack/spec check:generated green (api-surface/ and export-origins/ regenerated from a full dts build, +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/cli grade, 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/spec bump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registration green).

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:240 is manifest: ManifestSchema.optional(); compile.ts:271 is the ObjectStackDefinitionSchema.safeParse; compile.ts:596 is JSON.stringify(finalBundle, null, 2) with the write on :597; build.ts is a six-line subclass of Compile; ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.

Generated by Claude Code


Generated by Claude Code

… 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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/environment-routing.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx(via com.example.crm (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/concepts/north-star.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/kernel/services-checklist.mdx(via com.example.crm (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/releases/v17.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 9dd022aea05251a8bc8ee2161bd93931abd031b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd — the merge of head a1683952929e20211d7c3294fa84d8ee46f7fdfe into base 9dd022aea05251a8bc8ee2161bd93931abd031b9, 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 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

⚠️ 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 9dd022aea05251a8bc8ee2161bd93931abd031b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Independent contract re-review (Clause ②) — PASS (second, independent verdict)

Reviewed at head a1683952929e20211d7c3294fa84d8ee46f7fdfe by the director seat under the maintainer's direct summons of 2026-09-01, in-session at tier (machine-read fuse: session_context.model and last_served_model both equal CONTRACT_REVIEW_TIER). This seat wrote none of this diff and read neither the dispatch order nor the prior verdict's reasoning before deriving its own — the findings below are independently derived from the diff, the tests, ADR-0130 on origin/main, and the CI record.

Both limbs YES (path: packages/spec/src/stack.zod.ts; content: public-surface widening). Derived judgments, each verified against the artifact itself:

  1. ADR-0130 D4 fidelity, clause by clause: optional packages with manifest retained and the read-both rule stated at the schema (§D4 verbatim); the wrapper reservation exactly as the ADR words it ("each entry … is an object, never a bare inlined manifest body"), enforced by strictObject with a teaching history message; forward compatibility riding manifest.engines.protocol with a pin, no new negotiation mechanism; segmented loading, D5 ordering (ADR-0130 D5+D7:装载路径按拓扑序注册物内 N 包(复用 resolvePluginOrder)+ 存量产物逐位相同测试 #14162), D1/D3 gate (ADR-0130 D1+D3:installPackage 命名空间闸认同物共同所有者 + 安装期对象名唯一性检查(同 PR 机器不可拆) #14163) and D8 owner-field all untouched — every Non-goal held.
  2. Zero-regression argument is structural, not asserted: the top-level schema was already strict, so no existing artifact can carry a packages key — nothing that parsed before parses differently now. The .default([]) near-miss is pinned twice (schema-level exact key set; e2e on the artifact os build writes).
  3. Composition: COMPOSE_KEY_DISPOSITIONS packages: 'concat' is the right rule for the right reason; manifest's pick-one semantics deliberately untouched with a pin keeping ADR-0130 follow-up row 3 visible.
  4. check-stack-collection-maps exception is a classification fix, not a gate weakening: packages genuinely is not a metadata collection (no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no register kind); the exception is a named list with its own self-test pin, and the gate's reference set is unchanged at 31.
  5. Changeset: @objectstack/specminor, no BREAKING banner, no ADR-0087 disposition owed — correct for a pure widening; the absent @objectstack/cli grade is backed by the measured zero-source-line pass-through (e2e on real compiles).
  6. Docs drift advisory audited: none of the six flagged pages makes a closed top-level-key enumeration claim; nothing is falsified by an additive optional key.
  7. CI at head: 39 check runs, all success or conditional-skip; check-governed-merges --test 0 of 7 paths governed. The ablation's collateral finding (ManifestSchema strips unknown keys) is correctly filed as ManifestSchema strips unknown keys silently — a namespace typo inside manifest: parses green and the namespace comes out undefined #14192, not folded in.

⚠️ Procedural record — why this second review exists. The PASS at comment 5493145799 on #14161 was rendered by the session that implemented this diff, reviewing its own product, and it cleared the carrier label single-sided. The in-seat review path (2026-08-31 ruling) covers a tier seat reviewing a lower-tier implementer's increment — "审的是低档实现者的契约增量,非自身产物" — and the independence requirement exists precisely for this case. The substance of that verdict is confirmed correct by this independent pass; the process gap is recorded separately as a finding card, and the label is not re-hung since the two verdicts agree and this review completes the independence the gate wanted.

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

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 12:57
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit e621291Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-14161-artifact-packages-schema branch September 1, 2026 13:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130 D4:产物 schema 增加可选 packages[] 列表,两种形态都读,分段加载键位结构性预留

3 participants

@os-sam@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4) - #14191

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema
Sep 1, 2026
Merged

feat(spec): the release artifact may carry N package manifests — optional packages[] (ADR-0130 D4)#14191
os-support-ai merged 2 commits into
mainfrom
claude/issue-14161-artifact-packages-schema

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#14161 — epic #14122, implementing ADR-0130 D4 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md).

ObjectStackDefinitionSchema gains an optionalpackages key 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: packages present → iterate; packages absent → treat manifest as 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-review is 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 under manifest:, 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/integrity would have to be bolted onto the shared ManifestSchema and 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: ManifestSchema is not strict. Under the flattened shape, { ref: './segments/cpq.json', integrity: 'sha256-…' }parses green with both keys silently dropped. The wrapper is a strictObject, 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 existing manifest.engines.protocol mechanism (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); the installPackage co-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

FileWhy
packages/spec/src/stack.zod.tsArtifactPackageEntrySchema (+ its …Parsed alias, ADR-0122) and the optional packages key; a COMPOSE_KEY_DISPOSITIONS entry
packages/spec/src/stack-artifact-packages.test.ts17 pins — both read branches, the reservation, the negative cases, composition
packages/cli/test/compile-artifact-packages.e2e.test.ts4 pins on the artifact os build actually writes
scripts/check-stack-collection-maps.mjspackages is not a metadata collection — see below
packages/spec/api-surface/root.json, export-origins/root.jsonregenerated (additive: two entries)
.changeset/…@objectstack/spec: minor

os compile / os build: no source change was needed, and that was measured

The card names compile.ts:271 (parse) and :596 (write). Both anchors verified. No CLI source change is required: normalizeStackInput, lowerCallables and 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 TOTAL Record keyed on every ObjectStackDefinition key, 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. ⚠️ This does not repair manifest:'s deliberate pick-one semantics — a composeStacks preserve 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-level z.array(SomeSchema) as a metadata collection and reconciles it against eight enumerating sites. packages matches that shape and is not a metadata collection: no singular metadata-type name, no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no registerInMemory kind, 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, reasoned NON_COLLECTION_ARRAY_KEYS exception (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.

  • Resolution: the subject test imports './stack.zod', a package-relative path, so it resolves to source, not through exportsdist. No rebuild leg is required for this ablation to be real, and a stale dist cannot make it falsely green.
  • Mutation confirmed on disk, both directions counted before reading anything: wrapper form remaining 0 (expected 0), flattened form present 1 (expected 1), plus a non-empty git diff --stat.
  • Result: exit 1 — 6 failed / 11 passed of 17. Red: both read-branch acceptors, the engines.protocol carrier, 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.
  • Restore confirmed by bytes, not by exit code:git checkout HEAD -- ABSOLUTE-PATH (naming HEAD, never a bare --), then git diff HEAD empty and git hash-object equal to the HEAD blob — both 374e92fb5cd0c8db349af2084e9e2cba567c5461. The script carried a trap … EXIT INT TERM.

Verification — all on the final commit a16839529

  • Every runnable step of lint.yml's Lint & Repo Gates job, harvested from the workflow itself (132 steps, setup excluded), each exit code captured before any pipe: all green. This harvest earned its keep: it caught check:spec-parsed-alias (ADR-0122), which the path derivation did not name — the new alias needed an ArtifactPackageEntryParsed because ManifestSchema applies defaults, so z.input and z.infer genuinely 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-completeness exits 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 MET against a partially built tree; after turbo run build --filter='./packages/*' --filter='./packages/*/*' (what lint.yml does), check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt and check:type-check-coverage all re-ran green.
  • packages/spec full 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. ⚠️ An earlier reading showed 66 — every one a TS7016/TS7006 cascade from declarations my own OS_SKIP_DTS=1 closure 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.
  • Generated artifacts fresh: pnpm --filter @objectstack/spec check:generated green (api-surface/ and export-origins/ regenerated from a full dts build, +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/cli grade, 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/spec bump. ⛔ No BREAKING banner, so no ADR-0087 disposition marker is owed (check-adr-0087-registration green).

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:240 is manifest: ManifestSchema.optional(); compile.ts:271 is the ObjectStackDefinitionSchema.safeParse; compile.ts:596 is JSON.stringify(finalBundle, null, 2) with the write on :597; build.ts is a six-line subclass of Compile; ComposeStacksOptionsSchema's manifest pick sits where §1.4 says. Nothing needed correcting.

Generated by Claude Code


Generated by Claude Code

… 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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 7 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/environment-routing.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx(via com.example.crm (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/concepts/north-star.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/kernel/services-checklist.mdx(via com.example.crm (literal, a string literal on a changed line))

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

  • content/docs/releases/v12.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))
  • content/docs/releases/v17.mdx(via ObjectStackDefinitionSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/root.json, packages/spec/export-origins/root.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 128 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 9dd022aea05251a8bc8ee2161bd93931abd031b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4a1dadbc3513f7eaaf74dba43dbe76162b2e6edd — the merge of head a1683952929e20211d7c3294fa84d8ee46f7fdfe into base 9dd022aea05251a8bc8ee2161bd93931abd031b9, 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 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

⚠️ 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 9dd022aea05251a8bc8ee2161bd93931abd031b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Independent contract re-review (Clause ②) — PASS (second, independent verdict)

Reviewed at head a1683952929e20211d7c3294fa84d8ee46f7fdfe by the director seat under the maintainer's direct summons of 2026-09-01, in-session at tier (machine-read fuse: session_context.model and last_served_model both equal CONTRACT_REVIEW_TIER). This seat wrote none of this diff and read neither the dispatch order nor the prior verdict's reasoning before deriving its own — the findings below are independently derived from the diff, the tests, ADR-0130 on origin/main, and the CI record.

Both limbs YES (path: packages/spec/src/stack.zod.ts; content: public-surface widening). Derived judgments, each verified against the artifact itself:

  1. ADR-0130 D4 fidelity, clause by clause: optional packages with manifest retained and the read-both rule stated at the schema (§D4 verbatim); the wrapper reservation exactly as the ADR words it ("each entry … is an object, never a bare inlined manifest body"), enforced by strictObject with a teaching history message; forward compatibility riding manifest.engines.protocol with a pin, no new negotiation mechanism; segmented loading, D5 ordering (ADR-0130 D5+D7:装载路径按拓扑序注册物内 N 包(复用 resolvePluginOrder)+ 存量产物逐位相同测试 #14162), D1/D3 gate (ADR-0130 D1+D3:installPackage 命名空间闸认同物共同所有者 + 安装期对象名唯一性检查(同 PR 机器不可拆) #14163) and D8 owner-field all untouched — every Non-goal held.
  2. Zero-regression argument is structural, not asserted: the top-level schema was already strict, so no existing artifact can carry a packages key — nothing that parsed before parses differently now. The .default([]) near-miss is pinned twice (schema-level exact key set; e2e on the artifact os build writes).
  3. Composition: COMPOSE_KEY_DISPOSITIONS packages: 'concat' is the right rule for the right reason; manifest's pick-one semantics deliberately untouched with a pin keeping ADR-0130 follow-up row 3 visible.
  4. check-stack-collection-maps exception is a classification fix, not a gate weakening: packages genuinely is not a metadata collection (no PLURAL_TO_SINGULAR entry, no artifact subdirectory, no register kind); the exception is a named list with its own self-test pin, and the gate's reference set is unchanged at 31.
  5. Changeset: @objectstack/specminor, no BREAKING banner, no ADR-0087 disposition owed — correct for a pure widening; the absent @objectstack/cli grade is backed by the measured zero-source-line pass-through (e2e on real compiles).
  6. Docs drift advisory audited: none of the six flagged pages makes a closed top-level-key enumeration claim; nothing is falsified by an additive optional key.
  7. CI at head: 39 check runs, all success or conditional-skip; check-governed-merges --test 0 of 7 paths governed. The ablation's collateral finding (ManifestSchema strips unknown keys) is correctly filed as ManifestSchema strips unknown keys silently — a namespace typo inside manifest: parses green and the namespace comes out undefined #14192, not folded in.

⚠️ Procedural record — why this second review exists. The PASS at comment 5493145799 on #14161 was rendered by the session that implemented this diff, reviewing its own product, and it cleared the carrier label single-sided. The in-seat review path (2026-08-31 ruling) covers a tier seat reviewing a lower-tier implementer's increment — "审的是低档实现者的契约增量,非自身产物" — and the independence requirement exists precisely for this case. The substance of that verdict is confirmed correct by this independent pass; the process gap is recorded separately as a finding card, and the label is not re-hung since the two verdicts agree and this review completes the independence the gate wanted.

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

@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 12:57
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit e621291Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-14161-artifact-packages-schema branch September 1, 2026 13:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130 D4:产物 schema 增加可选 packages[] 列表,两种形态都读,分段加载键位结构性预留

3 participants

@os-sam@os-support-ai@claude