@lingjingai/scriptctl 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 { markMetadataConfidenceReviewed, readRunState, summarizeIssues, updateRunState, } from "./direct.js";
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
- 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 ?? "";
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) {
@@ -236,7 +225,11 @@ export async function currentRevisionOrZero(client) {
236
225
  throw exc;
237
226
  }
238
227
  }
239
- async function loadRemoteScript(opts, workspace) {
228
+ // All reads/edits operate on the DB-backed final script. The intermediate
229
+ // script.initial.json is a private artifact between `import init` and
230
+ // `import push`; it is not an editable surface.
231
+ async function loadScript(opts) {
232
+ const workspace = strOf(opts["workspace_path"] || "workspace");
240
233
  const client = scriptOutputClient(opts);
241
234
  let script;
242
235
  let revision;
@@ -276,34 +269,22 @@ async function loadRemoteScript(opts, workspace) {
276
269
  revision: Number((revision ?? {})["revision"] ?? 0),
277
270
  });
278
271
  }
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
272
  function validateSession(session, opts = {}) {
287
- const requireSource = opts.requireSource ?? false;
288
- if (!session.remote) {
289
- return validateScript(session.workspace, session.scriptPath, { requireSource });
290
- }
291
- const tmpPath = path.join(os.tmpdir(), `scriptctl-db-script-${randomUUID()}.json`);
292
- try {
293
- fs.writeFileSync(tmpPath, JSON.stringify(session.script, null, 2) + "\n", "utf-8");
294
- const validation = validateScript(session.workspace, tmpPath, { requireSource });
295
- validation["script_path"] = session.artifactLabel;
273
+ const persist = opts.persist ?? true;
274
+ // Validate the in-memory DB script directly (no tmp file). scriptData also
275
+ // tells validateScript not to read the local asset_metadata.json/episode_plan.json
276
+ // parse artifacts — those describe the local initial.json, not the DB script.
277
+ // requireSource:false (DB validation never needs source.txt). persist:false so
278
+ // validateScript doesn't write; we own the write below to set the nicer label.
279
+ const validation = validateScript(session.workspace, null, {
280
+ requireSource: false,
281
+ scriptData: session.script,
282
+ persist: false,
283
+ });
284
+ validation["script_path"] = session.artifactLabel;
285
+ if (persist)
296
286
  writeJson(path.join(directDir(session.workspace), "validation.json"), validation);
297
- return validation;
298
- }
299
- finally {
300
- try {
301
- fs.unlinkSync(tmpPath);
302
- }
303
- catch {
304
- // ignore
305
- }
306
- }
287
+ return validation;
307
288
  }
