@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +93 -0
- package/README.md +4 -0
- package/SKILL.md +4 -1
- 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-reflector.md +33 -0
- 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/apply-reflections/SKILL.md +17 -0
- package/dist/claude-code/.claude/skills/context/SKILL.md +137 -0
- package/dist/claude-code/.claude/skills/extract-learnings/SKILL.md +16 -0
- package/dist/claude-code/.claude/skills/instinct/SKILL.md +111 -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/hooks/gdd-decision-injector.js +115 -6
- package/package.json +3 -2
- package/reference/design-context-schema.md +159 -0
- package/reference/design-context-tag-vocab.md +82 -0
- package/reference/instinct-format.md +120 -0
- package/reference/registry.json +21 -0
- package/reference/schemas/design-context.schema.json +130 -0
- package/reference/schemas/events.schema.json +1 -1
- package/reference/schemas/instinct.schema.json +91 -0
- package/reference/schemas/mcp-gdd-tools.schema.json +34 -1
- package/reference/skill-graph.md +4 -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/instinct-store.cjs +677 -0
- package/scripts/lib/manifest/skills.json +24 -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/apply-reflections/SKILL.md +17 -0
- package/skills/context/SKILL.md +137 -0
- package/skills/extract-learnings/SKILL.md +16 -0
- package/skills/instinct/SKILL.md +111 -0
- package/skills/migrate-context/SKILL.md +123 -0
- package/skills/progress/SKILL.md +4 -0
|
@@ -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
|