@hegemonart/get-design-done 1.51.0 → 1.52.0

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 (40) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +47 -0
  4. package/README.md +2 -0
  5. package/SKILL.md +2 -0
  6. package/agents/a11y-mapper.md +30 -1
  7. package/agents/component-taxonomy-mapper.md +30 -1
  8. package/agents/design-debt-crawler.md +60 -60
  9. package/agents/design-research-synthesizer.md +27 -1
  10. package/agents/motion-mapper.md +35 -13
  11. package/agents/token-mapper.md +30 -1
  12. package/agents/visual-hierarchy-mapper.md +30 -1
  13. package/dist/claude-code/.claude/skills/context/SKILL.md +137 -0
  14. package/dist/claude-code/.claude/skills/migrate-context/SKILL.md +123 -0
  15. package/dist/claude-code/.claude/skills/progress/SKILL.md +4 -0
  16. package/package.json +3 -2
  17. package/reference/design-context-schema.md +159 -0
  18. package/reference/design-context-tag-vocab.md +82 -0
  19. package/reference/registry.json +14 -0
  20. package/reference/schemas/design-context.schema.json +130 -0
  21. package/reference/schemas/mcp-gdd-tools.schema.json +34 -1
  22. package/reference/skill-graph.md +3 -1
  23. package/scripts/lib/design-context/extract-a11y.mjs +188 -0
  24. package/scripts/lib/design-context/extract-components.mjs +243 -0
  25. package/scripts/lib/design-context/extract-motion.mjs +248 -0
  26. package/scripts/lib/design-context/extract-tokens.mjs +234 -0
  27. package/scripts/lib/design-context/extract-visual-hierarchy.mjs +178 -0
  28. package/scripts/lib/design-context/integration-map.mjs +251 -0
  29. package/scripts/lib/design-context/merge-fragments.mjs +227 -0
  30. package/scripts/lib/design-context-query.cjs +0 -0
  31. package/scripts/lib/manifest/skills.json +16 -0
  32. package/scripts/lib/mcp-tools-lint/index.cjs +3 -1
  33. package/sdk/mcp/gdd-mcp/schemas/gdd_context_query.schema.json +60 -0
  34. package/sdk/mcp/gdd-mcp/server.js +474 -158
  35. package/sdk/mcp/gdd-mcp/server.ts +9 -5
  36. package/sdk/mcp/gdd-mcp/tools/gdd_context_query.ts +35 -0
  37. package/sdk/mcp/gdd-mcp/tools/index.ts +18 -13
  38. package/skills/context/SKILL.md +137 -0
  39. package/skills/migrate-context/SKILL.md +123 -0
  40. package/skills/progress/SKILL.md +4 -0
