Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion docs/architecture/rfcs/monorepo-distribution-split-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 + 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
Expand Down Expand Up @@ -342,6 +342,26 @@ 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 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

| Date | Decision | Owner / approval | Alternatives | Normative sections changed |
Expand Down
18 changes: 17 additions & 1 deletion docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 边界测试 + 审查 `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 或性能主张。
Expand Down Expand Up @@ -230,6 +230,22 @@ 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 节该行的必需结果仍是零违规;被钉住的 13 条边是 M4/M5 的
迁移积压,不是被认可的接口,守卫变绿不满足该行,也不满足引用它的 M4/M5
里程碑。D1–D4 仍未决。
- **对规范设计的影响:** 无;必需结果不变,证据列在人工审查之外增加一项可执行的
过渡性不增长检查。

## 附录 B:决策日志

| 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 |
Expand Down
26 changes: 26 additions & 0 deletions tests/architecture/package_kernel_import_pins.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
100 changes: 100 additions & 0 deletions tests/architecture/test_package_kernel_import_ratchet.py
Original file line number Diff line number Diff line change
@@ -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"}
Loading