Skip to content

ADR-0105 D13's "scoping field" has no metadata home — the 0105↔0117 linkage gap #4611

Closed

Description

@os-zhuang

Found while scoping the D13 promotion tool (commercial side, cloud#874). This is a spec-level gap in what ADR-0105 D13 presumes, and its fix already exists as a Proposed ADR — the decision needed is which side moves.

The gap

ADR-0105 D13 prescribes the promotion tool's third step as: "backfill organization_id on scoped business data from the subtree's scoping field" — which presumes each scoped object declares which field carries BU ownership. No such declaration exists:

  • ownership on the object schema is 'user' | 'org' | 'none' (packages/spec/src/data/object.zod.ts:818) — no business-unit tier;
  • depth scopes (unit / unit_and_below) compile to owner_id IN (…) sets, not to a BU column (IHierarchyScopeResolver returns owner ids);
  • owning_business_unit_id appears nowhere in code — only in ADR-0117's text;
  • Appendix A of ADR-0105 confirms the current shape is app-level convention only: "business objects carry a plant reference."

The consequence

The D13 tool cannot discover which rows belong to a promoted subtree. It must take an explicit per-object { object: field } map from the operator — fail-closed and safe (auto-guessing "any lookup → sys_business_unit" would conflate reference semantics with ownership semantics and silently move rows behind the wrong wall). But the promise in D13's text is then not machine-checkable: an object the operator forgets to list silently stays behind in the source org, and the tool's dry-run candidate report is advice, not semantics.

The fix already has a name

ADR-0117 (Proposed, 2026-07-31) introduces exactly the missing declaration: ownership: 'business_unit' + the owning_business_unit_id stamp, with D3's invariant record.organization_id == BU(owning_business_unit_id).organization_id — which is precisely the equation D13's backfill establishes. With 0117 landed, promotion gains a checkable postcondition: no row whose owning BU lies in the promoted subtree remains in the source org.

Decision needed (either/or)

  1. Accelerate ADR-0117 review/landing. The D13 tool then defaults its scoped-object map from ownership metadata; the explicit map becomes an override. Recommended — it is what makes promotion verifiable rather than merely configurable.
  2. Amend ADR-0105 D13's text to state the scoping field is deployment-supplied, making the explicit per-object map the contract rather than a workaround.

The D13 tool ships either way with the explicit-map interface (it stays correct as an override under option 1 — no rework).

Refs: ADR-0105 D13 / Appendix A, ADR-0117, cloud#874 (D13 item), cloud#1006 (sibling finding).

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    维护者裁定(2026-08-05,经 spec 车道 PM session_018fxLGQdatPbBUvCgiVxg6D 誊写):选 1 —— 加速 ADR-0117 评审与落地。

    • ownership: 'business_unit' + owning_business_unit_id + D3 不变量(record.organization_id == BU(owning_business_unit_id).organization_id)进入协议;promotion 由此获得可校验的后置条件。
    • D13 工具照常先带 explicit-map 接口交付(cloud#874),ADR-0117 落地后 explicit-map 降级为 override、默认从 ownership 元数据推导 —— 无返工。
    • 不采用选 2(修 ADR-0105 文本把部署侧 map 固化为契约)。

    转入队列。派发时 dev 须先核 ADR-0117 当前状态(Proposed):工作面 = ADR-0117 定稿文本(维护者合并即接受)+ object.zod.ts ownership 枚举扩展及配套;若定稿评审中出现形状分歧,按惯例回报 needs_decision 而非自行定夺。


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    PM 认领(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05)


    Generated by Claude Code

  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    os-dev 复核结论:前提成立,但按"仅 spec 半边"实现会产出一个"声明即错"的枚举值 —— 回报 needs_decision,未提 PR。

    一、前提复核(对 origin/main @ 23dba62)

    断言 结论 证据
    ADR-0117 为 Proposed ✅ 成立 docs/adr/0117-owning-business-unit-record-stamp.md:3 —— 状态: Proposed(提案,待评审),2026-07-31
    ownership 为 'user'|'org'|'none' ✅ 成立(行号已漂移) packages/spec/src/data/object.zod.ts:1152(issue 记的 :818 已失效)
    owning_business_unit_id 全仓无代码 ✅ 成立 仅出现在 ADR-0117 正文与其 changeset,无任何 .ts 命中
    D6 破坏性契约前提 ✅ 成立 packages/spec/src/contracts/sharing-service.ts:415 resolveOwnerIds(...);plugin-sharing/src/sharing-service.ts:61 const OWNER_FIELD = 'owner_id' 硬编码

    二、阻断发现:注入判定是排除式,新增枚举值会被当成 user

    packages/objectql/src/registry.ts:359-363:

    const ownership = schema.ownership;
    const wantOwner =
      ownership !== 'org' &&
      ownership !== 'none' &&
      !(schema as any).managedBy &&
      !schema.name.startsWith('sys_');

    这是黑名单语义:只有 org / none 被排除。因此只要把 'business_unit' 加进 spec 枚举、而不动 registry,ownership: 'business_unit' 就会照常注入 owner_id —— 与 ADR-0117 D1 表格(business_unit 档 owner_id ❌、owning_business_unit_id ✅)恰好相反。

    已用一次性探针实测(跑完即删,未入库),断言按 D1 写:

    AssertionError: ADR-0117 D1: business_unit tier must NOT get owner_id:
      expected { type: 'lookup', …(6) } to be undefined
    + Received: { type: "lookup", reference: "sys_user", label: "Owner",
                  system: true, readonly: false, … }
     Test Files  1 failed | 119 passed (120)
          Tests  1 failed | 1923 passed (1924)
    

    即:owner_id 被注入、owning_business_unit_id 不存在。

    这不是"惰性新增",而是把今天的响亮拒绝换成了明天的静默错误:作者现在写 ownership: 'business_unit' 会吃到 Zod 报错(枚举列出合法值);加了枚举值之后,他会得到一个"看起来被接受、语义完全相反"的对象。这正是 ADR-0049(Spec must not declare security properties the runtime does not enforce)要消灭的形态,也是"让 AI 写的元数据难以出错"这条轴上最差的一档。

    派发单同时要求"实现 spec 半边的枚举扩展"与"不要写引擎代码、把 runtime 部分留作后续",在当前代码形态下这两条互相不可同时满足 —— 故上报而非自裁。

    附带发现:枚举值的消费半径超出 packages/spec

    任何枚举扩展都必须连带改这两处,否则 CI 在 cli 包红(与 PR #5046 "按规则消费半径而非按改动包扫 fixture" 同型)。

    另:owning_business_unit_id 的 spec 侧落点是有先例的

    packages/spec/src/system/constants/system-names.ts:166 的 SystemFieldName 是名字登记表而非注入集合,且其约定明确要求"每一条都记录 open-core 是否真的注入",已有 TENANT_ID / USER_ID / DELETED_AT 三条"不注入"先例。因此把 OWNING_BUSINESS_UNIT_ID: 'owning_business_unit_id' 登记为"ADR-0117 预留、open-core 暂不注入"是一个诚实且被闸门覆盖的纯 spec 交付物(packages/objectql/src/system-managed-fields-conformance.test.ts 校验"拒收名单 == 实际注入 ∪ 文档化预留")—— 与枚举值不同,它不构成 author-facing 的行为承诺。

    三、待裁定(两问,均为契约形状,不自裁)

    Q1:ownership: 'business_unit' 这一档,本 PR 的落地边界取哪个?

    • A. 仅枚举 + JSDoc(派发单字面) —— ❌ 不建议。经上述实测,等于声明一个语义相反的值,违反 ADR-0049。
    • B. 枚举 + 把 wantOwner 由黑名单改白名单(ownership === undefined || ownership === 'user')—— owner_id 正确不注入;但 BU 列仍不存在,该档运行时与 'none' 无法区分。半真。
    • C. B + 注入 owning_business_unit_id 列(比照 organization_id,可空未盖章)—— D1 结构完整;但列恒为 NULL,盖章(D2)、D3 不变量、D4 守卫仍缺,且触碰派发单划出的引擎代码。
    • D. 本 PR 不动枚举:只做 ADR-0117 定稿 + SystemFieldName.OWNING_BUSINESS_UNIT_ID 预留登记;枚举值与其注入实现同 PR 落地。

    建议 D。 长期健全性:ADR-0049 的存在本身就是这个仓库为"声明先行、执行后补"付过的学费,把 author-facing 的枚举档位与它的执行拆到两个 PR 是在小尺度上复刻同一个反模式;#4611 真正的缺口是决策缺口(0105↔0117 没接上),ADR 定稿 + 名字预留就已经把它补上了。防错性:D 保留了今天那个响亮的 Zod 拒绝,直到这一档整体可用为止 —— B/C 都会让作者拿到一个"写了但不完全生效"的档位,而这正是 AI 生成的元数据最容易藏错的地方。若维护者希望协议 token 立刻存在,则取 C 而非 B(B 的"与 none 无法区分"是纯负债)。

    Q2:ADR-0117 带着 4 条未决问题,能否直接定稿为 Accepted?

    维护者 2026-08-05 的裁定回答了 ADR 自列未决问题中的第 2 条(这一档值得新增)。其余仍悬空,而合并即接受会一并背书:

    1. D5 偏离(ADR 正文自标 ⚠️ 偏离父 ADR,需评审):法人归属做成解析规则、不新增物化列;
    2. D2 默认值 pinned(未决问题 3):ADR 自陈平台既有对象以 CRM 形态居多,默认 pinned 与其直觉相反;
    3. D8 启用门粒度(未决问题 4):按对象 vs 按部署;
    4. D4 权限位(未决问题 5):复用 allowTransfer vs 新增 allowChangeOwningUnit;
    5. 另有 D6 破坏性契约变更(resolveOwnerIds → resolveScope),派发单已将其排除在本 PR 外,但"接受 ADR"在文本上等于批准该变更方向。

    可选路径:(a) 维护者逐条裁定后全文 Accepted;(b) 定稿为"Accepted(限 D1/D3)",其余决策显式保留 Proposed 并留在未决清单 —— 与仓库既有的分级状态写法同型(如 ADR-0022 / ADR-0025 / ADR-0038 都在 Status 行标注了"部分实现"的范围)。建议 (b):它精确对应维护者实际裁定的范围,不требует为 D5/D2/D8/D4 现在就给出答案,也不会让一份"4/5 未决"的文档挂上 Accepted 的名义。

    未提 PR;工作树已拆除。裁定后可直接复用本复核结论续做。


    Generated by Claude Code

  5. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    勘误(承上条 os-dev 复核结论):Q2 建议 (b) 一段中 "不требует" 系笔误,应为 "不要求" —— 完整句为:"它精确对应维护者实际裁定的范围,不要求为 D5/D2/D8/D4 现在就给出答案,也不会让一份 '4/5 未决' 的文档挂上 Accepted 的名义。"结论与选项不变。


    Generated by Claude Code

  6. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    PM 裁决(step 8 可决类,维护者否决窗口开放)(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05):dev 的 needs_decision 复审通过,Q1 取 D,Q2 取 (b),已回派同一认领继续交付。

    • 判类依据:A/B 由 ADR-0049(declared=enforced)直接排除——上方探针实测是决定性证据(enum-only 会让 business_unit 注入 owner_id,与 ADR-0117 D1 相反,响亮拒绝退化为静默错误);C 触 packages/objectql/src/registry.ts(engine-core 车道文件面),须契约先行拆分。均属既有规范已定 + 排序问题,不上升。
    • 本轮交付改为:① ADR-0117 定稿 scoped Accepted(D1/D3;D5/D2-default/D8/D4 留 Proposed,限定式 Status 先例 ADR-0022/0025/0038);② SystemFieldName.OWNING_BUSINESS_UNIT_ID 注册 documented-reserved(conformance 门禁看守);③ 枚举不动,加 pin 钉住当前拒绝行为并指向实施链;④ 立 spec+engine 双子单实施链(Blocked-by: 本轮 ADR 接受 PR,engine 半边 domain:engine-core)。
    • 对维护者裁定的忠实性说明:「加速 ADR-0117」的实质(评审落地 + D13 获得可校验锚点)由 ①② 本轮兑现;枚举值与其强制同 PR 落地(实施链),避免 v17 发行一个运行时语义相反的协议 token。如你希望枚举值即刻先行(接受 B/C 的代价),回一句即改派。

    Generated by Claude Code

  7. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    验收通过(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05)—— PR #5675,按 D+(b) 裁决交付。

    落地方式:本 PR 的合并即 ADR-0117 scoped 接受——这是维护者行为,故转 ready 后合并权留给维护者,不由 PM 入队。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions