Repository navigation
Commit 769adff
feat(metadata-protocol)!: thread the installed-manifest lookup through every pair site; the ensure door creates no pair for a kernel package (#31060)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoF7zm6g9Ti1PzWyHemzLqez)_
Clause-②: yes (narrowing) — both arms. Narrowing: `POST
/api/v1/packages/:id/ensure-customization` answers a kernel base (scope
`system` / `cloud`) with `409 RESOURCE_CONFLICT`, where it created
`<id>.custom`; `POST /api/v1/automation/:name/clone` of a kernel-shipped
flow with no `package` named answers that `409` where it answered `200`
with the copy in `<id>.custom`; the exported pair helpers take the
installed-manifest lookup as a required argument. Widening: five new
exports, `installedManifestsOf`, `InstalledManifestLookup`,
`AppLookupManifest` and `InstalledPackageSource`
(`@objectstack/metadata-core`) and `kernelPackageScope`
(`@objectstack/metadata-protocol`), and a copy of a kernel-shipped
permission set with no `package` named is saved environment-wide under
the new notice code
`PERMISSION_SET_CLONE_ENVIRONMENT_WIDE_KERNEL_SOURCE`, where it landed
in `<kernel id>.custom`.
Metadata-protocol: none
Refs #29594
Ruling-ref: 6691022506
Ruling-ref: 6691217245
## 维护者速读
- **改了什么**:①所有「这个包的定制包是谁」的问题(`pairOf` / `isPairOf` /
`isServedOverrideRowOf` / 写闸 / flow 优先级 等 23 处、14 个文件,连同 cli 与
cloud-connection 的调用方)都改为携带「已安装清单查询」,`appOf` 仍是恒等,行为不变;②ensure
门对内核包(scope `system` / `cloud`)返回 409 `RESOURCE_CONFLICT`,不再创建
`<id>.custom`。
- **为什么改**:按裁定 6691022506(定制单位 = App)与 6691217245(本版本内核对象一律不可定制,ensure
门给可读的拒绝而非 422)。查询先穿好,PR 3 才能只改 `appOf` 一处把模块解析到它的 App。
- **风险与回滚**:已在某些部署上由旧门建好的内核配对包继续安装、继续被读取、继续可写,只拒绝 ensure
调用;内核条目的无包写入与指名内核包的写入仍按 D11 拒绝(403
NOT_OVERRIDABLE),提示语改为「本版本内核对象不可定制」,不再点名配对包;克隆平台权限集(如
member_default)且未指定包时,副本改存为环境级(维护者已选 A,2026-11-07T01:01Z)。回滚即 revert 本
PR。
-
**先看**:`packages/metadata-protocol/src/protocol-customization-package.ts`(内核判定)、`packages/metadata-core/src/customization-pair-lookup.ts`(查询入口)、`packages/objectql/src/protocol-custom-override-effective-read.test.ts`
末尾两条钉子。
- **两分钟验证**:`pnpm --filter @objectstack/runtime exec vitest run
--project local src/domains/packages-ensure-customization.test.ts`(内核
409、旧配对包保留);`pnpm --filter @objectstack/objectql exec vitest run
--project local
src/protocol-custom-override-effective-read.test.ts`(旧内核配对包的覆盖继续生效;两道关闭对内核条目仍拒绝且不点名配对包)。
- **需要你做的**:无,席位落地。PR 3(`appOf` 翻转、旧配对包容忍、ensure 门回答 App 配对包、HotCRM
演练)在本 PR 合并且 ADR-0146 修订 #31027 合并后开工。
## What changes
### 1. The installed-manifest lookup, threaded through every pair site
(behaviour unchanged)
`pairOf(base) = customizationPackageId(appOf(base))` landed in #31026
with `appOf` the identity. Resolving a module to its App (ADR-0146
amendment, merge question 11, #31027) needs the registry's installed
manifests (`type` / `dependencies` / `namespace` / `scope`) at **every**
site that asks "which pair customizes this package?". If only the ensure
door resolved the App, the override gate and the served-override read
(still identity) would refuse or hide the very override written into the
App pair. So this PR threads the lookup everywhere first, with `appOf`
still the identity, and PR 3 becomes a one-place flip.
- `@objectstack/metadata-core`: `InstalledManifestLookup`,
`installedManifestsOf(registry)`; `appOf`, `pairOf`, `isPairOf` take the
lookup (required, so the compiler names every site that has not passed
it).
- `@objectstack/metadata-protocol`:
`installedManifestsOfEngine(engine)`; `isServedOverrideRowOf(row, base,
installed)`, `OverrideWriteClosure.installed`, the overlay merge, the
lock state, the layered/list reads, the per-package layer, the
stored-flow slot, the served-overrides census, the package-less
migration census, the packaged-base refusal and the write gates. Hosts
that read the engine gain the `engine` getter (the codebase's
`readEngine` pattern).
- Callers outside it: `objectql` (plugin, registry uninstall refusal),
`runtime` (`customizeInPairRemedy`, the per-package customizations
read), `service-automation` (flow precedence, with the objectql
registry), `service-datasource`, `cli` (offline override declarations:
the walked manifests), `cloud-connection` (install-local: the delivered
manifests over the registry's).
- Proof nothing moves: `customization-pair-lookup.test.ts` asks every
row twice, with nothing installed and with every row installed (the
module declaring its App the way HotCRM's modules do), and gets the old
derivation; every existing suite of the touched packages is green with
the lookup passed.
- Review nit 3 of #31026: `os validate`'s customization-namespace
advisory skips a pair through `isCustomizationPackage`. Edge: an id
ending in `.custom` that no base derives (`a.custom.custom`, bare
`.custom`, `A.custom`) was skipped before, because
`customizationPackageId` answers `base_is_customization_package` for it,
and is now judged like any package, because `isCustomizationPackage`
answers `false` (measured on the head's spec build).
### 2. The kernel verdict on the ensure door (narrowing)
| Call | Before | After |
|:--|:--|:--|
| ensure a kernel base (scope `system` / `cloud`), no pair yet | `201`,
creates `<id>.custom` | `409 RESOURCE_CONFLICT`, nothing created or
written |
| ensure a kernel base whose pair an earlier version created | `200`,
`created: false` | `409 RESOURCE_CONFLICT`; the pair stays installed and
readable |
| every read of a kernel item's name with a declared override in that
legacy pair | the override is served | unchanged |
| a save into that legacy pair declaring the override | admitted |
unchanged (the write gate never asks the ensure door) |
| package-less save onto an item a kernel package ships (overlay types)
| `403 NOT_OVERRIDABLE`, naming the pair | `403 NOT_OVERRIDABLE`, the
sentence says kernel objects are not customizable in this version and
names no pair |
| save naming the kernel package onto an item it ships | `403
NOT_OVERRIDABLE`, naming the pair | `403 NOT_OVERRIDABLE`, the same
sentence, no pair named |
| clone a kernel-shipped permission set, no `package` | `200`, copy in
`<kernel id>.custom` (created) | `200`, copy environment-wide, code
`PERMISSION_SET_CLONE_ENVIRONMENT_WIDE_KERNEL_SOURCE` |
| clone a kernel-shipped flow, no `package` | `200`, copy in `<kernel
id>.custom` | `409 RESOURCE_CONFLICT`, the ensure door's sentence |
| any non-kernel base | — | unchanged |
**The code.** `409 RESOURCE_CONFLICT` is the door's existing family for
"the base's state rules the pair out" (a writable base, no namespace, a
namespace with no pair namespace, a non-SemVer version, a malformed
existing pair), every one of which answers `RESOURCE_CONFLICT` in
`packages-ensure-customization.test.ts`. `403` would misread as an
authority problem (even the platform operator is refused), and `422
NAMESPACE_CONFLICT` names another condition. No ledger code and no
`packages/spec` edit.
**Where.** First in `customizationPairEnsureRefusal`, the one reading of
the door's registry-decidable refusals, and the two overlay closes keep
their place: `overlayPairFixOf` asks the door's other judgements with
the kernel verdict left out (`judgeKernel: false`) and marks a kernel
base's fix with its `kernelScope`, so the package-less close and the
close of a write naming the base stay shut on a kernel item exactly
where they were, with a sentence that names no pair (seat ruling on
review should-fix 2; ruling record 6691217245; #31027 Q10 is unmerged).
**The "new similar" doors (found by the dogfood battery).** The
permission-set clone door landed a copy of a kernel-shipped set
(`member_default`, shipped by `com.objectstack.plugin-security`, scope
`system`) in the source package's pair through the ensure door, so the
verdict refused the commonest lock-and-clone path. A clone is a new
item, not a customization of the kernel item, so with no `package` named
the copy is now saved environment-wide, exactly as a copy of an
environment-authored set is, with its own notice and code
(`PERMISSION_SET_CLONE_ENVIRONMENT_WIDE_KERNEL_SOURCE`; a Console that
does not know the code shows the English sentence, as it does for any
unknown code). A named `package` still lands there. The kernel predicate
is one spelling, `kernelPackageScope`
(`@objectstack/metadata-protocol`), read by the door and the clone door.
The flow clone door has no environment-wide landing (an unpackaged flow
already needs `package`), so a kernel-shipped flow gets the ensure
door's 409 sentence. This landing is option A of a decision card put to
the maintainer; B (refuse, naming `package`) and C (a kernel pair for
clones only) are the alternatives.
**Test fixtures.** `packages-ensure-customization.test.ts` and
`automation-flow-clone-custom-package.test.ts` modelled "an installed
app" as `scope: 'system'`, which is the kernel's scope (manifest schema:
`system` = cross-project shared identity, `cloud` = control plane). They
now hold the base as code, the way an installed app's packages are held
(HotCRM 6961ab1 declares no scope).
## Blast radius (measured on `origin/main` 3fe5f2a)
23 `pairOf` / `isPairOf` sites in 14 files; `isServedOverrideRowOf` 17
non-test callers; `judgeOverrideWrite` 4 (incl. `cli` offline and
`cloud-connection` install-local). This PR: 31 source files (+471 /
−156), 16 test files (+342 / −123), one changeset; 48 files, +842 / −279
in all.
## PR 2 / PR 3 split
- **PR 2 (this):** the lookup through every site, `appOf` the identity;
the kernel verdict.
- **PR 3 (after this lands and #31027 merges):** `appOf` resolves App
membership from the lookup (Q11); legacy per-package pairs stay served
(Q12: `isPairOf` accepts the App pair or an installed legacy
`<base>.custom`); the ensure door answers the App pair with
`basePackageId` = the App id (the spec describe "`packageId`:
`<basePackageId>.custom`" stays true); the HotCRM rehearsal.
Findings kept for PR 3: the package-less migration's
incomplete-declarations census parses pair→base
(`customizationBasePackageId`) and misses module overrides under the App
unit; its census still lists a kernel item's package-less row as
`movable` and the apply step fails it with the kernel sentence (a
disposition for it is a contract question); the dogfood helper
`customization-pair.ts` and six `ui/packages/app-shell` call sites
derive `<base>.custom` client-side; a `customizable` flag on the package
list row would need a spec sub-card. A package-less row on a kernel item
stored before the D11 close stays served, but nothing can move it now:
its package-less delete is closed, and the ceremony's ensure step
refuses the kernel base. Its disposition belongs with the census
question already listed.
## Verification
Head `b94d1ac68d151d99ad908992ad25f65bf2413a6d`, which merges today's
`origin/main` 022b4e7 (merge commit 20c8cd6, no conflicts; no rebase).
Exit codes landed on disk before reading.
| Run | Result |
|:--|:--|
| `pnpm typecheck` in metadata-protocol, objectql, runtime,
service-automation (head) | exit 0 each |
| `pnpm test` on the head: metadata-protocol / objectql / runtime /
service-automation / metadata-core / lint | exit 0 each: 22512 (+26
skipped) / 11360 / 8876 (+19 skipped) / 3431 / 962 / 8331 passed |
| `pnpm test` on `1340933c` for the packages this round did not touch:
service-datasource / cloud-connection / cli | exit 0 each: 1279 / 931 /
7415 (+9) passed |
| dogfood battery on `021a8a3d` (no dogfood pin moved this round) | exit
0: 428 files passed, 3 skipped; 3562 tests passed |
| eslint `--no-inline-config` on every `.ts` file this round changed |
exit 0 |
| `dispatch-gates --commands` on the head (102 families: the 101 of the
last round plus `check:overlay-whitelist-table`, which the lifecycle
docs edit adds), each run with `</dev/null` | 102 exit 0 on the first
pass; reconciled with `--ran`: 102 derived, 102 run, 0 NOT-MEASURED, 0
UNRUN |
Gate notes: the three first-pass non-zeros of the earlier round
(`check-plugin-teardown-shape --self-test`,
`check:dual-build-cjs-loads`, `check:test-source-alias`) all exit 0 on
this head with no further change.
**Ablations** (`scripts/ablation-replace.mjs`, restore proven by blob
hash == HEAD and an empty `git status --porcelain`):
- the kernel check in `customizationPairEnsureRefusal` disabled
(`ablation-dist-preflight` confirmed the mutation in
`metadata-protocol/dist`): red — the three runtime door pins (system,
cloud, the legacy kernel pair) and the objectql pin of the legacy pair
served after a refused ensure;
- the kernel landing in `resolveDefaultCloneLanding` disabled: red —
`[#29594] a set a kernel package ships copies environment-wide`;
- the kernel close in `overlayPairFixOf` disabled (`judgeKernel: false`
removed, metadata-protocol dist rebuilt and checked): red, `the closes
keep refusing a kernel item's save, naming no pair` (the save was
admitted);
- `restoreFlowRegistration` on a refused landing in
`registerAndSaveFlow` disabled: red, both `[#29594] a kernel-shipped
flow` cases (the engine held the copy).
Both restored with `git checkout`, tree clean, suites re-run green.
The lookup threading needs no ablation: it changes no answer, which
every existing suite of the nine packages confirms, and
`customization-pair-lookup.test.ts` pins that both lookups (nothing
installed, every row installed) give the old derivation.
## Acceptance notes
- The harness's assignee/reviewer step is skipped: this PR goes through
the relay as a draft, and review is requested by the seat.
- No `packages/spec`, `ui/` or `skills/` edit; no ADR edit.
- 51 files: 31 source files, 16 test files, 3 docs pages
(`content/docs/concepts/metadata-lifecycle.mdx` added), 1 changeset.
<!-- signed: domain:server#2 · os-tesla ·
session_01L9qibyvxbrLP4jm4PargoH -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8304b08 commit 769adff
51 files changed
Lines changed: 984 additions & 297 deletions
File tree
- .changeset
- content/docs
- api
- concepts
- permissions
- packages
- cli/src/utils
- cloud-connection/src
- lint/src
- metadata-core/src
- metadata-protocol/src
- objectql/src
- qa/dogfood/test
- runtime/src/domains
- services
- service-automation/src
- service-datasource/src
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
115 | 117 | | |
116 | 118 | | |
117 | 119 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
185 | | - | |
| 185 | + | |
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
| |||
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
216 | | - | |
| 216 | + | |
217 | 217 | | |
218 | | - | |
| 218 | + | |
219 | 219 | | |
220 | 220 | | |
221 | 221 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
453 | 453 | | |
454 | 454 | | |
455 | 455 | | |
456 | | - | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
457 | 461 | | |
458 | 462 | | |
459 | 463 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
36 | | - | |
| 37 | + | |
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
56 | 57 | | |
57 | 58 | | |
58 | 59 | | |
| 60 | + | |
| 61 | + | |
59 | 62 | | |
60 | 63 | | |
61 | 64 | | |
| |||
75 | 78 | | |
76 | 79 | | |
77 | 80 | | |
| 81 | + | |
78 | 82 | | |
79 | 83 | | |
80 | 84 | | |
81 | 85 | | |
82 | 86 | | |
83 | 87 | | |
84 | 88 | | |
85 | | - | |
| 89 | + | |
86 | 90 | | |
87 | 91 | | |
88 | 92 | | |
| |||
107 | 111 | | |
108 | 112 | | |
109 | 113 | | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
110 | 117 | | |
111 | 118 | | |
112 | 119 | | |
| |||
133 | 140 | | |
134 | 141 | | |
135 | 142 | | |
136 | | - | |
| 143 | + | |
137 | 144 | | |
138 | 145 | | |
139 | 146 | | |
| |||
0 commit comments