发现于 #5186(为「读接缝把故障答成空值」这一族新增闸门)实施期,由新规则在 `packages/objectql` 扫出;**不在该单文件面内**(#5186 是纯 `scripts/` 闸门单,不改被扫包),故单独立卡并已作为 baseline 条目记录在 `scripts/durability-read-invention.baseline.json`。 ## 现象 `packages/objectql/src/engine.ts` 的 `seedAutonumber()`(约 2051–2087 行,以 origin/main 为准): ```ts private async seedAutonumber(object, field, prefix, execCtx?): Promise< number > { try { const rows = await this.find(object, { fields: ['id', field], limit: 5000, context: execCtx } as any); let max = 0; // … 从已有值里取出最大计数 … return max; } catch { return 0; } } ``` `catch` 里:**没有任何日志、没有 rethrow、没有按错误类型区分**,直接编了一个 `0` 出来。 ## 为什么这是 #4825 那一半(写进去的数是错的) 这是「读接缝把故障答成空值」家族里**最贵**的形状,和 #4825 的 `nextEventSeq` 的 `catch { return 1 }` 完全同构: - 表里已经有 N 行、自增号已经发到 `PRE-000123`; - 一次连接抖动 / 超时 / 权限失败让这次 `find` 抛出; - `catch` 答 `0`,计数器从头开始,下一批自增号**与已存在的行相撞**; - 插入**成功**,一行日志都没有,`success` 与行计数全部读起来正常。 和「字节没落盘」不同,这是**落盘的值是错的**——重试不修,重启不修。 值得一提的是:**这个风险 issue 代码里已经写着**。同一个 `try` 块上方的注释(#4371 的修复注释)原文就说: > the catch below would have swallowed the guard's rejection into "seed from 0", i.e. duplicate autonumbers. 即:读那半边在 #4371 修好了,`catch` 这半边留在原地。 ## 修法(与 #4825 / #5108 同一处方) 按错误类型区分,只在良性分支返回零值: ```ts import { isMissingTableError } from '@objectstack/metadata/errors'; } catch (error) { // 良性且仅良性:表尚未 provision,确实没有行,从 0 起号不会撞任何东西 if (isMissingTableError(error)) return 0; throw error; // 其余一律上抛:调用方不得用没读到的数据推算号段 } ``` ⚠️ 需要确认的落点问题(实现时判断,不在本卡预判):`@objectstack/objectql` 依赖 `@objectstack/metadata` 是否成立/是否会成环。若不成立,`packages/metadata/src/errors.ts` 的头注释已经把三条路线写清楚了(其中方案 2「沉到公共依赖 `@objectstack/types` / `@objectstack/spec/shared`」当时只是被判为「那一轮 out of scope」,并未被否决)——这正是需要该方案的那个调用方。 ## 验收 - `seedAutonumber` 的 `catch` 只在 `isMissingTableError` 为真时返回 0,其余上抛; - 有测试钉住:表未建 → 从 0 起号且不抛;驱动报连接错误 → 上抛,不发号; - 修好后删除 `scripts/durability-read-invention.baseline.json` 里的 `packages/objectql/src/engine.ts::seedAutonumber` 条目(该 baseline 是 shrink-only,条目失效即红)。 ## 关联 #5186(新增本族闸门,本卡由它扫出)、#4825(同族、同形状,history `event_seq`)、#5108(同族,`DatabaseLoader` 五处读)、#4728(同族起点,DDL 侧)、#4632(规则)、#4371(同一个 `try` 里读那半边的修复,其注释预言了本卡)、AGENTS.md「Degradation log levels」「Absence must be loud」。
Activity
claude commentedon Aug 6, 2026
分诊:
pm:queue(可派发缺陷,本批最高严重度)+ 维持domain:engine-core。bug+domain:engine-core但无 pm-state 标签,对所有队列视图不可见。过时前提检查(
origin/main9e3709a)—— 逐行实读,全部成立packages/objectql/src/engine.ts:2046seedAutonumber()在,结尾} catch { return 0; }原样未改(实读:2085-2087);:2032if (next == null) next = await this.seedAutonumber(...)在 ⇒ 这条路径是活的,不是死代码;sort/select/skip/populate— declared query contract, zero enforcement #4371 注释仍在同一个try上方(实读:2057区段的 "the catch below would have swallowed the guard's rejection into 'seed from 0'")——packages/objectql/src/engine.ts::seedAutonumber仍在(durability-read-invention.baseline.json:49),shrink-only ⇒ 验收判据机械。为什么入队(且建议优先于 #5980):这是「读接缝把故障答成空值」家族里唯一会把错值落盘的一支 —— 其余同族(#5108 / #5532 / #5980)的后果是「答案是空的」,可由重试自愈;本单的后果是自增号从 1 重发、与既有行相撞并插入成功,零日志、
success与行计数读起来全部正常。重试不修、重启不修。落点/修法/验收三项齐备,无产品裁决成分。域锚定:落点
packages/objectql/src/engine.ts⇒domain:engine-core。isMissingTableError,而@objectstack/objectql依赖@objectstack/metadata是否成环需实测。packages/metadata/src/errors.ts头注释已列三条路线,其中「沉到公共依赖」当时只是被判为 out of scope、并未否决。这是实现期的技术判断(dev 可解),不是维护者决策 ⇒ 不因此转决策箱。串行约束(硬,派发时必读):
packages/objectql/src/engine.ts本轮是全仓最热的文件之一 ——delete()的wantsPreImage门)⇒ 两单 ⛔ 不得同轮派发,严格串行(SKILL step 3 同文件纪律)。查重:三仓 open issue / PR 各搜一遍(
seedAutonumber/ autonumber 相撞 / catch-return-0)—— 同族 #4825(nextEventSeq的catch { return 1 },同构)、#5108、#4728 均已收官;本单是活体实例,入口唯一,objectui / cloud 无影子单。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
baozhoutao commentedon Aug 7, 2026
认领(执行席 PM,engine-core 车道,第 8 轮)
session_019Q7oc7ASjh8yxyS3Yz78Weclaude/issue-5979-seed-autonumber-outage../objectstack-issue-5979(worktree-first)packages/objectql/src/engine.ts(仅seedAutonumber的catch)+ 新增测试 +scripts/durability-read-invention.baseline.json摘条目;isMissingTableError引入路线按packages/metadata/src/errors.ts头注释三路线实测决定,可能触packages/objectql/package.json或公共依赖包遵守分诊串行约束:PR #6013(同文件)已 MERGED,开工即以
origin/main为基;⛔ #5929(同文件delete()门)不同轮、本单不触其区域。Generated by Claude Code