dsh-novel-write-engine 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ ### Changed
6
+
7
+ - **BREAKING:`novel_outline` 改为按标识 upsert**:`chapters` 按 `num`、`volumes` 按 `title` 合并;省略的卡保持不动,不再因“提交集合就是全集”而静默删除。删除必须显式 `remove` / `remove_volumes`,活动章、归档章、不存在的卡和已完成卷都会被拒绝。
8
+ - **可解释写入**:`novel_outline` 新增 `dry_run`(返回 `would_add`/`would_update`/`would_remove`,零写盘、零 journal)、`added`/`updated`/`removed` 变更回显、`outline/outline.deprecated.yaml` 一级回退副本;`novel_restore` 支持 `paths: ["outline/outline.yaml"]`、不要求 `chapter`。
9
+ - **工具契约**:持久集合的数组参数声明 upsert/replace/remove 语义;`novel_blueprint` 的卷列表仍是 replace,但删除卷必须显式 `confirm_remove:true`。DSH 的强制 JSON Schema 子集不接受未知 `x-semantics` 关键字,因此语义落在参数描述与 `src/tools/semantics.ts` 契约表中,并由 `tests/tool-contract.spec.ts` 强制。
10
+ - **文档**:`novel_outline` 描述明确“upsert by key”,P2 brief 增加“未提交卡保持”的提示,工具文档与升级说明同步。
11
+
12
+ ### Migration
13
+
14
+ - 老的**全量提交**调用在 upsert 下等价,无需改动。
15
+ - 老的**部分提交**以前会删掉未提交卡,现在只改提交的卡;这正是本次修正。
16
+ - 想清空计划卡:以前空数组可行,现在空数组是 no-op;必须显式 `remove: [3,4,...]`。数据格式不变,无需迁移文件。
17
+ - 同名卷会被新写入拒绝(upsert 以 `title` 为键,同名无法区分)。老项目只读加载仍兼容;若要提交 `volumes`/`remove_volumes`,先用 `novel_amend` 给其中一卷改名。
18
+
19
+ ### Tests
20
+
21
+ - 新增 `tests/024.spec.ts`(部分 upsert、显式删除、active/unknown/submitted/archived 拒绝、dry_run 零写入、deprecated、outline 恢复、volumes upsert/remove、blueprint confirm)与 `tests/tool-contract.spec.ts`。
22
+ - 质量:80 个测试文件 / 848 项测试全绿;覆盖率 93.97 / 89.14 / 98.00 / 93.97,逐文件门禁通过。
23
+
3
24
  ## 0.2.3
4
25
 
5
26
  ### Changed
package/README.md CHANGED
@@ -10,7 +10,9 @@
10
10
 
11
11
  0.2.3 把“账本说真话”补到旧项目:`report` 从 journal 回填旧章 effort,quality 每章只算最终评审并区分 `samples`/`attempts`;briefs 按当前章隔离;归档失败可直接修订;advisory 按章去重并带证据;面板 v5 改为书桌式工作台,项目选择器分离写作/待修复/失效;`check:host` 不再污染真实注册表。
12
12
 
13
- > **当前版本:0.2.3** · 中文界面 · MIT
13
+ 0.2.4 根治 `novel_outline` 的“省略即删除”:`chapters` 按 `num`、`volumes` 按 `title` 做 upsert,省略的卡保持不动;删除必须显式 `remove` / `remove_volumes`;新增 `dry_run`、`added`/`updated`/`removed` 回显、`outline.deprecated.yaml` 与 `novel_restore` 的 outline 恢复路径。这是有意的 BREAKING 语义修正:从“提交集合就是全集”变成“提交的是要改的对象”。
14
+
15
+ > **当前版本:0.2.4** · 中文界面 · MIT
14
16
  >
15
17
  > **宿主范围**:DSH `>=0.2.1-rc.2 <0.3.0-0`;Node `>=24.9.0`。
16
18
 
@@ -280,7 +282,7 @@ dsh plugin --profile web add dsh-novel-write-engine
280
282
  固定版本:
281
283
 
282
284
  ```bash
283
- dsh plugin --profile web add dsh-novel-write-engine@0.2.3
285
+ dsh plugin --profile web add dsh-novel-write-engine@0.2.4
284
286
  ```
285
287
 
286
288
  `dsh` 不在 PATH 时,用 `npx @deepseek-ai/dsh` 代替命令开头的 `dsh`。
@@ -335,13 +337,13 @@ corepack pnpm pack
335
337
  会得到类似:
336
338
 
337
339
  ```text
338
- dsh-novel-write-engine-0.2.3.tgz
340
+ dsh-novel-write-engine-0.2.4.tgz
339
341
  ```
340
342
 
341
343
  把这个 tarball 复制到目标机器,放在一个**稳定、不会随手删除**的路径,然后:
342
344
 
343
345
  ```bash
344
- dsh plugin --profile web add /absolute/path/to/dsh-novel-write-engine-0.2.3.tgz
346
+ dsh plugin --profile web add /absolute/path/to/dsh-novel-write-engine-0.2.4.tgz
345
347
  ```
346
348
 
347
349
  tarball 安装不会运行源码构建,也不会要求 `allowBuilds`;包内已经包含 `lib/`、`rulepacks/`、`briefs/`、`skills/`、`presets/` 和两层 patch。它是最接近「发布产物」的安装方式。
@@ -356,9 +358,9 @@ tarball 安装不会运行源码构建,也不会要求 `allowBuilds`;包内
356
358
  2. 点 **添加插件**(或旁边的箭头 → **安装第三方插件**)。
357
359
  3. 在包名字段填入以下任一形式:
358
360
  - npm 包名:`dsh-novel-write-engine`
359
- - 精确版本:`dsh-novel-write-engine@0.2.3`
361
+ - 精确版本:`dsh-novel-write-engine@0.2.4`
360
362
  - 源码目录:`/absolute/path/to/dsh-novel-write-engine`
361
- - tarball:`/absolute/path/to/dsh-novel-write-engine-0.2.3.tgz`
363
+ - tarball:`/absolute/path/to/dsh-novel-write-engine-0.2.4.tgz`
362
364
  4. npm 包名从 npm 官方源安装;本地目录和 tarball 直接使用宿主路径。
363
365
  5. 点 **安装**,等待完成。
364
366
  6. 如果出现 **立即启用**,点它;否则安装后到「已安装插件」里把 **dsh-novel-write-engine** 打开。
@@ -430,7 +432,7 @@ dsh --profile web --dump-config | grep -A8 '# == dsh-novel-write-engine'
430
432
  先做全书蓝图、第一卷大纲和世界观设定,再写第一章的世界观约束和章节计划。
431
433
  ```
432
434
 
433
- 模型会调用 `novel_init` 建项目,然后按 `novel_guide` 的提示做书级规划;也可以跳过蓝图直接写第一章,严格大纲是可选开关。0.2.3 推荐顺序:
435
+ 模型会调用 `novel_init` 建项目,然后按 `novel_guide` 的提示做书级规划;也可以跳过蓝图直接写第一章,严格大纲是可选开关。0.2.4 推荐顺序:
434
436
 
435
437
  ```text
436
438
  novel_init
@@ -546,7 +548,7 @@ novel_init
546
548
  | `novel_audit` | 任意 | 读追加式 journal 审计记录,含被拒 attempt;支持 chapter/op/status/since/until/cursor | 否 |
547
549
  | `novel_pipeline` | 任意 | 「下一步该做什么」的建议 | 否 |
548
550
  | `novel_blueprint` | 任意 | 写书级蓝图:梗概、主题、基调、视角、读者、卖点、结局、卷结构 | 是 |
