拆 __init__.py 为 entry_* + 抽 StoragePort/runtime_state + 结构契约(结构批) #110

Merged
Yushu merged 2 commits from refactor/97-split-entry-modules into main 2026-08-11 01:02:33 +00:00
Member

Closes #97

零新目录。2493 行的 __init__.py 切成 7 个 entry_* 入口簇 + 5 个跨簇共享模块,
包根只剩 48 行:注册触发 + host 公开面。

四个任务

  1. __init__.py:前置抽 runtime_state.py_pending_flushes/
    _last_group_chat_activity/_silence_window/_debouncer 四个共享状态);共享层
    另有 cost_metering / gate_pipeline / loop_factory / environment_pipeline
    不做私有名 re-export,改了约 30 处测试 import,让遗漏以 ImportError 立刻失败。
  2. StoragePort 抽进 storage_port.py,解掉 ports.py(host 契约文件)→ 651 行
    内存实现这条全图唯一的"向上"边。SentLog 本体留在 src/
  3. 结构契约:import-linter 两条 + CI 一步 + dev 依赖;visible_host_tools 移进
    tool_port.py形状与 AC 不同,见下。
  4. 包根 re-export configure/get_ports/ArisePortsports.py 那条一直写错调用
    路径的错误信息随之成立;ADR-0001 追加更新节(docs 分支本地提交)。

「只搬运」的机械证明

不靠"我看过了":AST 逐字比对拆分前后的全部顶层定义——85 个,0 丢失,仅 6 处差异
且每处都核过:

  • 5 处是把 _debouncer/_silence_window 从裸名改成 runtime_state.X 模块属性访问。
    这不是洁癖from x import y 在 import 时就把名字定死,而
    test_silence_window_wiring.py 正是靠 monkeypatch.setattr(..., "_silence_window", …)
    重绑它们——照搬 from ... import 会让那两条 fixture 静默失效。
  • 1 处是 _process_environment_signalbot: Botbot: Any(见下)。

任务三的形状被实测推翻了

AC 写的是「import-linter forbidden 契约禁 nonebot.adapters」。实测不可行
forbidden_modules 一旦指向外部包的非顶层子模块(nonebot.adapters),import-linter
在建图之前就显式拒绝,报 Invalid forbidden module nonebot.adapters: subpackages of external packages are not valid.(配置期 ValueError,退出码 1,CI 会红)。

改成两件工具各守一半(已与维护者确认):

  • 平台面tests/test_import_contract.py 用 AST 精确守「非 entry_* 不得 import
    nonebot.adapters」。例外是一个 glob 而不是一张表,新增入口不需登记。
    它当场抓到了一处真违规(environment_pipeline.py 拆分过程中自己带上的
    from nonebot.adapters import Bot)——修法是把 bot: Bot 改成 bot: Any
    make_egress 的契约本来就是 Callable[[Any], EgressPort],那个标注是装饰性的。
  • 结构面 → import-linter 两条契约:共享层不得依赖入口层、入口之间互不依赖。
    这是它真能表达的,也正是 runtime_state.py 存在的理由。

配置放 pyproject.toml 而非 .importlinter:后者按系统默认编码读,Windows(GBK)
会直接读崩在中文注释上。

一处加强而非削弱的守卫

issue #88 那个 vars(plugin) 机械枚举(命令边界覆盖面的唯一来源)会因为拆分而一个都
枚举不到
。没有收窄成 vars(entry_admin)——那样以后在别的 entry_* 里注册命令就看不见
了,而它存在的全部理由就是"不靠手写清单"。改成扫全包。

变异验证:收窄回单模块 → 8 failed + 3 skipped。那 3 个 skip 正是「机械枚举驱动的覆盖
会静默消失」这个已知形状,靠等值守卫兜住了。

两轴 code review(sonnet):3 条 finding 存活,均已处理

  • 一条子代理判定字段自相矛盾(写 REFUTED,正文却写「结论:判定为 CONFIRMED」):
    我最初在 pyproject.toml/测试 docstring/CI 注释/ADR 更新节里写的"import-linter 契约会
    静默通过"是错的——已独立复现(见上一节的准确描述),并按正文推理为准,改成
    准确描述。属于本仓记过的「给不做的分支配一个听起来合理但没核对到位的理由」这一类,
    这是第九次。结论不变(import-linter 确实表达不了那条规则),理由订正。
  • 两处文档计数不准:runtime_state.py 把只被反应式一条入口碰的 _debouncer 写成了
    「被三条入口碰」;gate_pipeline.py 漏了即刻追问也调 _evaluate_unified_gate
    (实际四条触发路径,不是三条)。逐条 grep 复核后改正。
  • loop_factory.py 那条(模块级摘要写"四条入口"、函数级 docstring 写"五条路径")
    经核实是两个不同的合法粒度,不是缺陷,未改。
  • conftest.py 加载顺序约束那条降级为「锦上添花」,未改。

