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.
- design_graph/__init__.py +0 -0
- design_graph/cli/__init__.py +0 -0
- design_graph/cli/_logging.py +36 -0
- design_graph/cli/build.py +586 -0
- design_graph/cli/databases.py +181 -0
- design_graph/cli/init.py +295 -0
- design_graph/cli/query.py +235 -0
- design_graph/cli/report.py +224 -0
- design_graph/cli/status.py +230 -0
- design_graph/cli/validate.py +326 -0
- design_graph/core/__init__.py +0 -0
- design_graph/core/constants.py +205 -0
- design_graph/core/graph_catalog.py +280 -0
- design_graph/core/models.py +786 -0
- design_graph/core/patterns.py +367 -0
- design_graph/extraction/__init__.py +0 -0
- design_graph/extraction/alias_extractor.py +79 -0
- design_graph/extraction/chunker.py +247 -0
- design_graph/extraction/component_extractor.py +639 -0
- design_graph/extraction/icon_extractor.py +81 -0
- design_graph/extraction/jsx_sanitizer.py +174 -0
- design_graph/extraction/module_data_extractor.py +95 -0
- design_graph/extraction/module_text_extractor.py +113 -0
- design_graph/extraction/plain_html_component_extractor.py +164 -0
- design_graph/extraction/prop_extractor.py +137 -0
- design_graph/extraction/screen_extractor.py +187 -0
- design_graph/extraction/section_extractor.py +625 -0
- design_graph/extraction/visual_function.py +23 -0
- design_graph/graph/__init__.py +0 -0
- design_graph/graph/diff.py +50 -0
- design_graph/graph/reader.py +1602 -0
- design_graph/graph/schema.py +210 -0
- design_graph/graph/writer.py +713 -0
- design_graph/mcp/__init__.py +0 -0
- design_graph/mcp/aliases.py +67 -0
- design_graph/mcp/metrics.py +222 -0
- design_graph/mcp/search.py +214 -0
- design_graph/mcp/server.py +352 -0
- design_graph/mcp/tools.py +1942 -0
- design_graph/parsing/__init__.py +0 -0
- design_graph/parsing/css_class_resolver.py +666 -0
- design_graph/parsing/format_detector.py +64 -0
- design_graph/parsing/html_parser.py +230 -0
- design_graph/parsing/js_parser.py +700 -0
- design_graph/parsing/palette_extractor.py +113 -0
- design_graph/parsing/source_loader.py +203 -0
- design_graph/parsing/token_extractor.py +426 -0
- design_graph/paths.py +67 -0
- design_graph/pipeline/__init__.py +0 -0
- design_graph/pipeline/build_progress.py +223 -0
- design_graph/pipeline/coordinator.py +440 -0
- design_graph/pipeline/state.py +199 -0
- design_graph/resources/skills/design-graph-ui-context/SKILL.md +124 -0
- design_graph/workspace.py +113 -0
- design_graph-0.34.0.dist-info/METADATA +575 -0
- design_graph-0.34.0.dist-info/RECORD +59 -0
- design_graph-0.34.0.dist-info/WHEEL +5 -0
- design_graph-0.34.0.dist-info/entry_points.txt +4 -0
- design_graph-0.34.0.dist-info/top_level.txt +1 -0
design_graph/__init__.py
ADDED
|
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}")
|