dsh-novel-write-engine 0.1.2 → 0.1.3

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/briefs/P2.md CHANGED
@@ -14,13 +14,16 @@
14
14
 
15
15
  | 字段 | 要求 |
16
16
  |---|---|
17
- | `title` | 不带「第N章」前缀,4~8 字;引擎会拼前缀并校验 |
17
+ | `title` | 不带「第N章」前缀;长度读 rulepack 的 `format.title.min_chars/max_chars`,引擎会拼前缀并校验 |
18
18
  | `scene_type` | 四选一:战斗/动作章、日常过渡章、情感爆发章、纯叙事章 |
19
19
  | `narrator` | 视角人物,必须在本章出场;第一人称写成 `陈默(第一人称「我」)`,引擎接受括号/引号里的正文别名 |
20
20
  | `beats` | 5~8 个节拍,每个是一句话的可检验动作 |
21
21
  | `opening` | 前两段打算怎么写(场景 + 动作 + 一个钩子) |
22
22
  | `ending` | 断章点,必须是新信息或新麻烦 |
23
23
  | `target_words` | `[{{min_words}}, {{max_words}}]` |
24
+ | `cast` | 可选,1–8 个本章真正上场的人物:`{name,entry_state?,goal?,knowledge?,exit_change?}`;name 必须来自 P1 世界计划或角色台账 |
25
+
26
+ `cast` 不是出场名单的重复:它告诉 P3 这一章谁在舞台上、各自带着什么目标与信息差。没有 cast 时,引擎会从计划正文与人物台账推断;新计划建议显式给出,避免 120 个角色里裁掉真正上场的人。
24
27
 
25
28
  ## 场景类型纪律
26
29
 
package/briefs/P3.md CHANGED
@@ -10,6 +10,10 @@
10
10
  - 不写 Markdown:禁 `#`、`**`、`---`、`>`、表格。
11
11
  - 不写「Day 1」「第X卷」「读者」「作者」这类出戏标签。
12
12
 
13
+ ## 上下文
14
+
15
+ P3 brief 的 `context.world_plan` 是 P1 的本章约束;`context.characters` 是本章活跃角色的完整档案,`context.other_characters` 只是压缩索引。先读 `world_plan` 与 `characters` 再落笔;不要在正文里临时发明未申报的世界规则或人物。
16
+
13
17
  ## 写法
14
18
 
15
19
  1. 一段一个信息点。先动作,后反应,最后才允许一句心理。
package/briefs/P4.md CHANGED
@@ -72,6 +72,6 @@
72
72
  - 每条 issue 写清 `problem` 与 `fix`,fix 要是可执行动作。
73
73
  - `dimension` 必须是本任务书十五维之一(`R-W-01`…`R-W-15` 的实际集合);`location` 必须是「第 N 段」,且 N 在本章实际段落范围内。
74
74
  - `blocker` 不能在 P4 或 P5 标记为 `waived`;只能修好后带正文证据标记 `fixed`。
75
- - 判断不了是否「作者故意」时,先按普通问题处理;0.1.2 不提供自动豁免。
75
+ - 判断不了是否「作者故意」时,先按普通问题处理;0.1.3 不提供自动豁免。
76
76
 
77
77
  完成后:仍有 blocker → P5 修订;全部清零 → P6 校验。
package/briefs/P6.md CHANGED
@@ -33,13 +33,13 @@
33
33
 
34
34
  ## 判定
35
35
 
36
- - 调用时可以传 `scope`:默认 `["content","integrity"]`;只跑 `content` 或只跑 `integrity` 是合法的,空数组/未知值会被拒绝。只跑一类时另一类规则不参与本次 check,但归档仍会重新执行全量机检。
36
+ - 调用时可以传 `scope`:默认 `["content","integrity"]`;只跑 `content` 或只跑 `integrity` 是合法的,空数组/未知值会被拒绝。只跑一类时另一类规则不参与本次 check,但**不会**进入 `ready_to_archive`:管线停在 P6 并返回 `CHECK_SCOPE_PARTIAL`,必须再跑一次完整 scope。
37
+ - 完整 scope 全过 → 管线进入 `ready_to_archive`,先 `novel_brief P8` 领取终审任务书,再调用 `novel_archive`。
37
38
  - 任一项 `fail` → 校验不过,先重新领取 `novel_brief P6`(check 已改变上下文,旧 digest 会失效),再调用 `novel_revise` 修订并回到 P4 重评(轮次 +1)。