验证

  • 全量 1544 passed(随机序 + -n auto 各一遍),ruff check 干净,
    lint-imports 2 kept / 0 broken。
  • 4 项变异全红:共享模块 import 平台适配层 / 包根又长出一个函数 / 共享层反过来
    import 入口层 / 穷举守卫收窄回单模块。还原后 md5 全部一致。

仍值得独立复核的两点

  1. 其余模块级可变状态的绑定语义。确认了 _debouncer/_silence_window 是被重绑的,
    改成了属性访问;其余(_pending_flushes/_last_group_chat_activity/
    _pending_followup_checks/_pending_sticker_learning/_pending_cross_platform_pulls/
    _in_flight_pull_users/_delegate_task_registry)判断都是原地修改,用
    from ... import 安全。判错一个就是一条静默失效。
  2. 簇划分。按入口可达性机械算的,但"_process_environment_signal 该不该独立成
    模块"这类是判断题,值得复看。

🤖 Generated with Claude Code

Closes #97 零新目录。2493 行的 `__init__.py` 切成 7 个 `entry_*` 入口簇 + 5 个跨簇共享模块, 包根只剩 **48 行**:注册触发 + host 公开面。 ## 四个任务 1. **拆 `__init__.py`**:前置抽 `runtime_state.py`(`_pending_flushes`/ `_last_group_chat_activity`/`_silence_window`/`_debouncer` 四个共享状态);共享层 另有 `cost_metering` / `gate_pipeline` / `loop_factory` / `environment_pipeline`。 **不做私有名 re-export**,改了约 30 处测试 import,让遗漏以 `ImportError` 立刻失败。 2. **`StoragePort` 抽进 `storage_port.py`**,解掉 `ports.py`(host 契约文件)→ 651 行 内存实现这条全图唯一的"向上"边。`SentLog` 本体留在 `src/`。 3. **结构契约**:import-linter 两条 + CI 一步 + dev 依赖;`visible_host_tools` 移进 `tool_port.py`。**形状与 AC 不同,见下。** 4. **包根 re-export** `configure`/`get_ports`/`ArisePorts`,`ports.py` 那条一直写错调用 路径的错误信息随之成立;ADR-0001 追加更新节(docs 分支本地提交)。 ## 「只搬运」的机械证明 不靠"我看过了":AST 逐字比对拆分前后的全部顶层定义——**85 个,0 丢失,仅 6 处差异**, 且每处都核过: - 5 处是把 `_debouncer`/`_silence_window` 从裸名改成 `runtime_state.X` 模块属性访问。 **这不是洁癖**:`from x import y` 在 import 时就把名字定死,而 `test_silence_window_wiring.py` 正是靠 `monkeypatch.setattr(..., "_silence_window", …)` **重绑**它们——照搬 `from ... import` 会让那两条 fixture 静默失效。 - 1 处是 `_process_environment_signal` 的 `bot: Bot` → `bot: Any`(见下)。 ## 任务三的形状被实测推翻了 AC 写的是「import-linter forbidden 契约禁 `nonebot.adapters`」。**实测不可行**: `forbidden_modules` 一旦指向外部包的非顶层子模块(`nonebot.adapters`),import-linter 在建图之前就显式拒绝,报 `Invalid forbidden module nonebot.adapters: subpackages of external packages are not valid.`(配置期 `ValueError`,退出码 1,CI 会红)。 **改成两件工具各守一半**(已与维护者确认): - **平台面** → `tests/test_import_contract.py` 用 AST 精确守「非 `entry_*` 不得 import `nonebot.adapters`」。例外是**一个 glob 而不是一张表**,新增入口不需登记。 它当场抓到了一处真违规(`environment_pipeline.py` 拆分过程中自己带上的 `from nonebot.adapters import Bot`)——修法是把 `bot: Bot` 改成 `bot: Any`, `make_egress` 的契约本来就是 `Callable[[Any], EgressPort]`,那个标注是装饰性的。 - **结构面** → import-linter 两条契约:共享层不得依赖入口层、入口之间互不依赖。 这是它真能表达的,也正是 `runtime_state.py` 存在的理由。 配置放 `pyproject.toml` 而非 `.importlinter`:后者按系统默认编码读,Windows(GBK) 会直接读崩在中文注释上。 ## 一处加强而非削弱的守卫 issue #88 那个 `vars(plugin)` 机械枚举(命令边界覆盖面的唯一来源)会因为拆分而**一个都 枚举不到**。没有收窄成 `vars(entry_admin)`——那样以后在别的 `entry_*` 里注册命令就看不见 了,而它存在的全部理由就是"不靠手写清单"。改成扫全包。 变异验证:收窄回单模块 → **8 failed + 3 skipped**。那 3 个 skip 正是「机械枚举驱动的覆盖 会静默消失」这个已知形状,靠等值守卫兜住了。 ## 两轴 code review(sonnet):3 条 finding 存活,均已处理 - **一条子代理判定字段自相矛盾**(写 REFUTED,正文却写「结论:判定为 CONFIRMED」): 我最初在 pyproject.toml/测试 docstring/CI 注释/ADR 更新节里写的"import-linter 契约会 静默通过"是**错的**——已独立复现(见上一节的准确描述),并按正文推理为准,改成 准确描述。属于本仓记过的「给不做的分支配一个听起来合理但没核对到位的理由」这一类, 这是第九次。结论不变(import-linter 确实表达不了那条规则),理由订正。 - 两处文档计数不准:`runtime_state.py` 把只被反应式一条入口碰的 `_debouncer` 写成了 「被三条入口碰」;`gate_pipeline.py` 漏了即刻追问也调 `_evaluate_unified_gate` (实际四条触发路径,不是三条)。逐条 grep 复核后改正。 - `loop_factory.py` 那条(模块级摘要写"四条入口"、函数级 docstring 写"五条路径") 经核实是两个不同的合法粒度,不是缺陷,未改。 - `conftest.py` 加载顺序约束那条降级为「锦上添花」,未改。 ## 验证 - 全量 **1544 passed**(随机序 + `-n auto` 各一遍),`ruff check` 干净, `lint-imports` 2 kept / 0 broken。 - **4 项变异全红**:共享模块 import 平台适配层 / 包根又长出一个函数 / 共享层反过来 import 入口层 / 穷举守卫收窄回单模块。还原后 md5 全部一致。 ## 仍值得独立复核的两点 1. **其余模块级可变状态的绑定语义**。确认了 `_debouncer`/`_silence_window` 是被重绑的, 改成了属性访问;其余(`_pending_flushes`/`_last_group_chat_activity`/ `_pending_followup_checks`/`_pending_sticker_learning`/`_pending_cross_platform_pulls`/ `_in_flight_pull_users`/`_delegate_task_registry`)判断都是**原地修改**,用 `from ... import` 安全。判错一个就是一条静默失效。 2. **簇划分**。按入口可达性机械算的,但"`_process_environment_signal` 该不该独立成 模块"这类是判断题,值得复看。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
零新目录。2493 行的包根切成 7 个 `entry_*` 入口簇 + 5 个跨簇共享模块,包根只剩
48 行:注册触发 + host 公开面。

