@winkjs/composer 0.5.1 → 0.6.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 (102) hide show
  1. package/package.json +2 -2
  2. package/src/core/batch/aoo-encoder.js +4 -4
  3. package/src/core/batch/sliding-window.js +2 -2
  4. package/src/core/codec/index.js +4 -4
  5. package/src/core/emitter-manager/mqtt/emitter.js +44 -14
  6. package/src/core/emitter-manager/terminal/assert-columns.js +1 -1
  7. package/src/core/emitter-manager/terminal/index.js +4 -4
  8. package/src/core/env-vars.js +44 -3
  9. package/src/core/logger/index.js +174 -0
  10. package/src/core/mqtt-protocol.js +2 -2
  11. package/src/core/partition-manager/get-stats.js +31 -0
  12. package/src/core/partition-manager/index.js +1 -0
  13. package/src/core/partition-manager/init.js +15 -0
  14. package/src/core/partition-manager/update.js +143 -87
  15. package/src/core/partition-manager/validate-flow.js +1 -1
  16. package/src/core/semantics/loader.js +1 -1
  17. package/src/core/semantics/warnings.js +5 -3
  18. package/src/core/shutdown-manager/index.js +71 -21
  19. package/src/core/source-manager/csv/start.js +109 -35
  20. package/src/core/source-manager/mqtt/client.js +95 -36
  21. package/src/core/source-manager/mqtt/dedup.js +3 -3
  22. package/src/core/source-manager/mqtt/index.js +1 -1
  23. package/src/core/source-manager/mqtt/status.js +67 -14
  24. package/src/core/source-manager/record-shape.js +55 -0
  25. package/src/core/source-manager/test-harness/field-generator.js +1 -1
  26. package/src/core/source-manager/test-harness/start.js +52 -10
  27. package/src/core/source-manager/test-harness/validate.js +1 -1
  28. package/src/core/storage-manager/questdb/assert-columns.js +2 -2
  29. package/src/core/storage-manager/questdb/ensure-tables.js +1 -1
  30. package/src/core/storage-manager/questdb/index.js +55 -17
  31. package/src/core/storage-manager/questdb/persist-plan.js +32 -8
  32. package/src/core/tunable/helpers.js +1 -1
  33. package/src/core/utils/callback-guard/index.js +155 -0
  34. package/src/core/utils/half-life/index.js +4 -4
  35. package/src/core/utils/validate/composer-validators.js +2 -2
  36. package/src/core/utils/validate/index.js +1 -1
  37. package/src/core/utils/validate/validate.js +1 -1
  38. package/src/core/wiring/assert-handle.js +5 -5
  39. package/src/core/wiring/assert-module.js +2 -2
  40. package/src/core/wiring/wire-emitters.js +4 -3
  41. package/src/core/wiring/wire-linear-graph.js +1 -1
  42. package/src/core/wiring/wire-node.js +6 -6
  43. package/src/core/wiring/wire-semantics.js +1 -1
  44. package/src/core/wiring/wire-storages.js +4 -3
  45. package/src/flow/driver.js +31 -6
  46. package/src/flow/expand-single-input-array.js +4 -4
  47. package/src/flow/flow-api-schemas.js +4 -4
  48. package/src/flow/flow.js +41 -41
  49. package/src/flow/for-each.js +12 -11
  50. package/src/flow/get-target-array.js +2 -2
  51. package/src/flow/load-node-module.js +1 -1
  52. package/src/flow/make-node-method.js +4 -4
  53. package/src/flow/resolve-pick-by-field.js +1 -1
  54. package/src/flow/run.js +176 -38
  55. package/src/flow/serialize.js +1 -1
  56. package/src/flow/spec-builder.js +1 -1
  57. package/src/flow/validate.js +1 -1
  58. package/src/nodes/butterworth-filter/init.js +5 -4
  59. package/src/nodes/categorize/update.js +3 -1
  60. package/src/nodes/controller/update.js +2 -1
  61. package/src/nodes/dwell-time-tracker/update.js +2 -1
  62. package/src/nodes/emit-if/helpers.js +57 -11
  63. package/src/nodes/emit-if/init.js +1 -1
  64. package/src/nodes/emit-if/update.js +8 -11
  65. package/src/nodes/page-hinkley/update.js +2 -1
  66. package/src/nodes/pass-if/update.js +3 -1
  67. package/src/nodes/persist-if/helpers.js +60 -4
  68. package/src/nodes/persist-if/init.js +1 -1
  69. package/src/nodes/persist-if/update.js +8 -16
  70. package/src/nodes/persistence-check/update.js +2 -1
  71. package/src/nodes/sanitize/update.js +4 -2
  72. package/src/nodes/swing-watch/update.js +3 -2
  73. package/src/nodes/threshold/update.js +2 -1
  74. package/src/nodes/trend/compute-confidence.js +4 -4
  75. package/src/nodes/trend/update.js +2 -1
  76. package/src/nodes/winnow/update.js +3 -1
  77. package/src/tools/training/features.js +13 -13
  78. package/src/tools/training/logistic.js +1 -1
  79. package/src/tools/training/metrics.js +2 -2
  80. package/src/tools/training/scale.js +2 -2
  81. package/src/tools/training/validate.js +7 -7
  82. package/src/core/utils/console-formatter/index.js +0 -207
  83. package/src/nodes/archive/csv/index.js +0 -6
  84. package/src/nodes/archive/csv/init.js +0 -36
  85. package/src/nodes/archive/csv/load.js +0 -70
  86. package/src/nodes/archive/csv/reset.js +0 -14
  87. package/src/nodes/archive/csv/simple-parser.js +0 -216
  88. package/src/nodes/archive/csv/test.js +0 -173
  89. package/src/nodes/archive/csv/update.js +0 -63
  90. package/src/nodes/sse-emitter/index.js +0 -7
  91. package/src/nodes/sse-emitter/init.js +0 -105
  92. package/src/nodes/sse-emitter/introspect.js +0 -134
  93. package/src/nodes/sse-emitter/publish-to.js +0 -6
  94. package/src/nodes/sse-emitter/recompute.js +0 -16
  95. package/src/nodes/sse-emitter/reset.js +0 -19
  96. package/src/nodes/sse-emitter/update.js +0 -69
  97. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +0 -36
  98. package/src/nodes/sse-emitter/utils/build-message.js +0 -31
  99. package/src/nodes/sse-emitter/utils/connection-manager.js +0 -45
  100. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +0 -20
  101. package/src/nodes/sse-emitter/utils/health-monitor.js +0 -38
  102. package/src/nodes/sse-emitter/utils/qos-handler.js +0 -41
