@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,1151 @@
1
+ import { vi } from "vitest"
2
+ import {
3
+ resolveIndex,
4
+ applyRange,
5
+ migrateEdgeOutputMode,
6
+ buildRangeLabel,
7
+ } from "../selector.js"
8
+ import { parseListExpression } from "../selector"
9
+ import { resolveListExpression } from "../selector"
10
+ import { selectListItems } from "../selector"
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // resolveIndex
14
+ // ---------------------------------------------------------------------------
15
+ describe("resolveIndex", () => {
16
+ it('resolves "1" to 0-based index 0', () => {
17
+ expect(resolveIndex("1", 5)).toBe(0)
18
+ })
19
+
20
+ it('resolves "3" to 0-based index 2', () => {
21
+ expect(resolveIndex("3", 5)).toBe(2)
22
+ })
23
+
24
+ it('resolves "last" to the final index', () => {
25
+ expect(resolveIndex("last", 5)).toBe(4)
26
+ })
27
+
28
+ it('resolves "last-1" to second-to-last index', () => {
29
+ expect(resolveIndex("last-1", 5)).toBe(3)
30
+ })
31
+
32
+ it('resolves "last-2" relative to a short list', () => {
33
+ expect(resolveIndex("last-2", 3)).toBe(0)
34
+ })
35
+
36
+ it("clamps out-of-bounds high values", () => {
37
+ expect(resolveIndex("10", 3)).toBe(2)
38
+ })
39
+
40
+ it("clamps out-of-bounds low values from last-N", () => {
41
+ expect(resolveIndex("last-5", 3)).toBe(0)
42
+ })
43
+
44
+ it("returns 0 for an empty list", () => {
45
+ expect(resolveIndex("1", 0)).toBe(0)
46
+ expect(resolveIndex("last", 0)).toBe(0)
47
+ })
48
+
49
+ it('falls back to default on malformed expr "abc"', () => {
50
+ expect(resolveIndex("abc", 5)).toBe(0)
51
+ })
52
+
53
+ it('falls back to default on "last-" (missing offset)', () => {
54
+ expect(resolveIndex("last-", 5)).toBe(0)
55
+ })
56
+
57
+ it("trims whitespace before parsing", () => {
58
+ expect(resolveIndex(" 2 ", 5)).toBe(1)
59
+ })
60
+ })
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // applyRange
64
+ // ---------------------------------------------------------------------------
65
+ describe("applyRange", () => {
66
+ const items = ["a", "b", "c", "d", "e"]
67
+
68
+ it("returns all items with default params", () => {
69
+ expect(applyRange(items)).toEqual(["a", "b", "c", "d", "e"])
70
+ })
71
+
72
+ it("slices from=2 to=4", () => {
73
+ expect(applyRange(items, "2", "4")).toEqual(["b", "c", "d"])
74
+ })
75
+
76
+ it("selects a single item with from=last to=last", () => {
77
+ expect(applyRange(items, "last", "last")).toEqual(["e"])
78
+ })
79
+
80
+ it("applies a positive step of 2", () => {
81
+ expect(applyRange(items, "1", "last", 2)).toEqual(["a", "c", "e"])
82
+ })
83
+
84
+ it("reverses with step=-1", () => {
85
+ expect(applyRange(items, "last", "1", -1)).toEqual([
86
+ "e",
87
+ "d",
88
+ "c",
89
+ "b",
90
+ "a",
91
+ ])
92
+ })
93
+
94
+ it("reverses with step=-2", () => {
95
+ expect(applyRange(items, "5", "1", -2)).toEqual(["e", "c", "a"])
96
+ })
97
+
98
+ it("returns empty on direction mismatch (forward range, negative step)", () => {
99
+ expect(applyRange(items, "1", "5", -1)).toEqual([])
100
+ })
101
+
102
+ it("returns empty on direction mismatch (reverse range, positive step)", () => {
103
+ expect(applyRange(items, "5", "1", 1)).toEqual([])
104
+ })
105
+
106
+ it("treats step=0 as step=1", () => {
107
+ expect(applyRange(items, "1", "last", 0)).toEqual([
108
+ "a",
109
+ "b",
110
+ "c",
111
+ "d",
112
+ "e",
113
+ ])
114
+ })
115
+
116
+ it("returns empty array for empty list", () => {
117
+ expect(applyRange([])).toEqual([])
118
+ })
119
+ })
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // migrateEdgeOutputMode
123
+ // ---------------------------------------------------------------------------
124
+ describe("migrateEdgeOutputMode", () => {
125
+ it("returns undefined for undefined input", () => {
126
+ expect(migrateEdgeOutputMode(undefined)).toBeUndefined()
127
+ })
128
+
129
+ it("returns data unchanged when no outputMode", () => {
130
+ const data = { foo: "bar" }
131
+ expect(migrateEdgeOutputMode(data)).toEqual({ foo: "bar" })
132
+ })
133
+
134
+ it('returns data unchanged when outputMode is "list"', () => {
135
+ const data = { outputMode: "list" }
136
+ expect(migrateEdgeOutputMode(data)).toEqual({ outputMode: "list" })
137
+ })
138
+
139
+ it('migrates "item:0" to 1-based itemIndex "1"', () => {
140
+ const result = migrateEdgeOutputMode({ outputMode: "item:0" })
141
+ expect(result).toEqual({ outputMode: "item", itemIndex: "1" })
142
+ })
143
+
144
+ it('migrates "item:2" to 1-based itemIndex "3"', () => {
145
+ const result = migrateEdgeOutputMode({ outputMode: "item:2" })
146
+ expect(result).toEqual({ outputMode: "item", itemIndex: "3" })
147
+ })
148
+
149
+ it('falls back to itemIndex "1" for "item:abc" (NaN)', () => {
150
+ const result = migrateEdgeOutputMode({ outputMode: "item:abc" })
151
+ expect(result).toEqual({ outputMode: "item", itemIndex: "1" })
152
+ })
153
+
154
+ it("preserves other data fields during migration", () => {
155
+ const data = { outputMode: "item:0", color: "red", count: 42 }
156
+ const result = migrateEdgeOutputMode(data)
157
+ expect(result).toEqual({
158
+ outputMode: "item",
159
+ itemIndex: "1",
160
+ color: "red",
161
+ count: 42,
162
+ })
163
+ })
164
+ })
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // buildRangeLabel
168
+ // ---------------------------------------------------------------------------
169
+ describe("buildRangeLabel", () => {
170
+ it('returns undefined for mode "last"', () => {
171
+ expect(buildRangeLabel("last")).toBeUndefined()
172
+ })
173
+
174
+ it('returns the itemIndex for mode "item"', () => {
175
+ expect(buildRangeLabel("item", undefined, undefined, undefined, "3")).toBe(
176
+ "3",
177
+ )
178
+ })
179
+
180
+ it('returns undefined for mode "item" without itemIndex', () => {
181
+ expect(buildRangeLabel("item")).toBeUndefined()
182
+ })
183
+
184
+ it('returns undefined for mode "all" with default range and step', () => {
185
+ expect(buildRangeLabel("all", "1", "last", 1)).toBeUndefined()
186
+ })
187
+
188
+ it('builds label for mode "all" with non-default from', () => {
189
+ expect(buildRangeLabel("all", "2")).toBe("2..last")
190
+ })
191
+
192
+ it('includes step suffix for mode "each" with step 2', () => {
193
+ expect(buildRangeLabel("each", undefined, undefined, 2)).toBe(
194
+ "1..last +2",
195
+ )
196
+ })
197
+
198
+ it('includes negative step suffix for mode "each" with step -1', () => {
199
+ expect(buildRangeLabel("each", undefined, undefined, -1)).toBe(
200
+ "1..last -1",
201
+ )
202
+ })
203
+
204
+ it('builds label for mode "all" with from and to', () => {
205
+ expect(buildRangeLabel("all", "2", "4")).toBe("2..4")
206
+ })
207
+
208
+ it('includes step suffix for mode "all" with step 2', () => {
209
+ expect(buildRangeLabel("all", undefined, undefined, 2)).toBe("1..last +2")
210
+ })
211
+
212
+ it('includes negative step suffix for mode "all" with step -1', () => {
213
+ expect(buildRangeLabel("all", "last", "1", -1)).toBe("last..1 -1")
214
+ })
215
+ })
216
+
217
+ describe("buildRangeLabel — List mode support", () => {
218
+ it("returns raw list expression when non-empty (under 18 chars)", () => {
219
+ expect(
220
+ buildRangeLabel("each", undefined, undefined, undefined, undefined, "list", "1, 2, last"),
221
+ ).toBe("1, 2, last")
222
+ expect(
223
+ buildRangeLabel("all", undefined, undefined, undefined, undefined, "list", "1, 3..5, last"),
224
+ ).toBe("1, 3..5, last")
225
+ })
226
+
227
+ it("truncates long list expressions to 18 chars with ellipsis", () => {
228
+ const result = buildRangeLabel(
229
+ "each",
230
+ undefined,
231
+ undefined,
232
+ undefined,
233
+ undefined,
234
+ "list",
235
+ "1, 3..5, 7, 9..11, last",
236
+ )
237
+ expect(result).toMatch(/…$/)
238
+ expect(result!.length).toBeLessThanOrEqual(19)
239
+ })
240
+
241
+ it("returns undefined when list mode with empty expression", () => {
242
+ expect(
243
+ buildRangeLabel("each", undefined, undefined, undefined, undefined, "list", ""),
244
+ ).toBeUndefined()
245
+ expect(
246
+ buildRangeLabel("each", undefined, undefined, undefined, undefined, "list", " "),
247
+ ).toBeUndefined()
248
+ })
249
+
250
+ it("does NOT fall through to range logic when list mode with empty expression", () => {
251
+ expect(
252
+ buildRangeLabel("each", "2", "last-1", 2, undefined, "list", ""),
253
+ ).toBeUndefined()
254
+ })
255
+
256
+ it("preserves existing range-label behavior when selectorMode is range", () => {
257
+ expect(
258
+ buildRangeLabel("each", "2", "last-1", 2, undefined, "range", undefined),
259
+ ).toBe("2..last-1 +2")
260
+ })
261
+
262
+ it("ignores selectorMode in item mode", () => {
263
+ expect(
264
+ buildRangeLabel("item", undefined, undefined, undefined, "3", "list", "1, 2"),
265
+ ).toBe("3")
266
+ })
267
+
268
+ it("preserves existing range-label behavior when selectorMode is absent", () => {
269
+ expect(buildRangeLabel("each", "2", "last-1", 2, undefined)).toBe("2..last-1 +2")
270
+ expect(buildRangeLabel("each", "2", "last-1")).toBe("2..last-1")
271
+ })
272
+ })
273
+
274
+ describe("parseListExpression", () => {
275
+ it("accepts valid expressions", () => {
276
+ expect(parseListExpression("1")).toEqual({ ok: true })
277
+ expect(parseListExpression("1, 2, last")).toEqual({ ok: true })
278
+ expect(parseListExpression("1..5")).toEqual({ ok: true })
279
+ expect(parseListExpression("1..10:2")).toEqual({ ok: true })
280
+ expect(parseListExpression("1..5:0")).toEqual({ ok: true })
281
+ expect(parseListExpression("1, 3..5, last")).toEqual({ ok: true })
282
+ expect(parseListExpression("")).toEqual({ ok: true })
283
+ expect(parseListExpression(" ")).toEqual({ ok: true })
284
+ expect(parseListExpression("last..1:-1")).toEqual({ ok: true })
285
+ })
286
+
287
+ it("rejects empty tokens between commas", () => {
288
+ expect(parseListExpression("1,,2")).toEqual({
289
+ ok: false,
290
+ error: "Empty item between commas",
291
+ })
292
+ })
293
+
294
+ it("rejects range with missing endpoint", () => {
295
+ expect(parseListExpression("..5")).toEqual({
296
+ ok: false,
297
+ error: "Range missing endpoint: ..5",
298
+ })
299
+ expect(parseListExpression("1..")).toEqual({
300
+ ok: false,
301
+ error: "Range missing endpoint: 1..",
302
+ })
303
+ })
304
+
305
+ it("rejects non-integer step", () => {
306
+ expect(parseListExpression("1..5:1.5")).toEqual({
307
+ ok: false,
308
+ error: "Step must be an integer",
309
+ })
310
+ })
311
+
312
+ it("rejects invalid index tokens", () => {
313
+ expect(parseListExpression("garbage")).toEqual({
314
+ ok: false,
315
+ error: "Invalid index: garbage",
316
+ })
317
+ })
318
+ })
319
+
320
+ describe("resolveListExpression", () => {
321
+ it("resolves single-index terms", () => {
322
+ expect(resolveListExpression("1, 2, 3", 5)).toEqual([0, 1, 2])
323
+ expect(resolveListExpression("1, 2, last", 5)).toEqual([0, 1, 4])
324
+ expect(resolveListExpression("last, 1, last-1", 5)).toEqual([4, 0, 3])
325
+ })
326
+
327
+ it("resolves range terms", () => {
328
+ expect(resolveListExpression("1..5", 10)).toEqual([0, 1, 2, 3, 4])
329
+ expect(resolveListExpression("1..last", 3)).toEqual([0, 1, 2])
330
+ expect(resolveListExpression("1..last-1", 5)).toEqual([0, 1, 2, 3])
331
+ })
332
+
333
+ it("resolves range with step", () => {
334
+ expect(resolveListExpression("1..10:2", 10)).toEqual([0, 2, 4, 6, 8])
335
+ expect(resolveListExpression("last..1:-1", 3)).toEqual([2, 1, 0])
336
+ expect(resolveListExpression("1..5:0", 10)).toEqual([0, 1, 2, 3, 4])
337
+ })
338
+
339
+ it("returns empty for direction-mismatched range", () => {
340
+ expect(resolveListExpression("1..10:-1", 10)).toEqual([])
341
+ })
342
+
343
+ it("mixes list and range terms", () => {
344
+ expect(resolveListExpression("1, 3..5, last", 10)).toEqual([0, 2, 3, 4, 9])
345
+ })
346
+
347
+ it("treats empty/whitespace expression as all items", () => {
348
+ expect(resolveListExpression("", 5)).toEqual([0, 1, 2, 3, 4])
349
+ expect(resolveListExpression(" ", 5)).toEqual([0, 1, 2, 3, 4])
350
+ })
351
+
352
+ it("falls back to all items on malformed input", () => {
353
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
354
+ expect(resolveListExpression("1,,2", 5)).toEqual([0, 1, 2, 3, 4])
355
+ expect(resolveListExpression("1, ,2", 5)).toEqual([0, 1, 2, 3, 4])
356
+ expect(resolveListExpression("1..garbage", 5)).toEqual([0, 1, 2, 3, 4])
357
+ expect(warn).toHaveBeenCalled()
358
+ warn.mockRestore()
359
+ })
360
+
361
+ it("preserves duplicates and user order", () => {
362
+ expect(resolveListExpression("1, 1, 2", 5)).toEqual([0, 0, 1])
363
+ })
364
+
365
+ it("clamps out-of-bounds indices", () => {
366
+ expect(resolveListExpression("100, 200", 5)).toEqual([4, 4])
367
+ })
368
+
369
+ it("returns [] for empty list", () => {
370
+ expect(resolveListExpression("1, 2", 0)).toEqual([])
371
+ })
372
+ })
373
+
374
+ describe("selectListItems", () => {
375
+ const items = ["a", "b", "c", "d", "e"]
376
+
377
+ it("dispatches to resolveListExpression when selectorMode is list", () => {
378
+ expect(
379
+ selectListItems(items, { selectorMode: "list", listExpression: "1, last" }),
380
+ ).toEqual(["a", "e"])
381
+ expect(
382
+ selectListItems(items, { selectorMode: "list", listExpression: "1..3" }),
383
+ ).toEqual(["a", "b", "c"])
384
+ })
385
+
386
+ it("dispatches to applyRange when selectorMode is range", () => {
387
+ expect(
388
+ selectListItems(items, { selectorMode: "range", rangeFrom: "2", rangeTo: "4" }),
389
+ ).toEqual(["b", "c", "d"])
390
+ })
391
+
392
+ it("falls back to applyRange when selectorMode is absent", () => {
393
+ expect(selectListItems(items, { rangeFrom: "2", rangeTo: "4" })).toEqual(["b", "c", "d"])
394
+ expect(selectListItems(items, {})).toEqual(items)
395
+ expect(selectListItems(items, undefined)).toEqual(items)
396
+ })
397
+
398
+ it("ignores listExpression when selectorMode is not list", () => {
399
+ expect(
400
+ selectListItems(items, { selectorMode: "range", listExpression: "1, 2" }),
401
+ ).toEqual(items)
402
+ expect(
403
+ selectListItems(items, { listExpression: "1, 2" }),
404
+ ).toEqual(items)
405
+ })
406
+
407
+ it("returns [] for empty input list", () => {
408
+ expect(
409
+ selectListItems([], { selectorMode: "list", listExpression: "1, 2" }),
410
+ ).toEqual([])
411
+ })
412
+ })
413
+
414
+ import { describeEdgeBehavior } from "../selector"
415
+
416
+ describe("describeEdgeBehavior — basic modes", () => {
417
+ it("last mode", () => {
418
+ expect(describeEdgeBehavior({ outputMode: "last" })).toBe(
419
+ "Passes the selected result.",
420
+ )
421
+ })
422
+
423
+ it("each default config", () => {
424
+ expect(describeEdgeBehavior({ outputMode: "each" })).toBe(
425
+ "Runs the next node one by one, per item.",
426
+ )
427
+ })
428
+
429
+ it("all default config", () => {
430
+ expect(describeEdgeBehavior({ outputMode: "all" })).toBe(
431
+ "Passes all items at once.",
432
+ )
433
+ })
434
+
435
+ it("default config treats empty-string range fields as defaults", () => {
436
+ expect(
437
+ describeEdgeBehavior({ outputMode: "each", rangeFrom: "", rangeTo: "" }),
438
+ ).toBe("Runs the next node one by one, per item.")
439
+ expect(
440
+ describeEdgeBehavior({ outputMode: "each", rangeFrom: " ", rangeTo: "last" }),
441
+ ).toBe("Runs the next node one by one, per item.")
442
+ })
443
+
444
+ it("default config treats step 0 as default", () => {
445
+ expect(describeEdgeBehavior({ outputMode: "each", rangeStep: 0 })).toBe(
446
+ "Runs the next node one by one, per item.",
447
+ )
448
+ })
449
+ })
450
+
451
+ describe("describeEdgeBehavior — item mode", () => {
452
+ it("item 1 → the first item", () => {
453
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "1" })).toBe(
454
+ "Passes only the first item.",
455
+ )
456
+ })
457
+ it("item N (N>=2) → item N", () => {
458
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "3" })).toBe(
459
+ "Passes only item 3.",
460
+ )
461
+ })
462
+ it("last", () => {
463
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "last" })).toBe(
464
+ "Passes only the last item.",
465
+ )
466
+ })
467
+ it("last-0 equivalent to last", () => {
468
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "last-0" })).toBe(
469
+ "Passes only the last item.",
470
+ )
471
+ })
472
+ it("last-1", () => {
473
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "last-1" })).toBe(
474
+ "Passes only the second-to-last item.",
475
+ )
476
+ })
477
+ it("last-4 uses ordinal fallback", () => {
478
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "last-4" })).toBe(
479
+ "Passes only the 5th-from-last item.",
480
+ )
481
+ })
482
+ it("canonicalizes empty/missing itemIndex to 1", () => {
483
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "" })).toBe(
484
+ "Passes only the first item.",
485
+ )
486
+ expect(describeEdgeBehavior({ outputMode: "item" })).toBe(
487
+ "Passes only the first item.",
488
+ )
489
+ })
490
+ it("canonicalizes malformed itemIndex to 1", () => {
491
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "garbage" })).toBe(
492
+ "Passes only the first item.",
493
+ )
494
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "0" })).toBe(
495
+ "Passes only the first item.",
496
+ )
497
+ expect(describeEdgeBehavior({ outputMode: "item", itemIndex: "\t3\n" })).toBe(
498
+ "Passes only item 3.",
499
+ )
500
+ })
501
+ })
502
+
503
+ describe("describeEdgeBehavior — Range tab SELECTION_PHRASE", () => {
504
+ it("each with simple range", () => {
505
+ expect(
506
+ describeEdgeBehavior({ outputMode: "each", rangeFrom: "2", rangeTo: "last-1" }),
507
+ ).toBe("Runs the next node once for items 2 through the second-to-last.")
508
+ })
509
+
510
+ it("all with simple range", () => {
511
+ expect(
512
+ describeEdgeBehavior({ outputMode: "all", rangeFrom: "2", rangeTo: "last-1" }),
513
+ ).toBe("Passes items 2 through the second-to-last at once.")
514
+ })
515
+
516
+ it("each full-list reverse", () => {
517
+ expect(
518
+ describeEdgeBehavior({
519
+ outputMode: "each",
520
+ rangeFrom: "last",
521
+ rangeTo: "1",
522
+ rangeStep: -1,
523
+ }),
524
+ ).toBe("Runs the next node once for all items in reverse order.")
525
+ })
526
+
527
+ it("each positive step > 1", () => {
528
+ expect(
529
+ describeEdgeBehavior({
530
+ outputMode: "each",
531
+ rangeFrom: "1",
532
+ rangeTo: "10",
533
+ rangeStep: 2,
534
+ }),
535
+ ).toBe("Runs the next node once for items 1 through 10 (every 2nd item).")
536
+ })
537
+
538
+ it("each step 3 and step 21 ordinals", () => {
539
+ expect(
540
+ describeEdgeBehavior({
541
+ outputMode: "each",
542
+ rangeFrom: "1",
543
+ rangeTo: "10",
544
+ rangeStep: 3,
545
+ }),
546
+ ).toBe("Runs the next node once for items 1 through 10 (every 3rd item).")
547
+ expect(
548
+ describeEdgeBehavior({
549
+ outputMode: "each",
550
+ rangeFrom: "1",
551
+ rangeTo: "21",
552
+ rangeStep: 21,
553
+ }),
554
+ ).toBe("Runs the next node once for items 1 through 21 (every 21st item).")
555
+ })
556
+
557
+ it("empty-result concrete", () => {
558
+ expect(
559
+ describeEdgeBehavior({
560
+ outputMode: "each",
561
+ rangeFrom: "5",
562
+ rangeTo: "2",
563
+ rangeStep: 1,
564
+ }),
565
+ ).toBe("Nothing selected — the next node won't run.")
566
+ expect(
567
+ describeEdgeBehavior({
568
+ outputMode: "all",
569
+ rangeFrom: "5",
570
+ rangeTo: "2",
571
+ rangeStep: 1,
572
+ }),
573
+ ).toBe("Nothing selected — the next node will get an empty bundle.")
574
+ })
575
+
576
+ it("empty-result both relative", () => {
577
+ expect(
578
+ describeEdgeBehavior({
579
+ outputMode: "each",
580
+ rangeFrom: "last",
581
+ rangeTo: "last-3",
582
+ rangeStep: 1,
583
+ }),
584
+ ).toBe("Nothing selected — the next node won't run.")
585
+ })
586
+
587
+ it("empty-result negative step with from < to", () => {
588
+ expect(
589
+ describeEdgeBehavior({
590
+ outputMode: "each",
591
+ rangeFrom: "1",
592
+ rangeTo: "5",
593
+ rangeStep: -1,
594
+ }),
595
+ ).toBe("Nothing selected — the next node won't run.")
596
+ })
597
+
598
+ it("mixed-kind skips empty-result detection (falls through)", () => {
599
+ expect(
600
+ describeEdgeBehavior({
601
+ outputMode: "each",
602
+ rangeFrom: "last-5",
603
+ rangeTo: "3",
604
+ rangeStep: 1,
605
+ }),
606
+ ).toBe("Runs the next node once for items the 6th-from-last through 3.")
607
+ })
608
+
609
+ it("A==B collapse concrete", () => {
610
+ expect(
611
+ describeEdgeBehavior({
612
+ outputMode: "each",
613
+ rangeFrom: "3",
614
+ rangeTo: "3",
615
+ }),
616
+ ).toBe("Runs the next node only on item 3.")
617
+ })
618
+
619
+ it("A==B collapse relative via last/last-0 alias", () => {
620
+ expect(
621
+ describeEdgeBehavior({
622
+ outputMode: "each",
623
+ rangeFrom: "last",
624
+ rangeTo: "last-0",
625
+ }),
626
+ ).toBe("Runs the next node only on the last item.")
627
+ })
628
+ })
629
+
630
+ describe("describeEdgeBehavior — List tab SELECTION_PHRASE", () => {
631
+ it("3-term list", () => {
632
+ expect(
633
+ describeEdgeBehavior({
634
+ outputMode: "each",
635
+ selectorMode: "list",
636
+ listExpression: "1, 3, last",
637
+ }),
638
+ ).toBe("Runs the next node once for items 1, 3, and the last one.")
639
+ })
640
+
641
+ it("all mode 3-term list with inner range", () => {
642
+ expect(
643
+ describeEdgeBehavior({
644
+ outputMode: "all",
645
+ selectorMode: "list",
646
+ listExpression: "1, 3..5, last",
647
+ }),
648
+ ).toBe("Passes items 1, 3 through 5, and the last one at once.")
649
+ })
650
+
651
+ it("2-term list (no Oxford comma)", () => {
652
+ expect(
653
+ describeEdgeBehavior({
654
+ outputMode: "each",
655
+ selectorMode: "list",
656
+ listExpression: "1, last",
657
+ }),
658
+ ).toBe("Runs the next node once for items 1 and the last one.")
659
+ expect(
660
+ describeEdgeBehavior({
661
+ outputMode: "all",
662
+ selectorMode: "list",
663
+ listExpression: "1, last",
664
+ }),
665
+ ).toBe("Passes items 1 and the last one at once.")
666
+ })
667
+
668
+ it("single-range-term list", () => {
669
+ expect(
670
+ describeEdgeBehavior({
671
+ outputMode: "each",
672
+ selectorMode: "list",
673
+ listExpression: "1..5",
674
+ }),
675
+ ).toBe("Runs the next node once for items 1 through 5.")
676
+ })
677
+
678
+ it("single-index term triggers special case", () => {
679
+ expect(
680
+ describeEdgeBehavior({
681
+ outputMode: "each",
682
+ selectorMode: "list",
683
+ listExpression: "last",
684
+ }),
685
+ ).toBe("Runs the next node only on the last item.")
686
+ expect(
687
+ describeEdgeBehavior({
688
+ outputMode: "all",
689
+ selectorMode: "list",
690
+ listExpression: "3",
691
+ }),
692
+ ).toBe("Passes only item 3.")
693
+ })
694
+
695
+ it("collapsed range term A..A only triggers single-index special case", () => {
696
+ expect(
697
+ describeEdgeBehavior({
698
+ outputMode: "all",
699
+ selectorMode: "list",
700
+ listExpression: "3..3",
701
+ }),
702
+ ).toBe("Passes only item 3.")
703
+ expect(
704
+ describeEdgeBehavior({
705
+ outputMode: "all",
706
+ selectorMode: "list",
707
+ listExpression: "last..last-0",
708
+ }),
709
+ ).toBe("Passes only the last item.")
710
+ })
711
+
712
+ it("empty/whitespace listExpression → default config", () => {
713
+ expect(
714
+ describeEdgeBehavior({
715
+ outputMode: "each",
716
+ selectorMode: "list",
717
+ listExpression: "",
718
+ }),
719
+ ).toBe("Runs the next node one by one, per item.")
720
+ expect(
721
+ describeEdgeBehavior({
722
+ outputMode: "all",
723
+ selectorMode: "list",
724
+ listExpression: " ",
725
+ }),
726
+ ).toBe("Passes all items at once.")
727
+ })
728
+
729
+ it("malformed listExpression → fallback to mode default", () => {
730
+ expect(
731
+ describeEdgeBehavior({
732
+ outputMode: "each",
733
+ selectorMode: "list",
734
+ listExpression: "garbage",
735
+ }),
736
+ ).toBe("Runs the next node one by one, per item.")
737
+ expect(
738
+ describeEdgeBehavior({
739
+ outputMode: "all",
740
+ selectorMode: "list",
741
+ listExpression: "garbage",
742
+ }),
743
+ ).toBe("Passes all items at once.")
744
+ })
745
+
746
+ it("List tab range term with step > 1", () => {
747
+ expect(
748
+ describeEdgeBehavior({
749
+ outputMode: "each",
750
+ selectorMode: "list",
751
+ listExpression: "1..10:2",
752
+ }),
753
+ ).toBe("Runs the next node once for items 1 through 10 (every 2nd).")
754
+ })
755
+
756
+ it("List tab empty-result range term rendered as-typed", () => {
757
+ expect(
758
+ describeEdgeBehavior({
759
+ outputMode: "each",
760
+ selectorMode: "list",
761
+ listExpression: "5..2",
762
+ }),
763
+ ).toBe("Runs the next node once for items 5 through 2.")
764
+ })
765
+ })
766
+
767
+ import type { FullSelectorMode, SelectorConfig, SelectorResult } from "../selector.js"
768
+
769
+ describe("SelectorConfig types", () => {
770
+ it("compiles for the item mode minimum config", () => {
771
+ const c: SelectorConfig = { mode: "item", itemIndex: "1" }
772
+ expect(c.mode).toBe("item")
773
+ })
774
+
775
+ it("compiles for the predicate mode full config", () => {
776
+ const c: SelectorConfig = {
777
+ mode: "predicate",
778
+ predicateField: "score",
779
+ predicateOp: ">=",
780
+ predicateValue: "15",
781
+ predicateMatch: "all",
782
+ predicateCaseSensitive: false,
783
+ }
784
+ expect(c.predicateMatch).toBe("all")
785
+ })
786
+
787
+ it("SelectorResult enforces both keys", () => {
788
+ const r: SelectorResult = { picked: ["a"], rest: ["b", "c"] }
789
+ expect(r.picked.length + r.rest.length).toBe(3)
790
+ })
791
+ })
792
+
793
+ import { runSelector } from "../selector.js"
794
+
795
+ describe("runSelector — item mode", () => {
796
+ it("picks 1-based index", () => {
797
+ const { picked, rest } = runSelector(
798
+ ["a", "b", "c", "d"],
799
+ { mode: "item", itemIndex: "2" },
800
+ )
801
+ expect(picked).toEqual(["b"])
802
+ expect(rest).toEqual(["a", "c", "d"])
803
+ })
804
+
805
+ it('resolves "last-1"', () => {
806
+ const { picked } = runSelector(
807
+ ["a", "b", "c", "d"],
808
+ { mode: "item", itemIndex: "last-1" },
809
+ )
810
+ expect(picked).toEqual(["c"])
811
+ })
812
+
813
+ it("returns empty arrays on empty input", () => {
814
+ const { picked, rest } = runSelector([], { mode: "item", itemIndex: "1" })
815
+ expect(picked).toEqual([])
816
+ expect(rest).toEqual([])
817
+ })
818
+ })
819
+
820
+ describe("runSelector — range mode", () => {
821
+ it("slices from/to inclusive", () => {
822
+ const { picked, rest } = runSelector(
823
+ ["a", "b", "c", "d", "e"],
824
+ { mode: "range", rangeFrom: "2", rangeTo: "4", rangeStep: 1 },
825
+ )
826
+ expect(picked).toEqual(["b", "c", "d"])
827
+ expect(rest).toEqual(["a", "e"])
828
+ })
829
+
830
+ it("partitions by INDEX (not value) so duplicates land in the right buckets", () => {
831
+ // If the implementation used items.indexOf(value) to recover picked
832
+ // indices, both "a"s would resolve to index 0 and the second "a" would
833
+ // end up in `rest`, while index 2 ("c") would leak into picked.
834
+ const { picked, rest } = runSelector(
835
+ ["a", "b", "a", "c"],
836
+ { mode: "range", rangeFrom: "2", rangeTo: "3", rangeStep: 1 },
837
+ )
838
+ expect(picked).toEqual(["b", "a"]) // indices 1, 2
839
+ expect(rest).toEqual(["a", "c"]) // indices 0, 3
840
+ })
841
+ })
842
+
843
+ describe("runSelector — list mode", () => {
844
+ it("picks at specific 1-based indices", () => {
845
+ const { picked, rest } = runSelector(
846
+ ["a", "b", "c", "d", "e"],
847
+ { mode: "list", listExpression: "1,3,5" },
848
+ )
849
+ expect(picked).toEqual(["a", "c", "e"])
850
+ expect(rest).toEqual(["b", "d"])
851
+ })
852
+ })
853
+
854
+ describe("runSelector — random mode", () => {
855
+ it("is deterministic for the same seed", () => {
856
+ const a = runSelector(["a", "b", "c", "d"], { mode: "random", seed: "42", randomCount: 2 })
857
+ const b = runSelector(["a", "b", "c", "d"], { mode: "random", seed: "42", randomCount: 2 })
858
+ expect(a.picked).toEqual(b.picked)
859
+ expect(a.rest).toEqual(b.rest)
860
+ })
861
+
862
+ it("returns different results for different seeds", () => {
863
+ const a = runSelector(["a", "b", "c", "d"], { mode: "random", seed: "1", randomCount: 2 })
864
+ const b = runSelector(["a", "b", "c", "d"], { mode: "random", seed: "999", randomCount: 2 })
865
+ // It's possible for two seeds to coincide, but with 4 items + count 2 this
866
+ // is overwhelmingly unlikely for the chosen seeds.
867
+ expect(a.picked).not.toEqual(b.picked)
868
+ })
869
+
870
+ it("samples without replacement", () => {
871
+ const { picked } = runSelector(["a", "b", "c", "d"], { mode: "random", seed: "7", randomCount: 3 })
872
+ expect(picked).toHaveLength(3)
873
+ expect(new Set(picked).size).toBe(3)
874
+ })
875
+
876
+ it("clamps count to list length", () => {
877
+ const { picked, rest } = runSelector(["a", "b"], { mode: "random", seed: "7", randomCount: 10 })
878
+ expect(picked).toHaveLength(2)
879
+ expect(rest).toHaveLength(0)
880
+ })
881
+
882
+ it("defaults count to 1 when omitted", () => {
883
+ const { picked } = runSelector(["a", "b", "c"], { mode: "random", seed: "7" })
884
+ expect(picked).toHaveLength(1)
885
+ })
886
+
887
+ it("uses Math.random when seed is empty", () => {
888
+ // No determinism check — just verify it runs and returns a valid result.
889
+ const { picked, rest } = runSelector(["a", "b", "c"], { mode: "random", randomCount: 1 })
890
+ expect(picked).toHaveLength(1)
891
+ expect(rest).toHaveLength(2)
892
+ })
893
+ })
894
+
895
+ describe("runSelector — modulo mode", () => {
896
+ it("picks index = divisor % length", () => {
897
+ const { picked, rest } = runSelector(
898
+ ["a", "b", "c"],
899
+ { mode: "modulo", moduloDivisor: "5" },
900
+ )
901
+ // 5 % 3 = 2 → index 2 → "c"
902
+ expect(picked).toEqual(["c"])
903
+ expect(rest).toEqual(["a", "b"])
904
+ })
905
+
906
+ it("handles divisor < length", () => {
907
+ const { picked } = runSelector(
908
+ ["a", "b", "c"],
909
+ { mode: "modulo", moduloDivisor: "1" },
910
+ )
911
+ expect(picked).toEqual(["b"])
912
+ })
913
+
914
+ it("falls back to index 0 on non-numeric divisor", () => {
915
+ const { picked } = runSelector(
916
+ ["a", "b", "c"],
917
+ { mode: "modulo", moduloDivisor: "abc" },
918
+ )
919
+ expect(picked).toEqual(["a"])
920
+ })
921
+
922
+ it("falls back to index 0 when divisor is missing", () => {
923
+ const { picked } = runSelector(["a", "b", "c"], { mode: "modulo" })
924
+ expect(picked).toEqual(["a"])
925
+ })
926
+ })
927
+
928
+ describe("runSelector — predicate mode", () => {
929
+ const items = [
930
+ JSON.stringify({ name: "a", score: 10 }),
931
+ JSON.stringify({ name: "b", score: 20 }),
932
+ JSON.stringify({ name: "c", score: 30 }),
933
+ ]
934
+
935
+ it("returns first matching item by default", () => {
936
+ const { picked, rest } = runSelector(items, {
937
+ mode: "predicate",
938
+ predicateField: "score",
939
+ predicateOp: ">=",
940
+ predicateValue: "15",
941
+ })
942
+ expect(picked).toHaveLength(1)
943
+ expect(JSON.parse(picked[0]).name).toBe("b")
944
+ expect(rest).toHaveLength(2)
945
+ })
946
+
947
+ it("returns all matching items when match='all'", () => {
948
+ const { picked, rest } = runSelector(items, {
949
+ mode: "predicate",
950
+ predicateField: "score",
951
+ predicateOp: ">=",
952
+ predicateValue: "15",
953
+ predicateMatch: "all",
954
+ })
955
+ expect(picked.map((p) => JSON.parse(p).name)).toEqual(["b", "c"])
956
+ expect(rest.map((p) => JSON.parse(p).name)).toEqual(["a"])
957
+ })
958
+
959
+ it("returns empty picked when no items match", () => {
960
+ const { picked, rest } = runSelector(items, {
961
+ mode: "predicate",
962
+ predicateField: "score",
963
+ predicateOp: ">",
964
+ predicateValue: "100",
965
+ })
966
+ expect(picked).toEqual([])
967
+ expect(rest).toEqual(items)
968
+ })
969
+
970
+ it("respects case-sensitive flag for string ops", () => {
971
+ const stringItems = [JSON.stringify({ name: "Hero" }), JSON.stringify({ name: "hero" })]
972
+ const sensitive = runSelector(stringItems, {
973
+ mode: "predicate", predicateField: "name", predicateOp: "=",
974
+ predicateValue: "hero", predicateMatch: "all", predicateCaseSensitive: true,
975
+ })
976
+ expect(sensitive.picked).toHaveLength(1)
977
+
978
+ const insensitive = runSelector(stringItems, {
979
+ mode: "predicate", predicateField: "name", predicateOp: "=",
980
+ predicateValue: "hero", predicateMatch: "all", predicateCaseSensitive: false,
981
+ })
982
+ expect(insensitive.picked).toHaveLength(2)
983
+ })
984
+
985
+ it("matches whole item when predicateField is blank", () => {
986
+ // The selector-config.tsx field input shows the placeholder
987
+ // "blank = whole item". evaluateCondition treats an empty field path as
988
+ // "compare against the whole parsed item" — selectByPredicate used to
989
+ // early-bail on missing predicateField, defeating that UI affordance.
990
+ const items = ["alpha", "bravo", "charlie"]
991
+ const out = runSelector(items, {
992
+ mode: "predicate",
993
+ predicateOp: "contains",
994
+ predicateValue: "rav",
995
+ predicateMatch: "all",
996
+ })
997
+ expect(out.picked).toEqual(["bravo"])
998
+ expect(out.rest).toEqual(["alpha", "charlie"])
999
+ })
1000
+
1001
+ it("defaults predicateOp to '=' when undefined", () => {
1002
+ // The selector-config.tsx op dropdown shows `op={config.predicateOp ?? "="}`.
1003
+ // First-time predicate use without explicitly picking an op used to
1004
+ // silently return picked=[] because the early-bail tripped. Now the
1005
+ // runtime matches the UI's visual default.
1006
+ const items = [
1007
+ JSON.stringify({ name: "hero" }),
1008
+ JSON.stringify({ name: "villain" }),
1009
+ ]
1010
+ const out = runSelector(items, {
1011
+ mode: "predicate",
1012
+ predicateField: "name",
1013
+ predicateValue: "villain",
1014
+ predicateMatch: "all",
1015
+ })
1016
+ expect(out.picked).toHaveLength(1)
1017
+ expect(JSON.parse(out.picked[0]).name).toBe("villain")
1018
+ })
1019
+
1020
+ it("uses case-INsensitive matching by default when predicateCaseSensitive is undefined", () => {
1021
+ // The selector-config.tsx case checkbox shows
1022
+ // `caseSensitive={config.predicateCaseSensitive ?? false}` — unchecked
1023
+ // by default. Before the fix, undefined config silently flipped to
1024
+ // case-sensitive at runtime (evaluateCondition's default), so the user
1025
+ // saw "case-insensitive" UI and got case-sensitive matching.
1026
+ const stringItems = [JSON.stringify({ name: "Hero" }), JSON.stringify({ name: "hero" })]
1027
+ const out = runSelector(stringItems, {
1028
+ mode: "predicate",
1029
+ predicateField: "name",
1030
+ predicateOp: "=",
1031
+ predicateValue: "hero",
1032
+ predicateMatch: "all",
1033
+ // predicateCaseSensitive intentionally omitted
1034
+ })
1035
+ expect(out.picked).toHaveLength(2)
1036
+ })
1037
+ })
1038
+
1039
+ describe("runSelector — named-key mode", () => {
1040
+ const items = [
1041
+ JSON.stringify({ name: "hero", url: "x" }),
1042
+ JSON.stringify({ name: "villain", url: "y" }),
1043
+ JSON.stringify({ name: "sidekick", url: "z" }),
1044
+ ]
1045
+
1046
+ it("picks the first item whose field equals value", () => {
1047
+ const { picked, rest } = runSelector(items, {
1048
+ mode: "named-key",
1049
+ namedKeyField: "name",
1050
+ namedKeyValue: "hero",
1051
+ })
1052
+ expect(picked).toHaveLength(1)
1053
+ expect(JSON.parse(picked[0]).url).toBe("x")
1054
+ expect(rest).toHaveLength(2)
1055
+ })
1056
+
1057
+ it("returns empty picked when no match", () => {
1058
+ const { picked, rest } = runSelector(items, {
1059
+ mode: "named-key",
1060
+ namedKeyField: "name",
1061
+ namedKeyValue: "ghost",
1062
+ })
1063
+ expect(picked).toEqual([])
1064
+ expect(rest).toEqual(items)
1065
+ })
1066
+ })
1067
+
1068
+ // ---------------------------------------------------------------------------
1069
+ // resolveSelectorRefs — {NodeLabel} resolution for template-aware fields
1070
+ // ---------------------------------------------------------------------------
1071
+ import { resolveSelectorRefs } from "../selector.js"
1072
+
1073
+ describe("resolveSelectorRefs", () => {
1074
+ const variables = new Map<string, string>([
1075
+ ["LoopIteration", "5"],
1076
+ ["HeroName", "knight"],
1077
+ ])
1078
+
1079
+ it("resolves refs in moduloDivisor", () => {
1080
+ const out = resolveSelectorRefs(
1081
+ { mode: "modulo", moduloDivisor: "{LoopIteration}" },
1082
+ undefined,
1083
+ variables,
1084
+ )
1085
+ expect(out.moduloDivisor).toBe("5")
1086
+ })
1087
+
1088
+ it("resolves refs in predicateValue, namedKeyValue, seed", () => {
1089
+ const out = resolveSelectorRefs(
1090
+ {
1091
+ mode: "named-key",
1092
+ namedKeyField: "name",
1093
+ namedKeyValue: "{HeroName}",
1094
+ seed: "{HeroName}",
1095
+ predicateValue: "{HeroName}",
1096
+ },
1097
+ undefined,
1098
+ variables,
1099
+ )
1100
+ expect(out.namedKeyValue).toBe("knight")
1101
+ expect(out.seed).toBe("knight")
1102
+ expect(out.predicateValue).toBe("knight")
1103
+ })
1104
+
1105
+ it("leaves non-template fields untouched", () => {
1106
+ const out = resolveSelectorRefs(
1107
+ { mode: "item", itemIndex: "last-1" },
1108
+ undefined,
1109
+ variables,
1110
+ )
1111
+ expect(out.itemIndex).toBe("last-1")
1112
+ })
1113
+
1114
+ it("leaves unresolved refs verbatim", () => {
1115
+ const out = resolveSelectorRefs(
1116
+ { mode: "modulo", moduloDivisor: "{Unknown}" },
1117
+ undefined,
1118
+ variables,
1119
+ )
1120
+ expect(out.moduloDivisor).toBe("{Unknown}")
1121
+ })
1122
+
1123
+ it("substitutes {{trigger.foo}} in predicateValue when triggerData is passed", () => {
1124
+ // Selector must keep `{{trigger.*}}` semantics in lockstep with
1125
+ // filter-list / router conditions — both call `substituteTriggerTokens`.
1126
+ const out = resolveSelectorRefs(
1127
+ {
1128
+ mode: "predicate",
1129
+ predicateField: "name",
1130
+ predicateOp: "=",
1131
+ predicateValue: "{{trigger.hero}}",
1132
+ },
1133
+ { hero: "knight" },
1134
+ variables,
1135
+ )
1136
+ expect(out.predicateValue).toBe("knight")
1137
+ })
1138
+
1139
+ it("mixes {Label} refs and {{trigger.*}} tokens in the same field", () => {
1140
+ const out = resolveSelectorRefs(
1141
+ {
1142
+ mode: "named-key",
1143
+ namedKeyField: "name",
1144
+ namedKeyValue: "{HeroName}-{{trigger.suffix}}",
1145
+ },
1146
+ { suffix: "v2" },
1147
+ variables,
1148
+ )
1149
+ expect(out.namedKeyValue).toBe("knight-v2")
1150
+ })
1151
+ })