@lingjingai/scriptctl 0.32.0 → 0.33.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.
@@ -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_DIRECT_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, expandCompactEpisodeResult, buildBatchPlan, buildEpisodePlan, DEFAULT_TITLE_CHUNK_SIZE, chunkEpisodesNeedingTitles, applyEpisodeTitlesToPlan, cleanSynopsisList, episodeTitleResponseMap, extractBatchWithRecovery, mergeEpisodeResultsWithReport, normalizeEpisodeResult, planSynopsisReduction, readCarriedSynopsis, sortDeep, uniqueAdd, validateEpisodeExtractionQuality, _md_push_asset, applyAssetGroupingDuplicateMerges, applyStateBindingPlan, bindDefaultStateRefsInScript, buildAssetCurationAuditContextText, buildAssetCurationContextText, buildAssetCurationRepairContextText, buildAssetGroupingContextText, buildLocationCanonicalizationAuditContextText, buildPrimaryAssetCurationChunks, groupedAssetCurationChunks, combineLocationCanonicalizationPlan, buildStateBindingContexts, assetCurationAuditSourceKeys, combineAssetCurationExpressionPlans, combinePrimaryAssetCurationPlans, combineAssetCurationPlans, curateScriptAssets, ensureDefaultStates, assetCurationAuditCandidateSourceKeys, locationCanonicalizationCandidateSourceKeys, missingAssetCurationRequiredSourceKeys, normalizeStateBindingPlanShape, parseAssetCurationExpressions, parseAssetCurationExpressionsTolerant, parseAssetGroupingExpressions, assertLocationCanonicalizationPlan, applyMetadataToScript, validateAssetGroupingPlan, } from "../domain/direct-core.js";
4
+ import { compactBatchResult, compactEpisodeResult, expandCompactEpisodeResult, buildBatchPlan, buildEpisodePlan, DEFAULT_TITLE_CHUNK_SIZE, chunkEpisodesNeedingTitles, applyEpisodeTitlesToPlan, cleanSynopsisList, episodeTitleResponseMap, extractBatchWithRecovery, mergeEpisodeResultsWithReport, normalizeEpisodeResult, planSynopsisReduction, readCarriedSynopsis, sortDeep, uniqueAdd, validateEpisodeExtractionQuality, _md_push_asset, applyAssetGroupingDuplicateMerges, applyStateCurationPlan, applyStateBindingPlan, bindDefaultStateRefsInScript, buildAssetCurationAuditContextText, buildAssetCurationContextText, buildAssetCurationRepairContextText, buildAssetGroupingContextText, buildLocationCanonicalizationAuditContextText, buildPrimaryAssetCurationChunks, buildStateCurationChunks, buildStateCurationContextText, groupedAssetCurationChunks, combineLocationCanonicalizationPlan, buildStateBindingContexts, assetCurationAuditSourceKeys, combineAssetCurationExpressionPlans, combinePrimaryAssetCurationPlans, combineAssetCurationPlans, curateScriptAssets, ensureDefaultStates, assetCurationAuditCandidateSourceKeys, locationCanonicalizationCandidateSourceKeys, missingAssetCurationRequiredSourceKeys, normalizeStateBindingPlanShape, parseStateCurationExpressions, parseAssetCurationExpressions, parseAssetCurationExpressionsTolerant, parseAssetGroupingExpressions, assertLocationCanonicalizationPlan, applyMetadataToScript, validateAssetGroupingPlan, stateCurationSourceKeysForRefs, assertStateCurationPlan, } 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 { remoteScriptOutputStoreFromEnv } from "../infra/script-output-api.js";
@@ -360,6 +360,7 @@ function isDirectStageName(value) {
360
360
  case "episode_synopsis":
361
361
  case "script_merge":
362
362
  case "asset_curation":
363
+ case "state_curation":
363
364
  case "state_binding":
364
365
  case "metadata_extract":
365
366
  case "script_synopsis":
@@ -501,6 +502,7 @@ function stageLabel(name) {
501
502
  case "episode_synopsis": return "补充信息";
502
503
  case "script_merge": return "生成剧本";
503
504
  case "asset_curation": return "补充信息";
505
+ case "state_curation": return "补充信息";
504
506
  case "state_binding": return "补充信息";
505
507
  case "metadata_extract": return "补充信息";
506
508
  case "script_synopsis": return "补充信息";
@@ -562,10 +564,11 @@ function stageOrder(name) {
562
564
  case "episode_synopsis": return 6;
563
565
  case "script_merge": return 7;
564
566
  case "asset_curation": return 8;
565
- case "state_binding": return 9;
566
- case "metadata_extract": return 10;
567
- case "script_synopsis": return 11;
568
- case "validate": return 12;
567
+ case "state_curation": return 9;
568
+ case "state_binding": return 10;
569
+ case "metadata_extract": return 11;
570
+ case "script_synopsis": return 12;
571
+ case "validate": return 13;
569
572
  }
570
573
  }
571
574
  function stageRatio(stage) {
@@ -801,6 +804,7 @@ function buildDirectStatusSnapshot(workspace) {
801
804
  const episodeResultsDir = path.join(dd, "episode_results");
802
805
  const trustEpisodeMergeArtifacts = canTrustStageArtifacts("episode_merge", runningStage, runStatus);
803
806
  const trustEpisodeSynopsisArtifacts = canTrustStageArtifacts("episode_synopsis", runningStage, runStatus);
807
+ const trustStateCurationArtifacts = canTrustStageArtifacts("state_curation", runningStage, runStatus);
804
808
  const trustStateBindingArtifacts = canTrustStageArtifacts("state_binding", runningStage, runStatus);
805
809
  const trustMetadataArtifacts = canTrustStageArtifacts("metadata_extract", runningStage, runStatus);
806
810
  const trustScriptSynopsisArtifacts = canTrustStageArtifacts("script_synopsis", runningStage, runStatus);
@@ -941,6 +945,9 @@ function buildDirectStatusSnapshot(workspace) {
941
945
  };
942
946
  const scriptStage = orderedFileStage("script_merge", path.join(dd, "script.initial.json"), runningStage, runStatus);
943
947
  const curationStage = orderedFileStage("asset_curation", path.join(dd, "asset_curation.json"), runningStage, runStatus);
948
+ const stateCurationStage = trustStateCurationArtifacts
949
+ ? orderedFileStage("state_curation", path.join(dd, "state_curation.json"), runningStage, runStatus)
950
+ : stageSnapshot("state_curation", runningStage === "state_curation" ? "running" : "pending", 0, 1);
944
951
  const stateBindingStage = trustStateBindingArtifacts
945
952
  ? orderedFileStage("state_binding", path.join(dd, "state_binding.json"), runningStage, runStatus)
946
953
  : stageSnapshot("state_binding", runningStage === "state_binding" ? "running" : "pending", 0, 1);
@@ -975,6 +982,7 @@ function buildDirectStatusSnapshot(workspace) {
975
982
  episodeSynopsisStage,
976
983
  scriptStage,
977
984
  curationStage,
985
+ stateCurationStage,
978
986
  stateBindingStage,
979
987
  metadataStage,
980
988
  scriptSynopsisStage,
@@ -1815,7 +1823,7 @@ function buildIncompleteReport(ctx) {
1815
1823
  return [report, EXIT_RUNTIME];
1816
1824
  }
1817
1825
  function initPreFanoutStages() {
1818
- const end = STAGES.findIndex((stage) => stage.name === "asset-curation");
1826
+ const end = STAGES.findIndex((stage) => stage.name === "state-curation");
1819
1827
  return STAGES.slice(0, end + 1);
1820
1828
  }
1821
1829
  function validateOnlyStages() {
@@ -2485,8 +2493,8 @@ export async function stageScriptMerge(ctx) {
2485
2493
  }
2486
2494
  }
2487
2495
  // --- asset-curation -------------------------------------------------------
2488
- const ASSET_GROUPING_CACHE_VERSION = 2;
2489
- const ASSET_CURATION_CACHE_VERSION = 2;
2496
+ const ASSET_GROUPING_CACHE_VERSION = 3;
2497
+ const ASSET_CURATION_CACHE_VERSION = 4;
2490
2498
  function assetCacheIdentity(options) {
2491
2499
  return {
2492
2500
  provider: options.providerName,
@@ -3150,6 +3158,211 @@ export async function stageAssetCuration(ctx) {
3150
3158
  }
3151
3159
  ctx.emitProgress("asset-curation", "success");
3152
3160
  }
3161
+ // --- state-curation -------------------------------------------------------
3162
+ const STATE_CURATION_CACHE_VERSION = 1;
3163
+ function stateCurationScriptFingerprint(script, options) {
3164
+ return sha256Text(JSON.stringify(sortDeep({
3165
+ cache_version: STATE_CURATION_CACHE_VERSION,
3166
+ ...assetCacheIdentity(options),
3167
+ script: {
3168
+ actors: script["actors"],
3169
+ locations: script["locations"],
3170
+ props: script["props"],
3171
+ episodes: script["episodes"],
3172
+ },
3173
+ })));
3174
+ }
3175
+ function stateCurationContextFingerprint(scriptFingerprint, partId, refs) {
3176
+ return sha256Text(JSON.stringify(sortDeep({
3177
+ script_fingerprint: scriptFingerprint,
3178
+ part_id: partId,
3179
+ refs,
3180
+ })));
3181
+ }
3182
+ function combineStateCurationPlans(parts) {
3183
+ const decisions = parts.flatMap((part) => asList(part.plan["decisions"]).filter(isDict));
3184
+ const expressions = parts.flatMap((part) => asList(part.plan["expressions"]));
3185
+ return {
3186
+ version: 1,
3187
+ format: "state-curation-exp-v1",
3188
+ expression_text: expressions.length > 0 ? expressions.map((expression) => `<exp>${expression}</exp>`).join("\n") : "<exp>NOOP # no state curation changes</exp>",
3189
+ expressions,
3190
+ decisions,
3191
+ parts: parts.map((part) => ({
3192
+ part_id: part.partId,
3193
+ refs: part.refs,
3194
+ context_fingerprint: part.contextFingerprint,
3195
+ context_chars: part.contextChars,
3196
+ reused: part.reused,
3197
+ decisions: asList(part.plan["decisions"]).length,
3198
+ })),
3199
+ };
3200
+ }
3201
+ async function loadOrRunStateCurationPart(ctx, script, curateStates, scriptFingerprint, partId, refs) {
3202
+ const artifactName = `state_curation_parts/${partId}.json`;
3203
+ const contextFingerprint = stateCurationContextFingerprint(scriptFingerprint, partId, refs);
3204
+ const cached = ctx.ws.readJsonSafe(artifactName);
3205
+ const cachedPlan = !ctx.state.bypassInternalCaches
3206
+ && isDict(cached)
3207
+ && cached["context_fingerprint"] === contextFingerprint
3208
+ && cached["parse_status"] === "ok"
3209
+ && isDict(cached["plan"])
3210
+ ? cached["plan"]
3211
+ : null;
3212
+ if (cachedPlan && isList(cachedPlan["decisions"])) {
3213
+ const cachedContextChars = isDict(cached) ? Number(cached["context_chars"] ?? 0) : 0;
3214
+ return {
3215
+ partId,
3216
+ refs: [...refs],
3217
+ contextFingerprint,
3218
+ contextChars: cachedContextChars,
3219
+ plan: cachedPlan,
3220
+ reused: true,
3221
+ };
3222
+ }
3223
+ const startedAtMs = Date.now();
3224
+ const contextText = buildStateCurationContextText(script, refs);
3225
+ const requiredKeys = stateCurationSourceKeysForRefs(script, refs);
3226
+ let expressionText = "";
3227
+ try {
3228
+ expressionText = await ctx.timing.span({ name: `state_curation.${partId}`, category: "chunk", parent: "state_curation.chunks", metadata: { artifact: artifactName } }, () => curateStates(script, contextText));
3229
+ const parsed = parseStateCurationExpressions(expressionText);
3230
+ const plan = assertStateCurationPlan(script, parsed, requiredKeys);
3231
+ ctx.ws.writeJson(artifactName, {
3232
+ version: 1,
3233
+ ...assetCacheArtifactFields(ctx, STATE_CURATION_CACHE_VERSION),
3234
+ context_fingerprint: contextFingerprint,
3235
+ script_fingerprint: scriptFingerprint,
3236
+ parse_status: "ok",
3237
+ part_id: partId,
3238
+ refs,
3239
+ required_state_ids: requiredKeys,
3240
+ context_chars: contextText.length,
3241
+ expression_text: expressionText,
3242
+ duration_ms: durationMs(startedAtMs, Date.now()),
3243
+ plan,
3244
+ updated_at: checkpointTimestamp(),
3245
+ });
3246
+ return {
3247
+ partId,
3248
+ refs: [...refs],
3249
+ contextFingerprint,
3250
+ contextChars: contextText.length,
3251
+ plan,
3252
+ reused: false,
3253
+ };
3254
+ }
3255
+ catch (exc) {
3256
+ const e = exc;
3257
+ ctx.ws.writeJson(artifactName, {
3258
+ version: 1,
3259
+ ...assetCacheArtifactFields(ctx, STATE_CURATION_CACHE_VERSION),
3260
+ context_fingerprint: contextFingerprint,
3261
+ script_fingerprint: scriptFingerprint,
3262
+ parse_status: "failed",
3263
+ part_id: partId,
3264
+ refs,
3265
+ required_state_ids: requiredKeys,
3266
+ context_chars: contextText.length,
3267
+ expression_text: expressionText,
3268
+ duration_ms: durationMs(startedAtMs, Date.now()),
3269
+ error: `${e?.name ?? "Error"}: ${e?.message ?? ""}`,
3270
+ updated_at: checkpointTimestamp(),
3271
+ });
3272
+ throw exc;
3273
+ }
3274
+ }
3275
+ export async function stageStateCuration(ctx) {
3276
+ const { ws, state, options } = ctx;
3277
+ const script = ensureScript(ctx);
3278
+ let plan = null;
3279
+ try {
3280
+ ctx.updateRunState({ status: "init_running", init_stage: "state_curation" });
3281
+ ctx.emitProgress("state-curation");
3282
+ const scriptFingerprint = stateCurationScriptFingerprint(script, options);
3283
+ const cached = ws.readJsonSafe("state_curation.json");
3284
+ if (!state.bypassInternalCaches && isDict(cached) && cached["script_fingerprint"] === scriptFingerprint && isList(cached["decisions"])) {
3285
+ const curation = await ctx.timing.span({ name: "state_curation.apply", category: "substage", parent: "state_curation" }, () => applyStateCurationPlan(script, { decisions: cached["decisions"] }));
3286
+ curation["script_fingerprint"] = scriptFingerprint;
3287
+ curation["cache_version"] = STATE_CURATION_CACHE_VERSION;
3288
+ curation["provider"] = options.providerName;
3289
+ curation["model"] = options.model;
3290
+ if (isList(cached["parts"]))
3291
+ curation["parts"] = cached["parts"];
3292
+ if (typeof cached["expression_text"] === "string")
3293
+ curation["expression_text"] = cached["expression_text"];
3294
+ if (isList(cached["expressions"]))
3295
+ curation["expressions"] = cached["expressions"];
3296
+ ws.writeJson("state_curation.json", curation);
3297
+ ws.delete("state_curation.failed.json");
3298
+ ctx.emitProgress("state-curation", "success");
3299
+ return;
3300
+ }
3301
+ const chunks = buildStateCurationChunks(script);
3302
+ if (chunks.length === 0) {
3303
+ const curation = applyStateCurationPlan(script, { decisions: [] });
3304
+ curation["script_fingerprint"] = scriptFingerprint;
3305
+ curation["cache_version"] = STATE_CURATION_CACHE_VERSION;
3306
+ curation["provider"] = options.providerName;
3307
+ curation["model"] = options.model;
3308
+ curation["parts"] = [];
3309
+ curation["expression_text"] = "<exp>NOOP # no state curation changes</exp>";
3310
+ curation["expressions"] = ["NOOP # no state curation changes"];
3311
+ ws.writeJson("state_curation.json", curation);
3312
+ ws.delete("state_curation.failed.json");
3313
+ ctx.emitProgress("state-curation", "success");
3314
+ return;
3315
+ }
3316
+ const provider = ctx.provider();
3317
+ if (!provider.curateStates) {
3318
+ throw new CliError("INIT FAILED: Provider does not support state curation", "Provider does not support state curation.", {
3319
+ exitCode: EXIT_RUNTIME,
3320
+ required: ["provider.curateStates(script, contextText)"],
3321
+ received: [`provider: ${provider.name || options.providerName}`],
3322
+ nextSteps: ["Use a direct provider that supports state curation."],
3323
+ });
3324
+ }
3325
+ ws.mkdir("state_curation_parts");
3326
+ ctx.updateRunState({ state_curation_completed_chunks: 0, state_curation_total_chunks: chunks.length });
3327
+ const curateStates = provider.curateStates.bind(provider);
3328
+ let completedChunks = 0;
3329
+ const outcomes = await pMapWithConcurrency(chunks, options.concurrency, async (chunk) => {
3330
+ const part = await loadOrRunStateCurationPart(ctx, script, curateStates, scriptFingerprint, chunk.part_id, chunk.refs);
3331
+ completedChunks += 1;
3332
+ ctx.updateRunState({ state_curation_completed_chunks: completedChunks, state_curation_total_chunks: chunks.length });
3333
+ return part;
3334
+ });
3335
+ const parts = unwrapConcurrentOutcomes(outcomes).sort((left, right) => left.partId.localeCompare(right.partId));
3336
+ plan = combineStateCurationPlans(parts);
3337
+ const curation = await ctx.timing.span({ name: "state_curation.apply", category: "substage", parent: "state_curation" }, () => applyStateCurationPlan(script, plan));
3338
+ curation["script_fingerprint"] = scriptFingerprint;
3339
+ curation["cache_version"] = STATE_CURATION_CACHE_VERSION;
3340
+ curation["provider"] = options.providerName;
3341
+ curation["model"] = options.model;
3342
+ curation["expression_text"] = plan["expression_text"];
3343
+ curation["expressions"] = plan["expressions"];
3344
+ curation["parts"] = plan["parts"];
3345
+ ws.writeJson("state_curation.json", curation);
3346
+ ws.delete("state_curation.failed.json");
3347
+ }
3348
+ catch (exc) {
3349
+ const e = exc;
3350
+ ws.writeJson("state_curation.failed.json", {
3351
+ version: 1,
3352
+ failed_at: checkpointTimestamp(),
3353
+ error: `${e?.name ?? "Error"}: ${e?.message ?? ""}`,
3354
+ plan,
3355
+ });
3356
+ stageFailure(options.workspace, exc, {
3357
+ title: "INIT FAILED: State curation failed",
3358
+ stage: "state_curation",
3359
+ required: ["provider state curation decisions for the assembled script state catalog"],
3360
+ nextSteps: ["Rerun init; completed extraction and asset curation checkpoints will be reused."],
3361
+ updates: { episode_completed: state.mergedResults.length },
3362
+ });
3363
+ }
3364
+ ctx.emitProgress("state-curation", "success");
3365
+ }
3153
3366
  const STATE_BINDING_CHUNK_CACHE_VERSION = 1;
3154
3367
  function stateBindingChunkFingerprint(options, context) {
3155
3368
  return sha256Text(JSON.stringify(sortDeep({
@@ -3746,6 +3959,34 @@ function renderStateBindingSummary(binding) {
3746
3959
  `chunks ${summary["chunks"] ?? 0}`,
3747
3960
  ];
3748
3961
  }
3962
+ function renderStateCurationSummary(curation) {
3963
+ const summary = isDict(curation["summary"]) ? curation["summary"] : {};
3964
+ const decisionSummary = isDict(summary["decisions"]) ? summary["decisions"] : {};
3965
+ const lines = [
3966
+ `state-curation: ` +
3967
+ `states ${summary["states_before"] ?? 0}->${summary["states_after"] ?? 0} ` +
3968
+ `(removed ${summary["states_removed"] ?? 0}), ` +
3969
+ `actor/location/prop ${summary["actor_states_after"] ?? 0}/${summary["location_states_after"] ?? 0}/${summary["prop_states_after"] ?? 0}, ` +
3970
+ `merge ${decisionSummary["merge"] ?? 0}, drop ${decisionSummary["drop"] ?? 0}, action ${decisionSummary["action_desc"] ?? 0}, rename ${decisionSummary["rename"] ?? 0}`,
3971
+ ];
3972
+ for (const decision of asList(curation["decisions"])) {
3973
+ if (!isDict(decision))
3974
+ continue;
3975
+ const action = strOf(decision["decision"]);
3976
+ if (!action || action === "keep")
3977
+ continue;
3978
+ const kind = strOf(decision["kind"]);
3979
+ const assetId = strOf(decision["asset_id"]);
3980
+ const stateId = strOf(decision["state_id"]);
3981
+ const target = action === "merge"
3982
+ ? ` -> ${decision["target_state_id"] || "-"}`
3983
+ : action === "rename"
3984
+ ? ` -> ${decision["new_name"] || "-"}`
3985
+ : "";
3986
+ lines.push(`state-curation ${kind}:${assetId}/${stateId} ${action}${target} — ${decision["reason"] || "-"}`);
3987
+ }
3988
+ return lines;
3989
+ }
3749
3990
  function renderAssetNormalizationSummary(normalization) {
3750
3991
  const summary = isDict(normalization["summary"]) ? normalization["summary"] : {};
3751
3992
  const lines = [
@@ -3858,6 +4099,7 @@ export function commandReview(opts) {
3858
4099
  // (validation issues recomputed live, plus batch extraction failures).
3859
4100
  const lines = [];
3860
4101
  const curationPath = path.join(dd, "asset_curation.json");
4102
+ const stateCurationPath = path.join(dd, "state_curation.json");
3861
4103
  const stateBindingPath = path.join(dd, "state_binding.json");
3862
4104
  const normalizationPath = path.join(dd, "asset_normalization.json");
3863
4105
  if (exists(normalizationPath)) {
@@ -3870,6 +4112,11 @@ export function commandReview(opts) {
3870
4112
  if (isDict(curation))
3871
4113
  lines.push(...renderAssetCurationSummary(curation));
3872
4114
  }
4115
+ if (exists(stateCurationPath)) {
4116
+ const curation = readJson(stateCurationPath);
4117
+ if (isDict(curation))
4118
+ lines.push(...renderStateCurationSummary(curation));
4119
+ }
3873
4120
  if (exists(stateBindingPath)) {
3874
4121
  const binding = readJson(stateBindingPath);
3875
4122
  if (isDict(binding))
@@ -3898,7 +4145,7 @@ export function commandReview(opts) {
3898
4145
  const report = {
3899
4146
  title: "DIRECT REVIEW",
3900
4147
  result: lines.length > 0 ? lines : ["No issues, curation decisions, or batch failures to review."],
3901
- artifacts: [scriptPath, normalizationPath, path.join(dd, "asset_grouping.json"), path.join(dd, "asset_curation.json"), stateBindingPath, batchResultsDir],
4148
+ artifacts: [scriptPath, normalizationPath, path.join(dd, "asset_grouping.json"), path.join(dd, "asset_curation.json"), stateCurationPath, stateBindingPath, batchResultsDir],
3902
4149
  next: [
3903
4150
  "Run `direct review --episode <n>` for source↔extract alignment.",
3904
4151
  "Edit with `--draft` (e.g. `scriptctl rename … --draft`), then validate/export.",