Skip to content

docs(adr): ADR-0146 D3 — one .custom pair per App, and one platform pair for the kernel suite - #31027

Draft
objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/project-thread-kouhp1
Draft

objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/project-thread-kouhp1

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

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 门(Customization pairs: every system package shares namespace sys, so only the first can get a .custom pair (422 NAMESPACE_CONFLICT on the rest) #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 feat(spec): add ActionSchema.openIn for declarative url new-tab (objectui #2043) #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 refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged #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 feat(spec): add ActionSchema.openIn for declarative url new-tab (objectui #2043) #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 Customization pairs: every system package shares namespace sys, so only the first can get a .custom pair (422 NAMESPACE_CONFLICT on the rest) #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.
The 21 gate lines, all exit 0
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

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No schema touched.)
  • 2. Every name I added uses the casing for its kind. (One package id, com.objectstack.kernel.custom, in the manifest id grammar.)
  • 3. No formatter ran; pnpm lint is CI's run — docs only.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. Publishes nothing (docs/adr/** only) — skip-changeset; both declaration lines are above.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/; docs/adr/README.md regenerated, not hand-edited.
  • 8. Title, body and comments are in English; the 速读 and the quoted rulings are in Chinese by rule.
  • The Before Submitting list in CONTRIBUTING.md is done.

… pair for the kernel suite

Amend D3 in place on ruling records 6691022506 and 6691217245: the
customization unit is the App (one `<app id>.custom`, never one per
module; HotCRM -> `app.objectstack.hotcrm.custom`), the kernel is one
platform suite with one platform pair `com.objectstack.kernel.custom`
whose range is the platform version `^18`, an absent kernel package's
overrides are kept and marked, and no kernel object is customizable in
this version (the Users / Organizations whitelist is the next one).
Q4, the Consequences and D11 follow; merge questions 8-10 name the
choices made where the rulings were silent. `pnpm adr:index` leaves
docs/adr/README.md byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxn8FPvL85i9w26qoq
@objectstack-fleet objectstack-fleet Bot added documentation Improvements or additions to documentation skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Nov 6, 2026
@github-actions github-actions Bot added the size/m label Nov 6, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibzzqHZdAVqgBAvxXKB

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e2cc08eb5248d78f6cd6f6b8841a11ab54220aaf
Local-runs: none
Scope: docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md + packages/spec/src/kernel/customization-package.ts, packages/spec/src/kernel/manifest.zod.ts (MANIFEST_ID_PATTERN, PluginEnginesSchema), packages/spec/src/kernel/protocol-version.ts, packages/spec/src/kernel/namespace-prefix.ts, packages/spec/src/kernel/platform-capabilities.ts, packages/spec/src/system/constants/platform-object-names.ts, packages/spec/src/kernel/package-registry.zod.ts, packages/metadata-core/src/protocol-handshake.ts, packages/metadata-protocol/src/protocol-customization-package.ts, packages/metadata-protocol/src/protocol-packaged-base-refusal.ts, packages/metadata-protocol/src/protocol-package-install.ts, packages/metadata-protocol/src/package-writability.ts, packages/objectql/src/registry.ts (coOwnersOf, isShareableNamespace, assertPackageUninstallable), packages/runtime/src/runtime-version.ts, .changeset/config.json, packages/runtime/package.json, scripts/adr-index.mjs, docs/adr/README.md, docs/adr/0019-app-as-consumer-unit.md, docs/adr/0130-release-artifact-as-co-ownership-boundary.md, docs/adr/0141-inter-app-dependency-declared-checked-before-install.md; objectstack-ai/hotcrm 6961ab1 src/{sales,service,revenue,marketing,o2c}/index.ts; PR #31026 (first consumer, pairOf/appOf). Cited code read, never reviewed; the diff has no remainder (one file).

Inputs: card #31014 body and both comments (claim 6691185615, os-dev-report 6692464232); PR #31027 body, file list (1 file, +133/−28) and the net diff against merge base 33bc8ae (= origin/main 7d9f1e9 for every cited file: zero drift); rulings 6691022506 and 6691217245 on #29594 read in full; check-runs on the head: every non-skipped run completed success at 2026-11-06T18:45Z (Lint · docs & code guards carries check:adr-index; Lint · repo & tooling gates carries check:pm-governed-prose; Governed Surface Queue Guard, Check Changeset, Check Documentation Links, Test Core 1/8 all green). PR author objectstack-fleet[bot]; draft; labels documentation, size/m, skip-changeset; no reviewer requested yet (the seat sets os-zhuang at hand-over, as the report says).

