docs: 回写全仓库对契约套件的指向,以及 README、架构与 CHANGELOG
套件从 tests/contract/ 搬进 polyloop.testing 之后,全仓库 28 处引用要重新指过。修了 12 处, 其余在 design/(只增不改)与 scratch/(由人清理)里。 **CLAUDE.md 改了四处事实**:§0 权威表里行为契约的权威、§0 那句依赖规则的条数、§5 目录树与 模块数、§1.8 那句「谁断言公共 Protocol 的签名」。§1 的其余硬约束与 §2 的人类门一条没动。 **architecture.md**:分层图第 4 层加一格,装配层从三个变四个;代码地图加一行;第十节按代码 逐项重写——那笔「工具段渲染样式」的欠账**没有被数字对上盖掉**,加了 fingerprints 之后请求 的字段数恰好还是十一,而组成已经换过,所以那一节正面写着它仍然欠着;新增第十条依赖规则 (pytest 只在 testing 那个 extra 里,别处 import 它会让下游的生产环境一 import 本库就 ModuleNotFoundError),带静态与运行时两半;删掉「src/ 下一行代码都没有」那段过期状态说明; 决策索引补齐 0008 到 0016,其中四行原描述说的不是那份文档真正定的东西。 **migrations/dissect.md** 那笔「内存实现不存在」的欠账还掉了。 **压测那边**三条测试守的是一条已经撤销的公共契约,改名并写清它们现在守的是场景自己的选择。 AppWorld 那处刻意的偏离(不补三个反引号)留着不恢复——那条路径要模型输出被 stop 序列截断才 触发,而压测不配 stop 序列,恢复的收益不抵重跑一次压测的成本。但注释的理由改对了:它现在是 一笔有出处的欠账,不是一个决定。 CHANGELOG 攒在「未发布」段,版本号不提前写(§1.10)。
This commit is contained in:
@@ -5,15 +5,7 @@
|
||||
> 第 3 档是定期复审。能用强的就不用弱的,完整说明见 `../README.md`。
|
||||
>
|
||||
> 本文件的第七、八、九节是**第 1 档**:那三节讲的分层、模块边界与抽象接缝由 `pyproject.toml`
|
||||
> 的 import-linter 契约与 `tests/contract/` 断言。**其余章节是第 2 档**。
|
||||
>
|
||||
> **当前状态:本文件描述的是目标结构,`src/` 下一行代码都没有。** 常青层本该描述当前真实
|
||||
> 情况,而这份在代码之前就存在。接受这个例外的理由与它的过期条件见
|
||||
> `../../README.md` 的阶段清单第 ③ 条。`src/` 落地完成后删除本段。
|
||||
>
|
||||
> 由此带来一个读者必须知道的约定:**后文以现在时提到的 `polyloop/` 路径,指的是落地之后
|
||||
> 该内容所在的位置**,不一定是现在就能打开的模块。这么写是为了让这份文档在代码落地那天
|
||||
> 不需要逐句改时态。
|
||||
> 的 import-linter 契约与 `polyloop.testing` 那套契约套件断言。**其余章节是第 2 档**。
|
||||
>
|
||||
> **本文件与 design doc 冲突时以本文件为准。** design doc 写完就冻结,它记录的是当时定了
|
||||
> 什么;本文件描述的是现在是什么样。两者对同一件事都会提到,这是有意的——但理由只在
|
||||
@@ -247,13 +239,14 @@ PolyLoop 把这个循环收成一份。它治理的单位是**一次运行**:
|
||||
`A ──▶ B` 读作「A 的代码里写了 `from polyloop.B import ...`」,也就是 A 依赖 B。
|
||||
|
||||
```
|
||||
第 4 层 ┌───────────────┐ ┌───────────────┐ ┌────────────────┐
|
||||
装配层 │ session │ │ stores │ │ adapters │
|
||||
│ 定义、请求 │ │ jsonl / 内存 │ │ PolyGateway │
|
||||
│ run、resume │ │ 存储实现 │ │ 适配器 │
|
||||
└───────────────┘ └───────────────┘ └────────────────┘
|
||||
这三个互不 import:session 不认识任何具体实现,具体实现也不
|
||||
认识 session。把它们装到一起的是调用方,不是库自己
|
||||
第 4 层 ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
|
||||
装配层 │ session │ │ stores │ │ adapters │ │ testing │
|
||||
│ 定义、请求 │ │jsonl / 内存│ │ PolyGateway│ │ 契约套件 │
|
||||
│ run、resume│ │ 存储实现 │ │ 适配器 │ │ 准入基类 │
|
||||
└────────────┘ └────────────┘ └────────────┘ └────────────┘
|
||||
这四个互不 import:session 不认识任何具体实现,具体实现也不
|
||||
认识 session,而契约套件三个都不认识——它只认接缝定义。把它们
|
||||
装到一起的是调用方,不是库自己
|
||||
│
|
||||
▼
|
||||
第 3 层 ┌───────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐
|
||||
@@ -277,8 +270,9 @@ PolyLoop 把这个循环收成一份。它治理的单位是**一次运行**:
|
||||
不必逐层往下传——分层禁止的是往上,不是要求一层一层往下
|
||||
```
|
||||
|
||||
第 4 层里 `stores` 与 `adapters` 只够到第 2 层(它们 import `ports` 去实现那些 Protocol,
|
||||
再 import `types` 用那些数据类型),不需要碰第 3 层。
|
||||
第 4 层里 `stores`、`adapters` 与 `testing` 只够到第 2 层(前两个 import `ports` 去实现那些
|
||||
Protocol,再 import `types` 用那些数据类型;契约套件 import 同样这两处,用来给下游的实现出
|
||||
题),不需要碰第 3 层。
|
||||
|
||||
**存储实现在上面而不是下面,这一点最容易画反。** 直觉上存储是底层设施,该垫在最底下;
|
||||
但按 import 方向,是存储实现去 import 接口定义,所以它在接口之上。库的核心不认识任何具体
|
||||
@@ -287,19 +281,25 @@ PolyLoop 把这个循环收成一份。它治理的单位是**一次运行**:
|
||||
判据只有一句:**`polyloop.types` 是依赖图的汇点**——所有箭头最终都指向它,没有一根从它出去。
|
||||
它就是「字段只增不删不改名」保护的那份合同本身。
|
||||
|
||||
### 九条依赖规则
|
||||
### 十条依赖规则
|
||||
|
||||
这些规则在代码里是**看不见的**——打开 `polyloop/ports/` 只会看到一堆正常的 Protocol,
|
||||
看不到那里缺了什么。所以必须写下来,并且每一条都有对应的机器断言。
|
||||
|
||||
**一、分层,自高向低**:装配层(`session`、`stores`、`adapters`)> 逻辑层(`tools`、
|
||||
**一、分层,自高向低**:装配层(`session`、`stores`、`adapters`、`testing`)> 逻辑层(`tools`、
|
||||
`_assembly`、`_stopping`、`_recovery`、`serialization`)> `ports` > `types`。低层不许
|
||||
import 高层。
|
||||
|
||||
**二、装配层那三个互相独立。** `session` 不许 import `stores` 或 `adapters`,反过来也不许。
|
||||
这条不能靠分层规则表达——同一层的模块在分层契约里默认是可以互相 import 的,要另立一条独立性
|
||||
契约。它守的是「库不顺手提供任何默认实现」:`session` 一旦 import 了某个存储实现,那个实现
|
||||
就成了隐式默认,而不传存储的人不会知道自己这次运行没有恢复能力。
|
||||
**二、装配层那四个互相独立。** `session`、`stores`、`adapters`、`testing` 两两之间都不许
|
||||
import。这条不能靠分层规则表达——同一层的模块在分层契约里默认是可以互相 import 的,要另立
|
||||
一条独立性契约。它守的是「库不顺手提供任何默认实现」:`session` 一旦 import 了某个存储实现,
|
||||
那个实现就成了隐式默认,而不传存储的人不会知道自己这次运行没有恢复能力。
|
||||
|
||||
契约套件落在这一层、并且同样被这条独立性契约管住,有它自己的一条理由:本库声明了一个 pytest
|
||||
插件入口,所以每一个装了本库的下游项目在 pytest 启动时都会加载 `polyloop.testing`。它一旦
|
||||
import `adapters`,那次加载就会把网关连同它的 provider 目录一起拉起来——那正是规则九要防的
|
||||
事,只不过触发路径不是 `import polyloop`,而是 pytest 自动加载插件
|
||||
(`../design/0014-contract-suite-distribution.md` 决策三)。
|
||||
|
||||
**三、`tools`、`_assembly`、`_stopping`、`_recovery`、`serialization` 五者互不 import**,
|
||||
不设豁免。它们之间的编织只能发生在 `session` 里。理由是这五个模块各自要能被单独测穷,
|
||||
@@ -330,11 +330,18 @@ import 高层。
|
||||
**八、`ports` 不许 import 其余任何 `polyloop` 模块。** 第一条已覆盖,单列是为了让违规信息
|
||||
直接指向「接缝定义模块被污染了」,而不是一条泛泛的分层报错。
|
||||
|
||||
**九、`import polyloop` 之后,`sys.modules` 里不许出现 `polygateway`。** 这条由契约测试
|
||||
断言,不是 import-linter。顶层只再导出五个公开模块;`stores` 与 `adapters` 必须显式
|
||||
**九、`import polyloop` 之后,`sys.modules` 里不许出现 `polygateway`。** 这条由测试断言,
|
||||
不是 import-linter。顶层只再导出五个公开模块;`stores` 与 `adapters` 必须显式
|
||||
import。理由是一个「顺手提供的默认模型客户端」会让每个进程在 import 时把网关连同它的
|
||||
provider 目录一起拉起来。
|
||||
|
||||
**十、除 `polyloop.testing` 外一切不许 import `pytest`,且 `import polyloop` 之后
|
||||
`sys.modules` 里也不许出现 `pytest`。** 这条有两半,形状和规则五加规则九那一对相同:静态
|
||||
那半是一条 import-linter 契约,运行时那半是一条测试。守的事很具体——pytest 只住在 `testing`
|
||||
这个 extra 里,核心的运行时依赖是空的,所以别处 import 它,下游的生产环境一 `import polyloop`
|
||||
就 `ModuleNotFoundError`,而生产环境通常根本没装 pytest。契约套件自己顶层就 import pytest,
|
||||
所以它和 `stores`、`adapters` 同一档,不进顶层的再导出,必须显式 import。
|
||||
|
||||
## 八、代码地图:每个模块装什么
|
||||
|
||||
| 位置 | 公开 | 装什么 |
|
||||
@@ -349,11 +356,18 @@ provider 目录一起拉起来。
|
||||
| `polyloop/_recovery/` | 否 | 恢复状态判定与运行身份校验 |
|
||||
| `polyloop/stores/` | 是,须显式 import | 库自带的存储实现 |
|
||||
| `polyloop/adapters/` | 是,须显式 import | PolyGateway 模型适配器 |
|
||||
| `polyloop/testing/` | 是,须显式 import | 五个接缝的契约套件:每个接缝一个基类,下游继承它验自己的实现 |
|
||||
|
||||
`polyloop/types/` 的读者是所有人:dissect 的 runner 读步记录与停止原因把轨迹头拼回去,
|
||||
GovDoc 的编排层读停止原因决定这个阶段算不算可挽救,写任何适配器的人都要构造这里的结构体。
|
||||
|
||||
`polyloop/ports/` 的读者是写适配器的人和 `tests/contract/`。它用 `typing.Protocol` 而不是
|
||||
`polyloop/testing/` 住在包里而不是 `tests/` 下,因为 `tests/` 不进发行包:`pip install polyloop`
|
||||
之后 site-packages 里没有它,而这套用例正是下游实现接缝时的准入标准,拿不到的准入标准不成其为
|
||||
标准。下游的接法是继承基类、在自己的子类里覆盖那几个必需 fixture;它不进 `polyloop/__init__.py`,
|
||||
因为它顶层就 import pytest,而 pytest 只在 `testing` 这个 extra 里,核心的运行时依赖是空的
|
||||
(`../design/0014-contract-suite-distribution.md` 决策一)。
|
||||
|
||||
`polyloop/ports/` 的读者是写适配器的人和 `polyloop/testing/`。它用 `typing.Protocol` 而不是
|
||||
抽象基类,理由是下游的对象往往已经是它自己的类、还要同时满足项目自己更宽的接口;只有
|
||||
结构化子类型能让同一个对象同时满足库的窄视图和项目的宽视图。
|
||||
|
||||
@@ -459,9 +473,21 @@ GovDoc 的编排层读停止原因决定这个阶段算不算可挽救,写任
|
||||
文本。它是不可变的,可以被并发复用。它还有一个只读方法,把四个接缝各自上报的参数聚合成
|
||||
一份快照——那是方法不是字段,因为聚合要向接缝逐个发问,而构造定义之前定义还不存在。
|
||||
|
||||
**请求**每次运行构造一个,持有这次运行独有的十一样数据:运行标识、预算、动作执行器、本次
|
||||
可见的工具集、上下文各段、注入内容、模型绑定、模型调用的重放策略、观察包装模板、工具段
|
||||
渲染样式、取消收尾时限。它构造廉价——无 I/O、无网络校验、无哈希计算。
|
||||
**请求**每次运行构造一个,持有这次运行独有的十一样数据:运行标识、预算、动作执行接缝、本次
|
||||
可见的工具集、上下文各段、按通道分组的注入内容、模型绑定、这次用的材料是哪一版(指纹)、
|
||||
模型调用的重放策略、观察包装模板、取消收尾时限。它构造廉价——无 I/O、无网络校验、无哈希计算。
|
||||
|
||||
模型绑定和指纹形状相同:都是下游自己定键名的字符串映射,都整个进参数快照,库都不解释里面
|
||||
装的是什么。含义不同。绑定记的是这次运行属于哪一格(哪个账本、第几轮、哪道题),库还把它
|
||||
原样透传给每次模型调用;指纹记的是这次用的材料是哪一版(提示词模板的哈希、技能库的版本
|
||||
这类),只进快照,不透传。两者混在一个字段里事后分不开:一组键值里既有「第 3 轮」又有一个
|
||||
sha,要靠键名的命名约定去猜哪个是哪个,而命名约定不在任何一处被断言
|
||||
(`../design/0015-parameter-snapshot-contract.md` 决策一)。
|
||||
|
||||
**「工具段渲染样式」不在这十一样里,代码里也没有任何对应物。** `0003` 决策三的请求字段表
|
||||
列了它,而它从第一版落地起就没有被实现,这笔欠账今天仍然欠着——它不是被哪次改动还掉的。
|
||||
请求的字段数确实还是十一,但其中那一格现在是指纹:数字对得上,组成已经换过
|
||||
(`0015` 的「留给后续的」记着这件事)。
|
||||
|
||||
切点是「跨运行变不变」。预算只在请求这一处,不设「定义给默认值、请求可覆盖」——两处取值
|
||||
意味着「这次到底跑的什么设置」要对照两个地方才答得出来。
|
||||
@@ -511,18 +537,21 @@ dissect 的每一次运行都是论文数据点,这个性质对结构提了几
|
||||
|
||||
## 十三、这份文档靠什么不腐烂
|
||||
|
||||
第七、八、九节将来由机器断言,每一节各有对应的检查:
|
||||
第七、八、九节各有对应的机器检查:
|
||||
|
||||
- **第七节**——九条依赖规则,前八条各一条 import-linter 契约(第一条是分层契约,第二、三条
|
||||
是独立性契约,其余是禁止型契约),第九条一个契约测试。
|
||||
- **第七节**——十条依赖规则。规则一、二、三由同一条分层契约表达,规则八在那条里已经被覆盖,
|
||||
另单列一条只为让违规信息直接指向 `ports`,规则四、五、七各一条禁止型契约,规则十的静态那半
|
||||
也是一条禁止型契约。剩下的落在 `tests/unit/` 的测试上:规则六(不许 import 任何第三方)
|
||||
写不成契约,因为「任何第三方」不是一份可枚举的清单;规则九和规则十的运行时那半写不成契约,
|
||||
因为 `sys.modules` 里有谁是运行时事实,不是静态图上的边。
|
||||
- **第八节**——一个断言检查 `polyloop/` 下有哪些位置,和第八节那张表逐行对得上。表里有一行
|
||||
在代码里找不到、或者代码里多出一个没写进表的位置,都算失败。没有这一条,新加的模块会
|
||||
悄悄绕过第七节的分层规则——一个规则里没提到的模块,等于没有任何约束。
|
||||
- **第九节**——一个断言检查接缝的数量和位置没有悄悄增长;另一个断言检查 `polyloop/` 下
|
||||
不出现重试、限流、熔断的实现。
|
||||
|
||||
**写到哪一步了:一条都没写。** 这些断言要等 `src/` 落地才写得出来,在那之前这三节没有机器
|
||||
兜底,只能靠人在实现时逐条对照。这是「架构文档先于代码存在」这个安排最实在的代价。
|
||||
**写到哪一步了:第七节那十条已经全部有断言,第八、九节还一条都没有。** 那两节现在没有机器
|
||||
兜底,只能靠人在改代码时逐条对照——表里多一行少一行、接缝悄悄变成六个,CI 都不会响。
|
||||
|
||||
其余章节是第 2 档:改相关代码时,改本文件是同一个提交的一部分。
|
||||
|
||||
@@ -530,8 +559,8 @@ dissect 的每一次运行都是论文数据点,这个性质对结构提了几
|
||||
|
||||
**公共类型的字段与枚举取值不在本文件里。** 英文名与签名定在
|
||||
`../design/0006-public-names-and-signatures.md`,行为契约定在 `../design/0007-seam-behaviour.md`;
|
||||
落地之后权威转移到 `src/polyloop/` 的代码与 `tests/contract/`。本文件只给五个接缝的
|
||||
Protocol 名与模块归属,不复述字段表——按 `../../CLAUDE.md` §0,那种复述腐烂的速度和代码一样快。
|
||||
落地之后权威转移到 `src/polyloop/` 的代码与 `polyloop/testing/` 那套契约套件。本文件只给五个
|
||||
接缝的 Protocol 名与模块归属,不复述字段表——按 `../../CLAUDE.md` §0,那种复述腐烂的速度和代码一样快。
|
||||
|
||||
**停止判定的顺序、停止原因的取值、两个预算计数的语义、步记录的字段清单不在本文件里。**
|
||||
这四样已经定了,在 `../design/0004-stopping-and-step-record.md`,字段表经
|
||||
@@ -539,9 +568,6 @@ Protocol 名与模块归属,不复述字段表——按 `../../CLAUDE.md` §0
|
||||
`../../CLAUDE.md` §0,公共类型的字段与枚举取值的权威是 `src/polyloop/`,不另写参考文档复述。
|
||||
那份 design doc 记的是第一版为什么定成这样,不是查字段的地方。
|
||||
|
||||
**事件出口的事件类型还没定**,所以这个接缝的契约套件现在写不了。方向已经定了——观察走
|
||||
事件流、干预走具名回调——但事件集与回调清单要独立成篇。
|
||||
|
||||
**多模态内容的规模度量没有答案。** 消息内容是块序列而不是裸字符串,第一版只定义文本块,
|
||||
它的度量是准确的字符数。将来加图片块时必须同时给出它的度量定义,以及上下文上限在混合
|
||||
内容下的语义。
|
||||
@@ -562,6 +588,15 @@ Protocol 名与模块归属,不复述字段表——按 `../../CLAUDE.md` §0
|
||||
| 存储的方法为什么这么切、为什么要前缀持久性、步记录那三处为什么改 | `../design/0005-storage-atomicity-and-record-fields.md` |
|
||||
| 公共类型与接缝叫什么、字段是什么形状、类型分到哪个模块 | `../design/0006-public-names-and-signatures.md` |
|
||||
| 三个动作状态什么时候赋上、动作被拒绝时观察从哪来、解释器能不能抛异常 | `../design/0007-seam-behaviour.md` |
|
||||
| 工具的实现挂在哪个字段上、它缺了什么时候报错、注册表派生的执行器怎么填动作结果 | `../design/0008-tool-handlers.md` |
|
||||
| 字段顺序算不算一份对外承诺、公共数据类为什么一律只收关键字参数 | `../design/0009-keyword-only-public-types.md` |
|
||||
| 上下文按什么顺序拼、注入槽为什么在那个位置、规模怎么量 | `../design/0010-context-assembly.md` |
|
||||
| 逐行追加那个存储的文件布局、坏行怎么算、`fsync` 落在哪几处 | `../design/0011-jsonl-run-store.md` |
|
||||
| 适配器为什么不自己装配网关客户端、消息怎么拼成一次网关调用、网关的异常为什么原样穿出 | `../design/0012-gateway-model-client.md` |
|
||||
| 事件集为什么只有一个取值、事件带的是什么、具名回调清单为什么现在是空的 | `../design/0013-event-set-and-callbacks.md` |
|
||||
| 契约套件怎么发给下游、下游怎么接上它、内存存储实现为什么叫易失 | `../design/0014-contract-suite-distribution.md` |
|
||||
| 配方版本记进请求的哪个字段、注入的通道维度为什么保留、快照取值为什么必须是字符串 | `../design/0015-parameter-snapshot-contract.md` |
|
||||
| 环境故障为什么走返回值、动作执行接缝抛异常时库为什么不接管、模型调用那侧为什么反而捕获 | `../design/0016-action-executor-failure.md` |
|
||||
|
||||
边界的当前裁决清单(哪些在界内、哪些在界外)在 `scope.md`,那份是常青的,会随新消费者
|
||||
接入而更新。每个下游要迁什么、迁完算不算数在 `../migrations/` 下对应那份。
|
||||
|
||||
@@ -210,11 +210,13 @@ PolyLoop 的日志按运行标识分文件(`../design/0011-jsonl-run-store.md`
|
||||
**要选一个存储实现并给它一个目录。** 存储接缝是必填的,不传就装配不起来。dissect 要恢复
|
||||
能力,所以装 `JsonlRunStore`,给它一个目录。
|
||||
|
||||
**「不提供恢复的内存实现」现在不存在。** `../design/0003` 的否决方案那一节定过:存储接缝
|
||||
必填,另外给一个明确命名的、不提供恢复的实现,好让「我不要恢复」成为一次看得见的选择而不是
|
||||
一个可以忘记传的参数。那个实现至今没写,`polyloop.stores` 里只有 `JsonlRunStore`。这一条对
|
||||
dissect 不构成阻塞——它本来就要恢复——但**不要照着那句话去找一个不存在的类**。真需要它的
|
||||
那天再补,补的时候要连同契约套件一起过。
|
||||
**「不提供恢复的内存实现」叫 `VolatileRunStore`。** `../design/0003` 的否决方案那一节定过:
|
||||
存储接缝必填,另外给一个明确命名的、不提供恢复的实现,好让「我不要恢复」成为一次看得见的
|
||||
选择而不是一个可以忘记传的参数。它现在和 `JsonlRunStore` 一起住在 `polyloop.stores`,两个
|
||||
实现跑的是同一套契约套件。它不提供的是**跨进程恢复**:日志随进程一起消失,选它就是选「我
|
||||
不要跨进程恢复」。名字为什么落在「易失」而不是「不提供恢复」上,见
|
||||
`../design/0014-contract-suite-distribution.md` 决策五。这一条对 dissect 不构成阻塞——它本来
|
||||
就要恢复。
|
||||
|
||||
**模型绑定要从关键字参数还原。** dissect 现在给每次调用传五个关键字参数(账本、轮次、
|
||||
阶段、题目、尝试序号)。库这边接的是一个字符串映射,所以适配器要做一次还原(把账本那个
|
||||
|
||||
Reference in New Issue
Block a user