Repository navigation
feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) - #29100
Conversation
…/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>
📓 Docs Drift CheckThis PR changes 1 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
… 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>
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Head-sha: 维护者速读
席位意见:可落地,等你批名字 + 契约复审。 席位核对 — ACCEPT ·
|
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken on the lane's Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Inputs read: card #28914 body and its three comments (triage, claim, os-dev-report); PR #29100 body, file list (11 paths), net diff vs ① Derived judgmentsEvery judgment below is read off the tree at the head, not the PR body.
② Semver level
③ Boundary flagsEach dev flag and each
④ Design judgment
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: VERDICT: PASS Generated by Claude Code |
|
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 |
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract reviewServed-tier: Inputs read: card #28914 (body, triage 6569574210, claim 6569719522, os-dev-report 6572174760), PR #29100 body, file list (11 paths) and net diff against ① Derived judgments
② Semver level
③ Boundary flagsDev report 6572174760 on the card; no maintainer ruling follows it on the card.
④ Design judgment
What holds. The shape must-fix 1 — the selection rule derives "the platform's own" from
Consequences on the card's scenario, both directions: a code package the runtime boots at the default scope is exported, and should-fix 1 — the docblock pins the mount site and the ledger shape of a slice that has not been written. "mounts should-fix 2 — "enabled or not" is stated without its install-side half. The bundle exports disabled packages (status Implemented-by: 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>
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract reviewServed-tier: Delta review on the FAIL record for ① Derived judgments
② Semver levelChangeset ③ Boundary flags
④ Design judgment
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 No must-fix. No should-fix. Implemented-by: VERDICT: PASS |
|
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. |
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Merge-only head The merge queue removed this PR 2026-11-02T17:30Z: spec What the dev did:
Readings on
There is no semantic change, so the seat carries the CONTRACT_REVIEW_TIER PASS ( CI red, judged a flake: |
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
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Merge-only head The merge queue removed the PR 2026-11-02T19:03Z. That was the round in which #29218 merged and moved the strictness
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. |
…eep both sides' tests in package-lifecycle.test.ts Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87 Co-authored-by: Claude <noreply@anthropic.com>
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) andcontent/docs/concepts/metadata-lifecycle.mdx(its export paragraph, which names only the per-packageGET /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. Noskills/**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]开头。Blocked-by本 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(withPackageExportBundleandPackageExportBundleParsed) inpackages/spec/src/api/package-lifecycle.zod.ts, exported from@objectstack/spec/api. It is declared ahead of its producer (ADR-0049): it is thedatathatGET /api/v1/packages/exportmust 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-levelhandlerStatus: 'planned'was retired under ADR-0049.packagesisz.array(PackageExportManifestSchema): each entry is exactly whatGET /packages/:id/exportanswers, and a test pins that the element IS that schema (identity, not a copy). A.custompair 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.source(InstalledPackageSourceSchema) is notcode, enabled or not, each once: the marketplace and authored packages, a.custompair among them. Acodepackage is registered by the deployment's own code (adefineStack()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 frommanifest.scopeorwritable, and the per-package export already reads the row that carriessource. A pair of acodebase is still exported. A metadata item bound to no package is in no entry; ADR-0146 D11's migration binds it first.enabledorstatuskey, so the bundle carries none: every entry installs enabled, and a package the tenant switched off comes back on. Both describes say so.manifest.dependenciesnames, so a pair comes after its base. The consumer installs in the order listed and never re-sorts.servedaudience, whichdecorateMetadataItemsredacts); the bundle adds no second rule, and acredentialRefstays a reference.studio.accessorsetup.access, or a system context). A closed, empty query set: the door must refuse a parameter400 VALIDATION_ERROR, naming it. Whole or absent: a package whose export cannot be assembled must fail the read, naming it.content/docs/references/api/package-lifecycle.mdxandcontent/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 bycheck:generated --fix..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 inpackages/cli, the round-trip and credential tests) isBlocked-bythis 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.custompair with anoverridesblock, and a third package extending the base; the base carries a connector whoseauthkeepscredentialRef):packagesisPackageExportManifestSchemaitself, and an entry with no identity is refused atpackages.1.id,packages.1.nameandpackages.1.version;packagesis refused atpackages;unrecognized_keysnaming it;[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);source(they namesourceandcode), never mentionscope, and state the missing enabled state.Acceptance notes
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_PATTERNneeds at least two dot-separated segments, soexportis never a package id and never shadowsGET /packages/:id.origin/main135054eb: neitherpackage-api.zod.tsnorpackage-api-assembled.zod.tscarriesGET /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').PackageApiContractssays a route is declared there "only in the same change that MOUNTS it — never ahead of its door".PackageApiContractsbinds 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.GetPackageCustomizationsRequestSchemaexists because that door has a path id.check:livenessledgers are per metadata type (packages/spec/liveness/TYPE.json); an API response schema has none.check:livenessis green.assemblePackageManifest(packages/runtime/src/domains/packages.ts) sets onlyid,name,versionandlabelbeside the metadata collections. It dropsmanifest.namespaceandmanifest.dependencies. A body with no namespace getsinstallPackage's id-derived default (deriveNamespaceFromPackageId,packages/metadata-protocol/src/protocol-package-install.ts), and for a pair that iscustom, not the base'sNS_custom: the exact caseprotocol-customization-package.tssets the namespace explicitly to avoid. So the round trip of a.custompair 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,PackageExportManifestSchemagains two declared fixed keys, which is a spec change (see Open questions).client.packages.exportAll(or similar) and theunwrap-misfire.pin.test.tsroster row belong with the server slice.{ packages }declares neithersuccessnordata, so the unwrap heuristic cannot fire on it.Open questions (for the maintainer, through the seat)
GET /api/v1/packages/export; the schemaPackageExportBundleSchemaand its typesPackageExportBundle/PackageExportBundleParsed; the keypackages. Recommendation: approve as written. They sit besidePackageExportManifestSchemaand the existingpackageskey ofGET /packages.scope: 'system'. The review ruled that wrong:InstalledPackageSourceSchemaforbids deriving a package's origin frommanifest.scope, and the runtime corrected the same derivation once already. A scope-lesscodepackage would be exported and then refused409on reinstall; a marketplace or authored package carryingscope: 'system'would be dropped. The rule now reads the registry row'ssource: every package whosesourceis notcode. 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).namespaceanddependencies. Options: A, the server slice measures the round trip first and, if a pair fails, a spec card adds both as declared fixed keys ofPackageExportManifestSchema; 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 withorigin/mainab085a04merged in) unless a line says otherwise, and on the later heads in the last block; every heavy run went throughos-verify-lock.sh, whoseVERDICT command-exitline is quoted.Package suites and type checks (packages/spec and its two named consumers, packages/runtime and packages/cli)
pnpm turbo run build --concurrency=2over the@objectstack/runtimeand@objectstack/clidependency 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);--fixregenerated exactly those, committed in020adeb0.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. Theintegrationlayer is declared to CI (the diff touches no CLI file).020adeb0, beforeorigin/mainab085a04was 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=0over the two changed.tsfiles: 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((throughablation-replace.mjs, anchor 1 → 0):package-lifecycle.test.ts1 failed | 29 passed ("is strict at the top level"). Restored: blobad533652== HEAD,git diff HEADempty.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.packages/runtime/srcagainst 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 statusclean.Derived gate families (dispatch-gates)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat0493264f: 103 commands, change set 11 paths vs merge baseab085a048(+221 / -8).0493264f. On the first passpnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: nodist/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 AXISyes (widening)+added,@objectstack/spec: minor.check:skill-frame-freshnessexit 0.check:dev-prereqsexit 1 is a box precondition (35ui/workspace dists not built in this container), not a reading about this diff.!in the title and no(narrowing)arm.Later heads: the main merge and the three review rounds
2882de4a(origin/main12aad143merged in throughos-regen-merge.sh, generated artifacts regenerated bycheck: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,--ran0 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-authoringexit 0; all 103 derived gates exit 0,--ran0 NOT-MEASURED;check-changeset-no-major.mjs --eventexit 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'sLint · docs & code guardswas red oncf37246eonly atcheck:cli-command-ids(1 of 52 gates; the derived set does not carry it). Nowpnpm check:cli-command-idsexit 0,check:doc-authoringexit 0,check:generated --fixrewrote onlycontent/docs/references/api/package-lifecycle.mdx, andpackage-lifecycle.test.tsis 31/31.1ce77b0a(contract review round 3: the selection reads the registry row'ssource, notmanifest.scope; both describes state the missing enabled state; the docblock no longer asks for aPackageApiContractsentry): spec build,check:generated(--fixrewrote onlycontent/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-idsandcheck:doc-authoringexit 0; all 103 derived gates exit 0,--ran0 NOT-MEASURED;check-changeset-no-major.mjs --eventexit 0.package-lifecycle.test.ts32/32. Ablation: thescoperule put back into thepackagesdescribe (ablation-replace.mjs, anchor 1 → 0) gives 1 failed | 31 passed (the new selection test); restored, blob == HEAD.runtimeandclisuites were not re-run after0493264f: the later commits change describe text and the main merge, and the branch touches neither package.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmScwpkMFTAD5HHubn1
Generated by Claude Code