303e4ebc1c
发布前置:readme 与 project.urls 从第一版就写全——缺 readme 的话 registry 包页面正文一片 空白,而 twine 只警告不阻塞上传,三步全绿产物却是坏的(PolyGateway 1.1.2 的教训)。 build 与 twine 钉进 dev extra 而不是靠人手装,PolyGateway 那边它们不在任何 extra 里, 于是发布指南得多写一条「记得先装」,那种步骤迟早有人漏。 README 里那条「项目还没有可用的功能、十个模块是空骨架」的横幅早就过期了,而打包会把当时的 README 固化进 sdist、发布后再改无效,所以在构建之前换成安装说明与现状。顺带修两处与事实 不符的:GovDoc-SaaS 的实现已经在 8 月 3 日整体清空,⑥ 对它的验收标准改成设计级验收。 新建 CHANGELOG.md,写清楚首个版本为什么是 1.0.1 而不是 0.x——「公共类型的字段只增不删不 改名」那条承诺从第一个下游装上它那天起就生效,而 0.x 意味着随时可以破坏兼容。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
178 lines
8.9 KiB
TOML
178 lines
8.9 KiB
TOML
[build-system]
|
||
requires = ["setuptools>=68.0"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[project]
|
||
name = "polyloop"
|
||
# 与 src/polyloop/__init__.py 的 __version__ 必须一致,由 tests/unit/test_package.py 断言。
|
||
version = "1.0.1"
|
||
description = "PolyLoop:实验室共用的 Agent 执行内核——一次运行的预算、停止语义、取消、逐步轨迹与 Skill 注入"
|
||
# registry 的包页面正文只认这一项:缺了它页面就是一片空白,而 twine 只会警告
|
||
# long_description missing,不阻塞上传——三步全绿、产物是坏的(PolyGateway 1.1.2 的教训)。
|
||
# README 在打包时被固化进产物,发布之后再改无效,所以改 README 必须排在构建之前。
|
||
readme = "README.md"
|
||
requires-python = ">=3.11"
|
||
# 3.11 是被下游钉死的:dissect 与 GovDoc-SaaS 都跑在 3.11,一个库不能要求比它的消费者更高的版本。
|
||
dependencies = []
|
||
# 核心没有运行时依赖,这是刻意的。公共类型与接缝签名上不许出现第三方类型(依赖规则 6),
|
||
# 否则那个包的 major 就是我们的 major。
|
||
|
||
[project.optional-dependencies]
|
||
# 模型适配器单独成一个 extra:不用它的人不该被迫装上网关,也不该在 import 时把网关连同
|
||
# 它的 provider 目录一起拉起来(依赖规则 9)。PolyGateway 不在公共 PyPI 上,装它要配私有源。
|
||
gateway = ["polygateway>=1.1,<2"]
|
||
# dev 工具链的版本必须钉死,不用下界。教训来自 CHSAnalyzer:本地 conda 是 ruff 0.15.1、
|
||
# CI 装到刚发布的 0.16.0,新版本多了几条规则,于是「本地全绿、CI 全红」。不钉的话 CI 会在
|
||
# 没有任何人改代码的情况下随上游发版随机变红,而随机的红叉很快就会训练所有人忽略红叉。
|
||
# 升级流程:改这里的版本号 → 本地重装 → 修掉新规则的告警 → 一起提交。
|
||
dev = [
|
||
"pytest==9.1.1",
|
||
"pytest-asyncio==1.4.0",
|
||
"ruff==0.16.2",
|
||
"import-linter==2.13",
|
||
# 发布用的两个。**刻意放进 dev 而不是靠人手装**:PolyGateway 那边它们不在任何 extra 里,
|
||
# 于是发布指南必须多写一条「记得先装这两个」,而那种步骤迟早有人漏。
|
||
"build==1.5.0",
|
||
"twine==7.0.0",
|
||
]
|
||
|
||
# 包页面上那几个链接。PyPI 的元数据里没有「仓库」这个字段,所以 registry 不会自动把包挂到
|
||
# 仓库上——那一步只能在网页上手动做,这里这几条是包页面上唯一能自带的去处。
|
||
[project.urls]
|
||
Homepage = "https://gitea.iomgaa.online/iomgaa/PolyLoop"
|
||
Changelog = "https://gitea.iomgaa.online/iomgaa/PolyLoop/src/branch/main/CHANGELOG.md"
|
||
Issues = "https://gitea.iomgaa.online/iomgaa/PolyLoop/issues"
|
||
|
||
[tool.setuptools.packages.find]
|
||
where = ["src"]
|
||
|
||
# py.typed 让下游装上之后拿得到类型信息。一个库不带它,下游的类型检查器会把整个包当成
|
||
# 无注解的黑盒,签名和公共类型上写的东西一条都用不上——而这个库对下游的承诺大半就写在
|
||
# 签名里(CLAUDE.md §6:下游看不到实现,只能靠签名和类型判断该传什么)。
|
||
[tool.setuptools.package-data]
|
||
polyloop = ["py.typed"]
|
||
|
||
[tool.ruff]
|
||
target-version = "py311"
|
||
line-length = 100
|
||
|
||
[tool.ruff.lint]
|
||
# S110(try-except-pass)与 E722(裸 except)断言 CLAUDE.md §1.7「禁止吞掉错误」。
|
||
# 这两条是硬约束的机器形式,不许在本文件或行内 noqa 里放开。
|
||
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "SIM", "TCH", "S110"]
|
||
ignore = ["E501"]
|
||
|
||
[tool.ruff.lint.isort]
|
||
known-first-party = ["polyloop"]
|
||
|
||
[tool.pytest.ini_options]
|
||
pythonpath = ["src"]
|
||
testpaths = ["tests"]
|
||
asyncio_mode = "auto"
|
||
|
||
# 四层的判据是「依赖什么」,不是「叫什么」(CLAUDE.md §1.9)。按名字分层的话,改个函数名
|
||
# 就要挪测试文件;按依赖分,只要这个测试还是不连外部服务,它就一直待在原地。
|
||
#
|
||
# unit 里会出现少数只读源码文本、根本不执行被测代码的测试(依赖规则 6 那条「types 与 ports
|
||
# 禁止 import 任何第三方包」就是这种,它写不成 import-linter 契约)。它们不连任何外部服务,
|
||
# 按上面那条判据就是 unit,不为它们单开一层。哪天这类测试多到想单独跑,再拆不迟。
|
||
markers = [
|
||
"unit: 用测试替身,不连任何外部服务",
|
||
"integration: 连真 PolyGateway",
|
||
"e2e: 打真实模型网关,会产生真实调用与费用",
|
||
"contract: 公共 Protocol 的行为一致性套件,也是任何新适配器的准入标准",
|
||
]
|
||
|
||
# --strict-markers:拼错的标记直接报错,不给警告。默认配置下 @pytest.mark.uint 只是一条
|
||
# 警告,而那个文件会从此不属于任何一层、被所有筛选漏掉,静默消失。
|
||
# --import-mode=importlib:按文件路径建模块名,不按 basename。默认的 prepend 模式下
|
||
# tests/unit/test_stopping.py 与 tests/integration/test_stopping.py 会被当成同一个模块,
|
||
# 收集时报 import file mismatch——而且挂掉的是**整次收集**,连没被 -m 选中的层也一起挂。
|
||
# 另一种解法是给每个层目录补 __init__.py,选这个是因为它不要求以后新建目录的人记得补。
|
||
# -m 'not e2e':e2e 打真实网关、要花钱,默认不跑。显式 `pytest -m e2e` 覆盖(CLI 的 -m
|
||
# 后到优先)。这一条是刻意的默认排除,不是静默降级——它写在这里,且 markers 里注明了原因。
|
||
addopts = "--strict-markers --import-mode=importlib -m 'not e2e'"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 依赖规则的机器形式。九条规则本身与它们各自的理由在
|
||
# research-wiki/design/0003-public-api-shape.md 决策八,当前形状在
|
||
# research-wiki/explanation/architecture.md 第七节。这里只写契约,不复述理由。
|
||
#
|
||
# 九条里有两条写不成 import-linter 契约,它们是 tests/ 里的测试:
|
||
# 规则 6(types 与 ports 禁止 import 任何第三方包)——「任何第三方」不是一份可枚举的清单。
|
||
# 规则 9(import polyloop 之后 sys.modules 里没有 polygateway)——那是运行时事实,不是静态图。
|
||
# ---------------------------------------------------------------------------
|
||
[tool.importlinter]
|
||
root_packages = ["polyloop"]
|
||
# 有几条契约要禁止 import 外部包(下游项目、polygateway、asyncio / pathlib),
|
||
# 而外部包默认不进 import 图,所以必须在顶层打开这个开关。
|
||
include_external_packages = true
|
||
|
||
# 规则 1、2、3、8。`|` 表示同层且互不 import,`:` 表示同层可以互相 import。
|
||
# 装配层那三个用 `|`:session 一旦 import 了某个存储实现,那个实现就成了隐式默认。
|
||
# 逻辑层那五个也用 `|`:它们之间的编织只能发生在 session 里。
|
||
[[tool.importlinter.contracts]]
|
||
name = "分层:装配层 > 逻辑层 > ports > types,且同层互不 import"
|
||
type = "layers"
|
||
layers = [
|
||
"polyloop.session | polyloop.stores | polyloop.adapters",
|
||
"polyloop.tools | polyloop._assembly | polyloop._stopping | polyloop._recovery | polyloop.serialization",
|
||
"polyloop.ports",
|
||
"polyloop.types",
|
||
]
|
||
|
||
# 规则 8 单列。上一条已经覆盖它,重复一遍是为了让违规信息直接指向
|
||
# 「接缝定义模块被污染了」,而不是一条读起来要绕一圈的分层报错。
|
||
[[tool.importlinter.contracts]]
|
||
name = "ports 只依赖 types,不依赖任何其他 polyloop 模块"
|
||
type = "forbidden"
|
||
source_modules = ["polyloop.ports"]
|
||
forbidden_modules = [
|
||
"polyloop.session",
|
||
"polyloop.stores",
|
||
"polyloop.adapters",
|
||
"polyloop.tools",
|
||
"polyloop._assembly",
|
||
"polyloop._stopping",
|
||
"polyloop._recovery",
|
||
"polyloop.serialization",
|
||
]
|
||
|
||
# 规则 4。三个下游的顶层包名:dissect 是 harness,GovDoc-SaaS 是 docagent_core,
|
||
# CHSAnalyzer 还没写到 agent 那一步,等它有了包名再加进来。
|
||
[[tool.importlinter.contracts]]
|
||
name = "不反向 import 任何下游项目"
|
||
type = "forbidden"
|
||
source_modules = ["polyloop"]
|
||
forbidden_modules = ["harness", "docagent_core"]
|
||
|
||
# 规则 5。适配器是唯一允许碰网关的地方。
|
||
[[tool.importlinter.contracts]]
|
||
name = "除 adapters 外一切禁止 import polygateway"
|
||
type = "forbidden"
|
||
source_modules = [
|
||
"polyloop.session",
|
||
"polyloop.stores",
|
||
"polyloop.tools",
|
||
"polyloop._assembly",
|
||
"polyloop._stopping",
|
||
"polyloop._recovery",
|
||
"polyloop.serialization",
|
||
"polyloop.ports",
|
||
"polyloop.types",
|
||
]
|
||
forbidden_modules = ["polygateway"]
|
||
|
||
# 规则 7。这一条是烟雾报警,不是纯度契约——os、subprocess、sqlite3 都能绕过它,
|
||
# 而 open() 是内置函数,import-linter 根本看不见。它拦得住最常见的那种偷懒
|
||
# (写着写着顺手 await 一下存储、顺手读个文件),拦不住存心的。
|
||
[[tool.importlinter.contracts]]
|
||
name = "三个纯逻辑模块不碰 I/O 与事件循环"
|
||
type = "forbidden"
|
||
source_modules = [
|
||
"polyloop._assembly",
|
||
"polyloop._stopping",
|
||
"polyloop._recovery",
|
||
]
|
||
forbidden_modules = ["asyncio", "pathlib"]
|