feat(provenance): govern source snapshots and data inputs
This commit is contained in:
@@ -7,6 +7,7 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -15,6 +16,19 @@ from pyproj import Transformer
|
||||
from shapely.geometry import box
|
||||
from shapely.ops import transform as shapely_transform
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from training_dataset_eligibility import ( # noqa: E402
|
||||
TrainingEligibilityError,
|
||||
assert_frozen_manifest_training_eligible,
|
||||
)
|
||||
from training_release_manifest import ( # noqa: E402
|
||||
TrainingReleaseError,
|
||||
assert_yolo_summary_bound_to_embedded_training_release,
|
||||
)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
@@ -73,6 +87,11 @@ def main() -> int:
|
||||
parser.add_argument("--internal-val-samples", required=True)
|
||||
parser.add_argument("--version", required=True)
|
||||
parser.add_argument("--output-dir", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--fixture-mode",
|
||||
action="store_true",
|
||||
help="Accept only an explicitly fixture-only source corpus; never use for operational holdout rotation.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
role_slugs = {
|
||||
@@ -86,6 +105,20 @@ def main() -> int:
|
||||
raise SystemExit("rotated holdout lists overlap")
|
||||
|
||||
manifest = json.loads(args.corpus_manifest.read_text(encoding="utf-8"))
|
||||
try:
|
||||
assert_frozen_manifest_training_eligible(
|
||||
args.corpus_manifest,
|
||||
fixture_mode=args.fixture_mode,
|
||||
verify_live=True,
|
||||
)
|
||||
for path in args.source_summary:
|
||||
assert_yolo_summary_bound_to_embedded_training_release(
|
||||
summary_path=path,
|
||||
corpus_manifest=args.corpus_manifest,
|
||||
fixture_mode=args.fixture_mode,
|
||||
)
|
||||
except (TrainingEligibilityError, TrainingReleaseError) as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
source_samples = {item["sample_slug"]: item for item in manifest["samples"]}
|
||||
missing = sorted(all_holdouts - source_samples.keys())
|
||||
if missing:
|
||||
@@ -126,6 +159,18 @@ def main() -> int:
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path = args.output_dir / "operator_samples_manifest.json"
|
||||
write_json(manifest_path, rotated_manifest)
|
||||
write_json(
|
||||
args.output_dir / "corpus-freeze.json",
|
||||
{
|
||||
"schema_version": 2,
|
||||
"dataset_version": rotated_manifest["dataset_version"],
|
||||
"manifest_sha256": sha256(manifest_path),
|
||||
"sample_count": len(rotated_manifest["samples"]),
|
||||
"immutable": True,
|
||||
"training_eligibility_policy": rotated_manifest["training_eligibility"]["policy_version"],
|
||||
"fixture_mode": bool(args.fixture_mode),
|
||||
},
|
||||
)
|
||||
leakage = audit_spatial_leakage(rotated_manifest["samples"])
|
||||
write_json(args.output_dir / "spatial-leakage-audit.json", leakage)
|
||||
if leakage["status"] != "ok":
|
||||
@@ -193,6 +238,11 @@ def main() -> int:
|
||||
"fit_samples_in_internal_validation": sorted(
|
||||
{tile["sample_slug"] for tile in train_tiles} & {tile["sample_slug"] for tile in internal_val_tiles}
|
||||
),
|
||||
"fixture_mode": bool(args.fixture_mode),
|
||||
"training_eligible": False,
|
||||
"training_eligibility_reason": (
|
||||
"A rotated split needs a new human review, label contract validation and immutable training release."
|
||||
),
|
||||
}
|
||||
if evidence["protected_samples_in_training"]:
|
||||
raise SystemExit("protected samples leaked into rotated training lists")
|
||||
|
||||
Reference in New Issue
Block a user