@lingjingai/scriptctl 0.50.0 → 0.51.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 (270) hide show
  1. package/changes/0.51.0.md +24 -0
  2. package/changes/unreleased.md +1 -2
  3. package/dist/cli/register-batch.d.ts +3 -0
  4. package/dist/cli/register-batch.js +28 -0
  5. package/dist/cli/register-batch.js.map +1 -0
  6. package/dist/cli/register-edits.d.ts +4 -0
  7. package/dist/cli/register-edits.js +226 -0
  8. package/dist/cli/register-edits.js.map +1 -0
  9. package/dist/cli/register-query.d.ts +4 -0
  10. package/dist/cli/register-query.js +93 -0
  11. package/dist/cli/register-query.js.map +1 -0
  12. package/dist/cli/register-tools.d.ts +3 -0
  13. package/dist/cli/register-tools.js +23 -0
  14. package/dist/cli/register-tools.js.map +1 -0
  15. package/dist/cli/register-workflows.d.ts +4 -0
  16. package/dist/cli/register-workflows.js +71 -0
  17. package/dist/cli/register-workflows.js.map +1 -0
  18. package/dist/cli/registry.d.ts +14 -0
  19. package/dist/cli/registry.js +65 -0
  20. package/dist/cli/registry.js.map +1 -0
  21. package/dist/cli.d.ts +3 -0
  22. package/dist/cli.js +146 -596
  23. package/dist/cli.js.map +1 -1
  24. package/dist/common.d.ts +12 -38
  25. package/dist/common.js +5 -11
  26. package/dist/common.js.map +1 -1
  27. package/dist/domain/authoring/shared/bracket.js +3 -3
  28. package/dist/domain/authoring/shared/bracket.js.map +1 -1
  29. package/dist/domain/authoring/v3/apply.js +3 -3
  30. package/dist/domain/authoring/v3/apply.js.map +1 -1
  31. package/dist/domain/authoring/v4/apply.js +6 -6
  32. package/dist/domain/authoring/v4/apply.js.map +1 -1
  33. package/dist/domain/authoring/v4/parser.js +2 -2
  34. package/dist/domain/authoring/v4/parser.js.map +1 -1
  35. package/dist/domain/authoring/v4/serializer.js +2 -2
  36. package/dist/domain/authoring/v4/serializer.js.map +1 -1
  37. package/dist/domain/script/dispatch.js +5 -7
  38. package/dist/domain/script/dispatch.js.map +1 -1
  39. package/dist/domain/script/shared.js +4 -6
  40. package/dist/domain/script/shared.js.map +1 -1
  41. package/dist/domain/script/v3/address.js +29 -38
  42. package/dist/domain/script/v3/address.js.map +1 -1
  43. package/dist/domain/script/v3/lookups.js +31 -32
  44. package/dist/domain/script/v3/lookups.js.map +1 -1
  45. package/dist/domain/script/v3/ordinal.js +4 -5
  46. package/dist/domain/script/v3/ordinal.js.map +1 -1
  47. package/dist/domain/script/v3/parse.js +3 -4
  48. package/dist/domain/script/v3/parse.js.map +1 -1
  49. package/dist/domain/script/v3/patch/helpers.js +6 -8
  50. package/dist/domain/script/v3/patch/helpers.js.map +1 -1
  51. package/dist/domain/script/v3/selector.js +6 -8
  52. package/dist/domain/script/v3/selector.js.map +1 -1
  53. package/dist/domain/script/v4/address.js +2 -3
  54. package/dist/domain/script/v4/address.js.map +1 -1
  55. package/dist/domain/script/v4/ids.js +6 -7
  56. package/dist/domain/script/v4/ids.js.map +1 -1
  57. package/dist/domain/script/v4/ordinal.js +4 -5
  58. package/dist/domain/script/v4/ordinal.js.map +1 -1
  59. package/dist/domain/script/v4/parse.js +3 -4
  60. package/dist/domain/script/v4/parse.js.map +1 -1
  61. package/dist/domain/script/v4/patch/apply.js +3 -3
  62. package/dist/domain/script/v4/patch/apply.js.map +1 -1
  63. package/dist/domain/script/v4/patch/helpers.js +3 -4
  64. package/dist/domain/script/v4/patch/helpers.js.map +1 -1
  65. package/dist/domain/script/v4/patch/ops-action.js +46 -49
  66. package/dist/domain/script/v4/patch/ops-action.js.map +1 -1
  67. package/dist/domain/script/v4/selector.js +2 -3
  68. package/dist/domain/script/v4/selector.js.map +1 -1
  69. package/dist/infra/ingest/video-discovery.js +7 -4
  70. package/dist/infra/ingest/video-discovery.js.map +1 -1
  71. package/dist/infra/script-output-store.js +6 -11
  72. package/dist/infra/script-output-store.js.map +1 -1
  73. package/dist/infra/script-working-copy.js +15 -16
  74. package/dist/infra/script-working-copy.js.map +1 -1
  75. package/dist/llm/config.js +3 -4
  76. package/dist/llm/config.js.map +1 -1
  77. package/dist/llm/router.js +6 -8
  78. package/dist/llm/router.js.map +1 -1
  79. package/dist/llm/tasks/ingest/text-normalize.js +3 -3
  80. package/dist/llm/tasks/ingest/text-normalize.js.map +1 -1
  81. package/dist/llm/tasks/ingest/video-serial.js +3 -3
  82. package/dist/llm/tasks/ingest/video-serial.js.map +1 -1
  83. package/dist/output.d.ts +2 -3
  84. package/dist/output.js +72 -109
  85. package/dist/output.js.map +1 -1
  86. package/dist/usecases/changelog.d.ts +2 -2
  87. package/dist/usecases/changelog.js +28 -36
  88. package/dist/usecases/changelog.js.map +1 -1
  89. package/dist/usecases/doctor.d.ts +2 -2
  90. package/dist/usecases/doctor.js +3 -5
  91. package/dist/usecases/doctor.js.map +1 -1
  92. package/dist/usecases/ingest/command.d.ts +2 -2
  93. package/dist/usecases/ingest/command.js +22 -5
  94. package/dist/usecases/ingest/command.js.map +1 -1
  95. package/dist/usecases/ingest/pass5-extract.js +5 -4
  96. package/dist/usecases/ingest/pass5-extract.js.map +1 -1
  97. package/dist/usecases/ingest/pass5-govern.js +13 -11
  98. package/dist/usecases/ingest/pass5-govern.js.map +1 -1
  99. package/dist/usecases/ingest/pipeline.js +19 -16
  100. package/dist/usecases/ingest/pipeline.js.map +1 -1
  101. package/dist/usecases/ingest/serial-govern.js +5 -4
  102. package/dist/usecases/ingest/serial-govern.js.map +1 -1
  103. package/dist/usecases/ingest/status.d.ts +2 -2
  104. package/dist/usecases/ingest/status.js +1 -1
  105. package/dist/usecases/ingest/status.js.map +1 -1
  106. package/dist/usecases/ingest/video-pipeline.js +22 -16
  107. package/dist/usecases/ingest/video-pipeline.js.map +1 -1
  108. package/dist/usecases/ingest/view-command.d.ts +2 -2
  109. package/dist/usecases/ingest/view-command.js +7 -6
  110. package/dist/usecases/ingest/view-command.js.map +1 -1
  111. package/dist/usecases/ingest/view-server.d.ts +2 -2
  112. package/dist/usecases/ingest/view-server.js +7 -5
  113. package/dist/usecases/ingest/view-server.js.map +1 -1
  114. package/dist/usecases/script/actions.d.ts +2 -2
  115. package/dist/usecases/script/actions.js +12 -13
  116. package/dist/usecases/script/actions.js.map +1 -1
  117. package/dist/usecases/script/actor.d.ts +2 -2
  118. package/dist/usecases/script/actor.js +3 -4
  119. package/dist/usecases/script/actor.js.map +1 -1
  120. package/dist/usecases/script/actors.d.ts +2 -2
  121. package/dist/usecases/script/actors.js +5 -5
  122. package/dist/usecases/script/actors.js.map +1 -1
  123. package/dist/usecases/script/add-actor.d.ts +2 -2
  124. package/dist/usecases/script/add-episode.d.ts +2 -2
  125. package/dist/usecases/script/add-location.d.ts +2 -2
  126. package/dist/usecases/script/add-prop.d.ts +2 -2
  127. package/dist/usecases/script/alias.d.ts +2 -2
  128. package/dist/usecases/script/alias.js +6 -8
  129. package/dist/usecases/script/alias.js.map +1 -1
  130. package/dist/usecases/script/assets.d.ts +2 -2
  131. package/dist/usecases/script/assets.js +15 -7
  132. package/dist/usecases/script/assets.js.map +1 -1
  133. package/dist/usecases/script/audit-assets.d.ts +2 -2
  134. package/dist/usecases/script/audit-assets.js +12 -15
  135. package/dist/usecases/script/audit-assets.js.map +1 -1
  136. package/dist/usecases/script/command-constraints.js +17 -18
  137. package/dist/usecases/script/command-constraints.js.map +1 -1
  138. package/dist/usecases/script/context.d.ts +2 -2
  139. package/dist/usecases/script/context.js +3 -4
  140. package/dist/usecases/script/context.js.map +1 -1
  141. package/dist/usecases/script/create.d.ts +2 -2
  142. package/dist/usecases/script/create.js +3 -4
  143. package/dist/usecases/script/create.js.map +1 -1
  144. package/dist/usecases/script/delete.d.ts +2 -2
  145. package/dist/usecases/script/delete.js +9 -12
  146. package/dist/usecases/script/delete.js.map +1 -1
  147. package/dist/usecases/script/describe.d.ts +2 -2
  148. package/dist/usecases/script/describe.js +6 -8
  149. package/dist/usecases/script/describe.js.map +1 -1
  150. package/dist/usecases/script/dialogue.d.ts +2 -2
  151. package/dist/usecases/script/dialogue.js +5 -6
  152. package/dist/usecases/script/dialogue.js.map +1 -1
  153. package/dist/usecases/script/do.d.ts +2 -2
  154. package/dist/usecases/script/do.js +63 -66
  155. package/dist/usecases/script/do.js.map +1 -1
  156. package/dist/usecases/script/emotion.d.ts +2 -2
  157. package/dist/usecases/script/emotion.js +3 -3
  158. package/dist/usecases/script/emotion.js.map +1 -1
  159. package/dist/usecases/script/episodes.d.ts +2 -2
  160. package/dist/usecases/script/episodes.js +5 -5
  161. package/dist/usecases/script/episodes.js.map +1 -1
  162. package/dist/usecases/script/export-authoring.d.ts +2 -2
  163. package/dist/usecases/script/export-authoring.js +26 -29
  164. package/dist/usecases/script/export-authoring.js.map +1 -1
  165. package/dist/usecases/script/extend.d.ts +2 -2
  166. package/dist/usecases/script/extend.js +22 -18
  167. package/dist/usecases/script/extend.js.map +1 -1
  168. package/dist/usecases/script/ids.d.ts +2 -2
  169. package/dist/usecases/script/ids.js +21 -17
  170. package/dist/usecases/script/ids.js.map +1 -1
  171. package/dist/usecases/script/importance.d.ts +2 -2
  172. package/dist/usecases/script/importance.js +3 -4
  173. package/dist/usecases/script/importance.js.map +1 -1
  174. package/dist/usecases/script/insert.d.ts +2 -2
  175. package/dist/usecases/script/insert.js +8 -10
  176. package/dist/usecases/script/insert.js.map +1 -1
  177. package/dist/usecases/script/issues.d.ts +2 -2
  178. package/dist/usecases/script/issues.js +10 -2
  179. package/dist/usecases/script/issues.js.map +1 -1
  180. package/dist/usecases/script/lib.d.ts +15 -9
  181. package/dist/usecases/script/lib.js +226 -166
  182. package/dist/usecases/script/lib.js.map +1 -1
  183. package/dist/usecases/script/locations.d.ts +2 -2
  184. package/dist/usecases/script/locations.js +5 -5
  185. package/dist/usecases/script/locations.js.map +1 -1
  186. package/dist/usecases/script/merge.d.ts +2 -2
  187. package/dist/usecases/script/merge.js +9 -12
  188. package/dist/usecases/script/merge.js.map +1 -1
  189. package/dist/usecases/script/move.d.ts +2 -2
  190. package/dist/usecases/script/move.js +6 -8
  191. package/dist/usecases/script/move.js.map +1 -1
  192. package/dist/usecases/script/parenthetical.d.ts +2 -2
  193. package/dist/usecases/script/parenthetical.js +9 -12
  194. package/dist/usecases/script/parenthetical.js.map +1 -1
  195. package/dist/usecases/script/props.d.ts +2 -2
  196. package/dist/usecases/script/props.js +5 -5
  197. package/dist/usecases/script/props.js.map +1 -1
  198. package/dist/usecases/script/refs.d.ts +2 -2
  199. package/dist/usecases/script/refs.js +23 -17
  200. package/dist/usecases/script/refs.js.map +1 -1
  201. package/dist/usecases/script/rename.d.ts +2 -2
  202. package/dist/usecases/script/rename.js +6 -8
  203. package/dist/usecases/script/rename.js.map +1 -1
  204. package/dist/usecases/script/replace.d.ts +2 -2
  205. package/dist/usecases/script/replace.js +6 -8
  206. package/dist/usecases/script/replace.js.map +1 -1
  207. package/dist/usecases/script/role.d.ts +2 -2
  208. package/dist/usecases/script/role.js +3 -4
  209. package/dist/usecases/script/role.js.map +1 -1
  210. package/dist/usecases/script/scenes.d.ts +2 -2
  211. package/dist/usecases/script/scenes.js +7 -5
  212. package/dist/usecases/script/scenes.js.map +1 -1
  213. package/dist/usecases/script/script-patch.d.ts +2 -2
  214. package/dist/usecases/script/script-patch.js +95 -112
  215. package/dist/usecases/script/script-patch.js.map +1 -1
  216. package/dist/usecases/script/script-validate.d.ts +2 -2
  217. package/dist/usecases/script/script-validate.js +11 -7
  218. package/dist/usecases/script/script-validate.js.map +1 -1
  219. package/dist/usecases/script/session.js +52 -63
  220. package/dist/usecases/script/session.js.map +1 -1
  221. package/dist/usecases/script/split.d.ts +2 -2
  222. package/dist/usecases/script/split.js +6 -8
  223. package/dist/usecases/script/split.js.map +1 -1
  224. package/dist/usecases/script/state-add.d.ts +2 -2
  225. package/dist/usecases/script/state-add.js +3 -4
  226. package/dist/usecases/script/state-add.js.map +1 -1
  227. package/dist/usecases/script/state-delete.d.ts +2 -2
  228. package/dist/usecases/script/state-delete.js +3 -4
  229. package/dist/usecases/script/state-delete.js.map +1 -1
  230. package/dist/usecases/script/state-rename.d.ts +2 -2
  231. package/dist/usecases/script/state-rename.js +3 -4
  232. package/dist/usecases/script/state-rename.js.map +1 -1
  233. package/dist/usecases/script/states.d.ts +2 -2
  234. package/dist/usecases/script/states.js +54 -27
  235. package/dist/usecases/script/states.js.map +1 -1
  236. package/dist/usecases/script/summary.d.ts +2 -2
  237. package/dist/usecases/script/summary.js +21 -2
  238. package/dist/usecases/script/summary.js.map +1 -1
  239. package/dist/usecases/script/synopsis-generate.d.ts +2 -2
  240. package/dist/usecases/script/synopsis-generate.js +38 -39
  241. package/dist/usecases/script/synopsis-generate.js.map +1 -1
  242. package/dist/usecases/script/synopsis.d.ts +2 -2
  243. package/dist/usecases/script/synopsis.js +9 -12
  244. package/dist/usecases/script/synopsis.js.map +1 -1
  245. package/dist/usecases/script/transition.d.ts +2 -2
  246. package/dist/usecases/script/transition.js +3 -4
  247. package/dist/usecases/script/transition.js.map +1 -1
  248. package/dist/usecases/script/type.d.ts +2 -2
  249. package/dist/usecases/script/type.js +3 -4
  250. package/dist/usecases/script/type.js.map +1 -1
  251. package/dist/usecases/script/v4/queries.d.ts +3 -2
  252. package/dist/usecases/script/v4/queries.js +43 -6
  253. package/dist/usecases/script/v4/queries.js.map +1 -1
  254. package/dist/usecases/script/working-copy.d.ts +4 -4
  255. package/dist/usecases/script/working-copy.js +96 -92
  256. package/dist/usecases/script/working-copy.js.map +1 -1
  257. package/dist/usecases/script/worldview.d.ts +2 -2
  258. package/dist/usecases/script/worldview.js +3 -4
  259. package/dist/usecases/script/worldview.js.map +1 -1
  260. package/dist/usecases/script/write-authoring.d.ts +2 -2
  261. package/dist/usecases/script/write-authoring.js +62 -81
  262. package/dist/usecases/script/write-authoring.js.map +1 -1
  263. package/dist/usecases/update.d.ts +2 -2
  264. package/dist/usecases/update.js +32 -47
  265. package/dist/usecases/update.js.map +1 -1
  266. package/package.json +1 -1
  267. package/skills/scriptctl/SKILL.md +8 -2
  268. package/dist/help-text.d.ts +0 -2
  269. package/dist/help-text.js +0 -1144
  270. package/dist/help-text.js.map +0 -1
