@avantf/dsh-mem 0.4.1 → 0.6.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.
Files changed (52) hide show
  1. package/README.md +19 -1
  2. package/lib/client.js +40 -9
  3. package/lib/engine/mem/config/config_files.d.ts +1 -1
  4. package/lib/engine/mem/db/dao/chunks.d.ts +20 -1
  5. package/lib/engine/mem/db/dao/entities.d.ts +35 -11
  6. package/lib/engine/mem/db/dao/fact_sources.d.ts +53 -0
  7. package/lib/engine/mem/db/dao/facts.d.ts +127 -12
  8. package/lib/engine/mem/db/invariants.d.ts +26 -0
  9. package/lib/engine/mem/db/tokenizer.d.ts +23 -0
  10. package/lib/engine/mem/db/vectors.d.ts +40 -8
  11. package/lib/engine/mem/entities/extract.d.ts +1 -1
  12. package/lib/engine/mem/eval/derive.d.ts +120 -0
  13. package/lib/engine/mem/eval/derived_runner.d.ts +88 -0
  14. package/lib/engine/mem/eval/metrics.d.ts +24 -0
  15. package/lib/engine/mem/eval/runner.d.ts +8 -1
  16. package/lib/engine/mem/index.d.ts +1 -1
  17. package/lib/engine/mem/modelBootstrap.d.ts +2 -2
  18. package/lib/engine/mem/runtime.d.ts +1 -1
  19. package/lib/engine/mem/store/common.d.ts +33 -11
  20. package/lib/engine/mem/store/entity_leg.d.ts +95 -0
  21. package/lib/engine/mem/store/floors.d.ts +63 -31
  22. package/lib/engine/mem/store/hybrid.d.ts +21 -26
  23. package/lib/engine/mem/store/knowledge.d.ts +18 -9
  24. package/lib/engine/mem/store/lexical.d.ts +44 -0
  25. package/lib/engine/mem/store/memory.d.ts +151 -24
  26. package/lib/engine/mem/store/secret_guard.d.ts +34 -0
  27. package/lib/engine/mem/store/time_window.d.ts +74 -0
  28. package/lib/engine/mem-contract/config.d.ts +9 -16
  29. package/lib/engine/mem-contract/family.d.ts +1 -1
  30. package/lib/engine/mem-contract/tools.d.ts +10 -0
  31. package/lib/engine/mem-contract/types.d.ts +164 -9
  32. package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +37 -7
  33. package/lib/engine/mem-retrieval/fusion.d.ts +14 -1
  34. package/lib/engine/mem-retrieval/index.d.ts +1 -3
  35. package/lib/engine/mem-retrieval/interfaces.d.ts +38 -14
  36. package/lib/engine/mem-retrieval/registry.d.ts +2 -4
  37. package/lib/engine/mem-retrieval/representation.d.ts +50 -0
  38. package/lib/engine/mem-retrieval/stats.d.ts +1 -3
  39. package/lib/index.js +2003 -565
  40. package/lib/types/client/index.d.ts.map +1 -1
  41. package/lib/types/index.d.ts +1 -1
  42. package/lib/types/index.d.ts.map +1 -1
  43. package/lib/types/prompt.d.ts +7 -0
  44. package/lib/types/prompt.d.ts.map +1 -1
  45. package/lib/types/remote.d.ts +12 -6
  46. package/lib/types/remote.d.ts.map +1 -1
  47. package/lib/types/vectorMigration.d.ts +44 -0
  48. package/lib/types/vectorMigration.d.ts.map +1 -0
  49. package/package.json +5 -5
  50. package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +0 -52
  51. package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +0 -10
  52. package/lib/engine/mem-retrieval/rerank.d.ts +0 -13
@@ -5,9 +5,9 @@
5
5
  * MAXIMUM, so every leg's head is 1.0 by construction and the fused number is only comparable
6
6
  * WITHIN one query. A cutoff on the fused score would therefore mean a different thing per query
