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
tan/model/manifest.py ADDED
@@ -0,0 +1,625 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Manifest data model for .alpmodel packages (canonical Python form).
3
+
4
+ `Target.caveats` is ADDITIVE AND OPTIONAL ON THE WIRE, deliberately (see
5
+ `_target_dict`): it is emitted only when non-empty, so a package with no
6
+ caveated target is byte-identical to one this module produced before the
7
+ field existed -- including alp-sdk's three committed C-test fixtures
8
+ (`tests/fixtures/alpmodel/minimal.alpmodel`,
9
+ `tests/unit/alpmodel_reader/src/fixture.h`, `tests/yocto/onnx_cpu_fixture.h`,
10
+ written by `tan.model._gen_fixture`), which therefore did NOT have to be
11
+ regenerated for it.
12
+
13
+ WHY IT IS SAFE TO ADD WITHOUT MOVING `package.CONTAINER_VERSION` -- measured,
14
+ not assumed. alp-sdk's on-device reader (`src/common/alp_model.c`,
15
+ `alp_model_parse`) walks the manifest key-by-key and ends BOTH its top-level
16
+ map loop and its per-target map loop with `else { ok = zcbor_any_skip(zs,
17
+ NULL); }`, so a key it does not know is skipped, not mis-indexed and not
18
+ rejected. `zcbor_any_skip` recurses through a nested list/map on a LOCAL state
19
+ copy (`zcbor_decode.c`), drawing nothing from the `zcbor_state_t zs[8]` backup
20
+ budget that reader sizes for its own explicit nesting. Compiled natively
21
+ against real zcbor and fed a container carrying this exact per-target
22
+ `caveats` list, that reader returns ALP_OK with every existing field
23
+ byte-identical. Bumping the CONTAINER_VERSION instead would make the same
24
+ reader return ALP_ERR_VERSION (-11) for every fielded device -- a breaking
25
+ change, for a field no on-device reader needs to read.
26
+
27
+ Keep the wire shape FLAT (a list of text strings). The reader's skip path
28
+ recurses in C per nesting level; a caveat payload has no reason to nest, and a
29
+ deeply nested one would spend stack the on-device reader never budgeted for.
30
+ """
31
+ from __future__ import annotations
32
+ import json as _json
33
+ import cbor2 as _cbor2
34
+ from dataclasses import dataclass, field, asdict
35
+
36
+ MANIFEST_SCHEMA_VERSION = 1
37
+
38
+ _TENSOR_KEYS = {"dtype", "rank", "shape", "scale", "zp"}
39
+ _TARGET_KEYS = {"backend", "silicon_ref", "blob_format", "accel_config",
40
+ "arena", "requires", "blob", "compiler_version", "caveats"}
41
+ _COV_KEYS = {"backend", "accel_config", "status", "reason"}
42
+
43
+ # The blob formats the SDK can describe, ENFORCED at decode (tan-cli#1074):
44
+ # `from_dict` rejects a `targets[]` entry whose `blob_format` is not a member
45
+ # of this set, naming the offending value and the accepted set. Reconciled
46
+ # against every real producer on `dev` at the time this became enforced --
47
+ # `adapters/cpu.py` ("tflite"), `adapters/ethos_u.py` ("vela_tflite"),
48
+ # `adapters/drpai.py` ("drpai_dir"), `adapters/deepx.py` ("dxnn"),
49
+ # `adapters/executorch.py` ("executorch") -- plus `_gen_fixture.py`'s
50
+ # `_onnx_cpu_manifest` ("onnx", the issue #1254 regression fixture decoded by
51
+ # alp-sdk's real on-device reader, `tests/yocto/onnx_cpu_fixture.h`). The
52
+ # omission of "executorch" from this set is exactly why enforcement was
53
+ # deferred past the four other guards PR #1068 closed in the same module: the
54
+ # set itself had already drifted out of sync with a live, default-registered
55
+ # adapter, and an unenforced constant cannot be wrong loudly -- it silently
56
+ # stopped meaning what its own comment claimed.
57
+ #
58
+ # The authoritative ACCEPTOR of these six strings is alp-sdk's own
59
+ # `src/backends/inference/alp_model_select.c` `_fmt_enum()` -- this set is
60
+ # duplicated truth across two repos with no parity gate between them. If
61
+ # alp-sdk's reader ever adds or drops a case, this set has to move to match
62
+ # it (not the other way around): `_fmt_enum()` runs on real hardware, this
63
+ # set only decides what `tan` will hand it.
64
+ VALID_BLOB_FORMATS = frozenset({"vela_tflite", "tflite", "drpai_dir", "dxnn", "onnx", "executorch"})
65
+
66
+
67
+ def _check_type(v: object, expected: type | tuple[type, ...], type_desc: str,
68
+ label: str, context: str) -> None:
69
+ """The single type gate every guard in this module routes through --
70
+ shared by `_required_field` (a mapping key that must be there),
71
+ `_optional_field` (one that may not be) and `_check_element_types` (a
72
+ list element), so the curated message shape and the `bool` rule below are
73
+ written once rather than per call site.
74
+
75
+ `bool` is rejected whenever `expected` names `int` but not `bool` itself
76
+ (tan-cli#1058 review round 3): Python's `bool` is an `int` subclass, so
77
+ `isinstance(True, int)` is `True` and a plain `isinstance` check would
78
+ otherwise let `arena=True` / `requires["sram_kib"]=True` / `shape=[True]`
79
+ decode silently -- re-emitted on the CBOR wire as byte `0xf5` (major type
80
+ 7, "true"), not an unsigned int -- straight into
81
+ `src/backends/inference/alp_model_select.c`'s fit gate as
82
+ `t->req_sram_kib`. A mechanism check here, not a per-field one, so every
83
+ current and future `(int, ...)` entry in `_TENSOR_TYPES`/`_TARGET_TYPES`/
84
+ `_REQUIRES_TYPES`/`_COVERAGE_TYPES`/`_TENSOR_ELEMENT_TYPES`/
85
+ `_TARGET_ELEMENT_TYPES` inherits it automatically. The one comparison in
86
+ this module that does NOT route through here is `from_dict`'s
87
+ schema-version gate, which therefore restates the rule in its own terms
88
+ (tan-cli#1064)."""
89
+ expected_types = expected if isinstance(expected, tuple) else (expected,)
90
+ if not isinstance(v, expected) or (isinstance(v, bool) and bool not in expected_types):
91
+ where = f" in {context}" if context else ""
92
+ raise ValueError(
93
+ f"malformed .alpmodel manifest: {label} must be {type_desc}, got {type(v).__name__}{where}"
94
+ )
95
+
96
+
97
+ def _required_field(d: dict, key: str, expected: type | tuple[type, ...], type_desc: str,
98
+ *, context: str = "") -> object:
99
+ """Look up a required field in a decoded `.alpmodel` manifest document (a
100
+ JSON object or CBOR map that has already passed the document-level
101
+ `isinstance(d, dict)` guard in `from_json`/`from_cbor`), raising a
102
+ curated `ValueError` naming the field instead of letting the bare
103
+ subscript this replaces escape as a raw `KeyError` (field absent) or,
104
+ worse, being handed to a coercing constructor that manufactures a wrong
105
+ value from the wrong type (`from_cbor`'s old `bytes(d["src_sha"])`
106
+ zero-filled/byte-wise-coerced an int/list/bool `src_sha` into a bogus
107
+ hash with no diagnostic at all -- tan-cli#1023 review round 2, the
108
+ document-level guard stopped one field short of `name`/`src_sha`
109
+ themselves).
110
+
111
+ `context`, when given, is appended to both messages as "in <context>" --
112
+ `_decode_list_field` below passes e.g. `"targets[2]"` so a malformed
113
+ nested element's error names WHICH element, the same way the top-level
114
+ calls (no `context`) name the document itself. Genuinely the same check
115
+ both shapes need: `d` here is a decoded sub-document (a `targets[]`
116
+ element) exactly as much as it is the top-level manifest.
117
+
118
+ `.alpmodel` packages are machine-generated CBOR/JSON, not hand-authored
119
+ documents like `board.yaml` -- but `package.py`'s own bounds-checked
120
+ header reads already treat their bytes as wire data, not trusted
121
+ first-party output, and a corrupted `src_sha` (the model-source identity
122
+ field written by `build.py:222`) is exactly the class of corruption this
123
+ container's own bounds checks exist to catch. Fail loudly here, the same
124
+ way, rather than degrade silently.
125
+
126
+ The type half of the check lives in `_check_type` above, shared with
127
+ `_optional_field` and `_check_element_types`."""
128
+ if key not in d:
129
+ where = f" in {context}" if context else ""
130
+ raise ValueError(f"malformed .alpmodel manifest: missing required field {key!r}{where}")
131
+ _check_type(d[key], expected, type_desc, f"field {key!r}", context)
132
+ return d[key]
133
+
134
+
135
+ def _optional_field(d: dict, key: str, expected: type | tuple[type, ...], type_desc: str,
136
+ *, context: str = "") -> object | None:
137
+ """`_required_field`'s sibling for a field that is OPTIONAL on the wire:
138
+ ABSENT is fine (the dataclass default stands), PRESENT-but-wrong-typed
139
+ raises exactly the curated `ValueError` a required field would
140
+ (tan-cli#1056).
141
+
142
+ A separate helper rather than a flag on `_required_field`, because the
143
+ two differ ONLY in the missing-key branch and overloading that branch
144
+ would make each call site's intent depend on an argument instead of on
145
+ which function it names.
146
+
147
+ Returns `None` for an absent field. That is unambiguous rather than
148
+ merely convenient: no `expected` in this module admits `NoneType`, so a
149
+ field that is PRESENT and `None` raises above and never reaches the
150
+ return -- the caller can read `None` as "absent" without a sentinel."""
151
+ if key not in d:
152
+ return None
153
+ _check_type(d[key], expected, type_desc, f"field {key!r}", context)
154
+ return d[key]
155
+
156
+
157
+ def _pick(d: dict, keys: set) -> dict:
158
+ return {k: d[k] for k in keys if k in d} # drop unknown keys + tolerate missing-known
159
+
160
+
161
+ def _check_nested_types(value: dict, inner_types: dict[str, tuple[type | tuple[type, ...], str]],
162
+ context: str) -> None:
163
+ """One level of `_required_field` under a field `_decode_list_field` has
164
+ already confirmed is a `dict` (`value`) -- see `nested_types` on that
165
+ function and `_REQUIRES_TYPES` below for the why. Only keys present in
166
+ `inner_types` AND in `value` are checked; a missing inner key is left
167
+ alone, matching `_pick`'s tolerate-missing-known convention elsewhere in
168
+ this module."""
169
+ for inner_key, (inner_expected, inner_desc) in inner_types.items():
170
+ if inner_key in value:
171
+ _required_field(value, inner_key, inner_expected, inner_desc, context=context)
172
+
173
+
174
+ def _check_element_types(value: list, expected: type | tuple[type, ...], type_desc: str,
175
+ context: str) -> None:
176
+ """`_check_nested_types`'s list-shaped sibling: one level of `_check_type`
177
+ under a field `_check_element_fields` has already confirmed is a `list`
178
+ (tan-cli#1063). The dict helper above takes a per-KEY map; a list has no
179
+ keys, so this one takes a single ELEMENT type and names the offending
180
+ index where the dict side names the key -- `element 1 must be an int, got
181
+ str in inputs[0].shape` is the exact counterpart of `field 'sram_kib'
182
+ must be an int, got str in targets[0].requires`.
183
+
184
+ Every element is checked, not just the first: a `shape` whose second
185
+ entry is the corrupted one is as wrong as one whose first is."""
186
+ for i, elem in enumerate(value):
187
+ _check_type(elem, expected, type_desc, f"element {i}", context)
188
+
189
+
190
+ def _check_schema_version(d: dict) -> None:
191
+ """Both `from_json` and `from_cbor` route through `from_dict` (tan-cli#1039)
192
+ -- this check is therefore written ONCE and enforced on both reader
193
+ paths, rather than living only in the JSON-era code `from_dict` used to
194
+ be (`from_cbor` built a `Manifest` directly and never called `from_dict`
195
+ at all, so a future manifest version read by an older `tan` over CBOR --
196
+ the PRODUCTION reader path for `.alpmodel` packages -- was silently
197
+ parsed as v1 instead of refused).
198
+
199
+ `type(v) is not int`, NOT `isinstance` (tan-cli#1064): `bool` is an
200
+ `int` subclass and `True == 1`, so the bare comparison accepted `v=True`
201
+ as v1 -- and float `1.0` too. This is the one comparison in this module
202
+ that reads a decoded value without routing through
203
+ `_required_field`/`_check_type`, so it restates their `bool` rule in its
204
+ own terms rather than inheriting it. A future manifest version written
205
+ as `1.0` or `True` is a manifest this reader does not understand, and
206
+ must be refused as loudly as `v=2` already was."""
207
+ v = d.get("v", MANIFEST_SCHEMA_VERSION)
208
+ if type(v) is not int or v != MANIFEST_SCHEMA_VERSION:
209
+ raise ValueError(f"unsupported manifest version {v!r}; expected {MANIFEST_SCHEMA_VERSION}")
210
+
211
+
212
+ def _check_blob_formats(targets: list["Target"]) -> None:
213
+ """`_TARGET_TYPES["blob_format"]` (used by `_check_element_fields` while
214
+ decoding `targets`) only confirms each target's `blob_format` is a `str`
215
+ -- it says nothing about WHICH string. `VALID_BLOB_FORMATS` was a real
216
+ constant instead of a comment for exactly the reason this function now
217
+ exists: an unlisted `blob_format` is now rejected ON DECODE (tan-cli#1074).
218
+
219
+ This is a READ-side guard only, not a write-side one -- `build.py`'s
220
+ `Blob(format=...)` construction never consults `VALID_BLOB_FORMATS` at
221
+ all, so a new adapter can still WRITE an unlisted format; it can no
222
+ longer do so silently, because the package this function's caller
223
+ guards is the same one every reader -- `tan model check`, flash, deploy
224
+ -- decodes. `python/tests/gates/test_blob_format_producers_stay_in_valid_blob_formats.py`
225
+ is the write-side half: it walks the adapter/fixture SOURCE for every
226
+ literal `blob_format` a producer emits and fails if one is missing from
227
+ this set, which is what actually keeps a new backend from shipping
228
+ silently -- this function alone only refuses to read the result back.
229
+
230
+ Called from `from_dict` once every target is already a `Target`
231
+ instance, rather than threaded through `_check_element_fields` as a
232
+ fourth kind of per-field check (type / nested / element) -- membership
233
+ against a closed value set is the only check in this module that is not
234
+ a shape check, and a purpose-built function reads more plainly than a new
235
+ parameter every OTHER `_decode_list_field` caller would have to pass
236
+ `None` for."""
237
+ for i, t in enumerate(targets):
238
+ if t.blob_format not in VALID_BLOB_FORMATS:
239
+ raise ValueError(
240
+ f"malformed .alpmodel manifest: field 'blob_format' must be "
241
+ f"one of {sorted(VALID_BLOB_FORMATS)}, got {t.blob_format!r} "
242
+ f"in targets[{i}]"
243
+ )
244
+
245
+
246
+ def _check_element_fields(elem: dict, item_keys: set, required: frozenset,
247
+ field_types: dict[str, tuple[type | tuple[type, ...], str]],
248
+ nested_types: dict | None, element_types: dict | None,
249
+ context: str) -> None:
250
+ """Guard ONE decoded element of a nested list field.
251
+
252
+ `required` fields first (alphabetically, so a message names the same
253
+ field it always has), then the OPTIONAL remainder of `item_keys` --
254
+ `Target.compiler_version`/`caveats` are carried through to the
255
+ constructor by `_pick` but sit in no `required` set, so before
256
+ tan-cli#1056 nothing type-checked them at all and
257
+ `{"compiler_version": 12345, "caveats": "not-a-list"}` constructed
258
+ silently.
259
+
260
+ `field_types` gives each field a REAL `expected` type, not the
261
+ accepts-anything `object` an earlier version passed (tan-cli#1049).
262
+ `nested_types` routes an already-checked-`dict` value through
263
+ `_check_nested_types` (see `_REQUIRES_TYPES`); `element_types` routes an
264
+ already-checked-`list` value through `_check_element_types` (see
265
+ `_TENSOR_ELEMENT_TYPES`, tan-cli#1063)."""
266
+ for field_name in sorted(required) + sorted(item_keys - required):
267
+ expected, type_desc = field_types[field_name]
268
+ if field_name in required:
269
+ value = _required_field(elem, field_name, expected, type_desc, context=context)
270
+ else:
271
+ value = _optional_field(elem, field_name, expected, type_desc, context=context)
272
+ if value is None:
273
+ continue
274
+ inner_types = nested_types.get(field_name) if nested_types else None
275
+ if inner_types:
276
+ _check_nested_types(value, inner_types, context=f"{context}.{field_name}")
277
+ elem_type = element_types.get(field_name) if element_types else None
278
+ if elem_type:
279
+ _check_element_types(value, elem_type[0], elem_type[1],
280
+ context=f"{context}.{field_name}")
281
+
282
+
283
+ def _decode_list_field(d: dict, key: str, item_keys: set, required: frozenset, ctor,
284
+ field_types: dict[str, tuple[type | tuple[type, ...], str]],
285
+ nested_types: dict[str, dict[str, tuple[type | tuple[type, ...], str]]]
286
+ | None = None,
287
+ element_types: dict[str, tuple[type | tuple[type, ...], str]]
288
+ | None = None) -> list:
289
+ """Decode a nested `.alpmodel` manifest list field (`inputs`/`outputs`/
290
+ `targets`/`coverage`) into `ctor` instances, guarding both the field and
291
+ each element the way `_required_field` guards a top-level one
292
+ (tan-cli#1040). Before this, every reader built these lists straight out
293
+ of `ctor(**t)` (`from_dict`) or `ctor(**_pick(t, item_keys))` (the old
294
+ `from_cbor`) with no element-type or completeness check: a non-mapping
295
+ element degrades `_pick` to `{}` rather than raising (`k in 'abc'` is a
296
+ legal, false, substring check), so a raw `TypeError` out of the dataclass
297
+ constructor was the first thing to notice, not a curated `ValueError` --
298
+ `inputs=['abc']` -> `Tensor.__init__() missing 5 required positional
299
+ arguments`; an incomplete `targets[]` entry names 7. `from_dict`'s own
300
+ bare `Tensor(**t)` (no `_pick`) failed even earlier on the `**` unpack
301
+ itself. One helper now closes both readers, since `from_cbor` routes
302
+ through `from_dict`. The per-field work moved to `_check_element_fields`
303
+ above -- see it for what `field_types`/`nested_types`/`element_types`
304
+ each do.
305
+
306
+ The field-level check below (is `d[key]` a list at all) is the same
307
+ shape one step up: a `targets` value that decodes but isn't a list would
308
+ otherwise reach `enumerate()` and, for a string, iterate its CHARACTERS
309
+ as "elements" -- silently wrong, not a crash."""
310
+ elems = d.get(key, [])
311
+ if not isinstance(elems, list):
312
+ raise ValueError(
313
+ f"malformed .alpmodel manifest: field {key!r} must be a list, got {type(elems).__name__}"
314
+ )
315
+ out = []
316
+ for i, elem in enumerate(elems):
317
+ if not isinstance(elem, dict):
318
+ raise ValueError(
319
+ f"malformed .alpmodel manifest: {key}[{i}] must be a mapping, "
320
+ f"got {type(elem).__name__}"
321
+ )
322
+ _check_element_fields(elem, item_keys, required, field_types,
323
+ nested_types, element_types, context=f"{key}[{i}]")
324
+ out.append(ctor(**_pick(elem, item_keys)))
325
+ return out
326
+
327
+
328
+ _TARGET_REQUIRED = frozenset(_TARGET_KEYS - {"compiler_version", "caveats"})
329
+
330
+ # Per-field `(expected type, type_desc)` for `_decode_list_field`'s element
331
+ # guard (tan-cli#1049) -- one entry per member of the `required` frozenset
332
+ # each caller below passes, so every required field of `Tensor`/`Target`/
333
+ # `Coverage` gets a real type check, not the `object`-accepts-anything one
334
+ # `_required_field`'s presence-only call used before -- `arena` and
335
+ # `requires["sram_kib"]` are exactly the figures
336
+ # `src/backends/inference/alp_model_select.c`'s fit gate reads, so a
337
+ # wrong-typed value there was the silent-wrong-value class, not a crash.
338
+ # `scale` accepts `(int, float)`: a whole-number scale is legal JSON/CBOR
339
+ # content (`1` is as valid as `1.0`) and the dataclass field is a plain
340
+ # `float` annotation, not an enforced constraint -- rejecting `int` here
341
+ # would be a new, undocumented restriction on well-formed input, not a
342
+ # defect fix.
343
+ _TENSOR_TYPES = {
344
+ "dtype": (str, "a string"),
345
+ "rank": (int, "an int"),
346
+ "shape": (list, "a list"),
347
+ "scale": ((int, float), "a number"),
348
+ "zp": (int, "an int"),
349
+ }
350
+ _TARGET_TYPES = {
351
+ "backend": (str, "a string"),
352
+ "silicon_ref": (str, "a string"),
353
+ "blob_format": (str, "a string"),
354
+ "accel_config": (str, "a string"),
355
+ "arena": (int, "an int"),
356
+ "requires": (dict, "a mapping"),
357
+ "blob": (int, "an int"),
358
+ # OPTIONAL on the wire (`_TARGET_REQUIRED` subtracts both), so these two
359
+ # reach `_check_element_fields` through `_optional_field`, not
360
+ # `_required_field`: absent is legal, present-but-wrong-typed is not.
361
+ # Before tan-cli#1056 they were in `_TARGET_KEYS` (so `_pick` handed them
362
+ # to the constructor) but in no type map at all, so
363
+ # `compiler_version=12345` / `caveats="not-a-list"` decoded silently.
364
+ "compiler_version": (str, "a string"),
365
+ "caveats": (list, "a list"),
366
+ }
367
+
368
+ # One level deeper than `_TARGET_TYPES["requires"]`, which only guards the
369
+ # container is a mapping at all (tan-cli#1049 review round 2). `sram_kib` is
370
+ # the other half of the same fit-gate figure `arena` is -- when present it
371
+ # must be an `int`, the same way `arena` itself already is. `op_features` is
372
+ # not checked here: nothing downstream reads it as anything but an opaque
373
+ # list yet, so adding a shape requirement for it now would be a new,
374
+ # undocumented restriction rather than closing a known gap.
375
+ _REQUIRES_TYPES = {
376
+ "sram_kib": (int, "an int"),
377
+ }
378
+ # Element types for a field whose own declared type is `list` -- the entries
379
+ # above guard the CONTAINER only, which is the same shape tan-cli#1049 closed
380
+ # for `Target.requires` one level down (tan-cli#1063). `shape` feeds
381
+ # tensor-shape maths downstream the way `requires["sram_kib"]` feeds
382
+ # `src/backends/inference/alp_model_select.c`'s fit gate, so
383
+ # `shape=["a","b"]` / `[None,None]` / `[{"a":1}]` / `[True,False]` were all
384
+ # silent-wrong values, not crashes. `caveats` is `list[str]` that `tan model
385
+ # build` and `tan model check --exact` render as customer-readable text, so a
386
+ # non-string element degrades a diagnostic surface silently (tan-cli#1056).
387
+ #
388
+ # `op_features` inside `requires` deliberately gets no entry here, for the
389
+ # same reason `_REQUIRES_TYPES` omits it: nothing downstream reads it as
390
+ # anything but an opaque list yet, so a shape requirement would be a new,
391
+ # undocumented restriction rather than a closed gap.
392
+ _TENSOR_ELEMENT_TYPES = {
393
+ "shape": (int, "an int"),
394
+ }
395
+ _TARGET_ELEMENT_TYPES = {
396
+ "caveats": (str, "a string"),
397
+ }
398
+ _COVERAGE_TYPES = {
399
+ "backend": (str, "a string"),
400
+ "accel_config": (str, "a string"),
401
+ "status": (str, "a string"),
402
+ "reason": (str, "a string"),
403
+ }
404
+
405
+
406
+ def _json_default(d: dict) -> dict:
407
+ out = dict(d)
408
+ out["src_sha"] = d["src_sha"].hex() # bytes -> hex string for JSON
409
+ return out
410
+
411
+
412
+ def _target_dict(t: "Target") -> dict:
413
+ """One target's wire form: `asdict`, minus an EMPTY `caveats`.
414
+
415
+ Dropping the empty case is what keeps this field additive in the strict
416
+ sense -- a package whose targets carry no caveat encodes to exactly the
417
+ bytes it did before `caveats` existed, so no committed fixture, no shipped
418
+ package and no on-device footprint moves for a field that has nothing to
419
+ say. Absent means "no caveats"; every reader must treat it that way (the
420
+ `_pick`-based `_decode_list_field` both `from_json` and `from_cbor` route
421
+ through already does, and so does `Target.caveats`'s own default)."""
422
+ d = asdict(t)
423
+ if not d["caveats"]:
424
+ del d["caveats"]
425
+ return d
426
+
427
+
428
+ @dataclass
429
+ class Tensor:
430
+ dtype: str
431
+ rank: int
432
+ shape: list[int]
433
+ scale: float
434
+ zp: int
435
+
436
+
437
+ @dataclass
438
+ class Target:
439
+ backend: str # cpu | ethos_u | drpai | deepx_dxm1
440
+ silicon_ref: str # e.g. "alif:ensemble:e8" or "*"
441
+ blob_format: str # one of VALID_BLOB_FORMATS; enforced by Manifest.from_dict
442
+ accel_config: str # "" when N/A
443
+ arena: int
444
+ requires: dict[str, object] # {"sram_kib": int, "op_features": list[str]}
445
+ blob: int # index into the package blob table
446
+ compiler_version: str = "" # e.g. "vela 4.1.0" | "passthrough"; "" when unknown
447
+ # The compiler's OWN unresolved caveats about the blob shipped for THIS
448
+ # target -- each already a complete, customer-readable sentence, carried
449
+ # verbatim from `adapters.Blob.caveats`. Today that is vela's "Compilation
450
+ # may be invalid or non-optimal" verdict when it fell back to its BUILT-IN
451
+ # default profile, whose memory model may not be the module's.
452
+ #
453
+ # This field exists because `arena` and `requires["sram_kib"]` beside it
454
+ # are exactly the figures alp-sdk's on-device selector consumes
455
+ # (`return e->arena_sram_kib == 0u || t->req_sram_kib <= e->arena_sram_kib;`,
456
+ # src/backends/inference/alp_model_select.c) -- so a blob compiled against
457
+ # a memory model the module does not have must not be able to reach a
458
+ # board with the package silent about it. `tan model check --exact` was
459
+ # already surfacing these; `tan model build` dropped them on the floor,
460
+ # which is the path that actually ships bytes to hardware.
461
+ #
462
+ # It is DIAGNOSTIC, never a substitute for refusing: a figure that cannot
463
+ # be derived is refused at the adapter
464
+ # (`ethos_u._refuse_zero_sram_footprint`), not caveated and shipped.
465
+ # Empty is omitted from the wire form entirely -- see `_target_dict`.
466
+ caveats: list[str] = field(default_factory=list)
467
+
468
+
469
+ @dataclass
470
+ class Coverage:
471
+ """A backend the package did NOT include a blob for, and why."""
472
+
473
+ backend: str
474
+ accel_config: str
475
+ status: str # compiled | skipped | incompatible
476
+ reason: str
477
+
478
+
479
+ @dataclass
480
+ class Manifest:
481
+ name: str
482
+ src_sha: bytes
483
+ inputs: list[Tensor] = field(default_factory=list)
484
+ outputs: list[Tensor] = field(default_factory=list)
485
+ targets: list[Target] = field(default_factory=list)
486
+ coverage: list[Coverage] = field(default_factory=list)
487
+
488
+ def to_dict(self) -> dict:
489
+ return {
490
+ "v": MANIFEST_SCHEMA_VERSION,
491
+ "name": self.name,
492
+ "src_sha": self.src_sha,
493
+ "inputs": [asdict(t) for t in self.inputs],
494
+ "outputs": [asdict(t) for t in self.outputs],
495
+ "targets": [_target_dict(t) for t in self.targets],
496
+ "coverage": [asdict(c) for c in self.coverage],
497
+ }
498
+
499
+ @classmethod
500
+ def from_dict(cls, d: dict) -> "Manifest":
501
+ _check_schema_version(d)
502
+ # `name`/`src_sha` were still bare subscripts here even after
503
+ # `from_json`/`from_cbor`'s own document-level + field-level guards
504
+ # (tan-cli#1074): a `from_dict` mapping missing either key raised a
505
+ # raw `KeyError` naming only the key, not the curated `ValueError`
506
+ # every other field in this module raises -- no context, no "malformed
507
+ # .alpmodel manifest" framing, nothing a caller could route on
508
+ # alongside the rest of this module's error contract. `from_dict` is
509
+ # the shared POST-NORMALISATION entry point both `from_json` (hex
510
+ # str -> bytes) and `from_cbor` (bytearray -> bytes) route through
511
+ # after decoding their own wire form of `src_sha` -- its own contract
512
+ # is therefore `bytes`, matching what both callers, and the direct
513
+ # round-trip caller in `tests/model/test_manifest.py`, already hand
514
+ # it.
515
+ # Computed as locals, in the SAME left-to-right order the original
516
+ # `cls(...)` keyword-argument list used (name, src_sha, inputs,
517
+ # outputs, targets, coverage) -- `targets` needs to exist as a local
518
+ # before `_check_blob_formats` can run on it, but that must not
519
+ # change WHICH error a malformed document reports first: a manifest
520
+ # missing `name` AND carrying a bad target still reports the
521
+ # missing `name`, the document's own required field, ahead of a
522
+ # nested element's (PR #1098 review nit).
523
+ name = _required_field(d, "name", str, "a string")
524
+ src_sha = _required_field(d, "src_sha", bytes, "a bytes object")
525
+ inputs = _decode_list_field(d, "inputs", _TENSOR_KEYS, _TENSOR_KEYS, Tensor, _TENSOR_TYPES,
526
+ element_types=_TENSOR_ELEMENT_TYPES)
527
+ outputs = _decode_list_field(d, "outputs", _TENSOR_KEYS, _TENSOR_KEYS, Tensor, _TENSOR_TYPES,
528
+ element_types=_TENSOR_ELEMENT_TYPES)
529
+ targets = _decode_list_field(d, "targets", _TARGET_KEYS, _TARGET_REQUIRED, Target, _TARGET_TYPES,
530
+ nested_types={"requires": _REQUIRES_TYPES},
531
+ element_types=_TARGET_ELEMENT_TYPES)
532
+ _check_blob_formats(targets) # tan-cli#1074: reject an unlisted blob_format
533
+ coverage = _decode_list_field(d, "coverage", _COV_KEYS, _COV_KEYS, Coverage, _COVERAGE_TYPES)
534
+ return cls(name=name, src_sha=src_sha, inputs=inputs, outputs=outputs,
535
+ targets=targets, coverage=coverage)
536
+
537
+ def to_json(self) -> str:
538
+ return _json.dumps(_json_default(self.to_dict()), indent=2)
539
+
540
+ @classmethod
541
+ def from_json(cls, text: str) -> "Manifest":
542
+ d = _json.loads(text)
543
+ if not isinstance(d, dict):
544
+ # Mirrors the `preset` guard in `targets.resolve_targets`
545
+ # (tan-cli#1018): a `.alpmodel` manifest that decodes but is
546
+ # not a JSON object (e.g. a bare list or a bare scalar) must
547
+ # not reach the bare `d["src_sha"]` subscript below -- that
548
+ # raises a raw TypeError ("list indices must be integers or
549
+ # slices, not str" / "string indices must be integers, not
550
+ # 'str'") instead of a curated error a caller can distinguish
551
+ # from any other TypeError in the call stack (tan-cli#1023).
552
+ # `package.py`'s own bounds-checked reads of the untrusted
553
+ # header already treat manifest bytes as wire data, not
554
+ # trusted first-party output -- a corrupt/adversarial manifest
555
+ # fails loudly here for the same reason, rather than being
556
+ # silently tolerated.
557
+ raise ValueError(
558
+ f"malformed .alpmodel manifest: expected a JSON object, got {type(d).__name__}"
559
+ )
560
+ # `d["name"]`/`d["src_sha"]` were still bare here even after the
561
+ # document-level guard above -- a manifest missing either raised a
562
+ # raw KeyError, and a non-string `src_sha` raised a raw
563
+ # `TypeError: fromhex() argument must be str, not int` out of
564
+ # `bytes.fromhex` below, neither distinguishable from an unrelated
565
+ # bug in the call stack (tan-cli#1023 review round 2). `_required_field`
566
+ # closes both the same way the document guard above does.
567
+ _required_field(d, "name", str, "a string")
568
+ src_sha_hex = _required_field(d, "src_sha", str, "a hex string")
569
+ try:
570
+ d["src_sha"] = bytes.fromhex(src_sha_hex)
571
+ except ValueError as exc:
572
+ raise ValueError(f"malformed .alpmodel manifest: field 'src_sha' is not valid hex: {exc}") from exc
573
+ return cls.from_dict(d)
574
+
575
+ def to_cbor(self) -> bytes:
576
+ return _cbor2.dumps(self.to_dict())
577
+
578
+ # tan-cli#1055: cbor2's decode exceptions (`CBORDecodeEOF` and its
579
+ # siblings, common base `CBORDecodeError`) are NOT `ValueError`
580
+ # subclasses, while `from_json`'s `json.JSONDecodeError` IS -- so a caller
581
+ # catching `ValueError` around a `.alpmodel` read, the contract every other
582
+ # guard in this module exists to honour, caught a malformed JSON manifest
583
+ # and missed a malformed CBOR one, on the PRODUCTION reader path.
584
+ # `package.read_package`/`read_manifest_file` bounds-check the manifest
585
+ # region's offset and length (tan-cli#1045) but never that its bytes are
586
+ # well-formed CBOR: a bit flip, a truncated write or plain non-CBOR bytes
587
+ # land in the `except` below.
588
+ @classmethod
589
+ def from_cbor(cls, blob: bytes) -> "Manifest":
590
+ try:
591
+ d = _cbor2.loads(blob)
592
+ except _cbor2.CBORDecodeError as exc:
593
+ raise ValueError(
594
+ f"malformed .alpmodel manifest: not valid CBOR "
595
+ f"({type(exc).__name__}: {exc})"
596
+ ) from exc
597
+ if not isinstance(d, dict):
598
+ # See `from_json` above (tan-cli#1023): guard before the bare
599
+ # `d["name"]` subscript below, reached from both
600
+ # `package.read_manifest_file` and `package.read_package`.
601
+ raise ValueError(
602
+ f"malformed .alpmodel manifest: expected a CBOR map, got {type(d).__name__}"
603
+ )
604
+ # Same gap as `from_json` above, plus a worse failure mode: CBOR
605
+ # preserves `src_sha`'s wire type (unlike JSON's hex string), so an
606
+ # unguarded `bytes(d["src_sha"])` didn't raise at all for a
607
+ # wrong-typed `src_sha` -- `bytes(int)` is a zero-fill constructor
608
+ # and `bytes(list[int])` a byte-wise one, so a manifest whose
609
+ # `src_sha` CBOR major type got flipped from byte-string to
610
+ # unsigned-int/array/bool silently produced a `Manifest` carrying an
611
+ # INVENTED source hash (tan-cli#1023 review round 2, finding 1).
612
+ # `_required_field` requires the real wire type before `bytes()`
613
+ # ever runs on it.
614
+ _required_field(d, "name", str, "a string")
615
+ src_sha_raw = _required_field(d, "src_sha", (bytes, bytearray), "a byte string")
616
+ # Normalise in place (bytearray -> bytes, mirroring `from_json`'s
617
+ # hex-string -> bytes normalisation two methods up) and hand off to
618
+ # `from_dict` -- which is what actually enforces `MANIFEST_SCHEMA_VERSION`
619
+ # and guards the nested `inputs`/`outputs`/`targets`/`coverage`
620
+ # decodes (tan-cli#1039, tan-cli#1040). Before this, `from_cbor` built
621
+ # the `Manifest` directly and never reached either check: CBOR is the
622
+ # PRODUCTION reader path for `.alpmodel` packages, so the stricter
623
+ # checks used to sit on the path that matters less.
624
+ d["src_sha"] = bytes(src_sha_raw)
625
+ return cls.from_dict(d)