549
- | `novel_outline` | 任意 | 写卷/章卡;严格模式、归档卡保护、归档章自动回填 | 是 |
551
+ | `novel_outline` | 任意 | 按 `num` upsert 章卡、按 `title` upsert 卷;省略不删,`remove`/`remove_volumes` 显式删除,`dry_run` 预览 | 是 |
550
552
  | `novel_outline_import` | 任意 | 从 Markdown / YAML / JSON 导入大纲;`dry_run` 先预览 | 可选 |
551
553
  | `novel_worldbook` | 任意 | 增量更新世界观设定集(设定/规则/势力/地点/角色/术语/前史) | 是 |
552
554
  | `novel_search` | 任意 | 跨正文、摘要、大纲、世界观、台账搜索,带章号/段落号/片段 | 否 |
@@ -736,7 +738,8 @@ novel_blueprint {premise, logline, themes, tone, pov, audience,
736
738
  selling_points, ending, strict, volumes}
737
739
 
738
740
  # 2. 章卡;或从 Markdown/YAML/JSON 导入
739
- novel_outline {chapters}
741
+ novel_outline {chapters: [{num: 1, ...}], volumes: [...]}
742
+ # 删除:novel_outline {remove: [2]};预览:加 dry_run:true
740
743
  novel_outline_import {format:"markdown", content:"...", dry_run:true}
741
744
 
742
745
  # 3. 世界观设定集(按段替换)
@@ -927,6 +930,17 @@ corepack pnpm test
927
930
 
928
931
  ## 十一、升级
929
932
 
933
+ ### 11.0 从 0.2.3 升级到 0.2.4
934
+
935
+ 0.2.4 不迁移 `outline.yaml` 数据,只反转 `novel_outline` 的写入口语义:
936
+
937
+ 1. `chapters` 按 `num`、`volumes` 按 `title` upsert;省略的卡保持不动,不再被删除。
938
+ 2. 删除必须显式 `remove` / `remove_volumes`;活动章、归档章、不存在的卡和已完成卷会被拒绝。
939
+ 3. `dry_run:true` 可预演 `would_add/would_update/would_remove`;成功返回 `added/updated/removed`。
940
+ 4. `novel_outline` 正常写入会保留 `outline/outline.deprecated.yaml`;`novel_restore {txid, paths:["outline/outline.yaml"]}` 可恢复整份大纲(要求与当前章索引一致)。
941
+ 5. `novel_blueprint` 的卷列表仍是 replace;删卷必须 `confirm_remove:true`。
942
+ 6. 同名卷会被新写入拒绝;老项目若已有同名卷,先用 `novel_amend` 改名再提交 volumes(只读加载仍兼容)。
943
+
930
944
  ### 11.0 从 0.2.2 升级到 0.2.3
931
945
 
932
946
  0.2.3 不迁移项目数据,只改变读模型、面板 wire 版本与项目卫生:
@@ -1008,7 +1022,7 @@ dsh plugin --profile web add dsh-novel-write-engine@latest
1008
1022
  也可以固定版本:
1009
1023
 
1010
1024
  ```bash
1011
- dsh plugin --profile web add dsh-novel-write-engine@0.2.3
1025
+ dsh plugin --profile web add dsh-novel-write-engine@0.2.4
1012
1026
  ```
1013
1027
 
1014
1028
  升级后核对:
@@ -1050,7 +1064,7 @@ corepack pnpm pack
1050
1064
 
1051
1065
  ```bash
1052
1066
  dsh plugin --profile web remove dsh-novel-write-engine
1053
- dsh plugin --profile web add /absolute/path/to/dsh-novel-write-engine-0.2.3.tgz
1067
+ dsh plugin --profile web add /absolute/path/to/dsh-novel-write-engine-0.2.4.tgz
1054
1068
  ```
1055
1069
 
1056
1070
  ### 11.5 在 Web / Desktop 里升级
@@ -1318,6 +1332,14 @@ tests/ # 单测、集成、falsify、e2e
1318
1332
 
1319
1333
  ### 16.5 当前验证基线
1320
1334
 
