unthrown 5.7.0 → 5.9.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.
package/dist/index.mjs CHANGED
@@ -1125,7 +1125,7 @@ function fromThrowable(fn, qualify) {
1125
1125
  return (...args) => {
1126
1126
  try {
1127
1127
  const value = fn(...args);
1128
- return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1128
+ return isThenable(value) ? thenableReturnDefect(value, SYNC_FN_THENABLE) : Ok(value);
1129
1129
  } catch (cause) {
1130
1130
  return qualifyToResult(cause, triage);
1131
1131
  }
@@ -1168,7 +1168,7 @@ function fromSafeThrowable(fn) {
1168
1168
  return (...args) => {
1169
1169
  try {
1170
1170
  const value = fn(...args);
1171
- return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1171
+ return isThenable(value) ? thenableReturnDefect(value, SYNC_FN_THENABLE) : Ok(value);
1172
1172
  } catch (cause) {
1173
1173
  return defectRes(cause);
1174
1174
  }
@@ -1330,30 +1330,46 @@ function qualifyToResult(cause, qualify) {
1330
1330
  }
1331
1331
  }
1332
1332
  /**
1333
- * The Defect minted when a **synchronous** boundary's `fn` returns a thenable —
1334
- * i.e. an `async` function was handed to {@link fromThrowable} /
1335
- * {@link fromSafeThrowable}.
1333
+ * The message for {@link thenableReturnDefect} at a **synchronous boundary** —
1334
+ * an `async` function handed to {@link fromThrowable} / {@link fromSafeThrowable}.
1335
+ *
1336
+ * @internal
1337
+ */
1338
+ const SYNC_FN_THENABLE = "unthrown: fromThrowable/fromSafeThrowable wrap a SYNCHRONOUS function, but `fn` returned a thenable — its rejection would escape qualification. Use fromPromise/fromSafePromise instead.";
1339
+ /**
1340
+ * The message for {@link thenableReturnDefect} in an **accumulating aggregate** —
1341
+ * an `async` `merge` handed to {@link validateAll} and friends.
1342
+ *
1343
+ * @internal
1344
+ */
1345
+ const MERGE_THENABLE = "unthrown: an accumulating aggregate's `merge` must be SYNCHRONOUS, but it returned a thenable — its rejection would escape qualification.";
1346
+ /**
1347
+ * The Defect minted where a callback that must be **synchronous** returned a
1348
+ * thenable: a `fn` handed to {@link fromThrowable} / {@link fromSafeThrowable},
1349
+ * or a `merge` handed to an accumulating aggregate.
1336
1350
  *
1337
1351
  * @remarks
1338
1352
  * This is the sibling of the thenable-`qualify` net in {@link qualifyToResult},
1339
1353
  * and it closes a strictly worse hole. A synchronous boundary only ever sees a
1340
1354
  * synchronous `throw`, so an async `fn`'s rejection never reaches `qualify` at
1341
1355
  * all: it would sit inside `Ok(<Promise>)`, un-triaged, and then float as an
1342
- * unhandled rejection — which terminates the process on Node by default.
1343
- *
1344
- * Unlike the combinator callbacks, this cannot be banned at compile time
1345
- * without collateral damage: `T & NotThenable<T>` on `fn`'s return makes a
1346
- * **generic** function unassignable, so `fromSafeThrowable(structuredClone)`
1347
- * stops compiling and `T` collapses to `unknown`. (The phantom rest-tuple guard
1348
- * `fromPromise` uses fares worse.) So the ban is enforced here, at runtime,
1349
- * where it costs nothing: a Defect, plus adopt-and-silence so the orphaned
1350
- * rejection cannot float.
1356
+ * unhandled rejection — which terminates the process on Node by default. An
1357
+ * async `merge` is the same hole one channel over: `Err(<Promise>)`.
1358
+ *
1359
+ * The `fn` case cannot be banned at compile time without collateral damage:
1360
+ * `T & NotThenable<T>` on `fn`'s return makes a **generic** function
1361
+ * unassignable, so `fromSafeThrowable(structuredClone)` stops compiling and `T`
1362
+ * collapses to `unknown`. (The phantom rest-tuple guard `fromPromise` uses fares
1363
+ * worse.) `merge` *is* `NotThenable`-constrained, but a cast or an untyped
1364
+ * caller still reaches here. Either way the runtime answer is the same, and it
1365
+ * costs nothing: a Defect, plus adopt-and-silence so the orphaned rejection
1366
+ * cannot float.
1351
1367
  *
1352
1368
  * @internal
1353
1369
  */
1354
- function thenableReturnDefect(value) {
1355
- Promise.resolve(value).then(void 0, () => void 0);
1356
- return defectRes(/* @__PURE__ */ new TypeError("unthrown: fromThrowable/fromSafeThrowable wrap a SYNCHRONOUS function, but `fn` returned a thenable — its rejection would escape qualification. Use fromPromise/fromSafePromise instead."));
1370
+ function thenableReturnDefect(value, message) {
1371
+ silenceIfThenable(value);
1372
+ return defectRes(new TypeError(message));
1357
1373
  }
1358
1374
  /**
1359
1375
  * Fold an array of settled `Result`s: first `Err` wins, any `Defect` dominates,
@@ -1365,11 +1381,23 @@ function thenableReturnDefect(value) {
1365
1381
  function nonResultDefect() {
1366
1382
  return defectRes(/* @__PURE__ */ new TypeError("unthrown: aggregate received a non-Result element"));
1367
1383
  }
1368
- function foldArray(results) {
1384
+ /**
1385
+ * Resolve every input concurrently (order preserved), adopting each one
1386
+ * defensively: a cast/untyped rejecting thenable becomes a `Defect` rather than
1387
+ * rejecting the internal promise, so the "an `AsyncResult`'s internal promise
1388
+ * never rejects" invariant holds even for out-of-contract input.
1389
+ *
1390
+ * @internal
1391
+ */
1392
+ function settleAll(results) {
1393
+ return Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause))));
1394
+ }
1395
+ function foldArray(results, merge) {
1369
1396
  let firstErr;
1370
1397
  let firstDefect;
1371
1398
  const values = [];
1372
- for (const r of results) {
1399
+ const errors = [];
1400
+ for (const [i, r] of results.entries()) {
1373
1401
  if (!isResult(r)) {
1374
1402
  firstDefect ??= nonResultDefect();
1375
1403
  break;
@@ -1377,10 +1405,19 @@ function foldArray(results) {
1377
1405
  if (r.tag === "Defect") {
1378
1406
  firstDefect ??= r;
1379
1407
  break;
1380
- } else if (r.tag === "Err") firstErr ??= r;
1408
+ } else if (r.tag === "Err") if (merge) errors.push([i, r.error]);
1409
+ else firstErr ??= r;
1381
1410
  else values.push(r.value);
1382
1411
  }
1383
- return firstDefect ?? firstErr ?? Ok(values);
1412
+ if (firstDefect) return firstDefect;
1413
+ if (merge && errors.length > 0) try {
1414
+ const merged = merge(errors);
1415
+ if (isThenable(merged)) return thenableReturnDefect(merged, MERGE_THENABLE);
1416
+ return Err(merged);
1417
+ } catch (cause) {
1418
+ return defectRes(cause);
1419
+ }
1420
+ return firstErr ?? Ok(values);
1384
1421
  }
1385
1422
  /**
1386
1423
  * Fold a record of settled `Result`s with the same rules, else `Ok` of the
@@ -1398,9 +1435,17 @@ function foldArray(results) {
1398
1435
  *
1399
1436
  * @internal
1400
1437
  */
1401
- function foldRecord(results) {
1438
+ function foldRecord(results, merge) {
1402
1439
  const keys = Object.keys(results);
1403
- return foldArray(Object.values(results)).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
1440
+ return foldArray(Object.values(results), merge && ((errors) => merge(nameErrors(errors, keys)))).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
1441
+ }
1442
+ /** Drop the accumulated indices — the positional forms merge errors alone. @internal */
1443
+ function stripIndices(errors) {
1444
+ return errors.map(([, e]) => e);
1445
+ }
1446
+ /** Pair each accumulated index back onto its key. @internal */
1447
+ function nameErrors(errors, keys) {
1448
+ return errors.map(([i, e]) => [keys[i], e]);
1404
1449
  }
1405
1450
  /**
1406
1451
  * Collect a tuple/array of {@link Result}s into a single `Result` of all their
@@ -1412,7 +1457,8 @@ function foldRecord(results) {
1412
1457
  * `Err`. A **fixed tuple** keeps its positional types — `all([Ok(1), Ok("a")])`
1413
1458
  * is `Result<[number, string], …>` — while a **dynamic array** `Result<T, E>[]`
1414
1459
  * collapses to `Result<T[], E>` with no cast. For a **record** keyed by name,
1415
- * use {@link allFromDict}.
1460
+ * use {@link allFromDict}. To report **every** `Err` instead of only the first,
1461
+ * use {@link validateAll}.
1416
1462
  *
1417
1463
  * @category Aggregate
1418
1464
  *
@@ -1435,7 +1481,9 @@ function all(results) {
1435
1481
  *
1436
1482
  * @remarks
1437
1483
  * Same folding rules as {@link all}: first `Err` short-circuits, any `Defect`
1438
- * dominates. This is **not** error accumulation.
1484
+ * dominates. This is **not** error accumulation — for that, reach for
1485
+ * {@link validateAllFromDict}, which accumulates every `Err` and folds them into
1486
+ * one modeled error.
1439
1487
  *
1440
1488
  * @category Aggregate
1441
1489
  *
@@ -1458,7 +1506,8 @@ function allFromDict(results) {
1458
1506
  * The inputs are resolved **concurrently** (order preserved); the resolved
1459
1507
  * `Result`s are then folded with the same rules as {@link all} — first `Err`
1460
1508
  * short-circuits, any `Defect` dominates. As ever, the returned `AsyncResult`'s
1461
- * internal promise never rejects. For a **record**, use {@link allFromDictAsync}.
1509
+ * internal promise never rejects. For a **record**, use {@link allFromDictAsync};
1510
+ * to report **every** `Err`, use {@link validateAllAsync}.
1462
1511
  *
1463
1512
  * @category Aggregate
1464
1513
  *
@@ -1474,7 +1523,7 @@ function allFromDict(results) {
1474
1523
  * ```
1475
1524
  */
1476
1525
  function allAsync(results) {
1477
- return new AsyncRes(Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => foldArray(resolved)));
1526
+ return new AsyncRes(settleAll(results).then((resolved) => foldArray(resolved)));
1478
1527
  }
1479
1528
  /**
1480
1529
  * The asynchronous counterpart of {@link allFromDict}: combine a record of
@@ -1482,7 +1531,8 @@ function allAsync(results) {
1482
1531
  *
1483
1532
  * @remarks
1484
1533
  * Resolved concurrently (order preserved), folded with the {@link all} rules,
1485
- * and the internal promise never rejects.
1534
+ * and the internal promise never rejects. To report **every** `Err`, use
1535
+ * {@link validateAllFromDictAsync}.
1486
1536
  *
1487
1537
  * @category Aggregate
1488
1538
  *
@@ -1499,7 +1549,163 @@ function allAsync(results) {
1499
1549
  */
1500
1550
  function allFromDictAsync(results) {
1501
1551
  const keys = Object.keys(results);
1502
- return new AsyncRes(Promise.all(Object.values(results).map((ar) => Promise.resolve(ar).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => foldArray(resolved).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])))));
1552
+ return new AsyncRes(settleAll(Object.values(results)).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]])))));
1553
+ }
1554
+ /**
1555
+ * Collect a tuple/array of {@link Result}s, **accumulating every** `Err` and
1556
+ * merging them into a single modeled error — the accumulating counterpart of
1557
+ * {@link all}.
1558
+ *
1559
+ * @remarks
1560
+ * Same success channel as {@link all}: a **fixed tuple** keeps its positional
1561
+ * types, a **dynamic array** collapses to `Result<T[], E2>`. The difference is
1562
+ * the error channel — instead of the first `Err` winning, every `Err` is
1563
+ * collected in input order and handed to `merge`, whose return becomes the
1564
+ * modeled error.
1565
+ *
1566
+ * `merge` receives a **non-empty** list, so it is total: it is called only when
1567
+ * at least one `Err` was collected. It is **not** called when every element is
1568
+ * `Ok`, nor when a `Defect` is present.
1569
+ *
1570
+ * Any `Defect` still **dominates** — it wins over the accumulated errors, which
1571
+ * are discarded and never reach `merge`. A defect means something in this batch
1572
+ * failed in a way nobody modeled, so the violations computed alongside it are
1573
+ * not trustworthy. An out-of-contract non-`Result` element becomes a
1574
+ * `TypeError`-caused `Defect` the same way, and a throw inside `merge` becomes
1575
+ * a `Defect` too.
1576
+ *
1577
+ * `merge` must be **synchronous** — an `async` one is a compile error
1578
+ * ({@link NotThenable}), since a `Promise` in `E` is an unqualified rejection.
1579
+ *
1580
+ * For **schema-shaped** input (a request body, a form), reach for
1581
+ * `@unthrown/standard-schema`'s `fromSchema` instead — a validator already
1582
+ * hands you every issue as the modeled error. `validateAll` is for independent
1583
+ * checks you wrote yourself. For a **record** keyed by name, use
1584
+ * {@link validateAllFromDict}.
1585
+ *
1586
+ * @typeParam Rs - the tuple/array of input `Result` types.
1587
+ * @typeParam E2 - the merged error type.
1588
+ * @param results - the results to collect.
1589
+ * @param merge - folds the collected errors into one modeled error.
1590
+ *
1591
+ * @category Aggregate
1592
+ *
1593
+ * @example
1594
+ * ```ts
1595
+ * import { validateAll, Ok, Err } from "unthrown";
1596
+ *
1597
+ * // every Err is collected, not just the first
1598
+ * validateAll([Ok(1), Err("stock"), Err("credit")], (errors) => errors.join(" and "));
1599
+ * // => Err("stock and credit")
1600
+ *
1601
+ * // all-Ok keeps the positional tuple; `merge` never runs
1602
+ * validateAll([Ok(1), Ok("a")], (errors) => errors.join());
1603
+ * // => Ok([1, "a"]) typed Result<[number, string], string>
1604
+ * ```
1605
+ */
1606
+ function validateAll(results, merge) {
1607
+ return foldArray(results, (errors) => merge(stripIndices(errors)));
1608
+ }
1609
+ /**
1610
+ * Collect a **record** of {@link Result}s, accumulating every `Err` — the
1611
+ * accumulating counterpart of {@link allFromDict}, and the named counterpart of
1612
+ * {@link validateAll}.
1613
+ *
1614
+ * @remarks
1615
+ * `merge` receives a non-empty list of **`[key, error]` entries**, correlated
1616
+ * per key: `{ a: Result<A, E1>; b: Result<B, E2> }` yields
1617
+ * `["a", E1] | ["b", E2]`, so a `switch` on the key narrows the error and an
1618
+ * impossible pairing does not typecheck. That is what keeps two checks sharing
1619
+ * one error type distinguishable. Entries come in `Object.keys` order.
1620
+ *
1621
+ * Every other rule matches {@link validateAll}: any `Defect` dominates and
1622
+ * discards the accumulated errors, a throw in `merge` becomes a `Defect`, and
1623
+ * `merge` must be synchronous.
1624
+ *
1625
+ * @typeParam R - the record of input `Result` types.
1626
+ * @typeParam E2 - the merged error type.
1627
+ * @param results - the results to collect, keyed by name.
1628
+ * @param merge - folds the collected `[key, error]` entries into one error.
1629
+ *
1630
+ * @category Aggregate
1631
+ *
1632
+ * @example
1633
+ * ```ts
1634
+ * import { validateAllFromDict, Ok, Err } from "unthrown";
1635
+ *
1636
+ * validateAllFromDict(
1637
+ * { vatRate: Err("out of range"), currency: Ok("EUR"), dueDate: Err("past") },
1638
+ * (entries) => entries.map(([key, error]) => `${key}: ${error}`).join("; "),
1639
+ * );
1640
+ * // => Err("vatRate: out of range; dueDate: past")
1641
+ * ```
1642
+ */
1643
+ function validateAllFromDict(results, merge) {
1644
+ return foldRecord(results, (entries) => merge(entries));
1645
+ }
1646
+ /**
1647
+ * The asynchronous counterpart of {@link validateAll}: collect a tuple/array of
1648
+ * {@link AsyncResult}s, accumulating every `Err` into one merged error.
1649
+ *
1650
+ * @remarks
1651
+ * Every {@link validateAll} rule holds, with the inputs resolved
1652
+ * **concurrently** (order preserved) — as with {@link allAsync}, no work is
1653
+ * short-circuited either way; the fail-fast/accumulating split is purely which
1654
+ * errors get reported. The internal promise never rejects: an out-of-contract
1655
+ * rejecting thenable becomes a dominating `Defect`. `merge` stays synchronous
1656
+ * here too — this is exactly where its rejection would land unqualified in `E`.
1657
+ * For a **record**, use {@link validateAllFromDictAsync}.
1658
+ *
1659
+ * @typeParam Rs - the tuple/array of input `AsyncResult` types.
1660
+ * @typeParam E2 - the merged error type.
1661
+ * @param results - the async results to collect.
1662
+ * @param merge - folds the collected errors into one modeled error.
1663
+ *
1664
+ * @category Aggregate
1665
+ *
1666
+ * @example
1667
+ * ```ts
1668
+ * import { validateAllAsync, OkAsync, ErrAsync } from "unthrown";
1669
+ *
1670
+ * const checked = validateAllAsync(
1671
+ * [OkAsync(1), ErrAsync("stock"), ErrAsync("credit")],
1672
+ * (errors) => errors.join(" and "),
1673
+ * );
1674
+ * // (await checked) => Err("stock and credit")
1675
+ * ```
1676
+ */
1677
+ function validateAllAsync(results, merge) {
1678
+ return new AsyncRes(settleAll(results).then((resolved) => foldArray(resolved, (errors) => merge(stripIndices(errors)))));
1679
+ }
1680
+ /**
1681
+ * The asynchronous counterpart of {@link validateAllFromDict}: collect a record
1682
+ * of {@link AsyncResult}s, accumulating every `Err` into one merged error.
1683
+ *
1684
+ * @remarks
1685
+ * The {@link validateAllFromDict} rules, over inputs resolved concurrently as
1686
+ * in {@link validateAllAsync}.
1687
+ *
1688
+ * @typeParam R - the record of input `AsyncResult` types.
1689
+ * @typeParam E2 - the merged error type.
1690
+ * @param results - the async results to collect, keyed by name.
1691
+ * @param merge - folds the collected `[key, error]` entries into one error.
1692
+ *
1693
+ * @category Aggregate
1694
+ *
1695
+ * @example
1696
+ * ```ts
1697
+ * import { validateAllFromDictAsync, OkAsync, ErrAsync } from "unthrown";
1698
+ *
1699
+ * const checked = validateAllFromDictAsync(
1700
+ * { stock: ErrAsync("none left"), credit: OkAsync(500) },
1701
+ * (entries) => entries.map(([key, error]) => `${key}: ${error}`).join("; "),
1702
+ * );
1703
+ * // (await checked) => Err("stock: none left")
1704
+ * ```
1705
+ */
1706
+ function validateAllFromDictAsync(results, merge) {
1707
+ const keys = Object.keys(results);
1708
+ return new AsyncRes(settleAll(Object.values(results)).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]])), (entries) => merge(entries))));
1503
1709
  }
