boardforge 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. boardforge/__init__.py +7 -0
  2. boardforge/analysis/__init__.py +259 -0
  3. boardforge/cli.py +280 -0
  4. boardforge/config.py +218 -0
  5. boardforge/datasheets.py +604 -0
  6. boardforge/drc.py +71 -0
  7. boardforge/fabs/__init__.py +18 -0
  8. boardforge/fabs/base.py +108 -0
  9. boardforge/fabs/jlcpcb/__init__.py +60 -0
  10. boardforge/fabs/jlcpcb/profile.toml +56 -0
  11. boardforge/kicad.py +189 -0
  12. boardforge/netlist.py +118 -0
  13. boardforge/pcb/__init__.py +8 -0
  14. boardforge/pcb/astar.py +543 -0
  15. boardforge/pcb/build.py +158 -0
  16. boardforge/pcb/common.py +112 -0
  17. boardforge/pcb/congestion.py +116 -0
  18. boardforge/pcb/fanout.py +278 -0
  19. boardforge/pcb/fiducials.py +104 -0
  20. boardforge/pcb/finish.py +215 -0
  21. boardforge/pcb/measure.py +76 -0
  22. boardforge/pcb/pinswap.py +143 -0
  23. boardforge/pcb/place.py +556 -0
  24. boardforge/pcb/specctra.py +24 -0
  25. boardforge/pcbfile.py +41 -0
  26. boardforge/physical.py +811 -0
  27. boardforge/project.py +116 -0
  28. boardforge/render.py +66 -0
  29. boardforge/review/__init__.py +0 -0
  30. boardforge/review/bom_check.py +280 -0
  31. boardforge/review/budget.py +92 -0
  32. boardforge/review/constraints.py +61 -0
  33. boardforge/review/datasheet_check.py +172 -0
  34. boardforge/review/gate.py +80 -0
  35. boardforge/review/package_check.py +246 -0
  36. boardforge/review/spice_path.py +351 -0
  37. boardforge/routers/__init__.py +17 -0
  38. boardforge/routers/astar.py +87 -0
  39. boardforge/routers/base.py +77 -0
  40. boardforge/routers/freerouting.py +58 -0
  41. boardforge/routers/specctra.py +84 -0
  42. boardforge/skidl_runner.py +144 -0
  43. boardforge/state.py +54 -0
  44. boardforge/units.py +33 -0
  45. boardforge/vendor/README.md +5 -0
  46. boardforge/vendor/kicad_happy/LICENSE +21 -0
  47. boardforge/vendor/kicad_happy/VENDORED.md +25 -0
  48. boardforge/vendor/kicad_happy/how-it-works.md +254 -0
  49. boardforge/vendor/kicad_happy/skills/bom/SKILL.md +378 -0
  50. boardforge/vendor/kicad_happy/skills/bom/references/kicad-fields.md +68 -0
  51. boardforge/vendor/kicad_happy/skills/bom/references/ordering-and-fabrication.md +150 -0
  52. boardforge/vendor/kicad_happy/skills/bom/references/part-number-conventions.md +409 -0
  53. boardforge/vendor/kicad_happy/skills/bom/scripts/bom_manager.py +1298 -0
  54. boardforge/vendor/kicad_happy/skills/bom/scripts/edit_properties.py +438 -0
  55. boardforge/vendor/kicad_happy/skills/bom/scripts/kicad_sexp.py +85 -0
  56. boardforge/vendor/kicad_happy/skills/bom/scripts/sync_datasheet_urls.py +393 -0
  57. boardforge/vendor/kicad_happy/skills/bom/scripts/translate_bom_pnp.py +731 -0
  58. boardforge/vendor/kicad_happy/skills/datasheets/SKILL.md +119 -0
  59. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__init__.py +80 -0
  60. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__pycache__/__init__.cpython-314.pyc +0 -0
  61. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__pycache__/base_block.cpython-314.pyc +0 -0
  62. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__pycache__/codec.cpython-314.pyc +0 -0
  63. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__pycache__/extraction.cpython-314.pyc +0 -0
  64. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__pycache__/pinout.cpython-314.pyc +0 -0
  65. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__pycache__/regulator.cpython-314.pyc +0 -0
  66. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__pycache__/spec_value.cpython-314.pyc +0 -0
  67. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/__pycache__/trust_gating.cpython-314.pyc +0 -0
  68. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/base_block.py +132 -0
  69. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/codec.py +174 -0
  70. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/extraction.py +177 -0
  71. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/pinout.py +117 -0
  72. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/regulator.py +95 -0
  73. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/spec_value.py +51 -0
  74. boardforge/vendor/kicad_happy/skills/datasheets/datasheet_types/trust_gating.py +125 -0
  75. boardforge/vendor/kicad_happy/skills/datasheets/examples/abm8g-106-12.000mhz-t.json +372 -0
  76. boardforge/vendor/kicad_happy/skills/datasheets/examples/irlml6344.json +616 -0
  77. boardforge/vendor/kicad_happy/skills/datasheets/examples/lm2596-adj.json +507 -0
  78. boardforge/vendor/kicad_happy/skills/datasheets/examples/lm358.json +598 -0
  79. boardforge/vendor/kicad_happy/skills/datasheets/examples/mbrs540t3g.json +498 -0
  80. boardforge/vendor/kicad_happy/skills/datasheets/examples/stm32f103c8t6.json +2090 -0
  81. boardforge/vendor/kicad_happy/skills/datasheets/prompts/base.md +56 -0
  82. boardforge/vendor/kicad_happy/skills/datasheets/prompts/crystal.md +127 -0
  83. boardforge/vendor/kicad_happy/skills/datasheets/prompts/diode.md +89 -0
  84. boardforge/vendor/kicad_happy/skills/datasheets/prompts/mcu.md +163 -0
  85. boardforge/vendor/kicad_happy/skills/datasheets/prompts/opamp.md +164 -0
  86. boardforge/vendor/kicad_happy/skills/datasheets/prompts/pinout.md +54 -0
  87. boardforge/vendor/kicad_happy/skills/datasheets/prompts/regulator.md +56 -0
  88. boardforge/vendor/kicad_happy/skills/datasheets/prompts/scout.md +68 -0
  89. boardforge/vendor/kicad_happy/skills/datasheets/prompts/transistor.md +145 -0
  90. boardforge/vendor/kicad_happy/skills/datasheets/references/cache-layout.md +183 -0
  91. boardforge/vendor/kicad_happy/skills/datasheets/references/consumer-api.md +240 -0
  92. boardforge/vendor/kicad_happy/skills/datasheets/references/dispatch-claude-code.md +179 -0
  93. boardforge/vendor/kicad_happy/skills/datasheets/references/dispatcher-contract.md +112 -0
  94. boardforge/vendor/kicad_happy/skills/datasheets/references/extraction-pipeline.md +24 -0
  95. boardforge/vendor/kicad_happy/skills/datasheets/references/extraction-schema.md +387 -0
  96. boardforge/vendor/kicad_happy/skills/datasheets/references/field-extraction-guide.md +262 -0
  97. boardforge/vendor/kicad_happy/skills/datasheets/references/quality-scoring.md +205 -0
  98. boardforge/vendor/kicad_happy/skills/datasheets/schemas/CHANGELOG.md +180 -0
  99. boardforge/vendor/kicad_happy/skills/datasheets/schemas/base.schema.json +181 -0
  100. boardforge/vendor/kicad_happy/skills/datasheets/schemas/crystal.schema.json +106 -0
  101. boardforge/vendor/kicad_happy/skills/datasheets/schemas/diode.schema.json +148 -0
  102. boardforge/vendor/kicad_happy/skills/datasheets/schemas/extraction.schema.json +91 -0
  103. boardforge/vendor/kicad_happy/skills/datasheets/schemas/fixtures/lm2596-adj.example.json +507 -0
  104. boardforge/vendor/kicad_happy/skills/datasheets/schemas/fixtures/manifest.example.json +31 -0
  105. boardforge/vendor/kicad_happy/skills/datasheets/schemas/fixtures/minimal.example.json +95 -0
  106. boardforge/vendor/kicad_happy/skills/datasheets/schemas/manifest.schema.json +69 -0
  107. boardforge/vendor/kicad_happy/skills/datasheets/schemas/mcu.schema.json +252 -0
  108. boardforge/vendor/kicad_happy/skills/datasheets/schemas/opamp.schema.json +167 -0
  109. boardforge/vendor/kicad_happy/skills/datasheets/schemas/pinout.schema.json +130 -0
  110. boardforge/vendor/kicad_happy/skills/datasheets/schemas/plan.schema.json +100 -0
  111. boardforge/vendor/kicad_happy/skills/datasheets/schemas/regulator.schema.json +156 -0
  112. boardforge/vendor/kicad_happy/skills/datasheets/schemas/scout.schema.json +68 -0
  113. boardforge/vendor/kicad_happy/skills/datasheets/schemas/spec_value.schema.json +62 -0
  114. boardforge/vendor/kicad_happy/skills/datasheets/schemas/transistor.schema.json +221 -0
  115. boardforge/vendor/kicad_happy/skills/datasheets/scripts/__pycache__/datasheet_extract_cache.cpython-314.pyc +0 -0
  116. boardforge/vendor/kicad_happy/skills/datasheets/scripts/__pycache__/datasheet_features.cpython-314.pyc +0 -0
  117. boardforge/vendor/kicad_happy/skills/datasheets/scripts/__pycache__/datasheet_lookup.cpython-314.pyc +0 -0
  118. boardforge/vendor/kicad_happy/skills/datasheets/scripts/__pycache__/datasheet_score.cpython-314.pyc +0 -0
  119. boardforge/vendor/kicad_happy/skills/datasheets/scripts/__pycache__/datasheet_verify.cpython-314.pyc +0 -0
  120. boardforge/vendor/kicad_happy/skills/datasheets/scripts/_smoke_v14_roundtrip.py +211 -0
  121. boardforge/vendor/kicad_happy/skills/datasheets/scripts/datasheet_extract_cache.py +541 -0
  122. boardforge/vendor/kicad_happy/skills/datasheets/scripts/datasheet_features.py +423 -0
  123. boardforge/vendor/kicad_happy/skills/datasheets/scripts/datasheet_lookup.py +243 -0
  124. boardforge/vendor/kicad_happy/skills/datasheets/scripts/datasheet_page_selector.py +500 -0
  125. boardforge/vendor/kicad_happy/skills/datasheets/scripts/datasheet_score.py +459 -0
  126. boardforge/vendor/kicad_happy/skills/datasheets/scripts/datasheet_verify.py +1070 -0
  127. boardforge/vendor/kicad_happy/skills/datasheets/scripts/merge_results.py +318 -0
  128. boardforge/vendor/kicad_happy/skills/datasheets/scripts/plan_extraction.py +198 -0
  129. boardforge/vendor/kicad_happy/skills/datasheets/scripts/validate_extraction_result.py +142 -0
  130. boardforge/vendor/kicad_happy/skills/datasheets/scripts/validate_sanity_vector.py +157 -0
  131. boardforge/vendor/kicad_happy/skills/digikey/SKILL.md +377 -0
  132. boardforge/vendor/kicad_happy/skills/digikey/scripts/fetch_datasheet_digikey.py +547 -0
  133. boardforge/vendor/kicad_happy/skills/digikey/scripts/sync_datasheets_digikey.py +908 -0
  134. boardforge/vendor/kicad_happy/skills/element14/SKILL.md +273 -0
  135. boardforge/vendor/kicad_happy/skills/element14/scripts/fetch_datasheet_element14.py +516 -0
  136. boardforge/vendor/kicad_happy/skills/element14/scripts/sync_datasheets_element14.py +775 -0
  137. boardforge/vendor/kicad_happy/skills/emc/SKILL.md +206 -0
  138. boardforge/vendor/kicad_happy/skills/emc/references/emc-methodology.md +142 -0
  139. boardforge/vendor/kicad_happy/skills/emc/references/emc-standards.md +110 -0
  140. boardforge/vendor/kicad_happy/skills/emc/references/pcb-emc-rules.md +381 -0
  141. boardforge/vendor/kicad_happy/skills/emc/scripts/__pycache__/emc_envelope.cpython-314.pyc +0 -0
  142. boardforge/vendor/kicad_happy/skills/emc/scripts/__pycache__/emc_formulas.cpython-314.pyc +0 -0
  143. boardforge/vendor/kicad_happy/skills/emc/scripts/__pycache__/emc_rules.cpython-314.pyc +0 -0
  144. boardforge/vendor/kicad_happy/skills/emc/scripts/analyze_emc.py +682 -0
  145. boardforge/vendor/kicad_happy/skills/emc/scripts/emc_envelope.py +291 -0
  146. boardforge/vendor/kicad_happy/skills/emc/scripts/emc_formulas.py +1392 -0
  147. boardforge/vendor/kicad_happy/skills/emc/scripts/emc_rules.py +4552 -0
  148. boardforge/vendor/kicad_happy/skills/emc/scripts/emc_spice.py +708 -0
  149. boardforge/vendor/kicad_happy/skills/jlcpcb/SKILL.md +233 -0
  150. boardforge/vendor/kicad_happy/skills/kicad/SKILL.md +850 -0
  151. boardforge/vendor/kicad_happy/skills/kicad/references/config-reference.md +356 -0
  152. boardforge/vendor/kicad_happy/skills/kicad/references/datasheet-verification.md +322 -0
  153. boardforge/vendor/kicad_happy/skills/kicad/references/deep-review.md +162 -0
  154. boardforge/vendor/kicad_happy/skills/kicad/references/design-intent.md +123 -0
  155. boardforge/vendor/kicad_happy/skills/kicad/references/diff-analysis.md +355 -0
  156. boardforge/vendor/kicad_happy/skills/kicad/references/file-formats.md +379 -0
  157. boardforge/vendor/kicad_happy/skills/kicad/references/gerber-parsing.md +729 -0
  158. boardforge/vendor/kicad_happy/skills/kicad/references/manual-gerber-parsing.md +621 -0
  159. boardforge/vendor/kicad_happy/skills/kicad/references/manual-pcb-parsing.md +467 -0
  160. boardforge/vendor/kicad_happy/skills/kicad/references/manual-schematic-parsing.md +289 -0
  161. boardforge/vendor/kicad_happy/skills/kicad/references/net-tracing.md +169 -0
  162. boardforge/vendor/kicad_happy/skills/kicad/references/oshwa-certification.md +89 -0
  163. boardforge/vendor/kicad_happy/skills/kicad/references/output-schema.md +317 -0
  164. boardforge/vendor/kicad_happy/skills/kicad/references/pcb-layout-analysis.md +447 -0
  165. boardforge/vendor/kicad_happy/skills/kicad/references/pdf-schematic-extraction.md +315 -0
  166. boardforge/vendor/kicad_happy/skills/kicad/references/report-generation.md +696 -0
  167. boardforge/vendor/kicad_happy/skills/kicad/references/schematic-analysis.md +1165 -0
  168. boardforge/vendor/kicad_happy/skills/kicad/references/standards-compliance.md +638 -0
  169. boardforge/vendor/kicad_happy/skills/kicad/references/supplementary-data-sources.md +288 -0
  170. boardforge/vendor/kicad_happy/skills/kicad/references/what-if.md +500 -0
  171. boardforge/vendor/kicad_happy/skills/kicad/review/README.md +46 -0
  172. boardforge/vendor/kicad_happy/skills/kicad/review/fixtures/deep_review.example.json +39 -0
  173. boardforge/vendor/kicad_happy/skills/kicad/review/fixtures/design_context.example.json +9 -0
  174. boardforge/vendor/kicad_happy/skills/kicad/review/fixtures/review_annotations.example.json +22 -0
  175. boardforge/vendor/kicad_happy/skills/kicad/review/prompts/design_context.md +100 -0
  176. boardforge/vendor/kicad_happy/skills/kicad/review/schemas/deep_review.schema.json +72 -0
  177. boardforge/vendor/kicad_happy/skills/kicad/review/schemas/design_context.schema.json +59 -0
  178. boardforge/vendor/kicad_happy/skills/kicad/review/schemas/review_annotations.schema.json +49 -0
  179. boardforge/vendor/kicad_happy/skills/kicad/review/scripts/_mini_jsonschema.py +173 -0
  180. boardforge/vendor/kicad_happy/skills/kicad/review/scripts/build_review_plan.py +65 -0
  181. boardforge/vendor/kicad_happy/skills/kicad/review/scripts/deep_review_gate.py +347 -0
  182. boardforge/vendor/kicad_happy/skills/kicad/review/scripts/merge_annotations.py +175 -0
  183. boardforge/vendor/kicad_happy/skills/kicad/review/scripts/run_phase4_exercise.py +281 -0
  184. boardforge/vendor/kicad_happy/skills/kicad/review/scripts/validate_review.py +72 -0
  185. boardforge/vendor/kicad_happy/skills/kicad/scripts/README.md +389 -0
  186. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/analyzer_envelope.cpython-314.pyc +0 -0
  187. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/bus_resolver.cpython-314.pyc +0 -0
  188. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/capability_mode.cpython-314.pyc +0 -0
  189. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/detection_schema.cpython-314.pyc +0 -0
  190. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/detector_helpers.cpython-314.pyc +0 -0
  191. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/domain_detectors.cpython-314.pyc +0 -0
  192. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/finding_schema.cpython-314.pyc +0 -0
  193. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/inputs_builder.cpython-314.pyc +0 -0
  194. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/kicad_types.cpython-314.pyc +0 -0
  195. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/kicad_utils.cpython-314.pyc +0 -0
  196. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/lookup_detectors.cpython-314.pyc +0 -0
  197. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/lookup_helpers.cpython-314.pyc +0 -0
  198. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/netlist_queries.cpython-314.pyc +0 -0
  199. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/output_filters.cpython-314.pyc +0 -0
  200. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/pcb_connectivity.cpython-314.pyc +0 -0
  201. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/project_config.cpython-314.pyc +0 -0
  202. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/run_id.cpython-314.pyc +0 -0
  203. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/schema_codec.cpython-314.pyc +0 -0
  204. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/sexp_parser.cpython-314.pyc +0 -0
  205. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/signal_detectors.cpython-314.pyc +0 -0
  206. boardforge/vendor/kicad_happy/skills/kicad/scripts/__pycache__/validation_detectors.cpython-314.pyc +0 -0
  207. boardforge/vendor/kicad_happy/skills/kicad/scripts/analysis_cache.py +604 -0
  208. boardforge/vendor/kicad_happy/skills/kicad/scripts/analyze_gerbers.py +1670 -0
  209. boardforge/vendor/kicad_happy/skills/kicad/scripts/analyze_pcb.py +7544 -0
  210. boardforge/vendor/kicad_happy/skills/kicad/scripts/analyze_schematic.py +10421 -0
  211. boardforge/vendor/kicad_happy/skills/kicad/scripts/analyze_thermal.py +1222 -0
  212. boardforge/vendor/kicad_happy/skills/kicad/scripts/analyzer_envelope.py +295 -0
  213. boardforge/vendor/kicad_happy/skills/kicad/scripts/bus_resolver.py +289 -0
  214. boardforge/vendor/kicad_happy/skills/kicad/scripts/capability_mode.py +218 -0
  215. boardforge/vendor/kicad_happy/skills/kicad/scripts/check_report_sections.py +201 -0
  216. boardforge/vendor/kicad_happy/skills/kicad/scripts/cross_analysis.py +1201 -0
  217. boardforge/vendor/kicad_happy/skills/kicad/scripts/cross_verify.py +657 -0
  218. boardforge/vendor/kicad_happy/skills/kicad/scripts/detection_schema.py +654 -0
  219. boardforge/vendor/kicad_happy/skills/kicad/scripts/detector_helpers.py +76 -0
  220. boardforge/vendor/kicad_happy/skills/kicad/scripts/diff_analysis.py +1596 -0
  221. boardforge/vendor/kicad_happy/skills/kicad/scripts/domain_detectors.py +6161 -0
  222. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/__init__.py +0 -0
  223. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/__pycache__/__init__.cpython-314.pyc +0 -0
  224. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/__pycache__/gerber.cpython-314.pyc +0 -0
  225. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/__pycache__/pcb.cpython-314.pyc +0 -0
  226. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/__pycache__/schematic.cpython-314.pyc +0 -0
  227. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/__pycache__/thermal.cpython-314.pyc +0 -0
  228. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/cross_analysis.py +132 -0
  229. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/gerber.py +355 -0
  230. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/pcb.py +395 -0
  231. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/schematic.py +450 -0
  232. boardforge/vendor/kicad_happy/skills/kicad/scripts/envelopes/thermal.py +144 -0
  233. boardforge/vendor/kicad_happy/skills/kicad/scripts/export_issues.py +547 -0
  234. boardforge/vendor/kicad_happy/skills/kicad/scripts/fab_release_gate.py +604 -0
  235. boardforge/vendor/kicad_happy/skills/kicad/scripts/finding_schema.py +715 -0
  236. boardforge/vendor/kicad_happy/skills/kicad/scripts/gen_output_schema_md.py +230 -0
  237. boardforge/vendor/kicad_happy/skills/kicad/scripts/inputs_builder.py +107 -0
  238. boardforge/vendor/kicad_happy/skills/kicad/scripts/kicad_types.py +117 -0
  239. boardforge/vendor/kicad_happy/skills/kicad/scripts/kicad_utils.py +2278 -0
  240. boardforge/vendor/kicad_happy/skills/kicad/scripts/lifecycle_audit.py +1137 -0
  241. boardforge/vendor/kicad_happy/skills/kicad/scripts/lookup_detectors.py +701 -0
  242. boardforge/vendor/kicad_happy/skills/kicad/scripts/lookup_helpers.py +60 -0
  243. boardforge/vendor/kicad_happy/skills/kicad/scripts/methodology_gerbers.md +447 -0
  244. boardforge/vendor/kicad_happy/skills/kicad/scripts/methodology_pcb.md +473 -0
  245. boardforge/vendor/kicad_happy/skills/kicad/scripts/methodology_schematic.md +892 -0
  246. boardforge/vendor/kicad_happy/skills/kicad/scripts/netlist_queries.py +360 -0
  247. boardforge/vendor/kicad_happy/skills/kicad/scripts/output_filters.py +468 -0
  248. boardforge/vendor/kicad_happy/skills/kicad/scripts/pcb_connectivity.py +516 -0
  249. boardforge/vendor/kicad_happy/skills/kicad/scripts/project_config.py +956 -0
  250. boardforge/vendor/kicad_happy/skills/kicad/scripts/run_id.py +47 -0
  251. boardforge/vendor/kicad_happy/skills/kicad/scripts/schema_codec.py +145 -0
  252. boardforge/vendor/kicad_happy/skills/kicad/scripts/sexp_parser.py +233 -0
  253. boardforge/vendor/kicad_happy/skills/kicad/scripts/signal_detectors.py +4699 -0
  254. boardforge/vendor/kicad_happy/skills/kicad/scripts/summarize_findings.py +421 -0
  255. boardforge/vendor/kicad_happy/skills/kicad/scripts/validation_detectors.py +1440 -0
  256. boardforge/vendor/kicad_happy/skills/kicad/scripts/what_if.py +1554 -0
  257. boardforge/vendor/kicad_happy/skills/lcsc/SKILL.md +241 -0
  258. boardforge/vendor/kicad_happy/skills/lcsc/scripts/fetch_datasheet_lcsc.py +591 -0
  259. boardforge/vendor/kicad_happy/skills/lcsc/scripts/search_lcsc.py +360 -0
  260. boardforge/vendor/kicad_happy/skills/lcsc/scripts/sync_datasheets_lcsc.py +755 -0
  261. boardforge/vendor/kicad_happy/skills/lcsc/scripts/test_search_lcsc.py +358 -0
  262. boardforge/vendor/kicad_happy/skills/mouser/SKILL.md +265 -0
  263. boardforge/vendor/kicad_happy/skills/mouser/scripts/fetch_datasheet_mouser.py +600 -0
  264. boardforge/vendor/kicad_happy/skills/mouser/scripts/sync_datasheets_mouser.py +805 -0
  265. boardforge/vendor/kicad_happy/skills/pcbway/SKILL.md +179 -0
  266. boardforge/vendor/kicad_happy/skills/spice/SKILL.md +333 -0
  267. boardforge/vendor/kicad_happy/skills/spice/references/simulation-models.md +427 -0
  268. boardforge/vendor/kicad_happy/skills/spice/scripts/__pycache__/spice_model_generator.cpython-314.pyc +0 -0
  269. boardforge/vendor/kicad_happy/skills/spice/scripts/__pycache__/spice_models.cpython-314.pyc +0 -0
  270. boardforge/vendor/kicad_happy/skills/spice/scripts/extract_parasitics.py +319 -0
  271. boardforge/vendor/kicad_happy/skills/spice/scripts/simulate_subcircuits.py +689 -0
  272. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_model_cache.py +293 -0
  273. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_model_generator.py +231 -0
  274. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_models.py +196 -0
  275. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_part_library.py +447 -0
  276. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_results.py +1260 -0
  277. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_simulator.py +475 -0
  278. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_spec_fetcher.py +650 -0
  279. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_templates.py +1740 -0
  280. boardforge/vendor/kicad_happy/skills/spice/scripts/spice_tolerance.py +311 -0
  281. boardforge-0.1.0.dist-info/METADATA +258 -0
  282. boardforge-0.1.0.dist-info/RECORD +286 -0
  283. boardforge-0.1.0.dist-info/WHEEL +5 -0
  284. boardforge-0.1.0.dist-info/entry_points.txt +2 -0
  285. boardforge-0.1.0.dist-info/licenses/LICENSE +21 -0
  286. boardforge-0.1.0.dist-info/top_level.txt +1 -0
boardforge/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """boardforge: SKiDL circuit -> routed, manufacturable KiCad board.
2
+
3
+ Keep this module free of imports: the scripts in boardforge.pcb run under KiCad's own
4
+ Python, which imports this package but has none of boardforge's dependencies.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,259 @@
1
+ """Analysis-step registry (design doc §4.7).
2
+
3
+ Each step (schematic, pcb, thermal, emc, cross_verify, gerbers, release_gate) has one or
4
+ more implementations. The default is the vendored kicad-happy script, run as a subprocess.
5
+ Steps talk through JSON files with kicad-happy's schema
6
+ (``skills/kicad/references/output-schema.md``), so a replacement only has to write the same
7
+ JSON. Choose an implementation per step in boardforge.toml:
8
+
9
+ [checks.steps]
10
+ thermal = "boardforge"
11
+
12
+ Register a replacement with ``@register("thermal", "boardforge")`` on a function
13
+ ``(ctx) -> output path``.
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import subprocess
19
+ import sys
20
+
21
+ VENDOR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "vendor", "kicad_happy", "skills")
22
+ REGISTRY = {}
23
+
24
+ # Steps in dependency order; each lists the outputs it reads.
25
+ ORDER = ["schematic", "pcb", "thermal", "emc", "cross_verify", "gerbers", "release_gate"]
26
+
27
+
28
+ def vendor_script(rel):
29
+ return os.path.join(VENDOR, rel)
30
+
31
+
32
+ def register(step, impl):
33
+ def deco(fn):
34
+ REGISTRY.setdefault(step, {})[impl] = fn
35
+ return fn
36
+ return deco
37
+
38
+
39
+ class Context:
40
+ """What a step needs: input files and where outputs go. ``outputs`` fills as steps run."""
41
+
42
+ def __init__(self, sch, pcb, outdir, gerber_dir=None, rail_currents=None, drc_report=None,
43
+ fab_limits=None):
44
+ self.sch, self.pcb, self.outdir, self.gerber_dir = sch, pcb, outdir, gerber_dir
45
+ # {"name": "jlcpcb/multilayer", min_track, min_clearance, min_drill, annular_ring}:
46
+ # the fab tier the board was set up for; DFM findings are re-judged against it.
47
+ self.fab_limits = fab_limits
48
+ self.drc_report = drc_report # kicad-cli DRC JSON: the authority on unrouted connections
49
+ self.rail_currents = dict(rail_currents or {}) # {KiCad net: A}
50
+ self.outputs = {}
51
+ self.loads = [] # [(regulator ref, rail, A)] patched by schematic_with_loads()
52
+ os.makedirs(outdir, exist_ok=True)
53
+
54
+ def out(self, step):
55
+ return os.path.join(self.outdir, f"{step}.json")
56
+
57
+ def schematic_with_loads(self):
58
+ """The schematic analysis with declared rail currents in place of kicad-happy's load
59
+ guesses (LDO dissipation, switcher rail loads). Same file if nothing is declared."""
60
+ src = self.outputs["schematic"]
61
+ if not self.rail_currents:
62
+ return src
63
+ with open(src) as f:
64
+ sch = json.load(f)
65
+ self.loads = apply_rail_currents(sch, self.rail_currents)
66
+ path = self.out("schematic_loads")
67
+ with open(path, "w") as f:
68
+ json.dump(sch, f)
69
+ return path
70
+
71
+
72
+ def apply_rail_currents(sch, currents):
73
+ """Patch declared load currents into a kicad-happy schematic analysis, in place.
74
+ Returns [(regulator ref, rail, amps)] for the regulators changed."""
75
+ patched = []
76
+ for reg in sch.get("findings", []):
77
+ if reg.get("detector") != "detect_power_regulators":
78
+ continue
79
+ rail = reg.get("output_rail")
80
+ if rail not in currents:
81
+ continue
82
+ amps = float(currents[rail])
83
+ pd = reg.get("power_dissipation")
84
+ if isinstance(pd, dict) and "dropout_V" in pd: # LDO
85
+ pd["estimated_iout_A"] = round(amps, 4)
86
+ pd["estimated_pdiss_W"] = round(pd["dropout_V"] * amps, 4)
87
+ pd["_iout_provenance"] = {"source": "boardforge_config", "confidence": "declared",
88
+ "basis": "checks.rail_currents"}
89
+ elif isinstance(pd, dict) and pd.get("efficiency_assumed") and pd.get("vout_V"): # switcher
90
+ eta = pd["efficiency_assumed"]
91
+ pd["estimated_iout_A"] = round(amps, 4)
92
+ pd["estimated_pdiss_W"] = round(pd["vout_V"] * amps * (1 / eta - 1), 4)
93
+ pd["_iout_provenance"] = {"source": "boardforge_config", "confidence": "declared",
94
+ "basis": "checks.rail_currents"}
95
+ reg["estimated_iout_A"] = round(amps, 4)
96
+ patched.append((reg.get("ref"), rail, amps))
97
+ rails = sch.get("power_budget", {}).get("rails", {})
98
+ for rail, amps in currents.items():
99
+ if isinstance(rails.get(rail), dict):
100
+ rails[rail]["estimated_load_mA"] = round(float(amps) * 1000, 1)
101
+ return patched
102
+
103
+
104
+ def _vendor(script, *args, timeout=900):
105
+ r = subprocess.run([sys.executable, vendor_script(script), *map(str, args)],
106
+ capture_output=True, text=True, timeout=timeout)
107
+ if r.returncode not in (0, 1): # some analyzers exit 1 when they report errors
108
+ raise RuntimeError(f"{os.path.basename(script)} exit {r.returncode}: {r.stderr.strip()[-400:]}")
109
+
110
+
111
+ @register("schematic", "kicad_happy")
112
+ def _schematic(ctx):
113
+ _vendor("kicad/scripts/analyze_schematic.py", ctx.sch, "-o", ctx.out("schematic"))
114
+ return ctx.out("schematic")
115
+
116
+
117
+ @register("pcb", "kicad_happy")
118
+ def _pcb(ctx):
119
+ _vendor("kicad/scripts/analyze_pcb.py", ctx.pcb, "--full", "-o", ctx.out("pcb"))
120
+ return ctx.out("pcb")
121
+
122
+
123
+ @register("thermal", "kicad_happy")
124
+ def _thermal(ctx):
125
+ _vendor("kicad/scripts/analyze_thermal.py", "-s", ctx.schematic_with_loads(), "-p",
126
+ ctx.outputs["pcb"], "-o", ctx.out("thermal"))
127
+ return ctx.out("thermal")
128
+
129
+
130
+ @register("emc", "kicad_happy")
131
+ def _emc(ctx):
132
+ _vendor("emc/scripts/analyze_emc.py", "-s", ctx.outputs["schematic"], "-p", ctx.outputs["pcb"],
133
+ "-o", ctx.out("emc"))
134
+ return ctx.out("emc")
135
+
136
+
137
+ @register("cross_verify", "kicad_happy")
138
+ def _cross(ctx):
139
+ args = ["-s", ctx.schematic_with_loads(), "-p", ctx.outputs["pcb"], "-o", ctx.out("cross_verify")]
140
+ if "thermal" in ctx.outputs:
141
+ args += ["-t", ctx.outputs["thermal"]]
142
+ _vendor("kicad/scripts/cross_verify.py", *args)
143
+ return ctx.out("cross_verify")
144
+
145
+
146
+ @register("gerbers", "kicad_happy")
147
+ def _gerbers(ctx):
148
+ _vendor("kicad/scripts/analyze_gerbers.py", ctx.gerber_dir, "-o", ctx.out("gerbers"))
149
+ return ctx.out("gerbers")
150
+
151
+
152
+ # kicad-happy DFM-001 parameter -> the fab-tier minimum it is held to
153
+ DFM_LIMITS = {"track_spacing": "min_clearance", "track_width": "min_track", "via_drill": "min_drill",
154
+ "pad_drill": "min_drill", "annular_ring": "annular_ring"}
155
+
156
+
157
+ def dfm_for_fab(pcb, limits):
158
+ """Re-judge kicad-happy's DFM tier against the board's own fab tier. Its 'standard' limits
159
+ are JLCPCB's 1-2 layer ones (0.127 mm spacing), so a 4-layer board at JLCPCB's 0.09 mm
160
+ multilayer minimum was reported as needing the 'advanced' process. Findings within the
161
+ tier are dropped; the tier becomes 'standard' (meaning: this fab tier) when none are left.
162
+ Returns the parameters dropped."""
163
+ if not limits:
164
+ return []
165
+ dropped, keep = [], []
166
+ for f in pcb.get("findings", []):
167
+ par = f.get("parameter") if f.get("rule_id") == "DFM-001" else None
168
+ lim = limits.get(DFM_LIMITS.get(par, ""))
169
+ if lim is not None and isinstance(f.get("actual_mm"), (int, float)) and f["actual_mm"] >= lim - 1e-6:
170
+ dropped.append(par)
171
+ continue
172
+ keep.append(f)
173
+ pcb["findings"] = keep
174
+ left = [f for f in keep if f.get("rule_id") == "DFM-001" and f.get("parameter") in DFM_LIMITS]
175
+ dfm = pcb.setdefault("dfm_summary", {})
176
+ if dropped and not left:
177
+ dfm["dfm_tier"] = "standard"
178
+ dfm["judged_against"] = limits.get("name")
179
+ return dropped
180
+
181
+
182
+ def routing_from_drc(pcb, drc_report):
183
+ """Replace kicad-happy's routing status with KiCad's: kicad-happy counts a net as routed
184
+ once it has any copper, so a partly routed net passes. Nets come from the DRC report's
185
+ unconnected items (``Pad 1 [NET] of U1``)."""
186
+ nets = sorted({i["description"].split("[", 1)[1].split("]", 1)[0]
187
+ for u in drc_report.get("unconnected_items", []) for i in u.get("items", [])
188
+ if "[" in i.get("description", "")})
189
+ conn = pcb.setdefault("connectivity", {})
190
+ conn["unrouted_count"] = len(nets)
191
+ conn["routing_complete"] = not nets
192
+ conn["unrouted"] = [{"net_name": n, "source": "kicad-cli DRC"} for n in nets]
193
+ return nets
194
+
195
+
196
+ @register("release_gate", "kicad_happy")
197
+ def _gate(ctx):
198
+ # The gate compares schematic parts with *all* PCB footprints; board-only ones
199
+ # (fiducials, mounting holes) have no schematic symbol, and the schematic count leaves
200
+ # out DNP parts, so both are left out of the count.
201
+ # Its DFM tier is re-judged against the board's fab tier (dfm_for_fab).
202
+ with open(ctx.outputs["pcb"]) as f:
203
+ pcb = json.load(f)
204
+ fps = pcb.get("footprints", [])
205
+ pcb.setdefault("statistics", {})["footprint_count"] = sum(
206
+ 1 for fp in fps if not fp.get("board_only") and not fp.get("dnp"))
207
+ if ctx.drc_report and os.path.exists(ctx.drc_report):
208
+ with open(ctx.drc_report) as f:
209
+ routing_from_drc(pcb, json.load(f))
210
+ dfm_for_fab(pcb, ctx.fab_limits)
211
+ pcb_gate = ctx.out("pcb_for_gate")
212
+ with open(pcb_gate, "w") as f:
213
+ json.dump(pcb, f)
214
+ args = ["-s", ctx.outputs["schematic"], "-p", pcb_gate, "-o", ctx.out("release_gate")]
215
+ for step, flag in (("gerbers", "-g"), ("thermal", "-t"), ("emc", "-e")):
216
+ if step in ctx.outputs:
217
+ args += [flag, ctx.outputs[step]]
218
+ _vendor("kicad/scripts/fab_release_gate.py", *args)
219
+ return ctx.out("release_gate")
220
+
221
+
222
+ def summarize(path, top=5):
223
+ """Counts by severity plus the first few errors, from kicad-happy's common envelope."""
224
+ with open(path) as f:
225
+ d = json.load(f)
226
+ s = d.get("summary", {}) if isinstance(d, dict) else {}
227
+ out = {"by_severity": s.get("by_severity")}
228
+ findings = [x for x in d.get("findings", []) if not x.get("suppressed")]
229
+ errs = [x for x in findings if x.get("severity") == "error"]
230
+ out["errors"] = [f"{x.get('rule_id', '?')}: {(x.get('summary') or x.get('title') or '')[:90]}"
231
+ for x in errs[:top]]
232
+ if "overall_status" in d: # release gate
233
+ out = {"status": d["overall_status"], **{k: d.get("summary", {}).get(k)
234
+ for k in ("pass", "warn", "fail", "skip")}}
235
+ out["not_passed"] = [f"{c.get('status')}: {c.get('check_id')}: {c.get('message', '')[:80]}"
236
+ for c in d.get("checks", []) if c.get("status") in ("warn", "fail")][:top]
237
+ return out
238
+
239
+
240
+ def run_steps(ctx, steps, choice=None):
241
+ """Run the named steps in dependency order. A failure is recorded and later steps that
242
+ need its output are skipped. Returns {step: summary}."""
243
+ choice = choice or {}
244
+ results = {}
245
+ for step in [s for s in ORDER if s in steps]:
246
+ impl = choice.get(step, "kicad_happy")
247
+ fn = REGISTRY.get(step, {}).get(impl)
248
+ if fn is None:
249
+ results[step] = {"error": f"no implementation {impl!r}"}
250
+ continue
251
+ try:
252
+ path = fn(ctx)
253
+ ctx.outputs[step] = path
254
+ results[step] = summarize(path)
255
+ except KeyError as e:
256
+ results[step] = {"skipped": f"needs {e.args[0]} output"}
257
+ except Exception as e: # noqa: BLE001 - recorded, run continues
258
+ results[step] = {"error": f"{type(e).__name__}: {str(e)[:300]}"}
259
+ return results
boardforge/cli.py ADDED
@@ -0,0 +1,280 @@
1
+ """boardforge command line.
2
+
3
+ boardforge run [-C DIR] [--from c3] [--to c6] physical pipeline C1-C6 (or a range)
4
+ boardforge sch|board|place|route|check|fab one stage (C1 ... C6)
5
+ boardforge review B4 gate: constraints.json against the design
6
+ boardforge datasheets status|sync|add|check|show|publish|paths
7
+ part-fact cache (datasheet PDFs, extractions)
8
+ boardforge render [--side top|bottom|both] pictures of the current board (to check placement)
9
+ boardforge status stage results from pipeline_state.json
10
+ boardforge doctor check KiCad, Freerouting, config
11
+
12
+ Output is a short summary per stage; details are in <project>/build/reports/.
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+
20
+ from boardforge import __version__, fabs, routers
21
+ from boardforge.config import Project
22
+ from boardforge.state import STAGES, TITLES, State
23
+
24
+ ALIASES = {"sch": "c1", "board": "c2", "place": "c3", "route": "c4", "check": "c5", "fab": "c6"}
25
+
26
+
27
+ def _print_stage(res, verbose):
28
+ s = res["summary"]
29
+ print(f"[{res['stage']}] {TITLES[res['stage']]}: {res['status']} ({res['seconds']} s)")
30
+ text = json.dumps(s, indent=1 if verbose else None, default=str)
31
+ print(" " + (text if verbose else text[:600] + ("..." if len(text) > 600 else "")))
32
+ for w in res["warnings"][:12]:
33
+ print(f" ! {w}")
34
+ if len(res["warnings"]) > 12:
35
+ print(f" ! ... {len(res['warnings']) - 12} more (pipeline_state.json)")
36
+
37
+
38
+ def cmd_run(args, stages):
39
+ from boardforge.physical import Pipeline
40
+
41
+ from boardforge.physical import StageError
42
+
43
+ pipe = Pipeline(Project.load(args.dir))
44
+ for w in pipe.k.warnings:
45
+ print(f"! {w}")
46
+ failed = False
47
+ try:
48
+ results = pipe.run(stages)
49
+ except StageError as e:
50
+ print(f"! {e}")
51
+ return 1
52
+ for res in results:
53
+ _print_stage(res, args.verbose)
54
+ failed |= res["status"] == "failed"
55
+ return 1 if failed else 0
56
+
57
+
58
+ def cmd_review(args):
59
+ from boardforge.review.gate import review
60
+
61
+ rep = review(Project.load(args.dir))
62
+ print(f"[review] {rep['status']}: {rep['counts']} (reports/review.json)")
63
+ for r in rep["results"]:
64
+ detail = (f"{r['gain_db']} dB at {r['at_hz']:g} Hz, limit {r['limit']}" if "gain_db" in r
65
+ else r["why"] if r["status"] == "NOT CHECKED" and r.get("why")
66
+ else ",".join(r.get("refs", [])) or r.get("why", ""))
67
+ print(f" {r['status']:11} {r['tag']:7} {r['id']}: {detail}")
68
+ for f in r.get("findings", []):
69
+ if args.verbose or f["severity"] != "info":
70
+ print(f" {f['severity']}: {f['detail']}")
71
+ if args.verbose:
72
+ for n in r.get("notes", []):
73
+ print(f" {n}")
74
+ for p in rep["problems"]:
75
+ print(f" ! {p}")
76
+ return 1 if rep["status"] == "FAIL" else 0
77
+
78
+
79
+ def cmd_datasheets(args):
80
+ import os
81
+
82
+ from boardforge import datasheets as ds
83
+
84
+ proj = Project.load(args.dir)
85
+ try:
86
+ if args.action == "status":
87
+ rows = ds.status(proj)
88
+ shown = [r for r in rows if r["wanted"] or args.all]
89
+ print(f"{'MPN':28} {'refs':14} pdf extracted quality cache")
90
+ for r in shown:
91
+ refs = ",".join(r["refs"][:3]) + ("..." if len(r["refs"]) > 3 else "")
92
+ ext = ("stale" if r["stale"] else "yes") if r["extracted"] else "-"
93
+ print(f"{r['mpn'][:28]:28} {refs[:14]:14} {'yes' if r['pdf'] else '-':3} "
94
+ f"{ext:9} {r['quality'] if r['quality'] is not None else '-':7} "
95
+ f"{'yes' if r['in_cache'] else '-'}")
96
+ no_pdf = sum(not r["pdf"] for r in shown)
97
+ todo = sum(r["pdf"] and (not r["extracted"] or bool(r["stale"])) for r in shown)
98
+ print(f"{len(shown)} parts ({len(rows) - len(shown)} passives etc. hidden; --all shows them): "
99
+ f"{no_pdf} without a PDF, {todo} to extract")
100
+ return 0
101
+ if args.action == "paths":
102
+ project, shared = ds.stores(proj)
103
+ print(f"project: {project.root}\nshared cache: {shared.root if shared else '(none)'}\n"
104
+ f"prompts: {os.path.join(ds.SKILL, 'prompts')}\nschemas: {ds.SCHEMAS}\n"
105
+ f"references: {os.path.join(ds.SKILL, 'references')}\n"
106
+ f"examples: {os.path.join(ds.SKILL, 'examples')}")
107
+ return 0
108
+ if args.action in ("add", "check", "show") and not args.mpn:
109
+ raise ValueError(f"datasheets {args.action} needs MPNs")
110
+ if args.action == "add":
111
+ if len(args.mpn) != 2:
112
+ raise ValueError("datasheets add MPN FILE_OR_URL")
113
+ print(f" added {ds.add(proj, args.mpn[0], args.mpn[1], force=args.force)}")
114
+ return 0
115
+ if args.action == "check":
116
+ bad = 0
117
+ for r in ds.check(proj, args.mpn):
118
+ ok = not r["problems"]
119
+ bad += not ok
120
+ print(f"{r['mpn']}: {'ok' if ok else 'INVALID'}, quality {r['quality']}")
121
+ for x in r["problems"]:
122
+ print(f" ! schema {x}")
123
+ for x in r["issues"]:
124
+ print(f" ? {x}")
125
+ return 1 if bad else 0
126
+ if args.action == "show":
127
+ project, shared = ds.stores(proj)
128
+ for mpn in args.mpn:
129
+ ex = project.extraction(mpn) or (shared.extraction(mpn) if shared else None)
130
+ print(ds.summary(ex) if ex else f"! {mpn}: no extraction (run the librarian)")
131
+ return 0
132
+ if args.action == "sync":
133
+ res = ds.sync(proj, fresh=args.fresh, download=not args.offline)
134
+ else:
135
+ res = ds.publish(proj, force=args.force)
136
+ except (FileNotFoundError, ValueError) as e:
137
+ print(f"! {e}")
138
+ return 1
139
+ for k, v in res.items():
140
+ print(f" {k}: {len(v)}{' ' + ', '.join(v) if v else ''}")
141
+ return 0
142
+
143
+
144
+ def cmd_render(args):
145
+ from boardforge.kicad import Kicad
146
+ from boardforge.render import render
147
+
148
+ proj = Project.load(args.dir)
149
+ sides = ("top", "bottom") if args.side == "both" else (args.side,)
150
+ try:
151
+ for f in render(proj, Kicad.find(proj.cfg["kicad"]), sides, pours=args.pours):
152
+ print(f" {os.path.relpath(f, proj.root)}")
153
+ except FileNotFoundError as e:
154
+ print(f"! {e}")
155
+ return 1
156
+ return 0
157
+
158
+
159
+ def cmd_status(args):
160
+ proj = Project.load(args.dir)
161
+ st = State(proj.state_file)
162
+ for sid in STAGES:
163
+ s = st.stage(sid)
164
+ if s:
165
+ print(f"[{sid}] {TITLES[sid]}: {s['status']} ({s.get('seconds')} s, {s.get('finished')}), "
166
+ f"{len(s.get('warnings', []))} warnings")
167
+ else:
168
+ print(f"[{sid}] {TITLES[sid]}: not run")
169
+ return 0
170
+
171
+
172
+ def cmd_doctor(args):
173
+ from boardforge.kicad import Kicad, KicadError
174
+
175
+ ok = True
176
+ try:
177
+ proj = Project.load(args.dir)
178
+ cfg = proj.cfg
179
+ print(f"config: {proj.root}/boardforge.toml (design {cfg['design']['file']})")
180
+ except (ValueError, FileNotFoundError) as e:
181
+ print(f"config: {e} (checking machine settings only)")
182
+ from boardforge.config import DEFAULTS
183
+
184
+ cfg = DEFAULTS
185
+ try:
186
+ k = Kicad.find(cfg["kicad"])
187
+ print(f"kicad-cli: {k.cli} ({k.version})")
188
+ print(f"KiCad python: {k.python}")
189
+ print(f"symbols: {k.symbol_dir}\nfootprints: {k.footprint_dir}")
190
+ for w in k.warnings:
191
+ print(f"! {w}")
192
+ ok = False
193
+ except KicadError as e:
194
+ print(f"! {e}")
195
+ ok = False
196
+ for name in cfg["routing"]["chain"]:
197
+ good, msg = routers.get(name).available(cfg)
198
+ print(f"router {name}: {'ok' if good else 'MISSING'} {msg}")
199
+ ok &= good
200
+ from boardforge.review.spice_path import find_ngspice
201
+
202
+ ng = find_ngspice(cfg.get("review", {}).get("ngspice", ""))
203
+ print(f"ngspice: {ng or 'MISSING (the review gate reports SPICE checks as not checked)'}")
204
+ try:
205
+ f = fabs.get(cfg["fab"]["name"], cfg["fab"].get("tier"))
206
+ print(f"fab: {f.name} tier {f.tier_name}")
207
+ except ValueError as e:
208
+ print(f"! {e}")
209
+ ok = False
210
+ # Look the packages up without importing them: importing SKiDL creates .log/.erc files in
211
+ # the current directory.
212
+ from importlib import metadata, util
213
+
214
+ for pkg in ("skidl", "schematizer"):
215
+ if util.find_spec(pkg) is None:
216
+ print(f"! {pkg} is not installed")
217
+ ok = False
218
+ continue
219
+ try:
220
+ print(f"{pkg} {metadata.version(pkg)}")
221
+ except metadata.PackageNotFoundError:
222
+ print(f"{pkg} (version unknown)")
223
+ import os
224
+ import shutil
225
+
226
+ cache = cfg.get("datasheets", {}).get("cache", "")
227
+ print(f"datasheet cache: {os.path.expanduser(cache) if cache else '(none)'}")
228
+ print(f"pdftotext: {shutil.which('pdftotext') or 'MISSING (downloaded PDFs are not identity-checked)'}")
229
+ if util.find_spec("jsonschema") is None:
230
+ print("jsonschema: MISSING (datasheets publish can't validate extractions; "
231
+ "pip install 'boardforge[analysis]')")
232
+ return 0 if ok else 1
233
+
234
+
235
+ def main(argv=None):
236
+ ap = argparse.ArgumentParser(prog="boardforge", description=__doc__,
237
+ formatter_class=argparse.RawDescriptionHelpFormatter)
238
+ ap.add_argument("--version", action="version", version=__version__)
239
+ ap.add_argument("-C", "--dir", default=".", help="project directory (has boardforge.toml)")
240
+ ap.add_argument("-v", "--verbose", action="store_true", help="full stage summaries")
241
+ sub = ap.add_subparsers(dest="cmd", required=True)
242
+ r = sub.add_parser("run", help="run the physical pipeline")
243
+ r.add_argument("--from", dest="start", default="c1", choices=STAGES)
244
+ r.add_argument("--to", dest="end", default="c6", choices=STAGES)
245
+ for alias, sid in ALIASES.items():
246
+ sub.add_parser(alias, help=f"stage {sid}: {TITLES[sid]}")
247
+ sub.add_parser("review", help="B4 gate: check constraints.json")
248
+ d = sub.add_parser("datasheets", help="part-fact cache: status, sync, add, check, show, publish, paths")
249
+ d.add_argument("action", choices=("status", "sync", "add", "check", "show", "publish", "paths"))
250
+ d.add_argument("mpn", nargs="*", help="add: MPN FILE_OR_URL; check/show: MPNs")
251
+ d.add_argument("--all", action="store_true", help="status: include passives")
252
+ d.add_argument("--fresh", action="store_true", help="sync: don't copy from the shared cache")
253
+ d.add_argument("--offline", action="store_true", help="sync: don't download PDFs")
254
+ d.add_argument("--force", action="store_true",
255
+ help="publish: replace shared entries; add: skip the part-name check")
256
+ r2 = sub.add_parser("render", help="SVG/PNG of the current board (placed or routed)")
257
+ r2.add_argument("--side", choices=("top", "bottom", "both"), default="top")
258
+ r2.add_argument("--pours", action="store_true", help="keep copper pours (hide tracks under them)")
259
+ sub.add_parser("status", help="show stage results")
260
+ sub.add_parser("doctor", help="check the environment")
261
+ args = ap.parse_args(argv)
262
+
263
+ if args.cmd == "run":
264
+ i, j = STAGES.index(args.start), STAGES.index(args.end)
265
+ return cmd_run(args, STAGES[i:j + 1])
266
+ if args.cmd in ALIASES:
267
+ return cmd_run(args, (ALIASES[args.cmd],))
268
+ if args.cmd == "status":
269
+ return cmd_status(args)
270
+ if args.cmd == "render":
271
+ return cmd_render(args)
272
+ if args.cmd == "review":
273
+ return cmd_review(args)
274
+ if args.cmd == "datasheets":
275
+ return cmd_datasheets(args)
276
+ return cmd_doctor(args)
277
+
278
+
279
+ if __name__ == "__main__":
280
+ sys.exit(main())