@su-record/vibe 2.9.40 → 2.10.2

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 (182) hide show
  1. package/CLAUDE.md +28 -12
  2. package/README.en.md +2 -3
  3. package/README.md +33 -27
  4. package/agents/{teams/figma → figma}/figma-analyst.md +2 -2
  5. package/agents/research/{best-practices-agent.md → best-practices.md} +1 -1
  6. package/agents/research/{codebase-patterns-agent.md → codebase-patterns.md} +1 -1
  7. package/agents/research/{framework-docs-agent.md → framework-docs.md} +1 -1
  8. package/agents/research/{security-advisory-agent.md → security-advisory.md} +1 -1
  9. package/agents/teams/research-team.md +4 -4
  10. package/agents/teams/review-debate-team.md +2 -2
  11. package/agents/teams/security-team.md +4 -4
  12. package/dist/cli/auth.d.ts +0 -1
  13. package/dist/cli/auth.d.ts.map +1 -1
  14. package/dist/cli/auth.js +1 -18
  15. package/dist/cli/auth.js.map +1 -1
  16. package/dist/cli/collaborator.d.ts +3 -3
  17. package/dist/cli/collaborator.js +4 -4
  18. package/dist/cli/collaborator.js.map +1 -1
  19. package/dist/cli/commands/info.d.ts.map +1 -1
  20. package/dist/cli/commands/info.js +0 -1
  21. package/dist/cli/commands/info.js.map +1 -1
  22. package/dist/cli/commands/init.d.ts +3 -4
  23. package/dist/cli/commands/init.d.ts.map +1 -1
  24. package/dist/cli/commands/init.js +15 -20
  25. package/dist/cli/commands/init.js.map +1 -1
  26. package/dist/cli/commands/remove.d.ts.map +1 -1
  27. package/dist/cli/commands/remove.js +2 -7
  28. package/dist/cli/commands/remove.js.map +1 -1
  29. package/dist/cli/commands/update.d.ts.map +1 -1
  30. package/dist/cli/commands/update.js +10 -10
  31. package/dist/cli/commands/update.js.map +1 -1
  32. package/dist/cli/index.js +1 -3
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/postinstall/claude-agents.d.ts +3 -1
  35. package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
  36. package/dist/cli/postinstall/claude-agents.js +47 -9
  37. package/dist/cli/postinstall/claude-agents.js.map +1 -1
  38. package/dist/cli/postinstall/constants.d.ts +5 -0
  39. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  40. package/dist/cli/postinstall/constants.js +165 -23
  41. package/dist/cli/postinstall/constants.js.map +1 -1
  42. package/dist/cli/postinstall/cursor-skills.js +2 -2
  43. package/dist/cli/postinstall/main.d.ts.map +1 -1
  44. package/dist/cli/postinstall/main.js +26 -24
  45. package/dist/cli/postinstall/main.js.map +1 -1
  46. package/dist/cli/setup/LegacyMigration.d.ts +3 -3
  47. package/dist/cli/setup/LegacyMigration.d.ts.map +1 -1
  48. package/dist/cli/setup/LegacyMigration.js +3 -5
  49. package/dist/cli/setup/LegacyMigration.js.map +1 -1
  50. package/dist/cli/setup/ProjectSetup.d.ts +18 -8
  51. package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
  52. package/dist/cli/setup/ProjectSetup.js +70 -19
  53. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  54. package/dist/cli/setup.d.ts +1 -1
  55. package/dist/cli/setup.d.ts.map +1 -1
  56. package/dist/cli/setup.js +1 -1
  57. package/dist/cli/setup.js.map +1 -1
  58. package/dist/cli/utils/cli-detector.d.ts +0 -7
  59. package/dist/cli/utils/cli-detector.d.ts.map +1 -1
  60. package/dist/cli/utils/cli-detector.js +0 -95
  61. package/dist/cli/utils/cli-detector.js.map +1 -1
  62. package/dist/cli/utils.d.ts +1 -1
  63. package/dist/cli/utils.d.ts.map +1 -1
  64. package/dist/cli/utils.js +1 -2
  65. package/dist/cli/utils.js.map +1 -1
  66. package/dist/infra/lib/OrchestrateWorkflow.js +1 -1
  67. package/dist/infra/lib/OrchestrateWorkflow.js.map +1 -1
  68. package/dist/infra/lib/memory/MemoryStorage.d.ts +1 -1
  69. package/dist/infra/lib/memory/MemoryStorage.d.ts.map +1 -1
  70. package/dist/infra/lib/memory/MemoryStorage.js +2 -3
  71. package/dist/infra/lib/memory/MemoryStorage.js.map +1 -1
  72. package/dist/infra/lib/telemetry/SkillTelemetry.test.js +4 -4
  73. package/dist/infra/lib/telemetry/SkillTelemetry.test.js.map +1 -1
  74. package/dist/infra/orchestrator/parallelResearch.js +4 -4
  75. package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
  76. package/hooks/scripts/__tests__/pre-tool-guard.test.js +1 -1
  77. package/hooks/scripts/clone-extract.js +712 -0
  78. package/hooks/scripts/clone-refine.js +510 -0
  79. package/hooks/scripts/clone-to-scss.js +275 -0
  80. package/hooks/scripts/clone-validate.js +280 -0
  81. package/hooks/scripts/codex-notify.js +49 -0
  82. package/hooks/scripts/command-log.js +1 -1
  83. package/hooks/scripts/lib/dispatcher.js +2 -3
  84. package/hooks/scripts/lib/scope-from-spec.js +2 -4
  85. package/hooks/scripts/llm-orchestrate.js +2 -7
  86. package/hooks/scripts/prompt-dispatcher.js +3 -3
  87. package/hooks/scripts/step-counter.js +1 -1
  88. package/hooks/scripts/utils.js +5 -10
  89. package/package.json +2 -1
  90. package/skills/agents-md/SKILL.md +2 -0
  91. package/skills/arch-guard/SKILL.md +2 -0
  92. package/skills/brand-assets/SKILL.md +1 -0
  93. package/skills/capability-loop/SKILL.md +2 -0
  94. package/skills/characterization-test/SKILL.md +2 -0
  95. package/skills/chub-usage/SKILL.md +1 -0
  96. package/skills/claude-md-guide/SKILL.md +2 -0
  97. package/skills/clone/SKILL.md +361 -0
  98. package/skills/commerce-patterns/SKILL.md +1 -0
  99. package/skills/commit-push-pr/SKILL.md +1 -0
  100. package/skills/context7-usage/SKILL.md +1 -0
  101. package/skills/{vibe-contract → contract}/SKILL.md +7 -8
  102. package/skills/create-prd/SKILL.md +1 -0
  103. package/skills/design-audit/SKILL.md +1 -0
  104. package/skills/design-critique/SKILL.md +1 -0
  105. package/skills/design-distill/SKILL.md +1 -0
  106. package/skills/design-normalize/SKILL.md +1 -0
  107. package/skills/design-polish/SKILL.md +1 -0
  108. package/skills/design-teach/SKILL.md +2 -0
  109. package/skills/devlog/SKILL.md +1 -0
  110. package/skills/{vibe-docs → docs}/SKILL.md +8 -8
  111. package/skills/e2e-commerce/SKILL.md +1 -0
  112. package/skills/event-comms/SKILL.md +1 -0
  113. package/skills/event-ops/SKILL.md +1 -0
  114. package/skills/event-planning/SKILL.md +1 -0
  115. package/skills/exec-plan/SKILL.md +2 -0
  116. package/skills/{vibe-figma → figma}/SKILL.md +4 -3
  117. package/skills/{vibe-figma-convert → figma-convert}/SKILL.md +4 -3
  118. package/skills/{vibe-figma-extract → figma-extract}/SKILL.md +4 -3
  119. package/skills/git-worktree/SKILL.md +1 -0
  120. package/skills/handoff/SKILL.md +2 -0
  121. package/skills/{vibe-interview → interview}/SKILL.md +16 -16
  122. package/skills/parallel-research/SKILL.md +2 -0
  123. package/skills/{vibe-plan → plan}/SKILL.md +9 -9
  124. package/skills/prioritization-frameworks/SKILL.md +1 -0
  125. package/skills/priority-todos/SKILL.md +2 -0
  126. package/skills/{vibe-regress → regress}/SKILL.md +5 -6
  127. package/skills/rob-pike/SKILL.md +2 -0
  128. package/skills/seo-checklist/SKILL.md +1 -0
  129. package/skills/{vibe-spec → spec}/SKILL.md +14 -14
  130. package/skills/{vibe-spec-review → spec-review}/SKILL.md +8 -9
  131. package/skills/systematic-debugging/SKILL.md +2 -0
  132. package/skills/techdebt/SKILL.md +2 -0
  133. package/skills/{vibe-test → test}/SKILL.md +19 -19
  134. package/skills/tool-fallback/SKILL.md +1 -0
  135. package/skills/typescript-advanced-types/SKILL.md +1 -0
  136. package/skills/ui-ux-pro-max/SKILL.md +1 -0
  137. package/skills/user-personas/SKILL.md +1 -0
  138. package/skills/vercel-react-best-practices/SKILL.md +1 -0
  139. package/skills/vibe/SKILL.md +288 -0
  140. package/{commands/vibe.analyze.md → skills/vibe.analyze/SKILL.md} +2 -0
  141. package/skills/vibe.clone/SKILL.md +117 -0
  142. package/{commands/vibe.contract.md → skills/vibe.contract/SKILL.md} +3 -1
  143. package/{commands/vibe.docs.md → skills/vibe.docs/SKILL.md} +3 -1
  144. package/{commands/vibe.event.md → skills/vibe.event/SKILL.md} +2 -0
  145. package/{commands/vibe.figma.md → skills/vibe.figma/SKILL.md} +25 -23
  146. package/{commands/vibe.harness.md → skills/vibe.harness/SKILL.md} +2 -0
  147. package/{commands/vibe.reason.md → skills/vibe.reason/SKILL.md} +2 -0
  148. package/{commands/vibe.regress.md → skills/vibe.regress/SKILL.md} +5 -3
  149. package/{commands/vibe.review.md → skills/vibe.review/SKILL.md} +2 -0
  150. package/{commands/vibe.run.md → skills/vibe.run/SKILL.md} +5 -1
  151. package/{commands/vibe.scaffold.md → skills/vibe.scaffold/SKILL.md} +2 -0
  152. package/{commands/vibe.spec.md → skills/vibe.spec/SKILL.md} +36 -34
  153. package/{commands/vibe.test.md → skills/vibe.test/SKILL.md} +8 -6
  154. package/{commands/vibe.trace.md → skills/vibe.trace/SKILL.md} +7 -0
  155. package/{commands/vibe.utils.md → skills/vibe.utils/SKILL.md} +2 -0
  156. package/{commands/vibe.verify.md → skills/vibe.verify/SKILL.md} +4 -2
  157. package/skills/video-production/SKILL.md +1 -0
  158. package/vibe/rules/principles/dual-harness-doctrine.md +50 -0
  159. /package/agents/{teams/figma → figma}/figma-architect.md +0 -0
  160. /package/agents/{teams/figma → figma}/figma-auditor.md +0 -0
  161. /package/agents/{teams/figma → figma}/figma-builder.md +0 -0
  162. /package/skills/{vibe-docs → docs}/templates/architecture.md +0 -0
  163. /package/skills/{vibe-docs → docs}/templates/behavioral-principles.md +0 -0
  164. /package/skills/{vibe-docs → docs}/templates/readme.md +0 -0
  165. /package/skills/{vibe-docs → docs}/templates/release-notes.md +0 -0
  166. /package/skills/{vibe-figma → figma}/rubrics/extraction-checklist.md +0 -0
  167. /package/skills/{vibe-figma → figma}/templates/component-index.md +0 -0
  168. /package/skills/{vibe-figma → figma}/templates/component-spec.md +0 -0
  169. /package/skills/{vibe-figma → figma}/templates/figma-handoff.md +0 -0
  170. /package/skills/{vibe-figma → figma}/templates/remapped-tree.md +0 -0
  171. /package/skills/{vibe-figma-convert → figma-convert}/rubrics/conversion-rules.md +0 -0
  172. /package/skills/{vibe-figma-convert → figma-convert}/templates/component.md +0 -0
  173. /package/skills/{vibe-figma-extract → figma-extract}/rubrics/image-rules.md +0 -0
  174. /package/skills/{vibe-interview → interview}/checklists/api.md +0 -0
  175. /package/skills/{vibe-interview → interview}/checklists/feature.md +0 -0
  176. /package/skills/{vibe-interview → interview}/checklists/library.md +0 -0
  177. /package/skills/{vibe-interview → interview}/checklists/mobile.md +0 -0
  178. /package/skills/{vibe-interview → interview}/checklists/webapp.md +0 -0
  179. /package/skills/{vibe-interview → interview}/checklists/website.md +0 -0
  180. /package/skills/{vibe-regress → regress}/templates/bug.md +0 -0
  181. /package/skills/{vibe-regress → regress}/templates/test-jest.md +0 -0
  182. /package/skills/{vibe-regress → regress}/templates/test-vitest.md +0 -0
