@zenera/rag 1.1.10 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -376,6 +376,47 @@ The copies are a record, not an input: rebuilding reads the files you name, not
376
376
  the ones in `sources/`. A **document** index has no `--no-sources`, because
377
377
  there the copies are what every quoted line is read from.
378
378
 
379
+ **Vectors are paid for once.** Embedding is the whole cost of a build — the
380
+ reading and the writing are milliseconds, the round trips are minutes, and they
381
+ are the only part anyone is billed for. Almost none of it is new work: editing
382
+ one paragraph re-embeds every other one unchanged, and a build killed at 90%
383
+ starts again from nothing. So the vectors are kept in this machine's shared
384
+ cache, `~/.zenera/neo/cache/vectors/`, keyed by a hash of the exact text that
385
+ produced them together with the model that made them. Re-indexing an unchanged
386
+ corpus embeds nothing; re-indexing after an edit pays for the chunks that
387
+ changed.
388
+
389
+ The store is the machine's rather than the index's, which is the point: the same
390
+ corpus indexed into a second directory costs nothing the second time, and two
391
+ projects that quote the same handbook pay for it once between them.
392
+
393
+ The key is the text, never the chunk's position — inserting a sentence at the
394
+ top of a document shifts every later chunk's ordinal without changing a word of
395
+ it. Two documents that share a paragraph share its vector. Vectors another model
396
+ made are not evicted, they are simply never asked for, because the model is part
397
+ of the key. It is a cache, so every failure in it is a miss and nothing more.
398
+ `--no-cache` ignores it; `--cache-dir <dir>` keeps the work somewhere else.
399
+
400
+ Nothing is ever evicted by a build: what a corpus stops referring to is still
401
+ work somebody paid for. Getting rid of it is [`zen cache`](../cli/README.md)'s
402
+ job — `zen cache ls` to see what is there, `zen cache prune --older-than 30d` to
403
+ drop what has gone unread.
404
+
405
+ **Parsing is spread across cores, and also cached.** With the vectors cached,
406
+ parsing is what is left: reading a document, cutting it into chunks, and
407
+ recording its shape. It is CPU-bound and every document is independent of every
408
+ other, so a **document** index does it on a pool of worker threads — one per
409
+ core, less the one running the build. The pool is skipped for a handful of
410
+ documents, where starting the threads costs more than it saves.
411
+
412
+ The result is cached the same way the vectors are, under
413
+ `~/.zenera/neo/cache/docs-parse/`. The key covers the file's bytes, the name
414
+ stamped on its chunks, and every setting that decides where a chunk ends, so
415
+ changing `--chunk-tokens` misses on all of it and editing one file misses on one
416
+ file. Both caches are governed by the same `--no-cache` and the same
417
+ `--cache-dir`, and both fall back to doing the work when anything about them
418
+ looks wrong.
419
+
379
420
  ## Notes
380
421
 
381
422
  - Documents are **bundled, not dereferenced**: `#/components/schemas/User` is
