@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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/analyzer/estimator.d.ts +33 -0
  4. package/dist/analyzer/estimator.js +273 -0
  5. package/dist/analyzer/graph-checker.d.ts +13 -0
  6. package/dist/analyzer/graph-checker.js +153 -0
  7. package/dist/analyzer/scope.d.ts +21 -0
  8. package/dist/analyzer/scope.js +324 -0
  9. package/dist/analyzer/types.d.ts +17 -0
  10. package/dist/analyzer/types.js +323 -0
  11. package/dist/codegen/agents.d.ts +2 -0
  12. package/dist/codegen/agents.js +109 -0
  13. package/dist/codegen/backend.d.ts +16 -0
  14. package/dist/codegen/backend.js +1 -0
  15. package/dist/codegen/claude-backend.d.ts +9 -0
  16. package/dist/codegen/claude-backend.js +47 -0
  17. package/dist/codegen/codegen.d.ts +10 -0
  18. package/dist/codegen/codegen.js +57 -0
  19. package/dist/codegen/hooks.d.ts +2 -0
  20. package/dist/codegen/hooks.js +165 -0
  21. package/dist/codegen/orchestration.d.ts +3 -0
  22. package/dist/codegen/orchestration.js +250 -0
  23. package/dist/codegen/settings.d.ts +36 -0
  24. package/dist/codegen/settings.js +87 -0
  25. package/dist/compiler.d.ts +21 -0
  26. package/dist/compiler.js +101 -0
  27. package/dist/constants.d.ts +9 -0
  28. package/dist/constants.js +13 -0
  29. package/dist/errors/diagnostics.d.ts +21 -0
  30. package/dist/errors/diagnostics.js +25 -0
  31. package/dist/format.d.ts +12 -0
  32. package/dist/format.js +46 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +181 -0
  35. package/dist/lexer/lexer.d.ts +23 -0
  36. package/dist/lexer/lexer.js +268 -0
  37. package/dist/lexer/tokens.d.ts +96 -0
  38. package/dist/lexer/tokens.js +150 -0
  39. package/dist/lsp/features/code-actions.d.ts +7 -0
  40. package/dist/lsp/features/code-actions.js +58 -0
  41. package/dist/lsp/features/completions.d.ts +7 -0
  42. package/dist/lsp/features/completions.js +271 -0
  43. package/dist/lsp/features/definition.d.ts +3 -0
  44. package/dist/lsp/features/definition.js +32 -0
  45. package/dist/lsp/features/diagnostics.d.ts +4 -0
  46. package/dist/lsp/features/diagnostics.js +33 -0
  47. package/dist/lsp/features/hover.d.ts +7 -0
  48. package/dist/lsp/features/hover.js +88 -0
  49. package/dist/lsp/features/index.d.ts +9 -0
  50. package/dist/lsp/features/index.js +9 -0
  51. package/dist/lsp/features/references.d.ts +7 -0
  52. package/dist/lsp/features/references.js +53 -0
  53. package/dist/lsp/features/rename.d.ts +17 -0
  54. package/dist/lsp/features/rename.js +198 -0
  55. package/dist/lsp/features/symbols.d.ts +7 -0
  56. package/dist/lsp/features/symbols.js +74 -0
  57. package/dist/lsp/features/utils.d.ts +3 -0
  58. package/dist/lsp/features/utils.js +65 -0
  59. package/dist/lsp/features.d.ts +20 -0
  60. package/dist/lsp/features.js +513 -0
  61. package/dist/lsp/server.d.ts +2 -0
  62. package/dist/lsp/server.js +327 -0
  63. package/dist/parser/ast.d.ts +244 -0
  64. package/dist/parser/ast.js +10 -0
  65. package/dist/parser/parser.d.ts +95 -0
  66. package/dist/parser/parser.js +1175 -0
  67. package/dist/program-index.d.ts +21 -0
  68. package/dist/program-index.js +74 -0
  69. package/dist/resolver/resolver.d.ts +9 -0
  70. package/dist/resolver/resolver.js +136 -0
  71. package/dist/runner.d.ts +13 -0
  72. package/dist/runner.js +41 -0
  73. package/dist/runtime/executor.d.ts +56 -0
  74. package/dist/runtime/executor.js +285 -0
  75. package/dist/runtime/expr-eval.d.ts +3 -0
  76. package/dist/runtime/expr-eval.js +138 -0
  77. package/dist/runtime/flow-runner.d.ts +21 -0
  78. package/dist/runtime/flow-runner.js +230 -0
  79. package/dist/runtime/memory.d.ts +5 -0
  80. package/dist/runtime/memory.js +41 -0
  81. package/dist/runtime/prompt-builder.d.ts +12 -0
  82. package/dist/runtime/prompt-builder.js +66 -0
  83. package/dist/runtime/subprocess.d.ts +20 -0
  84. package/dist/runtime/subprocess.js +99 -0
  85. package/dist/runtime/token-tracker.d.ts +36 -0
  86. package/dist/runtime/token-tracker.js +56 -0
  87. package/dist/runtime/transforms.d.ts +2 -0
  88. package/dist/runtime/transforms.js +104 -0
  89. package/dist/types.d.ts +10 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils.d.ts +3 -0
  92. package/dist/utils.js +35 -0
  93. package/dist/version.d.ts +1 -0
  94. package/dist/version.js +11 -0
  95. 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,2 @@
1
+ import { NodeDecl } from '../parser/ast.js';
2
+ export declare function generateAgent(node: NodeDecl, memoryNames?: Set<string>, inputOverrides?: Map<string, string>): string;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { EdgeDecl } from '../parser/ast.js';
2
+ export declare function generateHook(edge: EdgeDecl): string | null;