Add multi-AOI calibration evidence portfolio
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-08 14:10:02 +02:00
parent c42ae23a61
commit 28a49bd0d5
8 changed files with 553 additions and 2 deletions
+7
View File
@@ -7,6 +7,13 @@
# Changelog
## Sprint 139 Multi-AOI calibration evidence portfolio (2026-07-08)
- Added `scripts/assemble_detection_calibration_evidence_portfolio.sh` to package multiple AOI calibration summaries and their persisted QA evidence bundles into one model-review portfolio.
- The assembler copies each summary into a sample folder, runs the existing evidence exporter per AOI and writes `calibration_evidence_portfolio.json` plus `calibration_evidence_portfolio.md`.
- Added readiness syntax coverage and a mocked-endpoint regression test for the portfolio convention.
- No backend API, migration, inference, provider fetching, model download, live data mutation or frontend runtime behavior changed.
## Sprint 138 Browser calibration evidence bundle smoke (2026-07-08)
- Added `scripts/smoke_detection_calibration_evidence_bundle.sh` to exercise the browser `detection-calibration-summary.json` -> QA evidence bundle path locally.
@@ -0,0 +1,178 @@
import json
import os
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def _bash_path(path: Path) -> str:
value = path.as_posix()
if len(value) > 2 and value[1] == ":":
return f"/mnt/{value[0].lower()}{value[2:]}"
return value
def _path_from_stdout(stdout: str, label: str) -> Path:
for line in stdout.splitlines():
if line.startswith(label):
raw_path = line.split(":", 1)[1].strip()
if raw_path.startswith("/mnt/") and len(raw_path) > 6 and raw_path[6] == "/":
drive = raw_path[5].upper()
return Path(f"{drive}:{raw_path[6:]}")
return Path(raw_path)
raise AssertionError(f"Missing {label!r} path in output:\n{stdout}")
def test_multi_aoi_calibration_evidence_portfolio_assembles_existing_evidence(tmp_path) -> None:
script_path = ROOT / "scripts" / "assemble_detection_calibration_evidence_portfolio.sh"
readiness_path = ROOT / "scripts" / "run_readiness_check.sh"
readme_path = ROOT / "scripts" / "README.md"
assert script_path.exists()
assert "bash -n scripts/assemble_detection_calibration_evidence_portfolio.sh" in readiness_path.read_text(
encoding="utf-8"
)
assert "calibration-evidence-portfolio-manifest.json" in readme_path.read_text(encoding="utf-8")
summary_a = tmp_path / "geel-summary.json"
summary_b = tmp_path / "mol-summary.json"
summary_a.write_text(
json.dumps(
{
"export_type": "detection_calibration_summary",
"project_id": "project-geel",
"rows": [
{
"threshold": 0.15,
"quality_check_id": "qc-geel",
"analysis_run_id": "analysis-geel",
"job_id": "job-geel",
"detection_count": 5,
"quality_score": 0.42,
"precision": 0.7,
"recall": 0.3,
"f1_score": 0.42,
}
],
}
),
encoding="utf-8",
)
summary_b.write_text(
json.dumps(
{
"export_type": "detection_calibration_summary",
"project_id": "project-mol",
"rows": [
{
"threshold": 0.35,
"quality_check_id": "qc-mol",
"analysis_run_id": "analysis-mol",
"job_id": "job-mol",
"detection_count": 3,
"quality_score": 0.6,
"precision": 1.0,
"recall": 0.43,
"f1_score": 0.6,
}
],
}
),
encoding="utf-8",
)
manifest = tmp_path / "calibration-evidence-portfolio-manifest.json"
manifest.write_text(
json.dumps(
{
"portfolio_name": "Kempen building model smoke",
"model_asset_id": "geointel-building-yolov8s-smoke-pt",
"model_sha256": "abc123",
"notes": "Operator comparison notes stay outside application state.",
"samples": [
{
"sample_slug": "geel",
"aoi_label": "Geel center",
"summary_path": _bash_path(summary_a),
"operator_notes": "Dense urban validation sample.",
},
{
"sample_slug": "mol",
"aoi_label": "Mol edge",
"summary_path": _bash_path(summary_b),
"operator_notes": "Lower-density validation sample.",
},
],
}
),
encoding="utf-8",
)
mock_bin = tmp_path / "mock-bin"
mock_bin.mkdir()
mock_curl = mock_bin / "curl"
mock_curl.write_text(
"""#!/usr/bin/env bash
set -euo pipefail
url="${@: -1}"
case "$url" in
*/api/v1/projects/project-geel/quality-checks/qc-geel/evidence/geojson)
role="match_candidate"
quality_check_id="qc-geel"
;;
*/api/v1/projects/project-mol/quality-checks/qc-mol/evidence/geojson)
role="false_negative"
quality_check_id="qc-mol"
;;
*)
echo "Unexpected URL: $url" >&2
exit 22
;;
esac
cat <<JSON
{"data":{"quality_check_id":"${quality_check_id}","warnings":[],"geojson":{"type":"FeatureCollection","features":[{"type":"Feature","id":"feature-${role}","properties":{"qa_evidence_role":"${role}","feature_id":"${role}-1"},"geometry":{"type":"Polygon","coordinates":[[[4.9,51.1],[4.91,51.1],[4.91,51.11],[4.9,51.11],[4.9,51.1]]]}}]}}}
JSON
""",
encoding="utf-8",
newline="\n",
)
mock_curl.chmod(0o755)
env = os.environ.copy()
env["PATH"] = f"{_bash_path(mock_bin)}:{env['PATH']}"
output_dir = _bash_path(tmp_path / "portfolio-output")
manifest_path = _bash_path(manifest)
mock_curl_path = _bash_path(mock_curl)
result = subprocess.run(
[
"bash",
"-lc",
(
f"CURL_BIN='{mock_curl_path}' "
f"CALIBRATION_PORTFOLIO_OUTPUT_DIR='{output_dir}' "
"bash scripts/assemble_detection_calibration_evidence_portfolio.sh "
f"http://mock-geointel '{manifest_path}'"
),
],
cwd=ROOT,
env=env,
check=True,
text=True,
capture_output=True,
)
assert "Detection calibration evidence portfolio passed" in result.stdout
portfolio_path = _path_from_stdout(result.stdout, "Portfolio JSON")
markdown_path = _path_from_stdout(result.stdout, "Portfolio Markdown")
portfolio = json.loads(portfolio_path.read_text(encoding="utf-8"))
markdown = markdown_path.read_text(encoding="utf-8")
assert portfolio["portfolio_name"] == "Kempen building model smoke"
assert portfolio["model_asset_id"] == "geointel-building-yolov8s-smoke-pt"
assert portfolio["sample_count"] == 2
assert portfolio["total_evidence_feature_count"] == 2
assert {sample["sample_slug"] for sample in portfolio["samples"]} == {"geel", "mol"}
assert portfolio["best_sample_by_score"]["sample_slug"] == "mol"
assert "Geel center" in markdown
assert "Mol edge" in markdown
assert "calibration_evidence_review.html" in markdown
+33
View File
@@ -1,3 +1,36 @@
## Sprint 139 Multi-AOI calibration evidence portfolio (2026-07-08)
Changed:
- Added `scripts/assemble_detection_calibration_evidence_portfolio.sh` for packaging multiple AOI calibration summaries and their persisted QA evidence bundles into one model-review portfolio.
- The assembler reads `calibration-evidence-portfolio-manifest.json`, copies each AOI summary into a sample folder, runs the existing `scripts/export_detection_calibration_evidence.sh` exporter per sample and writes:
- `calibration_evidence_portfolio.json`
- `calibration_evidence_portfolio.md`
- Added optional `CURL_BIN` support to `scripts/export_detection_calibration_evidence.sh` so operator smokes/tests can inject a deterministic endpoint mock while defaulting to normal `curl`.
- Added readiness syntax coverage and operator docs for the manifest convention.
- Updated `scripts/README.md`, `CHANGELOG.md` and `docs/TODO.md`.
- Added regression coverage in `backend/tests/test_sprint139_multi_aoi_calibration_evidence_portfolio.py`.
Tested:
- Red step: `python -m pytest backend\tests\test_sprint139_multi_aoi_calibration_evidence_portfolio.py -q` failed while `scripts/assemble_detection_calibration_evidence_portfolio.sh` was absent.
- `python -m pytest backend\tests\test_sprint139_multi_aoi_calibration_evidence_portfolio.py -q` (`1 passed`)
- `python -m pytest backend\tests\test_sprint139_multi_aoi_calibration_evidence_portfolio.py backend\tests\test_sprint138_calibration_evidence_bundle_smoke.py backend\tests\test_sprint137_browser_calibration_summary_evidence_script.py backend\tests\test_sprint125_detection_calibration_evidence_bundle.py -q` (`4 passed`)
- `bash -n scripts/assemble_detection_calibration_evidence_portfolio.sh`
- `bash scripts/assemble_detection_calibration_evidence_portfolio.sh --help`
- `bash -n scripts/export_detection_calibration_evidence.sh`
- `bash scripts/export_detection_calibration_evidence.sh --help`
- `python -m compileall backend/app`
- `bash scripts/run_readiness_check.sh` (`418 passed`; frontend typecheck/build passed; Alembic head `202606120900`; shell syntax gates passed)
Open:
- None for this pass.
Limitations:
- This is local/operator evidence packaging only. It does not run inference, call live production data by itself, mutate application data, add backend endpoints, change migrations, create QA metrics, promote thresholds, download models, add provider fetching or change frontend runtime behavior.
- The regression test uses mocked canonical QA evidence responses; real persisted QA evidence remains validated by running the portfolio assembler against live Detection Lab or calibration-sweep summaries.
Next recommended pass:
- Run the portfolio assembler against the existing Tower calibration summaries for at least two real AOIs, then use the portfolio JSON/Markdown as the first model-review handoff artifact before any further training or threshold promotion.
## Sprint 138 Browser calibration evidence bundle smoke (2026-07-08)
Changed:
+1
View File
@@ -425,5 +425,6 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add guided calibration summary export from the Detection Lab.
- [x] Allow the evidence bundle script to consume Detection Lab calibration summary exports.
- [x] Add a local browser-summary QA evidence bundle smoke using mocked canonical evidence responses.
- [x] Add a multi-AOI calibration evidence portfolio convention for model-review handoff.
- [ ] Add more AOIs after the tile-level baseline so the next local model attempt is not limited to Geel/Mol/Turnhout.
- [ ] Add negative/background AOIs so the next tile dataset is not all positive tiles.
+33
View File
@@ -461,6 +461,39 @@ exporter and verifies that `calibration_evidence.geojson`,
`calibration_evidence_summary.json` and `calibration_evidence_review.html` are
written correctly.
Assemble multiple AOI evidence bundles into one model-review portfolio:
```bash
bash scripts/assemble_detection_calibration_evidence_portfolio.sh \
http://192.168.10.150:1202 \
./calibration-evidence-portfolio-manifest.json
```
Example `calibration-evidence-portfolio-manifest.json`:
```json
{
"portfolio_name": "Kempen building model calibration",
"model_asset_id": "geointel-building-yolov8s-hardneg160r4e50-pt",
"model_sha256": "optional-model-checksum",
"notes": "Operator comparison notes.",
"samples": [
{
"sample_slug": "geel",
"aoi_label": "Geel center",
"summary_path": "/path/to/detection-calibration-summary.json",
"operator_notes": "Dense urban validation sample."
}
]
}
```
The portfolio assembler copies each summary into a deterministic sample folder,
runs the existing evidence exporter per AOI and writes
`calibration_evidence_portfolio.json` plus
`calibration_evidence_portfolio.md`. It is evidence packaging only: it does not
run inference, create QA checks or mutate application data.
The evidence export reads each persisted `quality_check_id`, calls the existing
QA evidence GeoJSON endpoint, writes `calibration_evidence.geojson`,
`calibration_evidence_summary.json` and a standalone
@@ -0,0 +1,296 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage:
bash scripts/assemble_detection_calibration_evidence_portfolio.sh [base_url] /path/to/calibration-evidence-portfolio-manifest.json
Required manifest shape:
{
"portfolio_name": "Kempen building model calibration",
"model_asset_id": "geointel-building-yolov8s-hardneg160r4e50-pt",
"model_sha256": "optional",
"notes": "optional operator notes",
"samples": [
{
"sample_slug": "geel",
"aoi_label": "Geel center",
"summary_path": "/path/to/detection-calibration-summary.json",
"operator_notes": "optional AOI notes"
}
]
}
Optional environment:
CALIBRATION_PORTFOLIO_OUTPUT_DIR Output directory, default: artifacts/detection-calibration-portfolio/<timestamp>.
CALIBRATION_EVIDENCE_MODE all or best, default: all. Forwarded to export_detection_calibration_evidence.sh.
This is operator evidence tooling only. It does not run inference, create QA
checks, mutate application data, download models or fetch providers.
EOF
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
BASE_URL="${1:-${GE_INTEL_BASE_URL:-http://localhost:1202}}"
MANIFEST_PATH="${2:-${CALIBRATION_PORTFOLIO_MANIFEST_PATH:-}}"
CALIBRATION_EVIDENCE_MODE="${CALIBRATION_EVIDENCE_MODE:-all}"
CURL_BIN="${CURL_BIN:-curl}"
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
OUTPUT_DIR="${CALIBRATION_PORTFOLIO_OUTPUT_DIR:-${ROOT}/artifacts/detection-calibration-portfolio/${timestamp}}"
if [ -z "$MANIFEST_PATH" ]; then
usage
exit 2
fi
case "${CALIBRATION_EVIDENCE_MODE}" in
all|best) ;;
*)
echo "CALIBRATION_EVIDENCE_MODE must be 'all' or 'best'" >&2
exit 2
;;
esac
if command -v cygpath >/dev/null 2>&1 && printf '%s' "$OUTPUT_DIR" | grep -Eq '^[A-Za-z]:\\'; then
OUTPUT_DIR="$(cygpath -u "$OUTPUT_DIR")"
fi
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, pathlib, shutil, 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 portfolio assembly" >&2
exit 1
fi
mkdir -p "$OUTPUT_DIR"
SAMPLE_REQUESTS="${OUTPUT_DIR}/portfolio_sample_requests.tsv"
PORTFOLIO_META="${OUTPUT_DIR}/portfolio_input_metadata.json"
"${PYTHON_BIN}" - "$MANIFEST_PATH" "$OUTPUT_DIR" "$SAMPLE_REQUESTS" "$PORTFOLIO_META" <<'PY'
import json
import os
import re
import shutil
import sys
from pathlib import Path
manifest_path_raw, output_dir_raw, requests_path_raw, metadata_path_raw = sys.argv[1:5]
def as_path(raw: str) -> Path:
return Path(raw).expanduser()
manifest_path = as_path(manifest_path_raw)
if not manifest_path.is_file():
raise SystemExit(f"Manifest is not readable: {manifest_path_raw}")
output_dir = as_path(output_dir_raw)
samples_dir = output_dir / "samples"
samples_dir.mkdir(parents=True, exist_ok=True)
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
samples = manifest.get("samples") or []
if not isinstance(samples, list) or not samples:
raise SystemExit("Portfolio manifest must contain at least one sample")
def safe_slug(raw: str) -> str:
slug = re.sub(r"[^A-Za-z0-9_.-]+", "-", str(raw or "").strip()).strip("-._").lower()
if not slug:
raise SystemExit("Every portfolio sample needs a non-empty sample_slug")
return slug
request_rows = []
normalized_samples = []
seen_slugs = set()
for sample in samples:
if not isinstance(sample, dict):
raise SystemExit("Every portfolio sample must be an object")
sample_slug = safe_slug(sample.get("sample_slug"))
if sample_slug in seen_slugs:
raise SystemExit(f"Duplicate portfolio sample_slug: {sample_slug}")
seen_slugs.add(sample_slug)
summary_path = as_path(str(sample.get("summary_path") or ""))
if not summary_path.is_file():
raise SystemExit(f"Sample summary_path is not readable for {sample_slug}: {summary_path}")
sample_dir = samples_dir / sample_slug
evidence_dir = sample_dir / "evidence"
sample_dir.mkdir(parents=True, exist_ok=True)
evidence_dir.mkdir(parents=True, exist_ok=True)
copied_summary = sample_dir / summary_path.name
shutil.copyfile(summary_path, copied_summary)
normalized = {
"sample_slug": sample_slug,
"aoi_label": sample.get("aoi_label") or sample_slug,
"operator_notes": sample.get("operator_notes") or "",
"source_summary_path": str(summary_path),
"copied_summary_path": str(copied_summary),
"evidence_dir": str(evidence_dir),
}
normalized_samples.append(normalized)
request_rows.append((sample_slug, str(copied_summary), str(evidence_dir)))
metadata = {
"portfolio_name": manifest.get("portfolio_name") or "GeoIntel detection calibration evidence portfolio",
"model_asset_id": manifest.get("model_asset_id"),
"model_sha256": manifest.get("model_sha256"),
"notes": manifest.get("notes") or "",
"manifest_path": str(manifest_path),
"samples": normalized_samples,
}
Path(metadata_path_raw).write_text(json.dumps(metadata, indent=2, sort_keys=True), encoding="utf-8")
with Path(requests_path_raw).open("w", encoding="utf-8") as handle:
for row in request_rows:
handle.write("\t".join(row) + "\n")
PY
echo "== GeoIntel detection calibration evidence portfolio =="
echo "Base URL: ${BASE_URL}"
echo "Manifest: ${MANIFEST_PATH}"
echo "Mode: ${CALIBRATION_EVIDENCE_MODE}"
echo "Output: ${OUTPUT_DIR}"
while IFS=$'\t' read -r sample_slug summary_path evidence_dir; do
echo "-- Portfolio sample ${sample_slug} --"
CALIBRATION_EVIDENCE_DIR="${evidence_dir}" \
CALIBRATION_EVIDENCE_MODE="${CALIBRATION_EVIDENCE_MODE}" \
CURL_BIN="${CURL_BIN:-curl}" \
bash scripts/export_detection_calibration_evidence.sh "${BASE_URL}" "${summary_path}"
done < "$SAMPLE_REQUESTS"
"${PYTHON_BIN}" - "$PORTFOLIO_META" "$OUTPUT_DIR" "$CALIBRATION_EVIDENCE_MODE" <<'PY'
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
metadata_path, output_dir_raw, mode = sys.argv[1:4]
output_dir = Path(output_dir_raw)
metadata = json.loads(Path(metadata_path).read_text(encoding="utf-8"))
samples = []
total_features = 0
role_counts: dict[str, int] = {}
for sample in metadata["samples"]:
evidence_dir = Path(sample["evidence_dir"])
evidence_summary_path = evidence_dir / "calibration_evidence_summary.json"
evidence_geojson_path = evidence_dir / "calibration_evidence.geojson"
evidence_review_path = evidence_dir / "calibration_evidence_review.html"
if not evidence_summary_path.is_file():
raise SystemExit(f"Missing evidence summary for sample {sample['sample_slug']}: {evidence_summary_path}")
evidence_summary = json.loads(evidence_summary_path.read_text(encoding="utf-8"))
feature_count = int(evidence_summary.get("feature_count") or 0)
total_features += feature_count
for role, count in (evidence_summary.get("role_counts") or {}).items():
role_counts[role] = role_counts.get(role, 0) + int(count)
runs = evidence_summary.get("runs") or []
best_run = max(
[run for run in runs if run.get("quality_score") is not None],
key=lambda run: run["quality_score"],
default=None,
)
samples.append(
{
"sample_slug": sample["sample_slug"],
"aoi_label": sample["aoi_label"],
"operator_notes": sample["operator_notes"],
"source_summary_path": sample["source_summary_path"],
"copied_summary_path": sample["copied_summary_path"],
"evidence_summary_path": str(evidence_summary_path),
"evidence_geojson_path": str(evidence_geojson_path),
"evidence_review_path": str(evidence_review_path),
"evidence_feature_count": feature_count,
"role_counts": evidence_summary.get("role_counts") or {},
"best_run_by_score": best_run,
"runs": runs,
}
)
def sample_score(item: dict) -> float:
best = item.get("best_run_by_score") or {}
value = best.get("quality_score")
return value if isinstance(value, (int, float)) else float("-inf")
best_sample = max(samples, key=sample_score, default=None)
portfolio = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"portfolio_name": metadata["portfolio_name"],
"model_asset_id": metadata.get("model_asset_id"),
"model_sha256": metadata.get("model_sha256"),
"notes": metadata.get("notes") or "",
"mode": mode,
"sample_count": len(samples),
"total_evidence_feature_count": total_features,
"role_counts": dict(sorted(role_counts.items())),
"best_sample_by_score": best_sample,
"samples": samples,
}
portfolio_path = output_dir / "calibration_evidence_portfolio.json"
portfolio_path.write_text(json.dumps(portfolio, indent=2, sort_keys=True), encoding="utf-8")
markdown_path = output_dir / "calibration_evidence_portfolio.md"
lines = [
f"# {portfolio['portfolio_name']}",
"",
f"- Generated: {portfolio['generated_at']}",
f"- Model asset: {portfolio.get('model_asset_id') or 'not recorded'}",
f"- Model SHA256: {portfolio.get('model_sha256') or 'not recorded'}",
f"- Mode: {portfolio['mode']}",
f"- Samples: {portfolio['sample_count']}",
f"- Evidence features: {portfolio['total_evidence_feature_count']}",
]
if portfolio.get("notes"):
lines.extend(["", "## Operator notes", "", portfolio["notes"]])
lines.extend(["", "## Samples", ""])
for sample in samples:
best = sample.get("best_run_by_score") or {}
lines.extend(
[
f"### {sample['aoi_label']} (`{sample['sample_slug']}`)",
"",
f"- Evidence features: {sample['evidence_feature_count']}",
f"- Best threshold: {best.get('threshold')}",
f"- Score: {best.get('quality_score')}",
f"- Precision: {best.get('precision')}",
f"- Recall: {best.get('recall')}",
f"- F1: {best.get('f1_score')}",
f"- Review HTML: `{sample['evidence_review_path']}`",
f"- Evidence GeoJSON: `{sample['evidence_geojson_path']}`",
f"- Evidence summary: `{sample['evidence_summary_path']}`",
]
)
if sample.get("operator_notes"):
lines.append(f"- Notes: {sample['operator_notes']}")
lines.append("")
markdown_path.write_text("\n".join(lines), encoding="utf-8")
print("Detection calibration evidence portfolio passed")
print(f"Portfolio JSON: {portfolio_path}")
print(f"Portfolio Markdown: {markdown_path}")
print(f"Samples: {portfolio['sample_count']}")
print(f"Evidence features: {portfolio['total_evidence_feature_count']}")
PY
@@ -20,6 +20,7 @@ Required input:
Optional environment:
CALIBRATION_EVIDENCE_MODE all or best, default: all.
CALIBRATION_EVIDENCE_DIR Output directory, default: the summary file directory.
CURL_BIN curl executable, default: curl.
EOF
}
@@ -53,7 +54,8 @@ case "${CALIBRATION_EVIDENCE_MODE}" in
;;
esac
if ! command -v curl >/dev/null 2>&1; then
CURL_BIN="${CURL_BIN:-curl}"
if ! command -v "${CURL_BIN}" >/dev/null 2>&1; then
echo "curl is required for detection calibration evidence export" >&2
exit 1
fi
@@ -153,7 +155,7 @@ while IFS=$'\t' read -r threshold project_id quality_check_id; do
threshold_label="$(printf '%s' "${threshold}" | tr '.-' 'pm')"
response_path="${CALIBRATION_EVIDENCE_DIR}/threshold_${threshold_label}_evidence_response.json"
echo "-- Evidence threshold ${threshold}, quality_check_id ${quality_check_id} --"
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/quality-checks/${quality_check_id}/evidence/geojson" > "${response_path}"
"${CURL_BIN}" -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/quality-checks/${quality_check_id}/evidence/geojson" > "${response_path}"
done < "${request_manifest}"
"${PYTHON_BIN}" - "${CALIBRATION_SUMMARY_PATH}" "${CALIBRATION_EVIDENCE_DIR}" "${CALIBRATION_EVIDENCE_MODE}" <<'PY'
+1
View File
@@ -61,6 +61,7 @@ bash -n scripts/verify_real_data_detection_qa_workflow.sh
bash -n scripts/run_detection_calibration_sweep.sh
bash -n scripts/export_detection_calibration_evidence.sh
bash -n scripts/smoke_detection_calibration_evidence_bundle.sh
bash -n scripts/assemble_detection_calibration_evidence_portfolio.sh
bash -n scripts/run_detection_quality_matrix.sh
bash -n scripts/run_multi_sample_detection_quality_matrix.sh
bash -n scripts/run_operator_hard_negative_detection_matrix.sh