@nmnmcc/ability 2.0.7 → 2.1.1

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 (86) hide show
  1. package/README.md +50 -27
  2. package/dist/Authorizer.d.ts +21 -12
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js.map +1 -1
  5. package/dist/Condition.d.ts +116 -16
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +27 -18
  8. package/dist/Condition.js.map +1 -1
  9. package/dist/Domain.d.ts +9 -0
  10. package/dist/Domain.d.ts.map +1 -1
  11. package/dist/Domain.js +5 -0
  12. package/dist/Domain.js.map +1 -1
  13. package/dist/Limits.d.ts +44 -13
  14. package/dist/Limits.d.ts.map +1 -1
  15. package/dist/Limits.js +52 -22
  16. package/dist/Limits.js.map +1 -1
  17. package/dist/Policy.d.ts +95 -12
  18. package/dist/Policy.d.ts.map +1 -1
  19. package/dist/Policy.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/internal/algebra.js +2 -2
  22. package/dist/internal/analysis.js +81 -75
  23. package/dist/internal/analysis.js.map +1 -1
  24. package/dist/internal/authorizer.d.ts.map +1 -1
  25. package/dist/internal/authorizer.js +11 -7
  26. package/dist/internal/authorizer.js.map +1 -1
  27. package/dist/internal/bind.js +86 -64
  28. package/dist/internal/bind.js.map +1 -1
  29. package/dist/internal/catalog.d.ts +33 -10
  30. package/dist/internal/catalog.d.ts.map +1 -1
  31. package/dist/internal/catalog.js +147 -72
  32. package/dist/internal/catalog.js.map +1 -1
  33. package/dist/internal/decide.js +25 -29
  34. package/dist/internal/decide.js.map +1 -1
  35. package/dist/internal/domain.js +19 -5
  36. package/dist/internal/domain.js.map +1 -1
  37. package/dist/internal/json.js +120 -76
  38. package/dist/internal/json.js.map +1 -1
  39. package/dist/internal/limits.d.ts +28 -0
  40. package/dist/internal/limits.d.ts.map +1 -1
  41. package/dist/internal/limits.js +75 -16
  42. package/dist/internal/limits.js.map +1 -1
  43. package/dist/internal/nodes.d.ts.map +1 -1
  44. package/dist/internal/nodes.js +53 -44
  45. package/dist/internal/nodes.js.map +1 -1
  46. package/dist/internal/patterns.js +50 -15
  47. package/dist/internal/patterns.js.map +1 -1
  48. package/dist/internal/policy-json.js +169 -127
  49. package/dist/internal/policy-json.js.map +1 -1
  50. package/dist/internal/policy-nodes.d.ts.map +1 -1
  51. package/dist/internal/policy-nodes.js +16 -13
  52. package/dist/internal/policy-nodes.js.map +1 -1
  53. package/dist/internal/policy.d.ts.map +1 -1
  54. package/dist/internal/policy.js +38 -22
  55. package/dist/internal/policy.js.map +1 -1
  56. package/dist/internal/resource.js +1 -1
  57. package/dist/internal/rule.d.ts.map +1 -1
  58. package/dist/internal/rule.js +6 -5
  59. package/dist/internal/rule.js.map +1 -1
  60. package/dist/internal/search.d.ts.map +1 -1
  61. package/dist/internal/search.js +106 -92
  62. package/dist/internal/search.js.map +1 -1
  63. package/dist/internal/trampoline.js +19 -5
  64. package/dist/internal/trampoline.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/Authorizer.ts +30 -20
  67. package/src/Condition.ts +308 -83
  68. package/src/Domain.ts +9 -0
  69. package/src/Limits.ts +69 -29
  70. package/src/Policy.ts +131 -15
  71. package/src/internal/analysis.ts +154 -124
  72. package/src/internal/authorizer.ts +29 -14
  73. package/src/internal/bind.ts +193 -136
  74. package/src/internal/catalog.ts +295 -112
  75. package/src/internal/decide.ts +58 -49
  76. package/src/internal/domain.ts +39 -3
  77. package/src/internal/json.ts +212 -117
  78. package/src/internal/limits.ts +101 -31
  79. package/src/internal/nodes.ts +65 -47
  80. package/src/internal/patterns.ts +72 -26
  81. package/src/internal/policy-json.ts +266 -197
  82. package/src/internal/policy-nodes.ts +31 -23
  83. package/src/internal/policy.ts +63 -23
  84. package/src/internal/rule.ts +16 -3
  85. package/src/internal/search.ts +186 -136
  86. package/src/internal/trampoline.ts +51 -4