@@ -0,0 +1,510 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * clone-refine.js — computed.json → sections.json
5
+ *
6
+ * Usage:
7
+ * node clone-refine.js <rendered.html> <computed.json> --out=<sections.json> --bp=mo|pc
8
+ * [--stylesheets=<stylesheets.json>] [--asset-map=<asset-map.json>]
9
+ *
10
+ * Refinement steps:
11
+ * 1. Tree reconstruction (flat node list → tree)
12
+ * 2. Section detection (semantic landmarks + visual heuristics)
13
+ * 3. Repeated pattern detection → component candidates (structural hash, ≥3 siblings)
14
+ * 4. Design token extraction with bucketing (colors / typography / spacing / radius / shadow)
15
+ * 5. Token-aware CSS values (e.g. color "#0070f3" → "var(--color-blue-500)")
16
+ *
17
+ * Output schema: see SKILL.md.
18
+ */
19
+
20
+ import fs from 'fs';
21
+ import path from 'path';
22
+
23
+ // ─── CLI ────────────────────────────────────────────────────────────
24
+ function parseArgs(argv) {
25
+ const [, , htmlPath, computedPath, ...rest] = argv;
26
+ const opts = {};
27
+ for (const a of rest) {
28
+ if (a.startsWith('--out=')) opts.out = a.slice(6);
29
+ else if (a.startsWith('--bp=')) opts.bp = a.slice(5);
30
+ else if (a.startsWith('--stylesheets=')) opts.stylesheets = a.slice(14);
31
+ else if (a.startsWith('--asset-map=')) opts.assetMap = a.slice(12);
32
+ }
33
+ return { htmlPath, computedPath, opts };
34
+ }
35
+
36
+ const { htmlPath, computedPath, opts } = parseArgs(process.argv);
37
+ if (!htmlPath || !computedPath || !opts.out) {
38
+ console.error('Usage: node clone-refine.js <rendered.html> <computed.json> --out=<sections.json> --bp=mo|pc');
39
+ process.exit(1);
40
+ }
41
+
42
+ // ─── Tree reconstruction ────────────────────────────────────────────
43
+ function buildTree(nodes) {
44
+ const byId = new Map();
45
+ for (const n of nodes) byId.set(n.id, { ...n, children: [] });
46
+ const roots = [];
47
+ for (const n of byId.values()) {
48
+ if (n.parent && byId.has(n.parent)) byId.get(n.parent).children.push(n);
49
+ else roots.push(n);
50
+ }
51
+ return { roots, byId };
52
+ }
53
+
54
+ // ─── CSS value parsers ──────────────────────────────────────────────
55
+ function parsePx(val) {
56
+ if (typeof val !== 'string') return null;
57
+ const m = /^(-?\d+(?:\.\d+)?)px$/.exec(val.trim());
58
+ return m ? Number(m[1]) : null;
59
+ }
60
+
61
+ function parseColor(val) {
62
+ if (typeof val !== 'string') return null;
63
+ const v = val.trim().toLowerCase();
64
+ if (!v || v === 'transparent' || v === 'currentcolor' || v === 'inherit') return null;
65
+ // rgb(a) / hsl(a)
66
+ let m = /^rgba?\(([^)]+)\)$/.exec(v);
67
+ if (m) {
68
+ const parts = m[1].split(/[,/\s]+/).filter(Boolean).map(Number);
69
+ if (parts.length >= 3 && parts.every((x) => !isNaN(x))) {
70
+ const [r, g, b, a] = parts;
71
+ if (a !== undefined && a < 1) return `rgba(${r}, ${g}, ${b}, ${a})`;
72
+ const hex = '#' + [r, g, b].map((x) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, '0')).join('');
73
+ return hex;
74
+ }
75
+ }
76
+ m = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(v);
77
+ if (m) {
78
+ let h = m[1];
79
+ if (h.length === 3) h = h.split('').map((c) => c + c).join('');
80
+ if (h.length === 4) h = h.split('').map((c) => c + c).join('');
81
+ return '#' + h;
82
+ }
83
+ return v;
84
+ }
85
+
86
+ // Snap spacing values to the closest grid step (4px → 8px → 16px)
87
+ function snapSpacing(px) {
88
+ if (px === null || px === undefined) return null;
89
+ if (px === 0) return 0;
90
+ const abs = Math.abs(px);
91
+ if (abs < 8) return Math.round(px); // sub-grid keep
92
+ if (abs < 32) return Math.round(px / 4) * 4;
93
+ if (abs < 96) return Math.round(px / 8) * 8;
94
+ return Math.round(px / 16) * 16;
95
+ }
96
+
97
+ // ─── Token extraction ───────────────────────────────────────────────
98
+ function extractTokens(nodes) {
99
+ const colorFreq = new Map();
100
+ const typoFreq = new Map();
101
+ const spacingFreq = new Map();
102
+ const radiusFreq = new Map();
103
+ const shadowFreq = new Map();
104
+
105
+ const addColor = (val) => {
106
+ const c = parseColor(val);
107
+ if (c) colorFreq.set(c, (colorFreq.get(c) || 0) + 1);
108
+ };
109
+ const addSpacing = (px) => {
110
+ if (px === null || px === undefined) return;
111
+ const snapped = snapSpacing(px);
112
+ if (snapped === null) return;
113
+ spacingFreq.set(snapped, (spacingFreq.get(snapped) || 0) + 1);
114
+ };
115
+
116
+ for (const n of nodes) {
117
+ if (!n.css) continue;
118
+ addColor(n.css['color']);
119
+ addColor(n.css['background-color']);
120
+ for (const side of ['top', 'right', 'bottom', 'left']) {
121
+ addColor(n.css[`border-${side}-color`]);
122
+ }
123
+ // gradient stops
124
+ const bg = n.css['background-image'];
125
+ if (bg && /gradient/i.test(bg)) {
126
+ const re = /(?:#[0-9a-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\))/gi;
127
+ let m;
128
+ while ((m = re.exec(bg)) !== null) addColor(m[0]);
129
+ }
130
+
131
+ // typography
132
+ const ff = n.css['font-family'];
133
+ const fs = n.css['font-size'];
134
+ const fw = n.css['font-weight'];
135
+ const lh = n.css['line-height'];
136
+ const ls = n.css['letter-spacing'];
137
+ if (ff && fs) {
138
+ const key = JSON.stringify({ ff, fs, fw: fw || '400', lh: lh || 'normal', ls: ls || 'normal' });
139
+ typoFreq.set(key, (typoFreq.get(key) || 0) + 1);
140
+ }
141
+
142
+ // spacing
143
+ for (const side of ['top', 'right', 'bottom', 'left']) {
144
+ addSpacing(parsePx(n.css[`margin-${side}`]));
145
+ addSpacing(parsePx(n.css[`padding-${side}`]));
146
+ }
147
+ addSpacing(parsePx(n.css['gap']));
148
+ addSpacing(parsePx(n.css['row-gap']));
149
+ addSpacing(parsePx(n.css['column-gap']));
150
+
151
+ // radius
152
+ for (const corner of ['top-left', 'top-right', 'bottom-left', 'bottom-right']) {
153
+ const r = parsePx(n.css[`border-${corner}-radius`]);
154
+ if (r !== null && r > 0) radiusFreq.set(r, (radiusFreq.get(r) || 0) + 1);
155
+ }
156
+
157
+ // shadow
158
+ const sh = n.css['box-shadow'];
159
+ if (sh && sh !== 'none') shadowFreq.set(sh, (shadowFreq.get(sh) || 0) + 1);
160
+ }
161
+
162
+ const sortedTop = (m, limit) => Array.from(m.entries())
163
+ .sort((a, b) => b[1] - a[1])
164
+ .slice(0, limit)
165
+ .map(([k, count]) => ({ value: k, count }));
166
+
167
+ // Name tokens
168
+ const colors = sortedTop(colorFreq, 20).map((t, i) => ({
169
+ name: nameColor(t.value, i),
170
+ value: t.value,
171
+ count: t.count,
172
+ }));
173
+
174
+ const typography = sortedTop(typoFreq, 12).map((t, i) => {
175
+ const parsed = JSON.parse(t.value);
176
+ return {
177
+ name: `text-${i + 1}`,
178
+ family: parsed.ff,
179
+ size: parsed.fs,
180
+ weight: parsed.fw,
181
+ lineHeight: parsed.lh,
182
+ letterSpacing: parsed.ls,
183
+ count: t.count,
184
+ };
185
+ });
186
+
187
+ const spacing = sortedTop(spacingFreq, 16).map((t) => ({
188
+ name: `space-${t.value}`,
189
+ value: `${t.value}px`,
190
+ count: t.count,
191
+ }));
192
+
193
+ const radius = sortedTop(radiusFreq, 8).map((t) => ({
194
+ name: `radius-${t.value}`,
195
+ value: `${t.value}px`,
196
+ count: t.count,
197
+ }));
198
+
199
+ const shadow = sortedTop(shadowFreq, 6).map((t, i) => ({
200
+ name: `shadow-${i + 1}`,
201
+ value: t.value,
202
+ count: t.count,
203
+ }));
204
+
205
+ return { colors, typography, spacing, radius, shadow };
206
+ }
207
+
208
+ function nameColor(hex, idx) {
209
+ // Heuristic naming: hex → semantic label by brightness/saturation
210
+ if (hex === '#ffffff' || hex === '#fff') return 'color-white';
211
+ if (hex === '#000000' || hex === '#000') return 'color-black';
212
+ if (hex.startsWith('rgba')) return `color-overlay-${idx + 1}`;
213
+ // Compute HSL roughly for naming
214
+ const m = /^#([0-9a-f]{6})$/.exec(hex);
215
+ if (m) {
216
+ const r = parseInt(m[1].slice(0, 2), 16) / 255;
217
+ const g = parseInt(m[1].slice(2, 4), 16) / 255;
218
+ const b = parseInt(m[1].slice(4, 6), 16) / 255;
219
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
220
+ const l = (max + min) / 2;
221
+ const d = max - min;
222
+ if (d < 0.06) return `color-gray-${Math.round(l * 100)}`;
223
+ let h = 0;
224
+ if (max === r) h = ((g - b) / d) % 6;
225
+ else if (max === g) h = (b - r) / d + 2;
226
+ else h = (r - g) / d + 4;
227
+ h = Math.round(h * 60);
228
+ if (h < 0) h += 360;
229
+ const hue =
230
+ h < 15 ? 'red' :
231
+ h < 45 ? 'orange' :
232
+ h < 70 ? 'yellow' :
233
+ h < 165 ? 'green' :
234
+ h < 200 ? 'teal' :
235
+ h < 250 ? 'blue' :
236
+ h < 290 ? 'purple' :
237
+ h < 330 ? 'pink' : 'red';
238
+ return `color-${hue}-${Math.round(l * 100)}`;
239
+ }
240
+ return `color-${idx + 1}`;
241
+ }
242
+
243
+ // ─── Section detection ──────────────────────────────────────────────
244
+ const SEMANTIC_SECTION_TAGS = new Set(['header', 'nav', 'main', 'section', 'article', 'aside', 'footer']);
245
+
246
+ function findSections(roots) {
247
+ // 1. Find <body>
248
+ const html = roots.find((r) => r.tag === 'html');
249
+ if (!html) return [];
250
+ const body = html.children.find((c) => c.tag === 'body');
251
+ if (!body) return [];
252
+
253
+ // 2. Collect candidate landmarks from body subtree (BFS, prefer top-level)
254
+ const sections = [];
255
+ const seenIds = new Set();
256
+
257
+ const tryAdd = (node, label) => {
258
+ if (seenIds.has(node.id)) return;
259
+ if (!node.box || node.box.h < 40) return;
260
+ seenIds.add(node.id);
261
+ sections.push({ node, label });
262
+ };
263
+
264
+ // First pass: explicit semantic tags within body
265
+ const queue = [...body.children];
266
+ while (queue.length) {
267
+ const n = queue.shift();
268
+ if (SEMANTIC_SECTION_TAGS.has(n.tag)) {
269
+ tryAdd(n, sectionLabelFor(n));
270
+ // Don't descend — keep section as atomic unit
271
+ } else {
272
+ queue.push(...n.children);
273
+ }
274
+ }
275
+
276
+ // Second pass: if no semantic sections found, fall back to top-level body children with significant height
277
+ if (sections.length === 0) {
278
+ for (const child of body.children) {
279
+ if (child.box && child.box.h >= 100) tryAdd(child, sectionLabelFor(child));
280
+ }
281
+ }
282
+
283
+ // Sort by visual order (y position)
284
+ sections.sort((a, b) => a.node.box.y - b.node.box.y);
285
+
286
+ // Disambiguate labels (Hero → Hero, Hero-2 if duplicate)
287
+ const labelCounts = new Map();
288
+ for (const s of sections) {
289
+ const base = s.label;
290
+ const count = (labelCounts.get(base) || 0) + 1;
291
+ labelCounts.set(base, count);
292
+ s.label = count > 1 ? `${base}-${count}` : base;
293
+ }
294
+
295
+ return sections;
296
+ }
297
+
298
+ function sectionLabelFor(node) {
299
+ // Prefer explicit class/id hints
300
+ const cls = (node.classes || '').toLowerCase();
301
+ const hints = [
302
+ ['hero', 'Hero'],
303
+ ['banner', 'Banner'],
304
+ ['header', 'Header'],
305
+ ['nav', 'Nav'],
306
+ ['footer', 'Footer'],
307
+ ['feature', 'Features'],
308
+ ['testimonial', 'Testimonials'],
309
+ ['pricing', 'Pricing'],
310
+ ['cta', 'CTA'],
311
+ ['faq', 'FAQ'],
312
+ ['gallery', 'Gallery'],
313
+ ['contact', 'Contact'],
314
+ ];
315
+ for (const [pat, name] of hints) if (cls.includes(pat) || (node.attrs && (node.attrs.role || '').includes(pat))) return name;
316
+ // Fall back to semantic tag
317
+ const tagLabels = { header: 'Header', footer: 'Footer', nav: 'Nav', main: 'Main', article: 'Article', aside: 'Aside' };
318
+ if (tagLabels[node.tag]) return tagLabels[node.tag];
319
+ return 'Section';
320
+ }
321
+
322
+ // ─── Component pattern detection ────────────────────────────────────
323
+ // Structural hash: tag + class signature + first-level child tag list + leaf count tier
324
+ function structuralHash(node, depth = 0) {
325
+ if (depth > 3) return '';
326
+ const classSig = (node.classes || '').split(/\s+/).filter(Boolean).sort().slice(0, 4).join('.');
327
+ const childTags = (node.children || []).map((c) => c.tag).slice(0, 8).join(',');
328
+ const childCount = (node.children || []).length;
329
+ const sizeBucket = node.box ? `${Math.round(node.box.w / 40)}x${Math.round(node.box.h / 40)}` : '?';
330
+ return `${node.tag}|${classSig}|${childTags}|${childCount}|${sizeBucket}`;
331
+ }
332
+
333
+ function detectComponents(sectionRoot) {
334
+ // Group siblings at each depth by structural hash
335
+ const candidates = [];
336
+ const walk = (parent) => {
337
+ if (!parent.children || parent.children.length < 3) {
338
+ for (const c of (parent.children || [])) walk(c);
339
+ return;
340
+ }
341
+ const groups = new Map();
342
+ for (const child of parent.children) {
343
+ const hash = structuralHash(child);
344
+ if (!groups.has(hash)) groups.set(hash, []);
345
+ groups.get(hash).push(child);
346
+ }
347
+ for (const [hash, members] of groups) {
348
+ if (members.length >= 3) {
349
+ candidates.push({
350
+ hash,
351
+ count: members.length,
352
+ parentId: parent.id,
353
+ memberIds: members.map((m) => m.id),
354
+ exemplarTag: members[0].tag,
355
+ exemplarClasses: members[0].classes,
356
+ exemplarBox: members[0].box,
357
+ });
358
+ }
359
+ }
360
+ for (const c of parent.children) walk(c);
361
+ };
362
+ walk(sectionRoot);
363
+ return candidates;
364
+ }
365
+
366
+ // ─── BG image classification ────────────────────────────────────────
367
+ function classifyImages(node) {
368
+ const out = { bg: [], content: [] };
369
+ const walk = (n) => {
370
+ if (n.css) {
371
+ const bg = n.css['background-image'];
372
+ if (bg && bg !== 'none' && /url\(/.test(bg)) {
373
+ const re = /url\(['"]?([^'")]+)['"]?\)/g;
374
+ let m;
375
+ while ((m = re.exec(bg)) !== null) out.bg.push(m[1]);
376
+ }
377
+ }
378
+ if (n.tag === 'img') {
379
+ const src = (n.attrs && (n.attrs.currentSrc || n.attrs.src));
380
+ if (src) out.content.push(src);
381
+ }
382
+ for (const c of (n.children || [])) walk(c);
383
+ };
384
+ walk(node);
385
+ return out;
386
+ }
387
+
388
+ // ─── Trim children to essential layout/text/image nodes ─────────────
389
+ function trimSubtree(node) {
390
+ if (!node) return null;
391
+ const trimmed = {
392
+ id: node.id,
393
+ tag: node.tag,
394
+ classes: node.classes || '',
395
+ pseudo: node.pseudo || false,
396
+ isSvg: node.isSvg || false,
397
+ box: node.box,
398
+ css: node.css || {},
399
+ };
400
+ if (node.attrs) {
401
+ const a = node.attrs;
402
+ const keep = {};
403
+ for (const k of ['src', 'currentSrc', 'href', 'alt', 'title', 'role', 'ariaLabel', 'type', 'name', 'placeholder']) {
404
+ if (a[k]) keep[k] = a[k];
405
+ }
406
+ if (Object.keys(keep).length) trimmed.attrs = keep;
407
+ }
408
+ if (node.text) trimmed.text = node.text;
409
+ if (node.svgMarkup) trimmed.svgMarkup = node.svgMarkup;
410
+ if (node.children && node.children.length) {
411
+ trimmed.children = node.children.map(trimSubtree);
412
+ }
413
+ return trimmed;
414
+ }
415
+
416
+ // ─── Token application: replace literal values with token refs ──────
417
+ function applyTokens(nodes, tokens) {
418
+ const colorByValue = new Map(tokens.colors.map((t) => [t.value, t.name]));
419
+ const spacingByValue = new Map(tokens.spacing.map((t) => [t.value, t.name]));
420
+ const radiusByValue = new Map(tokens.radius.map((t) => [t.value, t.name]));
421
+ const shadowByValue = new Map(tokens.shadow.map((t) => [t.value, t.name]));
422
+
423
+ const sub = (val, map) => {
424
+ if (!val) return val;
425
+ const normalized = parseColor(val) || val;
426
+ if (map.has(normalized)) return `var(--${map.get(normalized)})`;
427
+ if (map.has(val)) return `var(--${map.get(val)})`;
428
+ return val;
429
+ };
430
+
431
+ const walkAndApply = (n) => {
432
+ if (n.css) {
433
+ for (const prop of Object.keys(n.css)) {
434
+ if (prop === 'color' || prop === 'background-color' || prop.endsWith('-color')) {
435
+ n.css[prop] = sub(n.css[prop], colorByValue);
436
+ } else if (prop.startsWith('margin-') || prop.startsWith('padding-') || prop === 'gap' || prop === 'row-gap' || prop === 'column-gap') {
437
+ const px = parsePx(n.css[prop]);
438
+ if (px !== null) {
439
+ const snapped = snapSpacing(px);
440
+ const key = `${snapped}px`;
441
+ if (spacingByValue.has(key)) n.css[prop] = `var(--${spacingByValue.get(key)})`;
442
+ }
443
+ } else if (prop.endsWith('-radius')) {
444
+ if (radiusByValue.has(n.css[prop])) n.css[prop] = `var(--${radiusByValue.get(n.css[prop])})`;
445
+ } else if (prop === 'box-shadow') {
446
+ if (shadowByValue.has(n.css[prop])) n.css[prop] = `var(--${shadowByValue.get(n.css[prop])})`;
447
+ }
448
+ }
449
+ }
450
+ for (const c of (n.children || [])) walkAndApply(c);
451
+ };
452
+ for (const n of nodes) walkAndApply(n);
453
+ }
454
+
455
+ // ─── Main ───────────────────────────────────────────────────────────
456
+ function main() {
457
+ const computed = JSON.parse(fs.readFileSync(computedPath, 'utf8'));
458
+ const { roots, byId } = buildTree(computed.nodes);
459
+
460
+ // Token extraction first (across whole document)
461
+ const tokens = extractTokens(computed.nodes);
462
+
463
+ // Section detection
464
+ const sectionEntries = findSections(roots);
465
+ console.log(`[clone-refine] detected ${sectionEntries.length} sections, ${tokens.colors.length} colors, ${tokens.typography.length} typo, ${tokens.spacing.length} spacings`);
466
+
467
+ // Build refined sections
468
+ const sections = sectionEntries.map(({ node, label }) => {
469
+ const subtree = trimSubtree(node);
470
+ const components = detectComponents(node);
471
+ const images = classifyImages(node);
472
+ return {
473
+ name: label,
474
+ nodeRef: node.id,
475
+ tag: node.tag,
476
+ classes: node.classes,
477
+ box: node.box,
478
+ css: node.css || {},
479
+ components,
480
+ images,
481
+ children: (subtree.children || []),
482
+ };
483
+ });
484
+
485
+ // Apply token substitution last (so original values are preserved through detection)
486
+ applyTokens(sections, tokens);
487
+
488
+ const out = {
489
+ meta: {
490
+ feature: path.basename(path.dirname(opts.out)) || 'feature',
491
+ url: computed.meta.url,
492
+ viewport: computed.meta.viewport,
493
+ bp: opts.bp || computed.meta.bp,
494
+ generatedAt: new Date().toISOString(),
495
+ },
496
+ tokens,
497
+ sections,
498
+ };
499
+
500
+ fs.mkdirSync(path.dirname(opts.out), { recursive: true });
501
+ fs.writeFileSync(opts.out, JSON.stringify(out, null, 2));
502
+ console.log(`[clone-refine] done → ${opts.out}`);
503
+ }
504
+
505
+ try { main(); }
506
+ catch (e) {
507
+ console.error(`[clone-refine] FAIL: ${e.message}`);
508
+ if (process.env.DEBUG) console.error(e.stack);
509
+ process.exit(1);
510
+ }