lvkit 0.2.0__tar.gz → 0.3.0__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 (197) hide show
  1. {lvkit-0.2.0 → lvkit-0.3.0}/PKG-INFO +1 -1
  2. {lvkit-0.2.0 → lvkit-0.3.0}/pyproject.toml +1 -1
  3. lvkit-0.3.0/scripts/audit_unresolved.py +311 -0
  4. lvkit-0.3.0/scripts/dump_primitive_context.py +253 -0
  5. lvkit-0.3.0/scripts/gen_semantics_probe.py +363 -0
  6. {lvkit-0.2.0 → lvkit-0.3.0}/scripts/generate_docs.py +20 -3
  7. {lvkit-0.2.0 → lvkit-0.3.0}/scripts/generate_python.py +10 -0
  8. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/__init__.py +1 -1
  9. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/cli.py +84 -5
  10. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/ast_utils.py +8 -2
  11. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/builder.py +9 -0
  12. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/class_builder.py +3 -1
  13. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/context.py +27 -1
  14. lvkit-0.3.0/src/lvkit/codegen/elementwise.py +127 -0
  15. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/__init__.py +9 -1
  16. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/case.py +15 -1
  17. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/compound.py +29 -7
  18. lvkit-0.3.0/src/lvkit/codegen/nodes/formula.py +117 -0
  19. lvkit-0.3.0/src/lvkit/codegen/nodes/in_place.py +261 -0
  20. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/primitive.py +39 -5
  21. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/subvi.py +126 -40
  22. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/stubs.py +1 -1
  23. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/primitives.json +269 -49
  24. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/docs/generate.py +4 -0
  25. lvkit-0.3.0/src/lvkit/extractor.py +245 -0
  26. lvkit-0.3.0/src/lvkit/formula/__init__.py +33 -0
  27. lvkit-0.3.0/src/lvkit/formula/cparser.py +325 -0
  28. lvkit-0.3.0/src/lvkit/formula/emit.py +533 -0
  29. lvkit-0.3.0/src/lvkit/formula/lexer.py +112 -0
  30. lvkit-0.3.0/src/lvkit/formula/nodes.py +119 -0
  31. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/construction.py +268 -89
  32. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/core.py +83 -34
  33. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/describe.py +298 -40
  34. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/diff.py +242 -17
  35. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/loading.py +240 -12
  36. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/models.py +30 -1
  37. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/operations.py +74 -6
  38. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/queries.py +17 -8
  39. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/labview_error_codes.py +1 -1
  40. lvkit-0.3.0/src/lvkit/mcp/__init__.py +5 -0
  41. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/mcp/schemas.py +0 -31
  42. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/mcp/server.py +70 -77
  43. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/mcp/tools.py +15 -64
  44. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/models.py +28 -0
  45. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/constants.py +36 -2
  46. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/front_panel.py +5 -0
  47. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/models.py +30 -9
  48. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/node_types.py +297 -5
  49. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/nodes/__init__.py +2 -0
  50. lvkit-0.3.0/src/lvkit/parser/nodes/decompose.py +74 -0
  51. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/nodes/sequence.py +1 -1
  52. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/utils.py +2 -0
  53. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/vi.py +57 -33
  54. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/pipeline.py +38 -15
  55. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/primitive_resolver.py +6 -0
  56. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/project_store.py +14 -8
  57. lvkit-0.3.0/src/lvkit/runtime/__init__.py +1 -0
  58. lvkit-0.3.0/src/lvkit/runtime/lv.py +206 -0
  59. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/skill_templates/lvkit-convert/SKILL.md +5 -5
  60. lvkit-0.3.0/src/lvkit/skill_templates/lvkit-describe/SKILL.md +66 -0
  61. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/skill_templates/lvkit-idiomatic/SKILL.md +0 -19
  62. lvkit-0.3.0/tests/test_case_codegen.py +230 -0
  63. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_compound_codegen.py +129 -0
  64. lvkit-0.3.0/tests/test_dependency_path_refs.py +98 -0
  65. lvkit-0.3.0/tests/test_determinism.py +52 -0
  66. lvkit-0.3.0/tests/test_diff_wiring_denoise.py +184 -0
  67. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_e2e_codegen.py +78 -0
  68. lvkit-0.3.0/tests/test_elementwise.py +100 -0
  69. lvkit-0.3.0/tests/test_formula_codegen.py +126 -0
  70. lvkit-0.3.0/tests/test_formula_runtime.py +95 -0
  71. lvkit-0.3.0/tests/test_formula_semantics.py +198 -0
  72. lvkit-0.3.0/tests/test_formula_transpile.py +225 -0
  73. lvkit-0.3.0/tests/test_frame_attribution.py +172 -0
  74. lvkit-0.3.0/tests/test_in_place_codegen.py +506 -0
  75. lvkit-0.3.0/tests/test_lvpy.py +11 -0
  76. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_naming.py +5 -1
  77. lvkit-0.3.0/tests/test_primitive_resolutions.py +86 -0
  78. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_project_store.py +18 -1
  79. lvkit-0.2.0/scripts/analyze_vi.py +0 -75
  80. lvkit-0.2.0/src/lvkit/extractor.py +0 -98
  81. lvkit-0.2.0/src/lvkit/mcp/__init__.py +0 -7
  82. lvkit-0.2.0/src/lvkit/skill_templates/lvkit-describe/SKILL.md +0 -108
  83. lvkit-0.2.0/tests/test_lvpy.py +0 -8
  84. {lvkit-0.2.0 → lvkit-0.3.0}/.gitignore +0 -0
  85. {lvkit-0.2.0 → lvkit-0.3.0}/.pre-commit-config.yaml +0 -0
  86. {lvkit-0.2.0 → lvkit-0.3.0}/LICENSE +0 -0
  87. {lvkit-0.2.0 → lvkit-0.3.0}/README.md +0 -0
  88. {lvkit-0.2.0 → lvkit-0.3.0}/scripts/generate_driver_data.py +0 -0
  89. {lvkit-0.2.0 → lvkit-0.3.0}/scripts/populate_vilib.py +0 -0
  90. {lvkit-0.2.0 → lvkit-0.3.0}/scripts/sync_skills.sh +0 -0
  91. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/_data.py +0 -0
  92. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/README.md +0 -0
  93. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/__init__.py +0 -0
  94. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/ast_optimizer.py +0 -0
  95. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/condition_builder.py +0 -0
  96. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/dataflow.py +0 -0
  97. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/error_handler.py +0 -0
  98. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/expressions.py +0 -0
  99. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/fragment.py +0 -0
  100. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/function.py +0 -0
  101. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/imports.py +0 -0
  102. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/base.py +0 -0
  103. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/constant.py +0 -0
  104. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/invoke_node.py +0 -0
  105. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/loop.py +0 -0
  106. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/nmux.py +0 -0
  107. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/printf.py +0 -0
  108. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/property_node.py +0 -0
  109. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/nodes/sequence.py +0 -0
  110. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/codegen/unresolved.py +0 -0
  111. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/_index.json +0 -0
  112. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/daqmx.json +0 -0
  113. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/nidcpower.json +0 -0
  114. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/nidigital.json +0 -0
  115. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/nidmm.json +0 -0
  116. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/nifgen.json +0 -0
  117. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/niscope.json +0 -0
  118. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/niswitch.json +0 -0
  119. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/serial.json +0 -0
  120. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/drivers/visa.json +0 -0
  121. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/labview_error_codes.json +0 -0
  122. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/openg/_index.json +0 -0
  123. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/openg/array.json +0 -0
  124. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/openg/file.json +0 -0
  125. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/openg/string.json +0 -0
  126. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/openg/time.json +0 -0
  127. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/openg/variant.json +0 -0
  128. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/primitives-from-pdf.json +0 -0
  129. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/_index.json +0 -0
  130. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/_pending_terminals.json +0 -0
  131. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/_types.json +0 -0
  132. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/application-control.json +0 -0
  133. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/array.json +0 -0
  134. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/boolean.json +0 -0
  135. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/cluster.json +0 -0
  136. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/comparison.json +0 -0
  137. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/error-handling.json +0 -0
  138. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/file-io.json +0 -0
  139. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/numeric.json +0 -0
  140. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/other.json +0 -0
  141. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/string.json +0 -0
  142. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/structures.json +0 -0
  143. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/data/vilib/variant.json +0 -0
  144. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/docs/__init__.py +0 -0
  145. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/docs/html_generator.py +0 -0
  146. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/docs/template.css +0 -0
  147. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/docs/utils.py +0 -0
  148. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/__init__.py +0 -0
  149. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/analysis.py +0 -0
  150. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/graph/flowchart.py +0 -0
  151. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/labview_error.py +0 -0
  152. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/__init__.py +0 -0
  153. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/defaults.py +0 -0
  154. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/flags.py +0 -0
  155. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/metadata.py +0 -0
  156. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/naming.py +0 -0
  157. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/nodes/base.py +0 -0
  158. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/nodes/case.py +0 -0
  159. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/nodes/constant.py +0 -0
  160. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/nodes/loop.py +0 -0
  161. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/type_mapping.py +0 -0
  162. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/parser/type_resolution.py +0 -0
  163. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/py.typed +0 -0
  164. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/skill_templates/__init__.py +0 -0
  165. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/skill_templates/lvkit-resolve-primitive/SKILL.md +0 -0
  166. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/skill_templates/lvkit-resolve-vilib/SKILL.md +0 -0
  167. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/structure.py +0 -0
  168. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/terminal_collector.py +0 -0
  169. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/type_defaults.py +0 -0
  170. {lvkit-0.2.0 → lvkit-0.3.0}/src/lvkit/vilib_resolver.py +0 -0
  171. {lvkit-0.2.0 → lvkit-0.3.0}/tests/__init__.py +0 -0
  172. {lvkit-0.2.0 → lvkit-0.3.0}/tests/conftest.py +0 -0
  173. {lvkit-0.2.0 → lvkit-0.3.0}/tests/helpers.py +0 -0
  174. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_ast_builder.py +0 -0
  175. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_ast_optimizer.py +0 -0
  176. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_case_parser.py +0 -0
  177. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_codegen_context.py +0 -0
  178. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_condition_builder.py +0 -0
  179. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_constant_decoding.py +0 -0
  180. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_diff.py +0 -0
  181. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_driver_codegen.py +0 -0
  182. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_dynamic_dispatch.py +0 -0
  183. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_error_codegen.py +0 -0
  184. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_error_handling.py +0 -0
  185. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_graceful_degradation.py +0 -0
  186. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_loop_codegen.py +0 -0
  187. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_new_codegen.py +0 -0
  188. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_parallel_codegen.py +0 -0
  189. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_parser.py +0 -0
  190. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_parser_regression.py +0 -0
  191. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_sequence.py +0 -0
  192. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_soft_codegen.py +0 -0
  193. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_type_driven_fixes.py +0 -0
  194. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_type_fields.py +0 -0
  195. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_type_loading.py +0 -0
  196. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_type_mapping.py +0 -0
  197. {lvkit-0.2.0 → lvkit-0.3.0}/tests/test_vi_graph.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvkit
3
- Version: 0.2.0
3
+ Version: 0.3.0
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.2.0"
7
+ version = "0.3.0"
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,311 @@
1
+ #!/usr/bin/env python3
2
+ """Audit primitives and vilib VIs referenced in samples but not in our data.
3
+
4
+ Walks samples/, parses each VI via XML pairs, cross-references primitives and
5
+ vilib dependencies against the JSON data files, and emits a markdown report
6
+ to .tmp/audit-unresolved.md sorted by usage count (highest first).
7
+
8
+ Usage: uv run python scripts/audit_unresolved.py
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections import defaultdict
14
+ from datetime import date
15
+ from pathlib import Path
16
+
17
+ from lvkit.parser import parse_vi
18
+ from lvkit.parser.node_types import PrimitiveNode
19
+ from lvkit.primitive_resolver import PrimitiveResolver
20
+ from lvkit.vilib_resolver import VIEntry, VILibResolver
21
+
22
+ REPO_ROOT = Path(__file__).parent.parent
23
+ SAMPLES = REPO_ROOT / "samples"
24
+ REPORT_PATH = REPO_ROOT / ".tmp" / "audit-unresolved.md"
25
+
26
+ # Status buckets
27
+ CAPTURED = "captured"
28
+ PARTIAL = "partial"
29
+ MISSING = "missing"
30
+
31
+
32
+ def find_bd_xml_pairs(samples_root: Path) -> list[tuple[Path, Path | None]]:
33
+ """Find all (bd_xml, main_xml) pairs under samples/.
34
+
35
+ Returns a list of (bd_xml, main_xml) tuples. main_xml is optional.
36
+ Dedups by bd_xml absolute path.
37
+ """
38
+ seen: set[Path] = set()
39
+ pairs: list[tuple[Path, Path | None]] = []
40
+ for bd_xml in samples_root.rglob("*_BDHb.xml"):
41
+ resolved = bd_xml.resolve()
42
+ if resolved in seen:
43
+ continue
44
+ seen.add(resolved)
45
+ # Derive main_xml path: same stem minus _BDHb
46
+ main_xml_name = bd_xml.name.replace("_BDHb.xml", ".xml")
47
+ main_xml = bd_xml.parent / main_xml_name
48
+ if not main_xml.exists():
49
+ main_xml = None
50
+ pairs.append((bd_xml, main_xml))
51
+ return pairs
52
+
53
+
54
+ def classify_primitive(entry: dict | None) -> str:
55
+ """Classify a primitive entry as captured/partial/missing."""
56
+ if entry is None:
57
+ return MISSING
58
+ if entry.get("placeholder"):
59
+ return PARTIAL
60
+ python_code = entry.get("python_code")
61
+ if not python_code:
62
+ return PARTIAL
63
+ terminals = entry.get("terminals") or []
64
+ if not terminals:
65
+ return PARTIAL
66
+ for t in terminals:
67
+ if t.get("index") is None:
68
+ return PARTIAL
69
+ return CAPTURED
70
+
71
+
72
+ def classify_vilib(entry: VIEntry | None) -> str:
73
+ """Classify a vilib VI entry as captured/partial/missing."""
74
+ if entry is None:
75
+ return MISSING
76
+ if entry.status == "needs_terminals":
77
+ return PARTIAL
78
+ for t in entry.terminals:
79
+ if t.index is None:
80
+ return PARTIAL
81
+ return CAPTURED
82
+
83
+
84
+ def main() -> None:
85
+ prim_resolver = PrimitiveResolver()
86
+ vilib_resolver = VILibResolver()
87
+
88
+ # prim_id -> {"count": int, "name": str, "vis": set[str]}
89
+ prim_usage: dict[str, dict] = defaultdict(
90
+ lambda: {"count": 0, "name": None, "vis": set()}
91
+ )
92
+ # vilib_path -> {"count": int, "name": str, "vis": set[str]}
93
+ vilib_usage: dict[str, dict] = defaultdict(
94
+ lambda: {"count": 0, "name": None, "vis": set()}
95
+ )
96
+
97
+ pairs = find_bd_xml_pairs(SAMPLES)
98
+ print(f"Scanning {len(pairs)} VIs from {SAMPLES}...")
99
+
100
+ parse_failures: list[tuple[Path, str]] = []
101
+ for bd_xml, main_xml in pairs:
102
+ try:
103
+ parsed = parse_vi(bd_xml=bd_xml, main_xml=main_xml)
104
+ except Exception as e:
105
+ parse_failures.append((bd_xml, f"{type(e).__name__}: {e}"))
106
+ continue
107
+
108
+ vi_label = bd_xml.name.replace("_BDHb.xml", "")
109
+
110
+ # Primitives pass
111
+ for node in parsed.block_diagram.nodes:
112
+ if not isinstance(node, PrimitiveNode):
113
+ continue
114
+ if node.prim_res_id is None:
115
+ continue
116
+ key = str(node.prim_res_id)
117
+ prim_usage[key]["count"] += 1
118
+ prim_usage[key]["vis"].add(vi_label)
119
+ if node.name and prim_usage[key]["name"] is None:
120
+ prim_usage[key]["name"] = node.name
121
+
122
+ # VIlib pass (requires main_xml)
123
+ for ref in parsed.metadata.dependency_refs:
124
+ if not ref.path_tokens or ref.path_tokens[0] != "<vilib>":
125
+ continue
126
+ vilib_path = "/".join(ref.path_tokens[1:])
127
+ vilib_usage[vilib_path]["count"] += 1
128
+ vilib_usage[vilib_path]["vis"].add(vi_label)
129
+ if vilib_usage[vilib_path]["name"] is None:
130
+ vilib_usage[vilib_path]["name"] = ref.name
131
+
132
+ # Classify primitives
133
+ prim_buckets: dict[str, list] = {CAPTURED: [], PARTIAL: [], MISSING: []}
134
+ for prim_id, info in prim_usage.items():
135
+ entry = prim_resolver.get_by_id(prim_id)
136
+ status = classify_primitive(entry)
137
+ name = (entry or {}).get("name") or info["name"] or "Unknown"
138
+ prim_buckets[status].append({
139
+ "id": prim_id,
140
+ "name": name,
141
+ "count": info["count"],
142
+ "vis": sorted(info["vis"]),
143
+ })
144
+
145
+ # Classify vilib
146
+ vilib_buckets: dict[str, list] = {CAPTURED: [], PARTIAL: [], MISSING: []}
147
+ for path, info in vilib_usage.items():
148
+ entry = vilib_resolver.resolve(path)
149
+ if entry is None and info["name"]:
150
+ entry = vilib_resolver.resolve_by_name(info["name"])
151
+ status = classify_vilib(entry)
152
+ name = (entry.name if entry else info["name"]) or "Unknown"
153
+ vilib_buckets[status].append({
154
+ "path": path,
155
+ "name": name,
156
+ "count": info["count"],
157
+ "vis": sorted(info["vis"]),
158
+ "status": entry.status if entry else None,
159
+ })
160
+
161
+ # Sort by count desc
162
+ for bucket in prim_buckets.values():
163
+ bucket.sort(key=lambda x: -x["count"])
164
+ for bucket in vilib_buckets.values():
165
+ bucket.sort(key=lambda x: -x["count"])
166
+
167
+ # JSON-level audit: count all entries in vilib JSON (not just sample refs).
168
+ # Each entry appears under multiple keys in _by_name — dedup by object id.
169
+ seen_ids: set[int] = set()
170
+ json_vilib_total = 0
171
+ json_vilib_captured = 0
172
+ json_vilib_needs_terminals = 0
173
+ json_vilib_missing_idx = 0
174
+ for entry in vilib_resolver._by_name.values():
175
+ if id(entry) in seen_ids:
176
+ continue
177
+ seen_ids.add(id(entry))
178
+ json_vilib_total += 1
179
+ if entry.status == "needs_terminals":
180
+ json_vilib_needs_terminals += 1
181
+ elif any(t.index is None for t in entry.terminals):
182
+ json_vilib_missing_idx += 1
183
+ else:
184
+ json_vilib_captured += 1
185
+
186
+ json_prim_total = len(prim_resolver.get_all_ids())
187
+ json_prim_captured = sum(
188
+ 1 for pid in prim_resolver.get_all_ids()
189
+ if classify_primitive(prim_resolver.get_by_id(pid)) == CAPTURED
190
+ )
191
+ json_prim_partial = json_prim_total - json_prim_captured
192
+
193
+ # Write report
194
+ REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
195
+ lines: list[str] = []
196
+ lines.append(f"# Unresolved Audit — {date.today().isoformat()}\n")
197
+ lines.append(f"Scanned {len(pairs)} VI block diagrams from `samples/`.\n")
198
+
199
+ lines.append("## Summary (sample usage)\n")
200
+ lines.append(
201
+ f"- **Primitives**: "
202
+ f"{len(prim_buckets[CAPTURED])} captured / "
203
+ f"{len(prim_buckets[PARTIAL])} partial / "
204
+ f"{len(prim_buckets[MISSING])} missing "
205
+ f"({len(prim_usage)} unique primResIDs seen)"
206
+ )
207
+ lines.append(
208
+ f"- **VIlib**: "
209
+ f"{len(vilib_buckets[CAPTURED])} captured / "
210
+ f"{len(vilib_buckets[PARTIAL])} partial / "
211
+ f"{len(vilib_buckets[MISSING])} missing "
212
+ f"({len(vilib_usage)} unique paths seen)"
213
+ )
214
+ lines.append("")
215
+ lines.append("## JSON data state (all entries, regardless of samples)\n")
216
+ lines.append(
217
+ f"- **Primitives JSON**: {json_prim_total} total — "
218
+ f"{json_prim_captured} captured / {json_prim_partial} partial"
219
+ )
220
+ lines.append(
221
+ f"- **VIlib JSON**: {json_vilib_total} total — "
222
+ f"{json_vilib_captured} captured / "
223
+ f"{json_vilib_needs_terminals} needs_terminals / "
224
+ f"{json_vilib_missing_idx} missing indices"
225
+ )
226
+ if parse_failures:
227
+ lines.append(f"- **Parse failures**: {len(parse_failures)}")
228
+ lines.append("")
229
+
230
+ def _sample_vis(vis: list[str], limit: int = 3) -> str:
231
+ if len(vis) <= limit:
232
+ return ", ".join(vis)
233
+ return ", ".join(vis[:limit]) + f" (+{len(vis) - limit} more)"
234
+
235
+ # Missing primitives
236
+ lines.append("## Missing Primitives\n")
237
+ lines.append("| primResID | Name | Count | Sample VIs |")
238
+ lines.append("|-----------|------|-------|------------|")
239
+ for p in prim_buckets[MISSING]:
240
+ lines.append(
241
+ f"| {p['id']} | {p['name']} | {p['count']} | {_sample_vis(p['vis'])} |"
242
+ )
243
+ if not prim_buckets[MISSING]:
244
+ lines.append("| _(none)_ | | | |")
245
+ lines.append("")
246
+
247
+ # Partial primitives
248
+ lines.append("## Partial Primitives (needs terminals or python_code)\n")
249
+ lines.append("| primResID | Name | Count | Sample VIs |")
250
+ lines.append("|-----------|------|-------|------------|")
251
+ for p in prim_buckets[PARTIAL]:
252
+ lines.append(
253
+ f"| {p['id']} | {p['name']} | {p['count']} | {_sample_vis(p['vis'])} |"
254
+ )
255
+ if not prim_buckets[PARTIAL]:
256
+ lines.append("| _(none)_ | | | |")
257
+ lines.append("")
258
+
259
+ # Missing vilib
260
+ lines.append("## Missing VIlib VIs\n")
261
+ lines.append("| Path | Count | Sample VIs |")
262
+ lines.append("|------|-------|------------|")
263
+ for v in vilib_buckets[MISSING]:
264
+ lines.append(
265
+ f"| `{v['path']}` | {v['count']} | {_sample_vis(v['vis'])} |"
266
+ )
267
+ if not vilib_buckets[MISSING]:
268
+ lines.append("| _(none)_ | | |")
269
+ lines.append("")
270
+
271
+ # Partial vilib
272
+ lines.append("## Partial VIlib VIs (needs terminal indices)\n")
273
+ lines.append("| Path | Status | Count | Sample VIs |")
274
+ lines.append("|------|--------|-------|------------|")
275
+ for v in vilib_buckets[PARTIAL]:
276
+ lines.append(
277
+ f"| `{v['path']}` | {v['status']} | {v['count']} | "
278
+ f"{_sample_vis(v['vis'])} |"
279
+ )
280
+ if not vilib_buckets[PARTIAL]:
281
+ lines.append("| _(none)_ | | | |")
282
+ lines.append("")
283
+
284
+ # Parse failures (if any)
285
+ if parse_failures:
286
+ lines.append("## Parse Failures\n")
287
+ for path, err in parse_failures[:30]:
288
+ rel = path.relative_to(REPO_ROOT)
289
+ lines.append(f"- `{rel}` — {err}")
290
+ if len(parse_failures) > 30:
291
+ lines.append(f"- _(+{len(parse_failures) - 30} more)_")
292
+ lines.append("")
293
+
294
+ REPORT_PATH.write_text("\n".join(lines))
295
+ print(f"Report written to {REPORT_PATH.relative_to(REPO_ROOT)}")
296
+ print(
297
+ f" Primitives: {len(prim_buckets[CAPTURED])} captured / "
298
+ f"{len(prim_buckets[PARTIAL])} partial / "
299
+ f"{len(prim_buckets[MISSING])} missing"
300
+ )
301
+ print(
302
+ f" VIlib: {len(vilib_buckets[CAPTURED])} captured / "
303
+ f"{len(vilib_buckets[PARTIAL])} partial / "
304
+ f"{len(vilib_buckets[MISSING])} missing"
305
+ )
306
+ if parse_failures:
307
+ print(f" Parse failures: {len(parse_failures)}")
308
+
309
+
310
+ if __name__ == "__main__":
311
+ main()
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env python3
2
+ """Dump PrimitiveResolutionNeeded-shaped context for missing primitives.
3
+
4
+ For each primResID seen in samples but not captured in primitives.json,
5
+ emit a markdown section matching the PrimitiveResolutionNeeded message
6
+ format (terminal indices, directions, types, usage count, sample VIs).
7
+
8
+ The output feeds the `lvkit-resolve-primitive` skill — one section per
9
+ primitive, sorted by usage count descending.
10
+
11
+ Usage:
12
+ uv run python scripts/dump_primitive_context.py [--limit 25]
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ from collections import defaultdict
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ from lvkit.parser import parse_vi
23
+ from lvkit.parser.node_types import PrimitiveNode
24
+ from lvkit.primitive_resolver import PrimitiveResolver
25
+
26
+ REPO_ROOT = Path(__file__).parent.parent
27
+ SAMPLES = REPO_ROOT / "samples"
28
+ REPORT_PATH = REPO_ROOT / ".tmp" / "missing-primitives-context.md"
29
+
30
+ # primResID range → hint (copied from SKILL.md Step 4)
31
+ RANGE_HINTS: list[tuple[int, int, str]] = [
32
+ (1044, 1064, "Array operations"),
33
+ (1061, 1081, "Numeric/arithmetic"),
34
+ (1083, 1128, "Path/comparison/boolean"),
35
+ (1140, 1170, "Type conversion, variant, data manipulation"),
36
+ (1300, 1340, "Timing, constants, clusters"),
37
+ (1419, 1435, "Path operations"),
38
+ (1500, 1540, "String operations"),
39
+ (1600, 1610, "Flatten/unflatten"),
40
+ (1809, 1911, "Array index/sort/delete"),
41
+ (1999, 1999, "Path constant"),
42
+ (2073, 2076, "Error handling"),
43
+ (2401, 2401, "Merge Errors"),
44
+ (8003, 8083, "File I/O"),
45
+ (8100, 8101, "VI info"),
46
+ (8201, 8205, "Variant operations"),
47
+ (9000, 9114, "VI Server, references, scripting"),
48
+ ]
49
+
50
+
51
+ def range_hint(prim_id: int) -> str:
52
+ for lo, hi, hint in RANGE_HINTS:
53
+ if lo <= prim_id <= hi:
54
+ return f"{lo}-{hi}: {hint}"
55
+ return "(no range hint)"
56
+
57
+
58
+ @dataclass
59
+ class TerminalObservation:
60
+ index: int | None
61
+ direction: str
62
+ type_name: str
63
+ name: str | None
64
+
65
+
66
+ @dataclass
67
+ class Invocation:
68
+ vi_label: str
69
+ bd_xml: Path
70
+ terminals: list[TerminalObservation]
71
+
72
+
73
+ def find_bd_xml_pairs(samples_root: Path) -> list[tuple[Path, Path | None]]:
74
+ seen: set[Path] = set()
75
+ pairs: list[tuple[Path, Path | None]] = []
76
+ for bd_xml in samples_root.rglob("*_BDHb.xml"):
77
+ resolved = bd_xml.resolve()
78
+ if resolved in seen:
79
+ continue
80
+ seen.add(resolved)
81
+ main_xml = bd_xml.parent / bd_xml.name.replace("_BDHb.xml", ".xml")
82
+ if not main_xml.exists():
83
+ main_xml = None
84
+ pairs.append((bd_xml, main_xml))
85
+ return pairs
86
+
87
+
88
+ def collect_invocations(
89
+ pairs: list[tuple[Path, Path | None]],
90
+ missing_ids: set[int],
91
+ max_per_id: int,
92
+ ) -> dict[int, tuple[int, list[Invocation]]]:
93
+ """Scan all pairs, collect up to max_per_id invocations per primResID.
94
+
95
+ Returns: prim_id -> (total_count, invocations)
96
+ """
97
+ counts: dict[int, int] = defaultdict(int)
98
+ invocations: dict[int, list[Invocation]] = defaultdict(list)
99
+
100
+ for bd_xml, main_xml in pairs:
101
+ try:
102
+ parsed = parse_vi(bd_xml=bd_xml, main_xml=main_xml)
103
+ except Exception:
104
+ continue
105
+
106
+ vi_label = bd_xml.name.replace("_BDHb.xml", "")
107
+ for node in parsed.block_diagram.nodes:
108
+ if not isinstance(node, PrimitiveNode):
109
+ continue
110
+ if node.prim_res_id not in missing_ids:
111
+ continue
112
+ counts[node.prim_res_id] += 1
113
+
114
+ # Only record detailed invocations up to max_per_id
115
+ if len(invocations[node.prim_res_id]) >= max_per_id:
116
+ continue
117
+
118
+ terms: list[TerminalObservation] = []
119
+ for _uid, t_info in parsed.block_diagram.terminal_info.items():
120
+ if t_info.parent_uid != node.uid:
121
+ continue
122
+ type_name = (
123
+ t_info.parsed_type.type_name
124
+ if t_info.parsed_type else "?"
125
+ )
126
+ terms.append(TerminalObservation(
127
+ index=t_info.index,
128
+ direction="output" if t_info.is_output else "input",
129
+ type_name=type_name,
130
+ name=t_info.name,
131
+ ))
132
+ terms.sort(key=lambda t: (t.index is None, t.index or 0))
133
+ invocations[node.prim_res_id].append(Invocation(
134
+ vi_label=vi_label,
135
+ bd_xml=bd_xml,
136
+ terminals=terms,
137
+ ))
138
+
139
+ return {pid: (counts[pid], invs) for pid, invs in invocations.items()}
140
+
141
+
142
+ def main() -> None:
143
+ ap = argparse.ArgumentParser()
144
+ ap.add_argument(
145
+ "--limit", type=int, default=25,
146
+ help="Number of missing primitives to include (top N by usage)",
147
+ )
148
+ ap.add_argument(
149
+ "--invocations", type=int, default=3,
150
+ help="Max sample invocations per primResID",
151
+ )
152
+ args = ap.parse_args()
153
+
154
+ resolver = PrimitiveResolver()
155
+
156
+ # First pass: count all primResIDs in samples to build the missing set
157
+ # sorted by count. Reuse audit logic: classify missing vs captured via
158
+ # resolver.get_by_id + python_code/terminals check.
159
+ pairs = find_bd_xml_pairs(SAMPLES)
160
+ print(f"Scanning {len(pairs)} VIs...")
161
+
162
+ all_counts: dict[int, int] = defaultdict(int)
163
+ for bd_xml, main_xml in pairs:
164
+ try:
165
+ parsed = parse_vi(bd_xml=bd_xml, main_xml=main_xml)
166
+ except Exception:
167
+ continue
168
+ for node in parsed.block_diagram.nodes:
169
+ if isinstance(node, PrimitiveNode) and node.prim_res_id is not None:
170
+ all_counts[node.prim_res_id] += 1
171
+
172
+ # Filter to missing primitives
173
+ missing: list[tuple[int, int]] = []
174
+ for pid, count in all_counts.items():
175
+ entry = resolver.get_by_id(str(pid))
176
+ if entry is None:
177
+ missing.append((pid, count))
178
+ continue
179
+ # Partial: no python_code or any terminal missing index
180
+ if not entry.get("python_code"):
181
+ missing.append((pid, count))
182
+ continue
183
+ terms = entry.get("terminals") or []
184
+ if not terms or any(t.get("index") is None for t in terms):
185
+ missing.append((pid, count))
186
+
187
+ missing.sort(key=lambda x: -x[1])
188
+ top = missing[:args.limit]
189
+ print(f"Found {len(missing)} missing/partial primitives, taking top {len(top)}")
190
+
191
+ # Second pass: collect invocation details only for the top N
192
+ top_ids = {pid for pid, _ in top}
193
+ details = collect_invocations(pairs, top_ids, args.invocations)
194
+
195
+ # Write the report
196
+ REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
197
+ lines: list[str] = []
198
+ lines.append("# Missing primitives — resolution context\n")
199
+ lines.append(
200
+ f"Top {len(top)} of {len(missing)} missing/partial primitives "
201
+ f"by usage count across {len(pairs)} sample VIs.\n"
202
+ )
203
+ lines.append(
204
+ "Each section matches the `PrimitiveResolutionNeeded` diagnostic "
205
+ "format. Feed one section at a time to the "
206
+ "`lvkit-resolve-primitive` skill.\n"
207
+ )
208
+
209
+ for pid, count in top:
210
+ invs = details.get(pid, (count, []))[1]
211
+ lines.append(f"## primResID {pid} — {count} uses\n")
212
+ lines.append(f"**Range hint**: {range_hint(pid)}\n")
213
+
214
+ existing = resolver.get_by_id(str(pid))
215
+ if existing is not None:
216
+ status = "partial"
217
+ reason = []
218
+ if not existing.get("python_code"):
219
+ reason.append("no python_code")
220
+ terms = existing.get("terminals") or []
221
+ if not terms:
222
+ reason.append("no terminals")
223
+ elif any(t.get("index") is None for t in terms):
224
+ reason.append("missing terminal indices")
225
+ lines.append(
226
+ f"**Current entry**: {existing.get('name', '?')} "
227
+ f"({status}: {', '.join(reason)})\n"
228
+ )
229
+ else:
230
+ lines.append("**Current entry**: (none)\n")
231
+
232
+ for i, inv in enumerate(invs, 1):
233
+ lines.append(f"### Sample {i}: {inv.vi_label}")
234
+ rel = inv.bd_xml.relative_to(REPO_ROOT)
235
+ lines.append(f"File: `{rel}`")
236
+ lines.append("Wired terminals from graph:")
237
+ for t in inv.terminals:
238
+ parts = [
239
+ f"index={t.index}",
240
+ f"direction={t.direction}",
241
+ f"type={t.type_name}",
242
+ ]
243
+ if t.name:
244
+ parts.append(f"name={t.name}")
245
+ lines.append(f"- {' '.join(parts)}")
246
+ lines.append("")
247
+
248
+ REPORT_PATH.write_text("\n".join(lines))
249
+ print(f"Report written to {REPORT_PATH.relative_to(REPO_ROOT)}")
250
+
251
+
252
+ if __name__ == "__main__":
253
+ main()