@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
@@ -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
@@ -145,6 +146,13 @@ const storages = ( function () {
145
146
  // to assetClass.name if not explicitly specified.
146
147
  const wire = async function ( specs, storageConfigs, storageModules, assetClass = null ) {
147
148
  const pendingStorages = [];
149
+ // Names whose factory has been called in THIS wire() call. The
150
+ // singleton slot is filled only after every pending factory has
151
+ // settled. Without this set, a flow with several persist-if nodes
152
+ // on one storage would call the factory once per node and keep
153
+ // only the last handle. Each orphan would hold a flush timer, an
154
+ // agent, and a sender for the life of the process.
155
+ const pendingNames = new Set();
148
156
 
149
157
  for ( const spec of specs ) {
150
158
  // Skip non-persistIf nodes
@@ -154,14 +162,14 @@ const storages = ( function () {
154
162
 
155
163
  const storageName = spec.storageName;
156
164
 
157
- // Skip if already in singleton registry
158
- if ( singletons[ storageName ] ) {
165
+ // Skip if already in singleton registry, or already being built
166
+ if ( singletons[ storageName ] || pendingNames.has( storageName ) ) {
159
167
  continue; // eslint-disable-line no-continue
160
168
  }
161
169
 
162
170
  const module = storageModules[ storageName ];
163
171
  if ( !module?.createStorage ) {
164
- throw new Error( `composer: Invalid storage module: ${storageName}` );
172
+ throw new Error( `winkComposer/wiring: Invalid storage module: ${storageName}` );
165
173
  }
166
174
 
167
175
  // ADR-018 module-surface check: the module must say what a
@@ -195,6 +203,7 @@ const storages = ( function () {
195
203
 
196
204
  // createStorage may return Promise (async) or storage directly (sync)
197
205
  const storageOrPromise = module.createStorage( effectiveConfig );
206
+ pendingNames.add( storageName );
198
207
  pendingStorages.push( { storageName, storageOrPromise } );
199
208
  }
200
209
 
@@ -322,8 +331,8 @@ const storages = ( function () {
322
331
  if ( result.status === 'rejected' ) {
323
332
  const err = result.reason || {};
324
333
  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}`
334
+ logger.error(
335
+ `winkComposer/wiring: storage '${names[ i ]}' shutdown failed [${err.code || 'UNKNOWN'}]: ${err.message || String( err )}${dropped}`
327
336
  );
328
337
  }
329
338
  } );
@@ -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
  }
package/src/flow/flow.js CHANGED
@@ -47,6 +47,27 @@ import { validateWithSchema } from '../core/utils/validate/index.js';
47
47
  import { assetClassSchema } from '../core/semantics/schemas/index.js';
48
48
  import { expandTemplateSpec } from './expand-template-spec.js';
49
49
 
50
+ /**
51
+ * Throws the flow's setup error for an adapter config that the
52
+ * adapter's own schema rejected. The error carries `INVALID_CONFIG`,
53
+ * the ADR-018 setup code, and the ADR-028 bracket. A program matches
54
+ * the code; an operator reads the remedy the schema names.
55
+ *
56
+ * @param {{valid: boolean, errors: string[]}} validation - The schema result
57
+ * @param {string} label - `flow.<method>.<adapterId>`, for the message
58
+ * @throws {Error} INVALID_CONFIG when the config is invalid
59
+ */
60
+ const throwIfAdapterConfigInvalid = function ( validation, label ) {
61
+ if ( validation.valid ) {
62
+ return;
63
+ }
64
+ const err = new Error(
65
+ `winkComposer/${label}: validation failed [INVALID_CONFIG]:\n - ${validation.errors.join( '\n - ' )}`
66
+ );
67
+ err.code = 'INVALID_CONFIG';
68
+ throw err;
69
+ }; // throwIfAdapterConfigInvalid()
70
+
50
71
  const flow = function ( flowName = 'flow1' ) {
51
72
  const api = Object.create( null );
52
73
 
@@ -91,7 +112,7 @@ const flow = function ( flowName = 'flow1' ) {
91
112
  const enforceConfigFirst = function ( methodName ) {
92
113
  if ( pipelineStarted ) {
93
114
  throw new Error(
94
- `WinkComposer/flow: .${methodName}() must be called before any nodes or .switch()/.groupBy()`
115
+ `winkComposer/flow: .${methodName}() must be called before any nodes or .switch()/.groupBy()`
95
116
  );
96
117
  }
97
118
  };
@@ -100,11 +121,11 @@ const flow = function ( flowName = 'flow1' ) {
100
121
  const validateSwitchComplete = function () {
101
122
  if ( switchState.active ) {
102
123
  if ( switchState.caseOrder.length === 0 ) {
103
- throw new Error( 'WinkComposer/flow: .switch() requires at least one .case()' );
124
+ throw new Error( 'winkComposer/flow: .switch() requires at least one .case()' );
104
125
  }
105
126
  if ( !switchState.caseEnded ) {
106
127
  throw new Error(
107
- `WinkComposer/flow: case '${switchState.currentCase}' must end with .break()`
128
+ `winkComposer/flow: case '${switchState.currentCase}' must end with .break()`
108
129
  );
109
130
  }
110
131
  }
@@ -118,7 +139,7 @@ const flow = function ( flowName = 'flow1' ) {
118
139
  const collisions = collectFlowOutputCollisions( switchState, flowDefinition );
119
140
  if ( collisions.length > 0 ) {
120
141
  throw new Error(
121
- 'WinkComposer/flow: output field collision(s) - two nodes on one path ' +
142
+ 'winkComposer/flow: output field collision(s) - two nodes on one path ' +
122
143
  'write the same message field:\n - ' + collisions.join( '\n - ' )
123
144
  );
124
145
  }
@@ -160,10 +181,10 @@ const flow = function ( flowName = 'flow1' ) {
160
181
  api.source = function ( adapter, sourceConfig = {} ) {
161
182
  enforceConfigFirst( 'source' );
162
183
  if ( typeof adapter !== 'object' || adapter === null ) {
163
- throw new Error( 'WinkComposer/flow: source adapter must be an imported module' );
184
+ throw new Error( 'winkComposer/flow: source adapter must be an imported module' );
164
185
  }
165
186
  if ( typeof adapter.start !== 'function' ) {
166
- throw new Error( 'WinkComposer/flow: source adapter must have a start() function' );
187
+ throw new Error( 'winkComposer/flow: source adapter must have a start() function' );
167
188
  }
168
189
 
169
190
  // If the adapter provides a configSchema, we validate config here.
@@ -171,7 +192,7 @@ const flow = function ( flowName = 'flow1' ) {
171
192
  // not later inside adapter.start().
172
193
  if ( adapter.configSchema ) {
173
194
  const validation = validateWithSchema( adapter.configSchema, sourceConfig, 'source' );
174
- validation.throwIfInvalid( `flow/source:${adapter.id || 'unknown'}` );
195
+ throwIfAdapterConfigInvalid( validation, `flow.source.${adapter.id || 'unknown'}` );
175
196
  }
176
197
 
177
198
  runtime.source = { adapter, config: sourceConfig };
@@ -181,13 +202,13 @@ const flow = function ( flowName = 'flow1' ) {
181
202
  api.emitter = function ( adapter, emitterConfig = {} ) {
182
203
  enforceConfigFirst( 'emitter' );
183
204
  if ( typeof adapter !== 'object' || adapter === null ) {
184
- throw new Error( 'WinkComposer/flow: emitter adapter must be an imported module' );
205
+ throw new Error( 'winkComposer/flow: emitter adapter must be an imported module' );
185
206
  }
186
207
  if ( typeof adapter.id !== 'string' || !adapter.id ) {
187
- throw new Error( 'WinkComposer/flow: emitter adapter must have an id' );
208
+ throw new Error( 'winkComposer/flow: emitter adapter must have an id' );
188
209
  }
189
210
  if ( typeof adapter.createEmitter !== 'function' ) {
190
- throw new Error( 'WinkComposer/flow: emitter adapter must have a createEmitter() function' );
211
+ throw new Error( 'winkComposer/flow: emitter adapter must have a createEmitter() function' );
191
212
  }
192
213
 
193
214
  // If the adapter provides a configSchema, we validate config here.
@@ -195,7 +216,7 @@ const flow = function ( flowName = 'flow1' ) {
195
216
  // not later inside adapter.createEmitter().
196
217
  if ( adapter.configSchema ) {
197
218
  const validation = validateWithSchema( adapter.configSchema, emitterConfig, 'emitter' );
198
- validation.throwIfInvalid( `flow/emitter:${adapter.id}` );
219
+ throwIfAdapterConfigInvalid( validation, `flow.emitter.${adapter.id}` );
199
220
  }
200
221
 
201
222
  runtime.emitters[ adapter.id ] = { adapter, config: emitterConfig };
@@ -205,13 +226,13 @@ const flow = function ( flowName = 'flow1' ) {
205
226
  api.storage = function ( adapter, storageConfig = {} ) {
206
227
  enforceConfigFirst( 'storage' );
207
228
  if ( typeof adapter !== 'object' || adapter === null ) {
208
- throw new Error( 'WinkComposer/flow: storage adapter must be an imported module' );
229
+ throw new Error( 'winkComposer/flow: storage adapter must be an imported module' );
209
230
  }
210
231
  if ( typeof adapter.id !== 'string' || !adapter.id ) {
211
- throw new Error( 'WinkComposer/flow: storage adapter must have an id' );
232
+ throw new Error( 'winkComposer/flow: storage adapter must have an id' );
212
233
  }
213
234
  if ( typeof adapter.createStorage !== 'function' ) {
214
- throw new Error( 'WinkComposer/flow: storage adapter must have a createStorage() function' );
235
+ throw new Error( 'winkComposer/flow: storage adapter must have a createStorage() function' );
215
236
  }
216
237
 
217
238
  // If the adapter provides a configSchema, we validate config here.
@@ -219,7 +240,7 @@ const flow = function ( flowName = 'flow1' ) {
219
240
  // not later inside adapter.createStorage().
220
241
  if ( adapter.configSchema ) {
221
242
  const validation = validateWithSchema( adapter.configSchema, storageConfig, 'storage' );
222
- validation.throwIfInvalid( `flow/storage:${adapter.id}` );
243
+ throwIfAdapterConfigInvalid( validation, `flow.storage.${adapter.id}` );
223
244
  }
224
245
 
225
246
  runtime.storages[ adapter.id ] = { adapter, config: storageConfig };
@@ -230,7 +251,7 @@ const flow = function ( flowName = 'flow1' ) {
230
251
  enforceConfigFirst( 'assetId' );
231
252
 
232
253
  if ( typeof field !== 'string' || field.length === 0 ) {
233
- throw new Error( 'WinkComposer/flow: .assetId() requires a non-empty string field name' );
254
+ throw new Error( 'winkComposer/flow: .assetId() requires a non-empty string field name' );
234
255
  }
235
256
 
236
257
  // Schema-based validation for robust error handling
@@ -251,7 +272,7 @@ const flow = function ( flowName = 'flow1' ) {
251
272
  if ( assetClassDef === null || assetClassDef === undefined || typeof assetClassDef !== 'object' ) {
252
273
  const actualType = assetClassDef === null ? 'null' : typeof assetClassDef;
253
274
  throw new Error(
254
- `WinkComposer/flow.assetClass: Expected object, got ${actualType}`
275
+ `winkComposer/flow.assetClass: Expected object, got ${actualType}`
255
276
  );
256
277
  }
257
278
 
@@ -266,7 +287,7 @@ const flow = function ( flowName = 'flow1' ) {
266
287
  const validation = validateWithSchema( assetClassSchema, assetClassDef, 'assetClass' );
267
288
  if ( !validation.valid ) {
268
289
  throw new Error(
269
- `WinkComposer/flow.assetClass: validation failed:\n - ${validation.errors.join( '\n - ' )}`
290
+ `winkComposer/flow.assetClass: validation failed:\n - ${validation.errors.join( '\n - ' )}`
270
291
  );
271
292
  }
272
293
 
@@ -298,7 +319,7 @@ const flow = function ( flowName = 'flow1' ) {
298
319
  */
299
320
  api.switch = function ( field ) {
300
321
  if ( typeof field !== 'string' || field.length === 0 ) {
301
- throw new Error( 'WinkComposer/flow: .switch() requires a non-empty string field name' );
322
+ throw new Error( 'winkComposer/flow: .switch() requires a non-empty string field name' );
302
323
  }
303
324
 
304
325
  // Schema-based validation: field must be a valid identifier (mirrors .assetId())
@@ -307,21 +328,21 @@ const flow = function ( flowName = 'flow1' ) {
307
328
 
308
329
  // Check groupBy before switchState (endGroup sets switchState.active)
309
330
  if ( groupByState.active || groupByState.expanded ) {
310
- throw new Error( 'WinkComposer/flow: .switch() cannot be used with .groupBy()' );
331
+ throw new Error( 'winkComposer/flow: .switch() cannot be used with .groupBy()' );
311
332
  }
312
333
  if ( switchState.active ) {
313
- throw new Error( 'WinkComposer/flow: .switch() can only be called once per flow' );
334
+ throw new Error( 'winkComposer/flow: .switch() can only be called once per flow' );
314
335
  }
315
336
  if ( flowDefinition.length > 0 ) {
316
337
  throw new Error(
317
- 'WinkComposer/flow: .switch() cannot be called after nodes - ' +
338
+ 'winkComposer/flow: .switch() cannot be called after nodes - ' +
318
339
  'use .switch() before defining any nodes, or omit .switch() for single-pipeline flows'
319
340
  );
320
341
  }
321
342
 
322
343
  // Validate field conflict with partition
323
344
  if ( runtime.partitionField === field ) {
324
- throw new Error( 'WinkComposer/flow: partition field and switch field must be different' );
345
+ throw new Error( 'winkComposer/flow: partition field and switch field must be different' );
325
346
  }
326
347
 
327
348
  runtime.specializationField = field;
@@ -340,15 +361,15 @@ const flow = function ( flowName = 'flow1' ) {
340
361
  */
341
362
  api.case = function ( key ) {
342
363
  if ( !switchState.active ) {
343
- throw new Error( 'WinkComposer/flow: .case() requires an active .switch()' );
364
+ throw new Error( 'winkComposer/flow: .case() requires an active .switch()' );
344
365
  }
345
366
  if ( switchState.currentCase !== null && !switchState.caseEnded ) {
346
367
  throw new Error(
347
- `WinkComposer/flow: previous case '${switchState.currentCase}' must end with .break() before starting a new case`
368
+ `winkComposer/flow: previous case '${switchState.currentCase}' must end with .break() before starting a new case`
348
369
  );
349
370
  }
350
371
  if ( switchState.caseSpecs[ key ] !== undefined ) {
351
- throw new Error( `WinkComposer/flow: duplicate case key '${key}'` );
372
+ throw new Error( `winkComposer/flow: duplicate case key '${key}'` );
352
373
  }
353
374
 
354
375
  switchState.currentCase = key;
@@ -367,14 +388,14 @@ const flow = function ( flowName = 'flow1' ) {
367
388
  */
368
389
  api.break = function () {
369
390
  if ( !switchState.active ) {
370
- throw new Error( 'WinkComposer/flow: .break() requires an active .switch()' );
391
+ throw new Error( 'winkComposer/flow: .break() requires an active .switch()' );
371
392
  }
372
393
  if ( switchState.currentCase === null || switchState.caseEnded ) {
373
- throw new Error( 'WinkComposer/flow: .break() must follow a .case() with at least one node' );
394
+ throw new Error( 'winkComposer/flow: .break() must follow a .case() with at least one node' );
374
395
  }
375
396
  if ( switchState.caseSpecs[ switchState.currentCase ].length === 0 ) {
376
397
  throw new Error(
377
- `WinkComposer/flow: case '${switchState.currentCase}' is empty - add at least one node before .break()`
398
+ `winkComposer/flow: case '${switchState.currentCase}' is empty - add at least one node before .break()`
378
399
  );
379
400
  }
380
401
 
@@ -408,7 +429,7 @@ const flow = function ( flowName = 'flow1' ) {
408
429
  api.groupBy = function ( field, values ) {
409
430
  // Validation: field must be non-empty string
410
431
  if ( typeof field !== 'string' || field.length === 0 ) {
411
- throw new Error( 'WinkComposer/flow: .groupBy() requires a non-empty string field name' );
432
+ throw new Error( 'winkComposer/flow: .groupBy() requires a non-empty string field name' );
412
433
  }
413
434
 
414
435
  // Schema-based validation: field must be a valid identifier (mirrors .assetId())
@@ -417,14 +438,14 @@ const flow = function ( flowName = 'flow1' ) {
417
438
 
418
439
  // Validation: values must be array with at least 2 elements
419
440
  if ( !Array.isArray( values ) || values.length < 2 ) {
420
- throw new Error( 'WinkComposer/flow: .groupBy() requires at least 2 group values' );
441
+ throw new Error( 'winkComposer/flow: .groupBy() requires at least 2 group values' );
421
442
  }
422
443
 
423
444
  // Validation: each value must be string or number
424
445
  for ( let i = 0; i < values.length; i += 1 ) {
425
446
  const v = values[ i ];
426
447
  if ( typeof v !== 'string' && typeof v !== 'number' ) {
427
- throw new Error( `WinkComposer/flow: group value at index ${i} must be a string or number` );
448
+ throw new Error( `winkComposer/flow: group value at index ${i} must be a string or number` );
428
449
  }
429
450
  }
430
451
 
@@ -433,32 +454,32 @@ const flow = function ( flowName = 'flow1' ) {
433
454
  for ( let i = 0; i < values.length; i += 1 ) {
434
455
  const v = values[ i ];
435
456
  if ( seen[ v ] ) {
436
- throw new Error( `WinkComposer/flow: duplicate group value '${v}' in .groupBy()` );
457
+ throw new Error( `winkComposer/flow: duplicate group value '${v}' in .groupBy()` );
437
458
  }
438
459
  seen[ v ] = 1;
439
460
  }
440
461
 
441
462
  // Validation: cannot call groupBy twice (check this first - user intent is to reuse groupBy)
442
463
  if ( groupByState.active || groupByState.expanded ) {
443
- throw new Error( 'WinkComposer/flow: .groupBy() can only be called once per flow' );
464
+ throw new Error( 'winkComposer/flow: .groupBy() can only be called once per flow' );
444
465
  }
445
466
 
446
467
  // Validation: cannot use groupBy if switchState is active (manual switch, not from endGroup)
447
468
  if ( switchState.active ) {
448
- throw new Error( 'WinkComposer/flow: .groupBy() cannot be used inside .switch()' );
469
+ throw new Error( 'winkComposer/flow: .groupBy() cannot be used inside .switch()' );
449
470
  }
450
471
 
451
472
  // Validation: must come before any nodes
452
473
  if ( flowDefinition.length > 0 ) {
453
474
  throw new Error(
454
- 'WinkComposer/flow: .groupBy() cannot be called after nodes - ' +
475
+ 'winkComposer/flow: .groupBy() cannot be called after nodes - ' +
455
476
  'use .groupBy() before defining any nodes'
456
477
  );
457
478
  }
458
479
 
459
480
  // Validation: groupBy field cannot equal partition field
460
481
  if ( runtime.partitionField === field ) {
461
- throw new Error( 'WinkComposer/flow: partition field and groupBy field must be different' );
482
+ throw new Error( 'winkComposer/flow: partition field and groupBy field must be different' );
462
483
  }
463
484
 
464
485
  // Initialize groupBy state
@@ -487,12 +508,12 @@ const flow = function ( flowName = 'flow1' ) {
487
508
  api.endGroup = function () {
488
509
  // Validation: must be in active groupBy state
489
510
  if ( !groupByState.active ) {
490
- throw new Error( 'WinkComposer/flow: .endGroup() requires an active .groupBy()' );
511
+ throw new Error( 'winkComposer/flow: .endGroup() requires an active .groupBy()' );
491
512
  }
492
513
 
493
514
  // Validation: must have at least one template node
494
515
  if ( groupByState.templateSpecs.length === 0 ) {
495
- throw new Error( 'WinkComposer/flow: .groupBy() must contain at least one node before .endGroup()' );
516
+ throw new Error( 'winkComposer/flow: .groupBy() must contain at least one node before .endGroup()' );
496
517
  }
497
518
 
498
519
  // Set specialization field (same as switch does)
@@ -553,7 +574,7 @@ const flow = function ( flowName = 'flow1' ) {
553
574
 
554
575
  // Single-pipeline mode (backward compatible)
555
576
  if ( flowDefinition.length === 0 ) {
556
- throw new Error( 'WinkComposer/flow: Cannot build empty flow - add at least one node' );
577
+ throw new Error( 'winkComposer/flow: Cannot build empty flow - add at least one node' );
557
578
  }
558
579
  const imports = Array.from( importSet ).sort();
559
580
  return serializeModule( { imports, flowName, specs: flowDefinition } );
@@ -646,7 +667,7 @@ const flow = function ( flowName = 'flow1' ) {
646
667
 
647
668
  // Single-pipeline mode (backward compatible)
648
669
  if ( flowDefinition.length === 0 ) {
649
- throw new Error( 'WinkComposer/flow: Cannot run empty flow - add at least one node' );
670
+ throw new Error( 'winkComposer/flow: Cannot run empty flow - add at least one node' );
650
671
  }
651
672
  const handle = await runFlow( flowName, flowDefinition, importSet, runtime );
652
673
  return handle;