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 +824 -800
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +266 -254
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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.
|
|
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
|
|
3246
|
-
if (
|
|
3247
|
-
|
|
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
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
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
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
`Invalid input: ${input}. Provide a Figma URL or JSON file path.`
|
|
3070
|
+
);
|
|
3273
3071
|
}
|
|
3274
|
-
function
|
|
3275
|
-
const
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
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
|
-
|
|
3296
|
-
|
|
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
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
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
|
|
3314
|
-
|
|
3315
|
-
return fileName.replace(/\.json$/, "");
|
|
3300
|
+
function clamp(value, min, max) {
|
|
3301
|
+
return Math.max(min, Math.min(max, value));
|
|
3316
3302
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
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
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
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
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
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
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
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
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
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
|
-
|
|
3376
|
-
|
|
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
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
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
|
-
|
|
3395
|
-
}
|
|
3396
|
-
case "top-issues":
|
|
3397
|
-
default:
|
|
3398
|
-
return summaries.slice(0, maxNodes);
|
|
3403
|
+
};
|
|
3399
3404
|
}
|
|
3405
|
+
return scores;
|
|
3400
3406
|
}
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
"
|
|
3405
|
-
"
|
|
3406
|
-
|
|
3407
|
-
|
|
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
|
-
|
|
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
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
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
|
|
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
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
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
|
|
3450
|
-
return
|
|
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
|
|
3453
|
-
return
|
|
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
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
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 (
|
|
3465
|
-
const
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
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
|
-
|
|
3478
|
-
`Invalid input: ${input}. Provide a Figma URL or JSON file path.`
|
|
3479
|
-
);
|
|
3622
|
+
return merged;
|
|
3480
3623
|
}
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
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
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
const
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3649
|
+
function escapeHtml(text) {
|
|
3650
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
3659
|
+
return map[sev];
|
|
3499
3660
|
}
|
|
3500
|
-
function
|
|
3501
|
-
const
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
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
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
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));
|