@lingjingai/scriptctl 0.35.0 → 0.36.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 (367) hide show
  1. package/README.md +21 -5
  2. package/changes/0.36.0.md +22 -0
  3. package/dist/bin.js +0 -0
  4. package/dist/cli.d.ts +2 -1
  5. package/dist/cli.js +52 -128
  6. package/dist/cli.js.map +1 -1
  7. package/dist/common.d.ts +0 -16
  8. package/dist/common.js +2 -342
  9. package/dist/common.js.map +1 -1
  10. package/dist/domain/ingest/apply-plan.d.ts +23 -0
  11. package/dist/domain/ingest/apply-plan.js +210 -0
  12. package/dist/domain/ingest/apply-plan.js.map +1 -0
  13. package/dist/domain/ingest/assembler.d.ts +57 -0
  14. package/dist/domain/ingest/assembler.js +181 -0
  15. package/dist/domain/ingest/assembler.js.map +1 -0
  16. package/dist/domain/ingest/chunker.d.ts +9 -0
  17. package/dist/domain/ingest/chunker.js +38 -0
  18. package/dist/domain/ingest/chunker.js.map +1 -0
  19. package/dist/domain/ingest/evidence.d.ts +5 -0
  20. package/dist/domain/ingest/evidence.js +66 -0
  21. package/dist/domain/ingest/evidence.js.map +1 -0
  22. package/dist/domain/ingest/govern.d.ts +3 -0
  23. package/dist/domain/ingest/govern.js +127 -0
  24. package/dist/domain/ingest/govern.js.map +1 -0
  25. package/dist/domain/ingest/jsonl-parser.d.ts +6 -0
  26. package/dist/domain/ingest/jsonl-parser.js +187 -0
  27. package/dist/domain/ingest/jsonl-parser.js.map +1 -0
  28. package/dist/domain/ingest/markdown-parser.d.ts +6 -0
  29. package/dist/domain/ingest/markdown-parser.js +186 -0
  30. package/dist/domain/ingest/markdown-parser.js.map +1 -0
  31. package/dist/domain/ingest/records.d.ts +71 -0
  32. package/dist/domain/ingest/records.js +9 -0
  33. package/dist/domain/ingest/records.js.map +1 -0
  34. package/dist/domain/ingest/renumber.d.ts +14 -0
  35. package/dist/domain/ingest/renumber.js +130 -0
  36. package/dist/domain/ingest/renumber.js.map +1 -0
  37. package/dist/domain/ingest/script-v3.d.ts +94 -0
  38. package/dist/domain/ingest/script-v3.js +7 -0
  39. package/dist/domain/ingest/script-v3.js.map +1 -0
  40. package/dist/domain/ingest/state-reconcile.d.ts +11 -0
  41. package/dist/domain/ingest/state-reconcile.js +90 -0
  42. package/dist/domain/ingest/state-reconcile.js.map +1 -0
  43. package/dist/domain/ingest/validation.d.ts +11 -0
  44. package/dist/domain/ingest/validation.js +168 -0
  45. package/dist/domain/ingest/validation.js.map +1 -0
  46. package/dist/domain/ingest/video-aggregate.d.ts +23 -0
  47. package/dist/domain/ingest/video-aggregate.js +89 -0
  48. package/dist/domain/ingest/video-aggregate.js.map +1 -0
  49. package/dist/domain/ingest/video-apply.d.ts +37 -0
  50. package/dist/domain/ingest/video-apply.js +236 -0
  51. package/dist/domain/ingest/video-apply.js.map +1 -0
  52. package/dist/domain/ingest/video-md-parser.d.ts +2 -0
  53. package/dist/domain/ingest/video-md-parser.js +233 -0
  54. package/dist/domain/ingest/video-md-parser.js.map +1 -0
  55. package/dist/domain/ingest/video-records.d.ts +56 -0
  56. package/dist/domain/ingest/video-records.js +9 -0
  57. package/dist/domain/ingest/video-records.js.map +1 -0
  58. package/dist/domain/loom/index.d.ts +2 -1
  59. package/dist/domain/loom/index.js +12 -10
  60. package/dist/domain/loom/index.js.map +1 -1
  61. package/dist/domain/loom/types.d.ts +3 -2
  62. package/dist/domain/script/lookups.d.ts +3 -2
  63. package/dist/domain/script/lookups.js +25 -8
  64. package/dist/domain/script/lookups.js.map +1 -1
  65. package/dist/domain/script/patch/apply.js +2 -2
  66. package/dist/domain/script/patch/apply.js.map +1 -1
  67. package/dist/domain/script/patch/helpers.d.ts +3 -7
  68. package/dist/domain/script/patch/helpers.js +23 -96
  69. package/dist/domain/script/patch/helpers.js.map +1 -1
  70. package/dist/domain/script/patch/ops-action.d.ts +0 -2
  71. package/dist/domain/script/patch/ops-action.js +34 -100
  72. package/dist/domain/script/patch/ops-action.js.map +1 -1
  73. package/dist/domain/script/patch/ops-asset.d.ts +1 -0
  74. package/dist/domain/script/patch/ops-asset.js +102 -155
  75. package/dist/domain/script/patch/ops-asset.js.map +1 -1
  76. package/dist/domain/script/patch/ops-dialogue.d.ts +1 -4
  77. package/dist/domain/script/patch/ops-dialogue.js +43 -220
  78. package/dist/domain/script/patch/ops-dialogue.js.map +1 -1
  79. package/dist/domain/script/patch/ops-extend.js +8 -2
  80. package/dist/domain/script/patch/ops-extend.js.map +1 -1
  81. package/dist/domain/script/patch/ops-meta.js.map +1 -1
  82. package/dist/domain/script/patch/ops-scene.js +26 -39
  83. package/dist/domain/script/patch/ops-scene.js.map +1 -1
  84. package/dist/domain/script/patch/ops-state.d.ts +3 -3
  85. package/dist/domain/script/patch/ops-state.js +33 -49
  86. package/dist/domain/script/patch/ops-state.js.map +1 -1
  87. package/dist/domain/script/patch/registry.js +9 -13
  88. package/dist/domain/script/patch/registry.js.map +1 -1
  89. package/dist/domain/script/refs.js +23 -111
  90. package/dist/domain/script/refs.js.map +1 -1
  91. package/dist/domain/script/scene-refs.d.ts +3 -0
  92. package/dist/domain/script/scene-refs.js +35 -0
  93. package/dist/domain/script/scene-refs.js.map +1 -0
  94. package/dist/domain/script/schema.js +17 -21
  95. package/dist/domain/script/schema.js.map +1 -1
  96. package/dist/domain/script/text-util.d.ts +7 -0
  97. package/dist/domain/script/text-util.js +91 -0
  98. package/dist/domain/script/text-util.js.map +1 -0
  99. package/dist/help-text.js +117 -410
  100. package/dist/help-text.js.map +1 -1
  101. package/dist/infra/converters.js +1 -1
  102. package/dist/infra/converters.js.map +1 -1
  103. package/dist/infra/episode-title.d.ts +4 -0
  104. package/dist/infra/episode-title.js +129 -0
  105. package/dist/infra/episode-title.js.map +1 -0
  106. package/dist/infra/ingest/review-renderer.d.ts +1 -0
  107. package/dist/infra/ingest/review-renderer.js +431 -0
  108. package/dist/infra/ingest/review-renderer.js.map +1 -0
  109. package/dist/infra/ingest/source-resolver.d.ts +21 -0
  110. package/dist/infra/ingest/source-resolver.js +51 -0
  111. package/dist/infra/ingest/source-resolver.js.map +1 -0
  112. package/dist/infra/ingest/text-reader.d.ts +3 -0
  113. package/dist/infra/ingest/text-reader.js +27 -0
  114. package/dist/infra/ingest/text-reader.js.map +1 -0
  115. package/dist/infra/ingest/video-discovery.d.ts +8 -0
  116. package/dist/infra/ingest/video-discovery.js +60 -0
  117. package/dist/infra/ingest/video-discovery.js.map +1 -0
  118. package/dist/infra/ingest/workspace-store.d.ts +18 -0
  119. package/dist/infra/ingest/workspace-store.js +70 -0
  120. package/dist/infra/ingest/workspace-store.js.map +1 -0
  121. package/dist/infra/llm/anthropic-model.d.ts +11 -0
  122. package/dist/infra/llm/anthropic-model.js +109 -0
  123. package/dist/infra/llm/anthropic-model.js.map +1 -0
  124. package/dist/infra/llm/gemini-upload.d.ts +9 -0
  125. package/dist/infra/llm/gemini-upload.js +94 -0
  126. package/dist/infra/llm/gemini-upload.js.map +1 -0
  127. package/dist/infra/llm/gemini-video-model.d.ts +47 -0
  128. package/dist/infra/llm/gemini-video-model.js +167 -0
  129. package/dist/infra/llm/gemini-video-model.js.map +1 -0
  130. package/dist/infra/llm/openai-compatible-model.d.ts +13 -0
  131. package/dist/infra/llm/openai-compatible-model.js +143 -0
  132. package/dist/infra/llm/openai-compatible-model.js.map +1 -0
  133. package/dist/infra/llm/proxy.d.ts +1 -0
  134. package/dist/infra/llm/proxy.js +30 -0
  135. package/dist/infra/llm/proxy.js.map +1 -0
  136. package/dist/infra/llm/retry.d.ts +5 -0
  137. package/dist/infra/llm/retry.js +20 -0
  138. package/dist/infra/llm/retry.js.map +1 -0
  139. package/dist/llm/composition.d.ts +5 -0
  140. package/dist/llm/composition.js +5 -0
  141. package/dist/llm/composition.js.map +1 -0
  142. package/dist/llm/config.d.ts +26 -0
  143. package/dist/llm/config.js +201 -0
  144. package/dist/llm/config.js.map +1 -0
  145. package/dist/llm/registry.d.ts +44 -0
  146. package/dist/llm/registry.js +48 -0
  147. package/dist/llm/registry.js.map +1 -0
  148. package/dist/llm/router.d.ts +23 -0
  149. package/dist/llm/router.js +0 -0
  150. package/dist/llm/router.js.map +1 -0
  151. package/dist/llm/tasks/ingest/asset-curate.d.ts +13 -0
  152. package/dist/llm/tasks/ingest/asset-curate.js +45 -0
  153. package/dist/llm/tasks/ingest/asset-curate.js.map +1 -0
  154. package/dist/llm/tasks/ingest/asset-extract.d.ts +12 -0
  155. package/dist/llm/tasks/ingest/asset-extract.js +51 -0
  156. package/dist/llm/tasks/ingest/asset-extract.js.map +1 -0
  157. package/dist/llm/tasks/ingest/asset-govern.d.ts +22 -0
  158. package/dist/llm/tasks/ingest/asset-govern.js +143 -0
  159. package/dist/llm/tasks/ingest/asset-govern.js.map +1 -0
  160. package/dist/llm/tasks/ingest/mention-resolve.d.ts +9 -0
  161. package/dist/llm/tasks/ingest/mention-resolve.js +39 -0
  162. package/dist/llm/tasks/ingest/mention-resolve.js.map +1 -0
  163. package/dist/llm/tasks/ingest/plot-speaker.d.ts +11 -0
  164. package/dist/llm/tasks/ingest/plot-speaker.js +72 -0
  165. package/dist/llm/tasks/ingest/plot-speaker.js.map +1 -0
  166. package/dist/llm/tasks/ingest/roster-merge.d.ts +46 -0
  167. package/dist/llm/tasks/ingest/roster-merge.js +235 -0
  168. package/dist/llm/tasks/ingest/roster-merge.js.map +1 -0
  169. package/dist/llm/tasks/ingest/state-merge.d.ts +10 -0
  170. package/dist/llm/tasks/ingest/state-merge.js +84 -0
  171. package/dist/llm/tasks/ingest/state-merge.js.map +1 -0
  172. package/dist/llm/tasks/ingest/state-reconcile.d.ts +8 -0
  173. package/dist/llm/tasks/ingest/state-reconcile.js +44 -0
  174. package/dist/llm/tasks/ingest/state-reconcile.js.map +1 -0
  175. package/dist/llm/tasks/ingest/text-normalize.d.ts +10 -0
  176. package/dist/llm/tasks/ingest/text-normalize.js +89 -0
  177. package/dist/llm/tasks/ingest/text-normalize.js.map +1 -0
  178. package/dist/llm/tasks/ingest/video-correct.d.ts +30 -0
  179. package/dist/llm/tasks/ingest/video-correct.js +144 -0
  180. package/dist/llm/tasks/ingest/video-correct.js.map +1 -0
  181. package/dist/llm/tasks/ingest/video-transcribe.d.ts +4 -0
  182. package/dist/llm/tasks/ingest/video-transcribe.js +105 -0
  183. package/dist/llm/tasks/ingest/video-transcribe.js.map +1 -0
  184. package/dist/llm/tasks/schemas.d.ts +5 -0
  185. package/dist/llm/tasks/schemas.js +107 -0
  186. package/dist/llm/tasks/schemas.js.map +1 -0
  187. package/dist/llm/types.d.ts +97 -0
  188. package/dist/llm/types.js +13 -0
  189. package/dist/llm/types.js.map +1 -0
  190. package/dist/usecases/doctor.d.ts +1 -1
  191. package/dist/usecases/doctor.js +36 -9
  192. package/dist/usecases/doctor.js.map +1 -1
  193. package/dist/usecases/ingest/command.d.ts +5 -0
  194. package/dist/usecases/ingest/command.js +57 -0
  195. package/dist/usecases/ingest/command.js.map +1 -0
  196. package/dist/usecases/ingest/fanout.d.ts +10 -0
  197. package/dist/usecases/ingest/fanout.js +23 -0
  198. package/dist/usecases/ingest/fanout.js.map +1 -0
  199. package/dist/usecases/ingest/pass4-mention-resolve.d.ts +11 -0
  200. package/dist/usecases/ingest/pass4-mention-resolve.js +52 -0
  201. package/dist/usecases/ingest/pass4-mention-resolve.js.map +1 -0
  202. package/dist/usecases/ingest/pass5-extract.d.ts +7 -0
  203. package/dist/usecases/ingest/pass5-extract.js +50 -0
  204. package/dist/usecases/ingest/pass5-extract.js.map +1 -0
  205. package/dist/usecases/ingest/pass5-govern.d.ts +13 -0
  206. package/dist/usecases/ingest/pass5-govern.js +155 -0
  207. package/dist/usecases/ingest/pass5-govern.js.map +1 -0
  208. package/dist/usecases/ingest/pipeline.d.ts +28 -0
  209. package/dist/usecases/ingest/pipeline.js +144 -0
  210. package/dist/usecases/ingest/pipeline.js.map +1 -0
  211. package/dist/usecases/ingest/publish-command.d.ts +4 -0
  212. package/dist/usecases/ingest/publish-command.js +77 -0
  213. package/dist/usecases/ingest/publish-command.js.map +1 -0
  214. package/dist/usecases/ingest/status.d.ts +39 -0
  215. package/dist/usecases/ingest/status.js +155 -0
  216. package/dist/usecases/ingest/status.js.map +1 -0
  217. package/dist/usecases/ingest/video-pass3.d.ts +26 -0
  218. package/dist/usecases/ingest/video-pass3.js +0 -0
  219. package/dist/usecases/ingest/video-pass3.js.map +1 -0
  220. package/dist/usecases/ingest/video-pipeline.d.ts +32 -0
  221. package/dist/usecases/ingest/video-pipeline.js +202 -0
  222. package/dist/usecases/ingest/video-pipeline.js.map +1 -0
  223. package/dist/usecases/ingest/view-command.d.ts +4 -0
  224. package/dist/usecases/ingest/view-command.js +51 -0
  225. package/dist/usecases/ingest/view-command.js.map +1 -0
  226. package/dist/usecases/script/actions.js +2 -2
  227. package/dist/usecases/script/actions.js.map +1 -1
  228. package/dist/usecases/script/actor.js.map +1 -1
  229. package/dist/usecases/script/actors.js.map +1 -1
  230. package/dist/usecases/script/add-actor.js.map +1 -1
  231. package/dist/usecases/script/add-episode.js.map +1 -1
  232. package/dist/usecases/script/add-location.js.map +1 -1
  233. package/dist/usecases/script/add-prop.js.map +1 -1
  234. package/dist/usecases/script/alias.js.map +1 -1
  235. package/dist/usecases/script/assets.js.map +1 -1
  236. package/dist/usecases/script/context.js.map +1 -1
  237. package/dist/usecases/script/create.js +3 -6
  238. package/dist/usecases/script/create.js.map +1 -1
  239. package/dist/usecases/script/delete.js.map +1 -1
  240. package/dist/usecases/script/describe.js.map +1 -1
  241. package/dist/usecases/script/dialogue.js +6 -13
  242. package/dist/usecases/script/dialogue.js.map +1 -1
  243. package/dist/usecases/script/do.js +1 -1
  244. package/dist/usecases/script/do.js.map +1 -1
  245. package/dist/usecases/script/episodes.js.map +1 -1
  246. package/dist/usecases/script/{add-speaker.d.ts → importance.d.ts} +1 -1
  247. package/dist/usecases/script/importance.js +17 -0
  248. package/dist/usecases/script/importance.js.map +1 -0
  249. package/dist/usecases/script/insert.js.map +1 -1
  250. package/dist/usecases/script/issues.js.map +1 -1
  251. package/dist/usecases/script/lib.d.ts +8 -6
  252. package/dist/usecases/script/lib.js +81 -97
  253. package/dist/usecases/script/lib.js.map +1 -1
  254. package/dist/usecases/script/locations.js.map +1 -1
  255. package/dist/usecases/script/merge.js.map +1 -1
  256. package/dist/usecases/script/move.js.map +1 -1
  257. package/dist/usecases/script/props.js.map +1 -1
  258. package/dist/usecases/script/refs.js.map +1 -1
  259. package/dist/usecases/script/rename.js.map +1 -1
  260. package/dist/usecases/script/replace.js.map +1 -1
  261. package/dist/usecases/script/role.js.map +1 -1
  262. package/dist/usecases/script/scenes.js +8 -15
  263. package/dist/usecases/script/scenes.js.map +1 -1
  264. package/dist/usecases/script/script-patch.js +1 -1
  265. package/dist/usecases/script/script-patch.js.map +1 -1
  266. package/dist/usecases/script/script-validate.js +1 -1
  267. package/dist/usecases/script/script-validate.js.map +1 -1
  268. package/dist/usecases/script/session.d.ts +4 -4
  269. package/dist/usecases/script/session.js +57 -78
  270. package/dist/usecases/script/session.js.map +1 -1
  271. package/dist/usecases/script/split.js.map +1 -1
  272. package/dist/usecases/script/state-add.js.map +1 -1
  273. package/dist/usecases/script/state-delete.js.map +1 -1
  274. package/dist/usecases/script/state-rename.js.map +1 -1
  275. package/dist/usecases/script/summary.js.map +1 -1
  276. package/dist/usecases/script/synopsis-generate.d.ts +4 -5
  277. package/dist/usecases/script/synopsis-generate.js +30 -46
  278. package/dist/usecases/script/synopsis-generate.js.map +1 -1
  279. package/dist/usecases/script/transition.js.map +1 -1
  280. package/dist/usecases/script/type.js.map +1 -1
  281. package/dist/usecases/script/worldview.js.map +1 -1
  282. package/package.json +22 -17
  283. package/skills/scriptctl/SKILL.md +140 -120
  284. package/skills/scriptctl/references/atomic-write-workflow.md +12 -9
  285. package/skills/scriptctl/references/ingest-workflow.md +85 -0
  286. package/skills/scriptctl/references/state-reference-repair.md +26 -39
  287. package/dist/domain/direct/runner.d.ts +0 -25
  288. package/dist/domain/direct/runner.js +0 -88
  289. package/dist/domain/direct/runner.js.map +0 -1
  290. package/dist/domain/direct/stage.d.ts +0 -108
  291. package/dist/domain/direct/stage.js +0 -134
  292. package/dist/domain/direct/stage.js.map +0 -1
  293. package/dist/domain/direct/stages/asset-curation.d.ts +0 -2
  294. package/dist/domain/direct/stages/asset-curation.js +0 -10
  295. package/dist/domain/direct/stages/asset-curation.js.map +0 -1
  296. package/dist/domain/direct/stages/batch-extract.d.ts +0 -2
  297. package/dist/domain/direct/stages/batch-extract.js +0 -12
  298. package/dist/domain/direct/stages/batch-extract.js.map +0 -1
  299. package/dist/domain/direct/stages/batch-plan.d.ts +0 -2
  300. package/dist/domain/direct/stages/batch-plan.js +0 -9
  301. package/dist/domain/direct/stages/batch-plan.js.map +0 -1
  302. package/dist/domain/direct/stages/episode-merge.d.ts +0 -2
  303. package/dist/domain/direct/stages/episode-merge.js +0 -11
  304. package/dist/domain/direct/stages/episode-merge.js.map +0 -1
  305. package/dist/domain/direct/stages/episode-plan.d.ts +0 -2
  306. package/dist/domain/direct/stages/episode-plan.js +0 -9
  307. package/dist/domain/direct/stages/episode-plan.js.map +0 -1
  308. package/dist/domain/direct/stages/episode-synopsis.d.ts +0 -2
  309. package/dist/domain/direct/stages/episode-synopsis.js +0 -12
  310. package/dist/domain/direct/stages/episode-synopsis.js.map +0 -1
  311. package/dist/domain/direct/stages/episode-titles.d.ts +0 -2
  312. package/dist/domain/direct/stages/episode-titles.js +0 -12
  313. package/dist/domain/direct/stages/episode-titles.js.map +0 -1
  314. package/dist/domain/direct/stages/index.d.ts +0 -19
  315. package/dist/domain/direct/stages/index.js +0 -41
  316. package/dist/domain/direct/stages/index.js.map +0 -1
  317. package/dist/domain/direct/stages/metadata.d.ts +0 -2
  318. package/dist/domain/direct/stages/metadata.js +0 -11
  319. package/dist/domain/direct/stages/metadata.js.map +0 -1
  320. package/dist/domain/direct/stages/script-merge.d.ts +0 -2
  321. package/dist/domain/direct/stages/script-merge.js +0 -15
  322. package/dist/domain/direct/stages/script-merge.js.map +0 -1
  323. package/dist/domain/direct/stages/script-synopsis.d.ts +0 -2
  324. package/dist/domain/direct/stages/script-synopsis.js +0 -12
  325. package/dist/domain/direct/stages/script-synopsis.js.map +0 -1
  326. package/dist/domain/direct/stages/source-prepare.d.ts +0 -2
  327. package/dist/domain/direct/stages/source-prepare.js +0 -9
  328. package/dist/domain/direct/stages/source-prepare.js.map +0 -1
  329. package/dist/domain/direct/stages/state-binding.d.ts +0 -2
  330. package/dist/domain/direct/stages/state-binding.js +0 -9
  331. package/dist/domain/direct/stages/state-binding.js.map +0 -1
  332. package/dist/domain/direct/stages/state-curation.d.ts +0 -2
  333. package/dist/domain/direct/stages/state-curation.js +0 -10
  334. package/dist/domain/direct/stages/state-curation.js.map +0 -1
  335. package/dist/domain/direct/stages/validate.d.ts +0 -2
  336. package/dist/domain/direct/stages/validate.js +0 -10
  337. package/dist/domain/direct/stages/validate.js.map +0 -1
  338. package/dist/domain/direct-core.d.ts +0 -313
  339. package/dist/domain/direct-core.js +0 -8514
  340. package/dist/domain/direct-core.js.map +0 -1
  341. package/dist/domain/script/validate.d.ts +0 -22
  342. package/dist/domain/script/validate.js +0 -997
  343. package/dist/domain/script/validate.js.map +0 -1
  344. package/dist/infra/providers.d.ts +0 -97
  345. package/dist/infra/providers.js +0 -1396
  346. package/dist/infra/providers.js.map +0 -1
  347. package/dist/usecases/direct.d.ts +0 -27
  348. package/dist/usecases/direct.js +0 -4172
  349. package/dist/usecases/direct.js.map +0 -1
  350. package/dist/usecases/parse.d.ts +0 -15
  351. package/dist/usecases/parse.js +0 -429
  352. package/dist/usecases/parse.js.map +0 -1
  353. package/dist/usecases/script/add-speaker.js +0 -22
  354. package/dist/usecases/script/add-speaker.js.map +0 -1
  355. package/dist/usecases/script/export.d.ts +0 -3
  356. package/dist/usecases/script/export.js +0 -111
  357. package/dist/usecases/script/export.js.map +0 -1
  358. package/dist/usecases/script/overlap.d.ts +0 -3
  359. package/dist/usecases/script/overlap.js +0 -21
  360. package/dist/usecases/script/overlap.js.map +0 -1
  361. package/dist/usecases/script/speakers.d.ts +0 -3
  362. package/dist/usecases/script/speakers.js +0 -28
  363. package/dist/usecases/script/speakers.js.map +0 -1
  364. package/dist/usecases/script/state-change.d.ts +0 -3
  365. package/dist/usecases/script/state-change.js +0 -24
  366. package/dist/usecases/script/state-change.js.map +0 -1
  367. package/skills/scriptctl/references/direct-workflow.md +0 -72
@@ -1,997 +0,0 @@
1
- import * as path from "node:path";
2
- import { ACTION_TYPE_VALUES, CliError, DELIVERY_VALUES, EFFECTIVE_FROM_VALUES, EXIT_INPUT, ROLE_TYPE_VALUES, SCRIPT_SCHEMA_VERSION, SCRIPT_TARGET_KINDS, SPEAKER_SOURCE_KINDS, directDir, exists, readJson, readText, writeJson, } from "../../common.js";
3
- import { canonicalAssetNameKey, cleanName, contentHasSpeakerPrefix, inferNonActorSpeakerKind, isTechnicalEpisodeTitle, sceneContext, stateRejectionReason, } from "../direct-core.js";
4
- import { asList, isDict, isList, strOf } from "./shared.js";
5
- export function entityFieldMissingIssue(kindLabel, field, idValue) {
6
- const isDescription = field === "description";
7
- return {
8
- code: "SCHEMA_ENTITY_FIELD_MISSING",
9
- severity: isDescription ? "warning" : "error",
10
- ...(isDescription ? { incomplete_ok: true } : {}),
11
- summary: `${kindLabel} missing ${field}: ${idValue}`,
12
- repair_hint: isDescription
13
- ? "Describe with `describe` when ready (an undescribed asset is publishable)."
14
- : `Patch ${kindLabel.toLowerCase()} metadata.`,
15
- };
16
- }
17
- // ---------------------------------------------------------------------------
18
- // validate_script
19
- // ---------------------------------------------------------------------------
20
- /**
21
- * Validate a script's schema and asset wiring.
22
- *
23
- * Pass either:
24
- * - `scriptPath: string` (or null → default to direct's script.initial.json), or
25
- * - `opts.scriptData: Dict` to validate an in-memory object without writing to disk.
26
- *
27
- * Callers that already have the script as a parsed object (episode draft / publish
28
- * after assembly) should prefer `scriptData` to avoid the write-validate-rm dance.
29
- *
30
- * Completeness issues (no episodes, empty episode/scene, scene without location,
31
- * missing asset description) are emitted as warnings (`incomplete_ok`), not
32
- * errors — an unfinished but structurally-sound script passes. Only integrity
33
- * violations (dangling refs, dup ids, invalid enums, missing ids/names) fail it.
34
- */
35
- export function validateScript(workspace, scriptPath = null, opts = {}) {
36
- const requireSource = opts.requireSource ?? true;
37
- const dd = directDir(workspace);
38
- const sourcePath = path.join(workspace, "source.txt");
39
- if (requireSource && !exists(sourcePath)) {
40
- throw new CliError("VALIDATE BLOCKED: source.txt not found", "source.txt not found.", {
41
- exitCode: EXIT_INPUT,
42
- required: ["workspace/source.txt"],
43
- received: [sourcePath],
44
- nextSteps: ["Run scriptctl direct init first."],
45
- });
46
- }
47
- if (exists(sourcePath))
48
- readText(sourcePath); // touch, mirror Python
49
- let script;
50
- if (opts.scriptData !== undefined) {
51
- script = opts.scriptData;
52
- }
53
- else {
54
- const finalScriptPath = scriptPath ?? path.join(dd, "script.initial.json");
55
- if (!exists(finalScriptPath)) {
56
- throw new CliError("VALIDATE BLOCKED: script not found", "Script JSON not found.", {
57
- exitCode: EXIT_INPUT,
58
- required: ["script.initial.json or --script-path"],
59
- received: [finalScriptPath],
60
- nextSteps: ["Run scriptctl direct init first."],
61
- });
62
- }
63
- try {
64
- script = readJson(finalScriptPath);
65
- }
66
- catch (exc) {
67
- throw new CliError("VALIDATE BLOCKED: script JSON invalid", "Script JSON invalid.", {
68
- exitCode: EXIT_INPUT,
69
- required: ["valid script JSON"],
70
- received: [`${finalScriptPath}: ${exc.message}`],
71
- nextSteps: ["Fix script.initial.json or rerun init."],
72
- });
73
- }
74
- }
75
- const issues = [];
76
- for (const field of ["version", "title", "worldview", "style", "actors", "locations", "props", "speakers", "episodes"]) {
77
- if (!(field in script)) {
78
- issues.push({
79
- code: "SCHEMA_TOP_FIELD_MISSING",
80
- severity: "error",
81
- summary: `Top-level field missing: ${field}`,
82
- repair_hint: "Patch script.json or rerun init.",
83
- });
84
- }
85
- }
86
- if (script["version"] !== SCRIPT_SCHEMA_VERSION && script["version"] !== String(SCRIPT_SCHEMA_VERSION)) {
87
- issues.push({
88
- code: "SCHEMA_VERSION_UNSUPPORTED",
89
- severity: "warning",
90
- summary: `script version should be ${SCRIPT_SCHEMA_VERSION}.`,
91
- repair_hint: "Update the internal numeric version when migrating script.json.",
92
- });
93
- }
94
- const actorIds = new Set();
95
- for (const item of asList(script["actors"])) {
96
- const id = strOf(item["actor_id"]);
97
- if (id)
98
- actorIds.add(id);
99
- }
100
- const locationIds = new Set();
101
- for (const item of asList(script["locations"])) {
102
- const id = strOf(item["location_id"]);
103
- if (id)
104
- locationIds.add(id);
105
- }
106
- const propIds = new Set();
107
- for (const item of asList(script["props"])) {
108
- const id = strOf(item["prop_id"]);
109
- if (id)
110
- propIds.add(id);
111
- }
112
- const speakerDisplayNames = new Set();
113
- for (const item of asList(script["speakers"])) {
114
- if (!isDict(item))
115
- continue;
116
- const name = strOf(item["display_name"]).trim();
117
- if (name)
118
- speakerDisplayNames.add(cleanName(name));
119
- }
120
- const canonicalAssetNames = new Map();
121
- const checkCanonicalAssetName = (kind, kindLabel, id, name) => {
122
- if (!id || !name)
123
- return;
124
- const key = canonicalAssetNameKey(kind, name);
125
- const existing = canonicalAssetNames.get(key);
126
- if (!existing) {
127
- canonicalAssetNames.set(key, { kindLabel, id, name });
128
- return;
129
- }
130
- if (existing.id === id)
131
- return;
132
- issues.push({
133
- code: "DUPLICATE_ASSET_NAME",
134
- severity: "error",
135
- summary: `${kindLabel} canonical name duplicates ${existing.kindLabel}: ${name} (${id}) conflicts with ${existing.name} (${existing.id})`,
136
- repair_hint: "Merge duplicate same-kind assets or rename one to a distinct canonical name.",
137
- });
138
- };
139
- const actors = asList(script["actors"]);
140
- for (let idx = 0; idx < actors.length; idx++) {
141
- const actor = actors[idx];
142
- if (!isDict(actor)) {
143
- issues.push({
144
- code: "SCHEMA_ENTITY_FIELD_MISSING",
145
- severity: "error",
146
- summary: `actors[${idx}] is not an object.`,
147
- repair_hint: "Patch actor entry.",
148
- });
149
- continue;
150
- }
151
- const actorName = strOf(actor["actor_name"]).trim();
152
- for (const field of ["actor_id", "actor_name", "description"]) {
153
- if (!strOf(actor[field]).trim()) {
154
- issues.push(entityFieldMissingIssue("Actor", field, strOf(actor["actor_id"]) || String(idx)));
155
- }
156
- }
157
- if (actorName && cleanName(actorName) !== actorName) {
158
- issues.push({
159
- code: "ASSET_NAME_HAS_STATE_ANNOTATION",
160
- severity: "error",
161
- summary: `Actor name contains state annotation: ${actorName}`,
162
- repair_hint: "Keep actor_name canonical and move state text to states[] or action content only if reusable.",
163
- });
164
- }
165
- if (actorName && inferNonActorSpeakerKind(actorName) !== "actor") {
166
- issues.push({
167
- code: "NONHUMAN_AS_ACTOR",
168
- severity: "error",
169
- summary: `Non-human speaker is registered as actor: ${actorName}`,
170
- repair_hint: "Use speakers[] with source_kind system/broadcast/prop/group/other; do not create an actor.",
171
- });
172
- }
173
- checkCanonicalAssetName("actor", "Actor", strOf(actor["actor_id"]), actorName);
174
- if (actor["role_type"] && !ROLE_TYPE_VALUES.includes(strOf(actor["role_type"]))) {
175
- issues.push({
176
- code: "ENUM_ROLE_TYPE",
177
- severity: "error",
178
- summary: `Actor role_type invalid: ${actor["role_type"]}`,
179
- repair_hint: "Patch role_type to 主角 or 配角.",
180
- });
181
- }
182
- }
183
- const locations = asList(script["locations"]);
184
- for (let idx = 0; idx < locations.length; idx++) {
185
- const loc = locations[idx];
186
- if (!isDict(loc)) {
187
- issues.push({
188
- code: "SCHEMA_ENTITY_FIELD_MISSING",
189
- severity: "error",
190
- summary: `locations[${idx}] is not an object.`,
191
- repair_hint: "Patch location entry.",
192
- });
193
- continue;
194
- }
195
- const locationName = strOf(loc["location_name"]).trim();
196
- for (const field of ["location_id", "location_name", "description"]) {
197
- if (!strOf(loc[field]).trim()) {
198
- issues.push(entityFieldMissingIssue("Location", field, strOf(loc["location_id"]) || String(idx)));
199
- }
200
- }
201
- if (locationName && cleanName(locationName) !== locationName) {
202
- issues.push({
203
- code: "ASSET_NAME_HAS_STATE_ANNOTATION",
204
- severity: "error",
205
- summary: `Location name contains state annotation: ${locationName}`,
206
- repair_hint: "Keep location_name canonical and move durable visual variants to states[] only when reusable.",
207
- });
208
- }
209
- checkCanonicalAssetName("location", "Location", strOf(loc["location_id"]), locationName);
210
- }
211
- const propsArr = asList(script["props"]);
212
- for (let idx = 0; idx < propsArr.length; idx++) {
213
- const prop = propsArr[idx];
214
- if (!isDict(prop)) {
215
- issues.push({
216
- code: "SCHEMA_ENTITY_FIELD_MISSING",
217
- severity: "error",
218
- summary: `props[${idx}] is not an object.`,
219
- repair_hint: "Patch prop entry.",
220
- });
221
- continue;
222
- }
223
- const propName = strOf(prop["prop_name"]).trim();
224
- for (const field of ["prop_id", "prop_name", "description"]) {
225
- if (!strOf(prop[field]).trim()) {
226
- issues.push(entityFieldMissingIssue("Prop", field, strOf(prop["prop_id"]) || String(idx)));
227
- }
228
- }
229
- if (propName && cleanName(propName) !== propName) {
230
- issues.push({
231
- code: "ASSET_NAME_HAS_STATE_ANNOTATION",
232
- severity: "error",
233
- summary: `Prop name contains state annotation: ${propName}`,
234
- repair_hint: "Keep prop_name canonical and move durable visual variants to states[] only when reusable.",
235
- });
236
- }
237
- checkCanonicalAssetName("prop", "Prop", strOf(prop["prop_id"]), propName);
238
- }
239
- const stateIdsByAsset = new Map();
240
- const assetLabelsById = new Map();
241
- const assetIdsByLabel = new Map();
242
- const stateIdsByAssetLabel = new Map();
243
- const stateNameKeysByAssetLabel = new Set();
244
- for (const [collection, idKey] of [["actors", "actor_id"], ["locations", "location_id"], ["props", "prop_id"]]) {
245
- const targetKind = { actors: "actor", locations: "location", props: "prop" }[collection];
246
- const nameKey = { actors: "actor_name", locations: "location_name", props: "prop_name" }[collection];
247
- for (const asset of asList(script[collection])) {
248
- const assetId = strOf(asset[idKey]);
249
- const assetName = strOf(asset[nameKey]).trim();
250
- const assetNameKey = cleanName(assetName);
251
- const assetLabelKey = `${targetKind}::${assetNameKey}`;
252
- if (assetId) {
253
- assetLabelsById.set(assetId, { kind: targetKind, name: assetName });
254
- if (assetNameKey)
255
- assetIdsByLabel.set(assetLabelKey, assetId);
256
- }
257
- const rawStates = asset["states"];
258
- stateIdsByAsset.set(assetId, new Set());
259
- if (assetNameKey)
260
- stateIdsByAssetLabel.set(assetLabelKey, stateIdsByAsset.get(assetId));
261
- const seenStateNames = new Set();
262
- // Mirror Python `asset.get("states", []) or []`: absent/null → empty list (no issue)
263
- if (rawStates !== undefined && rawStates !== null && !isList(rawStates)) {
264
- issues.push({
265
- code: "SCHEMA_STATE_LIST_INVALID",
266
- severity: "error",
267
- summary: `${collection} ${assetId} states must be an array.`,
268
- repair_hint: "Patch states to an array.",
269
- });
270
- continue;
271
- }
272
- const states = isList(rawStates) ? rawStates : [];
273
- let defaultStateCount = 0;
274
- for (const state of states) {
275
- if (!isDict(state)) {
276
- issues.push({
277
- code: "SCHEMA_STATE_INVALID",
278
- severity: "error",
279
- summary: `${collection} ${assetId} contains a non-object state.`,
280
- repair_hint: "Patch or remove the invalid state.",
281
- });
282
- continue;
283
- }
284
- const sid = strOf(state["state_id"]);
285
- const sname = strOf(state["state_name"] || state["name"]);
286
- if (!sid || !sname) {
287
- issues.push({
288
- code: "SCHEMA_STATE_FIELD_MISSING",
289
- severity: "error",
290
- summary: `${collection} ${assetId} state missing state_id or state_name.`,
291
- repair_hint: "Patch state_id/state_name.",
292
- });
293
- }
294
- if (sid && stateIdsByAsset.get(assetId).has(sid)) {
295
- issues.push({
296
- code: "DUPLICATE_STATE_ID",
297
- severity: "error",
298
- summary: `Duplicate state_id ${targetKind}:${assetId}/${sid}`,
299
- repair_hint: `Two states share id ${sid}. There is no verb to re-id a state, and delete ${targetKind}:${assetId}/${sid} would remove BOTH. Fix the raw script.json: give one state a fresh state_id (or drop the unwanted one), then re-validate.`,
300
- });
301
- }
302
- if (sid === "default")
303
- defaultStateCount += 1;
304
- if (sname && seenStateNames.has(sname)) {
305
- issues.push({
306
- code: "DUPLICATE_STATE_NAME",
307
- severity: "warning",
308
- summary: `Duplicate state_name "${sname}" on ${targetKind}:${assetId}/${sid}`,
309
- repair_hint: `Inspect with: scriptctl states ${targetKind}:${assetId} — then rename if distinct: scriptctl rename ${targetKind}:${assetId}/${sid} "<新名>"`,
310
- });
311
- }
312
- const reason = stateRejectionReason(targetKind, sname);
313
- if (sname && reason) {
314
- issues.push({
315
- code: "STATE_LABEL_INVALID",
316
- severity: "error",
317
- summary: `${collection} ${assetId} state label invalid: ${sname}`,
318
- repair_hint: "Use one clear state label; do not combine multiple states into one label.",
319
- });
320
- }
321
- if (sid)
322
- stateIdsByAsset.get(assetId).add(sid);
323
- if (sname) {
324
- seenStateNames.add(sname);
325
- if (assetNameKey)
326
- stateNameKeysByAssetLabel.add(`${assetLabelKey}::${sname}`);
327
- }
328
- }
329
- if (states.length > 0 && defaultStateCount === 0) {
330
- issues.push({
331
- code: "DEFAULT_STATE_MISSING",
332
- severity: "error",
333
- summary: `${collection} ${assetId} has states but no asset-scoped default state.`,
334
- repair_hint: "Run direct state-curation or set the most common state_id to `default` while preserving its state_name.",
335
- });
336
- }
337
- }
338
- }
339
- const speakerIds = new Set();
340
- const speakersArr = asList(script["speakers"]);
341
- for (let idx = 0; idx < speakersArr.length; idx++) {
342
- const speaker = speakersArr[idx];
343
- if (!isDict(speaker)) {
344
- issues.push({
345
- code: "SCHEMA_SPEAKER_INVALID",
346
- severity: "error",
347
- summary: `speakers[${idx}] is not an object.`,
348
- repair_hint: "Patch speaker entry.",
349
- });
350
- continue;
351
- }
352
- const speakerId = strOf(speaker["speaker_id"]);
353
- const sourceKind = strOf(speaker["source_kind"]);
354
- const sourceId = speaker["source_id"];
355
- if (!speakerId || !strOf(speaker["display_name"]).trim()) {
356
- issues.push({
357
- code: "SCHEMA_SPEAKER_FIELD_MISSING",
358
- severity: "error",
359
- summary: `Speaker missing speaker_id or display_name: ${idx}`,
360
- repair_hint: "Patch speaker metadata.",
361
- });
362
- }
363
- if (speakerIds.has(speakerId)) {
364
- issues.push({
365
- code: "DUPLICATE_SPEAKER_ID",
366
- severity: "error",
367
- summary: `Duplicate speaker_id: ${speakerId}`,
368
- repair_hint: "Rename or merge duplicate speakers.",
369
- });
370
- }
371
- if (speakerId)
372
- speakerIds.add(speakerId);
373
- if (!SPEAKER_SOURCE_KINDS.has(sourceKind)) {
374
- issues.push({
375
- code: "ENUM_SPEAKER_SOURCE_KIND",
376
- severity: "error",
377
- summary: `speaker.source_kind invalid: ${sourceKind || "<empty>"}`,
378
- repair_hint: "Use actor/location/prop/system/broadcast/group/other.",
379
- });
380
- }
381
- if (sourceKind === "actor" && !actorIds.has(strOf(sourceId))) {
382
- issues.push({
383
- code: "DANGLING_SPEAKER_SOURCE_REF",
384
- severity: "error",
385
- summary: `Speaker actor source not found: ${sourceId}`,
386
- repair_hint: "Patch source_id or source_kind.",
387
- });
388
- }
389
- if (sourceKind === "location" && !locationIds.has(strOf(sourceId))) {
390
- issues.push({
391
- code: "DANGLING_SPEAKER_SOURCE_REF",
392
- severity: "error",
393
- summary: `Speaker location source not found: ${sourceId}`,
394
- repair_hint: "Patch source_id or source_kind.",
395
- });
396
- }
397
- if (sourceKind === "prop" && !propIds.has(strOf(sourceId))) {
398
- issues.push({
399
- code: "DANGLING_SPEAKER_SOURCE_REF",
400
- severity: "error",
401
- summary: `Speaker prop source not found: ${sourceId}`,
402
- repair_hint: "Patch source_id or source_kind.",
403
- });
404
- }
405
- }
406
- const planPath = path.join(dd, "episode_plan.json");
407
- if (exists(planPath)) {
408
- const plan = readJson(planPath);
409
- if (plan["boundary_confidence"] === "low") {
410
- issues.push({
411
- code: "LOW_CONFIDENCE_EPISODE_PLAN",
412
- severity: "warning",
413
- summary: "Episode plan used a single-episode fallback.",
414
- repair_hint: "Inspect the initial script and adjust structure with patch if needed.",
415
- });
416
- }
417
- }
418
- const episodes = asList(script["episodes"]);
419
- if (episodes.length === 0) {
420
- issues.push({ code: "NO_EPISODES", severity: "warning", incomplete_ok: true, summary: "No episodes in script.", repair_hint: "Add episodes with add-episode (an empty script is publishable but renders to nothing)." });
421
- }
422
- const episodeIds = new Set();
423
- let totalScenes = 0;
424
- let totalActions = 0;
425
- let lastSceneNum = 0;
426
- const usedPropStateIds = new Set();
427
- const issueNullableStateRef = (kind, assetId, episode, scene) => {
428
- if ((stateIdsByAsset.get(assetId) ?? new Set()).size === 0)
429
- return;
430
- const label = assetLabelsById.get(assetId);
431
- issues.push({
432
- code: "STATE_REF_MISSING",
433
- severity: "warning",
434
- incomplete_ok: true,
435
- episode,
436
- scene,
437
- asset_kind: kind,
438
- asset_id: assetId,
439
- summary: `${kind} ${label?.name || assetId} has states but scene ref has no state_id.`,
440
- repair_hint: "在 ep_*.md 的场景引用里写明状态,或确认资产只有默认/唯一状态让 parse 自动绑定。",
441
- });
442
- };
443
- for (const ep of episodes) {
444
- const epId = strOf(ep["episode_id"]).trim();
445
- if (!epId) {
446
- issues.push({
447
- code: "EPISODE_ID_MISSING",
448
- severity: "error",
449
- summary: "Episode missing episode_id.",
450
- repair_hint: "Patch episode_id to ep_### or rerun init.",
451
- });
452
- }
453
- else if (episodeIds.has(epId)) {
454
- issues.push({
455
- code: "DUPLICATE_EPISODE_ID",
456
- severity: "error",
457
- episode: epId,
458
- summary: `Duplicate episode_id: ${epId}`,
459
- repair_hint: "Rerun init or patch episode ids to a unique ep_### sequence.",
460
- });
461
- }
462
- if (epId)
463
- episodeIds.add(epId);
464
- const epTitle = strOf(ep["title"]).trim();
465
- if (!epTitle || isTechnicalEpisodeTitle(epTitle)) {
466
- issues.push({
467
- code: "EPISODE_TITLE_INVALID",
468
- severity: "warning",
469
- episode: ep["episode_id"],
470
- summary: `Episode title is missing or technical: ${epTitle || "<empty>"}`,
471
- repair_hint: 'Rerun `direct init` to regenerate, or `script patch episode <episode_id> --field title --value "第N集:短标题"`.',
472
- });
473
- }
474
- const scenes = asList(ep["scenes"]);
475
- if (scenes.length === 0) {
476
- issues.push({
477
- code: "EPISODE_WITHOUT_SCENES",
478
- severity: "warning",
479
- incomplete_ok: true,
480
- episode: ep["episode_id"],
481
- summary: "Episode has no scenes.",
482
- repair_hint: "Add scenes with insert (an empty episode is publishable but renders to nothing).",
483
- });
484
- }
485
- for (const scene of scenes) {
486
- totalScenes += 1;
487
- const rawSceneId = strOf(scene["scene_id"]);
488
- const match = /^scn_(\d+)$/.exec(rawSceneId);
489
- if (match) {
490
- const sceneNum = parseInt(match[1], 10);
491
- if (sceneNum <= lastSceneNum) {
492
- issues.push({
493
- code: "REF_SCENE_ID_NONMONO",
494
- severity: "error",
495
- episode: ep["episode_id"],
496
- scene: rawSceneId,
497
- summary: `scene_id must increase globally: ${rawSceneId}`,
498
- repair_hint: "Rerun init or patch scene ids to a global monotonic sequence.",
499
- });
500
- }
501
- lastSceneNum = Math.max(lastSceneNum, sceneNum);
502
- }
503
- else {
504
- issues.push({
505
- code: "REF_SCENE_ID_INVALID",
506
- severity: "error",
507
- episode: ep["episode_id"],
508
- scene: rawSceneId || null,
509
- summary: "scene_id must use scn_### format.",
510
- repair_hint: "Patch scene_id to scn_###.",
511
- });
512
- }
513
- const ctx = sceneContext(scene);
514
- for (const unresolved of asList(ctx["unresolved_state_refs"])) {
515
- const kind = strOf(unresolved["kind"]);
516
- const assetName = strOf(unresolved["asset_name"]).trim();
517
- const stateName = strOf(unresolved["state_name"]).trim();
518
- if (!SCRIPT_TARGET_KINDS.has(kind) || !assetName || !stateName)
519
- continue;
520
- const assetLabelKey = `${kind}::${cleanName(assetName)}`;
521
- if (stateNameKeysByAssetLabel.has(`${assetLabelKey}::${stateName}`))
522
- continue;
523
- const assetId = assetIdsByLabel.get(assetLabelKey);
524
- const currentStateIds = stateIdsByAssetLabel.get(assetLabelKey) ?? new Set();
525
- const refKeys = {
526
- actor: { list: "actors", id: "actor_id" },
527
- location: { list: "locations", id: "location_id" },
528
- prop: { list: "props", id: "prop_id" },
529
- }[kind];
530
- const currentRefResolved = asList(ctx[refKeys.list])
531
- .some((ref) => strOf(ref[refKeys.id]) === assetId && currentStateIds.has(strOf(ref["state_id"])));
532
- if (currentRefResolved)
533
- continue;
534
- issues.push({
535
- code: "UNRESOLVED_EXPLICIT_STATE_REF",
536
- severity: "warning",
537
- incomplete_ok: true,
538
- episode: ep["episode_id"],
539
- scene: scene["scene_id"],
540
- asset_kind: kind,
541
- summary: `${kind} ${assetName} explicit state not found: ${stateName}`,
542
- repair_hint: "修改 ep_*.md 中的状态名,或在对应 人物/场景/道具.md 中登记这个状态。",
543
- });
544
- }
545
- const ctxLocations = asList(ctx["locations"]);
546
- if (ctxLocations.length === 0) {
547
- issues.push({
548
- code: "SCENE_WITHOUT_LOCATION",
549
- severity: "warning",
550
- incomplete_ok: true,
551
- episode: ep["episode_id"],
552
- scene: scene["scene_id"],
553
- summary: "Scene has no location.",
554
- repair_hint: "Set a location with context (an unlocated scene is publishable but cannot render).",
555
- });
556
- }
557
- for (const ref of ctxLocations) {
558
- const locationId = strOf(ref["location_id"]);
559
- const stateId = strOf(ref["state_id"]);
560
- if (!locationIds.has(locationId)) {
561
- issues.push({
562
- code: "DANGLING_LOCATION_REF",
563
- severity: "error",
564
- episode: ep["episode_id"],
565
- scene: scene["scene_id"],
566
- summary: `Location ref not found: ${locationId || "<empty>"}`,
567
- repair_hint: "Patch scene location to an existing location_id.",
568
- });
569
- }
570
- if (stateId && !(stateIdsByAsset.get(locationId) ?? new Set()).has(stateId)) {
571
- issues.push({
572
- code: "DANGLING_LOCATION_STATE_REF",
573
- severity: "error",
574
- episode: ep["episode_id"],
575
- scene: scene["scene_id"],
576
- summary: `Location state ref not found: ${stateId}`,
577
- repair_hint: "Patch location state_id or asset states.",
578
- });
579
- }
580
- if (locationIds.has(locationId) && !stateId)
581
- issueNullableStateRef("location", locationId, ep["episode_id"], scene["scene_id"]);
582
- }
583
- for (const ref of asList(ctx["actors"])) {
584
- const actorId = strOf(ref["actor_id"]);
585
- const stateId = strOf(ref["state_id"]);
586
- if (!actorIds.has(actorId)) {
587
- issues.push({
588
- code: "DANGLING_ACTOR_REF",
589
- severity: "error",
590
- episode: ep["episode_id"],
591
- scene: scene["scene_id"],
592
- summary: `Actor ref not found: ${actorId || "<empty>"}`,
593
- repair_hint: "Patch scene actor refs to existing actor_id.",
594
- });
595
- }
596
- if (stateId && !(stateIdsByAsset.get(actorId) ?? new Set()).has(stateId)) {
597
- issues.push({
598
- code: "DANGLING_ACTOR_STATE_REF",
599
- severity: "error",
600
- episode: ep["episode_id"],
601
- scene: scene["scene_id"],
602
- summary: `Actor state ref not found: ${stateId}`,
603
- repair_hint: "Patch actor state_id or asset states.",
604
- });
605
- }
606
- if (actorIds.has(actorId) && !stateId)
607
- issueNullableStateRef("actor", actorId, ep["episode_id"], scene["scene_id"]);
608
- }
609
- for (const ref of asList(ctx["props"])) {
610
- const propId = strOf(ref["prop_id"]);
611
- const stateId = strOf(ref["state_id"]);
612
- if (!propIds.has(propId)) {
613
- issues.push({
614
- code: "DANGLING_PROP_REF",
615
- severity: "error",
616
- episode: ep["episode_id"],
617
- scene: scene["scene_id"],
618
- summary: `Prop ref not found: ${propId || "<empty>"}`,
619
- repair_hint: "Patch scene prop refs to existing prop_id.",
620
- });
621
- }
622
- if (stateId && !(stateIdsByAsset.get(propId) ?? new Set()).has(stateId)) {
623
- issues.push({
624
- code: "DANGLING_PROP_STATE_REF",
625
- severity: "error",
626
- episode: ep["episode_id"],
627
- scene: scene["scene_id"],
628
- summary: `Prop state ref not found: ${stateId}`,
629
- repair_hint: "Patch prop state_id or asset states.",
630
- });
631
- }
632
- if (stateId)
633
- usedPropStateIds.add(stateId);
634
- if (propIds.has(propId) && !stateId)
635
- issueNullableStateRef("prop", propId, ep["episode_id"], scene["scene_id"]);
636
- }
637
- const actionsArr = asList(scene["actions"]);
638
- if (actionsArr.length === 0) {
639
- issues.push({
640
- code: "SCENE_WITHOUT_ACTIONS",
641
- severity: "warning",
642
- incomplete_ok: true,
643
- episode: ep["episode_id"],
644
- scene: scene["scene_id"],
645
- summary: "Scene has no actions.",
646
- repair_hint: "Add actions with insert (an empty scene is publishable but renders to nothing).",
647
- });
648
- }
649
- for (let actionIndex = 0; actionIndex < actionsArr.length; actionIndex++) {
650
- const action = actionsArr[actionIndex];
651
- totalActions += 1;
652
- const kind = strOf(action["type"]).trim();
653
- if (!ACTION_TYPE_VALUES.has(kind)) {
654
- issues.push({
655
- code: "ENUM_ACTION_TYPE",
656
- severity: "error",
657
- episode: ep["episode_id"],
658
- scene: scene["scene_id"],
659
- action_index: actionIndex,
660
- summary: `action.type invalid: ${kind || "<empty>"}`,
661
- repair_hint: "Patch action.type to dialogue, inner_thought, or action.",
662
- });
663
- }
664
- const content = strOf(action["content"]).trim();
665
- const hasOverlapLines = action["delivery"] === "overlap" && isList(action["lines"]) && action["lines"].length > 0;
666
- if (!content && !hasOverlapLines) {
667
- issues.push({
668
- code: "EMPTY_ACTION_CONTENT",
669
- severity: "error",
670
- episode: ep["episode_id"],
671
- scene: scene["scene_id"],
672
- action_index: actionIndex,
673
- summary: "Action content is empty.",
674
- repair_hint: "Patch content from a source span.",
675
- });
676
- }
677
- if (content && contentHasSpeakerPrefix(content, speakerDisplayNames)) {
678
- issues.push({
679
- code: "DIALOGUE_CONTENT_SPEAKER_PREFIX",
680
- severity: "error",
681
- episode: ep["episode_id"],
682
- scene: scene["scene_id"],
683
- action_index: actionIndex,
684
- summary: "Dialogue content appears to contain speaker prefixes.",
685
- repair_hint: "Move speaker labels into speaker_id/speakers/lines and keep content as spoken text only.",
686
- });
687
- }
688
- if (action["type"] === "dialogue") {
689
- const delivery = strOf(action["delivery"] || "single");
690
- if (!DELIVERY_VALUES.has(delivery)) {
691
- issues.push({
692
- code: "ENUM_DIALOGUE_DELIVERY",
693
- severity: "error",
694
- episode: ep["episode_id"],
695
- scene: scene["scene_id"],
696
- action_index: actionIndex,
697
- summary: `dialogue.delivery invalid: ${delivery}`,
698
- repair_hint: "Use single, simultaneous, overlap, or group.",
699
- });
700
- }
701
- if (delivery === "overlap") {
702
- const lines = asList(action["lines"]);
703
- if (lines.length === 0) {
704
- issues.push({
705
- code: "DIALOGUE_OVERLAP_LINES_EMPTY",
706
- severity: "error",
707
- episode: ep["episode_id"],
708
- scene: scene["scene_id"],
709
- action_index: actionIndex,
710
- summary: "Overlapping dialogue has no lines.",
711
- repair_hint: "Add lines with speaker_id and content.",
712
- });
713
- }
714
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
715
- const line = lines[lineIdx] ?? {};
716
- const sid = strOf(line["speaker_id"]);
717
- const lineContent = strOf(line["content"]).trim();
718
- if (!speakerIds.has(sid)) {
719
- issues.push({
720
- code: "DANGLING_DIALOGUE_SPEAKER_REF",
721
- severity: "error",
722
- episode: ep["episode_id"],
723
- scene: scene["scene_id"],
724
- action_index: actionIndex,
725
- summary: `Overlap line speaker not found: ${sid || "<empty>"}`,
726
- repair_hint: "Patch speaker_id or add a speaker.",
727
- });
728
- }
729
- if (!lineContent) {
730
- issues.push({
731
- code: "EMPTY_DIALOGUE_LINE",
732
- severity: "error",
733
- episode: ep["episode_id"],
734
- scene: scene["scene_id"],
735
- action_index: actionIndex,
736
- summary: `Overlap line ${lineIdx} content is empty.`,
737
- repair_hint: "Patch line content.",
738
- });
739
- }
740
- else if (contentHasSpeakerPrefix(lineContent, speakerDisplayNames)) {
741
- issues.push({
742
- code: "DIALOGUE_CONTENT_SPEAKER_PREFIX",
743
- severity: "error",
744
- episode: ep["episode_id"],
745
- scene: scene["scene_id"],
746
- action_index: actionIndex,
747
- summary: `Overlap line ${lineIdx} content contains a speaker prefix.`,
748
- repair_hint: "Keep each overlap line content as spoken text only.",
749
- });
750
- }
751
- }
752
- }
753
- else if (action["speakers"]) {
754
- const speakersInAction = asList(action["speakers"]);
755
- if (speakersInAction.length === 0) {
756
- issues.push({
757
- code: "DIALOGUE_SPEAKERS_INVALID",
758
- severity: "error",
759
- episode: ep["episode_id"],
760
- scene: scene["scene_id"],
761
- action_index: actionIndex,
762
- summary: "dialogue.speakers must be an array.",
763
- repair_hint: "Patch speakers list.",
764
- });
765
- }
766
- for (const speaker of speakersInAction) {
767
- const sid = strOf(speaker["speaker_id"]);
768
- if (!speakerIds.has(sid)) {
769
- issues.push({
770
- code: "DANGLING_DIALOGUE_SPEAKER_REF",
771
- severity: "error",
772
- episode: ep["episode_id"],
773
- scene: scene["scene_id"],
774
- action_index: actionIndex,
775
- summary: `Dialogue speaker not found: ${sid || "<empty>"}`,
776
- repair_hint: "Patch speaker_id or add a speaker.",
777
- });
778
- }
779
- }
780
- }
781
- else if (action["speaker_id"]) {
782
- const sid = strOf(action["speaker_id"]);
783
- if (!speakerIds.has(sid)) {
784
- issues.push({
785
- code: "DANGLING_DIALOGUE_SPEAKER_REF",
786
- severity: "error",
787
- episode: ep["episode_id"],
788
- scene: scene["scene_id"],
789
- action_index: actionIndex,
790
- summary: `Dialogue speaker not found: ${sid}`,
791
- repair_hint: "Patch speaker_id or add a speaker.",
792
- });
793
- }
794
- }
795
- else if (!action["actor_id"]) {
796
- issues.push({
797
- code: "DIALOGUE_WITHOUT_SPEAKER",
798
- severity: "error",
799
- episode: ep["episode_id"],
800
- scene: scene["scene_id"],
801
- action_index: actionIndex,
802
- summary: "Dialogue has no speaker.",
803
- repair_hint: "Patch speaker_id, speakers, lines, or legacy actor_id.",
804
- });
805
- }
806
- }
807
- if (action["type"] === "inner_thought") {
808
- const sid = strOf(action["speaker_id"]);
809
- if (sid && !speakerIds.has(sid)) {
810
- issues.push({
811
- code: "DANGLING_DIALOGUE_SPEAKER_REF",
812
- severity: "error",
813
- episode: ep["episode_id"],
814
- scene: scene["scene_id"],
815
- action_index: actionIndex,
816
- summary: `Inner thought speaker not found: ${sid}`,
817
- repair_hint: "Patch speaker_id or add a speaker.",
818
- });
819
- }
820
- if (!action["actor_id"] && !sid) {
821
- issues.push({
822
- code: "DIALOGUE_WITHOUT_ACTOR",
823
- severity: "error",
824
- episode: ep["episode_id"],
825
- scene: scene["scene_id"],
826
- action_index: actionIndex,
827
- summary: "Inner thought has no actor_id or speaker_id.",
828
- repair_hint: "Patch actor_id, speaker_id, or speaker registry.",
829
- });
830
- }
831
- }
832
- if (action["actor_id"] && !actorIds.has(strOf(action["actor_id"]))) {
833
- issues.push({
834
- code: "DANGLING_ACTION_ACTOR_REF",
835
- severity: "error",
836
- episode: ep["episode_id"],
837
- scene: scene["scene_id"],
838
- action_index: actionIndex,
839
- summary: `Action actor_id not found: ${action["actor_id"]}`,
840
- repair_hint: "Patch action actor_id to an existing actor.",
841
- });
842
- }
843
- for (const change of asList(action["state_changes"])) {
844
- const targetKind = strOf(change["target_kind"]);
845
- const targetId = strOf(change["target_id"]);
846
- const fromStateId = strOf(change["from_state_id"]);
847
- const toStateId = strOf(change["to_state_id"]);
848
- const effectiveFrom = strOf(change["effective_from"] || "after_action");
849
- if (!SCRIPT_TARGET_KINDS.has(targetKind)) {
850
- issues.push({
851
- code: "STATE_CHANGE_TARGET_KIND_INVALID",
852
- severity: "error",
853
- episode: ep["episode_id"],
854
- scene: scene["scene_id"],
855
- action_index: actionIndex,
856
- summary: `state_changes target_kind invalid: ${targetKind || "<empty>"}`,
857
- repair_hint: "Use actor, location, or prop.",
858
- });
859
- continue;
860
- }
861
- const validIds = { actor: actorIds, location: locationIds, prop: propIds }[targetKind];
862
- if (!validIds.has(targetId)) {
863
- issues.push({
864
- code: "DANGLING_STATE_CHANGE_TARGET_REF",
865
- severity: "error",
866
- episode: ep["episode_id"],
867
- scene: scene["scene_id"],
868
- action_index: actionIndex,
869
- summary: `State change target not found: ${targetKind}:${targetId}`,
870
- repair_hint: "Patch target_id.",
871
- });
872
- }
873
- for (const [label, sid] of [["from_state_id", fromStateId], ["to_state_id", toStateId]]) {
874
- if (sid && !(stateIdsByAsset.get(targetId) ?? new Set()).has(sid)) {
875
- issues.push({
876
- code: "DANGLING_STATE_CHANGE_STATE_REF",
877
- severity: "error",
878
- episode: ep["episode_id"],
879
- scene: scene["scene_id"],
880
- action_index: actionIndex,
881
- summary: `State change ${label} not found: ${sid}`,
882
- repair_hint: "Patch state change or asset states.",
883
- });
884
- }
885
- }
886
- if (!EFFECTIVE_FROM_VALUES.has(effectiveFrom)) {
887
- issues.push({
888
- code: "ENUM_STATE_CHANGE_EFFECTIVE_FROM",
889
- severity: "error",
890
- episode: ep["episode_id"],
891
- scene: scene["scene_id"],
892
- action_index: actionIndex,
893
- summary: `effective_from invalid: ${effectiveFrom}`,
894
- repair_hint: "Use before_action or after_action.",
895
- });
896
- }
897
- }
898
- const transition = action["transition_prompt"];
899
- if (transition !== null && transition !== undefined) {
900
- if (!isDict(transition)) {
901
- issues.push({
902
- code: "TRANSITION_PROMPT_INVALID",
903
- severity: "error",
904
- episode: ep["episode_id"],
905
- scene: scene["scene_id"],
906
- action_index: actionIndex,
907
- summary: "transition_prompt must be an object.",
908
- repair_hint: "Patch or clear transition_prompt.",
909
- });
910
- }
911
- else {
912
- const targetKind = strOf(transition["target_kind"]);
913
- const targetId = strOf(transition["target_id"]);
914
- if (!SCRIPT_TARGET_KINDS.has(targetKind)) {
915
- issues.push({
916
- code: "TRANSITION_PROMPT_TARGET_KIND_INVALID",
917
- severity: "error",
918
- episode: ep["episode_id"],
919
- scene: scene["scene_id"],
920
- action_index: actionIndex,
921
- summary: `transition target_kind invalid: ${targetKind || "<empty>"}`,
922
- repair_hint: "Use actor, location, or prop.",
923
- });
924
- }
925
- else {
926
- const validIds = { actor: actorIds, location: locationIds, prop: propIds }[targetKind];
927
- if (!validIds.has(targetId)) {
928
- issues.push({
929
- code: "DANGLING_TRANSITION_TARGET_REF",
930
- severity: "error",
931
- episode: ep["episode_id"],
932
- scene: scene["scene_id"],
933
- action_index: actionIndex,
934
- summary: `Transition target not found: ${targetKind}:${targetId}`,
935
- repair_hint: "Patch transition target.",
936
- });
937
- }
938
- }
939
- }
940
- }
941
- }
942
- }
943
- }
944
- for (const prop of asList(script["props"])) {
945
- const propId = strOf(prop["prop_id"]);
946
- const propName = strOf(prop["prop_name"]).trim();
947
- const propStateIds = stateIdsByAsset.get(propId) ?? new Set();
948
- if (propStateIds.size === 0)
949
- continue;
950
- let hasUsedState = false;
951
- for (const stateId of propStateIds) {
952
- if (usedPropStateIds.has(stateId)) {
953
- hasUsedState = true;
954
- break;
955
- }
956
- }
957
- if (!hasUsedState) {
958
- issues.push({
959
- code: "PROP_STATES_UNUSED",
960
- severity: "warning",
961
- incomplete_ok: true,
962
- asset_kind: "prop",
963
- asset_id: propId,
964
- summary: `Prop ${propName || propId} defines states but no scene binds any of them.`,
965
- repair_hint: "在 ep_*.md 的 `- 道具:` 中写明状态,或删除不需要的道具状态。",
966
- });
967
- }
968
- }
969
- const hasBlocking = issues.some((it) => it["severity"] === "blocking");
970
- const hasErrors = issues.some((it) => it["severity"] === "error");
971
- const validation = {
972
- passed: !hasBlocking && !hasErrors,
973
- has_blocking: hasBlocking,
974
- issues,
975
- stats: {
976
- episodes: episodes.length,
977
- scenes: totalScenes,
978
- actions: totalActions,
979
- actors: asList(script["actors"]).length,
980
- locations: asList(script["locations"]).length,
981
- props: asList(script["props"]).length,
982
- speakers: asList(script["speakers"]).length,
983
- },
984
- script_path: opts.scriptData !== undefined ? "<in-memory>" : (scriptPath ?? path.join(dd, "script.initial.json")),
985
- validated_at: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
986
- };
987
- // persist defaults to true (direct/write flows write validation.json into
988
- // directDir); callers that want a side-effect-free check (e.g. `parse`) pass
989
- // persist:false so no direct artifacts are created.
990
- if (opts.persist !== false)
991
- writeJson(path.join(dd, "validation.json"), validation);
992
- return validation;
993
- }
994
- // ---------------------------------------------------------------------------
995
- // Resolve/lookup helpers
996
- // ---------------------------------------------------------------------------
997
- //# sourceMappingURL=validate.js.map