test(architecture): pin the top-level module count before it grows again - #5191
Conversation
Milestone M0 of monorepo-distribution-split-v0 asked for a checked-in budget and a guard that fails when loopx/ gains a top-level module. The import-boundary tests only protect inward edges, so 66 new top-level modules landed in 45 days with nothing to push back. Signed-off-by: karenchuu <25980598+karenchuu@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Exact reviewed head: d05dccedb5d0a0ffe45f91ca85bb510efb2a690d; immutable baseline: 9eaacfbf2ff93d5386cee82ddf0847d9c739e78a.
动机
依据是 distribution-split RFC 的 M0 和 §9:给直接位于 loopx/ 的 Python 模块数量建立护栏,避免迁移前继续无声增长。独立读取基线 Git tree,实际为 147;RFC 的148是旧示例,不应照抄。这里完成的是 M0 的有界验收,不是关闭 #5072 的整个分发拆分计划。
改动思路
复用现有 architecture pytest 入口,用一个可审查 JSON fixture 保存基线、数量和三项入口 allowlist。直接 glob 只数顶层模块;数量超出时失败,迁移减少数量而未更新 pin 时也失败。既有 import-boundary 和 maintainability 测试管的是依赖边/单文件指标,没有等价的顶层数量护栏。保持现状会留下 M0 缺口;新增运行时服务或第二份隐藏基线都没有必要。
具体改动
完整 diff 是两个文件 +112/-0:top_level_module_budget.json 固定完整基线 SHA、147和 __init__.py / entrypoint.py / cli.py;test_top_level_module_budget.py 校验 fixture、真实树数量与入口,并用四个临时树测试超出、等于、低于预算及嵌套排除。生产代码、UI、权限和持久化均未改动。
独立跑 native collector:6 passed。额外使用基线真实147个文件名构造临时树,调用这份 PR 的真实测试函数:新增第148个顶层文件被拒绝;移走 CLI 入口被拒绝;146个文件仍固定147被拒绝;仅新增嵌套模块通过,迁移后降低 pin 通过。不是拿 helper 的当前输出反推预期。
对主干的风险
没有发现阻塞问题。Ruff、format、new-file mypy、diff check 和两条 changed-path public-boundary 检查通过。依赖准备一致后,完整 architecture lane:基线 812 passed / 2 failed,head 818 passed / 2 failed。我比较了失败用例身份和完整诊断,不只比较数量:registry-I/O census 与 goal-instance inventory 两处诊断相同,且不在这个 diff 的因果路径上;不能把它们算作本 PR 回归。按当前配置未查询或等待远端 CI。
非阻塞 P2:_count_offenders 的 “newest path first” 实际只是文件名字典序倒排,不知道哪个文件新加;真实失败信息也只报告数量,未像 PR 描述所说列出新增 offender。建议改准命名/注释和 PR 描述,或简化数量谓词。这个问题不影响当前 count/allowlist 的拒绝语义。
同一 PR 仍可修改预算并增加模块,这不是不可绕过的历史硬限;预算变化必须继续由 exact-diff review 审查。这里没有验证后续 M1/M2 的兼容 shim 或 M3 的分发决策,也不要求本 PR 先实现那些工作。
我的整体评价
APPROVE。这是有真实维护收益、已接入正常测试入口的完整小切片;同作者 bounded all-state 扫描未发现同形低价值批次。Future-facing pass 已检查最近的 import/size owner,认定不应强行合并不同演进原因的护栏;上述 lexical diagnostics 简化是非阻塞建议,不需要扩展新框架。批准这份测试 PR 不意味着批准后续分发迁移,也不是合并授权。
English verdict: APPROVE - head d05dcce. The accepted M0 module-count guard has distinct durable value: independently measured147, six native tests, real-consumer growth/move/entrypoint counterexamples and static checks pass. Full architecture base/head812/818 passed with the same two complete failure diagnostics. Clarify the non-blocking lexicographic "newest" wording; no remote CI queried and no runtime migration qualified.
Goal And Delivered Outcome
Outcome basis / optional anchor: RFC
docs/architecture/rfcs/monorepo-distribution-split-v0.md(Accepted, landed in docs(rfc): monorepo distribution split v0 (loopx-core / loopx-workspace / capability packages) + top-level module budget #5073). Section 9 row "Top-level module count never grows" and the milestone table's M0 row name exactly what this PR adds:tests/architecture/top_level_module_budget.jsonplustests/architecture/test_top_level_module_budget.py. The tracking issue is RFC: split LoopX distributions inside the monorepo (loopx-core / loopx-workspace / capability packages) and pin top-level module growth #5072.Goal/source and gap: the RFC's own problem statement is that "the import-boundary tests protect
control_planeinward edges but nothing stops the top level from growing". Measured on the base commit of this PR (9eaacfbf2):ls loopx/*.py | wc -l= 147, andgit log --diff-filter=Aover the previous 45 days shows 66 distinct top-level modules added, 6 of them in the last seven days. The RFC's Appendix A already recorded the same surface drifting 143 → 148 between the proposal being written and being merged. Without a pin, the first move PR (M1) cannot state what it reduced, and every later count is unanchored.Observable before → after, with the validation row that proves it: before, adding
loopx/<new>.pycollected and passed with no signal. After, the guard reports the offending module names and fails; dropping a file into a subpackage without loweringmax_top_level_modulesin the same PR also fails. Proven by theunitandregression_parityrows below (mutations 1, 2, 3 and 4 each produce a failing run).Issue/task and intended base: Related to RFC: split LoopX distributions inside the monorepo (loopx-core / loopx-workspace / capability packages) and pin top-level module growth #5072 — that issue tracks the whole distribution-split program (M0-M5), so it is not closed here. Base is current
main.Scope And Continuation
main. Remaining milestones stay open: M1 (loopx/chat_*→loopx/chat/with shims) and M2 (*_goal_mode→loopx/hosts/) each move files and lower the pinned number, M3+ needs the unapproved decisions D1-D4 first.max_top_level_modules: 148, measured at its own base. The RFC's 2026-09-27 Appendix A entry says the count was re-measured "so M0 pins a count the tree actually has", andloopx/*.pyis 147 at this base, so this PR pins 147 rather than copying 148 forward. Pinning 148 would let one more top-level module in on arrival.max_top_level_modulesby the number of moved files" — which is exactly what the equality assertion here requires.Validation
d05dccedb(head of this branch); baseline comparison run on the unmodified9eaacfbf2.unitpassedpython3 -m pytest -q tests/architecture/test_top_level_module_budget.py→ 6 passed. Two rows pin the real tree (count equals the budget; allowlisted entry points__init__.py,entrypoint.py,cli.pyare still at the top level) and four rows pin the comparison helper against synthetic trees: over budget, exactly at budget, under budget, and a tree with a subpackage holding extra.pyfiles.integrationpassedpython3 -m pytest -q tests/architecturewith the repository's npm dev dependencies installed: head 2 failed, and the unmodified base at the same commit 2 failed, with the same two test ids on both sides (tests/architecture/test_goal_instance_binding_inventory.py::test_goal_instance_inventory_does_not_replace_the_registry_io_census,tests/architecture/test_project_registry_io_census.py::test_checked_in_project_registry_io_manifest_is_current), both pre-existing at this base. An earlier run of the same lane, taken beforenpm ci --ignore-scripts, reported 125 failed / 667 passed on head against 125 / 661 on base, with 90 of those sharing the one cause the repository prints itself (npm dev dependencies missing); the identical-failure comparison held there too, and that row is superseded rather than wrong.staticpassedruff check,ruff format --checkon the new test file,git diff --check, andloopx check --scan-path tests/architecture(public-boundary scan clean, 25 files).mypyon the new file reports no issues; it is not in the pinned[tool.mypy] filesallowlist and this PR does not widen that list.regression_paritypassedloopx/cli.pyinto a subpackage (2 failed: allowlist and equality), rename the fixtureschema(2 failed), shortenbaseline_committo an abbreviated sha (2 failed), repeat a name inallowlist(2 failed), delete the fixture file (2 failed), make the offender slice ignore the overflow width (1 failed). One is equivalent and disclosed as such: changing the early return fromlen(modules) <= maximumtolen(modules) < maximumstays green, because atlen == maximumthe fall-through slice becomes[:0], which is also empty. Two of the nine are caught only by the synthetic rows, not by the real-tree rows, which is why those rows exist.real_entrypointnot_applicablereal_backendnot_applicableintegration(full lane)passedpython3 -m pytest -q tests -m "not stage2c_e2e"on head: 12324 passed / 15 failed / 49 skipped (47m31s); on the unmodified base at the same commit: 12318 passed / 15 failed / 49 skipped (1h03m). The passed delta is exactly the 6 tests this PR adds. Fourteen failures are shared; the remaining slot differs one way on each side, and both resolve as pre-existing or load-related under exclusive re-run of each test on each tree:tests/test_external_scheduler_worker.py::test_default_invocation_persists_backoff_statefails on both trees in isolation, andtests/extensions/test_extension_runtime.py::test_extension_run_terminates_provider_on_timeoutpasses on both trees in isolation. The two aggregate runs also overlapped in wall-clock with another suite, which is the likely reason a timing-sensitive case moved. No failure here is attributable to this diff.pytestcollectingtests/architecture), so the covered surface is the whole diff. What is deliberately not enforced, and why:max_top_level_modulesin the same diff. Nothing in the tree holds a second copy of that number to contradict it, so keeping the budget honest stays a review judgement; the equality assertion at least makes such a change appear as a visible one-line budget edit instead of a silent acceptance of growth.loopx/<subpackage>/is the behaviour the RFC asks for, not a bypass, so the guard must not (and does not) flag it.uv venvplusuv pip install -e ".[test]"rather thanuv sync; Node is pinned to the repository's qualified 22 line for the TypeScript-backed scan; andnpm ci --ignore-scriptswas run in both trees before theintegrationand full-lane numbers quoted above.See validation disclosure guidance.
Frontend / Visual Evidence
Type of Change
LoopX Area
Technical Direction
monorepo-distribution-split-v0milestone M0, tracked by RFC: split LoopX distributions inside the monorepo (loopx-core / loopx-workspace / capability packages) and pin top-level module growth #5072.Shared-authority RFC fixture impact
N/A — this PR claims no progress on the TypeScript control-plane migration or the shared Goal Authority RFC, changes no canonical record and adds no fixture dimension.
Boundary Checklist
.loopx/,.codex/goals/, and liveACTIVE_GOAL_STATE.md).none.