@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,80 @@
1
+ // core/test-utils/sealed-record.js
2
+
3
+ /**
4
+ * @fileoverview Test harness that proves a sink consumes a record
5
+ * synchronously.
6
+ *
7
+ * ADR-023 obliges every sink to read everything it needs from an incoming
8
+ * message before its hot-path call returns, and never to keep the
9
+ * reference for a later read. With a reused record (the handbook's
10
+ * record-reuse pattern, and the future declarative shaping), a late read
11
+ * would see a LATER message's values — silently corrupted data. This
12
+ * harness makes that rule testable: wrap a record, hand it to the sink,
13
+ * seal it the moment the call returns, and any read arriving after the
14
+ * seal is recorded as a violation.
15
+ *
16
+ * Violations are RECORDED, never thrown. A late read typically happens
17
+ * inside the sink's own async callback; a throw there would surface as an
18
+ * unhandled rejection instead of a clean assertion failure. Tests seal,
19
+ * await a tick, then assert `violations` is empty.
20
+ *
21
+ * Symbol-keyed accesses are ignored on purpose: engines and inspection
22
+ * tools probe symbols (Symbol.toStringTag and friends), while record
23
+ * columns are always string-named. Counting symbol traffic would produce
24
+ * false violations that have nothing to do with data reads.
25
+ */
26
+
27
+ /**
28
+ * Wraps a plain record in a Proxy that logs every read and, once sealed,
29
+ * records further reads as violations.
30
+ *
31
+ * @param {Object} fields - Column values the record carries.
32
+ * @returns {{record: Object, seal: Function, violations: Array, reads: Array}}
33
+ * `record` behaves exactly like `fields`; `seal()` marks the moment the
34
+ * sink's call returned (idempotent); `reads` holds `{trap, key}` entries
35
+ * logged before the seal, `violations` the ones logged after it.
36
+ */
37
+ const makeSealedRecord = function ( fields ) {
38
+ const reads = [];
39
+ const violations = [];
40
+ let sealed = false;
41
+
42
+ const log = function ( trap, key ) {
43
+ if ( typeof key === 'symbol' ) {
44
+ return;
45
+ }
46
+ const entry = { trap, key };
47
+ if ( sealed ) {
48
+ violations.push( entry );
49
+ } else {
50
+ reads.push( entry );
51
+ }
52
+ }; // log()
53
+
54
+ const record = new Proxy( fields, {
55
+ get: function ( target, key, receiver ) {
56
+ log( 'get', key );
57
+ return Reflect.get( target, key, receiver );
58
+ },
59
+ has: function ( target, key ) {
60
+ log( 'has', key );
61
+ return Reflect.has( target, key );
62
+ },
63
+ ownKeys: function ( target ) {
64
+ log( 'ownKeys', null );
65
+ return Reflect.ownKeys( target );
66
+ },
67
+ getOwnPropertyDescriptor: function ( target, key ) {
68
+ log( 'getOwnPropertyDescriptor', key );
69
+ return Reflect.getOwnPropertyDescriptor( target, key );
70
+ }
71
+ } );
72
+
73
+ const seal = function () {
74
+ sealed = true;
75
+ }; // seal()
76
+
77
+ return { record, seal, violations, reads };
78
+ }; // makeSealedRecord()
79
+
80
+ export { makeSealedRecord };
@@ -0,0 +1,149 @@
1
+ // core/test-utils/soak-signature.js
2
+
3
+ /**
4
+ * @fileoverview Release-soak signature policy for the MQTT emitter.
5
+ *
6
+ * The release gate runs the emitter soak (`slow-soak-mqtt-emitter.specs.js`)
7
+ * and has to judge a lossy run. One loss mechanism is documented and
8
+ * reported upstream: mqtt.js's reconnect-clear race — on every mid-run
9
+ * reconnect, `_onConnect` wipes the message-id registrations
10
+ * (client.js:1171) before the store re-drive re-registers them
11
+ * asynchronously (client.js:1223); a publish landing in that window
12
+ * overwrites an unacked store entry. The result, measured at
13
+ * ~12–14k msg/s: 1–6 messages lost per reconnect, the store counter
14
+ * left one high per loss (drift), and NO error callback anywhere —
15
+ * so shutdown times out on a phantom count over an empty store.
16
+ *
17
+ * The policy as first ratified (2026-07-07) tolerated a lossy run
18
+ * that carried exactly that signature —
19
+ *
20
+ * 1. at least one mid-run reconnect (the mechanism needs one),
21
+ * 2. loss small and bounded by the reconnect count,
22
+ * 3. zero delivery-failure callbacks (the race produces none), and
23
+ * 4. a shutdown throw, when present, classified `SHUTDOWN_TIMEOUT`
24
+ * with an exact `dropped.count` — never `DELIVERY_FAILED`.
25
+ *
26
+ * **The tolerance was retired by operator ruling on 2026-07-10.**
27
+ * ADR-021 moved the emitter onto mqtt.js's synchronous memory store,
28
+ * which is immune to the erase-then-rebuild race — so the signature's
29
+ * mechanism no longer exists in this build, and a verdict that can
30
+ * excuse a loss is a hole, not a tolerance. Every lossy run now fails.
31
+ * The signature detection is kept as DIAGNOSTICS: when a loss matches
32
+ * the retired signature, the failure reason says so, because "this
33
+ * looks like the foreclosed upstream race" is the first lead an
34
+ * investigator needs.
35
+ *
36
+ * This is test infrastructure (lives beside `tcp-proxy.js`), but the
37
+ * policy itself is tested — see `test/soak-signature.specs.js`.
38
+ */
39
+
40
+ /**
41
+ * Per-reconnect loss allowance. Measured losses were 1–6 per reconnect
42
+ * at ~12–14k msg/s; 10 gives headroom without letting an unrelated
43
+ * loss mechanism hide behind a single reconnect.
44
+ *
45
+ * @type {number}
46
+ */
47
+ const LOSS_ALLOWANCE_PER_RECONNECT = 10;
48
+
49
+ /**
50
+ * Loss floor that counts as clean regardless of reconnects: one packet
51
+ * per 100,000 accepted (min 1) may be on the wire at the instant the
52
+ * test process dies. Predates the signature policy (the soak's
53
+ * original coverage assertion).
54
+ *
55
+ * @param {number} accepted - messages accepted by publishNow
56
+ * @returns {number} allowed gap count for a clean verdict
57
+ */
58
+ const wireRaceFloor = function ( accepted ) {
59
+ return Math.max( 1, Math.floor( accepted / 100_000 ) );
60
+ };
61
+
62
+ /**
63
+ * Judges a completed soak run against the release-gate policy.
64
+ *
65
+ * @param {Object} outcome - What the soak observed
66
+ * @param {Error|null} outcome.shutdownError - throw from emitter.shutdown(),
67
+ * or null when shutdown resolved cleanly
68
+ * @param {Array} outcome.deliveryFailures - every onDeliveryFailure call
69
+ * @param {number} outcome.coverageGaps - accepted IDs never received
70
+ * @param {number} outcome.accepted - messages accepted by publishNow
71
+ * @param {number} outcome.reconnects - emitter getHealth().stats.reconnects
72
+ * @returns {{verdict: 'clean'|'regression', reason: string}}
73
+ */
74
+ const evaluateSoakOutcome = function ( outcome ) {
75
+ const {
76
+ shutdownError,
77
+ deliveryFailures,
78
+ coverageGaps,
79
+ accepted,
80
+ reconnects
81
+ } = outcome;
82
+
83
+ // Any delivery-failure callback is off-signature: the reconnect-clear
84
+ // race never produces one. Judged first — it blocks even a run that
85
+ // would otherwise look clean.
86
+ if ( deliveryFailures.length > 0 ) {
87
+ return {
88
+ verdict: 'regression',
89
+ reason: `${deliveryFailures.length} delivery failure(s) — first: ` +
90
+ `[${deliveryFailures[ 0 ].code}] ${deliveryFailures[ 0 ].message}`
91
+ };
92
+ }
93
+
94
+ // Clean: shutdown resolved and gaps are within the wire-race floor.
95
+ if ( !shutdownError && coverageGaps <= wireRaceFloor( accepted ) ) {
96
+ return {
97
+ verdict: 'clean',
98
+ reason: `no shutdown throw, no delivery failures, gaps ${coverageGaps} ` +
99
+ `within the wire-race floor (${wireRaceFloor( accepted )})`
100
+ };
101
+ }
102
+
103
+ // From here the run is lossy and WILL fail. The checks below exist
104
+ // for the diagnosis in the reason: they tell an off-signature loss
105
+ // (name the first mismatched fact) apart from one that matches the
106
+ // retired reconnect-clear signature (name the match — see header).
107
+ if ( shutdownError && shutdownError.code !== 'SHUTDOWN_TIMEOUT' ) {
108
+ return {
109
+ verdict: 'regression',
110
+ reason: `shutdown threw [${shutdownError.code}] — the race's only throw is ` +
111
+ 'SHUTDOWN_TIMEOUT (a DELIVERY_FAILED means real flush failures)'
112
+ };
113
+ }
114
+ const droppedCount = shutdownError ? shutdownError.dropped?.count : 0;
115
+ if ( shutdownError && typeof droppedCount !== 'number' ) {
116
+ return {
117
+ verdict: 'regression',
118
+ reason: 'shutdown throw carries no dropped.count — cannot verify the drift signature'
119
+ };
120
+ }
121
+ if ( reconnects < 1 ) {
122
+ return {
123
+ verdict: 'regression',
124
+ reason: `loss with zero mid-run reconnects (gaps ${coverageGaps}, ` +
125
+ `dropped ${droppedCount || 0}) — the reconnect-clear race needs a reconnect; ` +
126
+ 'this is a different mechanism'
127
+ };
128
+ }
129
+ const totalLoss = Math.max( coverageGaps, droppedCount || 0 );
130
+ const allowance = LOSS_ALLOWANCE_PER_RECONNECT * reconnects;
131
+ if ( totalLoss > allowance ) {
132
+ return {
133
+ verdict: 'regression',
134
+ reason: `loss ${totalLoss} exceeds the per-reconnect allowance ` +
135
+ `(${allowance} for ${reconnects} reconnect(s)) — too large for the race alone`
136
+ };
137
+ }
138
+
139
+ return {
140
+ verdict: 'regression',
141
+ reason: `matches the RETIRED mqtt.js reconnect-clear race signature: ${totalLoss} lost ` +
142
+ `across ${reconnects} reconnect(s), zero delivery failures` +
143
+ ( shutdownError ? `, SHUTDOWN_TIMEOUT with dropped.count ${droppedCount}` : '' ) +
144
+ ' — tolerance retired 2026-07-10 (ADR-021\'s synchronous store forecloses the ' +
145
+ 'mechanism), so this match needs investigation, not acceptance'
146
+ };
147
+ };
148
+
149
+ export { evaluateSoakOutcome, LOSS_ALLOWANCE_PER_RECONNECT, wireRaceFloor };
@@ -0,0 +1,106 @@
1
+ // core/test-utils/tcp-proxy.js
2
+
3
+ /* eslint-disable no-empty-function */
4
+
5
+ /**
6
+ * @fileoverview Tiny TCP proxy for outage-recovery tests. Listens on
7
+ * a port, forwards each connection bidirectionally to a target port.
8
+ * Closing the proxy server tears down all live sockets so existing
9
+ * client sockets see the outage immediately; reopening on the same
10
+ * port restores connectivity.
11
+ *
12
+ * Used by the QuestDB recovery tests and the MQTT emitter
13
+ * recovery tests. Both adapters needed an in-test way to simulate
14
+ * "service unreachable" without coupling tests to docker daemon
15
+ * access. The pattern is identical for any TCP-based service.
16
+ *
17
+ * Cross-cutting test infrastructure mirrors `src/core/source-manager/test-harness/`
18
+ * — both live under `src/core/` because they're shared across adapter
19
+ * modules.
20
+ *
21
+ * Socket errors during proxy toggling are deliberately swallowed:
22
+ * the partner socket may close mid-pipe while the test toggles the
23
+ * proxy, and unhandled-error events would crash the test runner.
24
+ * The test asserts higher-level outcomes (delivery counts, health
25
+ * transitions, pipeline survival), not socket-level cleanliness.
26
+ *
27
+ * @module test-utils/tcp-proxy
28
+ */
29
+
30
+ import net from 'node:net';
31
+
32
+ /**
33
+ * Start a TCP proxy on `listenPort` that forwards to `targetPort` on
34
+ * 127.0.0.1. Returns the underlying `net.Server` once it's listening;
35
+ * pass it to `stopProxy()` to tear down.
36
+ *
37
+ * **Why we track client sockets ourselves:** `net.Server` doesn't
38
+ * expose `closeAllConnections()` (only `http.Server` does, since
39
+ * Node 18.2). Without tracking, `server.close()` waits for client
40
+ * sockets to close from their end, which never happens for clients
41
+ * holding persistent connections (mqtt.js with keepalive). We track
42
+ * incoming sockets in a Set and destroy them explicitly on
43
+ * `stopProxy()` — that sends RST to the client so it sees the
44
+ * disconnect immediately.
45
+ *
46
+ * @param {number} listenPort
47
+ * @param {number} targetPort
48
+ * @returns {Promise<net.Server>}
49
+ */
50
+ export const startProxy = function ( listenPort, targetPort ) {
51
+ const sockets = new Set();
52
+ const server = net.createServer( function ( clientSocket ) {
53
+ sockets.add( clientSocket );
54
+ clientSocket.on( 'close', function () {
55
+ sockets.delete( clientSocket );
56
+ } );
57
+ const targetSocket = net.connect( targetPort, '127.0.0.1', function () {
58
+ clientSocket.pipe( targetSocket );
59
+ targetSocket.pipe( clientSocket );
60
+ } );
61
+ // See file-header note on swallowed errors.
62
+ clientSocket.on( 'error', function () {} );
63
+ targetSocket.on( 'error', function () {} );
64
+ } );
65
+ // Attach the tracked-socket Set to the server object so
66
+ // `stopProxy()` can find it. Underscore-prefixed since this is
67
+ // a test-utility internal, not a public API.
68
+ // eslint-disable-next-line no-underscore-dangle
69
+ server._sockets = sockets;
70
+ return new Promise( function ( resolve ) {
71
+ server.listen( listenPort, '127.0.0.1', function () {
72
+ resolve( server );
73
+ } );
74
+ } );
75
+ };
76
+
77
+ /**
78
+ * Stop a proxy started via `startProxy()`. Force-destroys every
79
+ * tracked client socket first (sending RST so clients see the
80
+ * disconnect immediately, not at next keepalive timeout), then
81
+ * waits for the server to close cleanly.
82
+ *
83
+ * Without this, persistent-connection clients (mqtt.js with
84
+ * keepalive) keep their TCP socket alive indefinitely after the
85
+ * proxy "closes". `server.close()` would never fire its callback,
86
+ * and the client would never fire its disconnect events — the
87
+ * connection appears intact even though the proxy is gone. The MQTT
88
+ * emitter recovery tests surfaced this; the fix is to destroy from
89
+ * our side.
90
+ *
91
+ * @param {net.Server} server
92
+ * @returns {Promise<void>}
93
+ */
94
+ export const stopProxy = function ( server ) {
95
+ return new Promise( function ( resolve ) {
96
+ // eslint-disable-next-line no-underscore-dangle
97
+ const sockets = server._sockets || new Set();
98
+ for ( const sock of sockets ) {
99
+ sock.destroy();
100
+ }
101
+ sockets.clear();
102
+ server.close( function () {
103
+ resolve();
104
+ } );
105
+ } );
106
+ };
@@ -0,0 +1,254 @@
1
+ /**
2
+ * @fileoverview Helper functions for creating tunable parameters.
3
+ *
4
+ * These helpers provide common patterns for dynamic parameters with proper
5
+ * context capture for LLM/dashboard consumption. Each helper creates a
6
+ * function with custom toString() and .semantics for introspection.
7
+ *
8
+ * @example
9
+ * // WiFi RSSI thresholds by protocol
10
+ * .threshold('rssiAlert', 'rssi', { alert: 'rssi_low' }, {
11
+ * threshold: lookupByField( 'protocolType', {
12
+ * '802.11ac': -70,
13
+ * '802.11n': -75
14
+ * }, -75 )
15
+ * })
16
+ */
17
+
18
+ /**
19
+ * Create a tunable parameter that looks up value by a message field.
20
+ *
21
+ * Useful for protocol-specific thresholds, asset-class configs, etc.
22
+ *
23
+ * @param {string} field - Message field to use as lookup key
24
+ * @param {Object} map - Lookup map from field values to parameter values
25
+ * @param {*} defaultVal - Default value if field value not in map
26
+ * @returns {Function} Tunable function with .semantics attached
27
+ *
28
+ * @example
29
+ * const threshold = lookupByField( 'protocolType', {
30
+ * '802.11ac': -70,
31
+ * '802.11n': -75
32
+ * }, -75 );
33
+ *
34
+ * threshold( { protocolType: '802.11ac' } ); // → -70
35
+ * threshold( { protocolType: '802.11g' } ); // → -75 (default)
36
+ */
37
+ export const lookupByField = function ( field, map, defaultVal ) {
38
+ const fn = ( msg ) => map[ msg[ field ] ] ?? defaultVal;
39
+
40
+ fn.toString = () => `lookupByField("${field}", ${JSON.stringify( map )}, ${JSON.stringify( defaultVal )})`;
41
+ fn.semantics = {
42
+ type: 'lookupByField',
43
+ field,
44
+ map,
45
+ default: defaultVal
46
+ };
47
+
48
+ return fn;
49
+ }; // lookupByField()
50
+
51
+ /**
52
+ * Create a build-time resolver that picks an option value by the current fan field.
53
+ *
54
+ * `pickByField` is `lookupByField`'s build-time sibling, used INSIDE a `forEach`:
55
+ * it resolves to `map[ each.field ]` once, when the fan expands, and leaves no
56
+ * function behind at runtime. Use it for an option whose value differs per channel
57
+ * (a per-string clean range, a per-channel threshold). A field missing from `map`
58
+ * fails at build, not at the first message.
59
+ *
60
+ * It is resolved by the fan, never called per message, so its body throws if it is
61
+ * ever invoked (that would mean it was used outside a `forEach`). `.semantics`
62
+ * carries the map for the fan's resolver; `.toString` serializes it.
63
+ *
64
+ * @param {Object} map - Map from field name to that channel's option value
65
+ * @returns {Function} Resolver marked with .semantics for build-time resolution
66
+ *
67
+ * @example
68
+ * .forEach( [ 'scb1', 'scb2' ], ( each ) => each
69
+ * .threshold( 'hi', each.field, { active: 'high' },
70
+ * { threshold: pickByField( { scb1: 0.8, scb2: 0.6 } ), mode: 'above' } ) )
71
+ */
72
+ export const pickByField = function ( map ) {
73
+ const fn = () => {
74
+ throw new Error(
75
+ 'WinkComposer/flow: pickByField is resolved at build time inside forEach; ' +
76
+ 'it has no runtime value. Use it only for a forEach option.'
77
+ );
78
+ };
79
+
80
+ fn.toString = () => `pickByField(${JSON.stringify( map )})`;
81
+ fn.semantics = {
82
+ type: 'pickByField',
83
+ map
84
+ };
85
+
86
+ return fn;
87
+ }; // pickByField()
88
+
89
+ /**
90
+ * Create a tunable parameter that scales a message field value.
91
+ *
92
+ * Computes: (msg[field] * factor) + offset, optionally snapped to step.
93
+ * Useful for adaptive sensitivity (e.g., delta = stdev * 0.5).
94
+ *
95
+ * @param {string} field - Message field to read
96
+ * @param {number} factor - Multiplication factor
97
+ * @param {number} [offset=0] - Additive offset
98
+ * @param {number} [step=0] - If > 0, snap result to nearest multiple
99
+ * @returns {Function} Tunable function with .semantics attached
100
+ *
101
+ * @example
102
+ * // Adaptive sensitivity: delta tracks noise level
103
+ * const delta = scaleBy( 'fuel_stdev', 0.5 );
104
+ * delta( { fuel_stdev: 0.046 } ); // → 0.023
105
+ *
106
+ * @example
107
+ * // With step snapping
108
+ * const rounded = scaleBy( 'value', 1.5, 10, 5 );
109
+ * rounded( { value: 20 } ); // → 40 (30 + 10 = 40, already on step)
110
+ */
111
+ export const scaleBy = function ( field, factor, offset = 0, step = 0 ) {
112
+ const fn = ( msg ) => {
113
+ const raw = ( msg[ field ] * factor ) + offset;
114
+ return step > 0 ? Math.round( raw / step ) * step : raw;
115
+ };
116
+
117
+ fn.toString = () => `scaleBy("${field}", ${factor}, ${offset}, ${step})`;
118
+ fn.semantics = {
119
+ type: 'scaleBy',
120
+ field,
121
+ factor,
122
+ offset,
123
+ step
124
+ };
125
+
126
+ return fn;
127
+ }; // scaleBy()
128
+
129
+ /**
130
+ * Create a conditional tunable parameter that returns different values based on predicate.
131
+ *
132
+ * Useful for mode-dependent behavior (warmup vs normal, idle vs active).
133
+ *
134
+ * @param {Function} predicate - Function (msg) => boolean
135
+ * @param {*} trueVal - Value or function when predicate is true
136
+ * @param {*} falseVal - Value or function when predicate is false
137
+ * @param {string} predicateDesc - Human-readable description of predicate
138
+ * @returns {Function} Tunable function with .semantics attached
139
+ *
140
+ * @example
141
+ * // Mode-dependent threshold
142
+ * const threshold = chooseWhen(
143
+ * ( msg ) => msg.isWarmup,
144
+ * 100, // warmup: high threshold
145
+ * 78, // normal: standard threshold
146
+ * 'msg.isWarmup'
147
+ * );
148
+ *
149
+ * threshold( { isWarmup: true } ); // → 100
150
+ * threshold( { isWarmup: false } ); // → 78
151
+ */
152
+ export const chooseWhen = function ( predicate, trueVal, falseVal, predicateDesc ) {
153
+ const trueFn = typeof trueVal === 'function' ? trueVal : () => trueVal;
154
+ const falseFn = typeof falseVal === 'function' ? falseVal : () => falseVal;
155
+
156
+ const fn = ( msg ) => ( predicate( msg ) ? trueFn( msg ) : falseFn( msg ) );
157
+
158
+ fn.toString = () => `chooseWhen(${predicateDesc}, ${trueVal}, ${falseVal})`;
159
+ fn.semantics = {
160
+ type: 'chooseWhen',
161
+ predicate: predicateDesc,
162
+ trueVal,
163
+ falseVal
164
+ };
165
+
166
+ return fn;
167
+ }; // chooseWhen()
168
+
169
+ /**
170
+ * Create a tunable parameter that clamps a message field value to [min, max].
171
+ *
172
+ * Useful for safety limits or bounded parameters.
173
+ *
174
+ * @param {string} field - Message field to read
175
+ * @param {number} min - Minimum value (inclusive)
176
+ * @param {number} max - Maximum value (inclusive)
177
+ * @returns {Function} Tunable function with .semantics attached
178
+ *
179
+ * @example
180
+ * const safeThreshold = clampTo( 'requestedThreshold', 10, 100 );
181
+ * safeThreshold( { requestedThreshold: 5 } ); // → 10
182
+ * safeThreshold( { requestedThreshold: 50 } ); // → 50
183
+ * safeThreshold( { requestedThreshold: 150 } ); // → 100
184
+ */
185
+ export const clampTo = function ( field, min, max ) {
186
+ const fn = ( msg ) => {
187
+ const val = msg[ field ];
188
+ return val < min ? min : ( val > max ? max : val );
189
+ };
190
+
191
+ fn.toString = () => `clampTo("${field}", ${min}, ${max})`;
192
+ fn.semantics = {
193
+ type: 'clampTo',
194
+ field,
195
+ min,
196
+ max
197
+ };
198
+
199
+ return fn;
200
+ }; // clampTo()
201
+
202
+ /**
203
+ * Create a tunable parameter that reads a field directly from the message.
204
+ *
205
+ * Simplest dynamic parameter — just passes through a message field.
206
+ * Useful when the parameter should track another field exactly.
207
+ *
208
+ * @param {string} field - Message field to read
209
+ * @param {*} [defaultVal] - Default value if field is missing/undefined
210
+ * @returns {Function} Tunable function with .semantics attached
211
+ *
212
+ * @example
213
+ * const dynamicThreshold = fromField( 'learnedBaseline', 50 );
214
+ * dynamicThreshold( { learnedBaseline: 72 } ); // → 72
215
+ * dynamicThreshold( { otherField: 10 } ); // → 50 (default)
216
+ */
217
+ export const fromField = function ( field, defaultVal ) {
218
+ const fn = ( msg ) => msg[ field ] ?? defaultVal;
219
+
220
+ fn.toString = () => `fromField("${field}", ${JSON.stringify( defaultVal )})`;
221
+ fn.semantics = {
222
+ type: 'fromField',
223
+ field,
224
+ default: defaultVal
225
+ };
226
+
227
+ return fn;
228
+ }; // fromField()
229
+
230
+ /**
231
+ * Create a tunable parameter that adds an offset to a message field value.
232
+ *
233
+ * Simpler than scaleBy when factor is 1.
234
+ *
235
+ * @param {string} field - Message field to read
236
+ * @param {number} offset - Value to add
237
+ * @returns {Function} Tunable function with .semantics attached
238
+ *
239
+ * @example
240
+ * const threshold = offsetBy( 'baseline', 10 );
241
+ * threshold( { baseline: 72 } ); // → 82
242
+ */
243
+ export const offsetBy = function ( field, offset ) {
244
+ const fn = ( msg ) => msg[ field ] + offset;
245
+
246
+ fn.toString = () => `offsetBy("${field}", ${offset})`;
247
+ fn.semantics = {
248
+ type: 'offsetBy',
249
+ field,
250
+ offset
251
+ };
252
+
253
+ return fn;
254
+ }; // offsetBy()
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @fileoverview Tunable pattern for dynamic parameters.
3
+ *
4
+ * Enables parameters to adapt to message context (operating mode, regime, etc.)
5
+ * by normalizing static values and functions to a uniform callable interface.
6
+ *
7
+ * All node parameters become functions, enabling dynamic behavior
8
+ * without type-checking overhead during message processing.
9
+ *
10
+ * ## Guard Pattern (for node implementors)
11
+ *
12
+ * Dynamic tunables (user-supplied functions) can throw at runtime.
13
+ * Each node's update.js wraps all tunable resolves in a single try/catch.
14
+ * On throw, JS assignment semantics guarantee the state field retains its
15
+ * previous good value (RHS evaluated first; failed RHS ⇒ assignment
16
+ * never executes). Nodes seed state fields in init.js so the first-message
17
+ * edge case has a meaningful fallback.
18
+ *
19
+ * Log suppression: `state.tunableErrorLogged` flag prevents console
20
+ * flooding — one log per error episode, reset on successful recovery
21
+ * or on node reset (reset.js clears the flag).
22
+ *
23
+ * Static tunables (`asTunable(42)` → `() => 42`) cannot throw, so the
24
+ * guard only activates for dynamic tunables.
25
+ */
26
+
27
+ /**
28
+ * Convert value to function. Static values become () => value.
29
+ * Functions pass through unchanged.
30
+ *
31
+ * This enables uniform parameter access in node update() functions:
32
+ * `const threshold = state.thresholdFn( msg )` works whether threshold
33
+ * was configured as a static number or a dynamic function.
34
+ *
35
+ * @param {*} v - Static value or function
36
+ * @returns {Function} Always returns a function
37
+ *
38
+ * @example
39
+ * // Static value
40
+ * const fn = asTunable( 42 );
41
+ * fn( msg ); // → 42
42
+ *
43
+ * @example
44
+ * // Dynamic function
45
+ * const fn = asTunable( ( msg ) => msg.stdev * 0.5 );
46
+ * fn( { stdev: 10 } ); // → 5
47
+ */
48
+ export const asTunable = ( v ) => ( typeof v === 'function' ? v : () => v );
49
+
50
+ /**
51
+ * Extract parameter context for LLM/dashboard consumption.
52
+ *
53
+ * Provides introspection metadata about whether a parameter is static
54
+ * or dynamic, including the formula string for dynamic parameters.
55
+ * Helper functions can attach `.semantics` for richer context.
56
+ *
57
+ * @param {string} name - Parameter name
58
+ * @param {*} value - Parameter value (static or function)
59
+ * @returns {Object} Context object with type, value/formula, and semantics
60
+ *
61
+ * @example
62
+ * // Static parameter
63
+ * extractParamContext( 'threshold', 78 );
64
+ * // → { name: 'threshold', type: 'static', value: 78 }
65
+ *
66
+ * @example
67
+ * // Dynamic parameter
68
+ * extractParamContext( 'delta', ( msg ) => msg.stdev * 0.5 );
69
+ * // → { name: 'delta', type: 'dynamic', formula: '( msg ) => msg.stdev * 0.5', semantics: null }
70
+ *
71
+ * @example
72
+ * // Dynamic parameter with helper semantics
73
+ * const scaledDelta = scaleBy( 'stdev', 0.5 );
74
+ * extractParamContext( 'delta', scaledDelta );
75
+ * // → { name: 'delta', type: 'dynamic', formula: 'scaleBy("stdev", 0.5, 0, 0)', semantics: { type: 'scaleBy', ... } }
76
+ */
77
+ export const extractParamContext = function ( name, value ) {
78
+ if ( typeof value === 'function' ) {
79
+ return {
80
+ name,
81
+ type: 'dynamic',
82
+ formula: value.toString(),
83
+ semantics: value.semantics || null
84
+ };
85
+ }
86
+ return { name, type: 'static', value };
87
+ }; // extractParamContext()