@nodaro/shared 1.4.1

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 (665) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +32 -0
  3. package/dist/index.cjs +12506 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +12269 -0
  6. package/dist/index.d.ts +12269 -0
  7. package/dist/index.js +11831 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +54 -0
  10. package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
  11. package/src/__tests__/ai-avatar-pricing.test.ts +321 -0
  12. package/src/__tests__/apply-video-negative-prompt.test.ts +76 -0
  13. package/src/__tests__/attach-columns.test.ts +28 -0
  14. package/src/__tests__/audio-crossfade-curves.test.ts +33 -0
  15. package/src/__tests__/build-model-menu.test.ts +58 -0
  16. package/src/__tests__/character-aspect-defaults.test.ts +142 -0
  17. package/src/__tests__/character-mention-slug.test.ts +199 -0
  18. package/src/__tests__/character-reference-photo-kinds.test.ts +32 -0
  19. package/src/__tests__/character-usage-mode.test.ts +147 -0
  20. package/src/__tests__/character-variant-assets.test.ts +142 -0
  21. package/src/__tests__/character-voice.test.ts +130 -0
  22. package/src/__tests__/cinematic-avatar-pricing.test.ts +119 -0
  23. package/src/__tests__/combine-transitions.test.ts +137 -0
  24. package/src/__tests__/component-types.test.ts +87 -0
  25. package/src/__tests__/condition-variables.test.ts +98 -0
  26. package/src/__tests__/credit-identifiers.test.ts +811 -0
  27. package/src/__tests__/default-video-provider.test.ts +63 -0
  28. package/src/__tests__/entity-image-handle.test.ts +43 -0
  29. package/src/__tests__/entity-variants.test.ts +33 -0
  30. package/src/__tests__/fal-lip-sync-sets.test.ts +31 -0
  31. package/src/__tests__/featured-entities.test.ts +33 -0
  32. package/src/__tests__/film-pricing.test.ts +64 -0
  33. package/src/__tests__/filter-condition.test.ts +285 -0
  34. package/src/__tests__/freecut-protocol.test.ts +35 -0
  35. package/src/__tests__/generate-text-items.test.ts +19 -0
  36. package/src/__tests__/generated-results.test.ts +50 -0
  37. package/src/__tests__/graph-utils.test.ts +618 -0
  38. package/src/__tests__/group-aggregation.test.ts +83 -0
  39. package/src/__tests__/i18n-locale-completeness.test.ts +156 -0
  40. package/src/__tests__/image-critic-node.test.ts +86 -0
  41. package/src/__tests__/image-mask-mode.test.ts +16 -0
  42. package/src/__tests__/image-ref-limit.test.ts +78 -0
  43. package/src/__tests__/inpaint-region.test.ts +24 -0
  44. package/src/__tests__/input-field-map-parity.test.ts +30 -0
  45. package/src/__tests__/json-evaluator.test.ts +289 -0
  46. package/src/__tests__/json-path.test.ts +64 -0
  47. package/src/__tests__/lip-sync-pricing.test.ts +142 -0
  48. package/src/__tests__/list-merge.test.ts +96 -0
  49. package/src/__tests__/list-sort.test.ts +134 -0
  50. package/src/__tests__/llm-models.test.ts +445 -0
  51. package/src/__tests__/location-mention-slug.test.ts +246 -0
  52. package/src/__tests__/locations-coverage-critic-schema.test.ts +92 -0
  53. package/src/__tests__/loop-delimiter.test.ts +39 -0
  54. package/src/__tests__/lottie-slots.test.ts +245 -0
  55. package/src/__tests__/model-catalog-flux2-resolutions.test.ts +21 -0
  56. package/src/__tests__/model-constants-location.test.ts +20 -0
  57. package/src/__tests__/model-constants.test.ts +122 -0
  58. package/src/__tests__/model-tree.test.ts +86 -0
  59. package/src/__tests__/no-internal-imports.test.ts +25 -0
  60. package/src/__tests__/node-default-mappings.test.ts +175 -0
  61. package/src/__tests__/node-mappable-fields.test.ts +7 -0
  62. package/src/__tests__/node-preset-export.test.ts +46 -0
  63. package/src/__tests__/node-preset-extract.test.ts +166 -0
  64. package/src/__tests__/node-refs-canonical.test.ts +54 -0
  65. package/src/__tests__/node-refs-combine.test.ts +54 -0
  66. package/src/__tests__/node-refs.test.ts +187 -0
  67. package/src/__tests__/node-runtime-keys.test.ts +99 -0
  68. package/src/__tests__/object-aspect-defaults.test.ts +59 -0
  69. package/src/__tests__/parameter-node-value.test.ts +137 -0
  70. package/src/__tests__/pinterest-url.test.ts +35 -0
  71. package/src/__tests__/pipeline-chat.test.ts +110 -0
  72. package/src/__tests__/pipeline-validation.test.ts +98 -0
  73. package/src/__tests__/presentation-utils.test.ts +1137 -0
  74. package/src/__tests__/producer-types.test.ts +47 -0
  75. package/src/__tests__/prompt-length-limits.test.ts +156 -0
  76. package/src/__tests__/provider-directive-defaults.test.ts +46 -0
  77. package/src/__tests__/reduce-strategy-registry.test.ts +43 -0
  78. package/src/__tests__/reference-board-templates.test.ts +33 -0
  79. package/src/__tests__/reference-roles.test.ts +36 -0
  80. package/src/__tests__/resolve-field-mappings.test.ts +110 -0
  81. package/src/__tests__/resolve-pipeline-model.test.ts +76 -0
  82. package/src/__tests__/scene-node-types.test.ts +80 -0
  83. package/src/__tests__/scraper-actors.test.ts +67 -0
  84. package/src/__tests__/seedance-2-catalog.test.ts +18 -0
  85. package/src/__tests__/seedance-2-credit-id.test.ts +23 -0
  86. package/src/__tests__/seedance-aspects.test.ts +52 -0
  87. package/src/__tests__/selector.test.ts +1151 -0
  88. package/src/__tests__/settled-with-limit.test.ts +214 -0
  89. package/src/__tests__/social-media-specs.test.ts +129 -0
  90. package/src/__tests__/sub-workflow-validation.test.ts +88 -0
  91. package/src/__tests__/switchx-pricing.test.ts +28 -0
  92. package/src/__tests__/to-connected-references.test.ts +153 -0
  93. package/src/__tests__/utilities.test.ts +541 -0
  94. package/src/__tests__/video-analysis-pricing.test.ts +58 -0
  95. package/src/__tests__/video-analysis.test.ts +63 -0
  96. package/src/__tests__/video-audio-capability.test.ts +204 -0
  97. package/src/__tests__/video-gen-providers.test.ts +28 -0
  98. package/src/__tests__/video-mode-aliases.test.ts +150 -0
  99. package/src/__tests__/video-ref-limits.test.ts +72 -0
  100. package/src/__tests__/video-utils.test.ts +303 -0
  101. package/src/__tests__/voice-changer-models.test.ts +33 -0
  102. package/src/__tests__/workflow-export-strip.test.ts +46 -0
  103. package/src/ai-avatar-pricing.ts +213 -0
  104. package/src/ancestor-refs.ts +48 -0
  105. package/src/animals.ts +231 -0
  106. package/src/audio-crossfade-curves.ts +73 -0
  107. package/src/audio-fx-presets.ts +64 -0
  108. package/src/avatar-payload-validators.ts +214 -0
  109. package/src/caption-styles.ts +31 -0
  110. package/src/character-aspect-defaults.ts +102 -0
  111. package/src/character-facets.ts +37 -0
  112. package/src/character-mention-slug.ts +166 -0
  113. package/src/character-usage-mode.ts +109 -0
  114. package/src/character-variant-assets.ts +121 -0
  115. package/src/character-voice.ts +151 -0
  116. package/src/cinematic-avatar-pricing.ts +111 -0
  117. package/src/clone-utils.ts +46 -0
  118. package/src/combine-transitions.ts +482 -0
  119. package/src/community.ts +112 -0
  120. package/src/component-types.ts +127 -0
  121. package/src/condition-variables.ts +65 -0
  122. package/src/credit-estimators/index.ts +15 -0
  123. package/src/credit-estimators/video-utils.ts +165 -0
  124. package/src/credit-identifiers.ts +304 -0
  125. package/src/entity-approval-types.ts +31 -0
  126. package/src/entity-aspect-defaults.ts +82 -0
  127. package/src/entity-asset-types.ts +296 -0
  128. package/src/entity-image-handle.ts +36 -0
  129. package/src/extra-refs.ts +139 -0
  130. package/src/featured-entities.ts +136 -0
  131. package/src/film-pricing.ts +117 -0
  132. package/src/filter-condition.ts +250 -0
  133. package/src/flux2-pricing.ts +18 -0
  134. package/src/freecut-protocol.ts +52 -0
  135. package/src/furniture.ts +167 -0
  136. package/src/generate-text-items.ts +15 -0
  137. package/src/generated-results.ts +63 -0
  138. package/src/group-aggregation.ts +75 -0
  139. package/src/hint-graph-types.ts +18 -0
  140. package/src/i18n/action-fx.ar.ts +89 -0
  141. package/src/i18n/action-fx.de.ts +89 -0
  142. package/src/i18n/action-fx.es.ts +89 -0
  143. package/src/i18n/action-fx.fr.ts +89 -0
  144. package/src/i18n/action-fx.he.ts +89 -0
  145. package/src/i18n/action-fx.hi.ts +89 -0
  146. package/src/i18n/action-fx.ja.ts +89 -0
  147. package/src/i18n/action-fx.ko.ts +89 -0
  148. package/src/i18n/action-fx.pt-BR.ts +89 -0
  149. package/src/i18n/action-fx.ru.ts +89 -0
  150. package/src/i18n/action-fx.zh-CN.ts +89 -0
  151. package/src/i18n/aesthetic.ar.ts +170 -0
  152. package/src/i18n/aesthetic.de.ts +48 -0
  153. package/src/i18n/aesthetic.es.ts +169 -0
  154. package/src/i18n/aesthetic.fr.ts +52 -0
  155. package/src/i18n/aesthetic.he.ts +169 -0
  156. package/src/i18n/aesthetic.hi.ts +55 -0
  157. package/src/i18n/aesthetic.ja.ts +165 -0
  158. package/src/i18n/aesthetic.ko.ts +169 -0
  159. package/src/i18n/aesthetic.pt-BR.ts +148 -0
  160. package/src/i18n/aesthetic.ru.ts +155 -0
  161. package/src/i18n/aesthetic.zh-CN.ts +172 -0
  162. package/src/i18n/animals.ar.ts +145 -0
  163. package/src/i18n/animals.de.ts +144 -0
  164. package/src/i18n/animals.es.ts +158 -0
  165. package/src/i18n/animals.fr.ts +144 -0
  166. package/src/i18n/animals.he.ts +155 -0
  167. package/src/i18n/animals.hi.ts +157 -0
  168. package/src/i18n/animals.ja.ts +159 -0
  169. package/src/i18n/animals.ko.ts +159 -0
  170. package/src/i18n/animals.pt-BR.ts +157 -0
  171. package/src/i18n/animals.ru.ts +155 -0
  172. package/src/i18n/animals.zh-CN.ts +155 -0
  173. package/src/i18n/atmosphere.ar.ts +49 -0
  174. package/src/i18n/atmosphere.de.ts +47 -0
  175. package/src/i18n/atmosphere.es.ts +50 -0
  176. package/src/i18n/atmosphere.fr.ts +47 -0
  177. package/src/i18n/atmosphere.he.ts +47 -0
  178. package/src/i18n/atmosphere.hi.ts +49 -0
  179. package/src/i18n/atmosphere.ja.ts +49 -0
  180. package/src/i18n/atmosphere.ko.ts +49 -0
  181. package/src/i18n/atmosphere.pt-BR.ts +49 -0
  182. package/src/i18n/atmosphere.ru.ts +47 -0
  183. package/src/i18n/atmosphere.zh-CN.ts +152 -0
  184. package/src/i18n/backdrop.ar.ts +53 -0
  185. package/src/i18n/backdrop.de.ts +46 -0
  186. package/src/i18n/backdrop.es.ts +54 -0
  187. package/src/i18n/backdrop.fr.ts +52 -0
  188. package/src/i18n/backdrop.he.ts +51 -0
  189. package/src/i18n/backdrop.hi.ts +59 -0
  190. package/src/i18n/backdrop.ja.ts +61 -0
  191. package/src/i18n/backdrop.ko.ts +59 -0
  192. package/src/i18n/backdrop.pt-BR.ts +59 -0
  193. package/src/i18n/backdrop.ru.ts +57 -0
  194. package/src/i18n/backdrop.zh-CN.ts +57 -0
  195. package/src/i18n/camera-format.ar.ts +40 -0
  196. package/src/i18n/camera-format.de.ts +37 -0
  197. package/src/i18n/camera-format.es.ts +41 -0
  198. package/src/i18n/camera-format.fr.ts +37 -0
  199. package/src/i18n/camera-format.he.ts +37 -0
  200. package/src/i18n/camera-format.hi.ts +42 -0
  201. package/src/i18n/camera-format.ja.ts +37 -0
  202. package/src/i18n/camera-format.ko.ts +37 -0
  203. package/src/i18n/camera-format.pt-BR.ts +46 -0
  204. package/src/i18n/camera-format.ru.ts +42 -0
  205. package/src/i18n/camera-format.zh-CN.ts +42 -0
  206. package/src/i18n/camera-motions.ar.ts +86 -0
  207. package/src/i18n/camera-motions.de.ts +74 -0
  208. package/src/i18n/camera-motions.es.ts +86 -0
  209. package/src/i18n/camera-motions.fr.ts +86 -0
  210. package/src/i18n/camera-motions.he.ts +86 -0
  211. package/src/i18n/camera-motions.hi.ts +99 -0
  212. package/src/i18n/camera-motions.ja.ts +99 -0
  213. package/src/i18n/camera-motions.ko.ts +99 -0
  214. package/src/i18n/camera-motions.pt-BR.ts +99 -0
  215. package/src/i18n/camera-motions.ru.ts +99 -0
  216. package/src/i18n/camera-motions.zh-CN.ts +99 -0
  217. package/src/i18n/character-fx.ar.ts +72 -0
  218. package/src/i18n/character-fx.de.ts +72 -0
  219. package/src/i18n/character-fx.es.ts +72 -0
  220. package/src/i18n/character-fx.fr.ts +72 -0
  221. package/src/i18n/character-fx.he.ts +72 -0
  222. package/src/i18n/character-fx.hi.ts +72 -0
  223. package/src/i18n/character-fx.ja.ts +72 -0
  224. package/src/i18n/character-fx.ko.ts +72 -0
  225. package/src/i18n/character-fx.pt-BR.ts +72 -0
  226. package/src/i18n/character-fx.ru.ts +72 -0
  227. package/src/i18n/character-fx.zh-CN.ts +72 -0
  228. package/src/i18n/color-look.ar.ts +50 -0
  229. package/src/i18n/color-look.de.ts +47 -0
  230. package/src/i18n/color-look.es.ts +50 -0
  231. package/src/i18n/color-look.fr.ts +50 -0
  232. package/src/i18n/color-look.he.ts +49 -0
  233. package/src/i18n/color-look.hi.ts +52 -0
  234. package/src/i18n/color-look.ja.ts +54 -0
  235. package/src/i18n/color-look.ko.ts +54 -0
  236. package/src/i18n/color-look.pt-BR.ts +54 -0
  237. package/src/i18n/color-look.ru.ts +52 -0
  238. package/src/i18n/color-look.zh-CN.ts +53 -0
  239. package/src/i18n/composition-effects.ar.ts +26 -0
  240. package/src/i18n/composition-effects.de.ts +25 -0
  241. package/src/i18n/composition-effects.es.ts +27 -0
  242. package/src/i18n/composition-effects.fr.ts +25 -0
  243. package/src/i18n/composition-effects.he.ts +25 -0
  244. package/src/i18n/composition-effects.hi.ts +27 -0
  245. package/src/i18n/composition-effects.ja.ts +27 -0
  246. package/src/i18n/composition-effects.ko.ts +27 -0
  247. package/src/i18n/composition-effects.pt-BR.ts +25 -0
  248. package/src/i18n/composition-effects.ru.ts +82 -0
  249. package/src/i18n/composition-effects.zh-CN.ts +25 -0
  250. package/src/i18n/era.ar.ts +37 -0
  251. package/src/i18n/era.de.ts +34 -0
  252. package/src/i18n/era.es.ts +37 -0
  253. package/src/i18n/era.fr.ts +37 -0
  254. package/src/i18n/era.he.ts +36 -0
  255. package/src/i18n/era.hi.ts +39 -0
  256. package/src/i18n/era.ja.ts +41 -0
  257. package/src/i18n/era.ko.ts +39 -0
  258. package/src/i18n/era.pt-BR.ts +41 -0
  259. package/src/i18n/era.ru.ts +121 -0
  260. package/src/i18n/era.zh-CN.ts +39 -0
  261. package/src/i18n/exposure-settings.ar.ts +28 -0
  262. package/src/i18n/exposure-settings.de.ts +26 -0
  263. package/src/i18n/exposure-settings.es.ts +28 -0
  264. package/src/i18n/exposure-settings.fr.ts +29 -0
  265. package/src/i18n/exposure-settings.he.ts +28 -0
  266. package/src/i18n/exposure-settings.hi.ts +31 -0
  267. package/src/i18n/exposure-settings.ja.ts +31 -0
  268. package/src/i18n/exposure-settings.ko.ts +31 -0
  269. package/src/i18n/exposure-settings.pt-BR.ts +31 -0
  270. package/src/i18n/exposure-settings.ru.ts +31 -0
  271. package/src/i18n/exposure-settings.zh-CN.ts +31 -0
  272. package/src/i18n/framing.ar.ts +78 -0
  273. package/src/i18n/framing.de.ts +73 -0
  274. package/src/i18n/framing.es.ts +78 -0
  275. package/src/i18n/framing.fr.ts +78 -0
  276. package/src/i18n/framing.he.ts +77 -0
  277. package/src/i18n/framing.hi.ts +82 -0
  278. package/src/i18n/framing.ja.ts +84 -0
  279. package/src/i18n/framing.ko.ts +82 -0
  280. package/src/i18n/framing.pt-BR.ts +84 -0
  281. package/src/i18n/framing.ru.ts +82 -0
  282. package/src/i18n/framing.zh-CN.ts +82 -0
  283. package/src/i18n/furniture.ar.ts +92 -0
  284. package/src/i18n/furniture.de.ts +93 -0
  285. package/src/i18n/furniture.es.ts +101 -0
  286. package/src/i18n/furniture.fr.ts +93 -0
  287. package/src/i18n/furniture.he.ts +101 -0
  288. package/src/i18n/furniture.hi.ts +101 -0
  289. package/src/i18n/furniture.ja.ts +101 -0
  290. package/src/i18n/furniture.ko.ts +101 -0
  291. package/src/i18n/furniture.pt-BR.ts +101 -0
  292. package/src/i18n/furniture.ru.ts +101 -0
  293. package/src/i18n/furniture.zh-CN.ts +101 -0
  294. package/src/i18n/held-prop.ar.ts +74 -0
  295. package/src/i18n/held-prop.de.ts +65 -0
  296. package/src/i18n/held-prop.es.ts +83 -0
  297. package/src/i18n/held-prop.fr.ts +74 -0
  298. package/src/i18n/held-prop.he.ts +73 -0
  299. package/src/i18n/held-prop.hi.ts +82 -0
  300. package/src/i18n/held-prop.ja.ts +84 -0
  301. package/src/i18n/held-prop.ko.ts +84 -0
  302. package/src/i18n/held-prop.pt-BR.ts +84 -0
  303. package/src/i18n/held-prop.ru.ts +82 -0
  304. package/src/i18n/held-prop.zh-CN.ts +82 -0
  305. package/src/i18n/index.ts +208 -0
  306. package/src/i18n/instrumentation.ar.ts +195 -0
  307. package/src/i18n/instrumentation.de.ts +195 -0
  308. package/src/i18n/instrumentation.es.ts +195 -0
  309. package/src/i18n/instrumentation.fr.ts +195 -0
  310. package/src/i18n/instrumentation.he.ts +195 -0
  311. package/src/i18n/instrumentation.hi.ts +195 -0
  312. package/src/i18n/instrumentation.ja.ts +195 -0
  313. package/src/i18n/instrumentation.ko.ts +195 -0
  314. package/src/i18n/instrumentation.pt-BR.ts +195 -0
  315. package/src/i18n/instrumentation.ru.ts +195 -0
  316. package/src/i18n/instrumentation.zh-CN.ts +195 -0
  317. package/src/i18n/lens.ar.ts +24 -0
  318. package/src/i18n/lens.de.ts +24 -0
  319. package/src/i18n/lens.es.ts +24 -0
  320. package/src/i18n/lens.fr.ts +24 -0
  321. package/src/i18n/lens.he.ts +24 -0
  322. package/src/i18n/lens.hi.ts +24 -0
  323. package/src/i18n/lens.ja.ts +24 -0
  324. package/src/i18n/lens.ko.ts +24 -0
  325. package/src/i18n/lens.pt-BR.ts +24 -0
  326. package/src/i18n/lens.ru.ts +25 -0
  327. package/src/i18n/lens.zh-CN.ts +24 -0
  328. package/src/i18n/lighting.ar.ts +86 -0
  329. package/src/i18n/lighting.de.ts +84 -0
  330. package/src/i18n/lighting.es.ts +89 -0
  331. package/src/i18n/lighting.fr.ts +84 -0
  332. package/src/i18n/lighting.he.ts +88 -0
  333. package/src/i18n/lighting.hi.ts +90 -0
  334. package/src/i18n/lighting.ja.ts +90 -0
  335. package/src/i18n/lighting.ko.ts +90 -0
  336. package/src/i18n/lighting.pt-BR.ts +90 -0
  337. package/src/i18n/lighting.ru.ts +88 -0
  338. package/src/i18n/lighting.zh-CN.ts +92 -0
  339. package/src/i18n/loop-subject.ar.ts +39 -0
  340. package/src/i18n/loop-subject.de.ts +39 -0
  341. package/src/i18n/loop-subject.es.ts +39 -0
  342. package/src/i18n/loop-subject.fr.ts +39 -0
  343. package/src/i18n/loop-subject.he.ts +39 -0
  344. package/src/i18n/loop-subject.hi.ts +39 -0
  345. package/src/i18n/loop-subject.ja.ts +39 -0
  346. package/src/i18n/loop-subject.ko.ts +39 -0
  347. package/src/i18n/loop-subject.pt-BR.ts +39 -0
  348. package/src/i18n/loop-subject.ru.ts +39 -0
  349. package/src/i18n/loop-subject.zh-CN.ts +39 -0
  350. package/src/i18n/materials.ar.ts +80 -0
  351. package/src/i18n/materials.de.ts +80 -0
  352. package/src/i18n/materials.es.ts +87 -0
  353. package/src/i18n/materials.fr.ts +80 -0
  354. package/src/i18n/materials.he.ts +86 -0
  355. package/src/i18n/materials.hi.ts +86 -0
  356. package/src/i18n/materials.ja.ts +88 -0
  357. package/src/i18n/materials.ko.ts +88 -0
  358. package/src/i18n/materials.pt-BR.ts +88 -0
  359. package/src/i18n/materials.ru.ts +86 -0
  360. package/src/i18n/materials.zh-CN.ts +86 -0
  361. package/src/i18n/mood.ar.ts +61 -0
  362. package/src/i18n/mood.de.ts +61 -0
  363. package/src/i18n/mood.es.ts +66 -0
  364. package/src/i18n/mood.fr.ts +61 -0
  365. package/src/i18n/mood.he.ts +64 -0
  366. package/src/i18n/mood.hi.ts +66 -0
  367. package/src/i18n/mood.ja.ts +66 -0
  368. package/src/i18n/mood.ko.ts +66 -0
  369. package/src/i18n/mood.pt-BR.ts +64 -0
  370. package/src/i18n/mood.ru.ts +64 -0
  371. package/src/i18n/mood.zh-CN.ts +64 -0
  372. package/src/i18n/music-genre.ar.ts +302 -0
  373. package/src/i18n/music-genre.de.ts +302 -0
  374. package/src/i18n/music-genre.es.ts +302 -0
  375. package/src/i18n/music-genre.fr.ts +302 -0
  376. package/src/i18n/music-genre.he.ts +302 -0
  377. package/src/i18n/music-genre.hi.ts +302 -0
  378. package/src/i18n/music-genre.ja.ts +302 -0
  379. package/src/i18n/music-genre.ko.ts +302 -0
  380. package/src/i18n/music-genre.pt-BR.ts +302 -0
  381. package/src/i18n/music-genre.ru.ts +302 -0
  382. package/src/i18n/music-genre.zh-CN.ts +302 -0
  383. package/src/i18n/music-mood.ar.ts +75 -0
  384. package/src/i18n/music-mood.de.ts +75 -0
  385. package/src/i18n/music-mood.es.ts +75 -0
  386. package/src/i18n/music-mood.fr.ts +75 -0
  387. package/src/i18n/music-mood.he.ts +75 -0
  388. package/src/i18n/music-mood.hi.ts +75 -0
  389. package/src/i18n/music-mood.ja.ts +75 -0
  390. package/src/i18n/music-mood.ko.ts +75 -0
  391. package/src/i18n/music-mood.pt-BR.ts +75 -0
  392. package/src/i18n/music-mood.ru.ts +75 -0
  393. package/src/i18n/music-mood.zh-CN.ts +75 -0
  394. package/src/i18n/person.ar.ts +638 -0
  395. package/src/i18n/person.de.ts +629 -0
  396. package/src/i18n/person.es.ts +645 -0
  397. package/src/i18n/person.fr.ts +621 -0
  398. package/src/i18n/person.he.ts +656 -0
  399. package/src/i18n/person.hi.ts +654 -0
  400. package/src/i18n/person.ja.ts +662 -0
  401. package/src/i18n/person.ko.ts +662 -0
  402. package/src/i18n/person.pt-BR.ts +656 -0
  403. package/src/i18n/person.ru.ts +656 -0
  404. package/src/i18n/person.zh-CN.ts +656 -0
  405. package/src/i18n/photo-genre.ar.ts +58 -0
  406. package/src/i18n/photo-genre.de.ts +52 -0
  407. package/src/i18n/photo-genre.es.ts +64 -0
  408. package/src/i18n/photo-genre.fr.ts +58 -0
  409. package/src/i18n/photo-genre.he.ts +62 -0
  410. package/src/i18n/photo-genre.hi.ts +63 -0
  411. package/src/i18n/photo-genre.ja.ts +65 -0
  412. package/src/i18n/photo-genre.ko.ts +63 -0
  413. package/src/i18n/photo-genre.pt-BR.ts +65 -0
  414. package/src/i18n/photo-genre.ru.ts +63 -0
  415. package/src/i18n/photo-genre.zh-CN.ts +63 -0
  416. package/src/i18n/photographer.ar.ts +75 -0
  417. package/src/i18n/photographer.de.ts +74 -0
  418. package/src/i18n/photographer.es.ts +77 -0
  419. package/src/i18n/photographer.fr.ts +77 -0
  420. package/src/i18n/photographer.he.ts +78 -0
  421. package/src/i18n/photographer.hi.ts +80 -0
  422. package/src/i18n/photographer.ja.ts +84 -0
  423. package/src/i18n/photographer.ko.ts +84 -0
  424. package/src/i18n/photographer.pt-BR.ts +83 -0
  425. package/src/i18n/photographer.ru.ts +78 -0
  426. package/src/i18n/photographer.zh-CN.ts +78 -0
  427. package/src/i18n/pose.ar.ts +95 -0
  428. package/src/i18n/pose.de.ts +96 -0
  429. package/src/i18n/pose.es.ts +104 -0
  430. package/src/i18n/pose.fr.ts +96 -0
  431. package/src/i18n/pose.he.ts +104 -0
  432. package/src/i18n/pose.hi.ts +104 -0
  433. package/src/i18n/pose.ja.ts +104 -0
  434. package/src/i18n/pose.ko.ts +104 -0
  435. package/src/i18n/pose.pt-BR.ts +104 -0
  436. package/src/i18n/pose.ru.ts +104 -0
  437. package/src/i18n/pose.zh-CN.ts +104 -0
  438. package/src/i18n/post-process-effects.ar.ts +25 -0
  439. package/src/i18n/post-process-effects.de.ts +24 -0
  440. package/src/i18n/post-process-effects.es.ts +26 -0
  441. package/src/i18n/post-process-effects.fr.ts +24 -0
  442. package/src/i18n/post-process-effects.he.ts +24 -0
  443. package/src/i18n/post-process-effects.hi.ts +26 -0
  444. package/src/i18n/post-process-effects.ja.ts +26 -0
  445. package/src/i18n/post-process-effects.ko.ts +26 -0
  446. package/src/i18n/post-process-effects.pt-BR.ts +24 -0
  447. package/src/i18n/post-process-effects.ru.ts +24 -0
  448. package/src/i18n/post-process-effects.zh-CN.ts +24 -0
  449. package/src/i18n/render-quality.ar.ts +34 -0
  450. package/src/i18n/render-quality.de.ts +30 -0
  451. package/src/i18n/render-quality.es.ts +35 -0
  452. package/src/i18n/render-quality.fr.ts +34 -0
  453. package/src/i18n/render-quality.he.ts +33 -0
  454. package/src/i18n/render-quality.hi.ts +39 -0
  455. package/src/i18n/render-quality.ja.ts +39 -0
  456. package/src/i18n/render-quality.ko.ts +39 -0
  457. package/src/i18n/render-quality.pt-BR.ts +37 -0
  458. package/src/i18n/render-quality.ru.ts +37 -0
  459. package/src/i18n/render-quality.zh-CN.ts +37 -0
  460. package/src/i18n/seasons.ar.ts +10 -0
  461. package/src/i18n/seasons.de.ts +10 -0
  462. package/src/i18n/seasons.es.ts +10 -0
  463. package/src/i18n/seasons.fr.ts +10 -0
  464. package/src/i18n/seasons.he.ts +10 -0
  465. package/src/i18n/seasons.hi.ts +10 -0
  466. package/src/i18n/seasons.ja.ts +10 -0
  467. package/src/i18n/seasons.ko.ts +10 -0
  468. package/src/i18n/seasons.pt-BR.ts +10 -0
  469. package/src/i18n/seasons.ru.ts +10 -0
  470. package/src/i18n/seasons.zh-CN.ts +10 -0
  471. package/src/i18n/setting.ar.ts +73 -0
  472. package/src/i18n/setting.de.ts +73 -0
  473. package/src/i18n/setting.es.ts +77 -0
  474. package/src/i18n/setting.fr.ts +73 -0
  475. package/src/i18n/setting.he.ts +76 -0
  476. package/src/i18n/setting.hi.ts +76 -0
  477. package/src/i18n/setting.ja.ts +78 -0
  478. package/src/i18n/setting.ko.ts +76 -0
  479. package/src/i18n/setting.pt-BR.ts +78 -0
  480. package/src/i18n/setting.ru.ts +76 -0
  481. package/src/i18n/setting.zh-CN.ts +76 -0
  482. package/src/i18n/style.ar.ts +55 -0
  483. package/src/i18n/style.de.ts +54 -0
  484. package/src/i18n/style.es.ts +54 -0
  485. package/src/i18n/style.fr.ts +54 -0
  486. package/src/i18n/style.he.ts +54 -0
  487. package/src/i18n/style.hi.ts +54 -0
  488. package/src/i18n/style.ja.ts +54 -0
  489. package/src/i18n/style.ko.ts +54 -0
  490. package/src/i18n/style.pt-BR.ts +56 -0
  491. package/src/i18n/style.ru.ts +54 -0
  492. package/src/i18n/style.zh-CN.ts +54 -0
  493. package/src/i18n/styling.ar.ts +295 -0
  494. package/src/i18n/styling.de.ts +285 -0
  495. package/src/i18n/styling.es.ts +317 -0
  496. package/src/i18n/styling.fr.ts +285 -0
  497. package/src/i18n/styling.he.ts +307 -0
  498. package/src/i18n/styling.hi.ts +323 -0
  499. package/src/i18n/styling.ja.ts +317 -0
  500. package/src/i18n/styling.ko.ts +311 -0
  501. package/src/i18n/styling.pt-BR.ts +301 -0
  502. package/src/i18n/styling.ru.ts +301 -0
  503. package/src/i18n/styling.zh-CN.ts +307 -0
  504. package/src/i18n/temporal.ar.ts +31 -0
  505. package/src/i18n/temporal.de.ts +24 -0
  506. package/src/i18n/temporal.es.ts +27 -0
  507. package/src/i18n/temporal.fr.ts +28 -0
  508. package/src/i18n/temporal.he.ts +31 -0
  509. package/src/i18n/temporal.hi.ts +31 -0
  510. package/src/i18n/temporal.ja.ts +31 -0
  511. package/src/i18n/temporal.ko.ts +31 -0
  512. package/src/i18n/temporal.pt-BR.ts +31 -0
  513. package/src/i18n/temporal.ru.ts +31 -0
  514. package/src/i18n/temporal.zh-CN.ts +31 -0
  515. package/src/i18n/transitions.ar.ts +97 -0
  516. package/src/i18n/transitions.de.ts +97 -0
  517. package/src/i18n/transitions.es.ts +97 -0
  518. package/src/i18n/transitions.fr.ts +97 -0
  519. package/src/i18n/transitions.he.ts +97 -0
  520. package/src/i18n/transitions.hi.ts +97 -0
  521. package/src/i18n/transitions.ja.ts +97 -0
  522. package/src/i18n/transitions.ko.ts +97 -0
  523. package/src/i18n/transitions.pt-BR.ts +97 -0
  524. package/src/i18n/transitions.ru.ts +97 -0
  525. package/src/i18n/transitions.zh-CN.ts +97 -0
  526. package/src/i18n/types.ts +158 -0
  527. package/src/i18n/vehicles.ar.ts +137 -0
  528. package/src/i18n/vehicles.de.ts +125 -0
  529. package/src/i18n/vehicles.es.ts +138 -0
  530. package/src/i18n/vehicles.fr.ts +125 -0
  531. package/src/i18n/vehicles.he.ts +136 -0
  532. package/src/i18n/vehicles.hi.ts +138 -0
  533. package/src/i18n/vehicles.ja.ts +138 -0
  534. package/src/i18n/vehicles.ko.ts +138 -0
  535. package/src/i18n/vehicles.pt-BR.ts +136 -0
  536. package/src/i18n/vehicles.ru.ts +136 -0
  537. package/src/i18n/vehicles.zh-CN.ts +136 -0
  538. package/src/i18n/voice-character.ar.ts +126 -0
  539. package/src/i18n/voice-character.de.ts +126 -0
  540. package/src/i18n/voice-character.es.ts +126 -0
  541. package/src/i18n/voice-character.fr.ts +126 -0
  542. package/src/i18n/voice-character.he.ts +126 -0
  543. package/src/i18n/voice-character.hi.ts +126 -0
  544. package/src/i18n/voice-character.ja.ts +126 -0
  545. package/src/i18n/voice-character.ko.ts +126 -0
  546. package/src/i18n/voice-character.pt-BR.ts +126 -0
  547. package/src/i18n/voice-character.ru.ts +126 -0
  548. package/src/i18n/voice-character.zh-CN.ts +126 -0
  549. package/src/i18n/voice-delivery.ar.ts +77 -0
  550. package/src/i18n/voice-delivery.de.ts +77 -0
  551. package/src/i18n/voice-delivery.es.ts +77 -0
  552. package/src/i18n/voice-delivery.fr.ts +77 -0
  553. package/src/i18n/voice-delivery.he.ts +77 -0
  554. package/src/i18n/voice-delivery.hi.ts +77 -0
  555. package/src/i18n/voice-delivery.ja.ts +77 -0
  556. package/src/i18n/voice-delivery.ko.ts +77 -0
  557. package/src/i18n/voice-delivery.pt-BR.ts +77 -0
  558. package/src/i18n/voice-delivery.ru.ts +77 -0
  559. package/src/i18n/voice-delivery.zh-CN.ts +77 -0
  560. package/src/i18n/weapons.ar.ts +115 -0
  561. package/src/i18n/weapons.de.ts +114 -0
  562. package/src/i18n/weapons.es.ts +116 -0
  563. package/src/i18n/weapons.fr.ts +103 -0
  564. package/src/i18n/weapons.he.ts +114 -0
  565. package/src/i18n/weapons.hi.ts +116 -0
  566. package/src/i18n/weapons.ja.ts +116 -0
  567. package/src/i18n/weapons.ko.ts +116 -0
  568. package/src/i18n/weapons.pt-BR.ts +114 -0
  569. package/src/i18n/weapons.ru.ts +114 -0
  570. package/src/i18n/weapons.zh-CN.ts +114 -0
  571. package/src/image-critic-node.ts +61 -0
  572. package/src/image-critic-types.ts +40 -0
  573. package/src/index.ts +866 -0
  574. package/src/inject-upstream.ts +15 -0
  575. package/src/inpaint-region.ts +51 -0
  576. package/src/json-evaluator.ts +732 -0
  577. package/src/json-path.ts +52 -0
  578. package/src/lip-sync-pricing.ts +90 -0
  579. package/src/list-merge.ts +40 -0
  580. package/src/list-sort.ts +122 -0
  581. package/src/list-source-resolver.ts +50 -0
  582. package/src/llm-models.ts +245 -0
  583. package/src/location-mention-slug.ts +291 -0
  584. package/src/location-preset-catalog-map.ts +128 -0
  585. package/src/loop-delimiter.ts +73 -0
  586. package/src/lora-routing.ts +95 -0
  587. package/src/lottie-overlay-catalog.ts +175 -0
  588. package/src/lottie-slots.ts +309 -0
  589. package/src/model-catalog.ts +2386 -0
  590. package/src/model-constants.ts +1910 -0
  591. package/src/model-tree.ts +120 -0
  592. package/src/monetization.ts +15 -0
  593. package/src/multi-pick.ts +42 -0
  594. package/src/node-default-mappings.ts +262 -0
  595. package/src/node-mappable-fields.ts +70 -0
  596. package/src/node-preset-export.ts +56 -0
  597. package/src/node-preset-extract.ts +147 -0
  598. package/src/node-refs.ts +253 -0
  599. package/src/node-runtime-keys.ts +117 -0
  600. package/src/object-aspect-defaults.ts +72 -0
  601. package/src/object-picker-types.ts +22 -0
  602. package/src/parameter-node-value.ts +284 -0
  603. package/src/pinterest-url.ts +28 -0
  604. package/src/pipeline-chat.ts +72 -0
  605. package/src/pipeline-defaults.ts +318 -0
  606. package/src/pipeline-events.ts +162 -0
  607. package/src/pipeline-state-types.ts +196 -0
  608. package/src/pipeline-types.ts +647 -0
  609. package/src/pipeline-validation.ts +87 -0
  610. package/src/presentation-types.ts +23 -0
  611. package/src/presentation-utils.ts +397 -0
  612. package/src/producer-types.ts +188 -0
  613. package/src/progress-curve.ts +99 -0
  614. package/src/provider-directive-defaults.ts +69 -0
  615. package/src/reduce-strategy-registry.ts +121 -0
  616. package/src/reference-board-templates.ts +81 -0
  617. package/src/reference-roles.ts +153 -0
  618. package/src/reference-sheet/__tests__/board-to-column.test.ts +20 -0
  619. package/src/reference-sheet/__tests__/catalog.test.ts +50 -0
  620. package/src/reference-sheet/__tests__/cost.test.ts +38 -0
  621. package/src/reference-sheet/__tests__/no-drift.test.ts +28 -0
  622. package/src/reference-sheet/__tests__/panel-plan.test.ts +61 -0
  623. package/src/reference-sheet/__tests__/panel-prompts.test.ts +17 -0
  624. package/src/reference-sheet/__tests__/plan-generation.test.ts +72 -0
  625. package/src/reference-sheet/__tests__/presets.test.ts +54 -0
  626. package/src/reference-sheet/__tests__/types.test.ts +24 -0
  627. package/src/reference-sheet/catalog.ts +144 -0
  628. package/src/reference-sheet/cost.ts +37 -0
  629. package/src/reference-sheet/credit-id.ts +7 -0
  630. package/src/reference-sheet/index.ts +8 -0
  631. package/src/reference-sheet/panel-plan.ts +65 -0
  632. package/src/reference-sheet/panel-prompts.ts +14 -0
  633. package/src/reference-sheet/plan-generation.ts +79 -0
  634. package/src/reference-sheet/presets.ts +77 -0
  635. package/src/reference-sheet/types.ts +92 -0
  636. package/src/repeat-types.ts +97 -0
  637. package/src/resolve-field-mappings.ts +119 -0
  638. package/src/route-filter.ts +80 -0
  639. package/src/scene-helper-types.ts +249 -0
  640. package/src/scene-node-types.ts +281 -0
  641. package/src/scraper-actors.ts +57 -0
  642. package/src/scraper-output-schemas.ts +25 -0
  643. package/src/seasons.ts +45 -0
  644. package/src/selector.ts +864 -0
  645. package/src/settled-with-limit.ts +42 -0
  646. package/src/shot-element.ts +43 -0
  647. package/src/social-media-specs.ts +121 -0
  648. package/src/social-post.ts +24 -0
  649. package/src/sub-workflow-validation.ts +78 -0
  650. package/src/supported-fonts.ts +40 -0
  651. package/src/surround.ts +113 -0
  652. package/src/switchx-pricing.ts +33 -0
  653. package/src/text-separators.ts +59 -0
  654. package/src/to-connected-references.ts +77 -0
  655. package/src/types.ts +272 -0
  656. package/src/variant-job-id.ts +17 -0
  657. package/src/vehicles.ts +213 -0
  658. package/src/video-analysis-pricing.ts +74 -0
  659. package/src/video-analysis.ts +127 -0
  660. package/src/video-duration.ts +25 -0
  661. package/src/voice-changer-models.ts +36 -0
  662. package/src/voice-matcher-types.ts +37 -0
  663. package/src/voices.ts +92 -0
  664. package/src/weapons.ts +190 -0
  665. package/src/workflow-export.ts +153 -0