- **只搬运的机械证明**:拆分前 85 个顶层定义,AST 逐字比对后 0 丢失、仅 6 处差异
  ——4 处是把会被测试重绑的 `_debouncer`/`_silence_window` 改成模块属性访问
  (`from x import y` 会在 import 时定死名字,让 monkeypatch 静默失效),
  1 处同因,1 处是 `_process_environment_signal` 的 `bot: Bot` → `bot: Any`。
- **不做私有名 re-export**:改了 ~30 处测试 import,让遗漏以 ImportError 立刻失败。
  `_last_persona_fingerprint` 那条 fixture 现在打在 `entry_admin` 上。
- **`StoragePort` 抽进 `storage_port.py`**,解掉 `ports.py`(host 契约)→ 651 行
  内存实现这条全图唯一的"向上"边。`SentLog` 本体留在 `src/`。
- **`visible_host_tools` 移进 `tool_port.py`**(它自己的 docstring 就写着该在那儿)。
- **包根 re-export `configure`/`get_ports`/`ArisePorts`**,`ports.py` 那条一直写错
  调用路径的错误信息随之成立。
- **结构契约**:import-linter 两条(共享层不依赖入口层、入口之间互不依赖)+ CI 一步;
  「core 不碰平台适配层」改由 `tests/test_import_contract.py` 用 AST 守——原因见下条。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sonnet 跑的两轴 review:3 条 finding 存活(1 条判定字段自相矛盾,按正文推理为准)。

- runtime_state.py/gate_pipeline.py 两处文档把入口触达范围数错了:`_debouncer`
  实际只被反应式一条入口碰(不是三条);`_evaluate_unified_gate` 实际被四条触发
  路径调用而不是三条(漏了即刻追问)。逐条 grep 复核确认。
- 更重要的一条:我写进五处的"import-linter 契约会静默通过"是错的。独立复现:
  `forbidden_modules` 一旦指向外部包的非顶层子模块(`nonebot.adapters`),
  import-linter 在建图之前就显式拒绝,报 `ValueError`、退出码 1、CI 会红——不是
  "看不见违规所以悄悄放行"。结论不变(import-linter 确实表达不了这条规则),
  但给出的理由是错的,属于"给不做的分支配一个听起来合理但实际没核对到位的理由"
  这一类,已在 pyproject.toml/test_import_contract.py/ci.yml 三处改成准确描述。

loop_factory.py 那条 finding 经核实是文档在描述两个不同的合法粒度(入口簇数 vs
TriggerPath 枚举值数),不是缺陷,未改。conftest.py 加载顺序那条降级为锦上添花,
未改。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit 49494f577d into main 2026-08-11 01:02:33 +00:00
Yushu deleted branch refactor/97-split-entry-modules 2026-08-11 01:02:34 +00:00
Sign in to join this conversation.
No description provided.