Skip to main content

Foothold

CI PyPI Python License: Apache-2.0 Checked with mypy Ruff

Which 20 files should I read first? Foothold answers that for a Python repository in under a second, without an API key — the foothold you need before you can start climbing an unfamiliar codebase.

$ foothold map ~/src/rich
99 modules (99 source, 0 test) · 38,437 lines · 1,884 import statements
Top 6 files by structural weight
┏━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ # ┃ file                      ┃ score ┃ in ┃  loc ┃ why                      ┃
┡━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 1 │ console.py                │ 0.695 │ 49 │ 2699 │ imported by 49 modules   │
│ 2 │ cells.py                  │ 0.542 │ 31 │  353 │ imported by 31 modules   │
│ 3 │ _unicode_data/__init__.py │ 0.380 │  1 │   94 │ high transitive reach    │
│ 4 │ text.py                   │ 0.353 │ 31 │ 1364 │ imported by 31 modules   │
│ 5 │ style.py                  │ 0.299 │ 30 │  797 │ imported by 30 modules   │
│ 6 │ segment.py                │ 0.220 │ 21 │  781 │ 781 lines                │
└───┴───────────────────────────┴───────┴────┴──────┴──────────────────────────┘

That ordering is not a guess. It falls out of the import graph, weighted by how often each file has been edited.

Two ways to run it. On your machine:

uv tool install foothold && foothold map .

Or in CI, where every pull request gets the reading path as a comment:

- uses: actions/checkout@v7
  with:
    fetch-depth: 0          # the churn signal needs real history
- uses: serdairy/foothold@v0.2.3
  with:
    comment: "true"         # needs permissions: pull-requests: write

Full reference in GitHub Action below; the listing is Foothold reading path on the Marketplace.

The problem

Contributor onboarding is the most expensive unpaid work in open source, and it is paid twice — once by the newcomer who spends a weekend deciding which of 400 files matter, and once by the maintainer answering the same orientation question in every issue thread.

The usual mitigations do not hold. ARCHITECTURE.md is written once at project inception and drifts within two releases. Generated API references list every symbol and rank none of them. Pasting a repository into a chat window costs ~46,000 tokens for a project the size of networkx, and produces fluent prose with no grounding in the actual import graph.

Foothold splits the problem in two. Ranking is deterministic — a graph, a churn count, a formula you can read. Prose is optional and sits on top of an already-correct, already-pruned selection. The expensive part is the part that does not need a model.

Install

uv tool install foothold      # or: pipx install foothold

Three runtime dependencies: typer, rich, networkx. PageRank is implemented in pure Python specifically to avoid pulling ~100 MB of scipy and numpy into a CLI.

Commands

Command What it does Network
foothold map . Rank the files that hold the repo together none
foothold docs . -o ARCHITECTURE.md Write a deterministic architecture document with a Mermaid graph none
foothold issues . --max 10 Propose good-first-issue candidates, off the critical path none
foothold explain . --dry-run Print the exact payload a model would receive none
foothold explain . Prose walkthrough grounded in the ranked map OpenAI API
foothold docs . --narrate The same document, with an overview section OpenAI API

The two commands that cost money print an estimate and require confirmation; --yes is mandatory for non-interactive use.

GitHub Action

Listed on the GitHub Marketplace as Foothold reading path.

The minimum that does something useful — job summary only, no token, no write permission:

- uses: actions/checkout@v7
  with:
    fetch-depth: 0        # the churn signal needs real history
- uses: serdairy/foothold@v0.2.3
  with:
    top: "20"

Job summaries are easy to miss, so comment: "true" puts the result where reviewers already look. It posts one comment and edits that same comment on later pushes:

permissions:
  contents: read
  pull-requests: write

# ...
      - uses: serdairy/foothold@v0.2.3
        with:
          comment: "true"
Input Default What it does
path . Repository root to analyse
command map map, docs or issues
top 20 How many files to report
output ARCHITECTURE.md File written when command: docs
version latest Pin a foothold version, e.g. 0.2.3
summary true Write the result to the job summary
since Scope to files changed since a ref; auto uses the PR base branch
comment false Post the result as one pull request comment, edited in place
cache true Restore and save the parse cache between runs
github-token github.token Token used for that comment
fetch-history true Deepen a shallow checkout so churn has commits to read
python-version 3.12 Python that runs foothold, independent of the analysed project

