@lingjingai/scriptctl 0.49.16 → 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 (525) hide show
  1. package/README.md +8 -2
  2. package/changes/0.50.0.md +63 -0
  3. package/changes/0.51.0.md +24 -0
  4. package/dist/cli/register-batch.d.ts +3 -0
  5. package/dist/cli/register-batch.js +28 -0
  6. package/dist/cli/register-batch.js.map +1 -0
  7. package/dist/cli/register-edits.d.ts +4 -0
  8. package/dist/cli/register-edits.js +226 -0
  9. package/dist/cli/register-edits.js.map +1 -0
  10. package/dist/cli/register-query.d.ts +4 -0
  11. package/dist/cli/register-query.js +93 -0
  12. package/dist/cli/register-query.js.map +1 -0
  13. package/dist/cli/register-tools.d.ts +3 -0
  14. package/dist/cli/register-tools.js +23 -0
  15. package/dist/cli/register-tools.js.map +1 -0
  16. package/dist/cli/register-workflows.d.ts +4 -0
  17. package/dist/cli/register-workflows.js +71 -0
  18. package/dist/cli/register-workflows.js.map +1 -0
  19. package/dist/cli/registry.d.ts +14 -0
  20. package/dist/cli/registry.js +65 -0
  21. package/dist/cli/registry.js.map +1 -0
  22. package/dist/cli.d.ts +3 -0
  23. package/dist/cli.js +146 -575
  24. package/dist/cli.js.map +1 -1
  25. package/dist/common.d.ts +12 -38
  26. package/dist/common.js +5 -11
  27. package/dist/common.js.map +1 -1
  28. package/dist/domain/authoring/apply.d.ts +1 -33
  29. package/dist/domain/authoring/apply.js +1 -783
  30. package/dist/domain/authoring/apply.js.map +1 -1
  31. package/dist/domain/authoring/export.d.ts +1 -7
  32. package/dist/domain/authoring/export.js +1 -84
  33. package/dist/domain/authoring/export.js.map +1 -1
  34. package/dist/domain/authoring/parser.d.ts +2 -3
  35. package/dist/domain/authoring/parser.js +2 -914
  36. package/dist/domain/authoring/parser.js.map +1 -1
  37. package/dist/domain/authoring/serializer.d.ts +1 -2
  38. package/dist/domain/authoring/serializer.js +1 -243
  39. package/dist/domain/authoring/serializer.js.map +1 -1
  40. package/dist/domain/authoring/shared/bracket.d.ts +4 -0
  41. package/dist/domain/authoring/shared/bracket.js +95 -0
  42. package/dist/domain/authoring/shared/bracket.js.map +1 -0
  43. package/dist/domain/authoring/shared/types.d.ts +12 -0
  44. package/dist/domain/authoring/shared/types.js +2 -0
  45. package/dist/domain/authoring/shared/types.js.map +1 -0
  46. package/dist/domain/authoring/types.d.ts +2 -87
  47. package/dist/domain/authoring/types.js +2 -1
  48. package/dist/domain/authoring/types.js.map +1 -1
  49. package/dist/domain/authoring/v3/apply.d.ts +33 -0
  50. package/dist/domain/authoring/v3/apply.js +784 -0
  51. package/dist/domain/authoring/v3/apply.js.map +1 -0
  52. package/dist/domain/authoring/v3/export.d.ts +7 -0
  53. package/dist/domain/authoring/v3/export.js +85 -0
  54. package/dist/domain/authoring/v3/export.js.map +1 -0
  55. package/dist/domain/authoring/v3/parser.d.ts +3 -0
  56. package/dist/domain/authoring/v3/parser.js +823 -0
  57. package/dist/domain/authoring/v3/parser.js.map +1 -0
  58. package/dist/domain/authoring/v3/serializer.d.ts +2 -0
  59. package/dist/domain/authoring/v3/serializer.js +244 -0
  60. package/dist/domain/authoring/v3/serializer.js.map +1 -0
  61. package/dist/domain/authoring/v3/types.d.ts +76 -0
  62. package/dist/domain/authoring/v3/types.js +2 -0
  63. package/dist/domain/authoring/v3/types.js.map +1 -0
  64. package/dist/domain/authoring/v4/apply.d.ts +40 -0
  65. package/dist/domain/authoring/v4/apply.js +451 -0
  66. package/dist/domain/authoring/v4/apply.js.map +1 -0
  67. package/dist/domain/authoring/v4/parser.d.ts +2 -0
  68. package/dist/domain/authoring/v4/parser.js +729 -0
  69. package/dist/domain/authoring/v4/parser.js.map +1 -0
  70. package/dist/domain/authoring/v4/serializer.d.ts +6 -0
  71. package/dist/domain/authoring/v4/serializer.js +280 -0
  72. package/dist/domain/authoring/v4/serializer.js.map +1 -0
  73. package/dist/domain/authoring/v4/types.d.ts +50 -0
  74. package/dist/domain/authoring/v4/types.js +2 -0
  75. package/dist/domain/authoring/v4/types.js.map +1 -0
  76. package/dist/domain/ingest/apply-plan.d.ts +1 -1
  77. package/dist/domain/ingest/assembler.d.ts +9 -0
  78. package/dist/domain/ingest/assembler.js +98 -36
  79. package/dist/domain/ingest/assembler.js.map +1 -1
  80. package/dist/domain/ingest/asset-audit.d.ts +50 -0
  81. package/dist/domain/ingest/asset-audit.js +196 -0
  82. package/dist/domain/ingest/asset-audit.js.map +1 -0
  83. package/dist/domain/ingest/production-validation.d.ts +5 -4
  84. package/dist/domain/ingest/production-validation.js +10 -25
  85. package/dist/domain/ingest/production-validation.js.map +1 -1
  86. package/dist/domain/ingest/records.d.ts +1 -1
  87. package/dist/domain/ingest/records.js +1 -1
  88. package/dist/domain/ingest/records.js.map +1 -1
  89. package/dist/domain/ingest/serial-context.d.ts +1 -1
  90. package/dist/domain/ingest/validation.d.ts +1 -11
  91. package/dist/domain/ingest/validation.js +1 -199
  92. package/dist/domain/ingest/validation.js.map +1 -1
  93. package/dist/domain/ingest/video-apply.js +68 -45
  94. package/dist/domain/ingest/video-apply.js.map +1 -1
  95. package/dist/domain/script/address.d.ts +1 -32
  96. package/dist/domain/script/address.js +1 -144
  97. package/dist/domain/script/address.js.map +1 -1
  98. package/dist/domain/script/dispatch.d.ts +14 -0
  99. package/dist/domain/script/dispatch.js +45 -0
  100. package/dist/domain/script/dispatch.js.map +1 -0
  101. package/dist/domain/script/lookups.d.ts +1 -33
  102. package/dist/domain/script/lookups.js +1 -316
  103. package/dist/domain/script/lookups.js.map +1 -1
  104. package/dist/domain/script/parse.d.ts +1 -3
  105. package/dist/domain/script/parse.js +1 -164
  106. package/dist/domain/script/parse.js.map +1 -1
  107. package/dist/domain/script/patch/apply.d.ts +1 -2
  108. package/dist/domain/script/patch/apply.js +1 -30
  109. package/dist/domain/script/patch/apply.js.map +1 -1
  110. package/dist/domain/script/patch/helpers.d.ts +1 -17
  111. package/dist/domain/script/patch/helpers.js +1 -210
  112. package/dist/domain/script/patch/helpers.js.map +1 -1
  113. package/dist/domain/script/patch/inline-speaker.d.ts +1 -2
  114. package/dist/domain/script/patch/inline-speaker.js +1 -49
  115. package/dist/domain/script/patch/inline-speaker.js.map +1 -1
  116. package/dist/domain/script/patch/ops-action.d.ts +1 -12
  117. package/dist/domain/script/patch/ops-action.js +1 -366
  118. package/dist/domain/script/patch/ops-action.js.map +1 -1
  119. package/dist/domain/script/patch/ops-asset.d.ts +1 -14
  120. package/dist/domain/script/patch/ops-asset.js +1 -364
  121. package/dist/domain/script/patch/ops-asset.js.map +1 -1
  122. package/dist/domain/script/patch/ops-dialogue.d.ts +1 -2
  123. package/dist/domain/script/patch/ops-dialogue.js +1 -25
  124. package/dist/domain/script/patch/ops-dialogue.js.map +1 -1
  125. package/dist/domain/script/patch/ops-extend.d.ts +1 -8
  126. package/dist/domain/script/patch/ops-extend.js +1 -110
  127. package/dist/domain/script/patch/ops-extend.js.map +1 -1
  128. package/dist/domain/script/patch/ops-meta.d.ts +1 -5
  129. package/dist/domain/script/patch/ops-meta.js +1 -81
  130. package/dist/domain/script/patch/ops-meta.js.map +1 -1
  131. package/dist/domain/script/patch/ops-scene.d.ts +1 -6
  132. package/dist/domain/script/patch/ops-scene.js +1 -350
  133. package/dist/domain/script/patch/ops-scene.js.map +1 -1
  134. package/dist/domain/script/patch/ops-state.d.ts +1 -8
  135. package/dist/domain/script/patch/ops-state.js +1 -164
  136. package/dist/domain/script/patch/ops-state.js.map +1 -1
  137. package/dist/domain/script/patch/registry.d.ts +1 -2
  138. package/dist/domain/script/patch/registry.js +1 -52
  139. package/dist/domain/script/patch/registry.js.map +1 -1
  140. package/dist/domain/script/patch/types.d.ts +1 -10
  141. package/dist/domain/script/patch/types.js +1 -1
  142. package/dist/domain/script/patch/types.js.map +1 -1
  143. package/dist/domain/script/refs.d.ts +1 -4
  144. package/dist/domain/script/refs.js +1 -63
  145. package/dist/domain/script/refs.js.map +1 -1
  146. package/dist/domain/script/scene-refs.d.ts +1 -3
  147. package/dist/domain/script/scene-refs.js +1 -34
  148. package/dist/domain/script/scene-refs.js.map +1 -1
  149. package/dist/domain/script/schema.d.ts +1 -6
  150. package/dist/domain/script/schema.js +1 -55
  151. package/dist/domain/script/schema.js.map +1 -1
  152. package/dist/domain/script/selector.d.ts +1 -45
  153. package/dist/domain/script/selector.js +1 -184
  154. package/dist/domain/script/selector.js.map +1 -1
  155. package/dist/domain/script/shared/regex.d.ts +2 -0
  156. package/dist/domain/script/shared/regex.js +7 -0
  157. package/dist/domain/script/shared/regex.js.map +1 -0
  158. package/dist/domain/script/shared/types.d.ts +18 -0
  159. package/dist/domain/script/shared/types.js +9 -0
  160. package/dist/domain/script/shared/types.js.map +1 -0
  161. package/dist/domain/script/shared/validation.d.ts +10 -0
  162. package/dist/domain/script/shared/validation.js +2 -0
  163. package/dist/domain/script/shared/validation.js.map +1 -0
  164. package/dist/domain/script/shared.js +4 -6
  165. package/dist/domain/script/shared.js.map +1 -1
  166. package/dist/domain/script/types.d.ts +1 -123
  167. package/dist/domain/script/types.js +1 -8
  168. package/dist/domain/script/types.js.map +1 -1
  169. package/dist/domain/script/v3/address.d.ts +32 -0
  170. package/dist/domain/script/v3/address.js +136 -0
  171. package/dist/domain/script/v3/address.js.map +1 -0
  172. package/dist/domain/script/v3/lookups.d.ts +33 -0
  173. package/dist/domain/script/v3/lookups.js +316 -0
  174. package/dist/domain/script/v3/lookups.js.map +1 -0
  175. package/dist/domain/script/v3/ordinal.d.ts +8 -0
  176. package/dist/domain/script/v3/ordinal.js +91 -0
  177. package/dist/domain/script/v3/ordinal.js.map +1 -0
  178. package/dist/domain/script/v3/parse.d.ts +3 -0
  179. package/dist/domain/script/v3/parse.js +164 -0
  180. package/dist/domain/script/v3/parse.js.map +1 -0
  181. package/dist/domain/script/v3/patch/apply.d.ts +2 -0
  182. package/dist/domain/script/v3/patch/apply.js +31 -0
  183. package/dist/domain/script/v3/patch/apply.js.map +1 -0
  184. package/dist/domain/script/v3/patch/helpers.d.ts +17 -0
  185. package/dist/domain/script/v3/patch/helpers.js +209 -0
  186. package/dist/domain/script/v3/patch/helpers.js.map +1 -0
  187. package/dist/domain/script/v3/patch/inline-speaker.d.ts +2 -0
  188. package/dist/domain/script/v3/patch/inline-speaker.js +50 -0
  189. package/dist/domain/script/v3/patch/inline-speaker.js.map +1 -0
  190. package/dist/domain/script/v3/patch/ops-action.d.ts +12 -0
  191. package/dist/domain/script/v3/patch/ops-action.js +367 -0
  192. package/dist/domain/script/v3/patch/ops-action.js.map +1 -0
  193. package/dist/domain/script/v3/patch/ops-asset.d.ts +14 -0
  194. package/dist/domain/script/v3/patch/ops-asset.js +365 -0
  195. package/dist/domain/script/v3/patch/ops-asset.js.map +1 -0
  196. package/dist/domain/script/v3/patch/ops-dialogue.d.ts +2 -0
  197. package/dist/domain/script/v3/patch/ops-dialogue.js +26 -0
  198. package/dist/domain/script/v3/patch/ops-dialogue.js.map +1 -0
  199. package/dist/domain/script/v3/patch/ops-extend.d.ts +8 -0
  200. package/dist/domain/script/v3/patch/ops-extend.js +111 -0
  201. package/dist/domain/script/v3/patch/ops-extend.js.map +1 -0
  202. package/dist/domain/script/v3/patch/ops-meta.d.ts +5 -0
  203. package/dist/domain/script/v3/patch/ops-meta.js +82 -0
  204. package/dist/domain/script/v3/patch/ops-meta.js.map +1 -0
  205. package/dist/domain/script/v3/patch/ops-scene.d.ts +6 -0
  206. package/dist/domain/script/v3/patch/ops-scene.js +351 -0
  207. package/dist/domain/script/v3/patch/ops-scene.js.map +1 -0
  208. package/dist/domain/script/v3/patch/ops-state.d.ts +8 -0
  209. package/dist/domain/script/v3/patch/ops-state.js +165 -0
  210. package/dist/domain/script/v3/patch/ops-state.js.map +1 -0
  211. package/dist/domain/script/v3/patch/registry.d.ts +2 -0
  212. package/dist/domain/script/v3/patch/registry.js +53 -0
  213. package/dist/domain/script/v3/patch/registry.js.map +1 -0
  214. package/dist/domain/script/v3/patch/types.d.ts +10 -0
  215. package/dist/domain/script/v3/patch/types.js +2 -0
  216. package/dist/domain/script/v3/patch/types.js.map +1 -0
  217. package/dist/domain/script/v3/refs.d.ts +4 -0
  218. package/dist/domain/script/v3/refs.js +64 -0
  219. package/dist/domain/script/v3/refs.js.map +1 -0
  220. package/dist/domain/script/v3/scene-refs.d.ts +3 -0
  221. package/dist/domain/script/v3/scene-refs.js +35 -0
  222. package/dist/domain/script/v3/scene-refs.js.map +1 -0
  223. package/dist/domain/script/v3/schema.d.ts +6 -0
  224. package/dist/domain/script/v3/schema.js +56 -0
  225. package/dist/domain/script/v3/schema.js.map +1 -0
  226. package/dist/domain/script/v3/selector.d.ts +44 -0
  227. package/dist/domain/script/v3/selector.js +178 -0
  228. package/dist/domain/script/v3/selector.js.map +1 -0
  229. package/dist/domain/script/v3/types.d.ts +106 -0
  230. package/dist/domain/script/v3/types.js +2 -0
  231. package/dist/domain/script/v3/types.js.map +1 -0
  232. package/dist/domain/script/v3/validation.d.ts +3 -0
  233. package/dist/domain/script/v3/validation.js +200 -0
  234. package/dist/domain/script/v3/validation.js.map +1 -0
  235. package/dist/domain/script/v4/address.d.ts +61 -0
  236. package/dist/domain/script/v4/address.js +99 -0
  237. package/dist/domain/script/v4/address.js.map +1 -0
  238. package/dist/domain/script/v4/factory.d.ts +2 -0
  239. package/dist/domain/script/v4/factory.js +13 -0
  240. package/dist/domain/script/v4/factory.js.map +1 -0
  241. package/dist/domain/script/v4/from-v3.d.ts +29 -0
  242. package/dist/domain/script/v4/from-v3.js +307 -0
  243. package/dist/domain/script/v4/from-v3.js.map +1 -0
  244. package/dist/domain/script/v4/ids.d.ts +27 -0
  245. package/dist/domain/script/v4/ids.js +62 -0
  246. package/dist/domain/script/v4/ids.js.map +1 -0
  247. package/dist/domain/script/v4/ordinal.d.ts +3 -0
  248. package/dist/domain/script/v4/ordinal.js +66 -0
  249. package/dist/domain/script/v4/ordinal.js.map +1 -0
  250. package/dist/domain/script/v4/parse.d.ts +3 -0
  251. package/dist/domain/script/v4/parse.js +17 -0
  252. package/dist/domain/script/v4/parse.js.map +1 -0
  253. package/dist/domain/script/v4/patch/apply.d.ts +3 -0
  254. package/dist/domain/script/v4/patch/apply.js +24 -0
  255. package/dist/domain/script/v4/patch/apply.js.map +1 -0
  256. package/dist/domain/script/v4/patch/helpers.d.ts +32 -0
  257. package/dist/domain/script/v4/patch/helpers.js +138 -0
  258. package/dist/domain/script/v4/patch/helpers.js.map +1 -0
  259. package/dist/domain/script/v4/patch/ops-action.d.ts +13 -0
  260. package/dist/domain/script/v4/patch/ops-action.js +376 -0
  261. package/dist/domain/script/v4/patch/ops-action.js.map +1 -0
  262. package/dist/domain/script/v4/patch/ops-asset.d.ts +17 -0
  263. package/dist/domain/script/v4/patch/ops-asset.js +617 -0
  264. package/dist/domain/script/v4/patch/ops-asset.js.map +1 -0
  265. package/dist/domain/script/v4/patch/ops-extend.d.ts +11 -0
  266. package/dist/domain/script/v4/patch/ops-extend.js +86 -0
  267. package/dist/domain/script/v4/patch/ops-extend.js.map +1 -0
  268. package/dist/domain/script/v4/patch/ops-meta.d.ts +5 -0
  269. package/dist/domain/script/v4/patch/ops-meta.js +49 -0
  270. package/dist/domain/script/v4/patch/ops-meta.js.map +1 -0
  271. package/dist/domain/script/v4/patch/ops-scene-ref.d.ts +3 -0
  272. package/dist/domain/script/v4/patch/ops-scene-ref.js +98 -0
  273. package/dist/domain/script/v4/patch/ops-scene-ref.js.map +1 -0
  274. package/dist/domain/script/v4/patch/ops-structure.d.ts +6 -0
  275. package/dist/domain/script/v4/patch/ops-structure.js +236 -0
  276. package/dist/domain/script/v4/patch/ops-structure.js.map +1 -0
  277. package/dist/domain/script/v4/patch/registry.d.ts +2 -0
  278. package/dist/domain/script/v4/patch/registry.js +50 -0
  279. package/dist/domain/script/v4/patch/registry.js.map +1 -0
  280. package/dist/domain/script/v4/patch/schema.d.ts +7 -0
  281. package/dist/domain/script/v4/patch/schema.js +75 -0
  282. package/dist/domain/script/v4/patch/schema.js.map +1 -0
  283. package/dist/domain/script/v4/patch/types.d.ts +10 -0
  284. package/dist/domain/script/v4/patch/types.js +2 -0
  285. package/dist/domain/script/v4/patch/types.js.map +1 -0
  286. package/dist/domain/script/v4/selector.d.ts +11 -0
  287. package/dist/domain/script/v4/selector.js +124 -0
  288. package/dist/domain/script/v4/selector.js.map +1 -0
  289. package/dist/domain/script/v4/types.d.ts +120 -0
  290. package/dist/domain/script/v4/types.js +2 -0
  291. package/dist/domain/script/v4/types.js.map +1 -0
  292. package/dist/domain/script/v4/validation.d.ts +2 -0
  293. package/dist/domain/script/v4/validation.js +378 -0
  294. package/dist/domain/script/v4/validation.js.map +1 -0
  295. package/dist/infra/ingest/video-discovery.js +7 -4
  296. package/dist/infra/ingest/video-discovery.js.map +1 -1
  297. package/dist/infra/script-output-store.js +6 -11
  298. package/dist/infra/script-output-store.js.map +1 -1
  299. package/dist/infra/script-working-copy.js +15 -16
  300. package/dist/infra/script-working-copy.js.map +1 -1
  301. package/dist/llm/config.js +5 -4
  302. package/dist/llm/config.js.map +1 -1
  303. package/dist/llm/registry.d.ts +1 -0
  304. package/dist/llm/registry.js +3 -0
  305. package/dist/llm/registry.js.map +1 -1
  306. package/dist/llm/router.js +6 -8
  307. package/dist/llm/router.js.map +1 -1
  308. package/dist/llm/tasks/ingest/asset-audit.d.ts +6 -0
  309. package/dist/llm/tasks/ingest/asset-audit.js +164 -0
  310. package/dist/llm/tasks/ingest/asset-audit.js.map +1 -0
  311. package/dist/llm/tasks/ingest/asset-curate.d.ts +1 -1
  312. package/dist/llm/tasks/ingest/text-normalize.js +3 -3
  313. package/dist/llm/tasks/ingest/text-normalize.js.map +1 -1
  314. package/dist/llm/tasks/ingest/video-serial.js +3 -3
  315. package/dist/llm/tasks/ingest/video-serial.js.map +1 -1
  316. package/dist/llm/types.d.ts +1 -1
  317. package/dist/llm/types.js.map +1 -1
  318. package/dist/output.d.ts +2 -3
  319. package/dist/output.js +72 -109
  320. package/dist/output.js.map +1 -1
  321. package/dist/usecases/changelog.d.ts +2 -2
  322. package/dist/usecases/changelog.js +28 -36
  323. package/dist/usecases/changelog.js.map +1 -1
  324. package/dist/usecases/doctor.d.ts +2 -2
  325. package/dist/usecases/doctor.js +3 -5
  326. package/dist/usecases/doctor.js.map +1 -1
  327. package/dist/usecases/ingest/command.d.ts +2 -2
  328. package/dist/usecases/ingest/command.js +23 -6
  329. package/dist/usecases/ingest/command.js.map +1 -1
  330. package/dist/usecases/ingest/final-asset-audit.d.ts +39 -0
  331. package/dist/usecases/ingest/final-asset-audit.js +88 -0
  332. package/dist/usecases/ingest/final-asset-audit.js.map +1 -0
  333. package/dist/usecases/ingest/pass5-extract.js +5 -4
  334. package/dist/usecases/ingest/pass5-extract.js.map +1 -1
  335. package/dist/usecases/ingest/pass5-govern.js +13 -11
  336. package/dist/usecases/ingest/pass5-govern.js.map +1 -1
  337. package/dist/usecases/ingest/pipeline.js +77 -23
  338. package/dist/usecases/ingest/pipeline.js.map +1 -1
  339. package/dist/usecases/ingest/serial-govern.d.ts +2 -1
  340. package/dist/usecases/ingest/serial-govern.js +16 -8
  341. package/dist/usecases/ingest/serial-govern.js.map +1 -1
  342. package/dist/usecases/ingest/status.d.ts +2 -2
  343. package/dist/usecases/ingest/status.js +1 -1
  344. package/dist/usecases/ingest/status.js.map +1 -1
  345. package/dist/usecases/ingest/video-pipeline.js +121 -29
  346. package/dist/usecases/ingest/video-pipeline.js.map +1 -1
  347. package/dist/usecases/ingest/view-command.d.ts +2 -2
  348. package/dist/usecases/ingest/view-command.js +7 -6
  349. package/dist/usecases/ingest/view-command.js.map +1 -1
  350. package/dist/usecases/ingest/view-server.d.ts +2 -2
  351. package/dist/usecases/ingest/view-server.js +9 -7
  352. package/dist/usecases/ingest/view-server.js.map +1 -1
  353. package/dist/usecases/script/actions.d.ts +2 -2
  354. package/dist/usecases/script/actions.js +106 -11
  355. package/dist/usecases/script/actions.js.map +1 -1
  356. package/dist/usecases/script/actor.d.ts +2 -2
  357. package/dist/usecases/script/actor.js +3 -4
  358. package/dist/usecases/script/actor.js.map +1 -1
  359. package/dist/usecases/script/actors.d.ts +2 -2
  360. package/dist/usecases/script/actors.js +18 -9
  361. package/dist/usecases/script/actors.js.map +1 -1
  362. package/dist/usecases/script/add-actor.d.ts +2 -2
  363. package/dist/usecases/script/add-episode.d.ts +2 -2
  364. package/dist/usecases/script/add-location.d.ts +2 -2
  365. package/dist/usecases/script/add-prop.d.ts +2 -2
  366. package/dist/usecases/script/alias.d.ts +2 -2
  367. package/dist/usecases/script/alias.js +13 -14
  368. package/dist/usecases/script/alias.js.map +1 -1
  369. package/dist/usecases/script/assets.d.ts +2 -2
  370. package/dist/usecases/script/assets.js +29 -5
  371. package/dist/usecases/script/assets.js.map +1 -1
  372. package/dist/usecases/script/audit-assets.d.ts +4 -0
  373. package/dist/usecases/script/audit-assets.js +37 -0
  374. package/dist/usecases/script/audit-assets.js.map +1 -0
  375. package/dist/usecases/script/command-constraints.d.ts +36 -0
  376. package/dist/usecases/script/command-constraints.js +114 -0
  377. package/dist/usecases/script/command-constraints.js.map +1 -0
  378. package/dist/usecases/script/context.d.ts +2 -2
  379. package/dist/usecases/script/context.js +34 -8
  380. package/dist/usecases/script/context.js.map +1 -1
  381. package/dist/usecases/script/create.d.ts +2 -2
  382. package/dist/usecases/script/create.js +5 -6
  383. package/dist/usecases/script/create.js.map +1 -1
  384. package/dist/usecases/script/delete.d.ts +2 -2
  385. package/dist/usecases/script/delete.js +15 -15
  386. package/dist/usecases/script/delete.js.map +1 -1
  387. package/dist/usecases/script/describe.d.ts +2 -2
  388. package/dist/usecases/script/describe.js +13 -12
  389. package/dist/usecases/script/describe.js.map +1 -1
  390. package/dist/usecases/script/dialogue.d.ts +2 -2
  391. package/dist/usecases/script/dialogue.js +18 -9
  392. package/dist/usecases/script/dialogue.js.map +1 -1
  393. package/dist/usecases/script/dispatch.d.ts +3 -0
  394. package/dist/usecases/script/dispatch.js +10 -0
  395. package/dist/usecases/script/dispatch.js.map +1 -0
  396. package/dist/usecases/script/do.d.ts +2 -2
  397. package/dist/usecases/script/do.js +105 -45
  398. package/dist/usecases/script/do.js.map +1 -1
  399. package/dist/usecases/script/emotion.d.ts +2 -2
  400. package/dist/usecases/script/emotion.js +14 -3
  401. package/dist/usecases/script/emotion.js.map +1 -1
  402. package/dist/usecases/script/episodes.d.ts +2 -2
  403. package/dist/usecases/script/episodes.js +19 -8
  404. package/dist/usecases/script/episodes.js.map +1 -1
  405. package/dist/usecases/script/export-authoring.d.ts +2 -2
  406. package/dist/usecases/script/export-authoring.js +47 -26
  407. package/dist/usecases/script/export-authoring.js.map +1 -1
  408. package/dist/usecases/script/extend.d.ts +2 -2
  409. package/dist/usecases/script/extend.js +43 -31
  410. package/dist/usecases/script/extend.js.map +1 -1
  411. package/dist/usecases/script/ids.d.ts +2 -2
  412. package/dist/usecases/script/ids.js +68 -21
  413. package/dist/usecases/script/ids.js.map +1 -1
  414. package/dist/usecases/script/importance.d.ts +2 -2
  415. package/dist/usecases/script/importance.js +3 -4
  416. package/dist/usecases/script/importance.js.map +1 -1
  417. package/dist/usecases/script/insert.d.ts +2 -2
  418. package/dist/usecases/script/insert.js +80 -46
  419. package/dist/usecases/script/insert.js.map +1 -1
  420. package/dist/usecases/script/issues.d.ts +2 -2
  421. package/dist/usecases/script/issues.js +10 -2
  422. package/dist/usecases/script/issues.js.map +1 -1
  423. package/dist/usecases/script/lib.d.ts +62 -7
  424. package/dist/usecases/script/lib.js +782 -184
  425. package/dist/usecases/script/lib.js.map +1 -1
  426. package/dist/usecases/script/locations.d.ts +2 -2
  427. package/dist/usecases/script/locations.js +18 -9
  428. package/dist/usecases/script/locations.js.map +1 -1
  429. package/dist/usecases/script/merge.d.ts +2 -2
  430. package/dist/usecases/script/merge.js +17 -17
  431. package/dist/usecases/script/merge.js.map +1 -1
  432. package/dist/usecases/script/move.d.ts +2 -2
  433. package/dist/usecases/script/move.js +30 -13
  434. package/dist/usecases/script/move.js.map +1 -1
  435. package/dist/usecases/script/parenthetical.d.ts +3 -0
  436. package/dist/usecases/script/parenthetical.js +52 -0
  437. package/dist/usecases/script/parenthetical.js.map +1 -0
  438. package/dist/usecases/script/props.d.ts +2 -2
  439. package/dist/usecases/script/props.js +18 -9
  440. package/dist/usecases/script/props.js.map +1 -1
  441. package/dist/usecases/script/refs.d.ts +2 -2
  442. package/dist/usecases/script/refs.js +61 -11
  443. package/dist/usecases/script/refs.js.map +1 -1
  444. package/dist/usecases/script/rename.d.ts +2 -2
  445. package/dist/usecases/script/rename.js +13 -12
  446. package/dist/usecases/script/rename.js.map +1 -1
  447. package/dist/usecases/script/replace.d.ts +2 -2
  448. package/dist/usecases/script/replace.js +6 -8
  449. package/dist/usecases/script/replace.js.map +1 -1
  450. package/dist/usecases/script/role.d.ts +2 -2
  451. package/dist/usecases/script/role.js +3 -4
  452. package/dist/usecases/script/role.js.map +1 -1
  453. package/dist/usecases/script/scenes.d.ts +2 -2
  454. package/dist/usecases/script/scenes.js +35 -7
  455. package/dist/usecases/script/scenes.js.map +1 -1
  456. package/dist/usecases/script/script-patch.d.ts +2 -2
  457. package/dist/usecases/script/script-patch.js +219 -72
  458. package/dist/usecases/script/script-patch.js.map +1 -1
  459. package/dist/usecases/script/script-validate.d.ts +2 -2
  460. package/dist/usecases/script/script-validate.js +11 -7
  461. package/dist/usecases/script/script-validate.js.map +1 -1
  462. package/dist/usecases/script/session.d.ts +3 -0
  463. package/dist/usecases/script/session.js +74 -65
  464. package/dist/usecases/script/session.js.map +1 -1
  465. package/dist/usecases/script/split.d.ts +2 -2
  466. package/dist/usecases/script/split.js +18 -14
  467. package/dist/usecases/script/split.js.map +1 -1
  468. package/dist/usecases/script/state-add.d.ts +2 -2
  469. package/dist/usecases/script/state-add.js +3 -4
  470. package/dist/usecases/script/state-add.js.map +1 -1
  471. package/dist/usecases/script/state-delete.d.ts +2 -2
  472. package/dist/usecases/script/state-delete.js +3 -4
  473. package/dist/usecases/script/state-delete.js.map +1 -1
  474. package/dist/usecases/script/state-rename.d.ts +2 -2
  475. package/dist/usecases/script/state-rename.js +3 -4
  476. package/dist/usecases/script/state-rename.js.map +1 -1
  477. package/dist/usecases/script/states.d.ts +2 -2
  478. package/dist/usecases/script/states.js +91 -20
  479. package/dist/usecases/script/states.js.map +1 -1
  480. package/dist/usecases/script/summary.d.ts +2 -2
  481. package/dist/usecases/script/summary.js +21 -2
  482. package/dist/usecases/script/summary.js.map +1 -1
  483. package/dist/usecases/script/synopsis-generate.d.ts +2 -2
  484. package/dist/usecases/script/synopsis-generate.js +38 -39
  485. package/dist/usecases/script/synopsis-generate.js.map +1 -1
  486. package/dist/usecases/script/synopsis.d.ts +2 -2
  487. package/dist/usecases/script/synopsis.js +20 -16
  488. package/dist/usecases/script/synopsis.js.map +1 -1
  489. package/dist/usecases/script/transition.d.ts +2 -2
  490. package/dist/usecases/script/transition.js +3 -4
  491. package/dist/usecases/script/transition.js.map +1 -1
  492. package/dist/usecases/script/type.d.ts +2 -2
  493. package/dist/usecases/script/type.js +3 -4
  494. package/dist/usecases/script/type.js.map +1 -1
  495. package/dist/usecases/script/v3/mutations.d.ts +2 -0
  496. package/dist/usecases/script/v3/mutations.js +5 -0
  497. package/dist/usecases/script/v3/mutations.js.map +1 -0
  498. package/dist/usecases/script/v4/mutations.d.ts +2 -0
  499. package/dist/usecases/script/v4/mutations.js +6 -0
  500. package/dist/usecases/script/v4/mutations.js.map +1 -0
  501. package/dist/usecases/script/v4/queries.d.ts +15 -0
  502. package/dist/usecases/script/v4/queries.js +191 -0
  503. package/dist/usecases/script/v4/queries.js.map +1 -0
  504. package/dist/usecases/script/working-copy.d.ts +4 -4
  505. package/dist/usecases/script/working-copy.js +98 -94
  506. package/dist/usecases/script/working-copy.js.map +1 -1
  507. package/dist/usecases/script/worldview.d.ts +2 -2
  508. package/dist/usecases/script/worldview.js +3 -4
  509. package/dist/usecases/script/worldview.js.map +1 -1
  510. package/dist/usecases/script/write-authoring.d.ts +2 -2
  511. package/dist/usecases/script/write-authoring.js +125 -50
  512. package/dist/usecases/script/write-authoring.js.map +1 -1
  513. package/dist/usecases/update.d.ts +2 -2
  514. package/dist/usecases/update.js +32 -47
  515. package/dist/usecases/update.js.map +1 -1
  516. package/package.json +3 -2
  517. package/skills/scriptctl/SKILL.md +128 -282
  518. package/skills/scriptctl/references/atomic-edit-workflow.md +23 -38
  519. package/skills/scriptctl/references/bulk-authoring.md +35 -154
  520. package/skills/scriptctl/references/ingest-workflow.md +12 -11
  521. package/skills/scriptctl/references/production-writing-standard.md +38 -67
  522. package/skills/scriptctl/references/state-reference-repair.md +19 -42
  523. package/dist/help-text.d.ts +0 -2
  524. package/dist/help-text.js +0 -1112
  525. package/dist/help-text.js.map +0 -1
