factbag 0.0.0-alpha.2 → 0.0.0-alpha.4

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 (54) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -4
  3. package/dist/core/{chunk-5J3WGMN2.js → chunk-N2RRSOEN.js} +2 -1
  4. package/dist/core/{chunk-X3LSDJL4.js → chunk-QOMFY3XB.js} +4 -1
  5. package/dist/core/dsl/enumeration.d.ts +5 -1
  6. package/dist/core/dsl/index.d.ts +4 -4
  7. package/dist/core/dsl/index.js +76 -7
  8. package/dist/core/dsl/operators/arithmetic.d.ts +69 -2
  9. package/dist/core/dsl/operators/comparison.d.ts +11 -1
  10. package/dist/core/dsl/types.d.ts +2 -0
  11. package/dist/core/exceptions/programmer-errors.d.ts +4 -4
  12. package/dist/core/explanation/explain.d.ts +5 -1
  13. package/dist/core/explanation/index.js +45 -5
  14. package/dist/core/knowledge-base/index.js +2 -2
  15. package/dist/core/query/index.d.ts +1 -1
  16. package/dist/core/query/index.js +406 -67
  17. package/dist/core/solver/calendar.d.ts +4 -0
  18. package/dist/core/solver/candidate-search.d.ts +3 -1
  19. package/dist/core/solver/program-index.d.ts +17 -0
  20. package/dist/core/solver/query-validation/utils.d.ts +2 -0
  21. package/dist/core/types/atom.d.ts +7 -2
  22. package/dist/core/types/goal.d.ts +23 -5
  23. package/dist/core/types/judgment.d.ts +2 -2
  24. package/dist/core/types/schema.d.ts +2 -2
  25. package/dist/jev/chunk-QEZ4O3YS.js +203 -0
  26. package/dist/jev/chunk-ZAAXPTJJ.js +105 -0
  27. package/dist/jev/classifier/classifiable-predicates.d.ts +12 -0
  28. package/dist/jev/classifier/classifier.d.ts +16 -0
  29. package/dist/jev/classifier/extract-evidence.d.ts +17 -0
  30. package/dist/jev/classifier/filter-predicates.d.ts +9 -0
  31. package/dist/jev/classifier/filter-symbols.d.ts +14 -0
  32. package/dist/jev/classifier/index.d.ts +6 -0
  33. package/dist/jev/classifier/index.js +246 -0
  34. package/dist/jev/classifier/types.d.ts +13 -0
  35. package/dist/jev/config.d.ts +17 -0
  36. package/dist/jev/config.js +18 -0
  37. package/dist/jev/jev/model.d.ts +11 -0
  38. package/dist/jev/jev/prompts.d.ts +29 -0
  39. package/dist/jev/resolve.d.ts +5 -72
  40. package/dist/jev/resolve.js +24 -47
  41. package/dist/jev/settings.d.ts +11 -0
  42. package/dist/jev/types/config.d.ts +18 -0
  43. package/dist/jev/types/model.d.ts +58 -0
  44. package/dist/jev/types/run.d.ts +9 -0
  45. package/dist/jev/utils.d.ts +9 -0
  46. package/package.json +7 -3
  47. package/dist/jev/chunk-PISXBJOT.js +0 -206
  48. package/dist/jev/classifier.d.ts +0 -28
  49. package/dist/jev/classifier.js +0 -171
  50. package/dist/jev/client.d.ts +0 -62
  51. package/dist/jev/jobs.d.ts +0 -61
  52. package/dist/jev/tools.d.ts +0 -9
  53. package/dist/jev/types.d.ts +0 -63
  54. /package/dist/core/{chunk-A6ZZJWIO.js → chunk-NDUIQAH4.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # factbag
2
2
 
3
+ ## 0.0.0-alpha.4
4
+
5
+ ### Minor Changes
6
+
7
+ - 2814aab: Allow enumerations to hold numbers or dates as well as strings, with one member
8
+ type per enumeration. Members must be unique; dates compare by instant and are
9
+ copied on construction. NaN and invalid dates are rejected. Enumeration membership
10
+ validation at assertion and query time is unchanged.
11
+ - 1f5bb1b: Classify predicates whose fields are number or Date enumerations, alongside
12
+ string enumerations. Dates are sent to the model as ISO strings and retained as
13
+ `Date` values in evidence. Open-ended `String`, `Number`, and `Date` fields
14
+ remain unsupported.
15
+ - 9ca1766: Reorganize classification and resolution to separate evidence workflows from model requests. The workflows own evidence admission and knowledge-base updates; the Jev implementation owns prompts, encoding, batching, and response validation.
16
+
17
+ **Breaking changes:** call the classifier with `classifier({ corpus, input, config })`. Classification and resolution jobs now expose only `execute(): Promise<void>`; read the resulting knowledge base from `run.kb`. Job metadata and assessment and resolution reports are removed. Replace `RequestConfig`, `ResolutionConfig`, and nested `AdmissionPolicy` settings with the shared, flat `Config`. Pass an SDK client directly instead of implementing the removed custom client interfaces.
18
+
19
+ Classification now executes one job per chunk across all permitted predicates. All responses for the chunk must validate before its evidence is written. Failed jobs can be retried without partial evidence, repeating the chunk's requests; successful repeats add fresh evidence.
20
+
21
+ Optionally filter candidates by passage relevance with `config: { routing: true }`. Each chunk first asks which predicates the passage may bear on. For predicates that make claims about supplied entities or values, it can also filter those symbols before combining them into complete candidate facts. `candidateProductCutoff` controls when this extra filtering runs. Filtering defaults to off: false negatives can suppress evidence, including denials, and the extra requests do not guarantee cost savings.
22
+
23
+ Expose `createConfig`, `Config`, and shared `DEFAULT_*` constants through `factbag/jev/config`. Normalize settings once and preserve a shared lazy client when a factory-created config is reused across workflows.
24
+
25
+ Export `fieldEquals` from `factbag/dsl` and use core equality in Jev: entities compare by constructor and ID, dates by instant. Resolution now groups contradictory evidence about separate instances of the same entity.
26
+
27
+ Encode candidate values when building requests, using the original entities and dates instead of construction-time JSON snapshots. Candidate domains remain fixed at run construction.
28
+
29
+ ## 0.0.0-alpha.3
30
+
31
+ ### Minor Changes
32
+
33
+ - 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.
34
+ - 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.
35
+ - 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.
36
+
37
+ ### Patch Changes
38
+
39
+ - 01cc338: Speed up queries over large rulesets with linear-time recursive predicate grouping,
40
+ per-query rule and evidence indexes, and judgment reuse during candidate discovery.
41
+ Preserve candidate pruning and distinguish same-named predicate declarations when
42
+ tracking active atoms, avoiding incorrect recursion cutoffs.
43
+ - 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.
44
+ - a074dab: Judge evidence-free, closed-world recursive components together as a least fixed
45
+ point instead of enumerating recursive proof paths. Doubly recursive transitive
46
+ closures and symmetric compositions no longer require exponential proof search.
47
+ Other components retain the existing search semantics. Variable queries recognize
48
+ a non-recursive disjunctive base case within an exclusive definition.
49
+
3
50
  ## 0.0.0-alpha.2
4
51
 
5
52
  ### Major Changes
package/README.md CHANGED
@@ -79,7 +79,7 @@ class Person {
79
79
 
80
80
  const hasIncomeOver200k = predicate(
81
81
  "has_income_over_200k",
82
- { person: Person, year: enumeration("2024", "2025") },
82
+ { person: Person, year: enumeration(2024, 2025) },
83
83
  {
84
84
  description:
85
85
  "The person's total individual annual income exceeded USD 200,000 in the " +
@@ -117,8 +117,8 @@ const kb = new KnowledgeBase();
117
117
 
118
118
  kb.rule(isQualifiedInvestor({ person })).if(
119
119
  and(
120
- hasIncomeOver200k({ person, year: "2024" }),
121
- hasIncomeOver200k({ person, year: "2025" }),
120
+ hasIncomeOver200k({ person, year: 2024 }),
121
+ hasIncomeOver200k({ person, year: 2025 }),
122
122
  expectsSameIncome({ person }),
123
123
  ),
124
124
  );
@@ -170,7 +170,7 @@ const incomeEvidence = guard(kb)
170
170
  ### 6. Ask Jev to assess the income evidence
171
171
 
172
172
  ```ts
173
- const run = classifier(corpus, incomeEvidence);
173
+ const run = classifier({ corpus, input: incomeEvidence });
174
174
 
175
175
  for (const job of run) {
176
176
  await job.execute(); // Makes a live Jev request.
@@ -82,5 +82,6 @@ export {
82
82
  isAtomForPredicate,
83
83
  atomEquals,
84
84
  fieldEquals,
85
- fieldIdentityToken
85
+ fieldIdentityToken,
86
+ isEntityValue
86
87
  };
@@ -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;
@@ -1,3 +1,7 @@
1
1
  import type { EnumType } from "../types/schema.js";
2
- /** Declare a nonempty finite string domain. Values must be unique. */
2
+ /** Declare a nonempty finite domain of one member type. Values must be unique. */
3
3
  export declare function enumeration<const Value extends string>(first: Value, ...rest: readonly Value[]): EnumType<Value>;
4
+ /** Declare a nonempty finite number domain. NaN and duplicate values are rejected. */
5
+ export declare function enumeration<const Value extends number>(first: Value, ...rest: readonly Value[]): EnumType<Value>;
6
+ /** Declare a nonempty finite date domain. Dates are copied and must have unique, valid instants. */
7
+ export declare function enumeration(first: Date, ...rest: readonly Date[]): EnumType<Date>;
@@ -6,16 +6,16 @@
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";
15
15
  export type { Annotations, AnnotationValue } from "../types/annotations.js";
16
16
  export { variable } from "./variable.js";
17
17
  export type { Formula, Scalar, Term } from "./types.js";
18
- export { assertAtomLike, assertGroundAtom } from "../types/atom.js";
18
+ export { assertAtomLike, assertGroundAtom, fieldEquals } from "../types/atom.js";
19
19
  export type { AnyAtom, Atom, GroundAtom, GroundFields, NonGroundAtom, PatternFields, } from "../types/atom.js";
20
20
  export type { Entity, EnumType, InferRuntime, PredicateSchema, RuntimeConstructor, RuntimeType, } from "../types/schema.js";
21
21
  export { Var } from "../types/variable.js";
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  assertAtomLike,
3
- assertGroundAtom
4
- } from "../chunk-5J3WGMN2.js";
3
+ assertGroundAtom,
4
+ fieldEquals
5
+ } from "../chunk-N2RRSOEN.js";
5
6
  import {
6
7
  Var
7
8
  } from "../chunk-UVOCOH2I.js";
@@ -68,8 +69,53 @@ function div(left, right) {
68
69
  assertSupportedValue(right);
69
70
  return { kind: "div", args: [left, right] };
70
71
  }
72
+ function daysBetween(later, earlier) {
73
+ assertSupportedValue(later);
74
+ assertSupportedValue(earlier);
75
+ return { kind: "daysBetween", args: [later, earlier] };
76
+ }
77
+ function yearsBetween(later, earlier) {
78
+ assertSupportedValue(later);
79
+ assertSupportedValue(earlier);
80
+ return { kind: "yearsBetween", args: [later, earlier] };
81
+ }
82
+ function min(left, right) {
83
+ assertSupportedValue(left);
84
+ assertSupportedValue(right);
85
+ return { kind: "min", args: [left, right] };
86
+ }
87
+ function max(left, right) {
88
+ assertSupportedValue(left);
89
+ assertSupportedValue(right);
90
+ return { kind: "max", args: [left, right] };
91
+ }
92
+ function roundTo(value, places) {
93
+ assertSupportedValue(value);
94
+ assertSupportedValue(places);
95
+ return { kind: "roundTo", args: [value, places] };
96
+ }
97
+ function floorTo(value, places) {
98
+ assertSupportedValue(value);
99
+ assertSupportedValue(places);
100
+ return { kind: "floorTo", args: [value, places] };
101
+ }
102
+ function ceilTo(value, places) {
103
+ assertSupportedValue(value);
104
+ assertSupportedValue(places);
105
+ return { kind: "ceilTo", args: [value, places] };
106
+ }
71
107
 
72
108
  // src/dsl/operators/comparison.ts
109
+ function eq(left, right) {
110
+ assertSupportedValue(left);
111
+ assertSupportedValue(right);
112
+ return { kind: "eq", args: [left, right] };
113
+ }
114
+ function neq(left, right) {
115
+ assertSupportedValue(left);
116
+ assertSupportedValue(right);
117
+ return { kind: "neq", args: [left, right] };
118
+ }
73
119
  function gt(left, right) {
74
120
  assertSupportedValue(left);
75
121
  assertSupportedValue(right);
@@ -121,10 +167,23 @@ function predicate(name, params, metadata) {
121
167
 
122
168
  // src/dsl/enumeration.ts
123
169
  function enumeration(first, ...rest) {
124
- const values = [first, ...rest];
125
- if (values.some((value) => typeof value !== "string") || new Set(values).size !== values.length) {
126
- throw new Error("Enum values must be unique strings");
127
- }
170
+ const kind = first instanceof Date ? "date" : typeof first;
171
+ const seen = /* @__PURE__ */ new Set();
172
+ const values = [first, ...rest].map((value) => {
173
+ const memberKind = value instanceof Date ? "date" : typeof value;
174
+ if (memberKind !== kind || kind !== "string" && kind !== "number" && kind !== "date") {
175
+ throw new Error("Enum values must all be strings, numbers, or dates of one type");
176
+ }
177
+ const key = value instanceof Date ? value.getTime() : value;
178
+ if (typeof key === "number" && Number.isNaN(key)) {
179
+ throw new Error("Enum values must not be NaN or invalid dates");
180
+ }
181
+ if (seen.has(key)) {
182
+ throw new Error("Enum values must be unique (dates compare by instant)");
183
+ }
184
+ seen.add(key);
185
+ return value instanceof Date ? new Date(key) : value;
186
+ });
128
187
  return Object.freeze({ kind: "enum", values: Object.freeze(values) });
129
188
  }
130
189
 
@@ -138,20 +197,30 @@ export {
138
197
  and,
139
198
  assertAtomLike,
140
199
  assertGroundAtom,
200
+ ceilTo,
141
201
  closedPredicate,
142
202
  count,
203
+ daysBetween,
143
204
  div,
144
205
  enumeration,
206
+ eq,
145
207
  exists,
208
+ fieldEquals,
209
+ floorTo,
146
210
  gt,
147
211
  gte,
148
212
  letIn,
149
213
  lte,
214
+ max,
215
+ min,
150
216
  mul,
217
+ neq,
151
218
  not,
152
219
  or,
153
220
  predicate,
221
+ roundTo,
154
222
  sub,
155
223
  sum,
156
- variable
224
+ variable,
225
+ yearsBetween
157
226
  };
@@ -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-N2RRSOEN.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";