| |
| """Stage the frozen Day4.3 runtime assets into the Android alpha project.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import hashlib |
| import json |
| import shutil |
| from datetime import datetime, timezone |
| from pathlib import Path |
|
|
|
|
| EXPECTED = { |
| "models/text_classifier_int8.bin": |
| "5239754a0431241b805e805f3e1b603deb48380ebc1f0ea62a3f3a4c4d372cfd", |
| "config/rule_schema.json": |
| "2b5811a5cd8f95eb3283bed12f9cd56ed6fef47b20be787daa1567c80a4e2beb", |
| } |
|
|
| EXPECTED_CANDIDATE_ID = "day43-fusion-only-frozen-visual-v1" |
| EXPECTED_VISUAL_CHECKPOINT_SHA256 = ( |
| "63780ae3f1716bc2f0d36995a3d780dd47a2a1f9ca98c7ec9be6c69c935afabd" |
| ) |
| MAX_PARITY_ERROR = 1e-4 |
|
|
|
|
| def sha256(path: Path) -> str: |
| digest = hashlib.sha256() |
| with path.open("rb") as stream: |
| for chunk in iter(lambda: stream.read(1024 * 1024), b""): |
| digest.update(chunk) |
| return digest.hexdigest() |
|
|
|
|
| def copy_verified(source: Path, target: Path, expected: str | None = None) -> dict: |
| actual = sha256(source) |
| if expected is not None and actual != expected: |
| raise RuntimeError( |
| f"hash mismatch for {source}: expected {expected}, got {actual}" |
| ) |
| target.parent.mkdir(parents=True, exist_ok=True) |
| shutil.copy2(source, target) |
| if sha256(target) != actual: |
| raise RuntimeError(f"copy verification failed: {target}") |
| return { |
| "path": target.as_posix(), |
| "bytes": target.stat().st_size, |
| "sha256": actual, |
| } |
|
|
|
|
| def main() -> int: |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "--workspace", |
| type=Path, |
| default=Path(__file__).resolve().parents[2], |
| ) |
| parser.add_argument("--visual-model", type=Path) |
| parser.add_argument("--visual-report", type=Path) |
| args = parser.parse_args() |
|
|
| workspace = args.workspace.resolve() |
| project = workspace / "training_workspace" |
| frozen = project / "mobile_deployment" / "candidate_day43_v1" |
| portable = project / "mobile_deployment" / "runtime_day43_v1" / "fusion" |
| assets = ( |
| project |
| / "android" |
| / "Gravekeeper" |
| / "app" |
| / "src" |
| / "main" |
| / "assets" |
| ) |
|
|
| sources = { |
| "models/text_classifier_int8.bin": frozen / "text" / "text_classifier_int8.bin", |
| "models/text_classifier_int8.json": frozen / "text" / "text_classifier_int8.json", |
| "models/fusion_model.json": portable / "fusion_model.json", |
| "models/fusion_test_vectors.json": portable / "fusion_test_vectors.json", |
| "config/fusion_schema.json": portable / "fusion_schema.json", |
| "config/rule_schema.json": frozen / "rules" / "rule_schema.json", |
| "config/rule_test_vectors.json": frozen / "rules" / "rule_test_vectors.json", |
| } |
|
|
| records = [] |
| for relative, source in sources.items(): |
| if "locked" in source.as_posix().lower(): |
| raise RuntimeError("locked-test paths are forbidden") |
| records.append(copy_verified( |
| source, |
| assets / relative, |
| EXPECTED.get(relative), |
| )) |
|
|
| visual = None |
| if args.visual_model: |
| model_path = args.visual_model.resolve() |
| if model_path.suffix.lower() != ".tflite": |
| raise RuntimeError("visual model must be a .tflite file") |
| if "locked" in model_path.as_posix().lower(): |
| raise RuntimeError("locked-test paths are forbidden") |
| if not args.visual_report: |
| raise RuntimeError("--visual-report is required with --visual-model") |
| report_path = args.visual_report.resolve() |
| report = json.loads(report_path.read_text(encoding="utf-8")) |
| if report.get("status") != "PASS": |
| raise RuntimeError("visual export report is not PASS") |
| if report.get("candidate_id") != EXPECTED_CANDIDATE_ID: |
| raise RuntimeError("visual export report has the wrong candidate_id") |
| source = report.get("source", {}) |
| if source.get("checkpoint_sha256") != EXPECTED_VISUAL_CHECKPOINT_SHA256: |
| raise RuntimeError("visual export report has the wrong checkpoint hash") |
| parity = report.get("parity", {}) |
| if parity.get("status") != "PASS": |
| raise RuntimeError("visual export parity did not pass") |
| maximum_error = parity.get("maximum_absolute_error") |
| if not isinstance(maximum_error, (int, float)) or maximum_error > MAX_PARITY_ERROR: |
| raise RuntimeError("visual export parity exceeds the 1e-4 tolerance") |
| if report.get("locked_test_read_or_used") is not False: |
| raise RuntimeError("visual report does not attest locked-test non-use") |
| artifact = report.get("artifact", {}) |
| if artifact.get("sha256") != sha256(model_path): |
| raise RuntimeError("visual model hash does not match the export report") |
| if artifact.get("bytes") != model_path.stat().st_size: |
| raise RuntimeError("visual model size does not match the export report") |
| destination = assets / "models" / "gravekeeper_visual.tflite" |
| visual = copy_verified(model_path, destination) |
| records.append(visual) |
| records.append(copy_verified( |
| report_path, |
| assets / "models" / "visual_export_report.json", |
| )) |
|
|
| manifest = { |
| "format": "health_marketing_android_runtime_assets", |
| "version": 1, |
| "generated_at": datetime.now(timezone.utc).isoformat(), |
| "candidate_id": "day43-fusion-only-frozen-visual-v1", |
| "fusion_threshold": 0.48764924527277753, |
| "visual_model_present": visual is not None, |
| "human_labels_frozen": True, |
| "semantic_relabeling_performed": False, |
| "locked_test_read_or_used": False, |
| "quality_status": "QUALITY_GATE_FAILED_PENDING_FRESH_BLIND_ACCEPTANCE", |
| "day5_allowed": False, |
| "files": records, |
| } |
| manifest_path = assets / "config" / "runtime_asset_manifest.json" |
| manifest_path.parent.mkdir(parents=True, exist_ok=True) |
| manifest_path.write_text( |
| json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", |
| encoding="utf-8", |
| ) |
| print(json.dumps({ |
| "status": "STAGED", |
| "assets": str(assets), |
| "visual_model_present": visual is not None, |
| "file_count": len(records) + 1, |
| "manifest_sha256": sha256(manifest_path), |
| }, ensure_ascii=False, indent=2)) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|