fix(spec)!: HierarchyScopeContext 声明 organizationId 为权威租户字段并转必填 (#5858) - #5973
Conversation
… authority and requires it (#5858) `HierarchyScopeContext` declared `organizationId?` and `tenantId?` side by side with no doc saying which one carries the caller's active organization. The one in-repo producer filled `organizationId` from a `SharingExecutionContext` whose only tenancy member is `tenantId` (structurally always null), while the real consumer reads `organizationId` and skipped tenant isolation on null — two individually contract-compliant ends adding up to a reachable cross-org read (#5852). - `organizationId` documented as AUTHORITATIVE (null = platform/unscoped, matching `EvalUser.organizationId`), per the #3280/#3290 naming convention that `scripts/check-org-identifier.mjs` gates. - `organizationId?: string | null` -> `organizationId: string | null`: a producer that omits the caller's org now fails to compile instead of handing every resolver an `undefined`. - `tenantId` retained as a `@deprecated` alias (NOT removed) with the explicit "a resolver must not depend on it alone" obligation. - `IHierarchyScopeResolver.resolveOwnerIds` documents the fail-closed rule: a null organization is "no org", never "every org". Pins: two compile-time probes (`@ts-expect-error` on the omitted field and on tenantId-only; a RequiredKeys pin in both directions) plus an AST prose pin over the three doc obligations, in a file with a zero budget in test-typecheck-debt.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 111 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31108010067 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
队列管家:⛔ 已修签名再现 = 新问题,本座位不重投(第 8 轮巡检)本 PR 13:52:34Z 在队列里被踢出(run 31108010067)。按 #5810 签名台账认签名,判为 objectstack 表第 2 行「已修签名」的再现,因此 ⛔ 不重投,只留判读与路由。红与本 PR 的 diff 无关(连坐),处置权归本 PR 所属车道( 完整签名(取完整日志归档,非 tail;note 7 的两个陷阱本轮都现身且都被绕过——同 job 的 11 个测试文件与 229 条用例全绿,
台账那一行的三个要素—— 为什么它是新问题而不是那条老 flaky(可证伪的读数,均取自 #4856 的 起因(commit 级,非「相邻」推断):该测试文件由 #5954(
建议动作(本座位无权执行,仅指引):
⛔ 本座位未做任何入队/撤队/重跑/合并/ready-draft 切换/认领。让行核验:本 PR 最近 30 分钟仅 台账升级已在锚点 #5810 提请(⛔ 本座位不自行加表)。 Generated by Claude Code |
按 #5859 裁决(C)追加:`SharingService` 新增 late-bound `tenancy` 姿态探针(读法与 SecurityPlugin 为 Layer-0 墙读 `tenancy` 服务一致,由 SharingServicePlugin 接线), 按 ADR-0105 D1 的既有分叉决定「没有活动组织」意味着什么: - `single`(纯单租户,无组织):行为不变,DEPTH 照常 —— 那是唯一隐含租户,不是「所有组织」; - `group` / `isolated`:权威组织缺失/空白 → 不咨询 resolver,回落 owner-only, warn 点名 ADR-0095 D1 / ADR-0105 D1 与 #5973 的 fail-closed 契约义务; - 姿态解析不出(未接线/抛错/词表外)→ 按有墙处理,未知姿态不是 single 的证据。 测试两个方向都钉:single+无组织仍 widened(先绿保持绿)、walled+无组织拒绝(先红后绿)、 姿态不可解析拒绝、legacy `isolationActive:false` 视为无墙、空白组织在两侧各自的表现。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
Fixes #5858
按 issue 上 PM 2026-08-06 裁决执行(B + C + fail-closed doc)。⛔ 未重开方向讨论。
改了什么
packages/spec/src/contracts/sharing-service.ts一处契约:organizationId定为权威字段。doc 写明「承载调用方的活动组织,null= platform/unscoped」,措辞与packages/spec/src/identity/eval-user.zod.ts:129的EvalUser.organizationIddescribe 对齐,并在注释里点名依据(Unify the developer-facing org identifier: hooks exposesession.tenantIdwhile RLS/seed/columns useorganizationId(addorganizationIdas the blessed name) #3280 定名、Remove the deprecated hook/actionctx.session.tenantIdalias in the next major (converge onorganizationId) #3290 v11 移除session.tenantId别名、scripts/check-org-identifier.mjs硬门)。organizationId?: string | null改为organizationId: string | null。producer 漏填从「doc 没说清楚」变成 typecheck 失败;「没有组织」仍然合法,但必须显式写成null——null是值,不是缺省。tenantId保留为 deprecated 别名,⛔ 未删除。JSDoc 标@deprecated,写明它仍是通用 driver 层租户旋钮(database-per-tenant 内核里合法地放 environment id),并明确「resolver 不得单独依赖它,也不得拿它顶替为null的organizationId」。退役另行立单。IHierarchyScopeResolver.resolveOwnerIds补 fail-closed 义务:权威字段为null时实现方必须 fail-closed —— 「没有组织」永远不等于「所有组织」,返回 owner-only 或抛出(sharing 层同等处理),⛔ 不得静默按无租户约束构建 owner set,也不得退回读tenantId。外加
.changeset/hierarchy-scope-organization-authority.md(@objectstack/spec: major)与钉子测试。未碰packages/plugins/plugin-sharing的任何代码(生产逻辑与测试都没动 —— 见下方「机械补齐:零处」)。P1 / P2 / P3 前提核验(全部成立)
P1 ——
check-org-identifier.mjs门与 #3280/#3290 公约仍活着:成立。脚本头部原文仍在陈述该公约:「#3280 made
organizationIdthe blessed developer-facing name for the caller's active org across the JS authoring surface … #3290 REMOVED it from the hook/actionctx.sessionsurface entirely (v11 major)」。同一段还自己记录了本裁决援引的那条:driver 层tenantId在 database-per-tenant 内核里合法地是 environment id,门禁刻意不匹配它。本地跑过一遍:check-org-identifier: OK (1660 author-facing source file(s), no removed session.tenantId alias)。P2 —— 仓内唯一 producer 仍是
plugin-sharing/src/sharing-service.ts:875-880:成立。HierarchyScopeContext是结构化类型,构造点即「传给resolveOwnerIds的第一实参」。两轮普查:该类型在 spec 之外零 import,所以不存在「显式标注了该类型的第三处」。后 4 处全是 resolver 实现(消费侧,签名
c: any),不是构造点;plugin-sharing/src/sharing-service.test.ts:853/867/881/893同理,是 stub resolver 而非 stub context。机械补齐:零处。 唯一构造点 :875-880 本来就填了
organizationId这个键(填的是恒null的值 —— 语义修复归 #5859,本单⛔不做),所以必填化在本仓内是零破坏。实测两个可能受影响的包 typecheck 全绿,没有一处需要补organizationId: null:如实申报一处与裁决预期的偏差:裁决预计「qa/dogfood 与 plugin-sharing 测试 stub」需要补键,实测不需要 —— 因为它们是 resolver 实现而不是 context 构造点。裁决对生产构造点「预计不红」的判断则完全成立。
P3 —— PM 裁决评论仍是最后一条裁决:成立。 issue #5858 共 2 条评论:认领评论(12:28:59Z)+ PM 裁决评论(12:41:15Z)。裁决之后无任何评论,不存在更晚的维护者否决。
测试
钉子落在
packages/spec/src/contracts/sharing-service.test.ts(该文件在packages/spec/test-typecheck-debt.json里无条目,即错误预算为 0 —— 任何新错误直接让check:test-typecheck变红;#5286 的机制)。organizationId的对象字面量、以及只给tenantId的对象字面量,各带一条@ts-expect-error。['userId', 'organizationId'],反向用@ts-expect-error钉住tenantId仍是可选 —— 这同时是「⛔ 不删除tenantId」的守卫(删了它,反向那行引用即断)。['userId', 'organizationId', 'tenantId'](反空过),organizationIddoc 含AUTHORITATIVE/platform/unscoped/MUST scope its owner set by this field,tenantIddoc 含@deprecated/Not the authority for hierarchy scoping,resolveOwnerIdsdoc 含Fail CLOSED/never widen;并以userId作诚实反例(断言它不含AUTHORITATIVE—— 它是身份不是租户),证明匹配是有区分度的。反向验证(方向为预先判定的「红」,实测吻合)。 把
?放回去、其余不动,重跑测试层 tsc:3 条错误落在预算为 0 的文件上 → 红。恢复后复跑,
typecheck与check:test-typecheck均绿。正向证据(全部前台阻塞执行,共享 flock 串行):
生成物
按纪律整体重生成(⛔ 未手改任何生成物,⛔ 未动
authorable-surface.base.json锚点):gen:schema→gen:openapi(#5371)→gen:api-surface。结果:生成物零 diff —— 本单改的是纯 TypeScript 接口(无 Zod schema、无新增/删除导出),api-surface.json只记录名字级别的存在性,HierarchyScopeContext早已在册。authorable-surface.base.json相对72bd873滞后 16 个 key —— 生成器自己声明这是预期而非错误,按纪律不做重锚。changeset 档位
@objectstack/spec: major。必填化对 producer 是 breaking(implementer 反而更容易 —— 必填属性只会让消费更简单)。当前处于 v17 rc 窗口(packages/spec版本17.0.0-rc.2,.changeset/pre.json为mode: pre/tag: rc),窗口内@objectstack/spec: major是既有惯例而非新开先例,同窗口在库的同档 changeset 包括adr-0113-required-write-contract.md(同为「把一个宽松契约收紧成必填/强制」)、adr-0114-field-errors-rename.md、多个retire-*。故本单按 major 落,⛔ 未为了凑档位把字段降级回 optional。与同批其他单的关系(如实申报)
IStorageService.list(prefix)契约成员(零消费方,双适配器语义分叉 —— #5266 方案 2,维护者已批) #5540(packages/spec/src/contracts/storage-service.ts摘IStorageService.list):同目录不同文件,零共享符号。PM 预警的「生成物基线相交」在本单侧没有发生 —— 本 PR 生成物零 diff(上面已实测),而 spec: 按 ADR-0049 摘除IStorageService.list(prefix)契约成员(零消费方,双适配器语义分叉 —— #5266 方案 2,维护者已批) #5540 摘导出必然改api-surface.json,所以串行接力时冲突面是单向的,只在 spec: 按 ADR-0049 摘除IStorageService.list(prefix)契约成员(零消费方,双适配器语义分叉 —— #5266 方案 2,维护者已批) #5540 那一侧。packages/spec/scripts/build-openapi.ts摘路由段):不同文件;本单未动任何 Zod schema,重跑gen:openapi后openapi.json零 diff。HttpMethod,后写覆盖前写 —— 参考页只描述了 5 值那份,7 值的线上契约(HEAD/OPTIONS)不见了 #5832(shared/http.zod.ts的HttpMethoddef-key 碰撞):不同文件,且HierarchyScopeContext是纯 TS 接口、根本不进 json-schema 的 def-key 命名空间。HookEvent的枚举注释仍在说批量写把行级谓词「carried ininput」—— 与 #5273 同一句假话,在同文件 40 行之外 #5899(data/hook.zod.ts的HookEvent注释订正):不同文件,纯注释。四单均为「完全无影响」 —— 既没变简单,也没变难,更没变得不必要。没有任何一单的完成范围被本 PR 覆盖或抢跑。
未做的事(范围红线)
tenantId(也未删organizationId)—— 退役走单独流程。packages/plugins/plugin-sharing:producer 把租户值真正映射进organizationId的语义修复归 [#5852 producer 半边]resolveOwnerScopeIds按权威字段填充 hierarchy resolver context + 补「resolver 收到的 org 非空」断言 #5859(identity 车道),本单只把契约面钉住,让那处修复有一个明确的目标字段。Generated by Claude Code