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.
Files changed (32) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +36 -36
  4. package/bin/esp32-kit-metadata.js +7 -6
  5. package/bin/install-esp32-kit.js +2 -0
  6. package/package.json +1 -1
  7. package/skills/tirtc-esp32-builder/SKILL.md +4 -4
  8. package/skills/tirtc-esp32-builder/USAGE.md +1 -1
  9. package/skills/tirtc-esp32-builder/assets/board-audio-contract.example.json +66 -0
  10. package/skills/tirtc-esp32-builder/assets/board-video-contract.example.json +95 -0
  11. package/skills/tirtc-esp32-builder/assets/developer-intake-prompt.md +12 -7
  12. package/skills/tirtc-esp32-builder/assets/hardware-ir-v2.example.json +6 -0
  13. package/skills/tirtc-esp32-builder/assets/lckfb-szpi-esp32s3-portable-prompt.md +80 -0
  14. package/skills/tirtc-esp32-builder/assets/report-template.md +16 -1
  15. package/skills/tirtc-esp32-builder/assets/tirtc-runtime-contract.example.json +10 -0
  16. package/skills/tirtc-esp32-builder/references/audio-contract.md +51 -0
  17. package/skills/tirtc-esp32-builder/references/capability-rules.md +3 -3
  18. package/skills/tirtc-esp32-builder/references/environment.md +8 -0
  19. package/skills/tirtc-esp32-builder/references/hardware-ir.md +8 -2
  20. package/skills/tirtc-esp32-builder/references/reporting.md +4 -2
  21. package/skills/tirtc-esp32-builder/references/runtime-contract.md +34 -0
  22. package/skills/tirtc-esp32-builder/references/video-contract.md +31 -0
  23. package/skills/tirtc-esp32-builder/references/workflow.md +4 -2
  24. package/skills/tirtc-esp32-builder/scripts/audio_contract.py +320 -0
  25. package/skills/tirtc-esp32-builder/scripts/doctor.py +7 -1
  26. package/skills/tirtc-esp32-builder/scripts/hardware_ir.py +399 -6
  27. package/skills/tirtc-esp32-builder/scripts/install_audio_gate.py +66 -0
  28. package/skills/tirtc-esp32-builder/scripts/install_runtime_gate.py +66 -0
  29. package/skills/tirtc-esp32-builder/scripts/install_video_gate.py +66 -0
  30. package/skills/tirtc-esp32-builder/scripts/project_portability.py +86 -0
  31. package/skills/tirtc-esp32-builder/scripts/runtime_contract.py +314 -0
  32. package/skills/tirtc-esp32-builder/scripts/video_contract.py +407 -0
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
+ import hashlib
7
8
  import json
8
9
  import re
9
10
  import shutil
@@ -11,6 +12,10 @@ import sys
11
12
  from pathlib import Path
12
13
  from typing import Any
13
14
 
15
+ from audio_contract import verify_contract as verify_audio_contract
16
+ from runtime_contract import verify_contract as verify_runtime_contract
17
+ from video_contract import verify_contract as verify_video_contract
18
+
14
19
 