@@ -1,12 +1,19 @@
1
- import { CliError, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_USAGE, } from "../../common.js";
2
- import { applyPatchOperations } from "../../domain/script/patch/apply.js";
1
+ import { CliError, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_USAGE, sha256Text, } from "../../common.js";
2
+ import { applyPatchOperationsV4 } from "../../domain/script/v4/patch/apply.js";
3
+ import { isV4Id } from "../../domain/script/v4/ids.js";
4
+ import { requireScriptV4 } from "../../domain/script/v4/parse.js";
5
+ import { validateScriptV4 } from "../../domain/script/v4/validation.js";
3
6
  import { sceneRefs } from "../../domain/script/scene-refs.js";
4
7
  import { isDangerousRegex, MAX_GREP_PATTERN_LEN, parseInSelector, selectorSingleActionPoint, WHOLE_SCRIPT, } from "../../domain/script/selector.js";
5
8
  import { parseAnyAddress, } from "../../domain/script/address.js";
9
+ import { parseV4Address } from "../../domain/script/v4/address.js";
6
10
  import { summarizeIssues } from "../../domain/script/text-util.js";
7
11
  import { sortDeep } from "../../domain/script/text-util.js";
8
12
  import { asList, idNum, isDict, strOf, } from "../../domain/script/shared.js";
