Repository navigation
Description
来源:PR #3168(#3058 的右尺寸落地)对抗式核查时发现。原计划想把对象级记录归属模型 ownership 声明进 ObjectSchema,核查后发现这么做会撞上一个真实的、多面的契约冲突,故未在 #3168 中改动,单开此 issue。
现象:一个 ownership key,四处互不一致的处理
对象 schema 上的 ownership 属性同时被四处以不同语义对待:
-
引擎读它当「记录归属模型」
'user' | 'org' | 'none'
packages/objectql/src/registry.ts:272—applySystemFields里:const ownership = (schema as any).ownership as 'user' | 'org' | 'none' | undefined; const wantOwner = ownership !== 'org' && ownership !== 'none' && !managedBy && !name.startsWith('sys_');
ownership: 'org' | 'none'用于opt-outowner_id注入(Dataverse 风格 catalog/junction 表)。这是有单测的行为:packages/objectql/src/registry.test.ts:695(respects ownership: "org" / "none" opt-out)。注意读取用的是(schema as any)—— 因为该属性未在ObjectSchema声明。 -
ObjectSchema.create()把它当未知 key 直接拒绝(报错)
packages/spec/src/data/object.zod.ts:ownership不在ObjectSchemaBase(:595)中;顶层未知 key 由unknownKeyError(:1162)按 ADR-0032「no silent failure」/ Object-levelworkflows: [...](and any unknown ObjectSchema key) is silently stripped at build — no error/warning (ADR-0032 'no silent failure', metadata layer) #1535 抛错,并有编译期NoExcessObjectKeys(:1186)兜底。也就是说,作者若在ObjectSchema.create({ ..., ownership: 'org' })里设它,会拿到构建错误 —— 引擎依赖的这个 opt-out,作者无法经受支持的create()路径合法设置。 -
脚手架仍在生成
ownership: 'own'
packages/cli/src/commands/generate.ts:28的 object 模板仍输出ownership: 'own',。作者把它塞进ObjectSchema.create()就会命中上面的拒绝(正是packages/cli/CHANGELOG.md:3876描述、并声称已对 app/plugin 脚手架修掉的那个迁移陷阱 —— 但generate.ts的 object 脚手架未清理)。而'own'对引擎(facet 1)来说!== 'org' && !== 'none',等价于「user 归属」,语义也不对味。 -
CLI 把它当「贡献者归属」
'own' | 'extend'显示
packages/cli/src/commands/info.ts:77—const ownership = obj.ownership || 'own';,再打印(N fields, {ownership})。这是第三种语义:'own' | 'extend'(ObjectOwnershipEnum,object.zod.ts:1459)其实是ObjectContributor.ownership(registry.ts:29)—— 描述某个包是 own 还是 extend 一个对象,经registerObject(..., ownership='own')(registry.ts:581)设置在贡献者记录上,而非作者的对象 schema。CLI 读错了源,并用'own'兜底。
为什么是债
- 契约漏洞:一个 opt-out(记录归属)被引擎依赖且有测试,却对应一个校验器主动拒绝、作者无法合法声明的 key。测试之所以绿,是因为
registry.test.ts:695传的是未经ObjectSchema.create()校验的裸对象;经校验的作者路径与被测路径不一致。 - 命名撞车:
ownership同时承载「记录归属模型user/org/none」与「贡献者归属own/extend」两个正交概念,registry.ts与cli/info.ts各取一义。 - 脚手架回归:
generate.ts仍发一个会被create()拒绝、且语义错位的ownership: 'own'。
可选解法(需定契约方向,勿盲改)
- A. 把记录归属模型立为一等字段:在
ObjectSchemaBase声明ownership: z.enum(['user','org','none']).optional(),同步:generate.ts脚手架值改'user'、info.ts按记录模型显示、registry.ts:272去掉as any。让被测的引擎 opt-out 变为可合法声明。代价:ownership之名仍与贡献者own/extend概念在语义上重叠。 - B. 给记录归属模型换个无歧义的名(如
recordOwnership/ownershipModel):声明它、迁移引擎读取、修脚手架+CLI,own/extend之名留给贡献者概念。命名更干净,但要迁移。 - C. 判定记录归属 opt-out 不应由作者在 schema 上设,改由别的机制驱动,并移除
registry.ts对schema.ownership的读取。
三条都触及 ADR-0032(严格 key)与归属模型,建议先定方向再落地。
关联
- 发现来源:PR test(security): pin server-managed field set + tenant-wall write guard; fix systemFields docs (#3058) #3168 / 架构:把「服务端托管字段」收敛成一个声明式 schema 概念(统一 owner_id / 公开表单托管集 / organization_id 的中间件特例) #3058
- ADR-0032(no silent failure)/ Object-level
workflows: [...](and any unknown ObjectSchema key) is silently stripped at build — no error/warning (ADR-0032 'no silent failure', metadata layer) #1535(未知 key 拒绝) packages/cli/CHANGELOG.md:3876(app/plugin 脚手架的同类迁移陷阱记录)
Activity
fix(spec): declare `ownership` as a first-class ObjectSchema field (#…
fix(vscode): drop invalid `ownership: 'own'` from the object snippet;…
fix(spec): drop the dead `systemFields.owner` key (#3175 follow-up) (#…
test(spec): the fourth data/ file group's test titles state each cite…