@winkjs/composer 0.0.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +42 -103
  3. package/package.json +53 -29
  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,658 @@
1
+ // flow/flow.js
2
+
3
+ /**
4
+ * @fileoverview Composable DSL for streaming analytics pipelines.
5
+ *
6
+ * Supports two modes:
7
+ * - Transpilation: `.build()` returns executable JS source code
8
+ * - Direct execution: `.run()` wires and starts the pipeline
9
+ *
10
+ * Features:
11
+ * - Fluent chainable API for node composition
12
+ * - Array sugar for single-input nodes (auto-generates storeAs as ${field}_${label})
13
+ * - Lazy module loading at run time
14
+ * - Switch/case/break syntax for multiple specialized pipelines
15
+ *
16
+ * State Machine:
17
+ * - FLOW (config): Config methods + nodes + .switch() + terminals allowed
18
+ * - FLOW (pipeline): Nodes + .switch() + terminals allowed (config blocked)
19
+ * - SWITCH: Only .case() allowed
20
+ * - CASE: Node methods + .break() allowed
21
+ * - BREAK: .case() + terminals allowed
22
+ */
23
+
24
+ import * as nodes from '../nodes/index.js';
25
+ import { makeCollisionChecker } from '../core/utils/flow/index.js';
26
+ import { serializeModule } from './serialize.js';
27
+ import { makeNodeMethod } from './make-node-method.js';
28
+ import { makeForEach } from './for-each.js';
29
+ import { runFlow } from './run.js';
30
+ import { validateFlow } from './validate.js';
31
+ import { inspectFlow } from './inspect.js';
32
+ import { loadNodeModules } from './load-node-module.js';
33
+ import { collectFlowOutputCollisions } from './check-output-collisions.js';
34
+ import {
35
+ assetIdSchema,
36
+ yieldSchema,
37
+ switchSchema,
38
+ groupBySchema,
39
+ validateFlowConfig
40
+ } from './flow-api-schemas.js';
41
+ import { validateWithSchema } from '../core/utils/validate/index.js';
42
+ // Asset-class validation goes through the deep semantics schema directly.
43
+ // A shallow flow-side wrapper used to live in flow-api-schemas.js; it was
44
+ // removed so both entry paths (loadSemantics() and hand-construction via
45
+ // .assetClass()) validate against the same deep schema — a single source
46
+ // of truth for what makes an asset class valid.
47
+ import { assetClassSchema } from '../core/semantics/schemas/index.js';
48
+ import { expandTemplateSpec } from './expand-template-spec.js';
49
+
50
+ const flow = function ( flowName = 'flow1' ) {
51
+ const api = Object.create( null );
52
+
53
+ const flowDefinition = [];
54
+ const importSet = new Set();
55
+ const isNodeNameDuplicate = makeCollisionChecker();
56
+
57
+ // Runtime configuration (for .run() mode)
58
+ const runtime = {
59
+ source: null,
60
+ emitters: Object.create( null ),
61
+ storages: Object.create( null ),
62
+ partitionField: null,
63
+ specializationField: null,
64
+ yieldThreshold: null,
65
+ assetClass: null
66
+ };
67
+
68
+ // Switch/case/break state machine
69
+ const switchState = {
70
+ active: false, // true once .switch() is called
71
+ currentCase: null, // key of the case currently being defined
72
+ caseSpecs: Object.create( null ), // { [caseKey]: specArray }
73
+ caseOrder: [], // preserves declaration order of cases
74
+ caseEnded: false // true after .break(), false after .case()
75
+ };
76
+
77
+ // GroupBy state machine (syntactic sugar for switch/case)
78
+ const groupByState = {
79
+ active: false, // true after .groupBy(), false after .endGroup()
80
+ field: null, // grouping field (e.g., 'rpmBand')
81
+ groupValues: [], // group values (e.g., ['idle', 'low', 'cruise'])
82
+ templateSpecs: [], // template specs collected before expansion
83
+ templateNodeChecker: null, // local collision checker for template
84
+ expanded: false // true after .endGroup() expansion
85
+ };
86
+
87
+ // Track if pipeline phase has started (for config-first enforcement)
88
+ let pipelineStarted = false;
89
+
90
+ // Helper: Enforce config methods come before any nodes or .switch()/.groupBy()
91
+ const enforceConfigFirst = function ( methodName ) {
92
+ if ( pipelineStarted ) {
93
+ throw new Error(
94
+ `WinkComposer/flow: .${methodName}() must be called before any nodes or .switch()/.groupBy()`
95
+ );
96
+ }
97
+ };
98
+
99
+ // Helper: Validate switch is complete before terminal methods
100
+ const validateSwitchComplete = function () {
101
+ if ( switchState.active ) {
102
+ if ( switchState.caseOrder.length === 0 ) {
103
+ throw new Error( 'WinkComposer/flow: .switch() requires at least one .case()' );
104
+ }
105
+ if ( !switchState.caseEnded ) {
106
+ throw new Error(
107
+ `WinkComposer/flow: case '${switchState.currentCase}' must end with .break()`
108
+ );
109
+ }
110
+ }
111
+ };
112
+
113
+ // Output-field overwrite guard: two nodes on one runtime path must not write the
114
+ // same message field, or the second silently overwrites the first. Collisions are
115
+ // collected per path (the linear flow, or one switch case), so the same field across
116
+ // sibling cases stays allowed - only one case runs per message.
117
+ const assertNoOutputCollisions = function () {
118
+ const collisions = collectFlowOutputCollisions( switchState, flowDefinition );
119
+ if ( collisions.length > 0 ) {
120
+ throw new Error(
121
+ 'WinkComposer/flow: output field collision(s) - two nodes on one path ' +
122
+ 'write the same message field:\n - ' + collisions.join( '\n - ' )
123
+ );
124
+ }
125
+ };
126
+
127
+ // Discover node factories exposing DSL metadata
128
+ const nodeNames = Object.keys( nodes )
129
+ .filter( ( n ) => ( typeof nodes[ n ]?.getDSLMetadata === 'function' ) )
130
+ .sort();
131
+
132
+ // Bind handlers via factory to avoid loop-closure issues
133
+ // Note: switchState, groupByState, and markPipelineStarted are passed for multi-specialization routing
134
+ const markPipelineStarted = function () {
135
+ pipelineStarted = true;
136
+ };
137
+ const flowState = {
138
+ flowDefinition,
139
+ importSet,
140
+ isNodeNameDuplicate,
141
+ switchState,
142
+ groupByState,
143
+ markPipelineStarted
144
+ };
145
+ for ( let i = 0; i < nodeNames.length; i += 1 ) {
146
+ const node = nodeNames[ i ];
147
+ const meta = nodes[ node ].getDSLMetadata();
148
+ api[ node ] = makeNodeMethod( api, node, meta, flowState );
149
+ }
150
+
151
+ // The chain fan: fans a multi-node chain across a field list (the multi-node
152
+ // sibling of array sugar). Wired after the per-node methods so it shares the
153
+ // same flowState (definitions, checkers, router).
154
+ api.forEach = makeForEach( api, flowState );
155
+
156
+ // ========================================================================
157
+ // RUNTIME CONFIGURATION METHODS (chainable)
158
+ // ========================================================================
159
+
160
+ api.source = function ( adapter, sourceConfig = {} ) {
161
+ enforceConfigFirst( 'source' );
162
+ if ( typeof adapter !== 'object' || adapter === null ) {
163
+ throw new Error( 'WinkComposer/flow: source adapter must be an imported module' );
164
+ }
165
+ if ( typeof adapter.start !== 'function' ) {
166
+ throw new Error( 'WinkComposer/flow: source adapter must have a start() function' );
167
+ }
168
+
169
+ // If the adapter provides a configSchema, we validate config here.
170
+ // This is fail-fast: bad config is rejected when the flow is defined,
171
+ // not later inside adapter.start().
172
+ if ( adapter.configSchema ) {
173
+ const validation = validateWithSchema( adapter.configSchema, sourceConfig, 'source' );
174
+ validation.throwIfInvalid( `flow/source:${adapter.id || 'unknown'}` );
175
+ }
176
+
177
+ runtime.source = { adapter, config: sourceConfig };
178
+ return api;
179
+ };
180
+
181
+ api.emitter = function ( adapter, emitterConfig = {} ) {
182
+ enforceConfigFirst( 'emitter' );
183
+ if ( typeof adapter !== 'object' || adapter === null ) {
184
+ throw new Error( 'WinkComposer/flow: emitter adapter must be an imported module' );
185
+ }
186
+ if ( typeof adapter.id !== 'string' || !adapter.id ) {
187
+ throw new Error( 'WinkComposer/flow: emitter adapter must have an id' );
188
+ }
189
+ if ( typeof adapter.createEmitter !== 'function' ) {
190
+ throw new Error( 'WinkComposer/flow: emitter adapter must have a createEmitter() function' );
191
+ }
192
+
193
+ // If the adapter provides a configSchema, we validate config here.
194
+ // This is fail-fast: bad config is rejected when the flow is defined,
195
+ // not later inside adapter.createEmitter().
196
+ if ( adapter.configSchema ) {
197
+ const validation = validateWithSchema( adapter.configSchema, emitterConfig, 'emitter' );
198
+ validation.throwIfInvalid( `flow/emitter:${adapter.id}` );
199
+ }
200
+
201
+ runtime.emitters[ adapter.id ] = { adapter, config: emitterConfig };
202
+ return api;
203
+ };
204
+
205
+ api.storage = function ( adapter, storageConfig = {} ) {
206
+ enforceConfigFirst( 'storage' );
207
+ if ( typeof adapter !== 'object' || adapter === null ) {
208
+ throw new Error( 'WinkComposer/flow: storage adapter must be an imported module' );
209
+ }
210
+ if ( typeof adapter.id !== 'string' || !adapter.id ) {
211
+ throw new Error( 'WinkComposer/flow: storage adapter must have an id' );
212
+ }
213
+ if ( typeof adapter.createStorage !== 'function' ) {
214
+ throw new Error( 'WinkComposer/flow: storage adapter must have a createStorage() function' );
215
+ }
216
+
217
+ // If the adapter provides a configSchema, we validate config here.
218
+ // This is fail-fast: bad config is rejected when the flow is defined,
219
+ // not later inside adapter.createStorage().
220
+ if ( adapter.configSchema ) {
221
+ const validation = validateWithSchema( adapter.configSchema, storageConfig, 'storage' );
222
+ validation.throwIfInvalid( `flow/storage:${adapter.id}` );
223
+ }
224
+
225
+ runtime.storages[ adapter.id ] = { adapter, config: storageConfig };
226
+ return api;
227
+ };
228
+
229
+ api.assetId = function ( field ) {
230
+ enforceConfigFirst( 'assetId' );
231
+
232
+ if ( typeof field !== 'string' || field.length === 0 ) {
233
+ throw new Error( 'WinkComposer/flow: .assetId() requires a non-empty string field name' );
234
+ }
235
+
236
+ // Schema-based validation for robust error handling
237
+ const validation = validateFlowConfig( { field }, assetIdSchema, 'assetId' );
238
+ validation.throwIfInvalid();
239
+
240
+ runtime.partitionField = field;
241
+ return api;
242
+ };
243
+
244
+ api.assetClass = function ( assetClassDef ) {
245
+ enforceConfigFirst( 'assetClass' );
246
+
247
+ // Pre-check: must be an object before we hand it to the schema.
248
+ // The deep semantics schema's first-tier validators expect to walk
249
+ // properties (`name`, `columns`, `insightTypes`); a non-object
250
+ // would crash inside the schema with a less helpful message.
251
+ if ( assetClassDef === null || assetClassDef === undefined || typeof assetClassDef !== 'object' ) {
252
+ const actualType = assetClassDef === null ? 'null' : typeof assetClassDef;
253
+ throw new Error(
254
+ `WinkComposer/flow.assetClass: Expected object, got ${actualType}`
255
+ );
256
+ }
257
+
258
+ // Validate against the deep semantics schema directly.
259
+ // This is the same schema the semantics loader runs against JSON
260
+ // files, so both entry paths — loadSemantics() and hand-construction
261
+ // via .assetClass() — agree on what counts as a valid asset class.
262
+ // Closes the Path 1B silent-fail surface where a hand-constructed
263
+ // asset class missing column types or with malformed columns would
264
+ // previously slip through the shallow flow-side validator and
265
+ // surface much later as silent VARCHAR coercion in QuestDB.
266
+ const validation = validateWithSchema( assetClassSchema, assetClassDef, 'assetClass' );
267
+ if ( !validation.valid ) {
268
+ throw new Error(
269
+ `WinkComposer/flow.assetClass: validation failed:\n - ${validation.errors.join( '\n - ' )}`
270
+ );
271
+ }
272
+
273
+ runtime.assetClass = assetClassDef;
274
+ return api;
275
+ };
276
+
277
+ api.yield = function ( options ) {
278
+ enforceConfigFirst( 'yield' );
279
+
280
+ // Schema-based validation for robust error handling
281
+ const validation = validateFlowConfig( options, yieldSchema, 'yield' );
282
+ validation.throwIfInvalid();
283
+
284
+ runtime.yieldThreshold = options.threshold;
285
+ return api;
286
+ };
287
+
288
+ // ========================================================================
289
+ // SWITCH/CASE/BREAK METHODS (State Machine for Multi-Specialization)
290
+ // ========================================================================
291
+
292
+ /**
293
+ * Enters SWITCH state. Defines the field for specialization routing.
294
+ * After .switch(), only .case() is allowed.
295
+ *
296
+ * @param {string} field - Message field name for specialization routing
297
+ * @returns {Object} API object for chaining (only .case() available)
298
+ */
299
+ api.switch = function ( field ) {
300
+ if ( typeof field !== 'string' || field.length === 0 ) {
301
+ throw new Error( 'WinkComposer/flow: .switch() requires a non-empty string field name' );
302
+ }
303
+
304
+ // Schema-based validation: field must be a valid identifier (mirrors .assetId())
305
+ const validation = validateFlowConfig( { field }, switchSchema, 'switch' );
306
+ validation.throwIfInvalid();
307
+
308
+ // Check groupBy before switchState (endGroup sets switchState.active)
309
+ if ( groupByState.active || groupByState.expanded ) {
310
+ throw new Error( 'WinkComposer/flow: .switch() cannot be used with .groupBy()' );
311
+ }
312
+ if ( switchState.active ) {
313
+ throw new Error( 'WinkComposer/flow: .switch() can only be called once per flow' );
314
+ }
315
+ if ( flowDefinition.length > 0 ) {
316
+ throw new Error(
317
+ 'WinkComposer/flow: .switch() cannot be called after nodes - ' +
318
+ 'use .switch() before defining any nodes, or omit .switch() for single-pipeline flows'
319
+ );
320
+ }
321
+
322
+ // Validate field conflict with partition
323
+ if ( runtime.partitionField === field ) {
324
+ throw new Error( 'WinkComposer/flow: partition field and switch field must be different' );
325
+ }
326
+
327
+ runtime.specializationField = field;
328
+ switchState.active = true;
329
+ pipelineStarted = true;
330
+
331
+ return api;
332
+ };
333
+
334
+ /**
335
+ * Enters CASE state. Creates a new specialization pipeline.
336
+ * After .case(), node methods and .break() are allowed.
337
+ *
338
+ * @param {string|number} key - Unique identifier for this case
339
+ * @returns {Object} API object for chaining (nodes + .break() available)
340
+ */
341
+ api.case = function ( key ) {
342
+ if ( !switchState.active ) {
343
+ throw new Error( 'WinkComposer/flow: .case() requires an active .switch()' );
344
+ }
345
+ if ( switchState.currentCase !== null && !switchState.caseEnded ) {
346
+ throw new Error(
347
+ `WinkComposer/flow: previous case '${switchState.currentCase}' must end with .break() before starting a new case`
348
+ );
349
+ }
350
+ if ( switchState.caseSpecs[ key ] !== undefined ) {
351
+ throw new Error( `WinkComposer/flow: duplicate case key '${key}'` );
352
+ }
353
+
354
+ switchState.currentCase = key;
355
+ switchState.caseEnded = false;
356
+ switchState.caseSpecs[ key ] = [];
357
+ switchState.caseOrder.push( key );
358
+
359
+ return api;
360
+ };
361
+
362
+ /**
363
+ * Exits CASE state, enters BREAK state.
364
+ * After .break(), .case() or terminal methods are allowed.
365
+ *
366
+ * @returns {Object} API object for chaining (.case() + terminals available)
367
+ */
368
+ api.break = function () {
369
+ if ( !switchState.active ) {
370
+ throw new Error( 'WinkComposer/flow: .break() requires an active .switch()' );
371
+ }
372
+ if ( switchState.currentCase === null || switchState.caseEnded ) {
373
+ throw new Error( 'WinkComposer/flow: .break() must follow a .case() with at least one node' );
374
+ }
375
+ if ( switchState.caseSpecs[ switchState.currentCase ].length === 0 ) {
376
+ throw new Error(
377
+ `WinkComposer/flow: case '${switchState.currentCase}' is empty - add at least one node before .break()`
378
+ );
379
+ }
380
+
381
+ switchState.caseEnded = true;
382
+
383
+ return api;
384
+ };
385
+
386
+ // ========================================================================
387
+ // GROUPBY/ENDGROUP METHODS (Syntactic Sugar for Multi-Specialization)
388
+ // ========================================================================
389
+
390
+ /**
391
+ * Enters GROUPBY state. Collects template nodes for expansion.
392
+ * After .groupBy(), node methods and .endGroup() are allowed.
393
+ *
394
+ * groupBy is pure syntactic sugar that expands to switch/case at build time.
395
+ * It provides a concise way to define identical pipelines with parameter variations.
396
+ *
397
+ * @param {string} field - Message field for grouping (e.g., 'rpmBand')
398
+ * @param {Array<string|number>} values - Group values to expand (e.g., ['idle', 'low', 'cruise'])
399
+ * @returns {Object} API object for chaining (nodes + .endGroup() available)
400
+ *
401
+ * @example
402
+ * .groupBy( 'rpmBand', [ 'idle', 'low', 'cruise' ] )
403
+ * .pageHinkley( 'ph', 'r2', { phShift: 'shift' }, {
404
+ * lambda: lookupByField( 'rpmBand', { idle: 3.4, low: 3.2, cruise: 2.4 } )
405
+ * })
406
+ * .endGroup()
407
+ */
408
+ api.groupBy = function ( field, values ) {
409
+ // Validation: field must be non-empty string
410
+ if ( typeof field !== 'string' || field.length === 0 ) {
411
+ throw new Error( 'WinkComposer/flow: .groupBy() requires a non-empty string field name' );
412
+ }
413
+
414
+ // Schema-based validation: field must be a valid identifier (mirrors .assetId())
415
+ const validation = validateFlowConfig( { field }, groupBySchema, 'groupBy' );
416
+ validation.throwIfInvalid();
417
+
418
+ // Validation: values must be array with at least 2 elements
419
+ if ( !Array.isArray( values ) || values.length < 2 ) {
420
+ throw new Error( 'WinkComposer/flow: .groupBy() requires at least 2 group values' );
421
+ }
422
+
423
+ // Validation: each value must be string or number
424
+ for ( let i = 0; i < values.length; i += 1 ) {
425
+ const v = values[ i ];
426
+ if ( typeof v !== 'string' && typeof v !== 'number' ) {
427
+ throw new Error( `WinkComposer/flow: group value at index ${i} must be a string or number` );
428
+ }
429
+ }
430
+
431
+ // Validation: no duplicate values
432
+ const seen = Object.create( null );
433
+ for ( let i = 0; i < values.length; i += 1 ) {
434
+ const v = values[ i ];
435
+ if ( seen[ v ] ) {
436
+ throw new Error( `WinkComposer/flow: duplicate group value '${v}' in .groupBy()` );
437
+ }
438
+ seen[ v ] = 1;
439
+ }
440
+
441
+ // Validation: cannot call groupBy twice (check this first - user intent is to reuse groupBy)
442
+ if ( groupByState.active || groupByState.expanded ) {
443
+ throw new Error( 'WinkComposer/flow: .groupBy() can only be called once per flow' );
444
+ }
445
+
446
+ // Validation: cannot use groupBy if switchState is active (manual switch, not from endGroup)
447
+ if ( switchState.active ) {
448
+ throw new Error( 'WinkComposer/flow: .groupBy() cannot be used inside .switch()' );
449
+ }
450
+
451
+ // Validation: must come before any nodes
452
+ if ( flowDefinition.length > 0 ) {
453
+ throw new Error(
454
+ 'WinkComposer/flow: .groupBy() cannot be called after nodes - ' +
455
+ 'use .groupBy() before defining any nodes'
456
+ );
457
+ }
458
+
459
+ // Validation: groupBy field cannot equal partition field
460
+ if ( runtime.partitionField === field ) {
461
+ throw new Error( 'WinkComposer/flow: partition field and groupBy field must be different' );
462
+ }
463
+
464
+ // Initialize groupBy state
465
+ groupByState.active = true;
466
+ groupByState.field = field;
467
+ groupByState.groupValues = values.slice(); // defensive copy
468
+ groupByState.templateSpecs = [];
469
+ groupByState.templateNodeChecker = makeCollisionChecker();
470
+
471
+ pipelineStarted = true;
472
+
473
+ return api;
474
+ };
475
+
476
+ /**
477
+ * Exits GROUPBY state. Expands template specs into switch/case structure.
478
+ *
479
+ * For each group value:
480
+ * 1. Deep clone each template spec
481
+ * 2. Prefix node name: 'corr' → 'idle_corr'
482
+ * 3. Resolve matching tunables: lookupByField('rpmBand', {...}) → concrete value
483
+ * 4. Prefix trigger targets: ['corr'] → ['idle_corr']
484
+ *
485
+ * @returns {Object} API object for chaining (terminals available)
486
+ */
487
+ api.endGroup = function () {
488
+ // Validation: must be in active groupBy state
489
+ if ( !groupByState.active ) {
490
+ throw new Error( 'WinkComposer/flow: .endGroup() requires an active .groupBy()' );
491
+ }
492
+
493
+ // Validation: must have at least one template node
494
+ if ( groupByState.templateSpecs.length === 0 ) {
495
+ throw new Error( 'WinkComposer/flow: .groupBy() must contain at least one node before .endGroup()' );
496
+ }
497
+
498
+ // Set specialization field (same as switch does)
499
+ runtime.specializationField = groupByState.field;
500
+
501
+ // Activate switch state (we're producing switch/case structure)
502
+ switchState.active = true;
503
+
504
+ // Expand templates into switch cases
505
+ for ( let i = 0; i < groupByState.groupValues.length; i += 1 ) {
506
+ const groupValue = groupByState.groupValues[ i ];
507
+
508
+ // Initialize case
509
+ switchState.caseSpecs[ groupValue ] = [];
510
+ switchState.caseOrder.push( groupValue );
511
+
512
+ // Expand each template spec for this group
513
+ for ( let j = 0; j < groupByState.templateSpecs.length; j += 1 ) {
514
+ const templateSpec = groupByState.templateSpecs[ j ];
515
+ const expandedSpec = expandTemplateSpec(
516
+ templateSpec,
517
+ groupValue,
518
+ groupByState.field
519
+ );
520
+ switchState.caseSpecs[ groupValue ].push( expandedSpec );
521
+ }
522
+ }
523
+
524
+ // Mark switch as complete (no more cases allowed)
525
+ switchState.caseEnded = true;
526
+ switchState.currentCase = groupByState.groupValues[ groupByState.groupValues.length - 1 ];
527
+
528
+ // Mark groupBy as complete
529
+ groupByState.active = false;
530
+ groupByState.expanded = true;
531
+
532
+ return api;
533
+ };
534
+
535
+ // ========================================================================
536
+ // TERMINAL METHODS
537
+ // ========================================================================
538
+
539
+ api.build = function () {
540
+ validateSwitchComplete();
541
+ assertNoOutputCollisions();
542
+
543
+ if ( switchState.active ) {
544
+ // Multi-specialization mode
545
+ const imports = Array.from( importSet ).sort();
546
+ return serializeModule( {
547
+ imports,
548
+ flowName,
549
+ specsByCase: switchState.caseSpecs,
550
+ caseOrder: switchState.caseOrder
551
+ } );
552
+ }
553
+
554
+ // Single-pipeline mode (backward compatible)
555
+ if ( flowDefinition.length === 0 ) {
556
+ throw new Error( 'WinkComposer/flow: Cannot build empty flow - add at least one node' );
557
+ }
558
+ const imports = Array.from( importSet ).sort();
559
+ return serializeModule( { imports, flowName, specs: flowDefinition } );
560
+ };
561
+
562
+ api.validate = async function () {
563
+ validateSwitchComplete();
564
+
565
+ // Output-field overwrites surface as validation errors here; build()/run() throw
566
+ // on the same condition. Collected per path, so cross-case reuse is allowed.
567
+ const outputCollisions = collectFlowOutputCollisions( switchState, flowDefinition );
568
+
569
+ // Load node modules for trigger validation (control method validation)
570
+ const importedNodeNames = Array.from( importSet );
571
+ const nodeModulesMap = await loadNodeModules( importedNodeNames );
572
+ const nodeModules = Object.create( null );
573
+ for ( const [ name, mod ] of nodeModulesMap ) {
574
+ nodeModules[ name ] = mod;
575
+ }
576
+
577
+ // Build registered-adapter maps `{ id: module }` for cross-flow
578
+ // target validation AND capability-driven semanticsRequirement
579
+ // checks. runtime.emitters / runtime.storages each
580
+ // store `{ id: { adapter, config } }`; we extract just the
581
+ // adapter modules here so validate.js doesn't need to know about
582
+ // our `{ adapter, config }` wrapper.
583
+ const registeredEmitters = Object.create( null );
584
+ for ( const [ id, { adapter } ] of Object.entries( runtime.emitters ) ) {
585
+ registeredEmitters[ id ] = adapter;
586
+ }
587
+ const registeredStorages = Object.create( null );
588
+ for ( const [ id, { adapter } ] of Object.entries( runtime.storages ) ) {
589
+ registeredStorages[ id ] = adapter;
590
+ }
591
+
592
+ if ( switchState.active ) {
593
+ // Validate all specializations in parallel
594
+ const validationPromises = switchState.caseOrder.map( ( key ) =>
595
+ Promise.resolve( validateFlow( `${flowName}:${key}`, switchState.caseSpecs[ key ], nodeModules, registeredEmitters, registeredStorages, runtime.assetClass ) )
596
+ .then( ( result ) => ( { key, result } ) )
597
+ );
598
+ const results = await Promise.all( validationPromises );
599
+
600
+ const allErrors = [];
601
+ for ( let i = 0; i < results.length; i += 1 ) {
602
+ const { key, result } = results[ i ];
603
+ if ( !result.valid ) {
604
+ allErrors.push( ...result.errors.map( ( e ) => `[case '${key}'] ${e}` ) );
605
+ }
606
+ }
607
+ for ( let i = 0; i < outputCollisions.length; i += 1 ) {
608
+ allErrors.push( outputCollisions[ i ] );
609
+ }
610
+ return {
611
+ valid: allErrors.length === 0,
612
+ errors: allErrors
613
+ };
614
+ }
615
+
616
+ // Single-pipeline mode
617
+ const result = await validateFlow( flowName, flowDefinition, nodeModules, registeredEmitters, registeredStorages, runtime.assetClass );
618
+ if ( outputCollisions.length === 0 ) {
619
+ return result;
620
+ }
621
+ return {
622
+ valid: false,
623
+ errors: result.errors.concat( outputCollisions )
624
+ };
625
+ };
626
+
627
+ api.inspect = function () {
628
+ return inspectFlow( flowName, flowDefinition, importSet, runtime, switchState );
629
+ };
630
+
631
+ api.run = async function () {
632
+ validateSwitchComplete();
633
+ assertNoOutputCollisions();
634
+
635
+ if ( switchState.active ) {
636
+ // Multi-specialization mode
637
+ const handle = await runFlow(
638
+ flowName,
639
+ switchState.caseSpecs,
640
+ importSet,
641
+ runtime,
642
+ switchState.caseOrder
643
+ );
644
+ return handle;
645
+ }
646
+
647
+ // Single-pipeline mode (backward compatible)
648
+ if ( flowDefinition.length === 0 ) {
649
+ throw new Error( 'WinkComposer/flow: Cannot run empty flow - add at least one node' );
650
+ }
651
+ const handle = await runFlow( flowName, flowDefinition, importSet, runtime );
652
+ return handle;
653
+ };
654
+
655
+ return api;
656
+ }; // flow()
657
+
658
+ export { flow };