@winkjs/composer 0.0.2 → 0.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 (451) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -80
  3. package/package.json +56 -28
  4. package/src/composer.js +75 -0
  5. package/src/core/batch/aoo-encoder.js +148 -0
  6. package/src/core/batch/index.js +12 -0
  7. package/src/core/batch/sliding-window.js +91 -0
  8. package/src/core/codec/index.js +137 -0
  9. package/src/core/emitter-manager/mqtt/constants.js +102 -0
  10. package/src/core/emitter-manager/mqtt/emitter.js +780 -0
  11. package/src/core/emitter-manager/mqtt/index.js +190 -0
  12. package/src/core/emitter-manager/mqtt/mqtt-store.js +538 -0
  13. package/src/core/emitter-manager/terminal/assert-columns.js +98 -0
  14. package/src/core/emitter-manager/terminal/formatters.js +160 -0
  15. package/src/core/emitter-manager/terminal/index.js +341 -0
  16. package/src/core/env-vars.js +245 -0
  17. package/src/core/mqtt-protocol.js +47 -0
  18. package/src/core/partition-manager/index.js +7 -0
  19. package/src/core/partition-manager/init.js +44 -0
  20. package/src/core/partition-manager/introspect.js +15 -0
  21. package/src/core/partition-manager/publish-to.js +6 -0
  22. package/src/core/partition-manager/recompute.js +5 -0
  23. package/src/core/partition-manager/reset.js +5 -0
  24. package/src/core/partition-manager/update.js +264 -0
  25. package/src/core/partition-manager/validate-flow.js +157 -0
  26. package/src/core/semantics/digest.js +128 -0
  27. package/src/core/semantics/index.js +83 -0
  28. package/src/core/semantics/loader.js +571 -0
  29. package/src/core/semantics/schemas/asset-class-schema.js +218 -0
  30. package/src/core/semantics/schemas/column-schema.js +651 -0
  31. package/src/core/semantics/schemas/enum-schema.js +153 -0
  32. package/src/core/semantics/schemas/index.js +51 -0
  33. package/src/core/semantics/warnings.js +141 -0
  34. package/src/core/shutdown-manager/index.js +289 -0
  35. package/src/core/source-manager/csv/index.js +171 -0
  36. package/src/core/source-manager/csv/start.js +456 -0
  37. package/src/core/source-manager/mqtt/client.js +406 -0
  38. package/src/core/source-manager/mqtt/constants.js +147 -0
  39. package/src/core/source-manager/mqtt/dedup.js +244 -0
  40. package/src/core/source-manager/mqtt/index.js +260 -0
  41. package/src/core/source-manager/mqtt/status.js +546 -0
  42. package/src/core/source-manager/test-harness/comparator.js +226 -0
  43. package/src/core/source-manager/test-harness/field-generator.js +85 -0
  44. package/src/core/source-manager/test-harness/fuzz.js +87 -0
  45. package/src/core/source-manager/test-harness/index.js +115 -0
  46. package/src/core/source-manager/test-harness/prng.js +57 -0
  47. package/src/core/source-manager/test-harness/start.js +232 -0
  48. package/src/core/source-manager/test-harness/validate.js +213 -0
  49. package/src/core/storage-manager/questdb/assert-columns.js +120 -0
  50. package/src/core/storage-manager/questdb/ensure-tables.js +187 -0
  51. package/src/core/storage-manager/questdb/index.js +1251 -0
  52. package/src/core/storage-manager/questdb/persist-plan.js +488 -0
  53. package/src/core/storage-manager/questdb/writers.js +142 -0
  54. package/src/core/test-utils/sealed-record.js +80 -0
  55. package/src/core/test-utils/soak-signature.js +149 -0
  56. package/src/core/test-utils/tcp-proxy.js +106 -0
  57. package/src/core/tunable/helpers.js +254 -0
  58. package/src/core/tunable/index.js +87 -0
  59. package/src/core/utils/console-formatter/index.js +207 -0
  60. package/src/core/utils/create-smart-store-as/index.js +98 -0
  61. package/src/core/utils/create-smart-store-as/sanitize-field.js +46 -0
  62. package/src/core/utils/create-smart-store-as/smart-truncate.js +93 -0
  63. package/src/core/utils/flow/index.js +5 -0
  64. package/src/core/utils/flow/make-collision-checker.js +23 -0
  65. package/src/core/utils/flow/to-kebab.js +20 -0
  66. package/src/core/utils/half-life/index.js +111 -0
  67. package/src/core/utils/node/disable.js +11 -0
  68. package/src/core/utils/node/enable.js +11 -0
  69. package/src/core/utils/node/execute-triggers.js +40 -0
  70. package/src/core/utils/node/index.js +10 -0
  71. package/src/core/utils/node/pause.js +11 -0
  72. package/src/core/utils/node/populate-predicate-inputs.js +14 -0
  73. package/src/core/utils/node/publish-nan.js +19 -0
  74. package/src/core/utils/node/reset-predicate-inputs.js +11 -0
  75. package/src/core/utils/node/unpause.js +11 -0
  76. package/src/core/utils/node/validate-spec.js +22 -0
  77. package/src/core/utils/options/resolve-field-keyed.js +115 -0
  78. package/src/core/utils/quantize/index.js +75 -0
  79. package/src/core/utils/stats/formulas.js +67 -0
  80. package/src/core/utils/stats/index.js +3 -0
  81. package/src/core/utils/validate/composer-validators.js +141 -0
  82. package/src/core/utils/validate/helpers.js +206 -0
  83. package/src/core/utils/validate/index.js +12 -0
  84. package/src/core/utils/validate/type-validators.js +538 -0
  85. package/src/core/utils/validate/validate.js +107 -0
  86. package/src/core/utils/validate/validators.js +177 -0
  87. package/src/core/wiring/assert-handle.js +57 -0
  88. package/src/core/wiring/assert-module.js +63 -0
  89. package/src/core/wiring/index.js +5 -0
  90. package/src/core/wiring/node-type-to-module.js +17 -0
  91. package/src/core/wiring/wire-emitters.js +256 -0
  92. package/src/core/wiring/wire-linear-graph.js +25 -0
  93. package/src/core/wiring/wire-node.js +157 -0
  94. package/src/core/wiring/wire-semantics.js +173 -0
  95. package/src/core/wiring/wire-storages.js +344 -0
  96. package/src/flow/check-output-collisions.js +93 -0
  97. package/src/flow/consts.js +9 -0
  98. package/src/flow/deep-clone-spec.js +64 -0
  99. package/src/flow/driver.js +171 -0
  100. package/src/flow/expand-single-input-array.js +76 -0
  101. package/src/flow/expand-template-spec.js +93 -0
  102. package/src/flow/flow-api-schemas.js +300 -0
  103. package/src/flow/flow.js +658 -0
  104. package/src/flow/for-each.js +225 -0
  105. package/src/flow/get-signature-pattern.js +31 -0
  106. package/src/flow/get-target-array.js +46 -0
  107. package/src/flow/inspect.js +67 -0
  108. package/src/flow/load-node-module.js +81 -0
  109. package/src/flow/make-node-method.js +112 -0
  110. package/src/flow/resolve-pick-by-field.js +77 -0
  111. package/src/flow/resolve-tunables.js +76 -0
  112. package/src/flow/run.js +392 -0
  113. package/src/flow/serialize.js +161 -0
  114. package/src/flow/signatures-schema.js +222 -0
  115. package/src/flow/spec-builder.js +65 -0
  116. package/src/flow/validate-triggers.js +250 -0
  117. package/src/flow/validate.js +261 -0
  118. package/src/nodes/accumulate/index.js +21 -0
  119. package/src/nodes/accumulate/init.js +48 -0
  120. package/src/nodes/accumulate/introspect.js +123 -0
  121. package/src/nodes/accumulate/publish-to.js +34 -0
  122. package/src/nodes/accumulate/recompute.js +27 -0
  123. package/src/nodes/accumulate/reset.js +25 -0
  124. package/src/nodes/accumulate/update.js +42 -0
  125. package/src/nodes/appraise/calibrate.js +64 -0
  126. package/src/nodes/appraise/decision.js +66 -0
  127. package/src/nodes/appraise/deviation.js +140 -0
  128. package/src/nodes/appraise/index.js +23 -0
  129. package/src/nodes/appraise/init.js +165 -0
  130. package/src/nodes/appraise/integrate.js +45 -0
  131. package/src/nodes/appraise/introspect.js +352 -0
  132. package/src/nodes/appraise/publish-to.js +93 -0
  133. package/src/nodes/appraise/receptor.js +69 -0
  134. package/src/nodes/appraise/recompute.js +79 -0
  135. package/src/nodes/appraise/reset.js +53 -0
  136. package/src/nodes/appraise/update.js +130 -0
  137. package/src/nodes/archive/csv/index.js +6 -0
  138. package/src/nodes/archive/csv/init.js +36 -0
  139. package/src/nodes/archive/csv/load.js +70 -0
  140. package/src/nodes/archive/csv/reset.js +14 -0
  141. package/src/nodes/archive/csv/simple-parser.js +216 -0
  142. package/src/nodes/archive/csv/test.js +173 -0
  143. package/src/nodes/archive/csv/update.js +63 -0
  144. package/src/nodes/butterworth-filter/index.js +18 -0
  145. package/src/nodes/butterworth-filter/init.js +184 -0
  146. package/src/nodes/butterworth-filter/introspect.js +228 -0
  147. package/src/nodes/butterworth-filter/publish-to.js +21 -0
  148. package/src/nodes/butterworth-filter/recompute.js +13 -0
  149. package/src/nodes/butterworth-filter/reset.js +30 -0
  150. package/src/nodes/butterworth-filter/update.js +38 -0
  151. package/src/nodes/categorize/index.js +19 -0
  152. package/src/nodes/categorize/init.js +58 -0
  153. package/src/nodes/categorize/introspect.js +177 -0
  154. package/src/nodes/categorize/publish-to.js +29 -0
  155. package/src/nodes/categorize/recompute.js +14 -0
  156. package/src/nodes/categorize/reset.js +16 -0
  157. package/src/nodes/categorize/update.js +53 -0
  158. package/src/nodes/controller/index.js +9 -0
  159. package/src/nodes/controller/init.js +55 -0
  160. package/src/nodes/controller/introspect.js +91 -0
  161. package/src/nodes/controller/publish-to.js +8 -0
  162. package/src/nodes/controller/recompute.js +9 -0
  163. package/src/nodes/controller/reset.js +17 -0
  164. package/src/nodes/controller/update.js +63 -0
  165. package/src/nodes/diff/index.js +19 -0
  166. package/src/nodes/diff/init.js +37 -0
  167. package/src/nodes/diff/introspect.js +132 -0
  168. package/src/nodes/diff/publish-to.js +21 -0
  169. package/src/nodes/diff/recompute.js +12 -0
  170. package/src/nodes/diff/reset.js +12 -0
  171. package/src/nodes/diff/update.js +37 -0
  172. package/src/nodes/digest-moments/formulas.js +12 -0
  173. package/src/nodes/digest-moments/index.js +14 -0
  174. package/src/nodes/digest-moments/init.js +67 -0
  175. package/src/nodes/digest-moments/introspect.js +154 -0
  176. package/src/nodes/digest-moments/publish-to.js +34 -0
  177. package/src/nodes/digest-moments/recompute.js +14 -0
  178. package/src/nodes/digest-moments/reset.js +14 -0
  179. package/src/nodes/digest-moments/update.js +120 -0
  180. package/src/nodes/dwell-time-tracker/index.js +14 -0
  181. package/src/nodes/dwell-time-tracker/init.js +59 -0
  182. package/src/nodes/dwell-time-tracker/introspect.js +137 -0
  183. package/src/nodes/dwell-time-tracker/publish-to.js +22 -0
  184. package/src/nodes/dwell-time-tracker/recompute.js +8 -0
  185. package/src/nodes/dwell-time-tracker/reset.js +23 -0
  186. package/src/nodes/dwell-time-tracker/update.js +103 -0
  187. package/src/nodes/emit-if/helpers.js +94 -0
  188. package/src/nodes/emit-if/index.js +16 -0
  189. package/src/nodes/emit-if/init.js +67 -0
  190. package/src/nodes/emit-if/introspect.js +102 -0
  191. package/src/nodes/emit-if/publish-to.js +13 -0
  192. package/src/nodes/emit-if/recompute.js +12 -0
  193. package/src/nodes/emit-if/reset.js +27 -0
  194. package/src/nodes/emit-if/update.js +93 -0
  195. package/src/nodes/es-correlation/index.js +19 -0
  196. package/src/nodes/es-correlation/init.js +73 -0
  197. package/src/nodes/es-correlation/introspect.js +176 -0
  198. package/src/nodes/es-correlation/publish-to.js +32 -0
  199. package/src/nodes/es-correlation/recompute.js +56 -0
  200. package/src/nodes/es-correlation/reset.js +27 -0
  201. package/src/nodes/es-correlation/update.js +110 -0
  202. package/src/nodes/es-mean/index.js +20 -0
  203. package/src/nodes/es-mean/init.js +58 -0
  204. package/src/nodes/es-mean/introspect.js +129 -0
  205. package/src/nodes/es-mean/publish-to.js +19 -0
  206. package/src/nodes/es-mean/recompute.js +16 -0
  207. package/src/nodes/es-mean/reset.js +24 -0
  208. package/src/nodes/es-mean/update.js +69 -0
  209. package/src/nodes/es-pairwise-correlation/index.js +19 -0
  210. package/src/nodes/es-pairwise-correlation/init.js +98 -0
  211. package/src/nodes/es-pairwise-correlation/introspect.js +187 -0
  212. package/src/nodes/es-pairwise-correlation/publish-to.js +31 -0
  213. package/src/nodes/es-pairwise-correlation/recompute.js +69 -0
  214. package/src/nodes/es-pairwise-correlation/reset.js +29 -0
  215. package/src/nodes/es-pairwise-correlation/update.js +89 -0
  216. package/src/nodes/es-stats/index.js +21 -0
  217. package/src/nodes/es-stats/init.js +105 -0
  218. package/src/nodes/es-stats/introspect.js +157 -0
  219. package/src/nodes/es-stats/publish-to.js +42 -0
  220. package/src/nodes/es-stats/recompute.js +74 -0
  221. package/src/nodes/es-stats/reset.js +49 -0
  222. package/src/nodes/es-stats/update-envelope.js +44 -0
  223. package/src/nodes/es-stats/update-welford.js +64 -0
  224. package/src/nodes/es-stats/update.js +59 -0
  225. package/src/nodes/index.js +83 -0
  226. package/src/nodes/invert-flag/index.js +20 -0
  227. package/src/nodes/invert-flag/init.js +36 -0
  228. package/src/nodes/invert-flag/introspect.js +115 -0
  229. package/src/nodes/invert-flag/publish-to.js +23 -0
  230. package/src/nodes/invert-flag/recompute.js +12 -0
  231. package/src/nodes/invert-flag/reset.js +12 -0
  232. package/src/nodes/invert-flag/update.js +32 -0
  233. package/src/nodes/kalman1d/index.js +20 -0
  234. package/src/nodes/kalman1d/init.js +103 -0
  235. package/src/nodes/kalman1d/introspect.js +250 -0
  236. package/src/nodes/kalman1d/publish-to.js +34 -0
  237. package/src/nodes/kalman1d/recompute.js +23 -0
  238. package/src/nodes/kalman1d/reset.js +29 -0
  239. package/src/nodes/kalman1d/update.js +144 -0
  240. package/src/nodes/kernel/index.js +18 -0
  241. package/src/nodes/kernel/init.js +66 -0
  242. package/src/nodes/kernel/introspect.js +163 -0
  243. package/src/nodes/kernel/presets.js +74 -0
  244. package/src/nodes/kernel/publish-to.js +25 -0
  245. package/src/nodes/kernel/recompute.js +14 -0
  246. package/src/nodes/kernel/reset.js +20 -0
  247. package/src/nodes/kernel/update.js +69 -0
  248. package/src/nodes/lag/index.js +21 -0
  249. package/src/nodes/lag/init.js +87 -0
  250. package/src/nodes/lag/introspect.js +202 -0
  251. package/src/nodes/lag/publish-to.js +64 -0
  252. package/src/nodes/lag/recompute.js +33 -0
  253. package/src/nodes/lag/reset.js +49 -0
  254. package/src/nodes/lag/update.js +133 -0
  255. package/src/nodes/median3/index.js +19 -0
  256. package/src/nodes/median3/init.js +40 -0
  257. package/src/nodes/median3/introspect.js +116 -0
  258. package/src/nodes/median3/publish-to.js +21 -0
  259. package/src/nodes/median3/recompute.js +12 -0
  260. package/src/nodes/median3/reset.js +15 -0
  261. package/src/nodes/median3/update.js +58 -0
  262. package/src/nodes/moments-digest/copy-from.js +12 -0
  263. package/src/nodes/moments-digest/flush.js +17 -0
  264. package/src/nodes/moments-digest/index.js +132 -0
  265. package/src/nodes/moments-digest/init.js +84 -0
  266. package/src/nodes/moments-digest/introspect.js +122 -0
  267. package/src/nodes/moments-digest/prelude.js +28 -0
  268. package/src/nodes/moments-digest/publish-to.js +31 -0
  269. package/src/nodes/moments-digest/recompute.js +9 -0
  270. package/src/nodes/moments-digest/reset.js +20 -0
  271. package/src/nodes/moments-digest/update.js +182 -0
  272. package/src/nodes/node-names.js +67 -0
  273. package/src/nodes/page-hinkley/index.js +20 -0
  274. package/src/nodes/page-hinkley/init.js +66 -0
  275. package/src/nodes/page-hinkley/introspect.js +193 -0
  276. package/src/nodes/page-hinkley/publish-to.js +28 -0
  277. package/src/nodes/page-hinkley/recompute.js +14 -0
  278. package/src/nodes/page-hinkley/reset.js +23 -0
  279. package/src/nodes/page-hinkley/update.js +105 -0
  280. package/src/nodes/pass-if/index.js +21 -0
  281. package/src/nodes/pass-if/init.js +37 -0
  282. package/src/nodes/pass-if/introspect.js +93 -0
  283. package/src/nodes/pass-if/publish-to.js +15 -0
  284. package/src/nodes/pass-if/recompute.js +15 -0
  285. package/src/nodes/pass-if/reset.js +18 -0
  286. package/src/nodes/pass-if/update.js +37 -0
  287. package/src/nodes/persist-if/helpers.js +111 -0
  288. package/src/nodes/persist-if/index.js +9 -0
  289. package/src/nodes/persist-if/init.js +98 -0
  290. package/src/nodes/persist-if/introspect.js +106 -0
  291. package/src/nodes/persist-if/publish-to.js +21 -0
  292. package/src/nodes/persist-if/recompute.js +20 -0
  293. package/src/nodes/persist-if/reset.js +35 -0
  294. package/src/nodes/persist-if/update.js +112 -0
  295. package/src/nodes/persistence-check/index.js +14 -0
  296. package/src/nodes/persistence-check/init.js +38 -0
  297. package/src/nodes/persistence-check/introspect.js +144 -0
  298. package/src/nodes/persistence-check/publish-to.js +11 -0
  299. package/src/nodes/persistence-check/recompute.js +7 -0
  300. package/src/nodes/persistence-check/reset.js +12 -0
  301. package/src/nodes/persistence-check/update.js +57 -0
  302. package/src/nodes/process-index/index.js +15 -0
  303. package/src/nodes/process-index/init.js +48 -0
  304. package/src/nodes/process-index/introspect.js +182 -0
  305. package/src/nodes/process-index/publish-to.js +29 -0
  306. package/src/nodes/process-index/recompute.js +8 -0
  307. package/src/nodes/process-index/reset.js +8 -0
  308. package/src/nodes/process-index/update.js +75 -0
  309. package/src/nodes/ratio/index.js +14 -0
  310. package/src/nodes/ratio/init.js +37 -0
  311. package/src/nodes/ratio/introspect.js +155 -0
  312. package/src/nodes/ratio/publish-to.js +15 -0
  313. package/src/nodes/ratio/recompute.js +7 -0
  314. package/src/nodes/ratio/reset.js +7 -0
  315. package/src/nodes/ratio/update.js +47 -0
  316. package/src/nodes/sanitize/index.js +82 -0
  317. package/src/nodes/sanitize/init.js +82 -0
  318. package/src/nodes/sanitize/introspect.js +194 -0
  319. package/src/nodes/sanitize/publish-to.js +15 -0
  320. package/src/nodes/sanitize/recompute.js +8 -0
  321. package/src/nodes/sanitize/reset.js +14 -0
  322. package/src/nodes/sanitize/update.js +102 -0
  323. package/src/nodes/spike-guard/index.js +14 -0
  324. package/src/nodes/spike-guard/init.js +47 -0
  325. package/src/nodes/spike-guard/introspect.js +130 -0
  326. package/src/nodes/spike-guard/publish-to.js +35 -0
  327. package/src/nodes/spike-guard/recompute.js +14 -0
  328. package/src/nodes/spike-guard/reset.js +19 -0
  329. package/src/nodes/spike-guard/update.js +89 -0
  330. package/src/nodes/sse-emitter/index.js +7 -0
  331. package/src/nodes/sse-emitter/init.js +105 -0
  332. package/src/nodes/sse-emitter/introspect.js +134 -0
  333. package/src/nodes/sse-emitter/publish-to.js +6 -0
  334. package/src/nodes/sse-emitter/recompute.js +16 -0
  335. package/src/nodes/sse-emitter/reset.js +19 -0
  336. package/src/nodes/sse-emitter/update.js +69 -0
  337. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +36 -0
  338. package/src/nodes/sse-emitter/utils/build-message.js +31 -0
  339. package/src/nodes/sse-emitter/utils/connection-manager.js +45 -0
  340. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +20 -0
  341. package/src/nodes/sse-emitter/utils/health-monitor.js +38 -0
  342. package/src/nodes/sse-emitter/utils/qos-handler.js +41 -0
  343. package/src/nodes/state-change-detector/helpers.js +35 -0
  344. package/src/nodes/state-change-detector/index.js +12 -0
  345. package/src/nodes/state-change-detector/init.js +56 -0
  346. package/src/nodes/state-change-detector/introspect.js +146 -0
  347. package/src/nodes/state-change-detector/publish-to.js +20 -0
  348. package/src/nodes/state-change-detector/recompute.js +6 -0
  349. package/src/nodes/state-change-detector/reset.js +20 -0
  350. package/src/nodes/state-change-detector/update.js +97 -0
  351. package/src/nodes/sw-stats/index.js +14 -0
  352. package/src/nodes/sw-stats/init.js +51 -0
  353. package/src/nodes/sw-stats/introspect.js +131 -0
  354. package/src/nodes/sw-stats/publish-to.js +45 -0
  355. package/src/nodes/sw-stats/recompute.js +33 -0
  356. package/src/nodes/sw-stats/reset.js +15 -0
  357. package/src/nodes/sw-stats/update.js +39 -0
  358. package/src/nodes/swing-watch/index.js +17 -0
  359. package/src/nodes/swing-watch/init.js +144 -0
  360. package/src/nodes/swing-watch/introspect.js +188 -0
  361. package/src/nodes/swing-watch/publish-to.js +53 -0
  362. package/src/nodes/swing-watch/recompute.js +22 -0
  363. package/src/nodes/swing-watch/reset.js +55 -0
  364. package/src/nodes/swing-watch/update.js +767 -0
  365. package/src/nodes/tally/index.js +15 -0
  366. package/src/nodes/tally/init.js +32 -0
  367. package/src/nodes/tally/introspect.js +116 -0
  368. package/src/nodes/tally/publish-to.js +28 -0
  369. package/src/nodes/tally/recompute.js +10 -0
  370. package/src/nodes/tally/reset.js +10 -0
  371. package/src/nodes/tally/update.js +44 -0
  372. package/src/nodes/threshold/index.js +14 -0
  373. package/src/nodes/threshold/init.js +121 -0
  374. package/src/nodes/threshold/introspect.js +192 -0
  375. package/src/nodes/threshold/publish-to.js +11 -0
  376. package/src/nodes/threshold/recompute.js +8 -0
  377. package/src/nodes/threshold/reset.js +14 -0
  378. package/src/nodes/threshold/update.js +54 -0
  379. package/src/nodes/transform/helpers.js +39 -0
  380. package/src/nodes/transform/index.js +15 -0
  381. package/src/nodes/transform/init.js +35 -0
  382. package/src/nodes/transform/introspect.js +118 -0
  383. package/src/nodes/transform/publish-to.js +23 -0
  384. package/src/nodes/transform/recompute.js +12 -0
  385. package/src/nodes/transform/reset.js +11 -0
  386. package/src/nodes/transform/update.js +32 -0
  387. package/src/nodes/trend/compute-confidence.js +112 -0
  388. package/src/nodes/trend/index.js +14 -0
  389. package/src/nodes/trend/init.js +81 -0
  390. package/src/nodes/trend/introspect.js +151 -0
  391. package/src/nodes/trend/publish-to.js +22 -0
  392. package/src/nodes/trend/recompute.js +15 -0
  393. package/src/nodes/trend/reset.js +26 -0
  394. package/src/nodes/trend/update.js +93 -0
  395. package/src/nodes/tw-stats/copy-from.js +16 -0
  396. package/src/nodes/tw-stats/flush.js +12 -0
  397. package/src/nodes/tw-stats/index.js +41 -0
  398. package/src/nodes/tw-stats/init.js +92 -0
  399. package/src/nodes/tw-stats/introspect.js +171 -0
  400. package/src/nodes/tw-stats/publish-to.js +108 -0
  401. package/src/nodes/tw-stats/recompute.js +9 -0
  402. package/src/nodes/tw-stats/reset.js +16 -0
  403. package/src/nodes/tw-stats/update.js +113 -0
  404. package/src/nodes/unbalance/index.js +15 -0
  405. package/src/nodes/unbalance/init.js +59 -0
  406. package/src/nodes/unbalance/introspect.js +167 -0
  407. package/src/nodes/unbalance/publish-to.js +52 -0
  408. package/src/nodes/unbalance/recompute.js +11 -0
  409. package/src/nodes/unbalance/reset.js +10 -0
  410. package/src/nodes/unbalance/update.js +83 -0
  411. package/src/nodes/vector-distance/index.js +14 -0
  412. package/src/nodes/vector-distance/init.js +54 -0
  413. package/src/nodes/vector-distance/introspect.js +149 -0
  414. package/src/nodes/vector-distance/publish-to.js +35 -0
  415. package/src/nodes/vector-distance/recompute.js +11 -0
  416. package/src/nodes/vector-distance/reset.js +20 -0
  417. package/src/nodes/vector-distance/update.js +139 -0
  418. package/src/nodes/winnow/index.js +12 -0
  419. package/src/nodes/winnow/init.js +82 -0
  420. package/src/nodes/winnow/introspect.js +253 -0
  421. package/src/nodes/winnow/publish-to.js +51 -0
  422. package/src/nodes/winnow/recompute.js +14 -0
  423. package/src/nodes/winnow/reset.js +34 -0
  424. package/src/nodes/winnow/update.js +203 -0
  425. package/src/tools/training/features.js +227 -0
  426. package/src/tools/training/index.js +21 -0
  427. package/src/tools/training/logistic.js +238 -0
  428. package/src/tools/training/metrics.js +126 -0
  429. package/src/tools/training/scale.js +239 -0
  430. package/src/tools/training/validate.js +103 -0
  431. package/src/windowing/count-sliding/create.js +16 -0
  432. package/src/windowing/count-sliding/get-reverse-chronological-index.js +19 -0
  433. package/src/windowing/count-sliding/index.js +6 -0
  434. package/src/windowing/count-sliding/is-not-full.js +5 -0
  435. package/src/windowing/count-sliding/push.js +15 -0
  436. package/src/windowing/count-sliding/reset.js +11 -0
  437. package/src/windowing/count-sliding/size.js +5 -0
  438. package/src/windowing/time-sliding/create.js +25 -0
  439. package/src/windowing/time-sliding/push.js +42 -0
  440. package/CODE_OF_CONDUCT.md +0 -46
  441. package/CONTRIBUTING.md +0 -132
  442. package/docs/.nojekyll +0 -0
  443. package/docs/CONTRIBUTING.md +0 -134
  444. package/docs/README.md +0 -132
  445. package/docs/_coverpage.md +0 -26
  446. package/docs/_media/bg.png +0 -0
  447. package/docs/_media/wink-logo.png +0 -0
  448. package/docs/_navbar.md +0 -14
  449. package/docs/_sidebar.md +0 -2
  450. package/docs/index.html +0 -44
  451. package/eslint.config.js +0 -283
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @fileoverview Build-time guard against output-field overwrites.
3
+ *
4
+ * Two nodes on the same runtime path must not write the same message field. If they
5
+ * did, the second node's `publishTo` would overwrite the first node's value silently.
6
+ * This module finds such collisions at build time so the flow fails loudly instead.
7
+ *
8
+ * A flow has these runtime paths: a flow with no `.switch()`/`.groupBy()` is one
9
+ * linear path (every node runs on every message); a switched flow is one path per
10
+ * case (only one case runs per message), and `.switch()`/`.groupBy()` forbid any node
11
+ * before the switch, so a case has no shared prefix to merge in. So uniqueness is
12
+ * checked within each path, never across paths: the same field written by two sibling
13
+ * cases is fine, because only one of those cases runs for a given message.
14
+ *
15
+ * Scope: this checks declared outputs - the `storeAs` names in a spec's `stats`. That
16
+ * is exactly what the fan naming rule (`${field}_${label}`) produces. It does not
17
+ * cover a node that derives its output field names from its input field at init time
18
+ * (for example `momentsDigest`, which writes `${field}_M1`, `${field}_mean`, and so
19
+ * on, with no `stats` in its spec). Catching those needs each node to declare the
20
+ * fields it writes; that is tracked as a separate follow-up.
21
+ *
22
+ * @see flow.js - build()/run()/validate() call these before producing or running a flow
23
+ */
24
+
25
+ /**
26
+ * Record one spec's declared outputs against the path's owner map, appending an error
27
+ * for any field a previous node on this path already writes.
28
+ *
29
+ * @param {Object} spec - A built spec with a `stats` object
30
+ * @param {Object} owner - Map of storeAs -> first node name that writes it (mutated)
31
+ * @param {string[]} errors - Collision messages (mutated)
32
+ * @param {string} scopeLabel - Text appended to the field name in the error
33
+ */
34
+ const recordSpecOutputs = function ( spec, owner, errors, scopeLabel ) {
35
+ // eslint-disable-next-line guard-for-in
36
+ for ( const stat in spec.stats ) {
37
+ const storeAs = spec.stats[ stat ].storeAs;
38
+ if ( owner[ storeAs ] === undefined ) {
39
+ owner[ storeAs ] = spec.name;
40
+ } else {
41
+ errors.push(
42
+ `output field '${storeAs}'${scopeLabel} is written by two nodes on the ` +
43
+ `same path: '${owner[ storeAs ]}' and '${spec.name}'. The second write would ` +
44
+ 'overwrite the first. Give them distinct output labels.'
45
+ );
46
+ }
47
+ }
48
+ }; // recordSpecOutputs()
49
+
50
+ /**
51
+ * Find duplicate output fields within one path's specs.
52
+ *
53
+ * @param {Array} specs - The specs on one runtime path (linear flow or one case)
54
+ * @param {string} scopeLabel - Text appended to the field name in the error (for
55
+ * example ` in case 'idle'`); empty for a linear flow
56
+ * @returns {string[]} One message per collision; empty when the path is clean
57
+ */
58
+ export const findOutputCollisions = function ( specs, scopeLabel ) {
59
+ const owner = Object.create( null ); // storeAs -> name of the node that writes it
60
+ const errors = [];
61
+
62
+ for ( let i = 0; i < specs.length; i += 1 ) {
63
+ const spec = specs[ i ];
64
+ if ( spec.stats ) {
65
+ recordSpecOutputs( spec, owner, errors, scopeLabel );
66
+ }
67
+ }
68
+
69
+ return errors;
70
+ }; // findOutputCollisions()
71
+
72
+ /**
73
+ * Find output-field collisions across a whole flow, one path at a time.
74
+ *
75
+ * @param {Object} switchState - The flow's switch/case state
76
+ * @param {Array} flowDefinition - The linear flow's specs (used when not switched)
77
+ * @returns {string[]} One message per collision across all paths; empty when clean
78
+ */
79
+ export const collectFlowOutputCollisions = function ( switchState, flowDefinition ) {
80
+ if ( switchState.active ) {
81
+ const errors = [];
82
+ for ( let i = 0; i < switchState.caseOrder.length; i += 1 ) {
83
+ const key = switchState.caseOrder[ i ];
84
+ const caseErrors = findOutputCollisions( switchState.caseSpecs[ key ], ` in case '${key}'` );
85
+ for ( let j = 0; j < caseErrors.length; j += 1 ) {
86
+ errors.push( caseErrors[ j ] );
87
+ }
88
+ }
89
+ return errors;
90
+ }
91
+
92
+ return findOutputCollisions( flowDefinition, '' );
93
+ }; // collectFlowOutputCollisions()
@@ -0,0 +1,9 @@
1
+ export const SIGNATURE_PATTERNS = Object.create( null );
2
+ SIGNATURE_PATTERNS.nameLogic = 'NAME_LOGIC';
3
+ SIGNATURE_PATTERNS.namePredicateOutputsOptions = 'NAME_PREDICATE_OUTPUTS_OPTIONS';
4
+ SIGNATURE_PATTERNS.namePredicateOptions = 'NAME_PREDICATE_OPTIONS';
5
+ SIGNATURE_PATTERNS.nameXOutputsOptions = 'NAME_X_OUTPUTS_OPTIONS';
6
+ SIGNATURE_PATTERNS.nameXYOutputsOptions = 'NAME_X_Y_OUTPUTS_OPTIONS';
7
+ SIGNATURE_PATTERNS.nameSugarOutputsOptions = 'NAME_SUGAR_OUTPUTS_OPTIONS';
8
+ SIGNATURE_PATTERNS.nameXOptions = 'NAME_X_OPTIONS';
9
+ SIGNATURE_PATTERNS.unknown = 'UNK_PATTERN';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @fileoverview Deep clone utility for groupBy template expansion.
3
+ *
4
+ * Clones spec objects while preserving:
5
+ * - Functions (predicates, tunables) - kept by reference
6
+ * - RegExp objects - properly cloned
7
+ * - undefined values - preserved
8
+ * - Nested objects/arrays - recursively cloned
9
+ *
10
+ * Used during `.endGroup()` to create independent spec copies for each
11
+ * group value without sharing mutable state.
12
+ */
13
+
14
+ /**
15
+ * Deep clones a value, preserving functions by reference.
16
+ *
17
+ * @param {*} value - Value to clone
18
+ * @returns {*} Cloned value (functions returned by reference)
19
+ */
20
+ const deepCloneValue = function ( value ) {
21
+ // Null and undefined pass through
22
+ if ( value === null || value === undefined ) {
23
+ return value;
24
+ }
25
+
26
+ const type = typeof value;
27
+
28
+ // Functions - keep reference (tunables, predicates)
29
+ if ( type === 'function' ) {
30
+ return value;
31
+ }
32
+
33
+ // Primitives (string, number, boolean, symbol, bigint)
34
+ if ( type !== 'object' ) {
35
+ return value;
36
+ }
37
+
38
+ // RegExp - clone properly
39
+ if ( value instanceof RegExp ) {
40
+ // eslint-disable-next-line security/detect-non-literal-regexp -- cloning an existing RegExp; source/flags come from the object being cloned, not user input
41
+ return new RegExp( value.source, value.flags );
42
+ }
43
+
44
+ // Arrays - recurse
45
+ if ( Array.isArray( value ) ) {
46
+ const result = [];
47
+ for ( let i = 0; i < value.length; i += 1 ) {
48
+ result.push( deepCloneValue( value[ i ] ) );
49
+ }
50
+ return result;
51
+ }
52
+
53
+ // Objects - use null prototype for safety
54
+ const result = Object.create( null );
55
+ const keys = Object.keys( value );
56
+ for ( let i = 0; i < keys.length; i += 1 ) {
57
+ const key = keys[ i ];
58
+ result[ key ] = deepCloneValue( value[ key ] );
59
+ }
60
+
61
+ return result;
62
+ };
63
+
64
+ export { deepCloneValue };
@@ -0,0 +1,171 @@
1
+ // flow/driver.js
2
+
3
+ /**
4
+ * @fileoverview Headless-flow driver.
5
+ *
6
+ * A driver feeds a running flow that has no source. It plays the role a source
7
+ * adapter plays: it calls `handle.processMessage( msg )` once per message. It
8
+ * exists so callers get that contract right in one place — the sync-first call
9
+ * (ADR-013), awaiting only on the rare yield tick, and routing a node fault to a
10
+ * handler so one bad message cannot stop the loop.
11
+ *
12
+ * Hot-path allocation: none. A successful message allocates nothing. `feedOne`
13
+ * returns `undefined` (it is not an async function, so no Promise wrapper).
14
+ * `feedAll` keeps primitive counters and iterates a sync source with `for...of`
15
+ * (no per-item await). Faults route to a callback rather than a per-message
16
+ * result object. Promises are allocated only on the rare yield tick (ADR-013,
17
+ * ADR-024) and on faults — never on the success path (ADR-004 zero-allocation
18
+ * hot path). The yield-tick Promise arrives AFTER the message is fully
19
+ * processed (ADR-024 process-then-breathe), so ignoring it never delays or
20
+ * reorders a message; awaiting it is purely the event-loop breath.
21
+ *
22
+ * What it does NOT do: detect a silently dropped message. A message dropped for
23
+ * an over-cap asset id, or a missing assetId field, returns nothing from
24
+ * `processMessage`, exactly as a success does — so `feedAll` counts it as
25
+ * processed. See `docs/handbook/headless-flow.md`.
26
+ *
27
+ * @see docs/handbook/headless-flow.md
28
+ */
29
+
30
+ /**
31
+ * Default fault handler. Surfaces every fault so none is silent. Allocates only
32
+ * when a fault occurs (rare), never on the success path. Provide your own
33
+ * `onError` to route or throttle in production.
34
+ *
35
+ * @param {Error} error - The node fault (wrapped: `error.nodeModule`,
36
+ * `error.cause`).
37
+ * @returns {void}
38
+ */
39
+ const logFault = function ( error ) {
40
+ console.error( `composer/headlessDriver: a message failed — ${error.message}` );
41
+ }; // logFault()
42
+
43
+ /**
44
+ * Create a driver bound to one headless flow handle.
45
+ *
46
+ * @param {Object} handle - Handle returned by `flow(...).run()`. Must expose a
47
+ * `processMessage` function.
48
+ * @param {Object} [opts={}] - Options.
49
+ * @param {function(Error, Object): void} [opts.onError] - Called as
50
+ * `onError( error, msg )` for every message that faults, in both `feedOne` and
51
+ * `feedAll`. Defaults to logging the fault to the console.
52
+ * @returns {{feedOne: function, feedAll: function}} The driver.
53
+ * @throws {TypeError} When `handle` has no `processMessage`, or `onError` is
54
+ * given but is not a function.
55
+ */
56
+ const headlessDriver = function ( handle, opts = {} ) {
57
+ if ( !handle || typeof handle.processMessage !== 'function' ) {
58
+ throw new TypeError(
59
+ 'composer/headlessDriver: handle must be the object returned by ' +
60
+ 'flow(...).run() — it has no processMessage() function'
61
+ );
62
+ }
63
+
64
+ const onError = opts.onError === undefined ? logFault : opts.onError;
65
+ if ( typeof onError !== 'function' ) {
66
+ throw new TypeError(
67
+ 'composer/headlessDriver: onError must be a function'
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Feed one message. Use this for push sources, where an event hands you a
73
+ * message and there is no loop to own.
74
+ *
75
+ * Zero allocation on the success path: returns `undefined` synchronously. A
76
+ * fault is routed to `onError`, never thrown, so the caller's event handler
77
+ * is never interrupted. On the rare yield tick it returns a Promise you MAY
78
+ * await for backpressure; that Promise never rejects (a yield-path fault
79
+ * still routes to `onError`).
80
+ *
81
+ * @param {Object} msg - The message to feed.
82
+ * @returns {(undefined|Promise<void>)} `undefined` on the hot path; a Promise
83
+ * on the yield tick.
84
+ */
85
+ const feedOne = function ( msg ) {
86
+ let pending;
87
+ try {
88
+ pending = handle.processMessage( msg );
89
+ } catch ( error ) {
90
+ onError( error, msg );
91
+ return undefined;
92
+ }
93
+ if ( pending instanceof Promise ) {
94
+ return pending.catch( function ( error ) {
95
+ onError( error, msg );
96
+ } );
97
+ }
98
+ return undefined;
99
+ }; // feedOne()
100
+
101
+ /**
102
+ * Feed every message from a source, in order. Use this for pull sources,
103
+ * where you iterate the data.
104
+ *
105
+ * Consumes any sync or async iterable — an array, a generator, an async
106
+ * generator, or a Node object-mode stream. A sync source is iterated with
107
+ * `for...of`, so it allocates nothing per message; an async source is pulled
108
+ * with `for await`, where a Promise per item is inherent. Each message is
109
+ * processed and awaited before the next begins, so the flow sees one message
110
+ * at a time and an async source throttles itself. A faulting message does
111
+ * not stop the stream: it is counted in `failed` and routed to `onError`.
112
+ *
113
+ * A failure of the source iterator itself (a stream that errors mid-read) is
114
+ * not caught here — it rejects the returned Promise for the caller to handle.
115
+ *
116
+ * @param {Iterable|AsyncIterable} source - Messages to feed, in order.
117
+ * @returns {Promise<{processed: number, failed: number}>} Counts taken after
118
+ * the source is exhausted.
119
+ * @throws {TypeError} When `source` is not a sync or async iterable.
120
+ */
121
+ const feedAll = async function ( source ) {
122
+ const isAsync = typeof source?.[ Symbol.asyncIterator ] === 'function';
123
+ const isSync = typeof source?.[ Symbol.iterator ] === 'function';
124
+ if ( !isAsync && !isSync ) {
125
+ throw new TypeError(
126
+ 'composer/headlessDriver.feedAll: source must be a sync or async ' +
127
+ 'iterable (an array, a generator, or a stream)'
128
+ );
129
+ }
130
+
131
+ let processed = 0;
132
+ let failed = 0;
133
+
134
+ // Two loops on purpose: `for await` over a sync source would allocate a
135
+ // Promise per item (it awaits each value). The sync branch uses `for...of`
136
+ // and awaits only on the rare yield tick, keeping the hot path alloc-free.
137
+ if ( isAsync ) {
138
+ for await ( const msg of source ) {
139
+ try {
140
+ const pending = handle.processMessage( msg );
141
+ if ( pending instanceof Promise ) {
142
+ await pending;
143
+ }
144
+ processed += 1;
145
+ } catch ( error ) {
146
+ failed += 1;
147
+ onError( error, msg );
148
+ }
149
+ }
150
+ } else {
151
+ for ( const msg of source ) {
152
+ try {
153
+ const pending = handle.processMessage( msg );
154
+ if ( pending instanceof Promise ) {
155
+ await pending; // eslint-disable-line no-await-in-loop
156
+ }
157
+ processed += 1;
158
+ } catch ( error ) {
159
+ failed += 1;
160
+ onError( error, msg );
161
+ }
162
+ }
163
+ }
164
+
165
+ return { processed, failed };
166
+ }; // feedAll()
167
+
168
+ return { feedOne, feedAll };
169
+ }; // headlessDriver()
170
+
171
+ export { headlessDriver };
@@ -0,0 +1,76 @@
1
+ import { SIGNATURE_PATTERNS } from './consts.js';
2
+
3
+ const expandXOutputsOptions = function ( meta, nodeArgs, flowState ) {
4
+ const { isNodeNameDuplicate } = flowState;
5
+ const specs = [];
6
+ const seenX = Object.create( null );
7
+ const [ baseName, inputs, outputs, ...balanceArgs ] = nodeArgs;
8
+ // const inputs = args[ 1 ];
9
+ // const outputs = args[ 2 ]
10
+
11
+ for ( let i = 0; i < inputs.length; i += 1 ) {
12
+ const param = inputs[ i ];
13
+
14
+ if ( seenX[ param ] === 1 ) {
15
+ throw new Error( `WinkComposer/flow: duplicate input found: '${param}'.` );
16
+ }
17
+ seenX[ param ] = 1;
18
+
19
+ const instName = `${baseName}_${param}`;
20
+ if ( isNodeNameDuplicate( instName ) ) throw Error( `WinkComposer/flow: duplicate node ${instName} found.` );
21
+ const stats = Object.create( null );
22
+ for ( const st in outputs ) {
23
+ if (Object.prototype.hasOwnProperty.call( outputs, st ) ) {
24
+ const stName = outputs[ st ];
25
+ stats[ st ] = Object.create( null );
26
+ // Fixed fan naming: ${field}_${label}. The same rule forEach uses, so
27
+ // array sugar and forEach name outputs identically. (Overwrites on one
28
+ // path are caught by the build-time guard, check-output-collisions.js.)
29
+ stats[ st ].storeAs = `${param}_${stName}`;
30
+ }
31
+ }
32
+
33
+ const spec = meta.buildSpec( instName, param, stats, ...balanceArgs );
34
+ specs.push( spec );
35
+ }
36
+
37
+ return specs;
38
+ }; // expandXOutputsOptions()
39
+
40
+ const expandXOptions = function ( meta, nodeArgs, flowState ) {
41
+ const { isNodeNameDuplicate } = flowState;
42
+ const specs = [];
43
+ const seenX = Object.create( null );
44
+ const [ baseName, inputs, ...balanceArgs ] = nodeArgs;
45
+
46
+ for ( let i = 0; i < inputs.length; i += 1 ) {
47
+ const param = inputs[ i ];
48
+
49
+ if ( seenX[ param ] === 1 ) {
50
+ throw new Error( `WinkComposer/flow: duplicate input found: '${param}'.` );
51
+ }
52
+ seenX[ param ] = 1;
53
+
54
+ const instName = `${baseName}_${param}`;
55
+ if ( isNodeNameDuplicate( instName ) ) throw Error( `WinkComposer/flow: duplicate node ${instName} found.` );
56
+
57
+ const spec = meta.buildSpec( instName, param, ...balanceArgs );
58
+ specs.push( spec );
59
+ }
60
+
61
+ return specs;
62
+ }; // expandXOptions()
63
+
64
+ /**
65
+ * Expand array sugar for single-input nodes into multiple canonical calls.
66
+ * Assumes canonical signature: buildSpec( name, x, storeAs, options )
67
+ * - name is made unique per input as `${baseName}_${param}`
68
+ * - storeAs follows the fixed fan rule `${param}_${stat}` (same as forEach)
69
+ * Returns an array of per-input specs (throws on invalid inputs).
70
+ *
71
+ * nodeArgs: baseName, inputs, outputs, balance stuff
72
+ */
73
+ export const expandSingleInputArraySugar = function ( meta, nodeArgs, flowState, pattern ) {
74
+ if ( pattern === SIGNATURE_PATTERNS.nameXOptions ) return expandXOptions( meta, nodeArgs, flowState );
75
+ return expandXOutputsOptions( meta, nodeArgs, flowState );
76
+ }; // expandSingleInputArraySugar()
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @fileoverview Template spec expansion for groupBy construct.
3
+ *
4
+ * Expands a template spec for a specific group value by:
5
+ * 1. Deep cloning the template (preserving functions)
6
+ * 2. Prefixing the node name with group value
7
+ * 3. Resolving matching tunables to concrete values
8
+ * 4. Prefixing trigger targets with group value
9
+ *
10
+ * Note: storeAs values are NOT prefixed. Output field names remain the same
11
+ * across all groups since each case is an isolated pipeline path.
12
+ *
13
+ * @see ADR-007: groupBy DSL Construct
14
+ */
15
+
16
+ import { deepCloneValue } from './deep-clone-spec.js';
17
+ import { resolveTunablesInValue } from './resolve-tunables.js';
18
+
19
+ /**
20
+ * Prefixes trigger target names with the group value.
21
+ *
22
+ * Handles:
23
+ * - Direct triggers: spec.triggers[].targets
24
+ * - Controller logic triggers: spec.logic[].triggers[].targets
25
+ *
26
+ * @param {Object} spec - The spec to process (mutated in place)
27
+ * @param {string|number} groupValue - Prefix to add
28
+ */
29
+ const prefixTriggerTargets = function ( spec, groupValue ) {
30
+ const prefixTargets = function ( triggers ) {
31
+ if ( !Array.isArray( triggers ) ) {
32
+ return;
33
+ }
34
+
35
+ for ( let i = 0; i < triggers.length; i += 1 ) {
36
+ const trigger = triggers[ i ];
37
+ if ( Array.isArray( trigger.targets ) ) {
38
+ trigger.targets = trigger.targets.map(
39
+ ( target ) => `${groupValue}_${target}`
40
+ );
41
+ }
42
+ }
43
+ };
44
+
45
+ // Direct triggers on the spec
46
+ prefixTargets( spec.triggers );
47
+
48
+ // Controller node logic triggers
49
+ if ( Array.isArray( spec.logic ) ) {
50
+ for ( let j = 0; j < spec.logic.length; j += 1 ) {
51
+ prefixTargets( spec.logic[ j ].triggers );
52
+ }
53
+ }
54
+ };
55
+
56
+ /**
57
+ * Expands a template spec for a specific group value.
58
+ *
59
+ * Performs:
60
+ * 1. Deep clone (preserves functions by reference)
61
+ * 2. Prefix node name: 'corr' → 'idle_corr'
62
+ * 3. Resolve matching tunables: lookupByField('rpmBand', {idle: 3.4}) → 3.4
63
+ * 4. Prefix trigger targets: ['corr', 'ph'] → ['idle_corr', 'idle_ph']
64
+ *
65
+ * Note: storeAs values are NOT prefixed. Each group writes to the same
66
+ * output field names since they run in isolated pipeline paths.
67
+ *
68
+ * @param {Object} templateSpec - Original template spec
69
+ * @param {string|number} groupValue - Group value for this expansion
70
+ * @param {string} groupByField - The field used for grouping
71
+ * @returns {Object} Expanded spec ready for the group's case
72
+ */
73
+ const expandTemplateSpec = function ( templateSpec, groupValue, groupByField ) {
74
+ // Step 1: Deep clone (preserves functions by reference)
75
+ const spec = deepCloneValue( templateSpec );
76
+
77
+ // Step 2: Prefix node name
78
+ spec.name = `${groupValue}_${spec.name}`;
79
+
80
+ // Step 3: Resolve tunables (returns new object with resolved values)
81
+ const resolved = resolveTunablesInValue( spec, groupByField, groupValue );
82
+
83
+ // Step 4: Prefix trigger targets (mutates in place)
84
+ prefixTriggerTargets( resolved, groupValue );
85
+
86
+ // Note: storeAs values are intentionally NOT prefixed
87
+ // Each group outputs to the same field names (e.g., 'r2', 'shiftDetected')
88
+ // This is correct because each case is an isolated pipeline path
89
+
90
+ return resolved;
91
+ };
92
+
93
+ export { expandTemplateSpec, prefixTriggerTargets };