Repository navigation
Description
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
Part of #28236 · blocks N-09 (write gate, not yet cut — it will carry Blocked-by: this card) · lane: domain:skills (docs/adr/**) · opened at the maintainer's direction in a direct desktop session, verbatim: 「落卡:缺口一和三写进 N-09 的卡面(合并后切卡时),缺口二开决策卡」, then 「缺口二开一个 fable 综合评估」. Riders for gaps 1 and 3: #28236 comment 6541767586. ⛔ Not a claim.
一句话问题
伙伴的扩展包(依赖 HotCRM)先声明覆盖了 HotCRM 的「客户列表」视图,之后客户想在自己的 hotcrm.custom 里改这个视图,会被当成「第二个声明者」拒绝;客户也没法在伙伴的覆盖上再改一层。反过来,客户先改了,伙伴包就发布不了,或者按缺口一绕过检查装进来。谁说了算由谁先发布决定,这和 ADR「客户的定制都进 .custom」的前提冲突。
背景与 Governing text
ADR-0146 已合入(#28295,docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md,main):
- D2 :188–189:「A dependent package a vendor or partner ships may carry the same declaration …; the customer's own changes still live in
.custom.」 - D2 :212–214:「The declaration is open to every package in the closure, not only
.custom… One declarer per base item: a second package declaring the same base item is refused at publish, naming the first.」 - D2 :224–227 + D3:只能覆盖闭包内的项;
.custom只依赖基础包,伙伴包不在它的闭包里。所以.custom既不能当第二个声明者,也不能覆盖伙伴的覆盖。 - D8.2 :404–408:只返回声明选中的那一份,「whichever closure package declared it」;分层读取只有
code/override/effective三层,override写死为「the.customitem」,没有声明者之间的先后。 - D9.1 :433–434 同样没有先后。
- 来源:[Decision] ADR-0146 rewrite (#28237): is an undeclared same-named item of a non-pair dependent package refused at publish like the pair's, or kept as that package's own item — and is
actionoverridable in this epic #28364 裁决 6540351677 的 rider 3,原文「Any package in the closure may declare an override, not only.custom; one declarer per base item — the second publish is refused naming the first.」 - 裁决记录里写的代价只有「改一个通用名」(ADR :217–219),没写「客户失去对该项的定制权」。[Decision] ADR-0146 rewrite (#28237): is an undeclared same-named item of a non-pair dependent package refused at publish like the pair's, or kept as that package's own item — and is
actionoverridable in this epic #28364 选项 A 的原文是「要改基础包,就到.custom声明覆盖」。rider 3 是在裁决对话里加的扩展,它的后果此前没出现在任何记录上。 - 没有现成文本能化解:ADR-0048 §3.3/§3.4 只管无依赖关系的两个包;ADR-0141 D5 讲的是命名空间;ADR-0126 §7.1 把「按到达先后决定」判为缺陷。
协议声明: 本卡只定 ADR 文本,不改 spec 键。overrides 键由 N-08 落地,门禁由 N-09 实现,分层读取由 N-10 实现。
前提(每条带 re-check)
- 上面引的五处原文仍在 main。re-check:
git fetch origin main && git show origin/main:docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md | sed -n '188,189p;212,214p;404,408p;433,434p' - 今天没有任何代码路径能让非
.custom的依赖包覆盖基础项。re-check:git show origin/main:packages/metadata-protocol/src/protocol-environment-rows.ts | sed -n '200,211p'(fix(metadata-protocol): a dependent package's same-named row no longer replaces the base package's item on the read naming no package #28304 后,唯一出品该名的代码包就是这个地址的主人,「a row bound to another package … is never served」);grep -rn "overrides\|basedOn" packages/spec/src零命中。 - 仓内没有伙伴或 ISV 包。唯一的
manifest.dependencies示例是同一厂商的模块(examples/app-multi-package/src/packages/orders/index.ts:65),而且没有同名项;「伙伴」只出现在枚举(content/docs/references/marketplace/package.mdx:60)和设计稿(docs/design/marketplace-publishing.md:143)里。re-check:git grep -n "dependencies" origin/main -- 'examples/**/index.ts' 'examples/**/objectstack.config.ts' - 安装入口只查
DEPENDENCY_MISSING,不查声明槽位。re-check:git show origin/main:packages/metadata-protocol/src/protocol-package-install.ts | sed -n '258,268p'
选项
| 选项 | 做法 | 业务效果(HotCRM / 伙伴扩展包 / hotcrm.custom) |
工作量 |
|---|---|---|---|
A 只有 .custom 能声明覆盖(撤回 rider 3) |
依赖包里和基础包同名的项只有一条出路:改名。依赖包带 overrides 声明,在发布、os validate、安装三处都拒绝 |
伙伴只能加新名字的视图,再通过导航贡献挂进去(ADR-0029 D7)。客户永远有最后一句话。厂商要改自己模块的视图,发新版即可 | ADR 收窄约 9 处(清单见裁后执行)· N-09 少一个「naming the first」分支 · N-10 保持两层 · 小 |
B 保留 rider 3,.custom 永远在最上层 |
加一句「配对包的声明压过任何依赖包的声明;依赖包之间仍只许一个声明者」 | 伙伴先覆盖了,客户在 .custom 里照样能改,并且最终生效;伙伴那份被整份遮住,不做合并。伙伴之间仍然是先到者独占 |
D2、D8.2、D9.1 各加一级先后 · 分层读取多一层并标出来源包 · N-11 流程排序多一档 · 中 |
| C 维持现状,把代价写进 ADR | 原样,代价清单加一段 | 伙伴先到,客户只能去求伙伴改;客户先改了,伙伴包装不上,或者绕过检查装上后环境里有两个声明者,而 D8 没规定听谁的 | ADR 加一段 · 零 |
不列:「按闭包深度排序、多层叠加」。这要永久维护一套通用排序,而且没有需求。
推荐:A(若坚持 rider 3,则取 B;⛔ 不取 C)
- 长远:一个基础项的覆盖者由角色决定,不由先后决定。
.custom是客户层,依赖包是贡献层,两者不争同一个名字。分层读取保持草稿的两层,以后做升级扫描(缺口三)也只需比对.custom一处。 - 拉动:伙伴覆盖基础项的实测拉动是 0,仓内连一个伙伴包都没有。真实拉动只有客户定制,这一点 HotCRM 走查已有记录。
- 防错:依赖包只有「改名」一条出路,AI 不会把客户定制错写进伙伴包。选 C 的话,客户的
.custom被拒,AI 会去想办法绕。 - 不扩散:A 收窄一项零拉动能力,少一个分支,不加门禁。
- 重开 rider 3 的理由是新事实:裁决记录上的代价只有「改通用名」,没有「客户失去定制权」。Salesforce 托管包的做法与 A 同形:扩展包只能在自己的命名空间里新增,订阅方才是覆盖层。这一点未验证,凭通用知识,仓内没有资料。
os-decision-facets
① 长远:A 由角色定覆盖者(.custom 独占),读取两层不变;B 客户层在上但伙伴之间仍先到先得;C 语义权利由安装先后决定,违背 D2「customer's own changes still live in .custom」
② 拉动:伙伴/ISV 依赖包仓内为 0,无代码路径让非 .custom 依赖包覆盖;客户定制拉动已实测(HotCRM 走查)
③ 防错:A 依赖包同名唯一出路改名,三点响亮拒;B 伙伴覆盖被静默遮住难察觉;C 客户 .custom 被拒诱导绕行
④ 不扩散:A 收窄零拉动能力、少分支、不加门;B 多一档先后;C 不加东西但留洞
Prior rulings read: closure any-package declare / one declarer per base item → 1 hit (#28364 ruling 6540351677 rider 3); ADR-0048 §3.3/§3.4, ADR-0126 §7.1, ADR-0141 D3/D5; ADR-0146 :217–219 cost-on-record
与缺口一/三的关系
- 缺口一(检查只在发布时,安装和
os validate不查;已作为 rider 记在 N-09):在 rider 3 下,伙伴包带着声明安装进来时,安装入口不知道.custom是否已经占了这个槽位,环境里会出现两个声明者,而 D8 没规定听谁的。这是本缺口伤得最深的地方。选 A,安装入口的检查就退化成一条与类型无关的规则:「依赖包工件不得携带overrides,不得与基础包同名」,照 D10__c三处执法的先例落进 N-09,不另开新门。 - 缺口三(基础包升级新增名字,客户的项静默变成孤儿):选项不改变缺口三本身。但选 A 后,合法持有基础包名字的只剩
.custom,升级扫描只需比对一处;选 B 还要重新判断每个伙伴声明和配对包的先后。
裁后执行
-
裁 A:开一个 ADR-0146 修订 PR(Tier H,维护者亲合),收窄以下各处:
- :188–189 删伙伴那句。
- :191–192 改为「An item of the base's paired
.custompackage」。 - :212–214 改为「The declaration is admitted from the paired
.customalone; a dependent's same-named item has one fix, rename; a dependent that carries the declaration is refused at publish, atos validateand at the install door」,:210「the refusal names both fixes」只对.custom成立。 - :408 改为「the
.customitem that declares it」;:433–434 改为「from.custom」。 - 替代表 :524 / :526 / :530 收窄。
- Q1 行 :553 和 Consequences :580 同步。
- N-09 行 :603 改为「admit declared (from
.custom), refuse undeclared / out-of-closure / excluded type / a dependent's declaration」。 - ADR-0048 :3、ADR-0126 :3 的指针行删去「or the declared override a package in its dependency closure publishes」。
N-09 卡面按 A 写。
-
裁 B:同样开修订 PR,在 :214 后加「The pair outranks every other declarer:
.custom's declaration is served over a dependent's and is never refused as a second declarer; among dependents, one declarer per base item.」D8.2 写明三级先后code < dependent override < .custom override,分层读取的override标出来源包,D9.1 同步。N-10、N-11 卡面加上这一档。 -
裁 C:ADR 代价清单加一段「伙伴先声明则客户失去该项的定制权,反之亦然」。N-09 照现文实现,安装入口也要拒第二个声明者。
-
裁定后本卡关闭,N-09 摘掉
Blocked-by:,按裁定切卡派发。
Analysis-tier: fable comprehensive assessment (desktop session subagent), seat spot-checked ADR :186–219, :404–408 and protocol-package-install.ts:258–268 on main
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 31, 2026
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
2026-10-31T10:22Z
Ruled: A — only the paired
<base>.customdeclares an override; rider 3 of 6540351677 ("any package in the closure may declare") is withdrawn.Maintainer, verbatim, in the desktop PM session, replying 「同意」 to the seat's business explanation that recommended A (partner packages only add items under their own names; the customer's
.customis the one layer that changes a base item; owner fixed by role, not by publish order). Freshness gate: thread had 0 comments at recording; body unedited since filing.What this card now carries (execution card,
pm:queue, lanedomain:skills): one ADR amendment PR todocs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md(Tier H — maintainer merges), applying the "裁 A" list in this card's body verbatim in intent:.custompackage" (:191–192); the closure paragraph (:212–214) becomes — a dependent's same-named item has one fix, rename; a dependent that carriesoverridesis refused at publish, atos validateand at the install door; "the refusal names both fixes" holds for.customonly..customitem that declares it"; D9.1 (:433–434) "from.custom".Line numbers are readings of main at filing; the PR re-reads them. PR body:
Fixes #28390,## 维护者速读(草稿)per the governed-surface rule.Effect on the N-09 riders (#28236 comment 6541767586): under A the Supersede-table residue and the D2-heading residue become true as written and need no edit; gap 1's install-door check reduces to "a dependent artifact carries no
overridesand no base item's name". N-09 carriesBlocked-by: #28390and implements A.[-][Decision] ADR-0146 D2: when a partner's dependent package declares an override of a base item, can the customer's `.custom` still customize that item — or does the first declarer hold it[/-][+]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)[/+]objectstack-fleet commentedon Oct 31, 2026
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
2026-10-31T12:20Z
Scope added to this card's amendment PR. Maintainer, verbatim, in the desktop PM session: 「同意」 to the seat's recommendation that N-09's ADR edits ride in this PR so N-09 (#28410) stays code-only. Add to the "裁 A" list in 6542177429:
actionin a dependent is refused like any other kind; until action override opens, the only fix is rename..customitem already holds displaces that item (D8 item 2) — no door judges a base upgrade (ADR-0141 D3 deferred); N-09 makes the install/upgrade door name each collision, N-10 surfaces the orphan in the layered read.Source: N-09 riders on the epic (6541767586 item 3 and item 2(b), amended by 6542181562). The Supersede-table and D2-heading residues need no edit under A.
objectstack-fleet commentedon Oct 31, 2026
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
2026-10-31T22:49Z
Notify: scope · #28390 · one more ADR-0146 text item, from the N-08 ruling
The maintainer's ruling on PR #28467 (recorded on #28404 as 6549129352: 田騏, verbatim 「同意」, 2026-10-31 20:44Z) carries follow-up (c):
basedOnrecords the base package'smanifest.versionthe override was taken from, not a history version of the item; "the ADR-0146 D2 wording is to be aligned in the #28390 text amendment". PR #28467 already changed the schema's.describe()to this meaning.Added to this card's amendment, beside the items in 6543281223. Lines read on
origin/maintoday:basedOn: '<the base item's version the override was taken from>'→ the base package'smanifest.version.basedOnrecords which version of the base's item the author started from" → which version of the base package.basedOnset to the base item's version at migration" → the installed base package's version at migration.basedOnbeing recorded and read by a human, and need no change of meaning. Re-read them when editing, in case a sentence there says "item version".Nothing else changes:
basedOnstays recorded and never judged, which is the epic's Not in this epic line.objectstack-fleet commentedon Oct 31, 2026
domain:skills#1 · os-tesla · session_01L9qibzCtJt8Q2Z63fyRNix
Claim: skills seat 1 round 1 (single card) · card thread · 2026-10-31T22:59Z
Session:
session_01L9qibzCtJt8Q2Z63fyRNix(the seat); card threadsession_01L9qibzxSvCxtE6cpabwBDyAccount:
os-tesla(the seat's linked user; the card's assignee)Branch:
claude/project-thread-i8f3y5Worktree: the card thread's own container checkout
Domain:
domain:skillsSeat:
domain:skills#1File surface:
docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md, the:3pointer lines of ADR-0048 and ADR-0126. Stop on breach.Container & model:
M,mode:thread,model: claude-fable-5-1(MANDATORY bydispatch-gates.mjs --tierfor this surface), effortxhigh; contract-review tier before the queue; Tier HClause-②: no
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6550188803
Premise: holds (PR #28295 merged the ADR; the ruled lines still carry rider 3's wording; the dev re-reads each named line on
main; the dev re-checks first)Ruling-ref:
6542177429(A, maintainer 「同意」), scope6543281223, scope6550188803(from N-08 ruling6549129352)Serial constraints cleared: open PR #28467 (another lane) touches
skills/*/references/_index.mdonly, outside this surface; no other open PR or claim touches itResume-from: none (fresh from
origin/main)Order:
/mnt/project-files/skills-seat-1/r2026-10-31/dispatch-28390.md(project file)State:
pm:queue→pm:dispatched, assigned toos-tesla, recorded by this claim.objectstack-fleet commentedon Oct 31, 2026
domain:skills#1 · os-tesla · session_01L9qibzxSvCxtE6cpabwBDy
os-dev-report
Posted 2026-10-31T23:26Z · CI check-runs read 2026-10-31T23:24Z
{ "issue": 28390, "status": "done", "branch": "claude/project-thread-i8f3y5", "pr": "https://github.com/objectstack-ai/objectstack/pull/28486", "session": "session_01L9qibzxSvCxtE6cpabwBDy", "premise_still_valid": true, "summary": "ADR-0146 amended in place on ruling A (6542177429): the override declaration is admitted from the paired `.custom` alone, a dependent's same-named item has one fix (rename), and a dependent that carries `overrides` is refused at publish, at `os validate` and at the install door; D2, D8.2, D9.1, the Supersede table rows (ADR-0126 D1, ADR-0126 §7.1, ADR-0048 §3.4), the Q1 row, the Consequences, the N-09 row, the Status line and Refs are narrowed to match, and the ADR-0048 :3 / ADR-0126 :3 pointer lines drop the closure-wide declarer. Added scope landed too: D9's action paragraph reads the landed ledger switch (#27842 and #27504 closed 2026-10-30) and refuses a dependent's undeclared same-name action with rename as the only fix (6543281223 items 1–2); the Consequences gain gap 3, the base-upgrade displacement of a `.custom` item (item 3); `basedOn` records the base package's `manifest.version` in the D2 shape, the D2 prose and the D11 migration paragraph, matching PR #28467's describe() (6550188803). Every card-quoted line still said what the card quoted on main at 82b29fe5; 22 anchored replacements, each asserting one hit. Draft PR #28486 (head 8ca6f396, 3 files, +63/−36) carries the ruled-item → line table and `skip-changeset` (docs/adr only). Options B and C were not taken.", "tests": "None owed — docs/adr text only, no test or runtime behaviour changed, no ablation. Residue check at head: grep -nE 'any package in the closure|closure package|dependent that declares|one declarer' over the three files leaves exactly two hits (0146 :19 and :223), both the sentences recording the rider's withdrawal.", "gates": "Local subset (head 8ca6f396): `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 21 commands; all 21 ran with </dev/null, exit codes captured before any pipe, all exit 0 (check:adr-links + self-test, check:adr-anchors, check:adr-index, check:adr-symbol-anchors, check:doc-authoring, check:closed-ceremony-text, check:pm-prior-rulings, check:pm-governed-merges, check:nul-bytes, check:gitlink-declared, check:closing-keyword-parity + self-test, check:comment-mask-corpus, check:ci-filter-parity, check:cross-package-test-inputs, check:driver-memory-census, check:refd-timer-probe, check:watch-hint-literal, plan-test-core-shards --self-test, and @objectstack/lint check:doc-formula-expressions after `pnpm --filter '@objectstack/lint...' build` under os-verify-lock, VERDICT command-exit 0, 113 s). `--ran` reconciliation: 21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN; changed lines 99 under the 3000 threshold. Also `node scripts/adr-index.mjs --check` exit 0 (README index unchanged). CI on PR #28486 at head 8ca6f396, read before convergence (the CI-read stamp is the header line above): success — Check Changeset, Governed Surface Queue Guard, PR-body guards (claim · duplicate · Part-of · single-writer path), Check Documentation Links, Check PR Size, filter; in_progress — the Lint & Repo Gates lanes (ESLint; docs & code guards; repo & tooling gates; PM dispatch-gates; CI, release & engine gates; scoped ×3; tooling self-tests), Type Check (workspace, source gates, consumer gates, debt ledger), Plan Test Core shards, Test UI, Dogfood Regression Gate, Auto Label; skipped by path filter — Build Core, Build Docs, Build UI Deps, Type Check UI, Lint UI, Test UI shards, Build & E2E UI, Live E2E UI, Dogfood Verify CLI, Temporal Conformance, UI Published Artifacts, the opt-in harnesses. The PR's check-runs are the record; the dev does not wait for CI.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "9 — GitHub MCP READ tools only: issue_read ×5 (get #28390; get_comments #28390 ×2; get #27842; get #27504), pull_request_read ×3 (get #28467; get #28486; get_check_runs #28486), get_file_contents ×1 (override-declaration.zod.ts at refs/pull/28467/head). 0 write tools. The harness's assignee/reviewer MCP step was skipped on purpose (relay-only writes; Tier H hand-off is the seat's).", "api_writes": "0 direct REST writes. Relay strokes: pr_create via fleet-write (run 41252295991 → PR #28486, 15285 bytes sent, 15285 stored, identical; labels [skip-changeset]; no assignees) and this os-dev-report comment via post-stamped. git push ×1 (claude/project-thread-i8f3y5 @ 8ca6f396, after the seat's Claim: 6550291768 named the branch).", "open_questions": [ { "question": "ADR-0048 :3's clause 'or the declared override a package in its dependency closure publishes' was narrowed to 'or the declared override its paired `<base>.custom` publishes' rather than dropped literally (6542177429 says 'drop'). A literal drop leaves the pointer saying the only owner of a shipped name is the shipping package, which contradicts D8 (the pair's declared override is served within that address).", "options": ["A keep the narrowing (ADR-0048 :3 stays consistent with D8 and with the §3.4 half of the same line)", "B drop the clause literally (one-clause change on ADR-0048 :3)"], "recommendation": "A, because the ruling's intent is to remove the closure-wide declarer, not the pair's override, and D8 still names the pair's override as the owner within the base's address; reversible in one clause if the maintainer prefers B." } ], "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 1, 2026
domain:skills#1 · os-tesla · session_01L9qibzCtJt8Q2Z63fyRNix
Landed · 2026-11-01T03:53Z
f7636153(enqueued by os-zhuang, Tier H; seat ACCEPT 6550678929, contract review PASS 6550660808)completedby the PR'sFixeslineState:
pm:dispatchedremoved (closed card).fix(metadata-protocol): seed the read pins below the gate; writer-awa…
test(runtime): the customizations read test lays the gate-refused row…
objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_01FEkQ5pFHjQjFRsVAurERSJ
Evidence for option B at locale grain (from #28779, measured on main
8f294e04)The ADR-0146 D2 text says a dependent package "adds items under its own names". Since PR #28777 an email template is judged by name and locale, so a dependent may now publish a base template's name at a locale the base does not ship. #28779 measured what the base's reads then answer.
Setup on the showcase. The showcase ships its "task done" email in English only. A partner-style package that depends on the showcase publishes the same template name in French. It is accepted (its English version under that name is still refused).
What a reader of the showcase's template sees in French:
So the partner's French body is answered on the showcase's address when read one at a time, but never listed and never sent. No read labels it as the showcase's, so this is not the full silent-replacement shape of Context §5; it is a contribution answered on the base's address by the by-name reads only.
The customer's own French version (in
com.example.showcase.custom, withoutoverrides) behaves the same way: shown when opened by name, missing from the lists, and never sent.Where it comes from: the by-name read for the showcase finds no showcase or customization row in French and falls back to the registry's cross-package "first match" (ADR-0048 §3.3); the lists keep a name the showcase ships to the showcase's items at name grain.
The behaviour is pinned as a shape test (named SHAPE) in PR #28800; the readings are in the PR body. #28779 stays open for the seat's decision: whether a dependent's (or the pair's) added locale should be served and sent for the base's template, and on which reads.
Generated by Claude Code