1
0

review-G.md 12 KB

Research: review-G(命令壳 / bin / installer / host-shells + 测试脚手架掩盖审计)

  • Query: G 区审查——45+ 薄壳契约、bin 动态派发、installer/host-shells、测试脚手架掩盖
  • Scope: internal
  • Date: 2026-07-05
  • 审查范围: v7/bin/webnovel-writer.js、v7/src/commands/(48 个文件)、v7/src/installer/、v7/src/host-shells/、v7/skills/webnovel-writer/SKILL.md、v7/roles/、v7/test/commands/_helper.js、test/fixtures/、scripts/pack-install-e2e.mjs

每条格式:[severity] file:line 一句话问题 | 怀疑理由 | 建议探针 | 置信


候选清单(9 条)

G-2 [P1] migrate 名册类型「角色」vs 系统过滤「character」——迁移书角色实体全不可见

src/migrate/transform.js:117 + src/cache/rebuilder.js:257

  • 问题: migrate 把 v6 的 e.type(中文「角色」)原样写进名册「类型」列;rebuilder const type = row.类型 || 'character' 原样入 entities.type,而 review 名册 / list-characters / report-book-stats / book-status 全部按 WHERE type = 'character'(英文)过滤 → 迁移书的角色以 type='角色' 入库,对上述四处全部不可见。
  • 怀疑理由: read-v6.js:85 type: r.type || '角色'transform.js:123 if (e.type !== '角色') continue、测试 _v6.js:51 INSERT entities VALUES('jiangyao','角色',...) 三处确证 migrate 子系统用「角色」;消费端 review/index.js:56storage/adapters/EntityReader.js:107commands/report-book-stats.js:10prep/book-status.js:25 全按 'character'rebuilder.js:38 scanEntities(plain INSERT,type=角色)先于 :47 scanCharacters,后者 :291 ON CONFLICT(id) DO UPDATE 未含 type → 角色卡的 'character' 不会覆盖已入库的「角色」。sample-book fixture 用英文 character 恰好掩盖此漂移;test/migrate/e2e.test.js 只查 COUNT(*) FROM chapters 与 next 序号,从不查 entities.type / list-characters。
  • 建议探针: 迁移 tempV6SqliteSELECT type FROM entities WHERE id='江遥'(预期 '角色');再进程内跑 list-characters(预期空数组)、report-book-stats(预期角色数 0)。
  • 置信: 高

G-1 [P2/掩盖] gitBookCtx 建的仓库与真实建书结构不同(无 .gitignore / 未设 core.quotepath / 工作区被 git 跟踪)

test/commands/_helper.js:77-86

  • 问题: gitBookCtx = tempBookCtx(整拷 fixture,无 .gitignore)+ git init + git add -A + commit。真实建书 persistCreateBookstate-machine/persist.js:60,66)写 .gitignore(含 .cache/工作区/)、git.setQuotepathFalse()、只 git.add(written)(仅 book.yaml/大纲/AGENTS/.gitignore)。→ 测试仓库把 工作区/细纲.md 纳入 git 跟踪、无 core.quotepath;真实仓库 工作区/ 未跟踪。
  • 怀疑理由: 历史 P0「手动 git init 掩盖真实路径」同型。goto-chapterreset --hardstate-machine/flows/goto-chapter.js:57)回退全部已跟踪文件——测试仓库(工作区被跟踪)会删/回退 工作区草稿,真实仓库(工作区未跟踪)保留,行为相反;M6「批次进行中 × goto-chapter」交叉的关键不变量(待定稿批次在 工作区/ 应免于 reset)无法用此脚手架如实验证。finalize 工作区清理、relink 的 status 前缀过滤在两种仓库形态语义不同。
  • 建议探针: persistCreateBook 真实建书 vs gitBookCtx 各建一仓,git ls-files | grep 工作区 对比;在两种仓库跑 goto-chapter --confirm 后查 工作区/细纲.md 是否仍在。
  • 置信: 高(结构差异已确证;对具体断言的掩盖需探针)

G-6 [P2] M6 六命令 + M7 export/migrate 无 spawn-bin 覆盖,只有进程内调用

