tirtc-device-builder 0.2.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 +36 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/NOTICE +7 -0
- package/README.md +604 -0
- package/SECURITY.md +15 -0
- package/bin/tirtc-device-builder.js +214 -0
- package/package.json +56 -0
- package/skills/tirtc-esp32-builder/SKILL.md +34 -0
- package/skills/tirtc-esp32-builder/USAGE.md +94 -0
- package/skills/tirtc-esp32-builder/agents/openai.yaml +4 -0
- package/skills/tirtc-esp32-builder/assets/hardware-ir.example.json +78 -0
- package/skills/tirtc-esp32-builder/assets/report-template.md +55 -0
- package/skills/tirtc-esp32-builder/references/capability-rules.md +32 -0
- package/skills/tirtc-esp32-builder/references/environment.md +54 -0
- package/skills/tirtc-esp32-builder/references/hardware-ir.md +49 -0
- package/skills/tirtc-esp32-builder/references/reporting.md +23 -0
- package/skills/tirtc-esp32-builder/references/workflow.md +70 -0
- package/skills/tirtc-esp32-builder/scripts/doctor.py +441 -0
- package/skills/tirtc-esp32-builder/scripts/hardware_ir.py +425 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create, validate, and assess TiRTC embedded Hardware IR files."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
FEATURES = {
|
|
15
|
+
"h5_live_audio",
|
|
16
|
+
"h5_live_video",
|
|
17
|
+
"h5_talkback",
|
|
18
|
+
"ai_talk",
|
|
19
|
+
}
|
|
20
|
+
VERIFICATION_LEVELS = {
|
|
21
|
+
"extracted": 1,
|
|
22
|
+
"corroborated": 2,
|
|
23
|
+
"build_verified": 3,
|
|
24
|
+
"hardware_verified": 4,
|
|
25
|
+
"hil_verified": 5,
|
|
26
|
+
}
|
|
27
|
+
READY_STATUSES = {"READY_TO_PORT", "HIL_VERIFIED"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_ir(path: Path) -> dict[str, Any]:
|
|
31
|
+
try:
|
|
32
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
except FileNotFoundError as exc:
|
|
34
|
+
raise ValueError(f"Hardware IR does not exist: {path}") from exc
|
|
35
|
+
except json.JSONDecodeError as exc:
|
|
36
|
+
raise ValueError(f"invalid JSON at line {exc.lineno}: {exc.msg}") from exc
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
raise ValueError("Hardware IR root must be an object")
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def mapping(value: Any, path: str, errors: list[str]) -> dict[str, Any]:
|
|
43
|
+
if not isinstance(value, dict):
|
|
44
|
+
errors.append(f"{path} must be an object")
|
|
45
|
+
return {}
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def nonempty_string(value: Any, path: str, errors: list[str]) -> None:
|
|
50
|
+
if not isinstance(value, str) or not value.strip():
|
|
51
|
+
errors.append(f"{path} must be a non-empty string")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def positive_int(value: Any, path: str, errors: list[str], allow_zero: bool = False) -> None:
|
|
55
|
+
minimum = 0 if allow_zero else 1
|
|
56
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
|
57
|
+
errors.append(f"{path} must be an integer >= {minimum}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def nullable_bool(value: Any, path: str, errors: list[str]) -> None:
|
|
61
|
+
if value is not None and not isinstance(value, bool):
|
|
62
|
+
errors.append(f"{path} must be true, false, or null")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def validate_source_refs(
|
|
66
|
+
section: dict[str, Any], path: str, source_ids: set[str], errors: list[str]
|
|
67
|
+
) -> None:
|
|
68
|
+
refs = section.get("source_refs")
|
|
69
|
+
if not isinstance(refs, list) or not refs:
|
|
70
|
+
errors.append(f"{path}.source_refs must be a non-empty array")
|
|
71
|
+
return
|
|
72
|
+
for index, ref in enumerate(refs):
|
|
73
|
+
if not isinstance(ref, str) or not ref:
|
|
74
|
+
errors.append(f"{path}.source_refs[{index}] must be a non-empty string")
|
|
75
|
+
elif ref not in source_ids:
|
|
76
|
+
errors.append(f"{path}.source_refs[{index}] references unknown source {ref!r}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def validate_codec_list(value: Any, path: str, errors: list[str]) -> None:
|
|
80
|
+
if not isinstance(value, list):
|
|
81
|
+
errors.append(f"{path} must be an array")
|
|
82
|
+
return
|
|
83
|
+
for index, item in enumerate(value):
|
|
84
|
+
prefix = f"{path}[{index}]"
|
|
85
|
+
codec = mapping(item, prefix, errors)
|
|
86
|
+
nonempty_string(codec.get("name"), f"{prefix}.name", errors)
|
|
87
|
+
rates = codec.get("sample_rates_hz")
|
|
88
|
+
if not isinstance(rates, list) or not rates:
|
|
89
|
+
errors.append(f"{prefix}.sample_rates_hz must be a non-empty array")
|
|
90
|
+
else:
|
|
91
|
+
for rate_index, rate in enumerate(rates):
|
|
92
|
+
positive_int(rate, f"{prefix}.sample_rates_hz[{rate_index}]", errors)
|
|
93
|
+
verification = codec.get("verification")
|
|
94
|
+
if verification not in VERIFICATION_LEVELS:
|
|
95
|
+
errors.append(
|
|
96
|
+
f"{prefix}.verification must be one of "
|
|
97
|
+
+ ", ".join(VERIFICATION_LEVELS)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_ir(data: dict[str, Any]) -> list[str]:
|
|
102
|
+
errors: list[str] = []
|
|
103
|
+
if data.get("schema_version") != 1:
|
|
104
|
+
errors.append("schema_version must be 1")
|
|
105
|
+
|
|
106
|
+
board = mapping(data.get("board"), "board", errors)
|
|
107
|
+
for key in ("id", "vendor", "model", "hardware_revision"):
|
|
108
|
+
nonempty_string(board.get(key), f"board.{key}", errors)
|
|
109
|
+
|
|
110
|
+
sources = data.get("sources")
|
|
111
|
+
source_ids: set[str] = set()
|
|
112
|
+
if not isinstance(sources, list) or not sources:
|
|
113
|
+
errors.append("sources must be a non-empty array")
|
|
114
|
+
else:
|
|
115
|
+
for index, item in enumerate(sources):
|
|
116
|
+
prefix = f"sources[{index}]"
|
|
117
|
+
source = mapping(item, prefix, errors)
|
|
118
|
+
for key in ("id", "kind", "location"):
|
|
119
|
+
nonempty_string(source.get(key), f"{prefix}.{key}", errors)
|
|
120
|
+
source_id = source.get("id")
|
|
121
|
+
if isinstance(source_id, str) and source_id:
|
|
122
|
+
if source_id in source_ids:
|
|
123
|
+
errors.append(f"duplicate source id {source_id!r}")
|
|
124
|
+
source_ids.add(source_id)
|
|
125
|
+
|
|
126
|
+
soc = mapping(data.get("soc"), "soc", errors)
|
|
127
|
+
nonempty_string(soc.get("target"), "soc.target", errors)
|
|
128
|
+
nonempty_string(soc.get("module"), "soc.module", errors)
|
|
129
|
+
positive_int(soc.get("flash_mb"), "soc.flash_mb", errors)
|
|
130
|
+
positive_int(soc.get("psram_mb"), "soc.psram_mb", errors, allow_zero=True)
|
|
131
|
+
validate_source_refs(soc, "soc", source_ids, errors)
|
|
132
|
+
|
|
133
|
+
toolchain = mapping(data.get("toolchain"), "toolchain", errors)
|
|
134
|
+
nonempty_string(toolchain.get("framework"), "toolchain.framework", errors)
|
|
135
|
+
nonempty_string(
|
|
136
|
+
toolchain.get("framework_version"), "toolchain.framework_version", errors
|
|
137
|
+
)
|
|
138
|
+
toolchain_verification = toolchain.get("verification")
|
|
139
|
+
if toolchain_verification not in VERIFICATION_LEVELS:
|
|
140
|
+
errors.append(
|
|
141
|
+
"toolchain.verification must be one of "
|
|
142
|
+
+ ", ".join(VERIFICATION_LEVELS)
|
|
143
|
+
)
|
|
144
|
+
tirtc = mapping(toolchain.get("tirtc"), "toolchain.tirtc", errors)
|
|
145
|
+
for key in ("platform", "version", "sdk_path", "build_contract"):
|
|
146
|
+
nonempty_string(tirtc.get(key), f"toolchain.tirtc.{key}", errors)
|
|
147
|
+
validate_source_refs(toolchain, "toolchain", source_ids, errors)
|
|
148
|
+
|
|
149
|
+
camera = mapping(data.get("camera"), "camera", errors)
|
|
150
|
+
nullable_bool(camera.get("present"), "camera.present", errors)
|
|
151
|
+
h264 = mapping(camera.get("h264"), "camera.h264", errors)
|
|
152
|
+
nullable_bool(h264.get("available"), "camera.h264.available", errors)
|
|
153
|
+
nullable_bool(
|
|
154
|
+
h264.get("key_frame_control"), "camera.h264.key_frame_control", errors
|
|
155
|
+
)
|
|
156
|
+
verification = h264.get("verification")
|
|
157
|
+
if verification not in VERIFICATION_LEVELS:
|
|
158
|
+
errors.append(
|
|
159
|
+
"camera.h264.verification must be one of "
|
|
160
|
+
+ ", ".join(VERIFICATION_LEVELS)
|
|
161
|
+
)
|
|
162
|
+
validate_source_refs(camera, "camera", source_ids, errors)
|
|
163
|
+
|
|
164
|
+
for name in ("audio_input", "audio_output"):
|
|
165
|
+
media = mapping(data.get(name), name, errors)
|
|
166
|
+
nullable_bool(media.get("present"), f"{name}.present", errors)
|
|
167
|
+
validate_codec_list(media.get("codecs"), f"{name}.codecs", errors)
|
|
168
|
+
validate_source_refs(media, name, source_ids, errors)
|
|
169
|
+
|
|
170
|
+
features = mapping(data.get("features"), "features", errors)
|
|
171
|
+
requested = features.get("requested")
|
|
172
|
+
if not isinstance(requested, list) or not requested:
|
|
173
|
+
errors.append("features.requested must be a non-empty array")
|
|
174
|
+
else:
|
|
175
|
+
seen: set[str] = set()
|
|
176
|
+
for index, feature in enumerate(requested):
|
|
177
|
+
if feature not in FEATURES:
|
|
178
|
+
errors.append(
|
|
179
|
+
f"features.requested[{index}] must be one of "
|
|
180
|
+
+ ", ".join(sorted(FEATURES))
|
|
181
|
+
)
|
|
182
|
+
elif feature in seen:
|
|
183
|
+
errors.append(f"features.requested contains duplicate {feature!r}")
|
|
184
|
+
else:
|
|
185
|
+
seen.add(feature)
|
|
186
|
+
return errors
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def codec_requirement(media: dict[str, Any], section: str) -> tuple[str, str, int]:
|
|
190
|
+
present = media.get("present")
|
|
191
|
+
if present is None:
|
|
192
|
+
return "NEEDS_CONFIRMATION", f"{section} presence is unknown", 0
|
|
193
|
+
if present is False:
|
|
194
|
+
return "BLOCKED", f"{section} is not present", 0
|
|
195
|
+
codecs = media.get("codecs", [])
|
|
196
|
+
for codec in codecs:
|
|
197
|
+
if not isinstance(codec, dict) or str(codec.get("name", "")).lower() != "alaw":
|
|
198
|
+
continue
|
|
199
|
+
if 8000 not in codec.get("sample_rates_hz", []):
|
|
200
|
+
continue
|
|
201
|
+
verification = codec.get("verification")
|
|
202
|
+
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
203
|
+
if level < VERIFICATION_LEVELS["corroborated"]:
|
|
204
|
+
return (
|
|
205
|
+
"NEEDS_CONFIRMATION",
|
|
206
|
+
f"{section} A-law 8 kHz path is only {verification}",
|
|
207
|
+
level,
|
|
208
|
+
)
|
|
209
|
+
return "SATISFIED", f"{section} provides A-law 8 kHz", level
|
|
210
|
+
return "BLOCKED", f"{section} has no A-law 8 kHz path", 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def video_requirement(camera: dict[str, Any]) -> tuple[str, str, int]:
|
|
214
|
+
present = camera.get("present")
|
|
215
|
+
if present is None:
|
|
216
|
+
return "NEEDS_CONFIRMATION", "camera presence is unknown", 0
|
|
217
|
+
if present is False:
|
|
218
|
+
return "BLOCKED", "camera is not present", 0
|
|
219
|
+
h264 = camera.get("h264", {})
|
|
220
|
+
available = h264.get("available")
|
|
221
|
+
if available is None:
|
|
222
|
+
return "NEEDS_CONFIRMATION", "H.264 encoder availability is unknown", 0
|
|
223
|
+
if available is False:
|
|
224
|
+
return "BLOCKED", "H.264 encoder is unavailable", 0
|
|
225
|
+
output_format = h264.get("output_format")
|
|
226
|
+
if output_format is None:
|
|
227
|
+
return "NEEDS_CONFIRMATION", "H.264 output format is unknown", 0
|
|
228
|
+
if str(output_format).lower() != "h264_annex_b":
|
|
229
|
+
return "BLOCKED", "H5 requires H.264 Annex-B access units", 0
|
|
230
|
+
key_frame = h264.get("key_frame_control")
|
|
231
|
+
if key_frame is None:
|
|
232
|
+
return "NEEDS_CONFIRMATION", "key-frame request control is unknown", 0
|
|
233
|
+
if key_frame is False:
|
|
234
|
+
return "BLOCKED", "key-frame requests cannot reach the encoder", 0
|
|
235
|
+
verification = h264.get("verification")
|
|
236
|
+
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
237
|
+
if level < VERIFICATION_LEVELS["corroborated"]:
|
|
238
|
+
return (
|
|
239
|
+
"NEEDS_CONFIRMATION",
|
|
240
|
+
f"H.264 Annex-B path is only {verification}",
|
|
241
|
+
level,
|
|
242
|
+
)
|
|
243
|
+
return "SATISFIED", "camera provides H.264 Annex-B and IDR control", level
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def combine_requirements(requirements: list[tuple[str, str, int]]) -> dict[str, Any]:
|
|
247
|
+
reasons = [reason for _, reason, _ in requirements]
|
|
248
|
+
states = {state for state, _, _ in requirements}
|
|
249
|
+
levels = [level for state, _, level in requirements if state == "SATISFIED"]
|
|
250
|
+
if "BLOCKED" in states:
|
|
251
|
+
status = "BLOCKED"
|
|
252
|
+
elif "NEEDS_CONFIRMATION" in states:
|
|
253
|
+
status = "NEEDS_CONFIRMATION"
|
|
254
|
+
elif levels and min(levels) >= VERIFICATION_LEVELS["hil_verified"]:
|
|
255
|
+
status = "HIL_VERIFIED"
|
|
256
|
+
else:
|
|
257
|
+
status = "READY_TO_PORT"
|
|
258
|
+
return {"status": status, "reasons": reasons}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def project_requirements(data: dict[str, Any]) -> list[tuple[str, str, int]]:
|
|
262
|
+
requirements: list[tuple[str, str, int]] = []
|
|
263
|
+
revision = data["board"]["hardware_revision"].strip().lower()
|
|
264
|
+
if revision in {"unknown", "unspecified", "n/a"}:
|
|
265
|
+
requirements.append(
|
|
266
|
+
("NEEDS_CONFIRMATION", "exact hardware revision is unresolved", 0)
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
requirements.append(
|
|
270
|
+
("SATISFIED", f"hardware revision is {data['board']['hardware_revision']}", 2)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
target = data["soc"]["target"].strip().lower()
|
|
274
|
+
if target != "esp32s3":
|
|
275
|
+
requirements.append(
|
|
276
|
+
(
|
|
277
|
+
"BLOCKED",
|
|
278
|
+
f"current starter generator supports esp32s3, not {target}",
|
|
279
|
+
0,
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
requirements.append(("SATISFIED", "ESP32-S3 starter is available", 2))
|
|
284
|
+
|
|
285
|
+
platform = data["toolchain"]["tirtc"]["platform"].strip().lower()
|
|
286
|
+
expected_platform = "espressif-esp32s3"
|
|
287
|
+
if target == "esp32s3" and platform != expected_platform:
|
|
288
|
+
requirements.append(
|
|
289
|
+
(
|
|
290
|
+
"BLOCKED",
|
|
291
|
+
f"TiRTC platform {platform} does not match {expected_platform}",
|
|
292
|
+
0,
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
requirements.append(("SATISFIED", "TiRTC platform matches target", 2))
|
|
297
|
+
|
|
298
|
+
verification = data["toolchain"].get("verification")
|
|
299
|
+
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
300
|
+
if level < VERIFICATION_LEVELS["corroborated"]:
|
|
301
|
+
requirements.append(
|
|
302
|
+
(
|
|
303
|
+
"NEEDS_CONFIRMATION",
|
|
304
|
+
f"toolchain and SDK contract are only {verification}",
|
|
305
|
+
level,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
else:
|
|
309
|
+
requirements.append(
|
|
310
|
+
("SATISFIED", "toolchain and SDK contract are corroborated", level)
|
|
311
|
+
)
|
|
312
|
+
return requirements
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def assess_ir(data: dict[str, Any]) -> dict[str, Any]:
|
|
316
|
+
requested = data["features"]["requested"]
|
|
317
|
+
audio_input = data["audio_input"]
|
|
318
|
+
audio_output = data["audio_output"]
|
|
319
|
+
camera = data["camera"]
|
|
320
|
+
result: dict[str, Any] = {}
|
|
321
|
+
for feature in requested:
|
|
322
|
+
if feature == "h5_live_audio":
|
|
323
|
+
requirements = [codec_requirement(audio_input, "audio_input")]
|
|
324
|
+
elif feature == "h5_live_video":
|
|
325
|
+
requirements = [video_requirement(camera)]
|
|
326
|
+
elif feature == "h5_talkback":
|
|
327
|
+
requirements = [codec_requirement(audio_output, "audio_output")]
|
|
328
|
+
else:
|
|
329
|
+
requirements = [
|
|
330
|
+
codec_requirement(audio_input, "audio_input"),
|
|
331
|
+
codec_requirement(audio_output, "audio_output"),
|
|
332
|
+
]
|
|
333
|
+
result[feature] = combine_requirements(requirements)
|
|
334
|
+
return {
|
|
335
|
+
"board_id": data["board"]["id"],
|
|
336
|
+
"hardware_revision": data["board"]["hardware_revision"],
|
|
337
|
+
"project_gate": combine_requirements(project_requirements(data)),
|
|
338
|
+
"features": result,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def command_init(args: argparse.Namespace) -> int:
|
|
343
|
+
output = args.output.resolve()
|
|
344
|
+
if output.exists():
|
|
345
|
+
print(f"refusing to overwrite existing file: {output}", file=sys.stderr)
|
|
346
|
+
return 2
|
|
347
|
+
example = Path(__file__).resolve().parent.parent / "assets" / "hardware-ir.example.json"
|
|
348
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
349
|
+
shutil.copyfile(example, output)
|
|
350
|
+
print(f"created Hardware IR: {output}")
|
|
351
|
+
return 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def command_validate(args: argparse.Namespace) -> int:
|
|
355
|
+
try:
|
|
356
|
+
data = load_ir(args.path)
|
|
357
|
+
except ValueError as exc:
|
|
358
|
+
print(str(exc), file=sys.stderr)
|
|
359
|
+
return 2
|
|
360
|
+
errors = validate_ir(data)
|
|
361
|
+
if errors:
|
|
362
|
+
for error in errors:
|
|
363
|
+
print(f"error: {error}", file=sys.stderr)
|
|
364
|
+
return 2
|
|
365
|
+
print(f"valid Hardware IR: {args.path}")
|
|
366
|
+
return 0
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def command_assess(args: argparse.Namespace) -> int:
|
|
370
|
+
try:
|
|
371
|
+
data = load_ir(args.path)
|
|
372
|
+
except ValueError as exc:
|
|
373
|
+
print(str(exc), file=sys.stderr)
|
|
374
|
+
return 2
|
|
375
|
+
errors = validate_ir(data)
|
|
376
|
+
if errors:
|
|
377
|
+
for error in errors:
|
|
378
|
+
print(f"error: {error}", file=sys.stderr)
|
|
379
|
+
return 2
|
|
380
|
+
assessment = assess_ir(data)
|
|
381
|
+
print(json.dumps(assessment, ensure_ascii=False, indent=2))
|
|
382
|
+
if args.strict:
|
|
383
|
+
statuses = {
|
|
384
|
+
item["status"] for item in assessment["features"].values()
|
|
385
|
+
}
|
|
386
|
+
statuses.add(assessment["project_gate"]["status"])
|
|
387
|
+
if not statuses.issubset(READY_STATUSES):
|
|
388
|
+
return 3
|
|
389
|
+
return 0
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def parse_args() -> argparse.Namespace:
|
|
393
|
+
parser = argparse.ArgumentParser(
|
|
394
|
+
description="Create, validate, and assess TiRTC embedded Hardware IR files."
|
|
395
|
+
)
|
|
396
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
397
|
+
|
|
398
|
+
init_parser = subparsers.add_parser("init", help="create a new Hardware IR")
|
|
399
|
+
init_parser.add_argument("output", type=Path)
|
|
400
|
+
init_parser.set_defaults(handler=command_init)
|
|
401
|
+
|
|
402
|
+
validate_parser = subparsers.add_parser("validate", help="validate an IR")
|
|
403
|
+
validate_parser.add_argument("path", type=Path)
|
|
404
|
+
validate_parser.set_defaults(handler=command_validate)
|
|
405
|
+
|
|
406
|
+
assess_parser = subparsers.add_parser(
|
|
407
|
+
"assess", help="assess requested features against current starter contracts"
|
|
408
|
+
)
|
|
409
|
+
assess_parser.add_argument("path", type=Path)
|
|
410
|
+
assess_parser.add_argument(
|
|
411
|
+
"--strict",
|
|
412
|
+
action="store_true",
|
|
413
|
+
help="return non-zero unless every requested feature is ready or HIL verified",
|
|
414
|
+
)
|
|
415
|
+
assess_parser.set_defaults(handler=command_assess)
|
|
416
|
+
return parser.parse_args()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def main() -> int:
|
|
420
|
+
args = parse_args()
|
|
421
|
+
return args.handler(args)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
raise SystemExit(main())
|