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/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) => ({
|
|
@@ -2942,6 +2959,10 @@ defineRule({
|
|
|
2942
2959
|
function isOnGrid(value, gridBase) {
|
|
2943
2960
|
return value % gridBase === 0;
|
|
2944
2961
|
}
|
|
2962
|
+
function hasOwnBoundVariables(obj) {
|
|
2963
|
+
const bv = obj["boundVariables"];
|
|
2964
|
+
return bv !== void 0 && bv !== null && Object.keys(bv).length > 0;
|
|
2965
|
+
}
|
|
2945
2966
|
var rawValueDef = {
|
|
2946
2967
|
id: "raw-value",
|
|
2947
2968
|
name: "Raw Value",
|
|
@@ -2956,6 +2977,7 @@ var rawValueCheck = (node, context) => {
|
|
|
2956
2977
|
if (!hasStyleReference(node, "fill") && !hasBoundVariable(node, "fills")) {
|
|
2957
2978
|
for (const fill of node.fills) {
|
|
2958
2979
|
const fillObj = fill;
|
|
2980
|
+
if (hasOwnBoundVariables(fillObj)) continue;
|
|
2959
2981
|
if (fillObj["type"] === "SOLID" && fillObj["color"]) {
|
|
2960
2982
|
const c = fillObj["color"];
|
|
2961
2983
|
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();
|
|
@@ -2990,9 +3012,10 @@ var rawValueCheck = (node, context) => {
|
|
|
2990
3012
|
}
|
|
2991
3013
|
}
|
|
2992
3014
|
if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
|
|
2993
|
-
if (!hasStyleReference(node, "effect")) {
|
|
3015
|
+
if (!hasStyleReference(node, "effect") && !hasBoundVariable(node, "effects")) {
|
|
2994
3016
|
for (const effect of node.effects) {
|
|
2995
3017
|
const effectObj = effect;
|
|
3018
|
+
if (hasOwnBoundVariables(effectObj)) continue;
|
|
2996
3019
|
if (effectObj["type"] === "DROP_SHADOW" || effectObj["type"] === "INNER_SHADOW") {
|
|
2997
3020
|
const shadowType = effectObj["type"] === "DROP_SHADOW" ? "drop shadow" : "inner shadow";
|
|
2998
3021
|
const offset = effectObj["offset"];
|
|
@@ -3023,6 +3046,7 @@ var rawValueCheck = (node, context) => {
|
|
|
3023
3046
|
}
|
|
3024
3047
|
const spacingKeys = ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "itemSpacing"];
|
|
3025
3048
|
for (const key of spacingKeys) {
|
|
3049
|
+
if (key === "itemSpacing" && node.primaryAxisAlignItems === "SPACE_BETWEEN") continue;
|
|
3026
3050
|
const value = node[key];
|
|
3027
3051
|
if (value !== void 0 && value > 0 && !hasBoundVariable(node, key)) {
|
|
3028
3052
|
const label = key === "itemSpacing" ? "gap" : key.replace("padding", "padding-").toLowerCase();
|
|
@@ -3055,14 +3079,15 @@ var irregularSpacingCheck = (node, context, options) => {
|
|
|
3055
3079
|
const spacingEntries = [];
|
|
3056
3080
|
for (const key of ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom"]) {
|
|
3057
3081
|
const v = node[key];
|
|
3058
|
-
if (v !== void 0 && v > 0) spacingEntries.push({ value: v, subType: "padding" });
|
|
3082
|
+
if (v !== void 0 && v > 0) spacingEntries.push({ key, value: v, subType: "padding" });
|
|
3059
3083
|
}
|
|
3060
|
-
if (node.itemSpacing !== void 0 && node.itemSpacing > 0) {
|
|
3061
|
-
spacingEntries.push({ value: node.itemSpacing, subType: "gap" });
|
|
3084
|
+
if (node.itemSpacing !== void 0 && node.itemSpacing > 0 && node.primaryAxisAlignItems !== "SPACE_BETWEEN") {
|
|
3085
|
+
spacingEntries.push({ key: "itemSpacing", value: node.itemSpacing, subType: "gap" });
|
|
3062
3086
|
}
|
|
3063
3087
|
const commonValues = [1, 2];
|
|
3064
3088
|
for (const entry of spacingEntries) {
|
|
3065
3089
|
if (commonValues.includes(entry.value)) continue;
|
|
3090
|
+
if (hasBoundVariable(node, entry.key)) continue;
|
|
3066
3091
|
if (!isOnGrid(entry.value, gridBase)) {
|
|
3067
3092
|
return {
|
|
3068
3093
|
ruleId: irregularSpacingDef.id,
|
|
@@ -3297,6 +3322,41 @@ function getSeenStage1(context) {
|
|
|
3297
3322
|
function getSeenStage4(context) {
|
|
3298
3323
|
return getAnalysisState(context, SEEN_STAGE4_KEY, () => /* @__PURE__ */ new Set());
|
|
3299
3324
|
}
|
|
3325
|
+
function stage3GroupsKey(maxDepth) {
|
|
3326
|
+
return `missing-component:stage3Groups:depth=${maxDepth}`;
|
|
3327
|
+
}
|
|
3328
|
+
function nodeQualifiesForStage3(node, parent, insideInstance) {
|
|
3329
|
+
if (insideInstance) return false;
|
|
3330
|
+
if (node.type !== "FRAME") return false;
|
|
3331
|
+
if (parent?.type === "COMPONENT_SET") return false;
|
|
3332
|
+
if (!node.children || node.children.length === 0) return false;
|
|
3333
|
+
return true;
|
|
3334
|
+
}
|
|
3335
|
+
function buildStage3Groups(root, maxFingerprintDepth) {
|
|
3336
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3337
|
+
const walk = (node, parent, ancestorIsInstance) => {
|
|
3338
|
+
const insideInstance = ancestorIsInstance || node.type === "INSTANCE";
|
|
3339
|
+
if (nodeQualifiesForStage3(node, parent, ancestorIsInstance)) {
|
|
3340
|
+
const fp = buildFingerprint(node, maxFingerprintDepth);
|
|
3341
|
+
const existing = groups.get(fp);
|
|
3342
|
+
if (existing) existing.memberIds.push(node.id);
|
|
3343
|
+
else groups.set(fp, { memberIds: [node.id] });
|
|
3344
|
+
}
|
|
3345
|
+
if (node.children) {
|
|
3346
|
+
for (const child of node.children) walk(child, node, insideInstance);
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
walk(root, null, false);
|
|
3350
|
+
return groups;
|
|
3351
|
+
}
|
|
3352
|
+
function getStage3Groups(context, maxFingerprintDepth) {
|
|
3353
|
+
const root = context.analysisRoot ?? context.file.document;
|
|
3354
|
+
return getAnalysisState(
|
|
3355
|
+
context,
|
|
3356
|
+
stage3GroupsKey(maxFingerprintDepth),
|
|
3357
|
+
() => buildStage3Groups(root, maxFingerprintDepth)
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3300
3360
|
var missingComponentDef = {
|
|
3301
3361
|
id: "missing-component",
|
|
3302
3362
|
name: "Missing Component",
|
|
@@ -3311,7 +3371,8 @@ var missingComponentCheck = (node, context, options) => {
|
|
|
3311
3371
|
const matchingComponent = Object.values(components).find(
|
|
3312
3372
|
(c) => c.name.toLowerCase() === node.name.toLowerCase()
|
|
3313
3373
|
);
|
|
3314
|
-
const
|
|
3374
|
+
const scopeRoot = context.analysisRoot ?? context.file.document;
|
|
3375
|
+
const frameNames = collectFrameNames(scopeRoot);
|
|
3315
3376
|
const sameNameFrames = frameNames.get(node.name);
|
|
3316
3377
|
const firstFrame = sameNameFrames?.[0];
|
|
3317
3378
|
if (matchingComponent) {
|
|
@@ -3339,37 +3400,32 @@ var missingComponentCheck = (node, context, options) => {
|
|
|
3339
3400
|
};
|
|
3340
3401
|
}
|
|
3341
3402
|
}
|
|
3342
|
-
if (isInsideInstance(context))
|
|
3343
|
-
|
|
3344
|
-
|
|
3403
|
+
if (!nodeQualifiesForStage3(node, context.parent ?? null, isInsideInstance(context))) {
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3345
3406
|
const structureMinRepetitions = options?.["structureMinRepetitions"] ?? getRuleOption("missing-component", "structureMinRepetitions", 2);
|
|
3346
3407
|
const maxFingerprintDepth = options?.["maxFingerprintDepth"] ?? getRuleOption("missing-component", "maxFingerprintDepth", 3);
|
|
3408
|
+
const groups = getStage3Groups(context, maxFingerprintDepth);
|
|
3347
3409
|
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;
|
|
3410
|
+
const group = groups.get(fingerprint);
|
|
3411
|
+
if (!group) return null;
|
|
3412
|
+
if (group.memberIds.length < structureMinRepetitions) return null;
|
|
3413
|
+
if (group.memberIds[0] !== node.id) return null;
|
|
3414
|
+
return {
|
|
3415
|
+
ruleId: missingComponentDef.id,
|
|
3416
|
+
subType: "structure-repetition",
|
|
3417
|
+
nodeId: node.id,
|
|
3418
|
+
nodePath: context.path.join(" > "),
|
|
3419
|
+
...missingComponentMsg.structureRepetition(
|
|
3420
|
+
node.name,
|
|
3421
|
+
group.memberIds.length - 1
|
|
3422
|
+
),
|
|
3423
|
+
// #560 / delta 4a: surface the full group so the apply step can drive
|
|
3424
|
+
// the componentize+swap loop from a single user answer. `nodeId` is
|
|
3425
|
+
// the document-order first member; the rest are siblings or cross-
|
|
3426
|
+
// parent matches found by the scope-wide pass (#557).
|
|
3427
|
+
groupMembers: [...group.memberIds]
|
|
3428
|
+
};
|
|
3373
3429
|
}
|
|
3374
3430
|
if (node.type === "INSTANCE" && node.componentId) {
|
|
3375
3431
|
const seenStage4 = getSeenStage4(context);
|
|
@@ -3459,17 +3515,28 @@ defineRule({
|
|
|
3459
3515
|
var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
|
|
3460
3516
|
var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
|
|
3461
3517
|
var SEEN_MAIN_IDS_KEY = "unmapped-component:seen-main-ids";
|
|
3518
|
+
function browserCwd() {
|
|
3519
|
+
return typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : null;
|
|
3520
|
+
}
|
|
3462
3521
|
function codeConnectIsSetUp(context) {
|
|
3463
3522
|
return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
|
|
3464
|
-
|
|
3523
|
+
const cwd = browserCwd();
|
|
3524
|
+
if (cwd === null) return false;
|
|
3525
|
+
return existsSync(join(cwd, "figma.config.json"));
|
|
3465
3526
|
});
|
|
3466
3527
|
}
|
|
3467
3528
|
function codeConnectMappings(context) {
|
|
3468
|
-
return getAnalysisState(
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3529
|
+
return getAnalysisState(context, CODE_CONNECT_MAPPINGS_KEY, () => {
|
|
3530
|
+
const cwd = browserCwd();
|
|
3531
|
+
if (cwd === null) {
|
|
3532
|
+
return {
|
|
3533
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
3534
|
+
scannedFiles: [],
|
|
3535
|
+
skipReason: "no-config"
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
return parseCodeConnectMappings(cwd);
|
|
3539
|
+
});
|
|
3473
3540
|
}
|
|
3474
3541
|
function seenMainIds(context) {
|
|
3475
3542
|
return getAnalysisState(context, SEEN_MAIN_IDS_KEY, () => /* @__PURE__ */ new Set());
|
|
@@ -3983,6 +4050,7 @@ var RuleEngine = class {
|
|
|
3983
4050
|
analysisState,
|
|
3984
4051
|
scope,
|
|
3985
4052
|
rootNodeType,
|
|
4053
|
+
rootNode,
|
|
3986
4054
|
void 0,
|
|
3987
4055
|
void 0
|
|
3988
4056
|
);
|
|
@@ -4019,7 +4087,7 @@ var RuleEngine = class {
|
|
|
4019
4087
|
/**
|
|
4020
4088
|
* Recursively traverse the tree and run rules
|
|
4021
4089
|
*/
|
|
4022
|
-
traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
|
|
4090
|
+
traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, analysisRoot, parent, siblings) {
|
|
4023
4091
|
const nodePath = [...path, node.name];
|
|
4024
4092
|
const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
|
|
4025
4093
|
const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
|
|
@@ -4042,6 +4110,7 @@ var RuleEngine = class {
|
|
|
4042
4110
|
analysisState,
|
|
4043
4111
|
scope,
|
|
4044
4112
|
rootNodeType,
|
|
4113
|
+
analysisRoot,
|
|
4045
4114
|
findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
|
|
4046
4115
|
};
|
|
4047
4116
|
for (const rule of rules) {
|
|
@@ -4091,6 +4160,7 @@ var RuleEngine = class {
|
|
|
4091
4160
|
analysisState,
|
|
4092
4161
|
scope,
|
|
4093
4162
|
rootNodeType,
|
|
4163
|
+
analysisRoot,
|
|
4094
4164
|
node,
|
|
4095
4165
|
node.children
|
|
4096
4166
|
);
|
|
@@ -4526,7 +4596,7 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
4526
4596
|
}
|
|
4527
4597
|
|
|
4528
4598
|
// package.json
|
|
4529
|
-
var version2 = "0.12.
|
|
4599
|
+
var version2 = "0.12.4";
|
|
4530
4600
|
|
|
4531
4601
|
// src/core/engine/scoring.ts
|
|
4532
4602
|
var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
|
|
@@ -5837,6 +5907,22 @@ body {
|
|
|
5837
5907
|
border: 1px solid rgba(239,68,68,0.15);
|
|
5838
5908
|
}
|
|
5839
5909
|
|
|
5910
|
+
/* ---- Warning (skipped nodes, etc.) ---- */
|
|
5911
|
+
.warning-msg {
|
|
5912
|
+
background: var(--amber-bg);
|
|
5913
|
+
color: #92400e;
|
|
5914
|
+
padding: 10px 12px;
|
|
5915
|
+
border-radius: var(--radius-sm);
|
|
5916
|
+
font-size: 12px;
|
|
5917
|
+
margin-bottom: 16px;
|
|
5918
|
+
border: 1px solid rgba(245,158,11,0.2);
|
|
5919
|
+
}
|
|
5920
|
+
.warning-msg details { margin-top: 8px; }
|
|
5921
|
+
.warning-msg summary { cursor: pointer; font-weight: 500; }
|
|
5922
|
+
.warning-msg ul { margin: 8px 0 0 16px; padding: 0; }
|
|
5923
|
+
.warning-msg li { margin: 4px 0; }
|
|
5924
|
+
.warning-msg a { color: inherit; text-decoration: underline; }
|
|
5925
|
+
|
|
5840
5926
|
/* ---- Layout (plugin) ---- */
|
|
5841
5927
|
.container { padding: 16px; }
|
|
5842
5928
|
.header {
|
|
@@ -6552,7 +6638,11 @@ function mapToQuestion(issue, file) {
|
|
|
6552
6638
|
...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
|
|
6553
6639
|
...suggestedName !== void 0 ? { suggestedName } : {},
|
|
6554
6640
|
isInstanceChild: applyContext.isInstanceChild,
|
|
6555
|
-
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
|
|
6641
|
+
...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {},
|
|
6642
|
+
// #560 / Phase 3 delta 4a: thread groupMembers through from the
|
|
6643
|
+
// violation. Currently only populated by `missing-component`
|
|
6644
|
+
// Stage 3; non-group rules pass undefined and the field is omitted.
|
|
6645
|
+
...issue.violation.groupMembers !== void 0 ? { groupMembers: issue.violation.groupMembers } : {}
|
|
6556
6646
|
};
|
|
6557
6647
|
}
|
|
6558
6648
|
function deduplicateBySourceComponent(questions) {
|
|
@@ -6788,7 +6878,25 @@ var GotchaSurveyQuestionSchema = z.object({
|
|
|
6788
6878
|
// `[nodeId, ...replicaNodeIds]` so the same answer lands on every replica.
|
|
6789
6879
|
// Single-instance questions omit both fields.
|
|
6790
6880
|
replicas: z.number().int().min(2).optional(),
|
|
6791
|
-
replicaNodeIds: z.array(z.string()).optional()
|
|
6881
|
+
replicaNodeIds: z.array(z.string()).optional(),
|
|
6882
|
+
/**
|
|
6883
|
+
* Phase 3 (#508 / #560 / delta 4a): every node id in the rule's emitted
|
|
6884
|
+
* group, including `nodeId`. Currently only populated for
|
|
6885
|
+
* `missing-component:structure-repetition` (Stage 3) — one question per
|
|
6886
|
+
* fingerprint group, the apply step (delta 4b) iterates this list to
|
|
6887
|
+
* componentize the first member and swap the rest.
|
|
6888
|
+
*
|
|
6889
|
+
* Distinct from `replicas` / `replicaNodeIds`: those collapse N
|
|
6890
|
+
* instance-child questions that pre-existed as separate violations into
|
|
6891
|
+
* one. `groupMembers` carries N original group members from a single
|
|
6892
|
+
* group-shaped violation that never became N separate issues. The apply
|
|
6893
|
+
* step distinguishes by which field is set.
|
|
6894
|
+
*/
|
|
6895
|
+
// `.min(2)` locks the contract: a group-shaped emit always carries at
|
|
6896
|
+
// least 2 members (Stage 3 short-circuits below `structureMinRepetitions`
|
|
6897
|
+
// = 2). A future rule emitting `[]` or `[oneId]` is a programming error,
|
|
6898
|
+
// not a runtime case to handle gracefully.
|
|
6899
|
+
groupMembers: z.array(z.string()).min(2).optional()
|
|
6792
6900
|
});
|
|
6793
6901
|
var SurveyQuestionBatchSchema = z.object({
|
|
6794
6902
|
ruleId: z.string(),
|