Graft v3.4: LSP Rename and the 200th Ratchet Decision
Graft is a graph-native language for AI agent harness engineering. It compiles .gft source files into execution harnesses — declaring how agents communicate, what context they share, and how token budgets flow through a pipeline. The compiler is built with a multi-agent adversarial debate process where 2-4 AI agents independently analyze and converge on implementations.
v3.4 completes the LSP maturation arc with rename support, fixes a correctness gap in token estimation, and addresses the longest-running tech debt item in the codebase.
What's New
LSP Rename Support
The biggest feature: rename contexts, nodes, memories, and graphs across your entire workspace.
// Before: rename "Analyzer" to "CodeReviewer"
node Analyzer(model: sonnet, budget: 5k/2k) {
reads: [TaskSpec]
produces AnalysisResult { issues: List<String> }
}
edge Analyzer -> Reviewer // ← all references updated
Rename uses word-boundary regex (\bAnalyzer\b) to find all occurrences in the document — declaration, reads, writes, edges, graph flows. Cross-file rename scans the workspace export cache (built in v3.3) to find files that import the renamed name and updates those too.
The key design challenge: Graft's AST stores name references as plain strings without source locations. ContextRef.context, EdgeDecl.source, FlowNode.name are all just string — no SourceLocation attached. Text-based search with word boundaries was the pragmatic solution, avoiding an expensive AST refactor to add locations to every reference site.
Conditional Edge Token Estimation
Since v3.3, conditional edges execute at runtime. But the token estimator ignored them — a graph with conditional branches would undercount token costs. Fixed:
- Best case: cheapest branch target (minimum cost)
- Worst case: most expensive branch target (maximum cost)
donetargets contribute zero cost
This was a ~30-line fix in estimator.ts, closing a TODO that had been deferred since v2.0.
Hierarchical Document Symbols
The outline view now shows children:
▾ TaskSpec (Context)
description (Field)
criteria (Field)
▾ Analyzer (Node)
issues (Field)
risk_score (Field)
▾ Pipeline (Graph)
Analyzer (Function)
Reviewer (Function)
Context, node, and memory symbols show their fields as children. Graph symbols show flow node references.
features.ts Split
The LSP features module grew from 449 lines (v3.1) to 600+ lines (v3.4). It finally got split into 8 focused modules:
src/lsp/features/
├── index.ts — re-exports (server.ts import unchanged)
├── utils.ts — getWordAtPosition
├── diagnostics.ts — toDiagnostics, extractUndefinedName
├── hover.ts — getHoverInfo, KEYWORD_DOCS
├── completions.ts — getCompletions + 8 sub-handlers
├── definition.ts — getDefinitionLocation
├── symbols.ts — getDocumentSymbols + children
├── code-actions.ts — buildAutoImportActions (extracted from server.ts)
└── rename.ts — isRenameable, collectRenameLocations
The split was predicted in the v3.2 retro: "Split when the next LSP feature (code actions) is added." Two versions later, with rename adding the 9th feature category, the split was overdue.
The 200th Ratchet
v3.4 brought the total ratchet-locked decisions to exactly 200. Ratchets are confirmed design decisions that cannot be revisited unless a code reviewer explicitly demands a change. They range from foundational choices ([T1] ESM modules, [T4] LL(1)+LL(2) parser) to implementation details ([v3.4-R06] Conditional edge estimation: best=min branch cost, worst=max branch cost).
Of the 200, only 5 have ever been unlocked — and each unlock was justified by a real engineering need (e.g., unlocking [v2.2-R10] Parser/lexer remain throw-based when parser error recovery was added in v3.2).
Process Notes
v3.4 applied R-PROC-13 (proposed in v3.3 retro): tighten MEDIUM tier criteria to require design ambiguity, not just new runtime behavior. Only R1 (rename) was MEDIUM — the cross-file rename semantics had genuine design ambiguity. R2 (estimator fix + split) and R3 (hierarchical symbols + extraction) were DIRECT despite introducing new behavior, because the implementations were spec-constrained.
Try It
npm install -g @graft-lang/graft@3.4.0
graft compile pipeline.gft
graft check pipeline.gft
graft run pipeline.gft --input '{"query": "test"}' --dry-run
Stats
| Metric | v3.3 | v3.4 | Delta | |--------|------|------|-------| | Tests | 636 | 690 | +54 | | Ratchet decisions | 190 | 200 | +10 | | Agent calls | ~12 | ~10 | -2 | | Rounds | 4 | 4 | 0 | | NEEDS_CHANGES | 0 | 0 | 0 | | Debated rounds | 2 | 1 | -1 | | Direct rounds | 1 | 2 | +1 | | LSP features | 6 | 7 (+ rename) | +1 |
Built with Claude Opus 4.6 via Claude Code. April 2026.