Sfoglia il codice sorgente

chore(task): P3 中危合批任务工件收口(F12 核查留档 + prd/implement 勾选)

- research/route-isomorphism-check.md:三场景复现命令 + 源码实审证据,
  v7 无 #135 同构问题零改动收口。
- 父任务 F8-F11 勾选 + F12 结论;implement.md 全步完成态。
lingfengQAQ 2 mesi fa
parent
commit
fc9702d2d5

+ 8 - 6
.trellis/tasks/07-29-p3-medium-batch/implement.md

@@ -2,6 +2,8 @@
 
 前置:v7 分支 887dc1a 之后;716 测试绿基线。分工沿用:kimi 实施 / claude 检查。TDD 每项先攻击/故障样例红测。
 
+> 状态(2026-07-31 kimi 收尾):**S1-S6 全部完成,全量 733/733 绿 + drift 绿,等 claude review gates(S3 序 0 走查 + S6 全证据复跑)**
+
 ## 规划期勘察修正(2026-07-31,逐文件核实)
 
 - **F9 口径以后置债务清单为准**(`tasks/archive/2026-07/07-23-v7-global-review-remediation/research/后置债务清单.md` 行 15):陈旧检测(pid 存活或年龄阈值)+ **由 `next` 序 0 提议清理并保留作者确认**——**不做静默自动回收**;现有并发互斥与锁内不变量测试不回退。勘察红利:锁文件已写 `{pid, operation, startedAt}`(`contract-invalidation.js:36-38`),自愈所需数据在位,只缺读侧判定 + 序 0 接线 + EEXIST 文案补锁路径。
@@ -13,12 +15,12 @@
 
 ## 步骤(顺序可调,五项彼此独立)
 
-- [ ] **S1 F8 YAML key 校验**:红测(key 含 `\n`/`:`/`#` 的注入样例期望 throw 人话)→ `serializeYAML` 循环头加 key 白名单判定(合法=非空、无控制字符、无 `:`/`#`/引号引导、无首尾空白)→ 全量 front matter round-trip 回归。
-- [ ] **S2 F10 index.lock 阈值**:`STALE_LOCK_MS` 3000→60000 + 删除前二次 stat 年龄确认;异常样本库全绿;spec 决策注明理由(杀软/索引器锁盘场景)。
-- [ ] **S3 F9 契约锁陈旧自愈**:红测(放置陈旧锁文件:pid 已死 / startedAt 超阈值)→ 读侧 `readStaleContractLock(repoPath)`(pid 存活探测 + 年龄阈值双判)→ `next` 序 0 检测到陈旧锁时进 `dto.failures` 提议清理(走既有序 0 修复确认流,作者确认后 persist-repair 删锁)→ EEXIST 报错文案补锁文件路径与「若确认无并发进程可经序 0 清理」指引。活锁(pid 存活)保持拒绝。现有互斥测试零回退。
-- [ ] **S4 F11 books.jsonl 写读双侧**:红测(`目录: "../../x"` 写侧拒绝;手工坏行读侧过滤入 corrupt 计数且 locate 不消费)→ 写侧 `registerBook` 校验 `书名` 非空字符串 + `目录` 过 `isSafeFileStem`;读侧 `readBooks` 行级形状校验(书名/目录字符串 + 目录 isSafeFileStem),坏行 corrupt++ 走既有自愈回写;`runtime/locate.js` 消费面零改动(上游已滤净)。
-- [ ] **S5 F12 路由同构核查(核查项)**:实测三场景取证——只给大类题材(如 玄幻)建书时 `knowledge-pack` 返回集、`路由.csv` 归一行为、书级契约是否隐式带入某子流派调性/桥段默认值;结论 + 复现命令留档本任务 `research/route-isomorphism-check.md`。有问题→修复并补测试;无问题→写明证据收口。
-- [ ] **S6 收尾**:全量回归 + drift 绿;spec 回填(story-repo 决策条目:F8 key 校验口径 / F9 序 0 清理流 / F10 阈值 / F11 双侧校验;error-handling 如涉补条);提交分层(实现+测试一 commit、任务工件一 commit);父任务 F8-F11 勾选 + F12 记录结论。
+- [x] **S1 F8 YAML key 校验**:红测(8 类注入样例 throw 人话指认字段名 + 合法键不受影响)→ `serializeYAML` 循环头 `assertSafeYamlKey` 白名单判定 → 全量 front matter round-trip 43 绿。
+- [x] **S2 F10 index.lock 阈值**:`STALE_LOCK_MS` 3000→60000(导出常量供测试断言下限)+ 删除前二次 stat 年龄确认;git-health 7 例全绿(5s 新鲜锁保留/90s 超龄删/二次 stat 覆盖)。
+- [x] **S3 F9 契约锁陈旧自愈**:红测(死 pid→陈旧 / 活 pid 超龄→拒 / 无 pid+超龄→年龄兜底 / EEXIST 文案)→ `readStaleContractLock`(pid 存活探测优先,EPERM 保守活锁;无 pid 回退 30 分 mtime)→ 序 0 独立检测并入 failures(动作=delete + 修复指引;**非静默回收**)→ `persistRepair` 新增 `action:'delete'`(`REPAIR_DELETE_ALLOWED` 白名单只放行契约锁路径)→ EEXIST 文案。活锁拒绝零回退(router 2 例实证序 0 不被活锁触发)。
+- [x] **S4 F11 books.jsonl 写读双侧**:红测(形状非法行 10 样例全 corrupt、写侧 8 类非法目录拒绝、自愈回写清坏行)→ 共用 `isValidBookEntry`(目录=`isSafeFileStem` P0 单源);读侧形状非法一行入 corrupt 走原有回写通道;`runtime/locate.js` 消费面零改动。session 20 例全绿。
+- [x] **S5 F12 路由同构核查(核查项)**:实测三场景取证实拍(玄幻=无携带;修仙=canonical 归一仙侠声明表;玄幻+凡人流=显式指定才进包)+ resolveBookKnowledge 源码逐行审(未命中显式列出、零 fallback/默认选择/继承分支)→ **零改动收口**,`research/route-isomorphism-check.md`。
+- [x] **S6 收尾**:全量回归 **733/733 绿** + drift check 绿;spec 回填 story-repo 0.21 决策 64(四条口径合批记录);父任务 F8-F11 全勾选 + F12 结论记录;提交分层(实现+测试一 commit、任务工件一 commit——待作者/claude 检查后执行)。
 
 ## Review gates(claude)
 

