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.
@@ -1863,9 +1863,11 @@ function computeApplyContext(violation, instanceContext) {
1863
1863
  }
1864
1864
 
1865
1865
  // package.json
1866
- var version = "0.11.1";
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
- return grade === "S" || grade === "A+" || grade === "A";
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 isBatchable = BATCHABLE_SET.has(question.ruleId);
2286
+ const batchMode = resolveBatchMode(question.ruleId);
2278
2287
  const last = group.batches.at(-1);
2279
- if (last !== void 0 && last.ruleId === question.ruleId && isBatchable && last.batchable) {
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
- batchable: isBatchable,
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 server = new McpServer({
5165
- name: "canicode",
5166
- version: pkg.version
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, { designKey: computeDesignKey(input) });
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