9
13
  import { loadScriptForEdit, validateSession, saveScriptSession, syncValidationResult, scriptSourceText, } from "./session.js";
14
+ import { resolveActionTargetsV4 } from "./v4/queries.js";
15
+ import { applyVersionedMutations } from "./dispatch.js";
16
+ import { assertInsertCommandFlags, assertMoveCommandFlags, assertSceneRefCommandFlags, } from "./command-constraints.js";
10
17
  export function tokenizeLine(line) {
11
18
  const out = [];
12
19
  let cur = "";
@@ -75,6 +82,7 @@ export const DO_BOOL_FLAGS = {
75
82
  replace: new Set(["all", "regex"]),
76
83
  transition: new Set(["clear"]),
77
84
  emotion: new Set(["clear"]),
85
+ parenthetical: new Set(["remove"]),
78
86
  "scene-ref": new Set(["clear", "remove", "append"]),
79
87
  delete: new Set(["force"]),
80
88
  extend: new Set(["clear", "json-value"]),
@@ -82,6 +90,8 @@ export const DO_BOOL_FLAGS = {
82
90
  export const DO_REPEAT_FLAGS = {
83
91
  alias: new Set(["add", "remove"]),
84
92
  "add-actor": new Set(["alias"]),
93
+ "scene-ref": new Set(["state"]),
94
+ insert: new Set(["state"]),
85
95
  };
86
96
  function pushFlag(flags, repeats, name, val) {
87
97
  if (repeats.has(name)) {
@@ -145,12 +155,11 @@ export function parseDoLine(tokens) {
145
155
  return { verb, args, flags };
146
156
  }
147
157
  export function doErr(detail) {
148
- return new CliError("DO BLOCKED: line invalid", detail, {
158
+ return new CliError(detail, {
149
159
  exitCode: EXIT_USAGE,
150
- required: ["a valid <verb> ...args... line"],
151
- received: [detail],
152
- nextSteps: ["Each line is a scriptctl verb minus the leading `scriptctl`; see `scriptctl do --help`."],
153
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] },
154
163
  });
155
164
  }
156
165
  // ---- shared op-template builders ------------------------------------------
@@ -162,36 +171,46 @@ export function stateChangeTemplate(target, f) {
162
171
  const toSet = f.to !== undefined;
163
172
  const clearSet = Boolean(f.clear);
164
173
  if (toSet && clearSet) {
165
- throw new CliError("VERB BLOCKED: state-change flags mutually exclusive", "state-change: --to and --clear are mutually exclusive.", {
166
- exitCode: EXIT_USAGE, required: ["exactly one of --to <state> / --clear"], received: ["--to", "--clear"],
167
- 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"] },
168
179
  });
169
180
  }
170
181
  if (!toSet && !clearSet) {
171
- throw new CliError("VERB BLOCKED: state-change flag missing", "state-change: must specify --to <state> or --clear.", {
172
- exitCode: EXIT_USAGE, required: ["one of --to <state> / --clear"], received: ["<missing>"],
173
- 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>"] },
174
187
  });
175
188
  }
176
189
  // v3 has no per-action state_changes; state lives on scene.ref.* only.
177
- throw new CliError("VERB BLOCKED: state-change removed", "state-change was removed in v3; set a scene ref's state instead.", {
178
- exitCode: EXIT_USAGE, required: ["scene-ref state edit"], received: ["state-change"],
179
- 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"] },
180
195
  });
181
196
  }
182
197
  export function transitionTemplate(target, f) {
183
198
  const setBoth = f.process !== undefined || f.contrast !== undefined;
184
199
  const clearSet = Boolean(f.clear);
185
200
  if (setBoth && clearSet) {
186
- throw new CliError("VERB BLOCKED: transition flags mutually exclusive", "transition: --clear is mutually exclusive with --process/--contrast.", {
187
- exitCode: EXIT_USAGE, required: ["either --clear, or --process + --contrast"], received: ["--clear", "--process/--contrast"],
188
- 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"] },
189
206
  });
190
207
  }
191
208
  if (!setBoth && !clearSet) {
192
- throw new CliError("VERB BLOCKED: transition flag missing", "transition: must specify --process/--contrast or --clear.", {
193
- exitCode: EXIT_USAGE, required: ["--process + --contrast, or --clear"], received: ["<missing>"],
194
- 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>"] },
195
214
  });
196
215
  }
197
216
  return clearSet
@@ -202,15 +221,19 @@ export function emotionTemplate(f) {
202
221
  const emotionSet = f.emotion !== undefined;
203
222
  const clearSet = Boolean(f.clear);
204
223
  if (emotionSet && clearSet) {
205
- throw new CliError("VERB BLOCKED: emotion flags mutually exclusive", "emotion: value and --clear are mutually exclusive.", {
206
- exitCode: EXIT_USAGE, required: ["either <emotion>, or --clear"], received: ["<emotion>", "--clear"],
207
- 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"] },
208
229
  });
209
230
  }
210
231
  if (!emotionSet && !clearSet) {
211
- throw new CliError("VERB BLOCKED: emotion value missing", "emotion: must specify <emotion> or --clear.", {
212
- exitCode: EXIT_USAGE, required: ["<emotion>, or --clear"], received: ["<missing>"],
213
- 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>"] },
214
237
  });
215
238
  }
216
239
  return clearSet
@@ -223,9 +246,11 @@ export function contextTemplate(target, f) {
223
246
  const removeSet = Boolean(f.remove);
224
247
  const n = (stateSet ? 1 : 0) + (clearSet ? 1 : 0) + (removeSet ? 1 : 0);
225
248
  if (n === 0) {
226
- throw new CliError("VERB BLOCKED: context flag missing", "context: must specify --state X, --clear, or --remove.", {
227
- exitCode: EXIT_USAGE, required: ["one of --state <state_id> / --clear / --remove"], received: ["<missing>"],
228
- 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>"] },
229
254
  });
230
255
  }
231
256
  if (n > 1) {
@@ -236,30 +261,30 @@ export function contextTemplate(target, f) {
236
261
  passed.push("--clear");
237
262
  if (removeSet)
238
263
  passed.push("--remove");
239
- throw new CliError("VERB BLOCKED: context flags mutually exclusive", "context: --state / --clear / --remove are mutually exclusive.", {
240
- exitCode: EXIT_USAGE, required: ["exactly one of --state / --clear / --remove"], received: passed,
241
- 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 },
242
269
  });
243
270
  }
244
271
  const append = Boolean(f.append);
245
272
  const occurrenceRaw = strOf(f.occurrence).trim();
246
273
  const occurrence = occurrenceRaw ? Number(occurrenceRaw) : undefined;
247
274
  if (occurrence !== undefined && (!Number.isInteger(occurrence) || occurrence < 1)) {
248
- 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.", {
249
276
  exitCode: EXIT_USAGE,
250
- required: ["--occurrence <positive integer> (1-based)"],
251
- received: [occurrenceRaw],
252
- nextSteps: ["Use 1 for the first matching scene ref, 2 for the second, and so on."],
253
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] },
254
280
  });
255
281
  }
256
282
  if (append && (!stateSet || occurrence !== undefined)) {
257
- 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.", {
258
284
  exitCode: EXIT_USAGE,
259
- required: ["--state <state_id> --append"],
260
- received: [append ? "--append" : "", occurrence !== undefined ? "--occurrence" : "", clearSet ? "--clear" : "", removeSet ? "--remove" : ""].filter(Boolean),
261
- nextSteps: ["Append a new actor ref with --state <state_id> --append, or target an existing ref with --occurrence <n>."],
262
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) },
263
288
  });
264
289
  }
