Files
CM95/tools/cm95_production_plan_audit_v1.py

120 lines
4.4 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import argparse
from collections import Counter
import json
from pathlib import Path
def parse_expected(value: str) -> list[int]:
values: list[int] = []
for token in value.split(','):
token = token.strip()
if not token:
continue
if '-' in token:
a, b = token.split('-', 1)
start, end = int(a), int(b)
if end < start:
raise ValueError(f'invalid expected zoom range: {token}')
values.extend(range(start, end + 1))
else:
values.append(int(token))
return sorted(set(values))
def parse_args():
p = argparse.ArgumentParser()
p.add_argument('--plan', default='build/production/cm95_production_plan_v1.json')
p.add_argument('--output', default='build/production/cm95_production_plan_audit_v1.json')
p.add_argument('--expected-zooms', default='0-12')
return p.parse_args()
def main():
a = parse_args()
plan = json.loads(Path(a.plan).read_text(encoding='utf-8'))
jobs = plan.get('jobs') or []
keys = [(int(j['z']), int(j['x']), int(j['y'])) for j in jobs]
unique = set(keys)
by_zoom = Counter(z for z, _, _ in keys)
invalid = []
for z, x, y in keys:
n = 1 << z
if z < 0 or x < 0 or y < 0 or x >= n or y >= n:
invalid.append({'z': z, 'x': x, 'y': y})
source_stats = plan.get('source_stats') or {}
stats_mismatch = []
for z, count in sorted(by_zoom.items()):
declared = int((source_stats.get(str(z)) or {}).get('tile_count') or 0)
if declared != count:
stats_mismatch.append({'z': z, 'declared': declared, 'actual': count})
expected = parse_expected(a.expected_zooms)
planned_zooms = sorted(int(z) for z in (plan.get('zooms') or []))
missing_plan_zooms = sorted(set(expected) - set(planned_zooms))
unexpected_plan_zooms = sorted(set(planned_zooms) - set(expected))
missing_job_zooms = sorted(z for z in expected if by_zoom.get(z, 0) == 0)
failures = []
if len(unique) != len(keys):
failures.append('duplicate_jobs')
if invalid:
failures.append('invalid_xyz')
if int(plan.get('job_count') or 0) != len(keys):
failures.append('job_count_mismatch')
if stats_mismatch:
failures.append('source_stats_mismatch')
if missing_plan_zooms:
failures.append('missing_plan_zooms')
if unexpected_plan_zooms:
failures.append('unexpected_plan_zooms')
if missing_job_zooms:
failures.append('missing_job_zooms')
result = {
'schema': 'navsea.cm95.production-plan-audit/v1.1',
'plan_id': plan.get('plan_id'),
'release_id': plan.get('release_id'),
'dataset_id': plan.get('dataset_id'),
'builder_contract_version': plan.get('builder_contract_version'),
'expected_zooms': expected,
'planned_zooms': planned_zooms,
'job_count': len(keys),
'unique_job_count': len(unique),
'tile_count_by_zoom': {str(k): v for k, v in sorted(by_zoom.items())},
'missing_plan_zooms': missing_plan_zooms,
'unexpected_plan_zooms': unexpected_plan_zooms,
'missing_job_zooms': missing_job_zooms,
'invalid_xyz': invalid,
'source_stats_mismatch': stats_mismatch,
'failures': failures,
'overall': 'PASS' if not failures else 'FAIL',
}
out = Path(a.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
print(f"Plan ID : {result['plan_id']}")
print(f"Release ID : {result['release_id']}")
if result.get('builder_contract_version'):
print(f"Builder : {result['builder_contract_version']}")
print(f"Expected zooms : {a.expected_zooms}")
print(f"Jobs : {result['job_count']}")
for z in expected:
stat = source_stats.get(str(z)) or {}
enum_scales = stat.get('enumeration_scales') or []
ladder = stat.get('scale_ladder') or []
print(f"z{z} tiles : {by_zoom.get(z, 0)} enum={enum_scales} fallback={ladder}")
print(f"Duplicate jobs : {len(keys)-len(unique)}")
print(f"Invalid XYZ : {len(invalid)}")
print(f"Missing zooms : {len(missing_plan_zooms) + len(missing_job_zooms)}")
print(f"OVERALL : {result['overall']}")
if __name__ == '__main__':
main()