feat(migrate,objectql): 升级路径点名本部署仍未关闭的数据迁移门禁 — ADR-0104 (#3438) - #4253
Merged
Conversation
…ill open here (#3438) Both value-shape gates fail toward leniency — a deployment that never runs its migration keeps warning instead of rejecting, and keeps every released file forever. That default is right and completely silent, so a gate could sit open for the life of a deployment without anyone learning one command ends it. Two announcements, each where an upgrade actually looks: - `os migrate meta --from 16` ends by naming the data migrations a chain crossing into 17 leaves behind, scoped to the field classes the author's own metadata declares; --json carries the same list as `dataMigrations`. The command reads no database, so it reports what remains to do and never claims what a deployment has done — which is why the ADR's "with their gate status" lands on the other half. - The server logs one line per open gate at boot, naming the command that closes it. Only the lax posture announces itself; a verified gate already logs that it enforces, and an app declaring neither class costs nothing and says nothing. On `kernel:bootstrapped`, not `kernel:ready`: the answer depends on the storage service's own ready handler, which registers sys_migration and may attest a store it just created — racing it would tell a brand-new deployment its gates are open moments after they closed. Enforcement is unchanged: same gates, same flags, same default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KM46HedAWLbkuZyMBXn5wJ
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 29 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 31, 2026 02:15
os-zhuang
pushed a commit
that referenced
this pull request
Jul 31, 2026
…false (#3438) The boot line that names an open value-shape gate (#4253) fired on every deployment there is, and stated something untrue on any deployment that had already settled the posture with an environment switch. Both are one failure: an advisory that speaks where it does not apply is exactly how readers learn to ignore it, and the line's whole value is that a warn-first deployment is told once, credibly, which command ends warn mode. Neither defect is reachable from the suite that shipped with the announcement. `engine.test.ts` mocks `./registry`, and a mocked registry hands the engine exactly the fields the test wrote — so these cases get a sibling file built on the genuine `registerApp → registry` path, which is the only way a test sees what a deployment sees. `objectHasCoveredValueField` — the dormancy short-circuit meant to spare an object with no covered field the flag query — tested raw type membership, while the real registry INJECTS covered-type fields into every object it registers: `organization_id` and `owner_id` (both `system`), `created_by` and `updated_by` (both in `SKIP_FIELDS`), four `lookup`s. `validateRecord` skips every one of them before it reaches the value-shape check, so the short-circuit answered `true` for literally every object, never fired, and its WeakMap memoized a constant. It now counts by the validator's own `isScannableValueShapeField` — the predicate the scanner already imports. Three readings of "a covered field" drifting by one clause is how a gate ends up governing fields nothing enforces, which is the shape #4235 argued against. The announcement also consulted no environment switch, though both postures it reports on short-circuit ahead of the deployment flag it reads. Under `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` enforcement is already on for both classes, so "checked but NOT enforced here" was false; under either opt-out the operator chose leniency deliberately, so naming a migration that cannot change what they get is noise. Each gate consults its own pair — `mediaPostureSetByEnv` and `valueShapePostureSetByEnv`, siblings for the reason `mediaStrictEffective` and `valueShapeStrictEffective` are siblings, since the opt-outs are per-class while the opt-in opens both. Cheapest test first, so a kernel with nothing to say still reaches no flag query. Enforcement is unchanged: same gates, same flags, same default. The flag read was already memoized per process, so what this corrects is the property ADR-0104 states, not a user-visible cost. Verified: objectql 1345 (7 new), cli 628, spec check:generated 8/8, objectql build, check:release-notes and check:doc-authoring pass, ESLint clean. One pre-existing failure on main is untouched by this branch and reproduces with zero of its source changes: `query-expression-conformance.test.ts` > "the removed nested-select object form is refused BY NAME", from #4268. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
os-zhuang
pushed a commit
that referenced
this pull request
Jul 31, 2026
…false (#3438) The boot line that names an open value-shape gate (#4253) fired on every deployment there is, and stated something untrue on any deployment that had already settled the posture with an environment switch. Both are one failure: an advisory that speaks where it does not apply is exactly how readers learn to ignore it, and the line's whole value is that a warn-first deployment is told once, credibly, which command ends warn mode. Neither defect is reachable from the suite that shipped with the announcement. `engine.test.ts` mocks `./registry`, and a mocked registry hands the engine exactly the fields the test wrote — so these cases get a sibling file built on the genuine `registerApp → registry` path, which is the only way a test sees what a deployment sees. `objectHasCoveredValueField` — the dormancy short-circuit meant to spare an object with no covered field the flag query — tested raw type membership, while the real registry INJECTS covered-type fields into every object it registers: `organization_id` and `owner_id` (both `system`), `created_by` and `updated_by` (both in `SKIP_FIELDS`), four `lookup`s. `validateRecord` skips every one of them before it reaches the value-shape check, so the short-circuit answered `true` for literally every object, never fired, and its WeakMap memoized a constant. It now counts by the validator's own `isScannableValueShapeField` — the predicate the scanner already imports. Three readings of "a covered field" drifting by one clause is how a gate ends up governing fields nothing enforces, which is the shape #4235 argued against. The announcement also consulted no environment switch, though both postures it reports on short-circuit ahead of the deployment flag it reads. Under `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` enforcement is already on for both classes, so "checked but NOT enforced here" was false; under either opt-out the operator chose leniency deliberately, so naming a migration that cannot change what they get is noise. Each gate consults its own pair — `mediaPostureSetByEnv` and `valueShapePostureSetByEnv`, siblings for the reason `mediaStrictEffective` and `valueShapeStrictEffective` are siblings, since the opt-outs are per-class while the opt-in opens both. Cheapest test first, so a kernel with nothing to say still reaches no flag query. Enforcement is unchanged: same gates, same flags, same default. The flag read was already memoized per process, so what this corrects is the property ADR-0104 states, not a user-visible cost. The predicate fix is pinned by reverting it: with raw-type membership restored, `stays silent when no object declares a covered field` fails, because a `note` declaring only text still carries the four injected lookups. Verified on a full workspace build: objectql 1345 (7 new), cli 628, spec check:generated 8/8, check:release-notes and check:doc-authoring pass, ESLint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…false (#3438) (#4284) The boot line that names an open value-shape gate (#4253) fired on every deployment there is, and stated something untrue on any deployment that had already settled the posture with an environment switch. Both are one failure: an advisory that speaks where it does not apply is exactly how readers learn to ignore it, and the line's whole value is that a warn-first deployment is told once, credibly, which command ends warn mode. Neither defect is reachable from the suite that shipped with the announcement. `engine.test.ts` mocks `./registry`, and a mocked registry hands the engine exactly the fields the test wrote — so these cases get a sibling file built on the genuine `registerApp → registry` path, which is the only way a test sees what a deployment sees. `objectHasCoveredValueField` — the dormancy short-circuit meant to spare an object with no covered field the flag query — tested raw type membership, while the real registry INJECTS covered-type fields into every object it registers: `organization_id` and `owner_id` (both `system`), `created_by` and `updated_by` (both in `SKIP_FIELDS`), four `lookup`s. `validateRecord` skips every one of them before it reaches the value-shape check, so the short-circuit answered `true` for literally every object, never fired, and its WeakMap memoized a constant. It now counts by the validator's own `isScannableValueShapeField` — the predicate the scanner already imports. Three readings of "a covered field" drifting by one clause is how a gate ends up governing fields nothing enforces, which is the shape #4235 argued against. The announcement also consulted no environment switch, though both postures it reports on short-circuit ahead of the deployment flag it reads. Under `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` enforcement is already on for both classes, so "checked but NOT enforced here" was false; under either opt-out the operator chose leniency deliberately, so naming a migration that cannot change what they get is noise. Each gate consults its own pair — `mediaPostureSetByEnv` and `valueShapePostureSetByEnv`, siblings for the reason `mediaStrictEffective` and `valueShapeStrictEffective` are siblings, since the opt-outs are per-class while the opt-in opens both. Cheapest test first, so a kernel with nothing to say still reaches no flag query. Enforcement is unchanged: same gates, same flags, same default. The flag read was already memoized per process, so what this corrects is the property ADR-0104 states, not a user-visible cost. The predicate fix is pinned by reverting it: with raw-type membership restored, `stays silent when no object declares a covered field` fails, because a `note` declaring only text still carries the four injected lookups. Verified on a full workspace build: objectql 1345 (7 new), cli 628, spec check:generated 8/8, check:release-notes and check:doc-authoring pass, ESLint clean. Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…ly landed (#3438) (#4299) The 2026-07-30 addendum's rollout bullet says `os migrate meta --from 16` ends by naming the two data migrations "with their gate status". The implementation (#4253) deliberately names them without it, and the reasoning lived only in that PR's commit message — so the decision record kept prescribing the wrong half to the next implementer, and already misled one (#4284's first draft built exactly what the sentence says). The status half was a category error this ADR had already corrected two addenda earlier: per the 2026-07-27 addendum the evidence gate is per DEPLOYMENT, and `meta` acts on SOURCE, which deploys to zero-or-N deployments — so "its" gate status has no referent at meta time. Best-effort status (show it when a datastore happens to be reachable) would be worse than none: silence goes ambiguous, output turns environment-dependent, and the command gains a hang path on an unreachable database. Status is reported where a datastore is actually open — each migration command reports before it writes, and the boot line carries the live verdict, which after #4284 speaks only where it is true. Amended in place with the ADR's own convention (the L428-style dated inline parenthetical), original text preserved. Empty changeset per the adr-0104-attestation-adr-note precedent. Releases nothing. Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv Co-authored-by: Claude <noreply@anthropic.com>
6 tasks
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.
落地 ADR-0104 2026-07-30 addendum 里 #3438 的最后一项。前三项已分别由 #4213(D2 严格默认)、#4215(出生认证)、#4235(
os migrate value-shapes扫描门禁)完成。问题
两个值形态门禁都倒向宽松:没跑迁移的部署继续只告警不拒绝,且释放的文件永远保留。这个默认值是对的——但它完全无声。一个门禁可以在部署的整个生命周期里一直开着,而没有人知道"一条命令就能关上它"。
做法:两处发声,各自只说自己有资格说的话
1.
os migrate meta --from 16(元数据那一半) 在结尾点名跨入 17 时遗留的数据迁移,并且只列作者元数据真正声明了的类别——没有 media 字段的应用永远不会被告知文件迁移。--json同步输出dataMigrations。2. 运行中的服务器每次启动对每个仍开着的门禁打一行日志,点名关闭它的命令。
ADR 写的是
os migrate meta"ends by naming the two data migrations with their gate status"。实现时发现该命令纯跑元数据栈,根本不连数据库(loadConfig→normalizeStackInput→applyMetaMigrations),拿不到任何部署的门禁状态。我没有为了对齐措辞去硬造一个数据库连接,而是按各自能力如实拆分:
os migrate meta合起来正好覆盖 ADR 的意图,而且没有任何一方声称自己不知道的事。
关键设计点:为什么挂
kernel:bootstrapped而不是kernel:ready这条提示的答案依赖 storage 服务自己的
kernel:ready处理器——它注册sys_migration,并且可能刚给一个自己新建的库做完出生认证(#4215)。在kernel:ready里和它赛跑,会让一个全新部署在门禁刚关上的瞬间被告知"门禁还开着"。kernel.ts对kernel:bootstrapped的注释正是为这种依赖而写:「任何依赖其他插件kernel:ready产物的工作应放这里,而不是在kernel:ready(那里处理器顺序会与数据赛跑)」。只有宽松姿态才发声
强制行为零变化
同样的门禁、同样的标记、同样倒向宽松的默认值。本 PR 只增加"告知"。
测试
engine.test.ts+6:开着的门禁点名对应命令、按声明字段类别定向(有 media 不提 value-shapes)、已验证不提示、两类字段都没有则零查询零输出、每进程只说一次、注册表读取抛错不抛进启动。顺带给测试用的 registry mock 补上getAllObjects。migrate-meta.e2e.test.ts+6(真实 CLI 进程):media → 只列文件迁移、covered → 只列 value-shapes、两者都有 → 都列、两者都无 → 空、不跨 17 → 空、人类可读输出里也出现(含"dry-run by default"字样)。pnpm test132/132 绿 · lint ✅ · nul-bytes ✅ · doc-authoring ✅其中一条 e2e 我最初写错了前提(以为 16→17 存在"无需改写"的分支,实际该链对任何 stack 都会产生语义 TODO),已改为断言真实成立的行为。
文档
cli.mdx新增「How you find out a gate is open」一节,写明两处发声各自能回答什么、不能回答什么。至此 #3438 四项全部完成
os migrate value-shapes扫描门禁🤖 Generated with Claude Code
https://claude.ai/code/session_01KM46HedAWLbkuZyMBXn5wJ
Generated by Claude Code