@lingjingai/scriptctl 0.12.2 → 0.13.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/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 -24
- 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 +104 -237
- 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 +42 -75
- 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
|
|
@@ -941,10 +878,6 @@ export async function commandInit(opts) {
|
|
|
941
878
|
batch_reused: reusedBatchCount,
|
|
942
879
|
batch_failed: erroredBatches,
|
|
943
880
|
last_error: null,
|
|
944
|
-
review_status: "pending",
|
|
945
|
-
review_missing: [...REVIEW_TARGETS],
|
|
946
|
-
inspected_targets: [],
|
|
947
|
-
patch_count: 0,
|
|
948
881
|
});
|
|
949
882
|
const title = passed
|
|
950
883
|
? "INIT COMPLETE: Initial script ready"
|
|
@@ -961,7 +894,6 @@ export async function commandInit(opts) {
|
|
|
961
894
|
`episodes: ${results.length}/${totalEpisodes} complete`,
|
|
962
895
|
`batches: ${doneBatches}/${totalBatches} done (reused ${reusedBatchCount})`,
|
|
963
896
|
...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
|
|
964
|
-
"agent_review: pending",
|
|
965
897
|
],
|
|
966
898
|
artifacts: [
|
|
967
899
|
path.join(workspace, "source.txt"),
|
|
@@ -979,10 +911,10 @@ export async function commandInit(opts) {
|
|
|
979
911
|
issues: summarizeIssues(asList(validation["issues"])),
|
|
980
912
|
next: providerName === "mock"
|
|
981
913
|
? [
|
|
982
|
-
"Run
|
|
914
|
+
"Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export.",
|
|
983
915
|
"Do not export mock-provider results for delivery.",
|
|
984
916
|
]
|
|
985
|
-
: ["Run
|
|
917
|
+
: ["Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export."],
|
|
986
918
|
};
|
|
987
919
|
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
988
920
|
}
|
|
@@ -1304,173 +1236,81 @@ function renderAssetCurationSummary(curation) {
|
|
|
1304
1236
|
}
|
|
1305
1237
|
return lines;
|
|
1306
1238
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
const script = exists(scriptPath)
|
|
1323
|
-
? readJson(scriptPath)
|
|
1324
|
-
: { episodes: [], actors: [], locations: [], props: [] };
|
|
1325
|
-
let validation = exists(validationPath) ? readJson(validationPath) : { issues: [] };
|
|
1326
|
-
if (target === "issue" && exists(scriptPath)) {
|
|
1327
|
-
validation = validateScript(workspace, scriptPath, { requireSource: false });
|
|
1328
|
-
}
|
|
1329
|
-
const batchPlanPath = path.join(dd, "batch_plan.json");
|
|
1330
|
-
const batchPlan = exists(batchPlanPath) ? readJson(batchPlanPath) : { batches: [] };
|
|
1331
|
-
const batchCounts = new Map();
|
|
1332
|
-
for (const batch of asList(batchPlan["batches"])) {
|
|
1333
|
-
if (!isDict(batch))
|
|
1334
|
-
continue;
|
|
1335
|
-
const ep = Number(batch["episode"] ?? 0);
|
|
1336
|
-
batchCounts.set(ep, (batchCounts.get(ep) ?? 0) + 1);
|
|
1337
|
-
}
|
|
1338
|
-
const failedByEpisode = new Map();
|
|
1339
|
-
const batchResultsDir = path.join(dd, "batch_results");
|
|
1340
|
-
if (exists(batchResultsDir)) {
|
|
1341
|
-
const files = fs.readdirSync(batchResultsDir).filter((n) => n.endsWith(".error.json")).sort();
|
|
1342
|
-
for (const name of files) {
|
|
1343
|
-
const errorFile = path.join(batchResultsDir, name);
|
|
1344
|
-
try {
|
|
1345
|
-
const error = readJson(errorFile);
|
|
1346
|
-
const episodeNum = Number(error["episode"] ?? 0);
|
|
1347
|
-
if (!failedByEpisode.has(episodeNum))
|
|
1348
|
-
failedByEpisode.set(episodeNum, []);
|
|
1349
|
-
failedByEpisode.get(episodeNum).push(strOf(error["batch_id"]) || name.replace(/\.error\.json$/, ""));
|
|
1350
|
-
}
|
|
1351
|
-
catch {
|
|
1352
|
-
// ignore
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
const lines = [];
|
|
1357
|
-
if (target === "episode") {
|
|
1358
|
-
if (exists(scriptPath)) {
|
|
1359
|
-
for (const ep of asList(script["episodes"])) {
|
|
1360
|
-
if (itemId && itemId !== strOf(ep["episode_id"]))
|
|
1361
|
-
continue;
|
|
1362
|
-
const scenes = asList(ep["scenes"]);
|
|
1363
|
-
const sceneCount = scenes.length;
|
|
1364
|
-
let actionCount = 0;
|
|
1365
|
-
for (const scene of scenes)
|
|
1366
|
-
actionCount += asList(scene["actions"]).length;
|
|
1367
|
-
const rawNum = normalizeInt(strOf(ep["episode_id"]), lines.length + 1);
|
|
1368
|
-
const failed = (failedByEpisode.get(rawNum) ?? []).join(",") || "-";
|
|
1369
|
-
lines.push(`${ep["episode_id"]}: scenes=${sceneCount}, actions=${actionCount}, batches=${batchCounts.get(rawNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1370
|
-
}
|
|
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);
|
|
1371
1254
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
const plan = exists(planPath) ? readJson(planPath) : { episodes: [] };
|
|
1375
|
-
for (const ep of asList(plan["episodes"])) {
|
|
1376
|
-
const epId = fmtId("ep", Number(ep["episode"] ?? 0));
|
|
1377
|
-
if (itemId && itemId !== epId && itemId !== strOf(ep["episode"]))
|
|
1378
|
-
continue;
|
|
1379
|
-
const episodeNum = Number(ep["episode"] ?? 0);
|
|
1380
|
-
const failed = (failedByEpisode.get(episodeNum) ?? []).join(",") || "-";
|
|
1381
|
-
lines.push(`${epId}: batches=${batchCounts.get(episodeNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1382
|
-
}
|
|
1255
|
+
catch {
|
|
1256
|
+
// ignore unreadable error file
|
|
1383
1257
|
}
|
|
1384
1258
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
continue;
|
|
1396
|
-
const aliases = asList(asset["aliases"]).length;
|
|
1397
|
-
const states = asList(asset["states"]).length;
|
|
1398
|
-
const role = key === "actors" ? `, role_type=${asset["role_type"]}` : "";
|
|
1399
|
-
const description = strOf(asset["description"]).trim() ? "yes" : "missing";
|
|
1400
|
-
const singular = key.slice(0, -1);
|
|
1401
|
-
lines.push(`${singular} ${asset[idKey]}: ${asset[nameKey]} aliases=${aliases}, states=${states}${role}, description=${description}`);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
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
|
+
});
|
|
1404
1269
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
try {
|
|
1413
|
-
error = readJson(errorPath);
|
|
1414
|
-
}
|
|
1415
|
-
catch {
|
|
1416
|
-
continue;
|
|
1417
|
-
}
|
|
1418
|
-
if (itemId && itemId !== strOf(error["batch_id"]) && itemId !== strOf(error["episode"]) && itemId !== strOf(error["error_type"]))
|
|
1419
|
-
continue;
|
|
1420
|
-
lines.push(`error BATCH_FAILED: ${error["batch_id"]} episode ${error["episode"]} part ${error["part"]} - ${error["message"]}`);
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
for (const issue of asList(validation["issues"])) {
|
|
1424
|
-
if (itemId && itemId !== strOf(issue["code"]) && itemId !== strOf(issue["severity"]))
|
|
1425
|
-
continue;
|
|
1426
|
-
const whereParts = [];
|
|
1427
|
-
for (const k of ["episode", "scene", "action_index"]) {
|
|
1428
|
-
if (issue[k] !== null && issue[k] !== undefined)
|
|
1429
|
-
whereParts.push(strOf(issue[k]));
|
|
1430
|
-
}
|
|
1431
|
-
const where = whereParts.join(" ");
|
|
1432
|
-
lines.push(`${issue["severity"]} ${issue["code"]}: ${issue["summary"]}${where ? ` [${where}]` : ""}`);
|
|
1433
|
-
}
|
|
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
|
+
});
|
|
1434
1277
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
throw new CliError("INSPECT BLOCKED: --episode must be integers", "--episode must be integers.", {
|
|
1456
|
-
exitCode: EXIT_USAGE,
|
|
1457
|
-
required: ["--episode <n>[,<n>...]"],
|
|
1458
|
-
received: [`--episode ${episodeArg}`],
|
|
1459
|
-
nextSteps: ["Pass episode numbers like --episode 1,15,30."],
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
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);
|
|
1462
1298
|
const sourcePath = path.join(workspace, "source.txt");
|
|
1463
1299
|
if (!exists(sourcePath)) {
|
|
1464
|
-
throw new CliError("
|
|
1300
|
+
throw new CliError("REVIEW BLOCKED: source.txt not found", "source.txt not found.", {
|
|
1465
1301
|
exitCode: EXIT_INPUT,
|
|
1466
1302
|
required: [sourcePath],
|
|
1467
1303
|
received: ["source.txt missing"],
|
|
1468
1304
|
nextSteps: ["Run scriptctl direct init first."],
|
|
1469
1305
|
});
|
|
1470
1306
|
}
|
|
1307
|
+
const script = exists(scriptPath)
|
|
1308
|
+
? readJson(scriptPath)
|
|
1309
|
+
: { episodes: [], actors: [], locations: [], props: [] };
|
|
1471
1310
|
const sourceText = readText(sourcePath);
|
|
1472
1311
|
const episodePlanPath = path.join(dd, "episode_plan.json");
|
|
1473
1312
|
const episodePlan = exists(episodePlanPath) ? readJson(episodePlanPath) : { episodes: [] };
|
|
1313
|
+
const lines = [];
|
|
1474
1314
|
for (let idx = 0; idx < episodeFilters.length; idx++) {
|
|
1475
1315
|
if (idx > 0) {
|
|
1476
1316
|
lines.push("");
|
|
@@ -1478,23 +1318,50 @@ export function commandInspect(opts) {
|
|
|
1478
1318
|
}
|
|
1479
1319
|
lines.push(...renderReviewEpisode(sourceText, episodePlan, script, episodeFilters[idx]));
|
|
1480
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];
|
|
1481
1328
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1329
|
+
// No --episode: workspace-level diagnostics. Curation decisions + every issue
|
|
1330
|
+
// (validation issues recomputed live, plus batch extraction failures).
|
|
1331
|
+
const lines = [];
|
|
1332
|
+
const curationPath = path.join(dd, "asset_curation.json");
|
|
1333
|
+
if (exists(curationPath)) {
|
|
1334
|
+
const curation = readJson(curationPath);
|
|
1335
|
+
if (isDict(curation))
|
|
1336
|
+
lines.push(...renderAssetCurationSummary(curation));
|
|
1337
|
+
}
|
|
1338
|
+
const batchFailures = collectBatchFailures(batchResultsDir);
|
|
1339
|
+
for (const errors of batchFailures.values()) {
|
|
1340
|
+
for (const error of errors) {
|
|
1341
|
+
lines.push(`error BATCH_FAILED: ${error["batch_id"]} episode ${error["episode"]} part ${error["part"]} - ${error["message"]}`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
// persist:false keeps review read-only — it must not overwrite validation.json
|
|
1345
|
+
// (the help/comment promise "no state side effects").
|
|
1346
|
+
const validation = exists(scriptPath)
|
|
1347
|
+
? validateScript(workspace, scriptPath, { requireSource: false, persist: false })
|
|
1348
|
+
: { issues: [] };
|
|
1349
|
+
for (const issue of asList(validation["issues"])) {
|
|
1350
|
+
const whereParts = [];
|
|
1351
|
+
for (const k of ["episode", "scene", "action_index"]) {
|
|
1352
|
+
if (issue[k] !== null && issue[k] !== undefined)
|
|
1353
|
+
whereParts.push(strOf(issue[k]));
|
|
1354
|
+
}
|
|
1355
|
+
const where = whereParts.join(" ");
|
|
1356
|
+
lines.push(`${issue["severity"]} ${issue["code"]}: ${issue["summary"]}${where ? ` [${where}]` : ""}`);
|
|
1489
1357
|
}
|
|
1490
|
-
const state = addInspectedTarget(workspace, target);
|
|
1491
|
-
const missingReview = reviewBlockers(state);
|
|
1492
1358
|
const report = {
|
|
1493
|
-
title:
|
|
1494
|
-
result: lines.length > 0 ? lines : ["No
|
|
1359
|
+
title: "DIRECT REVIEW",
|
|
1360
|
+
result: lines.length > 0 ? lines : ["No issues, curation decisions, or batch failures to review."],
|
|
1361
|
+
artifacts: [scriptPath, path.join(dd, "asset_curation.json"), batchResultsDir],
|
|
1495
1362
|
next: [
|
|
1496
|
-
"
|
|
1497
|
-
|
|
1363
|
+
"Run `direct review --episode <n>` for source↔extract alignment.",
|
|
1364
|
+
"Edit with `--draft` (e.g. `scriptctl rename … --draft`), then validate/export.",
|
|
1498
1365
|
],
|
|
1499
1366
|
};
|
|
1500
1367
|
return [report, EXIT_OK];
|