factbag 0.0.0-alpha.0 → 0.0.0-alpha.2
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 +20 -0
- package/README.md +282 -20
- package/dist/core/{chunk-4IP7CXNV.js → chunk-5J3WGMN2.js} +1 -1
- package/dist/core/dsl/assert-supported-value.d.ts +2 -0
- package/dist/core/dsl/index.js +26 -1
- package/dist/core/dsl/operators/aggregate.d.ts +1 -1
- package/dist/core/dsl/operators/arithmetic.d.ts +4 -4
- package/dist/core/dsl/types.d.ts +3 -3
- package/dist/core/explanation/index.js +1 -1
- package/dist/core/knowledge-base/index.js +1 -1
- package/dist/core/query/index.js +182 -83
- package/dist/core/solver/query-validation/utils.d.ts +2 -0
- package/dist/core/types/goal.d.ts +1 -1
- package/dist/core/types/judgment.d.ts +2 -2
- package/dist/core/types/schema.d.ts +3 -3
- package/dist/jev/{chunk-2UXHML67.js → chunk-PISXBJOT.js} +2 -2
- package/dist/jev/classifier.js +2 -2
- package/dist/jev/resolve.js +1 -1
- package/package.json +2 -2
- /package/dist/core/{chunk-YF4PP42K.js → chunk-A6ZZJWIO.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# factbag
|
|
2
2
|
|
|
3
|
+
## 0.0.0-alpha.2
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- e212a94: Remove bigint support from predicate schemas, numeric expressions, aggregates, and numeric proof ranges. Numeric values now use JavaScript number exclusively, and an empty sum returns 0. The DSL rejects BigInt schemas and bigint operands rather than coercing them.
|
|
8
|
+
|
|
9
|
+
Migrate BigInt fields to Number and bigint literals to numbers. Division now uses number semantics (fractional results rather than bigint truncation); values outside the safe integer range can lose precision. Date support is unchanged.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- bc114ed: Add npm alpha version, CI status, and license badges to the README.
|
|
14
|
+
- a3583ab: Publish the full project README on npm, including the document-to-conclusion walkthrough.
|
|
15
|
+
|
|
16
|
+
## 0.0.0-alpha.1
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- ebc4a8a: A `letIn` or aggregate whose variable is already bound now constrains that variable instead of rebinding it. Before, a rule such as `tax(P, R) iff letIn(R, ...)` concluded `tax(alice, 99)` as true when its body computed 98, and an `or` branch evaluated under another branch's bindings reported those bindings as true. Proofs for such queries now carry a temporary variable and an `eq` comparison.
|
|
21
|
+
- eca65c2: An `or` is now judged per binding environment, so a branch that fails without binding a variable can no longer borrow the truth of a sibling branch whose bindings were discarded. Before, `and(or(p(X), r()), s(X))` could be judged true when `p(x1)` and `s(x3)` held but nothing satisfied both: the false `r()` branch produced an environment without `X`, the `or` was judged true there via `p(x1)`, and `s(X)` then ranged over every candidate. The recursion validator's edge collector also now carries variables bound in every `or` branch forward, matching the variable-use checker, so a comparison after such an `or` is no longer reported as unconstructive recursion.
|
|
22
|
+
|
|
3
23
|
## 0.0.0-alpha.0
|
|
4
24
|
|
|
5
25
|
Initial experimental distribution of core, corpus, guards, and Jev functionality.
|
package/README.md
CHANGED
|
@@ -1,44 +1,306 @@
|
|
|
1
1
|
# Factbag
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/factbag)
|
|
4
|
+
[](https://github.com/factbag/factbag/actions/workflows/ci.yml?query=branch%3Amain)
|
|
5
|
+
[](https://github.com/factbag/factbag/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
TypeSafe AI’s Jev model.
|
|
7
|
+
<img src="https://raw.githubusercontent.com/factbag/factbag/main/docs/assets/factbag.png" alt="An open canvas bag" align="right" width="180" />
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Turn documents into evidence and conclusions with [Jev](https://typesafe.ai/) and TypeScript.
|
|
10
|
+
|
|
11
|
+
Factbag uses TypeSafe AI’s Jev model to read passages and assess specific claims.
|
|
12
|
+
Those assessments become evidence in a knowledge base, alongside facts from your
|
|
13
|
+
application. You write the rules that connect them and query what follows.
|
|
14
|
+
|
|
15
|
+
Each piece of documentary evidence points back to its source passage. Conclusions
|
|
16
|
+
can be **true**, **false**, **conflicted**, or **unknown**. When documents disagree,
|
|
17
|
+
you can inspect the conflicting evidence, correct a particular fact, and query
|
|
18
|
+
the rules again.
|
|
19
|
+
|
|
20
|
+
> **Important:** Factbag is an early alpha release and still pretty rough - APIs will definitely change.
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
1. **Define the claims** you want Jev to assess, with descriptions of what counts
|
|
25
|
+
as evidence.
|
|
26
|
+
2. **Add documents** to a corpus and choose which predicates Jev may assert or deny.
|
|
27
|
+
3. **Execute classification jobs.** Jev assesses whether each passage supports,
|
|
28
|
+
contradicts, or is irrelevant to each candidate claim.
|
|
29
|
+
4. **Query the result.** Factbag combines admitted evidence with your existing
|
|
30
|
+
facts and rules, retaining the sources behind the conclusion.
|
|
13
31
|
|
|
14
32
|
## Install
|
|
15
33
|
|
|
16
34
|
```sh
|
|
17
|
-
npm install factbag
|
|
35
|
+
npm install factbag@alpha @typesafe-ai/sdk@^0.6.0
|
|
18
36
|
```
|
|
19
37
|
|
|
20
38
|
ESM only. Requires Node 22.18+ on the 22.x line, or Node 24 or 26.
|
|
21
|
-
This is an early prerelease; APIs may change.
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
Set your TypeSafe API key in the environment:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
export TYPESAFE_API_KEY="your-api-key"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## From a document to a conclusion
|
|
47
|
+
|
|
48
|
+
### 1. Decide on your ruleset
|
|
49
|
+
|
|
50
|
+
Start with the criteria your application needs to apply. These might be your own
|
|
51
|
+
business rules or requirements set out in legislation or regulations.
|
|
52
|
+
|
|
53
|
+
Here, we're translating the individual-income route in the SEC's
|
|
54
|
+
[accredited-investor definition, 17 CFR § 230.501(a)(6)](<https://www.ecfr.gov/current/title-17/chapter-II/part-230/section-230.501#p-230.501(a)(6)>),
|
|
55
|
+
into three conditions:
|
|
56
|
+
|
|
57
|
+
- Individual income exceeded $200,000 in the first of the two most recent years.
|
|
58
|
+
- Individual income exceeded $200,000 in the second year.
|
|
59
|
+
- The person reasonably expects to reach the same income level in the current year.
|
|
60
|
+
|
|
61
|
+
### 2. Define the claims
|
|
62
|
+
|
|
63
|
+
Declare the facts we'll combine, and describe the claims Jev will assess.
|
|
24
64
|
|
|
25
65
|
```ts
|
|
26
|
-
import { predicate, variable } from "factbag/dsl";
|
|
66
|
+
import { and, enumeration, predicate, variable } from "factbag/dsl";
|
|
27
67
|
import { KnowledgeBase } from "factbag/kb";
|
|
28
68
|
import { query } from "factbag/query";
|
|
69
|
+
import { Corpus } from "factbag/corpus";
|
|
70
|
+
import { guard } from "factbag/guards";
|
|
71
|
+
import { classifier } from "factbag/jev/classifier";
|
|
72
|
+
|
|
73
|
+
class Person {
|
|
74
|
+
constructor(
|
|
75
|
+
readonly id: string,
|
|
76
|
+
readonly name: string,
|
|
77
|
+
) {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hasIncomeOver200k = predicate(
|
|
81
|
+
"has_income_over_200k",
|
|
82
|
+
{ person: Person, year: enumeration("2024", "2025") },
|
|
83
|
+
{
|
|
84
|
+
description:
|
|
85
|
+
"The person's total individual annual income exceeded USD 200,000 in the " +
|
|
86
|
+
"specified year. Joint household income and forecasts do not count.",
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const expectsSameIncome = predicate(
|
|
91
|
+
"expects_same_income",
|
|
92
|
+
{ person: Person },
|
|
93
|
+
{
|
|
94
|
+
description:
|
|
95
|
+
"The person reasonably expects their individual income in 2026 to remain " +
|
|
96
|
+
"at least at the level earned in each of 2024 and 2025.",
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const isQualifiedInvestor = predicate("is_qualified_investor", {
|
|
101
|
+
person: Person,
|
|
102
|
+
});
|
|
103
|
+
const hasVerifiedIdentity = predicate("has_verified_identity", {
|
|
104
|
+
person: Person,
|
|
105
|
+
});
|
|
106
|
+
const isReadyForReview = predicate("is_ready_for_review", { person: Person });
|
|
107
|
+
```
|
|
29
108
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
109
|
+
### 3. Encode the ruleset
|
|
110
|
+
|
|
111
|
+
All three income conditions must hold for this route to qualification. Our
|
|
112
|
+
application also requires verified identity before sending an investor for review.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const person = variable("person");
|
|
33
116
|
const kb = new KnowledgeBase();
|
|
34
117
|
|
|
35
|
-
kb.rule(
|
|
36
|
-
|
|
118
|
+
kb.rule(isQualifiedInvestor({ person })).if(
|
|
119
|
+
and(
|
|
120
|
+
hasIncomeOver200k({ person, year: "2024" }),
|
|
121
|
+
hasIncomeOver200k({ person, year: "2025" }),
|
|
122
|
+
expectsSameIncome({ person }),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
37
125
|
|
|
38
|
-
|
|
126
|
+
kb.rule(isReadyForReview({ person })).if(
|
|
127
|
+
and(isQualifiedInvestor({ person }), hasVerifiedIdentity({ person })),
|
|
128
|
+
);
|
|
39
129
|
```
|
|
40
130
|
|
|
41
|
-
|
|
42
|
-
|
|
131
|
+
### 4. Import a known fact and add documents
|
|
132
|
+
|
|
133
|
+
Identity verification comes directly from our onboarding system. Income evidence
|
|
134
|
+
goes into the corpus for Jev to assess.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const alice = new Person("alice", "Alice");
|
|
138
|
+
kb.entities.add(alice);
|
|
139
|
+
|
|
140
|
+
kb.asserts(hasVerifiedIdentity({ person: alice }), {
|
|
141
|
+
source: "onboarding:alice",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const corpus = new Corpus();
|
|
145
|
+
|
|
146
|
+
// Tax returns provide evidence of past annual income.
|
|
147
|
+
corpus.add({ documentId: "alice-2024-federal-tax-return", text: "…" });
|
|
148
|
+
corpus.add({ documentId: "alice-2025-federal-tax-return", text: "…" });
|
|
149
|
+
|
|
150
|
+
// An employer letter supports the expectation of continued income.
|
|
151
|
+
corpus.add({
|
|
152
|
+
documentId: "alice-2026-employment-confirmation-letter",
|
|
153
|
+
text: "…",
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 5. Set Jev's permissions
|
|
158
|
+
|
|
159
|
+
The guard starts with no permissions. Permit Jev to assert or deny the two income
|
|
160
|
+
predicates.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const incomeEvidence = guard(kb)
|
|
164
|
+
.permit("assert", hasIncomeOver200k)
|
|
165
|
+
.permit("deny", hasIncomeOver200k)
|
|
166
|
+
.permit("assert", expectsSameIncome)
|
|
167
|
+
.permit("deny", expectsSameIncome);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 6. Ask Jev to assess the income evidence
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const run = classifier(corpus, incomeEvidence);
|
|
174
|
+
|
|
175
|
+
for (const job of run) {
|
|
176
|
+
await job.execute(); // Makes a live Jev request.
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 7. Query the conclusion
|
|
181
|
+
|
|
182
|
+
The output knowledge base combines the imported fact and rules with the admitted
|
|
183
|
+
evidence.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const qualification = query(run.kb, isQualifiedInvestor({ person: alice }));
|
|
187
|
+
const reviewReadiness = query(run.kb, isReadyForReview({ person: alice }));
|
|
188
|
+
|
|
189
|
+
// Both are "true" if Jev admits supporting evidence for all three income claims.
|
|
190
|
+
qualification.value; // "true"
|
|
191
|
+
reviewReadiness.value; // "true"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Results can be `true`, `false`, `conflicted`, or
|
|
195
|
+
`unknown`, depending on the evidence admitted.
|
|
196
|
+
|
|
197
|
+
### 8. Inspect the proof tree
|
|
198
|
+
|
|
199
|
+
Use `explainJudgment()` to follow a conclusion through its rule to the supporting
|
|
200
|
+
evidence and source passages.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { explainJudgment } from "factbag/explain";
|
|
204
|
+
|
|
205
|
+
const proof = explainJudgment(qualification);
|
|
206
|
+
JSON.stringify(proof, null, 2); // JSON shown below.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
An abbreviated example with supporting evidence for all three claims:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"claim": "is_qualified_investor(alice) = true",
|
|
214
|
+
"because": "is_qualified_investor(person) if and(...), with person = alice",
|
|
215
|
+
"given": [
|
|
216
|
+
{
|
|
217
|
+
"claim": "and(...) = true",
|
|
218
|
+
"given": [
|
|
219
|
+
{
|
|
220
|
+
"claim": "has_income_over_200k(alice, 2024) = true",
|
|
221
|
+
"because": {
|
|
222
|
+
"claim": "has_income_over_200k(alice, 2024) = true",
|
|
223
|
+
"source": "alice-2024-federal-tax-return#0-1"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"claim": "has_income_over_200k(alice, 2025) = true",
|
|
228
|
+
"because": {
|
|
229
|
+
"claim": "has_income_over_200k(alice, 2025) = true",
|
|
230
|
+
"source": "alice-2025-federal-tax-return#0-1"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"claim": "expects_same_income(alice) = true",
|
|
235
|
+
"because": {
|
|
236
|
+
"claim": "expects_same_income(alice) = true",
|
|
237
|
+
"source": "alice-2026-employment-confirmation-letter#0-1"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Source IDs identify document passages; their ranges depend on the document text.
|
|
247
|
+
Use `corpus.resolve(source)` to retrieve the passage behind a piece of evidence.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const source = "alice-2024-federal-tax-return#0-1";
|
|
251
|
+
corpus.resolve(source); // Object shown below.
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The result includes the exact passage and its containing document:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"chunk": {
|
|
259
|
+
"id": "alice-2024-federal-tax-return#0-1",
|
|
260
|
+
"documentId": "alice-2024-federal-tax-return",
|
|
261
|
+
"range": { "start": 0, "end": 1 },
|
|
262
|
+
"text": "…"
|
|
263
|
+
},
|
|
264
|
+
"document": {
|
|
265
|
+
"documentId": "alice-2024-federal-tax-return",
|
|
266
|
+
"text": "…",
|
|
267
|
+
"metadata": {}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 9. Review conflicting evidence (optional)
|
|
273
|
+
|
|
274
|
+
Suppose an original tax return puts Alice below the income threshold, but an
|
|
275
|
+
amended return puts her above it. Jev admits both accounts, leaving qualification
|
|
276
|
+
`conflicted`. In this example, the other income conditions have supporting evidence.
|
|
277
|
+
|
|
278
|
+
You could pass the conflicting evidence to a human for review, or ask Jev to
|
|
279
|
+
compare the sources using the `resolver`.
|
|
280
|
+
|
|
281
|
+
Evidence judged superseded is retracted from the resolver's output
|
|
282
|
+
knowledge base.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { resolver } from "factbag/jev/resolve";
|
|
286
|
+
|
|
287
|
+
query(run.kb, isQualifiedInvestor({ person: alice })).value; // "conflicted"
|
|
288
|
+
|
|
289
|
+
const review = resolver(corpus, run.kb);
|
|
290
|
+
|
|
291
|
+
for (const job of review) {
|
|
292
|
+
await job.execute();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// If the resolver retracts the superseded return's denial:
|
|
296
|
+
query(review.kb, isQualifiedInvestor({ person: alice })).value; // "true"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Go further
|
|
300
|
+
|
|
301
|
+
- [Introducing Factbag](https://daniel.du-pless.is/introducing-factbag): how and why
|
|
302
|
+
to use it.
|
|
303
|
+
- [Jev and the Art of Truth Maintenance](https://daniel.du-pless.is/jev-and-the-art-of-truth-maintenance):
|
|
304
|
+
the thinking behind the project.
|
|
43
305
|
|
|
44
306
|
Apache-2.0 licensed.
|
|
@@ -50,7 +50,7 @@ function fieldIdentityToken(value) {
|
|
|
50
50
|
return ["entity", value.constructor.name, value.id];
|
|
51
51
|
}
|
|
52
52
|
if (typeof value === "bigint") {
|
|
53
|
-
|
|
53
|
+
throw new TypeError("bigint is not supported; use number values");
|
|
54
54
|
}
|
|
55
55
|
return value;
|
|
56
56
|
}
|
package/dist/core/dsl/index.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertAtomLike,
|
|
3
3
|
assertGroundAtom
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-5J3WGMN2.js";
|
|
5
5
|
import {
|
|
6
6
|
Var
|
|
7
7
|
} from "../chunk-UVOCOH2I.js";
|
|
8
8
|
|
|
9
|
+
// src/dsl/assert-supported-value.ts
|
|
10
|
+
function assertSupportedValue(value) {
|
|
11
|
+
if (typeof value === "bigint" || value === BigInt) {
|
|
12
|
+
throw new TypeError("bigint is not supported; use number values and Number fields");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
// src/dsl/formula.ts
|
|
10
17
|
function not(atom) {
|
|
11
18
|
return { sign: "-", atom };
|
|
@@ -20,6 +27,7 @@ function exists(...args) {
|
|
|
20
27
|
return { kind: "exists", args };
|
|
21
28
|
}
|
|
22
29
|
function letIn(variable2, value, body) {
|
|
30
|
+
assertSupportedValue(value);
|
|
23
31
|
return { kind: "letIn", args: [variable2, value, body] };
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -31,6 +39,7 @@ function count(value) {
|
|
|
31
39
|
return aggregateBuilder("count", value);
|
|
32
40
|
}
|
|
33
41
|
function aggregateBuilder(kind, value) {
|
|
42
|
+
assertSupportedValue(value);
|
|
34
43
|
const where = (body, options) => ({
|
|
35
44
|
kind,
|
|
36
45
|
args: options === void 0 ? [value, body] : [value, body, options]
|
|
@@ -40,37 +49,53 @@ function aggregateBuilder(kind, value) {
|
|
|
40
49
|
|
|
41
50
|
// src/dsl/operators/arithmetic.ts
|
|
42
51
|
function add(left, right) {
|
|
52
|
+
assertSupportedValue(left);
|
|
53
|
+
assertSupportedValue(right);
|
|
43
54
|
return { kind: "add", args: [left, right] };
|
|
44
55
|
}
|
|
45
56
|
function sub(left, right) {
|
|
57
|
+
assertSupportedValue(left);
|
|
58
|
+
assertSupportedValue(right);
|
|
46
59
|
return { kind: "sub", args: [left, right] };
|
|
47
60
|
}
|
|
48
61
|
function mul(left, right) {
|
|
62
|
+
assertSupportedValue(left);
|
|
63
|
+
assertSupportedValue(right);
|
|
49
64
|
return { kind: "mul", args: [left, right] };
|
|
50
65
|
}
|
|
51
66
|
function div(left, right) {
|
|
67
|
+
assertSupportedValue(left);
|
|
68
|
+
assertSupportedValue(right);
|
|
52
69
|
return { kind: "div", args: [left, right] };
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
// src/dsl/operators/comparison.ts
|
|
56
73
|
function gt(left, right) {
|
|
74
|
+
assertSupportedValue(left);
|
|
75
|
+
assertSupportedValue(right);
|
|
57
76
|
return { kind: "gt", args: [left, right] };
|
|
58
77
|
}
|
|
59
78
|
function gte(left, right) {
|
|
79
|
+
assertSupportedValue(left);
|
|
80
|
+
assertSupportedValue(right);
|
|
60
81
|
return { kind: "gte", args: [left, right] };
|
|
61
82
|
}
|
|
62
83
|
function lte(left, right) {
|
|
84
|
+
assertSupportedValue(left);
|
|
85
|
+
assertSupportedValue(right);
|
|
63
86
|
return { kind: "lte", args: [left, right] };
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
// src/dsl/predicate.ts
|
|
67
90
|
function makePredicate(name, params, world, metadata) {
|
|
91
|
+
Object.values(params).forEach(assertSupportedValue);
|
|
68
92
|
const holder = {};
|
|
69
93
|
const construct = (fields) => {
|
|
70
94
|
const current = holder.current;
|
|
71
95
|
if (current === void 0) {
|
|
72
96
|
throw new Error(`Predicate ${name} was called before initialization`);
|
|
73
97
|
}
|
|
98
|
+
Object.values(fields).forEach(assertSupportedValue);
|
|
74
99
|
return { predicate: current, fields };
|
|
75
100
|
};
|
|
76
101
|
const declared = construct;
|
|
@@ -21,7 +21,7 @@ import type { AggregateBuilder, NumericTerm, Term } from "../types.js";
|
|
|
21
21
|
* );
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export declare function sum(value: NumericTerm): AggregateBuilder
|
|
24
|
+
export declare function sum(value: NumericTerm): AggregateBuilder;
|
|
25
25
|
/**
|
|
26
26
|
* Count the rows matched by the aggregate body. Runs through the same
|
|
27
27
|
* aggregate machinery as {@link sum}, but thresholds the number of matching
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DateTerm, Formula, NumericTerm } from "../types.js";
|
|
2
2
|
/** Add two numeric terms, producing a numeric rule-body expression. */
|
|
3
|
-
export declare function add(left: NumericTerm, right: NumericTerm): Formula<number
|
|
3
|
+
export declare function add(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
|
|
4
4
|
/**
|
|
5
5
|
* Subtract a numeric duration in milliseconds from a date term, producing a
|
|
6
6
|
* date expression.
|
|
@@ -20,8 +20,8 @@ export declare function add(left: NumericTerm, right: NumericTerm): Formula<numb
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function sub(left: DateTerm, right: NumericTerm): Formula<Date, readonly [DateTerm, NumericTerm]>;
|
|
22
22
|
/** Subtract one numeric term from another. */
|
|
23
|
-
export declare function sub(left: NumericTerm, right: NumericTerm): Formula<number
|
|
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
|
-
export declare function mul(left: NumericTerm, right: NumericTerm): Formula<number
|
|
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
|
-
export declare function div(left: NumericTerm, right: NumericTerm): Formula<number
|
|
27
|
+
export declare function div(left: NumericTerm, right: NumericTerm): Formula<number, readonly [NumericTerm, NumericTerm]>;
|
package/dist/core/dsl/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { Entity };
|
|
|
7
7
|
/** Predicate over stored literal handles. */
|
|
8
8
|
export type LiteralFilter = (literal: AnyLiteral) => boolean;
|
|
9
9
|
/** A scalar value accepted in example expressions. */
|
|
10
|
-
export type Scalar = string | number |
|
|
10
|
+
export type Scalar = string | number | boolean | Date;
|
|
11
11
|
/** A concrete value or logic variable used in formulas and examples. */
|
|
12
12
|
export type Term<Value = Scalar | Entity> = Value | Var;
|
|
13
13
|
/**
|
|
@@ -39,13 +39,13 @@ export type LiteralArgs<Args extends readonly unknown[]> = {
|
|
|
39
39
|
readonly [Index in keyof Args]: LiteralInput<Args[Index]>;
|
|
40
40
|
};
|
|
41
41
|
/** Numeric term accepted by arithmetic, comparisons, and aggregate helpers. */
|
|
42
|
-
export type NumericTerm = number |
|
|
42
|
+
export type NumericTerm = number | Var | Formula<number, readonly unknown[]>;
|
|
43
43
|
/** Date term accepted by date comparisons and date-offset arithmetic. */
|
|
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
47
|
/** Aggregate builder returned by sum(...) and count(...). */
|
|
48
|
-
export type AggregateBuilder<Value extends number
|
|
48
|
+
export type AggregateBuilder<Value extends number = number> = Readonly<{
|
|
49
49
|
where: (<const Body>(body: Body & RuleBodyInput<Body>) => Formula<Value, readonly [Body]>) & (<const Body, const CompleteWhen>(body: Body & RuleBodyInput<Body>, options: Readonly<{
|
|
50
50
|
completeWhen: CompleteWhen & RuleBodyInput<CompleteWhen>;
|
|
51
51
|
}>) => Formula<Value, readonly [Body, Readonly<{
|
package/dist/core/query/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isFormula
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-A6ZZJWIO.js";
|
|
4
4
|
import {
|
|
5
5
|
PrimitiveArithmeticProgrammerError,
|
|
6
6
|
UnconstructiveRecursionProgrammerError,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
isAtomForPredicate,
|
|
15
15
|
isAtomLike,
|
|
16
16
|
isGroundAtom
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-5J3WGMN2.js";
|
|
18
18
|
import {
|
|
19
19
|
AssertionError,
|
|
20
20
|
Var,
|
|
@@ -79,6 +79,15 @@ function uniqueVariables(variables) {
|
|
|
79
79
|
}
|
|
80
80
|
return unique;
|
|
81
81
|
}
|
|
82
|
+
function bindingsAfterOr(branchBindings, bound) {
|
|
83
|
+
const next = new Set(bound);
|
|
84
|
+
for (const candidate of uniqueVariables(branchBindings.flatMap((branch) => [...branch]))) {
|
|
85
|
+
if (branchBindings.every((branch) => branch.has(candidate))) {
|
|
86
|
+
next.add(candidate);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
82
91
|
function variablesIn(value) {
|
|
83
92
|
if (isVar(value)) {
|
|
84
93
|
return [value];
|
|
@@ -95,7 +104,7 @@ function collectBodyEdges(body, headPredicate, bound, edges) {
|
|
|
95
104
|
if (isAtomLike(body)) {
|
|
96
105
|
edges.push({ from: headPredicate, to: body.predicate, kind: "positive" });
|
|
97
106
|
bindVariables(body, bound);
|
|
98
|
-
return;
|
|
107
|
+
return bound;
|
|
99
108
|
}
|
|
100
109
|
if (isNegativeLiteral(body)) {
|
|
101
110
|
edges.push({
|
|
@@ -103,20 +112,26 @@ function collectBodyEdges(body, headPredicate, bound, edges) {
|
|
|
103
112
|
to: body.atom.predicate,
|
|
104
113
|
kind: body.atom.predicate.world === "closed" ? "closed-world-not" : "explicit-not"
|
|
105
114
|
});
|
|
106
|
-
|
|
107
|
-
return;
|
|
115
|
+
return bound;
|
|
108
116
|
}
|
|
109
117
|
if (isFormula(body, "and")) {
|
|
118
|
+
let nextBound = bound;
|
|
110
119
|
for (const arg of body.args) {
|
|
111
|
-
collectBodyEdges(arg, headPredicate,
|
|
120
|
+
nextBound = collectBodyEdges(arg, headPredicate, nextBound, edges);
|
|
112
121
|
}
|
|
113
|
-
return;
|
|
122
|
+
return nextBound;
|
|
123
|
+
}
|
|
124
|
+
if (isFormula(body, "or")) {
|
|
125
|
+
const branchBindings = body.args.map(
|
|
126
|
+
(arg) => collectBodyEdges(arg, headPredicate, new Set(bound), edges)
|
|
127
|
+
);
|
|
128
|
+
return bindingsAfterOr(branchBindings, bound);
|
|
114
129
|
}
|
|
115
|
-
if (isFormula(body, "
|
|
130
|
+
if (isFormula(body, "exists")) {
|
|
116
131
|
for (const arg of body.args) {
|
|
117
132
|
collectBodyEdges(arg, headPredicate, new Set(bound), edges);
|
|
118
133
|
}
|
|
119
|
-
return;
|
|
134
|
+
return bound;
|
|
120
135
|
}
|
|
121
136
|
if (isFormula(body, "letIn")) {
|
|
122
137
|
const [variable, value, nextBody] = body.args;
|
|
@@ -128,12 +143,12 @@ function collectBodyEdges(body, headPredicate, bound, edges) {
|
|
|
128
143
|
if (isVar(variable)) {
|
|
129
144
|
bound.add(variable);
|
|
130
145
|
}
|
|
131
|
-
collectBodyEdges(nextBody, headPredicate, bound, edges);
|
|
132
|
-
return;
|
|
146
|
+
return collectBodyEdges(nextBody, headPredicate, bound, edges);
|
|
133
147
|
}
|
|
134
148
|
if (isPrimitiveFormula(body) && hasUnboundVariable(body, bound)) {
|
|
135
149
|
edges.push({ from: headPredicate, to: headPredicate, kind: "primitive-ungrounded" });
|
|
136
150
|
}
|
|
151
|
+
return bound;
|
|
137
152
|
}
|
|
138
153
|
function collectAggregateEdges(aggregate, headPredicate, bound, edges) {
|
|
139
154
|
for (const arg of aggregate.args.slice(1)) {
|
|
@@ -252,7 +267,10 @@ function assertSafeBody(body, bound) {
|
|
|
252
267
|
return nextBound;
|
|
253
268
|
}
|
|
254
269
|
if (isFormula(body, "or")) {
|
|
255
|
-
return
|
|
270
|
+
return bindingsAfterOr(
|
|
271
|
+
body.args.map((arg) => assertSafeBody(arg, new Set(bound))),
|
|
272
|
+
bound
|
|
273
|
+
);
|
|
256
274
|
}
|
|
257
275
|
if (isFormula(body, "exists")) {
|
|
258
276
|
const nextBody = body.args.at(-1);
|
|
@@ -288,16 +306,6 @@ function assertSafeAggregate(aggregate, bound) {
|
|
|
288
306
|
assertSafeBody(options.completeWhen, new Set(bound));
|
|
289
307
|
}
|
|
290
308
|
}
|
|
291
|
-
function bindingsAvailableAfterOr(args, bound) {
|
|
292
|
-
const branchBindings = args.map((arg) => assertSafeBody(arg, new Set(bound)));
|
|
293
|
-
const next = new Set(bound);
|
|
294
|
-
for (const candidate of uniqueVariables(branchBindings.flatMap((branch) => [...branch]))) {
|
|
295
|
-
if (branchBindings.length > 0 && branchBindings.every((branch) => branch.has(candidate))) {
|
|
296
|
-
next.add(candidate);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return next;
|
|
300
|
-
}
|
|
301
309
|
function assertVariablesBound(variables, bound) {
|
|
302
310
|
const unbound = uniqueVariables(variables).filter((variable) => !bound.has(variable));
|
|
303
311
|
if (unbound.length > 0) {
|
|
@@ -727,7 +735,15 @@ function assertNever(value, message) {
|
|
|
727
735
|
}
|
|
728
736
|
function evaluateRuleBodyMany(literals, rules, body, bindings, active, context, pathCanHaveUnavailableBindings = false) {
|
|
729
737
|
if (isAtomLike(body)) {
|
|
730
|
-
return evaluateAtomBody(
|
|
738
|
+
return evaluateAtomBody(
|
|
739
|
+
literals,
|
|
740
|
+
rules,
|
|
741
|
+
erasedAtom(body),
|
|
742
|
+
bindings,
|
|
743
|
+
active,
|
|
744
|
+
context,
|
|
745
|
+
pathCanHaveUnavailableBindings
|
|
746
|
+
);
|
|
731
747
|
}
|
|
732
748
|
if (isLiteralLike(body) && body.sign === "-") {
|
|
733
749
|
const bodyAtom = erasedAtom(body.atom);
|
|
@@ -907,9 +923,13 @@ function evaluateOrBody(literals, rules, args, bindings, active, context, pathCa
|
|
|
907
923
|
)
|
|
908
924
|
);
|
|
909
925
|
const environments = candidateBindings.length === 0 ? [bindings] : candidateBindings;
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
926
|
+
const queue = [...environments];
|
|
927
|
+
const seen = new Set(queue.map(bindingsKey));
|
|
928
|
+
const results = [];
|
|
929
|
+
for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
|
|
930
|
+
const environment = next;
|
|
931
|
+
const judgments = args.flatMap((arg) => {
|
|
932
|
+
const branch = evaluateRuleBodyMany(
|
|
913
933
|
literals,
|
|
914
934
|
rules,
|
|
915
935
|
arg,
|
|
@@ -918,15 +938,27 @@ function evaluateOrBody(literals, rules, args, bindings, active, context, pathCa
|
|
|
918
938
|
context,
|
|
919
939
|
pathCanHaveUnavailableBindings
|
|
920
940
|
);
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
941
|
+
for (const result2 of branch) {
|
|
942
|
+
if (bindsBeyond(result2.bindings, environment)) {
|
|
943
|
+
const key = bindingsKey(result2.bindings);
|
|
944
|
+
if (!seen.has(key)) {
|
|
945
|
+
seen.add(key);
|
|
946
|
+
queue.push(result2.bindings);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const result = bestResult(
|
|
951
|
+
branch.filter((candidate) => !bindsBeyond(candidate.bindings, environment))
|
|
952
|
+
);
|
|
953
|
+
return result === void 0 ? [] : [result.judgment];
|
|
925
954
|
});
|
|
955
|
+
if (judgments.length === 0) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
926
958
|
const judgmentBindings = bindingsArray(environment);
|
|
927
959
|
const reboundJudgments = judgments.map((judgment) => withBindings(judgment, judgmentBindings));
|
|
928
960
|
const goal = { kind: "or", goals: reboundJudgments.map((judgment) => judgment.goal) };
|
|
929
|
-
|
|
961
|
+
results.push({
|
|
930
962
|
judgment: compoundJudgment(
|
|
931
963
|
goal,
|
|
932
964
|
maxGoalValue(...reboundJudgments.map((judgment) => judgment.value)),
|
|
@@ -934,8 +966,9 @@ function evaluateOrBody(literals, rules, args, bindings, active, context, pathCa
|
|
|
934
966
|
reboundJudgments
|
|
935
967
|
),
|
|
936
968
|
bindings: environment
|
|
937
|
-
};
|
|
938
|
-
}
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
return results;
|
|
939
972
|
}
|
|
940
973
|
function evaluateAndBody(literals, rules, args, bindings, active, context, pathCanHaveUnavailableBindings) {
|
|
941
974
|
let partials = [{ bindings, judgments: [] }];
|
|
@@ -976,7 +1009,7 @@ function evaluateAndBody(literals, rules, args, bindings, active, context, pathC
|
|
|
976
1009
|
};
|
|
977
1010
|
});
|
|
978
1011
|
}
|
|
979
|
-
function evaluateAtomBody(literals, rules, atom, bindings, active, context) {
|
|
1012
|
+
function evaluateAtomBody(literals, rules, atom, bindings, active, context, pathCanHaveUnavailableBindings = false) {
|
|
980
1013
|
const evaluateGrounded = (environment) => {
|
|
981
1014
|
const grounded = groundAtom(atom, environment);
|
|
982
1015
|
const judgmentBindings = bindingsArray(environment);
|
|
@@ -998,7 +1031,14 @@ function evaluateAtomBody(literals, rules, atom, bindings, active, context) {
|
|
|
998
1031
|
])
|
|
999
1032
|
)
|
|
1000
1033
|
};
|
|
1001
|
-
const candidateBindings = candidateGroundAtoms(
|
|
1034
|
+
const candidateBindings = candidateGroundAtoms(
|
|
1035
|
+
literals,
|
|
1036
|
+
rules,
|
|
1037
|
+
pattern,
|
|
1038
|
+
active,
|
|
1039
|
+
context.search,
|
|
1040
|
+
pathCanHaveUnavailableBindings
|
|
1041
|
+
).map((candidate) => extendBindings(atom, candidate, bindings)).filter((candidate) => candidate !== void 0);
|
|
1002
1042
|
if (candidateBindings.length === 0) {
|
|
1003
1043
|
if (atom.predicate.world === "closed") {
|
|
1004
1044
|
assert(!canGroundAtom(atom, bindings), "Ground closed-world absence must use ground solving");
|
|
@@ -1156,16 +1196,16 @@ function definedValue(value, message) {
|
|
|
1156
1196
|
return value;
|
|
1157
1197
|
}
|
|
1158
1198
|
function isDefinedValue(value) {
|
|
1159
|
-
return typeof value === "string" || typeof value === "number" || typeof value === "
|
|
1199
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "object" && value !== null;
|
|
1160
1200
|
}
|
|
1161
1201
|
function isGoalExpression(value) {
|
|
1162
|
-
return isVar(value) || typeof value === "string" || typeof value === "number" || typeof value === "
|
|
1202
|
+
return isVar(value) || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date || typeof value === "object" && value !== null && "id" in value && typeof value.id === "string";
|
|
1163
1203
|
}
|
|
1164
1204
|
function isScalarGoalExpression(value) {
|
|
1165
|
-
return isVar(value) || typeof value === "string" || typeof value === "number" || typeof value === "
|
|
1205
|
+
return isVar(value) || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date;
|
|
1166
1206
|
}
|
|
1167
1207
|
function isNumericGoalExpression(value) {
|
|
1168
|
-
return isVar(value) || typeof value === "number"
|
|
1208
|
+
return isVar(value) || typeof value === "number";
|
|
1169
1209
|
}
|
|
1170
1210
|
function evaluateExpression(expression, bindings, context) {
|
|
1171
1211
|
if (isVar(expression)) {
|
|
@@ -1181,7 +1221,7 @@ function evaluateExpression(expression, bindings, context) {
|
|
|
1181
1221
|
const right = evaluateExpression(expression.args[1], left.bindings, context);
|
|
1182
1222
|
assert(isScalarGoalExpression(left.expression), "Left arithmetic operand must be scalar");
|
|
1183
1223
|
assert(isScalarGoalExpression(right.expression), "Right arithmetic operand must be scalar");
|
|
1184
|
-
const variable = new Var(`$
|
|
1224
|
+
const variable = new Var(`${TEMPORARY_PREFIX}${context.nextTemp++}`);
|
|
1185
1225
|
const goal = {
|
|
1186
1226
|
kind: expression.kind,
|
|
1187
1227
|
bind: variable,
|
|
@@ -1220,6 +1260,10 @@ function evaluateExpression(expression, bindings, context) {
|
|
|
1220
1260
|
};
|
|
1221
1261
|
}
|
|
1222
1262
|
function evaluateExpressionInto(variable, expression, bindings, context) {
|
|
1263
|
+
const existing = valueForVariable(bindings, variable);
|
|
1264
|
+
if (existing !== UNBOUND) {
|
|
1265
|
+
return constrainBoundVariable(variable, existing, expression, bindings, context);
|
|
1266
|
+
}
|
|
1223
1267
|
if (isArithmeticFormula(expression)) {
|
|
1224
1268
|
const left = evaluateExpression(expression.args[0], bindings, context);
|
|
1225
1269
|
const right = evaluateExpression(expression.args[1], left.bindings, context);
|
|
@@ -1256,9 +1300,35 @@ function evaluateExpressionInto(variable, expression, bindings, context) {
|
|
|
1256
1300
|
}
|
|
1257
1301
|
const value = isVar(expression) ? valueForVariable(bindings, expression) : definedValue(expression, "Expression values must be defined");
|
|
1258
1302
|
const nextBindings = new Map(bindings);
|
|
1259
|
-
|
|
1303
|
+
if (value !== UNBOUND) {
|
|
1304
|
+
nextBindings.set(variable, value);
|
|
1305
|
+
}
|
|
1260
1306
|
return { value, expression: variable, bindings: nextBindings, judgments: [] };
|
|
1261
1307
|
}
|
|
1308
|
+
function constrainBoundVariable(variable, existing, expression, bindings, context) {
|
|
1309
|
+
const computed = evaluateExpression(expression, bindings, context);
|
|
1310
|
+
assert(
|
|
1311
|
+
isScalarGoalExpression(computed.expression),
|
|
1312
|
+
"A let binding of a bound variable must compare scalar values"
|
|
1313
|
+
);
|
|
1314
|
+
const goal = { kind: "eq", left: variable, right: computed.expression };
|
|
1315
|
+
const value = computed.value === UNBOUND ? "unknown" : compareValues("eq", existing, computed.value);
|
|
1316
|
+
const comparison = primitiveComparisonJudgment(
|
|
1317
|
+
goal,
|
|
1318
|
+
value,
|
|
1319
|
+
bindingsArray(computed.bindings),
|
|
1320
|
+
existing,
|
|
1321
|
+
variable,
|
|
1322
|
+
computed.value,
|
|
1323
|
+
computed.expression
|
|
1324
|
+
);
|
|
1325
|
+
return {
|
|
1326
|
+
value: existing,
|
|
1327
|
+
expression: variable,
|
|
1328
|
+
bindings: computed.bindings,
|
|
1329
|
+
judgments: [...computed.judgments, comparison]
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1262
1332
|
function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, active, context) {
|
|
1263
1333
|
const [valueExpression, whereBody, options] = aggregate.args;
|
|
1264
1334
|
const completeWhen = options?.completeWhen;
|
|
@@ -1282,11 +1352,11 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
|
|
|
1282
1352
|
assert(completeness !== void 0, "Aggregate completeness evaluation produced no best result");
|
|
1283
1353
|
if (completeness.judgment.value !== "true") {
|
|
1284
1354
|
const judgmentBindings = bindingsArray(bindings);
|
|
1285
|
-
let
|
|
1355
|
+
let goal2;
|
|
1286
1356
|
switch (aggregate.kind) {
|
|
1287
1357
|
case "sum":
|
|
1288
1358
|
assert(isNumericGoalExpression(valueExpression), "Sum value must be numeric");
|
|
1289
|
-
|
|
1359
|
+
goal2 = {
|
|
1290
1360
|
kind: "sum",
|
|
1291
1361
|
bind: variable,
|
|
1292
1362
|
value: valueExpression,
|
|
@@ -1295,7 +1365,7 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
|
|
|
1295
1365
|
};
|
|
1296
1366
|
break;
|
|
1297
1367
|
case "count":
|
|
1298
|
-
|
|
1368
|
+
goal2 = {
|
|
1299
1369
|
kind: "count",
|
|
1300
1370
|
bind: variable,
|
|
1301
1371
|
where: whereGoal,
|
|
@@ -1308,12 +1378,14 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
|
|
|
1308
1378
|
return {
|
|
1309
1379
|
kind: "incomplete",
|
|
1310
1380
|
result: {
|
|
1311
|
-
judgment: { value: "unknown", goal, bindings: judgmentBindings },
|
|
1381
|
+
judgment: { value: "unknown", goal: goal2, bindings: judgmentBindings },
|
|
1312
1382
|
bindings
|
|
1313
1383
|
}
|
|
1314
1384
|
};
|
|
1315
1385
|
}
|
|
1316
1386
|
}
|
|
1387
|
+
const existing = valueForVariable(bindings, variable);
|
|
1388
|
+
const target = existing === UNBOUND ? variable : new Var(`$tmp${context.nextTemp++}`);
|
|
1317
1389
|
const nextBindings = new Map(bindings);
|
|
1318
1390
|
const values = rows.map((row) => resolveAggregateValue(valueExpression, row.bindings));
|
|
1319
1391
|
let value;
|
|
@@ -1322,29 +1394,29 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
|
|
|
1322
1394
|
case "sum":
|
|
1323
1395
|
assert(isNumericGoalExpression(valueExpression), "Sum value must be numeric");
|
|
1324
1396
|
value = sumValues(values);
|
|
1325
|
-
nextBindings.set(
|
|
1397
|
+
nextBindings.set(target, value);
|
|
1326
1398
|
judgment = primitiveSumJudgment(
|
|
1327
1399
|
{
|
|
1328
1400
|
kind: "sum",
|
|
1329
|
-
bind:
|
|
1401
|
+
bind: target,
|
|
1330
1402
|
value: valueExpression,
|
|
1331
1403
|
where: whereGoal
|
|
1332
1404
|
},
|
|
1333
1405
|
bindingsArray(nextBindings),
|
|
1334
1406
|
values,
|
|
1335
|
-
|
|
1407
|
+
target,
|
|
1336
1408
|
value
|
|
1337
1409
|
);
|
|
1338
1410
|
break;
|
|
1339
1411
|
case "count": {
|
|
1340
1412
|
const range = countRange(possibleRows, isOpenAggregate(whereBody));
|
|
1341
1413
|
value = exactCountValue(range);
|
|
1342
|
-
nextBindings.set(
|
|
1414
|
+
nextBindings.set(target, value);
|
|
1343
1415
|
judgment = primitiveCountJudgment(
|
|
1344
|
-
{ kind: "count", bind:
|
|
1416
|
+
{ kind: "count", bind: target, where: whereGoal },
|
|
1345
1417
|
bindingsArray(nextBindings),
|
|
1346
1418
|
range,
|
|
1347
|
-
|
|
1419
|
+
target,
|
|
1348
1420
|
value
|
|
1349
1421
|
);
|
|
1350
1422
|
break;
|
|
@@ -1352,9 +1424,30 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
|
|
|
1352
1424
|
default:
|
|
1353
1425
|
return assertNever(aggregate.kind, "Unhandled aggregate kind");
|
|
1354
1426
|
}
|
|
1427
|
+
if (existing === UNBOUND) {
|
|
1428
|
+
return {
|
|
1429
|
+
kind: "complete",
|
|
1430
|
+
expression: { value, expression: variable, bindings: nextBindings, judgments: [judgment] }
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
const goal = { kind: "eq", left: variable, right: target };
|
|
1434
|
+
const comparison = primitiveComparisonJudgment(
|
|
1435
|
+
goal,
|
|
1436
|
+
compareValues("eq", existing, value),
|
|
1437
|
+
bindingsArray(nextBindings),
|
|
1438
|
+
existing,
|
|
1439
|
+
variable,
|
|
1440
|
+
value,
|
|
1441
|
+
target
|
|
1442
|
+
);
|
|
1355
1443
|
return {
|
|
1356
1444
|
kind: "complete",
|
|
1357
|
-
expression: {
|
|
1445
|
+
expression: {
|
|
1446
|
+
value: existing,
|
|
1447
|
+
expression: variable,
|
|
1448
|
+
bindings: nextBindings,
|
|
1449
|
+
judgments: [judgment, comparison]
|
|
1450
|
+
}
|
|
1358
1451
|
};
|
|
1359
1452
|
}
|
|
1360
1453
|
function resolveAggregateValue(expression, bindings) {
|
|
@@ -1369,10 +1462,10 @@ function aggregateValueIsAvailable(expression, bindings) {
|
|
|
1369
1462
|
return valueForVariable(bindings, expression) !== UNBOUND;
|
|
1370
1463
|
}
|
|
1371
1464
|
function sumValues(values) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1465
|
+
return values.reduce((total, value) => {
|
|
1466
|
+
assert(typeof value === "number", "Sum values must be numbers");
|
|
1467
|
+
return total + value;
|
|
1468
|
+
}, 0);
|
|
1376
1469
|
}
|
|
1377
1470
|
function aggregateWhereGoal(judgments) {
|
|
1378
1471
|
if (judgments.length === 1) {
|
|
@@ -1546,7 +1639,7 @@ function primitiveUncertaintyForComparison(value, left, leftExpression, right, r
|
|
|
1546
1639
|
return {};
|
|
1547
1640
|
}
|
|
1548
1641
|
function primitiveRangeForExact(value) {
|
|
1549
|
-
if (typeof value !== "number"
|
|
1642
|
+
if (typeof value !== "number") {
|
|
1550
1643
|
return {};
|
|
1551
1644
|
}
|
|
1552
1645
|
return { range: { lower: value, upper: value } };
|
|
@@ -1596,20 +1689,6 @@ function calculateValue(kind, left, right, goal) {
|
|
|
1596
1689
|
return assertNever(kind, "Unhandled arithmetic kind");
|
|
1597
1690
|
}
|
|
1598
1691
|
}
|
|
1599
|
-
if (typeof left === "bigint" && typeof right === "bigint") {
|
|
1600
|
-
switch (kind) {
|
|
1601
|
-
case "add":
|
|
1602
|
-
return left + right;
|
|
1603
|
-
case "sub":
|
|
1604
|
-
return left - right;
|
|
1605
|
-
case "mul":
|
|
1606
|
-
return left * right;
|
|
1607
|
-
case "div":
|
|
1608
|
-
return left / right;
|
|
1609
|
-
default:
|
|
1610
|
-
return assertNever(kind, "Unhandled arithmetic kind");
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
1692
|
throw new PrimitiveArithmeticProgrammerError(kind, [left, right], goal);
|
|
1614
1693
|
}
|
|
1615
1694
|
function compareValues(kind, left, right) {
|
|
@@ -1637,10 +1716,7 @@ function compareValues(kind, left, right) {
|
|
|
1637
1716
|
}
|
|
1638
1717
|
}
|
|
1639
1718
|
function compareOrderedValues(kind, left, right) {
|
|
1640
|
-
assert(
|
|
1641
|
-
(typeof left === "number" || typeof left === "bigint") && (typeof right === "number" || typeof right === "bigint"),
|
|
1642
|
-
"Values must be comparable"
|
|
1643
|
-
);
|
|
1719
|
+
assert(typeof left === "number" && typeof right === "number", "Values must be comparable");
|
|
1644
1720
|
switch (kind) {
|
|
1645
1721
|
case "lt":
|
|
1646
1722
|
return left < right ? "true" : "false";
|
|
@@ -1810,13 +1886,28 @@ function ruleContribution(kind, value) {
|
|
|
1810
1886
|
function bindingsArray(bindings) {
|
|
1811
1887
|
return [...bindings.entries()];
|
|
1812
1888
|
}
|
|
1889
|
+
var TEMPORARY_PREFIX = "$tmp";
|
|
1890
|
+
function isTemporary(variable) {
|
|
1891
|
+
return variable.name.startsWith(TEMPORARY_PREFIX);
|
|
1892
|
+
}
|
|
1893
|
+
function bindsBeyond(result, environment) {
|
|
1894
|
+
for (const variable of result.keys()) {
|
|
1895
|
+
if (!isTemporary(variable) && !environment.has(variable)) {
|
|
1896
|
+
return true;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return false;
|
|
1900
|
+
}
|
|
1901
|
+
function bindingsKey(binding) {
|
|
1902
|
+
return JSON.stringify(
|
|
1903
|
+
bindingsArray(binding).filter(([variable]) => !isTemporary(variable)).map(([variable, value]) => [variable.name, fieldIdentityToken(value)])
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1813
1906
|
function uniqueBindings(bindings) {
|
|
1814
1907
|
const seen = /* @__PURE__ */ new Set();
|
|
1815
1908
|
const unique = [];
|
|
1816
1909
|
for (const binding of bindings) {
|
|
1817
|
-
const key =
|
|
1818
|
-
bindingsArray(binding).map(([variable, value]) => [variable.name, fieldIdentityToken(value)])
|
|
1819
|
-
);
|
|
1910
|
+
const key = bindingsKey(binding);
|
|
1820
1911
|
if (!seen.has(key)) {
|
|
1821
1912
|
seen.add(key);
|
|
1822
1913
|
unique.push(binding);
|
|
@@ -1831,16 +1922,16 @@ function groundAtomKey(atom) {
|
|
|
1831
1922
|
const fields = atom.predicate.paramNames.map((name) => fieldIdentityToken(atom.fields[name]));
|
|
1832
1923
|
return `${atom.predicate.name}:${JSON.stringify(fields)}`;
|
|
1833
1924
|
}
|
|
1834
|
-
function candidateGroundAtoms(literals, rules, atom, active, search) {
|
|
1925
|
+
function candidateGroundAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings = false) {
|
|
1835
1926
|
return search.candidates.candidates(
|
|
1836
1927
|
atom,
|
|
1837
1928
|
matchingGroundAtoms(literals, atom),
|
|
1838
|
-
() => deriveCandidateAtoms(literals, rules, atom, active, search)
|
|
1929
|
+
() => deriveCandidateAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings)
|
|
1839
1930
|
).filter(
|
|
1840
1931
|
(candidate) => isAtomForPredicate(candidate, atom.predicate)
|
|
1841
1932
|
);
|
|
1842
1933
|
}
|
|
1843
|
-
function deriveCandidateAtoms(literals, rules, atom, active, search) {
|
|
1934
|
+
function deriveCandidateAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings = false) {
|
|
1844
1935
|
const atoms = [];
|
|
1845
1936
|
for (const rule of rules) {
|
|
1846
1937
|
const ruleHead = atomForPredicate(rule.head.atom, atom.predicate);
|
|
@@ -1852,9 +1943,17 @@ function deriveCandidateAtoms(literals, rules, atom, active, search) {
|
|
|
1852
1943
|
continue;
|
|
1853
1944
|
}
|
|
1854
1945
|
const context = { nextTemp: 0, search, candidatePredicate: atom.predicate };
|
|
1855
|
-
const bodies = evaluateRuleBodyMany(
|
|
1946
|
+
const bodies = evaluateRuleBodyMany(
|
|
1947
|
+
literals,
|
|
1948
|
+
rules,
|
|
1949
|
+
rule.body,
|
|
1950
|
+
bindings,
|
|
1951
|
+
active,
|
|
1952
|
+
context,
|
|
1953
|
+
pathCanHaveUnavailableBindings
|
|
1954
|
+
);
|
|
1856
1955
|
for (const body of bodies) {
|
|
1857
|
-
if (!canGroundAtom(ruleHead, body.bindings)) {
|
|
1956
|
+
if (body.judgment.value === "false" || !canGroundAtom(ruleHead, body.bindings)) {
|
|
1858
1957
|
continue;
|
|
1859
1958
|
}
|
|
1860
1959
|
const candidate = groundAtom(ruleHead, body.bindings);
|
|
@@ -23,5 +23,7 @@ export declare function isCompleteWhenOption(value: unknown): value is Readonly<
|
|
|
23
23
|
export declare function isPrimitiveFormula(value: unknown): boolean;
|
|
24
24
|
export declare function bindVariables(value: unknown, bound: Set<Var>): void;
|
|
25
25
|
export declare function uniqueVariables(variables: readonly Var[]): Var[];
|
|
26
|
+
/** Bindings available after an `or`: those held before it plus those every branch makes. */
|
|
27
|
+
export declare function bindingsAfterOr(branchBindings: readonly ReadonlySet<Var>[], bound: ReadonlySet<Var>): Set<Var>;
|
|
26
28
|
export declare function variablesIn(value: unknown): Var[];
|
|
27
29
|
export {};
|
|
@@ -84,7 +84,7 @@ export type ComparisonGoal<Kind extends "eq" | "neq" | "lt" | "lte" | "gt" | "gt
|
|
|
84
84
|
export type SumGoal = Readonly<{
|
|
85
85
|
readonly kind: "sum";
|
|
86
86
|
readonly bind: Var;
|
|
87
|
-
readonly value: GoalExpression<number
|
|
87
|
+
readonly value: GoalExpression<number>;
|
|
88
88
|
readonly where: Goal;
|
|
89
89
|
readonly completeWhen?: Goal;
|
|
90
90
|
}>;
|
|
@@ -56,8 +56,8 @@ export type PrimitiveEvaluation = Readonly<{
|
|
|
56
56
|
}>;
|
|
57
57
|
/** Numeric lower/upper bound produced or consumed by a primitive operation. */
|
|
58
58
|
export type NumericRange = Readonly<{
|
|
59
|
-
readonly lower: number
|
|
60
|
-
readonly upper: number |
|
|
59
|
+
readonly lower: number;
|
|
60
|
+
readonly upper: number | "infinity";
|
|
61
61
|
}>;
|
|
62
62
|
/** Why a primitive evaluation is not exact. */
|
|
63
63
|
export type PrimitiveUncertainty = Readonly<{
|
|
@@ -17,9 +17,9 @@ export type RuntimeConstructor<T> = (abstract new (...args: never[]) => T) & Rea
|
|
|
17
17
|
name: string;
|
|
18
18
|
}>;
|
|
19
19
|
/** Any runtime validator accepted in a predicate schema field. */
|
|
20
|
-
export type RuntimeType<T> = EnumType | RuntimeConstructor<T & Entity> |
|
|
20
|
+
export type RuntimeType<T> = EnumType | RuntimeConstructor<T & Entity> | BooleanConstructor | DateConstructor | NumberConstructor | StringConstructor;
|
|
21
21
|
/** A predicate schema maps field names to runtime validators/constructors. */
|
|
22
|
-
export type PredicateSchema = Readonly<Record<string,
|
|
22
|
+
export type PredicateSchema = Readonly<Record<string, BooleanConstructor | DateConstructor | NumberConstructor | StringConstructor | EnumType | ((abstract new (...args: never[]) => Entity) & Readonly<{
|
|
23
23
|
prototype: Entity;
|
|
24
24
|
name: string;
|
|
25
25
|
}>)>>;
|
|
@@ -28,6 +28,6 @@ export type PredicateSchema = Readonly<Record<string, BigIntConstructor | Boolea
|
|
|
28
28
|
* type. A schema-erased constructor (the bare `RuntimeConstructor<Entity>`)
|
|
29
29
|
* infers `unknown`, so erased atoms keep permissive field types.
|
|
30
30
|
*/
|
|
31
|
-
export type InferRuntime<T> = T extends EnumType<infer Value> ? Value : T extends StringConstructor ? string : T extends NumberConstructor ? number : T extends BooleanConstructor ? boolean : T extends
|
|
31
|
+
export type InferRuntime<T> = T extends EnumType<infer Value> ? Value : T extends StringConstructor ? string : T extends NumberConstructor ? number : T extends BooleanConstructor ? boolean : T extends DateConstructor ? Date : T extends {
|
|
32
32
|
prototype: infer Value;
|
|
33
33
|
} ? RuntimeConstructor<Entity> extends T ? unknown : Value : never;
|
|
@@ -65,8 +65,8 @@ function encodeField(value) {
|
|
|
65
65
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
66
66
|
return value;
|
|
67
67
|
}
|
|
68
|
-
if (
|
|
69
|
-
return value
|
|
68
|
+
if (value instanceof Date) {
|
|
69
|
+
return value.toISOString();
|
|
70
70
|
}
|
|
71
71
|
if (isRecord(value)) {
|
|
72
72
|
return JSON.parse(JSON.stringify({ ...value, type: value.constructor.name }));
|
package/dist/jev/classifier.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
admissionPolicy,
|
|
6
6
|
createJob,
|
|
7
7
|
jevClient
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-PISXBJOT.js";
|
|
9
9
|
|
|
10
10
|
// src/classifier.ts
|
|
11
11
|
import { chunkers } from "../corpus/index.js";
|
|
@@ -81,7 +81,7 @@ function validatePredicate(predicate, kb) {
|
|
|
81
81
|
function assertSupportedType(predicate) {
|
|
82
82
|
for (const name of predicate.paramNames) {
|
|
83
83
|
const type = predicate.params[name];
|
|
84
|
-
if (type === String || type === Number || type ===
|
|
84
|
+
if (type === String || type === Number || type === Date || type === void 0) {
|
|
85
85
|
throw new UnsupportedPredicateError(
|
|
86
86
|
`Cannot classify predicate "${predicate.name}": field "${name}" has an unsupported open-ended domain. Use Boolean, enumeration(...), or registered entities.`,
|
|
87
87
|
predicate,
|
package/dist/jev/resolve.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factbag",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.2",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"logic",
|
|
6
6
|
"rules-engine",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"@factbag/corpus": "0.1.0-next.0",
|
|
77
77
|
"@factbag/guards": "0.1.0-next.0",
|
|
78
78
|
"@factbag/jev": "0.1.0-next.0",
|
|
79
|
-
"@typesafe-ai/sdk": "0.
|
|
79
|
+
"@typesafe-ai/sdk": "0.7.0"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"@typesafe-ai/sdk": "^0.6.0"
|
|
File without changes
|