@featurevisor/core 2.10.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 +22 -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 +283 -49
  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 +153 -17
  41. package/lib/linter/featureSchema.js +536 -49
  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 +333 -52
  67. package/src/linter/conditionSchema.spec.ts +446 -0
  68. package/src/linter/featureSchema.spec.ts +1218 -0
  69. package/src/linter/featureSchema.ts +747 -70
  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,201 @@ 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
+ }
168
+ /**
169
+ * Recursively validates that every `required` array (at this level and in nested
170
+ * object/array schemas) only contains keys that exist in the same level's `properties`.
171
+ * Adds Zod issues with the correct path for invalid required field names.
172
+ */
173
+ function refineRequiredKeysInSchema(schema, pathPrefix, ctx) {
174
+ if (!schema || typeof schema !== "object")
175
+ return;
176
+ const effectiveType = schema.type;
177
+ const properties = schema.properties;
178
+ const required = schema.required;
179
+ const items = schema.items;
180
+ if (effectiveType === "object" &&
181
+ Array.isArray(required) &&
182
+ required.length > 0 &&
183
+ properties &&
184
+ typeof properties === "object") {
185
+ const allowedKeys = Object.keys(properties);
186
+ required.forEach((key, index) => {
187
+ if (!allowedKeys.includes(key)) {
188
+ ctx.addIssue({
189
+ code: zod_1.z.ZodIssueCode.custom,
190
+ message: `Unknown required field "${key}". \`required\` must only contain property names defined in \`properties\`. Allowed: ${allowedKeys.length ? allowedKeys.join(", ") : "(none)"}.`,
191
+ path: [...pathPrefix, "required", index],
192
+ });
193
+ }
194
+ });
195
+ }
196
+ if (properties && typeof properties === "object") {
197
+ for (const key of Object.keys(properties)) {
198
+ const nested = properties[key];
199
+ if (nested && typeof nested === "object") {
200
+ refineRequiredKeysInSchema(nested, [...pathPrefix, "properties", key], ctx);
201
+ }
202
+ }
203
+ }
204
+ if (items && typeof items === "object" && !Array.isArray(items)) {
205
+ refineRequiredKeysInSchema(items, [...pathPrefix, "items"], ctx);
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
+ }
215
+ }
21
216
  function typeOfValue(value) {
22
217
  if (value === null)
23
218
  return "null";
@@ -30,13 +225,45 @@ function typeOfValue(value) {
30
225
  /**
31
226
  * Validates a variable value against an array schema. Recursively validates each item
32
227
  * when the schema defines `items` (nested arrays/objects use the same refinement).
228
+ * Enforces minItems, maxItems, and uniqueItems when set.
33
229
  */
34
- function refineVariableValueArray(projectConfig, variableSchema, variableValue, path, ctx, variableKey) {
230
+ function refineVariableValueArray(projectConfig, variableSchema, variableValue, path, ctx, variableKey, schemasByKey) {
35
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
+ }
36
263
  const itemSchema = variableSchema.items;
37
264
  if (itemSchema) {
38
265
  variableValue.forEach((item, index) => {
39
- superRefineVariableValue(projectConfig, itemSchema, item, [...path, index], ctx, variableKey);
266
+ superRefineVariableValue(projectConfig, itemSchema, item, [...path, index], ctx, variableKey, schemasByKey);
40
267
  });
41
268
  }
42
269
  else {
@@ -63,12 +290,12 @@ function refineVariableValueArray(projectConfig, variableSchema, variableValue,
63
290
  * Validates a variable value against an object schema. Recursively validates each property
64
291
  * when the schema defines `properties` (nested objects/arrays use the same refinement).
65
292
  */
66
- function refineVariableValueObject(projectConfig, variableSchema, variableValue, path, ctx, variableKey) {
293
+ function refineVariableValueObject(projectConfig, variableSchema, variableValue, path, ctx, variableKey, schemasByKey) {
67
294
  const label = getVariableLabel(variableSchema, variableKey, path);
68
295
  const schemaProperties = variableSchema.properties;
69
296
  if (schemaProperties && typeof schemaProperties === "object") {
70
297
  const requiredKeys = variableSchema.required && variableSchema.required.length > 0
71
- ? variableSchema.required
298
+ ? variableSchema.required.filter((k) => Object.prototype.hasOwnProperty.call(schemaProperties, k))
72
299
  : Object.keys(schemaProperties);
73
300
  for (const key of requiredKeys) {
74
301
  if (!Object.prototype.hasOwnProperty.call(variableValue, key)) {
@@ -89,7 +316,7 @@ function refineVariableValueObject(projectConfig, variableSchema, variableValue,
89
316
  });
90
317
  }
91
318
  else {
92
- superRefineVariableValue(projectConfig, propSchema, variableValue[key], [...path, key], ctx, key);
319
+ superRefineVariableValue(projectConfig, propSchema, variableValue[key], [...path, key], ctx, key, schemasByKey);
93
320
  }
94
321
  }
95
322
  }
@@ -116,7 +343,7 @@ function refineVariableValueObject(projectConfig, variableSchema, variableValue,
116
343
  }
117
344
  }
118
345
  }
119
- function superRefineVariableValue(projectConfig, variableSchema, variableValue, path, ctx, variableKey) {
346
+ function superRefineVariableValue(projectConfig, variableSchema, variableValue, path, ctx, variableKey, schemasByKey) {
120
347
  const label = getVariableLabel(variableSchema, variableKey, path);
121
348
  if (!variableSchema) {
122
349
  const variableName = path.length > 0 && typeof path[path.length - 1] === "string"
@@ -130,6 +357,60 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
130
357
  });
131
358
  return;
132
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
+ }
133
414
  // Require a value (no undefined) for every variable usage
134
415
  if (variableValue === undefined) {
135
416
  ctx.addIssue({
@@ -139,9 +420,9 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
139
420
  });
140
421
  return;
141
422
  }
142
- const expectedType = variableSchema.type;
423
+ const expectedType = effectiveSchema.type;
143
424
  const gotType = typeOfValue(variableValue);
144
- // string — only string allowed
425
+ // string — only string allowed; schema minLength/maxLength/pattern applied when set
145
426
  if (expectedType === "string") {
146
427
  if (typeof variableValue !== "string") {
147
428
  ctx.addIssue({
@@ -151,6 +432,37 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
151
432
  });
152
433
  return;
153
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
+ }
154
466
  if (projectConfig.maxVariableStringLength &&
155
467
  variableValue.length > projectConfig.maxVariableStringLength) {
156
468
  ctx.addIssue({
@@ -185,6 +497,23 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
185
497
  message: `Variable "${label}" (type integer) must be an integer; got ${variableValue}.`,
186
498
  path,
187
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
+ });
188
517
  }
189
518
  return;
190
519
  }
@@ -204,6 +533,23 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
204
533
  message: `Variable "${label}" (type double) must be a finite number; got ${variableValue}.`,
205
534
  path,
206
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
+ });
207
553
  }
208
554
  return;
209
555
  }
