canicode 0.10.2 → 0.10.4
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/README.md +36 -31
- package/dist/cli/index.js +401 -22
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +261 -3
- package/dist/index.js +102 -10
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +200 -24
- package/dist/mcp/server.js.map +1 -1
- package/package.json +3 -2
- package/skills/canicode-gotchas/SKILL.md +66 -28
- package/skills/canicode-roundtrip/SKILL.md +290 -86
- package/skills/canicode-roundtrip/helpers.js +270 -10
package/dist/mcp/server.js
CHANGED
|
@@ -615,6 +615,14 @@ function defineRule(rule) {
|
|
|
615
615
|
ruleRegistry.register(rule);
|
|
616
616
|
return rule;
|
|
617
617
|
}
|
|
618
|
+
var AcknowledgmentSchema = z.object({
|
|
619
|
+
nodeId: z.string(),
|
|
620
|
+
ruleId: z.string()
|
|
621
|
+
});
|
|
622
|
+
z.array(AcknowledgmentSchema);
|
|
623
|
+
function normalizeNodeId(id) {
|
|
624
|
+
return id.replace(/-/g, ":");
|
|
625
|
+
}
|
|
618
626
|
|
|
619
627
|
// src/core/engine/rule-engine.ts
|
|
620
628
|
function calculateMaxDepth(node, currentDepth = 0) {
|
|
@@ -665,6 +673,7 @@ var RuleEngine = class {
|
|
|
665
673
|
targetNodeId;
|
|
666
674
|
excludeNamePattern;
|
|
667
675
|
excludeNodeTypes;
|
|
676
|
+
acknowledgments;
|
|
668
677
|
constructor(options = {}) {
|
|
669
678
|
this.configs = options.configs ?? RULE_CONFIGS;
|
|
670
679
|
this.enabledRuleIds = options.enabledRules ? new Set(options.enabledRules) : null;
|
|
@@ -672,6 +681,11 @@ var RuleEngine = class {
|
|
|
672
681
|
this.targetNodeId = options.targetNodeId;
|
|
673
682
|
this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
|
|
674
683
|
this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
|
|
684
|
+
this.acknowledgments = new Set(
|
|
685
|
+
(options.acknowledgments ?? []).map(
|
|
686
|
+
(a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`
|
|
687
|
+
)
|
|
688
|
+
);
|
|
675
689
|
}
|
|
676
690
|
/**
|
|
677
691
|
* Analyze a Figma file and return issues
|
|
@@ -706,6 +720,14 @@ var RuleEngine = class {
|
|
|
706
720
|
void 0,
|
|
707
721
|
void 0
|
|
708
722
|
);
|
|
723
|
+
if (this.acknowledgments.size > 0) {
|
|
724
|
+
for (const issue of issues) {
|
|
725
|
+
const key = `${normalizeNodeId(issue.violation.nodeId)}::${issue.violation.ruleId}`;
|
|
726
|
+
if (this.acknowledgments.has(key)) {
|
|
727
|
+
issue.acknowledged = true;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
709
731
|
return {
|
|
710
732
|
file,
|
|
711
733
|
issues,
|
|
@@ -1708,8 +1730,6 @@ function resolveTargetProperty(ruleId, subType) {
|
|
|
1708
1730
|
if (subType === "horizontal") return "layoutSizingHorizontal";
|
|
1709
1731
|
return ["layoutSizingHorizontal", "layoutSizingVertical"];
|
|
1710
1732
|
case "missing-size-constraint":
|
|
1711
|
-
if (subType === "wrap") return "minWidth";
|
|
1712
|
-
if (subType === "max-width") return "maxWidth";
|
|
1713
1733
|
return ["minWidth", "maxWidth"];
|
|
1714
1734
|
case "irregular-spacing":
|
|
1715
1735
|
if (subType === "gap") return "itemSpacing";
|
|
@@ -1753,7 +1773,7 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
1753
1773
|
}
|
|
1754
1774
|
|
|
1755
1775
|
// package.json
|
|
1756
|
-
var version = "0.10.
|
|
1776
|
+
var version = "0.10.4";
|
|
1757
1777
|
|
|
1758
1778
|
// src/core/engine/scoring.ts
|
|
1759
1779
|
function computeTotalScorePerCategory(configs) {
|
|
@@ -1809,7 +1829,8 @@ function calculateScores(result, configs) {
|
|
|
1809
1829
|
uniqueRulesPerCategory.get(category).add(ruleId);
|
|
1810
1830
|
ruleScorePerCategory.get(category).set(ruleId, Math.abs(issue.config.score));
|
|
1811
1831
|
const ruleCountMap = ruleIssueCountPerCategory.get(category);
|
|
1812
|
-
|
|
1832
|
+
const weight = issue.acknowledged === true ? 0.5 : 1;
|
|
1833
|
+
ruleCountMap.set(ruleId, (ruleCountMap.get(ruleId) ?? 0) + weight);
|
|
1813
1834
|
}
|
|
1814
1835
|
for (const category of CATEGORIES) {
|
|
1815
1836
|
const ruleCountMap = ruleIssueCountPerCategory.get(category);
|
|
@@ -1856,7 +1877,8 @@ function calculateScores(result, configs) {
|
|
|
1856
1877
|
risk: 0,
|
|
1857
1878
|
missingInfo: 0,
|
|
1858
1879
|
suggestion: 0,
|
|
1859
|
-
nodeCount
|
|
1880
|
+
nodeCount,
|
|
1881
|
+
acknowledgedCount: 0
|
|
1860
1882
|
};
|
|
1861
1883
|
for (const issue of result.issues) {
|
|
1862
1884
|
switch (issue.config.severity) {
|
|
@@ -1873,6 +1895,7 @@ function calculateScores(result, configs) {
|
|
|
1873
1895
|
summary.suggestion++;
|
|
1874
1896
|
break;
|
|
1875
1897
|
}
|
|
1898
|
+
if (issue.acknowledged === true) summary.acknowledgedCount++;
|
|
1876
1899
|
}
|
|
1877
1900
|
return {
|
|
1878
1901
|
overall: {
|
|
@@ -1923,7 +1946,14 @@ function formatScoreSummary(report) {
|
|
|
1923
1946
|
lines.push(` Risk: ${report.summary.risk}`);
|
|
1924
1947
|
lines.push(` Missing Info: ${report.summary.missingInfo}`);
|
|
1925
1948
|
lines.push(` Suggestion: ${report.summary.suggestion}`);
|
|
1926
|
-
|
|
1949
|
+
if (report.summary.acknowledgedCount > 0) {
|
|
1950
|
+
const unaddressed = report.summary.totalIssues - report.summary.acknowledgedCount;
|
|
1951
|
+
lines.push(
|
|
1952
|
+
` Total: ${report.summary.totalIssues} (${report.summary.acknowledgedCount} acknowledged via canicode annotations / ${unaddressed} unaddressed)`
|
|
1953
|
+
);
|
|
1954
|
+
} else {
|
|
1955
|
+
lines.push(` Total: ${report.summary.totalIssues}`);
|
|
1956
|
+
}
|
|
1927
1957
|
return lines.join("\n");
|
|
1928
1958
|
}
|
|
1929
1959
|
function buildResultJson(fileName, result, scores, options) {
|
|
@@ -1947,17 +1977,20 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
1947
1977
|
...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
|
|
1948
1978
|
...suggestedName !== void 0 ? { suggestedName } : {},
|
|
1949
1979
|
isInstanceChild: applyContext.isInstanceChild,
|
|
1950
|
-
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
|
|
1980
|
+
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {},
|
|
1981
|
+
...issue.acknowledged === true ? { acknowledged: true } : {}
|
|
1951
1982
|
};
|
|
1952
1983
|
});
|
|
1953
1984
|
const json = {
|
|
1954
1985
|
version,
|
|
1955
1986
|
analyzedAt: result.analyzedAt,
|
|
1956
1987
|
...options?.fileKey && { fileKey: options.fileKey },
|
|
1988
|
+
...options?.designKey && { designKey: options.designKey },
|
|
1957
1989
|
fileName,
|
|
1958
1990
|
nodeCount: result.nodeCount,
|
|
1959
1991
|
maxDepth: result.maxDepth,
|
|
1960
1992
|
issueCount: result.issues.length,
|
|
1993
|
+
acknowledgedCount: scores.summary.acknowledgedCount,
|
|
1961
1994
|
isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade),
|
|
1962
1995
|
blockingIssueCount: scores.summary.blocking,
|
|
1963
1996
|
scores: {
|
|
@@ -2084,26 +2117,97 @@ var GOTCHA_QUESTIONS = {
|
|
|
2084
2117
|
}
|
|
2085
2118
|
};
|
|
2086
2119
|
|
|
2120
|
+
// src/core/gotcha/group-and-batch-questions.ts
|
|
2121
|
+
var BATCHABLE_RULE_IDS = [
|
|
2122
|
+
"missing-size-constraint",
|
|
2123
|
+
"irregular-spacing",
|
|
2124
|
+
"no-auto-layout",
|
|
2125
|
+
"fixed-size-in-auto-layout"
|
|
2126
|
+
];
|
|
2127
|
+
var BATCHABLE_SET = new Set(BATCHABLE_RULE_IDS);
|
|
2128
|
+
var NO_SOURCE_SENTINEL = "_no-source";
|
|
2129
|
+
function groupAndBatchSurveyQuestions(questions) {
|
|
2130
|
+
if (questions.length === 0) {
|
|
2131
|
+
return { groups: [] };
|
|
2132
|
+
}
|
|
2133
|
+
const sorted = [...questions].sort(compareQuestions);
|
|
2134
|
+
const groups = [];
|
|
2135
|
+
let currentGroup = null;
|
|
2136
|
+
let lastGroupKey = null;
|
|
2137
|
+
for (const question of sorted) {
|
|
2138
|
+
const groupKey = sourceComponentKey(question);
|
|
2139
|
+
if (currentGroup === null || groupKey !== lastGroupKey) {
|
|
2140
|
+
currentGroup = {
|
|
2141
|
+
instanceContext: question.instanceContext ?? null,
|
|
2142
|
+
batches: []
|
|
2143
|
+
};
|
|
2144
|
+
groups.push(currentGroup);
|
|
2145
|
+
lastGroupKey = groupKey;
|
|
2146
|
+
}
|
|
2147
|
+
pushIntoBatch(currentGroup, question);
|
|
2148
|
+
}
|
|
2149
|
+
return { groups };
|
|
2150
|
+
}
|
|
2151
|
+
function compareQuestions(a, b) {
|
|
2152
|
+
const aKey = sourceComponentKey(a);
|
|
2153
|
+
const bKey = sourceComponentKey(b);
|
|
2154
|
+
if (aKey !== bKey) {
|
|
2155
|
+
if (aKey === NO_SOURCE_SENTINEL) return 1;
|
|
2156
|
+
if (bKey === NO_SOURCE_SENTINEL) return -1;
|
|
2157
|
+
return aKey.localeCompare(bKey);
|
|
2158
|
+
}
|
|
2159
|
+
if (a.ruleId !== b.ruleId) return a.ruleId.localeCompare(b.ruleId);
|
|
2160
|
+
if (a.nodeName !== b.nodeName) return a.nodeName.localeCompare(b.nodeName);
|
|
2161
|
+
return a.nodeId.localeCompare(b.nodeId);
|
|
2162
|
+
}
|
|
2163
|
+
function sourceComponentKey(question) {
|
|
2164
|
+
return question.instanceContext?.sourceComponentId ?? NO_SOURCE_SENTINEL;
|
|
2165
|
+
}
|
|
2166
|
+
function pushIntoBatch(group, question) {
|
|
2167
|
+
const sceneWeight = Math.max(question.replicas ?? 1, 1);
|
|
2168
|
+
const isBatchable = BATCHABLE_SET.has(question.ruleId);
|
|
2169
|
+
const last = group.batches.at(-1);
|
|
2170
|
+
if (last !== void 0 && last.ruleId === question.ruleId && isBatchable && last.batchable) {
|
|
2171
|
+
last.questions.push(question);
|
|
2172
|
+
last.totalScenes += sceneWeight;
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
group.batches.push({
|
|
2176
|
+
ruleId: question.ruleId,
|
|
2177
|
+
batchable: isBatchable,
|
|
2178
|
+
questions: [question],
|
|
2179
|
+
totalScenes: sceneWeight
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2087
2183
|
// src/core/gotcha/survey-generator.ts
|
|
2088
2184
|
var NODE_PATH_SEPARATOR = " > ";
|
|
2089
|
-
function generateGotchaSurvey(result, scores) {
|
|
2185
|
+
function generateGotchaSurvey(result, scores, options = {}) {
|
|
2090
2186
|
const grade = scores.overall.grade;
|
|
2091
2187
|
const relevantIssues = result.issues.filter(
|
|
2092
2188
|
(issue) => issue.config.severity === "blocking" || issue.config.severity === "risk"
|
|
2093
2189
|
);
|
|
2094
2190
|
const deduped = deduplicateSiblingIssues(relevantIssues);
|
|
2095
2191
|
const sorted = stableSortBySeverity(deduped);
|
|
2096
|
-
const
|
|
2192
|
+
const mapped = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
|
|
2193
|
+
const questions = deduplicateBySourceComponent(mapped);
|
|
2194
|
+
const groupedQuestions = groupAndBatchSurveyQuestions(questions);
|
|
2097
2195
|
return {
|
|
2098
2196
|
designGrade: grade,
|
|
2099
2197
|
isReadyForCodeGen: isReadyForCodeGen(grade),
|
|
2100
|
-
questions
|
|
2198
|
+
questions,
|
|
2199
|
+
groupedQuestions,
|
|
2200
|
+
designKey: options.designKey ?? ""
|
|
2101
2201
|
};
|
|
2102
2202
|
}
|
|
2103
2203
|
function deduplicateSiblingIssues(issues) {
|
|
2104
2204
|
const seen = /* @__PURE__ */ new Set();
|
|
2105
2205
|
const result = [];
|
|
2106
2206
|
for (const issue of issues) {
|
|
2207
|
+
if (isInstanceChildNodeId(issue.violation.nodeId)) {
|
|
2208
|
+
result.push(issue);
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2107
2211
|
const parentPath = getParentPath(issue.violation.nodePath);
|
|
2108
2212
|
const key = `${parentPath}||${issue.violation.ruleId}`;
|
|
2109
2213
|
if (!seen.has(key)) {
|
|
@@ -2163,6 +2267,50 @@ function mapToQuestion(issue, file) {
|
|
|
2163
2267
|
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
|
|
2164
2268
|
};
|
|
2165
2269
|
}
|
|
2270
|
+
function deduplicateBySourceComponent(questions) {
|
|
2271
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2272
|
+
const order = [];
|
|
2273
|
+
let uniqueCounter = 0;
|
|
2274
|
+
for (const q of questions) {
|
|
2275
|
+
const ic = q.instanceContext;
|
|
2276
|
+
let key;
|
|
2277
|
+
if (ic && ic.sourceComponentId && ic.sourceNodeId) {
|
|
2278
|
+
key = `${ic.sourceComponentId}::${ic.sourceNodeId}::${q.ruleId}`;
|
|
2279
|
+
} else {
|
|
2280
|
+
key = `__unique__${uniqueCounter++}`;
|
|
2281
|
+
}
|
|
2282
|
+
const bucket = groups.get(key);
|
|
2283
|
+
if (bucket) {
|
|
2284
|
+
bucket.push(q);
|
|
2285
|
+
} else {
|
|
2286
|
+
groups.set(key, [q]);
|
|
2287
|
+
order.push(key);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return order.map((key) => {
|
|
2291
|
+
const group = groups.get(key);
|
|
2292
|
+
const first = group[0];
|
|
2293
|
+
if (group.length === 1) return first;
|
|
2294
|
+
const otherIds = group.slice(1).map((q) => q.nodeId);
|
|
2295
|
+
const sourceComponentName = first.instanceContext?.sourceComponentName;
|
|
2296
|
+
const template = GOTCHA_QUESTIONS[first.ruleId];
|
|
2297
|
+
const renamed = {
|
|
2298
|
+
...first,
|
|
2299
|
+
replicas: group.length,
|
|
2300
|
+
replicaNodeIds: otherIds
|
|
2301
|
+
};
|
|
2302
|
+
if (sourceComponentName) {
|
|
2303
|
+
renamed.nodeName = sourceComponentName;
|
|
2304
|
+
if (template) {
|
|
2305
|
+
renamed.question = template.question.replace(
|
|
2306
|
+
"{nodeName}",
|
|
2307
|
+
sourceComponentName
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return renamed;
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2166
2314
|
function buildInstanceContext(nodeId, file) {
|
|
2167
2315
|
const parts = parseInstanceChildNodeId(nodeId);
|
|
2168
2316
|
if (!parts) return null;
|
|
@@ -3494,6 +3642,18 @@ function shutdownMonitoring() {
|
|
|
3494
3642
|
// src/core/monitoring/keys.ts
|
|
3495
3643
|
var POSTHOG_API_KEY = "phc_rBFeG140KqJLpUnlpYDEFgdMM6JozZeqQsf9twXf5Dq" ;
|
|
3496
3644
|
var SENTRY_DSN = "https://80a836a8300b25f17ef5bbf23afb5b3a@o4511080656207872.ingest.us.sentry.io/4511080661319680" ;
|
|
3645
|
+
function isFigmaUrl2(input) {
|
|
3646
|
+
return input.includes("figma.com/");
|
|
3647
|
+
}
|
|
3648
|
+
z.string();
|
|
3649
|
+
function computeDesignKey(input) {
|
|
3650
|
+
if (isFigmaUrl2(input)) {
|
|
3651
|
+
const { fileKey, nodeId } = parseFigmaUrl(input);
|
|
3652
|
+
if (!nodeId) return fileKey;
|
|
3653
|
+
return `${fileKey}#${nodeId.replace(/-/g, ":")}`;
|
|
3654
|
+
}
|
|
3655
|
+
return resolve(input);
|
|
3656
|
+
}
|
|
3497
3657
|
|
|
3498
3658
|
// src/core/rules/node-semantics.ts
|
|
3499
3659
|
function isContainerNode(node) {
|
|
@@ -4600,6 +4760,7 @@ var inconsistentNamingConventionCheck = (node, context) => {
|
|
|
4600
4760
|
if (nodeConvention && nodeConvention !== dominantConvention && maxCount >= 2) {
|
|
4601
4761
|
if (isCompatible(nodeConvention, dominantConvention, node.name)) return null;
|
|
4602
4762
|
const suggested = convertName(node.name, dominantConvention);
|
|
4763
|
+
if (suggested === node.name) return null;
|
|
4603
4764
|
return {
|
|
4604
4765
|
ruleId: inconsistentNamingConventionDef.id,
|
|
4605
4766
|
nodeId: node.id,
|
|
@@ -4695,6 +4856,15 @@ function hasStateInComponentMaster(node, context, statePattern) {
|
|
|
4695
4856
|
if (!master) return false;
|
|
4696
4857
|
return hasStateInVariantProps(master, statePattern);
|
|
4697
4858
|
}
|
|
4859
|
+
function canDetermineVariants(node, context) {
|
|
4860
|
+
if (node.type === "COMPONENT") return true;
|
|
4861
|
+
if (node.componentPropertyDefinitions !== void 0) return true;
|
|
4862
|
+
if (node.componentId !== void 0) {
|
|
4863
|
+
const defs = context.file.componentDefinitions;
|
|
4864
|
+
if (defs && defs[node.componentId] !== void 0) return true;
|
|
4865
|
+
}
|
|
4866
|
+
return false;
|
|
4867
|
+
}
|
|
4698
4868
|
var missingInteractionStateDef = {
|
|
4699
4869
|
id: "missing-interaction-state",
|
|
4700
4870
|
name: "Missing Interaction State",
|
|
@@ -4709,6 +4879,7 @@ var missingInteractionStateCheck = (node, context) => {
|
|
|
4709
4879
|
if (!interactiveType) return null;
|
|
4710
4880
|
const expectedStates = EXPECTED_STATES[interactiveType];
|
|
4711
4881
|
if (!expectedStates) return null;
|
|
4882
|
+
if (!canDetermineVariants(node, context)) return null;
|
|
4712
4883
|
const seen = getSeen(context);
|
|
4713
4884
|
const nodePath = context.path.join(" > ");
|
|
4714
4885
|
for (const state of expectedStates) {
|
|
@@ -4827,7 +4998,9 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4827
4998
|
token: z.string().optional().describe("Figma API token (falls back to FIGMA_TOKEN env var)"),
|
|
4828
4999
|
preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional().describe("Analysis preset"),
|
|
4829
5000
|
targetNodeId: z.string().optional().describe("Scope analysis to a specific node ID"),
|
|
4830
|
-
configPath: z.string().optional().describe("Path to config JSON file for rule overrides")
|
|
5001
|
+
configPath: z.string().optional().describe("Path to config JSON file for rule overrides"),
|
|
5002
|
+
openReport: z.boolean().optional().describe("Open the generated HTML report in the user's browser. Defaults to false \u2014 opt in only when a visible report is the explicit user request (#365). The HTML file is always written to disk regardless."),
|
|
5003
|
+
acknowledgments: z.array(AcknowledgmentSchema).optional().describe("(#371) Pre-resolved [{ nodeId, ruleId }] pairs harvested from canicode-authored Figma annotations (e.g. via the `readCanicodeAcknowledgments` Plugin helper inside a use_figma batch). Matching issues are flagged `acknowledged: true` and contribute half weight to the density score so re-analyze surfaces movement after a roundtrip even under ADR-012's annotate-by-default policy.")
|
|
4831
5004
|
},
|
|
4832
5005
|
{
|
|
4833
5006
|
readOnlyHint: false,
|
|
@@ -4835,7 +5008,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4835
5008
|
openWorldHint: true,
|
|
4836
5009
|
title: "Analyze Figma Design"
|
|
4837
5010
|
},
|
|
4838
|
-
async ({ input, token, preset, targetNodeId, configPath }) => {
|
|
5011
|
+
async ({ input, token, preset, targetNodeId, configPath, openReport, acknowledgments }) => {
|
|
4839
5012
|
trackEvent(EVENTS.MCP_TOOL_CALLED, { tool: "analyze" });
|
|
4840
5013
|
try {
|
|
4841
5014
|
const { file, nodeId } = await loadFile(input, token);
|
|
@@ -4847,7 +5020,8 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4847
5020
|
}
|
|
4848
5021
|
const result = analyzeFile(file, {
|
|
4849
5022
|
configs,
|
|
4850
|
-
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {}
|
|
5023
|
+
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {},
|
|
5024
|
+
...acknowledgments && acknowledgments.length > 0 ? { acknowledgments } : {}
|
|
4851
5025
|
});
|
|
4852
5026
|
const scores = calculateScores(result, configs);
|
|
4853
5027
|
const figmaToken = token ?? process.env["FIGMA_TOKEN"];
|
|
@@ -4856,14 +5030,16 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4856
5030
|
const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}`;
|
|
4857
5031
|
ensureReportsDir();
|
|
4858
5032
|
const reportPath = `${getReportsDir()}/report-${ts}-${file.fileKey}.html`;
|
|
4859
|
-
await new Promise((
|
|
5033
|
+
await new Promise((resolve6, reject) => {
|
|
4860
5034
|
writeFile(reportPath, html, "utf-8", (err) => {
|
|
4861
5035
|
if (err) reject(err);
|
|
4862
|
-
else
|
|
5036
|
+
else resolve6();
|
|
4863
5037
|
});
|
|
4864
5038
|
});
|
|
4865
|
-
|
|
4866
|
-
|
|
5039
|
+
if (openReport === true) {
|
|
5040
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
5041
|
+
exec(`${openCmd} "${reportPath}"`);
|
|
5042
|
+
}
|
|
4867
5043
|
trackEvent(EVENTS.ANALYSIS_COMPLETED, {
|
|
4868
5044
|
nodeCount: result.nodeCount,
|
|
4869
5045
|
issueCount: result.issues.length,
|
|
@@ -4875,7 +5051,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4875
5051
|
content: [
|
|
4876
5052
|
{
|
|
4877
5053
|
type: "text",
|
|
4878
|
-
text: JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey }), null, 2)
|
|
5054
|
+
text: JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input) }), null, 2)
|
|
4879
5055
|
}
|
|
4880
5056
|
]
|
|
4881
5057
|
};
|
|
@@ -4935,7 +5111,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4935
5111
|
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {}
|
|
4936
5112
|
});
|
|
4937
5113
|
const scores = calculateScores(result, configs);
|
|
4938
|
-
const survey = generateGotchaSurvey(result, scores);
|
|
5114
|
+
const survey = generateGotchaSurvey(result, scores, { designKey: computeDesignKey(input) });
|
|
4939
5115
|
trackEvent(EVENTS.ANALYSIS_COMPLETED, {
|
|
4940
5116
|
nodeCount: result.nodeCount,
|
|
4941
5117
|
issueCount: result.issues.length,
|
|
@@ -5056,10 +5232,10 @@ Get your token: Figma \u2192 Settings \u2192 Security \u2192 Personal access tok
|
|
|
5056
5232
|
|
|
5057
5233
|
## MCP Server (Claude Code / Cursor / Claude Desktop)
|
|
5058
5234
|
\`\`\`bash
|
|
5059
|
-
claude mcp add canicode
|
|
5235
|
+
claude mcp add canicode -- npx --yes --package=canicode canicode-mcp
|
|
5060
5236
|
\`\`\`
|
|
5061
5237
|
|
|
5062
|
-
Requires FIGMA_TOKEN for live Figma URL analysis.
|
|
5238
|
+
Requires FIGMA_TOKEN for live Figma URL analysis. The MCP server reads it from \`~/.canicode/config.json\` (set via \`canicode init --token \u2026\`) or from the host's environment, so do **not** pass \`-e FIGMA_TOKEN=\u2026\` to \`claude mcp add\` \u2014 \`@anthropic-ai/claude-code\`'s current parser rejects short-form flags placed before \`--\`. (#364, #366)
|
|
5063
5239
|
|
|
5064
5240
|
## CLI only (no MCP server)
|
|
5065
5241
|
|
|
@@ -5179,16 +5355,16 @@ ${inlineTopics[selectedTopic]}` }]
|
|
|
5179
5355
|
};
|
|
5180
5356
|
}
|
|
5181
5357
|
const { readFile: readFile3 } = await import('fs/promises');
|
|
5182
|
-
const { resolve:
|
|
5358
|
+
const { resolve: resolve6, dirname: dirname4 } = await import('path');
|
|
5183
5359
|
const { fileURLToPath } = await import('url');
|
|
5184
5360
|
try {
|
|
5185
5361
|
const __dirname = dirname4(fileURLToPath(import.meta.url));
|
|
5186
|
-
const docPath =
|
|
5362
|
+
const docPath = resolve6(__dirname, "../../docs/CUSTOMIZATION.md");
|
|
5187
5363
|
let content;
|
|
5188
5364
|
try {
|
|
5189
5365
|
content = await readFile3(docPath, "utf-8");
|
|
5190
5366
|
} catch {
|
|
5191
|
-
const altPath =
|
|
5367
|
+
const altPath = resolve6(__dirname, "../docs/CUSTOMIZATION.md");
|
|
5192
5368
|
content = await readFile3(altPath, "utf-8");
|
|
5193
5369
|
}
|
|
5194
5370
|
if (selectedTopic !== "all") {
|