firestarter 3.2.1__tar.gz → 3.2.2__tar.gz

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 (211) hide show
  1. {firestarter-3.2.1/firestarter.egg-info → firestarter-3.2.2}/PKG-INFO +1 -1
  2. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/_version.py +3 -3
  3. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/chip_resolver.py +44 -1
  4. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/cli_handlers.py +37 -7
  5. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/database.py +140 -33
  6. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/eprom_info.py +35 -7
  7. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/exceptions.py +6 -0
  8. {firestarter-3.2.1 → firestarter-3.2.2/firestarter.egg-info}/PKG-INFO +1 -1
  9. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter.egg-info/SOURCES.txt +1 -0
  10. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter.egg-info/scm_file_list.json +1 -0
  11. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter.egg-info/scm_version.json +2 -2
  12. {firestarter-3.2.1 → firestarter-3.2.2}/tests/__snapshots__/test_characterization.ambr +29 -28
  13. {firestarter-3.2.1 → firestarter-3.2.2}/tests/plan_corpus.py +11 -1
  14. firestarter-3.2.2/tests/test_chip_name_resolution.py +199 -0
  15. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_chip_resolver.py +2 -1
  16. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_derive_plan_structural_sentinel.py +20 -34
  17. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_write_blank_guard_pinning.py +10 -6
  18. {firestarter-3.2.1 → firestarter-3.2.2}/LICENSE +0 -0
  19. {firestarter-3.2.1 → firestarter-3.2.2}/MANIFEST.in +0 -0
  20. {firestarter-3.2.1 → firestarter-3.2.2}/README.md +0 -0
  21. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/__init__.py +0 -0
  22. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/address_parser.py +0 -0
  23. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/avr_tool.py +0 -0
  24. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/channel.py +0 -0
  25. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/chip_test.py +0 -0
  26. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/codec.py +0 -0
  27. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/compare.py +0 -0
  28. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/config.py +0 -0
  29. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/constants.py +0 -0
  30. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/data/chip_database.json +0 -0
  31. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/data/pinouts.json +0 -0
  32. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/diagnostic_report.py +0 -0
  33. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/dip24_2816_erase_gate.py +0 -0
  34. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/eprom_operations.py +0 -0
  35. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/erase_support.py +0 -0
  36. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/firmware.py +0 -0
  37. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/flash4_erase_gate.py +0 -0
  38. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/frame_parser.py +0 -0
  39. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/fw_release_gate.py +0 -0
  40. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/hardware.py +0 -0
  41. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/ic_layout.py +0 -0
  42. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/jp5_gate.py +0 -0
  43. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/jumper_table.py +0 -0
  44. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/lock_status.py +0 -0
  45. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/log_capture.py +0 -0
  46. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/logging_utils.py +0 -0
  47. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/main.py +0 -0
  48. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/messages.py +0 -0
  49. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/page_size_gate.py +0 -0
  50. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/protection_readability.py +0 -0
  51. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/py32_dfu.py +0 -0
  52. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/sdp_capability.py +0 -0
  53. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/sdp_honesty.py +0 -0
  54. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/serial_comm.py +0 -0
  55. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/submit.py +0 -0
  56. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/timestamps.py +0 -0
  57. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/transport_counters.py +0 -0
  58. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/utils.py +0 -0
  59. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/vpp_display.py +0 -0
  60. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter/write_blank_guard.py +0 -0
  61. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter.egg-info/dependency_links.txt +0 -0
  62. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter.egg-info/entry_points.txt +0 -0
  63. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter.egg-info/requires.txt +0 -0
  64. {firestarter-3.2.1 → firestarter-3.2.2}/firestarter.egg-info/top_level.txt +0 -0
  65. {firestarter-3.2.1 → firestarter-3.2.2}/pyproject.toml +0 -0
  66. {firestarter-3.2.1 → firestarter-3.2.2}/setup.cfg +0 -0
  67. {firestarter-3.2.1 → firestarter-3.2.2}/tests/__init__.py +0 -0
  68. {firestarter-3.2.1 → firestarter-3.2.2}/tests/conftest.py +0 -0
  69. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fake_chip.py +0 -0
  70. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/part_number_delta.json +0 -0
  71. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/report_shapes.py +0 -0
  72. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/at28c256-full-all-ok-sdp.json +0 -0
  73. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/attr01-status-axis-transport-fault.json +0 -0
  74. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/gh20-at28c256-fail.json +0 -0
  75. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/gh23-w27e257-fail.json +0 -0
  76. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/gh28-m27c512-fail.json +0 -0
  77. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/gh47-sst27sf512-pass.json +0 -0
  78. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-all-ok.json +0 -0
  79. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-blank-check-bad.json +0 -0
  80. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-canonical-name.json +0 -0
  81. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-comma-joined-name.json +0 -0
  82. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-runs-1.json +0 -0
  83. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/prune03-synthesized-fingerprint-match.json +0 -0
  84. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/sst27sf512-full-all-ok.json +0 -0
  85. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/sst27sf512-six-step-readback-gated.json +0 -0
  86. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/sst27sf512-six-step.json +0 -0
  87. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/synthetic-arm4-empty-results.json +0 -0
  88. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/synthetic-arm4-no-ok.json +0 -0
  89. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/uv-slot-write-pass.json +0 -0
  90. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/reports/w27e257-full-all-ok.json +0 -0
  91. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/shape_ids.json +0 -0
  92. {firestarter-3.2.1 → firestarter-3.2.2}/tests/fixtures/synthetic_nonzero_chip_id.py +0 -0
  93. {firestarter-3.2.1 → firestarter-3.2.2}/tests/golden/build_db_part_specific_constants.json +0 -0
  94. {firestarter-3.2.1 → firestarter-3.2.2}/tests/golden/chip_database_field_inventory.json +0 -0
  95. {firestarter-3.2.1 → firestarter-3.2.2}/tests/golden/wire_dict.json +0 -0
  96. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_address_parser.py +0 -0
  97. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_b15_page_size_corroboration.py +0 -0
  98. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_blast_radius_invariance.py +0 -0
  99. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_boot_block_hint.py +0 -0
  100. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_budget_failure_render.py +0 -0
  101. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_bug_characterization.py +0 -0
  102. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_build_db_constant_census.py +0 -0
  103. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_build_db_inclusion.py +0 -0
  104. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_build_db_interpret_timing.py +0 -0
  105. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_build_db_pinout_fork.py +0 -0
  106. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_canonical_part_number.py +0 -0
  107. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_characterization.py +0 -0
  108. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_chip_database_field_inventory.py +0 -0
  109. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_chip_test.py +0 -0
  110. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_chip_test_blank_check_order.py +0 -0
  111. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_chip_test_cycle.py +0 -0
  112. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_chip_test_sdp_leg.py +0 -0
  113. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_chip_test_timing.py +0 -0
  114. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_chip_test_uv_slot_write.py +0 -0
  115. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_cli_handlers.py +0 -0
  116. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_click_group_gate_hook.py +0 -0
  117. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_cobs.py +0 -0
  118. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_codec.py +0 -0
  119. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_codec_format_message.py +0 -0
  120. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_compare.py +0 -0
  121. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_config.py +0 -0
  122. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_config_band_refusal.py +0 -0
  123. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_connect_cost_harness.py +0 -0
  124. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_consistency_check.py +0 -0
  125. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_coverage_floor_v18.py +0 -0
  126. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_database_conversion.py +0 -0
  127. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_datasheet_overrides.py +0 -0
  128. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_decoder.py +0 -0
  129. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_derive_plan_no_drop_sweep.py +0 -0
  130. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_dev_group_channel_gating.py +0 -0
  131. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_dev_test_cmd.py +0 -0
  132. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_dev_tools_channel_gate.py +0 -0
  133. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_devtest_firmware_error_propagation.py +0 -0
  134. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_dfu_opcode_anchors.py +0 -0
  135. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_diagnostic_report.py +0 -0
  136. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_dip24_2816_erase_gate.py +0 -0
  137. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_endpoint_constants.py +0 -0
  138. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_eprom_database.py +0 -0
  139. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_eprom_info.py +0 -0
  140. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_eprom_operations.py +0 -0
  141. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_erase_blank_step_nonregression.py +0 -0
  142. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_erase_flag_invariants.py +0 -0
  143. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_error_code_seam.py +0 -0
  144. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_even_block.py +0 -0
  145. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_extra_chips_supplement.py +0 -0
  146. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_firmware_install.py +0 -0
  147. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_flash4_erase_gate.py +0 -0
  148. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_fw_list_failure_vs_empty.py +0 -0
  149. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_fw_port_targeting_and_blind_install.py +0 -0
  150. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_fw_release_gate.py +0 -0
  151. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_fw_update_dead_endpoint.py +0 -0
  152. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_fw_update_path_gate.py +0 -0
  153. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_fw_version_guard.py +0 -0
  154. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_fwguard.py +0 -0
  155. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_hardware.py +0 -0
  156. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_hw_revision_gate.py +0 -0
  157. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_ic_layout.py +0 -0
  158. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_info_vpp_and_erase.py +0 -0
  159. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_jp5_gate.py +0 -0
  160. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_jumper_table.py +0 -0
  161. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_lock_status_class_partition.py +0 -0
  162. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_lock_status_cli.py +0 -0
  163. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_lock_status_wire.py +0 -0
  164. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_log_capture.py +0 -0
  165. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_logging_utils.py +0 -0
  166. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_matrix_artifact.py +0 -0
  167. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_page_size_alignment_refusal.py +0 -0
  168. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_page_size_invariants.py +0 -0
  169. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_page_size_write_refusal.py +0 -0
  170. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_parse_devtest_issue.py +0 -0
  171. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_probe_spurious_setup_ack.py +0 -0
  172. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_programming_vcc_census.py +0 -0
  173. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_protect_flags_doc_measurements.py +0 -0
  174. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_protection_resolution.py +0 -0
  175. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_protection_status_catalog.py +0 -0
  176. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_protection_table_citations.py +0 -0
  177. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_protocol_not_implemented.py +0 -0
  178. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_protocol_not_implemented_production_path.py +0 -0
  179. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_provenance.py +0 -0
  180. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_pulse_us_override.py +0 -0
  181. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_py32_channel_gating.py +0 -0
  182. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_py32_dfu.py +0 -0
  183. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_py32_flash_map_host.py +0 -0
  184. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_py32_pyusb_absent.py +0 -0
  185. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_pyusb_api_surface.py +0 -0
  186. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_sdp_capability.py +0 -0
  187. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_sdp_db_invariant.py +0 -0
  188. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_sdp_honesty.py +0 -0
  189. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_serial_characterization.py +0 -0
  190. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_serial_comm.py +0 -0
  191. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_session_lease.py +0 -0
  192. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_setup_refusals.py +0 -0
  193. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_submit.py +0 -0
  194. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_timestamps.py +0 -0
  195. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_transport_counters.py +0 -0
  196. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_utils.py +0 -0
  197. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_uv_mask.py +0 -0
  198. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_validate_family_cmd.py +0 -0
  199. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_validate_oracle.py +0 -0
  200. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_validated_parts_regression_surface.py +0 -0
  201. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_vcc_margin_rail.py +0 -0
  202. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_vpp_decode_table.py +0 -0
  203. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_vpp_rail_classification.py +0 -0
  204. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_wire_dict_equivalence.py +0 -0
  205. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_write_blank_guard.py +0 -0
  206. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_write_guard_port_pinning.py +0 -0
  207. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_write_progress.py +0 -0
  208. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_write_response_budget.py +0 -0
  209. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_write_skip_erase_0x0d.py +0 -0
  210. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_write_skip_sdp_unlock.py +0 -0
  211. {firestarter-3.2.1 → firestarter-3.2.2}/tests/test_write_verify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firestarter
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: EPROM programmer software for the Relatively Universal ROM Programmer
5
5
  Author-email: Henrik Olsson <henols@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/henols/firestarter_app
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '3.2.1'
22
- __version_tuple__ = version_tuple = (3, 2, 1)
21
+ __version__ = version = '3.2.2'
22
+ __version_tuple__ = version_tuple = (3, 2, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g4526a89'
24
+ __commit_id__ = commit_id = 'ga7d562c'
@@ -10,11 +10,50 @@ Single chokepoint between CLI dispatch and the EPROM database lookup/conversion.
10
10
  from typing import Any
11
11
 
12
12
  from firestarter.database import EpromDatabase
13
- from firestarter.exceptions import ChipNotFoundError, ChipNotImplementedError
13
+ from firestarter.exceptions import (
14
+ AmbiguousChipNameError,
15
+ ChipNotFoundError,
16
+ ChipNotImplementedError,
17
+ )
14
18
 
15
19
  _NON_ZERO_WIRE_FIELDS = ("memory-size", "pin-count", "vpp_mv")
16
20
 
17
21
 
22
+ def _qualified_choices(name: str, db: EpromDatabase) -> list[str]:
23
+ """The row `name` selects first, then each other row it selects that
24
+ programs differently, all as qualified names. Empty when one row, or rows
25
+ that program the same, are selected."""
26
+ alternatives = db.ambiguous_alternatives(name)
27
+ if not alternatives:
28
+ return []
29
+ raw_config, manufacturer = db.get_eprom_config(name)
30
+ return [db.qualified_chip_name(name, raw_config, manufacturer), *alternatives]
31
+
32
+
33
+ def refuse_if_ambiguous(name: str, db: EpromDatabase) -> None:
34
+ """Refuse a name that selects chips that program differently: guessing
35
+ one would drive the wrong VPP, algorithm or timing."""
36
+ choices = _qualified_choices(name, db)
37
+ if choices:
38
+ listed = ", ".join(f"'{c}'" for c in choices)
39
+ raise AmbiguousChipNameError(
40
+ f"'{name}' names chips that program differently. Name one: {listed}."
41
+ )
42
+
43
+
44
+ def describe_ambiguity(name: str, db: EpromDatabase) -> str | None:
45
+ """For display commands, which use the first row: the other rows `name`
46
+ selects, or None."""
47
+ choices = _qualified_choices(name, db)
48
+ if not choices:
49
+ return None
50
+ others = ", ".join(f"'{c}'" for c in choices[1:])
51
+ return (
52
+ f"'{name}' also names chips that program differently: {others}. "
53
+ f"This shows '{choices[0]}'."
54
+ )
55
+
56
+
18
57
  def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
19
58
  """Resolve a chip name to its programmer-config dict.
20
59
 
@@ -26,6 +65,9 @@ def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
26
65
  conversion (``convert_to_programmer`` returns ``{}``) are treated as the
27
66
  not-found condition.
28
67
 
68
+ Raises ``AmbiguousChipNameError`` (a ``ChipNotFoundError``) when the name
69
+ selects rows that program differently; the message names each of them.
70
+
29
71
  Raises ``ChipNotImplementedError`` when the chip exists in the database but
30
72
  its ``support_status`` is not ``"supported"`` (covers protocol-not-implemented,
31
73
  adapter-required, and vpp-exceeds-max). This guard fires BEFORE any wire dict
@@ -49,6 +91,7 @@ def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
49
91
  # have a support_status, so raise ChipNotFoundError immediately.
50
92
  if raw_config is None:
51
93
  raise ChipNotFoundError(name)
94
+ refuse_if_ambiguous(name, db)
52
95
 
53
96
  # Support-status guard: refuse every program-capable operation
54
97
  # for non-supported chips BEFORE convert_to_programmer builds any wire dict.
@@ -38,7 +38,11 @@ from firestarter.channel import (
38
38
  dev_command_gate_message,
39
39
  is_dev_tools_enabled,
40
40
  )
41
- from firestarter.chip_resolver import resolve_chip
41
+ from firestarter.chip_resolver import (
42
+ describe_ambiguity,
43
+ refuse_if_ambiguous,
44
+ resolve_chip,
45
+ )
42
46
  from firestarter.chip_test import (
43
47
  OP_ID,
44
48
  SDP_HOLD_NOT_RUN,
@@ -160,10 +164,17 @@ def _complete_eprom(
160
164
  semantics at main.py:39-45.
161
165
  """ # noqa: E501
162
166
  db = EpromDatabase()
163
- return [
164
- click.shell_completion.CompletionItem(e["name"])
167
+ qualified = db.names_needing_qualifier()
168
+ # A row whose name also names chips that program differently is offered
169
+ # by the qualified name that selects it.
170
+ names = (
171
+ qualified.get((e["manufacturer"], e["name"]), e["name"])
165
172
  for e in db.get_eproms()
166
- if e["name"].lower().startswith(incomplete.lower())
173
+ )
174
+ return [
175
+ click.shell_completion.CompletionItem(name)
176
+ for name in names
177
+ if name.lower().startswith(incomplete.lower())
167
178
  ]
168
179
 
169
180
 
@@ -486,7 +497,11 @@ def _list_cmd(app: AppContext) -> None:
486
497
  """List all EPROMs in the database."""
487
498
  eprom_data_list = app.db.get_eproms()
488
499
  if eprom_data_list:
489
- print_eprom_list_table(eprom_data_list, app.eprom_presenter.spec_builder)
500
+ print_eprom_list_table(
501
+ eprom_data_list,
502
+ app.eprom_presenter.spec_builder,
503
+ app.db.names_needing_qualifier(),
504
+ )
490
505
  sys.exit(0)
491
506
  sys.exit(1)
492
507
 
@@ -504,6 +519,9 @@ def info(app: AppContext, eprom: str, config: bool, adapter: bool) -> None:
504
519
  logger.error(f"EPROM '{eprom}' not found in database.")
505
520
  sys.exit(1)
506
521
 
522
+ ambiguity = describe_ambiguity(eprom, app.db)
523
+ if ambiguity:
524
+ logger.warning(ambiguity)
507
525
  eprom_data_for_programmer = app.db.convert_to_programmer(eprom_details)
508
526
  raw_config_data, manufacturer = app.db.get_eprom_config(eprom)
509
527
 
@@ -534,7 +552,11 @@ def search(app: AppContext, text: str) -> None:
534
552
  """Search for EPROMs in the database."""
535
553
  search_results = app.db.search_eprom(text)
536
554
  if search_results:
537
- print_eprom_list_table(search_results, app.eprom_presenter.spec_builder)
555
+ print_eprom_list_table(
556
+ search_results,
557
+ app.eprom_presenter.spec_builder,
558
+ app.db.names_needing_qualifier(),
559
+ )
538
560
  sys.exit(0)
539
561
  sys.exit(1)
540
562
 
@@ -1505,7 +1527,9 @@ def chip_id(app: AppContext, eprom: str, force: bool) -> None:
1505
1527
  for ic in found_eproms_for_detected_id
1506
1528
  ]
1507
1529
  print_eprom_list_table(
1508
- mapped_found_eproms, app.eprom_presenter.spec_builder
1530
+ mapped_found_eproms,
1531
+ app.eprom_presenter.spec_builder,
1532
+ app.db.names_needing_qualifier(),
1509
1533
  )
1510
1534
  else:
1511
1535
  logger.warning(
@@ -3058,6 +3082,8 @@ def _canonical_part_number(part_number: str | None, raw_token: str) -> str | Non
3058
3082
  def _strip_paren(s: str) -> str:
3059
3083
  return re.sub(r"\([^)]*\)", "", s).strip().lower()
3060
3084
 
3085
+ # A manufacturer qualifier (PART@MANUFACTURER) is not part of the name.
3086
+ raw_token = raw_token.partition("@")[0].strip()
3061
3087
  query = raw_token.lower()
3062
3088
  if query == part_number.lower():
3063
3089
  return part_number
@@ -3166,6 +3192,10 @@ def dev_test(app: "AppContext", chip: str, fast: bool, submit: bool) -> None:
3166
3192
  # still runs the full community-validation sweep below.
3167
3193
  if not app.db.get_eprom(chip):
3168
3194
  raise ChipNotFoundError(f"{chip}: not found in database")
3195
+ # A name that selects chips that program differently is refused here,
3196
+ # not step by step: every step would be SKIPPED and the report would
3197
+ # name no chip.
3198
+ refuse_if_ambiguous(chip, app.db)
3169
3199
 
3170
3200
  # The chip must be known to be in the DB (see above) before its
3171
3201
  # electrical type can be read, so the UV-scope resolution happens here,
@@ -14,6 +14,7 @@ return the same instance.
14
14
  import json
15
15
  import logging
16
16
  import os
17
+ import re
17
18
  from pathlib import Path
18
19
  from typing import Any
19
20
 
@@ -145,6 +146,11 @@ def _read_config_file(filename: str) -> dict:
145
146
  return {}
146
147
 
147
148
 
149
+ def _strip_mode(name: str) -> str:
150
+ """'DS1245AB(RW)' -> 'ds1245ab': the name without its mode annotation."""
151
+ return re.sub(r"\([^)]*\)", "", name).strip().lower()
152
+
153
+
148
154
  class EpromDatabase:
149
155
  """
150
156
  Manages the EPROM and pin map database for the Firestarter application.
@@ -157,6 +163,7 @@ class EpromDatabase:
157
163
  def __init__(self, skip_local_override: bool = False):
158
164
  self.proms: dict[str, Any] = {}
159
165
  self.pin_maps: dict[str, Any] = {}
166
+ self._names_needing_qualifier: dict[tuple[str, str], str] | None = None
160
167
  self._initialize_database_core(skip_local_override=skip_local_override)
161
168
  logger.debug("EpromDatabase initialized.")
162
169
 
@@ -426,46 +433,146 @@ class EpromDatabase:
426
433
  for ic_config in ics
427
434
  ]
428
435
 
436
+ def find_chip_configs(self, chip_name: str) -> list[tuple[dict, str]]:
437
+ """Return every database row `chip_name` selects, as
438
+ (config_dict, manufacturer) pairs in database order.
439
+
440
+ A name matches a row's `part_number` directly, or one of its
441
+ comma-separated aliases. Upstream infoic.xml encodes chip aliases as
442
+ comma-separated lists (e.g. "AT28C256,AT28C256E,AT28HC256"), which
443
+ build_db.py preserves in the part_number field.
444
+
445
+ infoic.xml also annotates many names with a parenthetical mode, such
446
+ as "(RW)", "(TEST)" or "(RW3.3V)" (e.g. "DS1245AB(RW),DS1245Y(RW)"). A
447
+ name matches an annotated alias with the annotation stripped, but only
448
+ when no row matches the name as typed: "DS1220(TEST)" selects the
449
+ (TEST) row, never the (RW) row that "DS1220" also matches.
450
+
451
+ The same part number can name rows of different manufacturers, which
452
+ program differently. `PART@MANUFACTURER` selects one manufacturer's
453
+ rows; see `resolve_manufacturer` for how the manufacturer matches.
454
+ """
455
+ name, at, wanted = chip_name.partition("@")
456
+ manufacturer_wanted = self.resolve_manufacturer(wanted) if at else None
457
+ if at and manufacturer_wanted is None:
458
+ return []
459
+ query = name.strip().lower()
460
+ query_stripped = _strip_mode(name)
461
+ exact: list[tuple[dict, str]] = []
462
+ stripped: list[tuple[dict, str]] = []
463
+ for manufacturer, ics in self.proms.items():
464
+ if manufacturer_wanted is not None and manufacturer != manufacturer_wanted:
465
+ continue
466
+ for ic_config in ics:
467
+ part_number = ic_config.get("part_number", "")
468
+ aliases = part_number.split(",")
469
+ if query == part_number.lower() or query in (
470
+ a.strip().lower() for a in aliases
471
+ ):
472
+ exact.append((ic_config, manufacturer))
473
+ elif query_stripped and query_stripped in (
474
+ _strip_mode(a) for a in aliases
475
+ ):
476
+ stripped.append((ic_config, manufacturer))
477
+ return exact or stripped
478
+
429
479
  def get_eprom_config(self, chip_name: str):
430
480
  """
431
481
  Retrieves the raw configuration data for a specific EPROM by its name.
432
482
  Returns (config_dict, manufacturer_str) or (None, None).
433
483
 
434
- Matches against `part_number` directly OR against any of the
435
- comma-separated alias names within it. Upstream infoic.xml encodes
436
- chip aliases as comma-separated lists (e.g.,
437
- "AT28C256,AT28C256E,AT28HC256"), which build_db.py preserves in
438
- the part_number field. Without alias-aware lookup, queries like
439
- `firestarter info AT28C256` failed despite the chip being in the DB.
440
-
441
- Also handles infoic.xml's parenthetical mode annotations: many
442
- chip names carry "(RW)", "(TEST)", "(RW3.3V)" etc. (e.g.,
443
- "DS1245AB(RW),DS1245Y(RW)"). A plain query like "DS1245AB"
444
- is normalized to match against the paren-stripped alias.
484
+ The first row `find_chip_configs` selects; see there for how a name
485
+ matches.
445
486
  """
446
- import re
447
-
448
- def _strip_paren(s):
449
- # "DS1245AB(RW)" -> "DS1245AB"; preserves the canonical chip name.
450
- return re.sub(r"\([^)]*\)", "", s).strip().lower()
487
+ matches = self.find_chip_configs(chip_name)
488
+ return matches[0] if matches else (None, None)
489
+
490
+ def resolve_manufacturer(self, wanted: str) -> str | None:
491
+ """The manufacturer `wanted` names: an exact name, ignoring case, else
492
+ the one manufacturer it is a prefix of. None when it names none, or is
493
+ a prefix of more than one."""
494
+ wanted = wanted.strip().lower()
495
+ if not wanted:
496
+ return None
497
+ names = list(self.proms)
498
+ exact = [m for m in names if m.lower() == wanted]
499
+ if exact:
500
+ return exact[0]
501
+ prefixed = [m for m in names if m.lower().startswith(wanted)]
502
+ return prefixed[0] if len(prefixed) == 1 else None
503
+
504
+ def qualified_chip_name(
505
+ self, chip_name: str, ic_config: dict, manufacturer: str
506
+ ) -> str:
507
+ """The name that selects this one row: the row's alias that `chip_name`
508
+ matched, with its mode annotation, then `@` and the manufacturer.
509
+
510
+ The manufacturer is its first word when that word names it alone, so
511
+ the name needs no shell quoting ('27C128@MICROCHIP', not
512
+ '27C128@MICROCHIP memory').
513
+ """
514
+ name = chip_name.partition("@")[0].strip()
515
+ aliases = [a.strip() for a in ic_config.get("part_number", "").split(",")]
516
+ alias = next(
517
+ (a for a in aliases if a.lower() == name.lower()),
518
+ next(
519
+ (a for a in aliases if _strip_mode(a) == _strip_mode(name)),
520
+ aliases[0],
521
+ ),
522
+ )
523
+ first_word = manufacturer.split()[0] if manufacturer.split() else manufacturer
524
+ short = (
525
+ first_word
526
+ if self.resolve_manufacturer(first_word) == manufacturer
527
+ else manufacturer
528
+ )
529
+ return f"{alias}@{short}"
530
+
531
+ def names_needing_qualifier(self) -> dict[tuple[str, str], str]:
532
+ """Every row with an alias that, typed as shown, also selects rows that
533
+ program differently, keyed (manufacturer, part_number), with the
534
+ qualified name that selects it. Such a row can only be named with
535
+ `@MANUFACTURER`."""
536
+ if self._names_needing_qualifier is None:
537
+ by_name: dict[str, list[tuple[dict, str]]] = {}
538
+ for manufacturer, ics in self.proms.items():
539
+ for ic_config in ics:
540
+ for alias in ic_config.get("part_number", "").split(","):
541
+ by_name.setdefault(alias.strip().lower(), []).append(
542
+ (ic_config, manufacturer)
543
+ )
544
+ needing: dict[tuple[str, str], str] = {}
545
+ for name, rows in by_name.items():
546
+ if len(rows) < 2:
547
+ continue
548
+ wires = [
549
+ self.convert_to_programmer(self._map_data(ic, m)) for ic, m in rows
550
+ ]
551
+ if all(w == wires[0] for w in wires[1:]):
552
+ continue
553
+ for ic, m in rows:
554
+ needing.setdefault(
555
+ (m, ic.get("part_number", "")),
556
+ self.qualified_chip_name(name, ic, m),
557
+ )
558
+ self._names_needing_qualifier = needing
559
+ return self._names_needing_qualifier
451
560
 
452
- query = chip_name.lower()
453
- query_stripped = _strip_paren(chip_name)
454
- for manufacturer, ics in self.proms.items():
455
- for ic_config in ics:
456
- part_number = ic_config.get("part_number", "")
457
- if query == part_number.lower():
458
- return ic_config, manufacturer
459
- # Alias match: split on comma + match individual aliases.
460
- # Try exact alias first, then paren-stripped alias.
461
- if "," in part_number or "(" in part_number:
462
- aliases = [a.strip().lower() for a in part_number.split(",")]
463
- if query in aliases:
464
- return ic_config, manufacturer
465
- aliases_stripped = [_strip_paren(a) for a in part_number.split(",")]
466
- if query_stripped and query_stripped in aliases_stripped:
467
- return ic_config, manufacturer
468
- return None, None
561
+ def ambiguous_alternatives(self, chip_name: str) -> list[str]:
562
+ """Name the other rows `chip_name` selects that program differently
563
+ from the one `get_eprom_config` uses, each as a qualified name that
564
+ selects that row alone. Empty when the choice cannot matter.
565
+ """
566
+ matches = self.find_chip_configs(chip_name)
567
+ if len(matches) < 2:
568
+ return []
569
+ chosen = self.convert_to_programmer(self._map_data(*matches[0]))
570
+ return [
571
+ self.qualified_chip_name(chip_name, ic_config, manufacturer)
572
+ for ic_config, manufacturer in matches[1:]
573
+ if self.convert_to_programmer(self._map_data(ic_config, manufacturer))
574
+ != chosen
575
+ ]
469
576
 
470
577
  def get_eprom(self, chip_name: str):
471
578
  """
@@ -394,9 +394,17 @@ class EpromConsolePresenter:
394
394
 
395
395
 
396
396
  # --- Helper function for printing EPROM list (for testing/CLI) ---
397
- def print_eprom_list_table(eproms_data: list, spec_builder: EpromSpecBuilder):
397
+ def print_eprom_list_table(
398
+ eproms_data: list,
399
+ spec_builder: EpromSpecBuilder,
400
+ qualified_names: dict[tuple[str, str], str] | None = None,
401
+ ):
398
402
  """Prints a list of EPROM data in a table format. For CLI/testing.
399
403
 
404
+ A row in `qualified_names` (keyed (manufacturer, name)) can only be named
405
+ with `@MANUFACTURER`; its Name cell ends in '*' and a line under the table
406
+ gives an example.
407
+
400
408
  Column layout:
401
409
  - Name: dynamic width clamped to [13, 20]; names longer than 20 chars are
402
410
  truncated with a trailing ellipsis ('…') that counts toward the 20-char cap.
@@ -413,19 +421,26 @@ def print_eprom_list_table(eproms_data: list, spec_builder: EpromSpecBuilder):
413
421
  # Compute the widest rendered name (including any [!] suffix) across all rows,
414
422
  # then clamp to the [13, 20] range. Names that would exceed 20 chars are
415
423
  # truncated to 19 chars + '…' (ellipsis counts toward the 20-char cap).
416
- def _render_name(raw_name: str, has_bus_config: bool) -> str:
424
+ qualified_names = qualified_names or {}
425
+
426
+ def _render_name(raw_name: str, has_bus_config: bool, marked: bool) -> str:
417
427
  """Return the name string as it will be rendered in the table cell."""
418
428
  name = raw_name
419
429
  if not has_bus_config:
420
430
  name = (name[:11] + "[!]") if len(name) > 11 else f"{name}[!]"
421
- if len(name) > 20:
422
- name = name[:19] + "…" # ellipsis counts toward the 20-char cap
423
- return name
431
+ cap = 19 if marked else 20
432
+ if len(name) > cap:
433
+ name = name[: cap - 1] + "…" # ellipsis counts toward the cap
434
+ return name + "*" if marked else name
424
435
 
425
- rendered_names = [
426
- _render_name(ic.get("name", ""), bool(ic.get("bus-config")))
436
+ marked_rows = [
437
+ (ic.get("manufacturer", ""), ic.get("name", "")) in qualified_names
427
438
  for ic in eproms_data
428
439
  ]
440
+ rendered_names = [
441
+ _render_name(ic.get("name", ""), bool(ic.get("bus-config")), marked)
442
+ for ic, marked in zip(eproms_data, marked_rows)
443
+ ]
429
444
  name_w = max(13, min(20, max((len(n) for n in rendered_names), default=13)))
430
445
 
431
446
  divider = f"+{'':-<{name_w + 1}}+{'':-<18}+{'':-<6}+{'':-<12}+{'':-<13}+{'':-<6}+"
@@ -462,6 +477,19 @@ def print_eprom_list_table(eproms_data: list, spec_builder: EpromSpecBuilder):
462
477
  f"| {name: <{name_w}}| {ic.get('manufacturer', ''): <17}|{ic.get('pin-count', 0): >5} | {chip_id_str: <11}| {type_str_display: <12}| {vpp_str: <5}|" # noqa: E501
463
478
  )
464
479
  logger.info(divider)
480
+ example = next(
481
+ (
482
+ qualified_names[(ic.get("manufacturer", ""), ic.get("name", ""))]
483
+ for ic, marked in zip(eproms_data, marked_rows)
484
+ if marked
485
+ ),
486
+ None,
487
+ )
488
+ if example:
489
+ logger.info(
490
+ "* This name also selects chips that program differently. "
491
+ f"Add @MANUFACTURER to choose one, e.g. {example}"
492
+ )
465
493
 
466
494
 
467
495
  # Standalone test function
@@ -195,3 +195,9 @@ class ChipNotFoundError(Exception):
195
195
  """
196
196
 
197
197
  pass
198
+
199
+
200
+ class AmbiguousChipNameError(ChipNotFoundError):
201
+ """Raised when a chip name selects database rows that program differently
202
+ (VPP, algorithm, chip ID or pulse width), so no one chip is named. The
203
+ message lists a qualified name for each of them."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firestarter
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: EPROM programmer software for the Relatively Universal ROM Programmer
5
5
  Author-email: Henrik Olsson <henols@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/henols/firestarter_app
@@ -73,6 +73,7 @@ tests/test_build_db_pinout_fork.py
73
73
  tests/test_canonical_part_number.py
74
74
  tests/test_characterization.py
75
75
  tests/test_chip_database_field_inventory.py
76
+ tests/test_chip_name_resolution.py
76
77
  tests/test_chip_resolver.py
77
78
  tests/test_chip_test.py
78
79
  tests/test_chip_test_blank_check_order.py
@@ -119,6 +119,7 @@
119
119
  "tests/test_canonical_part_number.py",
120
120
  "tests/test_characterization.py",
121
121
  "tests/test_chip_database_field_inventory.py",
122
+ "tests/test_chip_name_resolution.py",
122
123
  "tests/test_chip_resolver.py",
123
124
  "tests/test_chip_test.py",
124
125
  "tests/test_chip_test_blank_check_order.py",
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "3.2.1",
2
+ "tag": "3.2.2",
3
3
  "distance": 0,
4
- "node": "g4526a89",
4
+ "node": "ga7d562c",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-10-25"