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.json → refactor_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
Squibis 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¶
- Why Squeaky Clean? — the greenfield context this inverts.
- Architecture deep-dive — the tier structure the regenerated project lands in.
- Squib grammar — what the recovered architecture is emitted as.
- CLI flags — the recovery commands in the full flag reference.