@lingjingai/scriptctl 0.28.0 → 0.28.2

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.
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { CliError, DEFAULT_BATCH_MAX_CHARS, DEFAULT_BATCH_MIN_LINES, DEFAULT_BATCH_MODE, DEFAULT_BATCH_TARGET_LINES, DEFAULT_CONCURRENCY, DEFAULT_MODEL, DEFAULT_PROVIDER, DEFAULT_SUMMARY_GROUP_SIZE, DEFAULT_SUMMARY_REDUCE_THRESHOLD_CHARS, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE, SUPPORTED_EXTS, deletePath, directDir, exists, fmtId, readJson, readText, sha256Text, writeJson, } from "../common.js";
4
- import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, DEFAULT_TITLE_CHUNK_SIZE, chunkEpisodesNeedingTitles, applyEpisodeTitlesToPlan, cleanSynopsisList, episodeTitleResponseMap, extractBatchWithRecovery, mergeEpisodeResults, normalizeEpisodeResult, planSynopsisReduction, readCarriedSynopsis, sortDeep, uniqueAdd, validateEpisodeExtractionQuality, _md_push_asset, curateScriptAssets, applyMetadataToScript, } from "../domain/direct-core.js";
4
+ import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, DEFAULT_TITLE_CHUNK_SIZE, chunkEpisodesNeedingTitles, applyEpisodeTitlesToPlan, cleanSynopsisList, episodeTitleResponseMap, extractBatchWithRecovery, mergeEpisodeResultsWithReport, normalizeEpisodeResult, planSynopsisReduction, readCarriedSynopsis, sortDeep, uniqueAdd, validateEpisodeExtractionQuality, _md_push_asset, curateScriptAssets, applyMetadataToScript, } from "../domain/direct-core.js";
5
5
  import { validateScript } from "../domain/script/validate.js";
6
6
  import { makeProvider, providerCapabilities } from "../infra/providers.js";
7
7
  import { makeSourceManifest, prepareSource, } from "../infra/converters.js";
