@winkjs/composer 0.0.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -80
  3. package/package.json +56 -28
  4. package/src/composer.js +75 -0
  5. package/src/core/batch/aoo-encoder.js +148 -0
  6. package/src/core/batch/index.js +12 -0
  7. package/src/core/batch/sliding-window.js +91 -0
  8. package/src/core/codec/index.js +137 -0
  9. package/src/core/emitter-manager/mqtt/constants.js +102 -0
  10. package/src/core/emitter-manager/mqtt/emitter.js +780 -0
  11. package/src/core/emitter-manager/mqtt/index.js +190 -0
  12. package/src/core/emitter-manager/mqtt/mqtt-store.js +538 -0
  13. package/src/core/emitter-manager/terminal/assert-columns.js +98 -0
  14. package/src/core/emitter-manager/terminal/formatters.js +160 -0
  15. package/src/core/emitter-manager/terminal/index.js +341 -0
  16. package/src/core/env-vars.js +245 -0
  17. package/src/core/mqtt-protocol.js +47 -0
  18. package/src/core/partition-manager/index.js +7 -0
  19. package/src/core/partition-manager/init.js +44 -0
  20. package/src/core/partition-manager/introspect.js +15 -0
  21. package/src/core/partition-manager/publish-to.js +6 -0
  22. package/src/core/partition-manager/recompute.js +5 -0
  23. package/src/core/partition-manager/reset.js +5 -0
  24. package/src/core/partition-manager/update.js +264 -0
  25. package/src/core/partition-manager/validate-flow.js +157 -0
  26. package/src/core/semantics/digest.js +128 -0
  27. package/src/core/semantics/index.js +83 -0
  28. package/src/core/semantics/loader.js +571 -0
  29. package/src/core/semantics/schemas/asset-class-schema.js +218 -0
  30. package/src/core/semantics/schemas/column-schema.js +651 -0
  31. package/src/core/semantics/schemas/enum-schema.js +153 -0
  32. package/src/core/semantics/schemas/index.js +51 -0
  33. package/src/core/semantics/warnings.js +141 -0
  34. package/src/core/shutdown-manager/index.js +289 -0
  35. package/src/core/source-manager/csv/index.js +171 -0
  36. package/src/core/source-manager/csv/start.js +456 -0
  37. package/src/core/source-manager/mqtt/client.js +406 -0
  38. package/src/core/source-manager/mqtt/constants.js +147 -0
  39. package/src/core/source-manager/mqtt/dedup.js +244 -0
  40. package/src/core/source-manager/mqtt/index.js +260 -0
  41. package/src/core/source-manager/mqtt/status.js +546 -0
  42. package/src/core/source-manager/test-harness/comparator.js +226 -0
  43. package/src/core/source-manager/test-harness/field-generator.js +85 -0
  44. package/src/core/source-manager/test-harness/fuzz.js +87 -0
  45. package/src/core/source-manager/test-harness/index.js +115 -0
  46. package/src/core/source-manager/test-harness/prng.js +57 -0
  47. package/src/core/source-manager/test-harness/start.js +232 -0
  48. package/src/core/source-manager/test-harness/validate.js +213 -0
  49. package/src/core/storage-manager/questdb/assert-columns.js +120 -0
  50. package/src/core/storage-manager/questdb/ensure-tables.js +187 -0
  51. package/src/core/storage-manager/questdb/index.js +1251 -0
  52. package/src/core/storage-manager/questdb/persist-plan.js +488 -0
  53. package/src/core/storage-manager/questdb/writers.js +142 -0
  54. package/src/core/test-utils/sealed-record.js +80 -0
  55. package/src/core/test-utils/soak-signature.js +149 -0
  56. package/src/core/test-utils/tcp-proxy.js +106 -0
  57. package/src/core/tunable/helpers.js +254 -0
  58. package/src/core/tunable/index.js +87 -0
  59. package/src/core/utils/console-formatter/index.js +207 -0
  60. package/src/core/utils/create-smart-store-as/index.js +98 -0
  61. package/src/core/utils/create-smart-store-as/sanitize-field.js +46 -0
  62. package/src/core/utils/create-smart-store-as/smart-truncate.js +93 -0
  63. package/src/core/utils/flow/index.js +5 -0
  64. package/src/core/utils/flow/make-collision-checker.js +23 -0
  65. package/src/core/utils/flow/to-kebab.js +20 -0
  66. package/src/core/utils/half-life/index.js +111 -0
  67. package/src/core/utils/node/disable.js +11 -0
  68. package/src/core/utils/node/enable.js +11 -0
  69. package/src/core/utils/node/execute-triggers.js +40 -0
  70. package/src/core/utils/node/index.js +10 -0
  71. package/src/core/utils/node/pause.js +11 -0
  72. package/src/core/utils/node/populate-predicate-inputs.js +14 -0
  73. package/src/core/utils/node/publish-nan.js +19 -0
  74. package/src/core/utils/node/reset-predicate-inputs.js +11 -0
  75. package/src/core/utils/node/unpause.js +11 -0
  76. package/src/core/utils/node/validate-spec.js +22 -0
  77. package/src/core/utils/options/resolve-field-keyed.js +115 -0
  78. package/src/core/utils/quantize/index.js +75 -0
  79. package/src/core/utils/stats/formulas.js +67 -0
  80. package/src/core/utils/stats/index.js +3 -0
  81. package/src/core/utils/validate/composer-validators.js +141 -0
  82. package/src/core/utils/validate/helpers.js +206 -0
  83. package/src/core/utils/validate/index.js +12 -0
  84. package/src/core/utils/validate/type-validators.js +538 -0
  85. package/src/core/utils/validate/validate.js +107 -0
  86. package/src/core/utils/validate/validators.js +177 -0
  87. package/src/core/wiring/assert-handle.js +57 -0
  88. package/src/core/wiring/assert-module.js +63 -0
  89. package/src/core/wiring/index.js +5 -0
  90. package/src/core/wiring/node-type-to-module.js +17 -0
  91. package/src/core/wiring/wire-emitters.js +256 -0
  92. package/src/core/wiring/wire-linear-graph.js +25 -0
  93. package/src/core/wiring/wire-node.js +157 -0
  94. package/src/core/wiring/wire-semantics.js +173 -0
  95. package/src/core/wiring/wire-storages.js +344 -0
  96. package/src/flow/check-output-collisions.js +93 -0
  97. package/src/flow/consts.js +9 -0
  98. package/src/flow/deep-clone-spec.js +64 -0
  99. package/src/flow/driver.js +171 -0
  100. package/src/flow/expand-single-input-array.js +76 -0
  101. package/src/flow/expand-template-spec.js +93 -0
  102. package/src/flow/flow-api-schemas.js +300 -0
  103. package/src/flow/flow.js +658 -0
  104. package/src/flow/for-each.js +225 -0
  105. package/src/flow/get-signature-pattern.js +31 -0
  106. package/src/flow/get-target-array.js +46 -0
  107. package/src/flow/inspect.js +67 -0
  108. package/src/flow/load-node-module.js +81 -0
  109. package/src/flow/make-node-method.js +112 -0
  110. package/src/flow/resolve-pick-by-field.js +77 -0
  111. package/src/flow/resolve-tunables.js +76 -0
  112. package/src/flow/run.js +392 -0
  113. package/src/flow/serialize.js +161 -0
  114. package/src/flow/signatures-schema.js +222 -0
  115. package/src/flow/spec-builder.js +65 -0
  116. package/src/flow/validate-triggers.js +250 -0
  117. package/src/flow/validate.js +261 -0
  118. package/src/nodes/accumulate/index.js +21 -0
  119. package/src/nodes/accumulate/init.js +48 -0
  120. package/src/nodes/accumulate/introspect.js +123 -0
  121. package/src/nodes/accumulate/publish-to.js +34 -0
  122. package/src/nodes/accumulate/recompute.js +27 -0
  123. package/src/nodes/accumulate/reset.js +25 -0
  124. package/src/nodes/accumulate/update.js +42 -0
  125. package/src/nodes/appraise/calibrate.js +64 -0
  126. package/src/nodes/appraise/decision.js +66 -0
  127. package/src/nodes/appraise/deviation.js +140 -0
  128. package/src/nodes/appraise/index.js +23 -0
  129. package/src/nodes/appraise/init.js +165 -0
  130. package/src/nodes/appraise/integrate.js +45 -0
  131. package/src/nodes/appraise/introspect.js +352 -0
  132. package/src/nodes/appraise/publish-to.js +93 -0
  133. package/src/nodes/appraise/receptor.js +69 -0
  134. package/src/nodes/appraise/recompute.js +79 -0
  135. package/src/nodes/appraise/reset.js +53 -0
  136. package/src/nodes/appraise/update.js +130 -0
  137. package/src/nodes/archive/csv/index.js +6 -0
  138. package/src/nodes/archive/csv/init.js +36 -0
  139. package/src/nodes/archive/csv/load.js +70 -0
  140. package/src/nodes/archive/csv/reset.js +14 -0
  141. package/src/nodes/archive/csv/simple-parser.js +216 -0
  142. package/src/nodes/archive/csv/test.js +173 -0
  143. package/src/nodes/archive/csv/update.js +63 -0
  144. package/src/nodes/butterworth-filter/index.js +18 -0
  145. package/src/nodes/butterworth-filter/init.js +184 -0
  146. package/src/nodes/butterworth-filter/introspect.js +228 -0
  147. package/src/nodes/butterworth-filter/publish-to.js +21 -0
  148. package/src/nodes/butterworth-filter/recompute.js +13 -0
  149. package/src/nodes/butterworth-filter/reset.js +30 -0
  150. package/src/nodes/butterworth-filter/update.js +38 -0
  151. package/src/nodes/categorize/index.js +19 -0
  152. package/src/nodes/categorize/init.js +58 -0
  153. package/src/nodes/categorize/introspect.js +177 -0
  154. package/src/nodes/categorize/publish-to.js +29 -0
  155. package/src/nodes/categorize/recompute.js +14 -0
  156. package/src/nodes/categorize/reset.js +16 -0
  157. package/src/nodes/categorize/update.js +53 -0
  158. package/src/nodes/controller/index.js +9 -0
  159. package/src/nodes/controller/init.js +55 -0
  160. package/src/nodes/controller/introspect.js +91 -0
  161. package/src/nodes/controller/publish-to.js +8 -0
  162. package/src/nodes/controller/recompute.js +9 -0
  163. package/src/nodes/controller/reset.js +17 -0
  164. package/src/nodes/controller/update.js +63 -0
  165. package/src/nodes/diff/index.js +19 -0
  166. package/src/nodes/diff/init.js +37 -0
  167. package/src/nodes/diff/introspect.js +132 -0
  168. package/src/nodes/diff/publish-to.js +21 -0
  169. package/src/nodes/diff/recompute.js +12 -0
  170. package/src/nodes/diff/reset.js +12 -0
  171. package/src/nodes/diff/update.js +37 -0
  172. package/src/nodes/digest-moments/formulas.js +12 -0
  173. package/src/nodes/digest-moments/index.js +14 -0
  174. package/src/nodes/digest-moments/init.js +67 -0
  175. package/src/nodes/digest-moments/introspect.js +154 -0
  176. package/src/nodes/digest-moments/publish-to.js +34 -0
  177. package/src/nodes/digest-moments/recompute.js +14 -0
  178. package/src/nodes/digest-moments/reset.js +14 -0
  179. package/src/nodes/digest-moments/update.js +120 -0
  180. package/src/nodes/dwell-time-tracker/index.js +14 -0
  181. package/src/nodes/dwell-time-tracker/init.js +59 -0
  182. package/src/nodes/dwell-time-tracker/introspect.js +137 -0
  183. package/src/nodes/dwell-time-tracker/publish-to.js +22 -0
  184. package/src/nodes/dwell-time-tracker/recompute.js +8 -0
  185. package/src/nodes/dwell-time-tracker/reset.js +23 -0
  186. package/src/nodes/dwell-time-tracker/update.js +103 -0
  187. package/src/nodes/emit-if/helpers.js +94 -0
  188. package/src/nodes/emit-if/index.js +16 -0
  189. package/src/nodes/emit-if/init.js +67 -0
  190. package/src/nodes/emit-if/introspect.js +102 -0
  191. package/src/nodes/emit-if/publish-to.js +13 -0
  192. package/src/nodes/emit-if/recompute.js +12 -0
  193. package/src/nodes/emit-if/reset.js +27 -0
  194. package/src/nodes/emit-if/update.js +93 -0
  195. package/src/nodes/es-correlation/index.js +19 -0
  196. package/src/nodes/es-correlation/init.js +73 -0
  197. package/src/nodes/es-correlation/introspect.js +176 -0
  198. package/src/nodes/es-correlation/publish-to.js +32 -0
  199. package/src/nodes/es-correlation/recompute.js +56 -0
  200. package/src/nodes/es-correlation/reset.js +27 -0
  201. package/src/nodes/es-correlation/update.js +110 -0
  202. package/src/nodes/es-mean/index.js +20 -0
  203. package/src/nodes/es-mean/init.js +58 -0
  204. package/src/nodes/es-mean/introspect.js +129 -0
  205. package/src/nodes/es-mean/publish-to.js +19 -0
  206. package/src/nodes/es-mean/recompute.js +16 -0
  207. package/src/nodes/es-mean/reset.js +24 -0
  208. package/src/nodes/es-mean/update.js +69 -0
  209. package/src/nodes/es-pairwise-correlation/index.js +19 -0
  210. package/src/nodes/es-pairwise-correlation/init.js +98 -0
  211. package/src/nodes/es-pairwise-correlation/introspect.js +187 -0
  212. package/src/nodes/es-pairwise-correlation/publish-to.js +31 -0
  213. package/src/nodes/es-pairwise-correlation/recompute.js +69 -0
  214. package/src/nodes/es-pairwise-correlation/reset.js +29 -0
  215. package/src/nodes/es-pairwise-correlation/update.js +89 -0
  216. package/src/nodes/es-stats/index.js +21 -0
  217. package/src/nodes/es-stats/init.js +105 -0
  218. package/src/nodes/es-stats/introspect.js +157 -0
  219. package/src/nodes/es-stats/publish-to.js +42 -0
  220. package/src/nodes/es-stats/recompute.js +74 -0
  221. package/src/nodes/es-stats/reset.js +49 -0
  222. package/src/nodes/es-stats/update-envelope.js +44 -0
  223. package/src/nodes/es-stats/update-welford.js +64 -0
  224. package/src/nodes/es-stats/update.js +59 -0
  225. package/src/nodes/index.js +83 -0
  226. package/src/nodes/invert-flag/index.js +20 -0
  227. package/src/nodes/invert-flag/init.js +36 -0
  228. package/src/nodes/invert-flag/introspect.js +115 -0
  229. package/src/nodes/invert-flag/publish-to.js +23 -0
  230. package/src/nodes/invert-flag/recompute.js +12 -0
  231. package/src/nodes/invert-flag/reset.js +12 -0
  232. package/src/nodes/invert-flag/update.js +32 -0
  233. package/src/nodes/kalman1d/index.js +20 -0
  234. package/src/nodes/kalman1d/init.js +103 -0
  235. package/src/nodes/kalman1d/introspect.js +250 -0
  236. package/src/nodes/kalman1d/publish-to.js +34 -0
  237. package/src/nodes/kalman1d/recompute.js +23 -0
  238. package/src/nodes/kalman1d/reset.js +29 -0
  239. package/src/nodes/kalman1d/update.js +144 -0
  240. package/src/nodes/kernel/index.js +18 -0
  241. package/src/nodes/kernel/init.js +66 -0
  242. package/src/nodes/kernel/introspect.js +163 -0
  243. package/src/nodes/kernel/presets.js +74 -0
  244. package/src/nodes/kernel/publish-to.js +25 -0
  245. package/src/nodes/kernel/recompute.js +14 -0
  246. package/src/nodes/kernel/reset.js +20 -0
  247. package/src/nodes/kernel/update.js +69 -0
  248. package/src/nodes/lag/index.js +21 -0
  249. package/src/nodes/lag/init.js +87 -0
  250. package/src/nodes/lag/introspect.js +202 -0
  251. package/src/nodes/lag/publish-to.js +64 -0
  252. package/src/nodes/lag/recompute.js +33 -0
  253. package/src/nodes/lag/reset.js +49 -0
  254. package/src/nodes/lag/update.js +133 -0
  255. package/src/nodes/median3/index.js +19 -0
  256. package/src/nodes/median3/init.js +40 -0
  257. package/src/nodes/median3/introspect.js +116 -0
  258. package/src/nodes/median3/publish-to.js +21 -0
  259. package/src/nodes/median3/recompute.js +12 -0
  260. package/src/nodes/median3/reset.js +15 -0
  261. package/src/nodes/median3/update.js +58 -0
  262. package/src/nodes/moments-digest/copy-from.js +12 -0
  263. package/src/nodes/moments-digest/flush.js +17 -0
  264. package/src/nodes/moments-digest/index.js +132 -0
  265. package/src/nodes/moments-digest/init.js +84 -0
  266. package/src/nodes/moments-digest/introspect.js +122 -0
  267. package/src/nodes/moments-digest/prelude.js +28 -0
  268. package/src/nodes/moments-digest/publish-to.js +31 -0
  269. package/src/nodes/moments-digest/recompute.js +9 -0
  270. package/src/nodes/moments-digest/reset.js +20 -0
  271. package/src/nodes/moments-digest/update.js +182 -0
  272. package/src/nodes/node-names.js +67 -0
  273. package/src/nodes/page-hinkley/index.js +20 -0
  274. package/src/nodes/page-hinkley/init.js +66 -0
  275. package/src/nodes/page-hinkley/introspect.js +193 -0
  276. package/src/nodes/page-hinkley/publish-to.js +28 -0
  277. package/src/nodes/page-hinkley/recompute.js +14 -0
  278. package/src/nodes/page-hinkley/reset.js +23 -0
  279. package/src/nodes/page-hinkley/update.js +105 -0
  280. package/src/nodes/pass-if/index.js +21 -0
  281. package/src/nodes/pass-if/init.js +37 -0
  282. package/src/nodes/pass-if/introspect.js +93 -0
  283. package/src/nodes/pass-if/publish-to.js +15 -0
  284. package/src/nodes/pass-if/recompute.js +15 -0
  285. package/src/nodes/pass-if/reset.js +18 -0
  286. package/src/nodes/pass-if/update.js +37 -0
  287. package/src/nodes/persist-if/helpers.js +111 -0
  288. package/src/nodes/persist-if/index.js +9 -0
  289. package/src/nodes/persist-if/init.js +98 -0
  290. package/src/nodes/persist-if/introspect.js +106 -0
  291. package/src/nodes/persist-if/publish-to.js +21 -0
  292. package/src/nodes/persist-if/recompute.js +20 -0
  293. package/src/nodes/persist-if/reset.js +35 -0
  294. package/src/nodes/persist-if/update.js +112 -0
  295. package/src/nodes/persistence-check/index.js +14 -0
  296. package/src/nodes/persistence-check/init.js +38 -0
  297. package/src/nodes/persistence-check/introspect.js +144 -0
  298. package/src/nodes/persistence-check/publish-to.js +11 -0
  299. package/src/nodes/persistence-check/recompute.js +7 -0
  300. package/src/nodes/persistence-check/reset.js +12 -0
  301. package/src/nodes/persistence-check/update.js +57 -0
  302. package/src/nodes/process-index/index.js +15 -0
  303. package/src/nodes/process-index/init.js +48 -0
  304. package/src/nodes/process-index/introspect.js +182 -0
  305. package/src/nodes/process-index/publish-to.js +29 -0
  306. package/src/nodes/process-index/recompute.js +8 -0
  307. package/src/nodes/process-index/reset.js +8 -0
  308. package/src/nodes/process-index/update.js +75 -0
  309. package/src/nodes/ratio/index.js +14 -0
  310. package/src/nodes/ratio/init.js +37 -0
  311. package/src/nodes/ratio/introspect.js +155 -0
  312. package/src/nodes/ratio/publish-to.js +15 -0
  313. package/src/nodes/ratio/recompute.js +7 -0
  314. package/src/nodes/ratio/reset.js +7 -0
  315. package/src/nodes/ratio/update.js +47 -0
  316. package/src/nodes/sanitize/index.js +82 -0
  317. package/src/nodes/sanitize/init.js +82 -0
  318. package/src/nodes/sanitize/introspect.js +194 -0
  319. package/src/nodes/sanitize/publish-to.js +15 -0
  320. package/src/nodes/sanitize/recompute.js +8 -0
  321. package/src/nodes/sanitize/reset.js +14 -0
  322. package/src/nodes/sanitize/update.js +102 -0
  323. package/src/nodes/spike-guard/index.js +14 -0
  324. package/src/nodes/spike-guard/init.js +47 -0
  325. package/src/nodes/spike-guard/introspect.js +130 -0
  326. package/src/nodes/spike-guard/publish-to.js +35 -0
  327. package/src/nodes/spike-guard/recompute.js +14 -0
  328. package/src/nodes/spike-guard/reset.js +19 -0
  329. package/src/nodes/spike-guard/update.js +89 -0
  330. package/src/nodes/sse-emitter/index.js +7 -0
  331. package/src/nodes/sse-emitter/init.js +105 -0
  332. package/src/nodes/sse-emitter/introspect.js +134 -0
  333. package/src/nodes/sse-emitter/publish-to.js +6 -0
  334. package/src/nodes/sse-emitter/recompute.js +16 -0
  335. package/src/nodes/sse-emitter/reset.js +19 -0
  336. package/src/nodes/sse-emitter/update.js +69 -0
  337. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +36 -0
  338. package/src/nodes/sse-emitter/utils/build-message.js +31 -0
  339. package/src/nodes/sse-emitter/utils/connection-manager.js +45 -0
  340. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +20 -0
  341. package/src/nodes/sse-emitter/utils/health-monitor.js +38 -0
  342. package/src/nodes/sse-emitter/utils/qos-handler.js +41 -0
  343. package/src/nodes/state-change-detector/helpers.js +35 -0
  344. package/src/nodes/state-change-detector/index.js +12 -0
  345. package/src/nodes/state-change-detector/init.js +56 -0
  346. package/src/nodes/state-change-detector/introspect.js +146 -0
  347. package/src/nodes/state-change-detector/publish-to.js +20 -0
  348. package/src/nodes/state-change-detector/recompute.js +6 -0
  349. package/src/nodes/state-change-detector/reset.js +20 -0
  350. package/src/nodes/state-change-detector/update.js +97 -0
  351. package/src/nodes/sw-stats/index.js +14 -0
  352. package/src/nodes/sw-stats/init.js +51 -0
  353. package/src/nodes/sw-stats/introspect.js +131 -0
  354. package/src/nodes/sw-stats/publish-to.js +45 -0
  355. package/src/nodes/sw-stats/recompute.js +33 -0
  356. package/src/nodes/sw-stats/reset.js +15 -0
  357. package/src/nodes/sw-stats/update.js +39 -0
  358. package/src/nodes/swing-watch/index.js +17 -0
  359. package/src/nodes/swing-watch/init.js +144 -0
  360. package/src/nodes/swing-watch/introspect.js +188 -0
  361. package/src/nodes/swing-watch/publish-to.js +53 -0
  362. package/src/nodes/swing-watch/recompute.js +22 -0
  363. package/src/nodes/swing-watch/reset.js +55 -0
  364. package/src/nodes/swing-watch/update.js +767 -0
  365. package/src/nodes/tally/index.js +15 -0
  366. package/src/nodes/tally/init.js +32 -0
  367. package/src/nodes/tally/introspect.js +116 -0
  368. package/src/nodes/tally/publish-to.js +28 -0
  369. package/src/nodes/tally/recompute.js +10 -0
  370. package/src/nodes/tally/reset.js +10 -0
  371. package/src/nodes/tally/update.js +44 -0
  372. package/src/nodes/threshold/index.js +14 -0
  373. package/src/nodes/threshold/init.js +121 -0
  374. package/src/nodes/threshold/introspect.js +192 -0
  375. package/src/nodes/threshold/publish-to.js +11 -0
  376. package/src/nodes/threshold/recompute.js +8 -0
  377. package/src/nodes/threshold/reset.js +14 -0
  378. package/src/nodes/threshold/update.js +54 -0
  379. package/src/nodes/transform/helpers.js +39 -0
  380. package/src/nodes/transform/index.js +15 -0
  381. package/src/nodes/transform/init.js +35 -0
  382. package/src/nodes/transform/introspect.js +118 -0
  383. package/src/nodes/transform/publish-to.js +23 -0
  384. package/src/nodes/transform/recompute.js +12 -0
  385. package/src/nodes/transform/reset.js +11 -0
  386. package/src/nodes/transform/update.js +32 -0
  387. package/src/nodes/trend/compute-confidence.js +112 -0
  388. package/src/nodes/trend/index.js +14 -0
  389. package/src/nodes/trend/init.js +81 -0
  390. package/src/nodes/trend/introspect.js +151 -0
  391. package/src/nodes/trend/publish-to.js +22 -0
  392. package/src/nodes/trend/recompute.js +15 -0
  393. package/src/nodes/trend/reset.js +26 -0
  394. package/src/nodes/trend/update.js +93 -0
  395. package/src/nodes/tw-stats/copy-from.js +16 -0
  396. package/src/nodes/tw-stats/flush.js +12 -0
  397. package/src/nodes/tw-stats/index.js +41 -0
  398. package/src/nodes/tw-stats/init.js +92 -0
  399. package/src/nodes/tw-stats/introspect.js +171 -0
  400. package/src/nodes/tw-stats/publish-to.js +108 -0
  401. package/src/nodes/tw-stats/recompute.js +9 -0
  402. package/src/nodes/tw-stats/reset.js +16 -0
  403. package/src/nodes/tw-stats/update.js +113 -0
  404. package/src/nodes/unbalance/index.js +15 -0
  405. package/src/nodes/unbalance/init.js +59 -0
  406. package/src/nodes/unbalance/introspect.js +167 -0
  407. package/src/nodes/unbalance/publish-to.js +52 -0
  408. package/src/nodes/unbalance/recompute.js +11 -0
  409. package/src/nodes/unbalance/reset.js +10 -0
  410. package/src/nodes/unbalance/update.js +83 -0
  411. package/src/nodes/vector-distance/index.js +14 -0
  412. package/src/nodes/vector-distance/init.js +54 -0
  413. package/src/nodes/vector-distance/introspect.js +149 -0
  414. package/src/nodes/vector-distance/publish-to.js +35 -0
  415. package/src/nodes/vector-distance/recompute.js +11 -0
  416. package/src/nodes/vector-distance/reset.js +20 -0
  417. package/src/nodes/vector-distance/update.js +139 -0
  418. package/src/nodes/winnow/index.js +12 -0
  419. package/src/nodes/winnow/init.js +82 -0
  420. package/src/nodes/winnow/introspect.js +253 -0
  421. package/src/nodes/winnow/publish-to.js +51 -0
  422. package/src/nodes/winnow/recompute.js +14 -0
  423. package/src/nodes/winnow/reset.js +34 -0
  424. package/src/nodes/winnow/update.js +203 -0
  425. package/src/tools/training/features.js +227 -0
  426. package/src/tools/training/index.js +21 -0
  427. package/src/tools/training/logistic.js +238 -0
  428. package/src/tools/training/metrics.js +126 -0
  429. package/src/tools/training/scale.js +239 -0
  430. package/src/tools/training/validate.js +103 -0
  431. package/src/windowing/count-sliding/create.js +16 -0
  432. package/src/windowing/count-sliding/get-reverse-chronological-index.js +19 -0
  433. package/src/windowing/count-sliding/index.js +6 -0
  434. package/src/windowing/count-sliding/is-not-full.js +5 -0
  435. package/src/windowing/count-sliding/push.js +15 -0
  436. package/src/windowing/count-sliding/reset.js +11 -0
  437. package/src/windowing/count-sliding/size.js +5 -0
  438. package/src/windowing/time-sliding/create.js +25 -0
  439. package/src/windowing/time-sliding/push.js +42 -0
  440. package/CODE_OF_CONDUCT.md +0 -46
  441. package/CONTRIBUTING.md +0 -132
  442. package/docs/.nojekyll +0 -0
  443. package/docs/CONTRIBUTING.md +0 -134
  444. package/docs/README.md +0 -132
  445. package/docs/_coverpage.md +0 -26
  446. package/docs/_media/bg.png +0 -0
  447. package/docs/_media/wink-logo.png +0 -0
  448. package/docs/_navbar.md +0 -14
  449. package/docs/_sidebar.md +0 -2
  450. package/docs/index.html +0 -44
  451. package/eslint.config.js +0 -283
