Files
srbhr--resume-matcher/apps/backend/e2e_monitor/__main__.py
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

248 lines
11 KiB
Python

"""CLI: ``uv run python -m e2e_monitor <move> [args]`` (run from apps/backend).
Every move calls ``ensure_enabled()`` first. ``sweep`` pre-seeds the isolated DB
with a known master, boots the servers, tailors N variations, judges + (when the
frontend is up) renders each, then writes the bundle (flow-trace, summary, and a
baseline-diff when a committed baseline exists).
"""
from __future__ import annotations
import argparse
import asyncio
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from e2e_monitor.baseline import diff_against_baseline, summary_to_baseline
from e2e_monitor.bundle import Bundle
from e2e_monitor.collect import build_flow_trace, build_summary
from e2e_monitor.flow import seed_master_db, tailor
from e2e_monitor.gate import MonitorDisabled, ensure_enabled
from e2e_monitor.judge import judge_variation
from e2e_monitor.manifest import build_manifest
from e2e_monitor.render import render_variation
from e2e_monitor.servers import Servers
_BACKEND = Path(__file__).resolve().parents[1]
_PKG = Path(__file__).resolve().parent
_ARTIFACTS = _BACKEND.parents[1] / "artifacts" / "e2e-monitor"
_FIXTURES = _PKG / "fixtures"
_BASELINE = _PKG / "baseline" / "baseline.json"
_STOPWORDS = frozenset({
"we", "you", "our", "your", "the", "a", "an", "and", "or", "for", "with",
"to", "of", "in", "on", "is", "are", "as", "at", "be", "by", "this", "that",
})
def _git_sha() -> str:
try:
return subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"], cwd=_BACKEND, text=True
).strip()
except Exception:
return "unknown"
def _now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _run_id() -> str:
return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
def _jds() -> list[tuple[str, str]]:
return sorted(
(p.stem, p.read_text(encoding="utf-8")) for p in (_FIXTURES / "jds").glob("*.txt")
)
def _say(msg: str) -> None:
"""Print live loop narration to stderr.
Progress/handoff text goes to stderr so the machine-readable ``bundle: <path>``
line stays alone on stdout for scripts/agents that parse it.
"""
print(msg, file=sys.stderr, flush=True)
def cmd_sweep(_: argparse.Namespace) -> int:
ensure_enabled()
from app.config import load_config_file
bundle = Bundle(root=_ARTIFACTS, run_id=_run_id())
bundle.ensure()
config = load_config_file()
bundle.write_json(
bundle.dir / "manifest.json",
build_manifest(run_id=bundle.run_id, git_sha=_git_sha(), config=config, started_at=_now_iso()),
)
_say("")
_say(" e2e-monitor · driving the real app end to end")
_say(f" provider {config.get('provider', '?')}/{config.get('model', '?')} · run {bundle.run_id}")
_say(" Captures an evidence bundle for an AI agent to JUDGE — built to run (or via")
_say(" the /monitor-e2e skill) while you work on the app as normal.")
_say("")
# Pre-seed the isolated DB with a known master BEFORE booting the server.
# Canonicalize to the exact ResumeData round-trip the app stores, so every
# optional field is present. Otherwise improve/preview hashes the raw
# (field-missing) dict while improve/confirm hashes the schema-defaulted
# round-trip — a mismatch the app rejects with 400 (its preview/confirm
# hash gate). See _hash_improved_data in app/routers/resumes.py.
from app.schemas import ResumeData
raw_master = json.loads((_FIXTURES / "master.json").read_text(encoding="utf-8"))
master = ResumeData.model_validate(raw_master).model_dump()
resume_id = seed_master_db(bundle.data_dir, master)
bundle.write_json(bundle.master_dir / "processed_data.json", master)
_say(" ✓ seed-master canonical master → isolated DB (your real DB is untouched)")
steps: list[dict[str, Any]] = []
# seed-master ran above (BEFORE boot) — record it first so flow-trace.json
# ordering matches actual execution.
steps.append({"stage": "seed-master", "ok": True, "ms": 0, "detail": {"resume_id": resume_id}})
servers = Servers(bundle=bundle)
variations: list[dict[str, Any]] = []
try:
_say(" ▶ boot spawning backend :8000 + frontend :3000 …")
boot = servers.boot()
steps.append({"stage": "boot", "ok": True, "ms": 0, "detail": boot})
_say(" ✓ boot backend up" + (" + frontend up" if boot.get("frontend_up") else " (frontend off — renders degrade to header+size)"))
for jd_key, jd_text in _jds():
vdir = bundle.variation_dir(jd_key)
(vdir / "job_description.txt").write_text(jd_text, encoding="utf-8")
keywords = [
kw for kw in (w.strip(":,.();") for w in jd_text.split())
if kw.istitle() and kw.lower() not in _STOPWORDS
][:8]
_say(f" ▶ {jd_key:<16} tailor → judge → render …")
try:
t = tailor(resume_id, jd_text, keywords, master)
except Exception as exc: # noqa: BLE001
steps.append({"stage": f"tailor:{jd_key}", "ok": False, "ms": 0, "error": str(exc)})
_say(f" ✗ {jd_key:<16} tailor FAILED: {str(exc)[:90]}")
continue
bundle.write_json(vdir / "tailored.json", t["tailored"])
bundle.write_json(vdir / "scores.json", t["scores"])
steps.append({"stage": f"tailor:{jd_key}", "ok": True, "ms": 0})
try:
judge = asyncio.run(judge_variation(jd_text, t["tailored"]))
except Exception as exc: # noqa: BLE001
judge = {"score": None, "reasons": f"judge failed: {exc}"}
steps.append({"stage": f"judge:{jd_key}", "ok": False, "ms": 0, "error": str(exc)})
bundle.write_json(vdir / "judge.json", judge)
render: dict[str, Any] = {"non_blank": None}
render_status = "skipped" # frontend down or no tailored id
if servers.frontend_up and t["tailored_resume_id"]:
try:
pdf, render = render_variation(t["tailored_resume_id"])
(vdir / "resume.pdf").write_bytes(pdf)
bundle.write_json(vdir / "render.json", render)
steps.append({"stage": f"render:{jd_key}", "ok": bool(render["non_blank"]), "ms": 0})
render_status = "non-blank" if render["non_blank"] else "BLANK!"
except Exception as exc: # noqa: BLE001
steps.append({"stage": f"render:{jd_key}", "ok": False, "ms": 0, "error": str(exc)})
render_status = "FAILED"
variations.append({"jd_key": jd_key, "scores": t["scores"], "judge": judge, "render": render})
# ✓ only when the judge produced a score AND the render didn't fail/blank;
# otherwise ⚠ — the marker must never claim success over a caught failure.
variation_ok = (judge or {}).get("score") is not None and render_status in ("non-blank", "skipped")
_say(
f" {'✓' if variation_ok else '⚠'} {jd_key:<16} "
f"judge={(judge or {}).get('score')} "
f"kw={t['scores']['jd_keyword_coverage']} "
f"render={render_status} "
f"fabricated={len(t['scores']['fabricated_employers'])}"
)
finally:
servers.teardown()
flow = build_flow_trace(steps)
bundle.write_json(bundle.dir / "flow-trace.json", flow)
summary = build_summary(flow=flow, variations=variations, provider=config.get("provider", ""))
bundle.write_json(bundle.dir / "summary.json", summary)
baseline_line = ""
if _BASELINE.exists():
current = {
v["jd_key"]: {
"jd_keyword_coverage": v["scores"]["jd_keyword_coverage"],
"judge_score": (v.get("judge") or {}).get("score"),
"non_blank": (v.get("render") or {}).get("non_blank"),
}
for v in variations
}
diff = diff_against_baseline(current, bundle.read_json(_BASELINE))
bundle.write_json(bundle.dir / "baseline-diff.json", diff)
baseline_line = (
" · no regression vs baseline"
if not diff["regressed"]
else f" · REGRESSED ({len(diff['regressions'])} — see baseline-diff.json)"
)
_say("")
_say(" ──────────────────────────────────────────────────────────────")
_say(
f" sweep complete · {summary['variations']} variations · "
f"flow {'all passed' if summary['flow_all_passed'] else 'HAD FAILURES'} · "
f"renders {summary['renders_non_blank']}/{summary['variations']} non-blank"
+ baseline_line
)
_say("")
_say(" ↳ NEXT — this is captured EVIDENCE, not a verdict. Hand it to an AI agent:")
_say(" • In Claude Code, invoke the /monitor-e2e skill, or just say")
_say(' "judge the latest e2e-monitor bundle".')
_say(" The agent reads the logs + artifacts, separates real issues from noise,")
_say(" and writes report.md. This harness is built to be DRIVEN BY an AI agent")
_say(" debugging in the background while you build the app as normal.")
_say("")
print(f"bundle: {bundle.dir}")
return 0
def cmd_update_baseline(args: argparse.Namespace) -> int:
ensure_enabled(require_key=False)
run_dir = Path(args.run_dir)
variations: list[dict[str, Any]] = []
for vdir in sorted((run_dir / "variations").glob("*")):
variations.append({
"jd_key": vdir.name,
"scores": Bundle.read_json(vdir / "scores.json"),
"judge": Bundle.read_json(vdir / "judge.json") if (vdir / "judge.json").exists() else {},
"render": Bundle.read_json(vdir / "render.json") if (vdir / "render.json").exists() else {},
})
_BASELINE.parent.mkdir(parents=True, exist_ok=True)
Bundle.write_json(_BASELINE, summary_to_baseline(variations))
print(f"baseline updated from {run_dir} -> {_BASELINE} (review + commit it)")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="e2e_monitor")
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("sweep").set_defaults(func=cmd_sweep)
ub = sub.add_parser("update-baseline")
ub.add_argument("run_dir")
ub.set_defaults(func=cmd_update_baseline)
args = parser.parse_args(argv)
try:
return args.func(args)
except MonitorDisabled as exc:
print(f"e2e-monitor: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())