factbag 0.0.0-alpha.2 → 0.0.0-alpha.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # factbag
2
2
 
3
+ ## 0.0.0-alpha.3
4
+
5
+ ### Minor Changes
6
+
7
+ - f8f2712: Add `eq` and `neq` rule-body comparisons for numbers, dates, strings, booleans, and entities. Entities compare by type and id rather than instance; dates compare by instant.
8
+ - 900b104: Add min, max, roundTo, floorTo, and ceilTo numeric arithmetic terms for rule bodies. Rounding supports whole-number decimal places (including negative places), with halves rounded away from zero. Decimal exponent shifting avoids multiplication artifacts at decimal rounding boundaries.
9
+ - 4f040e3: Add `daysBetween(later, earlier)` and `yearsBetween(later, earlier)` as number-valued arithmetic terms over dates. They count signed UTC calendar days and whole anniversary years, ignoring time of day; February 29 anniversaries fall on March 1 in common years. Numeric subtraction on variables now composes with other numeric operators.
10
+
11
+ ### Patch Changes
12
+
13
+ - 01cc338: Speed up queries over large rulesets with linear-time recursive predicate grouping,
14
+ per-query rule and evidence indexes, and judgment reuse during candidate discovery.
15
+ Preserve candidate pruning and distinguish same-named predicate declarations when
16
+ tracking active atoms, avoiding incorrect recursion cutoffs.
17
+ - 80d1e75: Share explanation nodes for reused proof judgments so explanation construction no longer grows with the number of paths through a shared proof. Identical sub-explanations are still listed once, and rendered content is unchanged.
18
+ - a074dab: Judge evidence-free, closed-world recursive components together as a least fixed
19
+ point instead of enumerating recursive proof paths. Doubly recursive transitive
20
+ closures and symmetric compositions no longer require exponential proof search.
21
+ Other components retain the existing search semantics. Variable queries recognize
22
+ a non-recursive disjunctive base case within an exclusive definition.
23
+
3
24
  ## 0.0.0-alpha.2
4
25
 
5
26
  ### Major Changes
@@ -13,7 +13,10 @@ var PrimitiveArithmeticProgrammerError = class extends ProgrammerError {
13
13
  /** The arithmetic goal being evaluated when the failure occurred. */
14
14
  goal;
15
15
  constructor(operation, operands, goal) {
16
- super(`${operation} expected numeric operands`);
16
+ const rounding = operation === "roundTo" || operation === "floorTo" || operation === "ceilTo";
17
+ super(
18
+ `${operation} expected ${operation === "daysBetween" || operation === "yearsBetween" ? "date" : "numeric"} operands${rounding ? " and a whole number of decimal places" : ""}`
19
+ );
17
20
  this.name = "PrimitiveArithmeticProgrammerError";
18
21
  this.operation = operation;
19
22
  this.operands = operands;
@@ -82,5 +82,6 @@ export {
82
82
  isAtomForPredicate,
83
83
  atomEquals,
84
84
  fieldEquals,
85
- fieldIdentityToken
85
+ fieldIdentityToken,
86
+ isEntityValue
86
87
  };
@@ -6,9 +6,9 @@
6
6
  */
7
7
  export { and, exists, letIn, not, or } from "./formula.js";
8
8
  export { count, sum } from "./operators/aggregate.js";
9
- export type { AggregateBuilder, ComparableTerm, DateTerm, NumericTerm } from "./types.js";
10
- export { add, div, mul, sub } from "./operators/arithmetic.js";
11
- export { gt, gte, lte } from "./operators/comparison.js";
9
+ export type { AggregateBuilder, ComparableTerm, DateTerm, EqualityTerm, NumericTerm, } from "./types.js";
10
+ export { add, ceilTo, daysBetween, div, floorTo, max, min, mul, roundTo, sub, yearsBetween, } from "./operators/arithmetic.js";
11
+ export { eq, gt, gte, lte, neq } from "./operators/comparison.js";
12
12
  export { closedPredicate, predicate } from "./predicate.js";
13
13
  export { enumeration } from "./enumeration.js";
14
14
  export type { ClosedWorldPredicate, OpenWorldPredicate, Predicate, PredicateBase, PredicateDeclaration, PredicateMetadata, PredicateWorld, } from "../types/predicate.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  assertAtomLike,
3
3
  assertGroundAtom
4
- } from "../chunk-5J3WGMN2.js";
4
+ } from "../chunk-RMQ3YX7V.js";
5
5
  import {
6
6
  Var
7
7
  } from "../chunk-UVOCOH2I.js";
@@ -68,8 +68,53 @@ function div(left, right) {
68
68
  assertSupportedValue(right);
69
69
  return { kind: "div", args: [left, right] };
70
70
  }
71
+ function daysBetween(later, earlier) {
72
+ assertSupportedValue(later);
73
+ assertSupportedValue(earlier);
74
+ return { kind: "daysBetween", args: [later, earlier] };
75
+ }
76
+ function yearsBetween(later, earlier) {
77
+ assertSupportedValue(later);
78
+ assertSupportedValue(earlier);
79
+ return { kind: "yearsBetween", args: [later, earlier] };
80
+ }
81
+ function min(left, right) {
82
+ assertSupportedValue(left);
83
+ assertSupportedValue(right);
84
+ return { kind: "min", args: [left, right] };
85
+ }
86
+ function max(left, right) {
87
+ assertSupportedValue(left);
88
+ assertSupportedValue(right);
89
+ return { kind: "max", args: [left, right] };
90
+ }
91
+ function roundTo(value, places) {
92
+ assertSupportedValue(value);
93
+ assertSupportedValue(places);
94
+ return { kind: "roundTo", args: [value, places] };
95
+ }
96
+ function floorTo(value, places) {
97
+ assertSupportedValue(value);
98
+ assertSupportedValue(places);
99
+ return { kind: "floorTo", args: [value, places] };
100
+ }
101
+ function ceilTo(value, places) {
102
+ assertSupportedValue(value);
103
+ assertSupportedValue(places);
104
+ return { kind: "ceilTo", args: [value, places] };
105
+ }
71
106
 
72
107
  // src/dsl/operators/comparison.ts
108
+ function eq(left, right) {
109
+ assertSupportedValue(left);
110
+ assertSupportedValue(right);
111
+ return { kind: "eq", args: [left, right] };
112
+ }
113
+ function neq(left, right) {
114
+ assertSupportedValue(left);
115
+ assertSupportedValue(right);
116
+ return { kind: "neq", args: [left, right] };
117
+ }
73
118
  function gt(left, right) {
74
119
  assertSupportedValue(left);
75
120
  assertSupportedValue(right);
@@ -138,20 +183,29 @@ export {
138
183
  and,
139
184
  assertAtomLike,
140
185
  assertGroundAtom,
186
+ ceilTo,
141
187
  closedPredicate,
142
188
  count,
189
+ daysBetween,
143
190
  div,
144
191
  enumeration,
192
+ eq,
145
193
  exists,
194
+ floorTo,
146
195
  gt,
147
196
  gte,
148
197
  letIn,
149
198
  lte,
199
+ max,
200
+ min,
150
201
  mul,
202
+ neq,
151
203
  not,
152
204
  or,
153
205
  predicate,
206
+ roundTo,
154
207
  sub,
155
208
  sum,
156
- variable
209
+ variable,
210
+ yearsBetween
157
211
  };
@@ -1,6 +1,8 @@
1
1
  import type { DateTerm, Formula, NumericTerm } from "../types.js";
2
2
  /** Add two numeric terms, producing a numeric rule-body expression. */
3
3
  export declare function add(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
4
+ /** Subtract one numeric term from another. */
5
+ export declare function sub(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
4
6
  /**
5
7
  * Subtract a numeric duration in milliseconds from a date term, producing a
6
8
  * date expression.
@@ -19,9 +21,74 @@ export declare function add(left: NumericTerm, right: NumericTerm): Formula<numb
19
21
  * ```
20
22
  */
21
23
  export declare function sub(left: DateTerm, right: NumericTerm): Formula<Date, readonly [DateTerm, NumericTerm]>;
22
- /** Subtract one numeric term from another. */
23
- export declare function sub(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
24
24
  /** Multiply two numeric terms, producing a numeric rule-body expression. */
25
25
  export declare function mul(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
26
26
  /** Divide one numeric term by another, producing a numeric rule-body expression. */
27
27
  export declare function div(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
28
+ /**
29
+ * Count signed UTC calendar days from `earlier` to `later`, ignoring time of day.
30
+ * Add one to count both the first and last day of a stay.
31
+ */
32
+ export declare function daysBetween(later: DateTerm, earlier: DateTerm): Formula<number, readonly [DateTerm, DateTerm]>;
33
+ /**
34
+ * Count signed whole years from `earlier` to `later` by UTC calendar anniversaries,
35
+ * ignoring time of day. February 29 anniversaries fall on March 1 in common years.
36
+ * A person's age on a day is `yearsBetween(day, birthday)`.
37
+ */
38
+ export declare function yearsBetween(later: DateTerm, earlier: DateTerm): Formula<number, readonly [DateTerm, DateTerm]>;
39
+ /**
40
+ * The lesser of two numeric terms.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // Deduction reduction cannot exceed 80% of the deductions.
45
+ * letIn(R, min(mul(0.03, Excess), mul(0.8, Deductions)), gte(R, 0))
46
+ * ```
47
+ */
48
+ export declare function min(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
49
+ /**
50
+ * The greater of two numeric terms.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * // The remaining allowance cannot be negative.
55
+ * letIn(A, max(sub(2000, Reduction), 0), gte(A, 0))
56
+ * ```
57
+ */
58
+ export declare function max(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
59
+ /**
60
+ * Round to a whole number of decimal places, with halves away from zero.
61
+ * Negative places round to tens, hundreds, and so on. Decimal exponent shifting
62
+ * avoids multiplication artifacts such as `1.005 * 100`, so `roundTo(1.005, 2)`
63
+ * yields `1.01`. Inputs and results still have JavaScript number precision;
64
+ * this does not recover precision already lost in an earlier expression.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // Tax is 15% of taxable income, in whole dollars.
69
+ * letIn(Tax, roundTo(mul(Income, 0.15), 0), gte(Tax, 0))
70
+ * ```
71
+ */
72
+ export declare function roundTo(value: NumericTerm, places: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
73
+ /**
74
+ * Round toward negative infinity at a whole number of decimal places.
75
+ * Negative places and decimal exponent shifting work as in {@link roundTo}.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * // Only complete thousands of eligible expenses count.
80
+ * letIn(Eligible, floorTo(Expenses, -3), gte(Eligible, 0))
81
+ * ```
82
+ */
83
+ export declare function floorTo(value: NumericTerm, places: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
84
+ /**
85
+ * Round toward positive infinity at a whole number of decimal places.
86
+ * Negative places and decimal exponent shifting work as in {@link roundTo}.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * // Two percentage points for each $2,500, or fraction thereof, of the excess.
91
+ * letIn(Points, mul(2, ceilTo(div(Excess, 2500), 0)), lte(Points, 100))
92
+ * ```
93
+ */
94
+ export declare function ceilTo(value: NumericTerm, places: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
@@ -1,4 +1,14 @@
1
- import type { ComparableTerm, Formula } from "../types.js";
1
+ import type { ComparableTerm, EqualityTerm, Formula } from "../types.js";
2
+ /**
3
+ * Require equal values: numbers, strings, and booleans by value, dates by instant,
4
+ * and entities by type and id rather than instance.
5
+ *
6
+ * @remarks
7
+ * Comparisons gate whether a rule fires but contribute no proof children.
8
+ */
9
+ export declare function eq(left: EqualityTerm, right: EqualityTerm): Formula<boolean, readonly [EqualityTerm, EqualityTerm]>;
10
+ /** Require the two terms to differ, in the sense of {@link eq}. */
11
+ export declare function neq(left: EqualityTerm, right: EqualityTerm): Formula<boolean, readonly [EqualityTerm, EqualityTerm]>;
2
12
  /**
3
13
  * Require the left term to be strictly greater than the right.
4
14
  *
@@ -44,6 +44,8 @@ export type NumericTerm = number | Var | Formula<number, readonly unknown[]>;
44
44
  export type DateTerm = Date | Var | Formula<Date, readonly unknown[]>;
45
45
  /** Term accepted by comparison helpers. */
46
46
  export type ComparableTerm = NumericTerm | DateTerm;
47
+ /** Term accepted by equality: a comparable term, a string, a boolean, or an entity. */
48
+ export type EqualityTerm = ComparableTerm | string | boolean | Entity;
47
49
  /** Aggregate builder returned by sum(...) and count(...). */
48
50
  export type AggregateBuilder<Value extends number = number> = Readonly<{
49
51
  where: (<const Body>(body: Body & RuleBodyInput<Body>) => Formula<Value, readonly [Body]>) & (<const Body, const CompleteWhen>(body: Body & RuleBodyInput<Body>, options: Readonly<{
@@ -1,4 +1,4 @@
1
- import type { ArithmeticGoal } from "../types/goal.js";
1
+ import type { ArithmeticGoal, ArithmeticKind } from "../types/goal.js";
2
2
  import type { PredicateDeclaration } from "../types/predicate.js";
3
3
  import type { AnyRule } from "../types/rule.js";
4
4
  import type { Var } from "../types/variable.js";
@@ -9,12 +9,12 @@ export declare class ProgrammerError extends Error {
9
9
  /** Arithmetic primitive with operands the solver cannot legally evaluate. */
10
10
  export declare class PrimitiveArithmeticProgrammerError extends ProgrammerError {
11
11
  /** The arithmetic operation that failed. */
12
- readonly operation: "add" | "sub" | "mul" | "div";
12
+ readonly operation: ArithmeticKind;
13
13
  /** The evaluated left and right operand values. */
14
14
  readonly operands: readonly [unknown, unknown];
15
15
  /** The arithmetic goal being evaluated when the failure occurred. */
16
- readonly goal: ArithmeticGoal<"add" | "sub" | "mul" | "div">;
17
- constructor(operation: "add" | "sub" | "mul" | "div", operands: readonly [unknown, unknown], goal: ArithmeticGoal<"add" | "sub" | "mul" | "div">);
16
+ readonly goal: ArithmeticGoal<ArithmeticKind>;
17
+ constructor(operation: ArithmeticKind, operands: readonly [unknown, unknown], goal: ArithmeticGoal<ArithmeticKind>);
18
18
  }
19
19
  /** Rule body uses variables before any constructive source can bind them. */
20
20
  export declare class UnsafeUnboundVariableProgrammerError extends ProgrammerError {
@@ -1,9 +1,13 @@
1
1
  import type { Judgment } from "../types/judgment.js";
2
2
  import type { Explanation } from "./types.js";
3
3
  /**
4
- * Convert a judgment's proof-support tree into a compact, LLM-readable
4
+ * Convert a judgment's proof-support graph into a compact, LLM-readable
5
5
  * {@link Explanation} of what was concluded and why.
6
6
  *
7
+ * The result is a directed acyclic graph (DAG): reused judgment objects share
8
+ * explanation nodes within this call. Serializing the result as JSON still
9
+ * expands shared nodes at every path to them.
10
+ *
7
11
  * @example
8
12
  * ```ts
9
13
  * const explanation = explainJudgment(query(kb, doctor({ person: alice })));
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isFormula
3
- } from "../chunk-A6ZZJWIO.js";
3
+ } from "../chunk-NDUIQAH4.js";
4
4
  import {
5
5
  AssertionError,
6
6
  isVar
@@ -8,6 +8,21 @@ import {
8
8
 
9
9
  // src/explanation/explain.ts
10
10
  function explainJudgment(judgment) {
11
+ return explainWithContext(judgment, {
12
+ explanations: /* @__PURE__ */ new WeakMap(),
13
+ contentKeys: /* @__PURE__ */ new WeakMap(),
14
+ keyIds: /* @__PURE__ */ new Map()
15
+ });
16
+ }
17
+ function explainWithContext(judgment, context) {
18
+ let explanation = context.explanations.get(judgment);
19
+ if (explanation === void 0) {
20
+ explanation = explainOnce(judgment, context);
21
+ context.explanations.set(judgment, explanation);
22
+ }
23
+ return explanation;
24
+ }
25
+ function explainOnce(judgment, context) {
11
26
  if (isClosedWorldAbsence(judgment)) {
12
27
  const atom = judgment.goal.goal.atom;
13
28
  return {
@@ -21,7 +36,7 @@ function explainJudgment(judgment) {
21
36
  return {
22
37
  claim: claimForJudgment(judgment),
23
38
  because: formatRuleApplication(ruleSupport.rule, ruleSupport.body.bindings),
24
- given: [explainJudgment(ruleSupport.body)]
39
+ given: [explainWithContext(ruleSupport.body, context)]
25
40
  };
26
41
  }
27
42
  const evidence = supports.flatMap(
@@ -41,7 +56,10 @@ function explainJudgment(judgment) {
41
56
  if (subgoals.length > 0) {
42
57
  return {
43
58
  claim: claimForJudgment(judgment),
44
- given: uniqueExplanations(subgoals.map(explainJudgment))
59
+ given: uniqueExplanations(
60
+ subgoals.map((subgoal) => explainWithContext(subgoal, context)),
61
+ context
62
+ )
45
63
  };
46
64
  }
47
65
  const closedWorldAbsence = supports.find((support) => support.source === "closed-world-absence");
@@ -56,11 +74,11 @@ function explainJudgment(judgment) {
56
74
  because: "no supporting evidence or rule was found"
57
75
  };
58
76
  }
59
- function uniqueExplanations(explanations) {
77
+ function uniqueExplanations(explanations, context) {
60
78
  const seen = /* @__PURE__ */ new Set();
61
79
  const unique = [];
62
80
  for (const explanation of explanations) {
63
- const key = JSON.stringify(explanation);
81
+ const key = contentKey(explanation, context);
64
82
  if (!seen.has(key)) {
65
83
  seen.add(key);
66
84
  unique.push(explanation);
@@ -68,6 +86,21 @@ function uniqueExplanations(explanations) {
68
86
  }
69
87
  return unique;
70
88
  }
89
+ function contentKey(explanation, context) {
90
+ let id = context.contentKeys.get(explanation);
91
+ if (id === void 0) {
92
+ const given = "given" in explanation ? explanation.given.map((child) => contentKey(child, context)) : void 0;
93
+ const because = "because" in explanation ? explanation.because : void 0;
94
+ const key = JSON.stringify([explanation.claim, because, given]);
95
+ id = context.keyIds.get(key);
96
+ if (id === void 0) {
97
+ id = context.keyIds.size;
98
+ context.keyIds.set(key, id);
99
+ }
100
+ context.contentKeys.set(explanation, id);
101
+ }
102
+ return id;
103
+ }
71
104
  function supportsForValue(judgment) {
72
105
  if (judgment.value === "true") {
73
106
  return judgment.positive;
@@ -144,6 +177,13 @@ function formatGoal(goal) {
144
177
  case "sub":
145
178
  case "mul":
146
179
  case "div":
180
+ case "daysBetween":
181
+ case "yearsBetween":
182
+ case "min":
183
+ case "max":
184
+ case "roundTo":
185
+ case "floorTo":
186
+ case "ceilTo":
147
187
  return `${goal.bind.name} = ${goal.kind}(${formatValue(goal.left)}, ${formatValue(goal.right)})`;
148
188
  case "eq":
149
189
  case "neq":
@@ -3,10 +3,10 @@ import {
3
3
  PrimitiveArithmeticProgrammerError,
4
4
  ProgrammerError,
5
5
  UnsafeUnboundVariableProgrammerError
6
- } from "../chunk-X3LSDJL4.js";
6
+ } from "../chunk-QOMFY3XB.js";
7
7
  import {
8
8
  isAtomLike
9
- } from "../chunk-5J3WGMN2.js";
9
+ } from "../chunk-RMQ3YX7V.js";
10
10
  import "../chunk-UVOCOH2I.js";
11
11
 
12
12
  // src/knowledge-base/assertions/exclusive-definitions.ts
@@ -6,5 +6,5 @@
6
6
  */
7
7
  export { query, queryAll } from "./query.js";
8
8
  export type { Binding, Bindings, GoalValue } from "../types/common.js";
9
- export type { AddGoal, AndGoal, AnyAtomGoal, ArithmeticGoal, AtomGoal, ComparisonGoal, CompoundGoal, CountGoal, DivGoal, EqGoal, ExistsGoal, Goal, GoalExpression, GroundAtomGoal, GtGoal, GteGoal, LetGoal, LtGoal, LteGoal, MulGoal, NeqGoal, NotGoal, OrGoal, PrimitiveGoal, SubGoal, SumGoal, } from "../types/goal.js";
9
+ export type { AddGoal, AndGoal, AnyAtomGoal, ArithmeticGoal, ArithmeticKind, AtomGoal, CeilToGoal, ComparisonGoal, CompoundGoal, CountGoal, DaysBetweenGoal, DivGoal, EqGoal, ExistsGoal, FloorToGoal, Goal, GoalExpression, GroundAtomGoal, GtGoal, GteGoal, LetGoal, LtGoal, LteGoal, MaxGoal, MinGoal, MulGoal, NeqGoal, NotGoal, OrGoal, PrimitiveGoal, RoundToGoal, SubGoal, SumGoal, YearsBetweenGoal, } from "../types/goal.js";
10
10
  export type { ArithmeticPrimitiveSupport, ClosedWorldAbsenceSupport, ComparisonPrimitiveSupport, CountPrimitiveSupport, ErasedEvidenceSupport, ErasedRuleSupport, EvidenceSupport, Judgment, JudgmentBase, JudgmentSupportFor, MissingBinding, NumericRange, PrimitiveEvaluation, PrimitiveSupport, PrimitiveSupportFor, PrimitiveUncertainty, QueryAllJudgment, QueryJudgment, RuleSupport, SubgoalsSupport, SumPrimitiveSupport, } from "../types/judgment.js";
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  isFormula
3
- } from "../chunk-A6ZZJWIO.js";
3
+ } from "../chunk-NDUIQAH4.js";
4
4
  import {
5
5
  PrimitiveArithmeticProgrammerError,
6
6
  UnconstructiveRecursionProgrammerError,
7
7
  UnsafeUnboundVariableProgrammerError
8
- } from "../chunk-X3LSDJL4.js";
8
+ } from "../chunk-QOMFY3XB.js";
9
9
  import {
10
10
  assertGroundAtom,
11
11
  atomEquals,
@@ -13,8 +13,9 @@ import {
13
13
  fieldIdentityToken,
14
14
  isAtomForPredicate,
15
15
  isAtomLike,
16
+ isEntityValue,
16
17
  isGroundAtom
17
- } from "../chunk-5J3WGMN2.js";
18
+ } from "../chunk-RMQ3YX7V.js";
18
19
  import {
19
20
  AssertionError,
20
21
  Var,
@@ -22,6 +23,39 @@ import {
22
23
  isVar
23
24
  } from "../chunk-UVOCOH2I.js";
24
25
 
26
+ // src/solver/calendar.ts
27
+ var DAY_MS = 24 * 60 * 60 * 1e3;
28
+ function utcDay(date) {
29
+ return Math.floor(date.getTime() / DAY_MS);
30
+ }
31
+ function daysBetween(later, earlier) {
32
+ return utcDay(later) - utcDay(earlier);
33
+ }
34
+ function yearsBetween(later, earlier) {
35
+ if (later.getTime() < earlier.getTime()) {
36
+ return -yearsBetween(earlier, later) || 0;
37
+ }
38
+ const years = later.getUTCFullYear() - earlier.getUTCFullYear();
39
+ const laterDay = later.getUTCMonth() * 100 + later.getUTCDate();
40
+ const earlierDay = earlier.getUTCMonth() * 100 + earlier.getUTCDate();
41
+ return laterDay < earlierDay ? years - 1 : years;
42
+ }
43
+
44
+ // src/types/goal.ts
45
+ var ARITHMETIC_KINDS = [
46
+ "add",
47
+ "sub",
48
+ "mul",
49
+ "div",
50
+ "daysBetween",
51
+ "yearsBetween",
52
+ "min",
53
+ "max",
54
+ "roundTo",
55
+ "floorTo",
56
+ "ceilTo"
57
+ ];
58
+
25
59
  // src/solver/query-validation/utils.ts
26
60
  function ruleEdges(rule) {
27
61
  const headPredicate = rule.head.atom.predicate;
@@ -31,6 +65,7 @@ function ruleEdges(rule) {
31
65
  return edges;
32
66
  }
33
67
  function reachablePredicates(start, edges) {
68
+ const next = adjacency(edges);
34
69
  const reachable = /* @__PURE__ */ new Set([start]);
35
70
  const stack = [start];
36
71
  while (stack.length > 0) {
@@ -38,16 +73,71 @@ function reachablePredicates(start, edges) {
38
73
  if (current === void 0) {
39
74
  continue;
40
75
  }
41
- for (const edge of edges) {
42
- if (edge.from !== current || reachable.has(edge.to)) {
76
+ for (const target of next.get(current) ?? []) {
77
+ if (reachable.has(target)) {
43
78
  continue;
44
79
  }
45
- reachable.add(edge.to);
46
- stack.push(edge.to);
80
+ reachable.add(target);
81
+ stack.push(target);
47
82
  }
48
83
  }
49
84
  return reachable;
50
85
  }
86
+ function adjacency(edges) {
87
+ const next = /* @__PURE__ */ new Map();
88
+ for (const { from, to } of edges) {
89
+ let targets = next.get(from);
90
+ if (targets === void 0) {
91
+ targets = /* @__PURE__ */ new Set();
92
+ next.set(from, targets);
93
+ }
94
+ targets.add(to);
95
+ }
96
+ return next;
97
+ }
98
+ function stronglyConnectedComponents(edges) {
99
+ const next = adjacency(edges);
100
+ const indices = /* @__PURE__ */ new Map();
101
+ const onStack = /* @__PURE__ */ new Set();
102
+ const stack = [];
103
+ const components = /* @__PURE__ */ new Map();
104
+ function visit(predicate) {
105
+ const index = indices.size;
106
+ indices.set(predicate, index);
107
+ let lowLink = index;
108
+ stack.push(predicate);
109
+ onStack.add(predicate);
110
+ for (const target of next.get(predicate) ?? []) {
111
+ const targetIndex = indices.get(target);
112
+ if (targetIndex === void 0) {
113
+ const targetLowLink = visit(target);
114
+ lowLink = Math.min(lowLink, targetLowLink);
115
+ } else if (onStack.has(target)) {
116
+ lowLink = Math.min(lowLink, targetIndex);
117
+ }
118
+ }
119
+ if (lowLink === index) {
120
+ const component = /* @__PURE__ */ new Set();
121
+ for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
122
+ onStack.delete(member);
123
+ component.add(member);
124
+ if (member === predicate) {
125
+ break;
126
+ }
127
+ }
128
+ for (const member of component) {
129
+ components.set(member, component);
130
+ }
131
+ }
132
+ return lowLink;
133
+ }
134
+ for (const predicate of next.keys()) {
135
+ if (!indices.has(predicate)) {
136
+ visit(predicate);
137
+ }
138
+ }
139
+ return components;
140
+ }
51
141
  function isNegativeLiteral(value) {
52
142
  return typeof value === "object" && value !== null && "sign" in value && value.sign === "-" && "atom" in value && isAtomLike(value.atom);
53
143
  }
@@ -58,7 +148,7 @@ function isCompleteWhenOption(value) {
58
148
  return typeof value === "object" && value !== null && "completeWhen" in value;
59
149
  }
60
150
  function isPrimitiveFormula(value) {
61
- return isFormula(value) && ["add", "sub", "mul", "div", "eq", "neq", "lt", "lte", "gt", "gte"].includes(value.kind);
151
+ return isFormula(value) && [...ARITHMETIC_KINDS, "eq", "neq", "lt", "lte", "gt", "gte"].includes(value.kind);
62
152
  }
63
153
  function bindVariables(value, bound) {
64
154
  for (const variable of variablesIn(value)) {
@@ -177,7 +267,7 @@ function collectBodyEdgesWithKind(body, headPredicate, bound, edges, kind) {
177
267
  function assertConstructiveRecursion(context) {
178
268
  const edges = context.rules.flatMap(ruleEdges);
179
269
  const reachable = reachablePredicates(context.atom.predicate, edges);
180
- const components = stronglyConnectedComponents(reachable, edges);
270
+ const components = componentsAmong(reachable, edges);
181
271
  const needsCandidateEnumeration = variablesIn(context.atom).length > 0;
182
272
  for (const component of components) {
183
273
  const internalEdges = edges.filter(
@@ -200,31 +290,22 @@ function assertConstructiveRecursion(context) {
200
290
  }
201
291
  function hasNonRecursiveBaseRule(component, rules) {
202
292
  return rules.some(
203
- (rule) => component.has(rule.head.atom.predicate) && !bodyReferencesPredicates(rule.body, component)
293
+ (rule) => component.has(rule.head.atom.predicate) && hasNonRecursiveAlternative(rule.body, component)
204
294
  );
205
295
  }
206
- function stronglyConnectedComponents(predicates, edges) {
207
- const components = [];
208
- const remaining = new Set(predicates);
209
- for (const predicate of predicates) {
210
- if (!remaining.has(predicate)) {
211
- continue;
212
- }
213
- const forward = reachablePredicates(predicate, edges);
214
- const backward = reachablePredicates(predicate, reverseEdges(edges));
215
- const component = /* @__PURE__ */ new Set();
216
- for (const candidate of predicates) {
217
- if (forward.has(candidate) && backward.has(candidate)) {
218
- component.add(candidate);
219
- remaining.delete(candidate);
220
- }
221
- }
222
- components.push(component);
296
+ function hasNonRecursiveAlternative(body, component) {
297
+ if (isFormula(body, "or")) {
298
+ return body.args.some((branch) => hasNonRecursiveAlternative(branch, component));
223
299
  }
224
- return components;
300
+ return !bodyReferencesPredicates(body, component);
225
301
  }
226
- function reverseEdges(edges) {
227
- return edges.map((edge) => ({ ...edge, from: edge.to, to: edge.from }));
302
+ function componentsAmong(predicates, edges) {
303
+ const components = /* @__PURE__ */ new Set();
304
+ const byPredicate = stronglyConnectedComponents(edges);
305
+ for (const predicate of predicates) {
306
+ components.add(byPredicate.get(predicate) ?? /* @__PURE__ */ new Set([predicate]));
307
+ }
308
+ return [...components];
228
309
  }
229
310
  function bodyReferencesPredicates(body, predicates) {
230
311
  if (isAtomLike(body)) {
@@ -451,18 +532,17 @@ function groundAtom(atom, bindings) {
451
532
  var CandidateSearch = class {
452
533
  groups = /* @__PURE__ */ new Map();
453
534
  tables = /* @__PURE__ */ new Map();
535
+ evaluatingGroups = 0;
454
536
  constructor(rules) {
455
- const edges = rules.flatMap(ruleEdges);
537
+ const components = stronglyConnectedComponents(rules.flatMap(ruleEdges));
456
538
  for (const rule of rules) {
457
539
  const predicate = rule.head.atom.predicate;
458
540
  if (this.groups.has(predicate)) {
459
541
  continue;
460
542
  }
461
543
  const group = { tables: /* @__PURE__ */ new Set(), evaluating: false, changed: false };
462
- for (const dependency of reachablePredicates(predicate, edges)) {
463
- if (reachablePredicates(dependency, edges).has(predicate)) {
464
- this.groups.set(dependency, group);
465
- }
544
+ for (const member of components.get(predicate) ?? [predicate]) {
545
+ this.groups.set(member, group);
466
546
  }
467
547
  }
468
548
  }
@@ -491,6 +571,7 @@ var CandidateSearch = class {
491
571
  group.changed = true;
492
572
  if (!group.evaluating) {
493
573
  group.evaluating = true;
574
+ this.evaluatingGroups += 1;
494
575
  try {
495
576
  do {
496
577
  group.changed = false;
@@ -500,6 +581,7 @@ var CandidateSearch = class {
500
581
  } while (group.changed);
501
582
  } finally {
502
583
  group.evaluating = false;
584
+ this.evaluatingGroups -= 1;
503
585
  }
504
586
  }
505
587
  return [...table.atoms.values()];
@@ -507,8 +589,17 @@ var CandidateSearch = class {
507
589
  sameComponent(left, right) {
508
590
  return left === right || this.groups.has(left) && this.groups.get(left) === this.groups.get(right);
509
591
  }
510
- get isEvaluating() {
511
- return [...this.groups.values()].some((group) => group.evaluating);
592
+ /** A judgment depending on these predicates might observe a partial candidate table. */
593
+ evaluatingAmong(predicates) {
594
+ if (this.evaluatingGroups === 0) {
595
+ return false;
596
+ }
597
+ for (const predicate of predicates) {
598
+ if (this.groups.get(predicate)?.evaluating === true) {
599
+ return true;
600
+ }
601
+ }
602
+ return false;
512
603
  }
513
604
  };
514
605
  function expandTable(table) {
@@ -540,13 +631,88 @@ function patternKey(atom) {
540
631
  );
541
632
  }
542
633
 
634
+ // src/solver/program-index.ts
635
+ var ProgramIndex = class {
636
+ literals = /* @__PURE__ */ new Map();
637
+ rules = /* @__PURE__ */ new Map();
638
+ edges;
639
+ reachable = /* @__PURE__ */ new Map();
640
+ predicateIds = /* @__PURE__ */ new Map();
641
+ constructor(literals, rules) {
642
+ for (const literal of literals) {
643
+ const predicate = literal.atom.predicate;
644
+ const group = this.literals.get(predicate);
645
+ if (group === void 0) {
646
+ this.literals.set(predicate, [literal]);
647
+ } else {
648
+ group.push(literal);
649
+ }
650
+ }
651
+ for (const rule of rules) {
652
+ const predicate = rule.head.atom.predicate;
653
+ const group = this.rules.get(predicate);
654
+ if (group === void 0) {
655
+ this.rules.set(predicate, [rule]);
656
+ } else {
657
+ group.push(rule);
658
+ }
659
+ }
660
+ this.edges = rules.flatMap(ruleEdges);
661
+ }
662
+ literalsFor(predicate) {
663
+ return this.literals.get(predicate) ?? [];
664
+ }
665
+ rulesFor(predicate) {
666
+ return this.rules.get(predicate) ?? [];
667
+ }
668
+ /** Atom keys must distinguish declarations even when they share a display name. */
669
+ predicateId(predicate) {
670
+ let id = this.predicateIds.get(predicate);
671
+ if (id === void 0) {
672
+ id = this.predicateIds.size;
673
+ this.predicateIds.set(predicate, id);
674
+ }
675
+ return id;
676
+ }
677
+ reaches(predicate) {
678
+ let reachable = this.reachable.get(predicate);
679
+ if (reachable === void 0) {
680
+ reachable = reachablePredicates(predicate, this.edges);
681
+ this.reachable.set(predicate, reachable);
682
+ }
683
+ return reachable;
684
+ }
685
+ };
686
+
543
687
  // src/solver/solve-all.ts
544
688
  var UNBOUND = /* @__PURE__ */ Symbol("factbag.unbound");
689
+ function tabledComponents(literals, rules) {
690
+ const edges = rules.flatMap(ruleEdges);
691
+ const components = stronglyConnectedComponents(edges);
692
+ const selfRecursive = new Set(
693
+ edges.filter((edge) => edge.from === edge.to).map((edge) => edge.from)
694
+ );
695
+ const stated = new Set(literals.map((literal) => literal.atom.predicate));
696
+ const tabled = /* @__PURE__ */ new Map();
697
+ for (const component of new Set(components.values())) {
698
+ const members = [...component];
699
+ if (members.some((member) => member.world !== "closed" || stated.has(member)) || !(component.size > 1 || members.some((member) => selfRecursive.has(member)))) {
700
+ continue;
701
+ }
702
+ for (const member of members) {
703
+ tabled.set(member, component);
704
+ }
705
+ }
706
+ return tabled;
707
+ }
545
708
  function solveQueryAll(literals, rules, atom) {
546
709
  validateQuery(literals, rules, atom);
547
- const result = solveQueryAllInternal(literals, rules, atom, /* @__PURE__ */ new Set(), {
710
+ const result = solveQueryAllInternal(literals, rules, atom, /* @__PURE__ */ new Map(), {
548
711
  candidates: new CandidateSearch(rules),
549
- judgments: /* @__PURE__ */ new Map()
712
+ judgments: /* @__PURE__ */ new Map(),
713
+ tabled: tabledComponents(literals, rules),
714
+ tables: /* @__PURE__ */ new Map(),
715
+ index: new ProgramIndex(literals, rules)
550
716
  });
551
717
  const stack = [result];
552
718
  const visited = /* @__PURE__ */ new Set();
@@ -584,7 +750,11 @@ function solveQueryAllInternal(literals, rules, atom, active, search) {
584
750
  );
585
751
  }
586
752
  function solveGroundAtom(literals, rules, atom, active, search) {
587
- if (search.candidates.isEvaluating) {
753
+ if (search.tabled.has(atom.predicate)) {
754
+ return solveTabledAtom(literals, rules, atom, active, search);
755
+ }
756
+ const dependencies = search.index.reaches(atom.predicate);
757
+ if (search.candidates.evaluatingAmong(dependencies)) {
588
758
  return evaluateGroundAtom(literals, rules, atom, active, search);
589
759
  }
590
760
  let judgments = search.judgments.get(atom.predicate);
@@ -592,7 +762,8 @@ function solveGroundAtom(literals, rules, atom, active, search) {
592
762
  judgments = /* @__PURE__ */ new Map();
593
763
  search.judgments.set(atom.predicate, judgments);
594
764
  }
595
- const key = JSON.stringify([groundAtomKey(atom), [...active].sort()]);
765
+ const relevantActive = [...active].filter(([, predicate]) => dependencies.has(predicate)).map(([key2]) => key2);
766
+ const key = JSON.stringify([groundAtomKey(atom, search), relevantActive.sort()]);
596
767
  const cached = judgments.get(key);
597
768
  if (cached !== void 0 && judgmentMatchesAtom(cached, atom)) {
598
769
  return cached;
@@ -604,12 +775,107 @@ function solveGroundAtom(literals, rules, atom, active, search) {
604
775
  function judgmentMatchesAtom(judgment, atom) {
605
776
  return atomEquals(judgment.goal.atom, atom);
606
777
  }
778
+ function solveTabledAtom(literals, rules, atom, active, search) {
779
+ const table = tableFor(search, atom.predicate);
780
+ const key = groundAtomKey(atom, search);
781
+ const enter = () => {
782
+ const absent = {
783
+ value: "false",
784
+ goal: { kind: "atom", atom },
785
+ bindings: [],
786
+ negative: [{ source: "closed-world-absence" }]
787
+ };
788
+ table.judgments.set(key, absent);
789
+ table.atoms.set(key, atom);
790
+ table.queue.push(key);
791
+ table.queued.add(key);
792
+ return absent;
793
+ };
794
+ if (table.running) {
795
+ const judgment = table.judgments.get(key) ?? enter();
796
+ if (table.evaluating !== void 0) {
797
+ let dependents = table.dependents.get(key);
798
+ if (dependents === void 0) {
799
+ dependents = /* @__PURE__ */ new Set();
800
+ table.dependents.set(key, dependents);
801
+ }
802
+ dependents.add(table.evaluating);
803
+ }
804
+ assert(judgmentMatchesAtom(judgment, atom), "Tabled judgment must match its atom");
805
+ return judgment;
806
+ }
807
+ const settled = table.judgments.get(key);
808
+ if (settled !== void 0) {
809
+ assert(judgmentMatchesAtom(settled, atom), "Tabled judgment must match its atom");
810
+ return settled;
811
+ }
812
+ enter();
813
+ table.running = true;
814
+ try {
815
+ for (let offset = 0; offset < table.queue.length; offset += 1) {
816
+ const next = table.queue[offset];
817
+ assert(next !== void 0, "Queued atom must have a key");
818
+ table.queued.delete(next);
819
+ const current = table.judgments.get(next);
820
+ const subject = table.atoms.get(next);
821
+ assert(current !== void 0 && subject !== void 0, "Queued atom must be in the table");
822
+ table.evaluating = next;
823
+ const judged = evaluateGroundAtom(literals, rules, subject, active, search);
824
+ table.evaluating = void 0;
825
+ table.judgments.set(next, judged);
826
+ if (judged.value === current.value) {
827
+ continue;
828
+ }
829
+ assert(
830
+ maxGoalValue(current.value, judged.value) === judged.value,
831
+ "Tabled recursion must be monotone"
832
+ );
833
+ const changes = (table.changes.get(next) ?? 0) + 1;
834
+ assert(changes <= 3, "Tabled recursion did not converge");
835
+ table.changes.set(next, changes);
836
+ for (const dependent of table.dependents.get(next) ?? []) {
837
+ if (!table.queued.has(dependent)) {
838
+ table.queue.push(dependent);
839
+ table.queued.add(dependent);
840
+ }
841
+ }
842
+ }
843
+ table.queue.length = 0;
844
+ } finally {
845
+ table.running = false;
846
+ table.evaluating = void 0;
847
+ }
848
+ const result = table.judgments.get(key);
849
+ assert(result !== void 0 && judgmentMatchesAtom(result, atom), "Run must judge its seed");
850
+ return result;
851
+ }
852
+ function tableFor(search, predicate) {
853
+ let table = search.tables.get(predicate);
854
+ if (table === void 0) {
855
+ const component = search.tabled.get(predicate);
856
+ assert(component !== void 0, "Only a tabled predicate has a table");
857
+ table = {
858
+ judgments: /* @__PURE__ */ new Map(),
859
+ atoms: /* @__PURE__ */ new Map(),
860
+ dependents: /* @__PURE__ */ new Map(),
861
+ changes: /* @__PURE__ */ new Map(),
862
+ queue: [],
863
+ queued: /* @__PURE__ */ new Set(),
864
+ running: false,
865
+ evaluating: void 0
866
+ };
867
+ for (const member of component) {
868
+ search.tables.set(member, table);
869
+ }
870
+ }
871
+ return table;
872
+ }
607
873
  function evaluateGroundAtom(literals, rules, atom, active, search) {
608
874
  const goal = { kind: "atom", atom };
609
875
  const bindings = [];
610
- const atomKey = groundAtomKey(atom);
611
- const positiveLiterals = matchingLiterals(literals, atom, "+");
612
- const negativeLiterals = matchingLiterals(literals, atom, "-");
876
+ const atomKey = groundAtomKey(atom, search);
877
+ const positiveLiterals = matchingLiterals(search.index.literalsFor(atom.predicate), atom, "+");
878
+ const negativeLiterals = matchingLiterals(search.index.literalsFor(atom.predicate), atom, "-");
613
879
  const values = [];
614
880
  const positive = [];
615
881
  const negative = [];
@@ -622,9 +888,9 @@ function evaluateGroundAtom(literals, rules, atom, active, search) {
622
888
  values.push("false");
623
889
  }
624
890
  if (!active.has(atomKey)) {
625
- const nextActive = new Set(active);
626
- nextActive.add(atomKey);
627
- for (const rule of rules) {
891
+ const nextActive = new Map(active);
892
+ nextActive.set(atomKey, atom.predicate);
893
+ for (const rule of search.index.rulesFor(atom.predicate)) {
628
894
  const match = matchRuleForAtom(rule, atom);
629
895
  if (match === void 0) {
630
896
  continue;
@@ -760,7 +1026,10 @@ function evaluateRuleBodyMany(literals, rules, body, bindings, active, context,
760
1026
  ];
761
1027
  }
762
1028
  const atom = groundAtom(bodyAtom, bindings);
763
- assert(!active.has(groundAtomKey(atom)), "Validated query must not recurse through negation");
1029
+ assert(
1030
+ !active.has(groundAtomKey(atom, context.search)),
1031
+ "Validated query must not recurse through negation"
1032
+ );
764
1033
  const child = solveGroundAtom(literals, rules, atom, active, context.search);
765
1034
  const goal = { kind: "not", goal: { kind: "atom", atom } };
766
1035
  const value = atom.predicate.world === "closed" ? negateAsFailureGoalValue(child.value) : negateGoalValue(child.value);
@@ -1070,8 +1339,15 @@ function evaluateAtomBody(literals, rules, atom, bindings, active, context, path
1070
1339
  function evaluateComparisonBody(kind, args, bindings, context, pathCanHaveUnavailableBindings) {
1071
1340
  const left = evaluateExpression(args[0], bindings, context);
1072
1341
  const right = evaluateExpression(args[1], left.bindings, context);
1073
- assert(isScalarGoalExpression(left.expression), "Left comparison operand must be scalar");
1074
- assert(isScalarGoalExpression(right.expression), "Right comparison operand must be scalar");
1342
+ const allowsEntities = kind === "eq" || kind === "neq";
1343
+ assert(
1344
+ isScalarGoalExpression(left.expression) || allowsEntities && isEntityValue(left.expression),
1345
+ "Left comparison operand must be scalar, or entity for equality"
1346
+ );
1347
+ assert(
1348
+ isScalarGoalExpression(right.expression) || allowsEntities && isEntityValue(right.expression),
1349
+ "Right comparison operand must be scalar, or entity for equality"
1350
+ );
1075
1351
  const nextBindings = right.bindings;
1076
1352
  const judgmentBindings = bindingsArray(nextBindings);
1077
1353
  const goal = {
@@ -1645,7 +1921,7 @@ function primitiveRangeForExact(value) {
1645
1921
  return { range: { lower: value, upper: value } };
1646
1922
  }
1647
1923
  function isArithmeticFormula(value) {
1648
- return isFormula(value) && ["add", "sub", "mul", "div"].includes(value.kind) && value.args.length === 2;
1924
+ return isFormula(value) && ARITHMETIC_KINDS.some((kind) => kind === value.kind) && value.args.length === 2;
1649
1925
  }
1650
1926
  function isComparisonFormula(value) {
1651
1927
  return isFormula(value) && ["eq", "neq", "lt", "lte", "gt", "gte"].includes(value.kind) && value.args.length === 2;
@@ -1663,18 +1939,17 @@ function calculateValue(kind, left, right, goal) {
1663
1939
  if (left === UNBOUND || right === UNBOUND) {
1664
1940
  return UNBOUND;
1665
1941
  }
1666
- if (left instanceof Date && typeof right === "number") {
1667
- switch (kind) {
1668
- case "sub":
1669
- return new Date(left.getTime() - right);
1670
- case "add":
1671
- case "mul":
1672
- case "div":
1673
- break;
1674
- default:
1675
- return assertNever(kind, "Unhandled arithmetic kind");
1942
+ if (left instanceof Date && right instanceof Date) {
1943
+ if (kind === "daysBetween") {
1944
+ return daysBetween(left, right);
1945
+ }
1946
+ if (kind === "yearsBetween") {
1947
+ return yearsBetween(left, right);
1676
1948
  }
1677
1949
  }
1950
+ if (left instanceof Date && typeof right === "number" && kind === "sub") {
1951
+ return new Date(left.getTime() - right);
1952
+ }
1678
1953
  if (typeof left === "number" && typeof right === "number") {
1679
1954
  switch (kind) {
1680
1955
  case "add":
@@ -1685,12 +1960,67 @@ function calculateValue(kind, left, right, goal) {
1685
1960
  return left * right;
1686
1961
  case "div":
1687
1962
  return left / right;
1963
+ case "daysBetween":
1964
+ case "yearsBetween":
1965
+ break;
1966
+ case "min":
1967
+ return Math.min(left, right);
1968
+ case "max":
1969
+ return Math.max(left, right);
1970
+ case "roundTo":
1971
+ case "floorTo":
1972
+ case "ceilTo":
1973
+ if (!Number.isInteger(right)) {
1974
+ throw new PrimitiveArithmeticProgrammerError(kind, [left, right], goal);
1975
+ }
1976
+ return roundAtPlaces(left, right, kind);
1688
1977
  default:
1689
1978
  return assertNever(kind, "Unhandled arithmetic kind");
1690
1979
  }
1691
1980
  }
1692
1981
  throw new PrimitiveArithmeticProgrammerError(kind, [left, right], goal);
1693
1982
  }
1983
+ function shiftDecimal(value, places) {
1984
+ if (value === 0 || !Number.isFinite(value)) {
1985
+ return value;
1986
+ }
1987
+ const [coefficient, exponent = "0"] = String(value).split("e");
1988
+ const shiftedExponent = Number(exponent) + places;
1989
+ if (shiftedExponent > 308) {
1990
+ return Math.sign(value) * Infinity;
1991
+ }
1992
+ if (shiftedExponent < -324) {
1993
+ return Math.sign(value) * 0;
1994
+ }
1995
+ return Number(`${coefficient}e${shiftedExponent}`);
1996
+ }
1997
+ function roundAtPlaces(value, places, kind) {
1998
+ if (value === 0 || !Number.isFinite(value)) {
1999
+ return value;
2000
+ }
2001
+ let shifted = shiftDecimal(value, places);
2002
+ if (!Number.isFinite(shifted)) {
2003
+ return value;
2004
+ }
2005
+ if (shifted === 0) {
2006
+ shifted = Math.sign(value) * Number.MIN_VALUE;
2007
+ }
2008
+ let rounded;
2009
+ switch (kind) {
2010
+ case "roundTo":
2011
+ rounded = Math.sign(shifted) * Math.round(Math.abs(shifted));
2012
+ break;
2013
+ case "floorTo":
2014
+ rounded = Math.floor(shifted);
2015
+ break;
2016
+ case "ceilTo":
2017
+ rounded = Math.ceil(shifted);
2018
+ break;
2019
+ default:
2020
+ return assertNever(kind, "Unhandled rounding kind");
2021
+ }
2022
+ return shiftDecimal(rounded, -places);
2023
+ }
1694
2024
  function compareValues(kind, left, right) {
1695
2025
  assert(left !== UNBOUND && right !== UNBOUND, "Comparison operands must be bound");
1696
2026
  if (isNumericRangeValue(left) && typeof right === "number") {
@@ -1703,9 +2033,9 @@ function compareValues(kind, left, right) {
1703
2033
  const rightComparable = right instanceof Date ? right.getTime() : right;
1704
2034
  switch (kind) {
1705
2035
  case "eq":
1706
- return leftComparable === rightComparable ? "true" : "false";
2036
+ return sameValue(leftComparable, rightComparable) ? "true" : "false";
1707
2037
  case "neq":
1708
- return leftComparable === rightComparable ? "false" : "true";
2038
+ return sameValue(leftComparable, rightComparable) ? "false" : "true";
1709
2039
  case "lt":
1710
2040
  case "lte":
1711
2041
  case "gt":
@@ -1715,6 +2045,15 @@ function compareValues(kind, left, right) {
1715
2045
  return assertNever(kind, "Unhandled comparison kind");
1716
2046
  }
1717
2047
  }
2048
+ function sameValue(left, right) {
2049
+ if (left === right) {
2050
+ return true;
2051
+ }
2052
+ if (isEntityValue(left) && isEntityValue(right)) {
2053
+ return JSON.stringify(fieldIdentityToken(left)) === JSON.stringify(fieldIdentityToken(right));
2054
+ }
2055
+ return false;
2056
+ }
1718
2057
  function compareOrderedValues(kind, left, right) {
1719
2058
  assert(typeof left === "number" && typeof right === "number", "Values must be comparable");
1720
2059
  switch (kind) {
@@ -1918,14 +2257,14 @@ function uniqueBindings(bindings) {
1918
2257
  function canGroundAtom(atom, bindings) {
1919
2258
  return Object.values(atom.fields).every((value) => !isVar(value) || bindings.has(value));
1920
2259
  }
1921
- function groundAtomKey(atom) {
2260
+ function groundAtomKey(atom, search) {
1922
2261
  const fields = atom.predicate.paramNames.map((name) => fieldIdentityToken(atom.fields[name]));
1923
- return `${atom.predicate.name}:${JSON.stringify(fields)}`;
2262
+ return `${search.index.predicateId(atom.predicate)}:${JSON.stringify(fields)}`;
1924
2263
  }
1925
2264
  function candidateGroundAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings = false) {
1926
2265
  return search.candidates.candidates(
1927
2266
  atom,
1928
- matchingGroundAtoms(literals, atom),
2267
+ matchingGroundAtoms(search.index.literalsFor(atom.predicate), atom),
1929
2268
  () => deriveCandidateAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings)
1930
2269
  ).filter(
1931
2270
  (candidate) => isAtomForPredicate(candidate, atom.predicate)
@@ -1933,7 +2272,7 @@ function candidateGroundAtoms(literals, rules, atom, active, search, pathCanHave
1933
2272
  }
1934
2273
  function deriveCandidateAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings = false) {
1935
2274
  const atoms = [];
1936
- for (const rule of rules) {
2275
+ for (const rule of search.index.rulesFor(atom.predicate)) {
1937
2276
  const ruleHead = atomForPredicate(rule.head.atom, atom.predicate);
1938
2277
  if (ruleHead === void 0) {
1939
2278
  continue;
@@ -0,0 +1,4 @@
1
+ /** Signed UTC calendar days from `earlier` to `later`, ignoring time of day. */
2
+ export declare function daysBetween(later: Date, earlier: Date): number;
3
+ /** Whole UTC anniversary years, with February 29 anniversaries on March 1 in common years. */
4
+ export declare function yearsBetween(later: Date, earlier: Date): number;
@@ -5,8 +5,10 @@ import type { AnyRule } from "../types/rule.js";
5
5
  export declare class CandidateSearch {
6
6
  private readonly groups;
7
7
  private readonly tables;
8
+ private evaluatingGroups;
8
9
  constructor(rules: readonly AnyRule[]);
9
10
  candidates(atom: Atom, seeds: readonly GroundAtom[], derive: () => readonly GroundAtom[]): GroundAtom[];
10
11
  sameComponent(left: PredicateDeclaration, right: PredicateDeclaration): boolean;
11
- get isEvaluating(): boolean;
12
+ /** A judgment depending on these predicates might observe a partial candidate table. */
13
+ evaluatingAmong(predicates: Iterable<PredicateDeclaration>): boolean;
12
14
  }
@@ -0,0 +1,17 @@
1
+ import type { AnyLiteral } from "../types/literal.js";
2
+ import type { PredicateDeclaration } from "../types/predicate.js";
3
+ import type { AnyRule } from "../types/rule.js";
4
+ /** Query-local lookups; predicate identity, not its name, determines dependencies. */
5
+ export declare class ProgramIndex {
6
+ private readonly literals;
7
+ private readonly rules;
8
+ private readonly edges;
9
+ private readonly reachable;
10
+ private readonly predicateIds;
11
+ constructor(literals: readonly AnyLiteral[], rules: readonly AnyRule[]);
12
+ literalsFor(predicate: PredicateDeclaration): readonly AnyLiteral[];
13
+ rulesFor(predicate: PredicateDeclaration): readonly AnyRule[];
14
+ /** Atom keys must distinguish declarations even when they share a display name. */
15
+ predicateId(predicate: PredicateDeclaration): number;
16
+ reaches(predicate: PredicateDeclaration): ReadonlySet<PredicateDeclaration>;
17
+ }
@@ -10,6 +10,8 @@ export type Edge = Readonly<{
10
10
  }>;
11
11
  export declare function ruleEdges(rule: AnyRule): Edge[];
12
12
  export declare function reachablePredicates(start: PredicateDeclaration, edges: readonly Edge[]): Set<PredicateDeclaration>;
13
+ /** Linear-time Tarjan components, including predicates that occur only as targets. */
14
+ export declare function stronglyConnectedComponents(edges: readonly Edge[]): ReadonlyMap<PredicateDeclaration, ReadonlySet<PredicateDeclaration>>;
13
15
  export declare function isNegativeLiteral(value: unknown): value is AnyLiteral & {
14
16
  sign: "-";
15
17
  };
@@ -1,4 +1,4 @@
1
- import type { InferRuntime, PredicateSchema } from "./schema.js";
1
+ import type { Entity, InferRuntime, PredicateSchema } from "./schema.js";
2
2
  import type { Var } from "./variable.js";
3
3
  import type { PredicateDeclaration } from "./predicate.js";
4
4
  /** An atom whose exact schema has been intentionally erased. */
@@ -52,4 +52,9 @@ export declare function atomEquals(left: AnyAtom, right: AnyAtom): boolean;
52
52
  export declare function fieldEquals(left: unknown, right: unknown): boolean;
53
53
  /** Canonical JSON-safe token for a field value, for use in string identity keys. */
54
54
  export declare function fieldIdentityToken(value: unknown): unknown;
55
+ export declare function isEntityValue(value: unknown): value is Entity & {
56
+ constructor: {
57
+ name: string;
58
+ };
59
+ };
55
60
  export {};
@@ -55,8 +55,26 @@ export type SubGoal = ArithmeticGoal<"sub">;
55
55
  export type MulGoal = ArithmeticGoal<"mul">;
56
56
  /** Arithmetic goal binding the quotient of two expressions. */
57
57
  export type DivGoal = ArithmeticGoal<"div">;
58
+ /** Arithmetic goal binding the UTC calendar days from the right date to the left. */
59
+ export type DaysBetweenGoal = ArithmeticGoal<"daysBetween">;
60
+ /** Arithmetic goal binding whole anniversary years from the right date to the left. */
61
+ export type YearsBetweenGoal = ArithmeticGoal<"yearsBetween">;
62
+ /** Arithmetic goal binding the lesser of two numeric expressions. */
63
+ export type MinGoal = ArithmeticGoal<"min">;
64
+ /** Arithmetic goal binding the greater of two numeric expressions. */
65
+ export type MaxGoal = ArithmeticGoal<"max">;
66
+ /** Arithmetic goal rounding halves away from zero at the right operand's decimal places. */
67
+ export type RoundToGoal = ArithmeticGoal<"roundTo">;
68
+ /** Arithmetic goal rounding toward negative infinity at the right operand's decimal places. */
69
+ export type FloorToGoal = ArithmeticGoal<"floorTo">;
70
+ /** Arithmetic goal rounding toward positive infinity at the right operand's decimal places. */
71
+ export type CeilToGoal = ArithmeticGoal<"ceilTo">;
72
+ /** The primitive arithmetic operations. */
73
+ export type ArithmeticKind = "add" | "sub" | "mul" | "div" | "daysBetween" | "yearsBetween" | "min" | "max" | "roundTo" | "floorTo" | "ceilTo";
74
+ /** Shared runtime classification for arithmetic formulas. */
75
+ export declare const ARITHMETIC_KINDS: readonly ArithmeticKind[];
58
76
  /** Bind the result of a primitive arithmetic operation. */
59
- export type ArithmeticGoal<Kind extends "add" | "sub" | "mul" | "div"> = Readonly<{
77
+ export type ArithmeticGoal<Kind extends ArithmeticKind> = Readonly<{
60
78
  readonly kind: Kind;
61
79
  readonly bind: Var;
62
80
  readonly left: GoalExpression<Scalar>;
@@ -77,8 +95,8 @@ export type GteGoal = ComparisonGoal<"gte">;
77
95
  /** Compare two value-producing expressions. */
78
96
  export type ComparisonGoal<Kind extends "eq" | "neq" | "lt" | "lte" | "gt" | "gte"> = Readonly<{
79
97
  readonly kind: Kind;
80
- readonly left: GoalExpression<Scalar>;
81
- readonly right: GoalExpression<Scalar>;
98
+ readonly left: GoalExpression;
99
+ readonly right: GoalExpression;
82
100
  }>;
83
101
  /** Sum a numeric expression over rows matched by a body goal. */
84
102
  export type SumGoal = Readonly<{
@@ -96,8 +114,8 @@ export type CountGoal = Readonly<{
96
114
  readonly completeWhen?: Goal;
97
115
  }>;
98
116
  /** A normalized rule-body target that the solver/proof system can evaluate. */
99
- export type Goal = AnyAtomGoal | NotGoal | AndGoal | OrGoal | ExistsGoal | LetGoal | AddGoal | SubGoal | MulGoal | DivGoal | EqGoal | NeqGoal | LtGoal | LteGoal | GtGoal | GteGoal | SumGoal | CountGoal;
117
+ export type Goal = AnyAtomGoal | NotGoal | AndGoal | OrGoal | ExistsGoal | LetGoal | AddGoal | SubGoal | MulGoal | DivGoal | DaysBetweenGoal | YearsBetweenGoal | MinGoal | MaxGoal | RoundToGoal | FloorToGoal | CeilToGoal | EqGoal | NeqGoal | LtGoal | LteGoal | GtGoal | GteGoal | SumGoal | CountGoal;
100
118
  /** A goal evaluated by recursively evaluating one or more child goals. */
101
119
  export type CompoundGoal = NotGoal | AndGoal | OrGoal | ExistsGoal | LetGoal;
102
120
  /** A goal evaluated directly by a primitive operator. */
103
- export type PrimitiveGoal = AddGoal | SubGoal | MulGoal | DivGoal | EqGoal | NeqGoal | LtGoal | LteGoal | GtGoal | GteGoal | SumGoal | CountGoal;
121
+ export type PrimitiveGoal = AddGoal | SubGoal | MulGoal | DivGoal | DaysBetweenGoal | YearsBetweenGoal | MinGoal | MaxGoal | RoundToGoal | FloorToGoal | CeilToGoal | EqGoal | NeqGoal | LtGoal | LteGoal | GtGoal | GteGoal | SumGoal | CountGoal;
@@ -1,4 +1,4 @@
1
- import type { AnyAtomGoal, AtomGoal, CompoundGoal, Goal, GroundAtomGoal, PrimitiveGoal } from "./goal.js";
1
+ import type { AnyAtomGoal, ArithmeticKind, AtomGoal, CompoundGoal, Goal, GroundAtomGoal, PrimitiveGoal } from "./goal.js";
2
2
  import type { AnyLiteral, Literal } from "./literal.js";
3
3
  import type { AnyRule, Rule } from "./rule.js";
4
4
  import type { Bindings } from "./common.js";
@@ -93,7 +93,7 @@ export type CountPrimitiveSupport = Readonly<{
93
93
  readonly results: Bindings;
94
94
  }>;
95
95
  /** Proof support for a primitive goal, narrowed to the support shape for the goal's kind. */
96
- export type PrimitiveSupportFor<GoalType extends PrimitiveGoal> = PrimitiveSupport | (GoalType["kind"] extends "add" | "sub" | "mul" | "div" ? ArithmeticPrimitiveSupport : GoalType["kind"] extends "eq" | "neq" | "lt" | "lte" | "gt" | "gte" ? ComparisonPrimitiveSupport : GoalType["kind"] extends "sum" ? SumPrimitiveSupport : GoalType["kind"] extends "count" ? CountPrimitiveSupport : never);
96
+ export type PrimitiveSupportFor<GoalType extends PrimitiveGoal> = PrimitiveSupport | (GoalType["kind"] extends ArithmeticKind ? ArithmeticPrimitiveSupport : GoalType["kind"] extends "eq" | "neq" | "lt" | "lte" | "gt" | "gte" ? ComparisonPrimitiveSupport : GoalType["kind"] extends "sum" ? SumPrimitiveSupport : GoalType["kind"] extends "count" ? CountPrimitiveSupport : never);
97
97
  /** Sources of support allowed for a specific goal shape. */
98
98
  export type JudgmentSupportFor<GoalType extends Goal> = GoalType extends AtomGoal<infer Schema> ? EvidenceSupport<Schema> | ClosedWorldAbsenceSupport | RuleSupport<Schema> : GoalType extends AnyAtomGoal ? ErasedEvidenceSupport | ClosedWorldAbsenceSupport | ErasedRuleSupport : GoalType extends PrimitiveGoal ? PrimitiveSupportFor<GoalType> : GoalType extends CompoundGoal ? SubgoalsSupport : never;
99
99
  /** The evaluated goal and in-scope variable bindings shared by every judgment. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factbag",
3
- "version": "0.0.0-alpha.2",
3
+ "version": "0.0.0-alpha.3",
4
4
  "keywords": [
5
5
  "logic",
6
6
  "rules-engine",