@nmnmcc/ability 2.1.1 → 2.1.3

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 (41) hide show
  1. package/README.md +2 -2
  2. package/dist/Authorizer.d.ts +11 -11
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js.map +1 -1
  5. package/dist/Condition.d.ts +6 -4
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +5 -3
  8. package/dist/Condition.js.map +1 -1
  9. package/dist/Policy.d.ts +26 -15
  10. package/dist/Policy.d.ts.map +1 -1
  11. package/dist/Policy.js +7 -6
  12. package/dist/Policy.js.map +1 -1
  13. package/dist/internal/algebra.js +8 -5
  14. package/dist/internal/algebra.js.map +1 -1
  15. package/dist/internal/authorizer.js +23 -9
  16. package/dist/internal/authorizer.js.map +1 -1
  17. package/dist/internal/bind.js +16 -10
  18. package/dist/internal/bind.js.map +1 -1
  19. package/dist/internal/catalog.d.ts.map +1 -1
  20. package/dist/internal/catalog.js +37 -7
  21. package/dist/internal/catalog.js.map +1 -1
  22. package/dist/internal/decide.js +2 -2
  23. package/dist/internal/decide.js.map +1 -1
  24. package/dist/internal/patterns.js +26 -26
  25. package/dist/internal/patterns.js.map +1 -1
  26. package/dist/internal/policy-nodes.js +21 -12
  27. package/dist/internal/policy-nodes.js.map +1 -1
  28. package/dist/internal/search.js +38 -26
  29. package/dist/internal/search.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/Authorizer.ts +10 -10
  32. package/src/Condition.ts +14 -8
  33. package/src/Policy.ts +34 -17
  34. package/src/internal/algebra.ts +12 -8
  35. package/src/internal/authorizer.ts +49 -34
  36. package/src/internal/bind.ts +28 -30
  37. package/src/internal/catalog.ts +50 -14
  38. package/src/internal/decide.ts +3 -1
  39. package/src/internal/patterns.ts +37 -29
  40. package/src/internal/policy-nodes.ts +19 -13
  41. package/src/internal/search.ts +31 -21
@@ -145,20 +145,26 @@ const fix = (root, field, value, search) => run(root.depth, (R) => {
145
145
  return go(root);
146
146
  });
147
147
  /** The fields of the tests of a condition, once for each test. */
148
- const testedFields = (root) => run(root.depth, (R) => {
149
- const go = (node) => {
150
- switch (node._tag) {
151
- case "Compare":
152
- case "In":
153
- case "Exists": return R.succeed([node.field]);
154
- case "And":
155
- case "Or": return R.map(R.forEach(node.conditions, (child) => R.suspend(() => go(child))), Arr.flatten);
156
- case "Not": return R.suspend(() => go(node.condition));
157
- default: return R.succeed([]);
158
- }
159
- };
160
- return go(root);
161
- });
148
+ const testedFields = (root) => {
149
+ const fields = [];
150
+ run(root.depth, (R) => {
151
+ const go = (node) => {
152
+ switch (node._tag) {
153
+ case "Compare":
154
+ case "In":
155
+ case "Exists":
156
+ fields.push(node.field);
157
+ return R.succeed(void 0);
158
+ case "And":
159
+ case "Or": return R.map(R.forEach(node.conditions, (child) => R.suspend(() => go(child))), () => void 0);
160
+ case "Not": return R.suspend(() => go(node.condition));
161
+ default: return R.succeed(void 0);
162
+ }
163
+ };
164
+ return go(root);
165
+ });
166
+ return fields;
167
+ };
162
168
  /**
163
169
  * Picks the next field to split on. A field comes before the fields below it,
164
170
  * so a value of a field can fix them. Then a field in more tests comes first,
@@ -240,27 +246,33 @@ const exampleOf = (assignment) => Arr.reduce(assignment, {}, (root, [field, valu
240
246
  * belongs to one search.
241
247
  */
242
248
  const constantsOf = (node) => {
243
- const pairs = run(node.depth, (R) => {
249
+ const all = /* @__PURE__ */ new Map();
250
+ const add = (field, values) => {
251
+ const known = all.get(field) ?? /* @__PURE__ */ new Set();
252
+ Arr.forEach(values, (value) => known.add(value));
253
+ all.set(field, known);
254
+ };
255
+ run(node.depth, (R) => {
244
256
  const go = (current) => {
245
257
  switch (current._tag) {
246
- case "Compare": return R.succeed([[current.field, [literalOf(current.value)]]]);
247
- case "In": return R.succeed([[current.field, Arr.map(current.values, literalOf)]]);
248
- case "Exists": return R.succeed([[current.field, []]]);
258
+ case "Compare":
259
+ add(current.field, [literalOf(current.value)]);
260
+ return R.succeed(void 0);
261
+ case "In":
262
+ add(current.field, Arr.map(current.values, literalOf));
263
+ return R.succeed(void 0);
264
+ case "Exists":
265
+ add(current.field, []);
266
+ return R.succeed(void 0);
249
267
  case "Under": throw new Error("Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.");
250
268
  case "And":
251
- case "Or": return R.map(R.forEach(current.conditions, (child) => R.suspend(() => go(child))), Arr.flatten);
269
+ case "Or": return R.map(R.forEach(current.conditions, (child) => R.suspend(() => go(child))), () => void 0);
252
270
  case "Not": return R.suspend(() => go(current.condition));
253
- default: return R.succeed([]);
271
+ default: return R.succeed(void 0);
254
272
  }
255
273
  };
256
274
  return go(node);
257
275
  });
258
- const all = /* @__PURE__ */ new Map();
259
- Arr.forEach(pairs, ([field, values]) => {
260
- const known = all.get(field) ?? /* @__PURE__ */ new Set();
261
- Arr.forEach(values, (value) => known.add(value));
262
- all.set(field, known);
263
- });
264
276
  return all;
265
277
  };
266
278
  /** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","names":["Algebra.testCompare","Trampoline.run","Nodes.always","Nodes.never","Algebra.and","Algebra.or","Algebra.not","Algebra.keyOf","Catalog.lookup","Catalog.anyKinds","Limits.searchSteps","Limits.searchFields","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\"\nimport * as Trampoline from \"./trampoline.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 = (root: Node, field: string, value: unknown, search: Search): Node =>\n\t// The walks of a condition take the steps of a trampoline, so a deep\n\t// condition does not fill the stack.\n\tTrampoline.run(root.depth, (R) => {\n\t\tconst go = (node: Node): Trampoline.Step<Node> => {\n\t\t\tconst children = (nodes: ReadonlyArray<Node>) =>\n\t\t\t\tR.forEach(nodes, (child) => R.suspend(() => go(child)))\n\t\t\tswitch (node._tag) {\n\t\t\t\tcase \"Compare\":\n\t\t\t\tcase \"In\":\n\t\t\t\tcase \"Exists\": {\n\t\t\t\t\tconst known = fixedValue(node.field, field, value)\n\t\t\t\t\treturn R.succeed(\n\t\t\t\t\t\tknown === free\n\t\t\t\t\t\t\t? node\n\t\t\t\t\t\t\t: testFixed(node, known, search)\n\t\t\t\t\t\t\t\t? Nodes.always\n\t\t\t\t\t\t\t\t: Nodes.never,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tcase \"And\":\n\t\t\t\t\treturn R.map(children(node.conditions), Algebra.and)\n\t\t\t\tcase \"Or\":\n\t\t\t\t\treturn R.map(children(node.conditions), Algebra.or)\n\t\t\t\tcase \"Not\":\n\t\t\t\t\treturn R.map(\n\t\t\t\t\t\tR.suspend(() => go(node.condition)),\n\t\t\t\t\t\tAlgebra.not,\n\t\t\t\t\t)\n\t\t\t\tdefault:\n\t\t\t\t\treturn R.succeed(node)\n\t\t\t}\n\t\t}\n\t\treturn go(root)\n\t})\n\n/** The fields of the tests of a condition, once for each test. */\nconst testedFields = (root: Node): ReadonlyArray<string> =>\n\tTrampoline.run(root.depth, (R) => {\n\t\tconst go = (node: Node): Trampoline.Step<ReadonlyArray<string>> => {\n\t\t\tswitch (node._tag) {\n\t\t\t\tcase \"Compare\":\n\t\t\t\tcase \"In\":\n\t\t\t\tcase \"Exists\":\n\t\t\t\t\treturn R.succeed([node.field])\n\t\t\t\tcase \"And\":\n\t\t\t\tcase \"Or\":\n\t\t\t\t\treturn R.map(\n\t\t\t\t\t\tR.forEach(node.conditions, (child) => R.suspend(() => go(child))),\n\t\t\t\t\t\tArr.flatten,\n\t\t\t\t\t)\n\t\t\t\tcase \"Not\":\n\t\t\t\t\treturn R.suspend(() => go(node.condition))\n\t\t\t\tdefault:\n\t\t\t\t\treturn R.succeed([])\n\t\t\t}\n\t\t}\n\t\treturn go(root)\n\t})\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 = (search: Search) => (R: Trampoline.Runner) => {\n\tconst go = (\n\t\tnode: Node,\n\t\tstate: SearchState,\n\t\tassignment: ReadonlyArray<readonly [string, unknown]>,\n\t): Trampoline.Step<Found> => {\n\t\tif (node._tag === \"True\") {\n\t\t\treturn R.succeed({ assignment: Option.some(assignment), state })\n\t\t}\n\t\tif (node._tag === \"False\") {\n\t\t\treturn R.succeed({ assignment: Option.none(), state })\n\t\t}\n\t\t// The key reads at most a small part of the node: a node with very\n\t\t// many nodes, or with very many values in its `In` tests, is its own\n\t\t// key. A step reads the nodes but not the values, which `membersOf`\n\t\t// keeps, so the budget counts the size.\n\t\tconst key = Algebra.keyOf(node)\n\t\tif (search.failed.has(key)) {\n\t\t\treturn R.succeed({ assignment: Option.none(), state })\n\t\t}\n\t\tif (state.steps + node.size > search.budget) {\n\t\t\tthrow new OutOfBudget()\n\t\t}\n\t\tconst field = nextField(node)\n\t\t// The cells are tried in order, until one gives values. A fold over\n\t\t// the cells does no work after a found answer, and it does not grow\n\t\t// the stack with the number of cells. Each call for the next field\n\t\t// is a step of the runner, so very many fields do not fill the stack.\n\t\treturn R.map(\n\t\t\tR.reduce(\n\t\t\t\tsearch.cells(field),\n\t\t\t\t{\n\t\t\t\t\tassignment: Option.none(),\n\t\t\t\t\tstate: { ...state, steps: state.steps + node.size },\n\t\t\t\t} as Found,\n\t\t\t\t(before, cell): Trampoline.Step<Found> => {\n\t\t\t\t\tif (Option.isSome(before.assignment)) {\n\t\t\t\t\t\treturn R.succeed(before)\n\t\t\t\t\t}\n\t\t\t\t\tconst steps = before.state.steps + node.size\n\t\t\t\t\tif (steps > search.budget) {\n\t\t\t\t\t\tthrow new OutOfBudget()\n\t\t\t\t\t}\n\t\t\t\t\treturn R.suspend(() =>\n\t\t\t\t\t\tgo(fix(node, field, cell, search), { ...before.state, steps }, [\n\t\t\t\t\t\t\t...assignment,\n\t\t\t\t\t\t\t[field, cell],\n\t\t\t\t\t\t]),\n\t\t\t\t\t)\n\t\t\t\t},\n\t\t\t),\n\t\t\t(result) => {\n\t\t\t\tif (Option.isNone(result.assignment)) {\n\t\t\t\t\tsearch.failed.add(key)\n\t\t\t\t}\n\t\t\t\treturn result\n\t\t\t},\n\t\t)\n\t}\n\treturn go\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 */\ntype Fields = Readonly<Record<string, unknown>>\n\nconst setPath = (\n\troot: Fields,\n\tnames: ReadonlyArray<string>,\n\tvalue: unknown,\n): Fields => {\n\tconst last = names[names.length - 1]\n\tif (last === 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 own = (base: Fields, name: string): unknown =>\n\t\tObject.hasOwn(base, name) ? base[name] : undefined\n\t// The object at each part of the path, from the root down to the object\n\t// that gets the value. A part that is not an object becomes empty. The\n\t// path is walked in a loop, so a long path does not fill the stack.\n\tconst bases = Arr.scan(names.slice(0, -1), root, (base, name): Fields => {\n\t\tconst inner = own(base, name)\n\t\treturn isObject(inner) && inner !== opaque ? (inner as Fields) : {}\n\t})\n\tconst target = bases[bases.length - 1]!\n\tconst inner = own(target, last)\n\tconst placed: Fields =\n\t\tvalue === opaque && isObject(inner)\n\t\t\t? target\n\t\t\t: { ...target, [last]: value === opaque ? {} : value }\n\t// Each object above gets the new object below it, from the bottom up.\n\treturn Arr.reduceRight(\n\t\tnames.slice(0, -1),\n\t\tplaced,\n\t\t(below, name, index): Fields => ({ ...bases[index]!, [name]: below }),\n\t)\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\ttype Pair = readonly [string, ReadonlyArray<Nodes.Scalar>]\n\t// The walk takes the steps of a trampoline, so a deep condition does not\n\t// fill the stack.\n\tconst pairs = Trampoline.run(node.depth, (R) => {\n\t\tconst go = (current: Node): Trampoline.Step<ReadonlyArray<Pair>> => {\n\t\t\tswitch (current._tag) {\n\t\t\t\tcase \"Compare\":\n\t\t\t\t\treturn R.succeed([[current.field, [literalOf(current.value)]]])\n\t\t\t\tcase \"In\":\n\t\t\t\t\treturn R.succeed([\n\t\t\t\t\t\t[current.field, Arr.map(current.values, literalOf)],\n\t\t\t\t\t])\n\t\t\t\tcase \"Exists\":\n\t\t\t\t\treturn R.succeed([[current.field, []]])\n\t\t\t\tcase \"Under\":\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.\",\n\t\t\t\t\t)\n\t\t\t\tcase \"And\":\n\t\t\t\tcase \"Or\":\n\t\t\t\t\treturn R.map(\n\t\t\t\t\t\tR.forEach(current.conditions, (child) =>\n\t\t\t\t\t\t\tR.suspend(() => go(child)),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tArr.flatten,\n\t\t\t\t\t)\n\t\t\t\tcase \"Not\":\n\t\t\t\t\treturn R.suspend(() => go(current.condition))\n\t\t\t\tdefault:\n\t\t\t\t\treturn R.succeed([])\n\t\t\t}\n\t\t}\n\t\treturn go(node)\n\t})\n\tconst all = new Map<string, Set<Nodes.Scalar>>()\n\tArr.forEach(pairs, ([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, parts?: number) =>\n\t(field: string): Catalog.Kinds =>\n\t\t(catalog === undefined ? undefined : Catalog.lookup(catalog, field, parts))\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\tfields: number = Limits.searchFields,\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 > fields) {\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\t// The search goes one call deeper for each field, so the runner is the\n\t\t// one for that many levels.\n\t\tconst found = Trampoline.run(constants.size, (R) =>\n\t\t\tsearchNode(search)(R)(Algebra.simplify(node), { steps: 0 }, []),\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","Trampoline.run","Nodes.always","Nodes.never","Algebra.and","Algebra.or","Algebra.not","Algebra.keyOf","Catalog.lookup","Catalog.anyKinds","Limits.searchSteps","Limits.searchFields","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\"\nimport * as Trampoline from \"./trampoline.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 = (root: Node, field: string, value: unknown, search: Search): Node =>\n\t// The walks of a condition take the steps of a trampoline, so a deep\n\t// condition does not fill the stack.\n\tTrampoline.run(root.depth, (R) => {\n\t\tconst go = (node: Node): Trampoline.Step<Node> => {\n\t\t\tconst children = (nodes: ReadonlyArray<Node>) =>\n\t\t\t\tR.forEach(nodes, (child) => R.suspend(() => go(child)))\n\t\t\tswitch (node._tag) {\n\t\t\t\tcase \"Compare\":\n\t\t\t\tcase \"In\":\n\t\t\t\tcase \"Exists\": {\n\t\t\t\t\tconst known = fixedValue(node.field, field, value)\n\t\t\t\t\treturn R.succeed(\n\t\t\t\t\t\tknown === free\n\t\t\t\t\t\t\t? node\n\t\t\t\t\t\t\t: testFixed(node, known, search)\n\t\t\t\t\t\t\t\t? Nodes.always\n\t\t\t\t\t\t\t\t: Nodes.never,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tcase \"And\":\n\t\t\t\t\treturn R.map(children(node.conditions), Algebra.and)\n\t\t\t\tcase \"Or\":\n\t\t\t\t\treturn R.map(children(node.conditions), Algebra.or)\n\t\t\t\tcase \"Not\":\n\t\t\t\t\treturn R.map(\n\t\t\t\t\t\tR.suspend(() => go(node.condition)),\n\t\t\t\t\t\tAlgebra.not,\n\t\t\t\t\t)\n\t\t\t\tdefault:\n\t\t\t\t\treturn R.succeed(node)\n\t\t\t}\n\t\t}\n\t\treturn go(root)\n\t})\n\n/** The fields of the tests of a condition, once for each test. */\nconst testedFields = (root: Node): ReadonlyArray<string> => {\n\t// The fields go into one list, in order. A walk that joined the lists of\n\t// the children would copy the list of a deep condition at each level. The\n\t// list belongs to this walk.\n\tconst fields: Array<string> = []\n\tTrampoline.run(root.depth, (R) => {\n\t\tconst go = (node: Node): Trampoline.Step<undefined> => {\n\t\t\tswitch (node._tag) {\n\t\t\t\tcase \"Compare\":\n\t\t\t\tcase \"In\":\n\t\t\t\tcase \"Exists\":\n\t\t\t\t\tfields.push(node.field)\n\t\t\t\t\treturn R.succeed(undefined)\n\t\t\t\tcase \"And\":\n\t\t\t\tcase \"Or\":\n\t\t\t\t\treturn R.map(\n\t\t\t\t\t\tR.forEach(node.conditions, (child) => R.suspend(() => go(child))),\n\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t)\n\t\t\t\tcase \"Not\":\n\t\t\t\t\treturn R.suspend(() => go(node.condition))\n\t\t\t\tdefault:\n\t\t\t\t\treturn R.succeed(undefined)\n\t\t\t}\n\t\t}\n\t\treturn go(root)\n\t})\n\treturn fields\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 = (search: Search) => (R: Trampoline.Runner) => {\n\tconst go = (\n\t\tnode: Node,\n\t\tstate: SearchState,\n\t\tassignment: ReadonlyArray<readonly [string, unknown]>,\n\t): Trampoline.Step<Found> => {\n\t\tif (node._tag === \"True\") {\n\t\t\treturn R.succeed({ assignment: Option.some(assignment), state })\n\t\t}\n\t\tif (node._tag === \"False\") {\n\t\t\treturn R.succeed({ assignment: Option.none(), state })\n\t\t}\n\t\t// The key reads at most a small part of the node: a node with very\n\t\t// many nodes, or with very many values in its `In` tests, is its own\n\t\t// key. A step reads the nodes but not the values, which `membersOf`\n\t\t// keeps, so the budget counts the size.\n\t\tconst key = Algebra.keyOf(node)\n\t\tif (search.failed.has(key)) {\n\t\t\treturn R.succeed({ assignment: Option.none(), state })\n\t\t}\n\t\tif (state.steps + node.size > search.budget) {\n\t\t\tthrow new OutOfBudget()\n\t\t}\n\t\tconst field = nextField(node)\n\t\t// The cells are tried in order, until one gives values. A fold over\n\t\t// the cells does no work after a found answer, and it does not grow\n\t\t// the stack with the number of cells. Each call for the next field\n\t\t// is a step of the runner, so very many fields do not fill the stack.\n\t\treturn R.map(\n\t\t\tR.reduce(\n\t\t\t\tsearch.cells(field),\n\t\t\t\t{\n\t\t\t\t\tassignment: Option.none(),\n\t\t\t\t\tstate: { ...state, steps: state.steps + node.size },\n\t\t\t\t} as Found,\n\t\t\t\t(before, cell): Trampoline.Step<Found> => {\n\t\t\t\t\tif (Option.isSome(before.assignment)) {\n\t\t\t\t\t\treturn R.succeed(before)\n\t\t\t\t\t}\n\t\t\t\t\tconst steps = before.state.steps + node.size\n\t\t\t\t\tif (steps > search.budget) {\n\t\t\t\t\t\tthrow new OutOfBudget()\n\t\t\t\t\t}\n\t\t\t\t\treturn R.suspend(() =>\n\t\t\t\t\t\tgo(fix(node, field, cell, search), { ...before.state, steps }, [\n\t\t\t\t\t\t\t...assignment,\n\t\t\t\t\t\t\t[field, cell],\n\t\t\t\t\t\t]),\n\t\t\t\t\t)\n\t\t\t\t},\n\t\t\t),\n\t\t\t(result) => {\n\t\t\t\tif (Option.isNone(result.assignment)) {\n\t\t\t\t\tsearch.failed.add(key)\n\t\t\t\t}\n\t\t\t\treturn result\n\t\t\t},\n\t\t)\n\t}\n\treturn go\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 */\ntype Fields = Readonly<Record<string, unknown>>\n\nconst setPath = (\n\troot: Fields,\n\tnames: ReadonlyArray<string>,\n\tvalue: unknown,\n): Fields => {\n\tconst last = names[names.length - 1]\n\tif (last === 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 own = (base: Fields, name: string): unknown =>\n\t\tObject.hasOwn(base, name) ? base[name] : undefined\n\t// The object at each part of the path, from the root down to the object\n\t// that gets the value. A part that is not an object becomes empty. The\n\t// path is walked in a loop, so a long path does not fill the stack.\n\tconst bases = Arr.scan(names.slice(0, -1), root, (base, name): Fields => {\n\t\tconst inner = own(base, name)\n\t\treturn isObject(inner) && inner !== opaque ? (inner as Fields) : {}\n\t})\n\tconst target = bases[bases.length - 1]!\n\tconst inner = own(target, last)\n\tconst placed: Fields =\n\t\tvalue === opaque && isObject(inner)\n\t\t\t? target\n\t\t\t: { ...target, [last]: value === opaque ? {} : value }\n\t// Each object above gets the new object below it, from the bottom up.\n\treturn Arr.reduceRight(\n\t\tnames.slice(0, -1),\n\t\tplaced,\n\t\t(below, name, index): Fields => ({ ...bases[index]!, [name]: below }),\n\t)\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\t// The constants go into one map as the walk finds them. A walk that\n\t// joined the lists of the children would copy the list of a deep\n\t// condition at each level. The map belongs to one search.\n\tconst all = new Map<string, Set<Nodes.Scalar>>()\n\tconst add = (field: string, values: ReadonlyArray<Nodes.Scalar>): void => {\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\t// The walk takes the steps of a trampoline, so a deep condition does not\n\t// fill the stack.\n\tTrampoline.run(node.depth, (R) => {\n\t\tconst go = (current: Node): Trampoline.Step<undefined> => {\n\t\t\tswitch (current._tag) {\n\t\t\t\tcase \"Compare\":\n\t\t\t\t\tadd(current.field, [literalOf(current.value)])\n\t\t\t\t\treturn R.succeed(undefined)\n\t\t\t\tcase \"In\":\n\t\t\t\t\tadd(current.field, Arr.map(current.values, literalOf))\n\t\t\t\t\treturn R.succeed(undefined)\n\t\t\t\tcase \"Exists\":\n\t\t\t\t\tadd(current.field, [])\n\t\t\t\t\treturn R.succeed(undefined)\n\t\t\t\tcase \"Under\":\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.\",\n\t\t\t\t\t)\n\t\t\t\tcase \"And\":\n\t\t\t\tcase \"Or\":\n\t\t\t\t\treturn R.map(\n\t\t\t\t\t\tR.forEach(current.conditions, (child) =>\n\t\t\t\t\t\t\tR.suspend(() => go(child)),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t)\n\t\t\t\tcase \"Not\":\n\t\t\t\t\treturn R.suspend(() => go(current.condition))\n\t\t\t\tdefault:\n\t\t\t\t\treturn R.succeed(undefined)\n\t\t\t}\n\t\t}\n\t\treturn go(node)\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, parts?: number) =>\n\t(field: string): Catalog.Kinds =>\n\t\t(catalog === undefined ? undefined : Catalog.lookup(catalog, field, parts))\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\tfields: number = Limits.searchFields,\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 > fields) {\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\t// The search goes one call deeper for each field, so the runner is the\n\t\t// one for that many levels.\n\t\tconst found = Trampoline.run(constants.size, (R) =>\n\t\t\tsearchNode(search)(R)(Algebra.simplify(node), { steps: 0 }, []),\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmnmcc/ability",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Typed, composable authorization policies for Effect programs.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/Authorizer.ts CHANGED
@@ -175,11 +175,11 @@ export interface Authorizer<D extends Domain.Any, E, R> {
175
175
  >
176
176
  /** Gives the decision of the policy. It does not ask the decider. */
177
177
  readonly decide: <const T extends Policy.DeepTarget<D>>(
178
- target: T & Policy.CheckField<D, T>,
178
+ target: Policy.Checked<D, T>,
179
179
  ) => Effect.Effect<Decision.Decision, PolicyUnavailable | E, R>
180
180
  /** Succeeds when the principal may do the act, and fails when it may not. */
181
181
  readonly check: <const T extends DeepTarget<D>>(
182
- target: T & Policy.CheckField<D, T>,
182
+ target: Policy.Checked<D, T>,
183
183
  ) => Effect.Effect<void, Forbidden | Pending | PolicyUnavailable | E, R>
184
184
  /**
185
185
  * Checks the read action first, and fails with the error of `hide` when the
@@ -188,7 +188,7 @@ export interface Authorizer<D extends Domain.Any, E, R> {
188
188
  * exists.
189
189
  */
190
190
  readonly guard: <const T extends Policy.DeepTarget<D>, E2>(
191
- target: T & Policy.CheckField<D, T>,
191
+ target: Policy.Checked<D, T>,
192
192
  options: GuardOptions<D, E2, T["resource"]["_tag"]>,
193
193
  ) => Effect.Effect<void, E2 | Forbidden | Pending | PolicyUnavailable | E, R>
194
194
  /**
@@ -198,7 +198,7 @@ export interface Authorizer<D extends Domain.Any, E, R> {
198
198
  * value, so the list is empty: it is not an error.
199
199
  */
200
200
  readonly filter: <const T extends Policy.DeepKindTarget<D>>(
201
- target: T & Policy.CheckField<D, T>,
201
+ target: Policy.Checked<D, T>,
202
202
  ) => Effect.Effect<
203
203
  Condition.Condition<
204
204
  Policy.ResidualReads<D, T["kind"]>,
@@ -213,7 +213,7 @@ export interface Authorizer<D extends Domain.Any, E, R> {
213
213
  * only when the answer is not `None`.
214
214
  */
215
215
  readonly coverage: <const T extends Policy.DeepKindTarget<D>>(
216
- target: T & Policy.CheckField<D, T>,
216
+ target: Policy.Checked<D, T>,
217
217
  ) => Effect.Effect<
218
218
  Policy.Coverage<Policy.ResidualReads<D, T["kind"]>, Condition.Opaque<E>>,
219
219
  PolicyUnavailable | E,
@@ -249,17 +249,17 @@ export interface ServiceClass<
249
249
  /** The Effect that gives the principal of the current request. */
250
250
  readonly principal: Effect.Effect<Domain.Principal<D>, E, R>
251
251
  readonly decide: <const T extends Policy.DeepTarget<D>>(
252
- target: T & Policy.CheckField<D, T>,
252
+ target: Policy.Checked<D, T>,
253
253
  ) => Effect.Effect<Decision.Decision, PolicyUnavailable | E, R | Self>
254
254
  readonly check: <const T extends DeepTarget<D>>(
255
- target: T & Policy.CheckField<D, T>,
255
+ target: Policy.Checked<D, T>,
256
256
  ) => Effect.Effect<
257
257
  void,
258
258
  Forbidden | Pending | PolicyUnavailable | E,
259
259
  R | Self
260
260
  >
261
261
  readonly guard: <const T extends Policy.DeepTarget<D>, E2>(
262
- target: T & Policy.CheckField<D, T>,
262
+ target: Policy.Checked<D, T>,
263
263
  options: GuardOptions<D, E2, T["resource"]["_tag"]>,
264
264
  ) => Effect.Effect<
265
265
  void,
@@ -267,7 +267,7 @@ export interface ServiceClass<
267
267
  R | Self
268
268
  >
269
269
  readonly filter: <const T extends Policy.DeepKindTarget<D>>(
270
- target: T & Policy.CheckField<D, T>,
270
+ target: Policy.Checked<D, T>,
271
271
  ) => Effect.Effect<
272
272
  Condition.Condition<
273
273
  Policy.ResidualReads<D, T["kind"]>,
@@ -277,7 +277,7 @@ export interface ServiceClass<
277
277
  R | Self
278
278
  >
279
279
  readonly coverage: <const T extends Policy.DeepKindTarget<D>>(
280
- target: T & Policy.CheckField<D, T>,
280
+ target: Policy.Checked<D, T>,
281
281
  ) => Effect.Effect<
282
282
  Policy.Coverage<Policy.ResidualReads<D, T["kind"]>, Condition.Opaque<E>>,
283
283
  PolicyUnavailable | E,
package/src/Condition.ts CHANGED
@@ -691,11 +691,15 @@ type PathEnd<
691
691
  * names them. For a union of types, a part must be in each of them.
692
692
  */
693
693
  type PathCheck<T, F extends string, R extends PathRules> = [T] extends [object]
694
- ? PathWalk<T, F, "", R, []> extends true
695
- ? F
696
- : // The names come from parts of `F`. Inference must not find `F` in
697
- // them, so a call gets its `F` from the path that it gives.
698
- NoInfer<PathWalk<T, F, "", R, []>>
694
+ ? // Each path of a union of paths, as of a field typed `FieldSelector`,
695
+ // is checked by itself.
696
+ F extends unknown
697
+ ? PathWalk<T, F, "", R, []> extends true
698
+ ? F
699
+ : // The names come from parts of `F`. Inference must not find `F`
700
+ // in them, so a call gets its `F` from the path that it gives.
701
+ NoInfer<PathWalk<T, F, "", R, []>>
702
+ : never
699
703
  : never
700
704
 
701
705
  /**
@@ -1445,9 +1449,11 @@ export type PlainCondition = Condition<any>
1445
1449
  * The Schema of a condition with no predicate. It checks the shape only. Use
1446
1450
  * `Policy.schema` to check the field names against a domain too.
1447
1451
  *
1448
- * The decoder takes at most 500 levels of conditions, and at most 1000000
1449
- * objects and array items, with a shared object counted for each place. It
1450
- * reads only the own data fields of the input.
1452
+ * The decoder takes the default limits: at most `conditionNodes` nodes,
1453
+ * 1000000, and `jsonNodes` objects and array items, 10000000, with a shared
1454
+ * object counted for each place. It stops at the first one past a limit. It
1455
+ * takes any number of levels, and it reads only the own data fields of the
1456
+ * input. Use `Policy.schema` for the limits of a domain.
1451
1457
  *
1452
1458
  * @stability stable
1453
1459
  * @category schemas
package/src/Policy.ts CHANGED
@@ -267,14 +267,30 @@ type KindOfTarget<T> = T extends { readonly kind: infer K extends string }
267
267
  * @category utility types
268
268
  * @since 2.1.0
269
269
  */
270
- export type CheckField<D, T> = T extends {
271
- readonly field: infer F extends string
272
- }
273
- ? {
274
- readonly field: Condition.SelectPath<Domain.DataOf<D, KindOfTarget<T>>, F>
275
- }
270
+ export type CheckField<D, T> = T extends { readonly field?: infer F }
271
+ ? [Exclude<F, undefined>] extends [never]
272
+ ? unknown
273
+ : {
274
+ // A field that the target may leave out is checked too, when it is
275
+ // there.
276
+ readonly field?: Condition.SelectPath<
277
+ Domain.DataOf<D, KindOfTarget<T>>,
278
+ Exclude<F, undefined> & string
279
+ >
280
+ }
276
281
  : unknown
277
282
 
283
+ /**
284
+ * A target or a request with its field checked, as {@link CheckField} does.
285
+ * Each member of a union is checked by itself, so a member with no field
286
+ * does not take the place of the check of another member.
287
+ *
288
+ * @stability stable
289
+ * @category utility types
290
+ * @since 2.1.3
291
+ */
292
+ export type Checked<D, T> = T extends unknown ? T & CheckField<D, T> : never
293
+
278
294
  /**
279
295
  * A request: a principal, an action, and a resource value.
280
296
  *
@@ -799,7 +815,7 @@ export const decide: <
799
815
  const Req extends DeepRequest<NoInfer<D>>,
800
816
  >(
801
817
  policy: Policy<D, X>,
802
- request: Req & CheckField<NoInfer<D>, Req>,
818
+ request: Checked<NoInfer<D>, Req>,
803
819
  ) => Effect.Effect<
804
820
  Decision.Decision,
805
821
  Condition.ErrorOf<X>,
@@ -818,7 +834,7 @@ export const decideSync: <
818
834
  const Req extends DeepRequest<NoInfer<D>>,
819
835
  >(
820
836
  policy: Policy<D>,
821
- request: Req & CheckField<NoInfer<D>, Req>,
837
+ request: Checked<NoInfer<D>, Req>,
822
838
  ) => Decision.Decision = Decide.decideSync
823
839
 
824
840
  /**
@@ -837,7 +853,7 @@ export const explain: <
837
853
  const Req extends DeepRequest<NoInfer<D>>,
838
854
  >(
839
855
  policy: Policy<D, X>,
840
- request: Req & CheckField<NoInfer<D>, Req>,
856
+ request: Checked<NoInfer<D>, Req>,
841
857
  ) => Effect.Effect<Explanation, Condition.ErrorOf<X>, Condition.ServicesOf<X>> =
842
858
  Decide.explain as never
843
859
 
@@ -905,8 +921,8 @@ export type ResidualReads<D, K> = Condition.Reads<
905
921
  *
906
922
  * A policy tree may use the condition of a child two times, so a small policy
907
923
  * may give a very large condition. When the condition would have more than
908
- * 10000 nodes, this function throws an error. In an `Authorizer`, the error
909
- * is a defect, so the check fails.
924
+ * the `residualNodes` of the domain, 100000 by default, this function throws
925
+ * an error. In an `Authorizer`, the error is a defect, so the check fails.
910
926
  *
911
927
  * @stability stable
912
928
  * @category evaluation
@@ -918,7 +934,7 @@ export const residual: <
918
934
  const Req extends DeepKindRequest<NoInfer<D>>,
919
935
  >(
920
936
  policy: Policy<D, X>,
921
- request: Req & CheckField<NoInfer<D>, Req>,
937
+ request: Checked<NoInfer<D>, Req>,
922
938
  ) => Condition.Condition<ResidualReads<D, Req["kind"]>, X> =
923
939
  Analysis.residual as never
924
940
 
@@ -980,7 +996,7 @@ export const coverage: <
980
996
  const Req extends DeepKindRequest<NoInfer<D>>,
981
997
  >(
982
998
  policy: Policy<D, X>,
983
- request: Req & CheckField<NoInfer<D>, Req>,
999
+ request: Checked<NoInfer<D>, Req>,
984
1000
  ) => Coverage<ResidualReads<D, Req["kind"]>, X> = Analysis.coverageAny as never
985
1001
 
986
1002
  // Schema
@@ -1031,10 +1047,11 @@ export type EncodedNode =
1031
1047
  * every kind, action, field and principal field against the domain, and the
1032
1048
  * `meta` of each rule against the `meta` Schema of the domain.
1033
1049
  *
1034
- * The decoder takes at most 256 levels of policies and 500 levels of
1035
- * conditions, and at most 1000000 objects and array items, with a shared
1036
- * object counted for each place. It reads only the own data fields of the
1037
- * input.
1050
+ * The decoder takes the limits of the domain: at most `policyNodes` nodes
1051
+ * of the tree, `conditionNodes` nodes of each condition, and `jsonNodes`
1052
+ * objects and array items, with a shared object counted for each place. It
1053
+ * stops at the first one past a limit. It takes any number of levels, and it
1054
+ * reads only the own data fields of the input.
1038
1055
  *
1039
1056
  * @stability stable
1040
1057
  * @category schemas
@@ -655,25 +655,29 @@ export const provide = (node: Node, context: Context.Context<never>): Node =>
655
655
 
656
656
  /** The fields that a condition tests, in the order that they first come. */
657
657
  export const fields = (node: Node): ReadonlyArray<string> => {
658
- const all = Trampoline.run(node.depth, (R) => {
659
- const go = (current: Node): Step<ReadonlyArray<string>> => {
658
+ // The fields go into one native `Set`, in order, as in `dedupe`: a field
659
+ // of a condition that is not bound to a domain may be any string. A walk
660
+ // that joined the lists of the children would copy the list of a deep
661
+ // condition at each level. The set belongs to this walk.
662
+ const seen = new Set<string>()
663
+ Trampoline.run(node.depth, (R) => {
664
+ const go = (current: Node): Step<undefined> => {
660
665
  switch (current._tag) {
661
666
  case "Compare":
662
667
  case "In":
663
668
  case "Exists":
664
- return R.succeed([current.field])
669
+ seen.add(current.field)
670
+ return R.succeed(undefined)
665
671
  case "And":
666
672
  case "Or":
667
- return R.map(eachChild(R, current.conditions, go), Arr.flatten)
673
+ return R.map(eachChild(R, current.conditions, go), () => undefined)
668
674
  case "Not":
669
675
  return R.suspend(() => go(current.condition))
670
676
  default:
671
- return R.succeed([])
677
+ return R.succeed(undefined)
672
678
  }
673
679
  }
674
680
  return go(node)
675
681
  })
676
- // A native `Set`, as in `dedupe`: a field of a condition that is not bound
677
- // to a domain may be any string.
678
- return Array.from(new Set(all))
682
+ return Array.from(seen)
679
683
  }
@@ -139,43 +139,58 @@ class PrincipalKey implements Equal.Equal {
139
139
  * the order that a loop sees them. `Equal` does not see the order of the
140
140
  * fields of an object or of the items of a map, but a loader may read it, as
141
141
  * `Object.values` does. So two keys need the same order too. A scalar is in
142
- * the text with its type, so the items of a set or a map show their order.
142
+ * the text with its type, so the items of a set or a map show their order,
143
+ * and `-0` is not `0`: `Equal` takes them as one, but a loader may not.
144
+ *
145
+ * Each object gets a number at its first visit, and a later visit of it, as
146
+ * of a part that the value shares or of a loop back to a part above, writes
147
+ * `#` and the number. So the walk ends for a value with a loop, and a shared
148
+ * part does not make the text very long. The numbers are in a native `Map`,
149
+ * because the walk must find an object by the object itself. The map belongs
150
+ * to one walk.
143
151
  */
144
- const orderOf: (value: unknown) => string = Trampoline.adaptive<
145
- unknown,
146
- string
147
- >((R, child) => (value, depth) => {
148
- if (typeof value !== "object" || value === null) {
149
- return R.succeed(
150
- typeof value === "string"
151
- ? JSON.stringify(value)
152
- : `${typeof value}:${String(value)}`,
153
- )
154
- }
155
- const items = (list: ReadonlyArray<unknown>) =>
156
- R.forEach(list, (item) => child(item, depth + 1))
157
- if (Array.isArray(value)) {
158
- return R.map(items(value), (texts) => `[${texts.join(",")}]`)
159
- }
160
- if (
161
- value instanceof Map ||
162
- value instanceof Set ||
163
- HashMap.isHashMap(value) ||
164
- HashSet.isHashSet(value) ||
165
- Chunk.isChunk(value)
166
- ) {
152
+ const orderOf = (root: unknown): string => {
153
+ const ids = new Map<object, number>()
154
+ return Trampoline.adaptive<unknown, string>((R, child) => (value, depth) => {
155
+ if (typeof value !== "object" || value === null) {
156
+ return R.succeed(
157
+ typeof value === "string"
158
+ ? JSON.stringify(value)
159
+ : Object.is(value, -0)
160
+ ? "number:-0"
161
+ : `${typeof value}:${String(value)}`,
162
+ )
163
+ }
164
+ const known = ids.get(value)
165
+ if (known !== undefined) {
166
+ return R.succeed(`#${String(known)}`)
167
+ }
168
+ ids.set(value, ids.size)
169
+ const items = (list: ReadonlyArray<unknown>) =>
170
+ R.forEach(list, (item) => child(item, depth + 1))
171
+ if (Array.isArray(value)) {
172
+ return R.map(items(value), (texts) => `[${texts.join(",")}]`)
173
+ }
174
+ if (
175
+ value instanceof Map ||
176
+ value instanceof Set ||
177
+ HashMap.isHashMap(value) ||
178
+ HashSet.isHashSet(value) ||
179
+ Chunk.isChunk(value)
180
+ ) {
181
+ return R.map(
182
+ items(Array.from(value as Iterable<unknown>)),
183
+ (texts) => `<${texts.join(",")}>`,
184
+ )
185
+ }
186
+ const names = Object.keys(value)
167
187
  return R.map(
168
- items(Array.from(value as Iterable<unknown>)),
169
- (texts) => `<${texts.join(",")}>`,
188
+ items(Arr.map(names, (name) => (value as Record<string, unknown>)[name])),
189
+ (texts) =>
190
+ `{${Arr.map(names, (name, index) => `${JSON.stringify(name)}:${texts[index]!}`).join(",")}}`,
170
191
  )
171
- }
172
- const names = Object.keys(value)
173
- return R.map(
174
- items(Arr.map(names, (name) => (value as Record<string, unknown>)[name])),
175
- (texts) =>
176
- `{${Arr.map(names, (name, index) => `${JSON.stringify(name)}:${texts[index]!}`).join(",")}}`,
177
- )
178
- })
192
+ })(root)
193
+ }
179
194
 
180
195
  /**
181
196
  * Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON