MertCapkin-GraphCraft 0.1.1__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 (49) hide show
  1. graphcraft/__init__.py +8 -0
  2. graphcraft/__main__.py +4 -0
  3. graphcraft/assets/.cursor/commands/graphcraft.md +24 -0
  4. graphcraft/assets/.cursor/rules/graphcraft.mdc +106 -0
  5. graphcraft/assets/.cursor/skills/design-strategist/DESIGN_STRATEGIST.md +23 -0
  6. graphcraft/assets/.cursor/skills/designer/DESIGNER.md +26 -0
  7. graphcraft/assets/.cursor/skills/mobile-app/MOBILE_APP.md +15 -0
  8. graphcraft/assets/.cursor/skills/mobile-game/MOBILE_GAME.md +19 -0
  9. graphcraft/assets/.cursor/skills/stitch-import/STITCH_IMPORT.md +25 -0
  10. graphcraft/assets/.cursor/skills/visual-review/VISUAL_REVIEW.md +20 -0
  11. graphcraft/assets/.graphcraft-assets-version +1 -0
  12. graphcraft/assets/.stitch/metadata.template.json +20 -0
  13. graphcraft/assets/design/screens/login.example.yaml +14 -0
  14. graphcraft/assets/design-system/components/button.example.yaml +20 -0
  15. graphcraft/assets/design-system/tokens.base.json +27 -0
  16. graphcraft/assets/design-system/tokens.json +27 -0
  17. graphcraft/assets/graphcraft.config.yaml +47 -0
  18. graphcraft/assets/handoff/AESTHETIC_BRIEF.md +52 -0
  19. graphcraft/assets/handoff/DESIGN_BRIEF.md +47 -0
  20. graphcraft/assets/orchestrator/GRAPHCRAFT.md +87 -0
  21. graphcraft/assets/packages/assets/README.md +22 -0
  22. graphcraft/assets/packages/ui-core/README.md +23 -0
  23. graphcraft/assets/packs/mobile-app/STACKS.md +28 -0
  24. graphcraft/assets/packs/mobile-game/STACKS.md +30 -0
  25. graphcraft/assets/packs/stitch/README.md +20 -0
  26. graphcraft/assets/packs/styles/minimal-dark/style.yaml +18 -0
  27. graphcraft/bootstrap.py +68 -0
  28. graphcraft/cli.py +63 -0
  29. graphcraft/constants.py +21 -0
  30. graphcraft/design_graph/__init__.py +1 -0
  31. graphcraft/design_graph/builder.py +218 -0
  32. graphcraft/design_graph/cli.py +83 -0
  33. graphcraft/design_graph/harmony.py +47 -0
  34. graphcraft/design_graph/query.py +81 -0
  35. graphcraft/design_graph/report.py +36 -0
  36. graphcraft/design_graph/schema.py +69 -0
  37. graphcraft/design_graph/stitch_adapter.py +83 -0
  38. graphcraft/doctor.py +104 -0
  39. graphcraft/init_cmd.py +78 -0
  40. graphcraft/installer.py +204 -0
  41. graphcraft/stitch/__init__.py +1 -0
  42. graphcraft/stitch/cli.py +3 -0
  43. graphcraft/stitch/import_cmd.py +68 -0
  44. mertcapkin_graphcraft-0.1.1.dist-info/METADATA +237 -0
  45. mertcapkin_graphcraft-0.1.1.dist-info/RECORD +49 -0
  46. mertcapkin_graphcraft-0.1.1.dist-info/WHEEL +5 -0
  47. mertcapkin_graphcraft-0.1.1.dist-info/entry_points.txt +2 -0
  48. mertcapkin_graphcraft-0.1.1.dist-info/licenses/LICENSE +21 -0
  49. mertcapkin_graphcraft-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,28 @@