1504
1710
  //#endregion
1505
1711
  //#region src/facade.ts
@@ -1508,7 +1714,8 @@ function allFromDictAsync(results) {
1508
1714
  * single, discoverable namespace: {@link Result.Ok}, {@link Result.Err},
1509
1715
  * {@link Result.Do}, {@link Result.fromNullable}, {@link Result.fromThrowable},
1510
1716
  * {@link Result.fromSafeThrowable}, {@link Result.all},
1511
- * {@link Result.allFromDict}, {@link Result.isOk}, {@link Result.isErr},
1717
+ * {@link Result.allFromDict}, {@link Result.validateAll},
1718
+ * {@link Result.validateAllFromDict}, {@link Result.isOk}, {@link Result.isErr},
1512
1719
  * {@link Result.isDefect}, {@link Result.isResult}.
1513
1720
  *
1514
1721
  * @remarks
@@ -1538,6 +1745,8 @@ const Result = {
1538
1745
  fromSafeThrowable,
1539
1746
  all,
1540
1747
  allFromDict,
1748
+ validateAll,
1749
+ validateAllFromDict,
1541
1750
  isOk,
1542
1751
  isErr,
1543
1752
  isDefect,
@@ -1548,7 +1757,8 @@ const Result = {
1548
1757
  * the matching namespace: {@link AsyncResult.Ok}, {@link AsyncResult.Err},
1549
1758
  * {@link AsyncResult.Do}, {@link AsyncResult.fromExecutor},
1550
1759
  * {@link AsyncResult.fromPromise}, {@link AsyncResult.fromSafePromise},
1551
- * {@link AsyncResult.all}, {@link AsyncResult.allFromDict}.
1760
+ * {@link AsyncResult.all}, {@link AsyncResult.allFromDict},
1761
+ * {@link AsyncResult.validateAll}, {@link AsyncResult.validateAllFromDict}.
1552
1762
  *
1553
1763
  * @remarks
1554
1764
  * The async sibling of {@link Result}. Statics are grouped by what they
@@ -1559,7 +1769,8 @@ const Result = {
1559
1769
  * functions carry (`AsyncResult.Ok` is `OkAsync`; `AsyncResult.Err` is
1560
1770
  * `ErrAsync`; `AsyncResult.Do` is `DoAsync`; `AsyncResult.all` is `allAsync`;
1561
1771
  * `AsyncResult.allFromDict` is
1562
- * `allFromDictAsync`). Like {@link Result}, the free functions remain the
1772
+ * `allFromDictAsync`; `AsyncResult.validateAll` is `validateAllAsync`). Like
1773
+ * {@link Result}, the free functions remain the
1563
1774
  * primary, tree-shakeable API; the value `AsyncResult` and the type
1564
1775
  * {@link AsyncResult} share one name.
1565
1776
  *
@@ -1583,7 +1794,9 @@ const AsyncResult = {
1583
1794
  fromPromise,
1584
1795
  fromSafePromise,
1585
1796
  all: allAsync,
1586
- allFromDict: allFromDictAsync
1797
+ allFromDict: allFromDictAsync,
1798
+ validateAll: validateAllAsync,
1799
+ validateAllFromDict: validateAllFromDictAsync
1587
1800
  };
1588
1801
  //#endregion
1589
1802
  //#region src/tagged.ts
@@ -1676,4 +1889,4 @@ function TaggedError(tag, options) {
1676
1889
  return TaggedErrorBase;
1677
1890
  }
1678
1891
  //#endregion
1679
- export { AsyncResult, Do, DoAsync, Err, ErrAsync, GetError, NonExhaustiveError, Ok, OkAsync, P, Result, TaggedError, all, allAsync, allFromDict, allFromDictAsync, fromExecutor, fromNullable, fromPromise, fromSafePromise, fromSafeThrowable, fromThrowable, isDefect, isErr, isOk, isResult, match };
1892
+ export { AsyncResult, Do, DoAsync, Err, ErrAsync, GetError, NonExhaustiveError, Ok, OkAsync, P, Result, TaggedError, all, allAsync, allFromDict, allFromDictAsync, fromExecutor, fromNullable, fromPromise, fromSafePromise, fromSafeThrowable, fromThrowable, isDefect, isErr, isOk, isResult, match, validateAll, validateAllAsync, validateAllFromDict, validateAllFromDictAsync };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unthrown",
3
- "version": "5.7.0",
3
+ "version": "5.9.0",
4
4
  "description": "Explicit errors as values, with a separate defect (panic) channel",
5
5
  "keywords": [
6
6
  "defect",