@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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two arms the P-02 regression net runs, over the derived cases from `derive.ts`.
|
|
3
|
+
*
|
|
4
|
+
* - `stubRank` / `runDerivedCaseStub`: a DETERMINISTIC, model-free retriever that scores a fact by
|
|
5
|
+
* how many of the query's literal fragments it contains. It exists to prove the FIXTURE, not the
|
|
6
|
+
* product: a planted carrier that literally contains the bridge must outrank the gold in the base
|
|
7
|
+
* arm, and removing it must put the gold back at rank 1. That is the "counterfactual self-check"
|
|
8
|
+
* the collision sensitivity requirement asks for, and it needs no model to be meaningful.
|
|
9
|
+
* - `runDerivedCaseReal`: the same cases driven through a real runtime (`remember` + `recall`), for
|
|
10
|
+
* the nightly job. It is duck-typed so this module does not depend on the runtime.
|
|
11
|
+
*
|
|
12
|
+
* The two arms share ONE outcome shape, so a report can say which arm produced which numbers.
|
|
13
|
+
*/
|
|
14
|
+
import { type DerivedCase, type GuardKind } from './derive.js';
|
|
15
|
+
export interface RankedFacts {
|
|
16
|
+
/** Fact indices, best first. */
|
|
17
|
+
ranked: number[];
|
|
18
|
+
/** The stub score of each ranked fact (parallel to `ranked`). */
|
|
19
|
+
scores: number[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Deterministic lexical-overlap stub: score = number of the query's distinct fragments the fact
|
|
23
|
+
* contains literally. Raw count, deliberately NOT length-normalized — the pathology under test is
|
|
24
|
+
* "a long unrelated note that happens to contain the bridge out-scores the short gold", which a
|
|
25
|
+
* per-character normalization would erase.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stubRank(query: string, facts: readonly string[], k?: number): RankedFacts;
|
|
28
|
+
export interface DerivedQueryOutcome {
|
|
29
|
+
query: string;
|
|
30
|
+
guard: GuardKind;
|
|
31
|
+
ranked: number[];
|
|
32
|
+
scores: number[];
|
|
33
|
+
gold_rank: number | null;
|
|
34
|
+
carrier_rank: number | null;
|
|
35
|
+
must_include_ok: boolean;
|
|
36
|
+
must_exclude_ok: boolean;
|
|
37
|
+
/** The fixture's own recorded collision rate (fraction of non-gold facts sharing a fragment). */
|
|
38
|
+
collision_rate: number;
|
|
39
|
+
}
|
|
40
|
+
export interface CollisionCheckOutcome {
|
|
41
|
+
/** `true` when the planted carrier really does outrank the gold in the base arm. */
|
|
42
|
+
base_carrier_before_gold: boolean;
|
|
43
|
+
/** `true` when removing the carrier puts the gold first (the counterfactual arm). */
|
|
44
|
+
counterfactual_gold_top1: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface DerivedCaseOutcome {
|
|
47
|
+
id: string;
|
|
48
|
+
/** Arm label so a merged report cannot confuse stub numbers with real ones. */
|
|
49
|
+
arm: 'stub' | 'real';
|
|
50
|
+
queries: DerivedQueryOutcome[];
|
|
51
|
+
collision_self_check: CollisionCheckOutcome | null;
|
|
52
|
+
}
|
|
53
|
+
export declare function runDerivedCaseStub(c: DerivedCase): DerivedCaseOutcome;
|
|
54
|
+
export interface DerivedSuiteOutcome {
|
|
55
|
+
arm: 'stub';
|
|
56
|
+
cases: DerivedCaseOutcome[];
|
|
57
|
+
all_collision_self_checks_pass: boolean;
|
|
58
|
+
/** A retriever that emits any query containing a time expression has broken the guard. */
|
|
59
|
+
any_time_word_query: boolean;
|
|
60
|
+
/** Every `irrelevant` negative-control query must come back empty. */
|
|
61
|
+
all_irrelevant_empty: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare function runDerivedSuiteStub(cases: readonly DerivedCase[]): DerivedSuiteOutcome;
|
|
64
|
+
/** The slice of the runtime the real arm needs. Duck-typed on purpose (no runtime import). */
|
|
65
|
+
export interface DerivedRuntimePort {
|
|
66
|
+
remember(req: {
|
|
67
|
+
action: 'add';
|
|
68
|
+
content: string;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
fact_id: number;
|
|
71
|
+
}>;
|
|
72
|
+
recall(req: {
|
|
73
|
+
action: 'search';
|
|
74
|
+
query: string;
|
|
75
|
+
limit?: number;
|
|
76
|
+
}): Promise<{
|
|
77
|
+
hits: {
|
|
78
|
+
ref_id: number;
|
|
79
|
+
}[];
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
/** Seed a case's facts and return the fact ids in `setup_facts` order. */
|
|
83
|
+
export declare function seedDerivedFacts(rt: DerivedRuntimePort, facts: readonly string[]): Promise<number[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Drive one derived case through a real runtime. The `k` of each query is honored with `limit`
|
|
86
|
+
* (the derived queries use k=3), and the returned order is the store's own.
|
|
87
|
+
*/
|
|
88
|
+
export declare function runDerivedCaseReal(rt: DerivedRuntimePort, c: DerivedCase): Promise<DerivedCaseOutcome>;
|
|
@@ -23,3 +23,27 @@ export interface AggregateMetrics {
|
|
|
23
23
|
must_exclude_pass_rate: number;
|
|
24
24
|
}
|
|
25
25
|
export declare function aggregate(perQuery: PerQueryMetrics[]): AggregateMetrics;
|
|
26
|
+
/** Discounted cumulative gain at k (binary relevance). */
|
|
27
|
+
export declare function dcgAtK(actual: number[], expected: number[], k: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* nDCG@k (binary relevance).
|
|
30
|
+
*
|
|
31
|
+
* 0 when there is nothing relevant to find (and when the ideal ranking is empty), never NaN — the
|
|
32
|
+
* per-query value is averaged and a NaN would poison the whole aggregate.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ndcgAtK(actual: number[], expected: number[], k: number): number;
|
|
35
|
+
/** How many relevant results appear in the first `n` slots. */
|
|
36
|
+
export declare function relevantInTop(actual: number[], expected: number[], n: number): number;
|
|
37
|
+
/** The window "top-3 relevant count" is measured over. */
|
|
38
|
+
export declare const TOP_N = 3;
|
|
39
|
+
export interface RankingMetrics {
|
|
40
|
+
n_queries: number;
|
|
41
|
+
mean_ndcg_at_k: number;
|
|
42
|
+
/** Relevant results inside the top {@link TOP_N}, summed over queries. */
|
|
43
|
+
top3_relevant_total: number;
|
|
44
|
+
/** Mean relevant results inside the top {@link TOP_N}. */
|
|
45
|
+
mean_top3_relevant: number;
|
|
46
|
+
/** Fraction of queries with at least one relevant result in the top {@link TOP_N}. */
|
|
47
|
+
top3_hit_rate: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function aggregateRanking(perQuery: PerQueryMetrics[]): RankingMetrics;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { type PerQueryMetrics, type AggregateMetrics } from './metrics.js';
|
|
1
|
+
import { type PerQueryMetrics, type AggregateMetrics, type RankingMetrics } from './metrics.js';
|
|
2
2
|
import type { EvalCase } from './loader.js';
|
|
3
3
|
/** A retrieve function over the case's facts, returning indices in relevance order. */
|
|
4
4
|
type RetrieveFn = (query: string, k: number, facts: string[]) => Promise<number[]>;
|
|
5
5
|
interface EvalReport {
|
|
6
6
|
perQuery: PerQueryMetrics[];
|
|
7
7
|
summary: AggregateMetrics;
|
|
8
|
+
/**
|
|
9
|
+
* P-02 ranking-quality metrics. A SIBLING of `summary`, never a key inside it: `eval_zh.spec.ts`
|
|
10
|
+
* asserts `summary` with `toEqual` over exactly seven keys, so a new key there would turn a
|
|
11
|
+
* frozen-number regression into a key-set failure. `perQuery` is unchanged, so the bench
|
|
12
|
+
* harness's `(ids, scores)` fingerprint over it cannot move.
|
|
13
|
+
*/
|
|
14
|
+
ranking: RankingMetrics;
|
|
8
15
|
}
|
|
9
16
|
export declare function evaluateCases(cases: EvalCase[], retrieve: RetrieveFn): Promise<EvalReport>;
|
|
10
17
|
export {};
|
|
@@ -20,8 +20,9 @@ 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';
|
|
26
27
|
export * from './store/floors.js';
|
|
27
|
-
export { registerSemanticBackend,
|
|
28
|
+
export { registerSemanticBackend, registerVectorStore, type SemanticBackend, type VectorStore, } from '../mem-retrieval/index.js';
|
|
@@ -40,8 +40,8 @@ export interface WarmOptions {
|
|
|
40
40
|
deferTokenizerUntilIdle?: boolean;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* Fire-and-forget model bootstrap for DSH startup. Configures the semantic
|
|
44
|
-
*
|
|
43
|
+
* Fire-and-forget model bootstrap for DSH startup. Configures the semantic
|
|
44
|
+
* model env (mirror + cache) and triggers the model download/load
|
|
45
45
|
* asynchronously, without blocking the plugin's `apply`. Returns the availability
|
|
46
46
|
* of the semantic path once the download/load completes.
|
|
47
47
|
*
|
|
@@ -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';
|
|
@@ -20,7 +20,7 @@ export interface RuntimeOptions {
|
|
|
20
20
|
semantic?: SemanticBackend;
|
|
21
21
|
/**
|
|
22
22
|
* The family framework's managed roots, as the built-in defaults for `tools.dir` and
|
|
23
|
-
* `semantic.cache_dir`
|
|
23
|
+
* `semantic.cache_dir` (see `LoadConfigOptions.managedRoots`).
|
|
24
24
|
*
|
|
25
25
|
* The DSH plugin passes `<family home>/tools` and `<family home>/models` while
|
|
26
26
|
* `@avantf/dsh-plugin-base` owns the fetching; the CLI and the MCP server pass nothing, because they
|
|
@@ -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'>;
|
|
@@ -28,10 +28,25 @@ export declare function normalizeWrites(values: readonly string[]): string[];
|
|
|
28
28
|
/**
|
|
29
29
|
* Identity of the vector space a store currently writes into.
|
|
30
30
|
*
|
|
31
|
-
* A change of backend, model
|
|
32
|
-
*
|
|
31
|
+
* A change of backend, model, width — or of anything else that decides which coordinates a vector
|
|
32
|
+
* lives in (pooling, normalization, the input window, the model revision behind an unchanged repo
|
|
33
|
+
* name) — makes every stored vector stale. The point of recording the identity is that such a
|
|
34
|
+
* change is DETECTED instead of silently mixing two spaces in one index. The backend declares its
|
|
35
|
+
* representation (`SemanticBackend.representation`); one that does not is fingerprinted as
|
|
36
|
+
* `rep=undeclared`, which is honest rather than a claim nobody verified.
|
|
33
37
|
*/
|
|
34
38
|
export declare function vectorSpaceOf(semantic: SemanticBackend, model: string): string;
|
|
39
|
+
/** The two contract counts plus the internal "predates the representation fingerprint" split. */
|
|
40
|
+
export interface StaleVectorCounts {
|
|
41
|
+
stale: number;
|
|
42
|
+
space_stale: number;
|
|
43
|
+
/**
|
|
44
|
+
* Of {@link space_stale}, how many were written BEFORE the representation fingerprint (`v2/…`).
|
|
45
|
+
* The count is what makes the warning able to say "this is a one-time format upgrade" instead of
|
|
46
|
+
* blaming a model swap the operator never made.
|
|
47
|
+
*/
|
|
48
|
+
legacy?: number;
|
|
49
|
+
}
|
|
35
50
|
/**
|
|
36
51
|
* Drop ids from the vector index, in one batch when the backend supports it.
|
|
37
52
|
*
|
|
@@ -59,10 +74,7 @@ export declare function evictVectors(vstore: VectorStore, ids: readonly number[]
|
|
|
59
74
|
* @param counts how many persisted vectors are unusable in the current space, by kind
|
|
60
75
|
* @param fix the manual entry that re-encodes them, as the operator would type it
|
|
61
76
|
*/
|
|
62
|
-
export declare function reportStaleVectors(kind: string, space: string, counts:
|
|
63
|
-
stale: number;
|
|
64
|
-
space_stale: number;
|
|
65
|
-
}, fix: string): void;
|
|
77
|
+
export declare function reportStaleVectors(kind: string, space: string, counts: StaleVectorCounts, fix: string): void;
|
|
66
78
|
/**
|
|
67
79
|
* Hand the event loop back between two synchronous batches.
|
|
68
80
|
*
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entity leg's metric: ANCHORED JACCARD WITH A SATURATING UNION.
|
|
3
|
+
*
|
|
4
|
+
* WHY THE OLD METRIC FAILED SHORT QUERIES. The leg scored the plain Jaccard ratio
|
|
5
|
+
* `|Q ∩ F| / |Q ∪ F|`. Both stores are written by the SAME entity extractor, which keeps every
|
|
6
|
+
* noun-ish token, so a real memory fact carries ~31 entity names (measured on the live store:
|
|
7
|
+
* min 3 / median 31 / max 66; `scripts/ENTITY_TOKENIZER_VALUE.md`). A query extracts one or two.
|
|
8
|
+
* Even a PERFECT 1-entity match is then `1 / (1 + 31 - 1) ≈ 0.03`, so with `min_jaccard` at any
|
|
9
|
+
* useful value the leg is unreachable for a short query BY CONSTRUCTION — measured: 1-entity
|
|
10
|
+
* queries could clear 0.2 against 2 of 80 facts. Extracting MORE entities only grows the
|
|
11
|
+
* denominator (the tokenizer direction was measured and rejected), so the fix has to be the
|
|
12
|
+
* metric, not the extractor.
|
|
13
|
+
*
|
|
14
|
+
* THE METRIC. `A` is the query's ANCHOR entities (below). The score keeps the Jaccard form — so a
|
|
15
|
+
* fact that is "about" the query outranks one that merely contains it — but the fact's own width
|
|
16
|
+
* enters through a SATURATING term:
|
|
17
|
+
*
|
|
18
|
+
* score(A, W, F) = |A ∩ F| / ( W + min(|F \ A|, ENTITY_UNION_CAP) )
|
|
19
|
+
*
|
|
20
|
+
* The query side is `W`, the query's FULL entity count, not `|A|`: the Jaccard denominator is the
|
|
21
|
+
* union, and the query's own width belongs in it whether or not each entity turned out to be
|
|
22
|
+
* discriminative (a question that names five things asks for more than one that names one). When
|
|
23
|
+
* every query entity is an anchor (the frozen eval set) and the fact is no wider than
|
|
24
|
+
* `W + ENTITY_UNION_CAP`, this IS the plain Jaccard ratio `|Q ∩ F| / |Q ∪ F|` — those facts keep
|
|
25
|
+
* their EXACT old score and ordering (verified bit-for-bit on all four eval arms). Past the cap the
|
|
26
|
+
* denominator stops growing, so a wide fact that genuinely mentions a query's rare entity is
|
|
27
|
+
* REACHABLE instead of being divided into oblivion. The cap is not a free parameter: the floor is
|
|
28
|
+
* 0.2 and a two-entity query sharing ONE entity must still clear it (`1 / (2 + C) >= 0.2 ⇒ C <= 3`),
|
|
29
|
+
* so `C = 3` is the largest fact penalty that keeps that shape.
|
|
30
|
+
*
|
|
31
|
+
* WHY ANCHORS (IDF FILTERING). Removing the width blow-up alone would let a corpus-wide word
|
|
32
|
+
* (`用户` / `插件` / `任务` …) admit EVERY fact that mentions it — "drag the whole store in". The
|
|
33
|
+
* extracted bags are dominated by such words, so the precision lever is rarity: an entity is an
|
|
34
|
+
* ANCHOR when its document frequency over the active corpus is at most {@link anchorCeiling}. Only
|
|
35
|
+
* anchors can produce candidates or contribute to the numerator; a query whose entities are ALL
|
|
36
|
+
* generic therefore yields no entity evidence and the other legs answer it. That is deliberate
|
|
37
|
+
* (silent beats noisy), and the FTS leg's 2-char substring fallback (§20.20⑦) serves the 2-char
|
|
38
|
+
* shape.
|
|
39
|
+
*
|
|
40
|
+
* WHAT `W` COUNTS, AND WHY IT IS NOT `|A|`. `W` is the query's FULL entity count — every entity the
|
|
41
|
+
* extractor returned, generic or absent included. A Jaccard denominator is the union of what the
|
|
42
|
+
* query asked for, and the old metric counted all of it; keeping `W` there is what makes a narrow
|
|
43
|
+
* fact's score IDENTICAL to the old ratio, so the frozen eval and every narrow real fact keep their
|
|
44
|
+
* exact numbers and ordering. Dropping the absent entities from `W` was measured and moved the live
|
|
45
|
+
* eval (`CI 用哪个 Python 版本` lost rank 1): a query that names things the corpus does not carry
|
|
46
|
+
* really is asking for more, and the floor is calibrated against that.
|
|
47
|
+
*
|
|
48
|
+
* THE CEILING SCALES WITH THE CORPUS, WITH A FLOOR. A fixed count would mean different things on a
|
|
49
|
+
* 3-fact eval case and a 300k-fact store, and a purely relative one would call every entity
|
|
50
|
+
* "generic" on a tiny corpus (`ceil(0.2 * 3) = 1` filters any name that appears twice — which is
|
|
51
|
+
* every name in a 3-fact relation case). {@link ANCHOR_MIN} forgives small corpora; the relative
|
|
52
|
+
* term is what bites on a real store. Both are calibrated, not guessed: DESIGN §20.19.
|
|
53
|
+
*
|
|
54
|
+
* @module store/entity_leg
|
|
55
|
+
*/
|
|
56
|
+
/** An entity is an anchor at or below `max(ANCHOR_MIN, ceil(ANCHOR_RATE × activeFacts))`. */
|
|
57
|
+
export declare const ANCHOR_RATE = 0.2;
|
|
58
|
+
/**
|
|
59
|
+
* Small-corpus forgiveness, and its bound.
|
|
60
|
+
*
|
|
61
|
+
* Without it, `ceil(0.2 × 3) = 1` would call every name appearing twice "generic" — which is every
|
|
62
|
+
* name in a 3-fact relation case — and the eval set would lose its entity leg entirely. This is
|
|
63
|
+
* measured, not guessed: at `2` the eval corpus calls `Python` (carried by all three facts of its
|
|
64
|
+
* case) generic, and `CI 用哪个 Python 版本` loses its answer's entity evidence to the FTS leg —
|
|
65
|
+
* the live arm regressed `must_exclude`, the degraded arm lost rank 1 on that query. `4` keeps a
|
|
66
|
+
* name a small corpus repeats; the relative term is what bites on a real store (a 3-fact case
|
|
67
|
+
* filters nothing, by design).
|
|
68
|
+
*/
|
|
69
|
+
export declare const ANCHOR_MIN = 4;
|
|
70
|
+
/**
|
|
71
|
+
* How many entities a fact may carry beyond the query's anchors before the width penalty stops
|
|
72
|
+
* growing (see the module doc: derived from the 0.2 floor and the 2-anchor reachability
|
|
73
|
+
* requirement, `1 / (2 + C) >= 0.2`).
|
|
74
|
+
*/
|
|
75
|
+
export declare const ENTITY_UNION_CAP = 3;
|
|
76
|
+
/** The document-frequency ceiling for a corpus of `corpus` active facts/chunks. */
|
|
77
|
+
export declare function anchorCeiling(corpus: number): number;
|
|
78
|
+
/**
|
|
79
|
+
* The query entities that may serve as evidence.
|
|
80
|
+
*
|
|
81
|
+
* `df` is the DOCUMENT FREQUENCY of each query entity over the active corpus (how many facts /
|
|
82
|
+
* chunks carry it). An entity is kept when it occurs at least once and at most
|
|
83
|
+
* {@link anchorCeiling} times; the returned order is the input order (the caller only needs the
|
|
84
|
+
* set, and a stable order keeps the SQL/scoring deterministic).
|
|
85
|
+
*/
|
|
86
|
+
export declare function selectAnchors(queryEntities: Iterable<string>, df: ReadonlyMap<string, number>, corpus: number): string[];
|
|
87
|
+
/**
|
|
88
|
+
* The leg's raw score, in [0, 1] — see the module doc for the formula and the constants.
|
|
89
|
+
*
|
|
90
|
+
* `0` for an empty anchor set (no discriminative query entity) and for "shares none". `queryWidth`
|
|
91
|
+
* is the query's full entity count (`W`), which keeps the narrow-fact case identical to the old
|
|
92
|
+
* Jaccard ratio; the fact's width only matters up to {@link ENTITY_UNION_CAP} extra entities, so a
|
|
93
|
+
* wide bag cannot divide a short query into nothing while a narrow one still ranks above a wide one.
|
|
94
|
+
*/
|
|
95
|
+
export declare function anchoredOverlap(anchors: readonly string[], queryWidth: number, factEntities: ReadonlySet<string>): number;
|
|
@@ -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>;
|
|
@@ -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,
|
|
9
|
-
* FTS — so that is where the floors live. The side benefit is that
|
|
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,14 +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
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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.
|
|
60
71
|
*
|
|
61
72
|
* @module store/floors
|
|
62
73
|
*/
|
|
@@ -86,16 +97,20 @@ export type FloorLeg = 'semantic' | 'fts' | 'jaccard';
|
|
|
86
97
|
* - fts: NOT a constant — {@link looseTermFloor}, half the query's distinct terms with a floor of
|
|
87
98
|
* 1. `1` here is the "no terms to scale by" baseline (the previously constant value); a query
|
|
88
99
|
* that cannot produce terms is not graded at all, and one that produces a single term (every
|
|
89
|
-
* 3-char CJK query
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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.
|
|
99
114
|
*/
|
|
100
115
|
export declare const LOOSE_FLOORS: RetrievalFloors;
|
|
101
116
|
/**
|
|
@@ -116,10 +131,10 @@ export declare function emptyFloorDrops(): RetrievalFloorDrops;
|
|
|
116
131
|
* The FTS bar actually applied to a query carrying `termCount` distinct terms.
|
|
117
132
|
*
|
|
118
133
|
* A query cannot be graded against more terms than it has (see the module comment: a 3-char CJK
|
|
119
|
-
* query has one trigram), so the configured bar is CLAMPED
|
|
120
|
-
* `0`/omitted leaves the value alone — such a query is not
|
|
121
|
-
* and changing the REPORTED value there would misdescribe
|
|
122
|
-
* (the operator's off switch).
|
|
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).
|
|
123
138
|
*
|
|
124
139
|
* This is the ONE place the clamp lives; `resolveFloors` (what the result reports / a run is handed)
|
|
125
140
|
* and `applyTermFloor` (what the leg does) both call it, so the number a run reads is the number its
|
|
@@ -133,7 +148,7 @@ export interface FloorResolution {
|
|
|
133
148
|
/** `loose` lowers each configured floor (see {@link LOOSE_FLOORS}); absent means `strict`. */
|
|
134
149
|
profile?: FloorProfile;
|
|
135
150
|
/**
|
|
136
|
-
* The distinct term count of the text being GRADED (`
|
|
151
|
+
* The distinct term count of the text being GRADED (`gradedTerms(text).length`), for the FTS
|
|
137
152
|
* clamp. It must describe the query the legs will actually run on: under 方案 A's augmentation the
|
|
138
153
|
* rewrite is a DIFFERENT text with its own term count, and resolving both runs from the original's
|
|
139
154
|
* count is the defect that let one incidental trigram through (see `store/hybrid.ts`). Callers with
|
|
@@ -182,16 +197,21 @@ export declare function applyScoreFloor(scores: Map<number, number>, floor: numb
|
|
|
182
197
|
*
|
|
183
198
|
* Substring containment, case-insensitively: FTS5's trigram tokenizer matches a quoted phrase by
|
|
184
199
|
* consecutive trigrams and is ASCII-case-insensitive, which is the same predicate as "the term
|
|
185
|
-
* occurs in the text"
|
|
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.
|
|
186
203
|
*/
|
|
187
204
|
export declare function countMatchedTerms(text: string, terms: readonly string[]): number;
|
|
188
205
|
/**
|
|
189
206
|
* Apply the per-row FTS floor to one FTS leg's scores.
|
|
190
207
|
*
|
|
191
|
-
* The terms come from `
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
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.
|
|
195
215
|
*
|
|
196
216
|
* The bar itself is {@link effectiveTermFloor}: a query that can only produce one term (a 3-char CJK
|
|
197
217
|
* query is one trigram) is measured against that one term, not against a configured 2 it could never
|