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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- 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
|
+
}
|