@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +47 -0
- package/README.md +2 -0
- package/SKILL.md +2 -0
- package/agents/a11y-mapper.md +30 -1
- package/agents/component-taxonomy-mapper.md +30 -1
- package/agents/design-debt-crawler.md +60 -60
- package/agents/design-research-synthesizer.md +27 -1
- package/agents/motion-mapper.md +35 -13
- package/agents/token-mapper.md +30 -1
- package/agents/visual-hierarchy-mapper.md +30 -1
- package/dist/claude-code/.claude/skills/context/SKILL.md +137 -0
- package/dist/claude-code/.claude/skills/migrate-context/SKILL.md +123 -0
- package/dist/claude-code/.claude/skills/progress/SKILL.md +4 -0
- package/package.json +3 -2
- package/reference/design-context-schema.md +159 -0
- package/reference/design-context-tag-vocab.md +82 -0
- package/reference/registry.json +14 -0
- package/reference/schemas/design-context.schema.json +130 -0
- package/reference/schemas/mcp-gdd-tools.schema.json +34 -1
- package/reference/skill-graph.md +3 -1
- package/scripts/lib/design-context/extract-a11y.mjs +188 -0
- package/scripts/lib/design-context/extract-components.mjs +243 -0
- package/scripts/lib/design-context/extract-motion.mjs +248 -0
- package/scripts/lib/design-context/extract-tokens.mjs +234 -0
- package/scripts/lib/design-context/extract-visual-hierarchy.mjs +178 -0
- package/scripts/lib/design-context/integration-map.mjs +251 -0
- package/scripts/lib/design-context/merge-fragments.mjs +227 -0
- package/scripts/lib/design-context-query.cjs +0 -0
- package/scripts/lib/manifest/skills.json +16 -0
- package/scripts/lib/mcp-tools-lint/index.cjs +3 -1
- package/sdk/mcp/gdd-mcp/schemas/gdd_context_query.schema.json +60 -0
- package/sdk/mcp/gdd-mcp/server.js +474 -158
- package/sdk/mcp/gdd-mcp/server.ts +9 -5
- package/sdk/mcp/gdd-mcp/tools/gdd_context_query.ts +35 -0
- package/sdk/mcp/gdd-mcp/tools/index.ts +18 -13
- package/skills/context/SKILL.md +137 -0
- package/skills/migrate-context/SKILL.md +123 -0
- package/skills/progress/SKILL.md +4 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/lib/design-context/integration-map.mjs - Phase 52 (DesignContext graph), executor D.
|
|
3
|
+
//
|
|
4
|
+
// Renders an Atomic-Design integration map (mermaid) from the canonical
|
|
5
|
+
// DesignContext graph at .design/context-graph.json. The map groups nodes into
|
|
6
|
+
// the four Atomic-Design tiers (Atomic, Molecular, Organism, Template) using the
|
|
7
|
+
// `layer` node subtype as the tier authority, then draws the `composes` and
|
|
8
|
+
// `extends` edges between entities so a reader sees how the system assembles.
|
|
9
|
+
//
|
|
10
|
+
// Tier assignment
|
|
11
|
+
// ---------------
|
|
12
|
+
// A `layer` node carries a subtype of Atomic / Molecular / Organism / Template
|
|
13
|
+
// and a set of `composes`/`extends` edges to the entities that sit in that tier.
|
|
14
|
+
// Every entity reachable from a layer node by a composes/extends edge is placed
|
|
15
|
+
// in that layer's tier. An entity with no layer membership lands in an
|
|
16
|
+
// "Unlayered" bucket so nothing is silently dropped. When the graph has NO layer
|
|
17
|
+
// nodes at all (a pre-taxonomy graph), every composes/extends participant falls
|
|
18
|
+
// into "Unlayered" and the map still renders.
|
|
19
|
+
//
|
|
20
|
+
// Pure render seam
|
|
21
|
+
// ----------------
|
|
22
|
+
// render(graph) -> mermaid markdown string. No I/O, no Date.now(), no deps. The
|
|
23
|
+
// CLI main() reads .design/context-graph.json and atomic-writes
|
|
24
|
+
// .design/INTEGRATION-MAP.md via the sibling graph/atomic-write helper. main() is
|
|
25
|
+
// non-fatal when the graph is absent or unreadable: it prints a notice to stderr
|
|
26
|
+
// and returns 0, so a pipeline step that always runs it never breaks a build.
|
|
27
|
+
//
|
|
28
|
+
// No network, no optional deps, no top-level Date.now() (stamped only in main()).
|
|
29
|
+
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { pathToFileURL } from 'node:url';
|
|
33
|
+
import { writeFileSync, renameSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
|
|
34
|
+
import { dirname, basename, join, resolve } from 'node:path';
|
|
35
|
+
|
|
36
|
+
const DEFAULT_GRAPH = path.join('.design', 'context-graph.json');
|
|
37
|
+
const DEFAULT_OUT = path.join('.design', 'INTEGRATION-MAP.md');
|
|
38
|
+
|
|
39
|
+
// The four Atomic-Design tiers, in render order (broad to composed).
|
|
40
|
+
const TIERS = ['Atomic', 'Molecular', 'Organism', 'Template'];
|
|
41
|
+
const UNLAYERED = 'Unlayered';
|
|
42
|
+
|
|
43
|
+
// Edges that express assembly. composes = whole -> part; extends = special -> base.
|
|
44
|
+
const ASSEMBLY_EDGE_TYPES = new Set(['composes', 'extends']);
|
|
45
|
+
|
|
46
|
+
function nodeList(graph) {
|
|
47
|
+
return Array.isArray(graph && graph.nodes) ? graph.nodes : [];
|
|
48
|
+
}
|
|
49
|
+
function edgeList(graph) {
|
|
50
|
+
return Array.isArray(graph && graph.edges) ? graph.edges : [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Map every node id to its Atomic-Design tier. A `layer` node's subtype names a
|
|
55
|
+
* tier; the entities it composes/extends inherit that tier. Ids not reached from
|
|
56
|
+
* any layer land in UNLAYERED. A node id may appear under at most one tier (first
|
|
57
|
+
* layer membership wins, layers walked in TIERS order then by id for stability).
|
|
58
|
+
*/
|
|
59
|
+
function tierByNodeId(graph) {
|
|
60
|
+
const nodes = nodeList(graph);
|
|
61
|
+
const edges = edgeList(graph);
|
|
62
|
+
const byId = new Map(nodes.filter((n) => n && typeof n.id === 'string').map((n) => [n.id, n]));
|
|
63
|
+
|
|
64
|
+
// Layer nodes grouped by their tier subtype.
|
|
65
|
+
const layerNodes = nodes.filter((n) => n && n.type === 'layer' && typeof n.id === 'string');
|
|
66
|
+
const tierOfLayer = new Map();
|
|
67
|
+
for (const ln of layerNodes) {
|
|
68
|
+
const sub = TIERS.includes(ln.subtype) ? ln.subtype : UNLAYERED;
|
|
69
|
+
tierOfLayer.set(ln.id, sub);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// For each layer, the entities it reaches via a composes/extends edge.
|
|
73
|
+
const assignment = new Map(); // nodeId -> tier
|
|
74
|
+
const orderedLayers = [...layerNodes].sort((a, b) => {
|
|
75
|
+
const ta = TIERS.indexOf(tierOfLayer.get(a.id));
|
|
76
|
+
const tb = TIERS.indexOf(tierOfLayer.get(b.id));
|
|
77
|
+
if (ta !== tb) return (ta === -1 ? TIERS.length : ta) - (tb === -1 ? TIERS.length : tb);
|
|
78
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
for (const ln of orderedLayers) {
|
|
82
|
+
const tier = tierOfLayer.get(ln.id) || UNLAYERED;
|
|
83
|
+
for (const e of edges) {
|
|
84
|
+
if (!e || !ASSEMBLY_EDGE_TYPES.has(e.type)) continue;
|
|
85
|
+
// A layer "owns" the member regardless of edge direction: the member is the
|
|
86
|
+
// non-layer endpoint of an assembly edge that touches this layer.
|
|
87
|
+
let member = null;
|
|
88
|
+
if (e.source === ln.id) member = e.target;
|
|
89
|
+
else if (e.target === ln.id) member = e.source;
|
|
90
|
+
if (member && byId.has(member) && !assignment.has(member)) assignment.set(member, tier);
|
|
91
|
+
}
|
|
92
|
+
// The layer node itself sits in its own tier.
|
|
93
|
+
if (!assignment.has(ln.id)) assignment.set(ln.id, tier);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { assignment, byId };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Sanitize a node id into a mermaid-safe node key (alnum + underscore). */
|
|
100
|
+
function mermaidKey(id) {
|
|
101
|
+
return 'n_' + String(id).replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Escape a label for a mermaid quoted node label. */
|
|
105
|
+
function mermaidLabel(node) {
|
|
106
|
+
const name = (node && (node.name || node.id)) || '';
|
|
107
|
+
const type = (node && node.type) || '';
|
|
108
|
+
const text = type ? `${name} (${type})` : `${name}`;
|
|
109
|
+
return text.replace(/"/g, "'").replace(/[\r\n]+/g, ' ');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Pure render: build the Atomic-Design integration map as mermaid markdown.
|
|
114
|
+
* @param {object} graph a DesignContext graph ({nodes[], edges[]}) or empty/absent.
|
|
115
|
+
* @returns {string} markdown with a fenced mermaid flowchart, always non-empty.
|
|
116
|
+
*/
|
|
117
|
+
export function render(graph) {
|
|
118
|
+
const nodes = nodeList(graph);
|
|
119
|
+
const edges = edgeList(graph);
|
|
120
|
+
const { assignment, byId } = tierByNodeId(graph);
|
|
121
|
+
|
|
122
|
+
// Bucket every assembly participant by tier (so the diagram only draws nodes
|
|
123
|
+
// that actually take part in composition, plus their layer nodes).
|
|
124
|
+
const buckets = new Map([...TIERS, UNLAYERED].map((t) => [t, []]));
|
|
125
|
+
const drawn = new Set();
|
|
126
|
+
const place = (id) => {
|
|
127
|
+
if (!byId.has(id) || drawn.has(id)) return;
|
|
128
|
+
const tier = assignment.get(id) || UNLAYERED;
|
|
129
|
+
buckets.get(tier).push(id);
|
|
130
|
+
drawn.add(id);
|
|
131
|
+
};
|
|
132
|
+
for (const e of edges) {
|
|
133
|
+
if (!e || !ASSEMBLY_EDGE_TYPES.has(e.type)) continue;
|
|
134
|
+
place(e.source);
|
|
135
|
+
place(e.target);
|
|
136
|
+
}
|
|
137
|
+
// Always show layer nodes even if they have no assembly edge yet.
|
|
138
|
+
for (const n of nodes) if (n && n.type === 'layer') place(n.id);
|
|
139
|
+
|
|
140
|
+
const lines = [];
|
|
141
|
+
lines.push('# Integration Map');
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push(
|
|
144
|
+
'Atomic-Design composition map derived from `.design/context-graph.json`. Nodes are grouped ' +
|
|
145
|
+
'by Atomic-Design tier (from `layer` node subtype); edges show `composes` and `extends` ' +
|
|
146
|
+
'relationships. Regenerate with `node scripts/lib/design-context/integration-map.mjs`.',
|
|
147
|
+
);
|
|
148
|
+
lines.push('');
|
|
149
|
+
|
|
150
|
+
const totalDrawn = [...buckets.values()].reduce((acc, ids) => acc + ids.length, 0);
|
|
151
|
+
if (totalDrawn === 0) {
|
|
152
|
+
lines.push('_No `composes`/`extends` relationships in the graph yet, so there is nothing to map._');
|
|
153
|
+
lines.push('');
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
lines.push('```mermaid');
|
|
158
|
+
lines.push('flowchart TD');
|
|
159
|
+
|
|
160
|
+
// Subgraphs per tier (only non-empty ones), in TIERS order then Unlayered.
|
|
161
|
+
for (const tier of [...TIERS, UNLAYERED]) {
|
|
162
|
+
const ids = buckets.get(tier);
|
|
163
|
+
if (!ids.length) continue;
|
|
164
|
+
lines.push(` subgraph ${tier}`);
|
|
165
|
+
for (const id of ids.sort()) {
|
|
166
|
+
const node = byId.get(id);
|
|
167
|
+
lines.push(` ${mermaidKey(id)}["${mermaidLabel(node)}"]`);
|
|
168
|
+
}
|
|
169
|
+
lines.push(' end');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Assembly edges (composes = solid arrow; extends = thick "extends" arrow).
|
|
173
|
+
const seenEdge = new Set();
|
|
174
|
+
for (const e of edges) {
|
|
175
|
+
if (!e || !ASSEMBLY_EDGE_TYPES.has(e.type)) continue;
|
|
176
|
+
if (!byId.has(e.source) || !byId.has(e.target)) continue;
|
|
177
|
+
const key = `${e.source}--${e.type}-->${e.target}`;
|
|
178
|
+
if (seenEdge.has(key)) continue;
|
|
179
|
+
seenEdge.add(key);
|
|
180
|
+
const a = mermaidKey(e.source);
|
|
181
|
+
const b = mermaidKey(e.target);
|
|
182
|
+
if (e.type === 'extends') lines.push(` ${a} -. extends .-> ${b}`);
|
|
183
|
+
else lines.push(` ${a} --> ${b}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lines.push('```');
|
|
187
|
+
lines.push('');
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Atomic write (inlined twin of scripts/lib/graph/atomic-write.mjs for a text
|
|
193
|
+
// payload; that helper is JSON-only and the map is markdown). Same tmp+rename
|
|
194
|
+
// invariant: tmp lives in the SAME dir as target (Windows-atomic rename).
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function atomicWriteText(target, body) {
|
|
198
|
+
const parent = dirname(target);
|
|
199
|
+
const base = basename(target);
|
|
200
|
+
const tmp = join(
|
|
201
|
+
parent,
|
|
202
|
+
`.${base}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`,
|
|
203
|
+
);
|
|
204
|
+
if (resolve(dirname(tmp)) !== resolve(parent)) {
|
|
205
|
+
throw new Error(`atomicWriteText invariant: tmp not in same dir as target (tmp=${tmp}, target=${target})`);
|
|
206
|
+
}
|
|
207
|
+
mkdirSync(parent, { recursive: true });
|
|
208
|
+
try {
|
|
209
|
+
writeFileSync(tmp, body, 'utf8');
|
|
210
|
+
renameSync(tmp, target);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (existsSync(tmp)) {
|
|
213
|
+
try { unlinkSync(tmp); } catch { /* swallow cleanup error; original throw wins */ }
|
|
214
|
+
}
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* CLI entry. Reads the graph (argv[0] or the default path), renders the map, and
|
|
221
|
+
* atomic-writes it (argv[1] or the default out). Non-fatal when the graph is
|
|
222
|
+
* absent or unreadable: prints a notice and returns 0.
|
|
223
|
+
* @returns {number} process exit code (0 always, by design; this is advisory).
|
|
224
|
+
*/
|
|
225
|
+
export function main(argv = process.argv.slice(2)) {
|
|
226
|
+
const graphPath = argv[0] || DEFAULT_GRAPH;
|
|
227
|
+
const outPath = argv[1] || DEFAULT_OUT;
|
|
228
|
+
|
|
229
|
+
if (!fs.existsSync(graphPath)) {
|
|
230
|
+
process.stderr.write(`integration-map: no graph at ${graphPath} (skipping, non-fatal)\n`);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
let graph;
|
|
234
|
+
try {
|
|
235
|
+
graph = JSON.parse(fs.readFileSync(graphPath, 'utf8'));
|
|
236
|
+
} catch (err) {
|
|
237
|
+
process.stderr.write(`integration-map: unreadable graph ${graphPath} (${err.message}) (skipping, non-fatal)\n`);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const md = render(graph);
|
|
242
|
+
const stamp = `<!-- generated ${new Date().toISOString()} by scripts/lib/design-context/integration-map.mjs -->\n`;
|
|
243
|
+
atomicWriteText(outPath, stamp + md);
|
|
244
|
+
process.stderr.write(`integration-map: wrote ${outPath}\n`);
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ESM "run as script" guard (Windows + POSIX safe via pathToFileURL).
|
|
249
|
+
const invokedDirectly =
|
|
250
|
+
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
251
|
+
if (invokedDirectly) process.exit(main());
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/lib/design-context/merge-fragments.mjs — Phase 52 (DesignContext graph), executor B.
|
|
3
|
+
//
|
|
4
|
+
// Deterministic, dependency-free fragment merger. Reads N mapper Fragments
|
|
5
|
+
// (schema_version 52.0) and produces the single merged Graph
|
|
6
|
+
// (schema_version 52.0, NO `mapper` field) written atomically to disk.
|
|
7
|
+
//
|
|
8
|
+
// Merge rules
|
|
9
|
+
// -----------
|
|
10
|
+
// NODES — deduped by `id`:
|
|
11
|
+
// - tags : union (order-stable, de-duplicated)
|
|
12
|
+
// - summary : prefer the first NON-STUB summary (non-empty) seen; an
|
|
13
|
+
// empty-string summary is a stub the LLM phase fills later
|
|
14
|
+
// - complexity : prefer the first non-'moderate' value (the stub default)
|
|
15
|
+
// - other fields : first-writer-wins, but later non-empty values fill gaps
|
|
16
|
+
// left empty/absent by the first writer (e.g. value, layer,
|
|
17
|
+
// subtype). type/name keep the first non-empty.
|
|
18
|
+
//
|
|
19
|
+
// EDGES — deduped by (source,target,type); for each edge we verify BOTH
|
|
20
|
+
// endpoints resolve to a node id that exists in SOME fragment (the merged node
|
|
21
|
+
// set):
|
|
22
|
+
// - both endpoints resolve -> keep the edge (this is cross-fragment
|
|
23
|
+
// "recovery": the a11y fragment can reference a
|
|
24
|
+
// component:* node defined only in the component
|
|
25
|
+
// fragment, and the edge survives the merge);
|
|
26
|
+
// - an endpoint is missing -> DROP the edge and report it. A missing
|
|
27
|
+
// endpoint cannot be recovered (no node with that
|
|
28
|
+
// id exists in any fragment), so the edge is
|
|
29
|
+
// truly dangling.
|
|
30
|
+
//
|
|
31
|
+
// "Could not fix" items (dropped dangling edges; any unresolved id) are written
|
|
32
|
+
// to stderr, one per line, prefixed `could-not-fix:`.
|
|
33
|
+
//
|
|
34
|
+
// Public API:
|
|
35
|
+
// merge(fragments) -> { graph, couldNotFix: string[] } (pure)
|
|
36
|
+
// main() -> reads argv globs / .design/fragments/*.json, atomic-writes graph
|
|
37
|
+
//
|
|
38
|
+
// No network, no deps beyond the sibling atomic-write helper, no top-level
|
|
39
|
+
// Date.now() (stamped in main()).
|
|
40
|
+
|
|
41
|
+
import fs from 'node:fs';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
import { pathToFileURL } from 'node:url';
|
|
44
|
+
import { atomicWriteJson } from '../graph/atomic-write.mjs';
|
|
45
|
+
|
|
46
|
+
const SCHEMA_VERSION = '52.0';
|
|
47
|
+
const DEFAULT_OUT = path.join('.design', 'context-graph.json');
|
|
48
|
+
const DEFAULT_FRAGMENT_DIR = path.join('.design', 'fragments');
|
|
49
|
+
|
|
50
|
+
const STUB_COMPLEXITY = 'moderate';
|
|
51
|
+
const NODE_RESERVED = new Set(['id', 'type', 'name', 'summary', 'tags', 'complexity']);
|
|
52
|
+
|
|
53
|
+
function isNonStubSummary(s) {
|
|
54
|
+
return typeof s === 'string' && s.trim().length > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Merge one node into the accumulator map (dedupe by id). */
|
|
58
|
+
function mergeNode(map, node) {
|
|
59
|
+
if (!node || typeof node.id !== 'string') return;
|
|
60
|
+
const existing = map.get(node.id);
|
|
61
|
+
if (!existing) {
|
|
62
|
+
// Clone so we never mutate the caller's input objects.
|
|
63
|
+
map.set(node.id, {
|
|
64
|
+
id: node.id,
|
|
65
|
+
type: node.type,
|
|
66
|
+
name: node.name,
|
|
67
|
+
summary: isNonStubSummary(node.summary) ? node.summary : '',
|
|
68
|
+
tags: Array.isArray(node.tags) ? [...new Set(node.tags)] : [],
|
|
69
|
+
complexity: node.complexity || STUB_COMPLEXITY,
|
|
70
|
+
...copyExtras({}, node),
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// tags: union, order-stable.
|
|
76
|
+
if (Array.isArray(node.tags) && node.tags.length) {
|
|
77
|
+
const seen = new Set(existing.tags);
|
|
78
|
+
for (const t of node.tags) if (!seen.has(t)) { existing.tags.push(t); seen.add(t); }
|
|
79
|
+
}
|
|
80
|
+
// summary: first non-stub wins.
|
|
81
|
+
if (!isNonStubSummary(existing.summary) && isNonStubSummary(node.summary)) {
|
|
82
|
+
existing.summary = node.summary;
|
|
83
|
+
}
|
|
84
|
+
// complexity: first non-default ('moderate' is the stub) wins.
|
|
85
|
+
if (existing.complexity === STUB_COMPLEXITY && node.complexity && node.complexity !== STUB_COMPLEXITY) {
|
|
86
|
+
existing.complexity = node.complexity;
|
|
87
|
+
}
|
|
88
|
+
// type/name: fill if the first writer left them empty.
|
|
89
|
+
if (!existing.type && node.type) existing.type = node.type;
|
|
90
|
+
if (!existing.name && node.name) existing.name = node.name;
|
|
91
|
+
// extras: fill gaps the first writer did not set.
|
|
92
|
+
copyExtras(existing, node, /* fillOnly */ true);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Copy non-reserved fields from `node` onto `target`. */
|
|
96
|
+
function copyExtras(target, node, fillOnly = false) {
|
|
97
|
+
for (const k of Object.keys(node)) {
|
|
98
|
+
if (NODE_RESERVED.has(k)) continue;
|
|
99
|
+
if (fillOnly && target[k] !== undefined && target[k] !== '' && target[k] !== null) continue;
|
|
100
|
+
target[k] = node[k];
|
|
101
|
+
}
|
|
102
|
+
return target;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pure merge.
|
|
107
|
+
* @param {object[]} fragments array of Fragment objects (each {nodes[], edges[]})
|
|
108
|
+
* @returns {{graph: object, couldNotFix: string[]}}
|
|
109
|
+
*/
|
|
110
|
+
export function merge(fragments) {
|
|
111
|
+
const list = Array.isArray(fragments) ? fragments : [fragments];
|
|
112
|
+
const nodeMap = new Map();
|
|
113
|
+
const couldNotFix = [];
|
|
114
|
+
|
|
115
|
+
// Pass 1: union all nodes (so edge recovery can see ids from any fragment).
|
|
116
|
+
for (const frag of list) {
|
|
117
|
+
if (!frag || !Array.isArray(frag.nodes)) continue;
|
|
118
|
+
for (const n of frag.nodes) mergeNode(nodeMap, n);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Pass 2: dedupe + validate edges against the merged node set.
|
|
122
|
+
const edgeMap = new Map();
|
|
123
|
+
for (const frag of list) {
|
|
124
|
+
if (!frag || !Array.isArray(frag.edges)) continue;
|
|
125
|
+
for (const e of frag.edges) {
|
|
126
|
+
if (!e || typeof e.source !== 'string' || typeof e.target !== 'string' || !e.type) {
|
|
127
|
+
couldNotFix.push(`could-not-fix: malformed edge ${JSON.stringify(e)}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const srcOk = nodeMap.has(e.source);
|
|
131
|
+
const dstOk = nodeMap.has(e.target);
|
|
132
|
+
if (!srcOk || !dstOk) {
|
|
133
|
+
// Truly dangling — no node with that id exists in ANY fragment.
|
|
134
|
+
const missing = [!srcOk ? `source=${e.source}` : null, !dstOk ? `target=${e.target}` : null]
|
|
135
|
+
.filter(Boolean).join(' ');
|
|
136
|
+
couldNotFix.push(`could-not-fix: dropped dangling edge (${e.type}) ${missing}`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Recovered (or always-resolved) — keep, deduped by (source,target,type).
|
|
140
|
+
const key = `${e.source}--${e.type}-->${e.target}`;
|
|
141
|
+
if (!edgeMap.has(key)) {
|
|
142
|
+
edgeMap.set(key, {
|
|
143
|
+
source: e.source,
|
|
144
|
+
target: e.target,
|
|
145
|
+
type: e.type,
|
|
146
|
+
direction: e.direction || 'forward',
|
|
147
|
+
weight: typeof e.weight === 'number' ? e.weight : 0.5,
|
|
148
|
+
});
|
|
149
|
+
} else if (typeof e.weight === 'number') {
|
|
150
|
+
// Keep the max weight when the same edge appears in two fragments.
|
|
151
|
+
const cur = edgeMap.get(key);
|
|
152
|
+
if (e.weight > cur.weight) cur.weight = e.weight;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const graph = {
|
|
158
|
+
schema_version: SCHEMA_VERSION,
|
|
159
|
+
generated_at: '',
|
|
160
|
+
nodes: [...nodeMap.values()],
|
|
161
|
+
edges: [...edgeMap.values()],
|
|
162
|
+
};
|
|
163
|
+
return { graph, couldNotFix };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// CLI helpers.
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/** Resolve argv into a concrete list of fragment file paths. */
|
|
171
|
+
function resolveInputs(argv) {
|
|
172
|
+
// argv shape: [...inputs?] [--out <path>]
|
|
173
|
+
const args = [...argv];
|
|
174
|
+
let out = DEFAULT_OUT;
|
|
175
|
+
const inputs = [];
|
|
176
|
+
for (let i = 0; i < args.length; i++) {
|
|
177
|
+
if (args[i] === '--out' || args[i] === '-o') { out = args[++i] || out; continue; }
|
|
178
|
+
inputs.push(args[i]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let files = [];
|
|
182
|
+
if (inputs.length) {
|
|
183
|
+
for (const inp of inputs) {
|
|
184
|
+
let st;
|
|
185
|
+
try { st = fs.statSync(inp); } catch { continue; }
|
|
186
|
+
if (st.isDirectory()) {
|
|
187
|
+
for (const f of fs.readdirSync(inp)) if (f.endsWith('.json')) files.push(path.join(inp, f));
|
|
188
|
+
} else if (st.isFile()) {
|
|
189
|
+
files.push(inp);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else if (fs.existsSync(DEFAULT_FRAGMENT_DIR)) {
|
|
193
|
+
for (const f of fs.readdirSync(DEFAULT_FRAGMENT_DIR)) {
|
|
194
|
+
if (f.endsWith('.json')) files.push(path.join(DEFAULT_FRAGMENT_DIR, f));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
files = [...new Set(files)].sort(); // deterministic order
|
|
198
|
+
return { files, out };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readFragment(file) {
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
process.stderr.write(`could-not-fix: unreadable fragment ${file} (${err.message})\n`);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** CLI entry: read fragments, merge, stamp generated_at, atomic-write graph. */
|
|
211
|
+
export function main(argv = process.argv.slice(2)) {
|
|
212
|
+
const { files, out } = resolveInputs(argv);
|
|
213
|
+
const fragments = files.map(readFragment).filter(Boolean);
|
|
214
|
+
const { graph, couldNotFix } = merge(fragments);
|
|
215
|
+
graph.generated_at = new Date().toISOString();
|
|
216
|
+
|
|
217
|
+
for (const line of couldNotFix) process.stderr.write(line + '\n');
|
|
218
|
+
|
|
219
|
+
atomicWriteJson(out, graph);
|
|
220
|
+
process.stderr.write(
|
|
221
|
+
`merged ${fragments.length} fragment(s) -> ${out} (${graph.nodes.length} nodes, ${graph.edges.length} edges, ${couldNotFix.length} could-not-fix)\n`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const invokedDirectly =
|
|
226
|
+
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
227
|
+
if (invokedDirectly) main();
|
|
Binary file
|
|
@@ -93,6 +93,14 @@
|
|
|
93
93
|
"user_invocable": true,
|
|
94
94
|
"tools": "Read, Write, Bash, Glob, Grep, AskUserQuestion, ToolSearch"
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
"name": "context",
|
|
98
|
+
"description": "Queries the typed DesignContext graph at .design/context-graph.json - the design-semantic map of tokens, components, variants, states, motion, a11y patterns, screens, layers, and design patterns plus the edges between them. Lists and filters nodes and edges, traces a path between two nodes, finds the consumers of a node, and reports unreachable nodes, dependency cycles, and coverage. Use when the user wants to inspect the design graph, find what depends on a token or component, trace how one node reaches another, or check graph health. Activates for requests involving the design context graph, design dependencies, token consumers, component composition, unreachable design nodes, design cycles, or design coverage.",
|
|
99
|
+
"argument_hint": "[nodes --type X | edges --type Z | path <a> <b> | consumers-of <id> | unreachable | cycles | coverage]",
|
|
100
|
+
"tools": "Read, Bash",
|
|
101
|
+
"user_invocable": true,
|
|
102
|
+
"registered_in_phase": "52"
|
|
103
|
+
},
|
|
96
104
|
{
|
|
97
105
|
"name": "continue",
|
|
98
106
|
"description": "Alias for {{command_prefix}}resume - restore session context from the most recent checkpoint.",
|
|
@@ -244,6 +252,14 @@
|
|
|
244
252
|
"user_invocable": true,
|
|
245
253
|
"tools": "Read, Write, Bash, Grep, Glob"
|
|
246
254
|
},
|
|
255
|
+
{
|
|
256
|
+
"name": "migrate-context",
|
|
257
|
+
"description": "Migrates a pre-Phase-52 project from the flat .design/map/*.md mapper notes to the typed DesignContext graph at .design/context-graph.json. Reads the old map notes, runs the deterministic extract-*.mjs passes to build mapper fragments, merges them with merge-fragments.mjs, validates the result with validate-design-context.cjs, and flags every low-confidence transform for human review before anything is trusted. Read-first and reversible; --dry-run previews the plan without writing. Use when upgrading a project to the DesignContext graph and .design/map/*.md still holds the only structured design notes. Activates for requests involving migrating design maps, building the context graph from old notes, or DesignContext graph migration.",
|
|
258
|
+
"argument_hint": "[--dry-run]",
|
|
259
|
+
"tools": "Read, Write, Bash",
|
|
260
|
+
"user_invocable": true,
|
|
261
|
+
"registered_in_phase": "52"
|
|
262
|
+
},
|
|
247
263
|
{
|
|
248
264
|
"name": "new-cycle",
|
|
249
265
|
"description": "Start a new design cycle. Creates cycle scope in STATE.md, initializes .design/CYCLES.md entry. Each cycle has its own goal and tracks its own decisions/tasks/pipeline runs.",
|
|
@@ -37,7 +37,9 @@ const path = require('node:path');
|
|
|
37
37
|
|
|
38
38
|
const DEFAULT_EXEMPTIONS = new Set(['index.ts', 'shared.ts']);
|
|
39
39
|
const DEFAULT_MAX_LOC = 30;
|
|
40
|
-
|
|
40
|
+
// Raised 12 -> 13 in Phase 52 (DesignContext keystone, D5) for the read-only
|
|
41
|
+
// `gdd_context_query` tool. Mirrors the cap in sdk/mcp/gdd-mcp/tools/index.ts.
|
|
42
|
+
const DEFAULT_TOOL_CAP = 13;
|
|
41
43
|
const TOOL_FILE_GLOB = /^gdd_[a-z0-9_]+\.ts$/;
|
|
42
44
|
|
|
43
45
|
const FORBIDDEN_IMPORT_PATTERNS = Object.freeze([
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "gdd_context_query",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"input": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": ["op"],
|
|
10
|
+
"properties": {
|
|
11
|
+
"op": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"enum": [
|
|
14
|
+
"nodes",
|
|
15
|
+
"edges",
|
|
16
|
+
"path",
|
|
17
|
+
"consumers-of",
|
|
18
|
+
"unreachable",
|
|
19
|
+
"cycles",
|
|
20
|
+
"coverage"
|
|
21
|
+
],
|
|
22
|
+
"description": "Which read-only query to run over the DesignContext graph."
|
|
23
|
+
},
|
|
24
|
+
"type": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Node type (op=nodes) or edge type (op=edges) filter."
|
|
27
|
+
},
|
|
28
|
+
"tag": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Tag filter for op=nodes."
|
|
31
|
+
},
|
|
32
|
+
"from": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Source node id for op=path."
|
|
35
|
+
},
|
|
36
|
+
"to": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "Target node id for op=path."
|
|
39
|
+
},
|
|
40
|
+
"id": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Node id for op=consumers-of."
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"output": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["op", "graph_present", "result"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"op": { "type": "string" },
|
|
51
|
+
"graph_present": { "type": "boolean" },
|
|
52
|
+
"path": { "type": "string" },
|
|
53
|
+
"result": {
|
|
54
|
+
"type": ["array", "object", "null"],
|
|
55
|
+
"description": "Query result; array for nodes/edges/consumers-of/unreachable/cycles, object for path/coverage, null when the graph is absent."
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|