@@ -228,7 +574,7 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
228
574
  });
229
575
  return;
230
576
  }
231
- refineVariableValueArray(projectConfig, variableSchema, variableValue, path, ctx, variableKey);
577
+ refineVariableValueArray(projectConfig, effectiveSchema, variableValue, path, ctx, variableKey, schemasByKey);
232
578
  return;
233
579
  }
234
580
  // object — only plain object allowed (no null, no array)
@@ -243,7 +589,7 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
243
589
  });
244
590
  return;
245
591
  }
246
- refineVariableValueObject(projectConfig, variableSchema, variableValue, path, ctx, variableKey);
592
+ refineVariableValueObject(projectConfig, effectiveSchema, variableValue, path, ctx, variableKey, schemasByKey);
247
593
  return;
248
594
  }
249
595
  // json — only string containing valid JSON allowed
@@ -286,7 +632,7 @@ function superRefineVariableValue(projectConfig, variableSchema, variableValue,
286
632
  });
287
633
  }
288
634
  function refineForce({ ctx, parsedFeature, // eslint-disable-line
289
- variableSchemaByKey, variationValues, force, pathPrefix, projectConfig, }) {
635
+ variableSchemaByKey, variationValues, force, pathPrefix, projectConfig, schemasByKey, }) {
290
636
  force.forEach((f, fN) => {
291
637
  // force[n].variation
292
638
  if (f.variation) {
@@ -301,17 +647,37 @@ variableSchemaByKey, variationValues, force, pathPrefix, projectConfig, }) {
301
647
  // force[n].variables[key]
302
648
  if (f.variables) {
303
649
  Object.keys(f.variables).forEach((variableKey) => {
304
- 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
+ }
305
661
  });
306
662
  }
307
663
  });
308
664
  }
309
- function refineRules({ ctx, parsedFeature, variableSchemaByKey, variationValues, rules, pathPrefix, projectConfig, }) {
665
+ function refineRules({ ctx, parsedFeature, variableSchemaByKey, variationValues, rules, pathPrefix, projectConfig, schemasByKey, }) {
310
666
  rules.forEach((rule, ruleN) => {
311
667
  // rules[n].variables[key]
312
668
  if (rule.variables) {
313
669
  Object.keys(rule.variables).forEach((variableKey) => {
314
- 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
+ }
315
681
  });
316
682
  }
317
683
  // rules[n].variationWeights
@@ -366,9 +732,9 @@ function refineRules({ ctx, parsedFeature, variableSchemaByKey, variationValues,
366
732
  }
367
733
  });
368
734
  }
369
- function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttributeKeys, availableSegmentKeys, availableFeatureKeys) {
370
- const propertyZodSchema = (0, propertySchema_1.getPropertyZodSchema)();
371
- 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;
372
738
  const variationValueZodSchema = zod_1.z.string().min(1);
373
739
  const plainGroupSegment = zod_1.z.string().refine((value) => value === "*" || availableSegmentKeys.includes(value), (value) => ({
374
740
  message: `Unknown segment key "${value}"`,
@@ -494,17 +860,98 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
494
860
  .record(zod_1.z
495
861
  .object({
496
862
  deprecated: zod_1.z.boolean().optional(),
497
- type: zod_1.z.union([zod_1.z.literal("json"), propertySchema_1.propertyTypeEnum]),
498
- // array: when omitted, treated as array of strings
499
- items: propertyZodSchema.optional(),
500
- // object: when omitted, treated as flat object (primitive values only)
501
- properties: zod_1.z.record(propertyZodSchema).optional(),
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(),
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(),
502
884
  description: zod_1.z.string().optional(),
503
885
  defaultValue: variableValueZodSchema,
504
886
  disabledValue: variableValueZodSchema.optional(),
505
887
  useDefaultWhenDisabled: zod_1.z.boolean().optional(),
506
888
  })
507
- .strict())
889
+ .strict()
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
+ }
952
+ // Validate required ⊆ properties at this level and in all nested object schemas
953
+ refineRequiredKeysInSchema(variableSchema, [], ctx);
954
+ }))
508
955
  .optional(),
509
956
  disabledVariationValue: variationValueZodSchema.optional(),
510
957
  variations: zod_1.z
@@ -574,6 +1021,10 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
574
1021
  if (!value.variablesSchema) {
575
1022
  return;
576
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]
577
1028
  const variableSchemaByKey = value.variablesSchema;
578
1029
  const variationValues = [];
579
1030
  if (value.variations) {
@@ -585,6 +1036,21 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
585
1036
  const variableKeys = Object.keys(variableSchemaByKey);
586
1037
  variableKeys.forEach((variableKey) => {
587
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
+ }
588
1054
  if (variableKey === "variation") {
589
1055
  ctx.addIssue({
590
1056
  code: zod_1.z.ZodIssueCode.custom,
@@ -593,38 +1059,55 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
593
1059
  });
594
1060
  }
595
1061
  // defaultValue
596
- superRefineVariableValue(projectConfig, variableSchema, variableSchema.defaultValue, ["variablesSchema", variableKey, "defaultValue"], ctx, variableKey);
1062
+ superRefineVariableValue(projectConfig, variableSchema, variableSchema.defaultValue, ["variablesSchema", variableKey, "defaultValue"], ctx, variableKey, schemasByKey);
597
1063
  // disabledValue (only when present)
598
1064
  if (variableSchema.disabledValue !== undefined) {
599
- superRefineVariableValue(projectConfig, variableSchema, variableSchema.disabledValue, ["variablesSchema", variableKey, "disabledValue"], ctx, variableKey);
1065
+ superRefineVariableValue(projectConfig, variableSchema, variableSchema.disabledValue, ["variablesSchema", variableKey, "disabledValue"], ctx, variableKey, schemasByKey);
600
1066
  }
601
1067
  });
602
- // variations
1068
+ // variations: validate variation.variables and variation.variableOverrides (each value against its variable schema)
603
1069
  if (value.variations) {
604
1070
  value.variations.forEach((variation, variationN) => {
605
- if (!variation.variables) {
606
- return;
607
- }
608
1071
  // variations[n].variables[key]
609
- for (const variableKey of Object.keys(variation.variables)) {
610
- const variableValue = variation.variables[variableKey];
611
- superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], variableValue, ["variations", variationN, "variables", variableKey], ctx, variableKey);
612
- // variations[n].variableOverrides[n].value
613
- if (variation.variableOverrides) {
614
- for (const variableKey of Object.keys(variation.variableOverrides)) {
615
- const overrides = variation.variableOverrides[variableKey];
616
- if (Array.isArray(overrides)) {
617
- overrides.forEach((override, overrideN) => {
618
- superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], override.value, [
619
- "variations",
620
- variationN,
621
- "variableOverrides",
622
- variableKey,
623
- overrideN,
624
- "value",
625
- ], ctx, variableKey);
626
- });
627
- }
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
+ });
628
1111
  }
629
1112
  }
630
1113
  }
@@ -643,6 +1126,7 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
643
1126
  pathPrefix: ["rules", environmentKey],
644
1127
  ctx,
645
1128
  projectConfig,
1129
+ schemasByKey,
646
1130
  });
647
1131
  }
648
1132
  // force
@@ -655,6 +1139,7 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
655
1139
  pathPrefix: ["force", environmentKey],
656
1140
  ctx,
657
1141
  projectConfig,
1142
+ schemasByKey,
658
1143
  });
659
1144
  }
660
1145
  }
@@ -671,6 +1156,7 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
671
1156
  pathPrefix: ["rules"],
672
1157
  ctx,
673
1158
  projectConfig,
1159
+ schemasByKey,
674
1160
  });
675
1161
  }
676
1162
  // force
@@ -683,6 +1169,7 @@ function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttrib
683
1169
  pathPrefix: ["force"],
684
1170
  ctx,
685
1171
  projectConfig,
1172
+ schemasByKey,
686
1173
  });
687
1174
  }
688
1175
  }