factbag 0.0.0-alpha.5 → 0.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # factbag
2
2
 
3
+ ## 0.0.0-alpha.6
4
+
5
+ ### Minor Changes
6
+
7
+ - 7225a51: Add `firstOfMonth(year, month)`, a date-valued arithmetic term for the first day of a month at UTC midnight, and `yearOf(date)`, a number-valued term for a date's UTC calendar year. Rules can now derive a year's bounds, such as `firstOfMonth(Y, 1)` and `sub(firstOfMonth(add(Y, 1), 1), 86_400_000)`, instead of looking them up in asserted calendar facts. A month outside 1 to 12, or a year that is not a whole number, throws `PrimitiveArithmeticProgrammerError` rather than rolling over. The query API adds `FirstOfMonthGoal` and the `"firstOfMonth"` arithmetic kind.
8
+ - 5114b2f: Show the `citation` and `justification` recorded with a piece of evidence in its explanation, beside its `source`. An evidence reason no longer repeats the claim of the step it supports; it names its own claim only when that differs, as in a conflict, or when the evidence has no provenance. `EvidenceReason.quote`, which was never filled, is replaced by `citation`.
9
+ - eab3679: Explain a rule application with the rule alone, `eligible(P) iff not(active_sanction(P))`, without the trailing `, with P = alice`. The values a rule was applied to are already written into the claim it supports and the claims it rests on.
10
+ - e4b343e: Add a `budget` option to `explainJudgment`: the most claims to explain. The conclusion is always explained, then the claims that decided it before those that were evaluated and changed nothing, and those near the conclusion before those far from it. A claim that rests on claims left out names them, with their values, under `notShown`. Without a budget the whole proof is explained, as before; written as JSON a deep proof repeats each shared claim on every path to it, so give a budget when explaining a judgment in text.
11
+
12
+ ### Patch Changes
13
+
14
+ - 7f26b45: Write explanation claims with the values their variables hold: `lte(1000, 36900) = true` instead of `lte(I, 36900) = true`, and `spouse(alice, S) = false` instead of `spouse(P, S) = false`. A variable with no value keeps its name. A term that computes a value states it, such as `H = max(1000, 500) = 1000`, instead of ending in `= true`.
15
+ - b7c85ff: Explain a comparison, arithmetic term or aggregate as `computed` instead of `no supporting evidence or rule was found`. When one could not be evaluated because a variable has no value, the explanation names the variable, such as `X has no value`.
16
+ - ed6ec84: Write a date at UTC midnight as a day in explanations, `2015-12-31` instead of `2015-12-31T00:00:00.000Z`. A date with a time of day keeps its full timestamp.
17
+ - 15391f2: Rest a rule application directly on the subgoals of its body in explanations. When a rule's body is an `and` or an `or`, the explanation no longer has a node that restates the body between the rule and its subgoals: the rule's `given` lists the subgoals themselves. A compound nested inside a body is still its own node, and so is a body that is a single atom or a negation.
18
+ - ecbd8d9: Stop naming the solver's temporaries, such as `$tmp0`, in explanations. A nested arithmetic term is written as the term and its value, `mul(1000, 0.15) = 150`; a term that could not be computed stands in for its result wherever that is used, `gte(add(X, 1), 18) = unknown`; and a rule's `with ...` list gives only the rule's own variables.
19
+
3
20
  ## 0.0.0-alpha.5
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -221,33 +221,25 @@ An abbreviated example with supporting evidence for all three claims:
221
221
  ```json
222
222
  {
223
223
  "claim": "is_qualified_investor(alice) = true",
224
- "because": "is_qualified_investor(person) if and(...), with person = alice",
224
+ "because": "is_qualified_investor(person) if and(...)",
225
225
  "given": [
226
226
  {
227
- "claim": "and(...) = true",
228
- "given": [
229
- {
230
- "claim": "has_income_over_200k(alice, 2024) = true",
231
- "because": {
232
- "claim": "has_income_over_200k(alice, 2024) = true",
233
- "source": "alice-2024-federal-tax-return#0-1"
234
- }
235
- },
236
- {
237
- "claim": "has_income_over_200k(alice, 2025) = true",
238
- "because": {
239
- "claim": "has_income_over_200k(alice, 2025) = true",
240
- "source": "alice-2025-federal-tax-return#0-1"
241
- }
242
- },
243
- {
244
- "claim": "expects_same_income(alice) = true",
245
- "because": {
246
- "claim": "expects_same_income(alice) = true",
247
- "source": "alice-2026-employment-confirmation-letter#0-1"
248
- }
249
- }
250
- ]
227
+ "claim": "has_income_over_200k(alice, 2024) = true",
228
+ "because": {
229
+ "source": "alice-2024-federal-tax-return#0-1"
230
+ }
231
+ },
232
+ {
233
+ "claim": "has_income_over_200k(alice, 2025) = true",
234
+ "because": {
235
+ "source": "alice-2025-federal-tax-return#0-1"
236
+ }
237
+ },
238
+ {
239
+ "claim": "expects_same_income(alice) = true",
240
+ "because": {
241
+ "source": "alice-2026-employment-confirmation-letter#0-1"
242
+ }
251
243
  }
252
244
  ]
253
245
  }
package/dist/dsl.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { A as PrimitiveGoal, B as Formula, C as LteGoal, Ct as Var, D as NeqGoal, Dt as PredicateSchema, E as MulGoal, Et as InferRuntime, F as AggregateBuilder, G as RuleBodyArgs, I as ComparableTerm, J as Scalar, K as RuleBodyInput, L as DateTerm, M as SubGoal, N as SumGoal, O as NotGoal, Ot as RuntimeConstructor, P as YearsBetweenGoal, R as EqualityTerm, S as LtGoal, St as Annotations, T as MinGoal, Tt as EnumType, V as FormulaArgumentInput, W as NumericTerm, Y as Term, _ as GoalExpression, _t as PredicateBase, a as ArithmeticKind, at as AnyAtom, b as GteGoal, bt as PredicateWorld, c as ComparisonGoal, ct as GroundFields, d as DaysBetweenGoal, dt as assertAtomLike, f as DivGoal, ft as assertGroundAtom, g as Goal, gt as Predicate, h as FloorToGoal, ht as OpenWorldPredicate, i as ArithmeticGoal, j as RoundToGoal, k as OrGoal, kt as RuntimeType, l as CompoundGoal, lt as NonGroundAtom, m as ExistsGoal, mt as ClosedWorldPredicate, n as AndGoal, nt as NegativeLiteral, o as AtomGoal, ot as Atom, p as EqGoal, pt as fieldEquals, r as AnyAtomGoal, s as CeilToGoal, st as GroundAtom, t as AddGoal, u as CountGoal, ut as PatternFields, v as GroundAtomGoal, vt as PredicateDeclaration, w as MaxGoal, wt as Entity, x as LetGoal, xt as AnnotationValue, y as GtGoal, yt as PredicateMetadata, z as ExistsArgs } from "./goal-BP745uKh.js";
2
- import { n as KnowledgeBase } from "./knowledge-base-Da5HLCsY.js";
3
- import { C as Bindings, S as Binding, _ as QueryAllJudgment, a as ErasedEvidenceSupport, b as SubgoalsSupport, c as Judgment, d as MissingBinding, f as NumericRange, g as PrimitiveUncertainty, h as PrimitiveSupportFor, i as CountPrimitiveSupport, l as JudgmentBase, m as PrimitiveSupport, n as ClosedWorldAbsenceSupport, o as ErasedRuleSupport, p as PrimitiveEvaluation, r as ComparisonPrimitiveSupport, s as EvidenceSupport, t as ArithmeticPrimitiveSupport, u as JudgmentSupportFor, v as QueryJudgment, w as GoalValue, x as SumPrimitiveSupport, y as RuleSupport } from "./judgment-Cr0JcUt5.js";
1
+ import { A as OrGoal, At as RuntimeType, B as ExistsArgs, C as LtGoal, Ct as Annotations, D as MulGoal, Dt as InferRuntime, E as MinGoal, Et as EnumType, F as YearsBetweenGoal, G as NumericTerm, H as FormulaArgumentInput, I as AggregateBuilder, K as RuleBodyArgs, L as ComparableTerm, M as RoundToGoal, N as SubGoal, O as NeqGoal, Ot as PredicateSchema, P as SumGoal, R as DateTerm, S as LetGoal, St as AnnotationValue, T as MaxGoal, Tt as Entity, V as Formula, X as Term, Y as Scalar, _ as Goal, _t as Predicate, a as ArithmeticKind, b as GtGoal, bt as PredicateMetadata, c as ComparisonGoal, ct as GroundAtom, d as DaysBetweenGoal, dt as PatternFields, f as DivGoal, ft as assertAtomLike, g as FloorToGoal, gt as OpenWorldPredicate, h as FirstOfMonthGoal, ht as ClosedWorldPredicate, i as ArithmeticGoal, j as PrimitiveGoal, k as NotGoal, kt as RuntimeConstructor, l as CompoundGoal, lt as GroundFields, m as ExistsGoal, mt as fieldEquals, n as AndGoal, o as AtomGoal, ot as AnyAtom, p as EqGoal, pt as assertGroundAtom, q as RuleBodyInput, r as AnyAtomGoal, rt as NegativeLiteral, s as CeilToGoal, st as Atom, t as AddGoal, u as CountGoal, ut as NonGroundAtom, v as GoalExpression, vt as PredicateBase, w as LteGoal, wt as Var, x as GteGoal, xt as PredicateWorld, y as GroundAtomGoal, yt as PredicateDeclaration, z as EqualityTerm } from "./goal-BBoSfPUN.js";
2
+ import { n as KnowledgeBase } from "./knowledge-base-CkU_xzmB.js";
3
+ import { C as Bindings, S as Binding, _ as QueryAllJudgment, a as ErasedEvidenceSupport, b as SubgoalsSupport, c as Judgment, d as MissingBinding, f as NumericRange, g as PrimitiveUncertainty, h as PrimitiveSupportFor, i as CountPrimitiveSupport, l as JudgmentBase, m as PrimitiveSupport, n as ClosedWorldAbsenceSupport, o as ErasedRuleSupport, p as PrimitiveEvaluation, r as ComparisonPrimitiveSupport, s as EvidenceSupport, t as ArithmeticPrimitiveSupport, u as JudgmentSupportFor, v as QueryJudgment, w as GoalValue, x as SumPrimitiveSupport, y as RuleSupport } from "./judgment-v8i3ks5N.js";
4
4
  /**
5
5
  * Negate an atom in a rule body. Open-world predicates require explicit
6
6
  * negative support; closed-world predicates use negation-as-failure.
@@ -124,6 +124,28 @@ export declare function daysBetween(later: DateTerm, earlier: DateTerm): Formula
124
124
  * A person's age on a day is `yearsBetween(day, birthday)`.
125
125
  */
126
126
  export declare function yearsBetween(later: DateTerm, earlier: DateTerm): Formula<number, readonly [DateTerm, DateTerm]>;
127
+ /**
128
+ * The first day of a month at UTC midnight, from a whole year and a whole month from 1
129
+ * (January) to 12. Other days of the year follow by date arithmetic.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * // The tax year Y runs from its first day to the day before the next year's.
134
+ * letIn(First, firstOfMonth(Y, 1), letIn(Last, sub(firstOfMonth(add(Y, 1), 1), dayMs), ...))
135
+ * ```
136
+ */
137
+ export declare function firstOfMonth(year: NumericTerm, month: NumericTerm): Formula<Date, readonly [NumericTerm, NumericTerm]>;
138
+ /**
139
+ * The UTC calendar year of a date, for dates from the year 0 on: the whole years from
140
+ * January 1 of the year 0, so it explains as that `yearsBetween`.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * // A death in an earlier year than Y.
145
+ * and(died({ person: P, day: D }), gt(Y, yearOf(D)))
146
+ * ```
147
+ */
148
+ export declare function yearOf(date: DateTerm): Formula<number, readonly [DateTerm, DateTerm]>;
127
149
  /**
128
150
  * The lesser of two numeric terms.
129
151
  *
@@ -284,4 +306,4 @@ export declare function query<Schema extends PredicateSchema>(kb: KnowledgeBase,
284
306
  * ```
285
307
  */
