@atom-workflow-agent/workflow-planner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +39 -0
- package/README.zh-CN.md +39 -0
- package/dist/capabilities/capability-coverage-resolver.d.ts +5 -0
- package/dist/capabilities/capability-coverage-resolver.d.ts.map +1 -0
- package/dist/capabilities/capability-coverage-resolver.js +229 -0
- package/dist/capabilities/capability-coverage-resolver.js.map +1 -0
- package/dist/catalog/planner-catalog.d.ts +14 -0
- package/dist/catalog/planner-catalog.d.ts.map +1 -0
- package/dist/catalog/planner-catalog.js +47 -0
- package/dist/catalog/planner-catalog.js.map +1 -0
- package/dist/draft/planner-draft-normalizer.d.ts +20 -0
- package/dist/draft/planner-draft-normalizer.d.ts.map +1 -0
- package/dist/draft/planner-draft-normalizer.js +534 -0
- package/dist/draft/planner-draft-normalizer.js.map +1 -0
- package/dist/evaluation/planning-quality.d.ts +80 -0
- package/dist/evaluation/planning-quality.d.ts.map +1 -0
- package/dist/evaluation/planning-quality.js +120 -0
- package/dist/evaluation/planning-quality.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/model/planning-model-port.d.ts +41 -0
- package/dist/model/planning-model-port.d.ts.map +1 -0
- package/dist/model/planning-model-port.js +16 -0
- package/dist/model/planning-model-port.js.map +1 -0
- package/dist/planning/planning-controller.d.ts +77 -0
- package/dist/planning/planning-controller.d.ts.map +1 -0
- package/dist/planning/planning-controller.js +773 -0
- package/dist/planning/planning-controller.js.map +1 -0
- package/dist/store/local-plan-store.d.ts +57 -0
- package/dist/store/local-plan-store.d.ts.map +1 -0
- package/dist/store/local-plan-store.js +190 -0
- package/dist/store/local-plan-store.js.map +1 -0
- package/dist/store/plan-storage-layout.d.ts +14 -0
- package/dist/store/plan-storage-layout.d.ts.map +1 -0
- package/dist/store/plan-storage-layout.js +25 -0
- package/dist/store/plan-storage-layout.js.map +1 -0
- package/dist/validation/planning-draft-validator.d.ts +20 -0
- package/dist/validation/planning-draft-validator.d.ts.map +1 -0
- package/dist/validation/planning-draft-validator.js +76 -0
- package/dist/validation/planning-draft-validator.js.map +1 -0
- package/dist/workspace/map-aggregate-schema.d.ts +8 -0
- package/dist/workspace/map-aggregate-schema.d.ts.map +1 -0
- package/dist/workspace/map-aggregate-schema.js +20 -0
- package/dist/workspace/map-aggregate-schema.js.map +1 -0
- package/dist/workspace/planning-frontier.d.ts +15 -0
- package/dist/workspace/planning-frontier.d.ts.map +1 -0
- package/dist/workspace/planning-frontier.js +185 -0
- package/dist/workspace/planning-frontier.js.map +1 -0
- package/dist/workspace/planning-workspace-service.d.ts +72 -0
- package/dist/workspace/planning-workspace-service.d.ts.map +1 -0
- package/dist/workspace/planning-workspace-service.js +528 -0
- package/dist/workspace/planning-workspace-service.js.map +1 -0
- package/dist/workspace/semantic-plan-compiler.d.ts +19 -0
- package/dist/workspace/semantic-plan-compiler.d.ts.map +1 -0
- package/dist/workspace/semantic-plan-compiler.js +643 -0
- package/dist/workspace/semantic-plan-compiler.js.map +1 -0
- package/dist/workspace/strict-binding-validator.d.ts +9 -0
- package/dist/workspace/strict-binding-validator.d.ts.map +1 -0
- package/dist/workspace/strict-binding-validator.js +232 -0
- package/dist/workspace/strict-binding-validator.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import {} from "@atom-workflow-agent/contracts";
|
|
2
|
+
import { requiredPropertiesForValue, schemaPathGuaranteed, schemasCompatible } from "@atom-workflow-agent/workflow-compiler";
|
|
3
|
+
import { mapAggregateSchema } from "./map-aggregate-schema.js";
|
|
4
|
+
function objectOf(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
6
|
+
}
|
|
7
|
+
function schemaType(schema) {
|
|
8
|
+
const object = objectOf(schema);
|
|
9
|
+
if (!object)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (typeof object.type === "string")
|
|
12
|
+
return object.type;
|
|
13
|
+
if (Array.isArray(object.enum) && object.enum.length > 0) {
|
|
14
|
+
const values = new Set(object.enum.map((value) => value === null ? "null" : typeof value));
|
|
15
|
+
return values.size === 1 ? [...values][0] : undefined;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
function inferSchema(value) {
|
|
20
|
+
if (value === null)
|
|
21
|
+
return { type: "null" };
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return { type: "array", items: value.length ? inferSchema(value[0]) : {} };
|
|
24
|
+
if (typeof value === "object")
|
|
25
|
+
return {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: Object.fromEntries(Object.entries(value).map(([name, nested]) => [name, inferSchema(nested)])),
|
|
28
|
+
required: Object.keys(value),
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
};
|
|
31
|
+
return { type: Number.isInteger(value) ? "integer" : typeof value };
|
|
32
|
+
}
|
|
33
|
+
function schemaAtPath(schema, path) {
|
|
34
|
+
if (path === "." || path === "")
|
|
35
|
+
return schema;
|
|
36
|
+
let current = schema;
|
|
37
|
+
for (const segment of path.split(".").filter(Boolean)) {
|
|
38
|
+
const object = objectOf(current);
|
|
39
|
+
current = segment === "[]" ? object?.items : objectOf(object?.properties)?.[segment];
|
|
40
|
+
if (current === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
function descriptionOf(schema, fallback) {
|
|
46
|
+
const description = objectOf(schema)?.description;
|
|
47
|
+
return typeof description === "string" && description.trim() ? description.trim() : fallback;
|
|
48
|
+
}
|
|
49
|
+
function schemaChildren(selectorBase, refBase, schema, order, prefix = "", parentGuaranteed = true) {
|
|
50
|
+
const root = selectorBase.source === "node" && prefix === "" ? [{
|
|
51
|
+
selector: { source: "node", nodeId: selectorBase.nodeId, path: "." },
|
|
52
|
+
name: selectorBase.nodeId,
|
|
53
|
+
schema,
|
|
54
|
+
description: descriptionOf(schema, `Atom 节点 ${selectorBase.nodeId} 的完整结构化输出。`),
|
|
55
|
+
ref: { $output: selectorBase.nodeId },
|
|
56
|
+
sourceRef: selectorBase.nodeId,
|
|
57
|
+
order,
|
|
58
|
+
guaranteed: parentGuaranteed,
|
|
59
|
+
}] : [];
|
|
60
|
+
const schemaObject = objectOf(schema);
|
|
61
|
+
const properties = objectOf(schemaObject?.properties);
|
|
62
|
+
if (!properties)
|
|
63
|
+
return root;
|
|
64
|
+
const required = new Set(Array.isArray(schemaObject?.required)
|
|
65
|
+
? schemaObject.required.filter((name) => typeof name === "string")
|
|
66
|
+
: []);
|
|
67
|
+
return [...root, ...Object.entries(properties).flatMap(([name, nested]) => {
|
|
68
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
69
|
+
const child = nested;
|
|
70
|
+
const sourceRef = selectorBase.source === "node" ? `${selectorBase.nodeId}.${path}` : undefined;
|
|
71
|
+
const guaranteed = parentGuaranteed && required.has(name);
|
|
72
|
+
const current = {
|
|
73
|
+
selector: { ...selectorBase, path },
|
|
74
|
+
name,
|
|
75
|
+
schema: child,
|
|
76
|
+
description: descriptionOf(child, path),
|
|
77
|
+
ref: selectorBase.source === "workflow" ? { $input: path } : { $output: sourceRef },
|
|
78
|
+
...(sourceRef ? { sourceRef } : {}),
|
|
79
|
+
order,
|
|
80
|
+
guaranteed,
|
|
81
|
+
};
|
|
82
|
+
return [current, ...schemaChildren(selectorBase, refBase, child, order, path, guaranteed)];
|
|
83
|
+
})];
|
|
84
|
+
}
|
|
85
|
+
function terms(value) {
|
|
86
|
+
const normalized = value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().replace(/[“”'",。;:、()()_.-]/g, " ");
|
|
87
|
+
const words = normalized.match(/[a-z0-9]+|[\u3400-\u9fff]/g) ?? [];
|
|
88
|
+
const joinedChinese = words.filter((word) => /^[\u3400-\u9fff]$/.test(word)).join("");
|
|
89
|
+
const bigrams = [...joinedChinese].slice(0, -1).map((char, index) => `${char}${[...joinedChinese][index + 1]}`);
|
|
90
|
+
return new Set([...words.filter((word) => word.length > 1 || /^[a-z0-9]+$/.test(word)), ...bigrams]);
|
|
91
|
+
}
|
|
92
|
+
function semanticScore(source, targetName, targetDescription) {
|
|
93
|
+
if (source.name === targetName)
|
|
94
|
+
return 1;
|
|
95
|
+
const left = terms(`${source.name} ${source.description}`);
|
|
96
|
+
const right = terms(`${targetName} ${targetDescription}`);
|
|
97
|
+
if (!left.size || !right.size)
|
|
98
|
+
return 0;
|
|
99
|
+
const overlap = [...left].filter((term) => right.has(term)).length;
|
|
100
|
+
return overlap / Math.min(left.size, right.size);
|
|
101
|
+
}
|
|
102
|
+
const MIN_SEMANTIC_SCORE = 0.34;
|
|
103
|
+
const MIN_SEMANTIC_MARGIN = 0.12;
|
|
104
|
+
/**
|
|
105
|
+
* Select a source without allowing registration order to decide business
|
|
106
|
+
* meaning. In a serial pipeline, one exact-name value on the latest control
|
|
107
|
+
* layer shadows older values; same-layer exact-name values (for example from
|
|
108
|
+
* parallel branches) remain ambiguous. Cross-name binding is allowed only when
|
|
109
|
+
* descriptions and schemas identify one source; equal descriptions or
|
|
110
|
+
* near-equal semantic scores remain an explicit Planner choice.
|
|
111
|
+
*/
|
|
112
|
+
function selectBindingSource(sources, targetName, targetSchema, targetDescription) {
|
|
113
|
+
const compatible = sources
|
|
114
|
+
.filter((source) => source.guaranteed)
|
|
115
|
+
.filter((source) => schemasCompatible(source.schema, targetSchema, source.schema, targetSchema))
|
|
116
|
+
.map((source) => ({
|
|
117
|
+
source,
|
|
118
|
+
score: semanticScore(source, targetName, targetDescription),
|
|
119
|
+
exactName: source.name === targetName,
|
|
120
|
+
exactDescription: source.description.trim() === targetDescription.trim(),
|
|
121
|
+
}));
|
|
122
|
+
const exactNames = compatible.filter(({ exactName }) => exactName);
|
|
123
|
+
if (exactNames.length > 0) {
|
|
124
|
+
const latestOrder = Math.max(...exactNames.map(({ source }) => source.order));
|
|
125
|
+
const latestExactNames = exactNames.filter(({ source }) => source.order === latestOrder);
|
|
126
|
+
const fieldExactNames = latestExactNames.filter(({ source }) => source.selector.source !== "value" && source.selector.path !== ".");
|
|
127
|
+
const mostSpecificExactNames = fieldExactNames.length > 0 ? fieldExactNames : latestExactNames;
|
|
128
|
+
if (mostSpecificExactNames.length === 1)
|
|
129
|
+
return { kind: "selected", candidate: mostSpecificExactNames[0] };
|
|
130
|
+
return { kind: "ambiguous", candidates: mostSpecificExactNames };
|
|
131
|
+
}
|
|
132
|
+
const exactDescriptions = compatible.filter(({ exactDescription }) => exactDescription);
|
|
133
|
+
if (exactDescriptions.length === 1)
|
|
134
|
+
return { kind: "selected", candidate: exactDescriptions[0] };
|
|
135
|
+
if (exactDescriptions.length > 1)
|
|
136
|
+
return { kind: "ambiguous", candidates: exactDescriptions };
|
|
137
|
+
const semantic = compatible
|
|
138
|
+
.filter(({ score }) => score >= MIN_SEMANTIC_SCORE)
|
|
139
|
+
.sort((left, right) => right.score - left.score);
|
|
140
|
+
const best = semantic[0];
|
|
141
|
+
if (!best)
|
|
142
|
+
return { kind: "none", candidates: [] };
|
|
143
|
+
const second = semantic[1];
|
|
144
|
+
if (second && best.score - second.score < MIN_SEMANTIC_MARGIN) {
|
|
145
|
+
return { kind: "ambiguous", candidates: semantic };
|
|
146
|
+
}
|
|
147
|
+
return { kind: "selected", candidate: best };
|
|
148
|
+
}
|
|
149
|
+
function candidateDetails(candidates) {
|
|
150
|
+
return candidates.slice(0, 5).map(({ source, score, exactName, exactDescription }) => ({
|
|
151
|
+
source: source.selector,
|
|
152
|
+
description: source.description,
|
|
153
|
+
schema: source.schema,
|
|
154
|
+
score,
|
|
155
|
+
exactName,
|
|
156
|
+
exactDescription,
|
|
157
|
+
guaranteed: source.guaranteed,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
function setPath(target, path, value) {
|
|
161
|
+
const segments = path.split(".").filter(Boolean);
|
|
162
|
+
let current = target;
|
|
163
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
164
|
+
const name = segments[index];
|
|
165
|
+
const existing = current[name];
|
|
166
|
+
if (existing === null || typeof existing !== "object" || Array.isArray(existing))
|
|
167
|
+
current[name] = {};
|
|
168
|
+
current = current[name];
|
|
169
|
+
}
|
|
170
|
+
current[segments.at(-1)] = value;
|
|
171
|
+
}
|
|
172
|
+
function cloneState(state) {
|
|
173
|
+
return { sources: [...state.sources], aliases: new Map(state.aliases), ...(state.item ? { item: state.item } : {}), ...(state.mapGroup ? { mapGroup: state.mapGroup } : {}) };
|
|
174
|
+
}
|
|
175
|
+
function nextControlOrder(state) {
|
|
176
|
+
return Math.max(-1, ...state.sources.map(({ order }) => order)) + 1;
|
|
177
|
+
}
|
|
178
|
+
class Compiler {
|
|
179
|
+
workflowName;
|
|
180
|
+
workflowInputs;
|
|
181
|
+
selectedAtomKeys;
|
|
182
|
+
descriptors;
|
|
183
|
+
errors = [];
|
|
184
|
+
evidence = new Map();
|
|
185
|
+
constructor(registry, workflowName, workflowInputs, selectedAtomKeys) {
|
|
186
|
+
this.workflowName = workflowName;
|
|
187
|
+
this.workflowInputs = workflowInputs;
|
|
188
|
+
this.selectedAtomKeys = selectedAtomKeys;
|
|
189
|
+
this.descriptors = new Map(registry.list().map((entry) => [entry.plannerDescriptor.key, entry.plannerDescriptor]));
|
|
190
|
+
}
|
|
191
|
+
compile(blocks, outputs) {
|
|
192
|
+
let state = this.initialState();
|
|
193
|
+
const steps = [];
|
|
194
|
+
for (const [index, block] of blocks.entries()) {
|
|
195
|
+
const compiled = this.step(block.plan, state, `$.blocks[${index}].plan`);
|
|
196
|
+
if (compiled) {
|
|
197
|
+
steps.push(compiled.step);
|
|
198
|
+
state = compiled.state;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const resolvedOutputs = {};
|
|
202
|
+
for (const [name, source] of Object.entries(outputs)) {
|
|
203
|
+
const resolved = this.resolveSource(source, state, `$.outputs.${name}`);
|
|
204
|
+
if (resolved)
|
|
205
|
+
resolvedOutputs[name] = resolved.ref;
|
|
206
|
+
}
|
|
207
|
+
if (this.errors.length || steps.length === 0) {
|
|
208
|
+
if (steps.length === 0)
|
|
209
|
+
this.errors.push({ code: "PLANNING_DRAFT_EMPTY", stage: "draft-schema", path: "$.blocks", message: "Semantic plan has no control region", hint: "Add at least one add-region operation" });
|
|
210
|
+
return { success: false, errors: this.errors };
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
success: true,
|
|
214
|
+
plannerDraft: {
|
|
215
|
+
schemaVersion: "1.0", kind: "workflow-draft", name: this.workflowName,
|
|
216
|
+
flow: steps.length === 1 ? steps[0] : { kind: "sequence", steps }, outputs: resolvedOutputs,
|
|
217
|
+
},
|
|
218
|
+
evidence: [...this.evidence.values()],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
initialState() {
|
|
222
|
+
const rootSchema = {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: Object.fromEntries(Object.entries(this.workflowInputs).map(([name, value]) => [name, { ...objectOf(inferSchema(value)), description: `用户提供的 Workflow 输入字段 ${name}。` }])),
|
|
225
|
+
required: Object.keys(this.workflowInputs),
|
|
226
|
+
};
|
|
227
|
+
return { sources: schemaChildren({ source: "workflow" }, "$input", rootSchema, 0), aliases: new Map() };
|
|
228
|
+
}
|
|
229
|
+
step(plan, before, path) {
|
|
230
|
+
switch (plan.kind) {
|
|
231
|
+
case "atom": return this.atom(plan, before, path);
|
|
232
|
+
case "sequence": {
|
|
233
|
+
let state = cloneState(before);
|
|
234
|
+
const steps = [];
|
|
235
|
+
plan.steps.forEach((nested, index) => {
|
|
236
|
+
const compiled = this.step(nested, state, `${path}.steps[${index}]`);
|
|
237
|
+
if (compiled) {
|
|
238
|
+
steps.push(compiled.step);
|
|
239
|
+
state = compiled.state;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return steps.length ? { step: steps.length === 1 ? steps[0] : { kind: "sequence", steps }, state } : undefined;
|
|
243
|
+
}
|
|
244
|
+
case "parallel": {
|
|
245
|
+
const branches = plan.branches.map((branch, index) => this.step(branch, cloneState(before), `${path}.branches[${index}]`)).filter((value) => Boolean(value));
|
|
246
|
+
const state = cloneState(before);
|
|
247
|
+
for (const branch of branches)
|
|
248
|
+
for (const source of branch.state.sources)
|
|
249
|
+
if (!state.sources.some((item) => JSON.stringify(item.selector) === JSON.stringify(source.selector)))
|
|
250
|
+
state.sources.push(source);
|
|
251
|
+
for (const branch of branches)
|
|
252
|
+
for (const [alias, atom] of branch.state.aliases)
|
|
253
|
+
state.aliases.set(alias, atom);
|
|
254
|
+
return branches.length ? { step: { kind: "parallel", branches: branches.map(({ step }) => step) }, state } : undefined;
|
|
255
|
+
}
|
|
256
|
+
case "decision": return this.decision(plan, before, path);
|
|
257
|
+
case "map": return this.map(plan, before, path);
|
|
258
|
+
case "repeat": return this.repeat(plan, before, path);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
atom(plan, before, path) {
|
|
262
|
+
const descriptor = this.descriptors.get(plan.atom);
|
|
263
|
+
if (!descriptor || !this.selectedAtomKeys.has(plan.atom)) {
|
|
264
|
+
this.errors.push({ code: "PLANNER_DRAFT_ATOM_NOT_SELECTED", stage: "atom-resolution", path: `${path}.atom`, message: `Atom is not in the selected Catalog Authority: ${plan.atom}`, received: plan.atom, hint: "Use an exact selected Atom key or report a missing capability" });
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
if (before.aliases.has(plan.id)) {
|
|
268
|
+
this.errors.push({ code: "PLANNER_DRAFT_ALIAS_DUPLICATE", stage: "draft-normalization", path: `${path}.id`, message: `Node id is already used: ${plan.id}`, hint: "Use one globally unique node id" });
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
const input = this.bindInput(plan.id, descriptor.inputSchema, plan.bindings ?? {}, before, `${path}.bindings`);
|
|
272
|
+
const step = { kind: "atom", as: plan.id, use: plan.atom, input, ...(plan.policy ? { policy: plan.policy } : {}) };
|
|
273
|
+
return { step, state: this.afterAtom(before, plan.id, plan.atom, descriptor.outputSchema) };
|
|
274
|
+
}
|
|
275
|
+
decision(plan, before, path) {
|
|
276
|
+
const atom = this.atom({ kind: "atom", id: plan.id, atom: plan.atom, ...(plan.bindings ? { bindings: plan.bindings } : {}), ...(plan.policy ? { policy: plan.policy } : {}) }, before, path);
|
|
277
|
+
if (!atom)
|
|
278
|
+
return undefined;
|
|
279
|
+
const descriptor = this.descriptors.get(plan.atom);
|
|
280
|
+
const branchSchema = schemaAtPath(descriptor.outputSchema, plan.select);
|
|
281
|
+
if (!branchSchema) {
|
|
282
|
+
this.errors.push({ code: "PLANNER_DRAFT_CONTROL_INVALID", stage: "control-graph", path: `${path}.select`, message: `Decision select does not exist in Atom output: ${plan.select}`, received: plan.select, hint: "Choose an exact relative output field path from the selected Atom descriptor" });
|
|
283
|
+
}
|
|
284
|
+
else if (schemaType(branchSchema) !== "string") {
|
|
285
|
+
this.errors.push({ code: "DECISION_SELECT_TYPE_MISMATCH", stage: "control-graph", path: `${path}.select`, message: "Decision select must resolve to a string output field", expected: "string", received: branchSchema, hint: "Choose a string output field whose values identify the business cases" });
|
|
286
|
+
}
|
|
287
|
+
const expected = Array.isArray(objectOf(branchSchema)?.enum) ? objectOf(branchSchema).enum : undefined;
|
|
288
|
+
const ids = Object.keys(plan.branches);
|
|
289
|
+
if (expected && (expected.some((id) => typeof id !== "string" || !ids.includes(id)) || ids.some((id) => !expected.includes(id)))) {
|
|
290
|
+
this.errors.push({ code: "DECISION_BRANCH_ENUM_MISMATCH", stage: "control-graph", path: `${path}.branches`, message: `Decision branches must exactly match the enum at output.${plan.select}`, expected: expected, received: ids, hint: "Declare one branch for every finite value of the selected output field" });
|
|
291
|
+
}
|
|
292
|
+
const branchesHaveNoEffect = Object.values(plan.branches).every((branch) => branch.mode === "continue" && !branch.flow && (!branch.result || Object.keys(branch.result).length === 0));
|
|
293
|
+
if (branchesHaveNoEffect) {
|
|
294
|
+
this.errors.push({
|
|
295
|
+
code: "PLANNING_DECISION_NO_EFFECT",
|
|
296
|
+
stage: "control-graph",
|
|
297
|
+
path: `${path}.branches`,
|
|
298
|
+
message: "Decision branch selection has no effect because every branch immediately continues without local flow or a typed result",
|
|
299
|
+
received: ids,
|
|
300
|
+
hint: "Give at least one branch distinct local work, terminal/revisit behavior, or typed results. If one branch must skip an Atom, keep that branch flow empty and export schema-compatible literal or existing values while another branch executes the Atom",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const branches = {};
|
|
304
|
+
const continuing = [];
|
|
305
|
+
for (const [branchId, branch] of Object.entries(plan.branches)) {
|
|
306
|
+
const branchPath = `${path}.branches.${branchId}`;
|
|
307
|
+
if (branch.mode === "terminal") {
|
|
308
|
+
branches[branchId] = { kind: "terminal", status: branch.status };
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (branch.mode === "continue" && !branch.flow) {
|
|
312
|
+
branches[branchId] = { kind: "continue" };
|
|
313
|
+
continuing.push({ branchId, state: cloneState(atom.state), hasFlow: false, ...(branch.result ? { result: branch.result } : {}) });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const compiled = this.step(branch.flow, cloneState(atom.state), `${branchPath}.flow`);
|
|
317
|
+
if (!compiled)
|
|
318
|
+
continue;
|
|
319
|
+
if (branch.mode === "revisit") {
|
|
320
|
+
const reentry = this.bindReentryInput(plan.id, descriptor.inputSchema, atom.state, compiled.state, `${branchPath}.reentry`);
|
|
321
|
+
branches[branchId] = {
|
|
322
|
+
kind: "revisit",
|
|
323
|
+
maxIterations: branch.maxIterations,
|
|
324
|
+
body: compiled.step,
|
|
325
|
+
...(Object.keys(reentry).length ? { reentry } : {}),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
branches[branchId] = compiled.step;
|
|
330
|
+
continuing.push({ branchId, state: compiled.state, hasFlow: true, ...(branch.result ? { result: branch.result } : {}) });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const state = cloneState(atom.state);
|
|
334
|
+
let merge;
|
|
335
|
+
const requiresMerge = continuing.length > 1 && (continuing.some(({ hasFlow }) => hasFlow) || continuing.some(({ result }) => result !== undefined));
|
|
336
|
+
if (requiresMerge) {
|
|
337
|
+
const missing = continuing.filter(({ result }) => !result || Object.keys(result).length === 0).map(({ branchId }) => branchId);
|
|
338
|
+
if (missing.length > 0) {
|
|
339
|
+
this.errors.push({
|
|
340
|
+
code: "PLANNING_BRANCH_RESULT_REQUIRED", stage: "data-link", path: `${path}.branches`,
|
|
341
|
+
message: "Every continuing branch must export one non-empty typed result before mutually-exclusive paths can rejoin",
|
|
342
|
+
expected: continuing.map(({ branchId }) => branchId), received: missing,
|
|
343
|
+
hint: "Add result mappings to every mode='continue' branch. Map the same logical field names to values available on that branch; Runtime will create the typed merge node",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const resolvedResults = continuing.map(({ branchId, state: branchState, result }) => {
|
|
348
|
+
const values = {};
|
|
349
|
+
const schemas = {};
|
|
350
|
+
for (const [name, source] of Object.entries(result)) {
|
|
351
|
+
const resolved = this.resolveSource(source, branchState, `${path}.branches.${branchId}.result.${name}`);
|
|
352
|
+
if (!resolved)
|
|
353
|
+
continue;
|
|
354
|
+
values[name] = resolved.ref;
|
|
355
|
+
schemas[name] = resolved.schema;
|
|
356
|
+
}
|
|
357
|
+
return { branchId, values, schemas };
|
|
358
|
+
});
|
|
359
|
+
const expectedKeys = Object.keys(resolvedResults[0].schemas).sort();
|
|
360
|
+
for (const current of resolvedResults.slice(1)) {
|
|
361
|
+
const receivedKeys = Object.keys(current.schemas).sort();
|
|
362
|
+
if (JSON.stringify(receivedKeys) !== JSON.stringify(expectedKeys)) {
|
|
363
|
+
this.errors.push({
|
|
364
|
+
code: "PLANNING_BRANCH_RESULT_MISMATCH", stage: "data-link", path: `${path}.branches.${current.branchId}.result`,
|
|
365
|
+
message: "Continuing branches must export exactly the same logical result fields",
|
|
366
|
+
expected: expectedKeys, received: receivedKeys,
|
|
367
|
+
hint: "Use identical result keys on every continuing branch; source node ids and source field names may differ",
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
for (const name of expectedKeys) {
|
|
372
|
+
const expectedSchema = resolvedResults[0].schemas[name];
|
|
373
|
+
const receivedSchema = current.schemas[name];
|
|
374
|
+
if (!schemasCompatible(expectedSchema, receivedSchema, expectedSchema, receivedSchema)
|
|
375
|
+
|| !schemasCompatible(receivedSchema, expectedSchema, receivedSchema, expectedSchema)) {
|
|
376
|
+
this.errors.push({
|
|
377
|
+
code: "PLANNING_BRANCH_RESULT_MISMATCH", stage: "data-link", path: `${path}.branches.${current.branchId}.result.${name}`,
|
|
378
|
+
message: `Branch result field '${name}' has an incompatible Schema`,
|
|
379
|
+
expected: expectedSchema, received: receivedSchema,
|
|
380
|
+
hint: "Choose one schema-compatible value for this logical result on every continuing branch",
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (expectedKeys.length > 0) {
|
|
386
|
+
const mergeAlias = `${plan.id}-merge`;
|
|
387
|
+
if (state.aliases.has(mergeAlias)) {
|
|
388
|
+
this.errors.push({ code: "PLANNER_DRAFT_ALIAS_DUPLICATE", stage: "draft-normalization", path: `${path}.id`, message: `Generated branch merge id is already used: ${mergeAlias}`, hint: "Rename the Decision node so its '<id>-merge' control id is unique" });
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const outputSchema = {
|
|
392
|
+
type: "object",
|
|
393
|
+
description: `Typed result selected from one continuing branch of Decision ${plan.id}.`,
|
|
394
|
+
properties: Object.fromEntries(expectedKeys.map((name) => [name, resolvedResults[0].schemas[name]])),
|
|
395
|
+
required: expectedKeys,
|
|
396
|
+
additionalProperties: false,
|
|
397
|
+
};
|
|
398
|
+
merge = {
|
|
399
|
+
as: mergeAlias,
|
|
400
|
+
outputSchema,
|
|
401
|
+
branches: Object.fromEntries(resolvedResults.map(({ branchId, values }) => [branchId, values])),
|
|
402
|
+
};
|
|
403
|
+
state.aliases.set(mergeAlias, "@runtime/branch-merge");
|
|
404
|
+
state.sources.push(...schemaChildren({ source: "node", nodeId: mergeAlias }, "$output", outputSchema, nextControlOrder(state)));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (continuing.length) {
|
|
410
|
+
const continuingStates = continuing.map(({ state: branchState }) => branchState);
|
|
411
|
+
const common = continuingStates.reduce((set, branch) => new Set([...set].filter((key) => branch.sources.some((source) => JSON.stringify(source.selector) === key))), new Set(continuingStates[0].sources.map((source) => JSON.stringify(source.selector))));
|
|
412
|
+
for (const source of continuingStates[0].sources)
|
|
413
|
+
if (common.has(JSON.stringify(source.selector)) && !state.sources.some((item) => JSON.stringify(item.selector) === JSON.stringify(source.selector)))
|
|
414
|
+
state.sources.push(source);
|
|
415
|
+
}
|
|
416
|
+
return { step: { kind: "decision", as: plan.id, use: plan.atom, select: plan.select, input: atom.step.input, branches, defaultBranch: plan.defaultBranch, ...(merge ? { merge } : {}), ...(plan.policy ? { policy: plan.policy } : {}) }, state };
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* A revisit is a new activation of the same Decision, not a replay of its
|
|
420
|
+
* original static input. Carry only unambiguous values produced inside the
|
|
421
|
+
* branch; all unaffected fields continue to use the Decision's frozen base
|
|
422
|
+
* input. This gives local repair loops SSA-like "latest value" semantics
|
|
423
|
+
* without exposing references or edge overlays to the planning model.
|
|
424
|
+
*/
|
|
425
|
+
bindReentryInput(alias, inputSchema, beforeBranch, afterBranch, path) {
|
|
426
|
+
const properties = objectOf(objectOf(inputSchema)?.properties) ?? {};
|
|
427
|
+
const branchAliases = new Set([...afterBranch.aliases.keys()].filter((candidate) => candidate !== alias && !beforeBranch.aliases.has(candidate)));
|
|
428
|
+
const freshSources = afterBranch.sources.filter((source) => source.selector.source === "node" && branchAliases.has(source.selector.nodeId));
|
|
429
|
+
const reentry = {};
|
|
430
|
+
for (const [name, targetRaw] of Object.entries(properties)) {
|
|
431
|
+
const targetSchema = targetRaw;
|
|
432
|
+
const targetDescription = descriptionOf(targetSchema, name);
|
|
433
|
+
const selection = selectBindingSource(freshSources, name, targetSchema, targetDescription);
|
|
434
|
+
if (selection.kind === "none")
|
|
435
|
+
continue;
|
|
436
|
+
if (selection.kind === "ambiguous") {
|
|
437
|
+
this.errors.push({
|
|
438
|
+
code: "PLANNING_BINDING_AMBIGUOUS", stage: "data-link", path: `${path}.${name}`,
|
|
439
|
+
message: `Multiple fresh branch sources could update ${alias}.${name} on Decision reentry`,
|
|
440
|
+
expected: { field: name, description: targetDescription, schema: targetSchema },
|
|
441
|
+
received: candidateDetails(selection.candidates),
|
|
442
|
+
hint: `Make the repair branch produce one uniquely named or described value for ${name}`,
|
|
443
|
+
});
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
reentry[name] = selection.candidate.source.ref;
|
|
447
|
+
this.captureEvidence(alias, name, selection.candidate.source, targetSchema);
|
|
448
|
+
}
|
|
449
|
+
return reentry;
|
|
450
|
+
}
|
|
451
|
+
map(plan, before, path) {
|
|
452
|
+
const items = this.resolveSource(plan.items, before, `${path}.items`);
|
|
453
|
+
if (!items)
|
|
454
|
+
return undefined;
|
|
455
|
+
const itemSchema = objectOf(items.schema)?.items;
|
|
456
|
+
if (schemaType(items.schema) !== "array" || !itemSchema) {
|
|
457
|
+
this.errors.push({ code: "MAP_ITEMS_NOT_ARRAY", stage: "data-link", path: `${path}.items`, message: "Map items source must be an array", received: items.schema, hint: "Choose an array field from Workflow input or an upstream node" });
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
const item = { selector: { source: "item", path: "." }, name: "item", schema: itemSchema, description: descriptionOf(itemSchema, "当前 Map item"), ref: { $item: "." }, order: nextControlOrder(before), guaranteed: true };
|
|
461
|
+
const bodyBefore = { sources: [...before.sources], aliases: new Map(before.aliases), item, mapGroup: plan.id };
|
|
462
|
+
const body = this.step(plan.body, bodyBefore, `${path}.body`);
|
|
463
|
+
if (!body)
|
|
464
|
+
return undefined;
|
|
465
|
+
const createdAliases = [...body.state.aliases].filter(([alias]) => !before.aliases.has(alias));
|
|
466
|
+
const state = cloneState(before);
|
|
467
|
+
const aggregateOrder = nextControlOrder(body.state);
|
|
468
|
+
for (const [alias, atomKey] of createdAliases) {
|
|
469
|
+
state.aliases.set(alias, atomKey);
|
|
470
|
+
const descriptor = this.descriptors.get(atomKey);
|
|
471
|
+
state.sources.push(...schemaChildren({ source: "node", nodeId: alias }, "$output", descriptor.outputSchema, aggregateOrder).map((source) => ({
|
|
472
|
+
...source,
|
|
473
|
+
schema: mapAggregateSchema(source.schema, plan.id),
|
|
474
|
+
description: descriptionOf(mapAggregateSchema(source.schema, plan.id), source.description),
|
|
475
|
+
guaranteed: true,
|
|
476
|
+
})));
|
|
477
|
+
}
|
|
478
|
+
return { step: { kind: "map", as: plan.id, items: items.ref, maxItems: plan.maxItems, ...(plan.maxConcurrency ? { maxConcurrency: plan.maxConcurrency } : {}), body: body.step }, state };
|
|
479
|
+
}
|
|
480
|
+
repeat(plan, before, path) {
|
|
481
|
+
const body = this.step(plan.body, cloneState(before), `${path}.body`);
|
|
482
|
+
if (!body)
|
|
483
|
+
return undefined;
|
|
484
|
+
const decisionAtom = this.atom({ kind: "atom", id: plan.decision.id, atom: plan.decision.atom, ...(plan.decision.bindings ? { bindings: plan.decision.bindings } : {}), ...(plan.decision.policy ? { policy: plan.decision.policy } : {}) }, body.state, `${path}.decision`);
|
|
485
|
+
if (!decisionAtom)
|
|
486
|
+
return undefined;
|
|
487
|
+
const decisionDescriptor = this.descriptors.get(plan.decision.atom);
|
|
488
|
+
const decisionSchema = schemaAtPath(decisionDescriptor.outputSchema, plan.decision.select);
|
|
489
|
+
if (!decisionSchema) {
|
|
490
|
+
this.errors.push({ code: "PLANNER_DRAFT_CONTROL_INVALID", stage: "control-graph", path: `${path}.decision.select`, message: `Repeat decision select does not exist in Atom output: ${plan.decision.select}`, received: plan.decision.select, hint: "Choose an exact relative output field path from the repeat decision Atom" });
|
|
491
|
+
}
|
|
492
|
+
else if (schemaType(decisionSchema) !== "string") {
|
|
493
|
+
this.errors.push({ code: "DECISION_SELECT_TYPE_MISMATCH", stage: "control-graph", path: `${path}.decision.select`, message: "Repeat decision select must resolve to a string output field", expected: "string", received: decisionSchema });
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
step: {
|
|
497
|
+
kind: "repeat", maxIterations: plan.maxIterations, body: body.step,
|
|
498
|
+
decision: {
|
|
499
|
+
as: plan.decision.id, use: plan.decision.atom, select: plan.decision.select, input: decisionAtom.step.input,
|
|
500
|
+
continueBranch: plan.decision.continueBranch, exitBranch: plan.decision.exitBranch, defaultBranch: plan.decision.defaultBranch,
|
|
501
|
+
...(plan.decision.policy ? { policy: plan.decision.policy } : {}),
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
state: decisionAtom.state,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
bindInput(alias, inputSchema, bindings, state, path) {
|
|
508
|
+
const object = objectOf(inputSchema);
|
|
509
|
+
const properties = objectOf(object?.properties) ?? {};
|
|
510
|
+
const input = {};
|
|
511
|
+
const explicitlyBoundTopLevels = new Set(Object.keys(bindings).map((targetPath) => targetPath.split(".")[0]));
|
|
512
|
+
for (const [targetPath, source] of Object.entries(bindings)) {
|
|
513
|
+
const targetSchema = schemaAtPath(inputSchema, targetPath);
|
|
514
|
+
if (!targetSchema) {
|
|
515
|
+
this.errors.push({ code: "INPUT_ADDITIONAL_PROPERTY_FORBIDDEN", stage: "data-link", path: `${path}.${targetPath}`, message: `Binding target does not exist in Atom input schema: ${targetPath}`, hint: "Use an exact input field path from the selected Atom descriptor" });
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const resolved = this.resolveSource(source, state, `${path}.${targetPath}`, targetSchema);
|
|
519
|
+
if (resolved) {
|
|
520
|
+
setPath(input, targetPath, resolved.ref);
|
|
521
|
+
this.captureEvidence(alias, targetPath, resolved, targetSchema);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const required = new Set(requiredPropertiesForValue(inputSchema, inputSchema, input));
|
|
525
|
+
for (const [name, targetRaw] of Object.entries(properties)) {
|
|
526
|
+
if (input[name] !== undefined || explicitlyBoundTopLevels.has(name))
|
|
527
|
+
continue;
|
|
528
|
+
// Optional Atom inputs carry business intent (for example parentCard,
|
|
529
|
+
// templateName, thresholds or provider overrides). Inferring them from
|
|
530
|
+
// any compatible upstream value can silently change behavior. Only
|
|
531
|
+
// required inputs are eligible for Runtime auto-binding; optional inputs
|
|
532
|
+
// must be bound explicitly by the Planner or supplied by the user.
|
|
533
|
+
const targetSchema = targetRaw;
|
|
534
|
+
const targetDescription = descriptionOf(targetSchema, name);
|
|
535
|
+
if (!required.has(name)) {
|
|
536
|
+
// A same-name Workflow input is user-owned intent, rather than an
|
|
537
|
+
// inferred value from another Atom. It is therefore safe to forward
|
|
538
|
+
// into an optional Atom input. Keep this deliberately stricter than
|
|
539
|
+
// normal semantic auto-binding: optional fields are never matched by
|
|
540
|
+
// description and are never sourced from an upstream node.
|
|
541
|
+
const workflowMatches = state.sources.filter((source) => source.selector.source === "workflow"
|
|
542
|
+
&& source.name === name
|
|
543
|
+
&& source.guaranteed
|
|
544
|
+
&& schemasCompatible(source.schema, targetSchema, source.schema, targetSchema));
|
|
545
|
+
if (workflowMatches.length === 1)
|
|
546
|
+
input[name] = workflowMatches[0].ref;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const selection = selectBindingSource([...state.sources, ...(state.item ? [state.item] : [])], name, targetSchema, targetDescription);
|
|
550
|
+
if (selection.kind === "selected") {
|
|
551
|
+
input[name] = selection.candidate.source.ref;
|
|
552
|
+
this.captureEvidence(alias, name, selection.candidate.source, targetSchema);
|
|
553
|
+
}
|
|
554
|
+
else if (selection.kind === "ambiguous" || required.has(name)) {
|
|
555
|
+
const code = selection.kind === "ambiguous" ? "PLANNING_BINDING_AMBIGUOUS" : "PLANNING_REQUIRED_INPUT_UNAVAILABLE";
|
|
556
|
+
this.errors.push({
|
|
557
|
+
code, stage: "data-link", path: `${path}.${name}`,
|
|
558
|
+
message: selection.kind === "ambiguous" ? `Multiple sources could satisfy ${alias}.${name}` : `No guaranteed source can satisfy ${alias}.${name}`,
|
|
559
|
+
expected: { field: name, description: targetDescription, schema: targetSchema },
|
|
560
|
+
received: candidateDetails(selection.candidates),
|
|
561
|
+
hint: selection.kind === "ambiguous" ? `Add an explicit binding for ${name} using one listed semantic source` : "Add a preceding capability, provide the field as Workflow input, or report a deterministic missing-input gap",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return input;
|
|
566
|
+
}
|
|
567
|
+
captureEvidence(alias, targetPath, source, targetSchema) {
|
|
568
|
+
if (!source.sourceRef || source.name === targetPath.split(".").at(-1))
|
|
569
|
+
return;
|
|
570
|
+
const targetDescription = descriptionOf(targetSchema, targetPath);
|
|
571
|
+
const key = `${alias}:${targetPath}:${source.sourceRef}`;
|
|
572
|
+
this.evidence.set(key, {
|
|
573
|
+
schemaVersion: "1.0", consumerAlias: alias, targetPath, sourceRef: source.sourceRef,
|
|
574
|
+
sourceDescription: source.description, targetDescription, sourceSchema: source.schema, targetSchema,
|
|
575
|
+
reason: "Runtime auto-binding found one unique schema-compatible semantic source", confidence: "high",
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
resolveSource(source, state, path, targetSchema) {
|
|
579
|
+
if (source.source === "value") {
|
|
580
|
+
const schema = inferSchema(source.value);
|
|
581
|
+
if (targetSchema && !schemasCompatible(schema, targetSchema, schema, targetSchema))
|
|
582
|
+
this.errors.push({ code: "REFERENCE_TYPE_MISMATCH", stage: "data-link", path, message: "Literal value does not match the target input schema", expected: targetSchema, received: schema, hint: "Use a value with the required type" });
|
|
583
|
+
return { selector: source, name: "value", schema, description: "Planner supplied literal value", ref: source.value, order: nextControlOrder(state), guaranteed: true };
|
|
584
|
+
}
|
|
585
|
+
if (source.source === "item") {
|
|
586
|
+
if (!state.item) {
|
|
587
|
+
this.errors.push({ code: "MAP_REFERENCE_OUTSIDE_MAP", stage: "data-link", path, message: "Item source is only available inside a Map body", hint: "Use a Workflow or upstream node source outside Map" });
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
const schema = schemaAtPath(state.item.schema, source.path);
|
|
591
|
+
if (!schema)
|
|
592
|
+
return this.missingPath(source, path, state.sources);
|
|
593
|
+
const guaranteed = source.path === "." || schemaPathGuaranteed(state.item.schema, source.path.split(".").filter(Boolean), state.item.schema);
|
|
594
|
+
if (!guaranteed) {
|
|
595
|
+
this.errors.push({
|
|
596
|
+
code: "PLANNING_BINDING_SOURCE_OPTIONAL", stage: "data-link", path,
|
|
597
|
+
message: `Selected Map item source is optional and not guaranteed: ${source.path}`,
|
|
598
|
+
received: source,
|
|
599
|
+
hint: "Use a required item field or add control flow that materializes a guaranteed value",
|
|
600
|
+
});
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
return { ...state.item, selector: source, name: source.path === "." ? "item" : source.path.split(".").at(-1), schema, description: descriptionOf(schema, state.item.description), ref: { $item: source.path }, guaranteed };
|
|
604
|
+
}
|
|
605
|
+
const match = state.sources.filter((candidate) => candidate.selector.source === source.source
|
|
606
|
+
&& (source.source !== "node" || (candidate.selector.source === "node" && candidate.selector.nodeId === source.nodeId))
|
|
607
|
+
&& candidate.selector.path === source.path).at(-1);
|
|
608
|
+
if (!match)
|
|
609
|
+
return this.missingPath(source, path, state.sources);
|
|
610
|
+
if (!match.guaranteed) {
|
|
611
|
+
this.errors.push({
|
|
612
|
+
code: "PLANNING_BINDING_SOURCE_OPTIONAL", stage: "data-link", path,
|
|
613
|
+
message: `Selected source is optional and not guaranteed: ${JSON.stringify(source)}`,
|
|
614
|
+
received: source,
|
|
615
|
+
hint: "Omit the optional binding, provide the value as Workflow input, or add control flow that materializes a required result",
|
|
616
|
+
});
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
if (targetSchema && !schemasCompatible(match.schema, targetSchema, match.schema, targetSchema)) {
|
|
620
|
+
this.errors.push({ code: "REFERENCE_TYPE_MISMATCH", stage: "data-link", path, message: "Selected source schema is incompatible with the target input", expected: targetSchema, received: match.schema, hint: "Choose one of the schema-compatible sources reported by Runtime auto-binding" });
|
|
621
|
+
}
|
|
622
|
+
return match;
|
|
623
|
+
}
|
|
624
|
+
missingPath(source, path, available) {
|
|
625
|
+
this.errors.push({
|
|
626
|
+
code: "PLANNING_REQUIRED_INPUT_UNAVAILABLE", stage: "data-link", path,
|
|
627
|
+
message: `Semantic source is not guaranteed or does not exist: ${JSON.stringify(source)}`,
|
|
628
|
+
received: available.slice(-20).map(({ selector }) => selector),
|
|
629
|
+
hint: "Use an exact available semantic source, add its producer earlier, or report a missing-input gap",
|
|
630
|
+
});
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
afterAtom(before, alias, atomKey, outputSchema) {
|
|
634
|
+
const state = cloneState(before);
|
|
635
|
+
state.aliases.set(alias, atomKey);
|
|
636
|
+
state.sources.push(...schemaChildren({ source: "node", nodeId: alias }, "$output", outputSchema, nextControlOrder(before)));
|
|
637
|
+
return state;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
export function compileSemanticPlan(input) {
|
|
641
|
+
return new Compiler(input.registry, input.workflowName, input.workflowInputs, new Set(input.selectedAtomKeys)).compile(input.blocks, input.outputs);
|
|
642
|
+
}
|
|
643
|
+
//# sourceMappingURL=semantic-plan-compiler.js.map
|