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
- An AST toolkit for the language and a deterministic fix shape.
- The threshold judgment (N≳50, deterministic vs per-site judgment).
- The pre-commit-skip protocol for codemod waves.
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
- Writing the transformer is upfront cost. It pays off only at scale, which the threshold encodes.
- Only for deterministic shapes. A backlog needing per-site judgment still goes to a per-site agent, not a codemod.
- Skipping the lint stanza is a scoped hole — justified by the transform's mechanical nature, marker-audited.
Example use within DocAble
- The N≳50 codemod threshold; AST transformers such as an inline-import hoisting codemod.
- The
pre-commit-skipfor codemod-class waves; codemods retained as deprecated exemplars.
Related Patterns
- See also (sibling) — typed-categories, remediation-verbs: the other bounded-move constraints; this one bounds how a mechanical change is executed.
- See also (cross-target) — this is the product-side face of the same codemod-first discipline the agent fleet uses for its own mechanical lint backlogs.