@featurevisor/core 2.11.0 → 2.12.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 (79) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/coverage/clover.xml +684 -3
  3. package/coverage/coverage-final.json +4 -0
  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 +1 -1
  9. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  10. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  11. package/coverage/lcov-report/index.html +25 -10
  12. package/coverage/lcov-report/linter/conditionSchema.ts.html +775 -0
  13. package/coverage/lcov-report/linter/featureSchema.ts.html +4924 -0
  14. package/coverage/lcov-report/linter/index.html +161 -0
  15. package/coverage/lcov-report/linter/schema.ts.html +1471 -0
  16. package/coverage/lcov-report/linter/segmentSchema.ts.html +130 -0
  17. package/coverage/lcov-report/list/index.html +1 -1
  18. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  19. package/coverage/lcov-report/parsers/index.html +1 -1
  20. package/coverage/lcov-report/parsers/json.ts.html +1 -1
  21. package/coverage/lcov-report/parsers/yml.ts.html +1 -1
  22. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  23. package/coverage/lcov-report/tester/index.html +1 -1
  24. package/coverage/lcov.info +1471 -0
  25. package/lib/builder/buildDatafile.js +15 -1
  26. package/lib/builder/buildDatafile.js.map +1 -1
  27. package/lib/config/projectConfig.d.ts +2 -0
  28. package/lib/config/projectConfig.js +3 -1
  29. package/lib/config/projectConfig.js.map +1 -1
  30. package/lib/datasource/datasource.d.ts +6 -1
  31. package/lib/datasource/datasource.js +16 -0
  32. package/lib/datasource/datasource.js.map +1 -1
  33. package/lib/datasource/filesystemAdapter.js +10 -0
  34. package/lib/datasource/filesystemAdapter.js.map +1 -1
  35. package/lib/generate-code/typescript.js +280 -46
  36. package/lib/generate-code/typescript.js.map +1 -1
  37. package/lib/linter/conditionSchema.spec.d.ts +1 -0
  38. package/lib/linter/conditionSchema.spec.js +331 -0
  39. package/lib/linter/conditionSchema.spec.js.map +1 -0
  40. package/lib/linter/featureSchema.d.ts +129 -20
  41. package/lib/linter/featureSchema.js +489 -48
  42. package/lib/linter/featureSchema.js.map +1 -1
  43. package/lib/linter/featureSchema.spec.d.ts +1 -0
  44. package/lib/linter/featureSchema.spec.js +978 -0
  45. package/lib/linter/featureSchema.spec.js.map +1 -0
  46. package/lib/linter/lintProject.js +67 -1
  47. package/lib/linter/lintProject.js.map +1 -1
  48. package/lib/linter/schema.d.ts +42 -0
  49. package/lib/linter/schema.js +417 -0
  50. package/lib/linter/schema.js.map +1 -0
  51. package/lib/linter/schema.spec.d.ts +1 -0
  52. package/lib/linter/schema.spec.js +483 -0
  53. package/lib/linter/schema.spec.js.map +1 -0
  54. package/lib/linter/segmentSchema.spec.d.ts +1 -0
  55. package/lib/linter/segmentSchema.spec.js +231 -0
  56. package/lib/linter/segmentSchema.spec.js.map +1 -0
  57. package/lib/tester/testFeature.js +5 -3
  58. package/lib/tester/testFeature.js.map +1 -1
  59. package/lib/utils/git.js +3 -0
  60. package/lib/utils/git.js.map +1 -1
  61. package/package.json +5 -5
  62. package/src/builder/buildDatafile.ts +17 -1
  63. package/src/config/projectConfig.ts +3 -0
  64. package/src/datasource/datasource.ts +23 -0
  65. package/src/datasource/filesystemAdapter.ts +7 -0
  66. package/src/generate-code/typescript.ts +330 -49
  67. package/src/linter/conditionSchema.spec.ts +446 -0
  68. package/src/linter/featureSchema.spec.ts +1218 -0
  69. package/src/linter/featureSchema.ts +671 -69
  70. package/src/linter/lintProject.ts +84 -0
  71. package/src/linter/schema.spec.ts +617 -0
  72. package/src/linter/schema.ts +462 -0
  73. package/src/linter/segmentSchema.spec.ts +273 -0
  74. package/src/tester/testFeature.ts +5 -3
  75. package/src/utils/git.ts +2 -0
  76. package/lib/linter/propertySchema.d.ts +0 -5
  77. package/lib/linter/propertySchema.js +0 -43
  78. package/lib/linter/propertySchema.js.map +0 -1
  79. package/src/linter/propertySchema.ts +0 -47
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getFeatureZodSchema = getFeatureZodSchema;
4
4
  const zod_1 = require("zod");
5
- const propertySchema_1 = require("./propertySchema");
5
+ const schema_1 = require("./schema");
6
6
  const tagRegex = /^[a-z0-9-]+$/;
7
7
  function isArrayOfStrings(value) {
8
8
  return Array.isArray(value) && value.every((v) => typeof v === "string");
@@ -18,6 +18,153 @@ function getVariableLabel(variableSchema, variableKey, path) {
18
18
  variableSchema?.key ??
19
19
  (path.length > 0 ? String(path[path.length - 1]) : "variable"));
20
20
  }
