From ffb1c6f8aa30860a3484456e5e4f54d3b22ea1a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:16:21 +0000 Subject: [PATCH 1/7] =?UTF-8?q?docs(adr):=20ADR-0141=20=E2=80=94=20an=20ap?= =?UTF-8?q?p=20may=20depend=20on=20another=20installed=20app:=20declared?= =?UTF-8?q?=20in=20manifest.dependencies,=20checked=20once=20before=20inst?= =?UTF-8?q?all,=20co-owning=20the=20namespace=20it=20shares?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the two maintainer rulings on the inter-app dependency decision (B′ 6369704387, namespace letter A 6377678758): declaration and reference stay the existing manifest key and the save door's closure; a pre-install check (installed, in range, else a clear refusal) fills the reserved dependency_missing category; uninstall protection, upgrade interception, topological/cascade install, the owner-side publish marker and marketplace version negotiation are deferred by name; permissions are unchanged; a declared dependency is one-way namespace co-ownership with the ADR-0130 D3 uniqueness check spanning it. ADR-0028 D4 is rewritten to "no cross-app reference without a declared dependency"; ADR-0019 gains an additive Related line; ADR-0130 gains an addendum moving D8's deferral end to marketplace publishing and marking its #14488 line stale. Claude-Session: https://claude.ai/code/session_01HwUcNzGjPhV6xtYkJ469qK Co-authored-by: Claude --- docs/adr/0019-app-as-consumer-unit.md | 1 + ...metadata-naming-and-namespace-isolation.md | 35 +- ...lease-artifact-as-co-ownership-boundary.md | 43 ++ ...endency-declared-checked-before-install.md | 418 ++++++++++++++++++ 4 files changed, 485 insertions(+), 12 deletions(-) create mode 100644 docs/adr/0141-inter-app-dependency-declared-checked-before-install.md diff --git a/docs/adr/0019-app-as-consumer-unit.md b/docs/adr/0019-app-as-consumer-unit.md index 8ac598a402e..1138f7991c8 100644 --- a/docs/adr/0019-app-as-consumer-unit.md +++ b/docs/adr/0019-app-as-consumer-unit.md @@ -5,6 +5,7 @@ **Deciders**: ObjectStack Protocol Architects **Builds on**: ADR-0003 (Package as First-Class Citizen), ADR-0006 (Three-Layer Tenancy), ADR-0016 (Studio Package Authoring & Publish) **Consumers**: `@objectstack/spec/kernel` (`plugin.zod.ts`), `@objectstack/spec/cloud` (`marketplace.zod.ts`), the Console `marketplace` UI, the Studio publish flow, `docs/design/marketplace-publishing.md` +**Related**: [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) (2026-10-24) — the separate, gated decision §Out of scope asks for on "inter-App dependencies", narrowed to a declared dependency and a pre-install check; additive cross-reference, this record's text is unchanged > **Revision history** > - **v1/v2** explored a much larger scheme: a metadata/code *plane* split, diff --git a/docs/adr/0028-metadata-naming-and-namespace-isolation.md b/docs/adr/0028-metadata-naming-and-namespace-isolation.md index 682e8e34ff4..5ea1e35a385 100644 --- a/docs/adr/0028-metadata-naming-and-namespace-isolation.md +++ b/docs/adr/0028-metadata-naming-and-namespace-isolation.md @@ -1,6 +1,6 @@ # ADR-0028: Metadata Naming & Namespace Isolation — Derived Physical Names, Namespace-Scoped Identity, and a Single Kernel Contract -**Status**: Deferred (2026-07-16; proposed 2026-06-01) — the target model (namespace as identity dimension, short authored names, derived physical names, `namingMode` dual-read, `sys` reservation, namespaced transport segments) is entirely unbuilt, and adopting it now would be a platform-wide migration (physical table derivation, transport routes, dual-read compatibility) with no present pain to justify it: the literal-prefix current-state contract (`spec/kernel/namespace-prefix.ts`) is enforced, collision-proof (ADR-0028/0048), and working. **Re-open trigger**: real multi-package ecosystem naming pain — e.g. a marketplace package whose natural short names are unusable under literal prefixing, or cross-environment portability requiring namespace remapping. Until then this stays a recorded design, not planned work. +**Status**: Deferred (2026-07-16; proposed 2026-06-01) — the target model (namespace as identity dimension, short authored names, derived physical names, `namingMode` dual-read, `sys` reservation, namespaced transport segments) is entirely unbuilt, and adopting it now would be a platform-wide migration (physical table derivation, transport routes, dual-read compatibility) with no present pain to justify it: the literal-prefix current-state contract (`spec/kernel/namespace-prefix.ts`) is enforced, collision-proof (ADR-0028/0048), and working. **Re-open trigger**: real multi-package ecosystem naming pain — e.g. a marketplace package whose natural short names are unusable under literal prefixing, or cross-environment portability requiring namespace remapping. Until then this stays a recorded design, not planned work. · **Amended** (2026-10-24, [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D6 — D4 is rewritten from "no cross-app references" to "no cross-app reference without a declared dependency"; the rest of this record stays Deferred) **Deciders**: ObjectStack Protocol Architects **Supersedes**: the hand-written object-namespace-prefix authoring rule documented in `packages/spec/src/kernel/manifest.zod.ts` (the `namespace` field) and enforced by `validateNamespacePrefix()` in `packages/spec/src/stack.zod.ts` — that rule's standalone record is [ADR-0129](./0129-object-name-is-the-canonical-id.md) (2026-08-29; it governs until this ADR's re-open trigger fires). **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0008](./0008-metadata-repository-and-change-log.md) (Repository · ChangeLog · Cache · Registry; `MetaRef = org/type/name`), [ADR-0010](./0010-metadata-protection-model.md) (protection model), [ADR-0019](./0019-app-as-consumer-unit.md) (app as the consumer-installable unit), [ADR-0025](./0025-plugin-package-distribution.md) (package distribution), [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) (**prerequisite** — kernel object ownership; D5/D6 below assume the kernel is properly owned per ADR-0029) @@ -33,9 +33,10 @@ metadata platform actually uses, and extends collision-freedom to **all** kinds: identifiers inject the namespace at generation. Uniqueness is enforced where it physically matters (storage tables, route table, tool registry), not on the authored name. -4. **Apps are sandboxed: no cross-app references.** The only legal - cross-boundary reference is **app → kernel**. This doubles as a security - boundary. +4. **No cross-app reference without a declared dependency** (rewritten + 2026-10-24 by ADR-0141; first proposed as full sandboxing, app → kernel + only). An app reaches the kernel and the apps it declares in + `dependencies`, nothing else. This doubles as a security boundary. 5. **The kernel is one unified, reserved namespace (`sys`) — one contract, but ownership distributed across first-party plugins.** It is the platform's public contract and the sole well-known import target (`sys.user`). Each @@ -152,13 +153,23 @@ schema), resolved back to `(ns, name)` at runtime. **Resolution pipeline:** `route /data/todo/task` → handler resolves `(todo, task)` → driver derives physical `todo_task` → response payloads use short names. -### D4 — Apps are sandboxed: no cross-app references (security boundary) +### D4 — No cross-app reference without a declared dependency (security boundary) -A `type: app` package may reference its own metadata (short names) and the -kernel (qualified), but **may not reference metadata owned by another app**. -This collapses the only remaining "two writing styles" risk to a single, -allow-listed case (kernel imports) and simultaneously enforces a tenant-style -isolation boundary between marketplace apps. +> **Rewritten (2026-10-24, [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D6**, rulings +> [`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387) and +> [`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758) on #25403). This +> decision first read *"Apps are sandboxed: no cross-app references"* — a `type: app` package *"may not reference +> metadata owned by another app"*. The isolation stays; its boundary is now the app's declared dependencies. The rest +> of this record stays Deferred and unchanged. + +A `type: app` package may reference its own metadata (short names), the +kernel (qualified), and the metadata of the apps its manifest declares in +`dependencies`; it **may not reference metadata owned by any other app**. +The closed set keeps the tenant-style isolation boundary between marketplace +apps — an app reaches only what it declared — and keeps the "two writing +styles" risk to declared, allow-listed cases. The runtime save door already +judges a package write against that closure (ADR-0141 D1); installing an app +whose declared dependency is absent or out of range is refused (ADR-0141 D2). ### D5 — The kernel is one unified, reserved namespace (`sys`) with object ownership distributed across first-party plugins @@ -233,7 +244,7 @@ reference target. It is: `validateNamespacePrefix` into a *namespace-scope* validator over **all** collections — names must be bare short names (no `ns_` prefix, no `__`), references resolve within the package namespace or to a qualified - `sys.x` / (forbidden) cross-app ref. Early failure with the exact fix string. + `sys.x` / a cross-app ref only to a declared dependency (D4). Early failure with the exact fix string. - **Install time** (package registry): register every `(namespace, type, name)` plus every derived transport key (route, connector id, tool name, webhook). The only true conflict left is **two @@ -257,7 +268,7 @@ the author/AI as a second style: name needs a `tableName`/`physicalName` override — derivation is the *default*, not mandatory (mirrors `columnName`). 3. **Cross-package references.** Within a package: short name. To the kernel: - qualified `sys.x`. Cross-app: forbidden (D4). This is the one place a + qualified `sys.x`. Cross-app: only to a declared dependency (D4). This is the one place a qualified form appears, and it is a single deterministic rule (like an `import`), not an arbitrary second style. 4. **Diagnostics.** Driver errors/logs will show `todo_task`; this is read-only diff --git a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md index 51cac23380d..7816143921f 100644 --- a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md +++ b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md @@ -9,6 +9,12 @@ ([#14512](https://github.com/objectstack-ai/objectstack/issues/14512)): a multi-package artifact carries its metadata **once**, in `packages[]` — the flattened copy is no longer emitted beside it. D4's read-both rule and D7 are unchanged, so every artifact already on disk still loads. +**D8's deferral end point amended by the 2026-10-24 addendum** +([ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D5, +[#25403](https://github.com/objectstack-ai/objectstack/issues/25403)): a package that declares `dependencies` on an +installed package and shares its namespace co-owns it across artifacts, so the owner field is deferred until +marketplace publishing rather than until a module's first standalone artifact. The same addendum records that the +2026-09-02 addendum's #14488 line is stale. D1–D7 are unchanged. **Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated: 「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in [#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane @@ -743,3 +749,40 @@ fallback bolted onto the emitter. Every one of those shapes stays readable by D4's read-both rule, and none is refused: a composition that was legal before this change stays legal. + +--- + +## Addendum (2026-10-24, #25403) — D8's deferral ends at marketplace publishing; the #14488 line is stale + +Recorded by [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md), which executes two maintainer +rulings on [#25403](https://github.com/objectstack-ai/objectstack/issues/25403): letter B′ +([`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387), 「同意」 +2026-10-23T16:28:30Z) and letter A +([`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758), 「同意 #8」 +2026-10-24T00:40:55Z). ⛔ This section edits no text above: D1–D8, §1–§8 and both earlier addenda read as written, +except where the two points below say how a sentence now reads. + +### D8's deferral now ends at marketplace publishing + +D6 and D8 said the owner field becomes necessary *"the day a module ships as its own artifact"*. ADR-0141 D5 answers that +day without the field: a package that declares `dependencies` on an installed package X and uses X's namespace is X's +co-owner at the install gate — one-way, recognised whichever registers first — and D3's per-object-name uniqueness +check spans those co-owners across artifacts exactly as it spans co-owners inside one artifact. Inside one tenant the +proof is the declaration plus the administrator's install act. + +So D8's deferral now ends at **marketplace publishing**: when a module is published for other tenants and must assert +*"I have the same publisher as HotCRM"* across vendors, the owner / publisher field lands, aligned to the +`publisherOrgId` reservation of ADR-0048's 2026-08-08 addendum (still Proposed), as D8 already requires. That field is +the later superset of ADR-0141 D5 and needs no undo of it. D6's cost statement stands for modules inside one artifact; +a module that becomes its own artifact trades it for the separate-version cost ADR-0141's Consequences state. + +### The #14488 line is stale + +The 2026-09-02 addendum's *What was NOT decided* section lists +[#14488](https://github.com/objectstack-ai/objectstack/issues/14488) (per-package composition of grants) as *"filed, not +decided"*. It is decided: #14488 was closed `not_planned` on 2026-09-12 by ruling +[`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259) — an artifact installs +whole, the app package's permission sets cover every module's objects, nothing composes grants into another package's +set, and a module carved out as its own artifact ships its own permission sets under ADR-0086 D4. The three inherited +inputs listed under that line remain accurate as history; none is open work. A standalone module's permission sets are +the module author's work, not a platform blocker (ADR-0141 D4, D6). diff --git a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md new file mode 100644 index 00000000000..2de0712b404 --- /dev/null +++ b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md @@ -0,0 +1,418 @@ +# ADR-0141: An app may depend on another installed app — declared in `manifest.dependencies`, checked once before install, and co-owning the namespace it shares + +**Status**: Proposed (2026-10-24). `docs/adr/**` is Tier H (Prime Directive #14): this record becomes **Accepted** by +the maintainer's merge, and ⛔ the execution cards under [Execution plan](#execution-plan-after-acceptance) are cut after +that merge and never before it. Until then a composite product keeps shipping as one release artifact under +[ADR-0130](./0130-release-artifact-as-co-ownership-boundary.md). +**Decided by**: two maintainer rulings on [#25403](https://github.com/objectstack-ai/objectstack/issues/25403), recorded +verbatim and untranslated: + +- ruling [`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387), batch #7 + item 1, letter **B′**, 「同意」 at 2026-10-23T16:28:30Z — the declaration stays as it is, the save door's closure is + the reference mechanism, a pre-install check is the only new gate, the rest is deferred by name, permissions are + unchanged. The maintainer's reasoning, from the director thread that day: + > 往往需要引用的数据并不多,可能就几个对象 (16:10:38Z) + > + > 包依赖本身没问题,也就是说如果 psa 依赖 crm, 安装psa 之前先检测crm有没有,没有就禁止安装,这个有必要嘛 (16:20:28Z) + > + > 如果这样,hotcrm 岂不是真实的可以分拆出很多模块 (16:25:59Z) / 而每个包可以独立安装 (16:26:18Z) +- ruling [`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758), batch #8 + item 2, letter **A**, 「同意 #8」 at 2026-10-24T00:40:55Z — 「声明依赖即命名空间共有,单向,D3 对象名唯一检查跨发布物同行」, + no new manifest field, and ADR-0130 D8's deferral reworded to end at marketplace publishing. + +The second ruling answers input 1 of the `repo:hotcrm` seat's comment +[`6370024454`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6370024454); its input 2 (a stale +line in ADR-0130) is corrected by D6 below. +**Builds on**: [ADR-0019](./0019-app-as-consumer-unit.md) (the App is the consumer unit; §Out of scope deferred +"inter-App dependencies" to a separate gated decision — this is that decision, narrowed), +[ADR-0130](./0130-release-artifact-as-co-ownership-boundary.md) D1, D3, D5, D6, D8 (co-ownership inside one artifact, the +gate/uniqueness pair, the one sorter, one version per artifact, the deferred owner field), +[ADR-0048](./0048-cross-package-metadata-collision.md) §3.2 and its +[2026-08-08 addendum](./0048-cross-package-metadata-collision.md#addendum-2026-08-08-publish-time--marketplace-namespace-exclusivity) +(the install-time namespace gate; the publisher-held reservation, still Proposed), +[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D4 (a package ships its own +permission sets and never writes a foreign one), [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) D1 (the +protocol handshake every install door already runs before it registers anything — the placement D2 reuses), +[ADR-0112](./0112-error-code-vocabulary-and-ledger.md) (the refusal envelope), +[ADR-0116](./0116-plugin-ordering-declared-contract.md) (ordering is a declared contract with one sorter), +[ADR-0129](./0129-object-name-is-the-canonical-id.md) (an object name carries its module prefix literally, which is why +a split module keeps `crm_*` and its namespace). +**Amends**: [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) D4 (a Deferred proposal; its sentence is +rewritten, D6 below); ADR-0130 D8's deferral end point and the stale #14488 line of its 2026-09-02 addendum (each by an +additive addendum on 0130; no original text of 0130 is edited). **Cross-references**: ADR-0019 gains one additive +`Related` line; its text is unchanged (ADR-0130 D2's recording convention). +**Consumers**: `@objectstack/spec` (`ManifestSchema.dependencies` in `kernel/manifest.zod.ts`, +`AppCompatibilityCheckSchema` and `AppManifestSchema` in `system/app-install.zod.ts`, `PackageDependencySchema` and +`PackageManifestSchema` in `marketplace/package-version.zod.ts`), `@objectstack/objectql` (`SchemaRegistry.installPackage` +and `refuseCoOwnedObjectNameCollision` in `registry.ts`), `@objectstack/metadata-protocol` (`installPackage` in +`protocol-package-install.ts`; `resolveWritePackageScope` in `protocol.ts`), `@objectstack/runtime` (`POST +/api/v1/packages`, `domains/packages.ts`), `@objectstack/cloud-connection` (`POST /api/v1/marketplace/install-local`), the +`objectui` Console's install refusal text, `packages/cli` (`os package publish`), and HotCRM's module split. +**Evidence**: every anchor in this repository was read at `origin/main` `47af3dde42` while drafting. Repositories this +session cannot read (`objectstack-ai/hotcrm`, `objectstack-ai/cloud`) are cited as the rulings cite them and marked +**not re-verified**. + +--- + +## TL;DR + +An installable app may now depend on another installed app and reference its objects. **Nothing new is authored**: + +1. **Declaration** is the `dependencies` key every manifest already has (`{ packageId: versionRange }`). +2. **Reference** is what the runtime save door already allows: a package's own objects, the objects of the packages its + `dependencies` declare (transitively), platform objects, and tenant objects that belong to no package. +3. **The one new gate is a pre-install check.** Installing a package checks each declared dependency — installed in the + target environment (or delivered by the same artifact) and at a version inside the declared range — and otherwise + refuses the install with a message naming what is missing (「请先安装 HotCRM 6.x」). It fills the `dependency_missing` + category `AppCompatibilityCheckSchema` has reserved with no producer. +4. **A package that declares a dependency on an installed package X and uses X's namespace co-owns that namespace with + X** at the install gate. One-way: X declares nothing and gains nothing. ADR-0130 D3's per-object-name uniqueness check + spans these co-owners across artifacts, exactly as it spans co-owners inside one artifact. +5. **Deferred, by name**: uninstall protection, upgrade interception, topological / cascade install, an owner-side + publish marker, marketplace version negotiation. **Permissions are unchanged**; record-level semantics across a + dependency are an open section, decided with the first consumer. + +The first consumer is a HotCRM module shipping on its own, revenue first. + +--- + +## Context + +### What the platform does today (measured at `47af3dde42`) + +**Declaration exists and is editable.** `ManifestSchema.dependencies` is `z.record(z.string(), z.string()).optional()`, +documented as a "Map of package IDs to version requirements" (`packages/spec/src/kernel/manifest.zod.ts:715`). The +package door edits it after install and validates only its shape (`packages/runtime/src/domains/packages.ts:2319–2335`, +recorded on the registry by `packages/objectql/src/registry.ts:6302`). + +**Reference is already judged against the declared closure — at the runtime save door only.** +`resolveWritePackageScope` computes the transitive closure of the written package's `manifest.dependencies` +(`packages/metadata-protocol/src/protocol.ts:7526–7606`), and the lint gate keeps an object when it belongs to the +package, to a declared dependency, to the platform, or to no package. A view bound outside it is refused `422` +(`list-view-object-outside-closure`, `packages/lint/src/rule-explanations.ts:3323`), whose fix line already reads +"declare a dependency on the package that defines the object". The same explanation records the boundary of that +judgement: `os validate`, `os build` and `os lint` "judge neither: a stack there is one app, and an unresolved binding +may be a dependency package's object the stack does not carry". A closure that does not resolve a declared dependency +keeps it in the closure and stops walking — an absent dependency is not a fault there. + +**At install, `manifest.dependencies` is read by exactly one consumer, and it orders — it never checks.** The dispatch +for this record assumed nothing at install reads the key; the measurement corrects that by one reader. +`artifactPackages` (`packages/core/src/artifact-packages.ts:331–337`) feeds each declared id to `resolvePluginOrder` as an +**optional** edge, so packages inside one artifact register in dependency order (ADR-0130 D5) and a declared id that is +not in the artifact "is simply not an edge here" — silently skipped by design (its header, lines 112–135). No install +door asks whether a declared dependency is installed, or at what version. `dependency_missing` is an enum value at +`packages/spec/src/system/app-install.zod.ts:88`; across the tree it occurs only there, in that file's test and in the +generated reference page — zero producers. `packages/core/src/dependency-resolver.ts` serves kernel plugins, not +installed packages. + +**The namespace gate admits co-owners from one source only.** `SchemaRegistry.installPackage` builds its co-owner set +from the artifact scope the load path passes (`registry.ts:5974`, `scope?.packageIds`) and refuses a package whose +non-shareable namespace an installed non-co-owner owns, throwing `NamespaceConflictError` (`registry.ts:5989–6005`, the +throw at `6003`; the class at `1886`, `422 NAMESPACE_CONFLICT`). Its other half, `refuseCoOwnedObjectNameCollision` +(`registry.ts:6153`), refuses two co-owners defining one object name (`ArtifactObjectNameConflictError`, +`DUPLICATE_ARTIFACT_OBJECT_NAME`) and returns early when the co-owner set is empty. So a module published as its own +artifact with HotCRM's namespace `crm` is refused next to an installed HotCRM today, whatever it declares. + +**Three schemas spell a dependency, in three shapes** — the alignment D8 records: + +| Carrier | Shape | Who reads it at `47af3dde42` | +|:---|:---|:---| +| `ManifestSchema.dependencies` (`kernel/manifest.zod.ts:715`) | record `{ [packageId]: versionRange }`, optional | the save-door closure; the intra-artifact sort; the package door's edit | +| `PackageManifestSchema.dependencies` (`marketplace/package-version.zod.ts:109`), entries `PackageDependencySchema` (`:56`) | array of `{ packageId, versionRange, optional = false }`, default `[]` | no runtime reader in this repository | +| `AppManifestSchema.dependencies` (`system/app-install.zod.ts:60`) | array of bare strings, no version, default `[]` | no reader outside `packages/spec` | + +`os package publish` snapshots the compiled `dist/objectstack.json` into `sys_package_version.manifest_json` +(`packages/cli/src/commands/package/publish.ts:11–12`), so the snapshot carries the record shape where +`PackageManifestSchema` declares an array. Whether the control plane parses that snapshot against the array schema lives +in `objectstack-ai/cloud` — **not measured** here. + +### Why now, and for whom + +ADR-0130 lets one product carry N packages inside one artifact, installed whole; its D6 records the cost — no per-module +hot-fix — and the escape hatch: a module that must ship on its own cadence becomes its own artifact, at which point D8's +deferral ends. The maintainer's rulings above take that escape hatch for real: a HotCRM module that ships and installs on +its own, beside an installed HotCRM. That needs three things the platform does not have — a check that the depended-on +app is there, a namespace gate that admits the module, and a record of what is deliberately left for later. The +`repo:hotcrm` seat's two module cards frame the sequence: hotcrm#2270 packages revenue and marketing inside the one +artifact now; hotcrm#2271 ships revenue on its own once this check is released (both **not re-verified**, cited from +comment `6370024454`). + +--- + +## Decision + +### D1 — Declaration and reference: no new mechanism + +An app — any installable package — declares what it depends on in `ManifestSchema.dependencies`, unchanged: +`{ "PACKAGE_ID": "SEMVER_RANGE" }`. A dependency **opens the reference**: the runtime save door judges a package's +writes against the closure already described above, and nothing else decides whether a cross-app reference is legal. + +⛔ No owner-side "published object" flag, no export list, no second reference mechanism. An undeclared cross-app +reference is refused where it is refused today — at the save door, `422` — and the fix it prints is the declaration. +`os validate`, `os build` and `os lint` keep judging neither side of a cross-package binding: a stack there is one app +and cannot see its dependencies' objects. + +### D2 — The pre-install check: installed, in range, or a clear refusal — and only that gate + +**What it judges.** For an install of package P, every entry `(id, range)` of `P.manifest.dependencies` is satisfied +when both hold: + +1. **Present.** A package with that id is installed in the target environment, or is delivered by the **same artifact** + as P. The second arm is what keeps every ADR-0130 artifact installable: inside one artifact the dependency is + installed in the same act (D1 of 0130) and registered first (D5 of 0130), so the check must not refuse a dependency + the act itself delivers. +2. **In range.** The present package's version satisfies `range`. A co-delivered package's version is the artifact's + one version (ADR-0130 D6). A version or range the parser cannot read does **not** satisfy — the refusal says which + side was unreadable. Fail closed: an unread bound is not a bound. + +Every manifest entry is required: the record shape has no optional arm (D8 states the marketplace shape's `optional` +flag and how it projects). + +**What it answers.** An unsatisfied entry refuses the install, before anything is registered or written, with the +ADR-0112 envelope and one `AppCompatibilityCheck` issue per unsatisfied entry — `severity: 'error'`, `category: +'dependency_missing'`, a `message` naming the dependency, the declared range, and what was found (absent, or installed at +version V). All unsatisfied entries are reported in one answer, never the first alone. The Console renders the refusal +in the author's terms (「请先安装 HotCRM 6.x」). The error code and HTTP status are the runtime card's to choose from the +ADR-0112 ledger; the category is fixed here. + +**Where it runs.** At the **install act** — the doors that already run the ADR-0087 D1 protocol handshake before they +register anything: `POST /api/v1/packages` on both of its arms (the protocol primitive `installPackage` in +`protocol-package-install.ts`, and the no-protocol fallback in `domains/packages.ts`), and +`POST /api/v1/marketplace/install-local`. One predicate serves every door; ⛔ no door carries its own copy, and the range +grammar is read by an existing parser, ⛔ never a second semver implementation. + +**Where it does not run.** Re-registration of an already-installed set is not an install act: boot registration of +code-composed packages, the `sys_packages` rehydrate, and the cached marketplace-manifest rehydrate. A boot fills the +registry incrementally, so "dependency X is not installed" concluded there can be contradicted a moment later by X +registering — the verdict AGENTS.md's *Startup registry reads* section names as a defect. What an installed set already +admitted stays admitted; a dependent whose dependency has since gone is D3's uninstall-protection item, not a boot +refusal. + +### D3 — Deferred, by name, not implemented now + +Each item below is a decision **not** to build yet, recorded so the next author finds a reason instead of an absence. +None is rejected; each has a trigger. + +| Deferred item | What happens meanwhile | Trigger to build it | +|:---|:---|:---| +| **Uninstall protection** — refusing to uninstall a package another installed package depends on | The uninstall proceeds. The dependent's references to the removed objects dangle; the existing ADR-0029 refusal (a package whose object another package `extend`s cannot be uninstalled) still holds, unchanged | A first customer uninstall that strands a dependent | +| **Upgrade interception** — refusing an upgrade of X that would leave a dependent's declared range | The upgrade proceeds; the dependent's range is not re-judged | A first dependent broken by an upgrade of what it depends on | +| **Topological / cascade install** — installing a set in dependency order, or pulling a missing dependency in automatically | The administrator installs dependencies first; D2's refusal says which. Inside one artifact, ADR-0130 D5's sort is unchanged | More than one level of standalone modules in a real product | +| **Owner-side publish marker** — X restricting which of its objects a dependent may reference | Every object of X is referenceable by a declared dependent, subject to D4 | A third-party dependent on a first-party app, i.e. marketplace publishing (D5's end point for ADR-0130 D8) | +| **Marketplace version negotiation** — choosing versions of X and P that satisfy each other | None; D2 judges the installed version against the declared range, once | Marketplace listings carrying more than one installable version of a depended-on app | + +### D4 — Permissions unchanged; record-level semantics are an open section + +A dependency opens the **reference**, not the **data**. A referencing app's users see the referenced app's records under +the referenced app's own permission sets and sharing rules; a lookup from P to an object of X confers no read on X's +records. Each package ships its own permission sets (ADR-0086 D4), and nothing composes into another package's set — the +same answer #14488's closing ruling gives a module carved out as its own artifact (D6 below). + +**Open section — decided with the first consumer, not here.** How a dependent's lookup to X's object behaves at record +level: what a user who may read P's record but not the referenced record of X sees in the lookup (the referenced id, a +redacted label, or nothing), and whether a dependent's list, report or search may filter through X's records it cannot +read. The answer is measured on HotCRM's revenue module against the existing record-access path, before the runtime card +for it is cut. + +### D5 — A declared dependency is namespace co-ownership, one-way + +At the install gate, package P's co-owner set gains every installed package X such that P declares X in +`manifest.dependencies` **and** P's namespace equals X's non-shareable namespace. Nothing else changes: + +- **One-way.** Only the dependent declares. X declares no dependency on its modules and gains nothing on P's side; P + gains admission to the shared namespace and nothing on X's side. +- **The D3 pair holds across artifacts.** ADR-0130 D3 made the namespace relaxation and the per-object-name uniqueness + check one change; the second half now spans these co-owners too. A dependent defining an object name X already owns + is refused at install, ahead of any DDL, with an error naming both packages and the object — the refusal + `refuseCoOwnedObjectNameCollision` already raises for co-owners inside one artifact. +- **No new manifest field.** The co-ownership proof is the declaration plus the administrator's install act. Its in- + tenant trust is that administrator's choice to install P beside X — the same trust that admits any package. +- **Direct declarations only.** A package sharing a namespace with several installed packages declares each one; the + `NamespaceConflictError` it meets otherwise names the owner it did not declare. +- **Recognised whichever of the two registers first.** The relation is declared by P alone, but the gate honours it on + both registrations: when X registers and an existing owner of X's namespace is a package that declares X, that owner + is X's co-owner for the namespace gate and for the uniqueness check alike. This is recognition, not a right — X still + declares nothing — and it is required because registration order at boot is not install order: a rehydrate that + registers P before X would otherwise refuse the depended-on app itself. Whether today's rehydrate order can put P + first is the runtime card's first reading. + +**ADR-0130 D8's deferral now ends at marketplace publishing**, not at a module's first standalone artifact. Within one +tenant, D5 proves co-ownership without an owner field. Across vendors — the moment a module is published to the +marketplace and must assert "I have the same publisher as HotCRM" — the owner / publisher field lands, aligned to the +`publisherOrgId` reservation of ADR-0048's 2026-08-08 addendum (still Proposed). That field is the later superset of D5 +(letter B of ruling `6377678758`) and needs no undo of it. Renaming a module's objects to dodge the gate stays refused +(ADR-0129; letter C of the same ruling). + +### D6 — What ADR-0028, ADR-0019 and ADR-0130 now say + +- **ADR-0028 D4** (Deferred, proposed 2026-06-01) proposed full isolation between apps — "may not reference metadata + owned by another app". Its sentence is rewritten to **"no cross-app reference without a declared dependency"**: an app + may reference its own metadata, the kernel, and the metadata of the apps its manifest declares in `dependencies`; + every other cross-app reference is refused. The isolation D4 wanted between marketplace apps is kept by the closed + set — an app reaches only what it declared, and the save door already refuses the rest. The rest of ADR-0028 stays + Deferred and untouched; D4 carries a note pointing here. +- **ADR-0019** §Out of scope keeps its text verbatim. Its header gains one additive `Related` line naming this record as + the gated decision its "Shared/published data model + inter-App dependencies" bullet asked for, narrowed to the + dependency half. The shared data model half (a platform-owned customer/supplier model, letter C on #25403) was not + chosen and stays out of scope. +- **ADR-0130** gains an addendum (no original text edited) recording two things: D8's deferral now ends at marketplace + publishing (D5 above), and the 2026-09-02 addendum's *What was NOT decided* line listing + [#14488](https://github.com/objectstack-ai/objectstack/issues/14488) as "filed, not decided" is stale — #14488 was closed + `not_planned` on 2026-09-12 by ruling + [`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259): an artifact + installs whole, nothing composes grants into another package's set, and a module carved out as its own artifact ships + its own permission sets under ADR-0086 D4. A standalone module's permission sets are therefore the module author's + work, not a platform blocker. + +### D7 — The first consumer: a HotCRM module shipping on its own, revenue first + +The scenario this record is accepted against is HotCRM's module split as its `docs/architecture/module-split-plan.md` +records it (the 2026-09-14 shape: the sales app plus service, revenue and marketing modules; **not re-verified**): the +revenue module ships as its own artifact, declares `dependencies: { HOTCRM_SALES_PACKAGE_ID: "HOTCRM_MAJOR_RANGE" }`, keeps +`namespace: 'crm'` and its `crm_*` object names, and installs beside an installed HotCRM. Installing it on an environment +without HotCRM, or with HotCRM outside the declared range, is refused by D2 with the message naming HotCRM and the range. + +Until the runtime card ships, composite products keep shipping as one artifact (ADR-0130). That is the state hotcrm#2270 +packages revenue and marketing in; hotcrm#2271 moves revenue to its own artifact after the check is released. + +### D8 — The dependency shapes, and the alignment the runtime card owes + +The authored source of truth is the **manifest record**, `ManifestSchema.dependencies`. D2 reads it, the save-door +closure reads it, D5 reads it. ⛔ No spec shape changes in this record. + +The marketplace snapshot's array, `PackageManifestSchema.dependencies` of `PackageDependencySchema`, is a **projection** of +that record, one entry per key: `packageId` = the key, `versionRange` = the value, `optional` = `false` (the record has +no optional arm, and D2 treats every entry as required). The runtime card owes: + +1. **One conversion** from record to array, used by every writer of a snapshot, so the snapshot conforms to its declared + schema instead of carrying the record shape it carries today (`publish.ts` writes the compiled manifest as-is). Where + the control plane reads `manifest_json` is in `objectstack-ai/cloud`; the card states what it found there. +2. **A stated fate for `optional`.** An `optional: true` entry has no record spelling. Until one is decided, a snapshot + never produces it, and D2 does not read it. +3. **A stated fate for `AppManifestSchema.dependencies`** (bare ids, no range, no reader outside `packages/spec`): either + it projects from the record (ids only, losing the range) or it is retired through the ADR-0087 conversion layer. The + card decides which with a measured reader count; this record decides neither. + +The resolver-side `ResolvedPackageDependencySchema` (`kernel/plugin-security.zod.ts`) is unaffected — it is the +resolution outcome, not a declaration, and its own header already tells the two apart. + +--- + +## Consequences + +**Positive** +- A customer can install CRM now and PSA, or a HotCRM module, later — without the module having been shipped to every + CRM customer and without swapping to a combined app. 「每个包可以独立安装」. +- One rule replaces several special cases: inside an artifact (ADR-0130), at the save door (the closure), and across apps + (this record), a cross-package reference is legal exactly when it is declared. +- An AI author's undeclared reference stays a loud `422` at the save door, and a missing dependency is a loud refusal at + install, instead of copied objects that drift silently. +- The work is small: no new authorable key, no schema change, one predicate at the install doors, one more source for the + co-owner set. + +**Negative / costs, stated plainly** +- Uninstalling or upgrading a depended-on app is not guarded (D3). An administrator can strand a dependent until those + items are built; the refusal at install is the only gate. +- A standalone module and its app now have **separate versions**, which ADR-0130 D6 avoided inside one artifact: the + version matrix it removed comes back across artifacts, bounded only by the declared range. +- D5 lets a module share a vendor's namespace on the administrator's say-so. Inside one tenant that is the install + choice the administrator already makes; across vendors it is not enough, which is why D8's deferral now ends at + marketplace publishing. + +**Unchanged** — the consumer model (ADR-0019 D1–D4: one app installs, opens, uninstalls), the artifact as a +control-plane noun (ADR-0130 D2), the one sorter (ADR-0130 D5), permission sets (ADR-0086 D4), and the save door's +closure rules. + +--- + +## Alternatives considered + +The letters as filed on #25403 and on ruling `6377678758`: + +1. **Keep apps isolated (#25403 letter A).** Composite products ship as one artifact. Not chosen: every later product is + either pre-installed on everyone or a swap to a bigger bundle, and cross-app master data travels by import and sync, + which drifts silently. +2. **The full inter-app dependency (#25403 letter B as filed).** Install ordering, uninstall protection, version + compatibility, cross-app permission semantics and Console dependency views at once. Narrowed by the maintainer to + B′: the declaration and one check now, the rest as D3's deferred items. +3. **A platform-owned shared master data model (#25403 letter C).** Not chosen: the platform would own a business data + model and every existing app's objects would migrate. Under this record a "master data app" is an ordinary app others + depend on. +4. **An owner-side "published objects" list (the director's variant D).** Not chosen: 「往往需要引用的数据并不多,可能就几个对象」 + — the declaration is enough; restricting what a dependent may reference is D3's publish-marker item. +5. **For the namespace — an owner / publisher field now (ruling `6377678758` letter B).** Not chosen now: it is the + later superset of D5, aligned to `publisherOrgId`, and lands at marketplace publishing without undoing D5. +6. **For the namespace — rename the module's objects (letter C).** Refused: ADR-0129 makes the object name the canonical + id, and a rename is the deep rewrite ADR-0130 exists to avoid. + +--- + +## Open items (not blocking acceptance; each becomes a reading on its card) + +1. **Record-level semantics across a dependency** — D4's open section, measured on HotCRM's revenue module. +2. **Rehydrate order** — whether any boot path can register a dependent before the package it depends on (D5's + recognition rule makes the answer harmless; the card records it). +3. **Declared ids that are not installable packages.** `ManifestSchema.dependencies`' own documentation example names an + npm-style id (`@steedos/plugin-auth`). Measured population in this repository: one declaring manifest, + `examples/app-multi-package/src/packages/orders/index.ts:65`, whose dependency is delivered by the same artifact (D2's + second arm). HotCRM's manifests are **not measured**. If an id that names no installable package turns up, the card + reports it; D2 does not exempt it. +4. **`optional` and `AppManifestSchema.dependencies`** — D8 items 2 and 3. + +## Open questions for the maintainer's merge decision + +These are the points this draft decided where the rulings were silent, listed so the merge is an informed act: + +1. **D2's second arm** — a dependency delivered by the same artifact counts as present. Without it every multi-package + artifact whose modules declare their app (`examples/app-multi-package` today) would refuse itself. +2. **D2's placement** — install acts only, not boot re-registration. Checking at boot would be a startup-registry verdict + (AGENTS.md); the cost is that an environment whose dependency was removed outside an install act boots without + complaint (D3's uninstall item). +3. **D5's recognition rule** — the gate honours P's declaration on X's registration too. The ruling's "one-way" is kept + for declaration and rights; recognition from X's side is what keeps a rehydrate from refusing HotCRM itself. +4. **D5's direct-only scope** — co-ownership follows direct declarations, not the transitive closure the save door walks. + Narrower is the reversible direction; widening later needs no undo. + +--- + +## Verification notes + +- **The dispatch's assumption "nothing at install reads `manifest.dependencies`"** holds for checking and fails for + ordering: `artifactPackages` reads every declared id as an optional sort edge (Context above). It changes no decision — + the sort skips an absent id silently, which is exactly why D2 is needed — but the record states the reader. +- **The namespace refusal** is `NamespaceConflictError` in `packages/objectql/src/registry.ts`, as assumed. The line + comment `6370024454` cites (`:5942`) and ruling `6377678758`'s range (`~5929–5942`) have moved: at `47af3dde42` the + gate is `5989–6005`, the throw `6003`, the co-owner set `5974`. +- **"`revenue` first per ADR-0130 §1.3(c)"** (ruling `6369704387`): §1.3(c) names CPQ as the unsellable module, not + revenue. Revenue first is HotCRM's own ordering (module-split plan, hotcrm#2271); §1.3(c) is the general consequence + both serve. No decision moves. +- **ADR numbering.** `0136` and `0140` were taken by proposals that never landed (#18480; #25618, withdrawn + 2026-10-24) and are not reused; no open pull request carries an ADR at `47af3dde42`. This record is `0141`. + +--- + +## Execution plan (after acceptance) + +1. **Runtime card (XS–S, `domain:services` / objectql), one PR**: D2's predicate at the three install doors with the + `dependency_missing` issues and the Console refusal text; D5's dependency source for the co-owner set, recognised on + both registrations, with the ADR-0130 D3 gate test extended to a cross-artifact pair (a dependent defining an object + name X owns is refused, asserting `code` and `status`); D8's record-to-array conversion. Its first readings: open + items 2 and 3. +2. **Docs**: the package-authoring guide states how to declare a dependency and what the install refusal means. +3. **HotCRM** (its own lane): hotcrm#2271 ships revenue as its own artifact once (1) is released. + +## References + +- [#25403](https://github.com/objectstack-ai/objectstack/issues/25403) — the decision card; rulings `6369704387`, + `6377678758`; HotCRM seat input `6370024454` +- [#14488](https://github.com/objectstack-ai/objectstack/issues/14488) — closed `not_planned`, ruling `5645322259` +- ADR-0019, ADR-0028, ADR-0029, ADR-0048 (and its 2026-08-08 addendum), ADR-0086, ADR-0087, ADR-0112, ADR-0116, + ADR-0129, ADR-0130 +- `packages/spec/src/kernel/manifest.zod.ts`, `packages/spec/src/system/app-install.zod.ts`, + `packages/spec/src/marketplace/package-version.zod.ts` +- `packages/objectql/src/registry.ts` (`installPackage`, `refuseCoOwnedObjectNameCollision`, `NamespaceConflictError`) +- `packages/metadata-protocol/src/protocol.ts` (`resolveWritePackageScope`), + `packages/metadata-protocol/src/protocol-package-install.ts` +- `packages/core/src/artifact-packages.ts`, `packages/lint/src/rule-explanations.ts` + (`list-view-object-outside-closure`) +- hotcrm `docs/architecture/module-split-plan.md`, hotcrm#2270, hotcrm#2271 (not re-verified) From d9b22062884e11a3effef33504b280a1763cd313 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:22:55 +0000 Subject: [PATCH 2/7] docs(adr): ADR-0141 cites symbols, not line numbers (check-adr-symbol-anchors) Claude-Session: https://claude.ai/code/session_01HwUcNzGjPhV6xtYkJ469qK Co-authored-by: Claude --- ...endency-declared-checked-before-install.md | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md index 2de0712b404..3b74ebad0c4 100644 --- a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md +++ b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md @@ -81,15 +81,15 @@ The first consumer is a HotCRM module shipping on its own, revenue first. ### What the platform does today (measured at `47af3dde42`) **Declaration exists and is editable.** `ManifestSchema.dependencies` is `z.record(z.string(), z.string()).optional()`, -documented as a "Map of package IDs to version requirements" (`packages/spec/src/kernel/manifest.zod.ts:715`). The -package door edits it after install and validates only its shape (`packages/runtime/src/domains/packages.ts:2319–2335`, -recorded on the registry by `packages/objectql/src/registry.ts:6302`). +documented as a "Map of package IDs to version requirements" (`packages/spec/src/kernel/manifest.zod.ts#ManifestSchema`). The +package door edits it after install and validates only its shape (`packages/runtime/src/domains/packages.ts`, +recorded on the registry by `packages/objectql/src/registry.ts#SchemaRegistry`). **Reference is already judged against the declared closure — at the runtime save door only.** `resolveWritePackageScope` computes the transitive closure of the written package's `manifest.dependencies` -(`packages/metadata-protocol/src/protocol.ts:7526–7606`), and the lint gate keeps an object when it belongs to the +(`packages/metadata-protocol/src/protocol.ts#resolveWritePackageScope`), and the lint gate keeps an object when it belongs to the package, to a declared dependency, to the platform, or to no package. A view bound outside it is refused `422` -(`list-view-object-outside-closure`, `packages/lint/src/rule-explanations.ts:3323`), whose fix line already reads +(`list-view-object-outside-closure`, `packages/lint/src/rule-explanations.ts#LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION`), whose fix line already reads "declare a dependency on the package that defines the object". The same explanation records the boundary of that judgement: `os validate`, `os build` and `os lint` "judge neither: a stack there is one app, and an unresolved binding may be a dependency package's object the stack does not carry". A closure that does not resolve a declared dependency @@ -97,19 +97,19 @@ keeps it in the closure and stops walking — an absent dependency is not a faul **At install, `manifest.dependencies` is read by exactly one consumer, and it orders — it never checks.** The dispatch for this record assumed nothing at install reads the key; the measurement corrects that by one reader. -`artifactPackages` (`packages/core/src/artifact-packages.ts:331–337`) feeds each declared id to `resolvePluginOrder` as an +`resolveArtifactPackageEntries` (`packages/core/src/artifact-packages.ts#resolveArtifactPackageEntries`) feeds each declared id to `resolvePluginOrder` as an **optional** edge, so packages inside one artifact register in dependency order (ADR-0130 D5) and a declared id that is -not in the artifact "is simply not an edge here" — silently skipped by design (its header, lines 112–135). No install -door asks whether a declared dependency is installed, or at what version. `dependency_missing` is an enum value at -`packages/spec/src/system/app-install.zod.ts:88`; across the tree it occurs only there, in that file's test and in the +not in the artifact "is simply not an edge here" — silently skipped by design (its module header). No install +door asks whether a declared dependency is installed, or at what version. `dependency_missing` is an enum value of +`packages/spec/src/system/app-install.zod.ts#AppCompatibilityCheckSchema`; across the tree it occurs only there, in that file's test and in the generated reference page — zero producers. `packages/core/src/dependency-resolver.ts` serves kernel plugins, not installed packages. **The namespace gate admits co-owners from one source only.** `SchemaRegistry.installPackage` builds its co-owner set -from the artifact scope the load path passes (`registry.ts:5974`, `scope?.packageIds`) and refuses a package whose -non-shareable namespace an installed non-co-owner owns, throwing `NamespaceConflictError` (`registry.ts:5989–6005`, the -throw at `6003`; the class at `1886`, `422 NAMESPACE_CONFLICT`). Its other half, `refuseCoOwnedObjectNameCollision` -(`registry.ts:6153`), refuses two co-owners defining one object name (`ArtifactObjectNameConflictError`, +from the artifact scope the load path passes (`packages/objectql/src/registry.ts#installPackage`, `scope?.packageIds`, line 5974) and refuses a package whose +non-shareable namespace an installed non-co-owner owns, throwing `NamespaceConflictError` (lines 5989–6005, the +throw at 6003; the class `packages/objectql/src/registry.ts#NamespaceConflictError`, `422 NAMESPACE_CONFLICT`). Its other half, `refuseCoOwnedObjectNameCollision` +(`packages/objectql/src/registry.ts#refuseCoOwnedObjectNameCollision`), refuses two co-owners defining one object name (`ArtifactObjectNameConflictError`, `DUPLICATE_ARTIFACT_OBJECT_NAME`) and returns early when the co-owner set is empty. So a module published as its own artifact with HotCRM's namespace `crm` is refused next to an installed HotCRM today, whatever it declares. @@ -117,12 +117,12 @@ artifact with HotCRM's namespace `crm` is refused next to an installed HotCRM to | Carrier | Shape | Who reads it at `47af3dde42` | |:---|:---|:---| -| `ManifestSchema.dependencies` (`kernel/manifest.zod.ts:715`) | record `{ [packageId]: versionRange }`, optional | the save-door closure; the intra-artifact sort; the package door's edit | -| `PackageManifestSchema.dependencies` (`marketplace/package-version.zod.ts:109`), entries `PackageDependencySchema` (`:56`) | array of `{ packageId, versionRange, optional = false }`, default `[]` | no runtime reader in this repository | -| `AppManifestSchema.dependencies` (`system/app-install.zod.ts:60`) | array of bare strings, no version, default `[]` | no reader outside `packages/spec` | +| `ManifestSchema.dependencies` (`packages/spec/src/kernel/manifest.zod.ts#ManifestSchema`) | record `{ [packageId]: versionRange }`, optional | the save-door closure; the intra-artifact sort; the package door's edit | +| `PackageManifestSchema.dependencies` (`packages/spec/src/marketplace/package-version.zod.ts#PackageManifestSchema`), entries `#PackageDependencySchema` | array of `{ packageId, versionRange, optional = false }`, default `[]` | no runtime reader in this repository | +| `AppManifestSchema.dependencies` (`packages/spec/src/system/app-install.zod.ts#AppManifestSchema`) | array of bare strings, no version, default `[]` | no reader outside `packages/spec` | `os package publish` snapshots the compiled `dist/objectstack.json` into `sys_package_version.manifest_json` -(`packages/cli/src/commands/package/publish.ts:11–12`), so the snapshot carries the record shape where +(`packages/cli/src/commands/package/publish.ts`, its header steps 1–2), so the snapshot carries the record shape where `PackageManifestSchema` declares an array. Whether the control plane parses that snapshot against the array schema lives in `objectstack-ai/cloud` — **not measured** here. @@ -354,7 +354,7 @@ The letters as filed on #25403 and on ruling `6377678758`: recognition rule makes the answer harmless; the card records it). 3. **Declared ids that are not installable packages.** `ManifestSchema.dependencies`' own documentation example names an npm-style id (`@steedos/plugin-auth`). Measured population in this repository: one declaring manifest, - `examples/app-multi-package/src/packages/orders/index.ts:65`, whose dependency is delivered by the same artifact (D2's + `examples/app-multi-package/src/packages/orders/index.ts`, whose dependency is delivered by the same artifact (D2's second arm). HotCRM's manifests are **not measured**. If an id that names no installable package turns up, the card reports it; D2 does not exempt it. 4. **`optional` and `AppManifestSchema.dependencies`** — D8 items 2 and 3. @@ -378,10 +378,10 @@ These are the points this draft decided where the rulings were silent, listed so ## Verification notes - **The dispatch's assumption "nothing at install reads `manifest.dependencies`"** holds for checking and fails for - ordering: `artifactPackages` reads every declared id as an optional sort edge (Context above). It changes no decision — + ordering: `resolveArtifactPackageEntries` reads every declared id as an optional sort edge (Context above). It changes no decision — the sort skips an absent id silently, which is exactly why D2 is needed — but the record states the reader. - **The namespace refusal** is `NamespaceConflictError` in `packages/objectql/src/registry.ts`, as assumed. The line - comment `6370024454` cites (`:5942`) and ruling `6377678758`'s range (`~5929–5942`) have moved: at `47af3dde42` the + comment `6370024454` cites (line 5942) and ruling `6377678758`'s range (`~5929–5942`) have moved: at `47af3dde42` the gate is `5989–6005`, the throw `6003`, the co-owner set `5974`. - **"`revenue` first per ADR-0130 §1.3(c)"** (ruling `6369704387`): §1.3(c) names CPQ as the unsellable module, not revenue. Revenue first is HotCRM's own ordering (module-split plan, hotcrm#2271); §1.3(c) is the general consequence From 247fe2faa90bec0ed5fb2fd4a6014c17aaa8276f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 01:58:20 +0000 Subject: [PATCH 3/7] =?UTF-8?q?docs(adr):=20ADR-0141=20redrafted=20?= =?UTF-8?q?=E2=80=94=20rulings=20re-verified=20against=20the=20code,=20D2?= =?UTF-8?q?=20placement=20and=20D5=20relation=20restated,=20neighbour=20re?= =?UTF-8?q?cords=20re-amended?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WIP: full re-drafting pass over the first draft. Every factual claim re-read at the branch base; the maintainer's five verbatim lines carried whole; D2 names the three install acts and the three re-registration paths; D5 defines the co-owner relation once and states what reads it; D4 gains the ADR-0086 D4 discriminator question; ADR-0130's addendum records that #14488's premise sentence moved while its disposition stands. Claude-Session: https://claude.ai/code/session_01HwUcNzGjPhV6xtYkJ469qK Co-authored-by: Claude --- docs/adr/0019-app-as-consumer-unit.md | 2 +- ...metadata-naming-and-namespace-isolation.md | 51 +- ...lease-artifact-as-co-ownership-boundary.md | 78 ++- ...endency-declared-checked-before-install.md | 656 ++++++++++-------- 4 files changed, 455 insertions(+), 332 deletions(-) diff --git a/docs/adr/0019-app-as-consumer-unit.md b/docs/adr/0019-app-as-consumer-unit.md index 1138f7991c8..fa49cc3ac5c 100644 --- a/docs/adr/0019-app-as-consumer-unit.md +++ b/docs/adr/0019-app-as-consumer-unit.md @@ -5,7 +5,7 @@ **Deciders**: ObjectStack Protocol Architects **Builds on**: ADR-0003 (Package as First-Class Citizen), ADR-0006 (Three-Layer Tenancy), ADR-0016 (Studio Package Authoring & Publish) **Consumers**: `@objectstack/spec/kernel` (`plugin.zod.ts`), `@objectstack/spec/cloud` (`marketplace.zod.ts`), the Console `marketplace` UI, the Studio publish flow, `docs/design/marketplace-publishing.md` -**Related**: [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) (2026-10-24) — the separate, gated decision §Out of scope asks for on "inter-App dependencies", narrowed to a declared dependency and a pre-install check; additive cross-reference, this record's text is unchanged +**Related**: [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) (2026-10-25) — the separate, gated decision §Out of scope asks for on "Shared/published data model + inter-App dependencies", taken for the dependency half only (a declared `manifest.dependencies` entry plus a pre-install check; the shared data model half stays out of scope). Additive cross-reference under ADR-0130 D2's recording convention; this record's text is unchanged. > **Revision history** > - **v1/v2** explored a much larger scheme: a metadata/code *plane* split, diff --git a/docs/adr/0028-metadata-naming-and-namespace-isolation.md b/docs/adr/0028-metadata-naming-and-namespace-isolation.md index 5ea1e35a385..2ff92581b25 100644 --- a/docs/adr/0028-metadata-naming-and-namespace-isolation.md +++ b/docs/adr/0028-metadata-naming-and-namespace-isolation.md @@ -1,6 +1,6 @@ # ADR-0028: Metadata Naming & Namespace Isolation — Derived Physical Names, Namespace-Scoped Identity, and a Single Kernel Contract -**Status**: Deferred (2026-07-16; proposed 2026-06-01) — the target model (namespace as identity dimension, short authored names, derived physical names, `namingMode` dual-read, `sys` reservation, namespaced transport segments) is entirely unbuilt, and adopting it now would be a platform-wide migration (physical table derivation, transport routes, dual-read compatibility) with no present pain to justify it: the literal-prefix current-state contract (`spec/kernel/namespace-prefix.ts`) is enforced, collision-proof (ADR-0028/0048), and working. **Re-open trigger**: real multi-package ecosystem naming pain — e.g. a marketplace package whose natural short names are unusable under literal prefixing, or cross-environment portability requiring namespace remapping. Until then this stays a recorded design, not planned work. · **Amended** (2026-10-24, [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D6 — D4 is rewritten from "no cross-app references" to "no cross-app reference without a declared dependency"; the rest of this record stays Deferred) +**Status**: Deferred (2026-07-16; proposed 2026-06-01) — the target model (namespace as identity dimension, short authored names, derived physical names, `namingMode` dual-read, `sys` reservation, namespaced transport segments) is entirely unbuilt, and adopting it now would be a platform-wide migration (physical table derivation, transport routes, dual-read compatibility) with no present pain to justify it: the literal-prefix current-state contract (`spec/kernel/namespace-prefix.ts`) is enforced, collision-proof (ADR-0028/0048), and working. **Re-open trigger**: real multi-package ecosystem naming pain — e.g. a marketplace package whose natural short names are unusable under literal prefixing, or cross-environment portability requiring namespace remapping. Until then this stays a recorded design, not planned work. · **Amended** (2026-10-25, [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D6, rulings [`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387) and [`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758) on #25403): D4's sentence is rewritten from "Apps are sandboxed: no cross-app references" to "No cross-app reference without a declared dependency", and the three sentences that restate it (TL;DR item 4, D6's author-time bullet, leak-point 3) follow; nothing else moves and the record stays Deferred. **Deciders**: ObjectStack Protocol Architects **Supersedes**: the hand-written object-namespace-prefix authoring rule documented in `packages/spec/src/kernel/manifest.zod.ts` (the `namespace` field) and enforced by `validateNamespacePrefix()` in `packages/spec/src/stack.zod.ts` — that rule's standalone record is [ADR-0129](./0129-object-name-is-the-canonical-id.md) (2026-08-29; it governs until this ADR's re-open trigger fires). **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0008](./0008-metadata-repository-and-change-log.md) (Repository · ChangeLog · Cache · Registry; `MetaRef = org/type/name`), [ADR-0010](./0010-metadata-protection-model.md) (protection model), [ADR-0019](./0019-app-as-consumer-unit.md) (app as the consumer-installable unit), [ADR-0025](./0025-plugin-package-distribution.md) (package distribution), [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) (**prerequisite** — kernel object ownership; D5/D6 below assume the kernel is properly owned per ADR-0029) @@ -34,9 +34,11 @@ metadata platform actually uses, and extends collision-freedom to **all** kinds: it physically matters (storage tables, route table, tool registry), not on the authored name. 4. **No cross-app reference without a declared dependency** (rewritten - 2026-10-24 by ADR-0141; first proposed as full sandboxing, app → kernel - only). An app reaches the kernel and the apps it declares in - `dependencies`, nothing else. This doubles as a security boundary. + 2026-10-25 by ADR-0141 D6; first proposed as "Apps are sandboxed: no + cross-app references", app → kernel only). The legal cross-boundary + references are **app → kernel** and **app → an app its manifest declares in + `dependencies`**; every other cross-app reference is refused. The closed set + is the security boundary. 5. **The kernel is one unified, reserved namespace (`sys`) — one contract, but ownership distributed across first-party plugins.** It is the platform's public contract and the sole well-known import target (`sys.user`). Each @@ -155,21 +157,25 @@ schema), resolved back to `(ns, name)` at runtime. ### D4 — No cross-app reference without a declared dependency (security boundary) -> **Rewritten (2026-10-24, [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D6**, rulings -> [`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387) and -> [`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758) on #25403). This -> decision first read *"Apps are sandboxed: no cross-app references"* — a `type: app` package *"may not reference -> metadata owned by another app"*. The isolation stays; its boundary is now the app's declared dependencies. The rest -> of this record stays Deferred and unchanged. +> **Rewritten 2026-10-25 by [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D6** +> (rulings [`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387) and +> [`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758) on #25403). As +> proposed on 2026-06-01 this decision read *"Apps are sandboxed: no cross-app references"* — a `type: app` package +> *"may not reference metadata owned by another app"*, the only legal cross-boundary reference being app → kernel. The +> isolation stays; its boundary is now the set of apps the manifest declares. The rest of this record is unchanged and +> stays Deferred. A `type: app` package may reference its own metadata (short names), the kernel (qualified), and the metadata of the apps its manifest declares in -`dependencies`; it **may not reference metadata owned by any other app**. -The closed set keeps the tenant-style isolation boundary between marketplace -apps — an app reaches only what it declared — and keeps the "two writing -styles" risk to declared, allow-listed cases. The runtime save door already -judges a package write against that closure (ADR-0141 D1); installing an app -whose declared dependency is absent or out of range is refused (ADR-0141 D2). +`dependencies` (qualified); it **may not reference metadata owned by any other +app**. The "two writing styles" risk stays collapsed to allow-listed, +declaration-driven cases — a qualified form appears only for a cross-boundary +reference, and the set of legal targets is closed and written in the manifest — +and the closed set keeps the tenant-style isolation boundary between +marketplace apps: an app reaches only what it declared. On the enforced +present-state model (ADR-0129), the runtime save door already judges a package +write against that closure, and ADR-0141 D2 refuses to install an app whose +declared dependency is absent or out of range. ### D5 — The kernel is one unified, reserved namespace (`sys`) with object ownership distributed across first-party plugins @@ -243,8 +249,9 @@ reference target. It is: - **Author time** (`defineStack`, `os validate`): generalize `validateNamespacePrefix` into a *namespace-scope* validator over **all** collections — names must be bare short names (no `ns_` prefix, no `__`), - references resolve within the package namespace or to a qualified - `sys.x` / a cross-app ref only to a declared dependency (D4). Early failure with the exact fix string. + references resolve within the package namespace, to a qualified `sys.x`, or + to a qualified reference into a declared dependency (D4); any other cross-app + ref is refused. Early failure with the exact fix string. - **Install time** (package registry): register every `(namespace, type, name)` plus every derived transport key (route, connector id, tool name, webhook). The only true conflict left is **two @@ -268,9 +275,11 @@ the author/AI as a second style: name needs a `tableName`/`physicalName` override — derivation is the *default*, not mandatory (mirrors `columnName`). 3. **Cross-package references.** Within a package: short name. To the kernel: - qualified `sys.x`. Cross-app: only to a declared dependency (D4). This is the one place a - qualified form appears, and it is a single deterministic rule (like an - `import`), not an arbitrary second style. + qualified `sys.x`. To an app the manifest declares in `dependencies`: + qualified (D4). Any other app: forbidden (D4). A qualified form appears + only at a cross-boundary reference, and it is a single deterministic rule + (like an `import` whose targets are the declared dependencies), not an + arbitrary second style. 4. **Diagnostics.** Driver errors/logs will show `todo_task`; this is read-only and not authored, so it does not reintroduce ambiguity (cosmetic mapping only). diff --git a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md index 7816143921f..b7c857554d9 100644 --- a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md +++ b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md @@ -9,11 +9,12 @@ ([#14512](https://github.com/objectstack-ai/objectstack/issues/14512)): a multi-package artifact carries its metadata **once**, in `packages[]` — the flattened copy is no longer emitted beside it. D4's read-both rule and D7 are unchanged, so every artifact already on disk still loads. -**D8's deferral end point amended by the 2026-10-24 addendum** +**D8's deferral end point amended by the 2026-10-25 addendum** ([ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D5, -[#25403](https://github.com/objectstack-ai/objectstack/issues/25403)): a package that declares `dependencies` on an -installed package and shares its namespace co-owns it across artifacts, so the owner field is deferred until -marketplace publishing rather than until a module's first standalone artifact. The same addendum records that the +[#25403](https://github.com/objectstack-ai/objectstack/issues/25403)): a package that declares +`dependencies` on an installed package and shares its namespace is that package's co-owner at the +install gate, across artifacts, so the owner field is deferred until **marketplace publishing** +rather than until a module's first standalone artifact. The same addendum records that the 2026-09-02 addendum's #14488 line is stale. D1–D7 are unchanged. **Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated: 「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in @@ -752,37 +753,54 @@ fallback bolted onto the emitter. --- -## Addendum (2026-10-24, #25403) — D8's deferral ends at marketplace publishing; the #14488 line is stale +## Addendum (2026-10-25, #25403) — D8's deferral ends at marketplace publishing; the #14488 line is stale -Recorded by [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md), which executes two maintainer -rulings on [#25403](https://github.com/objectstack-ai/objectstack/issues/25403): letter B′ -([`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387), 「同意」 -2026-10-23T16:28:30Z) and letter A -([`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758), 「同意 #8」 -2026-10-24T00:40:55Z). ⛔ This section edits no text above: D1–D8, §1–§8 and both earlier addenda read as written, -except where the two points below say how a sentence now reads. +**Provenance.** Two maintainer rulings on +[#25403](https://github.com/objectstack-ai/objectstack/issues/25403), recorded by +[ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md): letter B′ +([`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387), +「同意」 2026-10-23T16:28:30Z — an app may depend on another installed app; one pre-install check; +the rest deferred by name) and letter A +([`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758), +「同意 #8」 2026-10-24T00:40:55Z — 「声明依赖即命名空间共有,单向,D3 对象名唯一检查跨发布物同行」). ⛔ This +section edits no text above: D1–D8, §1–§8 and both earlier addenda read as written; the two points +below say how two of their sentences now resolve. ### D8's deferral now ends at marketplace publishing -D6 and D8 said the owner field becomes necessary *"the day a module ships as its own artifact"*. ADR-0141 D5 answers that -day without the field: a package that declares `dependencies` on an installed package X and uses X's namespace is X's -co-owner at the install gate — one-way, recognised whichever registers first — and D3's per-object-name uniqueness -check spans those co-owners across artifacts exactly as it spans co-owners inside one artifact. Inside one tenant the -proof is the declaration plus the administrator's install act. - -So D8's deferral now ends at **marketplace publishing**: when a module is published for other tenants and must assert -*"I have the same publisher as HotCRM"* across vendors, the owner / publisher field lands, aligned to the -`publisherOrgId` reservation of ADR-0048's 2026-08-08 addendum (still Proposed), as D8 already requires. That field is -the later superset of ADR-0141 D5 and needs no undo of it. D6's cost statement stands for modules inside one artifact; -a module that becomes its own artifact trades it for the separate-version cost ADR-0141's Consequences state. +D6 and D8 above say the owner field becomes necessary *"the day a module ships as its own +artifact"*. ADR-0141 D5 answers that day without the field: a package that declares `dependencies` +on an installed package X and uses X's namespace is X's co-owner at the install gate — one-way (X +declares nothing and gains nothing), read from either end at registration, direct declarations +only — and D3's per-object-name uniqueness check spans those co-owners across artifacts exactly as +it spans co-owners inside one artifact, because the registry reads one co-owner set for both halves +of the pair. Inside one tenant the proof is the declaration plus the administrator's install act; +no manifest field is added. + +So D8's deferral now ends at **marketplace publishing**: when a module is published for other +tenants and must assert *"I have the same publisher as HotCRM"* across vendors, the owner / +publisher field lands, aligned to the `publisherOrgId` reservation of ADR-0048's 2026-08-08 +addendum (still Proposed; its A.5 precondition is the opening of third-party publishing), as D8 +already requires. That field is the later superset of ADR-0141 D5 and needs no undo of it. D6's +cost statement stands for modules inside one artifact; a module that becomes its own artifact +trades that cost for the separate-version cost ADR-0141's Consequences state, bounded by the +declared range and judged once, at install. ### The #14488 line is stale The 2026-09-02 addendum's *What was NOT decided* section lists -[#14488](https://github.com/objectstack-ai/objectstack/issues/14488) (per-package composition of grants) as *"filed, not -decided"*. It is decided: #14488 was closed `not_planned` on 2026-09-12 by ruling -[`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259) — an artifact installs -whole, the app package's permission sets cover every module's objects, nothing composes grants into another package's -set, and a module carved out as its own artifact ships its own permission sets under ADR-0086 D4. The three inherited -inputs listed under that line remain accurate as history; none is open work. A standalone module's permission sets are -the module author's work, not a platform blocker (ADR-0141 D4, D6). +[#14488](https://github.com/objectstack-ai/objectstack/issues/14488) (per-package composition of +grants) as *"filed, not decided"*. It is decided: #14488 was closed `not_planned` on 2026-09-12 by +ruling [`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259) +— an artifact installs whole, the app package's permission sets cover every module's objects, +nothing composes grants into another package's set, and a module carved out as its own artifact +ships its own permission sets under ADR-0086 D4; the card is ⛔ not reopened for that case. The three +inherited inputs listed under that line remain accurate as history; none is open work. + +One sentence of that ruling has since moved, and it is said here so a reader of both records is not +left to reconcile them: its premise 「目前的要求是发布物是完整安装的,没有设计单独安装其中一个模块的需求」 was +the state on 2026-09-12; the maintainer's B′ ruling of 2026-10-23 (「而每个包可以独立安装」) is the +requirement it said did not exist. The ruling's disposition is unchanged by that: a standalone +module's permission sets are the module author's work under ADR-0086 D4, not a platform blocker +(ADR-0141 D4, D6). What record-level access across a dependency means is ADR-0141 D4's open +section, decided with the first consumer. diff --git a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md index 3b74ebad0c4..661d0be4fe3 100644 --- a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md +++ b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md @@ -1,76 +1,92 @@ # ADR-0141: An app may depend on another installed app — declared in `manifest.dependencies`, checked once before install, and co-owning the namespace it shares -**Status**: Proposed (2026-10-24). `docs/adr/**` is Tier H (Prime Directive #14): this record becomes **Accepted** by -the maintainer's merge, and ⛔ the execution cards under [Execution plan](#execution-plan-after-acceptance) are cut after -that merge and never before it. Until then a composite product keeps shipping as one release artifact under -[ADR-0130](./0130-release-artifact-as-co-ownership-boundary.md). -**Decided by**: two maintainer rulings on [#25403](https://github.com/objectstack-ai/objectstack/issues/25403), recorded -verbatim and untranslated: - -- ruling [`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387), batch #7 - item 1, letter **B′**, 「同意」 at 2026-10-23T16:28:30Z — the declaration stays as it is, the save door's closure is - the reference mechanism, a pre-install check is the only new gate, the rest is deferred by name, permissions are - unchanged. The maintainer's reasoning, from the director thread that day: +**Status**: Proposed (2026-10-25; first draft 2026-10-24). `docs/adr/**` is Tier H (Prime Directive #14): this record +becomes **Accepted** by the maintainer's merge, and ⛔ the cards under [Execution plan](#execution-plan-after-acceptance) +are cut after that merge, never before it. Until the pre-install check ships, a composite product keeps shipping as one +release artifact under [ADR-0130](./0130-release-artifact-as-co-ownership-boundary.md). **This record declares; it +implements nothing** — no schema, no code, no changeset. +**Decided by**: two maintainer rulings on [#25403](https://github.com/objectstack-ai/objectstack/issues/25403), executed +here as written and ⛔ not re-opened: + +- Ruling [`6369704387`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6369704387) — batch #7 + item 1, letter **B′**, 「同意」 at 2026-10-23T16:28:30Z. The card's letter B (full inter-app dependency: install + ordering, uninstall protection, version compatibility, cross-app permission semantics, Console dependency views), + narrowed by the maintainer to: the declaration stays as it is, the save door's closure is the reference mechanism, a + pre-install check is the only new gate, the rest is deferred by name, permissions are unchanged. The maintainer's + reasoning, verbatim and untranslated, as the ruling records it from the director thread of 2026-10-23: + > 25403 如果按照包依赖的方式,整个开发会复杂很多,如果每个应用包是单独安装的,有其他方式把数据串起来吗 (16:10:20Z) + > > 往往需要引用的数据并不多,可能就几个对象 (16:10:38Z) > > 包依赖本身没问题,也就是说如果 psa 依赖 crm, 安装psa 之前先检测crm有没有,没有就禁止安装,这个有必要嘛 (16:20:28Z) > > 如果这样,hotcrm 岂不是真实的可以分拆出很多模块 (16:25:59Z) / 而每个包可以独立安装 (16:26:18Z) -- ruling [`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758), batch #8 - item 2, letter **A**, 「同意 #8」 at 2026-10-24T00:40:55Z — 「声明依赖即命名空间共有,单向,D3 对象名唯一检查跨发布物同行」, - no new manifest field, and ADR-0130 D8's deferral reworded to end at marketplace publishing. + > + > 同意 (16:28:30Z) +- Ruling [`6377678758`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6377678758) — batch #8 + item 2, letter **A**, 「同意 #8」 at 2026-10-24T00:40:55Z, raised by the `repo:hotcrm` seat's input + [`6370024454`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6370024454): dependency-declared + namespace co-ownership — 「声明依赖即命名空间共有,单向,D3 对象名唯一检查跨发布物同行」 — no new manifest field, and + ADR-0130 D8's deferral reworded to end at marketplace publishing. Letters B (an owner / publisher field now) and C + (rename the module's objects) were not chosen. -The second ruling answers input 1 of the `repo:hotcrm` seat's comment -[`6370024454`](https://github.com/objectstack-ai/objectstack/issues/25403#issuecomment-6370024454); its input 2 (a stale -line in ADR-0130) is corrected by D6 below. **Builds on**: [ADR-0019](./0019-app-as-consumer-unit.md) (the App is the consumer unit; §Out of scope deferred -"inter-App dependencies" to a separate gated decision — this is that decision, narrowed), -[ADR-0130](./0130-release-artifact-as-co-ownership-boundary.md) D1, D3, D5, D6, D8 (co-ownership inside one artifact, the -gate/uniqueness pair, the one sorter, one version per artifact, the deferred owner field), +"Shared/published data model + inter-App dependencies" to a separate gated decision — this record is that decision for +the dependency half), [ADR-0130](./0130-release-artifact-as-co-ownership-boundary.md) D1, D3, D5, D6, D8 (co-ownership +inside one artifact; the gate-and-uniqueness pair; the one sorter; one version per artifact; the deferred owner field), [ADR-0048](./0048-cross-package-metadata-collision.md) §3.2 and its [2026-08-08 addendum](./0048-cross-package-metadata-collision.md#addendum-2026-08-08-publish-time--marketplace-namespace-exclusivity) -(the install-time namespace gate; the publisher-held reservation, still Proposed), -[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D4 (a package ships its own -permission sets and never writes a foreign one), [ADR-0087](./0087-metadata-protocol-upgrade-contract.md) D1 (the -protocol handshake every install door already runs before it registers anything — the placement D2 reuses), -[ADR-0112](./0112-error-code-vocabulary-and-ledger.md) (the refusal envelope), -[ADR-0116](./0116-plugin-ordering-declared-contract.md) (ordering is a declared contract with one sorter), -[ADR-0129](./0129-object-name-is-the-canonical-id.md) (an object name carries its module prefix literally, which is why -a split module keeps `crm_*` and its namespace). -**Amends**: [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) D4 (a Deferred proposal; its sentence is -rewritten, D6 below); ADR-0130 D8's deferral end point and the stale #14488 line of its 2026-09-02 addendum (each by an -additive addendum on 0130; no original text of 0130 is edited). **Cross-references**: ADR-0019 gains one additive -`Related` line; its text is unchanged (ADR-0130 D2's recording convention). -**Consumers**: `@objectstack/spec` (`ManifestSchema.dependencies` in `kernel/manifest.zod.ts`, -`AppCompatibilityCheckSchema` and `AppManifestSchema` in `system/app-install.zod.ts`, `PackageDependencySchema` and +(the install-time namespace gate; the publisher-held reservation, still Proposed, whose activation precondition A.5 is +third-party publishing), [ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D4 (a package +ships its own permission sets and never writes a foreign one), +[ADR-0087](./0087-metadata-protocol-upgrade-contract.md) D1 (the protocol handshake every install door runs before it +registers anything), [ADR-0112](./0112-error-code-vocabulary-and-ledger.md) D3 (a refusal's code is ledger-registered; +the envelope is `code` + `status`), [ADR-0116](./0116-plugin-ordering-declared-contract.md) (ordering is a declared +contract with one sorter), [ADR-0129](./0129-object-name-is-the-canonical-id.md) (an object name carries its module +prefix literally — why a split module keeps `crm_*` and the `crm` namespace), and +[ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) (the one uninstall refusal that exists +today: a package whose object another package `extend`s). +**Amends**: [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) D4 — a Deferred proposal whose sentence is +rewritten (D6 below); ADR-0130 D8's deferral end point and the stale #14488 line of its 2026-09-02 addendum — by an +additive addendum on 0130, no original text of 0130 edited; ADR-0048 §3.2's install gate gains a second co-owner source +(D5) — by cross-reference from here, exactly as ADR-0130 D1 amended that gate, ⛔ no text of 0048 edited. +**Cross-references**: ADR-0019 gains one additive `Related` header line; its text is unchanged (ADR-0130 D2's recording +convention, which that record offered as the follow-up should the maintainer want the amendment visible from 0019). +**Consumers**: `@objectstack/spec` (`ManifestSchema.dependencies` in `kernel/manifest.zod.ts`; +`AppCompatibilityCheckSchema` and `AppManifestSchema` in `system/app-install.zod.ts`; `PackageDependencySchema` and `PackageManifestSchema` in `marketplace/package-version.zod.ts`), `@objectstack/objectql` (`SchemaRegistry.installPackage` -and `refuseCoOwnedObjectNameCollision` in `registry.ts`), `@objectstack/metadata-protocol` (`installPackage` in -`protocol-package-install.ts`; `resolveWritePackageScope` in `protocol.ts`), `@objectstack/runtime` (`POST -/api/v1/packages`, `domains/packages.ts`), `@objectstack/cloud-connection` (`POST /api/v1/marketplace/install-local`), the -`objectui` Console's install refusal text, `packages/cli` (`os package publish`), and HotCRM's module split. -**Evidence**: every anchor in this repository was read at `origin/main` `47af3dde42` while drafting. Repositories this -session cannot read (`objectstack-ai/hotcrm`, `objectstack-ai/cloud`) are cited as the rulings cite them and marked +and `refuseCoOwnedObjectNameCollision` in `registry.ts`; the manifest service in `plugin.ts`), +`@objectstack/metadata-protocol` (`installPackage` in `protocol-package-install.ts`; `resolveWritePackageScope` in +`protocol.ts`), `@objectstack/runtime` (`POST /api/v1/packages` and `PATCH /api/v1/packages/:id`, `domains/packages.ts`), +`@objectstack/cloud-connection` (`POST /api/v1/marketplace/install-local`), the `objectui` Console's install refusal +text, `packages/cli` (`os package publish`), and HotCRM's module split. +**Evidence**: every anchor into this repository was read at `origin/main` `47af3dde42`, the base of this record's branch +(the tree outside `docs/adr/` is identical at the record's head). `objectstack-ai/hotcrm` and `objectstack-ai/cloud` are +not readable from the drafting session; their facts are cited as the rulings and the HotCRM seat cite them and marked **not re-verified**. --- ## TL;DR -An installable app may now depend on another installed app and reference its objects. **Nothing new is authored**: +An installable app may depend on another installed app and reference its objects. Nothing new is authored; one gate is +added; one gate learns a second source of co-owners: -1. **Declaration** is the `dependencies` key every manifest already has (`{ packageId: versionRange }`). -2. **Reference** is what the runtime save door already allows: a package's own objects, the objects of the packages its - `dependencies` declare (transitively), platform objects, and tenant objects that belong to no package. -3. **The one new gate is a pre-install check.** Installing a package checks each declared dependency — installed in the +1. **Declaration** is the `dependencies` key every manifest already has — `{ "PACKAGE_ID": "SEMVER_RANGE" }`. +2. **Reference** is what the runtime save door already admits: a package's own objects, the objects of the packages its + `dependencies` declare (transitively), platform objects, and tenant objects that belong to no package. An undeclared + cross-app reference is refused there today, `422`, with "declare a dependency" as the fix. +3. **The one new gate is a pre-install check.** Installing a package judges every declared dependency — installed in the target environment (or delivered by the same artifact) and at a version inside the declared range — and otherwise - refuses the install with a message naming what is missing (「请先安装 HotCRM 6.x」). It fills the `dependency_missing` - category `AppCompatibilityCheckSchema` has reserved with no producer. + refuses the whole install, before anything registers, with a message naming what is missing (the ruling's example: + 「请先安装 HotCRM 6.x」). It fills the `dependency_missing` category `AppCompatibilityCheckSchema` has reserved with no + producer. 4. **A package that declares a dependency on an installed package X and uses X's namespace co-owns that namespace with - X** at the install gate. One-way: X declares nothing and gains nothing. ADR-0130 D3's per-object-name uniqueness check - spans these co-owners across artifacts, exactly as it spans co-owners inside one artifact. -5. **Deferred, by name**: uninstall protection, upgrade interception, topological / cascade install, an owner-side - publish marker, marketplace version negotiation. **Permissions are unchanged**; record-level semantics across a - dependency are an open section, decided with the first consumer. + X at the install gate.** One-way: X declares nothing and gains nothing. ADR-0130 D3's per-object-name uniqueness + check spans these co-owners across artifacts exactly as it spans co-owners inside one artifact. +5. **Deferred, by name, with a trigger each**: uninstall protection, upgrade interception, topological / cascade install, + an owner-side publish marker, marketplace version negotiation. **Permissions are unchanged**; record-level semantics + across a dependency are an open section, decided with the first consumer. The first consumer is a HotCRM module shipping on its own, revenue first. @@ -78,64 +94,82 @@ The first consumer is a HotCRM module shipping on its own, revenue first. ## Context -### What the platform does today (measured at `47af3dde42`) - -**Declaration exists and is editable.** `ManifestSchema.dependencies` is `z.record(z.string(), z.string()).optional()`, -documented as a "Map of package IDs to version requirements" (`packages/spec/src/kernel/manifest.zod.ts#ManifestSchema`). The -package door edits it after install and validates only its shape (`packages/runtime/src/domains/packages.ts`, -recorded on the registry by `packages/objectql/src/registry.ts#SchemaRegistry`). - -**Reference is already judged against the declared closure — at the runtime save door only.** -`resolveWritePackageScope` computes the transitive closure of the written package's `manifest.dependencies` -(`packages/metadata-protocol/src/protocol.ts#resolveWritePackageScope`), and the lint gate keeps an object when it belongs to the -package, to a declared dependency, to the platform, or to no package. A view bound outside it is refused `422` -(`list-view-object-outside-closure`, `packages/lint/src/rule-explanations.ts#LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION`), whose fix line already reads -"declare a dependency on the package that defines the object". The same explanation records the boundary of that -judgement: `os validate`, `os build` and `os lint` "judge neither: a stack there is one app, and an unresolved binding -may be a dependency package's object the stack does not carry". A closure that does not resolve a declared dependency -keeps it in the closure and stops walking — an absent dependency is not a fault there. - -**At install, `manifest.dependencies` is read by exactly one consumer, and it orders — it never checks.** The dispatch -for this record assumed nothing at install reads the key; the measurement corrects that by one reader. -`resolveArtifactPackageEntries` (`packages/core/src/artifact-packages.ts#resolveArtifactPackageEntries`) feeds each declared id to `resolvePluginOrder` as an -**optional** edge, so packages inside one artifact register in dependency order (ADR-0130 D5) and a declared id that is -not in the artifact "is simply not an edge here" — silently skipped by design (its module header). No install -door asks whether a declared dependency is installed, or at what version. `dependency_missing` is an enum value of -`packages/spec/src/system/app-install.zod.ts#AppCompatibilityCheckSchema`; across the tree it occurs only there, in that file's test and in the -generated reference page — zero producers. `packages/core/src/dependency-resolver.ts` serves kernel plugins, not -installed packages. - -**The namespace gate admits co-owners from one source only.** `SchemaRegistry.installPackage` builds its co-owner set -from the artifact scope the load path passes (`packages/objectql/src/registry.ts#installPackage`, `scope?.packageIds`, line 5974) and refuses a package whose -non-shareable namespace an installed non-co-owner owns, throwing `NamespaceConflictError` (lines 5989–6005, the -throw at 6003; the class `packages/objectql/src/registry.ts#NamespaceConflictError`, `422 NAMESPACE_CONFLICT`). Its other half, `refuseCoOwnedObjectNameCollision` -(`packages/objectql/src/registry.ts#refuseCoOwnedObjectNameCollision`), refuses two co-owners defining one object name (`ArtifactObjectNameConflictError`, -`DUPLICATE_ARTIFACT_OBJECT_NAME`) and returns early when the co-owner set is empty. So a module published as its own -artifact with HotCRM's namespace `crm` is refused next to an installed HotCRM today, whatever it declares. +### 1. What the platform does today (measured at `47af3dde42`) + +**The declaration exists and is editable after install.** `ManifestSchema.dependencies` is +`z.record(z.string(), z.string()).optional()`, documented as "Map of package IDs to version requirements" with an +npm-style example (`packages/spec/src/kernel/manifest.zod.ts#ManifestSchema`). `PATCH /api/v1/packages/:id` edits it on +an installed package and validates its shape by reference to that very schema — nothing else: its own comment says +⛔ No in-use check is made (`packages/runtime/src/domains/packages.ts`). + +**Reference is already judged against the declared closure — at the runtime save door, nowhere else.** +`resolveWritePackageScope` computes the **transitive** closure of the written package's `manifest.dependencies` +(`packages/metadata-protocol/src/protocol.ts#resolveWritePackageScope`); a declared dependency the registry cannot resolve +stays in the closure and the walk stops there — an absent dependency is not a fault at the save door. A view bound outside +the closure is refused `422`, and the refusal's fix line already reads "declare a dependency on the package that defines +the object" (`packages/lint/src/rule-explanations.ts#LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION`). The same text records +the boundary of that judgement: `os validate`, `os build` and `os lint` "judge neither: a stack there is one app, and an +unresolved binding may be a dependency package's object the stack does not carry". What `defineStack` does say about a +reference across packages is measured in ADR-0130 §1.5 and pinned in `packages/spec/src/stack.zod.ts`: a lookup or +master-detail to an object the stack does not carry is admitted; a hook on such an object, and a package's own +`app.navigation` pointing at one, are refused — "navigation crosses only through contributions, and the split must +follow hook ownership". + +**At install, `manifest.dependencies` has exactly one reader, and it orders — it never checks.** The dispatch for this +record assumed nothing at install reads the key; the measurement corrects that by one reader. +`resolveArtifactPackageEntries` (`packages/core/src/artifact-packages.ts#resolveArtifactPackageEntries`) hands every +declared id to `resolvePluginOrder` as an **optional** edge, so the packages of one artifact register in dependency +order (ADR-0130 D5) and a declared id that is not in the artifact "is simply not an edge here" — skipped silently, by +design (its module header says why). No install door asks whether a declared dependency is installed, or at what version. +`dependency_missing` is an enum value of `packages/spec/src/system/app-install.zod.ts#AppCompatibilityCheckSchema`; across +the tree it occurs in that schema, its test, the generated JSON schema and the generated reference page — zero producers. +`packages/core/src/dependency-resolver.ts#SemanticVersionManager` resolves kernel plugins, not installed packages. + +**The namespace gate admits co-owners from one source only, and the uniqueness check follows that set.** +`SchemaRegistry.installPackage` (`packages/objectql/src/registry.ts#installPackage`) builds its co-owner set from the +artifact scope the load path passes (`ArtifactInstallScope.packageIds` — the artifact's own package list, built by the +manifest service in `packages/objectql/src/plugin.ts` and handed through `packages/objectql/src/engine.ts#registerApp`), +and refuses a package whose non-shareable namespace an installed non-co-owner owns — +`packages/objectql/src/registry.ts#NamespaceConflictError`, `422 NAMESPACE_CONFLICT` (downgraded to a warning only under +`OS_METADATA_COLLISION=warn`). Its other half, `packages/objectql/src/registry.ts#refuseCoOwnedObjectNameCollision`, +refuses two co-owners defining one object name (`ArtifactObjectNameConflictError`, `DUPLICATE_ARTIFACT_OBJECT_NAME`), +returns early when the co-owner set is empty, and deliberately skips a sitting owner that is **not** a co-owner — "a +cross-artifact claim, which the namespace gate above owns and refuses". The two halves read one set; whoever joins that set +is judged by both. So today a module published as its own artifact with HotCRM's namespace `crm` is refused beside an +installed HotCRM whatever it declares — the HotCRM seat's input 1, confirmed. **Three schemas spell a dependency, in three shapes** — the alignment D8 records: -| Carrier | Shape | Who reads it at `47af3dde42` | +| Carrier | Shape | Readers at `47af3dde42` | |:---|:---|:---| | `ManifestSchema.dependencies` (`packages/spec/src/kernel/manifest.zod.ts#ManifestSchema`) | record `{ [packageId]: versionRange }`, optional | the save-door closure; the intra-artifact sort; the package door's edit | -| `PackageManifestSchema.dependencies` (`packages/spec/src/marketplace/package-version.zod.ts#PackageManifestSchema`), entries `#PackageDependencySchema` | array of `{ packageId, versionRange, optional = false }`, default `[]` | no runtime reader in this repository | -| `AppManifestSchema.dependencies` (`packages/spec/src/system/app-install.zod.ts#AppManifestSchema`) | array of bare strings, no version, default `[]` | no reader outside `packages/spec` | - -`os package publish` snapshots the compiled `dist/objectstack.json` into `sys_package_version.manifest_json` -(`packages/cli/src/commands/package/publish.ts`, its header steps 1–2), so the snapshot carries the record shape where -`PackageManifestSchema` declares an array. Whether the control plane parses that snapshot against the array schema lives -in `objectstack-ai/cloud` — **not measured** here. - -### Why now, and for whom - -ADR-0130 lets one product carry N packages inside one artifact, installed whole; its D6 records the cost — no per-module -hot-fix — and the escape hatch: a module that must ship on its own cadence becomes its own artifact, at which point D8's -deferral ends. The maintainer's rulings above take that escape hatch for real: a HotCRM module that ships and installs on -its own, beside an installed HotCRM. That needs three things the platform does not have — a check that the depended-on -app is there, a namespace gate that admits the module, and a record of what is deliberately left for later. The -`repo:hotcrm` seat's two module cards frame the sequence: hotcrm#2270 packages revenue and marketing inside the one -artifact now; hotcrm#2271 ships revenue on its own once this check is released (both **not re-verified**, cited from -comment `6370024454`). +| `PackageManifestSchema.dependencies` (`packages/spec/src/marketplace/package-version.zod.ts#PackageManifestSchema`), entries `#PackageDependencySchema` | array of `{ packageId, versionRange, optional = false }`, default `[]` | none outside `packages/spec` in this repository | +| `AppManifestSchema.dependencies` (`packages/spec/src/system/app-install.zod.ts#AppManifestSchema`) | array of bare ids, no range, default `[]` | none outside `packages/spec` | + +`os package publish` snapshots the compiled artifact into `sys_package_version.manifest_json` +(`packages/cli/src/commands/package/publish.ts`, its header's step 2), so a snapshot carries the record shape where +`PackageManifestSchema` declares an array. Whether the control plane parses that snapshot against the array schema lives in +`objectstack-ai/cloud` — **not measured** here. + +**In-repo declaring population**: one manifest — `examples/app-multi-package/src/packages/orders/index.ts` declares +`dependencies: { 'com.example.multi.core': '^1.0.0' }` on the package delivered beside it in the same artifact. HotCRM's +manifests are **not measured** (the seat's input says every one declares `namespace: 'crm'`). + +### 2. Why now, and for whom + +ADR-0130 lets one product carry N packages inside one artifact, installed whole, and D6 records both the gain (no version +matrix) and the cost (no per-module hot-fix) — with one escape hatch: a module that must ship on its own cadence becomes +its own artifact, "at which point it needs the owner field D8 defers, and D8's deferral ends". The maintainer's rulings +take that escape hatch for real — 「如果这样,hotcrm 岂不是真实的可以分拆出很多模块」「而每个包可以独立安装」 — and +without the field: three things are missing for it, and this record supplies the first and third and defers the second: +a check that the depended-on app is there (D2), a namespace gate that admits the module (D5), and a written record of +what is deliberately left for later (D3). The HotCRM seat's two module cards frame the sequence: hotcrm#2270 packages +revenue and marketing inside the one artifact now; hotcrm#2271 ships revenue on its own once the check is released (both +**not re-verified**, cited from comment `6370024454`). + +The maintainer's first line on the card — 「如果按照包依赖的方式,整个开发会复杂很多」 — is why B′ is narrow. Every +mechanism beyond the declaration and one check is a permanent obligation; the card's own fourth axis counted four. This +record takes one and names the others as D3's deferred items, each with the trigger that would justify it. --- @@ -144,184 +178,231 @@ comment `6370024454`). ### D1 — Declaration and reference: no new mechanism An app — any installable package — declares what it depends on in `ManifestSchema.dependencies`, unchanged: -`{ "PACKAGE_ID": "SEMVER_RANGE" }`. A dependency **opens the reference**: the runtime save door judges a package's -writes against the closure already described above, and nothing else decides whether a cross-app reference is legal. - -⛔ No owner-side "published object" flag, no export list, no second reference mechanism. An undeclared cross-app -reference is refused where it is refused today — at the save door, `422` — and the fix it prints is the declaration. -`os validate`, `os build` and `os lint` keep judging neither side of a cross-package binding: a stack there is one app -and cannot see its dependencies' objects. - -### D2 — The pre-install check: installed, in range, or a clear refusal — and only that gate - -**What it judges.** For an install of package P, every entry `(id, range)` of `P.manifest.dependencies` is satisfied -when both hold: - -1. **Present.** A package with that id is installed in the target environment, or is delivered by the **same artifact** - as P. The second arm is what keeps every ADR-0130 artifact installable: inside one artifact the dependency is - installed in the same act (D1 of 0130) and registered first (D5 of 0130), so the check must not refuse a dependency - the act itself delivers. -2. **In range.** The present package's version satisfies `range`. A co-delivered package's version is the artifact's - one version (ADR-0130 D6). A version or range the parser cannot read does **not** satisfy — the refusal says which - side was unreadable. Fail closed: an unread bound is not a bound. - -Every manifest entry is required: the record shape has no optional arm (D8 states the marketplace shape's `optional` -flag and how it projects). - -**What it answers.** An unsatisfied entry refuses the install, before anything is registered or written, with the -ADR-0112 envelope and one `AppCompatibilityCheck` issue per unsatisfied entry — `severity: 'error'`, `category: -'dependency_missing'`, a `message` naming the dependency, the declared range, and what was found (absent, or installed at -version V). All unsatisfied entries are reported in one answer, never the first alone. The Console renders the refusal -in the author's terms (「请先安装 HotCRM 6.x」). The error code and HTTP status are the runtime card's to choose from the -ADR-0112 ledger; the category is fixed here. - -**Where it runs.** At the **install act** — the doors that already run the ADR-0087 D1 protocol handshake before they -register anything: `POST /api/v1/packages` on both of its arms (the protocol primitive `installPackage` in -`protocol-package-install.ts`, and the no-protocol fallback in `domains/packages.ts`), and -`POST /api/v1/marketplace/install-local`. One predicate serves every door; ⛔ no door carries its own copy, and the range -grammar is read by an existing parser, ⛔ never a second semver implementation. - -**Where it does not run.** Re-registration of an already-installed set is not an install act: boot registration of -code-composed packages, the `sys_packages` rehydrate, and the cached marketplace-manifest rehydrate. A boot fills the -registry incrementally, so "dependency X is not installed" concluded there can be contradicted a moment later by X -registering — the verdict AGENTS.md's *Startup registry reads* section names as a defect. What an installed set already -admitted stays admitted; a dependent whose dependency has since gone is D3's uninstall-protection item, not a boot -refusal. +`{ "PACKAGE_ID": "SEMVER_RANGE" }`. **A dependency opens the reference.** What a dependent may reference is what the +platform already admits for a declared package: the runtime save door judges a write against the transitive closure +described in Context §1, and `defineStack` keeps admitting a lookup or master-detail to an object the stack does not +carry while refusing a hook on it and a package's own navigation to it (ADR-0130 §1.5). Nothing else decides whether a +cross-app reference is legal. + +⛔ No owner-side "published object" flag, no export list, no second reference mechanism (the ruling's own words: the +closure "already honours the declared packages' objects"). An undeclared cross-app reference is refused where it is +refused today — at the save door, `422` — and the fix it prints is the declaration. `os validate`, `os build` and +`os lint` keep judging neither side of a cross-package binding: a stack there is one app and cannot see its dependencies' +objects. + +### D2 — The pre-install check: installed and in range, or a clear refusal — and only that gate + +**What it judges.** For an install that delivers package P, every entry `(id, range)` of `P.manifest.dependencies` is +satisfied when both hold: + +1. **Present.** The registry holds an installed package record for `id` (`SchemaRegistry.getPackage`), or `id` names a + package delivered by the **same artifact** as P. The second arm is what keeps every ADR-0130 artifact installable: + the check judges the whole artifact before any of it registers (below), so a dependency the same act delivers is not + yet "installed" at that moment, and refusing it would refuse `examples/app-multi-package` and every product split under + ADR-0130. It mirrors the one reader the key already has at install: inside the artifact a declared id is an edge, + outside it is a question for this check. +2. **In range.** The installed record's `manifest.version` — or, for a co-delivered package, that package's declared + version (one per artifact under ADR-0130 D6) — satisfies `range`, read in the npm semver range grammar + `ManifestSchema.dependencies` already documents and `engines.protocol` already uses. A version or range the reader + cannot parse does **not** satisfy; the refusal says which side was unreadable. Fail closed: an unread bound is not a + bound. + +Every record entry is required: the record shape has no optional arm (D8 says how the marketplace shape's `optional` +projects). Only P's **own** entries are judged — a dependency's dependencies were judged when it was installed. +Enablement (`InstalledPackageSchema.enabled`) is not judged: presence and version are the declared facts; what a disabled +dependency means for a dependent is an open item for the card (Open items 4). + +**What it answers.** One unsatisfied entry refuses the **whole install** — nothing is registered, written or persisted — +with the ADR-0112 envelope: `status` **422**, the status the sibling refusal at the same gate (`NAMESPACE_CONFLICT`) +answers for an environment-state conflict; a `code` the card registers in the ADR-0112 D3 ledger; and one +`AppCompatibilityCheck` issue per unsatisfied entry — `severity: 'error'`, `category: 'dependency_missing'`, and a +`message` naming the dependency, the declared range and what was found (absent, or installed at version V). Every +unsatisfied entry is reported in that one answer, never the first alone. The Console renders it in the author's terms +(「请先安装 HotCRM 6.x」); the text is the runtime card's, with the refusal's facts fixed here. + +**Where it runs — the install acts.** Three doors install a package into an environment, and each already refuses before +it registers anything (ADR-0087 D1's protocol handshake, `packages/metadata-core/src/protocol-handshake.ts#assertProtocolCompat`, +sits at each). The check sits beside that handshake: + +- the protocol primitive `packages/metadata-protocol/src/protocol-package-install.ts#installPackage`, which + `POST /api/v1/packages` calls when the protocol service resolves and which `duplicatePackage` and the base-package + path call in-process; +- the bare-registry fallback of `POST /api/v1/packages` when no protocol service resolves (`packages/runtime/src/domains/packages.ts`); +- `POST /api/v1/marketplace/install-local` (`packages/cloud-connection/src/marketplace-install-local-plugin.ts`), before it + hands the artifact to the manifest service — the same placement the picklist and bound-action seals already use in + `packages/objectql/src/plugin.ts`: "a refused install registers nothing". + +One predicate serves every door; ⛔ no door carries its own copy. The range is read by one of the readers the tree already +has (`packages/core/src/dependency-resolver.ts#SemanticVersionManager`, or +`packages/metadata-core/src/artifact-forward-conversion.ts#parseRangeFloor` and its siblings), extended if it must be — +⛔ never a third semver implementation. + +**Where it does not run — re-registration.** A boot re-registers what an environment already holds: code-composed +packages through the manifest service at `init`/`start`, the `sys_packages` rehydrate (`packages/services/service-package/src/index.ts`), +and the install-local plugin's cached-manifest rehydrate on `kernel:ready`. None is an install act, and a boot fills the +registry incrementally — a code package rehydrated at `start` would read a marketplace dependency that arrives at +`kernel:ready` as absent. Concluding "dependency X is not installed" there and refusing on it is the verdict AGENTS.md's +*Startup registry reads* section names as a defect. What an install admitted stays admitted; a dependent whose dependency +has since gone is D3's uninstall-protection item, not a boot refusal. ### D3 — Deferred, by name, not implemented now -Each item below is a decision **not** to build yet, recorded so the next author finds a reason instead of an absence. -None is rejected; each has a trigger. +Each item is a decision **not** to build yet, recorded so the next author finds a reason instead of an absence. None is +rejected; each has a trigger. Meanwhile the ADR-0029 refusal stands unchanged (a package whose object another package +`extend`s cannot be uninstalled — `packages/objectql/src/registry.ts#uninstallPackage` through `unregisterObjectsByPackage`), +and it is the only uninstall refusal there is. | Deferred item | What happens meanwhile | Trigger to build it | |:---|:---|:---| -| **Uninstall protection** — refusing to uninstall a package another installed package depends on | The uninstall proceeds. The dependent's references to the removed objects dangle; the existing ADR-0029 refusal (a package whose object another package `extend`s cannot be uninstalled) still holds, unchanged | A first customer uninstall that strands a dependent | -| **Upgrade interception** — refusing an upgrade of X that would leave a dependent's declared range | The upgrade proceeds; the dependent's range is not re-judged | A first dependent broken by an upgrade of what it depends on | -| **Topological / cascade install** — installing a set in dependency order, or pulling a missing dependency in automatically | The administrator installs dependencies first; D2's refusal says which. Inside one artifact, ADR-0130 D5's sort is unchanged | More than one level of standalone modules in a real product | -| **Owner-side publish marker** — X restricting which of its objects a dependent may reference | Every object of X is referenceable by a declared dependent, subject to D4 | A third-party dependent on a first-party app, i.e. marketplace publishing (D5's end point for ADR-0130 D8) | -| **Marketplace version negotiation** — choosing versions of X and P that satisfy each other | None; D2 judges the installed version against the declared range, once | Marketplace listings carrying more than one installable version of a depended-on app | +| **Uninstall protection** — refusing to uninstall a package another installed package depends on | The uninstall proceeds; the dependent's lookups dangle. Only ADR-0029's extender refusal stands | A first customer uninstall that strands a dependent | +| **Upgrade interception** — refusing an install-overwrite of X that would leave a dependent's declared range | The upgrade proceeds; no dependent's range is re-judged (same-package reinstall is exempt at the gate today) | A first dependent broken by an upgrade of what it depends on | +| **Topological / cascade install** — installing a set across artifacts in dependency order, or pulling a missing dependency in | The administrator installs dependencies first; D2's refusal names which. Inside one artifact ADR-0130 D5's sort is unchanged | More than one level of standalone modules in a real product | +| **Owner-side publish marker** — X restricting which of its objects a dependent may reference | Every object of X is referenceable by a declared dependent, under D4 | A third-party dependent on a first-party app — marketplace publishing, D5's end point for ADR-0130 D8 | +| **Marketplace version negotiation** — choosing versions of X and P that satisfy each other | None; D2 judges the installed version against the declared range, once | Listings carrying more than one installable version of a depended-on app | ### D4 — Permissions unchanged; record-level semantics are an open section A dependency opens the **reference**, not the **data**. A referencing app's users see the referenced app's records under the referenced app's own permission sets and sharing rules; a lookup from P to an object of X confers no read on X's -records. Each package ships its own permission sets (ADR-0086 D4), and nothing composes into another package's set — the -same answer #14488's closing ruling gives a module carved out as its own artifact (D6 below). - -**Open section — decided with the first consumer, not here.** How a dependent's lookup to X's object behaves at record -level: what a user who may read P's record but not the referenced record of X sees in the lookup (the referenced id, a -redacted label, or nothing), and whether a dependent's list, report or search may filter through X's records it cannot -read. The answer is measured on HotCRM's revenue module against the existing record-access path, before the runtime card -for it is cut. +records. Each package ships its own permission sets (ADR-0086 D4) and nothing composes into another package's set — the +answer #14488's closing ruling already gives a module carved out as its own artifact (D6 below). + +**Open section — decided with the first consumer, not here.** Three questions, measured on HotCRM's revenue module +against the existing record-access path before its runtime card is cut: + +1. What a user who may read P's record but not the referenced record of X sees in the lookup — the referenced id, a + redacted label, or nothing — and whether P's lists, reports and search may filter through X's records the user cannot + read. +2. Whether a dependent's **own** permission set may grant on a dependency's objects. ADR-0086 D4's conflict-freedom rests + on "each set only grants `objects._*` keys" — one namespace per package; D5 lets two artifacts share a + namespace, so that discriminator is gone across artifacts as ADR-0130's 2026-09-02 addendum already found it gone + inside one. Nothing in this repository was measured to refuse a package's set naming another package's object; the + card measures before it decides. +3. Whether anything at install or boot must change for the referenced app's sharing rules to apply to a dependent's + lookups as they apply to its own — the ruling's "a dependency opens the reference, not the data" is the invariant; the + card finds the seam. ### D5 — A declared dependency is namespace co-ownership, one-way -At the install gate, package P's co-owner set gains every installed package X such that P declares X in -`manifest.dependencies` **and** P's namespace equals X's non-shareable namespace. Nothing else changes: +**The relation.** Installed packages P and X are *dependency-declared co-owners* of namespace N when P declares X in +`manifest.dependencies`, N is P's namespace, N is X's namespace, and N is not a shareable platform namespace +(`packages/objectql/src/registry.ts#isShareableNamespace`). At the install gate, this relation is a second source of the +co-owner set ADR-0130 D1 introduced, beside the artifact scope. Nothing else changes: - **One-way.** Only the dependent declares. X declares no dependency on its modules and gains nothing on P's side; P - gains admission to the shared namespace and nothing on X's side. + gains admission to the shared namespace and nothing on X's side. The in-tenant trust behind the admission is the + administrator's choice to install P beside X — the same trust that admits any package into an environment. - **The D3 pair holds across artifacts.** ADR-0130 D3 made the namespace relaxation and the per-object-name uniqueness - check one change; the second half now spans these co-owners too. A dependent defining an object name X already owns - is refused at install, ahead of any DDL, with an error naming both packages and the object — the refusal - `refuseCoOwnedObjectNameCollision` already raises for co-owners inside one artifact. -- **No new manifest field.** The co-ownership proof is the declaration plus the administrator's install act. Its in- - tenant trust is that administrator's choice to install P beside X — the same trust that admits any package. -- **Direct declarations only.** A package sharing a namespace with several installed packages declares each one; the - `NamespaceConflictError` it meets otherwise names the owner it did not declare. -- **Recognised whichever of the two registers first.** The relation is declared by P alone, but the gate honours it on - both registrations: when X registers and an existing owner of X's namespace is a package that declares X, that owner - is X's co-owner for the namespace gate and for the uniqueness check alike. This is recognition, not a right — X still - declares nothing — and it is required because registration order at boot is not install order: a rehydrate that - registers P before X would otherwise refuse the depended-on app itself. Whether today's rehydrate order can put P - first is the runtime card's first reading. + check one change, and the registry reads one co-owner set for both halves; a co-owner by declaration is therefore + judged by both. A dependent defining an object name X already owns is refused at install, ahead of any DDL, with the + error `refuseCoOwnedObjectNameCollision` already raises for co-owners inside one artifact — `code` and `status` + asserted, as ADR-0130 D3's gate test requires. +- **No new manifest field.** The proof is the declaration plus the install act. ADR-0130 D8 stands: no owner or publisher + field on `ManifestSchema`. +- **Direct declarations only.** The relation reads P's own `dependencies`, not the transitive closure the save door + walks. A package sharing a namespace with several installed packages declares each one; the `NamespaceConflictError` it + meets otherwise names the owner it did not declare, and the fix is the declaration. Narrower is the reversible + direction. +- **Read from either end.** The relation is declared by P alone, and the gate reads it whichever of the pair arrives + second. On P's registration the sitting owner X is P's co-owner because P declares X; on X's registration a sitting + owner that declares X is X's co-owner for the namespace gate and the uniqueness check alike. This is recognition of one + declared fact, not a right X acquires — X still declares nothing — and it is required because registration order at + boot is not install order: a rehydrate that registers P before X would otherwise refuse the depended-on app itself. + Whether today's rehydrate paths can put P first is the runtime card's first reading (Open items 2). **ADR-0130 D8's deferral now ends at marketplace publishing**, not at a module's first standalone artifact. Within one tenant, D5 proves co-ownership without an owner field. Across vendors — the moment a module is published to the marketplace and must assert "I have the same publisher as HotCRM" — the owner / publisher field lands, aligned to the -`publisherOrgId` reservation of ADR-0048's 2026-08-08 addendum (still Proposed). That field is the later superset of D5 -(letter B of ruling `6377678758`) and needs no undo of it. Renaming a module's objects to dodge the gate stays refused -(ADR-0129; letter C of the same ruling). +`publisherOrgId` reservation of ADR-0048's 2026-08-08 addendum (still Proposed; its A.5 precondition is the opening of +third-party publishing). That field is the later superset of D5 (letter B of ruling `6377678758`) and needs no undo of +it. Renaming a module's objects to dodge the gate stays refused (ADR-0129; letter C of the same ruling, and ADR-0048 +§3.5's standing non-goal). ### D6 — What ADR-0028, ADR-0019 and ADR-0130 now say -- **ADR-0028 D4** (Deferred, proposed 2026-06-01) proposed full isolation between apps — "may not reference metadata - owned by another app". Its sentence is rewritten to **"no cross-app reference without a declared dependency"**: an app - may reference its own metadata, the kernel, and the metadata of the apps its manifest declares in `dependencies`; - every other cross-app reference is refused. The isolation D4 wanted between marketplace apps is kept by the closed - set — an app reaches only what it declared, and the save door already refuses the rest. The rest of ADR-0028 stays - Deferred and untouched; D4 carries a note pointing here. -- **ADR-0019** §Out of scope keeps its text verbatim. Its header gains one additive `Related` line naming this record as - the gated decision its "Shared/published data model + inter-App dependencies" bullet asked for, narrowed to the - dependency half. The shared data model half (a platform-owned customer/supplier model, letter C on #25403) was not - chosen and stays out of scope. -- **ADR-0130** gains an addendum (no original text edited) recording two things: D8's deferral now ends at marketplace - publishing (D5 above), and the 2026-09-02 addendum's *What was NOT decided* line listing - [#14488](https://github.com/objectstack-ai/objectstack/issues/14488) as "filed, not decided" is stale — #14488 was closed +- **ADR-0028 D4** (Deferred; proposed 2026-06-01) proposed full isolation: *"Apps are sandboxed: no cross-app + references"* — an app *"may not reference metadata owned by another app"*. Its sentence is rewritten to **"No cross-app + reference without a declared dependency"**: an app reaches its own metadata, the kernel, and the metadata of the apps + its manifest declares in `dependencies`; every other cross-app reference is refused. The isolation D4 wanted between + marketplace apps is kept by the closed set. The three sentences in 0028 that restate D4 (TL;DR item 4, D6's author-time + bullet, leak-point 3) follow it; the rest of ADR-0028 stays Deferred and untouched, and D4 carries a note quoting its + original sentence. +- **ADR-0019** keeps its text verbatim. Its header gains one additive `Related` line naming this record as the gated + decision its "Shared/published data model + inter-App dependencies" bullet asked for, taken for the dependency half. + The shared data model half (a platform-owned customer / supplier model — letter C on #25403) was not chosen and stays + out of scope. +- **ADR-0130** gains an addendum (no original text edited) recording two things. First, D8's deferral now ends at + marketplace publishing (D5 above). Second, the 2026-09-02 addendum's *What was NOT decided* line listing + [#14488](https://github.com/objectstack-ai/objectstack/issues/14488) as "filed, not decided" is stale: #14488 was closed `not_planned` on 2026-09-12 by ruling - [`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259): an artifact - installs whole, nothing composes grants into another package's set, and a module carved out as its own artifact ships - its own permission sets under ADR-0086 D4. A standalone module's permission sets are therefore the module author's - work, not a platform blocker. + [`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259) — an artifact installs + whole, the app package's sets cover every module's objects, nothing composes grants into another package's set, and a + module carved out as its own artifact ships its own permission sets under ADR-0086 D4; the card is not reopened for + that case. That ruling's premise sentence — 「目前的要求是发布物是完整安装的,没有设计单独安装其中一个模块的需求」 — is + the one the maintainer moved on 2026-10-23 (「而每个包可以独立安装」); its disposition is unchanged and is what D4 above + relies on: a standalone module's permission sets are the module author's work, not a platform blocker. ### D7 — The first consumer: a HotCRM module shipping on its own, revenue first The scenario this record is accepted against is HotCRM's module split as its `docs/architecture/module-split-plan.md` -records it (the 2026-09-14 shape: the sales app plus service, revenue and marketing modules; **not re-verified**): the -revenue module ships as its own artifact, declares `dependencies: { HOTCRM_SALES_PACKAGE_ID: "HOTCRM_MAJOR_RANGE" }`, keeps -`namespace: 'crm'` and its `crm_*` object names, and installs beside an installed HotCRM. Installing it on an environment -without HotCRM, or with HotCRM outside the declared range, is refused by D2 with the message naming HotCRM and the range. +records it — the 2026-09-14 shape: the sales app plus service, revenue and marketing modules (**not re-verified**). The +revenue module ships as its own artifact, declares `dependencies: { "HOTCRM_SALES_PACKAGE_ID": "HOTCRM_MAJOR_RANGE" }`, +keeps `namespace: 'crm'` and its `crm_*` object names, and installs beside an installed HotCRM. On an environment +without HotCRM, or with HotCRM outside the declared range, D2 refuses it naming HotCRM and the range; beside HotCRM, D5 +admits it to `crm` and the uniqueness check refuses any `crm_*` name HotCRM already owns. -Until the runtime card ships, composite products keep shipping as one artifact (ADR-0130). That is the state hotcrm#2270 -packages revenue and marketing in; hotcrm#2271 moves revenue to its own artifact after the check is released. +Until the runtime card ships, composite products keep shipping as one artifact under ADR-0130. That is the state +hotcrm#2270 packages revenue and marketing in; hotcrm#2271 moves revenue to its own artifact once the check is released. ### D8 — The dependency shapes, and the alignment the runtime card owes The authored source of truth is the **manifest record**, `ManifestSchema.dependencies`. D2 reads it, the save-door closure reads it, D5 reads it. ⛔ No spec shape changes in this record. -The marketplace snapshot's array, `PackageManifestSchema.dependencies` of `PackageDependencySchema`, is a **projection** of -that record, one entry per key: `packageId` = the key, `versionRange` = the value, `optional` = `false` (the record has -no optional arm, and D2 treats every entry as required). The runtime card owes: +The marketplace snapshot's array — `PackageManifestSchema.dependencies` of `PackageDependencySchema` — is a **projection** +of that record, one entry per key: `packageId` = the key, `versionRange` = the value, `optional` = `false` (the record +has no optional arm, and D2 treats every entry as required). The runtime card owes: -1. **One conversion** from record to array, used by every writer of a snapshot, so the snapshot conforms to its declared - schema instead of carrying the record shape it carries today (`publish.ts` writes the compiled manifest as-is). Where - the control plane reads `manifest_json` is in `objectstack-ai/cloud`; the card states what it found there. +1. **One conversion** from record to array, used by every writer of a snapshot, so a snapshot conforms to its declared + schema instead of carrying the record shape it carries today (`publish.ts` uploads the compiled artifact as it is). + Where the control plane reads `manifest_json` is in `objectstack-ai/cloud`; the card states what it found there. 2. **A stated fate for `optional`.** An `optional: true` entry has no record spelling. Until one is decided, a snapshot - never produces it, and D2 does not read it. + never produces it and D2 never reads it. 3. **A stated fate for `AppManifestSchema.dependencies`** (bare ids, no range, no reader outside `packages/spec`): either - it projects from the record (ids only, losing the range) or it is retired through the ADR-0087 conversion layer. The - card decides which with a measured reader count; this record decides neither. + it projects from the record (ids only, losing the range) or it retires through the ADR-0087 conversion layer. The card + decides with a measured reader count; this record decides neither. -The resolver-side `ResolvedPackageDependencySchema` (`kernel/plugin-security.zod.ts`) is unaffected — it is the -resolution outcome, not a declaration, and its own header already tells the two apart. +The resolver-side `ResolvedPackageDependencySchema` (`@objectstack/spec/kernel`) is unaffected: it is a resolution outcome, +not a declaration, and `PackageDependencySchema`'s own header already tells the two apart. --- ## Consequences **Positive** -- A customer can install CRM now and PSA, or a HotCRM module, later — without the module having been shipped to every - CRM customer and without swapping to a combined app. 「每个包可以独立安装」. -- One rule replaces several special cases: inside an artifact (ADR-0130), at the save door (the closure), and across apps +- A customer installs CRM now and PSA — or a HotCRM module — later, without the module having been shipped to every CRM + customer and without swapping to a combined app: 「每个包可以独立安装」. +- One rule replaces several special cases. Inside an artifact (ADR-0130), at the save door (the closure), and across apps (this record), a cross-package reference is legal exactly when it is declared. -- An AI author's undeclared reference stays a loud `422` at the save door, and a missing dependency is a loud refusal at - install, instead of copied objects that drift silently. -- The work is small: no new authorable key, no schema change, one predicate at the install doors, one more source for the - co-owner set. +- An AI author's undeclared reference stays a loud `422` at the save door, and a missing dependency becomes a loud refusal + at install, instead of copied objects that drift silently. +- The work is small and stays small: no new authorable key, no schema change, one predicate at three doors, one more + source for one co-owner set. **Negative / costs, stated plainly** - Uninstalling or upgrading a depended-on app is not guarded (D3). An administrator can strand a dependent until those - items are built; the refusal at install is the only gate. -- A standalone module and its app now have **separate versions**, which ADR-0130 D6 avoided inside one artifact: the - version matrix it removed comes back across artifacts, bounded only by the declared range. -- D5 lets a module share a vendor's namespace on the administrator's say-so. Inside one tenant that is the install - choice the administrator already makes; across vendors it is not enough, which is why D8's deferral now ends at - marketplace publishing. - -**Unchanged** — the consumer model (ADR-0019 D1–D4: one app installs, opens, uninstalls), the artifact as a -control-plane noun (ADR-0130 D2), the one sorter (ADR-0130 D5), permission sets (ADR-0086 D4), and the save door's -closure rules. + items are built; the refusal at install is the only gate, and the refusal's message is the only guidance. +- A standalone module and its app have **separate versions** — the matrix ADR-0130 D6 removed inside one artifact + returns across artifacts, bounded only by the declared range and judged only at install. +- D5 lets a module share a vendor's namespace on the administrator's say-so. Inside one tenant that is the install choice + the administrator already makes; across vendors it is not enough, which is why D8's deferral now ends at marketplace + publishing. +- D4's open section is real work before the first consumer ships: the permission-set discriminator ADR-0086 D4 relied on + is gone across artifacts. + +**Unchanged** — the consumer model (ADR-0019 D1–D4: one app installs, opens, uninstalls), the artifact as a control-plane +noun (ADR-0130 D2), the one sorter (ADR-0130 D5), one version per artifact (ADR-0130 D6), permission sets (ADR-0086 D4), +the save door's closure rules, and the compile-time boundary (`os validate` judges neither side). --- @@ -333,73 +414,83 @@ The letters as filed on #25403 and on ruling `6377678758`: either pre-installed on everyone or a swap to a bigger bundle, and cross-app master data travels by import and sync, which drifts silently. 2. **The full inter-app dependency (#25403 letter B as filed).** Install ordering, uninstall protection, version - compatibility, cross-app permission semantics and Console dependency views at once. Narrowed by the maintainer to - B′: the declaration and one check now, the rest as D3's deferred items. + compatibility, cross-app permission semantics and Console dependency views at once. Narrowed by the maintainer to B′: + 「整个开发会复杂很多」 — the declaration and one check now, the rest as D3's deferred items. 3. **A platform-owned shared master data model (#25403 letter C).** Not chosen: the platform would own a business data model and every existing app's objects would migrate. Under this record a "master data app" is an ordinary app others depend on. 4. **An owner-side "published objects" list (the director's variant D).** Not chosen: 「往往需要引用的数据并不多,可能就几个对象」 — the declaration is enough; restricting what a dependent may reference is D3's publish-marker item. -5. **For the namespace — an owner / publisher field now (ruling `6377678758` letter B).** Not chosen now: it is the - later superset of D5, aligned to `publisherOrgId`, and lands at marketplace publishing without undoing D5. +5. **For the namespace — an owner / publisher field now (ruling `6377678758` letter B).** Not chosen now: the later + superset of D5, aligned to `publisherOrgId`, landing at marketplace publishing without undoing D5. 6. **For the namespace — rename the module's objects (letter C).** Refused: ADR-0129 makes the object name the canonical - id, and a rename is the deep rewrite ADR-0130 exists to avoid. + id, and a rename is the deep rewrite ADR-0130 exists to avoid (ADR-0048 §3.5). --- ## Open items (not blocking acceptance; each becomes a reading on its card) -1. **Record-level semantics across a dependency** — D4's open section, measured on HotCRM's revenue module. -2. **Rehydrate order** — whether any boot path can register a dependent before the package it depends on (D5's - recognition rule makes the answer harmless; the card records it). -3. **Declared ids that are not installable packages.** `ManifestSchema.dependencies`' own documentation example names an - npm-style id (`@steedos/plugin-auth`). Measured population in this repository: one declaring manifest, - `examples/app-multi-package/src/packages/orders/index.ts`, whose dependency is delivered by the same artifact (D2's - second arm). HotCRM's manifests are **not measured**. If an id that names no installable package turns up, the card - reports it; D2 does not exempt it. -4. **`optional` and `AppManifestSchema.dependencies`** — D8 items 2 and 3. +1. **Record-level semantics across a dependency** — D4's three questions, measured on HotCRM's revenue module. +2. **Rehydrate order** — whether any boot path (`sys_packages`, the install-local ledger, code packages) can register a + dependent before the package it depends on. D5's read-from-either-end rule makes the answer harmless; the card records + it. +3. **Declared ids that are not installable packages.** The key's own documentation example is an npm-style id. The + in-repo population is one manifest, co-delivered (Context §1). If an id that names no installable package turns up in + HotCRM's manifests, the card reports it; D2 does not exempt it. +4. **A disabled dependency.** D2 judges presence and version. Whether `enabled: false` on X should satisfy, refuse, or + warn for a dependent is measured against what the save door's closure and the data door do with a disabled package + today. +5. **`optional` and `AppManifestSchema.dependencies`** — D8 items 2 and 3. ## Open questions for the maintainer's merge decision -These are the points this draft decided where the rulings were silent, listed so the merge is an informed act: - -1. **D2's second arm** — a dependency delivered by the same artifact counts as present. Without it every multi-package - artifact whose modules declare their app (`examples/app-multi-package` today) would refuse itself. -2. **D2's placement** — install acts only, not boot re-registration. Checking at boot would be a startup-registry verdict - (AGENTS.md); the cost is that an environment whose dependency was removed outside an install act boots without - complaint (D3's uninstall item). -3. **D5's recognition rule** — the gate honours P's declaration on X's registration too. The ruling's "one-way" is kept - for declaration and rights; recognition from X's side is what keeps a rehydrate from refusing HotCRM itself. -4. **D5's direct-only scope** — co-ownership follows direct declarations, not the transitive closure the save door walks. +These are the points this record decided where the rulings were silent, listed so the merge is an informed act. Merging +accepts them; an objection names the number. + +1. **D2's second arm** — a dependency delivered by the same artifact counts as present. Without it, judging the whole + artifact before anything registers would refuse every ADR-0130 artifact whose modules declare their app; judging per + package instead would leave a half-registered artifact behind on refusal. +2. **D2's placement** — the three install acts only, never boot re-registration. The cost: an environment whose + dependency was removed outside an install act boots without complaint (D3's uninstall item). +3. **D5 read from either end** — the gate honours P's declaration on X's registration too. "One-way" is kept for who + declares and who gains; recognition from X's side is what keeps a rehydrate from refusing HotCRM itself. +4. **D5 direct-only** — co-ownership follows P's own declarations, not the transitive closure the save door walks. Narrower is the reversible direction; widening later needs no undo. --- ## Verification notes -- **The dispatch's assumption "nothing at install reads `manifest.dependencies`"** holds for checking and fails for - ordering: `resolveArtifactPackageEntries` reads every declared id as an optional sort edge (Context above). It changes no decision — - the sort skips an absent id silently, which is exactly why D2 is needed — but the record states the reader. -- **The namespace refusal** is `NamespaceConflictError` in `packages/objectql/src/registry.ts`, as assumed. The line - comment `6370024454` cites (line 5942) and ruling `6377678758`'s range (`~5929–5942`) have moved: at `47af3dde42` the - gate is `5989–6005`, the throw `6003`, the co-owner set `5974`. -- **"`revenue` first per ADR-0130 §1.3(c)"** (ruling `6369704387`): §1.3(c) names CPQ as the unsellable module, not - revenue. Revenue first is HotCRM's own ordering (module-split plan, hotcrm#2271); §1.3(c) is the general consequence - both serve. No decision moves. -- **ADR numbering.** `0136` and `0140` were taken by proposals that never landed (#18480; #25618, withdrawn - 2026-10-24) and are not reused; no open pull request carries an ADR at `47af3dde42`. This record is `0141`. +- **"Nothing at install reads `manifest.dependencies`"** (the card's premise 6 and the dispatch) holds for checking and + fails for ordering: `resolveArtifactPackageEntries` reads every declared id as an optional sort edge (Context §1). No + decision moves — the sort skips an absent id silently, which is exactly why D2 is needed — but the record states the + reader. +- **"The namespace refusal is `NamespaceConflictError` in `packages/objectql/src/registry.ts`"** holds. The line numbers + comment `6370024454` and ruling `6377678758` cite have moved since they were written; this record cites the symbols + (`installPackage`, `NamespaceConflictError`, `refuseCoOwnedObjectNameCollision`), which the anchor gate resolves. +- **"`revenue` first per ADR-0130 §1.3(c)"** (ruling `6369704387`): §1.3(c) names CPQ as the module with no sellable + unit; it does not order HotCRM's modules. Revenue first is HotCRM's own ordering (module-split plan, hotcrm#2271); + §1.3(c) is the general consequence both serve. No decision moves. +- **#14488.** Closed `not_planned` 2026-09-12 by ruling `5645322259`, as the HotCRM seat's input 2 says. Its premise + sentence has since been moved by the maintainer's B′ ruling; its disposition stands (D6). +- **ADR numbering.** `0136` and `0140` were taken by proposals that never landed — PR #18480 (closed 2026-09-18, + superseded by the North Star) and PR #25618 (withdrawn 2026-10-24 by the maintainer's ruling in session) — and are not + reused; no landed record or open pull request carries either number, and no open pull request carries a new record. + This record is `0141`. --- ## Execution plan (after acceptance) -1. **Runtime card (XS–S, `domain:services` / objectql), one PR**: D2's predicate at the three install doors with the - `dependency_missing` issues and the Console refusal text; D5's dependency source for the co-owner set, recognised on - both registrations, with the ADR-0130 D3 gate test extended to a cross-artifact pair (a dependent defining an object - name X owns is refused, asserting `code` and `status`); D8's record-to-array conversion. Its first readings: open - items 2 and 3. -2. **Docs**: the package-authoring guide states how to declare a dependency and what the install refusal means. -3. **HotCRM** (its own lane): hotcrm#2271 ships revenue as its own artifact once (1) is released. +1. **Runtime card (XS–S, `domain:services` / objectql), one PR**: D2's predicate at the three install doors, with the + `dependency_missing` issues, the ledger-registered code and the Console refusal text; D5's dependency source for the + co-owner set, read from either end, with the ADR-0130 D3 gate test extended to a cross-artifact pair (a dependent + defining an object name X owns is refused, asserting `code` and `status`); D8's record-to-array conversion. Its first + readings: Open items 2, 3 and 4. +2. **Docs**: the package-authoring guide states how to declare a dependency, what the install refusal means, and that + `os validate` cannot see a dependency's objects. +3. **HotCRM** (its own lane): the D4 readings on the revenue module; hotcrm#2271 ships revenue as its own artifact once + (1) is released. ## References @@ -409,10 +500,15 @@ These are the points this draft decided where the rulings were silent, listed so - ADR-0019, ADR-0028, ADR-0029, ADR-0048 (and its 2026-08-08 addendum), ADR-0086, ADR-0087, ADR-0112, ADR-0116, ADR-0129, ADR-0130 - `packages/spec/src/kernel/manifest.zod.ts`, `packages/spec/src/system/app-install.zod.ts`, - `packages/spec/src/marketplace/package-version.zod.ts` -- `packages/objectql/src/registry.ts` (`installPackage`, `refuseCoOwnedObjectNameCollision`, `NamespaceConflictError`) + `packages/spec/src/marketplace/package-version.zod.ts`, `packages/spec/src/kernel/package-registry.zod.ts` +- `packages/objectql/src/registry.ts` (`installPackage`, `refuseCoOwnedObjectNameCollision`, `NamespaceConflictError`, + `uninstallPackage`), `packages/objectql/src/engine.ts` (`registerApp`), `packages/objectql/src/plugin.ts` - `packages/metadata-protocol/src/protocol.ts` (`resolveWritePackageScope`), - `packages/metadata-protocol/src/protocol-package-install.ts` -- `packages/core/src/artifact-packages.ts`, `packages/lint/src/rule-explanations.ts` - (`list-view-object-outside-closure`) + `packages/metadata-protocol/src/protocol-package-install.ts`, `packages/metadata-core/src/protocol-handshake.ts` +- `packages/core/src/artifact-packages.ts`, `packages/core/src/plugin-order.ts`, + `packages/core/src/dependency-resolver.ts`, `packages/metadata-core/src/artifact-forward-conversion.ts` +- `packages/runtime/src/domains/packages.ts`, `packages/cloud-connection/src/marketplace-install-local-plugin.ts`, + `packages/services/service-package/src/index.ts`, `packages/lint/src/rule-explanations.ts` + (`list-view-object-outside-closure`), `packages/spec/src/stack.zod.ts` +- `examples/app-multi-package/src/packages/orders/index.ts` - hotcrm `docs/architecture/module-split-plan.md`, hotcrm#2270, hotcrm#2271 (not re-verified) From fa7338085423ab04a9fefea4a4fac98e7ed0b79e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 02:28:48 +0000 Subject: [PATCH 4/7] =?UTF-8?q?docs(adr):=20ADR-0141=20patch=20round=202?= =?UTF-8?q?=20=E2=80=94=20ruling=20quote=20byte-exact,=20dependency=5Fmiss?= =?UTF-8?q?ing=20population,=20uninstall-refusal=20wording,=20D2=20tense?= =?UTF-8?q?=20in=20ADR-0028,=20merge=20questions=205=E2=80=9310?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract review PASS at 247fe2fa named five nits; all five land here. The quotation from ruling 5645322259 is copied from the comment body (interior space and full stop restored) in ADR-0141 D6 and ADR-0130's addendum; the dependency_missing population names the three tracked files only; D3's preamble says what the ADR-0029 refusal is the only instance of; ADR-0028 D4 says D2 decides, not refuses, since D2 is unbuilt; the merge-decision list carries the drafter's six further choices (422, fail-closed parse vs the handshake's warn-and-admit, required entries, own entries only, the D8 conversion's sizing, the 5645322259 reading). Claude-Session: https://claude.ai/code/session_01HwUcNzGjPhV6xtYkJ469qK Co-authored-by: Claude --- ...metadata-naming-and-namespace-isolation.md | 5 +-- ...lease-artifact-as-co-ownership-boundary.md | 2 +- ...endency-declared-checked-before-install.md | 34 ++++++++++++++++--- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/docs/adr/0028-metadata-naming-and-namespace-isolation.md b/docs/adr/0028-metadata-naming-and-namespace-isolation.md index 2ff92581b25..9a4fc820936 100644 --- a/docs/adr/0028-metadata-naming-and-namespace-isolation.md +++ b/docs/adr/0028-metadata-naming-and-namespace-isolation.md @@ -174,8 +174,9 @@ reference, and the set of legal targets is closed and written in the manifest and the closed set keeps the tenant-style isolation boundary between marketplace apps: an app reaches only what it declared. On the enforced present-state model (ADR-0129), the runtime save door already judges a package -write against that closure, and ADR-0141 D2 refuses to install an app whose -declared dependency is absent or out of range. +write against that closure, and ADR-0141 D2 decides that installing an app +whose declared dependency is absent or out of range is refused — a decision +its runtime card implements; nothing enforces it until that card lands. ### D5 — The kernel is one unified, reserved namespace (`sys`) with object ownership distributed across first-party plugins diff --git a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md index b7c857554d9..221af07943b 100644 --- a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md +++ b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md @@ -798,7 +798,7 @@ ships its own permission sets under ADR-0086 D4; the card is ⛔ not reopened fo inherited inputs listed under that line remain accurate as history; none is open work. One sentence of that ruling has since moved, and it is said here so a reader of both records is not -left to reconcile them: its premise 「目前的要求是发布物是完整安装的,没有设计单独安装其中一个模块的需求」 was +left to reconcile them: its premise 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 was the state on 2026-09-12; the maintainer's B′ ruling of 2026-10-23 (「而每个包可以独立安装」) is the requirement it said did not exist. The ruling's disposition is unchanged by that: a standalone module's permission sets are the module author's work under ADR-0086 D4, not a platform blocker diff --git a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md index 661d0be4fe3..4ab1e64075f 100644 --- a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md +++ b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md @@ -122,7 +122,8 @@ declared id to `resolvePluginOrder` as an **optional** edge, so the packages of order (ADR-0130 D5) and a declared id that is not in the artifact "is simply not an edge here" — skipped silently, by design (its module header says why). No install door asks whether a declared dependency is installed, or at what version. `dependency_missing` is an enum value of `packages/spec/src/system/app-install.zod.ts#AppCompatibilityCheckSchema`; across -the tree it occurs in that schema, its test, the generated JSON schema and the generated reference page — zero producers. +the tracked tree it occurs in that schema, its test and the generated reference page +(`content/docs/references/system/app-install.mdx`) — zero producers. `packages/core/src/dependency-resolver.ts#SemanticVersionManager` resolves kernel plugins, not installed packages. **The namespace gate admits co-owners from one source only, and the uniqueness check follows that set.** @@ -205,7 +206,8 @@ satisfied when both hold: version (one per artifact under ADR-0130 D6) — satisfies `range`, read in the npm semver range grammar `ManifestSchema.dependencies` already documents and `engines.protocol` already uses. A version or range the reader cannot parse does **not** satisfy; the refusal says which side was unreadable. Fail closed: an unread bound is not a - bound. + bound — deliberately the opposite of the protocol handshake beside it, which admits an unrecognised `engines.protocol` + range with a warning (merge question 6 says why). Every record entry is required: the record shape has no optional arm (D8 says how the marketplace shape's `optional` projects). Only P's **own** entries are judged — a dependency's dependencies were judged when it was installed. @@ -250,7 +252,9 @@ has since gone is D3's uninstall-protection item, not a boot refusal. Each item is a decision **not** to build yet, recorded so the next author finds a reason instead of an absence. None is rejected; each has a trigger. Meanwhile the ADR-0029 refusal stands unchanged (a package whose object another package `extend`s cannot be uninstalled — `packages/objectql/src/registry.ts#uninstallPackage` through `unregisterObjectsByPackage`), -and it is the only uninstall refusal there is. +and it is the only refusal that reads another package's relation to the one being removed; the install-local `DELETE` +door's own refusals (ADR-0131 D12's wall, a request it cannot serve) read the deployment's posture and the request, not +another package. | Deferred item | What happens meanwhile | Trigger to build it | |:---|:---|:---| @@ -338,7 +342,7 @@ it. Renaming a module's objects to dodge the gate stays refused (ADR-0129; lette [`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259) — an artifact installs whole, the app package's sets cover every module's objects, nothing composes grants into another package's set, and a module carved out as its own artifact ships its own permission sets under ADR-0086 D4; the card is not reopened for - that case. That ruling's premise sentence — 「目前的要求是发布物是完整安装的,没有设计单独安装其中一个模块的需求」 — is + that case. That ruling's premise sentence — 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 — is the one the maintainer moved on 2026-10-23 (「而每个包可以独立安装」); its disposition is unchanged and is what D4 above relies on: a standalone module's permission sets are the module author's work, not a platform blocker. @@ -445,7 +449,8 @@ The letters as filed on #25403 and on ruling `6377678758`: ## Open questions for the maintainer's merge decision These are the points this record decided where the rulings were silent, listed so the merge is an informed act. Merging -accepts them; an objection names the number. +accepts them; an objection names the number. Items 1–4 shape the mechanism; 5–8 shape D2's refusal; 9 is a sizing +consequence; 10 is a reading of an earlier ruling. 1. **D2's second arm** — a dependency delivered by the same artifact counts as present. Without it, judging the whole artifact before anything registers would refuse every ADR-0130 artifact whose modules declare their app; judging per @@ -456,6 +461,25 @@ accepts them; an objection names the number. declares and who gains; recognition from X's side is what keeps a rehydrate from refusing HotCRM itself. 4. **D5 direct-only** — co-ownership follows P's own declarations, not the transitive closure the save door walks. Narrower is the reversible direction; widening later needs no undo. +5. **D2's status is 422** — fixed by analogy with `NAMESPACE_CONFLICT`, the sibling refusal at the same gate for a + conflict with the environment's state; the code itself goes to the ADR-0112 D3 ledger on the runtime card. +6. **D2 fails closed on an unparseable version or range** — the opposite of `assertProtocolCompat` at the same doors, + which admits an unrecognised `engines.protocol` range with a warning so that no artifact already in the field is + refused by a parser gap. The reason the policies differ: the handshake grandfathers a fleet of existing artifacts, + while a dependency entry is written today by the dependent's own author about one named package (the in-repo + population is one manifest), and an unread bound would admit every version — the one outcome the check exists to + refuse. +7. **Every record entry is required; the marketplace `optional` arm projects to `false`** — the record shape has no + optional spelling, and inventing one would be the spec change this record forbids itself. +8. **Only P's own entries are judged** — a dependency's own dependencies were judged when it was installed; judging them + again would re-run D2 for packages the administrator already admitted. +9. **D8 item 1's record-to-array conversion enlarges the runtime card** beyond the ruling's sizing (XS–S for the check, + S for the objectql source). The ruling asks the two shapes to be "aligned"; a snapshot that keeps carrying the record + shape under an array schema is not aligned, so the conversion is where the alignment lands — the maintainer may cut + it as its own card instead. +10. **The reading that ruling `5645322259`'s premise was moved by B′** (D6; ADR-0130's addendum) — a reconciliation of two + maintainer rulings that neither ruling on #25403 makes. It is recorded because a reader of both records would + otherwise find them contradicting; the earlier ruling's disposition is untouched. --- From 2bf30f03fa6b2514168c701d74b9a781f09b9a86 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 09:36:37 +0000 Subject: [PATCH 5/7] =?UTF-8?q?docs(adr):=20ADR-0141=20patch=20round=203?= =?UTF-8?q?=20=E2=80=94=20review=20items=201=E2=80=938=20applied=20as=20ac?= =?UTF-8?q?cepted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight changes from the independent review the maintainer accepted in full: 1. D2 second arm: a dependency the same artifact delivers is judged for presence only (packages carry their own manifest.version; the artifact is one tested unit); no warning at install, the in-artifact range mismatch is os lint's. TL;DR 3, merge question 1. 2. D5 read-from-either-end: the stronger reason — installPackage refuses any sitting owner outside the co-owner set, so HotCRM could never be upgraded once a module sits in crm; the free upgrade guard recorded. 3. D5 sibling arm: a sitting owner that declares the same parent (holding the namespace) is a co-owner, read from either end, judged by the D3 uniqueness check too; keeps the second module independently installable. 4. D2 fail-closed paired with a stated range grammar (the tree's two readers accept less than npm's) and an author-side os lint/os validate rule; merge question 6, Execution plan card 2. 5. D8 item 1 (record-to-array snapshot conversion) is its own card, first step the control-plane reader; snapshots keep the record shape meanwhile. 6. Ruling 5645322259 read as unchanged by B′ (artifact installs whole; a standalone module is another artifact; B′ takes the carve-out the ruling named) in D6, Verification notes, merge question 10, 0130's addendum. 7. PATCH /api/v1/packages/:id: the post-install dependencies edit is deferred by name in D3 with interim state and trigger; merge question 11. 8. D7: the carved-out module is published as type: 'app' (ADR-0019 D2, isConsumerInstallable); HotCRM side not re-verified; ADR-0019 D1/D4 interaction noted in one sentence; Open item 6. Claude-Session: https://claude.ai/code/session_01HwUcNzGjPhV6xtYkJ469qK Co-authored-by: Claude --- ...lease-artifact-as-co-ownership-boundary.md | 16 +- ...endency-declared-checked-before-install.md | 282 ++++++++++++------ 2 files changed, 200 insertions(+), 98 deletions(-) diff --git a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md index 221af07943b..0215d635151 100644 --- a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md +++ b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md @@ -797,10 +797,12 @@ nothing composes grants into another package's set, and a module carved out as i ships its own permission sets under ADR-0086 D4; the card is ⛔ not reopened for that case. The three inherited inputs listed under that line remain accurate as history; none is open work. -One sentence of that ruling has since moved, and it is said here so a reader of both records is not -left to reconcile them: its premise 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 was -the state on 2026-09-12; the maintainer's B′ ruling of 2026-10-23 (「而每个包可以独立安装」) is the -requirement it said did not exist. The ruling's disposition is unchanged by that: a standalone -module's permission sets are the module author's work under ADR-0086 D4, not a platform blocker -(ADR-0141 D4, D6). What record-level access across a dependency means is ADR-0141 D4's open -section, decided with the first consumer. +One sentence of that ruling is read beside B′ here, so a reader of both records is not left to +reconcile them: its premise 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 is +unchanged — an artifact installs whole (D2 above stands), and a module shipped on its own under +ADR-0141 is another artifact, not a partial install of the first. The maintainer's B′ ruling of +2026-10-23 (「而每个包可以独立安装」) takes the path the ruling itself named for that case: a module +carved out as its own artifact ships its own permission sets under ADR-0086 D4. The ruling's +disposition is unchanged: a standalone module's permission sets are the module author's work, not a +platform blocker (ADR-0141 D4, D6). What record-level access across a dependency means is ADR-0141 +D4's open section, decided with the first consumer. diff --git a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md index 4ab1e64075f..edcf877995c 100644 --- a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md +++ b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md @@ -59,7 +59,8 @@ and `refuseCoOwnedObjectNameCollision` in `registry.ts`; the manifest service in `@objectstack/metadata-protocol` (`installPackage` in `protocol-package-install.ts`; `resolveWritePackageScope` in `protocol.ts`), `@objectstack/runtime` (`POST /api/v1/packages` and `PATCH /api/v1/packages/:id`, `domains/packages.ts`), `@objectstack/cloud-connection` (`POST /api/v1/marketplace/install-local`), the `objectui` Console's install refusal -text, `packages/cli` (`os package publish`), and HotCRM's module split. +text, `packages/cli` and `packages/lint` (`os package publish`; the `os lint` / `os validate` range rule of D2), and +HotCRM's module split. **Evidence**: every anchor into this repository was read at `origin/main` `47af3dde42`, the base of this record's branch (the tree outside `docs/adr/` is identical at the record's head). `objectstack-ai/hotcrm` and `objectstack-ai/cloud` are not readable from the drafting session; their facts are cited as the rulings and the HotCRM seat cite them and marked @@ -77,16 +78,19 @@ added; one gate learns a second source of co-owners: `dependencies` declare (transitively), platform objects, and tenant objects that belong to no package. An undeclared cross-app reference is refused there today, `422`, with "declare a dependency" as the fix. 3. **The one new gate is a pre-install check.** Installing a package judges every declared dependency — installed in the - target environment (or delivered by the same artifact) and at a version inside the declared range — and otherwise - refuses the whole install, before anything registers, with a message naming what is missing (the ruling's example: - 「请先安装 HotCRM 6.x」). It fills the `dependency_missing` category `AppCompatibilityCheckSchema` has reserved with no - producer. + target environment at a version inside the declared range, or delivered by the same artifact (presence only: the + artifact is one tested unit) — and otherwise refuses the whole install, before anything registers, with a message + naming what is missing (the ruling's example: 「请先安装 HotCRM 6.x」). It fills the `dependency_missing` category + `AppCompatibilityCheckSchema` has reserved with no producer. The range grammar it reads is stated by the runtime + card, and the author's tooling reports an unreadable range before install. 4. **A package that declares a dependency on an installed package X and uses X's namespace co-owns that namespace with - X at the install gate.** One-way: X declares nothing and gains nothing. ADR-0130 D3's per-object-name uniqueness - check spans these co-owners across artifacts exactly as it spans co-owners inside one artifact. + X at the install gate — and with X's other declared dependents that use it (siblings under one declared parent).** + One-way: X declares nothing and gains nothing. ADR-0130 D3's per-object-name uniqueness check spans these co-owners + across artifacts exactly as it spans co-owners inside one artifact. 5. **Deferred, by name, with a trigger each**: uninstall protection, upgrade interception, topological / cascade install, - an owner-side publish marker, marketplace version negotiation. **Permissions are unchanged**; record-level semantics - across a dependency are an open section, decided with the first consumer. + an owner-side publish marker, marketplace version negotiation, and judging a post-install edit of `dependencies`. + **Permissions are unchanged**; record-level semantics across a dependency are an open section, decided with the first + consumer. The first consumer is a HotCRM module shipping on its own, revenue first. @@ -153,8 +157,11 @@ installed HotCRM whatever it declares — the HotCRM seat's input 1, confirmed. `objectstack-ai/cloud` — **not measured** here. **In-repo declaring population**: one manifest — `examples/app-multi-package/src/packages/orders/index.ts` declares -`dependencies: { 'com.example.multi.core': '^1.0.0' }` on the package delivered beside it in the same artifact. HotCRM's -manifests are **not measured** (the seat's input says every one declares `namespace: 'crm'`). +`dependencies: { 'com.example.multi.core': '^1.0.0' }` on the package delivered beside it in the same artifact; each of the +two packages declares its own `version: '1.0.0'` (`examples/app-multi-package/src/packages/core/index.ts`), and +`composeStacks(…, { manifest: 'preserve' })` (`packages/spec/src/stack.zod.ts#composeStacks`) carries each manifest into +the artifact as authored. HotCRM's manifests are **not measured** (the seat's input says every one declares +`namespace: 'crm'`). ### 2. Why now, and for whom @@ -202,12 +209,17 @@ satisfied when both hold: yet "installed" at that moment, and refusing it would refuse `examples/app-multi-package` and every product split under ADR-0130. It mirrors the one reader the key already has at install: inside the artifact a declared id is an edge, outside it is a question for this check. -2. **In range.** The installed record's `manifest.version` — or, for a co-delivered package, that package's declared - version (one per artifact under ADR-0130 D6) — satisfies `range`, read in the npm semver range grammar - `ManifestSchema.dependencies` already documents and `engines.protocol` already uses. A version or range the reader - cannot parse does **not** satisfy; the refusal says which side was unreadable. Fail closed: an unread bound is not a - bound — deliberately the opposite of the protocol handshake beside it, which admits an unrecognised `engines.protocol` - range with a warning (merge question 6 says why). +2. **In range — for an installed record.** The record's `manifest.version` satisfies `range`, read in the npm semver + range grammar `ManifestSchema.dependencies` already documents and `engines.protocol` already uses. A dependency the + same artifact delivers is judged for **presence only**: the packages of one artifact each carry their own + `manifest.version` (Context §1 — `core` and `orders` both declare `1.0.0`, and `composeStacks` keeps each as + authored), so an artifact that bumps its core to `2.0.0` while a module still declares `^1.0.0` would be refused by + the platform over a unit its publisher built, versioned and tested as one (ADR-0130 D6). D2 says nothing about such + an entry — not a warning either: both packages sit in front of `os lint`, which is where an in-artifact range + mismatch is reported (the author-side rule below), and an install is not where an author first hears of it. A + version or range the reader cannot parse does **not** satisfy; the refusal says which side was unreadable. Fail + closed: an unread bound is not a bound — deliberately the opposite of the protocol handshake beside it, which admits + an unrecognised `engines.protocol` range with a warning (merge question 6 says why). Every record entry is required: the record shape has no optional arm (D8 says how the marketplace shape's `optional` projects). Only P's **own** entries are judged — a dependency's dependencies were judged when it was installed. @@ -234,10 +246,21 @@ sits at each). The check sits beside that handshake: hands the artifact to the manifest service — the same placement the picklist and bound-action seals already use in `packages/objectql/src/plugin.ts`: "a refused install registers nothing". -One predicate serves every door; ⛔ no door carries its own copy. The range is read by one of the readers the tree already -has (`packages/core/src/dependency-resolver.ts#SemanticVersionManager`, or -`packages/metadata-core/src/artifact-forward-conversion.ts#parseRangeFloor` and its siblings), extended if it must be — -⛔ never a third semver implementation. +One predicate serves every door; ⛔ no door carries its own copy. + +**The range reader, and the grammar it must state.** No package under `packages/` carries a semver library (the root +`semver` is a devDependency nothing there imports), and the tree's two range readers each read less than the grammar +the key documents: `packages/core/src/dependency-resolver.ts#SemanticVersionManager` accepts `*`, `latest`, an exact +version, one `^` `~` `>=` `>` `<=` `<` operator and `a - b`, answers `false` or throws on the rest — `6.x`, a +space-separated intersection, a `||` union — and its `parse` wants three numeric parts, so `^6` throws; +`packages/metadata-core/src/artifact-forward-conversion.ts#parseRangeFloor` computes only a lower bound. Two things +follow. The runtime card **states the grammar the check supports** — at least `^`, `~`, `>=`, an `x` wildcard, `||` and +the space intersection, so the ruling's own 「HotCRM 6.x」 is readable — by extending one of those readers, ⛔ never a +third semver implementation. And the author side catches an unreadable `dependencies` range **before** install: an +`os lint` / `os validate` rule beside the `engines.protocol` reading `os validate` already performs +(`packages/cli/src/utils/protocol-version-gap.ts#checkProtocolVersionGap`) reports a range outside the stated grammar +and, inside one artifact, a co-delivered entry whose range the sibling's declared version does not satisfy. Fail-closed +at install is the backstop, not the place an author learns the grammar. **Where it does not run — re-registration.** A boot re-registers what an environment already holds: code-composed packages through the manifest service at `init`/`start`, the `sys_packages` rehydrate (`packages/services/service-package/src/index.ts`), @@ -247,6 +270,11 @@ registry incrementally — a code package rehydrated at `start` would read a mar *Startup registry reads* section names as a defect. What an install admitted stays admitted; a dependent whose dependency has since gone is D3's uninstall-protection item, not a boot refusal. +**Nor the manifest edit.** `PATCH /api/v1/packages/:id` rewrites a stored `dependencies` whole after install, by +reference to `ManifestSchema.shape.dependencies` and with ⛔ no in-use check (Context §1). It is not an install act, and +D2 does not run there: an entry added or changed by that door is judged when P is next installed. The hole that leaves — +a stored declaration nobody judged — is D3's last item, deferred by name (merge question 11). + ### D3 — Deferred, by name, not implemented now Each item is a decision **not** to build yet, recorded so the next author finds a reason instead of an absence. None is @@ -263,6 +291,7 @@ another package. | **Topological / cascade install** — installing a set across artifacts in dependency order, or pulling a missing dependency in | The administrator installs dependencies first; D2's refusal names which. Inside one artifact ADR-0130 D5's sort is unchanged | More than one level of standalone modules in a real product | | **Owner-side publish marker** — X restricting which of its objects a dependent may reference | Every object of X is referenceable by a declared dependent, under D4 | A third-party dependent on a first-party app — marketplace publishing, D5's end point for ADR-0130 D8 | | **Marketplace version negotiation** — choosing versions of X and P that satisfy each other | None; D2 judges the installed version against the declared range, once | Listings carrying more than one installable version of a depended-on app | +| **Judging a post-install edit** — running D2's predicate on a `PATCH /api/v1/packages/:id` that adds or changes a `dependencies` entry (a fourth door) | The edit is applied as given — that door's standing disposition, ⛔ no in-use check; the entry is unjudged until P is next installed, and the save door keeps admitting an absent dependency as it does today | A stored declaration found unsatisfied after an edit, or the Console offering the edit to authors | ### D4 — Permissions unchanged; record-level semantics are an open section @@ -290,29 +319,46 @@ against the existing record-access path before its runtime card is cut: **The relation.** Installed packages P and X are *dependency-declared co-owners* of namespace N when P declares X in `manifest.dependencies`, N is P's namespace, N is X's namespace, and N is not a shareable platform namespace -(`packages/objectql/src/registry.ts#isShareableNamespace`). At the install gate, this relation is a second source of the -co-owner set ADR-0130 D1 introduced, beside the artifact scope. Nothing else changes: +(`packages/objectql/src/registry.ts#isShareableNamespace`). **Siblings join it**: an installed package S is also P's +co-owner of N when S and P each directly declare one installed package Q whose namespace is N — two modules under one +declared parent. At the install gate, this relation is a second source of the co-owner set ADR-0130 D1 introduced, +beside the artifact scope. Nothing else changes: - **One-way.** Only the dependent declares. X declares no dependency on its modules and gains nothing on P's side; P - gains admission to the shared namespace and nothing on X's side. The in-tenant trust behind the admission is the - administrator's choice to install P beside X — the same trust that admits any package into an environment. + gains admission to the shared namespace and nothing on X's side, and a sibling gains the same admission beside P. The + in-tenant trust behind the admission is the administrator's choice to install P beside X — the same trust that admits + any package into an environment. - **The D3 pair holds across artifacts.** ADR-0130 D3 made the namespace relaxation and the per-object-name uniqueness - check one change, and the registry reads one co-owner set for both halves; a co-owner by declaration is therefore - judged by both. A dependent defining an object name X already owns is refused at install, ahead of any DDL, with the - error `refuseCoOwnedObjectNameCollision` already raises for co-owners inside one artifact — `code` and `status` - asserted, as ADR-0130 D3's gate test requires. + check one change, and the registry reads one co-owner set for both halves; a co-owner by declaration — parent or + sibling — is therefore judged by both. A dependent defining an object name X already owns, or a sibling defining a + name the other sibling owns, is refused at install, ahead of any DDL, with the error + `refuseCoOwnedObjectNameCollision` already raises for co-owners inside one artifact — `code` and `status` asserted, as + ADR-0130 D3's gate test requires. - **No new manifest field.** The proof is the declaration plus the install act. ADR-0130 D8 stands: no owner or publisher field on `ManifestSchema`. -- **Direct declarations only.** The relation reads P's own `dependencies`, not the transitive closure the save door - walks. A package sharing a namespace with several installed packages declares each one; the `NamespaceConflictError` it - meets otherwise names the owner it did not declare, and the fix is the declaration. Narrower is the reversible - direction. +- **Direct declarations only.** The relation reads P's own `dependencies` and, for the sibling arm, S's own — never the + transitive closure the save door walks. The sibling arm exists because the direct reading alone breaks the second + module: revenue and marketing both declare sales, both use `crm`, and neither depends on the other; when marketing + installs second the sitting owners of `crm` are {sales, revenue}, marketing declares no revenue, and the gate refuses. + The only cure without the arm is to declare revenue, which D2 then requires installed first — modules stop being + independently installable from the second one on, and 「每个包可以独立安装」 is lost; the closure does not reach revenue + from marketing either. A sitting owner that is neither declared nor a sibling still meets `NamespaceConflictError` + naming it, and the fix is the declaration. Narrower is the reversible direction. - **Read from either end.** The relation is declared by P alone, and the gate reads it whichever of the pair arrives - second. On P's registration the sitting owner X is P's co-owner because P declares X; on X's registration a sitting - owner that declares X is X's co-owner for the namespace gate and the uniqueness check alike. This is recognition of one - declared fact, not a right X acquires — X still declares nothing — and it is required because registration order at - boot is not install order: a rehydrate that registers P before X would otherwise refuse the depended-on app itself. - Whether today's rehydrate paths can put P first is the runtime card's first reading (Open items 2). + second: on P's registration the sitting owner X is P's co-owner because P declares X; on X's registration a sitting + owner that declares X is X's co-owner for the namespace gate and the uniqueness check alike; a sibling is recognised + whichever sibling registers second, each reading the other's declaration of Q. This is recognition of one declared + fact, not a right X acquires — X still declares nothing — and it is not optional: + `packages/objectql/src/registry.ts#installPackage` refuses when any sitting owner of the namespace is neither the + installing package nor in the co-owner set (`getNamespaceOwners(...).find(owner => owner !== manifest.id && + !coOwners.has(owner))`), and once revenue is installed it *is* a sitting owner of `crm`. The next upgrade or reinstall + of HotCRM arrives second; read from P's side alone, HotCRM itself is refused `NAMESPACE_CONFLICT`, so HotCRM could + never be upgraded once a module is installed. Boot order is the second reason: a rehydrate that registers P before X + would refuse the depended-on app the same way; whether today's paths can is the runtime card's first reading (Open + items 2). One consequence comes free: on such an upgrade the uniqueness check judges X against its co-owner P, so an + upgrade of HotCRM that newly defines a `crm_*` name revenue already owns is refused naming revenue — a small upgrade + guard D3's interception item does not otherwise give, and the Console's refusal text must say which module holds the + name. **ADR-0130 D8's deferral now ends at marketplace publishing**, not at a module's first standalone artifact. Within one tenant, D5 proves co-ownership without an owner field. Across vendors — the moment a module is published to the @@ -342,18 +388,28 @@ it. Renaming a module's objects to dodge the gate stays refused (ADR-0129; lette [`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259) — an artifact installs whole, the app package's sets cover every module's objects, nothing composes grants into another package's set, and a module carved out as its own artifact ships its own permission sets under ADR-0086 D4; the card is not reopened for - that case. That ruling's premise sentence — 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 — is - the one the maintainer moved on 2026-10-23 (「而每个包可以独立安装」); its disposition is unchanged and is what D4 above - relies on: a standalone module's permission sets are the module author's work, not a platform blocker. + that case. That ruling's premise — 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 — is + **unchanged** by B′: an artifact still installs whole (ADR-0130 D2 stands), and a module shipped on its own is another + artifact, not a partial install of the first. B′ (「而每个包可以独立安装」) takes the path the ruling itself named for + that case — a module carved out as its own artifact ships its own permission sets — so the ruling's disposition stands + as written and is what D4 above relies on: a standalone module's permission sets are the module author's work, not a + platform blocker. ### D7 — The first consumer: a HotCRM module shipping on its own, revenue first The scenario this record is accepted against is HotCRM's module split as its `docs/architecture/module-split-plan.md` records it — the 2026-09-14 shape: the sales app plus service, revenue and marketing modules (**not re-verified**). The -revenue module ships as its own artifact, declares `dependencies: { "HOTCRM_SALES_PACKAGE_ID": "HOTCRM_MAJOR_RANGE" }`, -keeps `namespace: 'crm'` and its `crm_*` object names, and installs beside an installed HotCRM. On an environment -without HotCRM, or with HotCRM outside the declared range, D2 refuses it naming HotCRM and the range; beside HotCRM, D5 -admits it to `crm` and the uniqueness check refuses any `crm_*` name HotCRM already owns. +revenue module ships as its own artifact whose consumer-installable package is **`type: 'app'`** — only that type +installs for a consumer (`packages/spec/src/kernel/plugin.zod.ts#isConsumerInstallable`, ADR-0019 D2); inside the +combined artifact it is an internal-contribution type under ADR-0130 D2, so the carve-out changes its `type` (the HotCRM +side **not re-verified**). It declares `dependencies: { "HOTCRM_SALES_PACKAGE_ID": "HOTCRM_MAJOR_RANGE" }`, keeps +`namespace: 'crm'` and its `crm_*` object names, and installs beside an installed HotCRM. On an environment without +HotCRM, or with HotCRM outside the declared range, D2 refuses it naming HotCRM and the range; beside HotCRM, D5 admits it +to `crm` and the uniqueness check refuses any `crm_*` name HotCRM already owns; marketing, shipped the same way, is +admitted as revenue's sibling. Under ADR-0019 the standalone module is therefore an App in its own right — installed, +opened and uninstalled as one (D1, D4) — and D4's "set of namespaces" gains a namespace two Apps hold, which the +registry's `Map>` already represents: uninstalling the module releases its own ownership and +objects, not the namespace HotCRM still owns. Until the runtime card ships, composite products keep shipping as one artifact under ADR-0130. That is the state hotcrm#2270 packages revenue and marketing in; hotcrm#2271 moves revenue to its own artifact once the check is released. @@ -365,11 +421,15 @@ closure reads it, D5 reads it. ⛔ No spec shape changes in this record. The marketplace snapshot's array — `PackageManifestSchema.dependencies` of `PackageDependencySchema` — is a **projection** of that record, one entry per key: `packageId` = the key, `versionRange` = the value, `optional` = `false` (the record -has no optional arm, and D2 treats every entry as required). The runtime card owes: +has no optional arm, and D2 treats every entry as required). Three things are owed — the first on its own card, the +other two on the runtime card: 1. **One conversion** from record to array, used by every writer of a snapshot, so a snapshot conforms to its declared schema instead of carrying the record shape it carries today (`publish.ts` uploads the compiled artifact as it is). - Where the control plane reads `manifest_json` is in `objectstack-ai/cloud`; the card states what it found there. + **Its own card** (merge question 9), whose first step reads how the control plane parses + `sys_package_version.manifest_json` — in `objectstack-ai/cloud`, not readable here — because changing the snapshot's + shape before that reading could break the very reader it is meant to satisfy. Until that card lands, snapshots keep + carrying the record shape, as today: no new harm, and D2 reads the manifest record, never the snapshot. 2. **A stated fate for `optional`.** An `optional: true` entry has no record spelling. Until one is decided, a snapshot never produces it and D2 never reads it. 3. **A stated fate for `AppManifestSchema.dependencies`** (bare ids, no range, no reader outside `packages/spec`): either @@ -391,11 +451,16 @@ not a declaration, and `PackageDependencySchema`'s own header already tells the - An AI author's undeclared reference stays a loud `422` at the save door, and a missing dependency becomes a loud refusal at install, instead of copied objects that drift silently. - The work is small and stays small: no new authorable key, no schema change, one predicate at three doors, one more - source for one co-owner set. + source for one co-owner set (read two ways: a declared parent, a shared declared parent), one author-side lint rule. +- Two modules of one app install in either order and need not declare each other (the sibling arm), and an upgrade of + the app that would take a module's object name is refused for free (D5). **Negative / costs, stated plainly** -- Uninstalling or upgrading a depended-on app is not guarded (D3). An administrator can strand a dependent until those - items are built; the refusal at install is the only gate, and the refusal's message is the only guidance. +- Uninstalling or upgrading a depended-on app is not guarded (D3), and a `dependencies` entry edited in after install is + not judged until the next install of that package. An administrator can strand a dependent until those items are + built; the refusal at install is the only gate, and the refusal's message is the only guidance. +- The gate's range reader must grow a stated grammar before the first consumer's range is readable; until the author-side + rule exists, an unreadable range is refused at install, not reported at build (D2). - A standalone module and its app have **separate versions** — the matrix ADR-0130 D6 removed inside one artifact returns across artifacts, bounded only by the declared range and judged only at install. - D5 lets a module share a vendor's namespace on the administrator's say-so. Inside one tenant that is the install choice @@ -404,9 +469,10 @@ not a declaration, and `PackageDependencySchema`'s own header already tells the - D4's open section is real work before the first consumer ships: the permission-set discriminator ADR-0086 D4 relied on is gone across artifacts. -**Unchanged** — the consumer model (ADR-0019 D1–D4: one app installs, opens, uninstalls), the artifact as a control-plane -noun (ADR-0130 D2), the one sorter (ADR-0130 D5), one version per artifact (ADR-0130 D6), permission sets (ADR-0086 D4), -the save door's closure rules, and the compile-time boundary (`os validate` judges neither side). +**Unchanged** — the consumer model (ADR-0019 D1–D4: one app installs, opens, uninstalls — a standalone module is itself +an App), the artifact as a control-plane noun that installs whole (ADR-0130 D2), the one sorter (ADR-0130 D5), one +version per artifact (ADR-0130 D6), permission sets (ADR-0086 D4), the save door's closure rules, the snapshot shape +(D8, until its own card), and the compile-time boundary (`os validate` judges neither side of a cross-artifact binding). --- @@ -436,8 +502,9 @@ The letters as filed on #25403 and on ruling `6377678758`: 1. **Record-level semantics across a dependency** — D4's three questions, measured on HotCRM's revenue module. 2. **Rehydrate order** — whether any boot path (`sys_packages`, the install-local ledger, code packages) can register a - dependent before the package it depends on. D5's read-from-either-end rule makes the answer harmless; the card records - it. + dependent before the package it depends on. D5's read-from-either-end rule makes the answer harmless for a declared + pair; the sibling arm reads the parent's namespace from the parent's record, so a boot that registers two siblings + before their parent is the case the card measures first. 3. **Declared ids that are not installable packages.** The key's own documentation example is an npm-style id. The in-repo population is one manifest, co-delivered (Context §1). If an id that names no installable package turns up in HotCRM's manifests, the card reports it; D2 does not exempt it. @@ -445,41 +512,61 @@ The letters as filed on #25403 and on ruling `6377678758`: warn for a dependent is measured against what the save door's closure and the data door do with a disabled package today. 5. **`optional` and `AppManifestSchema.dependencies`** — D8 items 2 and 3. +6. **The carved-out module's `type`.** D7 states `type: 'app'`. The card confirms the artifact `os build` writes for a + standalone module carries it, and reads what each install door does today with a top-level package of another type + (not measured here). ## Open questions for the maintainer's merge decision These are the points this record decided where the rulings were silent, listed so the merge is an informed act. Merging -accepts them; an objection names the number. Items 1–4 shape the mechanism; 5–8 shape D2's refusal; 9 is a sizing -consequence; 10 is a reading of an earlier ruling. - -1. **D2's second arm** — a dependency delivered by the same artifact counts as present. Without it, judging the whole - artifact before anything registers would refuse every ADR-0130 artifact whose modules declare their app; judging per - package instead would leave a half-registered artifact behind on refusal. -2. **D2's placement** — the three install acts only, never boot re-registration. The cost: an environment whose - dependency was removed outside an install act boots without complaint (D3's uninstall item). +accepts them; an objection names the number. Items 1–4 and 11 shape the mechanism; 5–8 shape D2's refusal; 9 is a sizing +decision; 10 is a reading of an earlier ruling. + +1. **D2's second arm — a dependency delivered by the same artifact counts as present, and is judged for presence only.** + Without the arm, judging the whole artifact before anything registers would refuse every ADR-0130 artifact whose + modules declare their app, and judging per package instead would leave a half-registered artifact behind on refusal. + Without the presence-only reading, an artifact whose packages carry their own `manifest.version` (as + `examples/app-multi-package`'s do) would be refused by the platform over a range its publisher built and tested as one + unit; D2 says nothing about such an entry, and the mismatch is `os lint`'s to report. +2. **D2's placement** — the three install acts only; never boot re-registration, never the manifest edit (11). The cost: + an environment whose dependency was removed outside an install act boots without complaint (D3's uninstall item). 3. **D5 read from either end** — the gate honours P's declaration on X's registration too. "One-way" is kept for who - declares and who gains; recognition from X's side is what keeps a rehydrate from refusing HotCRM itself. -4. **D5 direct-only** — co-ownership follows P's own declarations, not the transitive closure the save door walks. + declares and who gains; recognition from X's side is what lets HotCRM be upgraded or reinstalled at all once a module + sits in `crm` (the gate refuses any sitting owner outside the co-owner set), keeps a rehydrate from refusing HotCRM + itself, and brings the upgrade guard D5 records. +4. **D5 direct-only, with the sibling arm** — co-ownership follows P's own declarations and those of a sibling under one + declared parent that holds the namespace, never the transitive closure the save door walks. Without the arm the + second module of one app is not independently installable; with it there is no new field and no closure walk. Narrower is the reversible direction; widening later needs no undo. 5. **D2's status is 422** — fixed by analogy with `NAMESPACE_CONFLICT`, the sibling refusal at the same gate for a conflict with the environment's state; the code itself goes to the ADR-0112 D3 ledger on the runtime card. -6. **D2 fails closed on an unparseable version or range** — the opposite of `assertProtocolCompat` at the same doors, - which admits an unrecognised `engines.protocol` range with a warning so that no artifact already in the field is - refused by a parser gap. The reason the policies differ: the handshake grandfathers a fleet of existing artifacts, - while a dependency entry is written today by the dependent's own author about one named package (the in-repo - population is one manifest), and an unread bound would admit every version — the one outcome the check exists to - refuse. +6. **D2 fails closed on an unparseable version or range, and the grammar is stated** — the opposite of + `assertProtocolCompat` at the same doors, which admits an unrecognised `engines.protocol` range with a warning so that + no artifact already in the field is refused by a parser gap. The reason the policies differ: the handshake + grandfathers a fleet of existing artifacts, while a dependency entry is written today by the dependent's own author + about one named package (the in-repo population is one manifest), and an unread bound would admit every version — the + one outcome the check exists to refuse. Because the tree's two readers accept less than the documented grammar (`6.x` + reads as nothing today), fail-closed is paired with a stated grammar on the runtime card and an author-side rule that + reports an unreadable range before install — otherwise the ruling's own example would be refused at the gate it was + written for. 7. **Every record entry is required; the marketplace `optional` arm projects to `false`** — the record shape has no optional spelling, and inventing one would be the spec change this record forbids itself. 8. **Only P's own entries are judged** — a dependency's own dependencies were judged when it was installed; judging them again would re-run D2 for packages the administrator already admitted. -9. **D8 item 1's record-to-array conversion enlarges the runtime card** beyond the ruling's sizing (XS–S for the check, - S for the objectql source). The ruling asks the two shapes to be "aligned"; a snapshot that keeps carrying the record - shape under an array schema is not aligned, so the conversion is where the alignment lands — the maintainer may cut - it as its own card instead. -10. **The reading that ruling `5645322259`'s premise was moved by B′** (D6; ADR-0130's addendum) — a reconciliation of two - maintainer rulings that neither ruling on #25403 makes. It is recorded because a reader of both records would - otherwise find them contradicting; the earlier ruling's disposition is untouched. +9. **D8 item 1's record-to-array conversion is its own card**, not the runtime card's — the ruling's sizing (XS–S for the + check, S for the objectql source) is kept, and the conversion waits on one reading: how the control plane parses + `sys_package_version.manifest_json` (`objectstack-ai/cloud`, not readable here). Changing the snapshot's shape before + that reading could break the reader it is meant to satisfy; until then snapshots carry the record shape as they do + today, no new harm. +10. **The reading of ruling `5645322259`** (D6; ADR-0130's addendum) — its premise is read as unchanged by B′: an + artifact installs whole (ADR-0130 D2), a standalone module is another artifact, and B′ takes the carve-out path the + ruling itself named. A reconciliation neither ruling on #25403 makes; recorded so a reader of both records does not + find them contradicting. +11. **The post-install edit of `dependencies` is deferred, not a fourth door** — `PATCH /api/v1/packages/:id` rewrites + the declaration after install with no in-use check, so an entry added there meets D2 only at the package's next + install. Running D2's predicate on that edit is one call at one more door; it is deferred (D3) because B′ named one + new gate, and the edit's practical case — an author declaring the package whose object the save door just refused — + names a package that is present. The maintainer may take the fourth door instead; it needs no undo. --- @@ -495,8 +582,9 @@ consequence; 10 is a reading of an earlier ruling. - **"`revenue` first per ADR-0130 §1.3(c)"** (ruling `6369704387`): §1.3(c) names CPQ as the module with no sellable unit; it does not order HotCRM's modules. Revenue first is HotCRM's own ordering (module-split plan, hotcrm#2271); §1.3(c) is the general consequence both serve. No decision moves. -- **#14488.** Closed `not_planned` 2026-09-12 by ruling `5645322259`, as the HotCRM seat's input 2 says. Its premise - sentence has since been moved by the maintainer's B′ ruling; its disposition stands (D6). +- **#14488.** Closed `not_planned` 2026-09-12 by ruling `5645322259`, as the HotCRM seat's input 2 says. Its premise — + an artifact installs whole — is not moved by B′, which takes the carve-out path the ruling itself named; its + disposition stands (D6). - **ADR numbering.** `0136` and `0140` were taken by proposals that never landed — PR #18480 (closed 2026-09-18, superseded by the North Star) and PR #25618 (withdrawn 2026-10-24 by the maintainer's ruling in session) — and are not reused; no landed record or open pull request carries either number, and no open pull request carries a new record. @@ -507,14 +595,24 @@ consequence; 10 is a reading of an earlier ruling. ## Execution plan (after acceptance) 1. **Runtime card (XS–S, `domain:services` / objectql), one PR**: D2's predicate at the three install doors, with the - `dependency_missing` issues, the ledger-registered code and the Console refusal text; D5's dependency source for the - co-owner set, read from either end, with the ADR-0130 D3 gate test extended to a cross-artifact pair (a dependent - defining an object name X owns is refused, asserting `code` and `status`); D8's record-to-array conversion. Its first - readings: Open items 2, 3 and 4. -2. **Docs**: the package-authoring guide states how to declare a dependency, what the install refusal means, and that - `os validate` cannot see a dependency's objects. -3. **HotCRM** (its own lane): the D4 readings on the revenue module; hotcrm#2271 ships revenue as its own artifact once - (1) is released. + `dependency_missing` issues, the ledger-registered code, the stated range grammar and the Console refusal text; D5's + dependency source for the co-owner set — declared parent and sibling arm, read from either end — with the ADR-0130 D3 + gate test extended to a cross-artifact pair and a sibling pair (a dependent defining an object name X owns, and a + sibling defining a name the other sibling owns, are refused, asserting `code` and `status`), and the upgrade case (X + reinstalled beside its module is admitted; X newly defining a name the module owns is refused); D8 items 2 and 3. + Its first readings: Open items 2, 3, 4 and 6. +2. **Author-side card (XS, `packages/cli` / `packages/lint`)**: the `os lint` / `os validate` rule that reports a + `dependencies` range outside the stated grammar and, inside one artifact, a co-delivered entry whose range the + sibling's declared version does not satisfy (D2). Released with or before (1), so the first refusal an author meets is + at build. +3. **Snapshot-alignment card (S, `packages/cli`, with a reading in `objectstack-ai/cloud`)**: D8 item 1 — first the + control-plane reader of `sys_package_version.manifest_json`, then the one record-to-array conversion. Independent of + (1); snapshots keep the record shape until it lands. +4. **Docs**: the package-authoring guide states how to declare a dependency, the range grammar, what the install refusal + means, that two modules of one app need not declare each other, and that `os validate` cannot see a dependency's + objects. +5. **HotCRM** (its own lane): the D4 readings on the revenue module; the carved-out module's `type: 'app'` (D7); + hotcrm#2271 ships revenue as its own artifact once (1) is released. ## References @@ -524,7 +622,8 @@ consequence; 10 is a reading of an earlier ruling. - ADR-0019, ADR-0028, ADR-0029, ADR-0048 (and its 2026-08-08 addendum), ADR-0086, ADR-0087, ADR-0112, ADR-0116, ADR-0129, ADR-0130 - `packages/spec/src/kernel/manifest.zod.ts`, `packages/spec/src/system/app-install.zod.ts`, - `packages/spec/src/marketplace/package-version.zod.ts`, `packages/spec/src/kernel/package-registry.zod.ts` + `packages/spec/src/marketplace/package-version.zod.ts`, `packages/spec/src/kernel/package-registry.zod.ts`, + `packages/spec/src/kernel/plugin.zod.ts` (`isConsumerInstallable`) - `packages/objectql/src/registry.ts` (`installPackage`, `refuseCoOwnedObjectNameCollision`, `NamespaceConflictError`, `uninstallPackage`), `packages/objectql/src/engine.ts` (`registerApp`), `packages/objectql/src/plugin.ts` - `packages/metadata-protocol/src/protocol.ts` (`resolveWritePackageScope`), @@ -533,6 +632,7 @@ consequence; 10 is a reading of an earlier ruling. `packages/core/src/dependency-resolver.ts`, `packages/metadata-core/src/artifact-forward-conversion.ts` - `packages/runtime/src/domains/packages.ts`, `packages/cloud-connection/src/marketplace-install-local-plugin.ts`, `packages/services/service-package/src/index.ts`, `packages/lint/src/rule-explanations.ts` - (`list-view-object-outside-closure`), `packages/spec/src/stack.zod.ts` -- `examples/app-multi-package/src/packages/orders/index.ts` + (`list-view-object-outside-closure`), `packages/spec/src/stack.zod.ts` (`composeStacks`), + `packages/cli/src/utils/protocol-version-gap.ts` (`checkProtocolVersionGap`) +- `examples/app-multi-package/src/packages/orders/index.ts`, `examples/app-multi-package/src/packages/core/index.ts` - hotcrm `docs/architecture/module-split-plan.md`, hotcrm#2270, hotcrm#2271 (not re-verified) From 5c4071ef9c1c1745eb751f58560fef9b2b7a5924 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 10:12:37 +0000 Subject: [PATCH 6/7] =?UTF-8?q?docs(adr):=20ADR-0141=20patch=20round=204?= =?UTF-8?q?=20=E2=80=94=20independent=20review=20F1=E2=80=93F11=20applied?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F1 D5/D7: the depended-on app's companions must declare it — the registry keeps no artifact record after install (InstalledPackageSchema, scope not persisted), so a standalone module meets the app's other packages as foreign sitting owners and the app's next upgrade meets the module; stated as a precondition, Open item 7 for the HotCRM lane. F2 D5: hydrate order is a requirement — the sys_packages hydrate and the install-local ledger register in list/readdir order and a refused row stays absent for the process; decided: both paths sort their batch by manifest.dependencies through resolvePluginOrder (ADR-0116's sorter); merge question 12; Open item 2 narrowed to cross-path order. F3 D2: three range readers, not two — rangeAdmitsMajor reads the full grammar at major granularity, parseRangeFloor the floor only, SemanticVersionManager full versions over a narrow grammar with no caller; the handshake family in metadata-core is the reader to extend. F4 0130 addendum (and header pointer) name the sibling arm. F5 DUPLICATE_ARTIFACT_OBJECT_NAME keeps its code; message worded per co-ownership source. F6 PATCH door reaches only authored packages (WRITABLE_PACKAGE_REQUIRED, ADR-0070 D2) and can delete the entry that is D5's proof; D3 row and merge question 11. F7 Ruling 5645322259 read in two halves: artifact-installs-whole unchanged; the need the second half denied is met by another artifact. F8 Amends/D6 name 0130's header pointer line beside the addendum. F9 D7: HotCRM-side hooks and permission-set preconditions, not re-verified. F10 Merge question 5 weighs the 409 precedents and keeps 422. F11 kernel:ready seals folded into the hydrate-order requirement. Claude-Session: https://claude.ai/code/session_01HwUcNzGjPhV6xtYkJ469qK Co-authored-by: Claude --- ...lease-artifact-as-co-ownership-boundary.md | 23 +- ...endency-declared-checked-before-install.md | 220 +++++++++++++----- 2 files changed, 169 insertions(+), 74 deletions(-) diff --git a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md index 0215d635151..350ca720597 100644 --- a/docs/adr/0130-release-artifact-as-co-ownership-boundary.md +++ b/docs/adr/0130-release-artifact-as-co-ownership-boundary.md @@ -13,7 +13,7 @@ it. D4's read-both rule and D7 are unchanged, so every artifact already on disk ([ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) D5, [#25403](https://github.com/objectstack-ai/objectstack/issues/25403)): a package that declares `dependencies` on an installed package and shares its namespace is that package's co-owner at the -install gate, across artifacts, so the owner field is deferred until **marketplace publishing** +install gate, across artifacts (and its declared siblings' — ADR-0141 D5), so the owner field is deferred until **marketplace publishing** rather than until a module's first standalone artifact. The same addendum records that the 2026-09-02 addendum's #14488 line is stale. D1–D7 are unchanged. **Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated: @@ -772,7 +772,8 @@ D6 and D8 above say the owner field becomes necessary *"the day a module ships a artifact"*. ADR-0141 D5 answers that day without the field: a package that declares `dependencies` on an installed package X and uses X's namespace is X's co-owner at the install gate — one-way (X declares nothing and gains nothing), read from either end at registration, direct declarations -only — and D3's per-object-name uniqueness check spans those co-owners across artifacts exactly as +only, plus siblings under one declared parent that holds the namespace, as ADR-0141 D5 defines — +and D3's per-object-name uniqueness check spans those co-owners across artifacts exactly as it spans co-owners inside one artifact, because the registry reads one co-owner set for both halves of the pair. Inside one tenant the proof is the declaration plus the administrator's install act; no manifest field is added. @@ -798,11 +799,13 @@ ships its own permission sets under ADR-0086 D4; the card is ⛔ not reopened fo inherited inputs listed under that line remain accurate as history; none is open work. One sentence of that ruling is read beside B′ here, so a reader of both records is not left to -reconcile them: its premise 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 is -unchanged — an artifact installs whole (D2 above stands), and a module shipped on its own under -ADR-0141 is another artifact, not a partial install of the first. The maintainer's B′ ruling of -2026-10-23 (「而每个包可以独立安装」) takes the path the ruling itself named for that case: a module -carved out as its own artifact ships its own permission sets under ADR-0086 D4. The ruling's -disposition is unchanged: a standalone module's permission sets are the module author's work, not a -platform blocker (ADR-0141 D4, D6). What record-level access across a dependency means is ADR-0141 -D4's open section, decided with the first consumer. +reconcile them: its premise 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 has +two halves. The first — an artifact installs whole — is unchanged: D2 above stands, and a module +shipped on its own under ADR-0141 is another artifact, not a partial install of the first. The +second — that no requirement to install one module alone existed — names the need the maintainer's +B′ ruling of 2026-10-23 (「而每个包可以独立安装」) now states, met by another artifact: the path the +ruling itself named for that case, a module carved out as its own artifact shipping its own +permission sets under ADR-0086 D4, never a partial install. The ruling's disposition is unchanged: +a standalone module's permission sets are the module author's work, not a platform blocker +(ADR-0141 D4, D6). What record-level access across a dependency means is ADR-0141 D4's open +section, decided with the first consumer. diff --git a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md index edcf877995c..1f063a1dad9 100644 --- a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md +++ b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md @@ -48,7 +48,8 @@ prefix literally — why a split module keeps `crm_*` and the `crm` namespace), today: a package whose object another package `extend`s). **Amends**: [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) D4 — a Deferred proposal whose sentence is rewritten (D6 below); ADR-0130 D8's deferral end point and the stale #14488 line of its 2026-09-02 addendum — by an -additive addendum on 0130, no original text of 0130 edited; ADR-0048 §3.2's install gate gains a second co-owner source +additive addendum on 0130 plus one status-header pointer line (the form 0130's 2026-09-22 line set), no original text +of 0130 edited; ADR-0048 §3.2's install gate gains a second co-owner source (D5) — by cross-reference from here, exactly as ADR-0130 D1 amended that gate, ⛔ no text of 0048 edited. **Cross-references**: ADR-0019 gains one additive `Related` header line; its text is unchanged (ADR-0130 D2's recording convention, which that record offered as the follow-up should the maintainer want the amendment visible from 0019). @@ -86,7 +87,9 @@ added; one gate learns a second source of co-owners: 4. **A package that declares a dependency on an installed package X and uses X's namespace co-owns that namespace with X at the install gate — and with X's other declared dependents that use it (siblings under one declared parent).** One-way: X declares nothing and gains nothing. ADR-0130 D3's per-object-name uniqueness check spans these co-owners - across artifacts exactly as it spans co-owners inside one artifact. + across artifacts exactly as it spans co-owners inside one artifact. Two conditions make it hold: every package of X's + own artifact that shares the namespace declares X too, and a boot re-registers each hydrate path's packages in + dependency order through the one sorter. 5. **Deferred, by name, with a trigger each**: uninstall protection, upgrade interception, topological / cascade install, an owner-side publish marker, marketplace version negotiation, and judging a post-install edit of `dependencies`. **Permissions are unchanged**; record-level semantics across a dependency are an open section, decided with the first @@ -128,7 +131,9 @@ design (its module header says why). No install door asks whether a declared dep `dependency_missing` is an enum value of `packages/spec/src/system/app-install.zod.ts#AppCompatibilityCheckSchema`; across the tracked tree it occurs in that schema, its test and the generated reference page (`content/docs/references/system/app-install.mdx`) — zero producers. -`packages/core/src/dependency-resolver.ts#SemanticVersionManager` resolves kernel plugins, not installed packages. +`packages/core/src/dependency-resolver.ts#SemanticVersionManager` is written for kernel plugins, not installed packages, +and has no caller in `packages/` outside its own file and test; the handshake's +`packages/metadata-core/src/protocol-handshake.ts#rangeAdmitsMajor` reads `engines.protocol` at major granularity. **The namespace gate admits co-owners from one source only, and the uniqueness check follows that set.** `SchemaRegistry.installPackage` (`packages/objectql/src/registry.ts#installPackage`) builds its co-owner set from the @@ -249,14 +254,21 @@ sits at each). The check sits beside that handshake: One predicate serves every door; ⛔ no door carries its own copy. **The range reader, and the grammar it must state.** No package under `packages/` carries a semver library (the root -`semver` is a devDependency nothing there imports), and the tree's two range readers each read less than the grammar -the key documents: `packages/core/src/dependency-resolver.ts#SemanticVersionManager` accepts `*`, `latest`, an exact -version, one `^` `~` `>=` `>` `<=` `<` operator and `a - b`, answers `false` or throws on the rest — `6.x`, a -space-separated intersection, a `||` union — and its `parse` wants three numeric parts, so `^6` throws; -`packages/metadata-core/src/artifact-forward-conversion.ts#parseRangeFloor` computes only a lower bound. Two things -follow. The runtime card **states the grammar the check supports** — at least `^`, `~`, `>=`, an `x` wildcard, `||` and -the space intersection, so the ruling's own 「HotCRM 6.x」 is readable — by extending one of those readers, ⛔ never a -third semver implementation. And the author side catches an unreadable `dependencies` range **before** install: an +`semver` is a devDependency nothing there imports), and the tree's three range readers each lack something D2 needs. +`packages/metadata-core/src/protocol-handshake.ts#rangeAdmitsMajor` (with `alternativeAdmitsMajor`) already reads the +documented grammar — `*`, `x`, `latest`, `^`, `~`, `N.x` / `N.*`, `||` unions, space- or comma-joined comparator +intersections, hyphen ranges, bare versions — but answers at **major granularity** only: `^6.2.0` against an installed +`6.1.0` answers true, which D2 cannot accept. `packages/metadata-core/src/artifact-forward-conversion.ts#parseRangeFloor`, +the same school by its own docstring, computes only a lower bound. `packages/core/src/dependency-resolver.ts#SemanticVersionManager` +compares full versions but reads a narrower grammar — `*`, `latest`, an exact version, one `^` `~` `>=` `>` `<=` `<` +operator, `a - b`; `6.x`, an intersection or a `||` union answers `false` or throws, and its `parse` wants three numeric +parts, so `^6` throws — and has no caller in `packages/` outside its own file and test (re-exported by +`@objectstack/core`'s index; "resolves kernel plugins" is its header's claim, not a measured caller). Two things follow. +The runtime card gives **one** reader both properties — the handshake family's grammar and exact-version granularity — +and **states the grammar the check supports**: at least `^`, `~`, `>=`, an `x` wildcard, `||` and the space intersection, +so the ruling's own 「HotCRM 6.x」 is readable. The drafter's choice is to extend the handshake family in +`packages/metadata-core`, on which every install door already depends and beside whose `assertProtocolCompat` the check +sits; ⛔ never a fourth reader. And the author side catches an unreadable `dependencies` range **before** install: an `os lint` / `os validate` rule beside the `engines.protocol` reading `os validate` already performs (`packages/cli/src/utils/protocol-version-gap.ts#checkProtocolVersionGap`) reports a range outside the stated grammar and, inside one artifact, a co-delivered entry whose range the sibling's declared version does not satisfy. Fail-closed @@ -268,12 +280,15 @@ and the install-local plugin's cached-manifest rehydrate on `kernel:ready`. None registry incrementally — a code package rehydrated at `start` would read a marketplace dependency that arrives at `kernel:ready` as absent. Concluding "dependency X is not installed" there and refusing on it is the verdict AGENTS.md's *Startup registry reads* section names as a defect. What an install admitted stays admitted; a dependent whose dependency -has since gone is D3's uninstall-protection item, not a boot refusal. +has since gone is D3's uninstall-protection item, not a boot refusal. The order a boot registers in is D5's requirement, +because the namespace gate does run there. **Nor the manifest edit.** `PATCH /api/v1/packages/:id` rewrites a stored `dependencies` whole after install, by -reference to `ManifestSchema.shape.dependencies` and with ⛔ no in-use check (Context §1). It is not an install act, and -D2 does not run there: an entry added or changed by that door is judged when P is next installed. The hole that leaves — -a stored declaration nobody judged — is D3's last item, deferred by name (merge question 11). +reference to `ManifestSchema.shape.dependencies` and with ⛔ no in-use check (Context §1); it reaches only authored +packages, since `requireWritablePackage(…, 'edit')` answers `422 WRITABLE_PACKAGE_REQUIRED` for a package provided by +code or an installed app (ADR-0070 D2). It is not an install act, and D2 does not run there: an entry added or changed +by that door is judged when P is next installed, and an entry deleted there — D5's proof of co-ownership — is missed at +the next related registration. Both are D3's last item, deferred by name (merge question 11). ### D3 — Deferred, by name, not implemented now @@ -291,7 +306,7 @@ another package. | **Topological / cascade install** — installing a set across artifacts in dependency order, or pulling a missing dependency in | The administrator installs dependencies first; D2's refusal names which. Inside one artifact ADR-0130 D5's sort is unchanged | More than one level of standalone modules in a real product | | **Owner-side publish marker** — X restricting which of its objects a dependent may reference | Every object of X is referenceable by a declared dependent, under D4 | A third-party dependent on a first-party app — marketplace publishing, D5's end point for ADR-0130 D8 | | **Marketplace version negotiation** — choosing versions of X and P that satisfy each other | None; D2 judges the installed version against the declared range, once | Listings carrying more than one installable version of a depended-on app | -| **Judging a post-install edit** — running D2's predicate on a `PATCH /api/v1/packages/:id` that adds or changes a `dependencies` entry (a fourth door) | The edit is applied as given — that door's standing disposition, ⛔ no in-use check; the entry is unjudged until P is next installed, and the save door keeps admitting an absent dependency as it does today | A stored declaration found unsatisfied after an edit, or the Console offering the edit to authors | +| **Judging a post-install edit** — running D2's predicate on a `PATCH /api/v1/packages/:id` that adds, changes or deletes a `dependencies` entry (a fourth door) | The edit is applied as given — that door's standing disposition, ⛔ no in-use check. The door reaches only **authored** packages (`422 WRITABLE_PACKAGE_REQUIRED` for a package provided by code or an installed app, ADR-0070 D2), so a marketplace-installed module cannot be edited there at all. An added entry is unjudged until P is next installed, and the save door keeps admitting an absent dependency as it does today. A deleted entry removes D5's proof of co-ownership: the next related registration — an upgrade of X, a reinstall of P, a boot — is refused at the namespace gate naming the other package | A stored declaration found unsatisfied or missing after an edit, or the Console offering the edit to authors | ### D4 — Permissions unchanged; record-level semantics are an open section @@ -333,7 +348,10 @@ beside the artifact scope. Nothing else changes: sibling — is therefore judged by both. A dependent defining an object name X already owns, or a sibling defining a name the other sibling owns, is refused at install, ahead of any DDL, with the error `refuseCoOwnedObjectNameCollision` already raises for co-owners inside one artifact — `code` and `status` asserted, as - ADR-0130 D3's gate test requires. + ADR-0130 D3's gate test requires. The code stays `DUPLICATE_ARTIFACT_OBJECT_NAME`; the message, which today reads + "Object name conflict inside one release artifact … delivered by the same artifact" + (`packages/objectql/src/registry.ts#ArtifactObjectNameConflictError`), is worded per co-ownership source — same + artifact, declared dependency, sibling — so a cross-artifact refusal never claims one artifact. - **No new manifest field.** The proof is the declaration plus the install act. ADR-0130 D8 stands: no owner or publisher field on `ManifestSchema`. - **Direct declarations only.** The relation reads P's own `dependencies` and, for the sibling arm, S's own — never the @@ -354,11 +372,41 @@ beside the artifact scope. Nothing else changes: !coOwners.has(owner))`), and once revenue is installed it *is* a sitting owner of `crm`. The next upgrade or reinstall of HotCRM arrives second; read from P's side alone, HotCRM itself is refused `NAMESPACE_CONFLICT`, so HotCRM could never be upgraded once a module is installed. Boot order is the second reason: a rehydrate that registers P before X - would refuse the depended-on app the same way; whether today's paths can is the runtime card's first reading (Open - items 2). One consequence comes free: on such an upgrade the uniqueness check judges X against its co-owner P, so an - upgrade of HotCRM that newly defines a `crm_*` name revenue already owns is refused naming revenue — a small upgrade - guard D3's interception item does not otherwise give, and the Console's refusal text must say which module holds the - name. + would refuse the depended-on app the same way. One consequence comes free: on such an upgrade the uniqueness check + judges X against its co-owner P, so an upgrade of HotCRM that newly defines a `crm_*` name revenue already owns is + refused naming revenue — a small upgrade guard D3's interception item does not otherwise give, and the Console's + refusal text must say which module holds the name. +- **The depended-on app's companions must declare it.** The gate refuses any sitting owner outside the co-owner set, and + after install the registry does not record which artifact a package came from: `InstalledPackageSchema` carries + `manifest` and `source`, no artifact (`packages/spec/src/kernel/package-registry.zod.ts#InstalledPackageSchema`), and + the manifest service's scope is "nothing persisted — the claim cannot outlive … the artifact that IS the claim" + (`packages/objectql/src/plugin.ts`). So the platform cannot infer that service is sales' companion. HotCRM's artifact + holds more than one `crm` package — sales and service at least (seat input `6370024454`), marketing inside it per + hotcrm#2270 — and two cases fail without a declaration: a standalone revenue declaring only sales meets service as a + sitting owner of `crm` and is refused naming service; a later HotCRM upgrade re-registers service, which neither + declares revenue nor is declared by it, and the upgrade is refused naming revenue. **Precondition**: every package of + X's artifact that shares N with a module directly declares X (or the module declares each such package). Then the + sibling arm admits both ways — service and revenue both declare sales, which holds `crm`. Inside the artifact the + declaration costs nothing: D2's second arm judges it for presence only, and it gives ADR-0130 D5's sort a real edge + (service after sales) where today there is a tie. Not verified in HotCRM — Open items 7, the HotCRM lane's reading + before hotcrm#2271 ships. +- **Hydrate order is a requirement, not a reading.** A boot registers in whatever order its source lists: the + `sys_packages` hydrate iterates `packageService.list()` and, when `installPackage` throws, logs that the package "was + NOT hydrated into the registry" and leaves it absent for the process lifetime + (`packages/services/service-package/src/index.ts`); the install-local plugin's `kernel:ready` rehydrate registers its + ledger entries in `readdirSync` order (`packages/cloud-connection/src/local-manifest-source.ts#list`, + `packages/cloud-connection/src/marketplace-install-local-plugin.ts`). Two siblings hydrated before their parent Q meet + the gate with Q's namespace unreadable; the second is refused and never recovered — the recorded verdict AGENTS.md's + *Startup registry reads* section names as a defect. The same order breaks the two `kernel:ready` seals in + `packages/objectql/src/plugin.ts` (`picklistVocabularySealed`, `boundActionTargetsSealed`): once sealed, an arriving + artifact must bind its actions and picklists to an object the registry holds or it declares, so a standalone revenue + whose action targets a HotCRM object is refused if its ledger entry is read before HotCRM's. **Decided**: both hydrate + paths order their batch by `manifest.dependencies` through `resolvePluginOrder`, declared ids as optional edges — + ADR-0116's one sorter, the reading `packages/core/src/artifact-packages.ts#resolveArtifactPackageEntries` already gives + the key inside an artifact — before registering any of it; an id absent from the batch is not an edge. The sibling arm + keeps reading Q's namespace from Q's record; ⛔ not a reading that admits a sibling without Q, which would let any two + packages sharing a utility dependency co-own a namespace (merge question 12). What a sort inside each path cannot + order — a dependent on one of D2's three paths whose parent hydrates on a later one — is Open items 2. **ADR-0130 D8's deferral now ends at marketplace publishing**, not at a module's first standalone artifact. Within one tenant, D5 proves co-ownership without an owner field. Across vendors — the moment a module is published to the @@ -381,19 +429,20 @@ it. Renaming a module's objects to dodge the gate stays refused (ADR-0129; lette decision its "Shared/published data model + inter-App dependencies" bullet asked for, taken for the dependency half. The shared data model half (a platform-owned customer / supplier model — letter C on #25403) was not chosen and stays out of scope. -- **ADR-0130** gains an addendum (no original text edited) recording two things. First, D8's deferral now ends at - marketplace publishing (D5 above). Second, the 2026-09-02 addendum's *What was NOT decided* line listing +- **ADR-0130** gains a status-header pointer line (the form its 2026-09-22 line set) and an addendum, no original text + edited, recording two things. First, D8's deferral now ends at marketplace publishing (D5 above). Second, the + 2026-09-02 addendum's *What was NOT decided* line listing [#14488](https://github.com/objectstack-ai/objectstack/issues/14488) as "filed, not decided" is stale: #14488 was closed `not_planned` on 2026-09-12 by ruling [`5645322259`](https://github.com/objectstack-ai/objectstack/issues/14488#issuecomment-5645322259) — an artifact installs whole, the app package's sets cover every module's objects, nothing composes grants into another package's set, and a module carved out as its own artifact ships its own permission sets under ADR-0086 D4; the card is not reopened for - that case. That ruling's premise — 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 — is - **unchanged** by B′: an artifact still installs whole (ADR-0130 D2 stands), and a module shipped on its own is another - artifact, not a partial install of the first. B′ (「而每个包可以独立安装」) takes the path the ruling itself named for - that case — a module carved out as its own artifact ships its own permission sets — so the ruling's disposition stands - as written and is what D4 above relies on: a standalone module's permission sets are the module author's work, not a - platform blocker. + that case. That ruling's premise sentence — 「目前的要求是发布物是完整安装的,没有设计单独安装其中 一个模块的需求。」 — + has two halves. The first, an artifact installs whole, is **unchanged**: ADR-0130 D2 stands, and a module shipped on + its own is another artifact, not a partial install of the first. The second, that no requirement to install one module + alone existed, names the need B′ (「而每个包可以独立安装」) now states — met by another artifact, the carve-out path + the ruling itself named, never by a partial install. The ruling's disposition is unchanged and is what D4 above relies + on: a standalone module's permission sets are the module author's work, not a platform blocker. ### D7 — The first consumer: a HotCRM module shipping on its own, revenue first @@ -411,6 +460,19 @@ opened and uninstalled as one (D1, D4) — and D4's "set of namespaces" gains a registry's `Map>` already represents: uninstalling the module releases its own ownership and objects, not the namespace HotCRM still owns. +Three HotCRM-side preconditions, each **not re-verified** and each the HotCRM lane's to confirm before hotcrm#2271 ships: + +- **Companions declare the app.** Every `crm` package left in HotCRM's artifact — service, marketing — directly + declares sales (D5's precondition). Without it, revenue's install is refused at the namespace gate naming the + undeclared package, and so is HotCRM's next upgrade, naming revenue. +- **Hooks follow ownership.** `defineStack` refuses a hook on an object the stack does not carry (ADR-0130 §1.5, + `packages/spec/src/stack.zod.ts`), so the carved-out revenue cannot carry a hook on a sales-owned object; a hook that + must stay crosses only by staying in the package that owns the object. +- **Permission sets move with the objects.** Today the sales app's sets cover every module's objects (ruling + `5645322259`); after the carve-out the grants on revenue's objects ship in revenue's artifact, since nothing composes + into another package's set (ADR-0086 D4). D4's open question 2 covers only the reverse direction — a dependent's own + set granting on a dependency's objects. + Until the runtime card ships, composite products keep shipping as one artifact under ADR-0130. That is the state hotcrm#2270 packages revenue and marketing in; hotcrm#2271 moves revenue to its own artifact once the check is released. @@ -451,7 +513,8 @@ not a declaration, and `PackageDependencySchema`'s own header already tells the - An AI author's undeclared reference stays a loud `422` at the save door, and a missing dependency becomes a loud refusal at install, instead of copied objects that drift silently. - The work is small and stays small: no new authorable key, no schema change, one predicate at three doors, one more - source for one co-owner set (read two ways: a declared parent, a shared declared parent), one author-side lint rule. + source for one co-owner set (read two ways: a declared parent, a shared declared parent), one author-side lint rule, + and the one sorter applied inside two hydrate loops. - Two modules of one app install in either order and need not declare each other (the sibling arm), and an upgrade of the app that would take a module's object name is refused for free (D5). @@ -459,8 +522,11 @@ not a declaration, and `PackageDependencySchema`'s own header already tells the - Uninstalling or upgrading a depended-on app is not guarded (D3), and a `dependencies` entry edited in after install is not judged until the next install of that package. An administrator can strand a dependent until those items are built; the refusal at install is the only gate, and the refusal's message is the only guidance. -- The gate's range reader must grow a stated grammar before the first consumer's range is readable; until the author-side - rule exists, an unreadable range is refused at install, not reported at build (D2). +- No reader in the tree has both the documented range grammar and exact-version granularity; the gate needs one before + the first consumer's range is readable, and until the author-side rule exists an unreadable range is refused at + install, not reported at build (D2). +- D5 holds for a module only if its app's own companions declare the app (D5, D7) — a HotCRM-side precondition not yet + verified; without it the module's install and the app's next upgrade are both refused at the namespace gate. - A standalone module and its app have **separate versions** — the matrix ADR-0130 D6 removed inside one artifact returns across artifacts, bounded only by the declared range and judged only at install. - D5 lets a module share a vendor's namespace on the administrator's say-so. Inside one tenant that is the install choice @@ -501,10 +567,10 @@ The letters as filed on #25403 and on ruling `6377678758`: ## Open items (not blocking acceptance; each becomes a reading on its card) 1. **Record-level semantics across a dependency** — D4's three questions, measured on HotCRM's revenue module. -2. **Rehydrate order** — whether any boot path (`sys_packages`, the install-local ledger, code packages) can register a - dependent before the package it depends on. D5's read-from-either-end rule makes the answer harmless for a declared - pair; the sibling arm reads the parent's namespace from the parent's record, so a boot that registers two siblings - before their parent is the case the card measures first. +2. **Rehydrate order across paths** — D5 requires each hydrate path to sort its own batch. What the card measures: whether + a dependent on one of D2's three paths can have its parent on a later one (code packages at `init`/`start`, the + `sys_packages` hydrate, the install-local ledger at `kernel:ready`), and whether the `kernel:ready` seals in + `packages/objectql/src/plugin.ts` run before or after the ledger rehydrate on the same hook. 3. **Declared ids that are not installable packages.** The key's own documentation example is an npm-style id. The in-repo population is one manifest, co-delivered (Context §1). If an id that names no installable package turns up in HotCRM's manifests, the card reports it; D2 does not exempt it. @@ -515,12 +581,14 @@ The letters as filed on #25403 and on ruling `6377678758`: 6. **The carved-out module's `type`.** D7 states `type: 'app'`. The card confirms the artifact `os build` writes for a standalone module carries it, and reads what each install door does today with a top-level package of another type (not measured here). +7. **Companion declarations in HotCRM's artifact** — D5's precondition: service and marketing directly declare sales + (not verified; the HotCRM lane confirms it, with D7's hook and permission-set moves, before hotcrm#2271 ships). ## Open questions for the maintainer's merge decision These are the points this record decided where the rulings were silent, listed so the merge is an informed act. Merging -accepts them; an objection names the number. Items 1–4 and 11 shape the mechanism; 5–8 shape D2's refusal; 9 is a sizing -decision; 10 is a reading of an earlier ruling. +accepts them; an objection names the number. Items 1–4, 11 and 12 shape the mechanism; 5–8 shape D2's refusal; 9 is a +sizing decision; 10 is a reading of an earlier ruling. 1. **D2's second arm — a dependency delivered by the same artifact counts as present, and is judged for presence only.** Without the arm, judging the whole artifact before anything registers would refuse every ADR-0130 artifact whose @@ -538,17 +606,26 @@ decision; 10 is a reading of an earlier ruling. declared parent that holds the namespace, never the transitive closure the save door walks. Without the arm the second module of one app is not independently installable; with it there is no new field and no closure walk. Narrower is the reversible direction; widening later needs no undo. -5. **D2's status is 422** — fixed by analogy with `NAMESPACE_CONFLICT`, the sibling refusal at the same gate for a - conflict with the environment's state; the code itself goes to the ADR-0112 D3 ledger on the runtime card. +5. **D2's status is 422** — the registry gate's family: `NAMESPACE_CONFLICT`, `DUPLICATE_ARTIFACT_OBJECT_NAME` + (`packages/objectql/src/registry.ts`) and `INVALID_ARTIFACT_PACKAGES` / `INVALID_ARTIFACT_PACKAGE_ENTRY` + (`packages/core/src/artifact-packages.ts`) all answer 422 for a delivered manifest judged against the environment + ahead of registration, and D2 is one more such judgement. The same doors also answer 409 — install-local's + `RESOURCE_CONFLICT` for a namespace a writable base holds and `MANIFEST_CONFLICT` for an id local code defines + (`packages/cloud-connection/src/marketplace-install-local-plugin.ts`), and ADR-0029's uninstall refusal + `RESOURCE_CONFLICT` (`packages/objectql/src/registry.ts#PackageUninstallExtendedError`) — for a request that collides + with an existing resource's state and holds until that resource changes. D2 refuses the manifest as delivered, which + is the 422 family; the code itself goes to the ADR-0112 D3 ledger on the runtime card. 6. **D2 fails closed on an unparseable version or range, and the grammar is stated** — the opposite of `assertProtocolCompat` at the same doors, which admits an unrecognised `engines.protocol` range with a warning so that no artifact already in the field is refused by a parser gap. The reason the policies differ: the handshake grandfathers a fleet of existing artifacts, while a dependency entry is written today by the dependent's own author about one named package (the in-repo population is one manifest), and an unread bound would admit every version — the - one outcome the check exists to refuse. Because the tree's two readers accept less than the documented grammar (`6.x` - reads as nothing today), fail-closed is paired with a stated grammar on the runtime card and an author-side rule that + one outcome the check exists to refuse. Because no reader in the tree has both the documented grammar and + exact-version granularity (`6.x` reads as nothing to `SemanticVersionManager`; `rangeAdmitsMajor` reads it at major + granularity only), fail-closed is paired with a stated grammar on the runtime card and an author-side rule that reports an unreadable range before install — otherwise the ruling's own example would be refused at the gate it was - written for. + written for. The reader to extend is the handshake family in `packages/metadata-core`, not `SemanticVersionManager`, + which has no caller. 7. **Every record entry is required; the marketplace `optional` arm projects to `false`** — the record shape has no optional spelling, and inventing one would be the spec change this record forbids itself. 8. **Only P's own entries are judged** — a dependency's own dependencies were judged when it was installed; judging them @@ -558,15 +635,26 @@ decision; 10 is a reading of an earlier ruling. `sys_package_version.manifest_json` (`objectstack-ai/cloud`, not readable here). Changing the snapshot's shape before that reading could break the reader it is meant to satisfy; until then snapshots carry the record shape as they do today, no new harm. -10. **The reading of ruling `5645322259`** (D6; ADR-0130's addendum) — its premise is read as unchanged by B′: an - artifact installs whole (ADR-0130 D2), a standalone module is another artifact, and B′ takes the carve-out path the - ruling itself named. A reconciliation neither ruling on #25403 makes; recorded so a reader of both records does not - find them contradicting. +10. **The reading of ruling `5645322259`** (D6; ADR-0130's addendum) — its disposition is unchanged; the first half of + its premise, an artifact installs whole, is unchanged (ADR-0130 D2); the need its second half denied — installing one + module alone — is now met by another artifact, the carve-out path the ruling itself named, never by a partial + install. A reconciliation neither ruling on #25403 makes; recorded so a reader of both records does not find them + contradicting. 11. **The post-install edit of `dependencies` is deferred, not a fourth door** — `PATCH /api/v1/packages/:id` rewrites - the declaration after install with no in-use check, so an entry added there meets D2 only at the package's next - install. Running D2's predicate on that edit is one call at one more door; it is deferred (D3) because B′ named one - new gate, and the edit's practical case — an author declaring the package whose object the save door just refused — - names a package that is present. The maintainer may take the fourth door instead; it needs no undo. + the declaration whole after install with no in-use check, so an entry added there meets D2 only at the package's + next install, and an entry deleted there removes D5's proof of co-ownership until the next related registration + refuses. The door is narrower than it looks: it reaches only authored packages (`422 WRITABLE_PACKAGE_REQUIRED` for a + package provided by code or an installed app, ADR-0070 D2), so no marketplace-installed module meets it. Running D2's + predicate on that edit is one call at one more door; it is deferred (D3) because B′ named one new gate, and the + edit's practical case — an author declaring the package whose object the save door just refused — names a package + that is present. The maintainer may take the fourth door instead; it needs no undo. +12. **Each hydrate path sorts its batch by `manifest.dependencies`** — both boot re-registration paths order their + entries through `resolvePluginOrder` with declared ids as optional edges before registering any of them, rather + than giving the sibling arm a reading that admits a sibling without its parent present. The sort is ADR-0116's one + sorter, already reading this key inside an artifact; the alternative would let any two packages sharing a utility + dependency co-own a namespace. It also closes the `kernel:ready` seals' order hazard (a module's bound action + targeting its app's object). Cost: a sort inside two hydrate loops; a dependent whose parent hydrates on a later + path is the card's residual reading (Open items 2). --- @@ -582,9 +670,9 @@ decision; 10 is a reading of an earlier ruling. - **"`revenue` first per ADR-0130 §1.3(c)"** (ruling `6369704387`): §1.3(c) names CPQ as the module with no sellable unit; it does not order HotCRM's modules. Revenue first is HotCRM's own ordering (module-split plan, hotcrm#2271); §1.3(c) is the general consequence both serve. No decision moves. -- **#14488.** Closed `not_planned` 2026-09-12 by ruling `5645322259`, as the HotCRM seat's input 2 says. Its premise — - an artifact installs whole — is not moved by B′, which takes the carve-out path the ruling itself named; its - disposition stands (D6). +- **#14488.** Closed `not_planned` 2026-09-12 by ruling `5645322259`, as the HotCRM seat's input 2 says. Of its premise + sentence, the first half — an artifact installs whole — is unchanged; the need the second half denied is now met by + another artifact, the carve-out path the ruling itself named; its disposition stands (D6). - **ADR numbering.** `0136` and `0140` were taken by proposals that never landed — PR #18480 (closed 2026-09-18, superseded by the North Star) and PR #25618 (withdrawn 2026-10-24 by the maintainer's ruling in session) — and are not reused; no landed record or open pull request carries either number, and no open pull request carries a new record. @@ -595,12 +683,14 @@ decision; 10 is a reading of an earlier ruling. ## Execution plan (after acceptance) 1. **Runtime card (XS–S, `domain:services` / objectql), one PR**: D2's predicate at the three install doors, with the - `dependency_missing` issues, the ledger-registered code, the stated range grammar and the Console refusal text; D5's - dependency source for the co-owner set — declared parent and sibling arm, read from either end — with the ADR-0130 D3 - gate test extended to a cross-artifact pair and a sibling pair (a dependent defining an object name X owns, and a - sibling defining a name the other sibling owns, are refused, asserting `code` and `status`), and the upgrade case (X - reinstalled beside its module is admitted; X newly defining a name the module owns is refused); D8 items 2 and 3. - Its first readings: Open items 2, 3, 4 and 6. + `dependency_missing` issues, the ledger-registered code, the range reader (the handshake family in + `packages/metadata-core` extended to exact-version granularity, D2) with its stated grammar, and the Console refusal + text; D5's dependency source for the co-owner set — declared parent and sibling arm, read from either end — with the + ADR-0130 D3 gate test extended to a cross-artifact pair and a sibling pair (a dependent defining an object name X + owns, and a sibling defining a name the other sibling owns, are refused, asserting `code` and `status`, the message + worded per co-ownership source), and the upgrade case (X reinstalled beside its module is admitted; X newly defining a + name the module owns is refused); the dependency sort in the `sys_packages` and install-local rehydrates through + `resolvePluginOrder` (D5); D8 items 2 and 3. Its first readings: Open items 2, 3, 4 and 6. 2. **Author-side card (XS, `packages/cli` / `packages/lint`)**: the `os lint` / `os validate` rule that reports a `dependencies` range outside the stated grammar and, inside one artifact, a co-delivered entry whose range the sibling's declared version does not satisfy (D2). Released with or before (1), so the first refusal an author meets is @@ -611,8 +701,9 @@ decision; 10 is a reading of an earlier ruling. 4. **Docs**: the package-authoring guide states how to declare a dependency, the range grammar, what the install refusal means, that two modules of one app need not declare each other, and that `os validate` cannot see a dependency's objects. -5. **HotCRM** (its own lane): the D4 readings on the revenue module; the carved-out module's `type: 'app'` (D7); - hotcrm#2271 ships revenue as its own artifact once (1) is released. +5. **HotCRM** (its own lane): the D4 readings on the revenue module; D7's preconditions — service and marketing declare + sales (Open items 7), hooks follow ownership, revenue's permission sets travel with it — and the carved-out module's + `type: 'app'`; hotcrm#2271 ships revenue as its own artifact once (1) is released. ## References @@ -631,7 +722,8 @@ decision; 10 is a reading of an earlier ruling. - `packages/core/src/artifact-packages.ts`, `packages/core/src/plugin-order.ts`, `packages/core/src/dependency-resolver.ts`, `packages/metadata-core/src/artifact-forward-conversion.ts` - `packages/runtime/src/domains/packages.ts`, `packages/cloud-connection/src/marketplace-install-local-plugin.ts`, - `packages/services/service-package/src/index.ts`, `packages/lint/src/rule-explanations.ts` + `packages/cloud-connection/src/local-manifest-source.ts`, `packages/services/service-package/src/index.ts`, + `packages/lint/src/rule-explanations.ts` (`list-view-object-outside-closure`), `packages/spec/src/stack.zod.ts` (`composeStacks`), `packages/cli/src/utils/protocol-version-gap.ts` (`checkProtocolVersionGap`) - `examples/app-multi-package/src/packages/orders/index.ts`, `examples/app-multi-package/src/packages/core/index.ts` From 02456273c9a1202c6ddf36e5e3daec634d833869 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 10:27:41 +0000 Subject: [PATCH 7/7] =?UTF-8?q?docs(adr):=20ADR-0141=20patch=20round=205?= =?UTF-8?q?=20=E2=80=94=20delta=20review=20N1=E2=80=93N4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N1 D5 hydrate sort: a cycle in a batch degrades its cyclic members to source order with a logged refusal and never throws out of the hydrate; resolveArtifactPackageEntries' artifact-level cycle throw is not inherited (both hydrate paths stay per-entry tolerant). Merge question 12 and Execution plan card 1 say the same. N2 Install-local sort nodes are ledger entries: a declared id resolves to the entry carrying that package; ids inside one entry are not edges. N3 D2 reader: the exact-version reader is a sibling function in the handshake's module; the handshake's verdicts and ADR-0087 D1's never-false-rejection contract are unchanged. N4 Runtime card restated as S (the ruling's XS–S cited as the ruling's estimate for the check alone) in the Execution plan, merge question 9 and Consequences. Claude-Session: https://claude.ai/code/session_01HwUcNzGjPhV6xtYkJ469qK Co-authored-by: Claude --- ...endency-declared-checked-before-install.md | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md index 1f063a1dad9..9e654b3b239 100644 --- a/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md +++ b/docs/adr/0141-inter-app-dependency-declared-checked-before-install.md @@ -268,7 +268,10 @@ The runtime card gives **one** reader both properties — the handshake family's and **states the grammar the check supports**: at least `^`, `~`, `>=`, an `x` wildcard, `||` and the space intersection, so the ruling's own 「HotCRM 6.x」 is readable. The drafter's choice is to extend the handshake family in `packages/metadata-core`, on which every install door already depends and beside whose `assertProtocolCompat` the check -sits; ⛔ never a fourth reader. And the author side catches an unreadable `dependencies` range **before** install: an +sits — as a **sibling function in the same module**, sharing the alternative and comparator parsing, so the handshake's +own verdicts (compatible, incompatible, admitted with a warning on `no-range` / `unparsed-range`) and ADR-0087 D1's +never-false-rejection contract are unchanged; ⛔ never a fourth reader. And the author side catches an unreadable +`dependencies` range **before** install: an `os lint` / `os validate` rule beside the `engines.protocol` reading `os validate` already performs (`packages/cli/src/utils/protocol-version-gap.ts#checkProtocolVersionGap`) reports a range outside the stated grammar and, inside one artifact, a co-delivered entry whose range the sibling's declared version does not satisfy. Fail-closed @@ -403,9 +406,18 @@ beside the artifact scope. Nothing else changes: whose action targets a HotCRM object is refused if its ledger entry is read before HotCRM's. **Decided**: both hydrate paths order their batch by `manifest.dependencies` through `resolvePluginOrder`, declared ids as optional edges — ADR-0116's one sorter, the reading `packages/core/src/artifact-packages.ts#resolveArtifactPackageEntries` already gives - the key inside an artifact — before registering any of it; an id absent from the batch is not an edge. The sibling arm - keeps reading Q's namespace from Q's record; ⛔ not a reading that admits a sibling without Q, which would let any two - packages sharing a utility dependency co-own a namespace (merge question 12). What a sort inside each path cannot + the key inside an artifact — before registering any of it; an id absent from the batch is not an edge. In the + install-local path the nodes are ledger entries, each an artifact that may carry several packages: a declared id first + resolves to the entry that carries that package, and ids declared between packages of one entry are not edges between + entries. **A cycle never costs the batch.** `packages/core/src/plugin-order.ts#resolvePluginOrder` throws on a cycle + through either edge kind, and `resolveArtifactPackageEntries` keeps that throw for the packages of one artifact; a + hydrate does not inherit it. A cycle is reachable here — two authored packages whose `PATCH` edits declare each other, + two marketplace artifacts that declare each other — and both hydrates are per-entry today ("one stale package must not + brick the environment", `packages/services/service-package/src/index.ts`), so a sort that threw outside the loop would + register nothing, worse than today. The cyclic members degrade to source order for that batch, with the cycle logged + as a refusal naming them; the acyclic rest keeps its sorted order, and nothing throws out of the hydrate. The sibling + arm keeps reading Q's namespace from Q's record; ⛔ not a reading that admits a sibling without Q, which would let any + two packages sharing a utility dependency co-own a namespace (merge question 12). What a sort inside each path cannot order — a dependent on one of D2's three paths whose parent hydrates on a later one — is Open items 2. **ADR-0130 D8's deferral now ends at marketplace publishing**, not at a module's first standalone artifact. Within one @@ -512,9 +524,9 @@ not a declaration, and `PackageDependencySchema`'s own header already tells the (this record), a cross-package reference is legal exactly when it is declared. - An AI author's undeclared reference stays a loud `422` at the save door, and a missing dependency becomes a loud refusal at install, instead of copied objects that drift silently. -- The work is small and stays small: no new authorable key, no schema change, one predicate at three doors, one more - source for one co-owner set (read two ways: a declared parent, a shared declared parent), one author-side lint rule, - and the one sorter applied inside two hydrate loops. +- The work is small and stays small — one runtime card of size S and two smaller ones: no new authorable key, no schema + change, one predicate at three doors, one more source for one co-owner set (read two ways: a declared parent, a shared + declared parent), one author-side lint rule, and the one sorter applied inside two hydrate loops. - Two modules of one app install in either order and need not declare each other (the sibling arm), and an upgrade of the app that would take a module's object name is refused for free (D5). @@ -630,8 +642,9 @@ sizing decision; 10 is a reading of an earlier ruling. optional spelling, and inventing one would be the spec change this record forbids itself. 8. **Only P's own entries are judged** — a dependency's own dependencies were judged when it was installed; judging them again would re-run D2 for packages the administrator already admitted. -9. **D8 item 1's record-to-array conversion is its own card**, not the runtime card's — the ruling's sizing (XS–S for the - check, S for the objectql source) is kept, and the conversion waits on one reading: how the control plane parses +9. **D8 item 1's record-to-array conversion is its own card**, not the runtime card's — the ruling's own estimate (XS–S + for the check, S for the objectql source) is cited as the ruling's; the runtime card is **S** for the reasons the + Execution plan states, and the conversion would push it past that. The conversion waits on one reading: how the control plane parses `sys_package_version.manifest_json` (`objectstack-ai/cloud`, not readable here). Changing the snapshot's shape before that reading could break the reader it is meant to satisfy; until then snapshots carry the record shape as they do today, no new harm. @@ -653,8 +666,10 @@ sizing decision; 10 is a reading of an earlier ruling. than giving the sibling arm a reading that admits a sibling without its parent present. The sort is ADR-0116's one sorter, already reading this key inside an artifact; the alternative would let any two packages sharing a utility dependency co-own a namespace. It also closes the `kernel:ready` seals' order hazard (a module's bound action - targeting its app's object). Cost: a sort inside two hydrate loops; a dependent whose parent hydrates on a later - path is the card's residual reading (Open items 2). + targeting its app's object). The sorter's cycle throw is not inherited: cyclic members of a batch fall back to + source order with a logged refusal, and the hydrate never throws — the per-entry tolerance both paths have today + stands. Cost: a sort inside two hydrate loops; a dependent whose parent hydrates on a later path is the card's + residual reading (Open items 2). --- @@ -682,15 +697,17 @@ sizing decision; 10 is a reading of an earlier ruling. ## Execution plan (after acceptance) -1. **Runtime card (XS–S, `domain:services` / objectql), one PR**: D2's predicate at the three install doors, with the - `dependency_missing` issues, the ledger-registered code, the range reader (the handshake family in - `packages/metadata-core` extended to exact-version granularity, D2) with its stated grammar, and the Console refusal - text; D5's dependency source for the co-owner set — declared parent and sibling arm, read from either end — with the - ADR-0130 D3 gate test extended to a cross-artifact pair and a sibling pair (a dependent defining an object name X - owns, and a sibling defining a name the other sibling owns, are refused, asserting `code` and `status`, the message - worded per co-ownership source), and the upgrade case (X reinstalled beside its module is admitted; X newly defining a - name the module owns is refused); the dependency sort in the `sys_packages` and install-local rehydrates through - `resolvePluginOrder` (D5); D8 items 2 and 3. Its first readings: Open items 2, 3, 4 and 6. +1. **Runtime card (S, `domain:services` / objectql), one PR** — the ruling estimated XS–S for the check alone; the card + as this record leaves it is S: D2's predicate at the three install doors, with the `dependency_missing` issues, the + ledger-registered code, the range reader (a sibling of the handshake family in `packages/metadata-core`, exact-version + granularity, the handshake's verdicts untouched, D2) with its stated grammar, and the Console refusal text; D5's + dependency source for the co-owner set — declared parent and sibling arm, read from either end — with the ADR-0130 D3 + gate test extended to a cross-artifact pair and a sibling pair (a dependent defining an object name X owns, and a + sibling defining a name the other sibling owns, are refused, asserting `code` and `status`, the message worded per + co-ownership source), and the upgrade case (X reinstalled beside its module is admitted; X newly defining a name the + module owns is refused); the dependency sort in the `sys_packages` and install-local rehydrates through + `resolvePluginOrder` (D5), ledger entries as nodes, a cycle degrading to source order with a logged refusal and never + throwing out of the hydrate; D8 items 2 and 3. Its first readings: Open items 2, 3, 4 and 6. 2. **Author-side card (XS, `packages/cli` / `packages/lint`)**: the `os lint` / `os validate` rule that reports a `dependencies` range outside the stated grammar and, inside one artifact, a co-delivered entry whose range the sibling's declared version does not satisfy (D2). Released with or before (1), so the first refusal an author meets is