lvkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lvkit/__init__.py +55 -0
- lvkit/_data.py +24 -0
- lvkit/cli.py +980 -0
- lvkit/codegen/README.md +147 -0
- lvkit/codegen/__init__.py +36 -0
- lvkit/codegen/ast_optimizer.py +436 -0
- lvkit/codegen/ast_utils.py +241 -0
- lvkit/codegen/builder.py +754 -0
- lvkit/codegen/class_builder.py +775 -0
- lvkit/codegen/condition_builder.py +316 -0
- lvkit/codegen/context.py +576 -0
- lvkit/codegen/dataflow.py +241 -0
- lvkit/codegen/error_handler.py +297 -0
- lvkit/codegen/expressions.py +202 -0
- lvkit/codegen/fragment.py +54 -0
- lvkit/codegen/function.py +307 -0
- lvkit/codegen/imports.py +132 -0
- lvkit/codegen/nodes/__init__.py +102 -0
- lvkit/codegen/nodes/base.py +50 -0
- lvkit/codegen/nodes/case.py +345 -0
- lvkit/codegen/nodes/compound.py +159 -0
- lvkit/codegen/nodes/constant.py +33 -0
- lvkit/codegen/nodes/invoke_node.py +83 -0
- lvkit/codegen/nodes/loop.py +744 -0
- lvkit/codegen/nodes/nmux.py +224 -0
- lvkit/codegen/nodes/primitive.py +673 -0
- lvkit/codegen/nodes/printf.py +70 -0
- lvkit/codegen/nodes/property_node.py +106 -0
- lvkit/codegen/nodes/sequence.py +65 -0
- lvkit/codegen/nodes/subvi.py +789 -0
- lvkit/codegen/stubs.py +184 -0
- lvkit/codegen/unresolved.py +156 -0
- lvkit/data/drivers/_index.json +15 -0
- lvkit/data/drivers/daqmx.json +2708 -0
- lvkit/data/drivers/nidcpower.json +545 -0
- lvkit/data/drivers/nidigital.json +830 -0
- lvkit/data/drivers/nidmm.json +620 -0
- lvkit/data/drivers/nifgen.json +490 -0
- lvkit/data/drivers/niscope.json +560 -0
- lvkit/data/drivers/niswitch.json +475 -0
- lvkit/data/drivers/serial.json +170 -0
- lvkit/data/drivers/visa.json +542 -0
- lvkit/data/labview_error_codes.json +1318 -0
- lvkit/data/openg/_index.json +12 -0
- lvkit/data/openg/array.json +6951 -0
- lvkit/data/openg/file.json +876 -0
- lvkit/data/openg/string.json +146 -0
- lvkit/data/openg/time.json +98 -0
- lvkit/data/openg/variant.json +33 -0
- lvkit/data/primitives-from-pdf.json +8452 -0
- lvkit/data/primitives.json +3166 -0
- lvkit/data/vilib/_index.json +19 -0
- lvkit/data/vilib/_pending_terminals.json +1391 -0
- lvkit/data/vilib/_types.json +24 -0
- lvkit/data/vilib/application-control.json +5408 -0
- lvkit/data/vilib/array.json +2071 -0
- lvkit/data/vilib/boolean.json +704 -0
- lvkit/data/vilib/cluster.json +776 -0
- lvkit/data/vilib/comparison.json +2388 -0
- lvkit/data/vilib/error-handling.json +2274 -0
- lvkit/data/vilib/file-io.json +4733 -0
- lvkit/data/vilib/numeric.json +1538 -0
- lvkit/data/vilib/other.json +87782 -0
- lvkit/data/vilib/string.json +3582 -0
- lvkit/data/vilib/structures.json +1040 -0
- lvkit/data/vilib/variant.json +1343 -0
- lvkit/docs/__init__.py +1 -0
- lvkit/docs/generate.py +400 -0
- lvkit/docs/html_generator.py +853 -0
- lvkit/docs/template.css +398 -0
- lvkit/docs/utils.py +109 -0
- lvkit/extractor.py +98 -0
- lvkit/graph/__init__.py +10 -0
- lvkit/graph/analysis.py +158 -0
- lvkit/graph/construction.py +1221 -0
- lvkit/graph/core.py +321 -0
- lvkit/graph/describe.py +493 -0
- lvkit/graph/diff.py +387 -0
- lvkit/graph/flowchart.py +281 -0
- lvkit/graph/loading.py +716 -0
- lvkit/graph/models.py +420 -0
- lvkit/graph/operations.py +446 -0
- lvkit/graph/queries.py +813 -0
- lvkit/labview_error.py +146 -0
- lvkit/labview_error_codes.py +48 -0
- lvkit/mcp/__init__.py +7 -0
- lvkit/mcp/schemas.py +239 -0
- lvkit/mcp/server.py +647 -0
- lvkit/mcp/tools.py +204 -0
- lvkit/models.py +388 -0
- lvkit/parser/__init__.py +96 -0
- lvkit/parser/constants.py +158 -0
- lvkit/parser/defaults.py +117 -0
- lvkit/parser/flags.py +26 -0
- lvkit/parser/front_panel.py +257 -0
- lvkit/parser/metadata.py +290 -0
- lvkit/parser/models.py +406 -0
- lvkit/parser/naming.py +77 -0
- lvkit/parser/node_types.py +600 -0
- lvkit/parser/nodes/__init__.py +16 -0
- lvkit/parser/nodes/base.py +80 -0
- lvkit/parser/nodes/case.py +389 -0
- lvkit/parser/nodes/constant.py +55 -0
- lvkit/parser/nodes/loop.py +134 -0
- lvkit/parser/nodes/sequence.py +150 -0
- lvkit/parser/type_mapping.py +387 -0
- lvkit/parser/type_resolution.py +254 -0
- lvkit/parser/utils.py +124 -0
- lvkit/parser/vi.py +1033 -0
- lvkit/pipeline.py +683 -0
- lvkit/primitive_resolver.py +633 -0
- lvkit/project_store.py +480 -0
- lvkit/py.typed +0 -0
- lvkit/skill_templates/__init__.py +15 -0
- lvkit/skill_templates/lvkit-convert/SKILL.md +141 -0
- lvkit/skill_templates/lvkit-describe/SKILL.md +108 -0
- lvkit/skill_templates/lvkit-idiomatic/SKILL.md +85 -0
- lvkit/skill_templates/lvkit-resolve-primitive/SKILL.md +275 -0
- lvkit/skill_templates/lvkit-resolve-vilib/SKILL.md +146 -0
- lvkit/structure.py +788 -0
- lvkit/terminal_collector.py +295 -0
- lvkit/type_defaults.py +195 -0
- lvkit/vilib_resolver.py +1160 -0
- lvkit-0.1.0.dist-info/METADATA +199 -0
- lvkit-0.1.0.dist-info/RECORD +128 -0
- lvkit-0.1.0.dist-info/WHEEL +4 -0
- lvkit-0.1.0.dist-info/entry_points.txt +3 -0
- lvkit-0.1.0.dist-info/licenses/LICENSE +201 -0
lvkit/cli.py
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
"""Command-line interface for lvkit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import __version__, primitive_resolver, vilib_resolver
|
|
12
|
+
from .graph import InMemoryVIGraph
|
|
13
|
+
from .project_store import (
|
|
14
|
+
find_project_store,
|
|
15
|
+
init_project_store,
|
|
16
|
+
install_claude_skills,
|
|
17
|
+
install_copilot_skills,
|
|
18
|
+
)
|
|
19
|
+
from .structure import (
|
|
20
|
+
discover_project_structure,
|
|
21
|
+
generate_python_structure_plan,
|
|
22
|
+
parse_lvclass,
|
|
23
|
+
parse_lvlib,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _add_project_root_arg(parser: argparse.ArgumentParser) -> None:
|
|
28
|
+
"""Add --project-root flag to a subparser."""
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--project-root",
|
|
31
|
+
default=None,
|
|
32
|
+
metavar="DIR",
|
|
33
|
+
help=(
|
|
34
|
+
"Project root containing a .lvkit/ resolution store. "
|
|
35
|
+
"Defaults to walking up from CWD looking for .lvkit/."
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _configure_resolvers(args: argparse.Namespace) -> Path | None:
|
|
41
|
+
"""Discover the project store and reset resolver singletons.
|
|
42
|
+
|
|
43
|
+
Must be called BEFORE any load_vi() so graph construction sees the
|
|
44
|
+
project mappings (used for terminal-index disambiguation).
|
|
45
|
+
|
|
46
|
+
Accepts --project-root in either form: the parent of .lvkit/ (the
|
|
47
|
+
project root), or the .lvkit/ directory itself.
|
|
48
|
+
|
|
49
|
+
Returns the project store directory if one was found, else None.
|
|
50
|
+
"""
|
|
51
|
+
project_root = getattr(args, "project_root", None)
|
|
52
|
+
store: Path | None
|
|
53
|
+
if project_root:
|
|
54
|
+
candidate = Path(project_root)
|
|
55
|
+
# Accept both "project root" and ".lvkit/" itself
|
|
56
|
+
if candidate.name == ".lvkit" and candidate.is_dir():
|
|
57
|
+
store = candidate
|
|
58
|
+
elif (candidate / ".lvkit").is_dir():
|
|
59
|
+
store = candidate / ".lvkit"
|
|
60
|
+
else:
|
|
61
|
+
store = None
|
|
62
|
+
else:
|
|
63
|
+
store = find_project_store()
|
|
64
|
+
|
|
65
|
+
primitive_resolver.reset_resolver(project_data_dir=store)
|
|
66
|
+
vilib_resolver.reset_resolver(project_data_dir=store)
|
|
67
|
+
return store
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main() -> int:
|
|
71
|
+
"""Main CLI entry point."""
|
|
72
|
+
parser = argparse.ArgumentParser(
|
|
73
|
+
prog="lvkit",
|
|
74
|
+
description="Understand, convert, and document LabVIEW VI files.",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument("--version", action="version", version=f"lvkit {__version__}")
|
|
77
|
+
|
|
78
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
79
|
+
|
|
80
|
+
# Structure command
|
|
81
|
+
struct_parser = subparsers.add_parser(
|
|
82
|
+
"structure", help="Analyze LabVIEW project structure"
|
|
83
|
+
)
|
|
84
|
+
struct_parser.add_argument("input", help="Directory, .lvlib, or .lvclass file")
|
|
85
|
+
struct_parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
86
|
+
struct_parser.add_argument(
|
|
87
|
+
"--plan", action="store_true", help="Generate Python structure plan"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# MCP server command
|
|
91
|
+
subparsers.add_parser(
|
|
92
|
+
"mcp",
|
|
93
|
+
help="Run MCP server for VI analysis",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Describe command - human-readable VI description
|
|
97
|
+
desc_parser = subparsers.add_parser(
|
|
98
|
+
"describe",
|
|
99
|
+
help="Describe a VI's purpose, signature, and structure",
|
|
100
|
+
)
|
|
101
|
+
desc_parser.add_argument(
|
|
102
|
+
"input_path",
|
|
103
|
+
help="Path to .vi file",
|
|
104
|
+
)
|
|
105
|
+
desc_parser.add_argument(
|
|
106
|
+
"--search-path",
|
|
107
|
+
action="append",
|
|
108
|
+
dest="search_paths",
|
|
109
|
+
default=[],
|
|
110
|
+
help="Search paths for SubVI resolution (can be repeated)",
|
|
111
|
+
)
|
|
112
|
+
desc_parser.add_argument(
|
|
113
|
+
"--chart", action="store_true",
|
|
114
|
+
help="Include Mermaid flowchart diagram",
|
|
115
|
+
)
|
|
116
|
+
_add_project_root_arg(desc_parser)
|
|
117
|
+
|
|
118
|
+
# Generate command - deterministic AST-based Python generation
|
|
119
|
+
gen_parser = subparsers.add_parser(
|
|
120
|
+
"generate",
|
|
121
|
+
help="Generate Python from VI files using deterministic AST pipeline",
|
|
122
|
+
)
|
|
123
|
+
gen_parser.add_argument(
|
|
124
|
+
"input_path",
|
|
125
|
+
help="Path to .vi, .lvlib, .lvclass, or directory",
|
|
126
|
+
)
|
|
127
|
+
gen_parser.add_argument(
|
|
128
|
+
"-o", "--output", default="outputs",
|
|
129
|
+
help="Output directory",
|
|
130
|
+
)
|
|
131
|
+
gen_parser.add_argument(
|
|
132
|
+
"--search-path",
|
|
133
|
+
action="append",
|
|
134
|
+
dest="search_paths",
|
|
135
|
+
default=[],
|
|
136
|
+
help="Search paths for SubVI resolution (can be repeated)",
|
|
137
|
+
)
|
|
138
|
+
gen_parser.add_argument(
|
|
139
|
+
"--no-expand", action="store_true",
|
|
140
|
+
help="Don't expand SubVIs",
|
|
141
|
+
)
|
|
142
|
+
# User-facing name is --placeholder-on-unresolved (descriptive of the
|
|
143
|
+
# output the user sees in their generated Python). Internally this
|
|
144
|
+
# flows to CodeGenContext.soft_unresolved (the codegen-time mode).
|
|
145
|
+
gen_parser.add_argument(
|
|
146
|
+
"--placeholder-on-unresolved",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help=(
|
|
149
|
+
"Don't fail on unknown primitives or vi.lib VIs. Instead emit "
|
|
150
|
+
"an inline `raise PrimitiveResolutionNeeded(...)` / `raise "
|
|
151
|
+
"VILibResolutionNeeded(...)` in the generated Python so the "
|
|
152
|
+
"build succeeds and unresolved calls are visible at runtime."
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
_add_project_root_arg(gen_parser)
|
|
156
|
+
|
|
157
|
+
# Docs command - generate HTML documentation
|
|
158
|
+
docs_parser = subparsers.add_parser(
|
|
159
|
+
"docs",
|
|
160
|
+
help="Generate HTML documentation for VI files",
|
|
161
|
+
)
|
|
162
|
+
docs_parser.add_argument(
|
|
163
|
+
"input_path",
|
|
164
|
+
help="Path to .vi, .lvlib, .lvclass, or directory",
|
|
165
|
+
)
|
|
166
|
+
docs_parser.add_argument(
|
|
167
|
+
"output_dir", help="Output directory for HTML files",
|
|
168
|
+
)
|
|
169
|
+
docs_parser.add_argument(
|
|
170
|
+
"--search-path",
|
|
171
|
+
action="append",
|
|
172
|
+
dest="search_paths",
|
|
173
|
+
default=[],
|
|
174
|
+
help="Search paths for SubVI resolution (can be repeated)",
|
|
175
|
+
)
|
|
176
|
+
docs_parser.add_argument(
|
|
177
|
+
"--no-expand", action="store_true",
|
|
178
|
+
help="Don't expand SubVIs",
|
|
179
|
+
)
|
|
180
|
+
_add_project_root_arg(docs_parser)
|
|
181
|
+
|
|
182
|
+
# Visualize command - interactive graph visualization
|
|
183
|
+
viz_parser = subparsers.add_parser(
|
|
184
|
+
"visualize",
|
|
185
|
+
help="Generate VI graphs as Mermaid flowcharts or interactive diagrams",
|
|
186
|
+
)
|
|
187
|
+
viz_parser.add_argument(
|
|
188
|
+
"input_path",
|
|
189
|
+
help="Path to .vi, .lvlib, .lvclass, or directory",
|
|
190
|
+
)
|
|
191
|
+
viz_parser.add_argument(
|
|
192
|
+
"-o", "--output",
|
|
193
|
+
default="outputs/graph.html",
|
|
194
|
+
help="Output HTML file (default: outputs/graph.html)",
|
|
195
|
+
)
|
|
196
|
+
viz_parser.add_argument(
|
|
197
|
+
"--search-path",
|
|
198
|
+
action="append",
|
|
199
|
+
dest="search_paths",
|
|
200
|
+
default=[],
|
|
201
|
+
help="Search paths for SubVI resolution (can be repeated)",
|
|
202
|
+
)
|
|
203
|
+
viz_parser.add_argument(
|
|
204
|
+
"--no-expand", action="store_true",
|
|
205
|
+
help="Don't expand SubVIs",
|
|
206
|
+
)
|
|
207
|
+
viz_parser.add_argument(
|
|
208
|
+
"--open", action="store_true",
|
|
209
|
+
help="Open in browser after generating",
|
|
210
|
+
)
|
|
211
|
+
viz_parser.add_argument(
|
|
212
|
+
"--mode",
|
|
213
|
+
default="dataflow",
|
|
214
|
+
choices=["dataflow", "deps"],
|
|
215
|
+
help="Graph type: dataflow (operations within VI) or deps (VI dependencies)",
|
|
216
|
+
)
|
|
217
|
+
viz_parser.add_argument(
|
|
218
|
+
"--format",
|
|
219
|
+
default=None,
|
|
220
|
+
choices=["interactive", "flowchart"],
|
|
221
|
+
help="Output format: flowchart (Mermaid, default for dataflow) "
|
|
222
|
+
"or interactive (pyvis, default for deps)",
|
|
223
|
+
)
|
|
224
|
+
_add_project_root_arg(viz_parser)
|
|
225
|
+
|
|
226
|
+
# Diff command - compare two VIs
|
|
227
|
+
diff_parser = subparsers.add_parser(
|
|
228
|
+
"diff",
|
|
229
|
+
help="Compare two versions of a VI",
|
|
230
|
+
)
|
|
231
|
+
diff_parser.add_argument(
|
|
232
|
+
"vi_a",
|
|
233
|
+
help="Path to first .vi file",
|
|
234
|
+
)
|
|
235
|
+
diff_parser.add_argument(
|
|
236
|
+
"vi_b",
|
|
237
|
+
help="Path to second .vi file",
|
|
238
|
+
)
|
|
239
|
+
diff_parser.add_argument(
|
|
240
|
+
"--long", action="store_true",
|
|
241
|
+
help="Show structured change report instead of unified diff",
|
|
242
|
+
)
|
|
243
|
+
diff_parser.add_argument(
|
|
244
|
+
"--search-path",
|
|
245
|
+
action="append",
|
|
246
|
+
dest="search_paths",
|
|
247
|
+
default=[],
|
|
248
|
+
help="Search paths for SubVI resolution (can be repeated)",
|
|
249
|
+
)
|
|
250
|
+
_add_project_root_arg(diff_parser)
|
|
251
|
+
|
|
252
|
+
# Init command - create .lvkit/ project store
|
|
253
|
+
init_parser = subparsers.add_parser(
|
|
254
|
+
"init",
|
|
255
|
+
help="Initialize a project-local .lvkit/ resolution store",
|
|
256
|
+
)
|
|
257
|
+
init_parser.add_argument(
|
|
258
|
+
"directory",
|
|
259
|
+
nargs="?",
|
|
260
|
+
default=".",
|
|
261
|
+
help="Directory in which to create .lvkit/ (default: current directory)",
|
|
262
|
+
)
|
|
263
|
+
init_parser.add_argument(
|
|
264
|
+
"--skills",
|
|
265
|
+
choices=["claude", "copilot", "all"],
|
|
266
|
+
default=None,
|
|
267
|
+
help=(
|
|
268
|
+
"Also install lvkit's resolve workflows into your LLM editor: "
|
|
269
|
+
"claude installs lvkit-prefixed Claude Code skills under "
|
|
270
|
+
".claude/skills/; copilot installs per-workflow prompts under "
|
|
271
|
+
".github/prompts/ plus a router at "
|
|
272
|
+
".github/instructions/lvkit.instructions.md; all does both."
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
init_parser.add_argument(
|
|
276
|
+
"--force",
|
|
277
|
+
action="store_true",
|
|
278
|
+
help="Overwrite existing skill files even if they have local edits",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
args = parser.parse_args()
|
|
282
|
+
|
|
283
|
+
if args.command == "structure":
|
|
284
|
+
return cmd_structure(args)
|
|
285
|
+
elif args.command == "mcp":
|
|
286
|
+
return cmd_mcp(args)
|
|
287
|
+
elif args.command == "describe":
|
|
288
|
+
return cmd_describe(args)
|
|
289
|
+
elif args.command == "generate":
|
|
290
|
+
return cmd_generate(args)
|
|
291
|
+
elif args.command == "docs":
|
|
292
|
+
return cmd_docs(args)
|
|
293
|
+
elif args.command == "visualize":
|
|
294
|
+
return cmd_visualize(args)
|
|
295
|
+
elif args.command == "diff":
|
|
296
|
+
return cmd_diff(args)
|
|
297
|
+
elif args.command == "init":
|
|
298
|
+
return cmd_init(args)
|
|
299
|
+
else:
|
|
300
|
+
parser.print_help()
|
|
301
|
+
return 0
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def cmd_structure(args: argparse.Namespace) -> int:
|
|
305
|
+
"""Handle the structure command."""
|
|
306
|
+
input_path = Path(args.input)
|
|
307
|
+
|
|
308
|
+
if not input_path.exists():
|
|
309
|
+
print(f"Error: Path not found: {input_path}", file=sys.stderr)
|
|
310
|
+
return 1
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
if input_path.suffix == ".lvclass":
|
|
314
|
+
# Single class
|
|
315
|
+
cls = parse_lvclass(input_path)
|
|
316
|
+
if args.json:
|
|
317
|
+
data = {
|
|
318
|
+
"name": cls.name,
|
|
319
|
+
"path": str(cls.path),
|
|
320
|
+
"parent_class": cls.parent_class,
|
|
321
|
+
"private_data": cls.private_data_ctl,
|
|
322
|
+
"methods": [
|
|
323
|
+
{
|
|
324
|
+
"name": m.name,
|
|
325
|
+
"scope": m.scope,
|
|
326
|
+
"is_static": m.is_static,
|
|
327
|
+
"vi_path": m.vi_path,
|
|
328
|
+
}
|
|
329
|
+
for m in cls.methods
|
|
330
|
+
],
|
|
331
|
+
}
|
|
332
|
+
print(json.dumps(data, indent=2))
|
|
333
|
+
else:
|
|
334
|
+
print(f"Class: {cls.name}")
|
|
335
|
+
if cls.parent_class:
|
|
336
|
+
print(f" Inherits: {cls.parent_class}")
|
|
337
|
+
if cls.private_data_ctl:
|
|
338
|
+
print(f" Private Data: {cls.private_data_ctl}")
|
|
339
|
+
if cls.methods:
|
|
340
|
+
print(" Methods:")
|
|
341
|
+
for m in cls.methods:
|
|
342
|
+
static = " [static]" if m.is_static else ""
|
|
343
|
+
print(f" - {m.name} ({m.scope}){static}")
|
|
344
|
+
|
|
345
|
+
elif input_path.suffix == ".lvlib":
|
|
346
|
+
# Single library
|
|
347
|
+
lib = parse_lvlib(input_path)
|
|
348
|
+
if args.json:
|
|
349
|
+
data = {
|
|
350
|
+
"name": lib.name,
|
|
351
|
+
"path": str(lib.path),
|
|
352
|
+
"version": lib.version,
|
|
353
|
+
"members": [
|
|
354
|
+
{"name": m.name, "type": m.member_type, "url": m.url}
|
|
355
|
+
for m in lib.members
|
|
356
|
+
],
|
|
357
|
+
}
|
|
358
|
+
print(json.dumps(data, indent=2))
|
|
359
|
+
else:
|
|
360
|
+
print(f"Library: {lib.name}")
|
|
361
|
+
if lib.version:
|
|
362
|
+
print(f" Version: {lib.version}")
|
|
363
|
+
if lib.members:
|
|
364
|
+
print(f" Members ({len(lib.members)}):")
|
|
365
|
+
for m in lib.members:
|
|
366
|
+
print(f" - {m.name} [{m.member_type}]")
|
|
367
|
+
|
|
368
|
+
elif input_path.is_dir():
|
|
369
|
+
# Directory - discover full project
|
|
370
|
+
structure = discover_project_structure(input_path)
|
|
371
|
+
|
|
372
|
+
if args.plan:
|
|
373
|
+
plan = generate_python_structure_plan(structure)
|
|
374
|
+
print(plan)
|
|
375
|
+
elif args.json:
|
|
376
|
+
print(json.dumps(structure, indent=2))
|
|
377
|
+
else:
|
|
378
|
+
print(f"Project Structure: {input_path}")
|
|
379
|
+
print(f" Libraries: {len(structure['libraries'])}")
|
|
380
|
+
print(f" Classes: {len(structure['classes'])}")
|
|
381
|
+
print(f" Standalone VIs: {len(structure['standalone_vis'])}")
|
|
382
|
+
print()
|
|
383
|
+
if structure['classes']:
|
|
384
|
+
print("Classes:")
|
|
385
|
+
for cls in structure['classes']:
|
|
386
|
+
methods = len(cls['methods'])
|
|
387
|
+
print(f" - {cls['name']} ({methods} methods)")
|
|
388
|
+
|
|
389
|
+
else:
|
|
390
|
+
print(f"Error: Unsupported file type: {input_path}", file=sys.stderr)
|
|
391
|
+
return 1
|
|
392
|
+
|
|
393
|
+
return 0
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
397
|
+
return 1
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def cmd_mcp(args: argparse.Namespace) -> int:
|
|
401
|
+
"""Handle the mcp command - run MCP server."""
|
|
402
|
+
from .mcp.server import main as mcp_main
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
print("Starting MCP server...", file=sys.stderr)
|
|
406
|
+
mcp_main()
|
|
407
|
+
return 0
|
|
408
|
+
except KeyboardInterrupt:
|
|
409
|
+
print("\nShutting down MCP server...", file=sys.stderr)
|
|
410
|
+
return 0
|
|
411
|
+
except Exception as e:
|
|
412
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
413
|
+
return 1
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def cmd_describe(args: argparse.Namespace) -> int:
|
|
417
|
+
"""Handle the describe command - human-readable VI description."""
|
|
418
|
+
from .graph.describe import describe_vi
|
|
419
|
+
|
|
420
|
+
input_path = Path(args.input_path)
|
|
421
|
+
if not input_path.exists():
|
|
422
|
+
print(f"Error: Path not found: {input_path}", file=sys.stderr)
|
|
423
|
+
return 1
|
|
424
|
+
|
|
425
|
+
_configure_resolvers(args)
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
graph = InMemoryVIGraph()
|
|
429
|
+
search_paths = [Path(p) for p in args.search_paths]
|
|
430
|
+
graph.load_vi(str(input_path), search_paths=search_paths)
|
|
431
|
+
|
|
432
|
+
vi_name = graph.resolve_vi_name(input_path.name)
|
|
433
|
+
|
|
434
|
+
print(describe_vi(graph, vi_name))
|
|
435
|
+
|
|
436
|
+
if args.chart:
|
|
437
|
+
from .graph.flowchart import flowchart
|
|
438
|
+
|
|
439
|
+
print()
|
|
440
|
+
print("## Dataflow Chart")
|
|
441
|
+
print()
|
|
442
|
+
print("```mermaid")
|
|
443
|
+
print(flowchart(graph, vi_name))
|
|
444
|
+
print("```")
|
|
445
|
+
|
|
446
|
+
return 0
|
|
447
|
+
except (ValueError, FileNotFoundError, KeyError) as e:
|
|
448
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
449
|
+
return 1
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
453
|
+
"""Handle the init command — create a project-local .lvkit/ store."""
|
|
454
|
+
root = Path(args.directory).resolve()
|
|
455
|
+
if not root.is_dir():
|
|
456
|
+
print(f"Error: Not a directory: {root}", file=sys.stderr)
|
|
457
|
+
return 1
|
|
458
|
+
|
|
459
|
+
store = init_project_store(root)
|
|
460
|
+
print(f"Initialized project store at {store}")
|
|
461
|
+
print(f" README: {store / 'README.md'}")
|
|
462
|
+
|
|
463
|
+
# Optional: install LLM editor skills
|
|
464
|
+
skills = getattr(args, "skills", None)
|
|
465
|
+
force = getattr(args, "force", False)
|
|
466
|
+
if skills in ("claude", "all"):
|
|
467
|
+
try:
|
|
468
|
+
written = install_claude_skills(root, force=force)
|
|
469
|
+
except FileExistsError as e:
|
|
470
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
471
|
+
return 1
|
|
472
|
+
if written:
|
|
473
|
+
print(f"Installed {len(written)} Claude Code skill(s):")
|
|
474
|
+
for p in written:
|
|
475
|
+
print(f" {p}")
|
|
476
|
+
else:
|
|
477
|
+
print("Claude Code skills already up to date.")
|
|
478
|
+
if skills in ("copilot", "all"):
|
|
479
|
+
try:
|
|
480
|
+
copilot_written = install_copilot_skills(root, force=force)
|
|
481
|
+
except FileExistsError as e:
|
|
482
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
483
|
+
return 1
|
|
484
|
+
if copilot_written:
|
|
485
|
+
print(f"Installed {len(copilot_written)} Copilot file(s):")
|
|
486
|
+
for p in copilot_written:
|
|
487
|
+
print(f" {p}")
|
|
488
|
+
else:
|
|
489
|
+
print("Copilot files already up to date.")
|
|
490
|
+
|
|
491
|
+
print()
|
|
492
|
+
print("Next steps:")
|
|
493
|
+
print(
|
|
494
|
+
" - Create .lvkit/primitives.json to override primitive mappings"
|
|
495
|
+
" (use lvkit's bundled primitives.json as a reference)"
|
|
496
|
+
)
|
|
497
|
+
print(
|
|
498
|
+
" - Add vi.lib mappings to .lvkit/vilib/<category>.json and register them"
|
|
499
|
+
" in .lvkit/vilib/_index.json"
|
|
500
|
+
)
|
|
501
|
+
print(" - lvkit will check .lvkit/ before its bundled data when resolving.")
|
|
502
|
+
if not skills:
|
|
503
|
+
print(
|
|
504
|
+
" - Run `lvkit init --skills all` to install resolve workflows"
|
|
505
|
+
" into Claude Code and/or Copilot."
|
|
506
|
+
)
|
|
507
|
+
return 0
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def cmd_diff(args: argparse.Namespace) -> int:
|
|
511
|
+
"""Handle the diff command — compare two VI versions."""
|
|
512
|
+
from .graph.diff import diff_structured, diff_text
|
|
513
|
+
|
|
514
|
+
path_a = Path(args.vi_a)
|
|
515
|
+
path_b = Path(args.vi_b)
|
|
516
|
+
|
|
517
|
+
for p in (path_a, path_b):
|
|
518
|
+
if not p.exists():
|
|
519
|
+
print(f"Error: Path not found: {p}", file=sys.stderr)
|
|
520
|
+
return 1
|
|
521
|
+
|
|
522
|
+
_configure_resolvers(args)
|
|
523
|
+
search_paths = [Path(p) for p in args.search_paths]
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
graph_a = InMemoryVIGraph()
|
|
527
|
+
graph_a.load_vi(str(path_a), search_paths=search_paths)
|
|
528
|
+
vi_name_a = graph_a.resolve_vi_name(path_a.name)
|
|
529
|
+
|
|
530
|
+
graph_b = InMemoryVIGraph()
|
|
531
|
+
graph_b.load_vi(str(path_b), search_paths=search_paths)
|
|
532
|
+
vi_name_b = graph_b.resolve_vi_name(path_b.name)
|
|
533
|
+
|
|
534
|
+
if args.long:
|
|
535
|
+
report = diff_structured(graph_a, graph_b, vi_name_a, vi_name_b)
|
|
536
|
+
if report.is_empty():
|
|
537
|
+
print("No changes detected.")
|
|
538
|
+
else:
|
|
539
|
+
print(report.format())
|
|
540
|
+
else:
|
|
541
|
+
result = diff_text(
|
|
542
|
+
graph_a, graph_b, vi_name_a, vi_name_b,
|
|
543
|
+
label_a=str(path_a), label_b=str(path_b),
|
|
544
|
+
)
|
|
545
|
+
if result:
|
|
546
|
+
print(result)
|
|
547
|
+
else:
|
|
548
|
+
print("No changes detected.")
|
|
549
|
+
|
|
550
|
+
return 0
|
|
551
|
+
except (ValueError, FileNotFoundError, KeyError) as e:
|
|
552
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
553
|
+
return 1
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def cmd_generate(args: argparse.Namespace) -> int:
|
|
557
|
+
"""Handle the generate command - AST-based Python generation."""
|
|
558
|
+
from .pipeline import generate_python
|
|
559
|
+
|
|
560
|
+
input_path = Path(args.input_path)
|
|
561
|
+
|
|
562
|
+
if not input_path.exists():
|
|
563
|
+
print(f"Error: Path not found: {input_path}", file=sys.stderr)
|
|
564
|
+
return 1
|
|
565
|
+
|
|
566
|
+
_configure_resolvers(args)
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
sp = [Path(p) for p in args.search_paths] if args.search_paths else None
|
|
570
|
+
result = generate_python(
|
|
571
|
+
input_path,
|
|
572
|
+
args.output,
|
|
573
|
+
search_paths=sp,
|
|
574
|
+
expand_subvis=not args.no_expand,
|
|
575
|
+
soft_unresolved=args.placeholder_on_unresolved,
|
|
576
|
+
)
|
|
577
|
+
return 1 if result["error"] > 0 else 0
|
|
578
|
+
|
|
579
|
+
except (ValueError, FileNotFoundError, KeyError, NotImplementedError) as e:
|
|
580
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
581
|
+
traceback.print_exc()
|
|
582
|
+
return 1
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def cmd_docs(args: argparse.Namespace) -> int:
|
|
586
|
+
"""Handle the docs command - generate HTML documentation."""
|
|
587
|
+
from .docs.generate import generate_documents
|
|
588
|
+
|
|
589
|
+
input_path = Path(args.input_path)
|
|
590
|
+
|
|
591
|
+
if not input_path.exists():
|
|
592
|
+
print(f"Error: Path not found: {input_path}", file=sys.stderr)
|
|
593
|
+
return 1
|
|
594
|
+
|
|
595
|
+
_configure_resolvers(args)
|
|
596
|
+
|
|
597
|
+
try:
|
|
598
|
+
result = generate_documents(
|
|
599
|
+
library_path=str(input_path),
|
|
600
|
+
output_dir=args.output_dir,
|
|
601
|
+
search_paths=args.search_paths if args.search_paths else None,
|
|
602
|
+
expand_subvis=not args.no_expand,
|
|
603
|
+
)
|
|
604
|
+
print("\n" + result)
|
|
605
|
+
return 0
|
|
606
|
+
|
|
607
|
+
except Exception as e:
|
|
608
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
609
|
+
traceback.print_exc()
|
|
610
|
+
return 1
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def cmd_visualize(args: argparse.Namespace) -> int:
|
|
614
|
+
"""Handle the visualize command — interactive graph in browser."""
|
|
615
|
+
input_path = Path(args.input_path)
|
|
616
|
+
if not input_path.exists():
|
|
617
|
+
print(f"Error: Path not found: {input_path}", file=sys.stderr)
|
|
618
|
+
return 1
|
|
619
|
+
|
|
620
|
+
_configure_resolvers(args)
|
|
621
|
+
|
|
622
|
+
graph = InMemoryVIGraph()
|
|
623
|
+
search_paths = (
|
|
624
|
+
[Path(p) for p in args.search_paths] if args.search_paths else None
|
|
625
|
+
)
|
|
626
|
+
expand = not args.no_expand
|
|
627
|
+
|
|
628
|
+
suffix = input_path.suffix.lower()
|
|
629
|
+
if suffix == ".lvclass":
|
|
630
|
+
graph.load_lvclass(str(input_path), expand, search_paths)
|
|
631
|
+
elif suffix == ".lvlib":
|
|
632
|
+
graph.load_lvlib(str(input_path), expand, search_paths)
|
|
633
|
+
elif input_path.is_dir():
|
|
634
|
+
graph.load_directory(str(input_path), expand, search_paths)
|
|
635
|
+
else:
|
|
636
|
+
graph.load_vi(str(input_path), expand, search_paths)
|
|
637
|
+
|
|
638
|
+
output = Path(args.output)
|
|
639
|
+
|
|
640
|
+
# Default format: flowchart for dataflow, interactive for deps
|
|
641
|
+
fmt = args.format or ("interactive" if args.mode == "deps" else "flowchart")
|
|
642
|
+
|
|
643
|
+
if fmt == "flowchart":
|
|
644
|
+
from .graph.flowchart import flowchart_html
|
|
645
|
+
|
|
646
|
+
vis = list(graph.list_vis())
|
|
647
|
+
primary_vi = vis[0] if vis else ""
|
|
648
|
+
html = flowchart_html(graph, primary_vi)
|
|
649
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
650
|
+
output.write_text(html)
|
|
651
|
+
else:
|
|
652
|
+
try:
|
|
653
|
+
import pyvis # type: ignore[import-untyped] # noqa: F401
|
|
654
|
+
except ImportError:
|
|
655
|
+
print(
|
|
656
|
+
"Error: pyvis not installed. Run: pip install pyvis",
|
|
657
|
+
file=sys.stderr,
|
|
658
|
+
)
|
|
659
|
+
return 1
|
|
660
|
+
if args.mode == "deps":
|
|
661
|
+
_visualize_deps(graph, output)
|
|
662
|
+
else:
|
|
663
|
+
_visualize_dataflow(graph, output)
|
|
664
|
+
|
|
665
|
+
print(f"Graph saved to {args.output}")
|
|
666
|
+
|
|
667
|
+
if args.open:
|
|
668
|
+
import webbrowser
|
|
669
|
+
webbrowser.open(f"file://{Path(args.output).resolve()}")
|
|
670
|
+
|
|
671
|
+
return 0
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
_GRAPH_OPTIONS = """
|
|
675
|
+
{
|
|
676
|
+
"physics": {
|
|
677
|
+
"barnesHut": {
|
|
678
|
+
"gravitationalConstant": -8000,
|
|
679
|
+
"centralGravity": 0.1,
|
|
680
|
+
"springLength": 200,
|
|
681
|
+
"springConstant": 0.04,
|
|
682
|
+
"damping": 0.3
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
"edges": {
|
|
686
|
+
"arrows": {
|
|
687
|
+
"to": {"enabled": true, "scaleFactor": 1.0, "type": "arrow"}
|
|
688
|
+
},
|
|
689
|
+
"color": {"color": "#555", "highlight": "#000"},
|
|
690
|
+
"width": 2,
|
|
691
|
+
"smooth": {"type": "curvedCW", "roundness": 0.15}
|
|
692
|
+
},
|
|
693
|
+
"nodes": {
|
|
694
|
+
"font": {"size": 14, "face": "arial", "bold": {"face": "arial"}},
|
|
695
|
+
"borderWidth": 2,
|
|
696
|
+
"shadow": true
|
|
697
|
+
},
|
|
698
|
+
"interaction": {
|
|
699
|
+
"hover": true,
|
|
700
|
+
"tooltipDelay": 100
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
"""
|
|
704
|
+
|
|
705
|
+
_PROPERTIES_PANEL = """
|
|
706
|
+
<div id="props" style="position:fixed;top:10px;left:10px;width:320px;
|
|
707
|
+
background:white;border:1px solid #ccc;padding:12px;
|
|
708
|
+
border-radius:8px;font-family:monospace;font-size:12px;
|
|
709
|
+
z-index:1000;box-shadow:0 2px 8px rgba(0,0,0,0.15);
|
|
710
|
+
max-height:80vh;overflow-y:auto">
|
|
711
|
+
<b style="font-size:14px">Properties</b>
|
|
712
|
+
<div id="propContent" style="margin-top:8px;color:#666">
|
|
713
|
+
Click a node to see details
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
<script>
|
|
717
|
+
network.on("click", function(params) {
|
|
718
|
+
if (params.nodes.length > 0) {
|
|
719
|
+
var nodeId = params.nodes[0];
|
|
720
|
+
var node = nodes.get(nodeId);
|
|
721
|
+
var html = "<b>" + (node.label || nodeId) + "</b><br><br>";
|
|
722
|
+
if (node.title) {
|
|
723
|
+
html += node.title.replace(/\\n/g, "<br>");
|
|
724
|
+
}
|
|
725
|
+
document.getElementById("propContent").innerHTML = html;
|
|
726
|
+
} else {
|
|
727
|
+
document.getElementById("propContent").innerHTML =
|
|
728
|
+
"Click a node to see details";
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
</script>
|
|
732
|
+
"""
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _build_legend(mode: str) -> str:
|
|
736
|
+
"""Build legend HTML for the graph."""
|
|
737
|
+
if mode == "deps":
|
|
738
|
+
return """
|
|
739
|
+
<div style="position:fixed;top:10px;right:10px;background:white;
|
|
740
|
+
border:1px solid #ccc;padding:12px;border-radius:8px;
|
|
741
|
+
font-family:monospace;font-size:13px;z-index:1000;
|
|
742
|
+
box-shadow:0 2px 8px rgba(0,0,0,0.15)">
|
|
743
|
+
<b style="font-size:14px">Dependency Graph</b><br><br>
|
|
744
|
+
<span style="color:#4CAF50">■</span> VI<br>
|
|
745
|
+
<span style="color:#FF9800">■</span> Library<br>
|
|
746
|
+
<span style="color:#2196F3">■</span> Class<br>
|
|
747
|
+
<span style="color:#9C27B0">■</span> Typedef<br>
|
|
748
|
+
<span style="color:#999">■</span> Stub (missing)<br>
|
|
749
|
+
<br><span style="color:#888">→ depends on</span>
|
|
750
|
+
</div>
|
|
751
|
+
"""
|
|
752
|
+
return """
|
|
753
|
+
<div style="position:fixed;top:10px;right:10px;background:white;
|
|
754
|
+
border:1px solid #ccc;padding:12px;border-radius:8px;
|
|
755
|
+
font-family:monospace;font-size:13px;z-index:1000;
|
|
756
|
+
box-shadow:0 2px 8px rgba(0,0,0,0.15)">
|
|
757
|
+
<b style="font-size:14px">Dataflow Graph</b><br><br>
|
|
758
|
+
<span style="color:#4CAF50">■</span> SubVI call<br>
|
|
759
|
+
<span style="color:#2196F3">■</span> Primitive operation<br>
|
|
760
|
+
<span style="color:#FF9800">◆</span> Structure (case/loop)<br>
|
|
761
|
+
<span style="color:#9C27B0">●</span> Constant<br>
|
|
762
|
+
<br><span style="color:#888">→ data flow</span>
|
|
763
|
+
</div>
|
|
764
|
+
"""
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _inject_extras(output: Path, mode: str) -> None:
|
|
768
|
+
"""Inject legend and properties panel into generated HTML."""
|
|
769
|
+
html = output.read_text()
|
|
770
|
+
extras = _build_legend(mode) + _PROPERTIES_PANEL
|
|
771
|
+
html = html.replace("</body>", extras + "</body>")
|
|
772
|
+
output.write_text(html)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _visualize_dataflow(
|
|
776
|
+
graph: InMemoryVIGraph, output: Path,
|
|
777
|
+
) -> None:
|
|
778
|
+
"""Visualize the dataflow graph for a single VI."""
|
|
779
|
+
from pyvis.network import Network # type: ignore[import-untyped]
|
|
780
|
+
|
|
781
|
+
vis = list(graph.list_vis())
|
|
782
|
+
if not vis:
|
|
783
|
+
print("Error: No VIs loaded", file=sys.stderr)
|
|
784
|
+
return
|
|
785
|
+
primary_vi = vis[0]
|
|
786
|
+
|
|
787
|
+
net = Network(
|
|
788
|
+
height="800px", width="100%", directed=True, notebook=False,
|
|
789
|
+
)
|
|
790
|
+
net.set_options(_GRAPH_OPTIONS)
|
|
791
|
+
|
|
792
|
+
node_styles = {
|
|
793
|
+
"vi": {"color": "#4CAF50", "shape": "box"},
|
|
794
|
+
"primitive": {"color": "#2196F3", "shape": "box"},
|
|
795
|
+
"structure": {"color": "#FF9800", "shape": "diamond"},
|
|
796
|
+
"constant": {"color": "#9C27B0", "shape": "ellipse"},
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
for nid in graph._vi_nodes.get(primary_vi, set()):
|
|
800
|
+
gnode = graph._graph.nodes[nid].get("node")
|
|
801
|
+
if not gnode or nid == primary_vi:
|
|
802
|
+
continue
|
|
803
|
+
|
|
804
|
+
kind = getattr(gnode, "kind", "unknown")
|
|
805
|
+
style = node_styles.get(kind, {"color": "#666", "shape": "box"})
|
|
806
|
+
label = _dataflow_label(gnode, kind)
|
|
807
|
+
tooltip = _dataflow_tooltip(gnode, kind, nid)
|
|
808
|
+
|
|
809
|
+
# Group by parent structure + frame for visual clustering
|
|
810
|
+
group = None
|
|
811
|
+
if gnode.parent and gnode.frame is not None:
|
|
812
|
+
group = f"{gnode.parent}::{gnode.frame}"
|
|
813
|
+
elif gnode.parent:
|
|
814
|
+
group = gnode.parent
|
|
815
|
+
|
|
816
|
+
net.add_node(
|
|
817
|
+
nid, label=label,
|
|
818
|
+
color=style["color"],
|
|
819
|
+
shape=style.get("shape", "box"),
|
|
820
|
+
title=tooltip,
|
|
821
|
+
group=group,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
added = {n["id"] for n in net.nodes}
|
|
825
|
+
for nid in added:
|
|
826
|
+
for _, dest, _, data in graph._graph.out_edges(
|
|
827
|
+
nid, data=True, keys=True,
|
|
828
|
+
):
|
|
829
|
+
if dest not in added:
|
|
830
|
+
continue
|
|
831
|
+
src_end = data.get("source")
|
|
832
|
+
dst_end = data.get("dest")
|
|
833
|
+
title = ""
|
|
834
|
+
if src_end and dst_end:
|
|
835
|
+
sn = src_end.name or ""
|
|
836
|
+
dn = dst_end.name or ""
|
|
837
|
+
if sn or dn:
|
|
838
|
+
title = f"{sn} → {dn}"
|
|
839
|
+
net.add_edge(nid, dest, title=title)
|
|
840
|
+
|
|
841
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
842
|
+
net.save_graph(str(output))
|
|
843
|
+
_inject_extras(output, "dataflow")
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _visualize_deps(
|
|
847
|
+
graph: InMemoryVIGraph, output: Path,
|
|
848
|
+
) -> None:
|
|
849
|
+
"""Visualize the dependency graph across VIs."""
|
|
850
|
+
from pyvis.network import Network # type: ignore[import-untyped]
|
|
851
|
+
|
|
852
|
+
net = Network(
|
|
853
|
+
height="800px", width="100%", directed=True, notebook=False,
|
|
854
|
+
)
|
|
855
|
+
net.set_options(_GRAPH_OPTIONS)
|
|
856
|
+
|
|
857
|
+
dep = graph._dep_graph
|
|
858
|
+
stubs = graph._stubs
|
|
859
|
+
|
|
860
|
+
for node_id in dep.nodes:
|
|
861
|
+
attrs = dep.nodes[node_id]
|
|
862
|
+
node_type = attrs.get("node_type", "vi")
|
|
863
|
+
is_stub = node_id in stubs
|
|
864
|
+
|
|
865
|
+
colors = {
|
|
866
|
+
"vi": "#4CAF50",
|
|
867
|
+
"library": "#FF9800",
|
|
868
|
+
"class": "#2196F3",
|
|
869
|
+
"typedef": "#9C27B0",
|
|
870
|
+
}
|
|
871
|
+
color = "#999" if is_stub else colors.get(node_type, "#666")
|
|
872
|
+
|
|
873
|
+
label = node_id.split(":")[-1] if ":" in node_id else node_id
|
|
874
|
+
tooltip = f"{node_type}: {node_id}"
|
|
875
|
+
if is_stub:
|
|
876
|
+
tooltip += "\n(missing/stub)"
|
|
877
|
+
fields = attrs.get("fields")
|
|
878
|
+
if fields:
|
|
879
|
+
tooltip += f"\nFields: {len(fields)}"
|
|
880
|
+
for i, f in enumerate(fields):
|
|
881
|
+
tooltip += f"\n [{i}] {f.name}"
|
|
882
|
+
|
|
883
|
+
net.add_node(
|
|
884
|
+
node_id, label=label, color=color,
|
|
885
|
+
shape="box",
|
|
886
|
+
title=tooltip,
|
|
887
|
+
borderWidth=1 if is_stub else 2,
|
|
888
|
+
font={"color": "#999"} if is_stub else {},
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
for src, dest in dep.edges:
|
|
892
|
+
net.add_edge(src, dest)
|
|
893
|
+
|
|
894
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
895
|
+
net.save_graph(str(output))
|
|
896
|
+
_inject_extras(output, "deps")
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _dataflow_label(gnode, kind: str) -> str:
|
|
900
|
+
"""Build readable label for a dataflow node."""
|
|
901
|
+
name = gnode.name or ""
|
|
902
|
+
if kind == "constant":
|
|
903
|
+
val = getattr(gnode, "value", "")
|
|
904
|
+
return f"{val}" if val is not None else "const"
|
|
905
|
+
if kind == "structure":
|
|
906
|
+
lt = getattr(gnode, "loop_type", None)
|
|
907
|
+
frames = getattr(gnode, "frames", [])
|
|
908
|
+
if lt:
|
|
909
|
+
return "While Loop" if lt == "whileLoop" else "For Loop"
|
|
910
|
+
if frames:
|
|
911
|
+
return f"Case [{len(frames)} frames]"
|
|
912
|
+
return name or "Structure"
|
|
913
|
+
return name.replace(".vi", "") or "?"
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _dataflow_tooltip(gnode, kind: str, nid: str) -> str:
|
|
917
|
+
"""Build detailed tooltip for a dataflow node."""
|
|
918
|
+
name = gnode.name or nid.split("::")[-1]
|
|
919
|
+
lines = [f"<b>{kind}: {name}</b>", f"ID: {nid}"]
|
|
920
|
+
|
|
921
|
+
prim_id = getattr(gnode, "prim_id", None)
|
|
922
|
+
if prim_id:
|
|
923
|
+
lines.append(f"primResID: {prim_id}")
|
|
924
|
+
|
|
925
|
+
node_type = getattr(gnode, "node_type", None)
|
|
926
|
+
if node_type:
|
|
927
|
+
lines.append(f"XML class: {node_type}")
|
|
928
|
+
|
|
929
|
+
terminals = getattr(gnode, "terminals", [])
|
|
930
|
+
inputs = [
|
|
931
|
+
t for t in terminals
|
|
932
|
+
if t.direction == "input" and not t.is_error_cluster
|
|
933
|
+
]
|
|
934
|
+
outputs = [
|
|
935
|
+
t for t in terminals
|
|
936
|
+
if t.direction == "output" and not t.is_error_cluster
|
|
937
|
+
]
|
|
938
|
+
|
|
939
|
+
if inputs:
|
|
940
|
+
lines.append("")
|
|
941
|
+
lines.append("<b>Inputs:</b>")
|
|
942
|
+
for t in inputs:
|
|
943
|
+
tname = t.name or f"idx{t.index}"
|
|
944
|
+
ttype = t.python_type()
|
|
945
|
+
lines.append(f" [{t.index}] {tname}: {ttype}")
|
|
946
|
+
|
|
947
|
+
if outputs:
|
|
948
|
+
lines.append("")
|
|
949
|
+
lines.append("<b>Outputs:</b>")
|
|
950
|
+
for t in outputs:
|
|
951
|
+
tname = t.name or f"idx{t.index}"
|
|
952
|
+
ttype = t.python_type()
|
|
953
|
+
lines.append(f" [{t.index}] {tname}: {ttype}")
|
|
954
|
+
|
|
955
|
+
if kind == "constant":
|
|
956
|
+
val = getattr(gnode, "value", None)
|
|
957
|
+
raw = getattr(gnode, "raw_value", None)
|
|
958
|
+
lv_type = getattr(gnode, "lv_type", None)
|
|
959
|
+
lines.append(f"\\nValue: {val!r}")
|
|
960
|
+
if raw:
|
|
961
|
+
lines.append(f"Raw: {raw}")
|
|
962
|
+
if lv_type:
|
|
963
|
+
lines.append(f"Type: {lv_type.to_python()}")
|
|
964
|
+
|
|
965
|
+
if kind == "structure":
|
|
966
|
+
frames = getattr(gnode, "frames", [])
|
|
967
|
+
if frames:
|
|
968
|
+
lines.append("")
|
|
969
|
+
lines.append("<b>Frames:</b>")
|
|
970
|
+
for f in frames:
|
|
971
|
+
default = " (default)" if f.is_default else ""
|
|
972
|
+
lines.append(
|
|
973
|
+
f" {f.selector_value}{default}"
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
return "\\n".join(lines)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
if __name__ == "__main__":
|
|
980
|
+
sys.exit(main())
|