① Derived judgments

  • Accept set / public surface: none moves. The diff is one ADR file; no packages/spec key, route, error code or export changes. com.objectstack.kernel.custom is a declared id in prose, not a constant in the tree. Right.
  • Status line (line 27 on the head): one dated · **Amended in place** (2026-11-06, ruling record … on #29594, maintainer, verbatim: 「① 定制单位 = App;同意」, and the second ruling record … verbatim: 「内核白名单…其他同意。」): … It lands by the maintainer's hand-merge of the PR cut from #31014. Same shape as the 2026-10-31 (docs(adr): ADR-0146 amendment — only the paired <base>.custom declares an override; a dependent package adds under its own names (ruled A, withdraws rider 3) #28390) and 2026-11-06 (docs(adr): ADR-0146 amendment — an upgrade plan may report a .custom override that hides a vendor change, as advice only; basedOn still never gates (epic cloud#3652 D2) #30881) sentences: dated, ruling record linked, maintainer's words verbatim and untranslated, the moved clauses listed, the landing act named. House form holds. Right.
  • D3 heading and the "unit is the App" block: one pair per App app.objectstack.hotcrm.custom; a module never owns a pair; the ensure door answers 201 then 200 and never 422 for a package of the same App; 422 NAMESPACE_CONFLICT stays for a namespace held outside the artifact (coOwnersOf arms unchanged — read: artifact, parent-dependency and sibling arms at registry.ts:8716-8740, untouched). Matches ruling ① word for word. HotCRM measured at hotcrm 6961ab1: src/sales/index.ts id app.objectstack.hotcrm type app; service/revenue/marketing/o2c ids app.objectstack.hotcrm.* type module; all namespace: 'crm', all 6.1.0. Four modules is the measured truth (the card's "three" predates hotcrm feat(spec): add ActionSchema.openIn for declarative url new-tab (objectui #2043) #2413). Right.
  • Namespace bullet: crm_custom via customizationNamespace('crm') (grammar 20 chars, suffix 7, ok); deriveNamespaceFromPackageId('app.objectstack.hotcrm.custom') answers custom and is never written — read at namespace-prefix.ts:54-63. The sys_ sentence cites validateObjectNamespacePrefix, whose line 33 is today's pass-through (if (objectName.startsWith('sys_')) return null;); the ADR says the ruling closes it. Right as a recorded decision, and the ruling's Workload gives the sys_ gap to domain:spec.
  • Dependency range: ^6 for HotCRM by customizationDependencyRange('6.1.0'); read at install by assertDeclaredDependencies (protocol-package-install.ts:334) through checkDeclaredDependencies. Right.
  • Platform suite paragraph: kernel = manifest scope system or cloud (READ_ONLY_PACKAGE_SCOPES at package-writability.ts:38, exactly those two). 109-package fixed group verified in .changeset/config.json; @objectstack/runtime at 18.3.0; PROTOCOL_VERSION 18.0.0, lockstep by the docblock's own test. Orphan tolerance states ruling ② as recorded (goal vs fact, PLATFORM_ALWAYS_ON_CAPABILITIES at platform-capabilities.ts:350). Ruling ③ recorded as "no kernel object customizable in this version; next version Users and Organizations only, with the two prerequisites". Right — with the ④ should-fix on the second prerequisite's anchor.
  • Not taken (a)(c)(e), ADR-0019 and ADR-0130 not amended: read ADR-0019 D1–D4 (App the only consumer unit; module/plugin invisible and not consumer-installable; suites fold into one App; an App owns a namespace set and uninstalls atomically over it) and ADR-0130 D1/D6 (artifact = co-ownership proof; one artifact one version, a module on its own cadence becomes its own artifact). Nothing in the amendment contradicts them: one pair per App IS ADR-0019 D1–D3 applied to customization; the pair's range on the App's version is ADR-0130 D6's "one version"; the pair's crm_custom is outside the App's namespace set so ADR-0019 D4's atomic uninstall is not reached while the pair exists (the refusal fires first). Right.
  • Q4 row, Consequences "Cost, stated", D11 migration sentence, Decided-here 8/9/10, Refs: each follows the new unit; the remaining hotcrm.custom mentions are covered by the new shorthand bullet. The residual "per base" wording at head lines 613 and 783 (the migration "reports per base") is correct under the shorthand (base = App or the platform pair). Right.
  • docs/adr/README.md unchanged — right. STATUS_LINE (/^\*\*Status\*\*:[ \t]*(.*)$/) captures line 3 only; the inserted line 27 opens with · **Amended in place** ( which headerFields' regex (**Key**: / **Key:**, optional leading -) does not match, so it is neither a field nor a status value; the row's date is the status value's first ISO date (2026-10-31, unchanged). The check-run "ADR index is current" is green on the head.
  • Header window — right. HEADER_WINDOW = 40; lines.slice(0, 40) reads lines 1–40. The one inserted line moved **Supersedes in part**: from 38 to 39 and **Amends**: to 40; only supersedes/superseded by/date keys are read, so nothing the index reads left the window and the dev's "one line of headroom" is exact. The dev's declared observation (a three-line sentence would have dropped the column) is correct and worth the note it got.

② Semver level

Docs only (docs/adr/**), skip-changeset label, PR body Clause-②: no, Metadata-protocol: none. Nothing publishes; Check Changeset green. Right.

③ Boundary flags

  • open_question 1 (pair id, A keep com.objectstack.kernel.custom) — answered A. Verified against the tree: matches MANIFEST_ID_PATTERN (/^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/); git grep -E 'com\.objectstack\.kernel\b' → 0 hits on origin/main 7d9f1e9 and on hotcrm 6961ab1; customizationBasePackageId is the pure inverse (customization-package.ts:106-111) so it answers com.objectstack.kernel; customizationNamespace('sys') → sys_custom, 10 chars. com.objectstack.platform with the '…'-bounded grep is in 22 test files at 33bc8ae as the dev says; the fixture-collision argument holds (per-package pair and platform pair would coincide there). Keep A.
  • open_question 2 (engines.platform, A) — answered A with a precision should-fix (④ S2): resolveDeclaredRange reads engines.protocol first and engines.platform only when protocol is absent (protocol-handshake.ts:246-256); assertProtocolCompat does run at installPackage (protocol-package-install.ts:325), so the declaration is judged at install — provided the pair's manifest carries no engines.protocol. checkDeclaredDependencies would indeed answer absent for a dependency on com.objectstack.kernel (protocol-handshake.ts:879-882). No ADR-0141 D2 amendment needed.
  • open_question 3 (kernel rows stay open, A) — answered A. packagelessOverlayWriteRefusal closes a write only where os migrate packageless-overlays could move the row (protocol-packaged-base-refusal.ts:237-247), and customizationPairEnsureRefusal's docblock states that the close leaves the write open where the door refuses (protocol-customization-package.ts:292-301); D8 item 1 keeps the package-less row winning until migrated. Closing with no pair to name would break D11's own "the sentence's two doors are always open". A is right. Note for the server lane (not a finding here): today's door does not refuse the FIRST kernel package asked — it would create com.objectstack.audit.custom and only the second answers 422 — so the kernel refusal of ruling ③ has to land in customizationPairEnsureRefusal before or with the migration, or the ceremony would move a kernel row into a per-package pair.
  • Deviation: resolveRuntimeVersion() is not the range's source — right: runtime-version.ts:80 prefers OS_RUNTIME_VERSION (image tag / sha) over the package version. The ADR's "release version the fixed group ships" is the correct reading.
  • Deviation: four modules, not three — verified, see ①.
  • Deviation: README byte-identical — right, see ①.
  • Deviation: commit author spelled objectstack-fleet[bot] — outside the hits file; the squash replaces it. Declared, accepted.
  • Deviation: MCP writes skipped / reviewer set at hand-over — process; the PR has no requested reviewer yet, the seat owes os-zhuang.
  • Acceptance note: cloud scope binds nothing here — verified: no scope: 'cloud' manifest under packages/** on origin/main (only CHANGELOG prose).
  • out_of_scope: code spelling untouched — by the card's order; the first consumer is refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged #31026 (pairOf/appOf, identity today, packages/metadata-core/src/customization-pair-lookup.ts), which cites the ruling and does not wait on this text. Right.
  • out_of_scope: header-window observation — judged right in ①; noted, not filed, is the correct disposition.
  • No security_findings; author is the relay bot, no off-relay login note owed.

④ Design judgment

hits: docs/adr/0146-…md, judged against the rulings, the cited code on origin/main (zero drift from the merge base), ADR-0019/0130/0141, the first consumer (#31026) and the execution plan (ruling 6691022506's Workload: ensure door by App, customizable verdict, orphan-tolerant platform pair; ruling 6691217245: whitelist next version).

Decided here 8 — the platform pair id. Holds. Constraints of the card's Expected 3 all verified (③ above). The id names the suite in the family every kernel id uses, is held by nothing, inverts cleanly, and the alternatives are weighed on evidence that checks out. No finding.

Decided here 9 — the range in engines.platform. Holds, one precision gap (S2). The install door runs assertProtocolCompat(manifest) before the registry write and the handshake compares the declared range to PROTOCOL_MAJOR (18), lockstep with the platform major; ^18 is therefore judged at install and re-install of the pair. manifest.dependencies cannot carry it: nothing holds com.objectstack.kernel, so checkDeclaredDependencies answers absent. The reversible middle (^major) matches merge question 1.

  • S2 (should-fix): resolveDeclaredRange prefers engines.protocol; a pair manifest that carries both keys has its platform range declared and never read. The ADR should say the platform pair declares engines.platform as its only engines key (or that engines.protocol is not written on it), so the server lane does not stamp the scaffold's engines.protocol beside it. In the same breath it may note that customizationPairEnsureRefusal's existing-pair check today demands "a dependency on the base in manifest.dependencies" (protocol-customization-package.ts:334-335) and must read engines.platform for the platform pair — implementation, but the one place the ADR's rule and today's gate disagree.

Decided here 10 — kernel overlay rows wait. Holds. The mechanism is already the tree's: the close and the ceremony share one subject reading, and a refused ensure leaves the write open; D8 item 1 serves the row meanwhile. The ADR's reason (an administrator must still be able to edit an existing kernel view or template) is the right one. No finding.

D3's remaining bullets under an App unit.

  • Walled postures: unchanged, posture-level, unit-independent. Holds.
  • Uninstall refusal: assertPackageUninstallable(packageId) derives the pair of the id being uninstalled (registry.ts:7311-7313) and names it in PackageUninstallExtendedError with the export door; after refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged #31026 that derivation is pairOf(id), so uninstalling the App names app.objectstack.hotcrm.custom, and an attempt to uninstall one module by id would be refused naming the same pair — consistent with ADR-0019 D4 (uninstall is atomic over the App) and with the amendment's "a module a later version no longer delivers: overrides kept and marked, nothing refused". Holds.
  • It receives: "from the Studio surface of the App or of any of its modules" — holds; the per-package customization read (N-15, D8 item 4) still answers per base, which the shorthand bullet maps to the App's pair filtered by the overridden package.
  • Readable customizable verdict (ruling (b)): one sentence, as the card asked. Holds.

Trying to break the unit — four should-fixes, no must-fix.

  • S1 (should-fix) — the second next-version prerequisite misreads its anchor. D3 says the sys_ heuristics "are replaced by a declared reading (isSystem, packages/spec/src/system/constants/platform-object-names.ts#hasPlatformObjectPrefix), so a customer's sys_custom_<short> object inherits none of them". On origin/main PLATFORM_OBJECT_PREFIXES = ['sys_', 'cloud_', 'ai_'] and hasPlatformObjectPrefix(name) is startsWith over that list (lines 187-201), whose own docblock calls it "the OLD heuristic"; hasPlatformObjectPrefix('sys_custom_x') answers true. No object-level isSystem declaration exists in packages/spec today (the only isSystem keys are the flow/AI service-context flags). So the sentence's consequence holds for a declared isSystem reading and is false for the anchored symbol. The ruling's wording ("isSystem / hasPlatformObjectPrefix()") was copied; the ADR, as the next card's Governing text, should name the declared reading as the thing to add and drop (or explicitly retire) the hasPlatformObjectPrefix anchor for the sys_custom_ case. Not a must-fix: it binds the next version, not step 1, and no step-1 consumer reads it.
  • S3 (should-fix) — no disposition for a pair created under the old unit. D3 has been in force since 2026-10-31 and the ensure door (metadata-protocol: the <base>.custom ensure door — the one idempotent creation path for the paired package, explicit <base namespace>_custom, walled 403 #28407) is landed: an environment may already hold app.objectstack.hotcrm.service.custom (a module's pair) or com.objectstack.<first kernel package>.custom (the one pair today's door creates before the 422s of Customization pairs: every system package shares namespace sys, so only the first can get a .custom pair (422 NAMESPACE_CONFLICT on the rest) #29594). After the change isPairOf('….service.custom', '….service') is false, so that package's declared overrides fall under D2 Q1 / D8 item 2 ("no place under this record") and nothing in the amendment names a ceremony that folds them into the App's pair or says they are refused/kept. One sentence in D11 (or Decided-here 11): a pre-amendment per-package pair is moved into the App's pair by the migration ceremony, or is served until moved — the same discipline D11 applies to package-less rows. The dev's rehearsal plan (dev:crm) is exactly where this bites.
  • S4 (should-fix) — the App of a package is not readable from the tree, and the artifact-less package has no rule. The amendment (and the ruling) define the App as "the type: 'app' package and the packages its artifact delivers". On origin/main nothing persists artifact membership: InstalledPackageSourceSchema is code | marketplace | authored (package-registry.zod.ts:67-71), the registry's artifact scope (scope?.packageIds, registry.ts:8722) is an install-time argument, and a code-booted App (HotCRM under dev:crm, every kernel package) has no artifact at all. The first consumer's appOf is the identity with "the seam the ruling moves" and no declared source. So the ensure door cannot, from the tree as cited, answer "which App delivers app.objectstack.hotcrm.service" — by id prefix? by the defineStack grouping? by a persisted artifact id? — and the ADR does not say, nor does it list the question under "Decided here where the rulings were silent". The same silence covers a package no App artifact delivers: ADR-0141 D3's standalone module, an operator-provisioned internal contribution (ADR-0019 D2), a type: 'plugin' installed alone — "a module never owns a pair" and "every installed App gets one pair" leave it either uncustomizable or its own unit, and refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged #31026's tests treat an "independent package" as its own App. Add a Decided-here item naming the membership reading the ensure door uses (or explicitly delegating it to the server lane's card) and the rule for the artifact-less package. Nit folded in: the ADR names only module and plugin as delivered types; ADR-0019 D2's internal list (driver, server, ui, theme, agent, objectql, adapter) can also travel in an artifact — "every package its artifact delivers" is the safer phrase. Should-fix, not must-fix: the unit decision itself is the maintainer's and holds against ADR-0019/0130; what is missing is the ADR's own convention of recording the silent point, and the server lane's ensure-door PR (which cites this text) can carry the delta before it lands.

Status line, index, window: judged right in ①.

Verdict reasoning. Every ruled item (①, ②'s constraint, ③, (b), not-taken a/c/e, Q4, D11, the sys_ close) is recorded faithfully; the three "decided here" items hold against the live doors; ADR-0019 D1–D4 and ADR-0130 D1/D6 are not contradicted; the README and header-window claims are exact; every check-run on the head is green. The four should-fixes are precision and silence gaps the delta can carry; none makes the design wrong against the code it cites.

Implemented-by: claude/project-thread-kouhp1
Reviewed-by: session_01L9qibzzqHZdAVqgBAvxXKB

VERDICT: PASS

…irs, engines key, isSystem anchor

Contract review record 6692724375 on PR #31027, four should-fixes:

- S1: the next-version prerequisite names an object-level `isSystem` as the
  declared reading to add; `hasPlatformObjectPrefix` is named as the old
  `startsWith` heuristic to retire (it answers true for `sys_custom_<short>`).
- S2: the platform pair carries `engines.platform` as its only `engines` key,
  and the ensure door's existing-pair check reads that key for it.
- S3: D11 and Decided-here 12 — a pair created under the per-package unit is
  folded into its App's pair by the migration ceremony, served until moved.
- S4: Decided-here 11 — the App of a package is read from the declarations the
  registry holds (the `dependency` co-ownership arm), a package that declares
  no App is its own unit; "every package its artifact delivers".

docs/adr/README.md regenerated, byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxn8FPvL85i9w26qoq
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibzzqHZdAVqgBAvxXKB

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 05ea9bbb78a26360c924e342f34357c1a2aa4a01
Local-runs: none
Scope: docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md + packages/objectql/src/registry.ts (coOwnersOf, isShareableNamespace, ArtifactInstallScope, the install-time namespace gate), packages/core/src/artifact-packages.ts (declaredDependencyIds), packages/metadata-protocol/src/protocol-customization-package.ts (customizationPairEnsureRefusal), packages/metadata-core/src/protocol-handshake.ts (resolveDeclaredRange), packages/spec/src/system/constants/platform-object-names.ts, packages/spec/src/kernel/package-registry.zod.ts, packages/apps/{account,setup,studio}/src/index.ts, scripts/adr-index.mjs; objectstack-ai/hotcrm 6961ab1 src/{sales,service,revenue,marketing,o2c}/index.ts; PR #31026. Delta review of the round-1 record on e2cc08e (the template has no Delta-of line, so it is stated here): one commit 05ea9bb on e2cc08e, one file, +56/−12 (whole PR against merge base 33bc8ae: +178/−29). Every cited file has zero drift between the merge base and origin/main 41dc302.

Inputs: the delta diff; the rewritten PR body (adds "Contract review round 1" and Decided-here 11–12); card #31014 unchanged; check-runs on 05ea9bb at 2026-11-06T19:10Z: every completed non-skipped run success, including the two docs jobs — Lint · docs & code guards (carries check:adr-index and check:adr-symbol-anchors) and Lint · repo & tooling gates (carries check:pm-governed-prose) — plus Lint · ESLint, Governed Surface Queue Guard, Check Documentation Links, Check PR Size, PR-body guards, Dogfood Regression Gate, Test UI, Type Check · workspace/source gates/debt ledger, the three Lint · scoped jobs, Lint · PM dispatch-gates, Plan Test Core shards, Auto Label, filter. Still in_progress, none reading docs/adr/**: Lint · CI release & engine gates, Test Core 1/8, Type Check · consumer gates. No red.

① Derived judgments

  • No accept-set or public-surface change (ADR prose only). Right.
  • Status line: still one line (line 27), now ending "merge questions 8 to 12; the App of a package is read from the declarations the registry holds, a package that declares no App is its own unit, and a pair created under the old per-package unit is folded into its App's pair by the migration ceremony (merge questions 11 and 12)". House form kept. Right.
  • Header window still holds: **Status**: line 3, **Decided by**: 28, **Supersedes in part**: 39, **Amends**: 40, **Builds on**: 41 — identical to the round-1 head; the delta added no header line. HEADER_WINDOW 40 reads lines 1–40; the one read field (Supersedes) sits at 39. Right.
  • README byte-identical: the same reasoning as round 1 (STATUS_LINE captures line 3 only; the lengthened line 27 matches no header-field regex). Right; Lint · docs & code guards (check:adr-index) is green on this head.
  • "every package its artifact delivers, whatever its type" replaces "module and plugin" (S4 nit). Right.
  • D3 item 9 bullet, D3 prerequisite sentence, D11 migrate bullet, Decided-here 11 and 12: judged in ④.

② Semver level

Unchanged: docs only, skip-changeset, Clause-②: no, Metadata-protocol: none. Right.

③ Boundary flags

  • Round-1 S1–S4 each claimed fixed in one commit: S1 fixed, S2 fixed, S3 partially fixed, S4 fixed with one edge (④).
  • Dev notes the round-1 commit carries the checkout's configured author (the e2cc08e author deviation stands, declared). Fine.
  • The dev counts 14 path#symbol anchors now; registry.ts#ArtifactInstallScope resolves (registry.ts:3095 export interface ArtifactInstallScope), package-registry.zod.ts#InstalledPackageSourceSchema resolves (line 67). The symbol-anchor gate (Lint · docs & code guards) is green on this head.
  • No new open_questions; no new deviations beyond the above.

④ Design judgment

S1 — fixed. The prerequisite now names "an object-level isSystem the next version adds to the object schema, which no spec key carries today" as the declared reading, and marks hasPlatformObjectPrefix ⛔ as "the old startsWith heuristic over sys_, cloud_ and ai_, which answers true for sys_custom_<short> and is one of the heuristics to retire". Exactly what platform-object-names.ts:187-201 says. Holds.

S2 — fixed. "The platform pair's manifest carries engines.platform as its only engines key — no engines.protocol, which the install handshake reads first — and the ensure door reads that key, not manifest.dependencies, when it checks an existing platform pair." Matches resolveDeclaredRange's order and names the one place today's customizationPairEnsureRefusal (declaredRangeOn over manifest.dependencies, lines 334-335) must gain an arm. With no engines.protocol, assertProtocolCompat at install judges ^18 against PROTOCOL_MAJOR 18. Holds.

S4 — Decided-here 11. Fixed; tried to break it, one edge remains (should-fix S4b).

  • The reading against the tree. declaredDependencyIds(manifest) is Object.keys(manifest.dependencies) (packages/core/src/artifact-packages.ts:196-200), and coOwnersOf's dependency arm filters those ids by holdsNamespace (registry.ts:8727-8731) — the ADR's "the one that holds its namespace where it declares several" is that filter, and the type test (getPackage(id)?.manifest?.type === 'app') reads the same installed record. Code-booted packages reach the registry through installPackage with their manifest (the ensure door already reads base.manifest.version of a dev:crm package), so the declaration is readable at the ensure door with no new persistence. Holds.
  • HotCRM's manifests at 6961ab1. The modules DO declare the App: src/service/index.ts:197, src/revenue/index.ts:101, src/marketing/index.ts:75, src/o2c/index.ts:36, each dependencies: { 'app.objectstack.hotcrm': '^6.1.0' }; src/sales/index.ts:94 is type: 'app' and declares nothing. The sales docblock (line 22) states ADR-0019's "exactly one type: 'app' package per artifact", and line 143 that "dependencies is the edge". Each module declares exactly one app and it holds crm, so appOf answers app.objectstack.hotcrm for all four and the App for itself. Holds.
  • "Never an id prefix" — right: com.objectstack.* kernel ids share a prefix with nothing that is an App, and HotCRM's app.objectstack.hotcrm.* would have given the same answer by accident; declaring the edge is the registry's own discipline.
  • "A type:'app' package is its own App" — an App declaring another App (ADR-0141 D1) stays its own unit, consistent with ADR-0141. The platform's own type: 'app'-looking surfaces (account, setup, studio under packages/apps/*) are type: 'plugin', scope: 'system', so the kernel reading ("a kernel package is never such a unit") meets no system-scoped type: 'app' today; D3's platform-suite paragraph already states the kernel as the exception to "everything above". Holds.
  • ADR-0141 D3's standalone module: member for customization, App for install/open/uninstall; uninstalling it alone keeps and marks its overrides. Consistent with the orphan sentence and with assertPackageUninstallable (after refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged #31026, pairOf(module) names the App's pair). A third-party module with its own namespace that declares one App is also a member under this reading (its overrides land in the vendor App's pair); that is a consequence the ADR accepts, not a break.
  • S4b (should-fix) — a member that declares only members. Item 11 reads direct declarations only (mirroring coOwnersOf: "Direct declarations only, never the closure"). A crm package delivered by the HotCRM artifact that declares only app.objectstack.hotcrm.revenue and not the App (a nested module) declares no type: 'app' package, so item 11 makes it "its own unit: <its id>.custom with crm_custom" — and the ensure door then answers 422 NAMESPACE_CONFLICT (protocol-customization-package.ts:352-358: crm_custom owned by the App's pair), contradicting "never 422 for a package of the same App", while at install the artifact arm admitted it silently. HotCRM has no such module today, so this is narrow. One sentence: a package whose declared parents are members of one App is a member of that App (the one step coOwnersOf's sibling arm already takes), or the readable verdict — never 422 for a package in a namespace an App's member holds.

S3 — Decided-here 12 and the D11 sentence. Partially fixed (should-fix S3b). The decision (fold into the App's pair; served as it is until moved; kernel ones wait with the kernel rows; never refuse or delete customer work) is right and complete as a disposition. The mechanics as written do not run against the live gate: in an environment holding app.objectstack.hotcrm.service.custom (namespace crm_custom, dependency on the module), the App's pair app.objectstack.hotcrm.custom cannot exist beside it — the ensure door answers 422 (line 352-358, another owner of crm_custom) and installPackage's namespace gate throws NamespaceConflictError (registry.ts:8545-8559; the two are not co-owners: the old pair declares the module, which holds crm, not crm_custom). So "its items and declared overrides move into its App's pair … and the emptied package is uninstalled" has no target package until the old one is gone, and until the ceremony runs, Customize on any HotCRM package would meet the 422 the amendment retires. Fix in one sentence: the ceremony re-identifies the pre-amendment pair as the App's pair (same namespace, same items; id and manifest.dependencies rewritten to the App, declarations re-based on the same base), or releases crm_custom first; and until it runs the ensure door answers the readable verdict naming the ceremony, not 422. Not a must-fix: the ceremony is D11's, not yet carded, and the decision it implements holds.

No regression in D3's other bullets, the platform suite paragraph, Q4, Consequences, Refs. ADR-0019 D1–D4 and ADR-0130 D1/D6 still not contradicted; item 11 leans on ADR-0141 D1/D5 (declared, checked dependency; co-ownership by declaration) exactly as the registry reads them.

Verdict reasoning. All four round-1 should-fixes are addressed; S1, S2 and S4 hold against the code they cite and HotCRM's real manifests; S3 and the nested-member edge each want one more sentence. No must-fix. Every check-run that reads the ADR surface (adr-index, symbol anchors, governed prose, documentation links, queue guard) is green on this head; the three still running (CI/release/engine gates, Test Core 1/8, consumer type gates) read no docs/adr/** path, and the seat reads them complete before the hand-over to os-zhuang.

Implemented-by: claude/project-thread-kouhp1
Reviewed-by: session_01L9qibzzqHZdAVqgBAvxXKB

VERDICT: PASS

…lk declared parents

Delta review record 6693213712 on PR #31027, two should-fixes:

- S3b (Decided-here 12, D11): a pair created under the per-package unit
  is re-identified as its App's pair by the ceremony — id and dependency
  rewritten, namespace, items and rows kept — because the old pair owns
  the `_custom` namespace and both the ensure door and the install door
  refuse a second owner; until then the ensure door serves the readable
  verdict naming the ceremony, not 422.
- S4b (Decided-here 11): the membership reading walks declared parents,
  so a nested member whose parents all resolve to one App is that App's
  member, never 422 inside one App; parents resolving to different Apps
  get the readable verdict.

docs/adr/README.md regenerated, byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxn8FPvL85i9w26qoq
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibzzqHZdAVqgBAvxXKB

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b13af10317bcebb6fc516319d4575934e3c7e100
Local-runs: none
Scope: docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md + packages/objectql/src/registry.ts (coOwnersOf, the install-time namespace gate, assertPackageUninstallable), packages/core/src/artifact-packages.ts (declaredDependencyIds), packages/metadata-protocol/src/protocol-customization-package.ts (customizationPairEnsureRefusal, the existing-pair check), packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts (rows bound by package_id), packages/spec/src/kernel/override-declaration.zod.ts (overrides.package, basedOn), packages/metadata-protocol/src/protocol-write-gates.ts, scripts/adr-index.mjs, docs/adr/0019-app-as-consumer-unit.md (D4), docs/adr/0141-inter-app-dependency-declared-checked-before-install.md; objectstack-ai/hotcrm 6961ab1 src/*/index.ts; examples/app-multi-package. Delta review of the round-2 record on 05ea9bb (delta-of stated here; the template has no Delta-of line): one commit b13af10 on 05ea9bb, one file, +27/−15 (whole PR against merge base 33bc8ae: +190/−29). Drift of cited files since the merge base: only registry.ts, by #30756 (asEnabled on the disabled-package extension fold, lines 748-5350) — none of the judged doors moved.

Inputs: the delta diff; the rewritten PR body (adds "Contract review round 2 … fixed on b13af10", Decided-here 11/12 restated); card #31014 unchanged; check-runs on b13af10 at 2026-11-06T19:26Z: every completed non-skipped run success (Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, PR-body guards, Test UI, filter); still in_progress: Lint · CI release & engine gates, Lint · ESLint, Lint · docs & code guards (check:adr-index, check:adr-symbol-anchors), Lint · repo & tooling gates (check:pm-governed-prose), Test Core 1/8, the four Type Check jobs. No red. The dev reports the same 21 derived families green locally on this head; the two docs jobs are the ones the seat reads complete before the hand-over.

① Derived judgments

  • No accept-set or public-surface change (ADR prose only). Right.
  • Status line (line 27, still one line): "folded into" becomes "re-identified as its App's pair by the migration ceremony". House form kept. Right.
  • Header window holds: **Status**: 3, **Decided by**: 28, **Supersedes in part**: 39, **Amends**: 40, **Builds on**: 41 — unchanged from the two earlier heads; the delta added no header line; HEADER_WINDOW 40 reads lines 1–40. Right.
  • README byte-identical: same reasoning as rounds 1–2 (only line 3 is the status value; line 27 matches no header-field regex). Right; the proving job is in progress.
  • D11 migrate bullet and Decided-here 12 (S3b), Decided-here 11's parent walk (S4b): judged in ④.

② Semver level

Unchanged: docs only, skip-changeset, Clause-②: no, Metadata-protocol: none. Right.

③ Boundary flags

  • Round-2 S3b and S4b each claimed fixed in one commit: S3b fixed for the measured case with one over-claim (④ S3c); S4b fixed in intent, under-specified in two places (④ S4c).
  • Dev notes the round-1 and round-2 commits carry the checkout's configured author; the e2cc08e author deviation stands, declared.
  • No new open_questions, no new anchors beyond customizationPairEnsureRefusal (already resolving).

④ Design judgment

S3b — Decided-here 12 and the D11 sentence. Fixed for the case the tree holds; one over-claim (should-fix S3c).

  • Coherent against the registry and the install door? Yes as a ceremony design, and only as one. No door on main rewrites an installed record's id (the update door edits a manifest under its id; the registry keys packages and namespaceOwners by id), so re-identification is a new step of D11's CLI ceremony, which the ADR already places under ADR-0131 D10's shape — acceptable for a record that "declares; implements nothing". It is the right choice against the gates: a fresh App pair cannot be created beside the old one (ensure door 422 at protocol-customization-package.ts:352-358; install door NamespaceConflictError, registry.ts:8545-8559, the two not being co-owners), and uninstalling first would drop the old pair's object_extension items with their __c column data — the ADR's reason is the tree's. Wording nit folded into S3c: "its namespace, items, rows and declared overrides stay in place" — rows bind to a package by package_id (12 sites in protocol-packageless-overlay-migration.ts), as do the namespace-owner set and the sys_packages row, so "stay in place" must mean "re-bound to the new id", not "untouched"; one clause makes that unambiguous.
  • Does "same base, same basedOn" hold? Yes. overrides.package names the BASE package whose item is replaced (override-declaration.zod.ts:78-92: "never a .custom package"), i.e. the module that ships the item, and basedOn is that base's manifest.version (lines 93-99, "recorded, never judged"). Re-identifying the pair changes neither: the declaring item still names app.objectstack.hotcrm.service at 6.1.0, and under the App unit the write gate's pairOf('…service') is the App's pair (refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged #31026's seam), so the declaration is valid in its new package. One artifact one version (ADR-0130 D6) makes the module's version the App's. Holds.
  • S3c (should-fix) — "the old unit let an App hold at most one such pair" is true only when every member holds the App's namespace. The old door refused a second owner of <ns>_custom, so for HotCRM (all five packages crm) at most one pair in crm_custom existed — the measured case holds. But ADR-0019 D4 gives an App "a set of namespaces" (its Context cites 13 sub-plugins "each with its own namespace"; ADR-0141:473 reads D4 the same way), and a module in crm_svc got a pair in crm_svc_custom beside the App's crm_custom with no refusal — so an App could hold several old pairs, one per distinct member namespace. For such a pair re-identification is wrong twice: two records cannot both become app.objectstack.hotcrm.custom, and a re-identified pair keeping crm_svc_custom violates Q4 (<app namespace>_custom) and is refused by the ensure door's existing-pair check ("lacks namespace 'crm_custom'", lines 330-333). In exactly that case the gates do NOT block the round-1 move (different namespaces collide with nothing), so the fix is one sentence with two arms: the old pair in the App's own namespace is re-identified; an old pair in another member namespace is moved into the App's pair (items and declarations re-based on the same base, same basedOn) and uninstalled once empty — its __c extensions moved, not dropped. No artifact in objectstack or hotcrm has a module whose namespace differs from its App's today (examples/app-multi-package orders is crm like its core), so this is a rule gap, not a live break: should-fix.
  • Kernel arm: "the whitelist version re-identifies it as the platform pair and lists the rows it cannot admit" is consistent with item 9 (the dependency becomes engines.platform; the only sys_custom owner is that one old kernel pair, since sys is one namespace and the old door 422'd the second). Holds.

S4b — Decided-here 11's parent walk. Fixed in intent; under-specified (should-fix S4c). The sentence: "a package that declares no type: 'app' package directly but whose declared parents all resolve to one App is a member of that App, and one whose parents resolve to different Apps gets the readable verdict — never 422 for a package of the same App". That closes the nested-member hole and matches declaredDependencyIds (Object.keys(manifest.dependencies), core/artifact-packages.ts:196-200) as the edge. As ADR text it must be unambiguous, and it leaves three readings open:

  1. Which parents count. A nested member that declares app.objectstack.hotcrm.revenue AND a kernel package (com.objectstack.audit) or a utility plugin that is "its own unit": under a literal reading its parents "resolve to different Apps" (HotCRM, the platform suite, the plugin's own unit) and it gets the verdict, which is the wrong answer for a package of the same App. The rule should count only parents that resolve to an App (a type: 'app' package, directly or by their own walk); kernel packages and own-unit packages are not Apps for this reading and are skipped.
  2. Termination. "Resolve" recurses with no depth or cycle rule; two modules declaring each other with no App above them loop. One clause: the walk is over the declared-dependency closure, each package visited once; a package the walk cannot resolve to an App is its own unit (or the verdict), never a loop. Note the ADR's own line 837 still says the direct rule reads what the package "directly declares" and coOwnersOf's docblock insists on "direct declarations only, never the closure the save door walks" — the walk IS a closure now, which is fine, but the record should say so and bound it.
  3. Tiebreak. The direct rule picks, among several declared Apps, "the one holding its namespace"; the walk says "all resolve to one App" with no tiebreak. State that the same namespace tiebreak applies to the set the walk returns, and the verdict only when it leaves none or several.
    The ADR anchors none of this to new code, HotCRM has no nested module (verified at 6961ab1: all four modules declare the App directly), and refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged #31026's appOf stays the identity until the ensure-door PR, so this is a precision should-fix, not a must-fix.

No regression elsewhere: D3's unit bullets, the platform suite paragraph, Q4, Consequences, items 8–10 and the status line are as reviewed in rounds 1–2; ADR-0019 D1–D4 and ADR-0130 D1/D6 still not contradicted (S3c is the ADR applying ADR-0019 D4 incompletely, not contradicting it).

Verdict reasoning. Both round-2 should-fixes are addressed as asked: the re-identification is the right mechanism against the live gates and keeps overrides.package/basedOn intact, and the parent walk removes the nested-member 422. What remains is one over-claim (a multi-namespace App under the old unit) and the walk's three unstated rules; each is one sentence and neither makes the design wrong against the code it cites or the HotCRM scenario it is accepted against. No must-fix. No CI red at write time; the docs jobs are in progress and the seat reads them complete before the hand-over to os-zhuang.

Implemented-by: claude/project-thread-kouhp1
Reviewed-by: session_01L9qibzzqHZdAVqgBAvxXKB

VERDICT: PASS

…e parent walk is a closure

Delta-2 review record 6693528804 on PR #31027, two should-fixes:

- S3c (Decided-here 12, D11): only the pair in the App's own namespace is
  re-identified; a pair in a member's own namespace (ADR-0019 D4) is moved
  into the App's pair, never dropped; rows are re-bound by package_id.
- S4c (Decided-here 11): the parent walk is the closure of declared
  dependencies, each package visited once, stopping at an App; only Apps
  reached count, kernel and own-unit packages are skipped; the direct
  rule's namespace tiebreak applies to the result set.

docs/adr/README.md regenerated, byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxn8FPvL85i9w26qoq
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibzzqHZdAVqgBAvxXKB

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2990a3dc269bc2a42d4c384d3106e77351e9451a
Local-runs: none
Scope: docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md + packages/objectql/src/registry.ts (coOwnersOf, the install-time namespace gate, isShareableNamespace, assertPackageUninstallable), packages/core/src/artifact-packages.ts (declaredDependencyIds), packages/metadata-protocol/src/protocol-customization-package.ts (customizationPairEnsureRefusal), packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts (rows bound by package_id), packages/spec/src/kernel/override-declaration.zod.ts, packages/spec/src/kernel/namespace-prefix.ts (validateObjectNamespacePrefix), scripts/adr-index.mjs, docs/adr/0019-app-as-consumer-unit.md (D2, D4), docs/adr/0141-inter-app-dependency-declared-checked-before-install.md (D1, D3); objectstack-ai/hotcrm 6961ab1 src/*/index.ts. Delta review of the round-3 record on b13af10 (delta-of stated here; the template has no Delta-of line): one commit 2990a3d on b13af10, one file, +28/−17 (whole PR against merge base 33bc8ae: four commits, ADR file only). Drift of cited files since the merge base: only registry.ts by #30756 (disabled-package extension fold), none of the judged doors moved. This is the seat's final fix round; the record judges the head the seat takes to ACCEPT.

Inputs: the delta diff; the rewritten PR body (adds "Contract review round 3 … fixed on 2990a3d", Decided-here 11/12 restated); card #31014 unchanged; check-runs on 2990a3d at 2026-11-06T19:40Z: every completed non-skipped run success (Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, Lint · PM dispatch-gates, the three Lint · scoped jobs, PR-body guards, Plan Test Core shards, Test UI, Type Check · workspace, Type Check · debt ledger, filter); still in_progress: Lint · docs & code guards (check:adr-index, check:adr-symbol-anchors), Lint · repo & tooling gates (check:pm-governed-prose), Lint · ESLint, Lint · CI release & engine gates, Test Core 1/8, Type Check · consumer gates, Type Check · source gates. No red. The dev reports the 21 derived families green locally on this head; the seat reads the two docs jobs complete before the hand-over.

① Derived judgments

  • No accept-set or public-surface change (ADR prose only). Right.
  • Status line (line 27, one line): "re-identified as, or moved into, its App's pair by the migration ceremony". House form kept. Right.
  • Header window holds: **Status**: 3, **Decided by**: 28, **Supersedes in part**: 39, **Amends**: 40, **Builds on**: 41 — identical on all four heads; the delta added no header line; HEADER_WINDOW 40 reads lines 1–40, the one read field sits at 39. Right.
  • README byte-identical: same reasoning as rounds 1–3 (only line 3 is the status value; line 27 matches no header-field regex). Right; the proving job (Lint · docs & code guards) is in progress at write time and was green on the three earlier heads with the same header.
  • D11 migrate bullet and Decided-here 12 (S3c), Decided-here 11's walk (S4c): judged in ④.

② Semver level

Unchanged: docs only, skip-changeset, Clause-②: no, Metadata-protocol: none. Right.

③ Boundary flags

  • Round-3 S3c and S4c each claimed fixed in one commit: both fixed (④). No new open_questions; no new path#symbol anchors in the delta.
  • The author deviation on e2cc08e stands as declared; the three fix commits carry the configured author.
  • The seat's note that this is the final fix round is honoured: the two remaining observations below are notes for the server lane's ensure-door card, not requests for another ADR round.

④ Design judgment

S3c — fixed. Decided-here 12 and the D11 sentence now have the two arms the round-3 record asked for: the pre-amendment pair in the App's own namespace is re-identified (id and dependency rewritten; rows re-bound by package_id; items and declared overrides kept, same base, same basedOn), and a pair in a member's own namespace (ADR-0019 D4) is moved into the App's pair "created or re-identified first" and the emptied package uninstalled — never dropped. Against the gates: the App's pair in crm_custom collides with nothing in crm_svc_custom, so the ensure/create runs; the emptied .custom package derives no pair of its own and extends nothing, so assertPackageUninstallable lets it go; "at most one pair per namespace existed" is the correct statement of what the old door's 422 guaranteed (one owner per <ns>_custom; sys is shareable but sys_custom is not). The "rows stay in place" nit is taken ("re-bound to the new id (they bind by package_id)"). Holds.

S4c — fixed. Decided-here 11's walk is now unambiguous on the three points: (1) only type: 'app' packages reached count, a kernel package or an own-unit package on the way is skipped, with the worked case (a member declaring a module and com.objectstack.audit is still that App's member); (2) it is the closure of declared dependencies, each package visited once so a cycle terminates, and it stops at a type: 'app' package (ADR-0141 D1: an App's own dependencies are another App's business — right, an App depending on an App stays its own unit); (3) several Apps take the direct rule's tiebreak (the one holding the package's namespace), and the readable verdict where that leaves none or several, never 422. Well-defined as text: the walk is a bounded closure over declaredDependencyIds with a visited set, the result set is filtered to Apps, and the tiebreak and verdict are those of the direct rule. HotCRM at 6961ab1 is unaffected (all four modules declare the App directly). Holds.

Must-fix hunt — none found. Read the whole amended D3, Q4, Consequences, D11, items 8–12 and the status line once more as a server-lane card would build on them:

  • The ensure door's answers form a total function over a base: kernel package (scope system/cloud) → readable verdict, no pair this version; type: 'app' → its own pair; a package whose direct or walked declarations reach one App (after the namespace tiebreak) → that App's pair, 201 then 200, never 422; several Apps left after the tiebreak → readable verdict; no App reached → its own unit's pair <id>.custom, <ns>_custom; a pair namespace held outside the artifact → 422 NAMESPACE_CONFLICT (the one 422 kept). No two rules claim the same input with different answers.
  • Items 9 and 12 agree on the platform pair's manifest (engines.platform only; the old kernel pair re-identified in the whitelist version with its dependency replaced by that key).
  • Items 10 and 12 agree on what waits (kernel rows and the old kernel pair, served as they are, writes open).
  • overrides.package/basedOn keep naming the shipping module and its version through re-identification and move, and pairOf(module) makes those declarations valid in the App's pair (override-declaration.zod.ts:78-99; refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged #31026's seam).
  • Uninstall: the refusal names the App's pair for the App and for a module asked by id; the standalone module's own uninstall keeps and marks its overrides — consistent with ADR-0019 D4 and ADR-0141 D3.
  • ADR-0019 D1–D4, ADR-0130 D1/D6, ADR-0141 D1/D2/D3/D5 are read, not contradicted.

Notes for the server lane's ensure-door card (should-fix notes, not ADR rounds):

  • N1 — the moved pair's own objects. A new object authored into a member-namespace pair is named crm_svc_custom_<short> (Q4's rule for that pair). Moved into the App's pair (namespace crm_custom), it fails validateObjectNamespacePrefix (namespace-prefix.ts:28-43: the name must start with crm_custom_) at the next publish of the App's pair, and renaming it means renaming a table with data. The ADR's "never dropped" is right; the ceremony card should decide whether the App's pair owns a set of namespaces for the moved objects (ADR-0019 D4 applied to the pair — the gate then admits crm_svc_custom_ under it, which needs a gate arm) or the objects keep their names under a declared exception. No live instance: no module in objectstack or hotcrm holds a namespace different from its App's today.
  • N2 — "A module never owns a pair" is scoped to members. The D3 bullet heading is absolute (the ruling's words), while item 11 gives a package that reaches no App — including a type: 'module' installed with no App declaration — its own pair <id>.custom. Item 11 is the explicit, later, "decided here" text and governs; the ensure-door card should read the bullet as "a module of an App never owns a pair" and not refuse a standalone, App-less module with the verdict on the heading's strength. One word in a future amendment ("A member never owns a pair") would close it; not needed for this landing.

Verdict reasoning. Both round-3 should-fixes are fixed as asked; the amendment is internally consistent as a server-lane card would build on it; the status line, index and header window hold; no check-run is red. The two notes are for the implementing card, not for this record's text. No must-fix.

Implemented-by: claude/project-thread-kouhp1
Reviewed-by: session_01L9qibzzqHZdAVqgBAvxXKB

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibzzqHZdAVqgBAvxXKB

维护者速读

席位意见:可以落地,待授权批准。

席位核对

ACCEPT · seat domain:spec#1 · head 2990a3dc · 2026-11-06T19:51Z

  1. Shape: Fixes #31014, draft, base main, 1 file (docs/adr/0146-…md), +201/−29. docs/adr/README.md regenerates byte-identical (the index cell reads line 3, which is unchanged). skip-changeset, Clause-②: no / Metadata-protocol: none, matching claim 6691185615. Authored on CONTRACT_REVIEW_TIER (Fable, switched by the maintainer 2026-11-06T18:16Z).
  2. Ruling: ① 6691022506 and ③ 6691217245 on Customization pairs: every system package shares namespace sys, so only the first can get a .custom pair (422 NAMESPACE_CONFLICT on the rest) #29594, quoted verbatim on the status line. The not-taken options (a), (c) and (e) are recorded. ADR-0019 and ADR-0130 are not amended.
  3. Contract review: at CONTRACT_REVIEW_TIER by an isolated reviewer, four rounds, every one PASS and none with a must-fix:
  4. Gates / check-runs: dev: 21 of 21 derived gate families exit 0 on 2990a3dc, reconciliation 21/21, check:pm-governed-prose 0, merge with origin/main clean (report 6692464232 and the PR body's Verification). On head 2990a3dc: 45 runs, 26 success, 19 skipped, 0 failed, 0 running (docs-only diff); mergeable_state clean. check-governed-merges --pr 31027: Tier H (docs/adr/** ×1), 230 changed lines.
  5. Accepted deviations: the commit author on e2cc08eb is the fleet bot (the branch is not force-pushable; a squash replaces it). HotCRM is measured as four modules, not three. The range source is the fixed-group release version, not resolveRuntimeVersion() (Decided-here 9). Decided-here 10–12 go beyond the order, and each is flagged for the maintainer.
  6. Landing: Tier H: pm:awaiting-merge plus a review requested from the approvers. ⛔ No ready, queue or auto-merge before an APPROVED review from a GOVERNED_APPROVERS account; after it, the seat lands. If the maintainer readies and enqueues it himself, that hand is the record.

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 6, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(adr): ADR-0146 D3 amendment — one .custom pair per App, and one platform pair for the kernel suite (ruling 6691022506 on #29594)

1 participant