Files
geointel/scripts/smoke_detection_calibration_evidence_bundle.sh
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

238 lines
6.7 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage:
bash scripts/smoke_detection_calibration_evidence_bundle.sh
Optional environment:
CALIBRATION_EVIDENCE_SMOKE_DIR Output directory for the temporary smoke files.
This smoke creates a browser-style detection-calibration-summary.json, serves
canonical QA evidence envelopes through a temporary mock curl command, runs the
real export_detection_calibration_evidence.sh script, and validates the emitted
GeoJSON, summary JSON and HTML review artifacts.
EOF
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
if [ -n "${PYTHON_BIN:-}" ]; then
PYTHON_BIN="${PYTHON_BIN}"
else
PYTHON_BIN=""
for candidate in python3 python.exe python; do
if command -v "${candidate}" >/dev/null 2>&1 && "${candidate}" -c "import json, sys" >/dev/null 2>&1; then
PYTHON_BIN="${candidate}"
break
fi
done
fi
if [ -z "${PYTHON_BIN}" ]; then
echo "A Python interpreter is required for the calibration evidence smoke" >&2
exit 1
fi
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
BASE_DIR="${CALIBRATION_EVIDENCE_SMOKE_DIR:-${ROOT}/artifacts/detection-calibration-smoke/${timestamp}}"
if printf '%s' "$BASE_DIR" | grep -Eq '^[A-Za-z]:\\'; then
if command -v wslpath >/dev/null 2>&1; then
BASE_DIR="$(wslpath -u "$BASE_DIR")"
elif command -v cygpath >/dev/null 2>&1; then
BASE_DIR="$(cygpath -u "$BASE_DIR")"
fi
fi
SUMMARY_DIR="${BASE_DIR}/summary"
OUTPUT_DIR="${BASE_DIR}/evidence"
MOCK_BIN_DIR="${BASE_DIR}/mock-bin"
mkdir -p "$SUMMARY_DIR" "$OUTPUT_DIR" "$MOCK_BIN_DIR"
SUMMARY_PATH="${SUMMARY_DIR}/detection-calibration-summary.json"
cat > "$SUMMARY_PATH" <<'JSON'
{
"export_type": "detection_calibration_summary",
"project_id": "project-smoke",
"quality_check_ids": ["qc-low", "qc-high"],
"rows": [
{
"threshold": 0.15,
"analysis_run_id": "analysis-low",
"job_id": "job-low",
"quality_check_id": "qc-low",
"detection_count": 3,
"quality_score": 0.5,
"precision": 0.5,
"recall": 0.5,
"f1_score": 0.5
},
{
"threshold": 0.35,
"analysis_run_id": "analysis-high",
"job_id": "job-high",
"quality_check_id": "qc-high",
"detection_count": 1,
"quality_score": 0.67,
"precision": 1.0,
"recall": 0.5,
"f1_score": 0.67
}
]
}
JSON
cat > "${MOCK_BIN_DIR}/curl" <<'BASH'
#!/usr/bin/env bash
set -euo pipefail
url="${@: -1}"
case "$url" in
*/api/v1/projects/project-smoke/quality-checks/qc-low/evidence/geojson)
cat <<'JSON'
{
"data": {
"project_id": "project-smoke",
"quality_check_id": "qc-low",
"warnings": [],
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "low-match-candidate",
"properties": {
"qa_evidence_role": "match_candidate",
"feature_id": "candidate-low-1"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[4.9000, 51.1000], [4.9010, 51.1000], [4.9010, 51.1010], [4.9000, 51.1010], [4.9000, 51.1000]]]
}
},
{
"type": "Feature",
"id": "low-false-positive",
"properties": {
"qa_evidence_role": "false_positive",
"feature_id": "candidate-low-2"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[4.9020, 51.1000], [4.9030, 51.1000], [4.9030, 51.1010], [4.9020, 51.1010], [4.9020, 51.1000]]]
}
}
]
}
}
}
JSON
;;
*/api/v1/projects/project-smoke/quality-checks/qc-high/evidence/geojson)
cat <<'JSON'
{
"data": {
"project_id": "project-smoke",
"quality_check_id": "qc-high",
"warnings": [],
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "high-match-reference",
"properties": {
"qa_evidence_role": "match_reference",
"feature_id": "reference-high-1"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[4.9040, 51.1000], [4.9050, 51.1000], [4.9050, 51.1010], [4.9040, 51.1010], [4.9040, 51.1000]]]
}
},
{
"type": "Feature",
"id": "high-false-negative",
"properties": {
"qa_evidence_role": "false_negative",
"feature_id": "reference-high-2"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[4.9060, 51.1000], [4.9070, 51.1000], [4.9070, 51.1010], [4.9060, 51.1010], [4.9060, 51.1000]]]
}
}
]
}
}
}
JSON
;;
*)
echo "Unexpected mocked evidence URL: $url" >&2
exit 22
;;
esac
BASH
chmod +x "${MOCK_BIN_DIR}/curl"
echo "== GeoIntel detection calibration evidence smoke =="
echo "Using mocked canonical evidence endpoint"
echo "Summary: ${SUMMARY_PATH}"
echo "Output: ${OUTPUT_DIR}"
PATH="${MOCK_BIN_DIR}:${PATH}" \
CALIBRATION_EVIDENCE_DIR="$OUTPUT_DIR" \
bash scripts/export_detection_calibration_evidence.sh http://mock-geointel "$SUMMARY_PATH"
"${PYTHON_BIN}" - "$OUTPUT_DIR" <<'PY'
import json
import os
import sys
output_dir = sys.argv[1]
summary_path = os.path.join(output_dir, "calibration_evidence_summary.json")
geojson_path = os.path.join(output_dir, "calibration_evidence.geojson")
html_path = os.path.join(output_dir, "calibration_evidence_review.html")
for path in (summary_path, geojson_path, html_path):
if not os.path.isfile(path):
raise SystemExit(f"Expected evidence artifact is missing: {path}")
with open(summary_path, "r", encoding="utf-8") as handle:
summary = json.load(handle)
with open(geojson_path, "r", encoding="utf-8") as handle:
geojson = json.load(handle)
with open(html_path, "r", encoding="utf-8") as handle:
html = handle.read()
expected_roles = {
"match_candidate": 1,
"match_reference": 1,
"false_positive": 1,
"false_negative": 1,
}
if summary.get("feature_count") != 4:
raise SystemExit(f"Expected 4 evidence features, got {summary.get('feature_count')}")
if summary.get("role_counts") != expected_roles:
raise SystemExit(f"Unexpected role counts: {summary.get('role_counts')}")
if len(summary.get("runs") or []) != 2:
raise SystemExit(f"Expected 2 calibration runs, got {len(summary.get('runs') or [])}")
if len(geojson.get("features") or []) != 4:
raise SystemExit(f"Expected 4 combined GeoJSON features, got {len(geojson.get('features') or [])}")
if "Detection calibration evidence review" not in html:
raise SystemExit("Evidence review HTML title is missing")
PY
echo "Detection calibration evidence smoke passed"