15
20
  FEATURES = {
16
21
  "h5_live_audio",
@@ -18,6 +23,8 @@ FEATURES = {
18
23
  "h5_talkback",
19
24
  "ai_talk",
20
25
  }
26
+ AUDIO_FEATURES = {"h5_live_audio", "h5_talkback", "ai_talk"}
27
+ VIDEO_FEATURES = {"h5_live_video"}
21
28
  VERIFICATION_LEVELS = {
22
29
  "extracted": 1,
23
30
  "corroborated": 2,
@@ -58,6 +65,16 @@ FEATURE_HIL_LEVEL = {
58
65
  "ai_talk": "L6",
59
66
  }
60
67
  SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
68
+ SOURCE_SCHEME_RE = re.compile(r"^([A-Za-z][A-Za-z0-9+.-]*):")
69
+ ALLOWED_SOURCE_SCHEMES = {
70
+ "http",
71
+ "https",
72
+ "device-kit",
73
+ "managed",
74
+ "official",
75
+ "user-input",
76
+ "user-supplied",
77
+ }
61
78
  Requirement = tuple[str, str, int]
62
79
 
63
80
 
@@ -73,6 +90,62 @@ def load_ir(path: Path) -> dict[str, Any]:
73
90
  return data
74
91
 
75
92
 
93
+ def sha256_file(path: Path) -> str:
94
+ digest = hashlib.sha256()
95
+ with path.open("rb") as stream:
96
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
97
+ digest.update(chunk)
98
+ return digest.hexdigest()
99
+
100
+
101
+ def validate_source_locations(data: dict[str, Any], ir_directory: Path) -> list[str]:
102
+ """Resolve local evidence and reject invented or machine-bound source locators."""
103
+ errors: list[str] = []
104
+ root = ir_directory.expanduser().resolve()
105
+ sources = data.get("sources")
106
+ if not isinstance(sources, list):
107
+ return errors
108
+ for index, item in enumerate(sources):
109
+ if not isinstance(item, dict):
110
+ continue
111
+ prefix = f"sources[{index}]"
112
+ location = item.get("location")
113
+ if not isinstance(location, str) or not location.strip():
114
+ continue
115
+ if ";" in location:
116
+ errors.append(
117
+ f"{prefix}.location must identify exactly one source, not a semicolon list"
118
+ )
119
+ continue
120
+ scheme_match = SOURCE_SCHEME_RE.match(location)
121
+ if scheme_match is not None:
122
+ scheme = scheme_match.group(1).lower()
123
+ if scheme not in ALLOWED_SOURCE_SCHEMES:
124
+ errors.append(
125
+ f"{prefix}.location uses unsupported source scheme {scheme!r}"
126
+ )
127
+ continue
128
+ relative = Path(location)
129
+ if relative.is_absolute():
130
+ errors.append(
131
+ f"{prefix}.location must be IR-relative or a supported source URI"
132
+ )
133
+ continue
134
+ resolved = (root / relative).resolve()
135
+ if not resolved.exists():
136
+ errors.append(
137
+ f"{prefix}.location does not resolve from the IR directory: {location}"
138
+ )
139
+ continue
140
+ expected_sha = item.get("sha256")
141
+ if expected_sha is not None:
142
+ if not resolved.is_file():
143
+ errors.append(f"{prefix}.sha256 can only describe a regular file")
144
+ elif sha256_file(resolved).lower() != str(expected_sha).lower():
145
+ errors.append(f"{prefix}.sha256 does not match {location}")
146
+ return errors
147
+
148
+
76
149
  def mapping(value: Any, path: str, errors: list[str]) -> dict[str, Any]:
77
150
  if not isinstance(value, dict):
78
151
  errors.append(f"{path} must be an object")
@@ -220,6 +293,36 @@ def validate_hardware_resources(
220
293
  validate_verification(
221
294
  i2s.get("verification"), "hardware_resources.i2s.verification", errors
222
295
  )
296
+ audio_contract = resources.get("audio_semantic_contract")
297
+ nullable_string(
298
+ audio_contract,
299
+ "hardware_resources.audio_semantic_contract",
300
+ errors,
301
+ )
302
+ if isinstance(audio_contract, str) and Path(audio_contract).is_absolute():
303
+ errors.append(
304
+ "hardware_resources.audio_semantic_contract must be project-relative"
305
+ )
306
+ video_contract = resources.get("video_semantic_contract")
307
+ nullable_string(
308
+ video_contract,
309
+ "hardware_resources.video_semantic_contract",
310
+ errors,
311
+ )
312
+ if isinstance(video_contract, str) and Path(video_contract).is_absolute():
313
+ errors.append(
314
+ "hardware_resources.video_semantic_contract must be project-relative"
315
+ )
316
+ runtime_contract = resources.get("runtime_semantic_contract")
317
+ nullable_string(
318
+ runtime_contract,
319
+ "hardware_resources.runtime_semantic_contract",
320
+ errors,
321
+ )
322
+ if isinstance(runtime_contract, str) and Path(runtime_contract).is_absolute():
323
+ errors.append(
324
+ "hardware_resources.runtime_semantic_contract must be project-relative"
325
+ )
223
326
 
224
327
  mapping_section = mapping(
225
328
  resources.get("audio_channel_mapping"),
@@ -396,6 +499,27 @@ def validate_runtime_evidence(
396
499
  validate_source_refs(record, prefix, source_ids, errors)
397
500
 
398
501
 
502
+ def validate_build_evidence(data: dict[str, Any], errors: list[str]) -> None:
503
+ if "build_evidence" not in data:
504
+ return
505
+ evidence = mapping(data.get("build_evidence"), "build_evidence", errors)
506
+ artifacts = evidence.get("artifacts")
507
+ if not isinstance(artifacts, list):
508
+ errors.append("build_evidence.artifacts must be an array")
509
+ return
510
+ for index, item in enumerate(artifacts):
511
+ prefix = f"build_evidence.artifacts[{index}]"
512
+ record = mapping(item, prefix, errors)
513
+ path_value = record.get("path")
514
+ nonempty_string(path_value, f"{prefix}.path", errors)
515
+ if isinstance(path_value, str) and Path(path_value).is_absolute():
516
+ errors.append(f"{prefix}.path must be project-relative")
517
+ positive_int(record.get("size_bytes"), f"{prefix}.size_bytes", errors)
518
+ sha = record.get("sha256")
519
+ if not isinstance(sha, str) or not SHA256_RE.fullmatch(sha):
520
+ errors.append(f"{prefix}.sha256 must be a 64-character SHA-256")
521
+
522
+
399
523
  def validate_ir(data: dict[str, Any]) -> list[str]:
400
524
  errors: list[str] = []
401
525
  schema_version = data.get("schema_version")
@@ -416,6 +540,11 @@ def validate_ir(data: dict[str, Any]) -> list[str]:
416
540
  source = mapping(item, prefix, errors)
417
541
  for key in ("id", "kind", "location"):
418
542
  nonempty_string(source.get(key), f"{prefix}.{key}", errors)
543
+ source_sha = source.get("sha256")
544
+ if source_sha is not None and (
545
+ not isinstance(source_sha, str) or not SHA256_RE.fullmatch(source_sha)
546
+ ):
547
+ errors.append(f"{prefix}.sha256 must be a 64-character SHA-256")
419
548
  source_id = source.get("id")
420
549
  if isinstance(source_id, str) and source_id:
421
550
  if source_id in source_ids:
@@ -483,6 +612,7 @@ def validate_ir(data: dict[str, Any]) -> list[str]:
483
612
  if schema_version == 2:
484
613
  validate_hardware_resources(data, source_ids, errors)
485
614
  validate_onboarding(data, source_ids, errors)
615
+ validate_build_evidence(data, errors)
486
616
  validate_runtime_evidence(data, source_ids, errors)
487
617
  return errors
488
618
 
@@ -818,6 +948,24 @@ def combine_requirements(
818
948
  return {"status": status, "reasons": reasons}
819
949
 
820
950
 
951
+ def constrain_project_gate(
952
+ project_gate: dict[str, Any], feature_assessments: dict[str, dict[str, Any]]
953
+ ) -> None:
954
+ """Prevent a project-level pass while a requested feature is unresolved."""
955
+ feature_states = {item["status"] for item in feature_assessments.values()}
956
+ if "BLOCKED" in feature_states:
957
+ project_gate["status"] = "BLOCKED"
958
+ project_gate["reasons"].append("at least one requested feature is blocked")
959
+ elif (
960
+ "NEEDS_CONFIRMATION" in feature_states
961
+ and project_gate["status"] != "BLOCKED"
962
+ ):
963
+ project_gate["status"] = "NEEDS_CONFIRMATION"
964
+ project_gate["reasons"].append(
965
+ "at least one requested feature still needs confirmation"
966
+ )
967
+
968
+
821
969
  def project_requirements(data: dict[str, Any]) -> list[Requirement]:
822
970
  requirements: list[Requirement] = []
823
971
  revision = data["board"]["hardware_revision"].strip().lower()
@@ -887,11 +1035,40 @@ def matching_runtime_evidence(
887
1035
  return None
888
1036
 
889
1037
 
890
- def artifact_requirement(artifact_sha256: str | None) -> Requirement:
1038
+ def matching_build_artifact(
1039
+ data: dict[str, Any], artifact_sha256: str | None
1040
+ ) -> dict[str, Any] | None:
1041
+ if artifact_sha256 is None:
1042
+ return None
1043
+ normalized = artifact_sha256.lower()
1044
+ build_evidence = data.get("build_evidence", {})
1045
+ if not isinstance(build_evidence, dict):
1046
+ return None
1047
+ artifacts = build_evidence.get("artifacts", [])
1048
+ if not isinstance(artifacts, list):
1049
+ return None
1050
+ for record in artifacts:
1051
+ if (
1052
+ isinstance(record, dict)
1053
+ and str(record.get("sha256", "")).lower() == normalized
1054
+ ):
1055
+ return record
1056
+ return None
1057
+
1058
+
1059
+ def artifact_requirement(
1060
+ data: dict[str, Any], artifact_sha256: str | None
1061
+ ) -> Requirement:
891
1062
  if artifact_sha256 is None:
892
1063
  return "NEEDS_CONFIRMATION", "exact build artifact SHA-256 is missing", 0
893
1064
  if not SHA256_RE.fullmatch(artifact_sha256):
894
1065
  return "BLOCKED", "build artifact SHA-256 is invalid", 0
1066
+ if matching_build_artifact(data, artifact_sha256) is None:
1067
+ return (
1068
+ "BLOCKED",
1069
+ f"artifact {artifact_sha256} is not present in build_evidence.artifacts",
1070
+ 0,
1071
+ )
895
1072
  return (
896
1073
  "SATISFIED",
897
1074
  f"build evidence is bound to artifact {artifact_sha256}",
@@ -899,10 +1076,46 @@ def artifact_requirement(artifact_sha256: str | None) -> Requirement:
899
1076
  )
900
1077
 
901
1078
 
1079
+ def artifact_file_requirement(
1080
+ data: dict[str, Any], artifact_sha256: str | None, project: Path
1081
+ ) -> Requirement:
1082
+ recorded = artifact_requirement(data, artifact_sha256)
1083
+ if recorded[0] != "SATISFIED" or artifact_sha256 is None:
1084
+ return recorded
1085
+ record = matching_build_artifact(data, artifact_sha256)
1086
+ if record is None:
1087
+ return "BLOCKED", "matching build artifact record disappeared", 0
1088
+ value = record.get("path")
1089
+ if not isinstance(value, str) or not value:
1090
+ return "BLOCKED", "matching build artifact path is missing", 0
1091
+ relative = Path(value)
1092
+ if relative.is_absolute():
1093
+ return "BLOCKED", "matching build artifact path is not project-relative", 0
1094
+ root = project.expanduser().resolve()
1095
+ path = (root / relative).resolve()
1096
+ if root != path and root not in path.parents:
1097
+ return "BLOCKED", "matching build artifact path escapes project root", 0
1098
+ if not path.is_file():
1099
+ return "BLOCKED", f"recorded build artifact does not exist: {value}", 0
1100
+ if path.stat().st_size != record.get("size_bytes"):
1101
+ return "BLOCKED", f"recorded build artifact size is stale for {value}", 0
1102
+ if sha256_file(path).lower() != artifact_sha256.lower():
1103
+ return "BLOCKED", f"recorded build artifact SHA-256 is stale for {value}", 0
1104
+ return (
1105
+ "SATISFIED",
1106
+ f"artifact file {value} matches size and SHA-256",
1107
+ VERIFICATION_LEVELS["build_verified"],
1108
+ )
1109
+
1110
+
902
1111
  def assess_ir(
903
1112
  data: dict[str, Any],
904
1113
  artifact_sha256: str | None = None,
905
1114
  phase: str | None = None,
1115
+ audio_gate: Requirement | None = None,
1116
+ video_gate: Requirement | None = None,
1117
+ runtime_gate: Requirement | None = None,
1118
+ artifact_gate: Requirement | None = None,
906
1119
  ) -> dict[str, Any]:
907
1120
  selected_phase = phase or ("hil" if artifact_sha256 else "intake")
908
1121
  if selected_phase not in ASSESSMENT_PHASES:
@@ -947,7 +1160,7 @@ def assess_ir(
947
1160
  resources = {}
948
1161
 
949
1162
  if selected_phase in {"build", "hil"}:
950
- project.append(artifact_requirement(artifact_sha256))
1163
+ project.append(artifact_gate or artifact_requirement(data, artifact_sha256))
951
1164
 
952
1165
  for feature in requested:
953
1166
  if schema_version == 1:
@@ -1010,6 +1223,33 @@ def assess_ir(
1010
1223
  audio_mapping_requirement(resources, minimum_verification),
1011
1224
  memory_requirement(resources, minimum_verification),
1012
1225
  ]
1226
+ if feature in AUDIO_FEATURES and selected_phase in {"build", "hil"}:
1227
+ requirements.append(
1228
+ audio_gate
1229
+ or (
1230
+ "NEEDS_CONFIRMATION",
1231
+ "audio semantic gate was not executed for this project",
1232
+ 0,
1233
+ )
1234
+ )
1235
+ if feature in VIDEO_FEATURES and selected_phase in {"build", "hil"}:
1236
+ requirements.append(
1237
+ video_gate
1238
+ or (
1239
+ "NEEDS_CONFIRMATION",
1240
+ "video semantic gate was not executed for this project",
1241
+ 0,
1242
+ )
1243
+ )
1244
+ if selected_phase in {"build", "hil"}:
1245
+ requirements.append(
1246
+ runtime_gate
1247
+ or (
1248
+ "NEEDS_CONFIRMATION",
1249
+ "TiRTC runtime semantic gate was not executed for this project",
1250
+ 0,
1251
+ )
1252
+ )
1013
1253
  result[feature] = combine_requirements(
1014
1254
  requirements, success_status=success_status
1015
1255
  )
@@ -1019,6 +1259,7 @@ def assess_ir(
1019
1259
  success_status=success_status,
1020
1260
  legacy_hil_from_levels=(schema_version == 1 and selected_phase == "intake"),
1021
1261
  )
1262
+ build_artifact = matching_build_artifact(data, artifact_sha256)
1022
1263
  evidence = matching_runtime_evidence(data, artifact_sha256)
1023
1264
  if schema_version == 2 and selected_phase == "hil":
1024
1265
  evidence_features = set(evidence.get("features", [])) if evidence else set()
@@ -1046,6 +1287,8 @@ def assess_ir(
1046
1287
  f"all requested features have matching artifact evidence for {artifact_sha256}"
1047
1288
  )
1048
1289
 
1290
+ constrain_project_gate(project_gate, result)
1291
+
1049
1292
  assessment: dict[str, Any] = {
1050
1293
  "schema_version": schema_version,
1051
1294
  "phase": selected_phase,
@@ -1059,7 +1302,8 @@ def assess_ir(
1059
1302
  assessment["selected_wifi_method"] = selected_wifi
1060
1303
  assessment["selected_binding_method"] = selected_binding
1061
1304
  assessment["artifact_sha256"] = artifact_sha256
1062
- assessment["artifact_evidence_matched"] = evidence is not None
1305
+ assessment["build_artifact_evidence_matched"] = build_artifact is not None
1306
+ assessment["runtime_artifact_evidence_matched"] = evidence is not None
1063
1307
  return assessment
1064
1308
 
1065
1309
 
@@ -1085,7 +1329,7 @@ def command_validate(args: argparse.Namespace) -> int:
1085
1329
  except ValueError as exc:
1086
1330
  print(str(exc), file=sys.stderr)
1087
1331
  return 2
1088
- errors = validate_ir(data)
1332
+ errors = validate_ir(data) + validate_source_locations(data, args.path.parent)
1089
1333
  if errors:
1090
1334
  for error in errors:
1091
1335
  print(f"error: {error}", file=sys.stderr)
@@ -1100,7 +1344,7 @@ def command_assess(args: argparse.Namespace) -> int:
1100
1344
  except ValueError as exc:
1101
1345
  print(str(exc), file=sys.stderr)
1102
1346
  return 2
1103
- errors = validate_ir(data)
1347
+ errors = validate_ir(data) + validate_source_locations(data, args.path.parent)
1104
1348
  if errors:
1105
1349
  for error in errors:
1106
1350
  print(f"error: {error}", file=sys.stderr)
@@ -1110,15 +1354,159 @@ def command_assess(args: argparse.Namespace) -> int:
1110
1354
  ):
1111
1355
  print("error: --artifact-sha256 must be a 64-character SHA-256", file=sys.stderr)
1112
1356
  return 2
1357
+ selected_phase = args.phase or (
1358
+ "hil" if args.artifact_sha256 is not None else "intake"
1359
+ )
1360
+ project = (args.project or args.path.parent).expanduser().resolve()
1361
+ audio_gate: Requirement | None = None
1362
+ audio_gate_result: dict[str, Any] | None = None
1363
+ if (
1364
+ data.get("schema_version") == 2
1365
+ and selected_phase in {"build", "hil"}
1366
+ and AUDIO_FEATURES.intersection(data["features"]["requested"])
1367
+ ):
1368
+ relative_contract = data["hardware_resources"].get(
1369
+ "audio_semantic_contract"
1370
+ )
1371
+ if not isinstance(relative_contract, str) or not relative_contract:
1372
+ audio_gate = (
1373
+ "NEEDS_CONFIRMATION",
1374
+ "hardware_resources.audio_semantic_contract is missing",
1375
+ 0,
1376
+ )
1377
+ audio_gate_result = {
1378
+ "ok": False,
1379
+ "errors": ["audio semantic contract is missing"],
1380
+ }
1381
+ else:
1382
+ contract = (project / relative_contract).resolve()
1383
+ if project != contract and project not in contract.parents:
1384
+ audio_gate_result = {
1385
+ "ok": False,
1386
+ "errors": ["audio semantic contract escapes project root"],
1387
+ }
1388
+ else:
1389
+ audio_gate_result = verify_audio_contract(contract, project)
1390
+ if audio_gate_result["ok"]:
1391
+ audio_gate = (
1392
+ "SATISFIED",
1393
+ "audio semantic gate passed: "
1394
+ + str(audio_gate_result.get("summary", "verified")),
1395
+ VERIFICATION_LEVELS["build_verified"],
1396
+ )
1397
+ else:
1398
+ audio_gate = (
1399
+ "BLOCKED",
1400
+ "audio semantic gate failed: "
1401
+ + "; ".join(audio_gate_result.get("errors", [])),
1402
+ 0,
1403
+ )
1404
+ video_gate: Requirement | None = None
1405
+ video_gate_result: dict[str, Any] | None = None
1406
+ if (
1407
+ data.get("schema_version") == 2
1408
+ and selected_phase in {"build", "hil"}
1409
+ and VIDEO_FEATURES.intersection(data["features"]["requested"])
1410
+ ):
1411
+ relative_contract = data["hardware_resources"].get(
1412
+ "video_semantic_contract"
1413
+ )
1414
+ if not isinstance(relative_contract, str) or not relative_contract:
1415
+ video_gate = (
1416
+ "NEEDS_CONFIRMATION",
1417
+ "hardware_resources.video_semantic_contract is missing",
1418
+ 0,
1419
+ )
1420
+ video_gate_result = {
1421
+ "ok": False,
1422
+ "errors": ["video semantic contract is missing"],
1423
+ }
1424
+ else:
1425
+ contract = (project / relative_contract).resolve()
1426
+ if project != contract and project not in contract.parents:
1427
+ video_gate_result = {
1428
+ "ok": False,
1429
+ "errors": ["video semantic contract escapes project root"],
1430
+ }
1431
+ else:
1432
+ video_gate_result = verify_video_contract(contract, project)
1433
+ if video_gate_result["ok"]:
1434
+ video_gate = (
1435
+ "SATISFIED",
1436
+ "video semantic gate passed: "
1437
+ + str(video_gate_result.get("summary", "verified")),
1438
+ VERIFICATION_LEVELS["build_verified"],
1439
+ )
1440
+ else:
1441
+ video_gate = (
1442
+ "BLOCKED",
1443
+ "video semantic gate failed: "
1444
+ + "; ".join(video_gate_result.get("errors", [])),
1445
+ 0,
1446
+ )
1447
+ runtime_gate: Requirement | None = None
1448
+ runtime_gate_result: dict[str, Any] | None = None
1449
+ if data.get("schema_version") == 2 and selected_phase in {"build", "hil"}:
1450
+ relative_contract = data["hardware_resources"].get(
1451
+ "runtime_semantic_contract"
1452
+ )
1453
+ if not isinstance(relative_contract, str) or not relative_contract:
1454
+ runtime_gate = (
1455
+ "NEEDS_CONFIRMATION",
1456
+ "hardware_resources.runtime_semantic_contract is missing",
1457
+ 0,
1458
+ )
1459
+ runtime_gate_result = {
1460
+ "ok": False,
1461
+ "errors": ["TiRTC runtime semantic contract is missing"],
1462
+ }
1463
+ else:
1464
+ contract = (project / relative_contract).resolve()
1465
+ if project != contract and project not in contract.parents:
1466
+ runtime_gate_result = {
1467
+ "ok": False,
1468
+ "errors": ["TiRTC runtime semantic contract escapes project root"],
1469
+ }
1470
+ else:
1471
+ runtime_gate_result = verify_runtime_contract(contract, project)
1472
+ if runtime_gate_result["ok"]:
1473
+ runtime_gate = (
1474
+ "SATISFIED",
1475
+ "TiRTC runtime semantic gate passed: "
1476
+ + str(runtime_gate_result.get("summary", "verified")),
1477
+ VERIFICATION_LEVELS["build_verified"],
1478
+ )
1479
+ else:
1480
+ runtime_gate = (
1481
+ "BLOCKED",
1482
+ "TiRTC runtime semantic gate failed: "
1483
+ + "; ".join(runtime_gate_result.get("errors", [])),
1484
+ 0,
1485
+ )
1486
+ artifact_gate = (
1487
+ artifact_file_requirement(data, args.artifact_sha256, project)
1488
+ if selected_phase in {"build", "hil"}
1489
+ else None
1490
+ )
1113
1491
  try:
1114
1492
  assessment = assess_ir(
1115
1493
  data,
1116
1494
  artifact_sha256=args.artifact_sha256,
1117
- phase=args.phase,
1495
+ phase=selected_phase,
1496
+ audio_gate=audio_gate,
1497
+ video_gate=video_gate,
1498
+ runtime_gate=runtime_gate,
1499
+ artifact_gate=artifact_gate,
1118
1500
  )
1119
1501
  except ValueError as exc:
1120
1502
  print(f"error: {exc}", file=sys.stderr)
1121
1503
  return 2
1504
+ if audio_gate_result is not None:
1505
+ assessment["audio_semantic_gate"] = audio_gate_result
1506
+ if video_gate_result is not None:
1507
+ assessment["video_semantic_gate"] = video_gate_result
1508
+ if runtime_gate_result is not None:
1509
+ assessment["runtime_semantic_gate"] = runtime_gate_result
1122
1510
  print(json.dumps(assessment, ensure_ascii=False, indent=2))
1123
1511
  if args.strict:
1124
1512
  statuses = {item["status"] for item in assessment["features"].values()}
@@ -1161,6 +1549,11 @@ def parse_args() -> argparse.Namespace:
1161
1549
  "--artifact-sha256",
1162
1550
  help="bind build or HIL assessment to this exact firmware artifact",
1163
1551
  )
1552
+ assess_parser.add_argument(
1553
+ "--project",
1554
+ type=Path,
1555
+ help="generated project root; defaults to the Hardware IR directory",
1556
+ )
1164
1557
  assess_parser.set_defaults(handler=command_assess)
1165
1558
  return parser.parse_args()
1166
1559
 
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """Install the portable audio semantic gate into a generated ESP-IDF project."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ MARKER = "# tirtc-audio-semantic-gate-v1"
13
+ BLOCK = f"""
14
+
15
+ {MARKER}
16
+ add_custom_target(tirtc_audio_semantic_gate ALL
17
+ COMMAND "${{PYTHON}}"
18
+ "${{CMAKE_CURRENT_LIST_DIR}}/tools/verify_audio_contract.py"
19
+ "${{CMAKE_CURRENT_LIST_DIR}}/board-audio-contract.json"
20
+ --project "${{CMAKE_CURRENT_LIST_DIR}}"
21
+ --evidence-out "${{CMAKE_BINARY_DIR}}/audio-contract-evidence.json"
22
+ COMMENT "Validating codec clocks, I2S topology, slots and shared-signal handoff"
23
+ VERBATIM
24
+ )
25
+ add_dependencies(${{CMAKE_PROJECT_NAME}}.elf tirtc_audio_semantic_gate)
26
+ """
27
+
28
+
29
+ def install(project: Path) -> None:
30
+ project = project.expanduser().resolve()
31
+ cmake = project / "CMakeLists.txt"
32
+ contract = project / "board-audio-contract.json"
33
+ if not cmake.is_file():
34
+ raise ValueError(f"ESP-IDF project CMakeLists.txt not found: {cmake}")
35
+ if not contract.is_file():
36
+ raise ValueError(
37
+ f"audio contract not found: {contract}; create and verify it before installing the gate"
38
+ )
39
+ tools = project / "tools"
40
+ tools.mkdir(parents=True, exist_ok=True)
41
+ destination = tools / "verify_audio_contract.py"
42
+ source = Path(__file__).resolve().with_name("audio_contract.py")
43
+ if destination.exists() and destination.read_bytes() != source.read_bytes():
44
+ raise ValueError(f"refusing to overwrite a different gate: {destination}")
45
+ if not destination.exists():
46
+ shutil.copyfile(source, destination)
47
+ text = cmake.read_text(encoding="utf-8")
48
+ if MARKER not in text:
49
+ cmake.write_text(text.rstrip() + BLOCK + "\n", encoding="utf-8")
50
+
51
+
52
+ def main() -> int:
53
+ parser = argparse.ArgumentParser()
54
+ parser.add_argument("project", type=Path)
55
+ args = parser.parse_args()
56
+ try:
57
+ install(args.project)
58
+ except ValueError as exc:
59
+ print(f"error: {exc}", file=sys.stderr)
60
+ return 2
61
+ print(f"installed TiRTC audio semantic gate: {args.project.resolve()}")
62
+ return 0
63
+
64
+
65
+ if __name__ == "__main__":
66
+ raise SystemExit(main())
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """Install the portable TiRTC runtime protocol gate into an ESP-IDF project."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ MARKER = "# tirtc-runtime-semantic-gate-v1"
13
+ BLOCK = f"""
14
+
15
+ {MARKER}
16
+ add_custom_target(tirtc_runtime_semantic_gate ALL
17
+ COMMAND "${{PYTHON}}"
18
+ "${{CMAKE_CURRENT_LIST_DIR}}/tools/verify_runtime_contract.py"
19
+ "${{CMAKE_CURRENT_LIST_DIR}}/tirtc-runtime-contract.json"
20
+ --project "${{CMAKE_CURRENT_LIST_DIR}}"
21
+ --evidence-out "${{CMAKE_BINARY_DIR}}/runtime-contract-evidence.json"
22
+ COMMENT "Validating TiRTC endpoint, callbacks, streams and AI media negotiation"
23
+ VERBATIM
24
+ )
25
+ add_dependencies(${{CMAKE_PROJECT_NAME}}.elf tirtc_runtime_semantic_gate)
26
+ """
27
+
28
+
29
+ def install(project: Path) -> None:
30
+ project = project.expanduser().resolve()
31
+ cmake = project / "CMakeLists.txt"
32
+ contract = project / "tirtc-runtime-contract.json"
33
+ if not cmake.is_file():
34
+ raise ValueError(f"ESP-IDF project CMakeLists.txt not found: {cmake}")
35
+ if not contract.is_file():
36
+ raise ValueError(
37
+ f"runtime contract not found: {contract}; create and verify it before installing the gate"
38
+ )
39
+ tools = project / "tools"
40
+ tools.mkdir(parents=True, exist_ok=True)
41
+ destination = tools / "verify_runtime_contract.py"
42
+ source = Path(__file__).resolve().with_name("runtime_contract.py")
43
+ if destination.exists() and destination.read_bytes() != source.read_bytes():
44
+ raise ValueError(f"refusing to overwrite a different gate: {destination}")
45
+ if not destination.exists():
46
+ shutil.copyfile(source, destination)
47
+ text = cmake.read_text(encoding="utf-8")
48
+ if MARKER not in text:
49
+ cmake.write_text(text.rstrip() + BLOCK + "\n", encoding="utf-8")
50
+
51
+
52
+ def main() -> int:
53
+ parser = argparse.ArgumentParser()
54
+ parser.add_argument("project", type=Path)
55
+ args = parser.parse_args()
56
+ try:
57
+ install(args.project)
58
+ except ValueError as exc:
59
+ print(f"error: {exc}", file=sys.stderr)
60
+ return 2
61
+ print(f"installed TiRTC runtime semantic gate: {args.project.resolve()}")
62
+ return 0
63
+
64
+
65
+ if __name__ == "__main__":
66
+ raise SystemExit(main())