@lingjingai/scriptctl 0.10.0 → 0.10.2
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 +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +0 -1
- package/dist/common.js +0 -1
- package/dist/common.js.map +1 -1
- package/dist/domain/script-core.d.ts +1 -0
- package/dist/domain/script-core.js +20 -5
- package/dist/domain/script-core.js.map +1 -1
- package/dist/help-text.js +1 -2
- package/dist/help-text.js.map +1 -1
- package/dist/infra/script-output-api.js +21 -10
- package/dist/infra/script-output-api.js.map +1 -1
- package/dist/usecases/direct.d.ts +0 -1
- package/dist/usecases/direct.js +3 -24
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/episode.js +6 -3
- package/dist/usecases/episode.js.map +1 -1
- package/dist/usecases/script.d.ts +0 -2
- package/dist/usecases/script.js +63 -76
- package/dist/usecases/script.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,14 +4,12 @@ type Dict = Record<string, unknown>;
|
|
|
4
4
|
export declare class ScriptEditSession {
|
|
5
5
|
workspace: string;
|
|
6
6
|
script: Dict;
|
|
7
|
-
scriptPath: string | null;
|
|
8
7
|
client: ScriptOutputStore | null;
|
|
9
8
|
projectGroupNo: string | null;
|
|
10
9
|
revision: number | null;
|
|
11
10
|
constructor(opts: {
|
|
12
11
|
workspace: string;
|
|
13
12
|
script: Dict;
|
|
14
|
-
scriptPath?: string | null;
|
|
15
13
|
client?: ScriptOutputStore | null;
|
|
16
14
|
projectGroupNo?: string | null;
|
|
17
15
|
revision?: number | null;
|
package/dist/usecases/script.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
1
|
import * as path from "node:path";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
2
|
import { CliError, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE, directDir, exists, readJson, readText, sha256Text, writeJson, } from "../common.js";
|
|
6
3
|
import { applyPatchOperations, collectAssetRefs, collectStateRefs, parseAnyAddress, parseStateTarget, validateScript, PATCH_OP_SCHEMA, } from "../domain/script-core.js";
|
|
7
4
|
import { RemoteScriptOutputStore } from "../infra/script-output-api.js";
|
|
8
5
|
import { LocalScriptOutputStore } from "../infra/local-script-output-store.js";
|
|
9
6
|
import { ScriptOutputApiError, resolveOutputMode, } from "../infra/script-output-store.js";
|
|
10
|
-
import {
|
|
7
|
+
import { readRunState, summarizeIssues, updateRunState, } from "./direct.js";
|
|
11
8
|
function strOf(v) {
|
|
12
9
|
if (v === null || v === undefined)
|
|
13
10
|
return "";
|
|
@@ -65,14 +62,12 @@ function episodeCharCounts(scenes) {
|
|
|
65
62
|
export class ScriptEditSession {
|
|
66
63
|
workspace;
|
|
67
64
|
script;
|
|
68
|
-
scriptPath;
|
|
69
65
|
client;
|
|
70
66
|
projectGroupNo;
|
|
71
67
|
revision;
|
|
72
68
|
constructor(opts) {
|
|
73
69
|
this.workspace = opts.workspace;
|
|
74
70
|
this.script = opts.script;
|
|
75
|
-
this.scriptPath = opts.scriptPath ?? null;
|
|
76
71
|
this.client = opts.client ?? null;
|
|
77
72
|
this.projectGroupNo = opts.projectGroupNo ?? null;
|
|
78
73
|
this.revision = opts.revision ?? null;
|
|
@@ -81,16 +76,10 @@ export class ScriptEditSession {
|
|
|
81
76
|
return this.client !== null;
|
|
82
77
|
}
|
|
83
78
|
get artifactLabel() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 ?? "";
|
|
79
|
+
// Public label — keeps the project id + revision (not secret; the user
|
|
80
|
+
// supplies the project id via --project-group-no/env) so two project groups
|
|
81
|
+
// at the same revision stay distinguishable in report.artifacts.
|
|
82
|
+
return `script-output/${this.projectGroupNo}@revision/${this.revision ?? 0}`;
|
|
94
83
|
}
|
|
95
84
|
}
|
|
96
85
|
export function scriptOutputClient(opts) {
|
|
@@ -231,12 +220,28 @@ export async function currentRevisionOrZero(client) {
|
|
|
231
220
|
}
|
|
232
221
|
catch (exc) {
|
|
233
222
|
if (exc instanceof ScriptOutputApiError) {
|
|
234
|
-
|
|
223
|
+
// WORKAROUND (see TODO.md "getRevision not-found signal"): the script-output
|
|
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;
|
|
235
236
|
}
|
|
236
237
|
throw exc;
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
|
-
|
|
240
|
+
// All reads/edits operate on the DB-backed final script. The intermediate
|
|
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");
|
|
240
245
|
const client = scriptOutputClient(opts);
|
|
241
246
|
let script;
|
|
242
247
|
let revision;
|
|
@@ -276,34 +281,22 @@ async function loadRemoteScript(opts, workspace) {
|
|
|
276
281
|
revision: Number((revision ?? {})["revision"] ?? 0),
|
|
277
282
|
});
|
|
278
283
|
}
|
|
279
|
-
async function loadScriptForEdit(opts) {
|
|
280
|
-
// All reads/edits operate on the DB-backed final script. The intermediate
|
|
281
|
-
// script.initial.json is a private artifact between `import init` and
|
|
282
|
-
// `import push`; it is no longer an editable surface.
|
|
283
|
-
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
284
|
-
return loadRemoteScript(opts, workspace);
|
|
285
|
-
}
|
|
286
284
|
function validateSession(session, opts = {}) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
285
|
+
const persist = opts.persist ?? true;
|
|
286
|
+
// Validate the in-memory DB script directly (no tmp file). scriptData also
|
|
287
|
+
// tells validateScript not to read the local asset_metadata.json/episode_plan.json
|
|
288
|
+
// parse artifacts — those describe the local initial.json, not the DB script.
|
|
289
|
+
// requireSource:false (DB validation never needs source.txt). persist:false so
|
|
290
|
+
// validateScript doesn't write; we own the write below to set the nicer label.
|
|
291
|
+
const validation = validateScript(session.workspace, null, {
|
|
292
|
+
requireSource: false,
|
|
293
|
+
scriptData: session.script,
|
|
294
|
+
persist: false,
|
|
295
|
+
});
|
|
296
|
+
validation["script_path"] = session.artifactLabel;
|
|
297
|
+
if (persist)
|
|
296
298
|
writeJson(path.join(directDir(session.workspace), "validation.json"), validation);
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
finally {
|
|
300
|
-
try {
|
|
301
|
-
fs.unlinkSync(tmpPath);
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
// ignore
|
|
305
|
-
}
|
|
306
|
-
}
|
|
299
|
+
return validation;
|
|
307
300
|
}
|
|
308
301
|
function validationIssuePath(issue) {
|
|
309
302
|
if (issue["path"])
|
|
@@ -393,12 +386,8 @@ function requestIdForScriptWrite(opts, op, payload) {
|
|
|
393
386
|
return `scriptctl:${op}:${sha256Text(canonical)}`;
|
|
394
387
|
}
|
|
395
388
|
async function saveScriptSession(session, opts, op) {
|
|
396
|
-
if (!session.remote) {
|
|
397
|
-
writeJson(session.scriptPath, session.script);
|
|
398
|
-
return [null, false];
|
|
399
|
-
}
|
|
400
389
|
if (session.client === null)
|
|
401
|
-
throw new Error("
|
|
390
|
+
throw new Error("script session missing client");
|
|
402
391
|
const baseRevision = Number(session.revision ?? 0);
|
|
403
392
|
const requestId = requestIdForScriptWrite(opts, op, { script: session.script });
|
|
404
393
|
let res;
|
|
@@ -460,7 +449,7 @@ function patchOperationsFromPayload(payload) {
|
|
|
460
449
|
// ---------------------------------------------------------------------------
|
|
461
450
|
export async function commandScriptValidate(opts) {
|
|
462
451
|
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
463
|
-
const session = await
|
|
452
|
+
const session = await loadScript(opts);
|
|
464
453
|
const validation = validateSession(session);
|
|
465
454
|
await syncValidationResult(session, validation);
|
|
466
455
|
const stats = validation["stats"] ?? {};
|
|
@@ -590,13 +579,13 @@ export async function commandScriptPatch(opts) {
|
|
|
590
579
|
}
|
|
591
580
|
const operations = patchOperationsFromPayload(payload);
|
|
592
581
|
const dryRun = Boolean(opts["dry_run"]);
|
|
593
|
-
const session = await
|
|
582
|
+
const session = await loadScript(opts);
|
|
594
583
|
const script = session.script;
|
|
595
584
|
const applied = applyPatchOperations(script, scriptSourceText(workspace), operations);
|
|
596
585
|
if (dryRun) {
|
|
597
586
|
// Validate the in-memory mutated script WITHOUT calling saveScriptSession.
|
|
598
587
|
// Run validate but skip syncValidationResult (which writes to remote) too.
|
|
599
|
-
const validation = validateSession(session);
|
|
588
|
+
const validation = validateSession(session, { persist: false });
|
|
600
589
|
const passed = Boolean(validation["passed"]);
|
|
601
590
|
const report = {
|
|
602
591
|
title: passed ? "SCRIPT PATCH DRY-RUN PASSED" : "SCRIPT PATCH DRY-RUN: Validation needs repair",
|
|
@@ -618,9 +607,6 @@ export async function commandScriptPatch(opts) {
|
|
|
618
607
|
const [newRevision, idempotent] = await saveScriptSession(session, opts, applied.length === 1 ? applied[0] : "script.patch");
|
|
619
608
|
const validation = validateSession(session);
|
|
620
609
|
await syncValidationResult(session, validation, newRevision);
|
|
621
|
-
// Patching worldview/role/description clears the parse-time
|
|
622
|
-
// LOW_CONFIDENCE_METADATA advisory (asset_metadata.json low → medium).
|
|
623
|
-
markMetadataConfidenceReviewed(workspace, operations);
|
|
624
610
|
const passed = Boolean(validation["passed"]);
|
|
625
611
|
const resultLines = [
|
|
626
612
|
`operations: ${applied.length}`,
|
|
@@ -648,7 +634,7 @@ export async function commandScriptPatch(opts) {
|
|
|
648
634
|
// ---------------------------------------------------------------------------
|
|
649
635
|
async function applySingleScriptOp(opts, op) {
|
|
650
636
|
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
651
|
-
const session = await
|
|
637
|
+
const session = await loadScript(opts);
|
|
652
638
|
const script = session.script;
|
|
653
639
|
const applied = applyPatchOperations(script, scriptSourceText(workspace), [op]);
|
|
654
640
|
const opName = applied[0] ?? strOf(op["op"]);
|
|
@@ -690,7 +676,7 @@ function summarizeScriptOp(_script, op) {
|
|
|
690
676
|
return `已执行 ${kind}`;
|
|
691
677
|
}
|
|
692
678
|
async function commandStateRefs(opts, plan = false) {
|
|
693
|
-
const session = await
|
|
679
|
+
const session = await loadScript(opts);
|
|
694
680
|
const script = session.script;
|
|
695
681
|
const args = asList(opts["_args"]);
|
|
696
682
|
const [targetKind, targetId, stateId] = parseStateTarget(args[0] ?? "");
|
|
@@ -744,7 +730,10 @@ export async function commandPush(opts) {
|
|
|
744
730
|
}
|
|
745
731
|
const state = readRunState(workspace);
|
|
746
732
|
const provider = strOf(state["provider"]);
|
|
747
|
-
|
|
733
|
+
// SCRIPTCTL_ALLOW_MOCK_PUSH is the current name; SCRIPTCTL_ALLOW_MOCK_EXPORT is
|
|
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) {
|
|
748
737
|
const report = {
|
|
749
738
|
title: "PUSH BLOCKED: Mock provider result",
|
|
750
739
|
result: ["script.initial.json was produced by --provider mock and was not pushed."],
|
|
@@ -776,15 +765,13 @@ export async function commandPush(opts) {
|
|
|
776
765
|
});
|
|
777
766
|
}
|
|
778
767
|
// Validation is advisory, not a gate: push always publishes so the agent can
|
|
779
|
-
// self-review and repair on the DB script afterwards.
|
|
780
|
-
//
|
|
781
|
-
|
|
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 });
|
|
782
772
|
const client = scriptOutputClient(opts);
|
|
783
773
|
const baseRevision = await currentRevisionOrZero(client);
|
|
784
|
-
|
|
785
|
-
const sortedScript = sortDeep(script);
|
|
786
|
-
const scriptHash = sha256Text(JSON.stringify(sortedScript));
|
|
787
|
-
const requestId = strOf(opts["request_id"]).trim() || `scriptctl-import-push:${scriptHash}`;
|
|
774
|
+
const requestId = requestIdForScriptWrite(opts, "import.push", { script });
|
|
788
775
|
let replaceRes;
|
|
789
776
|
try {
|
|
790
777
|
replaceRes = await client.replaceScript({
|
|
@@ -971,7 +958,7 @@ function buildReport(op, title, lines, artifactLabel, next) {
|
|
|
971
958
|
}
|
|
972
959
|
// ----- summary --------------------------------------------------------------
|
|
973
960
|
export async function commandSummary(opts) {
|
|
974
|
-
const session = await
|
|
961
|
+
const session = await loadScript(opts);
|
|
975
962
|
const script = session.script;
|
|
976
963
|
const episodes = asList(script["episodes"]);
|
|
977
964
|
const scenes = [];
|
|
@@ -994,7 +981,7 @@ export async function commandSummary(opts) {
|
|
|
994
981
|
}
|
|
995
982
|
// ----- episodes -------------------------------------------------------------
|
|
996
983
|
export async function commandEpisodes(opts) {
|
|
997
|
-
const session = await
|
|
984
|
+
const session = await loadScript(opts);
|
|
998
985
|
const script = session.script;
|
|
999
986
|
const itemId = strOf(opts["id"]).trim();
|
|
1000
987
|
const minChars = parseBound(opts["min_chars"]);
|
|
@@ -1048,7 +1035,7 @@ function formatScene(epId, scene, names) {
|
|
|
1048
1035
|
return `${epId}/${scene["scene_id"]} [${space} ${time}] location=${locations.join(",") || "-"} actors=${actors.join(",") || "-"} props=${props.join(",") || "-"} actions=${actionCount}`;
|
|
1049
1036
|
}
|
|
1050
1037
|
export async function commandScenes(opts) {
|
|
1051
|
-
const session = await
|
|
1038
|
+
const session = await loadScript(opts);
|
|
1052
1039
|
const script = session.script;
|
|
1053
1040
|
const inFilter = parseInFilter(strOf(opts["in"]));
|
|
1054
1041
|
const hasActor = strOf(opts["has_actor"]).trim();
|
|
@@ -1086,7 +1073,7 @@ export async function commandScenes(opts) {
|
|
|
1086
1073
|
}
|
|
1087
1074
|
// ----- actions --------------------------------------------------------------
|
|
1088
1075
|
export async function commandActions(opts) {
|
|
1089
|
-
const session = await
|
|
1076
|
+
const session = await loadScript(opts);
|
|
1090
1077
|
const script = session.script;
|
|
1091
1078
|
const inFilter = parseInFilter(strOf(opts["in"]));
|
|
1092
1079
|
const grep = strOf(opts["grep"]);
|
|
@@ -1271,7 +1258,7 @@ function listAssetsByKind(script, kind, opts) {
|
|
|
1271
1258
|
return lines;
|
|
1272
1259
|
}
|
|
1273
1260
|
export async function commandActors(opts) {
|
|
1274
|
-
const session = await
|
|
1261
|
+
const session = await loadScript(opts);
|
|
1275
1262
|
const lines = listAssetsByKind(session.script, "actor", {
|
|
1276
1263
|
id: strOf(opts["id"]).trim(),
|
|
1277
1264
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1280,7 +1267,7 @@ export async function commandActors(opts) {
|
|
|
1280
1267
|
return [buildReport("query.actors", "ACTORS", lines, session.artifactLabel, ["Use `scriptctl rename actor:<id>` / `describe` / `merge` to edit."]), EXIT_OK];
|
|
1281
1268
|
}
|
|
1282
1269
|
export async function commandLocations(opts) {
|
|
1283
|
-
const session = await
|
|
1270
|
+
const session = await loadScript(opts);
|
|
1284
1271
|
const lines = listAssetsByKind(session.script, "location", {
|
|
1285
1272
|
id: strOf(opts["id"]).trim(),
|
|
1286
1273
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1289,7 +1276,7 @@ export async function commandLocations(opts) {
|
|
|
1289
1276
|
return [buildReport("query.locations", "LOCATIONS", lines, session.artifactLabel, ["Use `scriptctl rename location:<id>` etc. to edit."]), EXIT_OK];
|
|
1290
1277
|
}
|
|
1291
1278
|
export async function commandProps(opts) {
|
|
1292
|
-
const session = await
|
|
1279
|
+
const session = await loadScript(opts);
|
|
1293
1280
|
const lines = listAssetsByKind(session.script, "prop", {
|
|
1294
1281
|
id: strOf(opts["id"]).trim(),
|
|
1295
1282
|
name: strOf(opts["name"]).trim(),
|
|
@@ -1298,7 +1285,7 @@ export async function commandProps(opts) {
|
|
|
1298
1285
|
return [buildReport("query.props", "PROPS", lines, session.artifactLabel, ["Use `scriptctl rename prop:<id>` etc. to edit."]), EXIT_OK];
|
|
1299
1286
|
}
|
|
1300
1287
|
export async function commandAssets(opts) {
|
|
1301
|
-
const session = await
|
|
1288
|
+
const session = await loadScript(opts);
|
|
1302
1289
|
const kindFilter = strOf(opts["kind"]).trim();
|
|
1303
1290
|
const nameOpt = strOf(opts["name"]).trim();
|
|
1304
1291
|
const idOpt = strOf(opts["id"]).trim();
|
|
@@ -1313,7 +1300,7 @@ export async function commandAssets(opts) {
|
|
|
1313
1300
|
}
|
|
1314
1301
|
// ----- speakers -------------------------------------------------------------
|
|
1315
1302
|
export async function commandSpeakers(opts) {
|
|
1316
|
-
const session = await
|
|
1303
|
+
const session = await loadScript(opts);
|
|
1317
1304
|
const script = session.script;
|
|
1318
1305
|
const idOpt = strOf(opts["id"]).trim();
|
|
1319
1306
|
const nameOpt = strOf(opts["name"]).trim();
|
|
@@ -1336,7 +1323,7 @@ export async function commandSpeakers(opts) {
|
|
|
1336
1323
|
}
|
|
1337
1324
|
// ----- issues ---------------------------------------------------------------
|
|
1338
1325
|
export async function commandIssues(opts) {
|
|
1339
|
-
const session = await
|
|
1326
|
+
const session = await loadScript(opts);
|
|
1340
1327
|
const severityFilter = strOf(opts["severity"]).trim();
|
|
1341
1328
|
const codeFilter = strOf(opts["code"]).trim();
|
|
1342
1329
|
const validation = validateSession(session);
|
|
@@ -1358,7 +1345,7 @@ export async function commandIssues(opts) {
|
|
|
1358
1345
|
}
|
|
1359
1346
|
// ----- refs (unified reverse lookup) ----------------------------------------
|
|
1360
1347
|
export async function commandRefs(opts) {
|
|
1361
|
-
const session = await
|
|
1348
|
+
const session = await loadScript(opts);
|
|
1362
1349
|
const script = session.script;
|
|
1363
1350
|
const args = asList(opts["_args"]);
|
|
1364
1351
|
const target = strOf(args[0]).trim();
|