canicode 0.12.1 → 0.12.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/dist/cli/index.js +155 -44
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +109 -40
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +113 -41
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/canicode-roundtrip/SKILL.md +102 -0
- package/skills/canicode-roundtrip/helpers-bootstrap.js +1 -1
- package/skills/canicode-roundtrip/helpers-installer.js +2 -2
- package/skills/canicode-roundtrip/helpers.js +429 -0
- package/skills/cursor/canicode-roundtrip/SKILL.md +102 -0
- package/skills/cursor/canicode-roundtrip/helpers-bootstrap.js +1 -1
- package/skills/cursor/canicode-roundtrip/helpers-installer.js +2 -2
- package/skills/cursor/canicode-roundtrip/helpers.js +429 -0
package/dist/cli/index.js
CHANGED
|
@@ -1408,7 +1408,24 @@ var EVENTS = {
|
|
|
1408
1408
|
// has a single place to wire it up.
|
|
1409
1409
|
ROUNDTRIP_DEFINITION_WRITE_SKIPPED: `${EVENT_PREFIX}roundtrip_definition_write_skipped`,
|
|
1410
1410
|
/** CLI `canicode roundtrip-tally` completed successfully. */
|
|
1411
|
-
ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally
|
|
1411
|
+
ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`,
|
|
1412
|
+
/**
|
|
1413
|
+
* Phase 3 (#508 / ADR-023): `applyComponentize` outcome — either a
|
|
1414
|
+
* successful `createComponentFromNode` call or one of the guard rejections
|
|
1415
|
+
* (instance-child, free-form parent, error) that route to the Strategy C
|
|
1416
|
+
* annotate-fallback. Surfaces under `props.outcome` so a Node-side reader
|
|
1417
|
+
* can split the funnel.
|
|
1418
|
+
*/
|
|
1419
|
+
ROUNDTRIP_COMPONENTIZE: `${EVENT_PREFIX}roundtrip_componentize`,
|
|
1420
|
+
/**
|
|
1421
|
+
* Phase 3 (#508 / ADR-023, #554): `applyReplaceWithInstance` outcome —
|
|
1422
|
+
* either a successful instance swap (`outcome: "replaced"`) or one of the
|
|
1423
|
+
* guard rejections (`"skipped-free-form-parent"`, `"skipped-prereq-missing"`,
|
|
1424
|
+
* `"error"`). The primitive pairs with `ROUNDTRIP_COMPONENTIZE` so a
|
|
1425
|
+
* Node-side reader can correlate componentize+swap pairs across a single
|
|
1426
|
+
* Phase 3 batch.
|
|
1427
|
+
*/
|
|
1428
|
+
ROUNDTRIP_REPLACE_WITH_INSTANCE: `${EVENT_PREFIX}roundtrip_replace_with_instance`
|
|
1412
1429
|
};
|
|
1413
1430
|
|
|
1414
1431
|
// src/core/monitoring/capture.ts
|
|
@@ -2515,8 +2532,8 @@ var missingComponentMsg = {
|
|
|
2515
2532
|
message: `"${name}" appears ${count} times`,
|
|
2516
2533
|
suggestion: `Extract as a reusable component`
|
|
2517
2534
|
}),
|
|
2518
|
-
structureRepetition: (name,
|
|
2519
|
-
message: `"${name}" and ${
|
|
2535
|
+
structureRepetition: (name, matchCount) => ({
|
|
2536
|
+
message: `"${name}" and ${matchCount} other frame(s) share the same internal structure`,
|
|
2520
2537
|
suggestion: `Extract a shared component from the repeated structure`
|
|
2521
2538
|
}),
|
|
2522
2539
|
styleOverride: (componentName, overrides) => ({
|
|
@@ -3297,6 +3314,41 @@ function getSeenStage1(context) {
|
|
|
3297
3314
|
function getSeenStage4(context) {
|
|
3298
3315
|
return getAnalysisState(context, SEEN_STAGE4_KEY, () => /* @__PURE__ */ new Set());
|
|
3299
3316
|
}
|
|
3317
|
+
function stage3GroupsKey(maxDepth) {
|
|
3318
|
+
return `missing-component:stage3Groups:depth=${maxDepth}`;
|
|
3319
|
+
}
|
|
3320
|
+
function nodeQualifiesForStage3(node, parent, insideInstance) {
|
|
3321
|
+
if (insideInstance) return false;
|
|
3322
|
+
if (node.type !== "FRAME") return false;
|
|
3323
|
+
if (parent?.type === "COMPONENT_SET") return false;
|
|
3324
|
+
if (!node.children || node.children.length === 0) return false;
|
|
3325
|
+
return true;
|
|
3326
|
+
}
|
|
3327
|
+
function buildStage3Groups(root, maxFingerprintDepth) {
|
|
3328
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3329
|
+
const walk = (node, parent, ancestorIsInstance) => {
|
|
3330
|
+
const insideInstance = ancestorIsInstance || node.type === "INSTANCE";
|
|
3331
|
+
if (nodeQualifiesForStage3(node, parent, ancestorIsInstance)) {
|
|
3332
|
+
const fp = buildFingerprint(node, maxFingerprintDepth);
|
|
3333
|
+
const existing = groups.get(fp);
|
|
3334
|
+
if (existing) existing.memberIds.push(node.id);
|
|
3335
|
+
else groups.set(fp, { memberIds: [node.id] });
|
|
3336
|
+
}
|
|
3337
|
+
if (node.children) {
|
|
3338
|
+
for (const child of node.children) walk(child, node, insideInstance);
|
|
3339
|
+
}
|
|
3340
|
+
};
|
|
3341
|
+
walk(root, null, false);
|
|
3342
|
+
return groups;
|
|
3343
|
+
}
|
|
3344
|
+
function getStage3Groups(context, maxFingerprintDepth) {
|
|
3345
|
+
const root = context.analysisRoot ?? context.file.document;
|
|
3346
|
+
return getAnalysisState(
|
|
3347
|
+
context,
|
|
3348
|
+
stage3GroupsKey(maxFingerprintDepth),
|
|
3349
|
+
() => buildStage3Groups(root, maxFingerprintDepth)
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3300
3352
|
var missingComponentDef = {
|
|
3301
3353
|
id: "missing-component",
|
|
3302
3354
|
name: "Missing Component",
|
|
@@ -3311,7 +3363,8 @@ var missingComponentCheck = (node, context, options) => {
|
|
|
3311
3363
|
const matchingComponent = Object.values(components).find(
|
|
3312
3364
|
(c) => c.name.toLowerCase() === node.name.toLowerCase()
|
|
3313
3365
|
);
|
|
3314
|
-
const
|
|
3366
|
+
const scopeRoot = context.analysisRoot ?? context.file.document;
|
|
3367
|
+
const frameNames = collectFrameNames(scopeRoot);
|
|
3315
3368
|
const sameNameFrames = frameNames.get(node.name);
|
|
3316
3369
|
const firstFrame = sameNameFrames?.[0];
|
|
3317
3370
|
if (matchingComponent) {
|
|
@@ -3339,37 +3392,32 @@ var missingComponentCheck = (node, context, options) => {
|
|
|
3339
3392
|
};
|
|
3340
3393
|
}
|
|
3341
3394
|
}
|
|
3342
|
-
if (isInsideInstance(context))
|
|
3343
|
-
|
|
3344
|
-
|
|
3395
|
+
if (!nodeQualifiesForStage3(node, context.parent ?? null, isInsideInstance(context))) {
|
|
3396
|
+
return null;
|
|
3397
|
+
}
|
|
3345
3398
|
const structureMinRepetitions = options?.["structureMinRepetitions"] ?? getRuleOption("missing-component", "structureMinRepetitions", 2);
|
|
3346
3399
|
const maxFingerprintDepth = options?.["maxFingerprintDepth"] ?? getRuleOption("missing-component", "maxFingerprintDepth", 3);
|
|
3400
|
+
const groups = getStage3Groups(context, maxFingerprintDepth);
|
|
3347
3401
|
const fingerprint = buildFingerprint(node, maxFingerprintDepth);
|
|
3348
|
-
const
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
);
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
)
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
nodePath: context.path.join(" > "),
|
|
3368
|
-
...missingComponentMsg.structureRepetition(node.name, count - 1)
|
|
3369
|
-
};
|
|
3370
|
-
}
|
|
3371
|
-
}
|
|
3372
|
-
return null;
|
|
3402
|
+
const group = groups.get(fingerprint);
|
|
3403
|
+
if (!group) return null;
|
|
3404
|
+
if (group.memberIds.length < structureMinRepetitions) return null;
|
|
3405
|
+
if (group.memberIds[0] !== node.id) return null;
|
|
3406
|
+
return {
|
|
3407
|
+
ruleId: missingComponentDef.id,
|
|
3408
|
+
subType: "structure-repetition",
|
|
3409
|
+
nodeId: node.id,
|
|
3410
|
+
nodePath: context.path.join(" > "),
|
|
3411
|
+
...missingComponentMsg.structureRepetition(
|
|
3412
|
+
node.name,
|
|
3413
|
+
group.memberIds.length - 1
|
|
3414
|
+
),
|
|
3415
|
+
// #560 / delta 4a: surface the full group so the apply step can drive
|
|
3416
|
+
// the componentize+swap loop from a single user answer. `nodeId` is
|
|
3417
|
+
// the document-order first member; the rest are siblings or cross-
|
|
3418
|
+
// parent matches found by the scope-wide pass (#557).
|
|
3419
|
+
groupMembers: [...group.memberIds]
|
|
3420
|
+
};
|
|
3373
3421
|
}
|
|
3374
3422
|
if (node.type === "INSTANCE" && node.componentId) {
|
|
3375
3423
|
const seenStage4 = getSeenStage4(context);
|
|
@@ -3458,6 +3506,7 @@ defineRule({
|
|
|
3458
3506
|
});
|
|
3459
3507
|
var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
|
|
3460
3508
|
var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
|
|
3509
|
+
var SEEN_MAIN_IDS_KEY = "unmapped-component:seen-main-ids";
|
|
3461
3510
|
function codeConnectIsSetUp(context) {
|
|
3462
3511
|
return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
|
|
3463
3512
|
return existsSync(join(process.cwd(), "figma.config.json"));
|
|
@@ -3470,6 +3519,9 @@ function codeConnectMappings(context) {
|
|
|
3470
3519
|
() => parseCodeConnectMappings(process.cwd())
|
|
3471
3520
|
);
|
|
3472
3521
|
}
|
|
3522
|
+
function seenMainIds(context) {
|
|
3523
|
+
return getAnalysisState(context, SEEN_MAIN_IDS_KEY, () => /* @__PURE__ */ new Set());
|
|
3524
|
+
}
|
|
3473
3525
|
var unmappedComponentDef = {
|
|
3474
3526
|
id: "unmapped-component",
|
|
3475
3527
|
name: "Unmapped Component",
|
|
@@ -3479,20 +3531,33 @@ var unmappedComponentDef = {
|
|
|
3479
3531
|
fix: "Run /canicode-roundtrip on this component to register a mapping. Figma's get_code_connect_map will skip if a mapping already exists."
|
|
3480
3532
|
};
|
|
3481
3533
|
var unmappedComponentCheck = (node, context) => {
|
|
3482
|
-
if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
|
|
3483
|
-
if (isInsideInstance(context)) return null;
|
|
3484
3534
|
if (!codeConnectIsSetUp(context)) return null;
|
|
3535
|
+
let mainId = null;
|
|
3536
|
+
let mainName = node.name;
|
|
3537
|
+
if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
|
|
3538
|
+
if (isInsideInstance(context)) return null;
|
|
3539
|
+
mainId = node.id;
|
|
3540
|
+
} else if (node.type === "INSTANCE" && node.componentId) {
|
|
3541
|
+
mainId = node.componentId;
|
|
3542
|
+
const meta = context.file.components[node.componentId];
|
|
3543
|
+
if (meta?.name) mainName = meta.name;
|
|
3544
|
+
} else {
|
|
3545
|
+
return null;
|
|
3546
|
+
}
|
|
3547
|
+
const seen = seenMainIds(context);
|
|
3548
|
+
if (seen.has(mainId)) return null;
|
|
3549
|
+
seen.add(mainId);
|
|
3485
3550
|
const mappings = codeConnectMappings(context);
|
|
3486
|
-
if (mappings.mappedNodeIds.has(
|
|
3487
|
-
const ack = context.findAcknowledgment(
|
|
3551
|
+
if (mappings.mappedNodeIds.has(mainId)) return null;
|
|
3552
|
+
const ack = context.findAcknowledgment(mainId, unmappedComponentDef.id);
|
|
3488
3553
|
if (ack && isRuleOptOutIntent(ack.intent) && ack.intent.ruleId === unmappedComponentDef.id) {
|
|
3489
3554
|
return null;
|
|
3490
3555
|
}
|
|
3491
3556
|
return {
|
|
3492
3557
|
ruleId: unmappedComponentDef.id,
|
|
3493
|
-
nodeId:
|
|
3558
|
+
nodeId: mainId,
|
|
3494
3559
|
nodePath: context.path.join(" > "),
|
|
3495
|
-
...unmappedComponentMsg(
|
|
3560
|
+
...unmappedComponentMsg(mainName)
|
|
3496
3561
|
};
|
|
3497
3562
|
};
|
|
3498
3563
|
defineRule({
|
|
@@ -3966,6 +4031,7 @@ var RuleEngine = class {
|
|
|
3966
4031
|
analysisState,
|
|
3967
4032
|
scope,
|
|
3968
4033
|
rootNodeType,
|
|
4034
|
+
rootNode,
|
|
3969
4035
|
void 0,
|
|
3970
4036
|
void 0
|
|
3971
4037
|
);
|
|
@@ -4002,7 +4068,7 @@ var RuleEngine = class {
|
|
|
4002
4068
|
/**
|
|
4003
4069
|
* Recursively traverse the tree and run rules
|
|
4004
4070
|
*/
|
|
4005
|
-
traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
|
|
4071
|
+
traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, analysisRoot, parent, siblings) {
|
|
4006
4072
|
const nodePath = [...path, node.name];
|
|
4007
4073
|
const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
|
|
4008
4074
|
const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
|
|
@@ -4025,6 +4091,7 @@ var RuleEngine = class {
|
|
|
4025
4091
|
analysisState,
|
|
4026
4092
|
scope,
|
|
4027
4093
|
rootNodeType,
|
|
4094
|
+
analysisRoot,
|
|
4028
4095
|
findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
|
|
4029
4096
|
};
|
|
4030
4097
|
for (const rule of rules) {
|
|
@@ -4074,6 +4141,7 @@ var RuleEngine = class {
|
|
|
4074
4141
|
analysisState,
|
|
4075
4142
|
scope,
|
|
4076
4143
|
rootNodeType,
|
|
4144
|
+
analysisRoot,
|
|
4077
4145
|
node,
|
|
4078
4146
|
node.children
|
|
4079
4147
|
);
|
|
@@ -4509,7 +4577,7 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
4509
4577
|
}
|
|
4510
4578
|
|
|
4511
4579
|
// package.json
|
|
4512
|
-
var version2 = "0.12.
|
|
4580
|
+
var version2 = "0.12.3";
|
|
4513
4581
|
|
|
4514
4582
|
// src/core/engine/scoring.ts
|
|
4515
4583
|
var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
|
|
@@ -6535,7 +6603,11 @@ function mapToQuestion(issue, file) {
|
|
|
6535
6603
|
...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
|
|
6536
6604
|
...suggestedName !== void 0 ? { suggestedName } : {},
|
|
6537
6605
|
isInstanceChild: applyContext.isInstanceChild,
|
|
6538
|
-
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
|
|
6606
|
+
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {},
|
|
6607
|
+
// #560 / Phase 3 delta 4a: thread groupMembers through from the
|
|
6608
|
+
// violation. Currently only populated by `missing-component`
|
|
6609
|
+
// Stage 3; non-group rules pass undefined and the field is omitted.
|
|
6610
|
+
...issue.violation.groupMembers !== void 0 ? { groupMembers: issue.violation.groupMembers } : {}
|
|
6539
6611
|
};
|
|
6540
6612
|
}
|
|
6541
6613
|
function deduplicateBySourceComponent(questions) {
|
|
@@ -6771,7 +6843,25 @@ var GotchaSurveyQuestionSchema = z.object({
|
|
|
6771
6843
|
// `[nodeId, ...replicaNodeIds]` so the same answer lands on every replica.
|
|
6772
6844
|
// Single-instance questions omit both fields.
|
|
6773
6845
|
replicas: z.number().int().min(2).optional(),
|
|
6774
|
-
replicaNodeIds: z.array(z.string()).optional()
|
|
6846
|
+
replicaNodeIds: z.array(z.string()).optional(),
|
|
6847
|
+
/**
|
|
6848
|
+
* Phase 3 (#508 / #560 / delta 4a): every node id in the rule's emitted
|
|
6849
|
+
* group, including `nodeId`. Currently only populated for
|
|
6850
|
+
* `missing-component:structure-repetition` (Stage 3) — one question per
|
|
6851
|
+
* fingerprint group, the apply step (delta 4b) iterates this list to
|
|
6852
|
+
* componentize the first member and swap the rest.
|
|
6853
|
+
*
|
|
6854
|
+
* Distinct from `replicas` / `replicaNodeIds`: those collapse N
|
|
6855
|
+
* instance-child questions that pre-existed as separate violations into
|
|
6856
|
+
* one. `groupMembers` carries N original group members from a single
|
|
6857
|
+
* group-shaped violation that never became N separate issues. The apply
|
|
6858
|
+
* step distinguishes by which field is set.
|
|
6859
|
+
*/
|
|
6860
|
+
// `.min(2)` locks the contract: a group-shaped emit always carries at
|
|
6861
|
+
// least 2 members (Stage 3 short-circuits below `structureMinRepetitions`
|
|
6862
|
+
// = 2). A future rule emitting `[]` or `[oneId]` is a programming error,
|
|
6863
|
+
// not a runtime case to handle gracefully.
|
|
6864
|
+
groupMembers: z.array(z.string()).min(2).optional()
|
|
6775
6865
|
});
|
|
6776
6866
|
var SurveyQuestionBatchSchema = z.object({
|
|
6777
6867
|
ruleId: z.string(),
|
|
@@ -7945,7 +8035,7 @@ function runCodeConnectChecks(cwd) {
|
|
|
7945
8035
|
}
|
|
7946
8036
|
var PUBLISH_CHECK_NAME = "Figma component published in a library";
|
|
7947
8037
|
async function runFigmaPublishCheck(input) {
|
|
7948
|
-
const { figmaUrl, token, fetchPublishedComponents } = input;
|
|
8038
|
+
const { figmaUrl, token, fetchPublishedComponents, fetchNodeType } = input;
|
|
7949
8039
|
let parsed;
|
|
7950
8040
|
try {
|
|
7951
8041
|
parsed = parseFigmaUrl(figmaUrl);
|
|
@@ -8011,6 +8101,23 @@ async function runFigmaPublishCheck(input) {
|
|
|
8011
8101
|
detail: `${match.name} (${match.node_id})`
|
|
8012
8102
|
};
|
|
8013
8103
|
}
|
|
8104
|
+
if (fetchNodeType) {
|
|
8105
|
+
let nodeType;
|
|
8106
|
+
try {
|
|
8107
|
+
nodeType = await fetchNodeType(parsed.fileKey, canonicalNodeId);
|
|
8108
|
+
} catch {
|
|
8109
|
+
nodeType = void 0;
|
|
8110
|
+
}
|
|
8111
|
+
if (nodeType && nodeType !== "COMPONENT" && nodeType !== "COMPONENT_SET") {
|
|
8112
|
+
return {
|
|
8113
|
+
name: PUBLISH_CHECK_NAME,
|
|
8114
|
+
pass: false,
|
|
8115
|
+
inconclusive: true,
|
|
8116
|
+
detail: `node ${canonicalNodeId} is type ${nodeType} \u2014 Code Connect mapping is per-component`,
|
|
8117
|
+
remediation: "Step 7 (Code Connect close-out) skips on screen-level scope anyway. To verify a specific component, re-invoke doctor with that component's URL."
|
|
8118
|
+
};
|
|
8119
|
+
}
|
|
8120
|
+
}
|
|
8014
8121
|
return {
|
|
8015
8122
|
name: PUBLISH_CHECK_NAME,
|
|
8016
8123
|
pass: false,
|
|
@@ -8058,7 +8165,11 @@ function registerDoctor(cli2) {
|
|
|
8058
8165
|
const publishCheck = await runFigmaPublishCheck({
|
|
8059
8166
|
figmaUrl: options.figmaUrl,
|
|
8060
8167
|
token,
|
|
8061
|
-
fetchPublishedComponents: client ? (fileKey) => client.getPublishedComponents(fileKey) : void 0
|
|
8168
|
+
fetchPublishedComponents: client ? (fileKey) => client.getPublishedComponents(fileKey) : void 0,
|
|
8169
|
+
fetchNodeType: client ? async (fileKey, nodeId) => {
|
|
8170
|
+
const response = await client.getFileNodes(fileKey, [nodeId]);
|
|
8171
|
+
return response.nodes[nodeId]?.document.type;
|
|
8172
|
+
} : void 0
|
|
8062
8173
|
});
|
|
8063
8174
|
results.push(publishCheck);
|
|
8064
8175
|
}
|