Harden split background promotion preflight
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-11 00:25:34 +02:00
parent 46db53dff4
commit a499c5fbfa
5 changed files with 98 additions and 5 deletions
+1
View File
@@ -18,6 +18,7 @@
- Added `--preflight-only` to `scripts/run_split_background_promotion_workflow.sh`. - Added `--preflight-only` to `scripts/run_split_background_promotion_workflow.sh`.
- Preflight validates local positive portfolio and operator manifest files, confirms required `pure_empty_negative` and `sparse_building_context` background categories, checks Python/curl availability and verifies the runtime API proxy returns the canonical envelope. - Preflight validates local positive portfolio and operator manifest files, confirms required `pure_empty_negative` and `sparse_building_context` background categories, checks Python/curl availability and verifies the runtime API proxy returns the canonical envelope.
- Hardened preflight compatibility for older operator manifests by deriving missing background categories from `reference_feature_count`, matching the existing matrix-runner behavior.
- Documented the quick post-redeploy preflight command before starting long configured-YOLO matrix inference. - Documented the quick post-redeploy preflight command before starting long configured-YOLO matrix inference.
- No model default, backend API, database migration, provider fetching, fake detection output or model download behavior changed. - No model default, backend API, database migration, provider fetching, fake detection output or model download behavior changed.
@@ -1,11 +1,32 @@
from __future__ import annotations from __future__ import annotations
import json
import os
import shlex
import subprocess
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
def _bash_path(path: Path) -> str:
raw_path = str(path)
if os.name != "nt":
return raw_path
result = subprocess.run(
["bash", "-lc", f"wslpath -a {shlex.quote(raw_path)}"],
capture_output=True,
text=True,
timeout=10,
check=False,
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
return raw_path
def test_split_background_promotion_workflow_runs_split_then_split_aware_report() -> None: def test_split_background_promotion_workflow_runs_split_then_split_aware_report() -> None:
script_path = ROOT / "scripts" / "run_split_background_promotion_workflow.sh" script_path = ROOT / "scripts" / "run_split_background_promotion_workflow.sh"
assert script_path.exists() assert script_path.exists()
@@ -35,6 +56,54 @@ def test_split_background_promotion_workflow_has_safe_preflight_mode() -> None:
assert "Split-background promotion preflight passed" in source assert "Split-background promotion preflight passed" in source
def test_split_background_preflight_derives_missing_background_categories(tmp_path: Path) -> None:
positive_portfolio = tmp_path / "positive.json"
positive_portfolio.write_text('{"items":[]}', encoding="utf-8")
manifest = tmp_path / "operator_samples_manifest.json"
manifest.write_text(
json.dumps(
{
"samples": [
{
"sample_slug": "postel_bos",
"sample_role": "background_candidate",
"reference_feature_count": 0,
},
{
"sample_slug": "kasterlee_bos",
"sample_role": "background_candidate",
"reference_feature_count": 7,
},
]
}
),
encoding="utf-8",
)
api_root = tmp_path / "api-root"
projects_endpoint = api_root / "api" / "v1" / "projects"
projects_endpoint.parent.mkdir(parents=True)
projects_endpoint.write_text('{"data":{"items":[]}}', encoding="utf-8")
command = (
f"PROMOTION_POSITIVE_PORTFOLIO_PATH={shlex.quote(_bash_path(positive_portfolio))} "
f"OPERATOR_SAMPLE_MANIFEST_PATH={shlex.quote(_bash_path(manifest))} "
f"bash scripts/run_split_background_promotion_workflow.sh --preflight-only "
f"{shlex.quote(f'file://{_bash_path(api_root)}')}"
)
result = subprocess.run(
["bash", "-lc", command],
cwd=ROOT,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert result.returncode == 0, result.stderr
assert "Split-background promotion preflight passed" in result.stdout
def test_readiness_checks_split_background_promotion_workflow_syntax() -> None: def test_readiness_checks_split_background_promotion_workflow_syntax() -> None:
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8") readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
+5
View File
@@ -301,6 +301,11 @@ For a quick post-redeploy check before the long matrix starts, use
checks local paths, required background categories and the runtime API envelope checks local paths, required background categories and the runtime API envelope
without running inference: without running inference:
Legacy operator manifests that do not yet contain explicit `background_category`
remain supported: the preflight derives `pure_empty_negative` from
`reference_feature_count == 0` and `sparse_building_context` from background
samples with persisted reference features, matching the matrix runner.
```bash ```bash
PROMOTION_POSITIVE_PORTFOLIO_PATH=artifacts/detection-quality-matrix/multi-sample/aoi1024bg512r3e50-positive/multi_sample_quality_summary.json \ PROMOTION_POSITIVE_PORTFOLIO_PATH=artifacts/detection-quality-matrix/multi-sample/aoi1024bg512r3e50-positive/multi_sample_quality_summary.json \
OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \ OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
+5
View File
@@ -724,6 +724,11 @@ presence, required `pure_empty_negative` and `sparse_building_context`
background categories, Python/curl availability and the frontend API proxy background categories, Python/curl availability and the frontend API proxy
envelope without starting inference: envelope without starting inference:
For older operator manifests that predate explicit `background_category`,
preflight uses the same fallback as the matrix runner: background samples with
`reference_feature_count == 0` are treated as `pure_empty_negative`, and
background samples with references are treated as `sparse_building_context`.
```bash ```bash
PROMOTION_POSITIVE_PORTFOLIO_PATH=/mnt/user/appdata/geointel/artifacts/detection-quality-matrix/multi-sample/aoi1024bg512r3e50-positive/multi_sample_quality_summary.json \ PROMOTION_POSITIVE_PORTFOLIO_PATH=/mnt/user/appdata/geointel/artifacts/detection-quality-matrix/multi-sample/aoi1024bg512r3e50-positive/multi_sample_quality_summary.json \
OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \ OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
@@ -110,11 +110,24 @@ samples = payload.get("samples") or payload.get("items") or []
if not isinstance(samples, list) or not samples: if not isinstance(samples, list) or not samples:
raise SystemExit(f"Operator sample manifest has no samples: {manifest_path}") raise SystemExit(f"Operator sample manifest has no samples: {manifest_path}")
background_categories = { background_categories = set()
str(sample.get("background_category") or "") for sample in samples:
for sample in samples if str(sample.get("sample_role") or "") != "background_candidate":
if str(sample.get("sample_role") or "") == "background_candidate" continue
}
category = str(sample.get("background_category") or "").strip()
if not category:
try:
reference_feature_count = int(sample.get("reference_feature_count") or 0)
except (TypeError, ValueError):
reference_feature_count = 0
category = (
"pure_empty_negative"
if reference_feature_count == 0
else "sparse_building_context"
)
background_categories.add(category)
missing = {"pure_empty_negative", "sparse_building_context"} - background_categories missing = {"pure_empty_negative", "sparse_building_context"} - background_categories
if missing: if missing:
raise SystemExit( raise SystemExit(