@typecad/cuttlefish 1.0.0-alpha.13 → 1.0.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/README.md +82 -79
  2. package/assets/editor-extensions/typecad-debug/README.md +141 -0
  3. package/assets/editor-extensions/typecad-debug/out/extension.js +347 -0
  4. package/assets/editor-extensions/typecad-debug/out/extension.js.map +1 -0
  5. package/assets/editor-extensions/typecad-debug/package.json +76 -0
  6. package/assets/editor-extensions/typecad-ui/LICENSE +27 -0
  7. package/assets/editor-extensions/typecad-ui/README.md +52 -0
  8. package/assets/editor-extensions/typecad-ui/file-icon-dark.png +0 -0
  9. package/assets/editor-extensions/typecad-ui/file-icon-light.png +0 -0
  10. package/assets/editor-extensions/typecad-ui/icon.png +0 -0
  11. package/assets/editor-extensions/typecad-ui/language-configuration.json +43 -0
  12. package/assets/editor-extensions/typecad-ui/package.json +54 -0
  13. package/assets/editor-extensions/typecad-ui/snippets/typecad-ui.json +80 -0
  14. package/assets/editor-extensions/typecad-ui/syntaxes/markdown-ui.json +45 -0
  15. package/assets/editor-extensions/typecad-ui/syntaxes/typecad-ui.tmLanguage.json +1269 -0
  16. package/dist/api/config.d.ts +30 -27
  17. package/dist/api/schema/types.d.ts +20 -3
  18. package/dist/api/shared/async-runtime-static.d.ts +1 -1
  19. package/dist/api/shared/async-runtime-static.js +5 -5
  20. package/dist/api/shared/async-types.d.ts +3 -2
  21. package/dist/api/shared/board-catalog.d.ts +2 -0
  22. package/dist/api/shared/board-catalog.js +6 -0
  23. package/dist/api/shared/board-resolver.d.ts +4 -7
  24. package/dist/api/shared/board-resolver.js +2 -1
  25. package/dist/api/shared/coop-scheduler.d.ts +1 -1
  26. package/dist/api/shared/coop-scheduler.js +1 -1
  27. package/dist/api/shared/cpp-type-ir.d.ts +1 -1
  28. package/dist/api/shared/cpp-type-ir.js +1 -1
  29. package/dist/api/shared/display-profile.d.ts +1 -1
  30. package/dist/api/shared/display-profile.js +3 -5
  31. package/dist/api/shared/framework-manifest-registry.d.ts +1 -1
  32. package/dist/api/shared/framework-manifest-registry.js +0 -1
  33. package/dist/api/shared/framework-manifest.d.ts +65 -507
  34. package/dist/api/shared/framework-manifest.js +6 -11
  35. package/dist/api/shared/hal-op-ir.d.ts +294 -685
  36. package/dist/api/shared/hal-op-ir.js +31 -65
  37. package/dist/api/shared/index.d.ts +0 -4
  38. package/dist/api/shared/index.js +1 -7
  39. package/dist/api/shared/ir-core.d.ts +6 -0
  40. package/dist/api/shared/ir-declarations.d.ts +4 -0
  41. package/dist/api/shared/ir.d.ts +1 -3
  42. package/dist/api/shared/platform-strategy.d.ts +14 -37
  43. package/dist/api/shared/polyfill-helper-registry.js +0 -5
  44. package/dist/api/shared/polyfill-types.js +0 -27
  45. package/dist/api/shared/promise-runtime.js +3 -3
  46. package/dist/api/shared/spdx-licenses.d.ts +12 -10
  47. package/dist/api/shared/spdx-licenses.js +1 -1
  48. package/dist/api/shared/toolchain-types.d.ts +1 -1
  49. package/dist/api/shared/toolchain-types.js +3 -3
  50. package/dist/api/shared/types.d.ts +0 -3
  51. package/dist/api/shared/validate-framework-manifest.js +13 -33
  52. package/dist/board-catalog/dts-reader.d.ts +220 -0
  53. package/dist/board-catalog/dts-reader.js +1052 -0
  54. package/dist/board-catalog/index.d.ts +11 -0
  55. package/dist/board-catalog/index.js +12 -0
  56. package/dist/board-catalog/pinconfig.d.ts +48 -0
  57. package/dist/board-catalog/pinconfig.js +263 -0
  58. package/dist/board-catalog/sdk.d.ts +82 -0
  59. package/dist/board-catalog/sdk.js +180 -0
  60. package/dist/board-catalog/store.d.ts +196 -0
  61. package/dist/board-catalog/store.js +363 -0
  62. package/dist/board-catalog/types.d.ts +233 -0
  63. package/dist/board-catalog/types.js +7 -0
  64. package/dist/board-catalog/walker.d.ts +112 -0
  65. package/dist/board-catalog/walker.js +1101 -0
  66. package/dist/cli-utils.d.ts +2 -2
  67. package/dist/cli-utils.js +5 -5
  68. package/dist/cli.js +227 -244
  69. package/dist/config-loader.d.ts +14 -19
  70. package/dist/config-loader.js +242 -78
  71. package/dist/config-schema.d.ts +19 -227
  72. package/dist/config-schema.js +9 -8
  73. package/dist/contract/board-generator.d.ts +4 -4
  74. package/dist/contract/board-generator.js +16 -5
  75. package/dist/contract/contract-parser.d.ts +36 -127
  76. package/dist/contract/contract-parser.js +76 -0
  77. package/dist/contract/index.d.ts +1 -1
  78. package/dist/contract/index.js +70 -21
  79. package/dist/create/active-board-catalog.d.ts +9 -0
  80. package/dist/create/active-board-catalog.js +94 -0
  81. package/dist/create/board-catalog.generated.d.ts +96 -0
  82. package/dist/create/board-catalog.generated.js +128737 -0
  83. package/dist/create/board-search.d.ts +23 -0
  84. package/dist/create/board-search.js +88 -0
  85. package/dist/create/editor-integration.d.ts +32 -0
  86. package/dist/create/editor-integration.js +257 -0
  87. package/dist/create/eslint-rules-template.d.ts +2 -2
  88. package/dist/create/framework-catalog.d.ts +48 -8
  89. package/dist/create/framework-catalog.js +58 -44
  90. package/dist/create/index.d.ts +8 -13
  91. package/dist/create/index.js +5 -9
  92. package/dist/create/mcu-target.d.ts +39 -0
  93. package/dist/create/mcu-target.js +80 -0
  94. package/dist/create/pack-targets.d.ts +19 -0
  95. package/dist/create/pack-targets.js +56 -0
  96. package/dist/create/{init-scaffold.d.ts → scaffold.d.ts} +8 -11
  97. package/dist/create/{init-scaffold.js → scaffold.js} +28 -87
  98. package/dist/create/templates.d.ts +51 -0
  99. package/dist/create/{init-templates.js → templates.js} +135 -116
  100. package/dist/create/wizard.d.ts +19 -0
  101. package/dist/create/wizard.js +314 -0
  102. package/dist/debug/preprocessor.d.ts +5 -0
  103. package/dist/debug/preprocessor.js +31 -7
  104. package/dist/diagnostics/diagnostics-report.d.ts +1 -1
  105. package/dist/diagnostics/diagnostics-report.js +3 -11
  106. package/dist/diagnostics/json-schema.d.ts +1 -1
  107. package/dist/diagnostics/md-writer.js +2 -2
  108. package/dist/diagnostics/mermaid-builder.d.ts +1 -1
  109. package/dist/diagnostics/mermaid-builder.js +1 -1
  110. package/dist/emit/cpp-emitter.js +4 -1
  111. package/dist/emit/emitters/class-emitter.js +32 -4
  112. package/dist/emit/emitters/emitter-context.d.ts +11 -0
  113. package/dist/emit/emitters/entrypoint-synthesizer.js +1 -1
  114. package/dist/emit/emitters/function-emitter-impl.js +33 -17
  115. package/dist/emit/emitters/output-finalizer.d.ts +7 -0
  116. package/dist/emit/emitters/output-finalizer.js +31 -23
  117. package/dist/emit/emitters/setup.js +147 -63
  118. package/dist/emit/emitters/top-level-prep.js +1 -1
  119. package/dist/emit/emitters/type-decl-emitter.js +13 -8
  120. package/dist/emit/emitters/ui-emitter.js +1 -1
  121. package/dist/emit/expression-renderer.d.ts +3 -3
  122. package/dist/emit/expression-renderer.js +17 -7
  123. package/dist/emit/route-hal-op.js +17 -48
  124. package/dist/emit/snprintf-helpers.d.ts +1 -3
  125. package/dist/emit/snprintf-helpers.js +1 -39
  126. package/dist/emit/statement-renderer.d.ts +0 -4
  127. package/dist/emit/statement-renderer.js +54 -45
  128. package/dist/emit/utils/async-state-machine.d.ts +16 -9
  129. package/dist/emit/utils/async-state-machine.js +56 -54
  130. package/dist/emit/utils/comment-helpers.d.ts +0 -8
  131. package/dist/emit/utils/comment-helpers.js +1 -1
  132. package/dist/emit/utils/cpp-helpers.d.ts +2 -2
  133. package/dist/emit/utils/cpp-helpers.js +2 -2
  134. package/dist/emit/utils/hal-op-cpp-type.js +4 -6
  135. package/dist/emit/utils/index.d.ts +3 -3
  136. package/dist/emit/utils/index.js +3 -3
  137. package/dist/emit/utils/type-inference.d.ts +1 -29
  138. package/dist/emit/utils/type-inference.js +0 -75
  139. package/dist/framework-package.d.ts +3 -0
  140. package/dist/framework-package.js +23 -6
  141. package/dist/framework-registry.d.ts +5 -5
  142. package/dist/frameworks/native/graphics/terminal-preview.d.ts +5 -0
  143. package/dist/frameworks/native/graphics/terminal-preview.js +33 -0
  144. package/dist/frameworks/native/index.d.ts +3 -0
  145. package/dist/frameworks/native/index.js +15 -0
  146. package/dist/frameworks/native/native-compile.d.ts +17 -0
  147. package/dist/frameworks/native/native-compile.js +170 -0
  148. package/dist/frameworks/native/native-config.d.ts +41 -0
  149. package/dist/frameworks/native/native-config.js +7 -0
  150. package/dist/frameworks/native/strategy.d.ts +97 -0
  151. package/dist/frameworks/native/strategy.js +664 -0
  152. package/dist/ir/board-resolver.d.ts +18 -23
  153. package/dist/ir/board-resolver.js +43 -401
  154. package/dist/ir/build-ir-state.d.ts +3 -0
  155. package/dist/ir/build-ir-state.js +42 -0
  156. package/dist/ir/build-ir.d.ts +1 -1
  157. package/dist/ir/build-ir.js +64 -45
  158. package/dist/ir/declaration-builders.js +1 -0
  159. package/dist/ir/expression-to-ir.js +17 -10
  160. package/dist/ir/hal/hal-emitter.d.ts +0 -2
  161. package/dist/ir/hal/hal-emitter.js +92 -41
  162. package/dist/ir/hal/hal-parser.d.ts +5 -5
  163. package/dist/ir/hal/hal-parser.js +128 -36
  164. package/dist/ir/hal/hal-plugins.d.ts +0 -1
  165. package/dist/ir/hal/hal-plugins.js +425 -716
  166. package/dist/ir/heap-analysis.js +2 -41
  167. package/dist/ir/identifier-collector.js +32 -20
  168. package/dist/ir/network-validation.js +7 -11
  169. package/dist/ir/ownership-analysis.js +75 -13
  170. package/dist/ir/peripheral-usage.d.ts +8 -2
  171. package/dist/ir/peripheral-usage.js +78 -149
  172. package/dist/ir/peripheral-validation.js +23 -3
  173. package/dist/ir/pin-capability-validation.d.ts +12 -0
  174. package/dist/ir/pin-capability-validation.js +61 -97
  175. package/dist/ir/pin-mode-validation.js +5 -5
  176. package/dist/ir/program-analysis.d.ts +8 -15
  177. package/dist/ir/program-analysis.js +140 -323
  178. package/dist/ir/pulldown-validation.js +11 -4
  179. package/dist/ir/resource-analysis.js +15 -3
  180. package/dist/ir/statement-to-ir.js +0 -6
  181. package/dist/ir/timing-validation.js +1 -1
  182. package/dist/ir/transformers/array-methods.js +11 -5
  183. package/dist/ir/transformers/call-statement.js +20 -87
  184. package/dist/ir/transformers/callback-context-registry.d.ts +1 -1
  185. package/dist/ir/transformers/callback-context-registry.js +8 -16
  186. package/dist/ir/transformers/control-flow.js +0 -13
  187. package/dist/ir/transformers/expressions.js +10 -9
  188. package/dist/ir/transformers/hal-call-resolver.d.ts +2 -2
  189. package/dist/ir/transformers/hal-call-resolver.js +24 -10
  190. package/dist/ir/transformers/hal-emit-helpers.d.ts +2 -2
  191. package/dist/ir/transformers/hal-emit-helpers.js +2 -2
  192. package/dist/ir/transformers/namespace-methods.js +1 -1
  193. package/dist/ir/transformers/ui-callback-lowering.d.ts +0 -2
  194. package/dist/ir/transformers/ui-callback-lowering.js +0 -24
  195. package/dist/ir/transformers/variables.js +338 -42
  196. package/dist/ir/try-catch-validation.js +4 -3
  197. package/dist/ir/type-resolution.js +15 -4
  198. package/dist/ir/validation-orchestrator.js +0 -9
  199. package/dist/libdef/cpp-to-decl.d.ts +0 -9
  200. package/dist/libdef/cpp-to-decl.js +0 -72
  201. package/dist/libdef/registry.d.ts +0 -1
  202. package/dist/libdef/registry.js +1 -24
  203. package/dist/library/catalog.d.ts +35 -0
  204. package/dist/library/catalog.js +67 -0
  205. package/dist/library/cli.d.ts +2 -0
  206. package/dist/library/cli.js +168 -0
  207. package/dist/library/init.d.ts +25 -0
  208. package/dist/library/init.js +397 -0
  209. package/dist/library/install.d.ts +9 -0
  210. package/dist/library/install.js +80 -0
  211. package/dist/library/registry-search.d.ts +34 -0
  212. package/dist/library/registry-search.js +50 -0
  213. package/dist/library/validate.d.ts +17 -0
  214. package/dist/library/validate.js +175 -0
  215. package/dist/library-packages.d.ts +95 -0
  216. package/dist/library-packages.js +275 -0
  217. package/dist/mapping/peripheral-names.js +9 -4
  218. package/dist/mapping/source-map.d.ts +3 -4
  219. package/dist/mapping/source-map.js +12 -18
  220. package/dist/orchestrator/graph-builder.d.ts +3 -3
  221. package/dist/orchestrator/graph-builder.js +29 -12
  222. package/dist/orchestrator/type-checker.d.ts +3 -3
  223. package/dist/orchestrator/type-checker.js +3 -3
  224. package/dist/platform/async-runtime.js +1 -1
  225. package/dist/platform/coop-scheduler-runtime.js +2 -2
  226. package/dist/platform/generic-debug-codegen.d.ts +36 -0
  227. package/dist/platform/generic-debug-codegen.js +185 -0
  228. package/dist/platform/generic-strategy.d.ts +3 -4
  229. package/dist/platform/generic-strategy.js +5 -80
  230. package/dist/platform/index.js +1 -1
  231. package/dist/platform/registry.d.ts +2 -2
  232. package/dist/platform/registry.js +2 -2
  233. package/dist/preview/client.js +17 -3
  234. package/dist/safety/asil-decorators.d.ts +32 -0
  235. package/dist/safety/asil-decorators.js +32 -0
  236. package/dist/safety/authoring.d.ts +85 -0
  237. package/dist/safety/authoring.js +61 -0
  238. package/dist/safety/engine.d.ts +3 -0
  239. package/dist/safety/engine.js +83 -0
  240. package/dist/safety/hal/ops.d.ts +48 -0
  241. package/dist/safety/hal/ops.js +46 -0
  242. package/dist/safety/iso26262/analyze.d.ts +13 -0
  243. package/dist/safety/iso26262/analyze.js +29 -0
  244. package/dist/safety/iso26262/asil.d.ts +10 -0
  245. package/dist/safety/iso26262/asil.js +22 -0
  246. package/dist/safety/iso26262/collect.d.ts +14 -0
  247. package/dist/safety/iso26262/collect.js +137 -0
  248. package/dist/safety/iso26262/goto-checker.d.ts +10 -0
  249. package/dist/safety/iso26262/goto-checker.js +72 -0
  250. package/dist/safety/iso26262/heap-checker.d.ts +7 -0
  251. package/dist/safety/iso26262/heap-checker.js +57 -0
  252. package/dist/safety/iso26262/init-checker.d.ts +13 -0
  253. package/dist/safety/iso26262/init-checker.js +127 -0
  254. package/dist/safety/iso26262/loop-checker.d.ts +7 -0
  255. package/dist/safety/iso26262/loop-checker.js +86 -0
  256. package/dist/safety/iso26262/recursion-checker.d.ts +9 -0
  257. package/dist/safety/iso26262/recursion-checker.js +134 -0
  258. package/dist/safety/iso26262/sidecar-writer.d.ts +25 -0
  259. package/dist/safety/iso26262/sidecar-writer.js +13 -0
  260. package/dist/safety/passes/pinMode-intercept.d.ts +3 -0
  261. package/dist/safety/passes/pinMode-intercept.js +60 -0
  262. package/dist/safety/runtime/mode-table.d.ts +5 -0
  263. package/dist/safety/runtime/mode-table.js +55 -0
  264. package/dist/safety/runtime/polyfills.d.ts +6 -0
  265. package/dist/safety/runtime/polyfills.js +12 -0
  266. package/dist/safety/runtime/result-struct.d.ts +11 -0
  267. package/dist/safety/runtime/result-struct.js +98 -0
  268. package/dist/safety/runtime/safe-int.d.ts +33 -0
  269. package/dist/safety/runtime/safe-int.js +170 -0
  270. package/dist/safety/runtime/safe-traits.d.ts +4 -0
  271. package/dist/safety/runtime/safe-traits.js +95 -0
  272. package/dist/safety/runtime/safe-variable.d.ts +39 -0
  273. package/dist/safety/runtime/safe-variable.js +226 -0
  274. package/dist/safety/runtime/vote.d.ts +8 -0
  275. package/dist/safety/runtime/vote.js +92 -0
  276. package/dist/safety/runtime/write-verify.d.ts +8 -0
  277. package/dist/safety/runtime/write-verify.js +71 -0
  278. package/dist/safety/safe-int-types.d.ts +45 -0
  279. package/dist/safety/safe-int-types.js +25 -0
  280. package/dist/safety/safe-variable-types.d.ts +29 -0
  281. package/dist/safety/safe-variable-types.js +19 -0
  282. package/dist/safety/safety-bridge.d.ts +5 -6
  283. package/dist/safety/safety-bridge.js +15 -28
  284. package/dist/safety/sidecar-bridge.d.ts +1 -3
  285. package/dist/safety/sidecar-bridge.js +3 -20
  286. package/dist/safety/specifiers.d.ts +6 -0
  287. package/dist/safety/specifiers.js +14 -0
  288. package/dist/safety-hook.d.ts +8 -7
  289. package/dist/safety-hook.js +12 -13
  290. package/dist/testing.d.ts +5 -6
  291. package/dist/testing.js +5 -5
  292. package/dist/transpile/resolution.d.ts +1 -1
  293. package/dist/transpile/resolution.js +18 -9
  294. package/dist/transpile/test-pins.d.ts +18 -0
  295. package/dist/transpile/test-pins.js +142 -0
  296. package/dist/transpile.d.ts +1 -2
  297. package/dist/transpile.js +90 -29
  298. package/dist/types.d.ts +45 -26
  299. package/dist/ui-hook.d.ts +1 -1
  300. package/dist/utils/cli.d.ts +2 -2
  301. package/dist/utils/cli.js +160 -157
  302. package/dist/utils/fs.d.ts +3 -4
  303. package/dist/utils/fs.js +4 -5
  304. package/dist/utils/strings.d.ts +0 -1
  305. package/dist/utils/strings.js +0 -8
  306. package/dist/utils/ui.d.ts +5 -2
  307. package/dist/utils/ui.js +13 -3
  308. package/package.json +158 -149
  309. package/dist/add-preset.d.ts +0 -4
  310. package/dist/add-preset.js +0 -74
  311. package/dist/api/shared/async-symbol-detector.d.ts +0 -11
  312. package/dist/api/shared/async-symbol-detector.js +0 -140
  313. package/dist/api/shared/native-display-op-resolver.d.ts +0 -10
  314. package/dist/api/shared/native-display-op-resolver.js +0 -64
  315. package/dist/create/board-checklist.d.ts +0 -2
  316. package/dist/create/board-checklist.js +0 -52
  317. package/dist/create/board-codegen.d.ts +0 -9
  318. package/dist/create/board-codegen.js +0 -238
  319. package/dist/create/board-generators.d.ts +0 -12
  320. package/dist/create/board-generators.js +0 -651
  321. package/dist/create/board-spec.d.ts +0 -637
  322. package/dist/create/board-spec.js +0 -214
  323. package/dist/create/init-templates.d.ts +0 -28
  324. package/dist/create/init-wizard.d.ts +0 -8
  325. package/dist/create/init-wizard.js +0 -182
  326. package/dist/install/framework-catalog.d.ts +0 -53
  327. package/dist/install/framework-catalog.js +0 -107
  328. package/dist/install/handle-install.d.ts +0 -35
  329. package/dist/install/handle-install.js +0 -177
  330. package/dist/install/index.d.ts +0 -4
  331. package/dist/install/index.js +0 -3
  332. package/dist/ir/pin-state-tracking.d.ts +0 -58
  333. package/dist/ir/pin-state-tracking.js +0 -182
  334. package/dist/ir/pwm-timer-sharing.d.ts +0 -4
  335. package/dist/ir/pwm-timer-sharing.js +0 -94
  336. package/dist/ir/timer0-pwm-timing-conflict.d.ts +0 -4
  337. package/dist/ir/timer0-pwm-timing-conflict.js +0 -72
  338. package/dist/libdef/component-decls.d.ts +0 -2
  339. package/dist/libdef/component-decls.js +0 -6
  340. package/dist/libdef/component-discovery.d.ts +0 -43
  341. package/dist/libdef/component-discovery.js +0 -83
  342. package/dist/theme-tokens.d.ts +0 -22
  343. package/dist/theme-tokens.js +0 -172
@@ -0,0 +1,1101 @@
1
+ // ---------------------------------------------------------------------------
2
+ // catalog-walker.ts — walk a Zephyr tree's boards/ and build the board data
3
+ // catalog (records + provenance).
4
+ //
5
+ // For every board variant yaml that carries an `identifier:` (the qualified
6
+ // west build target) and a same-basename .dts, extract the board-level facts
7
+ // with the tolerant dts-reader. One record per board VARIANT (identifier),
8
+ // keyed by the identifier.
9
+ //
10
+ // This is the ONE walker shared by the catalog producer:
11
+ // - `cuttlefish board sync` regenerates the machine-local catalog
12
+ // overlay from the user's own Zephyr tree, so board add/change/remove
13
+ // tracks `west update` instead of cuttlefish releases. (There is no
14
+ // compiled-in pack anymore — the overlay is the only catalog.)
15
+ //
16
+ // Silicon facts split by where the SoC's data lives: STM32-style per-pad
17
+ // pinctrl routes are harvested from the vendor HAL dtsi the board's include
18
+ // chain reaches (dts-reader.ts); ESP32's C-header matrices (LEDC channel×pad,
19
+ // SARADC) are shape-parsed from the tree here (harvestEspFamilyHeaders); and
20
+ // families whose silicon map has no in-tree source at all (nRF SAADC AIN
21
+ // pads) are synthesized in the manifest generator's family tables — never
22
+ // board-aware code here.
23
+ // ----------------------------------------------------------------------------
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { readBoardDts } from './dts-reader.js';
27
+ import { harvestPinconfig } from './pinconfig.js';
28
+ /** Cut an inline `# comment` — safe inside quoted values (variant yamls
29
+ * carry them, e.g. ai_m61_32s_kit's revision note). */
30
+ function stripYamlComment(value) {
31
+ const q = value[0];
32
+ if (q === '"' || q === "'") {
33
+ const end = value.indexOf(q, 1);
34
+ return end > 0 ? value.slice(0, end + 1) : value;
35
+ }
36
+ const hash = value.indexOf(' #');
37
+ return hash >= 0 ? value.slice(0, hash) : value;
38
+ }
39
+ /** Minimal line yaml reads (identifier:, name:) — full yaml not needed. */
40
+ function yamlField(file, field) {
41
+ const m = fs.readFileSync(file, 'utf8').match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
42
+ return m ? stripYamlComment(m[1]).trim() : undefined;
43
+ }
44
+ /** Read board.yml's board→soc index (see BoardYmlSocIndex). */
45
+ export function boardYmlSocIndex(dir) {
46
+ const yml = path.join(dir, 'board.yml');
47
+ if (!fs.existsSync(yml))
48
+ return { byName: new Map(), uniqueSocs: [], boardNames: [] };
49
+ const byName = new Map();
50
+ const uniqueSocs = [];
51
+ const boardNames = [];
52
+ let inBoardsList = false; // top-level `boards:` present (multi-board format)
53
+ let currentBoard;
54
+ let currentSocs = []; // socs collected for currentBoard
55
+ // A key line with its indent, innermost last. A `- name:` list entry is
56
+ // owned by the topmost key at an indent ≤ the entry's — the tree's
57
+ // board.ymls are not indent-consistent (xiao_ble puts soc entries at the
58
+ // same indent as the socs: key, lyra nests them two deeper), so the
59
+ // enclosing KEY, not the indentation depth, decides ownership.
60
+ const keyStack = [];
61
+ const flushEntry = () => {
62
+ // Only single-soc entries map board name → soc: multi-soc entries need
63
+ // qualified identifiers in their variant yamls.
64
+ if (currentBoard && currentSocs.length === 1)
65
+ byName.set(currentBoard, currentSocs[0]);
66
+ };
67
+ for (const rawLine of fs.readFileSync(yml, 'utf8').split('\n')) {
68
+ const line = rawLine.replace(/\r$/, '');
69
+ const trimmed = line.trim();
70
+ if (!trimmed || trimmed.startsWith('#'))
71
+ continue;
72
+ const indent = line.length - line.trimStart().length;
73
+ const entry = trimmed.match(/^- name:\s*(.+)$/);
74
+ const key = trimmed.match(/^([\w.-]+):(?:\s|$)/);
75
+ if (key) {
76
+ while (keyStack.length > 0 && keyStack[keyStack.length - 1].indent > indent)
77
+ keyStack.pop();
78
+ keyStack.push({ indent, key: key[1] });
79
+ if (indent === 0 && key[1] === 'boards')
80
+ inBoardsList = true;
81
+ continue;
82
+ }
83
+ // Legacy single-board header: `board: xiao_ble` (value on the key line).
84
+ if (indent === 0) {
85
+ const legacyBoard = trimmed.match(/^board:\s*(\S.*)$/);
86
+ if (legacyBoard)
87
+ boardNames.push(stripYamlComment(legacyBoard[1]).trim());
88
+ }
89
+ if (entry) {
90
+ const name = stripYamlComment(entry[1]).trim();
91
+ if (!name)
92
+ continue;
93
+ // Innermost enclosing key that owns a list at this entry's indent.
94
+ let owner;
95
+ for (let i = keyStack.length - 1; i >= 0; i--) {
96
+ if (keyStack[i].indent <= indent) {
97
+ owner = keyStack[i];
98
+ break;
99
+ }
100
+ }
101
+ if (owner?.key === 'boards') {
102
+ // A board entry under boards: — its socs follow under its own
103
+ // socs: key.
104
+ flushEntry();
105
+ currentBoard = name;
106
+ currentSocs = [];
107
+ boardNames.push(name);
108
+ }
109
+ else if (owner?.key === 'socs') {
110
+ if (inBoardsList)
111
+ currentSocs.push(name);
112
+ else
113
+ uniqueSocs.push(name);
114
+ }
115
+ // Every other list (variants:, …) is not ours.
116
+ }
117
+ }
118
+ flushEntry();
119
+ return { byName, uniqueSocs, boardNames };
120
+ }
121
+ /**
122
+ * west runner → probe-method metadata. Ids match the curated soc tables
123
+ * ('stlink', 'dfu', 'jlink', 'bossac', …) so `zephyr.probe` values stay
124
+ * stable across tiers; runners with no curated counterpart use their west
125
+ * runner name as the id (stm32cubeprogrammer, uf2, …). Flash-only tools
126
+ * carry debug: false (a bootloader is not a debugger — the debug path
127
+ * rejects them, resolveProbeMethod enforces it). Simulation-only runners
128
+ * (renode, simics) and debug-server helpers (stlink_gdbserver, trace32)
129
+ * stay unmapped — cuttlefish flashes real hardware.
130
+ */
131
+ const RUNNER_METHODS = {
132
+ 'openocd': { id: 'openocd', description: 'Any SWD/JTAG probe openocd supports', debug: true },
133
+ // Vendor-flavored openocd runner includes — the same openocd runner with
134
+ // vendor defaults (ST: onboard ST-Link; nRF5: nRF tap config). Without
135
+ // these, every ST board (118 in the tree) lost its primary probe method.
136
+ 'openocd-stm32': { id: 'openocd', description: 'ST-Link onboard or any SWD probe openocd supports', debug: true },
137
+ 'openocd-nrf5': { id: 'openocd', description: 'Any SWD/JTAG probe openocd supports (nRF5 defaults)', debug: true },
138
+ 'jlink': { id: 'jlink', description: 'J-Link probe (SWD)', debug: true },
139
+ 'dfu-util': { id: 'dfu', description: 'Built-in USB DFU bootloader: hold BOOT0, tap reset (no debug)', debug: false },
140
+ 'pyocd': { id: 'pyocd', description: 'Any CMSIS-DAP probe via pyOCD (no debug)', debug: false },
141
+ 'blackmagicprobe': { id: 'blackmagicprobe', description: 'Black Magic Probe (no debug)', debug: false },
142
+ 'stm32flash': { id: 'stm32flash', description: 'Built-in UART bootloader (no debug)', debug: false },
143
+ 'bossac': { id: 'bossac', description: 'Built-in USB bootloader: double-tap reset (no debug)', debug: false },
144
+ 'nrfjprog': { id: 'nrfjprog', description: 'Segger nRF command-line flasher (no debug)', debug: false },
145
+ 'esptool': { id: 'esptool', description: 'Espressif ROM bootloader over USB-serial (no debug)', debug: false },
146
+ // The ESP32 family's include name for the esptool runner.
147
+ 'esp32': { id: 'esptool', description: 'Espressif ROM bootloader over USB-serial (no debug)', debug: false },
148
+ 'linkserver': { id: 'linkserver', description: 'NXP LinkServer (no debug)', debug: false },
149
+ 'ezflashcli': { id: 'ezflashcli', description: 'Renesas EZ flash CLI (no debug)', debug: false },
150
+ 'stm32cubeprogrammer': { id: 'stm32cubeprogrammer', description: 'STM32CubeProgrammer CLI via ST-Link (no debug)', debug: false },
151
+ 'nrfutil': { id: 'nrfutil', description: 'nrfutil device — Nordic USB DFU / serial bootloader (no debug)', debug: false },
152
+ 'uf2': { id: 'uf2', description: 'UF2 bootloader: copy the firmware file (no debug)', debug: false },
153
+ 'silabs_commander': { id: 'silabs_commander', description: 'Simplicity Commander CLI (no debug)', debug: false },
154
+ 'probe-rs': { id: 'probe-rs', description: 'probe-rs flasher (no debug)', debug: false },
155
+ 'rfp': { id: 'rfp', description: 'Renesas Flash Programmer CLI (no debug)', debug: false },
156
+ 'bflb_mcu_tool': { id: 'bflb_mcu_tool', description: 'BouffaloLab bflb_mcu_tool (no debug)', debug: false },
157
+ 'xsdb': { id: 'xsdb', description: 'Xilinx xsdb flasher (no debug)', debug: false },
158
+ 'wchisp': { id: 'wchisp', description: 'WCH ISP bootloader (no debug)', debug: false },
159
+ 'spsdk': { id: 'spsdk', description: 'NXP SPSDK blhost (no debug)', debug: false },
160
+ 'minichlink': { id: 'minichlink', description: 'minichlink (WCH CH32, no debug)', debug: false },
161
+ 'mdb-hw': { id: 'mdb-hw', description: 'Synopsys MetaWare Debugger, hardware target (no debug)', debug: false },
162
+ 'wlink': { id: 'wlink', description: 'WCH-Link probe (no debug)', debug: false },
163
+ 'gd32isp': { id: 'gd32isp', description: 'GD32 UART ISP bootloader (no debug)', debug: false },
164
+ 'sftool': { id: 'sftool', description: 'SiFli sftool (no debug)', debug: false },
165
+ 'teensy': { id: 'teensy', description: 'Teensy loader (no debug)', debug: false },
166
+ 'nulink': { id: 'nulink', description: 'Nuvoton NuLink (no debug)', debug: false },
167
+ };
168
+ /**
169
+ * Scan board.cmake line-by-line tracking if/elseif/else/endif, recording
170
+ * every board_runner_args occurrence and every runner include with its
171
+ * condition stack. Variant selection happens later (pickArgs /
172
+ * activeRunners) — a dir shared by several boards guards each variant's
173
+ * runner args behind its own CONFIG_BOARD_<ID>, and boards guard whole
174
+ * RUNNERS behind core-specific configs (variscite's jlink sits inside
175
+ * if(CONFIG_SOC_MIMX8ML8_M7): the A53 target has no jlink at all).
176
+ * Args containing CMake variable references (${CONFIG_SOC}, …) only
177
+ * resolve inside the build — carried verbatim they would poison a real
178
+ * `west flash` invocation, so they are dropped at capture.
179
+ */
180
+ function scanBoardCmake(text) {
181
+ const runnerArgs = new Map();
182
+ const includes = [];
183
+ const conds = [];
184
+ for (const line of text.split('\n')) {
185
+ const branch = line.match(/^\s*(if|elseif|else|endif)\b\s*\(?\s*(.*?)\s*\)?\s*$/);
186
+ if (branch) {
187
+ const kw = branch[1];
188
+ const cond = kw === 'else' ? '' : branch[2];
189
+ if (kw === 'if')
190
+ conds.push(cond);
191
+ else if (kw === 'elseif')
192
+ conds[conds.length - 1] = cond;
193
+ else if (kw === 'else')
194
+ conds[conds.length - 1] = '';
195
+ else if (kw === 'endif')
196
+ conds.pop();
197
+ continue;
198
+ }
199
+ const argsM = line.match(/^\s*board_runner_args\(([\w.-]+)\s+(.*)\)\s*$/);
200
+ if (argsM) {
201
+ const args = [...argsM[2].matchAll(/"([^"]*)"/g)].map((q) => q[1]).filter((q) => !q.includes('${'));
202
+ const list = runnerArgs.get(argsM[1]) ?? [];
203
+ list.push({ conds: [...conds], args });
204
+ runnerArgs.set(argsM[1], list);
205
+ continue;
206
+ }
207
+ const incM = line.match(/^\s*include\(\$\{ZEPHYR_BASE\}\/boards\/common\/([\w.-]+)\.board\.cmake\)/);
208
+ if (incM)
209
+ includes.push({ name: incM[1], conds: [...conds] });
210
+ }
211
+ return { runnerArgs, includes };
212
+ }
213
+ /**
214
+ * Does one condition line apply to THIS target? Empty = always. A
215
+ * CONFIG_BOARD stem must match on a token boundary (a prefix must not
216
+ * match a longer board name); a CONFIG_SOC_<soc> guard matches any
217
+ * variant of that soc, and CONFIG_SOC_<soc>_<X> matches variants whose
218
+ * qualifiers include X.
219
+ */
220
+ function conditionMatchesTarget(cond, boardConfigs, soc, quals) {
221
+ if (!cond)
222
+ return true;
223
+ const cu = cond.toUpperCase();
224
+ for (const cfg of boardConfigs) {
225
+ if (new RegExp(`CONFIG_BOARD_${cfg}(?![A-Za-z0-9_])`).test(cu))
226
+ return true;
227
+ }
228
+ if (soc) {
229
+ const socUpper = soc.toUpperCase();
230
+ if (new RegExp(`CONFIG_SOC_${socUpper}(?![A-Za-z0-9_])`).test(cu))
231
+ return true;
232
+ if (quals.some((q) => cu.includes(`_${q.toUpperCase()}`)) && cu.includes(`CONFIG_SOC_${socUpper}`))
233
+ return true;
234
+ }
235
+ return false;
236
+ }
237
+ /**
238
+ * Pick the occurrence for one variant. Priority:
239
+ * 1. a condition naming one of this target's CONFIG_BOARD_<name> tokens
240
+ * (both Zephyr naming shapes: the board.yml entry name — duo_board_b —
241
+ * and the full underscored target — raytac's …_NRF5340_CPUNET), matched
242
+ * with a token boundary so CONFIG_BOARD_IMX8MM_EVK does not match
243
+ * …_EVK_MIMX8MM6_M4;
244
+ * 2. a CONFIG_SOC_<soc>_… condition mentioning one of the target's
245
+ * qualifier tokens (imx8mm guards its A53 args that way);
246
+ * 3. an unconditional occurrence;
247
+ * 4. the first (documented fallback).
248
+ */
249
+ function pickArgs(list, boardConfigs, soc, quals) {
250
+ for (const cfg of boardConfigs) {
251
+ const boardGuarded = list.find((o) => o.conds.some((c) => conditionMatchesTarget(c, [cfg], undefined, [])));
252
+ if (boardGuarded)
253
+ return boardGuarded.args;
254
+ }
255
+ if (soc && quals.length > 0) {
256
+ const socGuarded = list.find((o) => o.conds.some((c) => c && conditionMatchesTarget(c, [], soc, quals) && /CONFIG_SOC_/i.test(c)));
257
+ if (socGuarded)
258
+ return socGuarded.args;
259
+ }
260
+ const plain = list.find((o) => o.conds.length === 0 || o.conds.every((c) => c === ''));
261
+ if (plain)
262
+ return plain.args;
263
+ return list[0]?.args ?? [];
264
+ }
265
+ export function boardProbeMethods(dir, variant) {
266
+ const cmake = path.join(dir, 'board.cmake');
267
+ if (!fs.existsSync(cmake))
268
+ return undefined;
269
+ const text = fs.readFileSync(cmake, 'utf8');
270
+ const scan = scanBoardCmake(text);
271
+ const boardConfigs = variant?.boardConfigs ?? [];
272
+ const soc = variant?.soc;
273
+ const quals = variant?.quals ?? [];
274
+ // Active runners for THIS variant: unconditional includes always apply;
275
+ // guarded includes apply only when their condition names this target
276
+ // (variscite's jlink is inside if(CONFIG_SOC_…_M7) — the A53 target has
277
+ // no jlink at all, and offering it would flash the wrong core). Only
278
+ // ~24 files in the tree guard every include; a target matching none of
279
+ // a file's guards honestly gets no probe methods.
280
+ let includeNames = scan.includes.map((i) => i.name);
281
+ const anyGuarded = scan.includes.some((i) => i.conds.length > 0 && i.conds.some((c) => c !== ''));
282
+ if (anyGuarded) {
283
+ const active = new Set();
284
+ for (const inc of scan.includes) {
285
+ if (inc.conds.length === 0 || inc.conds.every((c) => c === ''))
286
+ active.add(inc.name);
287
+ else if (inc.conds.some((c) => conditionMatchesTarget(c, boardConfigs, soc, quals)))
288
+ active.add(inc.name);
289
+ }
290
+ includeNames = [...active];
291
+ }
292
+ // First include = west's default runner (preserve scan order).
293
+ const runners = [...new Set(includeNames)];
294
+ if (runners.length === 0)
295
+ return undefined;
296
+ let cfgLines = null;
297
+ const cfgPath = path.join(dir, 'support', 'openocd.cfg');
298
+ if (fs.existsSync(cfgPath)) {
299
+ cfgLines = fs.readFileSync(cfgPath, 'utf8').split('\n').map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith('#'));
300
+ }
301
+ const methods = [];
302
+ for (const runner of runners) {
303
+ // The vendor-flavored openocd includes get the same cfg treatment as
304
+ // plain openocd (stlink interface detection + debugCfg attach).
305
+ if (runner === 'openocd' || runner === 'openocd-stm32' || runner === 'openocd-nrf5') {
306
+ // An ST-Link interface cfg means the method is really 'stlink' — the
307
+ // curated-table id users know. debugCfg carries the whole cfg file,
308
+ // original line order (source/transport/events), comma-free join is
309
+ // not assumed by the consumer.
310
+ const isStlink = cfgLines?.some((l) => l.includes('interface/stlink')) ?? false;
311
+ methods.push({
312
+ id: isStlink ? 'stlink' : 'openocd',
313
+ description: isStlink
314
+ ? 'ST-Link or any SWD probe openocd supports (no BOOT0 needed) — also debugs'
315
+ : 'Any SWD/JTAG probe openocd supports — also debugs',
316
+ runner,
317
+ debug: true,
318
+ ...(cfgLines ? { debugCfg: cfgLines } : {}),
319
+ });
320
+ continue;
321
+ }
322
+ const meta = RUNNER_METHODS[runner];
323
+ if (!meta)
324
+ continue;
325
+ // Per-variant args: the occurrence whose CONFIG_BOARD / CONFIG_SOC
326
+ // guard names THIS variant wins (pickArgs).
327
+ const args = pickArgs(scan.runnerArgs.get(runner) ?? [], boardConfigs, soc, quals);
328
+ if (runner === 'jlink') {
329
+ const device = args.find((a) => a.startsWith('--device='));
330
+ methods.push({
331
+ id: 'jlink',
332
+ description: 'J-Link probe (SWD) — also debugs',
333
+ runner,
334
+ debug: true,
335
+ ...(device ? { debugDevice: device.slice('--device='.length) } : {}),
336
+ });
337
+ continue;
338
+ }
339
+ methods.push({ ...meta, runner, ...(args.length > 0 ? { args: [...args] } : {}) });
340
+ }
341
+ // Dedupe by method id: vendor-flavored includes can coexist with the
342
+ // plain one (openocd + openocd-stm32) — one method per id, first wins.
343
+ const seen = new Set();
344
+ const unique = methods.filter((m) => (seen.has(m.id) ? false : (seen.add(m.id), true)));
345
+ return unique.length > 0 ? unique : undefined;
346
+ }
347
+ /** VERSION file → 'MAJOR.MINOR.PATCH' ('' when unreadable). */
348
+ export function zephyrVersionOf(zephyrBase) {
349
+ try {
350
+ const text = fs.readFileSync(path.join(zephyrBase, 'VERSION'), 'utf8');
351
+ const m = text.match(/VERSION_MAJOR\s*=\s*(\d+)[\s\S]*?VERSION_MINOR\s*=\s*(\d+)/);
352
+ if (!m)
353
+ return '';
354
+ const patch = text.match(/PATCHLEVEL\s*=\s*(\d+)/);
355
+ return patch ? `${m[1]}.${m[2]}.${patch[1]}` : `${m[1]}.${m[2]}`;
356
+ }
357
+ catch {
358
+ return '';
359
+ }
360
+ }
361
+ /**
362
+ * Git HEAD of the tree, read straight off the filesystem (no git spawn —
363
+ * sync runs where git may be absent). Handles the plain clone (.git dir,
364
+ * HEAD → ref → loose ref or packed-refs) and the worktree/submodule form
365
+ * (.git file pointing at a gitdir). Best-effort: undefined when unreadable.
366
+ */
367
+ export function gitHeadOf(zephyrBase) {
368
+ try {
369
+ const dotGit = path.join(zephyrBase, '.git');
370
+ let gitDir = dotGit;
371
+ const st = fs.statSync(dotGit);
372
+ if (st.isFile()) {
373
+ const text = fs.readFileSync(dotGit, 'utf8');
374
+ const m = text.match(/gitdir:\s*(.+)/);
375
+ if (!m)
376
+ return undefined;
377
+ const dir = m[1].trim();
378
+ gitDir = path.isAbsolute(dir) ? dir : path.resolve(zephyrBase, dir);
379
+ }
380
+ const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
381
+ if (!head.startsWith('ref: '))
382
+ return head || undefined;
383
+ const ref = head.slice(5).trim();
384
+ const refFile = path.join(gitDir, ref);
385
+ if (fs.existsSync(refFile))
386
+ return fs.readFileSync(refFile, 'utf8').trim() || undefined;
387
+ const packed = fs.readFileSync(path.join(gitDir, 'packed-refs'), 'utf8');
388
+ const m = packed.match(new RegExp(`^([0-9a-f]{40,}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'));
389
+ return m ? m[1] : undefined;
390
+ }
391
+ catch {
392
+ return undefined;
393
+ }
394
+ }
395
+ /**
396
+ * Parse board.yml into its declared build targets, both shapes:
397
+ * multi-board (`boards:` list) and legacy (`board:` block), with socs and
398
+ * (nested) variants. Variant chains accumulate through nesting levels
399
+ * (board/soc/v1/v2).
400
+ */
401
+ export function boardYmlTargets(dir) {
402
+ const yml = path.join(dir, 'board.yml');
403
+ if (!fs.existsSync(yml))
404
+ return [];
405
+ const targets = [];
406
+ const boards = [];
407
+ let board;
408
+ let soc;
409
+ const keyStack = [];
410
+ const chainBases = [];
411
+ let chain = [];
412
+ const flushSoc = () => {
413
+ if (board && soc) {
414
+ board.socs.set(soc.name, soc.chains);
415
+ soc = undefined;
416
+ chain = [];
417
+ chainBases.length = 0;
418
+ }
419
+ };
420
+ const flushBoard = () => {
421
+ flushSoc();
422
+ board = undefined;
423
+ };
424
+ for (const rawLine of fs.readFileSync(yml, 'utf8').split('\n')) {
425
+ const line = rawLine.replace(/\r$/, '');
426
+ const trimmed = line.trim();
427
+ if (!trimmed || trimmed.startsWith('#'))
428
+ continue;
429
+ const indent = line.length - line.trimStart().length;
430
+ const entry = trimmed.match(/^- name:\s*(.+)$/);
431
+ const key = trimmed.match(/^([\w.-]+):(?:\s+(.*))?$/);
432
+ if (key && !trimmed.startsWith('-')) {
433
+ while (keyStack.length > 0 && keyStack[keyStack.length - 1].indent > indent)
434
+ keyStack.pop();
435
+ const k = key[1];
436
+ const v = (key[2] ?? '').trim();
437
+ if (indent === 0 && k === 'boards') {
438
+ flushBoard();
439
+ }
440
+ else if (indent === 0 && k === 'board' && v) {
441
+ // Legacy inline form: board: xiao_ble
442
+ flushBoard();
443
+ board = { name: stripYamlComment(v).trim(), socs: new Map() };
444
+ boards.push(board);
445
+ }
446
+ else if (k === 'variants') {
447
+ // Chains under this level accumulate from the enclosing chain.
448
+ chainBases.push([...chain]);
449
+ }
450
+ else if ((k === 'name' || k === 'full_name' || k === 'vendor') && v) {
451
+ const val = stripYamlComment(v).trim();
452
+ // A name: under a legacy board: block names the board.
453
+ if (k === 'name' && !board && keyStack.some((s) => s.key === 'board')) {
454
+ board = { name: val, socs: new Map() };
455
+ boards.push(board);
456
+ }
457
+ else if (k !== 'name' && board && !soc) {
458
+ if (k === 'full_name')
459
+ board.fullName = val;
460
+ if (k === 'vendor')
461
+ board.vendor = val;
462
+ }
463
+ }
464
+ keyStack.push({ indent, key: k });
465
+ continue;
466
+ }
467
+ if (entry) {
468
+ const name = stripYamlComment(entry[1]).trim().replace(/^['"]|['"]$/g, '');
469
+ if (!name)
470
+ continue;
471
+ let owner;
472
+ for (let i = keyStack.length - 1; i >= 0; i--) {
473
+ if (keyStack[i].indent <= indent) {
474
+ owner = keyStack[i].key;
475
+ break;
476
+ }
477
+ }
478
+ if (owner === 'boards') {
479
+ flushBoard();
480
+ board = { name, socs: new Map() };
481
+ boards.push(board);
482
+ }
483
+ else if (owner === 'socs') {
484
+ flushSoc();
485
+ soc = { name, chains: [] };
486
+ }
487
+ else if (owner === 'variants') {
488
+ const base = chainBases[chainBases.length - 1] ?? [];
489
+ chain = [...base, name];
490
+ soc?.chains.push([...chain]);
491
+ }
492
+ }
493
+ }
494
+ flushBoard();
495
+ for (const b of boards) {
496
+ for (const [socName, chains] of b.socs) {
497
+ targets.push({ identifier: `${b.name}/${socName}`, boardName: b.name, soc: socName, chain: [], ...(b.fullName ? { fullName: b.fullName } : {}), ...(b.vendor ? { vendor: b.vendor } : {}) });
498
+ for (const c of chains) {
499
+ targets.push({ identifier: [b.name, socName, ...c].join('/'), boardName: b.name, soc: socName, chain: c, ...(b.fullName ? { fullName: b.fullName } : {}), ...(b.vendor ? { vendor: b.vendor } : {}) });
500
+ }
501
+ }
502
+ }
503
+ return targets;
504
+ }
505
+ /** Compose the catalog record from one DTS's extracted facts. The canonical
506
+ * LED/button picks follow the DEVICETREE numbering, not child order: led0
507
+ * when aliased (mm_swiftio declares led0 = green_led as the SECOND child —
508
+ * picking the first aliased child made the manifest's LED0 contradict the
509
+ * devicetree's led0). Same for sw0. Extras then carry the remaining
510
+ * aliased nodes and the LED<N>/BUTTON<N> indices line up with dtSpec
511
+ * numbering. */
512
+ // ── ESP32 family header harvest ────────────────────────────────────────────
513
+ // ESP32 SoCs publish their silicon matrices as C headers, not pinctrl dtsi:
514
+ // the LEDC channel×pad macros in <zephyr>/include/zephyr/dt-bindings/pinctrl/
515
+ // <soc>-pinctrl.h (LEDC_CH<ch>_GPIO<pin> — every pair) and the SARADC map in
516
+ // the HAL module's components/soc/<soc>/include/soc/adc_channel.h
517
+ // (ADC<unit>_GPIO<pad>_CHANNEL <ch>). Both are shape-parsed; the file paths
518
+ // key on the target's soc segment (a data convention, like the letter-port
519
+ // families), never on board names.
520
+ function harvestEspFamilyHeaders(identifier, facts, zephyrBase, westRoot) {
521
+ const soc = identifier.split('/')[1];
522
+ if (!soc || !/^(esp32|esp32c)/.test(soc))
523
+ return {};
524
+ const out = {};
525
+ try {
526
+ const pinH = path.join(zephyrBase, 'include', 'zephyr', 'dt-bindings', 'pinctrl', soc + '-pinctrl.h');
527
+ if (fs.existsSync(pinH)) {
528
+ const src = fs.readFileSync(pinH, 'utf8');
529
+ const pads = new Set();
530
+ let maxCh = -1;
531
+ for (const m of src.matchAll(/#define LEDC_CH(\d+)_GPIO(\d+)\b/g)) {
532
+ pads.add(Number(m[2]));
533
+ maxCh = Math.max(maxCh, Number(m[1]));
534
+ }
535
+ if (pads.size > 0 && maxCh >= 0) {
536
+ out.pwmMatrix = { controller: facts.ledcNode ?? 'ledc0', channelCount: maxCh + 1, pads: [...pads].sort((a, b) => a - b) };
537
+ }
538
+ }
539
+ }
540
+ catch { /* header absent — no matrix facts */ }
541
+ try {
542
+ const adcH = path.join(westRoot, 'modules', 'hal', 'espressif', 'components', 'soc', soc, 'include', 'soc', 'adc_channel.h');
543
+ if (fs.existsSync(adcH)) {
544
+ const src = fs.readFileSync(adcH, 'utf8');
545
+ const routes = [];
546
+ for (const m of src.matchAll(/#define ADC(\d+)_GPIO(\d+)_CHANNEL\s+(\d+)/g)) {
547
+ // DT nodelabels: adc0 carries unit 1, adc1 unit 2.
548
+ routes.push({ source: 'adc' + (Number(m[1]) - 1), pad: Number(m[2]), channel: Number(m[3]) });
549
+ }
550
+ if (routes.length > 0)
551
+ out.espAdc = routes;
552
+ }
553
+ }
554
+ catch { /* header absent — no adc facts */ }
555
+ return out;
556
+ }
557
+ // ── RP2 header matrices ─────────────────────────────────────────────────────
558
+ // The RP2040/RP2350 publish their silicon routing as pinmux macros in
559
+ // include/zephyr/dt-bindings/pinctrl/rpi-pico-*-pinctrl.h: the ADC map
560
+ // (ADC_CH<n>_P<pad>, in the SoC headers — channels 0-3 = P26-P29 on BOTH
561
+ // SoCs) and the PWM slice map (PWM_<slice><A|B>_P<pad>, in the shared
562
+ // common header — every GPIO is PWM-capable, slice = pad/2, A/B = pad%2).
563
+ // The macro token IS the overlay's pinmux payload: the RP2 ADC and PWM
564
+ // drivers both apply pinctrl, so routes carry it for group synthesis.
565
+ function rp2PinctrlHeaderFile(soc) {
566
+ if (soc === 'rp2040')
567
+ return ['rpi-pico-rp2040-pinctrl.h', 'rpi-pico-pinctrl-common.h'];
568
+ if (soc === 'rp2350a')
569
+ return ['rpi-pico-rp2350a-pinctrl.h', 'rpi-pico-pinctrl-common.h'];
570
+ if (soc === 'rp2350b')
571
+ return ['rpi-pico-rp2350b-pinctrl.h', 'rpi-pico-pinctrl-common.h'];
572
+ return undefined;
573
+ }
574
+ function harvestRp2PinctrlHeaders(identifier, zephyrBase) {
575
+ const soc = identifier.split('/')[1];
576
+ const files = soc ? rp2PinctrlHeaderFile(soc) : undefined;
577
+ if (!files)
578
+ return {};
579
+ const adc = new Map();
580
+ const pwm = new Map();
581
+ for (const f of files) {
582
+ let src;
583
+ try {
584
+ src = fs.readFileSync(path.join(zephyrBase, 'include', 'zephyr', 'dt-bindings', 'pinctrl', f), 'utf8');
585
+ }
586
+ catch {
587
+ continue;
588
+ }
589
+ for (const m of src.matchAll(/#define (ADC_CH(\d+)_P(\d+))\b/g)) {
590
+ adc.set(m[1], { source: 'adc', channel: Number(m[2]), pad: Number(m[3]), pinctrl: m[1] });
591
+ }
592
+ for (const m of src.matchAll(/#define (PWM_(\d+)([AB])_P(\d+))\b/g)) {
593
+ // The pwm-rp2 driver decodes its channel cell as slice*2 + (B?1:0).
594
+ pwm.set(m[1], { source: 'pwm', channel: Number(m[2]) * 2 + (m[3] === 'B' ? 1 : 0), pad: Number(m[4]), pinctrl: m[1] });
595
+ }
596
+ }
597
+ return {
598
+ ...(adc.size > 0 ? { padAdc: [...adc.values()].sort((a, b) => a.channel - b.channel) } : {}),
599
+ ...(pwm.size > 0 ? { padPwm: [...pwm.values()].sort((a, b) => a.pad - b.pad) } : {}),
600
+ };
601
+ }
602
+ // ── Vendor pinconfig YAMLs (GD32 / Atmel / Bouffalolab) ─────────────────────
603
+ // The datasheet-sourced pin tables under each HAL module's `pinconfigs/`
604
+ // directory — see board-catalog/pinconfig.ts. ADC + (Atmel) DAC routes for
605
+ // now; the walker's coverage ledger records the families whose PWM pinmux
606
+ // synthesis is still pending instead of shipping false capability flags.
607
+ function composeRecord(base, facts, zephyrBase, westRoot) {
608
+ const esp = harvestEspFamilyHeaders(base.identifier, facts, zephyrBase, westRoot);
609
+ const rp2 = harvestRp2PinctrlHeaders(base.identifier, zephyrBase);
610
+ const pinconfig = harvestPinconfig(base.identifier, westRoot);
611
+ const adcRoutes = [...facts.adcPins, ...(pinconfig?.adc ?? [])];
612
+ // SAM DAC needs no pinmux group (the driver selects the output pad), so the
613
+ // pinconfig DAC route carries no pinctrl token — an empty token keeps it out
614
+ // of the overlay's pinctrl-0 emission, exactly like the ESP32 DAC synthesis.
615
+ const dacRoutes = [...facts.dacPins, ...(pinconfig?.dac ?? []).map((d) => ({ ...d, pinctrl: '' }))];
616
+ // SAM PWM routes carry the synthesized WO pinmux macro token (pinctrl), so
617
+ // they join the pwmPins pipeline verbatim — the overlay synthesizes the
618
+ // pad group from the macro like any other macro-form route.
619
+ const pwmRoutes = [...facts.pwmPins, ...(pinconfig?.pwm ?? [])];
620
+ const led = facts.leds.find((l) => l.alias === 'led0') ?? facts.leds.find((l) => l.alias);
621
+ const button = facts.buttons.find((b) => b.alias === 'sw0') ?? facts.buttons.find((b) => b.alias);
622
+ const rec = {
623
+ ...base,
624
+ console: facts.chosen['zephyr,console'],
625
+ // Addressable user LED (ws2812) only when no gpio-leds LED exists —
626
+ // a board with both keeps its devicetree-chosen led0.
627
+ ...(facts.stripLed && !led ? { stripLed: { controller: facts.stripLed.controller, pin: facts.stripLed.pin } } : {}),
628
+ ...(facts.usbDevice ? { usbDevice: facts.usbDevice } : {}),
629
+ ...(facts.usbController ? { usbController: facts.usbController } : {}),
630
+ // Board-level watchdog0 alias wins; boards that never wrote one fall
631
+ // back to the SoC-level watchdog node (nearly every SoC ships IWDG —
632
+ // the alias is authorial habit, not a hardware difference).
633
+ ...(facts.aliases['watchdog0'] || facts.wdtSocNode
634
+ ? { wdtNodeLabel: facts.aliases['watchdog0'] ?? facts.wdtSocNode }
635
+ : {}),
636
+ ...(facts.buses.i2c.length > 0 || facts.buses.spi.length > 0 || facts.buses.uart.length > 0
637
+ ? { buses: facts.buses }
638
+ : {}),
639
+ ...(facts.pwmLeds.length > 0
640
+ ? { pwmLeds: facts.pwmLeds.map((p) => ({
641
+ ...(p.alias ? { alias: p.alias } : {}),
642
+ controller: p.controller, channel: p.channel,
643
+ ...(p.periodNs != null ? { periodNs: p.periodNs } : {}),
644
+ ...(p.flags && p.flags.length > 0 ? { flags: [...p.flags] } : {}),
645
+ })) }
646
+ : {}),
647
+ // Silicon PWM/analog routes harvested from the SoC pinctrl files in the
648
+ // board's include chain (raw port/bit form — the manifest generator maps
649
+ // to global pin numbers and applies nodelabel conventions).
650
+ ...(pwmRoutes.length > 0 ? { pwmPins: pwmRoutes } : {}),
651
+ ...(adcRoutes.length > 0 ? { adcPins: adcRoutes } : {}),
652
+ ...(dacRoutes.length > 0 ? { dacPins: dacRoutes } : {}),
653
+ ...(facts.analogDevices.length > 0 ? { analogDevices: facts.analogDevices } : {}),
654
+ ...(esp.pwmMatrix ? { pwmMatrix: esp.pwmMatrix } : {}),
655
+ ...(facts.pwmNodes && facts.pwmNodes.length > 0 ? { pwmNodes: facts.pwmNodes } : {}),
656
+ ...(rp2.padAdc ? { padAdc: rp2.padAdc } : {}),
657
+ ...(rp2.padPwm ? { padPwm: rp2.padPwm } : {}),
658
+ ...(facts.connectorAdc.length > 0 ? { connectorAdc: facts.connectorAdc } : {}),
659
+ ...(facts.pinctrlWarnings && facts.pinctrlWarnings.length > 0 ? { pinctrlWarnings: facts.pinctrlWarnings } : {}),
660
+ ...(facts.flashKb ? { flashKb: facts.flashKb } : {}),
661
+ ...(facts.hasStoragePartition ? { hasStoragePartition: true } : {}),
662
+ ...(facts.storageReg ? { storageReg: facts.storageReg } : {}),
663
+ ...(facts.counterNodes.length > 0 ? { counterNodes: facts.counterNodes } : {}),
664
+ ...(facts.gpioControllers.length > 0 ? { gpioControllers: facts.gpioControllers } : {}),
665
+ ...(esp.espAdc ? { espAdc: esp.espAdc } : {}),
666
+ ...(led ? { led: { dtSpec: led.alias, controller: led.controller, pin: led.pin, flags: led.flags } } : {}),
667
+ ...(button ? { button: { dtSpec: button.alias, controller: button.controller, pin: button.pin, flags: button.flags } } : {}),
668
+ ...(facts.leds.length > 1 || facts.buttons.length > 1
669
+ ? {
670
+ extraLeds: facts.leds.filter((l) => l !== led && l.alias).map((l) => ({ dtSpec: l.alias, controller: l.controller, pin: l.pin, flags: l.flags })),
671
+ extraButtons: facts.buttons.filter((b) => b !== button && b.alias).map((b) => ({ dtSpec: b.alias, controller: b.controller, pin: b.pin, flags: b.flags })),
672
+ }
673
+ : {}),
674
+ ...(facts.connectors.filter((c) => Object.keys(c.pins).length > 0).length > 0
675
+ ? { connectors: facts.connectors.filter((c) => Object.keys(c.pins).length > 0).map((c) => ({ nodelabel: c.nodelabel, compatible: c.compatible, pins: c.pins })) }
676
+ : {}),
677
+ };
678
+ // prune empty arrays
679
+ for (const k of ['extraLeds', 'extraButtons'])
680
+ if (rec[k]?.length === 0)
681
+ delete rec[k];
682
+ // Coverage ledger: which source satisfied each capability (see
683
+ // BoardDataEntry.siliconSources). nRF SAADC's pad map is synthesized in
684
+ // boardgen (family table) — the walker records pwmNodes (the nRF psel
685
+ // matrix) as 'family'; SAADC itself is only boardgen-visible.
686
+ const siliconSources = {};
687
+ if (pinconfig?.adc.length)
688
+ siliconSources.adc = 'pinconfig';
689
+ else if (facts.adcPins.length)
690
+ siliconSources.adc = 'pinctrl';
691
+ else if (esp.espAdc?.length)
692
+ siliconSources.adc = 'header';
693
+ else if (rp2.padAdc?.length)
694
+ siliconSources.adc = 'header';
695
+ else if (facts.connectorAdc.length)
696
+ siliconSources.adc = 'connector';
697
+ if (facts.pwmPins.length)
698
+ siliconSources.pwm = 'pinctrl';
699
+ else if (pinconfig?.pwm?.length)
700
+ siliconSources.pwm = 'pinconfig';
701
+ else if (esp.pwmMatrix)
702
+ siliconSources.pwm = 'header';
703
+ else if (rp2.padPwm?.length)
704
+ siliconSources.pwm = 'header';
705
+ else if (facts.pwmNodes?.length)
706
+ siliconSources.pwm = 'family';
707
+ if (facts.dacPins.length)
708
+ siliconSources.dac = 'pinctrl';
709
+ else if (pinconfig?.dac?.length)
710
+ siliconSources.dac = 'pinconfig';
711
+ if (Object.keys(siliconSources).length > 0)
712
+ rec.siliconSources = siliconSources;
713
+ const hasFacts = Boolean(rec.console || rec.led || rec.button || rec.connectors);
714
+ return { record: rec, hasFacts };
715
+ }
716
+ /** Probe-method pin for one identifier (CONFIG_BOARD stems + soc quals). */
717
+ function variantPinFor(identifier, socIndex) {
718
+ const parts = identifier.split('/');
719
+ const underscored = identifier.replace(/\//g, '_');
720
+ const entry = socIndex.boardNames.find((n) => identifier === n || identifier.startsWith(n + '/'));
721
+ return {
722
+ boardConfigs: [...new Set([...(entry ? [entry] : []), underscored])]
723
+ .map((n) => n.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()),
724
+ ...(parts[1] ? { soc: parts[1] } : {}),
725
+ ...(parts.length > 2 ? { quals: parts.slice(2) } : {}),
726
+ };
727
+ }
728
+ /** Candidate .dts files for one identifier, best match first: the yaml's own
729
+ * basename, then progressively de-qualified shared forms — an @revision
730
+ * qualifier is stripped (nrf9160dk@0.7.0/nrf9160 → nrf9160dk/nrf9160) and
731
+ * trailing segments are dropped one at a time (…/nrf9160/ns shares the base
732
+ * nrf9160dk_nrf9160.dts). Boards that ship ONLY revision-qualified yamls
733
+ * (nrf9160dk) reach their base dts exclusively through these fallbacks. */
734
+ function dtsCandidatesFor(dir, identifier, yamlBase) {
735
+ const out = [];
736
+ if (yamlBase)
737
+ out.push(path.join(dir, yamlBase + '.dts'));
738
+ const segments = identifier.split('/');
739
+ segments[0] = segments[0].split('@')[0];
740
+ for (let n = segments.length; n >= 1; n--) {
741
+ const c = path.join(dir, segments.slice(0, n).join('_') + '.dts');
742
+ if (!out.includes(c))
743
+ out.push(c);
744
+ }
745
+ return out;
746
+ }
747
+ /**
748
+ * Board-dir overlays (user-facing `<board>*.overlay` files) document the
749
+ * analog header wiring the base DTS leaves out — Renesas RA boards carry
750
+ * BOTH the connector gpio-map and the io-channel-map ONLY there (joined by
751
+ * the shared child-spec token, e.g. ARDUINO_HEADER_R3_A0). Same trust level
752
+ * as the DK dtsi maps: board-authored wiring. Two joins: the overlay's own
753
+ * gpio-map (token → pad), then the base-DTS connectors' labels (comment
754
+ * token first, Arduino `A<idx>` position convention for numeric specs).
755
+ */
756
+ function overlayConnectorAdc(dir, files, connectors) {
757
+ const out = [];
758
+ for (const f of files) {
759
+ if (!f.endsWith('.overlay'))
760
+ continue;
761
+ let text;
762
+ try {
763
+ text = fs.readFileSync(path.join(dir, f), 'utf8');
764
+ }
765
+ catch {
766
+ continue;
767
+ }
768
+ const map = text.match(/io-channel-map\s*=([^;]*);/s);
769
+ if (!map)
770
+ continue;
771
+ // The overlay's own gpio-map: <child-spec 0 &ctrl PIN flags> → pad.
772
+ // Child specs are tokens (macros like ARDUINO_HEADER_R3_A0 or indexes).
773
+ const overlayPads = new Map();
774
+ const gpiomap = text.match(/gpio-map\s*=([^;]*);/s);
775
+ if (gpiomap) {
776
+ for (const g of gpiomap[1].matchAll(/<([\w]+)\s+\d+\s+&([\w,-]+)\s+(\d+)\s+\d+>/g)) {
777
+ if (!overlayPads.has(g[1]))
778
+ overlayPads.set(g[1], { controller: g[2], pin: Number(g[3]) });
779
+ }
780
+ }
781
+ for (const m of map[1].matchAll(/<([\w]+)\s+&([\w,-]+)\s+(\d+)\s*>[\s]*(?:\/\*\s*([A-Za-z_][\w-]*)?[^*]*\*\/)?/g)) {
782
+ const channel = Number(m[3]);
783
+ if (!Number.isFinite(channel))
784
+ continue;
785
+ const token = m[1];
786
+ // Join 1: the same token in the overlay's gpio-map.
787
+ const pad = overlayPads.get(token);
788
+ if (pad) {
789
+ out.push({ source: m[2], channel, controller: pad.controller, pin: pad.pin });
790
+ continue;
791
+ }
792
+ // Join 2: base-DTS connector labels — the comment's first token, else
793
+ // the Arduino uno-adc position convention (numeric index N = A<N>).
794
+ const idx = Number(token);
795
+ const labels = [
796
+ ...(m[4] ? [m[4]] : []),
797
+ ...(Number.isInteger(idx) && idx >= 0 && idx <= 7 ? [`A${idx}`] : []),
798
+ ];
799
+ for (const c of connectors) {
800
+ for (const label of labels) {
801
+ const ref = c.pins[label];
802
+ if (ref) {
803
+ out.push({ source: m[2], channel, controller: ref.controller, pin: ref.pin });
804
+ break;
805
+ }
806
+ }
807
+ }
808
+ }
809
+ }
810
+ return out;
811
+ }
812
+ /**
813
+ * Walk `<zephyrBase>/boards` and extract one record per board variant.
814
+ * Reader failures degrade to the base record (identifier/name/vendor/dts +
815
+ * probe methods) — a board whose DTS defeats the reader still resolves, it
816
+ * just carries no pin facts.
817
+ */
818
+ // Extraction revision history (GENERATOR_REV lives in store.ts):
819
+ // 1 initial; 2 multi-board board.yml + first-wins runner args + comma
820
+ // labels; 3 vendor runner aliases; 4 comment-before-property; 5 duplicate
821
+ // runner dedupe; 6 full flasher coverage + macro-arg filter; 7 per-variant
822
+ // guard selection; 8 board.yml-synthesized targets; 9 bus/usb/wdt facts +
823
+ // the move into cuttlefish (the compiled-in pack is gone); 10 silicon
824
+ // pinctrl harvest (STM32 tim/adc routes via module dts roots); 11 dac
825
+ // routes + SoC-level watchdog fallback + zephyr arch dts roots; 12
826
+ // pinconfigs YAML ADC + gpio-controller inventory + siliconSources ledger.
827
+ export function walkBoardCatalog(zephyrBase) {
828
+ const boardsRoot = path.join(zephyrBase, 'boards');
829
+ if (!fs.existsSync(boardsRoot)) {
830
+ throw new Error(`no boards/ directory at ${boardsRoot} — not a Zephyr tree?`);
831
+ }
832
+ // Vendor HAL module dts roots (Zephyr's MODULE_DTS_ROOTS, mirrored): the
833
+ // west workspace keeps modules/ beside the zephyr checkout, and the STM32
834
+ // pinctrl dtsi the board DTS includes (<st/f4/…-pinctrl.dtsi>) resolves
835
+ // from modules/hal/stm32/dts. Discovered, not hardcoded — a tree without
836
+ // modules simply yields no silicon pinctrl facts.
837
+ const westRoot = path.dirname(zephyrBase);
838
+ const moduleDtsRoots = [];
839
+ const discoverDtsRoots = (parent) => {
840
+ try {
841
+ for (const d of fs.readdirSync(parent, { withFileTypes: true })) {
842
+ if (!d.isDirectory())
843
+ continue;
844
+ const candidate = path.join(parent, d.name, 'dts');
845
+ if (fs.existsSync(candidate))
846
+ moduleDtsRoots.push(candidate);
847
+ // Zephyr module dts roots also expose `include/` for angle includes
848
+ // (`<dt-bindings/pinctrl/…>` — GigaDevice ships its per-part pinctrl
849
+ // headers there, not under dts/).
850
+ const includeRoot = path.join(parent, d.name, 'include');
851
+ if (fs.existsSync(path.join(includeRoot, 'dt-bindings')))
852
+ moduleDtsRoots.push(includeRoot);
853
+ }
854
+ }
855
+ catch {
856
+ // no such parent — plain zephyr checkout without a west workspace
857
+ }
858
+ };
859
+ discoverDtsRoots(path.join(westRoot, 'modules'));
860
+ discoverDtsRoots(path.join(westRoot, 'modules', 'hal'));
861
+ // The zephyr tree's own arch dts roots (dts/arm, dts/xtensa, …): board
862
+ // DTS files include their SoC dtsi through them (`<st/f4/…Xe.dtsi>`),
863
+ // which is where SoC-level nodes (the STM32 iwdg watchdog) live.
864
+ try {
865
+ const dtsRoot = path.join(zephyrBase, 'dts');
866
+ for (const d of fs.readdirSync(dtsRoot, { withFileTypes: true })) {
867
+ if (d.isDirectory())
868
+ moduleDtsRoots.push(path.join(dtsRoot, d.name));
869
+ }
870
+ }
871
+ catch {
872
+ // no dts/ dir — not a full checkout
873
+ }
874
+ const records = [];
875
+ let variants = 0, withFacts = 0, failures = 0, droppedYamls = 0;
876
+ const vendors = fs.readdirSync(boardsRoot, { withFileTypes: true })
877
+ .filter((d) => d.isDirectory());
878
+ for (const vendorDir of vendors) {
879
+ const boardDirs = fs.readdirSync(path.join(boardsRoot, vendorDir.name), { withFileTypes: true })
880
+ .filter((d) => d.isDirectory());
881
+ for (const boardDir of boardDirs) {
882
+ const dir = path.join(boardsRoot, vendorDir.name, boardDir.name);
883
+ const files = fs.readdirSync(dir);
884
+ const socIndex = boardYmlSocIndex(dir);
885
+ const seenIds = new Set();
886
+ for (const f of files) {
887
+ if (!f.endsWith('.yaml') || f === 'board.yml')
888
+ continue;
889
+ const raw = yamlField(path.join(dir, f), 'identifier');
890
+ if (!raw)
891
+ continue;
892
+ // Qualify: bare identifiers take the soc segment from board.yml —
893
+ // their own entry's soc in multi-board dirs, the sole soc in
894
+ // single-board dirs.
895
+ const identifier = raw.includes('/')
896
+ ? raw
897
+ : (() => {
898
+ const soc = socIndex.byName.get(raw)
899
+ ?? (socIndex.uniqueSocs.length === 1 ? socIndex.uniqueSocs[0] : null);
900
+ return soc ? `${raw}/${soc}` : null;
901
+ })();
902
+ if (!identifier || seenIds.has(identifier))
903
+ continue;
904
+ // Resolve the dts BEFORE claiming the identifier: a yaml whose
905
+ // same-basename dts is missing falls back to the board's shared base
906
+ // dts (revision-qualified yamls — nrf9160dk ships only
907
+ // *_0_14_0.yaml over nrf9160dk_nrf9160.dts). Marking seenIds first
908
+ // made such yamls vanish AND blocked the board.yml synthesis pass
909
+ // from covering the identifier — whole boards (nrf9160dk, the
910
+ // mt81xx ADSP targets) dropped out of the catalog silently.
911
+ const dtsFile = dtsCandidatesFor(dir, identifier, f.replace(/\.yaml$/, ''))
912
+ .find((p) => fs.existsSync(p));
913
+ if (!dtsFile) {
914
+ droppedYamls++;
915
+ continue;
916
+ }
917
+ variants++;
918
+ seenIds.add(identifier);
919
+ // Per-variant: runner args are selected by the variant's own
920
+ // CONFIG_BOARD guard, so this must run per yaml, not per dir.
921
+ const probeMethods = boardProbeMethods(dir, variantPinFor(identifier, socIndex));
922
+ const base = {
923
+ identifier,
924
+ name: yamlField(path.join(dir, f), 'name') ?? identifier,
925
+ vendor: vendorDir.name,
926
+ dts: path.basename(dtsFile),
927
+ ...(probeMethods ? { probeMethods } : {}),
928
+ };
929
+ try {
930
+ let facts = readBoardDts(dtsFile, { zephyrBoardsRoot: boardsRoot, moduleDtsRoots });
931
+ {
932
+ // Overlay wiring appended, deduped by (source, channel, pad) —
933
+ // sibling variant overlays (uno_r4 minima/wifi) repeat entries.
934
+ const seen = new Set(facts.connectorAdc.map((r) => `${r.source}:${r.channel}:${r.controller}.${r.pin}`));
935
+ const extra = overlayConnectorAdc(dir, files, facts.connectors)
936
+ .filter((r) => !seen.has(`${r.source}:${r.channel}:${r.controller}.${r.pin}`));
937
+ facts = { ...facts, connectorAdc: [...facts.connectorAdc, ...extra] };
938
+ }
939
+ const { record, hasFacts } = composeRecord(base, facts, zephyrBase, westRoot);
940
+ if (hasFacts)
941
+ withFacts++;
942
+ records.push(record);
943
+ }
944
+ catch (err) {
945
+ failures++;
946
+ if (failures <= 5)
947
+ console.error(`reader failed on ${identifier}: ${err.message}`);
948
+ records.push(base);
949
+ }
950
+ }
951
+ // Synthesis pass: board.yml-driven dirs ship NO per-variant yamls —
952
+ // nucleo_n657x0_q's `sb` target exists only as a board.yml entry.
953
+ // Emit every declared target the yaml flow did not already cover.
954
+ for (const t of boardYmlTargets(dir)) {
955
+ if (seenIds.has(t.identifier))
956
+ continue;
957
+ // No yamlBase preference here — the MOST qualified candidate must
958
+ // win (Zephyr's own variant dts chain: board_soc_variant → board_soc
959
+ // → board); the chain's tail covers the boardName fallback.
960
+ const dtsFile = dtsCandidatesFor(dir, t.identifier)
961
+ .find((p) => fs.existsSync(p));
962
+ if (!dtsFile)
963
+ continue;
964
+ seenIds.add(t.identifier);
965
+ variants++;
966
+ const probeMethods = boardProbeMethods(dir, variantPinFor(t.identifier, socIndex));
967
+ const base = {
968
+ identifier: t.identifier,
969
+ name: t.fullName ?? t.identifier,
970
+ vendor: t.vendor ?? vendorDir.name,
971
+ dts: path.basename(dtsFile),
972
+ ...(probeMethods ? { probeMethods } : {}),
973
+ };
974
+ try {
975
+ let facts = readBoardDts(dtsFile, { zephyrBoardsRoot: boardsRoot, moduleDtsRoots });
976
+ {
977
+ // Overlay wiring appended, deduped by (source, channel, pad) —
978
+ // sibling variant overlays (uno_r4 minima/wifi) repeat entries.
979
+ const seen = new Set(facts.connectorAdc.map((r) => `${r.source}:${r.channel}:${r.controller}.${r.pin}`));
980
+ const extra = overlayConnectorAdc(dir, files, facts.connectors)
981
+ .filter((r) => !seen.has(`${r.source}:${r.channel}:${r.controller}.${r.pin}`));
982
+ facts = { ...facts, connectorAdc: [...facts.connectorAdc, ...extra] };
983
+ }
984
+ const { record, hasFacts } = composeRecord(base, facts, zephyrBase, westRoot);
985
+ if (hasFacts)
986
+ withFacts++;
987
+ records.push(record);
988
+ }
989
+ catch (err) {
990
+ failures++;
991
+ if (failures <= 5)
992
+ console.error(`reader failed on ${t.identifier}: ${err.message}`);
993
+ records.push(base);
994
+ }
995
+ }
996
+ }
997
+ }
998
+ const coverage = {
999
+ adc: {},
1000
+ pwm: {},
1001
+ dac: {},
1002
+ };
1003
+ for (const r of records) {
1004
+ for (const cap of ['adc', 'pwm', 'dac']) {
1005
+ const src = r.siliconSources?.[cap];
1006
+ if (src)
1007
+ coverage[cap][src] = (coverage[cap][src] ?? 0) + 1;
1008
+ }
1009
+ }
1010
+ return {
1011
+ boards: Object.fromEntries(records.map((r) => [r.identifier, r])),
1012
+ provenance: {
1013
+ zephyrBase: path.resolve(zephyrBase),
1014
+ version: zephyrVersionOf(zephyrBase),
1015
+ ...(gitHeadOf(zephyrBase) ? { gitHead: gitHeadOf(zephyrBase) } : {}),
1016
+ boardsMtimeMs: fs.statSync(boardsRoot).mtimeMs,
1017
+ generatedAt: new Date().toISOString(),
1018
+ variants: records.length,
1019
+ },
1020
+ stats: { variants, withFacts, failures, droppedYamls, coverage },
1021
+ };
1022
+ }
1023
+ // ---------------------------------------------------------------------------
1024
+ // SoC-level facts from the installed tree — contract/custom-board support.
1025
+ // A custom PCB has no Zephyr board record, but its SoC's dtsi lives in the
1026
+ // installed tree and declares the soc's bus controllers (i2c0, usart1, ...).
1027
+ // ---------------------------------------------------------------------------
1028
+ /** Bus controller labels declared by the SoC's dtsi files in the installed
1029
+ * tree, classified by family. Best-effort: matches dts/dtsi files whose
1030
+ * name contains the soc name (a soc stem like 'stm32f411' matches
1031
+ * 'stm32f411xe.dtsi'), plus same-dir includes. */
1032
+ export function socBusLabelsFromTree(zephyrBase, soc) {
1033
+ const buses = { i2c: [], spi: [], uart: [] };
1034
+ const dtsRoot = path.join(zephyrBase, 'dts');
1035
+ if (!fs.existsSync(dtsRoot))
1036
+ return buses;
1037
+ const socLower = soc.toLowerCase();
1038
+ // Family-prefix stems: a soc's buses often live in a SHARED family dtsi
1039
+ // (stm32f411xe → stm32f4.dtsi declares usart1/i2c1), so match progressively
1040
+ // shorter prefixes of the soc name, down to 5 chars.
1041
+ const stems = new Set();
1042
+ for (let len = socLower.length; len >= 5; len--)
1043
+ stems.add(socLower.slice(0, len));
1044
+ const labeled = { i2c: [], spi: [], uart: [] };
1045
+ let order = 0;
1046
+ const seenFiles = new Set();
1047
+ const scan = (dir, depth) => {
1048
+ if (depth > 4)
1049
+ return;
1050
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1051
+ const full = path.join(dir, entry.name);
1052
+ if (entry.isDirectory()) {
1053
+ scan(full, depth + 1);
1054
+ continue;
1055
+ }
1056
+ if (!/\.dtsi?$/.test(entry.name))
1057
+ continue;
1058
+ const nameLower = entry.name.toLowerCase();
1059
+ const matches = [...stems].some((s) => nameLower.includes(s));
1060
+ if (!matches || seenFiles.has(full))
1061
+ continue;
1062
+ seenFiles.add(full);
1063
+ const weight = nameLower.length;
1064
+ let text;
1065
+ try {
1066
+ text = fs.readFileSync(full, 'utf8');
1067
+ }
1068
+ catch {
1069
+ continue;
1070
+ }
1071
+ const grab = (re, family) => {
1072
+ for (const m of text.matchAll(re)) {
1073
+ if (!m[1])
1074
+ continue;
1075
+ labeled[family].push({ label: m[1], weight, order: order++ });
1076
+ }
1077
+ };
1078
+ grab(/([a-z0-9_]+):\s*(?:i2c|twi)@/g, 'i2c');
1079
+ grab(/([a-z0-9_]+):\s*(?:spi|ssp)@/g, 'spi');
1080
+ // 'serial@' — STM32 names its usart nodes serial@<addr>.
1081
+ grab(/([a-z0-9_]+):\s*(?:uart|usart|eusart|serial)@/g, 'uart');
1082
+ }
1083
+ };
1084
+ scan(dtsRoot, 0);
1085
+ // Drop flash-controller aliases (octospi/xspi/quadspi/subghzspi are
1086
+ // XIP/flash buses, not user SPI) and the usb-serial console pseudo-node.
1087
+ const notFlash = (label) => !/^(octospi|xspi|quadspi|subghzspi)/.test(label);
1088
+ const notUsbSerial = (label) => label !== 'usb_serial';
1089
+ const finalize = (family) => {
1090
+ const seen = new Set();
1091
+ return labeled[family]
1092
+ .filter((e) => notFlash(e.label) && notUsbSerial(e.label))
1093
+ .sort((a, b) => (a.weight - b.weight) || (a.order - b.order))
1094
+ .filter((e) => (seen.has(e.label) ? false : (seen.add(e.label), true)))
1095
+ .map((e) => e.label);
1096
+ };
1097
+ buses.i2c = finalize('i2c');
1098
+ buses.spi = finalize('spi');
1099
+ buses.uart = finalize('uart');
1100
+ return buses;
1101
+ }