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.
- graphcraft/__init__.py +8 -0
- graphcraft/__main__.py +4 -0
- graphcraft/assets/.cursor/commands/graphcraft.md +24 -0
- graphcraft/assets/.cursor/rules/graphcraft.mdc +106 -0
- graphcraft/assets/.cursor/skills/design-strategist/DESIGN_STRATEGIST.md +23 -0
- graphcraft/assets/.cursor/skills/designer/DESIGNER.md +26 -0
- graphcraft/assets/.cursor/skills/mobile-app/MOBILE_APP.md +15 -0
- graphcraft/assets/.cursor/skills/mobile-game/MOBILE_GAME.md +19 -0
- graphcraft/assets/.cursor/skills/stitch-import/STITCH_IMPORT.md +25 -0
- graphcraft/assets/.cursor/skills/visual-review/VISUAL_REVIEW.md +20 -0
- graphcraft/assets/.graphcraft-assets-version +1 -0
- graphcraft/assets/.stitch/metadata.template.json +20 -0
- graphcraft/assets/design/screens/login.example.yaml +14 -0
- graphcraft/assets/design-system/components/button.example.yaml +20 -0
- graphcraft/assets/design-system/tokens.base.json +27 -0
- graphcraft/assets/design-system/tokens.json +27 -0
- graphcraft/assets/graphcraft.config.yaml +47 -0
- graphcraft/assets/handoff/AESTHETIC_BRIEF.md +52 -0
- graphcraft/assets/handoff/DESIGN_BRIEF.md +47 -0
- graphcraft/assets/orchestrator/GRAPHCRAFT.md +87 -0
- graphcraft/assets/packages/assets/README.md +22 -0
- graphcraft/assets/packages/ui-core/README.md +23 -0
- graphcraft/assets/packs/mobile-app/STACKS.md +28 -0
- graphcraft/assets/packs/mobile-game/STACKS.md +30 -0
- graphcraft/assets/packs/stitch/README.md +20 -0
- graphcraft/assets/packs/styles/minimal-dark/style.yaml +18 -0
- graphcraft/bootstrap.py +68 -0
- graphcraft/cli.py +63 -0
- graphcraft/constants.py +21 -0
- graphcraft/design_graph/__init__.py +1 -0
- graphcraft/design_graph/builder.py +218 -0
- graphcraft/design_graph/cli.py +83 -0
- graphcraft/design_graph/harmony.py +47 -0
- graphcraft/design_graph/query.py +81 -0
- graphcraft/design_graph/report.py +36 -0
- graphcraft/design_graph/schema.py +69 -0
- graphcraft/design_graph/stitch_adapter.py +83 -0
- graphcraft/doctor.py +104 -0
- graphcraft/init_cmd.py +78 -0
- graphcraft/installer.py +204 -0
- graphcraft/stitch/__init__.py +1 -0
- graphcraft/stitch/cli.py +3 -0
- graphcraft/stitch/import_cmd.py +68 -0
- mertcapkin_graphcraft-0.1.1.dist-info/METADATA +237 -0
- mertcapkin_graphcraft-0.1.1.dist-info/RECORD +49 -0
- mertcapkin_graphcraft-0.1.1.dist-info/WHEEL +5 -0
- mertcapkin_graphcraft-0.1.1.dist-info/entry_points.txt +2 -0
- mertcapkin_graphcraft-0.1.1.dist-info/licenses/LICENSE +21 -0
- 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
|
graphcraft/bootstrap.py
ADDED
|
@@ -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())
|
graphcraft/constants.py
ADDED
|
@@ -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
|