@compilr-dev/sdk 0.10.11 → 0.10.12

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.
@@ -38,6 +38,16 @@ const SUMMARY_RENDERERS = ['compact', 'detailed'];
38
38
  * slug doesn't map to a component.
39
39
  */
40
40
  const ICON_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
41
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-unnecessary-type-conversion --
42
+ * The entire validator section runs DEFENSIVE runtime checks against agent
43
+ * JSON arriving over IPC. TypeScript types are lies at this boundary — an
44
+ * agent's malformed input can have any field missing or wrong-typed, and we
45
+ * must return structured errors rather than throw. The "always-falsy" and
46
+ * "redundant String()" warnings would be true if we only saw type-safe
47
+ * callers, but our caller is `JSON.parse(IPC payload)` cast to Flow. The
48
+ * pragmatic alternative (passing `unknown` through every helper signature)
49
+ * would be a much larger refactor for no behavioral gain.
50
+ */
41
51
  /**
42
52
  * Validate a flow. Accepts `unknown` because inputs flow in as JSON from the
43
53
  * agent and TS types are lies at the boundary — runtime defensive checks
@@ -75,10 +85,18 @@ export function validateFlow(flowInput) {
75
85
  // 2. Each node's id must match the key it lives under
76
86
  for (const id of nodeIds) {
77
87
  const node = flow.nodes[id];
88
+ if (!node || typeof node !== 'object') {
89
+ errors.push({
90
+ code: 'INVALID_NODE_TYPE',
91
+ message: `flow.nodes['${id}'] is null or not an object`,
92
+ nodeId: id,
93
+ });
94
+ continue;
95
+ }
78
96
  if (node.id !== id) {
79
97
  errors.push({
80
98
  code: 'DUPLICATE_NODE_ID',
81
- message: `Node key '${id}' does not match node.id '${node.id}'`,
99
+ message: `Node key '${id}' does not match node.id '${String(node.id)}'`,
82
100
  nodeId: id,
83
101
  });
84
102
  }
@@ -86,6 +104,8 @@ export function validateFlow(flowInput) {
86
104
  // 3. Per-node validation: type, references, icons, renderers, conditions
87
105
  for (const id of nodeIds) {
88
106
  const node = flow.nodes[id];
107
+ if (!node || typeof node !== 'object')
108
+ continue; // already flagged above
89
109
  validateNode(node, flow.nodes, errors);
90
110
  }
91
111
  // 4. Reachability — warn on orphans
@@ -125,6 +145,16 @@ function validateNode(node, allNodes, errors) {
125
145
  }
126
146
  }
127
147
  function validateQuestion(node, allNodes, errors) {
148
+ // Defensive: agent JSON may omit required nested fields. Surface as
149
+ // structured errors rather than letting the validator throw.
150
+ if (!node.input || typeof node.input !== 'object') {
151
+ errors.push({
152
+ code: 'INVALID_NODE_TYPE',
153
+ message: `Question node '${node.id}' is missing the required 'input' object (must contain 'mode' and choices/options/etc.)`,
154
+ nodeId: node.id,
155
+ });
156
+ return;
157
+ }
128
158
  // Input mode must have a valid renderer if specified. The lookup CAN miss
129
159
  // at runtime if the agent passed an unknown mode in the JSON — TS narrowing
130
160
  // doesn't help us across the boundary, hence the explicit widened type.
@@ -132,7 +162,7 @@ function validateQuestion(node, allNodes, errors) {
132
162
  if (!validRenderers) {
133
163
  errors.push({
134
164
  code: 'INVALID_NODE_TYPE',
135
- message: `Unknown question input mode '${node.input.mode}'`,
165
+ message: `Unknown question input mode '${String(node.input.mode)}' for node '${node.id}' (expected: single, multi, text, or proposal)`,
136
166
  nodeId: node.id,
137
167
  });
138
168
  return;
@@ -144,23 +174,67 @@ function validateQuestion(node, allNodes, errors) {
144
174
  nodeId: node.id,
145
175
  });
146
176
  }
147
- // Validate icons on choices/proposals
177
+ // Validate choices/options arrays are present and well-formed
148
178
  if (node.input.mode === 'single' || node.input.mode === 'multi') {
149
- for (const choice of node.input.choices) {
150
- if (choice.icon !== undefined)
151
- checkIcon(choice.icon, node.id, errors);
179
+ if (!Array.isArray(node.input.choices)) {
180
+ errors.push({
181
+ code: 'INVALID_NODE_TYPE',
182
+ message: `Question '${node.id}' with mode '${node.input.mode}' requires a 'choices' array`,
183
+ nodeId: node.id,
184
+ });
185
+ }
186
+ else {
187
+ for (const choice of node.input.choices) {
188
+ if (!choice || typeof choice !== 'object')
189
+ continue;
190
+ if (choice.icon !== undefined)
191
+ checkIcon(choice.icon, node.id, errors);
192
+ }
152
193
  }
153
194
  }
154
195
  else if (node.input.mode === 'proposal') {
155
- for (const opt of node.input.options) {
156
- if (opt.icon !== undefined)
157
- checkIcon(opt.icon, node.id, errors);
196
+ if (!Array.isArray(node.input.options)) {
197
+ errors.push({
198
+ code: 'INVALID_NODE_TYPE',
199
+ message: `Question '${node.id}' with mode 'proposal' requires an 'options' array`,
200
+ nodeId: node.id,
201
+ });
202
+ }
203
+ else {
204
+ for (const opt of node.input.options) {
205
+ if (!opt || typeof opt !== 'object')
206
+ continue;
207
+ if (opt.icon !== undefined)
208
+ checkIcon(opt.icon, node.id, errors);
209
+ }
158
210
  }
159
211
  }
160
- // Validate next references
212
+ // Validate next references (defensive — `next` might be undefined)
213
+ if (node.next === undefined || node.next === null) {
214
+ errors.push({
215
+ code: 'UNRESOLVED_NEXT',
216
+ message: `Question node '${node.id}' is missing the required 'next' field (string nodeId or { byAnswer: ... })`,
217
+ nodeId: node.id,
218
+ });
219
+ return;
220
+ }
161
221
  validateNextRef(node.next, node, allNodes, errors);
162
222
  }
163
223
  function validateInfo(node, allNodes, errors) {
224
+ if (typeof node.title !== 'string' || node.title.length === 0) {
225
+ errors.push({
226
+ code: 'INVALID_NODE_TYPE',
227
+ message: `Info node '${node.id}' is missing required 'title' string`,
228
+ nodeId: node.id,
229
+ });
230
+ }
231
+ if (typeof node.body !== 'string') {
232
+ errors.push({
233
+ code: 'INVALID_NODE_TYPE',
234
+ message: `Info node '${node.id}' is missing required 'body' string (markdown content)`,
235
+ nodeId: node.id,
236
+ });
237
+ }
164
238
  if (node.render && !INFO_RENDERERS.includes(node.render)) {
165
239
  errors.push({
166
240
  code: 'INVALID_RENDERER',
@@ -168,31 +242,56 @@ function validateInfo(node, allNodes, errors) {
168
242
  nodeId: node.id,
169
243
  });
170
244
  }
245
+ if (node.next === undefined || node.next === null) {
246
+ errors.push({
247
+ code: 'UNRESOLVED_NEXT',
248
+ message: `Info node '${node.id}' is missing the required 'next' field`,
249
+ nodeId: node.id,
250
+ });
251
+ return;
252
+ }
171
253
  validateNextRef(node.next, node, allNodes, errors);
172
254
  }
173
255
  function validateBranch(node, allNodes, errors) {
174
- // All routes must reference existing nodes and use valid conditions
175
- for (let i = 0; i < node.routes.length; i++) {
176
- const route = node.routes[i];
177
- if (!isValidCondition(route.when)) {
178
- errors.push({
179
- code: 'INVALID_CONDITION',
180
- message: `Branch route ${String(i)} has an invalid condition shape`,
181
- nodeId: node.id,
182
- });
183
- }
184
- if (!(route.goto in allNodes)) {
185
- errors.push({
186
- code: 'UNRESOLVED_BRANCH',
187
- message: `Branch route ${String(i)} goto '${route.goto}' is not defined in flow.nodes`,
188
- nodeId: node.id,
189
- });
256
+ // Defensive: routes might be missing or non-array
257
+ if (!Array.isArray(node.routes)) {
258
+ errors.push({
259
+ code: 'INVALID_NODE_TYPE',
260
+ message: `Branch node '${node.id}' requires a 'routes' array (each item: { when: ..., goto: nodeId })`,
261
+ nodeId: node.id,
262
+ });
263
+ }
264
+ else {
265
+ for (let i = 0; i < node.routes.length; i++) {
266
+ const route = node.routes[i];
267
+ if (!route || typeof route !== 'object') {
268
+ errors.push({
269
+ code: 'INVALID_CONDITION',
270
+ message: `Branch route ${String(i)} on node '${node.id}' is null or not an object`,
271
+ nodeId: node.id,
272
+ });
273
+ continue;
274
+ }
275
+ if (!isValidCondition(route.when)) {
276
+ errors.push({
277
+ code: 'INVALID_CONDITION',
278
+ message: `Branch route ${String(i)} has an invalid condition shape`,
279
+ nodeId: node.id,
280
+ });
281
+ }
282
+ if (typeof route.goto !== 'string' || !(route.goto in allNodes)) {
283
+ errors.push({
284
+ code: 'UNRESOLVED_BRANCH',
285
+ message: `Branch route ${String(i)} goto '${String(route.goto)}' is not defined in flow.nodes`,
286
+ nodeId: node.id,
287
+ });
288
+ }
190
289
  }
191
290
  }
192
- if (!node.default || !(node.default in allNodes)) {
291
+ if (!node.default || typeof node.default !== 'string' || !(node.default in allNodes)) {
193
292
  errors.push({
194
293
  code: 'UNRESOLVED_BRANCH',
195
- message: `Branch default '${node.default}' is not defined in flow.nodes`,
294
+ message: `Branch default '${String(node.default)}' is not defined in flow.nodes`,
196
295
  nodeId: node.id,
197
296
  });
198
297
  }
@@ -218,12 +317,20 @@ function validateNextRef(next, node, allNodes, errors) {
218
317
  }
219
318
  return;
220
319
  }
221
- // byAnswer object
320
+ // byAnswer object — defensive on shape
321
+ if (!next || typeof next !== 'object' || !next.byAnswer || typeof next.byAnswer !== 'object') {
322
+ errors.push({
323
+ code: 'UNRESOLVED_NEXT',
324
+ message: `Node '${node.id}' next is not a valid NextRef (expected string nodeId or { byAnswer: { ... } })`,
325
+ nodeId: node.id,
326
+ });
327
+ return;
328
+ }
222
329
  for (const [answer, target] of Object.entries(next.byAnswer)) {
223
- if (!(target in allNodes)) {
330
+ if (typeof target !== 'string' || !(target in allNodes)) {
224
331
  errors.push({
225
332
  code: 'UNRESOLVED_NEXT',
226
- message: `Node '${node.id}' next.byAnswer['${answer}'] references '${target}' which is not defined in flow.nodes`,
333
+ message: `Node '${node.id}' next.byAnswer['${answer}'] references '${String(target)}' which is not defined in flow.nodes`,
227
334
  nodeId: node.id,
228
335
  });
229
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.10.11",
3
+ "version": "0.10.12",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",