tirtc-device-builder 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +36 -36
- package/bin/esp32-kit-metadata.js +7 -6
- package/bin/install-esp32-kit.js +2 -0
- package/package.json +1 -1
- package/skills/tirtc-esp32-builder/SKILL.md +4 -4
- package/skills/tirtc-esp32-builder/USAGE.md +1 -1
- package/skills/tirtc-esp32-builder/assets/board-audio-contract.example.json +66 -0
- package/skills/tirtc-esp32-builder/assets/board-video-contract.example.json +95 -0
- package/skills/tirtc-esp32-builder/assets/developer-intake-prompt.md +12 -7
- package/skills/tirtc-esp32-builder/assets/hardware-ir-v2.example.json +6 -0
- package/skills/tirtc-esp32-builder/assets/lckfb-szpi-esp32s3-portable-prompt.md +80 -0
- package/skills/tirtc-esp32-builder/assets/report-template.md +16 -1
- package/skills/tirtc-esp32-builder/assets/tirtc-runtime-contract.example.json +10 -0
- package/skills/tirtc-esp32-builder/references/audio-contract.md +51 -0
- package/skills/tirtc-esp32-builder/references/capability-rules.md +3 -3
- package/skills/tirtc-esp32-builder/references/environment.md +8 -0
- package/skills/tirtc-esp32-builder/references/hardware-ir.md +8 -2
- package/skills/tirtc-esp32-builder/references/reporting.md +4 -2
- package/skills/tirtc-esp32-builder/references/runtime-contract.md +34 -0
- package/skills/tirtc-esp32-builder/references/video-contract.md +31 -0
- package/skills/tirtc-esp32-builder/references/workflow.md +4 -2
- package/skills/tirtc-esp32-builder/scripts/audio_contract.py +320 -0
- package/skills/tirtc-esp32-builder/scripts/doctor.py +7 -1
- package/skills/tirtc-esp32-builder/scripts/hardware_ir.py +399 -6
- package/skills/tirtc-esp32-builder/scripts/install_audio_gate.py +66 -0
- package/skills/tirtc-esp32-builder/scripts/install_runtime_gate.py +66 -0
- package/skills/tirtc-esp32-builder/scripts/install_video_gate.py +66 -0
- package/skills/tirtc-esp32-builder/scripts/project_portability.py +86 -0
- package/skills/tirtc-esp32-builder/scripts/runtime_contract.py +314 -0
- package/skills/tirtc-esp32-builder/scripts/video_contract.py +407 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify a project-local TiRTC video pipeline contract."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
VIDEO_MEDIA = {
|
|
16
|
+
"mjpeg": "TIRTC_VIDEO_JPEG",
|
|
17
|
+
"h264": "TIRTC_VIDEO_H264",
|
|
18
|
+
"h265": "TIRTC_VIDEO_H265",
|
|
19
|
+
}
|
|
20
|
+
VIDEO_BOUNDARY = {
|
|
21
|
+
"mjpeg": "complete_jpeg",
|
|
22
|
+
"h264": "annex_b_access_unit",
|
|
23
|
+
"h265": "annex_b_access_unit",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sha256_file(path: Path) -> str:
|
|
28
|
+
digest = hashlib.sha256()
|
|
29
|
+
with path.open("rb") as stream:
|
|
30
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
31
|
+
digest.update(chunk)
|
|
32
|
+
return digest.hexdigest()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def project_file(project: Path, value: Any, label: str) -> Path:
|
|
36
|
+
if not isinstance(value, str) or not value.strip():
|
|
37
|
+
raise ValueError(f"{label} must be a non-empty project-relative path")
|
|
38
|
+
relative = Path(value)
|
|
39
|
+
if relative.is_absolute():
|
|
40
|
+
raise ValueError(f"{label} must be project-relative, got {value!r}")
|
|
41
|
+
resolved = (project / relative).resolve()
|
|
42
|
+
if project != resolved and project not in resolved.parents:
|
|
43
|
+
raise ValueError(f"{label} escapes project root: {value!r}")
|
|
44
|
+
return resolved
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def mapping(value: Any, label: str, errors: list[str]) -> dict[str, Any]:
|
|
48
|
+
if not isinstance(value, dict):
|
|
49
|
+
errors.append(f"{label} must be an object")
|
|
50
|
+
return {}
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def positive_int(value: Any, label: str, errors: list[str]) -> int | None:
|
|
55
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
56
|
+
errors.append(f"{label} must be a positive integer")
|
|
57
|
+
return None
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def lock_versions(lock_text: str) -> dict[str, str]:
|
|
62
|
+
versions: dict[str, str] = {}
|
|
63
|
+
current: str | None = None
|
|
64
|
+
for line in lock_text.splitlines():
|
|
65
|
+
match = re.match(r"^ ([^ ].*):$", line)
|
|
66
|
+
if match:
|
|
67
|
+
current = match.group(1)
|
|
68
|
+
continue
|
|
69
|
+
if current is not None:
|
|
70
|
+
version = re.match(r"^ version:\s*['\"]?([^'\"\s]+)", line)
|
|
71
|
+
if version:
|
|
72
|
+
versions[current] = version.group(1)
|
|
73
|
+
current = None
|
|
74
|
+
return versions
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_dependencies(
|
|
78
|
+
project: Path,
|
|
79
|
+
contract: dict[str, Any],
|
|
80
|
+
errors: list[str],
|
|
81
|
+
inputs: dict[str, str],
|
|
82
|
+
) -> None:
|
|
83
|
+
dependencies = contract.get("dependencies")
|
|
84
|
+
if not isinstance(dependencies, dict) or not dependencies:
|
|
85
|
+
errors.append("dependencies must map component names to exact locked versions")
|
|
86
|
+
return
|
|
87
|
+
lock = project / "dependencies.lock"
|
|
88
|
+
if not lock.is_file():
|
|
89
|
+
errors.append("dependencies.lock is missing")
|
|
90
|
+
return
|
|
91
|
+
text = lock.read_text(encoding="utf-8", errors="replace")
|
|
92
|
+
inputs["dependencies.lock"] = sha256_file(lock)
|
|
93
|
+
locked = lock_versions(text)
|
|
94
|
+
for name, expected in dependencies.items():
|
|
95
|
+
if not isinstance(name, str) or not isinstance(expected, str) or not expected:
|
|
96
|
+
errors.append("dependencies entries must be non-empty string pairs")
|
|
97
|
+
elif locked.get(name) != expected:
|
|
98
|
+
errors.append(
|
|
99
|
+
f"locked dependency mismatch for {name}: "
|
|
100
|
+
f"expected {expected}, got {locked.get(name, 'missing')}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def check_platform_contract(
|
|
105
|
+
project: Path,
|
|
106
|
+
contract: dict[str, Any],
|
|
107
|
+
errors: list[str],
|
|
108
|
+
inputs: dict[str, str],
|
|
109
|
+
) -> None:
|
|
110
|
+
try:
|
|
111
|
+
path = project_file(
|
|
112
|
+
project, contract.get("platform_contract"), "platform_contract"
|
|
113
|
+
)
|
|
114
|
+
except ValueError as exc:
|
|
115
|
+
errors.append(str(exc))
|
|
116
|
+
return
|
|
117
|
+
if not path.is_file():
|
|
118
|
+
errors.append(f"platform media contract does not exist: {path}")
|
|
119
|
+
return
|
|
120
|
+
try:
|
|
121
|
+
platform = json.loads(path.read_text(encoding="utf-8"))
|
|
122
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
123
|
+
errors.append(f"invalid platform media contract: {exc}")
|
|
124
|
+
return
|
|
125
|
+
inputs[str(path.relative_to(project))] = sha256_file(path)
|
|
126
|
+
if not isinstance(platform, dict) or platform.get("schema_version") != 1:
|
|
127
|
+
errors.append("platform media contract schema_version must be 1")
|
|
128
|
+
return
|
|
129
|
+
h5 = platform.get("h5")
|
|
130
|
+
video = h5.get("video_up") if isinstance(h5, dict) else None
|
|
131
|
+
if not isinstance(video, dict) or video.get("stream_id") != 11:
|
|
132
|
+
errors.append("platform media contract must define H5 video_up stream 11")
|
|
133
|
+
return
|
|
134
|
+
profiles = video.get("supported_profiles")
|
|
135
|
+
if not isinstance(profiles, list):
|
|
136
|
+
errors.append("platform H5 video supported_profiles must be an array")
|
|
137
|
+
return
|
|
138
|
+
supported: dict[str, dict[str, Any]] = {}
|
|
139
|
+
for index, item in enumerate(profiles):
|
|
140
|
+
if not isinstance(item, dict):
|
|
141
|
+
errors.append(
|
|
142
|
+
f"platform supported_profiles[{index}] must be an object"
|
|
143
|
+
)
|
|
144
|
+
continue
|
|
145
|
+
codec = item.get("codec")
|
|
146
|
+
if codec not in VIDEO_MEDIA:
|
|
147
|
+
errors.append(
|
|
148
|
+
f"platform supported_profiles[{index}].codec is unsupported"
|
|
149
|
+
)
|
|
150
|
+
continue
|
|
151
|
+
if codec in supported:
|
|
152
|
+
errors.append(f"platform video profile {codec} is duplicated")
|
|
153
|
+
continue
|
|
154
|
+
if item.get("media") != VIDEO_MEDIA[codec]:
|
|
155
|
+
errors.append(
|
|
156
|
+
f"platform video profile {codec} media must be {VIDEO_MEDIA[codec]}"
|
|
157
|
+
)
|
|
158
|
+
if item.get("send_boundary") != VIDEO_BOUNDARY[codec]:
|
|
159
|
+
errors.append(
|
|
160
|
+
f"platform video profile {codec} boundary must be "
|
|
161
|
+
f"{VIDEO_BOUNDARY[codec]}"
|
|
162
|
+
)
|
|
163
|
+
supported[codec] = item
|
|
164
|
+
camera = contract.get("camera")
|
|
165
|
+
selected = camera.get("codec") if isinstance(camera, dict) else None
|
|
166
|
+
if selected in VIDEO_MEDIA and selected not in supported:
|
|
167
|
+
errors.append(
|
|
168
|
+
f"board-selected codec {selected} is not supported by platform contract"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def check_scheduler(
|
|
173
|
+
project: Path,
|
|
174
|
+
contract: dict[str, Any],
|
|
175
|
+
errors: list[str],
|
|
176
|
+
inputs: dict[str, str],
|
|
177
|
+
) -> None:
|
|
178
|
+
scheduler = mapping(contract.get("scheduler"), "scheduler", errors)
|
|
179
|
+
wifi_core = scheduler.get("wifi_core")
|
|
180
|
+
camera_core = scheduler.get("camera_core")
|
|
181
|
+
for value, label in ((wifi_core, "wifi_core"), (camera_core, "camera_core")):
|
|
182
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
183
|
+
errors.append(f"scheduler.{label} must be an integer >= 0")
|
|
184
|
+
if isinstance(wifi_core, int) and isinstance(camera_core, int) and wifi_core == camera_core:
|
|
185
|
+
errors.append("camera event processing must not share the configured Wi-Fi core")
|
|
186
|
+
config_files = scheduler.get("config_files")
|
|
187
|
+
if not isinstance(config_files, list) or not config_files:
|
|
188
|
+
errors.append("scheduler.config_files must be a non-empty array")
|
|
189
|
+
return
|
|
190
|
+
for index, value in enumerate(config_files):
|
|
191
|
+
try:
|
|
192
|
+
config = project_file(project, value, f"scheduler.config_files[{index}]")
|
|
193
|
+
except ValueError as exc:
|
|
194
|
+
errors.append(str(exc))
|
|
195
|
+
continue
|
|
196
|
+
if not config.is_file():
|
|
197
|
+
errors.append(f"scheduler config does not exist: {config}")
|
|
198
|
+
continue
|
|
199
|
+
text = config.read_text(encoding="utf-8", errors="replace")
|
|
200
|
+
inputs[str(config.relative_to(project))] = sha256_file(config)
|
|
201
|
+
if isinstance(camera_core, int) and f"CONFIG_CAMERA_CORE{camera_core}=y" not in text:
|
|
202
|
+
errors.append(f"{config.relative_to(project)} does not select camera core {camera_core}")
|
|
203
|
+
if isinstance(wifi_core, int) and f"CONFIG_CAMERA_CORE{wifi_core}=y" in text:
|
|
204
|
+
errors.append(f"{config.relative_to(project)} pins camera processing to Wi-Fi core {wifi_core}")
|
|
205
|
+
wifi_config_files = scheduler.get("wifi_config_files")
|
|
206
|
+
if not isinstance(wifi_config_files, list) or not wifi_config_files:
|
|
207
|
+
errors.append("scheduler.wifi_config_files must be a non-empty array")
|
|
208
|
+
return
|
|
209
|
+
for index, value in enumerate(wifi_config_files):
|
|
210
|
+
try:
|
|
211
|
+
config = project_file(
|
|
212
|
+
project, value, f"scheduler.wifi_config_files[{index}]"
|
|
213
|
+
)
|
|
214
|
+
except ValueError as exc:
|
|
215
|
+
errors.append(str(exc))
|
|
216
|
+
continue
|
|
217
|
+
if not config.is_file():
|
|
218
|
+
errors.append(f"Wi-Fi scheduler config does not exist: {config}")
|
|
219
|
+
continue
|
|
220
|
+
text = config.read_text(encoding="utf-8", errors="replace")
|
|
221
|
+
inputs[str(config.relative_to(project))] = sha256_file(config)
|
|
222
|
+
if (
|
|
223
|
+
isinstance(wifi_core, int)
|
|
224
|
+
and f"CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_{wifi_core}=y" not in text
|
|
225
|
+
):
|
|
226
|
+
errors.append(
|
|
227
|
+
f"{config.relative_to(project)} does not pin Wi-Fi task to core {wifi_core}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def check_pipeline(contract: dict[str, Any], errors: list[str]) -> None:
|
|
232
|
+
camera = mapping(contract.get("camera"), "camera", errors)
|
|
233
|
+
codec = camera.get("codec")
|
|
234
|
+
if codec not in VIDEO_MEDIA:
|
|
235
|
+
errors.append("camera.codec must be one of h264, h265, mjpeg")
|
|
236
|
+
else:
|
|
237
|
+
boundary_field = (
|
|
238
|
+
"complete_jpeg_per_send"
|
|
239
|
+
if codec == "mjpeg"
|
|
240
|
+
else "complete_access_unit_per_send"
|
|
241
|
+
)
|
|
242
|
+
if camera.get(boundary_field) is not True:
|
|
243
|
+
errors.append(f"camera.{boundary_field} must be true")
|
|
244
|
+
expected_media = VIDEO_MEDIA[codec]
|
|
245
|
+
if camera.get("media") != expected_media:
|
|
246
|
+
errors.append(f"camera.media must be {expected_media} for {codec}")
|
|
247
|
+
if camera.get("stream_id") != 11:
|
|
248
|
+
errors.append("camera.stream_id must be 11 for the H5 video contract")
|
|
249
|
+
frame_buffers = positive_int(camera.get("frame_buffers"), "camera.frame_buffers", errors)
|
|
250
|
+
if frame_buffers is not None and frame_buffers < 2:
|
|
251
|
+
errors.append("camera.frame_buffers must be at least 2 for the selected realtime pipeline")
|
|
252
|
+
accepted_pids = camera.get("accepted_sensor_pids")
|
|
253
|
+
if (
|
|
254
|
+
not isinstance(accepted_pids, list)
|
|
255
|
+
or not accepted_pids
|
|
256
|
+
or any(
|
|
257
|
+
isinstance(pid, bool)
|
|
258
|
+
or not isinstance(pid, int)
|
|
259
|
+
or pid < 0
|
|
260
|
+
or pid > 0xFFFF
|
|
261
|
+
for pid in accepted_pids
|
|
262
|
+
)
|
|
263
|
+
or len(set(accepted_pids)) != len(accepted_pids)
|
|
264
|
+
):
|
|
265
|
+
errors.append(
|
|
266
|
+
"camera.accepted_sensor_pids must be a non-empty unique uint16 array"
|
|
267
|
+
)
|
|
268
|
+
if camera.get("unknown_sensor_policy") != "reject":
|
|
269
|
+
errors.append("camera.unknown_sensor_policy must be reject")
|
|
270
|
+
|
|
271
|
+
memory = mapping(contract.get("memory"), "memory", errors)
|
|
272
|
+
send_buffer = positive_int(
|
|
273
|
+
memory.get("max_send_buffer_bytes"), "memory.max_send_buffer_bytes", errors
|
|
274
|
+
)
|
|
275
|
+
frame_field = (
|
|
276
|
+
"max_complete_jpeg_bytes"
|
|
277
|
+
if codec == "mjpeg"
|
|
278
|
+
else "max_complete_access_unit_bytes"
|
|
279
|
+
)
|
|
280
|
+
complete_frame = positive_int(
|
|
281
|
+
memory.get(frame_field), f"memory.{frame_field}", errors
|
|
282
|
+
)
|
|
283
|
+
backpressure = positive_int(
|
|
284
|
+
memory.get("video_backpressure_bytes"), "memory.video_backpressure_bytes", errors
|
|
285
|
+
)
|
|
286
|
+
if None not in (send_buffer, complete_frame, backpressure):
|
|
287
|
+
if complete_frame > backpressure:
|
|
288
|
+
errors.append(
|
|
289
|
+
"one complete encoded frame/access unit must fit below the "
|
|
290
|
+
"video backpressure threshold"
|
|
291
|
+
)
|
|
292
|
+
if backpressure >= send_buffer:
|
|
293
|
+
errors.append("video backpressure threshold must be lower than max send buffer")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def check_assertions(
|
|
297
|
+
project: Path,
|
|
298
|
+
contract: dict[str, Any],
|
|
299
|
+
errors: list[str],
|
|
300
|
+
inputs: dict[str, str],
|
|
301
|
+
) -> None:
|
|
302
|
+
assertions = contract.get("implementation_assertions")
|
|
303
|
+
if not isinstance(assertions, list) or not assertions:
|
|
304
|
+
errors.append("implementation_assertions must be a non-empty array")
|
|
305
|
+
return
|
|
306
|
+
for index, item in enumerate(assertions):
|
|
307
|
+
label = f"implementation_assertions[{index}]"
|
|
308
|
+
assertion = mapping(item, label, errors)
|
|
309
|
+
try:
|
|
310
|
+
source = project_file(project, assertion.get("file"), f"{label}.file")
|
|
311
|
+
except ValueError as exc:
|
|
312
|
+
errors.append(str(exc))
|
|
313
|
+
continue
|
|
314
|
+
if not source.is_file():
|
|
315
|
+
errors.append(f"{label}.file does not exist: {source}")
|
|
316
|
+
continue
|
|
317
|
+
text = source.read_text(encoding="utf-8", errors="replace")
|
|
318
|
+
compact = re.sub(r"\s+", "", text)
|
|
319
|
+
inputs[str(source.relative_to(project))] = sha256_file(source)
|
|
320
|
+
for field, haystack, should_exist in (
|
|
321
|
+
("contains", text, True),
|
|
322
|
+
("contains_compact", compact, True),
|
|
323
|
+
("absent", text, False),
|
|
324
|
+
("absent_compact", compact, False),
|
|
325
|
+
):
|
|
326
|
+
needles = assertion.get(field, [])
|
|
327
|
+
if not isinstance(needles, list) or any(
|
|
328
|
+
not isinstance(needle, str) or not needle for needle in needles
|
|
329
|
+
):
|
|
330
|
+
errors.append(f"{label}.{field} must be an array of non-empty strings")
|
|
331
|
+
continue
|
|
332
|
+
for needle in needles:
|
|
333
|
+
found = needle in haystack
|
|
334
|
+
if found != should_exist:
|
|
335
|
+
verb = "missing" if should_exist else "contains forbidden"
|
|
336
|
+
errors.append(f"{label} {verb} {field} token: {needle}")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def verify_contract(contract_path: Path, project_path: Path) -> dict[str, Any]:
|
|
340
|
+
project = project_path.expanduser().resolve()
|
|
341
|
+
contract_file = contract_path.expanduser().resolve()
|
|
342
|
+
if not project.is_dir():
|
|
343
|
+
return {"ok": False, "errors": [f"project directory does not exist: {project}"]}
|
|
344
|
+
if not contract_file.is_file():
|
|
345
|
+
return {"ok": False, "errors": [f"video contract does not exist: {contract_file}"]}
|
|
346
|
+
if project != contract_file and project not in contract_file.parents:
|
|
347
|
+
return {"ok": False, "errors": ["video contract must be inside the project"]}
|
|
348
|
+
try:
|
|
349
|
+
contract = json.loads(contract_file.read_text(encoding="utf-8"))
|
|
350
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
351
|
+
return {"ok": False, "errors": [f"invalid video contract: {exc}"]}
|
|
352
|
+
if not isinstance(contract, dict):
|
|
353
|
+
return {"ok": False, "errors": ["video contract root must be an object"]}
|
|
354
|
+
errors: list[str] = []
|
|
355
|
+
inputs = {str(contract_file.relative_to(project)): sha256_file(contract_file)}
|
|
356
|
+
if contract.get("schema_version") != 1:
|
|
357
|
+
errors.append("schema_version must be 1")
|
|
358
|
+
evidence = contract.get("evidence")
|
|
359
|
+
if not isinstance(evidence, list) or len(evidence) < 2 or any(
|
|
360
|
+
not isinstance(item, str) or not item for item in evidence
|
|
361
|
+
):
|
|
362
|
+
errors.append("evidence must contain at least two non-empty source IDs")
|
|
363
|
+
check_dependencies(project, contract, errors, inputs)
|
|
364
|
+
check_platform_contract(project, contract, errors, inputs)
|
|
365
|
+
check_scheduler(project, contract, errors, inputs)
|
|
366
|
+
check_pipeline(contract, errors)
|
|
367
|
+
check_assertions(project, contract, errors, inputs)
|
|
368
|
+
camera = contract.get("camera", {})
|
|
369
|
+
memory = contract.get("memory", {})
|
|
370
|
+
return {
|
|
371
|
+
"ok": not errors,
|
|
372
|
+
"summary": (
|
|
373
|
+
f"codec={camera.get('codec')} stream={camera.get('stream_id')} "
|
|
374
|
+
f"buffers={camera.get('frame_buffers')} "
|
|
375
|
+
f"send_buffer={memory.get('max_send_buffer_bytes')}"
|
|
376
|
+
),
|
|
377
|
+
"inputs": inputs,
|
|
378
|
+
"errors": errors,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def main() -> int:
|
|
383
|
+
parser = argparse.ArgumentParser(
|
|
384
|
+
description="Verify locked camera dependencies, scheduler isolation, JPEG framing, memory, and adapter assertions."
|
|
385
|
+
)
|
|
386
|
+
parser.add_argument("contract", type=Path)
|
|
387
|
+
parser.add_argument("--project", type=Path, required=True)
|
|
388
|
+
parser.add_argument("--json", action="store_true")
|
|
389
|
+
parser.add_argument("--evidence-out", type=Path)
|
|
390
|
+
args = parser.parse_args()
|
|
391
|
+
result = verify_contract(args.contract, args.project)
|
|
392
|
+
if args.evidence_out is not None:
|
|
393
|
+
output = args.evidence_out.expanduser().resolve()
|
|
394
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
395
|
+
output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
396
|
+
if args.json:
|
|
397
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
398
|
+
elif result["ok"]:
|
|
399
|
+
print(f"PASS: video semantic contract: {result['summary']}")
|
|
400
|
+
else:
|
|
401
|
+
for error in result["errors"]:
|
|
402
|
+
print(f"FAIL: {error}", file=sys.stderr)
|
|
403
|
+
return 0 if result["ok"] else 3
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
if __name__ == "__main__":
|
|
407
|
+
raise SystemExit(main())
|