@nmnmcc/ability 2.0.4 → 2.0.6
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/README.md +7 -7
- package/dist/Condition.d.ts +4 -4
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +1 -1
- package/dist/Policy.d.ts +2 -2
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js.map +1 -1
- package/dist/internal/algebra.js +85 -10
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +37 -14
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +15 -12
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +6 -0
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +12 -9
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +1 -1
- package/dist/internal/domain.js +1 -1
- package/dist/internal/json.js +2 -10
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/nodes.d.ts +4 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +17 -7
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +1 -1
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +2 -8
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts +4 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +13 -4
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +6 -6
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +51 -34
- package/dist/internal/search.js.map +1 -1
- package/package.json +1 -1
- package/src/Condition.ts +5 -2
- package/src/Policy.ts +1 -2
- package/src/internal/algebra.ts +118 -21
- package/src/internal/authorizer.ts +56 -14
- package/src/internal/bind.ts +39 -18
- package/src/internal/catalog.ts +40 -27
- package/src/internal/json.ts +2 -11
- package/src/internal/nodes.ts +21 -11
- package/src/internal/patterns.ts +7 -3
- package/src/internal/policy-json.ts +2 -9
- package/src/internal/policy-nodes.ts +14 -6
- package/src/internal/rule.ts +13 -9
- package/src/internal/search.ts +81 -75
package/src/internal/search.ts
CHANGED
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as Arr from "effect/Array"
|
|
13
13
|
import * as Data from "effect/Data"
|
|
14
|
-
import * as HashMap from "effect/HashMap"
|
|
15
|
-
import * as HashSet from "effect/HashSet"
|
|
16
|
-
import * as MutableHashMap from "effect/MutableHashMap"
|
|
17
14
|
import * as Option from "effect/Option"
|
|
18
15
|
import { isObject } from "effect/Predicate"
|
|
19
16
|
import * as Algebra from "./algebra.ts"
|
|
@@ -146,14 +143,24 @@ const cellText = (cell: unknown): string =>
|
|
|
146
143
|
? `number:${Object.is(cell, -0) ? "0" : String(cell)}`
|
|
147
144
|
: `${typeof cell}:${String(cell)}`
|
|
148
145
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Keeps the first cell of each text, in order. The texts come from the
|
|
148
|
+
* values of a policy, so they go into a native `Set`: the hash of a string in
|
|
149
|
+
* Effect has no random seed, and a policy could hold very many strings with
|
|
150
|
+
* one hash. The engine hashes a string with a random seed.
|
|
151
|
+
*/
|
|
152
|
+
const dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> => {
|
|
153
|
+
// The set belongs to this call.
|
|
154
|
+
const seen = new Set<string>()
|
|
155
|
+
return Arr.filter(cells, (cell) => {
|
|
156
|
+
const text = cellText(cell)
|
|
157
|
+
if (seen.has(text)) {
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
seen.add(text)
|
|
161
|
+
return true
|
|
162
|
+
})
|
|
163
|
+
}
|
|
157
164
|
|
|
158
165
|
/** Gives one value from each cell of a field. */
|
|
159
166
|
const cellsOf = (
|
|
@@ -185,16 +192,23 @@ interface Search {
|
|
|
185
192
|
* The texts of the values of each `In` test, so one test of a value does
|
|
186
193
|
* not read every value of the test. It is a native `Map`, because it must
|
|
187
194
|
* find a node by the node itself: a hash of a node reads all its values.
|
|
195
|
+
* The texts are in a native `Set`, as in {@link dedupeCells}.
|
|
188
196
|
*/
|
|
189
|
-
readonly members: Map<Nodes.In,
|
|
197
|
+
readonly members: Map<Nodes.In, Set<string>>
|
|
198
|
+
/**
|
|
199
|
+
* The keys of the conditions that the search found to be unsatisfiable:
|
|
200
|
+
* see `Algebra.keyOf`. A condition with the same structure has the same
|
|
201
|
+
* free fields, so its answer is the same. The set only grows, so the steps
|
|
202
|
+
* of a search may share it. It is a native `Set`, as in
|
|
203
|
+
* {@link dedupeCells}.
|
|
204
|
+
*/
|
|
205
|
+
readonly failed: Set<string | Node>
|
|
190
206
|
readonly budget: number
|
|
191
207
|
}
|
|
192
208
|
|
|
193
209
|
/** What a search has done up to now. Each step gives a new one. */
|
|
194
210
|
interface SearchState {
|
|
195
211
|
readonly steps: number
|
|
196
|
-
/** The conditions that the search found to be unsatisfiable. */
|
|
197
|
-
readonly failed: HashSet.HashSet<Node>
|
|
198
212
|
}
|
|
199
213
|
|
|
200
214
|
interface Found {
|
|
@@ -228,13 +242,13 @@ const fixedValue = (
|
|
|
228
242
|
: null
|
|
229
243
|
: free
|
|
230
244
|
|
|
231
|
-
const membersOf = (node: Nodes.In, search: Search):
|
|
245
|
+
const membersOf = (node: Nodes.In, search: Search): Set<string> => {
|
|
232
246
|
const known = search.members.get(node)
|
|
233
247
|
if (known !== undefined) {
|
|
234
248
|
return known
|
|
235
249
|
}
|
|
236
250
|
// `===` finds no `NaN`, so the set has none, and `NaN` is in no test.
|
|
237
|
-
const members =
|
|
251
|
+
const members = new Set(
|
|
238
252
|
Arr.map(
|
|
239
253
|
Arr.filter(
|
|
240
254
|
Arr.map(node.values, literalOf),
|
|
@@ -257,8 +271,7 @@ const testFixed = (
|
|
|
257
271
|
return Algebra.testCompare(node.op, value, literalOf(node.value))
|
|
258
272
|
case "In":
|
|
259
273
|
return (
|
|
260
|
-
!Number.isNaN(value) &&
|
|
261
|
-
HashSet.has(membersOf(node, search), cellText(value))
|
|
274
|
+
!Number.isNaN(value) && membersOf(node, search).has(cellText(value))
|
|
262
275
|
)
|
|
263
276
|
case "Exists":
|
|
264
277
|
return value !== null
|
|
@@ -318,13 +331,11 @@ const testedFields = (node: Node): ReadonlyArray<string> => {
|
|
|
318
331
|
* and then the first name in order, so the choice is the same each time.
|
|
319
332
|
*/
|
|
320
333
|
const nextField = (node: Node): string => {
|
|
321
|
-
|
|
334
|
+
// A native `Map`, as in `dedupeCells`: a condition that is not bound to a
|
|
335
|
+
// domain may have any field names. The map belongs to this call.
|
|
336
|
+
const counts = new Map<string, number>()
|
|
322
337
|
Arr.forEach(testedFields(node), (field) =>
|
|
323
|
-
|
|
324
|
-
counts,
|
|
325
|
-
field,
|
|
326
|
-
Option.getOrElse(MutableHashMap.get(counts, field), () => 0) + 1,
|
|
327
|
-
),
|
|
338
|
+
counts.set(field, (counts.get(field) ?? 0) + 1),
|
|
328
339
|
)
|
|
329
340
|
const depth = (field: string) => field.split(".").length
|
|
330
341
|
const [first, ...rest] = Array.from(counts)
|
|
@@ -351,7 +362,15 @@ const searchNode = (
|
|
|
351
362
|
if (node._tag === "True") {
|
|
352
363
|
return { assignment: Option.some(assignment), state }
|
|
353
364
|
}
|
|
354
|
-
if (node._tag === "False"
|
|
365
|
+
if (node._tag === "False") {
|
|
366
|
+
return { assignment: Option.none(), state }
|
|
367
|
+
}
|
|
368
|
+
// The key reads at most a small part of the node: a node with very many
|
|
369
|
+
// nodes, or with very many values in its `In` tests, is its own key. A
|
|
370
|
+
// step reads the nodes but not the values, which `membersOf` keeps, so the
|
|
371
|
+
// budget counts the size.
|
|
372
|
+
const key = Algebra.keyOf(node)
|
|
373
|
+
if (search.failed.has(key)) {
|
|
355
374
|
return { assignment: Option.none(), state }
|
|
356
375
|
}
|
|
357
376
|
if (state.steps + node.size > search.budget) {
|
|
@@ -383,17 +402,10 @@ const searchNode = (
|
|
|
383
402
|
)
|
|
384
403
|
},
|
|
385
404
|
)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
state: {
|
|
391
|
-
...result.state,
|
|
392
|
-
// A condition with the same structure has the same free fields,
|
|
393
|
-
// so its answer is the same.
|
|
394
|
-
failed: HashSet.add(result.state.failed, node),
|
|
395
|
-
},
|
|
396
|
-
}
|
|
405
|
+
if (Option.isNone(result.assignment)) {
|
|
406
|
+
search.failed.add(key)
|
|
407
|
+
}
|
|
408
|
+
return result
|
|
397
409
|
}
|
|
398
410
|
|
|
399
411
|
/**
|
|
@@ -434,10 +446,12 @@ const exampleOf = (
|
|
|
434
446
|
(root, [field, value]) => setPath(root, field.split("."), value),
|
|
435
447
|
)
|
|
436
448
|
|
|
437
|
-
/**
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
449
|
+
/**
|
|
450
|
+
* The constants that the tests of each field use. They come from a policy, so
|
|
451
|
+
* they go into a native `Map` and `Set`, as in {@link dedupeCells}. The map
|
|
452
|
+
* belongs to one search.
|
|
453
|
+
*/
|
|
454
|
+
const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
|
|
441
455
|
const pairs = (
|
|
442
456
|
current: Node,
|
|
443
457
|
): ReadonlyArray<readonly [string, ReadonlyArray<Nodes.Scalar>]> => {
|
|
@@ -461,16 +475,13 @@ const constantsOf = (
|
|
|
461
475
|
return []
|
|
462
476
|
}
|
|
463
477
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
),
|
|
472
|
-
),
|
|
473
|
-
)
|
|
478
|
+
const all = new Map<string, Set<Nodes.Scalar>>()
|
|
479
|
+
Arr.forEach(pairs(node), ([field, values]) => {
|
|
480
|
+
const known = all.get(field) ?? new Set<Nodes.Scalar>()
|
|
481
|
+
Arr.forEach(values, (value) => known.add(value))
|
|
482
|
+
all.set(field, known)
|
|
483
|
+
})
|
|
484
|
+
return all
|
|
474
485
|
}
|
|
475
486
|
|
|
476
487
|
/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */
|
|
@@ -495,38 +506,33 @@ export const satisfiable = (
|
|
|
495
506
|
}
|
|
496
507
|
const constants = constantsOf(node)
|
|
497
508
|
// The search calls itself one time for each field that it fixes.
|
|
498
|
-
if (
|
|
509
|
+
if (constants.size > Limits.searchFields) {
|
|
499
510
|
return satisfiability.Unknown({ reason: "budget" })
|
|
500
511
|
}
|
|
501
|
-
// The cells of each field
|
|
502
|
-
|
|
503
|
-
const cells = MutableHashMap.empty<string, ReadonlyArray<unknown>>()
|
|
512
|
+
// The cells of each field, in a native `Map` as the constants are.
|
|
513
|
+
const cells = new Map<string, ReadonlyArray<unknown>>()
|
|
504
514
|
const search: Search = {
|
|
505
515
|
members: new Map(),
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
516
|
+
failed: new Set(),
|
|
517
|
+
cells: (field) => {
|
|
518
|
+
const made = cells.get(field)
|
|
519
|
+
if (made !== undefined) {
|
|
520
|
+
return made
|
|
521
|
+
}
|
|
522
|
+
// The cells come from the constants, so many constants are work
|
|
523
|
+
// that the budget must count before the cells are made.
|
|
524
|
+
const known = constants.get(field) ?? new Set<Nodes.Scalar>()
|
|
525
|
+
if (known.size > budget) {
|
|
526
|
+
throw new OutOfBudget()
|
|
527
|
+
}
|
|
528
|
+
const found = cellsOf(kindsOf(field), Array.from(known))
|
|
529
|
+
cells.set(field, found)
|
|
530
|
+
return found
|
|
531
|
+
},
|
|
521
532
|
budget,
|
|
522
533
|
}
|
|
523
534
|
try {
|
|
524
|
-
const found = searchNode(
|
|
525
|
-
Algebra.simplify(node),
|
|
526
|
-
search,
|
|
527
|
-
{ steps: 0, failed: HashSet.empty() },
|
|
528
|
-
[],
|
|
529
|
-
)
|
|
535
|
+
const found = searchNode(Algebra.simplify(node), search, { steps: 0 }, [])
|
|
530
536
|
return Option.match(found.assignment, {
|
|
531
537
|
onNone: () => satisfiability.Unsatisfiable(),
|
|
532
538
|
onSome: (assignment) =>
|