Explorar el Código

docs(v7): M5.5 spec 回填——spec 0.11(决策 35 缺时间锚点)/PRD 1.2/计划出口达成 + 任务工件

lingfengQAQ hace 19 horas
padre
commit
1e274b93cc

+ 2 - 0
.trellis/spec/backend/database-guidelines.md

@@ -20,6 +20,8 @@
 
 2.4 表名属机器域,用英文:`chapters`、`threads`、`secrets`、`entities`、`entity_aliases`、`fingerprints`。`entity_aliases` 是 `entities` 的别名索引表(alias → entity_id),供别名解析与唯一性校验。表设计必须支撑精准读取接口(PRD §3.6)的全部查询。
 
+2.5 **派生统计必须确定性可重算**(M5.5 起,`fingerprints` 行与 meta 统计 key 如 `imagery_top`):同一批源文件任何时候重算,结果必须逐字段一致——禁止时间戳、随机量、locale 相关排序(排序用码元序比较,禁止 `localeCompare`);对象序列化的键顺序必须由固定的构造顺序保证。"删缓存重算后指纹逐字段一致"是测试断言项。写入方约定:基线段与近段章段重合时只落基线行(同主键两次 upsert 会翻转 `is_baseline`,见 cache-design §1.5)。
+
 ## 3. 禁止事项
 
 3.1 禁止引入第二个数据库、向量库(语义检索为可选插件,永不做事实召回主路径)、常驻服务。

+ 2 - 0
.trellis/spec/backend/quality-guidelines.md

@@ -20,6 +20,8 @@
 
 2.3 **精准读取**:每类数据文件必须配"定位读到所需一段"的脚本接口;写作材料组装默认用片段,禁止默认整文件读取。
 
+2.4 **机检的阻断语义**:`pass = issues.length === 0` 是既有消费方依赖的契约。新增检查项默认走 `candidates` 通道(`{type, value, description}`,提醒不拦截);只有产品文档(PRD/spec)明确定为阻断项的才进 `issues`。禁止用 `blocking: false` 的 issue 表达提醒——那会让 pass 误判打回。跨章统计项由体检产出存缓存(meta/`fingerprints`),机检只消费,无数据静默跳过,禁止在机检里做全书扫描。
+
 ## 3. 编码与平台(CI 强制)
 
 3.1 一切文件 IO 显式 UTF-8 无 BOM;禁止依赖系统 locale。

+ 1 - 0
.trellis/tasks/07-04-m5-5-checkup/check.jsonl