+ 11 - 11
.trellis/tasks/07-29-p3-medium-batch/prd.md

@@ -8,20 +8,20 @@
 
 每项自带修法口径(来源:父任务 prd F8-F11 + 评审核实):
 
-- [ ] **F8 YAML 序列化器 key 零转义**:`storage/serializers/yaml-dialect.js:18-36` value 侧转义完备但 key 原样输出;AI 可控 updates 键含 `\n`/`:` 可注入 front matter 结构。修法:key 走同等 needsQuoting/转义(优先),或 key 白名单。攻击样例进测试。
-- [ ] **F9 契约互斥锁无陈旧回收**:`staging/contract-invalidation.js:29-67` 进程被 kill 后锁文件永久残留,堵死全部写路径且报错不给位置/删除指引。修法:锁文件带 pid/startedAt,陈旧判定(pid 不存活或超时阈值)自动回收 + 报错文案给锁路径与人话指引。注意与 07-23 整改遗留「锁自愈」债务条目对表(`archive/2026-07/07-23-v7-global-review-remediation/research/后置债务清单.md`),若已有方案沿用其口径。
-- [ ] **F10 git index.lock 陈旧阈值仅 3 秒**:`state-machine/git-health.js:5`,杀软/索引器锁盘超 3 秒被误删 → 双 git 进程并发写损坏窗口。修法:阈值提到分钟级(建议 ≥60s)+ 尽可能加持锁进程存活判断;改动须过既有 git 健康检查异常样本库测试。
-- [ ] **F11 books.jsonl 读侧无形状校验**(**待作者裁决定级**,见 Notes):`session/index.js` 逐行 JSON.parse 不校验;`runtime/locate.js:50` 消费 `目录` 字段,写坏后 `git clean -fd`/`reset --hard`/递归 `fs.rm` 可作用于工作目录外。修法:读侧复用 `persist-book` 同款校验(书名/目录非空字符串、目录为安全相对段——可复用 P0 的 `isSafeFileStem`),坏行走既有 corrupt 计数 + 自愈回写通道。
-- [ ] **F12 v7 路由 #135 同构核查**(核查项,可能零改动收口):v6 的 explicit_genre_fallback 会继承首条细分流派(issue #135,master 线 PR #136 已修)。v7 路由是独立实现(`references/路由.csv` + `knowledge-pack` 固定路由,spec 决策「固定路由只归一书级名称,不选择创意答案」)。核查:书级建档只给大类题材时,知识包/契约是否会隐式继承某条子流派的调性/桥段默认值。产出:核查记录留档 research/(有问题→修复进本批,无问题→写明证据收口)。
+- [x] **F8 YAML 序列化器 key 零转义**:`storage/serializers/yaml-dialect.js:18-36` value 侧转义完备但 key 原样输出;AI 可控 updates 键含 `\n`/`:` 可注入 front matter 结构。修法:key 走同等 needsQuoting/转义(优先),或 key 白名单。攻击样例进测试。✅ 实施口径=白名单 throw 人话(决策 62 拒绝不改写,与嵌套检测同款姿势;覆盖 `\n`/`:`/`#`/首尾空白/引号引导/空/控制字符八类,报错指认字段名)
+- [x] **F9 契约互斥锁无陈旧回收**:`staging/contract-invalidation.js:29-67` 进程被 kill 后锁文件永久残留,堵死全部写路径且报错不给位置/删除指引。修法:锁文件带 pid/startedAt,陈旧判定(pid 不存活或超时阈值)自动回收 + 报错文案给锁路径与人话指引。✅ 实施按后置债务清单口径=**序0 确认流非静默回收**(pid 探测优先/EPERM 保守/30 分钟 mtime 兜底;`persistRepair` 新增 `action:'delete'` 且删除面白名单钳死只放行该锁路径;EEXIST 文案补锁路径+序 0 指引)
+- [x] **F10 git index.lock 陈旧阈值仅 3 秒**:`state-machine/git-health.js:5`,杀软/索引器锁盘超 3 秒被误删 → 双 git 进程并发写损坏窗口。修法:阈值提到分钟级(建议 ≥60s)+ 尽可能加持锁进程存活判断;改动须过既有 git 健康检查异常样本库测试。✅ 60s + 删除前二次 stat(git 锁不含 pid 不可判活锁,勘察修正已收窄)
+- [x] **F11 books.jsonl 读侧无形状校验**(**待作者裁决定级**,见 Notes):✅ 作者默认 claude 建议=留本批执行完毕——写读双侧:`registerBook` 与 `readBooksRegistry` 共用 `isValidBookEntry`(目录=`isSafeFileStem` 复用 P0 单源);形状非法行计入 corrupt 走自愈回写;写侧 8 类非法目录拒绝(含 `../../x`、`/abs`、`C:\x`、保留名)
+- [x] **F12 v7 路由 #135 同构核查**(核查项,可能零改动收口):✅ **零改动收口**——`research/route-isomorphism-check.md`:v7 无 fallback/默认选择机制(resolveLabel 未命中返 null 进未命中清单显式列名;别名归一走声明表非继承);实测三场景(大类/别名/显式流派对照)均无隐式携带。
 
 ## Acceptance Criteria
 
