@avantf/dsh-mem 0.4.2 → 0.6.1
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 +19 -1
- package/lib/client.js +39 -9
- package/lib/engine/mem/config/config_files.d.ts +1 -1
- package/lib/engine/mem/db/dao/chunks.d.ts +80 -5
- package/lib/engine/mem/db/dao/entities.d.ts +35 -11
- package/lib/engine/mem/db/dao/fact_sources.d.ts +53 -0
- package/lib/engine/mem/db/dao/facts.d.ts +115 -12
- package/lib/engine/mem/db/dao/shared.d.ts +153 -0
- package/lib/engine/mem/db/invariants.d.ts +26 -0
- package/lib/engine/mem/db/tokenizer.d.ts +23 -0
- package/lib/engine/mem/db/vectors.d.ts +32 -6
- package/lib/engine/mem/entities/extract.d.ts +1 -1
- package/lib/engine/mem/eval/derive.d.ts +120 -0
- package/lib/engine/mem/eval/derived_runner.d.ts +88 -0
- package/lib/engine/mem/eval/index.d.ts +4 -0
- package/lib/engine/mem/eval/metrics.d.ts +24 -0
- package/lib/engine/mem/eval/runner.d.ts +8 -1
- package/lib/engine/mem/index.d.ts +2 -1
- package/lib/engine/mem/modelBootstrap.d.ts +2 -2
- package/lib/engine/mem/runtime.d.ts +3 -3
- package/lib/engine/mem/store/common.d.ts +18 -6
- package/lib/engine/mem/store/entity_leg.d.ts +95 -0
- package/lib/engine/mem/store/entity_sweep.d.ts +121 -0
- package/lib/engine/mem/store/floors.d.ts +51 -31
- package/lib/engine/mem/store/hybrid.d.ts +71 -26
- package/lib/engine/mem/store/knowledge.d.ts +134 -21
- package/lib/engine/mem/store/legs.d.ts +171 -0
- package/lib/engine/mem/store/lexical.d.ts +52 -0
- package/lib/engine/mem/store/memory.d.ts +177 -92
- package/lib/engine/mem/store/secret_guard.d.ts +34 -0
- package/lib/engine/mem/store/time_window.d.ts +74 -0
- package/lib/engine/mem/store/vector_repair.d.ts +248 -0
- package/lib/engine/mem-contract/config.d.ts +8 -16
- package/lib/engine/mem-contract/family.d.ts +1 -1
- package/lib/engine/mem-contract/tools.d.ts +14 -0
- package/lib/engine/mem-contract/types.d.ts +151 -15
- package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +37 -7
- package/lib/engine/mem-retrieval/fusion.d.ts +14 -1
- package/lib/engine/mem-retrieval/index.d.ts +1 -3
- package/lib/engine/mem-retrieval/interfaces.d.ts +38 -14
- package/lib/engine/mem-retrieval/registry.d.ts +2 -4
- package/lib/engine/mem-retrieval/representation.d.ts +50 -0
- package/lib/engine/mem-retrieval/stats.d.ts +1 -3
- package/lib/index.js +3008 -1008
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/prompt.d.ts +7 -0
- package/lib/types/prompt.d.ts.map +1 -1
- package/lib/types/remote.d.ts +12 -6
- package/lib/types/remote.d.ts.map +1 -1
- package/lib/types/vectorMigration.d.ts +22 -18
- package/lib/types/vectorMigration.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -3,18 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `MemoryStore.search` and `KnowledgeStore.search` used to carry parallel copies of this flow
|
|
5
5
|
* (~80 lines each: limit normalization → weights → over-fetch → leg cap → fuse → live filter →
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* slice → output budget → health event). The legs themselves are legitimately store-specific —
|
|
7
|
+
* different tables, different filters (`category` vs `domain`/`source`), and only memory has the
|
|
8
|
+
* HRR probe — but the ORCHESTRATION is one algorithm, and two copies of it drifted:
|
|
9
9
|
*
|
|
10
10
|
* - the `NaN` limit guard existed only in memory, so a non-finite limit reached knowledge's SQL
|
|
11
11
|
* `LIMIT` and took the whole cross-store query down with it (`Promise.all` has no per-leg catch);
|
|
12
12
|
* - `retriever.over_fetch_factor` was read only by memory, while knowledge hardcoded `limit * 3`,
|
|
13
13
|
* so one config knob silently meant two different things;
|
|
14
14
|
* - `recordLegCapped()` was called only by memory, so a capped knowledge leg was unobservable —
|
|
15
|
-
* exactly the blindness DESIGN §20.17 says the counter exists to remove
|
|
16
|
-
* - the reranker's used/fallback flags were a helper in memory and an inline expression in
|
|
17
|
-
* knowledge.
|
|
15
|
+
* exactly the blindness DESIGN §20.17 says the counter exists to remove.
|
|
18
16
|
*
|
|
19
17
|
* Fixing those one at a time would leave the next drift a matter of when. The stores now supply
|
|
20
18
|
* their legs and their hit mapping; everything that can drift lives here once.
|
|
@@ -26,7 +24,7 @@
|
|
|
26
24
|
* @module store/hybrid
|
|
27
25
|
*/
|
|
28
26
|
import { type Config, type FloorProfile, type RecallResult, type RetrievalFloorDrops, type RetrievalFloors } from '../../mem-contract/index.js';
|
|
29
|
-
import { type BudgetInput, type Hit, type
|
|
27
|
+
import { type BudgetInput, type Hit, type SemanticBackend } from '../../mem-retrieval/index.js';
|
|
30
28
|
import { type FloorLeg } from './floors.js';
|
|
31
29
|
/** The `limit` a caller gets when it passes nothing usable. */
|
|
32
30
|
export declare const DEFAULT_SEARCH_LIMIT = 10;
|
|
@@ -63,8 +61,12 @@ export interface HybridLeg {
|
|
|
63
61
|
capped?: boolean;
|
|
64
62
|
/**
|
|
65
63
|
* Which floor governs this leg, for {@link HybridResult.dropped_by_floor}'s per-leg breakdown.
|
|
66
|
-
* The HRR probe shares the
|
|
67
|
-
* attributable without double-counting the
|
|
64
|
+
* The HRR probe shares the entity (`jaccard`) floor; naming it separately keeps a narrowed
|
|
65
|
+
* candidate set attributable without double-counting the entity leg's own drops.
|
|
66
|
+
*
|
|
67
|
+
* The `jaccard` KEY is historical: the entity leg's metric is now anchored Jaccard with a
|
|
68
|
+
* saturating union, whose unit (and the `min_jaccard` floor on it) is defined in
|
|
69
|
+
* `store/entity_leg.ts` — read that before re-calibrating the knob.
|
|
68
70
|
*/
|
|
69
71
|
leg?: keyof RetrievalFloorDrops;
|
|
70
72
|
/** Candidates this leg removed because they fell below its floor. */
|
|
@@ -113,7 +115,6 @@ export interface HybridDeps<H> {
|
|
|
113
115
|
kind: 'memory' | 'knowledge';
|
|
114
116
|
config: Config;
|
|
115
117
|
semantic: SemanticBackend;
|
|
116
|
-
reranker: Reranker;
|
|
117
118
|
/**
|
|
118
119
|
* The legs to fuse, as promises (or values). Awaited concurrently; a rejection costs that leg
|
|
119
120
|
* only. The async setup a store needs before it can build its legs — memory extracts the query's
|
|
@@ -121,7 +122,7 @@ export interface HybridDeps<H> {
|
|
|
121
122
|
* body, before the array is returned.
|
|
122
123
|
*/
|
|
123
124
|
legs(ctx: HybridContext): Promise<readonly (HybridLeg | Promise<HybridLeg>)[]>;
|
|
124
|
-
/** Text for the fused ids: the live filter
|
|
125
|
+
/** Text for the fused ids: the live filter and the hit bodies. */
|
|
125
126
|
texts(ids: number[]): Map<number, string>;
|
|
126
127
|
/** Build the caller-facing hits from the ranked, live, already-sliced ids. */
|
|
127
128
|
hits(ranked: readonly Hit[], texts: Map<number, string>): H[];
|
|
@@ -177,6 +178,12 @@ export interface HybridPlan {
|
|
|
177
178
|
* `queries` counts questions instead of legs (DESIGN §20.5).
|
|
178
179
|
*/
|
|
179
180
|
recordStats?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* P-01: attach per-leg evidence to the returned hits (`RecallHit.scores` / `final`). Default
|
|
183
|
+
* false, and that default is load-bearing: the evidence is explanation only, so a caller that did
|
|
184
|
+
* not ask must not pay for it in tokens (`fuse` skips building the arrays entirely).
|
|
185
|
+
*/
|
|
186
|
+
includeScores?: boolean;
|
|
180
187
|
}
|
|
181
188
|
/** A hit after the output budget: `text` may have been shortened, and `truncated` says so. */
|
|
182
189
|
export type Budgeted<T> = T & {
|
|
@@ -207,17 +214,6 @@ export interface HybridResult<H> {
|
|
|
207
214
|
* rather than per store, because the health counter compares against whatever this returns.
|
|
208
215
|
*/
|
|
209
216
|
export declare function legCapFor(config: Config, overFetch: number): number;
|
|
210
|
-
/**
|
|
211
|
-
* Whether the configured reranker is actually in use — the two flags a retrieval event carries.
|
|
212
|
-
*
|
|
213
|
-
* "Configured" is not "used": a reranker that is selected but unavailable falls back to the fused
|
|
214
|
-
* order, and counting that as a rerank would make the counter lie in exactly the case it exists to
|
|
215
|
-
* expose (the reference implementation leaves `rerank_fallback` unfeedable).
|
|
216
|
-
*/
|
|
217
|
-
export declare function rerankState(reranker: Reranker): {
|
|
218
|
-
used: boolean;
|
|
219
|
-
fallback: boolean;
|
|
220
|
-
};
|
|
221
217
|
/**
|
|
222
218
|
* Bound a ranked hit list to a token budget, marking what was shortened.
|
|
223
219
|
*
|
|
@@ -233,9 +229,58 @@ export declare function applyOutputBudget<H extends BudgetInput>(config: Config,
|
|
|
233
229
|
/**
|
|
234
230
|
* Run one hybrid search.
|
|
235
231
|
*
|
|
236
|
-
* The order of the last
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* budget on hits that are about to be dropped.
|
|
232
|
+
* The order of the last steps is load-bearing: slice the fused (over-fetched) pool to `limit`,
|
|
233
|
+
* THEN apply the output budget, and only then tell the store what was returned. Budgeting before
|
|
234
|
+
* the slice would spend the budget on hits that are about to be dropped.
|
|
240
235
|
*/
|
|
241
236
|
export declare function hybridSearch<H extends BudgetInput>(deps: HybridDeps<H>, plan: HybridPlan): Promise<HybridResult<H>>;
|
|
237
|
+
/**
|
|
238
|
+
* The search-entry fields `MemoryStore.search` and `KnowledgeStore.search` hand to the orchestrator.
|
|
239
|
+
* Both stores' option types (`SearchInput` / `KnowledgeSearchOptions`) satisfy this structurally;
|
|
240
|
+
* everything else on them (`category` / `source` / `domain` / `includeHrr` / `track` / `onResult`) is
|
|
241
|
+
* leg- or domain-specific and stays at the store.
|
|
242
|
+
*/
|
|
243
|
+
export interface HybridSearchEntryFields {
|
|
244
|
+
limit?: number;
|
|
245
|
+
/**
|
|
246
|
+
* Knowledge only: overrides the config-derived pool size. Memory's `SearchInput` has no such field
|
|
247
|
+
* and leaves the plan key absent (see {@link runHybridSearch}).
|
|
248
|
+
*/
|
|
249
|
+
overFetch?: number;
|
|
250
|
+
maxTokens?: number;
|
|
251
|
+
queryVector?: Float32Array;
|
|
252
|
+
recordStats?: boolean;
|
|
253
|
+
floors?: FloorProfile;
|
|
254
|
+
includeScores?: boolean;
|
|
255
|
+
relaxLegs?: readonly FloorLeg[];
|
|
256
|
+
rewriteQuery?: (query: string) => string | undefined;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Map a store's search arguments onto {@link HybridPlan} and run {@link hybridSearch}. ONE copy.
|
|
260
|
+
*
|
|
261
|
+
* Before this, `MemoryStore.search` and `KnowledgeStore.search` carried the mapping verbatim side by
|
|
262
|
+
* side: the same field list, the same conditional spreads, the same call. That list is part of the
|
|
263
|
+
* orchestrator's contract, so an option added to one entry point and not the other would silently
|
|
264
|
+
* make the two stores query differently — the same shape of drift this module already documents
|
|
265
|
+
* (`NaN` limit, `over_fetch_factor`, unrecorded capped legs).
|
|
266
|
+
*
|
|
267
|
+
* The conditional spreads are deliberate, not cosmetic: a caller that did not pass `overFetch` /
|
|
268
|
+
* `includeScores` / `relaxLegs` / `rewriteQuery` leaves the plan key ABSENT rather than
|
|
269
|
+
* present-with-`undefined`. The orchestrator reads all four through `??` / `=== true` today, so the
|
|
270
|
+
* two are indistinguishable now — but the plan is a contract, and widening it with keys the caller
|
|
271
|
+
* never set is how "absent means default" decays into "present-undefined means set to nothing".
|
|
272
|
+
*/
|
|
273
|
+
export declare function runHybridSearch<H extends BudgetInput>(deps: HybridDeps<H>, query: string, fields: HybridSearchEntryFields | undefined): Promise<HybridResult<H>>;
|
|
274
|
+
/**
|
|
275
|
+
* The mapping itself, separated from the call so it can be pinned directly: the contract here is
|
|
276
|
+
* the SHAPE of the plan (which keys exist), which no behavioural assertion inside `hybridSearch`
|
|
277
|
+
* could observe once every optional field is read through `??` / `=== true`.
|
|
278
|
+
*/
|
|
279
|
+
export declare function hybridPlanOf(query: string, fields: HybridSearchEntryFields | undefined): HybridPlan;
|
|
280
|
+
/**
|
|
281
|
+
* Build a store's {@link HybridDeps} — the ONE construction site for the contract fields both stores
|
|
282
|
+
* share (`kind` / `config` / `semantic` / `legs` / `texts` / `hits` / `onReturn`). The per-store half
|
|
283
|
+
* is `parts`; the stores used to spell the whole object out twice, so a new required `HybridDeps`
|
|
284
|
+
* field was a change to remember in two places.
|
|
285
|
+
*/
|
|
286
|
+
export declare function makeHybridDeps<H>(kind: 'memory' | 'knowledge', config: Config, semantic: SemanticBackend, parts: Pick<HybridDeps<H>, 'legs' | 'texts' | 'hits' | 'onReturn'>): HybridDeps<H>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { type AvantfLogger, type Config, type DocumentDetail, type DocumentSummary, type DomainCatalog, type FloorProfile, type ImportResult, type IngestResult, type KbConflictReport, type KbIngestConflict, type KbSyncReport, type KnowledgeConfig, type RecallHit, type ReindexReport } from '../../mem-contract/index.js';
|
|
2
|
-
import type {
|
|
1
|
+
import { type AvantfLogger, type Config, type DocumentDetail, type DocumentSummary, type DomainCatalog, type FloorProfile, type ImportResult, type IngestResult, type KbConflictReport, type KbIngestConflict, type KbSyncReport, type KnowledgeConfig, type RecallHit, type ReindexReport, type VectorMigrationOutcome, type VectorMigrationProgress, type VectorSpaceHealth, type VectorsFixStoreReport } from '../../mem-contract/index.js';
|
|
2
|
+
import type { SemanticBackend, VectorStore } from '../../mem-retrieval/index.js';
|
|
3
3
|
import { type LexicalProbe } from './lexical.js';
|
|
4
4
|
import { type FloorLeg } from './floors.js';
|
|
5
5
|
import { type HybridResult } from './hybrid.js';
|
|
6
|
+
import { type VectorRepairTarget } from './vector_repair.js';
|
|
6
7
|
/** Options for {@link KnowledgeStore.search}. */
|
|
7
8
|
export interface KnowledgeSearchOptions {
|
|
8
9
|
domain?: string;
|
|
@@ -43,6 +44,8 @@ export interface KnowledgeSearchOptions {
|
|
|
43
44
|
* store-level "last result" field would race two concurrent queries.
|
|
44
45
|
*/
|
|
45
46
|
onResult?: (result: HybridResult<RecallHit>) => void;
|
|
47
|
+
/** P-01: attach per-leg evidence (`scores` / `final`) to every hit. Default false. */
|
|
48
|
+
includeScores?: boolean;
|
|
46
49
|
}
|
|
47
50
|
/**
|
|
48
51
|
* One identity a write will land on: the title it will use, the resolved local path it would read
|
|
@@ -87,7 +90,6 @@ export type IngestMode = 'add' | 'replace';
|
|
|
87
90
|
export declare class KnowledgeStore {
|
|
88
91
|
private readonly semantic;
|
|
89
92
|
private readonly vstore;
|
|
90
|
-
private readonly reranker;
|
|
91
93
|
/** The store config file `addDomain` writes the allowlist back to (`knowledgeConfigPath(home)`). */
|
|
92
94
|
private readonly domainConfigPath;
|
|
93
95
|
private readonly logger?;
|
|
@@ -109,6 +111,15 @@ export declare class KnowledgeStore {
|
|
|
109
111
|
private readonly docFiles;
|
|
110
112
|
/** > 0 while a batch (directory import) is writing: commits and baseline refreshes wait. */
|
|
111
113
|
private deferCorpusWrites;
|
|
114
|
+
/**
|
|
115
|
+
* Identity object for the shared entity sweep's in-flight guard (`store/entity_sweep.ts`).
|
|
116
|
+
*
|
|
117
|
+
* The guard itself lives in that module; this is only the per-store identity it keys on. It exists
|
|
118
|
+
* as a field — rather than the adapter object, which is built per call — because the guard must
|
|
119
|
+
* survive between two overlapping calls: `reindex` can be reached from a tool call and the
|
|
120
|
+
* settings-page button at once, and both would select the same stale chunks.
|
|
121
|
+
*/
|
|
122
|
+
private readonly entitySweepGuard;
|
|
112
123
|
/**
|
|
113
124
|
* Per-file stamps (`path → mtimeMs:size`) and the file-set they were taken from, so a reconcile
|
|
114
125
|
* can ask "what changed?" for the price of one `stat` per document instead of reading and
|
|
@@ -127,17 +138,22 @@ export declare class KnowledgeStore {
|
|
|
127
138
|
private readonly allowedDomains;
|
|
128
139
|
/** Whether the config allowlist is active (non-empty); `false` = `domains: []` = no restriction. */
|
|
129
140
|
private readonly domainsRestricted;
|
|
130
|
-
constructor(dbPath: string, config: Config, kbConfig: KnowledgeConfig, semantic: SemanticBackend, vstore: VectorStore,
|
|
141
|
+
constructor(dbPath: string, config: Config, kbConfig: KnowledgeConfig, semantic: SemanticBackend, vstore: VectorStore,
|
|
131
142
|
/** The store config file `addDomain` writes the allowlist back to (`knowledgeConfigPath(home)`). */
|
|
132
143
|
domainConfigPath: string, logger?: AvantfLogger | undefined);
|
|
133
144
|
/**
|
|
134
|
-
* Rebuild the in-memory vstore from persisted chunk vectors
|
|
145
|
+
* Rebuild the in-memory vstore from persisted chunk vectors, and say loudly what belongs to
|
|
146
|
+
* another vector space.
|
|
135
147
|
*
|
|
136
|
-
* Also detects a VECTOR-SPACE change, which the reload itself cannot see:
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
148
|
+
* Also detects a VECTOR-SPACE change, which the reload itself cannot see: the reload checks the
|
|
149
|
+
* width, and a different model at the same width produces dim-valid vectors that are then ranked
|
|
150
|
+
* against the current query's — two spaces mixed in one index, with no error anywhere.
|
|
151
|
+
*
|
|
152
|
+
* The reload, the count and the warning are the SHARED open-time step (`store/vector_repair.ts`),
|
|
153
|
+
* which is what turned the knowledge store from "warns about a swapped model but never repairs it"
|
|
154
|
+
* into the same automatic path memory has always had; this store's half is the row read. The
|
|
155
|
+
* manual entry stays `vectors --fix` (the vector-only repair); `kb_reindex` remains the full
|
|
156
|
+
* FTS + entities + vectors re-entry.
|
|
141
157
|
*/
|
|
142
158
|
private reloadIndex;
|
|
143
159
|
/**
|
|
@@ -242,8 +258,6 @@ export declare class KnowledgeStore {
|
|
|
242
258
|
warnings?: string[]): Promise<IngestResult>;
|
|
243
259
|
/** Materialize one document's editable copy; returns the `file` / `file_error` result fields. */
|
|
244
260
|
private writeManagedFile;
|
|
245
|
-
/** Batched eviction from the live vector index (removeMany when the backend supports it). */
|
|
246
|
-
private evictVectors;
|
|
247
261
|
/** The resolved ingestion boundary (see `ingest_guard.ts`). */
|
|
248
262
|
private get ingestLimits();
|
|
249
263
|
/**
|
|
@@ -287,7 +301,8 @@ export declare class KnowledgeStore {
|
|
|
287
301
|
*/
|
|
288
302
|
private writeReusedVectors;
|
|
289
303
|
/**
|
|
290
|
-
*
|
|
304
|
+
* This store's half of the shared encode-and-persist flow (`store/vector_repair.ts`): the backend
|
|
305
|
+
* call, the live index and the `WRITE_BATCH` transaction this store measured its way into.
|
|
291
306
|
*
|
|
292
307
|
* The model call is deliberately NOT batched, and that is a MEASURED decision, not an omission:
|
|
293
308
|
* a batched forward pass pads each item to the longest in its group, and real chunks span 53–550
|
|
@@ -298,15 +313,79 @@ export declare class KnowledgeStore {
|
|
|
298
313
|
* length-sorted) for callers whose texts suit it.
|
|
299
314
|
*
|
|
300
315
|
* What DID pay off is the write side: one commit per `WRITE_BATCH` chunks instead of one per
|
|
301
|
-
* chunk (measured 14.2 µs/row vs 2.2 µs/row), and the transaction never spans an `await
|
|
316
|
+
* chunk (measured 14.2 µs/row vs 2.2 µs/row), and the transaction never spans an `await` — that
|
|
317
|
+
* is the `writeBatch` below.
|
|
318
|
+
*/
|
|
319
|
+
private encodeWriteTarget;
|
|
320
|
+
/**
|
|
321
|
+
* Encode texts one at a time and persist them in `WRITE_BATCH` transactions.
|
|
322
|
+
*
|
|
323
|
+
* A failed chunk is skipped, not thrown: the rows are already committed and their old vectors
|
|
324
|
+
* evicted by the ingest path, so a throw here would leave the caller with a failed ingest and NO
|
|
325
|
+
* COUNT of what was skipped. The shortfall travels back to the tool result
|
|
326
|
+
* (`IngestResult.vectors_failed`) instead of living only in a log line.
|
|
302
327
|
*/
|
|
303
328
|
private encodeAndStore;
|
|
304
329
|
/**
|
|
305
|
-
* Identity of the vector space this store currently writes into (DESIGN §20):
|
|
306
|
-
* backend, model
|
|
307
|
-
*
|
|
330
|
+
* Identity of the vector space this store currently writes into (DESIGN §20): the representation
|
|
331
|
+
* fingerprint — backend, model, width AND the backend's declared pooling/normalization/input
|
|
332
|
+
* window/model revision — so any representation change makes stored chunk vectors stale instead
|
|
333
|
+
* of silently mixing two spaces. Public so diagnostics and tests can name the space.
|
|
334
|
+
*/
|
|
335
|
+
vectorSpace(): string;
|
|
336
|
+
/**
|
|
337
|
+
* Cheapest possible DETECTION of "the embedding space changed under this store": counts of
|
|
338
|
+
* persisted chunk vectors whose width or recorded space id no longer matches the one this store
|
|
339
|
+
* writes into. Reads blob LENGTHS only — never decodes — so the startup warning and the background
|
|
340
|
+
* drive can afford it at any corpus size.
|
|
341
|
+
*/
|
|
342
|
+
vectorSpaceHealth(): VectorSpaceHealth;
|
|
343
|
+
/** {@link vectorSpaceHealth} plus `missing`, the third term of the repair flow's `remaining`. */
|
|
344
|
+
private vectorSpaceCounts;
|
|
345
|
+
/** Async bootstrap the semantic backend (the shared flow's explicit-fix warmup). */
|
|
346
|
+
warmupSemantic(): Promise<boolean>;
|
|
347
|
+
/**
|
|
348
|
+
* This store's half of the shared repair flow (`store/vector_repair.ts`): the primitives the flow
|
|
349
|
+
* steps. The flow itself — batch size, event-loop yield, stop / resume / no-progress rules,
|
|
350
|
+
* `semantic.auto_migrate`, the dry-run split — lives there once, for both stores.
|
|
351
|
+
*/
|
|
352
|
+
vectorRepairTarget(): VectorRepairTarget;
|
|
353
|
+
/**
|
|
354
|
+
* The chunks the semantic leg cannot use (no vector / wrong width / another space). One SQL
|
|
355
|
+
* aggregate over blob lengths; `dropIds` is empty because knowledge overwrites in place.
|
|
356
|
+
*/
|
|
357
|
+
private classifyRepairVectors;
|
|
358
|
+
/**
|
|
359
|
+
* Encode + persist up to `limit` chunks the semantic leg cannot use, bounded and ordered by id.
|
|
360
|
+
*
|
|
361
|
+
* It does NOT re-extract entities or rebuild FTS: that is `kb_reindex`'s job, and the whole point
|
|
362
|
+
* of the automatic repair is that the vector half can run on its own — which is exactly the
|
|
363
|
+
* objection the "known boundary" in DESIGN §20 used to raise against a background run.
|
|
364
|
+
*/
|
|
365
|
+
private reencodeStaleChunks;
|
|
366
|
+
/**
|
|
367
|
+
* The explicit, unbounded repair behind `mem_admin vectors_fix` / `avantf-mem vectors --fix`.
|
|
368
|
+
* `dry_run` previews every count without writing — and without loading the model, which is why
|
|
369
|
+
* the preview also reports `would_warm` (see the contract's `VectorsFixStoreReport`).
|
|
370
|
+
*/
|
|
371
|
+
vectorsFix(dryRun?: boolean): Promise<VectorsFixStoreReport>;
|
|
372
|
+
/**
|
|
373
|
+
* ONE bounded slice of the vector-space migration: at most `batchSize` chunks are re-encoded, so
|
|
374
|
+
* the caller keeps control of the event loop between slices. This is the unit the background
|
|
375
|
+
* migration ({@link migrateVectors}) and its tests drive.
|
|
376
|
+
*/
|
|
377
|
+
migrateVectorsBatch(batchSize?: number): Promise<VectorMigrationProgress>;
|
|
378
|
+
/**
|
|
379
|
+
* Drive the bounded background migration for this store until it is current, the model is
|
|
380
|
+
* unavailable, or `shouldStop()` turns true (plugin unmount / process shutdown). The loop, the
|
|
381
|
+
* `semantic.auto_migrate` switch, the no-progress rule and the store-named logging all live in the
|
|
382
|
+
* shared flow (`store/vector_repair.ts`); this is the one-store entry.
|
|
308
383
|
*/
|
|
309
|
-
|
|
384
|
+
migrateVectors(opts?: {
|
|
385
|
+
batchSize?: number;
|
|
386
|
+
shouldStop?: () => boolean;
|
|
387
|
+
onProgress?: (progress: VectorMigrationOutcome) => void;
|
|
388
|
+
}): Promise<VectorMigrationOutcome>;
|
|
310
389
|
/**
|
|
311
390
|
* A stored vector is reusable when it exists, matches the space, and its text is unchanged.
|
|
312
391
|
*
|
|
@@ -317,8 +396,28 @@ export declare class KnowledgeStore {
|
|
|
317
396
|
* the store would keep serving a vector for text it no longer holds.
|
|
318
397
|
*/
|
|
319
398
|
private vectorReusable;
|
|
320
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* Extract one chunk's entity names, bounded by `MAX_ENTITIES_PER_CHUNK`.
|
|
401
|
+
*
|
|
402
|
+
* The bound is here, not in either caller: it is a property of what `chunk_entities` may hold for
|
|
403
|
+
* one chunk, and the ingest path and the sweep must not disagree about it.
|
|
404
|
+
*/
|
|
405
|
+
private extractRowEntities;
|
|
406
|
+
/** One transaction replacing the entity rows of the given chunks (the ingest path). */
|
|
321
407
|
private replaceChunkEntities;
|
|
408
|
+
/**
|
|
409
|
+
* The per-row primitive of the shared entity sweep (`store/entity_sweep.ts`).
|
|
410
|
+
*
|
|
411
|
+
* Replaces ONE chunk's entity rows and stamps that same row with `ENTITY_EXTRACTOR_VERSION`, in one
|
|
412
|
+
* transaction. Knowledge's domain difference from memory is exactly this: memory's transaction also
|
|
413
|
+
* moves triples, the HRR bundle and the conflict queue, while a chunk has none of those — its
|
|
414
|
+
* derived state is the entity names alone.
|
|
415
|
+
*
|
|
416
|
+
* Per row, not per batch, because the sweep's contract is that a failure leaves THAT row stale and
|
|
417
|
+
* retryable: inside a batched transaction one bad extraction would roll back the whole batch, and
|
|
418
|
+
* rows it never reached would look stamped.
|
|
419
|
+
*/
|
|
420
|
+
private replaceRowEntities;
|
|
322
421
|
/**
|
|
323
422
|
* Ingest local files/directories. Every path goes through the SAME boundary as
|
|
324
423
|
* `ingestUri` — a bare `kb_import` on `~/.ssh` would otherwise be the exact
|
|
@@ -434,7 +533,7 @@ export declare class KnowledgeStore {
|
|
|
434
533
|
* and the chunk→hit mapping.
|
|
435
534
|
*
|
|
436
535
|
* The limit normalization, the over-fetch factor, the leg cap, the capped-leg counter, the
|
|
437
|
-
* degraded weights
|
|
536
|
+
* degraded weights and the output budget all live there now. They used to be copied
|
|
438
537
|
* here, and every fix landed on the memory side only — a `NaN` limit reached this store's SQL and
|
|
439
538
|
* took the whole cross-store query down with it, `retriever.over_fetch_factor` meant something
|
|
440
539
|
* different per store, and a capped knowledge leg was invisible to the health counters.
|
|
@@ -457,13 +556,27 @@ export declare class KnowledgeStore {
|
|
|
457
556
|
* re-encoding (performance review §7.7 / P8).
|
|
458
557
|
*/
|
|
459
558
|
private semanticPath;
|
|
559
|
+
/**
|
|
560
|
+
* The semantic leg's scope filter, as a `filterTopk` primitive: `undefined` when NO domain/source
|
|
561
|
+
* was asked means "keep the whole pool" — the short-circuit is preserved, so no `chunks.meta`
|
|
562
|
+
* lookup runs at all. Otherwise one batched lookup, and rows whose domain/source do not match (or
|
|
563
|
+
* whose chunk is gone) are dropped.
|
|
564
|
+
*/
|
|
565
|
+
private scopeFilter;
|
|
460
566
|
/**
|
|
461
567
|
* Sync relevance probe for the plugin's conditional hint (DESIGN §12). Same contract as
|
|
462
568
|
* `MemoryStore.lexicalProbe` — see `store/lexical.ts`.
|
|
463
569
|
*/
|
|
464
570
|
lexicalProbe(text: string, stopAt?: number): LexicalProbe;
|
|
465
571
|
private ftsPath;
|
|
466
|
-
/**
|
|
572
|
+
/**
|
|
573
|
+
* Entity overlap against entities extracted AT INGEST time (chunk_entities) — no corpus scan.
|
|
574
|
+
*
|
|
575
|
+
* Same metric as the memory store's sibling (`store/entity_leg.ts` carries the measurement and
|
|
576
|
+
* the reasoning): anchored Jaccard with a saturating union over the query's rare entities, so a
|
|
577
|
+
* wide chunk cannot dilute the denominator and a corpus-wide word cannot drag the whole knowledge
|
|
578
|
+
* base in.
|
|
579
|
+
*/
|
|
467
580
|
private jaccardPath;
|
|
468
581
|
}
|
|
469
582
|
export interface Chunk {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The leg runner: how BOTH stores turn their own raw leg scores into `HybridLeg`s — plus the two
|
|
3
|
+
* leg SOURCES that were the same flow twice ({@link ftsLeg}, {@link semanticLeg}, §4.6 of
|
|
4
|
+
* `docs/vector-repair-shared-flow.md`). Both stores keep only their DAO/vstore primitives and their
|
|
5
|
+
* scope: the `-rank`/`rank` sign convention, the short-query fallback, `max(50, k)`, the dimension
|
|
6
|
+
* check's error and the Map insertion order live here once.
|
|
7
|
+
*
|
|
8
|
+
* `MemoryStore.searchLegs` and `KnowledgeStore.searchLegs` were the same skeleton written twice —
|
|
9
|
+
* a `leg()` wrapper whose `capped` flag is measured on the RAW (pre-floor) score set, the
|
|
10
|
+
* `applyTermFloor` / `applyScoreFloor` application, and the "semantic backend down ⇒ an EQUAL-WEIGHT
|
|
11
|
+
* empty leg with `droppedByFloor: 0`" fallback — and even the comments explaining them were the same
|
|
12
|
+
* paragraph twice. What legitimately differs is only the leg LIST and where each leg's scores come
|
|
13
|
+
* from: memory computes anchors once and shares them between the entity and HRR legs, has the HRR
|
|
14
|
+
* probe and the time-window scan; knowledge's entity leg is a query-driven async Jaccard. Those stay
|
|
15
|
+
* at the store, exactly as §2 of `docs/vector-repair-shared-flow.md` requires.
|
|
16
|
+
*
|
|
17
|
+
* WHY THE CAP IS MEASURED ON THE RAW SET. `size === cap` is the only observable "this leg was cut"
|
|
18
|
+
* signal (a leg that finished under the cap cannot have been trimmed), and it feeds the health
|
|
19
|
+
* counters so the headroom can be sized from data — what makes the cap measurable instead of a silent
|
|
20
|
+
* quality cliff (DESIGN §20.17). The relevance floors remove the tail anyway, so
|
|
21
|
+
* deriving the flag from the FLOORED size would erase the signal exactly when the cap bound. The
|
|
22
|
+
* floor functions only ever REMOVE entries, so the raw set is the honest measurement.
|
|
23
|
+
*
|
|
24
|
+
* WHY THE SEMANTIC LEG CARRIES NO `capped`. It is capped by the POOL size (`overFetch`), not by
|
|
25
|
+
* `legCap`, so comparing it against `legCap` would report a trim that never happened. That is why
|
|
26
|
+
* {@link LegRunner.semantic} is a separate method rather than a flag on {@link LegRunner.scored}:
|
|
27
|
+
* the absence is structural, not something a caller can forget to set.
|
|
28
|
+
*
|
|
29
|
+
* WHY COMPOSITION. No base class (see the spec §2): the two stores share no construction, DB, DAO or
|
|
30
|
+
* table, and a template-method base class would be a structural rewrite of two hub files. This is
|
|
31
|
+
* the same style as `store/common.ts` — free functions plus a small interface — so a THIRD store can
|
|
32
|
+
* be added by supplying its own leg list and sources and driving THIS runner (proved by a fake
|
|
33
|
+
* third store in `test/legs.spec.ts`).
|
|
34
|
+
*
|
|
35
|
+
* CONCURRENCY lives one level up, in `store/hybrid.ts`'s `runLegs`: it awaits the returned
|
|
36
|
+
* (value | promise) array concurrently and isolates a failing leg to itself. This module only BUILDS
|
|
37
|
+
* legs, so both stores get the same awaiting and isolation by construction.
|
|
38
|
+
*
|
|
39
|
+
* @module store/legs
|
|
40
|
+
*/
|
|
41
|
+
import { type FloorLeg } from './floors.js';
|
|
42
|
+
import { type HybridContext, type HybridLeg } from './hybrid.js';
|
|
43
|
+
import { type FtsTokenizer } from '../db/tokenizer.js';
|
|
44
|
+
/**
|
|
45
|
+
* The legs {@link LegRunner.scored} may wrap: those whose raw score has an absolute scale a floor
|
|
46
|
+
* can cut. `semantic` is excluded on purpose — it shares `applyScoreFloor` but must not carry the
|
|
47
|
+
* cap signal, and it is reached through {@link LegRunner.semantic} (see the module comment).
|
|
48
|
+
*/
|
|
49
|
+
export type ScoredLeg = Exclude<FloorLeg, 'semantic'>;
|
|
50
|
+
/** Build the leg runner for one query. `legCap` is the cap every non-semantic leg is measured against. */
|
|
51
|
+
export interface LegRunner {
|
|
52
|
+
/** The per-leg row cap in force for this query (see `hybrid.ts`'s `legCapFor`). */
|
|
53
|
+
readonly cap: number;
|
|
54
|
+
/**
|
|
55
|
+
* Wrap raw scores that have NO floor of their own (memory's HRR probe and time-window scan): the
|
|
56
|
+
* cap signal is attached and nothing else — no `leg` label, no `droppedByFloor`, exactly as those
|
|
57
|
+
* legs have always shipped (they share the entity leg's weight and floor report).
|
|
58
|
+
*/
|
|
59
|
+
plain(scores: Map<number, number>, weight: number): HybridLeg;
|
|
60
|
+
/**
|
|
61
|
+
* A score-floored leg: `applyScoreFloor` on the raw map, then the cap measured on the RAW map. The
|
|
62
|
+
* floored map is what the leg ships; `raw` is only ever read for the cap.
|
|
63
|
+
*/
|
|
64
|
+
scored(name: ScoredLeg, raw: Map<number, number>, weight: number, floor: number): HybridLeg;
|
|
65
|
+
/**
|
|
66
|
+
* The FTS leg: the per-row term floor (`applyTermFloor`) needs the candidates' texts, read by the
|
|
67
|
+
* store for the whole raw set (one batched query). The cap is measured on the raw map.
|
|
68
|
+
*/
|
|
69
|
+
term(name: 'fts', raw: Map<number, number>, texts: Map<number, string>, query: string, weight: number, floor: number): HybridLeg;
|
|
70
|
+
/**
|
|
71
|
+
* The semantic leg: score-floored like {@link scored}, but deliberately WITHOUT a `capped` key
|
|
72
|
+
* (see the module comment). The async model encode itself happens at the store, which is the only
|
|
73
|
+
* place that knows its backend and filters.
|
|
74
|
+
*/
|
|
75
|
+
semantic(raw: Map<number, number>, weight: number, floor: number): HybridLeg;
|
|
76
|
+
/**
|
|
77
|
+
* The fallback for a DOWN semantic backend: an EMPTY leg at the SAME weight, reporting zero floor
|
|
78
|
+
* drops. Equal weight, not zero: the leg is absent, the weight contract (`RecallResult.weights`)
|
|
79
|
+
* must not move to a fourth value, and the orchestrator already rebalanced the weights for
|
|
80
|
+
* `semAvail === false` (`DEGRADED_WEIGHTS`).
|
|
81
|
+
*/
|
|
82
|
+
semanticOff(weight: number): HybridLeg;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the leg runner for one query.
|
|
86
|
+
*
|
|
87
|
+
* The per-store leg ORDER is NOT here: it is the store's "leg list" (see §4.5 阶段 4) because leg
|
|
88
|
+
* index is part of the contract — `hybrid.ts`'s `unionLegs` merges the original and the
|
|
89
|
+
* self-reference rewrite runs BY INDEX, so a store that reordered its legs would silently re-map
|
|
90
|
+
* evidence. What this runner guarantees is that every leg, whichever order it lands in, gets the
|
|
91
|
+
* same wrapper, the same floor order and the same down-semantic fallback.
|
|
92
|
+
*/
|
|
93
|
+
export declare function makeLegRunner(ctx: Pick<HybridContext, 'legCap'>): LegRunner;
|
|
94
|
+
/** The per-store pieces of the FTS leg — see {@link ftsLeg}. `Scope` is opaque to the flow. */
|
|
95
|
+
export interface FtsLegOptions<Scope> {
|
|
96
|
+
query: string;
|
|
97
|
+
/** The per-leg row cap (the store's `legCap`), handed to whichever DAO statement runs. */
|
|
98
|
+
cap: number;
|
|
99
|
+
/** The store's scope shape (`{category, source}` / `{domain, source}`), passed straight through. */
|
|
100
|
+
scope: Scope;
|
|
101
|
+
/** The table's own tokenizer, for the `MATCH` build (`buildFtsQuery`). */
|
|
102
|
+
tokenizer: FtsTokenizer;
|
|
103
|
+
/** The FTS `MATCH` statement; its `rank` is bm25 (negative = better). */
|
|
104
|
+
search: (ftsQuery: string, scope: Scope, limit: number) => readonly {
|
|
105
|
+
id: number;
|
|
106
|
+
rank: number;
|
|
107
|
+
}[];
|
|
108
|
+
/** The short-query LIKE statement; its `rank` counts the terms the row contains (higher = better). */
|
|
109
|
+
substringSearch: (terms: readonly string[], scope: Scope, limit: number) => readonly {
|
|
110
|
+
id: number;
|
|
111
|
+
rank: number;
|
|
112
|
+
}[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The FTS leg, shared by `MemoryStore.ftsPath` and `KnowledgeStore.ftsPath` (§4.6.1).
|
|
116
|
+
*
|
|
117
|
+
* The two were line-for-line the same flow — `buildFtsQuery` → the `MATCH` statement with NEGATED
|
|
118
|
+
* bm25, else `substringTerms` → the LIKE statement with `rank` — differing only in the DAO and the
|
|
119
|
+
* order its scope parameters bind. Both conventions are load-bearing and preserved verbatim:
|
|
120
|
+
*
|
|
121
|
+
* - **bm25 is negative** (more negative = better match), so the `MATCH` branch negates it and higher
|
|
122
|
+
* stays better everywhere downstream;
|
|
123
|
+
* - **the LIKE `rank`** already counts contained terms, so it is used as-is, keeping the two branches
|
|
124
|
+
* on the same "higher = better" scale;
|
|
125
|
+
* - **an empty term list returns the empty map** (`substringTerms` yields none for a query with no
|
|
126
|
+
* 3-char run) — the leg is empty by construction, not by a query that matched nothing.
|
|
127
|
+
*
|
|
128
|
+
* THE SHORT-QUERY FALLBACK. `buildFtsQuery` returning `null` means the query carries no term the
|
|
129
|
+
* trigram index can express (measured: every term in the FTS table is 3 characters), which used to
|
|
130
|
+
* leave this leg empty by construction — the 2-char CJK shape. `substringTerms` is the one finer
|
|
131
|
+
* predicate the FTS5 trigram TABLE still answers (`LIKE '%…%'`), bounded by `cap`; the accuracy guard
|
|
132
|
+
* is the per-row term floor in `applyTermFloor`, which requires the row to CONTAIN the run.
|
|
133
|
+
*/
|
|
134
|
+
export declare function ftsLeg<Scope>(opts: FtsLegOptions<Scope>): Map<number, number>;
|
|
135
|
+
/** The per-store pieces of the semantic leg — see {@link semanticLeg}. */
|
|
136
|
+
export interface SemanticLegOptions {
|
|
137
|
+
query: string;
|
|
138
|
+
/** The requested leg size; the pool is over-fetched to `max(50, k)` below. */
|
|
139
|
+
k: number;
|
|
140
|
+
/** A caller-encoded vector for this exact query; skips {@link encode}. */
|
|
141
|
+
queryVector?: Float32Array;
|
|
142
|
+
/** Publishes the vector actually used, so the relaxed retry reuses it instead of re-encoding. */
|
|
143
|
+
onVector?: (vec: Float32Array) => void;
|
|
144
|
+
encode: (query: string) => Promise<Float32Array>;
|
|
145
|
+
/** The vstore's width, checked against the vector before it is scored. */
|
|
146
|
+
dim: number;
|
|
147
|
+
topk: (vec: Float32Array, k: number) => readonly {
|
|
148
|
+
id: number;
|
|
149
|
+
score: number;
|
|
150
|
+
}[];
|
|
151
|
+
/**
|
|
152
|
+
* The store's eligibility filter over the pool ids.
|
|
153
|
+
*
|
|
154
|
+
* `undefined` = **keep everything** — knowledge's "no domain/source asked" short-circuit stays a
|
|
155
|
+
* short-circuit (no lookup runs at all). A returned set is the allow-list; the flow still maps the
|
|
156
|
+
* pool in ITS order, so ids/scores/insertion order are the vstore's.
|
|
157
|
+
*/
|
|
158
|
+
filterTopk: (ids: readonly number[]) => ReadonlySet<number> | undefined;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The semantic leg, shared by `MemoryStore.semanticPath` and `KnowledgeStore.semanticPath` (§4.6.2).
|
|
162
|
+
*
|
|
163
|
+
* The flow: encode (unless the caller supplied the vector) → dimension check → publish → over-fetch
|
|
164
|
+
* `max(50, k)` from the vstore → empty pool early-returns → filter → map id→score. Both stores used
|
|
165
|
+
* to spell all of it out; only the filter differs, and that difference lives in `filterTopk`.
|
|
166
|
+
*
|
|
167
|
+
* Preserved verbatim: the `max(50, k)` over-fetch (the pool must leave room for the filter to remove
|
|
168
|
+
* rows without shrinking the leg below `k`), the `RetrievalInputError` and its exact message, the
|
|
169
|
+
* empty-pool early return, and the returned map's insertion order (the vstore's pool order).
|
|
170
|
+
*/
|
|
171
|
+
export declare function semanticLeg(opts: SemanticLegOptions): Promise<Map<number, number>>;
|
|
@@ -25,7 +25,22 @@
|
|
|
25
25
|
* `matched >= 2`, with the margin between 2 and 1 rather than a tuned threshold inside a
|
|
26
26
|
* continuum. A single-word query (`cgroup`) scores 1 and stays silent on purpose: one term is not
|
|
27
27
|
* evidence.
|
|
28
|
+
*
|
|
29
|
+
* THE SHORT-CJK FALLBACK (and why the hint probe does NOT use it). The trigram index has no term
|
|
30
|
+
* shorter than three characters — measured on the live store: `fts5vocab` over `facts_fts` holds
|
|
31
|
+
* 27906 distinct terms and EVERY one is 3 characters (and a 2-char `MATCH` returns nothing, even as
|
|
32
|
+
* a prefix query). So a 2-char CJK query produces no FTS term at all and that leg is empty by
|
|
33
|
+
* construction. The FTS LEG therefore falls back to the one finer predicate the FTS5 trigram TABLE
|
|
34
|
+
* does expose: `content LIKE '%…%'`, which matches a 2-char run by scanning (no index can serve a
|
|
35
|
+
* 2-char pattern). See {@link substringTerms} / {@link gradedTerms}.
|
|
36
|
+
*
|
|
37
|
+
* The conditional-hint probe deliberately keeps the index-expressible terms only: it is
|
|
38
|
+
* SYNCHRONOUS and runs on every step's prompt assembly, so it must stay a bounded number of
|
|
39
|
+
* `LIMIT 1` index lookups — an O(corpus) scan per 2-char term would put a full scan on the prompt
|
|
40
|
+
* path. Behaviour is unchanged there: a 2-char query scored 0 matched terms before and still does
|
|
41
|
+
* (`{terms: 0, matched: 0}`), so the hint stays silent exactly as it did.
|
|
28
42
|
*/
|
|
43
|
+
import { type FtsTokenizer } from '../db/tokenizer.js';
|
|
29
44
|
/** How many of an input's terms a store holds. `terms === 0` means the input had nothing to test. */
|
|
30
45
|
export interface LexicalProbe {
|
|
31
46
|
terms: number;
|
|
@@ -46,6 +61,36 @@ export type RelevanceHit = boolean;
|
|
|
46
61
|
* caller runs one `LIMIT 1` MATCH per term and counts the hits.
|
|
47
62
|
*/
|
|
48
63
|
export declare function relevanceTerms(text: string, cap?: number): string[];
|
|
64
|
+
/**
|
|
65
|
+
* The terms the FTS leg falls back to when the trigram index can express NONE of the query.
|
|
66
|
+
*
|
|
67
|
+
* A 2-char CJK run IS the finer granularity the FTS5 trigram TABLE can still answer — not as an
|
|
68
|
+
* indexed token (the vocabulary holds 3-character terms only, measured; a 2-char `MATCH`, quoted or
|
|
69
|
+
* prefixed, returns zero rows) but as the substring predicate FTS5 exposes on a trigram table
|
|
70
|
+
* (`content LIKE '%…%'`, which falls back to a scan for patterns under three characters). One-char
|
|
71
|
+
* runs are deliberately NOT included: a single character is not evidence (the same reason
|
|
72
|
+
* {@link looksRelevant} needs two terms), and it would make the leg match a large share of any
|
|
73
|
+
* corpus. Latin runs are not included either — `buildFtsQuery` already admits latin tokens from
|
|
74
|
+
* three characters, and the module's own floor (5, `MIN_LATIN`) is a separate, measured decision.
|
|
75
|
+
*
|
|
76
|
+
* PRECISION GUARD. The fallback is graded by the SAME per-row floor as the indexed path and by the
|
|
77
|
+
* reachability clamp: for a single 2-char term the effective bar is 1, and the term is a substring,
|
|
78
|
+
* so "the row contains the query's characters CONTIGUOUSLY" is what admits it — not "shares one
|
|
79
|
+
* incidental trigram". A two-run query ('李娜 张伟') produces two substring terms and is judged
|
|
80
|
+
* against `min(configured, 2)`, i.e. both runs must occur in the row.
|
|
81
|
+
*/
|
|
82
|
+
export declare function substringTerms(text: string, cap?: number): string[];
|
|
83
|
+
/**
|
|
84
|
+
* The term set the FTS leg is SEARCHED and GRADED with — the one definition of "the query's terms"
|
|
85
|
+
* for that leg (`store/floors.ts` grades with it, `store/hybrid.ts` resolves the reachability clamp
|
|
86
|
+
* from it, and the stores build their MATCH / substring probe from it).
|
|
87
|
+
*
|
|
88
|
+
* The fallback is whole-query, not per run: any text that yields even one index-expressible term is
|
|
89
|
+
* returned UNCHANGED from {@link relevanceTerms}. That is what keeps every long query bit-identical
|
|
90
|
+
* (a mixed text like `缓存失效 李娜` keeps only its trigrams, exactly as before) and confines the
|
|
91
|
+
* new behaviour to the shape that had NO leg at all.
|
|
92
|
+
*/
|
|
93
|
+
export declare function gradedTerms(text: string, cap?: number): string[];
|
|
49
94
|
/**
|
|
50
95
|
* The gate itself: is this store worth telling the model about?
|
|
51
96
|
*
|
|
@@ -65,3 +110,10 @@ export declare function looksRelevant(probe: LexicalProbe): boolean;
|
|
|
65
110
|
* path is synchronous); the default keeps an exact count for the calibration recorded above.
|
|
66
111
|
*/
|
|
67
112
|
export declare function probeTerms(text: string, holds: (term: string) => boolean, stopAt?: number): LexicalProbe;
|
|
113
|
+
/**
|
|
114
|
+
* The store probe both stores' `lexicalProbe` used to spell out verbatim: build THIS store's MATCH
|
|
115
|
+
* expression for one term (the tokenizer is a property of the store's FTS table) and ask that
|
|
116
|
+
* table whether anything holds it. `probeTerms` owns the term set and the counting; this owns the
|
|
117
|
+
* one part that has to differ, which is now a single `holds` closure per store.
|
|
118
|
+
*/
|
|
119
|
+
export declare function probeStoreTerms(text: string, tokenizer: FtsTokenizer, holds: (ftsQuery: string) => boolean, stopAt?: number): LexicalProbe;
|