Skip to content
zaidwhyPublic

About

Where the risk in a repo lives, from git history alone: hotspots, duplication, maintenance cost, architectural debt, migration plans. Zero dependencies, on PyPI.

Topics

Resources

Contributing

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

autocto: The file to review first is the one that changes most and branches most. 5 analyzers, no LLM; 162 tests; pipx install repo-autocto. Core strip: 40 commits over 17 weeks, Jul 2026 to Oct 2026.

autocto

CI PyPI License: MIT Python 3.12+ Tests

Automated CTO: repository health analyzers that read a codebase and its git history and report where the real engineering risk lives.

autocto pipeline: git history and the source tree feed four analyzers (hotspots, duplicates, maintenance cost, architectural debt); a fifth, the migration planner, orders human-proposed changes with a topological sort. Sample output against the recall repo: frontend/src/App.jsx scores 5289, churn 41 times complexity 129.

Status: all five planned analyzers are shipped (hotspots, duplicates, maintenance-cost, architectural-debt, migration-plan), plus a CLI (autocto) and a v0.2.0 release on PyPI. See Roadmap for what's next.

Quickstart

pipx install repo-autocto   # or: pip install repo-autocto
autocto report /path/to/some/repo
$ autocto hotspots ../recall --limit 3
path                  churn  complexity  score
--------------------  -----  ----------  -----
frontend/src/App.jsx  41     129         5289
backend/main.py       41     89          3649
backend/memory.py     11     49          539

