factbag 0.0.0-alpha.4 → 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 +32 -0
- package/README.md +35 -33
- package/dist/corpus.d.ts +112 -0
- package/dist/corpus.js +266 -0
- package/dist/dsl.d.ts +309 -0
- package/dist/dsl.js +476 -0
- package/dist/exceptions.d.ts +44 -0
- package/dist/exceptions.js +1 -0
- package/dist/explain.d.ts +225 -0
- package/dist/explain.js +415 -0
- package/dist/goal-BBoSfPUN.d.ts +333 -0
- package/dist/judgment-v8i3ks5N.d.ts +140 -0
- package/dist/kb.d.ts +71 -0
- package/dist/kb.js +298 -0
- package/dist/knowledge-base-CkU_xzmB.d.ts +76 -0
- package/dist/programmer-errors-DLE1xWJ_.js +77 -0
- package/dist/query-CN6EOPwp.js +2062 -0
- package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
- package/dist/types-DeYyybXd.js +46 -0
- package/package.json +42 -43
- package/dist/core/chunk-N2RRSOEN.js +0 -87
- package/dist/core/chunk-NDUIQAH4.js +0 -8
- package/dist/core/chunk-QOMFY3XB.js +0 -62
- package/dist/core/chunk-UVOCOH2I.js +0 -41
- package/dist/core/dsl/assert-supported-value.d.ts +0 -2
- package/dist/core/dsl/enumeration.d.ts +0 -7
- package/dist/core/dsl/formula.d.ts +0 -62
- package/dist/core/dsl/index.d.ts +0 -21
- package/dist/core/dsl/index.js +0 -226
- package/dist/core/dsl/operators/aggregate.d.ts +0 -30
- package/dist/core/dsl/operators/arithmetic.d.ts +0 -94
- package/dist/core/dsl/operators/comparison.d.ts +0 -23
- package/dist/core/dsl/predicate.d.ts +0 -33
- package/dist/core/dsl/types.d.ts +0 -61
- package/dist/core/dsl/variable.d.ts +0 -3
- package/dist/core/exceptions/programmer-errors.d.ts +0 -36
- package/dist/core/explanation/explain.d.ts +0 -24
- package/dist/core/explanation/index.d.ts +0 -8
- package/dist/core/explanation/index.js +0 -227
- package/dist/core/explanation/types.d.ts +0 -172
- package/dist/core/knowledge-base/assertions/exclusive-definitions.d.ts +0 -3
- package/dist/core/knowledge-base/index.d.ts +0 -13
- package/dist/core/knowledge-base/index.js +0 -196
- package/dist/core/knowledge-base/knowledge-base.d.ts +0 -81
- package/dist/core/query/index.d.ts +0 -10
- package/dist/core/query/index.js +0 -2385
- package/dist/core/query/query.d.ts +0 -47
- package/dist/core/solver/calendar.d.ts +0 -4
- package/dist/core/solver/candidate-search.d.ts +0 -14
- package/dist/core/solver/goal-value.d.ts +0 -6
- package/dist/core/solver/matching.d.ts +0 -10
- package/dist/core/solver/program-index.d.ts +0 -17
- package/dist/core/solver/query-validation/constructive-recursion.d.ts +0 -3
- package/dist/core/solver/query-validation/constructive-variable-use.d.ts +0 -3
- package/dist/core/solver/query-validation/context.d.ts +0 -10
- package/dist/core/solver/query-validation/index.d.ts +0 -1
- package/dist/core/solver/query-validation/utils.d.ts +0 -31
- package/dist/core/solver/query-validation/validate-query.d.ts +0 -6
- package/dist/core/solver/solve-all.d.ts +0 -6
- package/dist/core/solver/solve.d.ts +0 -6
- package/dist/core/types/annotations.d.ts +0 -6
- package/dist/core/types/atom.d.ts +0 -60
- package/dist/core/types/common.d.ts +0 -7
- package/dist/core/types/evidence-metadata.d.ts +0 -18
- package/dist/core/types/goal.d.ts +0 -121
- package/dist/core/types/judgment.d.ts +0 -137
- package/dist/core/types/literal.d.ts +0 -25
- package/dist/core/types/predicate.d.ts +0 -35
- package/dist/core/types/rule.d.ts +0 -16
- package/dist/core/types/schema.d.ts +0 -33
- package/dist/core/types/variable.d.ts +0 -13
- package/dist/core/utils/assert.d.ts +0 -4
- package/dist/core/utils/normalize-to-list.d.ts +0 -2
- package/dist/corpus/chunkers.d.ts +0 -7
- package/dist/corpus/corpus.d.ts +0 -41
- package/dist/corpus/index.d.ts +0 -8
- package/dist/corpus/index.js +0 -312
- package/dist/corpus/records.d.ts +0 -8
- package/dist/corpus/text.d.ts +0 -6
- package/dist/corpus/types.d.ts +0 -62
- package/dist/guards/guarded-kb.d.ts +0 -59
- package/dist/guards/index.d.ts +0 -7
- package/dist/guards/index.js +0 -121
- package/dist/guards/types.d.ts +0 -15
- package/dist/jev/chunk-M7RHUI2O.js +0 -15
- package/dist/jev/chunk-QEZ4O3YS.js +0 -203
- package/dist/jev/chunk-ZAAXPTJJ.js +0 -105
- package/dist/jev/classifier/classifiable-predicates.d.ts +0 -12
- package/dist/jev/classifier/classifier.d.ts +0 -16
- package/dist/jev/classifier/extract-evidence.d.ts +0 -17
- package/dist/jev/classifier/filter-predicates.d.ts +0 -9
- package/dist/jev/classifier/filter-symbols.d.ts +0 -14
- package/dist/jev/classifier/index.d.ts +0 -6
- package/dist/jev/classifier/index.js +0 -246
- package/dist/jev/classifier/types.d.ts +0 -13
- package/dist/jev/config.d.ts +0 -17
- package/dist/jev/config.js +0 -18
- package/dist/jev/errors.d.ts +0 -22
- package/dist/jev/errors.js +0 -6
- package/dist/jev/jev/model.d.ts +0 -11
- package/dist/jev/jev/prompts.d.ts +0 -29
- package/dist/jev/resolve.d.ts +0 -15
- package/dist/jev/resolve.js +0 -76
- package/dist/jev/settings.d.ts +0 -11
- package/dist/jev/types/config.d.ts +0 -18
- package/dist/jev/types/model.d.ts +0 -58
- package/dist/jev/types/run.d.ts +0 -9
- package/dist/jev/utils.d.ts +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
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
|
+
|
|
20
|
+
## 0.0.0-alpha.5
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- 19ac508: **Breaking:** errors have their own entry point, `factbag/exceptions`. `ProgrammerError`, `ExclusiveDefinitionProgrammerError`, `PrimitiveArithmeticProgrammerError`, and `UnsafeUnboundVariableProgrammerError` move there from `factbag/kb`. `UnconstructiveRecursionProgrammerError`, which the solver raised but no entry point exported, is now exported too. `UnsupportedPredicateError` moves there from `factbag/jev/errors` and now extends `ProgrammerError`.
|
|
25
|
+
- 19ac508: **Breaking:** Jev moves from `factbag/jev` to its own package, `@factbag/jev`. Install it alongside `factbag` and `@typesafe-ai/sdk`, which is now a required peer dependency at `^0.7.0`, and import `classifier`, `resolver`, `createConfig`, `Config`, and the `DEFAULT_*` constants from `@factbag/jev`. `factbag` no longer has a Jev entry point or an SDK peer dependency. `@factbag/jev` and `@factbag/clef` are released together with `factbag` and require the same `factbag` version.
|
|
26
|
+
|
|
27
|
+
**Breaking:** `@factbag/jev` has a single entry point. `factbag/jev/classifier`, `factbag/jev/resolve`, `factbag/jev/config`, and `factbag/jev/errors` have no replacement subpaths.
|
|
28
|
+
|
|
29
|
+
Add `batchSize` to Jev's `Config`: how many questions each request holds. It defaults to 64 and has no upper limit. An invalid `batchSize` throws when the config is created or a run is built.
|
|
30
|
+
|
|
31
|
+
Jev's `createConfig` now returns `Normalized & Config`, so `client` is no longer guaranteed on the result.
|
|
32
|
+
|
|
33
|
+
- dd3730b: **Breaking:** `factbag/guards` and `factbag/query` are removed. Import `guard`, `GuardedKB`, and the other guard exports from `factbag/kb`, and `query`, `queryAll`, `Judgment`, and the goal and support types from `factbag/dsl`.
|
|
34
|
+
|
|
3
35
|
## 0.0.0-alpha.4
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -32,9 +32,13 @@ the rules again.
|
|
|
32
32
|
## Install
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
|
-
npm install factbag@alpha @typesafe-ai/sdk@^0.
|
|
35
|
+
npm install factbag@alpha @factbag/jev@alpha @typesafe-ai/sdk@^0.7.0
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
`@factbag/jev` is released with `factbag` and needs the same version. To use
|
|
39
|
+
Cloudflare's Clef models on Workers AI instead, install `@factbag/clef@alpha`, which
|
|
40
|
+
does not need the TypeSafe SDK.
|
|
41
|
+
|
|
38
42
|
ESM only. Requires Node 22.18+ on the 22.x line, or Node 24 or 26.
|
|
39
43
|
|
|
40
44
|
Set your TypeSafe API key in the environment:
|
|
@@ -63,12 +67,7 @@ into three conditions:
|
|
|
63
67
|
Declare the facts we'll combine, and describe the claims Jev will assess.
|
|
64
68
|
|
|
65
69
|
```ts
|
|
66
|
-
import {
|
|
67
|
-
import { KnowledgeBase } from "factbag/kb";
|
|
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";
|
|
70
|
+
import { enumeration, predicate } from "factbag/dsl";
|
|
72
71
|
|
|
73
72
|
class Person {
|
|
74
73
|
constructor(
|
|
@@ -112,6 +111,9 @@ All three income conditions must hold for this route to qualification. Our
|
|
|
112
111
|
application also requires verified identity before sending an investor for review.
|
|
113
112
|
|
|
114
113
|
```ts
|
|
114
|
+
import { and, variable } from "factbag/dsl";
|
|
115
|
+
import { KnowledgeBase } from "factbag/kb";
|
|
116
|
+
|
|
115
117
|
const person = variable("person");
|
|
116
118
|
const kb = new KnowledgeBase();
|
|
117
119
|
|
|
@@ -134,6 +136,8 @@ Identity verification comes directly from our onboarding system. Income evidence
|
|
|
134
136
|
goes into the corpus for Jev to assess.
|
|
135
137
|
|
|
136
138
|
```ts
|
|
139
|
+
import { Corpus } from "factbag/corpus";
|
|
140
|
+
|
|
137
141
|
const alice = new Person("alice", "Alice");
|
|
138
142
|
kb.entities.add(alice);
|
|
139
143
|
|
|
@@ -160,6 +164,8 @@ The guard starts with no permissions. Permit Jev to assert or deny the two incom
|
|
|
160
164
|
predicates.
|
|
161
165
|
|
|
162
166
|
```ts
|
|
167
|
+
import { guard } from "factbag/kb";
|
|
168
|
+
|
|
163
169
|
const incomeEvidence = guard(kb)
|
|
164
170
|
.permit("assert", hasIncomeOver200k)
|
|
165
171
|
.permit("deny", hasIncomeOver200k)
|
|
@@ -170,6 +176,8 @@ const incomeEvidence = guard(kb)
|
|
|
170
176
|
### 6. Ask Jev to assess the income evidence
|
|
171
177
|
|
|
172
178
|
```ts
|
|
179
|
+
import { classifier } from "@factbag/jev";
|
|
180
|
+
|
|
173
181
|
const run = classifier({ corpus, input: incomeEvidence });
|
|
174
182
|
|
|
175
183
|
for (const job of run) {
|
|
@@ -183,6 +191,8 @@ The output knowledge base combines the imported fact and rules with the admitted
|
|
|
183
191
|
evidence.
|
|
184
192
|
|
|
185
193
|
```ts
|
|
194
|
+
import { query } from "factbag/dsl";
|
|
195
|
+
|
|
186
196
|
const qualification = query(run.kb, isQualifiedInvestor({ person: alice }));
|
|
187
197
|
const reviewReadiness = query(run.kb, isReadyForReview({ person: alice }));
|
|
188
198
|
|
|
@@ -211,33 +221,25 @@ An abbreviated example with supporting evidence for all three claims:
|
|
|
211
221
|
```json
|
|
212
222
|
{
|
|
213
223
|
"claim": "is_qualified_investor(alice) = true",
|
|
214
|
-
"because": "is_qualified_investor(person) if and(...)
|
|
224
|
+
"because": "is_qualified_investor(person) if and(...)",
|
|
215
225
|
"given": [
|
|
216
226
|
{
|
|
217
|
-
"claim": "
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
]
|
|
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
|
+
}
|
|
241
243
|
}
|
|
242
244
|
]
|
|
243
245
|
}
|
|
@@ -282,7 +284,7 @@ Evidence judged superseded is retracted from the resolver's output
|
|
|
282
284
|
knowledge base.
|
|
283
285
|
|
|
284
286
|
```ts
|
|
285
|
-
import { resolver } from "factbag/jev
|
|
287
|
+
import { resolver } from "@factbag/jev";
|
|
286
288
|
|
|
287
289
|
query(run.kb, isQualifiedInvestor({ person: alice })).value; // "conflicted"
|
|
288
290
|
|
package/dist/corpus.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Application-defined metadata. Values may include native objects and functions. */
|
|
2
|
+
type Metadata = Readonly<Record<string, unknown>>;
|
|
3
|
+
/** A half-open range, measured in JavaScript UTF-16 code units. */
|
|
4
|
+
type TextRange = Readonly<{
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
}>;
|
|
8
|
+
/** Location of the original artifact; text offsets refer to the stored text, not file bytes. */
|
|
9
|
+
type DocumentSource = Readonly<{
|
|
10
|
+
uri: string;
|
|
11
|
+
mediaType?: string;
|
|
12
|
+
}>;
|
|
13
|
+
/** Text supplied by the caller, after any file extraction or normalization. */
|
|
14
|
+
type DocumentInput<M extends Metadata = Metadata> = Readonly<{
|
|
15
|
+
documentId: string;
|
|
16
|
+
text: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
source?: DocumentSource;
|
|
19
|
+
metadata?: M;
|
|
20
|
+
}>;
|
|
21
|
+
/** Identifies one immutable document. */
|
|
22
|
+
type DocumentReference = Readonly<{
|
|
23
|
+
documentId: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** An immutable document. Re-adding its ID with different content is rejected. */
|
|
26
|
+
type CorpusDocument<M extends Metadata = Metadata> = DocumentReference & Readonly<{
|
|
27
|
+
text: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
source?: DocumentSource;
|
|
30
|
+
/** Shallow snapshot; nested values retain identity and remain caller-owned. */
|
|
31
|
+
metadata: Readonly<Partial<M>>;
|
|
32
|
+
}>;
|
|
33
|
+
/** A stable source range; its ID depends only on the document and absolute range. */
|
|
34
|
+
type ChunkReference = DocumentReference & Readonly<{
|
|
35
|
+
id: string;
|
|
36
|
+
range: TextRange;
|
|
37
|
+
}>;
|
|
38
|
+
/** A source range with text materialized for convenient consumption by models or UIs. */
|
|
39
|
+
type Chunk = ChunkReference & Readonly<{
|
|
40
|
+
text: string;
|
|
41
|
+
}>;
|
|
42
|
+
/** A resolved chunk together with its complete document. */
|
|
43
|
+
type ResolvedChunk<M extends Metadata = Metadata> = Readonly<{
|
|
44
|
+
chunk: Chunk;
|
|
45
|
+
document: CorpusDocument<M>;
|
|
46
|
+
}>;
|
|
47
|
+
/** Synchronous segmentation into source-ordered ranges relative to the supplied text. */
|
|
48
|
+
type Chunker = Readonly<{
|
|
49
|
+
name: string;
|
|
50
|
+
split: (text: string) => Iterable<TextRange>;
|
|
51
|
+
}>;
|
|
52
|
+
/** Bounded recursive splitting; size and overlap use grapheme counts unless measure is supplied. */
|
|
53
|
+
type RecursiveChunkOptions = Readonly<{
|
|
54
|
+
/** Positive integer maximum size of each emitted substring. */
|
|
55
|
+
size: number;
|
|
56
|
+
/** Desired maximum overlap, smaller than size. Defaults to zero. */
|
|
57
|
+
overlap?: number;
|
|
58
|
+
/** Deterministic nonnegative integer size of a substring, e.g. an exact tokenizer's count. */
|
|
59
|
+
measure?: (text: string) => number;
|
|
60
|
+
/** Preferred separators, in order. Defaults to paragraphs, lines, then words. */
|
|
61
|
+
separators?: readonly string[];
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* In-memory corpus of immutable documents and addressable source ranges.
|
|
65
|
+
* {@link Corpus.filter} returns views over the same storage; the filter is evaluated on read.
|
|
66
|
+
*/
|
|
67
|
+
export declare class Corpus<M extends Metadata = Metadata> {
|
|
68
|
+
#private;
|
|
69
|
+
/**
|
|
70
|
+
* Add a document to the shared storage; re-adding identical content is idempotent,
|
|
71
|
+
* different content throws. On a view, the document is visible only if it matches.
|
|
72
|
+
*/
|
|
73
|
+
add(input: DocumentInput<M>): CorpusDocument<M>;
|
|
74
|
+
/** Validate every input, including conflicts, before storing any; then store them in input order. */
|
|
75
|
+
add(inputs: readonly DocumentInput<M>[]): readonly CorpusDocument<M>[];
|
|
76
|
+
/** Read a document. Throws when absent or excluded by this view's filter. */
|
|
77
|
+
document(documentId: string): CorpusDocument<M>;
|
|
78
|
+
/** List every document matching this view's filter, in insertion order. */
|
|
79
|
+
documents(): readonly CorpusDocument<M>[];
|
|
80
|
+
/**
|
|
81
|
+
* Return a view over the same storage that also requires this predicate. Documents
|
|
82
|
+
* added later through any view appear when they match.
|
|
83
|
+
*/
|
|
84
|
+
filter(predicate: (document: CorpusDocument<M>) => boolean): Corpus<M>;
|
|
85
|
+
/**
|
|
86
|
+
* Split a document lazily, validating each range as it is consumed. If a later
|
|
87
|
+
* range is invalid, earlier yielded chunks remain valid and resolvable.
|
|
88
|
+
*/
|
|
89
|
+
chunk(target: DocumentReference, chunker: Chunker): Iterable<Chunk>;
|
|
90
|
+
private splitDocument;
|
|
91
|
+
/** Select an exact range of a document. */
|
|
92
|
+
slice(target: DocumentReference, selection: TextRange): Chunk;
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a chunk ID to its exact text and document. The ID encodes the document and
|
|
95
|
+
* range, so any valid ID resolves while its document exists; resolution ignores this
|
|
96
|
+
* view's filter. Throws for malformed IDs, unknown documents and invalid ranges.
|
|
97
|
+
*/
|
|
98
|
+
resolve(chunkId: string): ResolvedChunk<M>;
|
|
99
|
+
private store;
|
|
100
|
+
private requireStored;
|
|
101
|
+
private requireVisible;
|
|
102
|
+
}
|
|
103
|
+
declare namespace chunkers_d_exports {
|
|
104
|
+
export { recursive };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Construct a bounded splitter that prefers paragraph, line, and word boundaries.
|
|
108
|
+
* Ranges are produced lazily and never split a Unicode grapheme. Custom measures
|
|
109
|
+
* are checked on exact substrings; a grapheme that cannot fit raises RangeError.
|
|
110
|
+
*/
|
|
111
|
+
declare function recursive(options: RecursiveChunkOptions): Chunker;
|
|
112
|
+
export { type Chunk, type ChunkReference, type Chunker, type CorpusDocument, type DocumentInput, type DocumentReference, type DocumentSource, type Metadata, type RecursiveChunkOptions, type ResolvedChunk, type TextRange, chunkers_d_exports as chunkers };
|
package/dist/corpus.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
|
|
2
|
+
// ../corpus/src/records.ts
|
|
3
|
+
function record(value) {
|
|
4
|
+
if (!isRecord(value)) throw new Error("Expected an object");
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function string(value, name) {
|
|
11
|
+
if (typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function makeDocument(value) {
|
|
15
|
+
const input = record(value);
|
|
16
|
+
const documentId = string(input["documentId"], "documentId");
|
|
17
|
+
if (documentId.trim().length === 0) throw new Error("documentId must not be empty");
|
|
18
|
+
const rawMetadata = record(input["metadata"] ?? {});
|
|
19
|
+
if (Object.getPrototypeOf(rawMetadata) !== Object.prototype && Object.getPrototypeOf(rawMetadata) !== null) throw new Error("Metadata must be a plain object");
|
|
20
|
+
const metadata = value.metadata === void 0 ? {} : { ...value.metadata };
|
|
21
|
+
const rawSource = input["source"] === void 0 ? void 0 : record(input["source"]);
|
|
22
|
+
const source = rawSource === void 0 ? void 0 : Object.freeze({
|
|
23
|
+
uri: string(rawSource["uri"], "source.uri"),
|
|
24
|
+
...rawSource["mediaType"] === void 0 ? {} : { mediaType: string(rawSource["mediaType"], "source.mediaType") }
|
|
25
|
+
});
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
documentId,
|
|
28
|
+
text: string(input["text"], "text"),
|
|
29
|
+
...input["title"] === void 0 ? {} : { title: string(input["title"], "title") },
|
|
30
|
+
...source === void 0 ? {} : { source },
|
|
31
|
+
metadata: Object.freeze(metadata)
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function makeChunk(document, range) {
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
id: `${encodeURIComponent(document.documentId)}#${range.start}-${range.end}`,
|
|
37
|
+
documentId: document.documentId,
|
|
38
|
+
range: Object.freeze({
|
|
39
|
+
start: range.start,
|
|
40
|
+
end: range.end
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Parse a chunk ID back into its document and range. Only the canonical spelling is accepted. */
|
|
45
|
+
function parseChunkId(id) {
|
|
46
|
+
const [, encoded, start, end] = /^([^#]+)#(0|[1-9]\d*)-(0|[1-9]\d*)$/u.exec(id) ?? [];
|
|
47
|
+
if (encoded === void 0 || start === void 0 || end === void 0) throw new Error(`Invalid chunk ID: ${id}`);
|
|
48
|
+
let documentId;
|
|
49
|
+
try {
|
|
50
|
+
documentId = decodeURIComponent(encoded);
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error(`Invalid chunk ID: ${id}`);
|
|
53
|
+
}
|
|
54
|
+
if (encodeURIComponent(documentId) !== encoded) throw new Error(`Invalid chunk ID: ${id}`);
|
|
55
|
+
return {
|
|
56
|
+
documentId,
|
|
57
|
+
range: {
|
|
58
|
+
start: Number(start),
|
|
59
|
+
end: Number(end)
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// ../corpus/src/text.ts
|
|
64
|
+
function positiveInteger(value, name) {
|
|
65
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new Error(`${name} must be a positive safe integer`);
|
|
66
|
+
}
|
|
67
|
+
function nonnegativeInteger(value, name) {
|
|
68
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a nonnegative safe integer`);
|
|
69
|
+
}
|
|
70
|
+
function graphemeBoundaries(text) {
|
|
71
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
72
|
+
return [...Array.from(segmenter.segment(text), (segment) => segment.index), text.length];
|
|
73
|
+
}
|
|
74
|
+
function validateRange(range, boundaries) {
|
|
75
|
+
if (!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start >= range.end || !boundaries.has(range.start) || !boundaries.has(range.end)) throw new Error("Range must be nonempty, within the text, and on grapheme boundaries");
|
|
76
|
+
}
|
|
77
|
+
function boundaryAt(boundaries, index) {
|
|
78
|
+
const boundary = boundaries[index];
|
|
79
|
+
if (boundary === void 0) throw new Error("Invalid text boundary index");
|
|
80
|
+
return boundary;
|
|
81
|
+
}
|
|
82
|
+
// ../corpus/src/corpus.ts
|
|
83
|
+
/**
|
|
84
|
+
* In-memory corpus of immutable documents and addressable source ranges.
|
|
85
|
+
* {@link Corpus.filter} returns views over the same storage; the filter is evaluated on read.
|
|
86
|
+
*/
|
|
87
|
+
var Corpus = class Corpus {
|
|
88
|
+
#storage = { documents: /* @__PURE__ */ new Map() };
|
|
89
|
+
#filter = () => true;
|
|
90
|
+
add(input) {
|
|
91
|
+
if (!isBatch(input)) {
|
|
92
|
+
const document = makeDocument(input);
|
|
93
|
+
const existing = this.#storage.documents.get(document.documentId);
|
|
94
|
+
if (existing !== void 0) {
|
|
95
|
+
assertSameContent(existing, document);
|
|
96
|
+
return existing;
|
|
97
|
+
}
|
|
98
|
+
return this.store(document);
|
|
99
|
+
}
|
|
100
|
+
const pending = /* @__PURE__ */ new Map();
|
|
101
|
+
const documents = input.map((item) => {
|
|
102
|
+
const document = makeDocument(item);
|
|
103
|
+
const existing = pending.get(document.documentId) ?? this.#storage.documents.get(document.documentId);
|
|
104
|
+
if (existing !== void 0) {
|
|
105
|
+
assertSameContent(existing, document);
|
|
106
|
+
return existing;
|
|
107
|
+
}
|
|
108
|
+
pending.set(document.documentId, document);
|
|
109
|
+
return document;
|
|
110
|
+
});
|
|
111
|
+
for (const document of pending.values()) this.store(document);
|
|
112
|
+
return Object.freeze(documents);
|
|
113
|
+
}
|
|
114
|
+
/** Read a document. Throws when absent or excluded by this view's filter. */
|
|
115
|
+
document(documentId) {
|
|
116
|
+
return this.requireVisible({ documentId });
|
|
117
|
+
}
|
|
118
|
+
/** List every document matching this view's filter, in insertion order. */
|
|
119
|
+
documents() {
|
|
120
|
+
return Object.freeze([...this.#storage.documents.values()].filter(this.#filter));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Return a view over the same storage that also requires this predicate. Documents
|
|
124
|
+
* added later through any view appear when they match.
|
|
125
|
+
*/
|
|
126
|
+
filter(predicate) {
|
|
127
|
+
const parent = this.#filter;
|
|
128
|
+
const view = new Corpus();
|
|
129
|
+
view.#storage = this.#storage;
|
|
130
|
+
view.#filter = (document) => parent(document) && predicate(document);
|
|
131
|
+
return view;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Split a document lazily, validating each range as it is consumed. If a later
|
|
135
|
+
* range is invalid, earlier yielded chunks remain valid and resolvable.
|
|
136
|
+
*/
|
|
137
|
+
chunk(target, chunker) {
|
|
138
|
+
return this.splitDocument(this.requireVisible(target), chunker);
|
|
139
|
+
}
|
|
140
|
+
*splitDocument(document, chunker) {
|
|
141
|
+
const boundaries = new Set(graphemeBoundaries(document.text));
|
|
142
|
+
let previousStart = -1;
|
|
143
|
+
for (const range of chunker.split(document.text)) {
|
|
144
|
+
validateRange(range, boundaries);
|
|
145
|
+
if (range.start <= previousStart) throw new Error("Chunker ranges must advance in source order");
|
|
146
|
+
previousStart = range.start;
|
|
147
|
+
yield materialize(document, makeChunk(document, range));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Select an exact range of a document. */
|
|
151
|
+
slice(target, selection) {
|
|
152
|
+
const document = this.requireVisible(target);
|
|
153
|
+
validateRange(selection, new Set(graphemeBoundaries(document.text)));
|
|
154
|
+
return materialize(document, makeChunk(document, selection));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve a chunk ID to its exact text and document. The ID encodes the document and
|
|
158
|
+
* range, so any valid ID resolves while its document exists; resolution ignores this
|
|
159
|
+
* view's filter. Throws for malformed IDs, unknown documents and invalid ranges.
|
|
160
|
+
*/
|
|
161
|
+
resolve(chunkId) {
|
|
162
|
+
const { documentId, range } = parseChunkId(chunkId);
|
|
163
|
+
const document = this.requireStored({ documentId });
|
|
164
|
+
validateRange(range, new Set(graphemeBoundaries(document.text)));
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
chunk: materialize(document, makeChunk(document, range)),
|
|
167
|
+
document
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
store(document) {
|
|
171
|
+
this.#storage.documents.set(document.documentId, document);
|
|
172
|
+
return document;
|
|
173
|
+
}
|
|
174
|
+
requireStored(reference) {
|
|
175
|
+
const document = this.#storage.documents.get(reference.documentId);
|
|
176
|
+
if (document === void 0) throw new Error(`Unknown document: ${reference.documentId}`);
|
|
177
|
+
return document;
|
|
178
|
+
}
|
|
179
|
+
requireVisible(reference) {
|
|
180
|
+
const document = this.requireStored(reference);
|
|
181
|
+
if (!this.#filter(document)) throw new Error(`Document "${document.documentId}" is excluded by this corpus view`);
|
|
182
|
+
return document;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
function materialize(document, reference) {
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
...reference,
|
|
188
|
+
text: document.text.slice(reference.range.start, reference.range.end)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function assertSameContent(existing, document) {
|
|
192
|
+
const keys = Object.keys(existing.metadata);
|
|
193
|
+
if (existing.text !== document.text || existing.title !== document.title || existing.source?.uri !== document.source?.uri || existing.source?.mediaType !== document.source?.mediaType || keys.length !== Object.keys(document.metadata).length || !keys.every((key) => Object.hasOwn(document.metadata, key) && Object.is(existing.metadata[key], document.metadata[key]))) throw new Error(`Document "${document.documentId}" already exists with different content`);
|
|
194
|
+
}
|
|
195
|
+
function isBatch(input) {
|
|
196
|
+
return Array.isArray(input);
|
|
197
|
+
}
|
|
198
|
+
// ../corpus/src/chunkers.ts
|
|
199
|
+
var chunkers_exports = /* @__PURE__ */ __exportAll({ recursive: () => recursive });
|
|
200
|
+
/**
|
|
201
|
+
* Construct a bounded splitter that prefers paragraph, line, and word boundaries.
|
|
202
|
+
* Ranges are produced lazily and never split a Unicode grapheme. Custom measures
|
|
203
|
+
* are checked on exact substrings; a grapheme that cannot fit raises RangeError.
|
|
204
|
+
*/
|
|
205
|
+
function recursive(options) {
|
|
206
|
+
const { size, overlap = 0, measure } = options;
|
|
207
|
+
const separators = [...options.separators ?? [
|
|
208
|
+
"\n\n",
|
|
209
|
+
"\n",
|
|
210
|
+
" "
|
|
211
|
+
]];
|
|
212
|
+
positiveInteger(size, "size");
|
|
213
|
+
nonnegativeInteger(overlap, "overlap");
|
|
214
|
+
if (overlap >= size) throw new RangeError("overlap must be smaller than size");
|
|
215
|
+
if (separators.some((separator) => separator.length === 0)) throw new Error("Separators must be nonempty; grapheme fallback is automatic");
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
name: "recursive",
|
|
218
|
+
*split(text) {
|
|
219
|
+
const boundaries = graphemeBoundaries(text);
|
|
220
|
+
const indices = new Map(boundaries.map((offset, index) => [offset, index]));
|
|
221
|
+
const last = boundaries.length - 1;
|
|
222
|
+
function count(start, end) {
|
|
223
|
+
if (measure === void 0) return end - start;
|
|
224
|
+
const value = measure(text.slice(boundaryAt(boundaries, start), boundaryAt(boundaries, end)));
|
|
225
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("measure must return a nonnegative safe integer");
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
function boundedEnd(start) {
|
|
229
|
+
if (measure === void 0) return Math.min(start + size, last);
|
|
230
|
+
let end = start;
|
|
231
|
+
while (end < last && count(start, end + 1) <= size) end += 1;
|
|
232
|
+
if (end === start) throw new RangeError("A single grapheme exceeds the chunk size");
|
|
233
|
+
return end;
|
|
234
|
+
}
|
|
235
|
+
function preferredEnd(start, hardEnd) {
|
|
236
|
+
if (hardEnd === last) return hardEnd;
|
|
237
|
+
const startOffset = boundaryAt(boundaries, start);
|
|
238
|
+
const endOffset = boundaryAt(boundaries, hardEnd);
|
|
239
|
+
for (const separator of separators) {
|
|
240
|
+
const offset = text.lastIndexOf(separator, endOffset - separator.length);
|
|
241
|
+
const candidate = indices.get(offset + separator.length);
|
|
242
|
+
if (offset >= startOffset && candidate !== void 0 && candidate > start && candidate <= hardEnd) return candidate;
|
|
243
|
+
}
|
|
244
|
+
return hardEnd;
|
|
245
|
+
}
|
|
246
|
+
function nextStart(start, end) {
|
|
247
|
+
if (overlap === 0) return end;
|
|
248
|
+
if (measure === void 0) return Math.max(start + 1, end - overlap);
|
|
249
|
+
let next = end;
|
|
250
|
+
while (next > start + 1 && count(next - 1, end) <= overlap) next -= 1;
|
|
251
|
+
return next;
|
|
252
|
+
}
|
|
253
|
+
let start = 0;
|
|
254
|
+
while (start < last) {
|
|
255
|
+
const end = preferredEnd(start, boundedEnd(start));
|
|
256
|
+
yield {
|
|
257
|
+
start: boundaryAt(boundaries, start),
|
|
258
|
+
end: boundaryAt(boundaries, end)
|
|
259
|
+
};
|
|
260
|
+
if (end === last) return;
|
|
261
|
+
start = nextStart(start, end);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
export { Corpus, chunkers_exports as chunkers };
|