308
289
  function validationIssuePath(issue) {
309
290
  if (issue["path"])
@@ -393,12 +374,8 @@ function requestIdForScriptWrite(opts, op, payload) {
393
374
  return `scriptctl:${op}:${sha256Text(canonical)}`;
394
375
  }
395
376
  async function saveScriptSession(session, opts, op) {
396
- if (!session.remote) {
397
- writeJson(session.scriptPath, session.script);
398
- return [null, false];
399
- }
400
377
  if (session.client === null)
401
- throw new Error("remote script session missing client");
378
+ throw new Error("script session missing client");
402
379
  const baseRevision = Number(session.revision ?? 0);
403
380
  const requestId = requestIdForScriptWrite(opts, op, { script: session.script });
404
381
  let res;
@@ -460,7 +437,7 @@ function patchOperationsFromPayload(payload) {
460
437
  // ---------------------------------------------------------------------------
461
438
  export async function commandScriptValidate(opts) {
462
439
  const workspace = strOf(opts["workspace_path"] || "workspace");
463
- const session = await loadScriptForEdit(opts);
440
+ const session = await loadScript(opts);
464
441
  const validation = validateSession(session);
465
442
  await syncValidationResult(session, validation);
466
443
  const stats = validation["stats"] ?? {};
@@ -590,13 +567,13 @@ export async function commandScriptPatch(opts) {
590
567
  }
591
568
  const operations = patchOperationsFromPayload(payload);
592
569
  const dryRun = Boolean(opts["dry_run"]);
593
- const session = await loadScriptForEdit(opts);
570
+ const session = await loadScript(opts);
594
571
  const script = session.script;
595
572
  const applied = applyPatchOperations(script, scriptSourceText(workspace), operations);
596
573
  if (dryRun) {
597
574
  // Validate the in-memory mutated script WITHOUT calling saveScriptSession.
598
575
  // Run validate but skip syncValidationResult (which writes to remote) too.
599
- const validation = validateSession(session);
576
+ const validation = validateSession(session, { persist: false });
600
577
  const passed = Boolean(validation["passed"]);
601
578
  const report = {
602
579
  title: passed ? "SCRIPT PATCH DRY-RUN PASSED" : "SCRIPT PATCH DRY-RUN: Validation needs repair",
@@ -618,9 +595,6 @@ export async function commandScriptPatch(opts) {
618
595
  const [newRevision, idempotent] = await saveScriptSession(session, opts, applied.length === 1 ? applied[0] : "script.patch");
619
596
  const validation = validateSession(session);
620
597
  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
598
  const passed = Boolean(validation["passed"]);
625
599
  const resultLines = [
626
600
  `operations: ${applied.length}`,
@@ -648,7 +622,7 @@ export async function commandScriptPatch(opts) {
648
622
  // ---------------------------------------------------------------------------
649
623
  async function applySingleScriptOp(opts, op) {
650
624
  const workspace = strOf(opts["workspace_path"] || "workspace");
651
- const session = await loadScriptForEdit(opts);
625
+ const session = await loadScript(opts);
652
626
  const script = session.script;
653
627
  const applied = applyPatchOperations(script, scriptSourceText(workspace), [op]);
654
628
  const opName = applied[0] ?? strOf(op["op"]);
@@ -690,7 +664,7 @@ function summarizeScriptOp(_script, op) {
690
664
  return `已执行 ${kind}`;
691
665
  }
692
666
  async function commandStateRefs(opts, plan = false) {
693
- const session = await loadScriptForEdit(opts);
667
+ const session = await loadScript(opts);
694
668
  const script = session.script;
695
669
  const args = asList(opts["_args"]);
696
670
  const [targetKind, targetId, stateId] = parseStateTarget(args[0] ?? "");
@@ -744,7 +718,10 @@ export async function commandPush(opts) {
744
718
  }
745
719
  const state = readRunState(workspace);
746
720
  const provider = strOf(state["provider"]);
747
- if (provider === "mock" && process.env.SCRIPTCTL_ALLOW_MOCK_EXPORT !== "1") {
721
+ // SCRIPTCTL_ALLOW_MOCK_PUSH is the current name; SCRIPTCTL_ALLOW_MOCK_EXPORT is
722
+ // still honored for back-compat with environments configured before the rename.
723
+ const allowMock = process.env.SCRIPTCTL_ALLOW_MOCK_PUSH === "1" || process.env.SCRIPTCTL_ALLOW_MOCK_EXPORT === "1";
724
+ if (provider === "mock" && !allowMock) {
748
725
  const report = {
749
726
  title: "PUSH BLOCKED: Mock provider result",
750
727
  result: ["script.initial.json was produced by --provider mock and was not pushed."],
@@ -776,15 +753,13 @@ export async function commandPush(opts) {
776
753
  });
777
754
  }
778
755
  // Validation is advisory, not a gate: push always publishes so the agent can
779
- // self-review and repair on the DB script afterwards. Results are still
780
- // computed and synced so the review subagent has the issue list to act on.
781
- const validation = validateScript(workspace, scriptPath);
756
+ // self-review and repair on the DB script afterwards. requireSource:false so a
757
+ // missing source.txt never aborts the publish. Results are still computed and
758
+ // synced so the review subagent has the issue list to act on.
759
+ const validation = validateScript(workspace, scriptPath, { requireSource: false });
782
760
  const client = scriptOutputClient(opts);
783
761
  const baseRevision = await currentRevisionOrZero(client);
784
- // Sorted-keys serialization mirrors Python json.dumps(sort_keys=True, separators=(",",":"))
785
- const sortedScript = sortDeep(script);
786
- const scriptHash = sha256Text(JSON.stringify(sortedScript));
787
- const requestId = strOf(opts["request_id"]).trim() || `scriptctl-import-push:${scriptHash}`;
762
+ const requestId = requestIdForScriptWrite(opts, "import.push", { script });
788
763
  let replaceRes;
789
764
  try {
790
765
  replaceRes = await client.replaceScript({
@@ -971,7 +946,7 @@ function buildReport(op, title, lines, artifactLabel, next) {
971
946
  }
972
947
  // ----- summary --------------------------------------------------------------
973
948
  export async function commandSummary(opts) {
974
- const session = await loadScriptForEdit(opts);
949
+ const session = await loadScript(opts);
975
950
  const script = session.script;
976
951
  const episodes = asList(script["episodes"]);
977
952
  const scenes = [];
@@ -994,7 +969,7 @@ export async function commandSummary(opts) {
994
969
  }
995
970
  // ----- episodes -------------------------------------------------------------
996
971
  export async function commandEpisodes(opts) {
997
- const session = await loadScriptForEdit(opts);
972
+ const session = await loadScript(opts);
998
973
  const script = session.script;
999
974
  const itemId = strOf(opts["id"]).trim();
1000
975
  const minChars = parseBound(opts["min_chars"]);
@@ -1048,7 +1023,7 @@ function formatScene(epId, scene, names) {
1048
1023
  return `${epId}/${scene["scene_id"]} [${space} ${time}] location=${locations.join(",") || "-"} actors=${actors.join(",") || "-"} props=${props.join(",") || "-"} actions=${actionCount}`;
1049
1024
  }
1050
1025
  export async function commandScenes(opts) {
1051
- const session = await loadScriptForEdit(opts);
1026
+ const session = await loadScript(opts);
1052
1027
  const script = session.script;
1053
1028
  const inFilter = parseInFilter(strOf(opts["in"]));
1054
1029
  const hasActor = strOf(opts["has_actor"]).trim();
@@ -1086,7 +1061,7 @@ export async function commandScenes(opts) {
1086
1061
  }
1087
1062
  // ----- actions --------------------------------------------------------------
1088
1063
  export async function commandActions(opts) {
1089
- const session = await loadScriptForEdit(opts);
1064
+ const session = await loadScript(opts);
1090
1065
  const script = session.script;
1091
1066
  const inFilter = parseInFilter(strOf(opts["in"]));
1092
1067
  const grep = strOf(opts["grep"]);
@@ -1271,7 +1246,7 @@ function listAssetsByKind(script, kind, opts) {
1271
1246
  return lines;
1272
1247
  }
1273
1248
  export async function commandActors(opts) {
1274
- const session = await loadScriptForEdit(opts);
1249
+ const session = await loadScript(opts);
1275
1250
  const lines = listAssetsByKind(session.script, "actor", {
1276
1251
  id: strOf(opts["id"]).trim(),
1277
1252
  name: strOf(opts["name"]).trim(),
@@ -1280,7 +1255,7 @@ export async function commandActors(opts) {
1280
1255
  return [buildReport("query.actors", "ACTORS", lines, session.artifactLabel, ["Use `scriptctl rename actor:<id>` / `describe` / `merge` to edit."]), EXIT_OK];
1281
1256
  }
1282
1257
  export async function commandLocations(opts) {
1283
- const session = await loadScriptForEdit(opts);
1258
+ const session = await loadScript(opts);
1284
1259
  const lines = listAssetsByKind(session.script, "location", {
1285
1260
  id: strOf(opts["id"]).trim(),
1286
1261
  name: strOf(opts["name"]).trim(),
@@ -1289,7 +1264,7 @@ export async function commandLocations(opts) {
1289
1264
  return [buildReport("query.locations", "LOCATIONS", lines, session.artifactLabel, ["Use `scriptctl rename location:<id>` etc. to edit."]), EXIT_OK];
1290
1265
  }
1291
1266
  export async function commandProps(opts) {
1292
- const session = await loadScriptForEdit(opts);
1267
+ const session = await loadScript(opts);
1293
1268
  const lines = listAssetsByKind(session.script, "prop", {
1294
1269
  id: strOf(opts["id"]).trim(),
1295
1270
  name: strOf(opts["name"]).trim(),
@@ -1298,7 +1273,7 @@ export async function commandProps(opts) {
1298
1273
  return [buildReport("query.props", "PROPS", lines, session.artifactLabel, ["Use `scriptctl rename prop:<id>` etc. to edit."]), EXIT_OK];
1299
1274
  }
1300
1275
  export async function commandAssets(opts) {
1301
- const session = await loadScriptForEdit(opts);
1276
+ const session = await loadScript(opts);
1302
1277
  const kindFilter = strOf(opts["kind"]).trim();
1303
1278
  const nameOpt = strOf(opts["name"]).trim();
1304
1279
  const idOpt = strOf(opts["id"]).trim();
@@ -1313,7 +1288,7 @@ export async function commandAssets(opts) {
1313
1288
  }
1314
1289
  // ----- speakers -------------------------------------------------------------
1315
1290
  export async function commandSpeakers(opts) {
1316
- const session = await loadScriptForEdit(opts);
1291
+ const session = await loadScript(opts);
1317
1292
  const script = session.script;
1318
1293
  const idOpt = strOf(opts["id"]).trim();
1319
1294
  const nameOpt = strOf(opts["name"]).trim();
@@ -1336,7 +1311,7 @@ export async function commandSpeakers(opts) {
1336
1311
  }
1337
1312
  // ----- issues ---------------------------------------------------------------
1338
1313
  export async function commandIssues(opts) {
1339
- const session = await loadScriptForEdit(opts);
1314
+ const session = await loadScript(opts);
1340
1315
  const severityFilter = strOf(opts["severity"]).trim();
1341
1316
  const codeFilter = strOf(opts["code"]).trim();
1342
1317
  const validation = validateSession(session);
@@ -1358,7 +1333,7 @@ export async function commandIssues(opts) {
1358
1333
  }
1359
1334
  // ----- refs (unified reverse lookup) ----------------------------------------
1360
1335
  export async function commandRefs(opts) {
1361
- const session = await loadScriptForEdit(opts);
1336
+ const session = await loadScript(opts);
1362
1337
  const script = session.script;
1363
1338
  const args = asList(opts["_args"]);
1364
1339
  const target = strOf(args[0]).trim();