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,542 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """board.yaml validator with rich diagnostics, in-process.
3
+
4
+ PORTED from alp-sdk `scripts/alp_cli/validator.py` (tan-cli#270). Runs three
5
+ passes -- schema (ALP-B001..B004, in `board_validator_schema`), xref
6
+ (ALP-B005..B009, here) and compat (ALP-B010, in `board_validator_compat`).
7
+
8
+ The only deliberate departures from the SDK file, none behavioural:
9
+
10
+ * No module-scope root. The SDK file derived `REPO`/`METADATA` from
11
+ `__file__`; inside tan that would be tan's own tree. `metadata_root` and
12
+ `PlannerFacts.repo_root` are required parameters, rooted at the BOUND SDK
13
+ checkout (ADR-0017).
14
+ * The three things the SDK file lazily imported from `alp_orchestrate`
15
+ (`_BLOCK_SLUGS`, `plan_cameras`, `resolve_cores`) arrive in a `PlannerFacts`
16
+ instead, so this module loads without `tan.planner` -- which needs a bound
17
+ SDK root at import. `board_validator_run` builds it from `tan.planner`;
18
+ `PlannerFacts()` with no callables degrades exactly as the SDK file did when
19
+ that import failed (chip manifests only, no camera-owner checks).
20
+ * `fast_safe_load` is local: `tan.planner.strict_loaders` cannot be imported
21
+ before an SDK root is bound.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from difflib import get_close_matches
27
+ from collections.abc import Callable
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ import yaml
33
+
34
+ from tan.core.board_diagnostic import Diagnostic, DiagnosticCollector
35
+ from tan.core.board_validator_compat import _compat_pass
36
+ from tan.core.board_validator_schema import _schema_pass
37
+ from tan.core.board_yaml_pos import load_with_positions, node_position
38
+
39
+ _YAML_LOADER = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class PlannerFacts:
44
+ """The SDK-tree facts and planner leaves the validator consults."""
45
+
46
+ repo_root: Path
47
+ block_slugs: frozenset[str] = frozenset()
48
+ plan_cameras: Callable[..., Any] | None = None
49
+ resolve_cores: Callable[..., Any] | None = None
50
+
51
+
52
+ def fast_safe_load(text: str) -> Any:
53
+ """Exactly `yaml.safe_load(text)`, parsed by libyaml when available."""
54
+ return yaml.load(text, Loader=_YAML_LOADER)
55
+
56
+
57
+ def validate_board_yaml(
58
+ path: Path, *, metadata_root: Path, facts: PlannerFacts
59
+ ) -> DiagnosticCollector:
60
+ """Validate a board.yaml file. Returns a DiagnosticCollector.
61
+
62
+ *metadata_root* is the SoM / board-preset / SoC / schema search root:
63
+ the bound SDK checkout's ``metadata/`` (or a customer's out-of-tree copy
64
+ of it, which validates identically everywhere).
65
+ """
66
+ collector = DiagnosticCollector()
67
+ text = path.read_text(encoding="utf-8")
68
+ try:
69
+ data = load_with_positions(text, source=path)
70
+ except Exception as exc: # YAML parse error
71
+ collector.add(
72
+ Diagnostic(
73
+ severity="error",
74
+ path=path,
75
+ line=1,
76
+ col=1,
77
+ span=1,
78
+ code="ALP-B000",
79
+ message=f"YAML parse error: {exc}",
80
+ hint=None,
81
+ )
82
+ )
83
+ return collector
84
+
85
+ root = metadata_root
86
+ som_dir = root / "e1m_modules"
87
+ preset_dir = root / "boards"
88
+ soc_dir = root / "socs"
89
+ chip_dir = root / "chips"
90
+ camera_module_dir = root / "camera_modules"
91
+ schema_path = root / "schemas" / "board.schema.json"
92
+
93
+ _schema_pass(data, path, collector, schema_path=schema_path)
94
+ _xref_pass(data, path, collector, som_dir=som_dir, preset_dir=preset_dir,
95
+ chip_dir=chip_dir, camera_module_dir=camera_module_dir,
96
+ facts=facts)
97
+ _compat_pass(data, path, collector, som_dir=som_dir, soc_dir=soc_dir)
98
+ return collector
99
+
100
+
101
+ def _xref_pass(
102
+ data: dict[str, Any],
103
+ path: Path,
104
+ collector: DiagnosticCollector,
105
+ *,
106
+ som_dir: Path,
107
+ preset_dir: Path,
108
+ chip_dir: Path,
109
+ camera_module_dir: Path,
110
+ facts: PlannerFacts,
111
+ ) -> None:
112
+ som = data.get("som")
113
+ # #602: a schema-invalid `som:` (wrong type, e.g. a string/list instead
114
+ # of a mapping) is already reported by _schema_pass as ALP-B004 -- don't
115
+ # let this semantic pass crash on it, just skip SoM/preset xref work.
116
+ som = som if isinstance(som, dict) else {}
117
+ sku = som.get("sku")
118
+ som_doc: dict[str, Any] | None = None
119
+ if isinstance(sku, str):
120
+ sku_path = _sku_path(sku, som_dir=som_dir)
121
+ if sku_path is None:
122
+ line, col = node_position(som, "sku", target="value")
123
+ collector.add(
124
+ Diagnostic(
125
+ severity="error",
126
+ path=path,
127
+ line=line,
128
+ col=col,
129
+ span=len(sku),
130
+ code="ALP-B005",
131
+ message=f"SoM SKU '{sku}' does not resolve to a known module",
132
+ hint=_sku_suggestion(sku, som_dir=som_dir),
133
+ )
134
+ )
135
+ else:
136
+ som_doc = _load_metadata_yaml(sku_path)
137
+
138
+ preset = data.get("preset")
139
+ board_doc: dict[str, Any] | None = None
140
+ if isinstance(preset, str):
141
+ preset_path = preset_dir / f"{preset}.yaml"
142
+ if not preset_path.is_file():
143
+ line, col = node_position(data, "preset", target="value")
144
+ collector.add(
145
+ Diagnostic(
146
+ severity="error",
147
+ path=path,
148
+ line=line,
149
+ col=col,
150
+ span=len(preset),
151
+ code="ALP-B006",
152
+ message=f"board preset '{preset}' does not exist",
153
+ hint=_preset_suggestion(preset, preset_dir=preset_dir),
154
+ )
155
+ )
156
+ else:
157
+ board_doc = _load_metadata_yaml(preset_path)
158
+
159
+ if isinstance(preset, str) and isinstance(sku, str) and som_doc and board_doc:
160
+ _check_board_hosts_som_family(
161
+ data, path, collector, sku, som_doc, preset, board_doc,
162
+ preset_dir=preset_dir)
163
+
164
+ _check_chips_known(data, path, collector, chip_dir=chip_dir,
165
+ block_slugs=facts.block_slugs)
166
+ # With a `preset:` the connectors come from that preset; if the preset is
167
+ # missing (ALP-B006 already reported) there is nothing to check against.
168
+ if not isinstance(preset, str):
169
+ _check_cameras(data, path, collector, data.get("camera_connectors"),
170
+ camera_module_dir=camera_module_dir, inline=True,
171
+ som_doc=som_doc, facts=facts)
172
+ elif board_doc is not None:
173
+ _check_cameras(data, path, collector, board_doc.get("camera_connectors"),
174
+ camera_module_dir=camera_module_dir, som_doc=som_doc,
175
+ facts=facts)
176
+
177
+
178
+ def camera_connector_problems(connectors: Any, e1m_routes: Any, *,
179
+ shield_dir: Path,
180
+ families: Any = None) -> list[str]:
181
+ """Cross-checks of `camera_connectors:` that the schema cannot express,
182
+ shared by this validator (inline project boards) and
183
+ scripts/validate_metadata.py (board presets):
184
+
185
+ * every macro a connector names (`i2c`, `enable`, `reset`, `select[].gpio`)
186
+ must be declared in the same board's `e1m_routes:` (`buses` for `i2c`,
187
+ `gpio` for the rest) -- connectors reference routes, never restate pads;
188
+ * `lane_polarity` carries one flag per clock + data lane: `lanes + 1`;
189
+ * every `zephyr_shields` entry is a directory under zephyr/boards/shields/;
190
+ * `linux: true` only on CAM0 of a board for a `renesas-rzv2n*` SoM family
191
+ (`families`: the preset's `hosts_som_families`, or the project SoM's
192
+ family for an inline board; None skips the family half) -- the only
193
+ place a Linux camera path exists.
194
+ Follow-up: tighten to "scripts/gen_camera_dt.py (#2736) emits a
195
+ <board>-cam<n>-<module>.dtsi for this connector"; that needs the board
196
+ name, which an inline project board does not have.
197
+ """
198
+ if not isinstance(connectors, dict):
199
+ return []
200
+ routes = e1m_routes if isinstance(e1m_routes, dict) else {}
201
+
202
+ def macros(section: str) -> set[Any]:
203
+ return {e.get("macro") for e in (routes.get(section) or [])
204
+ if isinstance(e, dict)}
205
+
206
+ gpio, buses = macros("gpio"), macros("buses")
207
+ msgs: list[str] = []
208
+ for name, c in connectors.items():
209
+ if not isinstance(c, dict):
210
+ continue
211
+ refs = [("i2c", c.get("i2c"), buses, "buses"),
212
+ ("enable", c.get("enable"), gpio, "gpio"),
213
+ ("reset", c.get("reset"), gpio, "gpio")]
214
+ refs += [("select.gpio", sel.get("gpio"), gpio, "gpio")
215
+ for sel in (c.get("select") or []) if isinstance(sel, dict)]
216
+ for field, macro, known, section in refs:
217
+ if macro is not None and macro not in known:
218
+ msgs.append(f"camera_connectors.{name}.{field}: `{macro}` is not a "
219
+ f"macro in e1m_routes.{section}")
220
+ if c.get("linux"):
221
+ if name != "CAM0":
222
+ msgs.append(f"camera_connectors.{name}.linux: only CAM0 has a Linux "
223
+ f"camera path (the sensor DT is generated for CAM0)")
224
+ if families is not None and not any(
225
+ str(f).startswith("renesas-rzv2n") for f in families):
226
+ msgs.append(f"camera_connectors.{name}.linux: no Linux camera path "
227
+ f"for SoM family {', '.join(map(str, families)) or 'none'} "
228
+ f"(only renesas-rzv2n*)")
229
+ for sh in c.get("zephyr_shields") or []:
230
+ if not (shield_dir / str(sh)).is_dir():
231
+ msgs.append(f"camera_connectors.{name}.zephyr_shields: `{sh}` is not "
232
+ f"a shield under zephyr/boards/shields/")
233
+ pol, lanes = c.get("lane_polarity"), c.get("lanes")
234
+ if isinstance(pol, list) and isinstance(lanes, int) and len(pol) != lanes + 1:
235
+ msgs.append(f"camera_connectors.{name}.lane_polarity: {len(pol)} entries "
236
+ f"for {lanes} data lane(s); expected {lanes + 1} "
237
+ f"(clock lane + data lanes)")
238
+ return msgs
239
+
240
+
241
+ def _check_cameras(
242
+ data: dict[str, Any],
243
+ path: Path,
244
+ collector: DiagnosticCollector,
245
+ connectors: Any,
246
+ *,
247
+ camera_module_dir: Path,
248
+ facts: PlannerFacts,
249
+ inline: bool = False,
250
+ som_doc: Any = None,
251
+ ) -> None:
252
+ """ALP-B003 for camera declarations the schema cannot judge:
253
+
254
+ * a `cameras:` entry naming a connector the resolved board does not
255
+ expose, a module with no `metadata/camera_modules/<module>.yaml`, or a
256
+ connector listed twice (both valid identifiers to the schema, so a typo
257
+ would only surface when a generator looks the name up);
258
+ * a module with no `zephyr_shield:` while a Zephyr core is in use (it
259
+ could never be selected: the Zephyr build takes `-DSHIELD` from it);
260
+ * for an INLINE board, a `camera_connectors:` block whose macros do not
261
+ resolve in the project's own `e1m_routes:` or whose `lane_polarity` has
262
+ the wrong length (presets get the same check from validate_metadata.py).
263
+
264
+ Anchored on the `cameras` (or `camera_connectors`) key because the
265
+ position loader does not track individual list items.
266
+ """
267
+ def report(key: str, message: str) -> None:
268
+ line, col = node_position(data, key, target="key")
269
+ collector.add(
270
+ Diagnostic(severity="error", path=path, line=line, col=col,
271
+ span=len(key), code="ALP-B003", message=message))
272
+
273
+ if inline:
274
+ fam = som_doc.get("family") if isinstance(som_doc, dict) else None
275
+ for message in camera_connector_problems(
276
+ connectors, data.get("e1m_routes"),
277
+ shield_dir=facts.repo_root / "zephyr" / "boards" / "shields",
278
+ families=[fam] if fam else None):
279
+ report("camera_connectors", message)
280
+
281
+ cameras = data.get("cameras")
282
+ if not isinstance(cameras, list):
283
+ return
284
+ known_connectors = sorted(connectors) if isinstance(connectors, dict) else []
285
+ seen: set[str] = set()
286
+ for entry in cameras:
287
+ if not isinstance(entry, dict):
288
+ continue # schema pass reports the wrong type
289
+ connector = entry.get("connector")
290
+ module = entry.get("module")
291
+ if isinstance(connector, str):
292
+ if connector not in known_connectors:
293
+ report("cameras",
294
+ f"cameras: connector '{connector}' is not a camera connector of "
295
+ f"this board (known: {', '.join(known_connectors) or 'none'})")
296
+ if connector in seen:
297
+ report("cameras",
298
+ f"cameras: connector '{connector}' is listed more than once "
299
+ f"(one module per connector)")
300
+ seen.add(connector)
301
+ if isinstance(module, str) and not (camera_module_dir / f"{module}.yaml").is_file():
302
+ report("cameras",
303
+ f"cameras: unknown camera module '{module}' "
304
+ f"(no metadata/camera_modules/{module}.yaml)")
305
+ _check_camera_owners(data, cameras, connectors, som_doc, report,
306
+ camera_module_dir=camera_module_dir, facts=facts)
307
+
308
+
309
+ def _check_camera_owners(data, cameras, connectors, som_doc, report, *,
310
+ camera_module_dir: Path, facts: PlannerFacts) -> None:
311
+ """Ownership + static build checks via the shared leaf
312
+ `alp_orchestrate/camera_owner.py` (the same rule the planner applies):
313
+ an explicit/implied owner core, a module `zephyr_shield`, connector
314
+ `zephyr_shields`, and a shield overlay for the owner's board target.
315
+
316
+ The two leaves come in through *facts* (see the module docstring); absent
317
+ ones skip the check, as the SDK file's failed lazy import did."""
318
+ if not isinstance(som_doc, dict):
319
+ return
320
+ plan_cameras, resolve_cores = facts.plan_cameras, facts.resolve_cores
321
+ if plan_cameras is None or resolve_cores is None:
322
+ return
323
+ modules = {}
324
+ for entry in cameras:
325
+ mod = entry.get("module") if isinstance(entry, dict) else None
326
+ if isinstance(mod, str) and (camera_module_dir / f"{mod}.yaml").is_file():
327
+ modules[mod] = _load_metadata_yaml(camera_module_dir / f"{mod}.yaml") or {}
328
+ cores = resolve_cores(data.get("cores"), som_doc.get("topology"))
329
+ for plan in plan_cameras(cameras, connectors, cores, modules, facts.repo_root):
330
+ for message in plan.errors:
331
+ report("cameras", message)
332
+
333
+
334
+ def _known_chip_slugs(*, chip_dir: Path, block_slugs: frozenset[str]) -> set[str]:
335
+ """Every valid `chips:` token: real chip manifests plus the small set
336
+ of non-chip SDK 'block' helpers `chips:` may also legitimately name
337
+ (`button_led`, `pdm_mic`) -- see `_BLOCK_SLUGS` in
338
+ `alp_orchestrate/slugs.py`, the dependency-free leaf that is the single
339
+ source of truth for that distinction (kconfig.py's emitter and
340
+ check_example_portability.py both consult the same set; e.g.
341
+ `examples/connectivity/iot-connected-camera/board.yaml`'s
342
+ `chips: [ssd1306, button_led]`).
343
+
344
+ A manifest with `driver_status: planned` OR `driver_status: none`
345
+ (issue #1224 review; #1270 widened this from `planned`-only after
346
+ `metadata/chips/dp83825.yaml` shipped the tree's first `none`) ships no
347
+ `chips/<id>/` driver and declares no `ALP_SDK_CHIP_<NAME>` Kconfig
348
+ symbol -- naming either in `chips:` would emit the same
349
+ undeclared-symbol Kconfig line ALP-B008 exists to catch, so both
350
+ statuses are excluded from "known" the same way an unknown chip is.
351
+ `partial`/`complete`/`stub` all ship a real `chips/<id>/` driver (even
352
+ a stub has a `.c` file and a declared Kconfig symbol -- see
353
+ `chips/ar0234/`) so only these two "no driver at all" tiers apply.
354
+
355
+ *block_slugs* is that `_BLOCK_SLUGS` set, passed in (see the module
356
+ docstring); empty means "chip manifests only", which is also what the
357
+ SDK file did when it could not import `alp_orchestrate`.
358
+ """
359
+ known = {
360
+ p.stem for p in chip_dir.glob("*.yaml")
361
+ if (_load_metadata_yaml(p) or {}).get("driver_status") not in (
362
+ "planned", "none")
363
+ }
364
+ return known | block_slugs
365
+
366
+
367
+ def _check_chips_known(
368
+ data: dict[str, Any],
369
+ path: Path,
370
+ collector: DiagnosticCollector,
371
+ *,
372
+ chip_dir: Path,
373
+ block_slugs: frozenset[str],
374
+ ) -> None:
375
+ """Reject a `chips:` entry that names no real chip manifest or SDK
376
+ block helper (issue #1224).
377
+
378
+ `chips:` is schema-validated only against a permissive identifier
379
+ regex (`metadata/schemas/board.schema.json`), so a typo (e.g.
380
+ `no_such_chip_xyz`) passes schema validation clean and is emitted
381
+ verbatim into the generated `alp.conf` as
382
+ `CONFIG_ALP_SDK_CHIP_<TYPO>=y` -- a Kconfig symbol nothing declares,
383
+ so the driver is silently not built and the app fails at runtime
384
+ instead of at validation time. Cross-check every token against the
385
+ real chip registry (`metadata/chips/*.yaml`, `chip_id:` == filename
386
+ stem for every manifest) the same way `som.sku` (ALP-B005) and
387
+ `preset:` (ALP-B006) are already cross-checked.
388
+ """
389
+ chips = data.get("chips")
390
+ if not isinstance(chips, list):
391
+ return
392
+
393
+ known = _known_chip_slugs(chip_dir=chip_dir, block_slugs=block_slugs)
394
+ # Point at the `chips` KEY, not the list's VALUE node: for a
395
+ # multi-entry list target="value" gives every offending entry the
396
+ # position of the FIRST list item, so a caret sized to a LATER
397
+ # unknown entry (`span=len(chip)`) would underline an unrelated,
398
+ # valid entry instead (review round 3). The YAML position loader
399
+ # (alp_cli/yaml_pos.py) doesn't track individual list-item
400
+ # positions, so the precise fix -- a caret on the offending token
401
+ # itself -- needs a loader change; anchoring on the unambiguous
402
+ # `chips` key is the correct diagnostic today.
403
+ line, col = node_position(data, "chips", target="key")
404
+ seen: set[str] = set()
405
+ for chip in chips:
406
+ # Non-string / pattern-invalid entries are already reported by
407
+ # the schema pass (ALP-B003/ALP-B004); nothing new to say here.
408
+ if not isinstance(chip, str) or chip in known or chip in seen:
409
+ continue
410
+ seen.add(chip)
411
+ # A manifest with `driver_status: planned` or `driver_status: none`
412
+ # IS a real, committed metadata/chips/<chip>.yaml -- `_known_chip_slugs`
413
+ # excludes it because it declares no ALP_SDK_CHIP_<NAME> Kconfig
414
+ # symbol, not because the manifest is missing. Say that, not "no
415
+ # metadata/chips/<chip>.yaml" (false -- the file is right there)
416
+ # and don't offer a "did you mean" against an unrelated chip for
417
+ # an entry that was spelled correctly (review round 3 major).
418
+ chip_path = chip_dir / f"{chip}.yaml"
419
+ if chip_path.is_file():
420
+ status = (_load_metadata_yaml(chip_path) or {}).get("driver_status")
421
+ message = (
422
+ f"chips: '{chip}' has driver_status: {status} "
423
+ f"-- no Alp SDK driver or ALP_SDK_CHIP_{chip.upper()} "
424
+ f"symbol yet"
425
+ )
426
+ hint = None
427
+ else:
428
+ message = (
429
+ f"chips: unknown chip '{chip}' "
430
+ f"(no metadata/chips/{chip}.yaml)"
431
+ )
432
+ hint = _chip_suggestion(chip, chip_dir=chip_dir, block_slugs=block_slugs)
433
+ collector.add(
434
+ Diagnostic(
435
+ severity="error",
436
+ path=path,
437
+ line=line,
438
+ col=col,
439
+ span=len("chips"),
440
+ code="ALP-B008",
441
+ message=message,
442
+ hint=hint,
443
+ )
444
+ )
445
+
446
+
447
+ def _chip_suggestion(
448
+ chip: str, *, chip_dir: Path, block_slugs: frozenset[str]
449
+ ) -> str | None:
450
+ known = sorted(_known_chip_slugs(chip_dir=chip_dir, block_slugs=block_slugs))
451
+ match = get_close_matches(chip, known, n=1)
452
+ return f"did you mean '{match[0]}'?" if match else None
453
+
454
+
455
+ def _load_metadata_yaml(path: Path) -> dict[str, Any] | None:
456
+ try:
457
+ # libyaml-backed yaml.safe_load: the hottest metadata read in the
458
+ # validator (#2328).
459
+ doc = fast_safe_load(path.read_text(encoding="utf-8")) or {}
460
+ except yaml.YAMLError:
461
+ return None
462
+ return doc if isinstance(doc, dict) else None
463
+
464
+
465
+ def _sku_path(sku: str, *, som_dir: Path) -> Path | None:
466
+ for candidate in som_dir.rglob(f"{sku}.yaml"):
467
+ return candidate
468
+ return None
469
+
470
+
471
+ def _check_board_hosts_som_family(
472
+ data: dict[str, Any],
473
+ path: Path,
474
+ collector: DiagnosticCollector,
475
+ sku: str,
476
+ som_doc: dict[str, Any],
477
+ preset: str,
478
+ board_doc: dict[str, Any],
479
+ *,
480
+ preset_dir: Path,
481
+ ) -> None:
482
+ family = som_doc.get("family")
483
+ allowed = board_doc.get("hosts_som_families") or []
484
+ if not isinstance(family, str) or not isinstance(allowed, list):
485
+ return
486
+ allowed = [str(item) for item in allowed]
487
+ if family in allowed:
488
+ return
489
+
490
+ line, col = node_position(data, "preset", target="value")
491
+ compatible = _compatible_presets(family, preset_dir=preset_dir)
492
+ hint = (
493
+ f"use a board preset whose hosts_som_families includes '{family}'"
494
+ )
495
+ if compatible:
496
+ hint += f" (for example: {', '.join(compatible)})"
497
+ hint += ", or define a compatible board inline"
498
+ collector.add(
499
+ Diagnostic(
500
+ severity="error",
501
+ path=path,
502
+ line=line,
503
+ col=col,
504
+ span=len(preset),
505
+ code="ALP-B007",
506
+ message=(
507
+ f"board preset '{preset}' hosts SoM families {allowed}, "
508
+ f"but {sku} is family '{family}'"
509
+ ),
510
+ hint=hint,
511
+ )
512
+ )
513
+
514
+
515
+ def _compatible_presets(family: str, *, preset_dir: Path) -> list[str]:
516
+ out: list[str] = []
517
+ for board_path in sorted(preset_dir.glob("*.yaml")):
518
+ doc = _load_metadata_yaml(board_path) or {}
519
+ families = doc.get("hosts_som_families") or []
520
+ if isinstance(families, list) and family in [str(item) for item in families]:
521
+ out.append(board_path.stem)
522
+ return out
523
+
524
+
525
+ def _all_skus(*, som_dir: Path) -> list[str]:
526
+ return sorted(p.stem for p in som_dir.rglob("*.yaml") if p.stem.startswith("E1M-"))
527
+
528
+
529
+ def _all_presets(*, preset_dir: Path) -> list[str]:
530
+ return sorted(p.stem for p in preset_dir.glob("*.yaml"))
531
+
532
+
533
+ def _sku_suggestion(sku: str, *, som_dir: Path) -> str | None:
534
+ match = get_close_matches(sku, _all_skus(som_dir=som_dir), n=1)
535
+ return f"did you mean '{match[0]}'?" if match else None
536
+
537
+
538
+ def _preset_suggestion(preset: str, *, preset_dir: Path) -> str | None:
539
+ match = get_close_matches(preset, _all_presets(preset_dir=preset_dir), n=1)
540
+ return f"did you mean '{match[0]}'?" if match else None
541
+
542
+