test/integration/cli-main-loop.test.js:19 + scripts/pack-install-e2e.mjs

  • 问题: stage-chapter / batch-status / finalize-batch / batch-reject / batch-restage / batch-discard / export / migrate 全部只有「进程内 run()/模块函数」测试,无一经真 bin 子进程 spawn → bin 层的 scope 解析、参数解析、cache.close() finally、exitCode 约定对这 8 命令未端到端验证。
  • 怀疑理由: grep spawn|execFile.*webnovel-writer|bin/webnovel-writer 仅命中 finalize(git)/installer/cli-main-loop;test/migrate/e2e.test.js:8-9migrateV6/migrateCmd 进程内;cli-main-loop 只覆盖 M2-M5 主循环(next/persist-book/list-books/prepare/mechanical-check/review-input/save-review/finalize/switch-book);pack-install-e2e 只覆盖 init/persist-book/next/update。
  • 建议探针: 给某个 M6/M7 命令加一条 execFile(BIN,[...]) 冒烟,验 scope='book'(export/batch)与 scope='workdir'(migrate)在真实 cwd 下解析 + exitCode。
  • 置信: 高

G-4 [P2] finalize 工作区清理对 workspaceFiles 不挡 ..,与 stage-chapter 守卫不一致

src/finalize/index.js:142-144 + src/commands/finalize.js:20-24

  • 问题: finalize 只剥 工作区/ 前缀(finalize.js:21 replace(/^工作区[\\/]/,'')),不挡 ..finalizeChapter 直接 fs.rm(path.join(repoPath,'工作区',wf))wf='../定稿/正文/0001-x.md' 会逃逸删到工作区外。stage-chapter(staging/index.js:331 if (!name.includes('..')))对同类清理有守卫,两路不一致。
  • 怀疑理由: 清理在 commit 后跑,逃逸删定稿文件会被 git 记为 deleted(可 checkout 恢复),但属越界删除;payload 为作者/AI 可控。
  • 建议探针: finalize payload 传 workspaceFiles:['../定稿/正文/0001-...md'],观察该定稿文件是否被删(git status 出现 deleted)。
  • 置信: 中

G-3 [S] 名册「类型」列约定缺失——fixture 用英文 character、无角色/SKILL 指引规定该填什么

test/fixtures/sample-book/定稿/设定/名册.md:3-4 + SKILL.md:39

  • 问题: fixture 名册 类型列填英文 character(作者面向中文文件里的机器味,历史模式10),系统内部 entities.type 词表确为英文 'character';但 grep roles/ 无名册类型指引、SKILL.md rosterUpserts 未定义「类型」取值 → 手写书由 AI 自由填,若循全中文基调填「角色」则同样触发 G-2 的不可见问题。EntityWriter.upsertRosterRow 原样写入不校验。
  • 怀疑理由: EntityWriter.js:8 ROSTER_HEADERS 含「类型」但无值域约束;消费端硬编码 'character'。
  • 建议探针: 核对是否存在约束 rosterUpserts.类型 的 spec/schema;构造 类型='角色' 的手写名册跑 list-characters 看是否空。
  • 置信: 中高

G-5 [S/P2] docs/migration-guide.md 既不发布也不 vendored,用户不可达(非断链)

v7/docs/migration-guide.md + package.json:12-19 + src/installer/vendor.js:11

  • 问题: 面向作者的 v6→v7 迁移指引不在 npm files 白名单(['bin/','src/','roles/','skills/','adapters/','templates/'],无 docs/),也不在 vendored RUNTIME_ENTRIES(['bin','src','roles','package.json'])→ npx 装完、工作目录都拿不到;且 grep migration-guide 仅命中文件自身,无 SKILL/roles/migrate 输出引用(非断链)。migrate 输出指向的是 工作区/迁移报告.md(另一份,正常)。
  • 怀疑理由: 内容明确面向终端作者(「给用过 v6 的作者」、逐步操作),但产品内不可交付。
  • 建议探针: npm pack 解包看 docs/ 是否在 tar;确认该指引是否本就仅供仓库 README 引流。
  • 置信: 高(不发布已确证;是否算问题取决于设计意图)

G-9 [P2] bin 命令名直接拼路径 import,../ 可派发到 commands 目录外、报错人话降级

bin/webnovel-writer.js:106-108

  • 问题: commandPath = path.join(__dirname,'../src/commands',${command}.js) 后 import,command 含 ../ 经 path.join 归一后会 import 到 commands 外模块(如 ../runtime/locate → src/runtime/locate.js);因目标无 .run 导出,落到 catch 的「执行命令时出错:mod.run is not a function」而非「未知命令」,人话降级。
  • 怀疑理由: 无命令名白名单/正则校验。属操作者=用户本人的 CLI,非安全面;仅错误文案质量。
  • 建议探针: webnovel-writer ../runtime/locate 看报错文案与 exitCode(预期非「未知命令」)。
  • 置信: 中

