lvkit 0.6.2__tar.gz → 0.6.3__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 (348) hide show
  1. {lvkit-0.6.2 → lvkit-0.6.3}/.gitignore +4 -0
  2. {lvkit-0.6.2 → lvkit-0.6.3}/PKG-INFO +1 -1
  3. {lvkit-0.6.2 → lvkit-0.6.3}/pyproject.toml +1 -1
  4. lvkit-0.6.3/scripts/build_example_pages.sh +56 -0
  5. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/__init__.py +1 -1
  6. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/cache_paths.py +55 -0
  7. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/connector_pane_patterns.json +8 -8
  8. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/primitives.json +17 -16
  9. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/docs/generate.py +3 -4
  10. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/core.py +3 -0
  11. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/loading.py +5 -0
  12. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/queries.py +40 -7
  13. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/index/store.py +7 -54
  14. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/output_cache.py +10 -3
  15. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/__init__.py +17 -6
  16. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/draw.py +2 -2
  17. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/icons.py +18 -0
  18. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/nodes.py +9 -3
  19. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/scene.py +8 -0
  20. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/vi_diff.py +17 -2
  21. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_output_cache.py +16 -0
  22. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_primitive_name_uniqueness.py +4 -1
  23. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_subvi_nav.py +38 -0
  24. {lvkit-0.6.2 → lvkit-0.6.3}/.pre-commit-config.yaml +0 -0
  25. {lvkit-0.6.2 → lvkit-0.6.3}/LICENSE +0 -0
  26. {lvkit-0.6.2 → lvkit-0.6.3}/NOTICE +0 -0
  27. {lvkit-0.6.2 → lvkit-0.6.3}/README.md +0 -0
  28. {lvkit-0.6.2 → lvkit-0.6.3}/deploy/cloudrun/README.md +0 -0
  29. {lvkit-0.6.2 → lvkit-0.6.3}/docs/_internal/xml-schema/README.md +0 -0
  30. {lvkit-0.6.2 → lvkit-0.6.3}/editors/vscode/LICENSE +0 -0
  31. {lvkit-0.6.2 → lvkit-0.6.3}/editors/vscode/README.md +0 -0
  32. {lvkit-0.6.2 → lvkit-0.6.3}/mcpb/README.md +0 -0
  33. {lvkit-0.6.2 → lvkit-0.6.3}/plugin/README.md +0 -0
  34. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/audit_primitive_consistency.py +0 -0
  35. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/audit_terminal_order.py +0 -0
  36. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/audit_unresolved.py +0 -0
  37. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/build_hero.py +0 -0
  38. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/build_ni_function_catalog.py +0 -0
  39. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/connector_geometry_audit.py +0 -0
  40. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/dump_primitive_context.py +0 -0
  41. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/extract_corpus.py +0 -0
  42. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/gen_semantics_probe.py +0 -0
  43. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/generate_docs.py +0 -0
  44. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/generate_driver_data.py +0 -0
  45. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/generate_python.py +0 -0
  46. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/list_unknown_primitives.py +0 -0
  47. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/populate_vilib.py +0 -0
  48. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/pull_samples.sh +0 -0
  49. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/recategorize_vilib.py +0 -0
  50. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/render_gallery.py +0 -0
  51. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/reproduce_openg_corpus.py +0 -0
  52. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/study_unwired_direction.py +0 -0
  53. {lvkit-0.6.2 → lvkit-0.6.3}/scripts/wire_decode_probe.py +0 -0
  54. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/__main__.py +0 -0
  55. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/_data.py +0 -0
  56. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/_pylabview_patches.py +0 -0
  57. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/cli.py +0 -0
  58. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/README.md +0 -0
  59. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/__init__.py +0 -0
  60. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/ast_optimizer.py +0 -0
  61. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/ast_utils.py +0 -0
  62. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/builder.py +0 -0
  63. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/class_builder.py +0 -0
  64. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/condition_builder.py +0 -0
  65. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/context.py +0 -0
  66. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/dataflow.py +0 -0
  67. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/elementwise.py +0 -0
  68. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/error_handler.py +0 -0
  69. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/expressions.py +0 -0
  70. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/fragment.py +0 -0
  71. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/function.py +0 -0
  72. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/imports.py +0 -0
  73. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/__init__.py +0 -0
  74. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/base.py +0 -0
  75. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/case.py +0 -0
  76. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/compound.py +0 -0
  77. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/constant.py +0 -0
  78. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/event.py +0 -0
  79. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/first_call.py +0 -0
  80. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/formula.py +0 -0
  81. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/in_place.py +0 -0
  82. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/invoke_node.py +0 -0
  83. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/loop.py +0 -0
  84. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/nmux.py +0 -0
  85. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/primitive.py +0 -0
  86. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/printf.py +0 -0
  87. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/property_node.py +0 -0
  88. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/queue_ops.py +0 -0
  89. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/sequence.py +0 -0
  90. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/nodes/subvi.py +0 -0
  91. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/stubs.py +0 -0
  92. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/codegen/unresolved.py +0 -0
  93. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/connector_geometry.py +0 -0
  94. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/connector_pane_geometry.py +0 -0
  95. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/_index.json +0 -0
  96. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/daqmx.json +0 -0
  97. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/nidcpower.json +0 -0
  98. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/nidigital.json +0 -0
  99. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/nidmm.json +0 -0
  100. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/nifgen.json +0 -0
  101. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/niscope.json +0 -0
  102. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/niswitch.json +0 -0
  103. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/serial.json +0 -0
  104. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/drivers/visa.json +0 -0
  105. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/labview_error_codes.json +0 -0
  106. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/openg/_index.json +0 -0
  107. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/openg/array.json +0 -0
  108. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/openg/file.json +0 -0
  109. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/openg/string.json +0 -0
  110. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/openg/time.json +0 -0
  111. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/openg/variant.json +0 -0
  112. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/_index.json +0 -0
  113. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/_pending_terminals.json +0 -0
  114. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/_types.json +0 -0
  115. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/addons.json +0 -0
  116. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/application-control.json +0 -0
  117. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/array.json +0 -0
  118. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/boolean.json +0 -0
  119. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/cluster-class-variant.json +0 -0
  120. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/collection.json +0 -0
  121. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/comparison.json +0 -0
  122. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/connectivity.json +0 -0
  123. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/control-simulation.json +0 -0
  124. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/data-communication.json +0 -0
  125. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/dialog-user-interface.json +0 -0
  126. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/express.json +0 -0
  127. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/file-io.json +0 -0
  128. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/graphics-sound.json +0 -0
  129. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/instrument-io.json +0 -0
  130. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/mathematics.json +0 -0
  131. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/measurement-io.json +0 -0
  132. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/numeric.json +0 -0
  133. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/report-generation.json +0 -0
  134. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/signal-processing.json +0 -0
  135. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/string.json +0 -0
  136. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/structures.json +0 -0
  137. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/synchronization.json +0 -0
  138. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/timing.json +0 -0
  139. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/data/vilib/waveform.json +0 -0
  140. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/docs/__init__.py +0 -0
  141. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/docs/html_generator.py +0 -0
  142. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/docs/template.css +0 -0
  143. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/docs/utils.py +0 -0
  144. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/extractor.py +0 -0
  145. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/flattened_typedesc.py +0 -0
  146. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/formula/__init__.py +0 -0
  147. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/formula/cparser.py +0 -0
  148. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/formula/emit.py +0 -0
  149. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/formula/lexer.py +0 -0
  150. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/formula/nodes.py +0 -0
  151. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/__init__.py +0 -0
  152. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/analysis.py +0 -0
  153. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/builders/__init__.py +0 -0
  154. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/builders/context.py +0 -0
  155. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/builders/operations.py +0 -0
  156. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/builders/refs.py +0 -0
  157. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/builders/structures.py +0 -0
  158. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/construction.py +0 -0
  159. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/describe.py +0 -0
  160. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/diff.py +0 -0
  161. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/interface_order.py +0 -0
  162. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/models.py +0 -0
  163. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/netlist.py +0 -0
  164. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/op_walk.py +0 -0
  165. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/operations.py +0 -0
  166. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/graph/parallel_parse.py +0 -0
  167. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/index/__init__.py +0 -0
  168. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/index/build.py +0 -0
  169. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/index/model.py +0 -0
  170. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/index/project.py +0 -0
  171. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/index/query.py +0 -0
  172. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/index/sql.py +0 -0
  173. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/labview_error.py +0 -0
  174. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/labview_error_codes.py +0 -0
  175. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/labview_queue.py +0 -0
  176. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/load_mode.py +0 -0
  177. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/lv_detect.py +0 -0
  178. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/mcp/__init__.py +0 -0
  179. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/mcp/schemas.py +0 -0
  180. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/mcp/server.py +0 -0
  181. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/models.py +0 -0
  182. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/num_format.py +0 -0
  183. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/__init__.py +0 -0
  184. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/conp_types.py +0 -0
  185. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/constants.py +0 -0
  186. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/defaults.py +0 -0
  187. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/flags.py +0 -0
  188. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/fp_heap_type.py +0 -0
  189. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/front_panel.py +0 -0
  190. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/layout.py +0 -0
  191. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/metadata.py +0 -0
  192. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/models.py +0 -0
  193. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/naming.py +0 -0
  194. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/node_types.py +0 -0
  195. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/__init__.py +0 -0
  196. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/base.py +0 -0
  197. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/case.py +0 -0
  198. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/constant.py +0 -0
  199. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/decompose.py +0 -0
  200. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/disable.py +0 -0
  201. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/event.py +0 -0
  202. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/loop.py +0 -0
  203. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/nodes/sequence.py +0 -0
  204. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/type_mapping.py +0 -0
  205. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/type_resolution.py +0 -0
  206. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/utils.py +0 -0
  207. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/vi.py +0 -0
  208. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/parser/wire_table.py +0 -0
  209. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/pipeline.py +0 -0
  210. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/primitive_resolver.py +0 -0
  211. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/project_store.py +0 -0
  212. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/py.typed +0 -0
  213. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/backend.py +0 -0
  214. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/connector_pane.py +0 -0
  215. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/connector_pane_panel.py +0 -0
  216. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/diff_viewer.py +0 -0
  217. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/glyph.py +0 -0
  218. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/help_tip.py +0 -0
  219. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/lane_pass.py +0 -0
  220. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/properties_panel.py +0 -0
  221. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/render_viewer.py +0 -0
  222. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/style.py +0 -0
  223. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/templates/diff_viewer.html +0 -0
  224. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/templates/render_viewer.html +0 -0
  225. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/theme_control.py +0 -0
  226. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/theme_web.py +0 -0
  227. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/render/wire_router.py +0 -0
  228. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/runtime/__init__.py +0 -0
  229. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/runtime/lv.py +0 -0
  230. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/skill_templates/__init__.py +0 -0
  231. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/skill_templates/lvkit/SKILL.md +0 -0
  232. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/skill_templates/lvkit-convert/SKILL.md +0 -0
  233. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/skill_templates/lvkit-describe/SKILL.md +0 -0
  234. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/skill_templates/lvkit-document/SKILL.md +0 -0
  235. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/skill_templates/lvkit-query/SKILL.md +0 -0
  236. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/skill_templates/lvkit-resolve/SKILL.md +0 -0
  237. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/skill_templates/lvkit-review/SKILL.md +0 -0
  238. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/structure.py +0 -0
  239. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/terminal_collector.py +0 -0
  240. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/text_encoding.py +0 -0
  241. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/tools/__init__.py +0 -0
  242. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/tools/connector_geometry_profile.py +0 -0
  243. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/type_defaults.py +0 -0
  244. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/unresolved.py +0 -0
  245. {lvkit-0.6.2 → lvkit-0.6.3}/src/lvkit/vilib_resolver.py +0 -0
  246. {lvkit-0.6.2 → lvkit-0.6.3}/tests/__init__.py +0 -0
  247. {lvkit-0.6.2 → lvkit-0.6.3}/tests/conftest.py +0 -0
  248. {lvkit-0.6.2 → lvkit-0.6.3}/tests/helpers.py +0 -0
  249. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_array_ops_codegen.py +0 -0
  250. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_ast_builder.py +0 -0
  251. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_ast_optimizer.py +0 -0
  252. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_case_codegen.py +0 -0
  253. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_case_parser.py +0 -0
  254. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_class_field_walkup.py +0 -0
  255. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_class_parent_linkinfo.py +0 -0
  256. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_class_properties.py +0 -0
  257. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_cli.py +0 -0
  258. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_cli_cache.py +0 -0
  259. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_cli_graph_ops.py +0 -0
  260. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_cli_query.py +0 -0
  261. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_codegen_context.py +0 -0
  262. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_compound_codegen.py +0 -0
  263. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_condition_builder.py +0 -0
  264. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_connector_geometry.py +0 -0
  265. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_connector_pane_label_recovery.py +0 -0
  266. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_connector_pane_patterns.py +0 -0
  267. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_connector_pane_render.py +0 -0
  268. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_conp_types.py +0 -0
  269. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_constant_decoding.py +0 -0
  270. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_dependency_path_refs.py +0 -0
  271. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_determinism.py +0 -0
  272. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_diff.py +0 -0
  273. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_diff_properties.py +0 -0
  274. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_diff_viewer.py +0 -0
  275. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_disable_graph.py +0 -0
  276. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_disable_parser.py +0 -0
  277. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_docs_class_hierarchy.py +0 -0
  278. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_docs_render.py +0 -0
  279. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_driver_codegen.py +0 -0
  280. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_dynamic_dispatch.py +0 -0
  281. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_e2e_codegen.py +0 -0
  282. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_elementwise.py +0 -0
  283. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_error_codegen.py +0 -0
  284. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_error_handling.py +0 -0
  285. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_error_terminal_label_fallback.py +0 -0
  286. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_event_codegen.py +0 -0
  287. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_event_parser.py +0 -0
  288. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_extraction_cache.py +0 -0
  289. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_extractor_concurrency.py +0 -0
  290. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_extractor_long_path.py +0 -0
  291. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_first_call_codegen.py +0 -0
  292. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_formula_codegen.py +0 -0
  293. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_formula_runtime.py +0 -0
  294. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_formula_semantics.py +0 -0
  295. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_formula_transpile.py +0 -0
  296. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_fp_heap_type.py +0 -0
  297. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_frame_attribution.py +0 -0
  298. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_frame_zplane.py +0 -0
  299. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_glyphs.py +0 -0
  300. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_graceful_degradation.py +0 -0
  301. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_help_tip.py +0 -0
  302. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_import_startup.py +0 -0
  303. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_in_place_codegen.py +0 -0
  304. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_index.py +0 -0
  305. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_index_sql.py +0 -0
  306. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_index_warm_no_downgrade.py +0 -0
  307. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_interface_order.py +0 -0
  308. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_label_extraction.py +0 -0
  309. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_labview_queue.py +0 -0
  310. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_load_mode.py +0 -0
  311. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_load_upgrade.py +0 -0
  312. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_loop_codegen.py +0 -0
  313. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_lv_detect.py +0 -0
  314. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_lvpy.py +0 -0
  315. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_mcp_cache.py +0 -0
  316. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_mcp_evals.py +0 -0
  317. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_mcp_roots.py +0 -0
  318. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_naming.py +0 -0
  319. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_netlist.py +0 -0
  320. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_netlist_json.py +0 -0
  321. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_new_codegen.py +0 -0
  322. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_node_properties.py +0 -0
  323. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_parallel_codegen.py +0 -0
  324. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_parallel_parse.py +0 -0
  325. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_parser.py +0 -0
  326. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_parser_regression.py +0 -0
  327. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_primitive_positional_order.py +0 -0
  328. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_primitive_resolutions.py +0 -0
  329. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_progressive_index.py +0 -0
  330. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_progressive_warm.py +0 -0
  331. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_project_store.py +0 -0
  332. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_queue_codegen.py +0 -0
  333. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_render.py +0 -0
  334. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_render_viewer.py +0 -0
  335. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_sequence.py +0 -0
  336. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_soft_codegen.py +0 -0
  337. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_structure_lvproj.py +0 -0
  338. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_text_encoding.py +0 -0
  339. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_type_driven_fixes.py +0 -0
  340. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_type_fields.py +0 -0
  341. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_type_loading.py +0 -0
  342. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_type_mapping.py +0 -0
  343. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_unknown_nodes.py +0 -0
  344. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_unresolved.py +0 -0
  345. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_vi_graph.py +0 -0
  346. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_vi_properties.py +0 -0
  347. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_viewer_properties.py +0 -0
  348. {lvkit-0.6.2 → lvkit-0.6.3}/tests/test_vilib_docurls.py +0 -0
