build(repo): 落成工具链、依赖契约与十个模块的空骨架

第 ③ 阶段剩下的那半:架构文档之外,import-linter 契约也落地了。

pyproject.toml 把九条依赖规则里的七条写成五条 import-linter 契约。
分层用一条 layers 契约表达规则 1、2、3、8,`|` 表示同层互不 import;
另外四条 forbidden 分别管下游反向 import、polygateway 的唯一入口、
以及三个纯逻辑模块不碰 asyncio 与 pathlib。
契约不是平凡的绿:故意注入两处违规验证过,都被点名到行号。

先建十个模块的空包,是为了避开 PolyGateway bootstrap 期那段 Makefile 门控——
它当时没有包,lint-imports 报 module not found 而红,只好加一段跳过逻辑。
空包让契约从第一天就真的在跑。

剩下两条规则落不进契约,写成了 tests/unit 下的测试:
规则 6「types 与 ports 不许 import 任何第三方」判据要反过来写(只许标准库和自己),
规则 9「import polyloop 之后 sys.modules 里没有 polygateway」是运行时事实。
另加硬约束 §1.1 零业务假设的黑名单扫描——它第一次跑就抓到 _assembly 的 docstring
里写了 dissect 的业务词,已改。三个扫描类测试都带 fail-closed 守卫,
防止目录搬走之后扫到空列表安静地绿。

工程约定取自实验室已有项目:setuptools + src layout、ruff 十一项 select、
line-length 100 来自 PolyGateway;dev 工具链版本钉死、--strict-markers 与
--import-mode=importlib 来自 CHSAnalyzer 与 dissect 踩过的坑,各自的理由写在配置注释里。
e2e 默认不跑,它打真实网关要花钱。

CLAUDE.md §0 那句「契约还没写,要等 src/ 落地」已过期,改掉;
README 勾掉第 ②③ 阶段,补上本地检查命令与 GovDoc-Editor 那一行。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 21:28:59 -04:00
parent 766f7a0290
commit e017160c45
20 changed files with 497 additions and 13 deletions
+18
View File
@@ -0,0 +1,18 @@
"""PolyLoopAgent 执行内核。
治理单位是一次运行——围绕一个目标的有界多轮「模型决策 → 动作 → 观察」循环。
一次模型调用不归它管,那是 PolyGateway 的治理单位。
**这里只再导出五个公开模块**`types`、`ports`、`tools`、`serialization`、`session`。
`stores` 与 `adapters` 必须由使用者显式 import——顺手提供的默认实现会让每个进程在
import 时把网关连同它的 provider 目录一起拉起来,而不传存储的人不会知道自己这次运行
没有恢复能力。
**当前是空骨架。** 目录与依赖契约先于代码存在,形状见
`research-wiki/explanation/architecture.md`。
"""
#: 与 `pyproject.toml` 的 `project.version` 必须一致,由 `tests/unit/test_package.py` 断言。
#: 两处双写是因为运行时读不到构建元数据(未安装的源码树里 `importlib.metadata` 查不到),
#: 而下游报 bug 时第一件事就是问版本号。
__version__ = "0.0.0"
+9
View File
@@ -0,0 +1,9 @@
"""段序、注入槽、规模度量。内部模块。
渲染格式不在这里:那是调用方自己要改的东西。库一旦在内部按某个条件拼出自己的文本,
那段文本就成了库替调用方定的内容,而且不会出现在调用方的参数快照里。归这里的只有段的
顺序与注入槽的位置,那是纯函数不是扩展点。
**纯逻辑,不碰 I/O 与事件循环。** 机器只拦得住 `asyncio` 与 `pathlib` 这两个最常见的
入口,真正守住纯度的是这个模块的测试形态:不许用 fixture 起外部资源、不许有 `async def`。
"""
+7
View File
@@ -0,0 +1,7 @@
"""恢复状态判定与运行身份校验。内部模块。
读到结构上说不通的状态就失败,不修复也不带着它继续——一个被猜着修好的日志,会让后面
每一个基于它的判断都建立在猜测上,而且不会有任何地方提示这件事发生过。
**纯逻辑,约束同 `_assembly`。**
"""
+7
View File
@@ -0,0 +1,7 @@
"""停止判定与预算结算。内部模块。
判定顺序是有序的,不是一组独立条件——「恰好在最后一步做完」和「预算耗尽」的轨迹长度
一模一样,顺序错了两者会互换,而且不会有任何地方报错。
**纯逻辑,约束同 `_assembly`。**
"""
+5
View File
@@ -0,0 +1,5 @@
"""PolyGateway 模型适配器。**须由使用者显式 import。**
**这是全库唯一允许 import `polygateway` 的地方。** 不进 `polyloop/__init__.py`:一个
顺手提供的默认模型客户端会让每个进程在 import 时把网关连同它的 provider 目录一起拉起来。
"""
+7
View File
@@ -0,0 +1,7 @@
"""全部 Protocol 与它们的入参 / 返回结构体。
用 `typing.Protocol` 而不是抽象基类:下游的对象往往已经是它自己的类、还要同时满足项目
自己更宽的接口,只有结构化子类型能让同一个对象同时满足库的窄视图和项目的宽视图。
**签名上不许出现第三方类型**——出现了,那个包的 major 就是我们的 major。
"""
View File
+5
View File
@@ -0,0 +1,5 @@
"""记录的编解码与 schema major 校验。
读到未知 major 直接失败,不靠默认值补齐。一次静默的默认值填充会把「这件事没发生过」
改写成「发生了但值为空」,而这种损坏要到统计阶段才暴露。
"""
+7
View File
@@ -0,0 +1,7 @@
"""定义、请求,以及 `run` 与 `resume` 两个动词。
**唯一的驱动入口**:不导出低阶循环,也不导出单步。
逻辑层那五个模块之间的编织只能发生在这里——它们互不 import,这个模块是它们唯一的会合处。
代价是这个模块会长,这是接受了的。
"""
+5
View File
@@ -0,0 +1,5 @@
"""库自带的存储实现。**须由使用者显式 import。**
不进 `polyloop/__init__.py`,也不许被 `session` import`session` 一旦 import 了某个
存储实现,那个实现就成了隐式默认,而不传存储的人不会知道自己这次运行没有恢复能力。
"""
+11
View File
@@ -0,0 +1,11 @@
"""工具规格与注册表,以及由注册表派生的动作执行器。
一个模块持有关于工具的全部六件事:注册、模型可见 schema 的生成、存在性与参数校验、分发、
重放策略声明、完成标记。
**六件必须同源,不许把其中任何一件挪出去。** 挪出去的那份清单会跟注册表漂移,而漂移的
表现是「模型明明提交了,运行却没停」——它看起来像模型不听话,不像配置错了。
注册表是不可变值对象,取子集返回新实例。不能是进程级单例:同一进程里可能同时持有多份
不同的窄集合。
"""
+8
View File
@@ -0,0 +1,8 @@
"""公共值类型、枚举、持久化记录。
读者是所有人:下游读步记录与停止原因重建轨迹,读停止原因决定一次运行算不算可挽救,
写适配器的人要构造这里的结构体。
**这个模块的字段只增不删不改名,新增字段必带默认值**,持久化结构的 schema 变更走显式
版本。理由见 `CLAUDE.md` §1.3 与 §1.4。
"""