Decision records
ADR 0042 — AI-native, brownfield-aware loom appgen
- Status: Accepted (2026-07-12)
- Date: 2026-07-12
- Builds on: the P11 offline codegen (
parseIntent → AppSpec → generate),@loom/reasoning’s elevation ladder, and the folder convention (discoverFlows,loom gen). - Part of: the AI-native developer-experience line. DX layer only — the framework kernel is not modified.
- Related: requirements (
loom/docs/requirements/2026-07-12-ai-native-brownfield-appgen.md), evaluation (loom/docs/requirements/2026-07-12-ai-native-brownfield-appgen-evaluation.md).
Context
loom appgen was a one-shot greenfield scaffolder driven by a fixed keyword grammar. It could not
reason about an existing app, preview a change, replay a generation, or use a real model. The design
had always intended parseIntent to be swappable — provided any replacement still emits a structured
spec and codegen never sees raw NL.
Decision
Restructure loom appgen into four stages, with AI isolated to exactly one, and keep the whole
thing in the DX layer (@loom/appgen + an optional @loom/appgen-model + the CLI). Nothing in
@loom/core, @loom/compiler, @loom/workflow-runtime, @loom/effect-runtime, @loom/app, or
@loom/reasoning changes.
- scan —
scanApp(dir) → AppModel: deterministic TS-AST summary of flows/steps/gates and per-step handler state (stub | edited | external). Never raw file contents. - plan —
planChanges(intent, appModel) → ChangeSpec: the one NL stage. Tags each capability and stepcreate | extend | modify | delete. Deterministic by default (wraps the grammar parser + diffs the scan); a model planner emits the sameChangeSpec. - apply —
applyChanges(changeSpec, appModel) → FilePlan: creates from templates; surgical, formatting-preserving edits via a self-containedFlowEditor(TS-AST reader + offset-splice buffer); grant wiring via aConfigEditor; deletes remove generated files/steps. - validate — in-process
tsc(--check); the model path adds a semantic self-repair loop.
Key choices and why
- Own AST toolkit, not
ts-morphor the compiler’s pass. Surgicalmagic-string-style splices into the original source preserve untouched human formatting/comments byte-for-byte, which keeps the--dry-rundiff honest and the compiler untouched. - Content-hash safety gate, not the compiler’s step hash. The compiler hashes location; appgen needs to know whether a handler body is still the generated stub, so it keeps its own content hash with a single source of truth for the stub text.
tsc, notloom check, for validation.loom checkneeds a post-build compiler manifest that fresh source lacks;ts.createProgramgives structured diagnostics with no build.- Optional model as a separate package.
@loom/appgen-modelholds the Gemma/node-llama-cpp integration so@loom/appgenstays offline and near-zero-dep. The native runtime is anoptionalDependency, genuinely dynamically imported, and the CLI resolves the package from the target project — so neither the framework nor default CLI users carry it. - Reasoning-ladder-on-itself. The default path is free (deterministic). Elevate to a model only
when asked; the model path uses
@loom/reasoning’s cheap-first ladder with a validator confidence signal and a bounded self-repair loop, logging elevations into the spec lockfile (a DX-layer echo ofDECISION_RECORDED).
Consequences
loom appgenbecomes a plan → diff → confirm tool that works on net-new and existing apps, is replayable without a model (--from-spec), and never clobbers human-edited code.- The model is strictly a quality upgrade: every failure mode (weak output, missing native dep, crash)
degrades to the deterministic parser and still yields a valid
ChangeSpec. - New surface:
--dry-run,--apply,--check,--from-spec,--model/--model-big/--model-tier,--offline,--yes. New package@loom/appgen-model. New convention.loom/appgen/<ts>.spec.json. - Kernel ABI and event schema unchanged.
Notes
Real end-to-end testing against a Gemma GGUF fixed four integration bugs (ESM dynamic-import lowering,
the node-llama-cpp v3 grammar API, project-dir peer resolution, a minItems grammar segfault) and set
the footprint/stability defaults (bounded generation, small context). Environment guidance (Node LTS,
RAM per tier) lives in the @loom/appgen-model README.
Source: docs/adr/0042-ai-native-brownfield-appgen.md in the loom repository.