@jsleekr/graft 5.7.2 → 6.0.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.
@@ -163,3 +163,86 @@ function filterToJs(t) {
163
163
  const op = condition.op === '==' ? '===' : condition.op === '!=' ? '!==' : condition.op;
164
164
  return `result[${JSON.stringify(field)}] = (result[${JSON.stringify(field)}] || []).filter(item => item[${JSON.stringify(fieldName)}] ${op} ${valueStr});`;
165
165
  }
166
+ /**
167
+ * Generate a router hook for conditional edges.
168
+ * Evaluates branch conditions and writes routing decision.
169
+ */
170
+ export function generateConditionalHook(edge) {
171
+ if (edge.target.kind !== 'conditional')
172
+ return null;
173
+ const source = edge.source.toLowerCase();
174
+ const branches = edge.target.branches;
175
+ const targets = branches.map(b => b.target).filter(t => t !== 'done');
176
+ const conditionCode = branchesToJs(branches);
177
+ return `#!/usr/bin/env node
178
+ // Auto-generated by Graft Compiler
179
+ // Conditional routing: ${edge.source} -> {${branches.map(b => b.target).join(', ')}}
180
+
181
+ const fs = require('fs');
182
+ const path = require('path');
183
+
184
+ const INPUT = path.resolve('.graft/session/node_outputs/${source}.json');
185
+ const ROUTE = path.resolve('.graft/session/routing/${source}_route.json');
186
+
187
+ if (!fs.existsSync(INPUT)) {
188
+ process.exit(0);
189
+ }
190
+
191
+ const data = JSON.parse(fs.readFileSync(INPUT, 'utf-8'));
192
+
193
+ ${conditionCode}
194
+
195
+ fs.mkdirSync(path.dirname(ROUTE), { recursive: true });
196
+ fs.writeFileSync(ROUTE, JSON.stringify({ target, from: ${JSON.stringify(edge.source)} }, null, 2));
197
+ `;
198
+ }
199
+ function branchesToJs(branches) {
200
+ const lines = [];
201
+ let first = true;
202
+ for (const branch of branches) {
203
+ if (!branch.condition) {
204
+ // else branch
205
+ if (first) {
206
+ lines.push(`let target = ${JSON.stringify(branch.target)};`);
207
+ }
208
+ else {
209
+ lines.push(`} else {`);
210
+ lines.push(` target = ${JSON.stringify(branch.target)};`);
211
+ lines.push(`}`);
212
+ }
213
+ }
214
+ else {
215
+ const jsCondition = exprToJs(branch.condition);
216
+ if (first) {
217
+ lines.push(`let target = null;`);
218
+ lines.push(`if (${jsCondition}) {`);
219
+ lines.push(` target = ${JSON.stringify(branch.target)};`);
220
+ first = false;
221
+ }
222
+ else {
223
+ lines.push(`} else if (${jsCondition}) {`);
224
+ lines.push(` target = ${JSON.stringify(branch.target)};`);
225
+ }
226
+ }
227
+ }
228
+ // Close the if chain if we have conditions but no else
229
+ if (!first && !branches.some(b => !b.condition)) {
230
+ lines.push(`}`);
231
+ }
232
+ return lines.join('\n');
233
+ }
234
+ function exprToJs(expr) {
235
+ if (expr.kind === 'binary') {
236
+ const left = exprToJs(expr.left);
237
+ const right = exprToJs(expr.right);
238
+ const op = expr.op === '==' ? '===' : expr.op === '!=' ? '!==' : expr.op;
239
+ return `${left} ${op} ${right}`;
240
+ }
241
+ if (expr.kind === 'field_access') {
242
+ return `data[${JSON.stringify(expr.segments[0])}]`;
243
+ }
244
+ if (expr.kind === 'literal') {
245
+ return JSON.stringify(expr.value);
246
+ }
247
+ return formatExpr(expr);
248
+ }
@@ -7,12 +7,16 @@ export function generateOrchestration(program, report) {
7
7
  const index = new ProgramIndex(program);
8
8
  const memoryNames = new Set(program.memories.map(m => m.name));
9
9
  const edgeMap = new Map();
10
+ const conditionalEdgeMap = new Map();
10
11
  for (const edge of program.edges) {
11
12
  if (edge.target.kind === 'direct' && edge.transforms.length > 0) {
12
13
  edgeMap.set(`${edge.source}->${edge.target.node}`, { transforms: edge.transforms });
13
14
  }
15
+ else if (edge.target.kind === 'conditional') {
16
+ conditionalEdgeMap.set(edge.source, edge);
17
+ }
14
18
  }
15
- const { text: steps } = generateSteps(graph.flow, report, edgeMap, 1, null, index.nodeMap, memoryNames);
19
+ const { text: steps } = generateSteps(graph.flow, report, edgeMap, conditionalEdgeMap, 1, null, index.nodeMap, memoryNames);
16
20
  // Memory preamble
17
21
  const memorySection = program.memories.length > 0
18
22
  ? `
@@ -70,7 +74,42 @@ function describeTransforms(transforms) {
70
74
  }
71
75
  return parts.join(', then ');
72
76
  }
73
- function generateSteps(flow, report, edgeMap, startStep, prevNode, nodeMap, memoryNames) {
77
+ function describeCondition(expr) {
78
+ if (expr.kind === 'binary') {
79
+ const left = expr.left.kind === 'field_access' ? `\`${expr.left.segments[0]}\`` : formatExpr(expr.left);
80
+ const right = expr.right.kind === 'literal' ? `\`${expr.right.value}\`` : formatExpr(expr.right);
81
+ return `${left} ${expr.op} ${right}`;
82
+ }
83
+ return formatExpr(expr);
84
+ }
85
+ function generateConditionalRoutingStep(stepNum, edge, nodeMap, report) {
86
+ if (edge.target.kind !== 'conditional')
87
+ return '';
88
+ const source = edge.source.toLowerCase();
89
+ const branches = edge.target.branches;
90
+ let text = `
91
+ ### Step ${stepNum}: Conditional routing from ${edge.source}
92
+ - **Automatic**: Router hook evaluates conditions on ${edge.source}'s output
93
+ - Routing file: \`.graft/session/routing/${source}_route.json\`
94
+ - Read the \`target\` field and proceed accordingly:
95
+ `;
96
+ for (const branch of branches) {
97
+ const label = branch.condition ? describeCondition(branch.condition) : 'else (default)';
98
+ if (branch.target === 'done') {
99
+ text += ` - If ${label}: **pipeline complete**\n`;
100
+ }
101
+ else {
102
+ const nodeReport = report.nodes.find(n => n.name === branch.target);
103
+ const tokenInfo = nodeReport
104
+ ? ` (tokens: input ~${nodeReport.estimatedIn.toLocaleString('en-US')} / output ~${nodeReport.estimatedOut.toLocaleString('en-US')})`
105
+ : '';
106
+ text += ` - If ${label}: run **${branch.target}** agent${tokenInfo}\n`;
107
+ }
108
+ }
109
+ text += `- Each branch agent reads from \`.graft/session/node_outputs/${source}.json\`\n`;
110
+ return text;
111
+ }
112
+ function generateSteps(flow, report, edgeMap, conditionalEdgeMap, startStep, prevNode, nodeMap, memoryNames) {
74
113
  let text = '';
75
114
  let stepNum = startStep;
76
115
  let prev = prevNode;
@@ -154,9 +193,15 @@ function generateSteps(flow, report, edgeMap, startStep, prevNode, nodeMap, memo
154
193
  - Completion: \`===NODE_COMPLETE:${lowerName}===\`
155
194
  - Output: \`.graft/session/node_outputs/${lowerName}.json\`
156
195
  `;
196
+ stepNum++;
197
+ // Conditional routing after this node
198
+ const condEdge = conditionalEdgeMap.get(step.name);
199
+ if (condEdge && condEdge.target.kind === 'conditional') {
200
+ text += generateConditionalRoutingStep(stepNum, condEdge, nodeMap, report);
201
+ stepNum++;
202
+ }
157
203
  prev = step.name;
158
204
  prevParallelBranches = [];
159
- stepNum++;
160
205
  break;
161
206
  }
162
207
  case 'parallel': {
@@ -42,17 +42,25 @@ export function generateSettings(program, sourceFile, index) {
42
42
  // Collect all hook commands, then merge into a single "Write" matcher entry
43
43
  const hookCommands = [];
44
44
  for (const edge of program.edges) {
45
- if (edge.transforms.length === 0)
46
- continue;
47
- if (edge.target.kind !== 'direct')
48
- continue;
49
- const source = edge.source.toLowerCase();
50
- const target = edge.target.node.toLowerCase();
51
- hookCommands.push({
52
- type: 'command',
53
- command: `node .claude/hooks/${source}-to-${target}.js`,
54
- if: `Write(.graft/session/node_outputs/${source}.json)`,
55
- });
45
+ if (edge.target.kind === 'conditional') {
46
+ // Conditional edge → router hook
47
+ const source = edge.source.toLowerCase();
48
+ hookCommands.push({
49
+ type: 'command',
50
+ command: `node .claude/hooks/${source}-router.js`,
51
+ if: `Write(.graft/session/node_outputs/${source}.json)`,
52
+ });
53
+ }
54
+ else if (edge.target.kind === 'direct' && edge.transforms.length > 0) {
55
+ // Direct edge with transforms → transform hook
56
+ const source = edge.source.toLowerCase();
57
+ const target = edge.target.node.toLowerCase();
58
+ hookCommands.push({
59
+ type: 'command',
60
+ command: `node .claude/hooks/${source}-to-${target}.js`,
61
+ if: `Write(.graft/session/node_outputs/${source}.json)`,
62
+ });
63
+ }
56
64
  }
57
65
  const hookEntries = [];
58
66
  if (hookCommands.length > 0) {
@@ -3,6 +3,7 @@ import { GeneratedFile } from './codegen/codegen.js';
3
3
  import { GraftError } from './errors/diagnostics.js';
4
4
  import { Program } from './parser/ast.js';
5
5
  import { ProgramIndex } from './program-index.js';
6
+ import { CodegenBackend } from './codegen/backend.js';
6
7
  export interface ProgramResult {
7
8
  success: boolean;
8
9
  program?: Program;
@@ -15,7 +16,7 @@ export interface CompileResult extends ProgramResult {
15
16
  files?: GeneratedFile[];
16
17
  }
17
18
  export declare function compileToProgram(source: string, sourceFile: string): ProgramResult;
18
- export declare function compileAndGenerate(source: string, sourceFile: string): CompileResult;
19
+ export declare function compileAndGenerate(source: string, sourceFile: string, backend?: CodegenBackend): CompileResult;
19
20
  /** Backward-compatible alias for compileAndGenerate */
20
- export declare function compile(source: string, sourceFile: string): CompileResult;
21
- export declare function compileAndWrite(source: string, sourceFile: string, outDir: string): CompileResult;
21
+ export declare function compile(source: string, sourceFile: string, backend?: CodegenBackend): CompileResult;
22
+ export declare function compileAndWrite(source: string, sourceFile: string, outDir: string, backend?: CodegenBackend): CompileResult;
package/dist/compiler.js CHANGED
@@ -68,7 +68,7 @@ export function compileToProgram(source, sourceFile) {
68
68
  warnings.push(...report.warnings);
69
69
  return { success: true, program, index, report, errors, warnings };
70
70
  }
71
- export function compileAndGenerate(source, sourceFile) {
71
+ export function compileAndGenerate(source, sourceFile, backend) {
72
72
  const result = compileToProgram(source, sourceFile);
73
73
  if (!result.success || !result.program) {
74
74
  return result;
@@ -85,15 +85,15 @@ export function compileAndGenerate(source, sourceFile) {
85
85
  };
86
86
  }
87
87
  // Generate
88
- const files = generate(result.program, result.report, sourceFile, result.index);
88
+ const files = generate(result.program, result.report, sourceFile, result.index, backend);
89
89
  return { ...result, files };
90
90
  }
91
91
  /** Backward-compatible alias for compileAndGenerate */
92
- export function compile(source, sourceFile) {
93
- return compileAndGenerate(source, sourceFile);
92
+ export function compile(source, sourceFile, backend) {
93
+ return compileAndGenerate(source, sourceFile, backend);
94
94
  }
95
- export function compileAndWrite(source, sourceFile, outDir) {
96
- const result = compile(source, sourceFile);
95
+ export function compileAndWrite(source, sourceFile, outDir, backend) {
96
+ const result = compile(source, sourceFile, backend);
97
97
  if (result.success && result.files) {
98
98
  writeFiles(result.files, outDir);
99
99
  }
@@ -16,6 +16,12 @@ export declare class GraftError extends Error {
16
16
  readonly location: SourceLocation;
17
17
  readonly severity: 'error' | 'warning';
18
18
  readonly code?: GraftErrorCode | undefined;
19
- constructor(message: string, location: SourceLocation, severity?: 'error' | 'warning', code?: GraftErrorCode | undefined);
20
- format(source: string): string;
19
+ readonly help?: string | undefined;
20
+ constructor(message: string, location: SourceLocation, severity?: 'error' | 'warning', code?: GraftErrorCode | undefined, help?: string | undefined);
21
+ format(source: string, filename?: string): string;
21
22
  }
23
+ /**
24
+ * Find the closest match to `name` from `candidates` using Levenshtein distance.
25
+ * Returns the best match if distance <= maxDistance, otherwise undefined.
26
+ */
27
+ export declare function didYouMean(name: string, candidates: string[], maxDistance?: number): string | undefined;
@@ -2,24 +2,81 @@ export class GraftError extends Error {
2
2
  location;
3
3
  severity;
4
4
  code;
5
- constructor(message, location, severity = 'error', code) {
5
+ help;
6
+ constructor(message, location, severity = 'error', code, help) {
6
7
  super(message);
7
8
  this.location = location;
8
9
  this.severity = severity;
9
10
  this.code = code;
11
+ this.help = help;
10
12
  this.name = 'GraftError';
11
13
  }
12
- format(source) {
14
+ format(source, filename) {
13
15
  const lines = source.split('\n');
14
16
  const lineIdx = this.location.line - 1;
15
17
  const line = (lineIdx >= 0 && lineIdx < lines.length) ? lines[lineIdx] : '';
16
18
  const col = Math.max(0, this.location.column - 1);
17
- const pointer = ' '.repeat(col) + '^';
18
- return [
19
- `Error at line ${this.location.line}:${this.location.column}:`,
20
- ` ${line}`,
21
- ` ${pointer}`,
22
- ` ${this.message}`,
23
- ].join('\n');
19
+ const lineNumStr = String(this.location.line);
20
+ const gutter = ' '.repeat(lineNumStr.length);
21
+ // Underline: use length if available, otherwise single caret
22
+ const underlineLen = this.location.length && this.location.length > 0
23
+ ? this.location.length
24
+ : 1;
25
+ const underline = '^'.repeat(underlineLen);
26
+ const label = this.severity === 'warning' ? 'warning' : 'error';
27
+ const codeStr = this.code ? `[${this.code}]` : '';
28
+ const file = filename || '<source>';
29
+ const result = [
30
+ `${label}${codeStr}: ${this.message}`,
31
+ ` ${gutter}--> ${file}:${this.location.line}:${this.location.column}`,
32
+ ` ${gutter} |`,
33
+ ` ${lineNumStr} | ${line}`,
34
+ ` ${gutter} | ${' '.repeat(col)}${underline}`,
35
+ ];
36
+ if (this.help) {
37
+ result.push(` ${gutter} |`);
38
+ result.push(` ${gutter} = help: ${this.help}`);
39
+ }
40
+ return result.join('\n');
24
41
  }
25
42
  }
43
+ /**
44
+ * Find the closest match to `name` from `candidates` using Levenshtein distance.
45
+ * Returns the best match if distance <= maxDistance, otherwise undefined.
46
+ */
47
+ export function didYouMean(name, candidates, maxDistance = 3) {
48
+ let best;
49
+ let bestDist = maxDistance + 1;
50
+ for (const candidate of candidates) {
51
+ const dist = levenshtein(name.toLowerCase(), candidate.toLowerCase());
52
+ if (dist < bestDist) {
53
+ bestDist = dist;
54
+ best = candidate;
55
+ }
56
+ }
57
+ return bestDist <= maxDistance ? best : undefined;
58
+ }
59
+ function levenshtein(a, b) {
60
+ const m = a.length;
61
+ const n = b.length;
62
+ if (m === 0)
63
+ return n;
64
+ if (n === 0)
65
+ return m;
66
+ // Single-row DP
67
+ let prev = new Array(n + 1);
68
+ let curr = new Array(n + 1);
69
+ for (let j = 0; j <= n; j++)
70
+ prev[j] = j;
71
+ for (let i = 1; i <= m; i++) {
72
+ curr[0] = i;
73
+ for (let j = 1; j <= n; j++) {
74
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
75
+ curr[j] = Math.min(curr[j - 1] + 1, // insert
76
+ prev[j] + 1, // delete
77
+ prev[j - 1] + cost);
78
+ }
79
+ [prev, curr] = [curr, prev];
80
+ }
81
+ return prev[n];
82
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Graft AST pretty-printer — formats a Program back to .gft source.
3
+ */
4
+ import { Program } from './parser/ast.js';
5
+ export declare function formatProgram(program: Program): string;
@@ -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>;