@@ -211,13 +211,11 @@ function statusFromBatchState(state) {
211
211
  function stageSnapshot(name, status, completed, total, message) {
212
212
  const boundedTotal = Math.max(0, total);
213
213
  const boundedCompleted = Math.max(0, Math.min(completed, boundedTotal));
214
- const percent = boundedTotal > 0 ? Math.round((boundedCompleted / boundedTotal) * 100) : status === "success" ? 100 : 0;
215
214
  return {
216
215
  name,
217
216
  status,
218
217
  completed: boundedCompleted,
219
218
  total: boundedTotal,
220
- percent,
221
219
  pending: status === "skipped" ? 0 : Math.max(0, boundedTotal - boundedCompleted),
222
220
  warning: status === "warning" ? 1 : 0,
223
221
  error: status === "error" ? 1 : 0,
@@ -229,6 +227,17 @@ function fileStage(name, filePath, runningStage) {
229
227
  return stageSnapshot(name, "success", 1, 1);
230
228
  return stageSnapshot(name, runningStage === name ? "running" : "pending", 0, 1);
231
229
  }
230
+ function orderedFileStage(name, filePath, runningStage, runStatus) {
231
+ if (!canTrustStageArtifacts(name, runningStage, runStatus)) {
232
+ return stageSnapshot(name, "pending", 0, 1);
233
+ }
234
+ return fileStage(name, filePath, runningStage ?? "");
235
+ }
236
+ function canTrustStageArtifacts(name, runningStage, runStatus) {
237
+ if (runStatus !== "init_running" || !runningStage)
238
+ return true;
239
+ return stageOrder(name) <= stageOrder(runningStage);
240
+ }
232
241
  function readJsonFile(filePath, fallback) {
233
242
  if (!exists(filePath))
234
243
  return fallback;
@@ -296,15 +305,14 @@ function userStageRows(stages) {
296
305
  }
297
306
  const rows = [];
298
307
  for (const items of byLabel.values()) {
299
- const total = items.reduce((sum, item) => sum + stageWeight(item.name), 0);
300
- const completed = items.reduce((sum, item) => sum + stageWeight(item.name) * (item.status === "skipped" ? 1 : item.percent / 100), 0);
308
+ const total = items.reduce((sum, item) => sum + item.total, 0);
309
+ const completed = items.reduce((sum, item) => sum + (item.status === "skipped" ? item.total : item.completed), 0);
301
310
  const status = items.reduce((max, item) => statusPriority(item.status) > statusPriority(max) ? item.status : max, "success");
302
311
  rows.push({
303
312
  name: items[0].name,
304
313
  status,
305
314
  completed: Math.round(completed),
306
315
  total,
307
- percent: total > 0 ? Math.round((completed / total) * 100) : 0,
308
316
  pending: items.reduce((sum, item) => sum + item.pending, 0),
309
317
  warning: items.reduce((sum, item) => sum + item.warning, 0),
310
318
  error: items.reduce((sum, item) => sum + item.error, 0),
@@ -313,31 +321,33 @@ function userStageRows(stages) {
313
321
  }
314
322
  return rows;
315
323
  }
316
- function stageWeight(name) {
324
+ function stageOrder(name) {
317
325
  switch (name) {
318
- case "source_prepare": return 3;
319
- case "episode_plan": return 7;
320
- case "batch_plan": return 5;
321
- case "episode_titles": return 5;
322
- case "batch_extract": return 45;
323
- case "episode_merge": return 8;
324
- case "episode_synopsis": return 5;
326
+ case "source_prepare": return 0;
327
+ case "episode_plan": return 1;
328
+ case "episode_titles": return 2;
329
+ case "batch_plan": return 3;
330
+ case "batch_extract": return 4;
331
+ case "episode_merge": return 5;
332
+ case "episode_synopsis": return 6;
325
333
  case "script_merge": return 7;
326
- case "asset_curation": return 5;
327
- case "metadata_extract": return 3;
328
- case "script_synopsis": return 5;
329
- case "validate": return 2;
334
+ case "asset_curation": return 8;
335
+ case "metadata_extract": return 9;
336
+ case "script_synopsis": return 10;
337
+ case "validate": return 11;
330
338
  }
331
339
  }
332
- function snapshotProgressPercent(stages) {
333
- let done = 0;
334
- let total = 0;
335
- for (const stage of stages) {
336
- const weight = stageWeight(stage.name);
337
- total += weight;
338
- done += weight * (stage.status === "skipped" ? 1 : stage.percent / 100);
339
- }
340
- return total > 0 ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0;
340
+ function stageRatio(stage) {
341
+ if (stage.status === "success" || stage.status === "skipped")
342
+ return 1;
343
+ if (stage.total <= 0)
344
+ return 0;
345
+ return Math.max(0, Math.min(1, stage.completed / stage.total));
346
+ }
347
+ function stageCountText(stage) {
348
+ if (stage.status === "skipped")
349
+ return "-";
350
+ return `${stage.completed}/${stage.total}`;
341
351
  }
342
352
  function useProgressColor() {
343
353
  return Boolean(process.stderr.isTTY) && process.env.NO_COLOR === undefined;
@@ -363,7 +373,7 @@ function bold(text) {
363
373
  }
364
374
  function progressBar(stage) {
365
375
  const width = 24;
366
- const ratio = stage.percent / 100;
376
+ const ratio = stageRatio(stage);
367
377
  const filled = Math.round(ratio * width);
368
378
  const bar = `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
369
379
  return colorStatus(stage.status, bar);
@@ -387,13 +397,12 @@ function statusMark(stage) {
387
397
  }
388
398
  function renderProgressLine(stage) {
389
399
  const status = colorStatus(stage.status, stage.status.padEnd(7));
390
- const percent = `${stage.percent}%`;
391
400
  const bits = [
392
401
  statusMark(stage),
393
402
  bold(stageLabel(stage.name)),
394
403
  status,
395
404
  progressBar(stage),
396
- `${percent} ${stageUnit(stage.name)}`,
405
+ `${stageCountText(stage)} ${stageUnit(stage.name)}`,
397
406
  ];
398
407
  if (stage.error > 0)
399
408
  bits.push(ansi(31, `error ${stage.error}`));
@@ -406,7 +415,7 @@ function renderProgressLine(stage) {
406
415
  return bits.join(" ");
407
416
  }
408
417
  function renderStageDetail(stage) {
409
- const counts = [`${stage.percent}%`];
418
+ const counts = [stageCountText(stage)];
410
419
  if (stage.error > 0)
411
420
  counts.push(ansi(31, `error ${stage.error}`));
412
421
  if (stage.warning > 0)
@@ -416,9 +425,8 @@ function renderStageDetail(stage) {
416
425
  return ` ${dim("stage")} ${stageLabel(stage.name)} ${colorStatus(stage.status, statusText(stage.status))} ${counts.join(", ")}`;
417
426
  }
418
427
  function renderEpisodeDetail(episode) {
419
- const percent = episode.batches_total > 0 ? Math.round((episode.batches_done / episode.batches_total) * 100) : 0;
420
428
  const errors = episode.error_batches.length > 0 ? ansi(31, " has errors") : "";
421
- return ` ${dim("episode")} ${episode.episode_id} ${colorStatus(episode.status, statusText(episode.status))} ${percent}%, story ${colorStatus(episode.synopsis_status, statusText(episode.synopsis_status))}${errors}`;
429
+ return ` ${dim("episode")} ${episode.episode_id} ${colorStatus(episode.status, statusText(episode.status))} ${episode.batches_done}/${episode.batches_total}, story ${colorStatus(episode.synopsis_status, statusText(episode.synopsis_status))}${errors}`;
422
430
  }
423
431
  function renderBatchDetail(batch) {
424
432
  const reason = batch.error_code || batch.error_type || batch.message;
@@ -452,11 +460,11 @@ function renderProgressFrame() {
452
460
  }
453
461
  function progressStageWithStatus(stage, status) {
454
462
  if (status === "success")
455
- return { ...stage, status, completed: stage.total, percent: 100, pending: 0, warning: 0, error: 0 };
463
+ return { ...stage, status, completed: stage.total, pending: 0, warning: 0, error: 0 };
456
464
  if (status === "warning")
457
- return { ...stage, status, completed: stage.total, percent: 100, pending: 0, warning: Math.max(1, stage.warning), error: 0 };
465
+ return { ...stage, status, completed: stage.total, pending: 0, warning: Math.max(1, stage.warning), error: 0 };
458
466
  if (status === "skipped")
459
- return { ...stage, status, percent: 100, pending: 0, warning: 0, error: 0 };
467
+ return { ...stage, status, completed: stage.total, pending: 0, warning: 0, error: 0 };
460
468
  if (status === "error")
461
469
  return { ...stage, status, error: Math.max(1, stage.error) };
462
470
  return { ...stage, status };
@@ -534,7 +542,6 @@ function emitInitProgress(workspace, stageName, fallbackStatus = "running") {
534
542
  status: fallbackStatus,
535
543
  completed: fallbackStatus === "success" ? 1 : 0,
536
544
  total: 1,
537
- percent: fallbackStatus === "success" ? 100 : 0,
538
545
  pending: fallbackStatus === "running" || fallbackStatus === "pending" ? 1 : 0,
539
546
  warning: fallbackStatus === "warning" ? 1 : 0,
540
547
  error: fallbackStatus === "error" ? 1 : 0,
@@ -556,11 +563,15 @@ function buildDirectStatusSnapshot(workspace) {
556
563
  const batches = asList(batchPlan["batches"]);
557
564
  const state = readRunState(workspace);
558
565
  const rawRunningStage = strOf(state["init_stage"]);
559
- const runningStage = rawRunningStage && rawRunningStage !== "complete" ? rawRunningStage : null;
566
+ const runningStage = rawRunningStage && rawRunningStage !== "complete" && isDirectStageName(rawRunningStage) ? rawRunningStage : null;
560
567
  const runStatus = strOf(state["status"]);
561
568
  const failedStage = runStatus === "init_failed" ? runningStage : null;
562
569
  const batchResultsDir = path.join(dd, "batch_results");
563
570
  const episodeResultsDir = path.join(dd, "episode_results");
571
+ const trustEpisodeMergeArtifacts = canTrustStageArtifacts("episode_merge", runningStage, runStatus);
572
+ const trustEpisodeSynopsisArtifacts = canTrustStageArtifacts("episode_synopsis", runningStage, runStatus);
573
+ const trustMetadataArtifacts = canTrustStageArtifacts("metadata_extract", runningStage, runStatus);
574
+ const trustScriptSynopsisArtifacts = canTrustStageArtifacts("script_synopsis", runningStage, runStatus);
564
575
  const batchesByEpisode = new Map();
565
576
  const batchDetails = [];
566
577
  let doneBatches = 0;
@@ -601,7 +612,9 @@ function buildDirectStatusSnapshot(workspace) {
601
612
  batchesByEpisode.get(detail.episode).push(detail);
602
613
  }
603
614
  const pendingBatches = batches.length - doneBatches - errorBatches;
604
- const degradedSynopsisEpisodes = new Set(asList(state["summary_degraded_episodes"]).map((n) => Number(n)));
615
+ const degradedSynopsisEpisodes = trustEpisodeSynopsisArtifacts
616
+ ? new Set(asList(state["summary_degraded_episodes"]).map((n) => Number(n)))
617
+ : new Set();
605
618
  const episodeSnapshots = [];
606
619
  let completeEpisodes = 0;
607
620
  let episodeResults = 0;
@@ -613,10 +626,10 @@ function buildDirectStatusSnapshot(workspace) {
613
626
  const batchesError = epBatches.filter((b) => b.status === "error").length;
614
627
  const batchesPending = epBatches.length - batchesDone - batchesError;
615
628
  const epResultPath = episodeResultPath(episodeResultsDir, episode);
616
- const hasEpisodeResult = readableJsonFile(epResultPath);
629
+ const hasEpisodeResult = trustEpisodeMergeArtifacts && readableJsonFile(epResultPath);
617
630
  if (hasEpisodeResult)
618
631
  episodeResults++;
619
- const synopsis = readEpisodeSynopsis(episodeResultsDir, episode);
632
+ const synopsis = trustEpisodeSynopsisArtifacts ? readEpisodeSynopsis(episodeResultsDir, episode) : null;
620
633
  if (synopsis)
621
634
  episodeSynopses++;
622
635
  const status = batchesError > 0
@@ -694,22 +707,25 @@ function buildDirectStatusSnapshot(workspace) {
694
707
  ...stageSnapshot("episode_synopsis", episodeSynopsisStatus, episodeSynopses, episodes.length),
695
708
  warning: synopsisWarning,
696
709
  };
697
- const scriptStage = fileStage("script_merge", path.join(dd, "script.initial.json"), runningStage ?? "");
698
- const curationStage = fileStage("asset_curation", path.join(dd, "asset_curation.json"), runningStage ?? "");
699
- const metadataStage = fileStage("metadata_extract", path.join(dd, "asset_metadata.json"), runningStage ?? "");
710
+ const scriptStage = orderedFileStage("script_merge", path.join(dd, "script.initial.json"), runningStage, runStatus);
711
+ const curationStage = orderedFileStage("asset_curation", path.join(dd, "asset_curation.json"), runningStage, runStatus);
712
+ const metadataStage = Boolean(state["metadata_skipped"]) && trustMetadataArtifacts
713
+ ? stageSnapshot("metadata_extract", "skipped", 0, 1, "metadata: skipped (--skip-metadata)")
714
+ : orderedFileStage("metadata_extract", path.join(dd, "asset_metadata.json"), runningStage, runStatus);
700
715
  const synopsisPath = path.join(dd, "synopsis.json");
701
716
  const summaryLine = formatSummaryStatusLine(Boolean(state["summary_skipped"]), Boolean(state["summary_degraded"]), asList(state["summary_degraded_episodes"]).map((n) => Number(n)));
702
- const scriptSynopsisStatus = exists(synopsisPath)
717
+ const hasScriptSynopsis = trustScriptSynopsisArtifacts && exists(synopsisPath);
718
+ const scriptSynopsisStatus = hasScriptSynopsis
703
719
  ? Boolean(state["summary_degraded"]) ? "warning" : "success"
704
- : Boolean(state["summary_skipped"])
720
+ : trustScriptSynopsisArtifacts && Boolean(state["summary_skipped"])
705
721
  ? "skipped"
706
722
  : runningStage === "script_synopsis"
707
723
  ? "running"
708
724
  : "pending";
709
- const scriptSynopsisStage = stageSnapshot("script_synopsis", scriptSynopsisStatus, exists(synopsisPath) ? 1 : 0, 1, summaryLine ?? "");
725
+ const scriptSynopsisStage = stageSnapshot("script_synopsis", scriptSynopsisStatus, hasScriptSynopsis ? 1 : 0, 1, summaryLine ?? "");
710
726
  const validationPath = path.join(dd, "validation.json");
711
- let validationStage = fileStage("validate", validationPath, runningStage ?? "");
712
- if (exists(validationPath)) {
727
+ let validationStage = orderedFileStage("validate", validationPath, runningStage, runStatus);
728
+ if (exists(validationPath) && !(runStatus === "init_running" && runningStage && stageOrder("validate") > stageOrder(runningStage))) {
713
729
  const validation = readJson(validationPath);
714
730
  validationStage = stageSnapshot("validate", Boolean(validation["passed"]) ? "success" : "warning", 1, 1);
715
731
  }
@@ -735,12 +751,11 @@ function buildDirectStatusSnapshot(workspace) {
735
751
  overallStatus = "running";
736
752
  else if (maxStage === "warning")
737
753
  overallStatus = "warning";
738
- else if (stages.filter((s) => s.name !== "script_synopsis").every((s) => s.status === "success" || s.status === "warning"))
754
+ else if (stages.filter((s) => s.name !== "script_synopsis").every((s) => s.status === "success" || s.status === "warning" || s.status === "skipped"))
739
755
  overallStatus = "success";
740
756
  return {
741
757
  overall_status: overallStatus,
742
758
  running_stage: runningStage,
743
- progress_percent: snapshotProgressPercent(stages),
744
759
  stages,
745
760
  episodes: episodeSnapshots,
746
761
  batches: batchDetails,
@@ -750,18 +765,17 @@ function renderDirectStatusResult(snapshot) {
750
765
  const completedEpisodes = snapshot.episodes.filter((episode) => episode.status === "success").length;
751
766
  const result = [
752
767
  "Overall",
753
- `overall: ${statusText(snapshot.overall_status)}; current: ${snapshot.running_stage && isDirectStageName(snapshot.running_stage) ? stageLabel(snapshot.running_stage) : "-"}; progress: ${snapshot.progress_percent}%`,
754
- snapshot.episodes.length > 0 ? `episodes: ${Math.round((completedEpisodes / snapshot.episodes.length) * 100)}% 完成` : "episodes: 等待中",
768
+ `overall: ${statusText(snapshot.overall_status)}; current: ${snapshot.running_stage && isDirectStageName(snapshot.running_stage) ? stageLabel(snapshot.running_stage) : "-"}`,
769
+ snapshot.episodes.length > 0 ? `episodes: ${completedEpisodes}/${snapshot.episodes.length} 完成` : "episodes: 等待中",
755
770
  "Stages",
756
771
  ];
757
772
  for (const stage of userStageRows(snapshot.stages)) {
758
- result.push(`${stageLabel(stage.name)}: ${statusText(stage.status)} ${stage.percent}%${stage.error ? `, error ${stage.error}` : ""}${stage.warning ? `, warning ${stage.warning}` : ""}${stage.status === "pending" ? ", 等待中" : ""}${stage.message ? ` - ${stage.message}` : ""}`);
773
+ result.push(`${stageLabel(stage.name)}: ${statusText(stage.status)} ${stageCountText(stage)}${stage.error ? `, error ${stage.error}` : ""}${stage.warning ? `, warning ${stage.warning}` : ""}${stage.status === "pending" ? ", 等待中" : ""}${stage.message ? ` - ${stage.message}` : ""}`);
759
774
  }
760
775
  result.push("Episodes");
761
776
  for (const episode of snapshot.episodes) {
762
- const percent = episode.batches_total > 0 ? Math.round((episode.batches_done / episode.batches_total) * 100) : 0;
763
777
  const errors = episode.error_batches.length > 0 ? `, error ids: ${episode.error_batches.join(",")}` : "";
764
- result.push(`${episode.episode_id}: ${statusText(episode.status)} ${percent}%, story ${statusText(episode.synopsis_status)}${errors}`);
778
+ result.push(`${episode.episode_id}: ${statusText(episode.status)} ${episode.batches_done}/${episode.batches_total}, story ${statusText(episode.synopsis_status)}${errors}`);
765
779
  }
766
780
  result.push("Details");
767
781
  const noisyBatches = snapshot.batches.filter((batch) => batch.status !== "success");
@@ -1288,6 +1302,7 @@ async function commandInitImpl(opts) {
1288
1302
  // --resummarize forces the summary LLM to re-run even when structure is
1289
1303
  // reused (e.g. after a prompt change).
1290
1304
  const skipSummary = Boolean(opts["skip_summary"]);
1305
+ const skipMetadata = Boolean(opts["skip_metadata"]);
1291
1306
  const resummarize = Boolean(opts["resummarize"]);
1292
1307
  const summaryThresholdChars = parsePositiveIntOption(opts["summary_threshold_chars"], "--summary-threshold-chars", "USAGE ERROR: Invalid summary threshold", DEFAULT_SUMMARY_REDUCE_THRESHOLD_CHARS);
1293
1308
  const concurrency = parsePositiveIntOption(opts["concurrency"], "--concurrency", "USAGE ERROR: Invalid concurrency", DEFAULT_CONCURRENCY);
@@ -1339,6 +1354,7 @@ async function commandInitImpl(opts) {
1339
1354
  model,
1340
1355
  concurrency,
1341
1356
  source_path: path.resolve(source),
1357
+ metadata_skipped: skipMetadata,
1342
1358
  });
1343
1359
  emitInitProgress(workspace, "source_prepare");
1344
1360
  let info;
@@ -1624,7 +1640,7 @@ async function commandInitImpl(opts) {
1624
1640
  result: [
1625
1641
  `episodes: ${results.length}/${totalEpisodes} complete`,
1626
1642
  `incomplete episodes: ${incompleteEpisodes.join(", ")}`,
1627
- `content progress: ${totalBatches > 0 ? Math.round((doneBatches / totalBatches) * 100) : 0}% done, ${erroredBatches} error`,
1643
+ `content: ${doneBatches}/${totalBatches} done, ${erroredBatches} error`,
1628
1644
  ...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
1629
1645
  `provider: ${providerName}`,
1630
1646
  ],
@@ -1656,9 +1672,15 @@ async function commandInitImpl(opts) {
1656
1672
  last_error: null,
1657
1673
  });
1658
1674
  let script;
1675
+ const assetNormalizationPath = path.join(dd, "asset_normalization.json");
1676
+ const assetMetadataPath = path.join(dd, "asset_metadata.json");
1677
+ let assetNormalization = null;
1659
1678
  try {
1660
1679
  emitInitProgress(workspace, "script_merge");
1661
- script = mergeEpisodeResults(results, strOf(info["projectName"]) || path.basename(source, path.extname(source)));
1680
+ const merged = mergeEpisodeResultsWithReport(results, strOf(info["projectName"]) || path.basename(source, path.extname(source)));
1681
+ script = merged.script;
1682
+ assetNormalization = merged.assetNormalization;
1683
+ writeJson(assetNormalizationPath, assetNormalization);
1662
1684
  }
1663
1685
  catch (exc) {
1664
1686
  const e = exc;
@@ -1695,39 +1717,47 @@ async function commandInitImpl(opts) {
1695
1717
  });
1696
1718
  }
1697
1719
  emitInitProgress(workspace, "asset_curation", "success");
1698
- try {
1699
- updateRunState(workspace, { status: "init_running", init_stage: "metadata_extract" });
1700
- emitInitProgress(workspace, "metadata_extract");
1701
- let metadata = provider.extractMetadata ? await provider.extractMetadata(sourceText, script) : {};
1702
- if (!isDict(metadata))
1703
- metadata = {};
1704
- writeJson(path.join(dd, "asset_metadata.json"), metadata);
1705
- applyMetadataToScript(script, metadata);
1720
+ if (skipMetadata) {
1721
+ updateRunState(workspace, { status: "init_running", init_stage: "metadata_extract", metadata_skipped: true });
1722
+ if (exists(assetMetadataPath))
1723
+ deletePath(assetMetadataPath);
1724
+ emitInitProgress(workspace, "metadata_extract", "skipped");
1706
1725
  }
1707
- catch (exc) {
1708
- if (exc instanceof CliError) {
1726
+ else {
1727
+ try {
1728
+ updateRunState(workspace, { status: "init_running", init_stage: "metadata_extract", metadata_skipped: false });
1729
+ emitInitProgress(workspace, "metadata_extract");
1730
+ let metadata = provider.extractMetadata ? await provider.extractMetadata(sourceText, script) : {};
1731
+ if (!isDict(metadata))
1732
+ metadata = {};
1733
+ writeJson(assetMetadataPath, metadata);
1734
+ applyMetadataToScript(script, metadata);
1735
+ }
1736
+ catch (exc) {
1737
+ if (exc instanceof CliError) {
1738
+ throw initFailedReport(workspace, {
1739
+ title: exc.title,
1740
+ stage: "metadata_extract",
1741
+ exitCode: exc.exitCode,
1742
+ errorCode: exc.errorCode,
1743
+ required: exc.required.length > 0 ? exc.required : ["metadata JSON matching final script contract"],
1744
+ received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
1745
+ nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
1746
+ updates: { episode_completed: results.length },
1747
+ });
1748
+ }
1749
+ const e = exc;
1709
1750
  throw initFailedReport(workspace, {
1710
- title: exc.title,
1751
+ title: "INIT FAILED: Metadata extraction failed",
1711
1752
  stage: "metadata_extract",
1712
- exitCode: exc.exitCode,
1713
- errorCode: exc.errorCode,
1714
- required: exc.required.length > 0 ? exc.required : ["metadata JSON matching final script contract"],
1715
- received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
1716
- nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
1753
+ required: ["provider metadata for worldview, role_type, and asset descriptions"],
1754
+ received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
1755
+ nextSteps: ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
1717
1756
  updates: { episode_completed: results.length },
1718
1757
  });
1719
1758
  }
1720
- const e = exc;
1721
- throw initFailedReport(workspace, {
1722
- title: "INIT FAILED: Metadata extraction failed",
1723
- stage: "metadata_extract",
1724
- required: ["provider metadata for worldview, role_type, and asset descriptions"],
1725
- received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
1726
- nextSteps: ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
1727
- updates: { episode_completed: results.length },
1728
- });
1759
+ emitInitProgress(workspace, "metadata_extract", "success");
1729
1760
  }
1730
- emitInitProgress(workspace, "metadata_extract", "success");
1731
1761
  // Whole-script overview (reduce-2). Soft: a non-essential concept enhancement,
1732
1762
  // the opposite of batch extraction's hard-fail policy — failures degrade the
1733
1763
  // overview (recorded as summary_degraded + a sanitized reason) and NEVER block init.
@@ -1807,6 +1837,7 @@ async function commandInitImpl(opts) {
1807
1837
  summary_degraded: summaryDegraded,
1808
1838
  summary_degraded_episodes: summaryDegradedEpisodes,
1809
1839
  summary_degraded_reason: summaryDegradeReason || null,
1840
+ metadata_skipped: skipMetadata,
1810
1841
  last_error: null,
1811
1842
  });
1812
1843
  emitInitProgress(workspace, "validate", passed ? "success" : "warning");
@@ -1815,6 +1846,12 @@ async function commandInitImpl(opts) {
1815
1846
  : "INIT NEEDS AGENT: Initial script written with repair issues";
1816
1847
  const stats = validation["stats"] ?? {};
1817
1848
  const summaryLine = formatSummaryStatusLine(skipSummary, summaryDegraded, summaryDegradedEpisodes);
1849
+ const normalizationSummary = assetNormalization?.summary ?? {
1850
+ asset_merges: 0,
1851
+ location_states_from_names: 0,
1852
+ location_states_from_time: 0,
1853
+ review_items: 0,
1854
+ };
1818
1855
  const report = {
1819
1856
  title,
1820
1857
  result: [
@@ -1824,7 +1861,9 @@ async function commandInitImpl(opts) {
1824
1861
  `validation: ${passed ? "passed" : "needs repair"}`,
1825
1862
  `provider: ${providerName}`,
1826
1863
  `episodes: ${results.length}/${totalEpisodes} complete`,
1827
- `content progress: ${totalBatches > 0 ? Math.round((doneBatches / totalBatches) * 100) : 0}% done`,
1864
+ `content: ${doneBatches}/${totalBatches} done`,
1865
+ `normalization: merges=${normalizationSummary.asset_merges}, states=${normalizationSummary.location_states_from_names + normalizationSummary.location_states_from_time}, review=${normalizationSummary.review_items}`,
1866
+ ...(skipMetadata ? ["metadata: skipped"] : []),
1828
1867
  ...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
1829
1868
  ...(summaryLine ? [summaryLine] : []),
1830
1869
  ],
@@ -1835,8 +1874,9 @@ async function commandInitImpl(opts) {
1835
1874
  path.join(dd, "batch_plan.json"),
1836
1875
  batchResultsDir,
1837
1876
  episodeResultsDir,
1877
+ assetNormalizationPath,
1838
1878
  path.join(dd, "asset_curation.json"),
1839
- path.join(dd, "asset_metadata.json"),
1879
+ ...(skipMetadata ? [] : [assetMetadataPath]),
1840
1880
  path.join(dd, "synopsis.json"),
1841
1881
  scriptPath,
1842
1882
  path.join(dd, "validation.json"),
@@ -1883,7 +1923,6 @@ export function commandStatus(opts) {
1883
1923
  artifacts: [path.join(dd, "batch_results"), path.join(dd, "episode_results"), path.join(dd, "run_state.json")],
1884
1924
  overall_status: snapshot.overall_status,
1885
1925
  running_stage: snapshot.running_stage,
1886
- progress_percent: snapshot.progress_percent,
1887
1926
  stages: snapshot.stages,
1888
1927
  episodes: snapshot.episodes,
1889
1928
  batches: snapshot.batches,
@@ -2114,6 +2153,24 @@ function renderAssetCurationSummary(curation) {
2114
2153
  }
2115
2154
  return lines;
2116
2155
  }
2156
+ function renderAssetNormalizationSummary(normalization) {
2157
+ const summary = isDict(normalization["summary"]) ? normalization["summary"] : {};
2158
+ const lines = [
2159
+ `normalization: ` +
2160
+ `merges ${summary["asset_merges"] ?? 0}, ` +
2161
+ `location-states ${(Number(summary["location_states_from_names"] ?? 0) + Number(summary["location_states_from_time"] ?? 0))}, ` +
2162
+ `review ${summary["review_items"] ?? 0}`,
2163
+ ];
2164
+ for (const decision of asList(normalization["decisions"])) {
2165
+ if (!isDict(decision))
2166
+ continue;
2167
+ const action = strOf(decision["action"]);
2168
+ if (action !== "canonical_merge" && action !== "location_state_from_name" && action !== "location_state_from_time")
2169
+ continue;
2170
+ lines.push(`normalization ${action}: ${decision["from"] || "-"} -> ${decision["into"] || "-"}${decision["state"] ? ` state=${decision["state"]}` : ""} — ${decision["reason"] || "-"}`);
2171
+ }
2172
+ return lines;
2173
+ }
2117
2174
  // Collect batch extraction failures (batch_results/<key>.error.json) keyed by
2118
2175
  // episode. Shared by both review views — these are direct-stage failures the
2119
2176
  // final-script `issues` query can't see.
@@ -2208,6 +2265,12 @@ export function commandReview(opts) {
2208
2265
  // (validation issues recomputed live, plus batch extraction failures).
2209
2266
  const lines = [];
2210
2267
  const curationPath = path.join(dd, "asset_curation.json");
2268
+ const normalizationPath = path.join(dd, "asset_normalization.json");
2269
+ if (exists(normalizationPath)) {
2270
+ const normalization = readJson(normalizationPath);
2271
+ if (isDict(normalization))
2272
+ lines.push(...renderAssetNormalizationSummary(normalization));
2273
+ }
2211
2274
  if (exists(curationPath)) {
2212
2275
  const curation = readJson(curationPath);
2213
2276
  if (isDict(curation))
@@ -2236,7 +2299,7 @@ export function commandReview(opts) {
2236
2299
  const report = {
2237
2300
  title: "DIRECT REVIEW",
2238
2301
  result: lines.length > 0 ? lines : ["No issues, curation decisions, or batch failures to review."],
2239
- artifacts: [scriptPath, path.join(dd, "asset_curation.json"), batchResultsDir],
2302
+ artifacts: [scriptPath, normalizationPath, path.join(dd, "asset_curation.json"), batchResultsDir],
2240
2303
  next: [
2241
2304
  "Run `direct review --episode <n>` for source↔extract alignment.",
2242
2305
  "Edit with `--draft` (e.g. `scriptctl rename … --draft`), then validate/export.",