@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/dist/internal/search.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { searchSteps } from "./limits.js";
|
|
2
2
|
import { always, never } from "./nodes.js";
|
|
3
|
-
import { and, not, or, simplify, testCompare } from "./algebra.js";
|
|
3
|
+
import { and, keyOf, not, or, simplify, testCompare } from "./algebra.js";
|
|
4
4
|
import { anyKinds, lookup } from "./catalog.js";
|
|
5
5
|
import * as Data from "effect/Data";
|
|
6
6
|
import * as Arr from "effect/Array";
|
|
7
7
|
import { isObject } from "effect/Predicate";
|
|
8
|
-
import * as HashSet from "effect/HashSet";
|
|
9
8
|
import * as Option from "effect/Option";
|
|
10
|
-
import * as HashMap from "effect/HashMap";
|
|
11
|
-
import * as MutableHashMap from "effect/MutableHashMap";
|
|
12
9
|
// src/internal/search.ts
|
|
13
10
|
/**
|
|
14
11
|
* The search for a value that makes a condition true. Each test of a
|
|
@@ -79,11 +76,21 @@ const stringCells = (constants) => {
|
|
|
79
76
|
* answer for them: `0` and `-0` are one, and `NaN` is one.
|
|
80
77
|
*/
|
|
81
78
|
const cellText = (cell) => cell === opaque ? "opaque" : typeof cell === "number" ? `number:${Object.is(cell, -0) ? "0" : String(cell)}` : `${typeof cell}:${String(cell)}`;
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Keeps the first cell of each text, in order. The texts come from the
|
|
81
|
+
* values of a policy, so they go into a native `Set`: the hash of a string in
|
|
82
|
+
* Effect has no random seed, and a policy could hold very many strings with
|
|
83
|
+
* one hash. The engine hashes a string with a random seed.
|
|
84
|
+
*/
|
|
85
|
+
const dedupeCells = (cells) => {
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
return Arr.filter(cells, (cell) => {
|
|
88
|
+
const text = cellText(cell);
|
|
89
|
+
if (seen.has(text)) return false;
|
|
90
|
+
seen.add(text);
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
};
|
|
87
94
|
/** Gives one value from each cell of a field. */
|
|
88
95
|
const cellsOf = (kinds, constants) => dedupeCells([
|
|
89
96
|
...kinds.null ? [null] : [],
|
|
@@ -106,14 +113,14 @@ const fixedValue = (name, field, value) => name === field ? value : name.startsW
|
|
|
106
113
|
const membersOf = (node, search) => {
|
|
107
114
|
const known = search.members.get(node);
|
|
108
115
|
if (known !== void 0) return known;
|
|
109
|
-
const members =
|
|
116
|
+
const members = new Set(Arr.map(Arr.filter(Arr.map(node.values, literalOf), (literal) => !Number.isNaN(literal)), cellText));
|
|
110
117
|
search.members.set(node, members);
|
|
111
118
|
return members;
|
|
112
119
|
};
|
|
113
120
|
const testFixed = (node, value, search) => {
|
|
114
121
|
switch (node._tag) {
|
|
115
122
|
case "Compare": return testCompare(node.op, value, literalOf(node.value));
|
|
116
|
-
case "In": return !Number.isNaN(value) &&
|
|
123
|
+
case "In": return !Number.isNaN(value) && membersOf(node, search).has(cellText(value));
|
|
117
124
|
case "Exists": return value !== null;
|
|
118
125
|
}
|
|
119
126
|
};
|
|
@@ -151,8 +158,8 @@ const testedFields = (node) => {
|
|
|
151
158
|
* and then the first name in order, so the choice is the same each time.
|
|
152
159
|
*/
|
|
153
160
|
const nextField = (node) => {
|
|
154
|
-
const counts =
|
|
155
|
-
Arr.forEach(testedFields(node), (field) =>
|
|
161
|
+
const counts = /* @__PURE__ */ new Map();
|
|
162
|
+
Arr.forEach(testedFields(node), (field) => counts.set(field, (counts.get(field) ?? 0) + 1));
|
|
156
163
|
const depth = (field) => field.split(".").length;
|
|
157
164
|
const [first, ...rest] = Array.from(counts);
|
|
158
165
|
return Arr.reduce(rest, first, (best, next) => depth(next[0]) < depth(best[0]) || depth(next[0]) === depth(best[0]) && (next[1] > best[1] || next[1] === best[1] && next[0] < best[0]) ? next : best)[0];
|
|
@@ -167,7 +174,12 @@ const searchNode = (node, search, state, assignment) => {
|
|
|
167
174
|
assignment: Option.some(assignment),
|
|
168
175
|
state
|
|
169
176
|
};
|
|
170
|
-
if (node._tag === "False"
|
|
177
|
+
if (node._tag === "False") return {
|
|
178
|
+
assignment: Option.none(),
|
|
179
|
+
state
|
|
180
|
+
};
|
|
181
|
+
const key = keyOf(node);
|
|
182
|
+
if (search.failed.has(key)) return {
|
|
171
183
|
assignment: Option.none(),
|
|
172
184
|
state
|
|
173
185
|
};
|
|
@@ -188,13 +200,8 @@ const searchNode = (node, search, state, assignment) => {
|
|
|
188
200
|
steps
|
|
189
201
|
}, [...assignment, [field, cell]]);
|
|
190
202
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
state: {
|
|
194
|
-
...result.state,
|
|
195
|
-
failed: HashSet.add(result.state.failed, node)
|
|
196
|
-
}
|
|
197
|
-
};
|
|
203
|
+
if (Option.isNone(result.assignment)) search.failed.add(key);
|
|
204
|
+
return result;
|
|
198
205
|
};
|
|
199
206
|
/**
|
|
200
207
|
* Puts a value at a path in an object, and gives a new object. A value with
|
|
@@ -216,7 +223,11 @@ const setPath = (root, names, value) => {
|
|
|
216
223
|
};
|
|
217
224
|
};
|
|
218
225
|
const exampleOf = (assignment) => Arr.reduce(assignment, {}, (root, [field, value]) => setPath(root, field.split("."), value));
|
|
219
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* The constants that the tests of each field use. They come from a policy, so
|
|
228
|
+
* they go into a native `Map` and `Set`, as in {@link dedupeCells}. The map
|
|
229
|
+
* belongs to one search.
|
|
230
|
+
*/
|
|
220
231
|
const constantsOf = (node) => {
|
|
221
232
|
const pairs = (current) => {
|
|
222
233
|
switch (current._tag) {
|
|
@@ -230,7 +241,13 @@ const constantsOf = (node) => {
|
|
|
230
241
|
default: return [];
|
|
231
242
|
}
|
|
232
243
|
};
|
|
233
|
-
|
|
244
|
+
const all = /* @__PURE__ */ new Map();
|
|
245
|
+
Arr.forEach(pairs(node), ([field, values]) => {
|
|
246
|
+
const known = all.get(field) ?? /* @__PURE__ */ new Set();
|
|
247
|
+
Arr.forEach(values, (value) => known.add(value));
|
|
248
|
+
all.set(field, known);
|
|
249
|
+
});
|
|
250
|
+
return all;
|
|
234
251
|
};
|
|
235
252
|
/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */
|
|
236
253
|
const kindsIn = (catalog) => (field) => (catalog === void 0 ? void 0 : lookup(catalog, field))?.kinds ?? anyKinds;
|
|
@@ -242,24 +259,24 @@ const kindsIn = (catalog) => (field) => (catalog === void 0 ? void 0 : lookup(ca
|
|
|
242
259
|
const satisfiable = (node, kindsOf, budget = searchSteps) => {
|
|
243
260
|
if (node.opaque) return satisfiability.Unknown({ reason: "predicate" });
|
|
244
261
|
const constants = constantsOf(node);
|
|
245
|
-
if (
|
|
246
|
-
const cells =
|
|
262
|
+
if (constants.size > 256) return satisfiability.Unknown({ reason: "budget" });
|
|
263
|
+
const cells = /* @__PURE__ */ new Map();
|
|
247
264
|
const search = {
|
|
248
265
|
members: /* @__PURE__ */ new Map(),
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
266
|
+
failed: /* @__PURE__ */ new Set(),
|
|
267
|
+
cells: (field) => {
|
|
268
|
+
const made = cells.get(field);
|
|
269
|
+
if (made !== void 0) return made;
|
|
270
|
+
const known = constants.get(field) ?? /* @__PURE__ */ new Set();
|
|
271
|
+
if (known.size > budget) throw new OutOfBudget();
|
|
252
272
|
const found = cellsOf(kindsOf(field), Array.from(known));
|
|
253
|
-
|
|
273
|
+
cells.set(field, found);
|
|
254
274
|
return found;
|
|
255
|
-
}
|
|
275
|
+
},
|
|
256
276
|
budget
|
|
257
277
|
};
|
|
258
278
|
try {
|
|
259
|
-
const found = searchNode(simplify(node), search, {
|
|
260
|
-
steps: 0,
|
|
261
|
-
failed: HashSet.empty()
|
|
262
|
-
}, []);
|
|
279
|
+
const found = searchNode(simplify(node), search, { steps: 0 }, []);
|
|
263
280
|
return Option.match(found.assignment, {
|
|
264
281
|
onNone: () => satisfiability.Unsatisfiable(),
|
|
265
282
|
onSome: (assignment) => satisfiability.Satisfiable({ example: exampleOf(assignment) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.js","names":["Algebra.testCompare","Nodes.always","Nodes.never","Algebra.and","Algebra.or","Algebra.not","Catalog.lookup","Catalog.anyKinds","Limits.searchSteps","Algebra.simplify"],"sources":["../../src/internal/search.ts"],"sourcesContent":["/**\n * The search for a value that makes a condition true. Each test of a\n * condition compares one field with constants, so the constants cut the\n * values of each field into a few cells, and every test gives the same answer\n * for all the values of one cell. The search tries one value from each cell,\n * one field at a time.\n *\n * The values of a field come from its kinds in a catalog. The kinds hold every\n * real value, so a \"no\" holds for the real values too. A \"yes\" holds for the\n * kinds: a check of the Schema may still stop the example.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Data from \"effect/Data\"\nimport * as HashMap from \"effect/HashMap\"\nimport * as HashSet from \"effect/HashSet\"\nimport * as MutableHashMap from \"effect/MutableHashMap\"\nimport * as Option from \"effect/Option\"\nimport { isObject } from \"effect/Predicate\"\nimport * as Algebra from \"./algebra.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as Limits from \"./limits.ts\"\nimport * as Nodes from \"./nodes.ts\"\n\ntype Node = Nodes.Node\n\n/**\n * Values of fields, as a nested object. A field that is not in it may have any\n * value. A value with parts that the search does not look into, such as a\n * date, is an empty object.\n */\nexport interface Example {\n\treadonly [field: string]: unknown\n}\n\n/** The answer of a search. */\nexport type Satisfiability = Data.TaggedEnum<{\n\tSatisfiable: { readonly example: Example }\n\tUnsatisfiable: {}\n\tUnknown: { readonly reason: \"predicate\" | \"budget\" }\n}>\n\nconst satisfiability = Data.taggedEnum<Satisfiability>()\n\n/** Stands for a value with parts, such as a struct or a date. */\nconst opaque: object = Object.freeze({})\n\n/** Marks a field that a step of the search did not fix. */\nconst free: unique symbol = Symbol(\"free\")\n\n/** The least number that is greater than `value`. */\nconst nextUp = (value: number): number => {\n\tif (Number.isNaN(value) || value === Infinity) {\n\t\treturn value\n\t}\n\tif (value === 0) {\n\t\treturn Number.MIN_VALUE\n\t}\n\tconst view = new DataView(new ArrayBuffer(8))\n\tview.setFloat64(0, value)\n\tconst bits = view.getBigUint64(0)\n\tview.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n)\n\treturn view.getFloat64(0)\n}\n\n/**\n * Gives one number from each part of the number line that the constants\n * make: each constant, and one number between each two neighbours when there\n * is one. The ends of the domain are in the list too.\n */\nconst numberCells = (\n\tkind: \"integer\" | \"finite\" | \"any\",\n\tconstants: ReadonlyArray<number>,\n): ReadonlyArray<number> => {\n\tconst integer = kind === \"integer\"\n\tconst low = integer\n\t\t? Number.MIN_SAFE_INTEGER\n\t\t: kind === \"finite\"\n\t\t\t? -Number.MAX_VALUE\n\t\t\t: -Infinity\n\tconst high = integer\n\t\t? Number.MAX_SAFE_INTEGER\n\t\t: kind === \"finite\"\n\t\t\t? Number.MAX_VALUE\n\t\t\t: Infinity\n\tconst known: ReadonlyArray<number> = Arr.filter(\n\t\tconstants,\n\t\t(value: number) => !Number.isNaN(value),\n\t)\n\tconst points = Arr.filter(\n\t\tArr.flatMap(known, (value: number) =>\n\t\t\tinteger && Number.isFinite(value)\n\t\t\t\t? [Math.floor(value), Math.ceil(value)]\n\t\t\t\t: [value],\n\t\t),\n\t\t(value: number) => value >= low && value <= high,\n\t)\n\t// `0` and `-0` are one point: every test gives the same answer for both,\n\t// and `===` calls them equal.\n\tconst ordered: Array<number> = [low, high, ...points].sort((a, b) => a - b)\n\tconst sorted = Arr.dedupeAdjacentWith(\n\t\tordered,\n\t\t(a: number, b: number) => a === b,\n\t)\n\tconst cells = Arr.flatMap(sorted, (value, index) => {\n\t\tconst after = sorted[index + 1]\n\t\tconst between = integer ? value + 1 : nextUp(value)\n\t\treturn after !== undefined && between < after ? [value, between] : [value]\n\t})\n\treturn kind === \"any\" ? Arr.append(cells, Number.NaN) : cells\n}\n\n/**\n * Gives one string from each part that the constants make. Strings are in the\n * order of their UTF-16 code units, as `<` sees them. The least string after\n * `a` is `a + \"\\0\"`, so there is a string between `a` and `b` exactly when\n * `b` is not `a + \"\\0\"`.\n */\nconst stringCells = (\n\tconstants: ReadonlyArray<string>,\n): ReadonlyArray<string> => {\n\tconst ordered: Array<string> = [...constants].sort()\n\tconst sorted = Arr.dedupeAdjacentWith(\n\t\tordered,\n\t\t(a: string, b: string) => a === b,\n\t)\n\tif (sorted.length === 0) {\n\t\treturn [\"\"]\n\t}\n\treturn [\n\t\t...(sorted[0] === \"\" ? [] : [\"\"]),\n\t\t...Arr.flatMap(sorted, (value, index) => {\n\t\t\tconst next = `${value}\\0`\n\t\t\treturn next === sorted[index + 1] ? [value] : [value, next]\n\t\t}),\n\t]\n}\n\n/**\n * A text for a cell. Two cells get one text when every test gives the same\n * answer for them: `0` and `-0` are one, and `NaN` is one.\n */\nconst cellText = (cell: unknown): string =>\n\tcell === opaque\n\t\t? \"opaque\"\n\t\t: typeof cell === \"number\"\n\t\t\t? `number:${Object.is(cell, -0) ? \"0\" : String(cell)}`\n\t\t\t: `${typeof cell}:${String(cell)}`\n\n/** Keeps the first cell of each text, in order. */\nconst dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> =>\n\tArr.flatten(\n\t\tArr.mapAccum(cells, HashSet.empty<string>(), (seen, cell) => {\n\t\t\tconst text = cellText(cell)\n\t\t\treturn [HashSet.add(seen, text), HashSet.has(seen, text) ? [] : [cell]]\n\t\t})[1],\n\t)\n\n/** Gives one value from each cell of a field. */\nconst cellsOf = (\n\tkinds: Catalog.Kinds,\n\tconstants: ReadonlyArray<Nodes.Scalar>,\n): ReadonlyArray<unknown> =>\n\tdedupeCells([\n\t\t...(kinds.null ? [null] : []),\n\t\t...(kinds.object ? [opaque] : []),\n\t\t...(kinds.boolean ? [true, false] : []),\n\t\t...(kinds.string\n\t\t\t? stringCells(Arr.filter(constants, (value) => typeof value === \"string\"))\n\t\t\t: []),\n\t\t...(kinds.number === \"none\"\n\t\t\t? []\n\t\t\t: numberCells(\n\t\t\t\t\tkinds.number,\n\t\t\t\t\tArr.filter(constants, (value) => typeof value === \"number\"),\n\t\t\t\t)),\n\t\t...kinds.literals,\n\t])\n\nclass OutOfBudget extends Data.Error {}\n\n/** What one search keeps for all its steps. It lives only as long as the search. */\ninterface Search {\n\treadonly cells: (field: string) => ReadonlyArray<unknown>\n\t/**\n\t * The texts of the values of each `In` test, so one test of a value does\n\t * not read every value of the test. It is a native `Map`, because it must\n\t * find a node by the node itself: a hash of a node reads all its values.\n\t */\n\treadonly members: Map<Nodes.In, HashSet.HashSet<string>>\n\treadonly budget: number\n}\n\n/** What a search has done up to now. Each step gives a new one. */\ninterface SearchState {\n\treadonly steps: number\n\t/** The conditions that the search found to be unsatisfiable. */\n\treadonly failed: HashSet.HashSet<Node>\n}\n\ninterface Found {\n\treadonly assignment: Option.Option<ReadonlyArray<readonly [string, unknown]>>\n\treadonly state: SearchState\n}\n\nconst literalOf = (operand: Nodes.Operand): Nodes.Scalar => {\n\tif (operand._tag !== \"Literal\") {\n\t\tthrow new Error(\n\t\t\t\"Condition.satisfiable needs a condition with no principal fields. Call Condition.partial first.\",\n\t\t)\n\t}\n\treturn operand.value\n}\n\n/**\n * The value of the field `name` after `field` gets `value`. A field below\n * `field` is `null` when `value` is not a value with parts.\n */\nconst fixedValue = (\n\tname: string,\n\tfield: string,\n\tvalue: unknown,\n): unknown | typeof free =>\n\tname === field\n\t\t? value\n\t\t: name.startsWith(`${field}.`)\n\t\t\t? value === opaque\n\t\t\t\t? free\n\t\t\t\t: null\n\t\t\t: free\n\nconst membersOf = (node: Nodes.In, search: Search): HashSet.HashSet<string> => {\n\tconst known = search.members.get(node)\n\tif (known !== undefined) {\n\t\treturn known\n\t}\n\t// `===` finds no `NaN`, so the set has none, and `NaN` is in no test.\n\tconst members = HashSet.fromIterable(\n\t\tArr.map(\n\t\t\tArr.filter(\n\t\t\t\tArr.map(node.values, literalOf),\n\t\t\t\t(literal) => !Number.isNaN(literal),\n\t\t\t),\n\t\t\tcellText,\n\t\t),\n\t)\n\tsearch.members.set(node, members)\n\treturn members\n}\n\nconst testFixed = (\n\tnode: Nodes.Compare | Nodes.In | Nodes.Exists,\n\tvalue: unknown,\n\tsearch: Search,\n): boolean => {\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\t\treturn Algebra.testCompare(node.op, value, literalOf(node.value))\n\t\tcase \"In\":\n\t\t\treturn (\n\t\t\t\t!Number.isNaN(value) &&\n\t\t\t\tHashSet.has(membersOf(node, search), cellText(value))\n\t\t\t)\n\t\tcase \"Exists\":\n\t\t\treturn value !== null\n\t}\n}\n\n/** Puts one value into a field, and into the fields below it. */\nconst fix = (\n\tnode: Node,\n\tfield: string,\n\tvalue: unknown,\n\tsearch: Search,\n): Node => {\n\tconst fixChild = (child: Node) => fix(child, field, value, search)\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\tcase \"In\":\n\t\tcase \"Exists\": {\n\t\t\tconst known = fixedValue(node.field, field, value)\n\t\t\treturn known === free\n\t\t\t\t? node\n\t\t\t\t: testFixed(node, known, search)\n\t\t\t\t\t? Nodes.always\n\t\t\t\t\t: Nodes.never\n\t\t}\n\t\tcase \"And\":\n\t\t\treturn Algebra.and(Arr.map(node.conditions, fixChild))\n\t\tcase \"Or\":\n\t\t\treturn Algebra.or(Arr.map(node.conditions, fixChild))\n\t\tcase \"Not\":\n\t\t\treturn node.condition.pipe(fixChild, Algebra.not)\n\t\tdefault:\n\t\t\treturn node\n\t}\n}\n\n/** The fields of the tests of a condition, once for each test. */\nconst testedFields = (node: Node): ReadonlyArray<string> => {\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\tcase \"In\":\n\t\tcase \"Exists\":\n\t\t\treturn [node.field]\n\t\tcase \"And\":\n\t\tcase \"Or\":\n\t\t\treturn Arr.flatMap(node.conditions, testedFields)\n\t\tcase \"Not\":\n\t\t\treturn testedFields(node.condition)\n\t\tdefault:\n\t\t\treturn []\n\t}\n}\n\n/**\n * Picks the next field to split on. A field comes before the fields below it,\n * so a value of a field can fix them. Then a field in more tests comes first,\n * and then the first name in order, so the choice is the same each time.\n */\nconst nextField = (node: Node): string => {\n\tconst counts = MutableHashMap.empty<string, number>()\n\tArr.forEach(testedFields(node), (field) =>\n\t\tMutableHashMap.set(\n\t\t\tcounts,\n\t\t\tfield,\n\t\t\tOption.getOrElse(MutableHashMap.get(counts, field), () => 0) + 1,\n\t\t),\n\t)\n\tconst depth = (field: string) => field.split(\".\").length\n\tconst [first, ...rest] = Array.from(counts)\n\treturn Arr.reduce(rest, first!, (best, next) =>\n\t\tdepth(next[0]) < depth(best[0]) ||\n\t\t(depth(next[0]) === depth(best[0]) &&\n\t\t\t(next[1] > best[1] || (next[1] === best[1] && next[0] < best[0])))\n\t\t\t? next\n\t\t\t: best,\n\t)[0]\n}\n\n/**\n * Finds values of the fields that make a simple condition true. It splits on\n * one field at a time, and tries one value from each cell of the field. Each\n * step reads the whole condition, so the budget counts its size.\n */\nconst searchNode = (\n\tnode: Node,\n\tsearch: Search,\n\tstate: SearchState,\n\tassignment: ReadonlyArray<readonly [string, unknown]>,\n): Found => {\n\tif (node._tag === \"True\") {\n\t\treturn { assignment: Option.some(assignment), state }\n\t}\n\tif (node._tag === \"False\" || HashSet.has(state.failed, node)) {\n\t\treturn { assignment: Option.none(), state }\n\t}\n\tif (state.steps + node.size > search.budget) {\n\t\tthrow new OutOfBudget()\n\t}\n\tconst field = nextField(node)\n\t// The cells are tried in order, until one gives values. A fold over the\n\t// cells does no work after a found answer, and it does not grow the stack\n\t// with the number of cells.\n\tconst result = Arr.reduce(\n\t\tsearch.cells(field),\n\t\t{\n\t\t\tassignment: Option.none(),\n\t\t\tstate: { ...state, steps: state.steps + node.size },\n\t\t} as Found,\n\t\t(before, cell): Found => {\n\t\t\tif (Option.isSome(before.assignment)) {\n\t\t\t\treturn before\n\t\t\t}\n\t\t\tconst steps = before.state.steps + node.size\n\t\t\tif (steps > search.budget) {\n\t\t\t\tthrow new OutOfBudget()\n\t\t\t}\n\t\t\treturn searchNode(\n\t\t\t\tfix(node, field, cell, search),\n\t\t\t\tsearch,\n\t\t\t\t{ ...before.state, steps },\n\t\t\t\t[...assignment, [field, cell]],\n\t\t\t)\n\t\t},\n\t)\n\treturn Option.isSome(result.assignment)\n\t\t? result\n\t\t: {\n\t\t\t\tassignment: Option.none(),\n\t\t\t\tstate: {\n\t\t\t\t\t...result.state,\n\t\t\t\t\t// A condition with the same structure has the same free fields,\n\t\t\t\t\t// so its answer is the same.\n\t\t\t\t\tfailed: HashSet.add(result.state.failed, node),\n\t\t\t\t},\n\t\t\t}\n}\n\n/**\n * Puts a value at a path in an object, and gives a new object. A value with\n * parts (`opaque`) becomes an empty object, and it keeps an object that is\n * there.\n */\nconst setPath = (\n\troot: Readonly<Record<string, unknown>>,\n\tnames: ReadonlyArray<string>,\n\tvalue: unknown,\n): Readonly<Record<string, unknown>> => {\n\tconst [name, ...rest] = names\n\tif (name === undefined) {\n\t\treturn root\n\t}\n\t// Only an own field counts, as a condition reads it. An inherited one,\n\t// such as `__proto__`, is not a field of the example.\n\tconst inner = Object.hasOwn(root, name) ? root[name] : undefined\n\tif (rest.length === 0) {\n\t\treturn value === opaque && isObject(inner)\n\t\t\t? root\n\t\t\t: { ...root, [name]: value === opaque ? {} : value }\n\t}\n\tconst base =\n\t\tisObject(inner) && inner !== opaque\n\t\t\t? (inner as Readonly<Record<string, unknown>>)\n\t\t\t: {}\n\treturn { ...root, [name]: setPath(base, rest, value) }\n}\n\nconst exampleOf = (\n\tassignment: ReadonlyArray<readonly [string, unknown]>,\n): Example =>\n\tArr.reduce(\n\t\tassignment,\n\t\t{} as Readonly<Record<string, unknown>>,\n\t\t(root, [field, value]) => setPath(root, field.split(\".\"), value),\n\t)\n\n/** The constants that the tests of each field use. */\nconst constantsOf = (\n\tnode: Node,\n): HashMap.HashMap<string, HashSet.HashSet<Nodes.Scalar>> => {\n\tconst pairs = (\n\t\tcurrent: Node,\n\t): ReadonlyArray<readonly [string, ReadonlyArray<Nodes.Scalar>]> => {\n\t\tswitch (current._tag) {\n\t\t\tcase \"Compare\":\n\t\t\t\treturn [[current.field, [literalOf(current.value)]]]\n\t\t\tcase \"In\":\n\t\t\t\treturn [[current.field, Arr.map(current.values, literalOf)]]\n\t\t\tcase \"Exists\":\n\t\t\t\treturn [[current.field, []]]\n\t\t\tcase \"Under\":\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.\",\n\t\t\t\t)\n\t\t\tcase \"And\":\n\t\t\tcase \"Or\":\n\t\t\t\treturn Arr.flatMap(current.conditions, pairs)\n\t\t\tcase \"Not\":\n\t\t\t\treturn pairs(current.condition)\n\t\t\tdefault:\n\t\t\t\treturn []\n\t\t}\n\t}\n\treturn Arr.reduce(pairs(node), HashMap.empty(), (all, [field, values]) =>\n\t\tHashMap.set(\n\t\t\tall,\n\t\t\tfield,\n\t\t\tHashSet.union(\n\t\t\t\tOption.getOrElse(HashMap.get(all, field), HashSet.empty),\n\t\t\t\tHashSet.fromIterable(values),\n\t\t\t),\n\t\t),\n\t)\n}\n\n/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */\nexport const kindsIn =\n\t(catalog: Catalog.Catalog | undefined) =>\n\t(field: string): Catalog.Kinds =>\n\t\t(catalog === undefined ? undefined : Catalog.lookup(catalog, field))\n\t\t\t?.kinds ?? Catalog.anyKinds\n\n/**\n * Searches for values that make a condition true. `kindsOf` gives the kinds\n * of values of each field. The condition must have no principal fields and no\n * `Under` nodes.\n */\nexport const satisfiable = (\n\tnode: Node,\n\tkindsOf: (field: string) => Catalog.Kinds,\n\tbudget: number = Limits.searchSteps,\n): Satisfiability => {\n\tif (node.opaque) {\n\t\treturn satisfiability.Unknown({ reason: \"predicate\" })\n\t}\n\tconst constants = constantsOf(node)\n\t// The search calls itself one time for each field that it fixes.\n\tif (HashMap.size(constants) > Limits.searchFields) {\n\t\treturn satisfiability.Unknown({ reason: \"budget\" })\n\t}\n\t// The cells of each field. A field name is a string, so an Effect map\n\t// finds it.\n\tconst cells = MutableHashMap.empty<string, ReadonlyArray<unknown>>()\n\tconst search: Search = {\n\t\tmembers: new Map(),\n\t\tcells: (field) =>\n\t\t\tOption.getOrElse(MutableHashMap.get(cells, field), () => {\n\t\t\t\t// The cells come from the constants, so many constants are work\n\t\t\t\t// that the budget must count before the cells are made.\n\t\t\t\tconst known = Option.getOrElse(\n\t\t\t\t\tHashMap.get(constants, field),\n\t\t\t\t\tHashSet.empty,\n\t\t\t\t)\n\t\t\t\tif (HashSet.size(known) > budget) {\n\t\t\t\t\tthrow new OutOfBudget()\n\t\t\t\t}\n\t\t\t\tconst found = cellsOf(kindsOf(field), Array.from(known))\n\t\t\t\tMutableHashMap.set(cells, field, found)\n\t\t\t\treturn found\n\t\t\t}),\n\t\tbudget,\n\t}\n\ttry {\n\t\tconst found = searchNode(\n\t\t\tAlgebra.simplify(node),\n\t\t\tsearch,\n\t\t\t{ steps: 0, failed: HashSet.empty() },\n\t\t\t[],\n\t\t)\n\t\treturn Option.match(found.assignment, {\n\t\t\tonNone: () => satisfiability.Unsatisfiable(),\n\t\t\tonSome: (assignment) =>\n\t\t\t\tsatisfiability.Satisfiable({ example: exampleOf(assignment) }),\n\t\t})\n\t} catch (error) {\n\t\tif (error instanceof OutOfBudget) {\n\t\t\treturn satisfiability.Unknown({ reason: \"budget\" })\n\t\t}\n\t\tthrow error\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"search.js","names":["Algebra.testCompare","Nodes.always","Nodes.never","Algebra.and","Algebra.or","Algebra.not","Algebra.keyOf","Catalog.lookup","Catalog.anyKinds","Limits.searchSteps","Algebra.simplify"],"sources":["../../src/internal/search.ts"],"sourcesContent":["/**\n * The search for a value that makes a condition true. Each test of a\n * condition compares one field with constants, so the constants cut the\n * values of each field into a few cells, and every test gives the same answer\n * for all the values of one cell. The search tries one value from each cell,\n * one field at a time.\n *\n * The values of a field come from its kinds in a catalog. The kinds hold every\n * real value, so a \"no\" holds for the real values too. A \"yes\" holds for the\n * kinds: a check of the Schema may still stop the example.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Data from \"effect/Data\"\nimport * as Option from \"effect/Option\"\nimport { isObject } from \"effect/Predicate\"\nimport * as Algebra from \"./algebra.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as Limits from \"./limits.ts\"\nimport * as Nodes from \"./nodes.ts\"\n\ntype Node = Nodes.Node\n\n/**\n * Values of fields, as a nested object. A field that is not in it may have any\n * value. A value with parts that the search does not look into, such as a\n * date, is an empty object.\n */\nexport interface Example {\n\treadonly [field: string]: unknown\n}\n\n/** The answer of a search. */\nexport type Satisfiability = Data.TaggedEnum<{\n\tSatisfiable: { readonly example: Example }\n\tUnsatisfiable: {}\n\tUnknown: { readonly reason: \"predicate\" | \"budget\" }\n}>\n\nconst satisfiability = Data.taggedEnum<Satisfiability>()\n\n/** Stands for a value with parts, such as a struct or a date. */\nconst opaque: object = Object.freeze({})\n\n/** Marks a field that a step of the search did not fix. */\nconst free: unique symbol = Symbol(\"free\")\n\n/** The least number that is greater than `value`. */\nconst nextUp = (value: number): number => {\n\tif (Number.isNaN(value) || value === Infinity) {\n\t\treturn value\n\t}\n\tif (value === 0) {\n\t\treturn Number.MIN_VALUE\n\t}\n\tconst view = new DataView(new ArrayBuffer(8))\n\tview.setFloat64(0, value)\n\tconst bits = view.getBigUint64(0)\n\tview.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n)\n\treturn view.getFloat64(0)\n}\n\n/**\n * Gives one number from each part of the number line that the constants\n * make: each constant, and one number between each two neighbours when there\n * is one. The ends of the domain are in the list too.\n */\nconst numberCells = (\n\tkind: \"integer\" | \"finite\" | \"any\",\n\tconstants: ReadonlyArray<number>,\n): ReadonlyArray<number> => {\n\tconst integer = kind === \"integer\"\n\tconst low = integer\n\t\t? Number.MIN_SAFE_INTEGER\n\t\t: kind === \"finite\"\n\t\t\t? -Number.MAX_VALUE\n\t\t\t: -Infinity\n\tconst high = integer\n\t\t? Number.MAX_SAFE_INTEGER\n\t\t: kind === \"finite\"\n\t\t\t? Number.MAX_VALUE\n\t\t\t: Infinity\n\tconst known: ReadonlyArray<number> = Arr.filter(\n\t\tconstants,\n\t\t(value: number) => !Number.isNaN(value),\n\t)\n\tconst points = Arr.filter(\n\t\tArr.flatMap(known, (value: number) =>\n\t\t\tinteger && Number.isFinite(value)\n\t\t\t\t? [Math.floor(value), Math.ceil(value)]\n\t\t\t\t: [value],\n\t\t),\n\t\t(value: number) => value >= low && value <= high,\n\t)\n\t// `0` and `-0` are one point: every test gives the same answer for both,\n\t// and `===` calls them equal.\n\tconst ordered: Array<number> = [low, high, ...points].sort((a, b) => a - b)\n\tconst sorted = Arr.dedupeAdjacentWith(\n\t\tordered,\n\t\t(a: number, b: number) => a === b,\n\t)\n\tconst cells = Arr.flatMap(sorted, (value, index) => {\n\t\tconst after = sorted[index + 1]\n\t\tconst between = integer ? value + 1 : nextUp(value)\n\t\treturn after !== undefined && between < after ? [value, between] : [value]\n\t})\n\treturn kind === \"any\" ? Arr.append(cells, Number.NaN) : cells\n}\n\n/**\n * Gives one string from each part that the constants make. Strings are in the\n * order of their UTF-16 code units, as `<` sees them. The least string after\n * `a` is `a + \"\\0\"`, so there is a string between `a` and `b` exactly when\n * `b` is not `a + \"\\0\"`.\n */\nconst stringCells = (\n\tconstants: ReadonlyArray<string>,\n): ReadonlyArray<string> => {\n\tconst ordered: Array<string> = [...constants].sort()\n\tconst sorted = Arr.dedupeAdjacentWith(\n\t\tordered,\n\t\t(a: string, b: string) => a === b,\n\t)\n\tif (sorted.length === 0) {\n\t\treturn [\"\"]\n\t}\n\treturn [\n\t\t...(sorted[0] === \"\" ? [] : [\"\"]),\n\t\t...Arr.flatMap(sorted, (value, index) => {\n\t\t\tconst next = `${value}\\0`\n\t\t\treturn next === sorted[index + 1] ? [value] : [value, next]\n\t\t}),\n\t]\n}\n\n/**\n * A text for a cell. Two cells get one text when every test gives the same\n * answer for them: `0` and `-0` are one, and `NaN` is one.\n */\nconst cellText = (cell: unknown): string =>\n\tcell === opaque\n\t\t? \"opaque\"\n\t\t: typeof cell === \"number\"\n\t\t\t? `number:${Object.is(cell, -0) ? \"0\" : String(cell)}`\n\t\t\t: `${typeof cell}:${String(cell)}`\n\n/**\n * Keeps the first cell of each text, in order. The texts come from the\n * values of a policy, so they go into a native `Set`: the hash of a string in\n * Effect has no random seed, and a policy could hold very many strings with\n * one hash. The engine hashes a string with a random seed.\n */\nconst dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> => {\n\t// The set belongs to this call.\n\tconst seen = new Set<string>()\n\treturn Arr.filter(cells, (cell) => {\n\t\tconst text = cellText(cell)\n\t\tif (seen.has(text)) {\n\t\t\treturn false\n\t\t}\n\t\tseen.add(text)\n\t\treturn true\n\t})\n}\n\n/** Gives one value from each cell of a field. */\nconst cellsOf = (\n\tkinds: Catalog.Kinds,\n\tconstants: ReadonlyArray<Nodes.Scalar>,\n): ReadonlyArray<unknown> =>\n\tdedupeCells([\n\t\t...(kinds.null ? [null] : []),\n\t\t...(kinds.object ? [opaque] : []),\n\t\t...(kinds.boolean ? [true, false] : []),\n\t\t...(kinds.string\n\t\t\t? stringCells(Arr.filter(constants, (value) => typeof value === \"string\"))\n\t\t\t: []),\n\t\t...(kinds.number === \"none\"\n\t\t\t? []\n\t\t\t: numberCells(\n\t\t\t\t\tkinds.number,\n\t\t\t\t\tArr.filter(constants, (value) => typeof value === \"number\"),\n\t\t\t\t)),\n\t\t...kinds.literals,\n\t])\n\nclass OutOfBudget extends Data.Error {}\n\n/** What one search keeps for all its steps. It lives only as long as the search. */\ninterface Search {\n\treadonly cells: (field: string) => ReadonlyArray<unknown>\n\t/**\n\t * The texts of the values of each `In` test, so one test of a value does\n\t * not read every value of the test. It is a native `Map`, because it must\n\t * find a node by the node itself: a hash of a node reads all its values.\n\t * The texts are in a native `Set`, as in {@link dedupeCells}.\n\t */\n\treadonly members: Map<Nodes.In, Set<string>>\n\t/**\n\t * The keys of the conditions that the search found to be unsatisfiable:\n\t * see `Algebra.keyOf`. A condition with the same structure has the same\n\t * free fields, so its answer is the same. The set only grows, so the steps\n\t * of a search may share it. It is a native `Set`, as in\n\t * {@link dedupeCells}.\n\t */\n\treadonly failed: Set<string | Node>\n\treadonly budget: number\n}\n\n/** What a search has done up to now. Each step gives a new one. */\ninterface SearchState {\n\treadonly steps: number\n}\n\ninterface Found {\n\treadonly assignment: Option.Option<ReadonlyArray<readonly [string, unknown]>>\n\treadonly state: SearchState\n}\n\nconst literalOf = (operand: Nodes.Operand): Nodes.Scalar => {\n\tif (operand._tag !== \"Literal\") {\n\t\tthrow new Error(\n\t\t\t\"Condition.satisfiable needs a condition with no principal fields. Call Condition.partial first.\",\n\t\t)\n\t}\n\treturn operand.value\n}\n\n/**\n * The value of the field `name` after `field` gets `value`. A field below\n * `field` is `null` when `value` is not a value with parts.\n */\nconst fixedValue = (\n\tname: string,\n\tfield: string,\n\tvalue: unknown,\n): unknown | typeof free =>\n\tname === field\n\t\t? value\n\t\t: name.startsWith(`${field}.`)\n\t\t\t? value === opaque\n\t\t\t\t? free\n\t\t\t\t: null\n\t\t\t: free\n\nconst membersOf = (node: Nodes.In, search: Search): Set<string> => {\n\tconst known = search.members.get(node)\n\tif (known !== undefined) {\n\t\treturn known\n\t}\n\t// `===` finds no `NaN`, so the set has none, and `NaN` is in no test.\n\tconst members = new Set(\n\t\tArr.map(\n\t\t\tArr.filter(\n\t\t\t\tArr.map(node.values, literalOf),\n\t\t\t\t(literal) => !Number.isNaN(literal),\n\t\t\t),\n\t\t\tcellText,\n\t\t),\n\t)\n\tsearch.members.set(node, members)\n\treturn members\n}\n\nconst testFixed = (\n\tnode: Nodes.Compare | Nodes.In | Nodes.Exists,\n\tvalue: unknown,\n\tsearch: Search,\n): boolean => {\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\t\treturn Algebra.testCompare(node.op, value, literalOf(node.value))\n\t\tcase \"In\":\n\t\t\treturn (\n\t\t\t\t!Number.isNaN(value) && membersOf(node, search).has(cellText(value))\n\t\t\t)\n\t\tcase \"Exists\":\n\t\t\treturn value !== null\n\t}\n}\n\n/** Puts one value into a field, and into the fields below it. */\nconst fix = (\n\tnode: Node,\n\tfield: string,\n\tvalue: unknown,\n\tsearch: Search,\n): Node => {\n\tconst fixChild = (child: Node) => fix(child, field, value, search)\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\tcase \"In\":\n\t\tcase \"Exists\": {\n\t\t\tconst known = fixedValue(node.field, field, value)\n\t\t\treturn known === free\n\t\t\t\t? node\n\t\t\t\t: testFixed(node, known, search)\n\t\t\t\t\t? Nodes.always\n\t\t\t\t\t: Nodes.never\n\t\t}\n\t\tcase \"And\":\n\t\t\treturn Algebra.and(Arr.map(node.conditions, fixChild))\n\t\tcase \"Or\":\n\t\t\treturn Algebra.or(Arr.map(node.conditions, fixChild))\n\t\tcase \"Not\":\n\t\t\treturn node.condition.pipe(fixChild, Algebra.not)\n\t\tdefault:\n\t\t\treturn node\n\t}\n}\n\n/** The fields of the tests of a condition, once for each test. */\nconst testedFields = (node: Node): ReadonlyArray<string> => {\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\tcase \"In\":\n\t\tcase \"Exists\":\n\t\t\treturn [node.field]\n\t\tcase \"And\":\n\t\tcase \"Or\":\n\t\t\treturn Arr.flatMap(node.conditions, testedFields)\n\t\tcase \"Not\":\n\t\t\treturn testedFields(node.condition)\n\t\tdefault:\n\t\t\treturn []\n\t}\n}\n\n/**\n * Picks the next field to split on. A field comes before the fields below it,\n * so a value of a field can fix them. Then a field in more tests comes first,\n * and then the first name in order, so the choice is the same each time.\n */\nconst nextField = (node: Node): string => {\n\t// A native `Map`, as in `dedupeCells`: a condition that is not bound to a\n\t// domain may have any field names. The map belongs to this call.\n\tconst counts = new Map<string, number>()\n\tArr.forEach(testedFields(node), (field) =>\n\t\tcounts.set(field, (counts.get(field) ?? 0) + 1),\n\t)\n\tconst depth = (field: string) => field.split(\".\").length\n\tconst [first, ...rest] = Array.from(counts)\n\treturn Arr.reduce(rest, first!, (best, next) =>\n\t\tdepth(next[0]) < depth(best[0]) ||\n\t\t(depth(next[0]) === depth(best[0]) &&\n\t\t\t(next[1] > best[1] || (next[1] === best[1] && next[0] < best[0])))\n\t\t\t? next\n\t\t\t: best,\n\t)[0]\n}\n\n/**\n * Finds values of the fields that make a simple condition true. It splits on\n * one field at a time, and tries one value from each cell of the field. Each\n * step reads the whole condition, so the budget counts its size.\n */\nconst searchNode = (\n\tnode: Node,\n\tsearch: Search,\n\tstate: SearchState,\n\tassignment: ReadonlyArray<readonly [string, unknown]>,\n): Found => {\n\tif (node._tag === \"True\") {\n\t\treturn { assignment: Option.some(assignment), state }\n\t}\n\tif (node._tag === \"False\") {\n\t\treturn { assignment: Option.none(), state }\n\t}\n\t// The key reads at most a small part of the node: a node with very many\n\t// nodes, or with very many values in its `In` tests, is its own key. A\n\t// step reads the nodes but not the values, which `membersOf` keeps, so the\n\t// budget counts the size.\n\tconst key = Algebra.keyOf(node)\n\tif (search.failed.has(key)) {\n\t\treturn { assignment: Option.none(), state }\n\t}\n\tif (state.steps + node.size > search.budget) {\n\t\tthrow new OutOfBudget()\n\t}\n\tconst field = nextField(node)\n\t// The cells are tried in order, until one gives values. A fold over the\n\t// cells does no work after a found answer, and it does not grow the stack\n\t// with the number of cells.\n\tconst result = Arr.reduce(\n\t\tsearch.cells(field),\n\t\t{\n\t\t\tassignment: Option.none(),\n\t\t\tstate: { ...state, steps: state.steps + node.size },\n\t\t} as Found,\n\t\t(before, cell): Found => {\n\t\t\tif (Option.isSome(before.assignment)) {\n\t\t\t\treturn before\n\t\t\t}\n\t\t\tconst steps = before.state.steps + node.size\n\t\t\tif (steps > search.budget) {\n\t\t\t\tthrow new OutOfBudget()\n\t\t\t}\n\t\t\treturn searchNode(\n\t\t\t\tfix(node, field, cell, search),\n\t\t\t\tsearch,\n\t\t\t\t{ ...before.state, steps },\n\t\t\t\t[...assignment, [field, cell]],\n\t\t\t)\n\t\t},\n\t)\n\tif (Option.isNone(result.assignment)) {\n\t\tsearch.failed.add(key)\n\t}\n\treturn result\n}\n\n/**\n * Puts a value at a path in an object, and gives a new object. A value with\n * parts (`opaque`) becomes an empty object, and it keeps an object that is\n * there.\n */\nconst setPath = (\n\troot: Readonly<Record<string, unknown>>,\n\tnames: ReadonlyArray<string>,\n\tvalue: unknown,\n): Readonly<Record<string, unknown>> => {\n\tconst [name, ...rest] = names\n\tif (name === undefined) {\n\t\treturn root\n\t}\n\t// Only an own field counts, as a condition reads it. An inherited one,\n\t// such as `__proto__`, is not a field of the example.\n\tconst inner = Object.hasOwn(root, name) ? root[name] : undefined\n\tif (rest.length === 0) {\n\t\treturn value === opaque && isObject(inner)\n\t\t\t? root\n\t\t\t: { ...root, [name]: value === opaque ? {} : value }\n\t}\n\tconst base =\n\t\tisObject(inner) && inner !== opaque\n\t\t\t? (inner as Readonly<Record<string, unknown>>)\n\t\t\t: {}\n\treturn { ...root, [name]: setPath(base, rest, value) }\n}\n\nconst exampleOf = (\n\tassignment: ReadonlyArray<readonly [string, unknown]>,\n): Example =>\n\tArr.reduce(\n\t\tassignment,\n\t\t{} as Readonly<Record<string, unknown>>,\n\t\t(root, [field, value]) => setPath(root, field.split(\".\"), value),\n\t)\n\n/**\n * The constants that the tests of each field use. They come from a policy, so\n * they go into a native `Map` and `Set`, as in {@link dedupeCells}. The map\n * belongs to one search.\n */\nconst constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {\n\tconst pairs = (\n\t\tcurrent: Node,\n\t): ReadonlyArray<readonly [string, ReadonlyArray<Nodes.Scalar>]> => {\n\t\tswitch (current._tag) {\n\t\t\tcase \"Compare\":\n\t\t\t\treturn [[current.field, [literalOf(current.value)]]]\n\t\t\tcase \"In\":\n\t\t\t\treturn [[current.field, Arr.map(current.values, literalOf)]]\n\t\t\tcase \"Exists\":\n\t\t\t\treturn [[current.field, []]]\n\t\t\tcase \"Under\":\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.\",\n\t\t\t\t)\n\t\t\tcase \"And\":\n\t\t\tcase \"Or\":\n\t\t\t\treturn Arr.flatMap(current.conditions, pairs)\n\t\t\tcase \"Not\":\n\t\t\t\treturn pairs(current.condition)\n\t\t\tdefault:\n\t\t\t\treturn []\n\t\t}\n\t}\n\tconst all = new Map<string, Set<Nodes.Scalar>>()\n\tArr.forEach(pairs(node), ([field, values]) => {\n\t\tconst known = all.get(field) ?? new Set<Nodes.Scalar>()\n\t\tArr.forEach(values, (value) => known.add(value))\n\t\tall.set(field, known)\n\t})\n\treturn all\n}\n\n/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */\nexport const kindsIn =\n\t(catalog: Catalog.Catalog | undefined) =>\n\t(field: string): Catalog.Kinds =>\n\t\t(catalog === undefined ? undefined : Catalog.lookup(catalog, field))\n\t\t\t?.kinds ?? Catalog.anyKinds\n\n/**\n * Searches for values that make a condition true. `kindsOf` gives the kinds\n * of values of each field. The condition must have no principal fields and no\n * `Under` nodes.\n */\nexport const satisfiable = (\n\tnode: Node,\n\tkindsOf: (field: string) => Catalog.Kinds,\n\tbudget: number = Limits.searchSteps,\n): Satisfiability => {\n\tif (node.opaque) {\n\t\treturn satisfiability.Unknown({ reason: \"predicate\" })\n\t}\n\tconst constants = constantsOf(node)\n\t// The search calls itself one time for each field that it fixes.\n\tif (constants.size > Limits.searchFields) {\n\t\treturn satisfiability.Unknown({ reason: \"budget\" })\n\t}\n\t// The cells of each field, in a native `Map` as the constants are.\n\tconst cells = new Map<string, ReadonlyArray<unknown>>()\n\tconst search: Search = {\n\t\tmembers: new Map(),\n\t\tfailed: new Set(),\n\t\tcells: (field) => {\n\t\t\tconst made = cells.get(field)\n\t\t\tif (made !== undefined) {\n\t\t\t\treturn made\n\t\t\t}\n\t\t\t// The cells come from the constants, so many constants are work\n\t\t\t// that the budget must count before the cells are made.\n\t\t\tconst known = constants.get(field) ?? new Set<Nodes.Scalar>()\n\t\t\tif (known.size > budget) {\n\t\t\t\tthrow new OutOfBudget()\n\t\t\t}\n\t\t\tconst found = cellsOf(kindsOf(field), Array.from(known))\n\t\t\tcells.set(field, found)\n\t\t\treturn found\n\t\t},\n\t\tbudget,\n\t}\n\ttry {\n\t\tconst found = searchNode(Algebra.simplify(node), search, { steps: 0 }, [])\n\t\treturn Option.match(found.assignment, {\n\t\t\tonNone: () => satisfiability.Unsatisfiable(),\n\t\t\tonSome: (assignment) =>\n\t\t\t\tsatisfiability.Satisfiable({ example: exampleOf(assignment) }),\n\t\t})\n\t} catch (error) {\n\t\tif (error instanceof OutOfBudget) {\n\t\t\treturn satisfiability.Unknown({ reason: \"budget\" })\n\t\t}\n\t\tthrow error\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/Condition.ts
CHANGED
|
@@ -412,7 +412,8 @@ type ObjectPaths<
|
|
|
412
412
|
> = [O] extends [never]
|
|
413
413
|
? never
|
|
414
414
|
: // A record is one field to a rule or a request, as `permittedFields`
|
|
415
|
-
// lists it.
|
|
415
|
+
// lists it. So is a struct at the last part that a path may have, since
|
|
416
|
+
// a path does not go below it.
|
|
416
417
|
| (Parents extends true
|
|
417
418
|
? K
|
|
418
419
|
: Arrays extends true
|
|
@@ -420,7 +421,9 @@ type ObjectPaths<
|
|
|
420
421
|
? K
|
|
421
422
|
: number extends keyof O
|
|
422
423
|
? K
|
|
423
|
-
:
|
|
424
|
+
: [Depth] extends [0]
|
|
425
|
+
? K
|
|
426
|
+
: never
|
|
424
427
|
: never)
|
|
425
428
|
| `${K}.${FieldPathInternal<O, PreviousDepth[Depth], Arrays, Parents>}`
|
|
426
429
|
|
package/src/Policy.ts
CHANGED
|
@@ -53,8 +53,7 @@ import type * as Resource from "./Resource.ts"
|
|
|
53
53
|
* @category models
|
|
54
54
|
* @since 0.3.0
|
|
55
55
|
*/
|
|
56
|
-
export type Algorithm =
|
|
57
|
-
"denyOverrides" | "permitOverrides" | "firstApplicable" | "lastApplicable"
|
|
56
|
+
export type Algorithm = PolicyNodes.Algorithm
|
|
58
57
|
|
|
59
58
|
/**
|
|
60
59
|
* A list of rules with a combining algorithm.
|
package/src/internal/algebra.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import * as Arr from "effect/Array"
|
|
13
13
|
import type * as Context from "effect/Context"
|
|
14
14
|
import * as Effect from "effect/Effect"
|
|
15
|
-
import * as HashSet from "effect/HashSet"
|
|
16
15
|
import * as Option from "effect/Option"
|
|
17
16
|
import type * as Resource from "../Resource.ts"
|
|
18
17
|
import * as Nodes from "./nodes.ts"
|
|
@@ -22,19 +21,114 @@ type Node = Nodes.Node
|
|
|
22
21
|
|
|
23
22
|
const { always, never } = Nodes
|
|
24
23
|
|
|
25
|
-
//
|
|
24
|
+
// Keys of structure
|
|
25
|
+
|
|
26
|
+
/** The text of an operand. The type is in it, so `"1"` is not `1`. */
|
|
27
|
+
const operandText = (operand: Nodes.Operand): ReadonlyArray<string> =>
|
|
28
|
+
operand._tag === "Literal"
|
|
29
|
+
? [typeof operand.value, String(operand.value)]
|
|
30
|
+
: ["principal", operand.path]
|
|
31
|
+
|
|
32
|
+
/** The parts of the text of a condition with no predicate. */
|
|
33
|
+
const shapeOf = (node: Node): unknown => {
|
|
34
|
+
switch (node._tag) {
|
|
35
|
+
case "True":
|
|
36
|
+
case "False":
|
|
37
|
+
return [node._tag]
|
|
38
|
+
case "Compare":
|
|
39
|
+
return [node._tag, node.op, node.field, operandText(node.value)]
|
|
40
|
+
case "In":
|
|
41
|
+
return [node._tag, node.field, Arr.map(node.values, operandText)]
|
|
42
|
+
case "Exists":
|
|
43
|
+
return [node._tag, node.field]
|
|
44
|
+
case "Under":
|
|
45
|
+
return [node._tag, Arr.map(node.prefix, operandText)]
|
|
46
|
+
case "And":
|
|
47
|
+
case "Or":
|
|
48
|
+
return [node._tag, Arr.map(node.conditions, shapeOf)]
|
|
49
|
+
case "Not":
|
|
50
|
+
return [node._tag, shapeOf(node.condition)]
|
|
51
|
+
case "Predicate":
|
|
52
|
+
// A smart constructor and a search do not ask for the text of a
|
|
53
|
+
// predicate: it is equal only to itself.
|
|
54
|
+
throw new Error("A predicate has no text of structure")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
26
57
|
|
|
27
58
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
59
|
+
* The text of the structure of a condition with no predicate. Two conditions
|
|
60
|
+
* with one text give the same answer for every value. It reads all of the
|
|
61
|
+
* condition, so a search counts its size in the budget.
|
|
30
62
|
*/
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
const structureText = (node: Node): string => JSON.stringify(shapeOf(node))
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The work of the text of the structure of a condition: each node, and each
|
|
67
|
+
* value of an `In` test or an `Under` test. A node counts as one in `size`,
|
|
68
|
+
* also an `In` test with very many values. This walk reads the nodes only,
|
|
69
|
+
* so it takes `size` steps.
|
|
70
|
+
*/
|
|
71
|
+
const weightOf = (node: Node): number => {
|
|
72
|
+
switch (node._tag) {
|
|
73
|
+
case "In":
|
|
74
|
+
return 1 + node.values.length
|
|
75
|
+
case "Under":
|
|
76
|
+
return 1 + node.prefix.length
|
|
77
|
+
case "And":
|
|
78
|
+
case "Or":
|
|
79
|
+
return Arr.reduce(
|
|
80
|
+
node.conditions,
|
|
81
|
+
1,
|
|
82
|
+
(total, child) => total + weightOf(child),
|
|
83
|
+
)
|
|
84
|
+
case "Not":
|
|
85
|
+
return 1 + weightOf(node.condition)
|
|
86
|
+
default:
|
|
87
|
+
return 1
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The most nodes of a condition that a smart constructor reads for its key,
|
|
93
|
+
* and the most children of a child that it puts into a node. A larger child
|
|
94
|
+
* is its own key, and it stays one child, so the work of a constructor stays
|
|
95
|
+
* small. A deep chain of `And` nodes, for one, does not copy all its leaves
|
|
96
|
+
* at each level.
|
|
97
|
+
*/
|
|
98
|
+
const keyNodes = 64
|
|
99
|
+
|
|
100
|
+
/** The most work of a key, with the values of its tests: see {@link weightOf}. */
|
|
101
|
+
const keyWeight = 256
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The key of a child in a smart constructor: the text of its structure, or
|
|
105
|
+
* the node itself when it is large. The keys go into a native `Set`, not a
|
|
106
|
+
* `HashSet` of Effect. The hash of a string in Effect has no random seed, so
|
|
107
|
+
* a policy could hold very many strings with one hash, and each test of the
|
|
108
|
+
* set would read all of them. A native `Set` finds a node by the node itself,
|
|
109
|
+
* and it hashes a string with the random seed of the engine.
|
|
110
|
+
*/
|
|
111
|
+
export const keyOf = (node: Node): string | Node =>
|
|
112
|
+
node.size > keyNodes || weightOf(node) > keyWeight
|
|
113
|
+
? node
|
|
114
|
+
: structureText(node)
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Keeps the first node of each structure, in order. A large node is a copy
|
|
118
|
+
* only of itself.
|
|
119
|
+
*/
|
|
120
|
+
const dedupe = (nodes: ReadonlyArray<Node>): ReadonlyArray<Node> => {
|
|
121
|
+
// The set belongs to this call.
|
|
122
|
+
const seen = new Set<string | Node>()
|
|
123
|
+
return Arr.filter(nodes, (node) => {
|
|
124
|
+
const key = keyOf(node)
|
|
125
|
+
if (seen.has(key)) {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
seen.add(key)
|
|
129
|
+
return true
|
|
130
|
+
})
|
|
131
|
+
}
|
|
38
132
|
|
|
39
133
|
/**
|
|
40
134
|
* Removes the children that the other children make useless. In an `Or`,
|
|
@@ -44,15 +138,18 @@ const absorb = (
|
|
|
44
138
|
inner: "And" | "Or",
|
|
45
139
|
nodes: ReadonlyArray<Node>,
|
|
46
140
|
): ReadonlyArray<Node> => {
|
|
47
|
-
|
|
141
|
+
// A native `Set`, as in `dedupe`.
|
|
142
|
+
const all = new Set(Arr.map(nodes, keyOf))
|
|
48
143
|
return Arr.filter(
|
|
49
144
|
nodes,
|
|
50
145
|
(node) =>
|
|
51
146
|
node._tag !== inner ||
|
|
52
|
-
!Arr.some(node.conditions, (part) =>
|
|
147
|
+
!Arr.some(node.conditions, (part) => all.has(keyOf(part))),
|
|
53
148
|
)
|
|
54
149
|
}
|
|
55
150
|
|
|
151
|
+
// Smart constructors
|
|
152
|
+
|
|
56
153
|
/**
|
|
57
154
|
* Gives the result of an `And` with a `False` child, or of an `Or` with a
|
|
58
155
|
* `True` child. The children are tested in order, so a predicate before the
|
|
@@ -81,7 +178,8 @@ const shortCircuit = (
|
|
|
81
178
|
/**
|
|
82
179
|
* Makes an `And` or an `Or` of simple children, and makes it simple. It looks
|
|
83
180
|
* one level down only:
|
|
84
|
-
* - it puts the children of a child of the same kind into the node
|
|
181
|
+
* - it puts the children of a child of the same kind into the node, when
|
|
182
|
+
* the child has at most `keyNodes` of them;
|
|
85
183
|
* - it stops at a `False` in an `And`, or at a `True` in an `Or`;
|
|
86
184
|
* - it removes `True` from an `And`, `False` from an `Or`, copies, and the
|
|
87
185
|
* children that other children make useless.
|
|
@@ -94,7 +192,9 @@ const junction = (tag: "And" | "Or", children: ReadonlyArray<Node>): Node => {
|
|
|
94
192
|
const constant = tag === "And" ? "False" : "True"
|
|
95
193
|
const neutral = tag === "And" ? "True" : "False"
|
|
96
194
|
const flat = Arr.flatMap(children, (child) =>
|
|
97
|
-
child._tag === tag && !child.opaque
|
|
195
|
+
child._tag === tag && !child.opaque && child.conditions.length <= keyNodes
|
|
196
|
+
? child.conditions
|
|
197
|
+
: [child],
|
|
98
198
|
)
|
|
99
199
|
return Option.match(
|
|
100
200
|
Arr.findFirstIndex(flat, (child) => child._tag === constant),
|
|
@@ -490,10 +590,7 @@ export const fields = (node: Node): ReadonlyArray<string> => {
|
|
|
490
590
|
return []
|
|
491
591
|
}
|
|
492
592
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
HashSet.has(seen, field) ? [] : [field],
|
|
497
|
-
])[1],
|
|
498
|
-
)
|
|
593
|
+
// A native `Set`, as in `dedupe`: a field of a condition that is not bound
|
|
594
|
+
// to a domain may be any string.
|
|
595
|
+
return Array.from(new Set(all(node)))
|
|
499
596
|
}
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
// language service check for `unknown` in an Effect is off here.
|
|
8
8
|
// @effect-diagnostics anyUnknownInErrorContext:off
|
|
9
9
|
import * as Cache from "effect/Cache"
|
|
10
|
+
import * as Chunk from "effect/Chunk"
|
|
10
11
|
import * as Context from "effect/Context"
|
|
11
12
|
import type * as Duration from "effect/Duration"
|
|
12
13
|
import * as Effect from "effect/Effect"
|
|
13
14
|
import * as Equal from "effect/Equal"
|
|
14
15
|
import * as Hash from "effect/Hash"
|
|
16
|
+
import * as HashMap from "effect/HashMap"
|
|
17
|
+
import * as HashSet from "effect/HashSet"
|
|
15
18
|
import * as Layer from "effect/Layer"
|
|
16
19
|
import * as Result from "effect/Result"
|
|
17
20
|
import * as Schema from "effect/Schema"
|
|
@@ -103,18 +106,24 @@ type AnyTarget = {
|
|
|
103
106
|
class PrincipalKey implements Equal.Equal {
|
|
104
107
|
readonly principal: object
|
|
105
108
|
readonly text: string
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
/**
|
|
110
|
+
* The order of the parts of the decoded principal, from {@link orderOf},
|
|
111
|
+
* when the key is the JSON form. The decoded principals must be equal
|
|
112
|
+
* too, with their parts in the same order. `undefined` for a key from
|
|
113
|
+
* `cache.key`.
|
|
114
|
+
*/
|
|
115
|
+
readonly order: string | undefined
|
|
116
|
+
constructor(principal: object, text: string, order: string | undefined) {
|
|
109
117
|
this.principal = principal
|
|
110
118
|
this.text = text
|
|
111
|
-
this.
|
|
119
|
+
this.order = order
|
|
112
120
|
}
|
|
113
121
|
[Equal.symbol](that: unknown): boolean {
|
|
114
122
|
return (
|
|
115
123
|
that instanceof PrincipalKey &&
|
|
116
124
|
that.text === this.text &&
|
|
117
|
-
|
|
125
|
+
that.order === this.order &&
|
|
126
|
+
(this.order === undefined || Equal.equals(that.principal, this.principal))
|
|
118
127
|
)
|
|
119
128
|
}
|
|
120
129
|
[Hash.symbol](): number {
|
|
@@ -123,14 +132,36 @@ class PrincipalKey implements Equal.Equal {
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* tells them apart. So two principals with one key are equal, and the cache
|
|
132
|
-
* can not give one the policy of the other.
|
|
135
|
+
* The text of the order of the parts of a value: the names of the fields of
|
|
136
|
+
* each object, and the items of each list, set and map, in the order that a
|
|
137
|
+
* loop sees them. `Equal` does not see this order, but a loader may read it,
|
|
138
|
+
* as `Object.values` does. So two keys need the same order too. A scalar has
|
|
139
|
+
* no parts, and `Equal` compares it.
|
|
133
140
|
*/
|
|
141
|
+
const orderOf = (value: unknown): string => {
|
|
142
|
+
if (typeof value !== "object" || value === null) {
|
|
143
|
+
return ""
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
return `[${value.map(orderOf).join(",")}]`
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
value instanceof Map ||
|
|
150
|
+
value instanceof Set ||
|
|
151
|
+
HashMap.isHashMap(value) ||
|
|
152
|
+
HashSet.isHashSet(value) ||
|
|
153
|
+
Chunk.isChunk(value)
|
|
154
|
+
) {
|
|
155
|
+
return `<${Array.from(value as Iterable<unknown>, orderOf).join(",")}>`
|
|
156
|
+
}
|
|
157
|
+
return `{${Object.keys(value)
|
|
158
|
+
.map(
|
|
159
|
+
(name) =>
|
|
160
|
+
`${JSON.stringify(name)}:${orderOf((value as Record<string, unknown>)[name])}`,
|
|
161
|
+
)
|
|
162
|
+
.join(",")}}`
|
|
163
|
+
}
|
|
164
|
+
|
|
134
165
|
/**
|
|
135
166
|
* Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON
|
|
136
167
|
* form, as a struct with a symbol key has none.
|
|
@@ -148,6 +179,17 @@ const jsonCodecOf = (
|
|
|
148
179
|
}
|
|
149
180
|
}
|
|
150
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Makes the cache key of a principal from its JSON form. The key keeps the
|
|
184
|
+
* principal that the Schema decodes from that form, so the loader sees only
|
|
185
|
+
* the data that the key shows. A principal gets no key when it does not
|
|
186
|
+
* encode, or when the decoded value is not `Equal` to it. Two classes, or a
|
|
187
|
+
* string and a number, may have one JSON form, as in a union, and `Equal`
|
|
188
|
+
* tells them apart. A Schema may make data when it decodes, as the order of
|
|
189
|
+
* the fields of a record, so the key keeps that order too. So two
|
|
190
|
+
* principals with one key are equal, and the cache can not give one the
|
|
191
|
+
* policy of the other.
|
|
192
|
+
*/
|
|
151
193
|
const jsonKey = (
|
|
152
194
|
schema: Schema.Top,
|
|
153
195
|
): ((principal: object) => PrincipalKey | undefined) => {
|
|
@@ -170,7 +212,7 @@ const jsonKey = (
|
|
|
170
212
|
onFailure: () => undefined,
|
|
171
213
|
onSuccess: (decoded) =>
|
|
172
214
|
Equal.equals(decoded, principal)
|
|
173
|
-
? new PrincipalKey(decoded as object, text,
|
|
215
|
+
? new PrincipalKey(decoded as object, text, orderOf(decoded))
|
|
174
216
|
: undefined,
|
|
175
217
|
})
|
|
176
218
|
},
|
|
@@ -236,7 +278,7 @@ export const makeLayer = (
|
|
|
236
278
|
const text = custom(principal)
|
|
237
279
|
return text === undefined
|
|
238
280
|
? undefined
|
|
239
|
-
: new PrincipalKey(canonical?.principal ?? principal, text,
|
|
281
|
+
: new PrincipalKey(canonical?.principal ?? principal, text, undefined)
|
|
240
282
|
}
|
|
241
283
|
return Layer.effect(
|
|
242
284
|
service,
|