designlang 5.0.0 → 7.0.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 (51) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. package/CHANGELOG.md +43 -0
  6. package/README.md +177 -6
  7. package/bin/design-extract.js +302 -92
  8. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
  9. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
  10. package/package.json +13 -7
  11. package/src/config.js +59 -0
  12. package/src/crawler.js +297 -95
  13. package/src/extractors/a11y-remediation.js +47 -0
  14. package/src/extractors/animations.js +37 -5
  15. package/src/extractors/borders.js +40 -5
  16. package/src/extractors/component-clusters.js +39 -0
  17. package/src/extractors/components.js +77 -1
  18. package/src/extractors/css-health.js +151 -0
  19. package/src/extractors/gradients.js +25 -5
  20. package/src/extractors/scoring.js +20 -1
  21. package/src/extractors/semantic-regions.js +44 -0
  22. package/src/extractors/shadows.js +60 -17
  23. package/src/extractors/spacing.js +31 -2
  24. package/src/extractors/stack-fingerprint.js +88 -0
  25. package/src/extractors/variables.js +20 -1
  26. package/src/formatters/_token-ref.js +44 -0
  27. package/src/formatters/agent-rules.js +116 -0
  28. package/src/formatters/android-compose.js +164 -0
  29. package/src/formatters/dtcg-tokens.js +175 -0
  30. package/src/formatters/figma.js +66 -47
  31. package/src/formatters/flutter-dart.js +130 -0
  32. package/src/formatters/ios-swiftui.js +161 -0
  33. package/src/formatters/markdown.js +25 -0
  34. package/src/formatters/preview.js +65 -22
  35. package/src/formatters/svelte-theme.js +40 -0
  36. package/src/formatters/tailwind.js +57 -4
  37. package/src/formatters/theme.js +134 -0
  38. package/src/formatters/vue-theme.js +44 -0
  39. package/src/formatters/wordpress.js +267 -0
  40. package/src/history.js +8 -1
  41. package/src/index.js +76 -20
  42. package/src/mcp/resources.js +64 -0
  43. package/src/mcp/server.js +110 -0
  44. package/src/mcp/tools.js +149 -0
  45. package/src/utils.js +68 -0
  46. package/tests/cli.test.js +84 -0
  47. package/tests/extractors.test.js +792 -0
  48. package/tests/formatters.test.js +709 -0
  49. package/tests/mcp.test.js +68 -0
  50. package/tests/utils.test.js +413 -0
  51. package/website/app/globals.css +11 -11
