canicode 0.9.1 → 0.10.1
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 +40 -45
- package/dist/cli/index.js +1220 -603
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +308 -2
- package/dist/index.js +275 -11
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +494 -12
- package/dist/mcp/server.js.map +1 -1
- package/package.json +7 -5
- package/skills/canicode/SKILL.md +76 -0
- package/skills/canicode-gotchas/SKILL.md +160 -0
- package/skills/canicode-roundtrip/SKILL.md +413 -0
- package/skills/canicode-roundtrip/helpers.js +263 -0
- package/.claude/skills/design-to-code/PROMPT.md +0 -143
package/dist/mcp/server.js
CHANGED
|
@@ -510,6 +510,44 @@ function getConfigsWithPreset(preset) {
|
|
|
510
510
|
}
|
|
511
511
|
return configs;
|
|
512
512
|
}
|
|
513
|
+
var RULE_ANNOTATION_PROPERTIES = {
|
|
514
|
+
"missing-size-constraint": {
|
|
515
|
+
default: [{ type: "width" }, { type: "height" }]
|
|
516
|
+
},
|
|
517
|
+
"irregular-spacing": {
|
|
518
|
+
bySubType: {
|
|
519
|
+
gap: [{ type: "itemSpacing" }],
|
|
520
|
+
padding: [{ type: "padding" }]
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
"fixed-size-in-auto-layout": {
|
|
524
|
+
default: [{ type: "width" }, { type: "height" }, { type: "layoutMode" }]
|
|
525
|
+
},
|
|
526
|
+
"raw-value": {
|
|
527
|
+
bySubType: {
|
|
528
|
+
color: [{ type: "fills" }],
|
|
529
|
+
font: [
|
|
530
|
+
{ type: "fontSize" },
|
|
531
|
+
{ type: "fontFamily" },
|
|
532
|
+
{ type: "fontWeight" },
|
|
533
|
+
{ type: "lineHeight" }
|
|
534
|
+
],
|
|
535
|
+
spacing: [{ type: "itemSpacing" }, { type: "padding" }]
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
"absolute-position-in-auto-layout": {
|
|
539
|
+
default: [{ type: "layoutMode" }]
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
function getAnnotationProperties(ruleId, subType) {
|
|
543
|
+
const entry = RULE_ANNOTATION_PROPERTIES[ruleId];
|
|
544
|
+
if (!entry) return void 0;
|
|
545
|
+
if (subType !== void 0 && entry.bySubType) {
|
|
546
|
+
const match = entry.bySubType[subType];
|
|
547
|
+
if (match) return match;
|
|
548
|
+
}
|
|
549
|
+
return entry.default;
|
|
550
|
+
}
|
|
513
551
|
function getRuleOption(ruleId, optionKey, defaultValue) {
|
|
514
552
|
const config2 = RULE_CONFIGS[ruleId];
|
|
515
553
|
if (!config2.options) return defaultValue;
|
|
@@ -1625,8 +1663,97 @@ async function loadFromApi(fileKey, nodeId, token) {
|
|
|
1625
1663
|
return { file, nodeId };
|
|
1626
1664
|
}
|
|
1627
1665
|
|
|
1666
|
+
// src/core/adapters/instance-id-parser.ts
|
|
1667
|
+
function isInstanceChildNodeId(nodeId) {
|
|
1668
|
+
return nodeId.startsWith("I") && nodeId.includes(";");
|
|
1669
|
+
}
|
|
1670
|
+
function parseInstanceChildNodeId(nodeId) {
|
|
1671
|
+
if (!isInstanceChildNodeId(nodeId)) return null;
|
|
1672
|
+
const segments = nodeId.split(";");
|
|
1673
|
+
if (segments.length < 2) return null;
|
|
1674
|
+
const parentInstanceId = segments[0].replace(/^I/, "");
|
|
1675
|
+
const sourceNodeId = segments[segments.length - 1];
|
|
1676
|
+
if (!parentInstanceId || !sourceNodeId) return null;
|
|
1677
|
+
return { parentInstanceId, sourceNodeId };
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// src/core/gotcha/apply-context.ts
|
|
1681
|
+
var STRATEGY_BY_RULE = {
|
|
1682
|
+
// Strategy A — property modification
|
|
1683
|
+
"no-auto-layout": "property-mod",
|
|
1684
|
+
"fixed-size-in-auto-layout": "property-mod",
|
|
1685
|
+
"missing-size-constraint": "property-mod",
|
|
1686
|
+
"irregular-spacing": "property-mod",
|
|
1687
|
+
"non-semantic-name": "property-mod",
|
|
1688
|
+
// Strategy B — structural modification (needs user confirmation)
|
|
1689
|
+
"non-layout-container": "structural-mod",
|
|
1690
|
+
"deep-nesting": "structural-mod",
|
|
1691
|
+
"missing-component": "structural-mod",
|
|
1692
|
+
"detached-instance": "structural-mod",
|
|
1693
|
+
// Strategy C — annotation only
|
|
1694
|
+
"absolute-position-in-auto-layout": "annotation",
|
|
1695
|
+
"variant-structure-mismatch": "annotation",
|
|
1696
|
+
// Strategy D — auto-fix lower-severity issues from analyze output
|
|
1697
|
+
"non-standard-naming": "auto-fix",
|
|
1698
|
+
"inconsistent-naming-convention": "auto-fix",
|
|
1699
|
+
"raw-value": "auto-fix",
|
|
1700
|
+
"missing-interaction-state": "auto-fix",
|
|
1701
|
+
"missing-prototype": "auto-fix"
|
|
1702
|
+
};
|
|
1703
|
+
function resolveTargetProperty(ruleId, subType) {
|
|
1704
|
+
switch (ruleId) {
|
|
1705
|
+
case "no-auto-layout":
|
|
1706
|
+
return ["layoutMode", "itemSpacing"];
|
|
1707
|
+
case "fixed-size-in-auto-layout":
|
|
1708
|
+
if (subType === "horizontal") return "layoutSizingHorizontal";
|
|
1709
|
+
return ["layoutSizingHorizontal", "layoutSizingVertical"];
|
|
1710
|
+
case "missing-size-constraint":
|
|
1711
|
+
if (subType === "wrap") return "minWidth";
|
|
1712
|
+
if (subType === "max-width") return "maxWidth";
|
|
1713
|
+
return ["minWidth", "maxWidth"];
|
|
1714
|
+
case "irregular-spacing":
|
|
1715
|
+
if (subType === "gap") return "itemSpacing";
|
|
1716
|
+
return ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"];
|
|
1717
|
+
case "non-semantic-name":
|
|
1718
|
+
return "name";
|
|
1719
|
+
case "non-layout-container":
|
|
1720
|
+
return "layoutMode";
|
|
1721
|
+
case "non-standard-naming":
|
|
1722
|
+
case "inconsistent-naming-convention":
|
|
1723
|
+
return "name";
|
|
1724
|
+
case "deep-nesting":
|
|
1725
|
+
case "missing-component":
|
|
1726
|
+
case "detached-instance":
|
|
1727
|
+
case "absolute-position-in-auto-layout":
|
|
1728
|
+
case "variant-structure-mismatch":
|
|
1729
|
+
case "raw-value":
|
|
1730
|
+
case "missing-interaction-state":
|
|
1731
|
+
case "missing-prototype":
|
|
1732
|
+
return void 0;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function computeApplyContext(violation, instanceContext) {
|
|
1736
|
+
const ruleId = violation.ruleId;
|
|
1737
|
+
const applyStrategy = STRATEGY_BY_RULE[ruleId] ?? "annotation";
|
|
1738
|
+
const targetProperty = resolveTargetProperty(ruleId, violation.subType);
|
|
1739
|
+
const annotationProperties = getAnnotationProperties(
|
|
1740
|
+
ruleId,
|
|
1741
|
+
violation.subType
|
|
1742
|
+
);
|
|
1743
|
+
const parsed = parseInstanceChildNodeId(violation.nodeId);
|
|
1744
|
+
const isInstanceChild = parsed !== null || isInstanceChildNodeId(violation.nodeId);
|
|
1745
|
+
const sourceChildId = instanceContext?.sourceNodeId ?? parsed?.sourceNodeId;
|
|
1746
|
+
return {
|
|
1747
|
+
applyStrategy,
|
|
1748
|
+
...targetProperty !== void 0 ? { targetProperty } : {},
|
|
1749
|
+
...annotationProperties !== void 0 ? { annotationProperties } : {},
|
|
1750
|
+
isInstanceChild,
|
|
1751
|
+
...sourceChildId !== void 0 ? { sourceChildId } : {}
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1628
1755
|
// package.json
|
|
1629
|
-
var version = "0.
|
|
1756
|
+
var version = "0.10.1";
|
|
1630
1757
|
|
|
1631
1758
|
// src/core/engine/scoring.ts
|
|
1632
1759
|
function computeTotalScorePerCategory(configs) {
|
|
@@ -1655,6 +1782,9 @@ function calculateGrade(percentage) {
|
|
|
1655
1782
|
if (percentage >= 50) return "D";
|
|
1656
1783
|
return "F";
|
|
1657
1784
|
}
|
|
1785
|
+
function isReadyForCodeGen(grade) {
|
|
1786
|
+
return grade === "S" || grade === "A+" || grade === "A";
|
|
1787
|
+
}
|
|
1658
1788
|
function clamp(value, min, max) {
|
|
1659
1789
|
return Math.max(min, Math.min(max, value));
|
|
1660
1790
|
}
|
|
@@ -1802,14 +1932,24 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
1802
1932
|
const id = issue.violation.ruleId;
|
|
1803
1933
|
issuesByRule[id] = (issuesByRule[id] ?? 0) + 1;
|
|
1804
1934
|
}
|
|
1805
|
-
const issues = result.issues.map((issue) =>
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1935
|
+
const issues = result.issues.map((issue) => {
|
|
1936
|
+
const applyContext = computeApplyContext(issue.violation);
|
|
1937
|
+
const suggestedName = issue.violation.suggestedName;
|
|
1938
|
+
return {
|
|
1939
|
+
ruleId: issue.violation.ruleId,
|
|
1940
|
+
...issue.violation.subType && { subType: issue.violation.subType },
|
|
1941
|
+
severity: issue.config.severity,
|
|
1942
|
+
nodeId: issue.violation.nodeId,
|
|
1943
|
+
nodePath: issue.violation.nodePath,
|
|
1944
|
+
message: issue.violation.message,
|
|
1945
|
+
applyStrategy: applyContext.applyStrategy,
|
|
1946
|
+
...applyContext.targetProperty !== void 0 ? { targetProperty: applyContext.targetProperty } : {},
|
|
1947
|
+
...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
|
|
1948
|
+
...suggestedName !== void 0 ? { suggestedName } : {},
|
|
1949
|
+
isInstanceChild: applyContext.isInstanceChild,
|
|
1950
|
+
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
|
|
1951
|
+
};
|
|
1952
|
+
});
|
|
1813
1953
|
const json = {
|
|
1814
1954
|
version,
|
|
1815
1955
|
analyzedAt: result.analyzedAt,
|
|
@@ -1818,6 +1958,8 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
1818
1958
|
nodeCount: result.nodeCount,
|
|
1819
1959
|
maxDepth: result.maxDepth,
|
|
1820
1960
|
issueCount: result.issues.length,
|
|
1961
|
+
isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade),
|
|
1962
|
+
blockingIssueCount: scores.summary.blocking,
|
|
1821
1963
|
scores: {
|
|
1822
1964
|
overall: scores.overall,
|
|
1823
1965
|
categories: scores.byCategory
|
|
@@ -1831,6 +1973,219 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
1831
1973
|
}
|
|
1832
1974
|
return json;
|
|
1833
1975
|
}
|
|
1976
|
+
z.object({
|
|
1977
|
+
ruleId: z.string(),
|
|
1978
|
+
question: z.string(),
|
|
1979
|
+
hint: z.string(),
|
|
1980
|
+
example: z.string()
|
|
1981
|
+
});
|
|
1982
|
+
var GOTCHA_QUESTIONS = {
|
|
1983
|
+
// ── Pixel Critical (blocking) ──
|
|
1984
|
+
"no-auto-layout": {
|
|
1985
|
+
ruleId: "no-auto-layout",
|
|
1986
|
+
question: 'Frame "{nodeName}" has no Auto Layout. How should this area be laid out?',
|
|
1987
|
+
hint: "Describe the flex direction, gap, and alignment",
|
|
1988
|
+
example: "Vertical flex, gap 16px, items centered"
|
|
1989
|
+
},
|
|
1990
|
+
"absolute-position-in-auto-layout": {
|
|
1991
|
+
ruleId: "absolute-position-in-auto-layout",
|
|
1992
|
+
question: '"{nodeName}" uses absolute positioning inside an Auto Layout parent. Is this an intentional overlay, or should it flow with the layout?',
|
|
1993
|
+
hint: "Specify if this is a badge/overlay, or should be part of the normal flow",
|
|
1994
|
+
example: "This is a notification badge \u2014 position absolute, top-right corner"
|
|
1995
|
+
},
|
|
1996
|
+
"non-layout-container": {
|
|
1997
|
+
ruleId: "non-layout-container",
|
|
1998
|
+
question: '"{nodeName}" is a Group/Section used as a layout container. What layout structure should it have?',
|
|
1999
|
+
hint: "Describe the intended layout: flex direction, wrap, gap",
|
|
2000
|
+
example: "Horizontal flex, gap 12px, wrap on mobile"
|
|
2001
|
+
},
|
|
2002
|
+
// ── Responsive Critical (risk) ──
|
|
2003
|
+
"fixed-size-in-auto-layout": {
|
|
2004
|
+
ruleId: "fixed-size-in-auto-layout",
|
|
2005
|
+
question: '"{nodeName}" has a fixed size inside Auto Layout. Should it be responsive?',
|
|
2006
|
+
hint: "Specify which axis should be flexible (width, height, or both)",
|
|
2007
|
+
example: "Width should FILL the parent, height can stay fixed"
|
|
2008
|
+
},
|
|
2009
|
+
"missing-size-constraint": {
|
|
2010
|
+
ruleId: "missing-size-constraint",
|
|
2011
|
+
question: '"{nodeName}" uses FILL sizing without min/max constraints. What are the size boundaries?',
|
|
2012
|
+
hint: "Provide min-width, max-width, or both",
|
|
2013
|
+
example: "min-width 320px, max-width 1200px"
|
|
2014
|
+
},
|
|
2015
|
+
// ── Code Quality (risk) ──
|
|
2016
|
+
"missing-component": {
|
|
2017
|
+
ruleId: "missing-component",
|
|
2018
|
+
question: '"{nodeName}" appears to be a repeated structure. Should it be a reusable component?',
|
|
2019
|
+
hint: "Describe if this should be extracted as a component and what props it needs",
|
|
2020
|
+
example: "Yes, extract as ProductCard component with title, image, and price props"
|
|
2021
|
+
},
|
|
2022
|
+
"detached-instance": {
|
|
2023
|
+
ruleId: "detached-instance",
|
|
2024
|
+
question: '"{nodeName}" looks like a detached component instance. Should it use the original component or is it a new variant?',
|
|
2025
|
+
hint: "Specify whether to restore the component link or create a new variant",
|
|
2026
|
+
example: "This is a new variant \u2014 create a 'compact' variant of the original component"
|
|
2027
|
+
},
|
|
2028
|
+
"variant-structure-mismatch": {
|
|
2029
|
+
ruleId: "variant-structure-mismatch",
|
|
2030
|
+
question: '"{nodeName}" has variants with different child structures. Which structure is the canonical one?',
|
|
2031
|
+
hint: "Describe which variant has the correct structure, or if they should all match",
|
|
2032
|
+
example: "Default variant is canonical \u2014 other variants should toggle child visibility instead of adding/removing elements"
|
|
2033
|
+
},
|
|
2034
|
+
"deep-nesting": {
|
|
2035
|
+
ruleId: "deep-nesting",
|
|
2036
|
+
question: '"{nodeName}" is deeply nested. Can some intermediate layers be flattened or extracted?',
|
|
2037
|
+
hint: "Identify which wrapper layers are unnecessary or should become sub-components",
|
|
2038
|
+
example: "The inner wrapper is just for spacing \u2014 flatten it and use padding instead"
|
|
2039
|
+
},
|
|
2040
|
+
// ── Token Management ──
|
|
2041
|
+
"raw-value": {
|
|
2042
|
+
ruleId: "raw-value",
|
|
2043
|
+
question: '"{nodeName}" uses raw values without design tokens. What tokens should be used?',
|
|
2044
|
+
hint: "Specify the token names or variable references for colors, fonts, spacing, etc.",
|
|
2045
|
+
example: "Use $color-primary for the fill, $font-body for the text style"
|
|
2046
|
+
},
|
|
2047
|
+
"irregular-spacing": {
|
|
2048
|
+
ruleId: "irregular-spacing",
|
|
2049
|
+
question: '"{nodeName}" has spacing values that are off the design grid. What should the correct spacing be?',
|
|
2050
|
+
hint: "Provide the intended spacing value aligned to the grid system",
|
|
2051
|
+
example: "Gap should be 16px (4pt grid), not 15px"
|
|
2052
|
+
},
|
|
2053
|
+
// ── Interaction ──
|
|
2054
|
+
"missing-interaction-state": {
|
|
2055
|
+
ruleId: "missing-interaction-state",
|
|
2056
|
+
question: '"{nodeName}" appears interactive but is missing state variants. What interaction states are needed?',
|
|
2057
|
+
hint: "List the needed states: Hover, Active, Disabled, Focus",
|
|
2058
|
+
example: "Needs Hover (darken 10%) and Disabled (opacity 50%, no pointer events)"
|
|
2059
|
+
},
|
|
2060
|
+
"missing-prototype": {
|
|
2061
|
+
ruleId: "missing-prototype",
|
|
2062
|
+
question: '"{nodeName}" looks interactive but has no prototype interaction. What should happen on click/interaction?',
|
|
2063
|
+
hint: "Describe the interaction behavior: navigation, overlay, state change, etc.",
|
|
2064
|
+
example: "On click, navigate to the product detail page"
|
|
2065
|
+
},
|
|
2066
|
+
// ── Semantic ──
|
|
2067
|
+
"non-standard-naming": {
|
|
2068
|
+
ruleId: "non-standard-naming",
|
|
2069
|
+
question: '"{nodeName}" uses non-standard state names. What naming convention should be followed?',
|
|
2070
|
+
hint: "Specify the expected state name format (e.g., Hover, Disabled, Active)",
|
|
2071
|
+
example: 'Use "Hover" instead of "hover_v1", "Disabled" instead of "off"'
|
|
2072
|
+
},
|
|
2073
|
+
"non-semantic-name": {
|
|
2074
|
+
ruleId: "non-semantic-name",
|
|
2075
|
+
question: '"{nodeName}" has a non-semantic name. What is the purpose of this element?',
|
|
2076
|
+
hint: "Provide a descriptive name that reflects the element's role in the UI",
|
|
2077
|
+
example: 'Rename "Frame 12" to "HeroSection" or "ProductGrid"'
|
|
2078
|
+
},
|
|
2079
|
+
"inconsistent-naming-convention": {
|
|
2080
|
+
ruleId: "inconsistent-naming-convention",
|
|
2081
|
+
question: '"{nodeName}" uses a different naming convention than its siblings. Which convention should be used?',
|
|
2082
|
+
hint: "Choose one: camelCase, kebab-case, PascalCase, or Title Case",
|
|
2083
|
+
example: "Use PascalCase for all component layers (e.g., CardTitle, CardBody)"
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
// src/core/gotcha/survey-generator.ts
|
|
2088
|
+
var NODE_PATH_SEPARATOR = " > ";
|
|
2089
|
+
function generateGotchaSurvey(result, scores) {
|
|
2090
|
+
const grade = scores.overall.grade;
|
|
2091
|
+
const relevantIssues = result.issues.filter(
|
|
2092
|
+
(issue) => issue.config.severity === "blocking" || issue.config.severity === "risk"
|
|
2093
|
+
);
|
|
2094
|
+
const deduped = deduplicateSiblingIssues(relevantIssues);
|
|
2095
|
+
const sorted = stableSortBySeverity(deduped);
|
|
2096
|
+
const questions = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
|
|
2097
|
+
return {
|
|
2098
|
+
designGrade: grade,
|
|
2099
|
+
isReadyForCodeGen: isReadyForCodeGen(grade),
|
|
2100
|
+
questions
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
function deduplicateSiblingIssues(issues) {
|
|
2104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2105
|
+
const result = [];
|
|
2106
|
+
for (const issue of issues) {
|
|
2107
|
+
const parentPath = getParentPath(issue.violation.nodePath);
|
|
2108
|
+
const key = `${parentPath}||${issue.violation.ruleId}`;
|
|
2109
|
+
if (!seen.has(key)) {
|
|
2110
|
+
seen.add(key);
|
|
2111
|
+
result.push(issue);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return result;
|
|
2115
|
+
}
|
|
2116
|
+
function getParentPath(nodePath) {
|
|
2117
|
+
const lastSep = nodePath.lastIndexOf(NODE_PATH_SEPARATOR);
|
|
2118
|
+
if (lastSep === -1) return "";
|
|
2119
|
+
return nodePath.slice(0, lastSep);
|
|
2120
|
+
}
|
|
2121
|
+
function getNodeName(nodePath) {
|
|
2122
|
+
const lastSep = nodePath.lastIndexOf(NODE_PATH_SEPARATOR);
|
|
2123
|
+
if (lastSep === -1) return nodePath;
|
|
2124
|
+
return nodePath.slice(lastSep + NODE_PATH_SEPARATOR.length);
|
|
2125
|
+
}
|
|
2126
|
+
function stableSortBySeverity(issues) {
|
|
2127
|
+
const blocking = [];
|
|
2128
|
+
const risk = [];
|
|
2129
|
+
for (const issue of issues) {
|
|
2130
|
+
if (issue.config.severity === "blocking") {
|
|
2131
|
+
blocking.push(issue);
|
|
2132
|
+
} else {
|
|
2133
|
+
risk.push(issue);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return [...blocking, ...risk];
|
|
2137
|
+
}
|
|
2138
|
+
function mapToQuestion(issue, file) {
|
|
2139
|
+
const ruleId = issue.violation.ruleId;
|
|
2140
|
+
const template = GOTCHA_QUESTIONS[ruleId];
|
|
2141
|
+
if (!template) return null;
|
|
2142
|
+
const nodeName = getNodeName(issue.violation.nodePath);
|
|
2143
|
+
const instanceContext = buildInstanceContext(issue.violation.nodeId, file);
|
|
2144
|
+
const applyContext = computeApplyContext(
|
|
2145
|
+
issue.violation,
|
|
2146
|
+
instanceContext ?? void 0
|
|
2147
|
+
);
|
|
2148
|
+
const suggestedName = issue.violation.suggestedName;
|
|
2149
|
+
return {
|
|
2150
|
+
nodeId: issue.violation.nodeId,
|
|
2151
|
+
nodeName,
|
|
2152
|
+
ruleId,
|
|
2153
|
+
severity: issue.config.severity,
|
|
2154
|
+
question: template.question.replace("{nodeName}", nodeName),
|
|
2155
|
+
hint: template.hint,
|
|
2156
|
+
example: template.example,
|
|
2157
|
+
...instanceContext ? { instanceContext } : {},
|
|
2158
|
+
applyStrategy: applyContext.applyStrategy,
|
|
2159
|
+
...applyContext.targetProperty !== void 0 ? { targetProperty: applyContext.targetProperty } : {},
|
|
2160
|
+
...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
|
|
2161
|
+
...suggestedName !== void 0 ? { suggestedName } : {},
|
|
2162
|
+
isInstanceChild: applyContext.isInstanceChild,
|
|
2163
|
+
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function buildInstanceContext(nodeId, file) {
|
|
2167
|
+
const parts = parseInstanceChildNodeId(nodeId);
|
|
2168
|
+
if (!parts) return null;
|
|
2169
|
+
const parentInstance = findNodeById2(file.document, parts.parentInstanceId);
|
|
2170
|
+
const componentId = parentInstance?.componentId;
|
|
2171
|
+
const componentName = componentId ? file.components[componentId]?.name : void 0;
|
|
2172
|
+
return {
|
|
2173
|
+
parentInstanceNodeId: parts.parentInstanceId,
|
|
2174
|
+
sourceNodeId: parts.sourceNodeId,
|
|
2175
|
+
...componentId ? { sourceComponentId: componentId } : {},
|
|
2176
|
+
...componentName ? { sourceComponentName: componentName } : {}
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
function findNodeById2(node, id) {
|
|
2180
|
+
if (node.id === id) return node;
|
|
2181
|
+
if (node.children) {
|
|
2182
|
+
for (const child of node.children) {
|
|
2183
|
+
const found = findNodeById2(child, id);
|
|
2184
|
+
if (found) return found;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
1834
2189
|
|
|
1835
2190
|
// src/core/ui-constants.ts
|
|
1836
2191
|
var GAUGE_R = 54;
|
|
@@ -2998,7 +3353,14 @@ var EVENTS = {
|
|
|
2998
3353
|
MCP_TOOL_CALLED: `${EVENT_PREFIX}mcp_tool_called`,
|
|
2999
3354
|
// CLI
|
|
3000
3355
|
CLI_COMMAND: `${EVENT_PREFIX}cli_command`,
|
|
3001
|
-
CLI_INIT: `${EVENT_PREFIX}cli_init
|
|
3356
|
+
CLI_INIT: `${EVENT_PREFIX}cli_init`,
|
|
3357
|
+
// Roundtrip (ADR-012)
|
|
3358
|
+
// Wiring point for the roundtrip helper's `telemetry` callback. No Node-side
|
|
3359
|
+
// orchestrator reads this yet — the helper ships in a sandbox-pure IIFE that
|
|
3360
|
+
// cannot import `core/monitoring` directly, so the event fires through a
|
|
3361
|
+
// caller-supplied callback. Define the typed name here so a future consumer
|
|
3362
|
+
// has a single place to wire it up.
|
|
3363
|
+
ROUNDTRIP_DEFINITION_WRITE_SKIPPED: `${EVENT_PREFIX}roundtrip_definition_write_skipped`
|
|
3002
3364
|
};
|
|
3003
3365
|
|
|
3004
3366
|
// src/core/monitoring/capture.ts
|
|
@@ -4113,6 +4475,10 @@ defineRule({
|
|
|
4113
4475
|
});
|
|
4114
4476
|
|
|
4115
4477
|
// src/core/rules/naming/index.ts
|
|
4478
|
+
function capitalize(s) {
|
|
4479
|
+
if (!s) return s;
|
|
4480
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
4481
|
+
}
|
|
4116
4482
|
function detectNamingConvention(name) {
|
|
4117
4483
|
if (/^[a-z]+(-[a-z]+)*$/.test(name)) return "kebab-case";
|
|
4118
4484
|
if (/^[a-z]+(_[a-z]+)*$/.test(name)) return "snake_case";
|
|
@@ -4238,6 +4604,7 @@ var inconsistentNamingConventionCheck = (node, context) => {
|
|
|
4238
4604
|
ruleId: inconsistentNamingConventionDef.id,
|
|
4239
4605
|
nodeId: node.id,
|
|
4240
4606
|
nodePath: context.path.join(" > "),
|
|
4607
|
+
suggestedName: suggested,
|
|
4241
4608
|
...inconsistentNamingMsg(node.name, nodeConvention, dominantConvention, suggested)
|
|
4242
4609
|
};
|
|
4243
4610
|
}
|
|
@@ -4275,6 +4642,7 @@ var nonStandardNamingCheck = (node, context) => {
|
|
|
4275
4642
|
subType: "state-name",
|
|
4276
4643
|
nodeId: node.id,
|
|
4277
4644
|
nodePath: context.path.join(" > "),
|
|
4645
|
+
suggestedName: capitalize(suggestion),
|
|
4278
4646
|
...nonStandardNamingMsg.stateName(node.name, opt, suggestion)
|
|
4279
4647
|
};
|
|
4280
4648
|
}
|
|
@@ -4531,6 +4899,78 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
4531
4899
|
}
|
|
4532
4900
|
}
|
|
4533
4901
|
);
|
|
4902
|
+
server.tool(
|
|
4903
|
+
"gotcha-survey",
|
|
4904
|
+
`Generate a gotcha survey from a Figma design analysis.
|
|
4905
|
+
|
|
4906
|
+
Analyzes the design and returns a GotchaSurvey JSON with designGrade, isReadyForCodeGen, and questions[].
|
|
4907
|
+
When isReadyForCodeGen is true, questions will be empty (no survey needed).
|
|
4908
|
+
|
|
4909
|
+
Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKEN env var or the token parameter for live Figma URLs.`,
|
|
4910
|
+
{
|
|
4911
|
+
input: z.string().describe("Figma URL or local fixture path. Requires FIGMA_TOKEN for live URLs."),
|
|
4912
|
+
token: z.string().optional().describe("Figma API token (falls back to FIGMA_TOKEN env var)"),
|
|
4913
|
+
preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional().describe("Analysis preset"),
|
|
4914
|
+
targetNodeId: z.string().optional().describe("Scope analysis to a specific node ID"),
|
|
4915
|
+
configPath: z.string().optional().describe("Path to config JSON file for rule overrides")
|
|
4916
|
+
},
|
|
4917
|
+
{
|
|
4918
|
+
readOnlyHint: false,
|
|
4919
|
+
destructiveHint: false,
|
|
4920
|
+
openWorldHint: true,
|
|
4921
|
+
title: "Gotcha Survey"
|
|
4922
|
+
},
|
|
4923
|
+
async ({ input, token, preset, targetNodeId, configPath }) => {
|
|
4924
|
+
trackEvent(EVENTS.MCP_TOOL_CALLED, { tool: "gotcha-survey" });
|
|
4925
|
+
try {
|
|
4926
|
+
const { file, nodeId } = await loadFile(input, token);
|
|
4927
|
+
const effectiveNodeId = targetNodeId ?? nodeId;
|
|
4928
|
+
let configs = preset ? { ...getConfigsWithPreset(preset) } : { ...RULE_CONFIGS };
|
|
4929
|
+
if (configPath) {
|
|
4930
|
+
const configFile = await loadConfigFile(configPath);
|
|
4931
|
+
configs = mergeConfigs(configs, configFile);
|
|
4932
|
+
}
|
|
4933
|
+
const result = analyzeFile(file, {
|
|
4934
|
+
configs,
|
|
4935
|
+
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {}
|
|
4936
|
+
});
|
|
4937
|
+
const scores = calculateScores(result, configs);
|
|
4938
|
+
const survey = generateGotchaSurvey(result, scores);
|
|
4939
|
+
trackEvent(EVENTS.ANALYSIS_COMPLETED, {
|
|
4940
|
+
nodeCount: result.nodeCount,
|
|
4941
|
+
issueCount: result.issues.length,
|
|
4942
|
+
grade: scores.overall.grade,
|
|
4943
|
+
percentage: scores.overall.percentage,
|
|
4944
|
+
source: isJsonFile(input) || isFixtureDir(input) ? "fixture" : "figma"
|
|
4945
|
+
});
|
|
4946
|
+
return {
|
|
4947
|
+
content: [
|
|
4948
|
+
{
|
|
4949
|
+
type: "text",
|
|
4950
|
+
text: JSON.stringify(survey, null, 2)
|
|
4951
|
+
}
|
|
4952
|
+
]
|
|
4953
|
+
};
|
|
4954
|
+
} catch (error) {
|
|
4955
|
+
trackError(
|
|
4956
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
4957
|
+
{ tool: "gotcha-survey" }
|
|
4958
|
+
);
|
|
4959
|
+
trackEvent(EVENTS.ANALYSIS_FAILED, {
|
|
4960
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4961
|
+
});
|
|
4962
|
+
return {
|
|
4963
|
+
content: [
|
|
4964
|
+
{
|
|
4965
|
+
type: "text",
|
|
4966
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
4967
|
+
}
|
|
4968
|
+
],
|
|
4969
|
+
isError: true
|
|
4970
|
+
};
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
);
|
|
4534
4974
|
server.tool(
|
|
4535
4975
|
"list-rules",
|
|
4536
4976
|
"List all available analysis rules with their current configuration",
|
|
@@ -4584,6 +5024,7 @@ server.tool(
|
|
|
4584
5024
|
|
|
4585
5025
|
Available topics:
|
|
4586
5026
|
- setup: Installation and token configuration
|
|
5027
|
+
- scoring: Scoring model formula (density+diversity, severity weights, grades)
|
|
4587
5028
|
- rules: All rule IDs with default scores and severity
|
|
4588
5029
|
- config: Config overrides (scores, severity, node exclusions, thresholds)
|
|
4589
5030
|
- visual-compare: Pixel-level comparison between Figma and AI-generated code
|
|
@@ -4592,7 +5033,7 @@ Available topics:
|
|
|
4592
5033
|
|
|
4593
5034
|
Use this when the user asks about how to use canicode, configuration, rules, visual comparison, or any feature.`,
|
|
4594
5035
|
{
|
|
4595
|
-
topic: z.enum(["all", "setup", "rules", "config", "visual-compare", "design-tree"]).optional().describe("Topic to retrieve. Default: all")
|
|
5036
|
+
topic: z.enum(["all", "setup", "scoring", "rules", "config", "visual-compare", "design-tree"]).optional().describe("Topic to retrieve. Default: all")
|
|
4596
5037
|
},
|
|
4597
5038
|
{
|
|
4598
5039
|
readOnlyHint: true,
|
|
@@ -4618,7 +5059,18 @@ Get your token: Figma \u2192 Settings \u2192 Security \u2192 Personal access tok
|
|
|
4618
5059
|
claude mcp add canicode -e FIGMA_TOKEN=figd_xxxxxxxxxxxxx -- npx -y -p canicode canicode-mcp
|
|
4619
5060
|
\`\`\`
|
|
4620
5061
|
|
|
4621
|
-
Requires FIGMA_TOKEN for live Figma URL analysis
|
|
5062
|
+
Requires FIGMA_TOKEN for live Figma URL analysis.
|
|
5063
|
+
|
|
5064
|
+
## CLI only (no MCP server)
|
|
5065
|
+
|
|
5066
|
+
User-facing MCP tools have CLI equivalents with the same JSON shape, so skills and scripts can run without installing the canicode MCP server:
|
|
5067
|
+
|
|
5068
|
+
\`\`\`bash
|
|
5069
|
+
npx canicode analyze <input>
|
|
5070
|
+
npx canicode gotcha-survey <input> --json
|
|
5071
|
+
\`\`\`
|
|
5072
|
+
|
|
5073
|
+
The CLI spawns per call (slower than the long-running MCP server) but needs no extra setup beyond \`FIGMA_TOKEN\`.`,
|
|
4622
5074
|
"visual-compare": `# Visual Compare
|
|
4623
5075
|
|
|
4624
5076
|
Pixel-level comparison between Figma design and AI-generated code.
|
|
@@ -4664,6 +5116,36 @@ Viewport and device pixel ratio are auto-inferred from the Figma PNG dimensions
|
|
|
4664
5116
|
## Requirements
|
|
4665
5117
|
- npx playwright install chromium
|
|
4666
5118
|
- Figma API token with read access`,
|
|
5119
|
+
"scoring": `# Scoring Model
|
|
5120
|
+
|
|
5121
|
+
Score = density (70%) + diversity (30%), averaged across 6 categories.
|
|
5122
|
+
|
|
5123
|
+
## Severity weights
|
|
5124
|
+
|
|
5125
|
+
| Severity | Weight |
|
|
5126
|
+
|----------|--------|
|
|
5127
|
+
| blocking | 3.0x |
|
|
5128
|
+
| risk | 2.0x |
|
|
5129
|
+
| missing-info | 1.0x |
|
|
5130
|
+
| suggestion | 0.5x |
|
|
5131
|
+
|
|
5132
|
+
## Grades
|
|
5133
|
+
|
|
5134
|
+
| Grade | Min score |
|
|
5135
|
+
|-------|-----------|
|
|
5136
|
+
| S | 95 |
|
|
5137
|
+
| A+ | 90 |
|
|
5138
|
+
| A | 85 |
|
|
5139
|
+
| B+ | 80 |
|
|
5140
|
+
| B | 75 |
|
|
5141
|
+
| C+ | 70 |
|
|
5142
|
+
| C | 65 |
|
|
5143
|
+
| D | 50 |
|
|
5144
|
+
| F | <50 |
|
|
5145
|
+
|
|
5146
|
+
Floor: 5% minimum.
|
|
5147
|
+
|
|
5148
|
+
Full documentation: https://github.com/let-sunny/canicode/wiki/Scoring-Model`,
|
|
4667
5149
|
"design-tree": `# Design Tree
|
|
4668
5150
|
|
|
4669
5151
|
Generate a DOM-like design tree from a Figma fixture. Converts the node tree to a concise text format with inline CSS styles \u2014 50-100x smaller than raw JSON.
|