from __future__ import annotations import argparse import json import sys from datetime import UTC, datetime from hashlib import sha256 from pathlib import Path from uuid import uuid4 ROOT = Path(__file__).resolve().parents[1] BACKEND_ROOT = ROOT / "backend" if str(BACKEND_ROOT) not in sys.path: sys.path.insert(0, str(BACKEND_ROOT)) from app.models import Dataset, Metric, QualityCheck, SourceRegistry, SourceSnapshot # noqa: E402 from app.services.qa_service import QaService # noqa: E402 from app.services.quality_service import QualityService # noqa: E402 from app.services.source_registry_service import SourceRegistryService # noqa: E402 class BenchmarkSession: def __init__(self, datasets: list[Dataset]) -> None: self.datasets = {dataset.id: dataset for dataset in datasets} self.added: list[object] = [] self.commits = 0 self.refreshes: list[object] = [] def get(self, model, item_id): if model.__name__ == "Dataset": return self.datasets.get(item_id) if model.__name__ == "Area": return None return None def add(self, item) -> None: self.added.append(item) def commit(self) -> None: self.commits += 1 def refresh(self, item) -> None: self.refreshes.append(item) def _load_manifest() -> dict: manifest_path = ROOT / "fixtures" / "golden" / "golden_qa_benchmarks.json" if manifest_path.exists(): manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if not isinstance(manifest.get("scenarios"), list) or not manifest["scenarios"]: raise AssertionError("golden_qa_benchmarks.json must define at least one scenario") return manifest # Backwards-compatible fallback for older checkouts and local smoke scripts. expected = json.loads((ROOT / "fixtures" / "golden" / "expected_qa_metrics.json").read_text(encoding="utf-8")) return {"version": 0, "scenarios": [expected]} def _dataset(dataset_id, project_id, name: str, path: Path, *, role: str) -> Dataset: # The benchmark is still synthetic evidence, but it intentionally models # the same complete Phase 2 provenance binding required at the QA service # boundary. The candidate is a derived fixture; the reference simulates # a governed GRB building snapshot. No legacy fixture exception is used. source_key = "grb" if role == "reference" else "derived" source = SourceRegistry( id=uuid4(), **SourceRegistryService.definition_for(source_key).as_model_values(), ) checksum = sha256(path.read_bytes()).hexdigest() snapshot = SourceSnapshot( id=uuid4(), source_registry_id=source.id, snapshot_key=f"golden-qa:{source_key}:{checksum}", checksum_sha256=checksum, fetched_at=datetime.now(UTC), crs="EPSG:4326", units=source.default_units, spatial_resolution_json={"status": "fixture"}, temporal_coverage_json={"status": "fixture"}, geographic_coverage_json={"scope": "golden-qa-fixture"}, observed_schema_json={"dataset_type": "vector", "fixture_mode": True}, freshness_status="current", ingest_status="ingested", known_limitations_json=["Synthetic golden benchmark fixture; not production evidence."], snapshot_metadata_json={"fixture_mode": True, "benchmark": "golden-qa"}, ) return Dataset( id=dataset_id, project_id=project_id, name=name, dataset_type="vector", source="governed golden benchmark fixture", dataset_role="reference" if role == "reference" else "derived", source_name=source.source_key, reference_layer_name="buildings" if role == "reference" else None, storage_path=str(path), crs="EPSG:4326", checksum_sha256=checksum, source_registry_id=source.id, source_snapshot_id=snapshot.id, source_registry=source, source_snapshot=snapshot, data_contract_key="geointel.vector.geojson", data_contract_version="1.0.0", validation_status="passed", provenance_status="complete", lineage_status="not_applicable", quarantine_status="not_quarantined", metadata_json={"crs_assumed": False, "fixture_mode": True}, source_metadata={"fixture_mode": True, "source_registry_key": source.source_key}, provenance_metadata={"fixture_mode": True, "source_snapshot_id": str(snapshot.id)}, status="ready", ) def _assert_close(label: str, actual: float | int | None, expected: float | int | None, tolerance: float) -> None: if expected is None: if actual is not None: raise AssertionError(f"{label} drifted: actual={actual}, expected=None") return if actual is None: raise AssertionError(f"{label} is None, expected {expected}") if abs(float(actual) - float(expected)) > tolerance: raise AssertionError(f"{label} drifted: actual={actual}, expected={expected}, tolerance={tolerance}") def _run_scenario(expected: dict, session: BenchmarkSession, project_id) -> dict: added_before = len(session.added) commits_before = session.commits candidate_dataset_id = uuid4() reference_dataset_id = uuid4() candidate_path = ROOT / expected["candidate_fixture"] reference_path = ROOT / expected["reference_fixture"] tolerance = float(expected["tolerance"]) session.datasets[candidate_dataset_id] = _dataset( candidate_dataset_id, project_id, Path(expected["candidate_fixture"]).name, candidate_path, role="source", ) session.datasets[reference_dataset_id] = _dataset( reference_dataset_id, project_id, Path(expected["reference_fixture"]).name, reference_path, role="reference", ) result = QaService.compare_candidate_with_reference( db=session, project_id=project_id, candidate_dataset_id=candidate_dataset_id, reference_dataset_id=reference_dataset_id, iou_threshold=float(expected["iou_threshold"]), ) metrics = { "precision": result.precision, "recall": result.recall, "f1": result.f1_score, "mean_iou": result.mean_iou, "false_positive_count": result.false_positives, "false_negative_count": result.false_negatives, } _assert_close("candidate_feature_count", result.candidate_feature_count, expected["candidate_feature_count"], 0) _assert_close("reference_feature_count", result.reference_feature_count, expected["reference_feature_count"], 0) _assert_close("matches", result.matches, expected["matches"], 0) _assert_close("false_positive_count", result.false_positives, expected["false_positive_count"], 0) _assert_close("false_negative_count", result.false_negatives, expected["false_negative_count"], 0) _assert_close("precision", result.precision, expected["precision"], tolerance) _assert_close("recall", result.recall, expected["recall"], tolerance) _assert_close("f1", result.f1_score, expected["f1"], tolerance) _assert_close("mean_iou", result.mean_iou, expected["mean_iou"], tolerance) quality_check = QualityService.persist_quality_check( db=session, project_id=project_id, reference_dataset_id=reference_dataset_id, check_type="golden_candidate_vs_reference", status=result.status, score=result.f1_score, parameters={"iou_threshold": result.iou_threshold, "benchmark_id": expected["benchmark_id"]}, findings={ "matches": result.matches, "false_positives": result.false_positives, "false_negatives": result.false_negatives, }, candidate_dataset_id=candidate_dataset_id, metrics=metrics, ) scenario_added = session.added[added_before:] persisted_metrics = [item for item in scenario_added if isinstance(item, Metric)] return { "status": "passed", "benchmark_id": expected["benchmark_id"], "description": expected.get("description"), "metrics": metrics, "result_counts": { "candidate_feature_count": result.candidate_feature_count, "reference_feature_count": result.reference_feature_count, "matches": result.matches, }, "quality_check_id": str(quality_check.id), "persistence": { "quality_check_count": len([item for item in scenario_added if isinstance(item, QualityCheck)]), "metric_count": len(persisted_metrics), "metric_keys": [metric.metric_key for metric in persisted_metrics], "commit_count": session.commits - commits_before, }, "fixtures": { "candidate": expected["candidate_fixture"], "reference": expected["reference_fixture"], }, } def run_benchmark() -> dict: manifest = _load_manifest() project_id = uuid4() session = BenchmarkSession([]) scenarios = [_run_scenario(expected, session, project_id) for expected in manifest["scenarios"]] persisted_metrics = [item for item in session.added if isinstance(item, Metric)] aggregate_metric_keys = sorted({metric.metric_key for metric in persisted_metrics}) first = scenarios[0] return { "status": "passed", "version": manifest.get("version"), "scenario_count": len(scenarios), "scenarios": scenarios, "persistence": { "quality_check_count": len([item for item in session.added if isinstance(item, QualityCheck)]), "metric_count": len(persisted_metrics), "metric_keys": aggregate_metric_keys, "commit_count": session.commits, }, # Compatibility fields for scripts that still read the original single-scenario shape. "benchmark_id": first["benchmark_id"], "metrics": first["metrics"], "fixtures": first["fixtures"], } def main() -> int: parser = argparse.ArgumentParser(description="Run GeoIntel golden QA/QC benchmark.") parser.add_argument("--json", action="store_true", help="Print machine-readable JSON only.") args = parser.parse_args() try: payload = run_benchmark() except Exception as exc: if args.json: print(json.dumps({"status": "failed", "error": str(exc)}, indent=2)) else: print(f"Golden QA/QC benchmark failed: {exc}", file=sys.stderr) return 1 if args.json: print(json.dumps(payload, indent=2, sort_keys=True)) else: print("GeoIntel golden QA/QC benchmark passed") print(json.dumps(payload, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())