Skip to content

feat(metadata-protocol, cloud-connection): an unmanaged install can take a target namespace - #30997

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/project-thread-j4jgmg
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/project-thread-j4jgmg

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

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 (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 同意(An unmanaged install can take a target namespace: the copy is re-namespaced at install through the duplicate-package rename engine #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 duplicatePackaged 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 (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.
  • 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.

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).
  • 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.

Ablations (17, each turns a test red; scripts/ablation-replace.mjs, file restored to HEAD after each)
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
Derived gates (dispatch-gates --commands, reconciled with --ran)

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.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Two optional keys and one describe, at 340bd51a.)
  • 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 (af4a5ad9); check:generated passes.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Generated by Claude Code

…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
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/api/metadata-api.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/api/plugin-endpoints.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/automation/webhooks.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/data-modeling/import-mappings.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/deployment/install-modes.mdx (via targetNamespace (symbol, a field of interface NamespaceRename; a field of interface NamespaceRenameInput), INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/deployment/validating-metadata.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/permissions/authentication.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/permissions/sso.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/ui/forms.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/releases/v17/17-6.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/releases/v18/18-0.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/releases/v18/18-1.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/releases/v18/18-2.mdx (via INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))
  • content/docs/releases/v18/18-3.mdx (via duplicatePackage (symbol, a method of class ProtocolPackageCommitsFacet), INVALID_REQUEST (literal, a string literal in handleInstall; a string literal in unmanagedInstallRenameRefusal))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 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; 104 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 — 174 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 7d9f1e973ecc418c21736a58da2eb211f08a3544 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1fb8b9b7a262ab8aa2595032495546e16a165a68 — the merge of head cacc28d42ca4d7a23c170f9877869be5a0d40761 into base 7d9f1e973ecc418c21736a58da2eb211f08a3544, 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 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

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

… 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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8b377514f88d5e040ca98878bcde829b0fb834a4
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts + packages/spec/authorable-surface/api.json, content/docs/references/api/package-lifecycle.mdx, packages/metadata-protocol/src/package-namespace-rename.ts, packages/metadata-protocol/src/package-unmanaged-install.ts, packages/metadata-protocol/src/protocol-package-commits.ts, packages/metadata-protocol/src/protocol.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/metadata-protocol/src/package-unmanaged-install.test.ts, packages/metadata-protocol/src/package-namespace-rename.test.ts, packages/cloud-connection/src/marketplace-install-local-install-modes.test.ts, packages/objectql/src/protocol-unmanaged-import-rename-battery.test.ts, .changeset/30794-unmanaged-install-target-namespace.md, packages/spec/src/kernel/manifest.zod.ts (cited: the namespace rule), scripts/check-test-source-alias.mjs (cited: the red gate)

