234 lines
6.6 KiB
Bash
234 lines
6.6 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 command -v cygpath >/dev/null 2>&1 && printf '%s' "$BASE_DIR" | grep -Eq '^[A-Za-z]:\\'; then
|
|
BASE_DIR="$(cygpath -u "$BASE_DIR")"
|
|
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"
|