@featurevisor/core 2.13.0 → 2.15.0

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +1499 -1028
  3. package/coverage/coverage-final.json +21 -17
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  8. package/coverage/lcov-report/builder/index.html +38 -8
  9. package/coverage/lcov-report/builder/mutateVariables.ts.html +400 -0
  10. package/coverage/lcov-report/builder/mutator.ts.html +796 -0
  11. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  12. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  13. package/coverage/lcov-report/config/index.html +30 -15
  14. package/coverage/lcov-report/config/index.ts.html +88 -0
  15. package/coverage/lcov-report/config/projectConfig.ts.html +79 -46
  16. package/coverage/lcov-report/datasource/adapter.ts.html +74 -11
  17. package/coverage/lcov-report/datasource/datasource.ts.html +77 -26
  18. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +693 -69
  19. package/coverage/lcov-report/datasource/index.html +35 -35
  20. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  21. package/coverage/lcov-report/index.html +53 -53
  22. package/coverage/lcov-report/linter/attributeSchema.ts.html +4 -4
  23. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  24. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  25. package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
  26. package/coverage/lcov-report/linter/featureSchema.ts.html +848 -389
  27. package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
  28. package/coverage/lcov-report/linter/index.html +50 -35
  29. package/coverage/lcov-report/linter/lintProject.ts.html +437 -92
  30. package/coverage/lcov-report/linter/mutationNotation.ts.html +1309 -0
  31. package/coverage/lcov-report/linter/printError.ts.html +9 -9
  32. package/coverage/lcov-report/linter/schema.ts.html +69 -69
  33. package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
  34. package/coverage/lcov-report/linter/testSchema.ts.html +10 -10
  35. package/coverage/lcov-report/list/index.html +1 -1
  36. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  37. package/coverage/lcov-report/parsers/index.html +1 -1
  38. package/coverage/lcov-report/parsers/index.ts.html +4 -4
  39. package/coverage/lcov-report/parsers/json.ts.html +2 -2
  40. package/coverage/lcov-report/parsers/yml.ts.html +6 -6
  41. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  42. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  43. package/coverage/lcov-report/tester/index.html +1 -1
  44. package/coverage/lcov-report/utils/git.ts.html +118 -16
  45. package/coverage/lcov-report/utils/index.html +14 -14
  46. package/coverage/lcov.info +2918 -1932
  47. package/lib/builder/buildDatafile.js +8 -3
  48. package/lib/builder/buildDatafile.js.map +1 -1
  49. package/lib/builder/mutateVariables.d.ts +14 -0
  50. package/lib/builder/mutateVariables.js +90 -0
  51. package/lib/builder/mutateVariables.js.map +1 -0
  52. package/lib/builder/mutateVariables.spec.d.ts +1 -0
  53. package/lib/builder/mutateVariables.spec.js +1045 -0
  54. package/lib/builder/mutateVariables.spec.js.map +1 -0
  55. package/lib/builder/mutator.d.ts +20 -0
  56. package/lib/builder/mutator.js +223 -0
  57. package/lib/builder/mutator.js.map +1 -0
  58. package/lib/builder/mutator.spec.d.ts +1 -0
  59. package/lib/builder/mutator.spec.js +368 -0
  60. package/lib/builder/mutator.spec.js.map +1 -0
  61. package/lib/config/projectConfig.d.ts +3 -0
  62. package/lib/config/projectConfig.js +7 -1
  63. package/lib/config/projectConfig.js.map +1 -1
  64. package/lib/config/projectConfig.spec.d.ts +1 -0
  65. package/lib/config/projectConfig.spec.js +24 -0
  66. package/lib/config/projectConfig.spec.js.map +1 -0
  67. package/lib/datasource/adapter.d.ts +7 -0
  68. package/lib/datasource/adapter.js +7 -0
  69. package/lib/datasource/adapter.js.map +1 -1
  70. package/lib/datasource/datasource.d.ts +3 -1
  71. package/lib/datasource/datasource.js +6 -0
  72. package/lib/datasource/datasource.js.map +1 -1
  73. package/lib/datasource/filesystemAdapter.d.ts +8 -0
  74. package/lib/datasource/filesystemAdapter.js +134 -7
  75. package/lib/datasource/filesystemAdapter.js.map +1 -1
  76. package/lib/datasource/filesystemAdapter.spec.d.ts +1 -0
  77. package/lib/datasource/filesystemAdapter.spec.js +88 -0
  78. package/lib/datasource/filesystemAdapter.spec.js.map +1 -0
  79. package/lib/linter/conditionSchema.spec.js +2 -0
  80. package/lib/linter/conditionSchema.spec.js.map +1 -1
  81. package/lib/linter/featureSchema.d.ts +7 -7
  82. package/lib/linter/featureSchema.js +176 -103
  83. package/lib/linter/featureSchema.js.map +1 -1
  84. package/lib/linter/featureSchema.spec.js +167 -0
  85. package/lib/linter/featureSchema.spec.js.map +1 -1
  86. package/lib/linter/lintProject.d.ts +2 -0
  87. package/lib/linter/lintProject.js +85 -8
  88. package/lib/linter/lintProject.js.map +1 -1
  89. package/lib/linter/lintProject.spec.js +37 -0
  90. package/lib/linter/lintProject.spec.js.map +1 -1
  91. package/lib/linter/mutationNotation.d.ts +47 -0
  92. package/lib/linter/mutationNotation.js +381 -0
  93. package/lib/linter/mutationNotation.js.map +1 -0
  94. package/lib/linter/mutationNotation.spec.d.ts +1 -0
  95. package/lib/linter/mutationNotation.spec.js +549 -0
  96. package/lib/linter/mutationNotation.spec.js.map +1 -0
  97. package/lib/linter/segmentSchema.spec.js +2 -0
  98. package/lib/linter/segmentSchema.spec.js.map +1 -1
  99. package/lib/utils/git.js +32 -7
  100. package/lib/utils/git.js.map +1 -1
  101. package/package.json +2 -2
  102. package/src/builder/buildDatafile.ts +29 -3
  103. package/src/builder/mutateVariables.spec.ts +1134 -0
  104. package/src/builder/mutateVariables.ts +105 -0
  105. package/src/builder/mutator.spec.ts +413 -0
  106. package/src/builder/mutator.ts +237 -0
  107. package/src/config/projectConfig.spec.ts +31 -0
  108. package/src/config/projectConfig.ts +11 -0
  109. package/src/datasource/adapter.ts +21 -0
  110. package/src/datasource/datasource.ts +18 -1
  111. package/src/datasource/filesystemAdapter.spec.ts +153 -0
  112. package/src/datasource/filesystemAdapter.ts +216 -8
  113. package/src/linter/conditionSchema.spec.ts +2 -0
  114. package/src/linter/featureSchema.spec.ts +203 -0
  115. package/src/linter/featureSchema.ts +320 -167
  116. package/src/linter/lintProject.spec.ts +74 -0
  117. package/src/linter/lintProject.ts +123 -8
  118. package/src/linter/mutationNotation.spec.ts +642 -0
  119. package/src/linter/mutationNotation.ts +408 -0
  120. package/src/linter/segmentSchema.spec.ts +2 -0
  121. package/src/utils/git.ts +41 -7
