serdairy/foothold
GitHub: serdairy/foothold
Foothold 是一个开发者工具,通过分析 Python 代码库的 import 图和 git 编辑历史,为新贡献者生成确定性的文件阅读路径和架构文档。
Stars: 0 | Forks: 1
# Foothold
[](https://github.com/serdairy/foothold/actions/workflows/ci.yml)
[](https://pypi.org/project/foothold/)
[](https://pypi.org/project/foothold/)
[](LICENSE)
[](https://mypy-lang.org/)
[](https://github.com/astral-sh/ruff)
**我应该先阅读哪 20 个文件?** Foothold 能在不到一秒的时间内为一个 Python 代码库回答这个问题,而且无需 API key —— 这是你在开始攀登陌生的代码库之前所需的立足点。
```
$ 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 │
└───┴───────────────────────────┴───────┴────┴──────┴──────────────────────────┘
```
这种排序并非猜测。它源自 import graph,并根据每个文件的编辑频率进行了加权。
## 问题所在
贡献者入职是开源中最昂贵的无酬工作,而且它要被支付两次 —— 一次是由那些花整个周末去弄清楚 400 个文件中哪些重要的新手支付,另一次是由在每个 issue 中回答相同引导问题的维护者支付。
通常的缓解措施并不奏效。`ARCHITECTURE.md` 在项目初期编写一次,并在两次发布内就会产生偏差。生成的 API 参考列出了所有符号,却没有对它们进行排序。将代码库粘贴到聊天窗口中,对于 networkx 这样规模的项目大约需要耗费 46,000 个 token,并且生成的流畅文本完全没有基于实际的 import graph。
Foothold 将问题一分为二。**排序是确定性的** —— 一个 graph、一个 churn 数量、一个你可以读懂的公式。**文本说明是可选的**,并且建立在一个已经正确且已经过精简的选择之上。昂贵的部分是不需要模型的那部分。
## 安装
```
uv tool install foothold # or: pipx install foothold
```
三个运行时依赖项:`typer`、`rich`、`networkx`。PageRank 使用纯 Python 实现,专门为了避免将大约 100 MB 的 scipy 和 numpy 引入 CLI。
## 命令
| 命令 | 作用 | 网络 |
|---|---|---|
| `foothold map .` | 对支撑代码库的文件进行排序 | 无 |
| `foothold docs . -o ARCHITECTURE.md` | 编写带有 Mermaid 图的确定性架构文档 | 无 |
| `foothold issues . --max 10` | 提出不在关键路径上的 good-first-issue 候选者 | 无 |
| `foothold explain . --dry-run` | 打印模型将接收到的确切 payload | 无 |
| `foothold explain .` | 基于排序图的文本讲解 | OpenAI API |
| `foothold docs . --narrate` | 具有概述部分的相同文档 | OpenAI API |
这两个花钱的命令会打印估算值并需要确认;对于非交互式使用,`--yes` 是必需的。
## GitHub Action
将阅读路径放在每个 pull request 的作业摘要中。无需 token,无需写入权限,也无需任何配置:
```
- uses: actions/checkout@v7
with:
fetch-depth: 0 # the churn signal needs real history
- uses: serdairy/foothold@v0.1.3
with:
top: "20"
```
| 输入项 | 默认值 | 作用 |
|---|---|---|
| `path` | `.` | 要分析的项目根目录 |
| `command` | `map` | `map`、`docs` 或 `issues` |
| `top` | `20` | 报告的文件数量 |
| `output` | `ARCHITECTURE.md` | 当指定 `command: docs` 时写入的文件 |
| `version` | latest | 固定一个 foothold 版本,例如 `0.1.3` |
| `summary` | `true` | 将结果写入作业摘要 |
| `python-version` | `3.12` | 运行 foothold 的 Python 版本,与分析的项目无关 |
该操作将输出公开为 `steps..outputs.result`,因此你可以将其发布到你喜欢的任何位置。它仅运行 `pip install foothold` 而不执行其他任何操作 —— 无需拉取 container,也不会执行来自被分析代码库的任何代码。
`fetch-depth: 0` 非常重要:浅克隆没有历史记录,因此 churn 值会降至零,排序也会退化为纯粹的图结构。它仍然有效,只是提供的信息较少。
## 排序的工作原理
```
score = 0.45·pagerank + 0.30·churn + 0.15·fan-in + 0.10·log(loc)
```
每一项都在整个代码库内进行了 min-max 归一化,因此得分可以在代码库内进行比较,但不能跨代码库比较。权重保存在 `.foothold.toml` 中,并打印在每个生成的文档里 —— 无法被质疑的排序就是无法被信任的排序。
- 基于 project 内 import graph 的 **PageRank**。边的方向指向 *导入者 → 被导入者*,因此所有人都依赖的 module 得分很高。外部和 stdlib 的 import 被丢弃:它们增加了节点却没有增加信号。(`test_pagerank_ranks_dependencies_above_dependents` 保护了此方向 —— 将其反转会悄无声息地颠倒整个工具的逻辑。)
- 来自 `git log --since=18.months` 的 **Churn**。每次发布都会被编辑的文件也就是新手必须接触的文件。没有 git 历史记录的代码库会退化为零 churn 信号,而不是直接失败。
- 作为简单、易读计数的 **Fan-in**,因此无需了解 PageRank 即可解释列表的顶部。
- **Size**,经过对数缩放,作为微弱的决胜条件。
测试被排除在排序之外,并用于检测未经测试的 module。
## 它发送了什么,不发送什么
`foothold explain . --dry-run` 会打印完整的 payload。它包含文件路径、得分、entry point、import cycle 以及每个 module docstring 的第一行。**它不包含源代码** —— 有一个专门针对此点的测试。
结果是,上下文大小取决于 `--top`,而不是代码库大小:
| 代码库 | Module | 代码行数 | 发送的上下文 | 预算 token |
|---|---:|---:|---:|---:|
| foothold | 31 | 1,284 | 1,999 字符 | 899 |
| rich | 99 | 38,437 | 2,314 字符 | 978 |
| networkx | 565 | 183,241 | 2,837 字符 | 1,109 |
一个 183,000 行的代码库被描述在不到 3 KB 的内容中。完整的数字和方法见 [docs/cost-model.md](docs/cost-model.md)。
Foothold 也从不执行它读取的代码 —— 解析使用的是不会进行求值的 stdlib `ast`。请参阅 [SECURITY.md](SECURITY.md)。
## 架构
```
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](ARCHITECTURE.md) 由 `foothold docs` 生成,并在每次发布时刷新。它刻意没有被 CI 的相等性检查固定:churn 是一个输入项,因此排序会随着历史记录的积累而发生变动,逐字节的断言在每次提交时都会失败。CI 所断言的是,该生成器能在所有十二种 OS 和 Python 组合上针对此代码库运行。
## 局限性
明确指出,因为替代方案会浪费您的时间:
- **仅支持 Python。** 其他语言被解析为空。tree-sitter 支持将在 v0.3 版本提供。
- 动态 import(`importlib`、插件注册表、`__getattr__` 重导出)对静态分析是不可见的,这将导致重度依赖插件的架构排名偏低。
- Churn 需要真实的 git 历史记录。CI 必须使用 `fetch-depth: 0`;浅克隆会悄无声息地丢失该信号。
- 包含多个独立 package 的 Monorepo 被作为一个 graph 进行排序。将在 v0.5 版本支持。
- 得分只能在代码库内比较,绝不能跨代码库比较。
- 得分也会在同一个代码库内随着时间的推移而变化:churn 是通过滚动 18 个月的窗口来衡量的,因此相同的提交在今天和六个月后的排序可能会有所不同。排序描述的是代码库的当前状态,而不是其文件的固定属性。
## 路线图
| 版本 | 范围 | 状态 |
|---|---|---|
| **v0.1** | `map`、`docs`、`issues`、`explain`;GitHub Action;Python;86% 代码覆盖率 | **已发布** |
| v0.2 | 内容哈希缓存;基于 diff 的增量重新分析;`--since` | 下一步 |
| v0.3 | tree-sitter 解析器:TypeScript、JavaScript、Go | 计划中 |
| v0.4 | 带有角色的 `tour`;基于 PR 范围的阅读路径 | 计划中 |
| v0.5 | Monorepo 支持;调用图边,而不仅仅是 import | 计划中 |
| v1.0 | 稳定的 JSON schema;针对手写文档的基准测试套件 | 计划中 |
非目标:取代手写的设计文档、审查代码、作为托管服务运行。Foothold 是一个本地工具,可生成由你拥有并提交的文件。
## 许可证
Apache-2.0 —— 选择它而不是 MIT 是因为其明确的专利授权,这对于一个解析他人代码的工具来说非常重要。请参阅 [LICENSE](LICENSE)。
标签:Petitpotam, Python, SOC Prime, 云安全监控, 代码分析, 代码阅读, 凭证管理, 开发工具, 无后门, 特权检测, 逆向工具, 静态分析