constructelligence commited on
Commit
495ab9b
·
verified ·
1 Parent(s): 967c5eb

Upload release_check.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. release_check.py +125 -0
release_check.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Is this release deployable? Checks the checkpoint and the code set before anything is published.
3
+
4
+ Two failures this exists to stop (both happened):
5
+
6
+ * **A checkpoint the robot cannot use.** Without a condition head the safety gate
7
+ rejects every frame ("surface condition not assessed"); without a completion
8
+ threshold sign-off is disabled; without temperatures the confidence thresholds
9
+ mean nothing; without ``condition_operating_point`` damage flags run on
10
+ uncalibrated defaults that flag nearly every real photo.
11
+ * **Code that does not import.** A hand-kept file list drifted behind ``predict.py``'s
12
+ imports. ``release_files()`` takes every tracked top-level module instead, and
13
+ ``missing_imports()`` proves each published module's local imports are published too.
14
+
15
+ Usage::
16
+
17
+ python3 release_check.py --checkpoint artifacts/candidates/ft4-cal.pt # exit 1 = not deployable
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import ast
23
+ import json
24
+ import subprocess
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ PROJECT = Path(__file__).resolve().parent
29
+ # Data-collection and one-off scripts that are not part of the runtime kit.
30
+ EXCLUDE = {"fetch_construction_data.py", "fetch_surface_eval.py"}
31
+ EXTRA = ["requirements.txt", "metadata_template.csv", "console/index.html", "console/app.js", "console/style.css"]
32
+
33
+
34
+ def tracked_files(project=PROJECT):
35
+ out = subprocess.run(["git", "ls-files"], cwd=project, capture_output=True, text=True, check=True).stdout
36
+ return set(out.split())
37
+
38
+
39
+ def release_files(project=PROJECT):
40
+ """Every tracked top-level module (and its tests), minus data scripts, plus runtime assets."""
41
+ tracked = tracked_files(project)
42
+ modules = sorted(f for f in tracked if f.endswith(".py") and "/" not in f and f not in EXCLUDE)
43
+ return modules + [f for f in EXTRA if f in tracked]
44
+
45
+
46
+ def missing_imports(files, project=PROJECT):
47
+ """{published module: [local modules it imports that are not published]}."""
48
+ local = {p.stem for p in project.glob("*.py")}
49
+ published = {Path(f).stem for f in files if f.endswith(".py")}
50
+ gaps = {}
51
+ for name in files:
52
+ if not name.endswith(".py"):
53
+ continue
54
+ tree = ast.parse((project / name).read_text(), filename=name)
55
+ imported = set()
56
+ for node in ast.walk(tree):
57
+ if isinstance(node, ast.Import):
58
+ imported.update(alias.name.split(".")[0] for alias in node.names)
59
+ elif isinstance(node, ast.ImportFrom) and node.module and not node.level:
60
+ imported.add(node.module.split(".")[0])
61
+ missing = sorted((imported & local) - published)
62
+ if missing:
63
+ gaps[name] = missing
64
+ return gaps
65
+
66
+
67
+ def check_checkpoint(checkpoint):
68
+ """``{"errors": [...], "warnings": [...]}`` for a loaded checkpoint dict."""
69
+ from label_schema import CLASSES
70
+ errors, warnings = [], []
71
+ if tuple(checkpoint.get("classes") or ()) != tuple(CLASSES):
72
+ errors.append("class schema does not match label_schema.CLASSES")
73
+ if not checkpoint.get("condition_classes"):
74
+ errors.append("no surface-condition head: the gate rejects every frame (condition not assessed)")
75
+ if checkpoint.get("temperature") is None:
76
+ errors.append("no temperature calibration: run train.py --calibrate-checkpoint")
77
+ if checkpoint.get("completion_threshold") is None:
78
+ errors.append("no completion threshold: sign-off is disabled (train.py --calibrate-checkpoint)")
79
+ point = checkpoint.get("condition_operating_point")
80
+ if checkpoint.get("condition_classes") and not point:
81
+ errors.append("no condition_operating_point: damage flags use uncalibrated defaults "
82
+ "(calibrate_condition.py --write)")
83
+ elif point and point.get("false_alarm_set") != "real":
84
+ warnings.append("condition operating point was calibrated without real sound walls")
85
+ if "wallpaper" not in (checkpoint.get("material_classes") or []):
86
+ warnings.append("no wallpaper material class: wallpaper rests on the pattern cue alone")
87
+ return {"errors": errors, "warnings": warnings}
88
+
89
+
90
+ def run(checkpoint_path=None, project=PROJECT):
91
+ report = {"files": release_files(project)}
92
+ report["missing_imports"] = missing_imports(report["files"], project)
93
+ report["errors"] = [f"{m} imports unpublished {', '.join(g)}" for m, g in report["missing_imports"].items()]
94
+ report["warnings"] = []
95
+ if checkpoint_path:
96
+ import torch
97
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
98
+ result = check_checkpoint(checkpoint)
99
+ report["errors"] += result["errors"]
100
+ report["warnings"] += result["warnings"]
101
+ report["checkpoint"] = str(checkpoint_path)
102
+ report["deployable"] = not report["errors"]
103
+ return report
104
+
105
+
106
+ def main():
107
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
108
+ ap.add_argument("--checkpoint", type=Path)
109
+ ap.add_argument("--json", action="store_true")
110
+ args = ap.parse_args()
111
+ report = run(args.checkpoint)
112
+ if args.json:
113
+ print(json.dumps(report, indent=2))
114
+ else:
115
+ print(f"{len(report['files'])} files in the release")
116
+ for e in report["errors"]:
117
+ print("ERROR ", e)
118
+ for w in report["warnings"]:
119
+ print("warning", w)
120
+ print("deployable" if report["deployable"] else "NOT deployable")
121
+ sys.exit(0 if report["deployable"] else 1)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()