结构批:拆 __init__.py + 抽 runtime_state + StoragePort 抽出 + import-linter(结论:不建子包) #97
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
为什么不建子包(已拍板,别在实现期重开)
「平铺不好找」这个前提本身从没被公平测过——
.codegraph/codegraph.db停在 2026-07-02、落后 27 天,CLI 在本机崩。(该索引由维护者另行处理,不属本票。)任务一:拆
__init__.py(2274 行 → 用entry_*.py前缀,零新目录)它是本包唯一能在本仓 review 词汇表上直接落地的结构问题(Fowler
Divergent Change):@scheduler.scheduled_job/@event_postprocessor都可以放子模块,由__init__.pyimport 触发注册)。簇边界(可直接作为
entry_*.py的切分依据,行数为实测):_admin_gate/_argument_of)两个必须先做的前置(否则会踩坑)
runtime_state.py,收_pending_flushes(跨 3 簇)、_last_group_chat_activity(跨 2 簇)、_debouncer。不先做的话entry_drive_tick/entry_callback会去 importentry_reactive。_last_persona_fingerprint是标量、被tests/test_admin_commands.py:62重绑,re-export 会让那次 monkeypatch 静默失效(改的是包根那个名字,被测代码读的是子模块里的)。直接改那 ~25 行测试 import,让任何遗漏以ImportError的形式立刻失败,而不是变成一条假绿测试。任务二:
StoragePort聚合 Protocol 从sent_log.py抽出sent_log.py809 行里装着:聚合StoragePort(继承 24 个切片)+ 651 行SentLog内存实现。而SentLog与PersistentStorage的 public 方法各 71 个、逐字相同(同一接口的两个实现),71 个里只有 7 个跟"已发日志"有关——文件名是 issue #3 的历史沉积。副作用:
ports.py → sent_log.py是全图唯一一条从装配层「向上」进持久化层的边——host 契约文件为了拿一个 Protocol,拖进了 651 行内存实现。StoragePort抽到自己的模块,解掉那条边。SentLog本体留在src/。把它移进tests/是打包决定不是结构简化,-651 行是记账不是收益;要做请单独立项。任务三:import-linter,只要 forbidden 契约
只守一条:core 模块不得 import nonebot。例外:
entry_*.py(插件入口)、config.py(get_plugin_config)、storage.py(nonebot_plugin_orm)。拍板理由:
entry_*.py,没有机械守卫就可能漏进非 entry 模块。明确不采纳全套层级契约(
layers+exhaustive):它开张第一天就要给recency_decay/judge_persona_drift两个横切原语写ignore_imports例外,而 issue #88 刚以「一上线就需要长期维护的例外表才能产出零信号」否掉过同一形状的 CI 依赖审计脚本。forbidden 契约没有这个问题。.importlinter配置 + dev 依赖 + 一步 CI。visible_host_tools:它定义在admin.py:78,却被runtime_loop.py:10与subagent.py:32import——与 admin 无关,且它自己的 docstring 就写着该在工具港。移进tool_port.py。(这条是层级契约扫出来的 4 条违规里唯一一条即使不上层级契约也该修的。)明确不动
runtime_loop.py一行不动。三份方案罕见地一致:5 个公开方法 / 44 个方法,16 个 handler 共享同一个_TurnState(ADR-0012 pre-send regime 的真不变量——send_message本轮只入队不发,同轮self_recall/edit要在真发出前拦下改写)。拆它就是把深模块换成浅模块。storage.py不拆。Ousterhout 意义上最重要的事(把宽接口切成 26 个窄 Protocol、每片贴着自己的领域)这个仓已经做完了,1274 行按已声明切片归类分得干干净净——所以拆它不会让接口更深一分,只买可导航性,是上述三件事之后才轮得到的第四件。own_tables()用__module__.startswith(f"{__package__}.")筛表,改成子包后__package__会静默变义导致漏建表;所幸tests/test_schema_coverage.py用的是硬编码前缀(独立第二条推导路径)会红。什么时候才回头考虑目录
只有当 forbidden 契约在实践中被反复用
ignore_imports打补丁,或者出现「最近 N 个 PR 触及的簇数中位数 ≥ 3」这类实测信号(说明改动天然横切、目录也集中不了 diff),才值得重开。单独的「55 个文件看着乱」不算——Fowler 那张表上没有对应条目,
Speculative Generality倒是有。正文补充四处(2026-08-03 grill 结论,均为本票范围内)
一、新增任务:修
ports.py的错误文案 + 把 host 公开面 re-export 到包根这是本票的第四个任务,与任务一(拆
__init__.py)碰同一个文件,必须同批做——否则__init__.py会被连着重写两次。现状是错的:
ports.py:172的错误信息告诉 host 调nonebot_plugin_arise.configure(ArisePorts(...)),但configure从未被 re-export 到包根(__init__.py里只在一处注释文本中出现过这个词),真实路径是nonebot_plugin_arise.ports.configure(tests/conftest.py走的就是它)。issue #93 修过这条消息的"字段列表"那半,调用路径那半原样留着——而这是 host 第一次接入时看到的第一条错误信息。已拍板:re-export 到包根,那条文案随之变成真的。
__init__.py里 re-exportconfigure/get_ports/ArisePorts三个名字。ports.py:172的文案随之成立,不需要再改路径(只需确认它与 re-export 后的真实路径一致)。docs/adr/0001-standalone-core-and-ports.md(追加更新节):host 入口的公开面 = 包根。configure(已核实,两份文档零命中)——host 入口的公开面从没被设计文档规定过,目前唯一"规定"它的东西就是那条写错了的错误信息。所以这不是推翻既有决策,是填一个从没被决定过的空白;且零 host 已接入,现在定公开面免费。二、
storage.py不拆——补上可观测的重评触发条件正文「明确不动」那节现在写的是「是三件事之后才轮得到的第四件」,这是个没有触发条件的悬空留痕。补一条可观测的:
三、AC 补一条:
conftest.py的插件加载顺序约束tests/conftest.py::pytest_configure有一条载荷约束:NoneBot 必须是第一个 import 本插件的人,否则load_from_toml报Module ... is not loaded as a plugin。对本票的
entry_*.py拆分是安全的(collection 发生在pytest_configure之后),但三份评估方案都没把它写进 AC,我也漏了。写下来,免得实现时踩了才回头查:pytest_configure的加载顺序约束仍成立;若新增模块在 NoneBot 加载前被 import,会以Module ... is not loaded as a plugin的形式失败。四、
TriggerPath明示不处理(不是遗漏)红队实测 import-linter 的层级契约时报出 4 条违规,其中两条被点名:
visible_host_tools(本票已收,移进tool_port.py)和TriggerPath(定义在decision_snapshot.py,被 cost / explain / snapshot 三个模块消费)。TriggerPath本票不处理,理由:我们只上 forbidden 契约、不上层级契约(见正文任务三),而它只在层级契约下才算违规。visible_host_tools之所以仍要修,是因为它即使不上层级契约也是错位的(自己的 docstring 就写着该在工具港)。写出来是为了不留白——不是漏了,是判过不动。若将来真上层级契约,这条会重新变成待处理项。
措辞更正:正文「明确不动」里的「
runtime_loop.py一行不动」说过头了原意是不重构
RuntimeLoop这个类——5 public / 44 methods 的深模块,16 个 handler 共享_TurnState的 pre-send 不变量(ADR-0012:send_message本轮只入队不发,同轮self_recall/edit要在真发出前拦下改写),拆它就是把深模块换成浅模块。这一点不变。但字面「一行不动」与本票另外两项任务直接矛盾:
visible_host_tools从admin.py移进tool_port.py→ 强制改runtime_loop.py:10的 importStoragePort从sent_log.py抽出 → 同样强制改runtime_loop.py的 import准确表述:
runtime_loop.py的类结构与方法体一行不动;因其它模块搬家而必须跟随的 import 行照改。判断标准很简单——如果一处改动不是「跟着别人搬家改 import」,那它就超出本票范围了(构造函数的 44 个参数收 config dataclass 属于 #105,排在本票之后)。