G-7 [P2/低置信·跨D区] next 非 --json 路径直读 r.gitHealth.length,序1 可能未定义

src/commands/next.js:16

  • 问题: 非 --json 路径直接 r.gitHealth.fixed.length;若 determineNextState 在序1(空工作目录/无 book,allowNoBook 下 ctx.repoPath=null)返回不含 gitHealth 的对象,next(无 --json)会抛 "Cannot read length of undefined"。cli-main-loop 只测了空工作目录的 --json 路径(不碰 gitHealth)。
  • 怀疑理由: shell 无防御;序1 是否恒设 gitHealth 属状态机(D 区)职责。
  • 建议探针: 空 .webnovel/ 工作目录里跑 next(不带 --json),看是否抛未定义。
  • 置信: 低(取决于 determineNextState 返回形状)

G-8 [S/低] --help 硬编码「41 个读接口」计数无守卫,易漂移

bin/webnovel-writer.js:31

  • 问题: 精准读取接口(41 个,分布于 21 个命令) 是硬编码文案,与实际读接口数无测试绑定(历史模式7 双源)。命令清单本身三方无漂移(48 个命令文件全部在 --help;SKILL 引用为合理子集);「21 个命令」已核对准确(read/list/report 类命令恰 21)。
  • 怀疑理由: 数字硬编码。
  • 建议探针: 数 read/list/report 命令的 --flag 组合是否=41。
  • 置信: 低(仅文案)

复查在位(无新发现,历轮修复仍守)

位置 结论
bin cache.close() finally 覆盖 bin:150-152 cache 仅在 book/workdir-book 分支赋值,ensureReady 抛错前已赋值,finally 恒关;workdir/anywhere/no-book 分支不建 cache。在位
installer 新清单以旧为底防丢 installer/index.js:71 newFiles={...manifest.files} 在位
persistCreateBook git init+quotepath+ensureIdentity+随手 commit state-machine/persist.js:68-77 在位(P0-2)
薄壳契约「不碰 console/process」 grep commands/ `console. process.(exit|argv|cwd|stdout|stderr)`
init/update 将 report 映射为 output init.js:13/update.js:13 正确映射,报告可打印。无 G-hypothesis 的静默 bug
vendored 运行时含 migrate/export 新模块 vendor.js:11 RUNTIME_ENTRIES 含 'src' 整目录拷 天然含,migrate/export/staging 均在 .webnovel/src/
host-shells drift/条件块渲染 host-shells/generate.js:26-34 renderTemplate 处理 {{#if}}/{{#unless}}/{{var}};SKILL 新增自动模式/例外段无条件块(纯 {{cmd}} 插值),driftCheck 双跑字节比对确定性守住。无漂移
scope 分级逐命令 见下 无「该 workdir 却缺省成 book」误判

scope 核对(显式导出 8 个,其余缺省 'book')

  • anywhere: init(装 .webnovel/ 前要能跑)✓
  • workdir: list-books / update / switch-book / session-context / migrate(建新书,用 ctx.workdir)✓
  • workdir-or-book: persist-book(建书时书目录还不存在)✓
  • allowNoBook=true: next(空工作目录报序1)✓
  • 缺省 'book'(含 export / 全部 batch 命令 / finalize / goto-chapter / relink / health-check / impact / 全部 read/list/report)——均需书仓库;从工作目录启动经 resolveRunContext 解析当前书(mode='workdir-book' 带 repoPath+cache),从书仓库根启动 mode='book',两处皆可跑。无缺省成 book 却应 workdir 的命令

Caveats / Not Found

  • G-2 的实际用户影响面取决于 v6 数据的 entity type 取值,但 migrate 子系统(read-v6/transform/_v6 fixture)与缓存层的 '角色' vs 'character' 契约错配是代码级确定,非依赖数据。scanCharacters ON CONFLICT 不改 type 已在 rebuilder.js:289-298 逐行确认。
  • G-1 的「掩盖」需探针确认具体哪些断言被误绿;结构差异本身已确证。goto-chapter × 批次的深层语义正确性(批次建立在被 reset 章之上)属 E 区(staging/state-machine),此处只报脚手架无法如实建模。
  • G-7 属 D 区(determineNextState 返回形状),仅从 G 区薄壳视角标出,未深入状态机。
  • 未发现空壳/占位实现;未发现薄壳越权碰缓存生命周期;bin --version/--help/未知命令三路 exitCode 与错误人话(不带栈)均合规。