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,2009 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan validate` -- is this `board.yaml` structurally sound?
|
|
3
|
+
|
|
4
|
+
Two paths, mirroring `crates/tan-cli/src/commands/validate.rs`:
|
|
5
|
+
|
|
6
|
+
* ``--offline`` runs only the structural checks that ship inside tan. No SDK
|
|
7
|
+
checkout, no subprocess, no network. This is the path the two committed
|
|
8
|
+
conformance fixtures exercise, which is why their ``data.commandLine`` is
|
|
9
|
+
``""``.
|
|
10
|
+
* without ``--offline`` the real validator is a PORT of the SDK's own
|
|
11
|
+
``scripts/validate_board_yaml.py``, **run in-process since tan-cli#270**
|
|
12
|
+
(``tan.core.board_validator_run``; ``tests/parity/test_board_validator_parity.py``
|
|
13
|
+
holds it byte-for-byte to the script over the SDK's examples and an invalid-
|
|
14
|
+
board corpus, and ``HAND_PORT_HASHES`` pins the four SDK sources it was cut
|
|
15
|
+
from). tan still does not vendor alp-sdk's schema: the SDK owns
|
|
16
|
+
``metadata/schemas/`` and ADR-0017's doctrine is to consume what exists, so
|
|
17
|
+
the port reads ``metadata/**`` out of the resolved checkout. The port returns
|
|
18
|
+
what the script's process would have (exit status + stderr), and that goes
|
|
19
|
+
through the SAME ``analyze_validator_output`` below -- the status map, issue
|
|
20
|
+
codes and envelope are unchanged.
|
|
21
|
+
|
|
22
|
+
**Version skew (the one behavioural difference).** The port applies the
|
|
23
|
+
validator rules tan was audited against (``PORTED_FROM_SDK_COMMIT``) to
|
|
24
|
+
WHATEVER checkout is bound; the spawned script always matched the bound one.
|
|
25
|
+
A newer SDK's extra checks are not applied (tan warns with
|
|
26
|
+
``validate.sdk-validator-newer`` when the bound checkout's validator sources
|
|
27
|
+
hash differently from the pinned ones), and a crash reading such a
|
|
28
|
+
checkout is reported as ``validate.failed`` saying tan's validator could not
|
|
29
|
+
read this SDK -- not as a board defect. ``TAN_VALIDATE_ENGINE=subprocess``
|
|
30
|
+
runs the bound SDK's own validator instead.
|
|
31
|
+
|
|
32
|
+
**Why the spawn path below still exists:** that env var pins it
|
|
33
|
+
deliberately (the parity test's reference side, the skew escape hatch),
|
|
34
|
+
``tan diff`` shares its helpers, and ``validate.spawn-failed`` /
|
|
35
|
+
``validate.python-too-old`` are registered codes it alone can emit -- the
|
|
36
|
+
registry's retired-spelling gate matches a bare suffix across ``tan/``, so
|
|
37
|
+
they cannot be retired while ``diff.spawn-failed`` / ``diff.python-too-old`` /
|
|
38
|
+
``bootstrap.python-too-old`` share it. On the default engine no interpreter is
|
|
39
|
+
probed and ``data.commandLine`` is ``""``. **PORTED (tan-cli#376)** -- see
|
|
40
|
+
"the spawn path" below. Until #376 this branch refused with
|
|
41
|
+
``validate.spawn-not-implemented`` at exit 2, which made the DEFAULT
|
|
42
|
+
invocation the root quickstart documents (``tan validate``) incapable of
|
|
43
|
+
validating anything, and -- because exit 2 is also the genuine
|
|
44
|
+
validation-failure class -- indistinguishable from a bad board to any
|
|
45
|
+
caller that does not read the issue code.
|
|
46
|
+
|
|
47
|
+
**tan-cli#262 (decided for 0.5.0, TAKEN): a missing verdict is the
|
|
48
|
+
VALIDATOR's problem, not a tan crash -- exit 2, not exit 1.** An earlier
|
|
49
|
+
revision of this docstring left that an open question ("the genuine
|
|
50
|
+
release decision, tracked in tan-cli#262"); the maintainer has now
|
|
51
|
+
decided it. Measured
|
|
52
|
+
directly, running ``target/debug/tan.exe`` (tan 0.4.1-dev) with
|
|
53
|
+
``--format json``:
|
|
54
|
+
|
|
55
|
+
- empty directory -> exit **2**, ``validate.board-yaml-missing``
|
|
56
|
+
- ``board.yaml`` present, no SDK root -> exit **2**,
|
|
57
|
+
``validate.sdk-root-unresolved``
|
|
58
|
+
|
|
59
|
+
Exit 1 is reachable in the oracle only AFTER a validator actually spawns and
|
|
60
|
+
returns an exit status the resolver cannot map to a named outcome
|
|
61
|
+
(``Outcome::Failed``, see the table below) -- the oracle's own
|
|
62
|
+
``validation_outcome_exit_code`` (`crates/tan-cli/src/commands/validate.rs:
|
|
63
|
+
54-62`) sends that ONE case to ``RuntimeFailure`` (1) while every other
|
|
64
|
+
non-clean outcome already goes to ``ValidationFailure`` (2). alp-sdk-vscode
|
|
65
|
+
renders exit 2 as severity "warning" and exit 1 as "error", so that one
|
|
66
|
+
oracle case alone painted a genuinely-failing project red in the IDE --
|
|
67
|
+
indistinguishable from `tan` itself crashing.
|
|
68
|
+
|
|
69
|
+
This port deliberately does NOT mirror that one oracle case. "The validator
|
|
70
|
+
could not produce a verdict" is still the validator's verdict, in every
|
|
71
|
+
shape it takes here -- GENERALLY, and now that #376 has landed the spawn
|
|
72
|
+
path, that generality is what carries the decision: every reachable
|
|
73
|
+
``OUTCOME_FAILED`` (an exit status outside the 0-5 range the resolver
|
|
74
|
+
names, a validator that crashed with a traceback, a validator that ran past
|
|
75
|
+
[`VALIDATOR_TIMEOUT_S`]) emits ``ExitCode.VALIDATION_FAILURE`` (2), not
|
|
76
|
+
``RuntimeFailure`` (1) -- do not "fix" that back to oracle parity; that
|
|
77
|
+
parity is the bug #262 fixes.
|
|
78
|
+
A genuine `tan`-side crash is unaffected by this decision and keeps its own
|
|
79
|
+
exit code: an unexpected exception that escaped tan's own code stays
|
|
80
|
+
``ExitCode.INTERNAL_FAILURE`` (5). **tan-cli#498 defect 3 removed the other
|
|
81
|
+
case that used to sit here** -- a ``board.yaml`` tan could not READ (a
|
|
82
|
+
directory, a cp1252 file, a permission error) is now
|
|
83
|
+
``validate.board-yaml-unreadable`` at exit **2**, because it is the user's
|
|
84
|
+
file to fix, not a tan crash, and because the identical file on the spawn
|
|
85
|
+
path already answered exit 2 (measured, both paths). See the guard itself
|
|
86
|
+
for the full argument; the spawn-LAUNCH I/O error specifically
|
|
87
|
+
(the subprocess could not even be started -- no interpreter on PATH, the
|
|
88
|
+
script unreadable) is ``validate.spawn-failed`` at
|
|
89
|
+
``ExitCode.RUNTIME_FAILURE`` (1) -- the "generic runtime failure (e.g. I/O
|
|
90
|
+
or subprocess error)" `crates/tan-cli/src/exit.rs` itself names that code
|
|
91
|
+
for, and the ONE case #262's own text carved out for it. Only the
|
|
92
|
+
validator's-verdict exit code moves; tan's own crash exit codes do not.
|
|
93
|
+
A timeout is deliberately on the other side of that line: the child DID
|
|
94
|
+
start, so it is a verdict that never arrived (2), not a launch failure (1).
|
|
95
|
+
|
|
96
|
+
Exit 5 is wrong on every one of these paths regardless: reporting a
|
|
97
|
+
validator that could not answer as ``InternalFailure`` would tell CI/the
|
|
98
|
+
extension this is a tan crash, the same mistake the Rust comment on the
|
|
99
|
+
offline path (below) was written to stop repeating.
|
|
100
|
+
|
|
101
|
+
Re-measure before changing any of this. Do not infer the oracle's behaviour
|
|
102
|
+
from a report, a docstring, or ``crates/`` -- run the binary.
|
|
103
|
+
|
|
104
|
+
**The validator-exit-status -> outcome map**, measured on the oracle
|
|
105
|
+
(``target/debug/tan.exe``) with a contrived SDK (``scripts/alp_project.py``
|
|
106
|
+
marker + ``scripts/validate_board_yaml.py`` exiting a chosen code),
|
|
107
|
+
``board.yaml`` present, ``--sdk-root`` pointed at it, no ``--offline``:
|
|
108
|
+
|
|
109
|
+
=============== =============================== ===
|
|
110
|
+
validator exit outcome rc
|
|
111
|
+
=============== =============================== ===
|
|
112
|
+
0 clean 0
|
|
113
|
+
1 schema-violation 2
|
|
114
|
+
2 missing-preset 2
|
|
115
|
+
3 hardware-revision 2
|
|
116
|
+
5, 77 failed 1
|
|
117
|
+
=============== =============================== ===
|
|
118
|
+
|
|
119
|
+
So ``validate.failed`` is specifically the "anything outside the range the
|
|
120
|
+
resolver maps by number" case -- NOT every non-clean status, and in
|
|
121
|
+
particular not 2, 3, 4 or 5, which have their own named outcomes. A reader
|
|
122
|
+
must not infer "any nonzero -> failed" from this docstring. The ``rc``
|
|
123
|
+
column above is the ORACLE's, measured, and stays 1 for the ``failed`` row
|
|
124
|
+
-- that is a fact about ``target/debug/tan.exe``, not a decision, and must
|
|
125
|
+
not be edited to "fix" the table. This port's OWN rc for that row is 2, per
|
|
126
|
+
tan-cli#262 above -- a divergence recorded in prose here rather than in the
|
|
127
|
+
table because the table is a record of what was measured, not of this
|
|
128
|
+
port's choices.
|
|
129
|
+
|
|
130
|
+
**tan-cli#1262: this port's range is 0-5, so the table above is the
|
|
131
|
+
oracle's history, not this command's map.** The oracle was measured against
|
|
132
|
+
a contrived validator; the REAL one returns 4 and 5 as well, and those get
|
|
133
|
+
their own outcomes here rather than the oracle's ``failed`` -- see
|
|
134
|
+
[`_STATUS_OUTCOME`], which carries the measurement and the argument.
|
|
135
|
+
|
|
136
|
+
**The raw status is now carried too** (also #1262):
|
|
137
|
+
``data.validatorExitStatus`` reports ``out.returncode`` verbatim on the
|
|
138
|
+
spawn path, so the ``.get(..., OUTCOME_FAILED)`` fallback stops being
|
|
139
|
+
lossy -- a future validator exit this table does not yet name still arrives
|
|
140
|
+
as a NUMBER a consumer can act on, not just as ``failed``. The key is
|
|
141
|
+
OMITTED, not ``null``, wherever no validator returned a status: the whole
|
|
142
|
+
``--offline`` path, all three guards, a spawn-LAUNCH failure, and a timeout
|
|
143
|
+
(the child was killed, so ``TimeoutExpired`` carries no returncode).
|
|
144
|
+
|
|
145
|
+
``validate.spawn-not-implemented`` is GONE as of #376 (the branch that
|
|
146
|
+
emitted it is), and its ``contract/issue-codes.json`` entry deleted rather
|
|
147
|
+
than marked ``retired``: it was ``reserved``/``consumer: none``, which the
|
|
148
|
+
registry's own header defines as the state where "renaming or dropping it
|
|
149
|
+
costs nothing on the wire", and it never existed in the Rust CLI at all, so
|
|
150
|
+
no pinned v0.4.1 binary can have taught a consumer to expect it.
|
|
151
|
+
|
|
152
|
+
**The spawn path, as ported.** Three guards, then the subprocess, then the
|
|
153
|
+
child's stderr mapped into the envelope:
|
|
154
|
+
|
|
155
|
+
* ``board.yaml`` must exist -- ``validate.board-yaml-missing``, checked
|
|
156
|
+
FIRST and on both paths (see the guard's own comment).
|
|
157
|
+
* the alp-sdk checkout must resolve -- ``validate.sdk-root-unresolved``,
|
|
158
|
+
through [`resolve_sdk_root_ladder`], the NARROW ladder the oracle resolves
|
|
159
|
+
``validate`` with (that helper's own docstring names `validate` in the
|
|
160
|
+
thirteen). This is why ``--sdk-root`` is now READ rather than merely
|
|
161
|
+
accepted: an earlier revision of the option's help said "declared, not
|
|
162
|
+
consumed ... an arbitrary ``--sdk-root`` value changes nothing in the
|
|
163
|
+
envelope", which was true only because the oracle re-validates the flag
|
|
164
|
+
against the loader marker and an ARBITRARY path resolves to nothing.
|
|
165
|
+
``resolve_sdk_root_ladder`` does NOT re-validate it -- it returns an
|
|
166
|
+
explicit flag verbatim (I-31, terminal-for-REPORTING) -- so this command
|
|
167
|
+
applies tan-cli#257/#258's own guard at the call site, exactly as
|
|
168
|
+
`build_cmd` and `run_cmd` do. Without it a ``--sdk-root`` pointing at an
|
|
169
|
+
empty directory reached the SPAWN, whose child died with ``python.exe:
|
|
170
|
+
can't open file '...\\scripts\\validate_board_yaml.py'``, exit 2 -- which
|
|
171
|
+
this command's status map reads as ``missing-preset``. MEASURED before the
|
|
172
|
+
fix: tan answered ``validate.missing-preset`` and named the customer's
|
|
173
|
+
BOARD for the operator's bad flag; the oracle answers
|
|
174
|
+
``validate.sdk-root-unresolved`` on that same input, with no ``sdk`` block.
|
|
175
|
+
* the interpreter must be new enough to run the SDK scripts --
|
|
176
|
+
``validate.python-too-old``, the oracle's own guard 3
|
|
177
|
+
(`crates/tan-cli/src/commands/validate.rs:124-129`, message body at
|
|
178
|
+
`crates/tan-cli/src/util.rs:192-202`). The scripts use
|
|
179
|
+
``@dataclass(slots=True)``, so an older interpreter dies with a cryptic
|
|
180
|
+
``dataclass() got an unexpected keyword argument 'slots'`` from inside
|
|
181
|
+
alp-sdk instead of an actionable refusal. TWO deliberate divergences, both
|
|
182
|
+
the port's established shape for this same guard elsewhere: the floor is
|
|
183
|
+
the resolved SDK's OWN declared ``metadata/bootstrap.json``
|
|
184
|
+
``pythonMinVersion`` (`doctor_cmd.resolve_manifest_python_floor`, I-24)
|
|
185
|
+
rather than the oracle's compiled-in ``MIN_PYTHON = (3, 10)`` that can
|
|
186
|
+
drift from it, and the interpreter probed is [`_planner_python`]'s -- the
|
|
187
|
+
one that will actually be spawned two lines later -- not the bare PATH
|
|
188
|
+
``python``/``python3`` the oracle probes and then also spawns. Both are
|
|
189
|
+
`generate_cmd`/`model_cmd`'s decisions, and this reuses
|
|
190
|
+
`generate_cmd._python_too_old` rather than becoming a fourth copy of it.
|
|
191
|
+
|
|
192
|
+
The argv is the oracle's, verbatim --
|
|
193
|
+
``<python> <sdk>/scripts/validate_board_yaml.py --input <board>`` -- and
|
|
194
|
+
``data.commandLine`` reports exactly that string (the offline path, and every
|
|
195
|
+
one of the three guards, keep reporting ``""``, which the two committed
|
|
196
|
+
conformance fixtures pin).
|
|
197
|
+
|
|
198
|
+
**The ``sdk`` block.** ``sdk: {root, sourceTier}`` is emitted on the spawn
|
|
199
|
+
path whenever a checkout actually resolved, and OMITTED (absent, never null)
|
|
200
|
+
otherwise. Measured across every reachable shape: present with
|
|
201
|
+
``"sourceTier":"sdkRootFlag"`` for a valid ``--sdk-root`` and with
|
|
202
|
+
``"discovery"`` for a sibling checkout -- including on the
|
|
203
|
+
``board-yaml-missing`` refusal, where the SDK resolved and the board did not
|
|
204
|
+
-- and absent for ``--offline`` (with or without a resolvable checkout, and
|
|
205
|
+
with or without ``--sdk-root``), for the unresolved-SDK guard, and for a
|
|
206
|
+
``--sdk-root`` that failed the marker check. So the rule is not "the spawn
|
|
207
|
+
path reports an SDK" but "the envelope reports whatever SDK this run
|
|
208
|
+
resolved", and ``--offline`` resolves none by construction -- it has no
|
|
209
|
+
subprocess to point anywhere.
|
|
210
|
+
|
|
211
|
+
The interpreter is [`_planner_python_resolution`] (wrapped by
|
|
212
|
+
[`_planner_python`]), NOT the bare ``python3``/``python`` the oracle
|
|
213
|
+
hardcodes: the child imports ``jsonschema``/``PyYAML``/``colorama``, which
|
|
214
|
+
live in the workspace venv `tan bootstrap` creates, and a bare PATH
|
|
215
|
+
interpreter without them exits 1 with a traceback -- the exact collision
|
|
216
|
+
`is_interpreter_crash` exists to disambiguate. Preferring the venv does not
|
|
217
|
+
remove that guard (the venv can still be broken), it just stops the common
|
|
218
|
+
case from taking it.
|
|
219
|
+
|
|
220
|
+
**tan-cli#652: a bare-interpreter crash names the real remedy.** On the
|
|
221
|
+
released, frozen `tan`, `sys.executable` is `tan` itself (unusable -- see
|
|
222
|
+
`_planner_python`'s own docstring), so before a successful `tan bootstrap`
|
|
223
|
+
has created a workspace venv there is nothing for `_planner_python` to
|
|
224
|
+
prefer and it falls back to the SYSTEM `python3`/`python`, which almost
|
|
225
|
+
never has `jsonschema` installed. That reproduces `is_interpreter_crash`'s
|
|
226
|
+
collision on every out-of-order invocation of a released binary, and used to
|
|
227
|
+
surface as a raw `ModuleNotFoundError: No module named 'jsonschema'` quoted
|
|
228
|
+
inside "Validation ended with outcome 'failed'." -- true, but naming a
|
|
229
|
+
Python module instead of the actual, fixable condition. A pip/from-source
|
|
230
|
+
install never reproduces this: `jsonschema` is one of `tan`'s OWN declared
|
|
231
|
+
dependencies (see `pyproject.toml`'s comment on it), so it lands on the
|
|
232
|
+
SAME interpreter a source install's `python3` already is. `_synthesised_
|
|
233
|
+
finding`'s `used_workspace_venv` flag is what tells the two apart: `False`
|
|
234
|
+
only when [`_planner_python_resolution`] itself found no workspace venv to
|
|
235
|
+
prefer, so a venv that resolved but is independently broken (a corrupted
|
|
236
|
+
`tan bootstrap`, not a missing one) still gets the generic message rather
|
|
237
|
+
than being told to re-run a bootstrap that already ran.
|
|
238
|
+
|
|
239
|
+
Two hardenings the oracle does not have, both load-bearing:
|
|
240
|
+
|
|
241
|
+
* **A timeout.** ``Command::output()`` has none, so an SDK validator that
|
|
242
|
+
wedges wedges `tan` -- and under ``--format json`` a consumer then gets no
|
|
243
|
+
envelope at all, which is indistinguishable from a slow project. Bounded by
|
|
244
|
+
[`VALIDATOR_TIMEOUT_S`]; the timeout is a ``failed`` outcome (exit 2), see
|
|
245
|
+
the #262 paragraph above.
|
|
246
|
+
* **ANSI stripping** (`_ANSI_RE`) -- defence, NOT the load-bearing repair an
|
|
247
|
+
earlier revision of this docstring claimed. That revision stated as
|
|
248
|
+
MEASURED fact that the child writes ``\x1b[31merror[ALP-B005]\x1b[0m: ...``
|
|
249
|
+
to stderr and that "the oracle therefore drops every rich diagnostic it
|
|
250
|
+
spawned the validator to collect". Re-measured, per this module's own rule,
|
|
251
|
+
and the two halves came apart:
|
|
252
|
+
|
|
253
|
+
- The PARSER half is true. Driven through the oracle with a stand-in
|
|
254
|
+
validator writing one ANSI-wrapped and one plain ``error[ALP-B*]`` header
|
|
255
|
+
on the same run, only the plain one reached ``issues[]``:
|
|
256
|
+
``parse_rich_header`` anchors on the line START, so an escape in front of
|
|
257
|
+
``error`` costs the whole block.
|
|
258
|
+
- The PREMISE is false. ``validate_board_yaml.py:43`` does render with
|
|
259
|
+
``color=not args.no_color`` and ``alp_cli.diagnostic._use_color(True)``
|
|
260
|
+
does return True unconditionally -- but ``alp_cli.diagnostic`` calls
|
|
261
|
+
``colorama.init()`` at import, which replaces ``sys.stderr`` with an
|
|
262
|
+
``AnsiToWin32`` wrapper that STRIPS SGR escapes when the stream is not a
|
|
263
|
+
tty. Every spawn -- the oracle's and this one -- gives the child a pipe.
|
|
264
|
+
Captured bytes from a real ``render(..., color=True)`` through one:
|
|
265
|
+
``b'error[ALP-B005]: msg\r\n --> board.yaml:1:1\r\n |\r\n...'``, no
|
|
266
|
+
``\x1b`` anywhere. The colorama-ABSENT branch paints through a ``_Stub``
|
|
267
|
+
whose ``__getattr__`` returns ``""``, so it emits no escapes either.
|
|
268
|
+
There is no third branch, so the oracle does not in fact drop the real
|
|
269
|
+
SDK's rich diagnostics.
|
|
270
|
+
|
|
271
|
+
Kept anyway, at one regex per line: it covers exactly the conditions this
|
|
272
|
+
reasoning rests on ceasing to hold -- a renderer that stops going through
|
|
273
|
+
colorama, a ``FORCE_COLOR``-style override, a caller that hands the child a
|
|
274
|
+
pty. Still NOT "fixed" by passing ``--no-color``, for the unchanged reason:
|
|
275
|
+
an older SDK without that flag would argparse-error to exit 2, which this
|
|
276
|
+
command's own status map reads as ``missing-preset`` -- a wrong verdict is
|
|
277
|
+
worse than an escape sequence.
|
|
278
|
+
|
|
279
|
+
``Severity::Suggestion`` (the oracle's severity for a standalone ``hint:``
|
|
280
|
+
line) is emitted here as ``note``. The oracle has no ``--format
|
|
281
|
+
diagnostic-v1``/``sarif``; this port does, and both vocabularies are
|
|
282
|
+
``error|warning|note`` (``diagnostic-v1.schema.json``'s severity enum,
|
|
283
|
+
`_SARIF_LEVEL`). A literal ``"suggestion"`` would emit a document that fails
|
|
284
|
+
alp-sdk's own schema and raise ``KeyError`` on the SARIF path.
|
|
285
|
+
|
|
286
|
+
``tan_core::validate::is_summary_line`` is deliberately NOT ported: in the
|
|
287
|
+
oracle its branch (``i += 1; continue``) is byte-identical to the loop's own
|
|
288
|
+
fall-through (``i += 1``), so it classifies nothing -- an unmatched line is
|
|
289
|
+
skipped either way. Porting it would be porting a no-op.
|
|
290
|
+
|
|
291
|
+
**A wrong-shaped board.yaml is the USER's problem, not a tan crash.** The Rust
|
|
292
|
+
carries a comment earned the hard way: routing a malformed file through
|
|
293
|
+
``InternalFailure`` (exit 5) "told CI/the extension this was a tan crash" and
|
|
294
|
+
disagreed with the spawn path, which reports the identical file as exit 2
|
|
295
|
+
``schema-violation``. So a file that parses as YAML but does not fit the model
|
|
296
|
+
is a *validation* failure -- exit 2 -- and the one place in this port where
|
|
297
|
+
``ValidationFailure`` is genuinely the right code. (Contrast `tan build`, where a
|
|
298
|
+
malformed *plan* is exit 1: the consumer renders 2 as a warning and 1 as an
|
|
299
|
+
error, and an unbuildable plan is not a warning.)
|
|
300
|
+
|
|
301
|
+
The structural checks are deliberately about the CONTRACT's shape -- is there a
|
|
302
|
+
top-level ``os:``, is there a ``cores:`` block -- never about hardware. No SKU
|
|
303
|
+
list, no addresses, no pin names live here: the OS is derived from each core's
|
|
304
|
+
Cortex class by the SDK planner and is never selectable, so tan has no business
|
|
305
|
+
knowing which SKUs exist.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
from __future__ import annotations
|
|
309
|
+
|
|
310
|
+
import json
|
|
311
|
+
import os
|
|
312
|
+
import re
|
|
313
|
+
import subprocess
|
|
314
|
+
from dataclasses import dataclass
|
|
315
|
+
from pathlib import Path
|
|
316
|
+
from typing import Any
|
|
317
|
+
|
|
318
|
+
import typer
|
|
319
|
+
|
|
320
|
+
from tan.commands.doctor_cmd import resolve_manifest_python_floor
|
|
321
|
+
|
|
322
|
+
# `_python_too_old` is IMPORTED, not re-spelled: `generate_cmd` and `model_cmd`
|
|
323
|
+
# already carry this same guard for this same reason (a spawned SDK script that
|
|
324
|
+
# needs `@dataclass(slots=True)`), and the whole point of guard 3 is that a
|
|
325
|
+
# floor and a message duplicated per command drift apart per command.
|
|
326
|
+
# `model_cmd`'s copy is that drift already begun -- its message dropped the
|
|
327
|
+
# oracle's own "(VS Code users can instead set alpSdk.pythonPath)" sentence. A
|
|
328
|
+
# third copy here would make it the pattern.
|
|
329
|
+
from tan.commands.generate_cmd import _python_too_old
|
|
330
|
+
from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
|
|
331
|
+
from tan.core.sdk_discovery import with_sdk_search
|
|
332
|
+
# tan-cli#1031: this resolver MOVED to `tan.core.board_context` so `tan
|
|
333
|
+
# scaffold` could reuse it instead of growing a fourth project/board
|
|
334
|
+
# resolver. Aliased to the private name this module's call site (and
|
|
335
|
+
# `test_validate_command.py`'s prose) already spells; an alias is not a
|
|
336
|
+
# second definition, which `tests/gates/test_shared_helpers_have_one_
|
|
337
|
+
# definition.py` documents at length.
|
|
338
|
+
from tan.core.board_context import resolve_board_path as _resolve_board_path
|
|
339
|
+
from tan.core.global_flags import accept_global_flags
|
|
340
|
+
from tan.core.sdk_discovery import (
|
|
341
|
+
_planner_python_resolution,
|
|
342
|
+
resolve_sdk_root_ladder,
|
|
343
|
+
sdk_resolution_issues,
|
|
344
|
+
)
|
|
345
|
+
from tan.core.shapes import is_sdk_root, rejected_sdk_root_message
|
|
346
|
+
from tan.core.subprocess_env import spawn_env
|
|
347
|
+
from tan.core.uri_reference import (
|
|
348
|
+
cwd_base_uri_or_none,
|
|
349
|
+
is_absolute_path_reference,
|
|
350
|
+
path_to_uri_reference,
|
|
351
|
+
)
|
|
352
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
353
|
+
from tan.exit_codes import ExitCode
|
|
354
|
+
from tan.output_format import FORMAT_HELP, ValidateOutputFormat
|
|
355
|
+
from tan.version import TAN_VERSION
|
|
356
|
+
|
|
357
|
+
#: `data.schemaVersion` for this command's payload -- the envelope payload's own
|
|
358
|
+
#: version, unrelated to `board.yaml`'s `schemaVersion:`.
|
|
359
|
+
DATA_SCHEMA_VERSION = "1"
|
|
360
|
+
|
|
361
|
+
#: Seconds the SDK validator may run before it is killed. Generous for the same
|
|
362
|
+
#: reason `generate_cmd.EMIT_TIMEOUT_S` is (a cold run imports the whole
|
|
363
|
+
#: orchestrator package and reads every metadata file for the SKU), and bounded
|
|
364
|
+
#: for the reason the oracle's unbounded `Command::output()` is not: a wedged
|
|
365
|
+
#: child under `--format json` produces no envelope at all, which a consumer
|
|
366
|
+
#: cannot tell from a slow project. Read through the module at call time, so a
|
|
367
|
+
#: test can shorten it.
|
|
368
|
+
VALIDATOR_TIMEOUT_S = 300
|
|
369
|
+
|
|
370
|
+
#: tan-cli#270: `tan validate` runs the SDK's board.yaml validator IN-PROCESS
|
|
371
|
+
#: (`tan.core.board_validator_run`). Setting this to `subprocess` pins the
|
|
372
|
+
#: earlier engine -- spawning `<sdk>/scripts/validate_board_yaml.py` -- as a
|
|
373
|
+
#: deliberate choice, like `TAN_GENERATE_EXECUTOR=subprocess`. It is the
|
|
374
|
+
#: reference side of the engine-parity test, and what `tan diff`'s cross-check
|
|
375
|
+
#: still shares (`VALIDATOR_SCRIPT`).
|
|
376
|
+
VALIDATE_ENGINE_ENV = "TAN_VALIDATE_ENGINE"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
#: tan-cli#270: the alp-sdk commit the in-process validator was audited against
|
|
380
|
+
#: (the freshness gate asserts it equals `HAND_PORT_PINNED_SDK_COMMIT`).
|
|
381
|
+
PORTED_FROM_SDK_COMMIT = "84132b98a53a9f0e5ef9e7622a38c6d637439873"
|
|
382
|
+
|
|
383
|
+
#: Leading words of the finding `run_in_process_engine` writes for a crashed
|
|
384
|
+
#: port (kept for display/tests; `_Result.crashed` is the structured signal).
|
|
385
|
+
VALIDATOR_CRASH_PREFIX = "tan's built-in validator could not read this SDK"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _bound_sdk_version(sdk_root: Path) -> str:
|
|
389
|
+
"""`metadata/sdk_version.yaml`'s version, for the skew message."""
|
|
390
|
+
from tan.commands.sdk_cmd import parse_sdk_version_yaml
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
text = (Path(sdk_root) / "metadata" / "sdk_version.yaml").read_text(encoding="utf-8")
|
|
394
|
+
except (OSError, UnicodeDecodeError):
|
|
395
|
+
return "(version unknown)"
|
|
396
|
+
return "v" + (parse_sdk_version_yaml(text) or "(unknown)")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _subprocess_engine_requested() -> bool:
|
|
400
|
+
return os.environ.get(VALIDATE_ENGINE_ENV, "").strip().lower() == "subprocess"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def subprocess_engine_requested() -> bool:
|
|
404
|
+
"""Public name for the engine choice, shared with `tan diff`."""
|
|
405
|
+
return _subprocess_engine_requested()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _sdk_direction(sdk_root: Path) -> str | None:
|
|
409
|
+
"""`"newer"` / `"older"` when git can order the bound checkout against the
|
|
410
|
+
commit the port was audited at; None when it cannot (not a git checkout,
|
|
411
|
+
commit unknown, git missing, or HEAD is exactly the audited commit with
|
|
412
|
+
locally modified sources). A hash mismatch alone has no direction."""
|
|
413
|
+
import subprocess
|
|
414
|
+
|
|
415
|
+
from tan.core.doctor_git import _resolve_git_executable
|
|
416
|
+
from tan.core.subprocess_env import spawn_env
|
|
417
|
+
|
|
418
|
+
git = _resolve_git_executable()
|
|
419
|
+
if git is None:
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
def _git(*args: str) -> subprocess.CompletedProcess[str] | None:
|
|
423
|
+
try:
|
|
424
|
+
return subprocess.run( # noqa: S603
|
|
425
|
+
[git, "-C", str(sdk_root), *args],
|
|
426
|
+
capture_output=True, text=True, timeout=10, check=False, env=spawn_env(),
|
|
427
|
+
)
|
|
428
|
+
except (OSError, subprocess.SubprocessError):
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
def _is_ancestor(ancestor: str, descendant: str) -> bool | None:
|
|
432
|
+
run = _git("merge-base", "--is-ancestor", ancestor, descendant)
|
|
433
|
+
return None if run is None else {0: True, 1: False}.get(run.returncode)
|
|
434
|
+
|
|
435
|
+
head = _git("rev-parse", "HEAD")
|
|
436
|
+
if head is None or head.returncode != 0 or head.stdout.strip() == PORTED_FROM_SDK_COMMIT:
|
|
437
|
+
return None
|
|
438
|
+
if _is_ancestor(PORTED_FROM_SDK_COMMIT, "HEAD"):
|
|
439
|
+
return "newer"
|
|
440
|
+
if _is_ancestor("HEAD", PORTED_FROM_SDK_COMMIT):
|
|
441
|
+
return "older"
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def skew_warning(sdk_root: Path) -> str | None:
|
|
446
|
+
"""Message for `validate.sdk-validator-newer`, or None when the bound
|
|
447
|
+
checkout's validator sources are the ones the port was audited against."""
|
|
448
|
+
from tan.core.board_validator_skew import skewed_sources
|
|
449
|
+
|
|
450
|
+
skewed = skewed_sources(sdk_root)
|
|
451
|
+
if not skewed:
|
|
452
|
+
return None
|
|
453
|
+
version = _bound_sdk_version(sdk_root)
|
|
454
|
+
ported = PORTED_FROM_SDK_COMMIT[:8]
|
|
455
|
+
direction = _sdk_direction(sdk_root)
|
|
456
|
+
fallback = f"run with {VALIDATE_ENGINE_ENV}=subprocess to use the SDK's own validator."
|
|
457
|
+
if direction == "older":
|
|
458
|
+
return (
|
|
459
|
+
f"the bound alp-sdk ({version}) has validator sources older than the ones "
|
|
460
|
+
f"tan's built-in validator was ported from ({ported}): {', '.join(skewed)}. "
|
|
461
|
+
"The built-in validator may apply rules this SDK does not, so it can reject "
|
|
462
|
+
f"a board this SDK accepts -- {fallback}"
|
|
463
|
+
)
|
|
464
|
+
if direction == "newer":
|
|
465
|
+
return (
|
|
466
|
+
f"the bound alp-sdk ({version}) has validator sources "
|
|
467
|
+
f"newer than the ones tan's built-in validator was ported from "
|
|
468
|
+
f"({ported}): {', '.join(skewed)}. Rules added since are "
|
|
469
|
+
"not applied, so a clean verdict here may not match the SDK's own validator "
|
|
470
|
+
f"-- {fallback}"
|
|
471
|
+
)
|
|
472
|
+
return (
|
|
473
|
+
f"the bound alp-sdk ({version}) has validator sources that differ from the ones "
|
|
474
|
+
f"tan's built-in validator was ported from ({ported}): {', '.join(skewed)}. "
|
|
475
|
+
"It may be newer (rules not applied here) or older (rules this SDK lacks), so "
|
|
476
|
+
f"the verdict may not match the SDK's own validator -- {fallback}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def run_in_process_engine(board_path: str, sdk_root: Path) -> tuple[int, str, _Result]:
|
|
481
|
+
"""The default engine: `(status, stderr, result)`, crash-as-skew handling
|
|
482
|
+
included. Shared by `tan validate` and `tan diff` so both give one verdict."""
|
|
483
|
+
from tan.core.board_validator_run import run_board_validator
|
|
484
|
+
|
|
485
|
+
run = run_board_validator(board_path, sdk_root)
|
|
486
|
+
result = analyze_validator_output(run.status, run.stderr)
|
|
487
|
+
if _is_interpreter_crash(run.stderr):
|
|
488
|
+
# See the "Version skew" paragraph of the module docstring: a crash on a
|
|
489
|
+
# checkout that has moved past the audited commit is tan's gap, not a
|
|
490
|
+
# defect in the customer's board.
|
|
491
|
+
last = run.stderr.strip().splitlines()[-1]
|
|
492
|
+
result = _Result(
|
|
493
|
+
OUTCOME_FAILED,
|
|
494
|
+
(
|
|
495
|
+
_Finding(
|
|
496
|
+
"error",
|
|
497
|
+
f"{VALIDATOR_CRASH_PREFIX} "
|
|
498
|
+
f"(bound alp-sdk {_bound_sdk_version(sdk_root)}; the "
|
|
499
|
+
f"validator was ported from {PORTED_FROM_SDK_COMMIT[:8]}). "
|
|
500
|
+
"This is not a verdict on board.yaml -- retry with "
|
|
501
|
+
f"{VALIDATE_ENGINE_ENV}=subprocess to run the SDK's own "
|
|
502
|
+
f"validator. Underlying error: {last}",
|
|
503
|
+
),
|
|
504
|
+
),
|
|
505
|
+
crashed=True,
|
|
506
|
+
)
|
|
507
|
+
if result.outcome != OUTCOME_CLEAN and not result.findings:
|
|
508
|
+
result = _Result(
|
|
509
|
+
result.outcome,
|
|
510
|
+
(_synthesised_finding(result.outcome, run.stderr, used_workspace_venv=True),),
|
|
511
|
+
)
|
|
512
|
+
return run.status, run.stderr, result
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
#: `<sdk>/scripts/validate_board_yaml.py` -- the script the oracle spawns
|
|
516
|
+
#: (`crates/tan-cli/src/commands/validate.rs::run_spawn`). NOT
|
|
517
|
+
#: `python -m alp_cli.main validate`: that needs `<sdk>/scripts` on PYTHONPATH,
|
|
518
|
+
#: while a script path puts its own directory on `sys.path[0]` for free.
|
|
519
|
+
VALIDATOR_SCRIPT = ("scripts", "validate_board_yaml.py")
|
|
520
|
+
|
|
521
|
+
# The `--format` values this command accepts are declared ONCE, as
|
|
522
|
+
# `tan.output_format.ValidateOutputFormat` (tan-cli#403) -- not as a literal
|
|
523
|
+
# tuple here plus a hand-written membership check in [`validate`]'s body,
|
|
524
|
+
# which is how `--help`, `cli.py`'s root callback and all three
|
|
525
|
+
# shell-completion scripts came to name only `text` and `json` while this
|
|
526
|
+
# command really accepted four: nothing connected them to the list, so the two
|
|
527
|
+
# IDE-oriented formats stayed reachable but undiscoverable.
|
|
528
|
+
#
|
|
529
|
+
# `diagnostic-v1` and `sarif` are NOT part of the shared
|
|
530
|
+
# `Format::Text`/`Format::Json` enum every other command mirrors from
|
|
531
|
+
# `crates/tan-cli/src/cli.rs` -- there is no Rust precedent for them yet. They
|
|
532
|
+
# exist so alp-sdk's quality gate (`scripts/check_diagnostic_schema.py`) can
|
|
533
|
+
# point at `tan` instead of spawning
|
|
534
|
+
# `python -m alp_cli.main validate --format json`.
|
|
535
|
+
#
|
|
536
|
+
# WARNING -- `--format json` does NOT mean the same thing in the two CLIs.
|
|
537
|
+
# In alp-sdk (`scripts/alp_cli/validate.py:20,34`), `--format json` IS the
|
|
538
|
+
# diagnostic-v1 document. In tan, `--format json` is the envelope
|
|
539
|
+
# (`{command,ok,exitCode,project,data,issues}`; see `_emit` below) and the
|
|
540
|
+
# diagnostic-v1 document moved to its own `--format diagnostic-v1`. The
|
|
541
|
+
# obvious find-and-replace swap -- pointing the gate's argv at `tan` while
|
|
542
|
+
# keeping `"--format", "json"` -- validates the envelope against
|
|
543
|
+
# `diagnostic-v1.schema.json` (whose root is `additionalProperties: false`)
|
|
544
|
+
# and fails with something like "Additional properties are not allowed
|
|
545
|
+
# ('command', 'ok', 'exitCode', ...)", which names nothing about the real
|
|
546
|
+
# cause. The gate-side edit `check_diagnostic_schema.py` needs is therefore
|
|
547
|
+
# NOT a repoint of the same argv -- it must spawn
|
|
548
|
+
# `tan validate --offline --format diagnostic-v1`, not
|
|
549
|
+
# `tan validate --offline --format json`. `--format sarif` keeps its name
|
|
550
|
+
# unchanged in both CLIs, so only the `json` case silently diverges.
|
|
551
|
+
|
|
552
|
+
#: `diagnostic-v1.schema.json`'s own `schemaVersion` -- an integer `const: 1`,
|
|
553
|
+
#: unrelated to `DATA_SCHEMA_VERSION` above (a different document, a different
|
|
554
|
+
#: field, a different type).
|
|
555
|
+
_DIAGNOSTIC_SCHEMA_VERSION = 1
|
|
556
|
+
|
|
557
|
+
_SARIF_SCHEMA_URI = (
|
|
558
|
+
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/"
|
|
559
|
+
"sarif-schema-2.1.0.json"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
#: The SARIF `originalUriBaseIds` key `_sarif_document` declares for its
|
|
563
|
+
#: `sarif_base` parameter (tan-cli#1117, computed by
|
|
564
|
+
#: `tan.core.uri_reference.cwd_base_uri_or_none`). SARIF 2.1.0 SS3.4.4
|
|
565
|
+
#: (EXAMPLE 2, `"uriBaseId": "%srcroot%"`) notes a `uriBaseId` "can be any
|
|
566
|
+
#: string" and does not require the `%` wrapping, but its own example uses
|
|
567
|
+
#: it and this repo invents no other convention to match, so the same
|
|
568
|
+
#: spelling is followed.
|
|
569
|
+
_SARIF_URI_BASE_ID = "%CWD%"
|
|
570
|
+
|
|
571
|
+
#: Outcome strings. The first five are verbatim from
|
|
572
|
+
#: `tan_core::validate::Outcome::as_str`; the two `hardware-revision-*`
|
|
573
|
+
#: siblings are this port's own (tan-cli#1262 -- the frozen oracle has no
|
|
574
|
+
#: `Outcome` variant for either). The issue code is `validate.<outcome>`, so
|
|
575
|
+
#: these strings are wire contract.
|
|
576
|
+
OUTCOME_CLEAN = "clean"
|
|
577
|
+
OUTCOME_SCHEMA_VIOLATION = "schema-violation"
|
|
578
|
+
OUTCOME_MISSING_PRESET = "missing-preset"
|
|
579
|
+
OUTCOME_HARDWARE_REVISION = "hardware-revision"
|
|
580
|
+
#: tan-cli#1262. Named after the upstream constants they carry
|
|
581
|
+
#: (`EXIT_SDK_REVISION_UNKNOWN` = 4, `EXIT_SDK_REVISION_NOT_BUILDABLE` = 5)
|
|
582
|
+
#: rather than invented here, because the whole reason each has its OWN exit
|
|
583
|
+
#: code upstream is that a caller can act on it mechanically: alp-sdk's own
|
|
584
|
+
#: comments give the remedies as "pick a revision that exists" (4) and "pick a
|
|
585
|
+
#: revision whose status is buildable" (5), neither of which is exit 3's "pin a
|
|
586
|
+
#: different SDK, or change hw_rev".
|
|
587
|
+
OUTCOME_HARDWARE_REVISION_UNKNOWN = "hardware-revision-unknown"
|
|
588
|
+
OUTCOME_HARDWARE_REVISION_NOT_BUILDABLE = "hardware-revision-not-buildable"
|
|
589
|
+
OUTCOME_FAILED = "failed"
|
|
590
|
+
|
|
591
|
+
#: Validator exit status -> outcome. Rows 0-3 are verbatim from
|
|
592
|
+
#: `tan_core::validate::classify_validation_outcome`; rows 4 and 5 are this
|
|
593
|
+
#: port's own (tan-cli#1262). Anything else -- and a `None` status (killed,
|
|
594
|
+
#: never started) -- is `OUTCOME_FAILED`.
|
|
595
|
+
#:
|
|
596
|
+
#: **Reachability, MEASURED against alp-sdk v0.16.0 and `dev` at
|
|
597
|
+
#: `cfeafd148cb16d24a0e6c2feb7749769fec8f992`** (both identical on this point;
|
|
598
|
+
#: re-measure rather than trusting this line, and re-pin it when you do):
|
|
599
|
+
#: `scripts/validate_board_yaml.py` returns 0, 1, 3, 4 and 5 -- 3, 4 and 5
|
|
600
|
+
#: being `EXIT_SDK_REVISION_UNSUPPORTED`, `EXIT_SDK_REVISION_UNKNOWN` and
|
|
601
|
+
#: `EXIT_SDK_REVISION_NOT_BUILDABLE`, all three returned from its `main()`.
|
|
602
|
+
#: It never returns 2.
|
|
603
|
+
#:
|
|
604
|
+
#: **Exit 2's row is nonetheless LIVE, and the distinction is the whole
|
|
605
|
+
#: point of this paragraph**: these keys are the CHILD PROCESS's exit status,
|
|
606
|
+
#: not the script's return value. `main()` never returns 2, but the process
|
|
607
|
+
#: still exits 2 when the interpreter cannot open the script (MEASURED -- see
|
|
608
|
+
#: this module's docstring at the tan-cli#257/#258 guard, `python.exe: can't
|
|
609
|
+
#: open file '...\\scripts\\validate_board_yaml.py'`, exit 2, read here as
|
|
610
|
+
#: `missing-preset`) or when argparse rejects a flag (measured too, in the
|
|
611
|
+
#: `--no-color` paragraph of that same docstring). The #257/#258 guard below
|
|
612
|
+
#: closes only the `sdkRootFlag` tier; a stub checkout resolved through
|
|
613
|
+
#: `discovery` or a project pin, or a present-but-unreadable script, still
|
|
614
|
+
#: reaches this row. Do not re-derive it as dead: an earlier revision of this
|
|
615
|
+
#: very comment did, contradicting both docstring measurements above it.
|
|
616
|
+
#: Until #1262 rows 4 and 5 were absent, so both fell through to
|
|
617
|
+
#: `OUTCOME_FAILED` -- a code published as "produced no usable verdict ... a
|
|
618
|
+
#: crash" -- which made a real hw_rev refusal indistinguishable from `tan`
|
|
619
|
+
#: falling over and left alp-studio's pre-build gate no choice but to degrade.
|
|
620
|
+
_STATUS_OUTCOME = {
|
|
621
|
+
0: OUTCOME_CLEAN,
|
|
622
|
+
1: OUTCOME_SCHEMA_VIOLATION,
|
|
623
|
+
2: OUTCOME_MISSING_PRESET,
|
|
624
|
+
3: OUTCOME_HARDWARE_REVISION,
|
|
625
|
+
4: OUTCOME_HARDWARE_REVISION_UNKNOWN,
|
|
626
|
+
5: OUTCOME_HARDWARE_REVISION_NOT_BUILDABLE,
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
#: `Issue.severity` -> SARIF `level`, mirroring
|
|
630
|
+
#: `scripts/alp_cli/diagnostic_format.py:_SARIF_LEVEL`. A deliberate explicit
|
|
631
|
+
#: table, not a passthrough: an `Issue.severity` outside this set must raise
|
|
632
|
+
#: (KeyError) rather than silently emit a SARIF log whose `level` is not one
|
|
633
|
+
#: of the spec's three values. The spawn path now feeds it `warning` and
|
|
634
|
+
#: `note` as well as `error` -- see the module docstring on why the oracle's
|
|
635
|
+
#: fourth severity, `suggestion`, is emitted as `note` here.
|
|
636
|
+
_SARIF_LEVEL = {"error": "error", "warning": "warning", "note": "note"}
|
|
637
|
+
|
|
638
|
+
#: SGR escapes only -- everything `alp_cli.diagnostic.render` can emit through
|
|
639
|
+
#: colorama's `Fore`/`Style`. See the module docstring: the oracle's own spawn
|
|
640
|
+
#: leaves these in and then fails to parse its own child's output.
|
|
641
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
642
|
+
|
|
643
|
+
#: `^(error|warning|note)\[(ALP-[A-Z]\d+)\]:\s*(.+)` -- the header of a rich
|
|
644
|
+
#: diagnostic block (`tan_core::validate::parse_rich_header`). `(\S.*)` rather
|
|
645
|
+
#: than `(.+)` so a header whose message is whitespace-only does not match,
|
|
646
|
+
#: matching the Rust `if !message.is_empty()` after its `trim_start`.
|
|
647
|
+
#:
|
|
648
|
+
#: tan-cli#498 defect 2: the `ALP-[A-Z][0-9]+` run is now a CAPTURING group.
|
|
649
|
+
#: The oracle matches the same text and throws the code away
|
|
650
|
+
#: (`crates/tan-core/src/validate.rs:375-382` keeps severity + message only),
|
|
651
|
+
#: so every rich diagnostic reached a consumer as `validate-schema-violation`
|
|
652
|
+
#: with no way back to `docs/diagnostics/ALP-B005.md`. Diagnostic codes are on
|
|
653
|
+
#: this repo's verbatim-fidelity list; dropping one is not a formatting choice.
|
|
654
|
+
_RICH_HEADER_RE = re.compile(r"^(error|warning|note)\[(ALP-[A-Z][0-9]+)\]:\s*(\S.*)$")
|
|
655
|
+
|
|
656
|
+
#: `^\s+-->\s+\S+:(\d+):(\d+)` -- the location arrow. The trailing
|
|
657
|
+
#: `(?:\s|$)` reproduces `is_arrow_line`'s `rsplitn(3, ':')`: `a:1:2:xyz` is
|
|
658
|
+
#: NOT an arrow there (its last field is not numeric), and without the anchor
|
|
659
|
+
#: this regex would match its `a:1:2` prefix and disagree.
|
|
660
|
+
#:
|
|
661
|
+
#: The two number runs are CAPTURING since tan-cli#498 defect 2 -- the same
|
|
662
|
+
#: `line:col` the SDK renders and the oracle throws away. What is matched is
|
|
663
|
+
#: unchanged: adding a group changes no line's arrow-or-not verdict.
|
|
664
|
+
_ARROW_RE = re.compile(r"^\s+-->\s+\S+:([0-9]+):([0-9]+)(?:\s|$)")
|
|
665
|
+
|
|
666
|
+
#: `^\s+[|0-9^= ]` -- an indented source/underline/hint continuation line.
|
|
667
|
+
_BLOCK_CONTINUATION_RE = re.compile(r"^\s[\s|^=0-9]")
|
|
668
|
+
|
|
669
|
+
#: The caret underline inside a rich block (` | ^^^^^^^^^^`). The
|
|
670
|
+
#: whole line must be bar-then-carets, so a `^` that happens to appear in the
|
|
671
|
+
#: quoted SOURCE line above it cannot be mistaken for the underline. Its
|
|
672
|
+
#: length is `Diagnostic.span`, which is what turns an LSP range into a real
|
|
673
|
+
#: span instead of a zero-width point (tan-cli#498 defect 2).
|
|
674
|
+
_CARET_RE = re.compile(r"^\s*\|\s*(\^+)\s*$")
|
|
675
|
+
|
|
676
|
+
#: ` = hint: <text>` inside a rich block -- `Diagnostic.hint`, the field that
|
|
677
|
+
#: carries "did you mean 'E1M-AEN801'?". NOT reachable by `_HINT_RE` below,
|
|
678
|
+
#: which anchors on `hint:` at the start of a line and can never match the
|
|
679
|
+
#: `= ` prefix; the oracle consumes these lines as block padding and discards
|
|
680
|
+
#: them (tan-cli#498 defect 2).
|
|
681
|
+
_BLOCK_HINT_RE = re.compile(r"^\s*=\s*hint:\s*(\S.*?)\s*$")
|
|
682
|
+
|
|
683
|
+
#: ` = see: docs/diagnostics/ALP-B005.md` inside a rich block --
|
|
684
|
+
#: `Diagnostic.doc_url`, reported as `documentationUri`. Taken from the
|
|
685
|
+
#: validator's own output rather than rebuilt from the code: alp-sdk's
|
|
686
|
+
#: `_doc_url` honours `ALP_DIAG_BASE_URL`, so synthesising the path here would
|
|
687
|
+
#: contradict the child whenever that variable is set.
|
|
688
|
+
_BLOCK_SEE_RE = re.compile(r"^\s*=\s*see:\s*(\S.*?)\s*$")
|
|
689
|
+
|
|
690
|
+
#: `^(FAIL|WARN)\s+(.+)` -- the legacy line shape `validate_board_yaml.py`
|
|
691
|
+
#: still emits for its consistency failures (`FAIL consistency: ...`).
|
|
692
|
+
_FAIL_WARN_RE = re.compile(r"^(FAIL|WARN)\s+(\S.*)$")
|
|
693
|
+
|
|
694
|
+
#: `^\s{2,}\S` -- an indented continuation of a FAIL/WARN line.
|
|
695
|
+
_FAIL_CONTINUATION_RE = re.compile(r"^\s{2,}\S")
|
|
696
|
+
|
|
697
|
+
#: `^\s*(hint|suggestion|suggest):` (case-insensitive).
|
|
698
|
+
_HINT_RE = re.compile(r"^\s*(?:hint|suggestion|suggest):", re.IGNORECASE)
|
|
699
|
+
|
|
700
|
+
#: The header of an unhandled interpreter/environment crash. The validator
|
|
701
|
+
#: exits 1 both for a genuine schema violation AND when it dies importing
|
|
702
|
+
#: `jsonschema`, so this is the only thing that tells a broken validator
|
|
703
|
+
#: environment from a real "board.yaml is invalid" verdict (issue #38).
|
|
704
|
+
_TRACEBACK_HEADER = "Traceback (most recent call last):"
|
|
705
|
+
|
|
706
|
+
#: `ModuleNotFoundError: No module named 'jsonschema'` -- the exact line a
|
|
707
|
+
#: spawned SDK script dies on when it runs under the bare system interpreter
|
|
708
|
+
#: `_planner_python` falls back to before `tan bootstrap` has ever created a
|
|
709
|
+
#: workspace venv (tan-cli#652). Quotes may be single or double depending on
|
|
710
|
+
#: the interpreter/repr; the module name is matched generically (not pinned
|
|
711
|
+
#: to `jsonschema`) because the same collision reproduces for any of the
|
|
712
|
+
#: SDK's other declared imports (`yaml`, `colorama`, ...).
|
|
713
|
+
_MISSING_MODULE_RE = re.compile(r"ModuleNotFoundError: No module named ['\"]([\w.]+)['\"]")
|
|
714
|
+
|
|
715
|
+
#: Severity of a rich block, by its own header keyword. `note` is the oracle's
|
|
716
|
+
#: `Severity::Suggestion` arm.
|
|
717
|
+
_HEADER_SEVERITY = {"error": "error", "warning": "warning", "note": "note"}
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@dataclass(frozen=True)
|
|
721
|
+
class _Finding:
|
|
722
|
+
"""One thing the validator said, with everything it said ABOUT it.
|
|
723
|
+
|
|
724
|
+
tan-cli#498 defect 2. This used to be a bare `(severity, message)` tuple,
|
|
725
|
+
which is all the frozen oracle keeps -- so `error[ALP-B005]: ...` reached
|
|
726
|
+
the user as the message alone: no code to look up
|
|
727
|
+
`docs/diagnostics/ALP-B005.md` with, no `did you mean 'E1M-AEN801'?`, and
|
|
728
|
+
a zeroed `--format diagnostic-v1` range even though the SDK had supplied
|
|
729
|
+
`2:8`. The envelope's `issues[]` still carries `validate.<outcome>` as its
|
|
730
|
+
CODE -- that is wire contract, unchanged -- but the ALP code, hint and
|
|
731
|
+
documentation URI are no longer thrown away between the child's stderr and
|
|
732
|
+
the document a machine consumer reads.
|
|
733
|
+
|
|
734
|
+
`line`/`col`/`span` are the validator's own 1-BASED numbers (the same
|
|
735
|
+
convention `alp_cli.diagnostic` renders `--> path:line:col` from), and 0
|
|
736
|
+
means "not stated". Both exporters convert from this single 1-based
|
|
737
|
+
source, in opposite directions: `diagnostic-v1` is zero-based (LSP) and
|
|
738
|
+
SARIF is one-based (spec). Do not pre-convert either here.
|
|
739
|
+
"""
|
|
740
|
+
|
|
741
|
+
severity: str
|
|
742
|
+
message: str
|
|
743
|
+
#: `ALP-B005` and friends, or `None` for a finding tan itself produced
|
|
744
|
+
#: (every offline structural check) or a legacy `FAIL`/`WARN` line.
|
|
745
|
+
alp_code: str | None = None
|
|
746
|
+
hint: str | None = None
|
|
747
|
+
#: The `= see:` target verbatim, e.g. `docs/diagnostics/ALP-B005.md`.
|
|
748
|
+
doc_uri: str | None = None
|
|
749
|
+
line: int = 0
|
|
750
|
+
col: int = 0
|
|
751
|
+
span: int = 0
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@dataclass(frozen=True)
|
|
755
|
+
class _Result:
|
|
756
|
+
outcome: str
|
|
757
|
+
#: One `_Finding` per thing to report, in the order they must reach
|
|
758
|
+
#: `issues[]`. The offline checks only ever produce `error`; the spawn
|
|
759
|
+
#: path carries the validator's own severities (see `_HEADER_SEVERITY`
|
|
760
|
+
#: and `_severity_for_outcome`).
|
|
761
|
+
findings: tuple[_Finding, ...]
|
|
762
|
+
#: True when tan's in-process validator crashed, so board.yaml was never
|
|
763
|
+
#: judged (tan-cli#1523); `tan diff` branches on this, not on message text.
|
|
764
|
+
crashed: bool = False
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class BoardShapeError(Exception):
|
|
768
|
+
"""`board.yaml` parsed as YAML but does not fit the board model."""
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _load_yaml(text: str) -> Any:
|
|
772
|
+
"""Parse YAML using PyYAML when present, else a minimal top-level reader.
|
|
773
|
+
|
|
774
|
+
PyYAML is a declared base dependency, so the ImportError branch below is
|
|
775
|
+
only a stale-venv fallback, not the normal offline path. It degrades to
|
|
776
|
+
reading only what the structural checks actually consult: which top-level keys exist and whether each is a scalar
|
|
777
|
+
or a block. That is enough to distinguish `som: <scalar>` from
|
|
778
|
+
`som:` + an indented mapping, which is exactly what the checks below ask.
|
|
779
|
+
"""
|
|
780
|
+
try:
|
|
781
|
+
import yaml # noqa: PLC0415 (stale-venv fallback below)
|
|
782
|
+
except ImportError:
|
|
783
|
+
return _top_level_shape(text)
|
|
784
|
+
try:
|
|
785
|
+
return yaml.safe_load(text)
|
|
786
|
+
except Exception as err: # yaml.YAMLError and anything a loader raises
|
|
787
|
+
raise BoardShapeError(f"could not be parsed as YAML: {err}") from err
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _top_level_shape(text: str) -> dict[str, Any]:
|
|
791
|
+
"""Map each unindented `key:` to a scalar string or a nested-block marker.
|
|
792
|
+
|
|
793
|
+
Deliberately not a YAML parser. It answers one question -- is this key a
|
|
794
|
+
scalar or does it open a block -- because that is all the structural checks
|
|
795
|
+
need when PyYAML is absent.
|
|
796
|
+
"""
|
|
797
|
+
shape: dict[str, Any] = {}
|
|
798
|
+
pending: str | None = None
|
|
799
|
+
for raw in text.splitlines():
|
|
800
|
+
if not raw.strip() or raw.lstrip().startswith("#"):
|
|
801
|
+
continue
|
|
802
|
+
if raw[:1].isspace() or raw.lstrip().startswith("-"):
|
|
803
|
+
if pending is not None:
|
|
804
|
+
shape[pending] = {} # the key opened a block
|
|
805
|
+
pending = None
|
|
806
|
+
continue
|
|
807
|
+
pending = None
|
|
808
|
+
key, sep, rest = raw.partition(":")
|
|
809
|
+
if not sep:
|
|
810
|
+
continue
|
|
811
|
+
name = key.strip()
|
|
812
|
+
value = rest.strip()
|
|
813
|
+
if value:
|
|
814
|
+
shape[name] = value
|
|
815
|
+
else:
|
|
816
|
+
shape[name] = None
|
|
817
|
+
pending = name
|
|
818
|
+
return shape
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
#: `doc.get(key, _MISSING)` -- distinguishes an ABSENT key from one written
|
|
822
|
+
#: with no value (`cores:` on a line by itself, which PyYAML reads as `None`).
|
|
823
|
+
#: The two are different defects and get different messages.
|
|
824
|
+
_MISSING = object()
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def validate_board_text(text: str) -> _Result:
|
|
828
|
+
"""The offline structural validator. Pure: text in, outcome out.
|
|
829
|
+
|
|
830
|
+
**tan-cli#498 defect 1: the two structural checks below are UNCONDITIONAL.**
|
|
831
|
+
They used to sit behind `_effective_schema_version(doc) >= 2`, which no
|
|
832
|
+
conforming alp-sdk project can satisfy -- `scripts/alp_migrate` pins
|
|
833
|
+
`LATEST = 1` with an empty registry, `board.schema.json` calls an absent
|
|
834
|
+
`schemaVersion` "version 1 permanently", and 0 of the 100 `board.yaml`
|
|
835
|
+
files under `alp-sdk/examples` declare it (measured, alp-sdk `99e47476`).
|
|
836
|
+
Neither check ever ran, so `--offline` answered `clean`, exit 0, on boards
|
|
837
|
+
the SDK rejects -- on the very path `validate.sdk-root-unresolved`'s own
|
|
838
|
+
message recommends to a user with no checkout.
|
|
839
|
+
|
|
840
|
+
The gate was also WRONG about the SDK: nothing in `board.schema.json` is
|
|
841
|
+
conditioned on `schemaVersion`. Its ROOT carries `"required": ["som",
|
|
842
|
+
"cores"]` and `"not": {"required": ["os"]}` outright, so the v1-shaped
|
|
843
|
+
board the gate was protecting is the board the real validator refuses --
|
|
844
|
+
measured on the `contract/envelopes/validate-offline-clean` fixture's own
|
|
845
|
+
board.yaml (`som:` + `preset:`, no `cores:`), which
|
|
846
|
+
`scripts/validate_board_yaml.py` answers with `error[ALP-B001]: required
|
|
847
|
+
key 'cores' is missing`, exit 1.
|
|
848
|
+
|
|
849
|
+
**A deliberate divergence from the frozen oracle**, which carries the
|
|
850
|
+
identical dead gate (`crates/tan-core/src/validate.rs:282`), in the family
|
|
851
|
+
of tan-cli#262's. It costs that one conformance case, DECLARED in
|
|
852
|
+
`tests/conformance/test_contract_envelopes.py`'s `DELIBERATE_DIVERGENCE`
|
|
853
|
+
(with the full rationale) rather than re-recorded, for that map's own
|
|
854
|
+
reason: the golden is the cross-language contract the frozen Rust binary
|
|
855
|
+
is also held to.
|
|
856
|
+
|
|
857
|
+
Deliberately NOT added: a `som:` requirement (the schema has one too).
|
|
858
|
+
Making the shipped-dead checks reachable is the fix; growing this into a
|
|
859
|
+
second copy of `board.schema.json` is not -- ADR-0017 says the SDK owns
|
|
860
|
+
the schema.
|
|
861
|
+
"""
|
|
862
|
+
doc = _load_yaml(text)
|
|
863
|
+
if doc is None:
|
|
864
|
+
raise BoardShapeError("the document is empty")
|
|
865
|
+
if not isinstance(doc, dict):
|
|
866
|
+
raise BoardShapeError(
|
|
867
|
+
f"the top level must be a mapping of keys, got {type(doc).__name__}"
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
som = doc.get("som")
|
|
871
|
+
if som is not None and not isinstance(som, dict):
|
|
872
|
+
raise BoardShapeError(
|
|
873
|
+
"`som:` must be a mapping carrying a `sku:` key, but a scalar was "
|
|
874
|
+
f"given ({som!r}). Write it as:\n som:\n sku: <SKU>"
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
messages: list[str] = []
|
|
878
|
+
|
|
879
|
+
# I-02: the OS is derived from each core's Cortex class and is never
|
|
880
|
+
# selectable, so a top-level `os:` is rejected outright. `in`, not
|
|
881
|
+
# `get(...) is not None`: `board.schema.json`'s `"not": {"required":
|
|
882
|
+
# ["os"]}` fires on the KEY, so a bare `os:` with no value is the same
|
|
883
|
+
# violation and used to slip through.
|
|
884
|
+
if "os" in doc:
|
|
885
|
+
messages.append(
|
|
886
|
+
"unknown key 'os': the OS is a property of each core, derived from "
|
|
887
|
+
"its Cortex class, and is never selectable at the top level. Move "
|
|
888
|
+
"it under `cores:`:\n cores:\n <core-id>:\n os: <os>"
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
cores = doc.get("cores", _MISSING)
|
|
892
|
+
if cores is _MISSING:
|
|
893
|
+
messages.append(
|
|
894
|
+
"required key 'cores' is missing. Name at least one core:\n"
|
|
895
|
+
" cores:\n <core-id>:\n app: ./src"
|
|
896
|
+
)
|
|
897
|
+
elif not isinstance(cores, dict):
|
|
898
|
+
# An EMPTY mapping is deliberately not flagged: the no-PyYAML reader
|
|
899
|
+
# (`_top_level_shape`) represents every block-opening key as `{}` and
|
|
900
|
+
# cannot see inside it, so failing on `{}` would refuse every real
|
|
901
|
+
# board on a host without PyYAML. Emptiness is the SDK's to catch.
|
|
902
|
+
#
|
|
903
|
+
# One asymmetry follows from that and is accepted: a valueless
|
|
904
|
+
# `cores:` reads as `None` under PyYAML (refused here) and as `{}`
|
|
905
|
+
# under the fallback (accepted). The fallback is a degraded reader by
|
|
906
|
+
# construction -- its own docstring says it answers scalar-vs-block
|
|
907
|
+
# and nothing else -- and under-refusing there is the right side to
|
|
908
|
+
# err on, since the SDK validator still sees the real document.
|
|
909
|
+
messages.append(
|
|
910
|
+
f"`cores:` must be a mapping of core id -> settings, but a "
|
|
911
|
+
f"{type(cores).__name__} was given ({cores!r})"
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
outcome = OUTCOME_CLEAN if not messages else OUTCOME_SCHEMA_VIOLATION
|
|
915
|
+
return _Result(outcome, tuple(_Finding("error", message) for message in messages))
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
# ───────────────────────── full (spawn) validation ─────────────────────────
|
|
919
|
+
#
|
|
920
|
+
# Port of `tan_core::validate`'s spawn half + `validate.rs::run_spawn`'s
|
|
921
|
+
# subprocess. See the module docstring for the two deliberate divergences
|
|
922
|
+
# (timeout, ANSI stripping) and the one deliberate omission (`is_summary_line`,
|
|
923
|
+
# a no-op in the oracle).
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def classify_validator_status(status: int | None) -> str:
|
|
927
|
+
"""Validator exit status -> outcome (`classify_validation_outcome`). A
|
|
928
|
+
`None` status -- the process was killed, or never started -- lands on
|
|
929
|
+
`failed` through the same default, exactly as the oracle's `_ =>` arm
|
|
930
|
+
does; on POSIX a signal death arrives here as a NEGATIVE returncode
|
|
931
|
+
instead, which is equally unmapped and equally `failed`."""
|
|
932
|
+
return _STATUS_OUTCOME.get(status, OUTCOME_FAILED)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _severity_for_outcome(outcome: str) -> str:
|
|
936
|
+
"""`severity_for_outcome`: a missing preset is a warning, everything else
|
|
937
|
+
an error. Applies to legacy `FAIL` lines only -- a rich block and a `WARN`
|
|
938
|
+
line each carry their own severity."""
|
|
939
|
+
return "warning" if outcome == OUTCOME_MISSING_PRESET else "error"
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def parse_validator_stderr(stderr: str, severity: str) -> tuple[_Finding, ...]:
|
|
943
|
+
"""The validator's stderr as [`_Finding`]s -- port of
|
|
944
|
+
`tan_core::validate::parse_validation_issues`, plus tan-cli#498 defect 2.
|
|
945
|
+
|
|
946
|
+
Three shapes are recognised, and anything else is dropped: a rich
|
|
947
|
+
`error[ALP-B*]` block (whose `-->` arrow and indented source/hint
|
|
948
|
+
continuation lines are consumed with it, so one block is one finding), a
|
|
949
|
+
legacy `FAIL`/`WARN` line (folding its own indented continuations in,
|
|
950
|
+
joined by TWO spaces exactly as the oracle does), and a standalone
|
|
951
|
+
`hint:`/`suggestion:` line.
|
|
952
|
+
|
|
953
|
+
**What the block's continuation lines are now READ for.** The oracle walks
|
|
954
|
+
them only to know where the block ends -- "block code/line/col are parsed
|
|
955
|
+
for correct line skipping but not retained" is its own comment -- so the
|
|
956
|
+
`ALP-Bxxx` code, the `= hint:` remedy and the `= see:` documentation path
|
|
957
|
+
were consumed and dropped on every real run. Measured against alp-sdk's
|
|
958
|
+
own `tests/fixtures/board_yaml_bad/ALP-B005-bad-sku.yaml`, the user was
|
|
959
|
+
told the SKU was wrong and shown neither the suggested `E1M-AEN801` nor
|
|
960
|
+
the code that finds `docs/diagnostics/ALP-B005.md`. The skipping is
|
|
961
|
+
unchanged (same lines consumed, one block still one finding); the fields
|
|
962
|
+
now travel on the `_Finding` instead of being thrown away.
|
|
963
|
+
|
|
964
|
+
ANSI escapes are stripped per line before any of that -- see the module
|
|
965
|
+
docstring; without it the rich-header match fails on every real run.
|
|
966
|
+
"""
|
|
967
|
+
lines = [_ANSI_RE.sub("", raw) for raw in stderr.replace("\r\n", "\n").split("\n")]
|
|
968
|
+
findings: list[_Finding] = []
|
|
969
|
+
i = 0
|
|
970
|
+
while i < len(lines):
|
|
971
|
+
line = lines[i]
|
|
972
|
+
if not line.strip():
|
|
973
|
+
i += 1
|
|
974
|
+
continue
|
|
975
|
+
|
|
976
|
+
rich = _RICH_HEADER_RE.match(line)
|
|
977
|
+
if rich:
|
|
978
|
+
block_line = block_col = block_span = 0
|
|
979
|
+
hint: str | None = None
|
|
980
|
+
doc_uri: str | None = None
|
|
981
|
+
i += 1
|
|
982
|
+
# Only an arrow line opens a continuation run: a header on its own
|
|
983
|
+
# is a complete finding, and the next line may be another header.
|
|
984
|
+
if i < len(lines) and (arrow := _ARROW_RE.match(lines[i])):
|
|
985
|
+
block_line, block_col = int(arrow.group(1)), int(arrow.group(2))
|
|
986
|
+
i += 1
|
|
987
|
+
while i < len(lines) and _BLOCK_CONTINUATION_RE.match(lines[i]):
|
|
988
|
+
if (caret := _CARET_RE.match(lines[i])) and not block_span:
|
|
989
|
+
block_span = len(caret.group(1))
|
|
990
|
+
elif (found_hint := _BLOCK_HINT_RE.match(lines[i])) and hint is None:
|
|
991
|
+
hint = found_hint.group(1)
|
|
992
|
+
elif (see := _BLOCK_SEE_RE.match(lines[i])) and doc_uri is None:
|
|
993
|
+
doc_uri = see.group(1)
|
|
994
|
+
i += 1
|
|
995
|
+
findings.append(
|
|
996
|
+
_Finding(
|
|
997
|
+
_HEADER_SEVERITY[rich.group(1)],
|
|
998
|
+
rich.group(3).strip(),
|
|
999
|
+
alp_code=rich.group(2),
|
|
1000
|
+
hint=hint,
|
|
1001
|
+
doc_uri=doc_uri,
|
|
1002
|
+
line=block_line,
|
|
1003
|
+
col=block_col,
|
|
1004
|
+
span=block_span,
|
|
1005
|
+
)
|
|
1006
|
+
)
|
|
1007
|
+
continue
|
|
1008
|
+
|
|
1009
|
+
legacy = _FAIL_WARN_RE.match(line)
|
|
1010
|
+
if legacy:
|
|
1011
|
+
parts = [legacy.group(2).strip()]
|
|
1012
|
+
while i + 1 < len(lines) and _FAIL_CONTINUATION_RE.match(lines[i + 1]):
|
|
1013
|
+
i += 1
|
|
1014
|
+
parts.append(lines[i].strip())
|
|
1015
|
+
# WARN is a warning whatever the outcome; FAIL follows the outcome.
|
|
1016
|
+
findings.append(
|
|
1017
|
+
_Finding(
|
|
1018
|
+
"warning" if legacy.group(1) == "WARN" else severity,
|
|
1019
|
+
" ".join(parts),
|
|
1020
|
+
)
|
|
1021
|
+
)
|
|
1022
|
+
i += 1
|
|
1023
|
+
continue
|
|
1024
|
+
|
|
1025
|
+
if _HINT_RE.match(line):
|
|
1026
|
+
findings.append(_Finding("note", line.strip()))
|
|
1027
|
+
i += 1
|
|
1028
|
+
continue
|
|
1029
|
+
|
|
1030
|
+
i += 1
|
|
1031
|
+
return tuple(findings)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _is_interpreter_crash(stderr: str) -> bool:
|
|
1035
|
+
return any(line.lstrip().startswith(_TRACEBACK_HEADER) for line in stderr.splitlines())
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def analyze_validator_output(status: int | None, stderr: str) -> _Result:
|
|
1039
|
+
"""`(outcome, findings)` for one validator execution -- port of
|
|
1040
|
+
`analyze_validation_result`, crash reclassification included: a traceback
|
|
1041
|
+
on exit 1 is a broken validator ENVIRONMENT (`failed`), never a verdict
|
|
1042
|
+
about the board (`schema-violation`)."""
|
|
1043
|
+
outcome = classify_validator_status(status)
|
|
1044
|
+
if outcome == OUTCOME_SCHEMA_VIOLATION and _is_interpreter_crash(stderr):
|
|
1045
|
+
outcome = OUTCOME_FAILED
|
|
1046
|
+
return _Result(outcome, parse_validator_stderr(stderr, _severity_for_outcome(outcome)))
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _synthesised_finding(
|
|
1050
|
+
outcome: str, stderr: str, *, used_workspace_venv: bool = True
|
|
1051
|
+
) -> _Finding:
|
|
1052
|
+
"""The one finding a non-clean run with NOTHING parseable still owes the
|
|
1053
|
+
wire -- `to_cli_issues`' own synthesis, plus the last non-empty stderr
|
|
1054
|
+
line. The oracle stops at "Validation ended with outcome 'failed'.", which
|
|
1055
|
+
on the case that reaches this most often (a validator whose environment
|
|
1056
|
+
lacks `jsonschema`, so its whole stderr is a traceback the parser drops)
|
|
1057
|
+
tells the user nothing at all about what went wrong.
|
|
1058
|
+
|
|
1059
|
+
tan-cli#652: when `used_workspace_venv` is `False` -- `_planner_python`
|
|
1060
|
+
fell back to a bare PATH interpreter because no `tan bootstrap` workspace
|
|
1061
|
+
venv resolved for this project -- and the traceback names a missing
|
|
1062
|
+
module, the generic "Last line of validator output" message is replaced
|
|
1063
|
+
with one naming the actual, fixable cause (no workspace venv yet) instead
|
|
1064
|
+
of a raw `ModuleNotFoundError`. `used_workspace_venv` defaults to `True`
|
|
1065
|
+
so a caller that does not pass it (and every pre-#652 test) keeps
|
|
1066
|
+
today's generic message: this is opt-in enrichment of one specific,
|
|
1067
|
+
identifiable cause, not a new default finding for every crash.
|
|
1068
|
+
|
|
1069
|
+
A workspace venv that ITSELF lacks the module (a corrupted `tan
|
|
1070
|
+
bootstrap` run, not a missing one) does NOT take this branch --
|
|
1071
|
+
`used_workspace_venv` is `True` there, and the customer gets the generic
|
|
1072
|
+
message rather than being told to re-run a `tan bootstrap` that already
|
|
1073
|
+
ran. Telling those two apart is the whole reason this is a caller-supplied
|
|
1074
|
+
flag instead of a guess from the stderr text alone.
|
|
1075
|
+
"""
|
|
1076
|
+
if not used_workspace_venv:
|
|
1077
|
+
missing = _MISSING_MODULE_RE.search(stderr)
|
|
1078
|
+
if missing is not None:
|
|
1079
|
+
return _Finding(
|
|
1080
|
+
"error",
|
|
1081
|
+
f"the SDK validator's interpreter has no `{missing.group(1)}` "
|
|
1082
|
+
"module, because no `tan bootstrap` workspace venv was found "
|
|
1083
|
+
"for this project -- tan ran it under the system interpreter "
|
|
1084
|
+
"instead, which does not have it. Run `tan bootstrap` first, "
|
|
1085
|
+
"then re-run this command.",
|
|
1086
|
+
)
|
|
1087
|
+
message = f"Validation ended with outcome '{outcome}'."
|
|
1088
|
+
tail = [line.strip() for line in _ANSI_RE.sub("", stderr).splitlines() if line.strip()]
|
|
1089
|
+
if tail:
|
|
1090
|
+
message = f"{message} Last line of validator output: {tail[-1]}"
|
|
1091
|
+
return _Finding("error", message)
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
#: The position both exporters fall back to when NOTHING located the finding:
|
|
1095
|
+
#: every offline structural check (`board.yaml` parses fine; the finding is
|
|
1096
|
+
#: about the whole document), a legacy `FAIL`/`WARN` line, and any rich block
|
|
1097
|
+
#: whose `-->` arrow was absent. Zero satisfies
|
|
1098
|
+
#: `diagnostic-v1.schema.json`'s `required: [start, end]` honestly -- it is not
|
|
1099
|
+
#: a claim about *where* on the line, only that no better position is known.
|
|
1100
|
+
#:
|
|
1101
|
+
#: It is no longer the position of EVERY issue: since tan-cli#498 defect 2 a
|
|
1102
|
+
#: rich block keeps its arrow's `line:col` and its caret's span, which is what
|
|
1103
|
+
#: lets an IDE underline the offending value instead of the file's first
|
|
1104
|
+
#: character.
|
|
1105
|
+
_ZERO_POSITION = {"line": 0, "character": 0}
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _lsp_range(finding: _Finding) -> dict[str, Any]:
|
|
1109
|
+
"""`$defs/range` for one finding -- ZERO-based, the LSP convention
|
|
1110
|
+
`diagnostic-v1.schema.json` mandates and `alp_cli.diagnostic_format.
|
|
1111
|
+
_lsp_range` implements: `start = (line - 1, col - 1)`, `end` on the same
|
|
1112
|
+
line at `start.character + max(1, span)`.
|
|
1113
|
+
|
|
1114
|
+
A finding with no stated line (`line == 0`) collapses to
|
|
1115
|
+
[`_ZERO_POSITION`] rather than underflowing to `-1`, which
|
|
1116
|
+
`$defs/position`'s `minimum: 0` forbids."""
|
|
1117
|
+
if finding.line <= 0 or finding.col <= 0:
|
|
1118
|
+
return {"start": _ZERO_POSITION, "end": _ZERO_POSITION}
|
|
1119
|
+
start = {"line": finding.line - 1, "character": finding.col - 1}
|
|
1120
|
+
return {
|
|
1121
|
+
"start": start,
|
|
1122
|
+
"end": {
|
|
1123
|
+
"line": start["line"],
|
|
1124
|
+
"character": start["character"] + max(1, finding.span),
|
|
1125
|
+
},
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _diagnostic_code(issue: Issue, finding: _Finding) -> str:
|
|
1130
|
+
"""The `code` for the diagnostic/SARIF wire: the validator's OWN
|
|
1131
|
+
`ALP-Bxxx` when it gave one, else the envelope code with its dot stripped
|
|
1132
|
+
(`diagnostic-v1`'s `code` pattern `^[A-Za-z][A-Za-z0-9_-]*$` forbids a
|
|
1133
|
+
dot -- that is a different wire, not a rename of the envelope code).
|
|
1134
|
+
|
|
1135
|
+
tan-cli#498 defect 2: every entry used to be `validate-schema-violation`,
|
|
1136
|
+
which made the document alp-sdk's `check_diagnostic_schema.py` is meant to
|
|
1137
|
+
repoint at tan strictly less informative than the one it replaces --
|
|
1138
|
+
alp-sdk's own `_diagnostic_to_json` emits `ALP-B003`/`ALP-B005` there."""
|
|
1139
|
+
return finding.alp_code or issue.code.replace(".", "-")
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _issue_to_diagnostic(issue: Issue, finding: _Finding, board_path: str) -> dict[str, Any]:
|
|
1143
|
+
"""One reported finding as one `$defs/diagnostic` entry.
|
|
1144
|
+
|
|
1145
|
+
`hint` and `documentationUri` are emitted when -- and only when -- the
|
|
1146
|
+
validator supplied them, mirroring `_diagnostic_to_json`'s own `if
|
|
1147
|
+
diag.hint`. tan still does NOT synthesise a `documentationUri` for its own
|
|
1148
|
+
codes: it ships no `docs/diagnostics/<code>.md` landing pages, so there is
|
|
1149
|
+
no honest URL to invent. What changed in tan-cli#498 is that the SDK's
|
|
1150
|
+
`= see:` line is no longer discarded on the way past -- that URL is the
|
|
1151
|
+
child's, not tan's, and it exists."""
|
|
1152
|
+
diagnostic = {
|
|
1153
|
+
# tan-cli#1097: rendered through `path_to_uri_reference`, not
|
|
1154
|
+
# `board_path` bare -- a filesystem PATH is not a URI reference (on
|
|
1155
|
+
# Windows this was `C:\w\proj\board.yaml`, which no editor matches
|
|
1156
|
+
# by document URI, so the diagnostic attached to nothing, silently).
|
|
1157
|
+
# Fixed at the exporter, here and at the SARIF `artifactLocation`
|
|
1158
|
+
# below -- not in the path resolver; see `tan.core.uri_reference`'s
|
|
1159
|
+
# module docstring for the relative-vs-absolute split. This closes
|
|
1160
|
+
# the defect for an ABSOLUTE board path; a RELATIVE `uri` here is
|
|
1161
|
+
# left as a legal relative reference with no base declared -- this
|
|
1162
|
+
# module makes no claim about how a consumer resolves it (see the
|
|
1163
|
+
# module docstring's own note on why an earlier attempt at that,
|
|
1164
|
+
# tied to SARIF's `originalUriBaseIds`, was reverted).
|
|
1165
|
+
"uri": path_to_uri_reference(board_path),
|
|
1166
|
+
"range": _lsp_range(finding),
|
|
1167
|
+
"severity": issue.severity,
|
|
1168
|
+
"code": _diagnostic_code(issue, finding),
|
|
1169
|
+
# `finding.message`, NOT `issue.message`: the envelope's message is the
|
|
1170
|
+
# HUMAN composition ([`_issue_message`] folds the code and hint into it
|
|
1171
|
+
# because `issues[].code` is `validate.<outcome>` and has nowhere else
|
|
1172
|
+
# to put them). Here they are structured fields of their own, so
|
|
1173
|
+
# repeating them inside `message` would hand a machine consumer the
|
|
1174
|
+
# same three facts twice and diverge from alp-sdk's own
|
|
1175
|
+
# `_diagnostic_to_json`, which emits the bare message.
|
|
1176
|
+
"message": finding.message,
|
|
1177
|
+
}
|
|
1178
|
+
if finding.hint:
|
|
1179
|
+
diagnostic["hint"] = finding.hint
|
|
1180
|
+
if finding.doc_uri:
|
|
1181
|
+
diagnostic["documentationUri"] = finding.doc_uri
|
|
1182
|
+
return diagnostic
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _diagnostic_v1_document(
|
|
1186
|
+
reported: list[tuple[Issue, _Finding]], board_path: str
|
|
1187
|
+
) -> dict[str, Any]:
|
|
1188
|
+
"""`metadata/schemas/diagnostic-v1.schema.json`, mirroring
|
|
1189
|
+
`scripts/alp_cli/diagnostic_format.py:to_machine_json`'s shape.
|
|
1190
|
+
|
|
1191
|
+
tan-cli#1117 is deliberately scoped to `--format sarif` ONLY. LSP has no
|
|
1192
|
+
`originalUriBaseIds` equivalent -- a relative `uri` here is resolved by
|
|
1193
|
+
the CLIENT against whatever document root the editor/language server
|
|
1194
|
+
protocol negotiated, not by a base this document could declare -- so a
|
|
1195
|
+
relative `uri` stays exactly as tan-cli#1097 left it: unresolved,
|
|
1196
|
+
undocumented beyond this note. Resolution for the one consumer that
|
|
1197
|
+
matters, `alp-sdk-vscode`, falls to the WRAPPER
|
|
1198
|
+
(`src/alpCli/service.ts`), which shells this CLI and parses its output,
|
|
1199
|
+
not to an LSP client here parsing a protocol tan does not speak. Nothing
|
|
1200
|
+
in this function changed for tan-cli#1117; this docstring paragraph is
|
|
1201
|
+
the missing "say so" tan-cli#1117's own scope item asked for."""
|
|
1202
|
+
return {
|
|
1203
|
+
"schemaVersion": _DIAGNOSTIC_SCHEMA_VERSION,
|
|
1204
|
+
"tool": {"name": "tan", "version": TAN_VERSION},
|
|
1205
|
+
"diagnostics": [_issue_to_diagnostic(i, f, board_path) for i, f in reported],
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def _sarif_region(finding: _Finding) -> dict[str, int]:
|
|
1210
|
+
"""ONE-based SARIF `region`, the opposite convention to [`_lsp_range`] and
|
|
1211
|
+
a direct passthrough of the validator's own numbers plus the span width
|
|
1212
|
+
(`alp_cli.diagnostic_format._sarif_region`). An unlocated finding stays at
|
|
1213
|
+
`1:1`, as it was before tan-cli#498."""
|
|
1214
|
+
if finding.line <= 0 or finding.col <= 0:
|
|
1215
|
+
return {"startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 1}
|
|
1216
|
+
return {
|
|
1217
|
+
"startLine": finding.line,
|
|
1218
|
+
"startColumn": finding.col,
|
|
1219
|
+
"endLine": finding.line,
|
|
1220
|
+
"endColumn": finding.col + max(1, finding.span),
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _sarif_document(
|
|
1225
|
+
reported: list[tuple[Issue, _Finding]], board_path: str, sarif_base: str | None
|
|
1226
|
+
) -> dict[str, Any]:
|
|
1227
|
+
"""SARIF 2.1.0 (`runs[].results[]`), mirroring
|
|
1228
|
+
`scripts/alp_cli/diagnostic_format.py:to_sarif`. A separate artefact from
|
|
1229
|
+
`diagnostic-v1.schema.json` -- SARIF `region` is one-based by spec, so it
|
|
1230
|
+
does not reuse [`_lsp_range`].
|
|
1231
|
+
|
|
1232
|
+
`helpUri` follows `documentationUri` exactly: present on a rule whose
|
|
1233
|
+
validator named a `= see:` page, absent otherwise. tan invents none for
|
|
1234
|
+
its own codes (it has no landing pages), and `helpUri` is optional in the
|
|
1235
|
+
SARIF 2.1.0 schema.
|
|
1236
|
+
|
|
1237
|
+
tan-cli#1097 review measured that alp-sdk's own `to_sarif`/`_uri` --
|
|
1238
|
+
what "mirroring" above still means for shape -- used to emit
|
|
1239
|
+
`artifactLocation.uri` bare, with no scheme, before this function's `uri`
|
|
1240
|
+
handling diverged from it on purpose (see [`path_to_uri_reference`]).
|
|
1241
|
+
alp-sdk has since paid that same fix upstream (alp-sdk#1932, `43e5b2cb`,
|
|
1242
|
+
inside the `eff266b6` re-sync range this repo now pins) -- confirmed by
|
|
1243
|
+
reading `diagnostic_format.py`'s own `to_sarif`/`_uri` at that ref, not
|
|
1244
|
+
fixed here (a different repo, different release cadence).
|
|
1245
|
+
|
|
1246
|
+
tan-cli#1117: a RELATIVE `artifactLocation.uri` now carries a
|
|
1247
|
+
`uriBaseId` naming `_SARIF_URI_BASE_ID`, and the `run` declares that id
|
|
1248
|
+
in `originalUriBaseIds` pointing at `sarif_base` -- so a spec-conformant
|
|
1249
|
+
consumer resolves the reference the same way regardless of its own CWD,
|
|
1250
|
+
instead of falling back to it by luck. `originalUriBaseIds` is only
|
|
1251
|
+
ADDED when at least one location actually uses it: an absolute
|
|
1252
|
+
`--board-yaml` reference already resolves on its own, and SARIF 2.1.0
|
|
1253
|
+
SS3.4.4 says a location whose `uri` is absolute must NOT also carry a
|
|
1254
|
+
`uriBaseId` -- gated by [`is_absolute_path_reference`], not by sniffing
|
|
1255
|
+
the rendered string (see that function's own docstring for why sniffing
|
|
1256
|
+
is unsound for a driveless Windows path). A round-1 attempt at this PR
|
|
1257
|
+
tried the same shape and got the base itself wrong (anchored on `root`,
|
|
1258
|
+
missing a trailing slash) through code that could crash the command on a
|
|
1259
|
+
caller-supplied `--project` symlink loop; see `tan.core.uri_reference`'s
|
|
1260
|
+
module docstring for that account and how this version avoids both.
|
|
1261
|
+
|
|
1262
|
+
`sarif_base` is PRECOMPUTED -- see `cwd_base_uri_or_none`'s own
|
|
1263
|
+
docstring for why this function never calls it (or [`cwd_base_uri`])
|
|
1264
|
+
itself. `None` (CWD unavailable, or an absolute `board_path`, which never
|
|
1265
|
+
sets `base_needed` below regardless) reports the same thing `dev` gave
|
|
1266
|
+
before tan-cli#1117: a relative `uri` with no declared base."""
|
|
1267
|
+
rules: dict[str, dict[str, str]] = {}
|
|
1268
|
+
results = []
|
|
1269
|
+
base_needed = False
|
|
1270
|
+
for issue, finding in reported:
|
|
1271
|
+
code = _diagnostic_code(issue, finding)
|
|
1272
|
+
if code not in rules:
|
|
1273
|
+
rule = {"id": code}
|
|
1274
|
+
if finding.doc_uri:
|
|
1275
|
+
rule["helpUri"] = finding.doc_uri
|
|
1276
|
+
rules[code] = rule
|
|
1277
|
+
# tan-cli#1097: `artifactLocation` is a URI reference (SARIF 2.1.0),
|
|
1278
|
+
# never the bare filesystem path this used to emit -- see this
|
|
1279
|
+
# function's own docstring for what is, and is not, resolved by
|
|
1280
|
+
# this fix alone.
|
|
1281
|
+
artifact_location: dict[str, str] = {"uri": path_to_uri_reference(board_path)}
|
|
1282
|
+
if sarif_base is not None and not is_absolute_path_reference(board_path):
|
|
1283
|
+
artifact_location["uriBaseId"] = _SARIF_URI_BASE_ID
|
|
1284
|
+
base_needed = True
|
|
1285
|
+
results.append(
|
|
1286
|
+
{
|
|
1287
|
+
"ruleId": code,
|
|
1288
|
+
"level": _SARIF_LEVEL[issue.severity],
|
|
1289
|
+
# The bare message, for the same reason as `_issue_to_
|
|
1290
|
+
# diagnostic`'s: the code is `ruleId` and the landing page is
|
|
1291
|
+
# the rule's `helpUri`.
|
|
1292
|
+
"message": {"text": finding.message},
|
|
1293
|
+
"locations": [
|
|
1294
|
+
{
|
|
1295
|
+
"physicalLocation": {
|
|
1296
|
+
"artifactLocation": artifact_location,
|
|
1297
|
+
"region": _sarif_region(finding),
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
],
|
|
1301
|
+
}
|
|
1302
|
+
)
|
|
1303
|
+
run: dict[str, Any] = {
|
|
1304
|
+
"tool": {
|
|
1305
|
+
"driver": {
|
|
1306
|
+
"name": "tan",
|
|
1307
|
+
"informationUri": "https://github.com/alplabai/tan-cli",
|
|
1308
|
+
"version": TAN_VERSION,
|
|
1309
|
+
"rules": list(rules.values()),
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
"results": results,
|
|
1313
|
+
}
|
|
1314
|
+
if base_needed:
|
|
1315
|
+
# tan-cli#1117: declared once per RUN, not per result -- every
|
|
1316
|
+
# relative `board_path` in one invocation names the same file, so
|
|
1317
|
+
# one base entry covers every location that references it.
|
|
1318
|
+
# `sarif_base` cannot be `None` here: `base_needed` is only ever
|
|
1319
|
+
# set inside the `sarif_base is not None` branch above.
|
|
1320
|
+
run["originalUriBaseIds"] = {_SARIF_URI_BASE_ID: {"uri": sarif_base}}
|
|
1321
|
+
return {
|
|
1322
|
+
"$schema": _SARIF_SCHEMA_URI,
|
|
1323
|
+
"version": "2.1.0",
|
|
1324
|
+
"runs": [run],
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def _issue_message(finding: _Finding) -> str:
|
|
1329
|
+
"""The human-facing message for one finding: the validator's text, led by
|
|
1330
|
+
its own `ALP-Bxxx` code and trailed by the remedy and landing page it
|
|
1331
|
+
named.
|
|
1332
|
+
|
|
1333
|
+
tan-cli#498 defect 2, the human half. The envelope's `issues[].code` stays
|
|
1334
|
+
`validate.<outcome>` (wire contract, registered in
|
|
1335
|
+
`contract/issue-codes.json`), which is why the diagnostic code has to
|
|
1336
|
+
travel in the MESSAGE for a `--format text`/`json` reader: without it the
|
|
1337
|
+
user is told the SKU is wrong and given neither the suggestion nor
|
|
1338
|
+
anything to look `docs/diagnostics/ALP-B005.md` up by. Laid out as
|
|
1339
|
+
alp-sdk's own renderer lays it out, minus the severity word (already an
|
|
1340
|
+
`Issue` field) and the source excerpt (tan has not read the file)."""
|
|
1341
|
+
message = (
|
|
1342
|
+
f"{finding.alp_code}: {finding.message}" if finding.alp_code else finding.message
|
|
1343
|
+
)
|
|
1344
|
+
if finding.hint:
|
|
1345
|
+
message += f"\n hint: {finding.hint}"
|
|
1346
|
+
if finding.doc_uri:
|
|
1347
|
+
message += f"\n see: {finding.doc_uri}"
|
|
1348
|
+
return message
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def _emit(
|
|
1352
|
+
*,
|
|
1353
|
+
output_format: ValidateOutputFormat,
|
|
1354
|
+
root: str,
|
|
1355
|
+
board_path: str,
|
|
1356
|
+
outcome: str,
|
|
1357
|
+
issues: list[Issue],
|
|
1358
|
+
exit_code: ExitCode,
|
|
1359
|
+
command_line: str = "",
|
|
1360
|
+
sdk: SdkInfo | None = None,
|
|
1361
|
+
findings: tuple[_Finding, ...] | None = None,
|
|
1362
|
+
# tan-cli#1262: the validator's RAW exit status, for
|
|
1363
|
+
# `data.validatorExitStatus`. `None` means "no validator returned a
|
|
1364
|
+
# status", and the key is then OMITTED rather than emitted as `null` --
|
|
1365
|
+
# the same absent-not-null rule `sdk` follows. `None` is unambiguous here
|
|
1366
|
+
# in a way it is not in `classify_validator_status` (whose `None` is the
|
|
1367
|
+
# oracle's `Option<i32>`): `subprocess.run` yields an `int` returncode on
|
|
1368
|
+
# every platform, so this stays `None` only when none completed.
|
|
1369
|
+
# The value is that returncode VERBATIM, which on POSIX is NEGATIVE for a
|
|
1370
|
+
# signal death (`-9` for SIGKILL) despite the field being named for an
|
|
1371
|
+
# exit status -- the same `-N` `classify_validator_status`'s docstring
|
|
1372
|
+
# already calls out as equally unmapped and equally `failed`.
|
|
1373
|
+
validator_status: int | None = None,
|
|
1374
|
+
# tan-cli#1117 review round 3: NO DEFAULT, deliberately -- see
|
|
1375
|
+
# `cwd_base_uri_or_none`'s own docstring for why this is precomputed
|
|
1376
|
+
# rather than read here. A dropped `sarif_base=` kwarg at any of the
|
|
1377
|
+
# three call sites below is a `TypeError` at the call, not a silently
|
|
1378
|
+
# base-less document.
|
|
1379
|
+
sarif_base: str | None,
|
|
1380
|
+
) -> None:
|
|
1381
|
+
# tan-cli#478 review: `issues` may now carry the SDK-resolution pair
|
|
1382
|
+
# (`sdk.project-pin-unresolved`, `sdk.global-default-foreign-project`)
|
|
1383
|
+
# alongside this command's own findings. Those belong in the ENVELOPE and
|
|
1384
|
+
# nowhere else here:
|
|
1385
|
+
#
|
|
1386
|
+
# * `data.issueCount` reads as "how many findings does this board have";
|
|
1387
|
+
# counting a host fact made a CLEAN board report `outcome: "clean"`,
|
|
1388
|
+
# `exitCode: 0`, `issueCount: 1`.
|
|
1389
|
+
# * `--format diagnostic-v1` / `--format sarif` are ported alp-sdk
|
|
1390
|
+
# documents whose every entry is anchored at `board.yaml` 1:1. A CI job
|
|
1391
|
+
# uploading the SARIF would annotate line 1 of the customer's
|
|
1392
|
+
# board.yaml with "the machine-global default SDK was last set by ..."
|
|
1393
|
+
# -- a fact about the host, presented as a finding about the file.
|
|
1394
|
+
# * the tan-cli#350 text verdict keyed off `len(issues) == 1`, so a
|
|
1395
|
+
# prepended warning silently restored the "validate: validation
|
|
1396
|
+
# failure" wording that issue removed for the nothing-was-checked case.
|
|
1397
|
+
#
|
|
1398
|
+
# Split once, here, rather than at each of the four readers below.
|
|
1399
|
+
reportable = [issue for issue in issues if not issue.code.startswith("sdk.")]
|
|
1400
|
+
|
|
1401
|
+
# tan-cli#498 defect 2: `findings` is the RICH form of that same list, 1:1
|
|
1402
|
+
# with `reportable` and in the same order -- the diagnostic-v1/SARIF
|
|
1403
|
+
# documents read it for the `ALP-Bxxx` code, hint, documentation URI and
|
|
1404
|
+
# source range that `Issue` has nowhere to carry. A caller with nothing
|
|
1405
|
+
# richer to say (every `fail()` refusal) passes none, and each issue
|
|
1406
|
+
# stands in for its own location-less finding.
|
|
1407
|
+
#
|
|
1408
|
+
# Paired against `reportable`, NOT against `issues`, and that is the whole
|
|
1409
|
+
# interaction between #478 and #498: the callers below prepend
|
|
1410
|
+
# `sdk_context_issues` to `issues` while `findings=result.findings` counts
|
|
1411
|
+
# only the validator's own, so `zip(issues, findings)` would slide every
|
|
1412
|
+
# ALP code, hint and source range one position off the issue it describes
|
|
1413
|
+
# the moment a `.alp/sdk-path` pin misses. Filter first, then pair.
|
|
1414
|
+
if findings is None:
|
|
1415
|
+
findings = tuple(_Finding(issue.severity, issue.message) for issue in reportable)
|
|
1416
|
+
reported = list(zip(reportable, findings))
|
|
1417
|
+
|
|
1418
|
+
# Built for TEXT/JSON only (tan-cli#799): `Envelope.__init__` appends the
|
|
1419
|
+
# tan-cli#407 `sdk.discovery-divergent` warning at the shared seam
|
|
1420
|
+
# (`_with_sdk_divergence`). The three TEXT verdicts below loop `issues`
|
|
1421
|
+
# directly, and used to loop the PRE-envelope list -- built only inside
|
|
1422
|
+
# the JSON branch before -- so a seam-appended divergence issue reached
|
|
1423
|
+
# `--format json` and stayed silent on the default channel. DIAGNOSTIC_V1
|
|
1424
|
+
# and SARIF are ported documents that read neither `data` nor `envelope`
|
|
1425
|
+
# (see the two branches below), so building either for them would only
|
|
1426
|
+
# pay for `_with_sdk_divergence`'s filesystem ladder walk and discard the
|
|
1427
|
+
# result -- skip both there.
|
|
1428
|
+
if output_format in (ValidateOutputFormat.JSON, ValidateOutputFormat.TEXT):
|
|
1429
|
+
data = {
|
|
1430
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
1431
|
+
"outcome": outcome,
|
|
1432
|
+
"issueCount": len(reported),
|
|
1433
|
+
# The validator command line that actually ran, or `""` -- which
|
|
1434
|
+
# every guard and the whole offline path keep, and which the two
|
|
1435
|
+
# committed conformance fixtures pin.
|
|
1436
|
+
"commandLine": command_line,
|
|
1437
|
+
# `data.boardYamlPath` is UNTOUCHED by tan-cli#236 -- it names
|
|
1438
|
+
# where tan looked even on the missing-board refusal below; only
|
|
1439
|
+
# `project.boardYaml` (Rust's starkest instance of the bug: the
|
|
1440
|
+
# refusal one line below says the file does not exist, in the
|
|
1441
|
+
# same envelope that used to still name it) is
|
|
1442
|
+
# existence-filtered.
|
|
1443
|
+
"boardYamlPath": board_path,
|
|
1444
|
+
}
|
|
1445
|
+
# tan-cli#1262. Set only when a validator actually returned a status,
|
|
1446
|
+
# ABSENT otherwise -- never `null`. Conditional rather than nullable
|
|
1447
|
+
# because the offline path spawns nothing, so a `null` there would
|
|
1448
|
+
# assert a run that did not happen, and because TWO of the three
|
|
1449
|
+
# committed `validate-offline-*` conformance goldens pin `data`
|
|
1450
|
+
# byte-for-byte (`validate-offline-clean` is in
|
|
1451
|
+
# `test_contract_envelopes.DELIBERATE_DIVERGENCE` and therefore
|
|
1452
|
+
# `xfail(strict=True)`, so its `data` is not compared at all -- it
|
|
1453
|
+
# would NOT have caught a leak here).
|
|
1454
|
+
if validator_status is not None:
|
|
1455
|
+
data["validatorExitStatus"] = validator_status
|
|
1456
|
+
envelope = Envelope(
|
|
1457
|
+
"validate",
|
|
1458
|
+
Project.resolved(root, board_path),
|
|
1459
|
+
data,
|
|
1460
|
+
issues,
|
|
1461
|
+
exit_code,
|
|
1462
|
+
# Absent, not null, when nothing resolved -- `Envelope` omits
|
|
1463
|
+
# the key on `None`. See the module docstring's `sdk` block
|
|
1464
|
+
# paragraph for the measured presence/absence matrix.
|
|
1465
|
+
sdk=sdk,
|
|
1466
|
+
)
|
|
1467
|
+
# Rebind: every branch below now sees whatever the seam appended.
|
|
1468
|
+
# Safe against `reportable`/`reported`/`data["issueCount"]` above,
|
|
1469
|
+
# all computed from the PRE-seam list -- a seam-appended issue's
|
|
1470
|
+
# code always starts with "sdk.", so it was already excluded from
|
|
1471
|
+
# `reportable` either way and none of those three needs
|
|
1472
|
+
# recomputing.
|
|
1473
|
+
issues = envelope.issues
|
|
1474
|
+
|
|
1475
|
+
if output_format == ValidateOutputFormat.JSON:
|
|
1476
|
+
emit(envelope)
|
|
1477
|
+
elif output_format == ValidateOutputFormat.DIAGNOSTIC_V1:
|
|
1478
|
+
# indent=2, matching scripts/alp_cli/validate.py:34's
|
|
1479
|
+
# `json.dumps(to_machine_json(collector), indent=2)` -- these two
|
|
1480
|
+
# formats are ported documents, not the envelope (which is
|
|
1481
|
+
# deliberately compact; see `tan.envelope.emit`'s `separators`).
|
|
1482
|
+
typer.echo(json.dumps(_diagnostic_v1_document(reported, board_path), indent=2))
|
|
1483
|
+
elif output_format == ValidateOutputFormat.SARIF:
|
|
1484
|
+
# indent=2, matching scripts/alp_cli/validate.py:36.
|
|
1485
|
+
typer.echo(json.dumps(_sarif_document(reported, board_path, sarif_base), indent=2))
|
|
1486
|
+
else:
|
|
1487
|
+
stream = typer.get_text_stream("stderr")
|
|
1488
|
+
if len(reportable) == 1 and reportable[0].code == "validate.board-yaml-missing":
|
|
1489
|
+
# tan-cli#350: this is not a VALIDATION failure -- there is no
|
|
1490
|
+
# board.yaml to validate, so nothing was checked and found
|
|
1491
|
+
# wrong. Every other non-clean outcome below still says
|
|
1492
|
+
# "validate: validation failure"; only this one issue code gets
|
|
1493
|
+
# its own verdict wording. `issues[0].message` (shared with
|
|
1494
|
+
# `--format json`'s `issues[].message`) already names where tan
|
|
1495
|
+
# looked and the remedy -- see the guard above.
|
|
1496
|
+
stream.write("validate: no board.yaml to validate\n")
|
|
1497
|
+
# tan-cli#478 review finding 6: this branch prints `reportable[0]`
|
|
1498
|
+
# alone -- deliberately, per tan-cli#350, to keep the verdict
|
|
1499
|
+
# wording narrow -- but the sdk.* advisories `reportable` filters
|
|
1500
|
+
# OUT of `issueCount`/sarif/diagnostic-v1 still belong on the
|
|
1501
|
+
# customer's screen; the other two branches below already print
|
|
1502
|
+
# them because they loop over the unfiltered `issues`.
|
|
1503
|
+
#
|
|
1504
|
+
# tan-cli#799: severity-prefixed (`f"{issue.severity}: ..."`),
|
|
1505
|
+
# matching how `clean`/`size`/`image`/`run` render the identical
|
|
1506
|
+
# `sdk.*` seam issues -- a bare `issue.message` here was the one
|
|
1507
|
+
# text-channel divergence `test_narrow_text_channel_carries_the_
|
|
1508
|
+
# divergence_warning` measured (no "warning:" prefix on
|
|
1509
|
+
# `sdk.discovery-divergent`). `reportable[0].message` just below
|
|
1510
|
+
# stays bare on purpose: that line is validate's own verdict
|
|
1511
|
+
# wording (#350/#498), never an `sdk.*` advisory.
|
|
1512
|
+
for issue in issues:
|
|
1513
|
+
if issue.code.startswith("sdk."):
|
|
1514
|
+
stream.write(f"{issue.severity}: {issue.message}\n")
|
|
1515
|
+
stream.write(f"{reportable[0].message}\n")
|
|
1516
|
+
elif len(reportable) == 1 and reportable[0].code == "validate.board-yaml-unreadable":
|
|
1517
|
+
# tan-cli#498 defect 3, the text half, and the same reasoning as
|
|
1518
|
+
# #350's line above: a board.yaml tan could not even DECODE was
|
|
1519
|
+
# never checked, so "validation failure" claims a verdict that was
|
|
1520
|
+
# never reached. The exit code (2) and issue code are what a
|
|
1521
|
+
# machine consumer reads; this line is what a human does.
|
|
1522
|
+
#
|
|
1523
|
+
# Keyed off `reportable`, and the sdk.* advisories printed first,
|
|
1524
|
+
# for tan-cli#478 review finding 6's reason applied to this
|
|
1525
|
+
# branch: a prepended `sdk.project-pin-unresolved` would otherwise
|
|
1526
|
+
# take `len(issues)` to 2 and silently restore the "validation
|
|
1527
|
+
# failure" wording this branch exists to replace.
|
|
1528
|
+
stream.write("validate: board.yaml could not be read\n")
|
|
1529
|
+
for issue in issues:
|
|
1530
|
+
if issue.code.startswith("sdk."):
|
|
1531
|
+
stream.write(f"{issue.severity}: {issue.message}\n")
|
|
1532
|
+
stream.write(f"{reportable[0].message}\n")
|
|
1533
|
+
elif outcome != OUTCOME_CLEAN:
|
|
1534
|
+
stream.write("validate: validation failure\n")
|
|
1535
|
+
# tan-cli#799: `sdk.*` seam issues (e.g. `sdk.discovery-divergent`)
|
|
1536
|
+
# get the same `f"{severity}: {message}"` rendering the other
|
|
1537
|
+
# four narrow-text commands use; validate's own `reportable`
|
|
1538
|
+
# findings (already accounted for in `outcome`/`issueCount` above
|
|
1539
|
+
# this render block) keep their bare wording unchanged.
|
|
1540
|
+
for issue in issues:
|
|
1541
|
+
if issue.code.startswith("sdk."):
|
|
1542
|
+
stream.write(f"{issue.severity}: {issue.message}\n")
|
|
1543
|
+
else:
|
|
1544
|
+
stream.write(f"{issue.message}\n")
|
|
1545
|
+
else:
|
|
1546
|
+
# Keyed off the OUTCOME, not off `issues` being non-empty: a
|
|
1547
|
+
# SPAWNED validator that exits 0 having printed warnings
|
|
1548
|
+
# (`validate_board_yaml.py` renders every diagnostic and only
|
|
1549
|
+
# RETURNS 1 for `collector.has_errors()`) is a clean board that
|
|
1550
|
+
# still has something to say. Reading `issues` instead -- which is
|
|
1551
|
+
# what this branch did while only the offline path could reach it,
|
|
1552
|
+
# where a clean result has no messages by construction -- printed
|
|
1553
|
+
# "validate: validation failure" over exit 0. The oracle's own
|
|
1554
|
+
# `spawn_text` keys off the outcome for exactly this reason.
|
|
1555
|
+
stream.write(f"validate: {board_path} is clean\n")
|
|
1556
|
+
# Same tan-cli#799 severity-prefix rule as the `validation
|
|
1557
|
+
# failure` branch above: only the `sdk.*` seam issues get it.
|
|
1558
|
+
for issue in issues:
|
|
1559
|
+
if issue.code.startswith("sdk."):
|
|
1560
|
+
stream.write(f"{issue.severity}: {issue.message}\n")
|
|
1561
|
+
else:
|
|
1562
|
+
stream.write(f"{issue.message}\n")
|
|
1563
|
+
raise typer.Exit(int(exit_code))
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def validate(
|
|
1567
|
+
offline: bool = typer.Option(
|
|
1568
|
+
False, "--offline", help="Run only the structural checks that ship in tan."
|
|
1569
|
+
),
|
|
1570
|
+
project: str = typer.Option(
|
|
1571
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
1572
|
+
),
|
|
1573
|
+
board_yaml: str = typer.Option(
|
|
1574
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
1575
|
+
),
|
|
1576
|
+
sdk_root: str = typer.Option(
|
|
1577
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
1578
|
+
),
|
|
1579
|
+
output_format: ValidateOutputFormat = typer.Option(
|
|
1580
|
+
ValidateOutputFormat.TEXT, "--format", help=FORMAT_HELP
|
|
1581
|
+
),
|
|
1582
|
+
) -> None:
|
|
1583
|
+
"""Validate a board.yaml.
|
|
1584
|
+
|
|
1585
|
+
By default the SDK's board.yaml validator runs as a port inside tan. A
|
|
1586
|
+
bound alp-sdk newer than that port may add rules tan does not apply; tan
|
|
1587
|
+
warns (`validate.sdk-validator-newer`) when it can tell. Set the
|
|
1588
|
+
environment variable TAN_VALIDATE_ENGINE=subprocess to run the bound
|
|
1589
|
+
SDK's own scripts/validate_board_yaml.py instead.
|
|
1590
|
+
|
|
1591
|
+
`--sdk-root` must stay declared HERE, as a same-named local option, even
|
|
1592
|
+
though clap makes it `global = true` in Rust
|
|
1593
|
+
(`crates/tan-cli/src/cli.rs`): without it `cli._reorder_global_flags`
|
|
1594
|
+
relocates the flag to right after `validate` and Click rejects it there as
|
|
1595
|
+
unrecognised -- and the pre-subcommand position (`tan --sdk-root X
|
|
1596
|
+
validate`) is the one the extension actually uses
|
|
1597
|
+
(`alpCli/vscodeAdapter.ts`'s `withSdkRoot`). Since tan-cli#376 it is also
|
|
1598
|
+
READ -- and VALIDATED against the loader marker (tan-cli#257/#258) -- by
|
|
1599
|
+
the spawn path's own SDK guard; `--offline` still ignores it entirely,
|
|
1600
|
+
having no subprocess to point anywhere, and reports no `sdk` block.
|
|
1601
|
+
"""
|
|
1602
|
+
root, board_path = _resolve_board_path(project, board_yaml)
|
|
1603
|
+
|
|
1604
|
+
# tan-cli#1117 review round 2: computed ONCE, here -- see
|
|
1605
|
+
# `cwd_base_uri_or_none`'s own docstring for why.
|
|
1606
|
+
sarif_base = cwd_base_uri_or_none()
|
|
1607
|
+
|
|
1608
|
+
# tan-cli#488 defect 8: the identical unguarded prologue `build_cmd.build`
|
|
1609
|
+
# had (see its own comment there) -- everything below this point is now
|
|
1610
|
+
# inside a try, so a raise from ANYWHERE in it (guard resolution, the SDK
|
|
1611
|
+
# ladder, the offline read, the spawn) still produces the
|
|
1612
|
+
# `validate.internal-failure` envelope below instead of a raw traceback
|
|
1613
|
+
# with empty stdout. `typer.Exit` is re-raised untouched -- `fail()`/`_emit()`
|
|
1614
|
+
# already raise it as their OWN normal control flow (every guard above and
|
|
1615
|
+
# below is a `fail(...)` call), and it subclasses `Exception` via
|
|
1616
|
+
# `click.exceptions.Exit(RuntimeError)`, so a bare `except Exception` here
|
|
1617
|
+
# would swallow a NORMAL exit and re-report it as `internal-failure`.
|
|
1618
|
+
try:
|
|
1619
|
+
|
|
1620
|
+
# Resolved BEFORE the board.yaml guard, though it is guard 2, because the
|
|
1621
|
+
# `sdk` block is not a guard result -- it reports what this RUN resolved,
|
|
1622
|
+
# and the oracle emits it on the board-yaml-missing refusal too (measured:
|
|
1623
|
+
# a valid `--sdk-root` + an empty directory answers
|
|
1624
|
+
# `validate.board-yaml-missing` WITH `sdk.sourceTier: "sdkRootFlag"`).
|
|
1625
|
+
# Guard ORDER is unchanged -- board.yaml still refuses first.
|
|
1626
|
+
#
|
|
1627
|
+
# `os.path.abspath`, not `Path(root)`: the ladder WALKS ANCESTORS, and
|
|
1628
|
+
# `Path(".").parents` is EMPTY -- the default `--project` would never
|
|
1629
|
+
# discover the sibling `../alp-sdk` every workspace layout puts there.
|
|
1630
|
+
# Lexical (not `.resolve()`) for the same reason `build_output.normalise`
|
|
1631
|
+
# is: a project reached through a symlink keeps the name the user typed.
|
|
1632
|
+
#
|
|
1633
|
+
# Skipped entirely under `--offline`: no subprocess, so no interpreter and
|
|
1634
|
+
# no checkout to report, and the oracle emits no `sdk` there even when one
|
|
1635
|
+
# would resolve (measured with a sibling checkout AND with an explicit
|
|
1636
|
+
# `--sdk-root`).
|
|
1637
|
+
resolved_sdk: Path | None = None
|
|
1638
|
+
sdk_info: SdkInfo | None = None
|
|
1639
|
+
# tan-cli#478. Declared OUTSIDE the `offline` branch: that path consults
|
|
1640
|
+
# no pointer, so this stays empty -- but `fail()` and the success `_emit`
|
|
1641
|
+
# both read it unconditionally below, so no future early return can drop
|
|
1642
|
+
# the pair (tan-cli#464 review).
|
|
1643
|
+
sdk_context_issues: list[Issue] = []
|
|
1644
|
+
if not offline:
|
|
1645
|
+
sdk_resolution = resolve_sdk_root_ladder(sdk_root, Path(os.path.abspath(root)))
|
|
1646
|
+
resolved_sdk = sdk_resolution.path
|
|
1647
|
+
sdk_tier = sdk_resolution.tier
|
|
1648
|
+
# tan-cli#257/#258, at this call site because `resolve_sdk_root_ladder`
|
|
1649
|
+
# deliberately does not do it: an explicit `--sdk-root` comes back
|
|
1650
|
+
# verbatim and unvalidated, which is right for a caller that only
|
|
1651
|
+
# REPORTS the tier and wrong for this one, which spawns out of it. An
|
|
1652
|
+
# unresolvable explicit root is no root at all -- guard 2 below then
|
|
1653
|
+
# gives the refusal the oracle gives, instead of the SDK's own
|
|
1654
|
+
# `can't open file ...validate_board_yaml.py` reaching the status map
|
|
1655
|
+
# as a verdict about the customer's board.
|
|
1656
|
+
if sdk_tier == "sdkRootFlag" and not is_sdk_root(resolved_sdk):
|
|
1657
|
+
resolved_sdk = None
|
|
1658
|
+
if resolved_sdk is not None:
|
|
1659
|
+
sdk_info = SdkInfo.from_resolution(str(resolved_sdk), sdk_resolution)
|
|
1660
|
+
# tan-cli#478: `validate` SPAWNS out of the resolved root, so a
|
|
1661
|
+
# foreign `globalDefault` means another project's schemas decided
|
|
1662
|
+
# `clean` for this board.yaml. Reported `ok: true`, `issues: []`.
|
|
1663
|
+
sdk_context_issues = sdk_resolution_issues(
|
|
1664
|
+
sdk_resolution.broken_project_pin,
|
|
1665
|
+
sdk_tier,
|
|
1666
|
+
sdk_resolution.foreign_global_default_for,
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
def fail(code: str, message: str, exit_code: ExitCode) -> None:
|
|
1670
|
+
_emit(
|
|
1671
|
+
output_format=output_format,
|
|
1672
|
+
root=root,
|
|
1673
|
+
board_path=board_path,
|
|
1674
|
+
outcome=OUTCOME_FAILED,
|
|
1675
|
+
issues=[*sdk_context_issues, Issue(f"validate.{code}", "error", message)],
|
|
1676
|
+
exit_code=exit_code,
|
|
1677
|
+
sdk=sdk_info,
|
|
1678
|
+
sarif_base=sarif_base,
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
if not Path(board_path).exists():
|
|
1682
|
+
# Ordered BEFORE the SDK guard and the spawn, deliberately -- the same
|
|
1683
|
+
# order `run_spawn` puts its own three guards in. (The SDK is RESOLVED
|
|
1684
|
+
# above this, which is not the same thing: resolving fills the `sdk`
|
|
1685
|
+
# block the oracle reports on this very refusal; refusing is what stays
|
|
1686
|
+
# ordered, and no guard fires before this one.) MEASURED against the
|
|
1687
|
+
# oracle (`target/debug/tan.exe`, tan 0.4.1-dev) in an empty directory:
|
|
1688
|
+
# exit 2, `validate.board-yaml-missing`, `outcome: "failed"` -- and the
|
|
1689
|
+
# oracle reaches this guard on both paths, `--offline` or not. Anything
|
|
1690
|
+
# that short-circuits above this check answers a DIFFERENT question in
|
|
1691
|
+
# the one case a brand-new user hits first: before #376 a "not ported
|
|
1692
|
+
# yet" stub did, and an SDK guard hoisted above it would answer "no
|
|
1693
|
+
# alp-sdk checkout" to a project that has no board.yaml either way.
|
|
1694
|
+
#
|
|
1695
|
+
# tan-cli#350 (DELIBERATE divergence -- the oracle is byte-identical
|
|
1696
|
+
# here, down to exit code and message): the oracle's own wording,
|
|
1697
|
+
# "board.yaml path could not be resolved or the file does not
|
|
1698
|
+
# exist.", names no remedy and, worse, is fronted in text mode by
|
|
1699
|
+
# "validate: validation failure" -- a VERDICT that implies something
|
|
1700
|
+
# was checked and found wrong. Nothing was validated; there is no
|
|
1701
|
+
# board.yaml to validate. This is the state every user is in before
|
|
1702
|
+
# `tan init`, and the old wording sent them looking for a defect in a
|
|
1703
|
+
# file that does not exist. The message below names WHERE tan looked
|
|
1704
|
+
# and the two remedies every sibling guard names for its own missing
|
|
1705
|
+
# input (`build` names `--sdk-root`, `doctor` names `tan init` /
|
|
1706
|
+
# `--board-yaml <path>` for this exact guard -- see
|
|
1707
|
+
# `doctor_cmd.py`'s `_board_yaml_check`). The exit code (2) and issue
|
|
1708
|
+
# CODE (`validate.board-yaml-missing`) are UNCHANGED: a
|
|
1709
|
+
# found-but-invalid board.yaml still exits 2 as
|
|
1710
|
+
# `validate.schema-violation` and still prints "validate: validation
|
|
1711
|
+
# failure" below -- the issue code is how a machine consumer (or a
|
|
1712
|
+
# human reading `--format json`) tells the two apart, since the exit
|
|
1713
|
+
# code alone does not.
|
|
1714
|
+
fail(
|
|
1715
|
+
"board-yaml-missing",
|
|
1716
|
+
f"no board.yaml found at {board_path} -- run `tan init` to create "
|
|
1717
|
+
"one, or pass --board-yaml <path> to point at an existing file.",
|
|
1718
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1719
|
+
)
|
|
1720
|
+
return
|
|
1721
|
+
|
|
1722
|
+
#: The validator argv, for `data.commandLine`. Stays `""` on the offline
|
|
1723
|
+
#: path and on every guard -- nothing ran.
|
|
1724
|
+
command_line = ""
|
|
1725
|
+
|
|
1726
|
+
#: tan-cli#1262: the validator's raw exit status, for
|
|
1727
|
+
#: `data.validatorExitStatus`. Stays `None` -- so `_emit` OMITS the key
|
|
1728
|
+
#: -- everywhere no validator returned one: the whole offline path, all
|
|
1729
|
+
#: three guards, a spawn that failed to launch, and a timeout (the child
|
|
1730
|
+
#: was killed; `TimeoutExpired` carries no returncode).
|
|
1731
|
+
validator_status: int | None = None
|
|
1732
|
+
skew_message: str | None = None
|
|
1733
|
+
|
|
1734
|
+
if offline:
|
|
1735
|
+
try:
|
|
1736
|
+
text = Path(board_path).read_text(encoding="utf-8")
|
|
1737
|
+
except (OSError, UnicodeDecodeError) as err:
|
|
1738
|
+
# tan-cli#498 defect 3. Not a tan bug: an unreadable or
|
|
1739
|
+
# non-UTF-8 file is the user's to fix -- which this comment
|
|
1740
|
+
# already said while the code emitted
|
|
1741
|
+
# `validate.internal-failure` at exit 5, i.e. told CI and
|
|
1742
|
+
# alp-sdk-vscode that tan had CRASHED. Three measurements
|
|
1743
|
+
# against it: the identical file on the spawn path exits 2;
|
|
1744
|
+
# the envelope carried `data.outcome: "failed"` (a verdict)
|
|
1745
|
+
# beside `exitCode: 5`, contradicting this command's own
|
|
1746
|
+
# outcome->exit mapping below; and
|
|
1747
|
+
# `docs/bootstrap-manifest-unreadable.md` answers the same
|
|
1748
|
+
# input class for `tan bootstrap` as exit 2.
|
|
1749
|
+
#
|
|
1750
|
+
# 2, not 1: `validate.spawn-failed` owns exit 1 for "nothing
|
|
1751
|
+
# validated anything", which this resembles -- the tie is
|
|
1752
|
+
# broken by the SPAWN path answering exit 2 `validate.failed`
|
|
1753
|
+
# on the same cp1252 file (measured). One file must not be a
|
|
1754
|
+
# crash on one path and a verdict on the other.
|
|
1755
|
+
#
|
|
1756
|
+
# Its OWN code, not `validate.failed`: file-level problems
|
|
1757
|
+
# named before anything reads the board are a family
|
|
1758
|
+
# (`validate.board-yaml-missing` is the other), and a consumer
|
|
1759
|
+
# can act on "fix the encoding" without parsing prose. The
|
|
1760
|
+
# frozen oracle shares the exit-5 flaw; diverging is what
|
|
1761
|
+
# commit 49a5fde did for `tan sdk install`/`sdk switch` and
|
|
1762
|
+
# b4cef84 for the SIBLING BoardShapeError branch in the oracle
|
|
1763
|
+
# itself, left unapplied to the read case. A genuine tan crash
|
|
1764
|
+
# keeps exit 5 -- the `except Exception` backstop is unchanged.
|
|
1765
|
+
fail(
|
|
1766
|
+
"board-yaml-unreadable",
|
|
1767
|
+
f"could not read board.yaml: {err}",
|
|
1768
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1769
|
+
)
|
|
1770
|
+
return
|
|
1771
|
+
|
|
1772
|
+
try:
|
|
1773
|
+
result = validate_board_text(text)
|
|
1774
|
+
except BoardShapeError as err:
|
|
1775
|
+
fail(
|
|
1776
|
+
"schema-violation",
|
|
1777
|
+
f"board.yaml is not valid: {err}",
|
|
1778
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1779
|
+
)
|
|
1780
|
+
return
|
|
1781
|
+
except Exception as err: # never a bare traceback; the envelope is the contract
|
|
1782
|
+
fail(
|
|
1783
|
+
"internal-failure",
|
|
1784
|
+
f"validator failed unexpectedly: {err}",
|
|
1785
|
+
ExitCode.INTERNAL_FAILURE,
|
|
1786
|
+
)
|
|
1787
|
+
return
|
|
1788
|
+
else:
|
|
1789
|
+
if resolved_sdk is None:
|
|
1790
|
+
# The oracle's guard 2, message included in substance (its own
|
|
1791
|
+
# wording names only `--sdk-root`; the two other tiers this port's
|
|
1792
|
+
# ladder resolves are named here for the same reason
|
|
1793
|
+
# `trace_cmd.py`'s identical guard names them).
|
|
1794
|
+
#
|
|
1795
|
+
# tan-cli#381, FIFTH site: this line hardcoded "pin one with `tan
|
|
1796
|
+
# sdk switch <version|path>`" -- a subcommand that refuses outright
|
|
1797
|
+
# in this build (`sdk_cmd._run_not_ported`, `sdk.not-ported`). It
|
|
1798
|
+
# was not missed by the #305 sweep or the #381 sweep; it did not
|
|
1799
|
+
# EXIST when either ran. #376 wrote it here, from the oracle's own
|
|
1800
|
+
# (honest, there) wording, while #381 was cleaning the other four.
|
|
1801
|
+
# That is the whole mechanism, so the fix is not just this string:
|
|
1802
|
+
# `test_sdk_onboarding_dead_end.py`'s AST sweep now walks every
|
|
1803
|
+
# string literal under `python/tan/` and fails on the phrase, which
|
|
1804
|
+
# is what catches the SIXTH site before a human has to.
|
|
1805
|
+
#
|
|
1806
|
+
# tan-cli#497 defect 7: a REJECTED `--sdk-root` names the
|
|
1807
|
+
# value. The `--offline` sentence is kept on BOTH branches --
|
|
1808
|
+
# it is the one piece of remediation here that is not the flag
|
|
1809
|
+
# the caller just typed, and it is what an offline run needs.
|
|
1810
|
+
fail(
|
|
1811
|
+
"sdk-root-unresolved",
|
|
1812
|
+
rejected_sdk_root_message(
|
|
1813
|
+
sdk_root,
|
|
1814
|
+
"Nothing was validated. `tan validate --offline` runs the "
|
|
1815
|
+
"structural checks that need no SDK.",
|
|
1816
|
+
)
|
|
1817
|
+
if sdk_root
|
|
1818
|
+
else with_sdk_search(
|
|
1819
|
+
"alp-sdk root is unresolved. Use --sdk-root, place the project "
|
|
1820
|
+
f"near an alp-sdk checkout, or {NO_SDK_NEXT_STEPS}. "
|
|
1821
|
+
"`tan validate --offline` runs the structural checks that need "
|
|
1822
|
+
"no SDK.",
|
|
1823
|
+
os.path.abspath(root),
|
|
1824
|
+
),
|
|
1825
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1826
|
+
)
|
|
1827
|
+
return
|
|
1828
|
+
|
|
1829
|
+
if not _subprocess_engine_requested():
|
|
1830
|
+
# tan-cli#270: the DEFAULT engine. The SDK's validator is
|
|
1831
|
+
# ported in-process (`tan.core.board_validator_run`), so no
|
|
1832
|
+
# interpreter is probed and nothing is spawned -- the python
|
|
1833
|
+
# floor guard below guards a child interpreter that no longer
|
|
1834
|
+
# exists on this path. The run's (status, stderr) go through
|
|
1835
|
+
# the SAME `analyze_validator_output` a spawned script's did,
|
|
1836
|
+
# so the outcome map, issue codes and envelope are unchanged.
|
|
1837
|
+
# The customer's own file is read HERE, ahead of the engine, so
|
|
1838
|
+
# an undecodable or unreadable board.yaml is the user's to fix
|
|
1839
|
+
# (`validate.board-yaml-unreadable`, like `--offline`), not an
|
|
1840
|
+
# engine crash that blames the SDK.
|
|
1841
|
+
try:
|
|
1842
|
+
Path(board_path).read_text(encoding="utf-8")
|
|
1843
|
+
except (OSError, UnicodeDecodeError) as err:
|
|
1844
|
+
fail(
|
|
1845
|
+
"board-yaml-unreadable",
|
|
1846
|
+
f"could not read board.yaml: {err}",
|
|
1847
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1848
|
+
)
|
|
1849
|
+
return
|
|
1850
|
+
validator_status, _stderr, result = run_in_process_engine(
|
|
1851
|
+
board_path, resolved_sdk
|
|
1852
|
+
)
|
|
1853
|
+
skew_message = skew_warning(resolved_sdk)
|
|
1854
|
+
else:
|
|
1855
|
+
script = os.path.join(str(resolved_sdk), *VALIDATOR_SCRIPT)
|
|
1856
|
+
# tan-cli#652: also captures whether this resolved a `tan
|
|
1857
|
+
# bootstrap` workspace venv or fell back to a bare PATH name --
|
|
1858
|
+
# the flag `_synthesised_finding` below needs to tell "this
|
|
1859
|
+
# interpreter is missing a dependency because no workspace venv
|
|
1860
|
+
# exists yet" apart from any other interpreter defect.
|
|
1861
|
+
python_binary, used_workspace_venv = _planner_python_resolution(
|
|
1862
|
+
os.path.abspath(root), str(resolved_sdk)
|
|
1863
|
+
)
|
|
1864
|
+
|
|
1865
|
+
# The oracle's guard 3 (`validate.rs:124-129`), the one #376 left out.
|
|
1866
|
+
# AFTER the SDK guard because both of its inputs come from the resolved
|
|
1867
|
+
# checkout: the floor is that checkout's own declared
|
|
1868
|
+
# `pythonMinVersion`, and `_planner_python` prefers its workspace venv.
|
|
1869
|
+
# BEFORE the spawn because the whole point is to replace alp-sdk's
|
|
1870
|
+
# `dataclass() got an unexpected keyword argument 'slots'` traceback --
|
|
1871
|
+
# which arrives as validator exit 1 WITH a traceback, i.e. `failed`
|
|
1872
|
+
# with the traceback's last line quoted at the user -- with a message
|
|
1873
|
+
# naming the actual defect. `command_line` is still `""` here: nothing
|
|
1874
|
+
# ran, exactly as on guards 1 and 2 and as the oracle reports.
|
|
1875
|
+
floor, _floor_source = resolve_manifest_python_floor(str(resolved_sdk))
|
|
1876
|
+
if (too_old := _python_too_old(python_binary, floor)) is not None:
|
|
1877
|
+
fail("python-too-old", too_old, ExitCode.VALIDATION_FAILURE)
|
|
1878
|
+
return
|
|
1879
|
+
|
|
1880
|
+
# Verbatim from `run_spawn`'s own `format!` -- this string is reported,
|
|
1881
|
+
# never re-parsed, so it is built beside the argv rather than from it.
|
|
1882
|
+
command_line = f"{python_binary} {script} --input {board_path}"
|
|
1883
|
+
try:
|
|
1884
|
+
out = subprocess.run(
|
|
1885
|
+
[python_binary, script, "--input", board_path],
|
|
1886
|
+
capture_output=True,
|
|
1887
|
+
text=True,
|
|
1888
|
+
encoding="utf-8",
|
|
1889
|
+
errors="replace",
|
|
1890
|
+
# The validator never reads stdin; without this a child that
|
|
1891
|
+
# somehow prompts would block forever behind the timeout.
|
|
1892
|
+
stdin=subprocess.DEVNULL,
|
|
1893
|
+
timeout=VALIDATOR_TIMEOUT_S,
|
|
1894
|
+
env=spawn_env(),
|
|
1895
|
+
check=False,
|
|
1896
|
+
)
|
|
1897
|
+
except subprocess.TimeoutExpired:
|
|
1898
|
+
# The child STARTED, so this is a verdict that never arrived, not a
|
|
1899
|
+
# launch failure: `failed` at exit 2, per tan-cli#262.
|
|
1900
|
+
result = _Result(
|
|
1901
|
+
OUTCOME_FAILED,
|
|
1902
|
+
(
|
|
1903
|
+
_Finding(
|
|
1904
|
+
"error",
|
|
1905
|
+
f"the SDK validator did not finish within "
|
|
1906
|
+
f"{VALIDATOR_TIMEOUT_S}s and was killed: {command_line}",
|
|
1907
|
+
),
|
|
1908
|
+
),
|
|
1909
|
+
)
|
|
1910
|
+
except (OSError, ValueError, subprocess.SubprocessError) as err:
|
|
1911
|
+
# The one RUNTIME_FAILURE (1) case #262 carved out: the subprocess
|
|
1912
|
+
# could not even be started (no interpreter on PATH, the script
|
|
1913
|
+
# unreadable). Nothing validated anything, so this is not a verdict.
|
|
1914
|
+
fail(
|
|
1915
|
+
"spawn-failed",
|
|
1916
|
+
f"could not run the SDK validator ({command_line}): {err}",
|
|
1917
|
+
ExitCode.RUNTIME_FAILURE,
|
|
1918
|
+
)
|
|
1919
|
+
return
|
|
1920
|
+
else:
|
|
1921
|
+
# tan-cli#1262: captured BEFORE the mapping consumes it. The
|
|
1922
|
+
# `.get(..., OUTCOME_FAILED)` fallback is the right default
|
|
1923
|
+
# for an exit this build does not name, but it must not be all
|
|
1924
|
+
# a consumer is left with -- an UNMAPPED status reaches the
|
|
1925
|
+
# wire as a NUMBER, not only as `failed`.
|
|
1926
|
+
validator_status = out.returncode
|
|
1927
|
+
result = analyze_validator_output(out.returncode, out.stderr)
|
|
1928
|
+
if result.outcome != OUTCOME_CLEAN and not result.findings:
|
|
1929
|
+
# `to_cli_issues`' synthesis: a non-clean run must never reach a
|
|
1930
|
+
# consumer as "exit 2, zero issues", which reads as no problem.
|
|
1931
|
+
# `used_workspace_venv=used_workspace_venv` (tan-cli#652) is what
|
|
1932
|
+
# lets this become "run `tan bootstrap` first" instead of a raw
|
|
1933
|
+
# `ModuleNotFoundError` when that is the actual cause.
|
|
1934
|
+
result = _Result(
|
|
1935
|
+
result.outcome,
|
|
1936
|
+
(
|
|
1937
|
+
_synthesised_finding(
|
|
1938
|
+
result.outcome,
|
|
1939
|
+
out.stderr,
|
|
1940
|
+
used_workspace_venv=used_workspace_venv,
|
|
1941
|
+
),
|
|
1942
|
+
),
|
|
1943
|
+
)
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
issues = [
|
|
1947
|
+
*sdk_context_issues,
|
|
1948
|
+
*(
|
|
1949
|
+
Issue(f"validate.{result.outcome}", finding.severity, _issue_message(finding))
|
|
1950
|
+
for finding in result.findings
|
|
1951
|
+
),
|
|
1952
|
+
]
|
|
1953
|
+
reported_findings = result.findings
|
|
1954
|
+
if skew_message is not None:
|
|
1955
|
+
# Non-fatal: never changes the outcome or exit code. Kept 1:1 with
|
|
1956
|
+
# `issues` by appending the matching finding too.
|
|
1957
|
+
issues.append(Issue("validate.sdk-validator-newer", "warning", skew_message))
|
|
1958
|
+
reported_findings = (*result.findings, _Finding("warning", skew_message))
|
|
1959
|
+
exit_code = (
|
|
1960
|
+
ExitCode.SUCCESS
|
|
1961
|
+
if result.outcome == OUTCOME_CLEAN
|
|
1962
|
+
else ExitCode.VALIDATION_FAILURE
|
|
1963
|
+
)
|
|
1964
|
+
_emit(
|
|
1965
|
+
output_format=output_format,
|
|
1966
|
+
root=root,
|
|
1967
|
+
board_path=board_path,
|
|
1968
|
+
outcome=result.outcome,
|
|
1969
|
+
issues=issues,
|
|
1970
|
+
exit_code=exit_code,
|
|
1971
|
+
command_line=command_line,
|
|
1972
|
+
# tan-cli#1262. `None` -- key absent, not null -- everywhere no
|
|
1973
|
+
# validator returned a status, the offline path included.
|
|
1974
|
+
validator_status=validator_status,
|
|
1975
|
+
# `None` on the offline path (never resolved) -- the two committed
|
|
1976
|
+
# conformance fixtures are offline runs and stay `sdk`-less.
|
|
1977
|
+
sdk=sdk_info,
|
|
1978
|
+
# 1:1 with `issues`, built from the same list above -- the
|
|
1979
|
+
# diagnostic-v1/SARIF documents read the ALP code, hint,
|
|
1980
|
+
# documentation URI and range off these.
|
|
1981
|
+
findings=reported_findings,
|
|
1982
|
+
sarif_base=sarif_base,
|
|
1983
|
+
)
|
|
1984
|
+
except typer.Exit:
|
|
1985
|
+
raise
|
|
1986
|
+
except Exception as err: # never a bare traceback; the envelope is the contract
|
|
1987
|
+
_emit(
|
|
1988
|
+
output_format=output_format,
|
|
1989
|
+
root=root,
|
|
1990
|
+
board_path=board_path,
|
|
1991
|
+
outcome=OUTCOME_FAILED,
|
|
1992
|
+
issues=[
|
|
1993
|
+
Issue(
|
|
1994
|
+
"validate.internal-failure",
|
|
1995
|
+
"error",
|
|
1996
|
+
f"validator failed unexpectedly: {type(err).__name__}: {err}",
|
|
1997
|
+
)
|
|
1998
|
+
],
|
|
1999
|
+
exit_code=ExitCode.INTERNAL_FAILURE,
|
|
2000
|
+
# Plain data by now -- see `cwd_base_uri_or_none`'s docstring.
|
|
2001
|
+
sarif_base=sarif_base,
|
|
2002
|
+
)
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
# tan-cli#261: adds the seven oracle `GlobalArgs` flags this command was
|
|
2006
|
+
# still missing (`--all`/`--ci`/`--no-color`/`--non-interactive`/`--quiet`/
|
|
2007
|
+
# `--target`/`--verbose`) on top of `--board-yaml`, already declared and read
|
|
2008
|
+
# above; see `tan.core.global_flags`.
|
|
2009
|
+
validate = accept_global_flags(validate)
|