canicode 0.11.1 → 0.11.3
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 +2 -2
- package/dist/cli/index.js +188 -191
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +40 -9
- package/dist/index.js +33 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +84 -20
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +40 -2
- package/package.json +3 -2
- package/skills/canicode-gotchas/SKILL.md +74 -12
- package/skills/canicode-roundtrip/SKILL.md +69 -10
- package/skills/canicode-roundtrip/canicode-roundtrip-helpers.d.ts +54 -0
- package/skills/canicode-roundtrip/helpers-bootstrap.js +21 -0
- package/skills/canicode-roundtrip/helpers-installer.js +14 -0
- package/skills/cursor/canicode-gotchas/SKILL.md +74 -12
- package/skills/cursor/canicode-roundtrip/SKILL.md +69 -10
- package/skills/cursor/canicode-roundtrip/canicode-roundtrip-helpers.d.ts +54 -0
- package/skills/cursor/canicode-roundtrip/helpers-bootstrap.js +21 -0
- package/skills/cursor/canicode-roundtrip/helpers-installer.js +14 -0
package/dist/mcp/server.js
CHANGED
|
@@ -1863,9 +1863,11 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
1863
1863
|
}
|
|
1864
1864
|
|
|
1865
1865
|
// package.json
|
|
1866
|
-
var version = "0.11.
|
|
1866
|
+
var version = "0.11.3";
|
|
1867
1867
|
|
|
1868
1868
|
// src/core/engine/scoring.ts
|
|
1869
|
+
var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
|
|
1870
|
+
var DEFAULT_CODEGEN_READY_MIN_GRADE = "A";
|
|
1869
1871
|
function computeTotalScorePerCategory(configs) {
|
|
1870
1872
|
const totals = Object.fromEntries(
|
|
1871
1873
|
CATEGORIES.map((c) => [c, 0])
|
|
@@ -1892,8 +1894,9 @@ function calculateGrade(percentage) {
|
|
|
1892
1894
|
if (percentage >= 50) return "D";
|
|
1893
1895
|
return "F";
|
|
1894
1896
|
}
|
|
1895
|
-
function isReadyForCodeGen(grade) {
|
|
1896
|
-
|
|
1897
|
+
function isReadyForCodeGen(grade, minGrade) {
|
|
1898
|
+
const threshold = minGrade ?? DEFAULT_CODEGEN_READY_MIN_GRADE;
|
|
1899
|
+
return GRADE_ORDER.indexOf(grade) <= GRADE_ORDER.indexOf(threshold);
|
|
1897
1900
|
}
|
|
1898
1901
|
function clamp(value, min, max) {
|
|
1899
1902
|
return Math.max(min, Math.min(max, value));
|
|
@@ -2086,7 +2089,7 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
2086
2089
|
scope: result.scope,
|
|
2087
2090
|
issueCount: result.issues.length,
|
|
2088
2091
|
acknowledgedCount: scores.summary.acknowledgedCount,
|
|
2089
|
-
isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade),
|
|
2092
|
+
isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade, options?.codegenReadyMinGrade),
|
|
2090
2093
|
blockingIssueCount: scores.summary.blocking,
|
|
2091
2094
|
scores: {
|
|
2092
2095
|
overall: scores.overall,
|
|
@@ -2233,7 +2236,13 @@ var BATCHABLE_RULE_IDS = [
|
|
|
2233
2236
|
"no-auto-layout",
|
|
2234
2237
|
"fixed-size-in-auto-layout"
|
|
2235
2238
|
];
|
|
2239
|
+
var OPT_IN_BATCHABLE_RULE_IDS = [
|
|
2240
|
+
"missing-prototype"
|
|
2241
|
+
];
|
|
2236
2242
|
var BATCHABLE_SET = new Set(BATCHABLE_RULE_IDS);
|
|
2243
|
+
var OPT_IN_BATCHABLE_SET = new Set(
|
|
2244
|
+
OPT_IN_BATCHABLE_RULE_IDS
|
|
2245
|
+
);
|
|
2237
2246
|
var NO_SOURCE_SENTINEL = "_no-source";
|
|
2238
2247
|
function groupAndBatchSurveyQuestions(questions) {
|
|
2239
2248
|
if (questions.length === 0) {
|
|
@@ -2274,20 +2283,25 @@ function sourceComponentKey(question) {
|
|
|
2274
2283
|
}
|
|
2275
2284
|
function pushIntoBatch(group, question) {
|
|
2276
2285
|
const sceneWeight = Math.max(question.replicas ?? 1, 1);
|
|
2277
|
-
const
|
|
2286
|
+
const batchMode = resolveBatchMode(question.ruleId);
|
|
2278
2287
|
const last = group.batches.at(-1);
|
|
2279
|
-
if (last !== void 0 && last.ruleId === question.ruleId &&
|
|
2288
|
+
if (last !== void 0 && last.ruleId === question.ruleId && batchMode !== "none" && last.batchMode === batchMode) {
|
|
2280
2289
|
last.questions.push(question);
|
|
2281
2290
|
last.totalScenes += sceneWeight;
|
|
2282
2291
|
return;
|
|
2283
2292
|
}
|
|
2284
2293
|
group.batches.push({
|
|
2285
2294
|
ruleId: question.ruleId,
|
|
2286
|
-
|
|
2295
|
+
batchMode,
|
|
2287
2296
|
questions: [question],
|
|
2288
2297
|
totalScenes: sceneWeight
|
|
2289
2298
|
});
|
|
2290
2299
|
}
|
|
2300
|
+
function resolveBatchMode(ruleId) {
|
|
2301
|
+
if (BATCHABLE_SET.has(ruleId)) return "safe";
|
|
2302
|
+
if (OPT_IN_BATCHABLE_SET.has(ruleId)) return "opt-in";
|
|
2303
|
+
return "none";
|
|
2304
|
+
}
|
|
2291
2305
|
|
|
2292
2306
|
// src/core/gotcha/survey-generator.ts
|
|
2293
2307
|
var NODE_PATH_SEPARATOR = " > ";
|
|
@@ -2306,12 +2320,18 @@ function generateGotchaSurvey(result, scores, options = {}) {
|
|
|
2306
2320
|
const mapped = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
|
|
2307
2321
|
const questions = deduplicateBySourceComponent(mapped);
|
|
2308
2322
|
const groupedQuestions = groupAndBatchSurveyQuestions(questions);
|
|
2323
|
+
const PROPAGATION_CANDIDATE_THRESHOLD = 3;
|
|
2324
|
+
const propagationCandidates = questions.filter(
|
|
2325
|
+
(q) => q.isInstanceChild
|
|
2326
|
+
).length;
|
|
2327
|
+
const suggestedDefaultApply = propagationCandidates >= PROPAGATION_CANDIDATE_THRESHOLD;
|
|
2309
2328
|
return {
|
|
2310
2329
|
designGrade: grade,
|
|
2311
|
-
isReadyForCodeGen: isReadyForCodeGen(grade),
|
|
2330
|
+
isReadyForCodeGen: isReadyForCodeGen(grade, options.codegenReadyMinGrade),
|
|
2312
2331
|
questions,
|
|
2313
2332
|
groupedQuestions,
|
|
2314
|
-
designKey: options.designKey ?? ""
|
|
2333
|
+
designKey: options.designKey ?? "",
|
|
2334
|
+
suggestedDefaultApply
|
|
2315
2335
|
};
|
|
2316
2336
|
}
|
|
2317
2337
|
function deduplicateSiblingIssues(issues) {
|
|
@@ -3563,6 +3583,7 @@ var ConfigFileSchema = z.object({
|
|
|
3563
3583
|
excludeNodeTypes: z.array(z.string()).optional(),
|
|
3564
3584
|
excludeNodeNames: z.array(z.string()).optional(),
|
|
3565
3585
|
gridBase: z.number().int().positive().optional(),
|
|
3586
|
+
codegenReadyMinGrade: z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]).optional(),
|
|
3566
3587
|
rules: z.record(z.string(), RuleOverrideSchema).superRefine((rules, ctx) => {
|
|
3567
3588
|
const unknown = Object.keys(rules).filter((id) => !VALID_RULE_IDS.has(id));
|
|
3568
3589
|
if (unknown.length > 0) {
|
|
@@ -5161,10 +5182,24 @@ defineRule({
|
|
|
5161
5182
|
config({ quiet: true });
|
|
5162
5183
|
var require2 = createRequire(import.meta.url);
|
|
5163
5184
|
var pkg = require2("../../package.json");
|
|
5164
|
-
var
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5185
|
+
var SERVER_INSTRUCTIONS = `canicode has three channels. The MCP tools below cover analysis only \u2014 design write-back is a separate channel.
|
|
5186
|
+
|
|
5187
|
+
- **MCP tools** (this server): \`analyze\`, \`gotcha-survey\`, \`list-rules\`, \`docs\`, \`version\`, \`visual-compare\`. Use these for read-only analysis and reports.
|
|
5188
|
+
- **Skills** (slash commands, NOT MCP tools): \`/canicode\`, \`/canicode-gotchas\`, \`/canicode-roundtrip\`. Install with \`npx canicode init\` (Claude Code) or \`npx canicode init --cursor-skills\` (Cursor). The roundtrip skill is the only path that writes back to the Figma file via Plugin API.
|
|
5189
|
+
- **CLI**: \`npx canicode <command>\` \u2014 same JSON shape as MCP tools, useful for CI / scripting.
|
|
5190
|
+
|
|
5191
|
+
If a user asks to "run the roundtrip" or "fix gotchas in Figma" and you don't see \`canicode-roundtrip\` as a slash command, the skill isn't installed yet \u2014 tell them to run \`npx canicode init\`. Don't try to compose roundtrip out of MCP tools; the write-back logic lives in the skill.
|
|
5192
|
+
|
|
5193
|
+
Call \`docs\` with topic \`channels\` for the full matrix.`;
|
|
5194
|
+
var server = new McpServer(
|
|
5195
|
+
{
|
|
5196
|
+
name: "canicode",
|
|
5197
|
+
version: pkg.version
|
|
5198
|
+
},
|
|
5199
|
+
{
|
|
5200
|
+
instructions: SERVER_INSTRUCTIONS
|
|
5201
|
+
}
|
|
5202
|
+
);
|
|
5168
5203
|
server.tool(
|
|
5169
5204
|
"analyze",
|
|
5170
5205
|
`Analyze a Figma design for development-friendliness and AI-friendliness.
|
|
@@ -5178,7 +5213,8 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
5178
5213
|
configPath: z.string().optional().describe("Path to config JSON file for rule overrides"),
|
|
5179
5214
|
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."),
|
|
5180
5215
|
acknowledgments: z.array(AcknowledgmentSchema).optional().describe("(#371 / ADR-019) Pre-resolved acknowledgments from canicode-authored Figma annotations (e.g. via readCanicodeAcknowledgments in a use_figma batch). Each entry includes nodeId and ruleId; newer annotations may also carry intent, sceneWriteOutcome, and codegenDirective from a canicode-json fenced block (#444). Matching issues are flagged acknowledged and contribute half weight to the density score."),
|
|
5181
|
-
scope: z.enum(["page", "component"]).optional().describe("(#404) Override analysis scope \u2014 `page` (screen/section where container bounds are required) or `component` (standalone reusable unit where root FILL is the design contract). Defaults to auto-detection from the root node type: `COMPONENT` / `COMPONENT_SET` / `INSTANCE` roots resolve to `component`, everything else to `page`.")
|
|
5216
|
+
scope: z.enum(["page", "component"]).optional().describe("(#404) Override analysis scope \u2014 `page` (screen/section where container bounds are required) or `component` (standalone reusable unit where root FILL is the design contract). Defaults to auto-detection from the root node type: `COMPONENT` / `COMPONENT_SET` / `INSTANCE` roots resolve to `component`, everything else to `page`."),
|
|
5217
|
+
codegenReadyMinGrade: z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]).optional().describe("Minimum grade for code-gen readiness. Overrides the codegenReadyMinGrade field in configPath. Default: A")
|
|
5182
5218
|
},
|
|
5183
5219
|
{
|
|
5184
5220
|
readOnlyHint: false,
|
|
@@ -5186,16 +5222,19 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
5186
5222
|
openWorldHint: true,
|
|
5187
5223
|
title: "Analyze Figma Design"
|
|
5188
5224
|
},
|
|
5189
|
-
async ({ input, token, preset, targetNodeId, configPath, openReport, acknowledgments, scope }) => {
|
|
5225
|
+
async ({ input, token, preset, targetNodeId, configPath, openReport, acknowledgments, scope, codegenReadyMinGrade }) => {
|
|
5190
5226
|
trackEvent(EVENTS.MCP_TOOL_CALLED, { tool: "analyze" });
|
|
5191
5227
|
try {
|
|
5192
5228
|
const { file, nodeId } = await loadFile(input, token);
|
|
5193
5229
|
const effectiveNodeId = targetNodeId ?? nodeId;
|
|
5194
5230
|
let configs = preset ? { ...getConfigsWithPreset(preset) } : { ...RULE_CONFIGS };
|
|
5231
|
+
let configFileMinGrade;
|
|
5195
5232
|
if (configPath) {
|
|
5196
5233
|
const configFile = await loadConfigFile(configPath);
|
|
5197
5234
|
configs = mergeConfigs(configs, configFile);
|
|
5235
|
+
configFileMinGrade = configFile.codegenReadyMinGrade;
|
|
5198
5236
|
}
|
|
5237
|
+
const effectiveMinGrade = codegenReadyMinGrade ?? configFileMinGrade;
|
|
5199
5238
|
const result = analyzeFile(file, {
|
|
5200
5239
|
configs,
|
|
5201
5240
|
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {},
|
|
@@ -5230,7 +5269,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
5230
5269
|
content: [
|
|
5231
5270
|
{
|
|
5232
5271
|
type: "text",
|
|
5233
|
-
text: JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input) }), null, 2)
|
|
5272
|
+
text: JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input), ...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {} }), null, 2)
|
|
5234
5273
|
}
|
|
5235
5274
|
]
|
|
5236
5275
|
};
|
|
@@ -5268,7 +5307,8 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
5268
5307
|
preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional().describe("Analysis preset"),
|
|
5269
5308
|
targetNodeId: z.string().optional().describe("Scope analysis to a specific node ID"),
|
|
5270
5309
|
configPath: z.string().optional().describe("Path to config JSON file for rule overrides"),
|
|
5271
|
-
scope: z.enum(["page", "component"]).optional().describe("(#404) Override analysis scope \u2014 `page` or `component`. Defaults to auto-detection from the root node type.")
|
|
5310
|
+
scope: z.enum(["page", "component"]).optional().describe("(#404) Override analysis scope \u2014 `page` or `component`. Defaults to auto-detection from the root node type."),
|
|
5311
|
+
codegenReadyMinGrade: z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]).optional().describe("Minimum grade for code-gen readiness. Overrides the codegenReadyMinGrade field in configPath. Default: A")
|
|
5272
5312
|
},
|
|
5273
5313
|
{
|
|
5274
5314
|
readOnlyHint: false,
|
|
@@ -5276,23 +5316,29 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
|
|
|
5276
5316
|
openWorldHint: true,
|
|
5277
5317
|
title: "Gotcha Survey"
|
|
5278
5318
|
},
|
|
5279
|
-
async ({ input, token, preset, targetNodeId, configPath, scope }) => {
|
|
5319
|
+
async ({ input, token, preset, targetNodeId, configPath, scope, codegenReadyMinGrade }) => {
|
|
5280
5320
|
trackEvent(EVENTS.MCP_TOOL_CALLED, { tool: "gotcha-survey" });
|
|
5281
5321
|
try {
|
|
5282
5322
|
const { file, nodeId } = await loadFile(input, token);
|
|
5283
5323
|
const effectiveNodeId = targetNodeId ?? nodeId;
|
|
5284
5324
|
let configs = preset ? { ...getConfigsWithPreset(preset) } : { ...RULE_CONFIGS };
|
|
5325
|
+
let configFileMinGrade;
|
|
5285
5326
|
if (configPath) {
|
|
5286
5327
|
const configFile = await loadConfigFile(configPath);
|
|
5287
5328
|
configs = mergeConfigs(configs, configFile);
|
|
5329
|
+
configFileMinGrade = configFile.codegenReadyMinGrade;
|
|
5288
5330
|
}
|
|
5331
|
+
const effectiveMinGrade = codegenReadyMinGrade ?? configFileMinGrade;
|
|
5289
5332
|
const result = analyzeFile(file, {
|
|
5290
5333
|
configs,
|
|
5291
5334
|
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {},
|
|
5292
5335
|
...scope ? { scope } : {}
|
|
5293
5336
|
});
|
|
5294
5337
|
const scores = calculateScores(result, configs);
|
|
5295
|
-
const survey = generateGotchaSurvey(result, scores, {
|
|
5338
|
+
const survey = generateGotchaSurvey(result, scores, {
|
|
5339
|
+
designKey: computeDesignKey(input),
|
|
5340
|
+
...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {}
|
|
5341
|
+
});
|
|
5296
5342
|
trackEvent(EVENTS.ANALYSIS_COMPLETED, {
|
|
5297
5343
|
nodeCount: result.nodeCount,
|
|
5298
5344
|
issueCount: result.issues.length,
|
|
@@ -5381,6 +5427,7 @@ server.tool(
|
|
|
5381
5427
|
|
|
5382
5428
|
Available topics:
|
|
5383
5429
|
- setup: Installation and token configuration
|
|
5430
|
+
- channels: Which canicode features are MCP tools vs skills vs CLI \u2014 call this when a user asks for a feature you can't find as an MCP tool
|
|
5384
5431
|
- scoring: Scoring model formula (density+diversity, severity weights, grades)
|
|
5385
5432
|
- rules: All rule IDs with default scores and severity
|
|
5386
5433
|
- config: Config overrides (scores, severity, node exclusions, thresholds)
|
|
@@ -5390,7 +5437,7 @@ Available topics:
|
|
|
5390
5437
|
|
|
5391
5438
|
Use this when the user asks about how to use canicode, configuration, rules, visual comparison, or any feature.`,
|
|
5392
5439
|
{
|
|
5393
|
-
topic: z.enum(["all", "setup", "scoring", "rules", "config", "visual-compare", "design-tree"]).optional().describe("Topic to retrieve. Default: all")
|
|
5440
|
+
topic: z.enum(["all", "setup", "channels", "scoring", "rules", "config", "visual-compare", "design-tree"]).optional().describe("Topic to retrieve. Default: all")
|
|
5394
5441
|
},
|
|
5395
5442
|
{
|
|
5396
5443
|
readOnlyHint: true,
|
|
@@ -5401,6 +5448,23 @@ Use this when the user asks about how to use canicode, configuration, rules, vis
|
|
|
5401
5448
|
async ({ topic }) => {
|
|
5402
5449
|
const selectedTopic = topic ?? "all";
|
|
5403
5450
|
const inlineTopics = {
|
|
5451
|
+
"channels": `# Channels \u2014 Where Each Feature Lives
|
|
5452
|
+
|
|
5453
|
+
canicode is delivered through three channels. Knowing which is which prevents the common confusion where an agent searches the MCP tool list for a feature that actually lives in a skill.
|
|
5454
|
+
|
|
5455
|
+
| Feature | Channel | How to invoke |
|
|
5456
|
+
|---------|---------|---------------|
|
|
5457
|
+
| Analyze a Figma design | MCP tool / CLI | \`analyze\` (this server) or \`npx canicode analyze <url>\` |
|
|
5458
|
+
| Generate gotcha questions | MCP tool / CLI | \`gotcha-survey\` or \`npx canicode gotcha-survey <url> --json\` |
|
|
5459
|
+
| List rules / get docs / version | MCP tool | \`list-rules\`, \`docs\`, \`version\` |
|
|
5460
|
+
| Pixel-compare Figma vs HTML | MCP tool / CLI | \`visual-compare\` |
|
|
5461
|
+
| Q&A workflow that saves answers locally | **Skill** (slash command) | \`/canicode-gotchas <url>\` \u2014 install with \`npx canicode init\` |
|
|
5462
|
+
| Roundtrip: analyze \u2192 gotcha \u2192 write back to Figma \u2192 re-analyze | **Skill** (slash command) | \`/canicode-roundtrip <url>\` \u2014 install with \`npx canicode init\` |
|
|
5463
|
+
| Lightweight analyze-only skill (no MCP install) | **Skill** (slash command) | \`/canicode <url>\` \u2014 install with \`npx canicode init\` |
|
|
5464
|
+
|
|
5465
|
+
**Skills are NOT MCP tools.** They are slash commands installed under \`.claude/skills/\` (Claude Code) or \`.cursor/skills/\` (Cursor). If \`canicode-roundtrip\` is not in the slash-command list, the skill isn't installed \u2014 run \`npx canicode init\` (or \`--cursor-skills\` for Cursor).
|
|
5466
|
+
|
|
5467
|
+
The roundtrip skill is the **only** path that writes back to the Figma file (via the Figma MCP server's \`use_figma\`). Don't try to compose roundtrip out of \`analyze\` + \`gotcha-survey\` MCP calls \u2014 the write-back orchestration lives inside the skill.`,
|
|
5404
5468
|
"setup": `# Setup
|
|
5405
5469
|
|
|
5406
5470
|
## CLI
|