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:
@@ -9,6 +9,102 @@
|
||||
|
||||
## 未发布
|
||||
|
||||
**版本号在真的要发布的那一刻才定,这里不提前写。** 只 bump 版本号不叫发布(`CLAUDE.md`
|
||||
§1.10):PolyGateway 的 1.0.6 与 1.1.0 都完成了版本号 bump 与 CHANGELOG,却从未上传,registry
|
||||
长期停在 1.0.5,下游 `pip install` 拿不到任何修复且无人发现。提前把号写进这一段就是在重演
|
||||
那个形态——读到号的人会以为那一版已经在 registry 上,而它不在。
|
||||
|
||||
这一批改动回应的是下游项目在 PolyLoop 仓库上提的五个 issue。
|
||||
|
||||
### 契约套件随包发布
|
||||
|
||||
五个接缝的公共行为一致性用例从 `tests/contract/` 搬进 `polyloop.testing`,跟着 wheel 一起
|
||||
装到下游去。**`pip install polyloop` 之后 site-packages 里没有 `tests/`**,所以在此之前那套
|
||||
被称作「任何新适配器的准入标准」的用例,第一个下游根本拿不到。
|
||||
|
||||
**接法同时换了**:原来是在自己的 `conftest.py` 里覆盖同名 fixture,现在是继承契约基类。
|
||||
|
||||
```python
|
||||
from polyloop.testing import RunStoreContract
|
||||
|
||||
class TestMyPostgresStore(RunStoreContract):
|
||||
@pytest.fixture
|
||||
def store(self, pg_pool): return MyPostgresStore(pg_pool)
|
||||
```
|
||||
|
||||
换掉是因为覆盖同名 fixture 在装到 site-packages 之后无处落脚——pytest 的 `conftest.py` 只沿着
|
||||
被收集文件的目录链往上找,而套件所在的那条链在 site-packages 里,看不见下游仓库里的
|
||||
`conftest.py`。继承则不需要任何 `conftest.py` 魔法:子类定义在下游自己的测试文件里。顺带一个
|
||||
接缝可以接多个实现,各写一个子类。
|
||||
|
||||
跑它要装 `polyloop[testing]`(pytest 与 pytest-asyncio,版本只写下界,不和下游已经在用的
|
||||
pytest 打架)。async 用例的事件循环归下游管:把 `asyncio_mode` 设成 `"auto"`,或者自己给子类
|
||||
打标记。本包还声明了一个 pytest 插件入口,它唯一的作用是让 pytest 重写这些模块里的
|
||||
`assert`,失败时打印出等号两边的实际值。
|
||||
|
||||
**基类名、基类上的 fixture 名、每一条用例的方法名从此是公共承诺**,改名的代价和改公共类型的
|
||||
字段一样。
|
||||
|
||||
### 新增一个内存存储实现
|
||||
|
||||
`polyloop.stores.VolatileRunStore`:日志攒在进程内存里,进程一退就没了。它不提供的是**跨进程
|
||||
恢复**,不是「读不回来」——同一个进程里写进去的意图照样读得回来,它和逐行追加那个实现跑的是
|
||||
同一套存储契约。桶里存的是编码后的载荷、读的时候才解码,所以调用方后来改自己手里那个 dict
|
||||
改不到已经写下去的快照,读回来的日志被就地改动也污染不了存储本身——落盘那个实现每次都重新
|
||||
解析文件,天然如此,这个实现靠同一条路径对齐它。一条字段类型不对的记录在两个实现上的下场也
|
||||
一样:写得进去,读的时候抛同一个解码错误。
|
||||
|
||||
对下游的意义是测试和「我不要跨进程恢复」那一档不必再自己写一个存储:存储接缝是必填的,
|
||||
在此之前不想落盘的人只能自己造一个。关系数据库那种形态仍然由下游自己实现。
|
||||
|
||||
### `RunRequest` 新增 `fingerprints` 字段
|
||||
|
||||
`Mapping[str, str]`,默认空映射,**已有代码不受影响**。它记的是这次运行用的材料是哪一版——
|
||||
提示词模板的哈希、技能库的版本这类——全部键值以 `request.fingerprint.<name>` 进参数快照,
|
||||
不透传给模型调用。
|
||||
|
||||
它和 `model_binding` 的分界是「坐标还是配方版本」:绑定记这次运行属于哪一格(哪个账本、
|
||||
第几轮、哪道题),指纹记这次用的材料是哪一版。**一条指纹都没有时快照里一个键都不写**,
|
||||
所以今天已经在跑的配置算出来的快照逐字节不变。
|
||||
|
||||
同时补上一道校验:`fingerprints` 与 `model_binding` 的键值必须都是字符串,在构造请求时就拒绝
|
||||
非字符串;五个接缝 `parameters()` 上报的键值在聚合成快照时同样校验。在此之前这类值要等到续跑
|
||||
读日志的那一刻才炸——那时这次运行已经跑完、钱已经花了。
|
||||
|
||||
### ⚠ 参数快照里注入内容的键形状变了
|
||||
|
||||
**这是这一批里唯一一处会让已有行为变化的改动。** 注入的条目标识原来拍平成一个键
|
||||
`request.injected_entry_ids`,现在按通道分组,一个通道一个键
|
||||
`request.injected_entry_ids.<通道名>`。
|
||||
|
||||
**用旧版本跑了一半的运行,升级之后 `resume` 会抛 `ParameterDriftError`。** 失败是响亮的,不是
|
||||
静默的:错误信息把漂移的键逐个列出来,能看到旧的那个键消失、新的那几个键出现。处置有两条,
|
||||
和快照里任何一项变了时一样——那次运行重新开始,或者接受它跑不完。跑完了的运行不受影响,
|
||||
参数快照只在续跑时被比对。
|
||||
|
||||
改形状是因为拍平之后「声明了这个通道但一条都没选中」和「压根没有这个通道」得出同一个结果,
|
||||
而有下游要比较的正是这两种情形。分组之后前者是一个值为空串的键,后者是这个键不存在。通道
|
||||
之间按通道名字典序,通道内的顺序和贴进提示词的顺序一致。
|
||||
|
||||
**快照的键集合从来不是公共承诺**,所以这不是 `CLAUDE.md` §1.3 意义上的破坏性变更:下游换一个
|
||||
存储实现、改一个接缝的 `parameters()` 返回什么,快照照样会变、续跑照样报漂移,这本来就是这
|
||||
套设计的一部分。
|
||||
|
||||
### 动作执行接缝的契约补上「抛异常时会怎样」
|
||||
|
||||
`ActionExecutor` 的 docstring 原来只说了动作本身报错算「已执行」,实现方干脆不返回、直接抛出
|
||||
时会怎样一个字都没写,而库这一侧不捕获。现在正面写清三条:环境自己坏了(连不上、协议不对、
|
||||
会话没了)返回 `ActionStatus.ENV_ERROR`,不要以异常表达;真抛出来的异常库不捕获,原样穿出
|
||||
`run()` 与 `resume()`;`asyncio.CancelledError` 必须原样穿过。
|
||||
|
||||
**行为没有变,变的是它被写下来了。** 库不把执行器抛出的异常转成 `ENV_ERROR`,是因为那样会
|
||||
把适配器自己的 bug 伪装成环境故障送进下游的统计,而且要替这一步编一条步记录——那条记录会被
|
||||
恢复读成「上一步走完了」,把一个真正未知的状态抹成一个具体的值。异常抛出时日志停在「动作
|
||||
意图有、步记录无」,恢复照实把它读成状态未知。
|
||||
|
||||
把可预期的环境异常(连接超时、会话已关闭)捕获并返回 `ENV_ERROR` 是适配器的正常工作,不算
|
||||
`CLAUDE.md` §1.7 禁的那种吞错误——错误没有被吞,它变成了一个明确的状态值。
|
||||
|
||||
## 1.0.1(2026-08-11)
|
||||
|
||||
首个发布版本。十个模块全部落地,四层测试都在跑。
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
| 这类事实 | 权威处 |
|
||||
|---|---|
|
||||
| **哪些事归本库管、哪些不归**,以及判据 | `research-wiki/explanation/scope.md` |
|
||||
| 分层、依赖方向、模块边界 | `research-wiki/explanation/architecture.md`,由 `pyproject.toml` 的 import-linter 契约机器断言。九条依赖规则里有两条落不进契约(「不许 import 任何第三方」不是可枚举清单,「import 之后 `sys.modules` 里没有谁」是运行时事实),它们是 `tests/unit/` 里的测试 |
|
||||
| 公共 API 的行为契约:一次 `run` 到底保证什么、边界条件怎么结算 | `tests/contract/` 的公共契约套件。它同时是任何新适配器的准入标准 |
|
||||
| 分层、依赖方向、模块边界 | `research-wiki/explanation/architecture.md`,由 `pyproject.toml` 的 import-linter 契约机器断言。十条依赖规则里有两条落不进契约、还有一条只有一半落得进(「不许 import 任何第三方」不是可枚举清单,「import 之后 `sys.modules` 里没有谁」是运行时事实),落不进的那些是 `tests/unit/` 里的测试 |
|
||||
| 公共 API 的行为契约:一次 `run` 到底保证什么、边界条件怎么结算 | `src/polyloop/testing/` 的公共契约套件,随包发布。它同时是任何新适配器的准入标准 |
|
||||
| 公共类型的字段、不变量、枚举取值 | `src/polyloop/` 的代码与其测试。**不另写一份参考文档复述它们**——那份文档不重复代码的内容太少,而它腐烂的速度和代码一样快 |
|
||||
| 每个下游项目要迁走什么、迁完算不算数 | `research-wiki/migrations/` 下对应那份 |
|
||||
| 已定的决策及其理由 | `research-wiki/design/` 下相关编号最大的那份 |
|
||||
@@ -53,7 +53,7 @@
|
||||
6. **`asyncio.CancelledError` 永不捕获吞没。** 取消要能穿过模型调用与环境执行,in-flight 资源在 `finally` 释放。吞掉它的后果不是「取消失败」这么直白——是容器租约、连接和临时目录持续泄漏,而且一声不吭。
|
||||
7. **禁止吞掉错误**(`except Exception: pass` 及其跨行形态)。由 ruff `S110` / `E722` 断言。
|
||||
8. **测试绑行为,不绑实现。** 不写「断言某个内部类有哪些方法」这类测试——它只会让重构连坐。
|
||||
**公共 Protocol 的签名是例外**:它本身就是对下游的承诺,不是实现细节,所以 `tests/contract/` 断言它是应该的。判据是这个名字有没有对外承诺过——承诺过的改名是破坏性变更(§1.3),断言它就是在守那条承诺;没承诺过的改名只是重构,断言它就是在拖后腿。
|
||||
**公共 Protocol 的签名是例外**:它本身就是对下游的承诺,不是实现细节,所以 `src/polyloop/testing/` 断言它是应该的。判据是这个名字有没有对外承诺过——承诺过的改名是破坏性变更(§1.3),断言它就是在守那条承诺;没承诺过的改名只是重构,断言它就是在拖后腿。
|
||||
**断言某个名字「不存在」也是允许的**,用来守住一次删除决策。一个已经被删掉的字段没法被重命名,拖不动测试。代价是它守的只是名字不是概念——换个名字把同一个概念加回来,测试照样绿,所以理由必须同时写在被删字段所在类型的 docstring 里。
|
||||
9. **测试分层按「依赖什么」定,不按「叫什么」定。** 用测试替身的是 unit,连真 PolyGateway 的是 integration,打真实模型网关的是 e2e,验证公共 Protocol 行为一致性的是 contract。按名字分层的话,改个函数名就要挪测试文件;按依赖分,只要这个测试还是不连外部服务,它就一直待在原地。四层之间更细的界线在搭测试框架那个阶段定,现在不必较真。
|
||||
10. **发布 = 合并 + push + tag + 构建 + 上传 registry + 验证已发布。只 bump 版本号不叫发布。** 教训来自 PolyGateway:1.0.6 与 1.1.0 都完成了版本号 bump 与 CHANGELOG,却从未上传,registry 长期停在 1.0.5——下游 `pip install` 拿不到任何修复,且无人发现。**那次的补救只写了文档、没有回补上传,所以那两个版本到今天仍然不在 registry 上**,而 dissect 的依赖恰好钉在那个空区间里、装不上。这说明记下教训不等于修好问题。完整步骤与全部已知的坑见 `research-wiki/guides/releasing.md`。
|
||||
@@ -122,10 +122,11 @@ Codex 是 OpenAI 的编码模型,本仓库通过 `codex` 插件调用它。**
|
||||
★ research-wiki/reference/ 查得到的事实:日志字段契约、遥测口径
|
||||
(公共类型和枚举取值不在这里,权威见 §0 表格)
|
||||
★ research-wiki/scratch/ 一次性草稿。进 git,但由人在每轮工作会话结束前清理(AI 不要自动删)
|
||||
★ tests/contract/ 公共 Protocol 的行为一致性套件,是那份契约的权威(§0),
|
||||
也是任何新适配器的准入标准
|
||||
★ tests/contract/ 把库自带的实现接到契约套件上的那几个子类。套件本身不在这里
|
||||
★ tests/e2e/ 打真实模型网关,会产生真实费用。默认不跑,两道闸见 .env.example
|
||||
★ src/polyloop/ 库本体,十个模块
|
||||
★ src/polyloop/ 库本体,十一个模块
|
||||
★ src/polyloop/testing/ 公共 Protocol 的行为一致性套件,是那份契约的权威(§0),
|
||||
也是任何新适配器的准入标准。它随包发布,下游装了就拿得到
|
||||
```
|
||||
|
||||
常青层与记录层的分界、各类的更新触发点、`scratch/` 那条人工清理规则的已知风险,都在 `research-wiki/README.md`。
|
||||
|
||||
@@ -25,6 +25,18 @@ pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/py
|
||||
"polyloop[gateway]==1.0.*"
|
||||
```
|
||||
|
||||
契约套件随包发布,它要的 pytest 也是一个单独的 extra:
|
||||
|
||||
```bash
|
||||
pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ \
|
||||
"polyloop[testing]==1.0.*"
|
||||
```
|
||||
|
||||
**只有自己实现了某个接缝、要拿库这边的契约套件验它的时候才装这个。** 存储、模型调用、决策
|
||||
解释、动作执行、事件出口五处允许换实现,谁换了谁就得证明自己那份还满足接缝的行为契约,而
|
||||
证明的方式就是继承 `polyloop.testing` 里对应的基类跑一遍。只调 `run` / `resume`、五个接缝
|
||||
全用库自带或别人写好的实现的项目,不需要它。
|
||||
|
||||
写进 `requirements.txt` 的话,那一行 `--extra-index-url` 必须排在 `polyloop` 之前。
|
||||
|
||||
**这台开发机上的注意事项**:它设了 `http_proxy` 指向一个到不了外面的本地代理,走代理会失败,
|
||||
@@ -32,7 +44,7 @@ pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/py
|
||||
|
||||
## 现状
|
||||
|
||||
十个模块全部落地,四层测试都在跑。**还没有任何下游项目真的用过它**——这是它现在最大的未验证项,
|
||||
十一个模块全部落地,四层测试都在跑。**还没有任何下游项目真的用过它**——这是它现在最大的未验证项,
|
||||
下面的阶段清单是唯一的进度权威。
|
||||
|
||||
## 消费者与验收标准
|
||||
@@ -61,10 +73,12 @@ pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/py
|
||||
架构文档先于代码存在,此期间它是一份规格而不是描述,文档开头须写明这一点
|
||||
- [x] ④ 测试框架 —— 四层都在跑(划分判据是「依赖什么」,见 [CLAUDE.md](CLAUDE.md) §1.9)。
|
||||
e2e 打真实模型网关、会产生真实费用,所以默认不跑:要 `POLYLOOP_E2E=1` 加显式
|
||||
`pytest -m e2e`,配置见 [.env.example](.env.example)。`tests/contract/` 那套公共行为
|
||||
一致性用例接上了自带的存储实现,解释器与执行器那几条仍等下游把实现接进来
|
||||
- [x] ⑤ 实现 —— 十个模块全部落地,五个接缝都有调用点。**一处已知欠账**:`stores` 只有逐行
|
||||
追加那一种形态,关系数据库那种由下游自己实现,契约套件是它的准入标准
|
||||
`pytest -m e2e`,配置见 [.env.example](.env.example)。那套公共行为一致性用例住在
|
||||
`polyloop.testing` 里随包发出去,五个接缝在本仓库都接上了实现跑起来,接点是
|
||||
`tests/contract/` 与 `tests/integration/` 下那几个继承契约基类的子类
|
||||
- [x] ⑤ 实现 —— 十一个模块全部落地,五个接缝都有调用点。`stores` 有两种形态:逐行追加进
|
||||
本地文件的那个,和只留在进程内存里、进程一退就没了的那个。关系数据库那种仍然由下游
|
||||
自己实现,契约套件是它的准入标准,而套件随包发布在 `polyloop.testing` 里
|
||||
- [x] ⑥ 验收 —— 两件事都做完了。**一是自己造负载压**:照三个消费者将来的用法造负载,用真实
|
||||
数据真的打模型跑完,看这个内核在这个量级上扛不扛得住。**这一步之所以必须自己做,是因为
|
||||
三个消费者一个都还没到能用它的时候**,而「从没被任何人用过」是它当时最大的未验证项,
|
||||
|
||||
@@ -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 现在给每次调用传五个关键字参数(账本、轮次、
|
||||
阶段、题目、尝试序号)。库这边接的是一个字符串映射,所以适配器要做一次还原(把账本那个
|
||||
|
||||
@@ -9,7 +9,7 @@ import 时把网关连同它的 provider 目录一起拉起来,而不传存储
|
||||
没有恢复能力。
|
||||
|
||||
分层与依赖方向见 `research-wiki/explanation/architecture.md`,一次运行到底保证什么见
|
||||
`tests/contract/`。
|
||||
`polyloop.testing` 那套契约套件。
|
||||
"""
|
||||
|
||||
#: 与 `pyproject.toml` 的 `project.version` 必须一致,由 `tests/unit/test_package.py` 断言。
|
||||
|
||||
@@ -229,8 +229,9 @@ class LogRead:
|
||||
"""一份 `.jsonl` 日志按行读回来的结果。
|
||||
|
||||
`torn` 说的是末尾有没有一段没被换行终结的字节。判据照存储那边的规矩:**看有没有被换行
|
||||
终结,不看能不能解析**(`polyloop.stores` 的 `_parse`)。那段字节对应的那次写从来没有被
|
||||
确认过,按契约它就是没发生。
|
||||
终结,不看能不能解析**(`polyloop.stores` 那个逐行追加的实现,理由在
|
||||
`research-wiki/design/0011-jsonl-run-store.md`)。那段字节对应的那次写从来没有被确认过,
|
||||
按契约它就是没发生。
|
||||
"""
|
||||
|
||||
payloads: tuple[Mapping[str, object], ...]
|
||||
@@ -1423,7 +1424,7 @@ class AlwaysInvalidParser:
|
||||
"""对任何模型输出都返回无效决策。解析失败连击那一类用它。
|
||||
|
||||
它不是「解析不出来」,是**声明这一步解释不了**——契约要求 `parse` 同步、不抛异常、解释
|
||||
不出来时返回 `InvalidDecision`(`tests/contract/test_decision_parser.py`),这份实现照做。
|
||||
不出来时返回 `InvalidDecision`(`polyloop.testing.DecisionParserContract`),这份实现照做。
|
||||
"""
|
||||
|
||||
def parameters(self) -> Mapping[str, str]:
|
||||
|
||||
@@ -173,11 +173,15 @@ class AppWorldParser:
|
||||
history_text=output,
|
||||
decision=InvalidDecision(explanation=_FENCE_NOT_ON_ITS_OWN_LINE),
|
||||
)
|
||||
# **这里与被复刻的 dissect 实现有一处刻意的不同**:dissect 在这一支给历史文本补回
|
||||
# 结尾的三反引号(`parser.py:133-136`),让那条 assistant 消息形态完整;我们不补,
|
||||
# 因为公共契约要求 `len(history_text) <= len(reply.content)`
|
||||
# (`tests/contract/test_decision_parser.py:30`),补一个字符就违约。代价可以接受:
|
||||
# 压测不给模型配 stop 序列,这条路径基本不触发。代码抽取本身照旧兜底。
|
||||
# **这里与被复刻的 dissect 实现有一处不同**:dissect 在这一支给历史文本补回结尾的
|
||||
# 三反引号(`parser.py:133-136`),让那条 assistant 消息形态完整;这里不补。
|
||||
# 这处不同当初是为了迁就公共契约里一条「history_text 不长于模型原文」的断言,那条
|
||||
# 断言已经撤销(`polyloop.testing.DecisionParserContract` 里对应那条用例的 docstring
|
||||
# 写着理由),所以它现在没有存在的必要,是一笔记在案的欠账,不是一条长期决定。
|
||||
# 留着不恢复是因为收益不抵成本:补回去要重跑一次打真实网关的压测,才能继续说
|
||||
# 2026-08-11 那批轨迹是当前这版场景跑出来的;而这条路径要模型输出被 stop 序列截断
|
||||
# 才走得到,压测不给模型配 stop 序列。哪天要重跑压测,顺手把它补回来。代码抽取本身
|
||||
# 照旧兜底。
|
||||
return ParsedReply(
|
||||
history_text=output,
|
||||
decision=Action(text=code, tool_call=None),
|
||||
|
||||
@@ -750,8 +750,8 @@ class GovDocParser:
|
||||
"""
|
||||
|
||||
def parse(self, reply: ModelReply) -> ParsedReply:
|
||||
# 契约要求 `len(history_text) <= len(reply.content)`(`tests/contract/
|
||||
# test_decision_parser.py:30`),所以直接用原文,不拼接任何东西。
|
||||
# 压测这一侧的解析器一律只截不补,所以直接用原文,不拼接任何东西。公共契约
|
||||
# (`polyloop.testing.DecisionParserContract`)两种都放行,这条是压测自己的选择。
|
||||
history = reply.content
|
||||
decision = self._decide(reply.content)
|
||||
return ParsedReply(history_text=history, decision=decision)
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
分三块:解析器、提示词装配、动作执行器。
|
||||
|
||||
解析器那块的前五条是 `tests/contract/test_decision_parser.py` 那份公共契约的逐条复刻。契约
|
||||
套件本身是给下游接自己的实现用的(在自己的 `conftest.py` 里覆盖 fixture),压测这边不接那
|
||||
套装配、只把五条断言照着写一遍——它是任何新适配器的准入标准,压测的适配器也是适配器。
|
||||
解析器那块的前五条照着 `polyloop.testing.DecisionParserContract` 那份公共契约写。契约套件
|
||||
本身是给下游继承基类、在自己的子类里覆盖必需 fixture 用的,压测这边不接那套装配、只把五条
|
||||
断言照着写一遍——它是任何新适配器的准入标准,压测的适配器也是适配器。其中一条另外多守了一件
|
||||
公共契约没要求的事:`history_text` 不长于模型原文。公共契约不断言长度(解析器有权改写那段
|
||||
文本,改写既可能截短也可能补写),那条断言守的是压测这一侧自己的选择——解析器一律只截不补。
|
||||
|
||||
执行器那块用一个假会话,不起容器:这里要验的是「环境返回什么 → 结果对象怎么填」这个映射,
|
||||
而那个映射与容器里发生了什么无关。真起容器的那条链路由 `tools/soak/check_appworld.py` 走。
|
||||
@@ -68,11 +70,13 @@ def test_parse_is_synchronous():
|
||||
"什么代码都没有", # 一个围栏都没有
|
||||
],
|
||||
)
|
||||
def test_history_text_never_grows(content):
|
||||
"""`history_text` 不会比模型原文长(契约二)。
|
||||
def test_scenario_parser_only_trims_history_text(content):
|
||||
"""`history_text` 不会比模型原文长。这守的是这个场景自己的实现选择,不是公共契约。
|
||||
|
||||
这一条正是我们与被复刻的 dissect 实现分道的地方:dissect 在「未闭合围栏兜底」那一支给
|
||||
历史文本补回结尾的三反引号,补一个字符就会让这条断言失败。
|
||||
公共契约不断言长度:解析器有权改写回填历史的那段文本,改写既可能截短也可能补写
|
||||
(`polyloop.testing.DecisionParserContract`)。这个场景的解析器一律只截不补,所以长度
|
||||
不会涨。它也正是我们与被复刻的 dissect 实现分道的地方:dissect 在「未闭合围栏兜底」那一
|
||||
支给历史文本补回结尾的三反引号,补一个字符就会让这条断言失败。
|
||||
"""
|
||||
parsed = AppWorldParser().parse(_reply(content))
|
||||
|
||||
@@ -213,7 +217,7 @@ def test_empty_fence_is_a_failure_with_its_own_explanation():
|
||||
def test_unclosed_fence_still_yields_code():
|
||||
"""未闭合的围栏仍然抽得出代码,且历史文本不比原文长。
|
||||
|
||||
dissect 在这一支补回结尾的三反引号,我们不补——公共契约要求 history_text 不长于原文。
|
||||
dissect 在这一支补回结尾的三反引号,我们不补——压测的解析器一律只截不补。
|
||||
"""
|
||||
content = "我来查一下。\n\n```python\nprint(apis.api_docs.show_app_descriptions())"
|
||||
|
||||
|
||||
@@ -604,7 +604,8 @@ def test_always_invalid_parser_is_sync_and_never_raises() -> None:
|
||||
parsed = parser.parse(ModelReply(call_id=None, content="```python\nprint(1)\n```", thinking=""))
|
||||
assert isinstance(parsed.decision, InvalidDecision)
|
||||
assert parsed.decision.explanation.strip()
|
||||
# 契约:回填历史的那段不许比模型原文长。
|
||||
# 回填历史的那段不比模型原文长。公共契约不断言长度,这守的是这份注入用解析器自己的形状:
|
||||
# 它原样回填模型原文,不拼接任何东西。
|
||||
assert len(parsed.history_text) <= len("```python\nprint(1)\n```")
|
||||
assert parser.parameters()["kind"] == "always_invalid"
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
脱敏那块里最重要的一条是「校验函数对未脱敏文本确实会抛异常」。一个永远返回通过的校验函数比
|
||||
没有校验更糟:它会让所有人以为这道闸在守着,而它什么都没守。
|
||||
|
||||
解析器那块的前五条是 `tests/contract/test_decision_parser.py` 那份公共契约的逐条复刻。契约
|
||||
套件本身是给下游接自己的实现用的(在自己的 `conftest.py` 里覆盖 fixture),压测这边不接那套
|
||||
装配、只把五条断言照着写一遍——它是任何新适配器的准入标准,压测的适配器也是适配器。
|
||||
解析器那块的前五条照着 `polyloop.testing.DecisionParserContract` 那份公共契约写。契约套件
|
||||
本身是给下游继承基类、在自己的子类里覆盖必需 fixture 用的,压测这边不接那套装配、只把五条
|
||||
断言照着写一遍——它是任何新适配器的准入标准,压测的适配器也是适配器。其中一条另外多守了一件
|
||||
公共契约没要求的事:`history_text` 不长于模型原文。公共契约不断言长度(解析器有权改写那段
|
||||
文本,改写既可能截短也可能补写),那条断言守的是压测这一侧自己的选择——解析器一律只截不补。
|
||||
|
||||
用真实数据的那几条在数据目录不在时跳过而不是失败:那份数据是另一个项目的工作副本,不在本仓库
|
||||
里,换一台机器就没有。
|
||||
@@ -178,7 +180,13 @@ def test_contract_1_parse_is_synchronous():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("content", [_ACTION_REPLY, _INVALID_REPLY, "", "```json\n{}\n```"])
|
||||
def test_contract_2_history_text_is_not_longer(content: str):
|
||||
def test_scenario_parser_only_trims_history_text(content: str):
|
||||
"""`history_text` 不会比模型原文长。这守的是这个场景自己的实现选择,不是公共契约。
|
||||
|
||||
公共契约不断言长度:解析器有权改写回填历史的那段文本,改写既可能截短也可能补写
|
||||
(`polyloop.testing.DecisionParserContract`)。这个场景的解析器直接回填模型原文、不拼接
|
||||
任何东西,所以长度不会涨。
|
||||
"""
|
||||
parsed = GovDocParser().parse(_reply(content))
|
||||
assert isinstance(parsed.history_text, str)
|
||||
assert len(parsed.history_text) <= len(content)
|
||||
|
||||
Reference in New Issue
Block a user