feat(spec)!: 退休 datasource.readReplicas —— 声明了、strict 了、刚被加了校验,但没有任何东西打开过副本连接 (#4468) - #4481
Merged
Merged
Conversation
Several agents work this repo at once and an unassigned issue reads as an open invitation. Two agents starting the same issue burn the same hours twice and then race to land conflicting shapes for one problem. Makes assignment the first action of a task — before the worktree, before the first read — and states the corollary for Prime Directive #10: file a finding unassigned, assign it at the moment you actually start, so the issue list works as a queue other agents can trust. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WsgTqRF58HsQYKLsrZ5pQY
BREAKING CHANGE: `datasource.readReplicas` is removed. It described replica connections nothing ever opened. `ConnectableDatasource` and `DatasourceConnectionSpec` carry no replicas field, the driver factory never reads the key, and no query path distinguishes a read from a write — the platform has no read/write splitting at all, so every statement always went to the primary regardless of what was declared. There is no target to move to, because there is no read-replica routing to move to. Front replicas behind one endpoint (pgpool, ProxySQL, an RDS reader endpoint) and point `config` there; `os migrate meta --from 16` strips the key. Worth recording why this one survived so long: #4410 closed the `datasource.config` gap and, reasonably, extended the new per-driver validation over each `readReplicas` entry. The result was a slot with every marker of a working feature — declared, `.strict()`-guarded, and validated field-by-field against the driver's contract, rejecting a misspelt replica host by index. None of that is evidence of a consumer and all of it reads like one. Rigor is cheap to add to a dead slot and expensive to tell apart from life, which is why ADR-0049 asks for a consumer rather than for rigor. The retirement kit: - DatasourceSchema: key deleted (strict route), `readReplicas`/`replicas` added to the guidance map so the rejection carries the prescription; the #4410 validation loop removed with it - `datasource-read-replicas-removed` D2 conversion + step-17 chain wiring, retired from the load path like the other keys retired for misdescribing themselves - authorable-surface baseline line deleted deliberately (gate (a)'s strict- removal trip wire); spec-changes, upgrade guide, reference docs regenerated - pin test flipped from "validates each entry" to "rejects the slot" - release notes, strictness ledger, and the two pending changesets that still described the key corrected Follow-ups filed: #4479 (read-replica routing as a real feature request, starting from the read/write decision point) and #4480 (the same feature declared a second time as `DatabaseConnector.readReplicaConfig`, also unread). Closes #4468 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WsgTqRF58HsQYKLsrZ5pQY
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
added a commit
to objectstack-ai/objectui
that referenced
this pull request
Aug 1, 2026
`datasource.readReplicas` is retired in @objectstack/spec 17 — nothing in the platform ever opened a replica connection, so the "2 read replicas" pill reported a configuration that did not exist. The pill did not echo the draft back, which is what a preview panel is for and is harmless. It concluded. And it was the only surface in either repo that acknowledged the key at all, so it was the whole of the evidence that the feature worked. Companion to objectstack-ai/objectstack#4481.
os-zhuang
added a commit
that referenced
this pull request
Aug 1, 2026
…ype (#4487) (#4489) `GOVERNED` was a hand-maintained list and nothing compared it against the registry it claims to cover. It governed 15 of 25 registered metadata types while reporting itself complete: a type in the other ten was authorable — served by /api/v1/meta/types/:type, editable in Studio — and was never asked who reads its properties, so an inert key on it was invisible to CI and its silence read as success. `datasource` was in that state for its entire life. #4410, #4465 and #4481 found six inert keys on it by hand, two security-shaped: `schemaMode` was dropped between record and connection spec, so a database ObjectStack must never run DDL against was constructed as `managed`; `ssl` stopped at the record, so a TLS block with a CA certificate configured nothing while looking identical to one that worked. The gate is now answerable to the registry. Every registered type must be in GOVERNED or in PENDING_GOVERNANCE with a reason and an issue; registering a type and forgetting the ledger fails CI with the entry to write. The reverse rots too and also fails — a PENDING_GOVERNANCE row for a type since governed claims a debt that no longer exists. `datasource` is now governed: 43 properties classified with evidence, and the result is the highest dead ratio of any governed type — 20 of 43 have no runtime consumer. `capabilities.*` (11): the engine gates pushdown on the runtime driver's own `supports.*` object, a non-overlapping vocabulary. `healthCheck.*` (3): nothing schedules a datasource probe. `retryPolicy.*` (4): no connect or query path retries. Plus `external.label` and `external.requirePermission`. One correction ships with this, and it is why the audit was worth doing. `capabilities.readOnly` reads as a safety switch and gates nothing — and two shipped prescriptions pointed authors at it: the externalSettingsUnknownKeyError guidance in datasource.zod.ts and the #4465 changeset's relocation table. Both now name `external.allowWrites: false`, the write gate the engine checks. The v17 release notes carried a matching false claim about `capabilities` gating pushdown; corrected here too. The CLI advisory lint picks the ledger up, so `os compile` warns an author who sets any of the 20. That needed `datasource` in TYPE_COLLECTIONS: coverage grows by marking entries authorWarn only WITHIN a type the lint already walks, and a governed type whose collection is unregistered has a correct ledger that warns nobody. Nine types remain ungoverned and are now enumerated rather than implied (#4488). Claude-Session: https://claude.ai/code/session_01WsgTqRF58HsQYKLsrZ5pQY Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4468
结论
datasource.readReplicas描述的副本连接,从来没有任何东西打开过:ConnectableDatasource/DatasourceConnectionSpec没有 replicas 字段 —— 配置到不了驱动层createDefaultDatasourceDriverFactory从不读这个 key第三条是决定性的。就算副本连接建好了,也没有东西可以路由过去 —— 平台不存在读写分离。所以每一条语句一直都走主库,无论这里声明了什么。
按 ADR-0049 enforce-or-remove 走 remove,因为 enforce 的对象不存在:这不是一个"差最后一根线"的字段,它描述的整个机制都还没建。
为什么这个字段特别值得写一段
#4410(上一个 PR,#4465)修的是
datasource.config从来没人校验。修的过程中很自然地把新的按驱动校验也铺到了readReplicas的每一条上 —— 副本和主库配置同形,这个决定当时是对的。结果是这个 slot 集齐了一个"能用的功能"的全部外观特征:
.strict()守着,拼错 key 会被拒hostname写错会按下标报出来:"第 1 条副本的hostname应该是host"这些没有一条能证明存在消费者,而每一条读起来都像。给一个死 slot 加精度是廉价的,把它和活的区分开是昂贵的 —— 这正是 ADR-0049 要的是 consumer 而不是 rigor 的原因。
第二个被骗到的表面在 objectui:
DatasourcePreview渲染一枚 "2 read replicas" 的 pill。作者配好副本、保存、看到 pill 亮起,得到的是平台在说"我收到了"。它还是两个仓库里唯一承认这个 key 存在的地方,所以它就是"这功能能用"的全部证据。packages/spec/liveness/README.md有一条常设规则专治这个 —— authoring/preview renderer 永远不算 runtime consumer,2026-06 那次仅凭 preview 定性的扫描,13 个属性复核后错了 10 个。配套 PR:objectstack-ai/objectui#(见下方 objectui 分支)。两边一起走,避免出现 spec 已经拒收、UI 还在数副本数量的中间态。
改了什么
按
.claude/skills/spec-property-retirement的 strict-removal 路线(DatasourceSchema是.strict(),所以是删 key + guidance map,不是retiredKey()tombstone):DATASOURCE_KEYS删除;replicas别名改为共享同一条 prescription(而不是重命名到一个同样已消失的 key);#4410 加的readReplicas校验循环一并移除datasource-read-replicas-removed(D2)+ step-17 chain 接线,retiredFromLoadPath: true—— 和其他"自我描述失实"而非"被改名"的 key 同档authorable-surface.json的行有意删除 —— 这是 gate (a) 对 strict removal 的绊线,skill 明确要求在同一个 PR 里删掉;spec-changes.json/ upgrade guide / reference docs 重新生成major;另外修正了两个尚未发布的 changeset 里仍在描述这个 key 的段落顺手修正的一处旧文案
content/docs/releases/v17.mdx里 datasource 那段仍然写着"Nothing validates inside them … tracked as #4410"。#4410 已经在 #4465 里落地了,这句话在合并那一刻就过期了 —— 是我上一个 PR 漏掉的。这次一并改成描述现状。迁移
没有可迁移的目标,因为没有读副本路由可以迁移过去。
readReplicas: [{ host: 'replica-a', … }]replicas: [ … ]os migrate meta --from 16自动删除,每个 datasource 一条 notice。手写会被 parse 拒绝并给出处方。读吞吐不受影响 —— 那些连接本来就没建立过。真需要副本读,把它们藏在单个端点后面(pgpool、ProxySQL、RDS reader endpoint),
config指过去。这在这个 key 被删之前也是唯一有效的路径。验证
@objectstack/spec— 286 files / 7253 tests passed@objectstack/cli— 66 files / 678 tests passed(含migrate-meta.e2e,覆盖 conversion 重放与幂等)@objectstack/service-datasource— 11 files / 195 tests passeddatasource不在 liveness ledger 的GOVERNED列表里,所以没有 ledger 行要动。这本身是个缺口 ——datasource是注册的 metadata type,却不受活性闸门管辖,这正是这个 key 能一直躺着的结构性原因。没有并进本 PR,因为那是一次 ledger 扩容而不是一次 retirement。后续(已开)
DatabaseConnector.readReplicaConfig({ enabled, hosts: [{host, port, weight}] })是同一功能的第二处死声明,同样零消费者。单独开是因为初步看整个integration/connector/*声明层可能都没接上运行时,那是集群级问题,塞进这个 PR 会把一次干净的 retirement 变成失控的审计。未指派Generated by Claude Code