@winkjs/composer 0.5.1 → 0.7.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 (125) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/composer.js +1 -1
  4. package/src/core/batch/aoo-encoder.js +4 -4
  5. package/src/core/batch/sliding-window.js +2 -2
  6. package/src/core/codec/index.js +4 -4
  7. package/src/core/emitter-manager/mqtt/connection.js +209 -0
  8. package/src/core/emitter-manager/mqtt/constants.js +43 -29
  9. package/src/core/emitter-manager/mqtt/emitter.js +108 -633
  10. package/src/core/emitter-manager/mqtt/health.js +151 -0
  11. package/src/core/emitter-manager/mqtt/index.js +68 -4
  12. package/src/core/emitter-manager/mqtt/publish.js +292 -0
  13. package/src/core/emitter-manager/mqtt/resolve-config.js +312 -0
  14. package/src/core/emitter-manager/mqtt/shutdown-drain.js +247 -0
  15. package/src/core/emitter-manager/terminal/assert-columns.js +1 -1
  16. package/src/core/emitter-manager/terminal/index.js +20 -5
  17. package/src/core/env-vars.js +143 -16
  18. package/src/core/logger/index.js +174 -0
  19. package/src/core/mqtt-protocol.js +2 -2
  20. package/src/core/partition-manager/get-stats.js +31 -0
  21. package/src/core/partition-manager/index.js +1 -0
  22. package/src/core/partition-manager/init.js +15 -0
  23. package/src/core/partition-manager/update.js +143 -87
  24. package/src/core/partition-manager/validate-flow.js +1 -1
  25. package/src/core/semantics/loader.js +1 -1
  26. package/src/core/semantics/warnings.js +5 -3
  27. package/src/core/shutdown-manager/index.js +71 -21
  28. package/src/core/source-manager/csv/start.js +109 -35
  29. package/src/core/source-manager/mqtt/client.js +276 -83
  30. package/src/core/source-manager/mqtt/dedup.js +19 -14
  31. package/src/core/source-manager/mqtt/index.js +33 -6
  32. package/src/core/source-manager/mqtt/status.js +211 -46
  33. package/src/core/source-manager/record-shape.js +55 -0
  34. package/src/core/source-manager/test-harness/field-generator.js +1 -1
  35. package/src/core/source-manager/test-harness/start.js +52 -10
  36. package/src/core/source-manager/test-harness/validate.js +1 -1
  37. package/src/core/storage-manager/questdb/address-policy.js +264 -0
  38. package/src/core/storage-manager/questdb/assert-columns.js +2 -2
  39. package/src/core/storage-manager/questdb/delivery-gate.js +218 -0
  40. package/src/core/storage-manager/questdb/ensure-tables.js +1 -1
  41. package/src/core/storage-manager/questdb/flush-engine.js +852 -0
  42. package/src/core/storage-manager/questdb/flush-tracker.js +331 -0
  43. package/src/core/storage-manager/questdb/index.js +592 -827
  44. package/src/core/storage-manager/questdb/persist-plan.js +147 -162
  45. package/src/core/storage-manager/questdb/resolve-options.js +403 -0
  46. package/src/core/storage-manager/questdb/shutdown-drain.js +181 -0
  47. package/src/core/storage-manager/questdb/skip-warnings.js +180 -0
  48. package/src/core/test-utils/tcp-proxy.js +194 -1
  49. package/src/core/tunable/helpers.js +1 -1
  50. package/src/core/utils/address/index.js +362 -0
  51. package/src/core/utils/address/probe.js +210 -0
  52. package/src/core/utils/callback-guard/index.js +176 -0
  53. package/src/core/utils/clock/index.js +34 -0
  54. package/src/core/utils/half-life/index.js +4 -4
  55. package/src/core/utils/jitter/index.js +51 -0
  56. package/src/core/utils/line-rate/index.js +104 -0
  57. package/src/core/utils/quantize/index.js +43 -19
  58. package/src/core/utils/validate/composer-validators.js +2 -2
  59. package/src/core/utils/validate/index.js +1 -1
  60. package/src/core/utils/validate/validate.js +1 -1
  61. package/src/core/wiring/assert-handle.js +5 -5
  62. package/src/core/wiring/assert-module.js +2 -2
  63. package/src/core/wiring/wire-emitters.js +7 -4
  64. package/src/core/wiring/wire-linear-graph.js +1 -1
  65. package/src/core/wiring/wire-node.js +6 -6
  66. package/src/core/wiring/wire-semantics.js +1 -1
  67. package/src/core/wiring/wire-storages.js +14 -5
  68. package/src/flow/driver.js +31 -6
  69. package/src/flow/expand-single-input-array.js +4 -4
  70. package/src/flow/flow-api-schemas.js +4 -4
  71. package/src/flow/flow.js +62 -41
  72. package/src/flow/for-each.js +12 -11
  73. package/src/flow/get-target-array.js +2 -2
  74. package/src/flow/load-node-module.js +1 -1
  75. package/src/flow/make-node-method.js +4 -4
  76. package/src/flow/resolve-pick-by-field.js +1 -1
  77. package/src/flow/run.js +200 -40
  78. package/src/flow/serialize.js +1 -1
  79. package/src/flow/spec-builder.js +1 -1
  80. package/src/flow/validate.js +1 -1
  81. package/src/nodes/butterworth-filter/init.js +5 -4
  82. package/src/nodes/categorize/update.js +3 -1
  83. package/src/nodes/controller/update.js +2 -1
  84. package/src/nodes/dwell-time-tracker/update.js +2 -1
  85. package/src/nodes/emit-if/helpers.js +57 -11
  86. package/src/nodes/emit-if/init.js +1 -1
  87. package/src/nodes/emit-if/update.js +8 -11
  88. package/src/nodes/page-hinkley/update.js +2 -1
  89. package/src/nodes/pass-if/update.js +3 -1
  90. package/src/nodes/persist-if/helpers.js +60 -4
  91. package/src/nodes/persist-if/init.js +1 -1
  92. package/src/nodes/persist-if/update.js +8 -16
  93. package/src/nodes/persistence-check/update.js +2 -1
  94. package/src/nodes/sanitize/update.js +4 -2
  95. package/src/nodes/swing-watch/update.js +3 -2
  96. package/src/nodes/threshold/update.js +2 -1
  97. package/src/nodes/trend/compute-confidence.js +4 -4
  98. package/src/nodes/trend/update.js +2 -1
  99. package/src/nodes/winnow/update.js +3 -1
  100. package/src/tools/training/features.js +13 -13
  101. package/src/tools/training/logistic.js +1 -1
  102. package/src/tools/training/metrics.js +2 -2
  103. package/src/tools/training/scale.js +2 -2
  104. package/src/tools/training/validate.js +7 -7
  105. package/src/core/utils/console-formatter/index.js +0 -207
  106. package/src/nodes/archive/csv/index.js +0 -6
  107. package/src/nodes/archive/csv/init.js +0 -36
  108. package/src/nodes/archive/csv/load.js +0 -70
  109. package/src/nodes/archive/csv/reset.js +0 -14
  110. package/src/nodes/archive/csv/simple-parser.js +0 -216
  111. package/src/nodes/archive/csv/test.js +0 -173
  112. package/src/nodes/archive/csv/update.js +0 -63
  113. package/src/nodes/sse-emitter/index.js +0 -7
  114. package/src/nodes/sse-emitter/init.js +0 -105
  115. package/src/nodes/sse-emitter/introspect.js +0 -134
  116. package/src/nodes/sse-emitter/publish-to.js +0 -6
  117. package/src/nodes/sse-emitter/recompute.js +0 -16
  118. package/src/nodes/sse-emitter/reset.js +0 -19
  119. package/src/nodes/sse-emitter/update.js +0 -69
  120. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +0 -36
  121. package/src/nodes/sse-emitter/utils/build-message.js +0 -31
  122. package/src/nodes/sse-emitter/utils/connection-manager.js +0 -45
  123. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +0 -20
  124. package/src/nodes/sse-emitter/utils/health-monitor.js +0 -38
  125. package/src/nodes/sse-emitter/utils/qos-handler.js +0 -41
