Harden detection result review
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _polygon(x: float, y: float, size: float) -> dict:
|
||||
return {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[x, y],
|
||||
[x + size, y],
|
||||
[x + size, y + size],
|
||||
[x, y + size],
|
||||
[x, y],
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _evidence_feature(
|
||||
role: str,
|
||||
feature_id: str,
|
||||
geometry: dict,
|
||||
*,
|
||||
tile_index: int = 1,
|
||||
confidence: float | None = None,
|
||||
) -> dict:
|
||||
properties = {
|
||||
"qa_evidence_role": role,
|
||||
"candidate_feature_id": feature_id,
|
||||
"feature_class": "building",
|
||||
"tile_index": tile_index,
|
||||
"analysis_run_id": "run-1",
|
||||
"quality_check_id": "quality-1",
|
||||
"calibration_threshold": 0.15,
|
||||
"calibration_model_asset_id": "model-a",
|
||||
}
|
||||
if confidence is not None:
|
||||
properties["candidate_confidence"] = confidence
|
||||
return {
|
||||
"type": "Feature",
|
||||
"id": f"{role}:{feature_id}",
|
||||
"properties": properties,
|
||||
"geometry": geometry,
|
||||
}
|
||||
|
||||
|
||||
def _write_portfolio(
|
||||
tmp_path: Path,
|
||||
features: list[dict],
|
||||
*,
|
||||
declared_false_positives: int,
|
||||
) -> Path:
|
||||
evidence_dir = tmp_path / "samples" / "geel" / "evidence"
|
||||
evidence_dir.mkdir(parents=True)
|
||||
evidence_path = evidence_dir / "calibration_evidence.geojson"
|
||||
evidence_path.write_text(
|
||||
json.dumps({"type": "FeatureCollection", "features": features}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
portfolio_path = tmp_path / "calibration_evidence_portfolio.json"
|
||||
portfolio_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"model_asset_id": "model-a",
|
||||
"model_sha256": "abc123",
|
||||
"samples": [
|
||||
{
|
||||
"sample_slug": "geel",
|
||||
"role_counts": {
|
||||
"false_positive": declared_false_positives,
|
||||
"match_candidate": 1,
|
||||
},
|
||||
"evidence_geojson_path": str(evidence_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return portfolio_path
|
||||
|
||||
|
||||
def test_detection_results_table_uses_bounded_local_pagination() -> None:
|
||||
lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
styles = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "DETECTION_PAGE_SIZE_OPTIONS" in lab
|
||||
assert "visibleDetectionItems" in lab
|
||||
assert "detectionItems.slice" in lab
|
||||
assert "Detection result pagination" in lab
|
||||
assert "Previous detection results page" in lab
|
||||
assert "Next detection results page" in lab
|
||||
assert "pagination-toolbar" in styles
|
||||
assert "detectionItems.map((detection)" not in lab
|
||||
|
||||
|
||||
def test_false_positive_audit_builds_reviewable_persisted_evidence(tmp_path: Path) -> None:
|
||||
script = ROOT / "scripts" / "audit_detection_false_positive_evidence.py"
|
||||
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert script.exists()
|
||||
assert "py_compile scripts/audit_detection_false_positive_evidence.py" in readiness
|
||||
assert "COPY scripts/audit_detection_false_positive_evidence.py" in dockerfile
|
||||
|
||||
features = [
|
||||
_evidence_feature(
|
||||
"false_positive",
|
||||
"fp-small",
|
||||
_polygon(5.0, 51.2, 0.0001),
|
||||
tile_index=4,
|
||||
confidence=0.27,
|
||||
),
|
||||
_evidence_feature(
|
||||
"false_positive",
|
||||
"fp-large",
|
||||
_polygon(5.001, 51.2, 0.0003),
|
||||
tile_index=4,
|
||||
confidence=0.81,
|
||||
),
|
||||
_evidence_feature(
|
||||
"match_candidate",
|
||||
"matched",
|
||||
_polygon(5.002, 51.2, 0.0002),
|
||||
tile_index=7,
|
||||
),
|
||||
]
|
||||
portfolio_path = _write_portfolio(
|
||||
tmp_path / "portfolio",
|
||||
features,
|
||||
declared_false_positives=2,
|
||||
)
|
||||
output_dir = tmp_path / "audit"
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script),
|
||||
"--portfolio",
|
||||
str(portfolio_path),
|
||||
"--output-dir",
|
||||
str(output_dir),
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
report = json.loads(
|
||||
(output_dir / "detection_false_positive_audit.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert report["model_asset_id"] == "model-a"
|
||||
assert report["model_sha256"] == "abc123"
|
||||
assert report["false_positive_count"] == 2
|
||||
assert report["candidate_count"] == 3
|
||||
assert report["false_positive_rate"] == 2 / 3
|
||||
assert report["confidence_coverage_count"] == 2
|
||||
assert report["confidence"]["median"] == 0.54
|
||||
assert report["source_tile_counts"] == {"geel:4": 2}
|
||||
assert sum(bucket["count"] for bucket in report["area_buckets"].values()) == 2
|
||||
assert report["samples"][0]["false_positive_area_m2"]["median"] > 0
|
||||
assert report["recommendations"]
|
||||
|
||||
geojson = json.loads(
|
||||
(output_dir / "false_positives.geojson").read_text(encoding="utf-8")
|
||||
)
|
||||
assert geojson["type"] == "FeatureCollection"
|
||||
assert len(geojson["features"]) == 2
|
||||
assert all(
|
||||
feature["properties"]["qa_evidence_role"] == "false_positive"
|
||||
for feature in geojson["features"]
|
||||
)
|
||||
assert all(feature["properties"]["sample_slug"] == "geel" for feature in geojson["features"])
|
||||
assert all(feature["properties"]["area_m2"] > 0 for feature in geojson["features"])
|
||||
assert "False-positive audit JSON" in result.stdout
|
||||
assert (output_dir / "detection_false_positive_audit.md").is_file()
|
||||
|
||||
|
||||
def test_false_positive_audit_rejects_declared_role_count_drift(tmp_path: Path) -> None:
|
||||
script = ROOT / "scripts" / "audit_detection_false_positive_evidence.py"
|
||||
portfolio_path = _write_portfolio(
|
||||
tmp_path / "portfolio",
|
||||
[
|
||||
_evidence_feature(
|
||||
"false_positive",
|
||||
"fp-one",
|
||||
_polygon(5.0, 51.2, 0.0001),
|
||||
)
|
||||
],
|
||||
declared_false_positives=2,
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script),
|
||||
"--portfolio",
|
||||
str(portfolio_path),
|
||||
"--output-dir",
|
||||
str(tmp_path / "audit"),
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "declares 2 false positives but evidence contains 1" in result.stderr
|
||||
Reference in New Issue
Block a user