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:
@@ -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