@@ -6,6 +6,8 @@
6
6
  * and the status-signal path below.
7
7
  */
8
8
 
9
+ import { logger } from '../../core/logger/index.js';
10
+
9
11
  /**
10
12
  * Static fallback error the framework substitutes when an adapter breaks
11
13
  * the return contract — no result object at all, or `{ ok: false }` with
@@ -54,10 +56,52 @@ const recordEmissionFailure = function ( state, error ) {
54
56
  state.emitErrorLogged = true;
55
57
  state.firstEmissionError = error.message;
56
58
  state.firstEmissionErrorCode = error.code;
57
- console.error( state.emitErrorLogPrefix + error.code + '): ' + error.message );
59
+ logger.error( state.emitErrorLogPrefix + error.code + '): ' + error.message );
58
60
  }
59
61
  }; // recordEmissionFailure()
60
62
 
63
+ /**
64
+ * Substituted error for an emitter that THREW from publishNow — the
65
+ * other face of a broken return contract (ADR-018: the hot path never
66
+ * throws; a sink answers { ok }). Same code as the malformed-result
67
+ * face: the remediation is identical (fix the non-conforming adapter),
68
+ * and the message names the difference. Allocates on the failure path
69
+ * only.
70
+ *
71
+ * @param {Error} error - The thrown value
72
+ * @returns {Object} `{ code, message }` for the failure episode
73
+ */
74
+ const thrownSinkError = function ( error ) {
75
+ return {
76
+ code: 'MALFORMED_RESULT',
77
+ message: `emitter publishNow threw instead of returning { ok, error? }: ${error.message}`
78
+ };
79
+ }; // thrownSinkError()
80
+
81
+ /**
82
+ * Delivers one payload to the wired emitter with the sink call fully
83
+ * guarded. A conforming emitter never throws; a throwing one is a
84
+ * broken adapter, recorded in the emission episode — never blamed on
85
+ * the user's predicate, and never escaped into the pipeline where it
86
+ * would cost the whole message. Returns true only on `{ ok: true }`.
87
+ *
88
+ * @param {Object} state - Node state containing emitter and topic
89
+ * @param {Object} data - Payload to publish
90
+ * @returns {boolean} True when the emitter accepted the payload
91
+ */
92
+ const deliverToEmitter = function ( state, data ) {
93
+ try {
94
+ const result = state.emitter.publishNow( state.topic, data );
95
+ if ( result && result.ok ) {
96
+ return true;
97
+ }
98
+ recordEmissionFailure( state, ( result && result.error ) || MALFORMED_RESULT_ERROR );
99
+ } catch ( sinkError ) {
100
+ recordEmissionFailure( state, thrownSinkError( sinkError ) );
101
+ }
102
+ return false;
103
+ }; // deliverToEmitter()
104
+
61
105
  /**
62
106
  * Emits disable/enable status signal to the wired emitter. Published
63
107
  * unconditionally (no connectivity pre-check per ADR-018 — during a
@@ -80,15 +124,17 @@ const emitStatusSignal = function ( state, disable, reason ) {
80
124
  $timestamp: Date.now()
81
125
  };
82
126
 
83
- // Guarded read: this path runs inside update()'s catch block, so a
84
- // TypeError here would escape update() and kill the pipeline. A
85
- // malformed result counts as a failure with the static fallback
86
- // error instead.
87
- const result = state.emitter.publishNow( state.topic, signal );
88
- if ( result && result.ok ) {
89
- return;
90
- }
91
- recordEmissionFailure( state, ( result && result.error ) || MALFORMED_RESULT_ERROR );
127
+ // Fully guarded delivery: this path can run inside update()'s
128
+ // catch block, so a throwing emitter here would otherwise escape
129
+ // update() and kill the pipeline. deliverToEmitter contains both
130
+ // faces of a broken adapter — a throw and a malformed result.
131
+ deliverToEmitter( state, signal );
92
132
  }; // emitStatusSignal()
93
133
 
94
- export { assertAnnotateReturn, emitStatusSignal, recordEmissionFailure, MALFORMED_RESULT_ERROR };
134
+ export {
135
+ assertAnnotateReturn,
136
+ emitStatusSignal,
137
+ recordEmissionFailure,
138
+ deliverToEmitter,
139
+ MALFORMED_RESULT_ERROR
140
+ };
@@ -59,7 +59,7 @@ const init = function ( spec ) {
59
59
  state.firstEmissionErrorCode = null;
60
60
  // Static log prefix pre-built here per ADR-004 (no string building in
61
61
  // update beyond the two runtime fields, and only once per episode).
62
- state.emitErrorLogPrefix = `WinkComposer/emitIf: publish failed (node=${state.name}, insightType=${state.insightType}, code=`;
62
+ state.emitErrorLogPrefix = `winkComposer/emitIf: publish failed (node=${state.name}, insightType=${state.insightType}, code=`;
63
63
 
64
64
  return state;
65
65
  }; // init()
@@ -14,7 +14,8 @@
14
14
  * the two output gates share one failure model).
15
15
  */
