@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,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `facts` — the memory store's main table.
|
|
3
|
+
*
|
|
4
|
+
* Started with the READ paths the detector, the diagnostics and the clock self-heal need;
|
|
5
|
+
* the write path (`persistFact`, archive/restore/feedback, vector repair) moves here next, so
|
|
6
|
+
* the store ends up holding business rules only and no SQL. See DESIGN §19.
|
|
7
|
+
*/
|
|
8
|
+
import type { Db } from '../port.js';
|
|
9
|
+
/**
|
|
10
|
+
* Every `facts` column EXCEPT the two BLOBs (`hrr_vector` ~8 KB, `semantic_vector` ~2 KB per row).
|
|
11
|
+
*
|
|
12
|
+
* Multi-row reads that only need scalars used to be `SELECT *`, so a page of the admin list and
|
|
13
|
+
* every recall's trust settlement dragged the whole vector corpus through the driver — tens of
|
|
14
|
+
* megabytes per query at 33k facts, for columns the caller never looked at. The vector reads that DO
|
|
15
|
+
* want them keep their own explicit projections (`activeVectorRows`, `hrrRowsForFacts`).
|
|
16
|
+
*
|
|
17
|
+
* `getById` deliberately still selects everything: `restore` re-indexes the persisted vector it
|
|
18
|
+
* reads off that one row.
|
|
19
|
+
*
|
|
20
|
+
* `test/db_lifecycle.spec.ts` asserts this list against `PRAGMA table_info(facts)`, so a new column
|
|
21
|
+
* fails a test instead of silently vanishing from these two reads.
|
|
22
|
+
*/
|
|
23
|
+
export declare const FACT_COLUMNS_NO_BLOB = "fact_id, content, category, tags, trust_score, settle_clock,\n pinned, pinned_at, bonus_count, bonus_window_at, last_reinforced_at, archived_clock,\n retrieval_count, helpful_count, last_retrieved_at, embedding_model, vector_store, status,\n supersedes_id, entities_version, conflict_checked, archived_at, archive_reason, ttl_days,\n mirror_source, mirror_target, created_at, updated_at";
|
|
24
|
+
/**
|
|
25
|
+
* The FTS leg's exact SQL, exported so the plan guard (`test/fts_plan.spec.ts`) and the bench
|
|
26
|
+
* control group EXPLAIN the production text itself instead of a copy that can silently drift.
|
|
27
|
+
*
|
|
28
|
+
* `NOT INDEXED` on the `facts` join is LOAD-BEARING, not decoration — this query is the one place
|
|
29
|
+
* where a healthy-looking covering index (`idx_facts_status_category (status, category)`) is the
|
|
30
|
+
* WRONG drive. Without planner statistics SQLite cannot know that `MATCH` is the selective access
|
|
31
|
+
* path, so it drives from `facts` and RE-RUNS the whole external-content MATCH once per row:
|
|
32
|
+
*
|
|
33
|
+
* bad : SEARCH fa USING COVERING INDEX idx_facts_status_category (status=?)
|
|
34
|
+
* | SCAN f VIRTUAL TABLE INDEX 0:=M1 <- the inner `=M1`
|
|
35
|
+
* good: SCAN f VIRTUAL TABLE INDEX 0:M1
|
|
36
|
+
* | SEARCH fa USING INTEGER PRIMARY KEY (rowid=?)
|
|
37
|
+
*
|
|
38
|
+
* Measured on the synthetic corpus at n=1000 (selective query, ~10 hits) in the fresh-session
|
|
39
|
+
* shape: 574 ms → 4.6 ms for the leg; the hint's `LIMIT 1` shape is the SAME statement, 568 ms
|
|
40
|
+
* → 1.9 ms.
|
|
41
|
+
*
|
|
42
|
+
* Why the missing statistics are not a startup-only blip: the open tick's `PRAGMA optimize` writes
|
|
43
|
+
* no stats for an EMPTY database, so on a fresh install the whole first session runs without them
|
|
44
|
+
* while the corpus grows in it; the next tick (≤60 min heartbeat) or a restart is what writes them.
|
|
45
|
+
* `bench-memory` used to hide this because it seeds, then REOPENS the runtime (whose open tick
|
|
46
|
+
* analyzes). Hence "ANALYZE facts at open when `sqlite_stat1` lacks a `facts` row and `facts` has
|
|
47
|
+
* rows" does NOT close the window either: at open the fresh database has zero rows (the guard is
|
|
48
|
+
* false), and once rows exist the next optimize is up to an hour away. `NOT INDEXED` pins the
|
|
49
|
+
* FTS-driven plan independently of `sqlite_stat1`, so there is no window at all. It cannot change
|
|
50
|
+
* results — only the join order — because the rowid join to `facts` is an INTEGER PRIMARY KEY
|
|
51
|
+
* lookup (still available under `NOT INDEXED`), and `status`/`category` stay residual filters on
|
|
52
|
+
* that one joined row, exactly as in the good plan above. Same spirit as §19 `page()`'s
|
|
53
|
+
* `INDEXED BY`, which pins the plan against `PRAGMA optimize` flipping it back.
|
|
54
|
+
*/
|
|
55
|
+
export declare const FTS_SEARCH_SQL = "SELECT f.rowid AS id, bm25(facts_fts) AS rank FROM facts_fts f\n JOIN facts fa NOT INDEXED ON fa.fact_id = f.rowid\n WHERE facts_fts MATCH ? AND fa.status = 'active' AND (? IS NULL OR fa.category = ?)\n ORDER BY rank ASC\n LIMIT ?";
|
|
56
|
+
export declare class FactsDao {
|
|
57
|
+
private readonly db;
|
|
58
|
+
constructor(db: Db);
|
|
59
|
+
/**
|
|
60
|
+
* The subset of `ids` that is ACTIVE, batched.
|
|
61
|
+
*
|
|
62
|
+
* The catch-up sweep hands this the whole pending set, which grows with the corpus: one
|
|
63
|
+
* statement would exceed SQLite's bind-parameter cap (see `db/chunk.ts`).
|
|
64
|
+
*/
|
|
65
|
+
activeIds(ids: readonly number[]): number[];
|
|
66
|
+
/** One row by id (the store's `getRow`), undefined when absent. */
|
|
67
|
+
getById(factId: number): Record<string, unknown> | undefined;
|
|
68
|
+
/** Lowest `settle_clock` in the store (trust diagnostics). */
|
|
69
|
+
minSettleClock(): number | null;
|
|
70
|
+
/** Vector-index health counters: total rows, rows with a vector, active rows without one. */
|
|
71
|
+
vectorCounts(): {
|
|
72
|
+
total: number;
|
|
73
|
+
withSemantic: number;
|
|
74
|
+
missing: number;
|
|
75
|
+
};
|
|
76
|
+
/** Highest `settle_clock` ever written, or null on an empty store (clock self-heal). */
|
|
77
|
+
maxSettleClock(): number | null;
|
|
78
|
+
countAll(): number;
|
|
79
|
+
countByStatus(): {
|
|
80
|
+
active: number;
|
|
81
|
+
archived: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Rows by id (trust settlement, hit projection); order is the caller's business.
|
|
85
|
+
*
|
|
86
|
+
* Scalars only — see {@link FACT_COLUMNS_NO_BLOB}. This runs on EVERY recall, over the whole hit
|
|
87
|
+
* set, to read trust and window fields.
|
|
88
|
+
*/
|
|
89
|
+
rowsByIds(ids: readonly number[]): Record<string, unknown>[];
|
|
90
|
+
/** `fact_id → content` for a hit set (never N+1). */
|
|
91
|
+
textsByIds(ids: readonly number[]): Map<number, string>;
|
|
92
|
+
/** `fact_id → created_at/updated_at` for a hit set (the recall hit's two timestamps). */
|
|
93
|
+
timesByIds(ids: readonly number[]): Map<number, {
|
|
94
|
+
created_at: string;
|
|
95
|
+
updated_at: string | null;
|
|
96
|
+
}>;
|
|
97
|
+
/** Active rows that carry a persisted vector (index classification). */
|
|
98
|
+
activeVectorRows(): {
|
|
99
|
+
fact_id: number;
|
|
100
|
+
semantic_vector: Uint8Array;
|
|
101
|
+
embedding_model: string | null;
|
|
102
|
+
}[];
|
|
103
|
+
/**
|
|
104
|
+
* Facts per archive reason — the trust diagnostics breakdown. One grouped scan rather than
|
|
105
|
+
* a count per reason, because the reasons are few but the table is not.
|
|
106
|
+
*/
|
|
107
|
+
archivedByReason(): {
|
|
108
|
+
reason: string;
|
|
109
|
+
n: number;
|
|
110
|
+
}[];
|
|
111
|
+
/** Distinct embedding models among active facts (vector diagnostics). */
|
|
112
|
+
embeddingModels(): {
|
|
113
|
+
model: string | null;
|
|
114
|
+
n: number;
|
|
115
|
+
}[];
|
|
116
|
+
/**
|
|
117
|
+
* Insert one revision. `INSERT OR IGNORE` is the duplicate-content guard (R1), so the
|
|
118
|
+
* caller must treat `changes === 0` as "content already exists" and fall back to the
|
|
119
|
+
* duplicate branch.
|
|
120
|
+
*
|
|
121
|
+
* `createdAt` is named EXPLICITLY rather than left to the column DEFAULT, because a
|
|
122
|
+
* superseding revision must INHERIT the archived row's `created_at` (the memory's first
|
|
123
|
+
* assertion date, distinct from when its text was rewritten). `updated_at` and
|
|
124
|
+
* `last_retrieved_at` are stamped `CURRENT_TIMESTAMP` in the same statement: a revision
|
|
125
|
+
* whose `created_at` is old must not be judged idle by the next tick, where "idle" reads
|
|
126
|
+
* `COALESCE(last_retrieved_at, created_at)`.
|
|
127
|
+
*/
|
|
128
|
+
insertRevision(v: {
|
|
129
|
+
content: string;
|
|
130
|
+
category: string;
|
|
131
|
+
ttlDays: number;
|
|
132
|
+
supersedesId: number | null;
|
|
133
|
+
hrr: Uint8Array;
|
|
134
|
+
trust: number;
|
|
135
|
+
clock: number;
|
|
136
|
+
pinned: 0 | 1;
|
|
137
|
+
pinnedAt: string | null;
|
|
138
|
+
windowAt: string;
|
|
139
|
+
createdAt: string;
|
|
140
|
+
}): {
|
|
141
|
+
changes: number;
|
|
142
|
+
lastInsertRowid: number | bigint;
|
|
143
|
+
};
|
|
144
|
+
findByContent(content: string): Record<string, unknown> | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* ACTIVE facts whose entity/triple rows were produced by OLDER extraction rules.
|
|
147
|
+
*
|
|
148
|
+
* `entities_version < ?` (not `!= ?` or `IS NULL OR`) so `idx_facts_entities_version` serves it
|
|
149
|
+
* as a range seek; the column is `NOT NULL DEFAULT 0`, so no row can be invisible to it.
|
|
150
|
+
* Budgeted, because the first sweep after a rule change has the whole corpus to do.
|
|
151
|
+
*
|
|
152
|
+
* ORDER BY is the INDEX's own order (`entities_version, fact_id`), deliberately: ordering by
|
|
153
|
+
* `fact_id` alone cannot be served by an index whose first column is a RANGE, so the planner
|
|
154
|
+
* sorted every matching row in a temp b-tree before applying the LIMIT — i.e. the budget
|
|
155
|
+
* bounded what was rebuilt but not what was read. This order also drains the oldest vintage
|
|
156
|
+
* first, which is the more useful one when several rule changes are pending.
|
|
157
|
+
*/
|
|
158
|
+
staleEntityRows(version: number, limit: number): {
|
|
159
|
+
fact_id: number;
|
|
160
|
+
content: string;
|
|
161
|
+
}[];
|
|
162
|
+
countStaleEntities(version: number): number;
|
|
163
|
+
/** Stamp the rules that produced a batch of rows' entity/triple data. */
|
|
164
|
+
setEntitiesVersion(ids: readonly number[], version: number): number;
|
|
165
|
+
/**
|
|
166
|
+
* Replace a fact's HRR vector.
|
|
167
|
+
*
|
|
168
|
+
* The bundle is derived from the ENTITY NAMES (`encodeHrrEntityVector`), so re-extraction has to
|
|
169
|
+
* rewrite it together with the entity rows — otherwise the probe leg would keep scoring a bundle
|
|
170
|
+
* built from names the fact no longer has.
|
|
171
|
+
*/
|
|
172
|
+
setHrrVector(factId: number, bytes: Uint8Array): void;
|
|
173
|
+
/**
|
|
174
|
+
* ACTIVE facts still awaiting the EMBEDDING leg of the conflict check, in `fact_id` order.
|
|
175
|
+
*
|
|
176
|
+
* The durable replacement for the detector's in-process pending set: a fact written while the
|
|
177
|
+
* embedder was unavailable stays here across restarts instead of being lost (and instead of the
|
|
178
|
+
* set growing without bound).
|
|
179
|
+
*
|
|
180
|
+
* `semantic_vector IS NOT NULL` is a cheap pre-filter, NOT the completion test: the legs are
|
|
181
|
+
* scored from the live index, and a row can hold a vector the index cannot serve (another
|
|
182
|
+
* process's write, a blob this process's index dropped at rebuild). The store stamps only what a
|
|
183
|
+
* pass could actually complete, so such a row stays here until `vectors_fix` re-encodes it.
|
|
184
|
+
*
|
|
185
|
+
* `after` is what keeps that from becoming a stall. The drain takes rows ABOVE the last id it
|
|
186
|
+
* attempted, so a row it cannot complete is stepped over rather than pinned to the head with
|
|
187
|
+
* every row behind it: without this, either the drain re-selects the same un-completable rows
|
|
188
|
+
* forever (and never reaches the rest of the queue), or it has to stamp rows it never checked,
|
|
189
|
+
* which is the defect the completion test exists to prevent.
|
|
190
|
+
*
|
|
191
|
+
* The ORDER BY rides `idx_facts_conflict_pending` (both leading columns are equalities, so the
|
|
192
|
+
* index is already in `fact_id` order) — no temp b-tree, and no blob in the index.
|
|
193
|
+
*/
|
|
194
|
+
pendingConflictRows(limit: number, after?: number): {
|
|
195
|
+
fact_id: number;
|
|
196
|
+
content: string;
|
|
197
|
+
}[];
|
|
198
|
+
/**
|
|
199
|
+
* Facts whose embedding-leg check has not run.
|
|
200
|
+
*
|
|
201
|
+
* Deliberately WITHOUT the `semantic_vector IS NOT NULL` filter that {@link pendingConflictRows}
|
|
202
|
+
* needs: the interesting number is "how far behind is the detector", and with the embedder down
|
|
203
|
+
* that is exactly the population with NO vector — those are waiting for the model (and for
|
|
204
|
+
* `vectors_fix`), so hiding them would report an empty queue while nothing had been checked.
|
|
205
|
+
*/
|
|
206
|
+
countPendingConflicts(): number;
|
|
207
|
+
markConflictChecked(ids: readonly number[]): number;
|
|
208
|
+
/**
|
|
209
|
+
* Send facts BACK to the conflict queue (`conflict_checked = 0`), batched.
|
|
210
|
+
*
|
|
211
|
+
* Needed whenever a fact's derived inputs move, because the verdict is a statement about those
|
|
212
|
+
* inputs: the embedding leg's candidate set comes from `fact_entities`, and the structural leg
|
|
213
|
+
* reads `triples`. A rule change rewrites both (`reindexEntities`), so the old verdict was about
|
|
214
|
+
* a fact that no longer exists — and leaving the marker would keep it while `conflict_pending`
|
|
215
|
+
* reported zero behind. Same rule as {@link setSemanticVector}: what the check SAW changed.
|
|
216
|
+
*/
|
|
217
|
+
requeueConflictCheck(ids: readonly number[]): number;
|
|
218
|
+
/** Revive an archived row (R18/R19: refresh the idle clock, clear the archive bookkeeping). */
|
|
219
|
+
revive(factId: number, category: string, ttlDays: number, trust: number, clock: number): number;
|
|
220
|
+
/** Land explicit category/TTL on a row that already exists (pure duplicate). */
|
|
221
|
+
applyValues(factId: number, category: string, ttlDays: number): void;
|
|
222
|
+
statusOf(factId: number): string | undefined;
|
|
223
|
+
/**
|
|
224
|
+
* Land a (re-)encoded vector, and RE-QUEUE the fact for the conflict check.
|
|
225
|
+
*
|
|
226
|
+
* `conflict_checked` answers "has the embedding leg run against the vector in this row?", so a
|
|
227
|
+
* new vector invalidates it: the scorer's second input is the vector, and a re-encode (a model
|
|
228
|
+
* swap, `vectors_fix`, a first encode) is a different question from the one that was answered.
|
|
229
|
+
* Without the reset the marker means "checked in whatever space this row happened to be in when
|
|
230
|
+
* the leg last ran" — and after a model swap the leg had never run in the new space at all,
|
|
231
|
+
* while `conflict_pending` reported zero. See DESIGN §20.16.
|
|
232
|
+
*/
|
|
233
|
+
setSemanticVector(factId: number, vector: Uint8Array, model: string, store: string): void;
|
|
234
|
+
/** Archive with an explicit reason; returns how many rows moved. */
|
|
235
|
+
archive(factId: number, clock: number, reason: string): number;
|
|
236
|
+
/** The superseded revision leaves the active corpus (`replaced`). */
|
|
237
|
+
archiveSuperseded(factId: number, clock: number): number;
|
|
238
|
+
linkSupersede(factId: number, supersedesId: number): void;
|
|
239
|
+
/**
|
|
240
|
+
* Restore an archived fact, settling its trust and refreshing the idle clock (spec §2.6).
|
|
241
|
+
*
|
|
242
|
+
* `AND status = 'archived'` is load-bearing, like the guard on every sibling above: this UPDATE
|
|
243
|
+
* clears the archive bookkeeping and refreshes `last_retrieved_at`, so ungated it would take an
|
|
244
|
+
* ACTIVE row and (a) push its idle clock forward — silently extending the life of a fact the next
|
|
245
|
+
* tick was about to re-archive — and (b) reset `trust_score` to the settled value it computed. It
|
|
246
|
+
* returned `1` either way, so the caller could not tell that a row it never asked about had moved.
|
|
247
|
+
*/
|
|
248
|
+
restore(factId: number, trust: number, clock: number): number;
|
|
249
|
+
bumpHelpful(factId: number, delta: number): void;
|
|
250
|
+
/** Feedback outcome: pin at the permanent threshold (trust snaps to 1). */
|
|
251
|
+
pinOutcome(factId: number, clock: number, bonusCount: number, windowAt: string): void;
|
|
252
|
+
/** Feedback outcome: the forget line was reached — archive in the same statement. */
|
|
253
|
+
forgetOutcome(factId: number, trust: number, clock: number, bonusCount: number, windowAt: string): void;
|
|
254
|
+
/** Feedback outcome: reinforced (or decayed) but still active. */
|
|
255
|
+
reinforceOutcome(factId: number, trust: number, clock: number, bonusCount: number, windowAt: string): void;
|
|
256
|
+
pin(factId: number, clock: number): number;
|
|
257
|
+
unpin(factId: number, clock: number): number;
|
|
258
|
+
/**
|
|
259
|
+
* One page, newest first (the `list` action).
|
|
260
|
+
*
|
|
261
|
+
* TWO shapes, deliberately, because they want different indexes and the shared
|
|
262
|
+
* `(? IS NULL OR category = ?)` form made the planner sort EVERY active row for the unfiltered
|
|
263
|
+
* one (measured 124 ms at 33k facts against 2.1 ms with an index that carries the order):
|
|
264
|
+
*
|
|
265
|
+
* - unfiltered: pinned to `idx_facts_created(status, created_at DESC)`. `INDEXED BY` is
|
|
266
|
+
* load-bearing, not decoration — the lifecycle tick runs `PRAGMA optimize`, and once
|
|
267
|
+
* `sqlite_stat1` exists the planner can switch this query back to `SCAN facts` + a temp
|
|
268
|
+
* b-tree (verified at 33k rows), silently undoing the fix at exactly the scale it matters.
|
|
269
|
+
* - category-filtered: left to the planner, which seeks `idx_facts_status_category` and sorts
|
|
270
|
+
* only that category's rows. Pinning the created-at index here would instead walk the whole
|
|
271
|
+
* index in order and filter per row (measured 9.8 ms for a category with 17 rows at 33k),
|
|
272
|
+
* i.e. it would trade a fast path for a slow one to save an index we do not need.
|
|
273
|
+
*/
|
|
274
|
+
page(status: string, category: string | undefined, limit: number, offset: number): Record<string, unknown>[];
|
|
275
|
+
countInStatus(status: string, category?: string): number;
|
|
276
|
+
/** Every ACTIVE fact (the report's `active`). */
|
|
277
|
+
countActive(): number;
|
|
278
|
+
/** ACTIVE and permanent — pinned facts are exempt from decay (the report's `pinned`). */
|
|
279
|
+
countPinnedActive(): number;
|
|
280
|
+
/**
|
|
281
|
+
* ACTIVE facts whose decayed trust would reach zero within `withinDays` ACTIVE days —
|
|
282
|
+
* i.e. `trust_score - step × (clock - settle_clock) <= step × withinDays`.
|
|
283
|
+
*/
|
|
284
|
+
countForgettingWithin(p: {
|
|
285
|
+
step: number;
|
|
286
|
+
clock: number;
|
|
287
|
+
withinDays: number;
|
|
288
|
+
}): number;
|
|
289
|
+
/** Facts with a still-live (calendar 24h) reinforcement window — the per-fact quota. */
|
|
290
|
+
/**
|
|
291
|
+
* The two "reinforcement quota" numbers of `trust_diagnose`, from ONE statement.
|
|
292
|
+
*
|
|
293
|
+
* They share a predicate and used to run it twice — twice the mission for two columns of the same
|
|
294
|
+
* rows. It is now also sargable (`bonus_window_at > datetime('now', '-1 day')`, a plain string
|
|
295
|
+
* comparison on the stored UTC-ISO format) and served by `idx_facts_bonus_window`, instead of
|
|
296
|
+
* calling `julianday` on every row of `facts` (measured 13.4–16.2 ms at 20k, per query).
|
|
297
|
+
*/
|
|
298
|
+
bonusStatedToday(): {
|
|
299
|
+
count: number;
|
|
300
|
+
granted: number;
|
|
301
|
+
};
|
|
302
|
+
/** ACTIVE, unpinned facts unused for `days` CALENDAR days (the idle-archival fallback). */
|
|
303
|
+
countIdleCandidates(days: number): number;
|
|
304
|
+
/**
|
|
305
|
+
* Rows still due for ②③④ after a budgeted tick — what the NEXT pass will archive.
|
|
306
|
+
*
|
|
307
|
+
* One statement over the three predicates (they are alternatives, and the sets are disjoint:
|
|
308
|
+
* a row has either a TTL, a forgotten trust score, or an idle clock). Only called when a step
|
|
309
|
+
* actually filled its budget, so the common tick pays nothing for it.
|
|
310
|
+
*/
|
|
311
|
+
countArchiveBacklog(p: {
|
|
312
|
+
forgetThreshold: number;
|
|
313
|
+
idleCalendarDays: number;
|
|
314
|
+
}): number;
|
|
315
|
+
/**
|
|
316
|
+
* Drop unusable (stale-dim) vectors so a re-encode can replace them — and re-queue the same
|
|
317
|
+
* rows, for the same reason {@link setSemanticVector} does: the vector the leg was checked
|
|
318
|
+
* against is gone, so the check has to run again once the replacement lands.
|
|
319
|
+
*/
|
|
320
|
+
clearVectors(ids: readonly number[]): number;
|
|
321
|
+
/** Active facts with no persisted vector — what `vectors_fix` has to encode. */
|
|
322
|
+
missingVectorRows(): {
|
|
323
|
+
fact_id: number;
|
|
324
|
+
content: string;
|
|
325
|
+
}[];
|
|
326
|
+
/** Persisted HRR vectors of the active corpus (probe leg). */
|
|
327
|
+
/**
|
|
328
|
+
* Active HRR rows, NEWEST FIRST when capped.
|
|
329
|
+
*
|
|
330
|
+
* `LIMIT` without `ORDER BY` returns an arbitrary (implementation-defined) slice, which is the
|
|
331
|
+
* wrong way to bound a leg: the caller could not reason about what it had scored, and at 33k
|
|
332
|
+
* facts with a cap of 200 it silently made 32.8k of them unreachable. Recency is the honest
|
|
333
|
+
* prior for a memory store and `idx_facts_created(status, created_at DESC)` already serves it.
|
|
334
|
+
*/
|
|
335
|
+
activeHrrRows(category?: string, limit?: number): {
|
|
336
|
+
fact_id: number;
|
|
337
|
+
hrr_vector: Uint8Array;
|
|
338
|
+
}[];
|
|
339
|
+
/**
|
|
340
|
+
* HRR vectors for a KNOWN candidate set (batched) — the probe leg's normal path.
|
|
341
|
+
*
|
|
342
|
+
* Reading every active row meant decoding one 8 KB blob per fact and running 1024 `cos` calls
|
|
343
|
+
* on each (measured 1.37 s for a probe at 33k facts). The candidates come from the entity leg,
|
|
344
|
+
* which is the same information the HRR bundle encodes (its atoms ARE the entity names), so the
|
|
345
|
+
* narrowing removes the O(N) part rather than a meaningful part of the ranking.
|
|
346
|
+
*/
|
|
347
|
+
hrrRowsForFacts(ids: readonly number[], category?: string): {
|
|
348
|
+
fact_id: number;
|
|
349
|
+
hrr_vector: Uint8Array;
|
|
350
|
+
}[];
|
|
351
|
+
/** ACTIVE subset of `ids` in one category (`null` = any) — the semantic leg's filter. */
|
|
352
|
+
activeIdsIn(ids: readonly number[], category?: string): number[];
|
|
353
|
+
/** `retrieval_count` + the "last used" clock (spec §2.5), batched. */
|
|
354
|
+
touchUsage(ids: readonly number[]): void;
|
|
355
|
+
/** Recall bonus not granted: only the settled trust moved. */
|
|
356
|
+
settleTrust(factId: number, trust: number, clock: number): void;
|
|
357
|
+
/** Recall bonus granted: trust + the daily quota window. */
|
|
358
|
+
reinforce(factId: number, trust: number, clock: number, bonusCount: number, windowAt: string): void;
|
|
359
|
+
/**
|
|
360
|
+
* FTS5 leg over the external-content `facts_fts`.
|
|
361
|
+
*
|
|
362
|
+
* `bm25()` is negative (more negative = better), so the caller negates; a JOIN back to
|
|
363
|
+
* `facts` keeps archived rows and other categories out of the leg — and `NOT INDEXED` keeps
|
|
364
|
+
* that JOIN from becoming the drive (see {@link FTS_SEARCH_SQL}, which carries the plans and
|
|
365
|
+
* measurements).
|
|
366
|
+
*/
|
|
367
|
+
ftsSearch(ftsQuery: string, category?: string, limit?: number): {
|
|
368
|
+
id: number;
|
|
369
|
+
rank: number;
|
|
370
|
+
}[];
|
|
371
|
+
/**
|
|
372
|
+
* ①'s predicate: the active-day counter crossed an integer boundary since the last settle.
|
|
373
|
+
* Kept next to its four siblings so the tick's ORDER and the SQL stay in one place.
|
|
374
|
+
*
|
|
375
|
+
* SARGABLE on purpose. It used to be `CAST(settle_clock AS INTEGER) < CAST(:clock AS INTEGER)`,
|
|
376
|
+
* and a function on the LEFT column forbids a range seek on
|
|
377
|
+
* `idx_facts_trust(status, pinned, settle_clock)` — so every tick read EVERY active unpinned row
|
|
378
|
+
* (measured 13.5 ms per statement at 180k, twice: settle + countPendingSettle).
|
|
379
|
+
*
|
|
380
|
+
* Equivalence for `settle_clock >= 0` (it is an active-day counter, and clocks only advance):
|
|
381
|
+
* with `c = CAST(:clock AS INTEGER)` an integer, `floor(x) < c` ⟺ `x < c` — if `x < c` then
|
|
382
|
+
* `floor(x) <= x < c`; if `x >= c` then `floor(x) >= c`. So dropping the left CAST changes no
|
|
383
|
+
* row, and `settle_clock < c` is a seek + range scan of exactly the pending rows.
|
|
384
|
+
*/
|
|
385
|
+
private static readonly SETTLE_PREDICATE;
|
|
386
|
+
/** ① materialize decay, bounded to `budget` rows. */
|
|
387
|
+
settleBudgeted(p: {
|
|
388
|
+
step: number;
|
|
389
|
+
clock: number;
|
|
390
|
+
budget: number;
|
|
391
|
+
}): number;
|
|
392
|
+
/** ① materialize decay for every pending row (no budget). */
|
|
393
|
+
settleAll(p: {
|
|
394
|
+
step: number;
|
|
395
|
+
clock: number;
|
|
396
|
+
}): number;
|
|
397
|
+
/** Rows still awaiting ① (the budget ran out). */
|
|
398
|
+
countPendingSettle(p: {
|
|
399
|
+
clock: number;
|
|
400
|
+
}): number;
|
|
401
|
+
/**
|
|
402
|
+
* ② calendar TTL — explicit instruction, so `pinned` is NOT exempt.
|
|
403
|
+
*
|
|
404
|
+
* `budget` (0 = none) caps how many rows ONE tick may archive: the tick is a single IMMEDIATE
|
|
405
|
+
* transaction shared by several processes, so an unbudgeted pass over a large backlog held the
|
|
406
|
+
* write lock for as long as it took (measured 14400 rows in one tick). The remainder stays due
|
|
407
|
+
* and is reported as a deferred count, then archived by the next tick.
|
|
408
|
+
*/
|
|
409
|
+
archiveExpiredByTtl(p: {
|
|
410
|
+
clock: number;
|
|
411
|
+
budget?: number;
|
|
412
|
+
}): number[];
|
|
413
|
+
/**
|
|
414
|
+
* ③ trust-driven forgetting (① already clamped trust to the threshold).
|
|
415
|
+
*
|
|
416
|
+
* Sargable as written (`status`/`pinned` equality + `trust_score <= ?`), and served by
|
|
417
|
+
* `idx_facts_forget(status, pinned, trust_score)`: before that index this statement read every
|
|
418
|
+
* active unpinned row per tick (measured 18.6–27.4 ms at 180k, with ZERO matches).
|
|
419
|
+
*/
|
|
420
|
+
archiveForgotten(p: {
|
|
421
|
+
clock: number;
|
|
422
|
+
forgetThreshold: number;
|
|
423
|
+
budget?: number;
|
|
424
|
+
}): number[];
|
|
425
|
+
/**
|
|
426
|
+
* ④ calendar fallback: never touched for `idle_calendar_days`.
|
|
427
|
+
*
|
|
428
|
+
* The comparison is written as `<expression> < julianday('now', :modifier)` — the same
|
|
429
|
+
* expression `idx_facts_idle_cutoff` indexes — so it is a range seek. The old form
|
|
430
|
+
* (`julianday('now') - <expression> > :days`) called `julianday` on EVERY active row and could
|
|
431
|
+
* not use any index (measured 33–212 ms at 180k).
|
|
432
|
+
*/
|
|
433
|
+
archiveIdle(p: {
|
|
434
|
+
clock: number;
|
|
435
|
+
idleCalendarDays: number;
|
|
436
|
+
budget?: number;
|
|
437
|
+
}): number[];
|
|
438
|
+
/**
|
|
439
|
+
* ⑤ physical delete of archived rows past the retention window.
|
|
440
|
+
*
|
|
441
|
+
* TWO statements in this order, sharing one predicate:
|
|
442
|
+
*
|
|
443
|
+
* 1. clear `supersedes_id` on the rows about to be deleted. The column self-references
|
|
444
|
+
* `facts`, and an ACTIVE revision points at the archived revision it replaced, so the
|
|
445
|
+
* DELETE below failed with `SQLITE_CONSTRAINT_FOREIGNKEY` whenever a revision chain
|
|
446
|
+
* crossed `purge_after_archived_days`. Because the whole tick is ONE transaction and it
|
|
447
|
+
* runs from the store's constructor, that failure did not merely skip the purge: it rolled
|
|
448
|
+
* back settle/TTL/idle with it and left the process unable to start, permanently.
|
|
449
|
+
* 2. delete them.
|
|
450
|
+
*
|
|
451
|
+
* The predicate is built once and interpolated into both statements, so "which rows are
|
|
452
|
+
* purged" cannot drift between the unlink and the delete. Both run inside the caller's
|
|
453
|
+
* IMMEDIATE transaction (`runTrustTick`), so no reader observes the unlinked state.
|
|
454
|
+
*/
|
|
455
|
+
purgeArchived(p: {
|
|
456
|
+
clock: number;
|
|
457
|
+
enabled: number;
|
|
458
|
+
purgeAfterDays: number;
|
|
459
|
+
skipPinned: boolean;
|
|
460
|
+
budget?: number;
|
|
461
|
+
}): number[];
|
|
462
|
+
/** The two mutually exclusive purge branches (see `purgeArchived` for why they are split). */
|
|
463
|
+
private static purgeBranches;
|
|
464
|
+
private static purgeParams;
|
|
465
|
+
/** Archived rows still due for ⑤ after a budgeted pass (0 when the pass finished them). */
|
|
466
|
+
countPurgeBacklog(p: {
|
|
467
|
+
clock: number;
|
|
468
|
+
enabled: number;
|
|
469
|
+
purgeAfterDays: number;
|
|
470
|
+
skipPinned: boolean;
|
|
471
|
+
}): number;
|
|
472
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `avantf_stats` — the key/value side table (currently the active-day clock and the last
|
|
3
|
+
* presence timestamp).
|
|
4
|
+
*
|
|
5
|
+
* Lives in `db/dao/` like every other aggregate: the SQL belongs to the DAO, while what the
|
|
6
|
+
* clock MEANS and when it advances stays in `lifecycle/presence.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import type { Db } from '../port.js';
|
|
9
|
+
export declare class StatsDao {
|
|
10
|
+
private readonly db;
|
|
11
|
+
constructor(db: Db);
|
|
12
|
+
read(key: string): string | null;
|
|
13
|
+
write(key: string, value: string): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `triples` — the SPO graph behind `ask` / `chain`, the structural contradiction signals and
|
|
3
|
+
* the supersede cleanup.
|
|
4
|
+
*
|
|
5
|
+
* The two structural lookups are exported as SQL builders so that the statement whose QUERY PLAN
|
|
6
|
+
* matters (they must seek a triples index and never scan) is the very one production runs, not a
|
|
7
|
+
* copy of it kept somewhere else.
|
|
8
|
+
*/
|
|
9
|
+
import type { Db } from '../port.js';
|
|
10
|
+
interface TripleRow {
|
|
11
|
+
subj: string;
|
|
12
|
+
pred: string;
|
|
13
|
+
obj: string;
|
|
14
|
+
confidence: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Structural lookup ①: same (subj, obj), opposite polarity.
|
|
18
|
+
*
|
|
19
|
+
* JOIN, never `fact_id IN (SELECT fact_id FROM facts WHERE status='active')`: the IN-subquery
|
|
20
|
+
* makes SQLite materialize EVERY active fact id on each call, so the cost tracked the corpus
|
|
21
|
+
* instead of the match set — measured 202 ms with ZERO matches on a 300k-triple corpus, paid
|
|
22
|
+
* by every `remember`. The join keeps the plan on `idx_triples_subj` and checks the (indexed)
|
|
23
|
+
* status per candidate row: 0.18 ms, and it does not depend on planner statistics.
|
|
24
|
+
*/
|
|
25
|
+
export declare function polarityLookupSql(predCount: number): string;
|
|
26
|
+
/** Structural lookup ②: same (subj, pred), different object. Same rewrite as above. */
|
|
27
|
+
export declare function sameSubjPredLookupSql(): string;
|
|
28
|
+
/** The optional `subj`/`pred`/`obj` slots an `ask` pattern may fill. */
|
|
29
|
+
interface TriplePatternParts {
|
|
30
|
+
subj?: string;
|
|
31
|
+
pred?: string;
|
|
32
|
+
obj?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class TriplesDao {
|
|
35
|
+
private readonly db;
|
|
36
|
+
constructor(db: Db);
|
|
37
|
+
/** Idempotent insert for a fact's extracted triples. */
|
|
38
|
+
insertMany(factId: number, triples: readonly {
|
|
39
|
+
subj: string;
|
|
40
|
+
pred: string;
|
|
41
|
+
obj: string;
|
|
42
|
+
confidence: number;
|
|
43
|
+
source: string;
|
|
44
|
+
}[]): void;
|
|
45
|
+
/** The fact's triples, in insertion order (detail view). */
|
|
46
|
+
listForFact(factId: number): TripleRow[];
|
|
47
|
+
/** Minimal projection the contradiction detector reads. */
|
|
48
|
+
getByFact(factId: number): {
|
|
49
|
+
subj: string;
|
|
50
|
+
pred: string;
|
|
51
|
+
obj: string;
|
|
52
|
+
}[];
|
|
53
|
+
/** Drop a fact's triples — the superseded revision leaves the live graph. */
|
|
54
|
+
deleteForFact(factId: number): void;
|
|
55
|
+
findPolarityCounterparts(subj: string, obj: string, preds: readonly string[], excludeFactId: number): {
|
|
56
|
+
fact_id: number;
|
|
57
|
+
}[];
|
|
58
|
+
findSameSubjPredOtherObj(subj: string, pred: string, obj: string, excludeFactId: number): {
|
|
59
|
+
fact_id: number;
|
|
60
|
+
}[];
|
|
61
|
+
/**
|
|
62
|
+
* First hop of `chain`: the objects reachable from `subj` (optionally via one predicate).
|
|
63
|
+
*
|
|
64
|
+
* JOIN, not `fact_id IN (SELECT … WHERE status='active')` — the same shape {@link polarityLookupSql}
|
|
65
|
+
* documents as a measured 202 ms on a 300k-triple corpus with zero matches, because the subquery
|
|
66
|
+
* materializes EVERY active fact id per call. The chain path paid that on every hop.
|
|
67
|
+
*/
|
|
68
|
+
objectsForSubject(subj: string, pred?: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Second hop of `chain`: active facts whose subject is one of the mid names.
|
|
71
|
+
*
|
|
72
|
+
* JOIN for the same reason as {@link objectsForSubject}.
|
|
73
|
+
*/
|
|
74
|
+
activeFactsBySubject(subjs: readonly string[], secondPred?: string): number[];
|
|
75
|
+
/** Active facts whose triple matches every given slot exactly (`ask`, exact layer). */
|
|
76
|
+
activeFactsMatchingExact(parts: TriplePatternParts): number[];
|
|
77
|
+
/** Same, with `=` relaxed to `LIKE %term%` (`ask`, recall-recovery layer). */
|
|
78
|
+
activeFactsMatchingLike(parts: TriplePatternParts): number[];
|
|
79
|
+
}
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type OpenedStore, type StoreSchema } from './store.js';
|
|
2
|
+
/** Knowledge DB schema: documents under `domain → source`, split into chunks. */
|
|
3
|
+
export declare const KNOWLEDGE_DDL: string[];
|
|
4
|
+
/**
|
|
5
|
+
* The external-content FTS5 table, created separately so the tokenizer can be
|
|
6
|
+
* chosen at open time (same DESIGN §16 mitigation as the memory store).
|
|
7
|
+
*/
|
|
8
|
+
export declare function chunksFtsTableDdl(tokenizer: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Ordered schema steps. Step 1 is the base schema and MUST stay idempotent: a database
|
|
11
|
+
* created before this mechanism existed reports `user_version = 0` while already holding it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const KNOWLEDGE_SCHEMA: StoreSchema;
|
|
14
|
+
/** Open (or create) the knowledge DB, bringing its schema up to the current version. */
|
|
15
|
+
export declare function openKnowledgeDb(path: string): OpenedStore['db'];
|
|
16
|
+
/** Open and report which migrations ran (tests, and any explicit upgrade path). */
|
|
17
|
+
export declare function openKnowledgeStore(path: string): OpenedStore;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned schema migrations — the "upgrade" half of the store lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* The engine used to apply an idempotent `CREATE TABLE IF NOT EXISTS` blob on every open and
|
|
5
|
+
* call that "no migrations needed, a schema change ships by recreating the file". That
|
|
6
|
+
* stopped being true the first time a change had to fix data in place (the duplicate open
|
|
7
|
+
* contradiction rows that a partial unique index must not choke on), so the schema now has a
|
|
8
|
+
* version and ordered steps.
|
|
9
|
+
*
|
|
10
|
+
* Mechanics, deliberately boring:
|
|
11
|
+
* - the version lives in SQLite's own `PRAGMA user_version` (no side table needed to order
|
|
12
|
+
* steps), and every applied step is ALSO recorded in `schema_migrations` for audit;
|
|
13
|
+
* - each step runs in its own transaction together with its `user_version` bump, so a
|
|
14
|
+
* failure rolls the step back completely — a database is never "half upgraded";
|
|
15
|
+
* - step 1 is the base schema and stays idempotent, because databases created before this
|
|
16
|
+
* mechanism existed report `user_version = 0` while already holding the schema; adopting
|
|
17
|
+
* them is a no-op that just stamps the version.
|
|
18
|
+
*/
|
|
19
|
+
import type { Db } from './port.js';
|
|
20
|
+
import type { FtsTokenizer } from './tokenizer.js';
|
|
21
|
+
/** What a migration may need from its store (the FTS tokenizer is chosen before the DDL). */
|
|
22
|
+
interface MigrationContext {
|
|
23
|
+
readonly tokenizer: FtsTokenizer;
|
|
24
|
+
}
|
|
25
|
+
export interface Migration {
|
|
26
|
+
/** 1-based, contiguous, unique — see {@link validateMigrations}. */
|
|
27
|
+
readonly version: number;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
/** Must be idempotent enough to run against a database from the pre-versioning era. */
|
|
30
|
+
readonly up: (db: Db, ctx: MigrationContext) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface MigrationResult {
|
|
33
|
+
readonly from: number;
|
|
34
|
+
readonly to: number;
|
|
35
|
+
readonly applied: readonly number[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `ALTER TABLE … ADD COLUMN`, skipped when the column is already there.
|
|
39
|
+
*
|
|
40
|
+
* SQLite has no `ADD COLUMN IF NOT EXISTS`, and every step must stay runnable against a
|
|
41
|
+
* database from the pre-versioning era (`user_version = 0` with the schema already present),
|
|
42
|
+
* so the check is explicit rather than implied by the version number. New columns should ALSO
|
|
43
|
+
* be added to the base DDL: a fresh database then gets them from step 1 and this becomes a
|
|
44
|
+
* no-op, while an existing one is upgraded here.
|
|
45
|
+
*/
|
|
46
|
+
export declare function addColumnIfMissing(db: Db, table: string, column: string, columnDdl: string): void;
|
|
47
|
+
/** The schema version stored in the database header (0 = pre-versioning). */
|
|
48
|
+
export declare function readUserVersion(db: Db): number;
|
|
49
|
+
/**
|
|
50
|
+
* Steps must be 1..N with no gaps: a database at version k then always has a defined path
|
|
51
|
+
* forward, and a mistaken edit (duplicate or skipped number) fails at startup instead of
|
|
52
|
+
* silently leaving a store behind.
|
|
53
|
+
*/
|
|
54
|
+
export declare function validateMigrations(migrations: readonly Migration[]): void;
|
|
55
|
+
/**
|
|
56
|
+
* The database was written by a NEWER build than the one opening it.
|
|
57
|
+
*
|
|
58
|
+
* Migrations are one-way (each step's `up` fixes data in place, and there is no `down`), so an
|
|
59
|
+
* older build cannot know what the extra steps did: running it against such a schema is undefined
|
|
60
|
+
* behaviour dressed up as a normal open — it may read columns that moved, or write rows the newer
|
|
61
|
+
* schema constrains differently. Refusing is the only honest answer, and the message has to say
|
|
62
|
+
* what to do, because this is what a plugin downgrade looks like in practice.
|
|
63
|
+
*/
|
|
64
|
+
export declare class SchemaDowngradeError extends Error {
|
|
65
|
+
readonly databaseVersion: number;
|
|
66
|
+
readonly codeVersion: number;
|
|
67
|
+
constructor(databaseVersion: number, codeVersion: number);
|
|
68
|
+
}
|
|
69
|
+
/** Apply every migration newer than the stored version. Returns what ran. */
|
|
70
|
+
export declare function migrate(db: Db, migrations: readonly Migration[], ctx: MigrationContext): MigrationResult;
|
|
71
|
+
export {};
|