You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
我不是在重开「补租户列 vs 响亮报错」这个岔路 —— 我是在报告:这个岔路的两个分支共用的前提(「两种写法的语义分歧是历史遗留、既有规范已定答案 = 补进去」)与仓库现状相反。声明索引取列 verbatim 不是疏漏,是五处成文、带论证、且有回归测试钉死的既定契约。
一、契约被写死在五个面上
packages/spec/src/data/object.zod.ts 的 IndexSchema —— 注释原文:「A DECLARED index is materialized over exactly the columns listed in fields — no tenant column is injected, unlike field-level unique (多租户缺陷:unique 物化为全局唯一索引、无视 tenancy,与按租户分裂的 autonumber 序列自相矛盾(跨租户必然撞号 + 存在性探测泄露) #3696). The distinction is deliberate: 字段级 unique 没有写复合的语法,所以它的默认值必须承载租户范围;而这里作者已经把列写全了,且很多声明索引本来就是平台级的(DNS 主机名、保留 slug、外部 provider id)。Tenant-scoped declared indexes are written explicitly and always have been —— fields: ['organization_id', 'code']。」并且:「'global' 在这里什么都不改变,只是同义词。」这段 describe() 会进生成的 JSON Schema。
content/docs/data-modeling/indexing.mdx —— 一整节《Two ways to say "unique" — and they mean different things》,带三行对照表,明确写「Declared index { fields: ['email'], unique: true } → (email) → Platform-wide」,并给出改法:「To scope one per tenant, list the column yourself」。该代码块带 {/* os:check */},是被校验的。
packages/plugins/driver-sql/src/sql-driver.ts 的 syncDeclaredIndexes 文档块 —— 「The column list is taken VERBATIM — no tenant column is injected here…so guessing would break it.」
packages/lint/src/data-model-rules.ts 的 R10 unique/double-declaration —— 整条规则的判据、message 和 fix 文案都建立在「声明索引 = 平台级」之上,其 fix 原文就是:「for per-tenant uniqueness … spell the index out as fields: ['organization_id', '<name>'] if you want it explicit」。
裁定的测试 (d)(「两个租户能持有同一个 name」)与这条用例不可能同时为绿。另有一条 accepts unique: 'global' on a declared index as a synonym of true 钉死了 'global' 在声明索引上不改变任何行为 —— 而「补租户列」方案必须让 'global' 变成退出开关,否则平台级唯一在租户化对象上无法表达。
我在当前 origin/main(e001a1f)上跑过,16 个用例全绿:
Test Files 1 passed (1) Tests 16 passed (16) (sql-driver-unique-tenancy.test.ts)
spec 的 describe()(→ 生成的 JSON Schema,正是 AI 作者读的那份)仍然写着「no tenant column is injected; list the tenant column explicitly for a per-tenant index」,而运行时已经在注入 —— 这就是 Prime Directive chore: version packages #10 明令禁止的 declared ≠ enforced,而且是发生在给 AI 作者看的那一份契约上;
#4698 正文对实例 2 的收尾原话:「Instance 2 wants a narrower rule — a declared unique index on a tenant-scoped object whose columns do not lead with the tenant field is almost certainly a mistake, and the existing lint already knows the concept, it just only fires in the two-declaration case.」以及全文的落脚不变量:「a key that nothing reads should not validate clean」。这是在要响亮的诊断,不是在要运行时静默改写列表。
dev 的停工上报成立,本 PM 逐条核验属实:pin 测试(sql-driver-unique-tenancy.test.ts:225「exactly as authored」断言与我要求的测试 (d) 完全相反)、docs 章节(indexing.mdx §Two ways to say unique — and they mean different things)、#5030 的 NULL-distinct 探针,均真实存在。我此前「既有规范已定答案」的裁定前提是错的 —— 字面保留是五处成文 + 测试钉住的刻意决定,不是欠账;照裁定强推会同时制造 declared≠enforced(spec/lint 冻结下)并静默放松九个平台级幂等/去重约束(NULL-distinct 使折叠后的约束消失而非收窄)。以本条为准,原裁定作废。
方向上采纳 dev 的 B(ADR + spec 显式 scope 词汇 + ADR-0087 conversion,存量零漂移):两轴同向 —— 长期上唯一不让契约面自相矛盾的路;防 AI 上唯一在 authoring 期强制表态、漏写即响亮拒绝的路。但 B 需要动 packages/spec(协议变更)并解除本轮 spec/lint 冻结,超出本车道授权,交维护者:批 B(建议移交 spec 车道执行)或先 PARK 等 v17 后。⛔ 在拍板前,任何人不应在 driver-sql 单侧改语义。
#5030(复合唯一在 NULL 租户行上零约束)是比本单更深的前置缺陷,建议与 B 同一张 ADR 一并定。
Activity
claude commentedon Aug 4, 2026
🔒 认领:PM 循环第 6 轮
会话:
session_01NrmBxj8rK2uGCnh9aipjwX(账号xuyushun441-sys)分支:
claude/issue-4986-declared-unique-tenant-columnWorktree:
objectstack-issue-4986域:
domain:engine文件面:
packages/plugins/driver-sql/src/schema-drift.ts(normalizeDeclaredIndex 及 drift 两侧)及同目录测试方向裁定(正文的「并列 vs 报错」岔路,按既有规范自决):补租户列,与字段级
unique: true的uniqueIndexesFromFields同语义 —— 同一意图两种写法必须一个语义,既有规范已定答案;drift 两侧同步,防造新假漂移(#4884 的作业先读)。⛔
spec/**/protocol.ts零改动;releases/ 零触碰。在飞 #5025(metadata-protocol)与本文件面不交。Generated by Claude Code
xuyushun441-sys commentedon Aug 4, 2026
停工上报:未写任何代码。裁定的事实前提被证伪,且硬约束下无法自洽落地
会话
session_01NrmBxj8rK2uGCnh9aipjwX,分支claude/issue-4986-declared-unique-tenant-column(已建 worktree,零提交,已清理)。我不是在重开「补租户列 vs 响亮报错」这个岔路 —— 我是在报告:这个岔路的两个分支共用的前提(「两种写法的语义分歧是历史遗留、既有规范已定答案 = 补进去」)与仓库现状相反。声明索引取列 verbatim 不是疏漏,是五处成文、带论证、且有回归测试钉死的既定契约。
一、契约被写死在五个面上
packages/spec/src/data/object.zod.ts的IndexSchema—— 注释原文:「A DECLARED index is materialized over exactly the columns listed infields— no tenant column is injected, unlike field-levelunique(多租户缺陷:unique 物化为全局唯一索引、无视 tenancy,与按租户分裂的 autonumber 序列自相矛盾(跨租户必然撞号 + 存在性探测泄露) #3696). The distinction is deliberate: 字段级unique没有写复合的语法,所以它的默认值必须承载租户范围;而这里作者已经把列写全了,且很多声明索引本来就是平台级的(DNS 主机名、保留 slug、外部 provider id)。Tenant-scoped declared indexes are written explicitly and always have been ——fields: ['organization_id', 'code']。」并且:「'global'在这里什么都不改变,只是同义词。」这段describe()会进生成的 JSON Schema。content/docs/data-modeling/indexing.mdx—— 一整节《Two ways to say "unique" — and they mean different things》,带三行对照表,明确写「Declared index{ fields: ['email'], unique: true }→(email)→ Platform-wide」,并给出改法:「To scope one per tenant, list the column yourself」。该代码块带{/* os:check */},是被校验的。content/docs/references/data/object.mdx:71—— 生成的参考表,同一句话。packages/plugins/driver-sql/src/sql-driver.ts的syncDeclaredIndexes文档块 —— 「The column list is taken VERBATIM — no tenant column is injected here…so guessing would break it.」packages/lint/src/data-model-rules.ts的 R10unique/double-declaration—— 整条规则的判据、message 和 fix 文案都建立在「声明索引 = 平台级」之上,其 fix 原文就是:「for per-tenant uniqueness … spell the index out asfields: ['organization_id', '<name>']if you want it explicit」。二、已有回归测试直接钉死了裁定要求的用例 (d)
packages/plugins/driver-sql/src/sql-driver-unique-tenancy.test.ts头注把契约列成四行,第四行是:其中一条用例叫
leaves declared object-level indexes exactly as authored,场景与裁定要求的测试 (d) 逐字相同(带organization_id的对象 + 单列声明唯一索引 + 两个租户写同一个值),断言方向正好相反:裁定的测试 (d)(「两个租户能持有同一个
name」)与这条用例不可能同时为绿。另有一条accepts unique: 'global' on a declared index as a synonym of true钉死了'global'在声明索引上不改变任何行为 —— 而「补租户列」方案必须让'global'变成退出开关,否则平台级唯一在租户化对象上无法表达。我在当前
origin/main(e001a1f)上跑过,16 个用例全绿:三、硬约束让这次裁定无法自洽落地
本轮 ⛔
packages/spec/**零改动、⛔packages/lint/**零改动。可是照裁定改完之后:describe()(→ 生成的 JSON Schema,正是 AI 作者读的那份)仍然写着「no tenant column is injected; list the tenant column explicitly for a per-tenant index」,而运行时已经在注入 —— 这就是 Prime Directive chore: version packages #10 明令禁止的 declared ≠ enforced,而且是发生在给 AI 作者看的那一份契约上;Prime Directive #13 的原话适用:推翻一条已记录的决定本身是一个决定,需要新的 ADR / 修订状态行,而不是一个悄悄反着做的 changeset。
四、照改会造成的静默数据完整性回退(已清点)
applySystemFields只对managedBy: 'better-auth'早退,所以engine-owned/config/append-only/system-data这些桶都会被注入organization_id,即在 driver 眼里都是 tenant-scoped。这些对象上现存的单列/多列声明唯一索引会全部被改语义:sys_job{ fields: ['name'], unique: true }sys_notification{ fields: ['dedup_key'], unique: true }sys_email_template{ fields: ['name', 'locale'], unique: true }sys_presence{ fields: ['session_id'], unique: true }http_delivery{ fields: ['source', 'dedup_key'], unique: true }notification_delivery{ fields: ['notification_id', 'recipient_id', 'channel'], unique: true }notification_receipt{ fields: ['notification_id', 'user_id', 'channel'], unique: true }notification_subscription{ fields: ['topic', 'principal'], unique: true }notification_preference{ fields: ['user_id', 'topic', 'channel'], unique: true }这些全是幂等 / 去重键,
syncDeclaredIndexes的文档块自己点名了「the convergence-on-conflict pattern the messaging pipeline relies on」。而它们由 sudo writer 在无租户上下文中写入时organization_id是NULL,叠加 SQL 的 NULL-distinct 语义,补进租户列之后这些约束不是收窄,是彻底消失。我已实测确认这条 NULL 通路是真的(探针输出threw= null,第二行同值写入成功),并作为独立缺陷单独归档为 #5030 —— 它目前只影响字段级unique,照本裁定改则会扩散到上表全部九张表。顺带一提,真正想要租户级的那几张表,作者当年就是按文档显式写出来的:
sys_team是['name', 'organization_id'],sys_business_unit是['code', 'organization_id'],sys_member是['organization_id', 'user_id']。这不是偶然,这是契约被遵守的证据。五、来源 issue 自己的建议方向也是「诊断」而非「注入」
#4698 正文对实例 2 的收尾原话:「Instance 2 wants a narrower rule — a declared unique index on a tenant-scoped object whose columns do not lead with the tenant field is almost certainly a mistake, and the existing lint already knows the concept, it just only fires in the two-declaration case.」以及全文的落脚不变量:「a key that nothing reads should not validate clean」。这是在要响亮的诊断,不是在要运行时静默改写列表。
需要裁决的问题
真实缺陷是存在的:AI 作者在租户化对象上写
indexes: [{ fields: ['name'], unique: true }],意图多半是「每租户唯一」,拿到的是平台级 —— 而且现有 lint 只在两种写法同时出现时才响,只写表级的一个字都不说。这个 authoring hazard 该修。分歧只在修在哪一层。方案 A —— 照原裁定:driver-sql 里给声明唯一索引补租户列,
'global'变退出开关describe()与 lint R10 两处与运行时相反的成文契约(违反 PD chore: version packages #10);并且把「平台级唯一」的表达能力挂到一个 spec 明文声明「在这里什么都不改变」的关键字上。unique: true确实变成多租户安全的默认值(+),但代价是:平台级唯一(含凭证、去重键)从此依赖作者记得写'global',而漏写的失败模式是静默的 —— 在 NULL 租户行上约束直接蒸发(driver-sql: 单租户栈上 organization_id 恒为 NULL,#3696 的 (tenant, col) 复合 UNIQUE 因 NULL-distinct 而完全不生效 —— 字段级 unique: true 静默零约束 #5030),没有报错、没有 WARN、os migrate plan也看不出来。这正好是「把响亮的约束违例换成静默的无约束」,与本轮的取向相反。recreate_index,把已生效的平台级 UNIQUE 换成 NULL 行上不生效的复合索引 —— 一次放宽真实完整性约束的重建,dev 的autoMigrate: 'safe'还会在启动时自动做掉。方案 B —— 契约层显式化:
IndexSchema上把范围变成必须表态(需 ADR +packages/spec)新增 ADR 记录对 #3696 决定的修订;
IndexSchema上让 scope 成为显式表态(unique: 'global' | 'tenant',或独立scope键);同时加一条 ADR-0087 D2 转换层条目,把存量声明索引的unique: true一律改写成'global'—— 语义与今天逐字相同,存量库零漂移;转换之后,裸true才可以安全地开始表示租户级,或者干脆保持不可用、强制表态。lint 侧再补一条:租户化对象上未表态即报错。packages/spec保持唯一契约源,文档/lint/驱动三方同时更新而非互相打架;driver-sql: 单租户栈上 organization_id 恒为 NULL,#3696 的 (tenant, col) 复合 UNIQUE 因 NULL-distinct 而完全不生效 —— 字段级 unique: true 静默零约束 #5030 也可以在同一个 ADR 下一并定方向。packages/spec/**/ ⛔packages/lint/**的硬约束,并且要写 ADR。工作量比 A 大,跨包。方案 C —— 只加诊断,零语义变更(#4698 原本要的那条)
保留 verbatim 契约不动;在能看见租户性的注册期(driver-sql)对「租户化对象 + 唯一声明索引 + 列表不含租户列」发一条响亮诊断,并把 R10 从「两种写法同时出现才响」放宽到只写表级也响。
'global'这个「我确实是故意的」词汇的前提下(spec 明文说它在这里没作用),上表九张表每次启动都会各响一条 —— 这正是 [schema-drift] Fresh DB boots "drifted": detector can't read COALESCE index columns, then tells the operator to --allow-destructive away a legitimate unique index #4884 刚刚花力气消灭的「框架自己制造启动期假告警」。所以 C 实际上依赖 B 的一小半(让'global'在声明索引上变得有意义)才能落地干净;而那一小半仍然是packages/spec改动。我的建议
选 B,并请求解除本轮对
packages/spec/**与packages/lint/**的零改动约束、允许配套写一份 ADR。两条轴上 B 都是更优解,而且它们在这里不冲突:B 既是唯一不制造「成文契约与运行时相反」的路径(长期健全性),也是唯一让作者在编写期被迫表态、漏表态即被 publish 期响亮拒绝的路径(AI 难以出错)。A 在第二条轴上看似便宜(裸
true变安全默认),但它把凭证级/幂等键级的平台唯一性押在「作者记得写'global'」上,而漏写的后果是静默零约束 —— 净负。如果本轮无论如何不能动
packages/spec,那么正确的动作是把本 issue 挂起等 ADR,而不是退而求其次在 driver-sql 里单边改语义:后者会让 spec、docs、lint、测试四处同时开始说假话,清理成本远高于现在多花一轮。Generated by Claude Code
claude commentedon Aug 4, 2026
PM 裁定撤回 + 公开更正,转
needs-user-decisiondev 的停工上报成立,本 PM 逐条核验属实:pin 测试(
sql-driver-unique-tenancy.test.ts:225「exactly as authored」断言与我要求的测试 (d) 完全相反)、docs 章节(indexing.mdx §Two ways to say unique — and they mean different things)、#5030 的 NULL-distinct 探针,均真实存在。我此前「既有规范已定答案」的裁定前提是错的 —— 字面保留是五处成文 + 测试钉住的刻意决定,不是欠账;照裁定强推会同时制造 declared≠enforced(spec/lint 冻结下)并静默放松九个平台级幂等/去重约束(NULL-distinct 使折叠后的约束消失而非收窄)。以本条为准,原裁定作废。方向上采纳 dev 的 B(ADR + spec 显式 scope 词汇 + ADR-0087 conversion,存量零漂移):两轴同向 —— 长期上唯一不让契约面自相矛盾的路;防 AI 上唯一在 authoring 期强制表态、漏写即响亮拒绝的路。但 B 需要动
packages/spec(协议变更)并解除本轮 spec/lint 冻结,超出本车道授权,交维护者:批 B(建议移交 spec 车道执行)或先 PARK 等 v17 后。⛔ 在拍板前,任何人不应在 driver-sql 单侧改语义。#5030(复合唯一在 NULL 租户行上零约束)是比本单更深的前置缺陷,建议与 B 同一张 ADR 一并定。
Generated by Claude Code
claude commentedon Aug 4, 2026
维护者裁定(2026-08-04):本单由维护者单独安排,不入任何 PM 车道的派发视野;
needs-user-decision标签保留作占位,各车道 sweep 请让行。方案 B 的完整分析与 #5030 关联见上两条评论。Generated by Claude Code
os-zhuang commentedon Aug 4, 2026
🔒 认领:维护者直接指派(接上条维护者裁定,非 PM 车道派发)
会话:
session_01Akrzh2mHi2siSNVPPtfTw7(账号os-zhuang)分支:
claude/issue-4986-analysis-24ol6qWorktree:
objectstack-issue-4986-adr本轮交付物:方案 B 的 ADR 初稿(仅 ADR 文档,零代码语义改动),按维护者要求一次性覆盖各类业务需求场景(平台级唯一/租户级唯一/幂等去重键/单租户栈 NULL 租户行/存量迁移),并把 #5030 的方向纳入同一张 ADR。⛔ 维持上条 PM 撤回令:拍板前不动 driver-sql 单侧语义;本轮不改
packages/spec/packages/lint任何代码,仅提交 ADR 供评审。Generated by Claude Code
docs(adr): ADR-0120 — deployment-mode portability is a first-class co…
2 remaining items
os-zhuang commentedon Aug 4, 2026
🔒 认领:维护者直接派发(ADR-0120 17.x 波第二块 —— spec + lint 侧)
派发来源:维护者会话
session_01Akrzh2mHi2siSNVPPtfTw7(接上条维护者裁定;issue 已 assign 给共享身份,不改 assignee)分支:
claude/issue-4986-unique-scope-vocabWorktree:
objectstack-issue-4986-spec-lint本轮交付物(范围 =
packages/spec+packages/lint,含测试与再生成),照 ADR-0120(Status: Accepted 2026-08-04)执行,不重开方向之争:UniqueScopeSchema扩为boolean | 'global' | 'organization'(17.x 纯增量,零语义变化);§Terminology:'tenant'/'org'不接受、不做别名,zod 报错指名'organization';IndexSchema.unique注释块与describe()按新契约重写(verbatim 收窄到'global';裸true17.x 警告 → 协议 18 拒绝,指向 ADR-0120 协议 18 项:D2 conversion(声明索引 unique: true → 'global')+ 裸 true 硬拒 + synonym pin 退役 —— 挂 18 列车,现在勿动工 #5082);UniqueScopeSchema文档块「复合索引退化为单列」假豁免替换为 D3 真相(NULL 桶 + COALESCE);unique/unscoped-declared-index(警告)+ R10unique/double-declaration四象限重写,测试齐全;⛔ 范围外:driver-sql(由 #5030 的 PR 承载,本 PR 晚于它合并)、D5e 安装门与 indexing.mdx(#5081)、协议 18 项(#5082)、
content/docs/releases/零触碰。Generated by Claude Code
os-zhuang commentedon Aug 4, 2026
ADR-0120 已 Accepted 并合入 main(#5054 →
docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md,维护者裁定 2026-08-04)。此前 PM 撤回令中的 ⛔「拍板前不动 driver-sql 单侧语义」随 D8 解除,实现按 D7 分波:COALESCE(organization_id,'__global__')物化 + drift 两侧 + 重复预检),最先合;{ fields, unique: true }在 tenant-scoped 对象上落成平台级 UNIQUE —— normalizeDeclaredIndex 不补租户列(#4698 实例 2 移交) #4986(本单) — spec 词汇'organization'+ lint(D5a 新规则 + R10 重写),必须晚于 driver-sql: 单租户栈上 organization_id 恒为 NULL,#3696 的 (tenant, col) 复合 UNIQUE 因 NULL-distinct 而完全不生效 —— 字段级 unique: true 静默零约束 #5030 合(否则 spec 收词而 driver 未物化,declared≠enforced);true硬拒),挂起勿动。#5030 与本单已由维护者会话(
session_01Akrzh2mHi2siSNVPPtfTw7)派发 dev agent 开发中,均只开 draft PR;合并等 v17.0 cut 之后。各车道 sweep 请继续让行。Generated by Claude Code
os-zhuang commentedon Aug 4, 2026
🔒 接续认领:ADR-0120 17.x 波第二块(spec + lint 侧)—— 前一开发会话在环境回收时中断,本会话接手续做
派发来源:维护者会话
session_01Akrzh2mHi2siSNVPPtfTw7(同上条派发,任务与范围不变)分支:
claude/issue-4986-unique-scope-vocab(同分支续做,不另起)Worktree:
objectstack-issue-4986-spec-lint(前会话未提交改动完好,已全量审计后继续)范围与上条派发一致:D1 词汇(
'organization','tenant'/'org'拒绝并指名)、D1/D6IndexSchema.unique契约重写、D3 真相替换假豁免、D5a 新规则 + D5b R10 四象限、再生成 + changeset + ADR 锚点。⛔ 不动 driver-sql(#5030)、不动 D5e/indexing.mdx(#5081)、不动协议 18 项(#5082)、不碰content/docs/releases/。仅开 draft PR,合并等 v17.0 cut 且晚于 #5030。Generated by Claude Code
os-zhuang commentedon Aug 4, 2026
✅ spec + lint 侧交付:draft PR #5208(分支
claude/issue-4986-unique-scope-vocab,续做完成)lintUnscopedDeclaredIndexes未入 AUTHORING_RULES 注册表([cli/lint] 作者时规则的命令覆盖漂移是系统性的:23/26 条手工接线规则只跑部分命令,9 条 error 级有门禁盲区(os build 会发布 os lint 拒绝的流) #4409 wiring 测试红),已改为独立注册条目(validate/build)+lintDataModel直调(os lint),三命令全覆盖、每 finding 恰好一报。check:generated8/8 绿;check:adr-anchors绿。Generated by Claude Code
docs(adr): ADR-0120 — explicit unique-scope vocabulary (organization/…
os-zhuang commentedon Aug 4, 2026
落地同步核验记录(engine/services 车道 PM,会话
session_01Pbu27iNUfQCHeuS551Rqo7):按维护者「现在就合」指示派出的 os-regen 四步同步 agent 与维护者会话的自行同步(84396f2b2)并发相遇,未强推,转为交叉验证:(1) 对方合并的生成物 delta 与本 agent 独立整体重生成的结果字节一致;(2) 在更新的 main(c183a12bd,含 #5237 spec 提交)上预演队列即将执行的合并 —— 重生成后工作树零差异,check:generated9/9 最新,双方兄弟条目全部在位(#5237 allowAddTab、#5022、#5212__global__、#5203、#5182 与本 PR 的isOrganizationUnique/UniqueScopeSchema三态/两条 lint 规则);spec 306 文件 7871 测试 + lint 1194 全绿。结论:队列合并的 os-regen 静默吞并风险已实证排除,#5208 在队列中的头提交可放心落地。附带:adr-anchors.json 冲突按集合并集解决(ADR-0120 五锚全保留,26 文件 OK)。Generated by Claude Code
feat(driver-sql)!: NULL-safe organization uniques + declared-index 'o…
feat(spec,lint): unique 范围显式词汇 'organization' + D5a/D5b lint 规则(ADR-0…
fix(driver-memory): enforce object-level declared `indexes[]` uniquen…
fix(driver-memory): enforce object-level declared `indexes[]` uniquen…