@@ -17,6 +17,7 @@ import * as Nodes from "./nodes.ts"
17
17
  import * as Patterns from "./patterns.ts"
18
18
  import * as Place from "./place.ts"
19
19
  import { withSuggestion } from "./suggest.ts"
20
+ import * as Trampoline from "./trampoline.ts"
20
21
 
21
22
  /** One problem of a rule or a policy, with its path. */
22
23
  export interface Issue {
@@ -54,13 +55,16 @@ interface Memo {
54
55
  readonly issues: { count: number; noted: boolean }
55
56
  /** The steps of the checks of patterns so far. It belongs to one binding. */
56
57
  readonly work: { steps: number; noted: boolean }
58
+ /** The limits of the domain. */
59
+ readonly limits: Limits.Limits
57
60
  }
58
61
 
59
- const makeMemo = (): Memo => ({
62
+ const makeMemo = (limits: Limits.Limits): Memo => ({
60
63
  suggestions: new Map(),
61
64
  fits: new Map(),
62
65
  issues: { count: 0, noted: false },
63
66
  work: { steps: 0, noted: false },
67
+ limits,
64
68
  })
65
69
 
66
70
  /**
@@ -164,13 +168,23 @@ const fieldIssue = (
164
168
  field: string,
165
169
  at: ReadonlyArray<PropertyKey>,
166
170
  ): ReadonlyArray<Issue> => {
171
+ const parts = context.memo.limits.fieldPathParts
172
+ if (field.split(".", parts + 1).length > parts) {
173
+ return [
174
+ {
175
+ path: at,
176
+ issue: `The field path "${field}" has more than ${String(parts)} parts. The most is ${String(parts)}.`,
177
+ },
178
+ ]
179
+ }
167
180
  const message = `The field "${field}" is not a field of every resource of the rule`
168
181
  if (context.all || context.named.length === 0) {
169
182
  return [{ path: at, issue: message }]
170
183
  }
171
184
  const without = Arr.filter(
172
185
  context.named,
173
- (descriptor) => Catalog.lookup(descriptor.catalog, field) === undefined,
186
+ (descriptor) =>
187
+ Catalog.lookup(descriptor.catalog, field, parts) === undefined,
174
188
  )
175
189
  return without.length === 0
176
190
  ? []
@@ -203,7 +217,14 @@ const readIssues = (
203
217
  }
204
218
  return Arr.some(
205
219
  context.named,
206
- (descriptor) => !(Catalog.lookup(descriptor.catalog, field)?.plain ?? true),
220
+ (descriptor) =>
221
+ !(
222
+ Catalog.lookup(
223
+ descriptor.catalog,
224
+ field,
225
+ context.memo.limits.fieldPathParts,
226
+ )?.plain ?? true
227
+ ),
207
228
  )
208
229
  ? [
209
230
  {
@@ -225,7 +246,7 @@ const patternIssues = (
225
246
  pattern: string,
226
247
  at: ReadonlyArray<PropertyKey>,
227
248
  ): ReadonlyArray<Issue> =>
228
- Result.match(Patterns.parse(pattern), {
249
+ Result.match(Patterns.parse(pattern, memo.limits.patternParts), {
229
250
  onFailure: (issue) => [{ path: at, issue }],
230
251
  onSuccess: (parsed) => {
231
252
  // A rule with no known resource has an issue for that already. The
@@ -238,22 +259,27 @@ const patternIssues = (
238
259
  const key = `${descriptor.kind}\u0000${pattern}`
239
260
  return (
240
261
  Option.getOrElse(Option.fromUndefinedOr(memo.fits.get(key)), () => {
241
- // Each check of a pattern reads the fields of a catalog. The
262
+ // Each check of a pattern reads the steps of a catalog. The
242
263
  // work of one binding has a limit, so very many patterns stop
243
264
  // with one issue, and the policy does not load.
244
265
  memo.work.steps +=
245
- descriptor.catalog.fields.length * Math.max(parsed.length, 1)
246
- if (memo.work.steps > Limits.searchSteps) {
266
+ Math.max(descriptor.catalog.edges, 1) *
267
+ Math.max(parsed.length, 1)
268
+ if (memo.work.steps > memo.limits.searchSteps) {
247
269
  return true
248
270
  }
249
- const found = Patterns.fitsCatalog(parsed, descriptor.catalog)
271
+ const found = Patterns.fitsCatalog(
272
+ parsed,
273
+ descriptor.catalog,
274
+ memo.limits.fieldPathParts,
275
+ )
250
276
  memo.fits.set(key, found)
251
277
  return found
252
278
  }) || before
253
279
  )
254
280
  })
255
281
  // Past the limit of the work, `ruleIssues` gives one issue for it.
256
- return fits || memo.work.steps > Limits.searchSteps
282
+ return fits || memo.work.steps > memo.limits.searchSteps
257
283
  ? []
258
284
  : [
259
285
  {
@@ -270,7 +296,11 @@ const fieldsAt = (
270
296
  field: string,
271
297
  ): ReadonlyArray<Catalog.Field> =>
272
298
  Arr.flatMap(context.applicable, (descriptor) => {
273
- const found = Catalog.lookup(descriptor.catalog, field)
299
+ const found = Catalog.lookup(
300
+ descriptor.catalog,
301
+ field,
302
+ context.memo.limits.fieldPathParts,
303
+ )
274
304
  return found === undefined ? [] : [found]
275
305
  })
276
306
 
@@ -280,7 +310,11 @@ const principalField = (
280
310
  ref: string,
281
311
  at: ReadonlyArray<PropertyKey>,
282
312
  ): Catalog.Field | Issue => {
283
- const found = Catalog.lookup(context.domain.principalCatalog, ref)
313
+ const found = Catalog.lookup(
314
+ context.domain.principalCatalog,
315
+ ref,
316
+ context.memo.limits.fieldPathParts,
317
+ )
284
318
  // A read of the principal that is not plain data is a defect, as a read
285
319
  // of a resource is: see `readIssues`.
286
320
  if (found !== undefined && !found.plain) {
@@ -333,7 +367,7 @@ const distinctFields = (
333
367
  */
334
368
  const spendChecks = (memo: Memo, values: number, fields: number): boolean => {
335
369
  memo.work.steps += values * Math.max(fields, 1)
336
- return memo.work.steps <= Limits.searchSteps
370
+ return memo.work.steps <= memo.limits.searchSteps
337
371
  }
338
372
 
339
373
  /** The issues of one operand of a test of a field, with its distinct fields. */
@@ -490,8 +524,11 @@ const notWhole = (at: ReadonlyArray<PropertyKey>): Issue => ({
490
524
  */
491
525
  const maxIssues = 100
492
526
 
493
- /** A path that the walk makes only when it needs it, for an issue. */
494
- type LazyPath = () => ReadonlyArray<PropertyKey>
527
+ /**
528
+ * The place of a part of a policy, which a walk turns into a full path only
529
+ * for an issue: see `Place`.
530
+ */
531
+ type At = Place.Place | undefined
495
532
 
496
533
  /**
497
534
  * Puts the path of a node before the paths of its issues. A walk keeps no
@@ -499,7 +536,7 @@ type LazyPath = () => ReadonlyArray<PropertyKey>
499
536
  */
500
537
  const under = (
501
538
  memo: Memo,
502
- at: LazyPath,
539
+ at: At,
503
540
  issues: ReadonlyArray<Issue>,
504
541
  ): ReadonlyArray<Issue> => {
505
542
  if (issues.length === 0) {
@@ -508,7 +545,7 @@ const under = (
508
545
  const room = maxIssues - memo.issues.count
509
546
  memo.issues.count += issues.length
510
547
  const kept = Arr.map(Arr.take(issues, Math.max(room, 0)), (issue) => ({
511
- path: [...at(), ...issue.path],
548
+ path: [...Place.pathOf(at), ...issue.path],
512
549
  issue: issue.issue,
513
550
  }))
514
551
  const first = memo.issues.count > maxIssues && !memo.issues.noted
@@ -533,86 +570,95 @@ const under = (
533
570
  */
534
571
  const conditionIssues = (
535
572
  context: Context,
536
- node: Nodes.Node,
537
- at: LazyPath,
573
+ root: Nodes.Node,
574
+ rootAt: At,
538
575
  seen: Set<Nodes.Node>,
539
- ): ReadonlyArray<Issue> => {
540
- if (seen.has(node)) {
541
- return []
542
- }
543
- seen.add(node)
544
- const here = (issues: ReadonlyArray<Issue>) => under(context.memo, at, issues)
545
- // A test looks its field up in each resource of the rule. A rule for very
546
- // many kinds with very many tests is very much work, so it counts. Past
547
- // the limit, `ruleIssues` gives one issue for it, and the policy does not
548
- // load.
549
- const room = (values: number) =>
550
- spendChecks(context.memo, 1 + values, context.named.length)
551
- switch (node._tag) {
552
- case "Compare": {
553
- if (!room(1)) {
554
- return []
555
- }
556
- const exists = readIssues(context, node.field, ["field"])
557
- return here(
558
- exists.length > 0
559
- ? exists
560
- : operandIssues(
561
- context,
562
- node.field,
563
- distinctFields(context, node.field),
564
- node.op !== "eq" && node.op !== "ne",
565
- node.value,
566
- ["value"],
576
+ ): ReadonlyArray<Issue> =>
577
+ // The walk takes the steps of a trampoline, so a deep condition does not
578
+ // fill the stack.
579
+ Trampoline.run(root.depth, (R) => {
580
+ // A test looks its field up in each resource of the rule. A rule for
581
+ // very many kinds with very many tests is very much work, so it counts.
582
+ // Past the limit, `ruleIssues` gives one issue for it, and the policy
583
+ // does not load.
584
+ const room = (values: number) =>
585
+ spendChecks(context.memo, 1 + values, context.named.length)
586
+ const leaf = (node: Nodes.Node, at: At): ReadonlyArray<Issue> => {
587
+ const here = (issues: ReadonlyArray<Issue>) =>
588
+ under(context.memo, at, issues)
589
+ switch (node._tag) {
590
+ case "Compare": {
591
+ if (!room(1)) {
592
+ return []
593
+ }
594
+ const exists = readIssues(context, node.field, ["field"])
595
+ return here(
596
+ exists.length > 0
597
+ ? exists
598
+ : operandIssues(
599
+ context,
600
+ node.field,
601
+ distinctFields(context, node.field),
602
+ node.op !== "eq" && node.op !== "ne",
603
+ node.value,
604
+ ["value"],
605
+ ),
606
+ )
607
+ }
608
+ case "In": {
609
+ if (!room(0)) {
610
+ return []
611
+ }
612
+ const exists = readIssues(context, node.field, ["field"])
613
+ if (exists.length > 0) {
614
+ return here(exists)
615
+ }
616
+ const fields = distinctFields(context, node.field)
617
+ if (!spendChecks(context.memo, node.values.length, fields.length)) {
618
+ return []
619
+ }
620
+ return here(
621
+ Arr.flatMap(node.values, (operand, index) =>
622
+ operandIssues(context, node.field, fields, false, operand, [
623
+ "values",
624
+ index,
625
+ ]),
567
626
  ),
568
- )
569
- }
570
- case "In": {
571
- if (!room(0)) {
572
- return []
627
+ )
628
+ }
629
+ case "Exists":
630
+ return room(0) ? here(readIssues(context, node.field, ["field"])) : []
631
+ case "Under":
632
+ return here(underIssues(context, node.prefix, []))
633
+ default:
634
+ return []
573
635
  }
574
- const exists = readIssues(context, node.field, ["field"])
575
- if (exists.length > 0) {
576
- return here(exists)
636
+ }
637
+ const go = (
638
+ node: Nodes.Node,
639
+ at: At,
640
+ ): Trampoline.Step<ReadonlyArray<Issue>> => {
641
+ if (seen.has(node)) {
642
+ return R.succeed([])
577
643
  }
578
- const fields = distinctFields(context, node.field)
579
- if (!spendChecks(context.memo, node.values.length, fields.length)) {
580
- return []
644
+ seen.add(node)
645
+ switch (node._tag) {
646
+ case "And":
647
+ case "Or":
648
+ return R.map(
649
+ R.forEach(node.conditions, (child, index) =>
650
+ R.suspend(() => go(child, Place.at(at, "conditions", index))),
651
+ ),
652
+ Arr.flatten,
653
+ )
654
+ case "Not":
655
+ return R.suspend(() => go(node.condition, Place.at(at, "condition")))
656
+ default:
657
+ return R.succeed(leaf(node, at))
581
658
  }
582
- return here(
583
- Arr.flatMap(node.values, (operand, index) =>
584
- operandIssues(context, node.field, fields, false, operand, [
585
- "values",
586
- index,
587
- ]),
588
- ),
589
- )
590
659
  }
591
- case "Exists":
592
- return room(0) ? here(readIssues(context, node.field, ["field"])) : []
593
- case "Under":
594
- return here(underIssues(context, node.prefix, []))
595
- case "And":
596
- case "Or":
597
- return Arr.flatMap(node.conditions, (child, index) =>
598
- conditionIssues(
599
- context,
600
- child,
601
- () => [...at(), "conditions", index],
602
- seen,
603
- ),
604
- )
605
- case "Not":
606
- return conditionIssues(
607
- context,
608
- node.condition,
609
- () => [...at(), "condition"],
610
- seen,
611
- )
612
- default:
613
- return []
614
- }
615
- }
660
+ return go(root, rootAt)
661
+ })
616
662
 
617
663
  const hasAction = (
618
664
  descriptor: Resource.Descriptor,
@@ -623,15 +669,15 @@ const hasAction = (
623
669
 
624
670
  /**
625
671
  * Checks one rule against a domain. It gives one issue for each problem, up
626
- * to the most issues of the binding. `at` gives the path of the rule, and it
627
- * is made only for an issue that the binding keeps: a rule with very many
628
- * problems deep in a tree does not make a full path for each one.
672
+ * to the most issues of the binding. `at` is the place of the rule, and its
673
+ * full path is made only for an issue that the binding keeps: a rule with
674
+ * very many problems deep in a tree does not make a full path for each one.
629
675
  */
630
676
  export const ruleIssues = (
631
677
  domain: Domain.Any,
632
678
  rule: RuleData,
633
- at: LazyPath,
634
- memo: Memo = makeMemo(),
679
+ at: At,
680
+ memo: Memo = makeMemo(domain.limits),
635
681
  ): ReadonlyArray<Issue> => {
636
682
  const here = (issues: ReadonlyArray<Issue>) => under(memo, at, issues)
637
683
  const all = rule.resources.includes("*")
@@ -673,21 +719,26 @@ export const ruleIssues = (
673
719
  : fieldIssue(context, field, ["fields", index]),
674
720
  ),
675
721
  )
722
+ const conditionNodes = memo.limits.conditionNodes
676
723
  const whenIssues =
677
724
  rule.when === undefined
678
725
  ? []
679
- : Nodes.isNode(rule.when)
680
- ? conditionIssues(
681
- context,
682
- rule.when,
683
- () => [...at(), "when"],
684
- new Set(),
685
- )
686
- : here([notWhole(["when"])])
726
+ : !Nodes.isNode(rule.when)
727
+ ? here([notWhole(["when"])])
728
+ : // The constructors of the nodes check the ceiling, so the binding
729
+ // checks the limit of the domain.
730
+ rule.when.weight > conditionNodes
731
+ ? here([
732
+ {
733
+ path: ["when"],
734
+ issue: `A condition has ${String(rule.when.weight)} nodes and values. The most is ${String(conditionNodes)}.`,
735
+ },
736
+ ])
737
+ : conditionIssues(context, rule.when, Place.at(at, "when"), new Set())
687
738
  // The work is known only after every check of the rule. Past the limit, a
688
739
  // check gives no issue, so this issue must come, or the policy would load
689
740
  // with checks left out.
690
- const work = memo.work.steps > Limits.searchSteps && !memo.work.noted
741
+ const work = memo.work.steps > memo.limits.searchSteps && !memo.work.noted
691
742
  memo.work.noted ||= work
692
743
  return [
693
744
  ...Arr.flatMap(
@@ -732,8 +783,8 @@ export const ruleIssues = (
732
783
  ...(work
733
784
  ? [
734
785
  {
735
- path: at(),
736
- issue: `The policy takes more than ${String(Limits.searchSteps)} steps to check: its field patterns, and the fields and values of its tests on each resource. The most is ${String(Limits.searchSteps)}.`,
786
+ path: Place.pathOf(at),
787
+ issue: `The policy takes more than ${String(memo.limits.searchSteps)} steps to check: its field patterns, and the fields and values of its tests on each resource. The most is ${String(memo.limits.searchSteps)}.`,
737
788
  },
738
789
  ]
739
790
  : []),
@@ -760,46 +811,52 @@ export const treeIssues = (
760
811
  ): ReadonlyArray<Issue> => {
761
812
  // The rules come out of the tree first, and each one is checked after.
762
813
  // A check of a condition goes down its levels, so a check inside the walk
763
- // of the tree would go down the levels of both.
764
- // The place of each rule shares the path of its parent, and the full path
765
- // is made only when its rule is checked. A deep tree with very many rules
766
- // would need very much memory for a full copy of the path of each rule.
767
- const rulesOf = (
768
- node: TreeNode,
769
- place: Place.Place | undefined,
770
- level: number,
771
- ): ReadonlyArray<Issue | readonly [RuleData, Place.Place]> => {
772
- if (level > Limits.policyDepth) {
773
- return [
774
- {
775
- path: Place.pathOf(place),
776
- issue: `A policy tree has more than ${String(Limits.policyDepth)} levels. The most is ${String(Limits.policyDepth)}.`,
777
- },
778
- ]
779
- }
814
+ // of the tree would go down the levels of both. The place of each rule
815
+ // shares the path of its parent. The tree may be the JSON form, with no
816
+ // depth that a node knows, so the walk is a walk of the trampoline for an
817
+ // input of an unknown depth.
818
+ type Found = Issue | readonly [RuleData, Place.Place]
819
+ const rulesOf = Trampoline.adaptive<
820
+ { readonly node: TreeNode; readonly at: At },
821
+ ReadonlyArray<Found>
822
+ >((R, child) => ({ node, at }, level) => {
780
823
  switch (node._tag) {
781
824
  case "Rules":
782
- return Arr.map(
783
- node.rules,
784
- (rule, index) => [rule, Place.at(place, "rules", index)] as const,
825
+ return R.succeed(
826
+ Arr.map(
827
+ node.rules,
828
+ (rule, index) => [rule, Place.at(at, "rules", index)] as const,
829
+ ),
785
830
  )
786
831
  case "All":
787
832
  case "Any":
788
833
  case "FirstOf":
789
- return Arr.flatMap(node.policies, (child, index) =>
790
- rulesOf(child, Place.at(place, "policies", index), level + 1),
834
+ return R.map(
835
+ R.forEach(node.policies, (inner, index) =>
836
+ child(
837
+ { node: inner, at: Place.at(at, "policies", index) },
838
+ level + 1,
839
+ ),
840
+ ),
841
+ Arr.flatten,
791
842
  )
792
843
  case "Ceiling":
793
- return [
794
- ...rulesOf(node.cap, Place.at(place, "cap"), level + 1),
795
- ...rulesOf(node.policy, Place.at(place, "policy"), level + 1),
796
- ]
844
+ return R.map(
845
+ R.forEach(
846
+ [
847
+ { node: node.cap, at: Place.at(at, "cap") },
848
+ { node: node.policy, at: Place.at(at, "policy") },
849
+ ],
850
+ (part) => child(part, level + 1),
851
+ ),
852
+ Arr.flatten,
853
+ )
797
854
  }
798
- }
799
- const memo = makeMemo()
800
- return Arr.flatMap(rulesOf(root, undefined, 1), (found) =>
855
+ })
856
+ const memo = makeMemo(domain.limits)
857
+ return Arr.flatMap(rulesOf({ node: root, at: undefined }), (found) =>
801
858
  Array.isArray(found)
802
- ? ruleIssues(domain, found[0], () => Place.pathOf(found[1]), memo)
859
+ ? ruleIssues(domain, found[0], found[1], memo)
803
860
  : [found as Issue],
804
861
  )
805
862
  }