@nmnmcc/ability 2.0.6 → 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 (89) hide show
  1. package/README.md +56 -32
  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 +118 -18
  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 +101 -74
  22. package/dist/internal/algebra.js.map +1 -1
  23. package/dist/internal/analysis.js +81 -75
  24. package/dist/internal/analysis.js.map +1 -1
  25. package/dist/internal/authorizer.d.ts.map +1 -1
  26. package/dist/internal/authorizer.js +16 -11
  27. package/dist/internal/authorizer.js.map +1 -1
  28. package/dist/internal/bind.js +119 -80
  29. package/dist/internal/bind.js.map +1 -1
  30. package/dist/internal/catalog.d.ts +33 -10
  31. package/dist/internal/catalog.d.ts.map +1 -1
  32. package/dist/internal/catalog.js +153 -78
  33. package/dist/internal/catalog.js.map +1 -1
  34. package/dist/internal/decide.js +25 -29
  35. package/dist/internal/decide.js.map +1 -1
  36. package/dist/internal/domain.js +19 -5
  37. package/dist/internal/domain.js.map +1 -1
  38. package/dist/internal/json.js +120 -76
  39. package/dist/internal/json.js.map +1 -1
  40. package/dist/internal/limits.d.ts +28 -0
  41. package/dist/internal/limits.d.ts.map +1 -1
  42. package/dist/internal/limits.js +75 -16
  43. package/dist/internal/limits.js.map +1 -1
  44. package/dist/internal/nodes.d.ts +7 -0
  45. package/dist/internal/nodes.d.ts.map +1 -1
  46. package/dist/internal/nodes.js +86 -47
  47. package/dist/internal/nodes.js.map +1 -1
  48. package/dist/internal/patterns.js +50 -15
  49. package/dist/internal/patterns.js.map +1 -1
  50. package/dist/internal/policy-json.js +169 -127
  51. package/dist/internal/policy-json.js.map +1 -1
  52. package/dist/internal/policy-nodes.d.ts.map +1 -1
  53. package/dist/internal/policy-nodes.js +17 -14
  54. package/dist/internal/policy-nodes.js.map +1 -1
  55. package/dist/internal/policy.d.ts.map +1 -1
  56. package/dist/internal/policy.js +38 -22
  57. package/dist/internal/policy.js.map +1 -1
  58. package/dist/internal/resource.js +1 -1
  59. package/dist/internal/rule.d.ts.map +1 -1
  60. package/dist/internal/rule.js +6 -5
  61. package/dist/internal/rule.js.map +1 -1
  62. package/dist/internal/search.d.ts.map +1 -1
  63. package/dist/internal/search.js +106 -92
  64. package/dist/internal/search.js.map +1 -1
  65. package/dist/internal/trampoline.js +89 -0
  66. package/dist/internal/trampoline.js.map +1 -0
  67. package/package.json +1 -1
  68. package/src/Authorizer.ts +30 -20
  69. package/src/Condition.ts +308 -83
  70. package/src/Domain.ts +9 -0
  71. package/src/Limits.ts +69 -29
  72. package/src/Policy.ts +131 -15
  73. package/src/internal/algebra.ts +218 -135
  74. package/src/internal/analysis.ts +154 -122
  75. package/src/internal/authorizer.ts +34 -16
  76. package/src/internal/bind.ts +279 -148
  77. package/src/internal/catalog.ts +307 -133
  78. package/src/internal/decide.ts +58 -49
  79. package/src/internal/domain.ts +39 -3
  80. package/src/internal/json.ts +212 -117
  81. package/src/internal/limits.ts +101 -31
  82. package/src/internal/nodes.ts +101 -59
  83. package/src/internal/patterns.ts +72 -26
  84. package/src/internal/policy-json.ts +266 -197
  85. package/src/internal/policy-nodes.ts +33 -24
  86. package/src/internal/policy.ts +63 -23
  87. package/src/internal/rule.ts +16 -3
  88. package/src/internal/search.ts +186 -136
  89. package/src/internal/trampoline.ts +194 -0
@@ -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,30 +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
- if (memo.work.steps > Limits.searchSteps && !memo.work.noted) {
256
- memo.work.noted = true
257
- return [
258
- {
259
- path: at,
260
- issue: `The field patterns of the policy take more than ${String(Limits.searchSteps)} steps to check. The most is ${String(Limits.searchSteps)}.`,
261
- },
262
- ]
263
- }
264
- return fits
281
+ // Past the limit of the work, `ruleIssues` gives one issue for it.
282
+ return fits || memo.work.steps > memo.limits.searchSteps
265
283
  ? []
