factbag 0.0.0-alpha.4 → 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.
Files changed (107) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +18 -8
  3. package/dist/corpus.d.ts +112 -0
  4. package/dist/corpus.js +266 -0
  5. package/dist/dsl.d.ts +287 -0
  6. package/dist/dsl.js +445 -0
  7. package/dist/exceptions.d.ts +44 -0
  8. package/dist/exceptions.js +1 -0
  9. package/dist/{core/explanation/types.d.ts → explain.d.ts} +47 -23
  10. package/dist/explain.js +199 -0
  11. package/dist/goal-BP745uKh.d.ts +331 -0
  12. package/dist/judgment-Cr0JcUt5.d.ts +140 -0
  13. package/dist/kb.d.ts +71 -0
  14. package/dist/kb.js +298 -0
  15. package/dist/knowledge-base-Da5HLCsY.d.ts +76 -0
  16. package/dist/programmer-errors-DDfy03Y0.js +71 -0
  17. package/dist/query-Dxm7EpxZ.js +2051 -0
  18. package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
  19. package/dist/types-0ZvnLf2N.js +37 -0
  20. package/package.json +42 -43
  21. package/dist/core/chunk-N2RRSOEN.js +0 -87
  22. package/dist/core/chunk-NDUIQAH4.js +0 -8
  23. package/dist/core/chunk-QOMFY3XB.js +0 -62
  24. package/dist/core/chunk-UVOCOH2I.js +0 -41
  25. package/dist/core/dsl/assert-supported-value.d.ts +0 -2
  26. package/dist/core/dsl/enumeration.d.ts +0 -7
  27. package/dist/core/dsl/formula.d.ts +0 -62
  28. package/dist/core/dsl/index.d.ts +0 -21
  29. package/dist/core/dsl/index.js +0 -226
  30. package/dist/core/dsl/operators/aggregate.d.ts +0 -30
  31. package/dist/core/dsl/operators/arithmetic.d.ts +0 -94
  32. package/dist/core/dsl/operators/comparison.d.ts +0 -23
  33. package/dist/core/dsl/predicate.d.ts +0 -33
  34. package/dist/core/dsl/types.d.ts +0 -61
  35. package/dist/core/dsl/variable.d.ts +0 -3
  36. package/dist/core/exceptions/programmer-errors.d.ts +0 -36
  37. package/dist/core/explanation/explain.d.ts +0 -24
  38. package/dist/core/explanation/index.d.ts +0 -8
  39. package/dist/core/explanation/index.js +0 -227
  40. package/dist/core/knowledge-base/assertions/exclusive-definitions.d.ts +0 -3
  41. package/dist/core/knowledge-base/index.d.ts +0 -13
  42. package/dist/core/knowledge-base/index.js +0 -196
  43. package/dist/core/knowledge-base/knowledge-base.d.ts +0 -81
  44. package/dist/core/query/index.d.ts +0 -10
  45. package/dist/core/query/index.js +0 -2385
  46. package/dist/core/query/query.d.ts +0 -47
  47. package/dist/core/solver/calendar.d.ts +0 -4
  48. package/dist/core/solver/candidate-search.d.ts +0 -14
  49. package/dist/core/solver/goal-value.d.ts +0 -6
  50. package/dist/core/solver/matching.d.ts +0 -10
  51. package/dist/core/solver/program-index.d.ts +0 -17
  52. package/dist/core/solver/query-validation/constructive-recursion.d.ts +0 -3
  53. package/dist/core/solver/query-validation/constructive-variable-use.d.ts +0 -3
  54. package/dist/core/solver/query-validation/context.d.ts +0 -10
  55. package/dist/core/solver/query-validation/index.d.ts +0 -1
  56. package/dist/core/solver/query-validation/utils.d.ts +0 -31
  57. package/dist/core/solver/query-validation/validate-query.d.ts +0 -6
  58. package/dist/core/solver/solve-all.d.ts +0 -6
  59. package/dist/core/solver/solve.d.ts +0 -6
  60. package/dist/core/types/annotations.d.ts +0 -6
  61. package/dist/core/types/atom.d.ts +0 -60
  62. package/dist/core/types/common.d.ts +0 -7
  63. package/dist/core/types/evidence-metadata.d.ts +0 -18
  64. package/dist/core/types/goal.d.ts +0 -121
  65. package/dist/core/types/judgment.d.ts +0 -137
  66. package/dist/core/types/literal.d.ts +0 -25
  67. package/dist/core/types/predicate.d.ts +0 -35
  68. package/dist/core/types/rule.d.ts +0 -16
  69. package/dist/core/types/schema.d.ts +0 -33
  70. package/dist/core/types/variable.d.ts +0 -13
  71. package/dist/core/utils/assert.d.ts +0 -4
  72. package/dist/core/utils/normalize-to-list.d.ts +0 -2
  73. package/dist/corpus/chunkers.d.ts +0 -7
  74. package/dist/corpus/corpus.d.ts +0 -41
  75. package/dist/corpus/index.d.ts +0 -8
  76. package/dist/corpus/index.js +0 -312
  77. package/dist/corpus/records.d.ts +0 -8
  78. package/dist/corpus/text.d.ts +0 -6
  79. package/dist/corpus/types.d.ts +0 -62
  80. package/dist/guards/guarded-kb.d.ts +0 -59
  81. package/dist/guards/index.d.ts +0 -7
  82. package/dist/guards/index.js +0 -121
  83. package/dist/guards/types.d.ts +0 -15
  84. package/dist/jev/chunk-M7RHUI2O.js +0 -15
  85. package/dist/jev/chunk-QEZ4O3YS.js +0 -203
  86. package/dist/jev/chunk-ZAAXPTJJ.js +0 -105
  87. package/dist/jev/classifier/classifiable-predicates.d.ts +0 -12
  88. package/dist/jev/classifier/classifier.d.ts +0 -16
  89. package/dist/jev/classifier/extract-evidence.d.ts +0 -17
  90. package/dist/jev/classifier/filter-predicates.d.ts +0 -9
  91. package/dist/jev/classifier/filter-symbols.d.ts +0 -14
  92. package/dist/jev/classifier/index.d.ts +0 -6
  93. package/dist/jev/classifier/index.js +0 -246
  94. package/dist/jev/classifier/types.d.ts +0 -13
  95. package/dist/jev/config.d.ts +0 -17
  96. package/dist/jev/config.js +0 -18
  97. package/dist/jev/errors.d.ts +0 -22
  98. package/dist/jev/errors.js +0 -6
  99. package/dist/jev/jev/model.d.ts +0 -11
  100. package/dist/jev/jev/prompts.d.ts +0 -29
  101. package/dist/jev/resolve.d.ts +0 -15
  102. package/dist/jev/resolve.js +0 -76
  103. package/dist/jev/settings.d.ts +0 -11
  104. package/dist/jev/types/config.d.ts +0 -18
  105. package/dist/jev/types/model.d.ts +0 -58
  106. package/dist/jev/types/run.d.ts +0 -9
  107. package/dist/jev/utils.d.ts +0 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
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
+
3
18
  ## 0.0.0-alpha.4
4
19
 
5
20
  ### 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.6.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 { and, enumeration, predicate, variable } from "factbag/dsl";
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
 
@@ -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/resolve";
295
+ import { resolver } from "@factbag/jev";
286
296
 
287
297
  query(run.kb, isQualifiedInvestor({ person: alice })).value; // "conflicted"
288
298
 
@@ -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 };