@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 +124 -0
- package/dist/index.cjs +395 -156
- package/dist/index.d.cts +192 -5
- package/dist/index.d.ts +192 -5
- package/dist/index.js +388 -155
- package/package.json +1 -1
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
|