@@ -0,0 +1,155 @@
1
+ // core/utils/callback-guard/index.js
2
+
3
+ /**
4
+ * @fileoverview The shared guard for user-supplied callbacks.
5
+ *
6
+ * Adapters and the flow runtime hand the user's functions to this
7
+ * module once, at setup. The armed version they get back contains
8
+ * every fault the user's code can produce. That covers a plain
9
+ * throw, a rejected promise, and even a `throw null` with no
10
+ * message to read. Each fault becomes one classified
11
+ * CALLBACK_FAILED report. The operation that invoked the callback
12
+ * continues unharmed. This is ADR-018's callback-isolation rule:
13
+ * a misbehaving user callback never reaches transport code and
14
+ * never fails silently. ADR-027 scopes which callbacks are
15
+ * wrapped: notification callbacks only. A callback whose throw
16
+ * carries meaning (QuestDB's strict-mode onWarning) and the
17
+ * flow-guarded onMessage stay unwrapped.
18
+ *
19
+ * Design constraints, in force at every call site:
20
+ * - Wrap once at setup; never per message. The armed closure's
21
+ * success-path cost is one try frame and one typeof check. An
22
+ * async callback additionally costs one derived promise per
23
+ * call — that cost follows the user's own choice of an async
24
+ * handler.
25
+ * - Fixed two-argument signature: every wrapped callback takes at
26
+ * most two arguments, so no rest-array is allocated per call.
27
+ * A one-argument callback observes a trailing undefined; that is
28
+ * deliberate and documented here.
29
+ * - The fault reporter is throw-proof. Every containment guarantee
30
+ * reduces to "reporting a fault cannot itself fault". The report
31
+ * closure receives an already-safe detail string, never the raw
32
+ * thrown value. When the report closure itself fails, the guard
33
+ * falls back to one bare log line with no user-value
34
+ * interpolation.
35
+ */
36
+
37
+ import { logger } from '../../logger/index.js';
38
+
39
+ /**
40
+ * Frozen sentinel `wrapTransform` returns when the transform threw.
41
+ * Distinguishes "threw" from the documented null/undefined drop.
42
+ * Never exported from composer's public surface, so no user value
43
+ * can ever be identical to it.
44
+ */
45
+ const TRANSFORM_THREW = Object.freeze( {} );
46
+
47
+ /**
48
+ * Renders a thrown value as a safe string. `throw null`, a throwing
49
+ * `message` getter, and an object with no string form all yield a
50
+ * printable result instead of a second throw.
51
+ *
52
+ * @param {*} err - Whatever the user's code threw or rejected with
53
+ * @returns {string} A printable detail, never a throw
54
+ */
55
+ const describeFault = function ( err ) {
56
+ try {
57
+ if ( err && err.message ) {
58
+ return String( err.message );
59
+ }
60
+ return String( err );
61
+ } catch {
62
+ return 'unprintable error';
63
+ }
64
+ }; // describeFault()
65
+
66
+ /**
67
+ * Last-resort line when the site's own report closure fails. Bare on
68
+ * purpose: interpolating anything user-controlled here could throw
69
+ * again, and this is the floor the whole guard stands on.
70
+ */
71
+ const reportFallback = function () {
72
+ logger.error( 'winkComposer/callbackGuard: a callback fault report failed [CALLBACK_FAILED]' );
73
+ }; // reportFallback()
74
+
75
+ /**
76
+ * Arms one user callback. Returns null when no function was given,
77
+ * so `if ( onX )` presence checks at the sites keep their meaning
78
+ * (absent stays absent — no-handler fallbacks are untouched).
79
+ *
80
+ * @param {Function} fn - The user's callback, already validated by
81
+ * the site's own config validation (validate raw, then wrap)
82
+ * @param {Object} spec - `{ name, severity, report }`
83
+ * @param {string} spec.name - Callback name for the report
84
+ * @param {string} spec.severity - 'red' or 'yellow' per ADR-018:
85
+ * red when the callback signals something happening now, yellow
86
+ * for post-event reporting
87
+ * @param {Function} spec.report - Site channel: called as
88
+ * `report( severity, name, detail )` with detail already safe
89
+ * @returns {Function|null} The armed callback, or null
90
+ */
91
+ const wrapCallback = function ( fn, { name, severity, report } ) {
92
+ if ( typeof fn !== 'function' ) {
93
+ return null;
94
+ }
95
+ const faultFn = function ( err ) {
96
+ try {
97
+ report( severity, name, describeFault( err ) );
98
+ } catch {
99
+ reportFallback();
100
+ }
101
+ };
102
+ return function ( a, b ) {
103
+ try {
104
+ const result = fn( a, b );
105
+ if ( result && typeof result.then === 'function' ) {
106
+ // An async callback costs one derived promise per call.
107
+ // That allocation is proportional to the user's own
108
+ // choice of an async handler; a sync callback allocates
109
+ // nothing here.
110
+ const settled = result.then( undefined, faultFn );
111
+ // A native promise cannot reject past this point, because
112
+ // faultFn cannot throw. A non-conforming thenable can
113
+ // ignore its handlers and RETURN a rejected promise
114
+ // instead; sink that one level so it cannot leak as an
115
+ // unhandled rejection. The guard contains bugs, not
116
+ // in-process adversaries — a deeper chain of hostile
117
+ // thenables stays the caller's own fault.
118
+ if ( !( result instanceof Promise ) && settled && typeof settled.then === 'function' ) {
119
+ settled.then( undefined, faultFn );
120
+ }
121
+ }
122
+ } catch ( err ) {
123
+ faultFn( err );
124
+ }
125
+ };
126
+ }; // wrapCallback()
127
+
128
+ /**
129
+ * Arms a value-returning callback (the sources' `transform`). The
130
+ * caller keeps its control flow — a transform's return decides the
131
+ * row's fate — so this face only converts a throw into the sentinel
132
+ * and a safe fault report. Return-shape checks and the null drop
133
+ * stay at the site.
134
+ *
135
+ * @param {Function} fn - The transform, called as `fn( value )`
136
+ * @param {Function} onFault - Hoisted site closure, called as
137
+ * `onFault( detail, context )` on the failure path only
138
+ * @returns {Function} `( value, context ) => result | TRANSFORM_THREW`
139
+ */
140
+ const wrapTransform = function ( fn, onFault ) {
141
+ return function ( value, context ) {
142
+ try {
143
+ return fn( value );
144
+ } catch ( err ) {
145
+ try {
146
+ onFault( describeFault( err ), context );
147
+ } catch {
148
+ reportFallback();
149
+ }
150
+ return TRANSFORM_THREW;
151
+ }
152
+ };
153
+ }; // wrapTransform()
154
+
155
+ export { wrapCallback, wrapTransform, TRANSFORM_THREW };
@@ -14,7 +14,7 @@
14
14
  */
