Skip to content

Commit c44dd5e

Browse files
os-zhuangclaude
andauthored
fix(objectql,platform-objects): attest a fresh datastore on this boot's own data, not on the emptiness it remembers (#4769) (#4794)
A store created from empty recorded both ADR-0104 migrations as verified at `kernel:ready`, while the same boot was still seeding rows whose values contradict them. The certificate was true when written and false a second later: the first boot ran warn-first and kept the data, every later boot read the certificate, enforced it, and rejected the rows its predecessor had written. Same data, same code, one restart. Three changes, one invariant — a boot may not prove a contract it violates in that same boot: - The attestation waits for `app:seeded` (the inline seed's settle point, background continuation included), with `kernel:ready` as the backstop for kernels that never seed. Both enter the same idempotent call. - `attestFreshDatastore` consults the engine's tally of ADMITTED value-shape violations first and declines any migration id this boot has already contradicted, naming the object.field and the command that closes the gate. The two ids are judged independently — a bad `cover` does not sink the `location` gate. The engine records those admissions from the warn-first path with the exact predicate strict mode uses, so certifying still needs a scan while refuting needs the one counterexample the write already computed. - A certificate contradicted AFTER it was issued is revoked from the write path that contradicted it, scoped to creation attestations on a store this boot created; evidence produced by a real migration run is never rewritten. Also fixes the memoized flag read, the other half of why the first boot looked green: "the ledger says no" is now distinguished from "the ledger could not be asked" (sys_migration not registered yet, or the query threw). Both keep the gate closed, only the former is remembered, so one unlucky early write no longer freezes a whole boot's posture. The `kernel:bootstrapped` advisory reads the ledger directly for the same reason. Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny Co-authored-by: Claude <noreply@anthropic.com>
1 parent 25784cf commit c44dd5e

9 files changed

Lines changed: 1005 additions & 37 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
fix(objectql,platform-objects): 一次启动不能证明它自己随即违反的契约 —— ADR-0104 空库自证改为在本次启动写完数据后下结论 (#4769)
7+
8+
一个全新部署第一次 `pnpm dev` 全绿(130 rows,0 ERROR),**第二次启动开始永久 10 条
9+
ERROR**、10 条种子记录写不进去。数据没变、代码没变,只是重启了一次;被拒的正是首启
10+
自己写进去的数据。
11+
12+
根因不是哪个值算错了,是**顺序反了**`sys_migration` 里那两行
13+
(`adr-0104-file-references` / `adr-0104-value-shapes`)带着
14+
`{"attested":"datastore-created-empty"}` 写在 `kernel:ready`,而同一次启动的 seed
15+
还在往里写行。「空库 ⇒ 没有历史值」这个推理成立的前提是**没有数据可写**,而它恰恰
16+
写在即将写入 130 行之前 —— 证明落笔那一刻是真的,一秒之后就不是了。于是首启在
17+
warn-first 下把数据留下,之后每一次启动读到这张证书、进入 strict、拒掉前任写下的
18+
那批行。
19+
20+
## 改了什么
21+
22+
**证书必须覆盖它所声称的那批数据。**
23+
24+
- **写入时机**:新库自证改为在**本次启动自己的数据落定之后**进行 ——
25+
`app:seeded`(inline seed 结算点,含超出 `OS_INLINE_SEED_BUDGET_MS` 后台跑完的
26+
那一半),不 seed 的 kernel 仍由 `kernel:ready` 兜底。两条路径进的是同一个幂等
27+
调用。
28+
- **写入前提**:`attestFreshDatastore` 先问引擎「这次启动放行过违反该契约的值吗」。
29+
引擎在 warn-first 放行每一个不合形状的值时,用**与 strict 模式完全相同的判定**
30+
它记下来 —— 证明干净需要扫全库,证伪只需要一个反例,而这个反例写路径已经算出来
31+
了。任一条被本次启动证伪的迁移 id **不再自证**,部署维持 warn-first(真实且可
32+
恢复),并在日志里指名是哪个 `对象.字段` 让这道闸没关上、该跑哪条 `os migrate`
33+
两行一起改:`adr-0104-file-references``adr-0104-value-shapes` 各自独立判定,
34+
一个 `cover` 不合形状不牵连 `location`,反之亦然。
35+
- **写入之后**:证书若在签发之后被本次启动推翻(操作员显式开了
36+
`OS_ALLOW_LAX_MEDIA_VALUES` / `OS_ALLOW_LAX_VALUE_SHAPES`,或后台 seed 收尾晚于
37+
签发),引擎**撤销**它 —— `verified_at` 清空、`blocking` 记上、`details` 保留原
38+
`attested` 并补一条 `revoked`。只针对**本次启动亲手创建的库**上的自证行:扫过全
39+
库的真实迁移证据不会被一次写入的观察推翻。
40+
41+
**记忆化的第二张脸也一并修了。** 首启之所以「看起来是绿的」,一半靠的是进程内正好
42+
缓存了 `false``sys_migration` 在 kernel init 期间才注册,而第一条写可能赶在它之
43+
前 —— 那次读根本没读到账本,却被当成结论冻结了一整个进程的姿态。现在区分两种否定:
44+
**问过了、账本说不**(结论,照旧缓存)与**根本问不到**(未注册 / 查询抛错 —— 依旧
45+
`false`,闸依旧关着,但不记住,下一次写再问一次)。代价是账本存在之前每次写多一
46+
次 registry 查表(在任何查询之前就短路),账本可读之后即止。
47+
48+
启动横幅那条 ADR-0104 建议行(`kernel:bootstrapped`)也改为直接读账本而非读记忆化
49+
结果 —— 否则一个刚刚自证成功的新部署会被告知去跑一条已经不需要跑的迁移。
50+
51+
## 对既有部署的影响
52+
53+
- 数据本来就合规的新部署:行为不变,照旧 born-migrated,启动即 strict。
54+
- 种子数据不合规的新部署:**不再**发出那张假证书。首启与之后每一次启动一致地停在
55+
warn-first,并且每次都告诉你是哪一个值、跑哪条命令。数据本身该怎么修还是怎么修
56+
(showcase 的 `cover` 种子值在 #4774 单独跟踪)。
57+
- 已经跑过 `os migrate … --apply` 的部署:完全不受影响 —— 扫描得来的证据不经由本
58+
次改动的任何路径改写。

0 commit comments

Comments
 (0)