Repository navigation
datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583
Description
#4487 把 datasource 纳入活性账本时判了 43 条,其中 20 条 dead,并给每一条写了 authorHint。那次交付的是判定;处置没有人接,至今没有 issue 跟踪。这条就是那个位置。
对比一下同类死键的处置都是一簇一条 issue 立的 —— #3207(object.enable.trash/mru)、#4484(IDataDriver.findStream)、#4579(openApi31)、#4391(crypto.hash)。datasource 这 20 条是全仓最大的一块,也是唯一 20 条全部带 authorWarn 的(全平台 75 条 dead 里只有 29 条带告警),却是唯一没立项的。
处置方向已经定了:全部 remove
20 条的 authorHint 无一例外都是 "Delete it"。这不是本 issue 新做的判断,是 #4487 逐条闭合调用图后的结论 —— 没有一条走 enforce 路线,因为每一条都有一个已经在工作的、不同的机制:
| 簇 | 死键 | 真正被强制执行的是什么 |
|---|---|---|
capabilities.* |
11 条(整块) | 引擎的下推判断读的是运行时 driver 自己的 supports.*(objectql/src/engine.ts:3671, :4529, :4810),和 datasource 元数据是两套机制 |
retryPolicy.* |
4 条(整块) | 连接失败走 datasource-connection-service.ts 的启动策略(降级启动 / bootCritical fail-fast),不按计划重试 |
healthCheck.* |
3 条(整块) | 连接存活按需探测:driver handle 的 ping() / checkHealth()(contracts/datasource-driver-factory.ts:88-92) |
external.label / external.requirePermission |
2 条 | 前者用顶层 label;后者的访问控制走普通对象权限集 + RLS |
三个块会整块清空,这比「20 个键」更干净:DatasourceCapabilities(11/11 死)、healthCheck(3/3 死)、retryPolicy(4/4 死)作为 schema 块整体消失,external 块留下但少两个键。
最严重的一条:readOnly 已经在 shipped 示例里造成了它声称要防的那个 bug
examples/app-crm/src/datasources/crm.datasource.ts 里有一个叫 CRM Analytics Read Replica 的 datasource:
export const CrmAnalyticsDatasource = defineDatasource({
name: 'crm_analytics',
label: 'CRM Analytics Read Replica',
driver: 'sqlite',
config: { filename: ':memory:' },
capabilities: { readOnly: true }, // ← 没有任何写路径读它
active: true,
});它上面的注释自己写着(#4410 留下的):
readOnly是 datasource 能力,不是 sqlite 配置。它本来在config里,直到 #4410 给那个槽加了闸门 —— 一个没有驱动读的键,于是「读副本」是可写的,而每一个信号都说它不是。
搬家没有修好它。 #4410 把 readOnly 从 config(无闸门)挪到 capabilities(有 schema 闸门但仍无人读),注释描述的那个 bug 原封不动地活到今天:这个「只读副本」现在照样可写。
而且这里有个真实的能力缺口,不能靠改处方绕过
账本的 authorHint 给的替代是 external.allowWrites: false(objectql/src/engine.ts 的 assertWriteAllowed)。但读一下那个闸门:
const ds = this.datasourceDefs.get(dsName);
// No recorded definition, or an explicitly managed one ⇒ allow.
if (!ds || !ds.schemaMode || ds.schemaMode === 'managed') return;它只对 external / 联邦 datasource 生效。 crm 那个是本地 sqlite、没声明 schemaMode ⇒ 落进 managed 默认 ⇒ 闸门在第 1904 行直接放行。
也就是说:对一个 managed datasource,目前根本不存在任何被强制执行的只读机制。capabilities.readOnly 是唯一长得像的那个,而它什么也不做。
删掉这个键是对的(它是假合规),但要同时诚实地承认:这不是把作者指向另一个键就能了结的,那个键对他的场景不适用。建议本 issue 只做删除 + 把示例改成不再声称只读,并另立一条 feature issue 讨论 managed datasource 要不要有只读闸门 —— 参照 #4479(readReplicas 移除后另立读写分离立项)的先例,不要在退役 PR 里顺手发明一个机制。
影响半径(已实测)
DatasourceCapabilities全仓引用只有datasource.zod.ts自己两处(:370 driver-definition、:544 datasource)+ 它自己的datasource.test.ts。零运行时消费者,判定可复核。- 唯一的 authoring 站点是上面那个
app-crm示例(capabilities:)。healthCheck/retryPolicy无人 authoring。 - 手写文档没有教这三个块(
content/docs/data-modeling/*.mdx零命中)。healthCheck/retryPolicy在 docs 里的命中全是别的类型的生成参考页(model-registry、plugin-rest-api、events-queue/bus、startup-orchestrator)—— 就是 活性账本覆盖 worklist:9 个已注册 metadata type 仍未治理(#4487 建立闸门后的剩余债务) #4488 点名的「同名不同类型」陷阱,不要照着改。 DatasourceCapabilitiesType是导出类型,在公共 API 面上 ⇒check:api-surface会参与。
路线:strict-removal(账本行删除,不是墓碑)
三个块全部是 .strict()(datasource.zod.ts:427/551/568),按 .claude/skills/spec-property-retirement §2 走删除 + *_RETIRED_KEY_GUIDANCE 处方路线 —— 键离开 walked shape,所以账本行要一并删掉(留着会被 orphans.mts 判 ORPHAN)。反过来(留墓碑却删账本行)会判 UNCLASSIFIED。两个方向都会红。
DatasourceCapabilities 整块消失时,driver-definition 上那处 capabilities(:370)需要单独决定:它不在本账本治理范围内(账本治的是 datasource),但用的是同一个 schema。判它之前先闭合它自己的调用图,不要因为 datasource 侧判死就顺手判死。
建议的批次(可并行认领)
| 批 | 内容 | 备注 |
|---|---|---|
| A | capabilities.* 11 条 + DatasourceCapabilities 整块 + app-crm 示例 |
含安全形状,优先 |
| B | retryPolicy.* 4 条(整块) |
注意别碰 hook.retryPolicy / job.retryPolicy,那两个是被强制执行的,且拼写不同(backoffMs vs baseDelayMs)—— #4488 点名的最险的一个坑 |
| C | healthCheck.* 3 条(整块) |
同名不同类型命中 20 处,没有一处属于 datasource |
| D | external.label / external.requirePermission 2 条 |
块留下,只删两个键 |
每批一个 changeset,FROM → TO 写清楚。批 A 的 changeset 必须明说:删除 readOnly 不等于给了替代方案,managed datasource 的只读能力是另一条 issue。
验收
- 四个簇的键从 schema 消失,
*_RETIRED_KEY_GUIDANCE给出处方 packages/spec/liveness/datasource.json对应行删除(不是改状态),README 计数按现有口径重算check:liveness/check:api-surface/check:generated/ 全仓build+test绿- app-crm 示例不再声称自己只读
- CLI
lint-liveness-properties的 datasource 相关期望更新(20 条告警归零)
参考
- datasource 是注册的 metadata type,却不在 liveness 账本的 GOVERNED 里 —— 两个 PR 手工挖出 6 个死键,正是因为没有闸门看着它 #4487 —— 判定来源(方法论范本)
- datasource.config 至今无人校验:驱动 configSchema 是声明但完全惰性的(ADR-0049 enforce-or-remove,#4001 收尾发现) #4410 / feat(spec,service-datasource): datasource.config 按驱动契约校验 (#4410) #4465 / feat(spec)!: 退休 datasource.readReplicas —— 声明了、strict 了、刚被加了校验,但没有任何东西打开过副本连接 (#4468) #4481 —— 手工挖出那 6 个死键,本闸门存在的理由;datasource.config 至今无人校验:驱动 configSchema 是声明但完全惰性的(ADR-0049 enforce-or-remove,#4001 收尾发现) #4410 正是把
readOnly挪进capabilities的那次 - 读副本路由(read/write splitting):真要做的话,从查询路径开始,而不是从 datasource 上加个字段(#4468 移除 readReplicas 后的正式立项位) #4479 —— 「移除后另立 feature issue」的先例
- #4488 审计发现的四个"授权门断连":email_template / job / validation 的元数据条目到不了执行点,action 导航项点不动 #4509 —— 同一族的四个断连,刚合并;其中 email_template 走 enforce、validation 走 remove,本 issue 全部走 remove
- ADR-0049 enforce-or-remove / ADR-0078 no-silently-inert-metadata
.claude/skills/spec-property-retirement§2(路线分叉)、§4(14 面清单)
Activity
Merge origin/main into #4583 batch A
os-zhuang commentedon Aug 2, 2026
四批全部完成,
datasource死键 20 → 0。capabilities11 键 + 整块(含DriverDefinition.capabilities)retryPolicy×4、healthCheck×3、external.label/external.requirePermission全部走 remove,理由是同一条
九个(+11 个)键没有一个是「用得少」——是每一个都已经有另一套 live 机制在做它看起来在配置的那件事。下推判断读运行时 driver 自己的
supports.*;连接失败归启动策略(降级启动 /bootCritical);存活是 driver handle 的ping()/checkHealth()按需探测;联邦显示名是顶层label;联邦访问由普通对象权限集 + RLS 管。对面都已经有人了,所以没有可建的桥。这和同批 #4509 里
email_template走 enforce 是同一个判据的两侧:看的是「这个形状能不能承载这个功能」,不是「它是不是死的」。两处值得记下来的
capabilities.readOnly(批 A) —— 它读起来像安全属性,也确实被当作安全属性写:showcase 里一个标着 "CRM Analytics Read Replica" 的 datasource 就靠它声称只读,而它照常接受写入。这个键已经被搬过两次(#4410 移出config,#4465 移进capabilities),每个地址上都是惰性的。这次是删掉,而不是搬第三次。同时改掉了所有 SQL driver 共享的READ_ONLY_BELONGS_ON_DATASOURCE—— 它当时仍在把作者指向这个键。retryPolicy(批 B) —— 报错刻意不提供改名建议。hook.retryPolicy/job.retryPolicy是真被强制执行的,但它们是另一个类型上的另一个键,延迟拼作backoffMs而非baseDelayMs。而这个拼写不一致本身就是「没人读 datasource 那个」的证据——全仓没有代码同时读两种拼法。有测试钉住报错文案必须同时包含backoffMs与hook/job:一条把作者引向另一个类型同名键的处方,比没有处方更糟。刻意没做的事 → #4584
删掉
readOnly并没有给作者替代方案。external.allowWrites: false是唯一被强制执行的写闸门,但assertWriteAllowed对 managed(或未声明schemaMode)的 datasource 直接放行——本地库目前根本没有只读闸门。按 #4479 的先例,退役 PR 不就地发明机制:这个缺口立为 #4584,并在墓碑文案里明说它不适用于哪种情况、建议改用数据库账号 GRANT。把作者从一个惰性键指向另一个惰性键,只是把本次要退役的缺陷洗一遍再发出去。
沿途修补
objectql的metadataArrayKeys缺emailTemplates(#4488 审计发现的四个"授权门断连":email_template / job / validation 的元数据条目到不了执行点,action 导航项点不动 #4509 那批发现,同源问题)external: { label: … }只为让块非空,现在写external: {}lint-liveness-properties.test.ts跑在真实 shipped 账本上,所以每次删 datasource 账本行都会反转它的一条断言。它在两批里各翻了一次,现已收敛成断言零发现并保留作回归钉——将来谁重新引入一个 dead+authorWarn 的 datasource 属性,会在这里红,而不是悄悄发出去。关闭。
Generated by Claude Code
feat(spec)!: retire datasource.capabilities — eleven flags nothing re…
feat(spec)!: retire datasource retryPolicy / healthCheck / external i…
feat(spec)!: retire the five keys the lint could never warn about, an…
feat(spec)!: 清空剩余六条 authorWarn 死键 —— book ×2 / job.id / translation.v…
feat(spec)!: 移除 app.areas[] 的两个 fail-open 访问闸门 —— visible / requiredP…
feat(spec)!: 退役 DriverCapabilities 31 个零读者能力位 —— 全表活性审计,3 活 31 死(obje…
feat(spec)!: close AriaProps against unknown keys, reclassify widget …
docs(spec): ui/component.zod.ts 的 29 个站点判定为 `no gate` 而非收紧(objectstac…
feat(spec)!: object 内层块拒绝未知键 —— 13 站点收紧,2 个站点依据实测保留 (objectstack-ai#4001
feat(spec)!: 退役 NotificationActionSchema / EmbedConfigSchema —— 两个没有授…
feat(spec)!: 退役 ui/ 五个没有承载键的交互配置文件 —— touch/dnd/keyboard/animation/of…
docs(spec,drivers): managed-datasource read-only is a database privil…
docs(audits): re-verdict data/field.zod.ts authorable -> no door; rec…
fix(spec)!: a dataset-bound dashboard widget owns appearance, the dat…
fix(spec): os migrate meta guidance for the ui-* and plugin-* migrati…
fix(spec): os migrate meta guidance for the driver-*, kernel-* and sy…
fix(spec): the protocol 16 → 17 conversion summaries and three descri…
test(spec): the first data/ file group's test titles state each cited…