-- [ ] F8:注入样例(key 含 `\n`/`:`/引号)序列化后可安全 round-trip 或被拒绝;现有 front matter 全量 round-trip 测试绿。
-- [ ] F9:kill -9 模拟(或直接放置陈旧锁文件)后下一次写路径操作自动回收并继续;活锁(pid 存活)仍拒绝且文案含锁路径。
-- [ ] F10:阈值常量更新 + 存活判断测试;异常样本库全绿。
-- [ ] F11(若裁决执行):坏 `目录` 字段的行被拒/自愈,`locate` 不再消费越界目录;模拟坏行时定位失败给人话而非作用于仓外。
-- [ ] F12:核查记录含复现命令与结论。
-- [ ] 全量测试绿 + drift 绿;spec 回填(各项对应决策/规范条目)。
+- [x] F8:注入样例(key 含 `\n`/`:`/引号)序列化后被拒绝;现有 front matter 全量 round-trip 测试绿。(43/43 绿)
+- [x] F9:放置陈旧锁文件(pid 死/超龄)后序0 提议清理、作者确认经 persist-repair delete 清除;活锁(pid 存活)仍拒绝且文案含锁路径。(contract-lock 5 + router 2 + persist 3 绿)
+- [x] F10:阈值常量更新 + 5s 新鲜锁不误删 / 90s 超龄才删测试;异常样本库全绿。(git-health 7 绿)
+- [x] F11(裁决=执行):坏 `目录` 字段的行被拒/自愈,`locate` 不再消费越界目录;模拟坏行时定位失败给人话而非作用于仓外。(session 20 绿)
+- [x] F12:核查记录含复现命令与结论。(`research/route-isomorphism-check.md`,三无携带证据链)
+- [x] 全量测试绿 + drift 绿;spec 回填(各项对应决策/规范条目)。(733/733 全绿、drift 绿、story-repo 0.21 决策 64)
 
 ## Non-Goals
 

