design-graph 0.34.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. design_graph/__init__.py +0 -0
  2. design_graph/cli/__init__.py +0 -0
  3. design_graph/cli/_logging.py +36 -0
  4. design_graph/cli/build.py +586 -0
  5. design_graph/cli/databases.py +181 -0
  6. design_graph/cli/init.py +295 -0
  7. design_graph/cli/query.py +235 -0
  8. design_graph/cli/report.py +224 -0
  9. design_graph/cli/status.py +230 -0
  10. design_graph/cli/validate.py +326 -0
  11. design_graph/core/__init__.py +0 -0
  12. design_graph/core/constants.py +205 -0
  13. design_graph/core/graph_catalog.py +280 -0
  14. design_graph/core/models.py +786 -0
  15. design_graph/core/patterns.py +367 -0
  16. design_graph/extraction/__init__.py +0 -0
  17. design_graph/extraction/alias_extractor.py +79 -0
  18. design_graph/extraction/chunker.py +247 -0
  19. design_graph/extraction/component_extractor.py +639 -0
  20. design_graph/extraction/icon_extractor.py +81 -0
  21. design_graph/extraction/jsx_sanitizer.py +174 -0
  22. design_graph/extraction/module_data_extractor.py +95 -0
  23. design_graph/extraction/module_text_extractor.py +113 -0
  24. design_graph/extraction/plain_html_component_extractor.py +164 -0
  25. design_graph/extraction/prop_extractor.py +137 -0
  26. design_graph/extraction/screen_extractor.py +187 -0
  27. design_graph/extraction/section_extractor.py +625 -0
  28. design_graph/extraction/visual_function.py +23 -0
  29. design_graph/graph/__init__.py +0 -0
  30. design_graph/graph/diff.py +50 -0
  31. design_graph/graph/reader.py +1602 -0
  32. design_graph/graph/schema.py +210 -0
  33. design_graph/graph/writer.py +713 -0
  34. design_graph/mcp/__init__.py +0 -0
  35. design_graph/mcp/aliases.py +67 -0
  36. design_graph/mcp/metrics.py +222 -0
  37. design_graph/mcp/search.py +214 -0
  38. design_graph/mcp/server.py +352 -0
  39. design_graph/mcp/tools.py +1942 -0
  40. design_graph/parsing/__init__.py +0 -0
  41. design_graph/parsing/css_class_resolver.py +666 -0
  42. design_graph/parsing/format_detector.py +64 -0
  43. design_graph/parsing/html_parser.py +230 -0
  44. design_graph/parsing/js_parser.py +700 -0
  45. design_graph/parsing/palette_extractor.py +113 -0
  46. design_graph/parsing/source_loader.py +203 -0
  47. design_graph/parsing/token_extractor.py +426 -0
  48. design_graph/paths.py +67 -0
  49. design_graph/pipeline/__init__.py +0 -0
  50. design_graph/pipeline/build_progress.py +223 -0
  51. design_graph/pipeline/coordinator.py +440 -0
  52. design_graph/pipeline/state.py +199 -0
  53. design_graph/resources/skills/design-graph-ui-context/SKILL.md +124 -0
  54. design_graph/workspace.py +113 -0
  55. design_graph-0.34.0.dist-info/METADATA +575 -0
  56. design_graph-0.34.0.dist-info/RECORD +59 -0
  57. design_graph-0.34.0.dist-info/WHEEL +5 -0
  58. design_graph-0.34.0.dist-info/entry_points.txt +4 -0
  59. design_graph-0.34.0.dist-info/top_level.txt +1 -0