@@ -133,3 +133,7 @@ experiments/
133
133
 
134
134
  # Claude Code agent worktrees (transient)
135
135
  .claude/worktrees/
136
+
137
+ # CI deploy keys — never commit
138
+ pragmatest_deploy
139
+ pragmatest_deploy.pub
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lvkit
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: Understand and convert LabVIEW VIs to Python without a LabVIEW license
5
5
  Project-URL: Repository, https://github.com/pragmatest-dev/lvkit
6
6
  Project-URL: Issues, https://github.com/pragmatest-dev/lvkit/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lvkit"
7
- version = "0.6.2"
7
+ version = "0.6.3"
8
8
  description = "Understand and convert LabVIEW VIs to Python without a LabVIEW license"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # Regenerate the pragmatest.com lvkit example pages from PUBLIC sources cloned at
3
+ # build time — no local corpus, no hand-staged fixtures. Every input is a public
4
+ # git ref, so this runs identically on a fresh CI runner and on a dev box.
5
+ #
6
+ # scripts/build_example_pages.sh <output-dir>
7
+ #
8
+ # Produces, under <output-dir>:
9
+ # render/testcase.html — JKI VI Tester TestCase/run.vi @ tag 3.0.0
10
+ # diff/testcase.html — that VI 3.0.0 (base) -> 3.0.1 (head)
11
+ # render/md5.html — OpenG MD5 Message Digest (Binary String)__ogtk.vi
12
+ #
13
+ # Sources (both OSI-approved, redistributable):
14
+ # JKI-VI-Tester BSD-3-Clause github.com/JKISoftware/JKI-VI-Tester
15
+ # OpenG Toolkit BSD-3-Clause reproduced from upstream .vip via scripts/
16
+ set -euo pipefail
17
+
18
+ OUT="${1:?usage: build_example_pages.sh <output-dir>}"
19
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ mkdir -p "$OUT/render" "$OUT/diff"
21
+ OUT="$(cd "$OUT" && pwd)" # absolute — lvk() renders from $ROOT, OUT must not be relative to it
22
+ WORK="$(mktemp -d)"
23
+ trap 'rm -rf "$WORK"' EXIT
24
+
25
+ lvk() { ( cd "$ROOT" && uv run lvkit "$@" ); }
26
+
27
+ # --- JKI VI Tester: TestCase/run.vi at 3.0.0 (base) vs 3.0.1 (head) -----------
28
+ # run_base = the committed run.vi at tag 3.0.0; run_head = the same file at 3.0.1
29
+ # (JKI wrapped several nodes in a new Case between those releases). Both are real
30
+ # public commits — extracted with `git show`, never hand-edited.
31
+ JKI="$WORK/JKI-VI-Tester"
32
+ RUNVI="source/Classes/TestCase/run.vi"
33
+ echo "clone JKI-VI-Tester (BSD-3-Clause)"
34
+ git clone --quiet https://github.com/JKISoftware/JKI-VI-Tester.git "$JKI"
35
+ git -C "$JKI" show 3.0.0:"$RUNVI" > "$WORK/run_base.vi"
36
+ git -C "$JKI" show 3.0.1:"$RUNVI" > "$WORK/run_head.vi"
37
+
38
+ echo "→ render: testcase ($OUT/render/testcase.html)"
39
+ lvk render "$WORK/run_base.vi" --format html --search-path "$JKI/source" \
40
+ --no-cache -o "$OUT/render/testcase.html"
41
+
42
+ echo "→ diff: testcase ($OUT/diff/testcase.html)"
43
+ lvk diff "$WORK/run_base.vi" "$WORK/run_head.vi" --format html \
44
+ --search-path "$JKI/source" --no-cache -o "$OUT/diff/testcase.html"
45
+
46
+ # --- OpenG MD5 (BSD-3-Clause), reproduced from upstream .vip packages ---------
47
+ OPENG="$WORK/openg"
48
+ echo "reproduce OpenG corpus (BSD-3-Clause; fetches upstream .vip)"
49
+ uv run python "$ROOT/scripts/reproduce_openg_corpus.py" "$OPENG"
50
+ MD5="$OPENG/File Group 0/user.lib/_OpenG.lib/md5/md5.llb/MD5 Message Digest (Binary String)__ogtk.vi"
51
+
52
+ echo "→ render: md5 ($OUT/render/md5.html)"
53
+ lvk render "$MD5" --format html --search-path "$OPENG" \
54
+ --no-cache -o "$OUT/render/md5.html"
55
+
56
+ echo "done → $OUT"
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- __version__ = "0.6.2"
7
+ __version__ = "0.6.3"
8
8
 
