Skip to content

Commit 2a6a28d

Browse files
feat(objectql)!: a picklist, mapping or dataset name holds one package (bare-name phase 1, 1/4) (#30836)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DTo6jRMNdsU1vzk7FgXhuZm)_ domain:server#3 · os-elon-musk · session_016WV4d1EuHiCqvh8VEz81QD Refs #30803 (PR 1 of 4; the last PR of the series carries `Fixes`) Clause-②: no (narrowing) — the engine's package door (`SchemaRegistry.installPackage`) and item seam (`SchemaRegistry.registerItem`) refuse, after the cold boot, a package registration they accepted before: one whose picklist, mapping or dataset bears a name another installed package holds. Nothing is widened. Metadata-protocol: narrowed Skills-docs: none — `skills/**` does not teach that two packages may share a picklist, mapping or dataset name, and the naming rule is phase 2's (#30805); the prescription ships in the refusal and warning text and in ADR-0048's 2026-11-06 addendum. ⚠️ **Tier H**: this diff touches `docs/adr/0048-cross-package-metadata-collision.md` (§3.4 narrowing note + addendum). The seat routes the approver. ## 维护者速读 - 改了什么:按裁决 C 第一阶段,picklist、mapping、dataset 三类名字一个部署只能有一个包持有。冷启动结束(`kernel:bootstrapped`)后,第二个包再声明同名项,在安装入口和注册入口都被拒(422 `NAMESPACE_CONFLICT`),提示里点出两个持有包和改名建议(命名空间前缀,如 `beta_status`)。 - 为什么:实测两包同名时,字段的 picklist、导入的 mappingName、仪表盘的 datasetName 都不带包上下文,谁先注册就给谁,另一个包被默默错配(picklist 甚至拒绝自己包的取值)。 - 冷启动:已经同名的部署照常启动,只打一条告警,点名两个持有包和改名;开关 `BARE_NAME_COLD_BOOT_MODE` 默认 `'warn'`,等你裁决是否改成拒绝启动。 - 不变的:同一包重装或热重载;环境里同名覆盖(ADR-0005 覆盖层,从不算持有者);其它类型(page、按对象的 action、job 等)继续共存;`OS_METADATA_COLLISION=warn` 不降级。 - 先看:`packages/objectql/src/bare-name-one-holder.ts`(规则与开关)、`registry.ts` 的 `refuseBareNameConflicts` / `sealBareNameHolders`、`plugin.ts` 的 `kernel:bootstrapped` 封印、ADR-0048 新附录。 - 需要你做的:ADR 改动是 Tier H,需要审批人批准;冷启动拒绝还是告警,等你一句话。 ## What changes - **`packages/objectql/src/bare-name-one-holder.ts`** (new) — the rule and its vocabulary, the security catalog's sibling (`security-catalog-namespace.ts`) generalised: the covered types (`picklist`, `mapping`, `dataset`), the declared names a manifest carries (`declaredBareNames`, the engine's own collection sources, nested `plugins[]` one level), the holders (a package; a built-in, none today), the rename (`suggestedBareNameRename`: the namespace prefixed), and the one cold-boot switch `BARE_NAME_COLD_BOOT_MODE` (`'warn'`). - **`packages/objectql/src/registry.ts`** — - `BareNameConflictError`: the ADR-0112 envelope (`code: 'NAMESPACE_CONFLICT'`, `status: 422`), every conflict in one refusal, `conflicts[]` of `{ type, name, incomingPackageId, existingHolder, rename }`. - The package door: `installPackage` asks `refuseBareNameConflicts` ahead of every mutation (after the security catalog's and the default record page's refusals), records the package's claims, and `uninstallPackage` forgets them. No co-owner exemption, no `collisionPolicy` downgrade. - The item seam: `registerItem` refuses a package-bound picklist, mapping or dataset over another package's name. - One predicate for both doors (`bareNameHoldersOtherThan`): empty until sealed, empty when the incoming package already holds the name; holders are composite `<packageId>:<name>` slots in serving order plus install claims. ⛔ The bare slot (the environment catalog) is never read. - `sealBareNameHolders()` (the seal + every name the boot left held twice, sorted) and the module-level `sealBareNameHoldersAtColdBoot(registry, mode, logger)` (warn once, or refuse with `door: 'cold-boot'`). - **`packages/objectql/src/plugin.ts`** — the engine plugin seals at `kernel:bootstrapped` (after the `kernel:ready` install-local rehydrate), under `BARE_NAME_COLD_BOOT_MODE`. - **`packages/objectql/src/security-catalog-namespace.ts`** — one header sentence ("for every other type §3.4's coexistence stands") points at the sibling. - **`docs/adr/0048-cross-package-metadata-collision.md`** (Tier H) — a "Narrowed (2026-11-06)" note under §3.4, an Addenda-line entry, and the addendum: ruling C's text and phasing verbatim (record 6676670839), the measurement, what is refused, the cold boot and its switch, what stays as §3.4 has it, the types to follow, where it is implemented. It states that after phase 3 the refusal remains only as the backstop for namespace-less packages, the shared `base`/`system`/`sys` namespaces and third-party packages in the deprecation window. - **`scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json`** — the anchor. - **`.changeset/30803-bare-name-one-package-holder.md`** — `@objectstack/objectql` `minor`, both declaration lines, BREAKING Before / Now / Who moves / Unchanged, `adr-0087: not-required (no-migration-prescription)`, measured producers. ## Why (measured, stage 1) Booted showcase + two `AppPlugin` packages sharing one bare name per type, both registration orders, on `c17719dd` (`stage1-measurement.md` in the seat's project folder): | Type | Consumer | Served | Warned | |:--|:--|:--|:--| | `picklist` | field `picklist: NAME`, `/meta/object` options + write validator | first-registered list for BOTH objects; the second package's own values refused `VALIDATION_FAILED` | no | | `mapping` | `POST /data/OBJ/import` `{ mappingName }` | first-registered; the other package's import `400 MAPPING_TARGET_MISMATCH` | no | | `dataset` | `POST /analytics/dataset/query` `{ datasetName }` | first-registered | no | ## Verification - `packages/objectql/src/registry-bare-name-one-holder.test.ts` (new, 40 cells, real `ObjectKernel` + `ObjectQLPlugin`, the `manifest` service door): per type × door — package door (both holders, rename, no record left), item seam, nested plugin, uninstall releases, same-package reload (CONTROL), environment registration never judged and never a holder (CONTROL); per type at the cold boot — boots and warns once naming both holders and the rename; both holders keep their name across a reload while a third is refused naming both; `'refuse'` refuses with the doors' envelope; the cold boot's reading (served holder as existing, sorted, one warning); no-clash boot is silent; every conflict in one refusal; no namespace, no rename; a built-in name; CONTROLs: a non-covered type (`page`) coexists, a picklist extension is no holder, `collisionPolicy: 'warn'` does not downgrade. - `packages/qa/dogfood/test/bare-name-one-holder.dogfood.test.ts` (new, booted showcase + two `AppPlugin` packages + `MarketplaceInstallLocalPlugin`): the cold boot boots and logs ONE `WARN` line naming all three names, both holders and the renames; `POST /marketplace/install-local` of a third package declaring the held picklist name is `422 PLUGIN_REGISTER_FAILED` naming both holders and `gamma_…`, package list unchanged; a package redeclaring the showcase's mapping and dataset names is refused naming `com.example.showcase`; CONTROL: fresh names install. 4 passed. - Ablation (src): the engine plugin's seal call removed → 19 of the 40 unit cells red (every door refusal and the cold-boot warning), the 21 ablation/control cells stay green; restored. Ablation (dist): the dogfood file run against the pre-change `@objectstack/objectql` dist (dogfood imports it through dist) → 3 of 4 red (no warning line; both install-local cases `200`), the fresh-names CONTROL green; green after rebuilding the dist. - `@objectstack/objectql` full suite: 556 files / 11108 tests passed; `pnpm --filter @objectstack/objectql typecheck` (incl. the test-typecheck ratchet) exit 0. - Gates (`dispatch-gates --commands`, 81 derived + 5 roster families, reconciled with `--ran`) and the full dogfood battery at this head (`75d1339f`, `origin/main` merged): both were running when this PR opened; their results are in the os-dev report on #30803. ## Kept (the card's three) - Same-name actions on different objects stay keyed `objectName:name`; jobs stay keyed `appId:jobName` (`app-artifact-handlers.jobs.test.ts`); the unmanaged import's all-type refusal is untouched. Every existing security-catalog pin is unchanged (`registry-security-catalog-namespace.test.ts` and its siblings green in the full suite). ## Series and out of scope - PR2–PR4 (same mechanism, own pins, §3.4's "to follow" list): `api`, object-less `action`, engine function registry; `email_template`, `webhook`, `connector`, `flow`; `agent`, `skill`. A type the security seat claims lands there instead (asked of the maintainer by the seat). - **Candidates sharing a reader, not measured:** `tool` and `rag_pipeline` register by name like `agent` at the artifact door; included only on the seat's word. - **Cloud carrier note (not filed):** the AI runtime consumers of agent/skill/tool names (`service-ai`: agents, skills, `action_NAME` tool names, ToolRegistry) live in `objectstack-ai/cloud`, not in this repo, and were NOT MEASURED here. - Not refused: `analytics_cube` (a non-host package's cube never reaches the analytics service, 404 `CUBE_NOT_FOUND` both orders: a separate reach gap). ## Deviations - The seal lives in the engine plugin's `kernel:bootstrapped` hook, not the registry's own doors: during a boot every package registers again, so the doors judge nothing until every package of the boot has registered. A kernel with `bootPhaseHooks: false` never seals (its doors never refuse); stated in the module doc and the ADR. - Unlike the security catalog, the environment catalog is never a holder here: an environment item under a package's name is the ADR-0005 overlay (the seat's ruling 1). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request (first of a series by type family, per the seat). - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the maintainer brief is in Chinese by the seat's convention). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4d1EuHiCqvh8VEz81QD)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1697d5c commit 2a6a28d

10 files changed

Lines changed: 1366 additions & 17 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
feat(objectql)!: a picklist, mapping or dataset name holds one package — once the cold boot has sealed the registry, a package declaring a name another installed package already holds is refused at install and registration, naming both holders and the rename; a boot that already holds such a clash warns
6+
7+
Clause-②: no (narrowing) — the engine's package door (`SchemaRegistry.installPackage`) and item seam (`SchemaRegistry.registerItem`) refuse, after the cold boot, a package registration they accepted before: one whose picklist, mapping or dataset bears a name another installed package holds. Nothing is widened.
8+
Metadata-protocol: narrowed
9+
Skills-docs: none — `skills/**` does not teach that two packages may share a picklist, mapping or dataset name, and the naming rule itself is phase 2's (#30805); the prescription ships in the refusal and warning text and in ADR-0048's 2026-11-06 addendum.
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) the refusal removes no key, export or field and changes the shape of no stored body; what an author does about a refused name is pick a different one, which no conversion can choose for them -->
12+
13+
**BREAKING** accept-set narrowing at the package registration door: managed install (`install-local`, a post-start `manifest.register`, a hot reload) and package registration (`ObjectQL.registerApp` and every package-bound `registerItem`), for picklists, mappings and datasets. Maintainer ruling C on #30798, phase 1; ADR-0048 §3.4 narrowed (addendum 2026-11-06).
14+
15+
- **Why.** A field's `picklist: NAME`, an import's `mappingName` and a dashboard's `datasetName` are resolved with no package. Measured on a booted showcase with two packages sharing one bare name per type, in both registration orders: the picklist served both packages' objects the first-registered package's options (the other package's own values refused `VALIDATION_FAILED`), the other package's import answered `400 MAPPING_TARGET_MISMATCH`, and a dataset query ran the first-registered package's dataset. Each answer flipped with registration order, and nothing warned.
16+
- **Before.** Two installed packages could each ship a picklist, mapping or dataset under one name, at boot or by a later install; every caller was served whichever registered first.
17+
- **Now.** After the cold boot (the engine plugin seals the registry at `kernel:bootstrapped`, once every package of the boot has registered), a package registration declaring such a name another installed package holds is refused ahead of every mutation, so it leaves no record behind. The envelope is ADR-0112's: `code: 'NAMESPACE_CONFLICT'` (`NAMESPACE_CONFLICT_CODE`, exported) and `status: 422`; `install-local`'s inline import answers `422 PLUGIN_REGISTER_FAILED` with this message. The message lists every conflicting name, names both holders, and carries the rename that clears it: the incoming package's namespace prefixed to the name (`beta_status`). The thrown error carries `conflicts[]` with `{ type, name, incomingPackageId, existingHolder, rename }`. A third package declaring a name two packages already hold is refused naming both.
18+
- **The cold boot.** A deployment whose installed packages already share such a name still boots: one warning line names every such name, both holders and the rename, and the first-registered holder keeps being served. Whether that boot should be refused instead is the maintainer's open call; the switch (`BARE_NAME_COLD_BOOT_MODE`, `'warn'`) sits in `bare-name-one-holder.ts`.
19+
- **Who moves.** An author whose package declares a picklist, mapping or dataset under a name another package installed beside it already ships, at the next install or upgrade into a running deployment. **The one-line fix: rename the item in one of the two packages — the refusal names the namespace-prefixed name — or uninstall one of them.** A field, import or dashboard that named the old name must name the new one; nothing rewrites stored references.
20+
- **Unchanged.** The same package registering its own name again (a reload, a re-install, an upgrade, including a package that came through the boot beside another holder). An environment item under a package's name: a registration with no package (every `sys_metadata` hydration and metadata write-through) is never judged, and the environment catalog is never a holder — it is the ADR-0005 overlay. A picklist extension of another package's list. Every other metadata type, which keeps ADR-0048 §3.4's coexistence (`page/home` in two packages, object-bound actions keyed `objectName:name`, jobs keyed `appId:jobName`). `OS_METADATA_COLLISION=warn` does not downgrade the refusal. A kernel that withholds the boot-phase hooks never seals, so its doors never refuse.
21+
22+
**Measured producers.** On objectstack `c17719dd`, the four examples (`app-crm`, `app-showcase`, `app-todo`, `app-multi-package`) carry 7 picklist, mapping and dataset declarations, and no name has more than one holder; the platform's own packages declare none. HotCRM `6961ab1` (five packages, one artifact) carries 5 picklists, 3 mappings and 2 datasets, and no name has more than one holder. The two sets were measured separately, and two names coincide across them: the datasets `task_metrics` (HotCRM `src/sales/datasets/` and `app-todo`) and `opportunity_metrics` (HotCRM and `app-crm`). No shipped composition installs HotCRM beside an example, so nothing shipped is refused; an environment that installs HotCRM beside `app-todo` or `app-crm` is a Who-moves case above. Deployed and marketplace packages NOT MEASURED.

