Graft v4.1: Quality Hardening
Graft is a domain-specific language for defining LLM agent pipelines. The compiler takes .gft files and generates Claude Code harness structures — contexts, agents, hooks, and orchestration files. Development uses an adversarial debate harness where multiple AI agents independently analyze, critique, and converge before implementation.
v4.1 is a quality-only release — no new user-facing features. Four rounds, ~10 agent calls, 21 new tests (1,001 total). Closes 3 tech debt items from v4.0 and extracts the growing scope checker.
What This Version Fixes
Multi-segment condition resolution. v4.0 introduced Condition.left as an Expr (replacing a plain string field). A bridge function conditionFieldName() joined segments with . for backward compatibility. This worked for string keys but silently failed for runtime value lookup — result.score produced the key "result.score" instead of traversing result then score. The fix introduces resolveNestedField(segments, obj), a proper nested object traverser:
// Before (broken for nested objects):
const key = conditionFieldName(condition.left); // "result.score"
const value = output[key]; // undefined
// After:
const value = resolveNestedField(condition.left.segments, output);
// output.result.score -> 90
Graph call output isolation. Child graph calls shared the parent's outputs map. If both had a node named Worker, the child's output silently overwrote the parent's. Fixed with a shallow clone: outputs: new Map(ctx.outputs).
Division-by-zero warning. evaluateExpr now accepts an optional warnings array. On divide-by-zero, it pushes a warning instead of silently returning 0.
Scope Checker Extraction
scope.ts grew to ~697 lines across v3.x and v4.0. Six graph-related functions moved to graph-checker.ts (204 lines):
checkVarCollision— variable name vs declaration collisioncheckExprSources— expression source orderingcheckGraphCallArgs— argument type/arity validationcheckGraphRecursion— DFS cycle detectioncollectGraphCalls— graph call enumerationcheckLiteralParamType— literal argument type checking
Pure refactor: 0 new tests, all 996 existing tests passed unchanged.
Exhaustive Switches
TypeScript's never type now guards all FlowNode switches across 3 files (4 switch statements). Adding a new FlowNode kind without handling it in every switch now produces a compile-time error instead of silently falling through.
The Bridge Function Bug
The most interesting bug: a bridge function outliving its purpose. conditionFieldName() was designed for the v4.0-R1 Condition.field to Condition.left migration — it produced string representations for display and backward compatibility. It was never meant for value lookup, but got used that way in evaluateCondition. The bug was identified during v4.0-R2 cross-critique and deferred as tech debt. v4.1 removes the bridge entirely from runtime paths, replacing it with purpose-built traversal.
Process Notes
- Quality-only scoping prevented scope creep — every change addressed a pre-identified issue
- R1 launched two debate agents whose results were never explicitly consumed — the orchestrator's accumulated domain knowledge sufficed for targeted fixes
- R3 (scope extraction) validated at 0 new tests — pure refactoring with full test suite as safety net
Stats
| Metric | v4.0 | v4.1 | Delta | |--------|------|------|-------| | Tests | 980 | 1,001 | +21 | | Ratchet decisions | ~268 | ~279 | +11 | | Agent calls | ~14 | ~10 | -4 | | Rounds | 6 | 4 | -2 | | NEEDS_CHANGES | 0 | 0 | 0 |
Try It
npm install -g @graft-lang/graft
graft compile examples/chatbot.gft --out-dir ./output
graft run examples/hello.gft --input '{"question":"test"}' --dry-run
Or from source: git clone https://github.com/JSLEEKR/graft.git && cd graft && npm install && npm run build