1
+ # Mobile App Pack
2
+
3
+ `profile: mobile-app` in `graphcraft.config.yaml`
4
+
5
+ ## Supported stacks
6
+
7
+ | Stack | UI lib path | Notes |
8
+ |-------|-------------|-------|
9
+ | react-native | packages/ui-core/rn/ | StyleSheet + tokens |
10
+ | expo | packages/ui-core/expo/ | Expo Router screens |
11
+ | flutter | packages/ui-core/flutter/ | ThemeData from tokens.json |
12
+ | swiftui | packages/ui-core/swiftui/ | Asset catalog + SwiftUI Theme |
13
+ | jetpack-compose | packages/ui-core/compose/ | Material3 + custom tokens |
14
+ | kotlin-multiplatform | packages/ui-core/kmp/ | Shared theme module |
15
+ | ionic | packages/ui-core/ionic/ | CSS variables from tokens |
16
+ | capacitor | packages/ui-core/capacitor/ | Web + native shell |
17
+
18
+ ## Builder rules
19
+
20
+ - Consume tokens from `design-system/tokens.json`
21
+ - Map design graph `component:*` to package components
22
+ - Navigation follows `navigates_to` edges in design graph
23
+
24
+ ## Design graph query
25
+
26
+ ```bash
27
+ python -m graphcraft design query "screens"
28
+ ```
@@ -0,0 +1,30 @@
1
+ # Mobile Game Pack
2
+
3
+ `profile: mobile-game` in `graphcraft.config.yaml`
4
+
5
+ ## Supported stacks
6
+
7
+ | Stack | Scope | Notes |
8
+ |-------|-------|-------|
9
+ | unity-ugui | Meta-UI | Canvas, RectTransform, ScriptableObject themes |
10
+ | unity-ui-toolkit | Meta-UI | USS/UXML from tokens |
11
+ | godot | Meta-UI | Control nodes + theme resource |
12
+ | unreal-umg | Meta-UI | Widget blueprints + styles |
13
+ | defold | Meta-UI | GUI scenes |
14
+ | cocos | Meta-UI | Prefab UI |
15
+
16
+ ## Scope limit
17
+
18
+ Stitch/design graph targets **meta-UI**: menus, HUD overlays, shop, inventory, dialogs.
19
+
20
+ Gameplay canvas art is out of GraphCraft v0.1 scope.
21
+
22
+ ## Builder rules
23
+
24
+ - `stitch.scope: meta-ui-only` recommended
25
+ - Visual reference mode: PNG from `.stitch/designs/`
26
+ - Token → USS / ScriptableObject theme pipeline
27
+
28
+ ## Asset library
29
+
30
+ Place sprites in `packages/assets/` with manifest YAML linking to components.
@@ -0,0 +1,20 @@
1
+ # Stitch Pack
2
+
3
+ Optional prototype import from [Google Stitch](https://stitch.withgoogle.com/).
4
+
5
+ ## Setup
6
+
7
+ 1. `design_source: stitch` or `hybrid` in graphcraft.config.yaml
8
+ 2. Export Stitch project → `.stitch/`
9
+ 3. `graphcraft stitch import .`
10
+
11
+ ## Files
12
+
13
+ - `.stitch/DESIGN.md` — Stitch design system (official format)
14
+ - `.stitch/metadata.json` — screen map + flows
15
+ - `.stitch/designs/*.png` — visual ground truth
16
+
17
+ ## Docs
18
+
19
+ - [DESIGN.md spec](https://github.com/google-labs-code/design.md)
20
+ - Stitch MCP: `@keeponfirst/kof-stitch-mcp`
@@ -0,0 +1,18 @@
1
+ id: style:minimal-dark
2
+ label: Minimal Dark
3
+ mood:
4
+ - calm
5
+ - premium
6
+ - readable
7
+ priority: balanced
8
+ tokens:
9
+ extends: design-system/tokens.json
10
+ harmony:
11
+ contrast_min: 4.5
12
+ max_accent_colors: 2
13
+ components:
14
+ preferred:
15
+ - component:button-primary
16
+ discouraged: []
17
+ assets:
18
+ icon_set: assets:icons-minimal
@@ -0,0 +1,68 @@
1
+ """Ensure GraphStack + Graphify are available for graphcraft init."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+
8
+ PIP_SPEC = "MertCapkin_GraphStack[graphify]>=4.7,<5"
9
+ PIP_SPEC_GIT = (
10
+ "MertCapkin_GraphStack[graphify] @ git+https://github.com/MertCapkin/GraphStack.git"
11
+ )
12
+
13
+
14
+ def _echo(msg: str) -> None:
15
+ print(msg, flush=True)
16
+
17
+
18
+ def find_python() -> list[str]:
19
+ return [sys.executable]
20
+
21
+
22
+ def graphstack_available() -> bool:
23
+ try:
24
+ import graphstack # noqa: F401
25
+ return True
26
+ except ImportError:
27
+ pass
28
+ proc = subprocess.run(
29
+ [*find_python(), "-m", "graphstack", "--version"],
30
+ capture_output=True,
31
+ text=True,
32
+ check=False,
33
+ )
34
+ return proc.returncode == 0
35
+
36
+
37
+ def pip_install(*specs: str, quiet: bool = True) -> int:
38
+ if not specs:
39
+ return 0
40
+ cmd = [*find_python(), "-m", "pip", "install", "--upgrade"]
41
+ if quiet:
42
+ cmd.append("--quiet")
43
+ cmd.extend(specs)
44
+ _echo(f" pip install {' '.join(specs)}")
45
+ return subprocess.run(cmd, check=False).returncode
46
+
47
+
48
+ def ensure_graphstack(*, install: bool = True) -> bool:
49
+ if graphstack_available():
50
+ return True
51
+ if not install:
52
+ return False
53
+ _echo("")
54
+ _echo("Installing GraphStack (MertCapkin_GraphStack[graphify])...")
55
+ rc = pip_install(PIP_SPEC)
56
+ if rc != 0:
57
+ _echo("PyPI install failed — trying GitHub...")
58
+ rc = pip_install(PIP_SPEC_GIT)
59
+ return rc == 0 and graphstack_available()
60
+
61
+
62
+ def run_graphstack_init(target: str, *, non_interactive: bool, install_deps: bool) -> int:
63
+ cmd = [*find_python(), "-m", "graphstack", "init", target]
64
+ if non_interactive:
65
+ cmd.append("-y")
66
+ if install_deps:
67
+ cmd.append("--install-deps")
68
+ return subprocess.run(cmd, check=False).returncode
graphcraft/cli.py ADDED
@@ -0,0 +1,63 @@
1
+ """GraphCraft CLI dispatcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import subprocess
7
+ import sys
8
+
9
+ from . import __version__
10
+
11
+
12
+ def _build_parser() -> argparse.ArgumentParser:
13
+ parser = argparse.ArgumentParser(
14
+ prog="graphcraft",
15
+ description="GraphCraft — mobile design layer on GraphStack.",
16
+ )
17
+ parser.add_argument("--version", action="version", version=f"graphcraft {__version__}")
18
+ sub = parser.add_subparsers(dest="command", required=True)
19
+ sub.add_parser("install", help="Install GraphCraft overlay into a project", add_help=False)
20
+ sub.add_parser("init", help="GraphStack init + GraphCraft overlay", add_help=False)
21
+ sub.add_parser("doctor", help="GraphCraft + GraphStack health check", add_help=False)
22
+ sub.add_parser("design", help="Design graph commands", add_help=False)
23
+ sub.add_parser("stitch", help="Stitch import commands", add_help=False)
24
+ return parser
25
+
26
+
27
+ def _delegate_graphstack(rest: list[str]) -> int:
28
+ cmd = [sys.executable, "-m", "graphstack", *rest]
29
+ return subprocess.run(cmd, check=False).returncode
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ args = sys.argv[1:] if argv is None else argv
34
+ if not args or args[0] in ("-h", "--help"):
35
+ _build_parser().print_help()
36
+ return 0
37
+ if args[0] == "--version":
38
+ print(f"graphcraft {__version__}")
39
+ return 0
40
+
41
+ cmd, rest = args[0], args[1:]
42
+
43
+ if cmd == "install":
44
+ from .installer import run as install_run
45
+ return install_run(rest)
46
+ if cmd == "init":
47
+ from .init_cmd import run as init_run
48
+ return init_run(rest)
49
+ if cmd == "doctor":
50
+ from .doctor import run as doctor_run
51
+ return doctor_run(rest)
52
+ if cmd == "design":
53
+ from .design_graph.cli import run as design_run
54
+ return design_run(rest)
55
+ if cmd == "stitch":
56
+ from .stitch.cli import run as stitch_run
57
+ return stitch_run(rest)
58
+
59
+ return _delegate_graphstack([cmd, *rest])
60
+
61
+
62
+ if __name__ == "__main__":
63
+ raise SystemExit(main())
@@ -0,0 +1,21 @@
1
+ """GraphCraft path constants."""
2
+
3
+ from pathlib import Path
4
+
5
+ GRAPHCRAFT_OUT = Path("graphcraft-out")
6
+ DESIGN_GRAPH_JSON = GRAPHCRAFT_OUT / "design-graph.json"
7
+ DESIGN_REPORT = GRAPHCRAFT_OUT / "DESIGN_REPORT.md"
8
+ BRIDGE_JSON = GRAPHCRAFT_OUT / "bridge.json"
9
+
10
+ DESIGN_SYSTEM_DIR = Path("design-system")
11
+ DESIGN_SCREENS_DIR = Path("design") / "screens"
12
+ DESIGN_COMPONENTS_DIR = DESIGN_SYSTEM_DIR / "components"
13
+ STYLES_DIR = Path("packs") / "styles"
14
+ STITCH_DIR = Path(".stitch")
15
+ CONFIG_FILE = Path("graphcraft.config.yaml")
16
+
17
+ HANDOFF_AESTHETIC = Path("handoff") / "AESTHETIC_BRIEF.md"
18
+ HANDOFF_DESIGN = Path("handoff") / "DESIGN_BRIEF.md"
19
+
20
+ PACKAGES_UI = Path("packages") / "ui-core"
21
+ PACKAGES_ASSETS = Path("packages") / "assets"
@@ -0,0 +1 @@
1
+ """Design graph package."""
@@ -0,0 +1,218 @@
1
+ """Build design-graph.json from declarative design sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ try:
10
+ import yaml
11
+ except ImportError:
12
+ yaml = None # type: ignore
13
+
14
+ from ..constants import (
15
+ DESIGN_COMPONENTS_DIR,
16
+ DESIGN_SCREENS_DIR,
17
+ DESIGN_SYSTEM_DIR,
18
+ STITCH_DIR,
19
+ STYLES_DIR,
20
+ )
21
+ from .schema import empty_graph, make_edge, make_node
22
+ from .stitch_adapter import ingest_stitch
23
+ from .report import write_design_report
24
+
25
+
26
+ def _load_yaml(path: Path) -> dict[str, Any]:
27
+ if yaml is None:
28
+ raise RuntimeError("PyYAML required: pip install pyyaml")
29
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
30
+ return data if isinstance(data, dict) else {}
31
+
32
+
33
+ def _parse_tokens(tokens_path: Path, graph: dict[str, Any]) -> None:
34
+ if not tokens_path.is_file():
35
+ return
36
+ data = json.loads(tokens_path.read_text(encoding="utf-8"))
37
+
38
+ def walk(obj: Any, prefix: str = "") -> None:
39
+ if not isinstance(obj, dict):
40
+ return
41
+ if "$value" in obj or "value" in obj:
42
+ tid = f"token:{prefix}"
43
+ val = obj.get("$value") or obj.get("value")
44
+ graph["nodes"].append(
45
+ make_node(
46
+ tid,
47
+ ntype="token",
48
+ label=prefix,
49
+ source=str(tokens_path),
50
+ origin="extracted",
51
+ extra={"value": val, "token_type": obj.get("$type", "unknown")},
52
+ )
53
+ )
54
+ return
55
+ for key, child in obj.items():
56
+ if key.startswith("$"):
57
+ continue
58
+ child_prefix = f"{prefix}.{key}" if prefix else key
59
+ walk(child, child_prefix)
60
+
61
+ walk(data)
62
+
63
+
64
+ def _parse_component(path: Path, graph: dict[str, Any]) -> None:
65
+ data = _load_yaml(path)
66
+ cid = data.get("id") or f"component:{path.stem}"
67
+ graph["nodes"].append(
68
+ make_node(
69
+ str(cid),
70
+ ntype="component",
71
+ label=data.get("label", path.stem),
72
+ source=str(path),
73
+ origin="designed",
74
+ extra={
75
+ "collection": data.get("collection"),
76
+ "when_to_use": data.get("when_to_use"),
77
+ "when_not_to_use": data.get("when_not_to_use"),
78
+ },
79
+ )
80
+ )
81
+ for tok in data.get("uses_tokens") or []:
82
+ tid = tok if str(tok).startswith("token:") else f"token:{tok}"
83
+ graph["edges"].append(make_edge(str(cid), tid, "uses_token"))
84
+ for alt in data.get("alternatives") or []:
85
+ graph["edges"].append(make_edge(str(cid), str(alt), "alternative"))
86
+ for style in data.get("style_compatibility") or []:
87
+ sid = style if str(style).startswith("style:") else f"style:{style}"
88
+ graph["edges"].append(make_edge(str(cid), sid, "style_compatible"))
89
+ for other, score in (data.get("harmony_score_with") or {}).items():
90
+ etype = "harmonizes_with" if float(score) >= 0.5 else "clashes_with"
91
+ graph["edges"].append(
92
+ make_edge(str(cid), str(other), etype, extra={"score": float(score)})
93
+ )
94
+
95
+
96
+ def _parse_screen(path: Path, graph: dict[str, Any]) -> None:
97
+ data = _load_yaml(path)
98
+ sid = data.get("id") or f"screen:{path.stem}"
99
+ graph["nodes"].append(
100
+ make_node(
101
+ str(sid),
102
+ ntype="screen",
103
+ label=data.get("title", path.stem),
104
+ source=str(path),
105
+ origin="designed",
106
+ extra={"platform": data.get("platform"), "status": data.get("status", "draft")},
107
+ )
108
+ )
109
+ for comp in data.get("components") or []:
110
+ cid = comp if str(comp).startswith("component:") else f"component:{comp}"
111
+ graph["edges"].append(make_edge(str(sid), cid, "uses_component"))
112
+ for tok in data.get("tokens") or []:
113
+ tid = tok if str(tok).startswith("token:") else f"token:{tok}"
114
+ graph["edges"].append(make_edge(str(sid), tid, "uses_token"))
115
+ nav = data.get("navigation") or {}
116
+ if isinstance(nav, dict):
117
+ for _action, target in nav.items():
118
+ tid = target if str(target).startswith("screen:") else f"screen:{target}"
119
+ graph["edges"].append(make_edge(str(sid), tid, "navigates_to"))
120
+ impl = data.get("implements")
121
+ if impl:
122
+ graph["edges"].append(
123
+ make_edge(str(sid), str(impl), "implements", origin="bridge")
124
+ )
125
+
126
+
127
+ def _parse_style(path: Path, graph: dict[str, Any]) -> None:
128
+ data = _load_yaml(path)
129
+ sid = data.get("id") or f"style:{path.parent.name}"
130
+ graph["nodes"].append(
131
+ make_node(
132
+ str(sid),
133
+ ntype="style",
134
+ label=data.get("label", path.parent.name),
135
+ source=str(path),
136
+ origin="designed",
137
+ extra={"mood": data.get("mood"), "priority": data.get("priority")},
138
+ )
139
+ )
140
+ for comp in data.get("components", {}).get("preferred") or []:
141
+ graph["edges"].append(make_edge(str(sid), str(comp), "style_compatible"))
142
+ asset_set = data.get("assets", {}).get("icon_set")
143
+ if asset_set:
144
+ aid = asset_set if str(asset_set).startswith("assets:") else f"assets:{asset_set}"
145
+ graph["edges"].append(make_edge(str(sid), aid, "uses_asset"))
146
+
147
+
148
+ def build_design_graph(root: Path) -> dict[str, Any]:
149
+ graph = empty_graph()
150
+ tokens = root / DESIGN_SYSTEM_DIR / "tokens.json"
151
+ if not tokens.is_file():
152
+ tokens = root / DESIGN_SYSTEM_DIR / "tokens.base.json"
153
+ _parse_tokens(tokens, graph)
154
+
155
+ components_dir = root / DESIGN_COMPONENTS_DIR
156
+ if components_dir.is_dir():
157
+ for path in sorted(components_dir.glob("*.yaml")):
158
+ _parse_component(path, graph)
159
+
160
+ screens_dir = root / DESIGN_SCREENS_DIR
161
+ if screens_dir.is_dir():
162
+ for path in sorted(screens_dir.glob("*.yaml")):
163
+ _parse_screen(path, graph)
164
+
165
+ styles_dir = root / STYLES_DIR
166
+ if styles_dir.is_dir():
167
+ for style_file in sorted(styles_dir.glob("*/style.yaml")):
168
+ _parse_style(style_file, graph)
169
+
170
+ stitch_dir = root / STITCH_DIR
171
+ if stitch_dir.is_dir():
172
+ ingest_stitch(stitch_dir, graph)
173
+
174
+ _materialize_missing_targets(graph)
175
+ return graph
176
+
177
+
178
+ def _materialize_missing_targets(graph: dict[str, Any]) -> None:
179
+ """Create placeholder nodes for referenced IDs not explicitly defined."""
180
+ nodes = graph.get("nodes") or []
181
+ edges = graph.get("edges") or []
182
+ known = {n["id"] for n in nodes if "id" in n}
183
+ prefix_type = {
184
+ "screen:": "screen",
185
+ "component:": "component",
186
+ "token:": "token",
187
+ "style:": "style",
188
+ "assets:": "asset",
189
+ }
190
+ for e in edges:
191
+ for end in (e.get("source"), e.get("target")):
192
+ if not end or end in known:
193
+ continue
194
+ ntype = "component"
195
+ for prefix, t in prefix_type.items():
196
+ if str(end).startswith(prefix):
197
+ ntype = t
198
+ break
199
+ graph["nodes"].append(
200
+ make_node(
201
+ str(end),
202
+ ntype=ntype,
203
+ label=str(end).split(":", 1)[-1],
204
+ source="",
205
+ origin="inferred",
206
+ )
207
+ )
208
+ known.add(str(end))
209
+
210
+
211
+ def update_design_graph(root: Path) -> dict[str, Any]:
212
+ graph = build_design_graph(root)
213
+ out_dir = root / "graphcraft-out"
214
+ out_dir.mkdir(parents=True, exist_ok=True)
215
+ out_path = out_dir / "design-graph.json"
216
+ out_path.write_text(json.dumps(graph, indent=2), encoding="utf-8")
217
+ write_design_report(graph, out_dir / "DESIGN_REPORT.md")
218
+ return graph
@@ -0,0 +1,83 @@
1
+ """Design graph CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from ..constants import DESIGN_GRAPH_JSON
11
+ from .builder import update_design_graph
12
+ from .harmony import run_harmony_check
13
+ from .query import load_graph, query, validate
14
+
15
+
16
+ def run(argv: list[str]) -> int:
17
+ if not argv or argv[0] in ("-h", "--help"):
18
+ print("Usage: graphcraft design <update|query|validate|harmony> ...")
19
+ return 0
20
+
21
+ cmd, rest = argv[0], argv[1:]
22
+
23
+ if cmd == "update":
24
+ p = argparse.ArgumentParser(prog="graphcraft design update")
25
+ p.add_argument("root", nargs="?", default=".")
26
+ args = p.parse_args(rest)
27
+ root = Path(args.root).resolve()
28
+ graph = update_design_graph(root)
29
+ print(f"Design graph: {len(graph.get('nodes', []))} nodes, {len(graph.get('edges', []))} edges")
30
+ print(f" -> {root / DESIGN_GRAPH_JSON}")
31
+ return 0
32
+
33
+ if cmd == "query":
34
+ p = argparse.ArgumentParser(prog="graphcraft design query")
35
+ p.add_argument("question", help="design graph question")
36
+ p.add_argument("--graph", default=str(DESIGN_GRAPH_JSON))
37
+ args = p.parse_args(rest)
38
+ path = Path(args.graph)
39
+ if not path.is_file():
40
+ print(f"Missing {path} — run: graphcraft design update .")
41
+ return 1
42
+ print(query(load_graph(path), args.question))
43
+ return 0
44
+
45
+ if cmd == "validate":
46
+ p = argparse.ArgumentParser(prog="graphcraft design validate")
47
+ p.add_argument("--graph", default=str(DESIGN_GRAPH_JSON))
48
+ args = p.parse_args(rest)
49
+ path = Path(args.graph)
50
+ if not path.is_file():
51
+ print(f"Missing {path}")
52
+ return 1
53
+ issues = validate(load_graph(path))
54
+ if issues:
55
+ for i in issues:
56
+ print(f" ISSUE: {i}")
57
+ return 1
58
+ print("Design graph validation: PASS")
59
+ return 0
60
+
61
+ if cmd == "harmony":
62
+ p = argparse.ArgumentParser(prog="graphcraft design harmony")
63
+ p.add_argument("--graph", default=str(DESIGN_GRAPH_JSON))
64
+ p.add_argument("--screen", default=None)
65
+ args = p.parse_args(rest)
66
+ path = Path(args.graph)
67
+ if not path.is_file():
68
+ print(f"Missing {path}")
69
+ return 1
70
+ result = run_harmony_check(load_graph(path), args.screen)
71
+ print(f"Harmony: {result['overall']}")
72
+ for w in result["warnings"]:
73
+ print(f" WARN: {w}")
74
+ for p_msg in result["passed"]:
75
+ print(f" OK: {p_msg}")
76
+ return 0 if result["overall"] == "PASS" else 1
77
+
78
+ print(f"Unknown design subcommand: {cmd}")
79
+ return 1
80
+
81
+
82
+ if __name__ == "__main__":
83
+ raise SystemExit(run(sys.argv[1:]))
@@ -0,0 +1,47 @@
1
+ """Harmony checks on design graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def run_harmony_check(graph: dict[str, Any], screen_id: str | None = None) -> dict[str, Any]:
9
+ nodes = {n["id"]: n for n in graph.get("nodes") or [] if "id" in n}
10
+ edges = graph.get("edges") or []
11
+
12
+ clashes: list[dict] = []
13
+ warnings: list[str] = []
14
+ passed: list[str] = []
15
+
16
+ for e in edges:
17
+ if e.get("type") == "clashes_with":
18
+ clashes.append(e)
19
+
20
+ screens = [
21
+ n for n in nodes.values() if n.get("type") == "screen" and (not screen_id or n["id"] == screen_id)
22
+ ]
23
+
24
+ for screen in screens:
25
+ sid = screen["id"]
26
+ comp_ids = [
27
+ e["target"]
28
+ for e in edges
29
+ if e.get("source") == sid and e.get("type") == "uses_component"
30
+ ]
31
+ for i, a in enumerate(comp_ids):
32
+ for b in comp_ids[i + 1 :]:
33
+ for e in edges:
34
+ if e.get("type") == "clashes_with" and (
35
+ (e.get("source") == a and e.get("target") == b)
36
+ or (e.get("source") == b and e.get("target") == a)
37
+ ):
38
+ warnings.append(f"CLASH on {sid}: {a} ↔ {b}")
39
+ if not warnings:
40
+ passed.append(f"{sid}: no component clashes detected")
41
+
42
+ return {
43
+ "overall": "FAIL" if warnings else "PASS",
44
+ "clashes_in_graph": len(clashes),
45
+ "warnings": warnings,
46
+ "passed": passed,
47
+ }
@@ -0,0 +1,81 @@
1
+ """Simple design graph queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def load_graph(path: Path) -> dict[str, Any]:
11
+ return json.loads(path.read_text(encoding="utf-8"))
12
+
13
+
14
+ def _index(graph: dict[str, Any]) -> tuple[dict[str, dict], list[dict]]:
15
+ nodes = {n["id"]: n for n in graph.get("nodes") or [] if "id" in n}
16
+ return nodes, list(graph.get("edges") or [])
17
+
18
+
19
+ def query(graph: dict[str, Any], question: str) -> str:
20
+ q = question.lower()
21
+ nodes, edges = _index(graph)
22
+
23
+ if "screen" in q:
24
+ screens = [n for n in nodes.values() if n.get("type") == "screen"]
25
+ lines = [f"Screens ({len(screens)}):"]
26
+ for s in screens:
27
+ comps = [
28
+ e["target"]
29
+ for e in edges
30
+ if e.get("source") == s["id"] and e.get("type") == "uses_component"
31
+ ]
32
+ lines.append(f" {s['id']}: {s.get('label')} → components: {', '.join(comps) or 'none'}")
33
+ return "\n".join(lines)
34
+
35
+ if "token" in q:
36
+ tokens = [n for n in nodes.values() if n.get("type") == "token"]
37
+ return f"Tokens: {len(tokens)} defined. Sample: " + ", ".join(
38
+ t["id"] for t in tokens[:8]
39
+ )
40
+
41
+ if "style" in q:
42
+ styles = [n["id"] for n in nodes.values() if n.get("type") == "style"]
43
+ return "Styles: " + (", ".join(styles) or "none")
44
+
45
+ if "harmony" in q or "clash" in q:
46
+ clashes = [e for e in edges if e.get("type") == "clashes_with"]
47
+ harmonizes = [e for e in edges if e.get("type") == "harmonizes_with"]
48
+ return (
49
+ f"Harmony edges: {len(harmonizes)} harmonizes_with, "
50
+ f"{len(clashes)} clashes_with"
51
+ )
52
+
53
+ return (
54
+ f"Design graph: {len(nodes)} nodes, {len(edges)} edges. "
55
+ "Try: 'screens', 'tokens', 'styles', 'harmony'"
56
+ )
57
+
58
+
59
+ def validate(graph: dict[str, Any]) -> list[str]:
60
+ issues: list[str] = []
61
+ nodes, edges = _index(graph)
62
+ node_ids = set(nodes)
63
+
64
+ for e in edges:
65
+ if e.get("source") not in node_ids:
66
+ issues.append(f"Edge source missing node: {e.get('source')}")
67
+ if e.get("target") not in node_ids:
68
+ issues.append(f"Edge target missing node: {e.get('target')}")
69
+
70
+ for n in nodes.values():
71
+ if n.get("type") == "screen":
72
+ sid = n["id"]
73
+ if n.get("_origin") == "inferred":
74
+ continue
75
+ has_comp = any(
76
+ e.get("source") == sid and e.get("type") == "uses_component" for e in edges
77
+ )
78
+ if not has_comp and n.get("_origin") != "stitch":
79
+ issues.append(f"Screen {sid} has no uses_component edges")
80
+
81
+ return issues