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,253 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan doctor`'s ADR-0018 curated-library verdict, ported out of alp-sdk.
3
+
4
+ This is the port of `scripts/alp_cli/doctor.py::_check_libraries` (alp-sdk
5
+ ADR-0020 end-state B: the user command surface is `tan`, so a user-facing
6
+ check does not stay behind in alp-sdk as a second CLI). It answers one
7
+ question about the SELECTED PROJECT -- for every curated library the
8
+ project's `board.yaml` asks for, what TIER and LICENCE is it, and can it
9
+ actually be wired on this target?
10
+
11
+ ## Why it resolves rather than merely lists
12
+
13
+ Tier and licence could be read straight out of `metadata/libraries/<name>.yaml`
14
+ with no planner at all. The compatibility half cannot: `requires:` constraints
15
+ are checked against the project's SoC cores and per-core OSes, and a library
16
+ with no `integration:` section for any OS this project runs is a build-time
17
+ refusal. Reporting the first without the second is the failure mode a doctor
18
+ exists to prevent -- a green "lvgl (tier A, MIT)" line on a project where the
19
+ emitter will refuse it. So this routes through
20
+ `tan.planner.libraries.resolve_selection`, the SAME function
21
+ `tan.planner.buildplan` calls when it renders the plan; the report and the
22
+ build can therefore never disagree about a selection.
23
+
24
+ ## WARN, never FAIL
25
+
26
+ An incompatible selection is a WARN here because the hard gate is the emit:
27
+ `tan build` refuses outright. `tan doctor` is a preflight report and a `fail`
28
+ row from it costs exit 4, which would make an unbuildable-library selection
29
+ indistinguishable from a missing toolchain. Same posture alp-sdk's original
30
+ took, kept deliberately.
31
+
32
+ alp-sdk's `alp doctor` had a second lever this port does not carry: `--strict`,
33
+ which folded a WARN row into a nonzero exit (`n_warn` counted the libraries
34
+ row same as any other). `tan doctor` has no `--strict` flag AT ALL -- it is
35
+ not a libraries-specific gap, every WARN-worthy check in this module is
36
+ already exit-0-only (`exit_code_for` above only inspects `fail`). Adding
37
+ `--strict` scoped to just this one row would make it behave differently from
38
+ every other check `tan doctor` reports; that is a real, pre-existing capability
39
+ difference from `alp doctor`, not something this port introduces or reverses,
40
+ and it is accepted deliberately: `tan build`'s own refusal is the actual gate
41
+ a CI pipeline should script against, not a doctor exit code.
42
+
43
+ ## The defect fixed in the port, not carried across
44
+
45
+ alp-sdk's version read the RAW `libraries:` list out of the YAML and passed
46
+ each entry straight into `load_manifest()` and an f-string. board.yaml's
47
+ `libraries:` entries are `{name, cores?}` OBJECTS as well as bare strings
48
+ (`alp_orchestrate.loader._normalize_libraries`, and `tan.planner.loader`'s
49
+ relocated twin), so a core-scoped entry reached `load_manifest` as a dict:
50
+
51
+ $ cd examples/peripheral-io/fmt-formatting
52
+ $ PYTHONPATH=<alp-sdk>/scripts python3 -m alp_cli doctor
53
+ ...
54
+ alp_orchestrate.models.OrchestratorError: unknown library
55
+ `{'name': 'fmt', 'cores': ['m55_hp']}` in `libraries:` -- no manifest at
56
+ metadata/libraries/{'name': 'fmt', 'cores': ['m55_hp']}.yaml.
57
+
58
+ Measured, not reasoned: exit 1, the whole command down, on the SUCCESS path
59
+ (`resolve_selection` had already passed, using the loader's normalised names;
60
+ the raw re-read for the labels is what raised). All 42 in-tree alp-sdk
61
+ examples that select libraries use that shape, so there was no shipped
62
+ project the check worked on. alp-sdk's `docs/getting-started.md` carried it
63
+ as a known open bug; the port is where it goes away.
64
+
65
+ Names therefore come from `scoped_names(project)` -- the loader's own
66
+ normalised, deduplicated, two-channel view -- not from the raw document. The
67
+ dedup matters on its own: `libraries: [fmt, {name: fmt, cores: [m55_hp]}]` is
68
+ schema-valid (`uniqueItems` compares whole entries) and the raw read reports
69
+ `fmt` twice.
70
+
71
+ The raw read survives only as the cheap "is anything selected at all" peek (a
72
+ project selecting nothing must cost nothing and stay silent) and as the label
73
+ fallback when the project could not be LOADED, which is the one path where no
74
+ normalised view exists to take names from.
75
+ """
76
+
77
+ from __future__ import annotations
78
+
79
+ from dataclasses import dataclass
80
+ from pathlib import Path
81
+ from typing import Any
82
+
83
+ __all__ = ["LibraryReport", "inspect_selection"]
84
+
85
+ #: `outcome` values, and what each means for the caller's Check status.
86
+ #:
87
+ #: `ok` every selection resolved and is wireable -> pass
88
+ #: `unresolved` a name, a `requires:` or an `integration:` failed -> warn
89
+ #: `unreadable` `board.yaml` itself could not be parsed -> warn
90
+ #: `unverifiable` libraries ARE selected but nothing could check -> warn
91
+ #: them (no SDK checkout resolved, or the planner is
92
+ #: unavailable/already bound elsewhere in this process)
93
+ OUTCOMES = ("ok", "unresolved", "unreadable", "unverifiable")
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class LibraryReport:
98
+ """One project's curated-library verdict.
99
+
100
+ `labels` are the rendered `name (tier X, LICENCE)` strings, in declaration
101
+ order -- built even on the `unresolved` path, where the offending entry
102
+ reads `name (unknown)`, because "which of my five libraries is the broken
103
+ one" is the question that report has to answer.
104
+
105
+ `detail` carries the underlying refusal verbatim on every non-`ok`
106
+ outcome: it is the emitter's OWN message (`OrchestratorError`), so the
107
+ warning a customer reads here is word-for-word the error `tan build`
108
+ will print, not a paraphrase of it.
109
+ """
110
+
111
+ outcome: str
112
+ names: tuple[str, ...]
113
+ labels: tuple[str, ...] = ()
114
+ detail: str = ""
115
+
116
+ def __post_init__(self) -> None:
117
+ if self.outcome not in OUTCOMES:
118
+ raise ValueError(f"unknown library-report outcome {self.outcome!r}; expected one of {OUTCOMES}")
119
+
120
+
121
+ def _raw_names(document: Any) -> list[str]:
122
+ """The `libraries:` names as WRITTEN, for the two paths that have no
123
+ loaded project to ask (the emptiness peek, and the label fallback when
124
+ loading failed). Both entry shapes are accepted -- see the module
125
+ docstring for why reading only the string shape was a defect. NOT
126
+ deduplicated and NOT canonicalised: no loader has run on either path, so
127
+ what the customer wrote is all there is to report."""
128
+ if not isinstance(document, dict):
129
+ return []
130
+ entries = document.get("libraries") or []
131
+ if not isinstance(entries, list):
132
+ return []
133
+ names: list[str] = []
134
+ for entry in entries:
135
+ if isinstance(entry, str):
136
+ names.append(entry)
137
+ elif isinstance(entry, dict) and entry.get("name"):
138
+ names.append(str(entry["name"]))
139
+ return names
140
+
141
+
142
+ def _labels(names, load_manifest, metadata_root: Path) -> tuple[str, ...]:
143
+ """`name (tier X, LICENCE)` per selection, in declaration order.
144
+
145
+ Per-name `try`, not one around the loop: on the `unresolved` path exactly
146
+ one entry is usually the broken one, and "which of my five" is the whole
147
+ question that report answers. A single guard would collapse all five to
148
+ `(unknown)` the moment any one of them failed to load."""
149
+ out: list[str] = []
150
+ for name in names:
151
+ try:
152
+ manifest = load_manifest(name, metadata_root)
153
+ except Exception:
154
+ out.append(f"{name} (unknown)")
155
+ continue
156
+ out.append(f"{name} (tier {manifest.get('tier', '?')}, {manifest.get('license', '?')})")
157
+ return tuple(out)
158
+
159
+
160
+ def _unverifiable(names: list[str], detail: str) -> LibraryReport:
161
+ return LibraryReport(
162
+ "unverifiable", tuple(names), tuple(f"{n} (unverified)" for n in names), detail
163
+ )
164
+
165
+
166
+ def _planner(sdk_root: Path):
167
+ """`(load_board_yaml, load_manifest, resolve_selection, scoped_names)`.
168
+
169
+ Binds BEFORE importing `tan.planner`: `tan/planner/paths.py` evaluates
170
+ `REPO = sdk_root()` at module scope and half the package freezes
171
+ `metadata_root` as a default argument, so an unbound import cannot be
172
+ repaired afterwards (`tan.planner_root`). A rebind to a DIFFERENT root in
173
+ an already-planner-bound process raises `PlannerRootError` by design --
174
+ doctor is a REPORT and must not be the thing that decides which SDK a
175
+ process is pointed at, so the caller turns that into `unverifiable`
176
+ rather than forcing it."""
177
+ from tan.planner_root import bind_sdk_root
178
+
179
+ bind_sdk_root(sdk_root)
180
+ from tan.planner import load_board_yaml
181
+ from tan.planner.libraries import load_manifest, resolve_selection, scoped_names
182
+
183
+ return load_board_yaml, load_manifest, resolve_selection, scoped_names
184
+
185
+
186
+ def inspect_selection(
187
+ board_yaml: Path | None, sdk_root: Path | None
188
+ ) -> LibraryReport | None:
189
+ """Resolve the project's `libraries:` selection, or `None` for nothing to say.
190
+
191
+ `None` -- not a status, an ABSENCE -- for no `board.yaml` in scope and for
192
+ a `board.yaml` that selects no libraries; see `doctor_cmd.libraries_check`
193
+ for why a "0 selected" row is worse than no row. A project that DOES
194
+ select libraries always gets one, including when nothing could be
195
+ verified.
196
+
197
+ Never raises: every failure becomes an outcome, because this runs inside
198
+ `_collect`, whose stated invariant is that no probe may raise.
199
+ """
200
+ if board_yaml is None or not board_yaml.is_file():
201
+ return None
202
+ try:
203
+ import yaml
204
+
205
+ document = yaml.safe_load(board_yaml.read_text(encoding="utf-8")) or {}
206
+ except Exception as err:
207
+ # `boardYaml` owns the verdict on the file itself and runs immediately
208
+ # above this, so the wording here stays scoped to the library line.
209
+ return LibraryReport("unreadable", (), (), f"could not read {board_yaml}: {err}")
210
+
211
+ raw = _raw_names(document)
212
+ if not raw:
213
+ return None
214
+ if sdk_root is None:
215
+ return _unverifiable(
216
+ raw, "no alp-sdk checkout resolved, so metadata/libraries/*.yaml could not be read"
217
+ )
218
+ return _resolve(board_yaml, Path(sdk_root), raw)
219
+
220
+
221
+ def _resolve(board_yaml: Path, sdk_root: Path, raw: list[str]) -> LibraryReport | None:
222
+ """The second half of `inspect_selection`: does what the customer asked
223
+ for actually resolve against THIS SDK checkout and THIS target? Split from
224
+ the first half at the point the planner is needed -- everything above it
225
+ is a read of the customer's own document and answers with no SDK at all.
226
+ """
227
+ try:
228
+ load_board_yaml, load_manifest, resolve_selection, scoped_names = _planner(sdk_root)
229
+ except Exception as err:
230
+ return _unverifiable(raw, f"the library metadata in {sdk_root} could not be read: {err}")
231
+
232
+ metadata_root = sdk_root / "metadata"
233
+ try:
234
+ project = load_board_yaml(board_yaml, metadata_root=metadata_root)
235
+ except Exception as err:
236
+ # No loaded project means no normalised name list to take -- the raw
237
+ # spelling is all there is on this one path.
238
+ return LibraryReport(
239
+ "unresolved", tuple(raw), _labels(raw, load_manifest, metadata_root), str(err)
240
+ )
241
+
242
+ # `scoped_names`, not the raw list: the loader's own view of the selection,
243
+ # covering BOTH declaration channels with aliases already applied -- see
244
+ # the module docstring for the defect that reading `raw` here produced.
245
+ names = tuple(scoped_names(project))
246
+ if not names:
247
+ return None
248
+ labels = _labels(names, load_manifest, metadata_root)
249
+ try:
250
+ resolve_selection(project, metadata_root)
251
+ except Exception as err:
252
+ return LibraryReport("unresolved", names, labels, str(err))
253
+ return LibraryReport("ok", names, labels)
@@ -0,0 +1,185 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pure text-mode rendering for `tan doctor`.
3
+
4
+ `doctor_cmd.py`'s text branch used to be one `print` per check, with `detail`
5
+ and `fix` as unwrapped strings: on a real terminal that wraps at column 0, a
6
+ long `fix:` body visually merges into the next `[ pass]` row, and there is
7
+ no colour to tell `fail` from `pass` at a glance. This module fixes both,
8
+ plus adds a failures-only footer so a customer with several failures gets an
9
+ action list without re-reading the whole log.
10
+
11
+ v2 (three changes, bundled because they interact -- see
12
+ `doctor-v2-brief.md`): (1) one FIXED continuation indent for both `detail`
13
+ and `fix` bodies, replacing the old per-check-name-length indent, which is
14
+ also what makes (3) possible -- a check can be rendered without knowing any
15
+ other check's name length; (2) the failures footer now names the start of
16
+ each fix, not just the bare check name; (3) `render_check_lines` (one
17
+ check) and `render_doctor_footer` (everything after the checks) are exposed
18
+ separately so `doctor_cmd`'s streaming path can print each check as it
19
+ completes and call the footer once, at the end, instead of only ever having
20
+ the all-at-once `render_doctor_lines` this module used to expose.
21
+
22
+ Pure and IO-free -- no terminal probe, no `print` -- so it is testable
23
+ without one; `doctor_cmd.doctor` is the only caller, and reads `width` off
24
+ `shutil.get_terminal_size()` and `color` off `tan.env.use_color` before
25
+ calling in.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from tan.core.text_layout import wrap_block
30
+
31
+ # colorama Fore/Style literals, matching `tan.core.size`'s and
32
+ # `tan.core.faultdecode`'s convention rather than a new dependency.
33
+ _GREEN = "\x1b[32m"
34
+ _YELLOW = "\x1b[33m"
35
+ _RED = "\x1b[31m"
36
+ _CYAN = "\x1b[36m"
37
+ _RESET = "\x1b[0m"
38
+
39
+ _STATUS_HUE: dict[str, str] = {
40
+ "pass": _GREEN,
41
+ "warn": _YELLOW,
42
+ "fail": _RED,
43
+ "unknown": _CYAN,
44
+ }
45
+
46
+ #: Every continuation line -- wrapped `detail` text AND the `fix:` block --
47
+ #: lands on this ONE column now (review O-2: the old code re-derived the
48
+ #: detail indent from `len(prefix)`, so it drifted with name length (15 for
49
+ #: `sdk`, 23 for `pythonFloor`), while the fix block used its own separate
50
+ #: fixed 9 -- two rails that agreed with neither each other nor themselves
51
+ #: down the report). 10 is `len("[ fail] ")`, the bracket+status prefix
52
+ #: every check shares regardless of its name -- the floor a continuation can
53
+ #: never sit left of without reading as a new entry rather than one -- so a
54
+ #: continuation lands exactly under where the check's own name starts, for
55
+ #: every check, independent of that name's length.
56
+ _CONTINUATION_INDENT = 10
57
+
58
+ #: `fix: ` printed at `_CONTINUATION_INDENT`, like a second paragraph under
59
+ #: the check -- its own wrapped continuation lines land back on that SAME
60
+ #: column, not indented further under the label, which is the "same rail
61
+ #: for detail and fix" this task's change 1 asks for.
62
+ _FIX_PREFIX = " " * _CONTINUATION_INDENT + "fix: "
63
+
64
+ #: ~75 characters of a failing check's `fix` text in the footer row (review
65
+ #: range: 70-80). A CHARACTER budget, not a sentence split -- the `sdk`
66
+ #: check's own fix (its longest) is one comma-joined clause with no sentence
67
+ #: break, so first-sentence extraction would not shorten it at all.
68
+ _FOOTER_FIX_BUDGET = 75
69
+
70
+
71
+ def _truncate_fix(fix: str, budget: int = _FOOTER_FIX_BUDGET) -> str:
72
+ """`fix`, cut to `budget` characters on a word boundary where the budget
73
+ lands inside one, with a single `…` appended -- never a mid-word cut,
74
+ never a bare truncation with no marker. Returns `fix` byte-for-byte when
75
+ it already fits, with no trailing `…` added to text that was not cut."""
76
+ if len(fix) <= budget:
77
+ return fix
78
+ cut = fix[:budget]
79
+ boundary = cut.rfind(" ")
80
+ if boundary > 0:
81
+ cut = cut[:boundary]
82
+ return f"{cut}…"
83
+
84
+
85
+ def render_check_lines(check: dict, width: int, color: bool) -> list[str]:
86
+ """`[status] name: detail`, wrapped, plus an indented `fix:` block when
87
+ the check carries one -- ONE check's lines. `render_doctor_lines` below
88
+ (the batch path) and `doctor_cmd`'s streaming path (print the moment a
89
+ check completes, change 3) both call this SAME function, so the two
90
+ never fork into separate formatting code. The status token's OWN width
91
+ (`:>7`, matching `unknown`, the longest status word) is computed on the
92
+ plain text first, so a painted token never throws off the hanging
93
+ indent's alignment -- the ANSI bytes are invisible and must not count as
94
+ columns.
95
+ """
96
+ status = check["status"]
97
+ name = check["name"]
98
+ prefix = f"[{status:>7}] {name}: "
99
+ indent = " " * _CONTINUATION_INDENT
100
+ lines = wrap_block(check["detail"], width, prefix, indent)
101
+
102
+ hue = _STATUS_HUE.get(status)
103
+ if color and hue:
104
+ painted = f"[{hue}{status:>7}{_RESET}] {name}: "
105
+ lines[0] = painted + lines[0][len(prefix) :]
106
+
107
+ fix = check.get("fix")
108
+ if fix is not None:
109
+ lines.extend(wrap_block(fix, width, _FIX_PREFIX, indent))
110
+ return lines
111
+
112
+
113
+ def render_doctor_footer(
114
+ checks: list[dict], summary: dict, extra_issue_lines: list[str], width: int
115
+ ) -> list[str]:
116
+ """Everything AFTER the per-check blocks, in order:
117
+
118
+ 1. `extra_issue_lines` verbatim (already filtered by the caller against
119
+ `checked_codes`, tan-cli#375), each wrapped through `wrap_block` at
120
+ `width` -- the same seam every check block above already goes
121
+ through. Measured before this wrapping existed: a real
122
+ `doctor.fix-suppressed` line ran to 262 columns, unwrapped, inside
123
+ the very report this whole task exists to wrap.
124
+ 2. A failures-only footer, omitted entirely when nothing failed -- each
125
+ row names the START of its own fix, truncated to
126
+ `_FOOTER_FIX_BUDGET`, then wrapped the same way as (1): truncation
127
+ keeps the row SHORT, wrapping keeps whatever survives that cut
128
+ actually on screen. Measured unwrapped: `4 + len(name) + 2 +
129
+ _FOOTER_FIX_BUDGET + 1` (the trailing `+ 1` is `_truncate_fix`'s own
130
+ `…`) = 107 columns for `zephyrSdkAvailableForHost` (25 chars). A
131
+ failing check with no `fix` falls back to the bare `- <name>` row.
132
+ 3. The summary line, last, never wrapped -- one short, fixed sentence.
133
+
134
+ `width` is the SAME value `doctor_cmd.doctor` resolves for the per-check
135
+ blocks above -- never a second, independently floored number. Every
136
+ continuation in (1)/(2) lands at `_CONTINUATION_INDENT`, the same rail
137
+ check `detail`/`fix` continuations already use.
138
+
139
+ Split out from `render_doctor_lines` so `doctor_cmd`'s streaming path
140
+ can print each check as it completes and call this once, for the tail.
141
+ """
142
+ hanging = " " * _CONTINUATION_INDENT
143
+ lines: list[str] = []
144
+ for issue_line in extra_issue_lines:
145
+ lines.extend(wrap_block(issue_line, width, "", hanging))
146
+
147
+ failed = [c for c in checks if c["status"] == "fail"]
148
+ if failed:
149
+ lines.append("")
150
+ lines.append("Failed checks:")
151
+ for c in failed:
152
+ fix = c.get("fix")
153
+ body = f"{c['name']}: {_truncate_fix(fix)}" if fix else c["name"]
154
+ lines.extend(wrap_block(body, width, " - ", hanging))
155
+
156
+ lines.append("")
157
+ lines.append(
158
+ f"{summary['pass']} passed, {summary['warn']} warning(s), {summary['fail']} failed."
159
+ )
160
+ return lines
161
+
162
+
163
+ def render_doctor_lines(
164
+ checks: list[dict],
165
+ summary: dict,
166
+ extra_issue_lines: list[str],
167
+ width: int,
168
+ color: bool,
169
+ ) -> list[str]:
170
+ """The lines `doctor_cmd.doctor`'s text branch prints, in order: one
171
+ block per check (`render_check_lines`, SAME order `checks` arrives in --
172
+ the order is a diagnostic progression, `hostPrerequisites` failing is why
173
+ `west` fails too, and reordering would hide that), then the tail
174
+ (`render_doctor_footer`).
175
+
176
+ The one-call batch form, for a caller holding the full check list up
177
+ front (this module's own tests); the streaming caller in `doctor_cmd`
178
+ calls `render_check_lines`/`render_doctor_footer` directly instead, see
179
+ their docstrings for why.
180
+ """
181
+ lines: list[str] = []
182
+ for check in checks:
183
+ lines.extend(render_check_lines(check, width, color))
184
+ lines.extend(render_doctor_footer(checks, summary, extra_issue_lines, width))
185
+ return lines
@@ -0,0 +1,66 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The `scope` every `tan doctor` check carries on the wire (tan-cli#549).
3
+
4
+ A consumer rendering the doctor report has to split HOST rows from PROJECT
5
+ rows -- the dependency panel in `alp-sdk-vscode` shows the host half with no
6
+ folder open. Before this field the only handle was `checks[].name`, so the
7
+ split was a hand-written list of another program's identifiers. That list went
8
+ stale across two pin bumps and shipped `zephyrSdkHost`, an id this CLI stopped
9
+ emitting when the check was renamed `zephyrSdkAvailableForHost`; nothing
10
+ failed, the row was simply never admitted, and a missing row reads to a user
11
+ as "not a problem" rather than "not asked" (alp-sdk-vscode#472, patched
12
+ downstream in alp-sdk-vscode#487 with the caveat that a re-derived hand-list
13
+ rots again).
14
+
15
+ ## The two values, and the rule that decides between them
16
+
17
+ The question is **what the check's verdict is ABOUT** -- its subject -- not
18
+ which inputs it happened to read on the way to that verdict.
19
+
20
+ `host`
21
+ The subject is THIS MACHINE: a tool on PATH, an OS setting, the host
22
+ interpreter, the home directory, whether the pinned Zephyr SDK publishes a
23
+ build for this OS/arch. A project or SDK fact may refine the threshold or
24
+ the wording (`hostPython`'s floor is the higher of the SDK manifest's and
25
+ the workspace Zephyr's), but the thing being verdicted is the host, and
26
+ the row is worth rendering with no folder open.
27
+
28
+ `project`
29
+ The subject is the SELECTED PROJECT and the alp-sdk checkout / Zephyr
30
+ workspace resolved for it: whether a `board.yaml` is there, whether a
31
+ workspace resolved, whether that workspace's Zephyr matches the SDK's own
32
+ pin, where this venv or SDK came from. With nothing selected these still
33
+ report -- honestly, against "nothing selected" -- but they answer a
34
+ question the user has not asked yet.
35
+
36
+ The one judgement call worth naming: `sdk` and `pythonFloor` are `project`.
37
+ `sdk`'s top resolution tier is the machine-global `~/.alp/sdk-default`, which
38
+ argues for `host`; its subject is still "which checkout will THIS project
39
+ build against", and its lower tiers (`.alp/sdk-path`, the positional walk) are
40
+ directory-dependent. `pythonFloor` reads as a host fact from its name, but
41
+ both its arms verdict a DECLARATION -- the SDK manifest's `pythonMinVersion`
42
+ against the workspace Zephyr's, or the absence of a manifest to read -- and
43
+ its remedy in the no-SDK arm is "resolve an alp-sdk checkout". The host-side
44
+ half of that pair is `hostPython`, which is `host`.
45
+
46
+ ## Why the vocabulary lives here rather than beside `Check`
47
+
48
+ Nothing in this module imports typer or touches IO, so the gate that walks
49
+ every `Check(...)` call site (`python/tests/gates/test_doctor_check_scope.py`)
50
+ and `tan/commands/support_bundle_cmd.py`, which builds a doctor-shaped report
51
+ of its own, can both read the vocabulary without importing a command module.
52
+ """
53
+ from __future__ import annotations
54
+
55
+ #: The check verdicts a fact about this machine. See the module docstring.
56
+ HOST = "host"
57
+
58
+ #: The check verdicts a fact about the selected project/SDK/workspace.
59
+ PROJECT = "project"
60
+
61
+ #: The complete wire vocabulary, in the order the contract documents it. A
62
+ #: value outside this tuple is refused at construction (`Check.__post_init__`)
63
+ #: rather than shipped: a consumer filtering on `scope` cannot tell an
64
+ #: unrecognised value from a row it should hide, which is the same fail-open
65
+ #: silence the name-matching seam had.
66
+ CHECK_SCOPES: tuple[str, ...] = (HOST, PROJECT)