The action exposes the output as steps.<id>.outputs.result, so you can post it wherever you like. It runs pip install foothold and nothing else — no container to pull, no code from the analysed repository is executed.

fetch-depth: 0 matters: a shallow clone has no history, so the churn term collapses to zero and the ranking degrades to pure graph structure. It still works, it is just less informative. If you forget it the action notices, deepens the checkout itself, and warns in the log when it cannot — a quietly worse ranking is the one failure mode you would never spot.

On pull requests from forks GitHub issues a read-only token, so the comment cannot be posted. The action warns and leaves the result in the job summary rather than failing the job.

Reviewing a change

--since asks a different question from map. Instead of "where is the centre of this project", it answers "what did this diff touch, and what else has to be read because of it":

$ foothold map . --since main
4 changed source file(s) · 8 file(s) importing them · 4 changed test file(s)

Changed files are ranked by the same weight as the full map, and the second table lists everything that imports them, in the order worth reading. Changed tests are listed separately rather than reported as unranked, because ranking excludes tests by design. In the action, since: auto resolves to the pull request's base branch.

Speed and the cache

Parsing is almost all of the cost: on django, 3.6s of a 4.0s run. Foothold caches what parsing yields, keyed by the SHA-256 of the file's bytes.

Repository Modules First run Cached run
rich 213 0.32s 0.06s
django 2,920 3.5s 0.40s

The cache lives in ~/.cache/foothold (or $XDG_CACHE_HOME, or $FOOTHOLD_CACHE_DIR), never inside the repository being read, so it cannot show up in someone else's git status. It is written 0600, because it holds docstrings from whatever was analysed. Only path-independent facts are stored — line count, public definitions, docstring, raw import statements. Anything derived from where the file lives is recomputed every run, so a moved or renamed file cannot carry a stale answer with it.

foothold cache prints the location and size; foothold cache --clear empties it; --no-cache skips it for one run.

How the ranking works

score = 0.45·pagerank + 0.30·churn + 0.15·fan-in + 0.10·log(loc)

Each term is min-max normalised across the repository, so scores compare within a repo but not across repos. The weights live in .foothold.toml and are printed in every generated document — a ranking you cannot interrogate is a ranking you cannot trust.

  • PageRank over the in-project import graph. Edges point importer → imported, so a module everything depends on scores high. External and stdlib imports are dropped: they add nodes without adding signal. (test_pagerank_ranks_dependencies_above_dependents guards the direction — reversing it silently inverts the whole tool.)
  • Churn from git log --since=18.months. A file edited in every release is a file a newcomer will have to touch. Repositories without git history degrade to a zero churn signal rather than failing.
  • Fan-in as a plain, legible count, so the top of the list is explainable without understanding PageRank.
  • Size, log-scaled, as a weak tiebreaker.

Tests are excluded from the ranking and used instead to detect untested modules.

What it sends, and what it does not

foothold explain . --dry-run prints the complete payload. It contains file paths, scores, entry points, import cycles and the first line of each module docstring. It does not contain source code — there is a test asserting exactly that.

The consequence is that context size tracks --top, not repository size:

Repository Modules Lines of code Context sent Budgeted tokens
foothold 31 1,284 1,999 chars 899
rich 99 38,437 2,314 chars 978
networkx 565 183,241 2,837 chars 1,109

A 183,000-line codebase is described in under 3 KB. Full numbers and method in docs/cost-model.md.

Foothold also never executes the code it reads — parsing is stdlib ast, which does not evaluate. See SECURITY.md.

Architecture

src/foothold/
├── cli.py              # Typer entry point
├── analyze.py          # orchestration: collect → graph → rank → RepoMap
├── models.py           # the shared vocabulary; imported by 10 modules
├── config.py           # .foothold.toml, ranking weights
├── collectors/         # python_ast · git_history · markers    (offline)
├── graph/              # build (import graph) · rank (pagerank + weights)
├── issues.py           # good-first-issue heuristics           (offline)
├── render/             # terminal · markdown · mermaid         (offline)
└── narrator/           # the only module that talks to a model

