@boboddy/sdk 0.5.1 → 0.5.3
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.
- package/dist/definitions/pipelines/chain-graph.d.ts +29 -0
- package/dist/definitions/pipelines/compile-node-definitions.d.ts +0 -8
- package/dist/definitions/pipelines/define-pipeline.d.ts +2 -0
- package/dist/definitions/pipelines/index.js +2 -21
- package/dist/definitions/steps/define-code-step.d.ts +17 -5
- package/dist/definitions/steps/define-step.d.ts +1 -1
- package/dist/definitions/steps/index.js +97 -32
- package/dist/definitions/steps/step-features.d.ts +92 -31
- package/dist/definitions/validation/index.d.ts +2 -0
- package/dist/definitions/validation/index.js +394 -32
- package/dist/definitions/validation/json-schema-paths.d.ts +29 -0
- package/dist/definitions/validation/validate-definition-specs.d.ts +12 -23
- package/dist/definitions/validation/validate-input-bindings.d.ts +7 -0
- package/dist/definitions/validation/validation-issue.d.ts +52 -0
- package/dist/generated/types.gen.d.ts +2 -0
- package/dist/index.js +99 -53
- package/dist/push/index.js +484 -64
- package/package.json +1 -1
package/dist/push/index.js
CHANGED
|
@@ -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:
|
|
12095
|
-
signalExtractorDefinitions:
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
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,
|
|
@@ -13716,7 +13731,7 @@ var buildStepDefinitionsClient = (stepDefinitions) => {
|
|
|
13716
13731
|
key: spec.key,
|
|
13717
13732
|
name: spec.name,
|
|
13718
13733
|
description: spec.description,
|
|
13719
|
-
prompt: spec.prompt ??
|
|
13734
|
+
prompt: spec.prompt ?? null,
|
|
13720
13735
|
version: spec.version,
|
|
13721
13736
|
kind: spec.kind,
|
|
13722
13737
|
entrypointJson: spec.entrypointJson ?? 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:
|
|
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:
|
|
16100
|
-
suggestedChannels: exports_external.array(
|
|
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:
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
|
|
16139
|
-
|
|
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
|
-
|
|
16144
|
-
|
|
16145
|
-
|
|
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
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
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:`;
|