9
9
  # The public API is exposed LAZILY (PEP 562 module ``__getattr__``): the graph /
10
10
  # parser / structure stacks (networkx, pydantic, pylabview, PIL — ~230 ms) load
@@ -33,6 +33,7 @@ legibly separated at the top.
33
33
 
34
34
  from __future__ import annotations
35
35
 
36
+ import functools
36
37
  import hashlib
37
38
  import json
38
39
  import os
@@ -201,6 +202,60 @@ def sha256_file(path: Path) -> str:
201
202
  return hashlib.sha256(Path(path).read_bytes()).hexdigest()
202
203
 
203
204
 
205
+ # Top-level package subdirs that DEMONSTRABLY feed NO cached artifact — verified
206
+ # absent from the import closure of the fact/render producers and pinned by
207
+ # ``test_facts_fingerprint_skips_only_non_facts_dirs``. Skipping them means
208
+ # editing codegen / docs / the formula transpiler / the MCP transport layer
209
+ # doesn't force a needless rebuild. (Verified: ``render/`` imports none of these
210
+ # — Formula NODES are drawn from graph models, not the ``formula/`` transpiler.)
211
+ #
212
+ # This is an EXCLUDE list on purpose: the default for any file — including a
213
+ # brand-new module or ``data/`` table — is to be hashed, so a new dependency can
214
+ # never silently escape the fingerprint. The only failure mode (a dep ADDED under
215
+ # one of these dirs) trips the guard test, which fails loudly rather than letting
216
+ # staleness back in.
217
+ _FINGERPRINT_SKIP_DIRS = frozenset({"codegen", "docs", "formula", "mcp"})
218
+
219
+
220
+ @functools.lru_cache(maxsize=1)
221
+ def source_fingerprint() -> str:
222
+ """A hash of lvkit's own SOURCE + bundled data, so EVERY derived cache (the
223
+ SQLite index AND the render/diff output cache) self-invalidates the moment
224
+ that code changes — with no manual version to bump. This is the SINGLE shared
225
+ invalidation used by all caches; do not reintroduce a per-cache version.
226
+
227
+ A cache keyed only on input VI bytes cannot see a change to lvkit's own logic
228
+ (the VI bytes are identical) — so an edit to the parser/graph/render/index
229
+ code, or to a ``data/`` primitive/vilib table, would otherwise keep serving
230
+ output built by the OLD code. That is silent staleness. A hand-bumped version
231
+ can't defend against it because in active development the bump is forgotten
232
+ far more often than not.
233
+
234
+ Hashes every file in the package EXCEPT the ``_FINGERPRINT_SKIP_DIRS`` known
235
+ to feed no cache. Skipping is done by directory (a coarse, verifiable unit),
236
+ never by hand-picking modules — an *include* list is a guess that can miss a
237
+ transitive dep and reintroduce staleness, whereas defaulting to "hash it"
238
+ over-invalidates at worst (one extra cold rebuild). Memoized: the source
239
+ cannot change within a live process (Python does not hot-reload), and a dev
240
+ editing lvkit restarts it before the next run anyway.
241
+ """
242
+ import lvkit
243
+
244
+ pkg = Path(lvkit.__file__).resolve().parent
245
+ h = hashlib.sha256()
246
+ for f in sorted(pkg.rglob("*")):
247
+ if not f.is_file() or f.suffix == ".pyc" or "__pycache__" in f.parts:
248
+ continue
249
+ rel = f.relative_to(pkg)
250
+ if rel.parts[0] in _FINGERPRINT_SKIP_DIRS:
251
+ continue
252
+ h.update(rel.as_posix().encode())
253
+ h.update(b"\0")
254
+ h.update(f.read_bytes())
255
+ h.update(b"\0")
256
+ return h.hexdigest()
257
+
258
+
204
259
  def meta_fresh(vi_path: Path, meta_path: Path, extra: dict | None = None) -> bool:
