@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,21 @@
1
+ import { Program, ContextDecl, NodeDecl, MemoryDecl, EdgeDecl, GraphDecl, TypeExpr, Expr } from './parser/ast.js';
2
+ import type { SourceLocation } from './errors/diagnostics.js';
3
+ export interface LetBinding {
4
+ name: string;
5
+ value: Expr;
6
+ graphName: string;
7
+ location?: SourceLocation;
8
+ }
9
+ export declare class ProgramIndex {
10
+ readonly contextMap: Map<string, ContextDecl>;
11
+ readonly nodeMap: Map<string, NodeDecl>;
12
+ readonly memoryMap: Map<string, MemoryDecl>;
13
+ readonly edgesBySource: Map<string, EdgeDecl[]>;
14
+ readonly producesNodeMap: Map<string, NodeDecl>;
15
+ readonly graphMap: Map<string, GraphDecl>;
16
+ readonly producesFieldsMap: Map<string, Map<string, TypeExpr>>;
17
+ readonly memoryFieldsMap: Map<string, Map<string, TypeExpr>>;
18
+ readonly letBindingMap: Map<string, LetBinding>;
19
+ constructor(program: Program);
20
+ private collectLetBindings;
21
+ }
@@ -0,0 +1,74 @@
1
+ export class ProgramIndex {
2
+ contextMap;
3
+ nodeMap;
4
+ memoryMap;
5
+ edgesBySource;
6
+ producesNodeMap;
7
+ graphMap;
8
+ producesFieldsMap;
9
+ memoryFieldsMap;
10
+ letBindingMap;
11
+ constructor(program) {
12
+ this.contextMap = new Map();
13
+ for (const c of program.contexts) {
14
+ this.contextMap.set(c.name, c);
15
+ }
16
+ this.nodeMap = new Map();
17
+ this.producesNodeMap = new Map();
18
+ for (const n of program.nodes) {
19
+ this.nodeMap.set(n.name, n);
20
+ this.producesNodeMap.set(n.produces.name, n);
21
+ }
22
+ this.memoryMap = new Map();
23
+ for (const m of program.memories) {
24
+ this.memoryMap.set(m.name, m);
25
+ }
26
+ this.edgesBySource = new Map();
27
+ for (const e of program.edges) {
28
+ const existing = this.edgesBySource.get(e.source) ?? [];
29
+ existing.push(e);
30
+ this.edgesBySource.set(e.source, existing);
31
+ }
32
+ this.graphMap = new Map();
33
+ for (const g of program.graphs) {
34
+ this.graphMap.set(g.name, g);
35
+ }
36
+ this.producesFieldsMap = new Map();
37
+ for (const n of program.nodes) {
38
+ const fields = new Map();
39
+ for (const f of n.produces.fields) {
40
+ fields.set(f.name, f.type);
41
+ }
42
+ // Keyed by both node name AND produces name for different lookup patterns
43
+ this.producesFieldsMap.set(n.name, fields);
44
+ this.producesFieldsMap.set(n.produces.name, fields);
45
+ }
46
+ this.memoryFieldsMap = new Map();
47
+ for (const m of program.memories) {
48
+ const fields = new Map();
49
+ for (const f of m.fields) {
50
+ fields.set(f.name, f.type);
51
+ }
52
+ this.memoryFieldsMap.set(m.name, fields);
53
+ }
54
+ this.letBindingMap = new Map();
55
+ for (const g of program.graphs) {
56
+ this.collectLetBindings(g.flow, g.name);
57
+ }
58
+ }
59
+ collectLetBindings(nodes, graphName) {
60
+ for (const step of nodes) {
61
+ if (step.kind === 'let') {
62
+ this.letBindingMap.set(step.name, {
63
+ name: step.name,
64
+ value: step.value,
65
+ graphName,
66
+ location: step.location,
67
+ });
68
+ }
69
+ else if (step.kind === 'foreach') {
70
+ this.collectLetBindings(step.body, graphName);
71
+ }
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,9 @@
1
+ import { Program } from '../parser/ast.js';
2
+ import { GraftError } from '../errors/diagnostics.js';
3
+ export type FileReader = (absolutePath: string) => string;
4
+ export interface ResolveResult {
5
+ program: Program;
6
+ resolvedFiles: string[];
7
+ errors: GraftError[];
8
+ }
9
+ export declare function resolve(entryProgram: Program, sourceFile: string, readFile?: FileReader): ResolveResult;
@@ -0,0 +1,136 @@
1
+ // src/resolver/resolver.ts
2
+ import * as path from 'node:path';
3
+ import * as fs from 'node:fs';
4
+ import { Lexer } from '../lexer/lexer.js';
5
+ import { Parser } from '../parser/parser.js';
6
+ import { GraftError } from '../errors/diagnostics.js';
7
+ function extractExportables(program) {
8
+ const contexts = new Map();
9
+ for (const c of program.contexts)
10
+ contexts.set(c.name, c);
11
+ const nodes = new Map();
12
+ for (const n of program.nodes)
13
+ nodes.set(n.name, n);
14
+ return { contexts, nodes };
15
+ }
16
+ function parseSource(source) {
17
+ const lexer = new Lexer(source);
18
+ const tokens = lexer.tokenize();
19
+ const parser = new Parser(tokens);
20
+ const { program, errors } = parser.parse();
21
+ if (errors.length > 0) {
22
+ throw errors[0];
23
+ }
24
+ return program;
25
+ }
26
+ function emptyProgram() {
27
+ return { imports: [], memories: [], contexts: [], nodes: [], edges: [], graphs: [] };
28
+ }
29
+ export function resolve(entryProgram, sourceFile, readFile = (p) => fs.readFileSync(p, 'utf-8')) {
30
+ const absSourceFile = path.resolve(sourceFile);
31
+ const errors = [];
32
+ const ctx = {
33
+ entryFile: absSourceFile,
34
+ exportCache: new Map(),
35
+ ancestors: new Set([absSourceFile]),
36
+ declaredNames: new Map(),
37
+ resolvedFiles: [absSourceFile],
38
+ errors,
39
+ readFile,
40
+ };
41
+ // Register entry file's local names
42
+ for (const c of entryProgram.contexts)
43
+ ctx.declaredNames.set(c.name, absSourceFile);
44
+ for (const n of entryProgram.nodes)
45
+ ctx.declaredNames.set(n.name, absSourceFile);
46
+ // Cache entry file's exportables (for diamond import scenarios)
47
+ ctx.exportCache.set(absSourceFile, extractExportables(entryProgram));
48
+ // Resolve all imports
49
+ for (const importDecl of entryProgram.imports) {
50
+ resolveImport(importDecl, absSourceFile, entryProgram, ctx);
51
+ }
52
+ return { program: entryProgram, resolvedFiles: ctx.resolvedFiles, errors: ctx.errors };
53
+ }
54
+ function resolveImport(importDecl, importingFile, importingProgram, ctx) {
55
+ const targetPath = path.resolve(path.dirname(importingFile), importDecl.path);
56
+ importDecl.resolvedPath = targetPath; // v2.0-R05
57
+ // Validate .gft extension
58
+ if (!importDecl.path.endsWith('.gft')) {
59
+ ctx.errors.push(new GraftError(`Import path must end with .gft: "${importDecl.path}"`, importDecl.location, 'error', 'IMPORT_INVALID_PATH'));
60
+ return;
61
+ }
62
+ // Circular import detection
63
+ if (ctx.ancestors.has(targetPath)) {
64
+ ctx.errors.push(new GraftError(`Circular import detected: "${importDecl.path}"`, importDecl.location, 'error', 'IMPORT_CIRCULAR'));
65
+ return;
66
+ }
67
+ // Parse and cache target file if not already cached
68
+ if (!ctx.exportCache.has(targetPath)) {
69
+ let targetSource;
70
+ try {
71
+ targetSource = ctx.readFile(targetPath);
72
+ }
73
+ catch {
74
+ ctx.errors.push(new GraftError(`Import file not found: "${importDecl.path}"`, importDecl.location, 'error', 'IMPORT_NOT_FOUND'));
75
+ return;
76
+ }
77
+ let targetProgram;
78
+ try {
79
+ targetProgram = parseSource(targetSource);
80
+ }
81
+ catch (e) {
82
+ if (e instanceof GraftError) {
83
+ ctx.errors.push(new GraftError(`Error parsing imported file "${importDecl.path}": ${e.message}`, importDecl.location, 'error', 'IMPORT_PARSE_ERROR'));
84
+ }
85
+ else {
86
+ throw e;
87
+ }
88
+ return;
89
+ }
90
+ // CRITICAL INVARIANT: Extract exportables BEFORE recursing.
91
+ const exportables = extractExportables(targetProgram);
92
+ ctx.exportCache.set(targetPath, exportables);
93
+ if (!ctx.resolvedFiles.includes(targetPath)) {
94
+ ctx.resolvedFiles.push(targetPath);
95
+ }
96
+ // Recurse into target's imports
97
+ ctx.ancestors.add(targetPath);
98
+ for (const nestedImport of targetProgram.imports) {
99
+ resolveImport(nestedImport, targetPath, targetProgram, ctx);
100
+ }
101
+ ctx.ancestors.delete(targetPath);
102
+ }
103
+ // Only merge names into the importing program if this is the entry file.
104
+ // Nested imports only need to parse and cache for transitive re-export prevention.
105
+ if (importingFile !== ctx.entryFile)
106
+ return;
107
+ // Look up requested names from cached exportables
108
+ const exportables = ctx.exportCache.get(targetPath);
109
+ const availableNames = [...exportables.contexts.keys(), ...exportables.nodes.keys()];
110
+ for (const name of importDecl.names) {
111
+ const context = exportables.contexts.get(name);
112
+ const node = exportables.nodes.get(name);
113
+ if (!context && !node) {
114
+ const suggestion = availableNames.length > 0
115
+ ? `. Available: ${availableNames.join(', ')}`
116
+ : '. File has no importable declarations';
117
+ ctx.errors.push(new GraftError(`Name "${name}" not found in "${importDecl.path}"${suggestion}`, importDecl.location, 'error', 'IMPORT_NAME_NOT_FOUND'));
118
+ continue;
119
+ }
120
+ // Duplicate detection
121
+ if (ctx.declaredNames.has(name)) {
122
+ const existingFile = ctx.declaredNames.get(name);
123
+ ctx.errors.push(new GraftError(`Duplicate name "${name}": already declared in ${path.basename(existingFile)}`, importDecl.location, 'error', 'IMPORT_DUPLICATE_NAME'));
124
+ continue;
125
+ }
126
+ ctx.declaredNames.set(name, targetPath);
127
+ if (context) {
128
+ context.sourceFile = targetPath;
129
+ importingProgram.contexts.push(context);
130
+ }
131
+ if (node) {
132
+ node.sourceFile = targetPath;
133
+ importingProgram.nodes.push(node);
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,13 @@
1
+ import { RunResult, SpawnerFn } from './runtime/executor.js';
2
+ export type { RunResult, RunOptions, SpawnerFn } from './runtime/executor.js';
3
+ export interface RunInput {
4
+ sourceFile: string;
5
+ inputFile?: string;
6
+ input?: Record<string, unknown>;
7
+ workDir?: string;
8
+ dryRun?: boolean;
9
+ verbose?: boolean;
10
+ timeoutMs?: number;
11
+ spawner?: SpawnerFn;
12
+ }
13
+ export declare function run(opts: RunInput): Promise<RunResult>;
package/dist/runner.js ADDED
@@ -0,0 +1,41 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { compile } from './compiler.js';
4
+ import { Executor } from './runtime/executor.js';
5
+ export async function run(opts) {
6
+ const sourceFile = path.resolve(opts.sourceFile);
7
+ if (!fs.existsSync(sourceFile)) {
8
+ return { success: false, graph: '', nodeResults: [], finalOutput: null, totalDurationMs: 0, errors: [`Source file not found: ${sourceFile}`] };
9
+ }
10
+ const source = fs.readFileSync(sourceFile, 'utf-8');
11
+ const workDir = opts.workDir ?? path.dirname(sourceFile);
12
+ const compileResult = compile(source, sourceFile);
13
+ if (!compileResult.success || !compileResult.program) {
14
+ return { success: false, graph: '', nodeResults: [], finalOutput: null, totalDurationMs: 0, errors: compileResult.errors.map(e => e.message) };
15
+ }
16
+ const programIndex = compileResult.index;
17
+ let input;
18
+ if (opts.input) {
19
+ input = opts.input;
20
+ }
21
+ else if (opts.inputFile) {
22
+ const inputPath = path.resolve(opts.inputFile);
23
+ if (!fs.existsSync(inputPath)) {
24
+ return { success: false, graph: compileResult.program.graphs[0]?.name ?? '', nodeResults: [], finalOutput: null, totalDurationMs: 0, errors: [`Input file not found: ${inputPath}`] };
25
+ }
26
+ try {
27
+ input = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
28
+ }
29
+ catch (e) {
30
+ return { success: false, graph: compileResult.program.graphs[0]?.name ?? '', nodeResults: [], finalOutput: null, totalDurationMs: 0, errors: [`Failed to parse input JSON: ${e instanceof Error ? e.message : String(e)}`] };
31
+ }
32
+ }
33
+ else {
34
+ input = {};
35
+ }
36
+ const executor = new Executor(compileResult.program, {
37
+ sourceFile: path.basename(sourceFile), input, workDir,
38
+ dryRun: opts.dryRun, verbose: opts.verbose, timeoutMs: opts.timeoutMs, spawner: opts.spawner,
39
+ }, programIndex);
40
+ return executor.execute();
41
+ }
@@ -0,0 +1,56 @@
1
+ import { Program } from '../parser/ast.js';
2
+ import { SpawnOptions, SpawnResult, TokenUsage } from './subprocess.js';
3
+ import { ProgramIndex } from '../program-index.js';
4
+ export type SpawnerFn = (options: SpawnOptions) => Promise<SpawnResult>;
5
+ export interface RunOptions {
6
+ sourceFile: string;
7
+ input: Record<string, unknown>;
8
+ workDir: string;
9
+ dryRun?: boolean;
10
+ verbose?: boolean;
11
+ timeoutMs?: number;
12
+ spawner?: SpawnerFn;
13
+ }
14
+ export interface NodeResult {
15
+ node: string;
16
+ output: unknown;
17
+ durationMs: number;
18
+ success: boolean;
19
+ error?: string;
20
+ tokenUsage?: TokenUsage;
21
+ }
22
+ export interface RunResult {
23
+ success: boolean;
24
+ graph: string;
25
+ nodeResults: NodeResult[];
26
+ finalOutput: unknown;
27
+ totalDurationMs: number;
28
+ errors: string[];
29
+ tokenUsage?: {
30
+ budget: number;
31
+ consumed: number;
32
+ fraction: number;
33
+ perNode: Array<{
34
+ node: string;
35
+ actual?: number;
36
+ estimated: number;
37
+ }>;
38
+ };
39
+ }
40
+ export declare class Executor {
41
+ private program;
42
+ private options;
43
+ private index;
44
+ private outputs;
45
+ private spawner;
46
+ private sessionDir;
47
+ private nodeOutputDir;
48
+ private memoryDir;
49
+ private memoryNames;
50
+ private tracker;
51
+ constructor(program: Program, options: RunOptions, index?: ProgramIndex);
52
+ execute(): Promise<RunResult>;
53
+ private cleanSession;
54
+ private executeNode;
55
+ private storeOutput;
56
+ }
@@ -0,0 +1,285 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { spawnClaude, parseCLIOutput } from './subprocess.js';
4
+ import { applyTransforms } from './transforms.js';
5
+ import { TokenTracker } from './token-tracker.js';
6
+ import { MODEL_MAP } from '../constants.js';
7
+ import { loadMemory, saveMemory } from './memory.js';
8
+ import { ProgramIndex } from '../program-index.js';
9
+ import { buildPrompt, generateMockOutput } from './prompt-builder.js';
10
+ import { executeFlowNodes } from './flow-runner.js';
11
+ export class Executor {
12
+ program;
13
+ options;
14
+ index;
15
+ outputs;
16
+ spawner;
17
+ sessionDir;
18
+ nodeOutputDir;
19
+ memoryDir;
20
+ memoryNames;
21
+ tracker;
22
+ constructor(program, options, index) {
23
+ this.program = program;
24
+ this.options = options;
25
+ this.index = index ?? new ProgramIndex(program);
26
+ this.spawner = options.spawner ?? spawnClaude;
27
+ this.outputs = new Map();
28
+ this.memoryDir = path.join(options.workDir, '.graft', 'memory');
29
+ this.memoryNames = new Set(program.memories.map(m => m.name));
30
+ this.sessionDir = path.join(options.workDir, '.graft', 'session');
31
+ this.nodeOutputDir = path.join(this.sessionDir, 'node_outputs');
32
+ }
33
+ async execute() {
34
+ const startTime = Date.now();
35
+ const errors = [];
36
+ const nodeResults = [];
37
+ // Find the first graph
38
+ if (this.program.graphs.length === 0) {
39
+ return {
40
+ success: false,
41
+ graph: '',
42
+ nodeResults: [],
43
+ finalOutput: null,
44
+ totalDurationMs: Date.now() - startTime,
45
+ errors: ['No graph declaration found'],
46
+ };
47
+ }
48
+ const graph = this.program.graphs[0];
49
+ // Session cleanup: remove old files, preserve .gitkeep
50
+ this.cleanSession();
51
+ // Ensure session directory exists
52
+ fs.mkdirSync(this.nodeOutputDir, { recursive: true });
53
+ // Initialize token tracker
54
+ const tokenLogPath = path.join(this.options.workDir, '.graft', 'token_log.txt');
55
+ fs.mkdirSync(path.dirname(tokenLogPath), { recursive: true });
56
+ fs.writeFileSync(tokenLogPath, '');
57
+ this.tracker = new TokenTracker(graph.budget, tokenLogPath);
58
+ // Ensure memory directory exists (if memories declared)
59
+ if (this.program.memories.length > 0) {
60
+ fs.mkdirSync(this.memoryDir, { recursive: true });
61
+ }
62
+ // Write input to session
63
+ fs.writeFileSync(path.join(this.sessionDir, `${graph.input.toLowerCase()}.json`), JSON.stringify(this.options.input, null, 2));
64
+ // Execute flow nodes
65
+ const flowCtx = {
66
+ executeNode: (name) => this.executeNode(name),
67
+ getFailureStrategy: (name) => {
68
+ const nodeDecl = this.index.nodeMap.get(name);
69
+ return nodeDecl?.onFailure;
70
+ },
71
+ getConditionalEdge: (sourceName) => {
72
+ const edges = this.index.edgesBySource.get(sourceName);
73
+ if (!edges)
74
+ return null;
75
+ for (const edge of edges) {
76
+ if (edge.target.kind === 'conditional') {
77
+ return { branches: edge.target.branches, transforms: edge.transforms };
78
+ }
79
+ }
80
+ return null;
81
+ },
82
+ outputs: this.outputs,
83
+ input: this.options.input,
84
+ };
85
+ try {
86
+ await executeFlowNodes(graph.flow, nodeResults, errors, flowCtx);
87
+ }
88
+ catch (e) {
89
+ errors.push(e instanceof Error ? e.message : String(e));
90
+ }
91
+ // Determine final output
92
+ let finalOutput = null;
93
+ const outputName = graph.output.toLowerCase();
94
+ // Check outputs by produces name
95
+ for (const [key, val] of this.outputs) {
96
+ if (key.toLowerCase() === outputName) {
97
+ finalOutput = val;
98
+ break;
99
+ }
100
+ }
101
+ // If not found by produces name, try the last node result
102
+ if (finalOutput === null && nodeResults.length > 0) {
103
+ const lastSuccess = [...nodeResults].reverse().find(r => r.success);
104
+ if (lastSuccess)
105
+ finalOutput = lastSuccess.output;
106
+ }
107
+ return {
108
+ success: errors.length === 0,
109
+ graph: graph.name,
110
+ nodeResults,
111
+ finalOutput,
112
+ totalDurationMs: Date.now() - startTime,
113
+ errors,
114
+ tokenUsage: this.tracker.getSummary(),
115
+ };
116
+ }
117
+ cleanSession() {
118
+ if (!fs.existsSync(this.nodeOutputDir))
119
+ return;
120
+ const files = fs.readdirSync(this.nodeOutputDir);
121
+ for (const file of files) {
122
+ if (file === '.gitkeep')
123
+ continue;
124
+ fs.rmSync(path.join(this.nodeOutputDir, file), { force: true });
125
+ }
126
+ }
127
+ async executeNode(name) {
128
+ const startTime = Date.now();
129
+ const nodeDecl = this.index.nodeMap.get(name);
130
+ if (!nodeDecl) {
131
+ return {
132
+ node: name,
133
+ output: null,
134
+ durationMs: Date.now() - startTime,
135
+ success: false,
136
+ error: `Node '${name}' not found in program`,
137
+ };
138
+ }
139
+ // Load memory for reads that reference memory declarations
140
+ // ALWAYS reload from disk (no this.outputs.has guard — fixes foreach staleness)
141
+ for (const ref of nodeDecl.reads) {
142
+ if (this.memoryNames.has(ref.context)) {
143
+ const memData = loadMemory(this.memoryDir, ref.context, { verbose: this.options.verbose });
144
+ if (memData !== null) {
145
+ this.outputs.set(ref.context, memData);
146
+ }
147
+ else {
148
+ this.outputs.delete(ref.context);
149
+ }
150
+ }
151
+ }
152
+ // Build prompt context
153
+ const graph = this.program.graphs[0];
154
+ const promptCtx = {
155
+ outputs: this.outputs,
156
+ graphInputName: graph ? graph.input : '',
157
+ input: this.options.input,
158
+ };
159
+ // Dry run: produce mock output
160
+ if (this.options.dryRun) {
161
+ const mockOutput = generateMockOutput(nodeDecl);
162
+ this.storeOutput(nodeDecl, mockOutput);
163
+ const estimated = { in: nodeDecl.budgetIn, out: nodeDecl.budgetOut };
164
+ this.tracker.record(name, undefined, estimated);
165
+ return {
166
+ node: name,
167
+ output: mockOutput,
168
+ durationMs: Date.now() - startTime,
169
+ success: true,
170
+ };
171
+ }
172
+ // Build prompt
173
+ const prompt = buildPrompt(nodeDecl, promptCtx);
174
+ const resolvedModel = MODEL_MAP[nodeDecl.model] || nodeDecl.model;
175
+ const args = [
176
+ '--output-format', 'json',
177
+ '--model', resolvedModel,
178
+ '--max-tokens', String(nodeDecl.budgetOut),
179
+ '-p', prompt,
180
+ ];
181
+ try {
182
+ const result = await this.spawner({
183
+ args,
184
+ cwd: this.options.workDir,
185
+ timeoutMs: this.options.timeoutMs ?? 300000,
186
+ });
187
+ if (this.options.verbose) {
188
+ console.log(`[${name}] exit=${result.exitCode} stdout=${result.stdout.length}b stderr=${result.stderr.length}b`);
189
+ }
190
+ if (result.exitCode !== 0) {
191
+ // Try to extract output anyway
192
+ try {
193
+ const cliOutput = parseCLIOutput(result.stdout);
194
+ const output = cliOutput.content;
195
+ const tokenUsage = cliOutput.tokenUsage;
196
+ this.storeOutput(nodeDecl, output);
197
+ const estimated = { in: nodeDecl.budgetIn, out: nodeDecl.budgetOut };
198
+ this.tracker.record(name, tokenUsage, estimated);
199
+ return {
200
+ node: name,
201
+ output,
202
+ durationMs: Date.now() - startTime,
203
+ success: true,
204
+ tokenUsage,
205
+ };
206
+ }
207
+ catch {
208
+ return {
209
+ node: name,
210
+ output: null,
211
+ durationMs: Date.now() - startTime,
212
+ success: false,
213
+ error: `Node '${name}' exited with code ${result.exitCode}. stderr: ${result.stderr.slice(0, 500)}`,
214
+ };
215
+ }
216
+ }
217
+ const cliOutput = parseCLIOutput(result.stdout);
218
+ const output = cliOutput.content;
219
+ const tokenUsage = cliOutput.tokenUsage;
220
+ this.storeOutput(nodeDecl, output);
221
+ const estimated = { in: nodeDecl.budgetIn, out: nodeDecl.budgetOut };
222
+ this.tracker.record(name, tokenUsage, estimated);
223
+ if (this.options.verbose) {
224
+ if (this.tracker.isCritical) {
225
+ console.log(`[BUDGET] Critical: ${Math.round(this.tracker.fraction * 100)}% of budget consumed`);
226
+ }
227
+ else if (this.tracker.isWarning) {
228
+ console.log(`[BUDGET] Warning: ${Math.round(this.tracker.fraction * 100)}% of budget consumed`);
229
+ }
230
+ }
231
+ return {
232
+ node: name,
233
+ output,
234
+ durationMs: Date.now() - startTime,
235
+ success: true,
236
+ tokenUsage,
237
+ };
238
+ }
239
+ catch (e) {
240
+ return {
241
+ node: name,
242
+ output: null,
243
+ durationMs: Date.now() - startTime,
244
+ success: false,
245
+ error: e instanceof Error ? e.message : String(e),
246
+ };
247
+ }
248
+ }
249
+ storeOutput(nodeDecl, output) {
250
+ // Store by node name
251
+ this.outputs.set(nodeDecl.name, output);
252
+ // Store by produces name
253
+ this.outputs.set(nodeDecl.produces.name, output);
254
+ // Write to session directory
255
+ const fileName = nodeDecl.name.toLowerCase() + '.json';
256
+ fs.writeFileSync(path.join(this.nodeOutputDir, fileName), JSON.stringify(output, null, 2));
257
+ // Also write by produces name
258
+ const producesFileName = nodeDecl.produces.name.toLowerCase() + '.json';
259
+ if (producesFileName !== fileName) {
260
+ fs.writeFileSync(path.join(this.nodeOutputDir, producesFileName), JSON.stringify(output, null, 2));
261
+ }
262
+ // Apply edge transforms and write transformed outputs for downstream nodes
263
+ const edges = this.index.edgesBySource.get(nodeDecl.name) ?? [];
264
+ for (const edge of edges) {
265
+ if (edge.transforms.length > 0 && edge.target.kind === 'direct') {
266
+ const transformed = applyTransforms(output, edge.transforms);
267
+ const targetName = edge.target.node.toLowerCase();
268
+ const transformedFileName = `${nodeDecl.name.toLowerCase()}_to_${targetName}.json`;
269
+ fs.writeFileSync(path.join(this.nodeOutputDir, transformedFileName), JSON.stringify(transformed, null, 2));
270
+ }
271
+ }
272
+ // Save to memory for writes targets
273
+ for (const writeRef of nodeDecl.writes) {
274
+ if (this.memoryNames.has(writeRef.memory)) {
275
+ if (!this.options.dryRun) {
276
+ const mem = this.index.memoryMap.get(writeRef.memory);
277
+ if (mem) {
278
+ const fields = writeRef.field ? [writeRef.field] : undefined;
279
+ saveMemory(this.memoryDir, mem, output, fields);
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,3 @@
1
+ import { Expr } from '../parser/ast.js';
2
+ export declare function evaluateExpr(expr: Expr, outputs: Map<string, unknown>, variables?: Map<string, unknown>, warnings?: string[]): unknown;
3
+ export declare function resolveNestedField(segments: string[], obj: Record<string, unknown>): unknown;