Skip to content

feat(migrate,objectql): 升级路径点名本部署仍未关闭的数据迁移门禁 — ADR-0104 (#3438) - #4253

Merged
os-zhuang merged 1 commit into
mainfrom
claude/adr-0104-d3-wave2-eval-2v9jsy
Jul 31, 2026
Merged

feat(migrate,objectql): 升级路径点名本部署仍未关闭的数据迁移门禁 — ADR-0104 (#3438)#4253
os-zhuang merged 1 commit into
mainfrom
claude/adr-0104-d3-wave2-eval-2v9jsy

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

落地 ADR-0104 2026-07-30 addendum 里 #3438 的最后一项。前三项已分别由 #4213(D2 严格默认)、#4215(出生认证)、#4235os migrate value-shapes 扫描门禁)完成。

问题

两个值形态门禁都倒向宽松:没跑迁移的部署继续只告警不拒绝,且释放的文件永远保留。这个默认值是对的——但它完全无声。一个门禁可以在部署的整个生命周期里一直开着,而没有人知道"一条命令就能关上它"。

A gate nobody is told about is served by nobody.(ADR 原话)

做法:两处发声,各自只说自己有资格说的话

1. os migrate meta --from 16(元数据那一半) 在结尾点名跨入 17 时遗留的数据迁移,并且只列作者元数据真正声明了的类别——没有 media 字段的应用永远不会被告知文件迁移。--json 同步输出 dataMigrations

2. 运行中的服务器每次启动对每个仍开着的门禁打一行日志,点名关闭它的命令。

⚠️ 与 ADR 措辞的一处偏离(有意,且已在文档说明)

ADR 写的是 os migrate meta "ends by naming the two data migrations with their gate status"。实现时发现该命令纯跑元数据栈,根本不连数据库loadConfignormalizeStackInputapplyMetaMigrations),拿不到任何部署的门禁状态。

我没有为了对齐措辞去硬造一个数据库连接,而是按各自能力如实拆分:

说话者 有资格回答 无法回答
os migrate meta 还有哪些迁移要做(读元数据) 本部署做没做
启动中的服务器 本部署的实际门禁状态(读数据库)

合起来正好覆盖 ADR 的意图,而且没有任何一方声称自己不知道的事。

关键设计点:为什么挂 kernel:bootstrapped 而不是 kernel:ready

这条提示的答案依赖 storage 服务自己的 kernel:ready 处理器——它注册 sys_migration,并且可能刚给一个自己新建的库做完出生认证(#4215)。在 kernel:ready 里和它赛跑,会让一个全新部署在门禁刚关上的瞬间被告知"门禁还开着"kernel.tskernel:bootstrapped 的注释正是为这种依赖而写:「任何依赖其他插件 kernel:ready 产物的工作应放这里,而不是在 kernel:ready(那里处理器顺序会与数据赛跑)」。

只有宽松姿态才发声

  • 门禁已关 → 本来就会记录"正在强制",不再重复;
  • 应用两类字段都没声明 → 零查询、零输出(沿用既有的 dormancy 规则);
  • 读取失败 → 静默(一条提示绝不能成为启动失败的原因);
  • 每进程只说一次。

强制行为零变化

同样的门禁、同样的标记、同样倒向宽松的默认值。本 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 test 132/132 绿 · lint ✅ · nul-bytes ✅ · doc-authoring ✅

其中一条 e2e 我最初写错了前提(以为 16→17 存在"无需改写"的分支,实际该链对任何 stack 都会产生语义 TODO),已改为断言真实成立的行为。

文档

cli.mdx 新增「How you find out a gate is open」一节,写明两处发声各自能回答什么、不能回答什么。

至此 #3438 四项全部完成

PR
D2 动作参数默认严格 #4213
出生认证(新库创建即认证双标记) #4215
os migrate value-shapes 扫描门禁 #4235
升级路径提示 本 PR

🤖 Generated with Claude Code

https://claude.ai/code/session_01KM46HedAWLbkuZyMBXn5wJ


Generated by Claude Code

…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
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 12:52am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m and removed documentation Improvements or additions to documentation tests tooling labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/objectql.

29 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli, @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/objectql)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 31, 2026 02:15
@os-zhuang
os-zhuang merged commit 3ec8186 into main Jul 31, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/adr-0104-d3-wave2-eval-2v9jsy branch July 31, 2026 02:16
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants