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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1319 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan explain` -- describe a project template, a module template, or a
|
|
3
|
+
generation target. tan's teaching surface: the only command whose OUTPUT is the
|
|
4
|
+
product, so its wording is contract in the same way an envelope key is.
|
|
5
|
+
|
|
6
|
+
Mirrors `crates/tan-cli/src/commands/explain.rs` plus the two `tan-core`
|
|
7
|
+
registries it reads (`wizard/service/registry.rs`'s template definitions,
|
|
8
|
+
`loader.rs`'s `GENERATION_TARGET_CATALOG`). `--template` explains an
|
|
9
|
+
init/scaffold template, `--target` a generation output target, neither prints an
|
|
10
|
+
overview, and BOTH is an error -- exit 1, like an unknown id.
|
|
11
|
+
|
|
12
|
+
**Why the catalogue is vendored here and not read from the SDK.** Weighed
|
|
13
|
+
against `alp-sdk/docs/superpowers/specs/2026-07-29-tan-port-invariants.md`:
|
|
14
|
+
|
|
15
|
+
* **I-32** is the governing one, and it is not close. `tan explain --template`
|
|
16
|
+
describes what `tan init` will write, and `tan init` is SDK-FREE by design --
|
|
17
|
+
it reads the vendored `--emit scaffold` tree "without ever shelling the SDK"
|
|
18
|
+
so a fresh customer's first command works with no alp-sdk checkout anywhere.
|
|
19
|
+
A description sourced from a checkout would go silent (or lie) on exactly the
|
|
20
|
+
machine `tan init` is guaranteed to work on. Anti-pattern #22 in that doc names
|
|
21
|
+
shelling the SDK for this content specifically.
|
|
22
|
+
* **I-26 / ADR-0017** (`metadata/**` stays in alp-sdk; tan learns no hardware
|
|
23
|
+
fact) is what would otherwise pull the other way, as it does for
|
|
24
|
+
`tan examples`. It does not apply: this prose is tan's own description of
|
|
25
|
+
tan's own commands and tan's own emit outputs -- there is no `metadata/`
|
|
26
|
+
source for "Minimal template keeps generated code intentionally small". The
|
|
27
|
+
part-numbers inside the prose (TMP112, BME280, CC3501E, the two SKUs) enter no
|
|
28
|
+
decision here; they are transcribed English, and every one already ships in
|
|
29
|
+
tan via the vendored scaffold trees and `core/scaffold.py`'s
|
|
30
|
+
`DEFAULT_SOM_SKU`/`TEMPLATE_SUPPORTED_SKUS`. Nothing in this file is keyed
|
|
31
|
+
on a SKU, an address, or a pin.
|
|
32
|
+
|
|
33
|
+
The ONE derived line is "Default libraries", which reads each template's
|
|
34
|
+
vendored `board.yaml` rather than a registry field
|
|
35
|
+
(`scaffold.vendored_library_names_for`) -- tan-cli#124: the field is
|
|
36
|
+
deliberately blanked for a vendored template and reported "(none)" for
|
|
37
|
+
`edge-ai-starter`, whose scaffold declares `tflite-micro`.
|
|
38
|
+
|
|
39
|
+
**`data.som` (tan-cli#866): what `tan init` will accept/refuse for this
|
|
40
|
+
template, structured, not just in prose.** A project template's `--template`
|
|
41
|
+
hit carries `data.som.{initAcceptsSkus,initRefusesSkuPrefixes}`, computed by
|
|
42
|
+
`_som_support_data` from `scaffold.TEMPLATE_SUPPORTED_SKUS` /
|
|
43
|
+
`scaffold.UNSUPPORTED_SOM_FAMILY_PREFIXES` -- the SAME two tables `tan init`
|
|
44
|
+
already gates `init.invalid-som` / `init.som-unsupported` on, READ here
|
|
45
|
+
rather than retyped.
|
|
46
|
+
|
|
47
|
+
**These are `tan init` REFUSAL predictions, not a capability statement --
|
|
48
|
+
name them accordingly and do not rename them back.** (PR #985 review, major
|
|
49
|
+
1.) `TEMPLATE_SUPPORTED_SKUS` is exactly, and only, the set `init_cmd` checks
|
|
50
|
+
before writing a tree; it says nothing about which SKUs alp-sdk's own
|
|
51
|
+
scaffold catalog actually validates a template against. Measured: for every
|
|
52
|
+
template here except `iot-starter`/`multicore-mailbox`, alp-sdk's catalog
|
|
53
|
+
`supported.som_skus` is the narrow `["E1M-AEN801", "E1M-V2N101"]`
|
|
54
|
+
(`tan/templates/vendored/MANIFEST.md`), while `initRefusesSkuPrefixes` here
|
|
55
|
+
publishes `[]` for those same four (no family is unvendored today) -- i.e.
|
|
56
|
+
`tan init` refuses none of them, where the catalog validates 2. That is not a
|
|
57
|
+
bug in this field: tan is SDK-free by design (I-32 above) and `_family_bucket`
|
|
58
|
+
(`tan.core.scaffold`) deliberately falls an unrecognised SKU prefix onto the
|
|
59
|
+
default (Alif) tree rather than refusing it, so `tan init --template
|
|
60
|
+
sensor-starter --som E1M-AEN301` really does exit 0 today. A field NAMED
|
|
61
|
+
"supported" would be lying about capability for those 4 of 7 templates; a
|
|
62
|
+
field that says "what tan init will accept" is exactly true, including for
|
|
63
|
+
a SKU that does not exist (`E1M-ZZZ999` also predicts `ok`, correctly, by
|
|
64
|
+
this policy). `python/tests/gates/test_no_new_hardware_facts.py` already
|
|
65
|
+
tracks the underlying gap as its largest acknowledged debt item, retiring
|
|
66
|
+
only once the SDK catalog's own per-template family mapping is readable from
|
|
67
|
+
`metadata/**` -- that debt is orthogonal to and not resolved by this field;
|
|
68
|
+
do not read `initAcceptsSkus`/`initRefusesSkuPrefixes` as "the SDK says this
|
|
69
|
+
combination works".
|
|
70
|
+
|
|
71
|
+
Before this PR, the only place a template's SoM restriction was written down
|
|
72
|
+
was inside `details[]` prose ("... (E1M-AEN801 only)"), which a consumer
|
|
73
|
+
would have had to parse and which could silently drift from what `tan init`
|
|
74
|
+
actually enforces -- #866 is named for exactly that string. The `iot-starter`
|
|
75
|
+
description's own parenthetical, and its explanation's Wi-Fi-transport
|
|
76
|
+
sentence, are now GENERATED from the same data (`_only_note`, `_iot_wifi_note`),
|
|
77
|
+
so neither can disagree with it by construction; see
|
|
78
|
+
`tests/commands/test_explain_command.py`'s
|
|
79
|
+
`test_every_sku_mentioned_in_template_prose_matches_its_structured_som_data`
|
|
80
|
+
for the drift gate over every OTHER SKU mention in project-template prose
|
|
81
|
+
(`multicore-mailbox`'s explanation, the one sentence this change did not also
|
|
82
|
+
make generated). Scoped to PROJECT templates only -- module
|
|
83
|
+
templates and generation targets carry no SoM concept, so `data.som` is
|
|
84
|
+
absent (not `null`) on those two selector kinds, the same absent-vs-null
|
|
85
|
+
convention `--code`'s `data.diagnostic`/`data.suggestions` already use.
|
|
86
|
+
|
|
87
|
+
**Every failure is a coded issue, never a traceback.** Ambiguous selector,
|
|
88
|
+
unknown template, unknown target, and an unreadable vendored tree each map to an
|
|
89
|
+
`explain.*` code and exit 1/5; the catch-all at the bottom is the backstop. This
|
|
90
|
+
command spawns no subprocess and opens at most one file (a vendored board.yaml,
|
|
91
|
+
only for `--template`), so there is nothing to time out. `available` is emitted
|
|
92
|
+
on the failure path too -- deliberately, so a caller that guessed wrong gets the
|
|
93
|
+
valid ids in the same envelope.
|
|
94
|
+
|
|
95
|
+
NOT PORTED, and honest about it: `--quiet` (Rust suppresses the `- <detail>`
|
|
96
|
+
lines with it). The Python port carries no `--quiet` on any command, and one
|
|
97
|
+
command-local copy would be a worse surface than a clean refusal -- passing it
|
|
98
|
+
is a Click usage error naming the unknown flag.
|
|
99
|
+
|
|
100
|
+
**`--code` is a THIRD selector, and the one mode that reads a checkout**
|
|
101
|
+
(ADR-0020 end-state B). `tan explain --code ALP-B003` resolves an
|
|
102
|
+
alp-sdk diagnostic / `ALP_ERR_*` code out of the bound checkout's
|
|
103
|
+
`metadata/error-catalog.json` -- the lookup half of alp-sdk's
|
|
104
|
+
`scripts/alp_cli/explain.py`, whose retirement this enables. A FLAG on this
|
|
105
|
+
verb, not a new command: alp-sdk spells it `alp explain <code>` too, so the
|
|
106
|
+
muscle memory transfers and the ambiguity guard already here extends to it.
|
|
107
|
+
Not the positional, which already means TEMPLATE -- sniffing "does this
|
|
108
|
+
argument look like a code?" is exactly the ambiguous-input class
|
|
109
|
+
`explain.positional-template-conflict` refuses.
|
|
110
|
+
|
|
111
|
+
The other three paths stay BYTE-IDENTICAL, deliberately: `--code` adds no
|
|
112
|
+
line to the overview and no key to `data.available` (the `explain-overview`
|
|
113
|
+
golden pins both, and `--help` already lists the flag), and
|
|
114
|
+
`data.diagnostic`/`data.suggestions` appear on the `--code` path ALONE -- the
|
|
115
|
+
extension reads a `data` key it does not find with a `?? []` fallback, so an
|
|
116
|
+
absent key is the honest shape for a mode that did not run. I-32 therefore
|
|
117
|
+
still holds where it was measured to matter: with no alp-sdk anywhere,
|
|
118
|
+
`tan init`'s templates are still explainable. With no checkout resolvable
|
|
119
|
+
`--code` REFUSES (`explain.sdk-root-unresolved`) rather than answering from a
|
|
120
|
+
vendored copy -- the code list is alp-sdk's fact (I-26/ADR-0017), and a baked
|
|
121
|
+
copy would answer confidently out of date.
|
|
122
|
+
|
|
123
|
+
DIVERGENCE from `alp_cli/explain.py`, measured by running both against the same
|
|
124
|
+
alp-sdk `origin/dev` worktree, and each one deliberate
|
|
125
|
+
(`faultdecode_cmd.py:14-26` is the precedent for documenting it here):
|
|
126
|
+
|
|
127
|
+
* **Field VALUES are verbatim; the frame around them is tan's.** `explain.py`
|
|
128
|
+
writes `<CODE> (<kind>)` with two spaces then ` <label>: <value>` per
|
|
129
|
+
field; this writes `explain: <CODE> (<kind>)` then `- <label>: <value>`,
|
|
130
|
+
because that is the ONE shape every `explain` selector renders in and a
|
|
131
|
+
per-mode renderer would make two commands out of one. Same fields, same
|
|
132
|
+
order, same omissions, same text.
|
|
133
|
+
* **stderr, not stdout.** `explain.py` `click.echo`s to stdout; every `tan`
|
|
134
|
+
command's human text goes to stderr because stdout is the envelope channel.
|
|
135
|
+
* **`--format json` wraps; `explain.py --json` does not.** The raw entry
|
|
136
|
+
`explain.py` printed as its whole document is `data.diagnostic` here,
|
|
137
|
+
byte-for-byte, so the payload survives the wrapping intact -- the same
|
|
138
|
+
divergence, for the same reason, as `tan faultdecode --format json`.
|
|
139
|
+
* **NOT PORTED: `--no-color`.** `explain.py` paints the header cyan and each
|
|
140
|
+
label yellow; `tan explain` has never coloured any selector's output, so
|
|
141
|
+
there is nothing for a `--no-color` to turn off. The flag still PARSES (it
|
|
142
|
+
is one of the ten global flags) and is simply inert here, as on every other
|
|
143
|
+
`tan` command that emits no colour.
|
|
144
|
+
* **The `doc:` value stays repo-relative** (`docs/diagnostics/ALP-B003.md`),
|
|
145
|
+
exactly as the catalogue holds it. Joining it onto the resolved checkout
|
|
146
|
+
would be a value alp-sdk never wrote; `sdk.root` in the same envelope is
|
|
147
|
+
what a consumer joins it against.
|
|
148
|
+
* **The no-near-miss sentence is tan's own wording, not `explain.py`'s.**
|
|
149
|
+
`explain.py` prints ``unknown code 'ZZZ' -- run `alp explain` against a
|
|
150
|
+
code from metadata/error-catalog.json`` -- its own binary name and
|
|
151
|
+
subcommand, which cannot be repeated verbatim from a `tan`-only install;
|
|
152
|
+
this prints `` explain: unknown code 'ZZZ' -- pass a code from the SDK's
|
|
153
|
+
metadata/error-catalog.json.`` instead (`_unknown_code_line`). Only the
|
|
154
|
+
WITH-SUGGESTIONS miss sentence is verbatim from `explain.py`.
|
|
155
|
+
* **The whole JSON MISS document, not just the hit path.** `alp_cli explain
|
|
156
|
+
<bad> --json` prints `{"error": "unknown-code", "code": ..., "suggestions":
|
|
157
|
+
[...]}` as its ENTIRE stdout document for a miss. `tan explain --code <bad>
|
|
158
|
+
--format json` wraps instead, the same as the hit path: `data.diagnostic`
|
|
159
|
+
is `null` (never the bare `"error"` shape), `data.suggestions` carries the
|
|
160
|
+
shortlist, and the reason lives in `issues[0]` (`explain.code-unknown`) --
|
|
161
|
+
not folded into `data` the way `explain.py`'s single-document miss is.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
from __future__ import annotations
|
|
165
|
+
|
|
166
|
+
import os
|
|
167
|
+
import re
|
|
168
|
+
import sys
|
|
169
|
+
from dataclasses import dataclass, field
|
|
170
|
+
from pathlib import Path
|
|
171
|
+
|
|
172
|
+
import typer
|
|
173
|
+
|
|
174
|
+
from tan.core import error_catalog
|
|
175
|
+
from tan.core.global_flags import accept_global_flags
|
|
176
|
+
from tan.core.scaffold import (
|
|
177
|
+
TEMPLATE_SUPPORTED_SKUS,
|
|
178
|
+
UNSUPPORTED_SOM_FAMILY_PREFIXES,
|
|
179
|
+
TemplateDataError,
|
|
180
|
+
is_family_gated,
|
|
181
|
+
vendored_library_names_for,
|
|
182
|
+
)
|
|
183
|
+
from tan.core.text_layout import wrap_lines
|
|
184
|
+
from tan.env import wrap_width
|
|
185
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
186
|
+
from tan.exit_codes import ExitCode
|
|
187
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
188
|
+
|
|
189
|
+
#: `data.schemaVersion` for this command's payload.
|
|
190
|
+
DATA_SCHEMA_VERSION = "1"
|
|
191
|
+
|
|
192
|
+
#: An unrecognised `[TEMPLATE]`/`--template` value shaped like a diagnostic
|
|
193
|
+
#: code (`ALP-B003`, `ALP_ERR_NO_BACKEND`) rather than a template id. The
|
|
194
|
+
#: overview deliberately never lists `--code` (it would break the golden), so
|
|
195
|
+
#: `--help` was the ONLY way to discover it -- a caller who typed the
|
|
196
|
+
#: alp-sdk spelling minus the flag got "unknown template" with no pointer at
|
|
197
|
+
#: the flag that would have worked. Anchored (not a bare `search`) so an id
|
|
198
|
+
#: that merely CONTAINS one of these shapes mid-string doesn't false-positive.
|
|
199
|
+
_LOOKS_LIKE_A_DIAGNOSTIC_CODE = re.compile(r"^(ALP-B\d+|ALP_ERR_.+)$", re.IGNORECASE)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _code_hint(value: str) -> str:
|
|
203
|
+
"""The extra sentence appended to `explain.template-unknown`'s text line
|
|
204
|
+
when `value` is shaped like a diagnostic code -- empty string otherwise,
|
|
205
|
+
so every other rejected template keeps its existing, golden-pinned
|
|
206
|
+
wording verbatim."""
|
|
207
|
+
if _LOOKS_LIKE_A_DIAGNOSTIC_CODE.match(value):
|
|
208
|
+
return f" Looking for a diagnostic code? Use --code {value} instead."
|
|
209
|
+
return ""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _only_note(skus: tuple[str, ...] | None) -> str:
|
|
213
|
+
"""` (<SKU>[, <SKU>...] only)`, or `""` when `skus` is falsy -- the
|
|
214
|
+
description's trailing SoM qualifier, DERIVED from `TEMPLATE_SUPPORTED_SKUS`
|
|
215
|
+
(`tan.core.scaffold`) rather than a hand-typed SKU string in the registry
|
|
216
|
+
below. tan-cli#866: a hand-typed "(E1M-AEN801 only)" is exactly the second
|
|
217
|
+
source of truth the issue is about -- it can drift from the data the CLI
|
|
218
|
+
actually gates `tan init` on, silently, the way a copy-edit here never
|
|
219
|
+
would if a consumer parsed the sentence instead of `data.som`. Called
|
|
220
|
+
at MODULE LOAD, building `PROJECT_TEMPLATES` below -- must stay defined
|
|
221
|
+
ahead of that tuple."""
|
|
222
|
+
if not skus:
|
|
223
|
+
return ""
|
|
224
|
+
return f" ({', '.join(skus)} only)"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _iot_wifi_note(skus: tuple[str, ...] | None) -> str:
|
|
228
|
+
"""`iot-starter`'s explanation sentence about the CC3501E Wi-Fi transport,
|
|
229
|
+
with its SUPPORTED-SKU mentions (both of them) DERIVED from
|
|
230
|
+
`TEMPLATE_SUPPORTED_SKUS["iot-starter"]` rather than hand-typed a second
|
|
231
|
+
time. PR #985 review, major 3: #866 names this sentence, verbatim, as one
|
|
232
|
+
of the two prose carriers of this restriction the issue is about; before
|
|
233
|
+
this it was hand-typed independently of `_only_note`'s parenthetical, so
|
|
234
|
+
a hand-edit of only its trailing SKU passed the whole suite (mutant E in
|
|
235
|
+
that review). `E1M-V2N101` in the middle clause stays hand-typed: it is
|
|
236
|
+
not a claim of support (the opposite -- the Wi-Fi path that does NOT
|
|
237
|
+
exist there yet) and carries no table this module owns; it is the one
|
|
238
|
+
named entry in `test_explain_command.py`'s `_CONTRAST_MENTIONS`, so a
|
|
239
|
+
future rewrite that changes which SKU is contrasted still has to update
|
|
240
|
+
that gate on purpose rather than by accident.
|
|
241
|
+
|
|
242
|
+
`skus` empty/`None` is unreachable in practice (`iot-starter` is always a
|
|
243
|
+
key of `TEMPLATE_SUPPORTED_SKUS`) but degrades to a SKU-free form rather
|
|
244
|
+
than raising, matching `_only_note`'s style."""
|
|
245
|
+
if not skus:
|
|
246
|
+
return (
|
|
247
|
+
"AEN-only + preview: the CC3501E Wi-Fi transport is "
|
|
248
|
+
"silicon-validated on the supported SKU only; the Wi-Fi path on "
|
|
249
|
+
"any other SoM does not exist yet, so --som is fixed for this "
|
|
250
|
+
"template."
|
|
251
|
+
)
|
|
252
|
+
sku = skus[0]
|
|
253
|
+
return (
|
|
254
|
+
f"AEN-only + preview: {sku}'s CC3501E Wi-Fi transport is "
|
|
255
|
+
f"silicon-validated; the E1M-V2N101 Wi-Fi path does not exist yet, "
|
|
256
|
+
f"so --som is fixed to {sku} for this template."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(frozen=True)
|
|
261
|
+
class ProjectTemplate:
|
|
262
|
+
"""A `tan init` template as `tan explain` describes it.
|
|
263
|
+
|
|
264
|
+
`features` is a `(wifi, mqtt, ble, tls)` tuple defaulting to all-false,
|
|
265
|
+
collapsing Rust's `Option<WizardFeatureFlags>`: `format_feature_flags`
|
|
266
|
+
renders `None` as all-false, so the distinction is unobservable in output
|
|
267
|
+
and modelling it would only add a branch.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
id: str
|
|
271
|
+
label: str
|
|
272
|
+
description: str
|
|
273
|
+
explanation: tuple[str, ...]
|
|
274
|
+
features: tuple[bool, bool, bool, bool] = (False, False, False, False)
|
|
275
|
+
#: The registry's own `libs`, read ONLY for `minimal-app` -- every other
|
|
276
|
+
#: template's libraries come from its vendored board.yaml (tan-cli#124).
|
|
277
|
+
libs: tuple[str, ...] = ()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass(frozen=True)
|
|
281
|
+
class ModuleTemplate:
|
|
282
|
+
"""A `tan scaffold` module template. No `explanation`: Rust's
|
|
283
|
+
`module_template_details` reads description + function_prefix + a fixed
|
|
284
|
+
hint line and nothing else, so the registry's per-module explanation
|
|
285
|
+
strings are unreachable from this command."""
|
|
286
|
+
|
|
287
|
+
id: str
|
|
288
|
+
label: str
|
|
289
|
+
description: str
|
|
290
|
+
function_prefix: str
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(frozen=True)
|
|
294
|
+
class GenerationTarget:
|
|
295
|
+
"""One `tan generate --target <emit>` output, verbatim from
|
|
296
|
+
`GENERATION_TARGET_CATALOG` (`crates/tan-core/src/loader.rs`)."""
|
|
297
|
+
|
|
298
|
+
emit: str
|
|
299
|
+
display_name: str
|
|
300
|
+
output_relative_path: str
|
|
301
|
+
preview_label: str
|
|
302
|
+
preview_language_id: str
|
|
303
|
+
#: True when `output_relative_path` is a documentary template for a
|
|
304
|
+
#: per-SKU/core DIRECTORY, not a literal path (only `zephyr-board`).
|
|
305
|
+
is_directory: bool = False
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
#: The project-wizard templates, in registry order -- `TEMPLATE_DEFINITIONS`
|
|
309
|
+
#: (`crates/tan-core/src/wizard/service/registry.rs`). Order is wire contract:
|
|
310
|
+
#: `data.available.projectTemplates` and the overview's "Project templates:"
|
|
311
|
+
#: line both report it, and `contract/envelopes/explain-overview` pins both.
|
|
312
|
+
PROJECT_TEMPLATES: tuple[ProjectTemplate, ...] = (
|
|
313
|
+
ProjectTemplate(
|
|
314
|
+
id="minimal-app",
|
|
315
|
+
label="Minimal app",
|
|
316
|
+
description="Smallest baseline project with a simple main loop.",
|
|
317
|
+
explanation=(
|
|
318
|
+
"Minimal template keeps generated code intentionally small and neutral.",
|
|
319
|
+
"Use this baseline when you want full control over feature bring-up order.",
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
ProjectTemplate(
|
|
323
|
+
id="zephyr-app",
|
|
324
|
+
label="Zephyr app",
|
|
325
|
+
description=(
|
|
326
|
+
"West-buildable Zephyr application wired to board.yaml via the SDK loader."
|
|
327
|
+
),
|
|
328
|
+
explanation=(
|
|
329
|
+
"Real Zephyr app vendored from the SDK's `minimal` scaffold: "
|
|
330
|
+
"find_package(Zephyr) + board.yaml -> alp.conf via EXTRA_CONF_FILE.",
|
|
331
|
+
"Build with `west build -b <board>` after `export ALP_SDK_ROOT=<your "
|
|
332
|
+
"alp-sdk checkout>`.",
|
|
333
|
+
),
|
|
334
|
+
),
|
|
335
|
+
ProjectTemplate(
|
|
336
|
+
id="sensor-starter",
|
|
337
|
+
label="Sensor starter",
|
|
338
|
+
description=(
|
|
339
|
+
"West-buildable TMP112 i2c-master app wired to board.yaml via the SDK loader."
|
|
340
|
+
),
|
|
341
|
+
explanation=(
|
|
342
|
+
"Real Zephyr app vendored from the SDK's `sensor` scaffold: reads the "
|
|
343
|
+
"TMP112 temperature sensor via <alp/chips/tmp112.h> on BRD_I2C.",
|
|
344
|
+
"Build with `west build -b <board>` after `export ALP_SDK_ROOT=<your "
|
|
345
|
+
"alp-sdk checkout>`.",
|
|
346
|
+
),
|
|
347
|
+
),
|
|
348
|
+
ProjectTemplate(
|
|
349
|
+
id="iot-starter",
|
|
350
|
+
label="IoT starter",
|
|
351
|
+
description=(
|
|
352
|
+
"West-buildable Wi-Fi + MQTT/TLS telemetry app wired to board.yaml via "
|
|
353
|
+
"the SDK loader"
|
|
354
|
+
+ _only_note(TEMPLATE_SUPPORTED_SKUS.get("iot-starter"))
|
|
355
|
+
+ "."
|
|
356
|
+
),
|
|
357
|
+
explanation=(
|
|
358
|
+
"Real Zephyr app vendored from the SDK's `iot` scaffold: brings up Wi-Fi "
|
|
359
|
+
"via the CC3501E bridge and publishes an mqtts:// (TLS) MQTT telemetry "
|
|
360
|
+
"reading on a cadence, through the portable <alp/iot.h> surface.",
|
|
361
|
+
_iot_wifi_note(TEMPLATE_SUPPORTED_SKUS.get("iot-starter")),
|
|
362
|
+
"Build with `west build -b <board>` after `export ALP_SDK_ROOT=<your "
|
|
363
|
+
"alp-sdk checkout>`.",
|
|
364
|
+
),
|
|
365
|
+
# The ONE template whose flags are not blanked in the Rust registry:
|
|
366
|
+
# "Default features" still reads them, and the vendored board.yaml
|
|
367
|
+
# cannot supply `mqtt` (there is no `iot.mqtt` toggle -- MQTT is
|
|
368
|
+
# inherent to this app). `mqtt=false` here would contradict the
|
|
369
|
+
# explanation line above it.
|
|
370
|
+
features=(True, True, False, True),
|
|
371
|
+
),
|
|
372
|
+
ProjectTemplate(
|
|
373
|
+
id="edge-ai-starter",
|
|
374
|
+
label="Edge AI starter",
|
|
375
|
+
description=(
|
|
376
|
+
"West-buildable BME280 cold-chain-monitor app wired to board.yaml via "
|
|
377
|
+
"the SDK loader."
|
|
378
|
+
),
|
|
379
|
+
explanation=(
|
|
380
|
+
"Real Zephyr app vendored from the SDK's `edge-ai` scaffold: a BME280 "
|
|
381
|
+
"cold-chain integrity monitor (MKT / dewpoint / excursion time) plus a "
|
|
382
|
+
"TFLite-Micro anomaly score.",
|
|
383
|
+
'Heterogeneous board.yaml: the app core (m33_sm/m55_hp) runs inference '
|
|
384
|
+
'alongside a companion Cortex-A cluster (a55_cluster/a32_cluster, os: '
|
|
385
|
+
'"off").',
|
|
386
|
+
"Build with `west build -b <board>` after `export ALP_SDK_ROOT=<your "
|
|
387
|
+
"alp-sdk checkout>`.",
|
|
388
|
+
),
|
|
389
|
+
),
|
|
390
|
+
ProjectTemplate(
|
|
391
|
+
id="board-diagnostics",
|
|
392
|
+
label="Board diagnostics",
|
|
393
|
+
description=(
|
|
394
|
+
"West-buildable board self-test app wired to board.yaml via the SDK loader."
|
|
395
|
+
),
|
|
396
|
+
explanation=(
|
|
397
|
+
"Real Zephyr app vendored from the SDK's `diagnostics` scaffold: reads "
|
|
398
|
+
"the SoM/SoC identity, the RUN operating-point profile, and scans the "
|
|
399
|
+
"on-module I2C management bus for a pass/fail bring-up report.",
|
|
400
|
+
"Portable <alp/*> APIs only -- no chip driver -- so the same source runs "
|
|
401
|
+
"on every E1M family; a check a backend can't service reports SKIP, not "
|
|
402
|
+
"FAIL.",
|
|
403
|
+
"Build with `west build -b <board>` after `export ALP_SDK_ROOT=<your "
|
|
404
|
+
"alp-sdk checkout>`.",
|
|
405
|
+
),
|
|
406
|
+
),
|
|
407
|
+
ProjectTemplate(
|
|
408
|
+
id="multicore-mailbox",
|
|
409
|
+
label="Multicore -- mailbox (AEN M55-HP <-> M55-HE)",
|
|
410
|
+
description=(
|
|
411
|
+
"Dual-Zephyr-core starter for the Alif Ensemble E8: two Cortex-M55 "
|
|
412
|
+
"cores, both real project cores."
|
|
413
|
+
),
|
|
414
|
+
explanation=(
|
|
415
|
+
"Vendored from the SDK's `multicore-mailbox` scaffold. Both cores get "
|
|
416
|
+
"their own app -- `m55_hp: app: ./src` and `m55_he: app: ./peer` -- "
|
|
417
|
+
"which is the topology no --template/--cores combination could "
|
|
418
|
+
"scaffold before (tan-cli#864).",
|
|
419
|
+
"HP stages a payload in cache-coherent shared SRAM and signals HE "
|
|
420
|
+
"through the hardware mailbox over the portable <alp/mproc.h> "
|
|
421
|
+
"raw-shmem + mailbox + hwsem surface; HE echoes it back. Deliberately "
|
|
422
|
+
"NOT RPMsg -- no framing and no negotiated channel, just a raw pointer "
|
|
423
|
+
"view over a fixed carve-out plus a doorbell.",
|
|
424
|
+
"E1M-AEN801 only: the SDK catalog gates this template's "
|
|
425
|
+
"`supported.som_skus` to that one SKU and refuses to emit it for any "
|
|
426
|
+
"other, so tan refuses the same set rather than rendering it against "
|
|
427
|
+
"another tree.",
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
#: The module-scaffold templates, in registry order --
|
|
433
|
+
#: `MODULE_TEMPLATE_DEFINITIONS` (same Rust file).
|
|
434
|
+
MODULE_TEMPLATES: tuple[ModuleTemplate, ...] = (
|
|
435
|
+
ModuleTemplate(
|
|
436
|
+
id="sensor-driver",
|
|
437
|
+
label="Sensor driver module",
|
|
438
|
+
description="Adds a source/header pair for sensor acquisition logic.",
|
|
439
|
+
function_prefix="alp_sensor",
|
|
440
|
+
),
|
|
441
|
+
ModuleTemplate(
|
|
442
|
+
id="connectivity-service",
|
|
443
|
+
label="Connectivity service module",
|
|
444
|
+
description="Adds module skeleton for network/session orchestration.",
|
|
445
|
+
function_prefix="alp_conn",
|
|
446
|
+
),
|
|
447
|
+
ModuleTemplate(
|
|
448
|
+
id="inference-stage",
|
|
449
|
+
label="Inference stage module",
|
|
450
|
+
description="Adds module skeleton for model pre/post processing path.",
|
|
451
|
+
function_prefix="alp_infer",
|
|
452
|
+
),
|
|
453
|
+
ModuleTemplate(
|
|
454
|
+
id="diagnostics-check",
|
|
455
|
+
label="Diagnostics check module",
|
|
456
|
+
description="Adds bring-up and runtime health-check module scaffold.",
|
|
457
|
+
function_prefix="alp_diag",
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
#: Every generation target, in fixed catalog order. All TEN of them: tan-cli#165
|
|
462
|
+
#: was the catalogue carrying only four, so `tan explain --target zephyr-board`
|
|
463
|
+
#: answered "Unknown generation target" for a target `tan generate` accepts.
|
|
464
|
+
#: Kept in step with `generate_cmd._OUTPUT_RELATIVE_PATH` by
|
|
465
|
+
#: `tests/commands/test_explain_command.py`, which is the guard against those
|
|
466
|
+
#: two drifting apart again -- the exact failure #165 was.
|
|
467
|
+
GENERATION_TARGETS: tuple[GenerationTarget, ...] = (
|
|
468
|
+
GenerationTarget(
|
|
469
|
+
emit="zephyr-conf",
|
|
470
|
+
display_name="Zephyr config",
|
|
471
|
+
output_relative_path="build/generated/alp.conf",
|
|
472
|
+
preview_label="Zephyr config preview",
|
|
473
|
+
preview_language_id="properties",
|
|
474
|
+
),
|
|
475
|
+
GenerationTarget(
|
|
476
|
+
emit="dts-overlay",
|
|
477
|
+
display_name="Devicetree overlay",
|
|
478
|
+
output_relative_path="build/generated/alp.overlay",
|
|
479
|
+
preview_label="Devicetree overlay preview",
|
|
480
|
+
preview_language_id="dts",
|
|
481
|
+
),
|
|
482
|
+
GenerationTarget(
|
|
483
|
+
emit="cmake-args",
|
|
484
|
+
display_name="CMake args",
|
|
485
|
+
output_relative_path="build/generated/alp-cmake-args.txt",
|
|
486
|
+
preview_label="CMake args preview",
|
|
487
|
+
preview_language_id="plaintext",
|
|
488
|
+
),
|
|
489
|
+
GenerationTarget(
|
|
490
|
+
emit="yocto-conf",
|
|
491
|
+
display_name="Yocto config",
|
|
492
|
+
output_relative_path="build/generated/alp-yocto.conf",
|
|
493
|
+
preview_label="Yocto config preview",
|
|
494
|
+
preview_language_id="properties",
|
|
495
|
+
),
|
|
496
|
+
GenerationTarget(
|
|
497
|
+
emit="native-sim-overlay",
|
|
498
|
+
display_name="native_sim overlay",
|
|
499
|
+
# The one target outside build/generated/: Zephyr auto-discovers
|
|
500
|
+
# boards/<board>.overlay in the app's own SOURCE tree. Must stay
|
|
501
|
+
# verbatim equal to generate_cmd's path table.
|
|
502
|
+
output_relative_path="boards/native_sim_native_64.overlay",
|
|
503
|
+
preview_label="native_sim overlay preview",
|
|
504
|
+
preview_language_id="dts",
|
|
505
|
+
),
|
|
506
|
+
GenerationTarget(
|
|
507
|
+
emit="carrier-netlist",
|
|
508
|
+
display_name="Carrier netlist",
|
|
509
|
+
output_relative_path="build/generated/carrier-netlist.json",
|
|
510
|
+
preview_label="Carrier netlist preview",
|
|
511
|
+
preview_language_id="json",
|
|
512
|
+
),
|
|
513
|
+
GenerationTarget(
|
|
514
|
+
emit="west-libraries",
|
|
515
|
+
display_name="West libraries",
|
|
516
|
+
output_relative_path="build/generated/alp-west-libs.yml",
|
|
517
|
+
preview_label="West libraries preview",
|
|
518
|
+
preview_language_id="yaml",
|
|
519
|
+
),
|
|
520
|
+
GenerationTarget(
|
|
521
|
+
emit="hw-info-h",
|
|
522
|
+
display_name="Hardware info header",
|
|
523
|
+
output_relative_path="build/generated/alp_hw_info_build.h",
|
|
524
|
+
preview_label="Hardware info header preview",
|
|
525
|
+
preview_language_id="c",
|
|
526
|
+
),
|
|
527
|
+
GenerationTarget(
|
|
528
|
+
emit="os-topology",
|
|
529
|
+
display_name="OS topology",
|
|
530
|
+
output_relative_path="build/generated/os-topology.json",
|
|
531
|
+
preview_label="OS topology preview",
|
|
532
|
+
preview_language_id="json",
|
|
533
|
+
),
|
|
534
|
+
GenerationTarget(
|
|
535
|
+
emit="zephyr-board",
|
|
536
|
+
display_name="Zephyr board tree",
|
|
537
|
+
# Documentary only (`is_directory`): the real name is resolved per-run
|
|
538
|
+
# by `generate_cmd.zephyr_board_dir_name`. Never joined onto a
|
|
539
|
+
# workspace root from here -- this command writes nothing at all.
|
|
540
|
+
output_relative_path="build/boards/alp_e1m_<sku-slug>_<core>/",
|
|
541
|
+
preview_label="Zephyr board tree preview",
|
|
542
|
+
preview_language_id="plaintext",
|
|
543
|
+
is_directory=True,
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class ExplainError(Exception):
|
|
549
|
+
"""A failure with its issue code, message, exit code and human line already
|
|
550
|
+
decided. `selector_value` is the id the caller asked about, echoed back in
|
|
551
|
+
`data.selector.value` (empty for an ambiguous selector, which named two)."""
|
|
552
|
+
|
|
553
|
+
def __init__(
|
|
554
|
+
self,
|
|
555
|
+
code: str,
|
|
556
|
+
message: str,
|
|
557
|
+
text_line: str,
|
|
558
|
+
*,
|
|
559
|
+
exit_code: ExitCode = ExitCode.RUNTIME_FAILURE,
|
|
560
|
+
selector_value: str = "",
|
|
561
|
+
extra_data: dict | None = None,
|
|
562
|
+
extra_issues: list[Issue] | None = None,
|
|
563
|
+
) -> None:
|
|
564
|
+
super().__init__(message)
|
|
565
|
+
self.code = code
|
|
566
|
+
self.message = message
|
|
567
|
+
self.text_line = text_line
|
|
568
|
+
self.exit_code = exit_code
|
|
569
|
+
self.selector_value = selector_value
|
|
570
|
+
#: Keys folded into the failure envelope's `data` -- only the `--code`
|
|
571
|
+
#: miss uses it, to carry `suggestions` next to the refusal instead of
|
|
572
|
+
#: burying the shortlist in an issue message a consumer must re-parse.
|
|
573
|
+
self.extra_data = extra_data or {}
|
|
574
|
+
#: tan-cli#950: PREPENDED ahead of this error's own `Issue` by
|
|
575
|
+
#: `_fail`, the same `[*resolution_issues, Issue(...)]` shape
|
|
576
|
+
#: `clean_cmd._run` / `bootstrap_cmd._refusal` / `new_som_cmd.new_som`
|
|
577
|
+
#: use. Only `bind_sdk`'s `explain.sdk-root-unresolved` raise
|
|
578
|
+
#: populates this today (`sdk.project-pin-unresolved` /
|
|
579
|
+
#: `sdk.global-default-foreign-project`, tan-cli#263 review /
|
|
580
|
+
#: tan-cli#464): it is the one `ExplainError` site that can compute a
|
|
581
|
+
#: broken-pin/foreign-default fact and then discard the checkout it
|
|
582
|
+
#: came from, so it is also the one site the shared `Envelope(...,
|
|
583
|
+
#: sdk=...)` advisory machinery (`_with_sdk_resolution_advisories`)
|
|
584
|
+
#: cannot reach -- that machinery fires only when an `SdkInfo` was
|
|
585
|
+
#: actually built, and `bind_sdk` raises before building one.
|
|
586
|
+
self.extra_issues = extra_issues or []
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@dataclass
|
|
590
|
+
class _Result:
|
|
591
|
+
"""A resolved (non-error) explanation."""
|
|
592
|
+
|
|
593
|
+
kind: str
|
|
594
|
+
value: str
|
|
595
|
+
summary: str
|
|
596
|
+
details: list[str] = field(default_factory=list)
|
|
597
|
+
#: `--code` only: the catalogue entry verbatim plus an (empty on a hit)
|
|
598
|
+
#: suggestion list, folded into `data`. Empty for every other selector, so
|
|
599
|
+
#: their envelopes keep the exact key set the golden pins.
|
|
600
|
+
extra_data: dict = field(default_factory=dict)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ---------------------------------------------------------------------------
|
|
604
|
+
# Detail lines
|
|
605
|
+
# ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _format_library_names(names: list[str]) -> str:
|
|
609
|
+
"""A comma-joined library list, or `(none)` when empty. `(none)` is the
|
|
610
|
+
SOURCED answer for `zephyr-app`/`sensor-starter`/`board-diagnostics` --
|
|
611
|
+
their vendored board.yaml has no `libraries:` block -- not a default."""
|
|
612
|
+
return ", ".join(names) if names else "(none)"
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _format_feature_flags(features: tuple[bool, bool, bool, bool]) -> str:
|
|
616
|
+
"""`wifi=.. mqtt=.. ble=.. tls=..` with Rust's lowercase bool spelling.
|
|
617
|
+
`str(True)` would emit `True`, which no consumer of this line expects."""
|
|
618
|
+
wifi, mqtt, ble, tls = (str(f).lower() for f in features)
|
|
619
|
+
return f"wifi={wifi} mqtt={mqtt} ble={ble} tls={tls}"
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _som_support_data(template_id: str) -> dict[str, object]:
|
|
623
|
+
"""`data.som` for one project template -- what `tan init --template
|
|
624
|
+
<template_id>` will ACCEPT or REFUSE for `--som`, READ (never retyped)
|
|
625
|
+
from `TEMPLATE_SUPPORTED_SKUS`/`UNSUPPORTED_SOM_FAMILY_PREFIXES`
|
|
626
|
+
(`tan.core.scaffold`) so this cannot drift from the refusal it describes
|
|
627
|
+
(tan-cli#866).
|
|
628
|
+
|
|
629
|
+
PR #985 review, major 1: named `initAcceptsSkus`/`initRefusesSkuPrefixes`
|
|
630
|
+
ON PURPOSE, not `supportedSkus`/`unsupportedSkuPrefixes` -- this is a
|
|
631
|
+
REFUSAL policy, not a capability statement, and the two provably differ:
|
|
632
|
+
`tan.core.scaffold._family_bucket` falls an unrecognised SKU prefix onto
|
|
633
|
+
the default (Alif) tree rather than refusing it, so for every template
|
|
634
|
+
here except `iot-starter`/`multicore-mailbox`, `initAcceptsSkus` is wider
|
|
635
|
+
than alp-sdk's own scaffold-catalog `supported.som_skus` (see the module
|
|
636
|
+
docstring above for the measured gap). A name that said "supported"
|
|
637
|
+
would claim capability this field cannot back for 4 of 7 templates.
|
|
638
|
+
|
|
639
|
+
`initAcceptsSkus` mirrors the `init.invalid-som` allowlist -- `None` when
|
|
640
|
+
this template carries none. `initRefusesSkuPrefixes` mirrors the
|
|
641
|
+
`init.som-unsupported` family exclusion -- always `[]` when
|
|
642
|
+
`initAcceptsSkus` is set, because an exact-SKU allowlist is strictly
|
|
643
|
+
narrower than (and already implies) that exclusion; repeating it would be
|
|
644
|
+
a second way to say the same thing, the exact class of duplication this
|
|
645
|
+
field exists to retire. A template with neither restriction
|
|
646
|
+
(`minimal-app`, tan's one vendor-neutral, non-family-gated template --
|
|
647
|
+
`scaffold.is_family_gated` is `False` for it alone) reports
|
|
648
|
+
`initAcceptsSkus: null, initRefusesSkuPrefixes: []`: `tan init` accepts
|
|
649
|
+
every SoM for it, unconditionally.
|
|
650
|
+
"""
|
|
651
|
+
supported = TEMPLATE_SUPPORTED_SKUS.get(template_id)
|
|
652
|
+
if supported is not None:
|
|
653
|
+
return {"initAcceptsSkus": list(supported), "initRefusesSkuPrefixes": []}
|
|
654
|
+
if is_family_gated(template_id):
|
|
655
|
+
return {
|
|
656
|
+
"initAcceptsSkus": None,
|
|
657
|
+
"initRefusesSkuPrefixes": list(UNSUPPORTED_SOM_FAMILY_PREFIXES),
|
|
658
|
+
}
|
|
659
|
+
return {"initAcceptsSkus": None, "initRefusesSkuPrefixes": []}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _family_excluded_note(template_id: str) -> str | None:
|
|
663
|
+
"""One `details[]` line for a FAMILY-gated (but not exact-SKU-gated)
|
|
664
|
+
project template, DERIVED from `UNSUPPORTED_SOM_FAMILY_PREFIXES` -- `None`
|
|
665
|
+
when there is nothing to add.
|
|
666
|
+
|
|
667
|
+
PR #985 review, minor 5: before this, text mode said nothing at all about
|
|
668
|
+
the family exclusion for `zephyr-app`/`sensor-starter`/`edge-ai-starter`/
|
|
669
|
+
`board-diagnostics` even after `data.som.initRefusesSkuPrefixes` started
|
|
670
|
+
carrying it in JSON -- a human running `tan explain --template zephyr-app`
|
|
671
|
+
with no `--format json` only discovered the restriction as
|
|
672
|
+
`init.som-unsupported`, at `tan init` time. `None` for `minimal-app` (not
|
|
673
|
+
family-gated at all) and for the two exact-SKU-gated templates
|
|
674
|
+
(`iot-starter`/`multicore-mailbox`): `_only_note` already reports their
|
|
675
|
+
narrower restriction from the description line, so a second sentence here
|
|
676
|
+
would repeat it rather than add information."""
|
|
677
|
+
if template_id in TEMPLATE_SUPPORTED_SKUS or not is_family_gated(template_id):
|
|
678
|
+
return None
|
|
679
|
+
if not UNSUPPORTED_SOM_FAMILY_PREFIXES:
|
|
680
|
+
return None
|
|
681
|
+
prefixes = ", ".join(UNSUPPORTED_SOM_FAMILY_PREFIXES)
|
|
682
|
+
return f"Refuses --som for these SoM families: {prefixes}."
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _project_template_details(pt: ProjectTemplate) -> list[str]:
|
|
686
|
+
"""Description, per-template explanation, the family-exclusion note (when
|
|
687
|
+
one applies), default libraries, default features. Raises
|
|
688
|
+
`TemplateDataError` when the vendored board.yaml behind the libraries
|
|
689
|
+
line will not read."""
|
|
690
|
+
names = vendored_library_names_for(pt.id)
|
|
691
|
+
if names is None:
|
|
692
|
+
names = list(pt.libs) # `minimal-app` only: no vendored tree to read.
|
|
693
|
+
details = [pt.description, *pt.explanation]
|
|
694
|
+
family_note = _family_excluded_note(pt.id)
|
|
695
|
+
if family_note is not None:
|
|
696
|
+
details.append(family_note)
|
|
697
|
+
details.append(f"Default libraries: {_format_library_names(names)}")
|
|
698
|
+
details.append(f"Default features: {_format_feature_flags(pt.features)}")
|
|
699
|
+
return details
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _module_template_details(mt: ModuleTemplate) -> list[str]:
|
|
703
|
+
return [
|
|
704
|
+
mt.description,
|
|
705
|
+
f"Function prefix: {mt.function_prefix}",
|
|
706
|
+
"Use this template with tan scaffold to generate a module source/header "
|
|
707
|
+
"baseline.",
|
|
708
|
+
]
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _generation_target_details(target: GenerationTarget) -> list[str]:
|
|
712
|
+
"""tan-cli#165: `zephyr-board`'s path is a documentary template, so the
|
|
713
|
+
"(writes a directory of files, not one path)" suffix says so instead of
|
|
714
|
+
implying a real single file the way the other nine targets' paths are."""
|
|
715
|
+
note = " (writes a directory of files, not one path)" if target.is_directory else ""
|
|
716
|
+
return [
|
|
717
|
+
f"Display name: {target.display_name}",
|
|
718
|
+
f"Output path: {target.output_relative_path}{note}",
|
|
719
|
+
f"Preview label: {target.preview_label}",
|
|
720
|
+
f"Preview language: {target.preview_language_id}",
|
|
721
|
+
]
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _print_detail_lines(details: list[str], width: int | None) -> None:
|
|
725
|
+
"""`- <detail>` per entry, to stderr -- hard-wrapped past `width` when it
|
|
726
|
+
is not `None` (a real terminal; see `tan.env.wrap_width`), unwrapped
|
|
727
|
+
verbatim off one (piped/redirected, or non-interactive) -- the property
|
|
728
|
+
`tan explain | grep` and every existing golden depend on. Every detail
|
|
729
|
+
wraps the same way; see `tan.core.text_layout.wrap_lines` for why there
|
|
730
|
+
is no longer a "record-shaped" exemption here.
|
|
731
|
+
"""
|
|
732
|
+
for line in wrap_lines([f"- {detail}" for detail in details], width):
|
|
733
|
+
print(line, file=sys.stderr)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
# ---------------------------------------------------------------------------
|
|
737
|
+
# Resolution
|
|
738
|
+
# ---------------------------------------------------------------------------
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _clean(raw: str | None) -> str | None:
|
|
742
|
+
"""A selector as Rust reads it: trimmed, and blank counts as ABSENT --
|
|
743
|
+
`--template " "` prints the overview, it is not an unknown template."""
|
|
744
|
+
if raw is None:
|
|
745
|
+
return None
|
|
746
|
+
trimmed = raw.strip()
|
|
747
|
+
return trimmed or None
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _overview() -> _Result:
|
|
751
|
+
return _Result(
|
|
752
|
+
kind="overview",
|
|
753
|
+
value="all",
|
|
754
|
+
summary="tan explain topics",
|
|
755
|
+
details=[
|
|
756
|
+
"Use --template to explain a project template (init) or module template "
|
|
757
|
+
"(scaffold).",
|
|
758
|
+
"Use --target to explain a generation output target.",
|
|
759
|
+
"Project templates: " + ", ".join(t.id for t in PROJECT_TEMPLATES),
|
|
760
|
+
"Module templates: " + ", ".join(t.id for t in MODULE_TEMPLATES),
|
|
761
|
+
"Generation targets: " + ", ".join(t.emit for t in GENERATION_TARGETS),
|
|
762
|
+
],
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def resolve(template: str | None, target: str | None) -> _Result:
|
|
767
|
+
"""The explanation for this selector pair. Raises `ExplainError` for both
|
|
768
|
+
selectors at once or an unknown id, and `TemplateDataError` when a vendored
|
|
769
|
+
board.yaml will not read.
|
|
770
|
+
|
|
771
|
+
A project template shadows a module template of the same id, matching the
|
|
772
|
+
Rust's search order -- the two id sets are disjoint today, and this keeps
|
|
773
|
+
the tie-break explicit rather than accidental if they ever collide.
|
|
774
|
+
"""
|
|
775
|
+
template = _clean(template)
|
|
776
|
+
target = _clean(target)
|
|
777
|
+
|
|
778
|
+
if template is not None and target is not None:
|
|
779
|
+
raise ExplainError(
|
|
780
|
+
"explain.ambiguous-selector",
|
|
781
|
+
"Use either --template or --target for explain, not both.",
|
|
782
|
+
"explain: use either --template or --target, but not both in the same "
|
|
783
|
+
"command.",
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
if template is not None:
|
|
787
|
+
for pt in PROJECT_TEMPLATES:
|
|
788
|
+
if pt.id == template:
|
|
789
|
+
return _Result(
|
|
790
|
+
kind="project-template",
|
|
791
|
+
value=pt.id,
|
|
792
|
+
summary=f"{pt.label} ({pt.id})",
|
|
793
|
+
details=_project_template_details(pt),
|
|
794
|
+
# tan-cli#866: structured, so a consumer filters a picker
|
|
795
|
+
# without parsing `details[]` prose. Project templates
|
|
796
|
+
# only -- module templates and generation targets carry
|
|
797
|
+
# no SoM concept.
|
|
798
|
+
extra_data={"som": _som_support_data(pt.id)},
|
|
799
|
+
)
|
|
800
|
+
for mt in MODULE_TEMPLATES:
|
|
801
|
+
if mt.id == template:
|
|
802
|
+
return _Result(
|
|
803
|
+
kind="module-template",
|
|
804
|
+
value=mt.id,
|
|
805
|
+
summary=f"{mt.label} ({mt.id})",
|
|
806
|
+
details=_module_template_details(mt),
|
|
807
|
+
)
|
|
808
|
+
raise ExplainError(
|
|
809
|
+
"explain.template-unknown",
|
|
810
|
+
f"Unknown template '{template}'.",
|
|
811
|
+
f"explain: unknown template '{template}'. Run tan explain without "
|
|
812
|
+
f"selectors to list available topics.{_code_hint(template)}",
|
|
813
|
+
selector_value=template,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
if target is not None:
|
|
817
|
+
for gt in GENERATION_TARGETS:
|
|
818
|
+
if gt.emit == target:
|
|
819
|
+
return _Result(
|
|
820
|
+
kind="generation-target",
|
|
821
|
+
value=gt.emit,
|
|
822
|
+
summary=f"{gt.display_name} ({gt.emit})",
|
|
823
|
+
details=_generation_target_details(gt),
|
|
824
|
+
)
|
|
825
|
+
raise ExplainError(
|
|
826
|
+
"explain.target-unknown",
|
|
827
|
+
f"Unknown generation target '{target}'.",
|
|
828
|
+
f"explain: unknown generation target '{target}'. Run tan explain "
|
|
829
|
+
f"without selectors to list available targets.",
|
|
830
|
+
selector_value=target,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
return _overview()
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def bind_sdk(sdk_root_arg: str | None, project: str | None, code: str) -> tuple[Path, SdkInfo]:
|
|
837
|
+
"""The checkout `--code` reads, via the NARROW ladder (`--sdk-root` >
|
|
838
|
+
`.alp/sdk-path` > `~/.alp/sdk-default` > discovery), walked from the
|
|
839
|
+
`--project`-resolved workspace root -- `os.path.join(cwd, project)`, the
|
|
840
|
+
same join `build_cmd.build`/`run_cmd.run`/`inspect_cmd.inspect` use, so an
|
|
841
|
+
absolute `--project` replaces the cwd outright rather than nesting under it.
|
|
842
|
+
|
|
843
|
+
Narrow because `explain` is not one of the three commands the oracle routes
|
|
844
|
+
wide (`init`/`generate`/`examples`), so following the
|
|
845
|
+
thirteen-command majority makes `tan explain --code` answer out of the same
|
|
846
|
+
checkout `tan validate` used in the same directory -- the only reason to
|
|
847
|
+
look a code up while a build is failing. `--project` IS read here (unlike
|
|
848
|
+
every other selector on this command, see `explain`'s docstring): it is the
|
|
849
|
+
one input this whole ladder exists to resolve against, and the twelve other
|
|
850
|
+
`resolve_sdk_root_ladder` call sites all pass a `--project`-derived root,
|
|
851
|
+
not the bare CWD -- accepting the flag and then resolving from the CWD
|
|
852
|
+
anyway is the accepted-but-ignored input class this command refuses
|
|
853
|
+
everywhere else (`explain`'s `--sdk-root` rationale).
|
|
854
|
+
|
|
855
|
+
Raises `ExplainError` when no tier resolves anything, INCLUDING when
|
|
856
|
+
`--sdk-root` (the terminal tier) points at a directory that resolves but
|
|
857
|
+
carries no `SDK_MARKER` -- `resolve_sdk_root_ladder` itself does not check
|
|
858
|
+
that for its terminal tier (I-31: a typo'd flag must not fall through to a
|
|
859
|
+
lower tier), so an unmarked `--sdk-root` would otherwise reach
|
|
860
|
+
`resolve_code` and misreport as `explain.catalog-unreadable` ("run the
|
|
861
|
+
generator" in a checkout that was never one) instead of naming the real
|
|
862
|
+
problem. Either way this is a refusal naming what it could not find, never
|
|
863
|
+
an empty answer. Both imports are LOCAL so the three SDK-free paths keep
|
|
864
|
+
paying nothing for a mode they never enter.
|
|
865
|
+
|
|
866
|
+
tan-cli#950: the unresolved-SDK raise carries `resolution.broken_project_pin`
|
|
867
|
+
/ `.foreign_global_default_for` on `ExplainError.extra_issues` (the
|
|
868
|
+
`clean_cmd._run` / `bootstrap_cmd._refusal` / `new_som_cmd.new_som` shape --
|
|
869
|
+
the eighth instance of the tan-cli#900 class). This is the ONE branch that
|
|
870
|
+
needs it computed explicitly: the SUCCESS return below hands its `SdkInfo`
|
|
871
|
+
to `_emit`'s `Envelope(..., sdk=...)`, whose own
|
|
872
|
+
`_with_sdk_resolution_advisories` already discloses the same pair
|
|
873
|
+
generically from `SdkInfo.broken_project_pin` -- but `bind_sdk` raising
|
|
874
|
+
here means no `SdkInfo` is ever built, so that shared machinery never
|
|
875
|
+
runs and the fact would otherwise be discarded with the rest of
|
|
876
|
+
`resolution`.
|
|
877
|
+
"""
|
|
878
|
+
from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
|
|
879
|
+
from tan.core.sdk_discovery import (
|
|
880
|
+
global_default_foreign_project_issue,
|
|
881
|
+
project_pin_issue,
|
|
882
|
+
resolve_sdk_root_ladder,
|
|
883
|
+
with_sdk_search,
|
|
884
|
+
)
|
|
885
|
+
from tan.core.shapes import SDK_MARKER
|
|
886
|
+
|
|
887
|
+
cwd = Path.cwd()
|
|
888
|
+
workspace_root = cwd if project is None else Path(os.path.join(str(cwd), project))
|
|
889
|
+
resolution = resolve_sdk_root_ladder(sdk_root_arg, workspace_root)
|
|
890
|
+
if resolution.path is None or not resolution.path.joinpath(*SDK_MARKER).exists():
|
|
891
|
+
pin_issue = project_pin_issue(resolution.broken_project_pin, resolution.tier)
|
|
892
|
+
foreign_issue = global_default_foreign_project_issue(
|
|
893
|
+
resolution.foreign_global_default_for
|
|
894
|
+
)
|
|
895
|
+
raise ExplainError(
|
|
896
|
+
"explain.sdk-root-unresolved",
|
|
897
|
+
with_sdk_search(
|
|
898
|
+
f"alp-sdk root is unresolved, so no diagnostic catalogue could be "
|
|
899
|
+
f"read -- {NO_SDK_NEXT_STEPS}.",
|
|
900
|
+
workspace_root,
|
|
901
|
+
sdk_root_arg,
|
|
902
|
+
),
|
|
903
|
+
with_sdk_search(
|
|
904
|
+
f"explain: alp-sdk root is unresolved, so no diagnostic catalogue "
|
|
905
|
+
f"could be read -- {NO_SDK_NEXT_STEPS}.",
|
|
906
|
+
workspace_root,
|
|
907
|
+
sdk_root_arg,
|
|
908
|
+
),
|
|
909
|
+
selector_value=code.strip(),
|
|
910
|
+
extra_issues=[i for i in (pin_issue, foreign_issue) if i is not None],
|
|
911
|
+
)
|
|
912
|
+
return resolution.path, SdkInfo.from_resolution(str(resolution.path), resolution)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _unknown_code_line(code: str, suggestions: list[str]) -> str:
|
|
916
|
+
"""The difflib shortlist when there is one, otherwise a pointer at the
|
|
917
|
+
catalogue that holds every real code.
|
|
918
|
+
|
|
919
|
+
Only the WITH-SUGGESTIONS branch is `explain.py`'s own miss sentence,
|
|
920
|
+
verbatim. The no-near-miss branch is tan's OWN wording, not a port: measured
|
|
921
|
+
against alp-sdk `origin/dev`, `explain.py` there prints ``unknown code
|
|
922
|
+
'ZZZ' -- run `alp explain` against a code from metadata/error-
|
|
923
|
+
catalog.json`` (its own binary name, its own subcommand spelling), which
|
|
924
|
+
this port cannot repeat verbatim -- there is no `alp explain` in a
|
|
925
|
+
`tan`-only install. See the module DIVERGENCE list above.
|
|
926
|
+
"""
|
|
927
|
+
if suggestions:
|
|
928
|
+
return f"explain: unknown code '{code}'; did you mean: " + ", ".join(suggestions) + "?"
|
|
929
|
+
return (
|
|
930
|
+
f"explain: unknown code '{code}' -- pass a code from the SDK's "
|
|
931
|
+
f"metadata/error-catalog.json."
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def resolve_code(code: str, sdk_root: Path) -> _Result:
|
|
936
|
+
"""The explanation for one `ALP_ERR_*` / `ALP-Bxxx` code out of `sdk_root`.
|
|
937
|
+
|
|
938
|
+
Raises `ExplainError` for an unreadable catalogue and for an unknown code.
|
|
939
|
+
The miss carries its shortlist in `data.suggestions` as well as in the text
|
|
940
|
+
line, so a consumer gets the recovery options structured instead of having
|
|
941
|
+
to re-parse a sentence.
|
|
942
|
+
"""
|
|
943
|
+
try:
|
|
944
|
+
codes = error_catalog.load_codes(sdk_root)
|
|
945
|
+
except error_catalog.CatalogUnreadable as err:
|
|
946
|
+
raise ExplainError(
|
|
947
|
+
"explain.catalog-unreadable",
|
|
948
|
+
f"{err.detail} ({err.path}).",
|
|
949
|
+
f"explain: {err.detail} ({err.path}).",
|
|
950
|
+
selector_value=code.strip(),
|
|
951
|
+
) from None
|
|
952
|
+
key, suggestions = error_catalog.lookup(codes, code)
|
|
953
|
+
if key is None:
|
|
954
|
+
raise ExplainError(
|
|
955
|
+
"explain.code-unknown",
|
|
956
|
+
f"Unknown diagnostic code '{code}'.",
|
|
957
|
+
_unknown_code_line(code, suggestions),
|
|
958
|
+
selector_value=code.strip(),
|
|
959
|
+
extra_data={"diagnostic": None, "suggestions": suggestions},
|
|
960
|
+
)
|
|
961
|
+
entry = codes[key]
|
|
962
|
+
return _Result(
|
|
963
|
+
kind="diagnostic-code",
|
|
964
|
+
value=key,
|
|
965
|
+
summary=error_catalog.summary_line(entry),
|
|
966
|
+
details=error_catalog.detail_lines(entry),
|
|
967
|
+
extra_data={"diagnostic": entry, "suggestions": []},
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
# ---------------------------------------------------------------------------
|
|
972
|
+
# Envelope assembly
|
|
973
|
+
# ---------------------------------------------------------------------------
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _available() -> dict:
|
|
977
|
+
"""The full catalogue of explainable ids. Emitted on EVERY path, success and
|
|
978
|
+
failure alike, so a caller that guessed an id wrong learns the valid ones
|
|
979
|
+
from the same envelope."""
|
|
980
|
+
return {
|
|
981
|
+
"projectTemplates": [t.id for t in PROJECT_TEMPLATES],
|
|
982
|
+
"moduleTemplates": [t.id for t in MODULE_TEMPLATES],
|
|
983
|
+
"generationTargets": [t.emit for t in GENERATION_TARGETS],
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _data(
|
|
988
|
+
*, kind: str, value: str, summary: str, details: list[str], extra: dict | None = None
|
|
989
|
+
) -> dict:
|
|
990
|
+
"""`extra` is MERGED LAST and is empty on every path but `--code`: the
|
|
991
|
+
`explain-overview` golden pins this key set exactly, so a key that appears
|
|
992
|
+
unconditionally is a wire change for three selectors that gained nothing."""
|
|
993
|
+
return {
|
|
994
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
995
|
+
"selector": {"kind": kind, "value": value},
|
|
996
|
+
"summary": summary,
|
|
997
|
+
"details": details,
|
|
998
|
+
"available": _available(),
|
|
999
|
+
**(extra or {}),
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _print_sdk_resolution_warnings(sdk: SdkInfo | None) -> None:
|
|
1004
|
+
"""tan-cli#959: `explain: warning: <message>` lines to stderr for every
|
|
1005
|
+
fact `sdk_resolution_issues` finds in `sdk`'s resolution -- the text-mode
|
|
1006
|
+
disclosure of the same `sdk.project-pin-unresolved` /
|
|
1007
|
+
`sdk.global-default-foreign-project` pair `Envelope`'s
|
|
1008
|
+
`_with_sdk_resolution_advisories` already appends to `issues[]` for free
|
|
1009
|
+
under `--format json` (it fires there because `_emit`'s json branch is
|
|
1010
|
+
the only branch that ever constructs an `Envelope`). This is the text
|
|
1011
|
+
branch's own copy of that disclosure, called both from the success path
|
|
1012
|
+
(`explain`, ahead of the summary line) and from `_fail` (ahead of
|
|
1013
|
+
`err.text_line`) -- the two SDK-bound refusal sites, `resolve_code`'s
|
|
1014
|
+
`explain.catalog-unreadable` and `explain.code-unknown`, each bind an
|
|
1015
|
+
`SdkInfo` via `bind_sdk` before raising, so `sdk` is populated there the
|
|
1016
|
+
same way it is on success.
|
|
1017
|
+
|
|
1018
|
+
`sdk` is `None` on every path that never resolved a checkout --
|
|
1019
|
+
`--template`/`--target` (`resolve`'s three raises), the two selector-
|
|
1020
|
+
clash refusals (`explain.positional-template-conflict` fires before
|
|
1021
|
+
`--code` is even cleaned; `explain.ambiguous-selector` fires only once
|
|
1022
|
+
`--code` IS set, but both `_fail` before `bind_sdk` is ever called, so
|
|
1023
|
+
neither attempts checkout resolution), and `bind_sdk`'s own
|
|
1024
|
+
`explain.sdk-root-unresolved` raise (it raises BEFORE building an
|
|
1025
|
+
`SdkInfo`, which is why tan-cli#950 had to carry that one pair through
|
|
1026
|
+
`ExplainError.extra_issues` instead -- the two mechanisms are disjoint by
|
|
1027
|
+
construction, never double-printing the same fact).
|
|
1028
|
+
"""
|
|
1029
|
+
if sdk is None:
|
|
1030
|
+
return
|
|
1031
|
+
from tan.core.sdk_discovery import sdk_resolution_issues
|
|
1032
|
+
|
|
1033
|
+
for issue in sdk_resolution_issues(
|
|
1034
|
+
sdk.broken_project_pin, sdk.source_tier, sdk.foreign_global_default_for
|
|
1035
|
+
):
|
|
1036
|
+
for line in wrap_lines([f"explain: warning: {issue.message}"], wrap_width()):
|
|
1037
|
+
print(line, file=sys.stderr)
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _emit(
|
|
1041
|
+
json_mode: bool,
|
|
1042
|
+
data: dict,
|
|
1043
|
+
issues: list[Issue],
|
|
1044
|
+
exit_code: ExitCode,
|
|
1045
|
+
sdk: SdkInfo | None = None,
|
|
1046
|
+
) -> None:
|
|
1047
|
+
"""Write the one envelope (JSON) and exit. `project` is always null/null --
|
|
1048
|
+
explain reads no board.yaml on ANY path, `--code` included: a diagnostic
|
|
1049
|
+
code is a property of the SDK, not of a project. `sdk` is populated only
|
|
1050
|
+
when `--code` resolved a checkout, which is also the only path that can
|
|
1051
|
+
carry the shared `sdk.*` resolution advisories `Envelope` appends."""
|
|
1052
|
+
if json_mode:
|
|
1053
|
+
emit(
|
|
1054
|
+
Envelope(
|
|
1055
|
+
"explain",
|
|
1056
|
+
Project(root=None, board_yaml=None),
|
|
1057
|
+
data,
|
|
1058
|
+
issues,
|
|
1059
|
+
exit_code,
|
|
1060
|
+
sdk=sdk,
|
|
1061
|
+
)
|
|
1062
|
+
)
|
|
1063
|
+
raise typer.Exit(int(exit_code))
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def explain(
|
|
1067
|
+
template_arg: str = typer.Argument(
|
|
1068
|
+
None,
|
|
1069
|
+
metavar="[TEMPLATE]",
|
|
1070
|
+
help="Project or module template id -- shorthand for --template.",
|
|
1071
|
+
),
|
|
1072
|
+
template: str = typer.Option(
|
|
1073
|
+
None,
|
|
1074
|
+
"--template",
|
|
1075
|
+
metavar="ID",
|
|
1076
|
+
help="Project template (init) or module template (scaffold) to explain.",
|
|
1077
|
+
),
|
|
1078
|
+
target: str = typer.Option(
|
|
1079
|
+
None,
|
|
1080
|
+
"--target",
|
|
1081
|
+
metavar="EMIT",
|
|
1082
|
+
help="Generation output target to explain (e.g. zephyr-conf, zephyr-board).",
|
|
1083
|
+
),
|
|
1084
|
+
code: str = typer.Option(
|
|
1085
|
+
None,
|
|
1086
|
+
"--code",
|
|
1087
|
+
metavar="CODE",
|
|
1088
|
+
help="alp-sdk diagnostic or error code to explain (e.g. ALP-B003, "
|
|
1089
|
+
"ALP_ERR_NO_BACKEND). Reads the bound SDK checkout.",
|
|
1090
|
+
),
|
|
1091
|
+
project: str = typer.Option( # read by --code ONLY; see below
|
|
1092
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
1093
|
+
),
|
|
1094
|
+
sdk_root: str = typer.Option( # read by --code ONLY; see below
|
|
1095
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
1096
|
+
),
|
|
1097
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
1098
|
+
) -> None:
|
|
1099
|
+
"""Explain a project/module template or a generation target.
|
|
1100
|
+
|
|
1101
|
+
`--project` is declared on EVERY path but consumed by `--code` ALONE
|
|
1102
|
+
(`bind_sdk`): `--template`/`--target` are project-agnostic (they read no
|
|
1103
|
+
board.yaml, and the oracle's `project` stays `null`/`null` regardless of
|
|
1104
|
+
either flag's value on those two paths), but clap makes both `global =
|
|
1105
|
+
true` in Rust, so `tan --sdk-root X explain` / `tan --project X explain`
|
|
1106
|
+
must not be parse errors -- verified against the oracle. The reported
|
|
1107
|
+
envelope `project` key STILL stays `null`/`null` even on `--code`
|
|
1108
|
+
(`_emit`'s own docstring): `--project` there only picks WHICH checkout the
|
|
1109
|
+
SDK ladder resolves, the same role it plays for `tan validate`/`tan
|
|
1110
|
+
build`, it is not itself echoed as a project root. Accepting the flag and
|
|
1111
|
+
then resolving from the bare CWD regardless of its value would be the
|
|
1112
|
+
accepted-but-ignored input class this command refuses everywhere else --
|
|
1113
|
+
which is exactly what an earlier version of `bind_sdk` did (tan-cli#627
|
|
1114
|
+
review). `alp-sdk-vscode/src/ideHub/newProjectFlowPanel.ts:188` invokes
|
|
1115
|
+
exactly the `--sdk-root` shape. `--sdk-root` IS consumed, but only by
|
|
1116
|
+
`--code` (`bind_sdk`): it is the terminal tier of the SDK ladder (I-31),
|
|
1117
|
+
and the alternative -- accepting the flag that names the checkout the
|
|
1118
|
+
catalogue lives in and then discovering a different one -- is the
|
|
1119
|
+
accepted-but-ignored input class this command refuses elsewhere.
|
|
1120
|
+
|
|
1121
|
+
`[TEMPLATE]` (the positional) is a PORT-ONLY convenience, not oracle
|
|
1122
|
+
behavior: the real `crates/tan-cli` `ExplainArgs` has no positional field,
|
|
1123
|
+
only `#[arg(long)] template`, so `tan explain minimal-app` errors there too
|
|
1124
|
+
("Got unexpected extra argument(s)") even though every other `tan`
|
|
1125
|
+
subcommand that names a single id (`inspect`, `generate --target`, ...)
|
|
1126
|
+
reads naturally with one. Accepted here ONLY when `--template` is not
|
|
1127
|
+
already given; giving BOTH is a coded error (`explain.positional-template-
|
|
1128
|
+
conflict`), not a silent `--template`-wins -- the accepted-but-ignored
|
|
1129
|
+
input class this port keeps re-introducing. Folded into `template` before
|
|
1130
|
+
`resolve()` so the rest of this function is unaware of it.
|
|
1131
|
+
"""
|
|
1132
|
+
json_mode = output_format == "json"
|
|
1133
|
+
|
|
1134
|
+
if template_arg is not None and template is not None:
|
|
1135
|
+
_fail(
|
|
1136
|
+
json_mode,
|
|
1137
|
+
ExplainError(
|
|
1138
|
+
"explain.positional-template-conflict",
|
|
1139
|
+
f"Use either the positional template id ('{template_arg}') or "
|
|
1140
|
+
f"--template ('{template}'), not both.",
|
|
1141
|
+
f"explain: use either the positional template id ('{template_arg}') "
|
|
1142
|
+
f"or --template ('{template}'), not both in the same command.",
|
|
1143
|
+
),
|
|
1144
|
+
)
|
|
1145
|
+
return
|
|
1146
|
+
|
|
1147
|
+
# Captured BEFORE the fold below overwrites `template`: whether it was the
|
|
1148
|
+
# positional that supplied it (`--template` itself was absent), needed by
|
|
1149
|
+
# the `--code` clash message just below so it names what the caller
|
|
1150
|
+
# actually typed, the same way `explain.positional-template-conflict`
|
|
1151
|
+
# above already does.
|
|
1152
|
+
template_via_positional = template is None and template_arg is not None
|
|
1153
|
+
|
|
1154
|
+
if template is None:
|
|
1155
|
+
template = template_arg
|
|
1156
|
+
|
|
1157
|
+
code = _clean(code)
|
|
1158
|
+
if code is not None and (_clean(template) is not None or _clean(target) is not None):
|
|
1159
|
+
# FIRST, ahead of `resolve()`'s own template-vs-target check, so
|
|
1160
|
+
# `--template X --target Y --code Z` reports THIS message: it is the
|
|
1161
|
+
# accurate one for three selectors, where the pair's ("use either
|
|
1162
|
+
# --template or --target") names only two of them. The pair keeps its
|
|
1163
|
+
# own golden-pinned message on every input that reaches it, which is
|
|
1164
|
+
# every input with no `--code`. Same issue code for both -- one name
|
|
1165
|
+
# for "you named more than one thing to explain".
|
|
1166
|
+
#
|
|
1167
|
+
# `template_selector_name` names the POSITIONAL when that is what set
|
|
1168
|
+
# `template` (`tan explain minimal-app --code ALP-B003`): the fixed
|
|
1169
|
+
# wording named `--template` unconditionally there, even though the
|
|
1170
|
+
# caller never typed that flag -- the same accepted-but-ignored-input
|
|
1171
|
+
# confusion `explain.positional-template-conflict` exists to avoid.
|
|
1172
|
+
# `--target` stays fixed either way; it has no positional form.
|
|
1173
|
+
template_selector_name = (
|
|
1174
|
+
"the positional template id" if template_via_positional else "--template"
|
|
1175
|
+
)
|
|
1176
|
+
_fail(
|
|
1177
|
+
json_mode,
|
|
1178
|
+
ExplainError(
|
|
1179
|
+
"explain.ambiguous-selector",
|
|
1180
|
+
f"Use --code on its own; it cannot be combined with "
|
|
1181
|
+
f"{template_selector_name} or --target.",
|
|
1182
|
+
f"explain: use --code on its own, not combined with "
|
|
1183
|
+
f"{template_selector_name} or --target.",
|
|
1184
|
+
),
|
|
1185
|
+
)
|
|
1186
|
+
return
|
|
1187
|
+
|
|
1188
|
+
sdk: SdkInfo | None = None
|
|
1189
|
+
try:
|
|
1190
|
+
if code is not None:
|
|
1191
|
+
# Two statements, not one: `bind_sdk` raising leaves `sdk` None
|
|
1192
|
+
# (the tuple never binds), which is exactly what the failure
|
|
1193
|
+
# envelope should report -- nothing was resolved. A `resolve_code`
|
|
1194
|
+
# failure below DOES carry the checkout it read, so the reader can
|
|
1195
|
+
# see WHICH catalogue answered "unknown".
|
|
1196
|
+
sdk_root_path, sdk = bind_sdk(sdk_root, project, code)
|
|
1197
|
+
result = resolve_code(code, sdk_root_path)
|
|
1198
|
+
else:
|
|
1199
|
+
result = resolve(template, target)
|
|
1200
|
+
except ExplainError as err:
|
|
1201
|
+
_fail(json_mode, err, sdk)
|
|
1202
|
+
return
|
|
1203
|
+
except TemplateDataError as err:
|
|
1204
|
+
# A broken tan installation (the vendored tree did not ship), not a
|
|
1205
|
+
# project problem -- so INTERNAL_FAILURE, matching `init`'s
|
|
1206
|
+
# `init.template-unreadable`. The Rust `expect()`s the same read and
|
|
1207
|
+
# panics, which under `--format json` leaves stdout EMPTY and the
|
|
1208
|
+
# extension rendering nothing; a coded envelope is the port's contract.
|
|
1209
|
+
_fail(
|
|
1210
|
+
json_mode,
|
|
1211
|
+
ExplainError(
|
|
1212
|
+
"explain.template-unreadable",
|
|
1213
|
+
str(err),
|
|
1214
|
+
f"explain: {err}",
|
|
1215
|
+
exit_code=ExitCode.INTERNAL_FAILURE,
|
|
1216
|
+
selector_value=(template or "").strip(),
|
|
1217
|
+
),
|
|
1218
|
+
)
|
|
1219
|
+
return
|
|
1220
|
+
except Exception as err: # noqa: BLE001 -- the backstop; see the module docstring
|
|
1221
|
+
# `typer.Exit` cannot reach here: it is raised only from `_emit`, which
|
|
1222
|
+
# runs outside this try.
|
|
1223
|
+
_fail(
|
|
1224
|
+
json_mode,
|
|
1225
|
+
ExplainError(
|
|
1226
|
+
"explain.internal-failure",
|
|
1227
|
+
f"explain failed unexpectedly: {err.__class__.__name__}: {err}",
|
|
1228
|
+
f"explain: failed unexpectedly: {err.__class__.__name__}: {err}",
|
|
1229
|
+
exit_code=ExitCode.INTERNAL_FAILURE,
|
|
1230
|
+
selector_value=(template or target or code or "").strip(),
|
|
1231
|
+
),
|
|
1232
|
+
sdk,
|
|
1233
|
+
)
|
|
1234
|
+
return
|
|
1235
|
+
|
|
1236
|
+
if not json_mode:
|
|
1237
|
+
# stderr, in both formats: stdout is the envelope channel and nothing
|
|
1238
|
+
# else. Matches Rust's `emit()`, which `eprintln!`s every text line.
|
|
1239
|
+
# tan-cli#959: the SDK-resolution advisories (`sdk.project-pin-
|
|
1240
|
+
# unresolved` / `sdk.global-default-foreign-project`) print FIRST,
|
|
1241
|
+
# matching `_fail`'s own order and `bootstrap_cmd._refusal`'s
|
|
1242
|
+
# `warning_lines + outcome.text` -- a caveat about which checkout
|
|
1243
|
+
# answered belongs ahead of the answer itself, not after it.
|
|
1244
|
+
# The summary header is a short, bounded "<label> (<id>)" -- never
|
|
1245
|
+
# observed over any real width, so it is never run through
|
|
1246
|
+
# `wrap_block`; `_print_detail_lines` is where the actual wrap
|
|
1247
|
+
# decision lives.
|
|
1248
|
+
_print_sdk_resolution_warnings(sdk)
|
|
1249
|
+
print(f"explain: {result.summary}", file=sys.stderr)
|
|
1250
|
+
_print_detail_lines(result.details, wrap_width())
|
|
1251
|
+
_emit(
|
|
1252
|
+
json_mode,
|
|
1253
|
+
_data(
|
|
1254
|
+
kind=result.kind,
|
|
1255
|
+
value=result.value,
|
|
1256
|
+
summary=result.summary,
|
|
1257
|
+
details=result.details,
|
|
1258
|
+
extra=result.extra_data,
|
|
1259
|
+
),
|
|
1260
|
+
[],
|
|
1261
|
+
ExitCode.SUCCESS,
|
|
1262
|
+
sdk,
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def _fail(json_mode: bool, err: ExplainError, sdk: SdkInfo | None = None) -> None:
|
|
1267
|
+
"""The error envelope. Note the asymmetry with the success path, which is
|
|
1268
|
+
contract: `selector.kind` reverts to `overview` and summary/details go
|
|
1269
|
+
EMPTY even though the caller named a selector, because nothing was
|
|
1270
|
+
explained. `data.available` stays populated."""
|
|
1271
|
+
if not json_mode:
|
|
1272
|
+
# tan-cli#950: `err.extra_issues` (today only `bind_sdk`'s broken-pin
|
|
1273
|
+
# / foreign-global-default pair) printed AHEAD of the refusal line --
|
|
1274
|
+
# `bootstrap_cmd._refusal`'s `warning_lines + outcome.text` order --
|
|
1275
|
+
# since this text branch is the only path those facts reach; the JSON
|
|
1276
|
+
# branch below carries them in `issues[]` instead. Without this,
|
|
1277
|
+
# `--format json` would disclose the pin (tan-cli#677's asymmetry).
|
|
1278
|
+
for issue in err.extra_issues:
|
|
1279
|
+
for line in wrap_lines([f"explain: warning: {issue.message}"], wrap_width()):
|
|
1280
|
+
print(line, file=sys.stderr)
|
|
1281
|
+
# tan-cli#959: the other half of the same disclosure -- `sdk` is
|
|
1282
|
+
# populated (not `err.extra_issues`) on the two SDK-BOUND refusals,
|
|
1283
|
+
# `explain.catalog-unreadable` / `explain.code-unknown`, since both
|
|
1284
|
+
# raise from `resolve_code` AFTER `bind_sdk` already built an
|
|
1285
|
+
# `SdkInfo`. Disjoint from the loop above by construction (see
|
|
1286
|
+
# `_print_sdk_resolution_warnings`'s docstring), so this never
|
|
1287
|
+
# double-prints.
|
|
1288
|
+
_print_sdk_resolution_warnings(sdk)
|
|
1289
|
+
# A refusal sentence, not a record -- `--template`/`--target` echo
|
|
1290
|
+
# the CALLER's own (unbounded-length) input back into it (e.g.
|
|
1291
|
+
# "unknown template '<whatever was typed>'"), so this is the one
|
|
1292
|
+
# `explain` text line genuinely able to run long on a hostile or
|
|
1293
|
+
# just-long argument, not only in the two catalogue commands this
|
|
1294
|
+
# task is scoped to.
|
|
1295
|
+
for line in wrap_lines([err.text_line], wrap_width()):
|
|
1296
|
+
print(line, file=sys.stderr)
|
|
1297
|
+
_emit(
|
|
1298
|
+
json_mode,
|
|
1299
|
+
_data(
|
|
1300
|
+
kind="overview",
|
|
1301
|
+
value=err.selector_value,
|
|
1302
|
+
summary="",
|
|
1303
|
+
details=[],
|
|
1304
|
+
extra=err.extra_data,
|
|
1305
|
+
),
|
|
1306
|
+
[*err.extra_issues, Issue(err.code, "error", err.message)],
|
|
1307
|
+
err.exit_code,
|
|
1308
|
+
sdk,
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
# tan-cli#261: adds the seven oracle `GlobalArgs` flags this command was
|
|
1313
|
+
# still missing (`--all`/`--board-yaml`/`--ci`/`--no-color`/
|
|
1314
|
+
# `--non-interactive`/`--quiet`/`--verbose`) on top of `--target`, already
|
|
1315
|
+
# declared and read above; see `tan.core.global_flags`. `--project`/
|
|
1316
|
+
# `--sdk-root` are ALSO declared already (read by `--code` alone -- see
|
|
1317
|
+
# `explain`'s own docstring); the decorator leaves both untouched the same
|
|
1318
|
+
# way it leaves `--target` untouched.
|
|
1319
|
+
explain = accept_global_flags(explain)
|