From 6d904a09c21ef2d1ee096f191d0b9dfea92a7348 Mon Sep 17 00:00:00 2001 From: song Date: Wed, 14 Oct 2026 15:55:39 +0800 Subject: [PATCH 1/2] test(architecture): pin package imports of loopx internals The monorepo-distribution-split RFC says a co-located package registers through CapabilityRegistry and never imports kernel-private modules, and its Section 9 row relied on "review of packages/*/src" to prove zero violations. Three packages already import 13 loopx modules, including control_plane internals, and nothing stopped a fourth. Pin the current edges exactly, following test_top_level_module_budget: a new package -> loopx import fails with the RFC rule in the message, and a retired edge must lower the pin in the same PR. The scan is an AST walk over packages/*/src including function-body imports. Both RFC mirrors now cite the executable check and state that the pinned edges are the M4/M5 backlog, not a sanctioned surface. Signed-off-by: song --- .../rfcs/monorepo-distribution-split-v0.md | 20 +++- .../monorepo-distribution-split-v0.zh-CN.md | 16 ++- .../package_kernel_import_pins.json | 26 +++++ .../test_package_kernel_import_ratchet.py | 100 ++++++++++++++++++ 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 tests/architecture/package_kernel_import_pins.json create mode 100644 tests/architecture/test_package_kernel_import_ratchet.py diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index c0746aac67..0c65a7bc15 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -269,7 +269,7 @@ changes. | Kernel-only install without Node | package-smoke lane on a runner without Node: `pip install loopx-core && loopx --help && loopx status --format json` | exit 0 | Effectful commands excluded | | Effectful command without Node fails typed | same runner: `loopx heartbeat-prompt ...` | `node_unavailable` diagnostic, exit non-zero | — | | Absent capability package = off state | install `loopx-core` only; `loopx capability list` | capability reported unavailable, core commands unaffected | — | -| No kernel truth duplicated | import-boundary tests + review of `packages/*/src` for kernel-private imports | zero violations | Static imports only, as today | +| No kernel truth duplicated | import-boundary tests + `tests/architecture/test_package_kernel_import_ratchet.py` (pins every `loopx.*` import under `packages/*/src`) | no new package → kernel import; a retired edge lowers the pin in the same PR | Static imports only; 13 pre-existing edges in 3 packages (`loopx-ark-turn`, `loopx-finance-value-discovery`, `loopx-jev`) remain pinned, so "zero" is a target, not the measured state | | Meta-package parity | `pip install loopx` then existing full smoke | identical to pre-split smoke | — | Deterministic conformance rows above are required for each milestone; no live @@ -342,6 +342,24 @@ qualification or performance claim is made by this RFC. - **Effect on normative design:** the budget numbers in Sections 2 and 5 follow the new base; the decision to lower the count over time is unchanged. +### 2026-10-14 — Package → kernel import pin + +- **Baseline:** `70bc98198`. +- **Delivered:** `tests/architecture/test_package_kernel_import_ratchet.py` and + `package_kernel_import_pins.json` pin the 13 `loopx.*` modules that three + co-located packages import today. A new edge fails; a removed edge must + lower the pin in the same PR. +- **Evidence:** AST scan of `packages/*/src/**/*.py`, including function-body + imports. `loopx-ark-turn` imports `file_lock` and + `control_plane.turn_driver.host_candidate` without declaring `loopx` as a + dependency; `loopx-jev` imports seven `control_plane`/`capabilities` + modules; `loopx-finance-value-discovery` imports two `extensions` modules. +- **Known gaps:** the Section 9 row still names zero violations as the + target; the pinned edges are the migration backlog for M4/M5, not a + sanctioned surface. D1–D4 remain open. +- **Effect on normative design:** none; the row's evidence column now points + at an executable check instead of a manual review. + ## Appendix B: Decision log | Date | Decision | Owner / approval | Alternatives | Normative sections changed | diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md index 11d1629e7f..df7a4d743c 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -180,7 +180,7 @@ capability 包直接采用现有 `extension.toml` 契约,不做扩展。唯一 | 无 Node 的仅内核安装 | 无 Node 的 runner 上 package-smoke:`pip install loopx-core && loopx --help && loopx status --format json` | 退出码 0 | 不含有副作用命令 | | 无 Node 的有副作用命令类型化失败 | 同一 runner:`loopx heartbeat-prompt ...` | `node_unavailable` 诊断,非零退出 | — | | 缺失 capability 包 = off state | 仅安装 `loopx-core`;`loopx capability list` | capability 报告不可用,核心命令不受影响 | — | -| 无内核真相重复 | import 边界测试 + 审查 `packages/*/src` 是否 import 内核私有模块 | 零违规 | 与现状一致,仅静态 import | +| 无内核真相重复 | import 边界测试 + `tests/architecture/test_package_kernel_import_ratchet.py`(钉住 `packages/*/src` 下每一个 `loopx.*` import) | 不新增包 → 内核 import;退役的边必须在同一 PR 降低 pin | 仅静态 import;3 个包(`loopx-ark-turn`、`loopx-finance-value-discovery`、`loopx-jev`)现存 13 条边仍被钉住,“零”是目标而非实测状态 | | meta-package 一致性 | `pip install loopx` 后跑现有完整 smoke | 与拆分前 smoke 一致 | — | 以上确定性一致性行是每个里程碑的必需项;本 RFC 不做任何实机 qualification 或性能主张。 @@ -230,6 +230,20 @@ capability 包直接采用现有 `extension.toml` 契约,不做扩展。唯一 - **已知缺口:** 全部里程碑;D1–D4 仍为未决。 - **对规范性设计的影响:** 第 2、5 节的预算数字跟随新基线;"随时间只降不升"的决策不变。 +### 2026-10-14 — 包 → 内核 import 钉住 + +- **基线:** `70bc98198`。 +- **交付:** `tests/architecture/test_package_kernel_import_ratchet.py` 与 + `package_kernel_import_pins.json` 钉住三个 co-located 包今天 import 的 13 个 + `loopx.*` 模块。新增一条边即失败;移除一条边必须在同一 PR 降低 pin。 +- **证据:** 对 `packages/*/src/**/*.py` 做 AST 扫描,包含函数体内 import。 + `loopx-ark-turn` import 了 `file_lock` 与 `control_plane.turn_driver.host_candidate` + 却未声明 `loopx` 依赖;`loopx-jev` import 了 7 个 `control_plane`/`capabilities` + 模块;`loopx-finance-value-discovery` import 了 2 个 `extensions` 模块。 +- **已知缺口:** 第 9 节该行仍以零违规为目标;被钉住的边是 M4/M5 的迁移积压, + 不是被认可的接口。D1–D4 仍未决。 +- **对规范设计的影响:** 无;该行的证据列从人工审查改为可执行检查。 + ## 附录 B:决策日志 | 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 | diff --git a/tests/architecture/package_kernel_import_pins.json b/tests/architecture/package_kernel_import_pins.json new file mode 100644 index 0000000000..9ff6e1147e --- /dev/null +++ b/tests/architecture/package_kernel_import_pins.json @@ -0,0 +1,26 @@ +{ + "schema": "loopx_package_kernel_import_pins_v0", + "baseline_commit": "70bc98198ce9e8c33e461cc41fe77243d713dcf0", + "note": "Exact pin of every `loopx.*` module imported by a co-located package. The monorepo-distribution-split RFC says a package registers through CapabilityRegistry and never imports kernel-private modules; these are the pre-existing edges. Remove an entry in the PR that removes the import. Adding an entry needs a recorded design decision.", + "imports": { + "loopx-ark-turn": [ + "loopx.control_plane.turn_driver.host_candidate", + "loopx.file_lock" + ], + "loopx-finance-value-discovery": [ + "loopx.extensions.lark.presentation.message_card", + "loopx.extensions.presentation" + ], + "loopx-jev": [ + "loopx.capabilities.progress_review.receipt", + "loopx.control_plane.effect_runtime", + "loopx.control_plane.goals.acceptance", + "loopx.control_plane.work_items.autonomous_replan_ack", + "loopx.control_plane.work_items.external_progress_review", + "loopx.control_plane.work_items.progress_observation", + "loopx.control_plane.work_items.progress_result", + "loopx.entrypoint", + "loopx.file_lock" + ] + } +} diff --git a/tests/architecture/test_package_kernel_import_ratchet.py b/tests/architecture/test_package_kernel_import_ratchet.py new file mode 100644 index 0000000000..24547e947f --- /dev/null +++ b/tests/architecture/test_package_kernel_import_ratchet.py @@ -0,0 +1,100 @@ +"""Co-located packages do not grow new imports of `loopx` internals. + +Anchored on `docs/architecture/rfcs/monorepo-distribution-split-v0.md` +Section 5: a capability package "may only register providers, capability +contracts and CLI subcommands through `CapabilityRegistry`; it never imports +kernel-private modules." Three packages already do. This guard pins those +edges exactly so a new one needs a recorded decision and a removed one +lowers the pin in the same PR, following `test_top_level_module_budget`. +""" + +from __future__ import annotations + +import ast +import json +import re +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[2] +PACKAGES_ROOT = REPO_ROOT / "packages" +PINS_FIXTURE = Path(__file__).with_name("package_kernel_import_pins.json") +PINS_SCHEMA = "loopx_package_kernel_import_pins_v0" +_COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") + + +def _loopx_imports(source: str) -> set[str]: + """Every `loopx.*` module a source file imports, at any nesting depth.""" + found: set[str] = set() + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.ImportFrom): + if node.level == 0 and node.module and node.module.split(".")[0] == "loopx": + found.add(node.module) + elif isinstance(node, ast.Import): + found.update( + alias.name for alias in node.names if alias.name.split(".")[0] == "loopx" + ) + return found + + +def _observed(packages_root: Path | None = None) -> dict[str, list[str]]: + root = packages_root or PACKAGES_ROOT + observed: dict[str, set[str]] = {} + for path in sorted(root.glob("*/src/**/*.py")): + modules = _loopx_imports(path.read_text(encoding="utf-8")) + if modules: + observed.setdefault(path.relative_to(root).parts[0], set()).update(modules) + return {package: sorted(modules) for package, modules in sorted(observed.items())} + + +def _pins() -> dict[str, Any]: + payload = json.loads(PINS_FIXTURE.read_text(encoding="utf-8")) + assert payload["schema"] == PINS_SCHEMA, ( + f"{PINS_FIXTURE.name} schema is {payload['schema']!r}, expected {PINS_SCHEMA!r}" + ) + assert _COMMIT_SHA.match(str(payload["baseline_commit"])), ( + "baseline_commit must be the full 40-character commit the pins were measured at" + ) + imports = payload["imports"] + assert isinstance(imports, dict) and all( + isinstance(modules, list) and modules == sorted(set(modules)) + for modules in imports.values() + ), "imports must map package directory -> sorted, duplicate-free module list" + return payload + + +def test_packages_import_exactly_the_pinned_loopx_modules() -> None: + pinned = {package: set(modules) for package, modules in _pins()["imports"].items()} + observed = {package: set(modules) for package, modules in _observed().items()} + + added = { + package: sorted(observed.get(package, set()) - pinned.get(package, set())) + for package in observed + if observed.get(package, set()) - pinned.get(package, set()) + } + assert not added, ( + f"co-located packages gained imports of loopx internals: {added}. The " + "monorepo-distribution-split RFC routes packages through CapabilityRegistry; " + "a new kernel import needs a recorded design decision, not a pin update." + ) + + removed = { + package: sorted(pinned.get(package, set()) - observed.get(package, set())) + for package in pinned + if pinned.get(package, set()) - observed.get(package, set()) + } + assert not removed, ( + f"pinned package imports no longer exist: {removed}. Lower the pin in " + f"{PINS_FIXTURE.name} in the same PR so the ratchet records the retirement." + ) + + +def test_import_scan_sees_nested_and_aliased_imports() -> None: + source = ( + "import loopx.file_lock as fl\n" + "def f():\n" + " from loopx.control_plane import effect_runtime\n" + " from .local import helper\n" + " import json\n" + ) + assert _loopx_imports(source) == {"loopx.file_lock", "loopx.control_plane"} From 55cc22e9c753abcee74316c7d2885dc11f31d0ce Mon Sep 17 00:00:00 2001 From: song Date: Thu, 15 Oct 2026 08:25:58 +0800 Subject: [PATCH 2/2] docs(rfc): keep zero violations as the kernel-import required result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 9 of the monorepo split RFC keeps "zero violations" as the required result for "No kernel truth duplicated"; the package → kernel import ratchet is listed as an interim no-growth guard in the evidence column, with the note that a green pin does not satisfy the row or the M4/M5 milestones that cite it while 13 edges remain pinned. Appendix A says the same in both mirrors, so "no normative change" is now accurate. Signed-off-by: song --- .../rfcs/monorepo-distribution-split-v0.md | 14 ++++++++------ .../rfcs/monorepo-distribution-split-v0.zh-CN.md | 10 ++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index 0c65a7bc15..9cce2d0b2e 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -269,7 +269,7 @@ changes. | Kernel-only install without Node | package-smoke lane on a runner without Node: `pip install loopx-core && loopx --help && loopx status --format json` | exit 0 | Effectful commands excluded | | Effectful command without Node fails typed | same runner: `loopx heartbeat-prompt ...` | `node_unavailable` diagnostic, exit non-zero | — | | Absent capability package = off state | install `loopx-core` only; `loopx capability list` | capability reported unavailable, core commands unaffected | — | -| No kernel truth duplicated | import-boundary tests + `tests/architecture/test_package_kernel_import_ratchet.py` (pins every `loopx.*` import under `packages/*/src`) | no new package → kernel import; a retired edge lowers the pin in the same PR | Static imports only; 13 pre-existing edges in 3 packages (`loopx-ark-turn`, `loopx-finance-value-discovery`, `loopx-jev`) remain pinned, so "zero" is a target, not the measured state | +| No kernel truth duplicated | import-boundary tests + review of `packages/*/src` for kernel-private imports; interim guard: `tests/architecture/test_package_kernel_import_ratchet.py` pins every `loopx.*` import under `packages/*/src`, fails on a new edge, and requires a retired edge to lower the pin in the same PR | zero violations | Static imports only, as today. The pin currently holds 13 pre-existing edges in 3 packages (`loopx-ark-turn`, `loopx-finance-value-discovery`, `loopx-jev`); a green ratchet proves no growth, not this row, until the pin is empty | | Meta-package parity | `pip install loopx` then existing full smoke | identical to pre-split smoke | — | Deterministic conformance rows above are required for each milestone; no live @@ -354,11 +354,13 @@ qualification or performance claim is made by this RFC. `control_plane.turn_driver.host_candidate` without declaring `loopx` as a dependency; `loopx-jev` imports seven `control_plane`/`capabilities` modules; `loopx-finance-value-discovery` imports two `extensions` modules. -- **Known gaps:** the Section 9 row still names zero violations as the - target; the pinned edges are the migration backlog for M4/M5, not a - sanctioned surface. D1–D4 remain open. -- **Effect on normative design:** none; the row's evidence column now points - at an executable check instead of a manual review. +- **Known gaps:** the Section 9 row keeps zero violations as its required + result; the 13 pinned edges are the migration backlog for M4/M5, not a + sanctioned surface, and a green ratchet does not satisfy that row or the + M4/M5 milestones that cite it. D1–D4 remain open. +- **Effect on normative design:** none; the required result is unchanged and + the evidence column gains an executable interim no-growth check beside the + manual review. ## Appendix B: Decision log diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md index df7a4d743c..ca89334bd2 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -180,7 +180,7 @@ capability 包直接采用现有 `extension.toml` 契约,不做扩展。唯一 | 无 Node 的仅内核安装 | 无 Node 的 runner 上 package-smoke:`pip install loopx-core && loopx --help && loopx status --format json` | 退出码 0 | 不含有副作用命令 | | 无 Node 的有副作用命令类型化失败 | 同一 runner:`loopx heartbeat-prompt ...` | `node_unavailable` 诊断,非零退出 | — | | 缺失 capability 包 = off state | 仅安装 `loopx-core`;`loopx capability list` | capability 报告不可用,核心命令不受影响 | — | -| 无内核真相重复 | import 边界测试 + `tests/architecture/test_package_kernel_import_ratchet.py`(钉住 `packages/*/src` 下每一个 `loopx.*` import) | 不新增包 → 内核 import;退役的边必须在同一 PR 降低 pin | 仅静态 import;3 个包(`loopx-ark-turn`、`loopx-finance-value-discovery`、`loopx-jev`)现存 13 条边仍被钉住,“零”是目标而非实测状态 | +| 无内核真相重复 | import 边界测试 + 审查 `packages/*/src` 是否 import 内核私有模块;过渡守卫:`tests/architecture/test_package_kernel_import_ratchet.py` 钉住 `packages/*/src` 下每一个 `loopx.*` import,新增一条边即失败,退役的边必须在同一 PR 降低 pin | 零违规 | 与现状一致,仅静态 import。pin 当前仍持有 3 个包(`loopx-ark-turn`、`loopx-finance-value-discovery`、`loopx-jev`)的 13 条既有边;守卫变绿只证明不再增长,在 pin 清空之前不满足本行 | | meta-package 一致性 | `pip install loopx` 后跑现有完整 smoke | 与拆分前 smoke 一致 | — | 以上确定性一致性行是每个里程碑的必需项;本 RFC 不做任何实机 qualification 或性能主张。 @@ -240,9 +240,11 @@ capability 包直接采用现有 `extension.toml` 契约,不做扩展。唯一 `loopx-ark-turn` import 了 `file_lock` 与 `control_plane.turn_driver.host_candidate` 却未声明 `loopx` 依赖;`loopx-jev` import 了 7 个 `control_plane`/`capabilities` 模块;`loopx-finance-value-discovery` import 了 2 个 `extensions` 模块。 -- **已知缺口:** 第 9 节该行仍以零违规为目标;被钉住的边是 M4/M5 的迁移积压, - 不是被认可的接口。D1–D4 仍未决。 -- **对规范设计的影响:** 无;该行的证据列从人工审查改为可执行检查。 +- **已知缺口:** 第 9 节该行的必需结果仍是零违规;被钉住的 13 条边是 M4/M5 的 + 迁移积压,不是被认可的接口,守卫变绿不满足该行,也不满足引用它的 M4/M5 + 里程碑。D1–D4 仍未决。 +- **对规范设计的影响:** 无;必需结果不变,证据列在人工审查之外增加一项可执行的 + 过渡性不增长检查。 ## 附录 B:决策日志