From d54cdef3a3a7042225417f4e23f8c618fa1f19ca Mon Sep 17 00:00:00 2001 From: "lusendong.6789" Date: Sat, 7 Nov 2026 10:10:00 +0800 Subject: [PATCH] refactor(cli): demand-load registry inspection commands Signed-off-by: lusendong.6789 Co-authored-by: TRAE CLI --- .../rfcs/monorepo-distribution-split-v0.md | 45 +++++++ .../monorepo-distribution-split-v0.zh-CN.md | 33 ++++++ loopx/cli_commands/support_control.py | 69 ++--------- loopx/cli_runtime.py | 96 ++++++++++++++- tests/test_cli_entrypoint.py | 112 ++++++++++++++++++ 5 files changed, 295 insertions(+), 60 deletions(-) diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index c8e7ce79f9..7a72a2208d 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -824,6 +824,51 @@ Approval needs a reviewed decision, not implementation progress or silence. installation, R1 managed minimal or D1–D7. No preset, frontend operation, permission or persisted state changes; protected execution keeps its owner. +### 2026-11-07 — Registry inspection selected loading + +- **Baseline and boundary:** `ac6296225`. `registry` and `registry-boundary` + use the existing selected CLI route. Their shared registrar and transport + adapter stay in `cli_runtime`; the explicit full CLI reuses them through + `support_control`, retaining its inspection and renderer patch points. + Registry parsing, classification and Git decisions keep their existing owner. + Python adapts the CLI here; no new decision owner, provider or dispatcher + framework is introduced. Existing imports and I3's minimum one-minor-release + compatibility window remain intact. +- **Compatibility evidence:** 197 source CLI and support-ownership cases pass, + including 26 new inspection/result/rejection/patch-point cases and two help + cases. An independent full-CLI loading exclusion fails on the immutable base + and passes on the candidate through the same public entrypoint. Twenty + successful and rejected requests preserve output, status and exception + semantics across source and independently installed monolithic-wheel consoles. + Help and read paths also work with Node absent and exclude the unselected + Chat, Dashboard and benchmark execution modules. Both revisions pass the real + Git registry-boundary smoke in disposable temporary directories; the earlier + run under `.local` failed because that ancestor intentionally selects a private + registry classification. The failure is retained as fixture-placement evidence. +- **Cost qualification:** 30 interleaved base/head pairs for each of four + help/read workloads on both source and wheel yield 480 samples in 416.7s; + study setup/compatibility/profiling takes another 278.1s, excluding earlier + environment/wheel preparation and tests. On macOS arm64/Python 3.13.5 with + identical dependencies, precompiled bytecode and warm filesystem caches, + fresh-console help medians fall from 1.536–1.609s to 0.100–0.103s; registry + reads from 1.584–1.631s to 0.301–0.310s, and boundary reads from 1.542–1.612s + to 0.222–0.225s. Median RSS falls from about 91–92 MiB to 16–30 MiB. + Both help closures fall from 1,158 to 9 LoopX modules; registry/boundary reads + fall to 148/44. The PR records per-workload median/p95, CPU, separate audit + open counts and unchanged output bytes. No Node starts/IPC or Python-audit + socket events are observed; boundary reads keep their three Git queries. + Warm-cache block I/O is zero, while exact bytes read and native activity + outside the audit remain unknown. These results qualify local CLI cost + reductions, not portable D6 limits, full preparation/model costs or adoption. +- **Validation and remaining work:** changed-file lint passes. The CI lint scope + still fails on the baseline-existing F841, whose repair is already proposed + separately; this result is not waived. Source and wheel evidence does not + upgrade an installed host. M1/#8011, full R0/V10, R1's real managed Turn, + context/model composition, split installation, adoption and complete costs + remain open. No D2/D5/D6/D3, default or authority decision changes. The related + bounded refactor shares the existing grammar and inspection adapter; larger + support/control extraction remains outside this slice. + ## 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 5136da5181..82f0b1440b 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -599,6 +599,39 @@ R0 可以先于目录移动交付,但不代表内核合格。R3/R4 可选, 不关闭完整 R0/V10、M1、拆包安装、R1 managed minimal 或 D1–D7。不改变 preset、 前端操作、权限或持久化状态;受保护执行仍由既有 owner 所有。 +### 2026-11-07 — Registry 检查的选择性加载 + +- **基线与边界:** `ac6296225`。`registry` 和 `registry-boundary` 使用现有 + selected CLI 路由。共享注册器与传输适配器留在 `cli_runtime`;显式完整 CLI + 通过 `support_control` 复用它们,保留原有检查函数与 renderer patch point。 + Registry 解析、分类及 Git 判定仍归现有 owner。Python 在此只适配 CLI,不新增 + 决策 owner、provider 或 dispatcher 框架。既有导入与 I3 至少一个 minor release + 的兼容窗口保持不变。 +- **兼容证据:** 源码 CLI 与 support ownership 的 197 个用例通过,包含新增的 + 26 个检查结果、拒绝与 patch-point 用例及两个 help 用例。独立的完整 CLI 加载 + 排除断言经同一公开入口,在不可变基线上失败、候选版本上通过。20 个成功与 + 拒绝请求在源码及独立安装的单体 wheel 控制台上保持输出、状态与异常语义一致。 + Help 与读取路径在缺少 Node 时仍可用,并排除未选择的 Chat、Dashboard 和 + benchmark 执行模块。两版本在一次性临时目录中均通过真实 Git registry-boundary + smoke;先前放在 `.local` 下的运行失败,因为该祖先路径会按既有规则选择私有 + registry 分类。这次失败作为 fixture 放置证据保留。 +- **成本资格:** 源码与 wheel 各四条 help/读取路径,每条运行 30 对交替 base/head + 样本,共 480 个样本,采样耗时 416.7 秒;研究准备、兼容验证及 profiling 另耗时 + 278.1 秒,不含此前环境、wheel 准备及测试。macOS arm64/Python 3.13.5、相同依赖、 + 预编译字节码和暖文件缓存下,新进程控制台 help 中位数从 1.536–1.609 秒降至 + 0.100–0.103 秒;registry 读取从 1.584–1.631 秒降至 0.301–0.310 秒,边界读取从 + 1.542–1.612 秒降至 0.222–0.225 秒。RSS 中位数从约 91–92 MiB 降至 16–30 MiB。 + 两条 help 的 LoopX 模块闭包均从 1,158 降至 9;registry/边界读取降至 148/44。 + PR 记录逐路径 median/p95、CPU、独立 audit 的文件打开次数及未改变的输出字节。 + 未观察到 Node 启动/IPC 或 Python audit socket 事件;边界读取保留三次 Git 查询。 + 暖缓存块 I/O 为零,准确读取字节数及 audit 之外的原生活动仍未知。这些结果只限定 + 本地 CLI 成本改善,不建立可移植 D6 限值、完整准备/model 成本或采纳结论。 +- **验证与剩余工作:** 改动文件 lint 通过。CI lint 范围仍失败于基线已有的 F841, + 其修复已另行提案;这里不豁免该结果。源码与 wheel 证据不升级已安装 host。 + M1/#8011、完整 R0/V10、R1 的真实受管 Turn、context/model 组合、拆包安装、采纳 + 与完整成本仍开放。D2/D5/D6/D3、默认行为及权限判定均不改变。相关有界重构 + 共享既有 grammar 与检查适配器;更大的 support/control 提取不纳入本切片。 + ## 附录 B:决策日志 | 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 | diff --git a/loopx/cli_commands/support_control.py b/loopx/cli_commands/support_control.py index f42fed65b5..ff42b7bf7e 100644 --- a/loopx/cli_commands/support_control.py +++ b/loopx/cli_commands/support_control.py @@ -4,6 +4,10 @@ from collections.abc import Callable from pathlib import Path +from ..cli_runtime import ( + _handle_registry_inspection_command, + _register_registry_inspection_commands, +) from ..chat_server import ( DEFAULT_CHAT_HOST, DEFAULT_CHAT_PORT, @@ -106,30 +110,7 @@ def register_support_control_commands( register_update_command(subparsers, add_subcommand_format) register_dsh_launch_command(subparsers, add_subcommand_format) - subparsers.add_parser( - "registry", help="Inspect registry goals and adapter declarations." - ) - registry_boundary_parser = subparsers.add_parser( - "registry-boundary", - help="Classify a registry file as local-only, global-local, public projection, or public fixture.", - ) - registry_boundary_parser.add_argument( - "--path", - help="Registry path to classify. Defaults to the active --registry path.", - ) - registry_boundary_parser.add_argument( - "--require-not-tracked", - action="store_true", - help=( - "For registries that cannot be pushed, return non-zero if tracked " - "or if Git cannot verify the tracking state." - ), - ) - registry_boundary_parser.add_argument( - "--require-gitignored", - action="store_true", - help="Return non-zero if the registry should be ignored but is neither ignored nor tracked.", - ) + _register_registry_inspection_commands(subparsers) serve_status_parser = subparsers.add_parser( "serve-status", help="Serve live status JSON for the local dashboard." @@ -300,39 +281,13 @@ def handle_support_control_command( output_format=output_format, ) - if args.command == "registry": - payload = inspect_registry(registry_path) - print_payload(payload, args.format, render_registry_markdown) - return 0 if payload.get("ok") else 1 - - if args.command == "registry-boundary": - boundary_path = Path(args.path).expanduser() if args.path else registry_path - payload = inspect_registry_boundary(boundary_path) - git = payload.get("git") if isinstance(payload.get("git"), dict) else {} - if ( - args.require_not_tracked - and payload.get("ok") - and not payload.get("github_push_allowed") - and git.get("probe_status") == "git_unavailable" - and (git.get("inside_worktree") or git.get("worktree_discovery_unknown")) - ): - payload["ok"] = False - payload["error"] = "Could not determine whether the registry is tracked by Git." - if ( - args.require_gitignored - and payload.get("ok") - and payload.get("should_be_gitignored") - ): - if ( - git.get("inside_worktree") - and not git.get("ignored") - and not git.get("tracked") - ): - payload = dict(payload) - payload["ok"] = False - payload.setdefault("risks", []).append("registry_should_be_gitignored") - print_payload(payload, args.format, render_registry_boundary_markdown) - return 0 if payload.get("ok") else 1 + if args.command in {"registry", "registry-boundary"}: + return _handle_registry_inspection_command( + args, registry_path=registry_path, print_payload=print_payload, + inspect_registry=inspect_registry, render_registry_markdown=render_registry_markdown, + inspect_registry_boundary=inspect_registry_boundary, + render_registry_boundary_markdown=render_registry_boundary_markdown, + ) if args.command == "serve-status": try: diff --git a/loopx/cli_runtime.py b/loopx/cli_runtime.py index e5a5514495..75165b5a8a 100644 --- a/loopx/cli_runtime.py +++ b/loopx/cli_runtime.py @@ -7,6 +7,7 @@ from collections.abc import Callable from contextlib import contextmanager from pathlib import Path +from typing import Any from . import __version__ from .cli_commands.summary_all_registration import GLOBAL_READ_COMMANDS @@ -70,7 +71,9 @@ PROJECT_LIFECYCLE_COMMANDS = { "refresh-state", "checkpoint-context", "read-only-map", "reward", "operator-gate", } -_SELECTED_COMMANDS = _STATUS_COMMANDS | GLOBAL_READ_COMMANDS | PROJECT_LIFECYCLE_COMMANDS | { +# Existing CLI vocabulary; this group selects loading, not registry authority. +_REGISTRY_INSPECTION_COMMANDS = frozenset({"registry", "registry-boundary"}) +_SELECTED_COMMANDS = _STATUS_COMMANDS | GLOBAL_READ_COMMANDS | PROJECT_LIFECYCLE_COMMANDS | _REGISTRY_INSPECTION_COMMANDS | { "todo", "quota", "task-lease", "change-window", "delegation", "turn", "doctor", "commands", "authority-archive", "coordination-shadow", "extension", "slash-commands", "manager-inbox", @@ -310,9 +313,84 @@ def cli_invocation_route(argv: list[str]): yield +def _register_registry_inspection_commands(subparsers: argparse._SubParsersAction) -> None: + """Share the existing registry grammar with the complete support parser.""" + subparsers.add_parser( + "registry", help="Inspect registry goals and adapter declarations." + ) + registry_boundary_parser = subparsers.add_parser( + "registry-boundary", + help="Classify a registry file as local-only, global-local, public projection, or public fixture.", + ) + registry_boundary_parser.add_argument( + "--path", + help="Registry path to classify. Defaults to the active --registry path.", + ) + registry_boundary_parser.add_argument( + "--require-not-tracked", + action="store_true", + help=( + "For registries that cannot be pushed, return non-zero if tracked " + "or if Git cannot verify the tracking state." + ), + ) + registry_boundary_parser.add_argument( + "--require-gitignored", + action="store_true", + help="Return non-zero if the registry should be ignored but is neither ignored nor tracked.", + ) + + +def _handle_registry_inspection_command( + args: argparse.Namespace, + *, + registry_path: Path, + print_payload: Callable[[dict[str, Any], str, Callable[[dict[str, Any]], str]], None], + inspect_registry: Callable[[Path], dict[str, Any]], + render_registry_markdown: Callable[[dict[str, Any]], str], + inspect_registry_boundary: Callable[[Path], dict[str, Any]], + render_registry_boundary_markdown: Callable[[dict[str, Any]], str], +) -> int: + """Keep one inspection adapter and the full CLI's existing patch points.""" + if args.command == "registry": + payload = inspect_registry(registry_path) + print_payload(payload, args.format, render_registry_markdown) + return 0 if payload.get("ok") else 1 + + boundary_path = Path(args.path).expanduser() if args.path else registry_path + payload = inspect_registry_boundary(boundary_path) + git = payload.get("git") if isinstance(payload.get("git"), dict) else {} + if ( + args.require_not_tracked + and payload.get("ok") + and not payload.get("github_push_allowed") + and git.get("probe_status") == "git_unavailable" + and (git.get("inside_worktree") or git.get("worktree_discovery_unknown")) + ): + payload["ok"] = False + payload["error"] = "Could not determine whether the registry is tracked by Git." + if ( + args.require_gitignored + and payload.get("ok") + and payload.get("should_be_gitignored") + ): + if ( + git.get("inside_worktree") + and not git.get("ignored") + and not git.get("tracked") + ): + payload = dict(payload) + payload["ok"] = False + payload.setdefault("risks", []).append("registry_should_be_gitignored") + print_payload(payload, args.format, render_registry_boundary_markdown) + return 0 if payload.get("ok") else 1 + + def _build_selected_parser(command: str) -> LoopXArgumentParser: parser, subparsers = build_cli_parser(parser_class=_SelectedArgumentParser) - if command in _STATUS_COMMANDS: + if command in _REGISTRY_INSPECTION_COMMANDS: + _register_registry_inspection_commands(subparsers) + elif command in _STATUS_COMMANDS: from .cli_commands.status_registration import register_status_commands register_status_commands(subparsers, add_subcommand_format) @@ -710,7 +788,19 @@ def _dispatch_selected(args: argparse.Namespace, raw_argv: list[str]) -> int: if guard_result is not None: return guard_result registry_path, _registry_was_configured = resolve_cli_registry(args, raw_argv) - if args.command == "capability": + if args.command in _REGISTRY_INSPECTION_COMMANDS: + from .registry import ( + inspect_registry, inspect_registry_boundary, + render_registry_markdown, render_registry_boundary_markdown, + ) + + result = _handle_registry_inspection_command( + args, registry_path=registry_path, print_payload=print_payload, + inspect_registry=inspect_registry, render_registry_markdown=render_registry_markdown, + inspect_registry_boundary=inspect_registry_boundary, + render_registry_boundary_markdown=render_registry_boundary_markdown, + ) + elif args.command == "capability": from .cli_commands.capability import handle_capability_command # Keep the explicit full CLI's handler and protected operations unchanged. diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py index 44f6c15192..a5ed2ebe47 100644 --- a/tests/test_cli_entrypoint.py +++ b/tests/test_cli_entrypoint.py @@ -199,6 +199,116 @@ def write_command_fixture(tmp_path: Path) -> tuple[Path, Path]: return registry, runtime_root +@pytest.mark.parametrize("command", ["registry", "registry-boundary"]) +@pytest.mark.parametrize("format_name", ["json", "markdown"]) +@pytest.mark.parametrize("missing", [False, True]) +def test_registry_inspection_keeps_full_cli_result_and_read_only_fixture( + tmp_path: Path, command: str, format_name: str, missing: bool, +) -> None: + registry, runtime = write_command_fixture(tmp_path) + selected = registry.with_name("missing.json") if missing else registry + argv = ["--registry", str(selected), "--runtime-root", str(runtime), + "--format", format_name, command] + before = {p.relative_to(tmp_path): p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()} + legacy = run_cli_main("loopx.cli", argv) + current = run_cli_main("loopx.entrypoint", argv, forbidden_modules=( + "loopx.cli", "loopx.cli_commands.support_control", "loopx.chat_server", + "loopx.dashboard_launcher", "loopx.cli_commands.benchmark_dispatch", + )) + assert current.returncode == legacy.returncode == (1 if missing else 0), current.stderr + assert current.stdout == legacy.stdout + assert current.stderr == legacy.stderr == "" + if format_name == "json": + payload = json.loads(current.stdout) + assert payload["ok"] is not missing + if missing: + assert payload["error"] == "registry file does not exist" + elif command == "registry": + assert payload["goal_count"] == 1 + assert payload["goals"][0]["id"] == "perf-goal" + assert payload["goals"][0]["state_file_exists"] + else: + assert payload["schema_version"] == "loopx_registry_boundary_v0" + assert payload["should_be_gitignored"] and not payload["github_push_allowed"] + assert {p.relative_to(tmp_path): p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()} == before + + +@pytest.mark.parametrize("argv", [ + ["registry", "--format", "json"], ["registry", "--unknown"], + ["registry-boundary", "--path"], ["registry-boundary", "--require-not-tr"], + ["--format", "invalid", "registry"], ["--format", "", "registry"], + ["--reg", "unused", "registry"], ["--registry"], +]) +def test_registry_inspection_reuses_canonical_rejection(argv: list[str]) -> None: + legacy = run_cli_main("loopx.cli", argv) + current = run_cli_main("loopx.entrypoint", argv) + assert current.returncode == legacy.returncode == 2 + assert current.stdout == legacy.stdout == "" + assert current.stderr == legacy.stderr + + +@pytest.mark.parametrize("ok", [False, True]) +def test_full_registry_inspection_keeps_support_patch_points( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ok: bool, +) -> None: + from loopx.cli import main + from loopx.cli_commands import support_control + registry, runtime = write_command_fixture(tmp_path) + calls = [] + payload = {"ok": ok, "future": {"unknown": [None, True]}} + monkeypatch.setattr(support_control, "inspect_registry", lambda path: calls.append(path) or payload) + printed = [] + monkeypatch.setattr(support_control, "render_registry_markdown", lambda value: "preserved renderer") + # The full CLI supplies its print callback; preserve that boundary as well. + monkeypatch.setattr("loopx.cli.print_payload", lambda value, fmt, render: printed.append( + (value, fmt, render(value)))) + assert main(["--registry", str(registry), "--runtime-root", str(runtime), "registry"]) == (0 if ok else 1) + assert calls == [registry] + assert printed == [(payload, "markdown", "preserved renderer")] + + +@pytest.mark.parametrize("git, require_not_tracked, require_gitignored, push_allowed, should_ignore, ok", [ + ({"inside_worktree": True, "probe_status": "git_unavailable"}, True, False, False, True, False), + ({"worktree_discovery_unknown": True, "probe_status": "git_unavailable"}, True, False, False, True, False), + ({"inside_worktree": False, "probe_status": "git_unavailable"}, True, False, False, True, True), + ({"inside_worktree": True, "probe_status": "git_unavailable"}, True, False, True, False, True), + ({"inside_worktree": True, "ignored": False, "tracked": False}, False, True, False, True, False), + ({"inside_worktree": True, "ignored": True, "tracked": False}, False, True, False, True, True), + ({"inside_worktree": True, "ignored": False, "tracked": True}, False, True, False, True, False), + ({"inside_worktree": True, "ignored": False, "tracked": False}, False, True, True, False, True), +]) +def test_registry_inspection_preserves_git_requirement_precedence( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, git: dict[str, object], + require_not_tracked: bool, require_gitignored: bool, + push_allowed: bool, should_ignore: bool, ok: bool, +) -> None: + import argparse + from loopx.cli_commands import support_control + registry = tmp_path / "registry.json" + selected = tmp_path / "selected.json" + calls = [] + owner_rejected = bool(git.get("tracked") and not push_allowed) + payload = {"ok": not owner_rejected, "git": git, "github_push_allowed": push_allowed, + "should_be_gitignored": should_ignore, "future": {"retained": None}, + "risks": ["registry_tracked_but_not_push_allowed"] if owner_rejected else []} + monkeypatch.setattr(support_control, "inspect_registry_boundary", lambda path: calls.append(path) or payload) + printed = [] + args = argparse.Namespace(command="registry-boundary", path=str(selected), format="json", + require_not_tracked=require_not_tracked, require_gitignored=require_gitignored) + result = support_control.handle_support_control_command(args, registry_path=registry, + registry_was_supplied=True, print_payload=lambda value, fmt, render: printed.append((value, fmt)), + output_format=lambda args: args.format) + assert result == (0 if ok else 1) + assert calls == [selected] + assert len(printed) == 1 and printed[0][1] == "json" + assert printed[0][0]["ok"] is ok + assert printed[0][0]["future"] == {"retained": None} + if not ok and require_not_tracked: + assert printed[0][0]["error"] == "Could not determine whether the registry is tracked by Git." + if not ok and require_gitignored and not owner_rejected: + assert "registry_should_be_gitignored" in printed[0][0]["risks"] + + def test_version_flag_skips_full_cli_import() -> None: script = """ import contextlib @@ -294,6 +404,8 @@ def test_command_reference_does_not_require_default_runtime_route( ("argv", "registration_module", "handler_module"), [ (["commands", "--help"], "loopx.help_surface", "loopx.help_surface"), + (["registry", "--help"], "loopx.cli_runtime", "loopx.registry"), + (["registry-boundary", "--help"], "loopx.cli_runtime", "loopx.registry"), (["doctor", "--help"], "loopx.cli_commands.doctor", "loopx.cli_commands.doctor"), (["authority-archive", "--help"], "loopx.cli_commands.authority_archive", "loopx.cli_commands.authority_archive"), (["extension", "--help"], "loopx.cli_commands.extension", "loopx.cli_commands.extension"),