@lingjingai/scriptctl 0.20.0 → 0.22.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.
Files changed (208) hide show
  1. package/dist/cli.js +105 -11
  2. package/dist/cli.js.map +1 -1
  3. package/dist/common.d.ts +1 -0
  4. package/dist/common.js.map +1 -1
  5. package/dist/domain/script/address.d.ts +30 -0
  6. package/dist/domain/script/address.js +141 -0
  7. package/dist/domain/script/address.js.map +1 -0
  8. package/dist/domain/script/lookups.d.ts +33 -0
  9. package/dist/domain/script/lookups.js +309 -0
  10. package/dist/domain/script/lookups.js.map +1 -0
  11. package/dist/domain/script/patch/apply.d.ts +4 -0
  12. package/dist/domain/script/patch/apply.js +38 -0
  13. package/dist/domain/script/patch/apply.js.map +1 -0
  14. package/dist/domain/script/patch/helpers.d.ts +11 -0
  15. package/dist/domain/script/patch/helpers.js +141 -0
  16. package/dist/domain/script/patch/helpers.js.map +1 -0
  17. package/dist/domain/script/patch/ops-action.d.ts +12 -0
  18. package/dist/domain/script/patch/ops-action.js +371 -0
  19. package/dist/domain/script/patch/ops-action.js.map +1 -0
  20. package/dist/domain/script/patch/ops-asset.d.ts +13 -0
  21. package/dist/domain/script/patch/ops-asset.js +363 -0
  22. package/dist/domain/script/patch/ops-asset.js.map +1 -0
  23. package/dist/domain/script/patch/ops-dialogue.d.ts +5 -0
  24. package/dist/domain/script/patch/ops-dialogue.js +233 -0
  25. package/dist/domain/script/patch/ops-dialogue.js.map +1 -0
  26. package/dist/domain/script/patch/ops-meta.d.ts +3 -0
  27. package/dist/domain/script/patch/ops-meta.js +47 -0
  28. package/dist/domain/script/patch/ops-meta.js.map +1 -0
  29. package/dist/domain/script/patch/ops-scene.d.ts +6 -0
  30. package/dist/domain/script/patch/ops-scene.js +345 -0
  31. package/dist/domain/script/patch/ops-scene.js.map +1 -0
  32. package/dist/domain/script/patch/ops-state.d.ts +8 -0
  33. package/dist/domain/script/patch/ops-state.js +146 -0
  34. package/dist/domain/script/patch/ops-state.js.map +1 -0
  35. package/dist/domain/script/patch/registry.d.ts +2 -0
  36. package/dist/domain/script/patch/registry.js +50 -0
  37. package/dist/domain/script/patch/registry.js.map +1 -0
  38. package/dist/domain/script/patch/types.d.ts +10 -0
  39. package/dist/domain/script/patch/types.js +2 -0
  40. package/dist/domain/script/patch/types.js.map +1 -0
  41. package/dist/domain/script/refs.d.ts +4 -0
  42. package/dist/domain/script/refs.js +152 -0
  43. package/dist/domain/script/refs.js.map +1 -0
  44. package/dist/domain/script/schema.d.ts +6 -0
  45. package/dist/domain/script/schema.js +53 -0
  46. package/dist/domain/script/schema.js.map +1 -0
  47. package/dist/domain/script/selector.d.ts +45 -0
  48. package/dist/domain/script/selector.js +187 -0
  49. package/dist/domain/script/selector.js.map +1 -0
  50. package/dist/domain/script/shared.d.ts +15 -0
  51. package/dist/domain/script/shared.js +37 -0
  52. package/dist/domain/script/shared.js.map +1 -0
  53. package/dist/domain/script/validate.d.ts +22 -0
  54. package/dist/domain/script/validate.js +844 -0
  55. package/dist/domain/script/validate.js.map +1 -0
  56. package/dist/help-text.js +33 -8
  57. package/dist/help-text.js.map +1 -1
  58. package/dist/infra/script-output-store.d.ts +46 -10
  59. package/dist/infra/script-output-store.js +83 -11
  60. package/dist/infra/script-output-store.js.map +1 -1
  61. package/dist/output.d.ts +1 -1
  62. package/dist/output.js +11 -1
  63. package/dist/output.js.map +1 -1
  64. package/dist/usecases/direct.js +1 -1
  65. package/dist/usecases/direct.js.map +1 -1
  66. package/dist/usecases/doctor.js +32 -22
  67. package/dist/usecases/doctor.js.map +1 -1
  68. package/dist/usecases/parse.js +10 -3
  69. package/dist/usecases/parse.js.map +1 -1
  70. package/dist/usecases/script/actions.d.ts +3 -0
  71. package/dist/usecases/script/actions.js +71 -0
  72. package/dist/usecases/script/actions.js.map +1 -0
  73. package/dist/usecases/script/actor.d.ts +3 -0
  74. package/dist/usecases/script/actor.js +27 -0
  75. package/dist/usecases/script/actor.js.map +1 -0
  76. package/dist/usecases/script/actors.d.ts +3 -0
  77. package/dist/usecases/script/actors.js +15 -0
  78. package/dist/usecases/script/actors.js.map +1 -0
  79. package/dist/usecases/script/add-actor.d.ts +3 -0
  80. package/dist/usecases/script/add-actor.js +13 -0
  81. package/dist/usecases/script/add-actor.js.map +1 -0
  82. package/dist/usecases/script/add-episode.d.ts +3 -0
  83. package/dist/usecases/script/add-episode.js +9 -0
  84. package/dist/usecases/script/add-episode.js.map +1 -0
  85. package/dist/usecases/script/add-location.d.ts +3 -0
  86. package/dist/usecases/script/add-location.js +11 -0
  87. package/dist/usecases/script/add-location.js.map +1 -0
  88. package/dist/usecases/script/add-prop.d.ts +3 -0
  89. package/dist/usecases/script/add-prop.js +11 -0
  90. package/dist/usecases/script/add-prop.js.map +1 -0
  91. package/dist/usecases/script/add-speaker.d.ts +3 -0
  92. package/dist/usecases/script/add-speaker.js +22 -0
  93. package/dist/usecases/script/add-speaker.js.map +1 -0
  94. package/dist/usecases/script/alias.d.ts +3 -0
  95. package/dist/usecases/script/alias.js +40 -0
  96. package/dist/usecases/script/alias.js.map +1 -0
  97. package/dist/usecases/script/assets.d.ts +3 -0
  98. package/dist/usecases/script/assets.js +21 -0
  99. package/dist/usecases/script/assets.js.map +1 -0
  100. package/dist/usecases/script/context.d.ts +3 -0
  101. package/dist/usecases/script/context.js +20 -0
  102. package/dist/usecases/script/context.js.map +1 -0
  103. package/dist/usecases/script/create.d.ts +3 -0
  104. package/dist/usecases/script/create.js +67 -0
  105. package/dist/usecases/script/create.js.map +1 -0
  106. package/dist/usecases/script/delete.d.ts +3 -0
  107. package/dist/usecases/script/delete.js +79 -0
  108. package/dist/usecases/script/delete.js.map +1 -0
  109. package/dist/usecases/script/describe.d.ts +3 -0
  110. package/dist/usecases/script/describe.js +31 -0
  111. package/dist/usecases/script/describe.js.map +1 -0
  112. package/dist/usecases/script/dialogue.d.ts +3 -0
  113. package/dist/usecases/script/dialogue.js +28 -0
  114. package/dist/usecases/script/dialogue.js.map +1 -0
  115. package/dist/usecases/script/do.d.ts +3 -0
  116. package/dist/usecases/script/do.js +130 -0
  117. package/dist/usecases/script/do.js.map +1 -0
  118. package/dist/usecases/script/episodes.d.ts +3 -0
  119. package/dist/usecases/script/episodes.js +36 -0
  120. package/dist/usecases/script/episodes.js.map +1 -0
  121. package/dist/usecases/script/export.d.ts +3 -0
  122. package/dist/usecases/script/export.js +117 -0
  123. package/dist/usecases/script/export.js.map +1 -0
  124. package/dist/usecases/script/insert.d.ts +3 -0
  125. package/dist/usecases/script/insert.js +87 -0
  126. package/dist/usecases/script/insert.js.map +1 -0
  127. package/dist/usecases/script/issues.d.ts +3 -0
  128. package/dist/usecases/script/issues.js +27 -0
  129. package/dist/usecases/script/issues.js.map +1 -0
  130. package/dist/usecases/script/lib.d.ts +77 -0
  131. package/dist/usecases/script/lib.js +849 -0
  132. package/dist/usecases/script/lib.js.map +1 -0
  133. package/dist/usecases/script/locations.d.ts +3 -0
  134. package/dist/usecases/script/locations.js +15 -0
  135. package/dist/usecases/script/locations.js.map +1 -0
  136. package/dist/usecases/script/merge.d.ts +3 -0
  137. package/dist/usecases/script/merge.js +43 -0
  138. package/dist/usecases/script/merge.js.map +1 -0
  139. package/dist/usecases/script/move.d.ts +3 -0
  140. package/dist/usecases/script/move.js +33 -0
  141. package/dist/usecases/script/move.js.map +1 -0
  142. package/dist/usecases/script/overlap.d.ts +3 -0
  143. package/dist/usecases/script/overlap.js +21 -0
  144. package/dist/usecases/script/overlap.js.map +1 -0
  145. package/dist/usecases/script/props.d.ts +3 -0
  146. package/dist/usecases/script/props.js +15 -0
  147. package/dist/usecases/script/props.js.map +1 -0
  148. package/dist/usecases/script/refs.d.ts +3 -0
  149. package/dist/usecases/script/refs.js +61 -0
  150. package/dist/usecases/script/refs.js.map +1 -0
  151. package/dist/usecases/script/rename.d.ts +3 -0
  152. package/dist/usecases/script/rename.js +17 -0
  153. package/dist/usecases/script/rename.js.map +1 -0
  154. package/dist/usecases/script/replace.d.ts +3 -0
  155. package/dist/usecases/script/replace.js +35 -0
  156. package/dist/usecases/script/replace.js.map +1 -0
  157. package/dist/usecases/script/role.d.ts +3 -0
  158. package/dist/usecases/script/role.js +17 -0
  159. package/dist/usecases/script/role.js.map +1 -0
  160. package/dist/usecases/script/scenes.d.ts +3 -0
  161. package/dist/usecases/script/scenes.js +45 -0
  162. package/dist/usecases/script/scenes.js.map +1 -0
  163. package/dist/usecases/script/script-patch.d.ts +3 -0
  164. package/dist/usecases/script/script-patch.js +187 -0
  165. package/dist/usecases/script/script-patch.js.map +1 -0
  166. package/dist/usecases/script/script-validate.d.ts +3 -0
  167. package/dist/usecases/script/script-validate.js +30 -0
  168. package/dist/usecases/script/script-validate.js.map +1 -0
  169. package/dist/usecases/script/session.d.ts +56 -0
  170. package/dist/usecases/script/session.js +568 -0
  171. package/dist/usecases/script/session.js.map +1 -0
  172. package/dist/usecases/script/speakers.d.ts +3 -0
  173. package/dist/usecases/script/speakers.js +28 -0
  174. package/dist/usecases/script/speakers.js.map +1 -0
  175. package/dist/usecases/script/split.d.ts +3 -0
  176. package/dist/usecases/script/split.js +37 -0
  177. package/dist/usecases/script/split.js.map +1 -0
  178. package/dist/usecases/script/state-add.d.ts +3 -0
  179. package/dist/usecases/script/state-add.js +23 -0
  180. package/dist/usecases/script/state-add.js.map +1 -0
  181. package/dist/usecases/script/state-change.d.ts +3 -0
  182. package/dist/usecases/script/state-change.js +24 -0
  183. package/dist/usecases/script/state-change.js.map +1 -0
  184. package/dist/usecases/script/state-delete.d.ts +3 -0
  185. package/dist/usecases/script/state-delete.js +23 -0
  186. package/dist/usecases/script/state-delete.js.map +1 -0
  187. package/dist/usecases/script/state-rename.d.ts +3 -0
  188. package/dist/usecases/script/state-rename.js +17 -0
  189. package/dist/usecases/script/state-rename.js.map +1 -0
  190. package/dist/usecases/script/summary.d.ts +3 -0
  191. package/dist/usecases/script/summary.js +45 -0
  192. package/dist/usecases/script/summary.js.map +1 -0
  193. package/dist/usecases/script/transition.d.ts +3 -0
  194. package/dist/usecases/script/transition.js +24 -0
  195. package/dist/usecases/script/transition.js.map +1 -0
  196. package/dist/usecases/script/type.d.ts +3 -0
  197. package/dist/usecases/script/type.js +24 -0
  198. package/dist/usecases/script/type.js.map +1 -0
  199. package/dist/usecases/script/worldview.d.ts +3 -0
  200. package/dist/usecases/script/worldview.js +18 -0
  201. package/dist/usecases/script/worldview.js.map +1 -0
  202. package/package.json +1 -1
  203. package/dist/domain/script-core.d.ts +0 -78
  204. package/dist/domain/script-core.js +0 -3026
  205. package/dist/domain/script-core.js.map +0 -1
  206. package/dist/usecases/script.d.ts +0 -67
  207. package/dist/usecases/script.js +0 -2348
  208. package/dist/usecases/script.js.map +0 -1
