From 61fb0c2ea25d3b41ce5e5342d5dbef9191812bf3 Mon Sep 17 00:00:00 2001 From: Yernat Yestekov <2068106+doublewhy@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:47:46 -0700 Subject: [PATCH] perf(cli): bypass command imports for exact version (#1111) --- ...1-gov-901-cli-version-startup-preflight.md | 165 ++++++++++++++++++ docs/requirements/GOV-901/requirement.md | 6 + .../python/packages/raes_cli/entrypoint.py | 31 ++++ .../python/packages/raes_cli/main.py | 13 +- implementations/python/pyproject.toml | 2 +- .../python/tests/test_corpus_packaging.py | 36 ++++ .../test_issue_1111_cli_version_startup.py | 141 +++++++++++++++ .../tests/test_version_classification.py | 11 +- 8 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 docs/decisions/issue-1111-gov-901-cli-version-startup-preflight.md create mode 100644 implementations/python/packages/raes_cli/entrypoint.py create mode 100644 implementations/python/tests/test_issue_1111_cli_version_startup.py diff --git a/docs/decisions/issue-1111-gov-901-cli-version-startup-preflight.md b/docs/decisions/issue-1111-gov-901-cli-version-startup-preflight.md new file mode 100644 index 000000000..c4d73ad12 --- /dev/null +++ b/docs/decisions/issue-1111-gov-901-cli-version-startup-preflight.md @@ -0,0 +1,165 @@ +# Issue 1111 GOV-901 CLI Version Startup Preflight + +Date: 2026-08-12 + +Issue: #1111. + +Requirement: GOV-901. Discovery lineage: RUN-313 post-review. + +## Decision + +The installed `raes` console script uses a lightweight entry-point callable for +exact global `--version` and `-V` requests. Those two exact argument vectors +read the installed `raes` distribution version, preserve the honest +`0.0.0+unknown` fallback, print the existing output, and return success without +importing the Typer application or its command modules. + +Every other argument vector lazily imports and calls the existing +`raes_cli.main.app` object without changing `sys.argv`. The Typer application +and its version callback remain available for direct and in-process callers. +This is an import-lifetime change, not a new command, option, distribution, or +version source. + +## Measured Gap + +At `e56f3cf54a259e02cde405c1804a044451529936`, the console entry point named +`raes_cli.main:app` imported all six command trees before Typer could invoke +the eager version callback. A CPython 3.13.5 benchmark on macOS used fresh +child processes, a warmed filesystem page cache, `PYTHONHASHSEED=0`, one +warmup, and eight measured samples. Child CPU time came from the delta of +`resource.getrusage(resource.RUSAGE_CHILDREN)`; wall time came from +`time.perf_counter_ns()`. + +| Path | Median child CPU | Median wall | P95 child CPU | +| --- | ---: | ---: | ---: | +| Python no-op | 15.1 ms | 17.8 ms | 17.4 ms | +| `importlib.metadata.version("raes")` | 43.8 ms | 47.1 ms | 60.7 ms | +| `raes --version` | 1,107.1 ms | 1,121.4 ms | 1,197.6 ms | +| `raes --help` | 1,129.8 ms | 1,143.1 ms | 1,163.4 ms | + +`python -X importtime -c 'import raes_cli.main'` reported 975.8 ms +cumulatively for `raes_cli.main`. The largest owning chain was +`raes_cli.conformance` (847.2 ms), its fixture suite (839.8 ms), and +`raes_contracts.contracts` (645.2 ms). Cumulative import times overlap and are +not summed; they identify the command graph loaded before option dispatch. + +The benchmark is supporting evidence rather than a portable absolute latency +claim. Fresh process creation and CPU scheduling vary by host. The portable +defect signal is that an exact version probe imported processor, solver, +contract, conformance, and backend surfaces it could not use. + +## Existing Surface And Ownership Audit + +- `[project.scripts]` in `implementations/python/pyproject.toml` is the sole + installed `raes` command. PyPA console scripts target a no-argument callable, + so the existing entry can move to a lightweight function without adding a + second command or distribution. +- `raes_cli.main` remains the canonical Typer application and command registry. + Its SDL, processor, conformance, semantic, libvirt, and corpus registration + must still load for every delegated invocation. +- GOV-901, ADR-075, issue #90, and + `specs/evolution/versioning-deprecation-and-migration.md` own the version + value and fallback. The wrapper reuses that policy; it does not infer a + source-tree version or read `_version.py`. +- The issue #1097 compatibility lane already builds and installs a wheel, then + smokes `raes --version` and `raes --help`. Installed-wheel acceptance extends + that surface by checking the entry-point target and absence of heavy imports. +- RUN-313 issue #1099 optimized a pass-local processor projection and named CLI + startup as a non-goal. Solver and scheduler changes likewise begin after the + imports paid by the console launcher. RUN-313 therefore records how the gap + was found, while GOV-901 owns the remediation. +- `raes-mcp`, SDL/contract schemas, parsers, validators, runtime and backend + dispatch, module aliases, examples, and experiments do not participate in + distribution-version reporting and are unchanged. +- Repository and GitHub issue/PR searches found no parallel lazy-version entry + point. The existing Typer callback is retained instead of replaced. + +The boundary follows the PyPA +[Entry Points specification](https://packaging.python.org/en/latest/specifications/entry-points/), +which defines a console-script object as a no-argument callable, and Python's +[`importlib.metadata` documentation](https://docs.python.org/3/library/importlib.metadata.html), +which defines `version()` and `PackageNotFoundError` for installed distribution +metadata. + +## Exact-Argument And Compatibility Invariants + +- Only `sys.argv[1:] == ["--version"]` and `sys.argv[1:] == ["-V"]` take the + lightweight path. +- The result is exactly `raes \n`, or + `raes 0.0.0+unknown\n` when the distribution is absent. +- Empty arguments, `--help`, subcommands, unknown options, and version flags + combined with any other token delegate once to the existing Typer app. +- Delegation does not copy, normalize, reorder, or otherwise rewrite + `sys.argv`; Click/Typer retains ownership of parsing, diagnostics, help, and + exit behavior. +- Direct `CliRunner().invoke(raes_cli.main.app, ["--version"])` keeps the same + metadata and fallback behavior through a shared version helper. +- No import cache, persistent result, filesystem read, network access, + environment setting, telemetry, or new failure channel is added. + +## Alternatives Rejected + +- Keeping the evidence only would preserve correct output while making package + managers, support scripts, and compatibility probes pay the unrelated + command graph on every fresh process. +- Lazy Typer command registration could improve `--help`, but changes command + discovery, completion, help rendering, and error surfaces. It is outside the + exact-version defect and is deferred. +- Optimizing Pydantic schema construction would not remove the many other eager + imports. `schema_bundle()` already caches its generated template and returns + a defensive deep copy; changing that isolation contract is unrelated. +- A cross-process version cache adds invalidation and trust questions to a + metadata lookup that already takes only tens of milliseconds. +- Special-casing version inside `raes_cli.main` is too late because importing + the module already imports every command. Exiting during module import would + also break library and test callers. + +## Verification Boundary + +Unit tests cover both exact flags, installed metadata, the +`PackageNotFoundError` sentinel, and a table of non-exact argument forms. A +source subprocess probe asserts that the lightweight path does not import +`raes_cli.main`, `raes_conformance`, `raes_contracts`, `raes_processor`, or Z3. + +The timing guard uses five fresh processes and median child `process_time()`. +It requires the lightweight median to stay below 250 ms and below half the +same-host full-command-import median. The absolute budget is over five times +the measured metadata-only CPU median, while the paired relative check absorbs +large host-speed differences. The structural import assertions are the primary +regression oracle; no single wall-clock sample can fail the test. + +The existing installed-distribution integration fixture builds the real wheel, +installs it in a clean environment without repository `PYTHONPATH`, invokes +the generated `raes --version` script, resolves the published console entry +point, and verifies that exact `-V` does not import the Typer or contracts +graph. The compatibility lane continues to smoke both version and help across +every supported CPython release. + +A post-change same-host benchmark used CPython 3.14.4 free-threaded, one +warmup, and eight fresh processes per path. The reference proxy imported and +called `raes_cli.main.app` exactly as the former console target did; the new +path invoked the installed editable console script. Both received only +`--version`. + +| Path | Median child CPU | Median wall | P95 child CPU | +| --- | ---: | ---: | ---: | +| Pre-change entry-point proxy | 1,121.1 ms | 1,135.6 ms | 1,304.1 ms | +| New exact-version entry point | 38.5 ms | 40.4 ms | 48.3 ms | +| New delegated `--help` | 1,173.1 ms | 1,181.4 ms | 1,205.6 ms | + +The exact-version median used 96.6% less child CPU, or about 29.2 times less, +while help remained on the intentionally unchanged full-command path. + +Repository policy, requirement governance, changed line and branch coverage, +Ruff, built artifacts, documentation, and the canonical `verify_all.py` and +completion graphs remain required with `RAES_REQUIREMENT_UID=GOV-901`. + +## Nonclaims + +- This change does not optimize or place a latency budget on `raes --help` or + any subcommand. +- It does not optimize schema-bundle generation or remove defensive copies. +- It does not change reference-processor, solver, scheduler, runtime, backend, + or experiment performance. +- It does not change the supported Python range, release version, CLI syntax, + output schema, or compatibility policy. diff --git a/docs/requirements/GOV-901/requirement.md b/docs/requirements/GOV-901/requirement.md index c5e7f0a51..aec2515e7 100644 --- a/docs/requirements/GOV-901/requirement.md +++ b/docs/requirements/GOV-901/requirement.md @@ -35,3 +35,9 @@ Requirement inventory expansion. Compatibility claims need explicit versioning a - IMPLEMENTS → POLICY `tools/check_authority_boundary.py` (Authority-boundary gate: changelog_fragments root dropped (GOV-901)) - IMPLEMENTS → ADR `docs/decisions/adrs/adr-019-normative-authority-boundary-manifest.md` (ADR-019 amended: changelog.d/ removed after release-please replaced towncrier (GOV-901)) - TESTS → TEST `implementations/python/tests/test_version_classification.py` (Version-literal classification tests (GOV-901)) +- IMPLEMENTS → GITHUB_ISSUE `1111` (Exact-version CLI startup import boundary) +- IMPLEMENTS → CODE_FILE `implementations/python/packages/raes_cli/entrypoint.py` (Lazy exact-version console entry point) +- IMPLEMENTS → CONFIG `implementations/python/pyproject.toml` (Installed `raes` console-script binding) +- DOCUMENTS → DOCUMENTATION `docs/decisions/issue-1111-gov-901-cli-version-startup-preflight.md` (Measured startup boundary and compatibility invariants) +- TESTS → TEST `implementations/python/tests/test_issue_1111_cli_version_startup.py` (Source, fallback, delegation, and startup-budget guards) +- TESTS → TEST `implementations/python/tests/test_corpus_packaging.py` (Clean installed-wheel entry-point acceptance) diff --git a/implementations/python/packages/raes_cli/entrypoint.py b/implementations/python/packages/raes_cli/entrypoint.py new file mode 100644 index 000000000..46d7c99da --- /dev/null +++ b/implementations/python/packages/raes_cli/entrypoint.py @@ -0,0 +1,31 @@ +"""Lightweight installed-console entry point for the RAES CLI.""" + +from __future__ import annotations + +import sys +from importlib.metadata import PackageNotFoundError, version + +_NOT_INSTALLED_VERSION = "0.0.0+unknown" +_VERSION_ARGUMENTS = frozenset({"--version", "-V"}) + + +def _distribution_version() -> str: + """Return the governed distribution version or its honest sentinel.""" + + try: + return version("raes") + except PackageNotFoundError: + return _NOT_INSTALLED_VERSION + + +def main() -> None: + """Handle exact version probes without importing the full command graph.""" + + arguments = sys.argv[1:] + if len(arguments) == 1 and arguments[0] in _VERSION_ARGUMENTS: + print(f"raes {_distribution_version()}") + return + + from raes_cli.main import app + + app() diff --git a/implementations/python/packages/raes_cli/main.py b/implementations/python/packages/raes_cli/main.py index 8ccf35658..bec366cac 100644 --- a/implementations/python/packages/raes_cli/main.py +++ b/implementations/python/packages/raes_cli/main.py @@ -1,10 +1,9 @@ -"""Main entry point for the RAES CLI.""" - -from importlib.metadata import PackageNotFoundError, version +"""Typer application and command registry for the RAES CLI.""" import typer from raes_cli import conformance, corpus, libvirt, processor, sdl, semantic +from raes_cli.entrypoint import _distribution_version app = typer.Typer( name="raes", @@ -22,13 +21,7 @@ def _version_callback(value: bool) -> None: if value: - try: - current_version = version("raes") - except PackageNotFoundError: - # Honest PEP 440 not-installed sentinel (GOV-901): do not report a - # plausible-looking release when the distribution is absent. - current_version = "0.0.0+unknown" - typer.echo(f"raes {current_version}") + typer.echo(f"raes {_distribution_version()}") raise typer.Exit() diff --git a/implementations/python/pyproject.toml b/implementations/python/pyproject.toml index 4616679d9..1bc11ecfa 100644 --- a/implementations/python/pyproject.toml +++ b/implementations/python/pyproject.toml @@ -59,7 +59,7 @@ docs = [ ] [project.scripts] -raes = "raes_cli.main:app" +raes = "raes_cli.entrypoint:main" raes-mcp = "raes_mcp.server:main" # Version is sourced from the dedicated RAES package file below. Release Please diff --git a/implementations/python/tests/test_corpus_packaging.py b/implementations/python/tests/test_corpus_packaging.py index a44073e65..2b5ccca32 100644 --- a/implementations/python/tests/test_corpus_packaging.py +++ b/implementations/python/tests/test_corpus_packaging.py @@ -123,6 +123,42 @@ def test_installed_wheel_hard_cuts_sdl_import_namespace(installed_python: Path, assert result.returncode == 0, f"canonical namespace check failed:\n{result.stdout}\n{result.stderr}" +@requires_uv +def test_installed_wheel_version_entrypoint_stays_lazy(installed_python: Path, tmp_path: Path): + """The generated console script must retain the exact-version import boundary.""" + + environment = _sanitized_runtime_env(tmp_path) + raes = installed_python.parent / ("raes.exe" if sys.platform == "win32" else "raes") + expected = _run( + [ + str(installed_python), + "-c", + "from importlib.metadata import version; print(f\"raes {version('raes')}\")", + ], + cwd=tmp_path, + env=environment, + ) + result = _run([str(raes), "--version"], cwd=tmp_path, env=environment) + + assert expected.returncode == 0, expected.stderr + assert result.returncode == 0, result.stderr + assert result.stdout == expected.stdout + + inspection = """ +import sys +from importlib.metadata import entry_points + +entrypoint = next(item for item in entry_points(group="console_scripts") if item.name == "raes") +assert entrypoint.value == "raes_cli.entrypoint:main", entrypoint.value +sys.argv = ["raes", "-V"] +entrypoint.load()() +assert "raes_cli.main" not in sys.modules +assert not any(name == "raes_contracts" or name.startswith("raes_contracts.") for name in sys.modules) +""" + inspected = _run([str(installed_python), "-c", inspection], cwd=tmp_path, env=environment) + assert inspected.returncode == 0, f"installed entry-point check failed:\n{inspected.stdout}\n{inspected.stderr}" + + @requires_uv def test_corpus_discoverable_via_importlib_resources_from_installed_wheel(installed_python: Path, tmp_path: Path): """Acceptance: the corpus is discoverable via ``importlib.resources`` from diff --git a/implementations/python/tests/test_issue_1111_cli_version_startup.py b/implementations/python/tests/test_issue_1111_cli_version_startup.py new file mode 100644 index 000000000..c85c8e015 --- /dev/null +++ b/implementations/python/tests/test_issue_1111_cli_version_startup.py @@ -0,0 +1,141 @@ +"""GOV-901 regression guards for issue #1111's exact-version fast path.""" + +from __future__ import annotations + +import json +import os +import statistics +import subprocess +import sys +import types +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path +from unittest.mock import Mock + +import pytest +import raes_cli.entrypoint as cli_entrypoint + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +PACKAGES_ROOT = PROJECT_ROOT / "packages" +_FAST_PATH_CPU_BUDGET_SECONDS = 0.25 +_PROBE_SAMPLES = 5 + + +@pytest.mark.parametrize("flag", ["--version", "-V"]) +def test_exact_version_arguments_bypass_typer_command_imports( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + flag: str, +) -> None: + monkeypatch.delitem(sys.modules, "raes_cli.main", raising=False) + monkeypatch.setattr(sys, "argv", ["raes", flag]) + + cli_entrypoint.main() + + assert capsys.readouterr().out == f"raes {version('raes')}\n" + assert "raes_cli.main" not in sys.modules + + +def test_exact_version_fallback_is_honest_sentinel( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + def _raise(_distribution: str) -> str: + raise PackageNotFoundError + + monkeypatch.setattr(cli_entrypoint, "version", _raise) + monkeypatch.setattr(sys, "argv", ["raes", "--version"]) + + cli_entrypoint.main() + + assert capsys.readouterr().out == "raes 0.0.0+unknown\n" + + +@pytest.mark.parametrize( + "arguments", + [ + [], + ["--help"], + ["processor", "--help"], + ["--version", "unexpected"], + ["-V", "unexpected"], + ["--unknown"], + ], +) +def test_every_non_exact_argument_shape_delegates_without_rewriting_argv( + monkeypatch: pytest.MonkeyPatch, + arguments: list[str], +) -> None: + delegated = Mock() + fake_main = types.ModuleType("raes_cli.main") + fake_main.__dict__["app"] = delegated + original_argv = ["raes", *arguments] + monkeypatch.setitem(sys.modules, "raes_cli.main", fake_main) + monkeypatch.setattr(sys, "argv", original_argv) + + cli_entrypoint.main() + + delegated.assert_called_once_with() + assert sys.argv is original_argv + + +def _probe_environment() -> dict[str, str]: + current_pythonpath = os.environ.get("PYTHONPATH") + pythonpath = str(PACKAGES_ROOT) + if current_pythonpath: + pythonpath = os.pathsep.join((pythonpath, current_pythonpath)) + return {**os.environ, "PYTHONHASHSEED": "0", "PYTHONPATH": pythonpath} + + +def _run_cpu_probe(script: str) -> dict[str, object]: + result = subprocess.run( + [sys.executable, "-c", script], + cwd=PROJECT_ROOT, + env=_probe_environment(), + text=True, + capture_output=True, + check=True, + timeout=30, + ) + return json.loads(result.stderr.strip().splitlines()[-1]) + + +@pytest.mark.integration +def test_source_exact_version_startup_stays_within_relative_and_absolute_budget() -> None: + fast_script = """ +import json +import sys +import time + +started = time.process_time() +from raes_cli.entrypoint import main +sys.argv = ["raes", "--version"] +main() +print(json.dumps({ + "cpu_seconds": time.process_time() - started, + "main_loaded": "raes_cli.main" in sys.modules, + "heavy_loaded": any( + name == prefix or name.startswith(prefix + ".") + for name in sys.modules + for prefix in ("raes_conformance", "raes_contracts", "raes_processor", "z3") + ), +}), file=sys.stderr) +""" + full_graph_script = """ +import json +import time + +started = time.process_time() +import raes_cli.main +print(json.dumps({"cpu_seconds": time.process_time() - started}), file=__import__("sys").stderr) +""" + + fast_results = [_run_cpu_probe(fast_script) for _ in range(_PROBE_SAMPLES)] + full_graph_results = [_run_cpu_probe(full_graph_script) for _ in range(_PROBE_SAMPLES)] + fast_median = statistics.median(float(result["cpu_seconds"]) for result in fast_results) + full_graph_median = statistics.median(float(result["cpu_seconds"]) for result in full_graph_results) + + assert all(result["main_loaded"] is False for result in fast_results) + assert all(result["heavy_loaded"] is False for result in fast_results) + assert fast_median < _FAST_PATH_CPU_BUDGET_SECONDS + assert fast_median < full_graph_median / 2 diff --git a/implementations/python/tests/test_version_classification.py b/implementations/python/tests/test_version_classification.py index 645c3d899..f26d8a3c0 100644 --- a/implementations/python/tests/test_version_classification.py +++ b/implementations/python/tests/test_version_classification.py @@ -43,23 +43,30 @@ def test_cli_help_uses_raes_project_identity() -> None: assert "RAES" in result.stdout +def test_cli_version_callback_is_noop_when_not_requested() -> None: + from raes_cli.main import _version_callback + + assert _version_callback(False) is None + + def test_console_scripts_hard_cut_to_raes_names() -> None: scripts = { entry_point.name for entry_point in entry_points(group="console_scripts") - if entry_point.value in {"raes_cli.main:app", "raes_mcp.server:main"} + if entry_point.value in {"raes_cli.entrypoint:main", "raes_mcp.server:main"} } assert {"raes", "raes-mcp"} <= scripts def test_cli_version_fallback_is_honest_sentinel(monkeypatch) -> None: + import raes_cli.entrypoint as cli_entrypoint import raes_cli.main as cli_main def _raise(_distribution: str) -> str: raise PackageNotFoundError - monkeypatch.setattr(cli_main, "version", _raise) + monkeypatch.setattr(cli_entrypoint, "version", _raise) result = CliRunner().invoke(cli_main.app, ["--version"]) assert result.exit_code == 0