Appendix C - 18. Codemod-first threshold (N≳50 → AST transformer)

The Structure of Codemod-first threshold (N≳50 → AST transformer) — its shape at a glance:

A branching decision: count the sites and ask whether the fix shape is deterministic. Many sites with a deterministic shape route to one AST transformer; a shape needing per-site judgment routes to a per-site agent.

flowchart TD
  Backlog([Mechanical backlog]) --> Count{Many sites AND deterministic?}
  Count -->|yes| Codemod[One AST transformer]
  Count -->|no| PerSite[Agent per site]
  Codemod --> Apply([applied everywhere, identically])

Accessible description: a decision node asks whether the backlog has many sites and a deterministic fix shape. If so, it routes to one AST transformer applied identically everywhere; if the fix needs per-site judgment, it routes to one agent per site.

Projected from the catalogue entry repair-vocabulary / Codemod-first threshold (N≳50 → AST transformer).

On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns

Intent

Intent — For a large mechanical backlog (N≳50 sites with a deterministic fix shape), author one AST-level transformer instead of dispatching N agents, bounding how large mechanical changes are made.

Motivation

A large mechanical lint backlog (say 200 sites all needing the same deterministic edit) tempts one of two bad responses: dispatch N agents (expensive, and each introduces per-site drift) or hand-edit them (slow and inconsistent). The failure is N inconsistent hand/agent fixes for a change that is actually a single deterministic transform, and it recurs at every large mechanical backlog.

Applicability

Structure

The Structure diagram appears at the top of this page.

Sample Code

A codemod parses each file to a syntax tree, rewrites the matching nodes with the exact same transform, and writes the file back. Because it operates on structure rather than text, it cannot drift between sites the way N separate edits do. This one hoists a deferred import out of a function body to module scope.

import ast

class HoistImports(ast.NodeTransformer):
    """Deterministic transform: pull imports out of function bodies to module top.
    The same rewrite at every site, in one reviewable artifact — no per-site drift."""

    def __init__(self): self.hoisted: list[ast.stmt] = []

    def visit_FunctionDef(self, fn: ast.FunctionDef):
        kept = []
        for stmt in fn.body:
            if isinstance(stmt, (ast.Import, ast.ImportFrom)):
                self.hoisted.append(stmt)        # lift it to module scope
            else:
                kept.append(stmt)
        fn.body = kept or [ast.Pass()]
        return fn

def apply(source: str) -> str:
    tree = ast.parse(source)
    tf = HoistImports(); tree = tf.visit(tree)
    tree.body = tf.hoisted + tree.body           # imports now at the top
    return ast.unparse(ast.fix_missing_locations(tree))

Consequences

Example use within DocAble

Contents
© James C. Davis, 2026–present