dsh-novel-write-engine 0.2.7 → 0.2.9

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.
@@ -11,6 +11,23 @@ export interface BlockingDto {
11
11
  detail: string;
12
12
  hint?: string;
13
13
  }
14
+ /** Chapter-boundary projection; absent on older payloads. */
15
+ export interface ProgressDto {
16
+ active_chapter: number;
17
+ stage: string;
18
+ current: {
19
+ chapter: number;
20
+ stage: string;
21
+ } | null;
22
+ last_archived: number | null;
23
+ next: {
24
+ chapter: number;
25
+ stage: 'P1';
26
+ } | null;
27
+ finished: boolean;
28
+ label: string;
29
+ resume: string;
30
+ }
14
31
  export interface SummaryDto {
15
32
  schemaVersion: number;
16
33
  panelId: string;
@@ -21,8 +38,11 @@ export interface SummaryDto {
21
38
  platform: string;
22
39
  chapter: number;
23
40
  stage: string;
41
+ progress?: ProgressDto;
24
42
  revision: number;
25
43
  totalWords: number;
44
+ archivedWords?: number;
45
+ draftWords?: number;
26
46
  totalChapters: number;
27
47
  /** Book-wide archived average; absent on older payloads. */
28
48
  averageScore?: number;
@@ -75,6 +95,7 @@ export interface SummaryDto {
75
95
  };
76
96
  quality?: {
77
97
  averageScore: number;
98
+ last_ten_average?: number | null;
78
99
  scores: number[];
79
100
  trend: number | null;
80
101
  trend_basis?: 'last_five_vs_previous_five' | 'insufficient_samples' | 'insufficient_history';
@@ -117,7 +138,7 @@ export interface ChapterRowDto {
117
138
  num: number;
118
139
  title: string;
119
140
  wordCount: number;
120
- score: number;
141
+ score: number | null;
121
142
  stage: string;
122
143
  status: 'passed' | 'blocked' | 'draft';
123
144
  details?: string;
@@ -136,13 +157,6 @@ export interface ChaptersDto {
136
157
  panelId: string;
137
158
  rows: ChapterRowDto[];
138
159
  }
139
- export interface GatesDto {
140
- schemaVersion: number;
141
- panelId: string;
142
- stage: string;
143
- blocking: BlockingDto[];
144
- explanations: string[];
145
- }
146
160
  export interface ProjectOptionDto {
147
161
  root: string;
148
162
  title: string;
@@ -168,11 +182,20 @@ export interface PanelPayloadDto {
168
182
  selection: PanelSelectionDto;
169
183
  summary?: SummaryDto;
170
184
  chapters?: ChaptersDto;
171
- gates?: GatesDto;
172
185
  resume?: {
173
186
  text: string;
174
187
  };
175
188
  missing?: PanelMissingDto;
176
189
  error?: string;
177
190
  empty?: true;
191
+ /** Additive v7 section payload. */
192
+ payload_version?: 7;
193
+ section?: string;
194
+ items?: unknown;
195
+ total?: number;
196
+ returned?: number;
197
+ truncated?: boolean;
198
+ next_cursor?: string | null;
199
+ updated_at?: string;
200
+ etag?: string;
178
201
  }
@@ -1,10 +1,28 @@
1
1
  import { type ReactElement } from 'react';
2
2
  import { buildView, type PanelView } from './viewmodel.js';
3
+ import type { PanelPayloadDto } from './dto.js';
4
+ export type PanelTab = 'overview' | 'chapters' | 'characters' | 'foreshadows' | 'worldbook' | 'quality' | 'health';
5
+ export interface PanelSectionState {
6
+ loading: boolean;
7
+ error: string | null;
8
+ payload: PanelPayloadDto | null;
9
+ }
10
+ export interface ChapterQuery {
11
+ search: string;
12
+ status: 'all' | 'archived' | 'draft';
13
+ scene: string;
14
+ sort: 'num_asc' | 'num_desc';
15
+ cursor: string;
16
+ }
17
+ export interface ChapterDetailState {
18
+ chapter: number;
19
+ section: PanelSectionState;
20
+ }
3
21
  /**
4
22
  * Pure view: given a view model, return the element tree. Keeping the hooks in
5
23
  * `NovelPanel` makes every visual state renderable without a browser.
6
24
  */
7
- export declare function panelElement(view: PanelView, loading: boolean, onRefresh: () => void, onSelectProject?: (root: string | null) => void): ReactElement;
25
+ export declare function panelElement(view: PanelView, loading: boolean, onRefresh: () => void, onSelectProject?: (root: string | null) => void, tab?: PanelTab, onTab?: (tab: PanelTab) => void, section?: PanelSectionState | null, chapterQuery?: ChapterQuery, onChapterQuery?: (patch: Partial<ChapterQuery>) => void, sectionSearch?: string, onSectionSearch?: (value: string) => void, chapterDetail?: ChapterDetailState | null, onChapterDetail?: (chapter: number | null) => void, qualityDetail?: ChapterDetailState | null, onQualityDetail?: (chapter: number | null) => void): ReactElement;
8
26
  /** Stateful wrapper: fetch once, refresh on demand and after returning to the tab. */
9
27
  export declare function NovelPanel(): ReactElement;
10
28
  /** Sidebar icon: a small book mark, inheriting the current text colour. */
@@ -14,8 +14,8 @@ export interface PanelChapter {
14
14
  words: string;
15
15
  wordCount: number;
16
16
  score: string;
17
- scoreValue: number;
18
- scorePercent: number;
17
+ scoreValue: number | null;
18
+ scorePercent: number | null;
19
19
  badge: string;
20
20
  tone: 'ok' | 'bad' | 'muted';
21
21
  details: string;
@@ -69,6 +69,8 @@ export interface PanelView {
69
69
  stageLabel: string;
70
70
  /** The whole book reached its configured target; stage `done` alone is only one chapter. */
71
71
  finished: boolean;
72
+ /** Active chapter number for lazy quality detail. */
73
+ chapter: number;
72
74
  revisionLabel: string;
73
75
  phases: PanelPhase[];
74
76
  rows: PanelChapter[];
@@ -82,6 +84,8 @@ export interface PanelView {
82
84
  stats: {
83
85
  chapters: number;
84
86
  words: number;
87
+ archivedWords?: number;
88
+ draftWords?: number;
85
89
  averageScore: number;
86
90
  blocking: number;
87
91
  };
@@ -117,7 +121,7 @@ export interface PanelView {
117
121
  };
118
122
  quality: {
119
123
  averageScore: number;
120
- lastTenAverage: number;
124
+ lastTenAverage: number | null;
121
125
  bookMedian: number;
122
126
  bookMin: number;
123
127
  bookMax: number;
@@ -125,6 +129,7 @@ export interface PanelView {
125
129
  id: string;
126
130
  average: number;
127
131
  samples: number;
132
+ max?: number;
128
133
  }>;
129
134
  scores: number[];
130
135
  trend: number | null;
@@ -169,6 +174,9 @@ export interface PanelView {
169
174
  nextDetail: string;
170
175
  nextTool: string;
171
176
  emptyCommand: string;
177
+ /** Last successful sync / last background failure; filled by NovelPanel. */
178
+ lastSync?: string | null;
179
+ syncError?: string | null;
172
180
  /** Compatibility fields used by older cards and tests. */
173
181
  revision: string;
174
182
  next: string;
@@ -181,7 +189,8 @@ export declare const PANEL_SCHEMA_VERSION = 6;
181
189
  */
182
190
  export declare function buildView(payload: PanelPayloadDto | null, error?: string | null): PanelView;
183
191
  /** Fetch the panel payload; failures become a plain message, never a throw. */
184
- export declare function fetchPanel(fetcher: typeof fetch, path: string, signal?: AbortSignal): Promise<{
192
+ export declare function fetchPanel(fetcher: typeof fetch, path: string, signal?: AbortSignal, etag?: string): Promise<{
185
193
  payload: PanelPayloadDto | null;
186
194
  error: string | null;
195
+ notModified?: boolean;
187
196
  }>;
@@ -5,6 +5,8 @@ export declare const PANEL_ID = "novel-workshop";
5
5
  export declare const PANEL_ROUTE = "/api/novel-write-engine/panel";
6
6
  export declare const SKILL_NAME = "novel-write-engine";
7
7
  export declare const REMOTE_VERSION = 6;
8
+ /** User-visible panel/tutorial version; keep in sync with package.json. */
9
+ export declare const PANEL_VERSION = "0.2.9";
8
10
  /** The panel is a recent-history view; the wire payload and UI use one cap. */
9
11
  export declare const PANEL_CHAPTER_LIMIT = 30;
10
12
  /** Project-relative locations. Every write goes through these constants. */
@@ -45,7 +45,7 @@ export interface PanelBookData {
45
45
  average_words: number;
46
46
  remaining_words: number | null;
47
47
  remaining_words_basis: 'actual_average' | 'minimum_target' | 'none';
48
- percent: number;
48
+ percent: number | null;
49
49
  };
50
50
  current: {
51
51
  card: PanelCardData | null;
@@ -93,7 +93,7 @@ export interface PanelBookData {
93
93
  median: number;
94
94
  min: number;
95
95
  max: number;
96
- last_ten_average: number;
96
+ last_ten_average: number | null;
97
97
  trend: number | null;
98
98
  trend_basis: BookReport['quality']['trend_basis'];
99
99
  samples: number;
@@ -217,6 +217,18 @@ export interface WordMetrics {
217
217
  paragraph_count: number;
218
218
  paragraph_max_words: number;
219
219
  }
220
+ /** Protected evidence copied from the archived outline card at archive time. */
221
+ export interface ArchivedCardSnapshot {
222
+ title: string;
223
+ scene_type: SceneType;
224
+ target_words: [number, number];
225
+ cast: string[];
226
+ goal: string;
227
+ conflict: string;
228
+ hook: string;
229
+ notes: string;
230
+ archived_at: string | null;
231
+ }
220
232
  /** Derived chapter row kept in `state/chapters.yaml`. */
221
233
  export interface ChapterRecord {
222
234
  num: number;
@@ -239,6 +251,10 @@ export interface ChapterRecord {
239
251
  assigned_ids?: Record<string, string>;
240
252
  /** Digest of the archive request that produced this row. */
241
253
  archive_digest?: string;
254
+ /** Immutable card fields written by the archive transaction, not by amend. */
255
+ archive_card?: ArchivedCardSnapshot;
256
+ /** Canonical digest of archive_card; v1-prefixed like archive_digest. */
257
+ archive_card_digest?: string;
242
258
  }
243
259
  /** Summary block the model supplies with a draft and the engine persists. */
244
260
  export interface ChapterSummary {
@@ -301,8 +317,8 @@ export interface CharacterDelta {
301
317
  role?: string;
302
318
  profile?: Record<string, unknown>;
303
319
  current_state?: Record<string, unknown>;
320
+ /** Chapter is engine-assigned from the archive transaction, not caller-supplied. */
304
321
  growth?: {
305
- chapter: number;
306
322
  change: string;
307
323
  status_change: string;
308
324
  };
@@ -760,7 +776,7 @@ export interface BookReport {
760
776
  target: number | null;
761
777
  words: number;
762
778
  average_words: number;
763
- percent: number;
779
+ percent: number | null;
764
780
  remaining_words: number | null;
765
781
  remaining_words_min: number | null;
766
782
  remaining_words_average: number | null;
@@ -825,6 +841,7 @@ export interface BookReport {
825
841
  id: string;
826
842
  average: number;
827
843
  samples: number;
844
+ max: number;
828
845
  }>;
829
846
  };
830
847
  /** Per-chapter effort survives archive (pipeline rounds are reset). */
@@ -972,6 +989,8 @@ export interface GateInput {
972
989
  briefStage: BriefStage | null;
973
990
  draftTitle: string | null;
974
991
  sceneType: SceneType | null;
992
+ /** The submitted/active plan narrator; PG-05 uses it before a draft exists. */
993
+ narrator?: string | null;
975
994
  summary: ChapterSummary | null;
976
995
  /** P5 may omit `summary`; when omitted, the previous plan summary is reused. */
977
996
  summaryProvided?: boolean;
@@ -1034,6 +1053,36 @@ export interface Selection {
1034
1053
  title?: string;
1035
1054
  created_at?: string;
1036
1055
  }
1056
+ /**
1057
+ * One human-readable projection of the pipeline's chapter boundary.
1058
+ *
1059
+ * `pipeline.chapter`/`stage` are protocol values: after archiving chapter N the
1060
+ * pipeline points at N+1 and still says `done` because `done` describes the
1061
+ * chapter that was just archived. This projection is the only place that turns
1062
+ * those values into "chapter N archived; chapter N+1 starts at P1".
1063
+ */
1064
+ export interface ChapterProgress {
1065
+ active_chapter: number;
1066
+ stage: Stage;
1067
+ /** The chapter currently being written; null while `stage === 'done'`. */
1068
+ current: {
1069
+ chapter: number;
1070
+ stage: Stage;
1071
+ } | null;
1072
+ /** Highest archived chapter, or null before the first archive. */
1073
+ last_archived: number | null;
1074
+ /** Next chapter to start at P1; null while writing or after the book is finished. */
1075
+ next: {
1076
+ chapter: number;
1077
+ stage: 'P1';
1078
+ } | null;
1079
+ /** True only when the configured target has been reached at `stage === 'done'`. */
1080
+ finished: boolean;
1081
+ /** One line for status cards, commands and the panel. */
1082
+ label: string;
1083
+ /** Sentence used by the resume prompt instead of "当前第 N 章,阶段 done". */
1084
+ resume: string;
1085
+ }
1037
1086
  /** The `getStatus` payload. */
1038
1087
  export interface StatusReport {
1039
1088
  project: {
@@ -1054,6 +1103,8 @@ export interface StatusReport {
1054
1103
  /** SHA-256 of the active manuscript, or null when the stage has no draft. */
1055
1104
  manuscript_hash: string | null;
1056
1105
  stage: Stage;
1106
+ /** Chapter-boundary projection shared by every human-readable surface. */
1107
+ progress?: ChapterProgress;
1057
1108
  rounds: RoundCounters;
1058
1109
  blocking: BlockingItem[];
1059
1110
  /** Non-blocking recovery notes, e.g. a recoverable pending transaction. */
@@ -1106,6 +1157,8 @@ export interface StatusReport {
1106
1157
  export interface AdviceReport {
1107
1158
  stage: Stage;
1108
1159
  chapter: number;
1160
+ /** Same chapter-boundary projection as getStatus. */
1161
+ progress?: ChapterProgress;
1109
1162
  tool: string;
1110
1163
  args: Record<string, unknown>;
1111
1164
  reason: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-write-engine",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Novel writing engine for DeepSeek Harness: state machine, gates, rulepacks and ledgers moved from prompts into code.",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -63,7 +63,8 @@
63
63
  ],
64
64
  "external": [
65
65
  "react",
66
- "react/jsx-runtime"
66
+ "react/jsx-runtime",
67
+ "react-dom"
67
68
  ]
68
69
  }
69
70
  },
@@ -98,7 +99,8 @@
98
99
  "@deepseek-ai/dsh-skill": "^0.2.0-rc.1 || ^0.2.1-rc.1",
99
100
  "@deepseek-ai/schemastery": "^3.18.5",
100
101
  "@deepseek-ai/dsh-tools": "^0.2.0-rc.1 || ^0.2.1-rc.1",
101
- "react": "^18.0.0 || ^19.0.0"
102
+ "react": "^18.0.0 || ^19.0.0",
103
+ "react-dom": "^18.0.0 || ^19.0.0"
102
104
  },
103
105
  "peerDependenciesMeta": {
104
106
  "@deepseek-ai/dsh-client-ui-layout": {
@@ -112,6 +114,9 @@
112
114
  },
113
115
  "react": {
114
116
  "optional": true
117
+ },
118
+ "react-dom": {
119
+ "optional": true
115
120
  }
116
121
  },
117
122
  "devDependencies": {
@@ -17,6 +17,10 @@
17
17
  - 正文只经 novel_write_draft,改稿只经 novel_revise;归档只走 novel_archive,且必须带 state_delta。没有别的写入口。
18
18
  - 字数、引号、格式、门控结论一律以引擎返回为准,不要自己估算,也不要复述旧数字。
19
19
  - 评审每一分都要指到具体扣分项;指不出来就不要扣。
20
+ - 参数是严格 JSON:只用英文双引号,不写注释、尾逗号和 Markdown 围栏;不要把对象再包成 JSON 字符串,不要重复编码。
21
+ - 写工具一次只提交一个:novel_write_draft、novel_review、novel_check、novel_archive 读完结果再走下一步。
22
+ - 参数解析失败时不要原样重发大对象;先 novel_brief 重取当前阶段字段,缩小对象并逐字段核对。
23
+ - 归档只带 state_delta,不带 summary;归档后下一章从 novel_brief P1 重新开始。
20
24
  - 设定、前章细节、伏笔状态拿不准就查 novel_state,不许编。
21
25
  - { id: time-context, name: '@deepseek-ai/dsh-time-context' }
22
26
  - { id: tool-skill, name: '@deepseek-ai/dsh-tool-skill' }
@@ -1,6 +1,6 @@
1
1
  schema_version: 1
2
2
  name: default
3
- version: "1.0.1"
3
+ version: "1.1.0"
4
4
 
5
5
  lexicon:
6
6
  zero_tolerance:
@@ -182,10 +182,16 @@ platform:
182
182
  jinjiang: { words_per_chapter: [2500, 4000], dialogue_ratio: [0.35, 0.55] }
183
183
 
184
184
  genres:
185
- 玄幻修仙: { note: "力量体系必须自洽;升级要有代价,境界不能靠形容词抬升。" }
186
- 都市现代: { note: "现实都市,冲突来自资源、关系与规则;爽点要落在可验证的行动上。" }
185
+ 玄幻修仙:
186
+ note: "力量体系必须自洽;升级要有代价,境界不能靠形容词抬升。"
187
+ profile: { water_words: 0.30, pleasure_density: 2 }
188
+ 都市现代:
189
+ note: "现实都市,冲突来自资源、关系与规则;爽点要落在可验证的行动上。"
190
+ profile: { water_words: 0.32, pleasure_density: 1 }
187
191
  西方奇幻: { note: "种族、语言与力量来源要建条目;奇观不能替代人物选择。" }
188
- 悬疑推理: { note: "线索先于解答出现;每个伏笔都要在归档台账中可核销。" }
192
+ 悬疑推理:
193
+ note: "线索先于解答出现;每个伏笔都要在归档台账中可核销。"
194
+ profile: { period_density: { min: 1.5, max: 12 }, water_words: 0.30 }
189
195
  科幻: { note: "技术设定只写对情节有用的部分;禁止用术语代替冲突。" }
190
196
  游戏系统: { note: "系统提示是世界观内文本;奖励与惩罚必须可追踪。" }
191
197
  历史架空: { note: "礼制、称谓与器物要经得起基本推敲;不写现代口号。" }
@@ -193,7 +199,9 @@ genres:
193
199
  轻小说: { note: "关系推进靠具体互动,不靠内心独白宣布;对话要能区分说话人。" }
194
200
  军事: { note: "战术细节服务于人物选择;不写武器参数清单。" }
195
201
  末世流: { note: "生存资源必须记账;恐惧来自规则崩塌,不靠突然的音效式形容词。" }
196
- 言情: { note: "亲密关系按阶梯推进;单次独白要短,情绪落在动作上。" }
202
+ 言情:
203
+ note: "亲密关系按阶梯推进;单次独白要短,情绪落在动作上。"
204
+ profile: { period_density: { min: 1.5, max: 12 }, pleasure_density: 2 }
197
205
 
198
206
  crosschapter:
199
207
  duplicate_similarity: 0.9
package/skills/entry.md CHANGED
@@ -77,7 +77,8 @@ description: 引擎版小说创作流程——状态、门控、字数与写盘
77
77
 
78
78
  ## 工具怎么用
79
79
 
80
- - 参数是 JSON 对象;`summary`、`dimensions`、`issues`、`proof` 是结构化对象,按任务书字段填。
80
+ - 参数是严格 JSON 对象:英文双引号、无注释/尾逗号/围栏;`summary`、`dimensions`、`issues`、`proof` 是结构化对象/数组,按任务书字段填,不要再包成 JSON 字符串。
81
+ - 写工具一次只提交一个;参数解析失败时先重取 `novel_brief`,缩小对象再试,不要原样重发。
81
82
  - `novel_check` 必须带五个语义项(`L2-SCENE-ANCHOR` / `L2-CHARACTER-STATE` / `L2-CAUSALITY` / `L2-VOICE` / `L2-POV`),每项都要有正文逐字引用,且来自不同句子;缺一项即校验不过。
82
83
  - 失败分三类:参数错(改参数)、门控没过(改正文)、系统故障(查目录/稍后重试)。错误消息里的 code 说明属于哪类。
83
84
  - 门控失败会一次列出**全部**阻塞项;一次改完再交,不要逐条试。