@deepwatch/dsh-tools 0.1.0
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/LICENSE +21 -0
- package/README.md +106 -0
- package/lib/browser.d.ts +38 -0
- package/lib/browser.js +299 -0
- package/lib/index.d.ts +129 -0
- package/lib/index.js +710 -0
- package/lib/library-generations.d.ts +107 -0
- package/lib/library-generations.js +227 -0
- package/lib/library-search.d.ts +143 -0
- package/lib/library-search.js +407 -0
- package/lib/memory.d.ts +23 -0
- package/lib/memory.js +96 -0
- package/lib/read-plane.d.ts +237 -0
- package/lib/read-plane.js +688 -0
- package/lib/receipt-journal.d.ts +101 -0
- package/lib/receipt-journal.js +246 -0
- package/lib/sensory.d.ts +48 -0
- package/lib/sensory.js +277 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +649 -0
- package/lib/typert.remote-client.d.ts +32 -0
- package/lib/typert.remote-client.d.ts.map +1 -0
- package/lib/typert.remote-client.js +405 -0
- package/package.json +83 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Library index, and the only thing allowed to replace it.
|
|
3
|
+
*
|
|
4
|
+
* The host used to build its index once per process. Records written after
|
|
5
|
+
* start were invisible until somebody restarted the application, and the
|
|
6
|
+
* surface's rebuild control could not reach the host's index at all — so it
|
|
7
|
+
* said "Search again", which was honest and useless. This is the capability
|
|
8
|
+
* that makes it a real one.
|
|
9
|
+
*
|
|
10
|
+
* Four properties, and each exists because the obvious implementation gets it
|
|
11
|
+
* wrong.
|
|
12
|
+
*
|
|
13
|
+
* **A rebuild is an explicit operation.** Not a flag on a search. A search that
|
|
14
|
+
* might re-read the corpus is a search whose cost nobody can predict, and it
|
|
15
|
+
* leaves a caller no way to say "answer from what you have".
|
|
16
|
+
*
|
|
17
|
+
* **One rebuild at a time, and callers join it.** Two people pressing Refresh
|
|
18
|
+
* must not read the directory twice. A second caller waits on the first
|
|
19
|
+
* rebuild and receives its outcome, which is also why a repeated request id
|
|
20
|
+
* returns the recorded answer rather than starting anything.
|
|
21
|
+
*
|
|
22
|
+
* **Cancellation is by reference count.** A caller that stops waiting has
|
|
23
|
+
* withdrawn, not cancelled — the work may still be wanted by somebody else.
|
|
24
|
+
* The rebuild is abandoned when the last waiter leaves, and only then.
|
|
25
|
+
*
|
|
26
|
+
* **The swap is atomic and conditional.** The new index is built beside the
|
|
27
|
+
* one in service and installed only when it is complete and healthy. A failed
|
|
28
|
+
* or abandoned rebuild leaves the previous generation searchable, which is the
|
|
29
|
+
* difference between a refresh that did not work and a Library that broke.
|
|
30
|
+
*
|
|
31
|
+
* @module @deepwatch/dsh-tools/library-generations
|
|
32
|
+
*/
|
|
33
|
+
import type { IndexableRecord, LibraryIndex } from '@deepwatch/dsh-library';
|
|
34
|
+
import type { LibraryIndexState } from '@deepwatch/dsh-contracts/query/wire';
|
|
35
|
+
/** One built index, and what is true about it. */
|
|
36
|
+
export interface IndexGeneration {
|
|
37
|
+
/** Increments only when a healthy rebuild was swapped into service. */
|
|
38
|
+
readonly generation: number;
|
|
39
|
+
readonly startedAt: string;
|
|
40
|
+
readonly completedAt: string | null;
|
|
41
|
+
readonly sourceCount: number;
|
|
42
|
+
readonly recordCount: number;
|
|
43
|
+
readonly indexState: LibraryIndexState;
|
|
44
|
+
/** By filename, never by path. */
|
|
45
|
+
readonly skipped: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/** What a refresh did. Every one of these leaves a searchable Library. */
|
|
48
|
+
export type RefreshOutcome = {
|
|
49
|
+
readonly kind: 'refreshed';
|
|
50
|
+
readonly index: IndexGeneration;
|
|
51
|
+
} | {
|
|
52
|
+
readonly kind: 'cancelled';
|
|
53
|
+
readonly index: IndexGeneration;
|
|
54
|
+
} | {
|
|
55
|
+
readonly kind: 'failed';
|
|
56
|
+
readonly reason: string;
|
|
57
|
+
readonly index: IndexGeneration;
|
|
58
|
+
};
|
|
59
|
+
/** What the service needs to build an index and to timestamp what it built. */
|
|
60
|
+
export interface GenerationsConfig {
|
|
61
|
+
readonly roots: readonly string[];
|
|
62
|
+
/** Injected in tests so a generation record is comparable across runs. */
|
|
63
|
+
readonly now?: () => string;
|
|
64
|
+
/** Injected in tests to exercise failure and slowness without a filesystem. */
|
|
65
|
+
readonly build?: (roots: readonly string[], signal: AbortSignal) => Promise<{
|
|
66
|
+
readonly index: LibraryIndex | null;
|
|
67
|
+
readonly skipped: readonly string[];
|
|
68
|
+
readonly sourceCount: number;
|
|
69
|
+
}>;
|
|
70
|
+
}
|
|
71
|
+
/** The index in service, and the one operation that may replace it. */
|
|
72
|
+
export declare class LibraryGenerations {
|
|
73
|
+
#private;
|
|
74
|
+
constructor(config: GenerationsConfig);
|
|
75
|
+
/**
|
|
76
|
+
* Index one record the Host made, now, without a rebuild.
|
|
77
|
+
*
|
|
78
|
+
* The failure this closes: the evaluation produced 76 tool actions and a
|
|
79
|
+
* Library with nothing in it, because nothing indexed a receipt until
|
|
80
|
+
* somebody pressed Refresh — and Refresh read roots that did not contain
|
|
81
|
+
* them yet.
|
|
82
|
+
*/
|
|
83
|
+
addLive(record: IndexableRecord): void;
|
|
84
|
+
/** How many live records are being carried across rebuilds. */
|
|
85
|
+
liveCount(): number;
|
|
86
|
+
/**
|
|
87
|
+
* The index searches answer from, built on first use.
|
|
88
|
+
*
|
|
89
|
+
* Lazy rather than eager: a profile that never opens the Library should not
|
|
90
|
+
* pay for reading its evidence roots at boot.
|
|
91
|
+
*/
|
|
92
|
+
index(): LibraryIndex;
|
|
93
|
+
/** What is in service. Building it first if nothing is. */
|
|
94
|
+
generation(): IndexGeneration;
|
|
95
|
+
/**
|
|
96
|
+
* Read the roots again and, if that succeeds, put the result into service.
|
|
97
|
+
*
|
|
98
|
+
* `requestId` makes the operation idempotent: a caller that retries after a
|
|
99
|
+
* dropped connection gets the answer its first attempt produced rather than
|
|
100
|
+
* a second read of the corpus. `signal` is that caller's withdrawal, not a
|
|
101
|
+
* cancellation of the work — see the note on reference counting above.
|
|
102
|
+
*/
|
|
103
|
+
refresh(requestId: string, signal: AbortSignal): Promise<RefreshOutcome>;
|
|
104
|
+
/** Whether a rebuild is running. Reported to a surface as bounded progress. */
|
|
105
|
+
rebuilding(): boolean;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=library-generations.d.ts.map
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Library index, and the only thing allowed to replace it.
|
|
3
|
+
*
|
|
4
|
+
* The host used to build its index once per process. Records written after
|
|
5
|
+
* start were invisible until somebody restarted the application, and the
|
|
6
|
+
* surface's rebuild control could not reach the host's index at all — so it
|
|
7
|
+
* said "Search again", which was honest and useless. This is the capability
|
|
8
|
+
* that makes it a real one.
|
|
9
|
+
*
|
|
10
|
+
* Four properties, and each exists because the obvious implementation gets it
|
|
11
|
+
* wrong.
|
|
12
|
+
*
|
|
13
|
+
* **A rebuild is an explicit operation.** Not a flag on a search. A search that
|
|
14
|
+
* might re-read the corpus is a search whose cost nobody can predict, and it
|
|
15
|
+
* leaves a caller no way to say "answer from what you have".
|
|
16
|
+
*
|
|
17
|
+
* **One rebuild at a time, and callers join it.** Two people pressing Refresh
|
|
18
|
+
* must not read the directory twice. A second caller waits on the first
|
|
19
|
+
* rebuild and receives its outcome, which is also why a repeated request id
|
|
20
|
+
* returns the recorded answer rather than starting anything.
|
|
21
|
+
*
|
|
22
|
+
* **Cancellation is by reference count.** A caller that stops waiting has
|
|
23
|
+
* withdrawn, not cancelled — the work may still be wanted by somebody else.
|
|
24
|
+
* The rebuild is abandoned when the last waiter leaves, and only then.
|
|
25
|
+
*
|
|
26
|
+
* **The swap is atomic and conditional.** The new index is built beside the
|
|
27
|
+
* one in service and installed only when it is complete and healthy. A failed
|
|
28
|
+
* or abandoned rebuild leaves the previous generation searchable, which is the
|
|
29
|
+
* difference between a refresh that did not work and a Library that broke.
|
|
30
|
+
*
|
|
31
|
+
* @module @deepwatch/dsh-tools/library-generations
|
|
32
|
+
*/
|
|
33
|
+
import { buildIndex, buildIndexCancellable } from './library-search.js';
|
|
34
|
+
import { wireIndexState } from './read-plane.js';
|
|
35
|
+
/** How many settled request ids are remembered for idempotency. */
|
|
36
|
+
const REMEMBERED_REQUESTS = 64;
|
|
37
|
+
/** The index in service, and the one operation that may replace it. */
|
|
38
|
+
export class LibraryGenerations {
|
|
39
|
+
#config;
|
|
40
|
+
#current = null;
|
|
41
|
+
#inFlight = null;
|
|
42
|
+
#settled = new Map();
|
|
43
|
+
#nextGeneration = 1;
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.#config = config;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Records the Host produced while running, kept across rebuilds.
|
|
49
|
+
*
|
|
50
|
+
* An execution receipt is minted by this Host, in memory, seconds ago; a
|
|
51
|
+
* rebuild reads the evidence roots on disk. Without this, indexing a receipt
|
|
52
|
+
* and then pressing Refresh would lose it, and the Library would go from
|
|
53
|
+
* "searchable" to "empty" for no reason a person could see. Re-applied after
|
|
54
|
+
* every build, so Refresh converges on the same answer rather than racing
|
|
55
|
+
* whatever arrived while it ran.
|
|
56
|
+
*/
|
|
57
|
+
#live = new Map();
|
|
58
|
+
/**
|
|
59
|
+
* Index one record the Host made, now, without a rebuild.
|
|
60
|
+
*
|
|
61
|
+
* The failure this closes: the evaluation produced 76 tool actions and a
|
|
62
|
+
* Library with nothing in it, because nothing indexed a receipt until
|
|
63
|
+
* somebody pressed Refresh — and Refresh read roots that did not contain
|
|
64
|
+
* them yet.
|
|
65
|
+
*/
|
|
66
|
+
addLive(record) {
|
|
67
|
+
this.#live.set(record.recordId, record);
|
|
68
|
+
this.index().add(record);
|
|
69
|
+
}
|
|
70
|
+
/** How many live records are being carried across rebuilds. */
|
|
71
|
+
liveCount() {
|
|
72
|
+
return this.#live.size;
|
|
73
|
+
}
|
|
74
|
+
/** Re-apply the live records to a freshly built index. */
|
|
75
|
+
#applyLive(index) {
|
|
76
|
+
for (const record of this.#live.values())
|
|
77
|
+
index.add(record);
|
|
78
|
+
}
|
|
79
|
+
/** The clock, injectable so a test can assert on a generation record. */
|
|
80
|
+
#now() {
|
|
81
|
+
return this.#config.now?.() ?? new Date().toISOString();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The index searches answer from, built on first use.
|
|
85
|
+
*
|
|
86
|
+
* Lazy rather than eager: a profile that never opens the Library should not
|
|
87
|
+
* pay for reading its evidence roots at boot.
|
|
88
|
+
*/
|
|
89
|
+
index() {
|
|
90
|
+
if (this.#current === null) {
|
|
91
|
+
const startedAt = this.#now();
|
|
92
|
+
const built = buildIndex(this.#config.roots);
|
|
93
|
+
this.#applyLive(built.index);
|
|
94
|
+
this.#current = {
|
|
95
|
+
index: built.index,
|
|
96
|
+
meta: this.#describe(this.#nextGeneration, startedAt, built.index, built.skipped),
|
|
97
|
+
};
|
|
98
|
+
this.#nextGeneration += 1;
|
|
99
|
+
}
|
|
100
|
+
return this.#current.index;
|
|
101
|
+
}
|
|
102
|
+
/** What is in service. Building it first if nothing is. */
|
|
103
|
+
generation() {
|
|
104
|
+
this.index();
|
|
105
|
+
// `index()` above installs it, so this cannot be null. Reading through a
|
|
106
|
+
// local keeps that obvious to the compiler as well as to a reader.
|
|
107
|
+
const current = this.#current;
|
|
108
|
+
if (current === null)
|
|
109
|
+
throw new Error('library: the index did not install');
|
|
110
|
+
return current.meta;
|
|
111
|
+
}
|
|
112
|
+
#describe(generation, startedAt, index, skipped) {
|
|
113
|
+
return {
|
|
114
|
+
generation,
|
|
115
|
+
startedAt,
|
|
116
|
+
completedAt: this.#now(),
|
|
117
|
+
sourceCount: this.#config.roots.length,
|
|
118
|
+
recordCount: index.size,
|
|
119
|
+
// The index's own word for its condition, mapped to the wire's. A
|
|
120
|
+
// rebuild that read nothing is empty, not ready: an index nobody has
|
|
121
|
+
// filled is not a complete answer to anything.
|
|
122
|
+
indexState: wireIndexState(index.health, index.size),
|
|
123
|
+
skipped,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Read the roots again and, if that succeeds, put the result into service.
|
|
128
|
+
*
|
|
129
|
+
* `requestId` makes the operation idempotent: a caller that retries after a
|
|
130
|
+
* dropped connection gets the answer its first attempt produced rather than
|
|
131
|
+
* a second read of the corpus. `signal` is that caller's withdrawal, not a
|
|
132
|
+
* cancellation of the work — see the note on reference counting above.
|
|
133
|
+
*/
|
|
134
|
+
async refresh(requestId, signal) {
|
|
135
|
+
const remembered = this.#settled.get(requestId);
|
|
136
|
+
if (remembered !== undefined)
|
|
137
|
+
return remembered;
|
|
138
|
+
const flight = this.#inFlight ?? this.#start();
|
|
139
|
+
flight.waiters += 1;
|
|
140
|
+
const withdraw = () => {
|
|
141
|
+
flight.waiters -= 1;
|
|
142
|
+
// The last waiter leaving is what ends the work. While anybody is still
|
|
143
|
+
// waiting, one caller's deadline is not everybody's.
|
|
144
|
+
if (flight.waiters <= 0)
|
|
145
|
+
flight.controller.abort();
|
|
146
|
+
};
|
|
147
|
+
if (signal.aborted)
|
|
148
|
+
withdraw();
|
|
149
|
+
else
|
|
150
|
+
signal.addEventListener('abort', withdraw, { once: true });
|
|
151
|
+
try {
|
|
152
|
+
const outcome = await flight.promise;
|
|
153
|
+
this.#remember(requestId, outcome);
|
|
154
|
+
return outcome;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
if (!signal.aborted)
|
|
158
|
+
signal.removeEventListener('abort', withdraw);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Whether a rebuild is running. Reported to a surface as bounded progress. */
|
|
162
|
+
rebuilding() {
|
|
163
|
+
return this.#inFlight !== null;
|
|
164
|
+
}
|
|
165
|
+
#remember(requestId, outcome) {
|
|
166
|
+
this.#settled.set(requestId, outcome);
|
|
167
|
+
// Bounded: a long-lived host must not accumulate one entry per refresh
|
|
168
|
+
// anybody has ever asked for. Oldest first, which is insertion order.
|
|
169
|
+
while (this.#settled.size > REMEMBERED_REQUESTS) {
|
|
170
|
+
const oldest = this.#settled.keys().next();
|
|
171
|
+
if (oldest.done === true)
|
|
172
|
+
break;
|
|
173
|
+
this.#settled.delete(oldest.value);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
#start() {
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const startedAt = this.#now();
|
|
179
|
+
const build = this.#config.build ?? buildIndexCancellable;
|
|
180
|
+
// The generation in service before this ran, read now rather than after
|
|
181
|
+
// the await. Every outcome below reports it, so a caller always learns
|
|
182
|
+
// what it can still search.
|
|
183
|
+
const previous = this.generation();
|
|
184
|
+
// Declared before the body so the body can compare against it, and
|
|
185
|
+
// assigned immediately after. `run` reads it only in its `finally`, which
|
|
186
|
+
// cannot be reached before the first await.
|
|
187
|
+
let flight = null;
|
|
188
|
+
const run = async () => {
|
|
189
|
+
try {
|
|
190
|
+
const built = await build(this.#config.roots, controller.signal);
|
|
191
|
+
if (built.index === null)
|
|
192
|
+
return { kind: 'cancelled', index: previous };
|
|
193
|
+
// Before the generation is described, so its counts include them and a
|
|
194
|
+
// surface comparing generations is comparing the whole answer. A
|
|
195
|
+
// rebuild reads the roots; the receipts this Host minted while running
|
|
196
|
+
// are not there yet, and dropping them would take a searchable Library
|
|
197
|
+
// back to empty for no reason a person could see.
|
|
198
|
+
this.#applyLive(built.index);
|
|
199
|
+
const meta = this.#describe(this.#nextGeneration, startedAt, built.index, built.skipped);
|
|
200
|
+
this.#nextGeneration += 1;
|
|
201
|
+
// The swap. One assignment, after the new index is complete, so no
|
|
202
|
+
// search can ever observe a half-built one.
|
|
203
|
+
this.#current = { index: built.index, meta };
|
|
204
|
+
return { kind: 'refreshed', index: meta };
|
|
205
|
+
}
|
|
206
|
+
catch (cause) {
|
|
207
|
+
return {
|
|
208
|
+
kind: 'failed',
|
|
209
|
+
// The message and nothing else: a stack or an errno string would
|
|
210
|
+
// name the host's directories.
|
|
211
|
+
reason: cause instanceof Error ? cause.message : 'the rebuild failed',
|
|
212
|
+
index: previous,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
// Only if this is still the current flight. A later refresh that has
|
|
217
|
+
// already started must not be cleared by an earlier one finishing.
|
|
218
|
+
if (this.#inFlight === flight)
|
|
219
|
+
this.#inFlight = null;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
flight = { controller, waiters: 0, promise: run() };
|
|
223
|
+
this.#inFlight = flight;
|
|
224
|
+
return flight;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=library-generations.js.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library search, as a tool the host owns.
|
|
3
|
+
*
|
|
4
|
+
* The index has to live somewhere that can read the evidence store, and the
|
|
5
|
+
* browser cannot: a client plugin receives no config, `ctx.remote` is an event
|
|
6
|
+
* bus rather than a query client, and the boot graph carries no data. The host
|
|
7
|
+
* can read the store, so the host holds the index and the agent reaches it the
|
|
8
|
+
* way it reaches everything else in Watch — as a tool.
|
|
9
|
+
*
|
|
10
|
+
* It is the same `LibraryIndex` the client surface uses. One implementation,
|
|
11
|
+
* one set of semantics, one place where "every term must match" is decided;
|
|
12
|
+
* two would drift within a release and disagree about what the library
|
|
13
|
+
* contains.
|
|
14
|
+
*
|
|
15
|
+
* Three things it will not do.
|
|
16
|
+
*
|
|
17
|
+
* It reads only inside the roots it was configured with. `isWithinRoots`
|
|
18
|
+
* refuses traversal rather than normalising it, because normalising an attempt
|
|
19
|
+
* to escape produces a path that works.
|
|
20
|
+
*
|
|
21
|
+
* It returns no verdict. A search result is a pointer to a record, and whether
|
|
22
|
+
* that record's claim is true is `watch_verify`'s question. A tool that
|
|
23
|
+
* answered both would let a search become an assertion.
|
|
24
|
+
*
|
|
25
|
+
* And it never rebuilds silently. A stale or corrupt index is reported as such
|
|
26
|
+
* in the result, because a search that quietly returns less than it should is
|
|
27
|
+
* worse than one that says it is behind.
|
|
28
|
+
*
|
|
29
|
+
* @module @deepwatch/dsh-tools/library-search
|
|
30
|
+
*/
|
|
31
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
32
|
+
import { LibraryIndex } from '@deepwatch/dsh-library';
|
|
33
|
+
import type { IndexableRecord } from '@deepwatch/dsh-library';
|
|
34
|
+
/** Where the index is allowed to read from. */
|
|
35
|
+
export interface LibrarySearchConfig {
|
|
36
|
+
/**
|
|
37
|
+
* Directories holding evidence records.
|
|
38
|
+
*
|
|
39
|
+
* Nothing outside these is read, whatever a filename claims. An empty list
|
|
40
|
+
* means the tool has nothing to index and says so rather than falling back
|
|
41
|
+
* to somewhere convenient.
|
|
42
|
+
*/
|
|
43
|
+
readonly roots: readonly string[];
|
|
44
|
+
/**
|
|
45
|
+
* Who owns the index, when somebody does.
|
|
46
|
+
*
|
|
47
|
+
* The tool used to hold its own, which was fine while it was the only
|
|
48
|
+
* reader. It is not: the read plane answers the same corpus for the Library
|
|
49
|
+
* surface, and a refresh asked for by either has to be the same refresh.
|
|
50
|
+
* Two caches would drift inside one release and disagree about what the
|
|
51
|
+
* library contains — a person searching the UI and the agent searching the
|
|
52
|
+
* tool getting different answers to the same question.
|
|
53
|
+
*/
|
|
54
|
+
readonly generations?: LibraryGenerationsLike;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The part of `LibraryGenerations` this module needs.
|
|
58
|
+
*
|
|
59
|
+
* Structural rather than an import of the class, because the class imports the
|
|
60
|
+
* builders from here and a direct edge would close a cycle between two modules
|
|
61
|
+
* that are one concern.
|
|
62
|
+
*/
|
|
63
|
+
export interface LibraryGenerationsLike {
|
|
64
|
+
index(): LibraryIndex;
|
|
65
|
+
refresh(requestId: string, signal: AbortSignal): Promise<unknown>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read one JSON file into a record, or nothing.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately total: a malformed file, an unreadable one, or one holding
|
|
71
|
+
* something that is not a record yields no record rather than throwing. One bad
|
|
72
|
+
* file must not stop the whole library being searchable — a corpus is exactly
|
|
73
|
+
* where a single malformed entry is most likely and least excusable as a
|
|
74
|
+
* failure mode.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Every string in a record, at any depth.
|
|
78
|
+
*
|
|
79
|
+
* Reading only the top level looked reasonable and was wrong: a citation's
|
|
80
|
+
* text, a check's detail and a revision's transcript all live one or two
|
|
81
|
+
* levels down, so a phrase plainly present in the file returned nothing.
|
|
82
|
+
*
|
|
83
|
+
* Bounded on purpose. A record is data, not a program, so a deeply nested or
|
|
84
|
+
* self-referential one costs a fixed amount of work rather than a stack
|
|
85
|
+
* overflow — and a malformed file is exactly where that would show up.
|
|
86
|
+
*/
|
|
87
|
+
export declare function gatherText(value: unknown, depth?: number, seen?: Set<unknown>): string[];
|
|
88
|
+
/**
|
|
89
|
+
* The digest of a file's bytes.
|
|
90
|
+
*
|
|
91
|
+
* Bytes, not the decoded string. A file is what is on disk, and a record whose
|
|
92
|
+
* identity depended on how it happened to be decoded would change identity for
|
|
93
|
+
* a byte-order mark nobody typed.
|
|
94
|
+
*/
|
|
95
|
+
export declare function contentDigest(bytes: Uint8Array | string): string;
|
|
96
|
+
export declare function recordFromFile(path: string, raw: string | Uint8Array): IndexableRecord | null;
|
|
97
|
+
/**
|
|
98
|
+
* Read every record under the configured roots.
|
|
99
|
+
*
|
|
100
|
+
* Returns what it managed to read plus what it refused, because a caller that
|
|
101
|
+
* cannot tell "there is nothing here" from "I was not allowed to look" cannot
|
|
102
|
+
* report either honestly.
|
|
103
|
+
*/
|
|
104
|
+
export declare function collectRecords(roots: readonly string[]): {
|
|
105
|
+
readonly records: readonly IndexableRecord[];
|
|
106
|
+
readonly skipped: readonly string[];
|
|
107
|
+
};
|
|
108
|
+
/** Build a fresh index over the roots. Cheap enough to do on demand. */
|
|
109
|
+
export declare function buildIndex(roots: readonly string[]): {
|
|
110
|
+
readonly index: LibraryIndex;
|
|
111
|
+
readonly skipped: readonly string[];
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Build an index, yielding often enough that a caller can stop it.
|
|
115
|
+
*
|
|
116
|
+
* The synchronous builder above is what a tool call uses: it is one pass over
|
|
117
|
+
* a directory and returning a promise would buy nothing. A refresh is
|
|
118
|
+
* different — it is a person waiting, it has a deadline, and it has to be
|
|
119
|
+
* abandonable — so this one checks the signal between files and hands the
|
|
120
|
+
* event loop back so the check can actually fire.
|
|
121
|
+
*
|
|
122
|
+
* It builds into a *new* index. Nothing in service is touched until the caller
|
|
123
|
+
* decides to swap, which is what makes a failed or abandoned rebuild leave the
|
|
124
|
+
* previous generation exactly as it was.
|
|
125
|
+
*/
|
|
126
|
+
export declare function buildIndexCancellable(roots: readonly string[], signal: AbortSignal): Promise<{
|
|
127
|
+
readonly index: LibraryIndex | null;
|
|
128
|
+
readonly skipped: readonly string[];
|
|
129
|
+
readonly sourceCount: number;
|
|
130
|
+
}>;
|
|
131
|
+
/**
|
|
132
|
+
* Register the search tool, and hand back the index it holds.
|
|
133
|
+
*
|
|
134
|
+
* The accessor is returned rather than the index itself so the read plane sees
|
|
135
|
+
* a rebuild the moment it happens, without either side holding a reference to
|
|
136
|
+
* an object the other has replaced. One index answers both the agent's tool
|
|
137
|
+
* and the person's surface: two would drift inside a release and disagree
|
|
138
|
+
* about what the library contains.
|
|
139
|
+
*/
|
|
140
|
+
export declare function applyLibrarySearch(ctx: Context, config: LibrarySearchConfig): {
|
|
141
|
+
readonly index: () => LibraryIndex;
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=library-search.d.ts.map
|