File without changes
File without changes
@@ -0,0 +1,36 @@
1
+ """
2
+ Logging configuration for design-graph CLI commands.
3
+
4
+ Single entry point so both build.py and query.py configure logging
5
+ identically — level determined by --verbose / --quiet flags.
6
+
7
+ default → INFO (progress messages, build summary)
8
+ --verbose → DEBUG (module-level diagnostics, query plans)
9
+ --quiet → WARNING (only warnings and errors)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+
16
+
17
+ def configure_cli_logging(*, verbose: bool = False, quiet: bool = False) -> None:
18
+ """
19
+ Set up root logger for CLI use.
20
+
21
+ Precedence: quiet > verbose > default (INFO).
22
+ Format keeps messages readable without timestamps (those belong in structured logs).
23
+ """
24
+ if quiet:
25
+ level = logging.WARNING
26
+ elif verbose:
27
+ level = logging.DEBUG
28
+ else:
29
+ level = logging.INFO
30
+
31
+ logging.basicConfig(
32
+ level=level,
33
+ format="%(levelname)s %(name)s: %(message)s" if verbose else "%(message)s",
34
+ force=True, # override any previous basicConfig call
35
+ )
36
+ logging.getLogger("design_graph").setLevel(level)
@@ -0,0 +1,586 @@
1
+ """
2
+ CLI entry point: design-graph
3
+
4
+ Commands:
5
+ design-graph <proto.html> build knowledge graph
6
+ design-graph <proto.html> --diff show what changed since last build
7
+ design-graph <proto.html> --force rebuild even if HTML is unchanged
8
+ design-graph <proto.html> --db <path> save graph to a custom path
9
+ design-graph <proto.html> --name <name> save graph as <name>.db
10
+ design-graph <proto.html> --verbose show debug-level pipeline logs
11
+ design-graph <proto.html> --quiet suppress all output except errors
12
+ design-graph --version print version and exit
13
+ design-graph chunk <proto.html> export AI-ready chunks as JSONL
14
+ design-graph chunk <proto.html> --output <f> write JSONL to custom file
15
+ design-graph chunk <proto.html> --max-chars N set max chars per chunk (default 12000)
16
+ design-graph status show graph health and last build info
17
+ design-graph status --db <path> status for a specific database
18
+ design-graph report generate Markdown prototype report
19
+ design-graph report --db <path> report from a specific database
20
+ design-graph report --output <file> write report to a Markdown file
21
+ design-graph report --name <name> override prototype name in report
22
+ design-graph report --no-tokens exclude design token table
23
+ design-graph report --jsx include JSX snippets in report
24
+ design-graph init [path] configure the UI-context skill for an AI coding tool (interactive)
25
+ design-graph init [path] --tool claude,cursor configure it for specific tools without prompting
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import asyncio
32
+ import sys
33
+ from dataclasses import dataclass
34
+ from pathlib import Path
35
+
36
+ from design_graph.cli._logging import configure_cli_logging
37
+ from design_graph.core.graph_catalog import GraphDocumentName
38
+ from design_graph.core.models import resolve_icon_markers
39
+ from design_graph.paths import default_db_for
40
+ from design_graph.cli.databases import DatabaseCliArgs, parse_database_args
41
+ from design_graph.cli.init import InitCliArgs, parse_init_args
42
+
43
+
44
+ # ── Typed argument containers ─────────────────────────────────────────────────
45
+
46
+ @dataclass
47
+ class BuildCliArgs:
48
+ html_path: Path
49
+ db_path: Path | None
50
+ prototype_name: GraphDocumentName | None
51
+ show_diff: bool
52
+ force: bool
53
+ verbose: bool
54
+ quiet: bool
55
+ json_output: bool = False
56
+
57
+
58
+ @dataclass
59
+ class ValidateCliArgs:
60
+ db_path: Path | None
61
+ verbose: bool
62
+ json_output: bool
63
+ document: str | None = None
64
+
65
+
66
+ @dataclass
67
+ class ChunkCliArgs:
68
+ html_path: Path
69
+ output_path: Path
70
+ max_chars: int
71
+ verbose: bool
72
+
73
+
74
+ @dataclass
75
+ class StatusCliArgs:
76
+ db_path: Path | None
77
+ verbose: bool
78
+ document: str | None = None
79
+
80
+
81
+ @dataclass
82
+ class ReportCliArgs:
83
+ db_path: Path | None
84
+ output_path: Path | None # None → write to stdout
85
+ prototype_name: str | None # None → infer from selected db stem
86
+ include_tokens: bool
87
+ include_jsx: bool
88
+ verbose: bool
89
+ document: str | None = None
90
+
91
+
92
+ # ── Argument parsers (pure, testable, no I/O) ─────────────────────────────────
93
+
94
+ def parse_status_args(argv: list[str]) -> StatusCliArgs:
95
+ """Parse argv for the 'status' subcommand. Raises SystemExit on bad input."""
96
+ p = argparse.ArgumentParser(
97
+ prog="design-graph status",
98
+ description="Show design-graph database health and last build info.",
99
+ add_help=True,
100
+ )
101
+ p.add_argument("--db", dest="db_path", type=Path, default=None,
102
+ metavar="PATH", help="Graph database path")
103
+ p.add_argument("--doc", dest="document", default=None, metavar="NAME",
104
+ help="Prototype name to inspect")
105
+ p.add_argument("--verbose", action="store_true", help="Show debug-level logs")
106
+ ns = p.parse_args(argv)
107
+ return StatusCliArgs(db_path=ns.db_path, document=ns.document, verbose=ns.verbose)
108
+
109
+
110
+ def parse_build_args(argv: list[str]) -> BuildCliArgs:
111
+ """Parse argv for the 'build' command. Raises SystemExit on bad input."""
112
+ try:
113
+ from importlib.metadata import version as _pkg_version
114
+ _version = _pkg_version("design-graph")
115
+ except Exception:
116
+ _version = "dev"
117
+
118
+ p = argparse.ArgumentParser(
119
+ prog="design-graph",
120
+ description="Parse a prototype HTML into a Kuzu design-graph.",
121
+ add_help=True,
122
+ )
123
+ p.add_argument("--version", action="version", version=f"design-graph {_version}")
124
+ p.add_argument("html_path", type=Path, help="Path to the prototype HTML file")
125
+ target = p.add_mutually_exclusive_group()
126
+ target.add_argument("--db", dest="db_path", type=Path, default=None,
127
+ metavar="PATH", help="Custom graph database path")
128
+ target.add_argument("--name", dest="prototype_name", type=GraphDocumentName, default=None,
129
+ metavar="NAME", help="Database name to use as the file stem")
130
+ p.add_argument("--diff", dest="show_diff", action="store_true",
131
+ help="Show what changed since the last build")
132
+ p.add_argument("--force", action="store_true",
133
+ help="Rebuild even if the HTML is unchanged")
134
+ p.add_argument("--verbose", action="store_true",
135
+ help="Show debug-level pipeline logs")
136
+ p.add_argument("--quiet", action="store_true",
137
+ help="Suppress all output except errors")
138
+ p.add_argument("--json", dest="json_output", action="store_true",
139
+ help="Emit machine-readable JSON to stdout (for CI pipelines)")
140
+ ns = p.parse_args(argv)
141
+ return BuildCliArgs(
142
+ html_path=ns.html_path,
143
+ db_path=ns.db_path,
144
+ prototype_name=ns.prototype_name,
145
+ show_diff=ns.show_diff,
146
+ force=ns.force,
147
+ verbose=ns.verbose,
148
+ quiet=ns.quiet,
149
+ json_output=ns.json_output,
150
+ )
151
+
152
+
153
+ def parse_validate_args(argv: list[str]) -> ValidateCliArgs:
154
+ """Parse argv for the 'validate' subcommand."""
155
+ p = argparse.ArgumentParser(
156
+ prog="design-graph validate",
157
+ description="Validate design-graph database integrity.",
158
+ add_help=True,
159
+ )
160
+ p.add_argument("--db", dest="db_path", type=Path, default=None,
161
+ metavar="PATH", help="Graph database path")
162
+ p.add_argument("--doc", dest="document", default=None, metavar="NAME",
163
+ help="Prototype name to validate")
164
+ p.add_argument("--verbose", action="store_true", help="Show debug-level logs")
165
+ p.add_argument("--json", dest="json_output", action="store_true",
166
+ help="Output validation report as JSON")
167
+ ns = p.parse_args(argv)
168
+ return ValidateCliArgs(db_path=ns.db_path, document=ns.document,
169
+ verbose=ns.verbose, json_output=ns.json_output)
170
+
171
+
172
+ def parse_chunk_args(argv: list[str]) -> ChunkCliArgs:
173
+ """Parse argv for the 'chunk' subcommand. Raises SystemExit on bad input."""
174
+ p = argparse.ArgumentParser(
175
+ prog="design-graph chunk",
176
+ description="Export prototype as AI-ready JSONL chunks.",
177
+ add_help=True,
178
+ )
179
+ p.add_argument("html_path", type=Path, help="Path to the prototype HTML file")
180
+ p.add_argument("--output", dest="output_path", type=Path, default=None,
181
+ metavar="FILE", help="Output JSONL path (default: <proto>.jsonl)")
182
+ p.add_argument("--max-chars", type=int, default=12_000,
183
+ metavar="N", help="Maximum characters per chunk (default: 12000)")
184
+ p.add_argument("--verbose", action="store_true",
185
+ help="Show debug-level logs")
186
+ ns = p.parse_args(argv)
187
+ output_path = ns.output_path or ns.html_path.with_suffix(".jsonl")
188
+ return ChunkCliArgs(
189
+ html_path=ns.html_path,
190
+ output_path=output_path,
191
+ max_chars=ns.max_chars,
192
+ verbose=ns.verbose,
193
+ )
194
+
195
+
196
+ def parse_report_args(argv: list[str]) -> ReportCliArgs:
197
+ """Parse argv for the 'report' subcommand. Raises SystemExit on bad input."""
198
+ p = argparse.ArgumentParser(
199
+ prog="design-graph report",
200
+ description="Generate a Markdown prototype report from the design graph.",
201
+ add_help=True,
202
+ )
203
+ p.add_argument("--db", dest="db_path", type=Path, default=None,
204
+ metavar="PATH", help="Graph database path")
205
+ p.add_argument("--doc", dest="document", default=None, metavar="NAME",
206
+ help="Prototype name to report")
207
+ p.add_argument("--output", dest="output_path", type=Path, default=None,
208
+ metavar="FILE", help="Write report to this Markdown file (default: stdout)")
209
+ p.add_argument("--name", dest="prototype_name", default=None,
210
+ metavar="NAME", help="Prototype name shown in the report title")
211
+ p.add_argument("--no-tokens", dest="include_tokens", action="store_false", default=True,
212
+ help="Exclude the design-token table from the report")
213
+ p.add_argument("--jsx", dest="include_jsx", action="store_true",
214
+ help="Include JSX snippets in component sections")
215
+ p.add_argument("--verbose", action="store_true", help="Show debug-level logs")
216
+ ns = p.parse_args(argv)
217
+ return ReportCliArgs(
218
+ db_path=ns.db_path,
219
+ output_path=ns.output_path,
220
+ prototype_name=ns.prototype_name,
221
+ include_tokens=ns.include_tokens,
222
+ include_jsx=ns.include_jsx,
223
+ verbose=ns.verbose,
224
+ document=ns.document,
225
+ )
226
+
227
+
228
+ # ── Entry point ───────────────────────────────────────────────────────────────
229
+
230
+ def main() -> None:
231
+ args = sys.argv[1:]
232
+ if args in (["-h"], ["--help"]):
233
+ _print_main_help()
234
+ elif args and args[0] == "chunk":
235
+ _run_chunk(args[1:])
236
+ elif args and args[0] == "status":
237
+ _run_status(args[1:])
238
+ elif args and args[0] == "validate":
239
+ _run_validate(args[1:])
240
+ elif args and args[0] == "report":
241
+ _run_report(args[1:])
242
+ elif args and args[0] == "db":
243
+ _run_database(args[1:])
244
+ elif args and args[0] == "init":
245
+ _run_init(args[1:])
246
+ else:
247
+ _run_build(args)
248
+
249
+
250
+ # ── Command implementations ───────────────────────────────────────────────────
251
+
252
+ def _run_build(argv: list[str]) -> None:
253
+ try:
254
+ parsed = parse_build_args(argv)
255
+ except SystemExit:
256
+ raise
257
+
258
+ from design_graph.pipeline.coordinator import run_pipeline
259
+
260
+ configure_cli_logging(verbose=parsed.verbose, quiet=parsed.quiet)
261
+
262
+ if not parsed.html_path.exists():
263
+ print(f"error: file not found: {parsed.html_path}", file=sys.stderr)
264
+ sys.exit(1)
265
+
266
+ db_path = parsed.db_path or default_db_for(parsed.prototype_name or parsed.html_path.stem)
267
+ from design_graph.pipeline.state import BuildStateRepository
268
+ state_repository = BuildStateRepository.for_database(db_path)
269
+ state_path = state_repository.path
270
+ from design_graph.core.graph_catalog import GraphCatalog
271
+ catalog = GraphCatalog.discover(db_path.parent)
272
+ state_repository.migrate_legacy(
273
+ db_path.parent / ".graph-state.json",
274
+ known_databases=tuple(database.path for database in catalog.databases),
275
+ )
276
+
277
+ previous_state = state_repository.load()
278
+ source_changed = bool(
279
+ previous_state.source_path
280
+ and Path(previous_state.source_path).resolve() != parsed.html_path.resolve()
281
+ )
282
+ if source_changed:
283
+ print(
284
+ f"warning: {db_path.name} was previously built from {previous_state.source_path}; "
285
+ f"it will be replaced with {parsed.html_path}",
286
+ file=sys.stderr,
287
+ )
288
+
289
+ effective_force = parsed.force or source_changed
290
+ if effective_force:
291
+ state_repository.clear()
292
+
293
+ from design_graph.pipeline.build_progress import SilentBuildReporter, TerminalBuildReporter
294
+ reporter = (
295
+ SilentBuildReporter()
296
+ if parsed.quiet or parsed.json_output
297
+ else TerminalBuildReporter()
298
+ )
299
+
300
+ stats = asyncio.run(run_pipeline(
301
+ parsed.html_path, db_path, state_path,
302
+ show_diff=parsed.show_diff,
303
+ force=effective_force,
304
+ reporter=reporter,
305
+ ))
306
+
307
+ if stats is None:
308
+ if parsed.json_output:
309
+ import json as _json
310
+ print(_json.dumps({"status": "skipped", "reason": "unchanged"}))
311
+ elif not parsed.quiet:
312
+ print("Prototype unchanged — skipped. Use --force to rebuild.")
313
+ return
314
+
315
+ if parsed.json_output:
316
+ import json as _json
317
+ print(_json.dumps({
318
+ "status": "built",
319
+ "screens": stats.screens,
320
+ "components": stats.components,
321
+ "extracted_components": stats.extracted_components,
322
+ "unresolved_components": stats.unresolved_components,
323
+ "tokens": stats.tokens,
324
+ "icons": stats.icons,
325
+ "sections": stats.sections,
326
+ "interactions": stats.interactions,
327
+ "styles": stats.styles,
328
+ "texts": stats.texts,
329
+ "contains_rels": stats.contains_rels,
330
+ "component_props": stats.component_props,
331
+ "section_styles": stats.section_styles,
332
+ "write_errors": stats.write_errors,
333
+ "duration_seconds": round(stats.duration_seconds, 3),
334
+ }))
335
+ elif not parsed.quiet:
336
+ _print_build_summary(parsed.html_path, db_path, stats)
337
+
338
+
339
+ def _print_main_help() -> None:
340
+ """Print the complete command overview without importing pipeline dependencies."""
341
+ parser = argparse.ArgumentParser(
342
+ prog="design-graph",
343
+ description="Parse prototype HTML into a Kuzu design graph.",
344
+ usage="design-graph [--version] <html_path> [build options]\n"
345
+ " design-graph COMMAND [options]",
346
+ epilog=(
347
+ "commands:\n"
348
+ " chunk Export prototype content as AI-ready JSONL chunks\n"
349
+ " status Show database health and last build information\n"
350
+ " validate Validate database integrity (supports JSON output)\n"
351
+ " report Generate a Markdown prototype report\n"
352
+ " db List, inspect and select graph databases\n"
353
+ " init Configure the UI-context agent skill for Claude Code, Cursor,\n"
354
+ " Codex CLI, Antigravity or Kiro (interactive without --tool)\n\n"
355
+ "build options:\n"
356
+ " --db PATH Write to a custom database path\n"
357
+ " --name NAME Write to <name>.db under the graph directory\n"
358
+ " --diff Show changes since the previous build\n"
359
+ " --force Rebuild even when the HTML is unchanged\n"
360
+ " --verbose Show debug-level logs\n"
361
+ " --quiet Suppress output except errors\n"
362
+ " --json Emit machine-readable build output\n\n"
363
+ "Run 'design-graph COMMAND --help' for command-specific options."
364
+ ),
365
+ formatter_class=argparse.RawDescriptionHelpFormatter,
366
+ add_help=False,
367
+ )
368
+ parser.add_argument("-h", "--help", action="help", help="Show this help message and exit")
369
+ parser.add_argument("--version", action="store_true", help="Show version and exit")
370
+ parser.parse_args(["--help"])
371
+
372
+
373
+ def _run_chunk(argv: list[str]) -> None:
374
+ from design_graph.extraction.chunker import chunk_extracted_data, export_chunks_jsonl
375
+
376
+ try:
377
+ parsed = parse_chunk_args(argv)
378
+ except SystemExit:
379
+ raise
380
+
381
+ configure_cli_logging(verbose=parsed.verbose)
382
+
383
+ if not parsed.html_path.exists():
384
+ print(f"error: file not found: {parsed.html_path}", file=sys.stderr)
385
+ sys.exit(1)
386
+
387
+ count = asyncio.run(_build_and_export_chunks(parsed))
388
+ print(f"{count} chunks exported → {parsed.output_path}")
389
+
390
+
391
+ async def _build_and_export_chunks(parsed: ChunkCliArgs) -> int:
392
+ from design_graph.extraction.chunker import chunk_extracted_data, export_chunks_jsonl
393
+ from design_graph.parsing.format_detector import PLAIN_HTML
394
+ from design_graph.parsing.source_loader import load
395
+ from design_graph.pipeline.coordinator import (
396
+ EXTRACTION_CONCURRENCY,
397
+ _has_react_functions,
398
+ extract_plain_html,
399
+ extract_react,
400
+ )
401
+
402
+ sources = await load(parsed.html_path)
403
+
404
+ if sources.format == PLAIN_HTML and not _has_react_functions(sources.js):
405
+ comps, screens, sections_map, _tokens, _module_texts = await extract_plain_html(
406
+ sources, concurrency=EXTRACTION_CONCURRENCY
407
+ )
408
+ else:
409
+ comps, screens, sections_map, _tokens, _module_texts = await extract_react(
410
+ sources, concurrency=EXTRACTION_CONCURRENCY
411
+ )
412
+ # chunk export has no graph to resolve {[icon:id]} markers against —
413
+ # unlike GraphReader, it reads straight off this in-memory extraction
414
+ # pass — so markers are expanded here from the icons these same
415
+ # components just produced, before anything is chunked.
416
+ markup_by_icon_id = {icon.id: icon.markup for c in comps for icon in c.icons}
417
+ for c in comps:
418
+ c.jsx_snippet = resolve_icon_markers(c.jsx_snippet, markup_by_icon_id)
419
+ comps_d = {c.name: c for c in comps}
420
+
421
+ chunks = chunk_extracted_data(screens, sections_map, comps_d, parsed.max_chars)
422
+ export_chunks_jsonl(chunks, parsed.output_path)
423
+ return len(chunks)
424
+
425
+
426
+ def _run_validate(argv: list[str]) -> None:
427
+ import json as _json
428
+ from design_graph.cli.validate import render_validation_report, validate_graph
429
+
430
+ try:
431
+ parsed = parse_validate_args(argv)
432
+ except SystemExit:
433
+ raise
434
+
435
+ configure_cli_logging(verbose=parsed.verbose)
436
+
437
+ db_path = _select_database(parsed.db_path, parsed.document)
438
+ report = validate_graph(db_path)
439
+
440
+ if parsed.json_output:
441
+ print(_json.dumps(report.to_dict(), indent=2))
442
+ else:
443
+ print(render_validation_report(report))
444
+
445
+ if report.status == "errors":
446
+ sys.exit(1)
447
+
448
+
449
+ def _run_status(argv: list[str]) -> None:
450
+ from design_graph.cli.status import collect_graph_status, render_status_report
451
+
452
+ try:
453
+ parsed = parse_status_args(argv)
454
+ except SystemExit:
455
+ raise
456
+
457
+ configure_cli_logging(verbose=parsed.verbose)
458
+
459
+ from design_graph.workspace import GraphWorkspace
460
+ workspace = GraphWorkspace.open()
461
+ if workspace.catalog.databases or parsed.db_path or parsed.document:
462
+ selected = _select_graph(parsed.db_path, parsed.document)
463
+ db_path = selected.database.path
464
+ selection_source = selected.source.value
465
+ else:
466
+ db_path = workspace.catalog.directory / "design-graph.db"
467
+ selection_source = "empty workspace"
468
+ from design_graph.pipeline.state import BuildStateRepository
469
+ state_path = BuildStateRepository.for_database(db_path).path
470
+
471
+ report = collect_graph_status(
472
+ db_path=db_path, state_path=state_path,
473
+ selection_source=selection_source,
474
+ )
475
+ print(render_status_report(report))
476
+
477
+
478
+ def _run_report(argv: list[str]) -> None:
479
+ try:
480
+ parsed = parse_report_args(argv)
481
+ except SystemExit:
482
+ raise
483
+
484
+ configure_cli_logging(verbose=parsed.verbose)
485
+
486
+ db_path = _select_database(parsed.db_path, parsed.document)
487
+ prototype_name = parsed.prototype_name or db_path.stem
488
+
489
+ report = _build_report_from_graph(db_path, prototype_name, parsed)
490
+ _emit_report(report, parsed.output_path)
491
+
492
+
493
+ def _build_report_from_graph(
494
+ db_path: Path,
495
+ prototype_name: str,
496
+ parsed: ReportCliArgs,
497
+ ):
498
+ """Open db_path read-only and build a PrototypeReport. All graph imports are local (G9)."""
499
+ import kuzu
500
+
501
+ from design_graph.cli.report import ReportConfig, build_prototype_report
502
+ from design_graph.graph.reader import GraphReader
503
+
504
+ config = ReportConfig(
505
+ prototype_name=prototype_name,
506
+ include_tokens=parsed.include_tokens,
507
+ include_jsx=parsed.include_jsx,
508
+ )
509
+ db = kuzu.Database(str(db_path), read_only=True)
510
+ conn = kuzu.Connection(db)
511
+ reader = GraphReader(conn)
512
+ return build_prototype_report(reader, config)
513
+
514
+
515
+ def _emit_report(report, output_path: Path | None) -> None:
516
+ """Render report to Markdown and write to output_path or print to stdout."""
517
+ from design_graph.cli.report import render_markdown_report
518
+
519
+ md = render_markdown_report(report)
520
+
521
+ if output_path:
522
+ output_path.write_text(md, encoding="utf-8")
523
+ print(f"Report written to {output_path}")
524
+ else:
525
+ print(md)
526
+
527
+
528
+ def _auto_detect_db() -> Path:
529
+ """Compatibility entry point with deterministic, ambiguity-safe selection."""
530
+ from design_graph.workspace import GraphWorkspace
531
+ workspace = GraphWorkspace.open()
532
+ if not workspace.catalog.databases:
533
+ return workspace.catalog.directory / "design-graph.db"
534
+ return _select_database(None, None)
535
+
536
+
537
+ def _select_database(db_path: Path | None, document: str | None) -> Path:
538
+ return _select_graph(db_path, document).database.path
539
+
540
+
541
+ def _select_graph(db_path: Path | None, document: str | None):
542
+ from design_graph.core.graph_catalog import GraphCatalogError
543
+ from design_graph.workspace import GraphWorkspace
544
+ try:
545
+ return GraphWorkspace.open().select(db_path=db_path, document=document)
546
+ except GraphCatalogError as exc:
547
+ print(f"error: {exc}", file=sys.stderr)
548
+ raise SystemExit(2) from exc
549
+
550
+
551
+ def _run_database(argv: list[str]) -> None:
552
+ from design_graph.cli.databases import run_database_command
553
+ exit_code = run_database_command(parse_database_args(argv))
554
+ if exit_code:
555
+ raise SystemExit(exit_code)
556
+
557
+
558
+ def _run_init(argv: list[str]) -> None:
559
+ from design_graph.cli.init import run_init_command
560
+ exit_code = run_init_command(parse_init_args(argv))
561
+ if exit_code:
562
+ raise SystemExit(exit_code)
563
+
564
+
565
+ # ── Output formatting ─────────────────────────────────────────────────────────
566
+
567
+ def _print_build_summary(html_path: Path, db_path: Path, stats) -> None:
568
+ w = 55
569
+ print(f"\n{'─' * w}")
570
+ print(f" Prototype : {html_path.name}")
571
+ print(f" Graph DB : {db_path}")
572
+ print(f"{'─' * w}")
573
+ print(f" Screens: {stats.screens:>4} Sections: {stats.sections:>4}")
574
+ print(f" Components: {stats.components:>4} Tokens: {stats.tokens:>4}")
575
+ print(
576
+ f" Extracted: {stats.extracted_components:>4} "
577
+ f"Unresolved: {stats.unresolved_components:>4}"
578
+ )
579
+ print(f" UITexts: {stats.texts:>4} Styles: {stats.styles:>4}")
580
+ print(f" Icons: {stats.icons:>4}")
581
+ print(f" Interactions: {stats.interactions:>4} CONTAINS: {stats.contains_rels:>4}")
582
+ print(f" Props: {stats.component_props:>4} SecStyles: {stats.section_styles:>4}")
583
+ if stats.write_errors:
584
+ print(f" ⚠ Write errors: {stats.write_errors} (rerun with --verbose for details)")
585
+ print(f" Built in {stats.duration_seconds:.2f}s")
586
+ print(f"{'─' * w}")