@@ -0,0 +1,66 @@
1
+ import type { Embedder } from '@zenera/neo';
2
+ export declare const VECTOR_KIND = "vectors";
3
+ export interface VectorCache {
4
+ /** the vector this text already has, if it has one */
5
+ get(text: string): Float32Array | undefined;
6
+ /** keeps a vector for next time */
7
+ put(text: string, vector: Float32Array): void;
8
+ /** says the entries this build read are still wanted, so age means unused */
9
+ commit(): void;
10
+ /** for a build that failed; entries land as they are paid for, so nothing unwinds */
11
+ abandon(): void;
12
+ readonly hits: number;
13
+ }
14
+ /** A cache that remembers nothing, for `--no-cache`. */
15
+ export declare const NO_CACHE: VectorCache;
16
+ export interface VectorCacheOptions {
17
+ /** the reference as it was typed, which is part of what the vectors mean */
18
+ ref: string;
19
+ /** somewhere other than the shared store */
20
+ dir?: string;
21
+ /** when the caller asked for a width the model does not default to */
22
+ dimensions?: number;
23
+ }
24
+ export declare function openCache(embedder: Embedder, options: VectorCacheOptions): VectorCache;
25
+ export interface CachedEmbedOptions {
26
+ embedder: Embedder;
27
+ cache: VectorCache;
28
+ texts: readonly string[];
29
+ signal?: AbortSignal;
30
+ onProgress?: (done: number, total: number) => void;
31
+ }
32
+ /**
33
+ * Embeds only what the cache does not already have, and hands each request's
34
+ * answer to the cache as it lands rather than at the end, so a build killed
35
+ * half way keeps the half it paid for.
36
+ *
37
+ * Identical texts are embedded once. A corpus repeats itself more than it looks
38
+ * like it does — shared boilerplate, a table copied between two documents — and
39
+ * a duplicate is a whole vector's worth of request for an answer already held.
40
+ */
41
+ export declare function embedCached(options: CachedEmbedOptions): Promise<Float32Array[]>;
42
+ export interface EmbedStreamOptions<R> {
43
+ embedder: Embedder;
44
+ cache: VectorCache;
45
+ records: readonly R[];
46
+ textOf: (record: R) => string;
47
+ window?: number;
48
+ signal?: AbortSignal;
49
+ /** counted over every record, not over the window being worked on */
50
+ onProgress?: (done: number, total: number) => void;
51
+ onWindow: (records: readonly R[], vectors: readonly Float32Array[]) => Promise<void>;
52
+ }
53
+ /**
54
+ * Embeds in windows and hands each one straight to whatever stores it, so that
55
+ * peak memory is the window rather than the corpus. A corpus of 200k chunks at
56
+ * 3072 dimensions held about 12 GB of vectors this way round; it now holds
57
+ * whatever one window is, however large the corpus gets.
58
+ *
59
+ * Duplicate texts spanning two windows still cost one embedding, because the
60
+ * first window has already written them to the cache by the time the second
61
+ * asks. Under `--no-cache` they cost two, which is what `--no-cache` means.
62
+ *
63
+ * Returns the width the model answered with, for the manifest.
64
+ */
65
+ export declare function embedStream<R>(options: EmbedStreamOptions<R>): Promise<number>;
66
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1,172 @@
1
+ import { Cache, cacheKey } from '@zenera/cli/lib';
2
+ // ---------------------------------------------------------------------------
3
+ // Not paying twice for the same vector
4
+ //
5
+ // Embedding is the whole cost of a build: the reading and the writing are
6
+ // seconds, the round trips are minutes and the only part anyone is billed for.
7
+ // And almost none of it is new work. Re-indexing a corpus after editing one
8
+ // paragraph re-embeds every other paragraph unchanged, and a build killed at
9
+ // 90% starts again from nothing.
10
+ //
11
+ // The vectors live in the machine's shared cache, so two indexes built from the
12
+ // same corpus — or one corpus indexed twice into different directories — pay
13
+ // for it once between them.
14
+ //
15
+ // The key is the *text*, together with the model that would embed it. Never the
16
+ // chunk's position: inserting a sentence at the top of a document shifts every
17
+ // later chunk's ordinal without changing a word of it, and an ordinal key would
18
+ // miss all of them. Two documents that happen to share a paragraph share its
19
+ // vector; a different model shares nothing, because it asks a different
20
+ // question and so has a different key. That is also why there is nothing here
21
+ // that invalidates anything.
22
+ //
23
+ // It is a cache, so every error is a miss, and the shared store guarantees that
24
+ // much on its own: nothing in this file can fail a build.
25
+ // ---------------------------------------------------------------------------
26
+ export const VECTOR_KIND = 'vectors';
27
+ /** Bumped only if what a key means changes; the model is already in the key. */
28
+ const VECTOR_VERSION = 'v1';
29
+ /** A cache that remembers nothing, for `--no-cache`. */
30
+ export const NO_CACHE = {
31
+ get: () => undefined,
32
+ put: () => { },
33
+ commit: () => { },
34
+ abandon: () => { },
35
+ hits: 0,
36
+ };
37
+ export function openCache(embedder, options) {
38
+ return new StoredVectors(new Cache(VECTOR_KIND, { dir: options.dir }), embedder, options);
39
+ }
40
+ class StoredVectors {
41
+ #store;
42
+ #prefix;
43
+ constructor(store, embedder, options) {
44
+ this.#store = store;
45
+ this.#prefix = [VECTOR_VERSION, options.ref, embedder.id, options.dimensions];
46
+ }
47
+ get hits() {
48
+ return this.#store.hits;
49
+ }
50
+ /** Everything the vector is a function of, ending with the text itself. */
51
+ #key(text) {
52
+ return cacheKey(...this.#prefix, text);
53
+ }
54
+ get(text) {
55
+ const found = this.#store.get(this.#key(text));
56
+ if (typeof found !== 'string' || found.length === 0) {
57
+ return undefined;
58
+ }
59
+ const bytes = Buffer.from(found, 'base64');
60
+ if (bytes.byteLength === 0 || bytes.byteLength % 4 !== 0) {
61
+ return undefined;
62
+ }
63
+ // Buffer.from can land at any offset in the shared pool, and a
64
+ // Float32Array needs a multiple of four. Slicing copies to its own.
65
+ return new Float32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
66
+ }
67
+ put(text, vector) {
68
+ if (vector.length > 0) {
69
+ const bytes = new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength);
70
+ this.#store.put(this.#key(text), Buffer.from(bytes).toString('base64'));
71
+ }
72
+ }
73
+ commit() {
74
+ this.#store.commit();
75
+ }
76
+ abandon() { }
77
+ }
78
+ /**
79
+ * Embeds only what the cache does not already have, and hands each request's
80
+ * answer to the cache as it lands rather than at the end, so a build killed
81
+ * half way keeps the half it paid for.
82
+ *
83
+ * Identical texts are embedded once. A corpus repeats itself more than it looks
84
+ * like it does — shared boilerplate, a table copied between two documents — and
85
+ * a duplicate is a whole vector's worth of request for an answer already held.
86
+ */
87
+ export async function embedCached(options) {
88
+ const { cache, texts } = options;
89
+ const vectors = new Array(texts.length);
90
+ const wanted = new Map();
91
+ for (const [at, text] of texts.entries()) {
92
+ const hit = cache.get(text);
93
+ if (hit) {
94
+ vectors[at] = hit;
95
+ continue;
96
+ }
97
+ const waiting = wanted.get(text);
98
+ if (waiting) {
99
+ waiting.push(at);
100
+ }
101
+ else {
102
+ wanted.set(text, [at]);
103
+ }
104
+ }
105
+ const input = [...wanted.keys()];
106
+ const known = texts.length - [...wanted.values()].reduce((n, at) => n + at.length, 0);
107
+ if (input.length > 0) {
108
+ const response = await options.embedder.embed({
109
+ input,
110
+ taskType: 'document',
111
+ signal: options.signal,
112
+ onSlice: (at, slice) => {
113
+ for (const [i, vector] of slice.entries()) {
114
+ cache.put(input[at + i], Float32Array.from(vector));
115
+ }
116
+ },
117
+ onProgress: (done) => options.onProgress?.(known + done, texts.length),
118
+ });
119
+ for (const [i, vector] of response.vectors.entries()) {
120
+ const shared = Float32Array.from(vector);
121
+ // `put` rewrites the same bytes, so what a slice already saved costs
122
+ // nothing here. Not every embedder reports slices, and the cache
123
+ // cannot depend on it.
124
+ cache.put(input[i], shared);
125
+ for (const at of wanted.get(input[i])) {
126
+ vectors[at] = shared;
127
+ }
128
+ }
129
+ }
130
+ options.onProgress?.(texts.length, texts.length);
131
+ return vectors;
132
+ }
133
+ /**
134
+ * How many records are embedded, written and let go of before the next are
135
+ * looked at. At 3072 dimensions a window costs about 250 MB while it is in
136
+ * flight, and the embedder still sees enough texts at once to keep every
137
+ * request slot busy.
138
+ */
139
+ const WINDOW = 4096;
140
+ /**
141
+ * Embeds in windows and hands each one straight to whatever stores it, so that
142
+ * peak memory is the window rather than the corpus. A corpus of 200k chunks at
143
+ * 3072 dimensions held about 12 GB of vectors this way round; it now holds
144
+ * whatever one window is, however large the corpus gets.
145
+ *
146
+ * Duplicate texts spanning two windows still cost one embedding, because the
147
+ * first window has already written them to the cache by the time the second
148
+ * asks. Under `--no-cache` they cost two, which is what `--no-cache` means.
149
+ *
150
+ * Returns the width the model answered with, for the manifest.
151
+ */
152
+ export async function embedStream(options) {
153
+ const size = options.window ?? WINDOW;
154
+ const total = options.records.length;
155
+ let dimensions = 0;
156
+ for (let at = 0; at < total; at += size) {
157
+ const window = options.records.slice(at, at + size);
158
+ const vectors = await embedCached({
159
+ embedder: options.embedder,
160
+ cache: options.cache,
161
+ texts: window.map(options.textOf),
162
+ signal: options.signal,
163
+ onProgress: (done) => options.onProgress?.(at + done, total),
164
+ });
165
+ if (dimensions === 0) {
166
+ dimensions = vectors[0]?.length ?? 0;
167
+ }
168
+ await options.onWindow(window, vectors);
169
+ }
170
+ return dimensions;
171
+ }
172
+ //# sourceMappingURL=cache.js.map
@@ -1,3 +1,13 @@
1
1
  import { type Embedder } from '@zenera/neo';
