wip: local BUG-139/BUG-140 fixes before syncing origin/main
- pre_work_check.py: prefer repo .venv Python >=3.11, strict JYOTISH_PRE_WORK_PYTHON override, fail closed - tests/test_pre_work_check.py: regression coverage for venv preference / system 3.9 / strict override - globals.css: transient :active pressed feedback for entrypoint cards, hover only under (hover: hover) - consultation-entrypoint.test.ts: sticky-hover regression test - BUG_HISTORY.md: BUG-139 / BUG-140 records - pre_work_error_ledger.md: ERR-078 resolved entry
This commit is contained in:
+122
-7
@@ -11,15 +11,17 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PYTHON = sys.executable
|
||||
MINIMUM_PYTHON_VERSION = (3, 11)
|
||||
PYTHON_OVERRIDE_ENV = "JYOTISH_PRE_WORK_PYTHON"
|
||||
DEFAULT_COMMAND_TIMEOUT_SECONDS = 45
|
||||
DEFAULT_FRAGMENT_TIMEOUT_SECONDS = 90
|
||||
FOCUSED_TEST_TARGETS = [
|
||||
@@ -39,6 +41,97 @@ PRE_WORK_DOCS = [
|
||||
]
|
||||
|
||||
|
||||
def python_candidates(
|
||||
*,
|
||||
root: Path = ROOT,
|
||||
environ: dict[str, str] | None = None,
|
||||
current_executable: str | None = None,
|
||||
) -> list[str]:
|
||||
"""Return Python candidates in deterministic preference order.
|
||||
|
||||
An explicit override is strict: a broken override must fail closed rather
|
||||
than silently run the checks under a different environment.
|
||||
"""
|
||||
env = os.environ if environ is None else environ
|
||||
override = env.get(PYTHON_OVERRIDE_ENV, "").strip()
|
||||
if override:
|
||||
return [override]
|
||||
|
||||
candidates = [
|
||||
str(root / ".venv" / "bin" / "python"),
|
||||
str(root / ".venv" / "Scripts" / "python.exe"),
|
||||
current_executable or sys.executable,
|
||||
]
|
||||
candidates.extend(path for name in ("python3.13", "python3.12", "python3.11") if (path := shutil.which(name)))
|
||||
return list(dict.fromkeys(candidate for candidate in candidates if candidate))
|
||||
|
||||
|
||||
def probe_python(candidate: str) -> dict[str, Any]:
|
||||
probe = (
|
||||
"import importlib.util,json,sys;"
|
||||
"print(json.dumps({'version':list(sys.version_info[:3]),"
|
||||
"'pytest_available':importlib.util.find_spec('pytest') is not None}))"
|
||||
)
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[candidate, "-c", probe],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
return {"candidate": candidate, "ok": False, "error": str(exc)}
|
||||
|
||||
if completed.returncode != 0:
|
||||
return {
|
||||
"candidate": candidate,
|
||||
"ok": False,
|
||||
"error": (completed.stderr or completed.stdout).strip() or f"probe exited {completed.returncode}",
|
||||
}
|
||||
try:
|
||||
metadata = json.loads(completed.stdout)
|
||||
version = tuple(int(part) for part in metadata["version"])
|
||||
pytest_available = metadata["pytest_available"] is True
|
||||
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
|
||||
return {"candidate": candidate, "ok": False, "error": f"invalid probe output: {exc}"}
|
||||
|
||||
requirements = []
|
||||
if version < MINIMUM_PYTHON_VERSION:
|
||||
requirements.append(f"Python {version[0]}.{version[1]} is below required 3.11")
|
||||
if not pytest_available:
|
||||
requirements.append("pytest is not installed")
|
||||
return {
|
||||
"candidate": candidate,
|
||||
"ok": not requirements,
|
||||
"version": ".".join(str(part) for part in version),
|
||||
"pytest_available": pytest_available,
|
||||
"error": "; ".join(requirements),
|
||||
}
|
||||
|
||||
|
||||
def select_python(
|
||||
candidates: list[str] | None = None,
|
||||
probe: Callable[[str], dict[str, Any]] = probe_python,
|
||||
) -> dict[str, Any]:
|
||||
attempts = []
|
||||
for candidate in candidates or python_candidates():
|
||||
result = probe(candidate)
|
||||
attempts.append(result)
|
||||
if result.get("ok"):
|
||||
return {"ok": True, "executable": candidate, "attempts": attempts, **result}
|
||||
return {
|
||||
"ok": False,
|
||||
"executable": "",
|
||||
"attempts": attempts,
|
||||
"error": (
|
||||
"No usable project Python found. Create .venv with Python >=3.11 and install requirements-dev.txt, "
|
||||
f"or set {PYTHON_OVERRIDE_ENV} to a compatible interpreter."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def run(args: list[str], timeout: int, env: dict[str, str] | None = None) -> dict[str, Any]:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
@@ -73,8 +166,9 @@ def classify_status(
|
||||
pytest_ok: bool,
|
||||
remote_status: str,
|
||||
external_engine_ok: bool = True,
|
||||
python_ok: bool = True,
|
||||
) -> str:
|
||||
if not docs_ok or not fragment_ok or not pytest_ok or not external_engine_ok:
|
||||
if not docs_ok or not fragment_ok or not pytest_ok or not external_engine_ok or not python_ok:
|
||||
return "fail"
|
||||
if remote_status == "verified":
|
||||
return "pass"
|
||||
@@ -85,9 +179,23 @@ def build_report(remote_timeout: int, command_timeout: int, fragment_timeout: in
|
||||
docs = {path: (ROOT / path).exists() for path in PRE_WORK_DOCS}
|
||||
git_status = run(["git", "status", "--short", "--branch"], command_timeout)
|
||||
git_remote = run(["git", "remote", "-v"], command_timeout)
|
||||
fragment = run([PYTHON, "scripts/preflight_fragment_scan.py"], fragment_timeout)
|
||||
external_engine = run([PYTHON, EXTERNAL_ENGINE_DIAGNOSTIC_TARGET, "--json"], command_timeout)
|
||||
remote = run([PYTHON, "scripts/remote_repo_visibility_check.py", "--timeout", str(remote_timeout)], command_timeout)
|
||||
python_runtime = select_python()
|
||||
python = str(python_runtime.get("executable") or "")
|
||||
if python_runtime["ok"]:
|
||||
fragment = run([python, "scripts/preflight_fragment_scan.py"], fragment_timeout)
|
||||
external_engine = run([python, EXTERNAL_ENGINE_DIAGNOSTIC_TARGET, "--json"], command_timeout)
|
||||
remote = run([python, "scripts/remote_repo_visibility_check.py", "--timeout", str(remote_timeout)], command_timeout)
|
||||
else:
|
||||
unavailable = {
|
||||
"ok": False,
|
||||
"returncode": None,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"error": python_runtime["error"],
|
||||
}
|
||||
fragment = dict(unavailable)
|
||||
external_engine = dict(unavailable)
|
||||
remote = dict(unavailable)
|
||||
remote_report: dict[str, Any] = {}
|
||||
if remote["ok"]:
|
||||
try:
|
||||
@@ -101,7 +209,10 @@ def build_report(remote_timeout: int, command_timeout: int, fragment_timeout: in
|
||||
cache = Path(tempfile.gettempdir()) / "jyotish_preflight_fragment_scan_report.json"
|
||||
cache.write_text(fragment["stdout"], encoding="utf-8")
|
||||
env["PREFLIGHT_FRAGMENT_SCAN_REPORT"] = str(cache)
|
||||
pytest_result = run([PYTHON, "-m", "pytest", "-q", *FOCUSED_TEST_TARGETS], command_timeout, env=env)
|
||||
if python_runtime["ok"]:
|
||||
pytest_result = run([python, "-m", "pytest", "-q", *FOCUSED_TEST_TARGETS], command_timeout, env=env)
|
||||
else:
|
||||
pytest_result = {"ok": False, "stdout": "", "stderr": "", "error": python_runtime["error"]}
|
||||
remote_status = str(remote_report.get("status") or "blocked")
|
||||
status = classify_status(
|
||||
docs_ok=all(docs.values()),
|
||||
@@ -109,11 +220,13 @@ def build_report(remote_timeout: int, command_timeout: int, fragment_timeout: in
|
||||
pytest_ok=pytest_result["ok"],
|
||||
remote_status=remote_status,
|
||||
external_engine_ok=external_engine["ok"],
|
||||
python_ok=python_runtime["ok"],
|
||||
)
|
||||
return {
|
||||
"scope": "pre_work_check",
|
||||
"status": status,
|
||||
"must_not_claim_synced": remote_report.get("must_not_claim_synced", True),
|
||||
"python": python_runtime,
|
||||
"docs": docs,
|
||||
"git": {
|
||||
"status_ok": git_status["ok"],
|
||||
@@ -122,6 +235,7 @@ def build_report(remote_timeout: int, command_timeout: int, fragment_timeout: in
|
||||
"remote": git_remote["stdout"],
|
||||
},
|
||||
"checks": {
|
||||
"python_runtime_ok": python_runtime["ok"],
|
||||
"fragment_scan_ok": fragment["ok"],
|
||||
"external_engine_adapters_ok": external_engine["ok"],
|
||||
"remote_visibility_status": remote_status,
|
||||
@@ -129,6 +243,7 @@ def build_report(remote_timeout: int, command_timeout: int, fragment_timeout: in
|
||||
"focused_tests_ok": pytest_result["ok"],
|
||||
},
|
||||
"errors": {
|
||||
"python_runtime": python_runtime.get("error", ""),
|
||||
"fragment_scan": fragment["error"],
|
||||
"external_engine_adapters": external_engine["error"],
|
||||
"remote_visibility": remote["error"],
|
||||
|
||||
Reference in New Issue
Block a user