@@ -0,0 +1,767 @@
1
+ // @fileoverview
2
+ // update.js — hot path for swingWatch.
3
+ //
4
+ // ## What this file does
5
+ //
6
+ // On each new sample, this function maintains a sliding window (ring buffer)
7
+ // of the last W samples, then ranks every local extremum inside that window
8
+ // by its **topological persistence** — the peak-to-trough amplitude required
9
+ // to "kill" the feature. Only NEWLY-APPEARED persistence pairs (compared to
10
+ // the previous tick's diagram) are emitted as completion events. This design
11
+ // ensures downstream nodes like `appraise` see each significant event exactly
12
+ // once, on the tick it first becomes resolvable.
13
+ //
14
+ // ## Algorithm — Huber/Persistence1D, windowed batch
15
+ //
16
+ // The algorithm is the standard 1D sublevel-set persistence sweep, originally
17
+ // described for peak detection by Stefan Huber and implemented in Persistence1D
18
+ // by Kozlov & Weinkauf. It proceeds in three stages:
19
+ //
20
+ // 1. SORT the window's sample indices by value (ascending for minima,
21
+ // descending for maxima). This defines the order in which a rising
22
+ // "water level" sweeps through the signal.
23
+ //
24
+ // 2. SWEEP through the sorted indices. Each sample either:
25
+ // - Seeds a new connected component (if neither neighbour is processed)
26
+ // - Extends an existing component (if exactly one neighbour is processed)
27
+ // - Merges two components (if both neighbours are processed) — this is
28
+ // the "killing" event. The Elder Rule determines which component dies:
29
+ // the one whose birth value is HIGHER (= younger in the sublevel sweep).
30
+ // Persistence = merge height − dying component's birth value.
31
+ //
32
+ // 3. DIFF the resulting persistence diagram against the previous tick's
33
+ // diagram. Pairs that appear for the first time are emitted as completion
34
+ // events. Pairs that were already present (same physical samples, shifted
35
+ // by one in the window) are suppressed — and so are pairs minted by
36
+ // either window boundary (eviction re-formations at the left edge,
37
+ // provisional in-progress extrema at the right edge). See Section 3
38
+ // for the four suppression rules that guard the exactly-once contract.
39
+ //
40
+ // For maxima detection, the same algorithm runs on the negated signal — but
41
+ // instead of negating and re-sorting, we iterate the SAME sorted indices in
42
+ // reverse order (descending value = ascending in −f) and flip the Elder Rule.
43
+ //
44
+ // ## Performance
45
+ //
46
+ // - Complexity: O(W log W) per tick, dominated by the sort. V8's Timsort on
47
+ // a nearly-sorted array (one element changed per tick) approaches O(W).
48
+ // The union-find sweep is O(W α(W)) ≈ O(W).
49
+ // - At W=100 on M4 Max: ~11 µs/tick. On RPi (est. 6× slower): ~66 µs/tick.
50
+ // Headroom: 15K msg/sec on RPi, vs WiFi's 0.33 msg/sec or SCADA's 50.
51
+ //
52
+ // ## Zero allocation
53
+ //
54
+ // Every typed array is pre-allocated in init.js. This file creates no objects,
55
+ // arrays, or strings in the hot path. The single module-level object `mergeResult` is
56
+ // allocated once at import time and rewritten in-place on each merge — safe
57
+ // because the sweep is single-threaded and synchronous.
58
+ //
59
+ // ## References
60
+ //
61
+ // Huber, S. "Persistent Topology for Peak Detection."
62
+ // https://www.sthu.org/blog/13-perstopology-peakdetection/index.html
63
+ // Edelsbrunner, Letscher & Zomorodian (2002). "Topological Persistence
64
+ // and Simplification." Discrete & Computational Geometry, 28, 511–533.
65
+ // Kozlov & Weinkauf (2014). Persistence1D.
66
+ // https://github.com/weinkauf/Persistence1D
67
+
68
+ import { push, isNotFull } from '../../windowing/count-sliding/index.js';
69
+
70
+
71
+ // ═══════════════════════════════════════════════════════════════════════════
72
+ // SECTION 1: UNION-FIND
73
+ //
74
+ // The union-find (disjoint-set) data structure tracks connected components
75
+ // during the sweep. Each component is identified by a representative vertex.
76
+ // The representative carries the component's "birth value" — the value of
77
+ // the deepest local extremum that seeded it.
78
+ //
79
+ // Three parallel typed arrays (pre-allocated in init):
80
+ // ufParent[i] — parent pointer (self-referential for roots)
81
+ // ufBirthVal[i] — birth value of the component rooted at i
82
+ // ufBirthIdx[i] — birth index of the component rooted at i
83
+ //
84
+ // Path compression in find() ensures amortized near-O(1) per operation.
85
+ // No allocation: find() and merge write only to the pre-allocated arrays.
86
+ // ═══════════════════════════════════════════════════════════════════════════
87
+
88
+ // Module-level merge result object. Allocated once at import time, rewritten
89
+ // in-place by mergeSublevel/mergeSuperlevel. The caller reads the fields
90
+ // immediately before any subsequent merge call could overwrite them.
91
+ const mergeResult = { diedBirthVal: 0, diedBirthIdx: 0, persistence: 0, merged: false };
92
+
93
+ // Find the representative of the component containing vertex i.
94
+ // Uses path compression: after find(), every vertex on the path points
95
+ // directly to the root, making subsequent finds O(1).
96
+ const find = function ( parent, i ) {
97
+ let r = i;
98
+ while ( parent[ r ] !== r ) r = parent[ r ];
99
+ // Compress: point every visited node directly to root
100
+ let c = i;
101
+ while ( parent[ c ] !== r ) {
102
+ const next = parent[ c ];
103
+ parent[ c ] = r;
104
+ c = next;
105
+ }
106
+ return r;
107
+ }; // find()
108
+
109
+ // Merge two components WITHOUT emitting a persistence pair.
110
+ // Used for the "extension" case: when the current vertex has exactly one
111
+ // processed neighbour, it simply joins that neighbour's component. No
112
+ // component dies, so no pair is emitted.
113
+ //
114
+ // The Elder Rule still applies to decide which representative survives.
115
+ // For sublevel (ascending): lower birth value = older = survives.
116
+ // For superlevel (descending): higher birth value = older = survives.
117
+ // Ties are broken by birth index: earlier (smaller index) survives.
118
+
119
+ // Sublevel extension: higher birth value dies (younger in ascending sweep).
120
+ // Called as (p, p±1) from sweepMin. Only the `bVal[ra] > bVal[rb]` branch
121
+ // and the plateau tie-break's `bIdx[ra] > bIdx[rb]` branch fire in 1D:
122
+ // - ra = current vertex p (being processed), bVal[ra] = f(p).
123
+ // - rb = already-processed neighbor's root, bVal[rb] ≤ f(p).
124
+ // - Equal values only on plateaus, where extension always goes left-to-right
125
+ // (sublevel sorts ascending by idx within ties), so bIdx[ra]=p > bIdx[rb].
126
+ const mergeNoEmitSub = function ( parent, birthVal, birthIdx, a, b ) {
127
+ const ra = find( parent, a );
128
+ const rb = find( parent, b );
129
+
130
+ /* c8 ignore next -- defensive: left and right components are spatially disjoint in 1D sweep, so ra !== rb */
131
+ if ( ra === rb ) return;
132
+ if ( birthVal[ ra ] > birthVal[ rb ] ) {
133
+ parent[ ra ] = rb;
134
+
135
+ /* c8 ignore next 2 -- defensive: in 1D sublevel sweep, current vertex always has bVal ≥ processed neighbor's bVal */
136
+ } else if ( birthVal[ rb ] > birthVal[ ra ] ) {
137
+ parent[ rb ] = ra;
138
+ } else if ( birthIdx[ ra ] > birthIdx[ rb ] ) {
139
+ parent[ ra ] = rb;
140
+
141
+ /* c8 ignore next 3 -- defensive: sublevel plateau extension always has bIdx[ra]=p > bIdx[rb] */
142
+ } else {
143
+ parent[ rb ] = ra;
144
+ }
145
+ }; // mergeNoEmitSub()
146
+
147
+ // Superlevel extension: lower birth value dies (younger in descending sweep).
148
+ // Called as (p, p±1) from sweepMax, which iterates sortedIndices backward.
149
+ // Only the `bVal[ra] < bVal[rb]` branch and the plateau tie-break's ELSE
150
+ // branch fire in 1D:
151
+ // - Current vertex always has bVal ≤ processed neighbor.
152
+ // - Equal values only on plateaus, where backward iteration makes highest
153
+ // idx the seed; subsequent lower-idx extensions go right, giving
154
+ // bIdx[ra]=p < bIdx[rb].
155
+ const mergeNoEmitSup = function ( parent, birthVal, birthIdx, a, b ) {
156
+ const ra = find( parent, a );
157
+ const rb = find( parent, b );
158
+
159
+ /* c8 ignore next -- defensive: left and right components are spatially disjoint in 1D sweep */
160
+ if ( ra === rb ) return;
161
+ if ( birthVal[ ra ] < birthVal[ rb ] ) {
162
+ parent[ ra ] = rb;
163
+
164
+ /* c8 ignore next 3 -- defensive: in 1D superlevel sweep, current vertex always has bVal ≤ processed neighbor's bVal */
165
+ } else if ( birthVal[ rb ] < birthVal[ ra ] ) {
166
+ parent[ rb ] = ra;
167
+
168
+ /* c8 ignore next 3 -- defensive: superlevel plateau extension always has bIdx[ra]=p < bIdx[rb] (backward iteration) */
169
+ } else if ( birthIdx[ ra ] > birthIdx[ rb ] ) {
170
+ parent[ ra ] = rb;
171
+ } else {
172
+ parent[ rb ] = ra;
173
+ }
174
+ }; // mergeNoEmitSup()
175
+
176
+ // Merge two components WITH pair emission — SUBLEVEL sweep (ascending).
177
+ //
178
+ // This is called at a "merge point" where both neighbours of the current
179
+ // vertex are already processed. Two distinct components meet for the first
180
+ // time. The Elder Rule kills the younger component (higher birth value).
181
+ // The dying component's birth is paired with the current vertex (the merge
182
+ // height). Persistence = mergeHeight − dyingBirthValue.
183
+ //
184
+ // The result is written to the module-level mergeResult object (no allocation).
185
+ const mergeSublevel = function ( parent, birthVal, birthIdx, a, b, h ) {
186
+ const ra = find( parent, a );
187
+ const rb = find( parent, b );
188
+
189
+ /* c8 ignore next 4 -- defensive: LEFT and RIGHT components are disjoint in 1D before this merge point p, so ra !== rb */
190
+ if ( ra === rb ) {
191
+ mergeResult.merged = false;
192
+ return;
193
+ }
194
+ let loser;
195
+ let winner;
196
+ if ( birthVal[ ra ] > birthVal[ rb ] ) {
197
+ loser = ra;
198
+ winner = rb;
199
+ } else if ( birthVal[ rb ] > birthVal[ ra ] ) {
200
+ loser = rb;
201
+ winner = ra;
202
+
203
+ /* c8 ignore next 3 -- defensive: ra roots the LEFT component (bIdx in [0, p-1]), rb roots the RIGHT (bIdx in [p+1, W-1]), so bIdx[ra] < bIdx[rb] always */
204
+ } else if ( birthIdx[ ra ] > birthIdx[ rb ] ) {
205
+ loser = ra;
206
+ winner = rb;
207
+ } else {
208
+ loser = rb;
209
+ winner = ra;
210
+ }
211
+ mergeResult.diedBirthVal = birthVal[ loser ];
212
+ mergeResult.diedBirthIdx = birthIdx[ loser ];
213
+ mergeResult.persistence = h - birthVal[ loser ];
214
+ mergeResult.merged = true;
215
+ parent[ loser ] = winner;
216
+ }; // mergeSublevel()
217
+
218
+ // Merge two components WITH pair emission — SUPERLEVEL sweep (descending).
219
+ //
220
+ // Symmetric to sublevel: here we sweep from the highest value downward
221
+ // (equivalently, we run a sublevel sweep on the negated signal). The Elder
222
+ // Rule is flipped: the component with the LOWER birth value (= younger in
223
+ // the descending sweep, born later as the water drains) dies.
224
+ // Persistence = dyingBirthValue − mergeHeight (positive because birth > merge).
225
+ const mergeSuperlevel = function ( parent, birthVal, birthIdx, a, b, h ) {
226
+ const ra = find( parent, a );
227
+ const rb = find( parent, b );
228
+
229
+ /* c8 ignore next 4 -- defensive: LEFT and RIGHT components are disjoint in 1D before this merge point p, so ra !== rb */
230
+ if ( ra === rb ) {
231
+ mergeResult.merged = false;
232
+ return;
233
+ }
234
+ let loser;
235
+ let winner;
236
+ // Descending sweep: higher birthVal = older. Lower birthVal dies.
237
+ if ( birthVal[ ra ] < birthVal[ rb ] ) {
238
+ loser = ra;
239
+ winner = rb;
240
+ } else if ( birthVal[ rb ] < birthVal[ ra ] ) {
241
+ loser = rb;
242
+ winner = ra;
243
+
244
+ /* c8 ignore next 3 -- defensive: ra roots the LEFT component (bIdx in [0, p-1]), rb roots the RIGHT (bIdx in [p+1, W-1]), so bIdx[ra] < bIdx[rb] always */
245
+ } else if ( birthIdx[ ra ] > birthIdx[ rb ] ) {
246
+ loser = ra;
247
+ winner = rb;
248
+ } else {
249
+ loser = rb;
250
+ winner = ra;
251
+ }
252
+ mergeResult.diedBirthVal = birthVal[ loser ];
253
+ mergeResult.diedBirthIdx = birthIdx[ loser ];
254
+ mergeResult.persistence = birthVal[ loser ] - h;
255
+ mergeResult.merged = true;
256
+ parent[ loser ] = winner;
257
+ }; // mergeSuperlevel()
258
+
259
+
260
+ // ═══════════════════════════════════════════════════════════════════════════
261
+ // SECTION 2: PERSISTENCE SWEEPS
262
+ //
263
+ // Two sweep functions: sweepMin (sublevel, detects significant local minima)
264
+ // and sweepMax (superlevel, detects significant local maxima).
265
+ //
266
+ // Both follow the same structure — the Huber/Persistence1D algorithm:
267
+ //
268
+ // For each vertex p in sorted order:
269
+ // Check which of its immediate neighbours (p-1, p+1) are already
270
+ // processed. Four cases:
271
+ //
272
+ // NEITHER processed → p seeds a new component. Its birth value is f(p).
273
+ //
274
+ // ONE processed → p extends that neighbour's component. No pair is
275
+ // emitted because no two distinct components are meeting.
276
+ //
277
+ // BOTH processed → p is a merge point where two components meet. First
278
+ // extend p into the left component (mergeNoEmit), then merge the
279
+ // combined left+p component with the right component (merge with emit).
280
+ // The second merge produces the persistence pair.
281
+ //
282
+ // sweepMin iterates sortedIndices forward (ascending value).
283
+ // sweepMax iterates sortedIndices backward (descending value) — this is
284
+ // equivalent to sorting by −f ascending, without negating or re-sorting.
285
+ // The union-find arrays (ufParent, ufBirthVal, ufBirthIdx) and processed
286
+ // flags are cleared between the two sweeps and reused.
287
+ //
288
+ // Pairs whose persistence < threshold (eps) are filtered in-place. Surviving
289
+ // pairs are stored in the pre-allocated state.min*Arr / state.max*Arr typed
290
+ // arrays (parallel arrays instead of object arrays — zero allocation).
291
+ // ═══════════════════════════════════════════════════════════════════════════
292
+
293
+ // Sublevel sweep — detects significant local MINIMA.
294
+ // A local minimum is "born" when the water level first touches it (its value
295
+ // is the birth). It "dies" when the rising water level connects it to an
296
+ // older (deeper) minimum at a local maximum between them (the merge height).
297
+ // Persistence = merge height − birth value = amplitude of the oscillation
298
+ // this minimum participates in.
299
+ const sweepMin = function ( state, lin, W, eps ) {
300
+ const sorted = state.sortedIndices;
301
+ const parent = state.ufParent;
302
+ const bVal = state.ufBirthVal;
303
+ const bIdx = state.ufBirthIdx;
304
+ const processed = state.processed;
305
+
306
+ processed.fill( 0 );
307
+ state.minPairCount = 0;
308
+
309
+ for ( let k = 0; k < W; k += 1 ) {
310
+ const p = sorted[ k ];
311
+ const fP = lin[ p ];
312
+
313
+ // Check which neighbours are already in the processed set
314
+ const leftOK = p > 0 && processed[ p - 1 ] === 1;
315
+ const rightOK = p < ( W - 1 ) && processed[ p + 1 ] === 1;
316
+
317
+ // Initialize this vertex as its own component in the union-find
318
+ parent[ p ] = p;
319
+ bVal[ p ] = fP;
320
+ bIdx[ p ] = p;
321
+
322
+ if ( !leftOK && !rightOK ) {
323
+ // SEED: no processed neighbours → new component born at f(p).
324
+ // Nothing to merge. The component will grow as neighbours are
325
+ // processed later in the sweep.
326
+ } else if ( leftOK && !rightOK ) {
327
+ // EXTEND LEFT: only the left neighbour is processed. p joins
328
+ // that component. No second component is involved, so no pair.
329
+ mergeNoEmitSub( parent, bVal, bIdx, p, p - 1 );
330
+ } else if ( !leftOK && rightOK ) {
331
+ // EXTEND RIGHT: symmetric to extend-left.
332
+ mergeNoEmitSub( parent, bVal, bIdx, p, p + 1 );
333
+ } else {
334
+ // MERGE: both neighbours are processed. Two distinct components
335
+ // meet at p. First extend p into the left component (no pair),
336
+ // then merge the combined {left ∪ p} with the right component.
337
+ // The second merge is where the Elder Rule applies and the
338
+ // dying component's pair is recorded.
339
+ mergeNoEmitSub( parent, bVal, bIdx, p, p - 1 );
340
+ mergeSublevel( parent, bVal, bIdx, p, p + 1, fP );
341
+ if ( mergeResult.merged && mergeResult.persistence >= eps ) {
342
+ const i = state.minPairCount;
343
+ if ( i < state.maxPairs ) {
344
+ state.minBirthValArr[ i ] = mergeResult.diedBirthVal;
345
+ state.minBirthIdxArr[ i ] = mergeResult.diedBirthIdx;
346
+ state.minDeathValArr[ i ] = fP;
347
+ state.minDeathIdxArr[ i ] = p;
348
+ state.minPersArr[ i ] = mergeResult.persistence;
349
+ state.minPairCount = i + 1;
350
+ }
351
+ }
352
+ }
353
+ processed[ p ] = 1;
354
+ }
355
+ }; // sweepMin()
356
+
357
+ // Superlevel sweep — detects significant local MAXIMA.
358
+ // Symmetric to sweepMin but iterates in REVERSE through sortedIndices
359
+ // (descending value = the water level dropping from above). A local maximum
360
+ // is "born" when the draining water first exposes it. It "dies" when the
361
+ // water drops low enough to connect it to an older (higher) maximum.
362
+ // Persistence = birth value − merge height.
363
+ //
364
+ // Implementation note: instead of negating the signal and re-sorting, we
365
+ // reuse the same ascending sort and iterate backward. This halves the sort
366
+ // cost and avoids allocating a negated copy of the signal. The Elder Rule
367
+ // comparison is flipped (lower birth dies), and persistence is computed as
368
+ // birthVal − h (instead of h − birthVal) to keep it positive.
369
+ const sweepMax = function ( state, lin, W, eps ) {
370
+ const sorted = state.sortedIndices;
371
+ const parent = state.ufParent;
372
+ const bVal = state.ufBirthVal;
373
+ const bIdx = state.ufBirthIdx;
374
+ const processed = state.processed;
375
+
376
+ processed.fill( 0 );
377
+ state.maxPairCount = 0;
378
+
379
+ for ( let k = W - 1; k >= 0; k -= 1 ) {
380
+ const p = sorted[ k ];
381
+ const fP = lin[ p ];
382
+
383
+ const leftOK = p > 0 && processed[ p - 1 ] === 1;
384
+ const rightOK = p < ( W - 1 ) && processed[ p + 1 ] === 1;
385
+
386
+ parent[ p ] = p;
387
+ bVal[ p ] = fP;
388
+ bIdx[ p ] = p;
389
+
390
+ if ( !leftOK && !rightOK ) {
391
+ // SEED
392
+ } else if ( leftOK && !rightOK ) {
393
+ mergeNoEmitSup( parent, bVal, bIdx, p, p - 1 );
394
+ } else if ( !leftOK && rightOK ) {
395
+ mergeNoEmitSup( parent, bVal, bIdx, p, p + 1 );
396
+ } else {
397
+ // MERGE — Elder Rule flipped for superlevel
398
+ mergeNoEmitSup( parent, bVal, bIdx, p, p - 1 );
399
+ mergeSuperlevel( parent, bVal, bIdx, p, p + 1, fP );
400
+ if ( mergeResult.merged && mergeResult.persistence >= eps ) {
401
+ const i = state.maxPairCount;
402
+ if ( i < state.maxPairs ) {
403
+ state.maxBirthValArr[ i ] = mergeResult.diedBirthVal;
404
+ state.maxBirthIdxArr[ i ] = mergeResult.diedBirthIdx;
405
+ state.maxDeathValArr[ i ] = fP;
406
+ state.maxDeathIdxArr[ i ] = p;
407
+ state.maxPersArr[ i ] = mergeResult.persistence;
408
+ state.maxPairCount = i + 1;
409
+ }
410
+ }
411
+ }
412
+ processed[ p ] = 1;
413
+ }
414
+ }; // sweepMax()
415
+
416
+
417
+ // ═══════════════════════════════════════════════════════════════════════════
418
+ // SECTION 3: PAIR DIFFING
419
+ //
420
+ // The persistence diagram changes incrementally as the window slides: one
421
+ // sample enters on the right, one sample exits on the left. Most pairs
422
+ // persist between ticks (same physical samples, shifted by one position in
423
+ // the linearized window). Only NEWLY-APPEARED pairs should fire completion
424
+ // events — otherwise downstream nodes like `appraise` would see the same
425
+ // event re-emitted on every tick.
426
+ //
427
+ // The diffing works by comparing each current-tick pair (birthIdx, deathIdx)
428
+ // against the previous tick's pairs with an INDEX SHIFT correction:
429
+ //
430
+ // Previous tick's linearized index I → current tick's index (I − 1)
431
+ //
432
+ // because the oldest sample (at index 0 in the previous tick) has been
433
+ // evicted, and all other samples have shifted left by one position.
434
+ //
435
+ // A pair is "same" if prev.birthIdx − 1 === cur.birthIdx AND
436
+ // prev.deathIdx − 1 === cur.deathIdx. Pairs where either shifted index < 0
437
+ // have fallen off the window and are not candidates.
438
+ //
439
+ // BOUNDARY ARTIFACTS. Index-shift matching alone under-suppresses: both
440
+ // window edges mint pairs that are not new physical events. At the left
441
+ // edge, evicting the oldest sample can re-form a pair for a swing that was
442
+ // already reported, with different indices, so a matcher calls it "new"
443
+ // (confirmed on real plant data, 2026-07-20 value study: 46–78% of raw
444
+ // completions were such re-fires). At the right edge, a descent in progress
445
+ // births a provisional pair at the newest sample on every tick, re-reporting
446
+ // the same still-deepening swing. Emission is therefore flag-based — a pair
447
+ // emits exactly when its birth vertex has never been accounted — under four
448
+ // rules:
449
+ //
450
+ // (a) LEFT-BOUNDARY BIRTH — a pair whose birth vertex is window index 0
451
+ // never emits. The swing's left half is cut off by the window, so the
452
+ // birth can never be certified as a real extremum. This also stops
453
+ // the every-tick "walk" where an evicted deep swing's shoulder keeps
454
+ // re-forming a boundary pair as it slides out.
455
+ // (b) REBIRTH — a pair that inherits its death vertex from a
456
+ // previous-tick pair whose birth vertex was just evicted (previous
457
+ // birthIdx 0) is the same swing re-formed around a surviving ripple,
458
+ // not a new swing.
459
+ // (c) PER-VERTEX ACCOUNTING — each birth vertex drives at most one
460
+ // emission while it lives in the window. A per-ring-slot flag records
461
+ // vertices already accounted (emitted, or recognized as a duplicate);
462
+ // the flag clears when the slot is overwritten by a new sample. This
463
+ // catches re-pairings that (a) and (b) cannot see (e.g. a completed
464
+ // minimum re-pairing at a different death vertex after an elder
465
+ // eviction) and skips persisting pairs without index matching.
466
+ // (d) RIGHT-BOUNDARY BIRTH — a pair whose birth vertex is the newest
467
+ // sample (index W−1) is provisional: the extremum cannot be certified
468
+ // until its right neighbour arrives. It stays stored but unaccounted;
469
+ // it emits one tick after the true turning point, when the re-derived
470
+ // pair has birth index < W−1 and a still-clear flag.
471
+ //
472
+ // The STORED pair arrays remain the full finite-domain persistence diagram
473
+ // of the window (matching the GUDHI golden truth); the rules govern only
474
+ // which pairs surface as completion events.
475
+ //
476
+ // Deliberate consequences of these rules:
477
+ // - A genuine swing spanning the entire window (birth exactly at index 0
478
+ // when first resolvable) is never emitted — the price of (a). Size
479
+ // windowSize with margin over the slowest swing of interest.
480
+ // - Completions surface one sample after the extremum is confirmed — the
481
+ // price of (d), and the semantics a streaming consumer wants (no
482
+ // retractions of provisional events).
483
+ // - A swing revealed by truncation (an elder's interior ripple that flips
484
+ // younger after the elder's bottom evicts) still emits, once. It was
485
+ // never reported before, so this is not a duplicate.
486
+ //
487
+ // For each NEW pair, we select the one with the largest persistence as
488
+ // the completion event for this tick. If multiple new pairs appear (rare
489
+ // but possible when the new sample triggers a cascade of merges), only
490
+ // the deepest one drives the completion slots; `swingsThisTick` records
491
+ // the total count of genuinely new pairs.
492
+ // ═══════════════════════════════════════════════════════════════════════════
493
+
494
+ // Diff one direction's current pairs against previous tick and fire
495
+ // completion events for genuinely new pairs only, applying the three
496
+ // eviction-artifact suppression rules documented in the section banner.
497
+ // curBirthIdx/curDeathIdx/curPersist/curBirthVal are the current tick's
498
+ // parallel arrays; prevBirthIdx/prevDeathIdx/prevCount are previous tick's.
499
+ // keys is state.fieldKeys.min or state.fieldKeys.max (pre-built at init) —
500
+ // avoids per-tick string concatenation for the four target field names.
501
+ // flagArr is the direction's per-ring-slot accounted flag (rule c); head is
502
+ // the ring's oldest-sample slot, so window index i maps to ring slot
503
+ // (head + i) mod W.
504
+ // Account one current-tick pair (guard-clause form). Applies the four
505
+ // suppression rules and writes the completion slots for a genuinely new
506
+ // pair. Returns nothing; all effects land on state and flagArr.
507
+ //
508
+ // Emission is flag-based, not match-based: a pair emits exactly when its
509
+ // birth vertex has never been accounted (flag clear) and no rule blocks
510
+ // it. Persisting pairs are skipped by their standing flag, so no explicit
511
+ // same-pair index matching is needed.
512
+ const accountPair = function ( state, W, cb, cd, birthVal, pers,
513
+ prevBirthIdx, prevDeathIdx, prevCount, keys, flagArr, head ) {
514
+ // Rule (a): left-boundary birth — the swing's left half is cut off by
515
+ // the window, so the extremum can never be certified. Never emits and
516
+ // never flags (the slot is overwritten next tick anyway).
517
+ if ( cb === 0 ) return;
518
+
519
+ // Rule (d): right-boundary birth — the newest sample cannot yet be
520
+ // certified as an extremum because its right neighbour has not arrived.
521
+ // The pair stays stored but unaccounted; once a confirming sample
522
+ // lands, the re-derived pair (birth index < W−1, flag still clear)
523
+ // emits through the normal path below.
524
+ if ( cb === ( W - 1 ) ) return;
525
+
526
+ // Rule (b): a pair inheriting its death vertex from a previous-tick
527
+ // pair whose birth vertex was just evicted (previous birthIdx 0) is
528
+ // the same swing re-formed around a surviving ripple, not a new swing.
529
+ let rebirth = false;
530
+ for ( let j = 0; j < prevCount; j += 1 ) {
531
+ if ( ( ( prevDeathIdx[ j ] - 1 ) === cd ) && ( prevBirthIdx[ j ] === 0 ) ) {
532
+ rebirth = true;
533
+ break;
534
+ }
535
+ }
536
+
537
+ // Map the birth's window index to its ring slot (avoids %).
538
+ let slot = head + cb;
539
+ if ( slot >= W ) slot -= W;
540
+
541
+ if ( rebirth || ( flagArr[ slot ] === 1 ) ) {
542
+ // Rules (b)/(c): a duplicate of an already-accounted swing. Mark
543
+ // the vertex so later re-pairings stay suppressed; emit nothing.
544
+ flagArr[ slot ] = 1;
545
+ return;
546
+ }
547
+
548
+ // Genuinely new, certified completion.
549
+ flagArr[ slot ] = 1;
550
+ state.swingsThisTick += 1;
551
+ if ( !state[ keys.completed ] || ( pers > state[ keys.persistence ] ) ) {
552
+ state[ keys.completed ] = true;
553
+ state[ keys.birthValue ] = birthVal;
554
+ state[ keys.birthLag ] = ( W - 1 ) - cb;
555
+ state[ keys.persistence ] = pers;
556
+ }
557
+ }; // accountPair()
558
+
559
+ const diffOneSide = function ( state, W, curCount, curBirthIdxArr, curDeathIdxArr,
560
+ curBirthValArr, curPersArr, prevBirthIdx, prevDeathIdx, prevCount, keys,
561
+ flagArr, head ) {
562
+ for ( let i = 0; i < curCount; i += 1 ) {
563
+ accountPair( state, W, curBirthIdxArr[ i ], curDeathIdxArr[ i ],
564
+ curBirthValArr[ i ], curPersArr[ i ],
565
+ prevBirthIdx, prevDeathIdx, prevCount, keys, flagArr, head );
566
+ }
567
+ }; // diffOneSide()
568
+
569
+ const diffAndEmit = function ( state, W ) {
570
+ const head = state.ring.head;
571
+ if ( state.direction === 'both' || state.direction === 'dips' ) {
572
+ diffOneSide( state, W, state.minPairCount,
573
+ state.minBirthIdxArr, state.minDeathIdxArr,
574
+ state.minBirthValArr, state.minPersArr,
575
+ state.prevMinBirthIdx, state.prevMinDeathIdx,
576
+ state.prevMinPairCount, state.fieldKeys.min,
577
+ state.minEmitFlag, head );
578
+ }
579
+ if ( state.direction === 'both' || state.direction === 'peaks' ) {
580
+ diffOneSide( state, W, state.maxPairCount,
581
+ state.maxBirthIdxArr, state.maxDeathIdxArr,
582
+ state.maxBirthValArr, state.maxPersArr,
583
+ state.prevMaxBirthIdx, state.prevMaxDeathIdx,
584
+ state.prevMaxPairCount, state.fieldKeys.max,
585
+ state.maxEmitFlag, head );
586
+ }
587
+ if ( state.dipCompleted ) state.emitted += 1;
588
+ if ( state.peakCompleted ) state.emitted += 1;
589
+ }; // diffAndEmit()
590
+
591
+ // Copy current tick's pair indices to previous-tick storage for the next
592
+ // tick's diff comparison. Only indices are needed (not values or persistence)
593
+ // because the diff compares by position, not by magnitude.
594
+ const copyToPrev = function ( state ) {
595
+ for ( let i = 0; i < state.minPairCount; i += 1 ) {
596
+ state.prevMinBirthIdx[ i ] = state.minBirthIdxArr[ i ];
597
+ state.prevMinDeathIdx[ i ] = state.minDeathIdxArr[ i ];
598
+ }
599
+ state.prevMinPairCount = state.minPairCount;
600
+
601
+ for ( let i = 0; i < state.maxPairCount; i += 1 ) {
602
+ state.prevMaxBirthIdx[ i ] = state.maxBirthIdxArr[ i ];
603
+ state.prevMaxDeathIdx[ i ] = state.maxDeathIdxArr[ i ];
604
+ }
605
+ state.prevMaxPairCount = state.maxPairCount;
606
+ }; // copyToPrev()
607
+
608
+
609
+ // ═══════════════════════════════════════════════════════════════════════════
610
+ // SECTION 4: MAIN UPDATE
611
+ //
612
+ // The per-message entry point. Called by the wiring layer for every sample.
613
+ //
614
+ // Flow:
615
+ // Resolve the adaptive threshold from the tunable, applying the absolute floor.
616
+ // The try/catch pattern with log suppression follows the threshold node's
617
+ // convention for handling tunable errors gracefully.
618
+ const resolveThreshold = function ( state, msg ) {
619
+ try {
620
+ state.currentThreshold = state.thresholdFn( msg );
621
+ if ( state.tunableErrorLogged ) {
622
+ state.tunableErrorLogged = false;
623
+ }
624
+ } catch ( error ) {
625
+ if ( !state.tunableErrorLogged ) {
626
+ state.tunableErrorLogged = true;
627
+ const errMsg = 'WinkComposer/' + state.nodeType + ': tunable threw: ' + error.message;
628
+ console.error( errMsg );
629
+ }
630
+ }
631
+ const epsBase = state.currentThreshold;
632
+ // Apply the absolute floor: effective ε = max(resolved, minAbsoluteThreshold).
633
+ // Required for quiet signals where adaptive σ drops below the measurement
634
+ // noise floor — empirically validated on WiFi RSSI (see feasibility study).
635
+ return ( epsBase < state.minAbsoluteThreshold ) ? state.minAbsoluteThreshold : epsBase;
636
+ }; // resolveThreshold()
637
+
638
+ // 1. Guard (disable / pause)
639
+ // 2. Reset per-tick output slots (output scrubbing)
640
+ // 3. Extract and validate input
641
+ // 4. Resolve adaptive threshold
642
+ // 5. Push sample to ring buffer
643
+ // 6. Wait for warmup (window not yet full)
644
+ // 7. Linearize the circular ring buffer into a contiguous array
645
+ // 8. Sort sample indices by value (ascending)
646
+ // 9. Run sublevel sweep for minima (if direction allows)
647
+ // 10. Run superlevel sweep for maxima (if direction allows)
648
+ // 11. Diff current diagram against previous tick's diagram
649
+ // 12. Copy current pairs to previous-tick storage
650
+ // 13. Update diagnostics
651
+ //
652
+ // Steps 7–12 are the "batch per window" core. Their combined cost is
653
+ // O(W log W) dominated by the sort, with O(W) for the sweeps and O(P²)
654
+ // for the diff where P is the pair count (typically 5–20, negligible).
655
+ // ═══════════════════════════════════════════════════════════════════════════
656
+
657
+ const update = function ( state, msg ) {
658
+ // ── 1. Control guard ─────────────────────────────────────
659
+ if ( state.disable || state.pause ) return state;
660
+
661
+ // ── 2. Reset per-tick completion slots ───────────────────
662
+ // Output scrubbing: downstream must never read stale completion data
663
+ // from a previous tick. Set all slots to their "no event" state. The
664
+ // momentsDigest node establishes this convention — non-event ticks
665
+ // publish `undefined` for detail fields, not stale values.
666
+ state.dipCompleted = false;
667
+ state.peakCompleted = false;
668
+ state.dipValue = NaN;
669
+ state.peakValue = NaN;
670
+ state.dipLag = 0;
671
+ state.peakLag = 0;
672
+ state.dipSize = NaN;
673
+ state.peakSize = NaN;
674
+ state.swingsThisTick = 0;
675
+
676
+ // ── 3. Extract and validate input ────────────────────────
677
+ const xVal = msg[ state.x ];
678
+ state.inputValidationFailed = false;
679
+
680
+ if ( !Number.isFinite( xVal ) ) {
681
+ state.inputValidationFailed = true;
682
+ return state;
683
+ }
684
+
685
+ // ── 4. Resolve adaptive threshold ────────────────────────
686
+ const eps = resolveThreshold( state, msg );
687
+
688
+ // ── 5. Push to ring buffer ───────────────────────────────
689
+ // The slot being overwritten is the ring's head (next write position =
690
+ // oldest sample). Clear its accounted-emission flags so the incoming
691
+ // sample starts life unflagged (rule c bookkeeping).
692
+ const evictSlot = state.ring.head;
693
+ push( state.ring, xVal );
694
+ state.minEmitFlag[ evictSlot ] = 0;
695
+ state.maxEmitFlag[ evictSlot ] = 0;
696
+ state.received += 1;
697
+
698
+ // ── 6. Warmup gate ───────────────────────────────────────
699
+ // Do not run the sweep until the ring buffer is full. During warmup,
700
+ // completion slots remain in their "no event" state and publishTo
701
+ // will return without writing any fields to the message.
702
+ if ( isNotFull( state.ring ) ) return state;
703
+
704
+ const W = state.windowSize;
705
+
706
+ // ── 7. Linearize ring → contiguous array ─────────────────
707
+ // The ring buffer is circular: ring.head points to the next write
708
+ // position, which is also the OLDEST sample (it will be overwritten
709
+ // next tick). We need a contiguous array where index 0 = oldest and
710
+ // index W−1 = newest, because the sort and sweep operate on contiguous
711
+ // indices.
712
+ //
713
+ // Manual indexed loops avoid subarray()'s per-call view-object
714
+ // allocation. At W ≤ 256 the two loops iterate at most W elements
715
+ // total on typed arrays — V8 vectorizes trivially, zero allocation.
716
+ const buf = state.ring.buffer;
717
+ const head = state.ring.head;
718
+ const lin = state.linear;
719
+ const firstLen = W - head;
720
+ for ( let i = 0; i < firstLen; i += 1 ) lin[ i ] = buf[ head + i ];
721
+ for ( let i = 0; i < head; i += 1 ) lin[ firstLen + i ] = buf[ i ];
722
+
723
+ // ── 8. Sort indices by value ─────────────────────────────
724
+ // Initialize sortedIndices to [0, 1, 2, ..., W−1], then sort by
725
+ // linear[i] ascending. V8's Timsort detects existing runs — since the
726
+ // window changes by only one sample per tick, most of the sorted order
727
+ // is preserved, and Timsort approaches O(W) in practice.
728
+ //
729
+ // Tie-breaking by index (a − b) ensures deterministic results and
730
+ // matches Persistence1D's convention: earlier samples process first
731
+ // when values are equal. Comparator is pre-built in init to avoid
732
+ // per-tick closure allocation.
733
+ for ( let i = 0; i < W; i += 1 ) state.sortedIndices[ i ] = i;
734
+ state.sortedIndices.sort( state.compareIndices );
735
+
736
+ // ── 9. Sweep for minima ──────────────────────────────────
737
+ if ( state.direction === 'both' || state.direction === 'dips' ) {
738
+ sweepMin( state, lin, W, eps );
739
+ }
740
+
741
+ // ── 10. Sweep for maxima ─────────────────────────────────
742
+ // Uses the SAME sortedIndices (iterated backward) and resets the same
743
+ // uf* and processed arrays. No re-sort needed.
744
+ if ( state.direction === 'both' || state.direction === 'peaks' ) {
745
+ sweepMax( state, lin, W, eps );
746
+ }
747
+
748
+ // ── 11. Diff and emit ────────────────────────────────────
749
+ diffAndEmit( state, W );
750
+
751
+ // ── 12. Copy current → prev ──────────────────────────────
752
+ copyToPrev( state );
753
+
754
+ // ── 13. Update diagnostic ────────────────────────────────
755
+ // Cumulative swings per received sample. Can exceed 1.0 in
756
+ // direction='both' when ticks complete both a dip and a peak.
757
+ // The ternary's `: 0` branch is unreachable — received was incremented
758
+ // earlier in this function (step 3) and is therefore ≥ 1 here.
759
+ /* c8 ignore next 3 -- defensive: received ≥ 1 by line 561 */
760
+ state.swingRate = ( state.received > 0 ) ?
761
+ state.emitted / state.received :
762
+ 0;
763
+
764
+ return state;
765
+ }; // update()
766
+
767
+ export default update;