265
290
  if (removeSet)
@@ -275,39 +300,67 @@ export function dialogueTemplate(f) {
275
300
  const speakerKind = strOf(f.kind).trim();
276
301
  const label = strOf(f.label).trim();
277
302
  if (actorId && speakerKind) {
278
- 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.", {
279
304
  exitCode: EXIT_USAGE,
280
- required: ["exactly one of --actor <actor_id> or --kind <non-actor kind>"],
281
- received: ["--actor", "--kind"],
282
- nextSteps: ["Keep --actor for a character, or --kind for a non-actor source."],
283
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"] },
284
308
  });
285
309
  }
286
310
  if (actorId && label) {
287
- 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.", {
288
312
  exitCode: EXIT_USAGE,
289
- required: ["omit --label with --actor, or pair --label with --kind"],
290
- received: ["--actor", "--label"],
291
- nextSteps: ["Remove --label for an actor line, or use --kind for a labeled non-actor source."],
292
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"] },
293
316
  });
294
317
  }
295
318
  if (actorId)
296
319
  return { op: "dialogue.speaker.set", actor_id: actorId };
297
320
  if (speakerKind)
298
321
  return { op: "dialogue.speaker.set", kind: speakerKind, label: label || undefined };
299
- throw new CliError("VERB BLOCKED: dialogue speaker missing", "dialogue: pass --actor <id> or --kind <system|broadcast|offscreen|group>.", {
300
- exitCode: EXIT_USAGE, required: ["--actor <actor_id> or --kind <non-actor kind>"], received: ["<missing>"],
301
- 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>"] },
302
327
  });
303
328
  }
304
329
  // Translate one parsed verb line into a patch op dict (the SAME ops `patch`
305
330
  // consumes). `do` = the human/agent-friendly notation for those ops: no JSON,
306
331
  // no field-name guessing — the verbs you already type. Multipolar verbs
307
332
  // (delete/move/insert/merge/describe) pick their op by the address kind.