286
308
  export declare function queryAll<Schema extends PredicateSchema>(kb: KnowledgeBase, pattern: Atom<Schema>): QueryAllJudgment<Schema>;
287
- export { type AddGoal, type AggregateBuilder, type AndGoal, type AnnotationValue, type Annotations, type AnyAtom, type AnyAtomGoal, type ArithmeticGoal, type ArithmeticKind, type ArithmeticPrimitiveSupport, type Atom, type AtomGoal, type Binding, type Bindings, type CeilToGoal, type ClosedWorldAbsenceSupport, type ClosedWorldPredicate, type ComparableTerm, type ComparisonGoal, type ComparisonPrimitiveSupport, type CompoundGoal, type CountGoal, type CountPrimitiveSupport, type DateTerm, type DaysBetweenGoal, type DivGoal, type Entity, type EnumType, type EqGoal, type EqualityTerm, type ErasedEvidenceSupport, type ErasedRuleSupport, type EvidenceSupport, type ExistsGoal, type FloorToGoal, type Formula, type Goal, type GoalExpression, type GoalValue, type GroundAtom, type GroundAtomGoal, type GroundFields, type GtGoal, type GteGoal, type InferRuntime, type Judgment, type JudgmentBase, type JudgmentSupportFor, type LetGoal, type LtGoal, type LteGoal, type MaxGoal, type MinGoal, type MissingBinding, type MulGoal, type NeqGoal, type NonGroundAtom, type NotGoal, type NumericRange, type NumericTerm, type OpenWorldPredicate, type OrGoal, type PatternFields, type Predicate, type PredicateBase, type PredicateDeclaration, type PredicateMetadata, type PredicateSchema, type PredicateWorld, type PrimitiveEvaluation, type PrimitiveGoal, type PrimitiveSupport, type PrimitiveSupportFor, type PrimitiveUncertainty, type QueryAllJudgment, type QueryJudgment, type RoundToGoal, type RuleSupport, type RuntimeConstructor, type RuntimeType, type Scalar, type SubGoal, type SubgoalsSupport, type SumGoal, type SumPrimitiveSupport, type Term, Var, type YearsBetweenGoal, assertAtomLike, assertGroundAtom, fieldEquals };
309
+ export { type AddGoal, type AggregateBuilder, type AndGoal, type AnnotationValue, type Annotations, type AnyAtom, type AnyAtomGoal, type ArithmeticGoal, type ArithmeticKind, type ArithmeticPrimitiveSupport, type Atom, type AtomGoal, type Binding, type Bindings, type CeilToGoal, type ClosedWorldAbsenceSupport, type ClosedWorldPredicate, type ComparableTerm, type ComparisonGoal, type ComparisonPrimitiveSupport, type CompoundGoal, type CountGoal, type CountPrimitiveSupport, type DateTerm, type DaysBetweenGoal, type DivGoal, type Entity, type EnumType, type EqGoal, type EqualityTerm, type ErasedEvidenceSupport, type ErasedRuleSupport, type EvidenceSupport, type ExistsGoal, type FirstOfMonthGoal, type FloorToGoal, type Formula, type Goal, type GoalExpression, type GoalValue, type GroundAtom, type GroundAtomGoal, type GroundFields, type GtGoal, type GteGoal, type InferRuntime, type Judgment, type JudgmentBase, type JudgmentSupportFor, type LetGoal, type LtGoal, type LteGoal, type MaxGoal, type MinGoal, type MissingBinding, type MulGoal, type NeqGoal, type NonGroundAtom, type NotGoal, type NumericRange, type NumericTerm, type OpenWorldPredicate, type OrGoal, type PatternFields, type Predicate, type PredicateBase, type PredicateDeclaration, type PredicateMetadata, type PredicateSchema, type PredicateWorld, type PrimitiveEvaluation, type PrimitiveGoal, type PrimitiveSupport, type PrimitiveSupportFor, type PrimitiveUncertainty, type QueryAllJudgment, type QueryJudgment, type RoundToGoal, type RuleSupport, type RuntimeConstructor, type RuntimeType, type Scalar, type SubGoal, type SubgoalsSupport, type SumGoal, type SumPrimitiveSupport, type Term, Var, type YearsBetweenGoal, assertAtomLike, assertGroundAtom, fieldEquals };
package/dist/dsl.js CHANGED
@@ -1,5 +1,5 @@
1
- import { i as Var } from "./types-0ZvnLf2N.js";
2
- import { a as fieldEquals, i as assertGroundAtom, n as queryAll, r as assertAtomLike, t as query } from "./query-Dxm7EpxZ.js";
1
+ import { a as Var } from "./types-DeYyybXd.js";
2
+ import { a as fieldEquals, i as assertGroundAtom, n as queryAll, r as assertAtomLike, t as query } from "./query-CN6EOPwp.js";
3
3
  // ../core/src/dsl/assert-supported-value.ts
4
4
  /** Reject removed scalar types before building rules or atoms. */
5
5
  function assertSupportedValue(value) {
@@ -206,6 +206,37 @@ function yearsBetween(later, earlier) {
206
206
  };
207
207
  }
208
208
  /**
209
+ * The first day of a month at UTC midnight, from a whole year and a whole month from 1
210
+ * (January) to 12. Other days of the year follow by date arithmetic.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * // The tax year Y runs from its first day to the day before the next year's.
215
+ * letIn(First, firstOfMonth(Y, 1), letIn(Last, sub(firstOfMonth(add(Y, 1), 1), dayMs), ...))
216
+ * ```
217
+ */
218
+ function firstOfMonth(year, month) {
219
+ assertSupportedValue(year);
220
+ assertSupportedValue(month);
221
+ return {
222
+ kind: "firstOfMonth",
223
+ args: [year, month]
224
+ };
225
+ }
226
+ /**
227
+ * The UTC calendar year of a date, for dates from the year 0 on: the whole years from
228
+ * January 1 of the year 0, so it explains as that `yearsBetween`.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * // A death in an earlier year than Y.
233
+ * and(died({ person: P, day: D }), gt(Y, yearOf(D)))
234
+ * ```
235
+ */
236
+ function yearOf(date) {
237
+ return yearsBetween(date, firstOfMonth(0, 1));
238
+ }
239
+ /**
209
240
  * The lesser of two numeric terms.
210
241
  *
211
242
  * @example
@@ -442,4 +473,4 @@ function enumeration(first, ...rest) {
442
473
  function variable(name) {
443
474
  return new Var(name);
444
475
  }
445
- export { Var, add, and, assertAtomLike, assertGroundAtom, ceilTo, closedPredicate, count, daysBetween, div, enumeration, eq, exists, fieldEquals, floorTo, gt, gte, letIn, lte, max, min, mul, neq, not, or, predicate, query, queryAll, roundTo, sub, sum, variable, yearsBetween };
476
+ export { Var, add, and, assertAtomLike, assertGroundAtom, ceilTo, closedPredicate, count, daysBetween, div, enumeration, eq, exists, fieldEquals, firstOfMonth, floorTo, gt, gte, letIn, lte, max, min, mul, neq, not, or, predicate, query, queryAll, roundTo, sub, sum, variable, yearOf, yearsBetween };
@@ -1,4 +1,4 @@
1
- import { Ct as Var, X as AnyRule, a as ArithmeticKind, i as ArithmeticGoal, vt as PredicateDeclaration } from "./goal-BP745uKh.js";
1
+ import { Z as AnyRule, a as ArithmeticKind, i as ArithmeticGoal, wt as Var, yt as PredicateDeclaration } from "./goal-BBoSfPUN.js";
2
2
  /** Base class for errors that mean the caller supplied invalid program logic. */
3
3
  export declare class ProgrammerError extends Error {
4
4
  constructor(message: string);
@@ -1 +1 @@
1
- export { t as ExclusiveDefinitionProgrammerError, n as PrimitiveArithmeticProgrammerError, r as ProgrammerError, i as UnconstructiveRecursionProgrammerError, a as UnsafeUnboundVariableProgrammerError, o as UnsupportedPredicateError } from "./programmer-errors-DDfy03Y0.js";
1
+ export { t as ExclusiveDefinitionProgrammerError, n as PrimitiveArithmeticProgrammerError, r as ProgrammerError, i as UnconstructiveRecursionProgrammerError, a as UnsafeUnboundVariableProgrammerError, o as UnsupportedPredicateError } from "./programmer-errors-DLE1xWJ_.js";
package/dist/explain.d.ts CHANGED
@@ -1,8 +1,14 @@
1
- import { c as Judgment } from "./judgment-Cr0JcUt5.js";
1
+ import { c as Judgment } from "./judgment-v8i3ks5N.js";
2
2
  /** Shared shape for every compact, LLM-readable explanation node. */
3
3
  type ExplanationBase = Readonly<{
4
4
  /** The evaluated claim and its value, e.g. `eligible(alice) = true`. */
5
5
  claim: string;
6
+ /**
7
+ * Claims this one also rests on that a budget left unexplained, each with its value,
8
+ * e.g. `resident(alice) = true`. What decided a claim is explained before what did
9
+ * not, so these are most often claims that were evaluated and changed nothing.
10
+ */
11
+ notShown?: readonly string[];
6
12
  }>;
7
13
  /**
8
14
  * A compact, LLM-readable explanation node.
@@ -12,7 +18,7 @@ type ExplanationBase = Readonly<{
12
18
  * ```json
13
19
  * {
14
20
  * "claim": "eligible(alice) = true",
15
- * "because": "eligible(P) iff not(active_sanction(P)), with P = alice",
21
+ * "because": "eligible(P) iff not(active_sanction(P))",
16
22
  * "given": [
17
23
  * {
18
24
  * "claim": "not(active_sanction(alice)) = true",
@@ -33,7 +39,7 @@ type Explanation = EvidenceExplanation | DerivedExplanation | CompoundExplanatio
33
39
  * "claim": "active_sanction(bob) = true",
34
40
  * "because": {
35
41
  * "source": "sanctions_registry",
36
- * "quote": "Bob appears on the active sanctions list."
42
+ * "citation": "Bob appears on the active sanctions list."
37
43
  * }
38
44
  * }
39
45
  * ```
@@ -47,12 +53,12 @@ type Explanation = EvidenceExplanation | DerivedExplanation | CompoundExplanatio
47
53
  * {
48
54
  * "claim": "active_sanction(drew) = true",
49
55
  * "source": "sanctions_registry",
50
- * "quote": "Drew appears on the active sanctions list."
56
+ * "citation": "Drew appears on the active sanctions list."
51
57
  * },
52
58
  * {
53
59
  * "claim": "active_sanction(drew) = false",
54
60
  * "source": "appeals_board",
55
- * "quote": "Drew's sanction was overturned on appeal."
61
+ * "citation": "Drew's sanction was overturned on appeal."
56
62
  * }
57
63
  * ]
58
64
  * }
@@ -69,7 +75,7 @@ type EvidenceExplanation = ExplanationBase & Readonly<{
69
75
  * ```json
70
76
  * {
71
77
  * "claim": "eligible(bob) = false",
72
- * "because": "eligible(P) iff not(active_sanction(P)), with P = bob",
78
+ * "because": "eligible(P) iff not(active_sanction(P))",
73
79
  * "given": [
74
80
  * {
75
81
  * "claim": "not(active_sanction(bob)) = false",
@@ -78,7 +84,7 @@ type EvidenceExplanation = ExplanationBase & Readonly<{
78
84
  * "claim": "active_sanction(bob) = true",
79
85
  * "because": {
80
86
  * "source": "sanctions_registry",
81
- * "quote": "Bob appears on the active sanctions list."
87
+ * "citation": "Bob appears on the active sanctions list."
82
88
  * }
83
89
  * }
84
90
  * ]
@@ -106,12 +112,12 @@ type DerivedExplanation = ExplanationBase & Readonly<{
106
112
  * {
107
113
  * "claim": "active_sanction(drew) = true",
108
114
  * "source": "sanctions_registry",
109
- * "quote": "Drew appears on the active sanctions list."
115
+ * "citation": "Drew appears on the active sanctions list."
110
116
  * },
111
117
  * {
112
118
  * "claim": "active_sanction(drew) = false",
113
119
  * "source": "appeals_board",
114
- * "quote": "Drew's sanction was overturned on appeal."
120
+ * "citation": "Drew's sanction was overturned on appeal."
115
121
  * }
116
122
  * ]
117
123
  * }
@@ -146,7 +152,7 @@ type TerminalExplanation = ExplanationBase & Readonly<{
146
152
  * ```json
147
153
  * {
148
154
  * "source": "sanctions_registry",
149
- * "quote": "Bob appears on the active sanctions list."
155
+ * "citation": "Bob appears on the active sanctions list."
150
156
  * }
151
157
  * ```
152
158
  *
@@ -156,28 +162,48 @@ type TerminalExplanation = ExplanationBase & Readonly<{
156
162
  * {
157
163
  * "claim": "active_sanction(drew) = false",
158
164
  * "source": "appeals_board",
159
- * "quote": "Drew's sanction was overturned on appeal.",
165
+ * "citation": "Drew's sanction was overturned on appeal.",
160
166
  * "metadata": { "reviewed_by": "appeals_panel" }
161
167
  * }
162
168
  * ```
163
169
  */
164
170
  type EvidenceReason = Readonly<{
165
- /** Optional when the surrounding `claim` already names what the evidence proves. */
171
+ /**
172
+ * What this evidence says, e.g. `active_sanction(drew) = false`. Left out when it only
173
+ * repeats the claim of the explanation it sits under and the evidence has provenance.
174
+ */
166
175
  claim?: string;
167
176
  /** Where the evidence came from, e.g. `sanctions_registry`. */
168
177
  source?: string;
169
- /** The quoted text or excerpt that supports the evidence claim. */
170
- quote?: string;
178
+ /** The words of the source that support the claim, as recorded with the evidence. */
179
+ citation?: string;
180
+ /** Why the citation supports the claim, as stated by whoever recorded the evidence. */
181
+ justification?: string;
171
182
  /** Raw source-specific details that should not be flattened into prose. */
172
183
  metadata?: Readonly<Record<string, unknown>>;
173
184
  }>;
185
+ /**
186
+ * How much of a judgment's proof to explain.
187
+ */
188
+ type ExplanationOptions = Readonly<{
189
+ /**
190
+ * The most claims to explain. Without it, every claim of the proof is explained.
191
+ *
192
+ * Most of a proof is conditions that held and changed nothing, and a deep rule set has
193
+ * hundreds of them. Within a budget, the conclusion is always explained, then what
194
+ * decided it before what did not, and what is near it before what is far. A claim that
195
+ * rests on claims left out names them, with their values, in `notShown`.
196
+ */
197
+ budget?: number;
198
+ }>;
174
199
  /**
175
200
  * Convert a judgment's proof-support graph into a compact, LLM-readable
176
201
  * {@link Explanation} of what was concluded and why.
177
202
  *
178
- * The result is a directed acyclic graph (DAG): reused judgment objects share
179
- * explanation nodes within this call. Serializing the result as JSON still
180
- * expands shared nodes at every path to them.
203
+ * Without a budget, the result is a directed acyclic graph (DAG): reused judgment
204
+ * objects share explanation nodes within this call. Serializing it as JSON expands a
205
+ * shared node at every path to it, so the text of a deep proof grows with its paths, to
206
+ * megabytes. Give a `budget` to explain a judgment in text.
181
207
  *
182
208
  * @example
183
209
  * ```ts
@@ -187,10 +213,13 @@ type EvidenceReason = Readonly<{
187
213
  * ```json
188
214
  * {
189
215
  * "claim": "doctor(alice) = true",
190
- * "because": "doctor(P) iff and(passed_medical_school(P), has_active_license(P)), with P = alice",
191
- * "given": [{ "claim": "and(...) = true" }]
216
+ * "because": "doctor(P) iff and(passed_medical_school(P), has_active_license(P))",
217
+ * "given": [
218
+ * { "claim": "passed_medical_school(alice) = true" },
219
+ * { "claim": "has_active_license(alice) = true" }
220
+ * ]
192
221
  * }
193
222
  * ```
194
223
  */
195
- export declare function explainJudgment(judgment: Judgment): Explanation;
196
- export type { CompoundExplanation, DerivedExplanation, EvidenceExplanation, EvidenceReason, Explanation, ExplanationBase, TerminalExplanation };
224
+ export declare function explainJudgment(judgment: Judgment, options?: ExplanationOptions): Explanation;
225
+ export type { CompoundExplanation, DerivedExplanation, EvidenceExplanation, EvidenceReason, Explanation, ExplanationBase, ExplanationOptions, TerminalExplanation };
package/dist/explain.js CHANGED
@@ -1,12 +1,96 @@
1
- import { a as isVar, n as AssertionError, t as isFormula } from "./types-0ZvnLf2N.js";
1
+ import { n as AssertionError, o as isTemporary, s as isVar, t as isFormula } from "./types-DeYyybXd.js";
2
+ // ../core/src/explanation/relevance.ts
3
+ /**
4
+ * Which of the claims an explanation rests on did not decide its value.
5
+ *
6
+ * A false conjunction is false because of its false conditions; the true ones were
7
+ * evaluated and changed nothing. That is not part of what an explanation says, so it is
8
+ * kept beside the explanation rather than in it, for writing an explanation out within a
9
+ * budget: what decided a claim is worth a reader's attention before what did not.
10
+ */
11
+ const undecisive = /* @__PURE__ */ new WeakMap();
12
+ /** Record the claims under `explanation` that did not decide its value. */
13
+ function noteUndecisive(explanation, claims) {
14
+ const noted = new Set(claims);
15
+ if (noted.size > 0) undecisive.set(explanation, noted);
16
+ }
17
+ /** The claims under `explanation` that did not decide its value. None, if none were recorded. */
18
+ function undecisiveIn(explanation) {
19
+ return undecisive.get(explanation) ?? NONE;
20
+ }
21
+ const NONE = /* @__PURE__ */ new Set();
22
+ // ../core/src/explanation/budget.ts
23
+ /**
24
+ * The explanation cut down to its `budget` claims most worth a reader's attention, the
25
+ * conclusion first. A claim that rests on claims left out names them in `notShown`.
26
+ *
27
+ * A claim is as far from the conclusion as the fewest undecisive claims on the way to
28
+ * it, then as the number of claims on the way. So what decided the conclusion is kept
29
+ * before anything that did not, the conditions of the conclusion's own rule before those
30
+ * of a rule three levels down, and no claim is kept without the claim that rests on it.
31
+ *
32
+ * The budget counts claims as they are written out, so a claim that two others rest on
33
+ * counts under each: the result written as JSON has at most `budget` nodes.
34
+ */
35
+ function withinBudget(explanation, budget) {
36
+ const root = {
37
+ source: explanation,
38
+ children: /* @__PURE__ */ new Map()
39
+ };
40
+ const frontier = candidates(root, [0, 0]);
41
+ for (let kept = 1; kept < budget; kept++) {
42
+ const nearest = frontier.reduce((best, candidate) => best === void 0 || nearer(candidate.distance, best.distance) ? candidate : best, void 0);
43
+ if (nearest === void 0) break;
44
+ frontier.splice(frontier.indexOf(nearest), 1);
45
+ const child = {
46
+ source: nearest.source,
47
+ children: /* @__PURE__ */ new Map()
48
+ };
49
+ nearest.under.children.set(nearest.source, child);
50
+ frontier.push(...candidates(child, nearest.distance));
51
+ }
52
+ return write(root);
53
+ }
54
+ function nearer(a, b) {
55
+ return a[0] < b[0] || a[0] === b[0] && a[1] < b[1];
56
+ }
57
+ function candidates(kept, distance) {
58
+ const { source } = kept;
59
+ const undecisive = undecisiveIn(source);
60
+ return ("given" in source ? source.given : []).map((child) => ({
61
+ source: child,
62
+ under: kept,
63
+ distance: [distance[0] + (undecisive.has(child) ? 1 : 0), distance[1] + 1]
64
+ }));
65
+ }
66
+ function write(kept) {
67
+ const { source } = kept;
68
+ if (!("given" in source)) return source;
69
+ const given = [];
70
+ const notShown = [];
71
+ for (const child of source.given) {
72
+ const keptChild = kept.children.get(child);
73
+ if (keptChild === void 0) notShown.push(child.claim);
74
+ else given.push(write(keptChild));
75
+ }
76
+ return notShown.length === 0 ? {
77
+ ...source,
78
+ given
79
+ } : {
80
+ ...source,
81
+ given,
82
+ notShown: [...new Set(notShown)]
83
+ };
84
+ }
2
85
  // ../core/src/explanation/explain.ts
3
86
  /**
4
87
  * Convert a judgment's proof-support graph into a compact, LLM-readable
5
88
  * {@link Explanation} of what was concluded and why.
6
89
  *
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.
90
+ * Without a budget, the result is a directed acyclic graph (DAG): reused judgment
91
+ * objects share explanation nodes within this call. Serializing it as JSON expands a
92
+ * shared node at every path to it, so the text of a deep proof grows with its paths, to
93
+ * megabytes. Give a `budget` to explain a judgment in text.
10
94
  *
11
95
  * @example
12
96
  * ```ts
@@ -16,17 +100,22 @@ import { a as isVar, n as AssertionError, t as isFormula } from "./types-0ZvnLf2
16
100
  * ```json
17
101
  * {
18
102
  * "claim": "doctor(alice) = true",
19
- * "because": "doctor(P) iff and(passed_medical_school(P), has_active_license(P)), with P = alice",
20
- * "given": [{ "claim": "and(...) = true" }]
103
+ * "because": "doctor(P) iff and(passed_medical_school(P), has_active_license(P))",
104
+ * "given": [
105
+ * { "claim": "passed_medical_school(alice) = true" },
106
+ * { "claim": "has_active_license(alice) = true" }
107
+ * ]
21
108
  * }
22
109
  * ```
23
110
  */
24
- function explainJudgment(judgment) {
25
- return explainWithContext(judgment, {
111
+ function explainJudgment(judgment, options = {}) {
112
+ const explanation = explainWithContext(judgment, {
113
+ definitions: /* @__PURE__ */ new Map(),
26
114
  explanations: /* @__PURE__ */ new WeakMap(),
27
115
  contentKeys: /* @__PURE__ */ new WeakMap(),
28
116
  keyIds: /* @__PURE__ */ new Map()
29
117
  });
118
+ return options.budget === void 0 ? explanation : withinBudget(explanation, options.budget);
30
119
  }
31
120
  function explainWithContext(judgment, context) {
32
121
  let explanation = context.explanations.get(judgment);
@@ -40,40 +129,94 @@ function explainOnce(judgment, context) {
40
129
  if (isClosedWorldAbsence(judgment)) {
41
130
  const atom = judgment.goal.goal.atom;
42
131
  return {
43
- claim: claimForJudgment(judgment),
44
- because: `${formatAtom(atom)} is closed-world, and no evidence is known`
132
+ claim: claimForJudgment(judgment, context),
133
+ because: `${formatAtom(atom, scopeOf(judgment, context))} is closed-world, and no evidence is known`
45
134
  };
46
135
  }
47
136
  const supports = supportsForValue(judgment);
48
137
  const ruleSupport = supports.find((support) => support.source === "rule");
49
- if (ruleSupport !== void 0) return {
50
- claim: claimForJudgment(judgment),
51
- because: formatRuleApplication(ruleSupport.rule, ruleSupport.body.bindings),
52
- given: [explainWithContext(ruleSupport.body, context)]
53
- };
138
+ if (ruleSupport !== void 0) {
139
+ const body = explainWithContext(ruleSupport.body, context);
140
+ const explanation = {
141
+ claim: claimForJudgment(judgment, context),
142
+ because: formatRule(ruleSupport.rule),
143
+ given: ruleBodyExplanations(ruleSupport.body, body)
144
+ };
145
+ noteUndecisive(explanation, undecisiveIn(body));
146
+ return explanation;
147
+ }
54
148
  const evidence = supports.flatMap((support) => support.source === "evidence" ? support.literals : []);
55
149
  if (evidence.length > 0) {
56
- const reasons = evidence.map(evidenceReason);
150
+ const claim = claimForJudgment(judgment, context);
151
+ const reasons = evidence.map((literal) => evidenceReason(literal, claim));
57
152
  const firstReason = reasons[0];
58
153
  return {
59
- claim: claimForJudgment(judgment),
154
+ claim,
60
155
  because: reasons.length === 1 && firstReason !== void 0 ? firstReason : reasons
61
156
  };
62
157
  }
63
158
  const subgoals = supports.flatMap((support) => support.source === "subgoals" ? support.judgments : []);
64
- if (subgoals.length > 0) return {
65
- claim: claimForJudgment(judgment),
66
- given: uniqueExplanations(subgoals.map((subgoal) => explainWithContext(subgoal, context)), context)
67
- };
159
+ if (subgoals.length > 0) {
160
+ const explanation = {
161
+ claim: claimForJudgment(judgment, context),
162
+ given: uniqueExplanations(subgoals.map((subgoal) => explainWithContext(subgoal, context)), context)
163
+ };
164
+ const deciding = new Set(decidingSubgoals(judgment, subgoals).map((subgoal) => explainWithContext(subgoal, context)));
165
+ noteUndecisive(explanation, explanation.given.filter((given) => !deciding.has(given)));
166
+ return explanation;
167
+ }
68
168
  if (supports.find((support) => support.source === "closed-world-absence") !== void 0 && judgment.goal.kind === "atom") return {
69
- claim: claimForJudgment(judgment),
70
- because: `${formatAtom(judgment.goal.atom)} is closed-world, and no matching evidence is known`
169
+ claim: claimForJudgment(judgment, context),
170
+ because: `${formatAtom(judgment.goal.atom, scopeOf(judgment, context))} is closed-world, and no matching evidence is known`
171
+ };
172
+ const primitive = supports.find((support) => support.source === "primitive");
173
+ if (primitive !== void 0) return {
174
+ claim: claimForJudgment(judgment, context),
175
+ because: primitiveReason(primitive, scopeOf(judgment, context))
71
176
  };
72
177
  return {
73
- claim: claimForJudgment(judgment),
178
+ claim: claimForJudgment(judgment, context),
74
179
  because: "no supporting evidence or rule was found"
75
180
  };
76
181
  }
182
+ /**
183
+ * Why a comparison, arithmetic term or aggregate has its value. It is computed rather than
184
+ * supported by evidence or a rule, unless a variable it needs was never given a value.
185
+ */
186
+ function primitiveReason(support, scope) {
187
+ const uncertainty = support.source === "primitive" && "evaluation" in support ? support.evaluation.uncertainty ?? [] : [];
188
+ const missing = uncertainty.flatMap((reason) => reason.kind === "missing-binding" ? [formatValue(reason.variable, scope)] : []);
189
+ if (missing.length > 0) return `${[...new Set(missing)].join(", ")} ${missing.length === 1 ? "has" : "have"} no value`;
190
+ return uncertainty.length > 0 ? "computed from values that are not settled" : "computed";
191
+ }
192
+ /**
193
+ * What a rule application rests on. A body that is an `and` or an `or` is not a step of
194
+ * its own: the rule already states it, so the application rests directly on the body's
195
+ * subgoals. Any other body, such as a single atom or a negation, is the one thing it
196
+ * rests on.
197
+ */
198
+ function ruleBodyExplanations(body, explanation) {
199
+ return (body.goal.kind === "and" || body.goal.kind === "or") && "given" in explanation && !("because" in explanation) ? explanation.given : [explanation];
200
+ }
201
+ /**
202
+ * The subgoals that give a conjunction or disjunction its value. The rest were evaluated
203
+ * and changed nothing.
204
+ *
205
+ * A false `and` is decided by its false subgoals, all of them: each is something that
206
+ * would have to change. A true `or` is decided by one true subgoal, and the first will
207
+ * do. A true `and` and a false `or` need every subgoal. When the value is not settled,
208
+ * the subgoals that are not settled either way are what it waits on.
209
+ */
210
+ function decidingSubgoals(judgment, subgoals) {
211
+ const { kind } = judgment.goal;
212
+ if (kind !== "and" && kind !== "or") return subgoals;
213
+ const deciding = kind === "and" ? "false" : "true";
214
+ const neutral = kind === "and" ? "true" : "false";
215
+ if (judgment.value === neutral) return subgoals;
216
+ const decided = judgment.value === deciding ? subgoals.filter((subgoal) => subgoal.value === deciding) : subgoals.filter((subgoal) => subgoal.value !== neutral);
217
+ const chosen = kind === "or" && judgment.value === "true" ? decided.slice(0, 1) : decided;
218
+ return chosen.length > 0 ? chosen : subgoals;
219
+ }
77
220
  function uniqueExplanations(explanations, context) {
78
221
  const seen = /* @__PURE__ */ new Set();
79
222
  const unique = [];
@@ -125,21 +268,86 @@ function isClosedWorldAbsence(judgment) {
125
268
  if (child.value === "unknown") return true;
126
269
  return child.value === "false" && child.negative.some((childSupport) => childSupport.source === "closed-world-absence");
127
270
  }
128
- function claimForJudgment(judgment) {
129
- return `${formatGoal(judgment.goal)} = ${judgment.value}`;
271
+ const NO_SCOPE = {
272
+ values: /* @__PURE__ */ new Map(),
273
+ definitions: /* @__PURE__ */ new Map()
274
+ };
275
+ function scopeOf(judgment, context) {
276
+ noteDefinitions(judgment.goal, context.definitions);
277
+ return {
278
+ values: new Map(judgment.bindings),
279
+ definitions: context.definitions
280
+ };
281
+ }
282
+ /** Record the term that computes each temporary in the goal. A parent is always noted before its subgoals are explained. */
283
+ function noteDefinitions(goal, definitions) {
284
+ if ("bind" in goal && isTemporary(goal.bind)) definitions.set(goal.bind, goal);
285
+ if (goal.kind === "and" || goal.kind === "or") for (const subgoal of goal.goals) noteDefinitions(subgoal, definitions);
286
+ }
287
+ /**
288
+ * The goal as it was evaluated, and what it came to. Variables are written as the values
289
+ * they hold, and a variable without one keeps its name. A term that computes a value says
290
+ * the value it computed, such as `A = mul(1000, 0.15) = 150`; every other claim ends in
291
+ * its truth value. A temporary is never named: the term that computes it stands for it.
292
+ */
293
+ function claimForJudgment(judgment, context) {
294
+ const scope = scopeOf(judgment, context);
295
+ const { goal } = judgment;
296
+ if ("bind" in goal) {
297
+ const result = scope.values.has(goal.bind) && judgment.value === "true" ? formatValue(scope.values.get(goal.bind)) : judgment.value;
298
+ return `${formatComputation(goal, scope)} = ${result}`;
299
+ }
300
+ return `${formatGoal(goal, scope)} = ${judgment.value}`;
130
301
  }
131
- function evidenceReason(literal) {
302
+ /**
303
+ * A computing goal with the variable it binds left as a name, `A = mul(1000, 0.15)`, or
304
+ * with no name when it binds a temporary, `mul(1000, 0.15)`.
305
+ */
306
+ function formatComputation(goal, scope) {
307
+ const values = new Map(scope.values);
308
+ values.delete(goal.bind);
309
+ const term = formatTerm(goal, {
310
+ ...scope,
311
+ values
312
+ });
313
+ return isTemporary(goal.bind) ? term : `${goal.bind.name} = ${term}`;
314
+ }
315
+ /**
316
+ * A computing goal inside a larger one: the term and its value, `mul(1000, 0.15) = 150`,
317
+ * or with no value the variable it would bind, `A = mul(I, 0.15)`. A temporary with no
318
+ * value is written as its term wherever it is used, so here it is the term alone.
319
+ */
320
+ function formatComputing(goal, scope) {
321
+ const term = formatTerm(goal, scope);
322
+ if (scope.values.has(goal.bind)) return `${term} = ${formatValue(scope.values.get(goal.bind))}`;
323
+ return isTemporary(goal.bind) ? term : `${goal.bind.name} = ${term}`;
324
+ }
325
+ /** The computation itself, without the variable it binds: `mul(1000, 0.15)`. */
326
+ function formatTerm(goal, scope) {
327
+ if (goal.kind === "sum") return `sum(${formatValue(goal.value, scope)})`;
328
+ if (goal.kind === "count") return `count(${formatGoal(goal.where, scope)})`;
329
+ return `${goal.kind}(${formatValue(goal.left, scope)}, ${formatValue(goal.right, scope)})`;
330
+ }
331
+ /**
332
+ * What one piece of evidence says and where it comes from. Its claim is left out when it
333
+ * only repeats `stepClaim` beside provenance, so a step states its claim once.
334
+ */
335
+ function evidenceReason(literal, stepClaim) {
132
336
  const claim = `${formatAtom(literal.atom)} = ${literal.sign === "+" ? "true" : "false"}`;
133
- const source = literal.metadata?.source;
134
- return source === void 0 ? { claim } : {
337
+ const { source, citation, justification } = literal.metadata ?? {};
338
+ const provenance = {
339
+ ...source === void 0 ? {} : { source },
340
+ ...citation === void 0 ? {} : { citation },
341
+ ...justification === void 0 ? {} : { justification }
342
+ };
343
+ return claim === stepClaim && Object.keys(provenance).length > 0 ? provenance : {
135
344
  claim,
136
- source
345
+ ...provenance
137
346
  };
138
347
  }
139
- function formatRuleApplication(rule, bindings) {
140
- const base = `${formatLiteral(rule.head)} ${rule.kind} ${formatRuleBody(rule.body)}`;
141
- if (bindings.length === 0) return base;
142
- return `${base}, with ${bindings.map(([variable, value]) => `${variable.name} = ${formatValue(value)}`).join(", ")}`;
348
+ /** A rule as its author wrote it. The values it was applied to are in the claims it supports. */
349
+ function formatRule(rule) {
350
+ return `${formatLiteral(rule.head)} ${rule.kind} ${formatRuleBody(rule.body)}`;
143
351
  }
144
352
  function formatRuleBody(body) {
145
353
  if (isLiteral(body)) return formatLiteral(body);
@@ -150,44 +358,52 @@ function formatRuleBody(body) {
150
358
  function formatLiteral(literal) {
151
359
  return literal.sign === "-" ? `not(${formatAtom(literal.atom)})` : formatAtom(literal.atom);
152
360
  }
153
- function formatGoal(goal) {
361
+ function formatGoal(goal, scope = NO_SCOPE) {
154
362
  switch (goal.kind) {
155
- case "atom": return formatAtom(goal.atom);
156
- case "not": return `not(${formatGoal(goal.goal)})`;
363
+ case "atom": return formatAtom(goal.atom, scope);
364
+ case "not": return `not(${formatGoal(goal.goal, scope)})`;
157
365
  case "and":
158
- case "or": return `${goal.kind}(${goal.goals.map(formatGoal).join(", ")})`;
159
- case "exists": return `exists(${goal.variables.map((variable) => variable.name).join(", ")}: ${formatGoal(goal.goal)})`;
160
- case "let": return `let(${goal.variable.name} = ${formatValue(goal.value)}: ${formatGoal(goal.goal)})`;
366
+ case "or": return `${goal.kind}(${goal.goals.map((subgoal) => formatGoal(subgoal, scope)).join(", ")})`;
367
+ case "exists": return `exists(${goal.variables.map((variable) => variable.name).join(", ")}: ${formatGoal(goal.goal, scope)})`;
368
+ case "let": return `let(${goal.variable.name} = ${formatValue(goal.value, scope)}: ${formatGoal(goal.goal, scope)})`;
161
369
  case "add":
162
370
  case "sub":
163
371
  case "mul":
164
372
  case "div":
165
373
  case "daysBetween":
166
374
  case "yearsBetween":
375
+ case "firstOfMonth":
167
376
  case "min":
168
377
  case "max":
169
378
  case "roundTo":
170
379
  case "floorTo":
171
- case "ceilTo": return `${goal.bind.name} = ${goal.kind}(${formatValue(goal.left)}, ${formatValue(goal.right)})`;
380
+ case "ceilTo":
381
+ case "sum":
382
+ case "count": return formatComputing(goal, scope);
172
383
  case "eq":
173
384
  case "neq":
174
385
  case "lt":
175
386
  case "lte":
176
387
  case "gt":
177
- case "gte": return `${goal.kind}(${formatValue(goal.left)}, ${formatValue(goal.right)})`;
178
- case "sum": return `${goal.bind.name} = sum(${formatValue(goal.value)})`;
179
- case "count": return `${goal.bind.name} = count(${formatGoal(goal.where)})`;
388
+ case "gte": return `${goal.kind}(${formatValue(goal.left, scope)}, ${formatValue(goal.right, scope)})`;
180
389
  default: throw new AssertionError("Unhandled goal kind");
181
390
  }
182
391
  }
183
- function formatAtom(atom) {
184
- const values = atom.predicate.paramNames.map((name) => formatValue(atom.fields[name]));
185
- return `${atom.predicate.name}(${values.join(", ")})`;
392
+ function formatAtom(atom, scope = NO_SCOPE) {
393
+ const fields = atom.predicate.paramNames.map((name) => formatValue(atom.fields[name], scope));
394
+ return `${atom.predicate.name}(${fields.join(", ")})`;
186
395
  }
187
- function formatValue(value) {
188
- if (isVar(value)) return value.name;
396
+ function formatValue(value, scope = NO_SCOPE) {
397
+ if (isVar(value)) {
398
+ if (scope.values.has(value)) return formatValue(scope.values.get(value));
399
+ const definition = scope.definitions.get(value);
400
+ return definition === void 0 ? value.name : formatTerm(definition, scope);
401
+ }
189
402
  if (typeof value === "object" && value !== null && "id" in value && typeof value.id === "string") return value.id;
190
- if (value instanceof Date) return value.toISOString();
403
+ if (value instanceof Date) {
404
+ const text = value.toISOString();
405
+ return text.endsWith("T00:00:00.000Z") ? text.slice(0, 10) : text;
406
+ }
191
407
  return String(value);
192
408
  }
193
409
  function isLiteral(value) {
@@ -270,6 +270,8 @@ type DivGoal = ArithmeticGoal<"div">;
270
270
  type DaysBetweenGoal = ArithmeticGoal<"daysBetween">;
271
271
  /** Arithmetic goal binding whole anniversary years from the right date to the left. */
272
272
  type YearsBetweenGoal = ArithmeticGoal<"yearsBetween">;
273
+ /** Arithmetic goal binding the first day of the right month of the left year. */
274
+ type FirstOfMonthGoal = ArithmeticGoal<"firstOfMonth">;
273
275
  /** Arithmetic goal binding the lesser of two numeric expressions. */
274
276
  type MinGoal = ArithmeticGoal<"min">;
275
277
  /** Arithmetic goal binding the greater of two numeric expressions. */
@@ -281,7 +283,7 @@ type FloorToGoal = ArithmeticGoal<"floorTo">;
281
283
  /** Arithmetic goal rounding toward positive infinity at the right operand's decimal places. */
282
284
  type CeilToGoal = ArithmeticGoal<"ceilTo">;
283
285
  /** The primitive arithmetic operations. */
284
- type ArithmeticKind = "add" | "sub" | "mul" | "div" | "daysBetween" | "yearsBetween" | "min" | "max" | "roundTo" | "floorTo" | "ceilTo";
286
+ type ArithmeticKind = "add" | "sub" | "mul" | "div" | "daysBetween" | "yearsBetween" | "firstOfMonth" | "min" | "max" | "roundTo" | "floorTo" | "ceilTo";
285
287
  /** Bind the result of a primitive arithmetic operation. */
286
288
  type ArithmeticGoal<Kind extends ArithmeticKind> = Readonly<{
287
289
  readonly kind: Kind;
@@ -323,9 +325,9 @@ type CountGoal = Readonly<{
323
325
  readonly completeWhen?: Goal;
324
326
  }>;
325
327
  /** A normalized rule-body target that the solver/proof system can evaluate. */
326
- 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;
328
+ type Goal = AnyAtomGoal | NotGoal | AndGoal | OrGoal | ExistsGoal | LetGoal | AddGoal | SubGoal | MulGoal | DivGoal | DaysBetweenGoal | YearsBetweenGoal | FirstOfMonthGoal | MinGoal | MaxGoal | RoundToGoal | FloorToGoal | CeilToGoal | EqGoal | NeqGoal | LtGoal | LteGoal | GtGoal | GteGoal | SumGoal | CountGoal;
327
329
  /** A goal evaluated by recursively evaluating one or more child goals. */
328
330
  type CompoundGoal = NotGoal | AndGoal | OrGoal | ExistsGoal | LetGoal;
329
331
  /** A goal evaluated directly by a primitive operator. */
330
- type PrimitiveGoal = AddGoal | SubGoal | MulGoal | DivGoal | DaysBetweenGoal | YearsBetweenGoal | MinGoal | MaxGoal | RoundToGoal | FloorToGoal | CeilToGoal | EqGoal | NeqGoal | LtGoal | LteGoal | GtGoal | GteGoal | SumGoal | CountGoal;
331
- export { AnyLiteral as $, PrimitiveGoal as A, Formula as B, LteGoal as C, Var as Ct, NeqGoal as D, PredicateSchema as Dt, MulGoal as E, InferRuntime as Et, AggregateBuilder as F, RuleBodyArgs as G, LiteralArgs as H, ComparableTerm as I, Scalar as J, RuleBodyInput as K, DateTerm as L, SubGoal as M, SumGoal as N, NotGoal as O, RuntimeConstructor as Ot, YearsBetweenGoal as P, RuleKind as Q, EqualityTerm as R, LtGoal as S, Annotations as St, MinGoal as T, EnumType as Tt, LiteralFilter as U, FormulaArgumentInput as V, NumericTerm as W, AnyRule as X, Term as Y, Rule as Z, GoalExpression as _, PredicateBase as _t, ArithmeticKind as a, AnyAtom as at, GteGoal as b, PredicateWorld as bt, ComparisonGoal as c, GroundFields as ct, DaysBetweenGoal as d, assertAtomLike as dt, Literal as et, DivGoal as f, assertGroundAtom as ft, Goal as g, Predicate as gt, FloorToGoal as h, OpenWorldPredicate as ht, ArithmeticGoal as i, EvidenceMetadata as it, RoundToGoal as j, OrGoal as k, RuntimeType as kt, CompoundGoal as l, NonGroundAtom as lt, ExistsGoal as m, ClosedWorldPredicate as mt, AndGoal as n, NegativeLiteral as nt, AtomGoal as o, Atom as ot, EqGoal as p, fieldEquals as pt, RuleBuilder as q, AnyAtomGoal as r, PositiveLiteral as rt, CeilToGoal as s, GroundAtom as st, AddGoal as t, LiteralSign as tt, CountGoal as u, PatternFields as ut, GroundAtomGoal as v, PredicateDeclaration as vt, MaxGoal as w, Entity as wt, LetGoal as x, AnnotationValue as xt, GtGoal as y, PredicateMetadata as yt, ExistsArgs as z };
332
+ type PrimitiveGoal = AddGoal | SubGoal | MulGoal | DivGoal | DaysBetweenGoal | YearsBetweenGoal | FirstOfMonthGoal | MinGoal | MaxGoal | RoundToGoal | FloorToGoal | CeilToGoal | EqGoal | NeqGoal | LtGoal | LteGoal | GtGoal | GteGoal | SumGoal | CountGoal;
333
+ export { RuleKind as $, OrGoal as A, RuntimeType as At, ExistsArgs as B, LtGoal as C, Annotations as Ct, MulGoal as D, InferRuntime as Dt, MinGoal as E, EnumType as Et, YearsBetweenGoal as F, NumericTerm as G, FormulaArgumentInput as H, AggregateBuilder as I, RuleBuilder as J, RuleBodyArgs as K, ComparableTerm as L, RoundToGoal as M, SubGoal as N, NeqGoal as O, PredicateSchema as Ot, SumGoal as P, Rule as Q, DateTerm as R, LetGoal as S, AnnotationValue as St, MaxGoal as T, Entity as Tt, LiteralArgs as U, Formula as V, LiteralFilter as W, Term as X, Scalar as Y, AnyRule as Z, Goal as _, Predicate as _t, ArithmeticKind as a, EvidenceMetadata as at, GtGoal as b, PredicateMetadata as bt, ComparisonGoal as c, GroundAtom as ct, DaysBetweenGoal as d, PatternFields as dt, AnyLiteral as et, DivGoal as f, assertAtomLike as ft, FloorToGoal as g, OpenWorldPredicate as gt, FirstOfMonthGoal as h, ClosedWorldPredicate as ht, ArithmeticGoal as i, PositiveLiteral as it, PrimitiveGoal as j, NotGoal as k, RuntimeConstructor as kt, CompoundGoal as l, GroundFields as lt, ExistsGoal as m, fieldEquals as mt, AndGoal as n, LiteralSign as nt, AtomGoal as o, AnyAtom as ot, EqGoal as p, assertGroundAtom as pt, RuleBodyInput as q, AnyAtomGoal as r, NegativeLiteral as rt, CeilToGoal as s, Atom as st, AddGoal as t, Literal as tt, CountGoal as u, NonGroundAtom as ut, GoalExpression as v, PredicateBase as vt, LteGoal as w, Var as wt, GteGoal as x, PredicateWorld as xt, GroundAtomGoal as y, PredicateDeclaration as yt, EqualityTerm as z };
@@ -1,4 +1,4 @@
1
- import { $ as AnyLiteral, A as PrimitiveGoal, Ct as Var, Dt as PredicateSchema, X as AnyRule, Z as Rule, a as ArithmeticKind, et as Literal, g as Goal, l as CompoundGoal, o as AtomGoal, r as AnyAtomGoal, v as GroundAtomGoal } from "./goal-BP745uKh.js";
1
+ import { Ot as PredicateSchema, Q as Rule, Z as AnyRule, _ as Goal, a as ArithmeticKind, et as AnyLiteral, j as PrimitiveGoal, l as CompoundGoal, o as AtomGoal, r as AnyAtomGoal, tt as Literal, wt as Var, y as GroundAtomGoal } from "./goal-BBoSfPUN.js";
2
2
  /** Four-valued result for an evaluated proposition or goal. */
3
3
  type GoalValue = "false" | "unknown" | "conflicted" | "true";
4
4
  /** One variable assignment in scope for an evaluated goal. */
package/dist/kb.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { $ as AnyLiteral, Dt as PredicateSchema, Q as RuleKind, St as Annotations, U as LiteralFilter, X as AnyRule, Z as Rule, et as Literal, it as EvidenceMetadata, nt as NegativeLiteral, ot as Atom, q as RuleBuilder, rt as PositiveLiteral, st as GroundAtom, tt as LiteralSign, vt as PredicateDeclaration, xt as AnnotationValue } from "./goal-BP745uKh.js";
2
- import { n as KnowledgeBase, t as EntityRegistry } from "./knowledge-base-Da5HLCsY.js";
3
- import { _ as QueryAllJudgment, v as QueryJudgment } from "./judgment-Cr0JcUt5.js";
1
+ import { $ as RuleKind, Ct as Annotations, J as RuleBuilder, Ot as PredicateSchema, Q as Rule, St as AnnotationValue, W as LiteralFilter, Z as AnyRule, at as EvidenceMetadata, ct as GroundAtom, et as AnyLiteral, it as PositiveLiteral, nt as LiteralSign, rt as NegativeLiteral, st as Atom, tt as Literal, yt as PredicateDeclaration } from "./goal-BBoSfPUN.js";
2
+ import { n as KnowledgeBase, t as EntityRegistry } from "./knowledge-base-CkU_xzmB.js";
3
+ import { _ as QueryAllJudgment, v as QueryJudgment } from "./judgment-v8i3ks5N.js";
4
4
  /** Guarded knowledge-base operations that can be permitted independently. */
5
5
  type Operation = "assert" | "deny" | "query";
6
6
  /** An effective grant after blocks and duplicates have been removed. */
package/dist/kb.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as ExclusiveDefinitionProgrammerError } from "./programmer-errors-DDfy03Y0.js";
2
- import { n as queryAll, o as isAtomLike, t as query } from "./query-Dxm7EpxZ.js";
1
+ import { t as ExclusiveDefinitionProgrammerError } from "./programmer-errors-DLE1xWJ_.js";
2
+ import { n as queryAll, o as isAtomLike, t as query } from "./query-CN6EOPwp.js";
3
3
  // ../core/src/knowledge-base/assertions/exclusive-definitions.ts
4
4
  /** Assert that each predicate has either iff definition semantics or if rule-disjunction semantics, not both. */
5
5
  function assertExclusiveDefinitions(rules) {
@@ -1,4 +1,4 @@
1
- import { $ as AnyLiteral, Dt as PredicateSchema, H as LiteralArgs, U as LiteralFilter, X as AnyRule, it as EvidenceMetadata, kt as RuntimeType, nt as NegativeLiteral, ot as Atom, q as RuleBuilder, rt as PositiveLiteral, vt as PredicateDeclaration, wt as Entity } from "./goal-BP745uKh.js";
1
+ import { At as RuntimeType, J as RuleBuilder, Ot as PredicateSchema, Tt as Entity, U as LiteralArgs, W as LiteralFilter, Z as AnyRule, at as EvidenceMetadata, et as AnyLiteral, it as PositiveLiteral, rt as NegativeLiteral, st as Atom, yt as PredicateDeclaration } from "./goal-BBoSfPUN.js";
2
2
  /** Registry of known domain entities, keyed by constructor and id. */
3
3
  declare class EntityRegistry {
4
4
  private readonly entitiesByType;
@@ -15,13 +15,19 @@ var PrimitiveArithmeticProgrammerError = class extends ProgrammerError {
15
15
  /** The arithmetic goal being evaluated when the failure occurred. */
16
16
  goal;
17
17
  constructor(operation, operands, goal) {
18
- super(`${operation} expected ${operation === "daysBetween" || operation === "yearsBetween" ? "date" : "numeric"} operands${operation === "roundTo" || operation === "floorTo" || operation === "ceilTo" ? " and a whole number of decimal places" : ""}`);
18
+ super(`${operation} expected ${expectedOperands(operation)}`);
19
19
  this.name = "PrimitiveArithmeticProgrammerError";
20
20
  this.operation = operation;
21
21
  this.operands = operands;
22
22
  this.goal = goal;
23
23
  }
24
24
  };
25
+ function expectedOperands(operation) {
26
+ if (operation === "daysBetween" || operation === "yearsBetween") return "date operands";
27
+ if (operation === "firstOfMonth") return "a whole year and a whole month from 1 to 12";
28
+ if (operation === "roundTo" || operation === "floorTo" || operation === "ceilTo") return "numeric operands and a whole number of decimal places";
29
+ return "numeric operands";
30
+ }
25
31
  /** Rule body uses variables before any constructive source can bind them. */
26
32
  var UnsafeUnboundVariableProgrammerError = class extends ProgrammerError {
27
33
  /** The variables used before any constructive source could bind them. */
@@ -1,5 +1,5 @@
1
- import { a as isVar, i as Var, n as AssertionError, r as assert, t as isFormula } from "./types-0ZvnLf2N.js";
2
- import { a as UnsafeUnboundVariableProgrammerError, i as UnconstructiveRecursionProgrammerError, n as PrimitiveArithmeticProgrammerError } from "./programmer-errors-DDfy03Y0.js";
1
+ import { a as Var, i as TEMPORARY_PREFIX, n as AssertionError, o as isTemporary, r as assert, s as isVar, t as isFormula } from "./types-DeYyybXd.js";
2
+ import { a as UnsafeUnboundVariableProgrammerError, i as UnconstructiveRecursionProgrammerError, n as PrimitiveArithmeticProgrammerError } from "./programmer-errors-DLE1xWJ_.js";
3
3
  // ../core/src/types/atom.ts
4
4
  /** Return whether an atom has no logic variables in its fields. */
5
5
  function isGroundAtom(atom) {
@@ -85,6 +85,16 @@ function yearsBetween(later, earlier) {
85
85
  const years = later.getUTCFullYear() - earlier.getUTCFullYear();
86
86
  return later.getUTCMonth() * 100 + later.getUTCDate() < earlier.getUTCMonth() * 100 + earlier.getUTCDate() ? years - 1 : years;
87
87
  }
88
+ /**
89
+ * The first day of a month at UTC midnight, or undefined unless the year is a whole
90
+ * number a date can hold and the month a whole number from 1 to 12.
91
+ */
92
+ function firstOfMonth(year, month) {
93
+ if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) return;
94
+ const day = /* @__PURE__ */ new Date(0);
95
+ day.setUTCFullYear(year, month - 1, 1);
96
+ return Number.isNaN(day.getTime()) ? void 0 : day;
97
+ }
88
98
  // ../core/src/types/goal.ts
89
99
  /** Shared runtime classification for arithmetic formulas. */
90
100
  const ARITHMETIC_KINDS = [
@@ -94,6 +104,7 @@ const ARITHMETIC_KINDS = [
94
104
  "div",
95
105
  "daysBetween",
96
106
  "yearsBetween",
107
+ "firstOfMonth",
97
108
  "min",
98
109
  "max",
99
110
  "roundTo",
@@ -1359,7 +1370,7 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
1359
1370
  }
1360
1371
  }
1361
1372
  const existing = valueForVariable(bindings, variable);
1362
- const target = existing === UNBOUND ? variable : new Var(`$tmp${context.nextTemp++}`);
1373
+ const target = existing === UNBOUND ? variable : new Var(`${TEMPORARY_PREFIX}${context.nextTemp++}`);
1363
1374
  const nextBindings = new Map(bindings);
1364
1375
  const values = rows.map((row) => resolveAggregateValue(valueExpression, row.bindings));
1365
1376
  let value;
@@ -1650,6 +1661,11 @@ function calculateValue(kind, left, right, goal) {
1650
1661
  case "div": return left / right;
1651
1662
  case "daysBetween":
1652
1663
  case "yearsBetween": break;
1664
+ case "firstOfMonth": {
1665
+ const day = firstOfMonth(left, right);
1666
+ if (day === void 0) throw new PrimitiveArithmeticProgrammerError(kind, [left, right], goal);
1667
+ return day;
1668
+ }
1653
1669
  case "min": return Math.min(left, right);
1654
1670
  case "max": return Math.max(left, right);
1655
1671
  case "roundTo":
@@ -1883,16 +1899,11 @@ function bindingsArray(bindings) {
1883
1899
  return [...bindings.entries()];
1884
1900
  }
1885
1901
  /**
1886
- * The temporaries arithmetic introduces for its intermediate results. They record the
1887
- * computation in the proof tree but are not rule variables, so two environments that
1902
+ * Whether the result binds a rule variable the environment does not. Temporaries record
1903
+ * the computation in the proof tree but are not rule variables, so two environments that
1888
1904
  * differ only in temporaries are the same environment, and a result that adds only
1889
1905
  * temporaries binds nothing beyond the environment it was evaluated in.
1890
1906
  */
1891
- const TEMPORARY_PREFIX = "$tmp";
1892
- function isTemporary(variable) {
1893
- return variable.name.startsWith(TEMPORARY_PREFIX);
1894
- }
1895
- /** Whether the result binds a rule variable the environment does not. */
1896
1907
  function bindsBeyond(result, environment) {
1897
1908
  for (const variable of result.keys()) if (!isTemporary(variable) && !environment.has(variable)) return true;
1898
1909
  return false;
@@ -18,6 +18,15 @@ var Var = class {
18
18
  function isVar(value) {
19
19
  return value instanceof Var;
20
20
  }
21
+ /**
22
+ * The prefix of the variables the solver introduces for the intermediate results of
23
+ * arithmetic. No rule names them, so they are not written into explanations.
24
+ */
25
+ const TEMPORARY_PREFIX = "$tmp";
26
+ /** Return whether the solver introduced the variable for an intermediate result. */
27
+ function isTemporary(variable) {
28
+ return variable.name.startsWith(TEMPORARY_PREFIX);
29
+ }
21
30
  // ../core/src/utils/assert.ts
22
31
  var AssertionError = class extends Error {
23
32
  constructor(message = "Assertion failed") {
@@ -34,4 +43,4 @@ function assert(condition, message = "Assertion failed") {
34
43
  function isFormula(value, kind) {
35
44
  return typeof value === "object" && value !== null && "kind" in value && "args" in value && (kind === void 0 || value.kind === kind) && Array.isArray(value.args);
36
45
  }
37
- export { isVar as a, Var as i, AssertionError as n, assert as r, isFormula as t };
46
+ export { Var as a, TEMPORARY_PREFIX as i, AssertionError as n, isTemporary as o, assert as r, isVar as s, isFormula as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factbag",
3
- "version": "0.0.0-alpha.5",
3
+ "version": "0.0.0-alpha.6",
4
4
  "keywords": [
5
5
  "logic",
6
6
  "rules-engine",