@jsleekr/graft 5.8.0 → 6.0.1

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.
@@ -0,0 +1,170 @@
1
+ import { formatExpr } from './format.js';
2
+ export function formatProgram(program) {
3
+ const sections = [];
4
+ // Imports
5
+ for (const imp of program.imports) {
6
+ sections.push(formatImport(imp));
7
+ }
8
+ // Memories
9
+ for (const mem of program.memories) {
10
+ sections.push(formatMemory(mem));
11
+ }
12
+ // Contexts
13
+ for (const ctx of program.contexts) {
14
+ if (!ctx.sourceFile || ctx.sourceFile === program.contexts[0]?.sourceFile) {
15
+ sections.push(formatContext(ctx));
16
+ }
17
+ }
18
+ // Nodes
19
+ for (const node of program.nodes) {
20
+ if (!node.sourceFile || node.sourceFile === program.nodes[0]?.sourceFile) {
21
+ sections.push(formatNode(node));
22
+ }
23
+ }
24
+ // Edges
25
+ for (const edge of program.edges) {
26
+ sections.push(formatEdge(edge));
27
+ }
28
+ // Graphs
29
+ for (const graph of program.graphs) {
30
+ sections.push(formatGraph(graph));
31
+ }
32
+ return sections.join('\n\n') + '\n';
33
+ }
34
+ function formatImport(imp) {
35
+ return `import { ${imp.names.join(', ')} } from "${imp.path}"`;
36
+ }
37
+ function formatMemory(mem) {
38
+ const fields = mem.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
39
+ return `memory ${mem.name}(max_tokens: ${formatTokens(mem.maxTokens)}, storage: ${mem.storage}) {\n${fields}\n}`;
40
+ }
41
+ function formatContext(ctx) {
42
+ const fields = ctx.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
43
+ return `context ${ctx.name}(max_tokens: ${formatTokens(ctx.maxTokens)}) {\n${fields}\n}`;
44
+ }
45
+ function formatNode(node) {
46
+ const lines = [];
47
+ lines.push(`node ${node.name}(model: ${node.model}, budget: ${formatTokens(node.budgetIn)}/${formatTokens(node.budgetOut)}) {`);
48
+ // reads
49
+ if (node.reads.length > 0) {
50
+ lines.push(` reads: [${node.reads.map(formatContextRef).join(', ')}]`);
51
+ }
52
+ // tools
53
+ if (node.tools.length > 0) {
54
+ lines.push(` tools: [${node.tools.join(', ')}]`);
55
+ }
56
+ // writes
57
+ if (node.writes.length > 0) {
58
+ lines.push(` writes: [${node.writes.map(w => w.field ? `${w.memory}.${w.field}` : w.memory).join(', ')}]`);
59
+ }
60
+ // on_failure
61
+ if (node.onFailure) {
62
+ lines.push(` on_failure: ${formatFailure(node.onFailure)}`);
63
+ }
64
+ // produces
65
+ lines.push('');
66
+ const producesFields = node.produces.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
67
+ lines.push(` produces ${node.produces.name} {`);
68
+ lines.push(producesFields);
69
+ lines.push(` }`);
70
+ lines.push('}');
71
+ return lines.join('\n');
72
+ }
73
+ function formatEdge(edge) {
74
+ if (edge.target.kind === 'conditional') {
75
+ const branches = edge.target.branches.map(b => {
76
+ if (b.condition) {
77
+ return ` when ${formatExpr(b.condition)} -> ${b.target}`;
78
+ }
79
+ return ` else -> ${b.target}`;
80
+ }).join('\n');
81
+ return `edge ${edge.source} -> {\n${branches}\n}`;
82
+ }
83
+ let line = `edge ${edge.source} -> ${edge.target.node}`;
84
+ if (edge.transforms.length > 0) {
85
+ line += '\n | ' + edge.transforms.map(formatTransform).join('\n | ');
86
+ }
87
+ return line;
88
+ }
89
+ function formatGraph(graph) {
90
+ const params = graph.params.length > 0
91
+ ? `, ${graph.params.map(p => `${p.name}: ${p.type}${p.default !== undefined ? ` = ${p.default}` : ''}`).join(', ')}`
92
+ : '';
93
+ const flowStr = formatFlow(graph.flow, 2);
94
+ return `graph ${graph.name}(input: ${graph.input}, output: ${graph.output}, budget: ${formatTokens(graph.budget)}${params}) {\n${flowStr}\n}`;
95
+ }
96
+ function formatFlow(flow, indent) {
97
+ const pad = ' '.repeat(indent);
98
+ const parts = [];
99
+ for (const step of flow) {
100
+ switch (step.kind) {
101
+ case 'node':
102
+ parts.push(step.name);
103
+ break;
104
+ case 'parallel':
105
+ parts.push(`parallel { ${step.branches.join(' ')} }`);
106
+ break;
107
+ case 'foreach': {
108
+ const body = formatFlow(step.body, indent + 2);
109
+ parts.push(`foreach ${step.source}.${step.field} as ${step.binding} (max: ${step.maxIterations}) {\n${body}\n${pad}}`);
110
+ break;
111
+ }
112
+ case 'let':
113
+ parts.push(`let ${step.name} = ${formatExpr(step.value)}`);
114
+ break;
115
+ case 'graph_call':
116
+ parts.push(`${step.name}(${step.args.map(a => `${a.name}: ${formatExpr(a.value)}`).join(', ')})`);
117
+ break;
118
+ }
119
+ }
120
+ // Join with -> for sequential steps, but done is implicit
121
+ return pad + parts.join('\n' + pad + '-> ') + '\n' + pad + '-> done';
122
+ }
123
+ function formatTransform(t) {
124
+ switch (t.type) {
125
+ case 'select': return `select(${t.fields.join(', ')})`;
126
+ case 'drop': return `drop(${t.field})`;
127
+ case 'compact': return 'compact';
128
+ case 'truncate': return `truncate(${t.tokens})`;
129
+ case 'filter': return `filter(${t.field}, ${formatExpr(t.condition)})`;
130
+ }
131
+ }
132
+ function formatContextRef(ref) {
133
+ if (ref.field && ref.field.length === 1) {
134
+ return `${ref.context}.${ref.field[0]}`;
135
+ }
136
+ if (ref.field && ref.field.length > 1) {
137
+ return `${ref.context}.{${ref.field.join(', ')}}`;
138
+ }
139
+ return ref.context;
140
+ }
141
+ function formatFailure(f) {
142
+ switch (f.type) {
143
+ case 'retry': return `retry(${f.max})`;
144
+ case 'fallback': return `fallback(${f.node})`;
145
+ case 'retry_then_fallback': return `retry(${f.max}, fallback: ${f.node})`;
146
+ case 'skip': return 'skip';
147
+ case 'abort': return 'abort';
148
+ }
149
+ }
150
+ function formatType(t) {
151
+ switch (t.kind) {
152
+ case 'primitive': return t.name;
153
+ case 'primitive_range': return `${t.name}(${t.min}..${t.max})`;
154
+ case 'list': return `List<${formatType(t.element)}>`;
155
+ case 'map': return `Map<${formatType(t.key)}, ${formatType(t.value)}>`;
156
+ case 'optional': return `Optional<${formatType(t.inner)}>`;
157
+ case 'token_bounded': return `TokenBounded<${formatType(t.inner)}, ${t.max}>`;
158
+ case 'enum': return `enum(${t.values.join(', ')})`;
159
+ case 'struct': {
160
+ const fields = t.fields.map(f => `${f.name}: ${formatType(f.type)}`).join(', ');
161
+ return `${t.name} { ${fields} }`;
162
+ }
163
+ case 'domain': return t.name;
164
+ }
165
+ }
166
+ function formatTokens(n) {
167
+ if (n >= 1000 && n % 1000 === 0)
168
+ return `${n / 1000}k`;
169
+ return String(n);
170
+ }
@@ -0,0 +1,28 @@
1
+ import { GraftError } from './errors/diagnostics.js';
2
+ /** Function that calls the LLM. Injectable for testing. */
3
+ export type LLMCaller = (params: {
4
+ system: string;
5
+ userMessage: string;
6
+ }) => Promise<string>;
7
+ export interface GenerateOptions {
8
+ output?: string;
9
+ /** Override the LLM caller (for testing). */
10
+ llmCaller?: LLMCaller;
11
+ }
12
+ export interface GenerateResult {
13
+ source: string;
14
+ errors: GraftError[];
15
+ }
16
+ /**
17
+ * Extract .gft source from an LLM response.
18
+ * Multi-strategy: (1) ```gft fence, (2) any fence, (3) bare response.
19
+ */
20
+ export declare function extractGftSource(response: string): string;
21
+ /**
22
+ * Build the system prompt for .gft generation.
23
+ */
24
+ export declare function buildSystemPrompt(): string;
25
+ /**
26
+ * Generate a .gft file from a natural language description.
27
+ */
28
+ export declare function generateGft(description: string, options?: GenerateOptions): Promise<GenerateResult>;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * graft generate — natural language to .gft pipeline generation via Claude Code subprocess.
3
+ */
4
+ import { spawnClaude } from './runtime/subprocess.js';
5
+ import { compileToProgram } from './compiler.js';
6
+ import { GraftError } from './errors/diagnostics.js';
7
+ const SYSTEM_PROMPT = `You are a Graft (.gft) pipeline generator. Generate valid .gft source code based on the user's description.
8
+
9
+ ## .gft Syntax Reference
10
+
11
+ ### Context Declaration
12
+ \`\`\`
13
+ context <Name>(max_tokens: <N>) {
14
+ <fieldName>: <Type>
15
+ ...
16
+ }
17
+ \`\`\`
18
+ Types: String, Int, Float, Bool, List<T>, Map<K,V>, Optional<T>
19
+
20
+ ### Memory Declaration
21
+ \`\`\`
22
+ memory <Name>(max_tokens: <N>, storage: file) {
23
+ <fieldName>: <Type>
24
+ ...
25
+ }
26
+ \`\`\`
27
+
28
+ ### Node Declaration
29
+ \`\`\`
30
+ node <Name>(model: <model>, budget: <in>/<out>) {
31
+ reads: [<ContextOrProducesName>, ...]
32
+
33
+ produces <OutputName> {
34
+ <fieldName>: <Type>
35
+ ...
36
+ }
37
+ }
38
+ \`\`\`
39
+ Models: haiku, sonnet, opus. Budget format: input/output in token shorthand (e.g., 4k/2k, 8k/4k, 12k/6k).
40
+
41
+ ### Edge Declaration
42
+ \`\`\`
43
+ // Direct edge with transforms
44
+ edge <Source> -> <Target>
45
+ | select(<field1>, <field2>)
46
+ | compact
47
+ | filter(<field> <op> <value>)
48
+ | truncate(<N>)
49
+ | drop(<field>)
50
+
51
+ // Conditional edge
52
+ edge <Source> -> {
53
+ when <condition> -> <Target>
54
+ when <condition> -> <Target>
55
+ otherwise -> <Target>
56
+ }
57
+ \`\`\`
58
+
59
+ ### Graph Declaration
60
+ \`\`\`
61
+ graph <Name>(input: <Context>, output: <Produces>, budget: <N>) {
62
+ // Sequential
63
+ <Node1> -> <Node2> -> done
64
+
65
+ // Parallel
66
+ parallel {
67
+ <Node1>
68
+ <Node2>
69
+ }
70
+ -> <Node3> -> done
71
+
72
+ // Foreach
73
+ foreach(<Source>.<field> as <var>, max_iterations: <N>) {
74
+ <Node> -> done
75
+ }
76
+ }
77
+ \`\`\`
78
+
79
+ ## Rules
80
+ - Output ONLY valid .gft code inside a \`\`\`gft fenced block
81
+ - Do NOT use import statements
82
+ - Every node must have model, budget (in/out), reads, and produces with typed fields
83
+ - Every graph must declare input, output, and budget
84
+ - Use realistic token budgets: haiku 4k/2k, sonnet 8k/4k, opus 12k/6k
85
+ - Add edge transforms (select, compact) to reduce token flow between nodes
86
+ - Add comments to explain the pipeline
87
+
88
+ ## Complete Example
89
+
90
+ \`\`\`gft
91
+ // Adversarial Code Review Pipeline
92
+ // Security + Performance + Logic reviewers challenge each other,
93
+ // then a senior reviewer makes the final call.
94
+
95
+ context PullRequest(max_tokens: 3k) {
96
+ diff: String
97
+ description: String
98
+ files_changed: List<String>
99
+ }
100
+
101
+ node SecurityReviewer(model: sonnet, budget: 6k/3k) {
102
+ reads: [PullRequest]
103
+
104
+ produces SecurityAnalysis {
105
+ vulnerabilities: List<String>
106
+ severity: String
107
+ recommendation: String
108
+ }
109
+ }
110
+
111
+ node LogicReviewer(model: sonnet, budget: 6k/3k) {
112
+ reads: [PullRequest]
113
+
114
+ produces LogicAnalysis {
115
+ bugs: List<String>
116
+ edge_cases: List<String>
117
+ correctness: String
118
+ }
119
+ }
120
+
121
+ node PerformanceReviewer(model: haiku, budget: 4k/2k) {
122
+ reads: [PullRequest]
123
+
124
+ produces PerfAnalysis {
125
+ hotspots: List<String>
126
+ complexity_concerns: List<String>
127
+ impact: String
128
+ }
129
+ }
130
+
131
+ node SeniorReviewer(model: opus, budget: 10k/5k) {
132
+ reads: [PullRequest, SecurityAnalysis, LogicAnalysis, PerfAnalysis]
133
+
134
+ produces FinalReview {
135
+ approved: Bool
136
+ blocking_issues: List<String>
137
+ suggestions: List<String>
138
+ summary: String
139
+ }
140
+ }
141
+
142
+ edge SecurityReviewer -> SeniorReviewer
143
+ | select(vulnerabilities, severity)
144
+ | compact
145
+
146
+ edge LogicReviewer -> SeniorReviewer
147
+ | select(bugs, edge_cases)
148
+ | compact
149
+
150
+ edge PerformanceReviewer -> SeniorReviewer
151
+ | select(hotspots, complexity_concerns)
152
+ | compact
153
+
154
+ graph AdversarialReview(input: PullRequest, output: FinalReview, budget: 40k) {
155
+ parallel {
156
+ SecurityReviewer
157
+ LogicReviewer
158
+ PerformanceReviewer
159
+ }
160
+ -> SeniorReviewer -> done
161
+ }
162
+ \`\`\`
163
+ `;
164
+ const MAX_RETRIES = 2;
165
+ /**
166
+ * Extract .gft source from an LLM response.
167
+ * Multi-strategy: (1) ```gft fence, (2) any fence, (3) bare response.
168
+ */
169
+ export function extractGftSource(response) {
170
+ // Strategy 1: ```gft ... ```
171
+ const gftFence = response.match(/```gft\s*\n([\s\S]*?)```/);
172
+ if (gftFence)
173
+ return gftFence[1].trim();
174
+ // Strategy 2: any ``` ... ```
175
+ const anyFence = response.match(/```\s*\n([\s\S]*?)```/);
176
+ if (anyFence)
177
+ return anyFence[1].trim();
178
+ // Strategy 3: bare response (strip obvious markdown)
179
+ return response.replace(/^#+\s.*$/gm, '').trim();
180
+ }
181
+ /**
182
+ * Build the system prompt for .gft generation.
183
+ */
184
+ export function buildSystemPrompt() {
185
+ return SYSTEM_PROMPT;
186
+ }
187
+ /**
188
+ * Validate generated .gft source using the Graft compiler.
189
+ */
190
+ function validateSource(source) {
191
+ try {
192
+ const result = compileToProgram(source, 'generated.gft');
193
+ if (!result.success)
194
+ return result.errors;
195
+ return [];
196
+ }
197
+ catch (e) {
198
+ const msg = e instanceof Error ? e.message : String(e);
199
+ return [new GraftError(msg, { line: 0, column: 0, offset: 0 }, 'error')];
200
+ }
201
+ }
202
+ /**
203
+ * Default LLM caller using Claude Code subprocess.
204
+ */
205
+ function createDefaultCaller() {
206
+ return async ({ system, userMessage }) => {
207
+ const prompt = `${system}\n\n---\n\nUser request: ${userMessage}`;
208
+ const result = await spawnClaude({
209
+ args: ['--print', '--output-format', 'text', prompt],
210
+ cwd: process.cwd(),
211
+ timeoutMs: 120_000,
212
+ });
213
+ if (result.exitCode !== 0) {
214
+ throw new Error(`Claude Code exited with code ${result.exitCode}.\n` +
215
+ `Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code\n` +
216
+ (result.stderr ? `stderr: ${result.stderr.slice(0, 500)}` : ''));
217
+ }
218
+ return result.stdout;
219
+ };
220
+ }
221
+ /**
222
+ * Generate a .gft file from a natural language description.
223
+ */
224
+ export async function generateGft(description, options) {
225
+ if (!description.trim()) {
226
+ throw new Error('Description cannot be empty.');
227
+ }
228
+ const callLLM = options?.llmCaller ?? createDefaultCaller();
229
+ let bestSource = '';
230
+ let bestErrors = [];
231
+ let userMessage = description;
232
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
233
+ const text = await callLLM({ system: SYSTEM_PROMPT, userMessage });
234
+ const source = extractGftSource(text);
235
+ const errors = validateSource(source);
236
+ if (errors.length === 0) {
237
+ return { source, errors: [] };
238
+ }
239
+ // Track best attempt (fewest errors)
240
+ if (bestSource === '' || errors.length < bestErrors.length) {
241
+ bestSource = source;
242
+ bestErrors = errors;
243
+ }
244
+ // Build retry prompt with error feedback
245
+ if (attempt < MAX_RETRIES) {
246
+ const errorMessages = errors
247
+ .map(e => e.format(source, 'generated.gft'))
248
+ .join('\n');
249
+ userMessage =
250
+ `${description}\n\n` +
251
+ `Your previous attempt had these compilation errors. Fix them:\n\n${errorMessages}`;
252
+ }
253
+ }
254
+ return { source: bestSource, errors: bestErrors };
255
+ }