内容
# Heddle
[](https://github.com/davet47/heddle/actions/workflows/ci.yml)

**Heddle 将软件单元视为内容寻址的合约,而不是文件。** 一个使代理再生循环变得廉价的MCP服务器。
因为合约是内容寻址且依赖感知的,所以代理重用验证,精确计算爆炸半径,并从几百个上下文令牌中再生代码,而不是重新读取整个文件。构建系统询问哪些文件更改了。Heddle询问哪些软件义务更改了。
Heddle 是织机的一部分,它持有经纱线程,即固定的、耐用的线束,而梭子则在其中编织可丢弃的纬线。**合约是经纱。代码是纬线。**
## 问题
代理反复支付以重新发现软件结构。规范驱动的开发工具使得规范成为耐用工件,而代码是可再生的,但它们运行在普通文件上,因此每个再生循环都会重新导出项目已经知道的内容:
1. **上下文获取很昂贵。** 再生一个单元意味着重新读取整个规范和源文件:数千个令牌来学习少数几百个令牌。
2. **验证未被缓存。** 每次再生都重新运行(并重新读取)整个相关测试表面,即使单元的合约没有更改。
3. **爆炸半径是基于约定,而不是机制。** 当规范更改时,没有人告诉代理哪些依赖项被无效。
## 模型
Heddle 将每个软件单元视为具有显式依赖关系的 内容寻址合约,而不是文件。合约是小型YAML规范(签名、不变性、示例、依赖关系名称);其背后的实现是可再生的纬线。因为每个合约都有哈希值,其依赖关系都已命名,所以代理从文件中重新导出的结构成为Heddle计算和提供的内容。
## 结果
该模型带来了三件事,都是机械的:
- **验证缓存。** 绿色测试结果以合约、实现和依赖关系的哈希值为索引,从缓存中提供,直到其中一个更改。pytest仅在真正失败时运行。
- **机械爆炸半径。** 合约更改报告精确的无效依赖项集,通过哈希值传递,不基于约定。
- **微小的上下文数据包。** 重新生成单元的代理获取合约、其依赖关系的签名及其调用者,作为一个几百个令牌的数据包,而不是整个文件闭包。
### 数字
对20个合约样本项目执行相同的三个再生任务,一次使用原始文件读取,一次使用Heddle(tiktoken cl100k,通过 `uv run python bench/benchmark.py` 复制):
| 任务 | 原始文件 | Heddle | 减少 |
| --- | --- | --- | --- |
| revenue_by_region | 1925 | 369 | 5.2倍 |
| top_customers | 2137 | 337 | 6.3倍 |
| revenue_by_category | 1942 | 396 | 4.9倍 |
| **总计** | **6004** | **1102** | **5.4倍** |
原始模式计算基于文件代理每任务读取的内容:单元的规范文件、每个传递依赖关系的规范文件、依赖关系闭包中的每个源模块、单元的测试文件和运行测试套件的输出。它故意对基准测试非常宽容:它假设代理已经知道确切的依赖关系闭包,而这正是Heddle为您计算的内容。
相同的方法通过 `bench/sweep.py` 扫描 *每个* 示例项目的三个单元 -
Python、Go和TypeScript - 并且适用于任何Heddle项目,包括您自己的项目。完整的扫描平均值低于门控(它们计算几乎没有受益的叶类型);比率跟踪依赖关系深度,因此更深的项目得分更高。所有数字、分布和诚实的警告都位于 [docs/benchmarks.md](docs/benchmarks.md)。
## 快速开始
```bash
pip install heddle-mcp
# 或从源代码:pip install "git+https://github.com/davet47/heddle"
cd your-project
heddle init # 创建 .heddle/ 和 contracts/
heddle index # 从 contracts/ 构建存储
```
指向 Claude Code:
```bash
claude mcp add heddle -- heddle serve
```
(标准输入输出传输;服务器通过从工作目录向上遍历到最近的 `.heddle/` 解析项目。)
不熟悉工作流程?[docs/getting-started.md](docs/getting-started.md) 通过构建一个包合约优先的代理来讲解 - 给它的工作规则,审查循环和验证门控。
## 合约
`contracts/` 中每个单元一个YAML文件。最小,可写,哈希值:
```yaml
name: revenue_by_region
signature: "(sales: list[Sale]) -> dict[Region, float]"
deps: [Sale, Region] # 其他合约名称
invariants:
- 排除已完成为false的销售
- 排除具有空金额的销售
examples:
- 输入: "[Sale(region='QLD', amount=10, completed=True)]"
输出: "{'QLD': 10.0}"
tests: [tests/test_revenue.py::test_revenue_by_region] # pytest 节点ID
impl: src/revenue.py::revenue_by_region # 当前编织的纬线
status: inferred # 反向工程,还未人工审查;省略后确认
```
子目录是命名空间:`contracts/billing/invoice.yaml` 是合约
`billing/invoice`,因此相同的短名称可以存在于不同的文件夹中。合约的 `name` 必须与其在 `contracts/` 下的路径匹配。
### 何时编写合约
合约属于一个稳定的接口:其他单元依赖于它,并且您希望它能持续下去。其背后的实现是可丢弃的纬线,可以自由再生。放弃一个合约而不让它获得应有的地位是正确的使用,而不是失败。失败模式是相反的情况,即过度固定内部结构,您愿意重写的内容变成了繁琐的工作。
合约是经过审查的工件。编写一个合约很便宜,并且越来越便宜,所以真正的成本是审查它,而不是编写它。错误的合约比没有合约更糟糕,因为耐用的工件现在已经存在:代理将根据一个本身不正确的规范重新生成代码。在审查合约时,按照审查接口的方式进行审查,而不是像浏览生成代码一样。
合约可以由代理从现有代码反向工程,并声明它尚未获得审查:`status: inferred`。然后工具会标记 - 永远不会拒绝 - 任何爆炸半径或验证答案依赖于它(`inferred: true` 在依赖项上,`inferred` 列表在验证结果上,`status` 中的审查队列)。不存在意味着 `confirmed`,并且在审查后确认推断的合约是免费的:状态是出处,而不是含义,因此切换无效。
### 哈希语义
- **合约哈希**:sha256超过规范形式:键排序、空格标准化、注释剥离、示例顺序保留、依赖关系顺序忽略。`impl`、`tests`、`invariants` 和 `status` 被排除,因此 **重定位文件永远不会失效**,重新措辞不变,确认推断的合约永远不会失效。 不变性是文档,而不是机器义务;真正的检查是测试,其源代码在验证密钥中。
- **实现哈希**:sha256超过实现的规范AST,因此重新格式化和注释编辑永远不会破坏缓存。 文档字符串也被剥离。
- **验证密钥**:`(合约哈希,实现哈希,测试源哈希,工具链标识,传递依赖合约哈希)`。 Heddle缓存验证结果,其密钥为:如果闭包中的任何合约发生更改,实现更改,测试的源代码更改或工具链版本强制重新运行,则 ** toolchain** 组件(`python 3.11.7`,`go 1.21.5`,`node <v> ts <v>`)使得共享或跨机器的绿色声音:3.11通过永远不会提供给3.13。失败永远不会从缓存中提供。两个警告。缓存的绿色结果假设确定性测试,因此依赖于挂钟时间,网络或随机性的绿色结果可能会比条件过期(见 [Roadmap](ROADMAP.md))。
## MCP工具(整个表面)
| 工具 | 功能 |
|---|---|
| `get_contract` | 约300个令牌的上下文数据包:合约 + 哈希 + 一行依赖签名 + 调用者列表 |
| `put_contract` | 验证,写入 `contracts/<name>.yaml`,返回新哈希,语义更改差异和所有失效的依赖项 |
| `get_dependents` | 爆炸半径查询,直接或传递,名称 + 哈希;推断(未审查)的合约被标记 |
| `verify` | 每单元 `cached-pass` / `pass` / `fail` 加上顶级 `ok` 门控位;`radius=true` 将每个名称扩展到其完整爆炸半径;仅在缓存未命中时运行测试;失败以 ≤40个令牌的断言摘要返回,从不 traceback;推断合约在闭包中被标记 |
| `status` | 脏合约,过时验证,缓存命中率,已解析验证解释器,累计令牌计数器 |
每个工具返回结构化错误 - `{"error": {"code": "unknown_dep", "message": "'Regoin' not found — nearest: 'Region'"}}` - 从不堆栈跟踪。
### 验证解释器
`verify` 使用项目的python运行测试,解析顺序:
`heddle serve --python PATH` → `.heddle/config.json` (`{"python": "..."}`) → 自动检测的 `<project>/.venv` → 运行heddle的解释器。因此全局安装的heddle可以针对自己的虚拟环境验证项目,而无需安装到其中;`heddle status` 显示它解析的解释器。
`.heddle/config.json` 还接受 `verify_timeout`(每个pytest运行的秒数,默认为300)和 `pycache_trust`(默认为 `true`);设置 `pycache_trust: false` - 或传递 `--no-pycache-trust` - 在每次验证运行前清除项目的 `__pycache__`,以便过时的 `.pyc` 永远不会遮蔽当前源代码。
### 超越Python
实现的文件扩展名选择语言适配器 - 每个适配器都有一个规范的AST哈希器和一个测试运行器,因此相同的美观与意义哈希语义适用于每个语言:
- **Go** (`.go`):通过stdlib `go/ast`哈希,运行 `go test -json`。工具链像解释器一样解析:`.heddle/config.json` `{"go": "..."}`,否则PATH上的`go`。
- **TypeScript** (`.ts`/`.tsx`/`.mts`/`.cts`):通过目标项目 *自己的* `typescript`编译器API哈希,自动从 `package.json` 检测测试运行器(vitest / jest,否则Node的内置`node:test`)。需要Node >= 22.6;配置键 `{"node": "..."}`。
Python仍然是默认语言;项目可以自由混合语言。
### 团队共享验证缓存
默认情况下,缓存位于本地 `.heddle/store.db`。指向共享后端,一个绿色验证为整个团队服务 - CI一次验证一个单元,每个团队成员的代理获得 `cached-pass`:
```jsonc
// .heddle/config.json
{"shared": {"url": "https://cache.internal:8770", "token": "..."}}
```
使用 `python -m heddle.cache_server` 在任何地方运行后端(一个小型 bearer-token HTTP服务,基于SQLite存储;一个操作过程,而不是CLI命令)。只有绿色被发布 - 失败永远不会跨越边界 - 工具链中的密钥规则以上保持共享绿色在机器上的声音。如果共享存储无法访问,验证会静默降级为本地。
## CLI
`heddle init` · `heddle index` · `heddle serve` · `heddle status` · `heddle verify`。 `.heddle/`下的sqlite存储是派生状态:删除它并使用 `heddle index` 从 `contracts/` 重新构建。
`heddle verify <name>…` 从命令行运行与MCP工具相同的缓存验证,并以非零退出,除非每个单元都是绿色(失败,未知名称或无法验证的单元都会阻止)- 将其放入CI或预提交钩子。 `--radius` 将每个名称扩展到其完整爆炸半径,因此一个命令控制更改的整个爆炸半径:`heddle verify --radius Sale`.
## 试用示例项目
```bash
cd examples/sales
heddle init && heddle index && heddle serve # 然后将您的代理指向它
```
20个合约,25个测试,三层依赖关系。
还有Go的对应示例在 `examples/go-ledger` (8个合约,一个小型双重输入账本,相同的循环:`heddle init && heddle index`,然后 `heddle verify --radius Entry` 使用 `go test` 控制爆炸半径 - 需要Go工具链)和TypeScript的示例在 `examples/ts-cart` (8个合约,一个购物车;首先为其 `typescript` 安装 `npm install`,然后使用Node的内置 `node:test` 运行验证,Node >= 22.6)。
## 开发
```bash
uv sync
uv run pytest # 完整套件;哈希稳定性是承载套件
uv run python bench/benchmark.py
```
故意保持小:5个MCP工具,5个CLI命令,合约为纯YAML。 此README中没有的内容都是一个 [问题](ISSUES.md)。
## 许可证
Apache 2.0