7
7
  * (and would move whenever a leg's head changed). The legs' raw numbers do have an absolute scale —
8
- * cosine for the semantic leg, the Jaccard ratio for the entity leg, "distinct query terms hit" for
9
- * FTS — so that is where the floors live. The side benefit is that flooring a leg's tail cannot
10
- * move its maximum, so the cap-invariance §20.17 relies on is untouched.
8
+ * cosine for the semantic leg, anchored Jaccard for the entity leg (`store/entity_leg.ts`),
9
+ * "distinct query terms hit" for FTS — so that is where the floors live. The side benefit is that
10
+ * flooring a leg's tail cannot move its maximum, so the cap-invariance §20.17 relies on is untouched.
11
11
  *
12
12
  * THREE FLOORS, ONE SHAPE. `min_semantic_similarity` / `min_fts_terms` / `min_jaccard`; each `0`
13
13
  * means "this leg is not gated", and a candidate EQUAL to its floor is KEPT (only strictly-lower
@@ -49,10 +49,25 @@
49
49
  * of its own is bounded instead: it costs one extra query, only when the strict answer was empty,
50
50
  * and it can still report "nothing relevant" for a genuinely unrelated question.
51
51
  *
52
- * FTS REACHABILITY. `min_fts_terms` counts distinct query terms a row hits, but a query can only
53
- * produce so many terms (`relevanceTerms`: latin words ≥5 chars + CJK 3-grams) — a 3-character CJK
54
- * query yields exactly ONE trigram, so a configured bar of 2 is unreachable and that leg is empty
55
- * for it by construction. The effective bar is therefore `min(configured, termCount)`.
52
+ * FTS REACHABILITY, AND THE SHORT-QUERY FALLBACK. `min_fts_terms` counts distinct query terms a row
53
+ * hits, but a query can only produce so many terms — a 3-character CJK query yields exactly ONE
54
+ * trigram, so a configured bar of 2 is unreachable and that leg is empty for it by construction. The
55
+ * effective bar is therefore `min(configured, termCount)`, and `termCount` is that of the text being
56
+ * GRADED — with 方案 A's augmentation the original and the rewrite are different texts with
57
+ * different counts, so each resolves its own bar (`store/hybrid.ts`). Resolving the rewrite from the
58
+ * original's count is the defect that let a single incidental trigram pass a bar it could not reach
59
+ * (2026-10-04).
60
+ *
61
+ * That clamp still leaves the shape with NO term at all: a 2-char CJK query (`buildFtsQuery` returns
62
+ * `null` — the trigram index holds no term under three characters, measured on the live store). The
63
+ * term set is therefore {@link gradedTerms}: when the query yields no index-expressible term, the
64
+ * 2-char CJK runs themselves become the terms, and the legs search/grade them as SUBSTRINGS
65
+ * (`content LIKE '%…%'`, the finer predicate an FTS5 trigram table still exposes; see
66
+ * `store/lexical.ts`). The clamp stays the same rule and stays honest — one substring term is
67
+ * `min(configured, 1)`, and {@link applyTermFloor} re-derives it from the query it is grading, so
68
+ * the reported number is the applied number. The guard against the noise a 2-char term invites is
69
+ * the term itself: the row must CONTAIN the run contiguously (not merely share a trigram), and a
70
+ * multi-run query must contain `min(configured, runs)` of them.
56
71
  *
57
72
  * @module store/floors
58
73
  */
@@ -82,16 +97,20 @@ export type FloorLeg = 'semantic' | 'fts' | 'jaccard';
82
97
  * - fts: NOT a constant — {@link looseTermFloor}, half the query's distinct terms with a floor of
83
98
  * 1. `1` here is the "no terms to scale by" baseline (the previously constant value); a query
84
99
  * that cannot produce terms is not graded at all, and one that produces a single term (every
85
- * 3-char CJK query) is already graded against that one term by the reachability clamp.
86
- * - jaccard **0.15** (positive — the previous `0` was the ungated hole this closes). Measured, not
87
- * guessed, on two corpora: (a) the live store, 64 active facts, the common-entity questions a
88
- * real user asks about this repo ("插件怎么安装" / "任务怎么拆分" / "知识库在哪里" …) put their
89
- * TOP non-answering entity candidate at Jaccard 0.0909 and most at 0.02–0.07, so 0.15 removes that
90
- * whole tail; (b) the gold-labelled 35-query eval set, where the LOWEST Jaccard any answering pair
91
- * reached is 0.25 (「老王喜欢什么风格」) — a bottom line above 0.25 starts dropping gold pairs,
92
- * while 0.15 filters none. The entity leg cannot fully separate the two classes on short queries
93
- * (a one-entity query against a one-entity fact is 1.0 by construction, and non-gold pairs reach
94
- * 0.667), so this is deliberately a "no incidental small overlap" line, not a relevance classifier.
100
+ * 3-char CJK query, and every 2-char one through the substring fallback) is already graded
101
+ * against that one term by the reachability clamp.
102
+ * - jaccard **0.15** (positive — the previous `0` was the ungated hole this closes). The metric was
103
+ * recalibrated on 2026-10-04 (`store/entity_leg.ts`): the leg still scores a Jaccard ratio, but
104
+ * over the query's ANCHOR entities and with the fact's extra-entity term SATURATED at
105
+ * `ENTITY_UNION_CAP`. 0.15 on that unit means "shares at least one discriminative entity" for any
106
+ * query with up to six anchors (`1/6 = 0.167 > 0.15`), i.e. the bottom line that cannot be lowered
107
+ * to "any incidental overlap" without admitting a length-dependent fraction. The old Jaccard
108
+ * calibration, kept for the record: (a) on the live store the top non-answering candidate for
109
+ * common-entity questions sat at 0.0909 and most at 0.02–0.07; (b) the lowest Jaccard any
110
+ * answering pair on the gold 35-query set reached was 0.25. The entity leg cannot separate the
111
+ * classes on short queries by itself, so this is deliberately a "no incidental small overlap"
112
+ * line, not a relevance classifier — see `store/entity_leg.ts` for why the fact's own width was
113
+ * saturated rather than used as the separator.
95
114
  */
96
115
  export declare const LOOSE_FLOORS: RetrievalFloors;
97
116
  /**
@@ -112,21 +131,29 @@ export declare function emptyFloorDrops(): RetrievalFloorDrops;
112
131
  * The FTS bar actually applied to a query carrying `termCount` distinct terms.
113
132
  *
114
133
  * A query cannot be graded against more terms than it has (see the module comment: a 3-char CJK
115
- * query has one trigram), so the configured bar is CLAMPED to the reachable maximum. `termCount`
116
- * `0`/omitted leaves the value alone — such a query is not graded at all by {@link applyTermFloor},
117
- * and changing the REPORTED value there would misdescribe what the caller is reading. `0` stays `0`
118
- * (the operator's off switch).
119
- *
120
- * This is the ONE place the clamp lives; `resolveFloors` (what the result reports) and
121
- * `applyTermFloor` (what the leg does) both call it, so the number the caller reads is the number
122
- * the leg used.
134
+ * query has one trigram, and a 2-char one has one substring term), so the configured bar is CLAMPED
135
+ * to the reachable maximum. `termCount` `0`/omitted leaves the value alone — such a query is not
136
+ * graded at all by {@link applyTermFloor}, and changing the REPORTED value there would misdescribe
137
+ * what the caller is reading. `0` stays `0` (the operator's off switch).
138
+ *
139
+ * This is the ONE place the clamp lives; `resolveFloors` (what the result reports / a run is handed)
140
+ * and `applyTermFloor` (what the leg does) both call it, so the number a run reads is the number its
141
+ * leg used. It is a `min`, i.e. one-directional: it can LOWER an unreachable configured bar but never
142
+ * RAISE a bar inherited from another text, which is why the caller must resolve per graded text
143
+ * rather than reusing one text's resolution for another.
123
144
  */
124
145
  export declare function effectiveTermFloor(floor: number, termCount?: number): number;
125
146
  /** The per-query floor resolution inputs beyond the configuration. */
126
147
  export interface FloorResolution {
127
148
  /** `loose` lowers each configured floor (see {@link LOOSE_FLOORS}); absent means `strict`. */
128
149
  profile?: FloorProfile;
129
- /** This query's distinct term count (`relevanceTerms(query).length`), for the FTS clamp. */
150
+ /**
151
+ * The distinct term count of the text being GRADED (`gradedTerms(text).length`), for the FTS
152
+ * clamp. It must describe the query the legs will actually run on: under 方案 A's augmentation the
153
+ * rewrite is a DIFFERENT text with its own term count, and resolving both runs from the original's
154
+ * count is the defect that let one incidental trigram through (see `store/hybrid.ts`). Callers with
155
+ * several graded texts resolve once per text.
156
+ */
130
157
  termCount?: number;
131
158
  /**
132
159
  * Which legs a `'loose'` profile may lower, from {@link droppedLegs} of the pass that made the
@@ -170,16 +197,21 @@ export declare function applyScoreFloor(scores: Map<number, number>, floor: numb
170
197
  *
171
198
  * Substring containment, case-insensitively: FTS5's trigram tokenizer matches a quoted phrase by
172
199
  * consecutive trigrams and is ASCII-case-insensitive, which is the same predicate as "the term
173
- * occurs in the text". `relevanceTerms` lowercases its latin words, so the comparison has to.
200
+ * occurs in the text" — and the 2-char fallback terms ARE substrings by construction
201
+ * (`store/lexical.ts`), so one predicate covers both. `relevanceTerms` lowercases its latin words,
202
+ * so the comparison has to.
174
203
  */
175
204
  export declare function countMatchedTerms(text: string, terms: readonly string[]): number;
176
205
  /**
177
206
  * Apply the per-row FTS floor to one FTS leg's scores.
178
207
  *
179
- * The terms come from `relevanceTerms(query)` (latin words ≥ 5 chars + every CJK 3-gram) — the same
180
- * tokenisation the conditional-hint probe uses, so "relevant enough" means one thing in this repo.
181
- * A query with NO such term is not graded at all: there is nothing to count, and dropping every row
182
- * on a technicality would turn a 3–4 char latin query (`buildFtsQuery` still emits it) into silence.
208
+ * The terms come from `gradedTerms(query)` — latin words ≥ 5 chars + every CJK 3-gram, and (only
209
+ * when the query produces no such term) the 2-char CJK runs as substrings. The indexed terms are
210
+ * the same tokenisation the conditional-hint probe uses, so "relevant enough" means one thing in
211
+ * this repo; the fallback exists because the hint path may stay silent while the LEG must not
212
+ * (`store/lexical.ts`). A query with NO such term is not graded at all: there is nothing to count,
213
+ * and dropping every row on a technicality would turn a 3–4 char latin query (`buildFtsQuery` still
214
+ * emits it) into silence.
183
215
  *
184
216
  * The bar itself is {@link effectiveTermFloor}: a query that can only produce one term (a 3-char CJK
185
217
  * query is one trigram) is measured against that one term, not against a configured 2 it could never
@@ -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
- * rerank → slice → output budget → health event). The legs themselves are legitimately
7
- * store-specific — different tables, different filters (`category` vs `domain`/`source`), and only
8
- * memory has the HRR probe — but the ORCHESTRATION is one algorithm, and two copies of it drifted:
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 Reranker, type SemanticBackend } from '../../mem-retrieval/index.js';
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 Jaccard floor; naming it separately keeps a narrowed candidate set
67
- * attributable without double-counting the Jaccard leg's own drops.
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, the reranker's input, and the hit bodies. */
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,8 @@ 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 three steps is load-bearing: rerank the over-fetched pool, THEN slice to
237
- * `limit`, THEN apply the output budget, and only then tell the store what was returned. Slicing
238
- * before the rerank would rank an arbitrary subset; budgeting before the slice would spend the
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>>;
@@ -1,5 +1,5 @@
1
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 { Reranker, SemanticBackend, VectorStore } from '../../mem-retrieval/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';
@@ -43,6 +43,8 @@ export interface KnowledgeSearchOptions {
43
43
  * store-level "last result" field would race two concurrent queries.
44
44
  */
45
45
  onResult?: (result: HybridResult<RecallHit>) => void;
46
+ /** P-01: attach per-leg evidence (`scores` / `final`) to every hit. Default false. */
47
+ includeScores?: boolean;
46
48
  }
47
49
  /**
48
50
  * One identity a write will land on: the title it will use, the resolved local path it would read
@@ -87,7 +89,6 @@ export type IngestMode = 'add' | 'replace';
87
89
  export declare class KnowledgeStore {
88
90
  private readonly semantic;
89
91
  private readonly vstore;
90
- private readonly reranker;
91
92
  /** The store config file `addDomain` writes the allowlist back to (`knowledgeConfigPath(home)`). */
92
93
  private readonly domainConfigPath;
93
94
  private readonly logger?;
@@ -127,7 +128,7 @@ export declare class KnowledgeStore {
127
128
  private readonly allowedDomains;
128
129
  /** Whether the config allowlist is active (non-empty); `false` = `domains: []` = no restriction. */
129
130
  private readonly domainsRestricted;
130
- constructor(dbPath: string, config: Config, kbConfig: KnowledgeConfig, semantic: SemanticBackend, vstore: VectorStore, reranker: Reranker,
131
+ constructor(dbPath: string, config: Config, kbConfig: KnowledgeConfig, semantic: SemanticBackend, vstore: VectorStore,
131
132
  /** The store config file `addDomain` writes the allowlist back to (`knowledgeConfigPath(home)`). */
132
133
  domainConfigPath: string, logger?: AvantfLogger | undefined);
133
134
  /**
@@ -302,11 +303,12 @@ export declare class KnowledgeStore {
302
303
  */
303
304
  private encodeAndStore;
304
305
  /**
305
- * Identity of the vector space this store currently writes into (DESIGN §20): a change of
306
- * backend, model or width makes every stored chunk vector stale, and the whole point of
307
- * recording it is that such a change is DETECTED instead of silently mixing two spaces.
306
+ * Identity of the vector space this store currently writes into (DESIGN §20): the representation
307
+ * fingerprint — backend, model, width AND the backend's declared pooling/normalization/input
308
+ * window/model revision — so any representation change makes stored chunk vectors stale instead
309
+ * of silently mixing two spaces. Public so diagnostics and tests can name the space.
308
310
  */
309
- private vectorSpace;
311
+ vectorSpace(): string;
310
312
  /**
311
313
  * A stored vector is reusable when it exists, matches the space, and its text is unchanged.
312
314
  *
@@ -434,7 +436,7 @@ export declare class KnowledgeStore {
434
436
  * and the chunk→hit mapping.
435
437
  *
436
438
  * The limit normalization, the over-fetch factor, the leg cap, the capped-leg counter, the
437
- * degraded weights, the rerank and the output budget all live there now. They used to be copied
439
+ * degraded weights and the output budget all live there now. They used to be copied
438
440
  * here, and every fix landed on the memory side only — a `NaN` limit reached this store's SQL and
439
441
  * took the whole cross-store query down with it, `retriever.over_fetch_factor` meant something
440
442
  * different per store, and a capped knowledge leg was invisible to the health counters.
@@ -463,7 +465,14 @@ export declare class KnowledgeStore {
463
465
  */
464
466
  lexicalProbe(text: string, stopAt?: number): LexicalProbe;
465
467
  private ftsPath;
466
- /** Entity overlap against entities extracted AT INGEST time (chunk_entities) — no corpus scan. */
468
+ /**
469
+ * Entity overlap against entities extracted AT INGEST time (chunk_entities) — no corpus scan.
470
+ *
471
+ * Same metric as the memory store's sibling (`store/entity_leg.ts` carries the measurement and
472
+ * the reasoning): anchored Jaccard with a saturating union over the query's rare entities, so a
473
+ * wide chunk cannot dilute the denominator and a corpus-wide word cannot drag the whole knowledge
474
+ * base in.
475
+ */
467
476
  private jaccardPath;
468
477
  }
469
478
  export interface Chunk {
@@ -25,6 +25,20 @@
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
  */
29
43
  /** How many of an input's terms a store holds. `terms === 0` means the input had nothing to test. */
30
44
  export interface LexicalProbe {
@@ -46,6 +60,36 @@ export type RelevanceHit = boolean;
46
60
  * caller runs one `LIMIT 1` MATCH per term and counts the hits.
47
61
  */
48
62
  export declare function relevanceTerms(text: string, cap?: number): string[];
63
+ /**
64
+ * The terms the FTS leg falls back to when the trigram index can express NONE of the query.
65
+ *
66
+ * A 2-char CJK run IS the finer granularity the FTS5 trigram TABLE can still answer — not as an
67
+ * indexed token (the vocabulary holds 3-character terms only, measured; a 2-char `MATCH`, quoted or
68
+ * prefixed, returns zero rows) but as the substring predicate FTS5 exposes on a trigram table
69
+ * (`content LIKE '%…%'`, which falls back to a scan for patterns under three characters). One-char
70
+ * runs are deliberately NOT included: a single character is not evidence (the same reason
71
+ * {@link looksRelevant} needs two terms), and it would make the leg match a large share of any
72
+ * corpus. Latin runs are not included either — `buildFtsQuery` already admits latin tokens from
73
+ * three characters, and the module's own floor (5, `MIN_LATIN`) is a separate, measured decision.
74
+ *
75
+ * PRECISION GUARD. The fallback is graded by the SAME per-row floor as the indexed path and by the
76
+ * reachability clamp: for a single 2-char term the effective bar is 1, and the term is a substring,
77
+ * so "the row contains the query's characters CONTIGUOUSLY" is what admits it — not "shares one
78
+ * incidental trigram". A two-run query ('李娜 张伟') produces two substring terms and is judged
79
+ * against `min(configured, 2)`, i.e. both runs must occur in the row.
80
+ */
81
+ export declare function substringTerms(text: string, cap?: number): string[];
82
+ /**
83
+ * The term set the FTS leg is SEARCHED and GRADED with — the one definition of "the query's terms"
84
+ * for that leg (`store/floors.ts` grades with it, `store/hybrid.ts` resolves the reachability clamp
85
+ * from it, and the stores build their MATCH / substring probe from it).
86
+ *
87
+ * The fallback is whole-query, not per run: any text that yields even one index-expressible term is
88
+ * returned UNCHANGED from {@link relevanceTerms}. That is what keeps every long query bit-identical
89
+ * (a mixed text like `缓存失效 李娜` keeps only its trigrams, exactly as before) and confines the
90
+ * new behaviour to the shape that had NO leg at all.
91
+ */
92
+ export declare function gradedTerms(text: string, cap?: number): string[];
49
93
  /**
50
94
  * The gate itself: is this store worth telling the model about?
51
95
  *