@@ -0,0 +1,849 @@
1
+ import * as path from "node:path";
2
+ import { CliError, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_USAGE, directDir, } from "../../common.js";
3
+ import { applyPatchOperations } from "../../domain/script/patch/apply.js";
4
+ import { renumberSceneIds } from "../../domain/script/lookups.js";
5
+ import { isDangerousRegex, MAX_GREP_PATTERN_LEN, parseInSelector, selectorSingleActionPoint, WHOLE_SCRIPT, } from "../../domain/script/selector.js";
6
+ import { parseAnyAddress, } from "../../domain/script/address.js";
7
+ import { summarizeIssues, } from "../direct.js";
8
+ import { sortDeep } from "../../domain/direct-core.js";
9
+ import { asList, isDict, strOf, } from "../../domain/script/shared.js";
10
+ import { loadScriptForEdit, validateSession, saveScriptSession, syncValidationResult, scriptSourceText, } from "./session.js";
11
+ export function tokenizeLine(line) {
12
+ const out = [];
13
+ let cur = "";
14
+ let started = false;
15
+ let quoted = false;
16
+ let inSingle = false;
17
+ let inDouble = false;
18
+ const flush = () => { if (started) {
19
+ out.push({ text: cur, quoted });
20
+ cur = "";
21
+ started = false;
22
+ quoted = false;
23
+ } };
24
+ for (let i = 0; i < line.length; i++) {
25
+ const ch = line[i];
26
+ if (inSingle) {
27
+ if (ch === "'")
28
+ inSingle = false;
29
+ else {
30
+ cur += ch;
31
+ quoted = true;
32
+ }
33
+ }
34
+ else if (inDouble) {
35
+ if (ch === '"')
36
+ inDouble = false;
37
+ else if (ch === "\\" && i + 1 < line.length) {
38
+ cur += line[++i];
39
+ quoted = true;
40
+ }
41
+ else {
42
+ cur += ch;
43
+ quoted = true;
44
+ }
45
+ }
46
+ else if (ch === "'") {
47
+ inSingle = true;
48
+ started = true;
49
+ }
50
+ else if (ch === '"') {
51
+ inDouble = true;
52
+ started = true;
53
+ }
54
+ else if (ch === "\\" && i + 1 < line.length) {
55
+ cur += line[++i];
56
+ started = true;
57
+ quoted = true;
58
+ }
59
+ else if (ch === " " || ch === "\t" || ch === "\r") {
60
+ flush();
61
+ }
62
+ else {
63
+ cur += ch;
64
+ started = true;
65
+ }
66
+ }
67
+ flush();
68
+ return out;
69
+ }
70
+ // A token is a flag only if it was unquoted AND starts with `--`.
71
+ function isDoFlag(t) {
72
+ return !t.quoted && t.text.startsWith("--");
73
+ }
74
+ // Per-verb flag arity. Everything else takes the next token as its value.
75
+ export const DO_BOOL_FLAGS = {
76
+ replace: new Set(["all", "regex"]),
77
+ "state-change": new Set(["clear"]),
78
+ transition: new Set(["clear"]),
79
+ context: new Set(["clear", "remove"]),
80
+ delete: new Set(["force"]),
81
+ };
82
+ export const DO_REPEAT_FLAGS = {
83
+ alias: new Set(["add", "remove"]),
84
+ overlap: new Set(["line"]),
85
+ "add-actor": new Set(["alias"]),
86
+ };
87
+ function pushFlag(flags, repeats, name, val) {
88
+ if (repeats.has(name)) {
89
+ const arr = flags[name] ?? [];
90
+ arr.push(val);
91
+ flags[name] = arr;
92
+ }
93
+ else {
94
+ flags[name] = val;
95
+ }
96
+ }
97
+ export function parseDoLine(tokens) {
98
+ if (tokens.length === 0 || isDoFlag(tokens[0])) {
99
+ throw doErr("a line must start with a verb (not a flag)");
100
+ }
101
+ const verb = tokens[0].text;
102
+ const bools = DO_BOOL_FLAGS[verb] ?? new Set();
103
+ const repeats = DO_REPEAT_FLAGS[verb] ?? new Set();
104
+ const args = [];
105
+ const flags = {};
106
+ for (let i = 1; i < tokens.length; i++) {
107
+ const t = tokens[i];
108
+ if (isDoFlag(t)) {
109
+ let name = t.text.slice(2);
110
+ // Support `--flag=value` (a common CLI habit) alongside `--flag value`.
111
+ const eq = name.indexOf("=");
112
+ if (eq >= 0) {
113
+ const inlineVal = name.slice(eq + 1);
114
+ name = name.slice(0, eq);
115
+ if (!name)
116
+ throw doErr(`empty flag name in "${t.text}"`);
117
+ // A boolean flag written as `--all=false` must become false, not the
118
+ // truthy string "false" — `--force=false` especially must NOT force.
119
+ if (bools.has(name)) {
120
+ flags[name] = !["false", "0", "no", "off"].includes(inlineVal.toLowerCase());
121
+ }
122
+ else {
123
+ pushFlag(flags, repeats, name, inlineVal);
124
+ }
125
+ continue;
126
+ }
127
+ if (!name)
128
+ throw doErr(`empty flag name "${t.text}"`);
129
+ if (bools.has(name)) {
130
+ flags[name] = true;
131
+ continue;
132
+ }
133
+ // A value-taking flag must be followed by an actual value, not another
134
+ // unquoted `--flag` (or end-of-line) — otherwise `--to --from x` silently
135
+ // binds '--from' as --to's value. A quoted `"--x"` IS a valid value.
136
+ const next = tokens[i + 1];
137
+ if (next === undefined || isDoFlag(next)) {
138
+ throw doErr(`--${name} needs a value`);
139
+ }
140
+ pushFlag(flags, repeats, name, tokens[++i].text);
141
+ }
142
+ else {
143
+ args.push(t.text);
144
+ }
145
+ }
146
+ return { verb, args, flags };
147
+ }
148
+ export function doErr(detail) {
149
+ return new CliError("DO BLOCKED: line invalid", detail, {
150
+ exitCode: EXIT_USAGE,
151
+ required: ["a valid <verb> ...args... line"],
152
+ received: [detail],
153
+ nextSteps: ["Each line is a scriptctl verb minus the leading `scriptctl`; see `scriptctl do --help`."],
154
+ errorCode: "DO_LINE_INVALID",
155
+ });
156
+ }
157
+ // ---- shared op-template builders ------------------------------------------
158
+ // One source of truth for the verbs whose op has validation/defaults, so the
159
+ // typed CLI handler and `do`'s lineToOp can never drift. Each returns the op
160
+ // WITHOUT `at` (the caller adds the single address or sweeps it over a selector)
161
+ // and throws the SAME CliError the handler used to throw inline.
162
+ export function stateChangeTemplate(target, f) {
163
+ const toSet = f.to !== undefined;
164
+ const clearSet = Boolean(f.clear);
165
+ if (toSet && clearSet) {
166
+ throw new CliError("VERB BLOCKED: state-change flags mutually exclusive", "state-change: --to and --clear are mutually exclusive.", {
167
+ exitCode: EXIT_USAGE, required: ["exactly one of --to <state> / --clear"], received: ["--to", "--clear"],
168
+ nextSteps: ["Pick one and retry."], errorCode: "STATE_CHANGE_FLAG_CONFLICT",
169
+ });
170
+ }
171
+ if (!toSet && !clearSet) {
172
+ throw new CliError("VERB BLOCKED: state-change flag missing", "state-change: must specify --to <state> or --clear.", {
173
+ exitCode: EXIT_USAGE, required: ["one of --to <state> / --clear"], received: ["<missing>"],
174
+ nextSteps: ["Pick exactly one flag."], errorCode: "STATE_CHANGE_FLAG_MISSING",
175
+ });
176
+ }
177
+ return clearSet
178
+ ? { op: "action.state.remove", target }
179
+ : { op: "action.state.change", target, to: f.to, from: f.from, effective: f.effective || "after" };
180
+ }
181
+ export function transitionTemplate(target, f) {
182
+ const setBoth = f.process !== undefined || f.contrast !== undefined;
183
+ const clearSet = Boolean(f.clear);
184
+ if (setBoth && clearSet) {
185
+ throw new CliError("VERB BLOCKED: transition flags mutually exclusive", "transition: --clear is mutually exclusive with --process/--contrast.", {
186
+ exitCode: EXIT_USAGE, required: ["either --clear, or --process + --contrast"], received: ["--clear", "--process/--contrast"],
187
+ nextSteps: ["Pick one mode and retry."], errorCode: "TRANSITION_FLAG_CONFLICT",
188
+ });
189
+ }
190
+ if (!setBoth && !clearSet) {
191
+ throw new CliError("VERB BLOCKED: transition flag missing", "transition: must specify --process/--contrast or --clear.", {
192
+ exitCode: EXIT_USAGE, required: ["--process + --contrast, or --clear"], received: ["<missing>"],
193
+ nextSteps: ["Pick exactly one mode."], errorCode: "TRANSITION_FLAG_MISSING",
194
+ });
195
+ }
196
+ return clearSet
197
+ ? { op: "action.transition.clear", target }
198
+ : { op: "action.transition.set", target, process: f.process, contrast: f.contrast };
199
+ }
200
+ export function contextTemplate(target, f) {
201
+ const stateSet = f.state !== undefined;
202
+ const clearSet = Boolean(f.clear);
203
+ const removeSet = Boolean(f.remove);
204
+ const n = (stateSet ? 1 : 0) + (clearSet ? 1 : 0) + (removeSet ? 1 : 0);
205
+ if (n === 0) {
206
+ throw new CliError("VERB BLOCKED: context flag missing", "context: must specify --state X, --clear, or --remove.", {
207
+ exitCode: EXIT_USAGE, required: ["one of --state <state_id> / --clear / --remove"], received: ["<missing>"],
208
+ nextSteps: ["Pick exactly one flag."], errorCode: "CONTEXT_FLAG_MISSING",
209
+ });
210
+ }
211
+ if (n > 1) {
212
+ const passed = [];
213
+ if (stateSet)
214
+ passed.push("--state");
215
+ if (clearSet)
216
+ passed.push("--clear");
217
+ if (removeSet)
218
+ passed.push("--remove");
219
+ throw new CliError("VERB BLOCKED: context flags mutually exclusive", "context: --state / --clear / --remove are mutually exclusive.", {
220
+ exitCode: EXIT_USAGE, required: ["exactly one of --state / --clear / --remove"], received: passed,
221
+ nextSteps: ["Pick one and retry."], errorCode: "CONTEXT_FLAG_CONFLICT",
222
+ });
223
+ }
224
+ if (removeSet)
225
+ return { op: "context.ref.remove", target };
226
+ if (clearSet)
227
+ return { op: "context.clear", target };
228
+ return { op: "context.set", target, state: f.state === "none" ? "" : f.state };
229
+ }
230
+ export function dialogueTemplate(speakers, f) {
231
+ return { op: "dialogue.speakers", speakers, delivery: strOf(f.delivery).trim() || (speakers.length > 1 ? "simultaneous" : "single") };
232
+ }
233
+ // Translate one parsed verb line into a patch op dict (the SAME ops `patch`
234
+ // consumes). `do` = the human/agent-friendly notation for those ops: no JSON,
235
+ // no field-name guessing — the verbs you already type. Multipolar verbs
236
+ // (delete/move/insert/merge/describe) pick their op by the address kind.
237
+ export function lineToOp(verb, args, flags) {
238
+ const a0 = strOf(args[0]);
239
+ const a1 = strOf(args[1]);
240
+ switch (verb) {
241
+ case "replace":
242
+ return flags["regex"]
243
+ ? { op: "action.content.regex-replace", at: a0, pattern: regexPattern(strOf(flags["from"])), to: flags["to"] ?? "", all: Boolean(flags["all"]) }
244
+ : { op: "action.content.replace", at: a0, from: strOf(flags["from"]), to: flags["to"] ?? "", all: Boolean(flags["all"]) };
245
+ case "type":
246
+ return { op: "action.type.set", at: a0, type: a1 };
247
+ case "actor":
248
+ return { op: "action.actor.set", at: a0, actor_id: a1 };
249
+ case "state-change":
250
+ return { ...stateChangeTemplate(a1, { to: flags["to"], from: flags["from"], effective: flags["effective"], clear: Boolean(flags["clear"]) }), at: a0 };
251
+ case "transition":
252
+ return { ...transitionTemplate(a1, { process: flags["process"], contrast: flags["contrast"], clear: Boolean(flags["clear"]) }), at: a0 };
253
+ case "context":
254
+ return { ...contextTemplate(a1, { state: flags["state"], clear: Boolean(flags["clear"]), remove: Boolean(flags["remove"]) }), at: a0 };
255
+ case "dialogue":
256
+ return { ...dialogueTemplate(splitSpeakerList(flags["speakers"]), { delivery: flags["delivery"] }), at: a0 };
257
+ case "overlap":
258
+ return { op: "dialogue.overlap", at: a0, lines: flags["line"] ?? [] };
259
+ case "rename":
260
+ return { op: "asset.rename", target: a0, name: a1 };
261
+ case "role":
262
+ return { op: "asset.role.set", target: a0, role_type: a1 };
263
+ case "worldview":
264
+ return { op: "meta.worldview.set", worldview: a0 };
265
+ case "alias": {
266
+ const add = flags["add"] ?? [];
267
+ const remove = flags["remove"] ?? [];
268
+ if (add.length)
269
+ return { op: "asset.alias.add", target: a0, alias: add };
270
+ if (remove.length)
271
+ return { op: "asset.alias.remove", target: a0, alias: remove };
272
+ throw doErr("alias needs --add <alias> or --remove <alias>");
273
+ }
274
+ case "describe": {
275
+ const addr = parseAnyAddress(a0);
276
+ if (addr.kind === "state")
277
+ return { op: "state.describe", target: a0, description: a1 };
278
+ if (addr.kind === "asset")
279
+ return { op: "asset.describe", target: a0, description: a1 };
280
+ throw doErr(`describe cannot operate on a ${addr.kind} address (${a0}); use an asset or asset/state target`);
281
+ }
282
+ case "state-add":
283
+ return { op: "state.add", target: a0, name: a1, description: flags["description"], state_id: flags["state-id"] };
284
+ case "state-rename":
285
+ return { op: "state.rename", target: a0, name: a1 };
286
+ case "state-delete":
287
+ return { op: "state.delete", target: a0, strategy: flags["strategy"], replacement: flags["replacement"] };
288
+ case "add-actor":
289
+ return { op: "actor.add", name: a0, role_type: flags["role"], description: flags["description"], aliases: flags["alias"] ?? undefined };
290
+ case "add-location":
291
+ return { op: "location.add", name: a0, description: flags["description"] };
292
+ case "add-prop":
293
+ return { op: "prop.add", name: a0, description: flags["description"] };
294
+ case "add-episode":
295
+ return { op: "episode.add", title: flags["title"], episode_id: flags["id"] };
296
+ case "add-speaker":
297
+ return { op: "speaker.add", kind: flags["kind"], name: flags["name"], source_id: flags["source-id"], voice_desc: flags["voice-desc"], speaker_id: flags["speaker-id"] };
298
+ case "split":
299
+ return { op: "scene.split", at: a0, at_index: flags["at"], new_scene_id: flags["new-scene-id"] };
300
+ case "delete": {
301
+ const addr = parseAnyAddress(a0);
302
+ if (addr.kind === "action")
303
+ return { op: "action.delete", at: a0 };
304
+ if (addr.kind === "scene")
305
+ return { op: "scene.delete", at: a0, force: Boolean(flags["force"]) };
306
+ if (addr.kind === "speaker")
307
+ return { op: "speaker.delete", target: a0, strategy: flags["strategy"], replacement: flags["replacement"] };
308
+ if (addr.kind === "asset")
309
+ return { op: "asset.delete", target: a0, strategy: flags["strategy"], replacement: flags["replacement"] };
310
+ throw doErr(`delete cannot operate on a ${addr.kind} address (${a0}); episodes/states have no delete op`);
311
+ }
312
+ case "move": {
313
+ const addr = parseAnyAddress(a0);
314
+ if (addr.kind === "scene")
315
+ return { op: "scene.move", at: a0, to: a1, at_index: flags["at"] };
316
+ if (addr.kind === "action")
317
+ return { op: "action.move", at: a0, to: a1 };
318
+ throw doErr(`move cannot operate on a ${addr.kind} address (${a0}); move an action or a scene`);
319
+ }
320
+ case "merge": {
321
+ const addr = parseAnyAddress(a0);
322
+ if (addr.kind === "scene")
323
+ return { op: "scene.merge", from: a0, into: strOf(flags["into"]) };
324
+ if (addr.kind === "asset")
325
+ return { op: "asset.merge", from: a0, into: strOf(flags["into"]) };
326
+ throw doErr(`merge cannot operate on a ${addr.kind} address (${a0}); merge two assets or two scenes`);
327
+ }
328
+ case "insert": {
329
+ const addr = parseAnyAddress(a0);
330
+ if (addr.kind === "episode") {
331
+ return { op: "scene.insert", at: a0, location: flags["location"], time: flags["time"], space: flags["space"], at_index: flags["at"], before: flags["before"], after: flags["after"], scene_id: flags["scene-id"] };
332
+ }
333
+ if (addr.kind === "scene") {
334
+ return { op: "action.insert", at: a0, type: flags["type"], content: flags["content"], at_index: flags["at"], before: flags["before"], after: flags["after"], actor_id: flags["actor"], speaker_id: flags["speaker"] };
335
+ }
336
+ throw doErr(`insert needs ep_NNN (scene insert) or ep_NNN/scn_NNN (action insert), got a ${addr.kind} address (${a0})`);
337
+ }
338
+ default:
339
+ throw doErr(`unknown verb '${verb}' (structural/edit/create verbs are supported; queries are not)`);
340
+ }
341
+ }
342
+ export function actionAddress(epId, sceneId, idx) {
343
+ return `${epId}/${sceneId}#${idx}`;
344
+ }
345
+ export function formatInspectAction(epId, sceneId, idx, action) {
346
+ const addr = actionAddress(epId, sceneId, idx);
347
+ const type = strOf(action["type"]).trim() || "action";
348
+ const delivery = strOf(action["delivery"]).trim();
349
+ const tag = delivery ? `${type} ${delivery}` : type;
350
+ // Prefer the speaker_id (concrete) over actor_id (asset), and skip both when
351
+ // the action is narrative (no speaker). Format kept on a single line so agents
352
+ // can grep the output.
353
+ const speakerId = strOf(action["speaker_id"]).trim();
354
+ const actorId = strOf(action["actor_id"]).trim();
355
+ const who = speakerId || actorId;
356
+ let content = strOf(action["content"]);
357
+ if (!content) {
358
+ const lines = asList(action["lines"]);
359
+ if (lines.length > 0)
360
+ content = lines.map((l) => strOf(l["content"])).join(" / ");
361
+ }
362
+ const prefix = who ? `${addr} [${tag} ${who}]` : `${addr} [${tag}]`;
363
+ return content ? `${prefix} ${content}` : prefix;
364
+ }
365
+ // ---------------------------------------------------------------------------
366
+ // command_script_patch
367
+ // ---------------------------------------------------------------------------
368
+ export const SKIPPABLE_EDIT_CODES = new Set([
369
+ "REGEX_NO_MATCH",
370
+ "FROM_TEXT_NOT_FOUND",
371
+ "OVERLAP_DIALOGUE_UNSUPPORTED",
372
+ ]);
373
+ // ---------------------------------------------------------------------------
374
+ // Single-op helpers
375
+ // ---------------------------------------------------------------------------
376
+ export async function applySingleScriptOp(opts, op) {
377
+ const workspace = strOf(opts["workspace_path"] || "workspace");
378
+ const session = await loadScriptForEdit(opts);
379
+ const script = session.script;
380
+ const applied = applyPatchOperations(script, scriptSourceText(workspace), [op]);
381
+ const opName = applied[0] ?? strOf(op["op"]);
382
+ const [newRevision, idempotent] = await saveScriptSession(session, opts, opName);
383
+ const validation = validateSession(session);
384
+ await syncValidationResult(session, validation, newRevision);
385
+ const passed = Boolean(validation["passed"]);
386
+ const resultLines = [
387
+ `operation: ${opName}`,
388
+ `validation: ${passed ? "passed" : "needs repair"}`,
389
+ ];
390
+ if (session.remote) {
391
+ resultLines.push(`revision: ${newRevision}`);
392
+ resultLines.push(`idempotent: ${String(idempotent).toLowerCase()}`);
393
+ }
394
+ const report = {
395
+ title: passed ? "SCRIPT OP APPLIED" : "SCRIPT OP APPLIED: Validation needs repair",
396
+ op: opName,
397
+ changed: applied.length > 0,
398
+ summary: summarizeScriptOp(script, op),
399
+ warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
400
+ result: resultLines,
401
+ issues: summarizeIssues(asList(validation["issues"])),
402
+ artifacts: [session.artifactLabel, path.join(directDir(workspace), "validation.json")],
403
+ next: [passed ? "Continue editing the final script." : "Inspect issues and apply another patch."],
404
+ };
405
+ return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
406
+ }
407
+ export function summarizeScriptOp(_script, op) {
408
+ const kind = strOf(op["op"]);
409
+ if (kind === "actor.add")
410
+ return `已新增人物 ${op["name"] || op["actor_name"]}${op["role_type"] ? `(${op["role_type"]})` : ""}`;
411
+ if (kind === "location.add")
412
+ return `已新增场景地点 ${op["name"] || op["location_name"]}`;
413
+ if (kind === "prop.add")
414
+ return `已新增道具 ${op["name"] || op["prop_name"]}`;
415
+ if (kind === "episode.add")
416
+ return `已新增分集${op["episode_id"] ? ` ${op["episode_id"]}` : "(自动编号)"}${op["title"] ? `:${op["title"]}` : ""}`;
417
+ if (kind === "state.add")
418
+ return `已为 ${op["target"]} 新增状态 ${op["state_id"]}: ${op["name"] || op["state_name"]}`;
419
+ if (kind === "action.state.change")
420
+ return `已在 ${op["at"]} 为 ${op["target"]} 设置状态切换到 ${op["to"] || op["to_state_id"]}`;
421
+ if (kind.startsWith("state."))
422
+ return `已执行 ${kind}: ${op["target"]}`;
423
+ if (kind.startsWith("dialogue."))
424
+ return `已执行 ${kind}: ${op["at"]}`;
425
+ return `已执行 ${kind}`;
426
+ }
427
+ // Accept `/re/flags` as-is; wrap a bare pattern as `/pattern/` so
428
+ // `replace --regex --from '^\s*\[[^\]]*\]'` works without the user typing slashes.
429
+ export function regexPattern(raw) {
430
+ const s = raw.trim();
431
+ if (s.length >= 2 && s.startsWith("/") && s.lastIndexOf("/") > 0)
432
+ return s;
433
+ return `/${s}/`;
434
+ }
435
+ // The selector-driven write runner. Resolves `opts` to its matched actions
436
+ // (range/region + predicates) and applies one op template to each, as a single
437
+ // in-memory pass committed once. Guardrail: with no --apply it's a DRY-RUN that
438
+ // reports the blast radius and writes nothing; --apply commits. Per-action
439
+ // "no match here / overlap dialogue" errors are skipped (a sweep); anything else
440
+ // aborts the whole edit before any write. Shared by every selector-capable verb.
441
+ export async function applySelectorEdits(opts, template, verbLabel) {
442
+ const workspace = strOf(opts["workspace_path"] || "workspace");
443
+ const apply = Boolean(opts["apply"]);
444
+ const opName = strOf(template["op"]);
445
+ const session = await loadScriptForEdit(opts);
446
+ const script = session.script;
447
+ const targets = resolveActionTargets(script, opts);
448
+ const refs = targets.map((t) => `${t.epId}/${t.sceneId}#${t.idx}`);
449
+ const source = scriptSourceText(workspace);
450
+ let applied = 0;
451
+ let skipped = 0;
452
+ for (const ref of refs) {
453
+ try {
454
+ // Skip the per-op scene renumber; selector verbs are all non-structural,
455
+ // so we renumber once after the whole sweep instead of N times.
456
+ applyPatchOperations(script, source, [{ ...template, at: ref }], { renumber: false });
457
+ applied += 1;
458
+ }
459
+ catch (err) {
460
+ if (err instanceof CliError && err.errorCode && SKIPPABLE_EDIT_CODES.has(err.errorCode)) {
461
+ skipped += 1;
462
+ continue;
463
+ }
464
+ throw err;
465
+ }
466
+ }
467
+ if (applied > 0)
468
+ renumberSceneIds(script);
469
+ const sample = refs.slice(0, 12);
470
+ const blast = [
471
+ `verb: ${verbLabel} (${opName})`,
472
+ `matched: ${refs.length} action(s)`,
473
+ `${apply ? "applied" : "would-apply"}: ${applied}`,
474
+ `${apply ? "skipped" : "would-skip"}: ${skipped}`,
475
+ ...(sample.length ? [`targets: ${sample.join(", ")}${refs.length > sample.length ? ` … (+${refs.length - sample.length} more)` : ""}`] : []),
476
+ ];
477
+ if (!apply) {
478
+ const validation = validateSession(session);
479
+ const passed = Boolean(validation["passed"]);
480
+ const report = {
481
+ title: refs.length === 0 ? "SELECTOR EDIT DRY-RUN: no match" : "SELECTOR EDIT DRY-RUN (no write)",
482
+ op: `${opName}.dry-run`,
483
+ changed: false,
484
+ summary: `matched ${refs.length} action(s); would apply ${applied}, skip ${skipped} — pass --apply to commit`,
485
+ result: [...blast, `validation: ${passed ? "would pass" : "would need repair"}`, "dry-run: no write performed"],
486
+ warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
487
+ issues: summarizeIssues(asList(validation["issues"])),
488
+ artifacts: [session.artifactLabel],
489
+ next: [refs.length === 0 ? "Widen the selection (--in / --grep / predicates)." : "Re-run with --apply to commit."],
490
+ };
491
+ return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
492
+ }
493
+ if (refs.length === 0) {
494
+ return [{
495
+ title: "SELECTOR EDIT: no match",
496
+ op: opName,
497
+ changed: false,
498
+ summary: "matched 0 action(s); nothing written",
499
+ result: blast,
500
+ artifacts: [session.artifactLabel],
501
+ next: ["Widen the selection (--in / --grep / predicates)."],
502
+ }, EXIT_OK];
503
+ }
504
+ const [newRevision, idempotent] = await saveScriptSession(session, opts, opName);
505
+ const validation = validateSession(session);
506
+ await syncValidationResult(session, validation, newRevision);
507
+ const passed = Boolean(validation["passed"]);
508
+ const resultLines = [...blast, `validation: ${passed ? "passed" : "needs repair"}`];
509
+ if (session.remote) {
510
+ resultLines.push(`revision: ${newRevision}`);
511
+ resultLines.push(`idempotent: ${String(idempotent).toLowerCase()}`);
512
+ }
513
+ const report = {
514
+ title: passed ? "SELECTOR EDIT APPLIED" : "SELECTOR EDIT APPLIED: Validation needs repair",
515
+ op: opName,
516
+ changed: applied > 0,
517
+ summary: `matched ${refs.length} action(s); applied ${applied}, skipped ${skipped}`,
518
+ result: resultLines,
519
+ warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
520
+ issues: summarizeIssues(asList(validation["issues"])),
521
+ artifacts: [session.artifactLabel, path.join(directDir(workspace), "validation.json")],
522
+ next: [passed ? "Continue editing the final script." : "Inspect issues and apply another edit."],
523
+ };
524
+ return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
525
+ }
526
+ export { sortDeep };
527
+ // ===========================================================================
528
+ // 0.6.0 plural-noun query commands (replace `commandScriptInspect --target X`)
529
+ // ===========================================================================
530
+ //
531
+ // Each command is a thin read-only wrapper around the underlying script
532
+ // document. They all return a Report shaped like inspect's existing output but
533
+ // without the `--target` flag — the verb itself encodes the target. Filters
534
+ // use `--in <addr>` (compose with the address parsers) plus per-target
535
+ // predicates (--name / --kind / --has / --grep / --context / --type / --actor
536
+ // / --speaker / --severity / --code / --has-{actor,location,prop}).
537
+ // Resolve the speaker_id → source actor mapping once per call. Used by
538
+ // commandActions's --actor filter when the action speaks via a speaker.
539
+ export function speakerSourceActorMap(script) {
540
+ const map = new Map();
541
+ for (const s of asList(script["speakers"])) {
542
+ const sourceKind = strOf(s["source_kind"]);
543
+ if (sourceKind === "actor") {
544
+ const sid = strOf(s["speaker_id"]);
545
+ const src = strOf(s["source_id"]);
546
+ if (sid && src)
547
+ map.set(sid, src);
548
+ }
549
+ }
550
+ return map;
551
+ }
552
+ export function speakerIdsInAction(action) {
553
+ const ids = new Set();
554
+ const direct = strOf(action["speaker_id"]).trim();
555
+ if (direct)
556
+ ids.add(direct);
557
+ for (const sp of asList(action["speakers"])) {
558
+ const id = strOf(sp["speaker_id"]).trim();
559
+ if (id)
560
+ ids.add(id);
561
+ }
562
+ for (const line of asList(action["lines"])) {
563
+ const id = strOf(line["speaker_id"]).trim();
564
+ if (id)
565
+ ids.add(id);
566
+ }
567
+ return ids;
568
+ }
569
+ export function actorIdsInAction(action, speakerToActor) {
570
+ const ids = new Set();
571
+ const direct = strOf(action["actor_id"]).trim();
572
+ if (direct)
573
+ ids.add(direct);
574
+ for (const spkId of speakerIdsInAction(action)) {
575
+ const actor = speakerToActor.get(spkId);
576
+ if (actor)
577
+ ids.add(actor);
578
+ }
579
+ return ids;
580
+ }
581
+ // Build a grep matcher. /pattern/flags → RegExp, otherwise literal substring.
582
+ // Pre-flight checks block the common ReDoS shapes before they hit V8's regex
583
+ // engine: nested quantifiers like (a+)+, (a*)+, (a*|b*)+ etc. compile fine
584
+ // but hang the process on adversarial input. Since --grep accepts caller-
585
+ // controlled patterns and runs over every action in scope, the safe move is
586
+ // to reject patterns with quantifier-on-group-with-internal-quantifier and
587
+ // cap the pattern length.
588
+ export function buildGrepMatcher(raw) {
589
+ if (!raw)
590
+ return null;
591
+ const m = /^\/(.+)\/([gimsuy]*)$/.exec(raw);
592
+ if (m) {
593
+ const pattern = m[1];
594
+ if (isDangerousRegex(pattern)) {
595
+ throw new CliError("QUERY BLOCKED: regex unsafe", "regex pattern has nested quantifiers (ReDoS risk) or exceeds the length limit.", {
596
+ exitCode: EXIT_USAGE,
597
+ required: [`pattern length <= ${MAX_GREP_PATTERN_LEN}; no quantifier-on-group-with-internal-quantifier (e.g. (a+)+)`],
598
+ received: [`length=${pattern.length}, sample=${pattern.slice(0, 80)}`],
599
+ nextSteps: ["Rewrite the regex to avoid nested quantifiers, or use a literal substring (drop the /…/ wrapper)."],
600
+ errorCode: "REGEX_UNSAFE",
601
+ });
602
+ }
603
+ try {
604
+ const re = new RegExp(pattern, m[2]);
605
+ return (hay) => re.test(hay);
606
+ }
607
+ catch {
608
+ // Malformed regex — treat as literal.
609
+ }
610
+ }
611
+ return (hay) => hay.includes(raw);
612
+ }
613
+ export function buildReport(op, title, lines, artifactLabel, next) {
614
+ return {
615
+ title,
616
+ op,
617
+ changed: false,
618
+ summary: `${lines.length} item(s)`,
619
+ result: lines.length > 0 ? lines : ["No matching items."],
620
+ artifacts: [artifactLabel],
621
+ next,
622
+ };
623
+ }
624
+ // ----- summary --------------------------------------------------------------
625
+ export function synopsisPreview(text, limit) {
626
+ const oneLine = text.replace(/\s+/g, " ").trim();
627
+ return oneLine.length > limit ? `${oneLine.slice(0, limit)}…` : oneLine;
628
+ }
629
+ export function episodeInSelector(selector, epId, scenes) {
630
+ if (selector.isEmpty)
631
+ return true;
632
+ if (selector.matchesScene(epId, "scn_000000"))
633
+ return true;
634
+ for (const scene of scenes) {
635
+ if (selector.matchesScene(epId, strOf(scene["scene_id"])))
636
+ return true;
637
+ }
638
+ return false;
639
+ }
640
+ export function nameMaps(script) {
641
+ const out = { actor: new Map(), location: new Map(), prop: new Map() };
642
+ for (const a of asList(script["actors"]))
643
+ out.actor.set(strOf(a["actor_id"]), strOf(a["actor_name"]));
644
+ for (const l of asList(script["locations"]))
645
+ out.location.set(strOf(l["location_id"]), strOf(l["location_name"]));
646
+ for (const p of asList(script["props"]))
647
+ out.prop.set(strOf(p["prop_id"]), strOf(p["prop_name"]));
648
+ return out;
649
+ }
650
+ export function formatScene(epId, scene, names) {
651
+ const ctx = isDict(scene["context"]) ? scene["context"] : {};
652
+ const env = isDict(scene["environment"]) ? scene["environment"] : {};
653
+ const space = strOf(env["space"]) || "-";
654
+ const time = strOf(env["time"]) || "-";
655
+ const locations = asList(ctx["locations"]).map((r) => {
656
+ const id = strOf(r["location_id"]);
657
+ return `${id}(${names.location.get(id) || "?"})`;
658
+ });
659
+ const actors = asList(ctx["actors"]).map((r) => {
660
+ const id = strOf(r["actor_id"]);
661
+ return `${id}(${names.actor.get(id) || "?"})`;
662
+ });
663
+ const props = asList(ctx["props"]).map((r) => {
664
+ const id = strOf(r["prop_id"]);
665
+ return `${id}(${names.prop.get(id) || "?"})`;
666
+ });
667
+ const actionCount = asList(scene["actions"]).length;
668
+ return `${epId}/${scene["scene_id"]} [${space} ${time}] location=${locations.join(",") || "-"} actors=${actors.join(",") || "-"} props=${props.join(",") || "-"} actions=${actionCount}`;
669
+ }
670
+ export const HAS_FILTERS = new Set(["state-changes", "transition", "lines"]);
671
+ // Resolve a selection (--in range/region selector + --type/--actor/--speaker/
672
+ // --grep/--has predicates, AND-combined) to the matching actions, in document
673
+ // order. The single matcher behind both `actions` queries and selector-driven
674
+ // writes. Does NOT enforce "at least one filter" — that policy is caller-owned
675
+ // (queries refuse a bare dump; writes gate on --all). Validates the --has value.
676
+ export function resolveActionTargets(script, opts) {
677
+ const selector = parseInSelector(strOf(opts["in"]));
678
+ const typeFilter = strOf(opts["type"]).trim();
679
+ const actorFilter = strOf(opts["actor"]).trim();
680
+ const speakerFilter = strOf(opts["speaker"]).trim();
681
+ const hasFilter = strOf(opts["has"]).trim();
682
+ // Whitelist --has: silently accepting unknown values means a typo like
683
+ // "state_changes" (underscore) passed the filter-missing guard but bypassed
684
+ // every concrete predicate, returning the whole script.
685
+ if (hasFilter && !HAS_FILTERS.has(hasFilter)) {
686
+ throw new CliError("QUERY BLOCKED: --has value invalid", "--has value invalid.", {
687
+ exitCode: EXIT_USAGE,
688
+ required: [`--has: one of ${[...HAS_FILTERS].join(", ")}`],
689
+ received: [hasFilter],
690
+ nextSteps: ["Use a supported --has value."],
691
+ errorCode: "HAS_FILTER_INVALID",
692
+ });
693
+ }
694
+ const grepMatcher = buildGrepMatcher(strOf(opts["grep"]));
695
+ const speakerToActor = (actorFilter || speakerFilter) ? speakerSourceActorMap(script) : new Map();
696
+ const out = [];
697
+ for (const ep of asList(script["episodes"])) {
698
+ const epId = strOf(ep["episode_id"]);
699
+ for (const scene of asList(ep["scenes"])) {
700
+ const sceneId = strOf(scene["scene_id"]);
701
+ if (!selector.matchesScene(epId, sceneId))
702
+ continue;
703
+ const actions = asList(scene["actions"]);
704
+ for (let i = 0; i < actions.length; i++) {
705
+ if (!selector.matchesAction(epId, sceneId, i))
706
+ continue;
707
+ const a = actions[i];
708
+ if (typeFilter && strOf(a["type"]).trim() !== typeFilter)
709
+ continue;
710
+ if (speakerFilter && !speakerIdsInAction(a).has(speakerFilter))
711
+ continue;
712
+ if (actorFilter && !actorIdsInAction(a, speakerToActor).has(actorFilter))
713
+ continue;
714
+ if (hasFilter === "state-changes" && asList(a["state_changes"]).length === 0)
715
+ continue;
716
+ if (hasFilter === "transition" && !isDict(a["transition_prompt"]))
717
+ continue;
718
+ if (hasFilter === "lines" && asList(a["lines"]).length === 0)
719
+ continue;
720
+ if (grepMatcher) {
721
+ let hay = strOf(a["content"]);
722
+ for (const line of asList(a["lines"]))
723
+ hay += "\n" + strOf(line["content"]);
724
+ if (!grepMatcher(hay))
725
+ continue;
726
+ }
727
+ out.push({ epId, sceneId, idx: i, action: a });
728
+ }
729
+ }
730
+ }
731
+ return out;
732
+ }
733
+ // Whether a selection in `opts` has any filter at all (scope or predicate).
734
+ export function selectionHasAnyFilter(opts) {
735
+ return Boolean(!parseInSelector(strOf(opts["in"])).isEmpty ||
736
+ strOf(opts["grep"]) || strOf(opts["type"]).trim() || strOf(opts["actor"]).trim() ||
737
+ strOf(opts["speaker"]).trim() || strOf(opts["has"]).trim());
738
+ }
739
+ export function listAssetsByKind(script, kind, opts) {
740
+ const lines = [];
741
+ const [key, idKey, nameKey] = (() => {
742
+ switch (kind) {
743
+ case "actor": return ["actors", "actor_id", "actor_name"];
744
+ case "location": return ["locations", "location_id", "location_name"];
745
+ case "prop": return ["props", "prop_id", "prop_name"];
746
+ }
747
+ })();
748
+ // Build the set of asset ids referenced inside the --in scope. A range or
749
+ // multi-region selector takes the union of context refs over every matched
750
+ // scene; a bare single action point (e.g. ep_001/scn_003#2) keeps the legacy
751
+ // "narrow to that one action's actor" behavior.
752
+ // WHOLE_SCRIPT ('--in *') means "all assets" — it must NOT narrow to the set
753
+ // of assets referenced in scene context (that would drop registered-but-not-
754
+ // yet-placed assets). Only a real address-region selector narrows.
755
+ let scopeIds = null;
756
+ if (!opts.selector.isEmpty && opts.selector !== WHOLE_SCRIPT) {
757
+ scopeIds = new Set();
758
+ const single = selectorSingleActionPoint(opts.selector);
759
+ const pluralKey = kind === "actor" ? "actors" : kind === "location" ? "locations" : "props";
760
+ for (const ep of asList(script["episodes"])) {
761
+ const epId = strOf(ep["episode_id"]);
762
+ for (const scene of asList(ep["scenes"])) {
763
+ const sceneId = strOf(scene["scene_id"]);
764
+ if (!opts.selector.matchesScene(epId, sceneId))
765
+ continue;
766
+ if (single) {
767
+ // Single action point: matchesScene already isolates the target
768
+ // scene. Legacy narrowing is implemented for actor only.
769
+ if (kind === "actor") {
770
+ const a = asList(scene["actions"])[single.idx];
771
+ const aid = a ? strOf(a["actor_id"]).trim() : "";
772
+ if (aid)
773
+ scopeIds.add(aid);
774
+ }
775
+ continue;
776
+ }
777
+ const ctx = isDict(scene["context"]) ? scene["context"] : {};
778
+ for (const ref of asList(ctx[pluralKey])) {
779
+ scopeIds.add(strOf(ref[idKey]));
780
+ }
781
+ }
782
+ }
783
+ }
784
+ for (const asset of asList(script[key])) {
785
+ const id = strOf(asset[idKey]);
786
+ const name = strOf(asset[nameKey]);
787
+ if (opts.id && opts.id !== id)
788
+ continue;
789
+ if (opts.name && !name.includes(opts.name))
790
+ continue;
791
+ if (scopeIds && !scopeIds.has(id))
792
+ continue;
793
+ const aliases = asList(asset["aliases"]).map((s) => strOf(s));
794
+ const role = strOf(asset["role_type"]);
795
+ const descPresent = strOf(asset["description"]).trim() ? "yes" : "missing";
796
+ const states = asList(asset["states"]).length;
797
+ const extra = [];
798
+ if (aliases.length > 0)
799
+ extra.push(`aliases=(${aliases.join(",")})`);
800
+ if (role)
801
+ extra.push(`role=${role}`);
802
+ extra.push(`states=${states}`, `description=${descPresent}`);
803
+ lines.push(`${kind} ${id}: ${name || "-"} ${extra.join(" ")}`);
804
+ }
805
+ return lines;
806
+ }
807
+ export function splitSpeakerList(raw) {
808
+ if (Array.isArray(raw))
809
+ return raw.map((v) => strOf(v)).filter((v) => v);
810
+ return strOf(raw).split(",").map((s) => s.trim()).filter((s) => s);
811
+ }
812
+ export function blankCreateReport(session, validation, idempotent) {
813
+ const passed = Boolean(validation["passed"]);
814
+ const resultLines = ["created: blank script (schema v2)", `validation: ${passed ? "passed" : "needs repair"}`];
815
+ if (session.remote) {
816
+ resultLines.push(`revision: ${session.revision}`);
817
+ if (idempotent !== null)
818
+ resultLines.push(`idempotent: ${String(idempotent).toLowerCase()}`);
819
+ }
820
+ return {
821
+ title: passed ? "SCRIPT CREATED: Blank scaffold" : "SCRIPT CREATED: Validation needs repair",
822
+ op: "script.create",
823
+ changed: true,
824
+ summary: "已创建空白剧本(schema v2,待填充人物/地点/道具/分集)",
825
+ result: resultLines,
826
+ warnings: summarizeIssues(asList(validation["issues"])),
827
+ artifacts: [session.artifactLabel],
828
+ next: [
829
+ "用 worldview 设定世界观,用 add-actor / add-location / add-prop 注册资产,用 add-episode + insert 搭出分集与场景。",
830
+ ],
831
+ };
832
+ }
833
+ // ----- add-* (register new entities) ----------------------------------------
834
+ // Read the single positional <name> arg shared by add-actor/location/prop, or
835
+ // throw the common ARGS_MISSING usage error with a verb-specific example.
836
+ export function requireNameArg(opts, verb, example) {
837
+ const args = asList(opts["_args"]);
838
+ if (args.length < 1) {
839
+ throw new CliError(`VERB BLOCKED: ${verb} name missing`, "Name missing.", {
840
+ exitCode: EXIT_USAGE,
841
+ required: ["<name>"],
842
+ received: ["<empty>"],
843
+ nextSteps: [example],
844
+ errorCode: "ARGS_MISSING",
845
+ });
846
+ }
847
+ return args[0];
848
+ }
849
+ //# sourceMappingURL=lib.js.map