@suss/datalog 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,130 @@ you re-run with negated rules, the engine throws away what the previous
76
76
  pass derived and works the answer out again from the base facts. Either
77
77
  way, the database ends up with the answer for the facts it has now.
78
78
 
79
+ ## Tagging derivations
80
+
81
+ A fact is normally there or not there. Pass `evaluate` a `TagAlgebra`
82
+ and every derived fact also gets a tag: `combine` turns the body's
83
+ tags into the head's, and `merge` decides what to keep when the same
84
+ fact is derived a second time. This is a provenance semiring in the
85
+ sense of Green, Karvounarakis, and Tannen (2007); Soufflé's provenance
86
+ mode stores a minimal proof height with the same two operations. Rules
87
+ never mention tags, and a call without an algebra runs as before.
88
+
89
+ ```ts
90
+ const cost: TagAlgebra<number> = {
91
+ asserted: 0, // what an untagged fact contributes
92
+ absent: 0, // what a matched negated literal contributes
93
+ combine: (tags) => tags.reduce((sum, tag) => sum + tag, 1),
94
+ merge: (stored, incoming) => (incoming < stored ? incoming : stored),
95
+ };
96
+
97
+ evaluate(db, rules, cost);
98
+ db.tagOf("reaches", ["a", "c"]); // the cheapest derivation's cost
99
+ ```
100
+
101
+ `combine` is also handed the derivation itself: the `Rule` that fired,
102
+ and one entry per body literal saying which fact it matched, or which
103
+ grounded tuple a negated literal checked and did not find. An algebra
104
+ that only folds tags, like the cost above, ignores it; the witness
105
+ algebra below is built out of it.
106
+
107
+ Tags are stored beside the relation, keyed the same way as the facts,
108
+ never in the tuple. A tuple is its own identity: were the tag part of it, an
109
+ improved tag would read as a new fact and the fixpoint would never
110
+ close. `Database.add` reports "added", "improved", or "unchanged", and
111
+ an improved fact re-enters the delta so conclusions built on it
112
+ recompute with the better tag.
113
+
114
+ The contract on `merge`:
115
+
116
+ - Return the stored tag itself, the same value by `===`, when the new
117
+ derivation does not improve on it. Anything else is stored and
118
+ re-derives downstream.
119
+ - For recursive rule sets, merge must be a bounded meet: repeated
120
+ merges must stop improving after finitely many steps, the way `min`
121
+ over numbers does. The engine does not check this; an algebra that
122
+ keeps improving keeps evaluating.
123
+ - `undefined` is not a valid tag. The store uses it to mean "no tag",
124
+ and `tagOf` returns it for untagged facts.
125
+
126
+ Two caveats. Supply the same algebra on every evaluation of a rule set
127
+ over a database: a resumed run derives only from facts added since, so
128
+ it tags only what those reach. And rules rewritten by `deriveOnDemand`
129
+ refuse an algebra: tags would follow the demand-transformed rules
130
+ rather than the ones you wrote, so `evaluate` throws instead.
131
+
132
+ ## Witnesses, and proofs on demand
133
+
134
+ The `witnesses` algebra keeps one derivation per derived fact: the rule
135
+ that fired and one entry per body literal. Its merge keeps whatever is
136
+ already stored, so no fact ever re-enters the delta, the fixpoint
137
+ behaves exactly as it does untagged, and the tag stays one small object
138
+ per fact. `proofOf` then rebuilds the tree for one fact by walking
139
+ those witnesses backward, without re-running any rule:
140
+
141
+ ```ts
142
+ import { evaluate, proofOf, ruleLabel, witnesses } from "@suss/datalog";
143
+
144
+ evaluate(db, rules, witnesses);
145
+ const proof = proofOf(db, "reachable", ["util"]);
146
+ // {
147
+ // kind: "derived",
148
+ // rule: <the Rule that fired>,
149
+ // premises: [
150
+ // { kind: "derived", ... }, // walked further down
151
+ // { kind: "fact", relation: "calls", tuple: ["helper", "util"] },
152
+ // ],
153
+ // }
154
+ ```
155
+
156
+ A proof node is one of four kinds. `derived` has the rule and one
157
+ premise per body literal, in rule-body order. `fact` is a leaf with no
158
+ witness: the caller asserted it, or it was derived without the
159
+ algebra. `absence` is a leaf for a tuple missing from the database, at
160
+ the root when you ask about a fact that was never derived, and under a
161
+ derivation where a negated literal relied on the tuple being missing;
162
+ that absence is recorded at derivation time, so a proof can say "and no
163
+ handles(x) existed" without checking anything again. `truncated` is
164
+ where the walk stopped, at the depth cap (`maxDepth`, 128 unless you
165
+ say otherwise) or at a cycle.
166
+
167
+ First-wins gives you *a* proof, not the shortest one. When nine
168
+ derivations reach the same fact, the one stored is the one evaluation
169
+ found first. A different merge, say keeping the witness with the
170
+ smaller proof height, fits the same algebra interface without touching
171
+ the walk.
172
+
173
+ A proof shows each rule as `ruleLabel` renders it: the `name` you
174
+ passed as `rule`'s fourth argument, or the head and body relations, as
175
+ in `path :- path, !blocked`. The witness keeps the `Rule` object
176
+ itself, so two rules that render alike stay distinct.
177
+
178
+ ## Confidence levels
179
+
180
+ The `confidence` algebra propagates how sure the run is of each fact,
181
+ as one of `"high"`, `"medium"`, or `"low"`. A rule firing takes the
182
+ minimum level across its body, so a conclusion is only as sure as its
183
+ weakest premise, and a fact derived twice keeps the better level. An
184
+ asserted fact without a tag counts as high; to say less, assert it
185
+ with a level:
186
+
187
+ ```ts
188
+ import { confidence, evaluate } from "@suss/datalog";
189
+
190
+ db.add("edge", ["a", "b"]); // high: read from source
191
+ db.add("edge", ["b", "c"], "medium"); // a guess somebody should check
192
+ evaluate(db, rules, confidence);
193
+ db.tagOf("path", ["a", "c"]); // "medium"
194
+ ```
195
+
196
+ A rule can be a heuristic itself, and then its conclusions should not
197
+ outrank it. `confidenceWith` takes a level per rule and folds it into
198
+ the minimum; rules the callback returns `undefined` for count as
199
+ exact. Both operations are idempotent, so ten medium steps come out
200
+ medium, and a matched negation counts as high, because negation here
201
+ is exact over the database as computed.
202
+
79
203
  ## Deriving only what somebody asked for
80
204
 
81
205
  A rule set written for a whole program derives every conclusion its