# 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 双平台绿(run 28704414272,六 job 全绿,2026-07-04) - [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)