Skip to main content

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.

Status: all five planned analyzers are shipped (hotspots, duplicates, maintenance-cost, architectural-debt, migration-plan), plus a CLI (autocto) and a v0.1.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 . --limit 3
path                                churn  complexity  score
-----------------------------------  -----  ----------  -----
src/autocto/architectural_debt.py    12     47          564
src/autocto/maintenance_cost.py      9      28          252
src/autocto/hotspots.py              7      34          238

Every subcommand (hotspots, duplicates, maintenance, architecture, report) takes a repo path (default: current directory) and --json for machine-readable output. autocto --help and autocto <command> --help show every flag.

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 (import-statement name-matching against file stems, Python and JS/TS). Pure functions count_lines, extract_referenced_names, compute_fan_in, 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 reusing maintenance_cost.extract_referenced_names per file and resolving names to real files the same stem-matching way maintenance_cost.compute_fan_in does (pure function build_import_graph). 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 from compute_fan_in, fan_out 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.1.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.

Metadata

Release files for repo-autocto 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for repo-autocto 0.1.0
File Size Uploaded
repo_autocto-0.1.0.tar.gz 34.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for repo-autocto 0.1.0
File Interpreter ABI Platform
repo_autocto-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 60.6 kB

Release files / repo_autocto-0.1.0.tar.gz

Download URL repo_autocto-0.1.0.tar.gz
Size 34.1 kB
Tags Source
SHA-256 checksum
How to use checksums
0adb5b6ba7a10720ca469569e550692b9e59ad9ff3d1fbf74728450bb9969097
BLAKE2b-256 checksum
How to use checksums
dddbf505f7f5a7fe83d976f9c9d3f1ce41692d6daf0e5d1cf40864d657dbc074
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / repo_autocto-0.1.0-py3-none-any.whl

Download URL repo_autocto-0.1.0-py3-none-any.whl
Size 26.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aa9e7815c75f9b83502025da18ddc0ade6d256115bb683cbc13dc674a3630c49
BLAKE2b-256 checksum
How to use checksums
a2f4677c0ffdc074eaba923eeab8809c2662780aaa256b834758655547e5ae75
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page