15
15
  export const halfLifeToAlpha = function ( halfLife ) {
16
16
  if ( typeof halfLife !== 'number' || !Number.isFinite( halfLife ) || ( halfLife <= 0 ) ) {
17
- throw new Error( 'Half-life must be a finite number > 0' );
17
+ throw new Error( 'winkComposer/halfLife: Half-life must be a finite number > 0' );
18
18
  }
19
19
  // alpha = 1 - exp( -ln(2) / halfLife ); use expm1 to avoid cancellation
20
20
  let alpha = -Math.expm1( -( Math.LN2 / halfLife ) );
@@ -37,7 +37,7 @@ export const halfLifeToAlpha = function ( halfLife ) {
37
37
  */
38
38
  export const alphaToHalfLife = function ( alpha ) {
39
39
  if ( typeof alpha !== 'number' || !Number.isFinite( alpha ) || ( alpha <= 0 ) || ( alpha >= 1 ) ) {
40
- throw new Error( 'Alpha must be a finite number in (0,1)' );
40
+ throw new Error( 'winkComposer/halfLife: Alpha must be a finite number in (0,1)' );
41
41
  }
42
42
  // halfLife = ln(2) / -ln(1 - alpha) ; use log1p(-alpha) for stability
43
43
  return Math.LN2 / ( -Math.log1p( -alpha ) );
@@ -53,10 +53,10 @@ export const alphaToHalfLife = function ( alpha ) {
53
53
  */
54
54
  export const halfLifeToWarmupSamples = function ( halfLifeSamples, settledFraction = 0.95 ) {
55
55
  if ( typeof halfLifeSamples !== 'number' || !Number.isFinite( halfLifeSamples ) || ( halfLifeSamples <= 0 ) ) {
56
- throw new Error( 'halfLifeSamples must be a finite number > 0' );
56
+ throw new Error( 'winkComposer/halfLife: halfLifeSamples must be a finite number > 0' );
57
57
  }
58
58
  if ( typeof settledFraction !== 'number' || !Number.isFinite( settledFraction ) || ( settledFraction <= 0 ) || ( settledFraction >= 1 ) ) {
59
- throw new Error( 'settledFraction must be a finite number in (0,1)' );
59
+ throw new Error( 'winkComposer/halfLife: settledFraction must be a finite number in (0,1)' );
60
60
  }
61
61
  // nHL = -log2(1 - settledFraction) = -ln(1 - s)/ln2
62
62
  const nHalfLives = ( -Math.log1p( -settledFraction ) ) / Math.LN2;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * WinkComposer-specific validators
2
+ * winkComposer-specific validators
3
3
  * Domain-specific validation functions for streaming analytics nodes
4
4
  */
5
5
 
@@ -121,7 +121,7 @@ const noSelfTriggers = {
121
121
  !trgr.targets.includes( spec.name )
122
122
  );
123
123
  },
124
- error: 'composer/validation: Node cannot trigger control methods on itself'
124
+ error: 'winkComposer/validation: Node cannot trigger control methods on itself'
125
125
  }; // noSelfTriggers
126
126
 
127
127
  // Export all composer validators
@@ -1,5 +1,5 @@
1
1
  /**
2
- * WinkComposer validation module
2
+ * winkComposer validation module
3
3
  * Provides schema-based validation for node specifications
4
4
  */
5
5
 
@@ -95,7 +95,7 @@ const validateWithSchema = function ( schema, object, pathPrefix = 'object' ) {
95
95
  // Throw if invalid - used in init functions
96
96
  throwIfInvalid: ( nodeType ) => {
97
97
  if ( !valid ) {
98
- const errorMessage = `WinkComposer/${nodeType} validation failed:\n ${errors.join( '\n ' )}`;
98
+ const errorMessage = `winkComposer/${nodeType}: validation failed:\n ${errors.join( '\n ' )}`;
99
99
  throw new TypeError( errorMessage );
100
100
  }
101
101
  }
@@ -17,7 +17,7 @@
17
17
  * - Startup runs once per flow; the check costs nothing at runtime.
18
18
  *
19
19
  * Error message format (same across sinks):
20
- * `WinkComposer/adapter: '<adapterId>' missing required method '<name>'`
20
+ * `winkComposer/adapter: '<adapterId>' missing required method '<name>'`
21
21
  *
22
22
  * @see ADR-018 (sink method surface)
23
23
  */
@@ -32,23 +32,23 @@
32
32
  * @param {*} handle - the value returned from the factory
33
33
  * @param {string[]} requiredMethods - method names that must be functions
34
34
  * on the handle (e.g., [ 'publishNow', 'shutdown', 'getHealth' ])
35
- * @throws {Error} `WinkComposer/adapter: '<adapterId>' factory returned non-object handle`
35
+ * @throws {Error} `winkComposer/adapter: '<adapterId>' factory returned non-object handle`
36
36
  * if `handle` is null/undefined/non-object.
37
- * @throws {Error} `WinkComposer/adapter: '<adapterId>' missing required method '<name>'`
37
+ * @throws {Error} `winkComposer/adapter: '<adapterId>' missing required method '<name>'`
38
38
  * on the first missing or non-function method (fails fast — does not
39
39
  * enumerate every gap, since one is enough to abort wiring).
40
40
  */
41
41
  const assertHandle = function ( adapterId, handle, requiredMethods ) {
42
42
  if ( !handle || typeof handle !== 'object' ) {
43
43
  throw new Error(
44
- `WinkComposer/adapter: '${adapterId}' factory returned non-object handle`
44
+ `winkComposer/adapter: '${adapterId}' factory returned non-object handle`
45
45
  );
46
46
  }
47
47
  for ( let i = 0; i < requiredMethods.length; i += 1 ) {
48
48
  const name = requiredMethods[ i ];
49
49
  if ( typeof handle[ name ] !== 'function' ) {
50
50
  throw new Error(
51
- `WinkComposer/adapter: '${adapterId}' missing required method '${name}'`
51
+ `winkComposer/adapter: '${adapterId}' missing required method '${name}'`
52
52
  );
53
53
  }
54
54
  }
@@ -19,7 +19,7 @@
19
19
  * `src/core/test/adapter-module-surface.specs.js`.
20
20
  *
21
21
  * Error message format (same shape as assert-handle.js):
22
- * `WinkComposer/adapter: '<adapterId>' module missing valid
22
+ * `winkComposer/adapter: '<adapterId>' module missing valid
23
23
  * 'durabilityClass' export — got <value>; expected one of ...`
24
24
  *
25
25
  * @see ADR-018 (module surface, durability)
@@ -52,7 +52,7 @@ const assertModuleDurability = function ( adapterId, module ) {
52
52
  }
53
53
  const got = ( typeof value === 'string' ) ? `'${value}'` : String( value );
54
54
  const err = new Error(
55
- `WinkComposer/adapter: '${adapterId}' module missing valid 'durabilityClass' export ` +
55
+ `winkComposer/adapter: '${adapterId}' module missing valid 'durabilityClass' export ` +
56
56
  `(ADR-018) — got ${got}; expected one of ` +
57
57
  `${DURABILITY_CLASSES.map( ( c ) => `'${c}'` ).join( ' | ' )}`
58
58
  );
@@ -45,6 +45,7 @@
45
45
  import { assertHandle } from './assert-handle.js';
46
46
  import { assertModuleDurability } from './assert-module.js';
47
47
  import { applySemanticsRequirement } from './wire-semantics.js';
48
+ import { logger } from '../logger/index.js';
48
49
 
49
50
  /**
50
51
  * Methods the framework calls on an emitter handle later, so they have to
@@ -121,7 +122,7 @@ const emitters = ( function () {
121
122
  const module = emitterModules[ target ];
122
123
  const factory = module && module.createEmitter;
123
124
  if ( typeof factory !== 'function' ) {
124
- throw new Error( `composer: Invalid emitter module: ${target}` );
125
+ throw new Error( `winkComposer/wiring: Invalid emitter module: ${target}` );
125
126
  }
126
127
 
127
128
  // ADR-018 module-surface check: the module must say what
@@ -234,8 +235,8 @@ const emitters = ( function () {
234
235
  if ( result.status === 'rejected' ) {
235
236
  const err = result.reason || {};
236
237
  const dropped = err.dropped ? ` dropped=${describeDropped( err.dropped )}` : '';
237
- console.error(
238
- `WinkComposer/wiring: emitter '${names[ i ]}' shutdown failed [${err.code || 'UNKNOWN'}]: ${err.message || String( err )}${dropped}`
238
+ logger.error(
239
+ `winkComposer/wiring: emitter '${names[ i ]}' shutdown failed [${err.code || 'UNKNOWN'}]: ${err.message || String( err )}${dropped}`
239
240
  );
240
241
  }
241
242
  } );
@@ -11,7 +11,7 @@ const wireLinearGraph = function ( specs, nodeModules, options = {} ) {
11
11
  const nodeModule = nodeModules[ moduleName ];
12
12
 
13
13
  if (!nodeModule) {
14
- throw new Error( `composer/wireGraph: Node module '${moduleName}' not found for nodeType '${spec.nodeType}'` );
14
+ throw new Error( `winkComposer/wiring: Node module '${moduleName}' not found for nodeType '${spec.nodeType}'` );
15
15
  }
16
16
 
17
17
  const nextHops = nextHop ? [ nextHop ] : [];
@@ -37,27 +37,27 @@ const wireNode = function ( nodeModule, nodeIndex, nextHops = [], options = {} )
37
37
  const maxRecentErrors = options.maxRecentErrors || 5;
38
38
  // Validate required node module interface at wire-time
39
39
  if ( !nodeModule || typeof nodeModule.update !== 'function' ) {
40
- throw new TypeError( `composer/wireNode: Node module must implement update function; found ${typeof nodeModule.update}` );
40
+ throw new TypeError( `winkComposer/wiring: Node module must implement update function; found ${typeof nodeModule.update}` );
41
41
  }
42
42
 
43
43
  if ( typeof nodeModule.publishTo !== 'function' ) {
44
- throw new TypeError( `composer/wireNode: Node module must implement publishTo function; found ${typeof nodeModule.publishTo}` );
44
+ throw new TypeError( `winkComposer/wiring: Node module must implement publishTo function; found ${typeof nodeModule.publishTo}` );
45
45
  }
46
46
 
47
47
  // Validate next hops are executable functions
48
48
  if ( !Array.isArray( nextHops ) ) {
49
- throw new TypeError( `composer/wireNode: Next hops must be an array; found ${typeof nextHops}` );
49
+ throw new TypeError( `winkComposer/wiring: Next hops must be an array; found ${typeof nextHops}` );
50
50
  }
51
51
 
52
52
  for ( let i = 0; i < nextHops.length; i += 1 ) {
53
53
  if ( typeof nextHops[ i ] !== 'function' ) {
54
- throw new TypeError( `composer/wireNode: Next hop at index ${i} must be a function; found ${typeof nextHops[ i ]}` );
54
+ throw new TypeError( `winkComposer/wiring: Next hop at index ${i} must be a function; found ${typeof nextHops[ i ]}` );
55
55
  }
56
56
  }
57
57
 
58
58
  // Validate node index for state store access
59
59
  if ( typeof nodeIndex !== 'number' || nodeIndex < 0 ) {
60
- throw new RangeError( `composer/wireNode: Node index must be a non-negative number; found ${nodeIndex}` );
60
+ throw new RangeError( `winkComposer/wiring: Node index must be a non-negative number; found ${nodeIndex}` );
61
61
  }
62
62
 
63
63
  /**
@@ -142,7 +142,7 @@ const wireNode = function ( nodeModule, nodeIndex, nextHops = [], options = {} )
142
142
 
143
143
  // Provide node-level error isolation with contextual information
144
144
  // This prevents individual node failures from crashing the entire flow
145
- const nodeError = new Error( `Node execution failed at index ${nodeIndex}: ${error.message}` );
145
+ const nodeError = new Error( `winkComposer/wiring: Node execution failed at index ${nodeIndex}: ${error.message}` );
146
146
  nodeError.cause = error;
147
147
  nodeError.nodeIndex = nodeIndex;
148
148
  nodeError.nodeModule = nodeModule.getNodeType ? nodeModule.getNodeType() : 'unknown';
@@ -151,7 +151,7 @@ const applySemanticsRequirement = function ( adapterId, module, runtimeAssetClas
151
151
  // QuestDB's existing createStorage guard; this layer
152
152
  // catches it earlier in the lifecycle.
153
153
  const err = new Error(
154
- `WinkComposer/wiring: adapter '${adapterId}' declares semanticsRequirement.assetClass.required ` +
154
+ `winkComposer/wiring: adapter '${adapterId}' declares semanticsRequirement.assetClass.required ` +
155
155
  'but the flow has no .assetClass() — add .assetClass(assetClassDef) to the flow definition'
156
156
  );
157
157
  err.code = 'MISSING_ASSET_CLASS';
@@ -48,6 +48,7 @@
48
48
  import { assertHandle } from './assert-handle.js';
49
49
  import { assertModuleDurability } from './assert-module.js';
50
50
  import { applySemanticsRequirement } from './wire-semantics.js';
51
+ import { logger } from '../logger/index.js';
51
52
 
52
53
  /**
53
54
  * Methods the framework calls on a storage handle later, so they have to
@@ -161,7 +162,7 @@ const storages = ( function () {
161
162
 
162
163
  const module = storageModules[ storageName ];
163
164
  if ( !module?.createStorage ) {
164
- throw new Error( `composer: Invalid storage module: ${storageName}` );
165
+ throw new Error( `winkComposer/wiring: Invalid storage module: ${storageName}` );
165
166
  }
166
167
 
167
168
  // ADR-018 module-surface check: the module must say what a
@@ -322,8 +323,8 @@ const storages = ( function () {
322
323
  if ( result.status === 'rejected' ) {
323
324
  const err = result.reason || {};
324
325
  const dropped = err.dropped ? ` dropped=${describeDropped( err.dropped )}` : '';
325
- console.error(
326
- `WinkComposer/wiring: storage '${names[ i ]}' shutdown failed [${err.code || 'UNKNOWN'}]: ${err.message || String( err )}${dropped}`
326
+ logger.error(
327
+ `winkComposer/wiring: storage '${names[ i ]}' shutdown failed [${err.code || 'UNKNOWN'}]: ${err.message || String( err )}${dropped}`
327
328
  );
328
329
  }
329
330
  } );
@@ -24,9 +24,18 @@
24
24
  * `processMessage`, exactly as a success does — so `feedAll` counts it as
25
25
  * processed. See `docs/handbook/headless-flow.md`.
26
26
  *
27
+ * `err.code` vocabulary (console classification; listed per ADR-018):
28
+ * - `CALLBACK_FAILED` — the user's `onError` itself threw or rejected.
29
+ * The shared callback guard contains the fault (ADR-018): the feed
30
+ * loop continues, counters stay truthful, and each fault becomes one
31
+ * classified console line carrying the detail.
32
+ *
27
33
  * @see docs/handbook/headless-flow.md
28
34
  */
29
35
 
36
+ import { wrapCallback } from '../core/utils/callback-guard/index.js';
37
+ import { logger } from '../core/logger/index.js';
38
+
30
39
  /**
31
40
  * Default fault handler. Surfaces every fault so none is silent. Allocates only
32
41
  * when a fault occurs (rare), never on the success path. Provide your own
@@ -37,7 +46,7 @@
37
46
  * @returns {void}
38
47
  */
39
48
  const logFault = function ( error ) {
40
- console.error( `composer/headlessDriver: a message failed — ${error.message}` );
49
+ logger.error( `winkComposer/headlessDriver: a message failed — ${error.message}` );
41
50
  }; // logFault()
42
51
 
43
52
  /**
@@ -56,18 +65,34 @@ const logFault = function ( error ) {
56
65
  const headlessDriver = function ( handle, opts = {} ) {
57
66
  if ( !handle || typeof handle.processMessage !== 'function' ) {
58
67
  throw new TypeError(
59
- 'composer/headlessDriver: handle must be the object returned by ' +
68
+ 'winkComposer/headlessDriver: handle must be the object returned by ' +
60
69
  'flow(...).run() — it has no processMessage() function'
61
70
  );
62
71
  }
63
72
 
64
- const onError = opts.onError === undefined ? logFault : opts.onError;
65
- if ( typeof onError !== 'function' ) {
73
+ const userOnError = opts.onError === undefined ? logFault : opts.onError;
74
+ if ( typeof userOnError !== 'function' ) {
66
75
  throw new TypeError(
67
- 'composer/headlessDriver: onError must be a function'
76
+ 'winkComposer/headlessDriver: onError must be a function'
68
77
  );
69
78
  }
70
79
 
80
+ // Armed once at construction (validate raw first, then wrap). The
81
+ // fault reporter is user code too, so it gets the same containment
82
+ // as the messages it reports on. ADR-018's rule: a misbehaving
83
+ // user callback never interrupts the operation that invoked it.
84
+ // A throwing or rejecting onError costs one classified console
85
+ // line. The feed continues and the counters stay truthful.
86
+ const onError = wrapCallback( userOnError, {
87
+ name: 'onError',
88
+ severity: 'red',
89
+ report: function ( severity, name, detail ) {
90
+ logger.error(
91
+ `winkComposer/headlessDriver: user callback ${name} failed [CALLBACK_FAILED]: ${detail}`
92
+ );
93
+ }
94
+ } );
95
+
71
96
  /**
72
97
  * Feed one message. Use this for push sources, where an event hands you a
73
98
  * message and there is no loop to own.
@@ -123,7 +148,7 @@ const headlessDriver = function ( handle, opts = {} ) {
123
148
  const isSync = typeof source?.[ Symbol.iterator ] === 'function';
124
149
  if ( !isAsync && !isSync ) {
125
150
  throw new TypeError(
126
- 'composer/headlessDriver.feedAll: source must be a sync or async ' +
151
+ 'winkComposer/headlessDriver.feedAll: source must be a sync or async ' +
127
152
  'iterable (an array, a generator, or a stream)'
128
153
  );
129
154
  }
@@ -12,12 +12,12 @@ const expandXOutputsOptions = function ( meta, nodeArgs, flowState ) {
12
12
  const param = inputs[ i ];
13
13
 
14
14
  if ( seenX[ param ] === 1 ) {
15
- throw new Error( `WinkComposer/flow: duplicate input found: '${param}'.` );
15
+ throw new Error( `winkComposer/flow: duplicate input found: '${param}'.` );
16
16
  }
17
17
  seenX[ param ] = 1;
18
18
 
19
19
  const instName = `${baseName}_${param}`;
20
- if ( isNodeNameDuplicate( instName ) ) throw Error( `WinkComposer/flow: duplicate node ${instName} found.` );
20
+ if ( isNodeNameDuplicate( instName ) ) throw Error( `winkComposer/flow: duplicate node ${instName} found.` );
21
21
  const stats = Object.create( null );
22
22
  for ( const st in outputs ) {
23
23
  if (Object.prototype.hasOwnProperty.call( outputs, st ) ) {
@@ -47,12 +47,12 @@ const expandXOptions = function ( meta, nodeArgs, flowState ) {
47
47
  const param = inputs[ i ];
48
48
 
49
49
  if ( seenX[ param ] === 1 ) {
50
- throw new Error( `WinkComposer/flow: duplicate input found: '${param}'.` );
50
+ throw new Error( `winkComposer/flow: duplicate input found: '${param}'.` );
51
51
  }
52
52
  seenX[ param ] = 1;
53
53
 
54
54
  const instName = `${baseName}_${param}`;
55
- if ( isNodeNameDuplicate( instName ) ) throw Error( `WinkComposer/flow: duplicate node ${instName} found.` );
55
+ if ( isNodeNameDuplicate( instName ) ) throw Error( `winkComposer/flow: duplicate node ${instName} found.` );
56
56
 
57
57
  const spec = meta.buildSpec( instName, param, ...balanceArgs );
58
58
  specs.push( spec );
@@ -226,7 +226,7 @@ export const validateFlowConfig = function ( config, schema, methodName ) {
226
226
  valid: false,
227
227
  errors,
228
228
  throwIfInvalid: () => {
229
- throw new Error( `WinkComposer/flow.${methodName}: ${errors[ 0 ]}` );
229
+ throw new Error( `winkComposer/flow.${methodName}: ${errors[ 0 ]}` );
230
230
  }
231
231
  };
232
232
  }
@@ -241,7 +241,7 @@ export const validateFlowConfig = function ( config, schema, methodName ) {
241
241
  throwIfInvalid: () => {
242
242
  if ( !validation.valid ) {
243
243
  throw new Error(
244
- `WinkComposer/flow.${methodName}: validation failed:\n - ${validation.errors.join( '\n - ' )}`
244
+ `winkComposer/flow.${methodName}: validation failed:\n - ${validation.errors.join( '\n - ' )}`
245
245
  );
246
246
  }
247
247
  }
@@ -266,7 +266,7 @@ export const validateFlowArrayConfig = function ( config, schema, methodName ) {
266
266
  valid: false,
267
267
  errors,
268
268
  throwIfInvalid: () => {
269
- throw new Error( `WinkComposer/flow.${methodName}: ${errors[ 0 ]}` );
269
+ throw new Error( `winkComposer/flow.${methodName}: ${errors[ 0 ]}` );
270
270
  }
271
271
  };
272
272
  }
@@ -280,7 +280,7 @@ export const validateFlowArrayConfig = function ( config, schema, methodName ) {
280
280
  throwIfInvalid: () => {
281
281
  if ( !validation.valid ) {
282
282
  throw new Error(
283
- `WinkComposer/flow.${methodName}: validation failed:\n - ${validation.errors.join( '\n - ' )}`
283
+ `winkComposer/flow.${methodName}: validation failed:\n - ${validation.errors.join( '\n - ' )}`
284
284
  );
285
285
  }
286
286
  }