308
- export function lineToOp(verb, args, flags) {
333
+ function repeatedDoFlag(raw) {
334
+ if (raw === undefined)
335
+ return [];
336
+ return Array.isArray(raw) ? raw.map(strOf) : [strOf(raw)];
337
+ }
338
+ function doInsertFlags(flags) {
339
+ return {
340
+ type: flags["type"],
341
+ content: flags["content"],
342
+ emotion: flags["emotion"],
343
+ parenthetical: flags["parenthetical"],
344
+ actionId: flags["action-id"],
345
+ at: flags["at"],
346
+ before: flags["before"],
347
+ after: flags["after"],
348
+ actor: flags["actor"],
349
+ speaker: flags["speaker"],
350
+ speakerState: flags["speaker-state"],
351
+ kind: flags["kind"],
352
+ label: flags["label"],
353
+ location: flags["location"],
354
+ time: flags["time"],
355
+ space: flags["space"],
356
+ sceneId: flags["scene-id"],
357
+ states: repeatedDoFlag(flags["state"]),
358
+ };
359
+ }
360
+ export function lineToOp(verb, args, flags, scriptVersion = 3) {
309
361
  const a0 = strOf(args[0]);
310
362
  const a1 = strOf(args[1]);
363
+ const parseAddress = (raw) => scriptVersion === 4 ? parseV4Address(raw) : parseAnyAddress(raw);
311
364
  switch (verb) {
312
365
  case "replace":
313
366
  return flags["regex"]
@@ -320,20 +373,52 @@ export function lineToOp(verb, args, flags) {
320
373
  case "transition":
321
374
  return { ...transitionTemplate(a1, { process: flags["process"], contrast: flags["contrast"], clear: Boolean(flags["clear"]) }), at: a0 };
322
375
  case "emotion":
376
+ if (scriptVersion === 4)
377
+ throw doErr("emotion belongs to Script v3; use parenthetical for Script v4");
323
378
  return { ...emotionTemplate({ emotion: args[1], clear: Boolean(flags["clear"]) }), at: a0 };
324
- case "scene-ref":
379
+ case "parenthetical":
380
+ if (scriptVersion === 3)
381
+ throw doErr("parenthetical belongs to Script v4; use emotion for Script v3");
382
+ if (Boolean(flags["remove"]) === (flags["set"] !== undefined)) {
383
+ throw doErr("parenthetical needs exactly one of --set <text> or --remove");
384
+ }
385
+ return Boolean(flags["remove"])
386
+ ? { op: "action.parenthetical.remove", at: a0 }
387
+ : { op: "action.parenthetical.set", at: a0, parenthetical: strOf(flags["set"]) };
388
+ case "scene-ref": {
389
+ const sceneRefFlags = {
390
+ states: repeatedDoFlag(flags["state"]),
391
+ clear: Boolean(flags["clear"]),
392
+ remove: Boolean(flags["remove"]),
393
+ append: Boolean(flags["append"]),
394
+ occurrence: flags["occurrence"],
395
+ };
396
+ assertSceneRefCommandFlags(scriptVersion, sceneRefFlags);
397
+ if (scriptVersion === 4) {
398
+ return sceneRefFlags.remove
399
+ ? { op: "scene.ref.remove", at: a0, target: a1 }
400
+ : { op: "scene.ref.set", at: a0, target: a1, state_ids: sceneRefFlags.states };
401
+ }
325
402
  return {
326
403
  ...contextTemplate(a1, {
327
- state: flags["state"],
328
- clear: Boolean(flags["clear"]),
329
- remove: Boolean(flags["remove"]),
330
- append: Boolean(flags["append"]),
331
- occurrence: flags["occurrence"],
404
+ state: sceneRefFlags.states[0],
405
+ clear: sceneRefFlags.clear,
406
+ remove: sceneRefFlags.remove,
407
+ append: sceneRefFlags.append,
408
+ occurrence: sceneRefFlags.occurrence,
332
409
  }),
333
410
  at: a0,
334
411
  };
412
+ }
335
413
  case "dialogue":
336
- return { ...dialogueTemplate({ actor: flags["actor"], kind: flags["kind"], label: flags["label"] }), at: a0 };
414
+ if (scriptVersion === 3 && flags["state"] !== undefined) {
415
+ throw doErr("dialogue --state belongs to Script v4");
416
+ }
417
+ return {
418
+ ...dialogueTemplate({ actor: flags["actor"], kind: flags["kind"], label: flags["label"] }),
419
+ ...(scriptVersion === 4 && flags["state"] !== undefined ? { state_id: flags["state"] } : {}),
420
+ at: a0,
421
+ };
337
422
  case "rename":
338
423
  return { op: "asset.rename", target: a0, name: a1 };
339
424
  case "role":
@@ -346,14 +431,15 @@ export function lineToOp(verb, args, flags) {
346
431
  if (args.length > 2)
347
432
  throw doErr("synopsis text with spaces must be quoted");
348
433
  if (args.length === 1) {
349
- if (/^ep_/.test(a0))
434
+ if (/^ep_/.test(a0) || /^ep[1-9A-HJ-NP-Za-km-z]{6}$/.test(a0)) {
350
435
  throw doErr(`synopsis for ${a0} needs text`);
436
+ }
351
437
  return { op: "meta.synopsis.set", synopsis: a0 };
352
438
  }
353
- const addr = parseAnyAddress(a0);
439
+ const addr = parseAddress(a0);
354
440
  if (addr.kind !== "episode")
355
441
  throw doErr(`synopsis target must be an episode id or omitted for global (${a0})`);
356
- return { op: "episode.synopsis.set", episode_id: addr.episodeId, synopsis: a1 };
442
+ return { op: "episode.synopsis.set", episode_id: "id" in addr ? addr.id : addr.episodeId, synopsis: a1 };
357
443
  }
358
444
  case "alias": {
359
445
  const add = flags["add"] ?? [];
@@ -365,7 +451,7 @@ export function lineToOp(verb, args, flags) {
365
451
  throw doErr("alias needs --add <alias> or --remove <alias>");
366
452
  }
367
453
  case "describe": {
368
- const addr = parseAnyAddress(a0);
454
+ const addr = parseAddress(a0);
369
455
  if (addr.kind === "state")
370
456
  return { op: "state.describe", target: a0, description: a1 };
371
457
  if (addr.kind === "asset")
@@ -373,7 +459,8 @@ export function lineToOp(verb, args, flags) {
373
459
  throw doErr(`describe cannot operate on a ${addr.kind} address (${a0}); use an asset or asset/state target`);
374
460
  }
375
461
  case "extend": {
376
- parseAnyAddress(a0); // early-validate (any kind is valid)
462
+ if (a0 !== "global")
463
+ parseAddress(a0);
377
464
  if (flags["clear"]) {
378
465
  if (args.length >= 3)
379
466
  throw doErr("extend --clear takes no value (drop --clear to set, or drop the value to delete)");
@@ -399,37 +486,52 @@ export function lineToOp(verb, args, flags) {
399
486
  case "state-delete":
400
487
  return { op: "state.delete", target: a0, strategy: flags["strategy"], replacement: flags["replacement"] };
401
488
  case "add-actor":
402
- return { op: "actor.add", name: a0, role_type: flags["role"], description: flags["description"], aliases: flags["alias"] ?? undefined };
489
+ return { op: "actor.add", actor_id: flags["id"], name: a0, role_type: flags["role"], description: flags["description"], aliases: flags["alias"] ?? undefined };
403
490
  case "add-location":
404
- return { op: "location.add", name: a0, description: flags["description"] };
491
+ return { op: "location.add", location_id: flags["id"], name: a0, description: flags["description"] };
405
492
  case "add-prop":
406
- return { op: "prop.add", name: a0, description: flags["description"] };
493
+ return { op: "prop.add", prop_id: flags["id"], name: a0, description: flags["description"] };
407
494
  case "add-episode":
408
495
  return { op: "episode.add", title: flags["title"], episode_id: flags["id"] };
409
496
  case "importance":
410
497
  return { op: "importance.set", target: a0, importance: a1 };
411
498
  case "split":
412
- return { op: "scene.split", at: a0, at_index: flags["at"], new_scene_id: flags["new-scene-id"] };
499
+ return scriptVersion === 4
500
+ ? { op: "scene.split", at: a0, at_action: flags["at"], new_scene_id: flags["new-scene-id"] }
501
+ : { op: "scene.split", at: a0, at_index: Number(flags["at"]), new_scene_id: flags["new-scene-id"] };
413
502
  case "delete": {
414
- const addr = parseAnyAddress(a0);
503
+ const addr = parseAddress(a0);
415
504
  if (addr.kind === "action")
416
505
  return { op: "action.delete", at: a0 };
417
506
  if (addr.kind === "scene")
418
507
  return { op: "scene.delete", at: a0, force: Boolean(flags["force"]) };
508
+ if (addr.kind === "state")
509
+ return { op: "state.delete", target: a0, strategy: flags["strategy"], replacement: flags["replacement"] };
419
510
  if (addr.kind === "asset")
420
511
  return { op: "asset.delete", target: a0, strategy: flags["strategy"], replacement: flags["replacement"] };
421
- throw doErr(`delete cannot operate on a ${addr.kind} address (${a0}); episodes/states have no delete op`);
512
+ throw doErr(`delete cannot operate on a ${addr.kind} address (${a0})`);
422
513
  }
423
514
  case "move": {
424
- const addr = parseAnyAddress(a0);
425
- if (addr.kind === "scene")
426
- return { op: "scene.move", at: a0, to: a1, at_index: flags["at"] };
427
- if (addr.kind === "action")
428
- return { op: "action.move", at: a0, to: a1 };
515
+ assertMoveCommandFlags(scriptVersion, {
516
+ at: flags["at"],
517
+ before: flags["before"],
518
+ after: flags["after"],
519
+ });
520
+ const addr = parseAddress(a0);
521
+ if (addr.kind === "scene") {
522
+ return scriptVersion === 4
523
+ ? { op: "scene.move", at: a0, to: a1, before: flags["before"], after: flags["after"] }
524
+ : { op: "scene.move", at: a0, to: a1, at_index: flags["at"] };
525
+ }
526
+ if (addr.kind === "action") {
527
+ return scriptVersion === 4
528
+ ? { op: "action.move", at: a0, to: a1, before: flags["before"], after: flags["after"] }
529
+ : { op: "action.move", at: a0, to: a1 };
530
+ }
429
531
  throw doErr(`move cannot operate on a ${addr.kind} address (${a0}); move an action or a scene`);
430
532
  }
431
533
  case "merge": {
432
- const addr = parseAnyAddress(a0);
534
+ const addr = parseAddress(a0);
433
535
  if (addr.kind === "scene")
434
536
  return { op: "scene.merge", from: a0, into: strOf(flags["into"]) };
435
537
  if (addr.kind === "asset")
@@ -437,12 +539,46 @@ export function lineToOp(verb, args, flags) {
437
539
  throw doErr(`merge cannot operate on a ${addr.kind} address (${a0}); merge two assets or two scenes`);
438
540
  }
439
541
  case "insert": {
440
- const addr = parseAnyAddress(a0);
542
+ const addr = parseAddress(a0);
543
+ const insertFlags = doInsertFlags(flags);
441
544
  if (addr.kind === "episode") {
442
- 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"] };
545
+ assertInsertCommandFlags(scriptVersion, "scene", insertFlags);
546
+ return {
547
+ op: "scene.insert",
548
+ at: a0,
549
+ location: flags["location"],
550
+ time: flags["time"],
551
+ space: flags["space"],
552
+ ...(scriptVersion === 4
553
+ ? { location_state_ids: insertFlags.states, before: flags["before"], after: flags["after"] }
554
+ : { at_index: flags["at"], before: flags["before"], after: flags["after"] }),
555
+ scene_id: flags["scene-id"],
556
+ };
443
557
  }
444
558
  if (addr.kind === "scene") {
445
- 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"], kind: flags["kind"], label: flags["label"], emotion: flags["emotion"] };
559
+ assertInsertCommandFlags(scriptVersion, "action", insertFlags);
560
+ return {
561
+ op: "action.insert",
562
+ at: a0,
563
+ type: flags["type"],
564
+ content: flags["content"],
565
+ before: flags["before"],
566
+ after: flags["after"],
567
+ actor_id: flags["actor"],
568
+ kind: flags["kind"],
569
+ label: flags["label"],
570
+ ...(scriptVersion === 4
571
+ ? {
572
+ action_id: flags["action-id"],
573
+ state_id: flags["speaker-state"],
574
+ parenthetical: flags["parenthetical"],
575
+ }
576
+ : {
577
+ at_index: flags["at"],
578
+ speaker_id: flags["speaker"],
579
+ emotion: flags["emotion"],
580
+ }),
581
+ };
446
582
  }
447
583
  throw doErr(`insert needs ep_NNN (scene insert) or ep_NNN/scn_NNN (action insert), got a ${addr.kind} address (${a0})`);
448
584
  }
@@ -519,65 +655,402 @@ export const SKIPPABLE_EDIT_CODES = new Set([
519
655
  "FROM_TEXT_NOT_FOUND",
520
656
  "OVERLAP_DIALOGUE_UNSUPPORTED",
521
657
  ]);
658
+ export function applyPatchOperationsForSession(session, source, operations) {
659
+ return applyVersionedMutations(session.version, session.script, source, operations);
660
+ }
661
+ function v4Counts(script) {
662
+ return {
663
+ episodes: script.episodes.length,
664
+ scenes: script.episodes.reduce((sum, episode) => sum + episode.scenes.length, 0),
665
+ actions: script.episodes.reduce((sum, episode) => sum + episode.scenes.reduce((sceneSum, scene) => sceneSum + scene.actions.length, 0), 0),
666
+ actors: script.actors.length,
667
+ locations: script.locations.length,
668
+ props: script.props.length,
669
+ states: [...script.actors, ...script.locations, ...script.props]
670
+ .reduce((sum, asset) => sum + asset.states.length, 0),
671
+ };
672
+ }
673
+ function v4CanonicalIds(script) {
674
+ return [
675
+ ...script.actors.flatMap((asset) => [
676
+ asset.actor_id,
677
+ ...asset.states.map((state) => `${asset.actor_id}/${state.state_id}`),
678
+ ]),
679
+ ...script.locations.flatMap((asset) => [
680
+ asset.location_id,
681
+ ...asset.states.map((state) => `${asset.location_id}/${state.state_id}`),
682
+ ]),
683
+ ...script.props.flatMap((asset) => [
684
+ asset.prop_id,
685
+ ...asset.states.map((state) => `${asset.prop_id}/${state.state_id}`),
686
+ ]),
687
+ ...script.episodes.flatMap((episode) => [
688
+ episode.episode_id,
689
+ ...episode.scenes.flatMap((scene) => [
690
+ scene.scene_id,
691
+ ...scene.actions.map((action) => action.action_id),
692
+ ]),
693
+ ]),
694
+ ];
695
+ }
696
+ function orderedIntersection(left, right) {
697
+ return left.filter((value) => right.has(value));
698
+ }
699
+ function v4OperationTargets(operation) {
700
+ const values = [];
701
+ const isIdentity = (value) => {
702
+ if (value === "global")
703
+ return true;
704
+ if (isV4Id("episode", value)
705
+ || isV4Id("scene", value)
706
+ || isV4Id("action", value)
707
+ || isV4Id("actor", value)
708
+ || isV4Id("location", value)
709
+ || isV4Id("prop", value))
710
+ return true;
711
+ const slash = value.indexOf("/");
712
+ if (slash < 0 || slash !== value.lastIndexOf("/"))
713
+ return false;
714
+ const owner = value.slice(0, slash);
715
+ const state = value.slice(slash + 1);
716
+ return (isV4Id("actor", owner)
717
+ || isV4Id("location", owner)
718
+ || isV4Id("prop", owner)) && (state === "default" || isV4Id("state", state));
719
+ };
720
+ const add = (value) => {
721
+ if (typeof value !== "string" || value.trim() === "")
722
+ return;
723
+ const normalized = value.trim();
724
+ if (isIdentity(normalized))
725
+ values.push(normalized);
726
+ };
727
+ const first = (...fields) => {
728
+ for (const field of fields) {
729
+ const value = operation[field];
730
+ if (typeof value === "string" && value.trim() !== "")
731
+ return value;
732
+ }
733
+ return undefined;
734
+ };
735
+ const addState = (ownerValue, stateValue) => {
736
+ const owner = strOf(ownerValue).trim();
737
+ const state = strOf(stateValue).trim();
738
+ if (owner && state)
739
+ add(`${owner}/${state}`);
740
+ };
741
+ const addStates = (ownerValue, stateValues) => {
742
+ if (!Array.isArray(stateValues))
743
+ return;
744
+ stateValues.forEach((state) => addState(ownerValue, state));
745
+ };
746
+ const addPlacement = () => {
747
+ add(operation["before"]);
748
+ add(operation["after"]);
749
+ };
750
+ const kind = strOf(operation["op"]);
751
+ switch (kind) {
752
+ case "action.parenthetical.set":
753
+ case "action.parenthetical.remove":
754
+ case "action.content.replace":
755
+ case "action.content.regex-replace":
756
+ case "action.type.set":
757
+ case "action.delete":
758
+ case "action.transition.clear":
759
+ add(operation["at"]);
760
+ break;
761
+ case "action.actor.set":
762
+ case "dialogue.speaker.set":
763
+ add(operation["at"]);
764
+ add(operation["actor_id"]);
765
+ addState(operation["actor_id"], operation["state_id"]);
766
+ break;
767
+ case "action.insert":
768
+ add(operation["at"]);
769
+ add(first("action_id", "id"));
770
+ add(operation["actor_id"]);
771
+ addState(operation["actor_id"], operation["state_id"]);
772
+ addPlacement();
773
+ break;
774
+ case "action.move":
775
+ add(operation["at"]);
776
+ add(operation["to"]);
777
+ addPlacement();
778
+ break;
779
+ case "action.transition.set":
780
+ add(operation["at"]);
781
+ add(operation["target"]);
782
+ break;
783
+ case "scene.ref.set":
784
+ case "scene.ref.remove":
785
+ add(operation["at"]);
786
+ add(operation["target"]);
787
+ addStates(operation["target"], operation["state_ids"]);
788
+ break;
789
+ case "actor.add":
790
+ add(first("actor_id", "id"));
791
+ break;
792
+ case "location.add":
793
+ add(first("location_id", "id"));
794
+ break;
795
+ case "prop.add":
796
+ add(first("prop_id", "id"));
797
+ break;
798
+ case "state.add": {
799
+ const owner = operation["target"];
800
+ add(owner);
801
+ addState(owner, first("state_id", "id"));
802
+ break;
803
+ }
804
+ case "state.rename":
805
+ case "state.describe":
806
+ add(operation["target"]);
807
+ break;
808
+ case "state.delete": {
809
+ const target = strOf(operation["target"]).trim();
810
+ add(target);
811
+ const slash = target.indexOf("/");
812
+ if (slash > 0)
813
+ addState(target.slice(0, slash), operation["replacement"]);
814
+ break;
815
+ }
816
+ case "asset.rename":
817
+ case "asset.describe":
818
+ case "asset.role.set":
819
+ case "asset.alias.set":
820
+ case "asset.alias.add":
821
+ case "asset.alias.remove":
822
+ case "importance.set":
823
+ add(operation["target"]);
824
+ break;
825
+ case "asset.merge":
826
+ add(first("from", "source"));
827
+ add(first("into", "target"));
828
+ break;
829
+ case "asset.delete":
830
+ add(operation["target"]);
831
+ if (operation["strategy"] === "replace")
832
+ add(operation["replacement"]);
833
+ break;
834
+ case "episode.add":
835
+ add(first("episode_id", "id"));
836
+ break;
837
+ case "episode.synopsis.set":
838
+ add(first("episode_id", "target", "at"));
839
+ break;
840
+ case "meta.worldview.set":
841
+ case "meta.synopsis.set":
842
+ add("global");
843
+ break;
844
+ case "scene.insert": {
845
+ const location = first("location", "location_id");
846
+ add(first("at", "episode_id"));
847
+ add(first("scene_id", "id"));
848
+ add(location);
849
+ addStates(location, operation["location_state_ids"]);
850
+ addPlacement();
851
+ break;
852
+ }
853
+ case "scene.move":
854
+ add(operation["at"]);
855
+ add(operation["to"]);
856
+ addPlacement();
857
+ break;
858
+ case "scene.delete":
859
+ add(operation["at"]);
860
+ break;
861
+ case "scene.split":
862
+ add(operation["at"]);
863
+ add(first("at_action", "action_id"));
864
+ add(first("new_scene_id", "scene_id"));
865
+ break;
866
+ case "scene.merge":
867
+ add(operation["from"]);
868
+ add(operation["into"]);
869
+ break;
870
+ case "extend.set":
871
+ case "extend.clear":
872
+ add(operation["at"]);
873
+ break;
874
+ default:
875
+ throw new Error(`V4 plan target mapping is missing for operation ${kind}`);
876
+ }
877
+ return [...new Set(values)];
878
+ }
879
+ export function buildV4MutationPlan(session, operations, options = {}) {
880
+ if (session.version !== 4) {
881
+ throw new CliError("A v4 mutation plan requires Script v4.", {
882
+ exitCode: EXIT_USAGE,
883
+ errorCode: "V4_PLAN_VERSION_INVALID",
884
+ data: { actual: [`Script v${session.version}`] },
885
+ });
886
+ }
887
+ const original = requireScriptV4(session.script);
888
+ const proposed = structuredClone(original);
889
+ const originalIds = v4CanonicalIds(original);
890
+ const applied = [];
891
+ const steps = [];
892
+ for (const [index, operation] of operations.entries()) {
893
+ const beforeIds = v4CanonicalIds(proposed);
894
+ const beforeSet = new Set(beforeIds);
895
+ const before = v4Counts(proposed);
896
+ let status = "applied";
897
+ try {
898
+ applied.push(...applyPatchOperationsV4(proposed, [operation]));
899
+ }
900
+ catch (error) {
901
+ if (error instanceof CliError
902
+ && error.errorCode
903
+ && options.skipCodes?.has(error.errorCode)) {
904
+ status = "skipped";
905
+ }
906
+ else {
907
+ throw error;
908
+ }
909
+ }
910
+ const afterIds = v4CanonicalIds(proposed);
911
+ const afterSet = new Set(afterIds);
912
+ steps.push({
913
+ index,
914
+ operation,
915
+ targets: v4OperationTargets(operation),
916
+ status,
917
+ before,
918
+ after: v4Counts(proposed),
919
+ created_ids: afterIds.filter((id) => !beforeSet.has(id)),
920
+ removed_ids: beforeIds.filter((id) => !afterSet.has(id)),
921
+ retained_ids: orderedIntersection(beforeIds, afterSet),
922
+ });
923
+ }
924
+ const proposedIds = v4CanonicalIds(proposed);
925
+ const originalSet = new Set(originalIds);
926
+ const proposedSet = new Set(proposedIds);
927
+ const targets = [...new Set(steps.flatMap((step) => step.targets))];
928
+ const touched = new Set([
929
+ ...targets,
930
+ ...steps.flatMap((step) => step.created_ids),
931
+ ...steps.flatMap((step) => step.removed_ids),
932
+ ]);
933
+ const plan = {
934
+ version: 1,
935
+ script_version: 4,
936
+ baseline: session.baselineHash,
937
+ operations,
938
+ targets,
939
+ steps,
940
+ before: v4Counts(original),
941
+ after: v4Counts(proposed),
942
+ created_ids: proposedIds.filter((id) => !originalSet.has(id)),
943
+ removed_ids: originalIds.filter((id) => !proposedSet.has(id)),
944
+ retained_ids: orderedIntersection(originalIds, proposedSet),
945
+ untouched_ids: orderedIntersection(originalIds, proposedSet)
946
+ .filter((id) => !touched.has(id)),
947
+ validation: validateScriptV4(proposed),
948
+ };
949
+ return {
950
+ plan,
951
+ digest: `sha256:${sha256Text(JSON.stringify(sortDeep(plan)))}`,
952
+ applied,
953
+ };
954
+ }
955
+ const V4_PLANNED_ID_FIELDS = {
956
+ "actor.add": "actor_id",
957
+ "location.add": "location_id",
958
+ "prop.add": "prop_id",
959
+ "state.add": "state_id",
960
+ "episode.add": "episode_id",
961
+ "scene.insert": "scene_id",
962
+ "scene.split": "new_scene_id",
963
+ "action.insert": "action_id",
964
+ };
965
+ export function assertV4PlanHasExplicitNewIds(operations) {
966
+ for (const operation of operations) {
967
+ const kind = strOf(operation["op"]);
968
+ const field = V4_PLANNED_ID_FIELDS[kind];
969
+ if (!field || strOf(operation[field]).trim())
970
+ continue;
971
+ const idKind = field === "new_scene_id" ? "scene" : field.replace(/_id$/, "");
972
+ throw new CliError("Planned creation operations require an explicit candidate ID.", {
973
+ exitCode: EXIT_USAGE,
974
+ errorCode: "V4_PLAN_ID_REQUIRED",
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>`] },
977
+ });
978
+ }
979
+ }
522
980
  // ---------------------------------------------------------------------------
523
981
  // Single-op helpers
524
982
  // ---------------------------------------------------------------------------
525
- export async function applySingleScriptOp(opts, op) {
983
+ export async function applySingleScriptOpToSession(session, opts, op) {
984
+ const plan = strOf(opts["plan"]).trim();
985
+ if (plan) {
986
+ throw new CliError("Single-point writes do not use plan digests.", {
987
+ exitCode: EXIT_USAGE,
988
+ errorCode: "MULTI_PLAN_USAGE_INVALID",
989
+ hint: "Remove --plan from this single-point edit.",
990
+ data: { actual: [plan] },
991
+ });
992
+ }
993
+ return applyScriptOpsToSession(session, opts, [op]);
994
+ }
995
+ export async function applyScriptOpsToSession(session, opts, operations) {
526
996
  const workspace = strOf(opts["workspace_path"] || "workspace");
527
- const session = await loadScriptForEdit(opts);
528
- const script = session.script;
529
- const applied = applyPatchOperations(script, scriptSourceText(workspace), [op]);
530
- const opName = applied[0] ?? strOf(op["op"]);
997
+ const applied = applyPatchOperationsForSession(session, scriptSourceText(workspace), operations);
998
+ const opName = applied.length === 1 ? applied[0] : "script.ops";
531
999
  const [newRevision, idempotent] = await saveScriptSession(session, opts, opName);
532
1000
  const validation = validateSession(session);
533
1001
  await syncValidationResult(session, validation, newRevision);
534
1002
  const passed = Boolean(validation["passed"]);
535
1003
  // One outcome line: what changed + revision (remote) + validation verdict.
536
1004
  const rev = session.remote ? `; rev ${newRevision}` : "";
537
- const resultLine = `${opName} applied${rev}; validation ${passed ? "passed" : "needs repair"}`;
1005
+ const resultLine = `${applied.join(", ")} applied${rev}; validation ${passed ? "passed" : "needs repair"}`;
1006
+ const issues = summarizeIssues(asList(validation["issues"]));
538
1007
  const report = {
539
- title: passed ? "SCRIPT OP APPLIED" : "SCRIPT OP APPLIED: Validation needs repair",
540
- op: opName,
541
- changed: applied.length > 0,
542
- ...(session.remote ? { idempotent } : {}),
543
- warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
544
- result: [resultLine],
545
- issues: summarizeIssues(asList(validation["issues"])),
546
- artifacts: [session.artifactLabel],
547
- // Success carries no guidance; only a failed validation needs a next step.
548
- 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
+ },
549
1018
  };
550
1019
  return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
551
1020
  }
1021
+ export async function applySingleScriptOp(opts, op) {
1022
+ const session = await loadScriptForEdit(opts);
1023
+ return applySingleScriptOpToSession(session, opts, op);
1024
+ }
552
1025
  export function summarizeScriptOp(_script, op) {
553
1026
  const kind = strOf(op["op"]);
554
1027
  if (kind === "actor.add")
555
- 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"]})` : ""}`;
556
1029
  if (kind === "location.add")
557
- return `已新增场景地点 ${op["name"] || op["location_name"]}`;
1030
+ return `Added location ${op["name"] || op["location_name"]}`;
558
1031
  if (kind === "prop.add")
559
- return `已新增道具 ${op["name"] || op["prop_name"]}`;
1032
+ return `Added prop ${op["name"] || op["prop_name"]}`;
560
1033
  if (kind === "episode.add")
561
- 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"]}` : ""}`;
562
1035
  if (kind === "meta.synopsis.set")
563
- return `已更新全剧梗概`;
1036
+ return "Updated script synopsis";
564
1037
  if (kind === "episode.synopsis.set")
565
- return `已更新分集梗概 ${op["episode_id"] ?? op["target"] ?? op["at"]}`;
1038
+ return `Updated episode synopsis ${op["episode_id"] ?? op["target"] ?? op["at"]}`;
566
1039
  if (kind === "state.add")
567
- 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"]}`;
568
1041
  if (kind === "importance.set")
569
- return `已将 ${op["target"]} 重要度设为 ${op["importance"]}`;
1042
+ return `Set ${op["target"]} importance to ${op["importance"]}`;
570
1043
  if (kind === "scene.ref.set")
571
- return `已在 ${op["at"]} 设置引用 ${op["target"]}${op["state"] ? `(状态 ${op["state"]})` : ""}`;
1044
+ return `Set reference ${op["target"]} on ${op["at"]}${op["state"] ? ` (state ${op["state"]})` : ""}`;
572
1045
  if (kind === "extend.set")
573
- return `已在 ${op["at"]} 设置 extend.${op["key"]}`;
1046
+ return `Set extend.${op["key"]} on ${op["at"]}`;
574
1047
  if (kind === "extend.clear")
575
- 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"]}`;
576
1049
  if (kind.startsWith("state."))
577
- return `已执行 ${kind}: ${op["target"]}`;
1050
+ return `Applied ${kind}: ${op["target"]}`;
578
1051
  if (kind.startsWith("dialogue."))
579
- return `已执行 ${kind}: ${op["at"]}`;
580
- return `已执行 ${kind}`;
1052
+ return `Applied ${kind}: ${op["at"]}`;
1053
+ return `Applied ${kind}`;
581
1054
  }
582
1055
  // Accept `/re/flags` as-is; wrap a bare pattern as `/pattern/` so
583
1056
  // `replace --regex --from '^\s*\[[^\]]*\]'` works without the user typing slashes.
@@ -593,61 +1066,129 @@ export function regexPattern(raw) {
593
1066
  // reports the blast radius and writes nothing; --apply commits. Per-action
594
1067
  // "no match here / overlap dialogue" errors are skipped (a sweep); anything else
595
1068
  // aborts the whole edit before any write. Shared by every selector-capable verb.
596
- export async function applySelectorEdits(opts, template, verbLabel) {
1069
+ export async function applySelectorEditsToSession(session, opts, template, verbLabel) {
597
1070
  const workspace = strOf(opts["workspace_path"] || "workspace");
598
1071
  const apply = Boolean(opts["apply"]);
599
1072
  const opName = strOf(template["op"]);
600
- const session = await loadScriptForEdit(opts);
601
1073
  const script = session.script;
602
- const targets = resolveActionTargets(script, opts);
603
- const refs = targets.map((t) => `${t.epId}/${t.sceneId}#${t.idx}`);
1074
+ const refs = session.version === 3
1075
+ ? resolveActionTargets(script, opts).map((target) => `${target.epId}/${target.sceneId}#${target.idx}`)
1076
+ : resolveActionTargetsV4(requireScriptV4(script), opts, { allowOrdinals: false })
1077
+ .map((target) => target.action.action_id);
1078
+ const operations = refs.map((ref) => ({ ...template, at: ref }));
1079
+ const multiV4 = session.version === 4 && refs.length > 1;
1080
+ const planned = multiV4
1081
+ ? buildV4MutationPlan(session, operations, { skipCodes: SKIPPABLE_EDIT_CODES })
1082
+ : null;
1083
+ const planDigest = planned?.digest ?? null;
1084
+ const suppliedPlan = strOf(opts["plan"]).trim();
1085
+ if (suppliedPlan && (!multiV4 || !apply)) {
1086
+ throw new CliError("A plan digest is only accepted for a v4 multi-target apply.", {
1087
+ exitCode: EXIT_USAGE,
1088
+ errorCode: "MULTI_PLAN_USAGE_INVALID",
1089
+ hint: "Remove --plan, or apply the same v4 multi-target selection that produced the digest.",
1090
+ data: { actual: [suppliedPlan] },
1091
+ });
1092
+ }
1093
+ if (multiV4 && apply && !suppliedPlan) {
1094
+ throw new CliError("A v4 multi-target write requires its dry-run plan digest.", {
1095
+ exitCode: EXIT_USAGE,
1096
+ errorCode: "MULTI_PLAN_REQUIRED",
1097
+ hint: "Run the same command without --apply, review the full plan, then retry with `--apply --plan <digest>`.",
1098
+ data: { expected: [planDigest ?? ""], actual: ["<missing>"] },
1099
+ });
1100
+ }
1101
+ if (multiV4 && apply && suppliedPlan !== planDigest) {
1102
+ throw new CliError("The supplied digest does not match the current targets and operations.", {
1103
+ exitCode: EXIT_INPUT,
1104
+ errorCode: "MULTI_PLAN_MISMATCH",
1105
+ hint: "Review the newly computed dry-run plan, then apply its exact digest.",
1106
+ data: { expected: [planDigest ?? ""], actual: [suppliedPlan] },
1107
+ });
1108
+ }
604
1109
  const source = scriptSourceText(workspace);
605
- let applied = 0;
606
- let skipped = 0;
607
- for (const ref of refs) {
608
- try {
609
- applyPatchOperations(script, source, [{ ...template, at: ref }]);
610
- applied += 1;
1110
+ let applied = planned?.plan.steps.filter((step) => step.status === "applied").length ?? 0;
1111
+ let skipped = planned?.plan.steps.filter((step) => step.status === "skipped").length ?? 0;
1112
+ if (!planned) {
1113
+ for (const ref of refs) {
1114
+ try {
1115
+ applyPatchOperationsForSession(session, source, [{ ...template, at: ref }]);
1116
+ applied += 1;
1117
+ }
1118
+ catch (err) {
1119
+ if (err instanceof CliError && err.errorCode && SKIPPABLE_EDIT_CODES.has(err.errorCode)) {
1120
+ skipped += 1;
1121
+ continue;
1122
+ }
1123
+ throw err;
1124
+ }
611
1125
  }
612
- catch (err) {
613
- if (err instanceof CliError && err.errorCode && SKIPPABLE_EDIT_CODES.has(err.errorCode)) {
614
- skipped += 1;
615
- continue;
1126
+ }
1127
+ else if (apply) {
1128
+ for (const step of planned.plan.steps) {
1129
+ if (step.status === "applied") {
1130
+ applyPatchOperationsForSession(session, source, [step.operation]);
616
1131
  }
617
- throw err;
618
1132
  }
619
1133
  }
620
- const sample = refs.slice(0, 12);
1134
+ const displayedRefs = multiV4 ? refs : refs.slice(0, 12);
621
1135
  const blast = [
622
1136
  `verb: ${verbLabel} (${opName})`,
623
1137
  `matched: ${refs.length} action(s)`,
624
1138
  `${apply ? "applied" : "would-apply"}: ${applied}`,
625
1139
  `${apply ? "skipped" : "would-skip"}: ${skipped}`,
626
- ...(sample.length ? [`targets: ${sample.join(", ")}${refs.length > sample.length ? ` … (+${refs.length - sample.length} more)` : ""}`] : []),
1140
+ ...(displayedRefs.length ? [`targets: ${displayedRefs.join(", ")}${refs.length > displayedRefs.length ? ` … (+${refs.length - displayedRefs.length} more)` : ""}`] : []),
1141
+ ...(planDigest ? [`plan_digest: ${planDigest}`] : []),
627
1142
  ];
628
1143
  if (!apply) {
629
- const validation = validateSession(session);
630
- const passed = Boolean(validation["passed"]);
1144
+ const legacyValidation = planned ? null : validateSession(session);
1145
+ const passed = planned
1146
+ ? planned.plan.validation.ok
1147
+ : Boolean(legacyValidation?.["passed"]);
1148
+ const plannedIssueLines = planned
1149
+ ? [...planned.plan.validation.errors, ...planned.plan.validation.warnings]
1150
+ .map((issue) => `${issue.path}: ${issue.code} - ${issue.message}`)
1151
+ : null;
1152
+ const legacyIssueLines = legacyValidation === null
1153
+ ? []
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];
631
1162
  const report = {
632
- title: refs.length === 0 ? "SELECTOR EDIT DRY-RUN: no match" : "SELECTOR EDIT DRY-RUN (no write)",
633
- op: `${opName}.dry-run`,
634
- changed: false,
635
- result: [...blast, `validation: ${passed ? "would pass" : "would need repair"}; dry-run, no write`],
636
- warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
637
- issues: summarizeIssues(asList(validation["issues"])),
638
- artifacts: [session.artifactLabel],
639
- next: [refs.length === 0 ? "Widen the selection (--in / --grep / predicates)." : "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
+ },
640
1180
  };
641
1181
  return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
642
1182
  }
643
1183
  if (refs.length === 0) {
644
1184
  return [{
645
- title: "SELECTOR EDIT: no match",
646
- op: opName,
647
- changed: false,
648
- result: blast,
649
- artifacts: [session.artifactLabel],
650
- 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
+ },
651
1192
  }, EXIT_OK];
652
1193
  }
653
1194
  const [newRevision, idempotent] = await saveScriptSession(session, opts, opName);
@@ -655,26 +1196,42 @@ export async function applySelectorEdits(opts, template, verbLabel) {
655
1196
  await syncValidationResult(session, validation, newRevision);
656
1197
  const passed = Boolean(validation["passed"]);
657
1198
  const rev = session.remote ? `; rev ${newRevision}` : "";
1199
+ const issues = summarizeIssues(asList(validation["issues"]));
658
1200
  const report = {
659
- title: passed ? "SELECTOR EDIT APPLIED" : "SELECTOR EDIT APPLIED: Validation needs repair",
660
- op: opName,
661
- changed: applied > 0,
662
- ...(session.remote ? { idempotent } : {}),
663
- result: [...blast, `validation: ${passed ? "passed" : "needs repair"}${rev}`],
664
- warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
665
- issues: summarizeIssues(asList(validation["issues"])),
666
- artifacts: [session.artifactLabel],
667
- 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
+ },
668
1221
  };
669
1222
  return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
670
1223
  }
1224
+ export async function applySelectorEdits(opts, template, verbLabel) {
1225
+ const session = await loadScriptForEdit(opts);
1226
+ return applySelectorEditsToSession(session, opts, template, verbLabel);
1227
+ }
671
1228
  export { sortDeep };
672
1229
  // ===========================================================================
673
1230
  // 0.6.0 plural-noun query commands (replace `commandScriptInspect --target X`)
674
1231
  // ===========================================================================
675
1232
  //
676
1233
  // Each command is a thin read-only wrapper around the underlying script
677
- // 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
678
1235
  // without the `--target` flag — the verb itself encodes the target. Filters
679
1236
  // use `--in <addr>` (compose with the address parsers) plus per-target
680
1237
  // predicates (--name / --kind / --has / --grep / --context / --type / --actor
@@ -725,17 +1282,19 @@ export function buildGrepMatcher(raw) {
725
1282
  if (m) {
726
1283
  const pattern = m[1];
727
1284
  if (isDangerousRegex(pattern)) {
728
- 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.", {
729
1286
  exitCode: EXIT_USAGE,
730
- required: [`pattern length <= ${MAX_GREP_PATTERN_LEN}; no quantifier-on-group-with-internal-quantifier (e.g. (a+)+)`],
731
- received: [`length=${pattern.length}, sample=${pattern.slice(0, 80)}`],
732
- nextSteps: ["Rewrite the regex to avoid nested quantifiers, or use a literal substring (drop the /…/ wrapper)."],
733
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)}`] },
734
1290
  });
735
1291
  }
736
1292
  try {
737
1293
  const re = new RegExp(pattern, m[2]);
738
- return (hay) => re.test(hay);
1294
+ return (hay) => {
1295
+ re.lastIndex = 0;
1296
+ return re.test(hay);
1297
+ };
739
1298
  }
740
1299
  catch {
741
1300
  // Malformed regex — treat as literal.
@@ -743,15 +1302,22 @@ export function buildGrepMatcher(raw) {
743
1302
  }
744
1303
  return (hay) => hay.includes(raw);
745
1304
  }
746
- export function buildReport(op, title, lines, artifactLabel) {
1305
+ export function buildReport(op, lines) {
1306
+ const outputLines = lines.length > 0 ? lines : ["No matches."];
747
1307
  return {
748
- title,
749
- op,
750
- changed: false,
751
- result: lines.length > 0 ? lines : ["No matching items."],
752
- artifacts: [artifactLabel],
1308
+ text: outputLines.join("\n"),
1309
+ data: {
1310
+ op,
1311
+ changed: false,
1312
+ },
753
1313
  };
754
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
+ }
755
1321
  // ----- summary --------------------------------------------------------------
756
1322
  export function synopsisPreview(text, limit) {
757
1323
  const oneLine = text.replace(/\s+/g, " ").trim();
@@ -815,12 +1381,11 @@ export function resolveActionTargets(script, opts) {
815
1381
  // "state_changes" (underscore) passed the filter-missing guard but bypassed
816
1382
  // every concrete predicate, returning the whole script.
817
1383
  if (hasFilter && !HAS_FILTERS.has(hasFilter)) {
818
- throw new CliError("QUERY BLOCKED: --has value invalid", "--has value invalid.", {
1384
+ throw new CliError("--has value invalid.", {
819
1385
  exitCode: EXIT_USAGE,
820
- required: [`--has: one of ${[...HAS_FILTERS].join(", ")}`],
821
- received: [hasFilter],
822
- nextSteps: ["Use a supported --has value."],
823
1386
  errorCode: "HAS_FILTER_INVALID",
1387
+ hint: "Use a supported --has value.",
1388
+ data: { expected: [`--has: one of ${[...HAS_FILTERS].join(", ")}`], actual: [hasFilter] },
824
1389
  });
825
1390
  }
826
1391
  const grepMatcher = buildGrepMatcher(strOf(opts["grep"]));
@@ -854,7 +1419,7 @@ export function resolveActionTargets(script, opts) {
854
1419
  }
855
1420
  // Whether a selection in `opts` has any filter at all (scope or predicate).
856
1421
  export function selectionHasAnyFilter(opts) {
857
- return Boolean(!parseInSelector(strOf(opts["in"])).isEmpty ||
1422
+ return Boolean(strOf(opts["in"]).trim() ||
858
1423
  strOf(opts["grep"]) || strOf(opts["type"]).trim() || strOf(opts["actor"]).trim() ||
859
1424
  strOf(opts["has"]).trim());
860
1425
  }
@@ -913,6 +1478,7 @@ export function assetAppearanceCounts(script, kind) {
913
1478
  }
914
1479
  export function listAssetsByKind(script, kind, opts) {
915
1480
  const lines = [];
1481
+ const rows = [];
916
1482
  const [key, idKey, nameKey] = (() => {
917
1483
  switch (kind) {
918
1484
  case "actor": return ["actors", "actor_id", "actor_name"];
@@ -989,8 +1555,42 @@ export function listAssetsByKind(script, kind, opts) {
989
1555
  extra.push(`scenes=${scns}`, `episodes_count=${eps}${arc}`);
990
1556
  }
991
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
+ });
992
1577
  }
993
- 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
+ };
994
1594
  }
995
1595
  export function splitSpeakerList(raw) {
996
1596
  if (Array.isArray(raw))
@@ -999,22 +1599,21 @@ export function splitSpeakerList(raw) {
999
1599
  }
1000
1600
  export function blankCreateReport(session, validation, idempotent) {
1001
1601
  const passed = Boolean(validation["passed"]);
1002
- const resultLines = ["created: blank script (schema v2)", `validation: ${passed ? "passed" : "needs repair"}`];
1602
+ const resultLines = [`created: blank script (Script v${session.version})`, `validation: ${passed ? "passed" : "needs repair"}`];
1003
1603
  if (session.remote) {
1004
1604
  resultLines.push(`revision: ${session.revision}`);
1005
1605
  if (idempotent !== null)
1006
1606
  resultLines.push(`idempotent: ${String(idempotent).toLowerCase()}`);
1007
1607
  }
1008
1608
  return {
1009
- title: passed ? "SCRIPT CREATED: Blank scaffold" : "SCRIPT CREATED: Validation needs repair",
1010
- op: "script.create",
1011
- changed: true,
1012
- result: resultLines,
1013
- warnings: summarizeIssues(asList(validation["issues"])),
1014
- artifacts: [session.artifactLabel],
1015
- next: [
1016
- "用 worldview 设定世界观,用 add-actor / add-location / add-prop 注册资产,用 add-episode + insert 搭出分集与场景。",
1017
- ],
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
+ },
1018
1617
  };
1019
1618
  }
1020
1619
  // ----- add-* (register new entities) ----------------------------------------
@@ -1023,12 +1622,11 @@ export function blankCreateReport(session, validation, idempotent) {
1023
1622
  export function requireNameArg(opts, verb, example) {
1024
1623
  const args = asList(opts["_args"]);
1025
1624
  if (args.length < 1) {
1026
- throw new CliError(`VERB BLOCKED: ${verb} name missing`, "Name missing.", {
1625
+ throw new CliError("Name missing.", {
1027
1626
  exitCode: EXIT_USAGE,
1028
- required: ["<name>"],
1029
- received: ["<empty>"],
1030
- nextSteps: [example],
1031
1627
  errorCode: "ARGS_MISSING",
1628
+ hint: example,
1629
+ data: { expected: ["<name>"], actual: ["<empty>"] },
1032
1630
  });
1033
1631
  }
1034
1632
  return args[0];