前置:已读 prd.md、design.md,以及后端规范(质量/数据/错误/目录结构)+ spec 0.8 + O4 缓存设计。 落点全部在
v7/src/与v7/test/;不碰 v6 与根遗产。 本机命令:cd v7 && node --test(Node 24.15.0 可用);Python 脚本用PYTHONUTF8=1 python。
背景:A–E 六个 commit 已落地,但 review 发现 D.2+D.3(2bb34f6)把 35 个 P1/P2 接口写成 console.log('{}') 空壳,且命令层零测试。根因是 design §6.2 旧契约自相矛盾(既 return 又 console.log + process.exit),命令无法单测。已 git revert 2bb34f6 撤回空壳。
修正后契约(design §6.2 已改):命令导出 run(args, options, ctx) 只返回 {ok, output?, error?},不碰 process/console/cache 生命周期;bin 唯一负责打印、退出码、cache.ensureReady/close。命令因此可单测,AC2 才成立。
重建顺序(覆盖原 D.0–D.3,全部真实现 + 每接口 test/commands 测试):
v7/.gitignore 忽略 .cache/;删冗余 test/chinese-path.test.js(保留 test/integration/chinese-path.test.js);rebuilder scanCharacters 改 upsert(修角色卡丢数据 bug)run+ctx 分发;6 个 P0 命令(read-chapter/read-thread/read-timeline/read-character/resolve-alias/report-overdue-threads)改 run 契约 + 补 test/commands/*.test.js大纲/总纲.md、大纲/第01卷.md、定稿/设定/世界观.md、定稿/摘要/章摘要/0002.md、悬念/感情线各一条目(design §7.1)--front-matter(已在 P0 文件内)--regex、report-secret-accumulation、report-thread-activity、report-style-drift(边界占位:读指纹对比基线、不做特征提取)41 接口权威清单见 prd.md AC2 表(21 命令文件承载)。下方原始 A–E checklist 保留作历史参考。
阶段 A:容错读写库(parser/serializer)
↓
阶段 B:Storage Adapter 小端口(8+ Reader + Writer 占位)
↓
阶段 C:.cache/index.db 五表 + 重建器(node:sqlite)
↓
阶段 D:41 精准读取接口 CLI(分 P0/P1/P2 三层,每层 sub-commit)
每阶段独立验证 + 一次 commit 检查点(除阶段 D 分三 sub-commit)。
js-yaml:cd v7 && npm install js-yaml(MIT、依赖树仅含 argparse)v7/src/storage/parsers/ 与 v7/src/storage/serializers/ 目录v7/src/storage/parsers/front-matter.js:
parseFrontMatter(content) 函数(分离 --- 包裹的 YAML 与 Markdown 正文){ok, data, body, error, rawYAML}(rawYAML 用于保留未知字段)--- 不存在/不配对 → ok=false;YAML 语法错误 → ok=false;不抛异常v7/test/storage/parsers/front-matter.test.js:
---、YAML 语法错误v7/src/storage/parsers/yaml-safe.js:
parseYAML(yamlString, options) 包装 js-yaml.load{ok: false, error}v7/src/storage/serializers/yaml-dialect.js:
serializeYAML(data) 手写序列化(防呆方言)needsQuoting(value) 辅助函数v7/test/storage/parsers/yaml-safe.test.js:
v7/test/storage/serializers/yaml-dialect.test.js:
[a, b])"123", "true", "A:B")v7/src/storage/serializers/front-matter.js:
serializeFrontMatter(data, body, originalYAML) 组装 ---\nYAML\n---\n正文v7/test/storage/parsers/yaml-safe.test.js(补充用例):
自定义字段: 值 的 YAML,解析 → 修改已知字段 → 序列化,断言自定义字段保留v7/src/storage/parsers/markdown-table.js:
parseMarkdownTable(content) 提取表头与行(返回 {ok, headers, rows, error})v7/src/storage/parsers/book-config.js:
parseBookConfig(yamlString) 读取平铺字段(spec §3){ok, data: {书名, 类型, 每章目标字数, ...}, error}v7/test/storage/parsers/markdown-table.test.js:时间线表、名册表v7/test/storage/parsers/book-config.test.js:正常 book.yaml、缺字段验证 A:cd v7 && node --test test/storage/parsers/ test/storage/serializers/ 全绿
提交 A:feat(v7): M1 阶段 A——容错读写库(parser/serializer)
v7/src/storage/adapters/ 目录v7/test/fixtures/sample-book/ 示例书仓库(design §7.1 完整示例):
定稿/正文/0001-开局.md(含完整 front matter,见 design §7.1)、0002-初遇.md定稿/设定/角色/林晚.md(含 front matter + 设定/对话/关系段落)大纲/伏笔/伏笔-001-神秘老者.md(含履历格式示例)定稿/设定/信息差/信息差-001-灭门真凶.md定稿/设定/时间线/第01卷.md(Markdown 表格)定稿/设定/名册.md(Markdown 表格)定稿/摘要/章摘要/0001.mdbook.yaml(含基本配置,见 design §7.1)v7/src/storage/adapters/ChapterReader.js:
constructor(repoPath, cache)(cache 可选)readFrontMatter(chapterNum):优先查缓存 chapters 表,缺失时读文件readBody(chapterNum)、readTail(chapterNum, wordCount)、readHead(...)readRange(start, end, fields)v7/test/storage/adapters/ChapterReader.test.js:
{ok: false, error}v7/src/storage/adapters/ThreadLedgerReader.js(design §4.3):
readBasicInfo(threadId)、readHistory(threadId)、readClosurePlan(threadId)、readDescription(threadId)listOverdue(bookConfig)(查询时计算悬了太久章数)listByType(type, status)v7/test/storage/adapters/ThreadLedgerReader.test.js:
v7/src/storage/adapters/EntityReader.js(design §4.4):
readCharacterFrontMatter(name)、readCharacterFull(name)resolveAlias(alias)(查 entity_aliases 表或解析名册)listCharacters(filter)TimelineReader.js:readCurrentVolume(), readVolumeRange(start, end), readByParticipant(name)SecretReader.js:readBasicInfo(id), readContent(id), listUnrevealed()OutlineReader.js:readOutlineSection(type, volumeNum, sectionTitle), listVolumes()BookConfigReader.js:read() 返回 book.yaml 对象ChapterWriter.js、ThreadLedgerWriter.js 只定接口(抛 "M2 实现" 占位错误)v7/src/storage/index.js 统一导出所有端口验证 B:cd v7 && node --test test/storage/adapters/ 全绿
提交 B:feat(v7): M1 阶段 B——Storage Adapter 小端口(8 Reader + Writer 占位)
v7/src/cache/index.js:
class CacheManager 构造函数(dbPath)ensureReady(repoPath):检查 db 存在性,不存在调用 rebuildFromSourcequery(sql, params)、close()node:sqlite 的 DatabaseSyncv7/src/cache/schema.js:五表 DDL 字符串(design §5.1 完整 SQL)ensureReady 内执行 schema.js 的 CREATE TABLE + CREATE INDEXv7/test/cache/CacheManager.test.js:
[]v7/src/cache/rebuilder.js:rebuildCache(repoPath, db) 函数(design §5.3)
{ok, warnings: [], errors: []}scanChapters(repoPath) 返回 Map<章号, 文件路径>(用于履历验证)scanThreads(repoPath, type) 扫描三类条目目录scanEntities(repoPath) 扫描角色卡 + 名册scanChapters 建立章号 → 文件路径映射(Map)rebuildCache 返回 {ok: false, errors}v7/test/cache/rebuilder.test.js:
v7/test/cache/rebuild-integration.test.js:
.cache/index.db验证 C:cd v7 && node --test test/cache/ 全绿
提交 C:feat(v7): M1 阶段 C——.cache/index.db 五表 + 重建器(node:sqlite)
v7/bin/webnovel-writer.js 增加子命令动态 import 分发(design §6.1):
process.argv[2] 为命令名await import(\../src/commands/\${命令}.js`)`commandModule.execute(args, options)v7/src/commands/ 目录read-timeline.js:--current-and-prev 选项(读当前卷+上一卷)read-thread.js:--fields=基本信息 / --履历read-chapter.js:--tail=N / --摘要(从 定稿/摘要/章摘要/NNNN.md 读)read-character.js:--front-matterresolve-alias.js:输入别名,输出正名或"未找到"report-overdue-threads.js:按类型分组返回悬了太久清单(design §6.3 查询时计算)v7/test/commands/:每个接口至少正常路径 + 边界(不存在 ID)cd v7 && node bin/webnovel-writer.js read-chapter 1 --tail=100 可执行验证 D1:P0 8 接口逐个手工冒烟 + node --test test/commands/ P0 测试绿
提交 D1:feat(v7): M1 阶段 D.1——精准读取 P0 接口(写章流程依赖 8 个)
read-chapter.js 补充 --front-matter 选项list-chapters.js:--章定位=推进 / --卷=N 筛选list-secrets.js:--reader-knows=falsegrep-story.js:<关键词> 全文检索(Grep 定稿/正文/*.md)report-book-stats.js:总章数/总字数/条目数/角色数report-weak-hook-streak.js:末尾连续弱钩章数(design §6.3,匹配"弱钩"或"-弱")验证 D2:P1 7 接口冒烟 + 测试绿
提交 D2:feat(v7): M1 阶段 D.2——精准读取 P1 接口(机检与全书近况 7 个)
--收尾计划 / --描述,list-threads 补 --type / --strength--head=N / 正文全文,read-chapters --range=start-end--current-volume / --卷=N / --在场=name--section,list-characters,read-worldview --section--内容--regex=pattern验证 D3:P2 26 接口批量冒烟(抽查 5 个详细测,其余正常路径测试绿)
提交 D3:feat(v7): M1 阶段 D.3——精准读取 P2 接口(AI 角色优化 26 个)
v7/test/integration/chinese-path.test.js(design §7.2 AC5):
os.tmpdir() 下建 测试书仓库/定稿/正文/0001-测试.mdawait cache.close() 关闭数据库连接(避免 Windows 文件锁导致清理失败).github/workflows/v7-ci.yml 已有 Windows job,此测试自动覆盖cd v7 && node --test 全绿(所有测试)implement.jsonl 填充真实条目(移除 _example):
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "零依赖、职责分界、精准读取"}{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "五表 DDL、重建器、容错读取"}{"file": ".trellis/spec/backend/error-handling.md", "reason": "永不带栈崩、中文错误"}{"file": "docs/architecture/story-repo-spec-2026-06-10.md", "reason": "格式法律文本(§1-6、§11)"}{"file": "docs/architecture/cache-design-2026-06-26.md", "reason": "五表 DDL、41 接口清单、派生值策略"}check.jsonl 填充:
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "评审清单 §5(零依赖/术语表/错误处理/职责分界/文档先行)"}{"file": "docs/architecture/story-repo-spec-2026-06-10.md", "reason": "不变量 2(删缓存可重建)、不变量 9(保留未知字段)、§2.3 防呆方言"}task.py current 确认任务状态feat(v7): M1 收尾——CI 验收项 + JSONL 清单git restore v7/ 对应子目录即可v7/src/cache/ 与 v7/test/cache/,不影响 A-Bfeat(v7): M1 阶段 A——容错读写库(parser/serializer)feat(v7): M1 阶段 B——Storage Adapter 小端口(8 Reader + Writer 占位)feat(v7): M1 阶段 C——.cache/index.db 五表 + 重建器(node:sqlite)feat(v7): M1 阶段 D.1——精准读取 P0 接口(写章流程依赖 8 个)feat(v7): M1 阶段 D.2——精准读取 P1 接口(机检与全书近况 7 个)feat(v7): M1 阶段 D.3——精准读取 P2 接口(AI 角色优化 26 个)收尾工作并入 commit 6 或单独 commit 7(按实际情况)。
重建后状态(2026-06-27,全量
node --test143 绿):
.cache 全量重建测试绿(test/cache/CacheManager.test.js「删除缓存后全量重建」+ test/cache/rebuilder.test.js)v7/package.json dependencies 仅 js-yaml(直接依赖唯一;传递依赖 argparse 同 nodeca 维护)