canicode 0.10.3 → 0.10.5
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 +35 -30
- package/dist/cli/index.js +358 -24
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +257 -3
- package/dist/index.js +96 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +157 -26
- package/dist/mcp/server.js.map +1 -1
- package/package.json +2 -1
- package/skills/canicode-gotchas/SKILL.md +66 -28
- package/skills/canicode-roundtrip/SKILL.md +180 -79
- package/skills/canicode-roundtrip/helpers.js +218 -2
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.5";
|
|
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,9 +2117,72 @@ 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"
|
|
@@ -2095,16 +2191,23 @@ function generateGotchaSurvey(result, scores) {
|
|
|
2095
2191
|
const sorted = stableSortBySeverity(deduped);
|
|
2096
2192
|
const mapped = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
|
|
2097
2193
|
const questions = deduplicateBySourceComponent(mapped);
|
|
2194
|
+
const groupedQuestions = groupAndBatchSurveyQuestions(questions);
|
|
2098
2195
|
return {
|
|
2099
2196
|
designGrade: grade,
|
|
2100
2197
|
isReadyForCodeGen: isReadyForCodeGen(grade),
|
|
2101
|
-
questions
|
|
2198
|
+
questions,
|
|
2199
|
+
groupedQuestions,
|
|
2200
|
+
designKey: options.designKey ?? ""
|
|
2102
2201
|
};
|
|
2103
2202
|
}
|
|
2104
2203
|
function deduplicateSiblingIssues(issues) {
|
|
2105
2204
|
const seen = /* @__PURE__ */ new Set();
|
|
2106
2205
|
const result = [];
|
|
2107
2206
|
for (const issue of issues) {
|
|
2207
|
+
if (isInstanceChildNodeId(issue.violation.nodeId)) {
|
|
2208
|
+
result.push(issue);
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2108
2211
|
const parentPath = getParentPath(issue.violation.nodePath);
|
|
2109
2212
|
const key = `${parentPath}||${issue.violation.ruleId}`;
|
|
2110
2213
|
if (!seen.has(key)) {
|
|
@@ -3539,6 +3642,18 @@ function shutdownMonitoring() {
|
|
|
3539
3642
|
// src/core/monitoring/keys.ts
|
|
3540
3643
|
var POSTHOG_API_KEY = "phc_rBFeG140KqJLpUnlpYDEFgdMM6JozZeqQsf9twXf5Dq" ;
|
|
3541
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
|
+
}
|
|
3542
3657
|
|
|
3543
3658
|
// src/core/rules/node-semantics.ts
|
|
3544
3659
|
function isContainerNode(node) {
|
|
@@ -4645,6 +4760,7 @@ var inconsistentNamingConventionCheck = (node, context) => {
|
|
|
4645
4760
|
if (nodeConvention && nodeConvention !== dominantConvention && maxCount >= 2) {
|
|
4646
4761
|
if (isCompatible(nodeConvention, dominantConvention, node.name)) return null;
|
|
4647
4762
|
const suggested = convertName(node.name, dominantConvention);
|
|
4763
|
+
if (suggested === node.name) return null;
|
|
4648
4764
|
return {
|
|
4649
4765
|
ruleId: inconsistentNamingConventionDef.id,
|
|
4650
4766
|
nodeId: node.id,
|
|
@@ -4740,12 +4856,22 @@ function hasStateInComponentMaster(node, context, statePattern) {
|
|
|
4740
4856
|
if (!master) return false;
|
|
4741
4857
|
return hasStateInVariantProps(master, statePattern);
|
|
4742
4858
|
}
|
|
4859
|
+
var VARIANT_POSITION_NAME_RE = /^[\w ]+=[^,]+(,\s*[\w ]+=[^,]+)*$/;
|
|
4860
|
+
function hasUsablePropDefs(propDefs) {
|
|
4861
|
+
return propDefs != null && typeof propDefs === "object";
|
|
4862
|
+
}
|
|
4743
4863
|
function canDetermineVariants(node, context) {
|
|
4744
|
-
if (node.
|
|
4745
|
-
if (node.
|
|
4864
|
+
if (hasUsablePropDefs(node.componentPropertyDefinitions)) return true;
|
|
4865
|
+
if (node.type === "COMPONENT") {
|
|
4866
|
+
return !VARIANT_POSITION_NAME_RE.test(node.name);
|
|
4867
|
+
}
|
|
4746
4868
|
if (node.componentId !== void 0) {
|
|
4747
4869
|
const defs = context.file.componentDefinitions;
|
|
4748
|
-
|
|
4870
|
+
const master = defs?.[node.componentId];
|
|
4871
|
+
if (master) {
|
|
4872
|
+
if (hasUsablePropDefs(master.componentPropertyDefinitions)) return true;
|
|
4873
|
+
return !VARIANT_POSITION_NAME_RE.test(master.name);
|
|
4874
|
+
}
|
|
4749
4875
|
}
|
|
4750
4876
|
return false;
|
|
4751
4877
|
}
|
|
@@ -4882,7 +5008,9 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4882
5008
|
token: z.string().optional().describe("Figma API token (falls back to FIGMA_TOKEN env var)"),
|
|
4883
5009
|
preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional().describe("Analysis preset"),
|
|
4884
5010
|
targetNodeId: z.string().optional().describe("Scope analysis to a specific node ID"),
|
|
4885
|
-
configPath: z.string().optional().describe("Path to config JSON file for rule overrides")
|
|
5011
|
+
configPath: z.string().optional().describe("Path to config JSON file for rule overrides"),
|
|
5012
|
+
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."),
|
|
5013
|
+
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.")
|
|
4886
5014
|
},
|
|
4887
5015
|
{
|
|
4888
5016
|
readOnlyHint: false,
|
|
@@ -4890,7 +5018,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4890
5018
|
openWorldHint: true,
|
|
4891
5019
|
title: "Analyze Figma Design"
|
|
4892
5020
|
},
|
|
4893
|
-
async ({ input, token, preset, targetNodeId, configPath }) => {
|
|
5021
|
+
async ({ input, token, preset, targetNodeId, configPath, openReport, acknowledgments }) => {
|
|
4894
5022
|
trackEvent(EVENTS.MCP_TOOL_CALLED, { tool: "analyze" });
|
|
4895
5023
|
try {
|
|
4896
5024
|
const { file, nodeId } = await loadFile(input, token);
|
|
@@ -4902,7 +5030,8 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4902
5030
|
}
|
|
4903
5031
|
const result = analyzeFile(file, {
|
|
4904
5032
|
configs,
|
|
4905
|
-
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {}
|
|
5033
|
+
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {},
|
|
5034
|
+
...acknowledgments && acknowledgments.length > 0 ? { acknowledgments } : {}
|
|
4906
5035
|
});
|
|
4907
5036
|
const scores = calculateScores(result, configs);
|
|
4908
5037
|
const figmaToken = token ?? process.env["FIGMA_TOKEN"];
|
|
@@ -4911,14 +5040,16 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4911
5040
|
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")}`;
|
|
4912
5041
|
ensureReportsDir();
|
|
4913
5042
|
const reportPath = `${getReportsDir()}/report-${ts}-${file.fileKey}.html`;
|
|
4914
|
-
await new Promise((
|
|
5043
|
+
await new Promise((resolve6, reject) => {
|
|
4915
5044
|
writeFile(reportPath, html, "utf-8", (err) => {
|
|
4916
5045
|
if (err) reject(err);
|
|
4917
|
-
else
|
|
5046
|
+
else resolve6();
|
|
4918
5047
|
});
|
|
4919
5048
|
});
|
|
4920
|
-
|
|
4921
|
-
|
|
5049
|
+
if (openReport === true) {
|
|
5050
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
5051
|
+
exec(`${openCmd} "${reportPath}"`);
|
|
5052
|
+
}
|
|
4922
5053
|
trackEvent(EVENTS.ANALYSIS_COMPLETED, {
|
|
4923
5054
|
nodeCount: result.nodeCount,
|
|
4924
5055
|
issueCount: result.issues.length,
|
|
@@ -4930,7 +5061,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4930
5061
|
content: [
|
|
4931
5062
|
{
|
|
4932
5063
|
type: "text",
|
|
4933
|
-
text: JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey }), null, 2)
|
|
5064
|
+
text: JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input) }), null, 2)
|
|
4934
5065
|
}
|
|
4935
5066
|
]
|
|
4936
5067
|
};
|
|
@@ -4990,7 +5121,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4990
5121
|
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {}
|
|
4991
5122
|
});
|
|
4992
5123
|
const scores = calculateScores(result, configs);
|
|
4993
|
-
const survey = generateGotchaSurvey(result, scores);
|
|
5124
|
+
const survey = generateGotchaSurvey(result, scores, { designKey: computeDesignKey(input) });
|
|
4994
5125
|
trackEvent(EVENTS.ANALYSIS_COMPLETED, {
|
|
4995
5126
|
nodeCount: result.nodeCount,
|
|
4996
5127
|
issueCount: result.issues.length,
|
|
@@ -5111,10 +5242,10 @@ Get your token: Figma \u2192 Settings \u2192 Security \u2192 Personal access tok
|
|
|
5111
5242
|
|
|
5112
5243
|
## MCP Server (Claude Code / Cursor / Claude Desktop)
|
|
5113
5244
|
\`\`\`bash
|
|
5114
|
-
claude mcp add canicode
|
|
5245
|
+
claude mcp add canicode -- npx --yes --package=canicode canicode-mcp
|
|
5115
5246
|
\`\`\`
|
|
5116
5247
|
|
|
5117
|
-
Requires FIGMA_TOKEN for live Figma URL analysis.
|
|
5248
|
+
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)
|
|
5118
5249
|
|
|
5119
5250
|
## CLI only (no MCP server)
|
|
5120
5251
|
|
|
@@ -5234,16 +5365,16 @@ ${inlineTopics[selectedTopic]}` }]
|
|
|
5234
5365
|
};
|
|
5235
5366
|
}
|
|
5236
5367
|
const { readFile: readFile3 } = await import('fs/promises');
|
|
5237
|
-
const { resolve:
|
|
5368
|
+
const { resolve: resolve6, dirname: dirname4 } = await import('path');
|
|
5238
5369
|
const { fileURLToPath } = await import('url');
|
|
5239
5370
|
try {
|
|
5240
5371
|
const __dirname = dirname4(fileURLToPath(import.meta.url));
|
|
5241
|
-
const docPath =
|
|
5372
|
+
const docPath = resolve6(__dirname, "../../docs/CUSTOMIZATION.md");
|
|
5242
5373
|
let content;
|
|
5243
5374
|
try {
|
|
5244
5375
|
content = await readFile3(docPath, "utf-8");
|
|
5245
5376
|
} catch {
|
|
5246
|
-
const altPath =
|
|
5377
|
+
const altPath = resolve6(__dirname, "../docs/CUSTOMIZATION.md");
|
|
5247
5378
|
content = await readFile3(altPath, "utf-8");
|
|
5248
5379
|
}
|
|
5249
5380
|
if (selectedTopic !== "all") {
|