canicode 0.12.2 → 0.12.4
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/CHANGELOG.md +172 -0
- package/README.md +46 -65
- package/dist/cli/index.js +154 -46
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +115 -44
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +135 -45
- package/dist/mcp/server.js.map +1 -1
- package/package.json +3 -1
- package/skills/canicode-roundtrip/SKILL.md +86 -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 +86 -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/mcp/server.js
CHANGED
|
@@ -852,6 +852,7 @@ var RuleEngine = class {
|
|
|
852
852
|
analysisState,
|
|
853
853
|
scope,
|
|
854
854
|
rootNodeType,
|
|
855
|
+
rootNode,
|
|
855
856
|
void 0,
|
|
856
857
|
void 0
|
|
857
858
|
);
|
|
@@ -888,7 +889,7 @@ var RuleEngine = class {
|
|
|
888
889
|
/**
|
|
889
890
|
* Recursively traverse the tree and run rules
|
|
890
891
|
*/
|
|
891
|
-
traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
|
|
892
|
+
traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, analysisRoot, parent, siblings) {
|
|
892
893
|
const nodePath = [...path, node.name];
|
|
893
894
|
const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
|
|
894
895
|
const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
|
|
@@ -911,6 +912,7 @@ var RuleEngine = class {
|
|
|
911
912
|
analysisState,
|
|
912
913
|
scope,
|
|
913
914
|
rootNodeType,
|
|
915
|
+
analysisRoot,
|
|
914
916
|
findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
|
|
915
917
|
};
|
|
916
918
|
for (const rule of rules) {
|
|
@@ -960,6 +962,7 @@ var RuleEngine = class {
|
|
|
960
962
|
analysisState,
|
|
961
963
|
scope,
|
|
962
964
|
rootNodeType,
|
|
965
|
+
analysisRoot,
|
|
963
966
|
node,
|
|
964
967
|
node.children
|
|
965
968
|
);
|
|
@@ -1943,7 +1946,7 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
1943
1946
|
}
|
|
1944
1947
|
|
|
1945
1948
|
// package.json
|
|
1946
|
-
var version = "0.12.
|
|
1949
|
+
var version = "0.12.4";
|
|
1947
1950
|
|
|
1948
1951
|
// src/core/engine/scoring.ts
|
|
1949
1952
|
var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
|
|
@@ -2655,7 +2658,11 @@ function mapToQuestion(issue, file) {
|
|
|
2655
2658
|
...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
|
|
2656
2659
|
...suggestedName !== void 0 ? { suggestedName } : {},
|
|
2657
2660
|
isInstanceChild: applyContext.isInstanceChild,
|
|
2658
|
-
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
|
|
2661
|
+
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {},
|
|
2662
|
+
// #560 / Phase 3 delta 4a: thread groupMembers through from the
|
|
2663
|
+
// violation. Currently only populated by `missing-component`
|
|
2664
|
+
// Stage 3; non-group rules pass undefined and the field is omitted.
|
|
2665
|
+
...issue.violation.groupMembers !== void 0 ? { groupMembers: issue.violation.groupMembers } : {}
|
|
2659
2666
|
};
|
|
2660
2667
|
}
|
|
2661
2668
|
function deduplicateBySourceComponent(questions) {
|
|
@@ -3684,6 +3691,22 @@ body {
|
|
|
3684
3691
|
border: 1px solid rgba(239,68,68,0.15);
|
|
3685
3692
|
}
|
|
3686
3693
|
|
|
3694
|
+
/* ---- Warning (skipped nodes, etc.) ---- */
|
|
3695
|
+
.warning-msg {
|
|
3696
|
+
background: var(--amber-bg);
|
|
3697
|
+
color: #92400e;
|
|
3698
|
+
padding: 10px 12px;
|
|
3699
|
+
border-radius: var(--radius-sm);
|
|
3700
|
+
font-size: 12px;
|
|
3701
|
+
margin-bottom: 16px;
|
|
3702
|
+
border: 1px solid rgba(245,158,11,0.2);
|
|
3703
|
+
}
|
|
3704
|
+
.warning-msg details { margin-top: 8px; }
|
|
3705
|
+
.warning-msg summary { cursor: pointer; font-weight: 500; }
|
|
3706
|
+
.warning-msg ul { margin: 8px 0 0 16px; padding: 0; }
|
|
3707
|
+
.warning-msg li { margin: 4px 0; }
|
|
3708
|
+
.warning-msg a { color: inherit; text-decoration: underline; }
|
|
3709
|
+
|
|
3687
3710
|
/* ---- Layout (plugin) ---- */
|
|
3688
3711
|
.container { padding: 16px; }
|
|
3689
3712
|
.header {
|
|
@@ -3909,7 +3932,24 @@ var EVENTS = {
|
|
|
3909
3932
|
// has a single place to wire it up.
|
|
3910
3933
|
ROUNDTRIP_DEFINITION_WRITE_SKIPPED: `${EVENT_PREFIX}roundtrip_definition_write_skipped`,
|
|
3911
3934
|
/** CLI `canicode roundtrip-tally` completed successfully. */
|
|
3912
|
-
ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally
|
|
3935
|
+
ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`,
|
|
3936
|
+
/**
|
|
3937
|
+
* Phase 3 (#508 / ADR-023): `applyComponentize` outcome — either a
|
|
3938
|
+
* successful `createComponentFromNode` call or one of the guard rejections
|
|
3939
|
+
* (instance-child, free-form parent, error) that route to the Strategy C
|
|
3940
|
+
* annotate-fallback. Surfaces under `props.outcome` so a Node-side reader
|
|
3941
|
+
* can split the funnel.
|
|
3942
|
+
*/
|
|
3943
|
+
ROUNDTRIP_COMPONENTIZE: `${EVENT_PREFIX}roundtrip_componentize`,
|
|
3944
|
+
/**
|
|
3945
|
+
* Phase 3 (#508 / ADR-023, #554): `applyReplaceWithInstance` outcome —
|
|
3946
|
+
* either a successful instance swap (`outcome: "replaced"`) or one of the
|
|
3947
|
+
* guard rejections (`"skipped-free-form-parent"`, `"skipped-prereq-missing"`,
|
|
3948
|
+
* `"error"`). The primitive pairs with `ROUNDTRIP_COMPONENTIZE` so a
|
|
3949
|
+
* Node-side reader can correlate componentize+swap pairs across a single
|
|
3950
|
+
* Phase 3 batch.
|
|
3951
|
+
*/
|
|
3952
|
+
ROUNDTRIP_REPLACE_WITH_INSTANCE: `${EVENT_PREFIX}roundtrip_replace_with_instance`
|
|
3913
3953
|
};
|
|
3914
3954
|
|
|
3915
3955
|
// src/core/monitoring/capture.ts
|
|
@@ -4309,8 +4349,8 @@ var missingComponentMsg = {
|
|
|
4309
4349
|
message: `"${name}" appears ${count} times`,
|
|
4310
4350
|
suggestion: `Extract as a reusable component`
|
|
4311
4351
|
}),
|
|
4312
|
-
structureRepetition: (name,
|
|
4313
|
-
message: `"${name}" and ${
|
|
4352
|
+
structureRepetition: (name, matchCount) => ({
|
|
4353
|
+
message: `"${name}" and ${matchCount} other frame(s) share the same internal structure`,
|
|
4314
4354
|
suggestion: `Extract a shared component from the repeated structure`
|
|
4315
4355
|
}),
|
|
4316
4356
|
styleOverride: (componentName, overrides) => ({
|
|
@@ -4736,6 +4776,10 @@ defineRule({
|
|
|
4736
4776
|
function isOnGrid(value, gridBase) {
|
|
4737
4777
|
return value % gridBase === 0;
|
|
4738
4778
|
}
|
|
4779
|
+
function hasOwnBoundVariables(obj) {
|
|
4780
|
+
const bv = obj["boundVariables"];
|
|
4781
|
+
return bv !== void 0 && bv !== null && Object.keys(bv).length > 0;
|
|
4782
|
+
}
|
|
4739
4783
|
var rawValueDef = {
|
|
4740
4784
|
id: "raw-value",
|
|
4741
4785
|
name: "Raw Value",
|
|
@@ -4750,6 +4794,7 @@ var rawValueCheck = (node, context) => {
|
|
|
4750
4794
|
if (!hasStyleReference(node, "fill") && !hasBoundVariable(node, "fills")) {
|
|
4751
4795
|
for (const fill of node.fills) {
|
|
4752
4796
|
const fillObj = fill;
|
|
4797
|
+
if (hasOwnBoundVariables(fillObj)) continue;
|
|
4753
4798
|
if (fillObj["type"] === "SOLID" && fillObj["color"]) {
|
|
4754
4799
|
const c = fillObj["color"];
|
|
4755
4800
|
const hex = `#${Math.round((c["r"] ?? 0) * 255).toString(16).padStart(2, "0")}${Math.round((c["g"] ?? 0) * 255).toString(16).padStart(2, "0")}${Math.round((c["b"] ?? 0) * 255).toString(16).padStart(2, "0")}`.toUpperCase();
|
|
@@ -4784,9 +4829,10 @@ var rawValueCheck = (node, context) => {
|
|
|
4784
4829
|
}
|
|
4785
4830
|
}
|
|
4786
4831
|
if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
|
|
4787
|
-
if (!hasStyleReference(node, "effect")) {
|
|
4832
|
+
if (!hasStyleReference(node, "effect") && !hasBoundVariable(node, "effects")) {
|
|
4788
4833
|
for (const effect of node.effects) {
|
|
4789
4834
|
const effectObj = effect;
|
|
4835
|
+
if (hasOwnBoundVariables(effectObj)) continue;
|
|
4790
4836
|
if (effectObj["type"] === "DROP_SHADOW" || effectObj["type"] === "INNER_SHADOW") {
|
|
4791
4837
|
const shadowType = effectObj["type"] === "DROP_SHADOW" ? "drop shadow" : "inner shadow";
|
|
4792
4838
|
const offset = effectObj["offset"];
|
|
@@ -4817,6 +4863,7 @@ var rawValueCheck = (node, context) => {
|
|
|
4817
4863
|
}
|
|
4818
4864
|
const spacingKeys = ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "itemSpacing"];
|
|
4819
4865
|
for (const key of spacingKeys) {
|
|
4866
|
+
if (key === "itemSpacing" && node.primaryAxisAlignItems === "SPACE_BETWEEN") continue;
|
|
4820
4867
|
const value = node[key];
|
|
4821
4868
|
if (value !== void 0 && value > 0 && !hasBoundVariable(node, key)) {
|
|
4822
4869
|
const label = key === "itemSpacing" ? "gap" : key.replace("padding", "padding-").toLowerCase();
|
|
@@ -4849,14 +4896,15 @@ var irregularSpacingCheck = (node, context, options) => {
|
|
|
4849
4896
|
const spacingEntries = [];
|
|
4850
4897
|
for (const key of ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom"]) {
|
|
4851
4898
|
const v = node[key];
|
|
4852
|
-
if (v !== void 0 && v > 0) spacingEntries.push({ value: v, subType: "padding" });
|
|
4899
|
+
if (v !== void 0 && v > 0) spacingEntries.push({ key, value: v, subType: "padding" });
|
|
4853
4900
|
}
|
|
4854
|
-
if (node.itemSpacing !== void 0 && node.itemSpacing > 0) {
|
|
4855
|
-
spacingEntries.push({ value: node.itemSpacing, subType: "gap" });
|
|
4901
|
+
if (node.itemSpacing !== void 0 && node.itemSpacing > 0 && node.primaryAxisAlignItems !== "SPACE_BETWEEN") {
|
|
4902
|
+
spacingEntries.push({ key: "itemSpacing", value: node.itemSpacing, subType: "gap" });
|
|
4856
4903
|
}
|
|
4857
4904
|
const commonValues = [1, 2];
|
|
4858
4905
|
for (const entry of spacingEntries) {
|
|
4859
4906
|
if (commonValues.includes(entry.value)) continue;
|
|
4907
|
+
if (hasBoundVariable(node, entry.key)) continue;
|
|
4860
4908
|
if (!isOnGrid(entry.value, gridBase)) {
|
|
4861
4909
|
return {
|
|
4862
4910
|
ruleId: irregularSpacingDef.id,
|
|
@@ -4921,6 +4969,41 @@ function getSeenStage1(context) {
|
|
|
4921
4969
|
function getSeenStage4(context) {
|
|
4922
4970
|
return getAnalysisState(context, SEEN_STAGE4_KEY, () => /* @__PURE__ */ new Set());
|
|
4923
4971
|
}
|
|
4972
|
+
function stage3GroupsKey(maxDepth) {
|
|
4973
|
+
return `missing-component:stage3Groups:depth=${maxDepth}`;
|
|
4974
|
+
}
|
|
4975
|
+
function nodeQualifiesForStage3(node, parent, insideInstance) {
|
|
4976
|
+
if (insideInstance) return false;
|
|
4977
|
+
if (node.type !== "FRAME") return false;
|
|
4978
|
+
if (parent?.type === "COMPONENT_SET") return false;
|
|
4979
|
+
if (!node.children || node.children.length === 0) return false;
|
|
4980
|
+
return true;
|
|
4981
|
+
}
|
|
4982
|
+
function buildStage3Groups(root, maxFingerprintDepth) {
|
|
4983
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4984
|
+
const walk = (node, parent, ancestorIsInstance) => {
|
|
4985
|
+
const insideInstance = ancestorIsInstance || node.type === "INSTANCE";
|
|
4986
|
+
if (nodeQualifiesForStage3(node, parent, ancestorIsInstance)) {
|
|
4987
|
+
const fp = buildFingerprint(node, maxFingerprintDepth);
|
|
4988
|
+
const existing = groups.get(fp);
|
|
4989
|
+
if (existing) existing.memberIds.push(node.id);
|
|
4990
|
+
else groups.set(fp, { memberIds: [node.id] });
|
|
4991
|
+
}
|
|
4992
|
+
if (node.children) {
|
|
4993
|
+
for (const child of node.children) walk(child, node, insideInstance);
|
|
4994
|
+
}
|
|
4995
|
+
};
|
|
4996
|
+
walk(root, null, false);
|
|
4997
|
+
return groups;
|
|
4998
|
+
}
|
|
4999
|
+
function getStage3Groups(context, maxFingerprintDepth) {
|
|
5000
|
+
const root = context.analysisRoot ?? context.file.document;
|
|
5001
|
+
return getAnalysisState(
|
|
5002
|
+
context,
|
|
5003
|
+
stage3GroupsKey(maxFingerprintDepth),
|
|
5004
|
+
() => buildStage3Groups(root, maxFingerprintDepth)
|
|
5005
|
+
);
|
|
5006
|
+
}
|
|
4924
5007
|
var missingComponentDef = {
|
|
4925
5008
|
id: "missing-component",
|
|
4926
5009
|
name: "Missing Component",
|
|
@@ -4935,7 +5018,8 @@ var missingComponentCheck = (node, context, options) => {
|
|
|
4935
5018
|
const matchingComponent = Object.values(components).find(
|
|
4936
5019
|
(c) => c.name.toLowerCase() === node.name.toLowerCase()
|
|
4937
5020
|
);
|
|
4938
|
-
const
|
|
5021
|
+
const scopeRoot = context.analysisRoot ?? context.file.document;
|
|
5022
|
+
const frameNames = collectFrameNames(scopeRoot);
|
|
4939
5023
|
const sameNameFrames = frameNames.get(node.name);
|
|
4940
5024
|
const firstFrame = sameNameFrames?.[0];
|
|
4941
5025
|
if (matchingComponent) {
|
|
@@ -4963,37 +5047,32 @@ var missingComponentCheck = (node, context, options) => {
|
|
|
4963
5047
|
};
|
|
4964
5048
|
}
|
|
4965
5049
|
}
|
|
4966
|
-
if (isInsideInstance(context))
|
|
4967
|
-
|
|
4968
|
-
|
|
5050
|
+
if (!nodeQualifiesForStage3(node, context.parent ?? null, isInsideInstance(context))) {
|
|
5051
|
+
return null;
|
|
5052
|
+
}
|
|
4969
5053
|
const structureMinRepetitions = options?.["structureMinRepetitions"] ?? getRuleOption("missing-component", "structureMinRepetitions", 2);
|
|
4970
5054
|
const maxFingerprintDepth = options?.["maxFingerprintDepth"] ?? getRuleOption("missing-component", "maxFingerprintDepth", 3);
|
|
5055
|
+
const groups = getStage3Groups(context, maxFingerprintDepth);
|
|
4971
5056
|
const fingerprint = buildFingerprint(node, maxFingerprintDepth);
|
|
4972
|
-
const
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
);
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
)
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
nodePath: context.path.join(" > "),
|
|
4992
|
-
...missingComponentMsg.structureRepetition(node.name, count - 1)
|
|
4993
|
-
};
|
|
4994
|
-
}
|
|
4995
|
-
}
|
|
4996
|
-
return null;
|
|
5057
|
+
const group = groups.get(fingerprint);
|
|
5058
|
+
if (!group) return null;
|
|
5059
|
+
if (group.memberIds.length < structureMinRepetitions) return null;
|
|
5060
|
+
if (group.memberIds[0] !== node.id) return null;
|
|
5061
|
+
return {
|
|
5062
|
+
ruleId: missingComponentDef.id,
|
|
5063
|
+
subType: "structure-repetition",
|
|
5064
|
+
nodeId: node.id,
|
|
5065
|
+
nodePath: context.path.join(" > "),
|
|
5066
|
+
...missingComponentMsg.structureRepetition(
|
|
5067
|
+
node.name,
|
|
5068
|
+
group.memberIds.length - 1
|
|
5069
|
+
),
|
|
5070
|
+
// #560 / delta 4a: surface the full group so the apply step can drive
|
|
5071
|
+
// the componentize+swap loop from a single user answer. `nodeId` is
|
|
5072
|
+
// the document-order first member; the rest are siblings or cross-
|
|
5073
|
+
// parent matches found by the scope-wide pass (#557).
|
|
5074
|
+
groupMembers: [...group.memberIds]
|
|
5075
|
+
};
|
|
4997
5076
|
}
|
|
4998
5077
|
if (node.type === "INSTANCE" && node.componentId) {
|
|
4999
5078
|
const seenStage4 = getSeenStage4(context);
|
|
@@ -5083,17 +5162,28 @@ defineRule({
|
|
|
5083
5162
|
var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
|
|
5084
5163
|
var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
|
|
5085
5164
|
var SEEN_MAIN_IDS_KEY = "unmapped-component:seen-main-ids";
|
|
5165
|
+
function browserCwd() {
|
|
5166
|
+
return typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : null;
|
|
5167
|
+
}
|
|
5086
5168
|
function codeConnectIsSetUp(context) {
|
|
5087
5169
|
return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
|
|
5088
|
-
|
|
5170
|
+
const cwd = browserCwd();
|
|
5171
|
+
if (cwd === null) return false;
|
|
5172
|
+
return existsSync(join(cwd, "figma.config.json"));
|
|
5089
5173
|
});
|
|
5090
5174
|
}
|
|
5091
5175
|
function codeConnectMappings(context) {
|
|
5092
|
-
return getAnalysisState(
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5176
|
+
return getAnalysisState(context, CODE_CONNECT_MAPPINGS_KEY, () => {
|
|
5177
|
+
const cwd = browserCwd();
|
|
5178
|
+
if (cwd === null) {
|
|
5179
|
+
return {
|
|
5180
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
5181
|
+
scannedFiles: [],
|
|
5182
|
+
skipReason: "no-config"
|
|
5183
|
+
};
|
|
5184
|
+
}
|
|
5185
|
+
return parseCodeConnectMappings(cwd);
|
|
5186
|
+
});
|
|
5097
5187
|
}
|
|
5098
5188
|
function seenMainIds(context) {
|
|
5099
5189
|
return getAnalysisState(context, SEEN_MAIN_IDS_KEY, () => /* @__PURE__ */ new Set());
|