@boboddy/sdk 0.5.1 → 0.5.2

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.
@@ -207,22 +207,22 @@ function enumeratePaths(node, root, maxDepth = 3, limit = 40) {
207
207
  visit(node, "", 1);
208
208
  return [...new Set(out)].sort();
209
209
  }
210
- function resolveSourcePath(schema, sourcePath) {
210
+ function resolvePathToNode(schema, sourcePath) {
211
211
  const segments = parseSourcePath(sourcePath);
212
212
  if (segments.length === 0)
213
- return { kind: "resolved" };
213
+ return { kind: "resolved", node: schema };
214
214
  let candidates = [schema];
215
215
  let resolvedPrefix = "";
216
216
  for (const segment of segments) {
217
- const expanded = candidates.flatMap((node) => flatten(node, schema) ?? []);
217
+ const expanded = candidates.flatMap((node2) => flatten(node2, schema) ?? []);
218
218
  if (expanded.length === 0)
219
219
  return { kind: "indeterminate" };
220
- const outcome = combine(expanded.map((node) => stepInto(node, segment)));
220
+ const outcome = combine(expanded.map((node2) => stepInto(node2, segment)));
221
221
  if (outcome.kind === "indeterminate")
222
222
  return { kind: "indeterminate" };
223
223
  if (outcome.kind === "invalid") {
224
224
  const availablePaths = [
225
- ...new Set(expanded.flatMap((node) => enumeratePaths(node, schema)))
225
+ ...new Set(expanded.flatMap((node2) => enumeratePaths(node2, schema)))
226
226
  ].sort();
227
227
  return {
228
228
  kind: "invalid",
@@ -235,7 +235,60 @@ function resolveSourcePath(schema, sourcePath) {
235
235
  candidates = [outcome.node];
236
236
  resolvedPrefix = resolvedPrefix ? `${resolvedPrefix}.${segment}` : segment;
237
237
  }
238
- return { kind: "resolved" };
238
+ const [node] = candidates;
239
+ return node ? { kind: "resolved", node } : { kind: "indeterminate" };
240
+ }
241
+ function resolveSourcePath(schema, sourcePath) {
242
+ const result = resolvePathToNode(schema, sourcePath);
243
+ if (result.kind === "resolved")
244
+ return { kind: "resolved" };
245
+ if (result.kind === "indeterminate")
246
+ return { kind: "indeterminate" };
247
+ return {
248
+ kind: "invalid",
249
+ resolvedPrefix: result.resolvedPrefix,
250
+ segment: result.segment,
251
+ reason: result.reason,
252
+ availablePaths: result.availablePaths
253
+ };
254
+ }
255
+ var KNOWN_TYPES = new Set([
256
+ "string",
257
+ "number",
258
+ "boolean",
259
+ "object",
260
+ "array",
261
+ "null"
262
+ ]);
263
+ function normalizeTypeName(name) {
264
+ return name === "integer" ? "number" : name;
265
+ }
266
+ function resolveSchemaType(node, root = node) {
267
+ const branches = flatten(node, root);
268
+ if (!branches || branches.length === 0)
269
+ return "unknown";
270
+ const types = new Set;
271
+ for (const branch of branches) {
272
+ const record = asRecord(branch);
273
+ if (!record)
274
+ return "unknown";
275
+ const names = [...typeNames(record)].map(normalizeTypeName);
276
+ if (names.length !== 1)
277
+ return "unknown";
278
+ const [name] = names;
279
+ if (!name || !KNOWN_TYPES.has(name))
280
+ return "unknown";
281
+ types.add(name);
282
+ }
283
+ if (types.size !== 1)
284
+ return "unknown";
285
+ return [...types][0];
286
+ }
287
+ function resolvePathType(schema, sourcePath) {
288
+ const result = resolvePathToNode(schema, sourcePath);
289
+ if (result.kind !== "resolved")
290
+ return "unknown";
291
+ return resolveSchemaType(result.node, schema);
239
292
  }
240
293
  // src/definitions/pipelines/chain-graph.ts
241
294
  function tryComputeTopoRanks(nodeDefinitions, dependencyEdges) {
@@ -288,6 +341,75 @@ function tryOrderNodeDefinitionsByTopoRank(nodeDefinitions, dependencyEdges) {
288
341
  return rankDiff !== 0 ? rankDiff : left.declarationIndex - right.declarationIndex;
289
342
  }).map(({ node }) => node);
290
343
  }
344
+ function tryComputeDominators(nodeDefinitions, dependencyEdges, entryNodeKey) {
345
+ const nodeKeys = new Set(nodeDefinitions.map((node) => node.nodeKey));
346
+ if (!nodeKeys.has(entryNodeKey))
347
+ return null;
348
+ if (tryComputeTopoRanks(nodeDefinitions, dependencyEdges) === null) {
349
+ return null;
350
+ }
351
+ const outgoing = new Map;
352
+ const incoming = new Map;
353
+ for (const key of nodeKeys) {
354
+ outgoing.set(key, []);
355
+ incoming.set(key, []);
356
+ }
357
+ for (const edge of dependencyEdges) {
358
+ outgoing.get(edge.fromNodeKey)?.push(edge.toNodeKey);
359
+ incoming.get(edge.toNodeKey)?.push(edge.fromNodeKey);
360
+ }
361
+ const reachable = new Set([entryNodeKey]);
362
+ const queue = [entryNodeKey];
363
+ while (queue.length > 0) {
364
+ const current = queue.shift();
365
+ if (current === undefined)
366
+ break;
367
+ for (const next of outgoing.get(current) ?? []) {
368
+ if (reachable.has(next))
369
+ continue;
370
+ reachable.add(next);
371
+ queue.push(next);
372
+ }
373
+ }
374
+ const dom = new Map;
375
+ dom.set(entryNodeKey, new Set([entryNodeKey]));
376
+ for (const key of reachable) {
377
+ if (key !== entryNodeKey)
378
+ dom.set(key, new Set(reachable));
379
+ }
380
+ let changed = true;
381
+ while (changed) {
382
+ changed = false;
383
+ for (const key of reachable) {
384
+ if (key === entryNodeKey)
385
+ continue;
386
+ let intersection = null;
387
+ for (const predecessor of incoming.get(key) ?? []) {
388
+ if (!reachable.has(predecessor))
389
+ continue;
390
+ const predecessorDom = dom.get(predecessor);
391
+ if (!predecessorDom)
392
+ continue;
393
+ if (intersection === null) {
394
+ intersection = new Set(predecessorDom);
395
+ continue;
396
+ }
397
+ for (const candidate of intersection) {
398
+ if (!predecessorDom.has(candidate))
399
+ intersection.delete(candidate);
400
+ }
401
+ }
402
+ const nextDom = intersection ?? new Set;
403
+ nextDom.add(key);
404
+ const currentDom = dom.get(key);
405
+ if (!currentDom || currentDom.size !== nextDom.size || [...currentDom].some((item) => !nextDom.has(item))) {
406
+ dom.set(key, nextDom);
407
+ changed = true;
408
+ }
409
+ }
410
+ }
411
+ return dom;
412
+ }
291
413
 
292
414
  // ../../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
293
415
  var exports_external = {};
@@ -15259,25 +15381,6 @@ function compileLoopState(stateKey, state, ctx) {
15259
15381
  function compileTerminalState(stateKey, kind) {
15260
15382
  return { nodeDefinitions: [{ nodeKey: stateKey, kind }], edges: [] };
15261
15383
  }
15262
- function assertNoIllegalConvergentEdges(pipelineKey, nodeKindByKey, edges) {
15263
- const incoming = new Map;
15264
- for (const edge of edges) {
15265
- const list = incoming.get(edge.toNodeKey) ?? [];
15266
- list.push(edge);
15267
- incoming.set(edge.toNodeKey, list);
15268
- }
15269
- for (const [targetKey, incomingEdges] of incoming) {
15270
- if (incomingEdges.length <= 1)
15271
- continue;
15272
- const hasInvalidSource = incomingEdges.some((edge) => {
15273
- const kind = nodeKindByKey.get(edge.fromNodeKey);
15274
- return kind !== "choice" && kind !== "loop";
15275
- });
15276
- if (hasInvalidSource) {
15277
- throw new Error(`Pipeline "${pipelineKey}": state "${targetKey}" has more than one incoming edge, but not every source is a 'choice'/'loop' state (unconditional convergent edges are not allowed \u2014 see docs/research/flat-pipeline-sdk-and-visual-designer.md \xA76).`);
15278
- }
15279
- }
15280
- }
15281
15384
 
15282
15385
  // src/definitions/pipelines/define-pipeline.ts
15283
15386
  function isWorkingNodeDefinition(node) {
@@ -15314,8 +15417,6 @@ function definePipeline(config2) {
15314
15417
  nodeDefinitions.push(...compiled.nodeDefinitions);
15315
15418
  dependencyEdges.push(...compiled.edges);
15316
15419
  }
15317
- const nodeKindByKey = new Map(nodeDefinitions.map((node) => [node.nodeKey, node.kind]));
15318
- assertNoIllegalConvergentEdges(config2.key, nodeKindByKey, dependencyEdges);
15319
15420
  let inputSchemaJson = null;
15320
15421
  if (config2.input) {
15321
15422
  try {
@@ -15331,13 +15432,14 @@ function definePipeline(config2) {
15331
15432
  version: config2.version ?? 1,
15332
15433
  status: config2.status ?? "active",
15333
15434
  inputSchemaJson,
15435
+ entryNodeKey: config2.startAt,
15334
15436
  _stepDefinitions: [...stepDefMap.values()],
15335
15437
  nodeDefinitions,
15336
15438
  dependencyEdges
15337
15439
  };
15338
15440
  }
15339
15441
 
15340
- // src/definitions/validation/validate-definition-specs.ts
15442
+ // src/definitions/validation/validation-issue.ts
15341
15443
  function listPaths(paths, limit = 24) {
15342
15444
  if (paths.length === 0)
15343
15445
  return "";
@@ -15345,6 +15447,250 @@ function listPaths(paths, limit = 24) {
15345
15447
  return paths.join(", ");
15346
15448
  return `${paths.slice(0, limit).join(", ")}, \u2026 (${String(paths.length - limit)} more)`;
15347
15449
  }
15450
+
15451
+ // src/definitions/validation/validate-input-bindings.ts
15452
+ var WORK_ITEM_TOP_LEVEL_FIELD_SET = new Set(WORK_ITEM_TOP_LEVEL_FIELDS);
15453
+ function isAutoBoundWorkItemField(field) {
15454
+ return field === "workItemTitle" || field === "workItemDescription";
15455
+ }
15456
+ function bindingContexts(pipeline) {
15457
+ const contexts = [];
15458
+ for (const node of pipeline.nodeDefinitions) {
15459
+ if (isWorkingNodeDefinition(node)) {
15460
+ contexts.push({
15461
+ nodeKey: node.nodeKey,
15462
+ branchKey: null,
15463
+ stepKey: node.stepKey,
15464
+ inputBindingsJson: node.inputBindingsJson ?? {}
15465
+ });
15466
+ continue;
15467
+ }
15468
+ if (node.kind === "parallel" && node.branches) {
15469
+ for (const [branchKey, branch] of Object.entries(node.branches)) {
15470
+ contexts.push({
15471
+ nodeKey: node.nodeKey,
15472
+ branchKey,
15473
+ stepKey: branch.stepKey,
15474
+ inputBindingsJson: branch.inputBindingsJson ?? {}
15475
+ });
15476
+ }
15477
+ }
15478
+ }
15479
+ return contexts;
15480
+ }
15481
+ function bindingContextLabel(pipelineKey, ctx) {
15482
+ return ctx.branchKey ? `Pipeline "${pipelineKey}" node "${ctx.nodeKey}" branch "${ctx.branchKey}"` : `Pipeline "${pipelineKey}" node "${ctx.nodeKey}"`;
15483
+ }
15484
+ function knownInputFields(specs) {
15485
+ const fields = new Set;
15486
+ for (const spec of specs) {
15487
+ const schema = spec.inputSchemaJson;
15488
+ if (!schema)
15489
+ continue;
15490
+ const properties = schema["properties"];
15491
+ if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
15492
+ continue;
15493
+ }
15494
+ for (const key of Object.keys(properties))
15495
+ fields.add(key);
15496
+ }
15497
+ return fields;
15498
+ }
15499
+ function requiredInputFields(specs) {
15500
+ const fields = new Set;
15501
+ for (const spec of specs) {
15502
+ const schema = spec.inputSchemaJson;
15503
+ if (!schema)
15504
+ continue;
15505
+ const required2 = schema["required"];
15506
+ if (!Array.isArray(required2))
15507
+ continue;
15508
+ for (const entry of required2) {
15509
+ if (typeof entry === "string")
15510
+ fields.add(entry);
15511
+ }
15512
+ }
15513
+ return fields;
15514
+ }
15515
+ function isJsonSchemaNode(value) {
15516
+ return typeof value === "boolean" || typeof value === "object" && value !== null && !Array.isArray(value);
15517
+ }
15518
+ function findPropertyNode(specs, field) {
15519
+ for (const spec of specs) {
15520
+ const schema = spec.inputSchemaJson;
15521
+ if (!schema)
15522
+ continue;
15523
+ const properties = schema["properties"];
15524
+ if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
15525
+ continue;
15526
+ }
15527
+ const node = properties[field];
15528
+ if (isJsonSchemaNode(node))
15529
+ return { node, root: schema };
15530
+ }
15531
+ return null;
15532
+ }
15533
+ function checkUnboundRequiredInputs(pipelines, stepsByKey) {
15534
+ const issues = [];
15535
+ for (const pipeline of pipelines) {
15536
+ for (const ctx of bindingContexts(pipeline)) {
15537
+ const specs = stepsByKey.get(ctx.stepKey);
15538
+ if (!specs)
15539
+ continue;
15540
+ const required2 = requiredInputFields(specs);
15541
+ if (required2.size === 0)
15542
+ continue;
15543
+ const bound = new Set(Object.keys(ctx.inputBindingsJson));
15544
+ const missing = [...required2].filter((field) => !bound.has(field) && !isAutoBoundWorkItemField(field)).sort();
15545
+ if (missing.length === 0)
15546
+ continue;
15547
+ const where = bindingContextLabel(pipeline.key, ctx);
15548
+ const boundList = [
15549
+ ...bound,
15550
+ "workItemTitle",
15551
+ "workItemDescription"
15552
+ ].sort();
15553
+ for (const field of missing) {
15554
+ issues.push({
15555
+ check: "unbound-required-input",
15556
+ severity: "error",
15557
+ pipelineKey: pipeline.key,
15558
+ nodeKey: ctx.nodeKey,
15559
+ branchKey: ctx.branchKey ?? undefined,
15560
+ message: `${where} runs step "${ctx.stepKey}", which requires input "${field}", ` + `but no binding provides it. Bound inputs: ${listPaths(boundList)}.`
15561
+ });
15562
+ }
15563
+ }
15564
+ }
15565
+ return issues;
15566
+ }
15567
+ function checkBindingTargetFields(pipelines, stepsByKey) {
15568
+ const issues = [];
15569
+ for (const pipeline of pipelines) {
15570
+ for (const ctx of bindingContexts(pipeline)) {
15571
+ const specs = stepsByKey.get(ctx.stepKey);
15572
+ const knownFields = specs ? knownInputFields(specs) : null;
15573
+ const where = bindingContextLabel(pipeline.key, ctx);
15574
+ for (const [field, binding] of Object.entries(ctx.inputBindingsJson)) {
15575
+ if (knownFields && !isAutoBoundWorkItemField(field) && !knownFields.has(field)) {
15576
+ issues.push({
15577
+ check: "binding-target-field",
15578
+ severity: "info",
15579
+ pipelineKey: pipeline.key,
15580
+ nodeKey: ctx.nodeKey,
15581
+ branchKey: ctx.branchKey ?? undefined,
15582
+ message: `${where} is passing information ("${field}") to step "${ctx.stepKey}" ` + `that it isn't explicitly asking for \u2014 the step declares no such ` + `additionalInput field, so the value is dropped. Declared fields: ` + `${knownFields.size > 0 ? listPaths([...knownFields].sort()) : "(none)"}.`
15583
+ });
15584
+ }
15585
+ if (binding.source === "work_item" && !binding.field.startsWith(WORK_ITEM_FIELDS_PATH_PREFIX) && !WORK_ITEM_TOP_LEVEL_FIELD_SET.has(binding.field)) {
15586
+ issues.push({
15587
+ check: "binding-target-field",
15588
+ severity: "error",
15589
+ pipelineKey: pipeline.key,
15590
+ nodeKey: ctx.nodeKey,
15591
+ branchKey: ctx.branchKey ?? undefined,
15592
+ message: `${where} binds input "${field}" to work_item field "${binding.field}", ` + `which is not a known top-level work-item field and does not start ` + `with "${WORK_ITEM_FIELDS_PATH_PREFIX}". Known top-level fields: ` + `${listPaths(WORK_ITEM_TOP_LEVEL_FIELDS)}.`
15593
+ });
15594
+ }
15595
+ }
15596
+ }
15597
+ }
15598
+ return issues;
15599
+ }
15600
+ function describeBindingSource(binding) {
15601
+ switch (binding.source) {
15602
+ case "step_signal":
15603
+ return `signal "${binding.signalKey}" of node "${binding.stepKey}"`;
15604
+ case "step_output":
15605
+ return `the output of node "${binding.stepKey}"`;
15606
+ case "signals_list":
15607
+ return `the signals list of fan-out node "${binding.stepKey}"`;
15608
+ case "pipeline_input":
15609
+ return `pipeline input "${binding.path}"`;
15610
+ case "work_item":
15611
+ return `work_item field "${binding.field}"`;
15612
+ case "literal":
15613
+ return "a literal value";
15614
+ case "fan_out_item":
15615
+ return "the fan-out item";
15616
+ }
15617
+ }
15618
+ function bindingSourceType(binding, pipeline, nodeByKey, stepsByKey) {
15619
+ if (binding.source === "step_signal") {
15620
+ const producerNode = nodeByKey.get(binding.stepKey);
15621
+ if (!producerNode || !isWorkingNodeDefinition(producerNode))
15622
+ return "unknown";
15623
+ const specs = stepsByKey.get(producerNode.stepKey) ?? [];
15624
+ for (const spec of specs) {
15625
+ const signal2 = spec.signalExtractorDefinitions.find((candidate) => candidate.key === binding.signalKey);
15626
+ if (signal2)
15627
+ return signal2.type;
15628
+ }
15629
+ return "unknown";
15630
+ }
15631
+ if (binding.source === "step_output") {
15632
+ const producerNode = nodeByKey.get(binding.stepKey);
15633
+ if (!producerNode || !isWorkingNodeDefinition(producerNode))
15634
+ return "unknown";
15635
+ const specs = stepsByKey.get(producerNode.stepKey) ?? [];
15636
+ for (const spec of specs) {
15637
+ if (spec.resultSchemaJson)
15638
+ return resolveSchemaType(spec.resultSchemaJson);
15639
+ }
15640
+ return "unknown";
15641
+ }
15642
+ if (binding.source === "signals_list")
15643
+ return "array";
15644
+ if (binding.source === "pipeline_input") {
15645
+ return pipeline.inputSchemaJson ? resolvePathType(pipeline.inputSchemaJson, binding.path) : "unknown";
15646
+ }
15647
+ if (binding.source === "work_item") {
15648
+ if (binding.field.startsWith(WORK_ITEM_FIELDS_PATH_PREFIX))
15649
+ return "unknown";
15650
+ return WORK_ITEM_TOP_LEVEL_FIELD_SET.has(binding.field) ? "string" : "unknown";
15651
+ }
15652
+ return "unknown";
15653
+ }
15654
+ function checkBindingTypeCompatibility(pipelines, stepsByKey) {
15655
+ const issues = [];
15656
+ for (const pipeline of pipelines) {
15657
+ const nodeByKey = new Map(pipeline.nodeDefinitions.map((node) => [node.nodeKey, node]));
15658
+ for (const ctx of bindingContexts(pipeline)) {
15659
+ const specs = stepsByKey.get(ctx.stepKey);
15660
+ const where = bindingContextLabel(pipeline.key, ctx);
15661
+ for (const [field, binding] of Object.entries(ctx.inputBindingsJson)) {
15662
+ if (binding.source === "fan_out_item")
15663
+ continue;
15664
+ if (isAutoBoundWorkItemField(field))
15665
+ continue;
15666
+ if (!specs)
15667
+ continue;
15668
+ const target = findPropertyNode(specs, field);
15669
+ if (!target)
15670
+ continue;
15671
+ const targetType = resolveSchemaType(target.node, target.root);
15672
+ if (targetType === "unknown")
15673
+ continue;
15674
+ const sourceType = bindingSourceType(binding, pipeline, nodeByKey, stepsByKey);
15675
+ if (sourceType === "unknown")
15676
+ continue;
15677
+ if (sourceType === targetType)
15678
+ continue;
15679
+ issues.push({
15680
+ check: "binding-type-mismatch",
15681
+ severity: "warning",
15682
+ pipelineKey: pipeline.key,
15683
+ nodeKey: ctx.nodeKey,
15684
+ branchKey: ctx.branchKey ?? undefined,
15685
+ message: `${where} binds input "${field}" (declared type "${targetType}") to ` + `${describeBindingSource(binding)}, which resolves to type ` + `"${sourceType}" \u2014 the types disagree.`
15686
+ });
15687
+ }
15688
+ }
15689
+ }
15690
+ return issues;
15691
+ }
15692
+
15693
+ // src/definitions/validation/validate-definition-specs.ts
15348
15694
  function quotedOrRoot(prefix) {
15349
15695
  return prefix ? `"${prefix}"` : "the result root";
15350
15696
  }
@@ -15363,6 +15709,7 @@ function checkSignalSourcePaths(steps) {
15363
15709
  const suffix = availablePaths.length > 0 ? ` Valid sourcePaths ${resolvedPrefix ? `under "${resolvedPrefix}"` : "for this step"}: ${listPaths(availablePaths)}.` : "";
15364
15710
  issues.push({
15365
15711
  check: "signal-source-path",
15712
+ severity: "error",
15366
15713
  message: `Step "${step.key}" declares signal "${signal2.key}" with sourcePath ` + `"${signal2.sourcePath}", which can never resolve against the step's ` + `result schema: ${cause}.${suffix}`
15367
15714
  });
15368
15715
  }
@@ -15385,6 +15732,7 @@ function checkHealthChecks(steps) {
15385
15732
  if (!mcpServerKeySet.has(check2.mcp)) {
15386
15733
  issues.push({
15387
15734
  check: "health-check-mcp-server",
15735
+ severity: "error",
15388
15736
  message: `${where} names MCP server "${check2.mcp}", but the step declares no ` + `such server in mcpServers. Declared servers: ${mcpServerKeys.length > 0 ? listPaths([...mcpServerKeys].sort()) : "(none)"}.`
15389
15737
  });
15390
15738
  }
@@ -15392,6 +15740,7 @@ function checkHealthChecks(steps) {
15392
15740
  if (check2.tool.startsWith(prefix)) {
15393
15741
  issues.push({
15394
15742
  check: "health-check-double-qualified",
15743
+ severity: "error",
15395
15744
  message: `${where} sets mcp "${check2.mcp}" and tool "${check2.tool}", which already ` + `starts with "${prefix}". When "mcp" is set, "tool" should be the bare tool ` + `name \u2014 OpenCode resolves it to "${prefix}${check2.tool}". Did you mean ` + `tool: "${check2.tool.slice(prefix.length)}"?`
15396
15745
  });
15397
15746
  }
@@ -15428,6 +15777,7 @@ function checkRouteTargets(pipelines, knownPipelineKeys) {
15428
15777
  continue;
15429
15778
  issues.push({
15430
15779
  check: "route-target",
15780
+ severity: "error",
15431
15781
  pipelineKey: pipeline.key,
15432
15782
  nodeKey: node.nodeKey,
15433
15783
  message: `Pipeline "${pipeline.key}" step "${stepLabel}" routes to pipeline ` + `"${target}", but no pipeline with that key was found on the server or ` + `in the current push batch. Push the target pipeline first.`
@@ -15483,6 +15833,7 @@ function checkSignalBindings(pipelines, stepsByKey) {
15483
15833
  const order = [...pipeline.nodeDefinitions.keys()].sort((left, right) => (ranks.get(left) ?? 0) - (ranks.get(right) ?? 0)).map((index) => pipeline.nodeDefinitions[index]?.nodeKey ?? "");
15484
15834
  const orderHint = `Nodes in "${pipeline.key}", in order: ${order.join(" \u2192 ")}.`;
15485
15835
  const nodeByKey = new Map(pipeline.nodeDefinitions.map((node) => [node.nodeKey, node]));
15836
+ const dominatorSets = tryComputeDominators(pipeline.nodeDefinitions, pipeline.dependencyEdges, pipeline.entryNodeKey);
15486
15837
  pipeline.nodeDefinitions.forEach((node, index) => {
15487
15838
  const consumerRank = ranks.get(index) ?? index;
15488
15839
  const where = `Pipeline "${pipeline.key}" node "${node.nodeKey}"`;
@@ -15493,6 +15844,7 @@ function checkSignalBindings(pipelines, stepsByKey) {
15493
15844
  continue;
15494
15845
  const what = source.kind === "signal" ? `binds input "${field}" to signal "${source.signalKey ?? ""}" of node "${source.nodeKey}"` : source.kind === "signals_list" ? `binds input "${field}" to the signals list of fan-out node "${source.nodeKey}"` : `binds input "${field}" to the output of node "${source.nodeKey}"`;
15495
15846
  const issueBase = {
15847
+ severity: "error",
15496
15848
  pipelineKey: pipeline.key,
15497
15849
  nodeKey: node.nodeKey,
15498
15850
  targetNodeKey: source.nodeKey
@@ -15507,11 +15859,12 @@ function checkSignalBindings(pipelines, stepsByKey) {
15507
15859
  });
15508
15860
  continue;
15509
15861
  }
15510
- if (producerRank >= consumerRank) {
15862
+ const producesBeforeConsumer = dominatorSets !== null ? source.nodeKey !== node.nodeKey && (dominatorSets.get(node.nodeKey)?.has(source.nodeKey) ?? false) : producerRank < consumerRank;
15863
+ if (!producesBeforeConsumer) {
15511
15864
  issues.push({
15512
15865
  check: "signal-binding",
15513
15866
  ...issueBase,
15514
- message: `${where} ${what}, but that node does not run before it, so the ` + `value will never exist. ${orderHint}`
15867
+ message: `${where} ${what}, but that node does not run on every path ` + `leading to this node, so the value may not exist. ${orderHint}`
15515
15868
  });
15516
15869
  continue;
15517
15870
  }
@@ -15543,11 +15896,14 @@ function validateDefinitionSpecs(specs, options = {}) {
15543
15896
  ...checkSignalSourcePaths(specs.steps),
15544
15897
  ...checkHealthChecks(specs.steps),
15545
15898
  ...checkRouteTargets(specs.pipelines, options.knownPipelineKeys ?? []),
15546
- ...checkSignalBindings(specs.pipelines, stepsByKey)
15899
+ ...checkSignalBindings(specs.pipelines, stepsByKey),
15900
+ ...checkUnboundRequiredInputs(specs.pipelines, stepsByKey),
15901
+ ...checkBindingTargetFields(specs.pipelines, stepsByKey),
15902
+ ...checkBindingTypeCompatibility(specs.pipelines, stepsByKey)
15547
15903
  ];
15548
15904
  }
15549
15905
  function assertValidDefinitionSpecs(specs, options = {}) {
15550
- const issues = validateDefinitionSpecs(specs, options);
15906
+ const issues = validateDefinitionSpecs(specs, options).filter((issue2) => issue2.severity === "error");
15551
15907
  if (issues.length === 0)
15552
15908
  return;
15553
15909
  const header = issues.length === 1 ? "Definition validation failed:" : `Definition validation failed with ${String(issues.length)} problems:`;
@@ -15556,8 +15912,14 @@ function assertValidDefinitionSpecs(specs, options = {}) {
15556
15912
  }
15557
15913
  export {
15558
15914
  assertValidDefinitionSpecs,
15915
+ checkBindingTargetFields,
15916
+ checkBindingTypeCompatibility,
15917
+ checkUnboundRequiredInputs,
15559
15918
  enumeratePaths,
15919
+ listPaths,
15560
15920
  parseSourcePath,
15921
+ resolvePathType,
15922
+ resolveSchemaType,
15561
15923
  resolveSourcePath,
15562
15924
  validateDefinitionSpecs
15563
15925
  };
@@ -43,3 +43,32 @@ export declare function enumeratePaths(node: JsonSchemaNode, root: JsonSchemaNod
43
43
  * `$ref`s are resolved against the root.
44
44
  */
45
45
  export declare function resolveSourcePath(schema: JsonSchemaNode, sourcePath: string): PathResolution;
46
+ /**
47
+ * The type vocabulary a schema node is reduced to. `"integer"` (a JSON
48
+ * Schema refinement Zod emits for `z.number().int()`) collapses into
49
+ * `"number"`; anything the schema doesn't pin to exactly one of these is
50
+ * `"unknown"`.
51
+ */
52
+ export type SchemaType = "string" | "number" | "boolean" | "object" | "array" | "null" | "unknown";
53
+ /**
54
+ * Reduces a JSON Schema node to its top-level type, per `SchemaType`.
55
+ *
56
+ * Follows the same `$ref` / `anyOf` / `oneOf` / `allOf` flattening
57
+ * `resolveSourcePath` uses, so a `$ref` is resolved against `root` (default:
58
+ * `node` itself — pass the document root explicitly when `node` came from
59
+ * deeper inside it). Returns `"unknown"` — never a guess — when: the schema
60
+ * is absent (`{}`, e.g. `z.unknown()`) or a boolean schema (`true` / `false`);
61
+ * a `$ref` fails to resolve; or the branches disagree on type (a union of
62
+ * a string and a number, say). A false "this is definitely a string" is
63
+ * worse than an honest "unknown" here, same bias as path resolution.
64
+ */
65
+ export declare function resolveSchemaType(node: JsonSchemaNode, root?: JsonSchemaNode): SchemaType;
66
+ /**
67
+ * The type `sourcePath` resolves to within `schema` — `"unknown"` whenever
68
+ * the path doesn't provably resolve to a single node (indeterminate or
69
+ * invalid) or the node it resolves to doesn't pin down a single type.
70
+ *
71
+ * Pass the step's `resultSchemaJson` as `schema`; `$ref`s are resolved
72
+ * against it as the document root, matching `resolveSourcePath`.
73
+ */
74
+ export declare function resolvePathType(schema: JsonSchemaNode, sourcePath: string): SchemaType;
@@ -1,5 +1,6 @@
1
1
  import { type PipelineDefinitionSpec } from "../pipelines/define-pipeline";
2
2
  import type { StepDefinitionSpec } from "../steps/define-step";
3
+ import { type DefinitionValidationIssue } from "./validation-issue";
3
4
  export type DefinitionSpecSet = {
4
5
  readonly pipelines: readonly PipelineDefinitionSpec[];
5
6
  readonly steps: readonly StepDefinitionSpec[];
@@ -12,32 +13,20 @@ export type ValidateDefinitionSpecsOptions = {
12
13
  */
13
14
  readonly knownPipelineKeys?: readonly string[];
14
15
  };
15
- export type DefinitionValidationIssue = {
16
- readonly check: "signal-source-path" | "route-target" | "signal-binding" | "health-check-mcp-server" | "health-check-double-qualified";
17
- readonly message: string;
18
- /**
19
- * The pipeline this issue belongs to, when the check is pipeline-scoped
20
- * (`route-target`/`signal-binding`) — absent for step-only checks
21
- * (`signal-source-path`/`health-check-*`), which have no pipeline
22
- * context of their own. Required before the designer (Phase 5) can
23
- * attach an error to the right graph node.
24
- */
25
- readonly pipelineKey?: string;
26
- /** The node this issue is about, when pipeline-scoped. */
27
- readonly nodeKey?: string;
28
- /**
29
- * A second, related node this issue is about — e.g. `signal-binding`'s
30
- * producer node, when different from `nodeKey`'s consumer. Absent when
31
- * the issue is about a single node, or when the "other end" isn't a
32
- * node in this pipeline at all (`route-target`'s target is a different
33
- * *pipeline*, not a node).
34
- */
35
- readonly targetNodeKey?: string;
36
- };
37
16
  /**
38
17
  * Runs every offline check over a batch of definitions and returns the issues
39
18
  * found, in check order. An empty array means the batch is clean.
40
19
  */
41
20
  export declare function validateDefinitionSpecs(specs: DefinitionSpecSet, options?: ValidateDefinitionSpecsOptions): DefinitionValidationIssue[];
42
- /** `validateDefinitionSpecs`, but throws a single aggregated error. */
21
+ /**
22
+ * `validateDefinitionSpecs`, but throws a single aggregated error.
23
+ *
24
+ * Only counts and lists `severity: "error"` issues — `"warning"`-tier issues
25
+ * (`binding-type-mismatch`) and `"info"`-tier issues (`binding-target-field`'s
26
+ * unbound-field-name sub-check) never block a push and are silently dropped
27
+ * from both the header count and the body here. They're still present in
28
+ * `validateDefinitionSpecs`'s own return value for any caller that wants to
29
+ * surface them (e.g. the designer, in a later phase); this is the one entry
30
+ * point whose whole job is "should this push be blocked."
31
+ */
43
32
  export declare function assertValidDefinitionSpecs(specs: DefinitionSpecSet, options?: ValidateDefinitionSpecsOptions): void;
@@ -0,0 +1,7 @@
1
+ import { type PipelineDefinitionSpec } from "../pipelines/define-pipeline";
2
+ import type { StepDefinitionSpec } from "../steps/define-step";
3
+ import { type DefinitionValidationIssue } from "./validation-issue";
4
+ declare function checkUnboundRequiredInputs(pipelines: readonly PipelineDefinitionSpec[], stepsByKey: Map<string, readonly StepDefinitionSpec[]>): DefinitionValidationIssue[];
5
+ declare function checkBindingTargetFields(pipelines: readonly PipelineDefinitionSpec[], stepsByKey: Map<string, readonly StepDefinitionSpec[]>): DefinitionValidationIssue[];
6
+ declare function checkBindingTypeCompatibility(pipelines: readonly PipelineDefinitionSpec[], stepsByKey: Map<string, readonly StepDefinitionSpec[]>): DefinitionValidationIssue[];
7
+ export { checkBindingTargetFields, checkBindingTypeCompatibility, checkUnboundRequiredInputs, };