Repository navigation
Description
发现自 #4556 的实现过程(PR #4742)跑真实 CLI 时。未认领。与 #4556 无关,任何 bootSchemaStack 系的子命令都能复现。
复现
cd examples/app-crm
OS_DATABASE_URL="file:/tmp/t.db" node packages/cli/bin/run.js migrate recorded-by --json命令正常返回 JSON、退出码正确。但在返回之后、进程关停时,稳定打出:
ERROR Find operation failed {"object":"sys_metadata","error":{"message":"Unable to acquire a connection", …}}
WARN [integrity] dangling-reference audit could not list an object {"object":"sys_metadata","error":"Unable to acquire a connection"}
ERROR Find operation failed {"object":"sys_view_definition","error":{"message":"Unable to acquire a connection", …}}
WARN [integrity] dangling-reference audit could not list an object {"object":"sys_view_definition","error":"Unable to acquire a connection"}
WARN [integrity] stored references that resolve to nothing (#4551)
{"scanned":0,"dangling":0,"undetermined":0,
"unreadableObjects":["sys_metadata","sys_view_definition"],
"truncatedObjects":[],"references":[]}
即 #4551 的巡检在 stack shutdown 之后才跑到这两个对象,此时 knex 连接池已经关了。
为什么值得修,而不只是噪音
巡检自己的文档把这件事写成了它最在意的一条设计原则:
Unknown and absent are DIFFERENT answers
The single most important property here. An existence probe that cannot run … must never be recorded as "the target does not exist" … Together they are what stops "0 dangling" from ever being read as "everything is fine".
unreadableObjects 这个桶存在的意义,正是让「我没能检查」区别于「我检查了,没问题」。而现在它在每一次正常的 CLI 调用里都非空 —— 一个恒定为真的告警不再携带任何信息,它训练操作者忽略这个桶。等到真有一个对象因为数据源故障读不出来时,那条信息会淹没在同样的两行里。
这也正是 AGENTS.md「Route & surface ownership」第 3 条要防的形态:
3. Absence must be loud. … a verifier that silently degrades (reusing a stale build, skipping a check it could not run) is worse than no verifier, because it reports success.
这里是它的镜像:一个永远报告自己降级了的巡检,与不报告一样没有信息量。
另外那两行 ERROR Find operation failed 是引擎层打的,级别是 ERROR,出现在一条成功的命令输出里 —— 对 CI 日志抓 ERROR 的流水线是直接的误报源。
可能的方向(未验证,需要看巡检的触发时机)
- A. 巡检在 shutdown 之前跑完,或在 shutdown 时跳过。 一次性 CLI 进程本来就不该在关停路径上做完整巡检 —— 它的定位是启动期/巡检期的可观测性,不是退出钩子。
- B. 关停中的连接失败不进
unreadableObjects。 把「引擎正在关停」与「数据源不可达」区分开:前者不是一个发现,不该占用那个桶。 - C. 一次性命令进程默认不跑这个巡检(由
os serve/ 巡检专用命令承担)。
倾向 A + B:A 解决时序,B 保证即使时序再次被打破,这个桶也不会被非发现污染。C 单独用的话会让巡检在 CLI 场景永久失明,与它的存在意义相悖。
边界
- 不涉及
sys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 的recorded_by改动 —— 在那条 PR 之前就存在,之后也照旧。 - 与 哨兵拆掉之后:#4441 的
readonly收窄与 #4551 的巡检跳过还需要吗?——注释已过期,跳过范围值得收窄 #4743(豁免范围是否收窄)是不同的问题:那条问「该不该查这些字段」,这条问「查的时候连接还在不在」。
Activity
os-zhuang commentedon Aug 3, 2026
认领:PM 循环第 4 轮
会话:
session_015Br2xsJsczFsTR9bvbh2Ny分支:
claude/issue-4747-integrity-audit-shutdownWorktree:
objectstack-issue-4747分诊:落地 objectstack(
packages/objectql/src/integrity/+src/lifecycle/);采纳 issue 的 A + B,无需维护者裁定。这属于恢复不变量类 —— 巡检自己的文档已经把规则写死了:
而现在
unreadableObjects在每一次正常 CLI 调用里都非空。一个恒为真的告警不携带任何信息,它训练操作者忽略这个桶 —— 等真有对象因数据源故障读不出来时,那条信息会淹没在同样的两行里。规则已经在仓里,现状违反了它,修它不需要谁批准。取 A + B 的理由:A(巡检跑在 shutdown 之前 / 关停时跳过)修时序;B(关停中的连接失败不进
unreadableObjects)保证即使时序再次被打破,这个桶也不会被非发现污染。单独的 C(一次性命令不跑巡检)不取 —— 那会让巡检在 CLI 场景永久失明,与它的存在意义相悖,issue 作者的判断是对的。一并处理:那两行
ERROR Find operation failed出现在一条成功命令的输出里,对抓 ERROR 的 CI 流水线是直接误报源,修完应当消失。packages/plugins/plugin-auth与 9 处scripts/i18n-extract.config.ts。另外 PR #4794 正在合并队列中,同样改packages/objectql(但落点是src/validation/与engine.ts,与本条的src/integrity/+src/lifecycle/不同文件)。请从最新 main 建分支;若 #4794 在你工作期间落地,rebase 一次即可。若你发现必须改engine.ts或src/validation/**,停下来在报告里说明。顺带一提:本条与 #4776(启动期判断跑在提供方注册之前)是同一族的镜像面 —— 那边是"判断下得太早",这边是"判断下得太晚(依赖已经拆了)"。共同点是检查与它依赖的资源之间没有明确的生命周期契约。
Generated by Claude Code
os-zhuang commentedon Aug 3, 2026
PM 复核:ACCEPT(待 changeset 修正 + CI 全绿) —— PR #4815。
根因比 issue 的猜测深一层,而且证据确凿
issue 猜的是「巡检在 shutdown 之后才跑到这两个对象」。dev 挖出来的事实是:shutdown 根本没发生过 —— 两处静默空转叠在一起:
ObjectQLPlugin把关停逻辑写在stop()里,而内核的插件契约是init/start/destroy,stop()从来没有被任何人调用过 —— 这件事仓里甚至写明过(DefaultDatasourcePlugin.destroy的注释原话:「stop()exists nowhere in the Plugin contract and is never called」)。bootSchemaStack().shutdown()调的是(runtime as any).stop?.(),而Runtime没有stop—— 一个强制转型加一个可选调用,把「没有关停」伪装成了「关停过了」。第二条尤其值得记住:
as any+?.的组合让一个从来不可能工作的调用安静地通过了所有检查。这与今晚反复出现的形状同族 —— 声明了做某件事的代码,实际什么都没做,而没有任何信号。A + B 都按裁定落地,B 的实现方式是对的
stop→destroy;shutdown()改走kernel.shutdown()—— 与os serve收 SIGTERM 同一条路径,一次性命令和服务器只有一条出口。signal),dev 明确拒绝了对Unable to acquire a connection做消息嗅探 —— 那会把一个错误文案变成事实上的第二份契约,换个 driver 就失灵(PD Add comprehensive test suite for Zod schema validation #12)。aborted是独立字段而不是unreadableObjects里的一条:「没人问」不是「数据源拒绝了」。不完整依然是响的(报告永远不会被读成 clean bill of health),但不占用只该装发现的桶。且aborted: false是显式的,消费者不用猜undefined是「跑完了」还是「旧报告形状」。测试把「修之前无法区分的两种情况」钉成了一对
开头那对是全部要义:同一段抛错代码 —— 没有 abort ⇒ 进桶、warn 照发;被 teardown race ⇒ 丢弃、
aborted: true、不发 warn。删掉 abort 处理第二条红,删掉进桶逻辑第一条红。另有:abort 在读之前 ⇒ 一次查询都不发(这正是ERROR Find operation failed消失的机制);中途 abort ⇒ 已证成的 dangling 保留;探针在关停下抛错不算undetermined(同一个范畴错误,隔壁桶)。集成测试在真实
bootSchemaStack上把两头都断言了,最后一条故意证明沉默不是空转:shutdown 后直接对引擎find会 reject,说明池确实关了,上面的安静是修复的结果而不是「本来就没东西可读」。验收第三条(巡检在 CLI 场景仍然运行)守住了 —— 没有「一次性命令不跑巡检」的开关,只有「引擎活着才读」的生命周期边界。被拒绝的选项 C 没有从后门溜进来。
一处要改:changeset 定级
@objectstack/objectql标了patch,但本 PR 给它新增了公共 API(AuditAbortSignal从包根导出、DanglingReferenceAuditOptions.signal、DanglingReferenceReport.aborted)。按 #4791 的先例(service-automation 因新增公共 API 标 minor),应为minor;@objectstack/cli保持patch。已通知 dev 修正,一行改动。超范围产出
pm:queue)—— 内核的 init/start 超时守卫定时器从不清除也不 unref,每个 CLI 进程干完活还要空转 ~120 秒。它正是本 issue 的使能条件:一个 3 秒就该退出的进程,被 8 根带 ref 的定时器钉到 120 秒,才活到 60 秒巡检定时器触发。带 probe 证据、修法定位到隔壁 30 行已有的正确写法(shutdown()自己的守卫连注释都写好了),质量很高。PluginContext暴露内核级 teardown 信号)已记入,归 [决策] 启动期「提供方尚未注册」与「根本没有提供方」在注册表里是同一个值 —— 一次 showcase 冷启暴露 3 个同形缺陷,是否收紧 kernel 注册表契约 #4776 裁定时一并考虑,本 PR 正确地没有自行推广。changeset 修正推上、CI 全绿后送合并队列。
Generated by Claude Code
fix(objectql,cli): 巡检随引擎一起停 —— `unreadableObjects` 不再被关停污染 (objectsta…
feat(objectql): audit readonly references into their own `provenance`…
fix(objectql): bound every lifecycle reap, not just the guarded ones (o…
fix(objectql): name the objects a finite row budget never reached (ob…
fix(objectql): archive 冷侧 keep prune 尊重 objectstack-ai#4747 的 teardow…
fix(objectql): a failed `find` reports at `warn`, not `error` — the r…
test(cli): the #4747 teardown pin holds on both compositions
fix(cli,runtime): one-shot CLI boots run no seed loader and arm no li…
feat(metadata-core,metadata-protocol,metadata,platform-objects,spec)!…