38
- - 全过 → 管线进入 `ready_to_archive`,先 `novel_brief P8` 领取终审任务书,再调用 `novel_archive`。
39
39
 
40
40
  ## 损坏与恢复
41
41
 
42
- - 如果 `novel_status` 返回 `state_errors` 或 `STATE_UNREADABLE`,不要手改文件;按 `next` 指向的 `novel_rollback` 回到 P6,重新取 P6 任务书并 `novel_check`。
42
+ - 如果 `novel_status` 返回 `state_errors` 或 `STATE_UNREADABLE`,不要手改文件;plan/review/check 按 `next` 指向的 `novel_rollback` 回到重建点;八个 ledger 则按 `next` 调 `novel_amend`,先用 `novel_state` 取 raw 文档再提交完整替换。
43
43
  - 如果 `novel_archive` 报 review 没有 `review_digest`,回退到 P4 重新 `novel_review`;不要编辑 `state/pipeline.yaml`。
44
44
 
45
45
  ## 不许做的
package/lib/client.js CHANGED
@@ -102,6 +102,10 @@ window.__ModuleLoader__.load({
102
102
  const stageLabel = (stage) => STAGE_LABELS[stage] ?? stage;
103
103
  const platformLabel = (platform) => PLATFORM_LABELS[platform] ?? platform;
104
104
  const toolLabel = (tool) => TOOL_LABELS[tool] ?? "继续";
105
+ const volumeLabel = (summary) => {
106
+ if (summary.volume === void 0) return "";
107
+ return ` · ${summary.volume_name === void 0 ? `第${summary.volume}卷` : summary.volume_name}${summary.act === void 0 ? "" : ` · ${summary.act}${summary.act_progress === void 0 ? "" : ` ${summary.act_progress}`}`}`;
108
+ };
105
109
  /**
106
110
  * Project the wire payload into strings. Version mismatch wins over everything
107
111
  * else: a stale panel must never render data from another schema as if it were
@@ -146,14 +150,15 @@ window.__ModuleLoader__.load({
146
150
  };
147
151
  });
148
152
  const scored = rows.filter((row) => row.tone === "ok");
149
- const average = scored.length === 0 ? 0 : scored.reduce((sum, row) => sum + row.scoreValue, 0) / scored.length;
153
+ const rowAverage = scored.length === 0 ? 0 : scored.reduce((sum, row) => sum + row.scoreValue, 0) / scored.length;
154
+ const average = summary.averageScore !== void 0 && Number.isFinite(summary.averageScore) ? summary.averageScore : rowAverage;
150
155
  const next = summary.next;
151
156
  const finished = summary.finished === true;
152
157
  const doneLabel = summary.stage === "done" ? finished ? "全书完成" : "本章已归档" : stageLabel(summary.stage);
153
158
  return {
154
159
  state: "ready",
155
160
  title: `《${summary.title}》`,
156
- subtitle: summary.stage === "done" ? `${summary.genre} · ${platformLabel(summary.platform)} · ${finished ? `共 ${summary.totalChapters} 章` : `下一章 第 ${summary.chapter} 章`}` : `${summary.genre} · ${platformLabel(summary.platform)} · 第 ${summary.chapter} 章`,
161
+ subtitle: summary.stage === "done" ? `${summary.genre} · ${platformLabel(summary.platform)} · ${finished ? `共 ${summary.totalChapters} 章` : `下一章 第 ${summary.chapter} 章`}${volumeLabel(summary)}` : `${summary.genre} · ${platformLabel(summary.platform)} · 第 ${summary.chapter} 章${volumeLabel(summary)}`,
157
162
  stageLabel: doneLabel,
158
163
  finished,
159
164
  revisionLabel: `第 ${summary.revision} 次修订`,
@@ -577,7 +582,7 @@ window.__ModuleLoader__.load({
577
582
  }, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), "字"), stat("平均评分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, view.state === "loading" ? loadingState() : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready ? phases(view) : null, ready && view.nextTitle !== "" ? (0, react.createElement)("section", {
578
583
  className: "nwe-next",
579
584
  "aria-label": "下一步"
580
- }, (0, react.createElement)("div", { className: "nwe-next-icon" }, arrowIcon()), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-next-kicker" }, "NEXT MOVE"), (0, react.createElement)("div", { className: "nwe-next-title" }, view.nextTitle), (0, react.createElement)("div", { className: "nwe-next-detail" }, view.nextDetail)), view.nextTool === "" ? null : (0, react.createElement)("code", { className: "nwe-next-tool" }, view.nextTool)) : null, ready ? (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, `${view.rows.length} 条记录${view.rows.length > 30 ? " · 显示最近 30 条" : ""}`)), view.rows.length === 0 ? (0, react.createElement)("div", {
585
+ }, (0, react.createElement)("div", { className: "nwe-next-icon" }, arrowIcon()), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-next-kicker" }, "NEXT MOVE"), (0, react.createElement)("div", { className: "nwe-next-title" }, view.nextTitle), (0, react.createElement)("div", { className: "nwe-next-detail" }, view.nextDetail)), view.nextTool === "" ? null : (0, react.createElement)("code", { className: "nwe-next-tool" }, view.nextTool)) : null, ready ? (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, `${view.stats.chapters} 条记录${view.stats.chapters > view.rows.filter((row) => row.tone === "ok").length ? ` · 显示最近 ${view.rows.length} 条` : ""}`)), view.rows.length === 0 ? (0, react.createElement)("div", {
581
586
  className: "nwe-empty",
582
587
  style: {
583
588
  marginTop: 12,
package/lib/index.d.ts CHANGED
@@ -171,10 +171,12 @@ interface CheckDocument {
171
171
  };
172
172
  verdict: 'pass' | 'fail';
173
173
  }
174
+ /** Every persisted document whose damage status/panel can report. */
175
+ type StateErrorCode = 'PROJECT_UNREADABLE' | 'PLAN_UNREADABLE' | 'WORLD_PLAN_UNREADABLE' | 'REVIEW_UNREADABLE' | 'CHECK_UNREADABLE' | 'CHAPTERS_UNREADABLE' | 'CHARACTERS_UNREADABLE' | 'WORLD_UNREADABLE' | 'LOCKS_UNREADABLE' | 'TIMELINE_UNREADABLE' | 'FORESHADOWS_UNREADABLE' | 'RELATIONSHIPS_UNREADABLE' | 'HANDOFF_UNREADABLE';
174
176
  /** A state document that exists but cannot be read safely. */
175
177
  interface StateError {
176
178
  path: string;
177
- code: 'PLAN_UNREADABLE' | 'REVIEW_UNREADABLE' | 'CHECK_UNREADABLE';
179
+ code: StateErrorCode;
178
180
  message: string;
179
181
  }
180
182
  /** Word statistics with the single engine-wide counting rule. */
@@ -503,6 +505,14 @@ interface WorldPlanInput {
503
505
  }>;
504
506
  continuity_notes: string[];
505
507
  }
508
+ /** A character the chapter plan puts on stage; no new persisted schema. */
509
+ interface ChapterCastMember {
510
+ name: string;
511
+ entry_state?: string;
512
+ goal?: string;
513
+ knowledge?: string[];
514
+ exit_change?: string;
515
+ }
506
516
  /** Chapter plan (`plans/chNNN.chapter.yaml`). */
507
517
  interface ChapterPlanInput {
508
518
  chapter: number;
@@ -513,6 +523,8 @@ interface ChapterPlanInput {
513
523
  opening: string;
514
524
  ending: string;
515
525
  target_words: [number, number];
526
+ /** Optional in 0.1.3: older plans remain readable; new plans may name 1–8 actors. */
527
+ cast?: ChapterCastMember[];
516
528
  }
517
529
  /** Draft submission payload. */
518
530
  interface DraftInput {
@@ -616,9 +628,14 @@ interface StatusReport {
616
628
  stage: Stage;
617
629
  rounds: RoundCounters;
618
630
  blocking: BlockingItem[];
619
- /** plan/review/check files that exist but cannot be read safely. */
631
+ /** plan/review/check/ledger files that exist but cannot be read safely. */
620
632
  state_errors: StateError[];
621
633
  metrics: WordMetrics | null;
634
+ /** Inherited handoff labels; absent before the first archive. */
635
+ volume?: number;
636
+ volume_name?: string;
637
+ act?: string;
638
+ act_progress?: string;
622
639
  next: {
623
640
  tool: string;
624
641
  args: Record<string, unknown>;
@@ -874,6 +891,10 @@ interface PanelSnapshot {
874
891
  check: number;
875
892
  };
876
893
  blocking: BlockingItem[];
894
+ volume?: number;
895
+ volume_name?: string;
896
+ act?: string;
897
+ act_progress?: string;
877
898
  /** Recommended next action; lets the panel explain a stage with no blockers. */
878
899
  next?: StatusReport['next'];
879
900
  /** True only when the configured total chapter target has been reached. */
@@ -883,6 +904,8 @@ interface PanelSnapshot {
883
904
  draft?: PanelDraft | null;
884
905
  /** Total words across the index, for the header. */
885
906
  totalWords: number;
907
+ /** Book-wide archived average, not just the panel's capped rows. */
908
+ averageScore?: number;
886
909
  /** The nine-phase strip state, derived from the pipeline stage. */
887
910
  phases: Array<{
888
911
  id: string;
@@ -901,6 +924,12 @@ interface PanelSummaryDto {
901
924
  revision: number;
902
925
  totalWords: number;
903
926
  totalChapters: number;
927
+ /** Book-wide archived average; absent on older payloads. */
928
+ averageScore?: number;
929
+ volume?: number;
930
+ volume_name?: string;
931
+ act?: string;
932
+ act_progress?: string;
904
933
  phases: Array<{
905
934
  id: string;
906
935
  done: boolean;
@@ -1084,6 +1113,10 @@ interface ExportInput {
1084
1113
  interface StateQueryInput {
1085
1114
  kind: DocKind | 'novel' | 'planWorld' | 'planChapter';
1086
1115
  chapter?: number;
1116
+ /** Chapter index defaults to a compact read; opt in to the full summaries. */
1117
+ include_summary?: boolean;
1118
+ /** Inclusive chapter range for the `chapters` index. */
1119
+ chapter_range?: [number, number];
1087
1120
  root?: string;
1088
1121
  }
1089
1122
  interface WordCountInput {
@@ -1100,6 +1133,8 @@ declare class EngineService {
1100
1133
  private readonly stores;
1101
1134
  private readonly mutex;
1102
1135
  private readonly replay;
1136
+ /** Status, panel and gates ask for the same stage context repeatedly. */
1137
+ private readonly briefContextCache;
1103
1138
  private readonly briefs;
1104
1139
  private readonly now;
1105
1140
  /** The project this process last touched; the panel follows it without a workspace hint. */
@@ -1139,6 +1174,12 @@ declare class EngineService {
1139
1174
  chapter: number;
1140
1175
  stage: Stage;
1141
1176
  }>>;
1177
+ /**
1178
+ * Read-only paths must surface a committed-journal mismatch. The write path
1179
+ * already fails closed; without this, status recommends the next write and
1180
+ * the model only learns about the mismatch when that write aborts.
1181
+ */
1182
+ private journalBlocking;
1142
1183
  getStatus(input?: StatusInput): Promise<OpResult<StatusReport>>;
1143
1184
  countWords(input?: WordCountInput): Promise<OpResult<WordMetrics>>;
1144
1185
  queryState(input: StateQueryInput): Promise<OpResult<unknown>>;
@@ -1287,6 +1328,13 @@ declare class EngineService {
1287
1328
  applied: boolean;
1288
1329
  }>>;
1289
1330
  private panelSnapshot;
1331
+ /**
1332
+ * The archive stage has only one legal operation, so a status/panel that
1333
+ * recommends it without checking the same gates as the archive creates a
1334
+ * loop. Evaluate the real archive gates through `gateInput`, then add the
1335
+ * machine pass and semantic evidence that archive re-checks separately.
1336
+ */
1337
+ private archivePreflight;
1290
1338
  private nextStep;
1291
1339
  /** A computed blocker for the retry budget the next operation would consume. */
1292
1340
  private roundsLimitBlock;
@@ -1318,10 +1366,18 @@ declare class EngineService {
1318
1366
  private briefContextFor;
1319
1367
  private briefDigest;
1320
1368
  private briefVariables;
1369
+ /**
1370
+ * The one place a gate input is assembled. Archive preflight evaluates the
1371
+ * same object as the real archive, so a gate cannot pass preflight and then
1372
+ * fail the write (the old PG-06/scope holes did exactly that).
1373
+ */
1374
+ private gateInput;
1321
1375
  private assertGates;
1322
1376
  private assertChapterParameter;
1323
1377
  private assertChapter;
1324
1378
  private load;
1379
+ /** A repair may bypass the journal hash only when the document is unreadable. */
1380
+ private stateDocIsUnreadable;
1325
1381
  /** Read a review/check without letting one corrupt file wedge every read. */
1326
1382
  private recoverableDoc;
1327
1383
  private mutate;
@@ -1370,4 +1426,4 @@ declare const Config: Schema<Config>;
1370
1426
  * created lazily per project root.
1371
1427
  */
1372
1428
  declare function apply(ctx: Context, config: Config): void;
1373
- export { AdviceReport, ArchiveHandoff, AuditRow, BlockingItem, BriefDocument, BriefLibrary, BriefStage, ChapterPlanInput, ChapterRecord, ChapterStatusRow, ChapterSummary, ChaptersDocument, CharacterDelta, CharactersDocument, CheckDocument, Config, Deduction, DeltaAction, DiffReport, DraftInput, ERROR_CODES, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, ErrorCode, ExportChapter, ExportKind, ForeshadowDelta, ForeshadowsDocument, GENRES, GateInput, GateOptions, GateResult, Genre, HandoffDocument, LocksDocument, MachineMetrics, NovelProject, OPERATIONS, OpResult, Operation, PLATFORMS, PanelChapterRow, PanelChaptersDto, PanelDraft, PanelGatesDto, PanelSnapshot, PanelSummaryDto, PipelineState, Platform, ReadingProof, ReadingProofReference, RelationshipDelta, RelationshipsDocument, ReviewDocument, ReviewInput, ReviewIssue, RevisionInput, RoundCounters, SCENE_TYPES, SCHEMA_VERSION, SceneType, ScoreDimension, SemanticItem, Severity, Stage, StateDelta, StateError, StatusReport, TimelineDelta, TimelineDocument, Violation, Warning, WordMetrics, WorldDelta, WorldDocument, WorldPlanInput, applicableGates, apply, blockedError, briefStageFor, chapterHeading, evaluateGates, faultError, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, verifyReference };
1429
+ export { AdviceReport, ArchiveHandoff, AuditRow, BlockingItem, BriefDocument, BriefLibrary, BriefStage, ChapterCastMember, ChapterPlanInput, ChapterRecord, ChapterStatusRow, ChapterSummary, ChaptersDocument, CharacterDelta, CharactersDocument, CheckDocument, Config, Deduction, DeltaAction, DiffReport, DraftInput, ERROR_CODES, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, ErrorCode, ExportChapter, ExportKind, ForeshadowDelta, ForeshadowsDocument, GENRES, GateInput, GateOptions, GateResult, Genre, HandoffDocument, LocksDocument, MachineMetrics, NovelProject, OPERATIONS, OpResult, Operation, PLATFORMS, PanelChapterRow, PanelChaptersDto, PanelDraft, PanelGatesDto, PanelSnapshot, PanelSummaryDto, PipelineState, Platform, ReadingProof, ReadingProofReference, RelationshipDelta, RelationshipsDocument, ReviewDocument, ReviewInput, ReviewIssue, RevisionInput, RoundCounters, SCENE_TYPES, SCHEMA_VERSION, SceneType, ScoreDimension, SemanticItem, Severity, Stage, StateDelta, StateError, StateErrorCode, StatusReport, TimelineDelta, TimelineDocument, Violation, Warning, WordMetrics, WorldDelta, WorldDocument, WorldPlanInput, applicableGates, apply, blockedError, briefStageFor, chapterHeading, evaluateGates, faultError, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, verifyReference };