21
+ /**
22
+ * Resolve variable schema to the Schema used for value validation.
23
+ * When variable has `schema` (reference), returns the parsed Schema from schemasByKey; otherwise returns the inline variable schema.
24
+ */
25
+ function resolveVariableSchema(variableSchema, schemasByKey) {
26
+ if (variableSchema.schema) {
27
+ return schemasByKey?.[variableSchema.schema] ?? null;
28
+ }
29
+ return variableSchema;
30
+ }
31
+ /** Resolve a schema by following schema references (schema: key). Used for nested schemas that may have oneOf. */
32
+ function resolveSchemaRefs(schema, schemasByKey) {
33
+ if (schema.schema && schemasByKey?.[schema.schema]) {
34
+ return resolveSchemaRefs(schemasByKey[schema.schema], schemasByKey);
35
+ }
36
+ return schema;
37
+ }
38
+ /**
39
+ * Returns true if the value matches the given schema (const, enum, type, object properties, array items, or exactly one of oneOf).
40
+ * Used for oneOf validation: value must match exactly one branch.
41
+ */
42
+ function valueMatchesSchema(schema, value, schemasByKey) {
43
+ const resolved = resolveSchemaRefs(schema, schemasByKey);
44
+ if (resolved.oneOf && Array.isArray(resolved.oneOf) && resolved.oneOf.length > 0) {
45
+ const matchCount = resolved.oneOf.filter((branch) => valueMatchesSchema(branch, value, schemasByKey)).length;
46
+ return matchCount === 1;
47
+ }
48
+ if (resolved.const !== undefined) {
49
+ return valueDeepEqual(value, resolved.const);
50
+ }
51
+ if (resolved.enum !== undefined && Array.isArray(resolved.enum)) {
52
+ return resolved.enum.some((e) => valueDeepEqual(value, e));
53
+ }
54
+ const type = resolved.type;
55
+ if (!type)
56
+ return false;
57
+ if (type === "string") {
58
+ if (typeof value !== "string")
59
+ return false;
60
+ const s = value;
61
+ if (resolved.minLength !== undefined && s.length < resolved.minLength)
62
+ return false;
63
+ if (resolved.maxLength !== undefined && s.length > resolved.maxLength)
64
+ return false;
65
+ if (resolved.pattern !== undefined) {
66
+ try {
67
+ if (!new RegExp(resolved.pattern).test(s))
68
+ return false;
69
+ }
70
+ catch {
71
+ return true;
72
+ }
73
+ }
74
+ return true;
75
+ }
76
+ if (type === "boolean")
77
+ return typeof value === "boolean";
78
+ if (type === "integer") {
79
+ if (typeof value !== "number" || !Number.isInteger(value))
80
+ return false;
81
+ if (resolved.minimum !== undefined && value < resolved.minimum)
82
+ return false;
83
+ if (resolved.maximum !== undefined && value > resolved.maximum)
84
+ return false;
85
+ return true;
86
+ }
87
+ if (type === "double") {
88
+ if (typeof value !== "number")
89
+ return false;
90
+ if (resolved.minimum !== undefined && value < resolved.minimum)
91
+ return false;
92
+ if (resolved.maximum !== undefined && value > resolved.maximum)
93
+ return false;
94
+ return true;
95
+ }
96
+ if (type === "json")
97
+ return typeof value === "string";
98
+ if (type === "object") {
99
+ if (typeof value !== "object" || value === null || Array.isArray(value))
100
+ return false;
101
+ const props = resolved.properties;
102
+ if (!props || typeof props !== "object")
103
+ return true;
104
+ const obj = value;
105
+ const required = new Set(resolved.required || []);
106
+ for (const key of required) {
107
+ if (!Object.prototype.hasOwnProperty.call(obj, key))
108
+ return false;
109
+ if (!valueMatchesSchema(props[key], obj[key], schemasByKey))
110
+ return false;
111
+ }
112
+ for (const key of Object.keys(obj)) {
113
+ const propSchema = props[key];
114
+ if (!propSchema)
115
+ return false;
116
+ if (!valueMatchesSchema(propSchema, obj[key], schemasByKey))
117
+ return false;
118
+ }
119
+ return true;
120
+ }
121
+ if (type === "array") {
122
+ if (!Array.isArray(value))
123
+ return false;
124
+ const arr = value;
125
+ if (resolved.minItems !== undefined && arr.length < resolved.minItems)
126
+ return false;
127
+ if (resolved.maxItems !== undefined && arr.length > resolved.maxItems)
128
+ return false;
129
+ if (resolved.uniqueItems) {
130
+ for (let i = 0; i < arr.length; i++) {
131
+ for (let j = i + 1; j < arr.length; j++) {
132
+ if (valueDeepEqual(arr[i], arr[j]))
133
+ return false;
134
+ }
135
+ }
136
+ }
137
+ const itemSchema = resolved.items;
138
+ if (!itemSchema || typeof itemSchema !== "object")
139
+ return arr.every((v) => typeof v === "string");
140
+ return arr.every((item) => valueMatchesSchema(itemSchema, item, schemasByKey));
141
+ }
142
+ return false;
143
+ }
144
+ /** Deep equality for variable values (primitives, plain objects, arrays). */
145
+ function valueDeepEqual(a, b) {
146
+ if (a === b)
147
+ return true;
148
+ if (typeof a !== typeof b)
149
+ return false;
150
+ if (a === null || b === null)
151
+ return a === b;
152
+ if (typeof a === "object" && typeof b === "object") {
153
+ if (Array.isArray(a) !== Array.isArray(b))
154
+ return false;
155
+ if (Array.isArray(a) && Array.isArray(b)) {
156
+ if (a.length !== b.length)
157
+ return false;
158
+ return a.every((v, i) => valueDeepEqual(v, b[i]));
159
+ }
160
+ const keysA = Object.keys(a).sort();
161
+ const keysB = Object.keys(b).sort();
162
+ if (keysA.length !== keysB.length || keysA.some((k, i) => k !== keysB[i]))
163
+ return false;
164
+ return keysA.every((k) => valueDeepEqual(a[k], b[k]));
165
+ }
166
+ return false;
167
+ }
21
168
  /**
22
169
  * Recursively validates that every `required` array (at this level and in nested
23
170
  * object/array schemas) only contains keys that exist in the same level's `properties`.
@@ -57,6 +204,14 @@ function refineRequiredKeysInSchema(schema, pathPrefix, ctx) {
57
204
  if (items && typeof items === "object" && !Array.isArray(items)) {
58
205
  refineRequiredKeysInSchema(items, [...pathPrefix, "items"], ctx);
59
206
  }
207
+ const oneOf = schema.oneOf;
208
+ if (oneOf && Array.isArray(oneOf)) {
209
+ oneOf.forEach((branch, i) => {
210
+ if (branch && typeof branch === "object") {
211
+ refineRequiredKeysInSchema(branch, [...pathPrefix, "oneOf", i], ctx);
212
+ }
213
+ });
214
+ }
60
215
  }
61
216
  function typeOfValue(value) {
62
217
  if (value === null)
@@ -70,13 +225,45 @@ function typeOfValue(value) {
70
225
  /**
71
226
  * Validates a variable value against an array schema. Recursively validates each item
72
227
  * when the schema defines `items` (nested arrays/objects use the same refinement).
228
+ * Enforces minItems, maxItems, and uniqueItems when set.
73
229
  */
