Repository navigation
Commit f4d9c09
feat(metadata-protocol, cloud-connection): an unmanaged install can take a target namespace (#30997)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D53oweAj47PTfWyBvvWKdXL)_
domain:server#1 · os-elon-musk · session_016WV4cxRibYnMTw289qPx3H
Part of #30794. This PR is Ask items 1–7 and 9. Ask item 8 (the ADR-0048
§3.5 amendment and the ADR-0131 D6 note) is PR B, which opens after this
one lands. A managed install after a *renamed* copy is waiting on the
maintainer (see Acceptance notes).
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — `skills/**` teaches no install mode (no
`installMode`, `--install-mode` or unmanaged copy appears in it), and
this PR adds no CLI flag. The docs page that teaches install modes,
`content/docs/deployment/install-modes.mdx`, is updated here.
Claim: 6676579365 (amended 6676622319) · Branch:
`claude/project-thread-j4jgmg`
Spec consent: the maintainer, 2026-11-06T15:07Z, recorded on #30794
(comment 6688079820): `targetNamespace` on the request and
`sourceNamespace` on the response
## 维护者速读
- 改了什么:非托管安装可以指定
`targetNamespace`,副本整体改名进新命名空间。对象名、视图、翻译和所有引用都会跟着改,规则与「复制包」完全相同,两者共用同一个改名引擎。入口有两个:`importUnmanagedPackage`,以及
`POST /api/v1/marketplace/install-local` 带 `installMode:
'unmanaged'`。不传时行为与以前一样。
- 为什么改:同一个包要装两份可编辑副本,或者想放在原包旁边各用各的名字,以前做不到,因为副本必须沿用源包的命名空间。ADR-0048 §3.5
把这件事记为待办,本卡就是来做它的。
- 风险与回滚:中等。复制包的改名逻辑原样搬进了一个共享函数,原有复制测试 20/20 仍全绿。顺带修了一处:裸名对象(如
`ticket`)的视图容器,复制后不再停在旧名上,两个入口都修了。托管安装带 `targetNamespace` 一律返回 400。回滚即撤
PR。
-
先看:`packages/metadata-protocol/src/package-namespace-rename.ts`(唯一的改名引擎);`package-unmanaged-install.ts`
里 `planUnmanagedImport`
的改名一段(容器展开之后才改名,所有拒绝都在写入之前);`protocol-package-commits.ts` 里
`duplicatePackage` 改为调用这个引擎的地方。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run
src/package-unmanaged-install.test.ts -t parity`。预期:同一个包分别走「复制」和「带
targetNamespace 的导入」,存下的条目一模一样。
- 需要你做的:spec 改动你已在 15:07Z 同意(#30794 评论
6688079820)。还剩一件等你拍板:改名后的副本旁边,再装同一个源包的托管版要不要拒绝(Q6 决策卡)。本 PR
不改这条规则。其余由席位复核、落地。
## What changes
**`@objectstack/metadata-protocol`: one rename engine, two callers**
(`package-namespace-rename.ts`, new)
- `planNamespaceRename({ sourcePackageId, sourceNamespace,
targetNamespace, items, retryVerb })` is the rename `duplicatePackage`
applied inline before this PR, moved verbatim with its comments. It is a
pure function over item bodies. It returns the object rename map, the
up-front object refusals, `copyName(type, name)`, `copyNameRefusal(type,
name)` and `rewrite(type, name, body)`. `rewrite` covers the reference
rewrite, the record-key collisions and the permission `fields` keys.
- `duplicatePackage` calls it. Only the remedy verb is a parameter
("then duplicate again" / "then install again"). The ADR-0086 D3
permission `packageId` rebind stays in the duplicate, where it was.
- **One fix, in both doors: a view named by its object follows the
object whole.** A view container is named by the object it binds. The
import names every container that way, and a stored base holds it under
that name. So a view whose name *is* a copied object's name now takes
that object's copy name. For a prefixed object this is the name the
prefix swap already gave. For a bare one (`ticket` → `dupdst_ticket`),
the swap kept `ticket` while the body's `object` followed the copy. The
copy's container then sat under the name the source's container holds,
the same shadowing the qualified-view rule already closes for
`ticket.board`. The objectql replay found it (below).
**`@objectstack/metadata-protocol`: the unmanaged import takes
`targetNamespace`**
- `planUnmanagedImport(manifest, { targetNamespace })`:
- It judges the value by `ManifestSchema.shape.namespace` and refuses
the platform-reserved `base`, `system` and `sys` (400 `VALIDATION_ERROR`
through the door, whose answer says the reason is in the server log; the
log line names `targetNamespace`. The changeset and `install-modes.mdx`
still say `INVALID_REQUEST`: a known prose mismatch, carried to the
card's handoff with the option of giving these two throws the
`INVALID_REQUEST` code).
- It enumerates the items under the source namespace, expanding view
containers. Only then does it run the engine, so every name a container
expands into takes its object's copy name.
- Before the base exists it refuses (400 `INVALID_REQUEST`, every item
named, ending "Nothing was installed."): an object whose copy name is
claimed, and a copy name that breaks `METADATA_ITEM_NAME_PATTERN`, the
63-character table identifier limit (objects) or the `sys_metadata.name`
column width (255, read off its declaration).
- Translations are named `<targetNamespace>_<locale>`, with the object
keys inside them renamed.
- `importUnmanagedPackage`:
- Each body is converted to today's protocol under the name the package
declares, then rewritten by the engine, the order the duplicate rewrites
in. A reference collision the converted body meets refuses the install
before any write.
- The same-name check (`unmanagedImportCollisions`, 409) runs on the
renamed names. The owned-namespace 409 names `targetNamespace` when one
was given.
- The answer carries `sourceNamespace` when the copy was renamed, and
has no such key otherwise.
**`@objectstack/cloud-connection`: the install-local door**
(`MarketplaceInstallLocalPlugin`)
- `targetNamespace` is forwarded to the import on both sources (inline
manifest and cloud fetch).
- With a managed install, or no `installMode`, it answers 400
`INVALID_REQUEST` before anything is fetched: a managed install is never
renamed.
- `installedFrom` gains `namespace` (the package's own) beside
`packageId` and `version`, which is the provenance Ask 5 names. The
operator log line says `renamed from '<source>'`. Both are unchanged
without `targetNamespace`.
**`@objectstack/spec`** (`api/package-lifecycle.zod.ts`):
`ImportUnmanagedPackageRequestSchema.targetNamespace` (optional) and
`ImportUnmanagedPackageResponseSchema.sourceNamespace` (optional). The
response `namespace` describe now covers both cases. The
authorable-surface shard and the package-lifecycle reference are
regenerated.
**Docs**: `content/docs/deployment/install-modes.mdx` gains "Installing
the copy under a namespace of its own" (the body, the rename rules,
every refusal, the provenance) and one row in the door's refusal table.
## Tests
- **The parity anchor** (`package-unmanaged-install.test.ts`, "the
rename parity"). One package goes two ways. In one, it is imported as a
base and then `duplicatePackage`d into `com.example.acme` / `acme`. In
the other, it is imported straight into `com.example.acme` with
`targetNamespace: 'acme'`. The active rows of the two copies are
identical: objects, a lookup, free text, a view item, a container, a
translation and a form action.
- **The duplicate battery, replayed through the import on a real
kernel**
(`packages/objectql/src/protocol-unmanaged-import-rename-battery.test.ts`,
new, 9 cases). Each fixture of the four `protocol-duplicate-package-*`
files is declared as a package body. Both doors run on a real
`ObjectKernel` + `ObjectQLPlugin`.
- The three copy fixtures (names in step, the bare object, permission
`fields` keys) store identical rows through both doors, and each also
asserts the battery's own pins on the import's rows.
- Every item the duplicate reports in `failed[]` is refused by the
import, with no base, no row and no history row naming the target. The
cases are: the claimed bare-object name, a `fields` key collision, an
`objects` key collision in both key orders, a translation key collision,
and a hook entry collision with its negative control.
- **One test per refusal**: `package-unmanaged-install.test.ts` (8
planner cases + 8 import cases) and `package-namespace-rename.test.ts`
(new, 6 engine cases, including the container rule).
- **The door**: `marketplace-install-local-install-modes.test.ts`, 3 new
cases. `targetNamespace` is forwarded and `installedFrom.namespace` is
recorded. Without it, nothing changes. Beside a default or a managed
install it is refused with no fetch, no import, no register and no
ledger.
- **Unchanged**: the four `protocol-duplicate-package-*` files are green
as they are, 20/20, after the extraction.
## Acceptance notes
- **The managed install after a *renamed* copy (Q6) is not decided
here.** The door's managed-after-unmanaged refusal (step 1f) is
namespace based. A renamed copy holds `targetNamespace`, not the
package's, so that refusal does not see it. The decision card is with
Elon. Step 1f is untouched, the docs say nothing about that case, and no
test pins either outcome.
- **The spec hunk, under the maintainer's consent.** The import's
request is a strict spec schema, so
`packages/spec/src/api/package-lifecycle.zod.ts` changes: two optional
keys and one describe, in commit `340bd51a`, with the regenerated
artifacts in `af4a5ad9`. No `domain:spec` seat held the hunk while it
waited, so the maintainer consented directly (#30794 comment
6688079820). The consent covers the spec hunk only. The contract review
and the other landing conditions are still owed. The hunk raises this
PR's review to CONTRACT_REVIEW_TIER.
- **Ask 4's name judgement (grammar, 63, 255) and the reserved-namespace
refusal are import-only.** The duplicate's refusals are exactly what
they were before this PR, so the duplicate is not narrowed here.
- **The import does not rebind a permission set's `packageId`.** The
duplicate rebinds a set that declares its source package (ADR-0086 D3).
The import never did, with or without a rename, and a package body that
declares `packageId` on a set is judged by the save door as before.
- **No CLI flag.** `os package install` does not send `targetNamespace`.
The docs give the HTTP body.
- **The reserved namespaces are spelled a third time.**
`PLATFORM_RESERVED_NAMESPACES` (`base`, `system`, `sys`) sits beside
objectql's `RESERVED_NAMESPACES` and the door's inline check. All three
agree today, and unifying them is outside this card's surface.
- **The replay lives in a new objectql file** rather than in the four
battery files, so the battery files are untouched. A container is named
by its object in the import, so the bare-object fixture holds its
container as `ticket` where the battery uses `ticket_views`.
- **cloud#3600 comment 6676044775 was not readable** from this session
(403), so it is NOT MEASURED here.
## Verification
Head `a39d3108` adds one commit on top of `8b377514`: a module-top
`import './lint-root-preload.testkit.js';` in
`package-unmanaged-install.test.ts`, for main's Test-source alias gate
(source-reach half, #30969). The measurements below are on `8b377514`.
<details><summary>Local tests and type-checks (head 8b37751, main
1aeeafd merged in; #30687 landed before it)</summary>
- `pnpm --filter @objectstack/metadata-protocol test`: 321 files passed
(4 skipped); 22510 tests passed.
- `pnpm --filter @objectstack/cloud-connection test`: 72 files, 921
tests passed.
- `pnpm --filter @objectstack/objectql exec vitest run
src/protocol-duplicate-package
src/protocol-unmanaged-import-rename-battery.test.ts`: 5 files, 29 tests
passed (the 20 battery cases unchanged, plus the 9 replay cases).
- `typecheck` for metadata-protocol, cloud-connection, objectql and
spec: exit 0 (objectql and spec include `check:test-typecheck`).
- `pnpm --filter @objectstack/spec check:generated`: exit 0.
- Whole-tree `pnpm build` under the verify lock: 118/118 tasks.
- `pnpm lint`: exit 0.
`check:test-typecheck` for spec ran out of heap at the default Node
limit in this container. With `NODE_OPTIONS=--max-old-space-size=8192`
it exits 0 (240 errors / 131 signatures held, unchanged). Every gate
below ran with that heap.
</details>
<details><summary>Ablations (17, each turns a test red;
scripts/ablation-replace.mjs, file restored to HEAD after
each)</summary>
| Removed | Red |
|:---|:---|
| The import's call to the engine (`rename.rewrite` in
`importUnmanagedPackage`) | 5, the parity among them |
| The planner's rename branch | 11 |
| The reserved-namespace refusal | 1 |
| The `targetNamespace` declaration check | 1 |
| The planner's claimed-object refusal | 1 |
| The engine's item-grammar refusal | 1 |
| The 63-character table limit | 2 |
| The 255-character item-name limit | 2 |
| The expanded-view refusal | 1 |
| The import's throw on a reference collision | 1 |
| The same-name check on the renamed names (judged on source names
instead) | 1 |
| The answer's `sourceNamespace` | 2 |
| The container-follows-its-object rule | 1 |
| The door's managed refusal | 2 |
| The door's `installedFrom.namespace` | 1 |
| The door forwarding `targetNamespace` to the import | 1 |
| The door forwarding `targetNamespace` to its unmanaged installer | 1 |
</details>
<details><summary>Derived gates (dispatch-gates --commands, reconciled
with --ran)</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 108 families on head `8b377514`.
Each was run with its exit code captured before any pipe.
```
✓ dispatch-gates --ran: 108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED (a DERIVED zero — all 108 recorded an exit code and none of them is 3).
```
All 108 exited 0 on this head, with the whole-tree build and the
teardown self-test's pinned fixture commit in place. On the earlier head
those three exited 3 until their prerequisites were met.
The whole `Lint · docs & code guards` job (52 run steps from `lint.yml`,
`check-cli-command-ids` included, the lane verdict step left out) was
also run locally: 52/52 exit 0.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Two optional keys and one describe, at `340bd51a`.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [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 (`af4a5ad9`); `check:generated` passes.
- [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.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H)_
<!-- signed: domain:server#1 · os-elon-musk ·
session_016WV4cytLmJxfAmgPVjUbbW -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3fe5f2a commit f4d9c09
14 files changed
Lines changed: 1968 additions & 361 deletions
File tree
- .changeset
- content/docs
- deployment
- references/api
- packages
- cloud-connection/src
- metadata-protocol/src
- objectql/src
- spec
- authorable-surface
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
| 95 | + | |
95 | 96 | | |
96 | 97 | | |
97 | 98 | | |
| |||
144 | 145 | | |
145 | 146 | | |
146 | 147 | | |
147 | | - | |
| 148 | + | |
| 149 | + | |
148 | 150 | | |
149 | 151 | | |
150 | 152 | | |
| |||
215 | 217 | | |
216 | 218 | | |
217 | 219 | | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
218 | 265 | | |
219 | 266 | | |
220 | 267 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
| 249 | + | |
249 | 250 | | |
250 | 251 | | |
251 | 252 | | |
| |||
259 | 260 | | |
260 | 261 | | |
261 | 262 | | |
262 | | - | |
| 263 | + | |
| 264 | + | |
263 | 265 | | |
264 | 266 | | |
265 | 267 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
506 | 506 | | |
507 | 507 | | |
508 | 508 | | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
76 | 84 | | |
77 | 85 | | |
78 | 86 | | |
| |||
1841 | 1849 | | |
1842 | 1850 | | |
1843 | 1851 | | |
| 1852 | + | |
| 1853 | + | |
| 1854 | + | |
| 1855 | + | |
| 1856 | + | |
| 1857 | + | |
| 1858 | + | |
| 1859 | + | |
| 1860 | + | |
| 1861 | + | |
| 1862 | + | |
| 1863 | + | |
| 1864 | + | |
| 1865 | + | |
| 1866 | + | |
| 1867 | + | |
| 1868 | + | |
| 1869 | + | |
| 1870 | + | |
1844 | 1871 | | |
1845 | 1872 | | |
1846 | 1873 | | |
| |||
2233 | 2260 | | |
2234 | 2261 | | |
2235 | 2262 | | |
| 2263 | + | |
2236 | 2264 | | |
2237 | 2265 | | |
2238 | 2266 | | |
| |||
4007 | 4035 | | |
4008 | 4036 | | |
4009 | 4037 | | |
| 4038 | + | |
| 4039 | + | |
4010 | 4040 | | |
4011 | 4041 | | |
4012 | | - | |
| 4042 | + | |
4013 | 4043 | | |
4014 | 4044 | | |
4015 | 4045 | | |
| |||
4029 | 4059 | | |
4030 | 4060 | | |
4031 | 4061 | | |
| 4062 | + | |
4032 | 4063 | | |
4033 | 4064 | | |
4034 | 4065 | | |
| |||
4059 | 4090 | | |
4060 | 4091 | | |
4061 | 4092 | | |
| 4093 | + | |
| 4094 | + | |
| 4095 | + | |
| 4096 | + | |
| 4097 | + | |
4062 | 4098 | | |
4063 | 4099 | | |
4064 | 4100 | | |
| 4101 | + | |
4065 | 4102 | | |
4066 | 4103 | | |
4067 | 4104 | | |
4068 | | - | |
| 4105 | + | |
| 4106 | + | |
4069 | 4107 | | |
4070 | 4108 | | |
4071 | 4109 | | |
| |||
0 commit comments