@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.
@@ -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;
@@ -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) {
@@ -231,12 +220,28 @@ export async function currentRevisionOrZero(client) {
231
220
  }
232
221
  catch (exc) {
233
222
  if (exc instanceof ScriptOutputApiError) {
234
- throw apiErrorToCli("SCRIPT API BLOCKED: Revision query failed", exc);
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
- async function loadRemoteScript(opts, workspace) {
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 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;
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
- return validation;
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("remote script session missing client");
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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
- if (provider === "mock" && process.env.SCRIPTCTL_ALLOW_MOCK_EXPORT !== "1") {
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. Results are still
780
- // computed and synced so the review subagent has the issue list to act on.
781
- const validation = validateScript(workspace, scriptPath);
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
- // 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}`;
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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 loadScriptForEdit(opts);
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();