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
|
@@ -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
|
|
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["
|
|
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=
|
|
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())
|