16
16
 
17
- import { assertAnnotateReturn, emitStatusSignal, recordEmissionFailure, MALFORMED_RESULT_ERROR } from './helpers.js';
17
+ import { assertAnnotateReturn, emitStatusSignal, deliverToEmitter } from './helpers.js';
18
+ import { logger } from '../../core/logger/index.js';
18
19
 
19
20
  const update = function ( state, msg ) {
20
21
  // Always increment pass count for observability
@@ -44,19 +45,15 @@ const update = function ( state, msg ) {
44
45
  // Per the ADR-018 sink return contract, the emitter answers
45
46
  // { ok: true } on success or
46
47
  // { ok: false, error: { code, message } } on failure.
47
- const result = state.emitter.publishNow( state.topic, data );
48
-
49
- if ( result && result.ok ) {
48
+ // deliverToEmitter guards the call itself too: a THROWING
49
+ // emitter is the other face of a broken contract, recorded
50
+ // in the same adapter episode — a non-conforming adapter
51
+ // can neither fail silently nor throw out of the hot path.
52
+ if ( deliverToEmitter( state, data ) ) {
50
53
  state.emissionCount += 1;
51
54
  state.lastEmissionTime = Date.now();
52
55
  // Recovery: a successful publish closes the failure episode.
53
56
  if ( state.emitErrorLogged ) state.emitErrorLogged = false;
54
- } else {
55
- // A malformed result (no object at all, or ok:false with no
56
- // error) counts as a failure with the static fallback error,
57
- // so a non-conforming adapter can neither fail silently
58
- // nor throw out of the hot path.
59
- recordEmissionFailure( state, ( result && result.error ) || MALFORMED_RESULT_ERROR );
60
57
  }
61
58
  }
62
59
 
@@ -76,7 +73,7 @@ const update = function ( state, msg ) {
76
73
  // Log first error per episode; suppress subsequent until recovery
77
74
  if ( !state.predicateErrorLogged ) {
78
75
  state.predicateErrorLogged = true;
79
- console.error( `WinkComposer/emitIf: predicate threw exception: ${error.message}` );
76
+ logger.error( `winkComposer/emitIf: predicate threw exception: ${error.message}` );
80
77
  }
81
78
  state.emissionErrors += 1;
82
79
  state.lastEmissionError = error.message;
@@ -39,6 +39,7 @@
39
39
  */
40
40
 
41
41
  import { executeTriggers } from '../../core/utils/node/index.js';
42
+ import { logger } from '../../core/logger/index.js';
42
43
 
43
44
  const update = function ( state, msg ) {
44
45
  if ( state.disable || state.pause ) return state;
@@ -64,7 +65,7 @@ const update = function ( state, msg ) {
64
65
  } catch ( error ) {
65
66
  if ( !state.tunableErrorLogged ) {
66
67
  state.tunableErrorLogged = true;
67
- console.error( `WinkComposer/${state.nodeType}: tunable threw: ${error.message}` );
68
+ logger.error( `winkComposer/${state.nodeType}: tunable threw: ${error.message}` );
68
69
  }
69
70
  }
70
71
 
@@ -8,6 +8,8 @@
8
8
  * is false. Predicate errors are caught and tracked per-episode.
9
9
  */
10
10
 
11
+ import { logger } from '../../core/logger/index.js';
12
+
11
13
  const update = function ( state, msg ) {
12
14
  if ( state.disable || state.pause ) return state;
13
15
 
@@ -26,7 +28,7 @@ const update = function ( state, msg ) {
26
28
  // Log first error per episode; suppress subsequent until recovery
27
29
  if ( !state.predicateErrorLogged ) {
28
30
  state.predicateErrorLogged = true;
29
- console.error( `WinkComposer/passIf: predicate threw exception: ${error.message}` );
31
+ logger.error( `winkComposer/passIf: predicate threw exception: ${error.message}` );
30
32
  }
31
33
  }
32
34
 
@@ -8,6 +8,8 @@
8
8
  * emit-if/helpers.js — the two output gates share one failure model.
9
9
  */
10
10
 
11
+ import { logger } from '../../core/logger/index.js';
12
+
11
13
  /**
12
14
  * Static fallback error the framework substitutes when an adapter breaks
13
15
  * the return contract — no result object at all, or `{ ok: false }` with
@@ -74,8 +76,8 @@ const sweepAnnotateKeys = function ( state, record, msg ) {
74
76
  }
75
77
 
76
78
  if ( invented.length > 0 ) {
77
- console.warn(
78
- `WinkComposer/persistIf: annotate for node '${state.name}' ` +
79
+ logger.warn(
80
+ `winkComposer/persistIf: annotate for node '${state.name}' ` +
79
81
  `(insightType '${state.insightType}') returned keys that are not declared columns: ` +
80
82
  `${invented.join( ', ' )}. These values are never stored, because the persist plan ` +
81
83
  'writes only declared columns. Check the key names against the asset class. ' +
@@ -104,8 +106,62 @@ const recordPersistFailure = function ( state, error ) {
104
106
  state.writeErrorLogged = true;
105
107
  state.firstPersistError = error.message;
106
108
  state.firstPersistErrorCode = error.code;
107
- console.error( state.writeErrorLogPrefix + error.code + '): ' + error.message );
109
+ logger.error( state.writeErrorLogPrefix + error.code + '): ' + error.message );
108
110
  }
109
111
  }; // recordPersistFailure()
110
112
 
111
- export { assertAnnotateReturn, sweepAnnotateKeys, recordPersistFailure, MALFORMED_RESULT_ERROR };
113
+ /**
114
+ * Substituted error for a storage adapter that THREW from write() —
115
+ * the other face of a broken return contract (ADR-018: the hot path
116
+ * never throws; a sink answers { ok }). Same code as the
117
+ * malformed-result face: the remediation is identical (fix the
118
+ * non-conforming adapter), and the message names the difference.
119
+ * Allocates on the failure path only. Mirrors emit-if.
120
+ *
121
+ * @param {Error} error - The thrown value
122
+ * @returns {Object} `{ code, message }` for the failure episode
123
+ */
124
+ const thrownSinkError = function ( error ) {
125
+ return {
126
+ code: 'MALFORMED_RESULT',
127
+ message: `storage write threw instead of returning { ok, error? }: ${error.message}`
128
+ };
129
+ }; // thrownSinkError()
130
+
131
+ /**
132
+ * Writes one record to the wired storage with the sink call fully
133
+ * guarded. A conforming adapter never throws; a throwing one is a
134
+ * broken adapter, recorded in the persist episode — never escaped
135
+ * into the pipeline where it would also cost the message's other
136
+ * outputs. Returns true only on `{ ok: true }`. Mirrors emit-if's
137
+ * deliverToEmitter.
138
+ *
139
+ * @param {Object} state - Node state containing storage, insightType,
140
+ * and partitionId
141
+ * @param {Object} record - Record to persist
142
+ * @returns {boolean} True when the storage accepted the record
143
+ */
144
+ const writeToStorage = function ( state, record ) {
145
+ try {
146
+ const result = state.storage.write(
147
+ state.insightType,
148
+ record,
149
+ state.partitionId
150
+ );
151
+ if ( result && result.ok ) {
152
+ return true;
153
+ }
154
+ recordPersistFailure( state, ( result && result.error ) || MALFORMED_RESULT_ERROR );
155
+ } catch ( sinkError ) {
156
+ recordPersistFailure( state, thrownSinkError( sinkError ) );
157
+ }
158
+ return false;
159
+ }; // writeToStorage()
160
+
161
+ export {
162
+ assertAnnotateReturn,
163
+ sweepAnnotateKeys,
164
+ recordPersistFailure,
165
+ writeToStorage,
166
+ MALFORMED_RESULT_ERROR
167
+ };
@@ -90,7 +90,7 @@ const init = function ( spec ) {
90
90
  state.firstPersistErrorCode = null;
91
91
  // Static log prefix pre-built here per ADR-004 (no string building in
92
92
  // update beyond the two runtime fields, and only once per episode).
93
- state.writeErrorLogPrefix = `WinkComposer/persistIf: storage write failed (node=${state.name}, insightType=${state.insightType}, code=`;
93
+ state.writeErrorLogPrefix = `winkComposer/persistIf: storage write failed (node=${state.name}, insightType=${state.insightType}, code=`;
94
94
 
95
95
  return state;
96
96
  }; // init()
@@ -10,7 +10,8 @@
10
10
  * it to the storage buffer if the predicate passed.
11
11
  */
12
12
 
13
- import { assertAnnotateReturn, sweepAnnotateKeys, recordPersistFailure, MALFORMED_RESULT_ERROR } from './helpers.js';
13
+ import { assertAnnotateReturn, sweepAnnotateKeys, writeToStorage } from './helpers.js';
14
+ import { logger } from '../../core/logger/index.js';
14
15
 
15
16
  /**
16
17
  * Process incoming message.
@@ -64,7 +65,7 @@ const update = function ( state, msg ) {
64
65
  // Log first error per episode; suppress subsequent until recovery
65
66
  if ( !state.predicateErrorLogged ) {
66
67
  state.predicateErrorLogged = true;
67
- console.error( `WinkComposer/persistIf: predicate threw exception: ${error.message}` );
68
+ logger.error( `winkComposer/persistIf: predicate threw exception: ${error.message}` );
68
69
  }
69
70
  state.persistErrors += 1;
70
71
  state.lastPersistError = error.message;
@@ -78,31 +79,22 @@ const update = function ( state, msg ) {
78
79
 
79
80
  // Write to storage if predicate returned true
80
81
  if ( shouldPersist ) {
81
- // Build record for storage (zero-alloc: storage.write handles it).
82
82
  // storage.write() is synchronous per ADR-013; the actual I/O happens
83
83
  // asynchronously via the storage adapter's background flush.
84
84
  // Per the ADR-018 sink return contract, the storage answers
85
85
  // { ok: true } on success or
86
86
  // { ok: false, error: { code, message } } on failure.
87
- const result = state.storage.write(
88
- state.insightType,
89
- record,
90
- state.partitionId
91
- );
92
-
93
- if ( result && result.ok ) {
87
+ // writeToStorage guards the call itself too: a THROWING adapter
88
+ // is the other face of a broken contract, recorded in the same
89
+ // adapter episode — a non-conforming adapter can neither fail
90
+ // silently nor throw out of the hot path.
91
+ if ( writeToStorage( state, record ) ) {
94
92
  state.persistCount += 1;
95
93
  // Wall-clock marker of the last successful write (post-mortem
96
94
  // reads only, like the first* error fields).
97
95
  state.lastPersistTime = Date.now();
98
96
  // Recovery: a successful write closes the failure episode.
99
97
  if ( state.writeErrorLogged ) state.writeErrorLogged = false;
100
- } else {
101
- // A malformed result (no object at all, or ok:false with no
102
- // error) counts as a failure with the static fallback error,
103
- // so a non-conforming adapter can neither fail silently
104
- // nor throw out of the hot path.
105
- recordPersistFailure( state, ( result && result.error ) || MALFORMED_RESULT_ERROR );
106
98
  }
107
99
  }
108
100
 
@@ -1,6 +1,7 @@
1
1
  // nodes/persistence-check/update.js
2
2
 
3
3
  import { executeTriggers } from '../../core/utils/node/index.js';
4
+ import { logger } from '../../core/logger/index.js';
4
5
 
5
6
  const update = function ( state, msg ) {
6
7
  if ( state.disable || state.pause ) return state;
@@ -16,7 +17,7 @@ const update = function ( state, msg ) {
16
17
  // Log first error per episode; suppress subsequent until recovery
17
18
  if ( !state.predicateErrorLogged ) {
18
19
  state.predicateErrorLogged = true;
19
- console.error( `WinkComposer/persistenceCheck: predicate threw exception: ${error.message}` );
20
+ logger.error( `winkComposer/persistenceCheck: predicate threw exception: ${error.message}` );
20
21
  }
21
22
  }
22
23
 
@@ -1,3 +1,5 @@
1
+ import { logger } from '../../core/logger/index.js';
2
+
1
3
  // Helper: Check value against range
2
4
  const checkRange = function ( state, value, rangeSpec ) {
3
5
  if ( !state.hasRange || !rangeSpec ) {
@@ -34,7 +36,7 @@ const checkPredicate = function ( state, value, msg ) {
34
36
  // Log first error per episode; suppress subsequent until recovery
35
37
  if ( !state.predicateErrorLogged ) {
36
38
  state.predicateErrorLogged = true;
37
- console.error( `WinkComposer/sanitize: predicate threw exception: ${error.message}` );
39
+ logger.error( `winkComposer/sanitize: predicate threw exception: ${error.message}` );
38
40
  }
39
41
  return false;
40
42
  }
@@ -64,7 +66,7 @@ const update = function ( state, msg ) {
64
66
  } catch ( error ) {
65
67
  if ( !state.tunableErrorLogged ) {
66
68
  state.tunableErrorLogged = true;
67
- console.error( `WinkComposer/${state.nodeType}: tunable threw: ${error.message}` );
69
+ logger.error( `winkComposer/${state.nodeType}: tunable threw: ${error.message}` );
68
70
  }
69
71
  }
70
72
  }
@@ -66,6 +66,7 @@
66
66
  // https://github.com/weinkauf/Persistence1D
67
67
 
68
68
  import { push, isNotFull } from '../../windowing/count-sliding/index.js';
69
+ import { logger } from '../../core/logger/index.js';
69
70
 
70
71
 
71
72
  // ═══════════════════════════════════════════════════════════════════════════
@@ -624,8 +625,8 @@ const resolveThreshold = function ( state, msg ) {
624
625
  } catch ( error ) {
625
626
  if ( !state.tunableErrorLogged ) {
626
627
  state.tunableErrorLogged = true;
627
- const errMsg = 'WinkComposer/' + state.nodeType + ': tunable threw: ' + error.message;
628
- console.error( errMsg );
628
+ const errMsg = 'winkComposer/' + state.nodeType + ': tunable threw: ' + error.message;
629
+ logger.error( errMsg );
629
630
  }
630
631
  }
631
632
  const epsBase = state.currentThreshold;
@@ -1,6 +1,7 @@
1
1
  // nodes/threshold/update.js
2
2
 
3
3
  import { executeTriggers } from '../../core/utils/node/index.js';
4
+ import { logger } from '../../core/logger/index.js';
4
5
 
5
6
  const update = function ( state, msg ) {
6
7
  if ( state.disable || state.pause ) return state;
@@ -27,7 +28,7 @@ const update = function ( state, msg ) {
27
28
  // Log first error per episode; suppress subsequent until recovery
28
29
  if ( !state.tunableErrorLogged ) {
29
30
  state.tunableErrorLogged = true;
30
- console.error( `WinkComposer/${state.nodeType}: tunable threw: ${error.message}` );
31
+ logger.error( `winkComposer/${state.nodeType}: tunable threw: ${error.message}` );
31
32
  }
32
33
  }
33
34
 
@@ -5,7 +5,7 @@ const computeConfidence = function ( state ) {
5
5
  // Provides gradual trust building as statistics stabilize
6
6
  if ( state.samples < state.warmupSamples ) {
7
7
  const ramp = state.samples / Math.max( state.warmupSamples, 1 );
8
- return ( ramp < 0 ) ? 0 : ( ramp > 1 ? 1 : ramp );
8
+ return Math.min( 1, Math.max( 0, ramp ) );
9
9
  }
10
10
 
11
11
  const epsilon = state.epsilon ?? 1e-12;
@@ -45,7 +45,7 @@ const computeConfidence = function ( state ) {
45
45
 
46
46
  // Combine factors: far from edge AND low noise AND persistent = high confidence
47
47
  const confStable = margin * quietness * persistenceFactor;
48
- return ( confStable < 0 ) ? 0 : ( confStable > 1 ? 1 : confStable );
48
+ return Math.min( 1, Math.max( 0, confStable ) );
49
49
  }
50
50
 
51
51
  // TRENDING CONFIDENCE: "How confident are we in this trend direction?"
@@ -105,8 +105,8 @@ const computeConfidence = function ( state ) {
105
105
  // Therefore, we omit the quietness factor for rising/falling states.
106
106
  const confTrend = signalClarity * boundaryClarity * persistenceFactor;
107
107
 
108
- // Ensure output is strictly in [0,1] despite numerical errors
109
- return ( confTrend < 0 ) ? 0 : ( confTrend > 1 ? 1 : confTrend );
108
+ // Clamp to [0,1] — the published confidence contract
109
+ return Math.min( 1, Math.max( 0, confTrend ) );
110
110
  }; // computeConfidence()
111
111
 
112
112
  export { computeConfidence };
@@ -1,6 +1,7 @@
1
1
  // nodes/trend/update.js
2
2
 
3
3
  import { computeConfidence } from './compute-confidence.js';
4
+ import { logger } from '../../core/logger/index.js';
4
5
 
5
6
  const update = function ( state, msg ) {
6
7
  if ( state.disable || state.pause ) return state;
@@ -47,7 +48,7 @@ const update = function ( state, msg ) {
47
48
  } catch ( error ) {
48
49
  if ( !state.tunableErrorLogged ) {
49
50
  state.tunableErrorLogged = true;
50
- console.error( `WinkComposer/${state.nodeType}: tunable threw: ${error.message}` );
51
+ logger.error( `winkComposer/${state.nodeType}: tunable threw: ${error.message}` );
51
52
  }
52
53
  }
53
54
 
@@ -22,6 +22,8 @@
22
22
  // Extracted helper: set anchor and mark significant.
23
23
  // Called from each of the five checks. Zero allocation — scalar
24
24
  // assignments only. Reduces update() cyclomatic complexity.
25
+ import { logger } from '../../core/logger/index.js';
26
+
25
27
  const setAnchor = function ( state, xVal, slope ) {
26
28
  state.anchor = xVal;
27
29
  state.anchorSlope = Number.isFinite( slope ) ? slope : 0;
@@ -80,7 +82,7 @@ const update = function ( state, msg ) {
80
82
  } catch ( error ) {
81
83
  if ( !state.tunableErrorLogged ) {
82
84
  state.tunableErrorLogged = true;
83
- console.error( `WinkComposer/${state.nodeType}: tunable threw: ${error.message}` );
85
+ logger.error( `winkComposer/${state.nodeType}: tunable threw: ${error.message}` );
84
86
  }
85
87
  }
86
88
 
@@ -38,13 +38,13 @@ const AVAILABLE_STATS = [ 'max', 'sum', 'countAbove' ];
38
38
  */
39
39
  const group = function ( name, ids, accessor, options ) {
40
40
  if ( typeof name !== 'string' || name.length === 0 ) {
41
- throw new Error( 'features.group: name must be a non-empty string' );
41
+ throw new Error( 'winkComposer/features.group: name must be a non-empty string' );
42
42
  }
43
43
  if ( !Array.isArray( ids ) || ids.length === 0 ) {
44
- throw new Error( 'features.group: ids must be a non-empty array' );
44
+ throw new Error( 'winkComposer/features.group: ids must be a non-empty array' );
45
45
  }
46
46
  if ( typeof accessor !== 'function' ) {
47
- throw new Error( 'features.group: accessor must be a function' );
47
+ throw new Error( 'winkComposer/features.group: accessor must be a function' );
48
48
  }
49
49
 
50
50
  const stats = ( options && Array.isArray( options.stats ) ) ?
@@ -54,7 +54,7 @@ const group = function ( name, ids, accessor, options ) {
54
54
  for ( let i = 0; i < stats.length; i += 1 ) {
55
55
  if ( AVAILABLE_STATS.indexOf( stats[ i ] ) === -1 ) {
56
56
  throw new Error(
57
- 'features.group: unknown stat "' + stats[ i ] +
57
+ 'winkComposer/features.group: unknown stat "' + stats[ i ] +
58
58
  '". Available: ' + AVAILABLE_STATS.join( ', ' )
59
59
  );
60
60
  }
@@ -65,7 +65,7 @@ const group = function ( name, ids, accessor, options ) {
65
65
  1.5;
66
66
 
67
67
  if ( typeof countAboveThreshold !== 'number' ) {
68
- throw new Error( 'features.group: countAboveThreshold must be a number' );
68
+ throw new Error( 'winkComposer/features.group: countAboveThreshold must be a number' );
69
69
  }
70
70
 
71
71
  return {
@@ -90,10 +90,10 @@ const group = function ( name, ids, accessor, options ) {
90
90
  */
91
91
  const scalar = function ( name, accessor ) {
92
92
  if ( typeof name !== 'string' || name.length === 0 ) {
93
- throw new Error( 'features.scalar: name must be a non-empty string' );
93
+ throw new Error( 'winkComposer/features.scalar: name must be a non-empty string' );
94
94
  }
95
95
  if ( typeof accessor !== 'function' ) {
96
- throw new Error( 'features.scalar: accessor must be a function' );
96
+ throw new Error( 'winkComposer/features.scalar: accessor must be a function' );
97
97
  }
98
98
  return { type: SCALAR, name: name, accessor: accessor };
99
99
  };
@@ -112,13 +112,13 @@ const scalar = function ( name, accessor ) {
112
112
  */
113
113
  const counter = function ( name, sourceAccessor, predicate ) {
114
114
  if ( typeof name !== 'string' || name.length === 0 ) {
115
- throw new Error( 'features.counter: name must be a non-empty string' );
115
+ throw new Error( 'winkComposer/features.counter: name must be a non-empty string' );
116
116
  }
117
117
  if ( typeof sourceAccessor !== 'function' ) {
118
- throw new Error( 'features.counter: sourceAccessor must be a function' );
118
+ throw new Error( 'winkComposer/features.counter: sourceAccessor must be a function' );
119
119
  }
120
120
  if ( typeof predicate !== 'function' ) {
121
- throw new Error( 'features.counter: predicate must be a function' );
121
+ throw new Error( 'winkComposer/features.counter: predicate must be a function' );
122
122
  }
123
123
  return { type: COUNTER, name: name, sourceAccessor: sourceAccessor, predicate: predicate };
124
124
  };
@@ -134,7 +134,7 @@ const counter = function ( name, sourceAccessor, predicate ) {
134
134
  */
135
135
  const buildExtractor = function ( schema ) {
136
136
  if ( !Array.isArray( schema ) || schema.length === 0 ) {
137
- throw new Error( 'features.buildExtractor: schema must be a non-empty array' );
137
+ throw new Error( 'winkComposer/features.buildExtractor: schema must be a non-empty array' );
138
138
  }
139
139
 
140
140
  // Build feature names from schema
@@ -149,7 +149,7 @@ const buildExtractor = function ( schema ) {
149
149
  names.push( col.name );
150
150
  } else {
151
151
  throw new Error(
152
- 'features.buildExtractor: unknown column type "' +
152
+ 'winkComposer/features.buildExtractor: unknown column type "' +
153
153
  String( col.type ) + '" at index ' + i
154
154
  );
155
155
  }
@@ -160,7 +160,7 @@ const buildExtractor = function ( schema ) {
160
160
  for ( let i = 0; i < names.length; i += 1 ) {
161
161
  if ( seen[ names[ i ] ] ) {
162
162
  throw new Error(
163
- 'features.buildExtractor: duplicate feature name "' + names[ i ] + '"'
163
+ 'winkComposer/features.buildExtractor: duplicate feature name "' + names[ i ] + '"'
164
164
  );
165
165
  }
166
166
  seen[ names[ i ] ] = true;
@@ -93,7 +93,7 @@ const train = function ( X, y, options ) {
93
93
  for ( let i = 0; i < n; i += 1 ) n1 += y[ i ];
94
94
  const n0 = n - n1;
95
95
  if ( n0 === 0 || n1 === 0 ) {
96
- throw new Error( 'train: y must contain both classes (0 and 1).' );
96
+ throw new Error( 'winkComposer/train: y must contain both classes (0 and 1).' );
97
97
  }
98
98
  w[ 0 ] = n / ( 2 * n0 );
99
99
  w[ 1 ] = n / ( 2 * n1 );
@@ -24,7 +24,7 @@ import { validateBinaryArray } from './validate.js';
24
24
  */
25
25
  const confusionMatrix = function ( predictions, labels ) {
26
26
  if ( predictions.length !== labels.length ) {
27
- throw new Error( 'confusionMatrix: predictions and labels must have the same length.' );
27
+ throw new Error( 'winkComposer/confusionMatrix: predictions and labels must have the same length.' );
28
28
  }
29
29
 
30
30
  validateBinaryArray( predictions, 'predictions', 'confusionMatrix' );
@@ -85,7 +85,7 @@ const classificationMetrics = function ( cm ) {
85
85
  */
86
86
  const sweepThresholds = function ( probabilities, labels, thresholds ) {
87
87
  if ( probabilities.length !== labels.length ) {
88
- throw new Error( 'sweepThresholds: probabilities and labels must have the same length.' );
88
+ throw new Error( 'winkComposer/sweepThresholds: probabilities and labels must have the same length.' );
89
89
  }
90
90
 
91
91
  validateBinaryArray( labels, 'labels', 'sweepThresholds' );
@@ -177,7 +177,7 @@ const computeRobustParams = function ( X ) {
177
177
  */
178
178
  const scale = function ( X, params ) {
179
179
  if ( !Array.isArray( X ) || X.length === 0 ) {
180
- throw new Error( 'scale: X must be a non-empty array of rows.' );
180
+ throw new Error( 'winkComposer/scale: X must be a non-empty array of rows.' );
181
181
  }
182
182
 
183
183
  const n = X.length;
@@ -221,7 +221,7 @@ const scale = function ( X, params ) {
221
221
  result[ i ] = out;
222
222
  }
223
223
  } else {
224
- throw new Error( 'scale: unknown method "' + method + '".' );
224
+ throw new Error( 'winkComposer/scale: unknown method "' + method + '".' );
225
225
  }
226
226
 
227
227
  return result;