1335
+ 0.2.4 在 Node 24.19 / DSH 0.2.1-rc.3 上完成:
1336
+
1337
+ - `npm test` 全绿:**80 个测试文件 / 848 项**;
1338
+ - 覆盖率:**语句 93.97% / 分支 89.14% / 函数 98.00% / 行 93.97%**;`test:coverage` 与逐文件 domain/book/insight/viewmodel 门禁全过;
1339
+ - `typecheck`、`lint`、`build` 通过;`check:hygiene`(143 files,0 v8 ignore)、`test:isolated`(60 源文件)、`validate:rulepack`、`check:pack`(25 files)、`check:artifacts`、`check:install`(26 packages)、`check:preset`(13 packages)、`check:host`、`check:crash` 全绿;
1340
+ - 0.2.4 outline 契约、dry_run、deprecated、outline restore、volumes upsert/remove 与 blueprint confirm 见 [0.2.4 验收](docs/0.2.4-acceptance.md);
1341
+ - 真实模型链路:`agnes-2.5-flash` 与 `agnes-3.0-flash` 在 0.2.4 临时项目完成 P1→P8、归档、report/doctor/panel v5、六种导出、`novel_reopen` 与重新归档;详见 [0.2.4 真实模型证据](docs/0.2.4-real-model-evidence.md)。
1342
+
1321
1343
  0.2.3 在 Node 24.19 / DSH 0.2.1-rc.3 上完成:
1322
1344
 
1323
1345
  - `npm test` 全绿:**78 个测试文件 / 836 项**;
@@ -1339,6 +1361,9 @@ tests/ # 单测、集成、falsify、e2e
1339
1361
  | [DESIGN.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/DESIGN.md) | 完整设计书:架构、数据模型、规则、门控、事务、面板、预设 |
1340
1362
  | [PLAN.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/PLAN.md) | 0.1.0 实施计划(历史) |
1341
1363
  | [docs/0.2.0-plan.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/docs/0.2.0-plan.md) | 0.2.0 整书生命周期:蓝图/大纲/世界观/章卡/观测 |
1364
+ | [docs/0.2.4-plan.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/docs/0.2.4-plan.md) | 0.2.4 outline upsert、显式删除、dry_run 与恢复路径的根治方案 |
1365
+ | [docs/0.2.4-acceptance.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/docs/0.2.4-acceptance.md) | 0.2.4 验收矩阵、质量门与边界 |
1366
+ | [docs/0.2.4-real-model-evidence.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/docs/0.2.4-real-model-evidence.md) | 0.2.4 两条真实模型 P1→P8 链路回执 |
1342
1367
  | [docs/0.2.3-plan.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/docs/0.2.3-plan.md) | 0.2.3 可信账本、工作流、advisory、面板 v5 与项目卫生 |
1343
1368
  | [docs/0.2.3-acceptance.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/docs/0.2.3-acceptance.md) | 0.2.3 验收矩阵、真实项目回归与质量门证据 |
