@avantf/dsh-mem 0.3.1 → 0.4.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.
- package/README.md +89 -28
- package/cordis.patch.yml +17 -0
- package/lib/client.js +155 -14
- package/lib/engine/mem/config/config_files.d.ts +26 -0
- package/lib/engine/mem/config/domains.d.ts +20 -0
- package/lib/engine/mem/config/loader.d.ts +35 -0
- package/lib/engine/mem/config/paths.d.ts +53 -0
- package/lib/engine/mem/db/chunk.d.ts +26 -0
- package/lib/engine/mem/db/conn.d.ts +13 -0
- package/lib/engine/mem/db/dao/chunks.d.ts +150 -0
- package/lib/engine/mem/db/dao/contradictions.d.ts +98 -0
- package/lib/engine/mem/db/dao/documents.d.ts +51 -0
- package/lib/engine/mem/db/dao/entities.d.ts +67 -0
- package/lib/engine/mem/db/dao/facts.d.ts +472 -0
- package/lib/engine/mem/db/dao/stats.d.ts +14 -0
- package/lib/engine/mem/db/dao/triples.d.ts +80 -0
- package/lib/engine/mem/db/hash.d.ts +2 -0
- package/lib/engine/mem/db/knowledge.d.ts +17 -0
- package/lib/engine/mem/db/migrations.d.ts +71 -0
- package/lib/engine/mem/db/port.d.ts +57 -0
- package/lib/engine/mem/db/schema.d.ts +26 -0
- package/lib/engine/mem/db/sqlite.d.ts +89 -0
- package/lib/engine/mem/db/store.d.ts +35 -0
- package/lib/engine/mem/db/tokenizer.d.ts +61 -0
- package/lib/engine/mem/db/vectors.d.ts +50 -0
- package/lib/engine/mem/dispatch.d.ts +51 -0
- package/lib/engine/mem/entities/extract.d.ts +84 -0
- package/lib/engine/mem/eval/loader.d.ts +19 -0
- package/lib/engine/mem/eval/metrics.d.ts +25 -0
- package/lib/engine/mem/eval/runner.d.ts +10 -0
- package/lib/engine/mem/eval/write_metrics.d.ts +88 -0
- package/lib/engine/mem/git.d.ts +73 -0
- package/lib/engine/mem/hrr/algebra.d.ts +30 -0
- package/lib/engine/mem/hrr/atoms.d.ts +22 -0
- package/lib/engine/mem/hrr/encode.d.ts +32 -0
- package/lib/engine/mem/hrr/index.d.ts +3 -0
- package/lib/engine/mem/index.d.ts +27 -0
- package/lib/engine/mem/lifecycle/contradiction.d.ts +156 -0
- package/lib/engine/mem/lifecycle/index.d.ts +5 -0
- package/lib/engine/mem/lifecycle/maintenance.d.ts +48 -0
- package/lib/engine/mem/lifecycle/presence.d.ts +29 -0
- package/lib/engine/mem/lifecycle/tick.d.ts +43 -0
- package/lib/engine/mem/lifecycle/trust.d.ts +90 -0
- package/lib/engine/mem/modelBootstrap.d.ts +207 -0
- package/lib/engine/mem/router.d.ts +51 -0
- package/lib/engine/mem/runtime.d.ts +232 -0
- package/lib/engine/mem/store/common.d.ts +82 -0
- package/lib/engine/mem/store/doc_files.d.ts +125 -0
- package/lib/engine/mem/store/document_text.d.ts +47 -0
- package/lib/engine/mem/store/floors.d.ts +191 -0
- package/lib/engine/mem/store/hybrid.d.ts +232 -0
- package/lib/engine/mem/store/ingest_guard.d.ts +25 -0
- package/lib/engine/mem/store/knowledge.d.ts +483 -0
- package/lib/engine/mem/store/lexical.d.ts +67 -0
- package/lib/engine/mem/store/memory.d.ts +543 -0
- package/lib/engine/mem/store/source_picker.d.ts +43 -0
- package/lib/engine/mem-contract/config.d.ts +129 -0
- package/lib/engine/mem-contract/env.d.ts +31 -0
- package/lib/engine/mem-contract/family.d.ts +25 -0
- package/lib/engine/mem-contract/index.d.ts +8 -0
- package/lib/engine/mem-contract/log.d.ts +29 -0
- package/lib/engine/mem-contract/remote.d.ts +80 -0
- package/lib/engine/mem-contract/text.d.ts +92 -0
- package/lib/engine/mem-contract/tools.d.ts +284 -0
- package/lib/engine/mem-contract/types.d.ts +615 -0
- package/lib/engine/mem-retrieval/adapters/auto_vstore.d.ts +47 -0
- package/lib/engine/mem-retrieval/adapters/hnswlib.d.ts +174 -0
- package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +121 -0
- package/lib/engine/mem-retrieval/adapters/local_numpy.d.ts +28 -0
- package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +52 -0
- package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +10 -0
- package/lib/engine/mem-retrieval/adapters/warm_gate.d.ts +46 -0
- package/lib/engine/mem-retrieval/budget.d.ts +30 -0
- package/lib/engine/mem-retrieval/fusion.d.ts +53 -0
- package/lib/engine/mem-retrieval/index.d.ts +14 -0
- package/lib/engine/mem-retrieval/interfaces.d.ts +82 -0
- package/lib/engine/mem-retrieval/log.d.ts +11 -0
- package/lib/engine/mem-retrieval/registry.d.ts +23 -0
- package/lib/engine/mem-retrieval/rerank.d.ts +13 -0
- package/lib/engine/mem-retrieval/stats.d.ts +64 -0
- package/lib/engine/mem-retrieval/text_budget.d.ts +52 -0
- package/lib/index.js +1773 -473
- package/lib/interface-version.json +2 -2
- package/lib/types/client/domains.d.ts +5 -4
- package/lib/types/client/domains.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/ingest.d.ts +1 -1
- package/lib/types/client/wire.d.ts +36 -0
- package/lib/types/client/wire.d.ts.map +1 -0
- package/lib/types/envinit-bootstrap.d.ts +3 -2
- package/lib/types/envinit.d.ts +2 -19
- package/lib/types/envinit.d.ts.map +1 -1
- package/lib/types/hints.d.ts +1 -1
- package/lib/types/index.d.ts +8 -3
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/open.d.ts +61 -11
- package/lib/types/open.d.ts.map +1 -1
- package/lib/types/prompt.d.ts +19 -1
- package/lib/types/prompt.d.ts.map +1 -1
- package/lib/types/provision.d.ts +1 -1
- package/lib/types/provision.d.ts.map +1 -1
- package/lib/types/reconcile.d.ts +64 -0
- package/lib/types/reconcile.d.ts.map +1 -0
- package/lib/types/remote.d.ts +53 -0
- package/lib/types/remote.d.ts.map +1 -1
- package/lib/types/render.d.ts +35 -0
- package/lib/types/render.d.ts.map +1 -0
- package/lib/types/tool_schema.d.ts +3 -13
- package/lib/types/tool_schema.d.ts.map +1 -1
- package/package.json +24 -15
- package/lib/client.js.map +0 -1
- package/lib/types/client/domains.js +0 -25
- package/lib/types/client/domains.js.map +0 -1
- package/lib/types/client/index.js +0 -1371
- package/lib/types/client/index.js.map +0 -1
- package/lib/types/client/ingest.js +0 -30
- package/lib/types/client/ingest.js.map +0 -1
- package/lib/types/client/paging.js +0 -36
- package/lib/types/client/paging.js.map +0 -1
- package/lib/types/client/summarize.js +0 -79
- package/lib/types/client/summarize.js.map +0 -1
- package/lib/types/data_home.js +0 -26
- package/lib/types/data_home.js.map +0 -1
- package/lib/types/envinit-bootstrap.js +0 -333
- package/lib/types/envinit.js +0 -402
- package/lib/types/envinit.js.map +0 -1
- package/lib/types/hints.js +0 -53
- package/lib/types/hints.js.map +0 -1
- package/lib/types/index.js +0 -755
- package/lib/types/index.js.map +0 -1
- package/lib/types/inject.js +0 -14
- package/lib/types/inject.js.map +0 -1
- package/lib/types/interface_gate.js +0 -84
- package/lib/types/interface_gate.js.map +0 -1
- package/lib/types/open.js +0 -133
- package/lib/types/open.js.map +0 -1
- package/lib/types/prompt.js +0 -148
- package/lib/types/prompt.js.map +0 -1
- package/lib/types/provision.js +0 -177
- package/lib/types/provision.js.map +0 -1
- package/lib/types/remote.js +0 -247
- package/lib/types/remote.js.map +0 -1
- package/lib/types/tool_schema.js +0 -149
- package/lib/types/tool_schema.js.map +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Config } from '../../mem-contract/index.js';
|
|
2
|
+
import { expandHome } from '../../mem-contract/index.js';
|
|
3
|
+
/** Expand a leading `~/` to the user home directory — single source in the contract. */
|
|
4
|
+
export { expandHome };
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the data home (default `~/.avantf`).
|
|
7
|
+
*
|
|
8
|
+
* Precedence, per DESIGN §3 and the family layering (root `AGENTS.md` 「边界与路径」: built-in →
|
|
9
|
+
* profile → store → ENV → explicit): an EXPLICIT caller value (⑤) → `AVANTF_HOME` (④) → the
|
|
10
|
+
* configured `common.dataHome` (②) → `~/.avantf`. The environment is a deployment override and
|
|
11
|
+
* outranks the config file.
|
|
12
|
+
*
|
|
13
|
+
* `configured` cannot stand in for layer ⑤: the schema gives it a default, so it always holds a
|
|
14
|
+
* value and would make an explicit argument indistinguishable from "nothing was configured" — which
|
|
15
|
+
* is exactly how the env var used to win over a caller's own `dataHome`.
|
|
16
|
+
*
|
|
17
|
+
* The slots are NAMED for the same reason the base kit's copy is: the two implementations used to
|
|
18
|
+
* disagree about their POSITIONAL order (`(explicit, env, common)` here, `(common, explicit)`…
|
|
19
|
+
* whichever way a reader guessed), so "which slot does the profile's `dataHome` belong in?" was
|
|
20
|
+
* answerable only by reading the call site. The base kit implements the same rule with the same named
|
|
21
|
+
* shape, and `packages/plugin/test/family_pin.spec.ts` pins the two implementations together.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveDataHome(input: {
|
|
24
|
+
/** Layer ②: the configured `common.dataHome`. */
|
|
25
|
+
readonly common: Pick<Config, 'dataHome'>;
|
|
26
|
+
/** Layer ⑤: an explicit caller value (the CLI's `--data-home`). */
|
|
27
|
+
readonly explicit?: string;
|
|
28
|
+
}): string;
|
|
29
|
+
/**
|
|
30
|
+
* Every config file lives in ONE directory, beside the prompt files.
|
|
31
|
+
*
|
|
32
|
+
* The layering is unchanged (① built-in → ② common → ③ store → ④ env → ⑤ explicit); only the
|
|
33
|
+
* locations moved, so all of a deployment's editable text — `configs/*.yaml` and `prompts/*.md` —
|
|
34
|
+
* is in one place instead of scattered through `memory/` and `knowledge/` next to the databases.
|
|
35
|
+
*/
|
|
36
|
+
export declare function configsDir(home: string): string;
|
|
37
|
+
export declare function commonConfigPath(home: string): string;
|
|
38
|
+
export declare function memoryDir(home: string): string;
|
|
39
|
+
export declare function knowledgeDir(home: string): string;
|
|
40
|
+
export declare function memoryConfigPath(home: string): string;
|
|
41
|
+
export declare function knowledgeConfigPath(home: string): string;
|
|
42
|
+
export declare function memoryDbPath(home: string, override?: string): string;
|
|
43
|
+
export declare function knowledgeDbPath(home: string, override?: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The managed document directory (default `<home>/knowledge/docs`).
|
|
46
|
+
*
|
|
47
|
+
* One editable `.md` per document, so the corpus can be maintained with ordinary tools. It sits
|
|
48
|
+
* BESIDE the database rather than replacing it: the file holds the body, the database holds the
|
|
49
|
+
* identity, chunks and indexes derived from it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function knowledgeDocsDir(home: string, override?: string): string;
|
|
52
|
+
/** Ensure the memory/knowledge layout exists under `home`. */
|
|
53
|
+
export declare function ensureDataLayout(home: string): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch an id list for a SQL `IN (...)` clause.
|
|
3
|
+
*
|
|
4
|
+
* SQLite caps the number of bound parameters in one statement (SQLITE_MAX_VARIABLE_NUMBER,
|
|
5
|
+
* 32 766 in the bundled build). A query whose id list can grow with the CORPUS rather than
|
|
6
|
+
* with the request — `entityBags` on the write-path contradiction check, `filterActive` on
|
|
7
|
+
* the catch-up sweep — will therefore hit "too many SQL variables" on a large store. The
|
|
8
|
+
* write path swallows that as best-effort, so the symptom is a detection feature that
|
|
9
|
+
* silently stops running instead of an error anyone notices.
|
|
10
|
+
*/
|
|
11
|
+
/** Split `items` into consecutive batches of at most `size` (an empty list yields none). */
|
|
12
|
+
export declare function batches<T>(items: readonly T[], size?: number): T[][];
|
|
13
|
+
/** A fixed-width `IN (…)` fragment plus the exactly-matching bound values. */
|
|
14
|
+
export interface InList<T> {
|
|
15
|
+
/** The `?,?,…` body to splice into `IN (${placeholders})` (or the literal `NULL` for an empty list). */
|
|
16
|
+
placeholders: string;
|
|
17
|
+
/** The values to bind — the input, padded to the rung with repeats of its last element. */
|
|
18
|
+
values: T[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Prepare a value list for an `IN (…)` clause at a ladder-rounded, cache-stable width.
|
|
22
|
+
*
|
|
23
|
+
* `count <= 0` yields `NULL` (matches nothing) rather than `IN ()`, a syntax error — callers guard
|
|
24
|
+
* empty lists anyway; this is belt-and-braces.
|
|
25
|
+
*/
|
|
26
|
+
export declare function inList<T>(values: readonly T[]): InList<T>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type OpenedStore, type StoreSchema } from './store.js';
|
|
2
|
+
import { type FtsTokenizer } from './tokenizer.js';
|
|
3
|
+
export type { Db, DbStatement, DbRunResult } from './port.js';
|
|
4
|
+
export type { FtsTokenizer };
|
|
5
|
+
/**
|
|
6
|
+
* Ordered schema steps. Step 1 is the base schema and MUST stay idempotent: a database
|
|
7
|
+
* created before this mechanism existed reports `user_version = 0` while already holding it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MEMORY_SCHEMA: StoreSchema;
|
|
10
|
+
/** Open (or create) the memory DB, bringing its schema up to the current version. */
|
|
11
|
+
export declare function openMemoryDb(path: string): OpenedStore['db'];
|
|
12
|
+
/** Open and report which migrations ran (tests, and any explicit upgrade path). */
|
|
13
|
+
export declare function openMemoryStore(path: string): OpenedStore;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doc_chunks` + `chunk_entities` + the `doc_chunks_fts` index — the knowledge read model.
|
|
3
|
+
*
|
|
4
|
+
* Every id list here is batched (`batches()`): candidate sets are derived from the CORPUS
|
|
5
|
+
* (entity overlap, semantic top-k), not from the request, so a plain `IN (…)` would trip
|
|
6
|
+
* SQLite's bind-parameter cap on a large store. That failure is silent on the search path
|
|
7
|
+
* (it just drops a leg), which is exactly what batching prevents.
|
|
8
|
+
*/
|
|
9
|
+
import type { DocumentChunk } from '../../../mem-contract/index.js';
|
|
10
|
+
import type { Db } from '../port.js';
|
|
11
|
+
/** One chunk to persist, in document order (`idx` is the position, not the array index). */
|
|
12
|
+
interface ChunkInsert {
|
|
13
|
+
idx: number;
|
|
14
|
+
text: string;
|
|
15
|
+
headingsPath: string;
|
|
16
|
+
sourceRef: string;
|
|
17
|
+
charStart: number;
|
|
18
|
+
charEnd: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A chunk plus the state of everything DERIVED from it (DESIGN §20).
|
|
22
|
+
*
|
|
23
|
+
* `content_hash` says which text the derivations were built from, `embedding_model` which
|
|
24
|
+
* vector space they live in, `entities_version` which extraction rules produced the entity
|
|
25
|
+
* rows. `reindex` decides per row what actually needs redoing from these; without them it has
|
|
26
|
+
* to assume every row is stale and pay for the whole corpus.
|
|
27
|
+
*/
|
|
28
|
+
export interface ChunkStateRow {
|
|
29
|
+
chunk_id: number;
|
|
30
|
+
text: string;
|
|
31
|
+
content_hash: string | null;
|
|
32
|
+
embedding_model: string | null;
|
|
33
|
+
entities_version: number | null;
|
|
34
|
+
/** `1` when a non-NULL vector blob is stored (SQLite has no boolean). */
|
|
35
|
+
has_vector: number;
|
|
36
|
+
}
|
|
37
|
+
/** A persisted chunk with the entity names extracted at ingest time. */
|
|
38
|
+
interface ChunkEntityRow {
|
|
39
|
+
chunk_id: number;
|
|
40
|
+
names: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
/** A search hit row: chunk body plus the owning document's identity. */
|
|
43
|
+
export interface ChunkHitRow {
|
|
44
|
+
chunk_id: number;
|
|
45
|
+
text: string;
|
|
46
|
+
source_ref: string;
|
|
47
|
+
domain: string;
|
|
48
|
+
source: string;
|
|
49
|
+
created_at: string | null;
|
|
50
|
+
updated_at: string | null;
|
|
51
|
+
}
|
|
52
|
+
export declare class ChunksDao {
|
|
53
|
+
private readonly db;
|
|
54
|
+
constructor(db: Db);
|
|
55
|
+
idsForDoc(docId: number): number[];
|
|
56
|
+
/** Re-ingest replaces the chunk set wholesale (entities/FTS follow via cascade/triggers). */
|
|
57
|
+
deleteForDoc(docId: number): void;
|
|
58
|
+
/** Insert the doc's chunks in one statement per chunk; returns the new ids in order. */
|
|
59
|
+
insertMany(docId: number, chunks: readonly ChunkInsert[]): number[];
|
|
60
|
+
/** Chunk bodies by id (entity extraction + vector encoding after ingest). */
|
|
61
|
+
texts(ids: readonly number[]): {
|
|
62
|
+
chunk_id: number;
|
|
63
|
+
text: string;
|
|
64
|
+
}[];
|
|
65
|
+
/** Chunk rows for the catalog detail view, in document order (`DocumentDetail.chunks`). */
|
|
66
|
+
rowsForDoc(docId: number): DocumentChunk[];
|
|
67
|
+
/** Persist a chunk vector together with the vector space it belongs to. */
|
|
68
|
+
setVector(chunkId: number, bytes: Buffer, embeddingModel: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* The doc's chunks WITH their stored vectors — the reuse index for a re-ingest.
|
|
71
|
+
*
|
|
72
|
+
* A re-ingest deletes and re-inserts every chunk (new ids), so without this the only way to keep a
|
|
73
|
+
* vector is to re-encode the same text; `vectorReusable` already knows how to tell "unchanged", but
|
|
74
|
+
* only `reindex` could use it. Read BEFORE the replace, inside its transaction.
|
|
75
|
+
*/
|
|
76
|
+
vectorStateForDoc(docId: number): (ChunkStateRow & {
|
|
77
|
+
vec: Buffer | null;
|
|
78
|
+
})[];
|
|
79
|
+
/**
|
|
80
|
+
* Write a batch of vectors in ONE transaction.
|
|
81
|
+
*
|
|
82
|
+
* `setVector` is autocommit: each chunk costs a commit (measured 14.2 µs/row against 2.2 µs in a
|
|
83
|
+
* transaction, and on a WAL database every commit appends its dirty pages). Callers encode in
|
|
84
|
+
* batches anyway, so this is the same loop with the boundary moved out — and the transaction
|
|
85
|
+
* NEVER spans an `await` (the caller encodes first, then calls this).
|
|
86
|
+
*/
|
|
87
|
+
setVectors(rows: readonly {
|
|
88
|
+
chunk_id: number;
|
|
89
|
+
bytes: Buffer;
|
|
90
|
+
embeddingModel: string;
|
|
91
|
+
}[]): void;
|
|
92
|
+
/** Record which extraction rules produced a chunk's entity rows (see `ENTITY_EXTRACTOR_VERSION`). */
|
|
93
|
+
setEntitiesVersion(ids: readonly number[], version: number): void;
|
|
94
|
+
/** Every persisted chunk vector, for rebuilding the in-memory index at open. */
|
|
95
|
+
/**
|
|
96
|
+
* Every persisted chunk vector, with the space it was written in.
|
|
97
|
+
*
|
|
98
|
+
* `embedding_model` is selected too (not just the bytes) so a reload can DETECT a model swap: a
|
|
99
|
+
* dim-valid vector from another space is still ranked, and without this the two spaces would mix
|
|
100
|
+
* in one index silently — the memory side had the check, the knowledge side did not.
|
|
101
|
+
*/
|
|
102
|
+
vectorRows(): {
|
|
103
|
+
id: number;
|
|
104
|
+
vec: Buffer;
|
|
105
|
+
embedding_model: string | null;
|
|
106
|
+
}[];
|
|
107
|
+
count(): number;
|
|
108
|
+
/**
|
|
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).
|
|
111
|
+
*/
|
|
112
|
+
replaceEntities(rows: readonly ChunkEntityRow[], replace?: boolean): void;
|
|
113
|
+
/** Batched entity bags: `Map<chunkId, names>` (ids with none are absent). */
|
|
114
|
+
entityBags(ids: readonly number[]): Map<number, string[]>;
|
|
115
|
+
/** Chunk body + owning-document identity for the fused candidate ids. */
|
|
116
|
+
hits(ids: readonly number[]): ChunkHitRow[];
|
|
117
|
+
/** `(chunk_id, domain, source)` for the semantic top-k, to apply the domain/source filter. */
|
|
118
|
+
meta(ids: readonly number[]): {
|
|
119
|
+
chunk_id: number;
|
|
120
|
+
domain: string;
|
|
121
|
+
source: string;
|
|
122
|
+
}[];
|
|
123
|
+
/**
|
|
124
|
+
* Corpus rows for the rebuild legs, optionally scoped to one domain, together with the state
|
|
125
|
+
* of their derivations. This is the `reindex` source: the caller compares `content_hash` /
|
|
126
|
+
* `embedding_model` / `entities_version` against what it is about to produce and only pays
|
|
127
|
+
* for the rows that are actually out of date (DESIGN §20).
|
|
128
|
+
*/
|
|
129
|
+
corpusState(domain?: string): ChunkStateRow[];
|
|
130
|
+
/** Rebuild the FTS index from the content table (write-path maintenance). */
|
|
131
|
+
rebuildFts(): void;
|
|
132
|
+
/** Active chunks sharing at least one entity name with the query (jaccard candidate set). */
|
|
133
|
+
/**
|
|
134
|
+
* Entity-overlap candidates, most-shared first and CAPPED (see the memory store's sibling).
|
|
135
|
+
*
|
|
136
|
+
* `limit` is applied per name-batch and the batches are unioned, so the result is bounded by
|
|
137
|
+
* `limit x ceil(names/batch)` — corpus-independent, which is what this query can promise. The
|
|
138
|
+
* caller is expected to enforce the real `cap` on the union AFTER scoring it (it re-computes the
|
|
139
|
+
* true Jaccard in JS): a union of per-batch tops is not a top-N, and a leg that returns more than
|
|
140
|
+
* the cap makes `scores.size === legCap` — the "was this leg trimmed" signal — report the opposite
|
|
141
|
+
* of the truth. (The memory store's sibling does not batch, so its single `LIMIT` is already exact.)
|
|
142
|
+
*/
|
|
143
|
+
candidatesByEntityNames(names: readonly string[], domain: string | undefined, source: string | undefined, limit: number): number[];
|
|
144
|
+
/** FTS leg (`bm25` rank; negated by the caller so higher = better). */
|
|
145
|
+
ftsSearch(ftsQuery: string, domain?: string, source?: string, limit?: number): {
|
|
146
|
+
id: number;
|
|
147
|
+
rank: number;
|
|
148
|
+
}[];
|
|
149
|
+
}
|
|
150
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contradiction_log` — the open/resolved conflict pairs.
|
|
3
|
+
*
|
|
4
|
+
* The detector decides WHAT conflicts; this DAO owns how a pair is recorded, deduped (the
|
|
5
|
+
* partial unique index makes `log` idempotent across processes), retired, and listed. Both
|
|
6
|
+
* sides of every read are filtered to ACTIVE facts: the corpus is "changed × active", so a
|
|
7
|
+
* pair whose other side left it is no longer actionable (see DESIGN §11).
|
|
8
|
+
*/
|
|
9
|
+
import { type ContradictionRecord } from '../../../mem-contract/index.js';
|
|
10
|
+
import type { Db } from '../port.js';
|
|
11
|
+
export interface ConflictPair {
|
|
12
|
+
/** Row id of the pair — the handle `contradict_resolve` takes. */
|
|
13
|
+
contradiction_id: number;
|
|
14
|
+
fact_a: number;
|
|
15
|
+
fact_b: number;
|
|
16
|
+
score: number;
|
|
17
|
+
}
|
|
18
|
+
/** A row of `contradiction_log` as stored: the pair plus its adjudication state. */
|
|
19
|
+
export interface StoredConflict {
|
|
20
|
+
id: number;
|
|
21
|
+
fact_a: number;
|
|
22
|
+
fact_b: number;
|
|
23
|
+
score: number;
|
|
24
|
+
/** 0 = open, 1 = closed (see `resolution` / `loser_fact_id` for how). */
|
|
25
|
+
resolved: number;
|
|
26
|
+
resolution: string | null;
|
|
27
|
+
loser_fact_id: number | null;
|
|
28
|
+
/** `auto` (a fact left the corpus) or `verdict` (explicit adjudication). */
|
|
29
|
+
resolved_by: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare class ContradictionsDao {
|
|
32
|
+
private readonly db;
|
|
33
|
+
constructor(db: Db);
|
|
34
|
+
/**
|
|
35
|
+
* Pairs the detector must NOT log again, normalized `min|max` — open pairs plus pairs closed
|
|
36
|
+
* by an explicit VERDICT.
|
|
37
|
+
*
|
|
38
|
+
* The distinction matters in both directions: a pair retired because a fact left the active
|
|
39
|
+
* corpus must be loggable again after `restore` (that is the documented archive → restore →
|
|
40
|
+
* re-detect cycle), while a pair someone adjudicated as `false_positive` — or as a
|
|
41
|
+
* `true_positive` where both statements stay — must not be resurrected by a later sweep just
|
|
42
|
+
* because a pending id was still queued.
|
|
43
|
+
*/
|
|
44
|
+
suppressedPairs(): Set<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Suppressed pairs that NAME `factId` — the write path's version of {@link suppressedPairs}.
|
|
47
|
+
*
|
|
48
|
+
* A single fact's check can only ever consult pairs it is part of (`maybeLog(fid, other, …)`),
|
|
49
|
+
* so reading the whole log was pure waste: measured 60.8 ms per write at 100k open pairs, while
|
|
50
|
+
* this answers in 0.06 ms. Both branches are index seeks on the plain `fact_a`/`fact_b` indexes
|
|
51
|
+
* (the partial UNIQUE index cannot serve the `resolved_by` disjunct).
|
|
52
|
+
*/
|
|
53
|
+
suppressedPairsFor(factId: number): Set<string>;
|
|
54
|
+
log(factA: number, factB: number, score: number): void;
|
|
55
|
+
resolve(id: number, resolution: 'true_positive' | 'false_positive', loserFactId?: number, resolvedBy?: 'auto' | 'verdict'): void;
|
|
56
|
+
/**
|
|
57
|
+
* One stored row by id — what an adjudication needs before it decides anything: which two
|
|
58
|
+
* facts the pair names (so a "loser" can be validated against it) and whether it is still
|
|
59
|
+
* open.
|
|
60
|
+
*/
|
|
61
|
+
getById(id: number): StoredConflict | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Close every OPEN conflict that involves `factId`.
|
|
64
|
+
*
|
|
65
|
+
* Called when a fact leaves the ACTIVE corpus (archived / superseded / forgotten).
|
|
66
|
+
* Two things depend on it:
|
|
67
|
+
* - the conflict is no longer actionable, and `list` would otherwise keep showing a
|
|
68
|
+
* row whose `content_a/content_b` describe a revision that is already archived;
|
|
69
|
+
* - `update` archives the old revision and writes a NEW fact, so without this every
|
|
70
|
+
* edit of a conflicting fact appended another permanent open row for the same
|
|
71
|
+
* logical conflict — and nothing in the API can retire one (there is no
|
|
72
|
+
* `resolve` action on any tool/CLI/MCP surface).
|
|
73
|
+
*
|
|
74
|
+
* The archived fact is recorded as the loser: it is the revision that left the store.
|
|
75
|
+
*/
|
|
76
|
+
resolveForFact(factId: number, resolution?: 'true_positive' | 'false_positive'): number;
|
|
77
|
+
/**
|
|
78
|
+
* {@link resolveForFact} for a BATCH of ids leaving the corpus — the lifecycle tick path.
|
|
79
|
+
*
|
|
80
|
+
* The per-id loop cost O(archived × log) plus one statement compile per id: measured 6.3 s for
|
|
81
|
+
* 999 ids at 99k open pairs. Two statements PER BATCH (one per side) do the same mission: each is
|
|
82
|
+
* an index seek on the plain `fact_a`/`fact_b` index, and the side that matched supplies the
|
|
83
|
+
* loser (the fact that actually left the corpus), so the recorded verdict is unchanged.
|
|
84
|
+
*/
|
|
85
|
+
resolveForFacts(factIds: readonly number[], resolution?: 'true_positive' | 'false_positive'): number;
|
|
86
|
+
list(resolved?: number, category?: string, threshold?: number, limit?: number): ContradictionRecord[];
|
|
87
|
+
/**
|
|
88
|
+
* Every OPEN conflict that names `factId`, most severe first, with BOTH sides still active.
|
|
89
|
+
*
|
|
90
|
+
* The detector's own `logged` list carries only what THIS pass inserted (`maybeLog` skips an
|
|
91
|
+
* already-open pair), while a caller asking "what conflicts with the fact I just wrote?"
|
|
92
|
+
* needs the whole open set: an update that merges into an existing fact changes state
|
|
93
|
+
* without inserting a row for a conflict that was already open, and reporting nothing there
|
|
94
|
+
* is a false all-clear.
|
|
95
|
+
*/
|
|
96
|
+
openConflictsFor(factId: number, limit?: number): ConflictPair[];
|
|
97
|
+
private normalize;
|
|
98
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `documents` — the knowledge corpus identity table (`domain → source → title`).
|
|
3
|
+
*
|
|
4
|
+
* Identity is the `(domain, source, title)` unique key, so `upsert` is `INSERT OR IGNORE`
|
|
5
|
+
* followed by a lookup: a re-ingest of the same document must land on the SAME `doc_id`
|
|
6
|
+
* (chunks are replaced under it), never create a second row.
|
|
7
|
+
*/
|
|
8
|
+
import type { Db } from '../port.js';
|
|
9
|
+
import type { DocumentRecord, DocumentSummary } from '../../../mem-contract/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* The stored document rows. The PAYLOAD shapes live in the contract (`kb list`/`kb detail` return
|
|
12
|
+
* them verbatim, and the settings page imports them), so these are aliases rather than a second
|
|
13
|
+
* declaration — a column added here without the contract would be a type error, not a silent
|
|
14
|
+
* extra field on the wire.
|
|
15
|
+
*/
|
|
16
|
+
type DocumentRow = DocumentSummary;
|
|
17
|
+
type DocumentDetailRow = DocumentRecord;
|
|
18
|
+
export declare class DocumentsDao {
|
|
19
|
+
private readonly db;
|
|
20
|
+
constructor(db: Db);
|
|
21
|
+
/** Idempotent upsert by the unique key; returns the existing or newly created id. */
|
|
22
|
+
upsert(domain: string, source: string, title: string, sourceUri?: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* One row by the unique identity `(domain, source, title)`, or `null` when no such document
|
|
25
|
+
* exists. Walks the `UNIQUE(domain, source, title)` index — the add-only guard's existence test.
|
|
26
|
+
*/
|
|
27
|
+
find(domain: string, source: string, title: string): DocumentRow | null;
|
|
28
|
+
/**
|
|
29
|
+
* Bump `updated_at` and record the URI that was actually read. `COALESCE` keeps the
|
|
30
|
+
* previous URI when this ingest did not come from one (plain text has none).
|
|
31
|
+
*/
|
|
32
|
+
touch(docId: number, sourceUri?: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Catalog page: newest first, optionally scoped to one domain and/or source.
|
|
35
|
+
*
|
|
36
|
+
* `limit`/`offset` page it for the UI (which renders one page at a time); omitting `limit` keeps
|
|
37
|
+
* the historical "everything" behavior the CLI and MCP surfaces expect.
|
|
38
|
+
*/
|
|
39
|
+
list(domain?: string, source?: string, limit?: number, offset?: number): DocumentRow[];
|
|
40
|
+
get(docId: number): DocumentDetailRow | null;
|
|
41
|
+
/**
|
|
42
|
+
* The distinct domains the library already holds, sorted. This is the "already in the library"
|
|
43
|
+
* half of the domain allowlist: a name in use must stay usable (and selectable) even when the
|
|
44
|
+
* configured allowlist would not have accepted it as a NEW domain.
|
|
45
|
+
*/
|
|
46
|
+
domains(): string[];
|
|
47
|
+
/** `true` when a document was actually deleted (cascades chunks/entities/FTS). */
|
|
48
|
+
remove(docId: number): boolean;
|
|
49
|
+
count(): number;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `entities` + `fact_entities` — the entity graph behind the jaccard / related / reason
|
|
3
|
+
* paths and the contradiction detector's candidate narrowing.
|
|
4
|
+
*
|
|
5
|
+
* Every id list that can grow with the CORPUS goes through `batches()`: a single `IN (…)`
|
|
6
|
+
* would exceed SQLite's bind-parameter cap and, on the write path, silently disable
|
|
7
|
+
* contradiction detection (see `db/chunk.ts`).
|
|
8
|
+
*/
|
|
9
|
+
import type { Db } from '../port.js';
|
|
10
|
+
export declare class EntitiesDao {
|
|
11
|
+
private readonly db;
|
|
12
|
+
constructor(db: Db);
|
|
13
|
+
/**
|
|
14
|
+
* Ensure every name exists and link the fact to them.
|
|
15
|
+
*
|
|
16
|
+
* `INSERT OR IGNORE` + the `name` uniqueness make this idempotent, which matters because
|
|
17
|
+
* both `add` and the revive branch of `persistFact` call it for the same fact.
|
|
18
|
+
*/
|
|
19
|
+
linkFact(factId: number, names: readonly string[]): void;
|
|
20
|
+
/**
|
|
21
|
+
* Drop one fact's links, leaving the shared `entities` vocabulary alone.
|
|
22
|
+
*
|
|
23
|
+
* `linkFact` is additive (`INSERT OR IGNORE`) — correct for the write path, where a fact's links
|
|
24
|
+
* are written once — so RE-EXTRACTION has to clear first or the old names would survive alongside
|
|
25
|
+
* the new ones. Orphaned `entities` rows are harmless: the entity leg joins `fact_entities`, so
|
|
26
|
+
* an unlinked name is unreachable, and a later fact using it reuses the row.
|
|
27
|
+
*/
|
|
28
|
+
unlinkFact(factId: number): void;
|
|
29
|
+
/** One fact's entity names (detail / contradiction scoring). */
|
|
30
|
+
namesForFact(factId: number): string[];
|
|
31
|
+
/** Batched entity bags for a candidate set: `Map<factId, names>` (ids with none get `[]`). */
|
|
32
|
+
bagsForFacts(ids: readonly number[]): Map<number, string[]>;
|
|
33
|
+
/** How many entities a fact is linked to — the scorer's `|A|`. */
|
|
34
|
+
countForFact(factId: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Candidate facts for the embedding leg, narrowed by the scorer's NECESSARY conditions
|
|
37
|
+
* (see {@link ContradictDetector}): at least `minShared` shared entities and at most
|
|
38
|
+
* `maxEntities` of their own, both active and never the checked fact itself.
|
|
39
|
+
*
|
|
40
|
+
* `INDEXED BY` pins the join order to mine → other → facts. Without statistics SQLite
|
|
41
|
+
* drives from `facts` by status instead (a corpus-sized scan: measured 115 ms with zero
|
|
42
|
+
* candidates on a 300k-fact store, versus 0.13 ms here), and it only collects statistics on
|
|
43
|
+
* the lifecycle tick — so a store that grew since startup would charge every write for the
|
|
44
|
+
* whole corpus. The hint makes the plan independent of ANALYZE timing.
|
|
45
|
+
*/
|
|
46
|
+
candidateFacts(factId: number, minShared: number, maxEntities: number): number[];
|
|
47
|
+
/** Active facts linked to `name`, optionally limited to one category (`related` leg). */
|
|
48
|
+
activeFactsForEntity(name: string, category?: string): number[];
|
|
49
|
+
/** Active facts linked to ALL of `names` (AND-join, `reason` leg). */
|
|
50
|
+
activeFactsForAllEntities(names: readonly string[]): number[];
|
|
51
|
+
/** Active facts sharing ANY of `names`, in one category (`jaccard` leg). */
|
|
52
|
+
/**
|
|
53
|
+
* Candidate facts for an entity-Jaccard query, ordered by the ACTUAL Jaccard and CAPPED.
|
|
54
|
+
*
|
|
55
|
+
* The cap is the whole point: this leg used to return every fact sharing ANY query entity —
|
|
56
|
+
* unbounded in the corpus (measured 6600 rows for a common phrase at 33k facts, and the fusion
|
|
57
|
+
* step then normalized and sorted all of them).
|
|
58
|
+
*
|
|
59
|
+
* Ordering by the shared COUNT alone (the first version of this cap) was WRONG: the leg's score
|
|
60
|
+
* is `shared / union`, so a fact sharing 3 of 10 entities (0.30) outranked one sharing 2 of 2
|
|
61
|
+
* (0.67) and pushed it out of the cap — measured: dropping the higher-scoring document. The
|
|
62
|
+
* ratio is computed in SQL instead: `union = |q| + |f| - shared`, and `|f|` is one indexed COUNT
|
|
63
|
+
* per candidate (the `fact_entities` primary key covers it). The caller still recomputes the same
|
|
64
|
+
* ratio in JS for the survivors, which is cheap (<= the cap) and keeps one formula in charge.
|
|
65
|
+
*/
|
|
66
|
+
candidateFactsForAnyEntity(names: readonly string[], category: string | undefined, limit: number): number[];
|
|
67
|
+
}
|