205
260
  """True if the cached artifact described by ``meta_path`` is still valid for
206
261
  ``vi_path``.
@@ -125,15 +125,15 @@
125
125
  "name": "special-7",
126
126
  "terminal_count": 7,
127
127
  "corpus_files": 2,
128
- "grid": {"cols": 3, "rows": 4},
128
+ "grid": {"cols": 3, "rows": 6},
129
129
  "cells": [
130
- {"index": 0, "col": 0, "row": 0, "colspan": 1, "rowspan": 1},
131
- {"index": 1, "col": 1, "row": 0, "colspan": 1, "rowspan": 1},
132
- {"index": 5, "col": 2, "row": 0, "colspan": 1, "rowspan": 2},
133
- {"index": 2, "col": 0, "row": 1, "colspan": 2, "rowspan": 2},
134
- {"index": 6, "col": 2, "row": 2, "colspan": 1, "rowspan": 2},
135
- {"index": 3, "col": 0, "row": 3, "colspan": 1, "rowspan": 1},
136
- {"index": 4, "col": 1, "row": 3, "colspan": 1, "rowspan": 1}
130
+ {"index": 0, "col": 0, "row": 0, "colspan": 1, "rowspan": 2},
131
+ {"index": 1, "col": 1, "row": 0, "colspan": 1, "rowspan": 2},
132
+ {"index": 5, "col": 2, "row": 0, "colspan": 1, "rowspan": 3},
133
+ {"index": 2, "col": 0, "row": 2, "colspan": 2, "rowspan": 2},
134
+ {"index": 6, "col": 2, "row": 3, "colspan": 1, "rowspan": 3},
135
+ {"index": 3, "col": 0, "row": 4, "colspan": 1, "rowspan": 2},
136
+ {"index": 4, "col": 1, "row": 4, "colspan": 1, "rowspan": 2}
137
137
  ]
138
138
  },
139
139
  "4818": {
@@ -305,40 +305,41 @@
305
305
  "verified": true
306
306
  },
307
307
  "1540": {
308
- "name": "Format Value",
308
+ "name": "Array To Spreadsheet String",
309
309
  "terminals": [
310
310
  {
311
311
  "index": 0,
312
312
  "direction": "out",
313
- "name": "result string",
314
- "type": "polymorphic"
313
+ "name": "spreadsheet_string",
314
+ "type": "String"
315
315
  },
316
316
  {
317
317
  "index": 1,
318
318
  "direction": "in",
319
- "name": "format string",
320
- "type": "polymorphic"
319
+ "name": "delimiter",
320
+ "type": "String",
321
+ "default_value": "'\\t'"
321
322
  },
322
323
  {
323
324
  "index": 2,
324
325
  "direction": "in",
325
- "name": "value",
326
- "type": "polymorphic"
326
+ "name": "array",
327
+ "type": "Array"
327
328
  },
328
329
  {
329
330
  "index": 3,
330
331
  "direction": "in",
331
- "name": "initial string",
332
- "type": "polymorphic",
333
- "default_value": "''"
332
+ "name": "format_string",
333
+ "type": "String"
334
334
  }
335
335
  ],
336
336
  "python_code": {
337
- "result_string": "in_3 + (in_1 % in_2)"
337
+ "spreadsheet_string": "(in_1 or '\\t').join(in_3 % x for x in in_2)"
338
338
  },
339
339
  "inline": true,
340
340
  "verified": true,
341
- "doc_url": "https://www.ni.com/docs/en-US/bundle/labview-api-ref/page/functions/format-value.html"
341
+ "doc_url": "https://www.ni.com/docs/en-US/bundle/labview-api-ref/page/functions/array-to-spreadsheet-string.html",
342
+ "note": "parmIndex from OBSERVED caller dataflow (OpenG '1D Array to String' control wiring: in_1<-delimiter, in_2<-array, in_3<-format '%s'): in_1=delimiter (default Tab), in_2=array, in_3=format string, out=spreadsheet string. python_code covers the common 1-D case; full N-D (Tab columns / EOL rows / page headers) would need an lvkit runtime helper, as with 1539."
342
343
  },
343
344
  "1050": {
344
345
  "name": "Add",
@@ -2206,12 +2207,12 @@
2206
2207
  "note": "Search 1D Array -- returns the index of element (searching from start_index) or -1 if not found. Delete From Array (class='aDelete') is a separate node_type entry. Was mislabeled 'Delete From Array'."
2207
2208
  },
2208
2209
  "1902": {
2209
- "name": "Reverse 1D Array",
2210
+ "name": "Transpose 2D Array",
2210
2211
  "terminals": [
2211
2212
  {
2212
2213
  "index": 0,
2213
2214
  "direction": "out",
2214
- "name": "reversed_array",
2215
+ "name": "transposed_array",
2215
2216
  "type": "Array"
2216
2217
  },
2217
2218
  {
@@ -2222,11 +2223,11 @@
2222
2223
  }
2223
2224
  ],
2224
2225
  "python_code": {
2225
- "reversed_array": "in_1[::-1]"
2226
+ "transposed_array": "[list(r) for r in zip(*in_1)]"
2226
2227
  },
2227
2228
  "inline": true,
2228
2229
  "verified": true,
2229
- "doc_url": "https://www.ni.com/docs/en-US/bundle/labview-api-ref/page/functions/reverse-1d-array.html"
2230
+ "doc_url": "https://www.ni.com/docs/en-US/bundle/labview-api-ref/page/functions/transpose-2d-array.html"
2230
2231
  },
2231
2232
  "1128": {
2232
2233
  "name": "Not A Number/Path/Refnum?",
@@ -17,6 +17,7 @@ from lvkit.graph import InMemoryVIGraph
17
17
  from lvkit.graph.loading import LoadMode
18
18
  from lvkit.models import CaseOperation, SequenceOperation
19
19
  from lvkit.render import render_vi_with_subvis
20
+ from lvkit.render.icons import resolve_icon_png
20
21
  from lvkit.structure import parse_lvclass, parse_lvlib
21
22
 
22
23
  logger = logging.getLogger(__name__)
@@ -106,11 +107,9 @@ def _collect_icons(graph: InMemoryVIGraph, output_dir: Path) -> dict[str, str]:
106
107
 
107
108
  vi_path = Path(vi_path)
108
109
 
109
- icon_source = vi_path.parent / f"{vi_path.stem}_ICON.png"
110
- if not icon_source.exists():
111
- icon_source = vi_path.parent / f"{vi_path.stem}_icl8.png"
110
+ icon_source = resolve_icon_png(vi_path.parent, vi_path.stem)
112
111
 
113
- if icon_source.exists():
112
+ if icon_source is not None:
114
113
  safe_name = vi_name.replace(":", "_").replace("/", "_").replace("\\", "_")
115
114
  safe_name = safe_name.replace(" ", "_").replace(".", "_")
116
115
  icon_dest = icons_dir / f"{safe_name}.png"
@@ -193,6 +193,9 @@ class InMemoryVIGraph(
193
193
  self._dep_load_mode: dict[str, LoadMode] = {}
194
194
  # Source file paths: vi_name -> Path to original .vi file
195
195
  self._source_paths: dict[str, Path] = {}
196
+ # vi_key -> qualified DISPLAY name (qname/basename). Display only, never a
197
+ # lookup key — resolve_vi_name still returns the vi_key (path) identity.
198
+ self._vi_display_names: dict[str, str] = {}
196
199
  # Search paths the graph was loaded with, retained so decoration-only
197
200
  # lookups (a SubVI's own _ICON.png) can locate a project-local .vi by
198
201
  # name even under a MINIMAL load that never walks the SubVI tree.
@@ -190,6 +190,7 @@ class LoadingMixin:
190
190
  _loaded_vis: set[str]
191
191
  _dep_load_mode: dict[str, LoadMode]
192
192
  _source_paths: dict[str, Path]
193
+ _vi_display_names: dict[str, str]
193
194
  _vi_metadata: dict[str, VIMetadata]
194
195
  _vi_properties: dict[str, VIProperties]
195
196
  _vi_health: dict[str, VIHealth]
@@ -814,6 +815,10 @@ class LoadingMixin:
814
815
  visited.add(vi_key)
815
816
 
816
817
  self._source_paths[vi_key] = source_file
818
+ # vi_key -> its qualified DISPLAY name (qname, else bare filename). Never a
819
+ # lookup key (vi_key/path is the identity); this is the human-facing title
820
+ # only, so the render/viewer shows "Class.lvclass:vi.vi" not the abspath.
821
+ self._vi_display_names[vi_key] = own_qname
817
822
  # Reverse indexes: display-name / qname -> [vi_key], for resolve_vi_name.
818
823
  # List-valued: on-disk duplicates share a name/qname across distinct keys.
819
824
  if vi_key not in self._name_to_keys.setdefault(unqualified_name, []):
@@ -65,6 +65,7 @@ class QueryMixin:
65
65
  _qname_to_keys: dict[str, list[str]]
66
66
  _loaded_vis: set[str]
67
67
  _source_paths: dict[str, Path]
68
+ _vi_display_names: dict[str, str]
68
69
  _vi_metadata: dict[str, VIMetadata]
69
70
  _vi_properties: dict[str, VIProperties]
70
71
  _vi_health: dict[str, VIHealth]
@@ -119,6 +120,19 @@ class QueryMixin:
119
120
  return self._pick_vi_key(keys)
120
121
  return vi_name
121
122
 
123
+ def vi_display_name(self, vi_name: str) -> str:
124
+ """The VI's human-facing qualified name (``Class.lvclass:vi.vi``, else the
125
+ bare filename) for titles/headers. DISPLAY ONLY — never a lookup key. The
126
+ identity remains the ``vi_key`` (path) from :meth:`resolve_vi_name`; this
127
+ maps that key to its qname so the viewer shows the qualified name instead
128
+ of the absolute source path. Falls back to the resolved key's basename,
129
+ then the input, when no display name was recorded."""
130
+ key = self.resolve_vi_name(vi_name)
131
+ display = self._vi_display_names.get(key)
132
+ if display:
133
+ return display
134
+ return Path(key).name or vi_name
135
+
122
136
  def _pick_vi_key(self, keys: list[str]) -> str:
123
137
  """Deterministically choose among duplicate ``vi_key``s that share a
124
138
  name/qname. Prefer the richest copy -- a stripped built copy loses
@@ -154,13 +168,32 @@ class QueryMixin:
154
168
  """Get the source file path for a VI (accepts a vi_key, qname, or name)."""
155
169
  return self._source_paths.get(self.resolve_vi_name(vi_name))
156
170
 
157
- def locate_vi_file(self, vi_name: str) -> Path | None:
158
- """Best-effort on-disk ``.vi`` for a SubVI by name: an already-loaded
159
- source first, else a filename search of the graph's retained search
160
- paths. Decoration-only (SubVI icons) — never forces a load. Under a
161
- MINIMAL load the SubVIs aren't in ``_source_paths``, so this filename
162
- search is what lets a project-local SubVI's own ``_ICON.png`` resolve.
163
- The search-path index is built once, lazily, and cached."""
171
+ def locate_vi_file(
172
+ self, vi_name: str, qualified_path: str | None = None
173
+ ) -> Path | None:
174
+ """Best-effort on-disk ``.vi`` for a SubVI. Decoration-only (SubVI icons /
175
+ click-nav) — never forces a load.
176
+
177
+ ``qualified_path`` (the caller's project-relative token, e.g.
178
+ ``/Lib1/Do.vi``) resolves to the EXACT file by PATH: joined to each search
179
+ root, the file AT that path IS the SubVI. This is what disambiguates two
180
+ SubVIs that share a bare filename across libraries (``Lib1/Do.vi`` vs
181
+ ``Lib2/Do.vi``) — a bare-name lookup collides, the path does not. A
182
+ ``<vilib>``/``<userlib>`` token is skipped here (resolved separately).
183
+
184
+ Falls back to an already-loaded source, then a bare-filename search of the
185
+ retained search paths (the last-resort path, which CAN collide on
186
+ duplicate names — callers with a ``qualified_path`` never reach it). Under
187
+ a MINIMAL load the SubVIs aren't in ``_source_paths``, so the filename
188
+ search is what lets a project-local SubVI's own ``_ICON.png`` resolve; its
189
+ index is built once, lazily, and cached."""
190
+ if qualified_path:
191
+ rel = qualified_path.replace("\\", "/").lstrip("/")
192
+ if rel and not rel.startswith("<"):
193
+ for root in self._search_paths:
194
+ candidate = root / rel
195
+ if candidate.is_file():
196
+ return candidate
164
197
  loaded = self._source_paths.get(self.resolve_vi_name(vi_name))
165
198
  if loaded is not None:
166
199
  return loaded
@@ -29,8 +29,6 @@ version bump to forget. See ``_ensure_facts_version``.
29
29
 
30
30
  from __future__ import annotations
31
31
 
32
- import functools
33
- import hashlib
34
32
  import json
35
33
  import sqlite3
36
34
  from collections.abc import Iterable
@@ -289,58 +287,13 @@ _FLAT_PROPERTY_COLUMNS: tuple[tuple[str, bool], ...] = (
289
287
  )
290
288
 
291
289
 
292
- # Top-level package subdirs that DEMONSTRABLY don't feed the index — verified
293
- # absent from the import closure of ``index.build``/``index.store`` (the code
294
- # that actually produces facts), and pinned by
295
- # ``test_facts_fingerprint_skips_only_non_facts_dirs``. Skipping them means
296
- # editing codegen / docs / the formula transpiler / the MCP transport layer
297
- # doesn't force a needless full-corpus rebuild.
298
- #
299
- # This is an EXCLUDE list on purpose: the default for any file — including a
300
- # brand-new module or ``data/`` table — is to be hashed, so a new facts
301
- # dependency can never silently escape the fingerprint. The only failure mode
302
- # (a facts dep ADDED under one of these dirs) trips the guard test, which fails
303
- # loudly rather than letting staleness back in.
304
- _FINGERPRINT_SKIP_DIRS = frozenset({"codegen", "docs", "formula", "mcp"})
305
-
306
-
307
- @functools.lru_cache(maxsize=1)
308
- def _facts_fingerprint() -> str:
309
- """A hash of lvkit's own facts-producing SOURCE + bundled data, so the index
310
- self-invalidates the moment that changes — with no manual version to bump.
311
-
312
- The index is a CACHE of what lvkit derives from each VI. Per-VI freshness is
313
- keyed on VI bytes (``meta.content_sha``), but that cannot see a change to
314
- lvkit's *extraction logic* (the VI bytes are identical) — so an edit to the
315
- parser/graph/index code, or to a ``data/`` primitive/vilib table, would
316
- otherwise keep serving facts built by the OLD code. That is silent staleness,
317
- and it corrupts everything reading the index: queries, the MCP server, and
318
- evals. A hand-bumped ``SCHEMA_VERSION`` can't defend against it because in
319
- active development the bump is forgotten far more often than not.
320
-
321
- Hashes every file in the package EXCEPT the ``_FINGERPRINT_SKIP_DIRS`` known
322
- not to feed facts. Skipping is done by directory (a coarse, verifiable unit),
323
- never by hand-picking the facts modules — an *include* list is a guess that
324
- can miss a transitive dep and reintroduce staleness, whereas defaulting to
325
- "hash it" over-invalidates at worst (one extra cold rebuild). Memoized: the
326
- source cannot change within a live process (Python does not hot-reload), and
327
- a dev editing lvkit restarts it before the next run anyway.
328
- """
329
- import lvkit
330
-
331
- pkg = Path(lvkit.__file__).resolve().parent
332
- h = hashlib.sha256()
333
- for f in sorted(pkg.rglob("*")):
334
- if not f.is_file() or f.suffix == ".pyc" or "__pycache__" in f.parts:
335
- continue
336
- rel = f.relative_to(pkg)
337
- if rel.parts[0] in _FINGERPRINT_SKIP_DIRS:
338
- continue
339
- h.update(rel.as_posix().encode())
340
- h.update(b"\0")
341
- h.update(f.read_bytes())
342
- h.update(b"\0")
343
- return h.hexdigest()
290
+ # The source fingerprint that invalidates the index now lives in ``cache_paths``
291
+ # (the light, dependency-free module) so EVERY cache — this SQLite index and the
292
+ # render/diff output cache — shares the exact SAME invalidation. Re-exported here
293
+ # under the historical names the index code + guard test reference. See
294
+ # ``cache_paths.source_fingerprint`` / ``_FINGERPRINT_SKIP_DIRS``.
295
+ _FINGERPRINT_SKIP_DIRS = cache_paths._FINGERPRINT_SKIP_DIRS
296
+ _facts_fingerprint = cache_paths.source_fingerprint
344
297
 
345
298
 
346
299
  def db_path(project_root: Path) -> Path:
@@ -17,9 +17,12 @@ Addressing mirrors extraction (see :mod:`lvkit.cache_paths`):
17
17
  addressed** in a flat ``<cache>/adhoc/render/<sha>.<ext>`` and swept by TTL.
18
18
 
19
19
  Freshness = the VI-content signal ``cache_paths.meta_fresh`` already uses PLUS
20
- the lvkit ``version``, text encoding, and an ``options`` tag — so a VI edit, an
21
- lvkit upgrade, a system-code-page change, or different ``--format``/``--theme``
22
- is a miss.
20
+ the SHARED ``cache_paths.source_fingerprint()`` (the same hash-of-lvkit-source
21
+ that invalidates the SQLite index — so a renderer/graph/parser/data edit busts
22
+ this cache too, with no version bump), the lvkit ``version`` (kept for readable
23
+ meta), text encoding, and an ``options`` tag — so a VI edit, an lvkit CODE
24
+ change, a system-code-page change, or different ``--format``/``--theme`` is a
25
+ miss.
23
26
  """
24
27
 
25
28
  from __future__ import annotations
@@ -154,6 +157,7 @@ def lookup_render(input_path: Path, fmt: str, options: str, version: str) -> str
154
157
  meta_path,
155
158
  {
156
159
  "lvkit_version": version,
160
+ "source_fingerprint": cache_paths.source_fingerprint(),
157
161
  "options": options,
158
162
  "text_encoding": labview_text_encoding(),
159
163
  },
@@ -172,6 +176,7 @@ def store_render(
172
176
  body,
173
177
  {
174
178
  "lvkit_version": version,
179
+ "source_fingerprint": cache_paths.source_fingerprint(),
175
180
  "options": options,
176
181
  "kind": "render",
177
182
  "text_encoding": labview_text_encoding(),
@@ -191,6 +196,7 @@ def lookup_diff(
191
196
  meta_path,
192
197
  {
193
198
  "lvkit_version": version,
199
+ "source_fingerprint": cache_paths.source_fingerprint(),
194
200
  "options": options,
195
201
  "before_sha": before_sha,
196
202
  "text_encoding": labview_text_encoding(),
@@ -215,6 +221,7 @@ def store_diff(
215
221
  body,
216
222
  {
217
223
  "lvkit_version": version,
224
+ "source_fingerprint": cache_paths.source_fingerprint(),
218
225
  "options": options,
219
226
  "before_sha": before_sha,
220
227
  "kind": "diff",
@@ -344,6 +344,7 @@ def render_vi(
344
344
  interactive=interactive,
345
345
  extra_css=extra_css,
346
346
  extra_attrs=extra_attrs,
347
+ display_name=graph.vi_display_name(vi_name),
347
348
  )
348
349
  aside = _vi_aside_svg(graph, vi_name, scene, theme)
349
350
  if aside:
@@ -377,7 +378,7 @@ def _vi_aside_svg(
377
378
  panel = render_connector_pane_help(
378
379
  ctx.connector_pattern_id,
379
380
  pane_terminals((*ctx.inputs, *ctx.outputs)),
380
- title=vi_name.split(":")[-1],
381
+ title=graph.vi_display_name(vi_name).split(":")[-1],
381
382
  description=ctx.description,
382
383
  icon_uri=icon_uri,
383
384
  theme=theme,
@@ -438,6 +439,7 @@ def _render_scene_svg(
438
439
  interactive: bool = True,
439
440
  extra_css: str = "",
440
441
  extra_attrs: dict[str, str] | None = None,
442
+ display_name: str | None = None,
441
443
  ) -> str:
442
444
  """Draw an already-built ``Scene`` to a self-contained interactive SVG.
443
445
 
@@ -445,7 +447,11 @@ def _render_scene_svg(
445
447
  dark ``--lv-*`` palette block for ``dark``/``auto`` theme modes. Empty keeps
446
448
  the ``<style>`` byte-identical to the legacy light output. ``extra_attrs``
447
449
  (``None`` by default) becomes root-``<svg>`` ``data-*`` attributes — e.g.
448
- ``render_vi``'s ``data-lv-properties``/``data-lv-health`` (task #19)."""
450
+ ``render_vi``'s ``data-lv-properties``/``data-lv-health`` (task #19).
451
+ ``display_name`` (defaults to ``vi_name``) is the DISPLAY-only accessible
452
+ name (aria-label): the qualified name, so the SVG never exposes the abspath
453
+ ``vi_name`` key. ``vi_name`` still drives the scoping ``root_id``."""
454
+ title = display_name if display_name is not None else vi_name
449
455
  backend = SvgBackend()
450
456
  draw_scene(scene, backend, theme)
451
457
  style = _BASE_CSS + extra_css
@@ -467,7 +473,7 @@ def _render_scene_svg(
467
473
  script = "\n".join(scripts).replace("__ROOT_ID__", json.dumps(root_id))
468
474
  return backend.render(
469
475
  scene.bounds,
470
- title=vi_name,
476
+ title=title,
471
477
  script=script,
472
478
  root_id=root_id,
473
479
  style=style,
@@ -475,7 +481,7 @@ def _render_scene_svg(
475
481
  )
476
482
  return backend.render(
477
483
  scene.bounds,
478
- title=vi_name,
484
+ title=title,
479
485
  style=style,
480
486
  extra_attrs=extra_attrs,
481
487
  )
@@ -505,7 +511,10 @@ def render_vi_with_subvis(
505
511
  if isinstance(rn.node, VINode):
506
512
  target = rn.node.qualified_name or rn.node.name
507
513
  if target:
508
- subvis[rn.node.id] = target
514
+ # Key by the DOCUMENT-LOCAL dom_id — the same value emitted as
515
+ # ``data-node`` — so the docs NAV map matches the SVG (and neither
516
+ # carries the abspath vi_key).
517
+ subvis[rn.dom_id] = target
509
518
  return _render_scene_svg(scene, vi_name, theme), subvis
510
519
 
511
520
 
@@ -586,4 +595,6 @@ def render_vi_file_titled(
586
595
 
587
596
  warm_all_loaded(graph)
588
597
  svg = render_vi(graph, name, theme=theme, theme_mode=theme_mode)
589
- return svg, name
598
+ # `name` is the vi_key (canonical source path) — the render identity. The
599
+ # TITLE is display-only: the qualified name, never the abspath key.
600
+ return svg, graph.vi_display_name(name)
@@ -362,7 +362,7 @@ def draw_node(node: RenderNode, backend: Backend, theme: Theme = DEFAULT_THEME)
362
362
  # the node's group to open even when it otherwise carries no tooltip.
363
363
  group_needed = bool(tooltip) or node.subvi_rel is not None
364
364
  if group_needed:
365
- data = {"node": node.node.id}
365
+ data = {"node": node.dom_id}
366
366
  if node.subvi_rel is not None:
367
367
  data["lv-vi-rel"] = node.subvi_rel
368
368
  # A resolvable node (primitive / vi.lib VI) links to its NI docs page
@@ -936,7 +936,7 @@ def _draw_connector_panel(node: RenderNode, backend: Backend, theme: Theme) -> N
936
936
  # All panel styling (hidden-until-hover, no pointer capture, drop shadow)
937
937
  # lives in the .lv-help CSS rule (render/__init__.py _BASE_CSS) — the single
938
938
  # source both the in-place reveal and a host's cloned overlay inherit.
939
- backend.begin_group(cls="lv-help", data={"node": node.node.id})
939
+ backend.begin_group(cls="lv-help", data={"node": node.dom_id})
940
940
  backend.rect(
941
941
  0.0,
942
942
  0.0,
@@ -16,6 +16,24 @@ import base64
16
16
  from collections import defaultdict
17
17
  from pathlib import Path
18
18
 
19
+ # LabVIEW dumps a VI's icon in several depth layers next to its heap XML. The
20
+ # block diagram shows the COLOR icon, so prefer the 256-color layer (icl8), then
21
+ # the 16-color layer (icl4), and only fall back to the 1-bit black-and-white
22
+ # ``_ICON.png`` when no color layer exists — otherwise a class/library banner
23
+ # color is silently dropped (e.g. Lib1/Lib2 members render mono).
24
+ _ICON_LAYER_SUFFIXES = ("_icl8.png", "_icl4.png", "_ICON.png")
25
+
26
+
27
+ def resolve_icon_png(directory: Path, stem: str) -> Path | None:
28
+ """Best available icon PNG for ``<stem>`` under ``directory``: 256-color,
29
+ else 16-color, else the 1-bit B&W fallback; ``None`` if none exist."""
30
+ for suffix in _ICON_LAYER_SUFFIXES:
31
+ candidate = directory / f"{stem}{suffix}"
32
+ if candidate.is_file():
33
+ return candidate
34
+ return None
35
+
36
+
19
37
  _WHITE_CUTOFF = 238 # channel value at/above which a pixel counts as background
20
38
 
21
39