Repository navigation
feat(metadata-protocol, cloud-connection): an unmanaged install can take a target namespace - #30997
Conversation
…amespace, and its answer the source namespace of a renamed copy ImportUnmanagedPackageRequestSchema gains `targetNamespace` (optional): the namespace the copy is renamed into. ImportUnmanagedPackageResponseSchema's `namespace` describes the request's target when it named one, and an optional `sourceNamespace` records the namespace a renamed copy's names came from. Claude-Session: https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H Co-authored-by: Claude <noreply@anthropic.com>
…ke a target namespace, renamed by the duplicate's own rename engine The rename logic of duplicatePackage moves whole into one pure engine, package-namespace-rename.ts, which the duplicate now calls. The unmanaged import's targetNamespace calls the same engine after the view containers are expanded: every copied object name takes the target namespace, every reference follows, translations are named for it, and every refusal the rename can meet is raised before the base exists. The install-local door passes targetNamespace to an unmanaged install, refuses it beside a managed one, and records the source namespace in installedFrom. Claude-Session: https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H Co-authored-by: Claude <noreply@anthropic.com>
…d the install-local door's targetNamespace The engine's own pins: its storage limits read off their declarations, a copy name the item grammar refuses is refused naming the item, an item the copy does not rename is never judged by name, and the remedy names the door. The door forwards targetNamespace to an unmanaged install, records the source namespace in installed_from, and refuses it on a managed install before any fetch. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H
…'s copy name; the duplicate battery replayed through the import A view container is named by the object it binds, which is how the unmanaged import names every container and how a stored base holds one. The prefix swap kept a bare object's container under its old name while the body's object followed the copy, so the copy's container sat under the name the source's container holds. The engine now gives such a view its object's copy name, in both doors. The objectql battery replay declares each duplicate fixture as a package body and imports it into the same namespace on a real kernel: the copy stores the same rows the duplicate stores, and every item the duplicate fails is refused by the import before anything is written. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H
…cle reference for targetNamespace Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H
…e of its own Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H
📓 Docs Drift CheckThis PR changes 3 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 174 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 1fb8b9b7a262ab8aa2595032495546e16a165a68 && git checkout 1fb8b9b7a262ab8aa2595032495546e16a165a68
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7d9f1e973ecc418c21736a58da2eb211f08a3544 cacc28d42ca4d7a23c170f9877869be5a0d40761 && git checkout -B drift-repro 7d9f1e973ecc418c21736a58da2eb211f08a3544 && git merge --no-ff cacc28d42ca4d7a23c170f9877869be5a0d40761
node scripts/docs-audit/affected-docs.mjs --json 7d9f1e973ecc418c21736a58da2eb211f08a3544
|
… collection The source-reach half of check:test-source-alias (on main since #30969) judges this edited file: its cases reach the protocol's lazy import('@objectstack/lint') (metadata-diagnostics.ts) inside a clocked window. Import ./lint-root-preload.testkit.js at module top, as the sibling protocol test files do. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRibYnMTw289qPx3H
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW Contract reviewServed-tier: Diff read against merge-base ① Derived judgmentsAccepted set and public surface, one line each. "Confirmed" means read in the cited code at the head. Spec hunk (the consented face)
Scope against the claim (6676622319)
Correctness
Security / tenancy
Tests and ablations
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW Contract review · deltaServed-tier: Delta over record 6689730882 (head ① Derived judgments
② Semver levelUnchanged: ③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW 席位核对 · ACCEPT · 2026-11-06T16:31Z · #30794 (PR A: unmanaged install takes a target namespace)Head-sha: a39d310
|
…-j4jgmg Conflict in packages/cloud-connection/src/marketplace-install-local-plugin.ts: main's dryRun refusals (step 0') and this branch's targetNamespace refusal (now step 0'') both sit right after the install-mode parse; both kept, main's first, so every body main already refused is refused with the same answer. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwymhkbKfSLjxsStXu
|
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH 席位核对 · ACCEPT (refresh) · 2026-11-06T19:32Z · #30794 (PR A: unmanaged install takes a target namespace)Head-sha: cacc28d
|
Requested by Elon · project thread
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 (noinstallMode,--install-modeor 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-j4jgmgSpec consent: the maintainer, 2026-11-06T15:07Z, recorded on #30794 (comment 6688079820):
targetNamespaceon the request andsourceNamespaceon the response维护者速读
targetNamespace,副本整体改名进新命名空间。对象名、视图、翻译和所有引用都会跟着改,规则与「复制包」完全相同,两者共用同一个改名引擎。入口有两个:importUnmanagedPackage,以及POST /api/v1/marketplace/install-local带installMode: 'unmanaged'。不传时行为与以前一样。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 的导入」,存下的条目一模一样。What changes
@objectstack/metadata-protocol: one rename engine, two callers (package-namespace-rename.ts, new)planNamespaceRename({ sourcePackageId, sourceNamespace, targetNamespace, items, retryVerb })is the renameduplicatePackageapplied 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)andrewrite(type, name, body).rewritecovers the reference rewrite, the record-key collisions and the permissionfieldskeys.duplicatePackagecalls it. Only the remedy verb is a parameter ("then duplicate again" / "then install again"). The ADR-0086 D3 permissionpackageIdrebind stays in the duplicate, where it was.ticket→dupdst_ticket), the swap keptticketwhile the body'sobjectfollowed 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 forticket.board. The objectql replay found it (below).@objectstack/metadata-protocol: the unmanaged import takestargetNamespaceplanUnmanagedImport(manifest, { targetNamespace }):ManifestSchema.shape.namespaceand refuses the platform-reservedbase,systemandsys(400VALIDATION_ERRORthrough the door, whose answer says the reason is in the server log; the log line namestargetNamespace. The changeset andinstall-modes.mdxstill sayINVALID_REQUEST: a known prose mismatch, carried to the card's handoff with the option of giving these two throws theINVALID_REQUESTcode).INVALID_REQUEST, every item named, ending "Nothing was installed."): an object whose copy name is claimed, and a copy name that breaksMETADATA_ITEM_NAME_PATTERN, the 63-character table identifier limit (objects) or thesys_metadata.namecolumn width (255, read off its declaration).<targetNamespace>_<locale>, with the object keys inside them renamed.importUnmanagedPackage:unmanagedImportCollisions, 409) runs on the renamed names. The owned-namespace 409 namestargetNamespacewhen one was given.sourceNamespacewhen the copy was renamed, and has no such key otherwise.@objectstack/cloud-connection: the install-local door (MarketplaceInstallLocalPlugin)targetNamespaceis forwarded to the import on both sources (inline manifest and cloud fetch).installMode, it answers 400INVALID_REQUESTbefore anything is fetched: a managed install is never renamed.installedFromgainsnamespace(the package's own) besidepackageIdandversion, which is the provenance Ask 5 names. The operator log line saysrenamed from '<source>'. Both are unchanged withouttargetNamespace.@objectstack/spec(api/package-lifecycle.zod.ts):ImportUnmanagedPackageRequestSchema.targetNamespace(optional) andImportUnmanagedPackageResponseSchema.sourceNamespace(optional). The responsenamespacedescribe now covers both cases. The authorable-surface shard and the package-lifecycle reference are regenerated.Docs:
content/docs/deployment/install-modes.mdxgains "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
package-unmanaged-install.test.ts, "the rename parity"). One package goes two ways. In one, it is imported as a base and thenduplicatePackaged intocom.example.acme/acme. In the other, it is imported straight intocom.example.acmewithtargetNamespace: '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.packages/objectql/src/protocol-unmanaged-import-rename-battery.test.ts, new, 9 cases). Each fixture of the fourprotocol-duplicate-package-*files is declared as a package body. Both doors run on a realObjectKernel+ObjectQLPlugin.fieldskeys) store identical rows through both doors, and each also asserts the battery's own pins on the import's rows.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, afieldskey collision, anobjectskey collision in both key orders, a translation key collision, and a hook entry collision with its negative control.package-unmanaged-install.test.ts(8 planner cases + 8 import cases) andpackage-namespace-rename.test.ts(new, 6 engine cases, including the container rule).marketplace-install-local-install-modes.test.ts, 3 new cases.targetNamespaceis forwarded andinstalledFrom.namespaceis 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.protocol-duplicate-package-*files are green as they are, 20/20, after the extraction.Acceptance notes
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.packages/spec/src/api/package-lifecycle.zod.tschanges: two optional keys and one describe, in commit340bd51a, with the regenerated artifacts inaf4a5ad9. Nodomain:specseat held the hunk while it waited, so the maintainer consented directly (An unmanaged install can take a target namespace: the copy is re-namespaced at install through the duplicate-package rename engine #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.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 declarespackageIdon a set is judged by the save door as before.os package installdoes not sendtargetNamespace. The docs give the HTTP body.PLATFORM_RESERVED_NAMESPACES(base,system,sys) sits beside objectql'sRESERVED_NAMESPACESand the door's inline check. All three agree today, and unifying them is outside this card's surface.ticketwhere the battery usesticket_views.Verification
Head
a39d3108adds one commit on top of8b377514: a module-topimport './lint-root-preload.testkit.js';inpackage-unmanaged-install.test.ts, for main's Test-source alias gate (source-reach half, #30969). The measurements below are on8b377514.Local tests and type-checks (head 8b37751, main 1aeeafd merged in; #30687 landed before it)
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).typecheckfor metadata-protocol, cloud-connection, objectql and spec: exit 0 (objectql and spec includecheck:test-typecheck).pnpm --filter @objectstack/spec check:generated: exit 0.pnpm buildunder the verify lock: 118/118 tasks.pnpm lint: exit 0.check:test-typecheckfor spec ran out of heap at the default Node limit in this container. WithNODE_OPTIONS=--max-old-space-size=8192it exits 0 (240 errors / 131 signatures held, unchanged). Every gate below ran with that heap.Ablations (17, each turns a test red; scripts/ablation-replace.mjs, file restored to HEAD after each)
rename.rewriteinimportUnmanagedPackage)targetNamespacedeclaration checksourceNamespaceinstalledFrom.namespacetargetNamespaceto the importtargetNamespaceto its unmanaged installerDerived gates (dispatch-gates --commands, reconciled with --ran)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 108 families on head8b377514. Each was run with its exit code captured before any pipe.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 guardsjob (52 run steps fromlint.yml,check-cli-command-idsincluded, the lane verdict step left out) was also run locally: 52/52 exit 0.Human Minimum
packages/specholds no runtime logic. (Two optional keys and one describe, at340bd51a.)pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed (af4a5ad9);check:generatedpasses.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code