74
- function refineVariableValueArray(projectConfig, variableSchema, variableValue, path, ctx, variableKey) {
230
+ function refineVariableValueArray(projectConfig, variableSchema, variableValue, path, ctx, variableKey, schemasByKey) {
75
231
  const label = getVariableLabel(variableSchema, variableKey, path);
232
+ const minItems = variableSchema.minItems;
233
+ const maxItems = variableSchema.maxItems;
234
+ const uniqueItems = variableSchema.uniqueItems;
235
+ if (minItems !== undefined && variableValue.length < minItems) {
236
+ ctx.addIssue({
237
+ code: zod_1.z.ZodIssueCode.custom,
238
+ message: `Variable "${label}" (type array) length (${variableValue.length}) is less than \`minItems\` (${minItems}).`,
239
+ path,
240
+ });
241
+ }
242
+ if (maxItems !== undefined && variableValue.length > maxItems) {
243
+ ctx.addIssue({
244
+ code: zod_1.z.ZodIssueCode.custom,
245
+ message: `Variable "${label}" (type array) length (${variableValue.length}) is greater than \`maxItems\` (${maxItems}).`,
246
+ path,
247
+ });
248
+ }
249
+ if (uniqueItems) {
250
+ for (let i = 0; i < variableValue.length; i++) {
251
+ for (let j = i + 1; j < variableValue.length; j++) {
252
+ if (valueDeepEqual(variableValue[i], variableValue[j])) {
253
+ ctx.addIssue({
254
+ code: zod_1.z.ZodIssueCode.custom,
255
+ message: `Variable "${label}" (type array) has duplicate items at indices ${i} and ${j} but \`uniqueItems\` is true.`,
256
+ path,
257
+ });
258
+ break;
259
+ }
260
+ }
261
+ }
262
+ }
76
263
  const itemSchema = variableSchema.items;
77
264
  if (itemSchema) {
78
265
  variableValue.forEach((item, index) => {
79
- superRefineVariableValue(projectConfig, itemSchema, item, [...path, index], ctx, variableKey);
266
+ superRefineVariableValue(projectConfig, itemSchema, item, [...path, index], ctx, variableKey, schemasByKey);
80
267
  });
81
268
  }
82
269
  else {
@@ -103,7 +290,7 @@ function refineVariableValueArray(projectConfig, variableSchema, variableValue,
103
290
  * Validates a variable value against an object schema. Recursively validates each property
104
291
  * when the schema defines `properties` (nested objects/arrays use the same refinement).
105
292
  */
106
- function refineVariableValueObject(projectConfig, variableSchema, variableValue, path, ctx, variableKey) {
293
+ function refineVariableValueObject(projectConfig, variableSchema, variableValue, path, ctx, variableKey, schemasByKey) {
107
294
  const label = getVariableLabel(variableSchema, variableKey, path);
108
295
  const schemaProperties = variableSchema.properties;
109
296
  if (schemaProperties && typeof schemaProperties === "object") {
@@ -129,7 +316,7 @@ function refineVariableValueObject(projectConfig, variableSchema, variableValue,
129
316
  });
130
317
  }
131
318
  else {
132
- superRefineVariableValue(projectConfig, propSchema, variableValue[key], [...path, key], ctx, key);
319
+ superRefineVariableValue(projectConfig, propSchema, variableValue[key], [...path, key], ctx, key, schemasByKey);
133
320
  }
134
321
  }
135
322
  }
@@ -156,7 +343,7 @@ function refineVariableValueObject(projectConfig, variableSchema, variableValue,
156
343
  }
157
344
  }
158
345
  }
159
- function superRefineVariableValue(projectConfig, variableSchema, variableValue, path, ctx, variableKey) {
346
+ function superRefineVariableValue(projectConfig, variableSchema, variableValue, path, ctx, variableKey, schemasByKey) {
160
347
  const label = getVariableLabel(variableSchema, variableKey, path);
161
348
  if (!variableSchema) {
162
349
  const variableName = path.length > 0 && typeof path[path.length - 1] === "string"
@@ -170,6 +357,60 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
170
357
  });
171
358
  return;
172
359
  }
360
+ const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
361
+ if (variableSchema.schema && effectiveSchema === null) {
362
+ ctx.addIssue({
363
+ code: zod_1.z.ZodIssueCode.custom,
364
+ message: `Schema "${variableSchema.schema}" could not be loaded for value validation.`,
365
+ path,
366
+ });
367
+ return;
368
+ }
369
+ if (!effectiveSchema) {
370
+ return;
371
+ }
372
+ const effectiveOneOf = effectiveSchema.oneOf;
373
+ if (effectiveOneOf !== undefined && Array.isArray(effectiveOneOf) && effectiveOneOf.length > 0) {
374
+ const matchCount = effectiveOneOf.filter((branch) => valueMatchesSchema(branch, variableValue, schemasByKey)).length;
375
+ if (matchCount === 0) {
376
+ ctx.addIssue({
377
+ code: zod_1.z.ZodIssueCode.custom,
378
+ message: `Variable "${label}" must match exactly one of the \`oneOf\` schemas (got ${JSON.stringify(variableValue)}; matched none).`,
379
+ path,
380
+ });
381
+ }
382
+ else if (matchCount > 1) {
383
+ ctx.addIssue({
384
+ code: zod_1.z.ZodIssueCode.custom,
385
+ message: `Variable "${label}" must match exactly one of the \`oneOf\` schemas (matched ${matchCount}).`,
386
+ path,
387
+ });
388
+ }
389
+ return;
390
+ }
391
+ const effectiveConst = effectiveSchema.const;
392
+ if (effectiveConst !== undefined) {
393
+ if (!valueDeepEqual(variableValue, effectiveConst)) {
394
+ ctx.addIssue({
395
+ code: zod_1.z.ZodIssueCode.custom,
396
+ message: `Variable "${label}" must equal the constant value defined in schema (got ${JSON.stringify(variableValue)}).`,
397
+ path,
398
+ });
399
+ }
400
+ return;
401
+ }
402
+ const effectiveEnum = effectiveSchema.enum;
403
+ if (effectiveEnum !== undefined && Array.isArray(effectiveEnum) && effectiveEnum.length > 0) {
404
+ const allowed = effectiveEnum.some((v) => valueDeepEqual(variableValue, v));
405
+ if (!allowed) {
406
+ ctx.addIssue({
407
+ code: zod_1.z.ZodIssueCode.custom,
408
+ message: `Variable "${label}" must be one of the allowed enum values (got ${JSON.stringify(variableValue)}).`,
409
+ path,
410
+ });
411
+ }
412
+ return;
413
+ }
173
414
  // Require a value (no undefined) for every variable usage
174
415
  if (variableValue === undefined) {
175
416
  ctx.addIssue({
@@ -179,9 +420,9 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
179
420
  });
180
421
  return;
181
422
  }
182
- const expectedType = variableSchema.type;
423
+ const expectedType = effectiveSchema.type;
183
424
  const gotType = typeOfValue(variableValue);
184
- // string — only string allowed
425
+ // string — only string allowed; schema minLength/maxLength/pattern applied when set
185
426
  if (expectedType === "string") {
186
427
  if (typeof variableValue !== "string") {
187
428
  ctx.addIssue({
@@ -191,6 +432,37 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
191
432
  });
192
433
  return;
193
434
  }
435
+ const strMinLen = effectiveSchema.minLength;
436
+ const strMaxLen = effectiveSchema.maxLength;
437
+ const strPattern = effectiveSchema.pattern;
438
+ if (strMinLen !== undefined && variableValue.length < strMinLen) {
439
+ ctx.addIssue({
440
+ code: zod_1.z.ZodIssueCode.custom,
441
+ message: `Variable "${label}" (type string) length (${variableValue.length}) is less than \`minLength\` (${strMinLen}).`,
442
+ path,
443
+ });
444
+ }
445
+ if (strMaxLen !== undefined && variableValue.length > strMaxLen) {
446
+ ctx.addIssue({
447
+ code: zod_1.z.ZodIssueCode.custom,
448
+ message: `Variable "${label}" (type string) length (${variableValue.length}) is greater than \`maxLength\` (${strMaxLen}).`,
449
+ path,
450
+ });
451
+ }
452
+ if (strPattern !== undefined) {
453
+ try {
454
+ if (!new RegExp(strPattern).test(variableValue)) {
455
+ ctx.addIssue({
456
+ code: zod_1.z.ZodIssueCode.custom,
457
+ message: `Variable "${label}" (type string) does not match \`pattern\`.`,
458
+ path,
459
+ });
460
+ }
461
+ }
462
+ catch {
463
+ // invalid regex already reported at schema parse time
464
+ }
465
+ }
194
466
  if (projectConfig.maxVariableStringLength &&
195
467
  variableValue.length > projectConfig.maxVariableStringLength) {
196
468
  ctx.addIssue({
@@ -225,6 +497,23 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
225
497
  message: `Variable "${label}" (type integer) must be an integer; got ${variableValue}.`,
226
498
  path,
227
499
  });
500
+ return;
501
+ }
502
+ const intMin = effectiveSchema.minimum;
503
+ const intMax = effectiveSchema.maximum;
504
+ if (intMin !== undefined && variableValue < intMin) {
505
+ ctx.addIssue({
506
+ code: zod_1.z.ZodIssueCode.custom,
507
+ message: `Variable "${label}" (type integer) must be >= minimum (${intMin}); got ${variableValue}.`,
508
+ path,
509
+ });
510
+ }
511
+ if (intMax !== undefined && variableValue > intMax) {
512
+ ctx.addIssue({
513
+ code: zod_1.z.ZodIssueCode.custom,
514
+ message: `Variable "${label}" (type integer) must be <= maximum (${intMax}); got ${variableValue}.`,
515
+ path,
516
+ });
228
517
  }
229
518
  return;
230
519
  }
@@ -244,6 +533,23 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
244
533
  message: `Variable "${label}" (type double) must be a finite number; got ${variableValue}.`,
245
534
  path,
246
535
  });
536
+ return;
537
+ }
538
+ const doubleMin = effectiveSchema.minimum;
539
+ const doubleMax = effectiveSchema.maximum;
540
+ if (doubleMin !== undefined && variableValue < doubleMin) {
541
+ ctx.addIssue({
542
+ code: zod_1.z.ZodIssueCode.custom,
543
+ message: `Variable "${label}" (type double) must be >= minimum (${doubleMin}); got ${variableValue}.`,
544
+ path,
545
+ });
546
+ }
547
+ if (doubleMax !== undefined && variableValue > doubleMax) {
548
+ ctx.addIssue({
549
+ code: zod_1.z.ZodIssueCode.custom,
550
+ message: `Variable "${label}" (type double) must be <= maximum (${doubleMax}); got ${variableValue}.`,
551
+ path,
552
+ });
247
553
  }
248
554
  return;
249
555
  }
@@ -268,7 +574,7 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
268
574
  });
269
575
  return;
270
576
  }
271
- refineVariableValueArray(projectConfig, variableSchema, variableValue, path, ctx, variableKey);
577
+ refineVariableValueArray(projectConfig, effectiveSchema, variableValue, path, ctx, variableKey, schemasByKey);
272
578
  return;
273
579
  }
274
580
  // object — only plain object allowed (no null, no array)
@@ -283,7 +589,7 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
283
589
  });
284
590
  return;
285
591
  }
286
- refineVariableValueObject(projectConfig, variableSchema, variableValue, path, ctx, variableKey);
592
+ refineVariableValueObject(projectConfig, effectiveSchema, variableValue, path, ctx, variableKey, schemasByKey);
287
593
  return;
288
594
  }
289
595
  // json — only string containing valid JSON allowed
@@ -326,7 +632,7 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
326
632
  });
327
633
  }
328
634
  function refineForce({ ctx, parsedFeature, // eslint-disable-line
329
- variableSchemaByKey, variationValues, force, pathPrefix, projectConfig, }) {
635
+ variableSchemaByKey, variationValues, force, pathPrefix, projectConfig, schemasByKey, }) {
330
636
  force.forEach((f, fN) => {
331
637
  // force[n].variation
332
638
  if (f.variation) {
@@ -341,17 +647,37 @@ variableSchemaByKey, variationValues, force, pathPrefix, projectConfig, }) {
341
647
  // force[n].variables[key]
342
648
  if (f.variables) {
343
649
  Object.keys(f.variables).forEach((variableKey) => {
344
- superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], f.variables[variableKey], pathPrefix.concat([fN, "variables", variableKey]), ctx, variableKey);
650
+ const variableSchema = variableSchemaByKey[variableKey];
651
+ if (!variableSchema) {
652
+ ctx.addIssue({
653
+ code: zod_1.z.ZodIssueCode.custom,
654
+ message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
655
+ path: pathPrefix.concat([fN, "variables", variableKey]),
656
+ });
657
+ }
658
+ else {
659
+ superRefineVariableValue(projectConfig, variableSchema, f.variables[variableKey], pathPrefix.concat([fN, "variables", variableKey]), ctx, variableKey, schemasByKey);
660
+ }
345
661
  });
346
662
  }
347
663
  });
348
664
  }
349
- function refineRules({ ctx, parsedFeature, variableSchemaByKey, variationValues, rules, pathPrefix, projectConfig, }) {
665
+ function refineRules({ ctx, parsedFeature, variableSchemaByKey, variationValues, rules, pathPrefix, projectConfig, schemasByKey, }) {
350
666
  rules.forEach((rule, ruleN) => {
351
667
  // rules[n].variables[key]
352
668
  if (rule.variables) {
353
669
  Object.keys(rule.variables).forEach((variableKey) => {
354
- superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], rule.variables[variableKey], pathPrefix.concat([ruleN, "variables", variableKey]), ctx, variableKey);
670
+ const variableSchema = variableSchemaByKey[variableKey];
671
+ if (!variableSchema) {
672
+ ctx.addIssue({
673
+ code: zod_1.z.ZodIssueCode.custom,
674
+ message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
675
+ path: pathPrefix.concat([ruleN, "variables", variableKey]),
676
+ });
677
+ }
678
+ else {
679
+ superRefineVariableValue(projectConfig, variableSchema, rule.variables[variableKey], pathPrefix.concat([ruleN, "variables", variableKey]), ctx, variableKey, schemasByKey);
680
+ }
355
681
  });
356
682
  }
357
683
  // rules[n].variationWeights
@@ -406,9 +732,9 @@ function refineRules({ ctx, parsedFeature, variableSchemaByKey, variationValues,
406
732
  }
407
733
  });
408
734
  }
409
- function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttributeKeys, availableSegmentKeys, availableFeatureKeys) {
410
- const propertyZodSchema = (0, propertySchema_1.getPropertyZodSchema)();
411
- const variableValueZodSchema = propertySchema_1.valueZodSchema;
735
+ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttributeKeys, availableSegmentKeys, availableFeatureKeys, availableSchemaKeys = [], schemasByKey = {}) {
736
+ const schemaZodSchema = (0, schema_1.getSchemaZodSchema)(availableSchemaKeys);
737
+ const variableValueZodSchema = schema_1.valueZodSchema;
412
738
  const variationValueZodSchema = zod_1.z.string().min(1);
413
739
  const plainGroupSegment = zod_1.z.string().refine((value) => value === "*" || availableSegmentKeys.includes(value), (value) => ({
414
740
  message: `Unknown segment key "${value}"`,
@@ -534,13 +860,27 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
534
860
  .record(zod_1.z
535
861
  .object({
536
862
  deprecated: zod_1.z.boolean().optional(),
537
- type: zod_1.z.union([zod_1.z.literal("json"), propertySchema_1.propertyTypeEnum]),
538
- // array: when omitted, treated as array of strings
539
- items: propertyZodSchema.optional(),
540
- // object: when omitted, treated as flat object (primitive values only)
541
- properties: zod_1.z.record(propertyZodSchema).optional(),
542
- // object: optional list of required property names
863
+ // Reference to a reusable schema (mutually exclusive with type/properties/required/items)
864
+ schema: zod_1.z
865
+ .string()
866
+ .refine((value) => availableSchemaKeys.includes(value), (value) => ({ message: `Unknown schema "${value}"` }))
867
+ .optional(),
868
+ // Inline schema (mutually exclusive with schema)
869
+ type: zod_1.z.union([zod_1.z.literal("json"), schema_1.propertyTypeEnum]).optional(),
870
+ items: schemaZodSchema.optional(),
871
+ properties: zod_1.z.record(schemaZodSchema).optional(),
543
872
  required: zod_1.z.array(zod_1.z.string()).optional(),
873
+ enum: zod_1.z.array(variableValueZodSchema).optional(),
874
+ const: variableValueZodSchema.optional(),
875
+ oneOf: zod_1.z.array(schemaZodSchema).min(1).optional(),
876
+ minimum: zod_1.z.number().optional(),
877
+ maximum: zod_1.z.number().optional(),
878
+ minLength: zod_1.z.number().optional(),
879
+ maxLength: zod_1.z.number().optional(),
880
+ pattern: zod_1.z.string().optional(),
881
+ minItems: zod_1.z.number().optional(),
882
+ maxItems: zod_1.z.number().optional(),
883
+ uniqueItems: zod_1.z.boolean().optional(),
544
884
  description: zod_1.z.string().optional(),
545
885
  defaultValue: variableValueZodSchema,
546
886
  disabledValue: variableValueZodSchema.optional(),
@@ -548,6 +888,67 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
548
888
  })
549
889
  .strict()
550
890
  .superRefine((variableSchema, ctx) => {
891
+ const hasRef = "schema" in variableSchema && variableSchema.schema != null;
892
+ const hasInline = "type" in variableSchema &&
893
+ variableSchema.type != null &&
894
+ variableSchema.type !== undefined;
895
+ const hasOneOf = "oneOf" in variableSchema &&
896
+ Array.isArray(variableSchema.oneOf) &&
897
+ variableSchema.oneOf.length > 0;
898
+ if (hasRef && (hasInline || hasOneOf)) {
899
+ ctx.addIssue({
900
+ code: zod_1.z.ZodIssueCode.custom,
901
+ message: "Variable schema cannot have both `schema` (reference) and inline properties (`type`, `oneOf`, `properties`, `required`, `items`). Use one or the other.",
902
+ path: [],
903
+ });
904
+ return;
905
+ }
906
+ if (hasRef) {
907
+ const hasInlineStructure = ("type" in variableSchema && variableSchema.type != null) ||
908
+ ("properties" in variableSchema && variableSchema.properties != null) ||
909
+ ("required" in variableSchema && variableSchema.required != null) ||
910
+ ("items" in variableSchema && variableSchema.items != null) ||
911
+ ("oneOf" in variableSchema && variableSchema.oneOf != null);
912
+ const hasInlineValidation = ("minimum" in variableSchema && variableSchema.minimum !== undefined) ||
913
+ ("maximum" in variableSchema && variableSchema.maximum !== undefined) ||
914
+ ("minLength" in variableSchema && variableSchema.minLength !== undefined) ||
915
+ ("maxLength" in variableSchema && variableSchema.maxLength !== undefined) ||
916
+ ("pattern" in variableSchema && variableSchema.pattern !== undefined) ||
917
+ ("minItems" in variableSchema && variableSchema.minItems !== undefined) ||
918
+ ("maxItems" in variableSchema && variableSchema.maxItems !== undefined) ||
919
+ ("uniqueItems" in variableSchema && variableSchema.uniqueItems !== undefined);
920
+ if (hasInlineStructure) {
921
+ ctx.addIssue({
922
+ code: zod_1.z.ZodIssueCode.custom,
923
+ message: "When `schema` is set, do not set `type`, `oneOf`, `properties`, `required`, or `items`.",
924
+ path: [],
925
+ });
926
+ }
927
+ if (hasInlineValidation) {
928
+ ctx.addIssue({
929
+ code: zod_1.z.ZodIssueCode.custom,
930
+ message: "When `schema` is set, do not set `minimum`, `maximum`, `minLength`, `maxLength`, `pattern`, `minItems`, `maxItems`, or `uniqueItems`; use the referenced schema to define these.",
931
+ path: [],
932
+ });
933
+ }
934
+ return;
935
+ }
936
+ if (!hasInline && !hasOneOf) {
937
+ ctx.addIssue({
938
+ code: zod_1.z.ZodIssueCode.custom,
939
+ message: "Variable schema must have either `schema` (reference to a schema key), `type` (inline schema), or `oneOf` (inline oneOf schemas).",
940
+ path: [],
941
+ });
942
+ return;
943
+ }
944
+ if (hasInline && hasOneOf) {
945
+ ctx.addIssue({
946
+ code: zod_1.z.ZodIssueCode.custom,
947
+ message: "Variable schema cannot have both `type` and `oneOf` at the top level. Use one or the other.",
948
+ path: [],
949
+ });
950
+ return;
951
+ }
551
952
  // Validate required ⊆ properties at this level and in all nested object schemas
552
953
  refineRequiredKeysInSchema(variableSchema, [], ctx);
553
954
  }))
@@ -620,6 +1021,10 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
620
1021
  if (!value.variablesSchema) {
621
1022
  return;
622
1023
  }
1024
+ // Every variable value is validated against its schema from variablesSchema. Sources covered:
1025
+ // 1. variablesSchema[key].defaultValue 2. variablesSchema[key].disabledValue
1026
+ // 3. variations[n].variables[key] 4. variations[n].variableOverrides[key][].value
1027
+ // 5. rules[env][n].variables[key] 6. force[env][n].variables[key]
623
1028
  const variableSchemaByKey = value.variablesSchema;
624
1029
  const variationValues = [];
625
1030
  if (value.variations) {
@@ -631,6 +1036,21 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
631
1036
  const variableKeys = Object.keys(variableSchemaByKey);
632
1037
  variableKeys.forEach((variableKey) => {
633
1038
  const variableSchema = variableSchemaByKey[variableKey];
1039
+ // When type and enum are both present, all enum values must match the type
1040
+ const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
1041
+ if (effectiveSchema &&
1042
+ effectiveSchema.type &&
1043
+ Array.isArray(effectiveSchema.enum) &&
1044
+ effectiveSchema.enum.length > 0) {
1045
+ (0, schema_1.refineEnumMatchesType)(effectiveSchema, ["variablesSchema", variableKey], ctx);
1046
+ }
1047
+ // Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
1048
+ if (!("schema" in variableSchema) || !variableSchema.schema) {
1049
+ const pathPrefix = ["variablesSchema", variableKey];
1050
+ (0, schema_1.refineMinimumMaximum)(variableSchema, pathPrefix, ctx);
1051
+ (0, schema_1.refineStringLengthPattern)(variableSchema, pathPrefix, ctx);
1052
+ (0, schema_1.refineArrayItems)(variableSchema, pathPrefix, ctx);
1053
+ }
634
1054
  if (variableKey === "variation") {
635
1055
  ctx.addIssue({
636
1056
  code: zod_1.z.ZodIssueCode.custom,
@@ -639,38 +1059,55 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
639
1059
  });
640
1060
  }
641
1061
  // defaultValue
642
- superRefineVariableValue(projectConfig, variableSchema, variableSchema.defaultValue, ["variablesSchema", variableKey, "defaultValue"], ctx, variableKey);
1062
+ superRefineVariableValue(projectConfig, variableSchema, variableSchema.defaultValue, ["variablesSchema", variableKey, "defaultValue"], ctx, variableKey, schemasByKey);
643
1063
  // disabledValue (only when present)
644
1064
  if (variableSchema.disabledValue !== undefined) {
645
- superRefineVariableValue(projectConfig, variableSchema, variableSchema.disabledValue, ["variablesSchema", variableKey, "disabledValue"], ctx, variableKey);
1065
+ superRefineVariableValue(projectConfig, variableSchema, variableSchema.disabledValue, ["variablesSchema", variableKey, "disabledValue"], ctx, variableKey, schemasByKey);
646
1066
  }
647
1067
  });
648
- // variations
1068
+ // variations: validate variation.variables and variation.variableOverrides (each value against its variable schema)
649
1069
  if (value.variations) {
650
1070
  value.variations.forEach((variation, variationN) => {
651
- if (!variation.variables) {
652
- return;
653
- }
654
1071
  // variations[n].variables[key]
655
- for (const variableKey of Object.keys(variation.variables)) {
656
- const variableValue = variation.variables[variableKey];
657
- superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], variableValue, ["variations", variationN, "variables", variableKey], ctx, variableKey);
658
- // variations[n].variableOverrides[n].value
659
- if (variation.variableOverrides) {
660
- for (const variableKey of Object.keys(variation.variableOverrides)) {
661
- const overrides = variation.variableOverrides[variableKey];
662
- if (Array.isArray(overrides)) {
663
- overrides.forEach((override, overrideN) => {
664
- superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], override.value, [
665
- "variations",
666
- variationN,
667
- "variableOverrides",
668
- variableKey,
669
- overrideN,
670
- "value",
671
- ], ctx, variableKey);
672
- });
673
- }
1072
+ if (variation.variables) {
1073
+ for (const variableKey of Object.keys(variation.variables)) {
1074
+ const variableValue = variation.variables[variableKey];
1075
+ const variableSchema = variableSchemaByKey[variableKey];
1076
+ if (!variableSchema) {
1077
+ ctx.addIssue({
1078
+ code: zod_1.z.ZodIssueCode.custom,
1079
+ message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
1080
+ path: ["variations", variationN, "variables", variableKey],
1081
+ });
1082
+ }
1083
+ else {
1084
+ superRefineVariableValue(projectConfig, variableSchema, variableValue, ["variations", variationN, "variables", variableKey], ctx, variableKey, schemasByKey);
1085
+ }
1086
+ }
1087
+ }
1088
+ // variations[n].variableOverrides[key][].value (validated even when variation.variables is absent)
1089
+ if (variation.variableOverrides) {
1090
+ for (const variableKey of Object.keys(variation.variableOverrides)) {
1091
+ const overrides = variation.variableOverrides[variableKey];
1092
+ const variableSchema = variableSchemaByKey[variableKey];
1093
+ if (!variableSchema) {
1094
+ ctx.addIssue({
1095
+ code: zod_1.z.ZodIssueCode.custom,
1096
+ message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
1097
+ path: ["variations", variationN, "variableOverrides", variableKey],
1098
+ });
1099
+ }
1100
+ else if (Array.isArray(overrides)) {
1101
+ overrides.forEach((override, overrideN) => {
1102
+ superRefineVariableValue(projectConfig, variableSchema, override.value, [
1103
+ "variations",
1104
+ variationN,
1105
+ "variableOverrides",
1106
+ variableKey,
1107
+ overrideN,
1108
+ "value",
1109
+ ], ctx, variableKey, schemasByKey);
1110
+ });
674
1111
  }
675
1112
  }
676
1113
  }
@@ -689,6 +1126,7 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
689
1126
  pathPrefix: ["rules", environmentKey],
690
1127
  ctx,
691
1128
  projectConfig,
1129
+ schemasByKey,
692
1130
  });
693
1131
  }
694
1132
  // force
@@ -701,6 +1139,7 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
701
1139
  pathPrefix: ["force", environmentKey],
702
1140
  ctx,
703
1141
  projectConfig,
1142
+ schemasByKey,
704
1143
  });
705
1144
  }
706
1145
  }
@@ -717,6 +1156,7 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
717
1156
  pathPrefix: ["rules"],
718
1157
  ctx,
719
1158
  projectConfig,
1159
+ schemasByKey,
720
1160
  });
721
1161
  }
722
1162
  // force
@@ -729,6 +1169,7 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
729
1169
  pathPrefix: ["force"],
730
1170
  ctx,
731
1171
  projectConfig,
1172
+ schemasByKey,
732
1173
  });
733
1174
  }
734
1175
  }