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,644 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan examples` -- the SDK's ready-made example projects, as a catalogue the
3
+ New Project flow scaffolds from via `tan init --from-example <sourceDir>`.
4
+
5
+ Mirrors `crates/tan-cli/src/commands/examples.rs` plus the two `tan-core`
6
+ helpers it composes (`wizard/filesystem.rs::discover_examples`,
7
+ `wizard/service/example_catalog.rs`). Kept in ONE module rather than split
8
+ across a `tan.core` twin: the whole thing is a directory walk and two README
9
+ scans, and a second file would only add an import.
10
+
11
+ **Which rule decides where the catalogue comes from.** The example list is
12
+ alp-sdk content, so the two candidate readings are "vendor a capture like
13
+ `tan init` does" and "read the resolved checkout". This reads the checkout, on
14
+ two invariants from
15
+ `alp-sdk/docs/superpowers/specs/2026-07-29-tan-port-invariants.md`:
16
+
17
+ * **I-32** bans *shelling* the SDK -- `tan init`/`tan scaffold` read a vendored
18
+ `--emit scaffold` tree "without ever shelling the SDK" because they must work
19
+ with NO checkout present at all. Nothing here spawns a subprocess; a
20
+ `read_dir` over `<sdk>/examples` is not a shell-out, so I-32 is untouched. Note
21
+ the anti-pattern it names (#22 in that doc) is *shelling* init instead of
22
+ reading the vendored tree -- not "reading a checkout".
23
+ * **I-26** ("tan must never learn a hardware fact ... `metadata/**` stays in
24
+ alp-sdk") is what forbids the vendored alternative HERE. A baked example
25
+ catalogue is a copy of alp-sdk content that drifts from the customer's actual
26
+ checkout, and every `sourceDir` it hands back is a path
27
+ `tan init --from-example` then cannot find. `tan examples`' requirement is the
28
+ inverse of `tan init`'s: with no checkout resolvable the catalogue is
29
+ legitimately EMPTY (exit 0, `examples: []`), which is exactly what the Rust
30
+ oracle does -- so it has no reason to carry a capture.
31
+
32
+ **One DELIBERATE divergence from the oracle** (tan-cli#400): an unresolvable
33
+ checkout also emits an `examples.sdk-root-unresolved` warning, which
34
+ `crates/tan-cli/src/commands/examples.rs` does not. That is a contract
35
+ ADDITION, not a port regression -- the oracle's own renderer-only hint is the
36
+ defect, and `crates/` ships to nobody (the release assets are PyInstaller
37
+ freezes of `python/tan`). Do not "restore parity" by deleting it; nothing in
38
+ `tests/parity/` pins this command's text or issues.
39
+
40
+ **Every failure is an envelope, never a traceback.** Discovery swallows the IO
41
+ errors the Rust swallows (an unreadable root or category is skipped, a README
42
+ that will not read or is not UTF-8 is treated as absent -- `read_to_string().ok()`
43
+ on the Rust side), and the catch-all in `examples` is the backstop for whatever
44
+ nobody enumerated. There is no subprocess to time out: this command spawns
45
+ nothing.
46
+
47
+ No `--os`, no `--backend`, no SKU list, no hardware facts -- the catalogue is
48
+ whatever directories carry a `board.yaml`, and nothing here reads one.
49
+
50
+ **tan-cli#484** adds `category`/`som`/`board`/`cores`/`coreCount`/`osSet`/
51
+ `declares` to each row, additively, when alp-sdk's generated
52
+ `metadata/catalog.json` has an entry for that `sourceDir`
53
+ (`tan.core.example_facets.load_example_facets`). That file is itself a
54
+ pre-computed artefact of alp-sdk's topology resolver, so this still does not
55
+ read a `board.yaml` or re-derive a hardware fact -- it reads one more
56
+ generated JSON file, the same shape `tan.core.error_catalog` already reads
57
+ `metadata/error-catalog.json` in.
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ import json
63
+ import os
64
+ import sys
65
+ from dataclasses import dataclass
66
+ from pathlib import Path
67
+
68
+ import typer
69
+
70
+ from tan.core.example_facets import ExampleFacets, load_example_facets
71
+ from tan.core.sdk_discovery import (
72
+ global_default_foreign_project_issue,
73
+ project_pin_issue,
74
+ resolve_sdk_root_wide,
75
+ sdk_ladder_divergence_issue,
76
+ with_sdk_search,
77
+ )
78
+ from tan.core.shapes import rejected_sdk_root_message
79
+ from tan.core.global_flags import accept_global_flags
80
+ from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
81
+ from tan.exit_codes import ExitCode
82
+ from tan.output_format import FORMAT_HELP, OutputFormat
83
+
84
+ #: `data.schemaVersion` for this command's payload.
85
+ DATA_SCHEMA_VERSION = "1"
86
+
87
+ #: First bytes that mark a README line as markdown STRUCTURE rather than prose.
88
+ #: Verbatim from `is_readme_prose` (`wizard/service/example_catalog.rs`) --
89
+ #: headings, blockquotes, HTML, links, badges, tables, list/rule markers, code.
90
+ _NON_PROSE_PREFIXES = "#><[!|-*+=`"
91
+
92
+ #: tan-cli#400: "no checkout resolved" is a DIAGNOSIS, not a shrug. Until this
93
+ #: landed the condition existed only as a parenthetical question in the text
94
+ #: renderer; the envelope carried `ok:true`, `examples: []`, `issues: []` --
95
+ #: indistinguishable from a healthy SDK that genuinely ships no examples. The
96
+ #: extension's picker therefore rendered an empty list with no error, no log
97
+ #: line and no warning, and `tan init --from-example <sourceDir>` was
98
+ #: unreachable with nothing pointing at the misconfigured root.
99
+ #:
100
+ #: Deliberately mirrors `presets_cmd.py`'s `SDK_UNRESOLVED_CODE` /
101
+ #: `SDK_UNRESOLVED_MESSAGE` -- same `<command>.sdk-root-unresolved` spelling and
102
+ #: same "condition, then what you got instead, then the remediation" shape that
103
+ #: `presets`, `pinmux`, `build`, `generate`, `init`, `model` and `trace` all
104
+ #: already use for this identical bad path. `reserved`, not `frozen`, in
105
+ #: `contract/issue-codes.json`: no consumer matches it with `===` yet.
106
+ SDK_UNRESOLVED_CODE = "examples.sdk-root-unresolved"
107
+ SDK_UNRESOLVED_MESSAGE = (
108
+ "alp-sdk root is unresolved. Returning an empty example catalogue; "
109
+ "pass --sdk-root <path> to name the checkout."
110
+ )
111
+
112
+ #: What the reader GOT instead, for the `--sdk-root`-was-given-and-rejected
113
+ #: branch (`tan.core.shapes.rejected_sdk_root_message`, tan-cli#497). The remediation
114
+ #: clause above is dropped there: recommending the flag the caller just typed
115
+ #: is what made the old message self-defeating.
116
+ SDK_UNRESOLVED_CONSEQUENCE = "Returning an empty example catalogue."
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class Example:
121
+ """One catalogue entry. `id`/`sourceDir`/`title`/`description` are the
122
+ emitted JSON order the frozen oracle also uses, matching `ExampleEntry`'s
123
+ declaration order in `examples.rs`. `facets` (tan-cli#484) is an ADDITIVE
124
+ Python-only extension the oracle never carried -- when
125
+ `metadata/catalog.json` has an entry for this `sourceDir`, its fields are
126
+ appended after the original four, in the order alp-sdk's own issue
127
+ illustrated: `category`, `som`, `board`, `cores`, `coreCount`, `osSet`,
128
+ `declares`. `facets is None` (an older SDK, or an example the catalogue
129
+ does not know about yet) reproduces the pre-#484 four-key wire shape
130
+ exactly -- no existing reader breaks."""
131
+
132
+ id: str
133
+ source_dir: str
134
+ title: str
135
+ description: str
136
+ facets: ExampleFacets | None = None
137
+
138
+ def as_dict(self) -> dict[str, object]:
139
+ out: dict[str, object] = {
140
+ "id": self.id,
141
+ "sourceDir": self.source_dir,
142
+ "title": self.title,
143
+ "description": self.description,
144
+ }
145
+ facets = self.facets
146
+ if facets is not None:
147
+ out["category"] = facets.category
148
+ if facets.som is not None:
149
+ out["som"] = facets.som
150
+ if facets.board is not None:
151
+ out["board"] = facets.board
152
+ if facets.cores is not None:
153
+ out["cores"] = [dict(core) for core in facets.cores]
154
+ if facets.core_count is not None:
155
+ out["coreCount"] = facets.core_count
156
+ if facets.os_set is not None:
157
+ out["osSet"] = list(facets.os_set)
158
+ if facets.declares is not None:
159
+ out["declares"] = dict(facets.declares)
160
+ return out
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Discovery
165
+ # ---------------------------------------------------------------------------
166
+
167
+
168
+ def _read_readme(example_dir: Path) -> str | None:
169
+ """A `README.md`'s text, or `None` when there is not a usable one.
170
+
171
+ Missing, unreadable, and non-UTF-8 all collapse to `None`, matching Rust's
172
+ `std::fs::read_to_string(...).ok()`. `encoding="utf-8"` is explicit (I-27):
173
+ the default would decode with the host locale, so a README carrying a `⚠️`
174
+ -- alp-sdk already ships several -- raises `UnicodeDecodeError` on a cp1252
175
+ Windows host and passes on ubuntu CI.
176
+ """
177
+ try:
178
+ return (example_dir / "README.md").read_text(encoding="utf-8")
179
+ except (OSError, UnicodeDecodeError, ValueError):
180
+ return None
181
+
182
+
183
+ def _subdirectories(root: Path) -> list[Path]:
184
+ """`root`'s immediate subdirectories, or `[]` when it will not read.
185
+
186
+ `follow_symlinks=False` deliberately: Rust's `DirEntry::file_type()` does
187
+ not traverse a symlink, so a symlinked example directory is not a directory
188
+ to the oracle and is skipped. (The `board.yaml` probe below DOES follow, as
189
+ `Path::is_file()` does.) Order is not relied on -- results are sorted.
190
+ """
191
+ try:
192
+ with os.scandir(root) as entries:
193
+ return [
194
+ Path(entry.path)
195
+ for entry in entries
196
+ if _is_dir_no_follow(entry)
197
+ ]
198
+ except OSError:
199
+ return []
200
+
201
+
202
+ def _is_dir_no_follow(entry: os.DirEntry) -> bool:
203
+ try:
204
+ return entry.is_dir(follow_symlinks=False)
205
+ except OSError:
206
+ return False
207
+
208
+
209
+ def discover_examples(
210
+ examples_root: Path,
211
+ facets_by_source_dir: dict[str, ExampleFacets] | None = None,
212
+ ) -> list[Example]:
213
+ """Scan `<sdk>/examples` exactly two levels deep for `<category>/<name>`
214
+ directories carrying a `board.yaml`, sorted by source dir.
215
+
216
+ A missing or unreadable root is an EMPTY list, never an error: "no SDK" and
217
+ "no examples" are the same renderable answer, and `tan examples` returning
218
+ exit 0 with `examples: []` is what lets the New Project flow say "no
219
+ examples available" instead of showing a failure.
220
+
221
+ `facets_by_source_dir` (tan-cli#484, `tan.core.example_facets`) is looked
222
+ up by the SAME `source_dir` this function derives -- a directory this walk
223
+ finds but the catalogue does not know about (a newer example than the
224
+ checkout's committed `metadata/catalog.json`) simply gets no facets, same
225
+ as an omitted or unreadable catalogue; discovery is never gated on it.
226
+ """
227
+ facets_by_source_dir = facets_by_source_dir or {}
228
+ found: list[Example] = []
229
+ for category in _subdirectories(examples_root):
230
+ for project in _subdirectories(category):
231
+ try:
232
+ has_board = (project / "board.yaml").is_file()
233
+ except OSError:
234
+ has_board = False
235
+ if not has_board:
236
+ continue
237
+ # Always `/`-joined: `sourceDir` is handed straight back to
238
+ # `tan init --from-example`, and a Windows `\` there is not a path
239
+ # the SDK examples tree can be indexed by.
240
+ source_dir = f"{category.name}/{project.name}"
241
+ readme = _read_readme(project)
242
+ found.append(
243
+ Example(
244
+ id=example_id_from_source_dir(source_dir),
245
+ source_dir=source_dir,
246
+ title=example_title_from_readme(readme, source_dir),
247
+ description=example_description_from_readme(readme),
248
+ facets=facets_by_source_dir.get(source_dir),
249
+ )
250
+ )
251
+ found.sort(key=lambda e: e.source_dir)
252
+ return found
253
+
254
+
255
+ # ---------------------------------------------------------------------------
256
+ # README-derived metadata (pure)
257
+ # ---------------------------------------------------------------------------
258
+
259
+
260
+ def example_id_from_source_dir(source_dir: str) -> str:
261
+ """The source dir is already unique across the catalogue, so it doubles as
262
+ the id. Kept as a named function because `id == sourceDir` is a contract the
263
+ extension may not assume forever, not an accident to inline away."""
264
+ return source_dir
265
+
266
+
267
+ def example_title_from_readme(readme: str | None, source_dir: str) -> str:
268
+ """The README's first markdown heading, else a title-cased leaf name."""
269
+ for line in (readme or "").splitlines():
270
+ trimmed = line.strip()
271
+ if trimmed.startswith("#"):
272
+ title = trimmed.lstrip("#").strip()
273
+ if title:
274
+ return title
275
+ return _title_case_leaf(source_dir)
276
+
277
+
278
+ def example_description_from_readme(readme: str | None) -> str:
279
+ """The README's first prose line -- skipping blanks, headings, blockquotes,
280
+ badges/links, HTML, tables, list markers and rules. `""` when there is
281
+ none."""
282
+ for line in (readme or "").splitlines():
283
+ trimmed = line.strip()
284
+ if trimmed and trimmed[0] not in _NON_PROSE_PREFIXES:
285
+ return trimmed
286
+ return ""
287
+
288
+
289
+ def _title_case_leaf(source_dir: str) -> str:
290
+ """`audio/i2s-tone` -> `I2s Tone`: split the leaf on `-`/`_`, upper-case each
291
+ word's first character only, and leave the rest of the word as found (so it
292
+ really is `I2s`, not `I2S` -- the fallback is deliberately dumb, a README
293
+ heading is how an example spells its own name)."""
294
+ leaf = source_dir.rsplit("/", 1)[-1]
295
+ words = [w for w in leaf.replace("_", "-").split("-") if w]
296
+ return " ".join(w[:1].upper() + w[1:] for w in words)
297
+
298
+
299
+ def strip_markdown_noise(text: str) -> str:
300
+ """Drop `![alt](url)` image/badge spans and `**` emphasis markers.
301
+
302
+ TEXT MODE ONLY -- `data.examples[]` keeps the raw README-derived value, so
303
+ the JSON a consumer parses is unaffected. A `![` with no closing `)` is not
304
+ markdown: keep it verbatim rather than eating the rest of the line.
305
+ """
306
+ out: list[str] = []
307
+ rest = text
308
+ while True:
309
+ start = rest.find("![")
310
+ if start == -1:
311
+ break
312
+ out.append(rest[:start])
313
+ end = rest.find(")", start)
314
+ if end == -1:
315
+ out.append(rest[start:])
316
+ rest = ""
317
+ break
318
+ rest = rest[end + 1 :]
319
+ out.append(rest)
320
+ return "".join(out).replace("**", "").strip()
321
+
322
+
323
+ # ---------------------------------------------------------------------------
324
+ # Filtering and text rendering
325
+ # ---------------------------------------------------------------------------
326
+
327
+
328
+ def example_matches_filter(entry: Example, needle: str) -> bool:
329
+ """Case-insensitive substring of `id` or `title`. Deliberately NOT
330
+ `sourceDir`/`description`: the former is the same string as `id`, the latter
331
+ is too free-form for a quick catalogue scan to key on."""
332
+ lowered = needle.lower()
333
+ return lowered in entry.id.lower() or lowered in entry.title.lower()
334
+
335
+
336
+ def example_category(entry: Example) -> str:
337
+ """The example's top-level directory -- `ai` for `ai/cold-chain-monitor`.
338
+ Prefers the catalogue's `category` facet (tan-cli#484) when the bound
339
+ checkout's `metadata/catalog.json` carries one for this example -- the
340
+ same producer-wins fallback order `alp-sdk-vscode`'s own
341
+ `exampleCategory()` uses ("the day tan forwards a category that
342
+ disagrees with the directory, the producer is right") -- and falls back
343
+ to the id prefix only when no facet is present (an older SDK, or an
344
+ example the catalogue does not know about yet)."""
345
+ facets = entry.facets
346
+ if facets is not None:
347
+ return facets.category
348
+ return entry.id.split("/", 1)[0]
349
+
350
+
351
+ def example_matches_category(entry: Example, category: str) -> bool:
352
+ return example_category(entry).lower() == category.lower()
353
+
354
+
355
+ def render_examples_text(
356
+ examples: list[Example],
357
+ filter_: str | None,
358
+ category: str | None,
359
+ verbose: bool,
360
+ sdk_resolved: bool,
361
+ ) -> list[str]:
362
+ """One `id title` line per example, `id` column aligned to the longest id in
363
+ THIS (possibly filtered) result set; `--verbose` appends the description.
364
+
365
+ Regression (tan-cli#164): this used to print only the count line, never the
366
+ entries `--from-example` needs a source dir from.
367
+
368
+ `sdk_resolved` splits the three ways an EMPTY result happens, which used to
369
+ collapse into one line that was wrong for two of them (tan-cli#400):
370
+
371
+ * **No checkout at all.** A headline only -- the diagnosis is the
372
+ `examples.sdk-root-unresolved` warning `examples()` prints straight after,
373
+ so the reason lives in exactly one place and reads the same in both output
374
+ modes. The filter branch is skipped HERE and not merely reordered: with no
375
+ catalogue to filter, `--filter blinky` used to answer `no example projects
376
+ match --filter "blinky".` and send the reader after a filter that had
377
+ nothing to do with it.
378
+ * **A checkout that ships no `examples/` tree.** Named as the checkout's own
379
+ answer. Byte-identical to the case above before this change, and the old
380
+ "is the alp-sdk checkout resolvable?" wording actively misdirected: it is,
381
+ and it has nothing.
382
+ * **A filter that matched nothing** -- unchanged, and now only reachable
383
+ when there really was a catalogue to filter.
384
+ """
385
+ if not examples:
386
+ if not sdk_resolved:
387
+ return ["examples: no example projects found."]
388
+ if filter_ is not None:
389
+ # `ensure_ascii=False` so a non-ASCII filter is echoed as typed,
390
+ # which is the closest one-liner to Rust's `{:?}` quoting.
391
+ return [
392
+ f"examples: no example projects match --filter "
393
+ f"{json.dumps(filter_, ensure_ascii=False)}."
394
+ ]
395
+ if category is not None:
396
+ return [
397
+ f"examples: no example projects in category "
398
+ f"{json.dumps(category, ensure_ascii=False)}."
399
+ ]
400
+ return ["examples: the resolved alp-sdk checkout ships no example projects."]
401
+ id_width = max(len(e.id) for e in examples)
402
+ lines = [f"examples: {len(examples)} example project(s) available"]
403
+ for e in examples:
404
+ line = f" {strip_markdown_noise(e.id):<{id_width}} {strip_markdown_noise(e.title)}"
405
+ if verbose:
406
+ description = strip_markdown_noise(e.description)
407
+ if description:
408
+ line += f" -- {description}"
409
+ lines.append(line)
410
+ if filter_ is None and category is None:
411
+ categories = sorted({example_category(e) for e in examples})
412
+ lines.append("")
413
+ lines.append(f"categories: {' '.join(categories)}")
414
+ lines.append("narrow with --category <NAME>, or --filter <TEXT> to search.")
415
+ return lines
416
+
417
+
418
+ # ---------------------------------------------------------------------------
419
+ # Entry point
420
+ # ---------------------------------------------------------------------------
421
+
422
+
423
+ @dataclass(frozen=True)
424
+ class _ResolvedSdk:
425
+ """`_resolve_sdk`'s own return shape -- a named result, not a growing
426
+ tuple (the same reasoning `tan.core.sdk_discovery.SdkRootResolution` documents): a fact
427
+ one caller needs (`foreign_global_default_for`, tan-cli#464) must not
428
+ force every field before it to be re-counted by position.
429
+
430
+ `.path`/`.display` are `None` together, on the SAME two conditions
431
+ `SdkRootResolution.path` itself is `None` for: no `--sdk-root` and
432
+ nothing else resolved, or a rejected `--sdk-root` (tan-cli#900). Never a
433
+ bare `_resolve_sdk(...) is None` any more -- see that function's own
434
+ docstring for why."""
435
+
436
+ path: Path | None
437
+ display: str | None
438
+ tier: str
439
+ broken_project_pin: str | None = None
440
+ foreign_global_default_for: str | None = None
441
+
442
+
443
+ def _resolve_sdk(sdk_root: str | None, workspace_root: Path) -> _ResolvedSdk:
444
+ """The resolved checkout as a [`_ResolvedSdk`] -- ALWAYS one, never a bare
445
+ `None` (tan-cli#900; the same fix tan-cli#468 made to
446
+ `presets_cmd.resolve_sdk`, applied here to this command's own WIDE ladder
447
+ instead of copying its narrow one). Collapsing to `None` the moment
448
+ nothing resolved is exactly what discarded `.broken_project_pin`/
449
+ `.foreign_global_default_for` on that path: a workspace whose
450
+ `.alp/sdk-path` names a checkout that no longer exists, with nothing else
451
+ resolvable, reported `examples.sdk-root-unresolved` alone -- the customer
452
+ was told the SDK could not be resolved but not that their own project pin
453
+ was the broken thing. Callers now check `.path is None`, the same
454
+ "unresolved" signal `SdkRootResolution.path` itself already carries.
455
+
456
+ `--sdk-root` is TERMINAL (I-31): a value that is not a real checkout
457
+ resolves to `.path is None` rather than falling through to discovery, so a
458
+ typo'd flag yields an empty catalogue instead of silently listing some
459
+ other checkout's examples.
460
+
461
+ `.display` is separate from `.path` because they are not interchangeable:
462
+ `Path("./sdk")` stringifies to `sdk`, dropping the `./` the caller typed,
463
+ while Rust records `--sdk-root` as-is -- and `sdk.root` is on the wire.
464
+
465
+ `.broken_project_pin` (tan-cli#263 review) is `None` on the `--sdk-root`
466
+ branch (nothing to fall through from), else whatever
467
+ `resolve_sdk_root_wide` carried through -- `examples()` turns it into the
468
+ shared `sdk.project-pin-unresolved` warning, unconditionally, whether or
469
+ not `.path` resolved. `.foreign_global_default_for` (tan-cli#464) is the
470
+ same carry-through for the `globalDefault` tier's own silence;
471
+ `examples()` turns it into `sdk.global-default-foreign-project`.
472
+ """
473
+ if sdk_root:
474
+ path = Path(sdk_root)
475
+ if (path / "scripts" / "alp_project.py").is_file():
476
+ return _ResolvedSdk(path, sdk_root, "sdkRootFlag")
477
+ return _ResolvedSdk(None, None, "sdkRootFlag")
478
+ # `.alp/sdk-path` project pin > the machine-global default
479
+ # (`~/.alp/sdk-default`) > the WIDE positional walk
480
+ # (`resolve_sdk_root_wide`) -- previously this went straight to the
481
+ # positional walk, silently ignoring `tan init`'s pin. Wide, not the narrow
482
+ # ladder thirteen other commands take: the oracle's `examples` lists the
483
+ # child `<ws>/alp-sdk`'s catalogue over a competing `../alp-sdk`'s
484
+ # (tan-cli#263). No `ALP_SDK_ROOT` tier (tried and reverted -- see
485
+ # `resolve_sdk_root_ladder`'s own docstring).
486
+ resolution = resolve_sdk_root_wide(None, workspace_root)
487
+ found = resolution.path
488
+ return _ResolvedSdk(
489
+ found,
490
+ str(found).replace("\\", "/") if found is not None else None,
491
+ resolution.tier,
492
+ resolution.broken_project_pin,
493
+ resolution.foreign_global_default_for,
494
+ )
495
+
496
+
497
+ def examples(
498
+ filter_: str = typer.Option(
499
+ None, "--filter", metavar="TEXT", help="Only examples whose id or title contains TEXT."
500
+ ),
501
+ category: str = typer.Option(
502
+ None,
503
+ "--category",
504
+ metavar="NAME",
505
+ help="Only examples in this catalogue category (a bare `tan examples` "
506
+ "prints the list).",
507
+ ),
508
+ verbose: bool = typer.Option(False, "--verbose", help="Append each example's description."),
509
+ project: str = typer.Option(
510
+ None, "--project", metavar="PATH", help="Project root (defaults to '.')."
511
+ ),
512
+ sdk_root: str = typer.Option(None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."),
513
+ output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
514
+ ) -> None:
515
+ """List the SDK's ready-made example projects."""
516
+ json_mode = output_format == "json"
517
+
518
+ sdk: _ResolvedSdk | None = None
519
+ found: list[Example] = []
520
+ issues: list[Issue] = []
521
+ exit_code = ExitCode.SUCCESS
522
+ try:
523
+ # Unnormalised join, matching `cli_workspace_root` -- discovery probes
524
+ # this path's own parent, so collapsing a trailing `..` here would move
525
+ # which directory the sibling `alp-sdk` is looked for beside.
526
+ workspace_root = Path.cwd() / project if project else Path.cwd()
527
+ sdk = _resolve_sdk(sdk_root, workspace_root)
528
+ # tan-cli#900: computed unconditionally, BEFORE the `sdk.path is None`
529
+ # branch below -- `_resolve_sdk` always returns a `.broken_project_pin`/
530
+ # `.foreign_global_default_for` pair now, whether or not `.path`
531
+ # resolved, and a workspace pinned to a gone checkout with nothing
532
+ # else resolvable deserves the same disclosure a resolved run gets,
533
+ # not silence just because nothing happened to fall back to.
534
+ pin_issue = project_pin_issue(sdk.broken_project_pin, sdk.tier)
535
+ if pin_issue is not None:
536
+ issues.append(pin_issue)
537
+ # tan-cli#464: the catalogue this command just listed can come from
538
+ # a `globalDefault` pointer a DIFFERENT project's bootstrap
539
+ # relocation actually wrote -- the same silence `sdk.project-
540
+ # pin-unresolved` above closes one tier up.
541
+ foreign_issue = global_default_foreign_project_issue(sdk.foreign_global_default_for)
542
+ if foreign_issue is not None:
543
+ issues.append(foreign_issue)
544
+ if sdk.path is not None:
545
+ # tan-cli#484: an OPTIONAL enrichment pass, looked up by
546
+ # `sourceDir` -- `load_example_facets` never raises, so a
547
+ # checkout predating `metadata/catalog.json` (or one where it
548
+ # fails to parse) still lists every example exactly as before,
549
+ # just without the extra keys.
550
+ found = discover_examples(
551
+ sdk.path / "examples", load_example_facets(sdk.path)
552
+ )
553
+ # tan-cli#407: the catalogue this command just listed can come from
554
+ # a different checkout than `tan build` plans against, with both
555
+ # envelopes reporting `sourceTier: "discovery"`. `sdk_root` is the
556
+ # RAW flag, not `sdk.display`'s resolved text -- naming a root
557
+ # explicitly puts both ladders on the same terminal tier, and the
558
+ # helper returns `None` there rather than warning about a collision
559
+ # the user never reached.
560
+ divergence = sdk_ladder_divergence_issue(sdk_root, workspace_root, wide=True)
561
+ if divergence is not None:
562
+ issues.append(divergence)
563
+ else:
564
+ # tan-cli#400. NOT an error: `examples: []` from no checkout is a
565
+ # renderable answer the New Project flow is built on, so this stays
566
+ # `warning`/`ExitCode.SUCCESS` and the `examples-catalog` golden's
567
+ # `ok:true` at rc 0 is untouched. What changes is that the empty
568
+ # catalogue now carries a REASON. Its presence is also the JSON-side
569
+ # discriminator between the two empty answers -- a checkout that
570
+ # resolves and ships no `examples/` tree emits no issue at all,
571
+ # because that is not a misconfiguration to warn anybody about.
572
+ # tan-cli#497 defect 7: when `--sdk-root` WAS given and rejected,
573
+ # name the value. `SDK_UNRESOLVED_MESSAGE`'s remediation is "pass
574
+ # --sdk-root <path>" -- the flag they just passed -- and the failing
575
+ # path appeared nowhere in the envelope or the stderr text.
576
+ issues.append(
577
+ Issue(
578
+ SDK_UNRESOLVED_CODE,
579
+ "warning",
580
+ rejected_sdk_root_message(sdk_root, SDK_UNRESOLVED_CONSEQUENCE)
581
+ if sdk_root
582
+ else with_sdk_search(SDK_UNRESOLVED_MESSAGE, workspace_root),
583
+ )
584
+ )
585
+ if filter_ is not None:
586
+ found = [e for e in found if example_matches_filter(e, filter_)]
587
+ if category is not None:
588
+ found = [e for e in found if example_matches_category(e, category)]
589
+ except Exception as err: # noqa: BLE001 -- the backstop; see the module docstring
590
+ # No registry entry applies (`contract/issue-codes.json` covers
591
+ # `bootstrap.*`/`debug-config.*` only), so this follows the port's
592
+ # existing `<command>.internal-failure` spelling. Unreachable through
593
+ # any enumerated path -- every IO error above is already swallowed the
594
+ # way the oracle swallows it -- and here so that the one nobody thought
595
+ # of still arrives as an envelope instead of a traceback on stderr with
596
+ # an empty stdout the extension renders as nothing.
597
+ found = []
598
+ issues = [
599
+ Issue(
600
+ "examples.internal-failure",
601
+ "error",
602
+ f"examples failed unexpectedly: {err.__class__.__name__}: {err}",
603
+ )
604
+ ]
605
+ exit_code = ExitCode.INTERNAL_FAILURE
606
+
607
+ if json_mode:
608
+ emit(
609
+ Envelope(
610
+ "examples",
611
+ # Both null, always: the catalogue is an SDK-wide fact, so there
612
+ # is no project this answer belongs to. Pinned by
613
+ # `contract/envelopes/examples-catalog`.
614
+ Project(root=None, board_yaml=None),
615
+ {
616
+ "schemaVersion": DATA_SCHEMA_VERSION,
617
+ "examples": [e.as_dict() for e in found],
618
+ },
619
+ issues,
620
+ exit_code,
621
+ sdk=(
622
+ SdkInfo.from_resolution(sdk.display, sdk)
623
+ if sdk is not None and sdk.path is not None
624
+ else None
625
+ ),
626
+ )
627
+ )
628
+ else:
629
+ resolved = sdk is not None and sdk.path is not None
630
+ for line in render_examples_text(found, filter_, category, verbose, resolved):
631
+ print(line, file=sys.stderr)
632
+ for issue in issues:
633
+ print(f"examples: {issue.message}", file=sys.stderr)
634
+ raise typer.Exit(int(exit_code))
635
+
636
+
637
+ # tan-cli#261: adds the seven oracle `GlobalArgs` flags this command was
638
+ # still missing (`--all`/`--board-yaml`/`--ci`/`--no-color`/
639
+ # `--non-interactive`/`--quiet`/`--target`) on top of `--verbose`, already
640
+ # declared and read above; see `tan.core.global_flags`. Every one of them is
641
+ # genuinely inert for `examples` -- its envelope's `project` is always
642
+ # `Project(root=None, board_yaml=None)`, an SDK-wide catalogue with no
643
+ # project of its own to anchor a `--board-yaml`/`--target` on.
644
+ examples = accept_global_flags(examples)