@hegemonart/get-design-done 1.50.1 → 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 (52) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +93 -0
  4. package/README.md +4 -0
  5. package/SKILL.md +4 -1
  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-reflector.md +33 -0
  10. package/agents/design-research-synthesizer.md +27 -1
  11. package/agents/motion-mapper.md +35 -13
  12. package/agents/token-mapper.md +30 -1
  13. package/agents/visual-hierarchy-mapper.md +30 -1
  14. package/dist/claude-code/.claude/skills/apply-reflections/SKILL.md +17 -0
  15. package/dist/claude-code/.claude/skills/context/SKILL.md +137 -0
  16. package/dist/claude-code/.claude/skills/extract-learnings/SKILL.md +16 -0
  17. package/dist/claude-code/.claude/skills/instinct/SKILL.md +111 -0
  18. package/dist/claude-code/.claude/skills/migrate-context/SKILL.md +123 -0
  19. package/dist/claude-code/.claude/skills/progress/SKILL.md +4 -0
  20. package/hooks/gdd-decision-injector.js +115 -6
  21. package/package.json +3 -2
  22. package/reference/design-context-schema.md +159 -0
  23. package/reference/design-context-tag-vocab.md +82 -0
  24. package/reference/instinct-format.md +120 -0
  25. package/reference/registry.json +21 -0
  26. package/reference/schemas/design-context.schema.json +130 -0
  27. package/reference/schemas/events.schema.json +1 -1
  28. package/reference/schemas/instinct.schema.json +91 -0
  29. package/reference/schemas/mcp-gdd-tools.schema.json +34 -1
  30. package/reference/skill-graph.md +4 -1
  31. package/scripts/lib/design-context/extract-a11y.mjs +188 -0
  32. package/scripts/lib/design-context/extract-components.mjs +243 -0
  33. package/scripts/lib/design-context/extract-motion.mjs +248 -0
  34. package/scripts/lib/design-context/extract-tokens.mjs +234 -0
  35. package/scripts/lib/design-context/extract-visual-hierarchy.mjs +178 -0
  36. package/scripts/lib/design-context/integration-map.mjs +251 -0
  37. package/scripts/lib/design-context/merge-fragments.mjs +227 -0
  38. package/scripts/lib/design-context-query.cjs +0 -0
  39. package/scripts/lib/instinct-store.cjs +677 -0
  40. package/scripts/lib/manifest/skills.json +24 -0
  41. package/scripts/lib/mcp-tools-lint/index.cjs +3 -1
  42. package/sdk/mcp/gdd-mcp/schemas/gdd_context_query.schema.json +60 -0
  43. package/sdk/mcp/gdd-mcp/server.js +474 -158
  44. package/sdk/mcp/gdd-mcp/server.ts +9 -5
  45. package/sdk/mcp/gdd-mcp/tools/gdd_context_query.ts +35 -0
  46. package/sdk/mcp/gdd-mcp/tools/index.ts +18 -13
  47. package/skills/apply-reflections/SKILL.md +17 -0
  48. package/skills/context/SKILL.md +137 -0
  49. package/skills/extract-learnings/SKILL.md +16 -0
  50. package/skills/instinct/SKILL.md +111 -0
  51. package/skills/migrate-context/SKILL.md +123 -0
  52. 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
+ }
@@ -10,7 +10,7 @@
10
10
  "type": {
11
11
  "type": "string",
12
12
  "minLength": 1,
13
- "description": "Free-form event type identifier. Pre-registered seeds: state.mutation, state.transition, stage.entered, stage.exited, hook.fired, error, capability_gap, kfm-candidate, router_pick, verify_outcome, rollout_started, rollout_advanced, rollout_stuck, budget_forecast, project_cap_warning, project_cap_halt, live_session_start, live_pick, live_generate, live_accept, live_discard, live_session_end."
13
+ "description": "Free-form event type identifier. Pre-registered seeds: state.mutation, state.transition, stage.entered, stage.exited, hook.fired, error, capability_gap, kfm-candidate, router_pick, verify_outcome, rollout_started, rollout_advanced, rollout_stuck, budget_forecast, project_cap_warning, project_cap_halt, live_session_start, live_pick, live_generate, live_accept, live_discard, live_session_end, instinct_emitted, instinct_promoted, instinct_decayed."
14
14
  },
