@lingjingai/scriptctl 0.10.3 → 0.10.4
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 +24 -11
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +4 -0
- package/dist/common.js +8 -0
- package/dist/common.js.map +1 -1
- package/dist/domain/direct-core.d.ts +1 -1
- package/dist/domain/direct-core.js +1 -16
- 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 +6 -0
- package/dist/usecases/direct.js +478 -13
- 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 +208 -85
- 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
|
}
|
|
@@ -958,7 +1081,7 @@ function buildReport(op, title, lines, artifactLabel, next) {
|
|
|
958
1081
|
}
|
|
959
1082
|
// ----- summary --------------------------------------------------------------
|
|
960
1083
|
export async function commandSummary(opts) {
|
|
961
|
-
const session = await
|
|
1084
|
+
const session = await loadScriptForEdit(opts);
|
|
962
1085
|
const script = session.script;
|
|
963
1086
|
const episodes = asList(script["episodes"]);
|
|
964
1087
|
const scenes = [];
|
|
@@ -981,7 +1104,7 @@ export async function commandSummary(opts) {
|
|
|
981
1104
|
}
|
|
982
1105
|
// ----- episodes -------------------------------------------------------------
|
|
983
1106
|
export async function commandEpisodes(opts) {
|
|
984
|
-
const session = await
|
|
1107
|
+
const session = await loadScriptForEdit(opts);
|
|
985
1108
|
const script = session.script;
|
|
986
1109
|
const itemId = strOf(opts["id"]).trim();
|
|
987
1110
|
const minChars = parseBound(opts["min_chars"]);
|
|
@@ -1035,7 +1158,7 @@ function formatScene(epId, scene, names) {
|
|
|
1035
1158
|
return `${epId}/${scene["scene_id"]} [${space} ${time}] location=${locations.join(",") || "-"} actors=${actors.join(",") || "-"} props=${props.join(",") || "-"} actions=${actionCount}`;
|
|
1036
1159
|
}
|
|
1037
1160
|
export async function commandScenes(opts) {
|
|
1038
|
-
const session = await
|
|
1161
|
+
const session = await loadScriptForEdit(opts);
|
|
1039
1162
|
const script = session.script;
|
|
1040
1163
|
const inFilter = parseInFilter(strOf(opts["in"]));
|
|
1041
1164
|
const hasActor = strOf(opts["has_actor"]).trim();
|
|
@@ -1073,7 +1196,7 @@ export async function commandScenes(opts) {
|
|
|
1073
1196
|
}
|
|
1074
1197
|
// ----- actions --------------------------------------------------------------
|
|
1075
1198
|
export async function commandActions(opts) {
|
|
1076
|
-
const session = await
|
|
1199
|
+
const session = await loadScriptForEdit(opts);
|
|
1077
1200
|
const script = session.script;
|
|
1078
1201
|
const inFilter = parseInFilter(strOf(opts["in"]));
|
|
1079
1202
|
const grep = strOf(opts["grep"]);
|
|
@@ -1258,7 +1381,7 @@ function listAssetsByKind(script, kind, opts) {
|
|
|
1258
1381
|
return lines;
|
|
1259
1382
|
}
|
|
1260
1383
|
export async function commandActors(opts) {
|
|
1261
|
-
const session = await
|
|
1384
|
+
const session = await loadScriptForEdit(opts);
|
|
1262
1385
|
const lines = listAssetsByKind(session.script, "actor", {
|
|
1263
1386
|
id: strOf(opts["id"]).trim(),
|
|
1264
1387
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1267,7 +1390,7 @@ export async function commandActors(opts) {
|
|
|
1267
1390
|
return [buildReport("query.actors", "ACTORS", lines, session.artifactLabel, ["Use `scriptctl rename actor:<id>` / `describe` / `merge` to edit."]), EXIT_OK];
|
|
1268
1391
|
}
|
|
1269
1392
|
export async function commandLocations(opts) {
|
|
1270
|
-
const session = await
|
|
1393
|
+
const session = await loadScriptForEdit(opts);
|
|
1271
1394
|
const lines = listAssetsByKind(session.script, "location", {
|
|
1272
1395
|
id: strOf(opts["id"]).trim(),
|
|
1273
1396
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1276,7 +1399,7 @@ export async function commandLocations(opts) {
|
|
|
1276
1399
|
return [buildReport("query.locations", "LOCATIONS", lines, session.artifactLabel, ["Use `scriptctl rename location:<id>` etc. to edit."]), EXIT_OK];
|
|
1277
1400
|
}
|
|
1278
1401
|
export async function commandProps(opts) {
|
|
1279
|
-
const session = await
|
|
1402
|
+
const session = await loadScriptForEdit(opts);
|
|
1280
1403
|
const lines = listAssetsByKind(session.script, "prop", {
|
|
1281
1404
|
id: strOf(opts["id"]).trim(),
|
|
1282
1405
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1285,7 +1408,7 @@ export async function commandProps(opts) {
|
|
|
1285
1408
|
return [buildReport("query.props", "PROPS", lines, session.artifactLabel, ["Use `scriptctl rename prop:<id>` etc. to edit."]), EXIT_OK];
|
|
1286
1409
|
}
|
|
1287
1410
|
export async function commandAssets(opts) {
|
|
1288
|
-
const session = await
|
|
1411
|
+
const session = await loadScriptForEdit(opts);
|
|
1289
1412
|
const kindFilter = strOf(opts["kind"]).trim();
|
|
1290
1413
|
const nameOpt = strOf(opts["name"]).trim();
|
|
1291
1414
|
const idOpt = strOf(opts["id"]).trim();
|
|
@@ -1300,7 +1423,7 @@ export async function commandAssets(opts) {
|
|
|
1300
1423
|
}
|
|
1301
1424
|
// ----- speakers -------------------------------------------------------------
|
|
1302
1425
|
export async function commandSpeakers(opts) {
|
|
1303
|
-
const session = await
|
|
1426
|
+
const session = await loadScriptForEdit(opts);
|
|
1304
1427
|
const script = session.script;
|
|
1305
1428
|
const idOpt = strOf(opts["id"]).trim();
|
|
1306
1429
|
const nameOpt = strOf(opts["name"]).trim();
|
|
@@ -1323,7 +1446,7 @@ export async function commandSpeakers(opts) {
|
|
|
1323
1446
|
}
|
|
1324
1447
|
// ----- issues ---------------------------------------------------------------
|
|
1325
1448
|
export async function commandIssues(opts) {
|
|
1326
|
-
const session = await
|
|
1449
|
+
const session = await loadScriptForEdit(opts);
|
|
1327
1450
|
const severityFilter = strOf(opts["severity"]).trim();
|
|
1328
1451
|
const codeFilter = strOf(opts["code"]).trim();
|
|
1329
1452
|
const validation = validateSession(session);
|
|
@@ -1345,7 +1468,7 @@ export async function commandIssues(opts) {
|
|
|
1345
1468
|
}
|
|
1346
1469
|
// ----- refs (unified reverse lookup) ----------------------------------------
|
|
1347
1470
|
export async function commandRefs(opts) {
|
|
1348
|
-
const session = await
|
|
1471
|
+
const session = await loadScriptForEdit(opts);
|
|
1349
1472
|
const script = session.script;
|
|
1350
1473
|
const args = asList(opts["_args"]);
|
|
1351
1474
|
const target = strOf(args[0]).trim();
|