@lingjingai/scriptctl 0.12.1 → 0.13.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.
- package/dist/cli.js +5 -6
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +0 -3
- package/dist/common.js +0 -1
- package/dist/common.js.map +1 -1
- package/dist/domain/script-core.js +1 -1
- package/dist/domain/script-core.js.map +1 -1
- package/dist/help-text.js +38 -30
- package/dist/help-text.js.map +1 -1
- package/dist/usecases/direct.d.ts +1 -5
- package/dist/usecases/direct.js +122 -249
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/episode.js +2 -2
- package/dist/usecases/episode.js.map +1 -1
- package/dist/usecases/script.d.ts +0 -1
- package/dist/usecases/script.js +37 -73
- package/dist/usecases/script.js.map +1 -1
- package/package.json +1 -1
package/dist/usecases/direct.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE,
|
|
4
|
-
import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, DEFAULT_TITLE_CHUNK_SIZE, chunkEpisodesNeedingTitles, applyEpisodeTitlesToPlan, episodeTitleResponseMap, extractBatchWithRecovery, mergeEpisodeResults, normalizeEpisodeResult,
|
|
3
|
+
import { CliError, DEFAULT_BATCH_MAX_CHARS, DEFAULT_BATCH_MIN_LINES, DEFAULT_BATCH_MODE, DEFAULT_BATCH_TARGET_LINES, DEFAULT_CONCURRENCY, DEFAULT_MODEL, DEFAULT_PROVIDER, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE, SUPPORTED_EXTS, deletePath, directDir, exists, fmtId, readJson, readText, writeJson, } from "../common.js";
|
|
4
|
+
import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, DEFAULT_TITLE_CHUNK_SIZE, chunkEpisodesNeedingTitles, applyEpisodeTitlesToPlan, episodeTitleResponseMap, extractBatchWithRecovery, mergeEpisodeResults, normalizeEpisodeResult, uniqueAdd, 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";
|
|
7
7
|
import { makeSourceManifest, prepareSource, } from "../infra/converters.js";
|
|
@@ -60,69 +60,6 @@ export function readRunState(workspace) {
|
|
|
60
60
|
return {};
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
export function addInspectedTarget(workspace, target) {
|
|
64
|
-
const state = readRunState(workspace);
|
|
65
|
-
const targets = [];
|
|
66
|
-
for (const item of asList(state["inspected_targets"])) {
|
|
67
|
-
const s = strOf(item);
|
|
68
|
-
if (s)
|
|
69
|
-
targets.push(s);
|
|
70
|
-
}
|
|
71
|
-
if (!targets.includes(target))
|
|
72
|
-
targets.push(target);
|
|
73
|
-
const missing = [];
|
|
74
|
-
for (const t of REVIEW_TARGETS) {
|
|
75
|
-
if (!targets.includes(t))
|
|
76
|
-
missing.push(t);
|
|
77
|
-
}
|
|
78
|
-
const reviewStatus = missing.length === 0 ? "reviewed" : "in_progress";
|
|
79
|
-
return updateRunState(workspace, {
|
|
80
|
-
inspected_targets: targets,
|
|
81
|
-
review_status: reviewStatus,
|
|
82
|
-
review_missing: missing,
|
|
83
|
-
last_inspect_target: target,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
export function markPatched(workspace, count) {
|
|
87
|
-
const state = readRunState(workspace);
|
|
88
|
-
const patchCount = Number(state["patch_count"] ?? 0) + count;
|
|
89
|
-
return updateRunState(workspace, {
|
|
90
|
-
patch_count: patchCount,
|
|
91
|
-
review_status: "patched",
|
|
92
|
-
review_missing: [],
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
export function markMetadataConfidenceReviewed(workspace, operations) {
|
|
96
|
-
const metadataOps = new Set(["meta.worldview.set", "asset.role.set", "asset.describe"]);
|
|
97
|
-
if (!operations.some((op) => isDict(op) && metadataOps.has(strOf(op["op"]))))
|
|
98
|
-
return;
|
|
99
|
-
const p = path.join(directDir(workspace), "asset_metadata.json");
|
|
100
|
-
if (!exists(p))
|
|
101
|
-
return;
|
|
102
|
-
let metadata;
|
|
103
|
-
try {
|
|
104
|
-
metadata = readJson(p);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (!isDict(metadata) || metadata["confidence"] !== "low")
|
|
110
|
-
return;
|
|
111
|
-
metadata["confidence"] = "medium";
|
|
112
|
-
metadata["confidence_reviewed_at"] = checkpointTimestamp();
|
|
113
|
-
writeJson(p, metadata);
|
|
114
|
-
}
|
|
115
|
-
export function reviewBlockers(state) {
|
|
116
|
-
if (Number(state["patch_count"] ?? 0) > 0)
|
|
117
|
-
return [];
|
|
118
|
-
const inspected = new Set();
|
|
119
|
-
for (const item of asList(state["inspected_targets"])) {
|
|
120
|
-
const s = strOf(item);
|
|
121
|
-
if (s)
|
|
122
|
-
inspected.add(s);
|
|
123
|
-
}
|
|
124
|
-
return REVIEW_TARGETS.filter((t) => !inspected.has(t));
|
|
125
|
-
}
|
|
126
63
|
// ---------------------------------------------------------------------------
|
|
127
64
|
// Paths for episode/batch results
|
|
128
65
|
// ---------------------------------------------------------------------------
|
|
@@ -324,7 +261,7 @@ async function providerExtractAssetCurationLocal(provider, sourceText, script) {
|
|
|
324
261
|
// Record a batch failure: write `<key>.error.json` (which marks the batch as
|
|
325
262
|
// "tried and failed" → sticky on plain resume) and drop any stale result file.
|
|
326
263
|
// The error_code (e.g. PROVIDER_CONTENT_FILTERED) is surfaced so the agent can
|
|
327
|
-
// see WHY in `direct status` / `direct
|
|
264
|
+
// see WHY in `direct status` / `direct review` and decide whether to re-run.
|
|
328
265
|
//
|
|
329
266
|
// Deliberate content-filter policy split: batch extraction is HARD (sticky
|
|
330
267
|
// error, episode blocked until an explicit re-run) because batch content is the
|
|
@@ -715,23 +652,28 @@ export async function commandInit(opts) {
|
|
|
715
652
|
episode_total: totalEpisodes,
|
|
716
653
|
batch_total: totalBatches,
|
|
717
654
|
});
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
655
|
+
// Persist each batch the moment it finishes (inside the worker), NOT after the
|
|
656
|
+
// whole pool drains. Per-unit files have no shared index, so concurrent writes
|
|
657
|
+
// are safe — and batch_results/ fills up live, so a crash/kill mid-run keeps
|
|
658
|
+
// every batch already extracted (the resume model depends on this).
|
|
659
|
+
const outcomes = await pMapWithConcurrency(pending, concurrency, async (batch) => {
|
|
660
|
+
try {
|
|
661
|
+
const result = await extractBatchWithRecovery(provider, sourceText, batch);
|
|
662
|
+
persistBatchResult(batchResultsDir, batch, result);
|
|
725
663
|
const errorPath = batchErrorPath(batchResultsDir, batch);
|
|
726
664
|
if (exists(errorPath))
|
|
727
665
|
deletePath(errorPath);
|
|
666
|
+
return true;
|
|
728
667
|
}
|
|
729
|
-
|
|
730
|
-
|
|
668
|
+
catch (error) {
|
|
669
|
+
writeBatchFailure(batchResultsDir, batch, error);
|
|
670
|
+
return false;
|
|
731
671
|
}
|
|
732
|
-
}
|
|
733
|
-
const
|
|
734
|
-
const
|
|
672
|
+
});
|
|
673
|
+
const freshOk = outcomes.filter((o) => o.ok && o.value === true).length;
|
|
674
|
+
const freshFail = outcomes.filter((o) => o.ok && o.value === false).length;
|
|
675
|
+
const doneBatches = reusedBatchCount + freshOk;
|
|
676
|
+
const erroredBatches = stickyErrorCount + freshFail;
|
|
735
677
|
// Assemble every episode whose batches are all present on disk (reused +
|
|
736
678
|
// freshly extracted, read uniformly from disk). Incomplete episodes are
|
|
737
679
|
// reported; assembling them is left to an explicit re-run.
|
|
@@ -936,10 +878,6 @@ export async function commandInit(opts) {
|
|
|
936
878
|
batch_reused: reusedBatchCount,
|
|
937
879
|
batch_failed: erroredBatches,
|
|
938
880
|
last_error: null,
|
|
939
|
-
review_status: "pending",
|
|
940
|
-
review_missing: [...REVIEW_TARGETS],
|
|
941
|
-
inspected_targets: [],
|
|
942
|
-
patch_count: 0,
|
|
943
881
|
});
|
|
944
882
|
const title = passed
|
|
945
883
|
? "INIT COMPLETE: Initial script ready"
|
|
@@ -956,7 +894,6 @@ export async function commandInit(opts) {
|
|
|
956
894
|
`episodes: ${results.length}/${totalEpisodes} complete`,
|
|
957
895
|
`batches: ${doneBatches}/${totalBatches} done (reused ${reusedBatchCount})`,
|
|
958
896
|
...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
|
|
959
|
-
"agent_review: pending",
|
|
960
897
|
],
|
|
961
898
|
artifacts: [
|
|
962
899
|
path.join(workspace, "source.txt"),
|
|
@@ -974,10 +911,10 @@ export async function commandInit(opts) {
|
|
|
974
911
|
issues: summarizeIssues(asList(validation["issues"])),
|
|
975
912
|
next: providerName === "mock"
|
|
976
913
|
? [
|
|
977
|
-
"Run
|
|
914
|
+
"Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export.",
|
|
978
915
|
"Do not export mock-provider results for delivery.",
|
|
979
916
|
]
|
|
980
|
-
: ["Run
|
|
917
|
+
: ["Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export."],
|
|
981
918
|
};
|
|
982
919
|
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
983
920
|
}
|
|
@@ -1299,173 +1236,81 @@ function renderAssetCurationSummary(curation) {
|
|
|
1299
1236
|
}
|
|
1300
1237
|
return lines;
|
|
1301
1238
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
const script = exists(scriptPath)
|
|
1318
|
-
? readJson(scriptPath)
|
|
1319
|
-
: { episodes: [], actors: [], locations: [], props: [] };
|
|
1320
|
-
let validation = exists(validationPath) ? readJson(validationPath) : { issues: [] };
|
|
1321
|
-
if (target === "issue" && exists(scriptPath)) {
|
|
1322
|
-
validation = validateScript(workspace, scriptPath, { requireSource: false });
|
|
1323
|
-
}
|
|
1324
|
-
const batchPlanPath = path.join(dd, "batch_plan.json");
|
|
1325
|
-
const batchPlan = exists(batchPlanPath) ? readJson(batchPlanPath) : { batches: [] };
|
|
1326
|
-
const batchCounts = new Map();
|
|
1327
|
-
for (const batch of asList(batchPlan["batches"])) {
|
|
1328
|
-
if (!isDict(batch))
|
|
1329
|
-
continue;
|
|
1330
|
-
const ep = Number(batch["episode"] ?? 0);
|
|
1331
|
-
batchCounts.set(ep, (batchCounts.get(ep) ?? 0) + 1);
|
|
1332
|
-
}
|
|
1333
|
-
const failedByEpisode = new Map();
|
|
1334
|
-
const batchResultsDir = path.join(dd, "batch_results");
|
|
1335
|
-
if (exists(batchResultsDir)) {
|
|
1336
|
-
const files = fs.readdirSync(batchResultsDir).filter((n) => n.endsWith(".error.json")).sort();
|
|
1337
|
-
for (const name of files) {
|
|
1338
|
-
const errorFile = path.join(batchResultsDir, name);
|
|
1339
|
-
try {
|
|
1340
|
-
const error = readJson(errorFile);
|
|
1341
|
-
const episodeNum = Number(error["episode"] ?? 0);
|
|
1342
|
-
if (!failedByEpisode.has(episodeNum))
|
|
1343
|
-
failedByEpisode.set(episodeNum, []);
|
|
1344
|
-
failedByEpisode.get(episodeNum).push(strOf(error["batch_id"]) || name.replace(/\.error\.json$/, ""));
|
|
1345
|
-
}
|
|
1346
|
-
catch {
|
|
1347
|
-
// ignore
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
const lines = [];
|
|
1352
|
-
if (target === "episode") {
|
|
1353
|
-
if (exists(scriptPath)) {
|
|
1354
|
-
for (const ep of asList(script["episodes"])) {
|
|
1355
|
-
if (itemId && itemId !== strOf(ep["episode_id"]))
|
|
1356
|
-
continue;
|
|
1357
|
-
const scenes = asList(ep["scenes"]);
|
|
1358
|
-
const sceneCount = scenes.length;
|
|
1359
|
-
let actionCount = 0;
|
|
1360
|
-
for (const scene of scenes)
|
|
1361
|
-
actionCount += asList(scene["actions"]).length;
|
|
1362
|
-
const rawNum = normalizeInt(strOf(ep["episode_id"]), lines.length + 1);
|
|
1363
|
-
const failed = (failedByEpisode.get(rawNum) ?? []).join(",") || "-";
|
|
1364
|
-
lines.push(`${ep["episode_id"]}: scenes=${sceneCount}, actions=${actionCount}, batches=${batchCounts.get(rawNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1365
|
-
}
|
|
1239
|
+
// Collect batch extraction failures (batch_results/<key>.error.json) keyed by
|
|
1240
|
+
// episode. Shared by both review views — these are direct-stage failures the
|
|
1241
|
+
// final-script `issues` query can't see.
|
|
1242
|
+
function collectBatchFailures(batchResultsDir) {
|
|
1243
|
+
const byEpisode = new Map();
|
|
1244
|
+
if (!exists(batchResultsDir))
|
|
1245
|
+
return byEpisode;
|
|
1246
|
+
const files = fs.readdirSync(batchResultsDir).filter((n) => n.endsWith(".error.json")).sort();
|
|
1247
|
+
for (const name of files) {
|
|
1248
|
+
try {
|
|
1249
|
+
const error = readJson(path.join(batchResultsDir, name));
|
|
1250
|
+
const episodeNum = Number(error["episode"] ?? 0);
|
|
1251
|
+
if (!byEpisode.has(episodeNum))
|
|
1252
|
+
byEpisode.set(episodeNum, []);
|
|
1253
|
+
byEpisode.get(episodeNum).push(error);
|
|
1366
1254
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
const plan = exists(planPath) ? readJson(planPath) : { episodes: [] };
|
|
1370
|
-
for (const ep of asList(plan["episodes"])) {
|
|
1371
|
-
const epId = fmtId("ep", Number(ep["episode"] ?? 0));
|
|
1372
|
-
if (itemId && itemId !== epId && itemId !== strOf(ep["episode"]))
|
|
1373
|
-
continue;
|
|
1374
|
-
const episodeNum = Number(ep["episode"] ?? 0);
|
|
1375
|
-
const failed = (failedByEpisode.get(episodeNum) ?? []).join(",") || "-";
|
|
1376
|
-
lines.push(`${epId}: batches=${batchCounts.get(episodeNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1377
|
-
}
|
|
1255
|
+
catch {
|
|
1256
|
+
// ignore unreadable error file
|
|
1378
1257
|
}
|
|
1379
1258
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
continue;
|
|
1391
|
-
const aliases = asList(asset["aliases"]).length;
|
|
1392
|
-
const states = asList(asset["states"]).length;
|
|
1393
|
-
const role = key === "actors" ? `, role_type=${asset["role_type"]}` : "";
|
|
1394
|
-
const description = strOf(asset["description"]).trim() ? "yes" : "missing";
|
|
1395
|
-
const singular = key.slice(0, -1);
|
|
1396
|
-
lines.push(`${singular} ${asset[idKey]}: ${asset[nameKey]} aliases=${aliases}, states=${states}${role}, description=${description}`);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1259
|
+
return byEpisode;
|
|
1260
|
+
}
|
|
1261
|
+
function parseEpisodeArg(episodeArg) {
|
|
1262
|
+
try {
|
|
1263
|
+
return episodeArg.split(",").map((s) => s.trim()).filter((s) => s).map((s) => {
|
|
1264
|
+
const n = parseInt(s, 10);
|
|
1265
|
+
if (Number.isNaN(n))
|
|
1266
|
+
throw new Error("nan");
|
|
1267
|
+
return n;
|
|
1268
|
+
});
|
|
1399
1269
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
try {
|
|
1408
|
-
error = readJson(errorPath);
|
|
1409
|
-
}
|
|
1410
|
-
catch {
|
|
1411
|
-
continue;
|
|
1412
|
-
}
|
|
1413
|
-
if (itemId && itemId !== strOf(error["batch_id"]) && itemId !== strOf(error["episode"]) && itemId !== strOf(error["error_type"]))
|
|
1414
|
-
continue;
|
|
1415
|
-
lines.push(`error BATCH_FAILED: ${error["batch_id"]} episode ${error["episode"]} part ${error["part"]} - ${error["message"]}`);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
for (const issue of asList(validation["issues"])) {
|
|
1419
|
-
if (itemId && itemId !== strOf(issue["code"]) && itemId !== strOf(issue["severity"]))
|
|
1420
|
-
continue;
|
|
1421
|
-
const whereParts = [];
|
|
1422
|
-
for (const k of ["episode", "scene", "action_index"]) {
|
|
1423
|
-
if (issue[k] !== null && issue[k] !== undefined)
|
|
1424
|
-
whereParts.push(strOf(issue[k]));
|
|
1425
|
-
}
|
|
1426
|
-
const where = whereParts.join(" ");
|
|
1427
|
-
lines.push(`${issue["severity"]} ${issue["code"]}: ${issue["summary"]}${where ? ` [${where}]` : ""}`);
|
|
1428
|
-
}
|
|
1270
|
+
catch {
|
|
1271
|
+
throw new CliError("REVIEW BLOCKED: --episode must be integers", "--episode must be integers.", {
|
|
1272
|
+
exitCode: EXIT_USAGE,
|
|
1273
|
+
required: ["--episode <n>[,<n>...]"],
|
|
1274
|
+
received: [`--episode ${episodeArg}`],
|
|
1275
|
+
nextSteps: ["Pass episode numbers like --episode 1,15,30."],
|
|
1276
|
+
});
|
|
1429
1277
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
throw new CliError("INSPECT BLOCKED: --episode must be integers", "--episode must be integers.", {
|
|
1451
|
-
exitCode: EXIT_USAGE,
|
|
1452
|
-
required: ["--episode <n>[,<n>...]"],
|
|
1453
|
-
received: [`--episode ${episodeArg}`],
|
|
1454
|
-
nextSteps: ["Pass episode numbers like --episode 1,15,30."],
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1278
|
+
}
|
|
1279
|
+
// ---------------------------------------------------------------------------
|
|
1280
|
+
// command_review — the direct-stage proofreading view. Read-only; no run_state
|
|
1281
|
+
// side effects. Two shapes:
|
|
1282
|
+
// * `direct review` workspace diagnostics: curation decisions +
|
|
1283
|
+
// issues (validation + batch extraction errors)
|
|
1284
|
+
// * `direct review --episode N` per-episode source.txt ↔ script.initial.json
|
|
1285
|
+
// side-by-side alignment (renderReviewEpisode)
|
|
1286
|
+
// Generic per-episode / per-asset listings live on the top-level `episodes` /
|
|
1287
|
+
// `assets` queries (use `--draft` to point them at script.initial.json).
|
|
1288
|
+
// ---------------------------------------------------------------------------
|
|
1289
|
+
export function commandReview(opts) {
|
|
1290
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
1291
|
+
const dd = directDir(workspace);
|
|
1292
|
+
const scriptPath = path.join(dd, "script.initial.json");
|
|
1293
|
+
const batchResultsDir = path.join(dd, "batch_results");
|
|
1294
|
+
const episodeArg = strOf(opts["episode"]).trim();
|
|
1295
|
+
// --episode: source ↔ extract alignment for the named episodes.
|
|
1296
|
+
if (episodeArg) {
|
|
1297
|
+
const episodeFilters = parseEpisodeArg(episodeArg);
|
|
1457
1298
|
const sourcePath = path.join(workspace, "source.txt");
|
|
1458
1299
|
if (!exists(sourcePath)) {
|
|
1459
|
-
throw new CliError("
|
|
1300
|
+
throw new CliError("REVIEW BLOCKED: source.txt not found", "source.txt not found.", {
|
|
1460
1301
|
exitCode: EXIT_INPUT,
|
|
1461
1302
|
required: [sourcePath],
|
|
1462
1303
|
received: ["source.txt missing"],
|
|
1463
1304
|
nextSteps: ["Run scriptctl direct init first."],
|
|
1464
1305
|
});
|
|
1465
1306
|
}
|
|
1307
|
+
const script = exists(scriptPath)
|
|
1308
|
+
? readJson(scriptPath)
|
|
1309
|
+
: { episodes: [], actors: [], locations: [], props: [] };
|
|
1466
1310
|
const sourceText = readText(sourcePath);
|
|
1467
1311
|
const episodePlanPath = path.join(dd, "episode_plan.json");
|
|
1468
1312
|
const episodePlan = exists(episodePlanPath) ? readJson(episodePlanPath) : { episodes: [] };
|
|
1313
|
+
const lines = [];
|
|
1469
1314
|
for (let idx = 0; idx < episodeFilters.length; idx++) {
|
|
1470
1315
|
if (idx > 0) {
|
|
1471
1316
|
lines.push("");
|
|
@@ -1473,23 +1318,51 @@ export function commandInspect(opts) {
|
|
|
1473
1318
|
}
|
|
1474
1319
|
lines.push(...renderReviewEpisode(sourceText, episodePlan, script, episodeFilters[idx]));
|
|
1475
1320
|
}
|
|
1321
|
+
const report = {
|
|
1322
|
+
title: `DIRECT REVIEW: episode ${episodeFilters.join(",")}`,
|
|
1323
|
+
result: lines.length > 0 ? lines : ["No matching items."],
|
|
1324
|
+
artifacts: [path.join(workspace, "source.txt"), path.join(dd, "episode_plan.json"), scriptPath],
|
|
1325
|
+
next: ["Edit with `--draft` (e.g. `scriptctl replace … --draft`) if the extraction is wrong, then validate/export."],
|
|
1326
|
+
};
|
|
1327
|
+
return [report, EXIT_OK];
|
|
1476
1328
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1329
|
+
// No --episode: workspace-level diagnostics. Curation decisions + every issue
|
|
1330
|
+
// (validation issues recomputed live, plus batch extraction failures).
|
|
1331
|
+
const script = exists(scriptPath)
|
|
1332
|
+
? readJson(scriptPath)
|
|
1333
|
+
: { episodes: [], actors: [], locations: [], props: [] };
|
|
1334
|
+
const lines = [];
|
|
1335
|
+
const curationPath = path.join(dd, "asset_curation.json");
|
|
1336
|
+
if (exists(curationPath)) {
|
|
1337
|
+
const curation = readJson(curationPath);
|
|
1338
|
+
if (isDict(curation))
|
|
1339
|
+
lines.push(...renderAssetCurationSummary(curation));
|
|
1340
|
+
}
|
|
1341
|
+
const batchFailures = collectBatchFailures(batchResultsDir);
|
|
1342
|
+
for (const errors of batchFailures.values()) {
|
|
1343
|
+
for (const error of errors) {
|
|
1344
|
+
lines.push(`error BATCH_FAILED: ${error["batch_id"]} episode ${error["episode"]} part ${error["part"]} - ${error["message"]}`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
const validation = exists(scriptPath)
|
|
1348
|
+
? validateScript(workspace, scriptPath, { requireSource: false })
|
|
1349
|
+
: { issues: [] };
|
|
1350
|
+
for (const issue of asList(validation["issues"])) {
|
|
1351
|
+
const whereParts = [];
|
|
1352
|
+
for (const k of ["episode", "scene", "action_index"]) {
|
|
1353
|
+
if (issue[k] !== null && issue[k] !== undefined)
|
|
1354
|
+
whereParts.push(strOf(issue[k]));
|
|
1355
|
+
}
|
|
1356
|
+
const where = whereParts.join(" ");
|
|
1357
|
+
lines.push(`${issue["severity"]} ${issue["code"]}: ${issue["summary"]}${where ? ` [${where}]` : ""}`);
|
|
1484
1358
|
}
|
|
1485
|
-
const state = addInspectedTarget(workspace, target);
|
|
1486
|
-
const missingReview = reviewBlockers(state);
|
|
1487
1359
|
const report = {
|
|
1488
|
-
title:
|
|
1489
|
-
result: lines.length > 0 ? lines : ["No
|
|
1360
|
+
title: "DIRECT REVIEW",
|
|
1361
|
+
result: lines.length > 0 ? lines : ["No issues, curation decisions, or batch failures to review."],
|
|
1362
|
+
artifacts: [scriptPath, path.join(dd, "asset_curation.json"), batchResultsDir],
|
|
1490
1363
|
next: [
|
|
1491
|
-
"
|
|
1492
|
-
|
|
1364
|
+
"Run `direct review --episode <n>` for source↔extract alignment.",
|
|
1365
|
+
"Edit with `--draft` (e.g. `scriptctl rename … --draft`), then validate/export.",
|
|
1493
1366
|
],
|
|
1494
1367
|
};
|
|
1495
1368
|
return [report, EXIT_OK];
|