@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/script.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
1
3
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { CliError, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE, directDir, exists, readJson, readText, scriptJsonPath, sha256Text, writeJson, } from "../common.js";
|
|
3
6
|
import { applyPatchOperations, collectAssetRefs, collectStateRefs, parseAnyAddress, parseStateTarget, validateScript, PATCH_OP_SCHEMA, } from "../domain/script-core.js";
|
|
4
7
|
import { RemoteScriptOutputStore } from "../infra/script-output-api.js";
|
|
5
8
|
import { LocalScriptOutputStore } from "../infra/local-script-output-store.js";
|
|
6
9
|
import { ScriptOutputApiError, resolveOutputMode, } from "../infra/script-output-store.js";
|
|
7
|
-
import { readRunState, summarizeIssues, updateRunState, } from "./direct.js";
|
|
10
|
+
import { markMetadataConfidenceReviewed, readRunState, reviewBlockers, summarizeIssues, updateRunState, markPatched, } from "./direct.js";
|
|
8
11
|
function strOf(v) {
|
|
9
12
|
if (v === null || v === undefined)
|
|
10
13
|
return "";
|
|
@@ -62,12 +65,14 @@ function episodeCharCounts(scenes) {
|
|
|
62
65
|
export class ScriptEditSession {
|
|
63
66
|
workspace;
|
|
64
67
|
script;
|
|
68
|
+
scriptPath;
|
|
65
69
|
client;
|
|
66
70
|
projectGroupNo;
|
|
67
71
|
revision;
|
|
68
72
|
constructor(opts) {
|
|
69
73
|
this.workspace = opts.workspace;
|
|
70
74
|
this.script = opts.script;
|
|
75
|
+
this.scriptPath = opts.scriptPath ?? null;
|
|
71
76
|
this.client = opts.client ?? null;
|
|
72
77
|
this.projectGroupNo = opts.projectGroupNo ?? null;
|
|
73
78
|
this.revision = opts.revision ?? null;
|
|
@@ -76,10 +81,16 @@ export class ScriptEditSession {
|
|
|
76
81
|
return this.client !== null;
|
|
77
82
|
}
|
|
78
83
|
get artifactLabel() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
if (this.remote) {
|
|
85
|
+
// Public label — strips the internal `db:/script-output/project-groups/`
|
|
86
|
+
// storage prefix (which leaks implementation), but keeps the project id
|
|
87
|
+
// and revision so two different project groups at the same revision stay
|
|
88
|
+
// distinguishable in user-visible report.artifacts entries. The project
|
|
89
|
+
// id is not a secret: the user supplies it themselves via
|
|
90
|
+
// `--project-group-no` or env config.
|
|
91
|
+
return `script-output/${this.projectGroupNo}@revision/${this.revision ?? 0}`;
|
|
92
|
+
}
|
|
93
|
+
return this.scriptPath ?? "";
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
96
|
export function scriptOutputClient(opts) {
|
|
@@ -201,7 +212,7 @@ export function apiErrorToCli(title, exc) {
|
|
|
201
212
|
isConflict
|
|
202
213
|
? "Reload the latest script revision and retry."
|
|
203
214
|
: isMissing
|
|
204
|
-
? "Run `scriptctl
|
|
215
|
+
? "Run `scriptctl direct export` to publish a script first."
|
|
205
216
|
: "Run `scriptctl doctor` to verify gateway configuration; retry if it was transient.",
|
|
206
217
|
],
|
|
207
218
|
errorCode: isConflict
|
|
@@ -220,28 +231,12 @@ export async function currentRevisionOrZero(client) {
|
|
|
220
231
|
}
|
|
221
232
|
catch (exc) {
|
|
222
233
|
if (exc instanceof ScriptOutputApiError) {
|
|
223
|
-
|
|
224
|
-
// backend returns a generic error (UNKNOWN_EXCEPTION, not a recognizable
|
|
225
|
-
// "no script yet" signal) when a project group has no published script, so
|
|
226
|
-
// a first publish can't tell first-publish from a real failure here.
|
|
227
|
-
// getRevision is only an optimistic-lock hint, not a correctness gate, so
|
|
228
|
-
// fall back to baseRevision 0 and let replaceScript decide. This is SAFE:
|
|
229
|
-
// replaceScript enforces the baseRevision lock server-side — it CREATES only
|
|
230
|
-
// when no script exists and REJECTS with a revision conflict if one already
|
|
231
|
-
// does — so base 0 never overwrites an existing script. A genuine
|
|
232
|
-
// gateway/auth failure still surfaces on the subsequent replaceScript call.
|
|
233
|
-
// Note: once a script exists, getRevision returns cleanly, so this fallback
|
|
234
|
-
// only ever engages on the no-script (first-publish) path.
|
|
235
|
-
return 0;
|
|
234
|
+
throw apiErrorToCli("SCRIPT API BLOCKED: Revision query failed", exc);
|
|
236
235
|
}
|
|
237
236
|
throw exc;
|
|
238
237
|
}
|
|
239
238
|
}
|
|
240
|
-
|
|
241
|
-
// script.initial.json is a private artifact between `import init` and
|
|
242
|
-
// `import push`; it is not an editable surface.
|
|
243
|
-
async function loadScript(opts) {
|
|
244
|
-
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
239
|
+
async function loadRemoteScript(opts, workspace) {
|
|
245
240
|
const client = scriptOutputClient(opts);
|
|
246
241
|
let script;
|
|
247
242
|
let revision;
|
|
@@ -260,7 +255,7 @@ async function loadScript(opts) {
|
|
|
260
255
|
exitCode: EXIT_INPUT,
|
|
261
256
|
required: ["existing script-output project document"],
|
|
262
257
|
received: [`projectGroupNo=${client.projectGroupNo}`],
|
|
263
|
-
nextSteps: ["Run `scriptctl
|
|
258
|
+
nextSteps: ["Run `scriptctl direct export` first, or pass `--script-path` to edit a local intermediate script JSON."],
|
|
264
259
|
errorCode: "SCRIPT_NOT_FOUND",
|
|
265
260
|
});
|
|
266
261
|
}
|
|
@@ -281,22 +276,66 @@ async function loadScript(opts) {
|
|
|
281
276
|
revision: Number((revision ?? {})["revision"] ?? 0),
|
|
282
277
|
});
|
|
283
278
|
}
|
|
279
|
+
async function loadScriptForEdit(opts) {
|
|
280
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
281
|
+
if (!opts["script_path"]) {
|
|
282
|
+
return loadRemoteScript(opts, workspace);
|
|
283
|
+
}
|
|
284
|
+
const p = scriptJsonPath(opts);
|
|
285
|
+
if (!exists(p)) {
|
|
286
|
+
throw new CliError("SCRIPT BLOCKED: Local script file not found", "Local script file not found.", {
|
|
287
|
+
exitCode: EXIT_INPUT,
|
|
288
|
+
required: ["--script-path existing local script JSON"],
|
|
289
|
+
received: [p],
|
|
290
|
+
nextSteps: ["Pass --script-path to an existing local intermediate script JSON, or omit --script-path to use DB-backed script-output."],
|
|
291
|
+
errorCode: "SCRIPT_NOT_FOUND",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
let data;
|
|
295
|
+
try {
|
|
296
|
+
data = readJson(p);
|
|
297
|
+
}
|
|
298
|
+
catch (exc) {
|
|
299
|
+
throw new CliError("SCRIPT BLOCKED: script JSON invalid", "script JSON invalid.", {
|
|
300
|
+
exitCode: EXIT_INPUT,
|
|
301
|
+
required: ["valid JSON"],
|
|
302
|
+
received: [`${p}: ${exc.message}`],
|
|
303
|
+
nextSteps: ["Fix the local script JSON syntax before editing."],
|
|
304
|
+
errorCode: "SCRIPT_JSON_INVALID",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (!isDict(data)) {
|
|
308
|
+
throw new CliError("SCRIPT BLOCKED: script root invalid", "script root invalid.", {
|
|
309
|
+
exitCode: EXIT_USAGE,
|
|
310
|
+
required: ["script root object"],
|
|
311
|
+
received: [Array.isArray(data) ? "array" : typeof data],
|
|
312
|
+
nextSteps: ["Use a valid script document object."],
|
|
313
|
+
errorCode: "SCRIPT_ROOT_INVALID",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return new ScriptEditSession({ workspace, scriptPath: p, script: data });
|
|
317
|
+
}
|
|
284
318
|
function validateSession(session, opts = {}) {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
requireSource
|
|
293
|
-
|
|
294
|
-
persist: false,
|
|
295
|
-
});
|
|
296
|
-
validation["script_path"] = session.artifactLabel;
|
|
297
|
-
if (persist)
|
|
319
|
+
const requireSource = opts.requireSource ?? false;
|
|
320
|
+
if (!session.remote) {
|
|
321
|
+
return validateScript(session.workspace, session.scriptPath, { requireSource });
|
|
322
|
+
}
|
|
323
|
+
const tmpPath = path.join(os.tmpdir(), `scriptctl-db-script-${randomUUID()}.json`);
|
|
324
|
+
try {
|
|
325
|
+
fs.writeFileSync(tmpPath, JSON.stringify(session.script, null, 2) + "\n", "utf-8");
|
|
326
|
+
const validation = validateScript(session.workspace, tmpPath, { requireSource });
|
|
327
|
+
validation["script_path"] = session.artifactLabel;
|
|
298
328
|
writeJson(path.join(directDir(session.workspace), "validation.json"), validation);
|
|
299
|
-
|
|
329
|
+
return validation;
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
try {
|
|
333
|
+
fs.unlinkSync(tmpPath);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// ignore
|
|
337
|
+
}
|
|
338
|
+
}
|
|
300
339
|
}
|
|
301
340
|
function validationIssuePath(issue) {
|
|
302
341
|
if (issue["path"])
|
|
@@ -386,8 +425,12 @@ function requestIdForScriptWrite(opts, op, payload) {
|
|
|
386
425
|
return `scriptctl:${op}:${sha256Text(canonical)}`;
|
|
387
426
|
}
|
|
388
427
|
async function saveScriptSession(session, opts, op) {
|
|
428
|
+
if (!session.remote) {
|
|
429
|
+
writeJson(session.scriptPath, session.script);
|
|
430
|
+
return [null, false];
|
|
431
|
+
}
|
|
389
432
|
if (session.client === null)
|
|
390
|
-
throw new Error("script session missing client");
|
|
433
|
+
throw new Error("remote script session missing client");
|
|
391
434
|
const baseRevision = Number(session.revision ?? 0);
|
|
392
435
|
const requestId = requestIdForScriptWrite(opts, op, { script: session.script });
|
|
393
436
|
let res;
|
|
@@ -445,11 +488,67 @@ function patchOperationsFromPayload(payload) {
|
|
|
445
488
|
return operations;
|
|
446
489
|
}
|
|
447
490
|
// ---------------------------------------------------------------------------
|
|
491
|
+
// command_patch (legacy: applies to script.initial.json directly with source)
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
export function commandPatch(opts) {
|
|
494
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
495
|
+
const patchPath = strOf(opts["patch"]);
|
|
496
|
+
const scriptPath = path.join(directDir(workspace), "script.initial.json");
|
|
497
|
+
const sourcePath = path.join(workspace, "source.txt");
|
|
498
|
+
if (!exists(patchPath)) {
|
|
499
|
+
throw new CliError("PATCH BLOCKED: Patch file not found", "Patch file not found.", {
|
|
500
|
+
exitCode: EXIT_INPUT,
|
|
501
|
+
required: ["--patch: existing JSON file"],
|
|
502
|
+
received: [patchPath],
|
|
503
|
+
nextSteps: ["Write a patch JSON file and rerun patch."],
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (!exists(scriptPath) || !exists(sourcePath)) {
|
|
507
|
+
throw new CliError("PATCH BLOCKED: Required artifact missing", "Required artifact missing.", {
|
|
508
|
+
exitCode: EXIT_INPUT,
|
|
509
|
+
required: ["source.txt and script.initial.json"],
|
|
510
|
+
received: [scriptPath, sourcePath],
|
|
511
|
+
nextSteps: ["Run scriptctl direct init first."],
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
let payload;
|
|
515
|
+
try {
|
|
516
|
+
payload = readJson(patchPath);
|
|
517
|
+
}
|
|
518
|
+
catch (exc) {
|
|
519
|
+
throw new CliError("PATCH BLOCKED: Patch JSON invalid", "Patch JSON invalid.", {
|
|
520
|
+
exitCode: EXIT_USAGE,
|
|
521
|
+
required: ["valid JSON patch file"],
|
|
522
|
+
received: [`${patchPath}: ${exc.message}`],
|
|
523
|
+
nextSteps: ["Fix patch JSON and rerun patch."],
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const operations = patchOperationsFromPayload(payload);
|
|
527
|
+
const script = readJson(scriptPath);
|
|
528
|
+
const applied = applyPatchOperations(script, readText(sourcePath), operations);
|
|
529
|
+
writeJson(scriptPath, script);
|
|
530
|
+
markMetadataConfidenceReviewed(workspace, operations);
|
|
531
|
+
const validation = validateScript(workspace, scriptPath);
|
|
532
|
+
markPatched(workspace, applied.length);
|
|
533
|
+
const passed = Boolean(validation["passed"]);
|
|
534
|
+
const report = {
|
|
535
|
+
title: passed ? "PATCH APPLIED: Validation passed" : "PATCH APPLIED: Repair issues remain",
|
|
536
|
+
result: [
|
|
537
|
+
`operations: ${applied.length}`,
|
|
538
|
+
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
539
|
+
],
|
|
540
|
+
artifacts: [scriptPath, path.join(directDir(workspace), "validation.json")],
|
|
541
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
542
|
+
next: [passed ? "Export the final script." : "Inspect remaining issues and apply another patch."],
|
|
543
|
+
};
|
|
544
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
545
|
+
}
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
448
547
|
// commandScriptValidate / commandScriptInspect
|
|
449
548
|
// ---------------------------------------------------------------------------
|
|
450
549
|
export async function commandScriptValidate(opts) {
|
|
451
550
|
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
452
|
-
const session = await
|
|
551
|
+
const session = await loadScriptForEdit(opts);
|
|
453
552
|
const validation = validateSession(session);
|
|
454
553
|
await syncValidationResult(session, validation);
|
|
455
554
|
const stats = validation["stats"] ?? {};
|
|
@@ -579,13 +678,13 @@ export async function commandScriptPatch(opts) {
|
|
|
579
678
|
}
|
|
580
679
|
const operations = patchOperationsFromPayload(payload);
|
|
581
680
|
const dryRun = Boolean(opts["dry_run"]);
|
|
582
|
-
const session = await
|
|
681
|
+
const session = await loadScriptForEdit(opts);
|
|
583
682
|
const script = session.script;
|
|
584
683
|
const applied = applyPatchOperations(script, scriptSourceText(workspace), operations);
|
|
585
684
|
if (dryRun) {
|
|
586
685
|
// Validate the in-memory mutated script WITHOUT calling saveScriptSession.
|
|
587
686
|
// Run validate but skip syncValidationResult (which writes to remote) too.
|
|
588
|
-
const validation = validateSession(session
|
|
687
|
+
const validation = validateSession(session);
|
|
589
688
|
const passed = Boolean(validation["passed"]);
|
|
590
689
|
const report = {
|
|
591
690
|
title: passed ? "SCRIPT PATCH DRY-RUN PASSED" : "SCRIPT PATCH DRY-RUN: Validation needs repair",
|
|
@@ -607,6 +706,12 @@ export async function commandScriptPatch(opts) {
|
|
|
607
706
|
const [newRevision, idempotent] = await saveScriptSession(session, opts, applied.length === 1 ? applied[0] : "script.patch");
|
|
608
707
|
const validation = validateSession(session);
|
|
609
708
|
await syncValidationResult(session, validation, newRevision);
|
|
709
|
+
// Direct-stage local patches: keep run_state in sync so reviewBlockers can
|
|
710
|
+
// unblock direct export. Detect via script path under directDir.
|
|
711
|
+
if (!session.remote && session.scriptPath && session.scriptPath.startsWith(directDir(workspace))) {
|
|
712
|
+
markMetadataConfidenceReviewed(workspace, operations);
|
|
713
|
+
markPatched(workspace, applied.length);
|
|
714
|
+
}
|
|
610
715
|
const passed = Boolean(validation["passed"]);
|
|
611
716
|
const resultLines = [
|
|
612
717
|
`operations: ${applied.length}`,
|
|
@@ -634,7 +739,7 @@ export async function commandScriptPatch(opts) {
|
|
|
634
739
|
// ---------------------------------------------------------------------------
|
|
635
740
|
async function applySingleScriptOp(opts, op) {
|
|
636
741
|
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
637
|
-
const session = await
|
|
742
|
+
const session = await loadScriptForEdit(opts);
|
|
638
743
|
const script = session.script;
|
|
639
744
|
const applied = applyPatchOperations(script, scriptSourceText(workspace), [op]);
|
|
640
745
|
const opName = applied[0] ?? strOf(op["op"]);
|
|
@@ -676,7 +781,7 @@ function summarizeScriptOp(_script, op) {
|
|
|
676
781
|
return `已执行 ${kind}`;
|
|
677
782
|
}
|
|
678
783
|
async function commandStateRefs(opts, plan = false) {
|
|
679
|
-
const session = await
|
|
784
|
+
const session = await loadScriptForEdit(opts);
|
|
680
785
|
const script = session.script;
|
|
681
786
|
const args = asList(opts["_args"]);
|
|
682
787
|
const [targetKind, targetId, stateId] = parseStateTarget(args[0] ?? "");
|
|
@@ -715,48 +820,72 @@ async function commandStateRefs(opts, plan = false) {
|
|
|
715
820
|
return [report, EXIT_OK];
|
|
716
821
|
}
|
|
717
822
|
// ---------------------------------------------------------------------------
|
|
718
|
-
//
|
|
823
|
+
// command_export (direct export)
|
|
719
824
|
// ---------------------------------------------------------------------------
|
|
720
|
-
export async function
|
|
825
|
+
export async function commandExport(opts) {
|
|
721
826
|
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
827
|
+
const force = Boolean(opts["force"]);
|
|
722
828
|
const scriptPath = path.join(directDir(workspace), "script.initial.json");
|
|
723
829
|
if (!exists(scriptPath)) {
|
|
724
|
-
throw new CliError("
|
|
830
|
+
throw new CliError("EXPORT BLOCKED: script.initial.json not found", "script.initial.json not found.", {
|
|
725
831
|
exitCode: EXIT_INPUT,
|
|
726
832
|
required: ["workspace/draft/scriptctl/direct/script.initial.json"],
|
|
727
833
|
received: [scriptPath],
|
|
728
|
-
nextSteps: ["Run scriptctl
|
|
834
|
+
nextSteps: ["Run scriptctl direct init first."],
|
|
729
835
|
});
|
|
730
836
|
}
|
|
731
837
|
const state = readRunState(workspace);
|
|
732
838
|
const provider = strOf(state["provider"]);
|
|
733
|
-
|
|
734
|
-
// still honored for back-compat with environments configured before the rename.
|
|
735
|
-
const allowMock = process.env.SCRIPTCTL_ALLOW_MOCK_PUSH === "1" || process.env.SCRIPTCTL_ALLOW_MOCK_EXPORT === "1";
|
|
736
|
-
if (provider === "mock" && !allowMock) {
|
|
839
|
+
if (provider === "mock" && process.env.SCRIPTCTL_ALLOW_MOCK_EXPORT !== "1") {
|
|
737
840
|
const report = {
|
|
738
|
-
title: "
|
|
739
|
-
result: ["script.initial.json was produced by --provider mock and was not
|
|
841
|
+
title: "EXPORT BLOCKED: Mock provider result",
|
|
842
|
+
result: ["script.initial.json was produced by --provider mock and was not exported."],
|
|
740
843
|
artifacts: [path.join(directDir(workspace), "run_state.json")],
|
|
741
844
|
next: ["Rerun init with --provider anthropic for deliverable conversion."],
|
|
742
845
|
};
|
|
743
846
|
return [report, EXIT_NEEDS_AGENT];
|
|
744
847
|
}
|
|
848
|
+
const missingReview = reviewBlockers(state);
|
|
849
|
+
if (missingReview.length > 0) {
|
|
850
|
+
const report = {
|
|
851
|
+
title: "EXPORT BLOCKED: Agent review incomplete",
|
|
852
|
+
result: ["script.initial.json was not exported.", `missing review: ${missingReview.join(", ")}`],
|
|
853
|
+
artifacts: [path.join(directDir(workspace), "run_state.json")],
|
|
854
|
+
next: ["Run inspect for each missing target, or apply a structured patch, then validate/export."],
|
|
855
|
+
};
|
|
856
|
+
return [report, EXIT_NEEDS_AGENT];
|
|
857
|
+
}
|
|
858
|
+
const validation = validateScript(workspace, scriptPath);
|
|
859
|
+
const blockingOrError = Boolean(validation["has_blocking"]) ||
|
|
860
|
+
asList(validation["issues"]).some((it) => isDict(it) && (it["severity"] === "blocking" || it["severity"] === "error"));
|
|
861
|
+
if (!validation["passed"] && (!force || blockingOrError)) {
|
|
862
|
+
const title = force
|
|
863
|
+
? "EXPORT BLOCKED: Validation errors require repair"
|
|
864
|
+
: "EXPORT BLOCKED: Validation needs agent repair";
|
|
865
|
+
const report = {
|
|
866
|
+
title,
|
|
867
|
+
result: ["script.initial.json was not exported."],
|
|
868
|
+
artifacts: [path.join(directDir(workspace), "validation.json")],
|
|
869
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
870
|
+
next: ["Inspect issues and apply structured patches, then validate/export."],
|
|
871
|
+
};
|
|
872
|
+
return [report, EXIT_NEEDS_AGENT];
|
|
873
|
+
}
|
|
745
874
|
let script;
|
|
746
875
|
try {
|
|
747
876
|
script = readJson(scriptPath);
|
|
748
877
|
}
|
|
749
878
|
catch (exc) {
|
|
750
|
-
throw new CliError("
|
|
879
|
+
throw new CliError("EXPORT BLOCKED: script.initial.json invalid", "script.initial.json invalid.", {
|
|
751
880
|
exitCode: EXIT_INPUT,
|
|
752
881
|
required: ["valid script.initial.json"],
|
|
753
882
|
received: [`${scriptPath}: ${exc.message}`],
|
|
754
|
-
nextSteps: ["Fix script.initial.json or rerun
|
|
883
|
+
nextSteps: ["Fix script.initial.json or rerun direct init."],
|
|
755
884
|
errorCode: "SCRIPT_JSON_INVALID",
|
|
756
885
|
});
|
|
757
886
|
}
|
|
758
887
|
if (!isDict(script)) {
|
|
759
|
-
throw new CliError("
|
|
888
|
+
throw new CliError("EXPORT BLOCKED: script root invalid", "script root invalid.", {
|
|
760
889
|
exitCode: EXIT_USAGE,
|
|
761
890
|
required: ["script root object"],
|
|
762
891
|
received: [Array.isArray(script) ? "array" : typeof script],
|
|
@@ -764,14 +893,12 @@ export async function commandPush(opts) {
|
|
|
764
893
|
errorCode: "SCRIPT_ROOT_INVALID",
|
|
765
894
|
});
|
|
766
895
|
}
|
|
767
|
-
// Validation is advisory, not a gate: push always publishes so the agent can
|
|
768
|
-
// self-review and repair on the DB script afterwards. requireSource:false so a
|
|
769
|
-
// missing source.txt never aborts the publish. Results are still computed and
|
|
770
|
-
// synced so the review subagent has the issue list to act on.
|
|
771
|
-
const validation = validateScript(workspace, scriptPath, { requireSource: false });
|
|
772
896
|
const client = scriptOutputClient(opts);
|
|
773
897
|
const baseRevision = await currentRevisionOrZero(client);
|
|
774
|
-
|
|
898
|
+
// Sorted-keys serialization mirrors Python json.dumps(sort_keys=True, separators=(",",":"))
|
|
899
|
+
const sortedScript = sortDeep(script);
|
|
900
|
+
const scriptHash = sha256Text(JSON.stringify(sortedScript));
|
|
901
|
+
const requestId = strOf(opts["request_id"]).trim() || `scriptctl-direct-export:${scriptHash}`;
|
|
775
902
|
let replaceRes;
|
|
776
903
|
try {
|
|
777
904
|
replaceRes = await client.replaceScript({
|
|
@@ -783,7 +910,7 @@ export async function commandPush(opts) {
|
|
|
783
910
|
}
|
|
784
911
|
catch (exc) {
|
|
785
912
|
if (exc instanceof ScriptOutputApiError) {
|
|
786
|
-
throw apiErrorToCli("SCRIPT API BLOCKED:
|
|
913
|
+
throw apiErrorToCli("SCRIPT API BLOCKED: Export write failed", exc);
|
|
787
914
|
}
|
|
788
915
|
throw exc;
|
|
789
916
|
}
|
|
@@ -797,21 +924,17 @@ export async function commandPush(opts) {
|
|
|
797
924
|
});
|
|
798
925
|
await syncValidationResult(remoteSession, validation, revision);
|
|
799
926
|
const outputLabel = remoteSession.artifactLabel;
|
|
800
|
-
updateRunState(workspace, { status: "
|
|
801
|
-
const passed = Boolean(validation["passed"]);
|
|
927
|
+
updateRunState(workspace, { status: "exported", output_path: outputLabel });
|
|
802
928
|
const report = {
|
|
803
|
-
title: "
|
|
929
|
+
title: "EXPORT COMPLETE: Final script stored in DB",
|
|
804
930
|
result: [
|
|
805
|
-
`validation: ${passed ? "passed" : "
|
|
931
|
+
`validation: ${validation["passed"] ? "passed" : "forced"}`,
|
|
806
932
|
`base_revision: ${baseRevision}`,
|
|
807
933
|
`revision: ${revision}`,
|
|
808
934
|
`idempotent: ${String(Boolean(replaceRes["idempotent"])).toLowerCase()}`,
|
|
809
935
|
],
|
|
810
|
-
issues: passed ? [] : summarizeIssues(asList(validation["issues"])),
|
|
811
936
|
artifacts: [outputLabel, path.join(directDir(workspace), "validation.json")],
|
|
812
|
-
next:
|
|
813
|
-
? ["Self-review the DB script, then proceed to downstream asset or footage stages."]
|
|
814
|
-
: ["Self-review and repair the DB script (validate / issues --severity error), then proceed downstream."],
|
|
937
|
+
next: ["Proceed to downstream asset or footage stages."],
|
|
815
938
|
};
|
|
816
939
|
return [report, EXIT_OK];
|
|
817
940
|
}
|
|
@@ -957,8 +1080,12 @@ function buildReport(op, title, lines, artifactLabel, next) {
|
|
|
957
1080
|
};
|
|
958
1081
|
}
|
|
959
1082
|
// ----- summary --------------------------------------------------------------
|
|
1083
|
+
function synopsisPreview(text, limit) {
|
|
1084
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
1085
|
+
return oneLine.length > limit ? `${oneLine.slice(0, limit)}…` : oneLine;
|
|
1086
|
+
}
|
|
960
1087
|
export async function commandSummary(opts) {
|
|
961
|
-
const session = await
|
|
1088
|
+
const session = await loadScriptForEdit(opts);
|
|
962
1089
|
const script = session.script;
|
|
963
1090
|
const episodes = asList(script["episodes"]);
|
|
964
1091
|
const scenes = [];
|
|
@@ -977,11 +1104,14 @@ export async function commandSummary(opts) {
|
|
|
977
1104
|
`props: ${asList(script["props"]).length}`,
|
|
978
1105
|
`speakers: ${asList(script["speakers"]).length}`,
|
|
979
1106
|
];
|
|
1107
|
+
const synopsis = strOf(script["synopsis"]).trim();
|
|
1108
|
+
if (synopsis)
|
|
1109
|
+
lines.push(`synopsis: ${synopsisPreview(synopsis, 120)}`);
|
|
980
1110
|
return [buildReport("query.summary", "SCRIPT SUMMARY", lines, session.artifactLabel, ["Use plural-noun queries (episodes / scenes / actions / actors / ...) or edit verbs to act on the script."]), EXIT_OK];
|
|
981
1111
|
}
|
|
982
1112
|
// ----- episodes -------------------------------------------------------------
|
|
983
1113
|
export async function commandEpisodes(opts) {
|
|
984
|
-
const session = await
|
|
1114
|
+
const session = await loadScriptForEdit(opts);
|
|
985
1115
|
const script = session.script;
|
|
986
1116
|
const itemId = strOf(opts["id"]).trim();
|
|
987
1117
|
const minChars = parseBound(opts["min_chars"]);
|
|
@@ -999,7 +1129,9 @@ export async function commandEpisodes(opts) {
|
|
|
999
1129
|
continue;
|
|
1000
1130
|
if (maxChars !== null && chars.total > maxChars)
|
|
1001
1131
|
continue;
|
|
1002
|
-
|
|
1132
|
+
const epSynopsis = strOf(ep["synopsis"]).trim();
|
|
1133
|
+
const synopsisField = epSynopsis ? `, synopsis=${synopsisPreview(epSynopsis, 60)}` : "";
|
|
1134
|
+
lines.push(`${ep["episode_id"]}: scenes=${scenes.length}, actions=${actionCount}, chars=${chars.total} (dialogue=${chars.dialogue}, action=${chars.action}), title=${ep["title"] || "-"}${synopsisField}`);
|
|
1003
1135
|
}
|
|
1004
1136
|
return [buildReport("query.episodes", "EPISODES", lines, session.artifactLabel, ["Use `scriptctl scenes --in <ep>` to drill in, or edit verbs to mutate."]), EXIT_OK];
|
|
1005
1137
|
}
|
|
@@ -1035,7 +1167,7 @@ function formatScene(epId, scene, names) {
|
|
|
1035
1167
|
return `${epId}/${scene["scene_id"]} [${space} ${time}] location=${locations.join(",") || "-"} actors=${actors.join(",") || "-"} props=${props.join(",") || "-"} actions=${actionCount}`;
|
|
1036
1168
|
}
|
|
1037
1169
|
export async function commandScenes(opts) {
|
|
1038
|
-
const session = await
|
|
1170
|
+
const session = await loadScriptForEdit(opts);
|
|
1039
1171
|
const script = session.script;
|
|
1040
1172
|
const inFilter = parseInFilter(strOf(opts["in"]));
|
|
1041
1173
|
const hasActor = strOf(opts["has_actor"]).trim();
|
|
@@ -1073,7 +1205,7 @@ export async function commandScenes(opts) {
|
|
|
1073
1205
|
}
|
|
1074
1206
|
// ----- actions --------------------------------------------------------------
|
|
1075
1207
|
export async function commandActions(opts) {
|
|
1076
|
-
const session = await
|
|
1208
|
+
const session = await loadScriptForEdit(opts);
|
|
1077
1209
|
const script = session.script;
|
|
1078
1210
|
const inFilter = parseInFilter(strOf(opts["in"]));
|
|
1079
1211
|
const grep = strOf(opts["grep"]);
|
|
@@ -1258,7 +1390,7 @@ function listAssetsByKind(script, kind, opts) {
|
|
|
1258
1390
|
return lines;
|
|
1259
1391
|
}
|
|
1260
1392
|
export async function commandActors(opts) {
|
|
1261
|
-
const session = await
|
|
1393
|
+
const session = await loadScriptForEdit(opts);
|
|
1262
1394
|
const lines = listAssetsByKind(session.script, "actor", {
|
|
1263
1395
|
id: strOf(opts["id"]).trim(),
|
|
1264
1396
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1267,7 +1399,7 @@ export async function commandActors(opts) {
|
|
|
1267
1399
|
return [buildReport("query.actors", "ACTORS", lines, session.artifactLabel, ["Use `scriptctl rename actor:<id>` / `describe` / `merge` to edit."]), EXIT_OK];
|
|
1268
1400
|
}
|
|
1269
1401
|
export async function commandLocations(opts) {
|
|
1270
|
-
const session = await
|
|
1402
|
+
const session = await loadScriptForEdit(opts);
|
|
1271
1403
|
const lines = listAssetsByKind(session.script, "location", {
|
|
1272
1404
|
id: strOf(opts["id"]).trim(),
|
|
1273
1405
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1276,7 +1408,7 @@ export async function commandLocations(opts) {
|
|
|
1276
1408
|
return [buildReport("query.locations", "LOCATIONS", lines, session.artifactLabel, ["Use `scriptctl rename location:<id>` etc. to edit."]), EXIT_OK];
|
|
1277
1409
|
}
|
|
1278
1410
|
export async function commandProps(opts) {
|
|
1279
|
-
const session = await
|
|
1411
|
+
const session = await loadScriptForEdit(opts);
|
|
1280
1412
|
const lines = listAssetsByKind(session.script, "prop", {
|
|
1281
1413
|
id: strOf(opts["id"]).trim(),
|
|
1282
1414
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1285,7 +1417,7 @@ export async function commandProps(opts) {
|
|
|
1285
1417
|
return [buildReport("query.props", "PROPS", lines, session.artifactLabel, ["Use `scriptctl rename prop:<id>` etc. to edit."]), EXIT_OK];
|
|
1286
1418
|
}
|
|
1287
1419
|
export async function commandAssets(opts) {
|
|
1288
|
-
const session = await
|
|
1420
|
+
const session = await loadScriptForEdit(opts);
|
|
1289
1421
|
const kindFilter = strOf(opts["kind"]).trim();
|
|
1290
1422
|
const nameOpt = strOf(opts["name"]).trim();
|
|
1291
1423
|
const idOpt = strOf(opts["id"]).trim();
|
|
@@ -1300,7 +1432,7 @@ export async function commandAssets(opts) {
|
|
|
1300
1432
|
}
|
|
1301
1433
|
// ----- speakers -------------------------------------------------------------
|
|
1302
1434
|
export async function commandSpeakers(opts) {
|
|
1303
|
-
const session = await
|
|
1435
|
+
const session = await loadScriptForEdit(opts);
|
|
1304
1436
|
const script = session.script;
|
|
1305
1437
|
const idOpt = strOf(opts["id"]).trim();
|
|
1306
1438
|
const nameOpt = strOf(opts["name"]).trim();
|
|
@@ -1323,7 +1455,7 @@ export async function commandSpeakers(opts) {
|
|
|
1323
1455
|
}
|
|
1324
1456
|
// ----- issues ---------------------------------------------------------------
|
|
1325
1457
|
export async function commandIssues(opts) {
|
|
1326
|
-
const session = await
|
|
1458
|
+
const session = await loadScriptForEdit(opts);
|
|
1327
1459
|
const severityFilter = strOf(opts["severity"]).trim();
|
|
1328
1460
|
const codeFilter = strOf(opts["code"]).trim();
|
|
1329
1461
|
const validation = validateSession(session);
|
|
@@ -1345,7 +1477,7 @@ export async function commandIssues(opts) {
|
|
|
1345
1477
|
}
|
|
1346
1478
|
// ----- refs (unified reverse lookup) ----------------------------------------
|
|
1347
1479
|
export async function commandRefs(opts) {
|
|
1348
|
-
const session = await
|
|
1480
|
+
const session = await loadScriptForEdit(opts);
|
|
1349
1481
|
const script = session.script;
|
|
1350
1482
|
const args = asList(opts["_args"]);
|
|
1351
1483
|
const target = strOf(args[0]).trim();
|