@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JSLEEKR
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ [![npm version](https://img.shields.io/npm/v/@graft-lang/graft.svg)](https://www.npmjs.com/package/@graft-lang/graft)
2
+ [![CI](https://github.com/JSLEEKR/graft/actions/workflows/ci.yml/badge.svg)](https://github.com/JSLEEKR/graft/actions/workflows/ci.yml)
3
+ [![Node.js](https://img.shields.io/node/v/@graft-lang/graft.svg)](https://nodejs.org)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
5
+
6
+ # Graft
7
+
8
+ **A graph-native language for AI agent pipelines.**
9
+
10
+ Graft compiles `.gft` files into [Claude Code](https://docs.anthropic.com/en/docs/claude-code) harness structures. Define your multi-agent pipeline declaratively, and the compiler generates the agents, hooks, orchestration, and settings — with compile-time token budget analysis.
11
+
12
+ ## Quick Start
13
+
14
+ ```bash
15
+ npm install -g @graft-lang/graft
16
+ ```
17
+
18
+ Write a pipeline (`hello.gft`):
19
+
20
+ ```graft
21
+ context UserRequest(max_tokens: 500) {
22
+ question: String
23
+ }
24
+
25
+ node Researcher(model: sonnet, budget: 2k/1k) {
26
+ reads: [UserRequest]
27
+ produces Research {
28
+ findings: List<String>
29
+ confidence: Float(0..1)
30
+ }
31
+ }
32
+
33
+ node Writer(model: haiku, budget: 1500/800) {
34
+ reads: [Research.findings]
35
+ produces Answer { response: String }
36
+ }
37
+
38
+ edge Researcher -> Writer | select(findings) | compact
39
+
40
+ graph SimpleQA(input: UserRequest, output: Answer, budget: 6k) {
41
+ Researcher -> Writer -> done
42
+ }
43
+ ```
44
+
45
+ Compile it:
46
+
47
+ ```
48
+ $ graft compile hello.gft
49
+
50
+ ✓ Parse OK
51
+ ✓ Scope check OK
52
+ ✓ Type check OK
53
+ ✓ Token analysis:
54
+ Researcher in ~ 500 out ~ 1,000
55
+ Writer in ~ 63 out ~ 800
56
+ Best path: 2,363 tokens ✓ within budget (6,000)
57
+
58
+ Generated:
59
+ .claude/agents/researcher.md ← agent definition
60
+ .claude/agents/writer.md ← agent definition
61
+ .claude/hooks/researcher-to-writer.js ← edge transform (Node.js)
62
+ .claude/CLAUDE.md ← orchestration plan
63
+ .claude/settings.json ← model routing + hooks
64
+ ```
65
+
66
+ Open the project directory in Claude Code — it picks up the generated `.claude/` structure and runs the pipeline.
67
+
68
+ ## What Does Graft Generate?
69
+
70
+ | Graft Source | Generated Output | Purpose |
71
+ |-------------|-----------------|---------|
72
+ | `node` | `.claude/agents/*.md` | Agent with model, tools, output schema |
73
+ | `edge \| transform` | `.claude/hooks/*.js` | Node.js data transform between nodes |
74
+ | `graph` | `.claude/CLAUDE.md` | Step-by-step orchestration plan |
75
+ | `memory` | `.graft/memory/*.json` | Persistent state across runs |
76
+ | config | `.claude/settings.json` | Model routing, budget, hook registration |
77
+
78
+ ## Why Graft?
79
+
80
+ Multi-agent systems waste tokens passing full context between agents. Graft fixes this:
81
+
82
+ - **Edge transforms** extract only what the next agent needs (`select`, `drop`, `compact`, `filter`)
83
+ - **Compile-time token analysis** catches budget overruns before you spend API credits
84
+ - **Typed output schemas** enforce structured JSON communication between agents
85
+ - **Explicit `reads`** declarations prevent context leaks — the compiler verifies scope
86
+
87
+ ## Language Features
88
+
89
+ ### Contexts and Nodes
90
+
91
+ ```graft
92
+ context TaskSpec(max_tokens: 1k) {
93
+ description: String
94
+ criteria: List<String>
95
+ }
96
+
97
+ node Analyzer(model: sonnet, budget: 5k/2k) {
98
+ reads: [TaskSpec]
99
+ tools: [file_read, terminal]
100
+ on_failure: retry(2)
101
+ produces AnalysisResult {
102
+ issues: List<Issue { file: FilePath, severity: enum(low, medium, high) }>
103
+ risk_score: Float(0..1)
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Edge Transforms
109
+
110
+ ```graft
111
+ edge Analyzer -> Reviewer
112
+ | filter(issues, severity >= medium)
113
+ | drop(reasoning_trace)
114
+ | compact
115
+ ```
116
+
117
+ ### Flow Control
118
+
119
+ ```graft
120
+ graph Pipeline(input: TaskSpec, output: Report, budget: 35k) {
121
+ Planner
122
+ -> parallel { SecurityReviewer PerformanceReviewer StyleReviewer }
123
+ -> Aggregator -> done
124
+ }
125
+ ```
126
+
127
+ Also supports: `foreach`, `let` variables with expressions, parameterized sub-graphs, conditional edge routing.
128
+
129
+ ### Imports and Memory
130
+
131
+ ```graft
132
+ import { UserMessage, SystemConfig } from "./shared.gft"
133
+
134
+ memory ConversationLog(max_tokens: 2k, storage: file) {
135
+ turns: List<Turn { role: String, content: String }>
136
+ summary: Optional<String>
137
+ }
138
+
139
+ node Responder(model: sonnet, budget: 4k/2k) {
140
+ reads: [UserMessage, SystemConfig, ConversationLog]
141
+ writes: [ConversationLog.turns]
142
+ produces Response { reply: String }
143
+ }
144
+ ```
145
+
146
+ ### Expressions
147
+
148
+ ```graft
149
+ A -> let score = A.risk_score * 2
150
+ -> let label = "Found ${len(A.items)} items"
151
+ -> let safe = score >= 50 && score <= 100
152
+ -> let fallback = A.name ?? "unknown"
153
+ -> B -> done
154
+ ```
155
+
156
+ Supported: arithmetic (`+`, `-`, `*`, `/`, `%`), comparison (`>`, `>=`, `==`, `!=`), logical (`&&`, `||`), null coalescing (`??`), string interpolation, builtins (`len`, `max`, `min`, `abs`, `round`, `keys`, `str`).
157
+
158
+ ### Type System
159
+
160
+ ```
161
+ String, Int, Float, Float(0..1), Bool // primitives
162
+ List<T>, Map<K, V>, Optional<T> // collections
163
+ TokenBounded<String, 100> // token-bounded types
164
+ enum(low, medium, high) // inline enums
165
+ Issue { file: FilePath, severity: ... } // inline structs
166
+ ```
167
+
168
+ ## CLI
169
+
170
+ ```bash
171
+ graft compile <file.gft> [--out-dir <dir>] # Compile to .claude/ structure
172
+ graft check <file.gft> # Parse + analyze only
173
+ graft run <file.gft> --input <json> [--dry-run] [--verbose] # Compile and execute
174
+ graft init <name> # Scaffold a new project
175
+ ```
176
+
177
+ ## Editor Support
178
+
179
+ ### VS Code
180
+
181
+ The [Graft VS Code extension](editors/vscode/) provides:
182
+ - Syntax highlighting (TextMate grammar)
183
+ - Real-time diagnostics
184
+ - Hover information (types, token budgets)
185
+ - Go-to-definition, find references, rename
186
+ - Completions (keywords, declarations, imports)
187
+ - Code actions (auto-import)
188
+ - Document symbols
189
+
190
+ ## Programmatic API
191
+
192
+ ```typescript
193
+ import { compileToProgram, compile } from '@graft-lang/graft/compiler';
194
+ import { Executor } from '@graft-lang/graft/runtime';
195
+ import type { Program, GraftErrorCode } from '@graft-lang/graft/types';
196
+
197
+ const result = compileToProgram(source, 'pipeline.gft');
198
+ if (result.success) {
199
+ console.log(`Parsed ${result.program.nodes.length} nodes`);
200
+ }
201
+ ```
202
+
203
+ ## Development
204
+
205
+ ```bash
206
+ git clone https://github.com/JSLEEKR/graft.git
207
+ cd graft && npm install
208
+ npm run build # Compile TypeScript
209
+ npm test # Run all 1,344 tests
210
+ ```
211
+
212
+ ## Version History
213
+
214
+ | Version | Highlights |
215
+ |---------|-----------|
216
+ | **v5.3** | Parallel codegen test coverage, hook entry merging |
217
+ | **v5.2** | Parallel→sequential edge transforms, agent input overrides, graceful hooks |
218
+ | **v5.1** | Claude Code compatibility, `graft init`, CI/CD, README rewrite |
219
+ | **v5.0** | Condition-to-Expr AST unification, strict equality, codegen expression display |
220
+ | **v4.9** | Codegen expression display |
221
+ | **v4.8** | LSP expression intelligence — hover, go-to-def, completions |
222
+ | **v4.7** | Null coalescing (`??`), runtime expression hardening |
223
+ | **v4.6** | Logical operators (`&&`, `||`), conditional type warnings |
224
+ | **v4.5** | Comparison operators, conditional expressions |
225
+ | **v4.4** | String interpolation, expression extraction |
226
+ | **v4.0** | Variables, expressions, graph parameters, graph calls |
227
+ | **v3.0** | Pluggable codegen backends, field-level writes, failure strategies |
228
+ | **v2.0** | Import system, persistent memory |
229
+ | **v1.0** | Full compiler pipeline, CLI |
230
+
231
+ See [CHANGELOG.md](CHANGELOG.md) for full details.
232
+
233
+ ## License
234
+
235
+ MIT
@@ -0,0 +1,33 @@
1
+ import { Program } from '../parser/ast.js';
2
+ import { GraftError } from '../errors/diagnostics.js';
3
+ import { ProgramIndex } from '../program-index.js';
4
+ export interface NodeTokenReport {
5
+ name: string;
6
+ estimatedIn: number;
7
+ estimatedOut: number;
8
+ }
9
+ export interface TokenReport {
10
+ graphName: string;
11
+ budget: number;
12
+ bestCase: number;
13
+ worstCase: number;
14
+ nodes: NodeTokenReport[];
15
+ warnings: GraftError[];
16
+ }
17
+ export declare class TokenEstimator {
18
+ private program;
19
+ private index;
20
+ private nodeMap;
21
+ private edgeMap;
22
+ private conditionalEdges;
23
+ constructor(program: Program, index?: ProgramIndex);
24
+ estimate(): TokenReport;
25
+ private collectNodeReports;
26
+ private computeFlowCosts;
27
+ private getConditionalBranchCosts;
28
+ private getNodeCost;
29
+ private getEstimatedIn;
30
+ private applyTransformReductions;
31
+ private getRetryMultiplier;
32
+ private getFallbackCost;
33
+ }
@@ -0,0 +1,273 @@
1
+ import { GraftError } from '../errors/diagnostics.js';
2
+ import { PARTIAL_FIELD_FACTOR, MAX_CONDITIONAL_HOPS } from '../constants.js';
3
+ import { ProgramIndex } from '../program-index.js';
4
+ export class TokenEstimator {
5
+ program;
6
+ index;
7
+ nodeMap;
8
+ edgeMap; // "source->target" key
9
+ conditionalEdges; // source -> branches
10
+ constructor(program, index) {
11
+ this.program = program;
12
+ this.index = index ?? new ProgramIndex(program);
13
+ this.nodeMap = this.index.nodeMap;
14
+ this.edgeMap = new Map();
15
+ this.conditionalEdges = new Map();
16
+ for (const edge of program.edges) {
17
+ if (edge.target.kind === 'direct') {
18
+ this.edgeMap.set(`${edge.source}->${edge.target.node}`, edge);
19
+ }
20
+ else {
21
+ this.conditionalEdges.set(edge.source, edge.target.branches);
22
+ }
23
+ }
24
+ }
25
+ estimate() {
26
+ const graph = this.program.graphs[0]; // v1: single graph
27
+ if (!graph) {
28
+ return { graphName: '', budget: 0, bestCase: 0, worstCase: 0, nodes: [], warnings: [] };
29
+ }
30
+ const warnings = [];
31
+ const nodeReports = [];
32
+ // Populate node reports (for display)
33
+ this.collectNodeReports(graph.flow, nodeReports, warnings);
34
+ // Compute best/worst case costs
35
+ const { best, worst } = this.computeFlowCosts(graph.flow, warnings);
36
+ const bestCase = best;
37
+ const worstCase = worst;
38
+ if (worstCase > graph.budget) {
39
+ warnings.push(new GraftError(`Worst-case token usage (${worstCase}) exceeds budget (${graph.budget})`, graph.location, 'warning', 'BUDGET_EXCEEDED'));
40
+ }
41
+ return {
42
+ graphName: graph.name,
43
+ budget: graph.budget,
44
+ bestCase,
45
+ worstCase,
46
+ nodes: nodeReports,
47
+ warnings,
48
+ };
49
+ }
50
+ collectNodeReports(steps, reports, warnings) {
51
+ for (const step of steps) {
52
+ switch (step.kind) {
53
+ case 'node': {
54
+ const node = this.nodeMap.get(step.name);
55
+ if (!node)
56
+ break;
57
+ const estimatedIn = this.getEstimatedIn(step.name, node);
58
+ if (estimatedIn > node.budgetIn) {
59
+ warnings.push(new GraftError(`Node '${step.name}' estimated input (${estimatedIn}) exceeds budgetIn (${node.budgetIn})`, node.location, 'warning', 'BUDGET_NODE_EXCEEDED'));
60
+ }
61
+ reports.push({ name: step.name, estimatedIn, estimatedOut: node.budgetOut });
62
+ break;
63
+ }
64
+ case 'parallel':
65
+ for (const branchName of step.branches) {
66
+ const node = this.nodeMap.get(branchName);
67
+ if (!node)
68
+ continue;
69
+ const estimatedIn = this.getEstimatedIn(branchName, node);
70
+ if (estimatedIn > node.budgetIn) {
71
+ warnings.push(new GraftError(`Node '${branchName}' estimated input (${estimatedIn}) exceeds budgetIn (${node.budgetIn})`, node.location, 'warning', 'BUDGET_NODE_EXCEEDED'));
72
+ }
73
+ reports.push({ name: branchName, estimatedIn, estimatedOut: node.budgetOut });
74
+ }
75
+ break;
76
+ case 'foreach':
77
+ this.collectNodeReports(step.body, reports, warnings);
78
+ break;
79
+ case 'let':
80
+ // Let bindings have no token cost
81
+ break;
82
+ case 'graph_call': {
83
+ // Recurse into called graph's flow for node reports
84
+ const calledGraph = this.index.graphMap.get(step.name);
85
+ if (calledGraph) {
86
+ this.collectNodeReports(calledGraph.flow, reports, warnings);
87
+ }
88
+ break;
89
+ }
90
+ default: {
91
+ const _exhaustive = step;
92
+ throw new Error(`Unhandled FlowNode kind: ${_exhaustive.kind}`);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ computeFlowCosts(steps, warnings) {
98
+ let best = 0;
99
+ let worst = 0;
100
+ for (const step of steps) {
101
+ switch (step.kind) {
102
+ case 'node': {
103
+ const node = this.nodeMap.get(step.name);
104
+ if (!node)
105
+ break;
106
+ const cost = this.getNodeCost(step.name, node);
107
+ const retryMul = this.getRetryMultiplier(node);
108
+ best += cost;
109
+ worst += cost * retryMul + this.getFallbackCost(node);
110
+ // Add conditional edge branch costs (multi-hop recursive)
111
+ const branchCosts = this.getConditionalBranchCosts(step.name, warnings, new Set([step.name]), 0);
112
+ best += branchCosts.best;
113
+ worst += branchCosts.worst;
114
+ break;
115
+ }
116
+ case 'parallel': {
117
+ // Parallel: all branches run. Total tokens = sum of all branches.
118
+ for (const branchName of step.branches) {
119
+ const node = this.nodeMap.get(branchName);
120
+ if (!node)
121
+ continue;
122
+ const cost = this.getNodeCost(branchName, node);
123
+ const retryMul = this.getRetryMultiplier(node);
124
+ best += cost;
125
+ worst += cost * retryMul + this.getFallbackCost(node);
126
+ }
127
+ break;
128
+ }
129
+ case 'foreach': {
130
+ // Foreach: body runs up to maxIterations times.
131
+ // Best case = 1 iteration. Worst case = maxIterations iterations.
132
+ const bodyCosts = this.computeFlowCosts(step.body, warnings);
133
+ best += bodyCosts.best * 1;
134
+ worst += bodyCosts.worst * step.maxIterations;
135
+ break;
136
+ }
137
+ case 'let':
138
+ // Let bindings have zero token cost
139
+ break;
140
+ case 'graph_call': {
141
+ // Graph call cost = called graph's flow cost
142
+ const calledGraph = this.index.graphMap.get(step.name);
143
+ if (calledGraph) {
144
+ const subCosts = this.computeFlowCosts(calledGraph.flow, warnings);
145
+ best += subCosts.best;
146
+ worst += subCosts.worst;
147
+ }
148
+ break;
149
+ }
150
+ default: {
151
+ const _exhaustive = step;
152
+ throw new Error(`Unhandled FlowNode kind: ${_exhaustive.kind}`);
153
+ }
154
+ }
155
+ }
156
+ return { best, worst };
157
+ }
158
+ getConditionalBranchCosts(source, warnings, visited, depth) {
159
+ const branches = this.conditionalEdges.get(source);
160
+ if (!branches)
161
+ return { best: 0, worst: 0 };
162
+ if (depth >= MAX_CONDITIONAL_HOPS) {
163
+ warnings.push(new GraftError(`Conditional chain from '${source}' exceeded maximum depth of ${MAX_CONDITIONAL_HOPS}`, { line: 0, column: 0, offset: 0 }, 'warning', 'BUDGET_CHAIN_DEPTH'));
164
+ return { best: 0, worst: 0 };
165
+ }
166
+ const branchBestCosts = [];
167
+ const branchWorstCosts = [];
168
+ for (const branch of branches) {
169
+ if (branch.target === 'done') {
170
+ branchBestCosts.push(0);
171
+ branchWorstCosts.push(0);
172
+ continue;
173
+ }
174
+ if (visited.has(branch.target)) {
175
+ // Cycle detected
176
+ warnings.push(new GraftError(`Conditional estimation cycle detected: ${[...visited, branch.target].join(' -> ')}`, { line: 0, column: 0, offset: 0 }, 'warning', 'BUDGET_CHAIN_CYCLE'));
177
+ branchBestCosts.push(0);
178
+ branchWorstCosts.push(0);
179
+ continue;
180
+ }
181
+ const node = this.nodeMap.get(branch.target);
182
+ if (!node)
183
+ continue; // unknown target, skip
184
+ const cost = this.getNodeCost(branch.target, node);
185
+ const retryMul = this.getRetryMultiplier(node);
186
+ // Per-branch visited set copy (handles diamonds correctly)
187
+ const branchVisited = new Set(visited);
188
+ branchVisited.add(branch.target);
189
+ const chainCosts = this.getConditionalBranchCosts(branch.target, warnings, branchVisited, depth + 1);
190
+ branchBestCosts.push(cost + chainCosts.best);
191
+ branchWorstCosts.push(cost * retryMul + chainCosts.worst);
192
+ }
193
+ if (branchBestCosts.length === 0)
194
+ return { best: 0, worst: 0 };
195
+ return { best: Math.min(...branchBestCosts), worst: Math.max(...branchWorstCosts) };
196
+ }
197
+ getNodeCost(nodeName, node) {
198
+ return this.getEstimatedIn(nodeName, node) + node.budgetOut;
199
+ }
200
+ getEstimatedIn(nodeName, node) {
201
+ let estimatedIn = 0;
202
+ for (const ref of node.reads) {
203
+ // If reading a context
204
+ const ctx = this.index.contextMap.get(ref.context);
205
+ if (ctx) {
206
+ estimatedIn += ref.field ? Math.floor(ctx.maxTokens * Math.min(PARTIAL_FIELD_FACTOR * ref.field.length, 1.0)) : ctx.maxTokens;
207
+ continue;
208
+ }
209
+ // If reading a memory
210
+ const mem = this.index.memoryMap.get(ref.context);
211
+ if (mem) {
212
+ estimatedIn += ref.field ? Math.floor(mem.maxTokens * Math.min(PARTIAL_FIELD_FACTOR * ref.field.length, 1.0)) : mem.maxTokens;
213
+ continue;
214
+ }
215
+ // If reading a produces output from upstream node
216
+ const sourceNode = this.index.producesNodeMap.get(ref.context);
217
+ if (sourceNode) {
218
+ let upstreamTokens = sourceNode.budgetOut;
219
+ // Check for edge transform reductions
220
+ const edgeKey = `${sourceNode.name}->${nodeName}`;
221
+ const edge = this.edgeMap.get(edgeKey);
222
+ if (edge) {
223
+ upstreamTokens = this.applyTransformReductions(upstreamTokens, edge.transforms);
224
+ }
225
+ estimatedIn += ref.field ? Math.floor(upstreamTokens * Math.min(PARTIAL_FIELD_FACTOR * ref.field.length, 1.0)) : upstreamTokens;
226
+ }
227
+ }
228
+ return estimatedIn;
229
+ }
230
+ applyTransformReductions(tokens, transforms) {
231
+ let result = tokens;
232
+ for (const t of transforms) {
233
+ switch (t.type) {
234
+ case 'select':
235
+ result = Math.floor(result * Math.min(PARTIAL_FIELD_FACTOR * t.fields.length, 1.0));
236
+ break;
237
+ case 'filter':
238
+ result = Math.floor(result * 0.5); // filter reduces ~50%
239
+ break;
240
+ case 'drop':
241
+ result = Math.floor(result * 0.85); // drop one field ~15% savings
242
+ break;
243
+ case 'compact':
244
+ result = Math.floor(result * 0.7); // compact ~30% reduction
245
+ break;
246
+ case 'truncate':
247
+ result = Math.min(result, t.tokens);
248
+ break;
249
+ }
250
+ }
251
+ return result;
252
+ }
253
+ getRetryMultiplier(node) {
254
+ if (!node.onFailure)
255
+ return 1;
256
+ switch (node.onFailure.type) {
257
+ case 'retry':
258
+ return 1 + node.onFailure.max;
259
+ case 'retry_then_fallback':
260
+ return 1 + node.onFailure.max;
261
+ default:
262
+ return 1;
263
+ }
264
+ }
265
+ getFallbackCost(node) {
266
+ if (!node.onFailure || node.onFailure.type !== 'retry_then_fallback')
267
+ return 0;
268
+ const fallbackNode = this.nodeMap.get(node.onFailure.node);
269
+ if (!fallbackNode)
270
+ return 0;
271
+ return this.getNodeCost(node.onFailure.node, fallbackNode);
272
+ }
273
+ }
@@ -0,0 +1,13 @@
1
+ import { FlowNode, Expr, GraphArg, GraphDecl } from '../parser/ast.js';
2
+ import { GraftError, SourceLocation } from '../errors/diagnostics.js';
3
+ import { ProgramIndex } from '../program-index.js';
4
+ export declare function checkVarCollision(name: string, graphName: string, location: SourceLocation, index: ProgramIndex, errors: GraftError[]): void;
5
+ export declare function checkExprSources(expr: Expr, seenNodes: Set<string>, declaredVars: Set<string>, graphName: string, errors: GraftError[]): void;
6
+ export declare function checkGraphCallArgs(args: GraphArg[], graphDecl: GraphDecl, seenNodes: Set<string>, declaredVars: Set<string>, graphName: string, location: SourceLocation, index: ProgramIndex, errors: GraftError[]): void;
7
+ export declare function checkGraphRecursion(graphs: {
8
+ name: string;
9
+ flow: FlowNode[];
10
+ location: SourceLocation;
11
+ }[], index: ProgramIndex, errors: GraftError[]): void;
12
+ export declare function collectGraphCalls(nodes: FlowNode[], calls: Set<string>): void;
13
+ export declare function checkLiteralParamType(value: string | number | boolean, type: 'Node' | 'Int' | 'String' | 'Bool'): boolean;