@@ -0,0 +1 @@
+{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

+ 104 - 0
.trellis/tasks/07-04-m5-5-checkup/design.md

@@ -0,0 +1,104 @@
+# M5.5 体检 design
+
+## 1. 总体结构:体检产出,机检/备料消费
+
+```
+runHealthCheck(v7/src/health-check/index.js,序 5 执行点,改造)
+  ├─ assembleBookStatus(既有,不动)
+  ├─ style-stats(新模块 v7/src/style-stats/index.js,纯函数零依赖)
+  │    ├─ splitSentences / splitParagraphs        分句分段
+  │    ├─ styleMetrics(text, exclude)             句长方差/段落分布/高频开头(单章与章段通用)
+  │    ├─ extractImagery(chapters, exclude)       跨章高频意象(Apriori n-gram)
+  │    └─ extractFingerprint(chapters, exclude)   指纹对象(内部复用上两者)
+  ├─ 缺时间锚点检查(chapters 表 + TimelineReader 比对)
+  ├─ fingerprints 表 upsert(基线段 + 滚动近段两行)
+  ├─ meta 写入:imagery_top JSON(新 key)+ last_health_check_chapter(既有)
+  └─ 体检报告.md 渲染(替换占位节)+ 结构化 data 返回(M6 对接面)
+
+消费方(均为小改):
+  - v7/src/prep/index.js:97          反复读清单 ← meta.imagery_top
+  - v7/src/mechanical-check/index.js 新增 2 个候选检查 ← meta.imagery_top / fingerprints 基线行
+  - v7/src/commands/report-style-drift.js ← fingerprints(补句长方差 delta)
+```
+
+体检是低频全量计算(每 50 章一次),机检是每章轻量消费——统计只在体检算一次,机检读缓存结果,零重复计算。
+
+## 2. 模块契约
+
+### 2.1 style-stats(新)
+
+纯函数、无 IO、无缓存依赖;全部固定排序纯计数,无时间戳无随机——AC3「删缓存重算逐字段一致」的根基。
+
+- `splitSentences(text)`:按 `/[。!?;…]+[”』」]?/` 切分,句 = 修剪后非空段;句长 = 去空白字符数
+- `splitParagraphs(text)`:按 `/\n+/` 切分修剪非空(网文一段一换行约定)
+- `styleMetrics(text, exclude)` → `{ 句数, 平均句长, 句长方差, 段落数, 平均段长, 段落分布, 高频开头 }`
+  - 段落分布:`{短(≤50字), 中(51-150), 长(151-300), 超长(>300)}` 各计数与占比
+  - 高频开头:句首 2 字聚合 top5 `[{开头, 次数, 占比}]`,排除名册专名/别名前缀("林晚…"是人名不是句式)
+- `extractImagery(chapters: [{num, text}], exclude: Set)` → `[{phrase, count, chapterCount}]`(全量,调用方取 top-N)
+  - 只在连续 CJK 字符段内取 n-gram(标点断开),n = 4..8
+  - **Apriori 分层**:先数 4-gram,全书次数 ≥ 阈值(10)的才扩展 5-gram,逐层到 8——内存有界(一把梭全长度 n-gram 在 300 万字级别内存爆炸)
+  - **排除**:短语包含任一名册专名/别名(长度 ≥2)即丢弃——人名动作短语("林晚冷笑一声")的重复是叙事常态不是意象复读,宁缺勿滥,漏报由编辑审兜底
+  - **跨章条件**:chapterCount ≥ 3(单章内猛复读归机检既有"复读"项管,不重复报)
+  - **最长优先去重**:按(长度 desc,次数 desc,字典序)稳定排序后逐个保留;候选短语是已保留短语的子串且次数 ≤ 父串次数 ×1.25 → 丢弃("气仿佛凝固"被"空气仿佛凝固"覆盖)
+  - 输出按(次数 desc,字典序 asc)稳定排序
+- `extractFingerprint(chapters, exclude)` → cache-design §1.5 五常用列 + `fingerprint_data` 完整 JSON(另含段落分布、高频开头、总字数、章数)
+  - `common_phrase_frequency`:本章段内 extractImagery(chapterCount 条件放宽为 ≥1,章段内统计)top10 `{phrase: count}`
+  - `vocabulary_richness`:滑动窗口 TTR——剥标点空白后按 1000 字窗口求 unique/窗长 的平均(末窗不足用实际长度);确定性且跨章段可比(朴素 unique/total 对文本长度敏感,不可比)
+
+### 2.2 体检编排(health-check/index.js 改造)
+
+- 配置:BookConfigReader 读 `文体基线起/止`(默认 1/30)、`体检周期`(默认 50)
+- 排除表:`SELECT id FROM entities` + `SELECT alias FROM entity_aliases`(同机检 checkNewProperNouns 取法,index.js:111-118)
+- 正文来源:chapters 表 `file_path` 逐章读定稿文件,`parseFrontMatter` 剥头取正文
+- 计算与落盘:
+  - 意象:全书章 → `extractImagery` → meta `imagery_top` = JSON top20
+  - **近段窗口 = [max(1, maxChapter − 体检周期 + 1), maxChapter]**——只依赖书状态不依赖 meta:meta 丢失/缓存重建不改变体检输出(AC3 确定性;若按 lastCheck 起算则 meta 丢失时窗口漂移)
+  - 基线段 = [基线起, min(基线止, maxChapter)];maxChapter < 基线起 → 指纹节人话降级「章数不足基线区间,暂不对比」
+  - 两段各 `extractFingerprint` → `INSERT OR REPLACE INTO fingerprints`(基线行 is_baseline=1,近段行 =0);历史段行自然累积 = 指纹历史(cache-design §1.5 语义),同段重算覆盖
+  - 缺时间锚点两种缺法合并:chapters 表 `story_time` 空 → 缺「故事时间」;TimelineReader 读全部卷时间线表收集「章」列章号,定稿章不在其中 → 时间线漏行。报告列章号与缺因
+- 报告:占位节(index.js:48-49)替换为四节——`## 高频意象(跨章)`、`## 句式体检`、`## 文体指纹漂移`(基线 vs 近段各指标 + delta + 人话建议:回拉文风或改 book.yaml 基线区间,脚本不自动改)、`## 缺时间锚点`(全齐报「无」)
+- 返回:`{ ok, filePath, maxChapter, data, error }`,`data = { 高频意象, 句式, 指纹: {基线, 近段, delta} | null, 缺时间锚点, 悬了太久, 条目活跃率, 连续弱钩 }`——M6「体检不过线」阈值判定的对接面,本任务不判定
+- 降级诚实(E3):三统计与缺锚点各自 try/catch,单项失败该节写「该项计算失败:<人话原因>」,不炸整体;报告照落、meta 照记
+
+### 2.3 机检两候选(mechanical-check/index.js 追加)
+
+走 **candidates 通道**(`{type, value, description}`)——`pass = issues.length === 0`(index.js:41)语义零改动,既有测试不动:
+
+- `checkImageryHits(body, cache, candidates)`:读 meta `imagery_top`;无 → 静默跳过;草稿命中短语 → `{type:'高频意象', value:短语, description:'「空气仿佛凝固」全书已用 47 次,本章又用 2 次,建议换个写法'}`
+- `checkStyleDeviation(body, cache, candidates)`:读 fingerprints 基线行;无 → 静默跳过;本章 `styleMetrics` vs 基线,**平均句长偏离 ≥30% 或句长方差偏离 ≥50%**(硬编码默认,参数化已出 scope)→ `{type:'句式偏离', ...}` 人话描述偏了什么
+
+### 2.4 备料接通(prep/index.js:97 一处)
+
+读 meta `imagery_top`:有 → top10 行「-「空气仿佛凝固」全书 47 次(12 章出现),本章避免再用」;无 → 「(尚未体检,暂无数据——首次体检后自动填充)」
+
+### 2.5 report-style-drift 补齐
+
+delta 增加 `sentence_length_variance_delta`;命令契约与输出格式(JSON)不变。
+
+### 2.6 rebuilder 注释同步
+
+rebuilder.js:49 注释改「fingerprints 由体检按需重算(M5.5),重建不填」——行为不变。
+
+## 3. 数据与兼容
+
+- meta 新 key `imagery_top`:派生物,CacheManager 既有 meta 跨重建保留逻辑自动覆盖;丢失 → 备料/机检降级占位、下次体检重建——与「体检记录丢失重测无害」同一语义
+- SCHEMA_VERSION 不变(无表结构变更,只是开始往既有表/meta 写数据)
+- 机检输出仅 candidates 增型;`runHealthCheck` 返回值增 `data` 字段(既有消费方 router.test.js:190 只断 ok 与 meta 效果,增字段无破坏)
+- 运行时依赖零新增(字符 n-gram + 标点分句,无分词库;spec §2.2「仅 js-yaml」口径不破)
+
+## 4. 权衡记录
+
+- **机检句式对比 vs 基线指纹**(而非全书均值):基线是作者在 book.yaml 钦定的风格锚,偏离基线才是"AI 味漂移"信号;全书均值会被漂移本身污染
+- **意象排除"包含专名即丢"**(而非"等于才丢"):误报毒害报告信任,宁缺勿滥
+- **近段窗口滚动**(而非 lastCheck 分段):确定性只系于书状态(见 §2.2)
+- **candidates 而非 blocking:false issue**:后者会让 `pass = issues.length===0` 误判打回,除非改 pass 语义——不动契约是更小的爆炸半径
+
+### 4.1 实施期细化(07-04 落地时补)
+
+- **意象排除按"专名出现处切断字符段"实现**("包含即丢"的强化版):严格按含名后置过滤会漏掉跨名碎片——「林晚冷笑一声」滤掉后「晚冷笑一声」以同等次数存活,照样毒害报告;切断后含名短语与跨名碎片都不可能成为 n-gram,名字后的通用搭配(「冷笑一声」)保留可报。方向与"宁缺勿滥"一致。
+- **基线段与近段重合只落基线行**:全书尚在基线区间内时两段章段相同,同主键两次 upsert 会让后写的近段行翻转 `is_baseline`、基线丢失。约定重合时只写基线行,指纹节如实说明"重合暂无漂移可比"(写入约定已回填 cache-design §1.5)。
+- **报告与代码用真实字段名「书内时间」**:PRD/spec 原文的「故事时间」是转述,front matter 实际字段与 TimelineWriter 表头都是「书内时间」(`story_time` 列)。
+
+## 5. 回滚
+
+全部增量:一个新模块 + 五处既有文件小改,单 commit 粒度分期(见 implement.md),`git revert` 可整体回退;无数据迁移——meta key 与 fingerprints 行是派生物,回退后自然失效,无残留危害。

+ 1 - 0
.trellis/tasks/07-04-m5-5-checkup/implement.jsonl

@@ -0,0 +1 @@
+{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

+ 59 - 0
.trellis/tasks/07-04-m5-5-checkup/implement.md

@@ -0,0 +1,59 @@
+# M5.5 实施清单
+
+执行方式:inline(同 M1-M5 先例),implement.jsonl / check.jsonl 保持种子行,Phase 2 上下文走 trellis-before-dev。四期,每期一 commit,期末验证绿才进下期。
+
+## P1 style-stats 统计算法(纯函数)
+
+- [x] P1.1 `v7/src/style-stats/index.js`:splitSentences / splitParagraphs / styleMetrics / extractImagery / extractFingerprint(契约与算法口径见 design §2.1)
+- [x] P1.2 `v7/test/style-stats/index.test.js`:
+  - 分句边界:引号收尾(「…!」)、省略号、分号;空文本
+  - 方差/均值:小样本手算对照
+  - 意象:构造重复短语样章——阈值边界(9 次不报 10 次报)、专名排除(含"林晚"的短语被丢)、最长优先去重(子串被父串覆盖)、跨章条件(chapterCount<3 不出)
+  - TTR:窗口平均与朴素 TTR 的差异用长短两文本验证
+  - **确定性**:同输入两次调用结果深等(deepStrictEqual)
+
+验证:`node --test v7/test/style-stats/`
+提交:`feat(v7): M5.5 P1——style-stats 统计算法(分句/意象/指纹)`
+
+## P2 体检编排
+
+- [x] P2.1 `v7/src/health-check/index.js` 改造:排除表 / 正文读取剥 front matter / 全书意象→meta / 两段指纹 upsert / 缺时间锚点 / 报告四节替换占位 / 结构化 data 返回 / 单项 try-catch 降级(design §2.2)
+- [x] P2.2 fixture:构造带重复意象与缺锚点章的测试书仓库(复用 sample-book 结构,专用小 fixture 或临时目录搭建,随测试现建避免污染既有 fixture 断言)
+- [x] P2.3 `v7/test/health-check/index.test.js`:
+  - AC1 后半:报告高频意象节含构造短语与全书次数
+  - AC2 前半:报告含句长方差/段落分布/高频开头
+  - AC3:体检 → 断言 fingerprints 两行 → 删 `.cache` 全量重建 → 再体检 → 指纹行逐字段一致
+  - AC5:锚点齐全报「无」;缺「故事时间」与时间线漏行各能列出章号
+  - AC7:data 形状断言(各 key 存在且类型稳定)
+  - 降级:构造单项失败(如时间线目录不可读),报告照落、该节含失败说明
+- [x] P2.4 序 5 回归:`v7/test/state-machine/router.test.js` 不改仍绿(体检后 meta 更新、next 不再报体检)
+
+验证:`node --test v7/test/health-check/ v7/test/state-machine/`
+提交:`feat(v7): M5.5 P2——体检编排(意象入 meta/指纹入表/缺时间锚点/结构化返回)`
+
+## P3 消费方接通
+
+- [x] P3.1 `v7/src/mechanical-check/index.js`:checkImageryHits + checkStyleDeviation 两候选(design §2.3);`v7/test/mechanical-check/` 补用例——有数据命中出候选、无数据静默跳过、句式阈值边界(29% 不报 31% 报)、pass 判定不受候选影响
+- [x] P3.2 `v7/src/prep/index.js:97`:反复读清单接 meta(design §2.4);`v7/test/prep/` 补有/无 imagery_top 两态断言
+- [x] P3.3 `v7/src/commands/report-style-drift.js`:补 sentence_length_variance_delta;对应 `v7/test/commands/report-style-drift.test.js` 更新
+- [x] P3.4 `v7/src/cache/rebuilder.js:49` 注释同步(行为不变)
+
+验证:`node --test v7/test/mechanical-check/ v7/test/prep/ v7/test/commands/`
+提交:`feat(v7): M5.5 P3——机检候选、备料反复读清单、drift 方差 delta`
+
+## P4 收口
+
+- [x] P4.1 全量测试:`node --test v7/test/`(Windows 本机跑)——374/374 绿(原 346 + 新增 28)
+- [x] P4.2 AC1-AC7 逐条复核,prd.md 打勾;AC1 前半(机检报出)在 P3.1 用例覆盖处标注
+- [x] P4.3 spec 回填(Phase 3.3):
+  - story-repo-spec 0.10 → 0.11:§9 术语「时间线孤儿」→「缺时间锚点」+ 变更记录一条(决策 35);§9 体检定义与实现对齐复核(补高频意象/句式两节与"体检产出、机检消费"分工)
+  - v7-prd.md 1.1 → 1.2:§4 #2 验收用词同步替换
+  - v7-implementation-plan.md §M5.5 出口达成标注
+  - 追加:cache-design §1.5 补"基线段与近段重合只落基线行"写入约定;backend/database-guidelines 增 2.5(派生统计确定性)、backend/quality-guidelines 增 2.4(机检候选通道语义)
+- [ ] P4.4 commit + push,CI 双平台绿
+
+## 风险与回滚点
+
+- P2 触碰序 5 执行点——router.test.js 回归必跑;报告文件名/meta key(`last_health_check_chapter`)绝不改
+- 机检 `pass = issues.length===0` 语义绝不动,新增只进 candidates
+- 每期独立 commit,任一期出问题 `git revert` 该期即可,无跨期数据耦合

+ 84 - 0
.trellis/tasks/07-04-m5-5-checkup/prd.md

@@ -0,0 +1,84 @@
+# M5.5 体检里程碑:高频意象统计、句式体检、文体指纹
+
+## Goal
+
+体检从"最小占位"升级为 spec §9 完整定义:三个零 token 统计算法落地(跨章高频意象、句式体检、文体指纹+基线漂移),填充 `fingerprints` 表激活 `report-style-drift`,高频意象接通备料"反复读清单"占位,并为 M6 停止条件"体检不过线"提供结构化数据面。
+
+对应实施计划 0.3 §2「M5.5 体检」(`docs/architecture/v7-implementation-plan.md:157`),上游依据:`v7-prd.md` §4 #9(桥段循环)/#11(AI 味)/#2(时间线,缺锚点检查部分)、`story-repo-spec` 0.10 §3(体检周期/文体基线)/§8 第 5 步(机检统计项)/§9(体检定义)/§10 序 5、`cache-design-2026-06-26.md` §1.5(fingerprints 表)。
+
+**里程碑定位**:M6 硬前置——M6 停止条件"体检不过线"依赖本任务的数据面;本任务不做阈值判定。
+
+## Background(已确认事实)
+
+- **序 5 已接通,体检是"最小版"**:`v7/src/state-machine/index.js:61-65` 按 `maxChapter - lastCheck >= 体检周期` 判定进入 `health-check` 态(needsAI=false);`runHealthCheck`(`v7/src/health-check/index.js`)已产 `工作区/体检报告.md`(全书近况 + 悬了太久 + 条目活跃率 + 连续弱钩),体检章号写 meta `last_health_check_chapter`(跨重建保留,丢失重测无害)。报告"文体指纹 / 高频意象 / 句式体检"节是占位(index.js:48-49"随 M5.5 落地")。
+- **fingerprints 表已建**(`v7/src/cache/schema.js:80-93`,SCHEMA_VERSION 2):PK `(chapter_range_start, chapter_range_end)`、`is_baseline`、5 个常用特征列 + `fingerprint_data` JSON;**无时间戳**(指纹是确定性派生物,删缓存重算结果必须一致,cache-design §1.5 不变量)。`rebuilder.js:49` 重建时留空,何时算哪段由调用方决定。
+- **report-style-drift 半激活**(`v7/src/commands/report-style-drift.js`):读表对比基线 vs 最近章段已实现(表空友好报错),但只输出 3 项 delta,**缺句长方差 delta**;特征提取函数不存在(M1 明确 defer:`06-27-m1-format-core/prd.md:266`)。
+- **备料占位**:`v7/src/prep/index.js:97`「反复读清单(暂空,跨章高频意象统计随 M5.5 体检补)」。
+- **机检推迟项**:M2 D2 决策(`06-27-m2-writing-flow/prd.md:46`)把"跨章高频意象统计、句式体检"推到体检里程碑,机检框架预留扩展点(`v7/src/mechanical-check/index.js` 顺序 check 函数,issues 带 `blocking` 字段;先例:新专名/信息差走非阻断候选)。
+- **book.yaml 策略参数已就位**:`体检周期: 50`、`文体基线起/止`(默认 1-30),BookConfigReader 已读(spec 0.10 §3)。
+- **缺时间锚点检查是 spec-实现缺口**:spec §9 体检定义含此项(原文用词"时间线孤儿",本任务改名,见决策 D4),PRD §4 #2 验收依赖它,v7 无任何实现;chapters 表有 `story_time` 列,TimelineReader 已有。
+- **v6 无可平移代码资产**:高频意象/句式仅存在于设计讨论记录,算法新写(spec 已定口径)。
+- **CLI 通道已有**:`health-check` 命令(`v7/src/commands/health-check.js`)走 run 契约;宿主经 `next` 序 5 → `health-check` 已可跑通。
+
+## Requirements
+
+### A. 跨章高频意象统计(PRD #9,零 token)
+
+- A1 全书扫描定稿正文,提取跨章重复短语(中文 n-gram 聚合 + 最长优先去重),排除名册专名/别名;产出 top-N 清单(短语、全书次数、出现章分布概要)
+- A2 清单进体检报告"高频意象"节("空气仿佛凝固:全书 47 次"这类可读呈现)
+- A3 统计结果存缓存(meta JSON,跨重建保留无害、重测刷新),备料读取接通"反复读清单":列 top-N「全书已用 N 次,本章避免再用」;从未体检 → 占位改为人话提示"尚未体检,暂无数据"
+- A4 机检接入(消费不生产):本章草稿命中缓存清单中的高频意象 → **非阻断**提醒(不改变机检 pass 判定);无体检数据 → 该项静默跳过
+
+### B. 句式体检(PRD #11)
+
+- B1 分句/分段统计:句长方差、段落长度分布、高频句式开头(句首短语 top-N)
+- B2 指标进体检报告"句式体检"节
+- B3 机检接入:本章句式指标 vs 基线指纹对比,偏离超容差 → **非阻断**提醒;无基线数据 → 静默跳过
+- B4 单章(机检)与章段(体检/指纹)复用同一统计函数,口径一致
+
+### C. 文体指纹提取 + 基线对比(激活 M1 留桩)
+
+- C1 特征提取函数:输入章号区间 → 读定稿正文 → 产指纹(avg_sentence_length / sentence_length_variance / avg_paragraph_length / common_phrase_frequency / vocabulary_richness + fingerprint_data 完整 JSON);**确定性**:同章段任何时候重算结果一致
+- C2 体检时 upsert 两行:基线章段(book.yaml `文体基线起/止`)+ 当前周期章段;重建器保持留空(注释同步更新)
+- C3 `report-style-drift` 补齐:输出增加句长方差 delta;体检报告"文体指纹"节含基线 vs 最近的漂移对比与人话说明(回拉或更新基线由作者决定,脚本只提示改 book.yaml 的方法,不自动改)
+
+### D. 缺时间锚点检查(原 spec 表述"时间线孤儿",本任务统一改名)
+
+- D1 两种缺法都查:定稿章 front matter 无「故事时间」(chapters 表 `story_time` 空);或时间线文件中无该章对应行。统一进体检报告"缺时间锚点"节(全齐报"无")
+- D2 对齐 PRD §4 #2 验收(原文"体检报告时间线孤儿为零"→"缺时间锚点的章为零",spec §9/PRD #2 用词随本任务 3.3 spec 回填一并替换)
+
+### E. 体检编排与 M6 数据面
+
+- E1 `runHealthCheck` 集成上述统计,替换占位节;返回值增加结构化 `data`(各项指标与清单),作为 M6 阈值判定的对接面(本任务不判"过线/不过线")
+- E2 报告仍落 `工作区/体检报告.md` 不入档;`last_health_check_chapter` 逻辑不变
+- E3 降级诚实:单项统计失败不炸整个体检,节内如实说明该项失败原因;永不带英文堆栈
+
+## Acceptance Criteria
+
+- [x] AC1(PRD #9 验收):构造重复意象样章 fixture(如"空气仿佛凝固"多章反复),体检报告高频意象节列出短语与全书次数;体检后机检对命中草稿产**非阻断**提醒(后半在 `test/health-check/index.test.js` 报告节用例;前半"机检报出"在 `test/mechanical-check/check.test.js`「高频意象命中」用例,meta 种子 = 体检产物,链路由体检测试证明 meta 确实写入)
+- [x] AC2(PRD #11 验收):体检报告含句长方差、段落长度分布、高频句式开头三类指标;机检句式项在偏离基线时产非阻断提醒、无基线时静默跳过
+- [x] AC3 指纹链路:体检后 `fingerprints` 表含基线 + 当前周期两行;`report-style-drift` 输出含句长方差在内的 delta;**删 `.cache` 全量重建后再次体检,指纹逐字段一致**(确定性)
+- [x] AC4 备料接通:体检后 `prepare-chapter` 产出的写作材料"反复读清单"含 top-N 高频意象及次数;未体检时输出人话占位
+- [x] AC5 缺时间锚点:时间锚点齐全的 fixture 报"无";构造缺「故事时间」(实字段名「书内时间」)或时间线漏行的章,体检能列出章号
+- [x] AC6 序 5 回归:体检后 meta 更新、`next` 不再报体检;既有全量测试绿(374/374,Windows 本机);CI 双平台绿随 P4.4 push 后回填 run 号:(待)
+- [x] AC7 M6 对接面:`runHealthCheck` 返回结构化 data(指纹漂移/高频意象/句式/悬了太久等),测试断言形状稳定
+
+## Out of Scope
+
+- ❌ 阈值判定与"体检不过线"停止条件(M6:数据面在本任务,判定归自动模式)
+- ❌ 卷复盘调用指纹提取(cache-design"调用方决定"机制已允许,接入随 M6/M7 按需)
+- ❌ 语气体检/遮名盲测/情绪波形图(7.x,PRD #4/#10/#12 明确后置)
+- ❌ 高频阈值/top-N 的 book.yaml 参数化(本版硬编码合理默认;报告是提醒不拦截,参数化收益低,7.x 视反馈)
+- ❌ 增量指纹计算(体检低频操作,每次全量重算章段可接受)
+- ❌ 分词库引入(保持"运行时直接依赖仅 js-yaml",用字符 n-gram + 标点分句的零依赖算法)
+
+## 决策记录(brainstorm)
+
+- **D1 机检两统计项按 spec §8 第 5 步回接,均非阻断**:出口原文"高频意象可被机检/体检报出";"报出"≠拦截,且与 M2 候选类先例(新专名/信息差不拦)一致。机检消费体检缓存(meta 清单/基线指纹),不在每章全书扫描——体检产出、机检消费的分工保持机检快与零重复计算。
+- **D2 全书高频意象存 meta JSON 不建新表**:派生物、单 key 读写、跨重建保留语义与体检章号一致;避免 SCHEMA_VERSION 变更触发全量重建。
+- **D3 指纹每次体检直接重算 upsert**(不查缺):确定性派生物重算结果相同,简单胜过缓存判断。
+- **D4 术语替换(作者确认 07-04)**:"时间线孤儿"面向作者不是人话,统一改为**"缺时间锚点"**;检查项纳入 M5.5,spec §9 与 PRD #2 原文用词随本任务 3.3 spec 回填替换。
+
+## Open Questions
+
+(无——scope 与术语均已收敛,技术口径见 design.md)

+ 26 - 0
.trellis/tasks/07-04-m5-5-checkup/task.json

@@ -0,0 +1,26 @@
+{
+  "id": "m5-5-checkup",
+  "name": "m5-5-checkup",
+  "title": "M5.5 体检里程碑:高频意象统计、句式体检、文体指纹",
+  "description": "",
+  "status": "in_progress",
+  "dev_type": null,
+  "scope": null,
+  "package": null,
+  "priority": "P2",
+  "creator": "codex",
+  "assignee": "codex",
+  "createdAt": "2026-07-04",
+  "completedAt": null,
+  "branch": null,
+  "base_branch": "v7",
+  "worktree_path": null,
+  "commit": null,
+  "pr_url": null,
+  "subtasks": [],
+  "children": [],
+  "parent": null,
+  "relatedFiles": [],
+  "notes": "",
+  "meta": {}
+}

+ 1 - 0
docs/architecture/cache-design-2026-06-26.md

@@ -169,6 +169,7 @@ CREATE INDEX idx_fingerprints_baseline ON fingerprints(is_baseline);
 - **指纹是确定性派生物**:同一章段(定稿不可改)任何时候重算都一样,所以身份只用 `(章段起, 章段止)`——**不带时间戳**。带 `computed_at` 会让"删 `.cache` 重建"对不上(时间戳无法复现),破坏不变量 2。
 - 何时算哪段由调用方决定(体检/卷复盘),算出来按章段 upsert;删缓存后这些行从定稿文本原样重算
 - 基线指纹由 `book.yaml` 的 `文体基线起/止` 决定
+- 基线段与近段章段完全重合时(全书尚在基线区间内),调用方只落基线行——同主键两次 upsert 会让后写的一行翻转 `is_baseline` 标记(M5.5 体检的写入约定)
 
 ---
 

+ 13 - 3
docs/architecture/story-repo-spec-2026-06-10.md

@@ -1,6 +1,10 @@
-# Story Repo 格式规格(v7 草案 0.10
+# Story Repo 格式规格(v7 草案 0.11
 
-> 状态:0.10。相对 0.9 的变更(2026-07-03 M1-M5 全量 review,决策 33-34 见 §14):
+> 状态:0.11。相对 0.10 的变更(2026-07-04 M5.5 体检落地,决策 35 见 §14):
+> - §9 体检定义术语替换:「时间线孤儿」→「缺时间锚点」(front matter 无「书内时间」或时间线漏行,两种缺法都查;PRD §4 #2 验收用词同步)
+> - §9 体检定义与实现对齐:报告含跨章高频意象与句式体检两节;高频意象清单供备料「反复读清单」与机检候选(§8 第 5 步)消费——体检产出、机检消费
+>
+> 0.10 相对 0.9 的变更(2026-07-03 M1-M5 全量 review,决策 33-34 见 §14):
 > - 序 2/序 4 执行责任落点钉死:手改补登由脚本 `relink` 命令 `fix(手改): 说明` 入档,卷复盘产物由脚本 commit `vol(NN): 复盘与下卷规划`(§9/§10)
 > - §11 增「改源流程自刷缓存」公约:定稿/回退/吃书/卷复盘/修复回写完成后由该流程刷新缓存
 >
@@ -445,7 +449,7 @@ ch(152): 北境的雪
 ## 9. 中环、外环与例外流程
 
 - **卷复盘** `vol(05): 复盘与下卷规划`:三类条目清账(本卷开/收/悬了太久清单)→ `摘要/卷摘要/` → 翻一遍灵感池 → 与作者对谈产出 `大纲/卷纲/第06卷.md` → 顺手做伏笔机会扫描(模型提 3-5 个"本卷可埋、N 卷后响"候选,必须引用总纲的具体远期节点,作者勾选后生成条目文件)。**执行体**:复盘产物由脚本落盘并 commit(`vol(NN)` 前缀)+ 刷新缓存——与定稿同为脚本责任,否则产物滞留未提交区误触序 2。
-- **体检**(手动模式每 50 章;连写中每批次一次):文体指纹 vs 基线区间的漂移报告 + 条目活跃率与悬了太久清单 + 时间线孤儿 → 报告进工作区,不入档;作者决定回拉或更新基线。
+- **体检**(手动模式每 50 章;连写中每批次一次):文体指纹 vs 基线区间的漂移报告 + 跨章高频意象与句式体检 + 条目活跃率与悬了太久清单 + 缺时间锚点(front matter 无「书内时间」,或时间线漏了该章的行)→ 报告进工作区,不入档;作者决定回拉或更新基线。高频意象清单同时进缓存,供备料「反复读清单」与机检候选(§8 第 5 步)消费——体检产出、机检消费,机检不做全书扫描。
 - **吃书**(作者界面叫吃书,commit 前缀仍 retcon):`retcon(87): 修正大长老境界设定`——显式流程,允许改定稿,要求 commit message 写明原因,设定/条目同步,留痕可查。
 - **影响分析**(脚本):改设定/吃书前,grep 正文+条目履历+时间线,列出"哪些章建立在这个事实上",分已发布/未发布两清单。未发布 → 直接改或吃书;**已发布 → 顺势圆**(生成向后兼容错误的圆设定方案——"已发布不可改"是网文铁律,顺势圆是主路径)。
 - **"回到第 N 章"**(人话命令):git 回滚的包装,自动模式跑废了一键回到批次起点或任意已定稿章。执行前展示影响范围,作者确认。
@@ -543,6 +547,12 @@ v6 的 8 个命令全部内化为以上状态。作者只需要一个入口和"
 
 ## 14. 决策记录
 
+### 0.10 → 0.11(依据:2026-07-04 M5.5 体检里程碑)
+
+| # | 变更 | 落点 | 来源 |
+|---|------|------|------|
+| 35 | 术语替换:「时间线孤儿」→「缺时间锚点」(面向作者不是人话);定义钉死两种缺法——定稿章 front matter 无「书内时间」、时间线中无该章对应行,体检报告列章号与缺因;体检定义同步补齐高频意象/句式体检两节与"体检产出、机检消费"分工 | §9 | M5.5 任务决策 D4(作者确认 07-04);PRD §4 #2 验收用词同步替换 |
+
 ### 0.9 → 0.10(依据:2026-07-03 M1-M5 全量 review,行为级探针复现)
 
 | # | 变更 | 落点 | 来源 |

+ 1 - 0
docs/architecture/v7-implementation-plan.md

@@ -163,6 +163,7 @@ M1 defer 的指纹提取与 M2 D2 决策推迟的统计项在此收口——此
 - 文体指纹提取 + 基线对比(填充 `fingerprints` 表,激活 `report-style-drift`)
 - 状态机序 5 接通:体检报告落工作区不入档;上次体检章号存 `.cache`(spec 0.9 §10 序 5,丢失则重测无害)
 - **出口**:PRD §4 #9/#11 验收方式可跑——高频意象可被机检/体检报出、句式体检指标进体检报告
+  (出口达成 2026-07-04:意象/句式/指纹/缺时间锚点四节进体检报告,`runHealthCheck` 返回结构化 data 供 M6 阈值判定;机检增高频意象命中与句式偏离两候选(非阻断);备料反复读清单接通;`report-style-drift` 补句长方差 delta。任务:`.trellis/tasks/07-04-m5-5-checkup/`,spec 0.11 决策 35)
 - **硬依赖**:M6 停止条件"体检不过线"依赖本里程碑,M6 不得先行
 
 ### M6 自动模式(按批次定稿)

+ 2 - 2
docs/architecture/v7-prd.md

@@ -1,6 +1,6 @@
 # webnovel-writer v7 产品需求文档(PRD)
 
-> 状态:1.1(2026-07-02 修订,仅 §5 依赖口径一处——运行时直接依赖仅 js-yaml,依据设计边界回顾与 story-repo-spec 0.9 决策 32;1.0 于 2026-06-12 作者逐项确认定稿)
+> 状态:1.2(2026-07-04 修订,仅 §4 #2 验收用词「时间线孤儿」→「缺时间锚点」,依据 M5.5 任务决策 D4 与 story-repo-spec 0.11 决策 35;1.1 于 2026-07-02 修订 §5 依赖口径一处;1.0 于 2026-06-12 作者逐项确认定稿)
 > 日期:2026-06-12
 > 读者:①开发实施者(含 AI);②issue 区用户(RFC 母本,裁剪时换大白话)
 > 地位:本 PRD 是 v7 的产品法律文本。spec 0.5 的"冻结"已解除——本 PRD 先行,`story-repo-spec` 与 `multi-agent-adaptation-spec` 随本文档修订(修订指令见 §10)。
@@ -233,7 +233,7 @@ v6 的 8 个命令全部内化为以上状态;作者只需要一个入口和"
 | # | 问题 | 解法 | 版本 | 验收方式 |
 |---|---|---|---|---|
 | 1 | 客观事实吃书(修为/资产/物品) | 角色卡结构化字段增量记账,定稿时脚本更新 | 7.0 | 改 100 章前的设定,影响分析能列全受影响章 |
-| 2 | 时间线混乱 | 每章强制时间锚点;时间线按卷拆分,append-only | 7.0 | 从第 1 章起强制;体检报告时间线孤儿为零 |
+| 2 | 时间线混乱 | 每章强制时间锚点;时间线按卷拆分,append-only | 7.0 | 从第 1 章起强制;体检报告缺时间锚点的章为零 |
 | 3 | 信息差泄密 | 登记表(每条一文件:知情人/读者已知/关键词)+ 机检出候选 + 设定校对判真伪;写作材料注入出场人物知识边界 | 7.0 | 构造泄密样章,三审能逮住;误报不拦截流程 |
 | 4 | 人设语气漂移 | 角色卡存典型对话原文 few-shot + 周期语气体检 + 遮名盲测 | 7.x(体检后置) | 盲测准确率报告 |
 | 5 | 摘要信息损耗 | 分层摘要(章≤200字/卷≤500字/全书骨架按需拼接、不落盘)+ Grep 正文管细节 + 账本即"人工标注的未来相关性" | 7.0 | 写第 0153 章时,写作材料能召回第 12 章埋下、第 152 章推进的伏笔 |