autocto
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
- Bug-hotspot analyzer (
src/autocto/hotspots.py) - churn x complexity overgit log; the files that change often AND are complex are where bugs cluster.analyze_repo(repo_dir)ranks a real repo's tracked files. - 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. - 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 bysize x churn x (1 + fan_in). - 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. - Migration-plan generator (
src/autocto/migration_plan.py) - turns a human-authored redesign proposal (a set ofProposedChanges, 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:
src/autocto/hotspots.py- churn (git log --numstatcommit-frequency count) x complexity (branch-keyword/operator density proxy). Pure functionsparse_numstat_log,complexity_score,compute_hotspotswired together by the one effectful entry point,analyze_repo(repo_dir, *, extensions=..., git_log_fn=...), which returnslist[Hotspot](path,churn,complexity,score).src/autocto/duplicates.py- token-shingle Jaccard similarity across files. Pure functionstokenize,shingle,jaccard_similarity,find_duplicateswired together byanalyze_repo(repo_dir, *, extensions=..., shingle_size=..., threshold=...), which returnslist[DuplicatePair](path_a,path_b,similarity).src/autocto/maintenance_cost.py- size (line count) x churn (reuseshotspots.parse_numstat_log) x dependency fan-in (import-statement name-matching against file stems, Python and JS/TS). Pure functionscount_lines,extract_referenced_names,compute_fan_in,estimate_costs(formula:size x churn x (1 + fan_in)- see that function's docstring for why1 + fan_in, not barefan_in) wired together byanalyze_repo(repo_dir, *, extensions=..., git_log_fn=...), which returnslist[MaintenanceCost](path,size,churn,fan_in,cost).src/autocto/architectural_debt.py- three independent signals over one same-repo import graph, built by reusingmaintenance_cost.extract_referenced_namesper file and resolving names to real files the same stem-matching waymaintenance_cost.compute_fan_indoes (pure functionbuild_import_graph). Import cycles:find_cyclesruns 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_filesflags files that clear BOTH a size threshold (line count, viamaintenance_cost.count_lines) AND a connections threshold (fan_in + fan_out,fan_infromcompute_fan_in,fan_outfrom the same import graph), returninglist[GodFile](path,size,fan_in,fan_out) ranked bysize x (fan_in + fan_out). Layering violations:find_layering_violationsonly runs when the caller passes an explicitlayer_order(an ordered list of top-level package names, most-foundational first) - there is no universal default across arbitrary repos, soNone(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 aslist[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 oneArchitecturalDebtReport(cycles,god_files,layering_violations). No git, no subprocess anywhere in this module - none of its three signals need churn.src/autocto/migration_plan.py- takes a human-authoredSequence[ProposedChange](each withid,description,files,depends_on,risk) and orders it withorder_changes(Kahn's-algorithm topological sort, ties broken byidascending for determinism, raisingCycleErroron an unresolvabledepends_oncycle 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)
| File | Size | Uploaded | |
|---|---|---|---|
| repo_autocto-0.1.0.tar.gz | 34.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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