@@ -0,0 +1,864 @@
1
+ /**
2
+ * Edge range utilities for resolving 1-based index expressions
3
+ * and applying range/step slicing to output lists.
4
+ *
5
+ * Used by both frontend DAG executor and backend orchestrator
6
+ * to support edge-level range, step, and item selection.
7
+ */
8
+
9
+ import { evaluateCondition, substituteTriggerTokens, tryParseJson } from "./filter-condition.js"
10
+ import { resolveNodeRefs } from "./node-refs.js"
11
+
12
+ /** Selector tab on an edge — "range" (from/to/step) or "list" (expression). */
13
+ export type SelectorMode = "range" | "list"
14
+
15
+ /** Per-edge output mode controlling how upstream results reach downstream. */
16
+ export type OutputMode = "last" | "each" | "all" | "item"
17
+
18
+ /** Edge-data fields consumed by selectListItems / isDefaultSelectorConfig. */
19
+ export type SelectorFields = {
20
+ selectorMode?: SelectorMode
21
+ listExpression?: string
22
+ rangeFrom?: string
23
+ rangeTo?: string
24
+ rangeStep?: number
25
+ }
26
+
27
+ /** Full selector mode set — extends the edge-selector modes with new operators. */
28
+ export type FullSelectorMode =
29
+ | "item"
30
+ | "range"
31
+ | "list"
32
+ | "random"
33
+ | "modulo"
34
+ | "predicate"
35
+ | "named-key"
36
+
37
+ /** Operator set for predicate mode — reused from filter-list. */
38
+ export type SelectorPredicateOp =
39
+ | ">" | "<" | ">=" | "<="
40
+ | "=" | "!="
41
+ | "contains" | "not_contains"
42
+ | "starts_with" | "ends_with"
43
+ | "regex"
44
+ | "exists" | "not_exists"
45
+
46
+ /** Config consumed by `runSelector` and `resolveSelectorRefs`. */
47
+ export type SelectorConfig = {
48
+ mode: FullSelectorMode
49
+ // item
50
+ itemIndex?: string
51
+ // range
52
+ rangeFrom?: string
53
+ rangeTo?: string
54
+ rangeStep?: number
55
+ // list
56
+ listExpression?: string
57
+ // random
58
+ seed?: string
59
+ randomCount?: number
60
+ // modulo
61
+ moduloDivisor?: string
62
+ // predicate
63
+ predicateField?: string
64
+ predicateOp?: SelectorPredicateOp
65
+ predicateValue?: string
66
+ predicateMatch?: "first" | "all"
67
+ predicateCaseSensitive?: boolean
68
+ // named-key
69
+ namedKeyField?: string
70
+ namedKeyValue?: string
71
+ }
72
+
73
+ /** Result of a selector run. `picked ∪ rest = items`, `picked ∩ rest = ∅`. */
74
+ export type SelectorResult = { picked: string[]; rest: string[] }
75
+
76
+ /**
77
+ * Resolves a 1-based index expression to a 0-based array index.
78
+ *
79
+ * Supported expressions:
80
+ * - "1", "2", ... — absolute 1-based index
81
+ * - "last" — last element
82
+ * - "last-1", "last-2", ... — relative from end
83
+ *
84
+ * Out-of-bounds values are clamped. Malformed input falls back to `defaultExpr`.
85
+ */
86
+ export function resolveIndex(
87
+ expr: string,
88
+ listLength: number,
89
+ defaultExpr: string = "1",
90
+ ): number {
91
+ if (listLength <= 0) return 0
92
+
93
+ const trimmed = expr.trim()
94
+ let index: number
95
+
96
+ if (trimmed === "last") {
97
+ index = listLength - 1
98
+ } else if (trimmed.startsWith("last-")) {
99
+ const offset = parseInt(trimmed.slice(5), 10)
100
+ if (isNaN(offset) || offset < 0) return resolveIndex(defaultExpr === expr ? "1" : defaultExpr, listLength)
101
+ index = listLength - 1 - offset
102
+ } else {
103
+ const n = parseInt(trimmed, 10)
104
+ if (isNaN(n)) return resolveIndex(defaultExpr === expr ? "1" : defaultExpr, listLength)
105
+ index = n - 1
106
+ }
107
+
108
+ return Math.max(0, Math.min(index, listLength - 1))
109
+ }
110
+
111
+ /**
112
+ * Applies range and step slicing to a list of strings.
113
+ *
114
+ * `from` and `to` are 1-based index expressions (see `resolveIndex`).
115
+ * `step` controls iteration direction and stride:
116
+ * - positive step: forward iteration (from must be <= to)
117
+ * - negative step: reverse iteration (from must be >= to)
118
+ * - step 0 is treated as step 1
119
+ *
120
+ * Returns empty array if direction mismatches (e.g., from < to with negative step).
121
+ */
122
+ export function applyRange(
123
+ list: string[],
124
+ from?: string,
125
+ to?: string,
126
+ step?: number,
127
+ ): string[] {
128
+ if (list.length === 0) return []
129
+
130
+ const fromIdx = resolveIndex(from ?? "1", list.length, "1")
131
+ const toIdx = resolveIndex(to ?? "last", list.length, "last")
132
+ const effectiveStep = step === 0 || step === undefined ? 1 : step
133
+
134
+ if (effectiveStep > 0 && fromIdx > toIdx) return []
135
+ if (effectiveStep < 0 && fromIdx < toIdx) return []
136
+
137
+ const result: string[] = []
138
+ if (effectiveStep > 0) {
139
+ for (let i = fromIdx; i <= toIdx; i += effectiveStep) result.push(list[i])
140
+ } else {
141
+ for (let i = fromIdx; i >= toIdx; i += effectiveStep) result.push(list[i])
142
+ }
143
+ return result
144
+ }
145
+
146
+ /**
147
+ * Migrates legacy `item:N` (0-based) outputMode to structured format.
148
+ *
149
+ * Legacy: `{ outputMode: "item:0" }` — 0-based index baked into mode string
150
+ * New: `{ outputMode: "item", itemIndex: "1" }` — 1-based expression
151
+ *
152
+ * Returns the data unchanged if not a legacy item mode.
153
+ */
154
+ export function migrateEdgeOutputMode(
155
+ data: Record<string, unknown> | undefined,
156
+ ): Record<string, unknown> | undefined {
157
+ if (!data) return data
158
+
159
+ const mode = data.outputMode as string | undefined
160
+ if (!mode || !mode.startsWith("item:")) return data
161
+
162
+ const idx = parseInt(mode.split(":")[1], 10)
163
+ return {
164
+ ...data,
165
+ outputMode: "item",
166
+ itemIndex: String(isNaN(idx) ? 1 : idx + 1),
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Builds the edge range label shown as a pill on the edge.
172
+ * Examples:
173
+ * - "2..last-1 +2" — range with step 2
174
+ * - "last..1 -1" — reversed
175
+ * - "3" — item mode, index 3
176
+ * - undefined — "last" mode has no label
177
+ */
178
+ export function buildRangeLabel(
179
+ mode: string,
180
+ rangeFrom?: string,
181
+ rangeTo?: string,
182
+ rangeStep?: number,
183
+ itemIndex?: string,
184
+ selectorMode?: SelectorMode,
185
+ listExpression?: string,
186
+ ): string | undefined {
187
+ return buildItemLabel(mode, rangeFrom, rangeTo, rangeStep, itemIndex, selectorMode, listExpression)
188
+ }
189
+
190
+ /** Internal: builds the item-side label (range / list / item / each). Mode "last" returns undefined. */
191
+ function buildItemLabel(
192
+ mode: string,
193
+ rangeFrom?: string,
194
+ rangeTo?: string,
195
+ rangeStep?: number,
196
+ itemIndex?: string,
197
+ selectorMode?: SelectorMode,
198
+ listExpression?: string,
199
+ ): string | undefined {
200
+ if (mode === "last") return undefined
201
+
202
+ if ((mode === "each" || mode === "all") && selectorMode === "list") {
203
+ const trimmed = (listExpression ?? "").trim()
204
+ if (trimmed === "") return undefined
205
+ return truncateLabel(trimmed, 18)
206
+ }
207
+
208
+ if (mode === "item") return itemIndex || undefined
209
+
210
+ const from = rangeFrom ?? "1"
211
+ const to = rangeTo ?? "last"
212
+ const step = rangeStep ?? 1
213
+ const isDefaultRange = from === "1" && to === "last"
214
+ const isDefaultStep = step === 1
215
+ if (isDefaultRange && isDefaultStep) return undefined
216
+
217
+ let label = `${from}..${to}`
218
+ if ((mode === "each" || mode === "all") && !isDefaultStep) {
219
+ label += step > 0 ? ` +${step}` : ` ${step}`
220
+ }
221
+ return label
222
+ }
223
+
224
+ function truncateLabel(s: string, maxLen: number): string {
225
+ if (s.length <= maxLen) return s
226
+ return s.slice(0, maxLen) + "…"
227
+ }
228
+
229
+ /**
230
+ * Validates a list expression without resolving indices.
231
+ * Used for live UI validation in the List tab input.
232
+ *
233
+ * Returns { ok: true } for empty/whitespace input — matches the
234
+ * "empty = all items" runtime semantics.
235
+ */
236
+ export function parseListExpression(
237
+ expr: string,
238
+ ): { ok: true } | { ok: false; error: string } {
239
+ const result = parseListTerms(expr.trim())
240
+ return result.ok ? { ok: true } : { ok: false, error: result.error }
241
+ }
242
+
243
+ /** Internal: validates a single index token ("1", "last", "last-3"). */
244
+ function isValidIndexToken(token: string): boolean {
245
+ if (token === "last") return true
246
+ if (/^[1-9]\d*$/.test(token)) return true
247
+ const m = /^last-(\d+)$/.exec(token)
248
+ if (m) return !isNaN(parseInt(m[1], 10))
249
+ return false
250
+ }
251
+
252
+ /**
253
+ * Parses a list expression and returns 0-based indices into the list,
254
+ * in user-specified order. Preserves duplicates.
255
+ *
256
+ * Supported syntax:
257
+ * <expr> := <term> ("," <term>)*
258
+ * <term> := <index> | <index> ".." <index> (":" <step>)?
259
+ * <index> := <number> | "last" | "last-" <number>
260
+ * <step> := integer (0 coerced to 1)
261
+ *
262
+ * Clamps out-of-bounds indices. Malformed input logs a warning and
263
+ * returns [0..listLength-1] (matches applyRange leniency).
264
+ */
265
+ export function resolveListExpression(expr: string, listLength: number): number[] {
266
+ if (listLength <= 0) return []
267
+ const result = parseListTerms(expr.trim())
268
+ if (!result.ok) {
269
+ console.warn("[edge-range] invalid list expression:", expr, "—", result.error)
270
+ return indexRange(0, listLength - 1, 1)
271
+ }
272
+ if (result.terms.length === 0) return indexRange(0, listLength - 1, 1)
273
+
274
+ const out: number[] = []
275
+ for (const term of result.terms) {
276
+ if (term.kind === "index") {
277
+ out.push(resolveIndex(term.token, listLength))
278
+ } else {
279
+ const from = resolveIndex(term.from, listLength)
280
+ const to = resolveIndex(term.to, listLength)
281
+ for (const idx of indexRange(from, to, term.step)) out.push(idx)
282
+ }
283
+ }
284
+ return out
285
+ }
286
+
287
+ /** Internal: inclusive index range with step. Returns [] if direction mismatches. */
288
+ function indexRange(from: number, to: number, step: number): number[] {
289
+ const out: number[] = []
290
+ if (step > 0) {
291
+ if (from > to) return []
292
+ for (let i = from; i <= to; i += step) out.push(i)
293
+ } else {
294
+ if (from < to) return []
295
+ for (let i = from; i >= to; i += step) out.push(i)
296
+ }
297
+ return out
298
+ }
299
+
300
+ /**
301
+ * Dispatches to either applyRange() or resolveListExpression() based on
302
+ * edgeData.selectorMode. Returns a new string[] containing the selected
303
+ * subset of items — or `items` unchanged when the edge has no filter.
304
+ */
305
+ export function selectListItems(
306
+ items: string[],
307
+ edgeData: SelectorFields | undefined,
308
+ ): string[] {
309
+ if (items.length === 0) return []
310
+ if (isDefaultSelectorConfig(edgeData)) return items
311
+ if (edgeData?.selectorMode === "list") {
312
+ const indices = resolveListExpression(edgeData.listExpression ?? "", items.length)
313
+ return indices.map((i) => items[i])
314
+ }
315
+ return applyRange(items, edgeData?.rangeFrom, edgeData?.rangeTo, edgeData?.rangeStep)
316
+ }
317
+
318
+ /**
319
+ * Returns true when edge data carries no active filter — empty/whitespace list
320
+ * expression in list mode, or defaults (1..last, step 1) in range mode.
321
+ * Callers can skip filtering entirely when this returns true.
322
+ */
323
+ export function isDefaultSelectorConfig(edgeData: SelectorFields | undefined): boolean {
324
+ if (!edgeData) return true
325
+ if (edgeData.selectorMode === "list") {
326
+ return ((edgeData.listExpression ?? "").trim()) === ""
327
+ }
328
+ const { from, to, step } = canonicalRange(edgeData)
329
+ return from === "1" && to === "last" && step === 1
330
+ }
331
+
332
+ export function describeEdgeBehavior(
333
+ edgeData:
334
+ | {
335
+ outputMode?: string
336
+ selectorMode?: SelectorMode
337
+ listExpression?: string
338
+ rangeFrom?: string
339
+ rangeTo?: string
340
+ rangeStep?: number
341
+ itemIndex?: string
342
+ }
343
+ | undefined,
344
+ ): string {
345
+ const mode = edgeData?.outputMode
346
+ return buildBaseSentence(edgeData, mode)
347
+ }
348
+
349
+ function buildBaseSentence(
350
+ edgeData: Parameters<typeof describeEdgeBehavior>[0],
351
+ mode: string | undefined,
352
+ ): string {
353
+ if (mode === "last") return "Passes the selected result."
354
+ if (mode === "item") return buildItemSentence(edgeData)
355
+ if (mode === "each") return buildEachSentence(edgeData)
356
+ if (mode === "all") return buildAllSentence(edgeData)
357
+ return "Passes all items at once."
358
+ }
359
+
360
+ function buildItemSentence(
361
+ edgeData: Parameters<typeof describeEdgeBehavior>[0],
362
+ ): string {
363
+ const canonical = canonicalItemIndex(edgeData?.itemIndex)
364
+ return `Passes only ${itemModeForm(canonical)}.`
365
+ }
366
+
367
+ function buildEachSentence(
368
+ edgeData: Parameters<typeof describeEdgeBehavior>[0],
369
+ ): string {
370
+ if (isDefaultSelectorConfig(edgeData)) return "Runs the next node one by one, per item."
371
+ if (edgeData?.selectorMode === "list") {
372
+ const result = parseListTerms((edgeData.listExpression ?? "").trim())
373
+ if (!result.ok) return "Runs the next node one by one, per item."
374
+ }
375
+ const phrase = buildSelectionPhrase(edgeData)
376
+ if (phrase.kind === "empty-result") {
377
+ return "Nothing selected — the next node won't run."
378
+ }
379
+ if (phrase.kind === "single-index") {
380
+ return `Runs the next node only on ${phrase.itemModeForm}.`
381
+ }
382
+ return `Runs the next node once for ${phrase.text}.`
383
+ }
384
+
385
+ function buildAllSentence(
386
+ edgeData: Parameters<typeof describeEdgeBehavior>[0],
387
+ ): string {
388
+ if (isDefaultSelectorConfig(edgeData)) return "Passes all items at once."
389
+ if (edgeData?.selectorMode === "list") {
390
+ const result = parseListTerms((edgeData.listExpression ?? "").trim())
391
+ if (!result.ok) return "Passes all items at once."
392
+ }
393
+ const phrase = buildSelectionPhrase(edgeData)
394
+ if (phrase.kind === "empty-result") {
395
+ return "Nothing selected — the next node will get an empty bundle."
396
+ }
397
+ if (phrase.kind === "single-index") {
398
+ return `Passes only ${phrase.itemModeForm}.`
399
+ }
400
+ return `Passes ${phrase.text} at once.`
401
+ }
402
+
403
+ type CanonicalRange = { from: string; to: string; step: number }
404
+
405
+ function canonicalItemIndex(raw: string | undefined): string {
406
+ const trimmed = (raw ?? "").trim()
407
+ if (trimmed === "") return "1"
408
+ if (!isValidIndexToken(trimmed)) return "1"
409
+ return trimmed
410
+ }
411
+
412
+ function canonicalRange(
413
+ edgeData: Parameters<typeof describeEdgeBehavior>[0],
414
+ ): CanonicalRange {
415
+ const from = ((edgeData?.rangeFrom ?? "").trim() || "1")
416
+ const to = ((edgeData?.rangeTo ?? "").trim() || "last")
417
+ const rawStep = edgeData?.rangeStep
418
+ const step = rawStep === undefined || rawStep === 0 ? 1 : rawStep
419
+ return { from, to, step }
420
+ }
421
+
422
+
423
+ function itemModeForm(canonical: string): string {
424
+ if (canonical === "1") return "the first item"
425
+ if (/^\d+$/.test(canonical)) return `item ${canonical}`
426
+ if (canonical === "last" || canonical === "last-0") return "the last item"
427
+ const m = /^last-(\d+)$/.exec(canonical)
428
+ if (m) {
429
+ const n = parseInt(m[1], 10)
430
+ if (n === 1) return "the second-to-last item"
431
+ if (n === 2) return "the third-to-last item"
432
+ if (n === 3) return "the fourth-to-last item"
433
+ return `the ${ordinal(n + 1)}-from-last item`
434
+ }
435
+ return "the first item"
436
+ }
437
+
438
+ function compactForm(canonical: string): string {
439
+ if (/^\d+$/.test(canonical)) return canonical
440
+ if (canonical === "last" || canonical === "last-0") return "the last one"
441
+ const m = /^last-(\d+)$/.exec(canonical)
442
+ if (m) {
443
+ const n = parseInt(m[1], 10)
444
+ if (n === 1) return "the second-to-last"
445
+ if (n === 2) return "the third-to-last"
446
+ if (n === 3) return "the fourth-to-last"
447
+ return `the ${ordinal(n + 1)}-from-last`
448
+ }
449
+ return canonical
450
+ }
451
+
452
+ function ordinal(n: number): string {
453
+ const absN = Math.abs(n)
454
+ const lastTwo = absN % 100
455
+ if (lastTwo >= 11 && lastTwo <= 13) return `${n}th`
456
+ switch (absN % 10) {
457
+ case 1: return `${n}st`
458
+ case 2: return `${n}nd`
459
+ case 3: return `${n}rd`
460
+ default: return `${n}th`
461
+ }
462
+ }
463
+
464
+ type SelectionPhrase =
465
+ | { kind: "items"; text: string }
466
+ | { kind: "single-index"; itemModeForm: string }
467
+ | { kind: "empty-result" }
468
+
469
+ function buildSelectionPhrase(
470
+ edgeData: Parameters<typeof describeEdgeBehavior>[0],
471
+ ): SelectionPhrase {
472
+ if (edgeData?.selectorMode === "list") {
473
+ return buildListSelectionPhrase((edgeData.listExpression ?? "").trim())
474
+ }
475
+ return buildRangeSelectionPhrase(canonicalRange(edgeData))
476
+ }
477
+
478
+ function indexKind(token: string): "concrete" | "relative" | "invalid" {
479
+ if (/^[1-9]\d*$/.test(token)) return "concrete"
480
+ if (token === "last" || /^last-\d+$/.test(token)) return "relative"
481
+ return "invalid"
482
+ }
483
+
484
+ function relativeOffset(token: string): number {
485
+ if (token === "last") return 0
486
+ const m = /^last-(\d+)$/.exec(token)
487
+ return m ? parseInt(m[1], 10) : 0
488
+ }
489
+
490
+ function compareTokens(a: string, b: string): -1 | 0 | 1 | "indeterminate" {
491
+ const ka = indexKind(a)
492
+ const kb = indexKind(b)
493
+ if (ka !== kb) return "indeterminate"
494
+ if (ka === "concrete") {
495
+ const na = parseInt(a, 10)
496
+ const nb = parseInt(b, 10)
497
+ return na < nb ? -1 : na > nb ? 1 : 0
498
+ }
499
+ if (ka === "relative") {
500
+ const oa = relativeOffset(a)
501
+ const ob = relativeOffset(b)
502
+ return oa < ob ? 1 : oa > ob ? -1 : 0
503
+ }
504
+ return "indeterminate"
505
+ }
506
+
507
+ function buildRangeSelectionPhrase(r: CanonicalRange): SelectionPhrase {
508
+ const { from, to, step } = r
509
+
510
+ const cmp = compareTokens(from, to)
511
+ if (cmp !== "indeterminate") {
512
+ if (step > 0 && cmp === 1) return { kind: "empty-result" }
513
+ if (step < 0 && cmp === -1) return { kind: "empty-result" }
514
+ }
515
+
516
+ if (cmp === 0) {
517
+ return { kind: "single-index", itemModeForm: itemModeForm(from) }
518
+ }
519
+
520
+ const absStep = Math.abs(step)
521
+ if (step === -1 && from === "last" && to === "1") {
522
+ return { kind: "items", text: "all items in reverse order" }
523
+ }
524
+
525
+ let text = `items ${compactForm(from)} through ${compactForm(to)}`
526
+ if (absStep === 1 && step < 0) {
527
+ text += " in reverse order"
528
+ } else if (absStep > 1 && step > 0) {
529
+ text += ` (every ${ordinal(absStep)} item)`
530
+ } else if (absStep > 1 && step < 0) {
531
+ text += ` (every ${ordinal(absStep)} item, in reverse)`
532
+ }
533
+ return { kind: "items", text }
534
+ }
535
+
536
+ type ListTerm =
537
+ | { kind: "index"; token: string }
538
+ | { kind: "range"; from: string; to: string; step: number }
539
+
540
+ type ParseResult =
541
+ | { ok: true; terms: ListTerm[] }
542
+ | { ok: false; error: string }
543
+
544
+ /**
545
+ * Core list-expression parser. Returns structured terms on success or a
546
+ * specific error message on failure. Empty input → zero terms (not an error).
547
+ * Error messages are surfaced unchanged in the edge-config tooltip, so they
548
+ * must stay stable:
549
+ * - "Empty item between commas"
550
+ * - "Invalid index: <token>"
551
+ * - "Range missing endpoint: <term>"
552
+ * - "Step must be an integer"
553
+ */
554
+ function parseListTerms(expr: string): ParseResult {
555
+ if (expr === "") return { ok: true, terms: [] }
556
+ const terms: ListTerm[] = []
557
+ const raw = expr.split(",")
558
+ for (const rawTerm of raw) {
559
+ const term = rawTerm.trim()
560
+ if (term === "") return { ok: false, error: "Empty item between commas" }
561
+ const rangeIdx = term.indexOf("..")
562
+ if (rangeIdx === -1) {
563
+ if (!isValidIndexToken(term)) return { ok: false, error: `Invalid index: ${term}` }
564
+ terms.push({ kind: "index", token: term })
565
+ continue
566
+ }
567
+ const left = term.slice(0, rangeIdx).trim()
568
+ const rightWithStep = term.slice(rangeIdx + 2).trim()
569
+ if (left === "" || rightWithStep === "") {
570
+ return { ok: false, error: `Range missing endpoint: ${term}` }
571
+ }
572
+ const colonIdx = rightWithStep.indexOf(":")
573
+ const right = colonIdx === -1 ? rightWithStep : rightWithStep.slice(0, colonIdx).trim()
574
+ const stepStr = colonIdx === -1 ? null : rightWithStep.slice(colonIdx + 1).trim()
575
+ if (right === "") return { ok: false, error: `Range missing endpoint: ${term}` }
576
+ if (!isValidIndexToken(left)) return { ok: false, error: `Invalid index: ${left}` }
577
+ if (!isValidIndexToken(right)) return { ok: false, error: `Invalid index: ${right}` }
578
+ let step = 1
579
+ if (stepStr !== null) {
580
+ if (!/^-?\d+$/.test(stepStr)) return { ok: false, error: "Step must be an integer" }
581
+ const parsed = parseInt(stepStr, 10)
582
+ step = parsed === 0 ? 1 : parsed
583
+ }
584
+ terms.push({ kind: "range", from: left, to: right, step })
585
+ }
586
+ return { ok: true, terms }
587
+ }
588
+
589
+ function buildListSelectionPhrase(expr: string): SelectionPhrase {
590
+ const result = parseListTerms(expr)
591
+ if (!result.ok) return { kind: "empty-result" }
592
+ if (result.terms.length === 0) return { kind: "items", text: "items" } // unreachable via callers; default-config catches empty
593
+
594
+ const normalized = result.terms.map<ListTerm>((t) => {
595
+ if (t.kind === "range") {
596
+ const cmp = compareTokens(t.from, t.to)
597
+ if (cmp === 0) return { kind: "index", token: t.from }
598
+ }
599
+ return t
600
+ })
601
+
602
+ if (normalized.length === 1 && normalized[0].kind === "index") {
603
+ return { kind: "single-index", itemModeForm: itemModeForm(normalized[0].token) }
604
+ }
605
+
606
+ const rendered = normalized.map((t) => {
607
+ if (t.kind === "index") return compactForm(t.token)
608
+ let text = `${compactForm(t.from)} through ${compactForm(t.to)}`
609
+ const absStep = Math.abs(t.step)
610
+ if (absStep === 1 && t.step < 0) {
611
+ if (t.from === "last" && t.to === "1") return "all items in reverse"
612
+ text += " in reverse"
613
+ } else if (absStep > 1 && t.step > 0) {
614
+ text += ` (every ${ordinal(absStep)})`
615
+ } else if (absStep > 1 && t.step < 0) {
616
+ text += ` (every ${ordinal(absStep)}, in reverse)`
617
+ }
618
+ return text
619
+ })
620
+
621
+ let joined: string
622
+ if (rendered.length === 1) {
623
+ joined = rendered[0]
624
+ } else if (rendered.length === 2) {
625
+ joined = `${rendered[0]} and ${rendered[1]}`
626
+ } else {
627
+ const head = rendered.slice(0, -1).join(", ")
628
+ joined = `${head}, and ${rendered[rendered.length - 1]}`
629
+ }
630
+ return { kind: "items", text: `items ${joined}` }
631
+ }
632
+
633
+ /**
634
+ * Run the selector against `items`. Returns the picked subset plus the
635
+ * complement (`rest`). For modes that pick by index, `rest` is computed by
636
+ * index exclusion (not value) so duplicate items are handled correctly.
637
+ */
638
+ export function runSelector(items: string[], config: SelectorConfig): SelectorResult {
639
+ if (items.length === 0) return { picked: [], rest: [] }
640
+
641
+ switch (config.mode) {
642
+ case "item": {
643
+ const idx = resolveIndex(config.itemIndex ?? "1", items.length, "1")
644
+ return splitByIndices(items, [idx])
645
+ }
646
+ case "range": {
647
+ const indices = applyRangeIndices(items.length, config.rangeFrom, config.rangeTo, config.rangeStep)
648
+ return splitByIndices(items, indices)
649
+ }
650
+ case "list": {
651
+ const indices = resolveListExpression(config.listExpression ?? "", items.length)
652
+ return splitByIndices(items, indices)
653
+ }
654
+ case "random":
655
+ return selectRandom(items, config.randomCount ?? 1, config.seed)
656
+ case "modulo":
657
+ return selectByModulo(items, config.moduloDivisor)
658
+ case "predicate":
659
+ return selectByPredicate(items, config)
660
+ case "named-key":
661
+ return selectByNamedKey(items, config.namedKeyField, config.namedKeyValue)
662
+ default:
663
+ return { picked: [], rest: items.slice() }
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Partition `items` into picked (indices ∈ pickedIndices, in their original
669
+ * order in the source list) and rest (everything else). Deduplicates
670
+ * `pickedIndices` so passing the same index twice doesn't double-count.
671
+ */
672
+ function splitByIndices(items: string[], pickedIndices: number[]): SelectorResult {
673
+ const pickedSet = new Set(pickedIndices.filter((i) => i >= 0 && i < items.length))
674
+ const picked: string[] = []
675
+ const rest: string[] = []
676
+ for (let i = 0; i < items.length; i++) {
677
+ if (pickedSet.has(i)) picked.push(items[i])
678
+ else rest.push(items[i])
679
+ }
680
+ return { picked, rest }
681
+ }
682
+
683
+ /**
684
+ * Returns the indices that `applyRange` would pick. Extracted so `runSelector`
685
+ * range mode can compute the exact picked-index set (`items.indexOf(value)`
686
+ * would return the wrong duplicate when the list contains repeated values).
687
+ */
688
+ export function applyRangeIndices(
689
+ length: number,
690
+ from?: string,
691
+ to?: string,
692
+ step?: number,
693
+ ): number[] {
694
+ if (length === 0) return []
695
+ const fromIdx = resolveIndex(from ?? "1", length, "1")
696
+ const toIdx = resolveIndex(to ?? "last", length, "last")
697
+ const effectiveStep = step === 0 || step === undefined ? 1 : step
698
+ if (effectiveStep > 0 && fromIdx > toIdx) return []
699
+ if (effectiveStep < 0 && fromIdx < toIdx) return []
700
+ const result: number[] = []
701
+ if (effectiveStep > 0) {
702
+ for (let i = fromIdx; i <= toIdx; i += effectiveStep) result.push(i)
703
+ } else {
704
+ for (let i = fromIdx; i >= toIdx; i += effectiveStep) result.push(i)
705
+ }
706
+ return result
707
+ }
708
+
709
+ /** mulberry32 PRNG — deterministic given a seed (any 32-bit signed integer). */
710
+ function mulberry32(seed: number): () => number {
711
+ let a = seed | 0
712
+ return () => {
713
+ a = (a + 0x6D2B79F5) | 0
714
+ let t = a
715
+ t = Math.imul(t ^ (t >>> 15), t | 1)
716
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
717
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
718
+ }
719
+ }
720
+
721
+ /** Hash a seed string to a 32-bit integer (FNV-1a variant). */
722
+ function hashSeed(s: string): number {
723
+ let h = 2166136261 | 0
724
+ for (let i = 0; i < s.length; i++) {
725
+ h ^= s.charCodeAt(i)
726
+ h = Math.imul(h, 16777619)
727
+ }
728
+ return h
729
+ }
730
+
731
+ /**
732
+ * Pick `count` items uniformly at random without replacement.
733
+ * If `seed` is non-empty, the PRNG is seeded deterministically.
734
+ * Otherwise `Math.random` is used (fresh randomness per call).
735
+ */
736
+ export function selectRandom(items: string[], count: number, seed?: string): SelectorResult {
737
+ if (items.length === 0) return { picked: [], rest: [] }
738
+ const k = Math.max(0, Math.min(count, items.length))
739
+ const rand = seed && seed.length > 0 ? mulberry32(hashSeed(seed)) : Math.random
740
+ // Partial Fisher-Yates: shuffle the first k positions, pick those indices.
741
+ const indices = items.map((_, i) => i)
742
+ for (let i = 0; i < k; i++) {
743
+ const j = i + Math.floor(rand() * (indices.length - i))
744
+ ;[indices[i], indices[j]] = [indices[j], indices[i]]
745
+ }
746
+ return splitByIndices(items, indices.slice(0, k))
747
+ }
748
+
749
+ /**
750
+ * Pick a single item at `divisor % items.length`. Non-numeric `divisor`
751
+ * falls back to index 0. Caller is responsible for resolving any
752
+ * `{NodeLabel}` template refs in `divisor` BEFORE calling this — see
753
+ * `resolveSelectorRefs`.
754
+ */
755
+ export function selectByModulo(items: string[], divisor: string | undefined): SelectorResult {
756
+ if (items.length === 0) return { picked: [], rest: [] }
757
+ const n = divisor ? Number.parseInt(divisor.trim(), 10) : NaN
758
+ const safeN = Number.isFinite(n) ? n : 0
759
+ const idx = ((safeN % items.length) + items.length) % items.length
760
+ return splitByIndices(items, [idx])
761
+ }
762
+
763
+ /**
764
+ * Pick items whose parsed JSON satisfies the predicate. `match: "first"`
765
+ * (default) stops at the first hit; `"all"` collects every hit.
766
+ *
767
+ * Reuses the filter-list condition evaluator so operator semantics
768
+ * (numeric coercion on `=`/`!=`, locale-aware ordering, case-sensitive
769
+ * text ops) stay identical to the Filter List node.
770
+ */
771
+ export function selectByPredicate(
772
+ items: string[],
773
+ config: SelectorConfig,
774
+ ): SelectorResult {
775
+ if (items.length === 0) return { picked: [], rest: [] }
776
+ // Default operator to "=" so the UI's visual default (selector-config.tsx
777
+ // shows `op={config.predicateOp ?? "="}`) matches runtime; first-time
778
+ // predicate use without explicitly setting an operator used to silently
779
+ // return picked=[].
780
+ // predicateField is intentionally allowed to be empty/undefined —
781
+ // evaluateCondition treats an empty path as "compare against the whole
782
+ // item" (filter-condition.ts:161), matching the UI placeholder hint
783
+ // "blank = whole item" on the field input.
784
+ const op = config.predicateOp ?? "="
785
+ // Pass caseSensitive explicitly. evaluateCondition defaults undefined to
786
+ // `true` (case-sensitive) to preserve legacy callers, but the selector
787
+ // UI checkbox shows unchecked by default — so undefined config must map
788
+ // to case-INsensitive at runtime to match what the user sees.
789
+ const opts = { caseSensitive: config.predicateCaseSensitive ?? false }
790
+ const matchAll = config.predicateMatch === "all"
791
+ const cond = {
792
+ id: "selector-predicate",
793
+ field: config.predicateField ?? "",
794
+ operator: op,
795
+ value: config.predicateValue ?? "",
796
+ valueType: "static" as const,
797
+ }
798
+ const matchedIndices: number[] = []
799
+ for (let i = 0; i < items.length; i++) {
800
+ const parsed = tryParseJson(items[i])
801
+ const hit = evaluateCondition(parsed, items[i], cond, undefined, opts)
802
+ if (hit) {
803
+ matchedIndices.push(i)
804
+ if (!matchAll) break
805
+ }
806
+ }
807
+ return splitByIndices(items, matchedIndices)
808
+ }
809
+
810
+ /**
811
+ * Shortcut for `predicate(op="=", match="first")` — pick the first item
812
+ * whose `field` strictly equals `value`. Implemented as a thin wrapper
813
+ * so the UI can offer a 2-field form ("look up by name") instead of the
814
+ * full 4-field predicate form.
815
+ */
816
+ export function selectByNamedKey(
817
+ items: string[],
818
+ field: string | undefined,
819
+ value: string | undefined,
820
+ ): SelectorResult {
821
+ return selectByPredicate(items, {
822
+ mode: "predicate",
823
+ predicateField: field,
824
+ predicateOp: "=",
825
+ predicateValue: value,
826
+ predicateMatch: "first",
827
+ })
828
+ }
829
+
830
+ /**
831
+ * Returns a new SelectorConfig with `{NodeLabel}` refs and `{{trigger.*}}` /
832
+ * `{{now}}` / `{{last_N_*:N}}` template tokens resolved in the four
833
+ * template-aware fields: moduloDivisor, predicateValue, namedKeyValue, seed.
834
+ * All other fields are passed through unchanged. Callers must build
835
+ * `variables` via `buildConditionVariables` (or equivalent) BEFORE calling.
836
+ *
837
+ * Other selector fields (itemIndex, range/list expressions, randomCount, etc.)
838
+ * are treated as literals — they have their own selector-lib syntax and are
839
+ * not resolved against the variable space.
840
+ *
841
+ * Token resolution matches `resolveConditionValue` exactly (both reuse
842
+ * `substituteTriggerTokens`) so the selector's `{{trigger.foo}}` semantics
843
+ * stay in lockstep with filter-list / router conditions.
844
+ *
845
+ * Pure: returns a new object; the input config is not mutated.
846
+ */
847
+ export function resolveSelectorRefs(
848
+ config: SelectorConfig,
849
+ triggerData: Record<string, unknown> | undefined,
850
+ variables: ReadonlyMap<string, string>,
851
+ ): SelectorConfig {
852
+ const resolve = (v: string | undefined): string | undefined => {
853
+ if (v == null) return v
854
+ const refsResolved = resolveNodeRefs(v, variables)
855
+ return substituteTriggerTokens(refsResolved, triggerData)
856
+ }
857
+ return {
858
+ ...config,
859
+ moduloDivisor: resolve(config.moduloDivisor),
860
+ predicateValue: resolve(config.predicateValue),
861
+ namedKeyValue: resolve(config.namedKeyValue),
862
+ seed: resolve(config.seed),
863
+ }
864
+ }