Skip to content

Agentic Architecture Recovery

Status: landed. Tracked as Milestones L / M / N on the public roadmap.

Squeaky Clean generates Clean-Architecture projects from a ProblemSpec. Agentic Architecture Recovery is the inverse path: point the framework at an existing brownfield project and it ingests the code, recovers a faithful Squib, analyzes it for Clean-Architecture violations, lets you triage which to fix, applies the fixes, and re-enters the standard generation pipeline.

Messy legacy code in → a freshly generated Clean-Architecture project out, with human checkpoints in the middle.

The pipeline

Five separated, independently re-runnable phases. Each persists an artifact, so you can stop, edit, and resume — nothing is a black box.

Phase CLI Input → Artifact LLM?
Recover --recover-from project → recovered.squib no
Analyze (part of --recover-from) artifact → categorized violations.json no
Triage --triage violations.jsonrefactor_plan.json no
Refactor --refactor Squib + plan → refactored.squib no (v1)
Regenerate --squib-file signed-off Squib → code yes

Recovery is faithful. The recovered Squib is a true photograph of the original — coupling, cycles, and god-classes included. All opinion about what is wrong lives in the separate Analyze phase; all fixing lives in the opt-in Refactor phase. That keeps the recovered artifact verifiable against the source and re-analyzable later with better rules.

Worked example

# 1. RECOVER + ANALYZE — ingest a project, emit a reviewable Squib + violations
squeaky --recover-from ./legacy-app --language python \
        --recover-out out/recovered.squib \
        --criteria testability,evolvability,simplicity,performance,migration_safety,delivery_speed
# → out/recovered.squib
# → out/recovered.squib.violations.json   (categorized findings)
# → out/recovered.squib.violations.md     (human-readable review)

# 2. TRIAGE — opt-out review; every category is addressed by default
squeaky --triage out/recovered.squib.violations.json
# prompts per category: "address all N framework-coupling violation(s)? [Y/n]"
# → out/refactor_plan.json

# 3. REFACTOR — apply the accepted transforms
squeaky --refactor out/recovered.squib --plan out/refactor_plan.json \
        --refactor-out out/refactored.squib
# → out/refactored.squib   (coupled classes split into Entity + Repository + Adapter)

# 4. REGENERATE — feed the signed-off Squib back into generation
squeaky --squib-file out/refactored.squib --legacy-tests ./legacy-app/tests

You can also edit recovered.squib by hand at step 1 and skip straight to --squib-file — the review gate reports any parse error with line context and waits for a corrected version.

Supported languages

--language {python,javascript,typescript,java}. Everything after ingest is language-neutral (layer assignment, pattern classification, decomposition, analysis, and refactoring all operate on the language-agnostic ClassCatalog/ArchitectureSpec), so only the extractor is language-specific.

Language Backend Fidelity
Python real ast walk high — bases, methods, fields, imports, decorators
Java regex (package-keyed FQNs) medium — imports resolve to real edges via package
JavaScript / TypeScript regex (path-keyed FQNs) medium — relative imports rarely resolve, so a sparser graph

Go and Rust are not yet supported for recovery and raise a clear error. The regex extractors are approximate by design; they produce a solid reviewable artifact, not a perfect parse. A tree-sitter/AST backend per language is the planned fidelity upgrade.

Ingest excludes test and vendored code across languages (test_*.py, *.test.ts, *.spec.ts, *Test.java, tests/, node_modules/, .venv/, target/, __tests__/, …) so the recovered architecture reflects production code only.

Analyze — violation categories

Each category points one of the framework's own generated-code rules inward at the recovered artifact. Findings are structured { category, target, detail, suggestion } with a stable category:target id.

  • framework-coupling — a domain class inheriting a foreign base (an ORM model, Active Record). Detected generically: a base that resolves to neither a sibling class nor the language standard library (a bounded allowlist), so no per-framework knowledge is needed. Per Clean Architecture, this class is a Dependency-Rule violation, not an Entity.
  • dependency-rule — a class importing a sibling in a strictly outer layer.
  • cyclic-dependency — module-level dependency cycles.
  • granularity — a class exceeding the ≤5-public-method bound.
  • decorative-class — a class with no methods and no invariants.

Architectural trade-off analysis (MCDA)

Whether a framework-coupled class should be preserved (kept as an Active-Record boundary object) or split (into a pure Entity + Repository port + Adapter) is a genuine trade-off with no universal answer. You supply an importance ranking of shared criteria via --criteria (most-important first):

testability, simplicity, performance, evolvability, migration_safety, delivery_speed

Rank → weights via rank-order centroid. Options are scored by weighted sum, with hard invariants (Dependency Rule, SOLID, acyclicity) as non-negotiable gates — never bought back by a high weighted score — and near-ties flagged for human review. Purity-first priorities recommend split; speed/risk-first priorities recommend preserve.

Refactor — what the transform does

For each framework-coupling violation you kept, the class is split 1→N:

Page (extends models.Model)   ──►   Page          -> Entity      (Domain)
                                     PageRepository-> Repository  (Domain port)
                                     PageAdapter   -> Adapter     (PageInfra, Infrastructure)

The Entity keeps the original members, the Repository port carries conventional save/find_by_id, and the Adapter (in a companion <Module>Infra module) implements the port — exports and cross-module dependencies wired so the result validates and can regenerate.

This is a skeleton, not a finished refactor. The Entity keeps all original members; which are business rules vs persistence concerns is not yet resolved — that member classification is the planned agentic follow-up (the first LLM step in this pipeline). Only the framework-coupling category has a transform today; the other categories are detected and triageable but not yet auto-transformed.

What "agentic" means here

The recovery front-half (Recover, Analyze, Triage, Refactor) is deterministic — no LLM. Agents enter in two places:

  • Pattern-classification tie-break. When deterministic fingerprints score two or more candidate patterns equally, an LLM picks from the candidate set with the class skeleton as context, routed through a content-addressed disk cache (replay stability). Output outside the candidate set is rejected in favor of SimpleClass, so a hallucinated pattern never reaches the generated code.
  • Regeneration. Once your Squib is signed off, the standard greenfield pipeline (Architect, Manager, atomic agents) takes over. The architect step is short-circuited — you've already given it the architecture.

Is the CLI interactive?

Mostly no — it is a flag-driven batch CLI. The one interactive step is --triage, which prompts a [Y/n] per violation category (default yes; closed stdin ⇒ yes). Everything else is determined by flags upfront, and every phase is persistable and re-runnable from its artifact.

See also