@@ -155,12 +155,11 @@ export function parseDoLine(tokens) {
155
155
  return { verb, args, flags };
156
156
  }
157
157
  export function doErr(detail) {
158
- return new CliError("DO BLOCKED: line invalid", detail, {
158
+ return new CliError(detail, {
159
159
  exitCode: EXIT_USAGE,
160
- required: ["a valid <verb> ...args... line"],
161
- received: [detail],
162
- nextSteps: ["Each line is a scriptctl verb minus the leading `scriptctl`; see `scriptctl do --help`."],
163
160
  errorCode: "DO_LINE_INVALID",
161
+ hint: "Each line is a scriptctl verb minus the leading `scriptctl`; see `scriptctl do --help`.",
162
+ data: { expected: ["a valid <verb> ...args... line"], actual: [detail] },
164
163
  });
165
164
  }
166
165
  // ---- shared op-template builders ------------------------------------------
@@ -172,36 +171,46 @@ export function stateChangeTemplate(target, f) {
172
171
  const toSet = f.to !== undefined;
173
172
  const clearSet = Boolean(f.clear);
174
173
  if (toSet && clearSet) {
175
- throw new CliError("VERB BLOCKED: state-change flags mutually exclusive", "state-change: --to and --clear are mutually exclusive.", {
176
- exitCode: EXIT_USAGE, required: ["exactly one of --to <state> / --clear"], received: ["--to", "--clear"],
177
- nextSteps: ["Pick one and retry."], errorCode: "STATE_CHANGE_FLAG_CONFLICT",
174
+ throw new CliError("state-change: --to and --clear are mutually exclusive.", {
175
+ exitCode: EXIT_USAGE,
176
+ errorCode: "STATE_CHANGE_FLAG_CONFLICT",
177
+ hint: "Pick one and retry.",
178
+ data: { expected: ["exactly one of --to <state> / --clear"], actual: ["--to", "--clear"] },
178
179
  });
179
180
  }
180
181
  if (!toSet && !clearSet) {
181
- throw new CliError("VERB BLOCKED: state-change flag missing", "state-change: must specify --to <state> or --clear.", {
182
- exitCode: EXIT_USAGE, required: ["one of --to <state> / --clear"], received: ["<missing>"],
183
- nextSteps: ["Pick exactly one flag."], errorCode: "STATE_CHANGE_FLAG_MISSING",
182
+ throw new CliError("state-change: must specify --to <state> or --clear.", {
183
+ exitCode: EXIT_USAGE,
184
+ errorCode: "STATE_CHANGE_FLAG_MISSING",
185
+ hint: "Pick exactly one flag.",
186
+ data: { expected: ["one of --to <state> / --clear"], actual: ["<missing>"] },
184
187
  });
185
188
  }
186
189
  // v3 has no per-action state_changes; state lives on scene.ref.* only.
187
- throw new CliError("VERB BLOCKED: state-change removed", "state-change was removed in v3; set a scene ref's state instead.", {
188
- exitCode: EXIT_USAGE, required: ["scene-ref state edit"], received: ["state-change"],
189
- nextSteps: ["Use `scene-ref <ep/scn> <kind:id> --state <state_id>` (op scene.ref.set)."], errorCode: "STATE_CHANGE_REMOVED",
190
+ throw new CliError("state-change was removed in v3; set a scene ref's state instead.", {
191
+ exitCode: EXIT_USAGE,
192
+ errorCode: "STATE_CHANGE_REMOVED",
193
+ hint: "Use `scene-ref <ep/scn> <kind:id> --state <state_id>` (op scene.ref.set).",
194
+ data: { expected: ["scene-ref state edit"], actual: ["state-change"] },
190
195
  });
191
196
  }
192
197
  export function transitionTemplate(target, f) {
193
198
  const setBoth = f.process !== undefined || f.contrast !== undefined;
194
199
  const clearSet = Boolean(f.clear);
195
200
  if (setBoth && clearSet) {
196
- throw new CliError("VERB BLOCKED: transition flags mutually exclusive", "transition: --clear is mutually exclusive with --process/--contrast.", {
197
- exitCode: EXIT_USAGE, required: ["either --clear, or --process + --contrast"], received: ["--clear", "--process/--contrast"],
198
- nextSteps: ["Pick one mode and retry."], errorCode: "TRANSITION_FLAG_CONFLICT",
201
+ throw new CliError("transition: --clear is mutually exclusive with --process/--contrast.", {
202
+ exitCode: EXIT_USAGE,
203
+ errorCode: "TRANSITION_FLAG_CONFLICT",
204
+ hint: "Pick one mode and retry.",
205
+ data: { expected: ["either --clear, or --process + --contrast"], actual: ["--clear", "--process/--contrast"] },
199
206
  });
200
207
  }
201
208
  if (!setBoth && !clearSet) {
202
- throw new CliError("VERB BLOCKED: transition flag missing", "transition: must specify --process/--contrast or --clear.", {
203
- exitCode: EXIT_USAGE, required: ["--process + --contrast, or --clear"], received: ["<missing>"],
204
- nextSteps: ["Pick exactly one mode."], errorCode: "TRANSITION_FLAG_MISSING",
209
+ throw new CliError("transition: must specify --process/--contrast or --clear.", {
210
+ exitCode: EXIT_USAGE,
211
+ errorCode: "TRANSITION_FLAG_MISSING",
212
+ hint: "Pick exactly one mode.",
213
+ data: { expected: ["--process + --contrast, or --clear"], actual: ["<missing>"] },
205
214
  });
206
215
  }
207
216
  return clearSet
@@ -212,15 +221,19 @@ export function emotionTemplate(f) {
212
221
  const emotionSet = f.emotion !== undefined;
213
222
  const clearSet = Boolean(f.clear);
214
223
  if (emotionSet && clearSet) {
215
- throw new CliError("VERB BLOCKED: emotion flags mutually exclusive", "emotion: value and --clear are mutually exclusive.", {
216
- exitCode: EXIT_USAGE, required: ["either <emotion>, or --clear"], received: ["<emotion>", "--clear"],
217
- nextSteps: ["Pick one mode and retry."], errorCode: "EMOTION_FLAG_CONFLICT",
224
+ throw new CliError("emotion: value and --clear are mutually exclusive.", {
225
+ exitCode: EXIT_USAGE,
226
+ errorCode: "EMOTION_FLAG_CONFLICT",
227
+ hint: "Pick one mode and retry.",
228
+ data: { expected: ["either <emotion>, or --clear"], actual: ["<emotion>", "--clear"] },
218
229
  });
219
230
  }
220
231
  if (!emotionSet && !clearSet) {
221
- throw new CliError("VERB BLOCKED: emotion value missing", "emotion: must specify <emotion> or --clear.", {
222
- exitCode: EXIT_USAGE, required: ["<emotion>, or --clear"], received: ["<missing>"],
223
- nextSteps: ["Pass an emotion label, or use --clear."], errorCode: "EMOTION_VALUE_MISSING",
232
+ throw new CliError("emotion: must specify <emotion> or --clear.", {
233
+ exitCode: EXIT_USAGE,
234
+ errorCode: "EMOTION_VALUE_MISSING",
235
+ hint: "Pass an emotion label, or use --clear.",
236
+ data: { expected: ["<emotion>, or --clear"], actual: ["<missing>"] },
224
237
  });
225
238
  }
226
239
  return clearSet
@@ -233,9 +246,11 @@ export function contextTemplate(target, f) {
233
246
  const removeSet = Boolean(f.remove);
234
247
  const n = (stateSet ? 1 : 0) + (clearSet ? 1 : 0) + (removeSet ? 1 : 0);
235
248
  if (n === 0) {
236
- throw new CliError("VERB BLOCKED: context flag missing", "context: must specify --state X, --clear, or --remove.", {
237
- exitCode: EXIT_USAGE, required: ["one of --state <state_id> / --clear / --remove"], received: ["<missing>"],
238
- nextSteps: ["Pick exactly one flag."], errorCode: "CONTEXT_FLAG_MISSING",
249
+ throw new CliError("context: must specify --state X, --clear, or --remove.", {
250
+ exitCode: EXIT_USAGE,
251
+ errorCode: "CONTEXT_FLAG_MISSING",
252
+ hint: "Pick exactly one flag.",
253
+ data: { expected: ["one of --state <state_id> / --clear / --remove"], actual: ["<missing>"] },
239
254
  });
240
255
  }
241
256
  if (n > 1) {
@@ -246,30 +261,30 @@ export function contextTemplate(target, f) {
246
261
  passed.push("--clear");
247
262
  if (removeSet)
248
263
  passed.push("--remove");
249
- throw new CliError("VERB BLOCKED: context flags mutually exclusive", "context: --state / --clear / --remove are mutually exclusive.", {
250
- exitCode: EXIT_USAGE, required: ["exactly one of --state / --clear / --remove"], received: passed,
251
- nextSteps: ["Pick one and retry."], errorCode: "CONTEXT_FLAG_CONFLICT",
264
+ throw new CliError("context: --state / --clear / --remove are mutually exclusive.", {
265
+ exitCode: EXIT_USAGE,
266
+ errorCode: "CONTEXT_FLAG_CONFLICT",
267
+ hint: "Pick one and retry.",
268
+ data: { expected: ["exactly one of --state / --clear / --remove"], actual: passed },
252
269
  });
253
270
  }
254
271
  const append = Boolean(f.append);
255
272
  const occurrenceRaw = strOf(f.occurrence).trim();
256
273
  const occurrence = occurrenceRaw ? Number(occurrenceRaw) : undefined;
257
274
  if (occurrence !== undefined && (!Number.isInteger(occurrence) || occurrence < 1)) {
258
- throw new CliError("VERB BLOCKED: scene-ref occurrence invalid", "scene-ref: --occurrence must be a positive integer.", {
275
+ throw new CliError("scene-ref: --occurrence must be a positive integer.", {
259
276
  exitCode: EXIT_USAGE,
260
- required: ["--occurrence <positive integer> (1-based)"],
261
- received: [occurrenceRaw],
262
- nextSteps: ["Use 1 for the first matching scene ref, 2 for the second, and so on."],
263
277
  errorCode: "SCENE_REF_OCCURRENCE_INVALID",
278
+ hint: "Use 1 for the first matching scene ref, 2 for the second, and so on.",
279
+ data: { expected: ["--occurrence <positive integer> (1-based)"], actual: [occurrenceRaw] },
264
280
  });
265
281
  }
266
282
  if (append && (!stateSet || occurrence !== undefined)) {
267
- throw new CliError("VERB BLOCKED: scene-ref append flags invalid", "scene-ref: --append requires --state and cannot be combined with --occurrence.", {
283
+ throw new CliError("scene-ref: --append requires --state and cannot be combined with --occurrence.", {
268
284
  exitCode: EXIT_USAGE,
269
- required: ["--state <state_id> --append"],
270
- received: [append ? "--append" : "", occurrence !== undefined ? "--occurrence" : "", clearSet ? "--clear" : "", removeSet ? "--remove" : ""].filter(Boolean),
271
- nextSteps: ["Append a new actor ref with --state <state_id> --append, or target an existing ref with --occurrence <n>."],
272
285
  errorCode: "SCENE_REF_APPEND_FLAG_CONFLICT",
286
+ hint: "Append a new actor ref with --state <state_id> --append, or target an existing ref with --occurrence <n>.",
287
+ data: { expected: ["--state <state_id> --append"], actual: [append ? "--append" : "", occurrence !== undefined ? "--occurrence" : "", clearSet ? "--clear" : "", removeSet ? "--remove" : ""].filter(Boolean) },
273
288
  });
274
289
  }
275
290
  if (removeSet)
@@ -285,30 +300,30 @@ export function dialogueTemplate(f) {
285
300
  const speakerKind = strOf(f.kind).trim();
286
301
  const label = strOf(f.label).trim();
287
302
  if (actorId && speakerKind) {
288
- throw new CliError("VERB BLOCKED: dialogue speaker ambiguous", "dialogue: --actor and --kind are mutually exclusive.", {
303
+ throw new CliError("dialogue: --actor and --kind are mutually exclusive.", {
289
304
  exitCode: EXIT_USAGE,
290
- required: ["exactly one of --actor <actor_id> or --kind <non-actor kind>"],
291
- received: ["--actor", "--kind"],
292
- nextSteps: ["Keep --actor for a character, or --kind for a non-actor source."],
293
305
  errorCode: "SPEAKER_AMBIGUOUS",
306
+ hint: "Keep --actor for a character, or --kind for a non-actor source.",
307
+ data: { expected: ["exactly one of --actor <actor_id> or --kind <non-actor kind>"], actual: ["--actor", "--kind"] },
294
308
  });
295
309
  }
296
310
  if (actorId && label) {
297
- throw new CliError("VERB BLOCKED: dialogue speaker label misplaced", "dialogue: --label is only valid with --kind.", {
311
+ throw new CliError("dialogue: --label is only valid with --kind.", {
298
312
  exitCode: EXIT_USAGE,
299
- required: ["omit --label with --actor, or pair --label with --kind"],
300
- received: ["--actor", "--label"],
301
- nextSteps: ["Remove --label for an actor line, or use --kind for a labeled non-actor source."],
302
313
  errorCode: "SPEAKER_LABEL_WITHOUT_KIND",
314
+ hint: "Remove --label for an actor line, or use --kind for a labeled non-actor source.",
315
+ data: { expected: ["omit --label with --actor, or pair --label with --kind"], actual: ["--actor", "--label"] },
303
316
  });
304
317
  }
305
318
  if (actorId)
306
319
  return { op: "dialogue.speaker.set", actor_id: actorId };
307
320
  if (speakerKind)
308
321
  return { op: "dialogue.speaker.set", kind: speakerKind, label: label || undefined };
309
- throw new CliError("VERB BLOCKED: dialogue speaker missing", "dialogue: pass --actor <id> or --kind <system|broadcast|offscreen|group>.", {
310
- exitCode: EXIT_USAGE, required: ["--actor <actor_id> or --kind <non-actor kind>"], received: ["<missing>"],
311
- nextSteps: ["Set --actor for an actor line, or --kind for a non-actor line."], errorCode: "DIALOGUE_SPEAKER_MISSING",
322
+ throw new CliError("dialogue: pass --actor <id> or --kind <system|broadcast|offscreen|group>.", {
323
+ exitCode: EXIT_USAGE,
324
+ errorCode: "DIALOGUE_SPEAKER_MISSING",
325
+ hint: "Set --actor for an actor line, or --kind for a non-actor line.",
326
+ data: { expected: ["--actor <actor_id> or --kind <non-actor kind>"], actual: ["<missing>"] },
312
327
  });
313
328
  }
314
329
  // Translate one parsed verb line into a patch op dict (the SAME ops `patch`
@@ -863,10 +878,10 @@ function v4OperationTargets(operation) {
863
878
  }
864
879
  export function buildV4MutationPlan(session, operations, options = {}) {
865
880
  if (session.version !== 4) {
866
- throw new CliError("SCRIPT PLAN BLOCKED: wrong version", "A v4 mutation plan requires Script v4.", {
881
+ throw new CliError("A v4 mutation plan requires Script v4.", {
867
882
  exitCode: EXIT_USAGE,
868
883
  errorCode: "V4_PLAN_VERSION_INVALID",
869
- received: [`Script v${session.version}`],
884
+ data: { actual: [`Script v${session.version}`] },
870
885
  });
871
886
  }
872
887
  const original = requireScriptV4(session.script);
@@ -954,12 +969,11 @@ export function assertV4PlanHasExplicitNewIds(operations) {
954
969
  if (!field || strOf(operation[field]).trim())
955
970
  continue;
956
971
  const idKind = field === "new_scene_id" ? "scene" : field.replace(/_id$/, "");
957
- throw new CliError("SCRIPT V4 PLAN BLOCKED: new ID missing", "Planned creation operations require an explicit candidate ID.", {
972
+ throw new CliError("Planned creation operations require an explicit candidate ID.", {
958
973
  exitCode: EXIT_USAGE,
959
974
  errorCode: "V4_PLAN_ID_REQUIRED",
960
- required: [`${kind}.${field}`],
961
- received: [`${kind}.${field}=<missing>`],
962
- nextSteps: [`Generate a candidate with \`scriptctl ids --kind ${idKind} --count 1\`, then include it in the operation.`],
975
+ hint: `Generate a candidate with \`scriptctl ids --kind ${idKind} --count 1\`, then include it in the operation.`,
976
+ data: { expected: [`${kind}.${field}`], actual: [`${kind}.${field}=<missing>`] },
963
977
  });
964
978
  }
965
979
  }
@@ -969,11 +983,11 @@ export function assertV4PlanHasExplicitNewIds(operations) {
969
983
  export async function applySingleScriptOpToSession(session, opts, op) {
970
984
  const plan = strOf(opts["plan"]).trim();
971
985
  if (plan) {
972
- throw new CliError("SCRIPT OP BLOCKED: --plan is not applicable", "Single-point writes do not use plan digests.", {
986
+ throw new CliError("Single-point writes do not use plan digests.", {
973
987
  exitCode: EXIT_USAGE,
974
988
  errorCode: "MULTI_PLAN_USAGE_INVALID",
975
- received: [plan],
976
- nextSteps: ["Remove --plan from this single-point edit."],
989
+ hint: "Remove --plan from this single-point edit.",
990
+ data: { actual: [plan] },
977
991
  });
978
992
  }
979
993
  return applyScriptOpsToSession(session, opts, [op]);
@@ -989,17 +1003,18 @@ export async function applyScriptOpsToSession(session, opts, operations) {
989
1003
  // One outcome line: what changed + revision (remote) + validation verdict.
990
1004
  const rev = session.remote ? `; rev ${newRevision}` : "";
991
1005
  const resultLine = `${applied.join(", ")} applied${rev}; validation ${passed ? "passed" : "needs repair"}`;
1006
+ const issues = summarizeIssues(asList(validation["issues"]));
992
1007
  const report = {
993
- title: passed ? "SCRIPT OP APPLIED" : "SCRIPT OP APPLIED: Validation needs repair",
994
- op: opName,
995
- changed: applied.length > 0,
996
- ...(session.remote ? { idempotent } : {}),
997
- warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
998
- result: [resultLine],
999
- issues: summarizeIssues(asList(validation["issues"])),
1000
- artifacts: [session.artifactLabel],
1001
- // Success carries no guidance; only a failed validation needs a next step.
1002
- next: passed ? undefined : ["Inspect issues and apply another patch."],
1008
+ text: [resultLine, ...issues].join("\n"),
1009
+ data: {
1010
+ op: opName,
1011
+ changed: applied.length > 0,
1012
+ revision: session.remote ? newRevision : undefined,
1013
+ idempotent: session.remote ? idempotent : undefined,
1014
+ validation,
1015
+ issues,
1016
+ script_path: session.artifactLabel,
1017
+ },
1003
1018
  };
1004
1019
  return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
1005
1020
  }
@@ -1010,32 +1025,32 @@ export async function applySingleScriptOp(opts, op) {
1010
1025
  export function summarizeScriptOp(_script, op) {
1011
1026
  const kind = strOf(op["op"]);
1012
1027
  if (kind === "actor.add")
1013
- return `已新增人物 ${op["name"] || op["actor_name"]}${op["role_type"] ? `(${op["role_type"]})` : ""}`;
1028
+ return `Added actor ${op["name"] || op["actor_name"]}${op["role_type"] ? ` (${op["role_type"]})` : ""}`;
1014
1029
  if (kind === "location.add")
1015
- return `已新增场景地点 ${op["name"] || op["location_name"]}`;
1030
+ return `Added location ${op["name"] || op["location_name"]}`;
1016
1031
  if (kind === "prop.add")
1017
- return `已新增道具 ${op["name"] || op["prop_name"]}`;
1032
+ return `Added prop ${op["name"] || op["prop_name"]}`;
1018
1033
  if (kind === "episode.add")
1019
- return `已新增分集${op["episode_id"] ? ` ${op["episode_id"]}` : "(自动编号)"}${op["title"] ? `:${op["title"]}` : ""}`;
1034
+ return `Added episode${op["episode_id"] ? ` ${op["episode_id"]}` : " (generated ID)"}${op["title"] ? `: ${op["title"]}` : ""}`;
1020
1035
  if (kind === "meta.synopsis.set")
1021
- return `已更新全剧梗概`;
1036
+ return "Updated script synopsis";
1022
1037
  if (kind === "episode.synopsis.set")
1023
- return `已更新分集梗概 ${op["episode_id"] ?? op["target"] ?? op["at"]}`;
1038
+ return `Updated episode synopsis ${op["episode_id"] ?? op["target"] ?? op["at"]}`;
1024
1039
  if (kind === "state.add")
1025
- return `已为 ${op["target"]} 新增状态 ${op["state_id"]}: ${op["name"] || op["state_name"]}`;
1040
+ return `Added state ${op["state_id"]}: ${op["name"] || op["state_name"]} to ${op["target"]}`;
1026
1041
  if (kind === "importance.set")
1027
- return `已将 ${op["target"]} 重要度设为 ${op["importance"]}`;
1042
+ return `Set ${op["target"]} importance to ${op["importance"]}`;
1028
1043
  if (kind === "scene.ref.set")
1029
- return `已在 ${op["at"]} 设置引用 ${op["target"]}${op["state"] ? `(状态 ${op["state"]})` : ""}`;
1044
+ return `Set reference ${op["target"]} on ${op["at"]}${op["state"] ? ` (state ${op["state"]})` : ""}`;
1030
1045
  if (kind === "extend.set")
1031
- return `已在 ${op["at"]} 设置 extend.${op["key"]}`;
1046
+ return `Set extend.${op["key"]} on ${op["at"]}`;
1032
1047
  if (kind === "extend.clear")
1033
- return op["key"] ? `已在 ${op["at"]} 删除 extend.${op["key"]}` : `已清空 ${op["at"]} 的 extend`;
1048
+ return op["key"] ? `Removed extend.${op["key"]} from ${op["at"]}` : `Cleared extend on ${op["at"]}`;
1034
1049
  if (kind.startsWith("state."))
1035
- return `已执行 ${kind}: ${op["target"]}`;
1050
+ return `Applied ${kind}: ${op["target"]}`;
1036
1051
  if (kind.startsWith("dialogue."))
1037
- return `已执行 ${kind}: ${op["at"]}`;
1038
- return `已执行 ${kind}`;
1052
+ return `Applied ${kind}: ${op["at"]}`;
1053
+ return `Applied ${kind}`;
1039
1054
  }
1040
1055
  // Accept `/re/flags` as-is; wrap a bare pattern as `/pattern/` so
1041
1056
  // `replace --regex --from '^\s*\[[^\]]*\]'` works without the user typing slashes.
@@ -1068,29 +1083,27 @@ export async function applySelectorEditsToSession(session, opts, template, verbL
1068
1083
  const planDigest = planned?.digest ?? null;
1069
1084
  const suppliedPlan = strOf(opts["plan"]).trim();
1070
1085
  if (suppliedPlan && (!multiV4 || !apply)) {
1071
- throw new CliError("SELECTOR EDIT BLOCKED: --plan is not applicable", "A plan digest is only accepted for a v4 multi-target apply.", {
1086
+ throw new CliError("A plan digest is only accepted for a v4 multi-target apply.", {
1072
1087
  exitCode: EXIT_USAGE,
1073
1088
  errorCode: "MULTI_PLAN_USAGE_INVALID",
1074
- received: [suppliedPlan],
1075
- nextSteps: ["Remove --plan, or apply the same v4 multi-target selection that produced the digest."],
1089
+ hint: "Remove --plan, or apply the same v4 multi-target selection that produced the digest.",
1090
+ data: { actual: [suppliedPlan] },
1076
1091
  });
1077
1092
  }
1078
1093
  if (multiV4 && apply && !suppliedPlan) {
1079
- throw new CliError("SELECTOR EDIT BLOCKED: plan required", "A v4 multi-target write requires its dry-run plan digest.", {
1094
+ throw new CliError("A v4 multi-target write requires its dry-run plan digest.", {
1080
1095
  exitCode: EXIT_USAGE,
1081
1096
  errorCode: "MULTI_PLAN_REQUIRED",
1082
- required: [planDigest ?? ""],
1083
- received: ["<missing>"],
1084
- nextSteps: ["Run the same command without --apply, review the full plan, then retry with `--apply --plan <digest>`."],
1097
+ hint: "Run the same command without --apply, review the full plan, then retry with `--apply --plan <digest>`.",
1098
+ data: { expected: [planDigest ?? ""], actual: ["<missing>"] },
1085
1099
  });
1086
1100
  }
1087
1101
  if (multiV4 && apply && suppliedPlan !== planDigest) {
1088
- throw new CliError("SELECTOR EDIT BLOCKED: plan mismatch", "The supplied digest does not match the current targets and operations.", {
1102
+ throw new CliError("The supplied digest does not match the current targets and operations.", {
1089
1103
  exitCode: EXIT_INPUT,
1090
1104
  errorCode: "MULTI_PLAN_MISMATCH",
1091
- required: [planDigest ?? ""],
1092
- received: [suppliedPlan],
1093
- nextSteps: ["Review the newly computed dry-run plan, then apply its exact digest."],
1105
+ hint: "Review the newly computed dry-run plan, then apply its exact digest.",
1106
+ data: { expected: [planDigest ?? ""], actual: [suppliedPlan] },
1094
1107
  });
1095
1108
  }
1096
1109
  const source = scriptSourceText(workspace);
@@ -1139,39 +1152,43 @@ export async function applySelectorEditsToSession(session, opts, template, verbL
1139
1152
  const legacyIssueLines = legacyValidation === null
1140
1153
  ? []
1141
1154
  : summarizeIssues(asList(legacyValidation["issues"]));
1155
+ const issues = plannedIssueLines ?? legacyIssueLines;
1156
+ const hint = refs.length === 0
1157
+ ? "Widen the selection with --in, --grep, or predicates."
1158
+ : planDigest
1159
+ ? `Run again with --apply --plan ${planDigest} to commit this exact plan.`
1160
+ : "Run again with --apply to commit.";
1161
+ const lines = [...blast, `validation: ${passed ? "would pass" : "would need repair"}; no write`, ...issues, hint];
1142
1162
  const report = {
1143
- title: refs.length === 0 ? "SELECTOR EDIT DRY-RUN: no match" : "SELECTOR EDIT DRY-RUN (no write)",
1144
- op: `${opName}.dry-run`,
1145
- changed: false,
1146
- result: [...blast, `validation: ${passed ? "would pass" : "would need repair"}; dry-run, no write`],
1147
- warnings: passed
1148
- ? []
1149
- : plannedIssueLines ?? legacyIssueLines,
1150
- issues: plannedIssueLines ?? legacyIssueLines,
1151
- artifacts: [session.artifactLabel],
1152
- ...(planDigest ? {
1153
- baseline: session.baselineHash,
1154
- plan_digest: planDigest,
1155
- operations,
1156
- targets: refs,
1157
- plan: planned?.plan,
1158
- } : {}),
1159
- next: [refs.length === 0
1160
- ? "Widen the selection (--in / --grep / predicates)."
1161
- : planDigest
1162
- ? `Re-run with --apply --plan ${planDigest} to commit this exact plan.`
1163
- : "Re-run with --apply to commit."],
1163
+ text: lines.join("\n"),
1164
+ data: {
1165
+ op: `${opName}.dry-run`,
1166
+ changed: false,
1167
+ matched: refs.length,
1168
+ applied,
1169
+ skipped,
1170
+ issues,
1171
+ script_path: session.artifactLabel,
1172
+ ...(planDigest ? {
1173
+ baseline: session.baselineHash,
1174
+ plan_digest: planDigest,
1175
+ operations,
1176
+ targets: refs,
1177
+ plan: planned?.plan,
1178
+ } : {}),
1179
+ },
1164
1180
  };
1165
1181
  return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
1166
1182
  }
1167
1183
  if (refs.length === 0) {
1168
1184
  return [{
1169
- title: "SELECTOR EDIT: no match",
1170
- op: opName,
1171
- changed: false,
1172
- result: blast,
1173
- artifacts: [session.artifactLabel],
1174
- next: ["Widen the selection (--in / --grep / predicates)."],
1185
+ text: [...blast, "Widen the selection with --in, --grep, or predicates."].join("\n"),
1186
+ data: {
1187
+ op: opName,
1188
+ changed: false,
1189
+ matched: 0,
1190
+ script_path: session.artifactLabel,
1191
+ },
1175
1192
  }, EXIT_OK];
1176
1193
  }
1177
1194
  const [newRevision, idempotent] = await saveScriptSession(session, opts, opName);
@@ -1179,23 +1196,28 @@ export async function applySelectorEditsToSession(session, opts, template, verbL
1179
1196
  await syncValidationResult(session, validation, newRevision);
1180
1197
  const passed = Boolean(validation["passed"]);
1181
1198
  const rev = session.remote ? `; rev ${newRevision}` : "";
1199
+ const issues = summarizeIssues(asList(validation["issues"]));
1182
1200
  const report = {
1183
- title: passed ? "SELECTOR EDIT APPLIED" : "SELECTOR EDIT APPLIED: Validation needs repair",
1184
- op: opName,
1185
- changed: applied > 0,
1186
- ...(session.remote ? { idempotent } : {}),
1187
- result: [...blast, `validation: ${passed ? "passed" : "needs repair"}${rev}`],
1188
- warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
1189
- issues: summarizeIssues(asList(validation["issues"])),
1190
- artifacts: [session.artifactLabel],
1191
- ...(planDigest ? {
1192
- baseline: session.baselineHash,
1193
- plan_digest: planDigest,
1194
- operations,
1195
- targets: refs,
1196
- plan: planned?.plan,
1197
- } : {}),
1198
- next: passed ? undefined : ["Inspect issues and apply another edit."],
1201
+ text: [...blast, `validation: ${passed ? "passed" : "needs repair"}${rev}`, ...issues].join("\n"),
1202
+ data: {
1203
+ op: opName,
1204
+ changed: applied > 0,
1205
+ matched: refs.length,
1206
+ applied,
1207
+ skipped,
1208
+ revision: session.remote ? newRevision : undefined,
1209
+ idempotent: session.remote ? idempotent : undefined,
1210
+ validation,
1211
+ issues,
1212
+ script_path: session.artifactLabel,
1213
+ ...(planDigest ? {
1214
+ baseline: session.baselineHash,
1215
+ plan_digest: planDigest,
1216
+ operations,
1217
+ targets: refs,
1218
+ plan: planned?.plan,
1219
+ } : {}),
1220
+ },
1199
1221
  };
1200
1222
  return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
1201
1223
  }
@@ -1209,7 +1231,7 @@ export { sortDeep };
1209
1231
  // ===========================================================================
1210
1232
  //
1211
1233
  // Each command is a thin read-only wrapper around the underlying script
1212
- // document. They all return a Report shaped like inspect's existing output but
1234
+ // document. They all return a CommandOutput shaped like inspect's existing output but
1213
1235
  // without the `--target` flag — the verb itself encodes the target. Filters
1214
1236
  // use `--in <addr>` (compose with the address parsers) plus per-target
1215
1237
  // predicates (--name / --kind / --has / --grep / --context / --type / --actor
@@ -1260,12 +1282,11 @@ export function buildGrepMatcher(raw) {
1260
1282
  if (m) {
1261
1283
  const pattern = m[1];
1262
1284
  if (isDangerousRegex(pattern)) {
1263
- throw new CliError("QUERY BLOCKED: regex unsafe", "regex pattern has nested quantifiers (ReDoS risk) or exceeds the length limit.", {
1285
+ throw new CliError("regex pattern has nested quantifiers (ReDoS risk) or exceeds the length limit.", {
1264
1286
  exitCode: EXIT_USAGE,
1265
- required: [`pattern length <= ${MAX_GREP_PATTERN_LEN}; no quantifier-on-group-with-internal-quantifier (e.g. (a+)+)`],
1266
- received: [`length=${pattern.length}, sample=${pattern.slice(0, 80)}`],
1267
- nextSteps: ["Rewrite the regex to avoid nested quantifiers, or use a literal substring (drop the /…/ wrapper)."],
1268
1287
  errorCode: "REGEX_UNSAFE",
1288
+ hint: "Rewrite the regex to avoid nested quantifiers, or use a literal substring (drop the /…/ wrapper).",
1289
+ data: { expected: [`pattern length <= ${MAX_GREP_PATTERN_LEN}; no quantifier-on-group-with-internal-quantifier (e.g. (a+)+)`], actual: [`length=${pattern.length}, sample=${pattern.slice(0, 80)}`] },
1269
1290
  });
1270
1291
  }
1271
1292
  try {
@@ -1281,15 +1302,22 @@ export function buildGrepMatcher(raw) {
1281
1302
  }
1282
1303
  return (hay) => hay.includes(raw);
1283
1304
  }
1284
- export function buildReport(op, title, lines, artifactLabel) {
1305
+ export function buildReport(op, lines) {
1306
+ const outputLines = lines.length > 0 ? lines : ["No matches."];
1285
1307
  return {
1286
- title,
1287
- op,
1288
- changed: false,
1289
- result: lines.length > 0 ? lines : ["No matching items."],
1290
- artifacts: [artifactLabel],
1308
+ text: outputLines.join("\n"),
1309
+ data: {
1310
+ op,
1311
+ changed: false,
1312
+ },
1291
1313
  };
1292
1314
  }
1315
+ export function buildTableReport(op, lines, rowColumns, rows) {
1316
+ const report = buildReport(op, lines);
1317
+ report.data["row_columns"] = rowColumns;
1318
+ report.data["rows"] = rows;
1319
+ return report;
1320
+ }
1293
1321
  // ----- summary --------------------------------------------------------------
1294
1322
  export function synopsisPreview(text, limit) {
1295
1323
  const oneLine = text.replace(/\s+/g, " ").trim();
@@ -1353,12 +1381,11 @@ export function resolveActionTargets(script, opts) {
1353
1381
  // "state_changes" (underscore) passed the filter-missing guard but bypassed
1354
1382
  // every concrete predicate, returning the whole script.
1355
1383
  if (hasFilter && !HAS_FILTERS.has(hasFilter)) {
1356
- throw new CliError("QUERY BLOCKED: --has value invalid", "--has value invalid.", {
1384
+ throw new CliError("--has value invalid.", {
1357
1385
  exitCode: EXIT_USAGE,
1358
- required: [`--has: one of ${[...HAS_FILTERS].join(", ")}`],
1359
- received: [hasFilter],
1360
- nextSteps: ["Use a supported --has value."],
1361
1386
  errorCode: "HAS_FILTER_INVALID",
1387
+ hint: "Use a supported --has value.",
1388
+ data: { expected: [`--has: one of ${[...HAS_FILTERS].join(", ")}`], actual: [hasFilter] },
1362
1389
  });
1363
1390
  }
1364
1391
  const grepMatcher = buildGrepMatcher(strOf(opts["grep"]));
@@ -1451,6 +1478,7 @@ export function assetAppearanceCounts(script, kind) {
1451
1478
  }
1452
1479
  export function listAssetsByKind(script, kind, opts) {
1453
1480
  const lines = [];
1481
+ const rows = [];
1454
1482
  const [key, idKey, nameKey] = (() => {
1455
1483
  switch (kind) {
1456
1484
  case "actor": return ["actors", "actor_id", "actor_name"];
@@ -1527,8 +1555,42 @@ export function listAssetsByKind(script, kind, opts) {
1527
1555
  extra.push(`scenes=${scns}`, `episodes_count=${eps}${arc}`);
1528
1556
  }
1529
1557
  lines.push(`${kind} ${id}: ${name || "-"} ${extra.join(" ")}`);
1558
+ const seen = appearances?.get(id);
1559
+ rows.push({
1560
+ addr: `${kind}:${id}`,
1561
+ kind,
1562
+ id,
1563
+ name,
1564
+ aliases,
1565
+ importance,
1566
+ role,
1567
+ states,
1568
+ description_present: descPresent === "yes",
1569
+ ...(appearances
1570
+ ? {
1571
+ scenes: seen?.scenes ?? 0,
1572
+ episodes_count: seen?.episodes.size ?? 0,
1573
+ episodes: seen ? compactEpisodeRanges(seen.episodes) : "",
1574
+ }
1575
+ : {}),
1576
+ });
1530
1577
  }
1531
- return lines;
1578
+ return {
1579
+ lines,
1580
+ rows,
1581
+ rowColumns: [
1582
+ "addr",
1583
+ "kind",
1584
+ "id",
1585
+ "name",
1586
+ "aliases",
1587
+ "importance",
1588
+ "role",
1589
+ "states",
1590
+ "description_present",
1591
+ ...(appearances ? ["scenes", "episodes_count", "episodes"] : []),
1592
+ ],
1593
+ };
1532
1594
  }
1533
1595
  export function splitSpeakerList(raw) {
1534
1596
  if (Array.isArray(raw))
@@ -1544,15 +1606,14 @@ export function blankCreateReport(session, validation, idempotent) {
1544
1606
  resultLines.push(`idempotent: ${String(idempotent).toLowerCase()}`);
1545
1607
  }
1546
1608
  return {
1547
- title: passed ? "SCRIPT CREATED: Blank scaffold" : "SCRIPT CREATED: Validation needs repair",
1548
- op: "script.create",
1549
- changed: true,
1550
- result: resultLines,
1551
- warnings: summarizeIssues(asList(validation["issues"])),
1552
- artifacts: [session.artifactLabel],
1553
- next: [
1554
- "用 worldview 设定世界观,用 add-actor / add-location / add-prop 注册资产,用 add-episode + insert 搭出分集与场景。",
1555
- ],
1609
+ text: resultLines.join("\n"),
1610
+ data: {
1611
+ op: "script.create",
1612
+ changed: true,
1613
+ validation,
1614
+ script_path: session.artifactLabel,
1615
+ ...(session.remote ? { revision: session.revision, idempotent } : {}),
1616
+ },
1556
1617
  };
1557
1618
  }
1558
1619
  // ----- add-* (register new entities) ----------------------------------------
@@ -1561,12 +1622,11 @@ export function blankCreateReport(session, validation, idempotent) {
1561
1622
  export function requireNameArg(opts, verb, example) {
1562
1623
  const args = asList(opts["_args"]);
1563
1624
  if (args.length < 1) {
1564
- throw new CliError(`VERB BLOCKED: ${verb} name missing`, "Name missing.", {
1625
+ throw new CliError("Name missing.", {
1565
1626
  exitCode: EXIT_USAGE,
1566
- required: ["<name>"],
1567
- received: ["<empty>"],
1568
- nextSteps: [example],
1569
1627
  errorCode: "ARGS_MISSING",
1628
+ hint: example,
1629
+ data: { expected: ["<name>"], actual: ["<empty>"] },
1570
1630
  });
1571
1631
  }
1572
1632
  return args[0];