@@ -0,0 +1,1121 @@
1
+ # designlang v7.0 Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Ship designlang v7.0 — MCP server + agent rules, DTCG semantic/composite tokens, multi-platform emitters (iOS/Android/Flutter/WordPress), Tailwind/stack fingerprint, CSS health audit, a11y remediation, semantic regions, and reusable component detection.
6
+
7
+ **Architecture:** Extend the existing `extractor → formatter` pipeline. Add 5 extractors, 6 formatters, and an MCP server. DTCG tokens become the new source-of-truth that all platform emitters consume. No LLM or vision deps — everything deterministic.
8
+
9
+ **Tech Stack:** Node.js 20+, ESM, Playwright, `node:test` test runner, `@modelcontextprotocol/sdk` (new).
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-04-18-designlang-v7-design.md`
12
+
13
+ ---
14
+
15
+ ## File Structure (create / modify)
16
+
17
+ ### Create
18
+ - `src/extractors/stack-fingerprint.js`
19
+ - `src/extractors/css-health.js`
20
+ - `src/extractors/semantic-regions.js`
21
+ - `src/extractors/component-clusters.js`
22
+ - `src/extractors/a11y-remediation.js`
23
+ - `src/formatters/dtcg-tokens.js`
24
+ - `src/formatters/ios-swiftui.js`
25
+ - `src/formatters/android-compose.js`
26
+ - `src/formatters/flutter-dart.js`
27
+ - `src/formatters/agent-rules.js`
28
+ - `src/mcp/server.js`
29
+ - `src/mcp/resources.js`
30
+ - `src/mcp/tools.js`
31
+ - `CHANGELOG.md`
32
+
33
+ ### Modify
34
+ - `src/index.js` — wire new extractors, DTCG output, platform dispatch, agent-rules emission
35
+ - `src/formatters/wordpress.js` — extend to a full block-theme skeleton (not just tokens)
36
+ - `src/formatters/tokens.js` — keep as legacy; switch default to dtcg-tokens
37
+ - `src/extractors/accessibility.js` — expose palette + WCAG helpers for remediation reuse
38
+ - `src/extractors/scoring.js` — add cssHealth, specificity, animationCatalog dimensions
39
+ - `bin/design-extract.js` — add `--platforms`, `--emit-agent-rules`, `--tokens-legacy` flags and `designlang mcp` subcommand
40
+ - `tests/extractors.test.js` — add tests for 5 new extractors
41
+ - `tests/formatters.test.js` — add tests for 6 new formatters
42
+ - `package.json` — add `@modelcontextprotocol/sdk`; bump version to 7.0.0
43
+ - `README.md` — document v7 features
44
+ - `AGENTS.md` / `CLAUDE.md` root — mention agent rules emitter
45
+
46
+ ---
47
+
48
+ ## Conventions (inherited)
49
+
50
+ - ESM modules, pure functions per extractor.
51
+ - Each extractor takes raw data (`styles` array and/or `rawData.light.*`) and returns a plain object or `null` on failure.
52
+ - Tests use `node:test` + `assert/strict`. Fixture factory pattern (see `tests/extractors.test.js::makeEl`).
53
+ - Wrap each new extractor call in `safeExtract(fn, ...)` inside `src/index.js`.
54
+ - Commit after each green test. Follow existing commit style (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`).
55
+
56
+ ---
57
+
58
+ ## Chunk 0: Preflight & dependencies
59
+
60
+ ### Task 0.1: Install MCP SDK
61
+
62
+ **Files:** `package.json`, `package-lock.json`
63
+
64
+ - [ ] **Step 1: Install dep**
65
+
66
+ ```bash
67
+ cd /Users/manavaryasingh/claude-plugin/design-extract
68
+ npm install @modelcontextprotocol/sdk
69
+ ```
70
+
71
+ - [ ] **Step 2: Verify install**
72
+
73
+ ```bash
74
+ node -e "import('@modelcontextprotocol/sdk/server/index.js').then(m=>console.log(Object.keys(m)))"
75
+ ```
76
+ Expected: output lists exports including `Server`.
77
+
78
+ - [ ] **Step 3: Commit**
79
+
80
+ ```bash
81
+ git add package.json package-lock.json
82
+ git commit -m "chore: add @modelcontextprotocol/sdk for v7 MCP server"
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Chunk 1: DTCG token formatter (foundation for emitters)
88
+
89
+ Rationale: platform emitters (iOS/Android/Flutter/WordPress/Agent rules/MCP) all consume the semantic token layer. Build this first.
90
+
91
+ ### Task 1.1: Define DTCG writer
92
+
93
+ **Files:**
94
+ - Create: `src/formatters/dtcg-tokens.js`
95
+ - Test: `tests/formatters.test.js` (extend)
96
+
97
+ DTCG spec (v1): each leaf is `{ "$value": <primitive or {composite}>, "$type": "<type>", "$extensions"?: {...} }`. References use `"{path.to.token}"` syntax.
98
+
99
+ - [ ] **Step 1: Write failing tests**
100
+
101
+ Add to `tests/formatters.test.js`:
102
+
103
+ ```js
104
+ import { formatDtcgTokens } from '../src/formatters/dtcg-tokens.js';
105
+
106
+ describe('formatDtcgTokens', () => {
107
+ const minimalDesign = {
108
+ colors: { primary: '#3b82f6', secondary: '#10b981', neutrals: ['#111','#888','#eee'], backgrounds: ['#fff'], text: ['#111'], all: [] },
109
+ typography: { families: ['Inter'], scale: [{ size:'16px', weight:'400', lineHeight:'1.5' }] },
110
+ spacing: { scale: ['4px','8px','16px'], base: '4px' },
111
+ shadows: { values: ['0 1px 2px rgba(0,0,0,0.1)'] },
112
+ borders: { radii: ['4px','8px'] },
113
+ variables: {},
114
+ };
115
+
116
+ it('emits $value/$type for every leaf', () => {
117
+ const out = formatDtcgTokens(minimalDesign);
118
+ assert.equal(out.primitive.color.brand.primary.$value, '#3b82f6');
119
+ assert.equal(out.primitive.color.brand.primary.$type, 'color');
120
+ });
121
+
122
+ it('emits semantic aliases referencing primitives', () => {
123
+ const out = formatDtcgTokens(minimalDesign);
124
+ assert.match(out.semantic.color.action.primary.$value, /^\{primitive\.color\.brand\.primary\}$/);
125
+ assert.equal(out.semantic.color.action.primary.$type, 'color');
126
+ });
127
+
128
+ it('emits composite typography tokens', () => {
129
+ const out = formatDtcgTokens(minimalDesign);
130
+ const body = out.semantic.typography.body;
131
+ assert.equal(body.$type, 'typography');
132
+ assert.equal(body.$value.fontFamily, 'Inter');
133
+ assert.equal(body.$value.fontSize, '16px');
134
+ });
135
+
136
+ it('round-trips through JSON unchanged', () => {
137
+ const out = formatDtcgTokens(minimalDesign);
138
+ assert.deepEqual(JSON.parse(JSON.stringify(out)), out);
139
+ });
140
+ });
141
+ ```
142
+
143
+ - [ ] **Step 2: Run → fail**
144
+
145
+ ```bash
146
+ node --test tests/formatters.test.js
147
+ ```
148
+ Expected: 4 failing tests, `formatDtcgTokens` not found.
149
+
150
+ - [ ] **Step 3: Implement formatter**
151
+
152
+ `src/formatters/dtcg-tokens.js`:
153
+
154
+ ```js
155
+ // DTCG v1 token formatter.
156
+ // Input: design object from extractDesignLanguage.
157
+ // Output: { primitive: {...}, semantic: {...}, $metadata: {...} }
158
+
159
+ function token(value, type, extensions) {
160
+ const t = { $value: value, $type: type };
161
+ if (extensions) t.$extensions = extensions;
162
+ return t;
163
+ }
164
+
165
+ function ref(path) { return `{${path}}`; }
166
+
167
+ function buildPrimitive(design) {
168
+ const brand = design.colors.primary || design.colors.all?.[0] || '#000';
169
+ const secondary = design.colors.secondary || null;
170
+ const neutrals = Object.fromEntries(
171
+ (design.colors.neutrals || []).map((v, i) => [`n${i * 100 + 100}`, token(v, 'color')])
172
+ );
173
+ const color = {
174
+ brand: {
175
+ primary: token(brand, 'color'),
176
+ ...(secondary && { secondary: token(secondary, 'color') }),
177
+ },
178
+ neutral: neutrals,
179
+ background: Object.fromEntries((design.colors.backgrounds || []).map((v, i) => [`bg${i}`, token(v, 'color')])),
180
+ text: Object.fromEntries((design.colors.text || []).map((v, i) => [`text${i}`, token(v, 'color')])),
181
+ };
182
+
183
+ const spacing = Object.fromEntries(
184
+ (design.spacing.scale || []).map((v, i) => [`s${i}`, token(v, 'dimension')])
185
+ );
186
+
187
+ const radius = Object.fromEntries(
188
+ (design.borders.radii || []).map((v, i) => [`r${i}`, token(v, 'dimension')])
189
+ );
190
+
191
+ const shadow = Object.fromEntries(
192
+ (design.shadows.values || []).map((v, i) => [`sh${i}`, token(v, 'shadow')])
193
+ );
194
+
195
+ const fontFamily = Object.fromEntries(
196
+ (design.typography.families || []).map((v, i) => [`f${i}`, token(v, 'fontFamily')])
197
+ );
198
+
199
+ return { color, spacing, radius, shadow, fontFamily };
200
+ }
201
+
202
+ function buildSemantic(design, primitive) {
203
+ const firstFontKey = Object.keys(primitive.fontFamily)[0] || 'f0';
204
+ const firstRadiusKey = Object.keys(primitive.radius)[0] || 'r0';
205
+ const firstShadowKey = Object.keys(primitive.shadow)[0] || 'sh0';
206
+
207
+ const color = {
208
+ action: {
209
+ primary: token(ref('primitive.color.brand.primary'), 'color'),
210
+ },
211
+ surface: {
212
+ default: token(ref('primitive.color.background.bg0'), 'color'),
213
+ },
214
+ text: {
215
+ body: token(ref('primitive.color.text.text0'), 'color'),
216
+ },
217
+ };
218
+ if (primitive.color.brand.secondary) {
219
+ color.action.secondary = token(ref('primitive.color.brand.secondary'), 'color');
220
+ }
221
+
222
+ const typography = {
223
+ body: token({
224
+ fontFamily: design.typography.families?.[0] || 'system-ui',
225
+ fontSize: design.typography.scale?.[0]?.size || '16px',
226
+ fontWeight: design.typography.scale?.[0]?.weight || '400',
227
+ lineHeight: design.typography.scale?.[0]?.lineHeight || '1.5',
228
+ }, 'typography'),
229
+ };
230
+
231
+ const radius = {
232
+ control: token(ref(`primitive.radius.${firstRadiusKey}`), 'dimension'),
233
+ };
234
+
235
+ const shadow = {
236
+ elevated: token(ref(`primitive.shadow.${firstShadowKey}`), 'shadow'),
237
+ };
238
+
239
+ return { color, typography, radius, shadow };
240
+ }
241
+
242
+ export function formatDtcgTokens(design) {
243
+ const primitive = buildPrimitive(design);
244
+ const semantic = buildSemantic(design, primitive);
245
+ return {
246
+ $metadata: {
247
+ generator: 'designlang',
248
+ version: '7.0.0',
249
+ spec: 'https://design-tokens.github.io/community-group/format/',
250
+ source: design.meta?.url,
251
+ generatedAt: design.meta?.timestamp,
252
+ },
253
+ primitive,
254
+ semantic,
255
+ };
256
+ }
257
+ ```
258
+
259
+ - [ ] **Step 4: Run → pass**
260
+
261
+ ```bash
262
+ node --test tests/formatters.test.js
263
+ ```
264
+ Expected: all 4 new tests PASS, no regressions.
265
+
266
+ - [ ] **Step 5: Commit**
267
+
268
+ ```bash
269
+ git add src/formatters/dtcg-tokens.js tests/formatters.test.js
270
+ git commit -m "feat(formatters): add DTCG v1 tokens with primitive + semantic + composites"
271
+ ```
272
+
273
+ ### Task 1.2: Wire DTCG as default, keep legacy behind flag
274
+
275
+ **Files:** `src/index.js`, `bin/design-extract.js`
276
+
277
+ - [ ] **Step 1: Add to index.js export logic**
278
+
279
+ In `src/index.js` (after design is assembled, near where other formatters run / output spec is built) — identify the place where `*-design-tokens.json` is written. Route through `formatDtcgTokens(design)` unless `options.tokensLegacy === true`, in which case use existing `formatTokens`.
280
+
281
+ - [ ] **Step 2: Add `--tokens-legacy` CLI flag**
282
+
283
+ In `bin/design-extract.js` add:
284
+ ```js
285
+ .option('--tokens-legacy', 'Emit pre-v7 flat token JSON (backward compat)')
286
+ ```
287
+ Pass `tokensLegacy` down into extractor options.
288
+
289
+ - [ ] **Step 3: Run full test suite + manual smoke**
290
+
291
+ ```bash
292
+ node --test tests/*.test.js
293
+ node bin/design-extract.js https://example.com -o /tmp/dl-smoke
294
+ head -50 /tmp/dl-smoke/example-com-design-tokens.json
295
+ ```
296
+ Expected: tests pass; JSON shows `$metadata`, `primitive`, `semantic` keys.
297
+
298
+ - [ ] **Step 4: Commit**
299
+
300
+ ```bash
301
+ git add src/index.js bin/design-extract.js
302
+ git commit -m "feat: use DTCG token format by default; legacy via --tokens-legacy"
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Chunk 2: New extractors
308
+
309
+ ### Task 2.1: `stack-fingerprint` extractor
310
+
311
+ **Files:**
312
+ - Create: `src/extractors/stack-fingerprint.js`
313
+ - Test: extend `tests/extractors.test.js`
314
+
315
+ Detects framework, Tailwind, analytics, CDN, fonts host from: page HTML, script URLs, window-global signature strings already present on the crawled DOM sample, meta tags, and class-name patterns. `crawler.js` should be extended to capture `rawData.light.stack = { scripts: [...urls], metas: [...], classNameSample: [...], windowGlobals: [...] }`. Extractor is pure over that payload.
316
+
317
+ - [ ] **Step 1: Extend crawler to collect stack signals**
318
+
319
+ Modify `src/crawler.js`: inside the existing `page.evaluate()` block, also return:
320
+ ```js
321
+ stack: {
322
+ scripts: Array.from(document.scripts).map(s => s.src).filter(Boolean).slice(0, 50),
323
+ metas: Array.from(document.querySelectorAll('meta[name],meta[property]')).map(m => ({ name: m.name||m.getAttribute('property'), content: m.content })).slice(0, 50),
324
+ classNameSample: Array.from(document.querySelectorAll('[class]')).slice(0, 500).map(e => e.className).filter(c => typeof c === 'string'),
325
+ windowGlobals: ['React','Vue','__NEXT_DATA__','__NUXT__','___gatsby','_remixContext','Shopify','wp'].filter(k => typeof window[k] !== 'undefined'),
326
+ }
327
+ ```
328
+
329
+ - [ ] **Step 2: Write failing tests**
330
+
331
+ ```js
332
+ import { extractStackFingerprint } from '../src/extractors/stack-fingerprint.js';
333
+
334
+ describe('extractStackFingerprint', () => {
335
+ it('detects Next.js from __NEXT_DATA__', () => {
336
+ const out = extractStackFingerprint({ windowGlobals: ['__NEXT_DATA__'], scripts: [], metas: [], classNameSample: [] });
337
+ assert.equal(out.framework, 'next');
338
+ });
339
+
340
+ it('detects Tailwind from utility-heavy classNames', () => {
341
+ const out = extractStackFingerprint({
342
+ windowGlobals: [],
343
+ scripts: [],
344
+ metas: [],
345
+ classNameSample: ['flex items-center gap-4 text-sm text-gray-600', 'px-4 py-2 rounded-md bg-blue-500', 'grid grid-cols-3 md:grid-cols-4'],
346
+ });
347
+ assert.equal(out.css.layer, 'tailwind');
348
+ assert.ok(out.css.tailwind.utilities.length > 0);
349
+ });
350
+
351
+ it('returns unknown when nothing matches', () => {
352
+ const out = extractStackFingerprint({ windowGlobals: [], scripts: [], metas: [], classNameSample: ['foo', 'bar'] });
353
+ assert.equal(out.framework, 'unknown');
354
+ assert.equal(out.css.layer, 'unknown');
355
+ });
356
+ });
357
+ ```
358
+
359
+ - [ ] **Step 3: Run → fail**
360
+
361
+ - [ ] **Step 4: Implement**
362
+
363
+ `src/extractors/stack-fingerprint.js`:
364
+
365
+ ```js
366
+ const FRAMEWORK_BY_GLOBAL = {
367
+ '__NEXT_DATA__': 'next',
368
+ '__NUXT__': 'nuxt',
369
+ '___gatsby': 'gatsby',
370
+ '_remixContext': 'remix',
371
+ 'React': 'react',
372
+ 'Vue': 'vue',
373
+ 'Shopify': 'shopify',
374
+ 'wp': 'wordpress',
375
+ };
376
+
377
+ const SCRIPT_PATTERNS = [
378
+ [/_next\/static/, 'next'],
379
+ [/\/nuxt\//, 'nuxt'],
380
+ [/\/astro\//, 'astro'],
381
+ [/\/sveltekit\//, 'sveltekit'],
382
+ [/shopify\./, 'shopify'],
383
+ [/wp-(content|includes)/, 'wordpress'],
384
+ [/webflow\.com/, 'webflow'],
385
+ [/framerusercontent/, 'framer'],
386
+ ];
387
+
388
+ const ANALYTICS = {
389
+ gtag: /googletagmanager\.com|google-analytics/,
390
+ plausible: /plausible\.io/,
391
+ posthog: /posthog\.com/,
392
+ segment: /segment\.(io|com)/,
393
+ mixpanel: /mixpanel/,
394
+ amplitude: /amplitude/,
395
+ hotjar: /hotjar/,
396
+ vercelInsights: /\/_vercel\/insights/,
397
+ };
398
+
399
+ const TAILWIND_UTIL = /(^|\s)(flex|grid|block|inline|hidden|text-(xs|sm|base|lg|xl|\d+xl)|text-(gray|slate|zinc|red|blue|green|amber|neutral|stone)-\d+|bg-(gray|slate|zinc|red|blue|green|amber|neutral|stone)-\d+|p[xy]?-\d+|m[xy]?-\d+|gap-\d+|rounded(-\w+)?|shadow(-\w+)?|items-(start|center|end|baseline|stretch)|justify-(start|center|end|between|around|evenly)|grid-cols-\d+|col-span-\d+)(\s|$)/;
400
+
401
+ function detectFramework(signals) {
402
+ for (const g of signals.windowGlobals || []) {
403
+ if (FRAMEWORK_BY_GLOBAL[g]) return FRAMEWORK_BY_GLOBAL[g];
404
+ }
405
+ for (const s of signals.scripts || []) {
406
+ for (const [re, name] of SCRIPT_PATTERNS) if (re.test(s)) return name;
407
+ }
408
+ return 'unknown';
409
+ }
410
+
411
+ function detectTailwind(signals) {
412
+ const classes = (signals.classNameSample || []).filter(c => typeof c === 'string');
413
+ const hits = classes.filter(c => TAILWIND_UTIL.test(c));
414
+ if (hits.length < Math.max(5, classes.length * 0.1)) return null;
415
+ const utilFreq = new Map();
416
+ for (const c of hits) {
417
+ for (const u of c.split(/\s+/).filter(Boolean)) {
418
+ utilFreq.set(u, (utilFreq.get(u) || 0) + 1);
419
+ }
420
+ }
421
+ const topUtilities = [...utilFreq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 100).map(([u, n]) => ({ utility: u, count: n }));
422
+ return { detected: true, utilities: topUtilities };
423
+ }
424
+
425
+ function detectAnalytics(signals) {
426
+ const found = [];
427
+ for (const [name, re] of Object.entries(ANALYTICS)) {
428
+ if ((signals.scripts || []).some(s => re.test(s))) found.push(name);
429
+ }
430
+ return found;
431
+ }
432
+
433
+ export function extractStackFingerprint(signals = {}) {
434
+ const framework = detectFramework(signals);
435
+ const tailwind = detectTailwind(signals);
436
+ const css = { layer: tailwind ? 'tailwind' : 'unknown', tailwind: tailwind || null };
437
+ return {
438
+ framework,
439
+ css,
440
+ analytics: detectAnalytics(signals),
441
+ detectedFrom: {
442
+ globalCount: (signals.windowGlobals || []).length,
443
+ scriptCount: (signals.scripts || []).length,
444
+ classSampleSize: (signals.classNameSample || []).length,
445
+ },
446
+ };
447
+ }
448
+ ```
449
+
450
+ - [ ] **Step 5: Wire into index.js**
451
+
452
+ In `src/index.js` add import + `design.stack = safeExtract(extractStackFingerprint, rawData.light.stack) || { framework: 'unknown', css: { layer: 'unknown' } };`
453
+
454
+ - [ ] **Step 6: Run full tests + smoke**
455
+
456
+ ```bash
457
+ node --test tests/*.test.js
458
+ node bin/design-extract.js https://vercel.com -o /tmp/dl-stack
459
+ node -e "console.log(JSON.parse(require('fs').readFileSync('/tmp/dl-stack/vercel-com-design-tokens.json'))?.$metadata || 'n/a')"
460
+ ```
461
+ Expected: tests pass; extraction completes.
462
+
463
+ - [ ] **Step 7: Commit**
464
+
465
+ ```bash
466
+ git add src/extractors/stack-fingerprint.js src/crawler.js src/index.js tests/extractors.test.js
467
+ git commit -m "feat(extractors): add stack-fingerprint for framework + Tailwind detection"
468
+ ```
469
+
470
+ ### Task 2.2: `css-health` extractor
471
+
472
+ **Files:**
473
+ - Create: `src/extractors/css-health.js`
474
+ - Modify: `src/crawler.js` (start/stop CSS coverage, capture stylesheet text)
475
+ - Test: extend `tests/extractors.test.js`
476
+
477
+ Computes: specificity distribution, `!important` count, duplicate declarations, unused bytes (from Coverage API), `@keyframes` catalog (name, steps, duration, easing), vendor-prefix flags.
478
+
479
+ - [ ] **Step 1: Extend crawler with CSS coverage**
480
+
481
+ In `src/crawler.js` before `page.goto`: `await page.coverage.startCSSCoverage();`
482
+ After page settles: `const cssCoverage = await page.coverage.stopCSSCoverage();`
483
+ Serialize: `{ coverage: cssCoverage.map(c => ({ url: c.url, text: c.text, totalBytes: c.text.length, ranges: c.ranges })) }` into `rawData.light.cssCoverage`.
484
+
485
+ - [ ] **Step 2: Write failing tests**
486
+
487
+ Minimal unit tests operating on a fake coverage payload:
488
+
489
+ ```js
490
+ import { extractCssHealth } from '../src/extractors/css-health.js';
491
+
492
+ describe('extractCssHealth', () => {
493
+ const payload = [{
494
+ url: 'https://x.com/a.css',
495
+ text: '.a{color:red}.a{color:red}.b{color:blue !important}.c-webkit-foo{color:x}@keyframes fade{0%{opacity:0}100%{opacity:1}}',
496
+ totalBytes: 1000,
497
+ ranges: [{ start: 0, end: 400 }], // 60% unused
498
+ }];
499
+
500
+ it('counts !important', () => {
501
+ const r = extractCssHealth(payload);
502
+ assert.equal(r.importantCount, 1);
503
+ });
504
+
505
+ it('counts duplicate declarations', () => {
506
+ const r = extractCssHealth(payload);
507
+ assert.ok(r.duplicates >= 1);
508
+ });
509
+
510
+ it('reports unused bytes', () => {
511
+ const r = extractCssHealth(payload);
512
+ assert.equal(r.unusedBytes, 600);
513
+ assert.equal(r.usedBytes, 400);
514
+ });
515
+
516
+ it('catalogs keyframes', () => {
517
+ const r = extractCssHealth(payload);
518
+ assert.ok(r.keyframes.some(k => k.name === 'fade'));
519
+ });
520
+ });
521
+ ```
522
+
523
+ - [ ] **Step 3: Run → fail**
524
+
525
+ - [ ] **Step 4: Implement**
526
+
527
+ `src/extractors/css-health.js`: parse text with regex (good enough without a CSS AST dep).
528
+ - `!important`: `/!important/g` count.
529
+ - Duplicates: tokenize `selector { declarations }` and count identical `prop:value` pairs occurring ≥2x per sheet.
530
+ - Unused bytes: `totalBytes − sum(range.end − range.start)`.
531
+ - Keyframes: `/@keyframes\s+([\w-]+)\s*\{([\s\S]*?)\n\}/g`; steps count = number of `%` blocks inside.
532
+ - Vendor-prefix audit: count `-webkit-`, `-moz-`, `-ms-`, `-o-` occurrences.
533
+ - Specificity: for top N selectors, compute `[a,b,c]` triple (ids, classes/attr/pseudo, type).
534
+
535
+ Output shape:
536
+ ```js
537
+ {
538
+ sheets: [{ url, totalBytes, usedBytes, unusedBytes, unusedPercent }],
539
+ usedBytes, unusedBytes, totalBytes,
540
+ importantCount, duplicates,
541
+ vendorPrefixes: { webkit, moz, ms, o },
542
+ keyframes: [{ name, steps, duration?, easing? }],
543
+ specificity: { max:[a,b,c], average:[a,b,c], distribution: [...] },
544
+ issues: ['3 !important rules','2 duplicate declarations', ...],
545
+ }
546
+ ```
547
+
548
+ - [ ] **Step 5: Wire into index.js + scoring**
549
+
550
+ In `src/index.js`: `design.cssHealth = safeExtract(extractCssHealth, rawData.light.cssCoverage) || null;`
551
+
552
+ In `src/extractors/scoring.js` add dimensions `cssHealth` (based on unusedPercent + duplicates + !important count), keep existing dimension keys intact (backward compat).
553
+
554
+ - [ ] **Step 6: Run tests + smoke**
555
+
556
+ - [ ] **Step 7: Commit**
557
+
558
+ ```bash
559
+ git add src/extractors/css-health.js src/extractors/scoring.js src/crawler.js src/index.js tests/extractors.test.js
560
+ git commit -m "feat(extractors): CSS health audit (specificity, !important, unused, keyframes)"
561
+ ```
562
+
563
+ ### Task 2.3: `a11y-remediation` extractor
564
+
565
+ **Files:**
566
+ - Create: `src/extractors/a11y-remediation.js`
567
+ - Modify: `src/extractors/accessibility.js` (export `contrastRatio`, `parseColor` if not already; or re-implement locally)
568
+ - Test: extend `tests/extractors.test.js`
569
+
570
+ For every failing fg/bg pair, search the extracted palette for the color minimizing ΔE (or simpler: closest in sRGB) that passes AA (4.5:1 for normal, 3:1 for large) or AAA (7:1 / 4.5:1).
571
+
572
+ - [ ] **Step 1: Write failing tests**
573
+
574
+ ```js
575
+ import { remediateFailingPairs } from '../src/extractors/a11y-remediation.js';
576
+
577
+ describe('remediateFailingPairs', () => {
578
+ it('suggests a palette color that passes AA', () => {
579
+ const failing = [{ fg: '#777777', bg: '#ffffff', ratio: 3.5, rule: 'AA-normal' }];
580
+ const palette = ['#000000', '#222222', '#555555', '#cccccc'];
581
+ const out = remediateFailingPairs(failing, palette);
582
+ assert.equal(out.length, 1);
583
+ assert.ok(out[0].suggestion);
584
+ assert.ok(out[0].suggestion.newRatio >= 4.5);
585
+ });
586
+
587
+ it('returns null suggestion when no palette color passes', () => {
588
+ const failing = [{ fg: '#eee', bg: '#fff', ratio: 1.1, rule: 'AA-normal' }];
589
+ const palette = ['#dedede'];
590
+ const out = remediateFailingPairs(failing, palette);
591
+ assert.equal(out[0].suggestion, null);
592
+ });
593
+ });
594
+ ```
595
+
596
+ - [ ] **Step 2: Run → fail**
597
+
598
+ - [ ] **Step 3: Implement**
599
+
600
+ Helpers (contrastRatio + relativeLuminance): standard WCAG formulas; short enough to inline here.
601
+
602
+ ```js
603
+ function toRgb(hex) {
604
+ const h = hex.replace('#', '');
605
+ const n = h.length === 3 ? h.split('').map(x => x + x).join('') : h;
606
+ const int = parseInt(n, 16);
607
+ return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
608
+ }
609
+
610
+ function relLum([r, g, b]) {
611
+ const f = c => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); };
612
+ return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
613
+ }
614
+
615
+ function contrast(a, b) {
616
+ const la = relLum(toRgb(a));
617
+ const lb = relLum(toRgb(b));
618
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
619
+ }
620
+
621
+ const THRESHOLDS = { 'AA-normal': 4.5, 'AA-large': 3, 'AAA-normal': 7, 'AAA-large': 4.5 };
622
+
623
+ export function remediateFailingPairs(failing = [], palette = []) {
624
+ return failing.map(p => {
625
+ const target = THRESHOLDS[p.rule] || 4.5;
626
+ // Try replacing fg first (typical), then bg.
627
+ let best = null;
628
+ for (const candidate of palette) {
629
+ const newRatio = contrast(candidate, p.bg);
630
+ if (newRatio >= target && (!best || newRatio > best.newRatio)) {
631
+ best = { replace: 'fg', color: candidate, newRatio: Math.round(newRatio * 100) / 100 };
632
+ }
633
+ }
634
+ return { ...p, suggestion: best };
635
+ });
636
+ }
637
+
638
+ export { contrast as _contrast };
639
+ ```
640
+
641
+ - [ ] **Step 4: Run → pass**
642
+
643
+ - [ ] **Step 5: Wire into index.js**
644
+
645
+ After `extractAccessibility`, call `remediateFailingPairs(design.accessibility.failingPairs, design.colors.all || [])` and attach as `design.accessibility.remediation`.
646
+
647
+ - [ ] **Step 6: Commit**
648
+
649
+ ```bash
650
+ git add src/extractors/a11y-remediation.js src/index.js tests/extractors.test.js
651
+ git commit -m "feat(a11y): remediation suggestions — nearest palette color passing WCAG"
652
+ ```
653
+
654
+ ### Task 2.4: `semantic-regions` extractor
655
+
656
+ **Files:**
657
+ - Create: `src/extractors/semantic-regions.js`
658
+ - Modify: `src/crawler.js` (serialize landmark + heading + bounding info)
659
+ - Test: extend `tests/extractors.test.js`
660
+
661
+ Classifier: start with landmark role (`nav`, `main`, `header`, `footer`, `aside`). For non-landmark sections, use heuristics:
662
+ - `hero` — first `<section>` above the fold with a heading + CTA button.
663
+ - `pricing` — contains ≥3 repeated "card" subtrees and the words "price"/"/mo"/"per month" (regex on text).
664
+ - `testimonials` — repeated subtrees + `<blockquote>` or word "customer"/"said"/"testimonial".
665
+ - `cta` — section with 1–2 buttons and short copy.
666
+ - `features` — repeated icon+heading+paragraph subtrees.
667
+ - Fallback `content`.
668
+
669
+ - [ ] **Step 1: Extend crawler to capture sections**
670
+
671
+ In `page.evaluate()`:
672
+ ```js
673
+ sections: Array.from(document.querySelectorAll('header, nav, main, section, footer, aside, [role="banner"], [role="contentinfo"], [role="complementary"], [role="navigation"]')).map(el => {
674
+ const r = el.getBoundingClientRect();
675
+ return {
676
+ tag: el.tagName.toLowerCase(),
677
+ role: el.getAttribute('role') || '',
678
+ className: el.className || '',
679
+ id: el.id || '',
680
+ text: (el.innerText || '').slice(0, 2000),
681
+ headings: Array.from(el.querySelectorAll('h1,h2,h3')).slice(0, 5).map(h => h.innerText || ''),
682
+ buttonCount: el.querySelectorAll('button, a[role="button"], .btn, [class*="button"]').length,
683
+ cardCount: el.querySelectorAll('article, li, [class*="card"], [class*="item"]').length,
684
+ bounds: { x: r.x, y: r.y, w: r.width, h: r.height },
685
+ };
686
+ })
687
+ ```
688
+
689
+ - [ ] **Step 2: Write failing tests**
690
+
691
+ ```js
692
+ import { extractSemanticRegions } from '../src/extractors/semantic-regions.js';
693
+
694
+ describe('extractSemanticRegions', () => {
695
+ it('labels header as nav', () => {
696
+ const out = extractSemanticRegions([{ tag:'header', role:'', className:'', id:'', text:'Home About', headings:[], buttonCount:3, cardCount:0, bounds:{x:0,y:0,w:1280,h:80} }]);
697
+ assert.equal(out[0].role, 'nav');
698
+ });
699
+
700
+ it('labels section with CTA + heading as hero', () => {
701
+ const out = extractSemanticRegions([{ tag:'section', role:'', className:'hero', id:'', text:'Welcome', headings:['Build better'], buttonCount:2, cardCount:0, bounds:{x:0,y:80,w:1280,h:600} }]);
702
+ assert.equal(out[0].role, 'hero');
703
+ });
704
+
705
+ it('labels pricing based on cards + keyword', () => {
706
+ const out = extractSemanticRegions([{ tag:'section', role:'', className:'', id:'', text:'Basic $9/mo Pro $29/mo Team $99/mo', headings:['Pricing'], buttonCount:3, cardCount:3, bounds:{x:0,y:0,w:1280,h:400} }]);
707
+ assert.equal(out[0].role, 'pricing');
708
+ });
709
+ });
710
+ ```
711
+
712
+ - [ ] **Step 3: Run → fail**
713
+
714
+ - [ ] **Step 4: Implement**
715
+
716
+ ```js
717
+ const KW = {
718
+ pricing: /\b(\$\s*\d|per\s?month|\/mo\b|pricing|free|billed)/i,
719
+ testimonials: /(customer|review|testimonial|said|“|”)/i,
720
+ features: /(feature|benefit|why|what you get)/i,
721
+ cta: /(get started|sign up|try free|start now|request demo|contact sales)/i,
722
+ };
723
+
724
+ function classify(s) {
725
+ const role = (s.role || '').toLowerCase();
726
+ const tag = (s.tag || '').toLowerCase();
727
+ if (tag === 'nav' || role === 'navigation') return 'nav';
728
+ if (tag === 'header' || role === 'banner') return 'nav';
729
+ if (tag === 'footer' || role === 'contentinfo') return 'footer';
730
+ if (tag === 'aside' || role === 'complementary') return 'sidebar';
731
+
732
+ const cls = (s.className || '').toLowerCase();
733
+ const id = (s.id || '').toLowerCase();
734
+ const blob = `${cls} ${id}`;
735
+ if (/hero/.test(blob)) return 'hero';
736
+ if (/pricing/.test(blob) || KW.pricing.test(s.text)) return s.cardCount >= 2 ? 'pricing' : 'pricing';
737
+ if (/testimonial|review/.test(blob) || KW.testimonials.test(s.text)) return 'testimonials';
738
+ if (/features?|grid/.test(blob) && s.cardCount >= 3) return 'features';
739
+ if (KW.features.test(s.text) && s.cardCount >= 3) return 'features';
740
+ if (s.buttonCount <= 2 && s.headings.length && s.text.length < 400 && KW.cta.test(s.text)) return 'cta';
741
+ if (s.headings.length === 1 && s.buttonCount >= 1 && s.bounds.h > 300) return 'hero';
742
+ return 'content';
743
+ }
744
+
745
+ export function extractSemanticRegions(sections = []) {
746
+ return sections.map(s => ({
747
+ role: classify(s),
748
+ tag: s.tag,
749
+ bounds: s.bounds,
750
+ heading: s.headings?.[0] || null,
751
+ buttonCount: s.buttonCount,
752
+ cardCount: s.cardCount,
753
+ className: s.className || null,
754
+ }));
755
+ }
756
+ ```
757
+
758
+ - [ ] **Step 5: Wire into index.js** → `design.regions = safeExtract(extractSemanticRegions, rawData.light.sections) || [];`
759
+
760
+ - [ ] **Step 6: Commit**
761
+
762
+ ```bash
763
+ git add src/extractors/semantic-regions.js src/crawler.js src/index.js tests/extractors.test.js
764
+ git commit -m "feat(extractors): semantic-regions classifier (nav/hero/pricing/footer/…)"
765
+ ```
766
+
767
+ ### Task 2.5: `component-clusters` extractor
768
+
769
+ **Files:**
770
+ - Create: `src/extractors/component-clusters.js`
771
+ - Modify: `src/crawler.js` (include DOM subtree hash signals per element)
772
+ - Test: extend `tests/extractors.test.js`
773
+
774
+ For each candidate element (buttons, cards, inputs, badges, tags) capture:
775
+ - `structuralHash` — `tag>child.tag>...` path string of first 2 levels.
776
+ - `styleVector` — ordered numeric encoding of computed-style subset (padding, bg, color, radius, border, fontSize, fontWeight).
777
+
778
+ Cluster by exact `structuralHash` + cosine similarity on `styleVector` > 0.95.
779
+
780
+ - [ ] **Step 1: Extend crawler candidates capture**
781
+
782
+ In `page.evaluate()` — augment the existing per-element serialization to include `structuralHash` and `styleVector` for buttons/links/inputs/cards.
783
+
784
+ - [ ] **Step 2: Write failing tests**
785
+
786
+ ```js
787
+ import { clusterComponents } from '../src/extractors/component-clusters.js';
788
+
789
+ describe('clusterComponents', () => {
790
+ it('collapses identical instances into one entry', () => {
791
+ const els = Array.from({length:5}, () => ({ kind:'button', structuralHash:'button>span', styleVector:[16,8,0,0], css:{bg:'#f00'} }));
792
+ const out = clusterComponents(els);
793
+ assert.equal(out.length, 1);
794
+ assert.equal(out[0].instanceCount, 5);
795
+ });
796
+
797
+ it('separates variants with different style vectors', () => {
798
+ const els = [
799
+ { kind:'button', structuralHash:'button>span', styleVector:[16,8,0,0], css:{bg:'#f00'} },
800
+ { kind:'button', structuralHash:'button>span', styleVector:[16,8,0,0], css:{bg:'#f00'} },
801
+ { kind:'button', structuralHash:'button>span', styleVector:[12,4,0,0], css:{bg:'#0f0'} },
802
+ ];
803
+ const out = clusterComponents(els);
804
+ assert.equal(out.length, 2);
805
+ });
806
+ });
807
+ ```
808
+
809
+ - [ ] **Step 3: Run → fail**
810
+
811
+ - [ ] **Step 4: Implement**
812
+
813
+ ```js
814
+ function cosine(a, b) {
815
+ let dot = 0, na = 0, nb = 0;
816
+ for (let i = 0; i < a.length; i++) { dot += a[i]*b[i]; na += a[i]*a[i]; nb += b[i]*b[i]; }
817
+ return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
818
+ }
819
+
820
+ export function clusterComponents(elements = [], { threshold = 0.95 } = {}) {
821
+ const byKind = {};
822
+ for (const el of elements) {
823
+ const key = `${el.kind}|${el.structuralHash}`;
824
+ (byKind[key] ||= []).push(el);
825
+ }
826
+ const out = [];
827
+ for (const group of Object.values(byKind)) {
828
+ const variants = [];
829
+ for (const el of group) {
830
+ const match = variants.find(v => cosine(v.example.styleVector, el.styleVector) >= threshold);
831
+ if (match) { match.instanceCount++; }
832
+ else { variants.push({ example: el, instanceCount: 1 }); }
833
+ }
834
+ out.push({
835
+ kind: group[0].kind,
836
+ structuralHash: group[0].structuralHash,
837
+ instanceCount: group.length,
838
+ variants: variants.map(v => ({ css: v.example.css, instanceCount: v.instanceCount })),
839
+ });
840
+ }
841
+ return out;
842
+ }
843
+ ```
844
+
845
+ - [ ] **Step 5: Wire into index.js**
846
+
847
+ `design.componentClusters = safeExtract(clusterComponents, rawData.light.componentCandidates) || [];`
848
+
849
+ - [ ] **Step 6: Update markdown formatter** in `src/formatters/markdown.js` — if `design.componentClusters.length`, render each as "Button — N instances, K variants" with CSS for each variant. Keep existing `components` section for back-compat until v8.
850
+
851
+ - [ ] **Step 7: Commit**
852
+
853
+ ```bash
854
+ git add src/extractors/component-clusters.js src/crawler.js src/index.js src/formatters/markdown.js tests/extractors.test.js
855
+ git commit -m "feat(extractors): reusable component clustering with variant detection"
856
+ ```
857
+
858
+ ---
859
+
860
+ ## Chunk 3: Platform emitters
861
+
862
+ Each emitter reads the **DTCG semantic layer** from `formatDtcgTokens(design)` (never the raw primitive layer directly) to stay consistent across platforms.
863
+
864
+ ### Task 3.1: `ios-swiftui.js` emitter
865
+
866
+ **Files:** Create `src/formatters/ios-swiftui.js`; test in `tests/formatters.test.js`.
867
+
868
+ - [ ] **Step 1: Write failing tests**
869
+
870
+ ```js
871
+ import { formatIosSwiftUI } from '../src/formatters/ios-swiftui.js';
872
+
873
+ it('emits SwiftUI Color extensions from tokens', () => {
874
+ const tokens = { primitive: { color: { brand: { primary: { $value:'#3b82f6', $type:'color' } } } }, semantic: { color: { action: { primary: { $value:'{primitive.color.brand.primary}', $type:'color' } } } } };
875
+ const out = formatIosSwiftUI(tokens);
876
+ assert.match(out, /extension Color/);
877
+ assert.match(out, /actionPrimary/);
878
+ assert.match(out, /0x3b82f6/i);
879
+ });
880
+ ```
881
+
882
+ - [ ] **Step 2: Implement**
883
+
884
+ Resolve `{primitive.x.y}` references; emit:
885
+ ```
886
+ import SwiftUI
887
+ extension Color {
888
+ static let actionPrimary = Color(hex: 0x3B82F6)
889
+ ...
890
+ }
891
+ extension CGFloat {
892
+ static let spacingS0: CGFloat = 4
893
+ ...
894
+ }
895
+ ```
896
+ Include a small `init(hex: UInt32)` helper at top.
897
+
898
+ - [ ] **Step 3: Run + commit**
899
+
900
+ ```bash
901
+ git add src/formatters/ios-swiftui.js tests/formatters.test.js
902
+ git commit -m "feat(formatters): iOS SwiftUI emitter from DTCG semantic tokens"
903
+ ```
904
+
905
+ ### Task 3.2: `android-compose.js` emitter
906
+
907
+ Same pattern — produce `Theme.kt` with `val ActionPrimary = Color(0xFF3B82F6)` and `dimens.xml` + `colors.xml` files. Tests: assert key tokens present.
908
+
909
+ - [ ] Write tests → fail → implement → pass → commit: `feat(formatters): Android Compose emitter`
910
+
911
+ ### Task 3.3: `flutter-dart.js` emitter
912
+
913
+ Produce `theme.dart` with a `class DesignTokens { static const Color actionPrimary = Color(0xFF3B82F6); ... }` and a `ThemeData buildTheme()` helper.
914
+
915
+ - [ ] Write tests → fail → implement → pass → commit: `feat(formatters): Flutter Dart emitter`
916
+
917
+ ### Task 3.4: WordPress block theme (extend existing `wordpress.js`)
918
+
919
+ Existing file likely only emits tokens. Extend to a full skeleton.
920
+
921
+ **Files:** Modify `src/formatters/wordpress.js`.
922
+
923
+ Output is a directory, not a single file. Export a function returning:
924
+ ```js
925
+ {
926
+ 'theme.json': '<string>',
927
+ 'style.css': '<string>',
928
+ 'functions.php': '<string>',
929
+ 'index.php': '<string>',
930
+ 'templates/index.html': '<string>',
931
+ }
932
+ ```
933
+
934
+ Caller writes each file under `<out>/wordpress-theme/`. `theme.json` follows block-editor schema: `{ version: 3, settings: { color: { palette: [...] }, typography: { fontSizes: [...], fontFamilies: [...] }, spacing: { spacingSizes: [...] } } }`.
935
+
936
+ - [ ] Write tests (assert `theme.json` parses and has `settings.color.palette` with extracted colors; `style.css` header contains `Theme Name`)
937
+ - [ ] Implement → pass → commit: `feat(formatters): WordPress block theme skeleton with theme.json`
938
+
939
+ ### Task 3.5: Wire `--platforms` dispatcher
940
+
941
+ **Files:** `src/index.js`, `bin/design-extract.js`.
942
+
943
+ - [ ] Add CLI flag `--platforms <csv>` default `web`.
944
+ - [ ] In `src/index.js` write-out phase: based on flag, call each platform emitter and write files under `<out>/<platform>/…` (or flat with suffix).
945
+ - [ ] Smoke: `designlang https://stripe.com --platforms all -o /tmp/dl-all && ls /tmp/dl-all`
946
+ - [ ] Commit: `feat: --platforms dispatcher for web/ios/android/flutter/wordpress`
947
+
948
+ ---
949
+
950
+ ## Chunk 4: Agent-facing layer
951
+
952
+ ### Task 4.1: `agent-rules.js` emitter
953
+
954
+ **Files:** Create `src/formatters/agent-rules.js`; test in `tests/formatters.test.js`.
955
+
956
+ Produces three strings (+ file-tree descriptor): `cursor.mdc`, `claude-skill.md`, `agents.md`, `claude-md-fragment.md`. All four read the same DTCG tokens + regions + components and template them into prose.
957
+
958
+ - [ ] Write tests (assert each output contains `semantic.color.action.primary`, the source URL, and a "how to use" section).
959
+ - [ ] Implement (use template literals; no frontmatter libs needed).
960
+ - [ ] Wire flag `--emit-agent-rules` in bin; in `src/index.js` write files to `<out>/.cursor/rules/designlang.mdc`, `<out>/.claude/skills/designlang/SKILL.md`, `<out>/CLAUDE.md.fragment`, `<out>/agents.md`.
961
+ - [ ] Commit: `feat: agent rules emitter for Cursor, Claude Code, generic agents`
962
+
963
+ ### Task 4.2: MCP server
964
+
965
+ **Files:**
966
+ - Create: `src/mcp/server.js`, `src/mcp/resources.js`, `src/mcp/tools.js`.
967
+ - Modify: `bin/design-extract.js` (add `mcp` subcommand).
968
+ - Test: `tests/mcp.test.js` (new).
969
+
970
+ Server reads the most recent extraction from `--output-dir` (or auto-discover latest in `./design-extract-output`), and exposes:
971
+
972
+ **Resources:**
973
+ - `designlang://tokens/primitive`
974
+ - `designlang://tokens/semantic`
975
+ - `designlang://regions`
976
+ - `designlang://components`
977
+ - `designlang://health`
978
+
979
+ **Tools:**
980
+ - `search_tokens({ query })` — substring/fuzzy over semantic+primitive names.
981
+ - `find_nearest_color({ hex, level })` — reuses `remediateFailingPairs` engine.
982
+ - `get_region({ name })` — returns a region entry.
983
+ - `get_component({ name, variant? })` — returns a cluster entry.
984
+ - `list_failing_contrast_pairs()` — returns `design.accessibility.remediation`.
985
+
986
+ - [ ] **Step 1: Write tests**
987
+
988
+ Test at the pure-logic layer (pass a fake design object into `resources.js`/`tools.js` and assert outputs). Do not start a real MCP server in CI.
989
+
990
+ ```js
991
+ import { buildResources } from '../src/mcp/resources.js';
992
+ import { buildTools } from '../src/mcp/tools.js';
993
+
994
+ const fakeDesign = { /* minimal shape */ };
995
+ const tokensDtcg = { primitive: { color: { brand: { primary: { $value: '#3b82f6', $type: 'color' } } } }, semantic: { color: { action: { primary: { $value: '{primitive.color.brand.primary}', $type: 'color' } } } } };
996
+
997
+ it('resources.list includes tokens + regions', () => {
998
+ const r = buildResources({ design: fakeDesign, tokens: tokensDtcg });
999
+ assert.ok(r.list().find(x => x.uri === 'designlang://tokens/semantic'));
1000
+ });
1001
+
1002
+ it('search_tokens finds semantic token', async () => {
1003
+ const t = buildTools({ design: fakeDesign, tokens: tokensDtcg });
1004
+ const result = await t.call('search_tokens', { query: 'primary' });
1005
+ assert.ok(JSON.stringify(result).includes('action.primary'));
1006
+ });
1007
+ ```
1008
+
1009
+ - [ ] **Step 2: Implement**
1010
+
1011
+ `src/mcp/server.js` wires `@modelcontextprotocol/sdk/server/index.js` + stdio transport; delegates to `resources.js` and `tools.js` (keep transport glue thin).
1012
+
1013
+ - [ ] **Step 3: Add `mcp` subcommand** in `bin/design-extract.js`:
1014
+
1015
+ ```js
1016
+ program
1017
+ .command('mcp')
1018
+ .description('Launch designlang MCP server over stdio')
1019
+ .option('--output-dir <path>', 'Source extraction directory', './design-extract-output')
1020
+ .action(async (opts) => { await (await import('../src/mcp/server.js')).run(opts); });
1021
+ ```
1022
+
1023
+ - [ ] **Step 4: Manual smoke**
1024
+
1025
+ ```bash
1026
+ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}' | node bin/design-extract.js mcp --output-dir ./design-extract-output
1027
+ ```
1028
+ Expected: MCP initialize response over stdout.
1029
+
1030
+ - [ ] **Step 5: Commit**
1031
+
1032
+ ```bash
1033
+ git add src/mcp/ bin/design-extract.js tests/mcp.test.js
1034
+ git commit -m "feat(mcp): stdio MCP server exposing tokens/regions/components/tools"
1035
+ ```
1036
+
1037
+ ---
1038
+
1039
+ ## Chunk 5: Docs, changelog, release
1040
+
1041
+ ### Task 5.1: Update README
1042
+
1043
+ **Files:** `README.md`.
1044
+
1045
+ - [ ] Add sections under "What Makes This Different":
1046
+ - 17. MCP Server (example `.cursor/rules` snippet)
1047
+ - 18. Multi-platform (iOS/Android/Flutter/WordPress) with one-line examples
1048
+ - 19. Stack Fingerprint + Tailwind mode
1049
+ - 20. CSS Health Audit
1050
+ - 21. A11y Remediation
1051
+ - [ ] Update "All Features" table with new flags + command.
1052
+ - [ ] Update CLI reference block.
1053
+ - [ ] Commit: `docs(readme): v7 features — MCP, platforms, health, remediation, regions`
1054
+
1055
+ ### Task 5.2: Write CHANGELOG
1056
+
1057
+ **Files:** Create `CHANGELOG.md`.
1058
+
1059
+ Include:
1060
+ - Breaking: token JSON now DTCG format. Migration: pass `--tokens-legacy` or upgrade consumers.
1061
+ - Additions: all 10 features listed with flag or command.
1062
+ - Full migration snippet (before/after token shape).
1063
+
1064
+ - [ ] Commit: `docs: CHANGELOG for v7.0`
1065
+
1066
+ ### Task 5.3: Version bump + tag + publish
1067
+
1068
+ - [ ] **Step 1: Bump version**
1069
+
1070
+ ```bash
1071
+ npm version 7.0.0 --no-git-tag-version
1072
+ ```
1073
+ (creates/updates version; no auto-commit so we control the commit message.)
1074
+
1075
+ - [ ] **Step 2: Final verification (human-in-the-loop)**
1076
+
1077
+ ```bash
1078
+ npm test
1079
+ node bin/design-extract.js https://vercel.com --platforms all --emit-agent-rules -o /tmp/dl-release-smoke
1080
+ ls -R /tmp/dl-release-smoke | head -40
1081
+ ```
1082
+ Expected: tests pass, smoke generates web + iOS + Android + Flutter + WordPress outputs + agent rules.
1083
+
1084
+ - [ ] **Step 3: Commit version bump**
1085
+
1086
+ ```bash
1087
+ git add package.json package-lock.json
1088
+ git commit -m "chore(release): v7.0.0"
1089
+ git tag v7.0.0
1090
+ ```
1091
+
1092
+ - [ ] **Step 4: Publish to npm** (REQUIRES USER APPROVAL — interactive)
1093
+
1094
+ ```bash
1095
+ npm publish --access public
1096
+ ```
1097
+ If `npm whoami` fails, prompt user to run `npm login` first. Do not use `--no-verify` or skip 2FA.
1098
+
1099
+ - [ ] **Step 5: Push tags**
1100
+
1101
+ ```bash
1102
+ git push && git push --tags
1103
+ ```
1104
+
1105
+ ---
1106
+
1107
+ ## Deferred (out of scope for v7.0)
1108
+
1109
+ - Bidirectional Figma Variables sync
1110
+ - Full JSX/Vue/Svelte component codegen
1111
+ - Versioned auto-published npm token package
1112
+ - Hosted shareable reports
1113
+ - Website revamp + "Try it free" backend service
1114
+ - CMS content-model extraction
1115
+ - Storybook/Chromatic integration
1116
+
1117
+ ## Skills referenced
1118
+
1119
+ - @superpowers:test-driven-development — every feature task is test-first
1120
+ - @superpowers:verification-before-completion — run full suite + smoke before marking a task complete
1121
+ - @superpowers:using-git-worktrees — consider a worktree if doing multiple features in parallel