‎docs/adr/0048-cross-package-metadata-collision.md‎

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
**Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (package as first-class citizen), [ADR-0005](./0005-metadata-customization-overlay.md) (artifact vs runtime overlay precedence), [ADR-0008](./0008-metadata-repository-and-change-log.md) (metadata repository, `MetaRef` identity), [ADR-0010](./0010-metadata-protection-model.md) (package provenance / `_packageId` stamping)
66
**Consumers**: `@objectstack/objectql` (`SchemaRegistry.registerItem`, `ObjectQL.registerApp`, install path), `@objectstack/console` / `objectui` (routing + metadata resolution), package authors, CLI/CI install path, the app marketplace registry.
77
**Surfaced by**: ADR-0046 review (doc naming) → generalised to all bare-named metadata → re-examined through the app-marketplace install lens.
8-
**Addenda**: [2026-08-08 — publish-time / marketplace namespace exclusivity](#addendum-2026-08-08-publish-time--marketplace-namespace-exclusivity) (**Accepted** 2026-08-08 — maintainer ruling on #1825, [acceptance comment 5226411687](https://github.com/objectstack-ai/objectstack/issues/1825#issuecomment-5226411687), landed by PR #6671; additive to §3.2, supersedes nothing).
8+
**Addenda**: [2026-08-08 — publish-time / marketplace namespace exclusivity](#addendum-2026-08-08-publish-time--marketplace-namespace-exclusivity) (**Accepted** 2026-08-08 — maintainer ruling on #1825, [acceptance comment 5226411687](https://github.com/objectstack-ai/objectstack/issues/1825#issuecomment-5226411687), landed by PR #6671; additive to §3.2, supersedes nothing); [2026-11-06 — bare names resolved without a package hold one package](#addendum-2026-11-06-bare-names-resolved-without-a-package-hold-one-package--34-narrowed) (**Accepted**, ruling C on #30798, phase 1; narrows §3.4 for picklists, mappings and datasets, supersedes nothing).
99

1010
---
1111

@@ -266,6 +266,21 @@ package — stays covered by the `naming/namespace-prefix` lint in `os lint`.
266266
> above. Every other type keeps §3.4 as written. See
267267
> [Addendum (2026-10-08)](#addendum-2026-10-08-the-security-catalog-holds-one-name-per-deployment--34-narrowed).
268268
269+
> **Narrowed (2026-11-06) — picklists, mappings and datasets are out of §3.4.**
270+
> Their consumers resolve the name with no package — a field's `picklist`, an
271+
> import's `mappingName`, a dashboard's `datasetName` — so with two holders every
272+
> caller is served the first-registered one, measured on a booted two-package
273+
> deployment (#30803). Once the cold boot has sealed the registry, a package whose
274+
> picklist, mapping or dataset bears a name another installed package holds is
275+
> refused at managed install and at package registration (a built-in's name, at
276+
> managed install), naming both holders and the rename. ⛔ The environment catalog is never a
277+
> holder: an environment item under a package's name is the ADR-0005 overlay.
278+
> The other types the measurement found resolving without a package context —
279+
> `api`, the object-less `action`, the engine function registry, `email_template`,
280+
> `webhook`, `connector`, `flow`, `agent` and `skill` — are to follow, each in its
281+
> own change. Every other type keeps §3.4 as written. See
282+
> [Addendum (2026-11-06)](#addendum-2026-11-06-bare-names-resolved-without-a-package-hold-one-package--34-narrowed).
283+
269284
### 3.5 Namespace rename-on-install is deferred (non-goal)
270285

271286
Renaming a colliding namespace on install would require rewriting **every**
@@ -869,3 +884,134 @@ for a package-bound registration that reaches the registry directly. Every packa
869884
registration reaches the package door before any in-memory registrar runs, so the
870885
boot of a conflicting composition — an artifact boot included — is refused in its
871886
first phase.
887+
888+
---
889+
890+
## Addendum (2026-11-06): bare names resolved without a package hold one package — §3.4 narrowed
891+
892+
> **Status of this addendum: Accepted** — it records phase 1 of a maintainer
893+
> ruling. It **narrows §3.4 for three more metadata types and supersedes no other
894+
> text**: §§1–6, the 2026-08-08 addendum and the 2026-10-08 addendum stand
895+
> verbatim.
896+
>
897+
> **Ruling this addendum records** — #30798, answered by the maintainer
898+
> 「同意 c」 (ruling record 6676670839, 2026-11-06). The ruled option's text,
899+
> verbatim:
900+
>
901+
> > **C. Every name carries the namespace** — Extend ADR-0129's literal prefix to
902+
> > every package-authored top-level metadata name (`crm_send_email`), except
903+
> > names already derived from a prefixed object (fields, object extensions,
904+
> > container views) or already namespaced (translations `<ns>_<locale>`);
905+
> > enforced at `defineStack`/`os validate` and the save door; same-name clashes
906+
> > become impossible by construction
907+
>
908+
> and its phasing, verbatim: "**Now:** extend the existing registration refusal
909+
> (A's mechanism) to the silent types. It closes silent mis-resolution for
910+
> packages already in the field. After phase 3 it remains only as the backstop
911+
> for the cases C cannot reach: packages that declare no namespace, the shared
912+
> `base`/`system`/`sys` namespaces, and third-party packages inside the
913+
> deprecation window." Phase 1 is #30803; the lint, the templates and the skills
914+
> are phase 2 (#30804, #30805); `defineStack`, the save door and the ADR-0129
915+
> amendment are phase 3 (#30806).
916+
917+
### P1.1 Why §3.4's premise does not hold for these three types
918+
919+
§3.4 rests on every caller carrying its package id. These three consumers carry
920+
none when they resolve the name. Measured on a booted showcase with two packages,
921+
`alpha` and `beta`, each shipping one item per type under the same bare name, in
922+
both registration orders (#30803, stage 1):
923+
924+
- **`picklist`** — a field's `picklist: NAME` resolves the first-registered
925+
package's list for both packages' objects: the second package's object is
926+
served the other package's options, and its own values are refused at write
927+
(`VALIDATION_FAILED`).
928+
- **`mapping`** — an import's `mappingName` resolves the first-registered
929+
package's mapping; the other package's import answers `400
930+
MAPPING_TARGET_MISMATCH`.
931+
- **`dataset`** — a dashboard's `datasetName` query runs the first-registered
932+
package's dataset.
933+
934+
Each answer flips with registration order, and none warns.
935+
936+
### P1.2 What is refused
937+
938+
Once the cold boot has sealed the registry (P1.3), a package registration is
939+
refused when a picklist, mapping or dataset it declares bears a name another
940+
holder already holds — at managed install (the package door,
941+
`SchemaRegistry.installPackage`, ahead of every mutation) and at package
942+
registration (the item seam, `SchemaRegistry.registerItem`, for a package-bound
943+
registration that reaches the registry directly). The holders:
944+
945+
- **an installed package** — including a disabled one, which is still installed
946+
and still holds its names; a nested plugin declares under its parent package;
947+
- **a built-in** — none today for these types; the vocabulary keeps the holder so
948+
a platform-held name is refused the way the security catalog refuses one, and,
949+
as there, only the package door asks it.
950+
951+
⛔ **Never the environment catalog** — the one difference from N.2. For these
952+
types an item authored in this environment under a package's name is the ADR-0005
953+
overlay of that package's item, not a second holder; a registration with no
954+
package is never judged, and the registry's bare slot is never read as a holder.
955+
956+
The error is N.2's envelope (`422`, the §3.2 namespace gate's code), lists every
957+
conflicting name in one refusal, names both holders, and carries the rename that
958+
clears it: the incoming package's namespace prefixed to the name (`beta_status`),
959+
the one naming rule ruling C ends at. A package with no namespace, or one in a
960+
shared `base`/`system`/`sys` namespace, is refused without a rename.
961+
962+
### P1.3 The cold boot
963+
964+
Every package registers again at every boot, through the same two doors, so a
965+
deployment that already holds a clash would stop booting if the doors refused
966+
then. During a boot the doors therefore judge nothing. Once every package of the
967+
boot has registered — `kernel:bootstrapped`, after the `kernel:ready` rehydrates —
968+
the engine plugin seals the registry, and every name two installed packages
969+
already hold is handled by one switch, `BARE_NAME_COLD_BOOT_MODE`:
970+
971+
- **`'warn'`** — one warning line naming every such name, both holders and the
972+
rename; the boot continues and keeps serving the first-registered holder;
973+
- **`'refuse'`** — the boot is refused with the P1.2 envelope, as N.3's
974+
amendment refuses the security catalog's.
975+
976+
The switch is `'warn'` until the maintainer rules on the cold boot of a
977+
deployment that already holds a clash. In either mode the doors refuse a NEW
978+
holder after the seal. A package that already holds a name is never a new holder
979+
of it: a reload, a re-install or an upgrade of a package that came through the
980+
boot beside another holder keeps its name, while a third package is refused,
981+
naming both. A kernel that withholds the boot-phase hooks never seals, and there
982+
the doors never refuse.
983+
984+
### P1.4 What stays as §3.4 has it
985+
986+
- **Same-package re-registration** is one holder, not two.
987+
- **Every type not listed** keeps §3.4's coexistence: two packages' `page/home`
988+
still coexist, an object-bound action stays keyed `objectName:name`, a job stays
989+
keyed `appId:jobName`, and the unmanaged import's own all-type refusal is
990+
unchanged.
991+
- **A picklist extension** (`picklistExtensions`) adds values to another
992+
package's list by design; it holds no name.
993+
- **A paired `.custom` package's declared override** (ADR-0146 D2) is a stored
994+
row bound to its pair, never a package holder, so the refusal does not close
995+
the override path.
996+
- **No downgrade.** `OS_METADATA_COLLISION=warn` softens the §3.2 namespace gate
997+
only.
998+
999+
### P1.5 To follow
1000+
1001+
The same measurement found these types resolving without a package context too:
1002+
`api` (the endpoint store is keyed by name — the other package's path answers
1003+
404), the object-less `action` (the last handler runs while `/meta` serves the
1004+
first declaration), the engine function registry (`functions:`, last wins, read by
1005+
flow `script` nodes and action `target` refs), `email_template`, `webhook` (the
1006+
second package's webhook is never subscribed), `connector`, `flow`, `agent` and
1007+
`skill`. Each joins P1.2's list in its own change under #30803, with its own pins,
1008+
narrowing §3.4 the same way; a type the security seat claims lands there instead.
1009+
`tool` and `rag_pipeline` share a reader with `agent` and were not measured.
1010+
1011+
### P1.6 Where it is implemented
1012+
1013+
`@objectstack/objectql`: the rule, the holders and the switch in
1014+
`bare-name-one-holder.ts`; the package door in `SchemaRegistry.installPackage` and
1015+
the item seam in `SchemaRegistry.registerItem`; the seal in
1016+
`SchemaRegistry.sealBareNameHolders`, called by the engine plugin at
1017+
`kernel:bootstrapped` through `sealBareNameHoldersAtColdBoot`.

0 commit comments

Comments
 (0)