@lingjingai/scriptctl 0.10.3 → 0.11.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/README.md +6 -6
- package/dist/cli.js +34 -11
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +5 -0
- package/dist/common.js +87 -0
- package/dist/common.js.map +1 -1
- package/dist/domain/direct-core.d.ts +7 -1
- package/dist/domain/direct-core.js +45 -20
- package/dist/domain/direct-core.js.map +1 -1
- package/dist/domain/script-core.d.ts +0 -1
- package/dist/domain/script-core.js +9 -41
- package/dist/domain/script-core.js.map +1 -1
- package/dist/help-text.js +95 -35
- 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/infra/script-output-api.js +10 -21
- package/dist/infra/script-output-api.js.map +1 -1
- package/dist/usecases/direct.d.ts +7 -0
- package/dist/usecases/direct.js +655 -14
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/episode.js +5 -8
- package/dist/usecases/episode.js.map +1 -1
- package/dist/usecases/script.d.ts +4 -1
- package/dist/usecases/script.js +218 -86
- package/dist/usecases/script.js.map +1 -1
- package/package.json +2 -2
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, 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
|
-
import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, enrichEpisodePlanTitles, extractBatchWithRecovery, mergeEpisodeResults, normalizeEpisodeResult, normalizeInt, recoverBatchFromSource, uniqueAdd, validateBatchExtractionQuality, validateEpisodeExtractionQuality, _md_push_asset, curateScriptAssets, applyMetadataToScript, } from "../domain/direct-core.js";
|
|
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, PARSE_MD_SPEC, REVIEW_TARGETS, SUPPORTED_EXTS, deletePath, deleteTree, directDir, exists, fmtId, readJson, readText, sha256Text, writeJson, } from "../common.js";
|
|
4
|
+
import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, enrichEpisodePlanTitles, extractBatchWithRecovery, mergeEpisodeResults, normalizeEpisodeResult, normalizeInt, parseMarkdownBatch, 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";
|
|
7
7
|
import { makeSourceManifest, prepareSource, } from "../infra/converters.js";
|
|
@@ -66,6 +66,69 @@ 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
|
+
}
|
|
69
132
|
// ---------------------------------------------------------------------------
|
|
70
133
|
// Paths for episode/batch results
|
|
71
134
|
// ---------------------------------------------------------------------------
|
|
@@ -522,7 +585,7 @@ function writeBatchFailure(dir, batch, exc) {
|
|
|
522
585
|
function initFailedReport(workspace, opts) {
|
|
523
586
|
const payload = {
|
|
524
587
|
status: "init_failed",
|
|
525
|
-
command: "
|
|
588
|
+
command: "direct init",
|
|
526
589
|
init_stage: opts.stage,
|
|
527
590
|
last_error: { title: opts.title, received: opts.received, failed_at: checkpointTimestamp() },
|
|
528
591
|
};
|
|
@@ -657,7 +720,7 @@ export async function commandInit(opts) {
|
|
|
657
720
|
const previousStateBeforeInit = readRunState(workspace);
|
|
658
721
|
updateRunState(workspace, {
|
|
659
722
|
status: "init_running",
|
|
660
|
-
command: "
|
|
723
|
+
command: "direct init",
|
|
661
724
|
init_stage: "source_prepare",
|
|
662
725
|
provider: providerName,
|
|
663
726
|
model,
|
|
@@ -910,11 +973,11 @@ export async function commandInit(opts) {
|
|
|
910
973
|
: "INIT INCOMPLETE: Batch extraction failed";
|
|
911
974
|
const nextSteps = sameFailuresRepeated
|
|
912
975
|
? [
|
|
913
|
-
"
|
|
976
|
+
"Run direct inspect --target issue to read failed batch details.",
|
|
914
977
|
"Do not rerun the same init command again until source, batch options, provider, or failed content has changed.",
|
|
915
978
|
]
|
|
916
979
|
: [
|
|
917
|
-
"
|
|
980
|
+
"Run direct inspect --target issue to review failed batches.",
|
|
918
981
|
"Rerun the same init once if failures look transient; completed checkpoints will be reused.",
|
|
919
982
|
];
|
|
920
983
|
const failedEpisodeSet = new Set(failedEpisodes);
|
|
@@ -957,6 +1020,7 @@ export async function commandInit(opts) {
|
|
|
957
1020
|
failure_signature: currentFailureSignature,
|
|
958
1021
|
failure_streak: failureStreak,
|
|
959
1022
|
last_error: { title: failureTitle, failed_at: checkpointTimestamp() },
|
|
1023
|
+
exportable: false,
|
|
960
1024
|
});
|
|
961
1025
|
const issues = failures.slice(0, 5).map((it) => `${it["batch_id"]} episode ${it["episode"]} part ${it["part"]}: ${it["error_type"]} - ${it["message"]}`);
|
|
962
1026
|
const report = {
|
|
@@ -1147,7 +1211,7 @@ export async function commandInit(opts) {
|
|
|
1147
1211
|
const status = passed ? "ready_for_agent" : "needs_agent_repair";
|
|
1148
1212
|
updateRunState(workspace, {
|
|
1149
1213
|
status,
|
|
1150
|
-
command: "
|
|
1214
|
+
command: "direct init",
|
|
1151
1215
|
init_stage: "complete",
|
|
1152
1216
|
checkpoint,
|
|
1153
1217
|
batch_checkpoint: batchCheckpoint,
|
|
@@ -1176,10 +1240,11 @@ export async function commandInit(opts) {
|
|
|
1176
1240
|
failure_signature: [],
|
|
1177
1241
|
failure_streak: 0,
|
|
1178
1242
|
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.
|
|
1182
1243
|
review_status: "pending",
|
|
1244
|
+
review_missing: [...REVIEW_TARGETS],
|
|
1245
|
+
inspected_targets: [],
|
|
1246
|
+
patch_count: 0,
|
|
1247
|
+
exportable: providerName !== "mock",
|
|
1183
1248
|
});
|
|
1184
1249
|
const title = passed
|
|
1185
1250
|
? "INIT COMPLETE: Initial script ready"
|
|
@@ -1196,7 +1261,7 @@ export async function commandInit(opts) {
|
|
|
1196
1261
|
`episode checkpoint reused: ${skipped.length}`,
|
|
1197
1262
|
`batches: ${completedBatches}/${asList(batchPlan["batches"]).length} completed`,
|
|
1198
1263
|
`batch checkpoint reused: ${skippedEpisodeBatchCount + skippedBatches.length}`,
|
|
1199
|
-
"
|
|
1264
|
+
"agent_review: pending",
|
|
1200
1265
|
],
|
|
1201
1266
|
artifacts: [
|
|
1202
1267
|
path.join(workspace, "source.txt"),
|
|
@@ -1214,10 +1279,10 @@ export async function commandInit(opts) {
|
|
|
1214
1279
|
issues: summarizeIssues(asList(validation["issues"])),
|
|
1215
1280
|
next: providerName === "mock"
|
|
1216
1281
|
? [
|
|
1217
|
-
"
|
|
1218
|
-
"
|
|
1282
|
+
"Run inspect for issue, episode, and asset; apply patches if needed; then validate/export.",
|
|
1283
|
+
"Do not export mock-provider results for delivery.",
|
|
1219
1284
|
]
|
|
1220
|
-
: ["Run
|
|
1285
|
+
: ["Run inspect for issue, episode, and asset; apply patches if needed; then validate/export."],
|
|
1221
1286
|
};
|
|
1222
1287
|
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
1223
1288
|
}
|
|
@@ -1233,4 +1298,580 @@ export function summarizeIssues(issues) {
|
|
|
1233
1298
|
const first = issues[0];
|
|
1234
1299
|
return [parts.join("; "), `first: ${first["code"]} - ${first["summary"]}`];
|
|
1235
1300
|
}
|
|
1301
|
+
// ---------------------------------------------------------------------------
|
|
1302
|
+
// command_parse — subagent-authored four-document md → script.initial.json
|
|
1303
|
+
//
|
|
1304
|
+
// Deterministic, no-LLM counterpart of `direct init`: instead of extracting an
|
|
1305
|
+
// LLM-written spec md from raw source, it parses md that subagents already wrote
|
|
1306
|
+
// (per-episode 正文 md + one shared 资产.md), assembles the same
|
|
1307
|
+
// script.initial.json, and hands off to the existing direct
|
|
1308
|
+
// inspect/validate/export downstream (zero changes there).
|
|
1309
|
+
// ---------------------------------------------------------------------------
|
|
1310
|
+
const _EP_FILE_RE = /^ep[_-]?0*(\d+)\.(?:md|markdown)$/i;
|
|
1311
|
+
function resolveAssetsPath(opts, mdDir) {
|
|
1312
|
+
const explicit = strOf(opts["assets"]).trim();
|
|
1313
|
+
if (explicit) {
|
|
1314
|
+
if (!exists(explicit) || !fs.statSync(explicit).isFile()) {
|
|
1315
|
+
throw new CliError("PARSE BLOCKED: assets md not found", "assets md not found.", {
|
|
1316
|
+
exitCode: EXIT_INPUT,
|
|
1317
|
+
required: ["--assets: existing shared 资产 md file"],
|
|
1318
|
+
received: [`--assets: ${explicit}`],
|
|
1319
|
+
nextSteps: ["Point --assets at the shared 资产.md, or omit it to auto-detect in the md dir."],
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
return explicit;
|
|
1323
|
+
}
|
|
1324
|
+
for (const name of ["资产.md", "assets.md", "资产.markdown"]) {
|
|
1325
|
+
const p = path.join(mdDir, name);
|
|
1326
|
+
if (exists(p))
|
|
1327
|
+
return p;
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
function collectEpisodeMdFiles(dir, assetsPath) {
|
|
1332
|
+
if (!exists(dir) || !fs.statSync(dir).isDirectory())
|
|
1333
|
+
return [];
|
|
1334
|
+
const assetsAbs = assetsPath ? path.resolve(assetsPath) : "";
|
|
1335
|
+
const out = [];
|
|
1336
|
+
for (const name of fs.readdirSync(dir)) {
|
|
1337
|
+
const m = _EP_FILE_RE.exec(name);
|
|
1338
|
+
if (!m)
|
|
1339
|
+
continue;
|
|
1340
|
+
const full = path.join(dir, name);
|
|
1341
|
+
if (path.resolve(full) === assetsAbs)
|
|
1342
|
+
continue;
|
|
1343
|
+
if (!fs.statSync(full).isFile())
|
|
1344
|
+
continue;
|
|
1345
|
+
out.push({ path: full, episode: parseInt(m[1], 10) });
|
|
1346
|
+
}
|
|
1347
|
+
out.sort((a, b) => a.episode - b.episode);
|
|
1348
|
+
return out;
|
|
1349
|
+
}
|
|
1350
|
+
export function commandParse(opts) {
|
|
1351
|
+
if (opts["spec"]) {
|
|
1352
|
+
return [{ title: "PARSE SPEC: 四文档 md 写法", body: PARSE_MD_SPEC }, EXIT_OK];
|
|
1353
|
+
}
|
|
1354
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
1355
|
+
const args = asList(opts["_args"]);
|
|
1356
|
+
const mdDir = strOf(opts["md_dir"] || args[0] || path.join(workspace, "parse"));
|
|
1357
|
+
if (!exists(mdDir) || !fs.statSync(mdDir).isDirectory()) {
|
|
1358
|
+
throw new CliError("PARSE BLOCKED: md workspace not found", "md workspace not found.", {
|
|
1359
|
+
exitCode: EXIT_INPUT,
|
|
1360
|
+
required: ["a directory with per-episode 正文 md + 资产.md"],
|
|
1361
|
+
received: [mdDir],
|
|
1362
|
+
nextSteps: ["Pass the md workspace dir: scriptctl parse <dir>. Run `scriptctl parse --spec` for the format."],
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
const assetsPath = resolveAssetsPath(opts, mdDir);
|
|
1366
|
+
let episodesDir = strOf(opts["episodes_dir"]).trim();
|
|
1367
|
+
if (!episodesDir) {
|
|
1368
|
+
const sub = path.join(mdDir, "episodes");
|
|
1369
|
+
episodesDir = exists(sub) && fs.statSync(sub).isDirectory() ? sub : mdDir;
|
|
1370
|
+
}
|
|
1371
|
+
const bodyFiles = collectEpisodeMdFiles(episodesDir, assetsPath);
|
|
1372
|
+
if (bodyFiles.length === 0) {
|
|
1373
|
+
throw new CliError("PARSE BLOCKED: no episode md found", "no episode md found.", {
|
|
1374
|
+
exitCode: EXIT_INPUT,
|
|
1375
|
+
required: ["per-episode body md named like ep_001.md"],
|
|
1376
|
+
received: [episodesDir],
|
|
1377
|
+
nextSteps: ["Add per-episode 正文 md (ep_001.md, ep_002.md, ...). Run `scriptctl parse --spec` for the format."],
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
let bible = null;
|
|
1381
|
+
if (assetsPath) {
|
|
1382
|
+
try {
|
|
1383
|
+
bible = parseMarkdownBatch(readText(assetsPath), { episode: 0, part: 1 }, { fragmentMode: true, allowEmptyScenes: true });
|
|
1384
|
+
}
|
|
1385
|
+
catch (exc) {
|
|
1386
|
+
const e = exc;
|
|
1387
|
+
throw new CliError("PARSE BLOCKED: assets md invalid", "assets md invalid.", {
|
|
1388
|
+
exitCode: EXIT_INPUT,
|
|
1389
|
+
required: ["shared 资产 md following `scriptctl parse --spec`"],
|
|
1390
|
+
received: [`${path.basename(assetsPath)}: ${(e?.message ?? "").slice(0, 200)}`],
|
|
1391
|
+
nextSteps: ["Fix the 资产 md and re-run parse."],
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const results = [];
|
|
1396
|
+
const sourceChunks = [];
|
|
1397
|
+
for (const file of bodyFiles) {
|
|
1398
|
+
const bodyText = readText(file.path);
|
|
1399
|
+
sourceChunks.push(`# ep_${pad3(file.episode)}\n${bodyText.trim()}`);
|
|
1400
|
+
try {
|
|
1401
|
+
results.push(parseMarkdownBatch(bodyText, { episode: file.episode, part: 1 }, { fragmentMode: true }));
|
|
1402
|
+
}
|
|
1403
|
+
catch (exc) {
|
|
1404
|
+
const e = exc;
|
|
1405
|
+
throw new CliError("PARSE BLOCKED: episode md invalid", "episode md invalid.", {
|
|
1406
|
+
exitCode: EXIT_INPUT,
|
|
1407
|
+
required: ["per-episode 正文 md following `scriptctl parse --spec`"],
|
|
1408
|
+
received: [`${path.basename(file.path)}: ${(e?.message ?? "").slice(0, 200)}`],
|
|
1409
|
+
nextSteps: ["Fix the episode md and re-run parse."],
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
results.sort((a, b) => Number(a["episode"] ?? 0) - Number(b["episode"] ?? 0));
|
|
1414
|
+
// Fold the shared asset declarations into the first episode result so their
|
|
1415
|
+
// descriptions / states flow into the merge. Asset names are deduplicated
|
|
1416
|
+
// globally by mergeEpisodeResults, so prepending bible-first gives the shared
|
|
1417
|
+
// (canonical) descriptions priority over anything inferred per episode.
|
|
1418
|
+
if (bible && results.length > 0) {
|
|
1419
|
+
const first = results[0];
|
|
1420
|
+
for (const key of ["actors", "locations", "props", "speakers", "state_definitions"]) {
|
|
1421
|
+
first[key] = [...asList(bible[key]), ...asList(first[key])];
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const title = strOf(opts["title"]).trim() || path.basename(path.resolve(mdDir));
|
|
1425
|
+
const script = mergeEpisodeResults(results, title);
|
|
1426
|
+
const globalSynopsis = bible ? strOf(bible["synopsis"]).trim() : "";
|
|
1427
|
+
if (globalSynopsis)
|
|
1428
|
+
script["synopsis"] = globalSynopsis;
|
|
1429
|
+
const dd = directDir(workspace);
|
|
1430
|
+
fs.mkdirSync(dd, { recursive: true });
|
|
1431
|
+
// Write source.txt so the existing direct validate/export downstream (which
|
|
1432
|
+
// gates on source.txt existing) works unchanged. For a parse-origin script the
|
|
1433
|
+
// authored md *is* the source, so we persist the concatenated bodies.
|
|
1434
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
1435
|
+
fs.writeFileSync(path.join(workspace, "source.txt"), sourceChunks.join("\n\n") + "\n", "utf-8");
|
|
1436
|
+
const scriptPath = path.join(dd, "script.initial.json");
|
|
1437
|
+
writeJson(scriptPath, script);
|
|
1438
|
+
const validation = validateScript(workspace, scriptPath, { requireSource: false });
|
|
1439
|
+
const passed = Boolean(validation["passed"]);
|
|
1440
|
+
updateRunState(workspace, {
|
|
1441
|
+
status: passed ? "ready_for_agent" : "needs_agent_repair",
|
|
1442
|
+
command: "parse",
|
|
1443
|
+
init_stage: "complete",
|
|
1444
|
+
provider: "parse",
|
|
1445
|
+
source_path: path.resolve(mdDir),
|
|
1446
|
+
script_path: scriptPath,
|
|
1447
|
+
validation_path: path.join(dd, "validation.json"),
|
|
1448
|
+
episode_total: results.length,
|
|
1449
|
+
episode_completed: results.length,
|
|
1450
|
+
review_status: "pending",
|
|
1451
|
+
review_missing: [...REVIEW_TARGETS],
|
|
1452
|
+
inspected_targets: [],
|
|
1453
|
+
patch_count: 0,
|
|
1454
|
+
exportable: true,
|
|
1455
|
+
last_error: null,
|
|
1456
|
+
});
|
|
1457
|
+
const stats = validation["stats"] ?? {};
|
|
1458
|
+
const report = {
|
|
1459
|
+
title: passed
|
|
1460
|
+
? "PARSE COMPLETE: Initial script ready"
|
|
1461
|
+
: "PARSE NEEDS AGENT: Initial script written with repair issues",
|
|
1462
|
+
result: [
|
|
1463
|
+
`episodes: ${stats["episodes"] ?? results.length}`,
|
|
1464
|
+
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1465
|
+
`actions: ${stats["actions"] ?? 0}`,
|
|
1466
|
+
`assets md: ${assetsPath ? path.basename(assetsPath) : "(none)"}`,
|
|
1467
|
+
`synopsis: ${globalSynopsis ? "yes" : "no"}`,
|
|
1468
|
+
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
1469
|
+
"agent_review: pending",
|
|
1470
|
+
],
|
|
1471
|
+
artifacts: [scriptPath, path.join(dd, "validation.json"), path.join(dd, "run_state.json")],
|
|
1472
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
1473
|
+
next: ["Run direct inspect (episode/asset/issue) for the two-stage review; apply patches if needed; then direct validate/export."],
|
|
1474
|
+
};
|
|
1475
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
1476
|
+
}
|
|
1477
|
+
// ---------------------------------------------------------------------------
|
|
1478
|
+
// command_validate
|
|
1479
|
+
// ---------------------------------------------------------------------------
|
|
1480
|
+
export function commandValidate(opts) {
|
|
1481
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
1482
|
+
const scriptPath = opts["script_path"] ? strOf(opts["script_path"]) : null;
|
|
1483
|
+
const validation = validateScript(workspace, scriptPath);
|
|
1484
|
+
const stats = validation["stats"] ?? {};
|
|
1485
|
+
const passed = Boolean(validation["passed"]);
|
|
1486
|
+
const report = {
|
|
1487
|
+
title: passed ? "VALIDATE PASSED: Script is ready" : "VALIDATE NEEDS AGENT: Repair issues found",
|
|
1488
|
+
result: [
|
|
1489
|
+
`episodes: ${stats["episodes"] ?? 0}`,
|
|
1490
|
+
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1491
|
+
`actions: ${stats["actions"] ?? 0}`,
|
|
1492
|
+
],
|
|
1493
|
+
artifacts: [path.join(directDir(workspace), "validation.json")],
|
|
1494
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
1495
|
+
next: [passed ? "Export script.json." : "Inspect issues and apply structured patches."],
|
|
1496
|
+
};
|
|
1497
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
1498
|
+
}
|
|
1499
|
+
// ---------------------------------------------------------------------------
|
|
1500
|
+
// command_inspect (incl. review rendering)
|
|
1501
|
+
// ---------------------------------------------------------------------------
|
|
1502
|
+
function renderReviewEpisode(sourceText, episodePlan, script, episodeNum) {
|
|
1503
|
+
const epId = fmtId("ep", episodeNum);
|
|
1504
|
+
const scriptEp = asList(script["episodes"]).find((ep) => ep["episode_id"] === epId);
|
|
1505
|
+
const planEp = asList(episodePlan["episodes"]).find((ep) => Number(ep["episode"] ?? 0) === episodeNum);
|
|
1506
|
+
const lines = [];
|
|
1507
|
+
if (!planEp && !scriptEp) {
|
|
1508
|
+
lines.push(`⚠ Episode ${episodeNum} not found in episode_plan.json or script.initial.json.`);
|
|
1509
|
+
const available = [...new Set(asList(episodePlan["episodes"]).map((ep) => Number(ep["episode"] ?? 0)))].sort((a, b) => a - b);
|
|
1510
|
+
if (available.length > 0)
|
|
1511
|
+
lines.push(`Available episodes: ${available.join(", ")}`);
|
|
1512
|
+
return lines;
|
|
1513
|
+
}
|
|
1514
|
+
const title = (scriptEp?.["title"] ?? planEp?.["title"]) || "(无标题)";
|
|
1515
|
+
lines.push("=".repeat(72));
|
|
1516
|
+
lines.push(`EPISODE ${episodeNum} / ${epId} — ${title}`);
|
|
1517
|
+
lines.push("=".repeat(72));
|
|
1518
|
+
lines.push("");
|
|
1519
|
+
lines.push("--- 原文 source.txt ---");
|
|
1520
|
+
if (planEp) {
|
|
1521
|
+
const span = isDict(planEp["source_span"]) ? planEp["source_span"] : {};
|
|
1522
|
+
const start = Number(span["start"] ?? 0);
|
|
1523
|
+
const end = Number(span["end"] ?? sourceText.length);
|
|
1524
|
+
const snippet = sourceText.slice(start, end).replace(/\s+$/, "");
|
|
1525
|
+
lines.push(snippet || "(empty)");
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
lines.push("(no episode_plan entry; source span unavailable)");
|
|
1529
|
+
}
|
|
1530
|
+
lines.push("");
|
|
1531
|
+
lines.push("--- 抽取 script.initial.json ---");
|
|
1532
|
+
if (scriptEp) {
|
|
1533
|
+
const actorIdToName = new Map();
|
|
1534
|
+
for (const a of asList(script["actors"]))
|
|
1535
|
+
actorIdToName.set(strOf(a["actor_id"]), strOf(a["actor_name"]));
|
|
1536
|
+
const speakerIdToName = new Map();
|
|
1537
|
+
for (const s of asList(script["speakers"]))
|
|
1538
|
+
speakerIdToName.set(strOf(s["speaker_id"]), strOf(s["display_name"]));
|
|
1539
|
+
const locationIdToName = new Map();
|
|
1540
|
+
for (const l of asList(script["locations"]))
|
|
1541
|
+
locationIdToName.set(strOf(l["location_id"]), strOf(l["location_name"]));
|
|
1542
|
+
const propIdToName = new Map();
|
|
1543
|
+
for (const p of asList(script["props"]))
|
|
1544
|
+
propIdToName.set(strOf(p["prop_id"]), strOf(p["prop_name"]));
|
|
1545
|
+
for (const scene of asList(scriptEp["scenes"])) {
|
|
1546
|
+
const env = isDict(scene["environment"]) ? scene["environment"] : {};
|
|
1547
|
+
const space = strOf(env["space"]) || "?";
|
|
1548
|
+
const timeOfDay = strOf(env["time"]) || "?";
|
|
1549
|
+
const sceneId = strOf(scene["scene_id"]) || "?";
|
|
1550
|
+
let locName = "";
|
|
1551
|
+
for (const ref of asList(scene["locations"])) {
|
|
1552
|
+
locName = locationIdToName.get(strOf(ref["location_id"])) || locName;
|
|
1553
|
+
break;
|
|
1554
|
+
}
|
|
1555
|
+
const actorNames = [];
|
|
1556
|
+
for (const ref of asList(scene["actors"])) {
|
|
1557
|
+
const n = actorIdToName.get(strOf(ref["actor_id"]));
|
|
1558
|
+
if (n)
|
|
1559
|
+
actorNames.push(n);
|
|
1560
|
+
}
|
|
1561
|
+
const propNames = [];
|
|
1562
|
+
for (const ref of asList(scene["props"])) {
|
|
1563
|
+
const n = propIdToName.get(strOf(ref["prop_id"]));
|
|
1564
|
+
if (n)
|
|
1565
|
+
propNames.push(n);
|
|
1566
|
+
}
|
|
1567
|
+
lines.push("");
|
|
1568
|
+
lines.push(`Scene ${sceneId} [${space} ${timeOfDay}] ${locName || "(未知场景)"}`);
|
|
1569
|
+
if (actorNames.length > 0)
|
|
1570
|
+
lines.push(` Actors: ${actorNames.join(", ")}`);
|
|
1571
|
+
if (propNames.length > 0)
|
|
1572
|
+
lines.push(` Props: ${propNames.join(", ")}`);
|
|
1573
|
+
for (const action of asList(scene["actions"])) {
|
|
1574
|
+
const kind = strOf(action["type"]);
|
|
1575
|
+
const content = strOf(action["content"]).replace(/\n/g, "\n ");
|
|
1576
|
+
let tag;
|
|
1577
|
+
if (kind === "dialogue") {
|
|
1578
|
+
const speaker = actorIdToName.get(strOf(action["actor_id"])) || speakerIdToName.get(strOf(action["speaker_id"])) || strOf(action["speaker"]) || "?";
|
|
1579
|
+
const emotion = strOf(action["emotion"]);
|
|
1580
|
+
tag = `dlg|${speaker}` + (emotion ? `|${emotion}` : "");
|
|
1581
|
+
}
|
|
1582
|
+
else if (kind === "inner_thought") {
|
|
1583
|
+
const speaker = actorIdToName.get(strOf(action["actor_id"])) || speakerIdToName.get(strOf(action["speaker_id"])) || strOf(action["speaker"]) || "?";
|
|
1584
|
+
const emotion = strOf(action["emotion"]);
|
|
1585
|
+
tag = `think|${speaker}` + (emotion ? `|${emotion}` : "");
|
|
1586
|
+
}
|
|
1587
|
+
else {
|
|
1588
|
+
tag = "act";
|
|
1589
|
+
}
|
|
1590
|
+
lines.push(` [${tag}] ${content}`);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
else {
|
|
1595
|
+
lines.push("(script.initial.json contains no entry for this episode)");
|
|
1596
|
+
}
|
|
1597
|
+
if (scriptEp) {
|
|
1598
|
+
const actorIdsInEp = new Set();
|
|
1599
|
+
const locationIdsInEp = new Set();
|
|
1600
|
+
const propIdsInEp = new Set();
|
|
1601
|
+
for (const scene of asList(scriptEp["scenes"])) {
|
|
1602
|
+
for (const ref of asList(scene["actors"])) {
|
|
1603
|
+
if (ref["actor_id"])
|
|
1604
|
+
actorIdsInEp.add(strOf(ref["actor_id"]));
|
|
1605
|
+
}
|
|
1606
|
+
for (const ref of asList(scene["locations"])) {
|
|
1607
|
+
if (ref["location_id"])
|
|
1608
|
+
locationIdsInEp.add(strOf(ref["location_id"]));
|
|
1609
|
+
}
|
|
1610
|
+
for (const ref of asList(scene["props"])) {
|
|
1611
|
+
if (ref["prop_id"])
|
|
1612
|
+
propIdsInEp.add(strOf(ref["prop_id"]));
|
|
1613
|
+
}
|
|
1614
|
+
for (const action of asList(scene["actions"])) {
|
|
1615
|
+
if (action["actor_id"])
|
|
1616
|
+
actorIdsInEp.add(strOf(action["actor_id"]));
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (actorIdsInEp.size > 0 || locationIdsInEp.size > 0 || propIdsInEp.size > 0) {
|
|
1620
|
+
lines.push("");
|
|
1621
|
+
lines.push(`--- Episode ${episodeNum} 资产 ---`);
|
|
1622
|
+
}
|
|
1623
|
+
if (actorIdsInEp.size > 0) {
|
|
1624
|
+
lines.push("Actors:");
|
|
1625
|
+
for (const actor of asList(script["actors"])) {
|
|
1626
|
+
if (!actorIdsInEp.has(strOf(actor["actor_id"])))
|
|
1627
|
+
continue;
|
|
1628
|
+
const desc = strOf(actor["description"]).trim() || "(无描述)";
|
|
1629
|
+
const role = strOf(actor["role_type"]) || "?";
|
|
1630
|
+
lines.push(` - ${actor["actor_id"]} ${actor["actor_name"]} [${role}] — ${desc}`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if (locationIdsInEp.size > 0) {
|
|
1634
|
+
lines.push("Locations:");
|
|
1635
|
+
for (const loc of asList(script["locations"])) {
|
|
1636
|
+
if (!locationIdsInEp.has(strOf(loc["location_id"])))
|
|
1637
|
+
continue;
|
|
1638
|
+
const desc = strOf(loc["description"]).trim() || "(无描述)";
|
|
1639
|
+
lines.push(` - ${loc["location_id"]} ${loc["location_name"]} — ${desc}`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
if (propIdsInEp.size > 0) {
|
|
1643
|
+
lines.push("Props:");
|
|
1644
|
+
for (const prop of asList(script["props"])) {
|
|
1645
|
+
if (!propIdsInEp.has(strOf(prop["prop_id"])))
|
|
1646
|
+
continue;
|
|
1647
|
+
const desc = strOf(prop["description"]).trim() || "(无描述)";
|
|
1648
|
+
lines.push(` - ${prop["prop_id"]} ${prop["prop_name"]} — ${desc}`);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return lines;
|
|
1653
|
+
}
|
|
1654
|
+
function renderAssetCurationSummary(curation) {
|
|
1655
|
+
const summary = isDict(curation["summary"]) ? curation["summary"] : {};
|
|
1656
|
+
const lines = [
|
|
1657
|
+
`curation: ` +
|
|
1658
|
+
`actors ${summary["actors_before"] ?? 0}->${summary["actors_after"] ?? 0} ` +
|
|
1659
|
+
`(removed ${summary["actors_removed"] ?? 0}), ` +
|
|
1660
|
+
`props ${summary["props_before"] ?? 0}->${summary["props_after"] ?? 0} ` +
|
|
1661
|
+
`(removed ${summary["props_removed"] ?? 0}), ` +
|
|
1662
|
+
`locations ${summary["locations_before"] ?? 0}->${summary["locations_after"] ?? 0} ` +
|
|
1663
|
+
`(merged ${summary["locations_merged"] ?? 0})`,
|
|
1664
|
+
];
|
|
1665
|
+
for (const actor of asList(curation["actors"])) {
|
|
1666
|
+
if (!isDict(actor) || actor["decision"] !== "remove")
|
|
1667
|
+
continue;
|
|
1668
|
+
lines.push(`curation actor ${actor["actor_id"]}: remove ${actor["name"] || "-"} (scenes=${actor["scene_count"] ?? 0}) — ${actor["reason"] || "-"}`);
|
|
1669
|
+
}
|
|
1670
|
+
for (const prop of asList(curation["props"])) {
|
|
1671
|
+
if (!isDict(prop) || prop["decision"] !== "remove")
|
|
1672
|
+
continue;
|
|
1673
|
+
lines.push(`curation prop ${prop["prop_id"]}: remove ${prop["name"] || "-"} (scenes=${prop["scene_count"] ?? 0}) — ${prop["reason"] || "-"}`);
|
|
1674
|
+
}
|
|
1675
|
+
for (const loc of asList(curation["locations"])) {
|
|
1676
|
+
if (!isDict(loc) || loc["decision"] !== "merge")
|
|
1677
|
+
continue;
|
|
1678
|
+
lines.push(`curation location ${loc["location_id"]}: merge ${loc["name"] || "-"} -> ${loc["target_location_id"] || "-"} — ${loc["reason"] || "-"}`);
|
|
1679
|
+
}
|
|
1680
|
+
return lines;
|
|
1681
|
+
}
|
|
1682
|
+
export function commandInspect(opts) {
|
|
1683
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
1684
|
+
const target = strOf(opts["target"]);
|
|
1685
|
+
const itemId = opts["id"] ? strOf(opts["id"]) : null;
|
|
1686
|
+
const dd = directDir(workspace);
|
|
1687
|
+
const scriptPath = path.join(dd, "script.initial.json");
|
|
1688
|
+
const validationPath = path.join(dd, "validation.json");
|
|
1689
|
+
if (!exists(scriptPath) && target === "asset") {
|
|
1690
|
+
throw new CliError("INSPECT BLOCKED: script.initial.json not found", "script.initial.json not found.", {
|
|
1691
|
+
exitCode: EXIT_INPUT,
|
|
1692
|
+
required: ["workspace/draft/scriptctl/direct/script.initial.json"],
|
|
1693
|
+
received: [scriptPath],
|
|
1694
|
+
nextSteps: ["Run scriptctl direct init first."],
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
const script = exists(scriptPath)
|
|
1698
|
+
? readJson(scriptPath)
|
|
1699
|
+
: { episodes: [], actors: [], locations: [], props: [] };
|
|
1700
|
+
let validation = exists(validationPath) ? readJson(validationPath) : { issues: [] };
|
|
1701
|
+
if (target === "issue" && exists(scriptPath)) {
|
|
1702
|
+
validation = validateScript(workspace, scriptPath, { requireSource: false });
|
|
1703
|
+
}
|
|
1704
|
+
const batchPlanPath = path.join(dd, "batch_plan.json");
|
|
1705
|
+
const batchPlan = exists(batchPlanPath) ? readJson(batchPlanPath) : { batches: [] };
|
|
1706
|
+
const batchCounts = new Map();
|
|
1707
|
+
for (const batch of asList(batchPlan["batches"])) {
|
|
1708
|
+
if (!isDict(batch))
|
|
1709
|
+
continue;
|
|
1710
|
+
const ep = Number(batch["episode"] ?? 0);
|
|
1711
|
+
batchCounts.set(ep, (batchCounts.get(ep) ?? 0) + 1);
|
|
1712
|
+
}
|
|
1713
|
+
const failedByEpisode = new Map();
|
|
1714
|
+
const batchResultsDir = path.join(dd, "batch_results");
|
|
1715
|
+
if (exists(batchResultsDir)) {
|
|
1716
|
+
const files = fs.readdirSync(batchResultsDir).filter((n) => n.endsWith(".error.json")).sort();
|
|
1717
|
+
for (const name of files) {
|
|
1718
|
+
const errorFile = path.join(batchResultsDir, name);
|
|
1719
|
+
try {
|
|
1720
|
+
const error = readJson(errorFile);
|
|
1721
|
+
const episodeNum = Number(error["episode"] ?? 0);
|
|
1722
|
+
if (!failedByEpisode.has(episodeNum))
|
|
1723
|
+
failedByEpisode.set(episodeNum, []);
|
|
1724
|
+
failedByEpisode.get(episodeNum).push(strOf(error["batch_id"]) || name.replace(/\.error\.json$/, ""));
|
|
1725
|
+
}
|
|
1726
|
+
catch {
|
|
1727
|
+
// ignore
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const lines = [];
|
|
1732
|
+
if (target === "episode") {
|
|
1733
|
+
if (exists(scriptPath)) {
|
|
1734
|
+
for (const ep of asList(script["episodes"])) {
|
|
1735
|
+
if (itemId && itemId !== strOf(ep["episode_id"]))
|
|
1736
|
+
continue;
|
|
1737
|
+
const scenes = asList(ep["scenes"]);
|
|
1738
|
+
const sceneCount = scenes.length;
|
|
1739
|
+
let actionCount = 0;
|
|
1740
|
+
for (const scene of scenes)
|
|
1741
|
+
actionCount += asList(scene["actions"]).length;
|
|
1742
|
+
const rawNum = normalizeInt(strOf(ep["episode_id"]), lines.length + 1);
|
|
1743
|
+
const failed = (failedByEpisode.get(rawNum) ?? []).join(",") || "-";
|
|
1744
|
+
lines.push(`${ep["episode_id"]}: scenes=${sceneCount}, actions=${actionCount}, batches=${batchCounts.get(rawNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
else {
|
|
1748
|
+
const planPath = path.join(dd, "episode_plan.json");
|
|
1749
|
+
const plan = exists(planPath) ? readJson(planPath) : { episodes: [] };
|
|
1750
|
+
for (const ep of asList(plan["episodes"])) {
|
|
1751
|
+
const epId = fmtId("ep", Number(ep["episode"] ?? 0));
|
|
1752
|
+
if (itemId && itemId !== epId && itemId !== strOf(ep["episode"]))
|
|
1753
|
+
continue;
|
|
1754
|
+
const episodeNum = Number(ep["episode"] ?? 0);
|
|
1755
|
+
const failed = (failedByEpisode.get(episodeNum) ?? []).join(",") || "-";
|
|
1756
|
+
lines.push(`${epId}: batches=${batchCounts.get(episodeNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
else if (target === "asset") {
|
|
1761
|
+
const curationPath = path.join(dd, "asset_curation.json");
|
|
1762
|
+
if (!itemId && exists(curationPath)) {
|
|
1763
|
+
const curation = readJson(curationPath);
|
|
1764
|
+
if (isDict(curation))
|
|
1765
|
+
lines.push(...renderAssetCurationSummary(curation));
|
|
1766
|
+
}
|
|
1767
|
+
for (const [key, idKey, nameKey] of [["actors", "actor_id", "actor_name"], ["locations", "location_id", "location_name"], ["props", "prop_id", "prop_name"]]) {
|
|
1768
|
+
for (const asset of asList(script[key])) {
|
|
1769
|
+
if (itemId && itemId !== strOf(asset[idKey]) && itemId !== strOf(asset[nameKey]))
|
|
1770
|
+
continue;
|
|
1771
|
+
const aliases = asList(asset["aliases"]).length;
|
|
1772
|
+
const states = asList(asset["states"]).length;
|
|
1773
|
+
const role = key === "actors" ? `, role_type=${asset["role_type"]}` : "";
|
|
1774
|
+
const description = strOf(asset["description"]).trim() ? "yes" : "missing";
|
|
1775
|
+
const singular = key.slice(0, -1);
|
|
1776
|
+
lines.push(`${singular} ${asset[idKey]}: ${asset[nameKey]} aliases=${aliases}, states=${states}${role}, description=${description}`);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
else if (target === "issue") {
|
|
1781
|
+
for (const errors of failedByEpisode.values()) {
|
|
1782
|
+
for (const batchId of errors) {
|
|
1783
|
+
const errorPath = path.join(batchResultsDir, `${batchId}.error.json`);
|
|
1784
|
+
if (!exists(errorPath))
|
|
1785
|
+
continue;
|
|
1786
|
+
let error;
|
|
1787
|
+
try {
|
|
1788
|
+
error = readJson(errorPath);
|
|
1789
|
+
}
|
|
1790
|
+
catch {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
if (itemId && itemId !== strOf(error["batch_id"]) && itemId !== strOf(error["episode"]) && itemId !== strOf(error["error_type"]))
|
|
1794
|
+
continue;
|
|
1795
|
+
lines.push(`error BATCH_FAILED: ${error["batch_id"]} episode ${error["episode"]} part ${error["part"]} - ${error["message"]}`);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
for (const issue of asList(validation["issues"])) {
|
|
1799
|
+
if (itemId && itemId !== strOf(issue["code"]) && itemId !== strOf(issue["severity"]))
|
|
1800
|
+
continue;
|
|
1801
|
+
const whereParts = [];
|
|
1802
|
+
for (const k of ["episode", "scene", "action_index"]) {
|
|
1803
|
+
if (issue[k] !== null && issue[k] !== undefined)
|
|
1804
|
+
whereParts.push(strOf(issue[k]));
|
|
1805
|
+
}
|
|
1806
|
+
const where = whereParts.join(" ");
|
|
1807
|
+
lines.push(`${issue["severity"]} ${issue["code"]}: ${issue["summary"]}${where ? ` [${where}]` : ""}`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
else if (target === "review") {
|
|
1811
|
+
const episodeArg = strOf(opts["episode"]).trim();
|
|
1812
|
+
if (!episodeArg) {
|
|
1813
|
+
throw new CliError("INSPECT BLOCKED: --episode required for review", "--episode required for review.", {
|
|
1814
|
+
exitCode: EXIT_USAGE,
|
|
1815
|
+
required: ["--episode <n>[,<n>...]"],
|
|
1816
|
+
received: ["--episode not provided"],
|
|
1817
|
+
nextSteps: ["Pass --episode 1 (single) or --episode 1,15,30 (multiple)."],
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
let episodeFilters;
|
|
1821
|
+
try {
|
|
1822
|
+
episodeFilters = episodeArg.split(",").map((s) => s.trim()).filter((s) => s).map((s) => {
|
|
1823
|
+
const n = parseInt(s, 10);
|
|
1824
|
+
if (Number.isNaN(n))
|
|
1825
|
+
throw new Error("nan");
|
|
1826
|
+
return n;
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
catch {
|
|
1830
|
+
throw new CliError("INSPECT BLOCKED: --episode must be integers", "--episode must be integers.", {
|
|
1831
|
+
exitCode: EXIT_USAGE,
|
|
1832
|
+
required: ["--episode <n>[,<n>...]"],
|
|
1833
|
+
received: [`--episode ${episodeArg}`],
|
|
1834
|
+
nextSteps: ["Pass episode numbers like --episode 1,15,30."],
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
const sourcePath = path.join(workspace, "source.txt");
|
|
1838
|
+
if (!exists(sourcePath)) {
|
|
1839
|
+
throw new CliError("INSPECT BLOCKED: source.txt not found", "source.txt not found.", {
|
|
1840
|
+
exitCode: EXIT_INPUT,
|
|
1841
|
+
required: [sourcePath],
|
|
1842
|
+
received: ["source.txt missing"],
|
|
1843
|
+
nextSteps: ["Run scriptctl direct init first."],
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
const sourceText = readText(sourcePath);
|
|
1847
|
+
const episodePlanPath = path.join(dd, "episode_plan.json");
|
|
1848
|
+
const episodePlan = exists(episodePlanPath) ? readJson(episodePlanPath) : { episodes: [] };
|
|
1849
|
+
for (let idx = 0; idx < episodeFilters.length; idx++) {
|
|
1850
|
+
if (idx > 0) {
|
|
1851
|
+
lines.push("");
|
|
1852
|
+
lines.push("");
|
|
1853
|
+
}
|
|
1854
|
+
lines.push(...renderReviewEpisode(sourceText, episodePlan, script, episodeFilters[idx]));
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
throw new CliError("INSPECT BLOCKED: Invalid target", "Invalid inspect target.", {
|
|
1859
|
+
exitCode: EXIT_USAGE,
|
|
1860
|
+
required: ["--target: episode, asset, issue, or review"],
|
|
1861
|
+
received: [`--target: ${target}`],
|
|
1862
|
+
nextSteps: ["Use one of the supported inspect targets."],
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
const state = addInspectedTarget(workspace, target);
|
|
1866
|
+
const missingReview = reviewBlockers(state);
|
|
1867
|
+
const report = {
|
|
1868
|
+
title: `INSPECT: ${target}`,
|
|
1869
|
+
result: lines.length > 0 ? lines : ["No matching items."],
|
|
1870
|
+
next: [
|
|
1871
|
+
"Use inspect details to prepare a structured patch, or continue required review.",
|
|
1872
|
+
missingReview.length === 0 ? "Review complete; run validate/export." : `Still inspect: ${missingReview.join(", ")}.`,
|
|
1873
|
+
],
|
|
1874
|
+
};
|
|
1875
|
+
return [report, EXIT_OK];
|
|
1876
|
+
}
|
|
1236
1877
|
//# sourceMappingURL=direct.js.map
|