Harden split background promotion preflight
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user