canicode 0.8.1 → 0.8.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.
package/dist/cli/index.js CHANGED
@@ -974,417 +974,8 @@ async function loadFromApi(fileKey, nodeId, token) {
974
974
  };
975
975
  }
976
976
 
977
- // src/core/engine/scoring.ts
978
- var SEVERITY_DENSITY_WEIGHT = {
979
- blocking: 3,
980
- risk: 2,
981
- "missing-info": 1,
982
- suggestion: 0.5
983
- };
984
- var TOTAL_RULES_PER_CATEGORY = {
985
- layout: 11,
986
- token: 7,
987
- component: 6,
988
- naming: 5,
989
- "ai-readability": 5,
990
- "handoff-risk": 5
991
- };
992
- var CATEGORY_WEIGHT = {
993
- layout: 1,
994
- token: 1,
995
- component: 1,
996
- naming: 1,
997
- "ai-readability": 1,
998
- "handoff-risk": 1
999
- };
1000
- var DENSITY_WEIGHT = 0.7;
1001
- var DIVERSITY_WEIGHT = 0.3;
1002
- var SCORE_FLOOR = 5;
1003
- function calculateGrade(percentage) {
1004
- if (percentage >= 95) return "S";
1005
- if (percentage >= 90) return "A+";
1006
- if (percentage >= 85) return "A";
1007
- if (percentage >= 80) return "B+";
1008
- if (percentage >= 75) return "B";
1009
- if (percentage >= 70) return "C+";
1010
- if (percentage >= 65) return "C";
1011
- if (percentage >= 50) return "D";
1012
- return "F";
1013
- }
1014
- function clamp(value, min, max) {
1015
- return Math.max(min, Math.min(max, value));
1016
- }
1017
- function calculateScores(result) {
1018
- const categoryScores = initializeCategoryScores();
1019
- const nodeCount = result.nodeCount;
1020
- const uniqueRulesPerCategory = /* @__PURE__ */ new Map();
1021
- for (const category of CATEGORIES) {
1022
- uniqueRulesPerCategory.set(category, /* @__PURE__ */ new Set());
1023
- }
1024
- for (const issue of result.issues) {
1025
- const category = issue.rule.definition.category;
1026
- const severity = issue.config.severity;
1027
- const ruleId = issue.rule.definition.id;
1028
- categoryScores[category].issueCount++;
1029
- categoryScores[category].bySeverity[severity]++;
1030
- categoryScores[category].weightedIssueCount += SEVERITY_DENSITY_WEIGHT[severity];
1031
- uniqueRulesPerCategory.get(category).add(ruleId);
1032
- }
1033
- for (const category of CATEGORIES) {
1034
- const catScore = categoryScores[category];
1035
- const uniqueRules = uniqueRulesPerCategory.get(category);
1036
- const totalRules = TOTAL_RULES_PER_CATEGORY[category];
1037
- catScore.uniqueRuleCount = uniqueRules.size;
1038
- let densityScore = 100;
1039
- if (nodeCount > 0 && catScore.issueCount > 0) {
1040
- const density = catScore.weightedIssueCount / nodeCount;
1041
- densityScore = clamp(Math.round(100 - density * 100), 0, 100);
1042
- }
1043
- catScore.densityScore = densityScore;
1044
- let diversityScore = 100;
1045
- if (catScore.issueCount > 0) {
1046
- const diversityRatio = uniqueRules.size / totalRules;
1047
- diversityScore = clamp(Math.round((1 - diversityRatio) * 100), 0, 100);
1048
- }
1049
- catScore.diversityScore = diversityScore;
1050
- const combinedScore = densityScore * DENSITY_WEIGHT + diversityScore * DIVERSITY_WEIGHT;
1051
- catScore.percentage = catScore.issueCount > 0 ? clamp(Math.round(combinedScore), SCORE_FLOOR, 100) : 100;
1052
- catScore.score = catScore.percentage;
1053
- catScore.maxScore = 100;
1054
- }
1055
- let totalWeight = 0;
1056
- let weightedSum = 0;
1057
- for (const category of CATEGORIES) {
1058
- const weight = CATEGORY_WEIGHT[category];
1059
- weightedSum += categoryScores[category].percentage * weight;
1060
- totalWeight += weight;
1061
- }
1062
- const overallPercentage = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 100;
1063
- const summary = {
1064
- totalIssues: result.issues.length,
1065
- blocking: 0,
1066
- risk: 0,
1067
- missingInfo: 0,
1068
- suggestion: 0,
1069
- nodeCount
1070
- };
1071
- for (const issue of result.issues) {
1072
- switch (issue.config.severity) {
1073
- case "blocking":
1074
- summary.blocking++;
1075
- break;
1076
- case "risk":
1077
- summary.risk++;
1078
- break;
1079
- case "missing-info":
1080
- summary.missingInfo++;
1081
- break;
1082
- case "suggestion":
1083
- summary.suggestion++;
1084
- break;
1085
- }
1086
- }
1087
- return {
1088
- overall: {
1089
- score: overallPercentage,
1090
- maxScore: 100,
1091
- percentage: overallPercentage,
1092
- grade: calculateGrade(overallPercentage)
1093
- },
1094
- byCategory: categoryScores,
1095
- summary
1096
- };
1097
- }
1098
- function initializeCategoryScores() {
1099
- const scores = {};
1100
- for (const category of CATEGORIES) {
1101
- scores[category] = {
1102
- category,
1103
- score: 100,
1104
- maxScore: 100,
1105
- percentage: 100,
1106
- issueCount: 0,
1107
- uniqueRuleCount: 0,
1108
- weightedIssueCount: 0,
1109
- densityScore: 100,
1110
- diversityScore: 100,
1111
- bySeverity: {
1112
- blocking: 0,
1113
- risk: 0,
1114
- "missing-info": 0,
1115
- suggestion: 0
1116
- }
1117
- };
1118
- }
1119
- return scores;
1120
- }
1121
- function formatScoreSummary(report) {
1122
- const lines = [];
1123
- lines.push(`Overall: ${report.overall.grade} (${report.overall.percentage}%)`);
1124
- lines.push("");
1125
- lines.push("By Category:");
1126
- for (const category of CATEGORIES) {
1127
- const cat = report.byCategory[category];
1128
- lines.push(` ${category}: ${cat.percentage}% (${cat.issueCount} issues, ${cat.uniqueRuleCount} rules)`);
1129
- }
1130
- lines.push("");
1131
- lines.push("Issues:");
1132
- lines.push(` Blocking: ${report.summary.blocking}`);
1133
- lines.push(` Risk: ${report.summary.risk}`);
1134
- lines.push(` Missing Info: ${report.summary.missingInfo}`);
1135
- lines.push(` Suggestion: ${report.summary.suggestion}`);
1136
- lines.push(` Total: ${report.summary.totalIssues}`);
1137
- return lines.join("\n");
1138
- }
1139
- var MatchConditionSchema = z.object({
1140
- // Node type conditions
1141
- type: z.array(z.string()).optional(),
1142
- notType: z.array(z.string()).optional(),
1143
- // Name conditions (case-insensitive, substring match)
1144
- nameContains: z.string().optional(),
1145
- nameNotContains: z.string().optional(),
1146
- namePattern: z.string().optional(),
1147
- // Size conditions
1148
- minWidth: z.number().optional(),
1149
- maxWidth: z.number().optional(),
1150
- minHeight: z.number().optional(),
1151
- maxHeight: z.number().optional(),
1152
- // Layout conditions
1153
- hasAutoLayout: z.boolean().optional(),
1154
- hasChildren: z.boolean().optional(),
1155
- minChildren: z.number().optional(),
1156
- maxChildren: z.number().optional(),
1157
- // Component conditions
1158
- isComponent: z.boolean().optional(),
1159
- isInstance: z.boolean().optional(),
1160
- hasComponentId: z.boolean().optional(),
1161
- // Visibility
1162
- isVisible: z.boolean().optional(),
1163
- // Fill/style conditions
1164
- hasFills: z.boolean().optional(),
1165
- hasStrokes: z.boolean().optional(),
1166
- hasEffects: z.boolean().optional(),
1167
- // Depth condition
1168
- minDepth: z.number().optional(),
1169
- maxDepth: z.number().optional()
1170
- });
1171
- var CustomRuleSchema = z.object({
1172
- id: z.string(),
1173
- category: CategorySchema,
1174
- severity: SeveritySchema,
1175
- score: z.number().int().max(0),
1176
- match: MatchConditionSchema,
1177
- message: z.string().optional(),
1178
- why: z.string(),
1179
- impact: z.string(),
1180
- fix: z.string(),
1181
- // Backward compat: silently ignore the old prompt field
1182
- prompt: z.string().optional()
1183
- });
1184
- var CustomRulesFileSchema = z.array(CustomRuleSchema);
1185
-
1186
- // src/core/rules/custom/custom-rule-loader.ts
1187
- async function loadCustomRules(filePath) {
1188
- const absPath = resolve(filePath);
1189
- const raw = await readFile(absPath, "utf-8");
1190
- const parsed = JSON.parse(raw);
1191
- const customRules = CustomRulesFileSchema.parse(parsed);
1192
- const rules = [];
1193
- const configs = {};
1194
- for (const cr of customRules) {
1195
- if (!cr.match) continue;
1196
- rules.push(toRule(cr));
1197
- configs[cr.id] = {
1198
- severity: cr.severity,
1199
- score: cr.score,
1200
- enabled: true
1201
- };
1202
- }
1203
- return { rules, configs };
1204
- }
1205
- function toRule(cr) {
1206
- return {
1207
- definition: {
1208
- id: cr.id,
1209
- name: cr.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
1210
- category: cr.category,
1211
- why: cr.why,
1212
- impact: cr.impact,
1213
- fix: cr.fix
1214
- },
1215
- check: createPatternCheck(cr)
1216
- };
1217
- }
1218
- function createPatternCheck(cr) {
1219
- return (node, context) => {
1220
- if (node.type === "DOCUMENT" || node.type === "CANVAS") return null;
1221
- const match = cr.match;
1222
- if (match.type && !match.type.includes(node.type)) return null;
1223
- if (match.notType && match.notType.includes(node.type)) return null;
1224
- if (match.nameContains !== void 0 && !node.name.toLowerCase().includes(match.nameContains.toLowerCase())) return null;
1225
- if (match.nameNotContains !== void 0 && node.name.toLowerCase().includes(match.nameNotContains.toLowerCase())) return null;
1226
- if (match.namePattern !== void 0 && !new RegExp(match.namePattern, "i").test(node.name)) return null;
1227
- const bbox = node.absoluteBoundingBox;
1228
- if (match.minWidth !== void 0 && (!bbox || bbox.width < match.minWidth)) return null;
1229
- if (match.maxWidth !== void 0 && (!bbox || bbox.width > match.maxWidth)) return null;
1230
- if (match.minHeight !== void 0 && (!bbox || bbox.height < match.minHeight)) return null;
1231
- if (match.maxHeight !== void 0 && (!bbox || bbox.height > match.maxHeight)) return null;
1232
- if (match.hasAutoLayout === true && !node.layoutMode) return null;
1233
- if (match.hasAutoLayout === false && node.layoutMode) return null;
1234
- if (match.hasChildren === true && (!node.children || node.children.length === 0)) return null;
1235
- if (match.hasChildren === false && node.children && node.children.length > 0) return null;
1236
- if (match.minChildren !== void 0 && (!node.children || node.children.length < match.minChildren)) return null;
1237
- if (match.maxChildren !== void 0 && node.children && node.children.length > match.maxChildren) return null;
1238
- if (match.isComponent === true && node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
1239
- if (match.isComponent === false && (node.type === "COMPONENT" || node.type === "COMPONENT_SET")) return null;
1240
- if (match.isInstance === true && node.type !== "INSTANCE") return null;
1241
- if (match.isInstance === false && node.type === "INSTANCE") return null;
1242
- if (match.hasComponentId === true && !node.componentId) return null;
1243
- if (match.hasComponentId === false && node.componentId) return null;
1244
- if (match.isVisible === true && !node.visible) return null;
1245
- if (match.isVisible === false && node.visible) return null;
1246
- if (match.hasFills === true && (!node.fills || node.fills.length === 0)) return null;
1247
- if (match.hasFills === false && node.fills && node.fills.length > 0) return null;
1248
- if (match.hasStrokes === true && (!node.strokes || node.strokes.length === 0)) return null;
1249
- if (match.hasStrokes === false && node.strokes && node.strokes.length > 0) return null;
1250
- if (match.hasEffects === true && (!node.effects || node.effects.length === 0)) return null;
1251
- if (match.hasEffects === false && node.effects && node.effects.length > 0) return null;
1252
- if (match.minDepth !== void 0 && context.depth < match.minDepth) return null;
1253
- if (match.maxDepth !== void 0 && context.depth > match.maxDepth) return null;
1254
- const msg = (cr.message ?? `"${node.name}" matched custom rule "${cr.id}"`).replace(/\{name\}/g, node.name).replace(/\{type\}/g, node.type);
1255
- return {
1256
- ruleId: cr.id,
1257
- nodeId: node.id,
1258
- nodePath: context.path.join(" > "),
1259
- message: msg
1260
- };
1261
- };
1262
- }
1263
- var RuleOverrideSchema = z.object({
1264
- score: z.number().int().max(0).optional(),
1265
- severity: SeveritySchema.optional(),
1266
- enabled: z.boolean().optional()
1267
- });
1268
- var ConfigFileSchema = z.object({
1269
- excludeNodeTypes: z.array(z.string()).optional(),
1270
- excludeNodeNames: z.array(z.string()).optional(),
1271
- gridBase: z.number().int().positive().optional(),
1272
- colorTolerance: z.number().int().positive().optional(),
1273
- rules: z.record(z.string(), RuleOverrideSchema).optional()
1274
- });
1275
- async function loadConfigFile(filePath) {
1276
- const absPath = resolve(filePath);
1277
- const raw = await readFile(absPath, "utf-8");
1278
- const parsed = JSON.parse(raw);
1279
- return ConfigFileSchema.parse(parsed);
1280
- }
1281
- function mergeConfigs(base, overrides) {
1282
- const merged = { ...base };
1283
- if (overrides.gridBase !== void 0) {
1284
- for (const [id, config2] of Object.entries(merged)) {
1285
- if (config2.options && "gridBase" in config2.options) {
1286
- merged[id] = {
1287
- ...config2,
1288
- options: { ...config2.options, gridBase: overrides.gridBase }
1289
- };
1290
- }
1291
- }
1292
- }
1293
- if (overrides.colorTolerance !== void 0) {
1294
- for (const [id, config2] of Object.entries(merged)) {
1295
- if (config2.options && "tolerance" in config2.options) {
1296
- merged[id] = {
1297
- ...config2,
1298
- options: { ...config2.options, tolerance: overrides.colorTolerance }
1299
- };
1300
- }
1301
- }
1302
- }
1303
- if (overrides.rules) {
1304
- for (const [ruleId, override] of Object.entries(overrides.rules)) {
1305
- const existing = merged[ruleId];
1306
- if (existing) {
1307
- merged[ruleId] = {
1308
- ...existing,
1309
- ...override.score !== void 0 && { score: override.score },
1310
- ...override.severity !== void 0 && { severity: override.severity },
1311
- ...override.enabled !== void 0 && { enabled: override.enabled }
1312
- };
1313
- }
1314
- }
1315
- }
1316
- return merged;
1317
- }
1318
-
1319
- // src/core/ui-constants.ts
1320
- var GAUGE_R = 54;
1321
- var GAUGE_C = Math.round(2 * Math.PI * GAUGE_R);
1322
- var CATEGORY_DESCRIPTIONS = {
1323
- layout: "Auto Layout, responsive constraints, nesting depth, absolute positioning",
1324
- token: "Design token binding for colors, fonts, shadows, spacing grid",
1325
- component: "Component reuse, detached instances, variant coverage",
1326
- naming: "Semantic layer names, naming conventions, default names",
1327
- "ai-readability": "Structure clarity for AI code generation, z-index, empty frames",
1328
- "handoff-risk": "Hardcoded values, text truncation, image placeholders, dev status"
1329
- };
1330
- var SEVERITY_ORDER = [
1331
- "blocking",
1332
- "risk",
1333
- "missing-info",
1334
- "suggestion"
1335
- ];
1336
-
1337
- // src/core/ui-helpers.ts
1338
- function gaugeColor(pct) {
1339
- if (pct >= 75) return "#22c55e";
1340
- if (pct >= 50) return "#f59e0b";
1341
- return "#ef4444";
1342
- }
1343
- function escapeHtml(text) {
1344
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1345
- }
1346
- function severityDot(sev) {
1347
- const map = {
1348
- blocking: "bg-red-500",
1349
- risk: "bg-amber-500",
1350
- "missing-info": "bg-zinc-400",
1351
- suggestion: "bg-green-500"
1352
- };
1353
- return map[sev];
1354
- }
1355
- function severityBadge(sev) {
1356
- const map = {
1357
- blocking: "bg-red-500/10 text-red-600 border-red-500/20",
1358
- risk: "bg-amber-500/10 text-amber-600 border-amber-500/20",
1359
- "missing-info": "bg-zinc-500/10 text-zinc-600 border-zinc-500/20",
1360
- suggestion: "bg-green-500/10 text-green-600 border-green-500/20"
1361
- };
1362
- return map[sev];
1363
- }
1364
- function scoreBadgeStyle(pct) {
1365
- if (pct >= 75) return "bg-green-500/10 text-green-700 border-green-500/20";
1366
- if (pct >= 50) return "bg-amber-500/10 text-amber-700 border-amber-500/20";
1367
- return "bg-red-500/10 text-red-700 border-red-500/20";
1368
- }
1369
- function renderGaugeSvg(pct, size, strokeW, grade) {
1370
- const offset = GAUGE_C * (1 - pct / 100);
1371
- const color = gaugeColor(pct);
1372
- if (grade) {
1373
- return `<svg width="${size}" height="${size}" viewBox="0 0 120 120" class="gauge-svg block">
1374
- <circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke-width="${strokeW}" stroke="#e4e4e7" class="stroke-border" />
1375
- <circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-dasharray="${GAUGE_C}" stroke-dashoffset="${offset}" transform="rotate(-90 60 60)" class="gauge-fill" />
1376
- <text x="60" y="60" text-anchor="middle" dominant-baseline="central" fill="currentColor" font-size="48" font-weight="700" font-family="Inter,-apple-system,sans-serif" class="font-sans">${escapeHtml(grade)}</text>
1377
- </svg>`;
1378
- }
1379
- return `<svg width="${size}" height="${size}" viewBox="0 0 120 120" class="gauge-svg block">
1380
- <circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke-width="${strokeW}" stroke="#e4e4e7" class="stroke-border" />
1381
- <circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-dasharray="${GAUGE_C}" stroke-dashoffset="${offset}" transform="rotate(-90 60 60)" class="gauge-fill" />
1382
- <text x="60" y="62" text-anchor="middle" dominant-baseline="central" fill="currentColor" font-size="28" font-weight="700" font-family="Inter,-apple-system,sans-serif" class="font-sans">${pct}</text>
1383
- </svg>`;
1384
- }
1385
-
1386
977
  // package.json
1387
- var version = "0.8.1";
978
+ var version = "0.8.2";
1388
979
  var AnalysisNodeTypeSchema = z.enum([
1389
980
  "DOCUMENT",
1390
981
  "CANVAS",
@@ -3239,434 +2830,863 @@ function renderValidatedRules(validatedRules) {
3239
2830
  for (const ruleId of validatedRules) {
3240
2831
  lines.push(`- \`${ruleId}\``);
3241
2832
  }
3242
- lines.push("");
3243
- return lines.join("\n");
2833
+ lines.push("");
2834
+ return lines.join("\n");
2835
+ }
2836
+ function renderMismatchDetails(mismatches) {
2837
+ if (mismatches.length === 0) {
2838
+ return "## Detailed Mismatch List\n\nNo mismatches found.\n";
2839
+ }
2840
+ const lines = [];
2841
+ lines.push("## Detailed Mismatch List");
2842
+ lines.push("");
2843
+ const grouped = {};
2844
+ for (const m of mismatches) {
2845
+ const list = grouped[m.type];
2846
+ if (list) {
2847
+ list.push(m);
2848
+ } else {
2849
+ grouped[m.type] = [m];
2850
+ }
2851
+ }
2852
+ for (const [type, cases] of Object.entries(grouped)) {
2853
+ lines.push(`### ${type} (${cases.length})`);
2854
+ lines.push("");
2855
+ for (const c of cases) {
2856
+ const ruleInfo = c.ruleId ? ` | Rule: \`${c.ruleId}\`` : "";
2857
+ const scoreInfo = c.currentScore !== void 0 ? ` | Score: ${c.currentScore}` : "";
2858
+ lines.push(`- **${c.nodePath}** (${c.nodeId})${ruleInfo}${scoreInfo} | Difficulty: ${c.actualDifficulty}`);
2859
+ lines.push(` > ${c.reasoning}`);
2860
+ }
2861
+ lines.push("");
2862
+ }
2863
+ return lines.join("\n");
2864
+ }
2865
+ function renderApplicationGuide(adjustments) {
2866
+ const lines = [];
2867
+ lines.push("## Application Guide");
2868
+ lines.push("");
2869
+ lines.push("To apply these calibration results:");
2870
+ lines.push("");
2871
+ lines.push("1. Review each adjustment proposal above");
2872
+ lines.push("2. Edit `src/core/rules/rule-config.ts` to update scores and severities");
2873
+ lines.push("3. Run `pnpm test:run` to verify no tests break");
2874
+ lines.push("4. Re-run calibration to confirm improvements");
2875
+ lines.push("");
2876
+ if (adjustments.length > 0) {
2877
+ lines.push("### Suggested Changes to `rule-config.ts`");
2878
+ lines.push("");
2879
+ lines.push("```typescript");
2880
+ for (const adj of adjustments) {
2881
+ lines.push(`// ${adj.ruleId}: ${adj.currentScore} -> ${adj.proposedScore} (${adj.confidence} confidence)`);
2882
+ if (adj.proposedSeverity) {
2883
+ lines.push(`// severity: "${adj.currentSeverity}" -> "${adj.proposedSeverity}"`);
2884
+ }
2885
+ }
2886
+ lines.push("```");
2887
+ lines.push("");
2888
+ }
2889
+ return lines.join("\n");
2890
+ }
2891
+ function getTimestamp() {
2892
+ const now = /* @__PURE__ */ new Date();
2893
+ return now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
2894
+ }
2895
+ function getDateTimeString() {
2896
+ const now = /* @__PURE__ */ new Date();
2897
+ const year = now.getFullYear();
2898
+ const month = String(now.getMonth() + 1).padStart(2, "0");
2899
+ const day = String(now.getDate()).padStart(2, "0");
2900
+ const hours = String(now.getHours()).padStart(2, "0");
2901
+ const minutes = String(now.getMinutes()).padStart(2, "0");
2902
+ return `${year}-${month}-${day}-${hours}-${minutes}`;
2903
+ }
2904
+ function extractFixtureName(fixturePath) {
2905
+ const fileName = fixturePath.split("/").pop() ?? fixturePath;
2906
+ return fileName.replace(/\.json$/, "");
2907
+ }
2908
+ var ActivityLogger = class {
2909
+ logPath;
2910
+ initialized = false;
2911
+ constructor(fixturePath, logDir = "logs/activity") {
2912
+ const dateTimeStr = getDateTimeString();
2913
+ const fixtureName = fixturePath ? extractFixtureName(fixturePath) : "unknown";
2914
+ this.logPath = resolve(logDir, `${dateTimeStr}-${fixtureName}.md`);
2915
+ }
2916
+ /**
2917
+ * Ensure the log directory and file header exist
2918
+ */
2919
+ async ensureInitialized() {
2920
+ if (this.initialized) return;
2921
+ const dir = dirname(this.logPath);
2922
+ if (!existsSync(dir)) {
2923
+ mkdirSync(dir, { recursive: true });
2924
+ }
2925
+ if (!existsSync(this.logPath)) {
2926
+ const ts = getDateTimeString();
2927
+ await writeFile(this.logPath, `# Calibration Activity Log \u2014 ${ts}
2928
+
2929
+ `, "utf-8");
2930
+ }
2931
+ this.initialized = true;
2932
+ }
2933
+ /**
2934
+ * Log a pipeline step
2935
+ */
2936
+ async logStep(activity) {
2937
+ await this.ensureInitialized();
2938
+ const lines = [];
2939
+ lines.push(`## ${getTimestamp()} \u2014 ${activity.step}`);
2940
+ if (activity.nodePath) {
2941
+ lines.push(`- Node: ${activity.nodePath}`);
2942
+ }
2943
+ lines.push(`- Result: ${activity.result}`);
2944
+ lines.push(`- Duration: ${activity.durationMs}ms`);
2945
+ lines.push("");
2946
+ await appendFile(this.logPath, lines.join("\n") + "\n", "utf-8");
2947
+ }
2948
+ /**
2949
+ * Log a summary at pipeline completion
2950
+ */
2951
+ async logSummary(summary) {
2952
+ await this.ensureInitialized();
2953
+ const lines = [];
2954
+ lines.push(`## ${getTimestamp()} \u2014 Pipeline Summary`);
2955
+ lines.push(`- Status: ${summary.status}`);
2956
+ lines.push(`- Total Duration: ${summary.totalDurationMs}ms`);
2957
+ lines.push(`- Nodes Analyzed: ${summary.nodesAnalyzed}`);
2958
+ lines.push(`- Nodes Converted: ${summary.nodesConverted}`);
2959
+ lines.push(`- Mismatches Found: ${summary.mismatches}`);
2960
+ lines.push(`- Adjustments Proposed: ${summary.adjustments}`);
2961
+ lines.push("");
2962
+ lines.push("---");
2963
+ lines.push("");
2964
+ await appendFile(this.logPath, lines.join("\n") + "\n", "utf-8");
2965
+ }
2966
+ getLogPath() {
2967
+ return this.logPath;
2968
+ }
2969
+ };
2970
+
2971
+ // src/agents/orchestrator.ts
2972
+ function selectNodes(summaries, strategy, maxNodes) {
2973
+ if (summaries.length === 0) return [];
2974
+ switch (strategy) {
2975
+ case "all":
2976
+ return summaries.slice(0, maxNodes);
2977
+ case "random": {
2978
+ const shuffled = [...summaries];
2979
+ for (let i = shuffled.length - 1; i > 0; i--) {
2980
+ const j = Math.floor(Math.random() * (i + 1));
2981
+ const temp = shuffled[i];
2982
+ shuffled[i] = shuffled[j];
2983
+ shuffled[j] = temp;
2984
+ }
2985
+ return shuffled.slice(0, maxNodes);
2986
+ }
2987
+ case "top-issues":
2988
+ default:
2989
+ return summaries.slice(0, maxNodes);
2990
+ }
2991
+ }
2992
+ var EXCLUDED_NODE_TYPES = /* @__PURE__ */ new Set([
2993
+ "VECTOR",
2994
+ "BOOLEAN_OPERATION",
2995
+ "STAR",
2996
+ "REGULAR_POLYGON",
2997
+ "ELLIPSE",
2998
+ "LINE"
2999
+ ]);
3000
+ function findNode(root, nodeId) {
3001
+ if (root.id === nodeId) return root;
3002
+ if (root.children) {
3003
+ for (const child of root.children) {
3004
+ const found = findNode(child, nodeId);
3005
+ if (found) return found;
3006
+ }
3007
+ }
3008
+ return null;
3244
3009
  }
3245
- function renderMismatchDetails(mismatches) {
3246
- if (mismatches.length === 0) {
3247
- return "## Detailed Mismatch List\n\nNo mismatches found.\n";
3010
+ function hasTextDescendant(node) {
3011
+ if (node.type === "TEXT") return true;
3012
+ if (node.children) {
3013
+ for (const child of node.children) {
3014
+ if (hasTextDescendant(child)) return true;
3015
+ }
3248
3016
  }
3249
- const lines = [];
3250
- lines.push("## Detailed Mismatch List");
3251
- lines.push("");
3252
- const grouped = {};
3253
- for (const m of mismatches) {
3254
- const list = grouped[m.type];
3255
- if (list) {
3256
- list.push(m);
3257
- } else {
3258
- grouped[m.type] = [m];
3017
+ return false;
3018
+ }
3019
+ var MIN_WIDTH = 200;
3020
+ var MIN_HEIGHT = 200;
3021
+ var ELIGIBLE_NODE_TYPES = /* @__PURE__ */ new Set([
3022
+ "FRAME",
3023
+ "COMPONENT",
3024
+ "INSTANCE"
3025
+ ]);
3026
+ function filterConversionCandidates(summaries, documentRoot) {
3027
+ return summaries.filter((summary) => {
3028
+ const node = findNode(documentRoot, summary.nodeId);
3029
+ if (!node) return false;
3030
+ if (EXCLUDED_NODE_TYPES.has(node.type)) return false;
3031
+ if (!ELIGIBLE_NODE_TYPES.has(node.type)) return false;
3032
+ if (isExcludedName(node.name)) return false;
3033
+ const bbox = node.absoluteBoundingBox;
3034
+ if (bbox && (bbox.width < MIN_WIDTH || bbox.height < MIN_HEIGHT)) return false;
3035
+ if (!node.children || node.children.length < 3) return false;
3036
+ if (!hasTextDescendant(node)) return false;
3037
+ return true;
3038
+ });
3039
+ }
3040
+ function isFigmaUrl2(input) {
3041
+ return input.includes("figma.com/");
3042
+ }
3043
+ function isJsonFile2(input) {
3044
+ return input.endsWith(".json");
3045
+ }
3046
+ async function loadFile2(input, token) {
3047
+ if (isJsonFile2(input)) {
3048
+ const filePath = resolve(input);
3049
+ if (!existsSync(filePath)) {
3050
+ throw new Error(`File not found: ${filePath}`);
3259
3051
  }
3052
+ const file = await loadFigmaFileFromJson(filePath);
3053
+ return { file, fileKey: file.fileKey, nodeId: void 0 };
3260
3054
  }
3261
- for (const [type, cases] of Object.entries(grouped)) {
3262
- lines.push(`### ${type} (${cases.length})`);
3263
- lines.push("");
3264
- for (const c of cases) {
3265
- const ruleInfo = c.ruleId ? ` | Rule: \`${c.ruleId}\`` : "";
3266
- const scoreInfo = c.currentScore !== void 0 ? ` | Score: ${c.currentScore}` : "";
3267
- lines.push(`- **${c.nodePath}** (${c.nodeId})${ruleInfo}${scoreInfo} | Difficulty: ${c.actualDifficulty}`);
3268
- lines.push(` > ${c.reasoning}`);
3055
+ if (isFigmaUrl2(input)) {
3056
+ const { fileKey, nodeId } = parseFigmaUrl(input);
3057
+ const figmaToken = token ?? process.env["FIGMA_TOKEN"];
3058
+ if (!figmaToken) {
3059
+ throw new Error(
3060
+ "Figma token required. Provide token or set FIGMA_TOKEN environment variable."
3061
+ );
3269
3062
  }
3270
- lines.push("");
3063
+ const client = new FigmaClient({ token: figmaToken });
3064
+ const response = await client.getFile(fileKey);
3065
+ const file = transformFigmaResponse(fileKey, response);
3066
+ return { file, fileKey, nodeId };
3271
3067
  }
3272
- return lines.join("\n");
3068
+ throw new Error(
3069
+ `Invalid input: ${input}. Provide a Figma URL or JSON file path.`
3070
+ );
3273
3071
  }
3274
- function renderApplicationGuide(adjustments) {
3275
- const lines = [];
3276
- lines.push("## Application Guide");
3277
- lines.push("");
3278
- lines.push("To apply these calibration results:");
3279
- lines.push("");
3280
- lines.push("1. Review each adjustment proposal above");
3281
- lines.push("2. Edit `src/core/rules/rule-config.ts` to update scores and severities");
3282
- lines.push("3. Run `pnpm test:run` to verify no tests break");
3283
- lines.push("4. Re-run calibration to confirm improvements");
3284
- lines.push("");
3285
- if (adjustments.length > 0) {
3286
- lines.push("### Suggested Changes to `rule-config.ts`");
3287
- lines.push("");
3288
- lines.push("```typescript");
3289
- for (const adj of adjustments) {
3290
- lines.push(`// ${adj.ruleId}: ${adj.currentScore} -> ${adj.proposedScore} (${adj.confidence} confidence)`);
3291
- if (adj.proposedSeverity) {
3292
- lines.push(`// severity: "${adj.currentSeverity}" -> "${adj.proposedSeverity}"`);
3293
- }
3072
+ function buildRuleScoresMap() {
3073
+ const scores = {};
3074
+ for (const [id, config2] of Object.entries(RULE_CONFIGS)) {
3075
+ scores[id] = { score: config2.score, severity: config2.severity };
3076
+ }
3077
+ return scores;
3078
+ }
3079
+ async function runCalibrationAnalyze(config2) {
3080
+ const parsed = CalibrationConfigSchema.parse(config2);
3081
+ const { file, fileKey, nodeId } = await loadFile2(parsed.input, parsed.token);
3082
+ const analyzeOptions = nodeId ? { targetNodeId: nodeId } : {};
3083
+ const analysisResult = analyzeFile(file, analyzeOptions);
3084
+ const analysisOutput = runAnalysisAgent({ analysisResult });
3085
+ const ruleScores = {
3086
+ ...buildRuleScoresMap(),
3087
+ ...extractRuleScores(analysisResult)
3088
+ };
3089
+ return { analysisOutput, ruleScores, fileKey };
3090
+ }
3091
+ function runCalibrationEvaluate(analysisJson, conversionJson, ruleScores) {
3092
+ const evaluationOutput = runEvaluationAgent({
3093
+ nodeIssueSummaries: analysisJson.nodeIssueSummaries.map((s) => ({
3094
+ nodeId: s.nodeId,
3095
+ nodePath: s.nodePath,
3096
+ flaggedRuleIds: s.flaggedRuleIds
3097
+ })),
3098
+ conversionRecords: conversionJson.records,
3099
+ ruleScores
3100
+ });
3101
+ const tuningOutput = runTuningAgent({
3102
+ mismatches: evaluationOutput.mismatches,
3103
+ ruleScores
3104
+ });
3105
+ const report = generateCalibrationReport({
3106
+ fileKey: analysisJson.fileKey,
3107
+ fileName: analysisJson.fileName,
3108
+ analyzedAt: analysisJson.analyzedAt,
3109
+ nodeCount: analysisJson.nodeCount,
3110
+ issueCount: analysisJson.issueCount,
3111
+ convertedNodeCount: conversionJson.records.length,
3112
+ skippedNodeCount: conversionJson.skippedNodeIds.length,
3113
+ scoreReport: analysisJson.scoreReport,
3114
+ mismatches: evaluationOutput.mismatches,
3115
+ validatedRules: evaluationOutput.validatedRules,
3116
+ adjustments: tuningOutput.adjustments,
3117
+ newRuleProposals: tuningOutput.newRuleProposals
3118
+ });
3119
+ return {
3120
+ evaluationOutput,
3121
+ tuningOutput,
3122
+ report
3123
+ };
3124
+ }
3125
+ async function runCalibration(config2, executor, options) {
3126
+ const parsed = CalibrationConfigSchema.parse(config2);
3127
+ const pipelineStart = Date.now();
3128
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3129
+ const logger = new ActivityLogger(parsed.input) ;
3130
+ try {
3131
+ let stepStart = Date.now();
3132
+ const { file, fileKey, nodeId } = await loadFile2(parsed.input, parsed.token);
3133
+ const analyzeOptions = nodeId ? { targetNodeId: nodeId } : {};
3134
+ const analysisResult = analyzeFile(file, analyzeOptions);
3135
+ const analysisOutput = runAnalysisAgent({ analysisResult });
3136
+ const ruleScores = {
3137
+ ...buildRuleScoresMap(),
3138
+ ...extractRuleScores(analysisResult)
3139
+ };
3140
+ await logger?.logStep({
3141
+ step: "Analysis",
3142
+ result: `${analysisResult.nodeCount} nodes, ${analysisResult.issues.length} issues, grade ${analysisOutput.scoreReport.overall.grade}`,
3143
+ durationMs: Date.now() - stepStart
3144
+ });
3145
+ stepStart = Date.now();
3146
+ const candidates = filterConversionCandidates(
3147
+ analysisOutput.nodeIssueSummaries,
3148
+ analysisResult.file.document
3149
+ );
3150
+ const selectedNodes = selectNodes(
3151
+ candidates,
3152
+ parsed.samplingStrategy,
3153
+ parsed.maxConversionNodes
3154
+ );
3155
+ const conversionOutput = await runConversionAgent(
3156
+ {
3157
+ fileKey,
3158
+ nodes: selectedNodes.map((n) => ({
3159
+ nodeId: n.nodeId,
3160
+ nodePath: n.nodePath,
3161
+ flaggedRuleIds: n.flaggedRuleIds
3162
+ }))
3163
+ },
3164
+ executor
3165
+ );
3166
+ await logger?.logStep({
3167
+ step: "Conversion",
3168
+ result: `${conversionOutput.records.length} converted, ${conversionOutput.skippedNodeIds.length} skipped`,
3169
+ durationMs: Date.now() - stepStart
3170
+ });
3171
+ stepStart = Date.now();
3172
+ const evaluationOutput = runEvaluationAgent({
3173
+ nodeIssueSummaries: selectedNodes.map((n) => ({
3174
+ nodeId: n.nodeId,
3175
+ nodePath: n.nodePath,
3176
+ flaggedRuleIds: n.flaggedRuleIds
3177
+ })),
3178
+ conversionRecords: conversionOutput.records,
3179
+ ruleScores
3180
+ });
3181
+ await logger?.logStep({
3182
+ step: "Evaluation",
3183
+ result: `${evaluationOutput.mismatches.length} mismatches, ${evaluationOutput.validatedRules.length} validated`,
3184
+ durationMs: Date.now() - stepStart
3185
+ });
3186
+ stepStart = Date.now();
3187
+ const tuningOutput = runTuningAgent({
3188
+ mismatches: evaluationOutput.mismatches,
3189
+ ruleScores
3190
+ });
3191
+ await logger?.logStep({
3192
+ step: "Tuning",
3193
+ result: `${tuningOutput.adjustments.length} adjustments, ${tuningOutput.newRuleProposals.length} new rule proposals`,
3194
+ durationMs: Date.now() - stepStart
3195
+ });
3196
+ const report = generateCalibrationReport({
3197
+ fileKey,
3198
+ fileName: file.name,
3199
+ analyzedAt: startedAt,
3200
+ nodeCount: analysisResult.nodeCount,
3201
+ issueCount: analysisResult.issues.length,
3202
+ convertedNodeCount: conversionOutput.records.length,
3203
+ skippedNodeCount: conversionOutput.skippedNodeIds.length,
3204
+ scoreReport: analysisOutput.scoreReport,
3205
+ mismatches: evaluationOutput.mismatches,
3206
+ validatedRules: evaluationOutput.validatedRules,
3207
+ adjustments: tuningOutput.adjustments,
3208
+ newRuleProposals: tuningOutput.newRuleProposals
3209
+ });
3210
+ const reportPath = resolve(parsed.outputPath);
3211
+ const reportDir = resolve(parsed.outputPath, "..");
3212
+ if (!existsSync(reportDir)) {
3213
+ mkdirSync(reportDir, { recursive: true });
3294
3214
  }
3295
- lines.push("```");
3296
- lines.push("");
3215
+ await writeFile(reportPath, report, "utf-8");
3216
+ await logger?.logSummary({
3217
+ totalDurationMs: Date.now() - pipelineStart,
3218
+ nodesAnalyzed: analysisResult.nodeCount,
3219
+ nodesConverted: conversionOutput.records.length,
3220
+ mismatches: evaluationOutput.mismatches.length,
3221
+ adjustments: tuningOutput.adjustments.length,
3222
+ status: "completed"
3223
+ });
3224
+ return {
3225
+ status: "completed",
3226
+ scoreReport: analysisOutput.scoreReport,
3227
+ nodeIssueSummaries: analysisOutput.nodeIssueSummaries,
3228
+ mismatches: evaluationOutput.mismatches,
3229
+ validatedRules: evaluationOutput.validatedRules,
3230
+ adjustments: tuningOutput.adjustments,
3231
+ newRuleProposals: tuningOutput.newRuleProposals,
3232
+ reportPath,
3233
+ logPath: logger?.getLogPath()
3234
+ };
3235
+ } catch (error) {
3236
+ const errorMessage = error instanceof Error ? error.message : String(error);
3237
+ await logger?.logSummary({
3238
+ totalDurationMs: Date.now() - pipelineStart,
3239
+ nodesAnalyzed: 0,
3240
+ nodesConverted: 0,
3241
+ mismatches: 0,
3242
+ adjustments: 0,
3243
+ status: `failed: ${errorMessage}`
3244
+ });
3245
+ return {
3246
+ status: "failed",
3247
+ scoreReport: {
3248
+ overall: { score: 0, maxScore: 100, percentage: 0, grade: "F" },
3249
+ byCategory: {},
3250
+ summary: { totalIssues: 0, blocking: 0, risk: 0, missingInfo: 0, suggestion: 0, nodeCount: 0 }
3251
+ },
3252
+ nodeIssueSummaries: [],
3253
+ mismatches: [],
3254
+ validatedRules: [],
3255
+ adjustments: [],
3256
+ newRuleProposals: [],
3257
+ reportPath: parsed.outputPath,
3258
+ error: errorMessage
3259
+ };
3297
3260
  }
3298
- return lines.join("\n");
3299
- }
3300
- function getTimestamp() {
3301
- const now = /* @__PURE__ */ new Date();
3302
- return now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
3303
3261
  }
3304
- function getDateTimeString() {
3305
- const now = /* @__PURE__ */ new Date();
3306
- const year = now.getFullYear();
3307
- const month = String(now.getMonth() + 1).padStart(2, "0");
3308
- const day = String(now.getDate()).padStart(2, "0");
3309
- const hours = String(now.getHours()).padStart(2, "0");
3310
- const minutes = String(now.getMinutes()).padStart(2, "0");
3311
- return `${year}-${month}-${day}-${hours}-${minutes}`;
3262
+
3263
+ // src/core/engine/scoring.ts
3264
+ var SEVERITY_DENSITY_WEIGHT = {
3265
+ blocking: 3,
3266
+ risk: 2,
3267
+ "missing-info": 1,
3268
+ suggestion: 0.5
3269
+ };
3270
+ var TOTAL_RULES_PER_CATEGORY = {
3271
+ layout: 11,
3272
+ token: 7,
3273
+ component: 6,
3274
+ naming: 5,
3275
+ "ai-readability": 5,
3276
+ "handoff-risk": 5
3277
+ };
3278
+ var CATEGORY_WEIGHT = {
3279
+ layout: 1,
3280
+ token: 1,
3281
+ component: 1,
3282
+ naming: 1,
3283
+ "ai-readability": 1,
3284
+ "handoff-risk": 1
3285
+ };
3286
+ var DENSITY_WEIGHT = 0.7;
3287
+ var DIVERSITY_WEIGHT = 0.3;
3288
+ var SCORE_FLOOR = 5;
3289
+ function calculateGrade(percentage) {
3290
+ if (percentage >= 95) return "S";
3291
+ if (percentage >= 90) return "A+";
3292
+ if (percentage >= 85) return "A";
3293
+ if (percentage >= 80) return "B+";
3294
+ if (percentage >= 75) return "B";
3295
+ if (percentage >= 70) return "C+";
3296
+ if (percentage >= 65) return "C";
3297
+ if (percentage >= 50) return "D";
3298
+ return "F";
3312
3299
  }
3313
- function extractFixtureName(fixturePath) {
3314
- const fileName = fixturePath.split("/").pop() ?? fixturePath;
3315
- return fileName.replace(/\.json$/, "");
3300
+ function clamp(value, min, max) {
3301
+ return Math.max(min, Math.min(max, value));
3316
3302
  }
3317
- var ActivityLogger = class {
3318
- logPath;
3319
- initialized = false;
3320
- constructor(fixturePath, logDir = "logs/activity") {
3321
- const dateTimeStr = getDateTimeString();
3322
- const fixtureName = fixturePath ? extractFixtureName(fixturePath) : "unknown";
3323
- this.logPath = resolve(logDir, `${dateTimeStr}-${fixtureName}.md`);
3303
+ function calculateScores(result) {
3304
+ const categoryScores = initializeCategoryScores();
3305
+ const nodeCount = result.nodeCount;
3306
+ const uniqueRulesPerCategory = /* @__PURE__ */ new Map();
3307
+ for (const category of CATEGORIES) {
3308
+ uniqueRulesPerCategory.set(category, /* @__PURE__ */ new Set());
3324
3309
  }
3325
- /**
3326
- * Ensure the log directory and file header exist
3327
- */
3328
- async ensureInitialized() {
3329
- if (this.initialized) return;
3330
- const dir = dirname(this.logPath);
3331
- if (!existsSync(dir)) {
3332
- mkdirSync(dir, { recursive: true });
3333
- }
3334
- if (!existsSync(this.logPath)) {
3335
- const ts = getDateTimeString();
3336
- await writeFile(this.logPath, `# Calibration Activity Log \u2014 ${ts}
3337
-
3338
- `, "utf-8");
3339
- }
3340
- this.initialized = true;
3310
+ for (const issue of result.issues) {
3311
+ const category = issue.rule.definition.category;
3312
+ const severity = issue.config.severity;
3313
+ const ruleId = issue.rule.definition.id;
3314
+ categoryScores[category].issueCount++;
3315
+ categoryScores[category].bySeverity[severity]++;
3316
+ categoryScores[category].weightedIssueCount += SEVERITY_DENSITY_WEIGHT[severity];
3317
+ uniqueRulesPerCategory.get(category).add(ruleId);
3341
3318
  }
3342
- /**
3343
- * Log a pipeline step
3344
- */
3345
- async logStep(activity) {
3346
- await this.ensureInitialized();
3347
- const lines = [];
3348
- lines.push(`## ${getTimestamp()} \u2014 ${activity.step}`);
3349
- if (activity.nodePath) {
3350
- lines.push(`- Node: ${activity.nodePath}`);
3319
+ for (const category of CATEGORIES) {
3320
+ const catScore = categoryScores[category];
3321
+ const uniqueRules = uniqueRulesPerCategory.get(category);
3322
+ const totalRules = TOTAL_RULES_PER_CATEGORY[category];
3323
+ catScore.uniqueRuleCount = uniqueRules.size;
3324
+ let densityScore = 100;
3325
+ if (nodeCount > 0 && catScore.issueCount > 0) {
3326
+ const density = catScore.weightedIssueCount / nodeCount;
3327
+ densityScore = clamp(Math.round(100 - density * 100), 0, 100);
3351
3328
  }
3352
- lines.push(`- Result: ${activity.result}`);
3353
- lines.push(`- Duration: ${activity.durationMs}ms`);
3354
- lines.push("");
3355
- await appendFile(this.logPath, lines.join("\n") + "\n", "utf-8");
3329
+ catScore.densityScore = densityScore;
3330
+ let diversityScore = 100;
3331
+ if (catScore.issueCount > 0) {
3332
+ const diversityRatio = uniqueRules.size / totalRules;
3333
+ diversityScore = clamp(Math.round((1 - diversityRatio) * 100), 0, 100);
3334
+ }
3335
+ catScore.diversityScore = diversityScore;
3336
+ const combinedScore = densityScore * DENSITY_WEIGHT + diversityScore * DIVERSITY_WEIGHT;
3337
+ catScore.percentage = catScore.issueCount > 0 ? clamp(Math.round(combinedScore), SCORE_FLOOR, 100) : 100;
3338
+ catScore.score = catScore.percentage;
3339
+ catScore.maxScore = 100;
3356
3340
  }
3357
- /**
3358
- * Log a summary at pipeline completion
3359
- */
3360
- async logSummary(summary) {
3361
- await this.ensureInitialized();
3362
- const lines = [];
3363
- lines.push(`## ${getTimestamp()} \u2014 Pipeline Summary`);
3364
- lines.push(`- Status: ${summary.status}`);
3365
- lines.push(`- Total Duration: ${summary.totalDurationMs}ms`);
3366
- lines.push(`- Nodes Analyzed: ${summary.nodesAnalyzed}`);
3367
- lines.push(`- Nodes Converted: ${summary.nodesConverted}`);
3368
- lines.push(`- Mismatches Found: ${summary.mismatches}`);
3369
- lines.push(`- Adjustments Proposed: ${summary.adjustments}`);
3370
- lines.push("");
3371
- lines.push("---");
3372
- lines.push("");
3373
- await appendFile(this.logPath, lines.join("\n") + "\n", "utf-8");
3341
+ let totalWeight = 0;
3342
+ let weightedSum = 0;
3343
+ for (const category of CATEGORIES) {
3344
+ const weight = CATEGORY_WEIGHT[category];
3345
+ weightedSum += categoryScores[category].percentage * weight;
3346
+ totalWeight += weight;
3374
3347
  }
3375
- getLogPath() {
3376
- return this.logPath;
3348
+ const overallPercentage = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 100;
3349
+ const summary = {
3350
+ totalIssues: result.issues.length,
3351
+ blocking: 0,
3352
+ risk: 0,
3353
+ missingInfo: 0,
3354
+ suggestion: 0,
3355
+ nodeCount
3356
+ };
3357
+ for (const issue of result.issues) {
3358
+ switch (issue.config.severity) {
3359
+ case "blocking":
3360
+ summary.blocking++;
3361
+ break;
3362
+ case "risk":
3363
+ summary.risk++;
3364
+ break;
3365
+ case "missing-info":
3366
+ summary.missingInfo++;
3367
+ break;
3368
+ case "suggestion":
3369
+ summary.suggestion++;
3370
+ break;
3371
+ }
3377
3372
  }
3378
- };
3379
-
3380
- // src/agents/orchestrator.ts
3381
- function selectNodes(summaries, strategy, maxNodes) {
3382
- if (summaries.length === 0) return [];
3383
- switch (strategy) {
3384
- case "all":
3385
- return summaries.slice(0, maxNodes);
3386
- case "random": {
3387
- const shuffled = [...summaries];
3388
- for (let i = shuffled.length - 1; i > 0; i--) {
3389
- const j = Math.floor(Math.random() * (i + 1));
3390
- const temp = shuffled[i];
3391
- shuffled[i] = shuffled[j];
3392
- shuffled[j] = temp;
3373
+ return {
3374
+ overall: {
3375
+ score: overallPercentage,
3376
+ maxScore: 100,
3377
+ percentage: overallPercentage,
3378
+ grade: calculateGrade(overallPercentage)
3379
+ },
3380
+ byCategory: categoryScores,
3381
+ summary
3382
+ };
3383
+ }
3384
+ function initializeCategoryScores() {
3385
+ const scores = {};
3386
+ for (const category of CATEGORIES) {
3387
+ scores[category] = {
3388
+ category,
3389
+ score: 100,
3390
+ maxScore: 100,
3391
+ percentage: 100,
3392
+ issueCount: 0,
3393
+ uniqueRuleCount: 0,
3394
+ weightedIssueCount: 0,
3395
+ densityScore: 100,
3396
+ diversityScore: 100,
3397
+ bySeverity: {
3398
+ blocking: 0,
3399
+ risk: 0,
3400
+ "missing-info": 0,
3401
+ suggestion: 0
3393
3402
  }
3394
- return shuffled.slice(0, maxNodes);
3395
- }
3396
- case "top-issues":
3397
- default:
3398
- return summaries.slice(0, maxNodes);
3403
+ };
3399
3404
  }
3405
+ return scores;
3400
3406
  }
3401
- var EXCLUDED_NODE_TYPES = /* @__PURE__ */ new Set([
3402
- "VECTOR",
3403
- "BOOLEAN_OPERATION",
3404
- "STAR",
3405
- "REGULAR_POLYGON",
3406
- "ELLIPSE",
3407
- "LINE"
3408
- ]);
3409
- function findNode(root, nodeId) {
3410
- if (root.id === nodeId) return root;
3411
- if (root.children) {
3412
- for (const child of root.children) {
3413
- const found = findNode(child, nodeId);
3414
- if (found) return found;
3415
- }
3407
+ function formatScoreSummary(report) {
3408
+ const lines = [];
3409
+ lines.push(`Overall: ${report.overall.grade} (${report.overall.percentage}%)`);
3410
+ lines.push("");
3411
+ lines.push("By Category:");
3412
+ for (const category of CATEGORIES) {
3413
+ const cat = report.byCategory[category];
3414
+ lines.push(` ${category}: ${cat.percentage}% (${cat.issueCount} issues, ${cat.uniqueRuleCount} rules)`);
3416
3415
  }
3417
- return null;
3416
+ lines.push("");
3417
+ lines.push("Issues:");
3418
+ lines.push(` Blocking: ${report.summary.blocking}`);
3419
+ lines.push(` Risk: ${report.summary.risk}`);
3420
+ lines.push(` Missing Info: ${report.summary.missingInfo}`);
3421
+ lines.push(` Suggestion: ${report.summary.suggestion}`);
3422
+ lines.push(` Total: ${report.summary.totalIssues}`);
3423
+ return lines.join("\n");
3418
3424
  }
3419
- function hasTextDescendant(node) {
3420
- if (node.type === "TEXT") return true;
3421
- if (node.children) {
3422
- for (const child of node.children) {
3423
- if (hasTextDescendant(child)) return true;
3424
- }
3425
+ function buildResultJson(fileName, result, scores) {
3426
+ const issuesByRule = {};
3427
+ for (const issue of result.issues) {
3428
+ const id = issue.violation.ruleId;
3429
+ issuesByRule[id] = (issuesByRule[id] ?? 0) + 1;
3425
3430
  }
3426
- return false;
3431
+ return {
3432
+ version,
3433
+ fileName,
3434
+ nodeCount: result.nodeCount,
3435
+ maxDepth: result.maxDepth,
3436
+ issueCount: result.issues.length,
3437
+ scores: {
3438
+ overall: scores.overall,
3439
+ categories: scores.byCategory
3440
+ },
3441
+ issuesByRule,
3442
+ summary: formatScoreSummary(scores)
3443
+ };
3427
3444
  }
3428
- var MIN_WIDTH = 200;
3429
- var MIN_HEIGHT = 200;
3430
- var ELIGIBLE_NODE_TYPES = /* @__PURE__ */ new Set([
3431
- "FRAME",
3432
- "COMPONENT",
3433
- "INSTANCE"
3434
- ]);
3435
- function filterConversionCandidates(summaries, documentRoot) {
3436
- return summaries.filter((summary) => {
3437
- const node = findNode(documentRoot, summary.nodeId);
3438
- if (!node) return false;
3439
- if (EXCLUDED_NODE_TYPES.has(node.type)) return false;
3440
- if (!ELIGIBLE_NODE_TYPES.has(node.type)) return false;
3441
- if (isExcludedName(node.name)) return false;
3442
- const bbox = node.absoluteBoundingBox;
3443
- if (bbox && (bbox.width < MIN_WIDTH || bbox.height < MIN_HEIGHT)) return false;
3444
- if (!node.children || node.children.length < 3) return false;
3445
- if (!hasTextDescendant(node)) return false;
3446
- return true;
3447
- });
3445
+ var MatchConditionSchema = z.object({
3446
+ // Node type conditions
3447
+ type: z.array(z.string()).optional(),
3448
+ notType: z.array(z.string()).optional(),
3449
+ // Name conditions (case-insensitive, substring match)
3450
+ nameContains: z.string().optional(),
3451
+ nameNotContains: z.string().optional(),
3452
+ namePattern: z.string().optional(),
3453
+ // Size conditions
3454
+ minWidth: z.number().optional(),
3455
+ maxWidth: z.number().optional(),
3456
+ minHeight: z.number().optional(),
3457
+ maxHeight: z.number().optional(),
3458
+ // Layout conditions
3459
+ hasAutoLayout: z.boolean().optional(),
3460
+ hasChildren: z.boolean().optional(),
3461
+ minChildren: z.number().optional(),
3462
+ maxChildren: z.number().optional(),
3463
+ // Component conditions
3464
+ isComponent: z.boolean().optional(),
3465
+ isInstance: z.boolean().optional(),
3466
+ hasComponentId: z.boolean().optional(),
3467
+ // Visibility
3468
+ isVisible: z.boolean().optional(),
3469
+ // Fill/style conditions
3470
+ hasFills: z.boolean().optional(),
3471
+ hasStrokes: z.boolean().optional(),
3472
+ hasEffects: z.boolean().optional(),
3473
+ // Depth condition
3474
+ minDepth: z.number().optional(),
3475
+ maxDepth: z.number().optional()
3476
+ });
3477
+ var CustomRuleSchema = z.object({
3478
+ id: z.string(),
3479
+ category: CategorySchema,
3480
+ severity: SeveritySchema,
3481
+ score: z.number().int().max(0),
3482
+ match: MatchConditionSchema,
3483
+ message: z.string().optional(),
3484
+ why: z.string(),
3485
+ impact: z.string(),
3486
+ fix: z.string(),
3487
+ // Backward compat: silently ignore the old prompt field
3488
+ prompt: z.string().optional()
3489
+ });
3490
+ var CustomRulesFileSchema = z.array(CustomRuleSchema);
3491
+
3492
+ // src/core/rules/custom/custom-rule-loader.ts
3493
+ async function loadCustomRules(filePath) {
3494
+ const absPath = resolve(filePath);
3495
+ const raw = await readFile(absPath, "utf-8");
3496
+ const parsed = JSON.parse(raw);
3497
+ const customRules = CustomRulesFileSchema.parse(parsed);
3498
+ const rules = [];
3499
+ const configs = {};
3500
+ for (const cr of customRules) {
3501
+ if (!cr.match) continue;
3502
+ rules.push(toRule(cr));
3503
+ configs[cr.id] = {
3504
+ severity: cr.severity,
3505
+ score: cr.score,
3506
+ enabled: true
3507
+ };
3508
+ }
3509
+ return { rules, configs };
3448
3510
  }
3449
- function isFigmaUrl2(input) {
3450
- return input.includes("figma.com/");
3511
+ function toRule(cr) {
3512
+ return {
3513
+ definition: {
3514
+ id: cr.id,
3515
+ name: cr.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
3516
+ category: cr.category,
3517
+ why: cr.why,
3518
+ impact: cr.impact,
3519
+ fix: cr.fix
3520
+ },
3521
+ check: createPatternCheck(cr)
3522
+ };
3451
3523
  }
3452
- function isJsonFile2(input) {
3453
- return input.endsWith(".json");
3524
+ function createPatternCheck(cr) {
3525
+ return (node, context) => {
3526
+ if (node.type === "DOCUMENT" || node.type === "CANVAS") return null;
3527
+ const match = cr.match;
3528
+ if (match.type && !match.type.includes(node.type)) return null;
3529
+ if (match.notType && match.notType.includes(node.type)) return null;
3530
+ if (match.nameContains !== void 0 && !node.name.toLowerCase().includes(match.nameContains.toLowerCase())) return null;
3531
+ if (match.nameNotContains !== void 0 && node.name.toLowerCase().includes(match.nameNotContains.toLowerCase())) return null;
3532
+ if (match.namePattern !== void 0 && !new RegExp(match.namePattern, "i").test(node.name)) return null;
3533
+ const bbox = node.absoluteBoundingBox;
3534
+ if (match.minWidth !== void 0 && (!bbox || bbox.width < match.minWidth)) return null;
3535
+ if (match.maxWidth !== void 0 && (!bbox || bbox.width > match.maxWidth)) return null;
3536
+ if (match.minHeight !== void 0 && (!bbox || bbox.height < match.minHeight)) return null;
3537
+ if (match.maxHeight !== void 0 && (!bbox || bbox.height > match.maxHeight)) return null;
3538
+ if (match.hasAutoLayout === true && !node.layoutMode) return null;
3539
+ if (match.hasAutoLayout === false && node.layoutMode) return null;
3540
+ if (match.hasChildren === true && (!node.children || node.children.length === 0)) return null;
3541
+ if (match.hasChildren === false && node.children && node.children.length > 0) return null;
3542
+ if (match.minChildren !== void 0 && (!node.children || node.children.length < match.minChildren)) return null;
3543
+ if (match.maxChildren !== void 0 && node.children && node.children.length > match.maxChildren) return null;
3544
+ if (match.isComponent === true && node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
3545
+ if (match.isComponent === false && (node.type === "COMPONENT" || node.type === "COMPONENT_SET")) return null;
3546
+ if (match.isInstance === true && node.type !== "INSTANCE") return null;
3547
+ if (match.isInstance === false && node.type === "INSTANCE") return null;
3548
+ if (match.hasComponentId === true && !node.componentId) return null;
3549
+ if (match.hasComponentId === false && node.componentId) return null;
3550
+ if (match.isVisible === true && !node.visible) return null;
3551
+ if (match.isVisible === false && node.visible) return null;
3552
+ if (match.hasFills === true && (!node.fills || node.fills.length === 0)) return null;
3553
+ if (match.hasFills === false && node.fills && node.fills.length > 0) return null;
3554
+ if (match.hasStrokes === true && (!node.strokes || node.strokes.length === 0)) return null;
3555
+ if (match.hasStrokes === false && node.strokes && node.strokes.length > 0) return null;
3556
+ if (match.hasEffects === true && (!node.effects || node.effects.length === 0)) return null;
3557
+ if (match.hasEffects === false && node.effects && node.effects.length > 0) return null;
3558
+ if (match.minDepth !== void 0 && context.depth < match.minDepth) return null;
3559
+ if (match.maxDepth !== void 0 && context.depth > match.maxDepth) return null;
3560
+ const msg = (cr.message ?? `"${node.name}" matched custom rule "${cr.id}"`).replace(/\{name\}/g, node.name).replace(/\{type\}/g, node.type);
3561
+ return {
3562
+ ruleId: cr.id,
3563
+ nodeId: node.id,
3564
+ nodePath: context.path.join(" > "),
3565
+ message: msg
3566
+ };
3567
+ };
3454
3568
  }
3455
- async function loadFile2(input, token) {
3456
- if (isJsonFile2(input)) {
3457
- const filePath = resolve(input);
3458
- if (!existsSync(filePath)) {
3459
- throw new Error(`File not found: ${filePath}`);
3569
+ var RuleOverrideSchema = z.object({
3570
+ score: z.number().int().max(0).optional(),
3571
+ severity: SeveritySchema.optional(),
3572
+ enabled: z.boolean().optional()
3573
+ });
3574
+ var ConfigFileSchema = z.object({
3575
+ excludeNodeTypes: z.array(z.string()).optional(),
3576
+ excludeNodeNames: z.array(z.string()).optional(),
3577
+ gridBase: z.number().int().positive().optional(),
3578
+ colorTolerance: z.number().int().positive().optional(),
3579
+ rules: z.record(z.string(), RuleOverrideSchema).optional()
3580
+ });
3581
+ async function loadConfigFile(filePath) {
3582
+ const absPath = resolve(filePath);
3583
+ const raw = await readFile(absPath, "utf-8");
3584
+ const parsed = JSON.parse(raw);
3585
+ return ConfigFileSchema.parse(parsed);
3586
+ }
3587
+ function mergeConfigs(base, overrides) {
3588
+ const merged = { ...base };
3589
+ if (overrides.gridBase !== void 0) {
3590
+ for (const [id, config2] of Object.entries(merged)) {
3591
+ if (config2.options && "gridBase" in config2.options) {
3592
+ merged[id] = {
3593
+ ...config2,
3594
+ options: { ...config2.options, gridBase: overrides.gridBase }
3595
+ };
3596
+ }
3460
3597
  }
3461
- const file = await loadFigmaFileFromJson(filePath);
3462
- return { file, fileKey: file.fileKey, nodeId: void 0 };
3463
3598
  }
3464
- if (isFigmaUrl2(input)) {
3465
- const { fileKey, nodeId } = parseFigmaUrl(input);
3466
- const figmaToken = token ?? process.env["FIGMA_TOKEN"];
3467
- if (!figmaToken) {
3468
- throw new Error(
3469
- "Figma token required. Provide token or set FIGMA_TOKEN environment variable."
3470
- );
3599
+ if (overrides.colorTolerance !== void 0) {
3600
+ for (const [id, config2] of Object.entries(merged)) {
3601
+ if (config2.options && "tolerance" in config2.options) {
3602
+ merged[id] = {
3603
+ ...config2,
3604
+ options: { ...config2.options, tolerance: overrides.colorTolerance }
3605
+ };
3606
+ }
3607
+ }
3608
+ }
3609
+ if (overrides.rules) {
3610
+ for (const [ruleId, override] of Object.entries(overrides.rules)) {
3611
+ const existing = merged[ruleId];
3612
+ if (existing) {
3613
+ merged[ruleId] = {
3614
+ ...existing,
3615
+ ...override.score !== void 0 && { score: override.score },
3616
+ ...override.severity !== void 0 && { severity: override.severity },
3617
+ ...override.enabled !== void 0 && { enabled: override.enabled }
3618
+ };
3619
+ }
3471
3620
  }
3472
- const client = new FigmaClient({ token: figmaToken });
3473
- const response = await client.getFile(fileKey);
3474
- const file = transformFigmaResponse(fileKey, response);
3475
- return { file, fileKey, nodeId };
3476
3621
  }
3477
- throw new Error(
3478
- `Invalid input: ${input}. Provide a Figma URL or JSON file path.`
3479
- );
3622
+ return merged;
3480
3623
  }
3481
- function buildRuleScoresMap() {
3482
- const scores = {};
3483
- for (const [id, config2] of Object.entries(RULE_CONFIGS)) {
3484
- scores[id] = { score: config2.score, severity: config2.severity };
3485
- }
3486
- return scores;
3624
+
3625
+ // src/core/ui-constants.ts
3626
+ var GAUGE_R = 54;
3627
+ var GAUGE_C = Math.round(2 * Math.PI * GAUGE_R);
3628
+ var CATEGORY_DESCRIPTIONS = {
3629
+ layout: "Auto Layout, responsive constraints, nesting depth, absolute positioning",
3630
+ token: "Design token binding for colors, fonts, shadows, spacing grid",
3631
+ component: "Component reuse, detached instances, variant coverage",
3632
+ naming: "Semantic layer names, naming conventions, default names",
3633
+ "ai-readability": "Structure clarity for AI code generation, z-index, empty frames",
3634
+ "handoff-risk": "Hardcoded values, text truncation, image placeholders, dev status"
3635
+ };
3636
+ var SEVERITY_ORDER = [
3637
+ "blocking",
3638
+ "risk",
3639
+ "missing-info",
3640
+ "suggestion"
3641
+ ];
3642
+
3643
+ // src/core/ui-helpers.ts
3644
+ function gaugeColor(pct) {
3645
+ if (pct >= 75) return "#22c55e";
3646
+ if (pct >= 50) return "#f59e0b";
3647
+ return "#ef4444";
3487
3648
  }
3488
- async function runCalibrationAnalyze(config2) {
3489
- const parsed = CalibrationConfigSchema.parse(config2);
3490
- const { file, fileKey, nodeId } = await loadFile2(parsed.input, parsed.token);
3491
- const analyzeOptions = nodeId ? { targetNodeId: nodeId } : {};
3492
- const analysisResult = analyzeFile(file, analyzeOptions);
3493
- const analysisOutput = runAnalysisAgent({ analysisResult });
3494
- const ruleScores = {
3495
- ...buildRuleScoresMap(),
3496
- ...extractRuleScores(analysisResult)
3649
+ function escapeHtml(text) {
3650
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3651
+ }
3652
+ function severityDot(sev) {
3653
+ const map = {
3654
+ blocking: "bg-red-500",
3655
+ risk: "bg-amber-500",
3656
+ "missing-info": "bg-zinc-400",
3657
+ suggestion: "bg-green-500"
3497
3658
  };
3498
- return { analysisOutput, ruleScores, fileKey };
3659
+ return map[sev];
3499
3660
  }
3500
- function runCalibrationEvaluate(analysisJson, conversionJson, ruleScores) {
3501
- const evaluationOutput = runEvaluationAgent({
3502
- nodeIssueSummaries: analysisJson.nodeIssueSummaries.map((s) => ({
3503
- nodeId: s.nodeId,
3504
- nodePath: s.nodePath,
3505
- flaggedRuleIds: s.flaggedRuleIds
3506
- })),
3507
- conversionRecords: conversionJson.records,
3508
- ruleScores
3509
- });
3510
- const tuningOutput = runTuningAgent({
3511
- mismatches: evaluationOutput.mismatches,
3512
- ruleScores
3513
- });
3514
- const report = generateCalibrationReport({
3515
- fileKey: analysisJson.fileKey,
3516
- fileName: analysisJson.fileName,
3517
- analyzedAt: analysisJson.analyzedAt,
3518
- nodeCount: analysisJson.nodeCount,
3519
- issueCount: analysisJson.issueCount,
3520
- convertedNodeCount: conversionJson.records.length,
3521
- skippedNodeCount: conversionJson.skippedNodeIds.length,
3522
- scoreReport: analysisJson.scoreReport,
3523
- mismatches: evaluationOutput.mismatches,
3524
- validatedRules: evaluationOutput.validatedRules,
3525
- adjustments: tuningOutput.adjustments,
3526
- newRuleProposals: tuningOutput.newRuleProposals
3527
- });
3528
- return {
3529
- evaluationOutput,
3530
- tuningOutput,
3531
- report
3661
+ function severityBadge(sev) {
3662
+ const map = {
3663
+ blocking: "bg-red-500/10 text-red-600 border-red-500/20",
3664
+ risk: "bg-amber-500/10 text-amber-600 border-amber-500/20",
3665
+ "missing-info": "bg-zinc-500/10 text-zinc-600 border-zinc-500/20",
3666
+ suggestion: "bg-green-500/10 text-green-600 border-green-500/20"
3532
3667
  };
3668
+ return map[sev];
3533
3669
  }
3534
- async function runCalibration(config2, executor, options) {
3535
- const parsed = CalibrationConfigSchema.parse(config2);
3536
- const pipelineStart = Date.now();
3537
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3538
- const logger = new ActivityLogger(parsed.input) ;
3539
- try {
3540
- let stepStart = Date.now();
3541
- const { file, fileKey, nodeId } = await loadFile2(parsed.input, parsed.token);
3542
- const analyzeOptions = nodeId ? { targetNodeId: nodeId } : {};
3543
- const analysisResult = analyzeFile(file, analyzeOptions);
3544
- const analysisOutput = runAnalysisAgent({ analysisResult });
3545
- const ruleScores = {
3546
- ...buildRuleScoresMap(),
3547
- ...extractRuleScores(analysisResult)
3548
- };
3549
- await logger?.logStep({
3550
- step: "Analysis",
3551
- result: `${analysisResult.nodeCount} nodes, ${analysisResult.issues.length} issues, grade ${analysisOutput.scoreReport.overall.grade}`,
3552
- durationMs: Date.now() - stepStart
3553
- });
3554
- stepStart = Date.now();
3555
- const candidates = filterConversionCandidates(
3556
- analysisOutput.nodeIssueSummaries,
3557
- analysisResult.file.document
3558
- );
3559
- const selectedNodes = selectNodes(
3560
- candidates,
3561
- parsed.samplingStrategy,
3562
- parsed.maxConversionNodes
3563
- );
3564
- const conversionOutput = await runConversionAgent(
3565
- {
3566
- fileKey,
3567
- nodes: selectedNodes.map((n) => ({
3568
- nodeId: n.nodeId,
3569
- nodePath: n.nodePath,
3570
- flaggedRuleIds: n.flaggedRuleIds
3571
- }))
3572
- },
3573
- executor
3574
- );
3575
- await logger?.logStep({
3576
- step: "Conversion",
3577
- result: `${conversionOutput.records.length} converted, ${conversionOutput.skippedNodeIds.length} skipped`,
3578
- durationMs: Date.now() - stepStart
3579
- });
3580
- stepStart = Date.now();
3581
- const evaluationOutput = runEvaluationAgent({
3582
- nodeIssueSummaries: selectedNodes.map((n) => ({
3583
- nodeId: n.nodeId,
3584
- nodePath: n.nodePath,
3585
- flaggedRuleIds: n.flaggedRuleIds
3586
- })),
3587
- conversionRecords: conversionOutput.records,
3588
- ruleScores
3589
- });
3590
- await logger?.logStep({
3591
- step: "Evaluation",
3592
- result: `${evaluationOutput.mismatches.length} mismatches, ${evaluationOutput.validatedRules.length} validated`,
3593
- durationMs: Date.now() - stepStart
3594
- });
3595
- stepStart = Date.now();
3596
- const tuningOutput = runTuningAgent({
3597
- mismatches: evaluationOutput.mismatches,
3598
- ruleScores
3599
- });
3600
- await logger?.logStep({
3601
- step: "Tuning",
3602
- result: `${tuningOutput.adjustments.length} adjustments, ${tuningOutput.newRuleProposals.length} new rule proposals`,
3603
- durationMs: Date.now() - stepStart
3604
- });
3605
- const report = generateCalibrationReport({
3606
- fileKey,
3607
- fileName: file.name,
3608
- analyzedAt: startedAt,
3609
- nodeCount: analysisResult.nodeCount,
3610
- issueCount: analysisResult.issues.length,
3611
- convertedNodeCount: conversionOutput.records.length,
3612
- skippedNodeCount: conversionOutput.skippedNodeIds.length,
3613
- scoreReport: analysisOutput.scoreReport,
3614
- mismatches: evaluationOutput.mismatches,
3615
- validatedRules: evaluationOutput.validatedRules,
3616
- adjustments: tuningOutput.adjustments,
3617
- newRuleProposals: tuningOutput.newRuleProposals
3618
- });
3619
- const reportPath = resolve(parsed.outputPath);
3620
- const reportDir = resolve(parsed.outputPath, "..");
3621
- if (!existsSync(reportDir)) {
3622
- mkdirSync(reportDir, { recursive: true });
3623
- }
3624
- await writeFile(reportPath, report, "utf-8");
3625
- await logger?.logSummary({
3626
- totalDurationMs: Date.now() - pipelineStart,
3627
- nodesAnalyzed: analysisResult.nodeCount,
3628
- nodesConverted: conversionOutput.records.length,
3629
- mismatches: evaluationOutput.mismatches.length,
3630
- adjustments: tuningOutput.adjustments.length,
3631
- status: "completed"
3632
- });
3633
- return {
3634
- status: "completed",
3635
- scoreReport: analysisOutput.scoreReport,
3636
- nodeIssueSummaries: analysisOutput.nodeIssueSummaries,
3637
- mismatches: evaluationOutput.mismatches,
3638
- validatedRules: evaluationOutput.validatedRules,
3639
- adjustments: tuningOutput.adjustments,
3640
- newRuleProposals: tuningOutput.newRuleProposals,
3641
- reportPath,
3642
- logPath: logger?.getLogPath()
3643
- };
3644
- } catch (error) {
3645
- const errorMessage = error instanceof Error ? error.message : String(error);
3646
- await logger?.logSummary({
3647
- totalDurationMs: Date.now() - pipelineStart,
3648
- nodesAnalyzed: 0,
3649
- nodesConverted: 0,
3650
- mismatches: 0,
3651
- adjustments: 0,
3652
- status: `failed: ${errorMessage}`
3653
- });
3654
- return {
3655
- status: "failed",
3656
- scoreReport: {
3657
- overall: { score: 0, maxScore: 100, percentage: 0, grade: "F" },
3658
- byCategory: {},
3659
- summary: { totalIssues: 0, blocking: 0, risk: 0, missingInfo: 0, suggestion: 0, nodeCount: 0 }
3660
- },
3661
- nodeIssueSummaries: [],
3662
- mismatches: [],
3663
- validatedRules: [],
3664
- adjustments: [],
3665
- newRuleProposals: [],
3666
- reportPath: parsed.outputPath,
3667
- error: errorMessage
3668
- };
3670
+ function scoreBadgeStyle(pct) {
3671
+ if (pct >= 75) return "bg-green-500/10 text-green-700 border-green-500/20";
3672
+ if (pct >= 50) return "bg-amber-500/10 text-amber-700 border-amber-500/20";
3673
+ return "bg-red-500/10 text-red-700 border-red-500/20";
3674
+ }
3675
+ function renderGaugeSvg(pct, size, strokeW, grade) {
3676
+ const offset = GAUGE_C * (1 - pct / 100);
3677
+ const color = gaugeColor(pct);
3678
+ if (grade) {
3679
+ return `<svg width="${size}" height="${size}" viewBox="0 0 120 120" class="gauge-svg block">
3680
+ <circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke-width="${strokeW}" stroke="#e4e4e7" class="stroke-border" />
3681
+ <circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-dasharray="${GAUGE_C}" stroke-dashoffset="${offset}" transform="rotate(-90 60 60)" class="gauge-fill" />
3682
+ <text x="60" y="60" text-anchor="middle" dominant-baseline="central" fill="currentColor" font-size="48" font-weight="700" font-family="Inter,-apple-system,sans-serif" class="font-sans">${escapeHtml(grade)}</text>
3683
+ </svg>`;
3669
3684
  }
3685
+ return `<svg width="${size}" height="${size}" viewBox="0 0 120 120" class="gauge-svg block">
3686
+ <circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke-width="${strokeW}" stroke="#e4e4e7" class="stroke-border" />
3687
+ <circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-dasharray="${GAUGE_C}" stroke-dashoffset="${offset}" transform="rotate(-90 60 60)" class="gauge-fill" />
3688
+ <text x="60" y="62" text-anchor="middle" dominant-baseline="central" fill="currentColor" font-size="28" font-weight="700" font-family="Inter,-apple-system,sans-serif" class="font-sans">${pct}</text>
3689
+ </svg>`;
3670
3690
  }
3671
3691
 
3672
3692
  // src/core/report-html/index.ts
@@ -4343,7 +4363,7 @@ function countNodes2(node) {
4343
4363
  }
4344
4364
  return count;
4345
4365
  }
4346
- cli.command("analyze <input>", "Analyze a Figma file or JSON fixture").option("--preset <preset>", "Analysis preset (relaxed | dev-friendly | ai-ready | strict)").option("--output <path>", "HTML report output path").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").option("--api", "Load via Figma REST API (requires FIGMA_TOKEN)").option("--screenshot", "Include screenshot comparison in report (requires ANTHROPIC_API_KEY)").option("--custom-rules <path>", "Path to custom rules JSON file").option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--no-open", "Don't open report in browser after analysis").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign --api --token YOUR_TOKEN").example(" canicode analyze ./fixtures/design.json --output report.html").example(" canicode analyze ./fixtures/design.json --custom-rules ./my-rules.json").example(" canicode analyze ./fixtures/design.json --config ./my-config.json").action(async (input, options) => {
4366
+ cli.command("analyze <input>", "Analyze a Figma file or JSON fixture").option("--preset <preset>", "Analysis preset (relaxed | dev-friendly | ai-ready | strict)").option("--output <path>", "HTML report output path").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").option("--api", "Load via Figma REST API (requires FIGMA_TOKEN)").option("--screenshot", "Include screenshot comparison in report (requires ANTHROPIC_API_KEY)").option("--custom-rules <path>", "Path to custom rules JSON file").option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--no-open", "Don't open report in browser after analysis").option("--json", "Output JSON results to stdout (same format as MCP)").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign --api --token YOUR_TOKEN").example(" canicode analyze ./fixtures/design.json --output report.html").example(" canicode analyze ./fixtures/design.json --custom-rules ./my-rules.json").example(" canicode analyze ./fixtures/design.json --config ./my-config.json").action(async (input, options) => {
4347
4367
  const analysisStart = Date.now();
4348
4368
  trackEvent(EVENTS.ANALYSIS_STARTED, { source: isJsonFile(input) ? "fixture" : "figma" });
4349
4369
  try {
@@ -4419,6 +4439,10 @@ Analyzing: ${file.name}`);
4419
4439
  const result = analyzeFile(file, analyzeOptions);
4420
4440
  console.log(`Nodes: ${result.nodeCount} (max depth: ${result.maxDepth})`);
4421
4441
  const scores = calculateScores(result);
4442
+ if (options.json) {
4443
+ console.log(JSON.stringify(buildResultJson(file.name, result, scores), null, 2));
4444
+ return;
4445
+ }
4422
4446
  console.log("\n" + "=".repeat(50));
4423
4447
  console.log(formatScoreSummary(scores));
4424
4448
  console.log("=".repeat(50));