wogiflow 1.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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,617 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Component Similarity Matcher
5
+ *
6
+ * Compares extracted Figma components against the codebase registry
7
+ * and calculates similarity scores based on CSS, structure, and naming.
8
+ *
9
+ * Usage:
10
+ * flow figma match <figma-components.json> # Match against registry
11
+ * flow figma match --stdin # Read from stdin
12
+ * flow figma match --threshold 80 # Set match threshold
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { getProjectRoot } = require('./flow-utils');
18
+
19
+ const PROJECT_ROOT = getProjectRoot();
20
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
21
+ const REGISTRY_PATH = path.join(WORKFLOW_DIR, 'state', 'component-registry.json');
22
+
23
+ // ============================================================
24
+ // Matching Configuration
25
+ // ============================================================
26
+
27
+ const MATCH_CONFIG = {
28
+ thresholds: {
29
+ EXACT_MATCH: 95, // Use directly, minimal/no changes
30
+ STRONG_MATCH: 80, // Use with minor adjustments
31
+ VARIANT_CANDIDATE: 60, // Could be a new variant of existing
32
+ NEW_COMPONENT: 60 // Below this = definitely new
33
+ },
34
+
35
+ weights: {
36
+ css: 0.35, // CSS properties (colors, spacing, etc.)
37
+ structure: 0.25, // DOM structure similarity
38
+ naming: 0.20, // Name similarity
39
+ behavior: 0.20 // Props/variants similarity
40
+ }
41
+ };
42
+
43
+ // ============================================================
44
+ // Similarity Calculator
45
+ // ============================================================
46
+
47
+ class SimilarityMatcher {
48
+ constructor(registry) {
49
+ this.registry = registry;
50
+ }
51
+
52
+ /**
53
+ * Match a single Figma component against all registry components
54
+ */
55
+ matchComponent(figmaComponent) {
56
+ const matches = [];
57
+
58
+ for (const registryComponent of this.registry.components) {
59
+ const score = this.calculateSimilarity(figmaComponent, registryComponent);
60
+
61
+ if (score > 0) {
62
+ matches.push({
63
+ registryComponent: registryComponent,
64
+ score: score,
65
+ breakdown: this.getScoreBreakdown(figmaComponent, registryComponent),
66
+ differences: this.getDifferences(figmaComponent, registryComponent),
67
+ suggestion: this.getSuggestion(score)
68
+ });
69
+ }
70
+ }
71
+
72
+ // Sort by score descending
73
+ matches.sort((a, b) => b.score - a.score);
74
+
75
+ return {
76
+ figmaComponent: {
77
+ id: figmaComponent.id,
78
+ name: figmaComponent.name,
79
+ type: figmaComponent.type,
80
+ figmaType: figmaComponent.figmaType
81
+ },
82
+ matches: matches.slice(0, 5), // Top 5 matches
83
+ bestMatch: matches[0] || null,
84
+ suggestion: this.getOverallSuggestion(figmaComponent, matches[0])
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Match all Figma components
90
+ */
91
+ matchAll(figmaComponents) {
92
+ const results = {
93
+ summary: {
94
+ total: figmaComponents.length,
95
+ exactMatches: 0,
96
+ strongMatches: 0,
97
+ variantCandidates: 0,
98
+ newComponents: 0
99
+ },
100
+ matches: []
101
+ };
102
+
103
+ for (const component of figmaComponents) {
104
+ const result = this.matchComponent(component);
105
+ results.matches.push(result);
106
+
107
+ // Update summary
108
+ if (result.bestMatch) {
109
+ const score = result.bestMatch.score;
110
+ if (score >= MATCH_CONFIG.thresholds.EXACT_MATCH) {
111
+ results.summary.exactMatches++;
112
+ } else if (score >= MATCH_CONFIG.thresholds.STRONG_MATCH) {
113
+ results.summary.strongMatches++;
114
+ } else if (score >= MATCH_CONFIG.thresholds.VARIANT_CANDIDATE) {
115
+ results.summary.variantCandidates++;
116
+ } else {
117
+ results.summary.newComponents++;
118
+ }
119
+ } else {
120
+ results.summary.newComponents++;
121
+ }
122
+ }
123
+
124
+ return results;
125
+ }
126
+
127
+ /**
128
+ * Calculate overall similarity score (0-100)
129
+ */
130
+ calculateSimilarity(figma, registry) {
131
+ const cssScore = this.calculateCSSScore(figma, registry);
132
+ const structureScore = this.calculateStructureScore(figma, registry);
133
+ const namingScore = this.calculateNamingScore(figma, registry);
134
+ const behaviorScore = this.calculateBehaviorScore(figma, registry);
135
+
136
+ const weightedScore =
137
+ cssScore * MATCH_CONFIG.weights.css +
138
+ structureScore * MATCH_CONFIG.weights.structure +
139
+ namingScore * MATCH_CONFIG.weights.naming +
140
+ behaviorScore * MATCH_CONFIG.weights.behavior;
141
+
142
+ return Math.round(weightedScore);
143
+ }
144
+
145
+ /**
146
+ * CSS similarity based on colors, spacing, typography, etc.
147
+ */
148
+ calculateCSSScore(figma, registry) {
149
+ let totalScore = 0;
150
+ let totalWeight = 0;
151
+
152
+ // Compare colors
153
+ const figmaColors = this.extractCSSValues(figma, 'colors');
154
+ const registryColors = this.extractCSSValuesFromRegistry(registry, 'color');
155
+ const colorScore = this.compareArrays(figmaColors, registryColors);
156
+ totalScore += colorScore * 30;
157
+ totalWeight += 30;
158
+
159
+ // Compare spacing
160
+ const figmaSpacing = this.extractCSSValues(figma, 'spacing');
161
+ const registrySpacing = this.extractCSSValuesFromRegistry(registry, 'spacing');
162
+ const spacingScore = this.compareArrays(figmaSpacing, registrySpacing);
163
+ totalScore += spacingScore * 25;
164
+ totalWeight += 25;
165
+
166
+ // Compare typography
167
+ const figmaTypo = this.extractCSSValues(figma, 'typography');
168
+ const registryTypo = this.extractCSSValuesFromRegistry(registry, 'typography');
169
+ const typoScore = this.compareArrays(figmaTypo, registryTypo);
170
+ totalScore += typoScore * 20;
171
+ totalWeight += 20;
172
+
173
+ // Compare radius
174
+ const figmaRadius = this.extractCSSValues(figma, 'radius');
175
+ const registryRadius = this.extractCSSValuesFromRegistry(registry, 'radius');
176
+ const radiusScore = this.compareArrays(figmaRadius, registryRadius);
177
+ totalScore += radiusScore * 15;
178
+ totalWeight += 15;
179
+
180
+ // Compare layout
181
+ const figmaLayout = this.extractCSSValues(figma, 'layout');
182
+ const registryLayout = this.extractCSSValuesFromRegistry(registry, 'layout');
183
+ const layoutScore = this.compareArrays(figmaLayout, registryLayout);
184
+ totalScore += layoutScore * 10;
185
+ totalWeight += 10;
186
+
187
+ return totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0;
188
+ }
189
+
190
+ extractCSSValues(figma, type) {
191
+ if (!figma.css || !figma.css[type]) return [];
192
+ return figma.css[type].map(item => {
193
+ if (typeof item.value === 'object') {
194
+ return item.shorthand || JSON.stringify(item.value);
195
+ }
196
+ return item.value;
197
+ }).filter(Boolean);
198
+ }
199
+
200
+ extractCSSValuesFromRegistry(registry, type) {
201
+ if (!registry.cssProperties) return [];
202
+ return registry.cssProperties
203
+ .filter(p => p.type === type)
204
+ .map(p => p.value);
205
+ }
206
+
207
+ /**
208
+ * Structure similarity based on depth, child count, etc.
209
+ */
210
+ calculateStructureScore(figma, registry) {
211
+ let score = 0;
212
+
213
+ // Component type match
214
+ if (figma.type === registry.type) {
215
+ score += 40;
216
+ } else if (
217
+ (figma.type === 'atom' && registry.type === 'molecule') ||
218
+ (figma.type === 'molecule' && registry.type === 'atom')
219
+ ) {
220
+ score += 20; // Adjacent types get partial credit
221
+ }
222
+
223
+ // Child count similarity
224
+ const figmaChildren = figma.structure?.childCount || figma.children?.length || 0;
225
+ const registryChildren = registry.childComponents?.length || 0;
226
+ const childDiff = Math.abs(figmaChildren - registryChildren);
227
+
228
+ if (childDiff === 0) score += 30;
229
+ else if (childDiff <= 2) score += 20;
230
+ else if (childDiff <= 4) score += 10;
231
+
232
+ // Depth similarity
233
+ const figmaDepth = figma.structure?.depth || 0;
234
+ const registryDepth = registry.structure?.depth || 0;
235
+ const depthDiff = Math.abs(figmaDepth - registryDepth);
236
+
237
+ if (depthDiff === 0) score += 30;
238
+ else if (depthDiff <= 1) score += 20;
239
+ else if (depthDiff <= 2) score += 10;
240
+
241
+ return score;
242
+ }
243
+
244
+ /**
245
+ * Name similarity using fuzzy matching
246
+ */
247
+ calculateNamingScore(figma, registry) {
248
+ const figmaName = this.normalizeName(figma.name);
249
+ const registryName = this.normalizeName(registry.name);
250
+
251
+ // Exact match
252
+ if (figmaName === registryName) return 100;
253
+
254
+ // Contains match
255
+ if (figmaName.includes(registryName) || registryName.includes(figmaName)) {
256
+ return 80;
257
+ }
258
+
259
+ // Word overlap
260
+ const figmaWords = figmaName.split(/[-_\s]/).filter(w => w.length > 2);
261
+ const registryWords = registryName.split(/[-_\s]/).filter(w => w.length > 2);
262
+ const commonWords = figmaWords.filter(w =>
263
+ registryWords.some(rw => rw.includes(w) || w.includes(rw))
264
+ );
265
+
266
+ if (commonWords.length > 0) {
267
+ return 60 + (commonWords.length / Math.max(figmaWords.length, registryWords.length)) * 40;
268
+ }
269
+
270
+ // Levenshtein distance
271
+ const distance = this.levenshteinDistance(figmaName, registryName);
272
+ const maxLen = Math.max(figmaName.length, registryName.length);
273
+ const similarity = 1 - (distance / maxLen);
274
+
275
+ return Math.round(similarity * 100);
276
+ }
277
+
278
+ normalizeName(name) {
279
+ return (name || '')
280
+ .toLowerCase()
281
+ .replace(/[^a-z0-9]/g, '')
282
+ .replace(/component|wrapper|container|item|view/g, '');
283
+ }
284
+
285
+ levenshteinDistance(str1, str2) {
286
+ const m = str1.length;
287
+ const n = str2.length;
288
+ const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
289
+
290
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
291
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
292
+
293
+ for (let i = 1; i <= m; i++) {
294
+ for (let j = 1; j <= n; j++) {
295
+ if (str1[i - 1] === str2[j - 1]) {
296
+ dp[i][j] = dp[i - 1][j - 1];
297
+ } else {
298
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
299
+ }
300
+ }
301
+ }
302
+
303
+ return dp[m][n];
304
+ }
305
+
306
+ /**
307
+ * Behavior similarity based on props and variants
308
+ */
309
+ calculateBehaviorScore(figma, registry) {
310
+ let score = 0;
311
+
312
+ // Check for similar variant properties
313
+ if (figma.figma?.variantProperties && registry.variants?.length > 0) {
314
+ const figmaVariants = Object.keys(figma.figma.variantProperties);
315
+ const registryVariants = registry.variants.map(v => v.name);
316
+
317
+ const commonVariants = figmaVariants.filter(v =>
318
+ registryVariants.some(rv => rv.toLowerCase() === v.toLowerCase())
319
+ );
320
+
321
+ if (commonVariants.length > 0) {
322
+ score += (commonVariants.length / Math.max(figmaVariants.length, registryVariants.length)) * 60;
323
+ }
324
+ }
325
+
326
+ // Check for Figma component match (if connected)
327
+ if (figma.figma?.isInstance && figma.figma?.componentId) {
328
+ score += 20;
329
+ }
330
+
331
+ // Check if both are same Figma type
332
+ if (figma.figma?.isComponent && registry.exports?.includes('default')) {
333
+ score += 20;
334
+ }
335
+
336
+ return Math.min(100, score);
337
+ }
338
+
339
+ /**
340
+ * Compare two arrays and return similarity (0-1)
341
+ */
342
+ compareArrays(arr1, arr2) {
343
+ if (arr1.length === 0 && arr2.length === 0) return 1;
344
+ if (arr1.length === 0 || arr2.length === 0) return 0;
345
+
346
+ // Normalize arrays for comparison
347
+ const norm1 = arr1.map(v => String(v).toLowerCase());
348
+ const norm2 = arr2.map(v => String(v).toLowerCase());
349
+
350
+ // Count matches (allowing partial matches for CSS values)
351
+ let matches = 0;
352
+ for (const val of norm1) {
353
+ if (norm2.some(v2 => {
354
+ // Exact match
355
+ if (v2 === val) return true;
356
+ // Contains match (e.g., "bg-blue-500" contains "blue")
357
+ if (v2.includes(val) || val.includes(v2)) return true;
358
+ // Number proximity (for spacing/sizing)
359
+ const num1 = parseFloat(val.replace(/[^0-9.-]/g, ''));
360
+ const num2 = parseFloat(v2.replace(/[^0-9.-]/g, ''));
361
+ if (!isNaN(num1) && !isNaN(num2)) {
362
+ return Math.abs(num1 - num2) <= Math.max(num1, num2) * 0.2; // Within 20%
363
+ }
364
+ return false;
365
+ })) {
366
+ matches++;
367
+ }
368
+ }
369
+
370
+ return matches / Math.max(norm1.length, norm2.length);
371
+ }
372
+
373
+ /**
374
+ * Get detailed score breakdown
375
+ */
376
+ getScoreBreakdown(figma, registry) {
377
+ return {
378
+ css: Math.round(this.calculateCSSScore(figma, registry)),
379
+ structure: Math.round(this.calculateStructureScore(figma, registry)),
380
+ naming: Math.round(this.calculateNamingScore(figma, registry)),
381
+ behavior: Math.round(this.calculateBehaviorScore(figma, registry))
382
+ };
383
+ }
384
+
385
+ /**
386
+ * Get differences between Figma component and registry component
387
+ */
388
+ getDifferences(figma, registry) {
389
+ const differences = [];
390
+
391
+ // Color differences
392
+ const figmaColors = this.extractCSSValues(figma, 'colors');
393
+ const registryColors = this.extractCSSValuesFromRegistry(registry, 'color');
394
+ const colorDiff = figmaColors.filter(c => !registryColors.includes(c));
395
+ if (colorDiff.length > 0) {
396
+ differences.push({
397
+ type: 'color',
398
+ figma: colorDiff.slice(0, 3), // Limit to 3
399
+ existing: registryColors.slice(0, 3),
400
+ description: `Different colors: ${colorDiff.slice(0, 3).join(', ')}`
401
+ });
402
+ }
403
+
404
+ // Spacing differences
405
+ const figmaSpacing = this.extractCSSValues(figma, 'spacing');
406
+ const registrySpacing = this.extractCSSValuesFromRegistry(registry, 'spacing');
407
+ const spacingDiff = figmaSpacing.filter(s => {
408
+ const val = parseInt(s) || 0;
409
+ return !registrySpacing.some(rs => {
410
+ const rsVal = parseInt(rs) || 0;
411
+ return Math.abs(val - rsVal) <= 4;
412
+ });
413
+ });
414
+ if (spacingDiff.length > 0) {
415
+ differences.push({
416
+ type: 'spacing',
417
+ figma: spacingDiff.slice(0, 3),
418
+ existing: registrySpacing.slice(0, 3),
419
+ description: `Different spacing: ${spacingDiff.slice(0, 3).join(', ')}`
420
+ });
421
+ }
422
+
423
+ // Structure differences
424
+ const figmaChildren = figma.structure?.childCount || 0;
425
+ const registryChildren = registry.childComponents?.length || 0;
426
+ if (Math.abs(figmaChildren - registryChildren) > 2) {
427
+ differences.push({
428
+ type: 'structure',
429
+ figma: figmaChildren,
430
+ existing: registryChildren,
431
+ description: `Child count: Figma has ${figmaChildren}, existing has ${registryChildren}`
432
+ });
433
+ }
434
+
435
+ // Type differences
436
+ if (figma.type !== registry.type) {
437
+ differences.push({
438
+ type: 'componentType',
439
+ figma: figma.type,
440
+ existing: registry.type,
441
+ description: `Type: Figma is ${figma.type}, existing is ${registry.type}`
442
+ });
443
+ }
444
+
445
+ return differences;
446
+ }
447
+
448
+ /**
449
+ * Get suggestion based on score
450
+ */
451
+ getSuggestion(score) {
452
+ if (score >= MATCH_CONFIG.thresholds.EXACT_MATCH) {
453
+ return 'use';
454
+ } else if (score >= MATCH_CONFIG.thresholds.STRONG_MATCH) {
455
+ return 'use-with-adjustments';
456
+ } else if (score >= MATCH_CONFIG.thresholds.VARIANT_CANDIDATE) {
457
+ return 'add-variant';
458
+ } else {
459
+ return 'create-new';
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Get overall suggestion for a component
465
+ */
466
+ getOverallSuggestion(figma, bestMatch) {
467
+ if (!bestMatch) {
468
+ return {
469
+ action: 'create-new',
470
+ reason: 'No matching components found in codebase',
471
+ confidence: 'high'
472
+ };
473
+ }
474
+
475
+ const score = bestMatch.score;
476
+
477
+ if (score >= MATCH_CONFIG.thresholds.EXACT_MATCH) {
478
+ return {
479
+ action: 'use',
480
+ component: bestMatch.registryComponent.name,
481
+ path: bestMatch.registryComponent.path,
482
+ reason: `${score}% match - use existing component directly`,
483
+ confidence: 'high'
484
+ };
485
+ }
486
+
487
+ if (score >= MATCH_CONFIG.thresholds.STRONG_MATCH) {
488
+ return {
489
+ action: 'use-with-adjustments',
490
+ component: bestMatch.registryComponent.name,
491
+ path: bestMatch.registryComponent.path,
492
+ differences: bestMatch.differences,
493
+ reason: `${score}% match - use with minor adjustments`,
494
+ confidence: 'medium'
495
+ };
496
+ }
497
+
498
+ if (score >= MATCH_CONFIG.thresholds.VARIANT_CANDIDATE) {
499
+ return {
500
+ action: 'add-variant',
501
+ component: bestMatch.registryComponent.name,
502
+ path: bestMatch.registryComponent.path,
503
+ differences: bestMatch.differences,
504
+ reason: `${score}% match - consider adding as new variant`,
505
+ confidence: 'medium',
506
+ suggestedVariantName: this.suggestVariantName(figma, bestMatch.registryComponent)
507
+ };
508
+ }
509
+
510
+ return {
511
+ action: 'create-new',
512
+ similarTo: bestMatch.registryComponent.name,
513
+ reason: `Only ${score}% match - recommend creating new component`,
514
+ confidence: 'high',
515
+ suggestedName: this.suggestComponentName(figma)
516
+ };
517
+ }
518
+
519
+ suggestVariantName(figma, registry) {
520
+ const figmaWords = (figma.name || '').split(/[-_\s]/).filter(w => w.length > 2);
521
+ const registryWords = (registry.name || '').split(/[-_\s]/).filter(w => w.length > 2);
522
+ const uniqueWords = figmaWords.filter(w =>
523
+ !registryWords.some(rw => rw.toLowerCase() === w.toLowerCase())
524
+ );
525
+
526
+ if (uniqueWords.length > 0) {
527
+ return uniqueWords[0].toLowerCase();
528
+ }
529
+
530
+ return 'variant';
531
+ }
532
+
533
+ suggestComponentName(figma) {
534
+ return (figma.name || 'Component')
535
+ .replace(/[^a-zA-Z0-9\s]/g, '')
536
+ .split(/\s+/)
537
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
538
+ .join('');
539
+ }
540
+ }
541
+
542
+ // ============================================================
543
+ // CLI
544
+ // ============================================================
545
+
546
+ async function main() {
547
+ const [,, input, ...args] = process.argv;
548
+
549
+ // Load registry
550
+ if (!fs.existsSync(REGISTRY_PATH)) {
551
+ console.error('❌ Component registry not found.');
552
+ console.error(' Run "flow figma scan" first to build the registry.');
553
+ process.exit(1);
554
+ }
555
+
556
+ const registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8'));
557
+ const matcher = new SimilarityMatcher(registry);
558
+
559
+ // Parse threshold argument
560
+ let threshold = MATCH_CONFIG.thresholds.VARIANT_CANDIDATE;
561
+ const thresholdIndex = args.indexOf('--threshold');
562
+ if (thresholdIndex !== -1 && args[thresholdIndex + 1]) {
563
+ threshold = parseInt(args[thresholdIndex + 1]);
564
+ }
565
+
566
+ if (input === '--stdin') {
567
+ // Read from stdin
568
+ let data = '';
569
+ process.stdin.setEncoding('utf8');
570
+
571
+ for await (const chunk of process.stdin) {
572
+ data += chunk;
573
+ }
574
+
575
+ const figmaData = JSON.parse(data);
576
+ const components = figmaData.components || [figmaData];
577
+
578
+ const results = matcher.matchAll(components);
579
+ console.log(JSON.stringify(results, null, 2));
580
+
581
+ } else if (input && fs.existsSync(input)) {
582
+ // Match from file
583
+ const figmaData = JSON.parse(fs.readFileSync(input, 'utf-8'));
584
+ const components = figmaData.components || [figmaData];
585
+
586
+ const results = matcher.matchAll(components);
587
+ console.log(JSON.stringify(results, null, 2));
588
+
589
+ } else {
590
+ console.log(`
591
+ Wogi Flow - Component Similarity Matcher
592
+
593
+ Usage:
594
+ flow figma match <figma-components.json> Match against registry
595
+ flow figma match --stdin Read from stdin
596
+ flow figma match --threshold 80 Set match threshold
597
+
598
+ Thresholds:
599
+ 95%+ = Use directly (exact match)
600
+ 80-95% = Use with adjustments (strong match)
601
+ 60-80% = Consider as variant
602
+ <60% = Create new component
603
+
604
+ Example:
605
+ ./scripts/flow-figma-extract.js figma-data.json | ./scripts/flow-figma-match.js --stdin
606
+ `);
607
+ }
608
+ }
609
+
610
+ module.exports = { SimilarityMatcher, MATCH_CONFIG };
611
+
612
+ if (require.main === module) {
613
+ main().catch(err => {
614
+ console.error(`Error: ${err.message}`);
615
+ process.exit(1);
616
+ });
617
+ }