15
15
  "timestamp": {
16
16
  "type": "string",
@@ -0,0 +1,91 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/hegemonart/get-design-done/reference/schemas/instinct.schema.json",
4
+ "title": "Instinct Unit",
5
+ "description": "An atomic, confidence-weighted design instinct learned across cycles. Validates the YAML frontmatter object of an instinct unit (see reference/instinct-format.md). Project-scoped units live at <root>/instincts/instincts.json; promoted global units live at ~/.claude/gdd/global-instincts.json. Persisted + queried by scripts/lib/instinct-store.cjs.",
6
+ "type": "object",
7
+ "required": ["id", "trigger", "confidence", "domain", "scope", "source"],
8
+ "properties": {
9
+ "id": {
10
+ "type": "string",
11
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
12
+ "minLength": 3,
13
+ "maxLength": 80,
14
+ "description": "Kebab-case stable identifier, e.g. \"prefer-token-over-hex\". Lowercase letters, digits, single hyphens."
15
+ },
16
+ "trigger": {
17
+ "type": "string",
18
+ "minLength": 8,
19
+ "maxLength": 280,
20
+ "description": "One sentence naming the situation that fires the instinct, e.g. \"When a color literal appears in a component, reach for a design token first.\""
21
+ },
22
+ "confidence": {
23
+ "type": "number",
24
+ "minimum": 0.3,
25
+ "maximum": 0.9,
26
+ "description": "Posterior trust in the instinct. Floor 0.3 (a fresh instinct is advisory, never directive); ceiling 0.9 (no instinct is ever certain). TTL decay multiplies this by 0.9 when the instinct goes unsurfaced."
27
+ },
28
+ "domain": {
29
+ "type": "string",
30
+ "enum": ["intake", "explore", "decide", "build", "verify", "operate", "utility"],
31
+ "description": "Lifecycle stage the instinct applies to, aligned to the Phase 50 lifecycle stages."
32
+ },
33
+ "scope": {
34
+ "type": "string",
35
+ "enum": ["project", "global"],
36
+ "description": "project = learned from one repository; global = promoted after the K/M gate across distinct projects."
37
+ },
38
+ "project_id": {
39
+ "type": "string",
40
+ "pattern": "^[0-9a-f]{8}$",
41
+ "description": "8-char hex sha of the normalized git origin the instinct was first learned from. Required for project scope; optional for global (a promoted instinct is no longer tied to one origin)."
42
+ },
43
+ "source": {
44
+ "type": "string",
45
+ "enum": ["reflection", "extract-learnings", "user"],
46
+ "description": "Which producer minted the instinct: a reflection pass, the extract-learnings step, or a direct user assertion."
47
+ },
48
+ "cycles_seen": {
49
+ "type": "integer",
50
+ "minimum": 0,
51
+ "description": "How many distinct design cycles have surfaced this instinct. Feeds the K=2 half of the promotion gate."
52
+ },
53
+ "project_ids": {
54
+ "type": "array",
55
+ "items": { "type": "string", "pattern": "^[0-9a-f]{8}$" },
56
+ "uniqueItems": true,
57
+ "description": "Set of distinct project ids that have surfaced this instinct. Its length feeds the M=2 half of the promotion gate."
58
+ },
59
+ "first_seen": {
60
+ "type": "string",
61
+ "format": "date",
62
+ "description": "ISO date (YYYY-MM-DD) the instinct was first recorded."
63
+ },
64
+ "last_seen": {
65
+ "type": "string",
66
+ "format": "date",
67
+ "description": "ISO date (YYYY-MM-DD) the instinct was last surfaced. Resets the TTL decay window."
68
+ },
69
+ "alpha": {
70
+ "type": "number",
71
+ "minimum": 0,
72
+ "description": "Beta posterior success weight. Seeded from the Beta(2,8) prior on promotion."
73
+ },
74
+ "beta": {
75
+ "type": "number",
76
+ "minimum": 0,
77
+ "description": "Beta posterior failure weight. Seeded from the Beta(2,8) prior on promotion."
78
+ },
79
+ "prior_class": {
80
+ "type": "string",
81
+ "description": "Tag recording which prior class seeded the posterior, e.g. \"instinct\"."
82
+ }
83
+ },
84
+ "additionalProperties": false,
85
+ "allOf": [
86
+ {
87
+ "if": { "properties": { "scope": { "const": "project" } } },
88
+ "then": { "required": ["project_id"] }
89
+ }
90
+ ]
91
+ }
@@ -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: 88. 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"]
@@ -80,8 +81,10 @@ flowchart TD
80
81
  n_graphify["graphify"]
81
82
  n_health["health"]
82
83
  n_help["help"]
84
+ n_instinct["instinct"]
83
85
  n_list_pins["list-pins"]
84
86
  n_locale["locale"]
87
+ n_migrate_context["migrate-context"]
85
88
  n_new_skill["new-skill"]
86
89
  n_next["next"]
87
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();