@lingjingai/scriptctl 0.9.8 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/cli.js +11 -24
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +0 -4
- package/dist/common.js +0 -8
- package/dist/common.js.map +1 -1
- package/dist/domain/direct-core.d.ts +1 -1
- package/dist/domain/script-core.d.ts +1 -0
- package/dist/domain/script-core.js +24 -9
- package/dist/domain/script-core.js.map +1 -1
- package/dist/help-text.js +35 -95
- package/dist/help-text.js.map +1 -1
- package/dist/infra/default-writing-prompt.d.ts +1 -1
- package/dist/infra/default-writing-prompt.js +1 -1
- package/dist/usecases/direct.d.ts +0 -6
- package/dist/usecases/direct.js +13 -478
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/episode.js +8 -5
- package/dist/usecases/episode.js.map +1 -1
- package/dist/usecases/script.d.ts +1 -4
- package/dist/usecases/script.js +72 -207
- package/dist/usecases/script.js.map +1 -1
- package/package.json +2 -2
package/dist/usecases/direct.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { CliError, DEFAULT_BATCH_MAX_CHARS, DEFAULT_BATCH_MIN_LINES, DEFAULT_BATCH_MODE, DEFAULT_BATCH_TARGET_LINES, DEFAULT_CONCURRENCY, DEFAULT_MODEL, DEFAULT_PROVIDER, DIRECT_CONTRACT_VERSION, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE,
|
|
3
|
+
import { CliError, DEFAULT_BATCH_MAX_CHARS, DEFAULT_BATCH_MIN_LINES, DEFAULT_BATCH_MODE, DEFAULT_BATCH_TARGET_LINES, DEFAULT_CONCURRENCY, DEFAULT_MODEL, DEFAULT_PROVIDER, DIRECT_CONTRACT_VERSION, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE, SUPPORTED_EXTS, deletePath, deleteTree, directDir, exists, readJson, readText, sha256Text, writeJson, } from "../common.js";
|
|
4
4
|
import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, enrichEpisodePlanTitles, extractBatchWithRecovery, mergeEpisodeResults, normalizeEpisodeResult, normalizeInt, recoverBatchFromSource, uniqueAdd, validateBatchExtractionQuality, validateEpisodeExtractionQuality, _md_push_asset, curateScriptAssets, applyMetadataToScript, } from "../domain/direct-core.js";
|
|
5
5
|
import { validateScript } from "../domain/script-core.js";
|
|
6
6
|
import { makeProvider } from "../infra/providers.js";
|
|
@@ -66,69 +66,6 @@ function failureSignature(items) {
|
|
|
66
66
|
out.sort();
|
|
67
67
|
return out;
|
|
68
68
|
}
|
|
69
|
-
export function addInspectedTarget(workspace, target) {
|
|
70
|
-
const state = readRunState(workspace);
|
|
71
|
-
const targets = [];
|
|
72
|
-
for (const item of asList(state["inspected_targets"])) {
|
|
73
|
-
const s = strOf(item);
|
|
74
|
-
if (s)
|
|
75
|
-
targets.push(s);
|
|
76
|
-
}
|
|
77
|
-
if (!targets.includes(target))
|
|
78
|
-
targets.push(target);
|
|
79
|
-
const missing = [];
|
|
80
|
-
for (const t of REVIEW_TARGETS) {
|
|
81
|
-
if (!targets.includes(t))
|
|
82
|
-
missing.push(t);
|
|
83
|
-
}
|
|
84
|
-
const reviewStatus = missing.length === 0 ? "reviewed" : "in_progress";
|
|
85
|
-
return updateRunState(workspace, {
|
|
86
|
-
inspected_targets: targets,
|
|
87
|
-
review_status: reviewStatus,
|
|
88
|
-
review_missing: missing,
|
|
89
|
-
last_inspect_target: target,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
export function markPatched(workspace, count) {
|
|
93
|
-
const state = readRunState(workspace);
|
|
94
|
-
const patchCount = Number(state["patch_count"] ?? 0) + count;
|
|
95
|
-
return updateRunState(workspace, {
|
|
96
|
-
patch_count: patchCount,
|
|
97
|
-
review_status: "patched",
|
|
98
|
-
review_missing: [],
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
export function markMetadataConfidenceReviewed(workspace, operations) {
|
|
102
|
-
const metadataOps = new Set(["meta.worldview.set", "asset.role.set", "asset.describe"]);
|
|
103
|
-
if (!operations.some((op) => isDict(op) && metadataOps.has(strOf(op["op"]))))
|
|
104
|
-
return;
|
|
105
|
-
const p = path.join(directDir(workspace), "asset_metadata.json");
|
|
106
|
-
if (!exists(p))
|
|
107
|
-
return;
|
|
108
|
-
let metadata;
|
|
109
|
-
try {
|
|
110
|
-
metadata = readJson(p);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (!isDict(metadata) || metadata["confidence"] !== "low")
|
|
116
|
-
return;
|
|
117
|
-
metadata["confidence"] = "medium";
|
|
118
|
-
metadata["confidence_reviewed_at"] = checkpointTimestamp();
|
|
119
|
-
writeJson(p, metadata);
|
|
120
|
-
}
|
|
121
|
-
export function reviewBlockers(state) {
|
|
122
|
-
if (Number(state["patch_count"] ?? 0) > 0)
|
|
123
|
-
return [];
|
|
124
|
-
const inspected = new Set();
|
|
125
|
-
for (const item of asList(state["inspected_targets"])) {
|
|
126
|
-
const s = strOf(item);
|
|
127
|
-
if (s)
|
|
128
|
-
inspected.add(s);
|
|
129
|
-
}
|
|
130
|
-
return REVIEW_TARGETS.filter((t) => !inspected.has(t));
|
|
131
|
-
}
|
|
132
69
|
// ---------------------------------------------------------------------------
|
|
133
70
|
// Paths for episode/batch results
|
|
134
71
|
// ---------------------------------------------------------------------------
|
|
@@ -585,7 +522,7 @@ function writeBatchFailure(dir, batch, exc) {
|
|
|
585
522
|
function initFailedReport(workspace, opts) {
|
|
586
523
|
const payload = {
|
|
587
524
|
status: "init_failed",
|
|
588
|
-
command: "
|
|
525
|
+
command: "import init",
|
|
589
526
|
init_stage: opts.stage,
|
|
590
527
|
last_error: { title: opts.title, received: opts.received, failed_at: checkpointTimestamp() },
|
|
591
528
|
};
|
|
@@ -720,7 +657,7 @@ export async function commandInit(opts) {
|
|
|
720
657
|
const previousStateBeforeInit = readRunState(workspace);
|
|
721
658
|
updateRunState(workspace, {
|
|
722
659
|
status: "init_running",
|
|
723
|
-
command: "
|
|
660
|
+
command: "import init",
|
|
724
661
|
init_stage: "source_prepare",
|
|
725
662
|
provider: providerName,
|
|
726
663
|
model,
|
|
@@ -973,11 +910,11 @@ export async function commandInit(opts) {
|
|
|
973
910
|
: "INIT INCOMPLETE: Batch extraction failed";
|
|
974
911
|
const nextSteps = sameFailuresRepeated
|
|
975
912
|
? [
|
|
976
|
-
"
|
|
913
|
+
"Read failed batch details in draft/scriptctl/direct/batch_results/*.error.json (run_state.json lists failed_batches).",
|
|
977
914
|
"Do not rerun the same init command again until source, batch options, provider, or failed content has changed.",
|
|
978
915
|
]
|
|
979
916
|
: [
|
|
980
|
-
"
|
|
917
|
+
"Read failed batches in draft/scriptctl/direct/batch_results/*.error.json (run_state.json lists failed_batches).",
|
|
981
918
|
"Rerun the same init once if failures look transient; completed checkpoints will be reused.",
|
|
982
919
|
];
|
|
983
920
|
const failedEpisodeSet = new Set(failedEpisodes);
|
|
@@ -1020,7 +957,6 @@ export async function commandInit(opts) {
|
|
|
1020
957
|
failure_signature: currentFailureSignature,
|
|
1021
958
|
failure_streak: failureStreak,
|
|
1022
959
|
last_error: { title: failureTitle, failed_at: checkpointTimestamp() },
|
|
1023
|
-
exportable: false,
|
|
1024
960
|
});
|
|
1025
961
|
const issues = failures.slice(0, 5).map((it) => `${it["batch_id"]} episode ${it["episode"]} part ${it["part"]}: ${it["error_type"]} - ${it["message"]}`);
|
|
1026
962
|
const report = {
|
|
@@ -1211,7 +1147,7 @@ export async function commandInit(opts) {
|
|
|
1211
1147
|
const status = passed ? "ready_for_agent" : "needs_agent_repair";
|
|
1212
1148
|
updateRunState(workspace, {
|
|
1213
1149
|
status,
|
|
1214
|
-
command: "
|
|
1150
|
+
command: "import init",
|
|
1215
1151
|
init_stage: "complete",
|
|
1216
1152
|
checkpoint,
|
|
1217
1153
|
batch_checkpoint: batchCheckpoint,
|
|
@@ -1240,11 +1176,10 @@ export async function commandInit(opts) {
|
|
|
1240
1176
|
failure_signature: [],
|
|
1241
1177
|
failure_streak: 0,
|
|
1242
1178
|
last_error: null,
|
|
1179
|
+
// Informational only — not a gate. Marks the freshly-imported script as not
|
|
1180
|
+
// yet self-reviewed; the post-push review is tracked by the agent
|
|
1181
|
+
// orchestration (script-reviewer subagent), not by a scriptctl command.
|
|
1243
1182
|
review_status: "pending",
|
|
1244
|
-
review_missing: [...REVIEW_TARGETS],
|
|
1245
|
-
inspected_targets: [],
|
|
1246
|
-
patch_count: 0,
|
|
1247
|
-
exportable: providerName !== "mock",
|
|
1248
1183
|
});
|
|
1249
1184
|
const title = passed
|
|
1250
1185
|
? "INIT COMPLETE: Initial script ready"
|
|
@@ -1261,7 +1196,7 @@ export async function commandInit(opts) {
|
|
|
1261
1196
|
`episode checkpoint reused: ${skipped.length}`,
|
|
1262
1197
|
`batches: ${completedBatches}/${asList(batchPlan["batches"]).length} completed`,
|
|
1263
1198
|
`batch checkpoint reused: ${skippedEpisodeBatchCount + skippedBatches.length}`,
|
|
1264
|
-
"
|
|
1199
|
+
"review: pending (post-push self-review)",
|
|
1265
1200
|
],
|
|
1266
1201
|
artifacts: [
|
|
1267
1202
|
path.join(workspace, "source.txt"),
|
|
@@ -1279,10 +1214,10 @@ export async function commandInit(opts) {
|
|
|
1279
1214
|
issues: summarizeIssues(asList(validation["issues"])),
|
|
1280
1215
|
next: providerName === "mock"
|
|
1281
1216
|
? [
|
|
1282
|
-
"
|
|
1283
|
-
"
|
|
1217
|
+
"Do not push mock-provider results for delivery.",
|
|
1218
|
+
"Rerun init with --provider anthropic, then run scriptctl import push.",
|
|
1284
1219
|
]
|
|
1285
|
-
: ["Run
|
|
1220
|
+
: ["Run scriptctl import push to publish the script to the DB."],
|
|
1286
1221
|
};
|
|
1287
1222
|
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
1288
1223
|
}
|
|
@@ -1298,404 +1233,4 @@ export function summarizeIssues(issues) {
|
|
|
1298
1233
|
const first = issues[0];
|
|
1299
1234
|
return [parts.join("; "), `first: ${first["code"]} - ${first["summary"]}`];
|
|
1300
1235
|
}
|
|
1301
|
-
// ---------------------------------------------------------------------------
|
|
1302
|
-
// command_validate
|
|
1303
|
-
// ---------------------------------------------------------------------------
|
|
1304
|
-
export function commandValidate(opts) {
|
|
1305
|
-
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
1306
|
-
const scriptPath = opts["script_path"] ? strOf(opts["script_path"]) : null;
|
|
1307
|
-
const validation = validateScript(workspace, scriptPath);
|
|
1308
|
-
const stats = validation["stats"] ?? {};
|
|
1309
|
-
const passed = Boolean(validation["passed"]);
|
|
1310
|
-
const report = {
|
|
1311
|
-
title: passed ? "VALIDATE PASSED: Script is ready" : "VALIDATE NEEDS AGENT: Repair issues found",
|
|
1312
|
-
result: [
|
|
1313
|
-
`episodes: ${stats["episodes"] ?? 0}`,
|
|
1314
|
-
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1315
|
-
`actions: ${stats["actions"] ?? 0}`,
|
|
1316
|
-
],
|
|
1317
|
-
artifacts: [path.join(directDir(workspace), "validation.json")],
|
|
1318
|
-
issues: summarizeIssues(asList(validation["issues"])),
|
|
1319
|
-
next: [passed ? "Export script.json." : "Inspect issues and apply structured patches."],
|
|
1320
|
-
};
|
|
1321
|
-
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
1322
|
-
}
|
|
1323
|
-
// ---------------------------------------------------------------------------
|
|
1324
|
-
// command_inspect (incl. review rendering)
|
|
1325
|
-
// ---------------------------------------------------------------------------
|
|
1326
|
-
function renderReviewEpisode(sourceText, episodePlan, script, episodeNum) {
|
|
1327
|
-
const epId = fmtId("ep", episodeNum);
|
|
1328
|
-
const scriptEp = asList(script["episodes"]).find((ep) => ep["episode_id"] === epId);
|
|
1329
|
-
const planEp = asList(episodePlan["episodes"]).find((ep) => Number(ep["episode"] ?? 0) === episodeNum);
|
|
1330
|
-
const lines = [];
|
|
1331
|
-
if (!planEp && !scriptEp) {
|
|
1332
|
-
lines.push(`⚠ Episode ${episodeNum} not found in episode_plan.json or script.initial.json.`);
|
|
1333
|
-
const available = [...new Set(asList(episodePlan["episodes"]).map((ep) => Number(ep["episode"] ?? 0)))].sort((a, b) => a - b);
|
|
1334
|
-
if (available.length > 0)
|
|
1335
|
-
lines.push(`Available episodes: ${available.join(", ")}`);
|
|
1336
|
-
return lines;
|
|
1337
|
-
}
|
|
1338
|
-
const title = (scriptEp?.["title"] ?? planEp?.["title"]) || "(无标题)";
|
|
1339
|
-
lines.push("=".repeat(72));
|
|
1340
|
-
lines.push(`EPISODE ${episodeNum} / ${epId} — ${title}`);
|
|
1341
|
-
lines.push("=".repeat(72));
|
|
1342
|
-
lines.push("");
|
|
1343
|
-
lines.push("--- 原文 source.txt ---");
|
|
1344
|
-
if (planEp) {
|
|
1345
|
-
const span = isDict(planEp["source_span"]) ? planEp["source_span"] : {};
|
|
1346
|
-
const start = Number(span["start"] ?? 0);
|
|
1347
|
-
const end = Number(span["end"] ?? sourceText.length);
|
|
1348
|
-
const snippet = sourceText.slice(start, end).replace(/\s+$/, "");
|
|
1349
|
-
lines.push(snippet || "(empty)");
|
|
1350
|
-
}
|
|
1351
|
-
else {
|
|
1352
|
-
lines.push("(no episode_plan entry; source span unavailable)");
|
|
1353
|
-
}
|
|
1354
|
-
lines.push("");
|
|
1355
|
-
lines.push("--- 抽取 script.initial.json ---");
|
|
1356
|
-
if (scriptEp) {
|
|
1357
|
-
const actorIdToName = new Map();
|
|
1358
|
-
for (const a of asList(script["actors"]))
|
|
1359
|
-
actorIdToName.set(strOf(a["actor_id"]), strOf(a["actor_name"]));
|
|
1360
|
-
const speakerIdToName = new Map();
|
|
1361
|
-
for (const s of asList(script["speakers"]))
|
|
1362
|
-
speakerIdToName.set(strOf(s["speaker_id"]), strOf(s["display_name"]));
|
|
1363
|
-
const locationIdToName = new Map();
|
|
1364
|
-
for (const l of asList(script["locations"]))
|
|
1365
|
-
locationIdToName.set(strOf(l["location_id"]), strOf(l["location_name"]));
|
|
1366
|
-
const propIdToName = new Map();
|
|
1367
|
-
for (const p of asList(script["props"]))
|
|
1368
|
-
propIdToName.set(strOf(p["prop_id"]), strOf(p["prop_name"]));
|
|
1369
|
-
for (const scene of asList(scriptEp["scenes"])) {
|
|
1370
|
-
const env = isDict(scene["environment"]) ? scene["environment"] : {};
|
|
1371
|
-
const space = strOf(env["space"]) || "?";
|
|
1372
|
-
const timeOfDay = strOf(env["time"]) || "?";
|
|
1373
|
-
const sceneId = strOf(scene["scene_id"]) || "?";
|
|
1374
|
-
let locName = "";
|
|
1375
|
-
for (const ref of asList(scene["locations"])) {
|
|
1376
|
-
locName = locationIdToName.get(strOf(ref["location_id"])) || locName;
|
|
1377
|
-
break;
|
|
1378
|
-
}
|
|
1379
|
-
const actorNames = [];
|
|
1380
|
-
for (const ref of asList(scene["actors"])) {
|
|
1381
|
-
const n = actorIdToName.get(strOf(ref["actor_id"]));
|
|
1382
|
-
if (n)
|
|
1383
|
-
actorNames.push(n);
|
|
1384
|
-
}
|
|
1385
|
-
const propNames = [];
|
|
1386
|
-
for (const ref of asList(scene["props"])) {
|
|
1387
|
-
const n = propIdToName.get(strOf(ref["prop_id"]));
|
|
1388
|
-
if (n)
|
|
1389
|
-
propNames.push(n);
|
|
1390
|
-
}
|
|
1391
|
-
lines.push("");
|
|
1392
|
-
lines.push(`Scene ${sceneId} [${space} ${timeOfDay}] ${locName || "(未知场景)"}`);
|
|
1393
|
-
if (actorNames.length > 0)
|
|
1394
|
-
lines.push(` Actors: ${actorNames.join(", ")}`);
|
|
1395
|
-
if (propNames.length > 0)
|
|
1396
|
-
lines.push(` Props: ${propNames.join(", ")}`);
|
|
1397
|
-
for (const action of asList(scene["actions"])) {
|
|
1398
|
-
const kind = strOf(action["type"]);
|
|
1399
|
-
const content = strOf(action["content"]).replace(/\n/g, "\n ");
|
|
1400
|
-
let tag;
|
|
1401
|
-
if (kind === "dialogue") {
|
|
1402
|
-
const speaker = actorIdToName.get(strOf(action["actor_id"])) || speakerIdToName.get(strOf(action["speaker_id"])) || strOf(action["speaker"]) || "?";
|
|
1403
|
-
const emotion = strOf(action["emotion"]);
|
|
1404
|
-
tag = `dlg|${speaker}` + (emotion ? `|${emotion}` : "");
|
|
1405
|
-
}
|
|
1406
|
-
else if (kind === "inner_thought") {
|
|
1407
|
-
const speaker = actorIdToName.get(strOf(action["actor_id"])) || speakerIdToName.get(strOf(action["speaker_id"])) || strOf(action["speaker"]) || "?";
|
|
1408
|
-
const emotion = strOf(action["emotion"]);
|
|
1409
|
-
tag = `think|${speaker}` + (emotion ? `|${emotion}` : "");
|
|
1410
|
-
}
|
|
1411
|
-
else {
|
|
1412
|
-
tag = "act";
|
|
1413
|
-
}
|
|
1414
|
-
lines.push(` [${tag}] ${content}`);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
else {
|
|
1419
|
-
lines.push("(script.initial.json contains no entry for this episode)");
|
|
1420
|
-
}
|
|
1421
|
-
if (scriptEp) {
|
|
1422
|
-
const actorIdsInEp = new Set();
|
|
1423
|
-
const locationIdsInEp = new Set();
|
|
1424
|
-
const propIdsInEp = new Set();
|
|
1425
|
-
for (const scene of asList(scriptEp["scenes"])) {
|
|
1426
|
-
for (const ref of asList(scene["actors"])) {
|
|
1427
|
-
if (ref["actor_id"])
|
|
1428
|
-
actorIdsInEp.add(strOf(ref["actor_id"]));
|
|
1429
|
-
}
|
|
1430
|
-
for (const ref of asList(scene["locations"])) {
|
|
1431
|
-
if (ref["location_id"])
|
|
1432
|
-
locationIdsInEp.add(strOf(ref["location_id"]));
|
|
1433
|
-
}
|
|
1434
|
-
for (const ref of asList(scene["props"])) {
|
|
1435
|
-
if (ref["prop_id"])
|
|
1436
|
-
propIdsInEp.add(strOf(ref["prop_id"]));
|
|
1437
|
-
}
|
|
1438
|
-
for (const action of asList(scene["actions"])) {
|
|
1439
|
-
if (action["actor_id"])
|
|
1440
|
-
actorIdsInEp.add(strOf(action["actor_id"]));
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
if (actorIdsInEp.size > 0 || locationIdsInEp.size > 0 || propIdsInEp.size > 0) {
|
|
1444
|
-
lines.push("");
|
|
1445
|
-
lines.push(`--- Episode ${episodeNum} 资产 ---`);
|
|
1446
|
-
}
|
|
1447
|
-
if (actorIdsInEp.size > 0) {
|
|
1448
|
-
lines.push("Actors:");
|
|
1449
|
-
for (const actor of asList(script["actors"])) {
|
|
1450
|
-
if (!actorIdsInEp.has(strOf(actor["actor_id"])))
|
|
1451
|
-
continue;
|
|
1452
|
-
const desc = strOf(actor["description"]).trim() || "(无描述)";
|
|
1453
|
-
const role = strOf(actor["role_type"]) || "?";
|
|
1454
|
-
lines.push(` - ${actor["actor_id"]} ${actor["actor_name"]} [${role}] — ${desc}`);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (locationIdsInEp.size > 0) {
|
|
1458
|
-
lines.push("Locations:");
|
|
1459
|
-
for (const loc of asList(script["locations"])) {
|
|
1460
|
-
if (!locationIdsInEp.has(strOf(loc["location_id"])))
|
|
1461
|
-
continue;
|
|
1462
|
-
const desc = strOf(loc["description"]).trim() || "(无描述)";
|
|
1463
|
-
lines.push(` - ${loc["location_id"]} ${loc["location_name"]} — ${desc}`);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
if (propIdsInEp.size > 0) {
|
|
1467
|
-
lines.push("Props:");
|
|
1468
|
-
for (const prop of asList(script["props"])) {
|
|
1469
|
-
if (!propIdsInEp.has(strOf(prop["prop_id"])))
|
|
1470
|
-
continue;
|
|
1471
|
-
const desc = strOf(prop["description"]).trim() || "(无描述)";
|
|
1472
|
-
lines.push(` - ${prop["prop_id"]} ${prop["prop_name"]} — ${desc}`);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
return lines;
|
|
1477
|
-
}
|
|
1478
|
-
function renderAssetCurationSummary(curation) {
|
|
1479
|
-
const summary = isDict(curation["summary"]) ? curation["summary"] : {};
|
|
1480
|
-
const lines = [
|
|
1481
|
-
`curation: ` +
|
|
1482
|
-
`actors ${summary["actors_before"] ?? 0}->${summary["actors_after"] ?? 0} ` +
|
|
1483
|
-
`(removed ${summary["actors_removed"] ?? 0}), ` +
|
|
1484
|
-
`props ${summary["props_before"] ?? 0}->${summary["props_after"] ?? 0} ` +
|
|
1485
|
-
`(removed ${summary["props_removed"] ?? 0}), ` +
|
|
1486
|
-
`locations ${summary["locations_before"] ?? 0}->${summary["locations_after"] ?? 0} ` +
|
|
1487
|
-
`(merged ${summary["locations_merged"] ?? 0})`,
|
|
1488
|
-
];
|
|
1489
|
-
for (const actor of asList(curation["actors"])) {
|
|
1490
|
-
if (!isDict(actor) || actor["decision"] !== "remove")
|
|
1491
|
-
continue;
|
|
1492
|
-
lines.push(`curation actor ${actor["actor_id"]}: remove ${actor["name"] || "-"} (scenes=${actor["scene_count"] ?? 0}) — ${actor["reason"] || "-"}`);
|
|
1493
|
-
}
|
|
1494
|
-
for (const prop of asList(curation["props"])) {
|
|
1495
|
-
if (!isDict(prop) || prop["decision"] !== "remove")
|
|
1496
|
-
continue;
|
|
1497
|
-
lines.push(`curation prop ${prop["prop_id"]}: remove ${prop["name"] || "-"} (scenes=${prop["scene_count"] ?? 0}) — ${prop["reason"] || "-"}`);
|
|
1498
|
-
}
|
|
1499
|
-
for (const loc of asList(curation["locations"])) {
|
|
1500
|
-
if (!isDict(loc) || loc["decision"] !== "merge")
|
|
1501
|
-
continue;
|
|
1502
|
-
lines.push(`curation location ${loc["location_id"]}: merge ${loc["name"] || "-"} -> ${loc["target_location_id"] || "-"} — ${loc["reason"] || "-"}`);
|
|
1503
|
-
}
|
|
1504
|
-
return lines;
|
|
1505
|
-
}
|
|
1506
|
-
export function commandInspect(opts) {
|
|
1507
|
-
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
1508
|
-
const target = strOf(opts["target"]);
|
|
1509
|
-
const itemId = opts["id"] ? strOf(opts["id"]) : null;
|
|
1510
|
-
const dd = directDir(workspace);
|
|
1511
|
-
const scriptPath = path.join(dd, "script.initial.json");
|
|
1512
|
-
const validationPath = path.join(dd, "validation.json");
|
|
1513
|
-
if (!exists(scriptPath) && target === "asset") {
|
|
1514
|
-
throw new CliError("INSPECT BLOCKED: script.initial.json not found", "script.initial.json not found.", {
|
|
1515
|
-
exitCode: EXIT_INPUT,
|
|
1516
|
-
required: ["workspace/draft/scriptctl/direct/script.initial.json"],
|
|
1517
|
-
received: [scriptPath],
|
|
1518
|
-
nextSteps: ["Run scriptctl direct init first."],
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
const script = exists(scriptPath)
|
|
1522
|
-
? readJson(scriptPath)
|
|
1523
|
-
: { episodes: [], actors: [], locations: [], props: [] };
|
|
1524
|
-
let validation = exists(validationPath) ? readJson(validationPath) : { issues: [] };
|
|
1525
|
-
if (target === "issue" && exists(scriptPath)) {
|
|
1526
|
-
validation = validateScript(workspace, scriptPath, { requireSource: false });
|
|
1527
|
-
}
|
|
1528
|
-
const batchPlanPath = path.join(dd, "batch_plan.json");
|
|
1529
|
-
const batchPlan = exists(batchPlanPath) ? readJson(batchPlanPath) : { batches: [] };
|
|
1530
|
-
const batchCounts = new Map();
|
|
1531
|
-
for (const batch of asList(batchPlan["batches"])) {
|
|
1532
|
-
if (!isDict(batch))
|
|
1533
|
-
continue;
|
|
1534
|
-
const ep = Number(batch["episode"] ?? 0);
|
|
1535
|
-
batchCounts.set(ep, (batchCounts.get(ep) ?? 0) + 1);
|
|
1536
|
-
}
|
|
1537
|
-
const failedByEpisode = new Map();
|
|
1538
|
-
const batchResultsDir = path.join(dd, "batch_results");
|
|
1539
|
-
if (exists(batchResultsDir)) {
|
|
1540
|
-
const files = fs.readdirSync(batchResultsDir).filter((n) => n.endsWith(".error.json")).sort();
|
|
1541
|
-
for (const name of files) {
|
|
1542
|
-
const errorFile = path.join(batchResultsDir, name);
|
|
1543
|
-
try {
|
|
1544
|
-
const error = readJson(errorFile);
|
|
1545
|
-
const episodeNum = Number(error["episode"] ?? 0);
|
|
1546
|
-
if (!failedByEpisode.has(episodeNum))
|
|
1547
|
-
failedByEpisode.set(episodeNum, []);
|
|
1548
|
-
failedByEpisode.get(episodeNum).push(strOf(error["batch_id"]) || name.replace(/\.error\.json$/, ""));
|
|
1549
|
-
}
|
|
1550
|
-
catch {
|
|
1551
|
-
// ignore
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
const lines = [];
|
|
1556
|
-
if (target === "episode") {
|
|
1557
|
-
if (exists(scriptPath)) {
|
|
1558
|
-
for (const ep of asList(script["episodes"])) {
|
|
1559
|
-
if (itemId && itemId !== strOf(ep["episode_id"]))
|
|
1560
|
-
continue;
|
|
1561
|
-
const scenes = asList(ep["scenes"]);
|
|
1562
|
-
const sceneCount = scenes.length;
|
|
1563
|
-
let actionCount = 0;
|
|
1564
|
-
for (const scene of scenes)
|
|
1565
|
-
actionCount += asList(scene["actions"]).length;
|
|
1566
|
-
const rawNum = normalizeInt(strOf(ep["episode_id"]), lines.length + 1);
|
|
1567
|
-
const failed = (failedByEpisode.get(rawNum) ?? []).join(",") || "-";
|
|
1568
|
-
lines.push(`${ep["episode_id"]}: scenes=${sceneCount}, actions=${actionCount}, batches=${batchCounts.get(rawNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
else {
|
|
1572
|
-
const planPath = path.join(dd, "episode_plan.json");
|
|
1573
|
-
const plan = exists(planPath) ? readJson(planPath) : { episodes: [] };
|
|
1574
|
-
for (const ep of asList(plan["episodes"])) {
|
|
1575
|
-
const epId = fmtId("ep", Number(ep["episode"] ?? 0));
|
|
1576
|
-
if (itemId && itemId !== epId && itemId !== strOf(ep["episode"]))
|
|
1577
|
-
continue;
|
|
1578
|
-
const episodeNum = Number(ep["episode"] ?? 0);
|
|
1579
|
-
const failed = (failedByEpisode.get(episodeNum) ?? []).join(",") || "-";
|
|
1580
|
-
lines.push(`${epId}: batches=${batchCounts.get(episodeNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
else if (target === "asset") {
|
|
1585
|
-
const curationPath = path.join(dd, "asset_curation.json");
|
|
1586
|
-
if (!itemId && exists(curationPath)) {
|
|
1587
|
-
const curation = readJson(curationPath);
|
|
1588
|
-
if (isDict(curation))
|
|
1589
|
-
lines.push(...renderAssetCurationSummary(curation));
|
|
1590
|
-
}
|
|
1591
|
-
for (const [key, idKey, nameKey] of [["actors", "actor_id", "actor_name"], ["locations", "location_id", "location_name"], ["props", "prop_id", "prop_name"]]) {
|
|
1592
|
-
for (const asset of asList(script[key])) {
|
|
1593
|
-
if (itemId && itemId !== strOf(asset[idKey]) && itemId !== strOf(asset[nameKey]))
|
|
1594
|
-
continue;
|
|
1595
|
-
const aliases = asList(asset["aliases"]).length;
|
|
1596
|
-
const states = asList(asset["states"]).length;
|
|
1597
|
-
const role = key === "actors" ? `, role_type=${asset["role_type"]}` : "";
|
|
1598
|
-
const description = strOf(asset["description"]).trim() ? "yes" : "missing";
|
|
1599
|
-
const singular = key.slice(0, -1);
|
|
1600
|
-
lines.push(`${singular} ${asset[idKey]}: ${asset[nameKey]} aliases=${aliases}, states=${states}${role}, description=${description}`);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
else if (target === "issue") {
|
|
1605
|
-
for (const errors of failedByEpisode.values()) {
|
|
1606
|
-
for (const batchId of errors) {
|
|
1607
|
-
const errorPath = path.join(batchResultsDir, `${batchId}.error.json`);
|
|
1608
|
-
if (!exists(errorPath))
|
|
1609
|
-
continue;
|
|
1610
|
-
let error;
|
|
1611
|
-
try {
|
|
1612
|
-
error = readJson(errorPath);
|
|
1613
|
-
}
|
|
1614
|
-
catch {
|
|
1615
|
-
continue;
|
|
1616
|
-
}
|
|
1617
|
-
if (itemId && itemId !== strOf(error["batch_id"]) && itemId !== strOf(error["episode"]) && itemId !== strOf(error["error_type"]))
|
|
1618
|
-
continue;
|
|
1619
|
-
lines.push(`error BATCH_FAILED: ${error["batch_id"]} episode ${error["episode"]} part ${error["part"]} - ${error["message"]}`);
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
for (const issue of asList(validation["issues"])) {
|
|
1623
|
-
if (itemId && itemId !== strOf(issue["code"]) && itemId !== strOf(issue["severity"]))
|
|
1624
|
-
continue;
|
|
1625
|
-
const whereParts = [];
|
|
1626
|
-
for (const k of ["episode", "scene", "action_index"]) {
|
|
1627
|
-
if (issue[k] !== null && issue[k] !== undefined)
|
|
1628
|
-
whereParts.push(strOf(issue[k]));
|
|
1629
|
-
}
|
|
1630
|
-
const where = whereParts.join(" ");
|
|
1631
|
-
lines.push(`${issue["severity"]} ${issue["code"]}: ${issue["summary"]}${where ? ` [${where}]` : ""}`);
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
else if (target === "review") {
|
|
1635
|
-
const episodeArg = strOf(opts["episode"]).trim();
|
|
1636
|
-
if (!episodeArg) {
|
|
1637
|
-
throw new CliError("INSPECT BLOCKED: --episode required for review", "--episode required for review.", {
|
|
1638
|
-
exitCode: EXIT_USAGE,
|
|
1639
|
-
required: ["--episode <n>[,<n>...]"],
|
|
1640
|
-
received: ["--episode not provided"],
|
|
1641
|
-
nextSteps: ["Pass --episode 1 (single) or --episode 1,15,30 (multiple)."],
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
let episodeFilters;
|
|
1645
|
-
try {
|
|
1646
|
-
episodeFilters = episodeArg.split(",").map((s) => s.trim()).filter((s) => s).map((s) => {
|
|
1647
|
-
const n = parseInt(s, 10);
|
|
1648
|
-
if (Number.isNaN(n))
|
|
1649
|
-
throw new Error("nan");
|
|
1650
|
-
return n;
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
catch {
|
|
1654
|
-
throw new CliError("INSPECT BLOCKED: --episode must be integers", "--episode must be integers.", {
|
|
1655
|
-
exitCode: EXIT_USAGE,
|
|
1656
|
-
required: ["--episode <n>[,<n>...]"],
|
|
1657
|
-
received: [`--episode ${episodeArg}`],
|
|
1658
|
-
nextSteps: ["Pass episode numbers like --episode 1,15,30."],
|
|
1659
|
-
});
|
|
1660
|
-
}
|
|
1661
|
-
const sourcePath = path.join(workspace, "source.txt");
|
|
1662
|
-
if (!exists(sourcePath)) {
|
|
1663
|
-
throw new CliError("INSPECT BLOCKED: source.txt not found", "source.txt not found.", {
|
|
1664
|
-
exitCode: EXIT_INPUT,
|
|
1665
|
-
required: [sourcePath],
|
|
1666
|
-
received: ["source.txt missing"],
|
|
1667
|
-
nextSteps: ["Run scriptctl direct init first."],
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
const sourceText = readText(sourcePath);
|
|
1671
|
-
const episodePlanPath = path.join(dd, "episode_plan.json");
|
|
1672
|
-
const episodePlan = exists(episodePlanPath) ? readJson(episodePlanPath) : { episodes: [] };
|
|
1673
|
-
for (let idx = 0; idx < episodeFilters.length; idx++) {
|
|
1674
|
-
if (idx > 0) {
|
|
1675
|
-
lines.push("");
|
|
1676
|
-
lines.push("");
|
|
1677
|
-
}
|
|
1678
|
-
lines.push(...renderReviewEpisode(sourceText, episodePlan, script, episodeFilters[idx]));
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
else {
|
|
1682
|
-
throw new CliError("INSPECT BLOCKED: Invalid target", "Invalid inspect target.", {
|
|
1683
|
-
exitCode: EXIT_USAGE,
|
|
1684
|
-
required: ["--target: episode, asset, issue, or review"],
|
|
1685
|
-
received: [`--target: ${target}`],
|
|
1686
|
-
nextSteps: ["Use one of the supported inspect targets."],
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
const state = addInspectedTarget(workspace, target);
|
|
1690
|
-
const missingReview = reviewBlockers(state);
|
|
1691
|
-
const report = {
|
|
1692
|
-
title: `INSPECT: ${target}`,
|
|
1693
|
-
result: lines.length > 0 ? lines : ["No matching items."],
|
|
1694
|
-
next: [
|
|
1695
|
-
"Use inspect details to prepare a structured patch, or continue required review.",
|
|
1696
|
-
missingReview.length === 0 ? "Review complete; run validate/export." : `Still inspect: ${missingReview.join(", ")}.`,
|
|
1697
|
-
],
|
|
1698
|
-
};
|
|
1699
|
-
return [report, EXIT_OK];
|
|
1700
|
-
}
|
|
1701
1236
|
//# sourceMappingURL=direct.js.map
|