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`.
- 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.
- 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
import json
import os
import shlex
import subprocess
from pathlib import Path
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:
script_path = ROOT / "scripts" / "run_split_background_promotion_workflow.sh"
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
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:
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
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
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 \
+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
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
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 \
@@ -110,11 +110,24 @@ samples = payload.get("samples") or payload.get("items") or []
if not isinstance(samples, list) or not samples:
raise SystemExit(f"Operator sample manifest has no samples: {manifest_path}")
background_categories = {
str(sample.get("background_category") or "")
for sample in samples
if str(sample.get("sample_role") or "") == "background_candidate"
}
background_categories = set()
for sample in samples:
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
if missing:
raise SystemExit(