@@ -1,7 +1,13 @@
1
- import type { Schema } from "@featurevisor/types";
1
+ import type { Schema, SchemaType } from "@featurevisor/types";
2
2
  import { z } from "zod";
3
3
 
4
4
  import { ProjectConfig } from "../config";
5
+ import {
6
+ isMutationKey,
7
+ validateMutationKey,
8
+ parsePathMapKey,
9
+ resolveSchemaAtPath,
10
+ } from "./mutationNotation";
5
11
  import {
6
12
  valueZodSchema,
7
13
  propertyTypeEnum,
@@ -250,27 +256,42 @@ function valueDeepEqual(a: unknown, b: unknown): boolean {
250
256
  return false;
251
257
  }
252
258
 
259
+ type SchemaLikeForRequired = {
260
+ type?: string;
261
+ properties?: Record<string, unknown>;
262
+ required?: string[];
263
+ items?: unknown;
264
+ schema?: string;
265
+ oneOf?: unknown[];
266
+ };
267
+
253
268
  /**
254
269
  * Recursively validates that every `required` array (at this level and in nested
255
270
  * object/array schemas) only contains keys that exist in the same level's `properties`.
256
271
  * Adds Zod issues with the correct path for invalid required field names.
272
+ * When schemasByKey is provided, resolves schema references (schema: key) so that
273
+ * referenced and nested schemas are validated too.
257
274
  */
258
275
  function refineRequiredKeysInSchema(
259
- schema: {
260
- type?: string;
261
- properties?: Record<string, unknown>;
262
- required?: string[];
263
- items?: unknown;
264
- },
276
+ schema: SchemaLikeForRequired,
265
277
  pathPrefix: (string | number)[],
266
278
  ctx: z.RefinementCtx,
279
+ schemasByKey?: Record<string, Schema>,
267
280
  ): void {
268
281
  if (!schema || typeof schema !== "object") return;
269
282
 
270
- const effectiveType = schema.type;
271
- const properties = schema.properties;
272
- const required = schema.required;
273
- const items = schema.items;
283
+ // Resolve schema reference so we validate the referenced schema (and can recurse into it)
284
+ let current: SchemaLikeForRequired = schema;
285
+ if (schema.schema && schemasByKey?.[schema.schema]) {
286
+ current = schemasByKey[schema.schema] as SchemaLikeForRequired;
287
+ refineRequiredKeysInSchema(current, pathPrefix, ctx, schemasByKey);
288
+ return;
289
+ }
290
+
291
+ const effectiveType = current.type;
292
+ const properties = current.properties;
293
+ const required = current.required;
294
+ const items = current.items;
274
295
 
275
296
  if (
276
297
  effectiveType === "object" &&
@@ -296,9 +317,10 @@ function refineRequiredKeysInSchema(
296
317
  const nested = properties[key];
297
318
  if (nested && typeof nested === "object") {
298
319
  refineRequiredKeysInSchema(
299
- nested as Parameters<typeof refineRequiredKeysInSchema>[0],
320
+ nested as SchemaLikeForRequired,
300
321
  [...pathPrefix, "properties", key],
301
322
  ctx,
323
+ schemasByKey,
302
324
  );
303
325
  }
304
326
  }
@@ -306,20 +328,22 @@ function refineRequiredKeysInSchema(
306
328
 
307
329
  if (items && typeof items === "object" && !Array.isArray(items)) {
308
330
  refineRequiredKeysInSchema(
309
- items as Parameters<typeof refineRequiredKeysInSchema>[0],
331
+ items as SchemaLikeForRequired,
310
332
  [...pathPrefix, "items"],
311
333
  ctx,
334
+ schemasByKey,
312
335
  );
313
336
  }
314
337
 
315
- const oneOf = (schema as { oneOf?: unknown[] }).oneOf;
338
+ const oneOf = (current as { oneOf?: unknown[] }).oneOf;
316
339
  if (oneOf && Array.isArray(oneOf)) {
317
340
  oneOf.forEach((branch, i) => {
318
341
  if (branch && typeof branch === "object") {
319
342
  refineRequiredKeysInSchema(
320
- branch as Parameters<typeof refineRequiredKeysInSchema>[0],
343
+ branch as SchemaLikeForRequired,
321
344
  [...pathPrefix, "oneOf", i],
322
345
  ctx,
346
+ schemasByKey,
323
347
  );
324
348
  }
325
349
  });
@@ -504,15 +528,21 @@ function refineVariableValueObject(
504
528
  }
505
529
  }
506
530
 
531
+ /** Schema or variable schema (e.g. type "json", defaultValue); used for value validation. */
532
+ type VariableSchemaLike =
533
+ | (Omit<Schema, "type"> & { type?: SchemaType | "json" })
534
+ | null
535
+ | undefined;
536
+
507
537
  function superRefineVariableValue(
508
538
  projectConfig: ProjectConfig,
509
- variableSchema,
510
- variableValue,
511
- path,
512
- ctx,
539
+ variableSchema: VariableSchemaLike,
540
+ variableValue: unknown,
541
+ path: (string | number)[],
542
+ ctx: z.RefinementCtx,
513
543
  variableKey?: string,
514
544
  schemasByKey?: Record<string, Schema>,
515
- ) {
545
+ ): void {
516
546
  const label = getVariableLabel(variableSchema, variableKey, path);
517
547
 
518
548
  if (!variableSchema) {
@@ -854,6 +884,66 @@ function superRefineVariableValue(
854
884
  });
855
885
  }
856
886
 
887
+ /**
888
+ * Validate a single variable entry (key + value) in variables map.
889
+ * Key may be a root variable name or mutation notation (e.g. "config.width", "items[0].name").
890
+ */
891
+ function refineVariableEntry(
892
+ projectConfig: ProjectConfig,
893
+ variableSchemaByKey: Record<string, unknown>,
894
+ variableKey: string,
895
+ variableValue: unknown,
896
+ path: (string | number)[],
897
+ ctx: z.RefinementCtx,
898
+ schemasByKey?: Record<string, Schema>,
899
+ ): void {
900
+ if (isMutationKey(variableKey)) {
901
+ const validation = validateMutationKey(
902
+ variableKey,
903
+ variableSchemaByKey as Record<string, Schema>,
904
+ schemasByKey,
905
+ );
906
+ if (!validation.valid && validation.error) {
907
+ ctx.addIssue({
908
+ code: z.ZodIssueCode.custom,
909
+ message: validation.error,
910
+ path,
911
+ });
912
+ return;
913
+ }
914
+ if (validation.valueSchema) {
915
+ superRefineVariableValue(
916
+ projectConfig,
917
+ validation.valueSchema,
918
+ variableValue,
919
+ path,
920
+ ctx,
921
+ variableKey,
922
+ schemasByKey,
923
+ );
924
+ }
925
+ return;
926
+ }
927
+ const variableSchema = variableSchemaByKey[variableKey];
928
+ if (!variableSchema) {
929
+ ctx.addIssue({
930
+ code: z.ZodIssueCode.custom,
931
+ message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
932
+ path,
933
+ });
934
+ return;
935
+ }
936
+ superRefineVariableValue(
937
+ projectConfig,
938
+ variableSchema as Parameters<typeof superRefineVariableValue>[1],
939
+ variableValue,
940
+ path,
941
+ ctx,
942
+ variableKey,
943
+ schemasByKey,
944
+ );
945
+ }
946
+
857
947
  function refineForce({
858
948
  ctx,
859
949
  parsedFeature, // eslint-disable-line
@@ -879,24 +969,15 @@ function refineForce({
879
969
  // force[n].variables[key]
880
970
  if (f.variables) {
881
971
  Object.keys(f.variables).forEach((variableKey) => {
882
- const variableSchema = variableSchemaByKey[variableKey];
883
- if (!variableSchema) {
884
- ctx.addIssue({
885
- code: z.ZodIssueCode.custom,
886
- message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
887
- path: pathPrefix.concat([fN, "variables", variableKey]),
888
- });
889
- } else {
890
- superRefineVariableValue(
891
- projectConfig,
892
- variableSchema,
893
- f.variables[variableKey],
894
- pathPrefix.concat([fN, "variables", variableKey]),
895
- ctx,
896
- variableKey,
897
- schemasByKey,
898
- );
899
- }
972
+ refineVariableEntry(
973
+ projectConfig,
974
+ variableSchemaByKey,
975
+ variableKey,
976
+ f.variables[variableKey],
977
+ pathPrefix.concat([fN, "variables", variableKey]),
978
+ ctx,
979
+ schemasByKey,
980
+ );
900
981
  });
901
982
  }
902
983
  });
@@ -916,24 +997,15 @@ function refineRules({
916
997
  // rules[n].variables[key]
917
998
  if (rule.variables) {
918
999
  Object.keys(rule.variables).forEach((variableKey) => {
919
- const variableSchema = variableSchemaByKey[variableKey];
920
- if (!variableSchema) {
921
- ctx.addIssue({
922
- code: z.ZodIssueCode.custom,
923
- message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
924
- path: pathPrefix.concat([ruleN, "variables", variableKey]),
925
- });
926
- } else {
927
- superRefineVariableValue(
928
- projectConfig,
929
- variableSchema,
930
- rule.variables[variableKey],
931
- pathPrefix.concat([ruleN, "variables", variableKey]),
932
- ctx,
933
- variableKey,
934
- schemasByKey,
935
- );
936
- }
1000
+ refineVariableEntry(
1001
+ projectConfig,
1002
+ variableSchemaByKey,
1003
+ variableKey,
1004
+ rule.variables[variableKey],
1005
+ pathPrefix.concat([ruleN, "variables", variableKey]),
1006
+ ctx,
1007
+ schemasByKey,
1008
+ );
937
1009
  });
938
1010
  }
939
1011
 
@@ -1014,6 +1086,7 @@ export function getFeatureZodSchema(
1014
1086
  ) {
1015
1087
  const schemaZodSchema = getSchemaZodSchema(availableSchemaKeys);
1016
1088
  const variableValueZodSchema = valueZodSchema;
1089
+ const variableValueOrNullZodSchema = z.union([valueZodSchema, z.null()]);
1017
1090
 
1018
1091
  const variationValueZodSchema = z.string().min(1);
1019
1092
 
@@ -1061,7 +1134,7 @@ export function getFeatureZodSchema(
1061
1134
 
1062
1135
  enabled: z.boolean().optional(),
1063
1136
  variation: variationValueZodSchema.optional(),
1064
- variables: z.record(variableValueZodSchema).optional(),
1137
+ variables: z.record(variableValueOrNullZodSchema).optional(),
1065
1138
  variationWeights: z.record(z.number().min(0).max(100)).optional(),
1066
1139
  })
1067
1140
  .strict(),
@@ -1109,7 +1182,7 @@ export function getFeatureZodSchema(
1109
1182
  segments: groupSegmentsZodSchema,
1110
1183
  enabled: z.boolean().optional(),
1111
1184
  variation: variationValueZodSchema.optional(),
1112
- variables: z.record(variableValueZodSchema).optional(),
1185
+ variables: z.record(variableValueOrNullZodSchema).optional(),
1113
1186
  })
1114
1187
  .strict(),
1115
1188
  z
@@ -1117,7 +1190,7 @@ export function getFeatureZodSchema(
1117
1190
  conditions: conditionsZodSchema,
1118
1191
  enabled: z.boolean().optional(),
1119
1192
  variation: variationValueZodSchema.optional(),
1120
- variables: z.record(variableValueZodSchema).optional(),
1193
+ variables: z.record(variableValueOrNullZodSchema).optional(),
1121
1194
  })
1122
1195
  .strict(),
1123
1196
  ]),
@@ -1299,11 +1372,12 @@ export function getFeatureZodSchema(
1299
1372
  });
1300
1373
  return;
1301
1374
  }
1302
- // Validate required ⊆ properties at this level and in all nested object schemas
1375
+ // Validate required ⊆ properties at this level and in all nested object schemas (resolve refs when schemasByKey provided)
1303
1376
  refineRequiredKeysInSchema(
1304
- variableSchema as Parameters<typeof refineRequiredKeysInSchema>[0],
1377
+ variableSchema as SchemaLikeForRequired,
1305
1378
  [],
1306
1379
  ctx,
1380
+ schemasByKey,
1307
1381
  );
1308
1382
  }),
1309
1383
  )
@@ -1318,7 +1392,7 @@ export function getFeatureZodSchema(
1318
1392
  description: z.string().optional(),
1319
1393
  value: variationValueZodSchema,
1320
1394
  weight: z.number().min(0).max(100),
1321
- variables: z.record(variableValueZodSchema).optional(),
1395
+ variables: z.record(variableValueOrNullZodSchema).optional(),
1322
1396
  variableOverrides: z
1323
1397
  .record(
1324
1398
  z.array(
@@ -1391,15 +1465,9 @@ export function getFeatureZodSchema(
1391
1465
  }
1392
1466
  }
1393
1467
 
1394
- if (!value.variablesSchema) {
1395
- return;
1396
- }
1397
-
1398
- // Every variable value is validated against its schema from variablesSchema. Sources covered:
1399
- // 1. variablesSchema[key].defaultValue 2. variablesSchema[key].disabledValue
1400
- // 3. variations[n].variables[key] 4. variations[n].variableOverrides[key][].value
1401
- // 5. rules[env][n].variables[key] 6. force[env][n].variables[key]
1402
- const variableSchemaByKey = value.variablesSchema;
1468
+ // When variablesSchema is absent, variableSchemaByKey is {} so any variable key used in
1469
+ // rules/force/variations will be reported as "not defined in variablesSchema".
1470
+ const variableSchemaByKey = value.variablesSchema ?? {};
1403
1471
  const variationValues: string[] = [];
1404
1472
 
1405
1473
  if (value.variations) {
@@ -1408,78 +1476,102 @@ export function getFeatureZodSchema(
1408
1476
  });
1409
1477
  }
1410
1478
 
1411
- // variablesSchema[key]
1412
- const variableKeys = Object.keys(variableSchemaByKey);
1413
- variableKeys.forEach((variableKey) => {
1414
- const variableSchema = variableSchemaByKey[variableKey];
1415
-
1416
- // When type and enum are both present, all enum values must match the type
1417
- const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
1418
- if (
1419
- effectiveSchema &&
1420
- effectiveSchema.type &&
1421
- Array.isArray(effectiveSchema.enum) &&
1422
- effectiveSchema.enum.length > 0
1423
- ) {
1424
- refineEnumMatchesType(
1425
- effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
1426
- ["variablesSchema", variableKey],
1427
- ctx,
1428
- );
1429
- }
1479
+ // variablesSchema[key] — only when feature defines variablesSchema
1480
+ if (value.variablesSchema) {
1481
+ const variableKeys = Object.keys(variableSchemaByKey);
1482
+ variableKeys.forEach((variableKey) => {
1483
+ const variableSchema = variableSchemaByKey[variableKey];
1430
1484
 
1431
- // Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
1432
- if (!("schema" in variableSchema) || !variableSchema.schema) {
1433
- const pathPrefix = ["variablesSchema", variableKey];
1434
- refineMinimumMaximum(
1435
- variableSchema as Parameters<typeof refineMinimumMaximum>[0],
1436
- pathPrefix,
1437
- ctx,
1438
- );
1439
- refineStringLengthPattern(
1440
- variableSchema as Parameters<typeof refineStringLengthPattern>[0],
1441
- pathPrefix,
1442
- ctx,
1443
- );
1444
- refineArrayItems(
1445
- variableSchema as Parameters<typeof refineArrayItems>[0],
1446
- pathPrefix,
1447
- ctx,
1448
- );
1449
- }
1485
+ // When variable references a schema by name, ensure it resolves and validate the referenced schema
1486
+ if ("schema" in variableSchema && variableSchema.schema) {
1487
+ const resolvedSchema = resolveVariableSchema(
1488
+ variableSchema as Parameters<typeof resolveVariableSchema>[0],
1489
+ schemasByKey,
1490
+ );
1491
+ if (!resolvedSchema) {
1492
+ ctx.addIssue({
1493
+ code: z.ZodIssueCode.custom,
1494
+ message: `Schema "${variableSchema.schema}" could not be loaded for variable "${variableKey}".`,
1495
+ path: ["variablesSchema", variableKey],
1496
+ });
1497
+ } else {
1498
+ refineRequiredKeysInSchema(
1499
+ resolvedSchema as SchemaLikeForRequired,
1500
+ ["variablesSchema", variableKey],
1501
+ ctx,
1502
+ schemasByKey,
1503
+ );
1504
+ }
1505
+ }
1450
1506
 
1451
- if (variableKey === "variation") {
1452
- ctx.addIssue({
1453
- code: z.ZodIssueCode.custom,
1454
- message: `Variable key "${variableKey}" is reserved and cannot be used.`,
1455
- path: ["variablesSchema", variableKey],
1456
- });
1457
- }
1507
+ // When type and enum are both present, all enum values must match the type
1508
+ const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
1509
+ if (
1510
+ effectiveSchema &&
1511
+ effectiveSchema.type &&
1512
+ Array.isArray(effectiveSchema.enum) &&
1513
+ effectiveSchema.enum.length > 0
1514
+ ) {
1515
+ refineEnumMatchesType(
1516
+ effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
1517
+ ["variablesSchema", variableKey],
1518
+ ctx,
1519
+ );
1520
+ }
1458
1521
 
1459
- // defaultValue
1460
- superRefineVariableValue(
1461
- projectConfig,
1462
- variableSchema,
1463
- variableSchema.defaultValue,
1464
- ["variablesSchema", variableKey, "defaultValue"],
1465
- ctx,
1466
- variableKey,
1467
- schemasByKey,
1468
- );
1522
+ // Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
1523
+ if (!("schema" in variableSchema) || !variableSchema.schema) {
1524
+ const pathPrefix = ["variablesSchema", variableKey];
1525
+ refineMinimumMaximum(
1526
+ variableSchema as Parameters<typeof refineMinimumMaximum>[0],
1527
+ pathPrefix,
1528
+ ctx,
1529
+ );
1530
+ refineStringLengthPattern(
1531
+ variableSchema as Parameters<typeof refineStringLengthPattern>[0],
1532
+ pathPrefix,
1533
+ ctx,
1534
+ );
1535
+ refineArrayItems(
1536
+ variableSchema as Parameters<typeof refineArrayItems>[0],
1537
+ pathPrefix,
1538
+ ctx,
1539
+ );
1540
+ }
1469
1541
 
1470
- // disabledValue (only when present)
1471
- if (variableSchema.disabledValue !== undefined) {
1542
+ if (variableKey === "variation") {
1543
+ ctx.addIssue({
1544
+ code: z.ZodIssueCode.custom,
1545
+ message: `Variable key "${variableKey}" is reserved and cannot be used.`,
1546
+ path: ["variablesSchema", variableKey],
1547
+ });
1548
+ }
1549
+
1550
+ // defaultValue
1472
1551
  superRefineVariableValue(
1473
1552
  projectConfig,
1474
1553
  variableSchema,
1475
- variableSchema.disabledValue,
1476
- ["variablesSchema", variableKey, "disabledValue"],
1554
+ variableSchema.defaultValue,
1555
+ ["variablesSchema", variableKey, "defaultValue"],
1477
1556
  ctx,
1478
1557
  variableKey,
1479
1558
  schemasByKey,
1480
1559
  );
1481
- }
1482
- });
1560
+
1561
+ // disabledValue (only when present)
1562
+ if (variableSchema.disabledValue !== undefined) {
1563
+ superRefineVariableValue(
1564
+ projectConfig,
1565
+ variableSchema,
1566
+ variableSchema.disabledValue,
1567
+ ["variablesSchema", variableKey, "disabledValue"],
1568
+ ctx,
1569
+ variableKey,
1570
+ schemasByKey,
1571
+ );
1572
+ }
1573
+ });
1574
+ }
1483
1575
 
1484
1576
  // variations: validate variation.variables and variation.variableOverrides (each value against its variable schema)
1485
1577
  if (value.variations) {
@@ -1487,32 +1579,21 @@ export function getFeatureZodSchema(
1487
1579
  // variations[n].variables[key]
1488
1580
  if (variation.variables) {
1489
1581
  for (const variableKey of Object.keys(variation.variables)) {
1490
- const variableValue = variation.variables[variableKey];
1491
- const variableSchema = variableSchemaByKey[variableKey];
1492
- if (!variableSchema) {
1493
- ctx.addIssue({
1494
- code: z.ZodIssueCode.custom,
1495
- message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
1496
- path: ["variations", variationN, "variables", variableKey],
1497
- });
1498
- } else {
1499
- superRefineVariableValue(
1500
- projectConfig,
1501
- variableSchema,
1502
- variableValue,
1503
- ["variations", variationN, "variables", variableKey],
1504
- ctx,
1505
- variableKey,
1506
- schemasByKey,
1507
- );
1508
- }
1582
+ refineVariableEntry(
1583
+ projectConfig,
1584
+ variableSchemaByKey,
1585
+ variableKey,
1586
+ variation.variables[variableKey],
1587
+ ["variations", variationN, "variables", variableKey],
1588
+ ctx,
1589
+ schemasByKey,
1590
+ );
1509
1591
  }
1510
1592
  }
1511
1593
 
1512
- // variations[n].variableOverrides[key][].value (validated even when variation.variables is absent)
1594
+ // variations[n].variableOverrides[key][].value (path-map or full value)
1513
1595
  if (variation.variableOverrides) {
1514
1596
  for (const variableKey of Object.keys(variation.variableOverrides)) {
1515
- const overrides = variation.variableOverrides[variableKey];
1516
1597
  const variableSchema = variableSchemaByKey[variableKey];
1517
1598
  if (!variableSchema) {
1518
1599
  ctx.addIssue({
@@ -1520,26 +1601,98 @@ export function getFeatureZodSchema(
1520
1601
  message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
1521
1602
  path: ["variations", variationN, "variableOverrides", variableKey],
1522
1603
  });
1523
- } else if (Array.isArray(overrides)) {
1524
- overrides.forEach((override, overrideN) => {
1604
+ continue;
1605
+ }
1606
+ // When variable references a schema by name, ensure it can be resolved
1607
+ const variableSchemaWithRef = variableSchema as { schema?: string };
1608
+ if (variableSchemaWithRef.schema) {
1609
+ const resolvedVarSchema = resolveVariableSchema(
1610
+ variableSchema as Parameters<typeof resolveVariableSchema>[0],
1611
+ schemasByKey,
1612
+ );
1613
+ if (!resolvedVarSchema) {
1614
+ ctx.addIssue({
1615
+ code: z.ZodIssueCode.custom,
1616
+ message: `Schema "${variableSchemaWithRef.schema}" could not be loaded for variable "${variableKey}".`,
1617
+ path: ["variations", variationN, "variableOverrides", variableKey],
1618
+ });
1619
+ continue;
1620
+ }
1621
+ }
1622
+ const overrides = variation.variableOverrides[variableKey];
1623
+ if (!Array.isArray(overrides)) continue;
1624
+ overrides.forEach((override, overrideN) => {
1625
+ const overrideValue = override.value;
1626
+ const valuePath = [
1627
+ "variations",
1628
+ variationN,
1629
+ "variableOverrides",
1630
+ variableKey,
1631
+ overrideN,
1632
+ "value",
1633
+ ];
1634
+ if (
1635
+ typeof overrideValue === "object" &&
1636
+ overrideValue !== null &&
1637
+ !Array.isArray(overrideValue)
1638
+ ) {
1639
+ const pathMap = overrideValue as Record<string, unknown>;
1640
+ for (const pathKey of Object.keys(pathMap)) {
1641
+ const pathSegments = parsePathMapKey(pathKey);
1642
+ if (!pathSegments || pathSegments.length === 0) {
1643
+ ctx.addIssue({
1644
+ code: z.ZodIssueCode.custom,
1645
+ message: `Invalid mutation path "${pathKey}" in variableOverride for "${variableKey}".`,
1646
+ path: [...valuePath, pathKey],
1647
+ });
1648
+ continue;
1649
+ }
1650
+ const atPathSchema = resolveSchemaAtPath(
1651
+ variableSchema as Schema,
1652
+ pathSegments,
1653
+ schemasByKey,
1654
+ );
1655
+ if (!atPathSchema) {
1656
+ const effectiveSchema = resolveVariableSchema(
1657
+ variableSchema as Parameters<typeof resolveVariableSchema>[0],
1658
+ schemasByKey,
1659
+ );
1660
+ const isFlatObject =
1661
+ effectiveSchema?.type === "object" &&
1662
+ (!effectiveSchema.properties ||
1663
+ Object.keys(effectiveSchema.properties).length === 0);
1664
+ if (isFlatObject) {
1665
+ continue;
1666
+ }
1667
+ ctx.addIssue({
1668
+ code: z.ZodIssueCode.custom,
1669
+ message: `Path "${pathKey}" is invalid for variable "${variableKey}" (not in schema).`,
1670
+ path: [...valuePath, pathKey],
1671
+ });
1672
+ continue;
1673
+ }
1674
+ superRefineVariableValue(
1675
+ projectConfig,
1676
+ atPathSchema,
1677
+ pathMap[pathKey],
1678
+ [...valuePath, pathKey],
1679
+ ctx,
1680
+ pathKey,
1681
+ schemasByKey,
1682
+ );
1683
+ }
1684
+ } else {
1525
1685
  superRefineVariableValue(
1526
1686
  projectConfig,
1527
- variableSchema,
1528
- override.value,
1529
- [
1530
- "variations",
1531
- variationN,
1532
- "variableOverrides",
1533
- variableKey,
1534
- overrideN,
1535
- "value",
1536
- ],
1687
+ variableSchema as Parameters<typeof superRefineVariableValue>[1],
1688
+ overrideValue,
1689
+ valuePath,
1537
1690
  ctx,
1538
1691
  variableKey,
1539
1692
  schemasByKey,
1540
1693
  );
1541
- });
1542
- }
1694
+ }
1695
+ });
1543
1696
  }
1544
1697
  }
1545
1698
  });