← Blog

Graft v4.1: Quality Hardening

3 min read
graftcompilerllmadversarial-debateclaude

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 collision
  • checkExprSources — expression source ordering
  • checkGraphCallArgs — argument type/arity validation
  • checkGraphRecursion — DFS cycle detection
  • collectGraphCalls — graph call enumeration
  • checkLiteralParamType — 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