Diff read against merge-base 1aeeafd9 (origin/main at the head's plain merge): 14 files, +1966/−361. Card #30794 body and all 7 comments read (claim 6676579365 amended 6676622319, spec consent 6688079820, os-dev-report 6689472616). PR body read. Author is objectstack-fleet[bot].

① Derived judgments

Accepted set and public surface, one line each. "Confirmed" means read in the cited code at the head.

Spec hunk (the consented face)

  • ImportUnmanagedPackageRequestSchema gains targetNamespace?: string (package-lifecycle.zod.ts:408). The schema is .strict(), so a body carrying the key was refused before and is accepted now. Widening. Exactly the key consent 6688079820 names. Correct.
  • ImportUnmanagedPackageResponseSchema gains sourceNamespace?: string (:445) and namespace's describe now covers the renamed case (:441). Exactly the second consented key. The code emits sourceNamespace only when a rename happened (protocol-package-commits.ts:721), so the old answer's key set is unchanged; pinned by "answers no sourceNamespace" in package-unmanaged-install.test.ts. Correct.
  • packages/spec/authorable-surface/api.json: exactly two added rows, api/ImportUnmanagedPackageRequest:targetNamespace and api/ImportUnmanagedPackageResponse:sourceNamespace, nothing else. That is precisely what two optional keys imply. The reference page content/docs/references/api/package-lifecycle.mdx gains the two rows and the re-described namespace row. "Spec property liveness" and Check Changeset are green on the head. Consistent.
  • No other published surface owed a change: the install-local door's body is not a strict spec schema (only installMode is parsed through InstallModeSchema, marketplace-install-local-plugin.ts:1798); installedFrom appears in no packages/spec/src schema. Confirmed.

Scope against the claim (6676622319)

  • Files touched: packages/metadata-protocol/**, packages/cloud-connection/**, packages/objectql/** (the replay of the duplicate battery, named in the claim's surface), content/docs/**, one root .changeset/, and packages/spec/** under the consent. No ui/**, skills/**, .claude/**, no CLI file. Inside the claim.

Correctness

  • One engine, not a fork: planNamespaceRename (package-namespace-rename.ts:142) is the duplicate's inline body moved whole; the diff of protocol-package-commits.ts removes the inline rename (−290 lines) and calls the engine (:288–302, retryVerb: 'duplicate'), reading copyName (:325, :400), objectRefusal (:395) and rewrite (:467). The import calls the same function (package-unmanaged-install.ts:541–547, retryVerb: 'install'; protocol-package-commits.ts:655). REFERENCE_SITES import moved with it. Confirmed; no second copy.
  • duplicatePackage's behaviour is unchanged except one case: copyName (:450–457) now maps a view row whose bare name IS a renamed object's name to that object's copy name (ticket → dupdst_ticket). Before, renameName kept ticket. This is a widening fix someone could meet: a duplicate of a base holding a bare object and its container. The old result stored the copy's container under the source's name (shadowed by name), the new result stores it beside the copied object. No refusal is added to the duplicate: copyNameRefusal is import-only (:472, docblock :82–84), and the ADR-0086 D3 packageId rebind stays in the duplicate (:483). The four protocol-duplicate-package-* files are untouched. Confirmed; a fix, not a narrowing. The changeset names it ("A bare object's view container follows it on a duplicate too").
  • Order and no write before refusal (import): request parse (400) → walled posture (409, :574) → plan: namespace rule (ManifestSchema.shape.namespace.safeParse, package-unmanaged-install.ts:480, regex ^[a-z][a-z0-9_]{1,19}$), reserved base/system/sys (:488, PLATFORM_RESERVED_NAMESPACES :103) → targetPackageId → item enumeration with containers expanded → rename refusals on names (claimed object, grammar METADATA_ITEM_NAME_PATTERN, 63 for objects, 255 for any item; :540–558, engine :472–493) → owned-namespace 409 on the TARGET namespace (:607) → convert then rewrite; a reference/key collision throws INVALID_REQUEST 400 (:655–670) → same-name 409 over the RENAMED names and expansions (:673). The base is installed after all of that. Confirmed.
  • Codes: rename refusals INVALID_REQUEST 400 with details.refused[] (:602–620); collisions and owned namespace RESOURCE_CONFLICT 409; the door's managed+targetNamespace refusal INVALID_REQUEST 400 before any fetch (door :1817–1827). The namespace-rule and reserved refusals use invalidRequest (:285): statusCode: 400 and NO code, which the boundary answers as VALIDATION_ERROR — the dev's own test pins [400, 'VALIDATION_ERROR']. See ③ (should-fix: the prose says INVALID_REQUEST).
  • Idempotency: a second import with the same targetNamespace meets the first copy's base as the namespace owner (409 at :607) and, by default, the same targetPackageId (existing 409). Nothing is merged or renamed a second time. Confirmed.
  • Translations are <targetNamespace>_<locale> with object keys inside renamed (planner test pins unmanagedTranslationItemName('acme','zh-CN'); engine renames record keys at reference sites :347). Confirmed.

Security / tenancy

  • A chosen targetNamespace cannot take another package's names: the registry's getNamespaceOwners refusal (code packages and writable bases alike, :607) plus the per-name unmanagedImportCollisions over every renamed name and expansion against environment rows of any package and managed artifacts (protocol.ts:11990). Both run before the base exists. Pinned by "refuses a target namespace another package owns" and "judges collisions on the RENAMED names". Confirmed.
  • Cross-org: the import is refused on a walled posture (:574, unchanged) and writes environment rows with no organization. targetNamespace adds no org-crossing path. Confirmed.
  • The value is judged by the manifest's own regex before it reaches any identifier, so no table name can carry an unexpected character. Reserved namespaces refused. Confirmed.

Tests and ablations

  • Parity (package-unmanaged-install.test.ts, "the rename parity") stores identical active rows through duplicate and import. The 8 planner + 8 import cases pin each refusal's status and code. The engine file has 6 cases including the container rule (package-namespace-rename.test.ts:67–81). The door has 3 cases (forwarded, unchanged without, refused beside managed/default with no fetch/import/register/ledger). The objectql replay (9 cases) runs both doors on a real kernel and asserts equality of stored items and each refusal. Removing the engine call, the planner branch, either limit, the reserved check, the collision judgement on renamed names, or the door's forward would make named assertions fail; the 17 ablation rows are consistent with what the tests assert. Read as red-on-removal.

② Semver level

  • @objectstack/spec: minor, @objectstack/metadata-protocol: patch, @objectstack/cloud-connection: patch. Clause-②: yes (widening) and Metadata-protocol: added in the PR body and the changeset, both at line start. Right. added is required here: the authorable-surface baseline moved (api.json), which check-changeset-no-major.mjs forbids beside none; the kind must grade at least ONE moved package minor, and spec is. Precedent: GetMetaItemLayeredRequest.member shipped Metadata-protocol: added for an API request key. Skills-docs: none — REASON is present in the PR body as SKILL.md:313 requires for a kind other than none; the reason holds (the only skills/** hit is a plugin-name listing in operations.md:110; no skill teaches installMode or an unmanaged copy). Check Changeset is green.

③ Boundary flags

  • Q6 (not ruled here, assessed for landing safety). Step 1f (door :2286, findEnvironmentBaseHoldingNamespace :3910) is untouched: it refuses a managed install when a WRITABLE base holds the package's declared namespace. Before this PR every unmanaged copy held the source namespace, so "same source package" and "same namespace" were the same test. After it, a RENAMED copy holds targetNamespace, so a later managed install of the source package is not refused by 1f. Assessment: this is not a regression and not a narrowing of an existing guarantee. Every pre-existing state (an unrenamed copy) is refused exactly as before, and the harm 1f guards against — the package's definitions taking over the copy's names at the next boot — cannot happen to a renamed copy, whose items carry different names. What is left unguarded is only the new capability's own edge (a policy question: should "same source package" be refused regardless of names). Landing PR A without the ruling is safe; the maintainer's answer (B, A or C) changes nothing already shipped. No test pins either outcome, as the PR says.
  • Should-fix (prose faces, for the lane seat's ACCEPT): the error code named for a refused or reserved targetNamespace. The changeset ("answers 400 INVALID_REQUEST") and content/docs/deployment/install-modes.mdx (new refusal row "400 INVALID_REQUEST, naming targetNamespace") name INVALID_REQUEST, but package-unmanaged-install.ts:480–495 throws through invalidRequest (:285), which carries statusCode: 400 and no code; the boundary answers VALIDATION_ERROR, and the PR's own planner test asserts exactly that. The same shape the existing targetPackageId refusal has. Either give those two throws code: 'INVALID_REQUEST' (then the sibling rename refusal at :610 and the door at :1818 agree) or correct the two prose claims. Not in the spec hunk (the describe names no code), so not a contract must-fix.
  • Landing condition ② is not met on this head: two red check-runs. Lint · CI, release & engine gates fails at the Test-source alias gate step (its "source reach" half), and Lint & Repo Gates is the lane aggregator of the same failure. Finding, verbatim from the job log: packages/metadata-protocol/src/package-unmanaged-install.test.ts:45: reaches import('@objectstack/lint') in the source under test, at packages/metadata-protocol/src/metadata-diagnostics.ts:494, reached during collection through package-unmanaged-install.test.ts → protocol.ts → metadata-diagnostics.ts, with no module-top load of @objectstack/lint in the test and the package resolving it through dist/. The half judges only test files a change adds or edits; this file is edited by the PR (its line-45 import of ./protocol.js is old, but the file is now in the judged set). The merge-base's lint lane is green, so this is not a same-signature red. Remedy, as the gate prints it: add import '@objectstack/lint'; at module top of packages/metadata-protocol/src/package-unmanaged-install.test.ts. Test-only, one line; the new head then takes a delta review by this reviewer (the change is neither regen nor prose). The other three test files carry no dynamic import()/require() and import only what sibling tests already import, so they trip neither half.
  • The door refuses targetNamespace beside a managed install (400). The door's body is not a strict published schema, so a managed body carrying that key was silently ignored before and is refused now. It is a refusal on a key no published contract declared, not a narrowing of a declared surface; recorded so nobody rediscovers it.
  • Three spellings of the reserved namespaces (PLATFORM_RESERVED_NAMESPACES, objectql's RESERVED_NAMESPACES, the door's inline check in findEnvironmentBaseHoldingNamespace). All read base/system/sys today. Drift risk only; the dev noted it, outside the card.
  • Author objectstack-fleet[bot], so no login to name.
  • check-governed-merges.mjs --pr 30997: 0 of 14 paths hit the governed register; 2327 changed lines, under the 3000 human-merge threshold; ordinary queue landing applies once checks are green. The tier here is owed by the published-schema face, not by a governed path.
  • Check-runs on the head at 2026-11-06T16:19Z: 62 total — 23 success, 8 skipped, 2 failure (named above), 29 in progress (Test Core 1–8, Test UI 1–16, Type Check ×4, Build Core, Build & E2E UI, Live E2E UI). The in-progress runs are not read as green.

④ Design judgment

  • Schema file against the ruling it implements: the maintainer's direction on An unmanaged install can take a target namespace: the copy is re-namespaced at install through the duplicate-package rename engine #30794 (a prefix at install to avoid duplicates) and ADR-0048 §3.5's deferred rename-on-install, under the spec consent of 6688079820. The two optional keys are the minimal request/response shape for that: the request names the namespace, the answer records the provenance only when a rename happened. The describe for targetNamespace states the rule it is judged by (manifest.namespace), the reserved set, and the default — all three are true in the code (:480, :488, :496). The describe for sourceNamespace says "present when the copy was renamed" and the code matches (:721, and targetNamespace equal to the source is treated as no rename, :540).
  • First consumer: the install-local door, which forwards the key and records installedFrom.namespace (door :2222, :3999, :4038), pinned by the door's tests. A managed install can never reach the rename (door :1818). The parity anchor shows the second door (duplicate) yields identical items, so the published semantics "the rename duplicatePackage applies" in the describe is a measured claim, not prose.
  • The check:generated artifacts and the reference page match the schema. Holds.

Implemented-by: claude/project-thread-j4jgmg
Reviewed-by: session_016WV4cytLmJxfAmgPVjUbbW

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract review · delta

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a39d3108790bca29ff42fe3de60bbb2079a01bb6
Local-runs: none
Scope: packages/metadata-protocol/src/package-unmanaged-install.test.ts + packages/metadata-protocol/src/lint-root-preload.testkit.ts (read at the head), scripts/check-test-source-alias.mjs (read at the head and at origin/main acd190b2, the #30969 merge that holds the source-reach half)

Delta over record 6689730882 (head 8b377514…). The new head's sole parent is 8b377514f88d5e040ca98878bcde829b0fb834a4, the sha that record reviewed. git diff 8b377514 a39d3108 --stat is exactly one file, packages/metadata-protocol/src/package-unmanaged-install.test.ts, +2/−0: a one-line comment ("Pays the lint root's first load at collection, never inside a clocked case.") and a module-top import './lint-root-preload.testkit.js';, placed first, before the vitest import. Commit test(metadata-protocol): unmanaged-install test pays the lint root at collection. Nothing under packages/spec/**, no source file, no docs, no changeset moved, so every judgment in ①, ②, ④ of the prior record stands unchanged; this delta re-reads only the one landing blocker it named.

① Derived judgments

  • The remedy is the one the gate asks for, spelled the sibling way. lint-root-preload.testkit.ts exists on the head and is a test-only file whose single statement is import '@objectstack/lint'; at module scope (its docblock names exactly this case: loadFormLayoutLint in metadata-diagnostics.ts, resolved through lint/dist/index.js). 41 other packages/metadata-protocol/src/*.test.ts files import it with the identical relative line, so the PR's test now matches its siblings rather than inventing a second spelling. Confirmed.
  • The source-reach half credits a relative hop. Read at acd190b2 (the merged ci(test-source-alias): follow a changed test into the source it loads, and red a cold literal import() paid there #30969 script, which is what the lane runs on the merge ref; the PR branch's own copy predates it): sourceReachFindings walks the test's collection-time reach inside the package, unions every reached file's module-scope loads into warm, and drops a finding when warm.has(spec). The testkit is a same-package relative static import, so @objectstack/lint is in warm and the import('@objectstack/lint') at metadata-diagnostics.ts:494 no longer reports. The script's own source-reach-warm control fixture pins this exact shape ("a load another reached file already pays during collection was reported" is a self-test failure). The clocked-window half is unaffected (the test file still has no dynamic import of its own). The flagged finding is answered by the rule's own words, not by a workaround.
  • Test behaviour otherwise unchanged. A side-effect import that only loads a package already loaded lazily by the source under test moves where the first load is paid (collection, unclocked) and yields the same module instance to the protocol's later import(). No case, assertion, fixture or timeout in the file moved; the diff has no other hunk. Confirmed.
  • Spec hunk, authorable surface, docs, changeset: untouched since 8b377514, so ① of 6689730882 holds as written.

② Semver level

Unchanged: @objectstack/spec minor, @objectstack/metadata-protocol patch, @objectstack/cloud-connection patch; Clause-②: yes (widening), Metadata-protocol: added. A test-only commit moves no package's grade.

③ Boundary flags

  • The prior record's landing blocker (Test-source alias gate, source-reach half, on this test file) is addressed on this head by the change above. Whether the lane itself reports green is a CI fact, recorded below.
  • The should-fix on the prose error code (INVALID_REQUEST vs the VALIDATION_ERROR the planner refusals actually answer) is not touched by this commit and stays open as a should-fix, not a contract must-fix.
  • Q6 (1f and a renamed copy) unchanged; still safe to land without the ruling.
  • Check-runs on a39d3108 at 2026-11-06T16:29Z: 42 registered — 20 success, 7 skipped, 0 failure, 15 in progress (Test Core 1–8, Build Core, Build UI Deps, Live E2E UI, Type Check · workspace / debt ledger / consumer gates, Lint · repo & tooling gates). Lint · CI, release & engine gates completed green at 16:28:40Z (it was the red lane on 8b377514, failing at the Test-source alias gate step; it took the same ~6 min this time). No red on the head. The Lint & Repo Gates aggregator and the rest of the prior head's 62 runs have not registered yet; the 15 in-progress runs are not read as green. Landing condition ② is met for the lane that blocked and is otherwise pending on the remaining runs.

Implemented-by: claude/project-thread-j4jgmg
Reviewed-by: session_016WV4cytLmJxfAmgPVjUbbW

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

  • Contract review: CONTRACT_REVIEW_TIER. Full record 6689730882 on 8b377514: PASS, no must-fix; its one landing blocker was the Test-source alias gate. Delta record 6689887821 on this head: PASS. The one commit is a module-top testkit import in package-unmanaged-install.test.ts, and the lint lane that was red is now green.
  • Spec consent: the maintainer's 「以上都同意。」 (record 6688079820): targetNamespace on the request and sourceNamespace on the response, nothing else. The api.json diff is exactly those two rows.
  • Read by the seat:
    • One rename engine (planNamespaceRename), moved whole out of the duplicate door, not forked. The duplicate's only behaviour change is the stated view-container fix: a bare-named view container now follows its object's copy name instead of shadowing the source. That is a widening.
    • Every refusal answers before the base exists.
    • A chosen targetNamespace cannot take over another package's items. The namespace-owner check and the per-name collision check over the renamed names both run before any write. Reserved namespaces are refused.
  • Q6 (renamed copies and step 1f) is not ruled. The review finds that PR A opens no hole that existed before. Step 1f is untouched and still refuses a managed install over an unrenamed copy. A renamed copy's items carry different names, so the takeover 1f guards against cannot happen to it. Landing without the ruling is safe, and the ruling goes with PR B.
  • Should-fix, carried:
    • The changeset and install-modes.mdx say a refused or reserved targetNamespace answers 400 INVALID_REQUEST. The code answers VALIDATION_ERROR through the door, with the reason only in the server log. I corrected the PR body. The prose-or-code choice goes into the card handoff, with the prepared prose patch.
    • Reserved namespaces are spelled in three places.
  • Gates: Lint · CI, release & engine gates is green on this head; at the time of this record the long lanes were still running and none was red. Locally: dev report 6689472616 (108/108 families on 8b377514). Auto-merge waits for the rest.
  • Changesets: @objectstack/spec minor; metadata-protocol and cloud-connection patch; Metadata-protocol: added (the authorable-surface shard moved), with its Skills-docs: line; Clause-②: yes (widening). All right.
  • Landing: 2,327 changed lines, no governed path. Ready, then auto-merge, through the relay. Part of #30794: PR B (the ADR-0048 §3.5 amendment, the ADR-0131 D6 note, and Q6) goes back to the queue with a handoff, because the lane is winding down.

…-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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit f4d9c09 Nov 6, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-j4jgmg branch November 6, 2026 19:55
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/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant