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`.
|
- 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")
|
||||||
|
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user