@@ -0,0 +1,130 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/hegemonart/get-design-done/reference/schemas/design-context.schema.json",
4
+ "title": "Design Context Graph",
5
+ "description": "The canonical typed knowledge graph of a design system, persisted at .design/context-graph.json. Nodes are design entities (tokens, components, screens, patterns); edges are typed relationships between them (uses-token, composes, transitions-to). Built by a two-phase mapper: a deterministic extract pass emits node/edge skeletons, then an LLM summary pass fills each node summary. Validated structurally by scripts/validate-design-context.cjs and queried by scripts/lib/design-context-query.cjs.",
6
+ "type": "object",
7
+ "required": ["schema_version", "nodes", "edges"],
8
+ "properties": {
9
+ "schema_version": {
10
+ "type": "string",
11
+ "minLength": 1,
12
+ "description": "Schema version of this graph document (e.g. \"52.0\")."
13
+ },
14
+ "generated_at": {
15
+ "type": "string",
16
+ "format": "date-time",
17
+ "description": "ISO-8601 timestamp the graph was last assembled (optional)."
18
+ },
19
+ "nodes": {
20
+ "type": "array",
21
+ "description": "All design entities in the graph. Node ids must be unique.",
22
+ "items": { "$ref": "#/definitions/node" }
23
+ },
24
+ "edges": {
25
+ "type": "array",
26
+ "description": "All typed relationships. Every source/target must resolve to a node id.",
27
+ "items": { "$ref": "#/definitions/edge" }
28
+ }
29
+ },
30
+ "additionalProperties": true,
31
+ "definitions": {
32
+ "node": {
33
+ "type": "object",
34
+ "required": ["id", "type", "name", "summary", "complexity"],
35
+ "properties": {
36
+ "id": {
37
+ "type": "string",
38
+ "minLength": 1,
39
+ "description": "Stable unique identifier for the node (referenced by edge source/target)."
40
+ },
41
+ "type": {
42
+ "type": "string",
43
+ "description": "The kind of design entity this node represents.",
44
+ "enum": [
45
+ "token",
46
+ "component",
47
+ "variant",
48
+ "state",
49
+ "motion-fragment",
50
+ "a11y-pattern",
51
+ "screen",
52
+ "layer",
53
+ "pattern",
54
+ "anti-pattern"
55
+ ]
56
+ },
57
+ "name": {
58
+ "type": "string",
59
+ "minLength": 1,
60
+ "description": "Human-readable name of the entity."
61
+ },
62
+ "summary": {
63
+ "type": "string",
64
+ "description": "One-line LLM-authored description of what the entity is and does. A stub summary (empty or identical to name) is flagged by the validator as a soft warning."
65
+ },
66
+ "tags": {
67
+ "type": "array",
68
+ "description": "Controlled-vocabulary tags grouping the node by concern (see reference/design-context-tag-vocab.md). Unknown tags are a soft warning, not a hard error.",
69
+ "items": { "type": "string" }
70
+ },
71
+ "complexity": {
72
+ "type": "string",
73
+ "description": "Coarse complexity bucket for the entity.",
74
+ "enum": ["simple", "moderate", "complex"]
75
+ },
76
+ "subtype": {
77
+ "type": "string",
78
+ "description": "Optional finer classification. For token nodes one of color/spacing/typography/radius/shadow; for layer nodes one of Atomic/Molecular/Organism/Template. Free-form for other node types."
79
+ }
80
+ },
81
+ "additionalProperties": true
82
+ },
83
+ "edge": {
84
+ "type": "object",
85
+ "required": ["source", "target", "type", "direction", "weight"],
86
+ "properties": {
87
+ "source": {
88
+ "type": "string",
89
+ "minLength": 1,
90
+ "description": "Node id the edge originates from."
91
+ },
92
+ "target": {
93
+ "type": "string",
94
+ "minLength": 1,
95
+ "description": "Node id the edge points to."
96
+ },
97
+ "type": {
98
+ "type": "string",
99
+ "description": "The kind of relationship between source and target.",
100
+ "enum": [
101
+ "uses-token",
102
+ "composes",
103
+ "extends",
104
+ "transitions-to",
105
+ "depends-on",
106
+ "mirrors",
107
+ "conflicts-with",
108
+ "referenced-by",
109
+ "tested-by",
110
+ "documented-by",
111
+ "consumes-context",
112
+ "provides-context"
113
+ ]
114
+ },
115
+ "direction": {
116
+ "type": "string",
117
+ "description": "Whether the relationship reads source-to-target (forward), target-to-source (backward), or both ways (bidirectional).",
118
+ "enum": ["forward", "backward", "bidirectional"]
119
+ },
120
+ "weight": {
121
+ "type": "number",
122
+ "minimum": 0,
123
+ "maximum": 1,
124
+ "description": "Relationship strength in the inclusive range 0..1."
125
+ }
126
+ },
127
+ "additionalProperties": true
128
+ }
129
+ }
130
+ }
@@ -7,7 +7,7 @@
7
7
  "properties": {
8
8
  "tools": {
9
9
  "type": "object",
10
- "description": "Per-tool input/output schemas keyed by tool name. Exactly 12 entries (D-03 hard cap).",
10
+ "description": "Per-tool input/output schemas keyed by tool name. Exactly 13 entries (D-03 cap, raised 12 -> 13 in Phase 52 for gdd_context_query).",
11
11
  "additionalProperties": false,
12
12
  "properties": {
13
13
  "gdd_status": {
@@ -33,6 +33,39 @@
33
33
  }
34
34
  }
35
35
  },
36
+ "gdd_context_query": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "required": ["input", "output"],
40
+ "properties": {
41
+ "input": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["op"],
45
+ "properties": {
46
+ "op": {
47
+ "type": "string",
48
+ "enum": ["nodes", "edges", "path", "consumers-of", "unreachable", "cycles", "coverage"]
49
+ },
50
+ "type": { "type": "string" },
51
+ "tag": { "type": "string" },
52
+ "from": { "type": "string" },
53
+ "to": { "type": "string" },
54
+ "id": { "type": "string" }
55
+ }
56
+ },
57
+ "output": {
58
+ "type": "object",
59
+ "required": ["op", "graph_present", "result"],
60
+ "properties": {
61
+ "op": { "type": "string" },
62
+ "graph_present": { "type": "boolean" },
63
+ "path": { "type": "string" },
64
+ "result": { "type": ["array", "object", "null"] }
65
+ }
66
+ }
67
+ }
68
+ },
36
69
  "gdd_phase_current": {
37
70
  "type": "object",
38
71
  "additionalProperties": false,
@@ -9,7 +9,7 @@ is a `composes_with` edge (the source calls the target as sub-orchestration); a
9
9
  a `next_skills` edge (a pipeline hint for what runs next). Stage grouping is best-effort and
10
10
  inferred from the skill name; skills with no stage keyword fall under Utility.
11
11
 
12
- Skills: 89. Composition edges: 0 composes_with, 6 next_skills.
12
+ Skills: 91. Composition edges: 0 composes_with, 6 next_skills.
13
13
 
14
14
  ```mermaid
15
15
  flowchart TD
@@ -72,6 +72,7 @@ flowchart TD
72
72
  n_cache_manager["cache-manager"]
73
73
  n_check_update["check-update"]
74
74
  n_connections["connections"]
75
+ n_context["context"]
75
76
  n_continue["continue"]
76
77
  n_debug["debug"]
77
78
  n_extract_learnings["extract-learnings"]
@@ -83,6 +84,7 @@ flowchart TD
83
84
  n_instinct["instinct"]
84
85
  n_list_pins["list-pins"]
85
86
  n_locale["locale"]
87
+ n_migrate_context["migrate-context"]
86
88
  n_new_skill["new-skill"]
87
89
  n_next["next"]
88
90
  n_note["note"]
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ // scripts/lib/design-context/extract-a11y.mjs — Phase 52 (DesignContext graph), executor B.
3
+ //
4
+ // Deterministic, dependency-free accessibility extractor. Regex-scans source
5
+ // for ARIA attributes, role=, keyboard handlers, focus states, semantic
6
+ // landmarks, skip links, and image alt coverage, and emits a Fragment
7
+ // (schema_version 52.0) of `a11y-pattern` nodes with `documented-by` /
8
+ // `referenced-by` edges:
9
+ // - component file --referenced-by--> a11y-pattern (the file exhibits the pattern)
10
+ // - a11y-pattern --documented-by--> wcag:<criterion> node (when a known pattern
11
+ // maps to a WCAG criterion we recognize)
12
+ //
13
+ // Semantics mirror agents/a11y-mapper.md (ARIA / keyboard / focus / landmarks /
14
+ // alt). Static-only — no live browser. Structural pass: summary='' and
15
+ // complexity='moderate' are stubs the LLM/mapper phase fills later. No network,
16
+ // no deps, no top-level Date.now() (stamped in main()).
17
+ //
18
+ // Public API:
19
+ // extract(roots, opts?) -> Fragment (pure)
20
+ // main() -> prints Fragment JSON to stdout
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { pathToFileURL } from 'node:url';
25
+
26
+ const MAPPER = 'a11y-mapper';
27
+ const SCHEMA_VERSION = '52.0';
28
+
29
+ const SCANNABLE_EXT = new Set([
30
+ '.tsx', '.jsx', '.ts', '.js',
31
+ '.vue', '.svelte', '.html', '.htm',
32
+ '.css', '.scss',
33
+ ]);
34
+ const SKIP_DIRS = new Set([
35
+ 'node_modules', '.git', 'dist', 'build', '.next', 'coverage',
36
+ '.design', '.planning', 'out', '.cache', '.turbo', '.svelte-kit',
37
+ ]);
38
+
39
+ function walk(root) {
40
+ const out = [];
41
+ let st;
42
+ try { st = fs.statSync(root); } catch { return out; }
43
+ if (st.isFile()) {
44
+ if (SCANNABLE_EXT.has(path.extname(root).toLowerCase())) out.push(root);
45
+ return out;
46
+ }
47
+ const stack = [root];
48
+ while (stack.length) {
49
+ const dir = stack.pop();
50
+ let entries;
51
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
52
+ for (const e of entries) {
53
+ const full = path.join(dir, e.name);
54
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) stack.push(full); }
55
+ else if (e.isFile() && SCANNABLE_EXT.has(path.extname(e.name).toLowerCase())) out.push(full);
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function slug(s) {
62
+ return String(s).trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'x';
63
+ }
64
+ function stubNode(id, type, name, extra) {
65
+ return { id, type, name, summary: '', tags: [], complexity: 'moderate', ...extra };
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // A11y matchers + WCAG mapping.
70
+ // ---------------------------------------------------------------------------
71
+
72
+ const ARIA_ATTR = /\baria-([a-z]+)\s*=/g;
73
+ const ROLE_ATTR = /\brole\s*=\s*["']([a-z]+)["']/g;
74
+ const KEYBOARD = /\b(?:tabIndex|tabindex|onKeyDown|onKeyPress|onKeyUp|on:keydown)\b/g;
75
+ const FOCUS_STATE = /:focus(?:-visible)?|focus-visible:|focus:/g;
76
+ const LANDMARK = /<(header|nav|main|section|article|aside|footer)\b/g;
77
+ const SKIP_LINK = /(?:skip-nav|skip-to-content|skip-link|#main-content)/g;
78
+ const IMG_TAG = /<img\b[^>]*>/gi;
79
+
80
+ // pattern-id -> WCAG criterion it satisfies/relates to.
81
+ const WCAG_FOR_PATTERN = {
82
+ 'aria-attributes': '4.1.2',
83
+ 'role-semantics': '4.1.2',
84
+ 'keyboard-support': '2.1.1',
85
+ 'focus-visible': '2.4.7',
86
+ 'semantic-landmarks': '1.3.1',
87
+ 'skip-link': '2.4.1',
88
+ 'image-alt': '1.1.1',
89
+ };
90
+
91
+ function collect(re, content, mapFn) {
92
+ const out = [];
93
+ re.lastIndex = 0;
94
+ let m;
95
+ while ((m = re.exec(content)) !== null) {
96
+ const v = mapFn(m);
97
+ if (v != null) out.push(v);
98
+ if (m.index === re.lastIndex) re.lastIndex++;
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * Pure extractor.
105
+ * @param {string[]|string} roots
106
+ * @param {{generatedAt?: string}} [opts]
107
+ * @returns {object} Fragment
108
+ */
109
+ export function extract(roots, opts = {}) {
110
+ const rootList = (Array.isArray(roots) ? roots : [roots]).filter(Boolean);
111
+ const nodeMap = new Map();
112
+ const edgeSet = new Map();
113
+
114
+ const addPattern = (patternId, name, extra) => {
115
+ const id = `a11y-pattern:${patternId}`;
116
+ if (!nodeMap.has(id)) nodeMap.set(id, stubNode(id, 'a11y-pattern', name, extra || {}));
117
+ return id;
118
+ };
119
+ const addEdge = (source, target, type, weight) => {
120
+ const key = `${source}--${type}-->${target}`;
121
+ if (!edgeSet.has(key)) edgeSet.set(key, { source, target, type, direction: 'forward', weight });
122
+ };
123
+ // a11y-pattern --documented-by--> wcag criterion node.
124
+ const linkWcag = (patternKey, patternNodeId) => {
125
+ const crit = WCAG_FOR_PATTERN[patternKey];
126
+ if (!crit) return;
127
+ const wid = `pattern:wcag-${slug(crit)}`;
128
+ if (!nodeMap.has(wid)) nodeMap.set(wid, stubNode(wid, 'pattern', `WCAG ${crit}`, { criterion: crit }));
129
+ addEdge(patternNodeId, wid, 'documented-by', 0.8);
130
+ };
131
+
132
+ for (const root of rootList) {
133
+ for (const abs of walk(root)) {
134
+ let content;
135
+ try { content = fs.readFileSync(abs, 'utf8'); } catch { continue; }
136
+ const ext = path.extname(abs).toLowerCase();
137
+ const baseName = path.basename(abs, ext);
138
+ const compId = `component:${slug(baseName)}`;
139
+
140
+ // Detect each pattern family present in this file.
141
+ const aria = collect(ARIA_ATTR, content, (m) => m[1]);
142
+ const roles = collect(ROLE_ATTR, content, (m) => m[1]);
143
+ const kbd = (content.match(KEYBOARD) || []).length;
144
+ const focus = (content.match(FOCUS_STATE) || []).length;
145
+ const landmarks = collect(LANDMARK, content, (m) => m[1]);
146
+ const skip = (content.match(SKIP_LINK) || []).length;
147
+ const imgs = collect(IMG_TAG, content, (m) => m[0]);
148
+
149
+ const families = [];
150
+ if (aria.length) families.push(['aria-attributes', 'ARIA attributes', { attributes: [...new Set(aria)].slice(0, 12) }]);
151
+ if (roles.length) families.push(['role-semantics', 'ARIA role semantics', { roles: [...new Set(roles)].slice(0, 12) }]);
152
+ if (kbd) families.push(['keyboard-support', 'Keyboard navigation', {}]);
153
+ if (focus) families.push(['focus-visible', 'Focus-visible states', {}]);
154
+ if (landmarks.length) families.push(['semantic-landmarks', 'Semantic landmarks', { landmarks: [...new Set(landmarks)] }]);
155
+ if (skip) families.push(['skip-link', 'Skip link', {}]);
156
+ if (imgs.length) {
157
+ const withAlt = imgs.filter((t) => /\balt\s*=/.test(t)).length;
158
+ families.push(['image-alt', 'Image alt coverage', { images: imgs.length, with_alt: withAlt }]);
159
+ }
160
+
161
+ for (const [key, name, extra] of families) {
162
+ const pid = addPattern(key, name, extra);
163
+ // The file *references* the a11y pattern.
164
+ addEdge(compId, pid, 'referenced-by', 0.5);
165
+ // The pattern is *documented-by* the WCAG criterion.
166
+ linkWcag(key, pid);
167
+ }
168
+ }
169
+ }
170
+
171
+ return {
172
+ schema_version: SCHEMA_VERSION,
173
+ mapper: MAPPER,
174
+ generated_at: opts.generatedAt || '',
175
+ nodes: [...nodeMap.values()],
176
+ edges: [...edgeSet.values()],
177
+ };
178
+ }
179
+
180
+ export function main(argv = process.argv.slice(2)) {
181
+ const roots = argv.length ? argv : [process.cwd()];
182
+ const fragment = extract(roots, { generatedAt: new Date().toISOString() });
183
+ process.stdout.write(JSON.stringify(fragment, null, 2) + '\n');
184
+ }
185
+
186
+ const invokedDirectly =
187
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
188
+ if (invokedDirectly) main();
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ // scripts/lib/design-context/extract-components.mjs — Phase 52 (DesignContext graph), executor B.
3
+ //
4
+ // Deterministic, dependency-free component extractor. Regex-scans component
5
+ // files (.tsx/.jsx/.vue/.svelte) for component definitions/exports, classifies
6
+ // each as Atomic / Molecular / Organism via import + composition heuristics, and
7
+ // emits a Fragment (schema_version 52.0) of `component` + `variant` + `layer`
8
+ // nodes with `composes` / `extends` edges.
9
+ //
10
+ // Heuristics (mirrors agents/component-taxonomy-mapper.md semantics):
11
+ // - Atomic : composes 0 other detected components
12
+ // - Molecular : composes 1-4 other detected components
13
+ // - Organism : composes 5+ OR is route/page-shaped (Page/Screen/Layout/View name)
14
+ // A `variant` node is emitted when a `variant`/`size`/`intent` prop union or a
15
+ // cva()/tv() variants block is detected; the variant `extends` its component.
16
+ // A `layer` node (subtype Atomic/Molecular/Organism) is emitted per distinct
17
+ // layer and the component `composes`... is captured component->component.
18
+ //
19
+ // Structural pass only: summary='' and complexity='moderate' are stubs the
20
+ // LLM/mapper phase fills later (validator soft-warns). No network, no deps, no
21
+ // top-level Date.now() (stamped in main()).
22
+ //
23
+ // Public API:
24
+ // extract(roots, opts?) -> Fragment (pure)
25
+ // main() -> prints Fragment JSON to stdout
26
+
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { pathToFileURL } from 'node:url';
30
+
31
+ const MAPPER = 'component-taxonomy-mapper';
32
+ const SCHEMA_VERSION = '52.0';
33
+
34
+ const COMPONENT_EXT = new Set(['.tsx', '.jsx', '.vue', '.svelte']);
35
+ const SKIP_DIRS = new Set([
36
+ 'node_modules', '.git', 'dist', 'build', '.next', 'coverage',
37
+ '.design', '.planning', 'out', '.cache', '.turbo', '.svelte-kit',
38
+ ]);
39
+
40
+ const ORGANISM_NAME = /(Page|Screen|Layout|View|Dashboard|Shell|Provider)$/;
41
+
42
+ /** Recursively collect component files under `root`. */
43
+ function walk(root) {
44
+ const out = [];
45
+ let st;
46
+ try { st = fs.statSync(root); } catch { return out; }
47
+ if (st.isFile()) {
48
+ if (COMPONENT_EXT.has(path.extname(root).toLowerCase())) out.push(root);
49
+ return out;
50
+ }
51
+ const stack = [root];
52
+ while (stack.length) {
53
+ const dir = stack.pop();
54
+ let entries;
55
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
56
+ for (const e of entries) {
57
+ const full = path.join(dir, e.name);
58
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) stack.push(full); }
59
+ else if (e.isFile() && COMPONENT_EXT.has(path.extname(e.name).toLowerCase())) out.push(full);
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function slug(s) {
66
+ return String(s).trim().replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'x';
67
+ }
68
+
69
+ // Canonical component id — lowercased so it MATCHES the file-basename-derived
70
+ // `component:<basename>` ids produced by the token / a11y / visual-hierarchy
71
+ // extractors. Component identity is case-insensitive across the graph so that a
72
+ // `referenced-by` edge from a11y (which only knows the filename) recovers
73
+ // against the `component` node defined here (which knows the export name).
74
+ // Cross-fragment edge recovery in merge-fragments.mjs depends on this.
75
+ function componentId(name) {
76
+ return `component:${slug(name).toLowerCase()}`;
77
+ }
78
+
79
+ function stubNode(id, type, name, extra) {
80
+ return { id, type, name, summary: '', tags: [], complexity: 'moderate', ...extra };
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Component detection.
85
+ // ---------------------------------------------------------------------------
86
+
87
+ // React/TS component definitions: function decl, arrow const, class, default export.
88
+ const DEF_FN = /(?:export\s+(?:default\s+)?)?function\s+([A-Z][A-Za-z0-9]*)\s*\(/g;
89
+ const DEF_ARROW = /(?:export\s+)?const\s+([A-Z][A-Za-z0-9]*)\s*(?::\s*[A-Za-z0-9_.<>,\s]+)?=\s*(?:\([^)]*\)|[A-Za-z0-9_]+)\s*=>/g;
90
+ const DEF_CLASS = /(?:export\s+(?:default\s+)?)?class\s+([A-Z][A-Za-z0-9]*)\s+extends\s+(?:React\.)?(?:Pure)?Component\b/g;
91
+ const DEF_MEMO = /(?:export\s+)?const\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?(?:memo|forwardRef)\s*\(/g;
92
+
93
+ // JSX usage of a capitalized tag → a composed child component.
94
+ const JSX_USE = /<([A-Z][A-Za-z0-9]*)[\s/>]/g;
95
+
96
+ // Variant signals.
97
+ const VARIANT_PROP = /\b(?:variant|intent|size|tone|appearance|kind)\??\s*:\s*([^;\n}]+)/g;
98
+ const CVA_BLOCK = /\b(?:cva|tv|cssVariants)\s*\(/g;
99
+
100
+ function collect(re, content, mapFn) {
101
+ const out = [];
102
+ re.lastIndex = 0;
103
+ let m;
104
+ while ((m = re.exec(content)) !== null) {
105
+ const v = mapFn(m);
106
+ if (v) out.push(v);
107
+ if (m.index === re.lastIndex) re.lastIndex++;
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /** Detect component definitions in one file. Returns array of names. */
113
+ function definedComponents(content, ext, baseName) {
114
+ const names = new Set();
115
+ if (ext === '.tsx' || ext === '.jsx') {
116
+ for (const n of collect(DEF_FN, content, (m) => m[1])) names.add(n);
117
+ for (const n of collect(DEF_ARROW, content, (m) => m[1])) names.add(n);
118
+ for (const n of collect(DEF_CLASS, content, (m) => m[1])) names.add(n);
119
+ for (const n of collect(DEF_MEMO, content, (m) => m[1])) names.add(n);
120
+ }
121
+ // Vue/Svelte (and React fallback): the file itself is a component named by
122
+ // its basename when nothing else was detected.
123
+ if (!names.size && /^[A-Z]/.test(baseName)) names.add(baseName);
124
+ else if (ext === '.vue' || ext === '.svelte') names.add(baseName);
125
+ return [...names];
126
+ }
127
+
128
+ /** Detect child components composed inside one file. */
129
+ function composedChildren(content) {
130
+ const kids = new Set();
131
+ for (const n of collect(JSX_USE, content, (m) => m[1])) {
132
+ // Skip well-known intrinsic-ish wrappers that aren't design components.
133
+ if (n === 'Fragment' || n === 'Suspense' || n === 'StrictMode') continue;
134
+ kids.add(n);
135
+ }
136
+ return kids;
137
+ }
138
+
139
+ function classifyLayer(name, childCount) {
140
+ if (ORGANISM_NAME.test(name) || childCount >= 5) return 'Organism';
141
+ if (childCount >= 1) return 'Molecular';
142
+ return 'Atomic';
143
+ }
144
+
145
+ /**
146
+ * Pure extractor.
147
+ * @param {string[]|string} roots
148
+ * @param {{generatedAt?: string}} [opts]
149
+ * @returns {object} Fragment
150
+ */
151
+ export function extract(roots, opts = {}) {
152
+ const rootList = (Array.isArray(roots) ? roots : [roots]).filter(Boolean);
153
+ const nodeMap = new Map();
154
+ const edgeSet = new Map();
155
+ const layerSeen = new Set();
156
+
157
+ // Pass 1: collect every defined component name (across files) so composes
158
+ // edges only point at components we actually saw defined somewhere.
159
+ const fileInfos = [];
160
+ const definedGlobal = new Set();
161
+ for (const root of rootList) {
162
+ for (const abs of walk(root)) {
163
+ let content;
164
+ try { content = fs.readFileSync(abs, 'utf8'); } catch { continue; }
165
+ const ext = path.extname(abs).toLowerCase();
166
+ const baseName = path.basename(abs, ext);
167
+ const defs = definedComponents(content, ext, baseName);
168
+ const kids = composedChildren(content);
169
+ const hasCva = CVA_BLOCK.test(content) || VARIANT_PROP.test(content);
170
+ // reset lastIndex side effects of .test on global regexes
171
+ CVA_BLOCK.lastIndex = 0; VARIANT_PROP.lastIndex = 0;
172
+ defs.forEach((d) => definedGlobal.add(d));
173
+ fileInfos.push({ defs, kids, hasCva });
174
+ }
175
+ }
176
+
177
+ // Pass 2: build nodes + edges.
178
+ for (const { defs, kids, hasCva } of fileInfos) {
179
+ for (const name of defs) {
180
+ const id = componentId(name);
181
+ // Children that are themselves defined components (drop self + unknown).
182
+ const realKids = [...kids].filter((k) => k !== name && definedGlobal.has(k));
183
+ const layer = classifyLayer(name, realKids.length);
184
+
185
+ if (!nodeMap.has(id)) {
186
+ nodeMap.set(id, stubNode(id, 'component', name, { layer }));
187
+ } else {
188
+ // Prefer the more-specific (higher) layer if seen twice.
189
+ const order = { Atomic: 0, Molecular: 1, Organism: 2 };
190
+ const prev = nodeMap.get(id);
191
+ if (order[layer] > order[prev.layer]) prev.layer = layer;
192
+ }
193
+
194
+ // layer node (one per distinct layer) + component depends-on-layer is
195
+ // implicit via the `layer` field; we emit the layer node so the graph has
196
+ // a navigable taxonomy anchor.
197
+ const layerId = `layer:${layer}`;
198
+ if (!layerSeen.has(layerId)) {
199
+ layerSeen.add(layerId);
200
+ nodeMap.set(layerId, stubNode(layerId, 'layer', `${layer} layer`, { subtype: layer }));
201
+ }
202
+
203
+ // composes edges: component -> child component.
204
+ for (const k of realKids) {
205
+ const childId = componentId(k);
206
+ const key = `${id}--composes-->${childId}`;
207
+ if (!edgeSet.has(key)) {
208
+ edgeSet.set(key, { source: id, target: childId, type: 'composes', direction: 'forward', weight: 0.6 });
209
+ }
210
+ }
211
+
212
+ // variant node + extends edge.
213
+ if (hasCva) {
214
+ const variantId = `variant:${slug(name).toLowerCase()}`;
215
+ if (!nodeMap.has(variantId)) {
216
+ nodeMap.set(variantId, stubNode(variantId, 'variant', `${name} (variants)`, {}));
217
+ }
218
+ const vkey = `${variantId}--extends-->${id}`;
219
+ if (!edgeSet.has(vkey)) {
220
+ edgeSet.set(vkey, { source: variantId, target: id, type: 'extends', direction: 'forward', weight: 0.7 });
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ return {
227
+ schema_version: SCHEMA_VERSION,
228
+ mapper: MAPPER,
229
+ generated_at: opts.generatedAt || '',
230
+ nodes: [...nodeMap.values()],
231
+ edges: [...edgeSet.values()],
232
+ };
233
+ }
234
+
235
+ export function main(argv = process.argv.slice(2)) {
236
+ const roots = argv.length ? argv : [process.cwd()];
237
+ const fragment = extract(roots, { generatedAt: new Date().toISOString() });
238
+ process.stdout.write(JSON.stringify(fragment, null, 2) + '\n');
239
+ }
240
+
241
+ const invokedDirectly =
242
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
243
+ if (invokedDirectly) main();