(Captured 2026-09-27 against a local checkout of recall. Run against autocto's own tree it is less interesting: every file there has a churn of 1.)

Every subcommand (hotspots, duplicates, maintenance, architecture, report) takes a repo path (default: current directory) and --json for machine-readable output. autocto plan proposal.json is the one command that takes a file instead of a repo: it orders a JSON redesign proposal (title, rationale, changes with id, description, files, depends_on, risk) into the markdown migration plan. autocto --help and autocto <command> --help show every flag.

To gate a pull request, hotspots and maintenance take --fail-over N: if any file's score (or cost) is above N, the offending files are printed to stderr and the command exits 1 (exit 2 stays for usage errors). It checks every file, not only the --limit rows shown.

autocto hotspots . --fail-over 5000   # CI step: fails the build when a file crosses the line

Source walks skip .git, node_modules, dist, build, caches, site-packages, and any virtual environment whose name starts with venv or .venv (so .venv312 and venv-3.14 too).

Prefer calling it from Python directly? Each analyzer is still a pure analyze_repo(repo_dir) function - the CLI is a thin wrapper over the same functions used below.

pip install -e ".[dev]"
python -c "
from pathlib import Path
from autocto.hotspots import analyze_repo

for hotspot in analyze_repo(Path('.'))[:3]:
    print(hotspot)
"

This runs the bug-hotspot analyzer against whatever repo you point it at - here, autocto's own working directory. See Demo below for real output, and Architecture for what each module returns.

Analyzers

  1. Bug-hotspot analyzer (src/autocto/hotspots.py) - churn x complexity over git log; the files that change often AND are complex are where bugs cluster. analyze_repo(repo_dir) ranks a real repo's tracked files.
  2. Duplicated-logic detector (src/autocto/duplicates.py) - token-shingle (Jaccard) similarity across files; analyze_repo(repo_dir) ranks a real repo's file pairs by duplication risk.
  3. Maintenance-cost estimator (src/autocto/maintenance_cost.py) - size x churn x dependency fan-in; analyze_repo(repo_dir) ranks a real repo's tracked files by size x churn x (1 + fan_in).
  4. Architectural-debt report (src/autocto/architectural_debt.py) - three independent signals over one same-repo import graph: import cycles (Tarjan strongly-connected components), god files (large AND highly-connected), and layering violations (only checked when the caller supplies an explicit layer order - see Architecture). analyze_repo(repo_dir) returns all three for a real repo.
  5. Migration-plan generator (src/autocto/migration_plan.py) - turns a human-authored redesign proposal (a set of ProposedChanges, each with what it touches and what it depends on) into an ordered, verifiable markdown plan. generate_migration_plan(title, changes) returns the rendered plan; unlike the other four, it has no "read a real repo" half - its only input is the proposal itself.

All analyzers are pure functions over repo data (or, for the migration-plan generator, over a proposal you supply): offline, deterministic, testable without API keys.

Demo

Real, captured output - not invented numbers. Three runs below, all against repos checked out locally at the time this README was written.

Bug-hotspot analyzer against recall (a separate, actively developed repo

with 94 commits of real history - more churn variance than autocto's own single-commit-per-file history shows)

Command:

python -c "
from pathlib import Path
from autocto.hotspots import analyze_repo

for hotspot in analyze_repo(Path('/path/to/recall'))[:5]:
    print(hotspot)
"

Output:

Hotspot(path='frontend/src/App.jsx', churn=29, complexity=129, score=3741)
Hotspot(path='backend/main.py', churn=14, complexity=89, score=1246)
Hotspot(path='backend/memory.py', churn=4, complexity=46, score=184)
Hotspot(path='eval/benchmark.py', churn=3, complexity=40, score=120)
Hotspot(path='backend/live.py', churn=4, complexity=19, score=76)

App.jsx is both the most-changed file in that repo's history (29 commits) and its most branch-heavy - exactly the churn x complexity signal this analyzer is built to surface.

Duplicated-logic detector against autocto's own tree

Command:

python -c "
from pathlib import Path
from autocto.duplicates import analyze_repo

pairs = analyze_repo(Path('.'))
print(f'{len(pairs)} pair(s) at or above the default 0.6 similarity threshold')
for pair in pairs:
    print(pair)
"

Output:

0 pair(s) at or above the default 0.6 similarity threshold

autocto's own two analyzer modules and their tests share no duplicated 5-token shingles above the default threshold - a correct negative result on a small, deliberately non-duplicated codebase, not a bug.

Maintenance-cost estimator against recall (same repo as the hotspot demo,

for a like-for-like comparison)

Command:

python -c "
from pathlib import Path
from autocto.maintenance_cost import analyze_repo

for cost in analyze_repo(Path('/path/to/recall'))[:5]:
    print(cost)
"

Output:

MaintenanceCost(path='frontend/src/App.jsx', size=772, churn=29, fan_in=1, cost=44776)
MaintenanceCost(path='backend/main.py', size=461, churn=14, fan_in=1, cost=12908)
MaintenanceCost(path='backend/memory.py', size=262, churn=4, fan_in=0, cost=1048)
MaintenanceCost(path='backend/live.py', size=125, churn=4, fan_in=1, cost=1000)
MaintenanceCost(path='eval/benchmark.py', size=309, churn=3, fan_in=0, cost=927)

App.jsx tops both this ranking and the hotspot one - it is large, the most-changed file in the repo, AND imported elsewhere, which is exactly what "maintenance cost" should mean: not just churn x complexity, but the size of the file and the blast radius of touching it.

Architectural-debt report against personal-llm (a bigger, more layered

codebase than recall or autocto itself - more likely to actually show something)

Command:

python -c "
from pathlib import Path
from autocto.architectural_debt import analyze_repo

report = analyze_repo(Path('/path/to/personal-llm'))
print(f'{len(report.cycles)} import cycle(s)')
print(f'{len(report.god_files)} god file(s)')
for god_file in report.god_files:
    print(' ', god_file)

layered = analyze_repo(
    Path('/path/to/personal-llm'),
    layer_order=['router', 'memory', 'rag', 'interfaces'],
)
print(f'{len(layered.layering_violations)} layering violation(s) under router -> memory -> rag -> interfaces')
"

Output:

0 import cycle(s)
3 god file(s)
  GodFile(path='src/personal_llm/memory/store.py', size=287, fan_in=14, fan_out=1)
  GodFile(path='src/personal_llm/interfaces/cli.py', size=302, fan_in=1, fan_out=11)
  GodFile(path='src/personal_llm/interfaces/api.py', size=257, fan_in=0, fan_out=9)
0 layering violation(s) under router -> memory -> rag -> interfaces

Real, captured, honest results, including the zeroes - same "a clean result is a correct result, not a suppressed one" rule duplicates.py's demo already follows. personal-llm has no import cycles and no layering violations under that ordering (its router package genuinely never reaches up into interfaces, by inspection of the real source), which is a legitimate, useful thing to report about a codebase: this project's dependency discipline is holding. memory/store.py is flagged as a god file because fourteen other files reference it by name (the SQLite-backed MemoryStore class is the one shared persistence layer) on top of nearly 300 lines - exactly the "big and everyone depends on it" combination this signal exists to surface.

Migration-plan generator over a real duplication in this repo

Unlike the other four analyzers, this one has no "scan a real repo" mode - its input is a proposal you write, not something analyze_repo discovers. So instead of pointing it at a repo, this worked example describes a real, verifiable duplication in autocto's own tree: duplicates.py, maintenance_cost.py, and architectural_debt.py each define an identical _iter_source_files/_SKIP_DIR_NAMES pair today (grep -n "_iter_source_files\|_SKIP_DIR_NAMES" src/autocto/*.py shows all three) - exactly the kind of duplication duplicates.py itself is built to flag, not yet acted on.

Command:

python -c "
from autocto.migration_plan import ProposedChange, generate_migration_plan

changes = [
    ProposedChange(
        id='extract-shared-file-walker',
        description='Extract the duplicated _iter_source_files/_SKIP_DIR_NAMES pair out of duplicates.py, maintenance_cost.py, and architectural_debt.py into one shared module',
        files=('src/autocto/_repo_walk.py',),
        risk='medium',
    ),
    ProposedChange(
        id='migrate-duplicates',
        description='Point duplicates.py at the shared walker instead of its own copy',
        files=('src/autocto/duplicates.py',),
        depends_on=('extract-shared-file-walker',),
        risk='low',
    ),
    ProposedChange(
        id='migrate-maintenance-cost',
        description='Point maintenance_cost.py at the shared walker',
        files=('src/autocto/maintenance_cost.py',),
        depends_on=('extract-shared-file-walker',),
        risk='low',
    ),
    ProposedChange(
        id='migrate-architectural-debt',
        description='Point architectural_debt.py at the shared walker',
        files=('src/autocto/architectural_debt.py',),
        depends_on=('extract-shared-file-walker', 'migrate-maintenance-cost'),
        risk='medium',
    ),
    ProposedChange(
        id='delete-dead-walkers',
        description='Delete the now-unused per-module _iter_source_files/_SKIP_DIR_NAMES copies',
        depends_on=('migrate-duplicates', 'migrate-maintenance-cost', 'migrate-architectural-debt'),
        risk='high',
    ),
]

print(generate_migration_plan('Consolidate duplicated file-walking logic', changes,
    rationale='duplicates.py, maintenance_cost.py, and architectural_debt.py each define an '
              'identical _iter_source_files/_SKIP_DIR_NAMES pair today (verified by grep) - this '
              'plan removes the duplication for real.'))
"

Output (real, generated by the command above, not hand-written):

# Migration Plan: Consolidate duplicated file-walking logic

## Rationale

duplicates.py, maintenance_cost.py, and architectural_debt.py each define an identical _iter_source_files/_SKIP_DIR_NAMES pair today (verified by grep) - this plan removes the duplication for real.

## Steps

Ordered so every change's dependencies land first (`autocto.migration_plan.order_changes`, topological sort). Execute top to bottom; do not skip ahead.

### 1. extract-shared-file-walker - Extract the duplicated _iter_source_files/_SKIP_DIR_NAMES pair out of duplicates.py, maintenance_cost.py, and architectural_debt.py into one shared module

**Risk:** medium
**Files:** src/autocto/_repo_walk.py
**Verify:** Run the full test suite; smoke-test the affected feature manually.

### 2. migrate-duplicates - Point duplicates.py at the shared walker instead of its own copy

**Risk:** low
**Files:** src/autocto/duplicates.py
**Depends on:** extract-shared-file-walker
**Verify:** Run the affected tests; confirm no other callers reference the old shape.

### 3. migrate-maintenance-cost - Point maintenance_cost.py at the shared walker

**Risk:** low
**Files:** src/autocto/maintenance_cost.py
**Depends on:** extract-shared-file-walker
**Verify:** Run the affected tests; confirm no other callers reference the old shape.

### 4. migrate-architectural-debt - Point architectural_debt.py at the shared walker

**Risk:** medium
**Files:** src/autocto/architectural_debt.py
**Depends on:** extract-shared-file-walker, migrate-maintenance-cost
**Verify:** Run the full test suite; smoke-test the affected feature manually.

### 5. delete-dead-walkers - Delete the now-unused per-module _iter_source_files/_SKIP_DIR_NAMES copies

**Risk:** high
**Depends on:** migrate-duplicates, migrate-maintenance-cost, migrate-architectural-debt
**Verify:** Run the full test suite; stage the change behind a flag or on a branch and get a second reviewer before merging.

## Rollback

Each step above should be its own commit (or PR) so any single step can be reverted independently without undoing steps that landed after it, as long as later steps did not themselves depend on it - check the "Depends on" list above before reverting a step other changes rely on.

## Reviewer instructions

This is a PLAN ONLY - nothing has been changed. A human executes each step, runs its verification, and only then moves to the next.

The topological sort put extract-shared-file-walker first (nothing depends on it) and delete-dead-walkers last (it depends on all three migrations), exactly as declared - not hand-ordered. This plan itself is not applied by this repo; it is only a worked demonstration of the generator.

Architecture

One line per planned analyzer, numbered to match Analyzers above:

  1. src/autocto/hotspots.py - churn (git log --numstat commit-frequency count) x complexity (branch-keyword/operator density proxy). Pure functions parse_numstat_log, complexity_score, compute_hotspots wired together by the one effectful entry point, analyze_repo(repo_dir, *, extensions=..., git_log_fn=...), which returns list[Hotspot] (path, churn, complexity, score).
  2. src/autocto/duplicates.py - token-shingle Jaccard similarity across files. Pure functions tokenize, shingle, jaccard_similarity, find_duplicates wired together by analyze_repo(repo_dir, *, extensions=..., shingle_size=..., threshold=...), which returns list[DuplicatePair] (path_a, path_b, similarity).
  3. src/autocto/maintenance_cost.py - size (line count) x churn (reuses hotspots.parse_numstat_log) x dependency fan-in (how many other files import it, from the real import graph built by src/autocto/imports.py, Python and JS/TS). Pure functions count_lines, extract_referenced_names, compute_fan_in (the older stem name-matching helpers, kept for callers with only bare names), estimate_costs (formula: size x churn x (1 + fan_in) - see that function's docstring for why 1 + fan_in, not bare fan_in) wired together by analyze_repo(repo_dir, *, extensions=..., git_log_fn=...), which returns list[MaintenanceCost] (path, size, churn, fan_in, cost).
  4. src/autocto/architectural_debt.py - three independent signals over one same-repo import graph, built by autocto.imports.resolve_imports, which resolves each import statement to the one file it names (Python via ast, packages, relative imports and src/ layouts; JS/TS relative specifiers, index.* and .js -> .ts). Imports of anything outside the repo give no edge; tsconfig path aliases and dynamic imports are not followed. build_import_graph is the older stem name-matching variant. Import cycles: find_cycles runs an iterative Tarjan strongly-connected-components pass over the graph and reports each component of size > 1 (a documented simplification versus enumerating every elementary cycle - see the function's docstring for why). God files: find_god_files flags files that clear BOTH a size threshold (line count, via maintenance_cost.count_lines) AND a connections threshold (fan_in + fan_out, fan_in and fan_out both from the same import graph), returning list[GodFile] (path, size, fan_in, fan_out) ranked by size x (fan_in + fan_out). Layering violations: find_layering_violations only runs when the caller passes an explicit layer_order (an ordered list of top-level package names, most-foundational first) - there is no universal default across arbitrary repos, so None (the default) means "skip the check", never a fabricated ordering; a violation is an import edge pointing from a more-foundational layer up into a less-foundational one, returned as list[LayeringViolation] (importer, importee, importer_layer, importee_layer). All three are wired together by the one effectful entry point, analyze_repo(repo_dir, *, extensions=..., layer_order=None, size_threshold=..., connections_threshold=...), which returns one ArchitecturalDebtReport (cycles, god_files, layering_violations). No git, no subprocess anywhere in this module - none of its three signals need churn.
  5. src/autocto/migration_plan.py - takes a human-authored Sequence[ProposedChange] (each with id, description, files, depends_on, risk) and orders it with order_changes (Kahn's-algorithm topological sort, ties broken by id ascending for determinism, raising CycleError on an unresolvable depends_on cycle or an unknown id). build_migration_plan(title, changes) attaches a risk-derived verification instruction to each step (MigrationStep); the one effectful-looking but actually still-pure entry point, generate_migration_plan(title, changes), builds then renders to markdown in one call. No filesystem or subprocess access anywhere in this module - it never reads a repo, only the proposal passed in.

All shipped modules follow the same shape: filesystem/subprocess access is isolated to analyze_repo, everything else is a pure function over strings and dicts. Where a module has a genuinely effectful operation (git log in hotspots/maintenance_cost) it is an injectable seam (git_log_fn) so tests never shell out to real git; architectural_debt.py needs no such seam at all, since none of its three signals depend on git history; migration_plan.py has no analyze_repo at all, since its whole job is over caller-supplied data, not a repo scan. See CONTRIBUTING.md for the pattern in detail.

Roadmap

All five Tier 4 analyzers in the ai-ecosystem Night Shift queue (PROJECT-GENESIS.md section 9) are shipped, plus the CLI and the v0.2.0 PyPI release. Nothing further is queued for this repo yet - next candidates would come from a fresh Night Shift pass over PROJECT-GENESIS.md section 9, or from real usage against other repos in the fleet.


One plate from the survey at github.com/zaidwhy. The strip on the plate above is this repository's own commit history, one band per week.

About

Where the risk in a repo lives, from git history alone: hotspots, duplication, maintenance cost, architectural debt, migration plans. Zero dependencies, on PyPI.

Topics

Resources

Contributing

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages