Repository navigation
Commit b3d1415
feat(runtime,client,cli): export a deployment's whole ontology as installable packages in one read (GET /packages/export, os package export) (#29755)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8HPBiw3dHprSTrka5xiidsK)_
Fixes #28914
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:运行时新开 `GET
/api/v1/packages/export`,一次导出整个部署的本体:所有不是由部署自身代码注册的已安装包(市场安装的、库里创作的,含
`.custom` 配对包,停用的也算),每一项就是现有单包导出 `GET /packages/:id/export`
的原样回答,按依赖顺序排(基础包在前)。配套 SDK 方法 `client.packages.exportAll()` 和 CLI 命令 `os
package export [--out DIR]`(每个包写一个带序号的目录,可直接 `os package install`);spec
去掉 `[PLANNED — not yet served]` 标记。
- 为什么改:官网承诺「本体是你的,可以导出到开源运行时跑」,今天只能一个包一个包导;spec 形状已由 #29100 落地,阻塞的
#29617(导出带 namespace / dependencies)已由 #29651 落地,本 PR 是最后的服务端切片。
- 风险与回滚:纯新增路由和命令,不改任何现有接口的回答;读门与单包导出相同(`studio.access` /
`setup.access`)。回滚即撤回本 PR。
- 先看:`packages/runtime/src/domains/packages.ts` 的
`assemblePackageExportBundle` 与新路由分支(在 `GET /packages/:id`
之前);`packages/cli/src/commands/package/export.ts`;`packages/spec/src/api/package-lifecycle.zod.ts`
的 docblock 与 describe(去标记的公开面)。
- 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run
src/domains/packages-export-bundle.test.ts`,预期 10
条全绿(选包、依赖序、单一生产者、读门、查询参数拒绝、整体或不答)。
- 需要你做的:无,席位做完契约复审后落地。
## What changes
- **Runtime (`packages/runtime/src/domains/packages.ts`).** A new branch
answers `GET /packages/export`, matched ahead of `GET /packages/:id`
(`export` is never a package id: `MANIFEST_ID_PATTERN` needs two
dot-separated segments). It asks the per-package export's read gate
first (`requireReadCapability`: `studio.access` or `setup.access`, or a
system context), then refuses any query parameter `400 VALIDATION_ERROR`
naming it (`unknownQueryParamRefusalMessage(query, [])`, the same
closed-set refusal `GET /packages/:id/customizations` uses).
`assemblePackageExportBundle` then:
- selects every registry row whose `source` is not `code`, disabled rows
included, each id once (the row's `source`, never `manifest.scope` or
`writable`, as `PackageExportBundleSchema` states);
- orders them with the boot's own sorter, `orderHydrationBatch` from
`@objectstack/core` (ADR-0141 D5), over
`declaredDependencyIds(row.manifest)`, so a `.custom` pair follows its
base whatever order the registry lists them in;
- composes each entry with `assemblePackageManifest`, the per-package
export's producer, and nothing else: no second clean or redaction rule;
- is whole or absent: a thrown assembly answers through
`errorFromThrown` (its status kept) with the message prefixed `The
export of package 'ID' could not be assembled, so no bundle is
answered:`; a `null` assembly answers 500 naming the package.
- **Mount (`packages/runtime/src/dispatcher-plugin.ts`).**
`mountPackagesRoute('get', '/export', …)`, registered before `/:id` so
the literal segment wins.
- **Route ledger (`packages/runtime/src/route-ledger.ts`).** Row `GET
/packages/export` → `packages.exportAll`, `responseSchema:
'PackageExportBundleSchema'`; the census sentence regenerated (116 →
117) by `check-route-ledger-census.mjs --fix`.
- **Client (`packages/client/src/index.ts`).**
`client.packages.exportAll()`, answering a `PackageExportBundle`; its
row in `unwrap-misfire.pin.test.ts`'s roster
(`PackageExportBundleSchema` declares `packages` only, so the
`success`+`data` heuristic cannot fire).
- **CLI (`packages/cli/src/commands/package/export.ts`, exported from
`src/index.ts`).** `os package export [--out DIR] [--url] [--token]`,
through `createApiClient` like the `os meta` commands. It writes
`DIR/NN-ID/objectstack.json`, one per entry, numbered in the order
served (zero-padded so a listing sorts in install order), and prints the
`os package install` commands in that order. Loud and never partial: a
refusal prints with status and code and writes nothing; an entry with no
id is refused naming its position; an `--out` directory that already
holds files is refused before the read; an empty bundle writes nothing
and says why.
- **Spec (`packages/spec/src/api/package-lifecycle.zod.ts`).** The
`[PLANNED — not yet served]` marker comes off
`PackageExportBundleSchema`'s describe, which now names the route and
`os package export`; the docblock's "MUST do / not served today" prose
now states what the door does. No key, type or accept set changes. The
test that pinned the marker is flipped (no `PLANNED`, names the route
and the command, no tracker id); the regenerated
`content/docs/references/api/package-lifecycle.mdx` follows
(`check:generated --fix`).
- **Docs.** `content/docs/deployment/cli.mdx`: a row in the package
command table and an `os package export` section (what is and is not
exported, the numbered install order, no secret, no enabled state, whole
or nothing, who can export, options).
`content/docs/concepts/metadata-lifecycle.mdx`: the export paragraph now
names the whole-deployment read beside the per-package one.
- **Changeset** `.changeset/28914-package-export-whole-deployment.md`:
runtime, client, cli, spec all `patch` (kind `none`, a non-protocol
widening per AGENTS.md's level rule).
## Why
The card's server slice: the maintainer's ruling on docs.objectos.ai
#334 (record 6568804196) approved building the whole-ontology export;
the spec slice (#29100) declared the answer and the blocker #29617
(#29651) made each per-package export carry what a reinstall reads. This
PR serves the read and the command and meets the card's two Done tests.
## Tests
- **Door pins** (new
`packages/runtime/src/domains/packages-export-bundle.test.ts`, real
dispatcher over a registry and protocol double, 10 tests): selection
(`code` excluded, disabled included, each once, parses as
`PackageExportBundleSchema`); dependency order with the registry listing
both dependents ahead of the base (control asserted); each entry
`toEqual` the `GET /packages/:id/export` answer for the same id; empty
when every row is `code`; the read gate (`studio.access` and
`setup.access` admitted, `manage_metadata` alone 403 with the registry
and protocol never read); `?format=zip` → 400 `VALIDATION_ERROR` naming
`format`, registry never read; whole or absent twice (no protocol → 500
naming the first package; a throw on the second package's assembly → its
503, message naming that package, no `data`); `export` never reaches
`getPackage`; the ledger row.
- **CLI pins** (new `packages/cli/test/package-export.test.ts`, the real
command and the real `ObjectStackClient`, only `createApiClient`
stubbed, 6 tests): numbered directories with each entry as served and
the install commands in order; **each written file is the `manifest` the
real `os package install FILE` posts, unchanged** (install driven per
file, its POST captured); a 503 refusal printed with status and code,
exit 1, nothing written; a non-empty `--out` refused before any request;
an empty bundle writes nothing; an id-less entry refused naming its
position.
- **Round trip and credential** (new
`packages/qa/dogfood/test/package-export-bundle-round-trip.dogfood.test.ts`,
two real showcase boots with install-local mounted, 5 tests). Source: a
base installed through install-local (object, app, dashboard, flow,
permission set, and a connector with `auth: { type: 'bearer',
credentialRef }`), its `.custom` pair with a published dashboard
override, and a third package depending on the base and extending its
object. The bundle is exactly those three ids (the showcase's own `code`
package absent), the base first, the pair carrying `namespace`
`exportbase_custom`, its base edge and its override; the connector
carries its `credentialRef` and the secret the reference resolves to on
the source (control: set in the process env) appears nowhere in the
response bytes. Target: each entry installs 200 through `POST
/marketplace/install-local` with `{ manifest: entry }` (the body `os
package install FILE` sends), in the order served, and the target's own
bundle answers the same ids in the same order with the same namespace,
dependencies and per-collection item names and counts; the pair's
override comes back with its `overrides` block.
- **Client**: `packages-lifecycle.test.ts` pins `exportAll` → `GET
/api/v1/packages/export` and the order served.
- **Spec**: the flipped marker test.
<details><summary>Ablation (one-time, runtime tests resolve packages.ts
from source; scripts/ablation-replace.mjs)</summary>
On `699399a4`, fix committed first:
- `orderHydrationBatch(nodes).ordered` → `nodes.map((n) => n.item)`:
`Tests 2 failed | 8 passed (10)` (dependency order; and the
null-protocol case, which names the first package and so reads the
order).
- `row?.source === 'code'` → `row?.source === 'never'`: `Tests 2 failed
| 8 passed (10)` (selection; empty bundle).
- Both restored: `blob after restore == blob at HEAD (8a46b0c)`,
`git diff HEAD` empty.
</details>
## Acceptance notes
- **The door's status on a failed assembly.** The declaration says only
"fails the read, naming that package". A thrown assembly keeps the
status `errorFromThrown` resolves for it; a `null` one (no metadata
protocol) answers 500. No new error code.
- **`Clause-②: yes (a new served route)`** from the claim is written
here as `yes (widening)`: AGENTS.md's closed arm set is `(widening)` /
`(narrowing)`, and a new route that changes no existing answer is a
widening. `Metadata-protocol: none`: nothing a metadata author writes
changes.
- **No new spec key**, and the spec diff is describe text and docblock
only; the marker drop is the published-schema face the seat's contract
review reads.
- `assemblePackageManifest` swallows a per-type read error as "type
unsupported here" (`continue`), as before; the bundle inherits that. Not
changed here.
- docs.objectos.ai #334's "export your ontology" page is the docs lane's
follow-up, as the card says.
## Verification
Head `43a7c550` (merge base `c9636f1e`); every heavy run under
`os-verify-lock`.
- `pnpm --filter @objectstack/spec test`: `Test Files 803 passed (803)`,
`Tests 23792 passed | 1 todo`; typecheck exit 0.
- `pnpm --filter @objectstack/client test`: `Test Files 54 passed (54)`,
`Tests 695 passed`; typecheck exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: `Test
Files 376 passed (376)`, `Tests 5295 passed`; typecheck exit 0. The
`integration` tier is CI's (the diff touches no integration-tier file or
spawn entry).
- `pnpm --filter @objectstack/runtime test`: `Test Files 532 passed
(532)`, `Tests 8535 passed | 19 skipped`; typecheck exit 0.
- `pnpm --filter @objectstack/dogfood typecheck` exit 0; the round-trip
file `Tests 5 passed (5)` after building the dogfood closure.
- `pnpm --filter @objectstack/spec check:generated --fix`: 1 stale
(`content/docs/references/**`), regenerated and re-checked green.
- These suites ran on `699399a4`; `43a7c550` changes one comment digit
in `route-ledger.ts` (the census sentence).
<details><summary>dispatch-gates --commands and the ran list</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 111 derived, the same list on `699399a4`
and `43a7c550`. First pass on `699399a4`: 107 exit 0;
`check:route-ledger-census` exit 1 (the census sentence read 116 against
117 rows: fixed with its own `--fix` in `43a7c550`); exit 3
(prerequisite not met) for `check:skill-examples` (client-react unbuilt)
and `check:dual-build-cjs-loads` (seven packages without `dist/`), both
exit 0 on `43a7c550` after building those packages;
`check-plugin-teardown-shape.mjs --self-test` exit 3 on both heads (its
fixture commit is outside the shallow clone): NOT MEASURED.
Re-run on `43a7c550`, exit 0 each: `check:route-ledger-census`,
`check:skill-examples`, `check:dual-build-cjs-loads`,
`check:type-check-debt`, `check:query-options-erasure`,
`check:route-envelope`, `check:dispatcher-error-vocabulary`,
`check:system-context-census`, `check:spec-docblock-symbol-anchors`,
`check:nul-bytes`, and repo-wide `pnpm lint` (`eslint .
--no-inline-config`).
`dispatch-gates --ran`: "111 derived famil(ies) accounted for — 110 run,
1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0. CI runs
every family.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (exit 0 on `43a7c550`).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [ ] The *Before Submitting* list in CONTRIBUTING.md is done (not
walked item by item in this run).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbWrCtyQtTGt4iYTTvX)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2545021 commit b3d1415
17 files changed
Lines changed: 975 additions & 42 deletions
File tree
- .changeset
- content/docs
- concepts
- deployment
- references/api
- packages
- client/src
- cli
- src
- commands/package
- test
- qa/dogfood/test
- runtime/src
- domains
- spec/src/api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
162 | 164 | | |
163 | 165 | | |
164 | 166 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2995 | 2995 | | |
2996 | 2996 | | |
2997 | 2997 | | |
| 2998 | + | |
2998 | 2999 | | |
2999 | 3000 | | |
3000 | 3001 | | |
| |||
3189 | 3190 | | |
3190 | 3191 | | |
3191 | 3192 | | |
| 3193 | + | |
| 3194 | + | |
| 3195 | + | |
| 3196 | + | |
| 3197 | + | |
| 3198 | + | |
| 3199 | + | |
| 3200 | + | |
| 3201 | + | |
| 3202 | + | |
| 3203 | + | |
| 3204 | + | |
| 3205 | + | |
| 3206 | + | |
| 3207 | + | |
| 3208 | + | |
| 3209 | + | |
| 3210 | + | |
| 3211 | + | |
| 3212 | + | |
| 3213 | + | |
| 3214 | + | |
| 3215 | + | |
| 3216 | + | |
| 3217 | + | |
| 3218 | + | |
| 3219 | + | |
| 3220 | + | |
| 3221 | + | |
| 3222 | + | |
| 3223 | + | |
| 3224 | + | |
| 3225 | + | |
| 3226 | + | |
| 3227 | + | |
| 3228 | + | |
| 3229 | + | |
| 3230 | + | |
| 3231 | + | |
| 3232 | + | |
| 3233 | + | |
| 3234 | + | |
| 3235 | + | |
| 3236 | + | |
| 3237 | + | |
| 3238 | + | |
| 3239 | + | |
| 3240 | + | |
3192 | 3241 | | |
3193 | 3242 | | |
3194 | 3243 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
317 | 317 | | |
318 | 318 | | |
319 | 319 | | |
320 | | - | |
| 320 | + | |
321 | 321 | | |
322 | 322 | | |
323 | 323 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| |||
0 commit comments