165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
"""Validate the local interpreter and foundation-gate assumptions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Literal, TypedDict
|
|
|
|
import tomli
|
|
|
|
# IMPORTANT: keep this adjacent import; direct script entrypoints put scripts/ on sys.path.
|
|
from environment_diagnostics import ( # type: ignore[import-not-found]
|
|
EnvironmentProbeConfig,
|
|
collect_environment_observation,
|
|
evaluate_environment,
|
|
)
|
|
|
|
|
|
class PreflightReport(TypedDict):
|
|
status: str
|
|
python: str
|
|
executable: str
|
|
environment_prefix: str
|
|
expected_environment: str
|
|
project_local_venv: bool
|
|
node: str
|
|
diagnostics: dict[str, object]
|
|
errors: list[str]
|
|
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _expected_environment(override: Path | None = None) -> Path:
|
|
if override is not None:
|
|
return override.resolve()
|
|
name = ".venv" if os.name == "nt" else ".venv-wsl"
|
|
return (REPOSITORY_ROOT / name).resolve()
|
|
|
|
|
|
def _is_within(path: Path, parent: Path) -> bool:
|
|
try:
|
|
path.relative_to(parent)
|
|
except ValueError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def build_report(
|
|
expected_environment_override: Path | None = None,
|
|
*,
|
|
optional_lane: Literal["none", "plot", "reference"] = "none",
|
|
) -> PreflightReport:
|
|
expected_environment = _expected_environment(expected_environment_override)
|
|
executable = Path(sys.executable).absolute()
|
|
environment_prefix = Path(sys.prefix).resolve()
|
|
local_environment = environment_prefix == expected_environment or _is_within(
|
|
environment_prefix, expected_environment
|
|
)
|
|
|
|
with (REPOSITORY_ROOT / "pyproject.toml").open("rb") as stream:
|
|
metadata = tomli.load(stream)
|
|
|
|
errors: list[str] = []
|
|
if sys.version_info < (3, 10): # noqa: UP036 - preflight must fail before package import
|
|
errors.append("Python 3.10 or newer is required.")
|
|
platform = "windows" if os.name == "nt" else "linux"
|
|
suffix = "win" if os.name == "nt" else "linux"
|
|
cache_root = Path(
|
|
os.environ.get(
|
|
"PRE_COMMIT_HOME",
|
|
REPOSITORY_ROOT / ".tmp" / f"pre-commit-{suffix}",
|
|
)
|
|
)
|
|
temp_root = Path(
|
|
os.environ.get(
|
|
"SIGMAX_TEMP_ROOT",
|
|
REPOSITORY_ROOT / ".tmp" / f"runtime-{suffix}",
|
|
)
|
|
)
|
|
diagnostics = evaluate_environment(
|
|
collect_environment_observation(
|
|
EnvironmentProbeConfig(
|
|
repository_root=REPOSITORY_ROOT,
|
|
environment_prefix=environment_prefix,
|
|
expected_environment=expected_environment,
|
|
cache_root=cache_root,
|
|
temp_root=temp_root,
|
|
platform=platform,
|
|
optional_lane=optional_lane,
|
|
path_value=os.environ.get("PATH", ""),
|
|
temp_capture_mitigated=(os.environ.get("SIGMAX_PYTEST_CAPTURE_MODE") == "sys"),
|
|
)
|
|
)
|
|
)
|
|
errors.extend(
|
|
f"[{issue.code}] {issue.summary} Remediation: {issue.remediation}"
|
|
for issue in diagnostics.issues
|
|
)
|
|
if metadata["project"]["requires-python"] != ">=3.10":
|
|
errors.append("pyproject.toml Python floor differs from the preflight policy.")
|
|
if metadata["project"]["dependencies"]:
|
|
errors.append("Mandatory runtime dependencies must remain empty at the foundation gate.")
|
|
|
|
node_status = (
|
|
"NOT_IMPLEMENTED" if (REPOSITORY_ROOT / "package.json").exists() else "NOT_APPLICABLE"
|
|
)
|
|
return {
|
|
"status": "FAIL" if errors else "PASS",
|
|
"python": ".".join(str(part) for part in sys.version_info[:3]),
|
|
"executable": str(executable),
|
|
"environment_prefix": str(environment_prefix),
|
|
"expected_environment": str(expected_environment),
|
|
"project_local_venv": local_environment,
|
|
"node": node_status,
|
|
"diagnostics": diagnostics.projection(),
|
|
"errors": errors,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--json", action="store_true", help="Emit machine-readable output.")
|
|
parser.add_argument(
|
|
"--expected-environment",
|
|
type=Path,
|
|
help="Override the expected environment path for contract testing.",
|
|
)
|
|
parser.add_argument(
|
|
"--optional-lane",
|
|
choices=("none", "plot", "reference"),
|
|
default="none",
|
|
help="Require one fixed optional dependency lane.",
|
|
)
|
|
arguments = parser.parse_args()
|
|
report = build_report(
|
|
arguments.expected_environment,
|
|
optional_lane=arguments.optional_lane,
|
|
)
|
|
|
|
if arguments.json:
|
|
print(json.dumps(report, sort_keys=True))
|
|
else:
|
|
print(f"Preflight: {report['status']}")
|
|
print(f"Python: {report['python']}")
|
|
print(f"Interpreter: {report['executable']}")
|
|
print(f"Environment: {report['environment_prefix']}")
|
|
print(f"Node lane: {report['node']}")
|
|
mitigations = report["diagnostics"].get("mitigations")
|
|
if isinstance(mitigations, list):
|
|
for mitigation in mitigations:
|
|
if isinstance(mitigation, str):
|
|
print(f"Environment mitigation: {mitigation}")
|
|
for error in report["errors"]:
|
|
print(f"ERROR: {error}", file=sys.stderr)
|
|
|
|
return 0 if report["status"] == "PASS" else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|