1344
1369
  | [docs/0.2.2-plan.md](https://github.com/ThzxxArt/dsh-novel-write-engine/blob/main/docs/0.2.2-plan.md) | 0.2.2 会话级项目选择、scan/resume、面板 v4 与完整卷 |
package/briefs/P2.md CHANGED
@@ -8,6 +8,7 @@
8
8
  - 上一章结尾(`state/handoff.yaml` 的 `last_summary`)。
9
9
  - 未回收伏笔:`novel_state foreshadows`。
10
10
  - 当前章卡:`novel_state {"kind":"outline"}`(`context.outline_card` 已带入 brief)。只要存在章卡,标题、场景类型、目标字数区间与 cast 就必须一致,否则 PG-13 会拒绝;严格模式还要求章卡必须存在。
11
+ - 更新章卡时用 `novel_outline` 的 upsert:只提交要改的 `num`,省略的章卡不会被删除;删除必须显式 `remove`。不要靠重交全量卡来“确保存在”。
11
12
  - 世界观相关条目:`context.worldbook`。
12
13
 
13
14
  ## 产出
package/lib/client.js CHANGED
@@ -1171,7 +1171,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1171
1171
  如果当前会话还没绑定或不确定是哪本书:先用 /novel current 看会话绑定,再调用 novel_status 看 selection;需要切换时用 /novel use <路径|书名|.>,不要靠 active 猜。
1172
1172
  第一次调用带 project_root 时,它会成为当前会话的持久绑定;后续无 root 调用会继续这本书。
1173
1173
 
1174
- 如果下一章还没有章卡:先用 novel_state {kind:"outline"} 取出全部章卡,再在末尾加上下一章的卡,用 novel_outline 整体提交;不要只提交新卡,也不要把已有 planned 卡丢掉。
1174
+ 如果下一章还没有章卡:用 novel_outline 只提交下一章的卡(按 num upsert),省略的已有卡会保持不动;不要重交全量,也不要用空数组清空。要删卡必须显式传 remove。
1175
1175
  如果 targets.total_chapters 已经写满:先用 novel_amend 把 total_chapters 调到至少包含下一章,不能低于已归档章数。
1176
1176
 
1177
1177
  之后严格按:novel_brief P1 → novel_plan_world → brief P2 → novel_plan_chapter → brief P3 → novel_write_draft → brief P4 → novel_review →(有 blocker)brief P5 → novel_revise → 回 P4 → brief P6 → novel_check → brief P8 → novel_archive 的顺序推进。
@@ -1457,7 +1457,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1457
1457
  ref: closeRef,
1458
1458
  onClick: onClose,
1459
1459
  "aria-label": "关闭使用教程"
1460
- }, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "续写下一章时,直接复制这段给 Agent"), (0, react.createElement)("p", null, "这段会先确认状态,再处理缺章卡和目标章数已满,最后按 P1→P8 推进;归档前会要求你确认 state_delta。"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_NEXT_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, "教程基于 0.2.3 · 具体字段以当前任务书和工具返回为准 · 面板只读")));
1460
+ }, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "续写下一章时,直接复制这段给 Agent"), (0, react.createElement)("p", null, "这段会先确认状态,再处理缺章卡和目标章数已满,最后按 P1→P8 推进;归档前会要求你确认 state_delta。"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_NEXT_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, "教程基于 0.2.4 · 具体字段以当前任务书和工具返回为准 · 面板只读")));
1461
1461
  const NovelHelp = () => {
1462
1462
  const [open, setOpen] = (0, react.useState)(false);
1463
1463
  const triggerRef = (0, react.useRef)(null);
package/lib/index.d.ts CHANGED
@@ -1296,8 +1296,14 @@ interface OutlineChapterInput {
1296
1296
  interface OutlineInput {
1297
1297
  book?: BookPatch;
1298
1298
  volumes?: VolumeInput[];
1299
- chapters: OutlineChapterInput[];
1299
+ chapters?: OutlineChapterInput[];
1300
1300
  strict?: boolean;
1301
+ /** Explicit deletion list; omitted cards are preserved. */
1302
+ remove?: number[];
1303
+ /** Explicit volume deletion list, keyed by title. */
1304
+ remove_volumes?: string[];
1305
+ /** Preview changes without writing or journaling. */
1306
+ dry_run?: boolean;
1301
1307
  }
1302
1308
  interface WorldbookPatch {
1303
1309
  premise?: string;
@@ -1808,7 +1814,7 @@ interface AmendSubmission {
1808
1814
  interface RestoreSubmission {
1809
1815
  txid: string;
1810
1816
  chapter?: number;
1811
- /** Restrict to these project-relative files; omitted restores every chapter file in the snapshot. */
1817
+ /** Restrict to chapter files, or exactly [outline/outline.yaml] to restore the plan. */
1812
1818
  paths?: string[];
1813
1819
  root?: string;
1814
1820
  }
@@ -1857,8 +1863,27 @@ interface WordCountInput {
1857
1863
  }
1858
1864
  interface BookPlanSubmission extends BookPatch {
1859
1865
  volumes?: VolumeInput[];
1866
+ /** Explicit confirmation when a volume-list replacement drops a volume. */
1867
+ confirm_remove?: boolean;
1860
1868
  root?: string;
1861
1869
  }
1870
+ interface OutlineMutationResult {
1871
+ planned: number;
1872
+ archived: number;
1873
+ total: number;
1874
+ strict: boolean;
1875
+ volumes: number;
1876
+ added: number[];
1877
+ updated: number[];
1878
+ removed: number[];
1879
+ volumes_added: string[];
1880
+ volumes_updated: string[];
1881
+ volumes_removed: string[];
1882
+ dry_run?: boolean;
1883
+ would_add?: number[];
1884
+ would_update?: number[];
1885
+ would_remove?: number[];
1886
+ }
1862
1887
  interface OutlineSubmission extends OutlineInput {
1863
1888
  root?: string;
1864
1889
  }
@@ -1991,13 +2016,8 @@ declare class EngineService {
1991
2016
  volumes: number;
1992
2017
  chapters: number;
1993
2018
  }>>;
1994
- submitOutline(input: OutlineSubmission): Promise<OpResult<{
1995
- planned: number;
1996
- archived: number;
1997
- total: number;
1998
- strict: boolean;
1999
- volumes: number;
2000
- }>>;
2019
+ private outlinePlanChange;
2020
+ submitOutline(input: OutlineSubmission): Promise<OpResult<OutlineMutationResult>>;
2001
2021
  importOutline(input: OutlineImportSubmission): Promise<OpResult<{
2002
2022
  format: string;
2003
2023
  planned: number;
@@ -2301,10 +2321,8 @@ declare class EngineService {
2301
2321
  */
2302
2322
  private assertPlanningDocumentReadable;
2303
2323
  /**
2304
- * A strict outline cannot silently lose the active chapter's card while the
2305
- * chapter is still being written. Otherwise a late replacement turns a
2306
- * strict project non-strict in effect and archive would auto-create the card
2307
- * the author was required to provide.
2324
+ * An import is a full replacement, unlike novel_outline's keyed upsert. It
2325
+ * must not silently lose the active card while strict mode still requires it.
2308
2326
  */
2309
2327
  private assertActiveCardPreserved;
2310
2328
  /**
@@ -2420,6 +2438,8 @@ declare const PANEL_CHAPTER_LIMIT = 30;
2420
2438
  /** Project-relative locations. Every write goes through these constants. */
2421
2439
  declare const PROJECT_FILE = "novel.yaml";
2422
2440
  declare const OUTLINE_FILE = "outline/outline.yaml";
2441
+ /** Last outline replaced by a normal planning write; one level is enough for repair. */
2442
+ declare const OUTLINE_DEPRECATED_FILE = "outline/outline.deprecated.yaml";
2423
2443
  declare const WORLDBOOK_FILE = "outline/worldbook.yaml";
2424
2444
  declare const STATE_DIR = "state";
2425
2445
  declare const CHAPTERS_DIR = "chapters";
@@ -2545,4 +2565,4 @@ declare const Config: Schema<Config>;
2545
2565
  * created lazily per project root.
2546
2566
  */
2547
2567
  declare function apply(ctx: Context, config: Config): void;
2548
- export { ARCHIVE_DIGESTS_FILE, AdviceReport, ArchiveHandoff, AuditRow, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BlockingItem, BookPlan, BookReport, BriefDocument, BriefLibrary, BriefReceipt, BriefReceiptsDocument, BriefStage, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, ChapterCastMember, ChapterPlanInput, ChapterRecord, ChapterStatusRow, ChapterSummary, ChaptersDocument, CharacterDelta, CharactersDocument, CheckDocument, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, Deduction, DeltaAction, DiffReport, DoctorCheck, DoctorReport, DraftInput, DraftState, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, ErrorCode, ExportChapter, ExportKind, ForeshadowDelta, ForeshadowsDocument, GENRES, GateInput, GateOptions, GateResult, Genre, GuideReport, GuideStep, HISTORY_DIR, HISTORY_KINDS, HandoffDocument, HistoryKind, INITIAL_STAGE, LIMITS, LOCK_FILE, LocksDocument, METADATA_LOCK_FILE, MachineMetrics, NovelProject, OPERATIONS, OUTLINE_DIR, OUTLINE_FILE, OpResult, Operation, OutlineChapter, OutlineDocument, OutlineProgress, OutlineVolume, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, PanelBook, PanelBundle, PanelChapterRow, PanelChaptersDto, PanelDraft, PanelGatesDto, PanelMissing, PanelProjectOption, PanelSelection, PanelSnapshot, PanelSummaryDto, PipelineState, Platform, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, ReadingProof, ReadingProofReference, RelationshipDelta, RelationshipsDocument, ResumePromptInput, ReviewDocument, ReviewInput, ReviewIssue, RevisionEdit, RevisionInput, RevisionPatch, RoundCounters, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, SceneType, ScoreDimension, SearchReport, SearchResultItem, Selection, SelectionSource, SemanticItem, Severity, Stage, StateDelta, StateError, StateErrorCode, StatusReport, TOOL_NAMES, TimelineDelta, TimelineDocument, ToolName, Violation, WORLDBOOK_FILE, Warning, WordMetrics, WorldDelta, WorldDocument, WorldPlanInput, WorldbookBackstory, WorldbookCharacter, WorldbookDocument, WorldbookFact, WorldbookFaction, WorldbookGlossary, WorldbookLocation, WorldbookRule, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
2568
+ export { ARCHIVE_DIGESTS_FILE, AdviceReport, ArchiveHandoff, AuditRow, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BlockingItem, BookPlan, BookReport, BriefDocument, BriefLibrary, BriefReceipt, BriefReceiptsDocument, BriefStage, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, ChapterCastMember, ChapterPlanInput, ChapterRecord, ChapterStatusRow, ChapterSummary, ChaptersDocument, CharacterDelta, CharactersDocument, CheckDocument, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, Deduction, DeltaAction, DiffReport, DoctorCheck, DoctorReport, DraftInput, DraftState, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, ErrorCode, ExportChapter, ExportKind, ForeshadowDelta, ForeshadowsDocument, GENRES, GateInput, GateOptions, GateResult, Genre, GuideReport, GuideStep, HISTORY_DIR, HISTORY_KINDS, HandoffDocument, HistoryKind, INITIAL_STAGE, LIMITS, LOCK_FILE, LocksDocument, METADATA_LOCK_FILE, MachineMetrics, NovelProject, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, OpResult, Operation, OutlineChapter, OutlineDocument, OutlineProgress, OutlineVolume, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, PanelBook, PanelBundle, PanelChapterRow, PanelChaptersDto, PanelDraft, PanelGatesDto, PanelMissing, PanelProjectOption, PanelSelection, PanelSnapshot, PanelSummaryDto, PipelineState, Platform, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, ReadingProof, ReadingProofReference, RelationshipDelta, RelationshipsDocument, ResumePromptInput, ReviewDocument, ReviewInput, ReviewIssue, RevisionEdit, RevisionInput, RevisionPatch, RoundCounters, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, SceneType, ScoreDimension, SearchReport, SearchResultItem, Selection, SelectionSource, SemanticItem, Severity, Stage, StateDelta, StateError, StateErrorCode, StatusReport, TOOL_NAMES, TimelineDelta, TimelineDocument, ToolName, Violation, WORLDBOOK_FILE, Warning, WordMetrics, WorldDelta, WorldDocument, WorldPlanInput, WorldbookBackstory, WorldbookCharacter, WorldbookDocument, WorldbookFact, WorldbookFaction, WorldbookGlossary, WorldbookLocation, WorldbookRule, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };