从 #4471 拆出的独立事项(#4471 的正文里明确说这条"值得单独定一条",#4631 只做了声明契约的强制,这条未实现)。 ## 问题 当一个 best-effort 降级的后果是**静默丢数据/丢持久性**——系统看起来正常运行,实际上写入不落盘——它就不该记 `warn`。#4420 正是这个形态:durable suspended-run store 挂在不存在的表上,每次写失败进 warn,没人读,重启丢光在途审批。#4460 在 service-automation 这一处把它提到了 `error`,但只是点状修复,没有形成可依循的规则。 ## 期望 定一条日志级别约定(建议落在 AGENTS.md 或相关 ADR 的 addendum): - 降级只影响**功能可用性**(界面缺一块、某能力不开)→ `warn`/`info` 合理; - 降级影响**持久性/数据一致性**(声称持久化的东西实际不持久、写入静默失败)→ 必须 `error`,且首次降级时一次性说清后果与修复动作(参考 #4460 后 `service-automation/src/plugin.ts` start() 里的 error 文案)。 可以顺带盘点现有 `catch { logger.warn(...) }` 形态里还有哪些属于第二类(#4631 的 `check:init-service-contract --list` 输出可作为盘点起点——init 期 best-effort 消费 `manifest` 注册 sys 表的插件都是候选)。 ## 参考 - #4471(根因模式)、#4420(事故)、#4460(点状修复)、#4631(声明契约强制,不含本条)
Activity
os-zhuang commentedon Aug 3, 2026
🔒 认领 —— 定规则 + 盘点,但盘点范围本轮收窄
session_015Br2xsJsczFsTR9bvbh2Ny(PM 循环,2026-08-03 轮)claude/issue-4632-durability-degradation-log-levelobjectstack-issue-4632assignee无法区分认领人。若你读到本条且你的 session ID 不是上面那个,本单已被占,请另选。PM 分诊:这不是「待拍板」,是「把已经说清的规则写下来」
正文已经把规则完整表述了(功能可用性降级 →
warn;持久性/数据一致性降级 →error+ 首次一次性说清后果与修复动作)。这属于 restore-invariant 类,不上报维护者,直接做。交付物有两半,第二半才是重点
第一半:规则落成文字。 落在 AGENTS.md(或相关 ADR 的 addendum —— 你判断哪个锚点更对,在 PR 里说明理由)。文案要能被 AI agent 当判据用:给出判定问句而不是形容词。建议形状:
第二半:盘点并修。 已有的
catch { logger.warn(...) }里哪些属于第二类。PM 已实测候选面(origin/main@203a4494d,packages/*/src/plugin.ts含logger.warn):#4631的check:init-service-contract --list输出是更完整的起点 —— init 期 best-effort 消费manifest注册 sys 表的插件都是候选。逐个判断,不要批量替换 —— 把功能性降级误提到error会制造噪音,那是这条规则的反面失败。本轮硬约束:两个包排除在盘点之外
packages/metadata/**与packages/metadata-protocol/**—— 本轮sys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 正在这两个包里改写入路径,同批次不能撞。若你在这两个包里发现了第二类降级,单开 issue 记录(带文件行号与后果),不要在本单修。packages/spec/**零改动(spec 整包归另一个 PM 会话)。content/docs/releases/。有牙比有文档重要
一条只写在 AGENTS.md 里、没有任何东西检查的约定,就是本仓反复修的那个「声明 ≠ 强制」形状 —— 用一条规则去修一个「声明未强制」的病,自己却只是声明,这很讽刺。请认真评估能否给它加一道机械检查(哪怕很窄):比如对已知的持久化 store 初始化路径,
catch里出现logger.warn即失败的 lint/gate。做不到就在 PR 里明说为什么做不到(判定需要语义理解、误报率不可接受,等等),那是可接受的结论;⛔ 不可接受的是不提这件事。若你判断这道闸门值得做但超出本单体量,单开 issue。
验收
error的每一处,文案说清后果与修复动作(照service-automation/src/plugin.tsstart() 的范本)Generated by Claude Code
os-zhuang commentedon Aug 3, 2026
✅ PM 验收通过 —— PR #4730,CI 全绿(19 success + 2 skipped,0 失败),已转 ready 并入合并队列
复核对着分支与 CI:
packages/spec/**零改动、content/docs/releases/**零改动、冻结包(metadata*)零改动 ✓;changeset 在位且 Check Changeset 绿 ✓catch当场适用的创作判据,不是子系统架构决策check:durability-log-level接入 lint.yml 必需 job;AST 走块结构;catch侧跨同文件 helper 追踪(实测逼出来的 —— 抽 helper 立刻被闸门抓成静默吞掉,不追踪的话「藏进间接层」就成了让闸门闭嘴的捷径,那正是 [automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420 形状);baseline 手写、shrink-only、故意不给--update;10 例 self-test 双向钉ObjectQL.syncSchemas()空catch {}—— 唯二调用方随后就往失败意味着不存在的表里 INSERT 并报安装成功);20+ 处判为第一类的逐条留了不改的理由,含最接近边界的 analytics getReadScope(安全域而非持久性域,dev 守住了维护者原文的边界没擅自扩)记入验收的两条判断
冻结包发现 #4728 / #4729 已在队列,#4556(PR #4742)落地后即可派。
Generated by Claude Code
fix(convention): 持久性降级记 error 而非 warn —— 把 objectstack-ai#4460 定成规则,并…
fix(security): permission-set 投影只写 spec 认的键;失败的 backfill 变响亮 (objects…
fix(automation,approvals): 节点类型校验推迟到插件词汇表封闭之后 —— approval flow 不再被误报「…
fix(metadata): sys_metadata 的 DDL 失败必须响亮,只静默「表已存在」一种 (objectstack-ai#…
feat(devx): gate the "startup registry verdict" class — read the regi…
fix(metadata-protocol): seed-loader 中「计为错误」的失败一律记 error,不再自相矛盾地记 warn (…
fix(seed-loader): a roll-up summary left stale by a seed is loud and …
fix(metadata-protocol): log the seed record dropped for an unresolvab…
fix(metadata): DatabaseLoader 的读故障不再被吞成「什么都没声明」 (objectstack-ai#5108) (…
fix(runtime,tooling): add saveMetaItem to the durability vocabulary a…
fix(metadata-protocol): report the pass-2 back-fill dropped when the …
fix(service-automation): 启动路径三条日志的外来 cause 改走结构化 meta (objectstack-ai…
fix(service-automation): route the three remaining suspended-run read…
fix(automation): report a subflow parent stranded by a failed up-bubb…
fix(automation): report a subflow parent stranded by a failed up-bubb…
fix(service-automation): a resumed child's refusal rolls up on both l…
fix(objectql): runtime syncSchemas() binds federated objects instead …