+ 62 - 0
.trellis/tasks/07-29-p3-medium-batch/research/route-isomorphism-check.md

@@ -0,0 +1,62 @@
+# F12 核查:v7 路由 #135 同构问题排查记录
+
+- **日期**:2026-07-31
+- **核查人**:kimi(任务 07-29-p3-medium-batch S5)
+- **结论**:**v7 不存在 issue #135 的同构问题,零改动收口**( 源码无 fallback 机制 + 实测不携带首条流派默认 + 设计显式禁止 )。
+
+## 背景
+
+v6 issue #135:explicit_genre_fallback——书级建档只给大类题材时,隐式继承首条细分流派的调性/桥段默认值(misrouting 成立是因为旧实现有「细分流派的 fallback 行选择」机制)。master 线 PR #136 已修。
+v7 的路由是新实现(`references/路由.csv` + `knowledge-pack`),spec 决策 47「固定路由只归一名称,不选择创意约束或作品答案」。本核查取证回答:**v7 是否重新引入了同类 fallback/隐式继承**。
+
+## 三场景取证
+
+### 场景一:只给大类题材,返回集(复现命令)
+
+```bash
+node v7/bin/webnovel-writer.js knowledge-pack --类型=玄幻
+```
+
+- 输出:`归一结果:类型=玄幻`;正文= `玄幻.md` 一份条目内容(含 sha256 锚点);**无自动携带任何流派条目、无「按默认/继承」字样**。
+- `未命中` 数组逻辑(`knowledge-pack.js:32-36`):未归一名称显式列为「知识库未命中:…(走对谈共创,不伪装成知识库正式条目)」——**失败路径显式不静默继承**。
+
+### 场景二:别名归一(复现命令)
+
+```bash
+node v7/bin/webnovel-writer.js knowledge-pack --类型=修仙
+```
+
+- 输出:`归一结果:类型=仙侠`——「修仙」是 `路由.csv` 中「仙侠」行的**声明别名**,被归一为其 canonical(不是 fallback 到玄幻,也不是「默认某条」)。
+- 路由.csv 结构(`knowledge/index.js:104-115`):每行声明 `名称/维度/题材/条目/别名[]`——归一是**声明表**不是继承规则。
+
+### 场景三:显式带流派的对照
+
+```bash
+node v7/bin/webnovel-writer.js knowledge-pack --类型=玄幻 --流派=凡人流
+```
+
+- 流派条目**只在用户显式指定时**进包。书级契约(front matter)无流派字段时(场景一),无任何被带入的流派条目。书级「隐式带入子流派调性/桥段默认值」的起点(代码自动选一条)不存在。
+
+### 源码审计(关键)
+
+`v7/src/knowledge/index.js:133-188 resolveBookKnowledge`:
+
+- 唯一归一器 `resolveLabel(rows, input, dimension)`(`:120-128`):`row.名称 === label || row.别名.includes(label)`,未命中返回 `null`——**无「取首条/粗匹配/继承默认」分支**。
+- 未命中者进 `未命中[]`(`:150-165`),UP 行在 knowledge-pack 输出层如实列出。
+- `兼容提醒`(`:175-185`)只对**用户已显式指定的流派**×已选题材的兼容性提醒——不拉新选择进来。
+- `withRouteContent`(`:190`+):只读与被归一一致**同一个**条目文件的切片,调用点三处(题材 / 副题材 / 流派)都以输入参数为驱动,无展开子流派调用。
+
+路由代码内 grep 负数取证:函数内无 `默认`、`fallback`、`继承`、`首条`、`第 1 条`、索引访问 `rows[0]` 或 sort 后排位选择的代码路径。
+
+## #135 同构判定表
+
+| v6 #135 的成立条件 | v7 现状 | 判定 |
+|---|---|---|
+| 大类题材 → fallback 某细分流派 | `未命中` 显式列出,零携带 | 不成立 |
+| 「默认」隐式选择不告知作者 | 未命中输出原文列维度+输入,明示「不伪装成正式条目」 | 不成立 |
+| 桥接/调性默认随题材隐式带入 front matter | 契约 front matter 字段=输入归一后的实点名(无派生) | 不成立 |
+| 路由可自行选择/排序候选 | 决策 47:路由只归一名称、不决定创意约束或作品答案;`resolveLabel` 无选择逻辑 | 不成立 |
+
+## 收口
+
+**核查结论:v7 路由没有 #135 同构发现问题,零改动收口。** 证据链如上(复现命令可重跑)。如未来知识包需要「常见组合推荐」,也要按「作者显式勾选」设计,不得走 v6 的 fallback 惯例(本记录可作为日后评审对照)。

