Repository navigation
Description
Context
ADR-0048 v1 shipped the install-time namespace gate (#1810): SchemaRegistry.installPackage throws NamespaceConflictError when a package's namespace is already owned by a different installed package (shareable base/system/sys exempt; same-package reinstall allowed; OS_METADATA_COLLISION=warn downgrades to a warning).
That protects a single environment at install time. The original ADR discussion also raised a publish-time / marketplace half that was deferred: when a vendor publishes a package to the platform/marketplace, the platform should register and exclusively reserve its namespace globally, so two vendors can't both ship crm and collide on a tenant that installs both.
Why install-time alone isn't enough
- Install-time only catches a conflict after both packages reach the same env. The marketplace can prevent the conflict before distribution.
- Local dev can't be policed (no central authority) — that's accepted. The marketplace IS the central authority, so it can enforce.
- Reverse-domain package ids are globally unique (route/identity key, ADR-0048 option A); namespaces (object table prefixes) are not, and a namespace collision is what actually breaks the DB/table layer.
Scope (open-source vs enterprise)
This is enterprise/marketplace surface. The open-source repo keeps the install-time gate + federation hooks; the registration/exclusivity service likely lives in the private marketplace backend. Define the contract here, implement enforcement in the enterprise tier.
Proposed direction (needs ADR-0048 addendum)
- Publish API: on publish, reserve
(namespace → owning package id / vendor)in a platform registry; reject a publish whose namespace is owned by a different vendor. - Rename / transfer flow: deferred in the original ADR ("暂不考虑"); revisit whether a vendor can rename a namespace or transfer ownership.
- Install-time cross-check: optionally consult the platform registry at install so a sideloaded package whose namespace is reserved by another vendor is flagged.
- Tenant override / alias: per the ADR, namespace may carry a per-tenant friendly alias distinct from the global reservation — confirm how that interacts with exclusivity.
Open questions
- Granularity of reservation: namespace only, or
(namespace, version-range)? - What about shareable namespaces (
base/system/sys) and platform-owned ones? - Grandfathering already-published packages with colliding namespaces.
Acceptance
- Publishing two different vendors' packages that claim the same namespace is rejected at publish time with a clear error.
- Documented contract so the open-source install-time gate and the enterprise publish-time gate compose without drift.
Related: ADR-0048, #1810 (install-time gate).
Activity
os-zhuang commentedon Aug 5, 2026
分诊:转决策箱
needs-user-decision,预挂domain:engine-core。分类理由:本单是 ADR-0048 的 publish-time / marketplace 半边 —— 需要 ADR addendum、保留粒度 / 共享命名空间 / 存量灰度(grandfathering)三个 open questions 均属产品契约拍板,且明示为 enterprise/marketplace 面(合约在本仓定义、执行在企业侧落地),不是可直接派发的具体缺陷。按 step 0 分类规则入维护者决策箱;深度三轴分析待实际取卡时再写。
过时前提检查(origin/main @ b4cdc58):install-time 门禁(#1810)仍是唯一执行点 ——
NamespaceConflictError在packages/objectql/src/registry.ts:671,带 pin 测试(registry-namespace-install-gate.test.ts);publish-time 注册/独占面自 2026-06-13 立单以来无任何落地。查重:三仓 open issue/PR 搜 ADR-0048 / marketplace namespace,无影子单(cloud#474 是 capability model,不同面)。前提未过时,决策仍待做。预挂域说明:开源侧触点是 SchemaRegistry(install-time cross-check 选项)→
domain:engine-core;若维护者裁决后合约面落 docs/ADR 或拆出 cloud 侧执行单,届时按 contract-first 拆分重挂。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
claude commentedon Aug 6, 2026
处置(2026-08-06):改标 pm:on-hold(维护者裁:三方 publisher 发布远期/未定)。
理由:撞名风险仅在三方发布开放后成立;评审管线虽已上线,但 namespace 独占检查在开放前落地即可,现在做属无拉动的能力扩张。
重启条件(硬,作为开放的验收标准):三方 publisher 开放前必须完成 namespace→publisher 唯一性检查——按最小版 A 执行:评审管线加唯一性检查(现只查 manifest_id 保留前缀,不查 namespace)+ ADR-0048 addendum 记三个默认(纯 namespace 粒度、共享名单沿用 install 闸的 base/system/sys、存量先到先得 grandfather);rename/transfer 继续 deferred。量级(届时)S-M。
经办:PM 会话
session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:
准——立 ADR-0048 addendum,定 publish-time / marketplace 全局命名空间独占注册契约。起步口径:保留粒度从纯 namespace 起步(不含 version-range,复杂度不前置);共享命名空间(base/system/sys)白名单、存量 grandfathering、install-time 反查平台注册表三个 open question 在 ADR 内逐条定。
流转:摘
needs-user-decision→pm:queue,归 engine-core 车道(ADR 起草单);enterprise 面实现随 ADR 结论另行拆单。评估与落地会话:
session_01N3uGFF8teXbpgtbEJ1aYXuGenerated by Claude Code
os-zhuang commentedon Aug 8, 2026
Claim: PM loop round 1 extension 2 (engine-core seat #6019; maintainer raised concurrency to 5+ with cloud cards)
Session:
session_01MwoubC3jL271FYt9rGXwxbBranch:
claude/issue-1825-adr0048-addendum-namespace-exclusivityWorktree: cloud session's own clone (mode:cloud)
Domain:
domain:engine-coreFile surface:
docs/adr/0048-*.md(addendum section; prior text kept verbatim per PD #13 if superseded) — ADR text only, NO code. The enterprise-side implementation is split later per the ruling ("enterprise 面实现随 ADR 结论另行拆单").Operative ruling note: the thread carries both an 08:28 on-hold disposition and the 10:42 fleet-approved verdict (准 — draft the addendum now); the later verdict matches the card's current machine state (
pm:queue) and is the one being executed. Drafting the contract now + implementing before third-party publishing opens satisfies the on-hold comment's hard restart condition rather than contradicting it.Serial constraints cleared: no in-flight card touches
docs/adr/0048-*; flagship's ADR touch is 0058.Container judgment: design card ⇒
mode:cloud(subagent slots full; maintainer directive).Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
os-dev final report (#1825)
Drafted the ADR-0048 addendum defining the publish-time / marketplace global namespace exclusivity contract. ADR text only — no code, no gate changes, no spec changes, per the ruling. Draft PR: #6671.
Premise re-verified against
main@f123670: the install-time gate (NamespaceConflictErrorinpackages/objectql/src/registry.ts, pinned byregistry-namespace-install-gate.test.ts) is still the only enforcement point, and the publish-time surface has zero landing —PackageSchemainpackages/spec/src/cloud/package.zod.tshas nonamespacefield at all,CreatePackageRequestSchemadoes not accept one, andpackages/cli/src/commands/package/publish.tstransmitsmanifest_idonly. The namespace never leaves the artifact. Premise holds.Prime Directive #13: nothing in §§1–6 was reversed, so nothing needed marking SUPERSEDED. The addendum is additive to §3.2 and says so explicitly at its head; the install gate's semantics and §3.5's deferral are untouched.
{ "issue": 1825, "premise_still_valid": true, "status": "complete", "branch": "claude/issue-1825-adr0048-addendum-namespace-exclusivity", "pr": 6671, "pr_state": "draft", "commits": ["d722c64"], "files_changed": ["docs/adr/0048-cross-package-metadata-collision.md"], "loc": "+338 / -1", "changeset": "none — docs-only; PM to apply `skip-changeset` at acceptance", "deliverable": "ADR text only, per the ruling; enterprise-side implementation to be split into separate cards", "premise_evidence": [ "packages/objectql/src/registry.ts — installPackage() namespace gate + NamespaceConflictError + isShareableNamespace() (RESERVED_NAMESPACES {base,system} plus sys); still the only enforcement point", "packages/objectql/src/registry-namespace-install-gate.test.ts — gate still pinned", "packages/spec/src/cloud/package.zod.ts — PackageSchema carries manifestId/ownerOrgId/visibility/publisher and NO namespace field; CreatePackageRequestSchema does not accept one", "packages/cli/src/commands/package/publish.ts — publish flow derives and transmits manifest_id only", "packages/spec/src/contracts/package-service.ts — namespaceConflicts/conflicts surfaced on the service contract (install-time only)" ], "addendum_contents": { "status": "Proposed — additive to ADR-0048 §3.2, supersedes no existing text", "A.0": "measured current state (install-time only; publish-time surface absent)", "A.1": "decisions D1–D6", "A.2": "minimal version A — the publish check stated as an algorithm", "A.3": "open/commercial boundary", "A.4": "explicit non-goals", "A.5": "hard activation precondition", "A.6": "phasing A1–A4", "A.7": "consequences" }, "open_questions": [ { "id": "D1", "question": "Reservation granularity — pure namespace, or (namespace, version-range)?", "settled_as": "Pure namespace. The reservation record is keyed on the bare namespace string; holder is the publisher org, with the first-claiming manifest_id recorded.", "rationale": "Real need: what breaks is a physical table name, and a table name has no version axis — two versions of one vendor's package never collide, two vendors' packages collide at every version pair. Long-term soundness: a range key would let A own crm@1.x and B own crm@2.x while the per-installation, version-blind install gate still collides on them — granting ownership the enforcement layer cannot honour is worse than no key; granularity can only be widened later, so narrow is the reversible direction. AI-safety: one key, one owner, one answer to 'who owns crm?'; a range-keyed registry has an interval-overlap predicate at its heart, and makes 'is this namespace free?' a function of the version being published.", "deviation_from_default": false }, { "id": "D2", "question": "Is the reservation held by the package or by the publisher? (not in the original three, but forced by D1's record shape)", "settled_as": "By the publisher org. A publish is rejected only when the namespace is reserved by a DIFFERENT publisher; same-publisher reuse is allowed and records an additional manifest_id under the existing reservation, with a warning.", "rationale": "Splits conflicts by whether anyone can resolve them. Cross-vendor is unresolvable by any party and hard-fails — that is what the registry exists for. Same-vendor (com.acme.crm2 succeeding a retired com.acme.crm on namespace crm) is entirely within one party's control, and blocking it would strand a vendor's own namespace behind their own deprecated package with no transfer flow to escape through (transfer is deferred). The install gate still refuses to co-install the pair, which is correct for a successor pair. This is the one place the publish gate is deliberately looser than the install gate; flagged as such in §A.7 so it does not read as accidental drift.", "deviation_from_default": false, "needs_signoff": true }, { "id": "D3", "question": "Shared namespaces (base/system/sys) and platform-owned ones — how do they interact with exclusivity?", "settled_as": "Whitelist = exactly RESERVED_NAMESPACES ∪ {sys}, read from the same constant the install gate uses. TWO rules: (1) never reserved, never conflict — mirroring isShareableNamespace(); (2) restricted by publisher tier — only publisher: 'objectstack' may publish a package whose namespace is in the whitelist; anyone else is rejected.", "rationale": "Real need: the platform genuinely ships many packages under sys (registerNamespace is intentionally many-to-one), so an exclusivity check over them would reject the platform's own composition. Long-term soundness: deriving the set from the same constant is what makes the two gates unable to drift — the issue's 'compose without drift' acceptance criterion, met by construction rather than convention. AI-safety: rule (2) is the non-obvious half — exemption from exclusivity must not read as open season, or the whitelist becomes the cheapest route for a third party to ship objects shadowing platform tables (manifest.zod.ts already declares these platform-reserved and sys_-prefixed object names platform-owned). An agent picking 'system' as a neutral-sounding namespace is refused with a tier reason instead of silently acquiring shadowing rights.", "deviation_from_default": false }, { "id": "D4", "question": "Grandfathering already-published packages with colliding namespaces.", "settled_as": "First-come, frozen, shrink-only. Back-fill at switch-on: the publisher of the earliest published version carrying a namespace wins the reservation (ties break on sys_package.created_at, then manifest_id lexicographically — deterministic and re-runnable). Every other publisher already shipping it gets a grandfathered claim permitting new versions of packages that ALREADY EXISTED under that namespace and nothing else — in particular not a new package. The grandfathered set is closed at switch-on and can only shrink.", "rationale": "Real need: retroactively invalidating a published package breaks tenants who already installed it; this addendum must not create an outage to close a future hole. Long-term soundness: first-come is the only rule needing no adjudicator, and a shrink-only exception list is the repo's standard ratchet shape — an exception that cannot grow eventually costs nothing. AI-safety: every alternative rule requires judgement ('who deserves crm?'), and a judgement call inside a gate is one an agent will make wrongly and confidently. Noted in the ADR: a grandfathered pair stays un-co-installable — that is the pre-existing state preserved, not a regression — and because third-party publishing is not open, the set is expected to be empty or near-empty at switch-on; a non-trivial back-fill is evidence the switch-on was late.", "deviation_from_default": false }, { "id": "D5", "question": "Whether/how install-time cross-checks the platform registry.", "settled_as": "Contract defined, implementation deferred, and it is ADVISORY and FAIL-OPEN, never authoritative. PackageService gains an OPTIONAL namespace-registry lookup port; on mismatch the install surface shows a warning and a trust signal and installs anyway; on lookup failure, timeout, or no platform connection it fails open silently.", "rationale": "Real need: the only scenario covered is a sideloaded artifact squatting a marketplace name — a trust problem, not a data-integrity one. Long-term soundness: (a) ADR-0048 §4's 'local development is unaffected until install' is load-bearing and a hard remote check would repeal it; (b) the invariant that matters — no two installed packages in this installation share a namespace — is already fully decided by the in-process Phase 1 gate with no network, so the registry adds provenance, not correctness, and a trust signal that can hard-fail an install turns a marketplace outage into a total install outage across every tenant; (c) advisory can be promoted to blocking later behind a host policy, whereas blocking cannot be relaxed once tenants depend on it. AI-safety: 'advisory, fail-open' is stated as the contract precisely so an implementer cannot read the deferred card as licence to add a network dependency to the install path.", "deviation_from_default": false }, { "id": "D6", "question": "How does the per-tenant friendly alias (§3.6) interact with global exclusivity? (raised in the issue body)", "settled_as": "They do not interact. The alias derives from the INSTALLED namespace, whose per-tenant uniqueness comes from the local install gate, not the global reservation; the global registry is never consulted to resolve a route; a tenant cannot alias a namespace it has not installed, and a global reservation confers no alias anywhere.", "rationale": "One is a publish-time authority record, the other a tenant-local presentation convenience over the canonical package-id route. The non-interaction is the design, and is stated explicitly because the issue asked.", "deviation_from_default": false } ], "activation_precondition": { "statement": "The namespace→publisher uniqueness check MUST land before third-party publishing opens — an acceptance criterion for opening it, not a follow-up.", "source": "「三方 publisher 开放前必须完成 namespace→publisher 唯一性检查」 (2026-08-06 disposition, quoted verbatim in §A.5)", "why_ordering_is_load_bearing": "Every package published before the check exists becomes, by D4, a grandfathered entry the registry can never revoke — so each day the gate is late permanently enlarges a set that is otherwise shrink-only. While publishing is first-party only that set is bounded by our own discipline; the moment it is open it is bounded by nothing. Building it earlier buys nothing (no third-party publishers, no cross-vendor collision), which is exactly why the card sat on hold." }, "commercial_boundary": { "precedent": "cloud ADR-0016 iron rule 强制免费、治理收费, as this repo applies it in ADR-0105 D12 (code vs. ACTIVATION, not code vs. code)", "open_free": [ "manifest.namespace and its validation; the namespace field on the publish payload schemas", "the install-time gate, NamespaceConflictError, isShareableNamespace(), the shareable-namespace constant, OS_METADATA_COLLISION=warn — all unchanged", "the advisory lookup PORT and its fail-open semantics (D5), the error codes, and this contract" ], "enterprise_paid": [ "the reservation registry itself — storage, the publish-time gate, ownership records", "the D4 back-fill and grandfathered ledger", "dispute handling and any future transfer administration" ], "why_the_iron_rule_holds": "The free half is the half that keeps an installation CORRECT — any deployment, any tier, offline or air-gapped, is fully protected from a namespace collision corrupting its schema, because the local gate refuses the install. The paid half prevents a collision from being discovered LATE and provides an operated global authority to adjudicate names, which is precisely what 治理收费 designates. A deployment without it is never unsafe, only less informed." }, "non_goals": [ "version-range reservation granularity — actively rejected as a starting position, not deferred pending design", "namespace rename / ownership transfer — remains DEFERRED per 「暂不考虑」 and the 2026-08-06 disposition; a reservation is created and released, never moved between publishers (D2's publisher-scoped key is what keeps the absence of a transfer flow from stranding a vendor's own name)", "namespace rename-on-install — unchanged non-goal, ADR-0048 §3.5", "squatting policy and dispute adjudication — operational marketplace policy, not a protocol decision", "any change to install-gate semantics, its shareable set, its same-package-reload exemption, or the OS_METADATA_COLLISION=warn downgrade", "enforcement outside the marketplace — local dev, private registries and sideloading are not policed and are not intended to be" ], "follow_up_cards_implied": [ "Phase A1 (open, this repo): carry `namespace` on PackageSchema / CreatePackageRequestSchema and have `objectstack package publish` read it off the artifact manifest — prerequisite for everything else; without it the enterprise gate has nothing to check", "Phase A2 (enterprise): the publish-time gate (§A.2 algorithm) + reservation store — THIS is the item bound by the activation precondition", "Phase A3 (enterprise): the D4 grandfathering back-fill, run once at switch-on, emitting the closed grandfathered set as a reviewable artifact", "Phase A4 (deferred): D5's advisory install-time cross-check — explicitly NOT a precondition for A2", "ADR-0112 ledger registration of the proposed rejection codes (NAMESPACE_ALREADY_RESERVED / NAMESPACE_RESERVED_PLATFORM) — lands with the implementation, not with this ADR" ], "gates_run": { "source": "enumerated one by one from .github/workflows/lint.yml", "all_green": true, "eslint_job": ["lint", "check:slot-lookup", "check:query-options-erasure", "check:verify-stand-in", "check:nul-bytes", "check:doc-authoring", "check:docs-audit-scope", "check:role-word", "check:quick-reference-counts", "check:adr-anchors", "check:org-identifier", "check:authz-resolver", "check:service-providers", "check:route-envelope", "check:error-code-casing", "check:wildcard-fallthrough", "check:meta-type-normalized", "check:init-service-contract", "check:durability-log-level", "check:startup-registry-verdict", "check:objectui-changeset", "check:release-notes", "check:release-body", "check:node-version", "check:workflow-status-functions", "check:shard-attestation", "check:published-files", "check:engine-double-contract", "check:resume-authority-declared", "check:merge-driver", "check:spec-parsed-alias"], "other_jobs": ["check:i18n", "check:i18n-coverage", "check:skill-frame-sync", "check:skill-compatibility"], "notes": [ "check:adr-anchors green including the ADR-number-uniqueness half — the addendum lives inside the existing 0048 record and claims no new number", "check:i18n and check:i18n-coverage initially reported PREREQUISITE NOT MET / COULD NOT MEASURE (they run the built CLI and lint the example configs); after a workspace build both are green — check:i18n OK (9 packages in sync), check:i18n-coverage OK (12 configs, 660 baselined, none new)", "the TypeScript type-check job is unaffected — zero .ts/.json files changed" ] }, "risks_and_caveats": [ "D2 (publisher-scoped rather than package-scoped reservation) is the one intentional asymmetry between the publish gate and the install gate. It is the LOOSE direction and only within a single publisher, but it is a judgement call beyond the four fixed defaults and is the item most worth an explicit maintainer nod.", "D3's rule (2) — restricting whitelist publishes to the first-party tier — extends the ruling's 'whitelist aligned with the install gate' rather than merely mirroring it. The alignment on WHICH namespaces is exact; the tier restriction is an addition, justified in §A.1 D3 because bare mirroring would make the whitelist an open shadowing route once third-party publishing opens.", "§A.2 notes that `namespace` must travel with the publish payload — a real open-side schema change on PackageSchema / CreatePackageRequestSchema. It is scoped OUT of this PR (ADR text only) and recorded as Phase A1, but it means the enterprise card has a hard dependency on an open-repo card landing first.", "The marketplace review pipeline's current 'manifest_id reserved prefix' check is enterprise-side and could not be read from this repo; it is described in §A.0 as stated on the issue thread, attributed as enterprise-side, and is not relied on by any decision here." ], "conduct": { "worktree": "/home/user/objectstack-1825 (dedicated worktree per Prime Directive #11; primary checkout detached first)", "stash_used": false, "assignees_touched": false, "releases_dir_touched": false, "code_or_spec_touched": false } }Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
PM review passed — handing to the maintainer as decision material (engine-core seat #6019). Label flipped to
needs-user-decision: the dev deliverable (ADR-0048 addendum, PR #6671, Proposed, additive to §3.2, nothing superseded) is complete and verified — one file, no code, premise re-measured (publish-time surface still has zero landing;PackageSchemacarries nonamespacefield at all), docs gates green,skip-changesetapplied. All four fixed positions from your 2026-08-06 disposition are honored (bare-namespace granularity; whitelist = the install gate'sbase/system/sys; first-come frozen shrink-only grandfathering; rename/transfer stays deferred), and the hard activation precondition is recorded verbatim in §A.5.Two items the dev flagged for your explicit nod (both argued in §A.1, neither contradicts the disposition): D2 — the reservation is publisher-scoped, so a vendor may reuse its own namespace across successor packages (the one deliberate looseness vs the install gate); D3 rule 2 — whitelist namespaces are publishable only by the first-party tier, an addition beyond bare mirroring so the exemption cannot become a shadowing route. Also note §A.2's implied open-side follow-up:
namespacemust travel on the publish payload (Phase A1, this repo) before the enterprise gate has anything to check.The PR stays DRAFT until you rule.
Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
Maintainer ruling (2026-08-08, via the engine-core PM session): ADR-0048 addendum ACCEPTED as drafted, including the two flagged extensions — D2 (reservation held by the publisher, so same-vendor successor packages may reuse their own namespace; cross-vendor stays a hard publish failure and the install gate still refuses co-install) and D3 rule 2 (whitelist namespaces
base/system/sysare publishable only by the first-party tier — exemption from exclusivity is not open season).Consequences now live:
skip-changesetapplied). The in-document status line reads "Proposed" at merge time; this comment is the acceptance record of authority, and the status-line flip rides the next docs touch (deliberately not a solo re-push).namespaceonPackageSchema/CreatePackageRequestSchema+ CLI reading it off the artifact) is the first implementation card; it changes the spec acceptance face, so it belongs to thedomain:speclane via triage. Phases A2/A3 are enterprise-side, bound by §A.5's hard activation precondition (uniqueness check lands before third-party publishing opens). A4 stays deferred and is explicitly not a precondition.Fixeslink).Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
Triage (state-machine repair only):
pm:dispatchedadded — no claim change, no dispatch by this seat.This card lost its pm-state at 13:55Z when
needs-user-decisioncame off with the maintainer's acceptance ruling, leavingdomain:engine-corewith no pm-state — a shape the triage sweep re-selects every round. The true state is dispatched and in flight: claimed 09:26Z by the engine-core seat (sessionsession_01MwoubC3jL271FYt9rGXwxb), deliverable PR #6671 accepted by the maintainer 13:55Z and headed to the merge queue, anchor closes on merge viaFixes.pm:dispatchedrecords exactly that; ownership and follow-through remain entirely with the engine-core seat.(The ruling's Phase A1 follow-up card is being filed by this seat separately, routed
domain:specper the ruling's own lane assignment.)本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code