bigpowers 2.42.1 → 2.43.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 (32) hide show
  1. package/.pi/package.json +2 -2
  2. package/.pi/prompts/extract-design.md +131 -0
  3. package/.pi/skills/extract-design/SKILL.md +133 -0
  4. package/CHANGELOG.md +7 -0
  5. package/SKILL-INDEX.md +2 -2
  6. package/extract-design/REFERENCE.md +52 -0
  7. package/extract-design/SKILL.md +77 -0
  8. package/extract-design/extract-design.feature +23 -0
  9. package/extract-design/scripts/classify-colors.js +21 -0
  10. package/extract-design/scripts/classify-rounded.js +10 -0
  11. package/extract-design/scripts/classify-spacing.js +14 -0
  12. package/extract-design/scripts/classify-typography.js +9 -0
  13. package/extract-design/scripts/collect-styles.js +149 -0
  14. package/extract-design/scripts/detect-components.js +12 -0
  15. package/extract-design/scripts/extract.js +306 -0
  16. package/extract-design/scripts/generate-prose.js +38 -0
  17. package/extract-design/scripts/lib/browser.js +206 -0
  18. package/extract-design/scripts/lib/constants.js +9 -0
  19. package/extract-design/scripts/lib/logging.js +8 -0
  20. package/extract-design/scripts/lib/retry.js +13 -0
  21. package/extract-design/scripts/lib/state.js +15 -0
  22. package/extract-design/scripts/lib/validator.js +11 -0
  23. package/extract-design/scripts/write-designd.js +16 -0
  24. package/extract-design/tests/fixtures/glassmorphism-dark/expected-tokens.json +1 -0
  25. package/extract-design/tests/fixtures/glassmorphism-dark/prototype.html +7 -0
  26. package/extract-design/tests/fixtures/minimal-no-styles/expected-tokens.json +1 -0
  27. package/extract-design/tests/fixtures/minimal-no-styles/prototype.html +2 -0
  28. package/extract-design/tests/fixtures/swiss-grid/expected-tokens.json +1 -0
  29. package/extract-design/tests/fixtures/swiss-grid/prototype.html +6 -0
  30. package/extract-design/tests/test-extraction.js +315 -0
  31. package/package.json +1 -1
  32. package/skills-lock.json +5 -0
package/.pi/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.42.1",
4
- "description": "71 skills — 70 agent skills for spec-driven, test-first software development by solo developers",
3
+ "version": "2.43.0",
4
+ "description": "72 skills — 70 agent skills for spec-driven, test-first software development by solo developers",
5
5
  "keywords": [
6
6
  "pi-package"
7
7
  ],
@@ -0,0 +1,131 @@
1
+ ---
2
+ description: "Extract a Google DESIGN.md file from an HTML prototype (claude.ai/design or any styled page) using Puppeteer, producing machine-readable tokens and AI-generated prose. Use when the user has an HTML prototype and wants a DESIGN.md to anchor their project's visual identity, or when seed-conventions has just scaffolded a new project."
3
+ ---
4
+
5
+
6
+ # Extract DESIGN.md from HTML
7
+
8
+ > **HARD GATE** — Do NOT write DESIGN.md without Puppeteer dual-pass extraction. Tokens from static HTML (Cheerio, regex, string scanning) are invalid — they miss cascade, custom properties, and Tailwind resolution.
9
+ >
10
+ > **HARD GATE** — Do NOT claim certainty where evidence is thin. Low-confidence color roles, component classifications, and prose assertions MUST be flagged with `<!-- AGENT NOTE: uncertain — validate during grill-me. Evidence: [what was observed] -->`.
11
+ >
12
+ > **HARD GATE** — Do NOT ship DESIGN.md without running `npx @google/design.md lint`. Unvalidated output is unverified output. If lint is unavailable (offline), flag prominently in terminal and in DESIGN.md prose.
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # First run — extract from HTML prototype
18
+ node extract-design/scripts/extract.js --source ./prototype.html
19
+
20
+ # From a published URL
21
+ node extract-design/scripts/extract.js --source https://my-prototype.example.com
22
+
23
+ # With a custom name
24
+ node extract-design/scripts/extract.js --source ./proto.html --name "My Design System"
25
+
26
+ # Update — re-extract from new HTML, diff against existing
27
+ node extract-design/scripts/extract.js --source ./proto-v2.html
28
+
29
+ # Lint-only — validate existing DESIGN.md without re-extraction
30
+ node extract-design/scripts/extract.js --lint-only
31
+ ```
32
+
33
+ ## Flow
34
+
35
+ 1. **Launch Puppeteer** — dual-pass (light + dark) with retry + timeout. CI flags: `--headless=new --no-sandbox --disable-gpu --disable-dbus --use-gl=angle --use-angle=swiftshader`.
36
+ 2. **Collect styles** — `page.evaluate()` collects computed styles from every element. Returns raw JSON to Node.js. Browser = sensor; Node = brain.
37
+ 3. **Classify tokens** — modular pipeline: colors (Material 3 roles), typography (scale detection), spacing (tolerance GCD), rounded (clustering), components (visual signature + pseudo-state variants).
38
+ 4. **Generate prose** — AI heuristics produce all 8 DESIGN.md sections. Overview and Do's/Don'ts flagged with agent notes.
39
+ 5. **Write + validate** — serialize to `specs/tech-architecture/DESIGN_LATEST.md`, run `npx @google/design.md lint`, report to terminal.
40
+ 6. **Handoff** — writes `handoff.next_skill: grill-me` to `specs/state.yaml` with uncertain decisions context.
41
+
42
+ ## Inputs
43
+
44
+ | Parameter | Required | Description |
45
+ |-----------|----------|-------------|
46
+ | `--source <file\|url>` | First run: yes. Update: optional | HTML prototype path or URL |
47
+ | `--name <string>` | No | Design system name (defaults to `<title>` or directory name) |
48
+ | `--lint-only` | No | Validate existing DESIGN.md without re-extraction |
49
+
50
+ ## Output
51
+
52
+ - `specs/tech-architecture/DESIGN_LATEST.md` — replaces `DESIGN_PLAN_LATEST.md` as the canonical design artifact
53
+ - Terminal summary: token counts, component count, lint result, uncertain decisions
54
+ - Structured JSON log to stderr: extraction events, timing, counts
55
+ - `specs/state.yaml` → `handoff.next_skill: grill-me` with context
56
+
57
+ ## Error Tiers
58
+
59
+ | Tier | Condition | Response |
60
+ |------|-----------|----------|
61
+ | Fatal | No Chrome, page load timeout after retries | Exit non-zero, suggest fixes |
62
+ | Degraded | Zero colors, zero typography, SPA shell | Write DESIGN.md with degradation warning |
63
+ | Warned | Lint errors, uncertain decisions | Write DESIGN.md, flag in terminal, hand off to grill-me |
64
+
65
+ ## Dependencies
66
+
67
+ - **Puppeteer** (Chrome binary) — wrapped behind `BrowserExtractor` interface for testability
68
+ - **`@google/design.md`** (soft, via `npx`) — wrapped behind `DesignValidator` interface. Warns and skips if offline.
69
+
70
+ ## verify
71
+
72
+ ```bash
73
+ node extract-design/tests/test-extraction.js
74
+ ```
75
+
76
+ See [REFERENCE.md](REFERENCE.md) for extraction algorithms and heuristics.
77
+
78
+ ---
79
+
80
+ # Extract Design — Reference
81
+
82
+ ## Extraction Algorithms
83
+
84
+ ### Color Classification
85
+ 1. Collect unique computed `background-color`, `color`, `border-color` from every DOM element.
86
+ 2. Count frequency and context (text vs. bg vs. border vs. button).
87
+ 3. Classify using Material 3 roles: surface = largest-area bg, on-surface = most-used text, tertiary = highest-saturation on CTAs, error = reddish.
88
+ 4. Surface container levels: luminance-ordered. Hard-cap at 3 unless data supports 5.
89
+ 5. Low-confidence → `<!-- AGENT NOTE -->` in Colors prose.
90
+
91
+ ### Typography Classification
92
+ 1. Collect unique (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing) tuples.
93
+ 2. Cluster by fontSize (±2px) → type scale.
94
+ 3. Detect heading hierarchy from HTML tag + computed size.
95
+ 4. Name levels: display-lg, headline-lg/md/sm, title-lg, body-lg/md/sm, label-lg/md/sm.
96
+ 5. Parse `<link>` and `@font-face`; warn if computed font differs from declared.
97
+
98
+ ### Spacing Classification
99
+ 1. Collect unique padding, margin, gap values. Filter: ignore values < 3 occurrences.
100
+ 2. Compute tolerance-based GCD (0.5px tolerance).
101
+ 3. Declare base unit. Detect half-step if gcd/2 values present ≥3 times.
102
+ 4. Map: unit×1 → sm, ×2 → md, ×4 → lg, ×8 → xl.
103
+
104
+ ### Rounded Classification
105
+ 1. Collect unique border-radius values. Cluster (1px tolerance).
106
+ 2. Name: none (0), sm (smallest), md (median), lg (large), xl (largest), full (9999px).
107
+
108
+ ### Component Detection
109
+ - Button: w<300px, h<64px, bg+radius set, short text. cursor:pointer is bonus.
110
+ - Card: bg+radius+padding set, larger area.
111
+ - Input: `<input>` or `<textarea>` tag.
112
+ - Pseudo-state variants: force :hover, :active, :focus for high-confidence components.
113
+
114
+ ### Prose Generation
115
+ Generates all 8 DESIGN.md sections. Overview and Do's/Don'ts flagged with `<!-- AGENT NOTE: Generated from visual analysis. Grill-me should validate. -->`.
116
+
117
+ ## Material 3 Token Conventions
118
+ Colors: primary, on-primary, secondary, tertiary, error, surface, on-surface, surface-container-*, outline, background, on-background.
119
+ Typography: display-lg/md/sm, headline-lg/md/sm, title-lg/md/sm, body-lg/md/sm, label-lg/md/sm.
120
+ Rounded: none, xs, sm, md, lg, xl, full.
121
+
122
+ ## CI Compatibility
123
+ Chrome flags: `--headless=new --no-sandbox --disable-gpu --disable-dbus --use-gl=angle --use-angle=swiftshader`
124
+
125
+ ## Defensive Code
126
+ Retry with backoff (3 attempts, 1s/2s/4s), Timeout (30s page load, 10s pseudo-state), Graceful degradation (Tier 2 on empty extraction).
127
+
128
+ ## Known Risks
129
+ - DESIGN.md spec is alpha. Skill pins to current spec.
130
+ - Puppeteer is heavy (~300MB Chrome). Document requirement clearly.
131
+ - Prose quality depends on AI heuristics. grill-me is the validation gate.
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: extract-design
3
+ description: "\"Extract a Google DESIGN.md file from an HTML prototype (claude.ai/design or any styled page) using Puppeteer, producing machine-readable tokens and AI-generated prose. Use when the user has an HTML prototype and wants a DESIGN.md to anchor their project's visual identity, or when seed-conventions has just scaffolded a new project.\""
4
+ model: sonnet
5
+ ---
6
+
7
+
8
+ # Extract DESIGN.md from HTML
9
+
10
+ > **HARD GATE** — Do NOT write DESIGN.md without Puppeteer dual-pass extraction. Tokens from static HTML (Cheerio, regex, string scanning) are invalid — they miss cascade, custom properties, and Tailwind resolution.
11
+ >
12
+ > **HARD GATE** — Do NOT claim certainty where evidence is thin. Low-confidence color roles, component classifications, and prose assertions MUST be flagged with `<!-- AGENT NOTE: uncertain — validate during grill-me. Evidence: [what was observed] -->`.
13
+ >
14
+ > **HARD GATE** — Do NOT ship DESIGN.md without running `npx @google/design.md lint`. Unvalidated output is unverified output. If lint is unavailable (offline), flag prominently in terminal and in DESIGN.md prose.
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ # First run — extract from HTML prototype
20
+ node extract-design/scripts/extract.js --source ./prototype.html
21
+
22
+ # From a published URL
23
+ node extract-design/scripts/extract.js --source https://my-prototype.example.com
24
+
25
+ # With a custom name
26
+ node extract-design/scripts/extract.js --source ./proto.html --name "My Design System"
27
+
28
+ # Update — re-extract from new HTML, diff against existing
29
+ node extract-design/scripts/extract.js --source ./proto-v2.html
30
+
31
+ # Lint-only — validate existing DESIGN.md without re-extraction
32
+ node extract-design/scripts/extract.js --lint-only
33
+ ```
34
+
35
+ ## Flow
36
+
37
+ 1. **Launch Puppeteer** — dual-pass (light + dark) with retry + timeout. CI flags: `--headless=new --no-sandbox --disable-gpu --disable-dbus --use-gl=angle --use-angle=swiftshader`.
38
+ 2. **Collect styles** — `page.evaluate()` collects computed styles from every element. Returns raw JSON to Node.js. Browser = sensor; Node = brain.
39
+ 3. **Classify tokens** — modular pipeline: colors (Material 3 roles), typography (scale detection), spacing (tolerance GCD), rounded (clustering), components (visual signature + pseudo-state variants).
40
+ 4. **Generate prose** — AI heuristics produce all 8 DESIGN.md sections. Overview and Do's/Don'ts flagged with agent notes.
41
+ 5. **Write + validate** — serialize to `specs/tech-architecture/DESIGN_LATEST.md`, run `npx @google/design.md lint`, report to terminal.
42
+ 6. **Handoff** — writes `handoff.next_skill: grill-me` to `specs/state.yaml` with uncertain decisions context.
43
+
44
+ ## Inputs
45
+
46
+ | Parameter | Required | Description |
47
+ |-----------|----------|-------------|
48
+ | `--source <file\|url>` | First run: yes. Update: optional | HTML prototype path or URL |
49
+ | `--name <string>` | No | Design system name (defaults to `<title>` or directory name) |
50
+ | `--lint-only` | No | Validate existing DESIGN.md without re-extraction |
51
+
52
+ ## Output
53
+
54
+ - `specs/tech-architecture/DESIGN_LATEST.md` — replaces `DESIGN_PLAN_LATEST.md` as the canonical design artifact
55
+ - Terminal summary: token counts, component count, lint result, uncertain decisions
56
+ - Structured JSON log to stderr: extraction events, timing, counts
57
+ - `specs/state.yaml` → `handoff.next_skill: grill-me` with context
58
+
59
+ ## Error Tiers
60
+
61
+ | Tier | Condition | Response |
62
+ |------|-----------|----------|
63
+ | Fatal | No Chrome, page load timeout after retries | Exit non-zero, suggest fixes |
64
+ | Degraded | Zero colors, zero typography, SPA shell | Write DESIGN.md with degradation warning |
65
+ | Warned | Lint errors, uncertain decisions | Write DESIGN.md, flag in terminal, hand off to grill-me |
66
+
67
+ ## Dependencies
68
+
69
+ - **Puppeteer** (Chrome binary) — wrapped behind `BrowserExtractor` interface for testability
70
+ - **`@google/design.md`** (soft, via `npx`) — wrapped behind `DesignValidator` interface. Warns and skips if offline.
71
+
72
+ ## verify
73
+
74
+ ```bash
75
+ node extract-design/tests/test-extraction.js
76
+ ```
77
+
78
+ See [REFERENCE.md](REFERENCE.md) for extraction algorithms and heuristics.
79
+
80
+ ---
81
+
82
+ # Extract Design — Reference
83
+
84
+ ## Extraction Algorithms
85
+
86
+ ### Color Classification
87
+ 1. Collect unique computed `background-color`, `color`, `border-color` from every DOM element.
88
+ 2. Count frequency and context (text vs. bg vs. border vs. button).
89
+ 3. Classify using Material 3 roles: surface = largest-area bg, on-surface = most-used text, tertiary = highest-saturation on CTAs, error = reddish.
90
+ 4. Surface container levels: luminance-ordered. Hard-cap at 3 unless data supports 5.
91
+ 5. Low-confidence → `<!-- AGENT NOTE -->` in Colors prose.
92
+
93
+ ### Typography Classification
94
+ 1. Collect unique (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing) tuples.
95
+ 2. Cluster by fontSize (±2px) → type scale.
96
+ 3. Detect heading hierarchy from HTML tag + computed size.
97
+ 4. Name levels: display-lg, headline-lg/md/sm, title-lg, body-lg/md/sm, label-lg/md/sm.
98
+ 5. Parse `<link>` and `@font-face`; warn if computed font differs from declared.
99
+
100
+ ### Spacing Classification
101
+ 1. Collect unique padding, margin, gap values. Filter: ignore values < 3 occurrences.
102
+ 2. Compute tolerance-based GCD (0.5px tolerance).
103
+ 3. Declare base unit. Detect half-step if gcd/2 values present ≥3 times.
104
+ 4. Map: unit×1 → sm, ×2 → md, ×4 → lg, ×8 → xl.
105
+
106
+ ### Rounded Classification
107
+ 1. Collect unique border-radius values. Cluster (1px tolerance).
108
+ 2. Name: none (0), sm (smallest), md (median), lg (large), xl (largest), full (9999px).
109
+
110
+ ### Component Detection
111
+ - Button: w<300px, h<64px, bg+radius set, short text. cursor:pointer is bonus.
112
+ - Card: bg+radius+padding set, larger area.
113
+ - Input: `<input>` or `<textarea>` tag.
114
+ - Pseudo-state variants: force :hover, :active, :focus for high-confidence components.
115
+
116
+ ### Prose Generation
117
+ Generates all 8 DESIGN.md sections. Overview and Do's/Don'ts flagged with `<!-- AGENT NOTE: Generated from visual analysis. Grill-me should validate. -->`.
118
+
119
+ ## Material 3 Token Conventions
120
+ Colors: primary, on-primary, secondary, tertiary, error, surface, on-surface, surface-container-*, outline, background, on-background.
121
+ Typography: display-lg/md/sm, headline-lg/md/sm, title-lg/md/sm, body-lg/md/sm, label-lg/md/sm.
122
+ Rounded: none, xs, sm, md, lg, xl, full.
123
+
124
+ ## CI Compatibility
125
+ Chrome flags: `--headless=new --no-sandbox --disable-gpu --disable-dbus --use-gl=angle --use-angle=swiftshader`
126
+
127
+ ## Defensive Code
128
+ Retry with backoff (3 attempts, 1s/2s/4s), Timeout (30s page load, 10s pseudo-state), Graceful degradation (Tier 2 on empty extraction).
129
+
130
+ ## Known Risks
131
+ - DESIGN.md spec is alpha. Skill pins to current spec.
132
+ - Puppeteer is heavy (~300MB Chrome). Document requirement clearly.
133
+ - Prose quality depends on AI heuristics. grill-me is the validation gate.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.43.0](https://github.com/danielvm-git/bigpowers/compare/v2.42.1...v2.43.0) (2026-06-29)
2
+
3
+
4
+ ### Features
5
+
6
+ * **skills:** add extract-design skill ([bda7ceb](https://github.com/danielvm-git/bigpowers/commit/bda7ceb113cba610d438dbb95af150b424e674b4))
7
+
1
8
  ## [2.42.1](https://github.com/danielvm-git/bigpowers/compare/v2.42.0...v2.42.1) (2026-06-29)
2
9
 
3
10
 
package/SKILL-INDEX.md CHANGED
@@ -3,8 +3,8 @@
3
3
  > **DO NOT EDIT** — This file is auto-generated by `scripts/generate-skill-index.sh`.
4
4
  > Edit `SKILL.md` source files or `skills-lock.json` instead. Run `bash scripts/sync-skills.sh` to regenerate.
5
5
 
6
- **Generated:** 2026-06-29T16:11:17Z
7
- **Skills:** 71
6
+ **Generated:** 2026-06-29T16:34:20Z
7
+ **Skills:** 72
8
8
 
9
9
  ---
10
10
 
@@ -0,0 +1,52 @@
1
+ # Extract Design — Reference
2
+
3
+ ## Extraction Algorithms
4
+
5
+ ### Color Classification
6
+ 1. Collect unique computed `background-color`, `color`, `border-color` from every DOM element.
7
+ 2. Count frequency and context (text vs. bg vs. border vs. button).
8
+ 3. Classify using Material 3 roles: surface = largest-area bg, on-surface = most-used text, tertiary = highest-saturation on CTAs, error = reddish.
9
+ 4. Surface container levels: luminance-ordered. Hard-cap at 3 unless data supports 5.
10
+ 5. Low-confidence → `<!-- AGENT NOTE -->` in Colors prose.
11
+
12
+ ### Typography Classification
13
+ 1. Collect unique (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing) tuples.
14
+ 2. Cluster by fontSize (±2px) → type scale.
15
+ 3. Detect heading hierarchy from HTML tag + computed size.
16
+ 4. Name levels: display-lg, headline-lg/md/sm, title-lg, body-lg/md/sm, label-lg/md/sm.
17
+ 5. Parse `<link>` and `@font-face`; warn if computed font differs from declared.
18
+
19
+ ### Spacing Classification
20
+ 1. Collect unique padding, margin, gap values. Filter: ignore values < 3 occurrences.
21
+ 2. Compute tolerance-based GCD (0.5px tolerance).
22
+ 3. Declare base unit. Detect half-step if gcd/2 values present ≥3 times.
23
+ 4. Map: unit×1 → sm, ×2 → md, ×4 → lg, ×8 → xl.
24
+
25
+ ### Rounded Classification
26
+ 1. Collect unique border-radius values. Cluster (1px tolerance).
27
+ 2. Name: none (0), sm (smallest), md (median), lg (large), xl (largest), full (9999px).
28
+
29
+ ### Component Detection
30
+ - Button: w<300px, h<64px, bg+radius set, short text. cursor:pointer is bonus.
31
+ - Card: bg+radius+padding set, larger area.
32
+ - Input: `<input>` or `<textarea>` tag.
33
+ - Pseudo-state variants: force :hover, :active, :focus for high-confidence components.
34
+
35
+ ### Prose Generation
36
+ Generates all 8 DESIGN.md sections. Overview and Do's/Don'ts flagged with `<!-- AGENT NOTE: Generated from visual analysis. Grill-me should validate. -->`.
37
+
38
+ ## Material 3 Token Conventions
39
+ Colors: primary, on-primary, secondary, tertiary, error, surface, on-surface, surface-container-*, outline, background, on-background.
40
+ Typography: display-lg/md/sm, headline-lg/md/sm, title-lg/md/sm, body-lg/md/sm, label-lg/md/sm.
41
+ Rounded: none, xs, sm, md, lg, xl, full.
42
+
43
+ ## CI Compatibility
44
+ Chrome flags: `--headless=new --no-sandbox --disable-gpu --disable-dbus --use-gl=angle --use-angle=swiftshader`
45
+
46
+ ## Defensive Code
47
+ Retry with backoff (3 attempts, 1s/2s/4s), Timeout (30s page load, 10s pseudo-state), Graceful degradation (Tier 2 on empty extraction).
48
+
49
+ ## Known Risks
50
+ - DESIGN.md spec is alpha. Skill pins to current spec.
51
+ - Puppeteer is heavy (~300MB Chrome). Document requirement clearly.
52
+ - Prose quality depends on AI heuristics. grill-me is the validation gate.
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: extract-design
3
+ description: "Extract a Google DESIGN.md file from an HTML prototype (claude.ai/design or any styled page) using Puppeteer, producing machine-readable tokens and AI-generated prose. Use when the user has an HTML prototype and wants a DESIGN.md to anchor their project's visual identity, or when seed-conventions has just scaffolded a new project."
4
+ model: sonnet
5
+ ---
6
+
7
+ # Extract DESIGN.md from HTML
8
+
9
+ > **HARD GATE** — Do NOT write DESIGN.md without Puppeteer dual-pass extraction. Tokens from static HTML (Cheerio, regex, string scanning) are invalid — they miss cascade, custom properties, and Tailwind resolution.
10
+ >
11
+ > **HARD GATE** — Do NOT claim certainty where evidence is thin. Low-confidence color roles, component classifications, and prose assertions MUST be flagged with `<!-- AGENT NOTE: uncertain — validate during grill-me. Evidence: [what was observed] -->`.
12
+ >
13
+ > **HARD GATE** — Do NOT ship DESIGN.md without running `npx @google/design.md lint`. Unvalidated output is unverified output. If lint is unavailable (offline), flag prominently in terminal and in DESIGN.md prose.
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # First run — extract from HTML prototype
19
+ node extract-design/scripts/extract.js --source ./prototype.html
20
+
21
+ # From a published URL
22
+ node extract-design/scripts/extract.js --source https://my-prototype.example.com
23
+
24
+ # With a custom name
25
+ node extract-design/scripts/extract.js --source ./proto.html --name "My Design System"
26
+
27
+ # Update — re-extract from new HTML, diff against existing
28
+ node extract-design/scripts/extract.js --source ./proto-v2.html
29
+
30
+ # Lint-only — validate existing DESIGN.md without re-extraction
31
+ node extract-design/scripts/extract.js --lint-only
32
+ ```
33
+
34
+ ## Flow
35
+
36
+ 1. **Launch Puppeteer** — dual-pass (light + dark) with retry + timeout. CI flags: `--headless=new --no-sandbox --disable-gpu --disable-dbus --use-gl=angle --use-angle=swiftshader`.
37
+ 2. **Collect styles** — `page.evaluate()` collects computed styles from every element. Returns raw JSON to Node.js. Browser = sensor; Node = brain.
38
+ 3. **Classify tokens** — modular pipeline: colors (Material 3 roles), typography (scale detection), spacing (tolerance GCD), rounded (clustering), components (visual signature + pseudo-state variants).
39
+ 4. **Generate prose** — AI heuristics produce all 8 DESIGN.md sections. Overview and Do's/Don'ts flagged with agent notes.
40
+ 5. **Write + validate** — serialize to `specs/tech-architecture/DESIGN_LATEST.md`, run `npx @google/design.md lint`, report to terminal.
41
+ 6. **Handoff** — writes `handoff.next_skill: grill-me` to `specs/state.yaml` with uncertain decisions context.
42
+
43
+ ## Inputs
44
+
45
+ | Parameter | Required | Description |
46
+ |-----------|----------|-------------|
47
+ | `--source <file\|url>` | First run: yes. Update: optional | HTML prototype path or URL |
48
+ | `--name <string>` | No | Design system name (defaults to `<title>` or directory name) |
49
+ | `--lint-only` | No | Validate existing DESIGN.md without re-extraction |
50
+
51
+ ## Output
52
+
53
+ - `specs/tech-architecture/DESIGN_LATEST.md` — replaces `DESIGN_PLAN_LATEST.md` as the canonical design artifact
54
+ - Terminal summary: token counts, component count, lint result, uncertain decisions
55
+ - Structured JSON log to stderr: extraction events, timing, counts
56
+ - `specs/state.yaml` → `handoff.next_skill: grill-me` with context
57
+
58
+ ## Error Tiers
59
+
60
+ | Tier | Condition | Response |
61
+ |------|-----------|----------|
62
+ | Fatal | No Chrome, page load timeout after retries | Exit non-zero, suggest fixes |
63
+ | Degraded | Zero colors, zero typography, SPA shell | Write DESIGN.md with degradation warning |
64
+ | Warned | Lint errors, uncertain decisions | Write DESIGN.md, flag in terminal, hand off to grill-me |
65
+
66
+ ## Dependencies
67
+
68
+ - **Puppeteer** (Chrome binary) — wrapped behind `BrowserExtractor` interface for testability
69
+ - **`@google/design.md`** (soft, via `npx`) — wrapped behind `DesignValidator` interface. Warns and skips if offline.
70
+
71
+ ## verify
72
+
73
+ ```bash
74
+ node extract-design/tests/test-extraction.js
75
+ ```
76
+
77
+ See [REFERENCE.md](REFERENCE.md) for extraction algorithms and heuristics.
@@ -0,0 +1,23 @@
1
+ Feature: Extract DESIGN.md from HTML prototype
2
+
3
+ Scenario: Extract tokens from a styled prototype
4
+ Given an HTML prototype with 3+ colors, 2+ font sizes, spacing values, and rounded corners
5
+ When extract-design runs on the prototype
6
+ Then DESIGN_LATEST.md contains a colors token group with at least 3 entries
7
+ And DESIGN_LATEST.md contains a typography token group with at least 2 levels
8
+ And all 8 prose sections are present
9
+
10
+ Scenario: Handle dark mode prototypes
11
+ Given an HTML prototype that responds to prefers-color-scheme: dark
12
+ When extract-design runs dual-pass extraction
13
+ Then DESIGN_LATEST.md contains a colors-dark token group
14
+
15
+ Scenario: Degrade gracefully on unstyleable HTML
16
+ Given an HTML file with no computed styles
17
+ When extract-design runs on the prototype
18
+ Then DESIGN_LATEST.md is written with a degradation warning
19
+
20
+ Scenario: Flag uncertain classifications
21
+ Given an HTML prototype where one color is used ambiguously
22
+ When extract-design classifies colors
23
+ Then the uncertain color's prose rationale contains an AGENT NOTE comment
@@ -0,0 +1,21 @@
1
+ function norm(c) { if (!c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)') return null; return c.trim(); }
2
+ function lum(c) { const m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (!m) return 0.5; const s = v => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); return 0.2126 * s(parseInt(m[1]) / 255) + 0.7152 * s(parseInt(m[2]) / 255) + 0.0722 * s(parseInt(m[3]) / 255); }
3
+ function isNeutral(c) { const m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (!m) return false; return Math.max(Math.abs(parseInt(m[1]) - parseInt(m[2])), Math.abs(parseInt(m[2]) - parseInt(m[3])), Math.abs(parseInt(m[1]) - parseInt(m[3]))) < 30; }
4
+
5
+ export function classifyColors(styles, mode = 'light') {
6
+ const u = []; const colors = {}; const stats = new Map();
7
+ for (const el of styles) {
8
+ for (const [prop, usage] of [['backgroundColor','bg'],['color','text'],['borderColor','border']]) {
9
+ const k = norm(el[prop]); if (!k) continue;
10
+ const s = stats.get(k) || { usage: new Set(), area: 0, count: 0 }; s.usage.add(usage); if (prop === 'backgroundColor') s.area += (el.width||0)*(el.height||0); s.count++; stats.set(k, s);
11
+ }
12
+ }
13
+ const neutrals = [], accents = [];
14
+ for (const [c, s] of stats) { if (isNeutral(c)) neutrals.push({ color: c, ...s, luminance: lum(c) }); else accents.push({ color: c, ...s }); }
15
+ neutrals.sort((a,b) => b.area - a.area); accents.sort((a,b) => b.count - a.count);
16
+ if (neutrals.length) { colors.surface = neutrals[0].color; colors.background = neutrals[0].color; const txt = [...stats.entries()].filter(([,s]) => s.usage.has('text')).sort((a,b) => b[1].count - a[1].count); if (txt.length) colors['on-surface'] = txt[0][0]; }
17
+ const containers = neutrals.slice(1); if (containers.length) { const isDark = mode === 'dark'; containers.sort((a,b) => isDark ? b.luminance - a.luminance : a.luminance - b.luminance); const maxC = Math.min(containers.length >= 5 ? 5 : 3, containers.length); const lvls = ['surface-container-lowest','surface-container-low','surface-container','surface-container-high','surface-container-highest']; for (let i = 0; i < maxC; i++) colors[lvls[i]] = containers[i].color; }
18
+ if (accents.length) { colors.tertiary = accents[0].color; if (accents.length > 1) colors.secondary = accents[1].color; const err = accents.find(a => { const m = a.color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); return m && parseInt(m[1]) > 180 && parseInt(m[2]) < 100 && parseInt(m[3]) < 100; }); if (err) colors.error = err.color; }
19
+ if (Object.keys(colors).length < 2) u.push('Fewer than 2 colors extracted.');
20
+ return { colors, uncertain: u };
21
+ }
@@ -0,0 +1,10 @@
1
+ function parseR(v) { if (!v || v === '0px' || v === '0') return 0; const m = v.match(/^([\d.]+)(px|rem)$/); return m ? parseFloat(m[1]) * (m[2] === 'rem' ? 16 : 1) : (parseFloat(v) || 0); }
2
+ export function classifyRounded(styles) { const u = []; const r = {}; const cnt = new Map();
3
+ for (const el of styles) { const px = parseR(el.borderRadius); cnt.set(px, (cnt.get(px) || 0) + 1); }
4
+ const freq = [...cnt.entries()].filter(([,c]) => c >= 2).map(([px]) => px).sort((a,b) => a - b);
5
+ if (!freq.length) { r.none = '0px'; return { rounded: r, uncertain: [] }; }
6
+ r.none = '0px'; const reg = freq.filter(v => v < 9998 && v > 0); const full = freq.find(v => v >= 9998);
7
+ if (full) r.full = `${full}px`; if (reg.length === 1) r.sm = `${reg[0]}px`;
8
+ else if (reg.length >= 2) { r.sm = `${reg[0]}px`; r.md = `${reg[1]}px`; } if (reg.length >= 3) r.lg = `${reg[2]}px`;
9
+ return { rounded: r, uncertain: u };
10
+ }
@@ -0,0 +1,14 @@
1
+ function toPx(v) { if (!v) return 0; const m = v.match(/^([\d.]+)(px|rem|em)$/); if (!m) return 0; return parseFloat(m[1]) * (m[2] === 'rem' || m[2] === 'em' ? 16 : 1); }
2
+ function tGcd(nums, tol = 0.5) { if (!nums.length) return null; let r = nums[0]; for (let i = 1; i < nums.length; i++) { let a = Math.abs(r), b = Math.abs(nums[i]); while (b > tol) { const t = b; b = a % b; a = t; } r = a; } return r; }
3
+ function collect(css, counts) { if (!css) return; for (const p of css.split(/\s+/)) { const v = toPx(p); if (v > 0) counts.set(v, (counts.get(v) || 0) + 1); } }
4
+
5
+ export function classifySpacing(styles) { const u = []; const s = {}; const counts = new Map();
6
+ for (const el of styles) { collect(el.padding, counts); collect(el.margin, counts); if (el.gap) collect(el.gap, counts); }
7
+ const freq = new Map(); for (const [v, c] of counts) { if (c >= 3) freq.set(v, c); }
8
+ if (freq.size < 3) u.push('Fewer than 3 spacing values.');
9
+ const nums = [...freq.keys()].map(toPx).filter(v => v > 0); const g = tGcd(nums, 0.5);
10
+ if (!g || g < 2) { const sorted = [...freq.entries()].sort((a,b) => b[1] - a[1]); s.unit = sorted.length ? `${toPx(sorted[0][0])}px` : '8px'; return { spacing: s, uncertain: u }; }
11
+ s.unit = `${g}px`; const half = g / 2; if (nums.filter(v => Math.abs(v - half) < 0.5).length >= 3) s.half = `${half}px`;
12
+ s.sm = `${g * 2}px`; s.md = `${g * 4}px`; s.lg = `${g * 8}px`; if (nums.some(v => v >= g * 16)) s.xl = `${g * 16}px`;
13
+ return { spacing: s, uncertain: u };
14
+ }
@@ -0,0 +1,9 @@
1
+ function px(v) { if (!v) return 0; const n = parseFloat(v); return isNaN(n) ? 0 : n; }
2
+ export function classifyTypography(styles) { const u = []; const t = {}; const txt = styles.filter(s => s.text && s.text.length > 0 && s.fontSize); const clusters = new Map();
3
+ for (const el of txt) { const s = Math.round(px(el.fontSize)); if (!s) continue; if (!clusters.has(s)) clusters.set(s, { fontFamily: el.fontFamily, fontWeight: el.fontWeight, lineHeight: el.lineHeight, letterSpacing: el.letterSpacing, tag: el.tag, count: 0 }); clusters.get(s).count++; }
4
+ const sorted = [...clusters.entries()].sort((a,b) => b[0] - a[0]); if (!sorted.length) { u.push('No typography detected.'); return { typography: {}, uncertain: u }; }
5
+ let di = 0, hi = 0, bi = 0;
6
+ for (const [sz, cl] of sorted) { const hd = /^h[1-6]$/.test(cl.tag) || sz >= 32; let n; if (sz >= 64 && di === 0) { n = 'display-lg'; di++; } else if (hd && sz >= 40) { n = hi === 0 ? 'headline-lg' : 'headline-md'; hi++; } else if (hd && sz >= 28) { n = hi <= 1 ? 'headline-md' : 'headline-sm'; hi++; } else if (hd) { n = 'title-lg'; } else if (sz <= 18 && bi === 0) { n = 'body-lg'; bi++; } else if (sz <= 18) { n = 'body-md'; bi++; } else if (sz <= 14) { n = 'label-md'; } else { n = 'body-sm'; }
7
+ t[n] = { fontFamily: cl.fontFamily, fontSize: `${sz}px`, fontWeight: String(cl.fontWeight || '400') }; if (cl.lineHeight && cl.lineHeight !== 'normal') t[n].lineHeight = cl.lineHeight; if (cl.letterSpacing && cl.letterSpacing !== 'normal' && cl.letterSpacing !== '0px') t[n].letterSpacing = cl.letterSpacing; }
8
+ return { typography: t, uncertain: u };
9
+ }