factbag 0.0.0-alpha.3 → 0.0.0-alpha.5
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 +41 -0
- package/README.md +22 -12
- package/dist/corpus.d.ts +112 -0
- package/dist/corpus.js +266 -0
- package/dist/dsl.d.ts +287 -0
- package/dist/dsl.js +445 -0
- package/dist/exceptions.d.ts +44 -0
- package/dist/exceptions.js +1 -0
- package/dist/{core/explanation/types.d.ts → explain.d.ts} +47 -23
- package/dist/explain.js +199 -0
- package/dist/goal-BP745uKh.d.ts +331 -0
- package/dist/judgment-Cr0JcUt5.d.ts +140 -0
- package/dist/kb.d.ts +71 -0
- package/dist/kb.js +298 -0
- package/dist/knowledge-base-Da5HLCsY.d.ts +76 -0
- package/dist/programmer-errors-DDfy03Y0.js +71 -0
- package/dist/query-Dxm7EpxZ.js +2051 -0
- package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
- package/dist/types-0ZvnLf2N.js +37 -0
- package/package.json +42 -39
- package/dist/core/chunk-NDUIQAH4.js +0 -8
- package/dist/core/chunk-QOMFY3XB.js +0 -62
- package/dist/core/chunk-RMQ3YX7V.js +0 -87
- 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 -3
- 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 -211
- 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/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-PISXBJOT.js +0 -206
- package/dist/jev/classifier.d.ts +0 -28
- package/dist/jev/classifier.js +0 -171
- package/dist/jev/client.d.ts +0 -62
- package/dist/jev/errors.d.ts +0 -22
- package/dist/jev/errors.js +0 -6
- package/dist/jev/jobs.d.ts +0 -61
- package/dist/jev/resolve.d.ts +0 -82
- package/dist/jev/resolve.js +0 -99
- package/dist/jev/tools.d.ts +0 -9
- package/dist/jev/types.d.ts +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# factbag
|
|
2
2
|
|
|
3
|
+
## 0.0.0-alpha.5
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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`.
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
10
|
+
**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.
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
Jev's `createConfig` now returns `Normalized & Config`, so `client` is no longer guaranteed on the result.
|
|
15
|
+
|
|
16
|
+
- 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`.
|
|
17
|
+
|
|
18
|
+
## 0.0.0-alpha.4
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- 2814aab: Allow enumerations to hold numbers or dates as well as strings, with one member
|
|
23
|
+
type per enumeration. Members must be unique; dates compare by instant and are
|
|
24
|
+
copied on construction. NaN and invalid dates are rejected. Enumeration membership
|
|
25
|
+
validation at assertion and query time is unchanged.
|
|
26
|
+
- 1f5bb1b: Classify predicates whose fields are number or Date enumerations, alongside
|
|
27
|
+
string enumerations. Dates are sent to the model as ISO strings and retained as
|
|
28
|
+
`Date` values in evidence. Open-ended `String`, `Number`, and `Date` fields
|
|
29
|
+
remain unsupported.
|
|
30
|
+
- 9ca1766: Reorganize classification and resolution to separate evidence workflows from model requests. The workflows own evidence admission and knowledge-base updates; the Jev implementation owns prompts, encoding, batching, and response validation.
|
|
31
|
+
|
|
32
|
+
**Breaking changes:** call the classifier with `classifier({ corpus, input, config })`. Classification and resolution jobs now expose only `execute(): Promise<void>`; read the resulting knowledge base from `run.kb`. Job metadata and assessment and resolution reports are removed. Replace `RequestConfig`, `ResolutionConfig`, and nested `AdmissionPolicy` settings with the shared, flat `Config`. Pass an SDK client directly instead of implementing the removed custom client interfaces.
|
|
33
|
+
|
|
34
|
+
Classification now executes one job per chunk across all permitted predicates. All responses for the chunk must validate before its evidence is written. Failed jobs can be retried without partial evidence, repeating the chunk's requests; successful repeats add fresh evidence.
|
|
35
|
+
|
|
36
|
+
Optionally filter candidates by passage relevance with `config: { routing: true }`. Each chunk first asks which predicates the passage may bear on. For predicates that make claims about supplied entities or values, it can also filter those symbols before combining them into complete candidate facts. `candidateProductCutoff` controls when this extra filtering runs. Filtering defaults to off: false negatives can suppress evidence, including denials, and the extra requests do not guarantee cost savings.
|
|
37
|
+
|
|
38
|
+
Expose `createConfig`, `Config`, and shared `DEFAULT_*` constants through `factbag/jev/config`. Normalize settings once and preserve a shared lazy client when a factory-created config is reused across workflows.
|
|
39
|
+
|
|
40
|
+
Export `fieldEquals` from `factbag/dsl` and use core equality in Jev: entities compare by constructor and ID, dates by instant. Resolution now groups contradictory evidence about separate instances of the same entity.
|
|
41
|
+
|
|
42
|
+
Encode candidate values when building requests, using the original entities and dates instead of construction-time JSON snapshots. Candidate domains remain fixed at run construction.
|
|
43
|
+
|
|
3
44
|
## 0.0.0-alpha.3
|
|
4
45
|
|
|
5
46
|
### 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(
|
|
@@ -79,7 +78,7 @@ class Person {
|
|
|
79
78
|
|
|
80
79
|
const hasIncomeOver200k = predicate(
|
|
81
80
|
"has_income_over_200k",
|
|
82
|
-
{ person: Person, year: enumeration(
|
|
81
|
+
{ person: Person, year: enumeration(2024, 2025) },
|
|
83
82
|
{
|
|
84
83
|
description:
|
|
85
84
|
"The person's total individual annual income exceeded USD 200,000 in the " +
|
|
@@ -112,13 +111,16 @@ 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
|
|
|
118
120
|
kb.rule(isQualifiedInvestor({ person })).if(
|
|
119
121
|
and(
|
|
120
|
-
hasIncomeOver200k({ person, year:
|
|
121
|
-
hasIncomeOver200k({ person, year:
|
|
122
|
+
hasIncomeOver200k({ person, year: 2024 }),
|
|
123
|
+
hasIncomeOver200k({ person, year: 2025 }),
|
|
122
124
|
expectsSameIncome({ person }),
|
|
123
125
|
),
|
|
124
126
|
);
|
|
@@ -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,7 +176,9 @@ const incomeEvidence = guard(kb)
|
|
|
170
176
|
### 6. Ask Jev to assess the income evidence
|
|
171
177
|
|
|
172
178
|
```ts
|
|
173
|
-
|
|
179
|
+
import { classifier } from "@factbag/jev";
|
|
180
|
+
|
|
181
|
+
const run = classifier({ corpus, input: incomeEvidence });
|
|
174
182
|
|
|
175
183
|
for (const job of run) {
|
|
176
184
|
await job.execute(); // Makes a live Jev request.
|
|
@@ -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
|
|
|
@@ -282,7 +292,7 @@ Evidence judged superseded is retracted from the resolver's output
|
|
|
282
292
|
knowledge base.
|
|
283
293
|
|
|
284
294
|
```ts
|
|
285
|
-
import { resolver } from "factbag/jev
|
|
295
|
+
import { resolver } from "@factbag/jev";
|
|
286
296
|
|
|
287
297
|
query(run.kb, isQualifiedInvestor({ person: alice })).value; // "conflicted"
|
|
288
298
|
|
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 };
|