ARCHITECTURE.md is generated by foothold docs and refreshed at each release. It is deliberately not pinned by a CI equality check: churn is an input, so the ranking moves as history accumulates, and a byte-for-byte assertion would fail on every commit. What CI does assert is that the generator runs against this repository on all twelve OS and Python combinations.

What it will not do

Foothold reads repositories it has no reason to trust, so a few things are deliberate rather than accidental:

  • Parsing is stdlib ast. The analysed code is never imported or executed.
  • Files that resolve outside the analysed root are skipped, symlinks included. A repository cannot use a symlink to pull an unrelated file into the output.
  • git is invoked with an explicit argv and no shell. Refs beginning with - are refused, because git would read them as options rather than revisions.
  • Nothing leaves the machine unless you pass --narrate or run explain, and explain --dry-run prints the exact payload first: paths, scores, docstring first lines, never source.

Limitations

Stated plainly, because the alternative wastes your time:

  • Python only. Other languages are parsed as nothing. tree-sitter support is v0.3.
  • Dynamic imports (importlib, plugin registries, __getattr__ re-exports) are invisible to static analysis and will under-rank plugin-heavy architectures.
  • Churn needs real git history. CI must use fetch-depth: 0; shallow clones silently lose that signal.
  • Monorepos with several independent packages are ranked as one graph. v0.5.
  • Scores are comparable within a repository, never across repositories.
  • Scores also move over time within one repository: churn is measured over a rolling 18-month window, so the same commit ranked today and in six months can differ. The ranking describes a repository's present, not a fixed property of its files.

Roadmap

Version Scope Status
v0.1 map, docs, issues, explain; GitHub Action; Python shipped
v0.2 Content-hash cache; --since diff scope; PR comments shipped
v0.3 tree-sitter parsers: TypeScript, JavaScript, Go next
v0.4 tour with personas; PR-scoped reading paths planned
v0.5 Monorepo support; call-graph edges, not just imports planned
v1.0 Stable JSON schema; benchmark suite against hand-written docs planned

Non-goals: replacing hand-written design documents, reviewing code, running as a hosted service. Foothold is a local tool that produces files you own and commit.

Contributing

Foothold exists because onboarding is hard, so its own onboarding has to be good:

git clone https://github.com/serdairy/foothold && cd foothold
uv sync --all-extras
uv run foothold map .        # start here
uv run pytest                # 23 tests, ~0.2s, no network

If map gives you a confusing reading order on your project, open a bad ranking issue — that is the most useful report this project can receive. See CONTRIBUTING.md.

License

Apache-2.0 — chosen over MIT for the explicit patent grant, which matters for a tool that parses other people's code. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

foothold-0.2.3.tar.gz (111.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

foothold-0.2.3-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

File details

Details for the file foothold-0.2.3.tar.gz.

File metadata

  • Download URL: foothold-0.2.3.tar.gz
  • Upload date:
  • Size: 111.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for foothold-0.2.3.tar.gz
Algorithm Hash digest
SHA256 25c034c71eaac278bf5bd65ee4ba8e569ccae567d01c7e920189db956a0109ec
MD5 61facee898997f9e28cb3e9ac60947b9
BLAKE2b-256 d009cd3f50ff573ea82249776d38e567a78b9a3c07fed3e5e5a99d93184e70b4

See more details on using hashes here.

Provenance

The following attestation bundles were made for foothold-0.2.3.tar.gz:

Publisher: release.yml on serdairy/foothold

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file foothold-0.2.3-py3-none-any.whl.

File metadata

  • Download URL: foothold-0.2.3-py3-none-any.whl
  • Upload date:
  • Size: 36.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for foothold-0.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 a86d72e30c87e1d80cb9bc1754355ccec37482109e1ae0cffc7f56499aee2a6c
MD5 0f1559a4b4fcbf60fb742f938200386c
BLAKE2b-256 760a5bd59a5fb38304db531fa9df9ba63cddc7ee35a5cd3820c603808fb257fd

See more details on using hashes here.

Provenance

The following attestation bundles were made for foothold-0.2.3-py3-none-any.whl:

Publisher: release.yml on serdairy/foothold

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page