# Pass B — consolidation & anti-pattern analysis Pass B finds *cross-file, AI-typical* problems that per-node rules (Pass A) can't: duplication, failure to reuse, reinvented utilities. It is **report-only** (these changes risk coupling — never autofix) and every candidate is **judged by a model with AHA guardrails** before it's reported, because mechanical similarity has false positives. Pipeline (each detector): **extract (0 tokens) → cluster (tool or cheap model) → JUDGE (mandatory) → consolidation report**. Tools are the bundled, non-intrusive binaries (`bin/manifest.json`); nothing is written into the target project. --- ## The judge contract (shared by all Pass B detectors) For each candidate cluster, give the model the real code spans and ask for exactly: ```json { "same_concept": true, "verdict": "consolidate | partial | keep-separate", "confidence": 0.0, "why": "cite concrete evidence from the code", "risk": "what coupling/regression consolidating could cause", "recommendation": "the concrete change, or why to leave it" } ``` Only report findings with `verdict != keep-separate` AND `confidence >= 0.6`. For `keep-separate`, record the reason in the report so re-runs don't re-flag it. When you write the finding into `findings.json`, the judge's `recommendation` becomes the required **`action`** (a concrete, executable instruction) and you must add a **`verify`** (the `config` build/test/typecheck command) and a one-line **`title`** — these three fields are mandatory for every finding (see `assets/ledger.schema.json`) so the acting agent can execute it without re-running this skill. ### AHA guardrails (put these in the judge prompt — they prevent harmful merges) - **Rule of three**: 2 occurrences usually isn't worth abstracting; prefer ≥3. - **"The wrong abstraction is worse than duplication"** (Sandi Metz). If unifying would need extra params/conditionals to satisfy diverging callers → `keep-separate`. - **Respect boundaries**: do NOT merge across layers / bounded contexts — e.g. a DB enum vs an API/wire enum vs a UI label set; one adapter vs another adapter; per-service copies in a microservice split. These look identical but are decoupled on purpose. - **Preserve intentional subsets**: a smaller set that's a *named category* (e.g. `COORDINATOR_ROLES` ⊂ `AgentRole`) is not duplication — recommend deriving it from the canonical source, not merging it away. - **Drift is a signal, not always a bug**: when near-identical definitions have *diverged* (one has an extra member), flag the divergence — it may be a latent bug (a value produced but unrepresentable elsewhere). Model tiering (see OPTIMIZATION.md): cluster + judge on a cheap model; escalate to a stronger model only on low confidence. Fan out one judge per cluster where the runtime has sub-agents; else judge sequentially. --- ## F1 — Consolidation core ### Duplicate enums / labels / unions 1. Extract with ast-grep: `enum_declaration`, `type_alias_declaration` (keep string-literal unions), `as const` arrays/objects. Record each definition's **member set** + location. 2. Cluster (cheap model, small N): group by member-set overlap — exact, subset/superset, high Jaccard. 3. Judge each cluster. Typical outcomes: identical union+const in the same package → consolidate (idiomatic TS: one `as const` array as source of truth, derive the union via `typeof arr[number]`); same-named types in different packages that drifted → consolidate + reconcile; intentional subset → derive, don't merge. ### Near-duplicate functions Two complementary signals: - **Exact / copy-paste** → `cpd` (bundled): `cpd --min-tokens 50 --reporters json --output --silent`. Token-based (Type-1/2). 0 model tokens. - **Renamed / structural ("almost the same")** → ast-grep extracts function nodes; normalize each to a **skeleton** (strip comments; replace identifiers→`V`, strings/numbers→`L`; drop whitespace); group by identical skeleton (ignore trivial: skeleton < ~120 chars). This catches Type-2/3 that `cpd` misses (proven on software-house: 7 clusters). Then judge each cluster: `unify` (extract shared helper / parametrize the literal that differs) | `partial` (share a stateless core, keep thin wrappers) | `keep-separate` (the "not quite" is essential — different data source, retry strategy, adapter, etc.). ### Untyped repeated object shapes (→ DTO) 1. ast-grep extracts object literals; record each one's **top-level key set**. 2. Keep only shapes with ≥3 keys repeated **≥3×** that do **NOT** match an existing `interface`/`type` member set. (An inline object matching an existing interface is normal usage — TS structurally type-checks it — **not** a smell. Do not report those.) 3. Judge: is this a real DTO worth naming, or incidental? Recommend an interface + reuse. ## F2 — Reinventing-the-wheel 1. ast-grep builds a lightweight signature index across the repo: `{name, params, returns, file, line}` for every function/method (no extra tool; universal-ctags is GPL → excluded). 2. For a target (a changed span, or scanning the repo): find existing functions with high name/parameter/return similarity to the candidate. 3. Judge: does the new code re-implement an existing utility? If yes, recommend importing the existing one. Guardrail: confirm the existing function's contract actually fits (don't force reuse across incompatible interfaces/perf constraints). --- ## Output Write `simplify/consolidation-report.md`: clusters grouped by detector, each with the verdict, why, risk, and recommendation; a separate "kept separate (and why)" section; and a "latent drift/bugs" callout. Add a ledger finding per reported cluster with `kind: "consolidation"` (see `assets/ledger.schema.json`). Pass B never edits files, regardless of the run mode.