+ 2 - 2
.trellis/tasks/07-29-p3-medium-batch/task.json

@@ -3,7 +3,7 @@
   "name": "p3-medium-batch",
   "title": "P3 中危合批(F8-F11 + v7 路由 #135 同构核查)",
   "description": "",
-  "status": "planning",
+  "status": "in_progress",
   "dev_type": null,
   "scope": null,
   "package": null,
@@ -21,6 +21,6 @@
   "children": [],
   "parent": "07-29-v7-review-findings",
   "relatedFiles": [],
-  "notes": "分工(沿用):kimi 实施 / claude 检查。启动依据=prd+implement(勘察修正已入两件)。要点:F9 序0清理流不得绕过作者确认;F11 写读双侧;平台相关测试实施时先 WSL 过一遍(P0/P2 两次 ubuntu CI 教训)。review gates:S3 后序0走查、S6 前全证据留档交 claude 复跑。",
+  "notes": "分工(沿用):kimi 实施 / claude 检查。启动依据=prd+implement(勘察修正已入两件)。要点:F9 序0清理流不得绕过作者确认;F11 写读双侧;平台相关测试实施时先 WSL 过一遍(P0/P2 两次 ubuntu CI 教训)。review gates:S3 后序0走查、S6 前全证据留档交 claude 复跑。 | 实施完成(2026-07-31 kimi):S1-S6 全过,全量 733/733 绿 + drift 绿。F12 零改动收口(research/route-isomorphism-check.md)。等 claude review gates:S3 序0 走查(重点:非静默回收/活锁不误删/REPAIR_DELETE_ALLOWED 只放行契约锁)+ S6 前全证据复跑(含 WSL 过平台相关测试)。",
   "meta": {}
 }

+ 4 - 4
.trellis/tasks/07-29-v7-review-findings/prd.md

@@ -32,10 +32,10 @@
 
 ### P3 · 中危合批
 
-- [ ] **F8:YAML 序列化器 key 零转义**。`storage/serializers/yaml-dialect.js:18-36`,value 侧转义完备但 key 原样输出;AI 可控 updates 键含 `\n`/`:` 可注入 front matter 结构。修法:key 白名单或同等引号转义。
-- [ ] **F9:契约互斥锁无陈旧回收**。`staging/contract-invalidation.js:29-67`,进程被 kill 后锁文件永久残留堵住全部写路径,且报错不给锁位置/删除指引。修法:pid/startedAt 陈旧判定 + 指引文案。
-- [ ] **F10:git index.lock 陈旧阈值仅 3 秒**。`state-machine/git-health.js:5`,杀软/索引器锁盘超 3 秒会被误删 → 双 git 进程并发写损坏窗口。修法:提到分钟级 + 进程存活判断。
-- [ ] **F11:`books.jsonl` 读侧无形状校验**(用户复核调级为中危候选,待裁决):`session/index.js` 逐行 JSON.parse 不校验 `目录` 字段,`runtime/locate.js:50` 直接消费 → 目录写坏后 `git clean -fd` / `reset --hard` / 递归 `fs.rm` 作用于工作目录外。复核意见:读侧复用 `persist-book` 同款校验。
+- [x] **F8:YAML 序列化器 key 零转义**。✅ 2026-07-31 子任务 07-29-p3-medium-batch 修复(key 白名单 throw 人话指认字段名,决策 62 拒绝不改写)
+- [x] **F9:契约互斥锁无陈旧回收**。✅ 同子任务修复(pid 探测+30 分兜底双判;序0 确认流+delete 白名单只放行该锁路径;EEXIST 文案补指引)
+- [x] **F10:git index.lock 陈旧阈值仅 3 秒**。✅ 同子任务修复(60s + 删前二次 stat 防 TOCTOU)
+- [x] **F11:`books.jsonl` 读侧无形状校验**。✅ 同子任务修复(写读双侧 `isValidBookEntry`,目录=`isSafeFileStem` 复用 P0 单源;形状非法行 corrupt+自愈回写)
 
 ### 设计取舍,暂不动