@jsleekr/graft 5.7.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 +21 -0
- package/README.md +235 -0
- package/dist/analyzer/estimator.d.ts +33 -0
- package/dist/analyzer/estimator.js +273 -0
- package/dist/analyzer/graph-checker.d.ts +13 -0
- package/dist/analyzer/graph-checker.js +153 -0
- package/dist/analyzer/scope.d.ts +21 -0
- package/dist/analyzer/scope.js +324 -0
- package/dist/analyzer/types.d.ts +17 -0
- package/dist/analyzer/types.js +323 -0
- package/dist/codegen/agents.d.ts +2 -0
- package/dist/codegen/agents.js +109 -0
- package/dist/codegen/backend.d.ts +16 -0
- package/dist/codegen/backend.js +1 -0
- package/dist/codegen/claude-backend.d.ts +9 -0
- package/dist/codegen/claude-backend.js +47 -0
- package/dist/codegen/codegen.d.ts +10 -0
- package/dist/codegen/codegen.js +57 -0
- package/dist/codegen/hooks.d.ts +2 -0
- package/dist/codegen/hooks.js +165 -0
- package/dist/codegen/orchestration.d.ts +3 -0
- package/dist/codegen/orchestration.js +250 -0
- package/dist/codegen/settings.d.ts +36 -0
- package/dist/codegen/settings.js +87 -0
- package/dist/compiler.d.ts +21 -0
- package/dist/compiler.js +101 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/errors/diagnostics.d.ts +21 -0
- package/dist/errors/diagnostics.js +25 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +181 -0
- package/dist/lexer/lexer.d.ts +23 -0
- package/dist/lexer/lexer.js +268 -0
- package/dist/lexer/tokens.d.ts +96 -0
- package/dist/lexer/tokens.js +150 -0
- package/dist/lsp/features/code-actions.d.ts +7 -0
- package/dist/lsp/features/code-actions.js +58 -0
- package/dist/lsp/features/completions.d.ts +7 -0
- package/dist/lsp/features/completions.js +271 -0
- package/dist/lsp/features/definition.d.ts +3 -0
- package/dist/lsp/features/definition.js +32 -0
- package/dist/lsp/features/diagnostics.d.ts +4 -0
- package/dist/lsp/features/diagnostics.js +33 -0
- package/dist/lsp/features/hover.d.ts +7 -0
- package/dist/lsp/features/hover.js +88 -0
- package/dist/lsp/features/index.d.ts +9 -0
- package/dist/lsp/features/index.js +9 -0
- package/dist/lsp/features/references.d.ts +7 -0
- package/dist/lsp/features/references.js +53 -0
- package/dist/lsp/features/rename.d.ts +17 -0
- package/dist/lsp/features/rename.js +198 -0
- package/dist/lsp/features/symbols.d.ts +7 -0
- package/dist/lsp/features/symbols.js +74 -0
- package/dist/lsp/features/utils.d.ts +3 -0
- package/dist/lsp/features/utils.js +65 -0
- package/dist/lsp/features.d.ts +20 -0
- package/dist/lsp/features.js +513 -0
- package/dist/lsp/server.d.ts +2 -0
- package/dist/lsp/server.js +327 -0
- package/dist/parser/ast.d.ts +244 -0
- package/dist/parser/ast.js +10 -0
- package/dist/parser/parser.d.ts +95 -0
- package/dist/parser/parser.js +1175 -0
- package/dist/program-index.d.ts +21 -0
- package/dist/program-index.js +74 -0
- package/dist/resolver/resolver.d.ts +9 -0
- package/dist/resolver/resolver.js +136 -0
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +41 -0
- package/dist/runtime/executor.d.ts +56 -0
- package/dist/runtime/executor.js +285 -0
- package/dist/runtime/expr-eval.d.ts +3 -0
- package/dist/runtime/expr-eval.js +138 -0
- package/dist/runtime/flow-runner.d.ts +21 -0
- package/dist/runtime/flow-runner.js +230 -0
- package/dist/runtime/memory.d.ts +5 -0
- package/dist/runtime/memory.js +41 -0
- package/dist/runtime/prompt-builder.d.ts +12 -0
- package/dist/runtime/prompt-builder.js +66 -0
- package/dist/runtime/subprocess.d.ts +20 -0
- package/dist/runtime/subprocess.js +99 -0
- package/dist/runtime/token-tracker.d.ts +36 -0
- package/dist/runtime/token-tracker.js +56 -0
- package/dist/runtime/transforms.d.ts +2 -0
- package/dist/runtime/transforms.js +104 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +35 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +11 -0
- package/package.json +70 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { BUILTIN_FUNCTIONS } from '../parser/ast.js';
|
|
2
|
+
import { GraftError } from '../errors/diagnostics.js';
|
|
3
|
+
import { ProgramIndex } from '../program-index.js';
|
|
4
|
+
export class TypeChecker {
|
|
5
|
+
program;
|
|
6
|
+
index;
|
|
7
|
+
constructor(program, index) {
|
|
8
|
+
this.program = program;
|
|
9
|
+
this.index = index ?? new ProgramIndex(program);
|
|
10
|
+
}
|
|
11
|
+
check() {
|
|
12
|
+
const diagnostics = [];
|
|
13
|
+
this.checkEdgeTransforms(diagnostics);
|
|
14
|
+
this.checkWritesSchemaOverlap(diagnostics);
|
|
15
|
+
this.checkConditionTypes(diagnostics);
|
|
16
|
+
this.checkExprTypes(diagnostics);
|
|
17
|
+
return diagnostics;
|
|
18
|
+
}
|
|
19
|
+
checkWritesSchemaOverlap(diagnostics) {
|
|
20
|
+
for (const node of this.program.nodes) {
|
|
21
|
+
if (node.writes.length === 0)
|
|
22
|
+
continue;
|
|
23
|
+
const producesFields = this.index.producesFieldsMap.get(node.name);
|
|
24
|
+
if (!producesFields)
|
|
25
|
+
continue; // scope checker catches
|
|
26
|
+
for (const writeRef of node.writes) {
|
|
27
|
+
const memoryFields = this.index.memoryFieldsMap.get(writeRef.memory);
|
|
28
|
+
if (!memoryFields)
|
|
29
|
+
continue; // scope checker catches undeclared
|
|
30
|
+
let hasOverlap = false;
|
|
31
|
+
for (const field of producesFields.keys()) {
|
|
32
|
+
if (memoryFields.has(field)) {
|
|
33
|
+
hasOverlap = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!hasOverlap) {
|
|
38
|
+
diagnostics.push(new GraftError(`Node '${node.name}' writes to memory '${writeRef.memory}' but produces no matching fields`, node.location, 'warning', 'TYPE_SCHEMA_MISMATCH'));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
checkEdgeTransforms(errors) {
|
|
44
|
+
for (const edge of this.program.edges) {
|
|
45
|
+
const sourceFields = this.index.producesFieldsMap.get(edge.source);
|
|
46
|
+
if (!sourceFields)
|
|
47
|
+
continue; // scope checker will catch this
|
|
48
|
+
for (const transform of edge.transforms) {
|
|
49
|
+
if (transform.type === 'select') {
|
|
50
|
+
for (const f of transform.fields) {
|
|
51
|
+
if (!sourceFields.has(f)) {
|
|
52
|
+
errors.push(new GraftError(`select: field '${f}' does not exist in '${edge.source}' output`, edge.location, 'error', 'TYPE_FIELD_NOT_FOUND'));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (transform.type === 'filter') {
|
|
57
|
+
if (!sourceFields.has(transform.field)) {
|
|
58
|
+
errors.push(new GraftError(`filter: field '${transform.field}' does not exist in '${edge.source}' output`, edge.location, 'error', 'TYPE_FIELD_NOT_FOUND'));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (transform.type === 'drop') {
|
|
62
|
+
if (!sourceFields.has(transform.field)) {
|
|
63
|
+
errors.push(new GraftError(`drop: field '${transform.field}' does not exist in '${edge.source}' output`, edge.location, 'error', 'TYPE_FIELD_NOT_FOUND'));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
checkConditionTypes(errors) {
|
|
70
|
+
for (const edge of this.program.edges) {
|
|
71
|
+
if (edge.target.kind !== 'conditional')
|
|
72
|
+
continue;
|
|
73
|
+
const sourceFields = this.index.producesFieldsMap.get(edge.source);
|
|
74
|
+
if (!sourceFields)
|
|
75
|
+
continue; // scope checker catches this
|
|
76
|
+
for (const branch of edge.target.branches) {
|
|
77
|
+
if (!branch.condition)
|
|
78
|
+
continue; // else branch
|
|
79
|
+
const cond = branch.condition;
|
|
80
|
+
if (cond.kind !== 'binary')
|
|
81
|
+
continue;
|
|
82
|
+
const { op } = cond;
|
|
83
|
+
// Only ordered comparisons need numeric types
|
|
84
|
+
if (op === '==' || op === '!=')
|
|
85
|
+
continue;
|
|
86
|
+
// Extract field name from left side
|
|
87
|
+
const field = cond.left.kind === 'field_access'
|
|
88
|
+
? cond.left.segments.join('.')
|
|
89
|
+
: cond.left.kind === 'call'
|
|
90
|
+
? `${cond.left.name}(...)`
|
|
91
|
+
: '<expr>';
|
|
92
|
+
const fieldType = sourceFields.get(field);
|
|
93
|
+
if (!fieldType)
|
|
94
|
+
continue; // scope checker catches this
|
|
95
|
+
if (!isNumericType(fieldType)) {
|
|
96
|
+
errors.push(new GraftError(`Ordered comparison '${op}' requires numeric type, but field '${field}' has type '${fieldType.kind === 'primitive' ? fieldType.name : fieldType.kind}'`, edge.location, 'error', 'TYPE_CONDITION_MISMATCH'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
checkExprTypes(errors) {
|
|
102
|
+
for (const graph of this.program.graphs) {
|
|
103
|
+
const varTypes = new Map();
|
|
104
|
+
this.walkFlowForTypes(graph.flow, varTypes, errors);
|
|
105
|
+
this.checkVarConditionTypes(varTypes, errors);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
walkFlowForTypes(nodes, varTypes, errors) {
|
|
109
|
+
for (const step of nodes) {
|
|
110
|
+
if (step.kind === 'let') {
|
|
111
|
+
const inferred = this.inferExprType(step.value, varTypes);
|
|
112
|
+
varTypes.set(step.name, inferred);
|
|
113
|
+
this.checkExprTypeErrors(step.value, varTypes, errors);
|
|
114
|
+
}
|
|
115
|
+
else if (step.kind === 'foreach') {
|
|
116
|
+
const bodyTypes = new Map(varTypes);
|
|
117
|
+
bodyTypes.set(step.binding, 'unknown');
|
|
118
|
+
this.walkFlowForTypes(step.body, bodyTypes, errors);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
inferExprType(expr, varTypes) {
|
|
123
|
+
switch (expr.kind) {
|
|
124
|
+
case 'literal': {
|
|
125
|
+
const v = expr.value;
|
|
126
|
+
if (typeof v === 'number')
|
|
127
|
+
return 'number';
|
|
128
|
+
if (typeof v === 'string')
|
|
129
|
+
return 'string';
|
|
130
|
+
if (typeof v === 'boolean')
|
|
131
|
+
return 'boolean';
|
|
132
|
+
return 'unknown';
|
|
133
|
+
}
|
|
134
|
+
case 'field_access': {
|
|
135
|
+
// Single-segment: check varTypes first (variable-first resolution)
|
|
136
|
+
if (expr.segments.length === 1) {
|
|
137
|
+
const varType = varTypes.get(expr.segments[0]);
|
|
138
|
+
if (varType)
|
|
139
|
+
return varType;
|
|
140
|
+
}
|
|
141
|
+
// Multi-segment: first segment is node name, second is field name
|
|
142
|
+
if (expr.segments.length >= 2) {
|
|
143
|
+
const fields = this.index.producesFieldsMap.get(expr.segments[0]);
|
|
144
|
+
if (fields) {
|
|
145
|
+
const fieldType = fields.get(expr.segments[1]);
|
|
146
|
+
if (fieldType)
|
|
147
|
+
return typeExprToInferred(fieldType);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return 'unknown';
|
|
151
|
+
}
|
|
152
|
+
case 'binary': {
|
|
153
|
+
const leftType = this.inferExprType(expr.left, varTypes);
|
|
154
|
+
const rightType = this.inferExprType(expr.right, varTypes);
|
|
155
|
+
// Null coalescing: result type is right side (fallback) if left is unknown
|
|
156
|
+
if (expr.op === '??') {
|
|
157
|
+
if (leftType !== 'unknown')
|
|
158
|
+
return leftType;
|
|
159
|
+
return rightType;
|
|
160
|
+
}
|
|
161
|
+
// Logical and comparison operators return boolean
|
|
162
|
+
if (expr.op === '&&' || expr.op === '||')
|
|
163
|
+
return 'boolean';
|
|
164
|
+
if (expr.op === '<' || expr.op === '>' || expr.op === '<=' || expr.op === '>=' || expr.op === '==' || expr.op === '!=') {
|
|
165
|
+
return 'boolean';
|
|
166
|
+
}
|
|
167
|
+
if (expr.op === '+') {
|
|
168
|
+
if (leftType === 'string' || rightType === 'string')
|
|
169
|
+
return 'string';
|
|
170
|
+
if (leftType === 'number' && rightType === 'number')
|
|
171
|
+
return 'number';
|
|
172
|
+
return 'unknown';
|
|
173
|
+
}
|
|
174
|
+
return 'number'; // -, *, /, % produce numbers
|
|
175
|
+
}
|
|
176
|
+
case 'unary': {
|
|
177
|
+
if (expr.op === '!')
|
|
178
|
+
return 'boolean';
|
|
179
|
+
return 'number'; // unary minus
|
|
180
|
+
}
|
|
181
|
+
case 'group':
|
|
182
|
+
return this.inferExprType(expr.inner, varTypes);
|
|
183
|
+
case 'call': {
|
|
184
|
+
const builtin = BUILTIN_FUNCTIONS[expr.name];
|
|
185
|
+
return builtin?.returnType ?? 'unknown';
|
|
186
|
+
}
|
|
187
|
+
case 'template':
|
|
188
|
+
return 'string';
|
|
189
|
+
case 'conditional': {
|
|
190
|
+
const consequentType = this.inferExprType(expr.consequent, varTypes);
|
|
191
|
+
const alternateType = this.inferExprType(expr.alternate, varTypes);
|
|
192
|
+
if (consequentType === alternateType)
|
|
193
|
+
return consequentType;
|
|
194
|
+
return 'unknown';
|
|
195
|
+
}
|
|
196
|
+
default: {
|
|
197
|
+
const _exhaustive = expr;
|
|
198
|
+
return _exhaustive;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
checkExprTypeErrors(expr, varTypes, errors) {
|
|
203
|
+
if (expr.kind === 'binary') {
|
|
204
|
+
const leftType = this.inferExprType(expr.left, varTypes);
|
|
205
|
+
const rightType = this.inferExprType(expr.right, varTypes);
|
|
206
|
+
if (leftType !== 'unknown' && rightType !== 'unknown') {
|
|
207
|
+
if (expr.op === '??') {
|
|
208
|
+
// No constraints — any types allowed
|
|
209
|
+
}
|
|
210
|
+
else if (expr.op === '&&' || expr.op === '||') {
|
|
211
|
+
if (leftType !== 'boolean' || rightType !== 'boolean') {
|
|
212
|
+
errors.push(new GraftError(`Operator '${expr.op}' expects boolean operands, got '${leftType}' and '${rightType}'`, expr.location, 'warning', 'TYPE_EXPR_MISMATCH'));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (expr.op === '==' || expr.op === '!=') {
|
|
216
|
+
// Equality allows any types
|
|
217
|
+
}
|
|
218
|
+
else if (expr.op === '<' || expr.op === '>' || expr.op === '<=' || expr.op === '>=') {
|
|
219
|
+
if (leftType !== 'number' || rightType !== 'number') {
|
|
220
|
+
errors.push(new GraftError(`Operator '${expr.op}' requires numeric operands, got '${leftType}' and '${rightType}'`, expr.location, 'error', 'TYPE_EXPR_MISMATCH'));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (expr.op === '+') {
|
|
224
|
+
if (leftType !== rightType) {
|
|
225
|
+
errors.push(new GraftError(`Operator '+' cannot be applied to types '${leftType}' and '${rightType}'`, expr.location, 'error', 'TYPE_EXPR_MISMATCH'));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
if (leftType !== 'number' || rightType !== 'number') {
|
|
230
|
+
errors.push(new GraftError(`Operator '${expr.op}' requires numeric operands, got '${leftType}' and '${rightType}'`, expr.location, 'error', 'TYPE_EXPR_MISMATCH'));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
this.checkExprTypeErrors(expr.left, varTypes, errors);
|
|
235
|
+
this.checkExprTypeErrors(expr.right, varTypes, errors);
|
|
236
|
+
}
|
|
237
|
+
else if (expr.kind === 'unary') {
|
|
238
|
+
const operandType = this.inferExprType(expr.operand, varTypes);
|
|
239
|
+
if (operandType !== 'unknown') {
|
|
240
|
+
if (expr.op === '!' && operandType !== 'boolean') {
|
|
241
|
+
errors.push(new GraftError(`Operator '!' requires boolean operand, got '${operandType}'`, expr.location, 'error', 'TYPE_EXPR_MISMATCH'));
|
|
242
|
+
}
|
|
243
|
+
else if (expr.op === '-' && operandType !== 'number') {
|
|
244
|
+
errors.push(new GraftError(`Unary '-' requires numeric operand, got '${operandType}'`, expr.location, 'error', 'TYPE_EXPR_MISMATCH'));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.checkExprTypeErrors(expr.operand, varTypes, errors);
|
|
248
|
+
}
|
|
249
|
+
else if (expr.kind === 'group') {
|
|
250
|
+
this.checkExprTypeErrors(expr.inner, varTypes, errors);
|
|
251
|
+
}
|
|
252
|
+
else if (expr.kind === 'call') {
|
|
253
|
+
const builtin = BUILTIN_FUNCTIONS[expr.name];
|
|
254
|
+
if (builtin && expr.args.length !== builtin.arity) {
|
|
255
|
+
errors.push(new GraftError(`Function '${expr.name}' expects ${builtin.arity} argument(s), got ${expr.args.length}`, expr.location, 'error', 'TYPE_FUNC_ARITY'));
|
|
256
|
+
}
|
|
257
|
+
for (const arg of expr.args) {
|
|
258
|
+
this.checkExprTypeErrors(arg, varTypes, errors);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (expr.kind === 'template') {
|
|
262
|
+
for (const part of expr.parts) {
|
|
263
|
+
if (part.kind === 'expr') {
|
|
264
|
+
this.checkExprTypeErrors(part.value, varTypes, errors);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (expr.kind === 'conditional') {
|
|
269
|
+
const consType = this.inferExprType(expr.consequent, varTypes);
|
|
270
|
+
const altType = this.inferExprType(expr.alternate, varTypes);
|
|
271
|
+
if (consType !== 'unknown' && altType !== 'unknown' && consType !== altType) {
|
|
272
|
+
errors.push(new GraftError(`Conditional branches have different types: '${consType}' and '${altType}'`, expr.location, 'warning', 'TYPE_CONDITIONAL_MISMATCH'));
|
|
273
|
+
}
|
|
274
|
+
this.checkExprTypeErrors(expr.condition, varTypes, errors);
|
|
275
|
+
this.checkExprTypeErrors(expr.consequent, varTypes, errors);
|
|
276
|
+
this.checkExprTypeErrors(expr.alternate, varTypes, errors);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
checkVarConditionTypes(varTypes, errors) {
|
|
280
|
+
for (const edge of this.program.edges) {
|
|
281
|
+
if (edge.target.kind !== 'conditional')
|
|
282
|
+
continue;
|
|
283
|
+
for (const branch of edge.target.branches) {
|
|
284
|
+
if (!branch.condition)
|
|
285
|
+
continue;
|
|
286
|
+
const cond = branch.condition;
|
|
287
|
+
if (cond.kind !== 'binary')
|
|
288
|
+
continue;
|
|
289
|
+
const { op, left } = cond;
|
|
290
|
+
if (op === '==' || op === '!=')
|
|
291
|
+
continue;
|
|
292
|
+
// Check if condition LHS is a variable reference (single-segment field_access)
|
|
293
|
+
if (left.kind === 'field_access' && left.segments.length === 1) {
|
|
294
|
+
const varName = left.segments[0];
|
|
295
|
+
const varType = varTypes.get(varName);
|
|
296
|
+
if (varType && varType !== 'unknown' && varType !== 'number') {
|
|
297
|
+
errors.push(new GraftError(`Ordered comparison '${op}' requires numeric type, but variable '${varName}' has type '${varType}'`, edge.location, 'error', 'TYPE_VAR_CONDITION'));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function typeExprToInferred(type) {
|
|
305
|
+
if (type.kind === 'primitive') {
|
|
306
|
+
switch (type.name) {
|
|
307
|
+
case 'String': return 'string';
|
|
308
|
+
case 'Int':
|
|
309
|
+
case 'Float': return 'number';
|
|
310
|
+
case 'Bool': return 'boolean';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (type.kind === 'primitive_range')
|
|
314
|
+
return 'number';
|
|
315
|
+
return 'unknown';
|
|
316
|
+
}
|
|
317
|
+
function isNumericType(type) {
|
|
318
|
+
if (type.kind === 'primitive')
|
|
319
|
+
return type.name === 'Int' || type.name === 'Float';
|
|
320
|
+
if (type.kind === 'primitive_range')
|
|
321
|
+
return true;
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { MODEL_MAP } from '../constants.js';
|
|
2
|
+
import { fieldsToJsonExample } from '../utils.js';
|
|
3
|
+
const TOOL_MAP = {
|
|
4
|
+
file_read: ['Read'],
|
|
5
|
+
file_write: ['Write', 'Edit'],
|
|
6
|
+
terminal: ['Bash'],
|
|
7
|
+
ast_parse: ['Bash'],
|
|
8
|
+
test_run: ['Bash'],
|
|
9
|
+
lint: ['Bash'],
|
|
10
|
+
browser: ['Bash'],
|
|
11
|
+
};
|
|
12
|
+
export function generateAgent(node, memoryNames = new Set(), inputOverrides = new Map()) {
|
|
13
|
+
const name = node.name.toLowerCase();
|
|
14
|
+
const resolvedModel = MODEL_MAP[node.model] || node.model;
|
|
15
|
+
const tools = resolveTools(node.tools);
|
|
16
|
+
const jsonSchema = fieldsToJsonExample(node.produces.fields);
|
|
17
|
+
const failureSection = formatFailure(node);
|
|
18
|
+
const writesSection = formatWrites(node, memoryNames);
|
|
19
|
+
const toolsLine = tools.length > 0 ? `\ntools: [${tools.join(', ')}]` : '';
|
|
20
|
+
return `---
|
|
21
|
+
name: ${name}
|
|
22
|
+
description: ${node.name} agent — produces ${node.produces.name}
|
|
23
|
+
model: ${resolvedModel}${toolsLine}
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# ${node.name} Agent
|
|
27
|
+
|
|
28
|
+
## Context Loading
|
|
29
|
+
${formatReads(node, memoryNames, inputOverrides)}
|
|
30
|
+
${writesSection}## Output Contract
|
|
31
|
+
Produce JSON output matching this schema:
|
|
32
|
+
\`\`\`json
|
|
33
|
+
${JSON.stringify(jsonSchema, null, 2)}
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
## Token Discipline
|
|
37
|
+
- Input budget: ${node.budgetIn} tokens. Read only what is necessary.
|
|
38
|
+
- Output budget: ${node.budgetOut} tokens. No explanations, no reasoning traces.
|
|
39
|
+
- Output ONLY the JSON result.
|
|
40
|
+
|
|
41
|
+
## Completion Protocol
|
|
42
|
+
1. Write result to \`.graft/session/node_outputs/${name}.json\`
|
|
43
|
+
2. Output: \`===NODE_COMPLETE:${name}===\`
|
|
44
|
+
|
|
45
|
+
${failureSection}`;
|
|
46
|
+
}
|
|
47
|
+
function resolveTools(tools) {
|
|
48
|
+
const resolved = new Set();
|
|
49
|
+
for (const tool of tools) {
|
|
50
|
+
const mapped = TOOL_MAP[tool];
|
|
51
|
+
if (mapped) {
|
|
52
|
+
for (const t of mapped)
|
|
53
|
+
resolved.add(t);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
resolved.add(tool);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...resolved];
|
|
60
|
+
}
|
|
61
|
+
function formatReads(node, memoryNames, inputOverrides = new Map()) {
|
|
62
|
+
if (node.reads.length === 0)
|
|
63
|
+
return 'No external context required.';
|
|
64
|
+
return node.reads.map(ref => {
|
|
65
|
+
const isMemory = memoryNames.has(ref.context);
|
|
66
|
+
const fieldLabel = ref.field
|
|
67
|
+
? (ref.field.length === 1 ? `.${ref.field[0]}` : `.{${ref.field.join(', ')}}`)
|
|
68
|
+
: '';
|
|
69
|
+
// Check if there's an edge-transformed input override for this context
|
|
70
|
+
const override = inputOverrides.get(ref.context);
|
|
71
|
+
if (override) {
|
|
72
|
+
return `- Load \`${ref.context}${fieldLabel}\` from \`${override}\``;
|
|
73
|
+
}
|
|
74
|
+
const dir = isMemory ? `.graft/memory/${ref.context.toLowerCase()}.json` : '.graft/session/';
|
|
75
|
+
return `- Load \`${ref.context}${fieldLabel}\` from \`${dir}\``;
|
|
76
|
+
}).join('\n');
|
|
77
|
+
}
|
|
78
|
+
function formatWrites(node, memoryNames) {
|
|
79
|
+
const memoryWrites = node.writes.filter(w => memoryNames.has(w.memory));
|
|
80
|
+
if (memoryWrites.length === 0)
|
|
81
|
+
return '';
|
|
82
|
+
return `
|
|
83
|
+
## Memory Saving
|
|
84
|
+
After producing output, save to persistent memory:
|
|
85
|
+
${memoryWrites.map(w => {
|
|
86
|
+
const fieldLabel = w.field ? `.${w.field}` : '';
|
|
87
|
+
return `- Save to \`.graft/memory/${w.memory.toLowerCase()}.json\`${fieldLabel ? ` (field: ${w.field})` : ''}`;
|
|
88
|
+
}).join('\n')}
|
|
89
|
+
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
function formatFailure(node) {
|
|
93
|
+
const name = node.name.toLowerCase();
|
|
94
|
+
if (!node.onFailure) {
|
|
95
|
+
return `## Failure Protocol\nOn failure, output: \`===NODE_FAILED:${name}===\``;
|
|
96
|
+
}
|
|
97
|
+
switch (node.onFailure.type) {
|
|
98
|
+
case 'retry':
|
|
99
|
+
return `## Failure Protocol\nRetry up to ${node.onFailure.max} times. After ${node.onFailure.max} failures, output: \`===NODE_FAILED:${name}===\``;
|
|
100
|
+
case 'fallback':
|
|
101
|
+
return `## Failure Protocol\nOn failure, delegate to ${node.onFailure.node} agent. If fallback also fails, output: \`===NODE_FAILED:${name}===\``;
|
|
102
|
+
case 'retry_then_fallback':
|
|
103
|
+
return `## Failure Protocol\nRetry up to ${node.onFailure.max} times. After ${node.onFailure.max} failures, delegate to ${node.onFailure.node} agent. If fallback also fails, output: \`===NODE_FAILED:${name}===\``;
|
|
104
|
+
case 'skip':
|
|
105
|
+
return `## Failure Protocol\nOn failure, skip this node. Output: \`===NODE_SKIPPED:${name}===\``;
|
|
106
|
+
case 'abort':
|
|
107
|
+
return `## Failure Protocol\nOn failure, abort the entire pipeline. Output: \`===PIPELINE_ABORTED:${name}===\``;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Program, NodeDecl, EdgeDecl } from '../parser/ast.js';
|
|
2
|
+
import { TokenReport } from '../analyzer/estimator.js';
|
|
3
|
+
import { ProgramIndex } from '../program-index.js';
|
|
4
|
+
export interface CodegenContext {
|
|
5
|
+
program: Program;
|
|
6
|
+
report: TokenReport;
|
|
7
|
+
index: ProgramIndex;
|
|
8
|
+
sourceFile: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CodegenBackend {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
generateAgent(node: NodeDecl, memoryNames: Set<string>, ctx: CodegenContext): string;
|
|
13
|
+
generateHook(edge: EdgeDecl, ctx: CodegenContext): string | null;
|
|
14
|
+
generateOrchestration(ctx: CodegenContext): string;
|
|
15
|
+
generateSettings(ctx: CodegenContext): Record<string, unknown>;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NodeDecl, EdgeDecl } from '../parser/ast.js';
|
|
2
|
+
import { CodegenBackend, CodegenContext } from './backend.js';
|
|
3
|
+
export declare class ClaudeCodeBackend implements CodegenBackend {
|
|
4
|
+
readonly name = "claude";
|
|
5
|
+
generateAgent(node: NodeDecl, memoryNames: Set<string>, ctx: CodegenContext): string;
|
|
6
|
+
generateHook(edge: EdgeDecl, _ctx: CodegenContext): string | null;
|
|
7
|
+
generateOrchestration(ctx: CodegenContext): string;
|
|
8
|
+
generateSettings(ctx: CodegenContext): Record<string, unknown>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { generateAgent } from './agents.js';
|
|
2
|
+
import { generateHook } from './hooks.js';
|
|
3
|
+
import { generateOrchestration } from './orchestration.js';
|
|
4
|
+
import { generateSettings } from './settings.js';
|
|
5
|
+
export class ClaudeCodeBackend {
|
|
6
|
+
name = 'claude';
|
|
7
|
+
generateAgent(node, memoryNames, ctx) {
|
|
8
|
+
// Compute input overrides: map produces names to actual file paths
|
|
9
|
+
const inputOverrides = new Map();
|
|
10
|
+
// 1. For edges with transforms: use the transformed output path
|
|
11
|
+
// 2. For edges without transforms: use the source's raw output path
|
|
12
|
+
for (const edge of ctx.program.edges) {
|
|
13
|
+
if (edge.target.kind === 'direct' && edge.target.node === node.name) {
|
|
14
|
+
const sourceNode = ctx.program.nodes.find(n => n.name === edge.source);
|
|
15
|
+
if (sourceNode) {
|
|
16
|
+
const producesName = sourceNode.produces.name;
|
|
17
|
+
if (edge.transforms.length > 0) {
|
|
18
|
+
inputOverrides.set(producesName, `.graft/session/node_outputs/${edge.source.toLowerCase()}_to_${node.name.toLowerCase()}.json`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
inputOverrides.set(producesName, `.graft/session/node_outputs/${edge.source.toLowerCase()}.json`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 3. For produces reads with no corresponding edge: resolve to producer's raw output
|
|
27
|
+
for (const ref of node.reads) {
|
|
28
|
+
if (inputOverrides.has(ref.context) || memoryNames.has(ref.context))
|
|
29
|
+
continue;
|
|
30
|
+
// Check if this read references a produces type from another node
|
|
31
|
+
const producerNode = ctx.program.nodes.find(n => n.produces.name === ref.context);
|
|
32
|
+
if (producerNode && producerNode.name !== node.name) {
|
|
33
|
+
inputOverrides.set(ref.context, `.graft/session/node_outputs/${producerNode.name.toLowerCase()}.json`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return generateAgent(node, memoryNames, inputOverrides);
|
|
37
|
+
}
|
|
38
|
+
generateHook(edge, _ctx) {
|
|
39
|
+
return generateHook(edge);
|
|
40
|
+
}
|
|
41
|
+
generateOrchestration(ctx) {
|
|
42
|
+
return generateOrchestration(ctx.program, ctx.report);
|
|
43
|
+
}
|
|
44
|
+
generateSettings(ctx) {
|
|
45
|
+
return generateSettings(ctx.program, ctx.sourceFile, ctx.index);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Program } from '../parser/ast.js';
|
|
2
|
+
import { TokenReport } from '../analyzer/estimator.js';
|
|
3
|
+
import { ProgramIndex } from '../program-index.js';
|
|
4
|
+
import { CodegenBackend } from './backend.js';
|
|
5
|
+
export interface GeneratedFile {
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function generate(program: Program, report: TokenReport, sourceFile: string, index?: ProgramIndex, backend?: CodegenBackend): GeneratedFile[];
|
|
10
|
+
export declare function writeFiles(files: GeneratedFile[], outDir: string): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ProgramIndex } from '../program-index.js';
|
|
4
|
+
import { ClaudeCodeBackend } from './claude-backend.js';
|
|
5
|
+
const defaultBackend = new ClaudeCodeBackend();
|
|
6
|
+
export function generate(program, report, sourceFile, index, backend) {
|
|
7
|
+
const idx = index ?? new ProgramIndex(program);
|
|
8
|
+
const be = backend ?? defaultBackend;
|
|
9
|
+
const ctx = { program, report, index: idx, sourceFile };
|
|
10
|
+
const files = [];
|
|
11
|
+
const memoryNames = new Set(program.memories.map(m => m.name));
|
|
12
|
+
// Agents
|
|
13
|
+
for (const node of program.nodes) {
|
|
14
|
+
files.push({
|
|
15
|
+
path: `.claude/agents/${node.name.toLowerCase()}.md`,
|
|
16
|
+
content: be.generateAgent(node, memoryNames, ctx),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// Hooks
|
|
20
|
+
for (const edge of program.edges) {
|
|
21
|
+
const hook = be.generateHook(edge, ctx);
|
|
22
|
+
if (hook && edge.target.kind === 'direct') {
|
|
23
|
+
const source = edge.source.toLowerCase();
|
|
24
|
+
const target = edge.target.node.toLowerCase();
|
|
25
|
+
files.push({
|
|
26
|
+
path: `.claude/hooks/${source}-to-${target}.js`,
|
|
27
|
+
content: hook,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Orchestration
|
|
32
|
+
files.push({
|
|
33
|
+
path: '.claude/CLAUDE.md',
|
|
34
|
+
content: be.generateOrchestration(ctx),
|
|
35
|
+
});
|
|
36
|
+
// Settings
|
|
37
|
+
const settings = be.generateSettings(ctx);
|
|
38
|
+
files.push({
|
|
39
|
+
path: '.claude/settings.json',
|
|
40
|
+
content: JSON.stringify(settings, null, 2),
|
|
41
|
+
});
|
|
42
|
+
// Runtime scaffold
|
|
43
|
+
files.push({ path: '.graft/session/node_outputs/.gitkeep', content: '' });
|
|
44
|
+
files.push({ path: '.graft/token_log.txt', content: '' });
|
|
45
|
+
// Memory scaffold — conditional
|
|
46
|
+
if (program.memories.length > 0) {
|
|
47
|
+
files.push({ path: '.graft/memory/.gitkeep', content: '' });
|
|
48
|
+
}
|
|
49
|
+
return files;
|
|
50
|
+
}
|
|
51
|
+
export function writeFiles(files, outDir) {
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const fullPath = path.join(outDir, file.path);
|
|
54
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
55
|
+
fs.writeFileSync(fullPath, file.content, 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
}
|