audit/revise 覆写 state-degraded 章节的 status 却保留 reviewNote,导致该章永久无法 repair-state(并可能让下一章静默跳过结算)
环境
@actalk/inkos1.8.0,Node v24.14.0,Linux- 受影响数据:
chapters/index.json里两个章节同时满足status === "ready-for-review"reviewNote={"kind":"state-degraded","baseStatus":"ready-for-review","injectedIssues":[…]}
现象
这两章都是正常降级过的(reviewNote 就是 InkOS 自己写的),但它们无法用官方命令修复:
$ inkos write repair-state <book> 14
Chapter 14 is not state-degraded. # ← 但它明明是而 repair-state 的准入恰好是(dist/pipeline/runner.js:1778-1779):
if (targetMeta.status !== "state-degraded") {
throw new Error(`Chapter ${targetChapter} is not state-degraded.`);
}根因:降级写入是对的,后续的 audit 把 status 覆写掉了,却留下 reviewNote
① 降级写入路径正确设置了 status = "state-degraded"
dist/pipeline/runner.js:1691
const resolvedStatus = chapterStatus ?? (auditResult.passed ? "ready-for-review" : "audit-failed");:1695 status: resolvedStatus,(传入 persistChapterArtifacts)
dist/pipeline/chapter-persistence.js:12 / 18-20
status: params.status,
reviewNote: params.status === "state-degraded"
? buildStateDegradedReviewNote(params.auditResult.passed ? "ready-for-review" : "audit-failed", params.degradedIssues)
: undefined,dist/pipeline/chapter-truth-validation.js:26 / :71 → chapterStatus: "state-degraded" / chapterStatus = "state-degraded";
即:reviewNote 只在 params.status === "state-degraded" 时写入,所以同一 entry 的 status 必然就是 "state-degraded"。 上游单测也钉死了这一点:packages/core/src/__tests__/pipeline-runner.test.ts:2651,2656(result.status 与 index[0].status 均为 state-degraded)、:2798,2799(auditIssues 必含注入 issue)。全 dist 只有一个 persistChapterArtifacts 调用点(runner.js:1692),不存在第二条降级写入路径。
② 但审计会覆写 status,而 ...ch 展开保留了 reviewNote
dist/pipeline/runner.js:862-869(auditDraft):
const updated = index.map((ch) => ch.number === targetChapter
? { ...ch, // ← 保留 reviewNote
status: (result.passed ? "ready-for-review" : "audit-failed"),
updatedAt: new Date().toISOString(),
auditIssues: result.issues.map(...) }
: ch);没有 state-degraded 守卫。reviseChapter 同样(runner.js:1171-1183),commands/review.js:104 的 approve 也覆写 status 并保留 reviewNote。
于是产生搁浅记录:status 被改回可放行值,reviewNote 仍在 → repair-state 永久进不去。
③ 数据证据(决定性)
在受影响的两条记录上程序化核对:reviewNote.injectedIssues 与 auditIssues 交集为 0。
而降级写入时 auditIssues 必然包含注入 issue(上游单测 :2799 断言)。→ 说明 auditIssues(与 status 出自同一条语句)是被后续某次重审整批替换的。
可以排除其它可能:reviseChapter(该章未被标 needs-revision)、resync(成功时会清 reviewNote,见 runner.js:1955-1968)、review approve(status 会变 approved)。auditDraft 是唯一自洽解释。
隔离复现(stub 掉 LLM,只跑真实索引更新逻辑):
BEFORE: status=state-degraded, reviewNote={...}, auditIssues=["[warning] fresh audit issue"]
AFTER : status=ready-for-review, reviewNote 原样保留, auditIssues=["[info] stub audit issue"]精确复刻了受影响数据的形态。
后果(比"修不了"更严重)
- 该章永久无法用
repair-state修复——准入只看status。 - 下一章的结算可能被静默跳过:
assertNoPendingStateRepair(runner.js:2503)会因为这个被改回的status而放行,于是"正文重审通过"被当成"状态已结算"。本项目就出现过这个后果——快照只到0..13而章节已到 15,逐章摘要缺失。 - 要恢复只能靠外部手段:放宽准入、或手工把
status改回state-degraded(本项目采用的临时解,repair-state随后正常读取baseStatus/injectedIssues完成修复,见runner.js:1840-1848)。
复现
# 1) 让某章进入降级:结算校验失败且重试后仍有 warning → status=state-degraded + reviewNote
# 2) 对该章跑一次普通审计
inkos audit <book> <n> --json
# 3) 观察 chapters/index.json:
# status 变回 ready-for-review/audit-failed,而 reviewNote 仍在
# 4) 再尝试修复
inkos write repair-state <book> <n>
# → Chapter <n> is not state-degraded.(另注:inkos write sync <book> <n> 不要求 state-degraded,但受 latest-only 限制,只能在目标章是最新章时使用;Studio 的 /resync/:chapter 同理。)
建议修复
1)止新:审计/修订不应降级已降级的章节
dist/pipeline/runner.js:865(TS pipeline/runner.ts:1317-1325)与 :1176:
status: ch.status === "state-degraded"
? "state-degraded"
: (result.passed ? "ready-for-review" : "audit-failed"),或至少在覆写时 reviewNote: undefined。
理由:正文重审通过 ≠ truth-state 已结算。改成 ready-for-review 会让 assertNoPendingStateRepair(runner.js:2503)放行下一章,从而静默丢掉状态结算。
2)兼容存量:准入放宽到"或 reviewNote 声明了降级"
dist/pipeline/runner.js:1778(TS runner.ts:2404):
if (targetMeta.status !== "state-degraded"
&& parseStateDegradedReviewNote(targetMeta.reviewNote) === null) {
throw new Error(`Chapter ${targetChapter} is not state-degraded.`);
}理由:修复体(_repairChapterStateLocked)本来就从 reviewNote 还原 baseStatus/injectedIssues 并在成功后清空它(runner.js:1840-1848),所以放宽准入天然兼容存量坏数据,修完即自愈。
⚠️ 两处建议应当一起做。只做 2 不做 1,新的搁浅记录会持续产生。
附:修正说明
我最初提交前提交过一版分析,认为"降级时忘记写 status"。那是错的——降级写入路径是对的(见 ① 与上游单测)。真正的问题是 ② 的覆写。特此更正,避免误导。
Source: Narcooo/inkos