@featurevisor/core 2.16.0 → 2.18.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 (73) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +798 -764
  3. package/coverage/coverage-final.json +5 -5
  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/mutateVariables.ts.html +1 -1
  10. package/coverage/lcov-report/builder/mutator.ts.html +18 -18
  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 +1 -1
  14. package/coverage/lcov-report/config/index.ts.html +1 -1
  15. package/coverage/lcov-report/config/projectConfig.ts.html +1 -1
  16. package/coverage/lcov-report/datasource/adapter.ts.html +1 -1
  17. package/coverage/lcov-report/datasource/datasource.ts.html +1 -1
  18. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +1 -1
  19. package/coverage/lcov-report/datasource/index.html +1 -1
  20. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  21. package/coverage/lcov-report/index.html +18 -18
  22. package/coverage/lcov-report/linter/attributeSchema.ts.html +1 -1
  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 +365 -188
  27. package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
  28. package/coverage/lcov-report/linter/index.html +34 -34
  29. package/coverage/lcov-report/linter/lintProject.ts.html +1 -1
  30. package/coverage/lcov-report/linter/mutationNotation.ts.html +99 -66
  31. package/coverage/lcov-report/linter/printError.ts.html +1 -1
  32. package/coverage/lcov-report/linter/schema.ts.html +170 -80
  33. package/coverage/lcov-report/linter/segmentSchema.ts.html +1 -1
  34. package/coverage/lcov-report/linter/testSchema.ts.html +1 -1
  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 +1 -1
  39. package/coverage/lcov-report/parsers/json.ts.html +1 -1
  40. package/coverage/lcov-report/parsers/yml.ts.html +1 -1
  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 +1 -1
  45. package/coverage/lcov-report/utils/index.html +1 -1
  46. package/coverage/lcov.info +1653 -1567
  47. package/json-schema/attribute.json +25 -9
  48. package/json-schema/feature.json +319 -238
  49. package/json-schema/segment.json +76 -45
  50. package/lib/generate-code/typescript.js +118 -50
  51. package/lib/generate-code/typescript.js.map +1 -1
  52. package/lib/linter/featureSchema.d.ts +9 -0
  53. package/lib/linter/featureSchema.js +38 -8
  54. package/lib/linter/featureSchema.js.map +1 -1
  55. package/lib/linter/featureSchema.spec.js +93 -0
  56. package/lib/linter/featureSchema.spec.js.map +1 -1
  57. package/lib/linter/mutationNotation.js +21 -11
  58. package/lib/linter/mutationNotation.js.map +1 -1
  59. package/lib/linter/mutationNotation.spec.js +18 -0
  60. package/lib/linter/mutationNotation.spec.js.map +1 -1
  61. package/lib/linter/schema.d.ts +1 -0
  62. package/lib/linter/schema.js +13 -0
  63. package/lib/linter/schema.js.map +1 -1
  64. package/lib/linter/schema.spec.js +51 -0
  65. package/lib/linter/schema.spec.js.map +1 -1
  66. package/package.json +5 -5
  67. package/src/generate-code/typescript.ts +150 -52
  68. package/src/linter/featureSchema.spec.ts +118 -0
  69. package/src/linter/featureSchema.ts +65 -6
  70. package/src/linter/mutationNotation.spec.ts +23 -0
  71. package/src/linter/mutationNotation.ts +18 -7
  72. package/src/linter/schema.spec.ts +72 -0
  73. package/src/linter/schema.ts +30 -0
@@ -257,6 +257,17 @@ describe("mutationNotation.ts", () => {
257
257
  expect(result).toBeNull();
258
258
  });
259
259
 
260
+ it("resolves unknown object key via additionalProperties", () => {
261
+ const result = resolveSchemaAtPath(
262
+ {
263
+ type: "object",
264
+ additionalProperties: { type: "string" },
265
+ },
266
+ [{ key: "dynamicKey" }],
267
+ );
268
+ expect(result).toEqual({ type: "string" });
269
+ });
270
+
260
271
  it("returns null when stepping into non-object", () => {
261
272
  const result = resolveSchemaAtPath(objectSchema, [{ key: "width" }, { key: "foo" }]);
262
273
  expect(result).toBeNull();
@@ -446,6 +457,18 @@ describe("mutationNotation.ts", () => {
446
457
  expect(r.error).toContain("path does not exist");
447
458
  });
448
459
 
460
+ it("valid when path uses additionalProperties", () => {
461
+ const schemas: Record<string, Schema> = {
462
+ labels: {
463
+ type: "object",
464
+ additionalProperties: { type: "string" },
465
+ },
466
+ };
467
+ const r = validateMutationKey("labels.headline", schemas);
468
+ expect(r.valid).toBe(true);
469
+ expect(r.valueSchema).toEqual({ type: "string" });
470
+ });
471
+
449
472
  it("invalid when target schema is oneOf", () => {
450
473
  const schemas: Record<string, Schema> = {
451
474
  x: {
@@ -88,9 +88,14 @@ export function resolveSchemaAtPath(
88
88
  if (seg.key) {
89
89
  if (current.type !== "object") return null;
90
90
  const props = current.properties;
91
- if (!props || typeof props !== "object") return null;
92
- const next = props[seg.key];
93
- if (next === undefined) return null;
91
+ const additional = current.additionalProperties;
92
+ const next = props && typeof props === "object" ? props[seg.key] : undefined;
93
+ if (next === undefined) {
94
+ if (!additional || typeof additional !== "object") return null;
95
+ current = resolveSchemaRef(additional, schemasByKey);
96
+ if (!current) return null;
97
+ continue;
98
+ }
94
99
  current = resolveSchemaRef(next, schemasByKey);
95
100
  if (!current) return null;
96
101
  }
@@ -135,9 +140,14 @@ function getContainerSchemaAtPath(
135
140
  } else {
136
141
  if (current.type !== "object") return null;
137
142
  const props = current.properties;
138
- if (!props || typeof props !== "object") return null;
139
- const next = props[seg.key];
140
- if (next === undefined) return null;
143
+ const additional = current.additionalProperties;
144
+ const next = props && typeof props === "object" ? props[seg.key] : undefined;
145
+ if (next === undefined) {
146
+ if (!additional || typeof additional !== "object") return null;
147
+ current = resolveSchemaRef(additional, schemasByKey);
148
+ if (!current) return null;
149
+ continue;
150
+ }
141
151
  current = resolveSchemaRef(next, schemasByKey);
142
152
  }
143
153
  if (!current) return null;
@@ -147,7 +157,8 @@ function getContainerSchemaAtPath(
147
157
  if ("index" in lastSegment || "selector" in lastSegment) {
148
158
  return { containerSchema: parentSchema, lastSegment, parentSchema };
149
159
  }
150
- const propSchema = parentSchema.properties?.[lastSegment.key];
160
+ const propSchema =
161
+ parentSchema.properties?.[lastSegment.key] ?? parentSchema.additionalProperties;
151
162
  const resolvedProp =
152
163
  propSchema && typeof propSchema === "object"
153
164
  ? resolveSchemaRef(propSchema, schemasByKey)
@@ -142,6 +142,20 @@ describe("schema.ts :: refineEnumMatchesType", () => {
142
142
  expect(issues[0].path).toEqual(["oneOf", 1, "enum", 1]);
143
143
  });
144
144
 
145
+ it("recurses into additionalProperties", () => {
146
+ const { issues, ctx } = createRefinementCtx();
147
+ refineEnumMatchesType(
148
+ {
149
+ type: "object",
150
+ additionalProperties: { type: "string", enum: ["a", 1] },
151
+ },
152
+ [],
153
+ ctx,
154
+ );
155
+ expect(issues).toHaveLength(1);
156
+ expect(issues[0].path).toEqual(["additionalProperties", "enum", 1]);
157
+ });
158
+
145
159
  it("does nothing when schema is null or not an object", () => {
146
160
  const { issues: i1, ctx: c1 } = createRefinementCtx();
147
161
  refineEnumMatchesType(null as any, [], c1);
@@ -215,6 +229,20 @@ describe("schema.ts :: refineMinimumMaximum", () => {
215
229
  expect(issues).toHaveLength(1);
216
230
  expect(issues[0].path).toEqual(["items", "minimum"]);
217
231
  });
232
+
233
+ it("recurses into additionalProperties", () => {
234
+ const { issues, ctx } = createRefinementCtx();
235
+ refineMinimumMaximum(
236
+ {
237
+ type: "object",
238
+ additionalProperties: { type: "integer", minimum: 10, maximum: 5 },
239
+ },
240
+ [],
241
+ ctx,
242
+ );
243
+ expect(issues).toHaveLength(1);
244
+ expect(issues[0].path).toEqual(["additionalProperties", "minimum"]);
245
+ });
218
246
  });
219
247
 
220
248
  describe("schema.ts :: refineStringLengthPattern", () => {
@@ -288,6 +316,20 @@ describe("schema.ts :: refineStringLengthPattern", () => {
288
316
  expect(issues).toHaveLength(1);
289
317
  expect(issues[0].path).toEqual(["properties", "code", "minLength"]);
290
318
  });
319
+
320
+ it("recurses into additionalProperties", () => {
321
+ const { issues, ctx } = createRefinementCtx();
322
+ refineStringLengthPattern(
323
+ {
324
+ type: "object",
325
+ additionalProperties: { type: "string", minLength: 10, maxLength: 5 },
326
+ },
327
+ [],
328
+ ctx,
329
+ );
330
+ expect(issues).toHaveLength(1);
331
+ expect(issues[0].path).toEqual(["additionalProperties", "minLength"]);
332
+ });
291
333
  });
292
334
 
293
335
  describe("schema.ts :: refineArrayItems", () => {
@@ -357,6 +399,25 @@ describe("schema.ts :: refineArrayItems", () => {
357
399
  expect(issues).toHaveLength(1);
358
400
  expect(issues[0].path).toEqual(["items", "minItems"]);
359
401
  });
402
+
403
+ it("recurses into additionalProperties", () => {
404
+ const { issues, ctx } = createRefinementCtx();
405
+ refineArrayItems(
406
+ {
407
+ type: "object",
408
+ additionalProperties: {
409
+ type: "array",
410
+ minItems: 5,
411
+ maxItems: 2,
412
+ items: { type: "string" },
413
+ },
414
+ },
415
+ [],
416
+ ctx,
417
+ );
418
+ expect(issues).toHaveLength(1);
419
+ expect(issues[0].path).toEqual(["additionalProperties", "minItems"]);
420
+ });
360
421
  });
361
422
 
362
423
  describe("schema.ts :: getSchemaZodSchema", () => {
@@ -491,6 +552,17 @@ describe("schema.ts :: getSchemaZodSchema", () => {
491
552
  expect(result.success).toBe(true);
492
553
  });
493
554
 
555
+ it("accepts object schema with additionalProperties", () => {
556
+ const Schema = getSchemaZodSchema([]);
557
+ const result = Schema.safeParse({
558
+ type: "object",
559
+ additionalProperties: {
560
+ type: "string",
561
+ },
562
+ });
563
+ expect(result.success).toBe(true);
564
+ });
565
+
494
566
  it("accepts array schema with items that reference another schema", () => {
495
567
  const Schema = getSchemaZodSchema(["link"]);
496
568
  const result = Schema.safeParse({
@@ -35,6 +35,7 @@ type SchemaLike = {
35
35
  uniqueItems?: boolean;
36
36
  items?: unknown;
37
37
  properties?: Record<string, unknown>;
38
+ additionalProperties?: unknown;
38
39
  oneOf?: unknown[];
39
40
  };
40
41
 
@@ -74,6 +75,13 @@ export function refineEnumMatchesType(
74
75
  );
75
76
  }
76
77
  }
78
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
79
+ refineEnumMatchesType(
80
+ schema.additionalProperties as SchemaLike,
81
+ [...pathPrefix, "additionalProperties"],
82
+ ctx,
83
+ );
84
+ }
77
85
  if (schema.oneOf && Array.isArray(schema.oneOf)) {
78
86
  schema.oneOf.forEach((branch, i) => {
79
87
  if (branch && typeof branch === "object") {
@@ -157,6 +165,13 @@ export function refineMinimumMaximum(
157
165
  );
158
166
  }
159
167
  }
168
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
169
+ refineMinimumMaximum(
170
+ schema.additionalProperties as SchemaLike,
171
+ [...pathPrefix, "additionalProperties"],
172
+ ctx,
173
+ );
174
+ }
160
175
  if (schema.oneOf && Array.isArray(schema.oneOf)) {
161
176
  schema.oneOf.forEach((branch, i) => {
162
177
  if (branch && typeof branch === "object") {
@@ -277,6 +292,13 @@ export function refineStringLengthPattern(
277
292
  );
278
293
  }
279
294
  }
295
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
296
+ refineStringLengthPattern(
297
+ schema.additionalProperties as SchemaLike,
298
+ [...pathPrefix, "additionalProperties"],
299
+ ctx,
300
+ );
301
+ }
280
302
  if (schema.oneOf && Array.isArray(schema.oneOf)) {
281
303
  schema.oneOf.forEach((branch, i) => {
282
304
  if (branch && typeof branch === "object") {
@@ -376,6 +398,13 @@ export function refineArrayItems(
376
398
  refineArrayItems(schema.properties[k] as SchemaLike, [...pathPrefix, "properties", k], ctx);
377
399
  }
378
400
  }
401
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
402
+ refineArrayItems(
403
+ schema.additionalProperties as SchemaLike,
404
+ [...pathPrefix, "additionalProperties"],
405
+ ctx,
406
+ );
407
+ }
379
408
  if (schema.oneOf && Array.isArray(schema.oneOf)) {
380
409
  schema.oneOf.forEach((branch, i) => {
381
410
  if (branch && typeof branch === "object") {
@@ -428,6 +457,7 @@ export function getSchemaZodSchema(schemaKeys: SchemaKey[] = []) {
428
457
  uniqueItems: z.boolean().optional(),
429
458
  required: z.array(z.string()).optional(),
430
459
  properties: z.record(z.string(), schemaZodSchema).optional(),
460
+ additionalProperties: schemaZodSchema.optional(),
431
461
  // Annotations: default?: Value; examples?: Value[];
432
462
 
433
463
  schema: z