@lingjingai/scriptctl 0.1.0 → 0.3.0

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.
Files changed (40) hide show
  1. package/README.md +72 -0
  2. package/dist/cli.js +309 -396
  3. package/dist/cli.js.map +1 -1
  4. package/dist/common.d.ts +9 -0
  5. package/dist/common.js.map +1 -1
  6. package/dist/domain/asset-registry.d.ts +141 -0
  7. package/dist/domain/asset-registry.js +318 -0
  8. package/dist/domain/asset-registry.js.map +1 -0
  9. package/dist/domain/collision-detector.d.ts +83 -0
  10. package/dist/domain/collision-detector.js +248 -0
  11. package/dist/domain/collision-detector.js.map +1 -0
  12. package/dist/domain/direct-core.d.ts +13 -1
  13. package/dist/domain/direct-core.js +19 -6
  14. package/dist/domain/direct-core.js.map +1 -1
  15. package/dist/domain/script-core.d.ts +11 -0
  16. package/dist/domain/script-core.js +34 -19
  17. package/dist/domain/script-core.js.map +1 -1
  18. package/dist/help-text.js +336 -4
  19. package/dist/help-text.js.map +1 -1
  20. package/dist/infra/converters.js +21 -7
  21. package/dist/infra/converters.js.map +1 -1
  22. package/dist/infra/default-writing-prompt.d.ts +31 -0
  23. package/dist/infra/default-writing-prompt.js +50 -0
  24. package/dist/infra/default-writing-prompt.js.map +1 -0
  25. package/dist/infra/default-writing-prompt.md +115 -0
  26. package/dist/infra/gemini-writer.d.ts +107 -0
  27. package/dist/infra/gemini-writer.js +207 -0
  28. package/dist/infra/gemini-writer.js.map +1 -0
  29. package/dist/infra/providers.d.ts +36 -0
  30. package/dist/infra/providers.js +186 -2
  31. package/dist/infra/providers.js.map +1 -1
  32. package/dist/output.js +26 -9
  33. package/dist/output.js.map +1 -1
  34. package/dist/usecases/episode.d.ts +48 -0
  35. package/dist/usecases/episode.js +1209 -0
  36. package/dist/usecases/episode.js.map +1 -0
  37. package/dist/usecases/script.d.ts +6 -2
  38. package/dist/usecases/script.js +49 -5
  39. package/dist/usecases/script.js.map +1 -1
  40. package/package.json +9 -5
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Asset collision detector for cross-episode incremental drafting.
3
+ *
4
+ * When a freshly-parsed fragment declares a new asset (in `## 人物`/`## 场景`/`## 道具`/
5
+ * `## 发声源`) whose name already exists in the registry, the detector raises a
6
+ * structured report. The orchestrator (episode subcommand) is expected to surface this
7
+ * to the agent, which decides among three resolutions:
8
+ * - accept_identity: same entity, drop the duplicate declaration (refs use existing id)
9
+ * - distinguish_rename: different entity, rename the new one to disambiguate
10
+ * - promote_to_state: same entity, different state — add a state to existing asset
11
+ *
12
+ * Only exact-name collisions are detected. The kind reflects whether the agent
13
+ * should treat it as blocking or a warning, based on description similarity:
14
+ * - hard: exact name match (description similar enough that it's almost certainly
15
+ * the same entity). Blocks commit; expects agent resolution.
16
+ * - soft: exact name match but descriptions clearly diverge — could be intentional
17
+ * reuse of a generic label (e.g. two unrelated "路人甲"). Warn but don't block.
18
+ *
19
+ * The detector is a pure function: it reads the fragment and the registry,
20
+ * never mutates either.
21
+ */
22
+ import type { AssetRegistry, AssetType, FragmentLike } from "./asset-registry.js";
23
+ export type CollisionKind = "hard" | "soft";
24
+ export type CollisionResolution = "accept_identity" | "distinguish_rename" | "promote_to_state";
25
+ export interface CollisionReport {
26
+ kind: CollisionKind;
27
+ assetType: AssetType;
28
+ fragmentDeclaration: {
29
+ name: string;
30
+ description: string | null;
31
+ firstSeenScene?: string;
32
+ firstSeenAction?: string;
33
+ };
34
+ registryEntry: {
35
+ id: string;
36
+ name: string;
37
+ description: string | null;
38
+ firstSeenEpisode: number;
39
+ firstSeenScene: string;
40
+ firstSeenAction: string;
41
+ lastSeenEpisode: number;
42
+ appearanceCount: number;
43
+ states: Array<{
44
+ id: string;
45
+ name: string;
46
+ }>;
47
+ sourceKind?: string;
48
+ };
49
+ descriptionSimilarity: number;
50
+ resolutions: CollisionResolution[];
51
+ }
52
+ export interface DetectCollisionsOptions {
53
+ /**
54
+ * Description Jaccard similarity (0..1) below which a `soft` collision is raised
55
+ * for matching names. Default 0.4 — chosen to flag clearly divergent descriptions
56
+ * while tolerating natural variance (e.g. "穿灰色西装" vs "灰西装中年男").
57
+ */
58
+ softSimilarityThreshold?: number;
59
+ }
60
+ /**
61
+ * Detect collisions between a freshly-parsed fragment and the existing registry.
62
+ *
63
+ * Returns a (possibly empty) list of reports. Caller (episode subcommand) treats:
64
+ * - `kind: "hard"` as blocking — agent must resolve before commit
65
+ * - `kind: "soft"` as warning — agent should consider but may proceed
66
+ * - `kind: "hint"` as info — surfaced for awareness only
67
+ *
68
+ * The episode number is taken from `fragment.episode` if available, otherwise
69
+ * passed via `opts.episode` (caller responsibility).
70
+ */
71
+ export declare function detectCollisions(fragment: FragmentLike, registry: AssetRegistry, opts?: DetectCollisionsOptions & {
72
+ episode?: number;
73
+ }): CollisionReport[];
74
+ /**
75
+ * Whether any report has `kind: "hard"` — convenience for episode.ts commit gating.
76
+ */
77
+ export declare function hasHardCollision(reports: CollisionReport[]): boolean;
78
+ /**
79
+ * Render a list of CollisionReports as a human-readable markdown summary suitable
80
+ * for stderr output. The machine-readable JSON is expected to be written to
81
+ * `workspace/episodes/ep<n>.collision.json` by the orchestrator.
82
+ */
83
+ export declare function summarizeReports(reports: CollisionReport[]): string;
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Asset collision detector for cross-episode incremental drafting.
3
+ *
4
+ * When a freshly-parsed fragment declares a new asset (in `## 人物`/`## 场景`/`## 道具`/
5
+ * `## 发声源`) whose name already exists in the registry, the detector raises a
6
+ * structured report. The orchestrator (episode subcommand) is expected to surface this
7
+ * to the agent, which decides among three resolutions:
8
+ * - accept_identity: same entity, drop the duplicate declaration (refs use existing id)
9
+ * - distinguish_rename: different entity, rename the new one to disambiguate
10
+ * - promote_to_state: same entity, different state — add a state to existing asset
11
+ *
12
+ * Only exact-name collisions are detected. The kind reflects whether the agent
13
+ * should treat it as blocking or a warning, based on description similarity:
14
+ * - hard: exact name match (description similar enough that it's almost certainly
15
+ * the same entity). Blocks commit; expects agent resolution.
16
+ * - soft: exact name match but descriptions clearly diverge — could be intentional
17
+ * reuse of a generic label (e.g. two unrelated "路人甲"). Warn but don't block.
18
+ *
19
+ * The detector is a pure function: it reads the fragment and the registry,
20
+ * never mutates either.
21
+ */
22
+ import { lookupActor, lookupLocation, lookupProp, lookupSpeaker, } from "./asset-registry.js";
23
+ const DEFAULT_SOFT_THRESHOLD = 0.4;
24
+ // ---------------------------------------------------------------------------
25
+ // Similarity scoring (lightweight, dependency-free)
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Character-level Jaccard similarity. Trims and normalises punctuation/whitespace.
29
+ * Tuned for short Chinese/English descriptions (≤ 100 chars), which is the typical
30
+ * scriptctl asset description length.
31
+ *
32
+ * Returns 1.0 when both strings are empty (or one is empty — to avoid false soft
33
+ * collisions when an existing entry has no description yet).
34
+ */
35
+ function descriptionSimilarity(a, b) {
36
+ const aa = (a ?? "").replace(/[\s\p{P}]+/gu, "");
37
+ const bb = (b ?? "").replace(/[\s\p{P}]+/gu, "");
38
+ if (aa === "" || bb === "")
39
+ return 1.0;
40
+ const setA = new Set([...aa]);
41
+ const setB = new Set([...bb]);
42
+ let intersect = 0;
43
+ for (const ch of setA)
44
+ if (setB.has(ch))
45
+ intersect += 1;
46
+ const union = setA.size + setB.size - intersect;
47
+ return union === 0 ? 1.0 : intersect / union;
48
+ }
49
+ function classifyMatch(fragmentDesc, registryDesc, softThreshold) {
50
+ const sim = descriptionSimilarity(fragmentDesc, registryDesc);
51
+ if (sim < softThreshold)
52
+ return { kind: "soft", similarity: sim };
53
+ return { kind: "hard", similarity: sim };
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // Entry → report builders
57
+ // ---------------------------------------------------------------------------
58
+ function toRegistryReport(entry) {
59
+ const base = {
60
+ id: entry.id,
61
+ name: entry.name,
62
+ description: entry.description,
63
+ firstSeenEpisode: entry.firstSeenEpisode,
64
+ firstSeenScene: entry.firstSeenScene,
65
+ firstSeenAction: entry.firstSeenAction,
66
+ lastSeenEpisode: entry.lastSeenEpisode,
67
+ appearanceCount: entry.appearanceCount,
68
+ states: entry.states.map((s) => ({ id: s.id, name: s.name })),
69
+ };
70
+ if ("sourceKind" in entry)
71
+ base.sourceKind = entry.sourceKind;
72
+ return base;
73
+ }
74
+ function findFragmentFirstSeen(fragment, episode, predicate, actionPicker) {
75
+ const scenes = fragment.scenes ?? [];
76
+ for (let i = 0; i < scenes.length; i++) {
77
+ if (!predicate(i))
78
+ continue;
79
+ const sceneNum = typeof scenes[i].scene_num === "number" ? scenes[i].scene_num : i + 1;
80
+ const sceneId = `ep_${String(episode).padStart(3, "0")}/scn_${String(sceneNum).padStart(3, "0")}`;
81
+ return { scene: sceneId, action: actionPicker(i) };
82
+ }
83
+ return { scene: "", action: "" };
84
+ }
85
+ function locateActorFirstSeen(fragment, episode, name) {
86
+ return findFragmentFirstSeen(fragment, episode, (i) => (fragment.scenes?.[i]?.actor_names ?? []).includes(name), (i) => {
87
+ const firstAction = (fragment.scenes?.[i]?.actions ?? []).find((a) => typeof a.content === "string" && a.content.includes(name));
88
+ return firstAction ? String(firstAction.content ?? "").trim() : "";
89
+ });
90
+ }
91
+ function locateLocationFirstSeen(fragment, episode, name) {
92
+ return findFragmentFirstSeen(fragment, episode, (i) => fragment.scenes?.[i]?.location_name === name, (i) => String(fragment.scenes?.[i]?.actions?.[0]?.content ?? "").trim());
93
+ }
94
+ function locatePropFirstSeen(fragment, episode, name) {
95
+ return findFragmentFirstSeen(fragment, episode, (i) => (fragment.scenes?.[i]?.prop_names ?? []).includes(name), (i) => {
96
+ const firstAction = (fragment.scenes?.[i]?.actions ?? []).find((a) => typeof a.content === "string" && a.content.includes(name));
97
+ return firstAction ? String(firstAction.content ?? "").trim() : "";
98
+ });
99
+ }
100
+ function locateSpeakerFirstSeen(fragment, episode, name) {
101
+ return findFragmentFirstSeen(fragment, episode, (i) => (fragment.scenes?.[i]?.actions ?? []).some((a) => typeof a.speaker === "string" && a.speaker === name), (i) => {
102
+ const firstAction = (fragment.scenes?.[i]?.actions ?? []).find((a) => typeof a.speaker === "string" && a.speaker === name);
103
+ return firstAction ? String(firstAction.content ?? "").trim() : "";
104
+ });
105
+ }
106
+ function buildReport(kind, assetType, decl, firstSeen, entry, similarity) {
107
+ const resolutions = assetType === "actor"
108
+ ? ["accept_identity", "distinguish_rename", "promote_to_state"]
109
+ : ["accept_identity", "distinguish_rename"];
110
+ return {
111
+ kind,
112
+ assetType,
113
+ fragmentDeclaration: {
114
+ name: decl.name,
115
+ description: decl.description ?? null,
116
+ firstSeenScene: firstSeen.scene || undefined,
117
+ firstSeenAction: firstSeen.action || undefined,
118
+ },
119
+ registryEntry: toRegistryReport(entry),
120
+ descriptionSimilarity: similarity,
121
+ resolutions,
122
+ };
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // Public API
126
+ // ---------------------------------------------------------------------------
127
+ /**
128
+ * Detect collisions between a freshly-parsed fragment and the existing registry.
129
+ *
130
+ * Returns a (possibly empty) list of reports. Caller (episode subcommand) treats:
131
+ * - `kind: "hard"` as blocking — agent must resolve before commit
132
+ * - `kind: "soft"` as warning — agent should consider but may proceed
133
+ * - `kind: "hint"` as info — surfaced for awareness only
134
+ *
135
+ * The episode number is taken from `fragment.episode` if available, otherwise
136
+ * passed via `opts.episode` (caller responsibility).
137
+ */
138
+ export function detectCollisions(fragment, registry, opts = {}) {
139
+ const softThreshold = opts.softSimilarityThreshold ?? DEFAULT_SOFT_THRESHOLD;
140
+ const episode = opts.episode ?? (typeof fragment.episode === "number" ? fragment.episode : 0);
141
+ const reports = [];
142
+ for (const decl of fragment.actors ?? []) {
143
+ const existing = lookupActor(registry, decl.name);
144
+ if (existing) {
145
+ const { kind, similarity } = classifyMatch(decl.description, existing.description, softThreshold);
146
+ const firstSeen = locateActorFirstSeen(fragment, episode, decl.name);
147
+ reports.push(buildReport(kind, "actor", decl, firstSeen, existing, similarity));
148
+ }
149
+ }
150
+ for (const decl of fragment.locations ?? []) {
151
+ const existing = lookupLocation(registry, decl.name);
152
+ if (existing) {
153
+ const { kind, similarity } = classifyMatch(decl.description, existing.description, softThreshold);
154
+ const firstSeen = locateLocationFirstSeen(fragment, episode, decl.name);
155
+ reports.push(buildReport(kind, "location", decl, firstSeen, existing, similarity));
156
+ }
157
+ }
158
+ for (const decl of fragment.props ?? []) {
159
+ const existing = lookupProp(registry, decl.name);
160
+ if (existing) {
161
+ const { kind, similarity } = classifyMatch(decl.description, existing.description, softThreshold);
162
+ const firstSeen = locatePropFirstSeen(fragment, episode, decl.name);
163
+ reports.push(buildReport(kind, "prop", decl, firstSeen, existing, similarity));
164
+ }
165
+ }
166
+ for (const decl of fragment.speakers ?? []) {
167
+ const existing = lookupSpeaker(registry, decl.name, decl.sourceKind);
168
+ if (existing) {
169
+ const { kind, similarity } = classifyMatch(decl.description, existing.description, softThreshold);
170
+ const firstSeen = locateSpeakerFirstSeen(fragment, episode, decl.name);
171
+ reports.push(buildReport(kind, "speaker", decl, firstSeen, existing, similarity));
172
+ }
173
+ }
174
+ return reports;
175
+ }
176
+ /**
177
+ * Whether any report has `kind: "hard"` — convenience for episode.ts commit gating.
178
+ */
179
+ export function hasHardCollision(reports) {
180
+ return reports.some((r) => r.kind === "hard");
181
+ }
182
+ // ---------------------------------------------------------------------------
183
+ // Human-readable summary (for CLI stderr output)
184
+ // ---------------------------------------------------------------------------
185
+ /**
186
+ * Render a list of CollisionReports as a human-readable markdown summary suitable
187
+ * for stderr output. The machine-readable JSON is expected to be written to
188
+ * `workspace/episodes/ep<n>.collision.json` by the orchestrator.
189
+ */
190
+ export function summarizeReports(reports) {
191
+ if (reports.length === 0)
192
+ return "";
193
+ const lines = [`检测到 ${reports.length} 个资产冲突:`, ""];
194
+ for (let i = 0; i < reports.length; i++) {
195
+ const r = reports[i];
196
+ lines.push(`### [${i + 1}/${reports.length}] ${r.kind.toUpperCase()} · ${r.assetType} · "${r.fragmentDeclaration.name}"`);
197
+ lines.push("");
198
+ lines.push("新声明:");
199
+ lines.push(` 名字: ${r.fragmentDeclaration.name}`);
200
+ if (r.fragmentDeclaration.description) {
201
+ lines.push(` 描述: ${r.fragmentDeclaration.description}`);
202
+ }
203
+ if (r.fragmentDeclaration.firstSeenScene) {
204
+ lines.push(` 首次出现: ${r.fragmentDeclaration.firstSeenScene}`);
205
+ }
206
+ if (r.fragmentDeclaration.firstSeenAction) {
207
+ lines.push(` 首次动作: ${r.fragmentDeclaration.firstSeenAction}`);
208
+ }
209
+ lines.push("");
210
+ lines.push(`已存在 (${r.registryEntry.id}):`);
211
+ lines.push(` 名字: ${r.registryEntry.name}`);
212
+ if (r.registryEntry.description) {
213
+ lines.push(` 描述: ${r.registryEntry.description}`);
214
+ }
215
+ if (r.registryEntry.sourceKind) {
216
+ lines.push(` 来源类型: ${r.registryEntry.sourceKind}`);
217
+ }
218
+ lines.push(` 首次出现: ${r.registryEntry.firstSeenScene} (ep${r.registryEntry.firstSeenEpisode})`);
219
+ if (r.registryEntry.firstSeenAction) {
220
+ lines.push(` 首次动作: ${r.registryEntry.firstSeenAction}`);
221
+ }
222
+ lines.push(` 最近出现: ep${r.registryEntry.lastSeenEpisode}`);
223
+ lines.push(` 出现总集数: ${r.registryEntry.appearanceCount}`);
224
+ if (r.registryEntry.states.length > 0) {
225
+ lines.push(` 已注册状态: [${r.registryEntry.states.map((s) => s.name).join(", ")}]`);
226
+ }
227
+ lines.push(` 描述相似度: ${(r.descriptionSimilarity * 100).toFixed(0)}%`);
228
+ lines.push("");
229
+ lines.push("可能处置:");
230
+ if (r.resolutions.includes("accept_identity")) {
231
+ lines.push(` [A] 同一${r.assetType === "actor" ? "人" : "项"},沿用 ${r.registryEntry.id}`);
232
+ lines.push(` → 编辑 ep<n>.md,删除资产段中重复声明,refs 不动`);
233
+ }
234
+ if (r.resolutions.includes("distinguish_rename")) {
235
+ lines.push(` [B] 不同${r.assetType === "actor" ? "人" : "项"},需区分`);
236
+ lines.push(` → 把这集所有 "${r.fragmentDeclaration.name}" 改名为更具体的名字`);
237
+ }
238
+ if (r.resolutions.includes("promote_to_state")) {
239
+ lines.push(` [C] 同一人不同形态/装扮`);
240
+ lines.push(` → scriptctl script state add actor:${r.registryEntry.id} --state-id <id> --name <name> --description <desc>`);
241
+ }
242
+ lines.push("");
243
+ }
244
+ lines.push("完整碰撞详情见 workspace/episodes/ep<n>.collision.json");
245
+ lines.push("解决后重跑:scriptctl episode draft <n> --resume (跳过 Gemini,仅重跑 parser + collisionCheck)");
246
+ return lines.join("\n");
247
+ }
248
+ //# sourceMappingURL=collision-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collision-detector.js","sourceRoot":"","sources":["../../src/domain/collision-detector.ts"],"names":[],"mappings":"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"}
@@ -86,7 +86,19 @@ export declare function parseOverlapMarkdownLines(content: string, scene: Dict):
86
86
  speaker_kind: string;
87
87
  content: string;
88
88
  }>;
89
- export declare function parseMarkdownBatch(text: string, batchPlan: Dict): Dict;
89
+ export interface ParseMarkdownBatchOptions {
90
+ /**
91
+ * When true, the parser treats input as a single-episode fragment:
92
+ * - batchPlan fields beyond `episode` may be omitted (sensible defaults applied)
93
+ * - the `# 资产` section is optional (already accepted by the strict parser, but explicitly tolerated here)
94
+ * - scene numbers need not start from 1 or be consecutive
95
+ * - error messages use "fragment" terminology
96
+ *
97
+ * Default false preserves existing direct init behavior (zero regression).
98
+ */
99
+ fragmentMode?: boolean;
100
+ }
101
+ export declare function parseMarkdownBatch(text: string, batchPlan: Dict, opts?: ParseMarkdownBatchOptions): Dict;
90
102
  export declare function expandCompactEpisodeResult(result: Dict, episodePlan: Dict): Dict;
91
103
  export declare function compactEpisodeResult(result: Dict): Dict;
92
104
  export declare function compactBatchResult(result: Dict): Dict;
@@ -1205,9 +1205,13 @@ export function parseSpeakerRef(raw) {
1205
1205
  if (!value)
1206
1206
  return ["actor", ""];
1207
1207
  if (value.includes(":") || value.includes(":")) {
1208
+ // reSplit only emits the separator slot when the pattern has a capture group;
1209
+ // /[::]/ doesn't, so the result is ["<kind>", "<name>"] — name lives at [1].
1210
+ // (Earlier code read parts[2], which is always undefined here and silently
1211
+ // dropped the name, leaving e.g. `system:李老板` as ["system", ""].)
1208
1212
  const parts = reSplit(/[::]/, value, 1);
1209
1213
  const left = parts[0] ?? "";
1210
- const right = parts[2] ?? "";
1214
+ const right = parts[1] ?? "";
1211
1215
  const kind = _MD_SPEAKER_KIND_NORM[left.trim().toLowerCase()] ?? _MD_SPEAKER_KIND_NORM[left.trim()];
1212
1216
  if (kind)
1213
1217
  return [kind, right.trim()];
@@ -1326,15 +1330,18 @@ export function parseOverlapMarkdownLines(content, scene) {
1326
1330
  lineContent = pieces.slice(2).join(":");
1327
1331
  }
1328
1332
  else {
1333
+ // reSplit on /[::]/ (no capture group) emits ["<left>", "<right>"];
1334
+ // the name is at [1], NOT [2]. Same off-by-one pattern that bit
1335
+ // parseSpeakerRef earlier — caught by tests/parser-overlap.test.ts now.
1329
1336
  const parts = reSplit(/[::]/, part, 1);
1330
1337
  rawSpeaker = parts[0] ?? "";
1331
- lineContent = parts[2] ?? "";
1338
+ lineContent = parts[1] ?? "";
1332
1339
  }
1333
1340
  }
1334
1341
  else {
1335
1342
  const parts = reSplit(/[::]/, part, 1);
1336
1343
  rawSpeaker = parts[0] ?? "";
1337
- lineContent = parts[2] ?? "";
1344
+ lineContent = parts[1] ?? "";
1338
1345
  }
1339
1346
  const entry = speakerRefEntry(scene, rawSpeaker);
1340
1347
  lineContent = lineContent.trim();
@@ -1388,8 +1395,11 @@ function applyMarkdownDialogueAnchor(action, scene, rawSpeakerArg, rawTag) {
1388
1395
  if (emotion)
1389
1396
  action["emotion"] = emotion;
1390
1397
  }
1391
- export function parseMarkdownBatch(text, batchPlan) {
1392
- const label = `${batchPlan["batch_id"] || "batch"} episode ${batchPlan["episode"]} part ${batchPlan["part"]}`;
1398
+ export function parseMarkdownBatch(text, batchPlan, opts = {}) {
1399
+ const fragmentMode = opts.fragmentMode === true;
1400
+ const labelKind = fragmentMode ? "fragment" : "batch";
1401
+ const labelPart = batchPlan["part"] === undefined && fragmentMode ? 1 : batchPlan["part"];
1402
+ const label = `${batchPlan["batch_id"] || labelKind} episode ${batchPlan["episode"]} part ${labelPart}`;
1393
1403
  let section = null;
1394
1404
  let assetSubsection = null;
1395
1405
  const scenes = [];
@@ -1603,7 +1613,10 @@ export function parseMarkdownBatch(text, batchPlan) {
1603
1613
  }
1604
1614
  }
1605
1615
  if (scenes.length === 0) {
1606
- throw _mdInvalid(label, "no scenes parsed; expected '## 场景 N' headers under '# 剧本'");
1616
+ const hint = fragmentMode
1617
+ ? "no scenes parsed in fragment; expected at least one '## 场景 N' header under '# 剧本'"
1618
+ : "no scenes parsed; expected '## 场景 N' headers under '# 剧本'";
1619
+ throw _mdInvalid(label, hint);
1607
1620
  }
1608
1621
  return {
1609
1622
  episode: Number(batchPlan["episode"] ?? 0),