tirtc-device-builder 0.4.0 → 0.6.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 +30 -13
- package/package.json +1 -1
- package/skills/tirtc-esp32-builder/SKILL.md +11 -5
- package/skills/tirtc-esp32-builder/USAGE.md +7 -3
- package/skills/tirtc-esp32-builder/assets/board-audio-contract.example.json +66 -0
- package/skills/tirtc-esp32-builder/assets/board-video-contract.example.json +94 -0
- package/skills/tirtc-esp32-builder/assets/developer-intake-prompt.md +12 -6
- package/skills/tirtc-esp32-builder/assets/hardware-ir-v2.example.json +5 -0
- package/skills/tirtc-esp32-builder/assets/lckfb-szpi-esp32s3-portable-prompt.md +51 -0
- package/skills/tirtc-esp32-builder/assets/report-template.md +11 -0
- package/skills/tirtc-esp32-builder/references/audio-contract.md +51 -0
- package/skills/tirtc-esp32-builder/references/capability-rules.md +11 -7
- package/skills/tirtc-esp32-builder/references/environment.md +8 -0
- package/skills/tirtc-esp32-builder/references/hardware-ir.md +18 -1
- package/skills/tirtc-esp32-builder/references/porting-risks.md +1 -1
- package/skills/tirtc-esp32-builder/references/reporting.md +6 -2
- package/skills/tirtc-esp32-builder/references/video-contract.md +30 -0
- package/skills/tirtc-esp32-builder/references/workflow.md +10 -2
- package/skills/tirtc-esp32-builder/scripts/audio_contract.py +320 -0
- package/skills/tirtc-esp32-builder/scripts/hardware_ir.py +406 -68
- package/skills/tirtc-esp32-builder/scripts/install_audio_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/video_contract.py +333 -0
|
@@ -11,6 +11,9 @@ import sys
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from audio_contract import verify_contract as verify_audio_contract
|
|
15
|
+
from video_contract import verify_contract as verify_video_contract
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
FEATURES = {
|
|
16
19
|
"h5_live_audio",
|
|
@@ -18,6 +21,8 @@ FEATURES = {
|
|
|
18
21
|
"h5_talkback",
|
|
19
22
|
"ai_talk",
|
|
20
23
|
}
|
|
24
|
+
AUDIO_FEATURES = {"h5_live_audio", "h5_talkback", "ai_talk"}
|
|
25
|
+
VIDEO_FEATURES = {"h5_live_video"}
|
|
21
26
|
VERIFICATION_LEVELS = {
|
|
22
27
|
"extracted": 1,
|
|
23
28
|
"corroborated": 2,
|
|
@@ -25,7 +30,12 @@ VERIFICATION_LEVELS = {
|
|
|
25
30
|
"hardware_verified": 4,
|
|
26
31
|
"hil_verified": 5,
|
|
27
32
|
}
|
|
28
|
-
|
|
33
|
+
ASSESSMENT_PHASES = {"intake", "build", "hil"}
|
|
34
|
+
PHASE_SUCCESS_STATUSES = {
|
|
35
|
+
"intake": {"READY_TO_PORT", "HIL_VERIFIED"},
|
|
36
|
+
"build": {"BUILD_VERIFIED", "HIL_VERIFIED"},
|
|
37
|
+
"hil": {"HIL_VERIFIED"},
|
|
38
|
+
}
|
|
29
39
|
VIDEO_CONTRACTS = {
|
|
30
40
|
"mjpeg": "jpeg_complete_frames",
|
|
31
41
|
"h264": "h264_annex_b_access_units",
|
|
@@ -215,6 +225,26 @@ def validate_hardware_resources(
|
|
|
215
225
|
validate_verification(
|
|
216
226
|
i2s.get("verification"), "hardware_resources.i2s.verification", errors
|
|
217
227
|
)
|
|
228
|
+
audio_contract = resources.get("audio_semantic_contract")
|
|
229
|
+
nullable_string(
|
|
230
|
+
audio_contract,
|
|
231
|
+
"hardware_resources.audio_semantic_contract",
|
|
232
|
+
errors,
|
|
233
|
+
)
|
|
234
|
+
if isinstance(audio_contract, str) and Path(audio_contract).is_absolute():
|
|
235
|
+
errors.append(
|
|
236
|
+
"hardware_resources.audio_semantic_contract must be project-relative"
|
|
237
|
+
)
|
|
238
|
+
video_contract = resources.get("video_semantic_contract")
|
|
239
|
+
nullable_string(
|
|
240
|
+
video_contract,
|
|
241
|
+
"hardware_resources.video_semantic_contract",
|
|
242
|
+
errors,
|
|
243
|
+
)
|
|
244
|
+
if isinstance(video_contract, str) and Path(video_contract).is_absolute():
|
|
245
|
+
errors.append(
|
|
246
|
+
"hardware_resources.video_semantic_contract must be project-relative"
|
|
247
|
+
)
|
|
218
248
|
|
|
219
249
|
mapping_section = mapping(
|
|
220
250
|
resources.get("audio_channel_mapping"),
|
|
@@ -391,6 +421,27 @@ def validate_runtime_evidence(
|
|
|
391
421
|
validate_source_refs(record, prefix, source_ids, errors)
|
|
392
422
|
|
|
393
423
|
|
|
424
|
+
def validate_build_evidence(data: dict[str, Any], errors: list[str]) -> None:
|
|
425
|
+
if "build_evidence" not in data:
|
|
426
|
+
return
|
|
427
|
+
evidence = mapping(data.get("build_evidence"), "build_evidence", errors)
|
|
428
|
+
artifacts = evidence.get("artifacts")
|
|
429
|
+
if not isinstance(artifacts, list):
|
|
430
|
+
errors.append("build_evidence.artifacts must be an array")
|
|
431
|
+
return
|
|
432
|
+
for index, item in enumerate(artifacts):
|
|
433
|
+
prefix = f"build_evidence.artifacts[{index}]"
|
|
434
|
+
record = mapping(item, prefix, errors)
|
|
435
|
+
path_value = record.get("path")
|
|
436
|
+
nonempty_string(path_value, f"{prefix}.path", errors)
|
|
437
|
+
if isinstance(path_value, str) and Path(path_value).is_absolute():
|
|
438
|
+
errors.append(f"{prefix}.path must be project-relative")
|
|
439
|
+
positive_int(record.get("size_bytes"), f"{prefix}.size_bytes", errors)
|
|
440
|
+
sha = record.get("sha256")
|
|
441
|
+
if not isinstance(sha, str) or not SHA256_RE.fullmatch(sha):
|
|
442
|
+
errors.append(f"{prefix}.sha256 must be a 64-character SHA-256")
|
|
443
|
+
|
|
444
|
+
|
|
394
445
|
def validate_ir(data: dict[str, Any]) -> list[str]:
|
|
395
446
|
errors: list[str] = []
|
|
396
447
|
schema_version = data.get("schema_version")
|
|
@@ -478,11 +529,20 @@ def validate_ir(data: dict[str, Any]) -> list[str]:
|
|
|
478
529
|
if schema_version == 2:
|
|
479
530
|
validate_hardware_resources(data, source_ids, errors)
|
|
480
531
|
validate_onboarding(data, source_ids, errors)
|
|
532
|
+
validate_build_evidence(data, errors)
|
|
481
533
|
validate_runtime_evidence(data, source_ids, errors)
|
|
482
534
|
return errors
|
|
483
535
|
|
|
484
536
|
|
|
485
|
-
def
|
|
537
|
+
def minimum_verification_for_phase(phase: str) -> str:
|
|
538
|
+
if phase == "intake":
|
|
539
|
+
return "corroborated"
|
|
540
|
+
return "build_verified"
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def codec_requirement(
|
|
544
|
+
media: dict[str, Any], section: str, minimum_verification: str = "corroborated"
|
|
545
|
+
) -> Requirement:
|
|
486
546
|
present = media.get("present")
|
|
487
547
|
if present is None:
|
|
488
548
|
return "NEEDS_CONFIRMATION", f"{section} presence is unknown", 0
|
|
@@ -496,17 +556,20 @@ def codec_requirement(media: dict[str, Any], section: str) -> Requirement:
|
|
|
496
556
|
continue
|
|
497
557
|
verification = codec.get("verification")
|
|
498
558
|
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
499
|
-
if level < VERIFICATION_LEVELS[
|
|
559
|
+
if level < VERIFICATION_LEVELS[minimum_verification]:
|
|
500
560
|
return (
|
|
501
561
|
"NEEDS_CONFIRMATION",
|
|
502
|
-
f"{section} A-law 8 kHz path is only {verification}"
|
|
562
|
+
f"{section} A-law 8 kHz path is only {verification}; "
|
|
563
|
+
f"{minimum_verification} is required",
|
|
503
564
|
level,
|
|
504
565
|
)
|
|
505
566
|
return "SATISFIED", f"{section} provides A-law 8 kHz", level
|
|
506
567
|
return "BLOCKED", f"{section} has no A-law 8 kHz path", 0
|
|
507
568
|
|
|
508
569
|
|
|
509
|
-
def legacy_video_requirement(
|
|
570
|
+
def legacy_video_requirement(
|
|
571
|
+
camera: dict[str, Any], minimum_verification: str = "corroborated"
|
|
572
|
+
) -> Requirement:
|
|
510
573
|
present = camera.get("present")
|
|
511
574
|
if present is None:
|
|
512
575
|
return "NEEDS_CONFIRMATION", "camera presence is unknown", 0
|
|
@@ -530,10 +593,11 @@ def legacy_video_requirement(camera: dict[str, Any]) -> Requirement:
|
|
|
530
593
|
return "BLOCKED", "key-frame requests cannot reach the encoder", 0
|
|
531
594
|
verification = h264.get("verification")
|
|
532
595
|
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
533
|
-
if level < VERIFICATION_LEVELS[
|
|
596
|
+
if level < VERIFICATION_LEVELS[minimum_verification]:
|
|
534
597
|
return (
|
|
535
598
|
"NEEDS_CONFIRMATION",
|
|
536
|
-
f"H.264 Annex-B path is only {verification}"
|
|
599
|
+
f"H.264 Annex-B path is only {verification}; "
|
|
600
|
+
f"{minimum_verification} is required",
|
|
537
601
|
level,
|
|
538
602
|
)
|
|
539
603
|
return "SATISFIED", "camera provides H.264 Annex-B and IDR control", level
|
|
@@ -548,7 +612,9 @@ def selected_item(items: Any, selected_id: Any) -> dict[str, Any] | None:
|
|
|
548
612
|
return None
|
|
549
613
|
|
|
550
614
|
|
|
551
|
-
def video_requirement_v2(
|
|
615
|
+
def video_requirement_v2(
|
|
616
|
+
camera: dict[str, Any], minimum_verification: str = "corroborated"
|
|
617
|
+
) -> Requirement:
|
|
552
618
|
present = camera.get("present")
|
|
553
619
|
if present is None:
|
|
554
620
|
return "NEEDS_CONFIRMATION", "camera presence is unknown", 0
|
|
@@ -585,10 +651,11 @@ def video_requirement_v2(camera: dict[str, Any]) -> Requirement:
|
|
|
585
651
|
return "BLOCKED", "H5 refresh requests cannot reach the media pipeline", 0
|
|
586
652
|
verification = profile.get("verification")
|
|
587
653
|
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
588
|
-
if level < VERIFICATION_LEVELS[
|
|
654
|
+
if level < VERIFICATION_LEVELS[minimum_verification]:
|
|
589
655
|
return (
|
|
590
656
|
"NEEDS_CONFIRMATION",
|
|
591
|
-
f"selected {codec} path is only {verification}"
|
|
657
|
+
f"selected {codec} path is only {verification}; "
|
|
658
|
+
f"{minimum_verification} is required",
|
|
592
659
|
level,
|
|
593
660
|
)
|
|
594
661
|
return (
|
|
@@ -599,7 +666,10 @@ def video_requirement_v2(camera: dict[str, Any]) -> Requirement:
|
|
|
599
666
|
|
|
600
667
|
|
|
601
668
|
def verified_bool_requirement(
|
|
602
|
-
section: dict[str, Any],
|
|
669
|
+
section: dict[str, Any],
|
|
670
|
+
field: str,
|
|
671
|
+
label: str,
|
|
672
|
+
minimum_verification: str = "corroborated",
|
|
603
673
|
) -> Requirement:
|
|
604
674
|
value = section.get(field)
|
|
605
675
|
if value is None:
|
|
@@ -608,12 +678,18 @@ def verified_bool_requirement(
|
|
|
608
678
|
return "BLOCKED", f"{label} is unresolved", 0
|
|
609
679
|
verification = section.get("verification")
|
|
610
680
|
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
611
|
-
if level < VERIFICATION_LEVELS[
|
|
612
|
-
return
|
|
681
|
+
if level < VERIFICATION_LEVELS[minimum_verification]:
|
|
682
|
+
return (
|
|
683
|
+
"NEEDS_CONFIRMATION",
|
|
684
|
+
f"{label} is only {verification}; {minimum_verification} is required",
|
|
685
|
+
level,
|
|
686
|
+
)
|
|
613
687
|
return "SATISFIED", f"{label} is resolved", level
|
|
614
688
|
|
|
615
689
|
|
|
616
|
-
def i2c_requirement(
|
|
690
|
+
def i2c_requirement(
|
|
691
|
+
resources: dict[str, Any], minimum_verification: str = "corroborated"
|
|
692
|
+
) -> Requirement:
|
|
617
693
|
i2c = resources.get("i2c", {})
|
|
618
694
|
used = i2c.get("used")
|
|
619
695
|
if used is None:
|
|
@@ -626,11 +702,16 @@ def i2c_requirement(resources: dict[str, Any]) -> Requirement:
|
|
|
626
702
|
if family == "none":
|
|
627
703
|
return "BLOCKED", "I2C is used but no driver family is selected", 0
|
|
628
704
|
return verified_bool_requirement(
|
|
629
|
-
i2c,
|
|
705
|
+
i2c,
|
|
706
|
+
"single_driver_family",
|
|
707
|
+
f"single {family} I2C driver family plan",
|
|
708
|
+
minimum_verification,
|
|
630
709
|
)
|
|
631
710
|
|
|
632
711
|
|
|
633
|
-
def i2s_requirement(
|
|
712
|
+
def i2s_requirement(
|
|
713
|
+
resources: dict[str, Any], minimum_verification: str = "corroborated"
|
|
714
|
+
) -> Requirement:
|
|
634
715
|
i2s = resources.get("i2s", {})
|
|
635
716
|
used = i2s.get("used")
|
|
636
717
|
if used is None:
|
|
@@ -640,11 +721,14 @@ def i2s_requirement(resources: dict[str, Any]) -> Requirement:
|
|
|
640
721
|
return verified_bool_requirement(
|
|
641
722
|
i2s,
|
|
642
723
|
"controller_and_gpio_ownership_resolved",
|
|
643
|
-
"I2S controller and GPIO ownership",
|
|
724
|
+
"I2S controller and GPIO ownership plan",
|
|
725
|
+
minimum_verification,
|
|
644
726
|
)
|
|
645
727
|
|
|
646
728
|
|
|
647
|
-
def audio_mapping_requirement(
|
|
729
|
+
def audio_mapping_requirement(
|
|
730
|
+
resources: dict[str, Any], minimum_verification: str = "corroborated"
|
|
731
|
+
) -> Requirement:
|
|
648
732
|
channel = resources.get("audio_channel_mapping", {})
|
|
649
733
|
required = channel.get("required")
|
|
650
734
|
if required is None:
|
|
@@ -652,23 +736,32 @@ def audio_mapping_requirement(resources: dict[str, Any]) -> Requirement:
|
|
|
652
736
|
if required is False:
|
|
653
737
|
return "SATISFIED", "audio channel/TDM mapping is not required", 2
|
|
654
738
|
return verified_bool_requirement(
|
|
655
|
-
channel,
|
|
739
|
+
channel,
|
|
740
|
+
"resolved",
|
|
741
|
+
"audio channel/TDM mapping",
|
|
742
|
+
minimum_verification,
|
|
656
743
|
)
|
|
657
744
|
|
|
658
745
|
|
|
659
|
-
def camera_realtime_requirement(
|
|
746
|
+
def camera_realtime_requirement(
|
|
747
|
+
resources: dict[str, Any], minimum_verification: str = "corroborated"
|
|
748
|
+
) -> Requirement:
|
|
660
749
|
return verified_bool_requirement(
|
|
661
750
|
resources.get("camera_realtime", {}),
|
|
662
751
|
"pipeline_safe",
|
|
663
|
-
"camera DMA/task realtime policy",
|
|
752
|
+
"camera DMA/task realtime policy design",
|
|
753
|
+
minimum_verification,
|
|
664
754
|
)
|
|
665
755
|
|
|
666
756
|
|
|
667
|
-
def memory_requirement(
|
|
757
|
+
def memory_requirement(
|
|
758
|
+
resources: dict[str, Any], minimum_verification: str = "corroborated"
|
|
759
|
+
) -> Requirement:
|
|
668
760
|
return verified_bool_requirement(
|
|
669
761
|
resources.get("memory", {}),
|
|
670
762
|
"startup_and_media_budgeted",
|
|
671
|
-
"startup and media memory budget",
|
|
763
|
+
"static startup and media memory budget",
|
|
764
|
+
minimum_verification,
|
|
672
765
|
)
|
|
673
766
|
|
|
674
767
|
|
|
@@ -750,7 +843,9 @@ def binding_requirement(onboarding: dict[str, Any]) -> Requirement:
|
|
|
750
843
|
|
|
751
844
|
|
|
752
845
|
def combine_requirements(
|
|
753
|
-
requirements: list[Requirement],
|
|
846
|
+
requirements: list[Requirement],
|
|
847
|
+
success_status: str = "READY_TO_PORT",
|
|
848
|
+
legacy_hil_from_levels: bool = False,
|
|
754
849
|
) -> dict[str, Any]:
|
|
755
850
|
reasons = [reason for _, reason, _ in requirements]
|
|
756
851
|
states = {state for state, _, _ in requirements}
|
|
@@ -766,10 +861,28 @@ def combine_requirements(
|
|
|
766
861
|
):
|
|
767
862
|
status = "HIL_VERIFIED"
|
|
768
863
|
else:
|
|
769
|
-
status =
|
|
864
|
+
status = success_status
|
|
770
865
|
return {"status": status, "reasons": reasons}
|
|
771
866
|
|
|
772
867
|
|
|
868
|
+
def constrain_project_gate(
|
|
869
|
+
project_gate: dict[str, Any], feature_assessments: dict[str, dict[str, Any]]
|
|
870
|
+
) -> None:
|
|
871
|
+
"""Prevent a project-level pass while a requested feature is unresolved."""
|
|
872
|
+
feature_states = {item["status"] for item in feature_assessments.values()}
|
|
873
|
+
if "BLOCKED" in feature_states:
|
|
874
|
+
project_gate["status"] = "BLOCKED"
|
|
875
|
+
project_gate["reasons"].append("at least one requested feature is blocked")
|
|
876
|
+
elif (
|
|
877
|
+
"NEEDS_CONFIRMATION" in feature_states
|
|
878
|
+
and project_gate["status"] != "BLOCKED"
|
|
879
|
+
):
|
|
880
|
+
project_gate["status"] = "NEEDS_CONFIRMATION"
|
|
881
|
+
project_gate["reasons"].append(
|
|
882
|
+
"at least one requested feature still needs confirmation"
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
|
|
773
886
|
def project_requirements(data: dict[str, Any]) -> list[Requirement]:
|
|
774
887
|
requirements: list[Requirement] = []
|
|
775
888
|
revision = data["board"]["hardware_revision"].strip().lower()
|
|
@@ -839,9 +952,63 @@ def matching_runtime_evidence(
|
|
|
839
952
|
return None
|
|
840
953
|
|
|
841
954
|
|
|
955
|
+
def matching_build_artifact(
|
|
956
|
+
data: dict[str, Any], artifact_sha256: str | None
|
|
957
|
+
) -> dict[str, Any] | None:
|
|
958
|
+
if artifact_sha256 is None:
|
|
959
|
+
return None
|
|
960
|
+
normalized = artifact_sha256.lower()
|
|
961
|
+
build_evidence = data.get("build_evidence", {})
|
|
962
|
+
if not isinstance(build_evidence, dict):
|
|
963
|
+
return None
|
|
964
|
+
artifacts = build_evidence.get("artifacts", [])
|
|
965
|
+
if not isinstance(artifacts, list):
|
|
966
|
+
return None
|
|
967
|
+
for record in artifacts:
|
|
968
|
+
if (
|
|
969
|
+
isinstance(record, dict)
|
|
970
|
+
and str(record.get("sha256", "")).lower() == normalized
|
|
971
|
+
):
|
|
972
|
+
return record
|
|
973
|
+
return None
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def artifact_requirement(
|
|
977
|
+
data: dict[str, Any], artifact_sha256: str | None
|
|
978
|
+
) -> Requirement:
|
|
979
|
+
if artifact_sha256 is None:
|
|
980
|
+
return "NEEDS_CONFIRMATION", "exact build artifact SHA-256 is missing", 0
|
|
981
|
+
if not SHA256_RE.fullmatch(artifact_sha256):
|
|
982
|
+
return "BLOCKED", "build artifact SHA-256 is invalid", 0
|
|
983
|
+
if matching_build_artifact(data, artifact_sha256) is None:
|
|
984
|
+
return (
|
|
985
|
+
"BLOCKED",
|
|
986
|
+
f"artifact {artifact_sha256} is not present in build_evidence.artifacts",
|
|
987
|
+
0,
|
|
988
|
+
)
|
|
989
|
+
return (
|
|
990
|
+
"SATISFIED",
|
|
991
|
+
f"build evidence is bound to artifact {artifact_sha256}",
|
|
992
|
+
VERIFICATION_LEVELS["build_verified"],
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
842
996
|
def assess_ir(
|
|
843
|
-
data: dict[str, Any],
|
|
997
|
+
data: dict[str, Any],
|
|
998
|
+
artifact_sha256: str | None = None,
|
|
999
|
+
phase: str | None = None,
|
|
1000
|
+
audio_gate: Requirement | None = None,
|
|
1001
|
+
video_gate: Requirement | None = None,
|
|
844
1002
|
) -> dict[str, Any]:
|
|
1003
|
+
selected_phase = phase or ("hil" if artifact_sha256 else "intake")
|
|
1004
|
+
if selected_phase not in ASSESSMENT_PHASES:
|
|
1005
|
+
raise ValueError(
|
|
1006
|
+
f"assessment phase must be one of {', '.join(sorted(ASSESSMENT_PHASES))}"
|
|
1007
|
+
)
|
|
1008
|
+
minimum_verification = minimum_verification_for_phase(selected_phase)
|
|
1009
|
+
success_status = (
|
|
1010
|
+
"READY_TO_PORT" if selected_phase == "intake" else "BUILD_VERIFIED"
|
|
1011
|
+
)
|
|
845
1012
|
schema_version = data.get("schema_version")
|
|
846
1013
|
requested = data["features"]["requested"]
|
|
847
1014
|
audio_input = data["audio_input"]
|
|
@@ -858,7 +1025,7 @@ def assess_ir(
|
|
|
858
1025
|
onboarding = data["onboarding"]
|
|
859
1026
|
project.extend(
|
|
860
1027
|
[
|
|
861
|
-
i2c_requirement(resources),
|
|
1028
|
+
i2c_requirement(resources, minimum_verification),
|
|
862
1029
|
wifi_requirement(onboarding),
|
|
863
1030
|
binding_requirement(onboarding),
|
|
864
1031
|
]
|
|
@@ -875,80 +1042,130 @@ def assess_ir(
|
|
|
875
1042
|
else:
|
|
876
1043
|
resources = {}
|
|
877
1044
|
|
|
1045
|
+
if selected_phase in {"build", "hil"}:
|
|
1046
|
+
project.append(artifact_requirement(data, artifact_sha256))
|
|
1047
|
+
|
|
878
1048
|
for feature in requested:
|
|
879
1049
|
if schema_version == 1:
|
|
880
1050
|
if feature == "h5_live_audio":
|
|
881
|
-
requirements = [
|
|
1051
|
+
requirements = [
|
|
1052
|
+
codec_requirement(
|
|
1053
|
+
audio_input, "audio_input", minimum_verification
|
|
1054
|
+
)
|
|
1055
|
+
]
|
|
882
1056
|
elif feature == "h5_live_video":
|
|
883
|
-
requirements = [
|
|
1057
|
+
requirements = [
|
|
1058
|
+
legacy_video_requirement(camera, minimum_verification)
|
|
1059
|
+
]
|
|
884
1060
|
elif feature == "h5_talkback":
|
|
885
|
-
requirements = [
|
|
1061
|
+
requirements = [
|
|
1062
|
+
codec_requirement(
|
|
1063
|
+
audio_output, "audio_output", minimum_verification
|
|
1064
|
+
)
|
|
1065
|
+
]
|
|
886
1066
|
else:
|
|
887
1067
|
requirements = [
|
|
888
|
-
codec_requirement(
|
|
889
|
-
|
|
1068
|
+
codec_requirement(
|
|
1069
|
+
audio_input, "audio_input", minimum_verification
|
|
1070
|
+
),
|
|
1071
|
+
codec_requirement(
|
|
1072
|
+
audio_output, "audio_output", minimum_verification
|
|
1073
|
+
),
|
|
890
1074
|
]
|
|
891
1075
|
result[feature] = combine_requirements(
|
|
892
|
-
requirements,
|
|
1076
|
+
requirements,
|
|
1077
|
+
success_status=success_status,
|
|
1078
|
+
legacy_hil_from_levels=(selected_phase == "intake"),
|
|
893
1079
|
)
|
|
894
1080
|
continue
|
|
895
1081
|
|
|
896
1082
|
if feature == "h5_live_audio":
|
|
897
1083
|
requirements = [
|
|
898
|
-
codec_requirement(audio_input, "audio_input"),
|
|
899
|
-
i2s_requirement(resources),
|
|
900
|
-
audio_mapping_requirement(resources),
|
|
901
|
-
memory_requirement(resources),
|
|
1084
|
+
codec_requirement(audio_input, "audio_input", minimum_verification),
|
|
1085
|
+
i2s_requirement(resources, minimum_verification),
|
|
1086
|
+
audio_mapping_requirement(resources, minimum_verification),
|
|
1087
|
+
memory_requirement(resources, minimum_verification),
|
|
902
1088
|
]
|
|
903
1089
|
elif feature == "h5_live_video":
|
|
904
1090
|
requirements = [
|
|
905
|
-
video_requirement_v2(camera),
|
|
906
|
-
camera_realtime_requirement(resources),
|
|
907
|
-
memory_requirement(resources),
|
|
1091
|
+
video_requirement_v2(camera, minimum_verification),
|
|
1092
|
+
camera_realtime_requirement(resources, minimum_verification),
|
|
1093
|
+
memory_requirement(resources, minimum_verification),
|
|
908
1094
|
]
|
|
909
1095
|
elif feature == "h5_talkback":
|
|
910
1096
|
requirements = [
|
|
911
|
-
codec_requirement(audio_output, "audio_output"),
|
|
912
|
-
i2s_requirement(resources),
|
|
913
|
-
memory_requirement(resources),
|
|
1097
|
+
codec_requirement(audio_output, "audio_output", minimum_verification),
|
|
1098
|
+
i2s_requirement(resources, minimum_verification),
|
|
1099
|
+
memory_requirement(resources, minimum_verification),
|
|
914
1100
|
]
|
|
915
1101
|
else:
|
|
916
1102
|
requirements = [
|
|
917
|
-
codec_requirement(audio_input, "audio_input"),
|
|
918
|
-
codec_requirement(audio_output, "audio_output"),
|
|
919
|
-
i2s_requirement(resources),
|
|
920
|
-
audio_mapping_requirement(resources),
|
|
921
|
-
memory_requirement(resources),
|
|
1103
|
+
codec_requirement(audio_input, "audio_input", minimum_verification),
|
|
1104
|
+
codec_requirement(audio_output, "audio_output", minimum_verification),
|
|
1105
|
+
i2s_requirement(resources, minimum_verification),
|
|
1106
|
+
audio_mapping_requirement(resources, minimum_verification),
|
|
1107
|
+
memory_requirement(resources, minimum_verification),
|
|
922
1108
|
]
|
|
923
|
-
|
|
1109
|
+
if feature in AUDIO_FEATURES and selected_phase in {"build", "hil"}:
|
|
1110
|
+
requirements.append(
|
|
1111
|
+
audio_gate
|
|
1112
|
+
or (
|
|
1113
|
+
"NEEDS_CONFIRMATION",
|
|
1114
|
+
"audio semantic gate was not executed for this project",
|
|
1115
|
+
0,
|
|
1116
|
+
)
|
|
1117
|
+
)
|
|
1118
|
+
if feature in VIDEO_FEATURES and selected_phase in {"build", "hil"}:
|
|
1119
|
+
requirements.append(
|
|
1120
|
+
video_gate
|
|
1121
|
+
or (
|
|
1122
|
+
"NEEDS_CONFIRMATION",
|
|
1123
|
+
"video semantic gate was not executed for this project",
|
|
1124
|
+
0,
|
|
1125
|
+
)
|
|
1126
|
+
)
|
|
1127
|
+
result[feature] = combine_requirements(
|
|
1128
|
+
requirements, success_status=success_status
|
|
1129
|
+
)
|
|
924
1130
|
|
|
925
1131
|
project_gate = combine_requirements(
|
|
926
|
-
project,
|
|
1132
|
+
project,
|
|
1133
|
+
success_status=success_status,
|
|
1134
|
+
legacy_hil_from_levels=(schema_version == 1 and selected_phase == "intake"),
|
|
927
1135
|
)
|
|
1136
|
+
build_artifact = matching_build_artifact(data, artifact_sha256)
|
|
928
1137
|
evidence = matching_runtime_evidence(data, artifact_sha256)
|
|
929
|
-
if schema_version == 2 and
|
|
930
|
-
evidence_features = set(evidence.get("features", []))
|
|
931
|
-
evidence_levels =
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1138
|
+
if schema_version == 2 and selected_phase == "hil":
|
|
1139
|
+
evidence_features = set(evidence.get("features", [])) if evidence else set()
|
|
1140
|
+
evidence_levels = (
|
|
1141
|
+
set(evidence.get("acceptance_levels", [])) if evidence else set()
|
|
1142
|
+
)
|
|
1143
|
+
for feature, feature_assessment in result.items():
|
|
1144
|
+
if feature_assessment["status"] != "BUILD_VERIFIED":
|
|
1145
|
+
continue
|
|
1146
|
+
required_level = FEATURE_HIL_LEVEL[feature]
|
|
1147
|
+
if feature in evidence_features and required_level in evidence_levels:
|
|
1148
|
+
feature_assessment["status"] = "HIL_VERIFIED"
|
|
1149
|
+
feature_assessment["reasons"].append(
|
|
1150
|
+
f"artifact {artifact_sha256} passed {required_level}"
|
|
1151
|
+
)
|
|
1152
|
+
else:
|
|
1153
|
+
feature_assessment["reasons"].append(
|
|
1154
|
+
f"artifact {artifact_sha256} has no matching {required_level} runtime evidence"
|
|
941
1155
|
)
|
|
942
1156
|
if result and all(
|
|
943
1157
|
item["status"] == "HIL_VERIFIED" for item in result.values()
|
|
944
|
-
):
|
|
1158
|
+
) and project_gate["status"] == "BUILD_VERIFIED":
|
|
945
1159
|
project_gate["status"] = "HIL_VERIFIED"
|
|
946
1160
|
project_gate["reasons"].append(
|
|
947
1161
|
f"all requested features have matching artifact evidence for {artifact_sha256}"
|
|
948
1162
|
)
|
|
949
1163
|
|
|
1164
|
+
constrain_project_gate(project_gate, result)
|
|
1165
|
+
|
|
950
1166
|
assessment: dict[str, Any] = {
|
|
951
1167
|
"schema_version": schema_version,
|
|
1168
|
+
"phase": selected_phase,
|
|
952
1169
|
"board_id": data["board"]["id"],
|
|
953
1170
|
"hardware_revision": data["board"]["hardware_revision"],
|
|
954
1171
|
"project_gate": project_gate,
|
|
@@ -959,7 +1176,8 @@ def assess_ir(
|
|
|
959
1176
|
assessment["selected_wifi_method"] = selected_wifi
|
|
960
1177
|
assessment["selected_binding_method"] = selected_binding
|
|
961
1178
|
assessment["artifact_sha256"] = artifact_sha256
|
|
962
|
-
assessment["
|
|
1179
|
+
assessment["build_artifact_evidence_matched"] = build_artifact is not None
|
|
1180
|
+
assessment["runtime_artifact_evidence_matched"] = evidence is not None
|
|
963
1181
|
return assessment
|
|
964
1182
|
|
|
965
1183
|
|
|
@@ -1005,12 +1223,122 @@ def command_assess(args: argparse.Namespace) -> int:
|
|
|
1005
1223
|
for error in errors:
|
|
1006
1224
|
print(f"error: {error}", file=sys.stderr)
|
|
1007
1225
|
return 2
|
|
1008
|
-
|
|
1226
|
+
if args.artifact_sha256 is not None and not SHA256_RE.fullmatch(
|
|
1227
|
+
args.artifact_sha256
|
|
1228
|
+
):
|
|
1229
|
+
print("error: --artifact-sha256 must be a 64-character SHA-256", file=sys.stderr)
|
|
1230
|
+
return 2
|
|
1231
|
+
selected_phase = args.phase or (
|
|
1232
|
+
"hil" if args.artifact_sha256 is not None else "intake"
|
|
1233
|
+
)
|
|
1234
|
+
project = (args.project or args.path.parent).expanduser().resolve()
|
|
1235
|
+
audio_gate: Requirement | None = None
|
|
1236
|
+
audio_gate_result: dict[str, Any] | None = None
|
|
1237
|
+
if (
|
|
1238
|
+
data.get("schema_version") == 2
|
|
1239
|
+
and selected_phase in {"build", "hil"}
|
|
1240
|
+
and AUDIO_FEATURES.intersection(data["features"]["requested"])
|
|
1241
|
+
):
|
|
1242
|
+
relative_contract = data["hardware_resources"].get(
|
|
1243
|
+
"audio_semantic_contract"
|
|
1244
|
+
)
|
|
1245
|
+
if not isinstance(relative_contract, str) or not relative_contract:
|
|
1246
|
+
audio_gate = (
|
|
1247
|
+
"NEEDS_CONFIRMATION",
|
|
1248
|
+
"hardware_resources.audio_semantic_contract is missing",
|
|
1249
|
+
0,
|
|
1250
|
+
)
|
|
1251
|
+
audio_gate_result = {
|
|
1252
|
+
"ok": False,
|
|
1253
|
+
"errors": ["audio semantic contract is missing"],
|
|
1254
|
+
}
|
|
1255
|
+
else:
|
|
1256
|
+
contract = (project / relative_contract).resolve()
|
|
1257
|
+
if project != contract and project not in contract.parents:
|
|
1258
|
+
audio_gate_result = {
|
|
1259
|
+
"ok": False,
|
|
1260
|
+
"errors": ["audio semantic contract escapes project root"],
|
|
1261
|
+
}
|
|
1262
|
+
else:
|
|
1263
|
+
audio_gate_result = verify_audio_contract(contract, project)
|
|
1264
|
+
if audio_gate_result["ok"]:
|
|
1265
|
+
audio_gate = (
|
|
1266
|
+
"SATISFIED",
|
|
1267
|
+
"audio semantic gate passed: "
|
|
1268
|
+
+ str(audio_gate_result.get("summary", "verified")),
|
|
1269
|
+
VERIFICATION_LEVELS["build_verified"],
|
|
1270
|
+
)
|
|
1271
|
+
else:
|
|
1272
|
+
audio_gate = (
|
|
1273
|
+
"BLOCKED",
|
|
1274
|
+
"audio semantic gate failed: "
|
|
1275
|
+
+ "; ".join(audio_gate_result.get("errors", [])),
|
|
1276
|
+
0,
|
|
1277
|
+
)
|
|
1278
|
+
video_gate: Requirement | None = None
|
|
1279
|
+
video_gate_result: dict[str, Any] | None = None
|
|
1280
|
+
if (
|
|
1281
|
+
data.get("schema_version") == 2
|
|
1282
|
+
and selected_phase in {"build", "hil"}
|
|
1283
|
+
and VIDEO_FEATURES.intersection(data["features"]["requested"])
|
|
1284
|
+
):
|
|
1285
|
+
relative_contract = data["hardware_resources"].get(
|
|
1286
|
+
"video_semantic_contract"
|
|
1287
|
+
)
|
|
1288
|
+
if not isinstance(relative_contract, str) or not relative_contract:
|
|
1289
|
+
video_gate = (
|
|
1290
|
+
"NEEDS_CONFIRMATION",
|
|
1291
|
+
"hardware_resources.video_semantic_contract is missing",
|
|
1292
|
+
0,
|
|
1293
|
+
)
|
|
1294
|
+
video_gate_result = {
|
|
1295
|
+
"ok": False,
|
|
1296
|
+
"errors": ["video semantic contract is missing"],
|
|
1297
|
+
}
|
|
1298
|
+
else:
|
|
1299
|
+
contract = (project / relative_contract).resolve()
|
|
1300
|
+
if project != contract and project not in contract.parents:
|
|
1301
|
+
video_gate_result = {
|
|
1302
|
+
"ok": False,
|
|
1303
|
+
"errors": ["video semantic contract escapes project root"],
|
|
1304
|
+
}
|
|
1305
|
+
else:
|
|
1306
|
+
video_gate_result = verify_video_contract(contract, project)
|
|
1307
|
+
if video_gate_result["ok"]:
|
|
1308
|
+
video_gate = (
|
|
1309
|
+
"SATISFIED",
|
|
1310
|
+
"video semantic gate passed: "
|
|
1311
|
+
+ str(video_gate_result.get("summary", "verified")),
|
|
1312
|
+
VERIFICATION_LEVELS["build_verified"],
|
|
1313
|
+
)
|
|
1314
|
+
else:
|
|
1315
|
+
video_gate = (
|
|
1316
|
+
"BLOCKED",
|
|
1317
|
+
"video semantic gate failed: "
|
|
1318
|
+
+ "; ".join(video_gate_result.get("errors", [])),
|
|
1319
|
+
0,
|
|
1320
|
+
)
|
|
1321
|
+
try:
|
|
1322
|
+
assessment = assess_ir(
|
|
1323
|
+
data,
|
|
1324
|
+
artifact_sha256=args.artifact_sha256,
|
|
1325
|
+
phase=selected_phase,
|
|
1326
|
+
audio_gate=audio_gate,
|
|
1327
|
+
video_gate=video_gate,
|
|
1328
|
+
)
|
|
1329
|
+
except ValueError as exc:
|
|
1330
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
1331
|
+
return 2
|
|
1332
|
+
if audio_gate_result is not None:
|
|
1333
|
+
assessment["audio_semantic_gate"] = audio_gate_result
|
|
1334
|
+
if video_gate_result is not None:
|
|
1335
|
+
assessment["video_semantic_gate"] = video_gate_result
|
|
1009
1336
|
print(json.dumps(assessment, ensure_ascii=False, indent=2))
|
|
1010
1337
|
if args.strict:
|
|
1011
1338
|
statuses = {item["status"] for item in assessment["features"].values()}
|
|
1012
1339
|
statuses.add(assessment["project_gate"]["status"])
|
|
1013
|
-
|
|
1340
|
+
allowed_statuses = PHASE_SUCCESS_STATUSES[assessment["phase"]]
|
|
1341
|
+
if not statuses.issubset(allowed_statuses):
|
|
1014
1342
|
return 3
|
|
1015
1343
|
return 0
|
|
1016
1344
|
|
|
@@ -1036,11 +1364,21 @@ def parse_args() -> argparse.Namespace:
|
|
|
1036
1364
|
assess_parser.add_argument(
|
|
1037
1365
|
"--strict",
|
|
1038
1366
|
action="store_true",
|
|
1039
|
-
help="return non-zero unless every requested feature
|
|
1367
|
+
help="return non-zero unless every requested feature passes the selected phase",
|
|
1368
|
+
)
|
|
1369
|
+
assess_parser.add_argument(
|
|
1370
|
+
"--phase",
|
|
1371
|
+
choices=sorted(ASSESSMENT_PHASES),
|
|
1372
|
+
help="assessment phase; defaults to intake, or hil when an artifact SHA is supplied",
|
|
1040
1373
|
)
|
|
1041
1374
|
assess_parser.add_argument(
|
|
1042
1375
|
"--artifact-sha256",
|
|
1043
|
-
help="bind HIL
|
|
1376
|
+
help="bind build or HIL assessment to this exact firmware artifact",
|
|
1377
|
+
)
|
|
1378
|
+
assess_parser.add_argument(
|
|
1379
|
+
"--project",
|
|
1380
|
+
type=Path,
|
|
1381
|
+
help="generated project root; defaults to the Hardware IR directory",
|
|
1044
1382
|
)
|
|
1045
1383
|
assess_parser.set_defaults(handler=command_assess)
|
|
1046
1384
|
return parser.parse_args()
|