@avantf/dsh-mem 0.6.0 → 0.6.2

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 (34) hide show
  1. package/lib/client.js +1 -0
  2. package/lib/engine/mem/db/dao/chunks.d.ts +60 -4
  3. package/lib/engine/mem/db/dao/shared.d.ts +153 -0
  4. package/lib/engine/mem/dispatch.d.ts +19 -1
  5. package/lib/engine/mem/eval/index.d.ts +4 -0
  6. package/lib/engine/mem/index.d.ts +2 -0
  7. package/lib/engine/mem/runtime.d.ts +2 -2
  8. package/lib/engine/mem/store/entity_sweep.d.ts +121 -0
  9. package/lib/engine/mem/store/hybrid.d.ts +50 -0
  10. package/lib/engine/mem/store/knowledge.d.ts +116 -12
  11. package/lib/engine/mem/store/legs.d.ts +171 -0
  12. package/lib/engine/mem/store/lexical.d.ts +8 -0
  13. package/lib/engine/mem/store/memory.d.ts +84 -72
  14. package/lib/engine/mem/store/vector_repair.d.ts +248 -0
  15. package/lib/engine/mem/trustHeartbeat.d.ts +47 -0
  16. package/lib/engine/mem-contract/tools.d.ts +4 -0
  17. package/lib/engine/mem-contract/types.d.ts +37 -6
  18. package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +52 -0
  19. package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +10 -0
  20. package/lib/engine/mem-retrieval/rerank.d.ts +13 -0
  21. package/lib/index.js +1653 -703
  22. package/lib/interface-version.json +1 -1
  23. package/lib/types/envinit-bootstrap.d.ts +1 -1
  24. package/lib/types/envinit.d.ts +0 -3
  25. package/lib/types/envinit.d.ts.map +1 -1
  26. package/lib/types/index.d.ts.map +1 -1
  27. package/lib/types/interface_gate.d.ts +70 -0
  28. package/lib/types/interface_gate.d.ts.map +1 -1
  29. package/lib/types/provision.d.ts +6 -0
  30. package/lib/types/provision.d.ts.map +1 -1
  31. package/lib/types/remote.d.ts.map +1 -1
  32. package/lib/types/vectorMigration.d.ts +22 -18
  33. package/lib/types/vectorMigration.d.ts.map +1 -1
  34. 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 (`replace` clears the
110
- * old rows first — used by reindex, where the extractor may have changed).
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?: boolean): void;
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, most-shared first and CAPPED (see the memory store's sibling).
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[]>;
@@ -13,7 +13,7 @@
13
13
  * `ToolSpec.key` is typed `string` (not a literal union), so exhaustiveness cannot be checked at
14
14
  * compile time — the spec assertion is the gate.
15
15
  */
16
- import type { KbAddRequest } from '../mem-contract/index.js';
16
+ import type { KbAddRequest, ToolEnvelope, ToolSpec } from '../mem-contract/index.js';
17
17
  import type { AvantfRuntime } from './runtime.js';
18
18
  /**
19
19
  * The default page for a model-facing `kb_list`.
@@ -49,3 +49,21 @@ export declare function supportsToolKey(key: string): boolean;
49
49
  * both adapters route the throw through their structured error envelope.
50
50
  */
51
51
  export declare function dispatchToolKey(rt: AvantfRuntime, key: string, args: Record<string, unknown>): Promise<unknown>;
52
+ /**
53
+ * The ONE model-facing boundary: validate against the contract spec, dispatch the key, and shape the
54
+ * result into the agent envelope.
55
+ *
56
+ * Every model-facing surface shares this — the DSH tool runner (`@avantf/dsh-mem`) and the MCP
57
+ * server — because the envelope is `ToolEnvelope`, a wire shape: an MCP caller and a DSH caller
58
+ * asking the same question must get the same fields. It used to live only in the plugin, so the MCP
59
+ * surface re-implemented it and the two could drift field by field.
60
+ *
61
+ * What stays in the CALLERS is everything AROUND the envelope: MCP's `textResult`/`isError` wrapper
62
+ * and the plugin's `present` view plus its Remote gateway. The Remote gateway is deliberately NOT
63
+ * routed through here (`mem/DESIGN.md` §239: it keeps `mem_admin`'s retention diagnostics and uses
64
+ * the wire error type, not `toolErr`).
65
+ *
66
+ * A validation failure answers `validationError` (named for the spec), an unknown key answers
67
+ * `toolErr`, and a throwing runtime call answers `toolErr` too — a raw exception never escapes.
68
+ */
69
+ export declare function runToolSpec(rt: AvantfRuntime, spec: ToolSpec, args: unknown): Promise<ToolEnvelope<unknown>>;
@@ -0,0 +1,4 @@
1
+ export * from './metrics.js';
2
+ export * from './loader.js';
3
+ export * from './runner.js';
4
+ export * from './write_metrics.js';
@@ -17,9 +17,11 @@ export * from './config/paths.js';
17
17
  export * from './config/loader.js';
18
18
  export * from './modelBootstrap.js';
19
19
  export * from './dispatch.js';
20
+ export * from './trustHeartbeat.js';
20
21
  export * from './runtime.js';
21
22
  export * from './store/memory.js';
22
23
  export * from './store/knowledge.js';
24
+ export * from './store/vector_repair.js';
23
25
  export * from './store/hybrid.js';
24
26
  export * from './store/source_picker.js';
25
27
  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
- }>): StoreResult<MemoryStore, 'vectorsFix'>;
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>;