Skip to content

Commit 8d13c0b

Browse files
docs(adr): ADR-0146 D3 — one .custom pair per App, and one platform pair for the kernel suite (#31027)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoEkPw1xTkxsbBcd5WD7CXjo)_ Fixes #31014 Clause-②: no Metadata-protocol: none **Tier H** (`docs/adr/**`, Prime Directive #14): this PR stays a draft. The spec seat runs the contract review at `CONTRACT_REVIEW_TIER`, then hands it to the approver (`os-zhuang`); it lands by the maintainer's hand or by an authorized APPROVED review followed by the seat's landing. No seat readies, queues or arms it. Ruling-ref: 6691022506 and 6691217245 on #29594. ## 维护者速读 - 改了什么:只改 ADR-0146 D3 的文字(加状态行一句、Q4 一行、Consequences 与 D11 各一句)。定制单位从「每个只读包一个 `.custom`」改为「每个 App 一个」:HotCRM(1 个 App + 4 个模块,同一命名空间 `crm`、同一版本 6.1.0)只有一个 `app.objectstack.hotcrm.custom`,模块永远没有自己的定制包,ensure 门对 App 的任何成员都答这同一个包,不再 422。内核整体是一个平台套件,只有一个平台定制包 `com.objectstack.kernel.custom`,依赖平台版本 `^18`,不看各内核包 manifest 自己的 version;某个内核包没装时它的覆盖保留并标记,不拒装。按你 17:32 的裁决:这一版内核对象一律不可定制,下一版先开 Users、Organizations 白名单。`pnpm adr:index` 跑过,索引无变化。 - 为什么改:你 17:17 的裁决 ①「定制单位 = App;同意」(记录 6691022506)和 17:32 的 ③(记录 6691217245)。server 车道正在改 ensure 门(#29594 PR 1 已认领),改完要引用这段 ADR 文字。 - 风险与回滚:只有文档,不发布任何包;回滚即撤本 PR。真正的风险是三处裁决没说、本 PR 自己定的点(见下「Decided here」8、9、10),你不同意哪条就点名。 - 先看:`docs/adr/0146-…md` 的 D3(新增「The unit is the App」段、「The kernel is one platform suite」段、「Not taken」段);文末「Decided here」第 8–10 条。 - 两分钟验证:`node scripts/adr-index.mjs --check` 预期 exit 0 且 README 无 diff;`pnpm check:adr-symbol-anchors` 预期绿(本 PR 新引了 14 个 `path#symbol` 锚)。 - 契约复审第一轮(记录 6692724375,PASS,四条 should-fix):已在 `05ea9bbb78` 一次改完;第二轮(记录 6693213712,PASS,两条 should-fix):已在 `b13af10317` 改完;第三轮(记录 6693528804,PASS,两条边界 should-fix,席位定为最后一轮):已在 `2990a3dc26` 改完。见文末「Decided here」第 11、12 条和「Contract review」三节;README 仍无变化。 - 需要你做的:Tier H,需要你(经 os-zhuang)审批;平台定制包 id `com.objectstack.kernel.custom` 是我选的,备选见下,不同意就改。 ## What changes One file: `docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md` (+201 / −29 against the merge base, over four commits: `e2cc08eb52`, the round-1 `05ea9bbb78`, the round-2 `b13af10317` and the round-3 `2990a3dc26`). `docs/adr/README.md` was regenerated with `pnpm adr:index` and is byte-identical (the Status line's first sentence, which the index reads, did not move). **D3, amended in place** (house form: the 2026-10-31 rewrite and the #30881 amendment), following the card's Expected 1–10: 1. **Status line**: one dated `**Amended in place** (2026-11-06, …)` sentence quoting both rulings verbatim, 「① 定制单位 = App;同意」 and 「内核白名单,可以下一版本再处理,首批名单建议:Users、Organizations;其他同意。」, linking ruling records 6691022506 and 6691217245. 2. **The unit is the App.** New paragraph and three bullets under D3: an App is the `type: 'app'` package plus every package its artifact delivers, whatever its type (ADR-0019 D1–D4; round 1, S4 nit), one artifact at one version (ADR-0130 D1/D6). One pair per App, `APP_ID.custom`; a module never owns a pair; a customization of any member lands in the App's pair; the ensure door creates on the first ask (201) and answers the existing pair after (200), never 422 for a member of the same App; 422 `NAMESPACE_CONFLICT` stays for a namespace held outside the artifact (`coOwnersOf` arms unchanged). The HotCRM example is measured: `objectstack-ai/hotcrm` `6961ab1` ships `app.objectstack.hotcrm` and **four** modules — `app.objectstack.hotcrm.service`, `.revenue`, `.marketing`, `.o2c` — all `namespace: 'crm'`, all `6.1.0`, so one pair `app.objectstack.hotcrm.custom`. (The card and the ruling said three modules; `.o2c` joined in hotcrm PR #2413.) 3. **The existing bullets re-worded for the App unit** — auto-created (per-App target), namespace (`APP_NS_custom`; the `sys_` pass-through close from ruling ① is stated, with the `validateObjectNamespacePrefix` anchor), dependency range (`^MAJOR` of the App's `manifest.version`; HotCRM `^6`), uninstall (of the App), "It receives" (from the App or any of its modules). Walled postures unchanged. One new bullet records ruling (b): the package list row and the ensure door serve a readable `customizable` verdict and the pair a Customize lands in. 4. **The kernel is one platform suite with one platform pair.** New paragraph: kernel packages (manifest `scope` `system` or `cloud`, the `READ_ONLY_PACKAGE_SCOPES` reading) form one suite with one pair, `com.objectstack.kernel.custom`, namespace `sys_custom`. Three bullets: the range is the platform version (`^18` today; the `fixed` group of 109 packages at 18.3.0, never a kernel manifest's own 0.1.0–3.x `version`), declared in `engines.platform`; orphan tolerance (② as recorded: full kernel install is a goal, not today's `serve` behaviour — tier presets, `requires`, the always-on slate, optional Security/Audit — so an absent kernel package's overrides are kept and marked, the install is never refused, the verdict is served for the packages present; the same handling for a module a later App version drops); and ruling ③: no kernel object is customizable in this version, the ensure door refuses a kernel package with the readable verdict and never with 422, the next version opens Users and Organizations only, with the two prerequisites (the `sys_` prefix heuristics replaced by an object-level `isSystem` the next version adds, with `hasPlatformObjectPrefix` named as the old heuristic to retire — round 1, S1; `customizable` judged at every write into the platform pair by the overridden object's package). 5. **Not taken**, after "Upgrade and audit": (a) a pair per package, (c) N pairs sharing `NS_custom`, (e) an explicit split key; ADR-0019 and ADR-0130 not amended. 6. **Q4 row**, **Consequences** "Cost, stated" (one per App, never one per module) and **D11** follow the new unit. Every other `hotcrm.custom` mention in the record stays as written on purpose: a new D3 bullet says that `hotcrm` is the record's shorthand for `app.objectstack.hotcrm`, so each of them names the App's pair. 7. **Decided here where the rulings were silent** gains items **8 to 12** (below; 11 and 12 from round 1), each also flagged inline. **Refs** gains the card. ### The platform pair id — chosen here, alternatives weighed **`com.objectstack.kernel.custom`** (base `com.objectstack.kernel`). Constraints from the card, all measured on `objectstack-ai/objectstack` `33bc8ae5` (the PR's merge base) and `objectstack-ai/hotcrm` `6961ab1`: - satisfies `MANIFEST_ID_PATTERN` (two or more dot-separated segments of lowercase letters, digits and hyphens, each starting with a letter or digit): yes; - held by no real package: `git grep -c -E "com\.objectstack\.kernel\b"` → 0 hits in both repositories (every kernel manifest id is `com.objectstack.SOMETHING`, 29 distinct ids in non-test source; none is `kernel`); - `customizationBasePackageId('com.objectstack.kernel.custom')` → `com.objectstack.kernel` (the pure inverse of `customizationPackageId`); - namespace `sys_custom` (the kernel namespace `sys` + `_custom`, 10 of the grammar's 20 characters); - names the suite, not one package, in the family the kernel ids use. Alternatives weighed and not taken: - `com.objectstack.platform` — the ruling's own word for the suite, and the strongest alternative. Not taken because **22 test files** at `33bc8ae5` already use `com.objectstack.platform` as a stand-in id for ONE kernel package (`PLATFORM_PKG`, `SYSTEM_SCOPED`, `KERNEL_ID` …). With that base, the per-package pair the old `customizationPackageId(base)` derives and the platform pair `pairOf(base)` resolves to would be the same string in every one of those fixtures, so none of them could tell the retired lookup from the new one; the server lane's refactor tests would pass on either. - `com.objectstack.sys` — names a namespace; a package is identified by its id (ADR-0048), and `sys` is already the suite's namespace, not its id. - `com.objectstack` — names the vendor, whose own Apps (showcase, HotCRM) are not the kernel; also a two-segment id that reads as a prefix of every kernel id. - `com.objectstack.platform-suite`, `com.objectstack.core` — `core` is used by three spec/metadata tests and the `package-version.zod.ts` example as a generic dependency id; `platform-suite` is free but coins a word the record does not use. If the maintainer prefers `com.objectstack.platform` despite the fixture collision, the change is one identifier in the ADR; the fixtures would then need their stand-in id renamed on the server lane's first PR. ### Decided here where the rulings were silent (new items, for the approver) - **8 — the platform pair's id**, above. - **9 — where the platform range is declared and what "platform version" means.** The range cannot be a `manifest.dependencies` entry: no package holds `com.objectstack.kernel`, so ADR-0141 D2's `checkDeclaredDependencies` would answer it `absent` at every install of the pair. It is declared in `engines.platform` (`^18`), which the install handshake already reads (`resolveDeclaredRange`) against `PROTOCOL_MAJOR`, kept in lockstep with the platform major (`protocol-version.ts`). The platform version is the release version the `fixed` group ships (`@objectstack/runtime`'s own `package.json`), and ⛔ not whatever `resolveRuntimeVersion()` answers: that function prefers the injected `OS_RUNTIME_VERSION` stamp (an image tag or a git sha) when one is set, which is not a SemVer major. The card's Expected 4 named that function as the reading of "one version"; the ADR names it as the thing NOT to read for the range, with the reason. - **10 — a package-less overlay row over a kernel item waits for the whitelist.** D11 gains one sentence: while the ensure door refuses a kernel package (ruling ③), the migration cannot create the platform pair, so such a row is not migrated; it stays as it is, served as D8 item 1 says, and its package-less write stays open, exactly as `customizationPairEnsureRefusal`'s docblock describes for any base whose pair the door refuses today. Closing that write with no pair to name would leave an administrator no way to change an existing kernel customization (a view, a translation, an email template) until the next version. - **11 — the App of a package, and the package that declares no App** (round 1, S4). Nothing in the tree persists which artifact delivered a package (`InstalledPackageSourceSchema` is `code | marketplace | authored`; `ArtifactInstallScope.packageIds` is an install-time argument), so the ensure door reads the App of a package the way the registry already reads a module's namespace co-ownership (ADR-0141 D1/D5; the `dependency` arm of `coOwnersOf`): a `type: 'app'` package is its own App; any other package's App is the `type: 'app'` package it directly declares in `manifest.dependencies` — the one holding its namespace where it declares several, and the readable verdict (not customizable: no single App) where that leaves none or several — never a guess, never an id prefix. The reading walks declared parents (round 2, S4b; made unambiguous in round 3, S4c): for a package that declares no App directly, the door takes the closure of its declared dependencies, each package visited once (a cycle terminates), stopping at a `type: 'app'` package (an App's own dependencies are another App's business, ADR-0141 D1); only the Apps reached count, a kernel or own-unit package on the way is skipped (a member declaring a module and `com.objectstack.audit` is still that App's member); one App makes the package its member, several take the direct rule's namespace tiebreak, and the readable verdict where that leaves none or several — never 422 for a package of the same App; HotCRM has no such nested module today. HotCRM's four modules each declare `app.objectstack.hotcrm` `^6.1.0`. ADR-0141 D3's standalone module is a member for customization (a second pair in `crm_custom` would be (c), not taken, and the namespace gate would refuse it) and stays an App in its own right for install, open and uninstall; uninstalling it alone keeps and marks its overrides. A package that declares no `type: 'app'` package, directly or through its declared parents (an operator-provisioned internal contribution, a plugin installed alone, a code-registered package outside any App), is its own unit, `PACKAGE_ID.custom` with `NS_custom`; a kernel package never is — its pair is the platform pair. `pairOf` / `appOf` (PR #31026) carries the reading; `appOf` is the identity until the ensure door's change. - **12 — a pair created under the per-package unit** (round 1, S3). D3 has been in force since 2026-10-31 and the ensure door is landed, so an environment may hold `app.objectstack.hotcrm.service.custom` or the one kernel pair today's door creates before its 422s. D11 and this item: the migration ceremony **re-identifies** such a package as its App's pair when it holds the App's own namespace — the installed record's id and dependency rewritten to the App's, its rows re-bound to the new id (they bind by `package_id`), its items and declared overrides kept (same base, same `basedOn`). Round 3, S3c: a member may hold a namespace of its own (ADR-0019 D4), so an App may also hold a pair in a member's namespace (`crm_svc_custom`); that one is moved, not re-identified — rows re-bound into the App's pair (created or re-identified first), the package uninstalled once it owns nothing — never dropped; at most one pair per namespace existed. Round 2, S3b: moving the items into a newly created App pair cannot run against today's gates, because the old pair owns `NS_custom` and both the ensure door (422 `NAMESPACE_CONFLICT`, `customizationPairEnsureRefusal`) and the install door (`NamespaceConflictError`) refuse a second owner, and uninstalling it first would drop its `__c` fields with their data. Until the ceremony runs the pair is served as it is and the ensure door serves the readable verdict naming the ceremony, not 422; a kernel one waits with D11's kernel rows until the whitelist version re-identifies it as the platform pair and lists the rows it cannot admit. ## Contract review round 1 (record 6692724375, PASS with four should-fixes) — fixed on `05ea9bbb78` Each claim was verified against `origin/main` before the edit; all four are in one commit, ADR file only, README byte-identical. - **S1 — the next-version prerequisite anchored the wrong reading.** `hasPlatformObjectPrefix` is `startsWith` over `sys_` / `cloud_` / `ai_` (its own docblock calls it the OLD heuristic) and answers true for `sys_custom_x`; no object-level `isSystem` exists in `packages/spec` (the only `isSystem` keys are service-context flags). The ADR now names an object-level `isSystem` the next version adds as the declared reading, and names `hasPlatformObjectPrefix` as one of the heuristics to retire (the anchor stays as the negative, so the next card finds the symbol). - **S2 — `engines.platform` must be the only `engines` key.** `resolveDeclaredRange` reads `engines.protocol` first. The D3 bullet and Decided-here 9 now say the platform pair's manifest carries `engines.platform` as its only `engines` key, and that the ensure door's existing-pair check — today `declaredRangeOn` over `manifest.dependencies` in `customizationPairEnsureRefusal` — reads that key for the platform pair: the one place the rule and today's gate disagree, carried by the server lane's ensure-door change. - **S3 — pre-amendment per-package pairs had no disposition.** Decided-here 12 and one sentence in D11's migrate bullet, above. - **S4 — the App of a package was not readable from the tree, and the artifact-less package had no rule.** Decided-here 11, above, plus the nit: the "unit is the App" paragraph now says "every package its artifact delivers, whatever its type", not only `module` / `plugin`, and the "module never owns a pair" bullet points at item 11. ## Contract review round 2 (delta record 6693213712, PASS with two should-fixes) — fixed on `b13af10317` Both verified against `origin/main` before the edit (ensure door: `customizationPairEnsureRefusal`'s namespace-owner arm answers 422; install door: `NamespaceConflictError` for a non-co-owner); one commit, ADR file only, README byte-identical. - **S3b — the fold could not run against today's gates.** Decided-here 12 and D11's migrate sentence now say the ceremony re-identifies the old pair as the App's pair (id and dependency rewritten; namespace, items, rows and declared overrides kept), why a fresh App pair cannot be created beside it, and that until the ceremony runs the ensure door serves the readable verdict naming the ceremony, not 422. - **S4b — direct declarations only would 422 a nested member.** Decided-here 11 now walks declared parents: all parents resolving to one App makes the package that App's member; parents resolving to different Apps get the readable verdict; never 422 for a package of the same App. HotCRM has no such module today. ## Contract review round 3 (delta-2 record 6693528804, PASS with two edge-case should-fixes; the seat's final fix round) — fixed on `2990a3dc26` Both are edge cases with no live instance; verified against ADR-0019 D4 (an App owns a set of namespaces) before the edit; one commit, ADR file only, README byte-identical. - **S3c — "at most one such pair per App" held only when every member shares the App's namespace.** Decided-here 12 and D11 now re-identify only the pair in the App's own namespace; a pair in a member's own namespace is moved into the App's pair (rows re-bound; the package uninstalled once it owns nothing), never dropped. Nit taken: rows are re-bound to the new id, they do not "stay in place". - **S4c — the parent walk made unambiguous.** Decided-here 11: (1) only parents that resolve to an App count, kernel and own-unit parents are skipped; (2) it is a closure over declared dependencies, each package visited once, so a cycle terminates, and it stops at an App; (3) the direct rule's namespace tiebreak applies to the result set. ## Why Ruling 6691022506 on #29594 (maintainer, 2026-11-06T17:17Z 「① 定制单位 = App;同意」) and ruling 6691217245 (17:32Z, the kernel whitelist). Today D3 gives one pair per read-only package; every kernel package shares namespace `sys`, so only the first can get a pair and the rest answer 422 `NAMESPACE_CONFLICT`, and a suite like HotCRM would get five pairs for one product. The server lane's ensure-door PR (`pairOf(base)` by App) cites this text once merged; its first PR (the lookup refactor, claim 6691163548) does not wait on it. ## Tests None owed: the diff touches `docs/adr/**` only — no package, no test, no generated artifact (local verification scope ①–②: no closure to build, no package suite to run). The ADR's own gates are the verification below. ## Acceptance notes - `docs/adr/README.md` regenerated and unchanged: the index reads only the first 40 header lines for `**Supersedes**`-style fields (`scripts/adr-index.mjs` `HEADER_WINDOW`). A three-line version of the status sentence pushed `**Supersedes in part**:` to line 42 and silently dropped ADR-0146's Supersedes column from the index; the sentence is one line so the field stays at line 39. Worth knowing for the next amendment of this record: there is one header line of headroom left. - The card and ruling ① count three HotCRM modules; `origin/main` of hotcrm ships four (`.o2c`, hotcrm PR #2413). The ADR names the four as measured. - The ADR states the `cloud` scope as part of the kernel suite because `READ_ONLY_PACKAGE_SCOPES` and the console's kernel-package reading both say `system` or `cloud`; no `scope: 'cloud'` manifest exists in this repository's `packages/**` (they live in the cloud repository), so the sentence binds nothing here today. - Commit `e2cc08eb52`'s author is spelled `objectstack-fleet[bot]` with the generic noreply email rather than this checkout's configured author (`Claude`, the anthropic noreply address) — my mistake on the commit line. Not rewritten: the thread branch is not force-pushable under Multi-agent discipline §3 (its name is not `claude/issue-*`), and the squash merge replaces the author anyway. Deviation, declared. The round-1, round-2 and round-3 commits (`05ea9bbb78`, `b13af10317`, `2990a3dc26`) carry the checkout's configured author. - Out of scope, not filed (carrier: the server lane's #29594 PRs): the code spelling of the same rules in `packages/spec/src/kernel/customization-package.ts` (`customizationPackageId` / `customizationNamespace` / `customizationDependencyRange`) is untouched here, as the card orders. ## Verification Head `2990a3dc26` (= `origin/main` `33bc8ae5ff` + `e2cc08eb52` + the round-1, round-2 and round-3 commits; 1 file, +201 / −29 against the merge base, 230 changed lines, under the 3000 human-merge threshold). The gate lines below are the round-3 run on `2990a3dc26`; the same 21 families were derived for each earlier head and were all green on each. - `node scripts/adr-index.mjs --check` → exit 0, `adr-index: unchanged docs/adr/README.md (237 lines)`. - `node scripts/pm/check-governed-merges.mjs --test docs/adr/0146-…md` → `docs/adr/**` Tier H by path. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **21** families; every one run in the foreground with stdin from `/dev/null`, exit code captured before any pipe, landed in `gates-ran-2990a3dc26.list`; `--ran` reconciliation: `21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN` (exit 0). Plus `pnpm --filter '@objectstack/lint...' build` under the verify lock (exit 0) as the prerequisite of `check:doc-formula-expressions`. <details><summary>The 21 gate lines, all exit 0</summary> ``` node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm check:adr-anchors :: exit 0 pnpm check:adr-index :: exit 0 pnpm check:adr-symbol-anchors :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-prior-rulings :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` </details> - Neighbours: of the 37 open PRs at the time of the first push, none changes `docs/adr/0146-*` or `docs/adr/README.md` relative to its merge base (11 mailbox PRs and the Version Packages PR carry the files only because their base is old; #30986 edits ADR-0050 and the index's ADR-0050 row, no overlap with this diff). - Writes: `git push` (4: `e2cc08eb52`, `05ea9bbb78`, `b13af10317`, `2990a3dc26`), relay `pr_create` (1, this PR, `skip-changeset` on the create), `os-dev-report` comment (1, post-stamped), relay `issue_patch` (3, this body after rounds 1, 2 and 3). `mcp_calls`: 0 writes (reads only: issue/PR reads of #31014, #29594, #31026 and the open-PR list). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. (One package id, `com.objectstack.kernel.custom`, in the manifest id grammar.) - [x] 3. No formatter ran; `pnpm lint` is CI's run — docs only. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Publishes nothing (`docs/adr/**` only) — `skip-changeset`; both declaration lines are above. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`; `docs/adr/README.md` regenerated, not hand-edited. - [x] 8. Title, body and comments are in English; the 速读 and the quoted rulings are in Chinese by rule. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. <!-- signed: domain:spec#1 · os-tesla · session_01L9qibxn8FPvL85i9w26qoq --> --------- Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 704efef commit 8d13c0b

1 file changed

Lines changed: 201 additions & 29 deletions

File tree

0 commit comments

Comments
 (0)