@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.
@@ -12082,6 +12082,12 @@ ${feature._promptAddition}` : feature._promptAddition;
12082
12082
  }
12083
12083
  // src/definitions/steps/define-code-step.ts
12084
12084
  function codeStep(config2) {
12085
+ const features = config2.features ?? [];
12086
+ let effectiveResult = config2.resultSchema;
12087
+ for (const feature of features) {
12088
+ effectiveResult = effectiveResult ? effectiveResult.extend(feature._resultExtension.shape) : feature._resultExtension;
12089
+ }
12090
+ const featureSignals = features.flatMap((f) => f._signals);
12085
12091
  const spec = {
12086
12092
  key: config2.key,
12087
12093
  name: config2.name,
@@ -12091,14 +12097,23 @@ function codeStep(config2) {
12091
12097
  status: config2.status ?? "active",
12092
12098
  prompt: null,
12093
12099
  inputSchemaJson: config2.inputSchema ? toJSONSchema(config2.inputSchema) : null,
12094
- resultSchemaJson: config2.resultSchema ? toJSONSchema(config2.resultSchema) : null,
12095
- signalExtractorDefinitions: (config2.signals ?? []).map((signal) => ({
12096
- key: signal.key ?? signal.sourcePath,
12097
- sourcePath: signal.sourcePath,
12098
- type: signal.type,
12099
- required: signal.required ?? true,
12100
- availableWhenResultStatusIn: signal.availableWhenResultStatusIn ?? null
12101
- })),
12100
+ resultSchemaJson: effectiveResult ? toJSONSchema(effectiveResult) : null,
12101
+ signalExtractorDefinitions: [
12102
+ ...(config2.signals ?? []).map((signal) => ({
12103
+ key: signal.key ?? signal.sourcePath,
12104
+ sourcePath: signal.sourcePath,
12105
+ type: signal.type,
12106
+ required: signal.required ?? true,
12107
+ availableWhenResultStatusIn: signal.availableWhenResultStatusIn ?? null
12108
+ })),
12109
+ ...featureSignals.map((signal) => ({
12110
+ key: signal.key,
12111
+ sourcePath: signal.sourcePath,
12112
+ type: signal.type,
12113
+ required: signal.required ?? true,
12114
+ availableWhenResultStatusIn: signal.availableWhenResultStatusIn ?? null
12115
+ }))
12116
+ ],
12102
12117
  opencodeMcpJson: null,
12103
12118
  opencodePluginJson: null,
12104
12119
  healthChecksJson: null,
@@ -16084,20 +16099,29 @@ function date4(params) {
16084
16099
  // ../../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
16085
16100
  config(en_default());
16086
16101
  // src/definitions/steps/step-features.ts
16102
+ var notificationKindSchema = exports_external.enum([
16103
+ "feedback_request",
16104
+ "status_update",
16105
+ "blocked",
16106
+ "result_ready",
16107
+ "warning"
16108
+ ]);
16109
+ var notificationPrioritySchema = exports_external.enum(["low", "normal", "high", "urgent"]);
16110
+ var notificationChannelSchema = exports_external.enum([
16111
+ "in_app",
16112
+ "work_item_platform_comment",
16113
+ "email",
16114
+ "slack"
16115
+ ]);
16087
16116
  var NOTIFICATION_SIGNAL_KEY = "$boboddy_notifications_v1";
16088
16117
  var NOTIFICATION_RESULT_KEY = "$boboddy_notifications_v1";
16118
+ var DEFAULT_NOTIFICATION_KIND = "status_update";
16089
16119
  var notificationItemSchema = exports_external.object({
16090
- kind: exports_external.enum([
16091
- "feedback_request",
16092
- "status_update",
16093
- "blocked",
16094
- "result_ready",
16095
- "warning"
16096
- ]).describe("The kind of user notification."),
16120
+ kind: notificationKindSchema.describe("The kind of user notification."),
16097
16121
  title: exports_external.string().describe("Short, human-readable notification title."),
16098
16122
  body: exports_external.string().describe("The notification body / details."),
16099
- priority: exports_external.enum(["low", "normal", "high", "urgent"]).describe("How important this notification is for the user."),
16100
- suggestedChannels: exports_external.array(exports_external.enum(["in_app", "work_item_platform_comment", "email", "slack"])).optional().describe("Channels the agent thinks are worth using. The platform policy decides the final channels."),
16123
+ priority: notificationPrioritySchema.describe("How important this notification is for the user."),
16124
+ suggestedChannels: exports_external.array(notificationChannelSchema).optional().describe("Channels the agent thinks are worth using. The platform policy decides the final channels."),
16101
16125
  payload: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe('Kind-specific structured data. For "feedback_request": { category, urgency, suggestedKey? }.')
16102
16126
  }).describe("A single user notification emitted by the agent.");
16103
16127
  var notificationsFeature = {
@@ -16127,23 +16151,62 @@ var notificationsFeature = {
16127
16151
  }
16128
16152
  ]
16129
16153
  };
16154
+ var feedbackRequestsFeature = {
16155
+ _resultExtension: exports_external.object({
16156
+ [NOTIFICATION_RESULT_KEY]: exports_external.array(notificationItemSchema.extend({ kind: exports_external.literal("feedback_request") })).optional()
16157
+ }),
16158
+ _promptAddition: [
16159
+ "## Feedback Requests",
16160
+ "",
16161
+ `If you need to ask a human a clarifying question, populate the \`${NOTIFICATION_RESULT_KEY}\` array with items of kind \`"feedback_request"\`.`,
16162
+ "Each item must include:",
16163
+ "- **title**: A short, human-readable summary of the question.",
16164
+ "- **body**: The full question.",
16165
+ "- **priority**: One of `low`, `normal`, `high`, `urgent`.",
16166
+ '- **payload**: `{ "category": string, "urgency": "blocking"|"clarification"|"assumption"|"informational", "suggestedKey"?: string }`.'
16167
+ ].join(`
16168
+ `),
16169
+ _signals: notificationsFeature._signals
16170
+ };
16130
16171
  var Features = {
16131
- notifications: Object.assign(() => notificationsFeature, {
16132
- signal: {
16133
- key: NOTIFICATION_SIGNAL_KEY,
16134
- find(signals) {
16135
- const match = signals.find((s) => s.key === NOTIFICATION_SIGNAL_KEY);
16136
- if (!match)
16137
- return;
16138
- const parsed = exports_external.array(notificationItemSchema).safeParse(match.valueJson);
16139
- return parsed.success ? parsed.data : undefined;
16140
- }
16141
- }
16172
+ notifications: () => notificationsFeature,
16173
+ feedbackRequests: () => feedbackRequestsFeature
16174
+ };
16175
+ var NotificationSignal = {
16176
+ key: NOTIFICATION_SIGNAL_KEY,
16177
+ find(signals) {
16178
+ const match = signals.find((s) => s.key === NOTIFICATION_SIGNAL_KEY);
16179
+ if (!match)
16180
+ return;
16181
+ const parsed = exports_external.array(notificationItemSchema).safeParse(match.valueJson);
16182
+ return parsed.success ? parsed.data : undefined;
16183
+ }
16184
+ };
16185
+ var Notify = {
16186
+ create: (item) => ({
16187
+ [NOTIFICATION_RESULT_KEY]: [item]
16142
16188
  }),
16143
- feedbackRequests: Object.assign(() => notificationsFeature, {
16144
- signal: {
16145
- key: NOTIFICATION_SIGNAL_KEY
16189
+ inApp: (title, body, priority, options) => Notify.create({
16190
+ kind: options?.kind ?? DEFAULT_NOTIFICATION_KIND,
16191
+ title,
16192
+ body,
16193
+ priority,
16194
+ suggestedChannels: ["in_app"],
16195
+ ...options?.payload ? { payload: options.payload } : {}
16196
+ }),
16197
+ feedbackRequest: (question, category, urgency, suggestedKey) => Notify.create({
16198
+ kind: "feedback_request",
16199
+ title: question,
16200
+ body: question,
16201
+ priority: "normal",
16202
+ payload: {
16203
+ category,
16204
+ urgency,
16205
+ ...suggestedKey ? { suggestedKey } : {}
16146
16206
  }
16207
+ }),
16208
+ merge: (...fragments) => ({
16209
+ [NOTIFICATION_RESULT_KEY]: fragments.flatMap((fragment) => fragment[NOTIFICATION_RESULT_KEY])
16147
16210
  })
16148
16211
  };
16149
16212
  // src/definitions/advancement-policies/define-advancement-policy.ts
@@ -16840,25 +16903,6 @@ function compileLoopState(stateKey, state, ctx) {
16840
16903
  function compileTerminalState(stateKey, kind) {
16841
16904
  return { nodeDefinitions: [{ nodeKey: stateKey, kind }], edges: [] };
16842
16905
  }
16843
- function assertNoIllegalConvergentEdges(pipelineKey, nodeKindByKey, edges) {
16844
- const incoming = new Map;
16845
- for (const edge of edges) {
16846
- const list = incoming.get(edge.toNodeKey) ?? [];
16847
- list.push(edge);
16848
- incoming.set(edge.toNodeKey, list);
16849
- }
16850
- for (const [targetKey, incomingEdges] of incoming) {
16851
- if (incomingEdges.length <= 1)
16852
- continue;
16853
- const hasInvalidSource = incomingEdges.some((edge) => {
16854
- const kind = nodeKindByKey.get(edge.fromNodeKey);
16855
- return kind !== "choice" && kind !== "loop";
16856
- });
16857
- if (hasInvalidSource) {
16858
- 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).`);
16859
- }
16860
- }
16861
- }
16862
16906
 
16863
16907
  // src/definitions/pipelines/define-pipeline.ts
16864
16908
  function isWorkingNodeDefinition(node) {
@@ -16895,8 +16939,6 @@ function definePipeline(config2) {
16895
16939
  nodeDefinitions.push(...compiled.nodeDefinitions);
16896
16940
  dependencyEdges.push(...compiled.edges);
16897
16941
  }
16898
- const nodeKindByKey = new Map(nodeDefinitions.map((node) => [node.nodeKey, node.kind]));
16899
- assertNoIllegalConvergentEdges(config2.key, nodeKindByKey, dependencyEdges);
16900
16942
  let inputSchemaJson = null;
16901
16943
  if (config2.input) {
16902
16944
  try {
@@ -16912,6 +16954,7 @@ function definePipeline(config2) {
16912
16954
  version: config2.version ?? 1,
16913
16955
  status: config2.status ?? "active",
16914
16956
  inputSchemaJson,
16957
+ entryNodeKey: config2.startAt,
16915
16958
  _stepDefinitions: [...stepDefMap.values()],
16916
16959
  nodeDefinitions,
16917
16960
  dependencyEdges
@@ -17024,6 +17067,7 @@ var buildPipelineDefinitionsClient = (pipelineDefinitions) => {
17024
17067
  description: spec.description,
17025
17068
  status: spec.status,
17026
17069
  inputSchemaJson: spec.inputSchemaJson,
17070
+ entryNodeKey: spec.entryNodeKey,
17027
17071
  nodeDefinitions,
17028
17072
  dependencyEdges
17029
17073
  };
@@ -17375,22 +17419,22 @@ function enumeratePaths(node, root, maxDepth = 3, limit = 40) {
17375
17419
  visit(node, "", 1);
17376
17420
  return [...new Set(out)].sort();
17377
17421
  }
17378
- function resolveSourcePath(schema, sourcePath) {
17422
+ function resolvePathToNode(schema, sourcePath) {
17379
17423
  const segments = parseSourcePath(sourcePath);
17380
17424
  if (segments.length === 0)
17381
- return { kind: "resolved" };
17425
+ return { kind: "resolved", node: schema };
17382
17426
  let candidates = [schema];
17383
17427
  let resolvedPrefix = "";
17384
17428
  for (const segment of segments) {
17385
- const expanded = candidates.flatMap((node) => flatten(node, schema) ?? []);
17429
+ const expanded = candidates.flatMap((node2) => flatten(node2, schema) ?? []);
17386
17430
  if (expanded.length === 0)
17387
17431
  return { kind: "indeterminate" };
17388
- const outcome = combine(expanded.map((node) => stepInto(node, segment)));
17432
+ const outcome = combine(expanded.map((node2) => stepInto(node2, segment)));
17389
17433
  if (outcome.kind === "indeterminate")
17390
17434
  return { kind: "indeterminate" };
17391
17435
  if (outcome.kind === "invalid") {
17392
17436
  const availablePaths = [
17393
- ...new Set(expanded.flatMap((node) => enumeratePaths(node, schema)))
17437
+ ...new Set(expanded.flatMap((node2) => enumeratePaths(node2, schema)))
17394
17438
  ].sort();
17395
17439
  return {
17396
17440
  kind: "invalid",
@@ -17403,7 +17447,60 @@ function resolveSourcePath(schema, sourcePath) {
17403
17447
  candidates = [outcome.node];
17404
17448
  resolvedPrefix = resolvedPrefix ? `${resolvedPrefix}.${segment}` : segment;
17405
17449
  }
17406
- return { kind: "resolved" };
17450
+ const [node] = candidates;
17451
+ return node ? { kind: "resolved", node } : { kind: "indeterminate" };
17452
+ }
17453
+ function resolveSourcePath(schema, sourcePath) {
17454
+ const result = resolvePathToNode(schema, sourcePath);
17455
+ if (result.kind === "resolved")
17456
+ return { kind: "resolved" };
17457
+ if (result.kind === "indeterminate")
17458
+ return { kind: "indeterminate" };
17459
+ return {
17460
+ kind: "invalid",
17461
+ resolvedPrefix: result.resolvedPrefix,
17462
+ segment: result.segment,
17463
+ reason: result.reason,
17464
+ availablePaths: result.availablePaths
17465
+ };
17466
+ }
17467
+ var KNOWN_TYPES = new Set([
17468
+ "string",
17469
+ "number",
17470
+ "boolean",
17471
+ "object",
17472
+ "array",
17473
+ "null"
17474
+ ]);
17475
+ function normalizeTypeName(name) {
17476
+ return name === "integer" ? "number" : name;
17477
+ }
17478
+ function resolveSchemaType(node, root = node) {
17479
+ const branches = flatten(node, root);
17480
+ if (!branches || branches.length === 0)
17481
+ return "unknown";
17482
+ const types = new Set;
17483
+ for (const branch of branches) {
17484
+ const record2 = asRecord(branch);
17485
+ if (!record2)
17486
+ return "unknown";
17487
+ const names = [...typeNames(record2)].map(normalizeTypeName);
17488
+ if (names.length !== 1)
17489
+ return "unknown";
17490
+ const [name] = names;
17491
+ if (!name || !KNOWN_TYPES.has(name))
17492
+ return "unknown";
17493
+ types.add(name);
17494
+ }
17495
+ if (types.size !== 1)
17496
+ return "unknown";
17497
+ return [...types][0];
17498
+ }
17499
+ function resolvePathType(schema, sourcePath) {
17500
+ const result = resolvePathToNode(schema, sourcePath);
17501
+ if (result.kind !== "resolved")
17502
+ return "unknown";
17503
+ return resolveSchemaType(result.node, schema);
17407
17504
  }
17408
17505
  // src/definitions/pipelines/chain-graph.ts
17409
17506
  function tryComputeTopoRanks(nodeDefinitions, dependencyEdges) {
@@ -17456,8 +17553,77 @@ function tryOrderNodeDefinitionsByTopoRank(nodeDefinitions, dependencyEdges) {
17456
17553
  return rankDiff !== 0 ? rankDiff : left.declarationIndex - right.declarationIndex;
17457
17554
  }).map(({ node }) => node);
17458
17555
  }
17556
+ function tryComputeDominators(nodeDefinitions, dependencyEdges, entryNodeKey) {
17557
+ const nodeKeys = new Set(nodeDefinitions.map((node) => node.nodeKey));
17558
+ if (!nodeKeys.has(entryNodeKey))
17559
+ return null;
17560
+ if (tryComputeTopoRanks(nodeDefinitions, dependencyEdges) === null) {
17561
+ return null;
17562
+ }
17563
+ const outgoing = new Map;
17564
+ const incoming = new Map;
17565
+ for (const key of nodeKeys) {
17566
+ outgoing.set(key, []);
17567
+ incoming.set(key, []);
17568
+ }
17569
+ for (const edge of dependencyEdges) {
17570
+ outgoing.get(edge.fromNodeKey)?.push(edge.toNodeKey);
17571
+ incoming.get(edge.toNodeKey)?.push(edge.fromNodeKey);
17572
+ }
17573
+ const reachable = new Set([entryNodeKey]);
17574
+ const queue = [entryNodeKey];
17575
+ while (queue.length > 0) {
17576
+ const current = queue.shift();
17577
+ if (current === undefined)
17578
+ break;
17579
+ for (const next of outgoing.get(current) ?? []) {
17580
+ if (reachable.has(next))
17581
+ continue;
17582
+ reachable.add(next);
17583
+ queue.push(next);
17584
+ }
17585
+ }
17586
+ const dom = new Map;
17587
+ dom.set(entryNodeKey, new Set([entryNodeKey]));
17588
+ for (const key of reachable) {
17589
+ if (key !== entryNodeKey)
17590
+ dom.set(key, new Set(reachable));
17591
+ }
17592
+ let changed = true;
17593
+ while (changed) {
17594
+ changed = false;
17595
+ for (const key of reachable) {
17596
+ if (key === entryNodeKey)
17597
+ continue;
17598
+ let intersection2 = null;
17599
+ for (const predecessor of incoming.get(key) ?? []) {
17600
+ if (!reachable.has(predecessor))
17601
+ continue;
17602
+ const predecessorDom = dom.get(predecessor);
17603
+ if (!predecessorDom)
17604
+ continue;
17605
+ if (intersection2 === null) {
17606
+ intersection2 = new Set(predecessorDom);
17607
+ continue;
17608
+ }
17609
+ for (const candidate of intersection2) {
17610
+ if (!predecessorDom.has(candidate))
17611
+ intersection2.delete(candidate);
17612
+ }
17613
+ }
17614
+ const nextDom = intersection2 ?? new Set;
17615
+ nextDom.add(key);
17616
+ const currentDom = dom.get(key);
17617
+ if (!currentDom || currentDom.size !== nextDom.size || [...currentDom].some((item) => !nextDom.has(item))) {
17618
+ dom.set(key, nextDom);
17619
+ changed = true;
17620
+ }
17621
+ }
17622
+ }
17623
+ return dom;
17624
+ }
17459
17625
 
17460
- // src/definitions/validation/validate-definition-specs.ts
17626
+ // src/definitions/validation/validation-issue.ts
17461
17627
  function listPaths(paths, limit = 24) {
17462
17628
  if (paths.length === 0)
17463
17629
  return "";
@@ -17465,6 +17631,250 @@ function listPaths(paths, limit = 24) {
17465
17631
  return paths.join(", ");
17466
17632
  return `${paths.slice(0, limit).join(", ")}, \u2026 (${String(paths.length - limit)} more)`;
17467
17633
  }
17634
+
17635
+ // src/definitions/validation/validate-input-bindings.ts
17636
+ var WORK_ITEM_TOP_LEVEL_FIELD_SET = new Set(WORK_ITEM_TOP_LEVEL_FIELDS);
17637
+ function isAutoBoundWorkItemField(field) {
17638
+ return field === "workItemTitle" || field === "workItemDescription";
17639
+ }
17640
+ function bindingContexts(pipeline) {
17641
+ const contexts = [];
17642
+ for (const node of pipeline.nodeDefinitions) {
17643
+ if (isWorkingNodeDefinition(node)) {
17644
+ contexts.push({
17645
+ nodeKey: node.nodeKey,
17646
+ branchKey: null,
17647
+ stepKey: node.stepKey,
17648
+ inputBindingsJson: node.inputBindingsJson ?? {}
17649
+ });
17650
+ continue;
17651
+ }
17652
+ if (node.kind === "parallel" && node.branches) {
17653
+ for (const [branchKey, branch] of Object.entries(node.branches)) {
17654
+ contexts.push({
17655
+ nodeKey: node.nodeKey,
17656
+ branchKey,
17657
+ stepKey: branch.stepKey,
17658
+ inputBindingsJson: branch.inputBindingsJson ?? {}
17659
+ });
17660
+ }
17661
+ }
17662
+ }
17663
+ return contexts;
17664
+ }
17665
+ function bindingContextLabel(pipelineKey, ctx) {
17666
+ return ctx.branchKey ? `Pipeline "${pipelineKey}" node "${ctx.nodeKey}" branch "${ctx.branchKey}"` : `Pipeline "${pipelineKey}" node "${ctx.nodeKey}"`;
17667
+ }
17668
+ function knownInputFields(specs) {
17669
+ const fields = new Set;
17670
+ for (const spec of specs) {
17671
+ const schema = spec.inputSchemaJson;
17672
+ if (!schema)
17673
+ continue;
17674
+ const properties = schema["properties"];
17675
+ if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
17676
+ continue;
17677
+ }
17678
+ for (const key of Object.keys(properties))
17679
+ fields.add(key);
17680
+ }
17681
+ return fields;
17682
+ }
17683
+ function requiredInputFields(specs) {
17684
+ const fields = new Set;
17685
+ for (const spec of specs) {
17686
+ const schema = spec.inputSchemaJson;
17687
+ if (!schema)
17688
+ continue;
17689
+ const required2 = schema["required"];
17690
+ if (!Array.isArray(required2))
17691
+ continue;
17692
+ for (const entry of required2) {
17693
+ if (typeof entry === "string")
17694
+ fields.add(entry);
17695
+ }
17696
+ }
17697
+ return fields;
17698
+ }
17699
+ function isJsonSchemaNode(value) {
17700
+ return typeof value === "boolean" || typeof value === "object" && value !== null && !Array.isArray(value);
17701
+ }
17702
+ function findPropertyNode(specs, field) {
17703
+ for (const spec of specs) {
17704
+ const schema = spec.inputSchemaJson;
17705
+ if (!schema)
17706
+ continue;
17707
+ const properties = schema["properties"];
17708
+ if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
17709
+ continue;
17710
+ }
17711
+ const node = properties[field];
17712
+ if (isJsonSchemaNode(node))
17713
+ return { node, root: schema };
17714
+ }
17715
+ return null;
17716
+ }
17717
+ function checkUnboundRequiredInputs(pipelines, stepsByKey) {
17718
+ const issues = [];
17719
+ for (const pipeline of pipelines) {
17720
+ for (const ctx of bindingContexts(pipeline)) {
17721
+ const specs = stepsByKey.get(ctx.stepKey);
17722
+ if (!specs)
17723
+ continue;
17724
+ const required2 = requiredInputFields(specs);
17725
+ if (required2.size === 0)
17726
+ continue;
17727
+ const bound = new Set(Object.keys(ctx.inputBindingsJson));
17728
+ const missing = [...required2].filter((field) => !bound.has(field) && !isAutoBoundWorkItemField(field)).sort();
17729
+ if (missing.length === 0)
17730
+ continue;
17731
+ const where = bindingContextLabel(pipeline.key, ctx);
17732
+ const boundList = [
17733
+ ...bound,
17734
+ "workItemTitle",
17735
+ "workItemDescription"
17736
+ ].sort();
17737
+ for (const field of missing) {
17738
+ issues.push({
17739
+ check: "unbound-required-input",
17740
+ severity: "error",
17741
+ pipelineKey: pipeline.key,
17742
+ nodeKey: ctx.nodeKey,
17743
+ branchKey: ctx.branchKey ?? undefined,
17744
+ message: `${where} runs step "${ctx.stepKey}", which requires input "${field}", ` + `but no binding provides it. Bound inputs: ${listPaths(boundList)}.`
17745
+ });
17746
+ }
17747
+ }
17748
+ }
17749
+ return issues;
17750
+ }
17751
+ function checkBindingTargetFields(pipelines, stepsByKey) {
17752
+ const issues = [];
17753
+ for (const pipeline of pipelines) {
17754
+ for (const ctx of bindingContexts(pipeline)) {
17755
+ const specs = stepsByKey.get(ctx.stepKey);
17756
+ const knownFields = specs ? knownInputFields(specs) : null;
17757
+ const where = bindingContextLabel(pipeline.key, ctx);
17758
+ for (const [field, binding] of Object.entries(ctx.inputBindingsJson)) {
17759
+ if (knownFields && !isAutoBoundWorkItemField(field) && !knownFields.has(field)) {
17760
+ issues.push({
17761
+ check: "binding-target-field",
17762
+ severity: "info",
17763
+ pipelineKey: pipeline.key,
17764
+ nodeKey: ctx.nodeKey,
17765
+ branchKey: ctx.branchKey ?? undefined,
17766
+ 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)"}.`
17767
+ });
17768
+ }
17769
+ if (binding.source === "work_item" && !binding.field.startsWith(WORK_ITEM_FIELDS_PATH_PREFIX) && !WORK_ITEM_TOP_LEVEL_FIELD_SET.has(binding.field)) {
17770
+ issues.push({
17771
+ check: "binding-target-field",
17772
+ severity: "error",
17773
+ pipelineKey: pipeline.key,
17774
+ nodeKey: ctx.nodeKey,
17775
+ branchKey: ctx.branchKey ?? undefined,
17776
+ 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)}.`
17777
+ });
17778
+ }
17779
+ }
17780
+ }
17781
+ }
17782
+ return issues;
17783
+ }
17784
+ function describeBindingSource(binding) {
17785
+ switch (binding.source) {
17786
+ case "step_signal":
17787
+ return `signal "${binding.signalKey}" of node "${binding.stepKey}"`;
17788
+ case "step_output":
17789
+ return `the output of node "${binding.stepKey}"`;
17790
+ case "signals_list":
17791
+ return `the signals list of fan-out node "${binding.stepKey}"`;
17792
+ case "pipeline_input":
17793
+ return `pipeline input "${binding.path}"`;
17794
+ case "work_item":
17795
+ return `work_item field "${binding.field}"`;
17796
+ case "literal":
17797
+ return "a literal value";
17798
+ case "fan_out_item":
17799
+ return "the fan-out item";
17800
+ }
17801
+ }
17802
+ function bindingSourceType(binding, pipeline, nodeByKey, stepsByKey) {
17803
+ if (binding.source === "step_signal") {
17804
+ const producerNode = nodeByKey.get(binding.stepKey);
17805
+ if (!producerNode || !isWorkingNodeDefinition(producerNode))
17806
+ return "unknown";
17807
+ const specs = stepsByKey.get(producerNode.stepKey) ?? [];
17808
+ for (const spec of specs) {
17809
+ const signal2 = spec.signalExtractorDefinitions.find((candidate) => candidate.key === binding.signalKey);
17810
+ if (signal2)
17811
+ return signal2.type;
17812
+ }
17813
+ return "unknown";
17814
+ }
17815
+ if (binding.source === "step_output") {
17816
+ const producerNode = nodeByKey.get(binding.stepKey);
17817
+ if (!producerNode || !isWorkingNodeDefinition(producerNode))
17818
+ return "unknown";
17819
+ const specs = stepsByKey.get(producerNode.stepKey) ?? [];
17820
+ for (const spec of specs) {
17821
+ if (spec.resultSchemaJson)
17822
+ return resolveSchemaType(spec.resultSchemaJson);
17823
+ }
17824
+ return "unknown";
17825
+ }
17826
+ if (binding.source === "signals_list")
17827
+ return "array";
17828
+ if (binding.source === "pipeline_input") {
17829
+ return pipeline.inputSchemaJson ? resolvePathType(pipeline.inputSchemaJson, binding.path) : "unknown";
17830
+ }
17831
+ if (binding.source === "work_item") {
17832
+ if (binding.field.startsWith(WORK_ITEM_FIELDS_PATH_PREFIX))
17833
+ return "unknown";
17834
+ return WORK_ITEM_TOP_LEVEL_FIELD_SET.has(binding.field) ? "string" : "unknown";
17835
+ }
17836
+ return "unknown";
17837
+ }
17838
+ function checkBindingTypeCompatibility(pipelines, stepsByKey) {
17839
+ const issues = [];
17840
+ for (const pipeline of pipelines) {
17841
+ const nodeByKey = new Map(pipeline.nodeDefinitions.map((node) => [node.nodeKey, node]));
17842
+ for (const ctx of bindingContexts(pipeline)) {
17843
+ const specs = stepsByKey.get(ctx.stepKey);
17844
+ const where = bindingContextLabel(pipeline.key, ctx);
17845
+ for (const [field, binding] of Object.entries(ctx.inputBindingsJson)) {
17846
+ if (binding.source === "fan_out_item")
17847
+ continue;
17848
+ if (isAutoBoundWorkItemField(field))
17849
+ continue;
17850
+ if (!specs)
17851
+ continue;
17852
+ const target = findPropertyNode(specs, field);
17853
+ if (!target)
17854
+ continue;
17855
+ const targetType = resolveSchemaType(target.node, target.root);
17856
+ if (targetType === "unknown")
17857
+ continue;
17858
+ const sourceType = bindingSourceType(binding, pipeline, nodeByKey, stepsByKey);
17859
+ if (sourceType === "unknown")
17860
+ continue;
17861
+ if (sourceType === targetType)
17862
+ continue;
17863
+ issues.push({
17864
+ check: "binding-type-mismatch",
17865
+ severity: "warning",
17866
+ pipelineKey: pipeline.key,
17867
+ nodeKey: ctx.nodeKey,
17868
+ branchKey: ctx.branchKey ?? undefined,
17869
+ message: `${where} binds input "${field}" (declared type "${targetType}") to ` + `${describeBindingSource(binding)}, which resolves to type ` + `"${sourceType}" \u2014 the types disagree.`
17870
+ });
17871
+ }
17872
+ }
17873
+ }
17874
+ return issues;
17875
+ }
17876
+
17877
+ // src/definitions/validation/validate-definition-specs.ts
17468
17878
  function quotedOrRoot(prefix) {
17469
17879
  return prefix ? `"${prefix}"` : "the result root";
17470
17880
  }
@@ -17483,6 +17893,7 @@ function checkSignalSourcePaths(steps) {
17483
17893
  const suffix = availablePaths.length > 0 ? ` Valid sourcePaths ${resolvedPrefix ? `under "${resolvedPrefix}"` : "for this step"}: ${listPaths(availablePaths)}.` : "";
17484
17894
  issues.push({
17485
17895
  check: "signal-source-path",
17896
+ severity: "error",
17486
17897
  message: `Step "${step.key}" declares signal "${signal2.key}" with sourcePath ` + `"${signal2.sourcePath}", which can never resolve against the step's ` + `result schema: ${cause}.${suffix}`
17487
17898
  });
17488
17899
  }
@@ -17505,6 +17916,7 @@ function checkHealthChecks(steps) {
17505
17916
  if (!mcpServerKeySet.has(check2.mcp)) {
17506
17917
  issues.push({
17507
17918
  check: "health-check-mcp-server",
17919
+ severity: "error",
17508
17920
  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)"}.`
17509
17921
  });
17510
17922
  }
@@ -17512,6 +17924,7 @@ function checkHealthChecks(steps) {
17512
17924
  if (check2.tool.startsWith(prefix)) {
17513
17925
  issues.push({
17514
17926
  check: "health-check-double-qualified",
17927
+ severity: "error",
17515
17928
  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)}"?`
17516
17929
  });
17517
17930
  }
@@ -17548,6 +17961,7 @@ function checkRouteTargets(pipelines, knownPipelineKeys) {
17548
17961
  continue;
17549
17962
  issues.push({
17550
17963
  check: "route-target",
17964
+ severity: "error",
17551
17965
  pipelineKey: pipeline.key,
17552
17966
  nodeKey: node.nodeKey,
17553
17967
  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.`
@@ -17603,6 +18017,7 @@ function checkSignalBindings(pipelines, stepsByKey) {
17603
18017
  const order = [...pipeline.nodeDefinitions.keys()].sort((left, right) => (ranks.get(left) ?? 0) - (ranks.get(right) ?? 0)).map((index) => pipeline.nodeDefinitions[index]?.nodeKey ?? "");
17604
18018
  const orderHint = `Nodes in "${pipeline.key}", in order: ${order.join(" \u2192 ")}.`;
17605
18019
  const nodeByKey = new Map(pipeline.nodeDefinitions.map((node) => [node.nodeKey, node]));
18020
+ const dominatorSets = tryComputeDominators(pipeline.nodeDefinitions, pipeline.dependencyEdges, pipeline.entryNodeKey);
17606
18021
  pipeline.nodeDefinitions.forEach((node, index) => {
17607
18022
  const consumerRank = ranks.get(index) ?? index;
17608
18023
  const where = `Pipeline "${pipeline.key}" node "${node.nodeKey}"`;
@@ -17613,6 +18028,7 @@ function checkSignalBindings(pipelines, stepsByKey) {
17613
18028
  continue;
17614
18029
  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}"`;
17615
18030
  const issueBase = {
18031
+ severity: "error",
17616
18032
  pipelineKey: pipeline.key,
17617
18033
  nodeKey: node.nodeKey,
17618
18034
  targetNodeKey: source.nodeKey
@@ -17627,11 +18043,12 @@ function checkSignalBindings(pipelines, stepsByKey) {
17627
18043
  });
17628
18044
  continue;
17629
18045
  }
17630
- if (producerRank >= consumerRank) {
18046
+ const producesBeforeConsumer = dominatorSets !== null ? source.nodeKey !== node.nodeKey && (dominatorSets.get(node.nodeKey)?.has(source.nodeKey) ?? false) : producerRank < consumerRank;
18047
+ if (!producesBeforeConsumer) {
17631
18048
  issues.push({
17632
18049
  check: "signal-binding",
17633
18050
  ...issueBase,
17634
- message: `${where} ${what}, but that node does not run before it, so the ` + `value will never exist. ${orderHint}`
18051
+ message: `${where} ${what}, but that node does not run on every path ` + `leading to this node, so the value may not exist. ${orderHint}`
17635
18052
  });
17636
18053
  continue;
17637
18054
  }
@@ -17663,11 +18080,14 @@ function validateDefinitionSpecs(specs, options = {}) {
17663
18080
  ...checkSignalSourcePaths(specs.steps),
17664
18081
  ...checkHealthChecks(specs.steps),
17665
18082
  ...checkRouteTargets(specs.pipelines, options.knownPipelineKeys ?? []),
17666
- ...checkSignalBindings(specs.pipelines, stepsByKey)
18083
+ ...checkSignalBindings(specs.pipelines, stepsByKey),
18084
+ ...checkUnboundRequiredInputs(specs.pipelines, stepsByKey),
18085
+ ...checkBindingTargetFields(specs.pipelines, stepsByKey),
18086
+ ...checkBindingTypeCompatibility(specs.pipelines, stepsByKey)
17667
18087
  ];
17668
18088
  }
17669
18089
  function assertValidDefinitionSpecs(specs, options = {}) {
17670
- const issues = validateDefinitionSpecs(specs, options);
18090
+ const issues = validateDefinitionSpecs(specs, options).filter((issue2) => issue2.severity === "error");
17671
18091
  if (issues.length === 0)
17672
18092
  return;
17673
18093
  const header = issues.length === 1 ? "Definition validation failed:" : `Definition validation failed with ${String(issues.length)} problems:`;