@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,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
|
+
}
|
package/dist/runner.d.ts
ADDED
|
@@ -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;
|