@avantf/dsh-mem 0.6.0 → 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/lib/client.js +1 -0
- package/lib/engine/mem/db/dao/chunks.d.ts +60 -4
- package/lib/engine/mem/db/dao/shared.d.ts +153 -0
- package/lib/engine/mem/eval/index.d.ts +4 -0
- package/lib/engine/mem/index.d.ts +1 -0
- package/lib/engine/mem/runtime.d.ts +2 -2
- package/lib/engine/mem/store/entity_sweep.d.ts +121 -0
- package/lib/engine/mem/store/hybrid.d.ts +50 -0
- package/lib/engine/mem/store/knowledge.d.ts +116 -12
- package/lib/engine/mem/store/legs.d.ts +171 -0
- package/lib/engine/mem/store/lexical.d.ts +8 -0
- package/lib/engine/mem/store/memory.d.ts +84 -72
- package/lib/engine/mem/store/vector_repair.d.ts +248 -0
- package/lib/engine/mem-contract/tools.d.ts +4 -0
- package/lib/engine/mem-contract/types.d.ts +37 -6
- package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +52 -0
- package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +10 -0
- package/lib/engine/mem-retrieval/rerank.d.ts +13 -0
- package/lib/index.js +1370 -538
- package/lib/types/index.d.ts.map +1 -1
- 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
package/lib/client.js
CHANGED
|
@@ -5346,6 +5346,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5346
5346
|
offset: count.optional(),
|
|
5347
5347
|
reason: optionalText.optional(),
|
|
5348
5348
|
dry_run: bool.optional(),
|
|
5349
|
+
store: optionalText.optional(),
|
|
5349
5350
|
contradiction_id: count.optional(),
|
|
5350
5351
|
resolution: optionalText.optional(),
|
|
5351
5352
|
loser_fact_id: count.optional()
|
|
@@ -104,12 +104,48 @@ export declare class ChunksDao {
|
|
|
104
104
|
vec: Buffer;
|
|
105
105
|
embedding_model: string | null;
|
|
106
106
|
}[];
|
|
107
|
+
/**
|
|
108
|
+
* Vector-space health of the WHOLE chunk corpus, by kind, WITHOUT decoding a single blob.
|
|
109
|
+
*
|
|
110
|
+
* The same two counts the open-time warning reports (`stale` = wrong width, `space_stale` = same
|
|
111
|
+
* width but another recorded space — `embedding_model IS NOT` is null-safe, so a row with no
|
|
112
|
+
* recorded space counts as foreign exactly like the JS comparison in `reloadIndex`) plus `missing`
|
|
113
|
+
* (no vector at all), which is the third reason the semantic leg cannot use a chunk.
|
|
114
|
+
*
|
|
115
|
+
* This is what lets the shared repair flow read "what is stale" cheaply per slice instead of
|
|
116
|
+
* re-scanning and hashing the corpus (`corpusState` reads every text; a batched migration must
|
|
117
|
+
* not).
|
|
118
|
+
*/
|
|
119
|
+
vectorSpaceCounts(space: string, expectedBytes: number): {
|
|
120
|
+
stale: number;
|
|
121
|
+
space_stale: number;
|
|
122
|
+
missing: number;
|
|
123
|
+
};
|
|
124
|
+
/** Chunks with no persisted vector — the repair flow's `missing` term in `remaining`. */
|
|
125
|
+
countMissingVectors(): number;
|
|
126
|
+
/**
|
|
127
|
+
* Chunks the semantic leg cannot use (no vector / wrong width / another space), bounded and
|
|
128
|
+
* ordered by id — the repair flow's encode source.
|
|
129
|
+
*
|
|
130
|
+
* The content-hash check `vectorReusable` adds is deliberately NOT here: a chunk whose vector is
|
|
131
|
+
* in the current space is usable even if its text was edited behind the store's back, and that
|
|
132
|
+
* (rarer) case belongs to `kb_reindex`, which re-derives every leg. Repairing the SPACE must not
|
|
133
|
+
* pay a jieba/hash pass over the corpus to find rows the space change did not touch.
|
|
134
|
+
*/
|
|
135
|
+
staleVectorRows(space: string, expectedBytes: number, limit: number): {
|
|
136
|
+
chunk_id: number;
|
|
137
|
+
text: string;
|
|
138
|
+
}[];
|
|
107
139
|
count(): number;
|
|
108
140
|
/**
|
|
109
|
-
* Replace the entity rows of the given chunks in one transaction
|
|
110
|
-
*
|
|
141
|
+
* Replace the entity rows of the given chunks in one transaction.
|
|
142
|
+
*
|
|
143
|
+
* `replace` is REQUIRED, not defaulted: `true` clears the old rows first (the entity sweep, where
|
|
144
|
+
* the extractor may have changed), `false` is the ingest path, where the chunk ids are freshly
|
|
145
|
+
* inserted and an absent old row set. A default would let one caller silently inherit the other's
|
|
146
|
+
* semantics — an extra `DELETE` per chunk on ingest, or an append-on-top-of-stale sweep.
|
|
111
147
|
*/
|
|
112
|
-
replaceEntities(rows: readonly ChunkEntityRow[], replace
|
|
148
|
+
replaceEntities(rows: readonly ChunkEntityRow[], replace: boolean): void;
|
|
113
149
|
/** Batched entity bags: `Map<chunkId, names>` (ids with none are absent). */
|
|
114
150
|
entityBags(ids: readonly number[]): Map<number, string[]>;
|
|
115
151
|
/** Chunk body + owning-document identity for the fused candidate ids. */
|
|
@@ -127,11 +163,31 @@ export declare class ChunksDao {
|
|
|
127
163
|
* for the rows that are actually out of date (DESIGN §20).
|
|
128
164
|
*/
|
|
129
165
|
corpusState(domain?: string): ChunkStateRow[];
|
|
166
|
+
/**
|
|
167
|
+
* Chunk rows whose `chunk_entities` were produced by OLDER extraction rules — the entity sweep's
|
|
168
|
+
* selection, bounded and ordered by id.
|
|
169
|
+
*
|
|
170
|
+
* `COALESCE(entities_version, 0) < :version`, not `entities_version != :version`: the column is
|
|
171
|
+
* NULLABLE on purpose (a row written before the derived-state columns existed must look stale
|
|
172
|
+
* exactly once and be adopted by the first rebuild — see `ChunkStateRow`), and a bare `<` would
|
|
173
|
+
* evaluate to NULL for those rows and let them hide from the sweep forever.
|
|
174
|
+
*
|
|
175
|
+
* The predicate is index-served in id order (`doc_chunks` is keyed by `chunk_id`), so the LIMIT
|
|
176
|
+
* bounds what is READ, not only what is rebuilt — the same property `FactsDao.staleEntityRows`
|
|
177
|
+
* documents for the memory side.
|
|
178
|
+
*/
|
|
179
|
+
chunkEntityRows(version: number, limit: number, domain?: string): {
|
|
180
|
+
chunk_id: number;
|
|
181
|
+
text: string;
|
|
182
|
+
}[];
|
|
183
|
+
/** How many chunks still carry entity rows from older rules — the sweep's `deferred` source. */
|
|
184
|
+
countStaleEntityChunks(version: number, domain?: string): number;
|
|
130
185
|
/** Rebuild the FTS index from the content table (write-path maintenance). */
|
|
131
186
|
rebuildFts(): void;
|
|
132
187
|
/** Active chunks sharing at least one entity name with the query (jaccard candidate set). */
|
|
133
188
|
/**
|
|
134
|
-
* Entity-overlap candidates,
|
|
189
|
+
* Entity-overlap candidates, highest-score first and CAPPED (see the memory store's sibling;
|
|
190
|
+
* the score/order tail itself lives in `./shared.js#entityCandidateTail`).
|
|
135
191
|
*
|
|
136
192
|
* `limit` is applied per name-batch and the batches are unioned, so the result is bounded by
|
|
137
193
|
* `limit x ceil(names/batch)` — corpus-independent, which is what this query can promise. The
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DAO layer's SHARED ALGORITHMS AND CONTRACTS.
|
|
3
|
+
*
|
|
4
|
+
* DAOs are split by AGGREGATE, not by store (`docs/vector-repair-shared-flow.md` §2), and each
|
|
5
|
+
* aggregate keeps its own SQL text: replacing a readable statement with a generic
|
|
6
|
+
* table/column-parameterized builder would cost the comments' index reasoning without converging a
|
|
7
|
+
* business rule — a `count`/`get`/`delete`/`list` one-liner is a different aggregate's statement,
|
|
8
|
+
* not a fork (§4.5 "明确不收敛").
|
|
9
|
+
*
|
|
10
|
+
* What DOES get one implementation here is the part that is literally "one rule, two tables":
|
|
11
|
+
*
|
|
12
|
+
* - the entity-candidate leg's SCORE and ORDER, plus its batch/union loop
|
|
13
|
+
* ({@link entityCandidates} / {@link entityCandidateTail});
|
|
14
|
+
* - the batched `UPDATE … SET entities_version = ?` ({@link setEntitiesVersionBatch});
|
|
15
|
+
* - the short-query LIKE fallback's build/guard/double-bind protocol
|
|
16
|
+
* ({@link likeSubstringLeg});
|
|
17
|
+
* - the document-frequency query shape ({@link queryDocFrequency});
|
|
18
|
+
* - the batched "entity-name bags for a row-id set" read ({@link entityBags}).
|
|
19
|
+
*
|
|
20
|
+
* Each caller still supplies its own statement (or its `FROM`/name/filter pieces), so the SQL stays
|
|
21
|
+
* at the call site while the rule lives here exactly once. No base class: every export is a free
|
|
22
|
+
* function, the style `store/common.ts` established.
|
|
23
|
+
*/
|
|
24
|
+
import type { Db } from '../port.js';
|
|
25
|
+
/**
|
|
26
|
+
* The entity-candidate leg's ORDER contract — the tail every candidate statement ends with.
|
|
27
|
+
*
|
|
28
|
+
* The formula is the leg's own score (anchored Jaccard with a saturating union,
|
|
29
|
+
* `store/entity_leg.ts`): `shared / (queryWidth + min(total - shared, widthCap))`. Ordering by it is
|
|
30
|
+
* load-bearing, not cosmetic: `fuse` scales each leg by its maximum, and a survivor's scaled value
|
|
31
|
+
* is unchanged only when the ordering — and therefore the maximum — survives the trim. A narrower
|
|
32
|
+
* row sharing one entity can therefore outrank a wide one sharing two, and the cap keeps it.
|
|
33
|
+
* `total ASC` is only a deterministic tie-break among equal ratios, and the id makes the order total.
|
|
34
|
+
*
|
|
35
|
+
* `idColumn` is the aggregate's own id (`fa.fact_id` / `ce.chunk_id`); that, the `total` subquery's
|
|
36
|
+
* table and the filter predicates are the ONLY differences between the two statements.
|
|
37
|
+
*/
|
|
38
|
+
export declare function entityCandidateTail(idColumn: string): string;
|
|
39
|
+
/** One call to the shared entity-candidate runner — see {@link entityCandidates}. */
|
|
40
|
+
export interface EntityCandidateQuery {
|
|
41
|
+
/** The (anchor) names to match: one `IN (…)`, or one per batch when {@link batchNames}. */
|
|
42
|
+
names: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* NULL-able filter values bound right AFTER the name list and BEFORE `queryWidth`, in the SQL's
|
|
45
|
+
* own order (both callers spell their predicates as `(? IS NULL OR …)`, so a value the filter
|
|
46
|
+
* does not use is still bound as `null`).
|
|
47
|
+
*/
|
|
48
|
+
filterParams: readonly unknown[];
|
|
49
|
+
limit: number;
|
|
50
|
+
/** The query's FULL entity count (`W` in `store/entity_leg.ts`); `widthCap` is its saturating cap. */
|
|
51
|
+
queryWidth: number;
|
|
52
|
+
widthCap: number;
|
|
53
|
+
/**
|
|
54
|
+
* `true` (knowledge): run one statement per name-batch and UNION the ids, because the query can in
|
|
55
|
+
* principle carry more names than a single `IN (…)` may bind. A union of per-batch tops is NOT a
|
|
56
|
+
* top-N, so that caller must enforce its real cap on the union AFTER scoring.
|
|
57
|
+
* `false` (memory): one statement and one exact `LIMIT` — the store's names are request-bounded,
|
|
58
|
+
* so a single `LIMIT` is already the top-N and the cap argument above holds bit-for-bit.
|
|
59
|
+
*/
|
|
60
|
+
batchNames: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The full statement for one placeholder list; it must project `id`, `shared`, `total` and end
|
|
63
|
+
* with {@link entityCandidateTail}.
|
|
64
|
+
*/
|
|
65
|
+
sql: (placeholders: string) => string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Run an entity-candidate query: bind order, batching and union are shared; the SQL is the caller's.
|
|
69
|
+
*
|
|
70
|
+
* Bind order (identical in both aggregates): `<names …>, <filterParams …>, queryWidth, widthCap, limit`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function entityCandidates(db: Db, q: EntityCandidateQuery): number[];
|
|
73
|
+
/**
|
|
74
|
+
* The batched `UPDATE … SET entities_version = ?` shared by `FactsDao` and `ChunksDao`.
|
|
75
|
+
*
|
|
76
|
+
* Only the aggregate's `table` / `idColumn` differ. Returns the summed `changes`; the knowledge
|
|
77
|
+
* caller ignores it (its method's signature returns `void`), the memory caller's `number` is
|
|
78
|
+
* unchanged.
|
|
79
|
+
*/
|
|
80
|
+
export declare function setEntitiesVersionBatch(db: Db, table: string, idColumn: string, ids: readonly number[], version: number): number;
|
|
81
|
+
/** The per-aggregate pieces of the document-frequency query — see {@link queryDocFrequency}. */
|
|
82
|
+
export interface DocFrequencyQuery {
|
|
83
|
+
/** `FROM …` with its JOINs: everything between the projection and `WHERE`. */
|
|
84
|
+
from: string;
|
|
85
|
+
/** The name expression (`e.name`, `name`), used as the alias, the IN predicate and `GROUP BY`. */
|
|
86
|
+
nameExpr: string;
|
|
87
|
+
/** A constant predicate ANDed after the IN clause; it must add NO bind parameters. */
|
|
88
|
+
where?: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Document frequency — "how many rows carry each name".
|
|
92
|
+
*
|
|
93
|
+
* Shared by `EntitiesDao.activeDocFrequency` and `ChunksDao.docFrequency`; the only difference is
|
|
94
|
+
* the population the caller's `where`/`from` describe (memory counts ACTIVE facts, knowledge has no
|
|
95
|
+
* status column and counts chunks). Only the caller's QUERY names are looked up, so this is one
|
|
96
|
+
* indexed COUNT per name rather than a corpus scan. A name absent from the map has frequency 0.
|
|
97
|
+
*/
|
|
98
|
+
export declare function queryDocFrequency(db: Db, names: readonly string[], q: DocFrequencyQuery): Map<string, number>;
|
|
99
|
+
/** The two SQL fragments {@link likeSubstringLeg} hands back to the caller's statement builder. */
|
|
100
|
+
export interface LikeSubstringFragments {
|
|
101
|
+
/** The `WHERE` predicate (OR of the term patterns). */
|
|
102
|
+
any: string;
|
|
103
|
+
/** The score expression (count of the terms the row contains), to be aliased as `rank`. */
|
|
104
|
+
count: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The SHORT-QUERY LIKE fallback shared by `FactsDao.ftsSubstringSearch` and
|
|
108
|
+
* `ChunksDao.ftsSubstringSearch`.
|
|
109
|
+
*
|
|
110
|
+
* The predicate text itself already comes from `db/tokenizer.ts#likeSubstring`; this owns the
|
|
111
|
+
* protocol AROUND it, which both copies spelled out:
|
|
112
|
+
*
|
|
113
|
+
* 1. build the fragments — empty terms yield no patterns, which means "match nothing" ⇒ `[]`;
|
|
114
|
+
* 2. `count` lands in the SELECT and `any` in the WHERE, so the SAME pattern list is bound TWICE:
|
|
115
|
+
* once with the SELECT's placeholders, then — after the caller's filter params — with the
|
|
116
|
+
* WHERE's;
|
|
117
|
+
* 3. `LIMIT ?` last, defaulting to `-1` (unbounded; the store bounds the leg).
|
|
118
|
+
*
|
|
119
|
+
* `ORDER BY rank DESC, <id> ASC` stays in the caller's SQL: the id column is aggregate-specific and
|
|
120
|
+
* `rank` must read the SELECT alias, so the fragments and the patterns are built once and used twice.
|
|
121
|
+
*/
|
|
122
|
+
export declare function likeSubstringLeg(db: Db, column: string, terms: readonly string[], buildSql: (fragments: LikeSubstringFragments) => string, filterParams?: readonly unknown[], limit?: number): {
|
|
123
|
+
id: number;
|
|
124
|
+
rank: number;
|
|
125
|
+
}[];
|
|
126
|
+
/** The per-aggregate SQL pieces of {@link entityBags}. */
|
|
127
|
+
export interface EntityBagsQuery {
|
|
128
|
+
/** `FROM …` with its alias: `fact_entities fe` / `chunk_entities`. */
|
|
129
|
+
table: string;
|
|
130
|
+
/** The id expression, also the `WHERE … IN (…)` subject: `fe.fact_id` / `chunk_id`. */
|
|
131
|
+
keyColumn: string;
|
|
132
|
+
/** The name expression: `e.name` / `name`. */
|
|
133
|
+
nameColumn: string;
|
|
134
|
+
/** The `JOIN` that reaches the name vocabulary, for aggregates that have one (`''`/omitted for `chunk_entities`). */
|
|
135
|
+
joinEntities?: string;
|
|
136
|
+
/**
|
|
137
|
+
* The DEFAULT semantics — the one real difference between the two callers, so it is explicit:
|
|
138
|
+
*
|
|
139
|
+
* - `true` (memory): every requested id is pre-seeded with `[]` in the REQUEST's order, so the
|
|
140
|
+
* caller can assume `bags.get(id)` exists. Names are appended in row order after that;
|
|
141
|
+
* - `false` (knowledge): only rows that actually carry entities appear (the caller uses `?? []`).
|
|
142
|
+
*/
|
|
143
|
+
seedEmpty: boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Batched entity-name bags for a row-id set: `Map<rowId, names>`.
|
|
147
|
+
*
|
|
148
|
+
* Shared by `EntitiesDao.bagsForFacts` and `ChunksDao.entityBags` (§4.6.3): one `IN (…)` statement
|
|
149
|
+
* per `batches()` slice, rows folded into the map in first-seen order. The SQL SHAPE is the only
|
|
150
|
+
* aggregate-specific part (`table` / `keyColumn` / `nameColumn` / `joinEntities`); the crucial
|
|
151
|
+
* `seedEmpty` difference above is the caller's to declare and stays observable.
|
|
152
|
+
*/
|
|
153
|
+
export declare function entityBags(db: Db, ids: readonly number[], q: EntityBagsQuery): Map<number, string[]>;
|
|
@@ -20,6 +20,7 @@ export * from './dispatch.js';
|
|
|
20
20
|
export * from './runtime.js';
|
|
21
21
|
export * from './store/memory.js';
|
|
22
22
|
export * from './store/knowledge.js';
|
|
23
|
+
export * from './store/vector_repair.js';
|
|
23
24
|
export * from './store/hybrid.js';
|
|
24
25
|
export * from './store/source_picker.js';
|
|
25
26
|
export * from './store/lexical.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AdminRequest, KbAddRequest, KbConflictReport, KbRequest, QueryRequest, RecallRequest, RecallResult, RememberRequest } from '../mem-contract/index.js';
|
|
1
|
+
import type { AdminRequest, KbAddRequest, KbConflictReport, KbRequest, QueryRequest, RecallRequest, RecallResult, RememberRequest, VectorsFixReport } from '../mem-contract/index.js';
|
|
2
2
|
import { type AvantfLogger, type StatsSummary } from '../mem-contract/index.js';
|
|
3
3
|
import { type SemanticBackend } from '../mem-retrieval/index.js';
|
|
4
4
|
import { type LoadedConfig } from './config/loader.js';
|
|
@@ -153,7 +153,7 @@ export type AdminDispatch = {
|
|
|
153
153
|
}>): StoreResult<MemoryStore, 'vectorsDiagnose'>;
|
|
154
154
|
(req: Extract<AdminRequest, {
|
|
155
155
|
action: 'vectors_fix';
|
|
156
|
-
}>):
|
|
156
|
+
}>): Promise<VectorsFixReport>;
|
|
157
157
|
(req: Extract<AdminRequest, {
|
|
158
158
|
action: 'contradict_check';
|
|
159
159
|
}>): StoreResult<MemoryStore, 'checkContradictions'>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE entity-version sweep both stores run.
|
|
3
|
+
*
|
|
4
|
+
* `entities_version` is the stamp that says "these entity rows were produced by the CURRENT
|
|
5
|
+
* extraction rules". A rule change makes every row carrying an older stamp stale, and the only way
|
|
6
|
+
* to close that gap is to re-extract those rows' text — for memory the entity links, the triples and
|
|
7
|
+
* the HRR bundle derived from them; for knowledge the `chunk_entities` rows. That SKELETON was
|
|
8
|
+
* written twice and drifted apart in exactly the ways a copy drifts: memory had the in-flight guard,
|
|
9
|
+
* the budget clamp and the `{rebuilt, deferred, skipped}` report, while knowledge had the same
|
|
10
|
+
* selection inlined in `reindex` with no guard at all. This module is the one copy; the stores
|
|
11
|
+
* contribute a per-aggregate adapter (which rows, how to extract, what to write) and nothing else.
|
|
12
|
+
*
|
|
13
|
+
* Layering, deliberate: this is an ORCHESTRATION rule, not SQL, so it lives beside `common.ts` and
|
|
14
|
+
* `legs.ts` in `store/` — the DAO layer stays split by aggregate and keeps its own statements
|
|
15
|
+
* (`docs/vector-repair-shared-flow.md` §2 / §7).
|
|
16
|
+
*
|
|
17
|
+
* What must NOT move here: the extractor, the rows a store writes, and that store's side effects.
|
|
18
|
+
* Memory's transaction also replaces triples, moves the HRR bundle and re-queues the conflict check
|
|
19
|
+
* (both conflict legs read what the sweep rebuilt); knowledge replaces `chunk_entities` under
|
|
20
|
+
* `MAX_ENTITIES_PER_CHUNK`. Those are domain semantics, and folding them in would trade one
|
|
21
|
+
* duplicated loop for two conditional branches.
|
|
22
|
+
*
|
|
23
|
+
* @module store/entity_sweep
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* What one bounded sweep pass did.
|
|
27
|
+
*
|
|
28
|
+
* `rebuilt` counts rows this pass re-extracted AND wrote — its batch size when it filled it. The
|
|
29
|
+
* stamp is what makes a corpus current, so a row re-extracted to the same names still counts (and is
|
|
30
|
+
* not selected again); a row whose write FAILED does not, because its old stamp survives. It is
|
|
31
|
+
* deliberately not "rows whose extraction output changed", which no caller could use: they drain a
|
|
32
|
+
* stale corpus by looping until `deferred === 0`.
|
|
33
|
+
*/
|
|
34
|
+
export interface EntitySweepReport {
|
|
35
|
+
/** Rows this pass re-extracted and wrote (its batch size, when it filled it and nothing failed). */
|
|
36
|
+
rebuilt: number;
|
|
37
|
+
/** Rows still carrying an older stamp after this pass. */
|
|
38
|
+
deferred: number;
|
|
39
|
+
/** Another pass was already running, so this one did nothing (`deferred` is still current). */
|
|
40
|
+
skipped: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Rows one sweep pass may rebuild when the caller names no budget.
|
|
44
|
+
*
|
|
45
|
+
* `MemoryStore.reindexEntities` has always defaulted to this, and the CLI passes it explicitly; it
|
|
46
|
+
* lives here so the shared loop and that public re-export cannot drift. Bounds MEMORY, not only
|
|
47
|
+
* mission: a pass selects every chosen row's text before rebuilding the first one.
|
|
48
|
+
*/
|
|
49
|
+
export declare const ENTITY_SWEEP_BATCH = 2000;
|
|
50
|
+
/**
|
|
51
|
+
* A row the sweep may rebuild, as far as the SHARED loop is concerned.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately unconstrained: the loop only routes a row from the selection to the adapter's own
|
|
54
|
+
* `extractAndWriteEntities`, so it must not name any column. Memory's row carries
|
|
55
|
+
* `{ fact_id, content }`, knowledge's `{ chunk_id, text }` — parameterizing the loop over the
|
|
56
|
+
* store's row type is what keeps those names out of the orchestration layer.
|
|
57
|
+
*/
|
|
58
|
+
export type EntitySweepRow = object;
|
|
59
|
+
/**
|
|
60
|
+
* Per-store primitives. One adapter per aggregate, deliberately narrow.
|
|
61
|
+
*
|
|
62
|
+
* `staleEntityBatch` returns BOTH the scope's full stale count and a bounded slice in one read: the
|
|
63
|
+
* loop needs the remaining count for its report and the rows for its mission, and two queries would
|
|
64
|
+
* scan the same table twice. The two numbers must describe the same population: `total` counts every
|
|
65
|
+
* row the store's scope considers stale (memory: every ACTIVE fact; knowledge: every chunk of the
|
|
66
|
+
* sweep's domain scope), and `rows` is that population ordered by id, truncated to `limit`.
|
|
67
|
+
*/
|
|
68
|
+
export interface EntitySweepTarget<T extends EntitySweepRow> {
|
|
69
|
+
/**
|
|
70
|
+
* The scope's full stale count plus at most `limit` of those rows, ordered by id.
|
|
71
|
+
*
|
|
72
|
+
* `limit` is `maxPerPass`; the loop calls this once per pass. Both numbers must describe the SAME
|
|
73
|
+
* population — `total` is every stale row of the store's scope (memory: every ACTIVE fact;
|
|
74
|
+
* knowledge: every chunk of the sweep's domain), `rows` is that population truncated.
|
|
75
|
+
*/
|
|
76
|
+
staleEntityBatch(limit: number): {
|
|
77
|
+
total: number;
|
|
78
|
+
rows: readonly T[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Optional count-only read of the SAME population as {@link staleEntityBatch}'s `total`.
|
|
82
|
+
*
|
|
83
|
+
* It exists for the guard branch, which needs the current deferred count and no rows; absent, that
|
|
84
|
+
* branch falls back to `staleEntityBatch(0).total`. The loop holds the guard for the whole pass and
|
|
85
|
+
* each store is single-threaded, so the count cannot move between the two calls it makes.
|
|
86
|
+
*/
|
|
87
|
+
staleEntityCount?(): number;
|
|
88
|
+
/** Re-extract one row's text and write that store's derived rows. May throw; the loop isolates. */
|
|
89
|
+
extractAndWriteEntities(row: T): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Log a row this pass could not rebuild. Required, not defaulted: only the store knows which
|
|
92
|
+
* column names the row in its own logs (`fact_id` / `chunk_id`), and a shared warning cannot.
|
|
93
|
+
*/
|
|
94
|
+
onRowError(row: T, error: unknown): void;
|
|
95
|
+
}
|
|
96
|
+
/** Everything {@link sweepVersionedEntities} needs beyond the adapter. */
|
|
97
|
+
export interface EntitySweepOptions {
|
|
98
|
+
/** Rows this pass may rebuild; defaults to {@link ENTITY_SWEEP_BATCH}, clamped at 0. */
|
|
99
|
+
budget?: number;
|
|
100
|
+
/** Return `true` to stop before the next row (plugin unloading, runtime closed). */
|
|
101
|
+
shouldStop?: () => boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Re-extract the entity rows written by older extraction rules, ONE bounded pass.
|
|
105
|
+
*
|
|
106
|
+
* The contract, in order: take the guard (an overlapping pass reports `skipped` and does nothing);
|
|
107
|
+
* read the scope's stale count and at most `maxPerPass` rows; re-extract each row through the
|
|
108
|
+
* adapter, isolating failures; report `{ rebuilt, deferred, skipped }`.
|
|
109
|
+
*
|
|
110
|
+
* Not an unbounded drain, on purpose: this is reachable from a settings-page button and a periodic
|
|
111
|
+
* heartbeat, so it must not block on a whole stale corpus. Callers that want the corpus current loop
|
|
112
|
+
* until `deferred === 0` (the CLI does).
|
|
113
|
+
*
|
|
114
|
+
* A row whose extraction or write throws is skipped, not fatal: the transaction that stamps the
|
|
115
|
+
* version never ran, so the row is still stale and the NEXT pass retries it — the same retry contract
|
|
116
|
+
* the knowledge store used per chunk. Letting the throw escape would instead abandon the rest of the
|
|
117
|
+
* batch with no report of what was done.
|
|
118
|
+
*
|
|
119
|
+
* @param guard the store's stable in-flight identity (see {@link SWEEPING})
|
|
120
|
+
*/
|
|
121
|
+
export declare function sweepVersionedEntities<T extends EntitySweepRow>(target: EntitySweepTarget<T>, guard: object, opts?: EntitySweepOptions): Promise<EntitySweepReport>;
|
|
@@ -234,3 +234,53 @@ export declare function applyOutputBudget<H extends BudgetInput>(config: Config,
|
|
|
234
234
|
* the slice would spend the budget on hits that are about to be dropped.
|
|
235
235
|
*/
|
|
236
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';
|
|
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
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;
|
|
@@ -110,6 +111,15 @@ export declare class KnowledgeStore {
|
|
|
110
111
|
private readonly docFiles;
|
|
111
112
|
/** > 0 while a batch (directory import) is writing: commits and baseline refreshes wait. */
|
|
112
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;
|
|
113
123
|
/**
|
|
114
124
|
* Per-file stamps (`path → mtimeMs:size`) and the file-set they were taken from, so a reconcile
|
|
115
125
|
* can ask "what changed?" for the price of one `stat` per document instead of reading and
|
|
@@ -132,13 +142,18 @@ export declare class KnowledgeStore {
|
|
|
132
142
|
/** The store config file `addDomain` writes the allowlist back to (`knowledgeConfigPath(home)`). */
|
|
133
143
|
domainConfigPath: string, logger?: AvantfLogger | undefined);
|
|
134
144
|
/**
|
|
135
|
-
* 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.
|
|
147
|
+
*
|
|
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.
|
|
136
151
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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.
|
|
142
157
|
*/
|
|
143
158
|
private reloadIndex;
|
|
144
159
|
/**
|
|
@@ -243,8 +258,6 @@ export declare class KnowledgeStore {
|
|
|
243
258
|
warnings?: string[]): Promise<IngestResult>;
|
|
244
259
|
/** Materialize one document's editable copy; returns the `file` / `file_error` result fields. */
|
|
245
260
|
private writeManagedFile;
|
|
246
|
-
/** Batched eviction from the live vector index (removeMany when the backend supports it). */
|
|
247
|
-
private evictVectors;
|
|
248
261
|
/** The resolved ingestion boundary (see `ingest_guard.ts`). */
|
|
249
262
|
private get ingestLimits();
|
|
250
263
|
/**
|
|
@@ -288,7 +301,8 @@ export declare class KnowledgeStore {
|
|
|
288
301
|
*/
|
|
289
302
|
private writeReusedVectors;
|
|
290
303
|
/**
|
|
291
|
-
*
|
|
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.
|
|
292
306
|
*
|
|
293
307
|
* The model call is deliberately NOT batched, and that is a MEASURED decision, not an omission:
|
|
294
308
|
* a batched forward pass pads each item to the longest in its group, and real chunks span 53–550
|
|
@@ -299,7 +313,17 @@ export declare class KnowledgeStore {
|
|
|
299
313
|
* length-sorted) for callers whose texts suit it.
|
|
300
314
|
*
|
|
301
315
|
* What DID pay off is the write side: one commit per `WRITE_BATCH` chunks instead of one per
|
|
302
|
-
* 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.
|
|
303
327
|
*/
|
|
304
328
|
private encodeAndStore;
|
|
305
329
|
/**
|
|
@@ -309,6 +333,59 @@ export declare class KnowledgeStore {
|
|
|
309
333
|
* of silently mixing two spaces. Public so diagnostics and tests can name the space.
|
|
310
334
|
*/
|
|
311
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.
|
|
383
|
+
*/
|
|
384
|
+
migrateVectors(opts?: {
|
|
385
|
+
batchSize?: number;
|
|
386
|
+
shouldStop?: () => boolean;
|
|
387
|
+
onProgress?: (progress: VectorMigrationOutcome) => void;
|
|
388
|
+
}): Promise<VectorMigrationOutcome>;
|
|
312
389
|
/**
|
|
313
390
|
* A stored vector is reusable when it exists, matches the space, and its text is unchanged.
|
|
314
391
|
*
|
|
@@ -319,8 +396,28 @@ export declare class KnowledgeStore {
|
|
|
319
396
|
* the store would keep serving a vector for text it no longer holds.
|
|
320
397
|
*/
|
|
321
398
|
private vectorReusable;
|
|
322
|
-
/**
|
|
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). */
|
|
323
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;
|
|
324
421
|
/**
|
|
325
422
|
* Ingest local files/directories. Every path goes through the SAME boundary as
|
|
326
423
|
* `ingestUri` — a bare `kb_import` on `~/.ssh` would otherwise be the exact
|
|
@@ -459,6 +556,13 @@ export declare class KnowledgeStore {
|
|
|
459
556
|
* re-encoding (performance review §7.7 / P8).
|
|
460
557
|
*/
|
|
461
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;
|
|
462
566
|
/**
|
|
463
567
|
* Sync relevance probe for the plugin's conditional hint (DESIGN §12). Same contract as
|
|
464
568
|
* `MemoryStore.lexicalProbe` — see `store/lexical.ts`.
|