tan-cli 0.7.0__py3-none-any.whl

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 (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,196 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """board.yaml SoC-capability pass (ALP-B010).
3
+
4
+ PORTED from alp-sdk `scripts/alp_cli/validator.py` (`_compat_pass` and its
5
+ SoC-capability helpers). Directories are passed in by the caller (the bound
6
+ SDK checkout's `metadata/`); nothing here derives a root from `__file__`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import yaml
16
+
17
+ from tan.core.board_diagnostic import Diagnostic, DiagnosticCollector
18
+
19
+
20
+ # Local on purpose: tan has no importable equivalent. `tan.planner.*` cannot be
21
+ # imported before an SDK root is bound (and this module must load without one),
22
+ # and `som_metadata.resolve_soc_path` is the metadata-root-rooted resolver the
23
+ # SDK file's own comment says NOT to reuse here (this site roots at a
24
+ # caller-injected `soc_dir`, alp-sdk#1096).
25
+ def split_silicon_ref(silicon: str | None) -> tuple[str, str, str] | None:
26
+ """Split a `vendor:family:part` `silicon:` key into its three slugs.
27
+
28
+ None when *silicon* is falsy or not exactly 3 colon-separated parts
29
+ (alp-sdk `alp_project_loader.split_silicon_ref`).
30
+ """
31
+ if not silicon:
32
+ return None
33
+ parts = silicon.split(":")
34
+ if len(parts) != 3:
35
+ return None
36
+ return parts[0], parts[1], parts[2]
37
+
38
+
39
+ def _compat_pass(
40
+ data: dict[str, Any],
41
+ path: Path,
42
+ collector: DiagnosticCollector,
43
+ *,
44
+ som_dir: Path,
45
+ soc_dir: Path,
46
+ ) -> None:
47
+ som = data.get("som")
48
+ sku = som.get("sku") if isinstance(som, dict) else None
49
+ silicon_ref = _silicon_ref_for_sku(sku, som_dir=som_dir)
50
+ if silicon_ref is None:
51
+ return
52
+ soc_caps = _load_soc_caps(silicon_ref, soc_dir=soc_dir)
53
+ if soc_caps is None:
54
+ return
55
+
56
+ # #602: `cores:` failing schema validation (wrong type, e.g. a string
57
+ # or list instead of a mapping) is already reported as ALP-B004 --
58
+ # guard here too instead of crashing on `.items()`.
59
+ cores = data.get("cores")
60
+ cores = cores if isinstance(cores, dict) else {}
61
+ for core_name, core in cores.items():
62
+ if not isinstance(core, dict):
63
+ continue
64
+ peripherals = core.get("peripherals") or []
65
+ if not isinstance(peripherals, list):
66
+ continue
67
+ for idx, periph in enumerate(peripherals):
68
+ # The board.yaml schema defines peripherals as an array of
69
+ # strings (e.g. ["can", "i2c"]). The schema pass already
70
+ # rejects non-string entries, so skip anything unexpected.
71
+ if not isinstance(periph, str):
72
+ continue
73
+ kind = periph
74
+ if _soc_has_kind(soc_caps, kind):
75
+ continue
76
+ # Best-effort line: the sequence loader only attaches position
77
+ # metadata to dict items; strings carry no __line__. Fall back
78
+ # to the core block's opening position.
79
+ line = core.get("__line__", 1)
80
+ col = core.get("__column__", 1)
81
+ collector.add(
82
+ Diagnostic(
83
+ # warning, not error: SoC peripherals JSON ingestion is
84
+ # incomplete for some parts, and some peripheral
85
+ # categories surface board-side rather than directly on
86
+ # the SoC (emmc / flash / ethernet via I/O controllers).
87
+ # A false-positive ALP-B010 must not block the build —
88
+ # surface the discrepancy and let the customer decide.
89
+ severity="warning",
90
+ path=path,
91
+ line=line,
92
+ col=col,
93
+ span=len(kind),
94
+ code="ALP-B010",
95
+ message=(
96
+ f"core '{core_name}': peripheral kind '{kind}' is not "
97
+ f"listed on silicon '{silicon_ref}' (SoC JSON may be "
98
+ f"incomplete or the peripheral is board-side)"
99
+ ),
100
+ hint=(
101
+ f"verify the SoC truly lacks {kind} before removing "
102
+ f"this entry; if the SoC has it but the metadata is "
103
+ f"stale, update metadata/socs/.../*.json"
104
+ ),
105
+ )
106
+ )
107
+
108
+
109
+ def _silicon_ref_for_sku(sku: str | None, *, som_dir: Path) -> str | None:
110
+ if not sku:
111
+ return None
112
+ for som_path in som_dir.rglob(f"{sku}.yaml"):
113
+ text = som_path.read_text(encoding="utf-8")
114
+ doc = yaml.safe_load(text) or {}
115
+ ref = doc.get("silicon")
116
+ if isinstance(ref, str):
117
+ return ref
118
+ return None
119
+
120
+
121
+ def _load_soc_caps(silicon_ref: str, *, soc_dir: Path) -> dict[str, int] | None:
122
+ # split_silicon_ref(), not resolve_soc_path(): this site roots at the
123
+ # caller-injected `soc_dir`, not at a metadata root. Rebuilding it as
124
+ # `resolve_soc_path(ref, soc_dir.parent)` happens to be exact today only
125
+ # because every caller passes `<root>/socs` -- it would silently resolve
126
+ # somewhere else the first time one didn't (#1096).
127
+ parts = split_silicon_ref(silicon_ref)
128
+ if parts is None:
129
+ return None
130
+ vendor, family, part = parts
131
+ fp = soc_dir / vendor / family / f"{part}.json"
132
+ if not fp.is_file():
133
+ return None
134
+ doc = json.loads(fp.read_text(encoding="utf-8"))
135
+ peripherals = doc.get("peripherals", {}) if isinstance(doc, dict) else {}
136
+ return peripherals if isinstance(peripherals, dict) else {}
137
+
138
+
139
+ # Map user-facing board.yaml peripheral kind names to the SoC JSON key
140
+ # prefixes that represent the underlying silicon capability. Entries
141
+ # here are only needed when the user-facing name differs from the SoC
142
+ # JSON key (e.g. 'counter' is a Zephyr driver-model class that maps to
143
+ # the SoC's timer_* counters; 'sensor' is a driver-model class with no
144
+ # silicon peripheral equivalent so it is always considered present).
145
+ _PERIPHERAL_KIND_ALIASES: dict[str, tuple[str, ...]] = {
146
+ # Zephyr COUNTER driver class backed by hardware timers.
147
+ "counter": ("timer",),
148
+ # Zephyr PWM driver class backed by hardware timers / PWM units.
149
+ "pwm": ("timer", "pwm"),
150
+ # Zephyr SENSOR driver class: software abstraction over various
151
+ # I2C/SPI sensors; no dedicated silicon peripheral block required.
152
+ # Always allow it -- the I2C/SPI bus is the actual constraint.
153
+ "sensor": (), # empty tuple = unconditionally present
154
+ }
155
+
156
+
157
+ def _soc_has_kind(caps: dict[str, int], kind: str) -> bool:
158
+ """Map a peripheral 'kind' onto SoC capability keys.
159
+
160
+ 'kind' is the user-facing peripheral category (i2c, spi, can, ...).
161
+ The SoC JSON uses the same names plus _lp suffixes for low-power
162
+ variants; presence in EITHER counts. Some peripherals also have
163
+ variant suffixes (e.g. can_fd) -- if the base name matches a key
164
+ whose value > 0 that also counts.
165
+
166
+ High-level Zephyr driver classes (counter, pwm, sensor) that do not
167
+ map 1-to-1 to SoC JSON keys are resolved via ``_PERIPHERAL_KIND_ALIASES``.
168
+ """
169
+ # Alias table: if the kind has a (possibly empty) alias list, check
170
+ # those SoC key prefixes instead. An empty alias list means the kind
171
+ # is always considered present (software-only abstraction).
172
+ if kind in _PERIPHERAL_KIND_ALIASES:
173
+ aliases = _PERIPHERAL_KIND_ALIASES[kind]
174
+ if not aliases:
175
+ return True # unconditionally present (e.g. 'sensor')
176
+ for alias_prefix in aliases:
177
+ direct_keys = (alias_prefix, f"{alias_prefix}_lp")
178
+ if any((caps.get(k, 0) or 0) > 0 for k in direct_keys):
179
+ return True
180
+ for key, count in caps.items():
181
+ if key == alias_prefix or key.startswith(f"{alias_prefix}_"):
182
+ if (count or 0) > 0:
183
+ return True
184
+ return False
185
+
186
+ # Direct and LP-suffixed match.
187
+ direct_keys = (kind, f"{kind}_lp")
188
+ if any((caps.get(k, 0) or 0) > 0 for k in direct_keys):
189
+ return True
190
+ # Variant-suffixed match: e.g. user writes 'can', SoC JSON has 'can_fd'.
191
+ for key, count in caps.items():
192
+ if key == kind or key.startswith(f"{kind}_"):
193
+ if (count or 0) > 0:
194
+ return True
195
+ return False
196
+
@@ -0,0 +1,129 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`validate_board_yaml.py`, in-process: the default engine behind `tan validate`.
3
+
4
+ PORTED from alp-sdk `scripts/validate_board_yaml.py` (tan-cli#270), pinned by
5
+ `tests/gates/test_planner_relocation_freshness.py::HAND_PORT_HASHES`.
6
+ The function returns what that script's process would have produced -- an exit
7
+ status, a stdout and a stderr -- so `tan.commands.validate_cmd` feeds it through
8
+ the SAME `analyze_validator_output` it always fed a spawned script's output
9
+ through, and the exit-code map, the issue codes and the envelope shape do not
10
+ move. `tests/parity/test_board_validator_parity.py` runs both engines over a
11
+ fixture corpus and asserts the three values are identical.
12
+
13
+ Two stages, as in the script:
14
+
15
+ 1. the rich diagnostic validator (`board_validator`), whose findings are
16
+ rendered to stderr; any error ends the run at status 1;
17
+ 2. the orchestrator's `load_board_yaml` -- here `tan.planner.loader`, already
18
+ in-process for `tan build` -- for the hard cross-field checks and the three
19
+ hw_rev refusals, which keep the script's own exit statuses 3, 4 and 5.
20
+
21
+ `metadata/**` is read from the bound SDK checkout (ADR-0017); the script's
22
+ `--metadata-root` / `--no-presets` / `--no-color` flags are not ported because
23
+ `tan validate` never passed them (the SDK-side script keeps serving
24
+ west wrappers and MCP).
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass
30
+ from pathlib import Path
31
+
32
+ from tan.core.board_diagnostic import render
33
+ from tan.core.board_validator import PlannerFacts, validate_board_yaml
34
+
35
+ #: The script's own exit statuses (`validate_board_yaml.py`), unchanged.
36
+ EXIT_SDK_REVISION_UNSUPPORTED = 3
37
+ EXIT_SDK_REVISION_UNKNOWN = 4
38
+ EXIT_SDK_REVISION_NOT_BUILDABLE = 5
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ValidatorRun:
43
+ """What the script's process would have left behind."""
44
+
45
+ status: int
46
+ stdout: str
47
+ stderr: str
48
+
49
+
50
+ def _traceback_stderr(exc: BaseException) -> str:
51
+ """An uncaught exception, in the one shape `analyze_validator_output`
52
+ recognises as a crash (`Traceback ...` header, `Type: message` last line),
53
+ so a tan-side fault is never read as a verdict about the customer's board."""
54
+ return (
55
+ "Traceback (most recent call last):\n"
56
+ f"{type(exc).__name__}: {exc}\n"
57
+ )
58
+
59
+
60
+ def run_board_validator(board_path: str, sdk_root: Path) -> ValidatorRun:
61
+ """Validate *board_path* against the metadata of the SDK at *sdk_root*."""
62
+ try:
63
+ return _run(board_path, Path(sdk_root))
64
+ except Exception as exc: # the script's uncaught-exception exit 1
65
+ return ValidatorRun(1, "", _traceback_stderr(exc))
66
+
67
+
68
+ def _run(board_path: str, sdk_root: Path) -> ValidatorRun:
69
+ path = Path(board_path)
70
+ if not path.is_file():
71
+ return ValidatorRun(1, "", f"FAIL {path}: file not found\n")
72
+
73
+ # Read first, as the script does: an undecodable board is its uncaught
74
+ # exception, and must not bind the planner to a root on the way.
75
+ source_text = path.read_text(encoding="utf-8")
76
+
77
+ # `tan.planner` freezes its metadata paths at import, so the SDK root is
78
+ # bound BEFORE anything below imports it (see `tan.planner_root`).
79
+ from tan.planner_root import bind_sdk_root
80
+
81
+ bind_sdk_root(sdk_root)
82
+ from tan.planner.camera_owner import plan_cameras, resolve_cores
83
+ from tan.planner.slugs import _BLOCK_SLUGS
84
+
85
+ collector = validate_board_yaml(
86
+ path,
87
+ metadata_root=sdk_root / "metadata",
88
+ facts=PlannerFacts(
89
+ repo_root=sdk_root,
90
+ block_slugs=_BLOCK_SLUGS,
91
+ plan_cameras=plan_cameras,
92
+ resolve_cores=resolve_cores,
93
+ ),
94
+ )
95
+ stderr = "".join(
96
+ render(diag, source_text=source_text, color=False) + "\n"
97
+ for diag in collector
98
+ )
99
+ if collector.has_errors():
100
+ return ValidatorRun(1, "", stderr)
101
+
102
+ from tan.planner import (
103
+ OrchestratorError,
104
+ SdkRevisionNotBuildable,
105
+ SdkRevisionUnknown,
106
+ SdkRevisionUnsupported,
107
+ load_board_yaml,
108
+ )
109
+
110
+ try:
111
+ load_board_yaml(path)
112
+ except SdkRevisionUnknown as exc:
113
+ return ValidatorRun(
114
+ EXIT_SDK_REVISION_UNKNOWN, "", f"{stderr}FAIL sdk-compat: {exc}\n"
115
+ )
116
+ except SdkRevisionNotBuildable as exc:
117
+ return ValidatorRun(
118
+ EXIT_SDK_REVISION_NOT_BUILDABLE, "", f"{stderr}FAIL sdk-compat: {exc}\n"
119
+ )
120
+ except SdkRevisionUnsupported as exc:
121
+ return ValidatorRun(
122
+ EXIT_SDK_REVISION_UNSUPPORTED, "", f"{stderr}FAIL sdk-compat: {exc}\n"
123
+ )
124
+ except OrchestratorError as exc:
125
+ return ValidatorRun(1, "", f"{stderr}FAIL consistency: {exc}\n")
126
+
127
+ warnings = sum(1 for diag in collector if diag.severity == "warning")
128
+ verdict = f"{path}: clean ({warnings} warning(s))" if warnings else f"{path}: clean"
129
+ return ValidatorRun(0, verdict + "\n", stderr)
@@ -0,0 +1,186 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """board.yaml JSON-Schema pass -> rich diagnostics (ALP-B001..B004, B099).
3
+
4
+ PORTED from alp-sdk `scripts/alp_cli/validator.py` (`_schema_pass` and its
5
+ helpers). The schema file is read from the BOUND SDK checkout's
6
+ `metadata/schemas/` (ADR-0017: facts stay in alp-sdk) -- the caller passes the
7
+ path; nothing here derives a root from `__file__`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re as _re
14
+ from difflib import get_close_matches
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import jsonschema
19
+
20
+ from tan.core.board_diagnostic import Diagnostic, DiagnosticCollector
21
+ from tan.core.board_yaml_pos import node_position
22
+
23
+
24
+ def load_board_schema(schema_path: Path) -> dict[str, Any]:
25
+ """Load board.schema.json from *schema_path*."""
26
+ return json.loads(schema_path.read_text(encoding="utf-8"))
27
+
28
+
29
+ def iter_schema_errors(
30
+ data: dict[str, Any], schema_path: Path
31
+ ) -> list[jsonschema.ValidationError]:
32
+ """Validate *data* against board.schema.json; errors sorted by path.
33
+
34
+ One schema file, one draft dialect (2020-12, matching the schema's own
35
+ `$schema` declaration), one error ordering.
36
+ """
37
+ validator = jsonschema.Draft202012Validator(load_board_schema(schema_path))
38
+ # Stringify path parts: absolute_path mixes ints (array indices) and
39
+ # strs (keys); a raw list comparison would TypeError across siblings.
40
+ return sorted(validator.iter_errors(data),
41
+ key=lambda e: [str(p) for p in e.absolute_path])
42
+
43
+
44
+ def _schema_pass(
45
+ data: dict[str, Any],
46
+ path: Path,
47
+ collector: DiagnosticCollector,
48
+ *,
49
+ schema_path: Path,
50
+ ) -> None:
51
+ # Strip __pos__ keys before handing to jsonschema (they're not in the schema).
52
+ clean = _strip_pos(data)
53
+ for err in iter_schema_errors(clean, schema_path):
54
+ diag = _schema_error_to_diagnostic(err, data, path)
55
+ if diag is not None:
56
+ collector.add(diag)
57
+
58
+
59
+ def _strip_pos(value: Any) -> Any:
60
+ if isinstance(value, dict):
61
+ return {
62
+ k: _strip_pos(v)
63
+ for k, v in value.items()
64
+ if not (isinstance(k, str) and k.startswith("__"))
65
+ }
66
+ if isinstance(value, list):
67
+ return [_strip_pos(v) for v in value]
68
+ return value
69
+
70
+
71
+ def _walk(data: dict[str, Any], path_seq: list[Any]) -> dict[str, Any] | None:
72
+ """Walk a path through the position-augmented document."""
73
+ cursor: Any = data
74
+ for step in path_seq:
75
+ if isinstance(cursor, dict) and step in cursor:
76
+ cursor = cursor[step]
77
+ elif isinstance(cursor, list) and isinstance(step, int) and step < len(cursor):
78
+ cursor = cursor[step]
79
+ else:
80
+ return None
81
+ return cursor if isinstance(cursor, dict) else None
82
+
83
+
84
+ def _schema_error_to_diagnostic(
85
+ err: jsonschema.ValidationError, data: dict[str, Any], path: Path
86
+ ) -> Diagnostic | None:
87
+ abs_path = list(err.absolute_path)
88
+ parent = _walk(data, abs_path[:-1]) if abs_path else data
89
+ line = parent.get("__line__", 1) if parent else 1
90
+ col = parent.get("__column__", 1) if parent else 1
91
+ span = 1
92
+
93
+ if err.validator == "required":
94
+ missing = err.message.split("'")[1] if "'" in err.message else "?"
95
+ return Diagnostic(
96
+ severity="error",
97
+ path=path,
98
+ line=line,
99
+ col=col,
100
+ span=span,
101
+ code="ALP-B001",
102
+ message=f"required key '{missing}' is missing",
103
+ hint=f"add a '{missing}:' entry to this block",
104
+ )
105
+
106
+ if err.validator == "additionalProperties":
107
+ # jsonschema reports additionalProperties errors AT the object that
108
+ # rejected the extra key: `abs_path` is that object's OWN path, not
109
+ # the offending key's -- so the offending key is only ever
110
+ # recoverable from the message text, never from `abs_path[-1]`.
111
+ # (Nested case: a `server:` block with `additionalProperties:
112
+ # false` rejecting `tls_ca_bundle` reports `abs_path ==
113
+ # ["ota", "server"]`; `abs_path[-1]` is `"server"`, the block
114
+ # itself, not the unknown key -- which used to surface as the
115
+ # misleading "unknown key 'server'".)
116
+ _m = _re.search(r"'([^']+)'", err.message)
117
+ bad_key = _m.group(1) if _m else "?"
118
+ offending = _walk(data, abs_path) if abs_path else data
119
+ if offending and "__keys__" in offending and bad_key in offending["__keys__"]:
120
+ parent = offending
121
+ line, col = node_position(parent, bad_key, target="key")
122
+ span = len(str(bad_key))
123
+ allowed = list(err.schema.get("properties", {}).keys())
124
+
125
+ suggestion = get_close_matches(str(bad_key), allowed, n=1)
126
+ hint = f"did you mean '{suggestion[0]}'?" if suggestion else None
127
+ return Diagnostic(
128
+ severity="error",
129
+ path=path,
130
+ line=line,
131
+ col=col,
132
+ span=span,
133
+ code="ALP-B002",
134
+ message=f"unknown key '{bad_key}'",
135
+ hint=hint,
136
+ )
137
+
138
+ if err.validator in {"enum", "pattern"}:
139
+ if abs_path and parent and "__keys__" in parent:
140
+ key = abs_path[-1]
141
+ if key in parent["__keys__"]:
142
+ line, col = node_position(parent, key, target="value")
143
+ span = max(1, len(str(parent.get(key, ""))))
144
+ if err.validator == "enum":
145
+ allowed = err.schema.get("enum", [])
146
+ hint = f"expected one of: {', '.join(map(repr, allowed))}"
147
+ else:
148
+ hint = f"value must match pattern: {err.schema.get('pattern')}"
149
+ return Diagnostic(
150
+ severity="error",
151
+ path=path,
152
+ line=line,
153
+ col=col,
154
+ span=span,
155
+ code="ALP-B003",
156
+ message=err.message,
157
+ hint=hint,
158
+ )
159
+
160
+ if err.validator == "type":
161
+ if abs_path and parent and "__keys__" in parent:
162
+ key = abs_path[-1]
163
+ if key in parent["__keys__"]:
164
+ line, col = node_position(parent, key, target="value")
165
+ return Diagnostic(
166
+ severity="error",
167
+ path=path,
168
+ line=line,
169
+ col=col,
170
+ span=1,
171
+ code="ALP-B004",
172
+ message=err.message,
173
+ hint=f"expected type: {err.schema.get('type')}",
174
+ )
175
+
176
+ # Fallback for any validator we haven't mapped yet.
177
+ return Diagnostic(
178
+ severity="error",
179
+ path=path,
180
+ line=line,
181
+ col=col,
182
+ span=1,
183
+ code="ALP-B099",
184
+ message=err.message,
185
+ hint=None,
186
+ )
@@ -0,0 +1,46 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Is the bound alp-sdk checkout's validator newer than the one tan ported?
3
+
4
+ `tan validate` runs a PORT of the SDK's board.yaml validator in-process, so a
5
+ checkout whose validator sources moved past the audited commit may carry rules
6
+ tan does not apply (a clean verdict that the SDK's own script would reject).
7
+ The four sources the port was cut from are hashed here; a differing hash in the
8
+ bound checkout is reported as a non-fatal warning. The hashes are kept equal to
9
+ `tests/gates/test_planner_relocation_freshness.py::HAND_PORT_HASHES` by
10
+ `tests/gates/test_board_validator_skew_pins.py`, so a re-port moves both.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ from pathlib import Path
17
+
18
+ #: alp-sdk-relative source -> sha256 at the commit the port was audited against.
19
+ PORTED_SOURCE_HASHES: dict[str, str] = {
20
+ "scripts/alp_cli/validator.py": "ce44ba907c411f763e39b07c168f959dd4455da20d3ffba9f8c28eaa5a1f810d",
21
+ "scripts/alp_cli/diagnostic.py": "6a96eb120d73640196cefbd30d13e40a515d00d11368e440123594189cd16d96",
22
+ "scripts/alp_cli/yaml_pos.py": "c83580d86a8f2bc575e20b44f4893847a6a08f765774861f2d29a8bd61ae5b74",
23
+ "scripts/validate_board_yaml.py": "91e06880f068447f98a2c68f1ffb4b6da29c18d2fcadfa0232d982e6612557d4",
24
+ }
25
+
26
+
27
+ def _digest(path: Path) -> str:
28
+ # CRLF -> LF so a Windows autocrlf checkout of identical sources is not "newer".
29
+ return hashlib.sha256(path.read_bytes().replace(b"\r\n", b"\n")).hexdigest()
30
+
31
+
32
+ def skewed_sources(sdk_root: Path) -> tuple[str, ...]:
33
+ """SDK-relative validator sources whose bytes differ from the audited ones.
34
+
35
+ A source absent from the checkout (or unreadable) is skipped: an SDK that
36
+ dropped the script has no newer rules for tan to miss.
37
+ """
38
+ skewed: list[str] = []
39
+ for rel, pinned in PORTED_SOURCE_HASHES.items():
40
+ path = Path(sdk_root, *rel.split("/"))
41
+ try:
42
+ if path.is_file() and _digest(path) != pinned:
43
+ skewed.append(rel)
44
+ except OSError:
45
+ continue
46
+ return tuple(skewed)