Skip to content

feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) - #29100

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-weie9b
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-weie9b

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#1 · os-steve · session_01ADHkmScwpkMFTAD5HHubn1

Requested by Steve · project thread
Part of #28914

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — content/docs/deployment/cli.mdx (a row and section for the CLI command that calls the read) and content/docs/concepts/metadata-lifecycle.mdx (its export paragraph, which names only the per-package GET /packages/:id/export). Both belong with the server slice that mounts the read and adds the command: taught today, they would send a reader to a route no runtime serves. No skills/** file teaches package export.
Ruling-ref: 6568804196

维护者速读

  • 改了什么:在 @objectstack/spec/api 声明「整租户本体导出」这个读的回答形状 PackageExportBundleSchema:{ packages },一次给出部署里不是由部署自己的代码注册的全部已安装包(注册表行 source 不是 code:市场安装的和库里创作的包,含 .custom 配对包),每一项就是现有单包导出 GET /packages/:id/export 的回答,按依赖顺序排好(基础包在前,它的 .custom 配对包在后)。导出不带启用状态,装回去每个包都是启用的。这是「先声明、后兑现」(ADR-0049):路由 GET /packages/export 和调用它的 CLI 命令是下一张服务端卡的事,本 PR 不碰运行时;在路由真正挂上之前,schema 的 describe 以 [PLANNED — not yet served] 开头。
  • 为什么改:官网承诺「本体是你的,可以导出到开源运行时跑」,今天只能一个包一个包导;您在 docs.objectos.ai Remove ObjectStackKernel legacy code and update to microkernel-only architecture #334 裁定「同意,同时立开发卡」,按车道规矩先定 spec 形状,服务端卡 Blocked-by 本 PR。
  • 风险与回滚:纯新增,没有任何现有形状或接受集变化;还没有运行时提供这个路由,所以对线上零影响。回滚即撤掉本 PR。
  • 先看:packages/spec/src/api/package-lifecycle.zod.ts 新增的 PackageExportBundleSchema 及其上方说明(选哪些包、什么顺序、凭据不出门、门禁和查询参数);packages/spec/src/api/package-lifecycle.test.ts 新增的 7 条测试;.changeset/28914-package-export-bundle.md。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/package-lifecycle.test.ts,应看到 32 条全过(其中 7 条是本次新增)。
  • 需要你做的:批准新公开名(路由路径 GET /api/v1/packages/export、schema 名 PackageExportBundleSchema、键 packages);选包规则已按契约复审改成读注册表行的 source(排除 code),不再从 manifest.scope 推导。批准后由席位落地。

What changes

  • PackageExportBundleSchema (with PackageExportBundle and PackageExportBundleParsed) in packages/spec/src/api/package-lifecycle.zod.ts, exported from @objectstack/spec/api. It is declared ahead of its producer (ADR-0049): it is the data that GET /api/v1/packages/export must answer once the server slice mounts the route on the runtime dispatcher, enveloped like its lifecycle siblings: { packages: PackageExportManifest[] }, strict at the top level. No runtime serves the route yet, so the schema's describe opens with [PLANNED — not yet served] and names ADR-0049, and the docblock states the door as what it MUST do. The change that mounts the route drops the marker, adds the route's ledger row and flips the test that pins the marker. The house has no other planned channel for an API response: the liveness ledgers are per metadata type, the runtime route ledger records only what the dispatcher serves, and the API-level handlerStatus: 'planned' was retired under ADR-0049.
    • One producer. packages is z.array(PackageExportManifestSchema): each entry is exactly what GET /packages/:id/export answers, and a test pins that the element IS that schema (identity, not a copy). A .custom pair is its own entry with its override declarations (ADR-0146 D8 item 3); environment customizations stay where the per-package export places them, under the package that owns them.
    • Which packages. Every package whose registry row's source (InstalledPackageSourceSchema) is not code, enabled or not, each once: the marketplace and authored packages, a .custom pair among them. A code package is registered by the deployment's own code (a defineStack() host's packages, a plugin's bundled manifest, a platform package), so it travels with that code and not with the bundle. The registry forbids deriving a package's origin from manifest.scope or writable, and the per-package export already reads the row that carries source. A pair of a code base is still exported. A metadata item bound to no package is in no entry; ADR-0146 D11's migration binds it first.
    • No enabled state. The per-package export has no enabled or status key, so the bundle carries none: every entry installs enabled, and a package the tenant switched off comes back on. Both describes say so.
    • Order. ADR-0141 D5's, the order a boot hydrates installed packages in: an entry comes after every entry its package's manifest.dependencies names, so a pair comes after its base. The consumer installs in the order listed and never re-sorts.
    • No credential value. The per-package export already withholds credential positions (it reads the served audience, which decorateMetadataItems redacts); the bundle adds no second rule, and a credentialRef stays a reference.
    • The door the server slice must build. The per-package export's read gate (studio.access or setup.access, or a system context). A closed, empty query set: the door must refuse a parameter 400 VALIDATION_ERROR, naming it. Whole or absent: a package whose export cannot be assembled must fail the read, naming it.
  • Generated: content/docs/references/api/package-lifecycle.mdx and content/docs/references/index.mdx (1594 → 1595 schemas), packages/spec/api-surface/api.json (+3 names), packages/spec/export-origins/api.json, packages/spec/declaration-map/api.json, packages/spec/json-schema.manifest/api.json (+api/PackageExportBundle), packages/spec/authorable-surface/api.json (+api/PackageExportBundle:packages), docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md (api/ 484 → 485). All written by check:generated --fix.
  • Changeset: .changeset/28914-package-export-bundle.md, @objectstack/spec: minor.

Forced test edits

None. No existing test changed.

Why

The maintainer's ruling on docs.objectos.ai #334 (record 6568804196): 「Cloud/Enterprise 租户怎么导出本体?同意,同时立开发卡。」 Today a tenant can export its ontology only one package at a time. This is the spec slice: it declares the read's answer first, and the server slice (the route in packages/runtime/src/domains/packages.ts, the CLI command in packages/cli, the round-trip and credential tests) is Blocked-by this PR.

Tests

packages/spec/src/api/package-lifecycle.test.ts, a new describe with 7 tests over a bundle shaped as the card's done criteria name it (a base, its .custom pair with an overrides block, and a third package extending the base; the base carries a connector whose auth keeps credentialRef):

  • the bundle parses and every entry is preserved, in the order listed;
  • the element of packages is PackageExportManifestSchema itself, and an entry with no identity is refused at packages.1.id, packages.1.name and packages.1.version;
  • an empty bundle parses (a deployment whose every package is registered by its own code);
  • an answer with no packages is refused at packages;
  • an unknown top-level member is refused unrecognized_keys naming it;
  • the describe opens with [PLANNED — not yet served], names ADR-0049 and the sentence that no runtime serves the route, and carries no tracker id (the server slice flips this test when it mounts the route);
  • both describes select by the registry row's source (they name source and code), never mention scope, and state the missing enabled state.

Acceptance notes

  • Route, not a member of an existing read (the seat's lean, taken). A member of GET /packages (ListInstalledPackagesResponseSchema) would make every list call pay for 27 metadata types per package, and the list keeps its own query set (status, type, enabled). The path cannot collide: MANIFEST_ID_PATTERN needs at least two dot-separated segments, so export is never a package id and never shadows GET /packages/:id.
  • Where the route is declared. Measured on origin/main 135054eb: neither package-api.zod.ts nor package-api-assembled.zod.ts carries GET /packages/:id/export. That route is declared by its response schema's docblock and the runtime route ledger row (packages/runtime/src/route-ledger.ts, responseSchema: 'PackageExportManifestSchema'). PackageApiContracts says a route is declared there "only in the same change that MOUNTS it — never ahead of its door". PackageApiContracts binds only the list, get, install and uninstall doors; no lifecycle route has an entry, so this one needs none. This PR adds no ledger row; the server slice adds it (responseSchema: 'PackageExportBundleSchema') with the mount.
  • No request schema. The door reads no path id, no body and no query parameter, so there is nothing to declare beyond the closed empty set, which the docblock states. GetPackageCustomizationsRequestSchema exists because that door has a path id.
  • No liveness row. check:liveness ledgers are per metadata type (packages/spec/liveness/TYPE.json); an API response schema has none. check:liveness is green.
  • The per-package export does not carry what a reinstall reads (for the server slice). Read on this head: assemblePackageManifest (packages/runtime/src/domains/packages.ts) sets only id, name, version and label beside the metadata collections. It drops manifest.namespace and manifest.dependencies. A body with no namespace gets installPackage's id-derived default (deriveNamespaceFromPackageId, packages/metadata-protocol/src/protocol-package-install.ts), and for a pair that is custom, not the base's NS_custom: the exact case protocol-customization-package.ts sets the namespace explicitly to avoid. So the round trip of a .custom pair likely fails until the per-package export carries both keys. This is a reading of source, not a reproduction; the server slice's round-trip test is the measurement. If it holds, PackageExportManifestSchema gains two declared fixed keys, which is a spec change (see Open questions).
  • The client SDK is untouched. client.packages.exportAll (or similar) and the unwrap-misfire.pin.test.ts roster row belong with the server slice. { packages } declares neither success nor data, so the unwrap heuristic cannot fire on it.

Open questions (for the maintainer, through the seat)

  1. New public names. The route path GET /api/v1/packages/export; the schema PackageExportBundleSchema and its types PackageExportBundle / PackageExportBundleParsed; the key packages. Recommendation: approve as written. They sit beside PackageExportManifestSchema and the existing packages key of GET /packages.
  2. Which packages: settled by the contract review (round 3). The first draft left out manifest scope: 'system'. The review ruled that wrong: InstalledPackageSourceSchema forbids deriving a package's origin from manifest.scope, and the runtime corrected the same derivation once already. A scope-less code package would be exported and then refused 409 on reinstall; a marketplace or authored package carrying scope: 'system' would be dropped. The rule now reads the registry row's source: every package whose source is not code. Disabled packages: exported, and both describes say the bundle carries no enabled state (the seat's lean, taken, because the bundle mirrors the per-package export).
  3. The per-package export's missing namespace and dependencies. Options: A, the server slice measures the round trip first and, if a pair fails, a spec card adds both as declared fixed keys of PackageExportManifestSchema; B, add them in this PR now. Recommendation: A. Declaring keys no producer emits yet would be declared-not-enforced, and the measurement belongs to the slice that runs the install.

Verification

Readings on head 0493264f (this branch with origin/main ab085a04 merged in) unless a line says otherwise, and on the later heads in the last block; every heavy run went through os-verify-lock.sh, whose VERDICT command-exit line is quoted.

Package suites and type checks (packages/spec and its two named consumers, packages/runtime and packages/cli)
  • pnpm turbo run build --concurrency=2 over the @objectstack/runtime and @objectstack/cli dependency closures: Tasks 62 successful, 62 total. VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date (exit 0). On the first head it named 5 stale (api-surface, export-origins, declaration-map, references docs, strictness-ledger counts); --fix regenerated exactly those, committed in 020adeb0.
  • pnpm --filter @objectstack/spec test: Test Files 790 passed (790), Tests 23349 passed, 1 todo. VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec typecheck: VERDICT command-exit 0.
  • pnpm --filter @objectstack/runtime test: Test Files 513 passed (513), Tests 8267 passed, 19 skipped. typecheck: VERDICT command-exit 0.
  • pnpm --filter @objectstack/cli exec vitest run --project unit: Test Files 368 passed (368), Tests 5196 passed. typecheck: VERDICT command-exit 0. The integration layer is declared to CI (the diff touches no CLI file).
  • The same round on 020adeb0, before origin/main ab085a04 was merged: spec 789/789 files (23330 tests), runtime 513/513, cli unit 368/368, all three typechecks exit 0.
  • pnpm exec eslint --no-inline-config --max-warnings=0 over the two changed .ts files: exit 0.
Ablations and the reverse type probe (on 020adeb; the merge did not touch either file)
  • package-lifecycle.zod.ts, the bundle's }).strict().describe( → }).describe( (through ablation-replace.mjs, anchor 1 → 0): package-lifecycle.test.ts 1 failed | 29 passed ("is strict at the top level"). Restored: blob ad533652 == HEAD, git diff HEAD empty.
  • package-lifecycle.zod.ts, z.array(PackageExportManifestSchema) → z.array( a hand-copied four-key object with an open catch-all ): 1 failed | 29 passed ("judges each entry by THE per-package export schema"). Every other test stayed green, so the copy is indistinguishable by parse alone and only the identity pin catches it. Restored: blob == HEAD.
  • Reverse type probe in packages/runtime/src against the rebuilt .d.ts: const ok: PackageExportBundle = { packages: [ … ] } compiles; { packages: [], cycles: [] } is TS2353 ('cycles' does not exist in type …), the probe's only error. Probe deleted; git status clean.
Derived gate families (dispatch-gates)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 0493264f: 103 commands, change set 11 paths vs merge base ab085a048 (+221 / -8).
  • All 103 exit 0 on 0493264f. On the first pass pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: no dist/ for connector-slack, organizations, service-print in this container). After building those three it reported 115 entry points across 70 packages load, exit 0.
  • dispatch-gates --ran: "103 derived famil(ies) accounted for — 103 run, 0 NOT-MEASURED (a DERIVED zero …)".
  • check-changeset-no-major.mjs --base origin/main --event (this body, offline): exit 0, LEVEL AXIS yes (widening) + added, @objectstack/spec: minor.
  • Local-only families: check:skill-frame-freshness exit 0. check:dev-prereqs exit 1 is a box precondition (35 ui/ workspace dists not built in this container), not a reading about this diff.
  • Full dogfood not run: no ! in the title and no (narrowing) arm.
Later heads: the main merge and the three review rounds
  • 2882de4a (origin/main 12aad143 merged in through os-regen-merge.sh, generated artifacts regenerated by check:generated --fix; branch delta unchanged): closure build, check:generated, spec test (Test Files 790 passed (790), Tests 23362 passed, 1 todo) and spec typecheck, each VERDICT command-exit 0; all 103 derived gates exit 0, --ran 0 NOT-MEASURED.
  • cf37246e (the docblock reads as declared ahead of its producer; the [PLANNED — not yet served] marker and its pin test): spec build, check:generated, spec test (Test Files 790 passed (790), Tests 23363 passed, 1 todo) and spec typecheck, each VERDICT command-exit 0; check:doc-authoring exit 0; all 103 derived gates exit 0, --ran 0 NOT-MEASURED; check-changeset-no-major.mjs --event exit 0. Ablation: removing the marker from the describe (ablation-replace.mjs, anchor 1 → 0) gives 1 failed | 30 passed (the marker test); restored, blob == HEAD.
  • a373ba53 (the describe and changeset no longer name a CLI command that does not exist yet): CI's Lint · docs & code guards was red on cf37246e only at check:cli-command-ids (1 of 52 gates; the derived set does not carry it). Now pnpm check:cli-command-ids exit 0, check:doc-authoring exit 0, check:generated --fix rewrote only content/docs/references/api/package-lifecycle.mdx, and package-lifecycle.test.ts is 31/31.
  • 1ce77b0a (contract review round 3: the selection reads the registry row's source, not manifest.scope; both describes state the missing enabled state; the docblock no longer asks for a PackageApiContracts entry): spec build, check:generated (--fix rewrote only content/docs/references/api/package-lifecycle.mdx), spec test (Test Files 790 passed (790), Tests 23364 passed, 1 todo) and spec typecheck, each VERDICT command-exit 0; pnpm check:cli-command-ids and check:doc-authoring exit 0; all 103 derived gates exit 0, --ran 0 NOT-MEASURED; check-changeset-no-major.mjs --event exit 0. package-lifecycle.test.ts 32/32. Ablation: the scope rule put back into the packages describe (ablation-replace.mjs, anchor 1 → 0) gives 1 failed | 31 passed (the new selection test); restored, blob == HEAD.
  • runtime and cli suites were not re-run after 0493264f: the later commits change describe text and the main merge, and the branch touches neither package.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmScwpkMFTAD5HHubn1


Generated by Claude Code

…/packages/export`)

`PackageExportBundleSchema` in `@objectstack/spec/api`: `{ packages }`, every
installed package but the platform's own (manifest scope `system`), each as
`GET /packages/:id/export` answers it, dependencies first (ADR-0141 D5). The
entries ARE `PackageExportManifestSchema`, so the per-package export stays the
one producer and the bundle composes it. Strict at the top level.

Spec slice only: the route, the CLI command and the round-trip tests are the
server slice.

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

Regenerated by `check:generated --fix` (5 stale: api-surface, export-origins,
declaration-map, references docs, strictness-ledger counts).

Claude-Session: https://claude.ai/code/session_01ADHkmScwpkMFTAD5HHubn1
Co-authored-by: Claude <noreply@anthropic.com>
`origin/main` 12aad14 moved the same api generated artifacts; the merge took
main's side and `check:generated --fix` regenerated them, so each holds main's
entries plus this branch's PackageExportBundle rows (+1 over main's counts).

Claude-Session: https://claude.ai/code/session_01ADHkmScwpkMFTAD5HHubn1
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json dbca3ef034537269cef482b342aa4ba7169dfe42.

⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 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 dbca3ef034537269cef482b342aa4ba7169dfe42 → packageMentionDocs.

Which tree this was computed on

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

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

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

… producer

No runtime mounts `GET /packages/export` yet, so the docblock no longer
states the route, its read gate, its closed query set or its whole-or-absent
failure as served facts. It states them as the contract the server slice
must meet (ADR-0049), and the schema's describe opens with
`[PLANNED — not yet served]` until a runtime mounts the route. A test pins
the marker and that the describe names no tracker id; the server slice
drops the marker with its route-ledger row and flips the test.

Claude-Session: https://claude.ai/code/session_01ADHkmScwpkMFTAD5HHubn1
Co-authored-by: Claude <noreply@anthropic.com>
…s not exist yet

check:cli-command-ids resolves every quoted `os …` phrase outside
packages/cli to a command under packages/cli/src/commands/, and the
whole-deployment export command lands only with the server slice. The
describe and the changeset now say "a CLI command that calls it" instead.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: a373ba5338bee5e443a061ebd1b053cedead96bd · read 2026-11-02T09:31Z

维护者速读

  • 改了什么:spec 先声明一个「整租户导出」的返回形状 PackageExportBundleSchema,对应未来的 GET /api/v1/packages/export:一次读出全部已安装包,每个包一项,按依赖顺序排好。这次只有 spec 的声明,服务端还没有挂这个路由,描述里标了 [PLANNED — not yet served]。
  • 为什么改:客户要能一次把自己的全部配置带走(packages: a tenant exports its whole ontology — every installed package, its .custom pair and environment customizations — as installable packages for the open runtime (os package export + the read it calls) #28914),现在只能一个包一个包地导。
  • 风险与回滚:只新增,不改任何现有输入或输出。路由由 server 车道的后续卡挂上,同时去掉标记。回滚即撤销本 PR。
  • 先看:packages/spec/src/api/package-lifecycle.zod.ts 的 PackageExportBundleSchema 及其文档注释。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/package-lifecycle.test.ts(31 个测试)。
  • 需要你做的:在决策卡上批准三个新名字:路由 GET /api/v1/packages/export、PackageExportBundleSchema、字段 packages。

席位意见:可落地,等你批名字 + 契约复审。

席位核对 — ACCEPT · domain:spec#1 · head a373ba53

  1. Shape: draft, base main, Part of #28914 (the server slice mounts the route later), Clause-②: yes (widening) / Metadata-protocol: added at line start in the body and the changeset (spec minor). Within claim 6569719522's surface: package-lifecycle.zod.ts + test, the regenerated spec ledgers (api-surface, authorable-surface, declaration-map, export-origins, json-schema manifest), references, the strictness-ledger count line. Not governed.
  2. Round 1 send-back (seat), fixed at cf37246e: the docblock claimed the route was dispatcher-served, but no handler mounts it (ADR-0049). It now opens "DECLARED AHEAD OF ITS PRODUCER", states every door rule as MUST, and the describe opens [PLANNED — not yet served]. Seat accepts that marker spelling. API response schemas have no planned channel (liveness ledgers are per metadata type; the route ledger and PackageApiContracts record only what is mounted). So the marker plus a test that pins it is the ADR-0049 carrier, and the server slice drops both in the change that mounts the route.
  3. Round 2 (CI), fixed at a373ba53: check:cli-command-ids was red on a quoted os package export, a command that doesn't exist. It is reworded to "a future CLI command that calls it", in the describe and the changeset alike. The dev's local gate run missed it because dispatch-gates doesn't derive that gate for a spec-only diff; the dev now runs it by hand.
  4. Open questions, seat's calls: Q2 = A and Q3 = A, as recorded at round 1. Q1 is the new names, and they go to the maintainer (decision card).
  5. Readings: package-lifecycle 31/31 (6 new: the marker pin among them); spec 790 files / 23363 tests; check:generated rewrote only references/api/package-lifecycle.mdx; cli-command-ids and doc-authoring exit 0; check-changeset-no-major --event and check-partof-closing-keyword exit 0 on the patched body. Check-runs on a373ba53 at this reading: 59 success, 10 skipped, 0 failed.
  6. PR body: the dev patched it via issue_patch at this head (read back identical, 17846 bytes). It reads "declared ahead of its producer", 31 tests (6 new), and has no os package export.
  7. Landing gates: (a) the maintainer approves the three new public names: GET /api/v1/packages/export, PackageExportBundleSchema, packages. (b) One CONTRACT_REVIEW_TIER review, handed to triage (line below). Then the seat readies and queues, since the PR is not governed.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (maintainer standing word 2026-10-28T15:04Z: the Fable quota is short, so contract reviews wait for triage) · head a373ba5

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head a373ba5338bee5e443a061ebd1b053cedead96bd · 2026-11-02T09:34Z

Taken on the lane's Request: contract-review doorbell (#28646, 6573757346). A new declaration (PackageExportBundleSchema, widening, Metadata-protocol: added), at CONTRACT_REVIEW_TIER.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a373ba5338bee5e443a061ebd1b053cedead96bd
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts + packages/spec/src/api/package-lifecycle.test.ts, packages/runtime/src/domains/packages.ts (assemblePackageManifest, the GET /packages/:id/export branch, requireReadCapability, the parts.length === 1 id branch), packages/metadata-protocol/src/protocol.ts (getMetaItemsPerPackage → readFlattenedMetaItems(…, 'served', 'per-package') → decorateMetadataItems), packages/metadata-protocol/src/metadata-diagnostics.ts + metadata-redaction.ts, packages/spec/src/integration/connector-credential-redaction.ts, packages/spec/src/kernel/metadata-credential-fields.ts, packages/metadata-protocol/src/package-writability.ts (READ_ONLY_PACKAGE_SCOPES), packages/spec/src/kernel/manifest.zod.ts (MANIFEST_ID_PATTERN, scope), packages/core/src/hydration-order.ts, packages/spec/src/api/package-api-assembled.zod.ts (PackageApiContracts), packages/runtime/src/route-ledger.ts, packages/spec/liveness/README.md + packages/spec/scripts/liveness/check-liveness.mts, docs/adr/0049, 0141, 0143, 0146, .github/workflows/lint.yml + scripts/ci/lane-verdict.mjs

Inputs read: card #28914 body and its three comments (triage, claim, os-dev-report); PR #29100 body, file list (11 paths), net diff vs main, the three PR comments (docs-drift, the seat's ACCEPT, the review claim); the 74 check-runs on the head, re-fetched by gh api after the saved snapshot showed one Check Changeset run still in progress. Ruling 6568804196 lives on docs.objectos.ai, which this session is not attached to (gh api 403, the attach was refused); it is judged on the card's verbatim quote 「Cloud/Enterprise 租户怎么导出本体?同意,同时立开发卡。」, which the PR body and the claim repeat identically.

① Derived judgments

Every judgment below is read off the tree at the head, not the PR body.

  • Public surface, +3 names in @objectstack/spec/api: PackageExportBundleSchema (const), PackageExportBundle and PackageExportBundleParsed (types) — api-surface/api.json, export-origins/api.json, declaration-map/api.json and json-schema.manifest/api.json each carry exactly those rows and nothing else. Right.
  • Authorable surface, +1 key api/PackageExportBundle:packages — the schema is .strict() with one key, so one row. Right. (There are no authorable-surface shards; check:authorable-surface is one step of the Type Check · source gates job, see below.)
  • Strictness ledger api/ 486 → 487 — one new .strict() site. Right.
  • Accept set of every existing schema: unchanged. The diff adds 86 lines after PackageExportManifestSchema and 3 type lines; PackageExportManifestSchema itself (four fixed keys + catchall(z.unknown()), ruling 4A) is not touched, so the per-package export's contract is not narrowed or widened. The bundle's own accept set: { packages: PackageExportManifest[] }, top-level strict, element open. Right, and the element is the per-package schema by identity (z.array(PackageExportManifestSchema)), which the test pins with toBe.
  • No PackageApiContracts entry, no route-ledger row. package-api-assembled.zod.ts:300 says a route is bound there "only in the same change that MOUNTS it — never ahead of its door"; route-ledger.ts:748 reads "it describes what is WIRED, not what is planned". On origin/main neither carries GET /packages/:id/export either (the route ledger has it at :546, as an SDK row). Leaving both to the server slice is right.
  • Liveness ledger: no row, and none is possible. packages/spec/liveness/*.json is one ledger per governed metadata type; the denominator (authorableTypes() in check-liveness.mts) is registered kinds ∪ unregistered kinds ∪ SPEC_ONLY_SCHEMAS. An API response schema is in none of those sets, so the ledger cannot carry PackageExportBundleSchema as planned or anything else. liveness/api.json is the api ENDPOINT metadata type (ApiEndpointSchema), not the API response surface. The dev's "no liveness row" deviation is right; the planned carrier for this schema is the describe marker plus the pin test, and the head's Spec property liveness check-run is green. The ledger's own MARKER_RE.planned (/\[planned|not yet implemented|coming soon/i) would read [PLANNED — not yet served] as planned if the schema were ever walked, so the spelling is compatible with the house recogniser.
  • Marker and pin. The describe opens [PLANNED — not yet served] and names ADR-0049; the test pins the prefix, the ADR, the "no runtime serves" sentence, and the absence of a tracker id (check:doc-authoring). The house has two sibling spellings on main: [PLANNED — not enforced] (connector-auth.zod.ts#clientSource, a key) and [PLANNED — not yet rendered] (ui/app.zod.ts, component.zod.ts, view.zod.ts, a renderer), each with a ^\[PLANNED — …\] pin. not yet served is the route-shaped third member of the same form, declared the same way (docblock states MUST, describe carries the marker, test pins it, the slice that serves it flips the test). Right, and the seat accepted the spelling at round 1.
  • Check-runs on the head: 74 runs; at the re-read none pending, every run success or skipped (the skipped ones are opt-in harnesses, matrix placeholders and the schedule gate). Check Changeset success (three runs, the formerly in-progress one completed); check:llms-txt and check:authorable-surface are steps of Type Check · source gates (lint.yml job typecheck-source-gates, lines 5293 and 5511), success — check:llms-txt runs continue-on-error: true, and the job's Lane verdict step reads each gate's outcome, so the job's success is the gate's own verdict, not the flag's; Spec property liveness success; Lint · docs & code guards success (the check:cli-command-ids red of cf37246e is cleared at this head; the describe and changeset no longer name a CLI command that does not exist). Green across the board.

② Semver level

.changeset/28914-package-export-bundle.md: '@objectstack/spec': minor. The diff publishes three new exported names and one new schema; it changes no existing shape, default, enum or accept set. Minor is right. Clause-②: yes (widening) and Metadata-protocol: added stand at line start in both the PR body and the changeset and match what the diff does: a widening of the public API surface, a new protocol declaration. Check Changeset is green on the head.

③ Boundary flags

Each dev flag and each open_questions entry, answered or escalated.

  1. New public names — for the maintainer, approval items (not FAIL reasons):
    • route path GET /api/v1/packages/export (named in the docblock, the describe and the changeset; served by nothing yet);
    • schema PackageExportBundleSchema, with types PackageExportBundle and PackageExportBundleParsed;
    • key packages.
      Reviewer's reading: the names sit beside PackageExportManifestSchema and the packages key of GET /packages; export can never be a package id (MANIFEST_ID_PATTERN requires at least two dot-separated segments), so the path never captures a real id. Approve-as-written is sound. Escalated to the maintainer, as the claim promised.
  2. Which packages (Q2: A, "every package but manifest scope: 'system'"). Escalated with one correction for the maintainer to rule on: the house's own platform-scope predicate is READ_ONLY_PACKAGE_SCOPES = ['system', 'cloud'] (packages/metadata-protocol/src/package-writability.ts:38), the constant isWritablePackage and isDisableableAppModule read, and manifest.scope is z.enum(['cloud', 'system', 'project']). The docblock and describe name system alone, so under the declared rule a scope: 'cloud' package — the hosted control plane's own, which no open runtime ships — would be exported into a bundle meant for the Apache-2.0 runtime. Recommendation: state the exclusion as "a platform scope (READ_ONLY_PACKAGE_SCOPES)" rather than system by name; the shape { packages } does not change. See ④, should-fix.
  3. manifest.namespace / manifest.dependencies missing from the per-package export (Q3: A). Confirmed in the tree: assemblePackageManifest sets only id, name, version and label beside the metadata collections. Agree with A: declaring keys no producer emits would be the declared-not-enforced shape ADR-0049 forbids, and the measurement is the server slice's round-trip test. One consequence for the server slice, escalated: the bundle's ordering rule reads manifest.dependencies, which the entries do not carry, so a consumer cannot check the order it is told never to re-sort, and a reinstalled .custom pair loses its base edge (ADR-0141 D1/D5, ADR-0146 D3). If the round trip fails, the fix is a spec card on PackageExportManifestSchema, as the dev says.
  4. Skills-docs follow-up owed (content/docs/deployment/cli.mdx, content/docs/concepts/metadata-lifecycle.mdx) — right to defer: both would teach a route no runtime serves. No skills/** file names package export on main.
  5. Dev deviation "no package-api edit" — right, see ①. "runtime/cli suites not re-run on the last two heads" — those heads changed describe text and merged main; Test Core (1–8/8) is green on this head, which is the measurement that counts.
  6. Dispatcher order, for the server slice (not a defect of this PR): on main, /packages/export reaches handlePackagesRequest as parts = ['export'], which the parts.length === 1 && m === 'GET' branch (packages.ts:2720) answers today as Package 'export' not found (404). The mount must sit above that branch. The docblock's "never shadows GET /packages/:id" is true in the direction it states; this is the other direction.

④ Design judgment

packages/spec/src/api/package-lifecycle.zod.ts — judged against the card's done criteria, ADR-0049 / 0141 D5 / 0143 D3 / 0146 D8, and its first consumer-to-be, assemblePackageManifest + the GET /packages/:id/export branch in packages/runtime/src/domains/packages.ts.

  • Composition holds. Each entry IS PackageExportManifestSchema (identity, pinned). GET /packages/:id/export answers deps.success(manifest), i.e. { success: true, data } with the manifest as data, and the route ledger's convention is "Enveloped — the named schema is the data". The bundle declares itself as the data of the same envelope, and { packages } declares neither success nor data, so the client unwrap heuristic cannot misfire on it. Holds.
  • Done criterion "base + .custom pair + extending package → three installable packages". The test fixture is exactly that triple, in dependency order, the pair carrying overrides: { package, basedOn } (ADR-0146 D8 item 3) and the extender carrying objectExtensions (the key assemblePackageManifest emits, EXTENSIONS_KEY). The shape admits it and preserves order. Holds at spec level; installability is the server slice's test.
  • Done criterion "nothing secret leaves" (ADR-0143 D3: credentialRef only). The bundle adds no redaction rule and declares none; the docblock says the per-package export already withholds. Read in the tree, that chain is real: assemblePackageManifest → protocol.getMetaItemsPerPackage → readFlattenedMetaItems(…, 'served', 'per-package') → protocol.ts:10877 audience === 'served' ? decorateMetadataItems(…) → redactMetadataItem(type, item) → the registry's connector redactor (connector-credential-redaction.ts#redactConnectorItem: withholds every auth.* key the instance-auth kind does not declare, every authentication.* key but type, the upstream header/env maps whole, non-object auth whole, URL userinfo and credential query params), the datasource redactor (datasources are not exported anyway), and deriveCredentialRedactor for any type whose schema marks .meta({ credential }) (WebhookSchema.secret / .headers). credentialRef is a DECLARED key of every ConnectorInstanceAuthSchema variant, so it survives as the reference. The spec cannot police this structurally (the element is honestly open, ruling 4A), and it should not: one producer, one redaction, is the right rule and the docblock states it as such. Holds. No cleartext channel is opened by this PR; the card's credential test belongs to the server slice and is listed in its handoff.
  • Order rule (ADR-0141 D5). packages/core/src/hydration-order.ts#orderHydrationBatch: dependencies first, source order where no edge decides, a cycle's members keep source order among themselves and are reported in cycles. The docblock's three sentences on order match that function exactly, including the cycle sentence. Holds. The top-level .strict() means a future cycles member on the wire is a spec change first, which the "strict" test pins ({ ...bundle, cycles: [] } → unrecognized_keys).
  • Read gate. The docblock requires the per-package export's gate: requireReadCapability (studio.access or setup.access, or isSystem) — packages.ts:402–430. Holds as a statement of the door's rule.
  • Marker form (ADR-0049). Declared ahead of its producer, marker in the describe, MUST-form docblock, pin test that the serving slice flips, no tracker id in customer-facing prose. Same form as the two sibling spellings on main. Holds.
  • Should-fix — the selection rule names system where the house names two platform scopes. READ_ONLY_PACKAGE_SCOPES = ['system', 'cloud'] is the one definition of "the platform's own" that the runtime reads (isWritablePackage, isDisableableAppModule). The docblock ("a package whose manifest scope is system is shipped by every runtime for itself") and the describe ("but the platform's own (manifest scope system)") encode one of the two values by hand. A cloud-scope package is not shipped by an open runtime either, and an "installable on the Apache-2.0 runtime" bundle has no use for it. Fix: say "a platform scope (READ_ONLY_PACKAGE_SCOPES: system, cloud)" in both places, or have the maintainer rule that cloud-scope packages ARE part of a tenant's exportable ontology (then the docblock should say why). The shape, the names and every test are unaffected either way; this is escalated under ③ item 2 for the maintainer's ruling on Q2. Not a must-fix: the rule is prose for a door nothing serves yet, the maintainer is already asked to approve it, and the server slice can carry the corrected wording with the mount.
  • Should-fix — a one-line handoff note. The docblock could name the dispatcher-order requirement (③ item 6) beside the "never shadows" sentence, so the server slice reads both directions in the place it will read first. Cosmetic.
  • Tried to break it, and could not: an entry with no identity is refused at packages.N.id/name/version; {} is refused at packages; { packages: [] } parses (a deployment of platform packages only); a hand-copied open object in place of the composed element is caught only by the identity pin — which exists. The lazySchema Proxy is the same object z.array(...) holds as .element, so the identity pin is stable under lazy evaluation and under OS_EAGER_SCHEMAS=1.

No must-fix. No security-relevant issue turned up (the redaction chain the docblock relies on is real in the tree and this PR opens no second path), so no security note is written.

Implemented-by: claude/project-thread-weie9b
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head a373ba5 · 2026-11-02T10:43Z

Triage ruling 6575141496 returned contract review to the lane, so the spec seat reviews this head itself at CONTRACT_REVIEW_TIER. The earlier handoff line in the seat record is withdrawn for this head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a373ba5338bee5e443a061ebd1b053cedead96bd
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts + packages/spec/src/kernel/package-registry.zod.ts, packages/spec/src/kernel/manifest.zod.ts, packages/spec/src/api/package-api-assembled.zod.ts, packages/runtime/src/domains/packages.ts, packages/runtime/src/route-ledger.ts, packages/core/src/hydration-order.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol-customization-package.ts, packages/objectql/src/registry.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, docs/adr/0049-no-unenforced-security-properties.md, docs/adr/0141-inter-app-dependency-declared-checked-before-install.md, docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md

Inputs read: card #28914 (body, triage 6569574210, claim 6569719522, os-dev-report 6572174760), PR #29100 body, file list (11 paths) and net diff against main at the head, and the head's 60 check-runs (every one success or skipped; none failed, none pending — Check Changeset, Governed Surface Queue Guard, Lint · docs & code guards, Lint · PM dispatch-gates, Spec property liveness, Test Core 8/8, Type Check UI, Build Docs all green). The PR thread's own records were not read. The remainder of the diff (generated api-surface, authorable-surface, declaration-map, export-origins, json-schema.manifest, strictness-ledger counts, regenerated content/docs/references/**, the test file and the changeset) is the lane seat's ACCEPT at TIER_DEFAULT; those files were read only to judge the hits file.

① Derived judgments

  • Public surface +3 names (PackageExportBundleSchema const, PackageExportBundle / PackageExportBundleParsed types) through export * from './package-lifecycle.zod' (api/index.ts:109); api-surface, declaration-map, export-origins, json-schema.manifest each gain exactly these; authorable-surface gains api/PackageExportBundle:packages; strictness-ledger api/ site total +1 (486 → 487, the new z.object, itself .strict()); references docs 1596 → 1597 schemas. All generated by the house's own generators; each derived row names what the diff publishes and nothing else — right.
  • No existing accept set moves. PackageExportManifestSchema (four fixed keys + open z.unknown() catch-all) is untouched; the bundle composes it (z.array(PackageExportManifestSchema), identity pinned by test). Pure widening — right.
  • Top-level .strict() on a declared-not-transcribed response: matches the file's newest sibling of the same kind, GetPackageCustomizationsResponseSchema (package-lifecycle.zod.ts:398), so a member the door starts serving is a spec change first — right.
  • No route-ledger row, no PackageApiContracts entry, no liveness row. package-api-assembled.zod.ts:300: a route is declared there "only in the same change that MOUNTS it — never ahead of its door"; the per-package export itself is declared by its ledger row (route-ledger.ts:546, responseSchema: 'PackageExportManifestSchema') and not by PackageApiContracts; liveness ledgers are per metadata type. Deferring all three to the mounting slice — right.
  • [PLANNED — not yet served] marker. ADR-0049 sets [EXPERIMENTAL — not enforced]; the UI protocol extended it to [PLANNED — not yet rendered] (app.zod.ts, view.zod.ts), each pinned by its own test and read by no gate. This third spelling is likewise test-pinned (the pin test refuses a tracker id in the describe) and collides with no recogniser — right.
  • Order rule (dependencies first; a cycle keeps listing order) transcribes orderHydrationBatch (packages/core/src/hydration-order.ts, "ADR-0141 D5 — the order a boot hydrates"); the pair-after-base consequence holds because the pair's manifest declares its base in manifest.dependencies (protocol-customization-package.ts:29) — right.
  • Credential rule ("the bundle adds no rule of its own"): assemblePackageManifest reads getMetaItemsPerPackage, which is readFlattenedMetaItems(…, 'served', 'per-package') (protocol.ts:9794), the audience decorateMetadataItems redacts (protocol.ts:10683) — right.
  • Read gate named as the per-package export's (requireReadCapability: studio.access / setup.access / isSystem, packages.ts:2518) and the closed empty query set in the shape of GET /packages/:id/customizations — right.
  • Selection rule — "every package but manifest scope: 'system'" (describe of packages, the schema describe, the docblock "Which packages") — wrong; see ④.

② Semver level

.changeset/28914-package-export-bundle.md: '@objectstack/spec': minor, Clause-②: yes (widening), Metadata-protocol: added; the PR body carries the same two lines. A new response schema plus a new authorable key is added, and added on a moved package takes at least minor — the same shape as the house's earlier response-schema additions (.changeset/26594-record-field-access-spec.md). Check Changeset is green on the head. Level and kind match what the diff publishes.
Clause-②: yes (widening)

③ Boundary flags

Dev report 6572174760 on the card; no maintainer ruling follows it on the card.

  • Open question 1 (new public names: GET /api/v1/packages/export, PackageExportBundleSchema, PackageExportBundle(Parsed), key packages). Not answered on the card. The names sit beside PackageExportManifestSchema and the packages key of GET /packages; MANIFEST_ID_PATTERN (kernel/manifest.zod.ts:360) needs two dot-separated segments, so export can never shadow /packages/:id — the dev's reading holds. Escalated to the maintainer: the seat's claim 6569719522 makes the landing wait on this word.
  • Open question 2 (which packages: A — all but manifest scope: 'system'; B — all, consumer skips). Answered here, with neither option: the deployment's own packages are the registry rows whose source is code; see ④ must-fix 1. The maintainer's word on the names (Q1) can carry this too.
  • Open question 3 (per-package export drops manifest.namespace and manifest.dependencies; a reinstalled pair would derive namespace custom and lose its base edge). Option A (measure on the round trip, then declare) is sound. One addition for the server slice: PackageExportManifestSchema's catch-all is z.unknown(), so the producer may emit both keys today without any spec change; a spec card is owed only to make them fixed keys. Escalated with the server slice, as the dev proposed.
  • Deviations. No package-api edit (verified: neither package-api.zod.ts nor package-api-assembled.zod.ts declares GET /packages/:id/export); no liveness row (verified above); two origin/main merges with artifacts regenerated (check:generated is green on the head via Lint · docs & code guards); runtime and cli suites not re-run locally after 0493264f — answered by Test Core 1/8 … 8/8 success on a373ba53; check:dual-build-cjs-loads box precondition — not a reading about the diff; cli integration project left to CI — covered by the same head run. All accounted for.
  • Skills-docs follow-up owed (content/docs/deployment/cli.mdx, content/docs/concepts/metadata-lifecycle.mdx): correctly deferred to the slice that mounts the read; teaching it now would send a reader to a route no runtime serves.
  • CI check-runs not cited by the dev: read here; all green.

④ Design judgment

packages/spec/src/api/package-lifecycle.zod.ts, judged against the ruling it implements (6568804196: a tenant exports its whole ontology to the open runtime), the ADRs it cites (ADR-0049, ADR-0141 D5, ADR-0146 D8/D11) and its first consumer (the server slice that will mount GET /packages/export in packages/runtime/src/domains/packages.ts, reading this docblock as its order).

What holds. The shape { packages: PackageExportManifest[] } is the right composition: one producer, no second assembly, strict at the top, open per entry as ruled for the per-package export (#12038 4A). Order, credential, gate and whole-or-absent rules each transcribe code that exists on the head (named in ①). The pin tests cover strictness, element identity, the required key, the empty bundle and the marker, and the dev's ablations show each pin is load-bearing.

must-fix 1 — the selection rule derives "the platform's own" from manifest.scope, which the published spec forbids and the runtime already ruled wrong. The describe of packages, the schema describe and the docblock ("a package whose manifest scope is system is shipped by every runtime for itself") all make manifest.scope === 'system' the predicate for what the bundle leaves out. The tree it cites says otherwise:

  • packages/spec/src/kernel/package-registry.zod.ts:49-71 — every registry row carries source: 'code' | 'marketplace' | 'authored', where code is "registered by the running deployment's own code: a defineStack() host's packages, a plugin's bundled manifest, a platform package", and "⛔ The value is the PRODUCER's statement … no reader derives it from other keys (writable, manifest.scope, a second list)". That enum is exactly "what every runtime ships for itself".
  • packages/runtime/src/domains/packages.ts:866-880 (feat(runtime): GET /packages 与 GET /packages/:id 每行携带服务端自己的可写判定 writable(isWritablePackage),Studio 不再用 scope 启发式反推 #14375 / ADR-0130 Consequences row 6) — Studio once derived the same platform-versus-tenant line from manifest.scope alone, and the server corrected it: "a package booted from an artifact through registerApp is read-only whatever its scope says, and a scope-less BOOTED package lands there too".
  • ManifestSchema.scope (kernel/manifest.zod.ts:636) is .default('project'): a plugin's bundled manifest that declares no scope is a code package at scope project.

Consequences on the card's scenario, both directions: a code package the runtime boots at the default scope is exported, and os package install of it on an open runtime that defines the same id is refused 409 MANIFEST_CONFLICT ("already defined by this runtime's local code", marketplace-install-local-plugin.ts:1976) — the bundle is no longer "installable packages", and the done criterion's three-package count is broken by every such entry; conversely a marketplace or authored package whose manifest carries scope: 'system' is dropped from the tenant's export although the target does not ship it. Meanwhile the per-package export already reads registry.getPackage(id), whose row carries source, so the server slice has the right key in hand and nothing to derive. The fix is textual and confined to this file (plus the changeset's mirror sentence and the regenerated docs): state the rule as "every package whose registry row's source is not code (InstalledPackageSourceSchema) — marketplace and authored packages, a .custom pair among them — each once, enabled or not", and drop manifest scope system from the two describes and the docblock. The marker pin test can then also check that the describe names source and not scope. Until then the contract orders its first consumer to implement a predicate the spec's own ⛔ forbids. FAIL.

should-fix 1 — the docblock pins the mount site and the ledger shape of a slice that has not been written. "mounts GET /packages/export in packages/runtime/src/domains/packages.ts, in the same change as its route-ledger row and its PackageApiContracts entry" names a file path and two ledger edits as this schema's contract. A route-ledger row is the house's declaration of a served route; a PackageApiContracts entry is not required of the lifecycle family (package-api-assembled.zod.ts:267-312 binds the list, get, install and uninstall doors only; no route this file declares has one, and the one that did, rollbackPackage, was retired under #12038 3A). Keep the marker-removal rule and the ledger row; drop the PackageApiContracts clause and the file path so the server slice is not sent to add a binding the family does not use.

should-fix 2 — "enabled or not" is stated without its install-side half. The bundle exports disabled packages (status disabled, PackageStatusEnum) as installable packages, so a reinstall on the open runtime enables what the tenant had switched off, and the per-package export carries no enabled/status key to say so. Either carry the rule to the consumer ("the bundle does not carry the enabled state; a consumer enables every entry") in the describe, or export only enabled packages. The choice is the card's; the silence is not.

Implemented-by: claude/project-thread-weie9b
Reviewed-by: session_01ADHkmPi25XXKx92Xmg4viS

VERDICT: FAIL

…d says it carries no enabled state

The bundle left out "the platform's own" by `manifest.scope: 'system'`. The
registry forbids deriving a package's origin from `manifest.scope`
(`InstalledPackageSourceSchema`), and the runtime already corrected the same
derivation once. The rule now reads the registry row's `source`: every
package whose `source` is not `code` (marketplace and authored packages,
a `.custom` pair among them). A `code` package travels with the code that
registers it, not with the bundle.

Both describes now say the bundle carries no enabled state, so every
entry installs enabled. The docblock no longer orders the server slice
to add a `PackageApiContracts` entry (that table binds only the list, get,
install and uninstall doors) or names its file; the marker comes off with
the route's ledger row. A new test pins the selection by `source`, the
absence of `scope` and the enabled-state sentence in both describes.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 1ce77b0 · 2026-11-02T11:50Z

Delta review of the round-3 commits by the same reviewer that recorded FAIL on a373ba5.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1ce77b0aaac41e3eb9e0f6187a18b35a1d8cdbcc
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts + packages/spec/src/kernel/package-registry.zod.ts, packages/spec/src/kernel/manifest.zod.ts, packages/spec/src/api/package-api-assembled.zod.ts, packages/runtime/src/domains/packages.ts, packages/runtime/src/route-ledger.ts, packages/core/src/hydration-order.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol-customization-package.ts, packages/objectql/src/registry.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, docs/adr/0049-no-unenforced-security-properties.md, docs/adr/0141-inter-app-dependency-declared-checked-before-install.md, docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md

Delta review on the FAIL record for a373ba53. Inputs read: card #28914 (body and every comment — no comment newer than the os-dev-report 6572174760, so no maintainer ruling yet), PR #29100 body and file list (11 paths, unchanged), the delta git diff a373ba53..1ce77b0a (one commit, 4 files: the hits file, its test, the changeset, the regenerated content/docs/references/api/package-lifecycle.mdx), the net diff against main for context, and the head's 66 check-runs (every one success or skipped; none failed, none pending — Check Changeset, Governed Surface Queue Guard, Lint · docs & code guards, Lint · PM dispatch-gates, Spec property liveness, Test Core 8/8, Type Check UI, Build Docs green). The PR thread's own records were not read. The remainder of the diff stays the lane seat's ACCEPT at TIER_DEFAULT; the test, changeset and regenerated page were read only to judge the hits file.

① Derived judgments

  • No public-surface change in the delta. The commit touches prose only: two .describe() strings, the docblock and an inline comment in package-lifecycle.zod.ts; no name, key, type or accept set moves, so api-surface, authorable-surface, declaration-map, export-origins, json-schema.manifest and the strictness count are unchanged from a373ba53 — right (check:generated inside Lint · docs & code guards is green, so the one regenerated page is current and no other artifact was owed).
  • Selection rule now reads the registry row's source (InstalledPackageSourceSchema, kernel/package-registry.zod.ts:67): every package whose source is not code, each once; a code package travels with the code it is registered by. This is the key package-registry.zod.ts:62-65 names as the producer's statement and forbids deriving from manifest.scope or writable; the per-package export already reads registry.getPackage(id), whose row carries it (InstalledPackageSchema.source, line 118; recorded by SchemaRegistry.installPackage, objectql/src/registry.ts:7007-7030). A .custom pair is authored, so a pair of a code base is exported, as the docblock says — right. "Empty when every package of the deployment is registered by its own code" — right.
  • Enabled state is now declared absent from the bundle (the per-package export has no enabled or status key — verified against assemblePackageManifest, packages.ts:3263-3268, which sets id, name, version, label? only), with the consequence stated: every entry installs enabled and the consumer disables again — right.
  • PackageApiContracts clause dropped from the docblock, the inline comment and the test comment; the mount file path dropped. What remains is the route-ledger row and the marker flip in the mounting change, the house's own declaration of a served route (route-ledger.ts:546) — right.
  • Everything judged right on a373ba53 (composition by identity, top-level .strict(), open entries, order rule from orderHydrationBatch, credential rule from the served audience, read gate and closed query set, [PLANNED — not yet served] marker) is unchanged in the delta — right.

② Semver level

Changeset .changeset/28914-package-export-bundle.md keeps '@objectstack/spec': minor, Clause-②: yes (widening), Metadata-protocol: added; only its selection sentence changed, to the same rule the describes state. The PR body carries the same two lines. A prose-only delta publishes no new kind; the level still matches what the diff publishes, and Check Changeset is green on the head.
Clause-②: yes (widening)

③ Boundary flags

  • must-fix 1 (selection by manifest.scope) — resolved. Both describes, the docblock and the changeset now select by the row's source; the new pin test asserts each describe contains `source` and `code`, matches no scope, mentions enabled, and that InstalledPackageSourceSchema.options holds code — so the describe cannot silently drift back to the forbidden derivation, and the enum it names stays real. The not.toMatch(/scope/) arm is deliberately broad; the docblock's "environment-scope customizations" sits outside the describes, so it does not trip it.
  • should-fix 1 (PackageApiContracts entry and file path ordered of the server slice) — resolved, as above.
  • should-fix 2 ("enabled or not" without its install-side half) — resolved: the card's choice (export disabled packages too) is kept and its consequence is stated in both describes and the docblock.
  • Open question 1 (new public names). Still unanswered on the card. Escalated, unchanged: the seat's claim 6569719522 makes the landing wait on the maintainer's word for the route path, the schema name and the key; the review does not substitute for it.
  • Open question 2 (which packages). Answered by this delta: neither of the dev's A/B, the registry's source axis. Closed.
  • Open question 3 (per-package export drops namespace / dependencies). Unchanged; escalated with the server slice, with the earlier note that PackageExportManifestSchema's z.unknown() catch-all already admits both keys, so the producer may emit them before any spec card.
  • The dev's deviations and the Skills-docs follow-up were accounted for on a373ba53 and nothing in the delta reopens them; Test Core 1/8 … 8/8 success on 1ce77b0a covers the runtime and cli suites once more.

④ Design judgment

packages/spec/src/api/package-lifecycle.zod.ts on 1ce77b0a, judged against ruling 6568804196, ADR-0049 / ADR-0141 D5 / ADR-0146 D8–D11, and its first consumer (the server slice that will mount GET /packages/export and read this docblock as its order).

The design now holds. The contract selects the tenant's packages by the one key the registry stamps for that purpose and forbids deriving, so the card's scenario comes out right on a real deployment: a plugin's bundled manifest at the default scope project is code and stays out of the bundle (no 409 MANIFEST_CONFLICT on the open runtime, marketplace-install-local-plugin.ts:1976), a marketplace or authored package carrying scope: 'system' is exported, and the base + .custom pair + extending package of the done criterion are three entries exactly. The enabled-state silence is closed, the server slice is no longer sent to add a PackageApiContracts binding the lifecycle family does not use, and every rule the docblock states transcribes code on the head. Trying to break it: a code base with an authored pair exports the pair alone — correct, the base arrives with the target's code; a deployment with only code packages answers { packages: [] } — declared; a disabled authored package reinstalls enabled — declared with its remedy. Nothing new is introduced by the delta: no shape, key, name or accept set changes, and the new pin test adds a cross-domain import only in the test file.

No must-fix. No should-fix.

Implemented-by: claude/project-thread-weie9b
Reviewed-by: session_01ADHkmPi25XXKx92Xmg4viS

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Naming decision withdrawn on #28914 (2026-11-02T17:07Z). Names are the seat's call, and the seat keeps them as built. The seat readies and queues this PR on its CONTRACT_REVIEW_TIER PASS at the current head.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 17:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Merge-only head 1e673873 · flake judgement · 2026-11-02T18:19Z

The merge queue removed this PR 2026-11-02T17:30Z: spec test:repo strictness-ledger-doc.test.ts counted 1293 untriaged sites against 1292, because the head predated main's movement.

What the dev did:

  • Merged main 399df5bb with os-regen-merge.sh and added one regen commit (package-lifecycle.mdx, and the api ledger shard 487 → 488).
  • gen:strictness-ledger on the merged tree changes nothing further.
  • The delta against main is unchanged: 11 files, +280/-8.

Readings on 1e673873:

  • check:strictness-ledger 0 and strictness-ledger-doc.test.ts 24/24
  • check:generated 0
  • spec 792/792, test:repo 89/89, typecheck 0
  • runtime 524/524

There is no semantic change, so the seat carries the CONTRACT_REVIEW_TIER PASS (1ce77b0a) to this head.

CI red, judged a flake: Test Core (8/8) in run 41493096213 failed one test. It is service-automation/src/oauth2-grant/authorization.test.ts › "the KV holds no verifier, …", at line 137. The test asserts that a random base64url ciphertext does not contain the 3-character string crm, which fails by chance about once in a few hundred runs. This PR touches no service-automation file, and 261 of 262 files passed. The seat re-runs the failed jobs once, then readies and queues.

origin/main moved the api shard and the authorable surface again; the
merge took main's side of the generated files, and check:generated
--fix rewrote them from the merged source with this branch's rows.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Merge-only head 007f80b9 · 2026-11-02T19:42Z

The merge queue removed the PR 2026-11-02T19:03Z. That was the round in which #29218 merged and moved the strictness api.md shard and authorable-surface/api.json, which this PR also writes. Stacked heads 3-way-merge that count wrong; this is #29524's class.

There is no semantic change, so the seat carries the CONTRACT_REVIEW_TIER PASS forward and re-queues. #29521 (also an api-shard writer) is held until this PR merges.

@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 19:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit f1d795b Nov 2, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-weie9b branch November 2, 2026 20:32
os-steve pushed a commit that referenced this pull request Nov 2, 2026
…eep both sides' tests in package-lifecycle.test.ts

Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant