@fogpipe/forma-core 0.12.0 → 0.13.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 (40) hide show
  1. package/README.md +12 -12
  2. package/dist/{chunk-U2OXXFEH.js → chunk-BDICNCE2.js} +1 -1
  3. package/dist/chunk-BDICNCE2.js.map +1 -0
  4. package/dist/{chunk-ZSW7NIMY.js → chunk-NKA3L2LJ.js} +64 -15
  5. package/dist/chunk-NKA3L2LJ.js.map +1 -0
  6. package/dist/engine/calculate.d.ts.map +1 -1
  7. package/dist/engine/enabled.d.ts.map +1 -1
  8. package/dist/engine/index.cjs +62 -13
  9. package/dist/engine/index.cjs.map +1 -1
  10. package/dist/engine/index.d.ts +8 -8
  11. package/dist/engine/index.d.ts.map +1 -1
  12. package/dist/engine/index.js +2 -2
  13. package/dist/engine/readonly.d.ts.map +1 -1
  14. package/dist/engine/required.d.ts.map +1 -1
  15. package/dist/engine/validate.d.ts.map +1 -1
  16. package/dist/engine/visibility.d.ts.map +1 -1
  17. package/dist/feel/index.cjs.map +1 -1
  18. package/dist/feel/index.js +1 -1
  19. package/dist/index.cjs +62 -13
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +2 -2
  22. package/dist/types.d.ts +16 -0
  23. package/dist/types.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/src/__tests__/feel.test.ts +67 -76
  26. package/src/__tests__/format.test.ts +19 -6
  27. package/src/__tests__/validate.test.ts +62 -20
  28. package/src/__tests__/visibility.test.ts +217 -85
  29. package/src/engine/calculate.ts +13 -10
  30. package/src/engine/enabled.ts +16 -6
  31. package/src/engine/index.ts +8 -28
  32. package/src/engine/readonly.ts +16 -6
  33. package/src/engine/required.ts +7 -5
  34. package/src/engine/validate.ts +43 -22
  35. package/src/engine/visibility.ts +40 -16
  36. package/src/feel/index.ts +12 -12
  37. package/src/format/index.ts +1 -1
  38. package/src/types.ts +46 -7
  39. package/dist/chunk-U2OXXFEH.js.map +0 -1
  40. package/dist/chunk-ZSW7NIMY.js.map +0 -1
@@ -3,17 +3,22 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect } from "vitest";
6
- import { getOptionsVisibility, getVisibleOptions } from "../engine/visibility.js";
6
+ import {
7
+ getOptionsVisibility,
8
+ getVisibleOptions,
9
+ } from "../engine/visibility.js";
7
10
  import type { Forma, SelectOption } from "../types.js";
8
11
 
9
12
  /**
10
13
  * Helper to create a minimal Forma spec for testing
11
14
  */
12
- function createTestSpec(options: {
13
- fields?: Record<string, unknown>;
14
- computed?: Record<string, { expression: string }>;
15
- referenceData?: Record<string, unknown>;
16
- } = {}): Forma {
15
+ function createTestSpec(
16
+ options: {
17
+ fields?: Record<string, unknown>;
18
+ computed?: Record<string, { expression: string }>;
19
+ referenceData?: Record<string, unknown>;
20
+ } = {},
21
+ ): Forma {
17
22
  const { fields = {}, computed, referenceData } = options;
18
23
 
19
24
  // Build fieldOrder from fields keys
@@ -53,7 +58,11 @@ describe("getOptionsVisibility", () => {
53
58
  type: "select",
54
59
  options: [
55
60
  { value: "junior", label: "Junior" },
56
- { value: "senior", label: "Senior", visibleWhen: "experience >= 5" },
61
+ {
62
+ value: "senior",
63
+ label: "Senior",
64
+ visibleWhen: "experience >= 5",
65
+ },
57
66
  ],
58
67
  },
59
68
  },
@@ -92,9 +101,21 @@ describe("getOptionsVisibility", () => {
92
101
  position: {
93
102
  type: "select",
94
103
  options: [
95
- { value: "dev_fe", label: "Frontend Dev", visibleWhen: 'department = "eng"' },
96
- { value: "dev_be", label: "Backend Dev", visibleWhen: 'department = "eng"' },
97
- { value: "recruiter", label: "Recruiter", visibleWhen: 'department = "hr"' },
104
+ {
105
+ value: "dev_fe",
106
+ label: "Frontend Dev",
107
+ visibleWhen: 'department = "eng"',
108
+ },
109
+ {
110
+ value: "dev_be",
111
+ label: "Backend Dev",
112
+ visibleWhen: 'department = "eng"',
113
+ },
114
+ {
115
+ value: "recruiter",
116
+ label: "Recruiter",
117
+ visibleWhen: 'department = "hr"',
118
+ },
98
119
  ],
99
120
  },
100
121
  },
@@ -102,11 +123,14 @@ describe("getOptionsVisibility", () => {
102
123
 
103
124
  // Engineering department
104
125
  const engResult = getOptionsVisibility({ department: "eng" }, spec);
105
- expect(engResult["position"].map(o => o.value)).toEqual(["dev_fe", "dev_be"]);
126
+ expect(engResult["position"].map((o) => o.value)).toEqual([
127
+ "dev_fe",
128
+ "dev_be",
129
+ ]);
106
130
 
107
131
  // HR department
108
132
  const hrResult = getOptionsVisibility({ department: "hr" }, spec);
109
- expect(hrResult["position"].map(o => o.value)).toEqual(["recruiter"]);
133
+ expect(hrResult["position"].map((o) => o.value)).toEqual(["recruiter"]);
110
134
  });
111
135
 
112
136
  it("should use computed values in expressions", () => {
@@ -116,7 +140,11 @@ describe("getOptionsVisibility", () => {
116
140
  type: "select",
117
141
  options: [
118
142
  { value: "standard", label: "Standard" },
119
- { value: "express", label: "Express", visibleWhen: "computed.total >= 50" },
143
+ {
144
+ value: "express",
145
+ label: "Express",
146
+ visibleWhen: "computed.total >= 50",
147
+ },
120
148
  ],
121
149
  },
122
150
  },
@@ -127,11 +155,14 @@ describe("getOptionsVisibility", () => {
127
155
 
128
156
  // Total = 40, only standard
129
157
  const lowResult = getOptionsVisibility({ quantity: 2, price: 20 }, spec);
130
- expect(lowResult["shipping"].map(o => o.value)).toEqual(["standard"]);
158
+ expect(lowResult["shipping"].map((o) => o.value)).toEqual(["standard"]);
131
159
 
132
160
  // Total = 100, both options
133
161
  const highResult = getOptionsVisibility({ quantity: 5, price: 20 }, spec);
134
- expect(highResult["shipping"].map(o => o.value)).toEqual(["standard", "express"]);
162
+ expect(highResult["shipping"].map((o) => o.value)).toEqual([
163
+ "standard",
164
+ "express",
165
+ ]);
135
166
  });
136
167
 
137
168
  it("should accept pre-computed values", () => {
@@ -141,14 +172,20 @@ describe("getOptionsVisibility", () => {
141
172
  type: "select",
142
173
  options: [
143
174
  { value: "basic", label: "Basic" },
144
- { value: "premium", label: "Premium", visibleWhen: "computed.score >= 100" },
175
+ {
176
+ value: "premium",
177
+ label: "Premium",
178
+ visibleWhen: "computed.score >= 100",
179
+ },
145
180
  ],
146
181
  },
147
182
  },
148
183
  });
149
184
 
150
- const result = getOptionsVisibility({}, spec, { computed: { score: 150 } });
151
- expect(result["tier"].map(o => o.value)).toEqual(["basic", "premium"]);
185
+ const result = getOptionsVisibility({}, spec, {
186
+ computed: { score: 150 },
187
+ });
188
+ expect(result["tier"].map((o) => o.value)).toEqual(["basic", "premium"]);
152
189
  });
153
190
  });
154
191
 
@@ -169,8 +206,16 @@ describe("getOptionsVisibility", () => {
169
206
  addon: {
170
207
  type: "select",
171
208
  options: [
172
- { value: "warranty", label: "Warranty", visibleWhen: 'item.category = "electronics"' },
173
- { value: "gift_wrap", label: "Gift Wrap", visibleWhen: 'item.category = "clothing"' },
209
+ {
210
+ value: "warranty",
211
+ label: "Warranty",
212
+ visibleWhen: 'item.category = "electronics"',
213
+ },
214
+ {
215
+ value: "gift_wrap",
216
+ label: "Gift Wrap",
217
+ visibleWhen: 'item.category = "clothing"',
218
+ },
174
219
  { value: "insurance", label: "Insurance" },
175
220
  ],
176
221
  },
@@ -180,19 +225,22 @@ describe("getOptionsVisibility", () => {
180
225
  });
181
226
 
182
227
  const data = {
183
- items: [
184
- { category: "electronics" },
185
- { category: "clothing" },
186
- ],
228
+ items: [{ category: "electronics" }, { category: "clothing" }],
187
229
  };
188
230
 
189
231
  const result = getOptionsVisibility(data, spec);
190
232
 
191
233
  // First item (electronics): warranty + insurance
192
- expect(result["items[0].addon"].map(o => o.value)).toEqual(["warranty", "insurance"]);
234
+ expect(result["items[0].addon"].map((o) => o.value)).toEqual([
235
+ "warranty",
236
+ "insurance",
237
+ ]);
193
238
 
194
239
  // Second item (clothing): gift_wrap + insurance
195
- expect(result["items[1].addon"].map(o => o.value)).toEqual(["gift_wrap", "insurance"]);
240
+ expect(result["items[1].addon"].map((o) => o.value)).toEqual([
241
+ "gift_wrap",
242
+ "insurance",
243
+ ]);
196
244
 
197
245
  // Category field has all options (no visibleWhen)
198
246
  expect(result["items[0].category"]).toHaveLength(2);
@@ -209,7 +257,11 @@ describe("getOptionsVisibility", () => {
209
257
  type: "select",
210
258
  options: [
211
259
  { value: "member", label: "Member" },
212
- { value: "lead", label: "Team Lead", visibleWhen: "itemIndex = 0" },
260
+ {
261
+ value: "lead",
262
+ label: "Team Lead",
263
+ visibleWhen: "itemIndex = 0",
264
+ },
213
265
  ],
214
266
  },
215
267
  },
@@ -224,11 +276,14 @@ describe("getOptionsVisibility", () => {
224
276
  const result = getOptionsVisibility(data, spec);
225
277
 
226
278
  // First item can be lead
227
- expect(result["members[0].role"].map(o => o.value)).toEqual(["member", "lead"]);
279
+ expect(result["members[0].role"].map((o) => o.value)).toEqual([
280
+ "member",
281
+ "lead",
282
+ ]);
228
283
 
229
284
  // Other items can only be member
230
- expect(result["members[1].role"].map(o => o.value)).toEqual(["member"]);
231
- expect(result["members[2].role"].map(o => o.value)).toEqual(["member"]);
285
+ expect(result["members[1].role"].map((o) => o.value)).toEqual(["member"]);
286
+ expect(result["members[2].role"].map((o) => o.value)).toEqual(["member"]);
232
287
  });
233
288
 
234
289
  it("should combine form data with item context", () => {
@@ -242,8 +297,16 @@ describe("getOptionsVisibility", () => {
242
297
  type: "select",
243
298
  options: [
244
299
  { value: "standard", label: "Standard" },
245
- { value: "express", label: "Express", visibleWhen: "isPremium = true" },
246
- { value: "priority", label: "Priority", visibleWhen: 'isPremium = true and item.value > 100' },
300
+ {
301
+ value: "express",
302
+ label: "Express",
303
+ visibleWhen: "isPremium = true",
304
+ },
305
+ {
306
+ value: "priority",
307
+ label: "Priority",
308
+ visibleWhen: "isPremium = true and item.value > 100",
309
+ },
247
310
  ],
248
311
  },
249
312
  },
@@ -252,25 +315,42 @@ describe("getOptionsVisibility", () => {
252
315
  });
253
316
 
254
317
  // Not premium
255
- const basicResult = getOptionsVisibility({
256
- isPremium: false,
257
- orders: [{ value: 50 }, { value: 200 }],
258
- }, spec);
318
+ const basicResult = getOptionsVisibility(
319
+ {
320
+ isPremium: false,
321
+ orders: [{ value: 50 }, { value: 200 }],
322
+ },
323
+ spec,
324
+ );
259
325
 
260
- expect(basicResult["orders[0].shipping"].map(o => o.value)).toEqual(["standard"]);
261
- expect(basicResult["orders[1].shipping"].map(o => o.value)).toEqual(["standard"]);
326
+ expect(basicResult["orders[0].shipping"].map((o) => o.value)).toEqual([
327
+ "standard",
328
+ ]);
329
+ expect(basicResult["orders[1].shipping"].map((o) => o.value)).toEqual([
330
+ "standard",
331
+ ]);
262
332
 
263
333
  // Premium with different order values
264
- const premiumResult = getOptionsVisibility({
265
- isPremium: true,
266
- orders: [{ value: 50 }, { value: 200 }],
267
- }, spec);
334
+ const premiumResult = getOptionsVisibility(
335
+ {
336
+ isPremium: true,
337
+ orders: [{ value: 50 }, { value: 200 }],
338
+ },
339
+ spec,
340
+ );
268
341
 
269
342
  // Low value order: standard + express
270
- expect(premiumResult["orders[0].shipping"].map(o => o.value)).toEqual(["standard", "express"]);
343
+ expect(premiumResult["orders[0].shipping"].map((o) => o.value)).toEqual([
344
+ "standard",
345
+ "express",
346
+ ]);
271
347
 
272
348
  // High value order: standard + express + priority
273
- expect(premiumResult["orders[1].shipping"].map(o => o.value)).toEqual(["standard", "express", "priority"]);
349
+ expect(premiumResult["orders[1].shipping"].map((o) => o.value)).toEqual([
350
+ "standard",
351
+ "express",
352
+ "priority",
353
+ ]);
274
354
  });
275
355
 
276
356
  it("should handle empty arrays", () => {
@@ -291,7 +371,9 @@ describe("getOptionsVisibility", () => {
291
371
  const result = getOptionsVisibility({ items: [] }, spec);
292
372
 
293
373
  // No item paths should exist
294
- expect(Object.keys(result).filter(k => k.startsWith("items["))).toHaveLength(0);
374
+ expect(
375
+ Object.keys(result).filter((k) => k.startsWith("items[")),
376
+ ).toHaveLength(0);
295
377
  });
296
378
 
297
379
  it("should handle missing array data", () => {
@@ -312,7 +394,9 @@ describe("getOptionsVisibility", () => {
312
394
  const result = getOptionsVisibility({}, spec);
313
395
 
314
396
  // No item paths should exist
315
- expect(Object.keys(result).filter(k => k.startsWith("items["))).toHaveLength(0);
397
+ expect(
398
+ Object.keys(result).filter((k) => k.startsWith("items[")),
399
+ ).toHaveLength(0);
316
400
  });
317
401
  });
318
402
 
@@ -324,7 +408,11 @@ describe("getOptionsVisibility", () => {
324
408
  type: "select",
325
409
  options: [
326
410
  { value: "valid", label: "Valid" },
327
- { value: "invalid", label: "Invalid", visibleWhen: "not valid FEEL !!!" },
411
+ {
412
+ value: "invalid",
413
+ label: "Invalid",
414
+ visibleWhen: "not valid FEEL !!!",
415
+ },
328
416
  ],
329
417
  },
330
418
  },
@@ -332,7 +420,7 @@ describe("getOptionsVisibility", () => {
332
420
 
333
421
  const result = getOptionsVisibility({}, spec);
334
422
 
335
- expect(result["status"].map(o => o.value)).toEqual(["valid"]);
423
+ expect(result["status"].map((o) => o.value)).toEqual(["valid"]);
336
424
  });
337
425
  });
338
426
 
@@ -344,7 +432,11 @@ describe("getOptionsVisibility", () => {
344
432
  type: "select",
345
433
  options: [
346
434
  { value: "basic", label: "Basic" },
347
- { value: "enterprise", label: "Enterprise", visibleWhen: "ref.features.enterpriseEnabled = true" },
435
+ {
436
+ value: "enterprise",
437
+ label: "Enterprise",
438
+ visibleWhen: "ref.features.enterpriseEnabled = true",
439
+ },
348
440
  ],
349
441
  },
350
442
  },
@@ -355,7 +447,10 @@ describe("getOptionsVisibility", () => {
355
447
 
356
448
  const result = getOptionsVisibility({}, spec);
357
449
 
358
- expect(result["plan"].map(o => o.value)).toEqual(["basic", "enterprise"]);
450
+ expect(result["plan"].map((o) => o.value)).toEqual([
451
+ "basic",
452
+ "enterprise",
453
+ ]);
359
454
  });
360
455
  });
361
456
  });
@@ -381,15 +476,21 @@ describe("getVisibleOptions", () => {
381
476
  it("should filter options based on visibleWhen", () => {
382
477
  const options: SelectOption[] = [
383
478
  { value: "always", label: "Always" },
384
- { value: "conditional", label: "Conditional", visibleWhen: "show = true" },
479
+ {
480
+ value: "conditional",
481
+ label: "Conditional",
482
+ visibleWhen: "show = true",
483
+ },
385
484
  ];
386
485
  const spec = createTestSpec();
387
486
 
388
- expect(getVisibleOptions(options, { show: false }, spec).map(o => o.value))
389
- .toEqual(["always"]);
487
+ expect(
488
+ getVisibleOptions(options, { show: false }, spec).map((o) => o.value),
489
+ ).toEqual(["always"]);
390
490
 
391
- expect(getVisibleOptions(options, { show: true }, spec).map(o => o.value))
392
- .toEqual(["always", "conditional"]);
491
+ expect(
492
+ getVisibleOptions(options, { show: true }, spec).map((o) => o.value),
493
+ ).toEqual(["always", "conditional"]);
393
494
  });
394
495
 
395
496
  it("should return empty array for undefined/empty options", () => {
@@ -413,7 +514,11 @@ describe("getVisibleOptions", () => {
413
514
  describe("with item context", () => {
414
515
  it("should use item data in expressions", () => {
415
516
  const options: SelectOption[] = [
416
- { value: "warranty", label: "Warranty", visibleWhen: 'item.type = "electronics"' },
517
+ {
518
+ value: "warranty",
519
+ label: "Warranty",
520
+ visibleWhen: 'item.type = "electronics"',
521
+ },
417
522
  { value: "shipping", label: "Shipping" },
418
523
  ];
419
524
  const spec = createTestSpec();
@@ -421,12 +526,15 @@ describe("getVisibleOptions", () => {
421
526
  const electronicsResult = getVisibleOptions(options, {}, spec, {
422
527
  item: { type: "electronics" },
423
528
  });
424
- expect(electronicsResult.map(o => o.value)).toEqual(["warranty", "shipping"]);
529
+ expect(electronicsResult.map((o) => o.value)).toEqual([
530
+ "warranty",
531
+ "shipping",
532
+ ]);
425
533
 
426
534
  const otherResult = getVisibleOptions(options, {}, spec, {
427
535
  item: { type: "clothing" },
428
536
  });
429
- expect(otherResult.map(o => o.value)).toEqual(["shipping"]);
537
+ expect(otherResult.map((o) => o.value)).toEqual(["shipping"]);
430
538
  });
431
539
 
432
540
  it("should use itemIndex in expressions", () => {
@@ -436,11 +544,17 @@ describe("getVisibleOptions", () => {
436
544
  ];
437
545
  const spec = createTestSpec();
438
546
 
439
- expect(getVisibleOptions(options, {}, spec, { itemIndex: 0, item: {} }).map(o => o.value))
440
- .toEqual(["lead", "member"]);
441
-
442
- expect(getVisibleOptions(options, {}, spec, { itemIndex: 1, item: {} }).map(o => o.value))
443
- .toEqual(["member"]);
547
+ expect(
548
+ getVisibleOptions(options, {}, spec, { itemIndex: 0, item: {} }).map(
549
+ (o) => o.value,
550
+ ),
551
+ ).toEqual(["lead", "member"]);
552
+
553
+ expect(
554
+ getVisibleOptions(options, {}, spec, { itemIndex: 1, item: {} }).map(
555
+ (o) => o.value,
556
+ ),
557
+ ).toEqual(["member"]);
444
558
  });
445
559
  });
446
560
 
@@ -453,14 +567,17 @@ describe("getVisibleOptions", () => {
453
567
  ];
454
568
  const spec = createTestSpec();
455
569
 
456
- expect(getVisibleOptions(options, { years: 1 }, spec).map(o => o.value))
457
- .toEqual(["junior"]);
570
+ expect(
571
+ getVisibleOptions(options, { years: 1 }, spec).map((o) => o.value),
572
+ ).toEqual(["junior"]);
458
573
 
459
- expect(getVisibleOptions(options, { years: 5 }, spec).map(o => o.value))
460
- .toEqual(["mid"]);
574
+ expect(
575
+ getVisibleOptions(options, { years: 5 }, spec).map((o) => o.value),
576
+ ).toEqual(["mid"]);
461
577
 
462
- expect(getVisibleOptions(options, { years: 10 }, spec).map(o => o.value))
463
- .toEqual(["senior"]);
578
+ expect(
579
+ getVisibleOptions(options, { years: 10 }, spec).map((o) => o.value),
580
+ ).toEqual(["senior"]);
464
581
  });
465
582
 
466
583
  it("should evaluate string equality", () => {
@@ -470,8 +587,9 @@ describe("getVisibleOptions", () => {
470
587
  ];
471
588
  const spec = createTestSpec();
472
589
 
473
- expect(getVisibleOptions(options, { type: "alpha" }, spec).map(o => o.value))
474
- .toEqual(["a"]);
590
+ expect(
591
+ getVisibleOptions(options, { type: "alpha" }, spec).map((o) => o.value),
592
+ ).toEqual(["a"]);
475
593
  });
476
594
 
477
595
  it("should evaluate boolean expressions", () => {
@@ -481,11 +599,17 @@ describe("getVisibleOptions", () => {
481
599
  ];
482
600
  const spec = createTestSpec();
483
601
 
484
- expect(getVisibleOptions(options, { isPremium: false }, spec).map(o => o.value))
485
- .toEqual(["basic"]);
486
-
487
- expect(getVisibleOptions(options, { isPremium: true }, spec).map(o => o.value))
488
- .toEqual(["premium", "basic"]);
602
+ expect(
603
+ getVisibleOptions(options, { isPremium: false }, spec).map(
604
+ (o) => o.value,
605
+ ),
606
+ ).toEqual(["basic"]);
607
+
608
+ expect(
609
+ getVisibleOptions(options, { isPremium: true }, spec).map(
610
+ (o) => o.value,
611
+ ),
612
+ ).toEqual(["premium", "basic"]);
489
613
  });
490
614
  });
491
615
 
@@ -497,11 +621,13 @@ describe("getVisibleOptions", () => {
497
621
  ];
498
622
  const spec = createTestSpec();
499
623
 
500
- expect(getVisibleOptions(options, { level: 1 }, spec).map(o => o.value))
501
- .toEqual([1]);
624
+ expect(
625
+ getVisibleOptions(options, { level: 1 }, spec).map((o) => o.value),
626
+ ).toEqual([1]);
502
627
 
503
- expect(getVisibleOptions(options, { level: 2 }, spec).map(o => o.value))
504
- .toEqual([1, 2]);
628
+ expect(
629
+ getVisibleOptions(options, { level: 2 }, spec).map((o) => o.value),
630
+ ).toEqual([1, 2]);
505
631
  });
506
632
 
507
633
  it("should preserve option order", () => {
@@ -512,19 +638,25 @@ describe("getVisibleOptions", () => {
512
638
  ];
513
639
  const spec = createTestSpec();
514
640
 
515
- expect(getVisibleOptions(options, { show: true }, spec).map(o => o.value))
516
- .toEqual(["z", "a", "m"]);
641
+ expect(
642
+ getVisibleOptions(options, { show: true }, spec).map((o) => o.value),
643
+ ).toEqual(["z", "a", "m"]);
517
644
  });
518
645
 
519
646
  it("should hide options with invalid FEEL", () => {
520
647
  const options: SelectOption[] = [
521
648
  { value: "valid", label: "Valid" },
522
- { value: "invalid", label: "Invalid", visibleWhen: "this is broken !!!" },
649
+ {
650
+ value: "invalid",
651
+ label: "Invalid",
652
+ visibleWhen: "this is broken !!!",
653
+ },
523
654
  ];
524
655
  const spec = createTestSpec();
525
656
 
526
- expect(getVisibleOptions(options, {}, spec).map(o => o.value))
527
- .toEqual(["valid"]);
657
+ expect(getVisibleOptions(options, {}, spec).map((o) => o.value)).toEqual([
658
+ "valid",
659
+ ]);
528
660
  });
529
661
  });
530
662
  });
@@ -48,7 +48,7 @@ import type {
48
48
  */
49
49
  export function calculate(
50
50
  data: Record<string, unknown>,
51
- spec: Forma
51
+ spec: Forma,
52
52
  ): Record<string, unknown> {
53
53
  const result = calculateWithErrors(data, spec);
54
54
  return result.values;
@@ -65,7 +65,7 @@ export function calculate(
65
65
  */
66
66
  export function calculateWithErrors(
67
67
  data: Record<string, unknown>,
68
- spec: Forma
68
+ spec: Forma,
69
69
  ): CalculationResult {
70
70
  if (!spec.computed) {
71
71
  return { values: {}, errors: [] };
@@ -87,7 +87,7 @@ export function calculateWithErrors(
87
87
  fieldDef,
88
88
  data,
89
89
  values, // Pass already-computed values for dependencies
90
- spec.referenceData // Pass reference data for lookups
90
+ spec.referenceData, // Pass reference data for lookups
91
91
  );
92
92
 
93
93
  if (result.success) {
@@ -130,7 +130,7 @@ function evaluateComputedField(
130
130
  fieldDef: ComputedField,
131
131
  data: Record<string, unknown>,
132
132
  computedSoFar: Record<string, unknown>,
133
- referenceData?: Record<string, unknown>
133
+ referenceData?: Record<string, unknown>,
134
134
  ): ComputeResult {
135
135
  // Check if any referenced computed field is null - propagate null to dependents
136
136
  // This prevents issues like: bmi is null, but bmiCategory still evaluates to "obese"
@@ -162,7 +162,7 @@ function evaluateComputedField(
162
162
 
163
163
  // Treat NaN and Infinity as null - prevents unexpected behavior in conditional expressions
164
164
  // (e.g., NaN < 18.5 is false, causing fallthrough in if-else chains)
165
- if (typeof result.value === "number" && (!Number.isFinite(result.value))) {
165
+ if (typeof result.value === "number" && !Number.isFinite(result.value)) {
166
166
  return {
167
167
  success: true,
168
168
  value: null,
@@ -200,14 +200,17 @@ function findComputedReferences(expression: string): string[] {
200
200
  * evaluated first.
201
201
  */
202
202
  function getComputationOrder(
203
- computed: Record<string, ComputedField>
203
+ computed: Record<string, ComputedField>,
204
204
  ): string[] {
205
205
  const fieldNames = Object.keys(computed);
206
206
 
207
207
  // Build dependency graph
208
208
  const deps = new Map<string, Set<string>>();
209
209
  for (const name of fieldNames) {
210
- deps.set(name, findComputedDependencies(computed[name].expression, fieldNames));
210
+ deps.set(
211
+ name,
212
+ findComputedDependencies(computed[name].expression, fieldNames),
213
+ );
211
214
  }
212
215
 
213
216
  // Topological sort
@@ -249,7 +252,7 @@ function getComputationOrder(
249
252
  */
250
253
  function findComputedDependencies(
251
254
  expression: string,
252
- availableFields: string[]
255
+ availableFields: string[],
253
256
  ): Set<string> {
254
257
  const deps = new Set<string>();
255
258
 
@@ -283,7 +286,7 @@ export function getFormattedValue(
283
286
  fieldName: string,
284
287
  data: Record<string, unknown>,
285
288
  spec: Forma,
286
- options?: FormatOptions
289
+ options?: FormatOptions,
287
290
  ): string | null {
288
291
  if (!spec.computed?.[fieldName]) {
289
292
  return null;
@@ -319,7 +322,7 @@ export function getFormattedValue(
319
322
  export function calculateField(
320
323
  fieldName: string,
321
324
  data: Record<string, unknown>,
322
- spec: Forma
325
+ spec: Forma,
323
326
  ): unknown {
324
327
  const computed = calculate(data, spec);
325
328
  return computed[fieldName] ?? null;