2
- export declare function resolveEmbedder(ref: string | undefined): Promise<Embedder>;
2
+ /**
3
+ * What a shorthand ref cannot say. Both are ceilings on what the embedder would
4
+ * otherwise work out for itself from the model and from what the provider
5
+ * refuses, so leaving them unset is the normal case.
6
+ */
7
+ export interface EmbedderTuning {
8
+ maxBatch?: number;
9
+ /** narrower vectors, where the model is trained to be truncated */
10
+ dimensions?: number;
11
+ }
12
+ export declare function resolveEmbedder(ref: string | undefined, tuning?: EmbedderTuning): Promise<Embedder>;
3
13
  //# sourceMappingURL=embedder.d.ts.map
@@ -1,20 +1,6 @@
1
1
  import { bold, CliError, CURATED, cyan, dim, ensureHome, envNames, form, KeyStore, note, PROVIDERS, table, usageError, } from '@zenera/cli/lib';
2
- import { createEmbedder } from '@zenera/neo';
3
- // ---------------------------------------------------------------------------
4
- // Getting an embedder, and saying what the choices are when there is none
5
- //
6
- // The keyring is materialised here, and only here. The `zen` frame does not do
7
- // it, and a command that forgets to looks exactly like a machine with no key:
8
- // `zen key ls` shows the credential live and the command still says the
9
- // environment variable is not set. Real env always wins, so a shell that names
10
- // a key is never overridden by one on disk.
11
- //
12
- // Nothing in here is about a particular index. Which model made the vectors is
13
- // recorded in the manifest and enforced on every later search, so this only has
14
- // to turn a reference into an embedder — or, given none, into a list worth
15
- // choosing from.
16
- // ---------------------------------------------------------------------------
17
- export async function resolveEmbedder(ref) {
2
+ import { createEmbedder, defaultModels } from '@zenera/neo';
3
+ export async function resolveEmbedder(ref, tuning) {
18
4
  ensureHome();
19
5
  const keys = await KeyStore.open();
20
6
  // Asked before materialising, because materialising is what erases the
@@ -24,7 +10,10 @@ export async function resolveEmbedder(ref) {
24
10
  if (!ref) {
25
11
  throw choices(keys, fromEnv);
26
12
  }
27
- return createEmbedder(ref);
13
+ if (tuning?.maxBatch === undefined && tuning?.dimensions === undefined) {
14
+ return createEmbedder(ref);
15
+ }
16
+ return createEmbedder({ ...defaultModels.parseEmbedding(ref), ...tuning });
28
17
  }
29
18
  /**
30
19
  * Well-known embedding models per provider, read off the CLI's catalog table so
@@ -12,11 +12,17 @@ export interface IndexHead {
12
12
  kind: IndexKind;
13
13
  createdAt: string;
14
14
  indexer: string;
15
- /** `ref` as it was typed, `id` as the embedder answers to it */
15
+ /**
16
+ * `ref` as it was typed, `id` as the embedder answers to it, `dimensions`
17
+ * as the vectors actually came back. `requested` only when a width was
18
+ * asked for out loud: a search has to ask for the same one, and asking for
19
+ * the model's own default is not the same as not asking.
20
+ */
16
21
  embedding: {
17
22
  ref: string;
18
23
  id: string;
19
24
  dimensions: number;
25
+ requested?: number;
20
26
  };
21
27
  /** whether the table carries an fts index, and whether it carries a vector one */
22
28
  indexes: {
@@ -2,6 +2,11 @@ export declare const LOCK_FILE = ".lock";
2
2
  export declare const README_FILE = "README.md";
3
3
  /** The floor on how often README.md is rewritten. */
4
4
  export declare const INTERVAL_MS = 5000;
5
+ /** How long one phase took, by its key rather than its sentence. */
6
+ export interface PhaseTiming {
7
+ name: string;
8
+ ms: number;
9
+ }
5
10
  export interface Building<S> {
6
11
  documents: readonly string[];
7
12
  embedding: string;
@@ -9,8 +14,14 @@ export interface Building<S> {
9
14
  now: number;
10
15
  /** the current phase, as it should be said out loud */
11
16
  step: string;
17
+ /** the current phase's key, for a report that counts different things per phase */
18
+ phase: string;
12
19
  done: number;
13
20
  total: number;
21
+ /** what the phase is still waiting on, when it is able to say */
22
+ pending: readonly string[];
23
+ /** every phase so far; the last is the one still running */
24
+ timings: readonly PhaseTiming[];
14
25
  /** what the documents turned out to hold, once they have been read */
15
26
  summary: S | undefined;
16
27
  }
@@ -18,12 +29,14 @@ export interface Completed<M> {
18
29
  dir: string;
19
30
  manifest: M;
20
31
  ms: number;
32
+ timings: readonly PhaseTiming[];
21
33
  }
22
34
  export interface Failed {
23
35
  documents: readonly string[];
24
36
  step: string;
25
37
  reason: unknown;
26
38
  started: number;
39
+ timings: readonly PhaseTiming[];
27
40
  }
28
41
  /** The three states a README can be in, written by whoever knows the subject. */
29
42
  export interface Report<S, M> {
@@ -45,9 +58,11 @@ export interface BuildPlan<S, M, P extends string> {
45
58
  export interface Journal<S, M, P extends string> {
46
59
  phase(name: P): void;
47
60
  read(summary: S, total: number): void;
48
- progress(done: number, total: number): void;
61
+ progress(done: number, total: number, pending?: readonly string[]): void;
49
62
  finish(manifest: M): void;
50
63
  fail(reason: unknown): void;
64
+ /** what each phase cost, for a caller that wants to say so out loud */
65
+ readonly timings: readonly PhaseTiming[];
51
66
  }
52
67
  /**
53
68
  * Takes the directory, or refuses it. Two builds writing one index would
@@ -46,8 +46,18 @@ export function beginBuild(plan) {
46
46
  let summary;
47
47
  let done = 0;
48
48
  let total = 0;
49
+ let pending = [];
49
50
  let wroteAt = 0;
50
51
  let closed = false;
52
+ const timings = [];
53
+ let phaseAt = started;
54
+ /** Closes the running phase off. Called on every transition, and at the end. */
55
+ const mark = () => {
56
+ const at = Date.now();
57
+ timings.push({ name: phase, ms: at - phaseAt });
58
+ phaseAt = at;
59
+ };
60
+ const sofar = () => [...timings, { name: phase, ms: Date.now() - phaseAt }];
51
61
  const write = (body) => {
52
62
  // Through a temp name, so a reader never catches half a file.
53
63
  const target = join(plan.dir, README_FILE);
@@ -62,8 +72,11 @@ export function beginBuild(plan) {
62
72
  started,
63
73
  now: Date.now(),
64
74
  step: plan.phases[phase],
75
+ phase,
65
76
  done,
66
77
  total,
78
+ pending,
79
+ timings: sofar(),
67
80
  summary,
68
81
  }));
69
82
  const maybe = () => {
@@ -87,7 +100,9 @@ export function beginBuild(plan) {
87
100
  report();
88
101
  return {
89
102
  phase(name) {
103
+ mark();
90
104
  phase = name;
105
+ pending = [];
91
106
  maybe();
92
107
  },
93
108
  read(seen, count) {
@@ -95,16 +110,33 @@ export function beginBuild(plan) {
95
110
  total = count;
96
111
  maybe();
97
112
  },
98
- progress(at, of) {
113
+ progress(at, of, waiting) {
99
114
  done = at;
100
115
  total = of;
116
+ pending = waiting ?? [];
101
117
  maybe();
102
118
  },
103
119
  finish(manifest) {
104
- close(plan.report.complete({ dir: plan.dir, manifest, ms: Date.now() - started }));
120
+ mark();
121
+ close(plan.report.complete({
122
+ dir: plan.dir,
123
+ manifest,
124
+ ms: Date.now() - started,
125
+ timings,
126
+ }));
105
127
  },
106
128
  fail(reason) {
107
- close(plan.report.failed({ documents, step: plan.phases[phase], reason, started }));
129
+ mark();
130
+ close(plan.report.failed({
131
+ documents,
132
+ step: plan.phases[phase],
133
+ reason,
134
+ started,
135
+ timings,
136
+ }));
137
+ },
138
+ get timings() {
139
+ return closed ? timings : sofar();
108
140
  },
109
141
  };
110
142
  }
@@ -4,6 +4,13 @@ export declare function fields(rows: readonly string[][]): string[];
4
4
  export declare function grid(headers: readonly string[], rows: readonly (readonly string[])[]): string[];
5
5
  export declare function plural(n: number, one: string, many?: string): string;
6
6
  export declare function duration(ms: number): string;
7
+ /** Sub-second, because a phase breakdown is read to compare phases against each other. */
8
+ export declare function span(ms: number): string;
9
+ /** Where the time went, which is the only way to know what is worth making faster. */
10
+ export declare const breakdown: (timings: readonly {
11
+ name: string;
12
+ ms: number;
13
+ }[]) => string;
7
14
  /** The first line only: a stack trace in a README helps nobody. */
8
15
  export declare function message(reason: unknown): string;
9
16
  export declare function searched(indexes: {
@@ -36,6 +36,19 @@ export function duration(ms) {
36
36
  ? `${minutes}m${seconds % 60}s`
37
37
  : `${Math.floor(minutes / 60)}h${minutes % 60}m`;
38
38
  }
39
+ /** Sub-second, because a phase breakdown is read to compare phases against each other. */
40
+ export function span(ms) {
41
+ if (ms < 1000) {
42
+ return `${Math.round(ms)}ms`;
43
+ }
44
+ if (ms < 60_000) {
45
+ return `${(ms / 1000).toFixed(1)}s`;
46
+ }
47
+ const seconds = Math.round(ms / 1000);
48
+ return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`;
49
+ }
50
+ /** Where the time went, which is the only way to know what is worth making faster. */
51
+ export const breakdown = (timings) => timings.map((t) => `${t.name} ${span(t.ms)}`).join(' · ');
39
52
  /** The first line only: a stack trace in a README helps nobody. */
40
53
  export function message(reason) {
41
54
  const text = reason instanceof Error ? reason.message : String(reason);
@@ -1,4 +1,5 @@
1
1
  import type { Embedder } from '@zenera/neo';
2
+ import { type PhaseTiming } from '../common/progress.ts';
2
3
  import { type ChunkOptions } from './chunk.ts';
3
4
  import { type Counts, type DocRecord, type Manifest } from './files.ts';
4
5
  import { type Corpus } from './load.ts';
@@ -13,12 +14,23 @@ export interface BuildOptions {
13
14
  embeddingRef?: string;
14
15
  /** told the manifest, so a store can say what wrote it */
15
16
  indexer: string;
16
- /** texts sent to the embedder at once */
17
- batch?: number;
18
17
  chunk?: ChunkOptions;
18
+ /**
19
+ * The width asked of the embedder, when one was asked for. Part of the cache
20
+ * key, because a truncated vector is a different vector; left undefined when
21
+ * nobody asked, because that is a different key again from asking for the
22
+ * number the model would have chosen anyway.
23
+ */
24
+ dimensions?: number;
25
+ /** reuse vectors and parses this machine already has; on by default */
26
+ cache?: boolean;
27
+ /** keep them somewhere other than the shared store */
28
+ cacheDir?: string;
19
29
  signal?: AbortSignal;
20
30
  /** what the documents turned out to hold, before a vector has been paid for */
21
31
  onRead?: (summary: BuildSummary) => void;
32
+ /** documents parsed so far, and what the pool is still on */
33
+ onReading?: (done: number, total: number, pending: readonly string[]) => void;
22
34
  onProgress?: (done: number, total: number) => void;
23
35
  }
24
36
  export interface BuildSummary {
@@ -29,6 +41,14 @@ export interface BuildSummary {
29
41
  export interface BuildResult {
30
42
  manifest: Manifest;
31
43
  chunks: ChunkRecord[];
44
+ /** what each phase cost, so a slow build can say which part was slow */
45
+ timings: readonly PhaseTiming[];
46
+ /** what came out of the shared cache instead of being done again */
47
+ reused: Reused;
48
+ }
49
+ export interface Reused {
50
+ parses: number;
51
+ vectors: number;
32
52
  }
33
53
  export declare function buildIndex(options: BuildOptions): Promise<BuildResult>;
34
54
  //# sourceMappingURL=build.d.ts.map