@avantf/dsh-mem 0.3.0 → 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.
Files changed (145) hide show
  1. package/README.md +89 -28
  2. package/cordis.patch.yml +17 -0
  3. package/lib/client.js +155 -14
  4. package/lib/dsh-build.json +5 -5
  5. package/lib/engine/mem/config/config_files.d.ts +26 -0
  6. package/lib/engine/mem/config/domains.d.ts +20 -0
  7. package/lib/engine/mem/config/loader.d.ts +35 -0
  8. package/lib/engine/mem/config/paths.d.ts +53 -0
  9. package/lib/engine/mem/db/chunk.d.ts +26 -0
  10. package/lib/engine/mem/db/conn.d.ts +13 -0
  11. package/lib/engine/mem/db/dao/chunks.d.ts +150 -0
  12. package/lib/engine/mem/db/dao/contradictions.d.ts +98 -0
  13. package/lib/engine/mem/db/dao/documents.d.ts +51 -0
  14. package/lib/engine/mem/db/dao/entities.d.ts +67 -0
  15. package/lib/engine/mem/db/dao/facts.d.ts +472 -0
  16. package/lib/engine/mem/db/dao/stats.d.ts +14 -0
  17. package/lib/engine/mem/db/dao/triples.d.ts +80 -0
  18. package/lib/engine/mem/db/hash.d.ts +2 -0
  19. package/lib/engine/mem/db/knowledge.d.ts +17 -0
  20. package/lib/engine/mem/db/migrations.d.ts +71 -0
  21. package/lib/engine/mem/db/port.d.ts +57 -0
  22. package/lib/engine/mem/db/schema.d.ts +26 -0
  23. package/lib/engine/mem/db/sqlite.d.ts +89 -0
  24. package/lib/engine/mem/db/store.d.ts +35 -0
  25. package/lib/engine/mem/db/tokenizer.d.ts +61 -0
  26. package/lib/engine/mem/db/vectors.d.ts +50 -0
  27. package/lib/engine/mem/dispatch.d.ts +51 -0
  28. package/lib/engine/mem/entities/extract.d.ts +84 -0
  29. package/lib/engine/mem/eval/loader.d.ts +19 -0
  30. package/lib/engine/mem/eval/metrics.d.ts +25 -0
  31. package/lib/engine/mem/eval/runner.d.ts +10 -0
  32. package/lib/engine/mem/eval/write_metrics.d.ts +88 -0
  33. package/lib/engine/mem/git.d.ts +73 -0
  34. package/lib/engine/mem/hrr/algebra.d.ts +30 -0
  35. package/lib/engine/mem/hrr/atoms.d.ts +22 -0
  36. package/lib/engine/mem/hrr/encode.d.ts +32 -0
  37. package/lib/engine/mem/hrr/index.d.ts +3 -0
  38. package/lib/engine/mem/index.d.ts +27 -0
  39. package/lib/engine/mem/lifecycle/contradiction.d.ts +156 -0
  40. package/lib/engine/mem/lifecycle/index.d.ts +5 -0
  41. package/lib/engine/mem/lifecycle/maintenance.d.ts +48 -0
  42. package/lib/engine/mem/lifecycle/presence.d.ts +29 -0
  43. package/lib/engine/mem/lifecycle/tick.d.ts +43 -0
  44. package/lib/engine/mem/lifecycle/trust.d.ts +90 -0
  45. package/lib/engine/mem/modelBootstrap.d.ts +207 -0
  46. package/lib/engine/mem/router.d.ts +51 -0
  47. package/lib/engine/mem/runtime.d.ts +232 -0
  48. package/lib/engine/mem/store/common.d.ts +82 -0
  49. package/lib/engine/mem/store/doc_files.d.ts +125 -0
  50. package/lib/engine/mem/store/document_text.d.ts +47 -0
  51. package/lib/engine/mem/store/floors.d.ts +191 -0
  52. package/lib/engine/mem/store/hybrid.d.ts +232 -0
  53. package/lib/engine/mem/store/ingest_guard.d.ts +25 -0
  54. package/lib/engine/mem/store/knowledge.d.ts +483 -0
  55. package/lib/engine/mem/store/lexical.d.ts +67 -0
  56. package/lib/engine/mem/store/memory.d.ts +543 -0
  57. package/lib/engine/mem/store/source_picker.d.ts +43 -0
  58. package/lib/engine/mem-contract/config.d.ts +129 -0
  59. package/lib/engine/mem-contract/env.d.ts +31 -0
  60. package/lib/engine/mem-contract/family.d.ts +25 -0
  61. package/lib/engine/mem-contract/index.d.ts +8 -0
  62. package/lib/engine/mem-contract/log.d.ts +29 -0
  63. package/lib/engine/mem-contract/remote.d.ts +80 -0
  64. package/lib/engine/mem-contract/text.d.ts +92 -0
  65. package/lib/engine/mem-contract/tools.d.ts +284 -0
  66. package/lib/engine/mem-contract/types.d.ts +615 -0
  67. package/lib/engine/mem-retrieval/adapters/auto_vstore.d.ts +47 -0
  68. package/lib/engine/mem-retrieval/adapters/hnswlib.d.ts +174 -0
  69. package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +121 -0
  70. package/lib/engine/mem-retrieval/adapters/local_numpy.d.ts +28 -0
  71. package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +52 -0
  72. package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +10 -0
  73. package/lib/engine/mem-retrieval/adapters/warm_gate.d.ts +46 -0
  74. package/lib/engine/mem-retrieval/budget.d.ts +30 -0
  75. package/lib/engine/mem-retrieval/fusion.d.ts +53 -0
  76. package/lib/engine/mem-retrieval/index.d.ts +14 -0
  77. package/lib/engine/mem-retrieval/interfaces.d.ts +82 -0
  78. package/lib/engine/mem-retrieval/log.d.ts +11 -0
  79. package/lib/engine/mem-retrieval/registry.d.ts +23 -0
  80. package/lib/engine/mem-retrieval/rerank.d.ts +13 -0
  81. package/lib/engine/mem-retrieval/stats.d.ts +64 -0
  82. package/lib/engine/mem-retrieval/text_budget.d.ts +52 -0
  83. package/lib/index.js +1773 -473
  84. package/lib/interface-version.json +2 -2
  85. package/lib/types/client/domains.d.ts +5 -4
  86. package/lib/types/client/domains.d.ts.map +1 -1
  87. package/lib/types/client/index.d.ts.map +1 -1
  88. package/lib/types/client/ingest.d.ts +1 -1
  89. package/lib/types/client/wire.d.ts +36 -0
  90. package/lib/types/client/wire.d.ts.map +1 -0
  91. package/lib/types/envinit-bootstrap.d.ts +3 -2
  92. package/lib/types/envinit.d.ts +2 -19
  93. package/lib/types/envinit.d.ts.map +1 -1
  94. package/lib/types/hints.d.ts +1 -1
  95. package/lib/types/index.d.ts +8 -3
  96. package/lib/types/index.d.ts.map +1 -1
  97. package/lib/types/open.d.ts +61 -11
  98. package/lib/types/open.d.ts.map +1 -1
  99. package/lib/types/prompt.d.ts +19 -1
  100. package/lib/types/prompt.d.ts.map +1 -1
  101. package/lib/types/provision.d.ts +1 -1
  102. package/lib/types/provision.d.ts.map +1 -1
  103. package/lib/types/reconcile.d.ts +64 -0
  104. package/lib/types/reconcile.d.ts.map +1 -0
  105. package/lib/types/remote.d.ts +53 -0
  106. package/lib/types/remote.d.ts.map +1 -1
  107. package/lib/types/render.d.ts +35 -0
  108. package/lib/types/render.d.ts.map +1 -0
  109. package/lib/types/tool_schema.d.ts +3 -13
  110. package/lib/types/tool_schema.d.ts.map +1 -1
  111. package/package.json +27 -18
  112. package/lib/client.js.map +0 -1
  113. package/lib/types/client/domains.js +0 -25
  114. package/lib/types/client/domains.js.map +0 -1
  115. package/lib/types/client/index.js +0 -1371
  116. package/lib/types/client/index.js.map +0 -1
  117. package/lib/types/client/ingest.js +0 -30
  118. package/lib/types/client/ingest.js.map +0 -1
  119. package/lib/types/client/paging.js +0 -36
  120. package/lib/types/client/paging.js.map +0 -1
  121. package/lib/types/client/summarize.js +0 -79
  122. package/lib/types/client/summarize.js.map +0 -1
  123. package/lib/types/data_home.js +0 -26
  124. package/lib/types/data_home.js.map +0 -1
  125. package/lib/types/envinit-bootstrap.js +0 -333
  126. package/lib/types/envinit.js +0 -402
  127. package/lib/types/envinit.js.map +0 -1
  128. package/lib/types/hints.js +0 -53
  129. package/lib/types/hints.js.map +0 -1
  130. package/lib/types/index.js +0 -755
  131. package/lib/types/index.js.map +0 -1
  132. package/lib/types/inject.js +0 -14
  133. package/lib/types/inject.js.map +0 -1
  134. package/lib/types/interface_gate.js +0 -84
  135. package/lib/types/interface_gate.js.map +0 -1
  136. package/lib/types/open.js +0 -133
  137. package/lib/types/open.js.map +0 -1
  138. package/lib/types/prompt.js +0 -148
  139. package/lib/types/prompt.js.map +0 -1
  140. package/lib/types/provision.js +0 -177
  141. package/lib/types/provision.js.map +0 -1
  142. package/lib/types/remote.js +0 -247
  143. package/lib/types/remote.js.map +0 -1
  144. package/lib/types/tool_schema.js +0 -149
  145. 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,2 @@
1
+ /** Stable content digest (sha1: change detection, not security). */
2
+ export declare function contentHash(text: string): string;
@@ -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 {};