266
284
  : [
267
285
  {
@@ -278,7 +296,11 @@ const fieldsAt = (
278
296
  field: string,
279
297
  ): ReadonlyArray<Catalog.Field> =>
280
298
  Arr.flatMap(context.applicable, (descriptor) => {
281
- const found = Catalog.lookup(descriptor.catalog, field)
299
+ const found = Catalog.lookup(
300
+ descriptor.catalog,
301
+ field,
302
+ context.memo.limits.fieldPathParts,
303
+ )
282
304
  return found === undefined ? [] : [found]
283
305
  })
284
306
 
@@ -288,7 +310,11 @@ const principalField = (
288
310
  ref: string,
289
311
  at: ReadonlyArray<PropertyKey>,
290
312
  ): Catalog.Field | Issue => {
291
- 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
+ )
292
318
  // A read of the principal that is not plain data is a defect, as a read
293
319
  // of a resource is: see `readIssues`.
294
320
  if (found !== undefined && !found.plain) {
@@ -314,15 +340,45 @@ const principalField = (
314
340
  const isIssue = (value: Catalog.Field | Issue): value is Issue =>
315
341
  "issue" in value
316
342
 
317
- /** The issues of one operand of a test of a field. */
343
+ /**
344
+ * The fields that a test reads, with one field for each distinct kind of
345
+ * value. A rule for very many kinds may have one field type in all of them,
346
+ * so each value of the test is checked one time for each distinct kind, not
347
+ * one time for each resource. The kinds are found by their text in a native
348
+ * `Map`, which belongs to this call.
349
+ */
350
+ const distinctFields = (
351
+ context: Context,
352
+ field: string,
353
+ ): ReadonlyArray<Catalog.Field> =>
354
+ Array.from(
355
+ new Map(
356
+ Arr.map(
357
+ fieldsAt(context, field),
358
+ (found) => [JSON.stringify(found.kinds), found] as const,
359
+ ),
360
+ ).values(),
361
+ )
362
+
363
+ /**
364
+ * Counts the work of the checks of the values of a test, as the work of the
365
+ * field patterns counts. Past the limit, the values are not checked, and
366
+ * `ruleIssues` gives one issue for the work.
367
+ */
368
+ const spendChecks = (memo: Memo, values: number, fields: number): boolean => {
369
+ memo.work.steps += values * Math.max(fields, 1)
370
+ return memo.work.steps <= memo.limits.searchSteps
371
+ }
372
+
373
+ /** The issues of one operand of a test of a field, with its distinct fields. */
318
374
  const operandIssues = (
319
375
  context: Context,
320
376
  field: string,
377
+ fields: ReadonlyArray<Catalog.Field>,
321
378
  ordered: boolean,
322
379
  operand: Operand,
323
380
  at: ReadonlyArray<PropertyKey>,
324
381
  ): ReadonlyArray<Issue> => {
325
- const fields = fieldsAt(context, field)
326
382
  if (operand._tag === "Literal") {
327
383
  const value = operand.value
328
384
  if (ordered && typeof value !== "string" && typeof value !== "number") {
@@ -468,8 +524,11 @@ const notWhole = (at: ReadonlyArray<PropertyKey>): Issue => ({
468
524
  */
469
525
  const maxIssues = 100
470
526
 
471
- /** A path that the walk makes only when it needs it, for an issue. */
472
- 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
473
532
 
474
533
  /**
475
534
  * Puts the path of a node before the paths of its issues. A walk keeps no
@@ -477,7 +536,7 @@ type LazyPath = () => ReadonlyArray<PropertyKey>
477
536
  */
478
537
  const under = (
479
538
  memo: Memo,
480
- at: LazyPath,
539
+ at: At,
481
540
  issues: ReadonlyArray<Issue>,
482
541
  ): ReadonlyArray<Issue> => {
483
542
  if (issues.length === 0) {
@@ -486,7 +545,7 @@ const under = (
486
545
  const room = maxIssues - memo.issues.count
487
546
  memo.issues.count += issues.length
488
547
  const kept = Arr.map(Arr.take(issues, Math.max(room, 0)), (issue) => ({
489
- path: [...at(), ...issue.path],
548
+ path: [...Place.pathOf(at), ...issue.path],
490
549
  issue: issue.issue,
491
550
  }))
492
551
  const first = memo.issues.count > maxIssues && !memo.issues.noted
@@ -511,68 +570,95 @@ const under = (
511
570
  */
512
571
  const conditionIssues = (
513
572
  context: Context,
514
- node: Nodes.Node,
515
- at: LazyPath,
573
+ root: Nodes.Node,
574
+ rootAt: At,
516
575
  seen: Set<Nodes.Node>,
517
- ): ReadonlyArray<Issue> => {
518
- if (seen.has(node)) {
519
- return []
520
- }
521
- seen.add(node)
522
- const here = (issues: ReadonlyArray<Issue>) => under(context.memo, at, issues)
523
- switch (node._tag) {
524
- case "Compare": {
525
- const exists = readIssues(context, node.field, ["field"])
526
- return here(
527
- exists.length > 0
528
- ? exists
529
- : operandIssues(
530
- context,
531
- node.field,
532
- node.op !== "eq" && node.op !== "ne",
533
- node.value,
534
- ["value"],
535
- ),
536
- )
537
- }
538
- case "In": {
539
- const exists = readIssues(context, node.field, ["field"])
540
- return here(
541
- exists.length > 0
542
- ? exists
543
- : Arr.flatMap(node.values, (operand, index) =>
544
- operandIssues(context, node.field, false, operand, [
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, [
545
623
  "values",
546
624
  index,
547
625
  ]),
548
626
  ),
549
- )
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 []
635
+ }
550
636
  }
551
- case "Exists":
552
- return here(readIssues(context, node.field, ["field"]))
553
- case "Under":
554
- return here(underIssues(context, node.prefix, []))
555
- case "And":
556
- case "Or":
557
- return Arr.flatMap(node.conditions, (child, index) =>
558
- conditionIssues(
559
- context,
560
- child,
561
- () => [...at(), "conditions", index],
562
- seen,
563
- ),
564
- )
565
- case "Not":
566
- return conditionIssues(
567
- context,
568
- node.condition,
569
- () => [...at(), "condition"],
570
- seen,
571
- )
572
- default:
573
- return []
574
- }
575
- }
637
+ const go = (
638
+ node: Nodes.Node,
639
+ at: At,
640
+ ): Trampoline.Step<ReadonlyArray<Issue>> => {
641
+ if (seen.has(node)) {
642
+ return R.succeed([])
643
+ }
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))
658
+ }
659
+ }
660
+ return go(root, rootAt)
661
+ })
576
662
 
577
663
  const hasAction = (
578
664
  descriptor: Resource.Descriptor,
@@ -582,15 +668,18 @@ const hasAction = (
582
668
  Arr.some(actions, (action) => descriptor.actions.includes(action))
583
669
 
584
670
  /**
585
- * Checks one rule against a domain. It gives one issue for each problem. `at`
586
- * is the path of the rule, for the paths of the issues.
671
+ * Checks one rule against a domain. It gives one issue for each problem, up
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.
587
675
  */
588
676
  export const ruleIssues = (
589
677
  domain: Domain.Any,
590
678
  rule: RuleData,
591
- at: ReadonlyArray<PropertyKey>,
592
- memo: Memo = makeMemo(),
679
+ at: At,
680
+ memo: Memo = makeMemo(domain.limits),
593
681
  ): ReadonlyArray<Issue> => {
682
+ const here = (issues: ReadonlyArray<Issue>) => under(memo, at, issues)
594
683
  const all = rule.resources.includes("*")
595
684
  // Each kind is checked one time, also when the list names it again. The
596
685
  // names come from a policy, so a native `Set` finds the copies:
@@ -623,46 +712,82 @@ export const ruleIssues = (
623
712
  const actions = Array.from(
624
713
  new Set(Arr.flatMap(known, (descriptor) => descriptor.actions)),
625
714
  )
715
+ const fieldIssues = Arr.flatMap(rule.fields ?? [], (field, index) =>
716
+ here(
717
+ field.includes("*")
718
+ ? patternIssues(memo, known, field, ["fields", index])
719
+ : fieldIssue(context, field, ["fields", index]),
720
+ ),
721
+ )
722
+ const conditionNodes = memo.limits.conditionNodes
723
+ const whenIssues =
724
+ rule.when === undefined
725
+ ? []
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())
738
+ // The work is known only after every check of the rule. Past the limit, a
739
+ // check gives no issue, so this issue must come, or the policy would load
740
+ // with checks left out.
741
+ const work = memo.work.steps > memo.limits.searchSteps && !memo.work.noted
742
+ memo.work.noted ||= work
626
743
  return [
627
- ...Arr.map(
744
+ ...Arr.flatMap(
628
745
  Arr.filter(names, (kind) => !HashMap.has(domain.resources, kind)),
629
- (kind): Issue => ({
630
- path: [...at, "resources"],
631
- issue: suggestIn(
632
- memo,
633
- `There is no resource with the kind "${kind}"`,
634
- kind,
635
- "~kinds",
636
- () => kinds,
637
- ),
638
- }),
746
+ (kind: string) =>
747
+ here([
748
+ {
749
+ path: ["resources"],
750
+ issue: suggestIn(
751
+ memo,
752
+ `There is no resource with the kind "${kind}"`,
753
+ kind,
754
+ "~kinds",
755
+ () => kinds,
756
+ ),
757
+ },
758
+ ]),
639
759
  ),
640
- ...Arr.map(
760
+ ...Arr.flatMap(
641
761
  Arr.filter(
642
762
  rule.actions,
643
763
  (action) => action !== "*" && !actions.includes(action),
644
764
  ),
645
- (action): Issue => ({
646
- path: [...at, "actions"],
647
- issue: suggestIn(
648
- memo,
649
- `The action "${action}" is not an action of these resources`,
650
- action,
651
- `~actions\u0000${names.join(",")}`,
652
- () => actions,
653
- ),
654
- }),
655
- ),
656
- ...Arr.flatMap(rule.fields ?? [], (field, index) =>
657
- field.includes("*")
658
- ? patternIssues(memo, known, field, [...at, "fields", index])
659
- : fieldIssue(context, field, [...at, "fields", index]),
765
+ (action: string) =>
766
+ here([
767
+ {
768
+ path: ["actions"],
769
+ issue: suggestIn(
770
+ memo,
771
+ `The action "${action}" is not an action of these resources`,
772
+ action,
773
+ `~actions\u0000${names.join(",")}`,
774
+ () => actions,
775
+ ),
776
+ },
777
+ ]),
660
778
  ),
661
- ...(rule.when === undefined
662
- ? []
663
- : Nodes.isNode(rule.when)
664
- ? conditionIssues(context, rule.when, () => [...at, "when"], new Set())
665
- : [notWhole([...at, "when"])]),
779
+ ...fieldIssues,
780
+ ...whenIssues,
781
+ // The issue of the work is not one of the most issues of the binding,
782
+ // so a policy past the limit always says why.
783
+ ...(work
784
+ ? [
785
+ {
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)}.`,
788
+ },
789
+ ]
790
+ : []),
666
791
  ]
667
792
  }
668
793
 
@@ -686,46 +811,52 @@ export const treeIssues = (
686
811
  ): ReadonlyArray<Issue> => {
687
812
  // The rules come out of the tree first, and each one is checked after.
688
813
  // A check of a condition goes down its levels, so a check inside the walk
689
- // of the tree would go down the levels of both.
690
- // The place of each rule shares the path of its parent, and the full path
691
- // is made only when its rule is checked. A deep tree with very many rules
692
- // would need very much memory for a full copy of the path of each rule.
693
- const rulesOf = (
694
- node: TreeNode,
695
- place: Place.Place | undefined,
696
- level: number,
697
- ): ReadonlyArray<Issue | readonly [RuleData, Place.Place]> => {
698
- if (level > Limits.policyDepth) {
699
- return [
700
- {
701
- path: Place.pathOf(place),
702
- issue: `A policy tree has more than ${String(Limits.policyDepth)} levels. The most is ${String(Limits.policyDepth)}.`,
703
- },
704
- ]
705
- }
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) => {
706
823
  switch (node._tag) {
707
824
  case "Rules":
708
- return Arr.map(
709
- node.rules,
710
- (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
+ ),
711
830
  )
712
831
  case "All":
713
832
  case "Any":
714
833
  case "FirstOf":
715
- return Arr.flatMap(node.policies, (child, index) =>
716
- 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,
717
842
  )
718
843
  case "Ceiling":
719
- return [
720
- ...rulesOf(node.cap, Place.at(place, "cap"), level + 1),
721
- ...rulesOf(node.policy, Place.at(place, "policy"), level + 1),
722
- ]
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
+ )
723
854
  }
724
- }
725
- const memo = makeMemo()
726
- 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) =>
727
858
  Array.isArray(found)
728
- ? ruleIssues(domain, found[0], Place.pathOf(found[1]), memo)
859
+ ? ruleIssues(domain, found[0], found[1], memo)
729
860
  : [found as Issue],
730
861
  )
731
862
  }