@avantf/dsh-mem 0.6.4 → 0.8.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 +19 -10
- package/lib/client.js +1665 -3466
- package/lib/dsh-build.json +6 -5
- package/lib/engine/graph-recall/in-memory.d.ts +93 -0
- package/lib/engine/graph-recall/index.d.ts +22 -0
- package/lib/engine/graph-recall/traverse.d.ts +119 -0
- package/lib/engine/graph-recall/types.d.ts +188 -0
- package/lib/engine/mem/config/config_files.d.ts +1 -1
- package/lib/engine/mem/config/loader.d.ts +2 -3
- package/lib/engine/mem/config/paths.d.ts +2 -13
- package/lib/engine/mem/db/dao/contradictions.d.ts +4 -22
- package/lib/engine/mem/db/dao/entities.d.ts +80 -7
- package/lib/engine/mem/db/dao/fact_sources.d.ts +3 -13
- package/lib/engine/mem/db/dao/facts.d.ts +311 -56
- package/lib/engine/mem/db/dao/triples.d.ts +1 -26
- package/lib/engine/mem/db/graph.d.ts +249 -0
- package/lib/engine/mem/db/journal_notes.d.ts +35 -0
- package/lib/engine/mem/db/migrations.d.ts +9 -0
- package/lib/engine/mem/db/port.d.ts +22 -3
- package/lib/engine/mem/db/schema.d.ts +119 -0
- package/lib/engine/mem/db/tokenizer.d.ts +9 -0
- package/lib/engine/mem/dbdrivers/fake/driver.d.ts +75 -0
- package/lib/engine/mem/dbdrivers/fake/index.d.ts +7 -0
- package/lib/engine/mem/dbdrivers/fake/surface.d.ts +34 -0
- package/lib/engine/mem/dbdrivers/fake/transport.d.ts +61 -0
- package/lib/engine/mem/dbdrivers/sqlite/connection.d.ts +14 -0
- package/lib/engine/mem/dbdrivers/sqlite/contradictions.d.ts +35 -0
- package/lib/engine/mem/dbdrivers/sqlite/dao/index.d.ts +16 -0
- package/lib/engine/mem/dbdrivers/sqlite/driver.d.ts +54 -0
- package/lib/engine/mem/dbdrivers/sqlite/facts.d.ts +117 -0
- package/lib/engine/mem/dbdrivers/sqlite/index.d.ts +6 -0
- package/lib/engine/mem/dbdrivers/sqlite/migrations.d.ts +15 -0
- package/lib/engine/mem/dbdrivers/sqlite/relations.d.ts +62 -0
- package/lib/engine/mem/dbdrivers/sqlite/search.d.ts +56 -0
- package/lib/engine/mem/dbdrivers/sqlite/sources.d.ts +37 -0
- package/lib/engine/mem/defaults.d.ts +3 -4
- package/lib/engine/mem/dispatch.d.ts +7 -32
- package/lib/engine/mem/entities/extract.d.ts +44 -47
- package/lib/engine/mem/eval/derived_runner.d.ts +6 -1
- package/lib/engine/mem/eval/relation_gold.d.ts +251 -0
- package/lib/engine/mem/eval/write_metrics.d.ts +1 -1
- package/lib/engine/mem/facts_io.d.ts +328 -0
- package/lib/engine/mem/index.d.ts +10 -2
- package/lib/engine/mem/interfaces/capabilities.d.ts +30 -0
- package/lib/engine/mem/interfaces/common.d.ts +11 -0
- package/lib/engine/mem/interfaces/errors.d.ts +65 -0
- package/lib/engine/mem/interfaces/index.d.ts +18 -0
- package/lib/engine/mem/interfaces/migrations.d.ts +21 -0
- package/lib/engine/mem/interfaces/repositories.d.ts +471 -0
- package/lib/engine/mem/interfaces/search_index.d.ts +113 -0
- package/lib/engine/mem/interfaces/storage_backend.d.ts +84 -0
- package/lib/engine/mem/interfaces/storage_driver.d.ts +43 -0
- package/lib/engine/mem/interfaces/version.d.ts +34 -0
- package/lib/engine/mem/lifecycle/contradiction.d.ts +2 -2
- package/lib/engine/mem/maintenance_paths.d.ts +41 -0
- package/lib/engine/mem/modelBootstrap.d.ts +1 -1
- package/lib/engine/mem/relations/confidence.d.ts +39 -0
- package/lib/engine/mem/relations/extract.d.ts +128 -0
- package/lib/engine/mem/relations/index.d.ts +32 -0
- package/lib/engine/mem/relations/name-quality.d.ts +78 -0
- package/lib/engine/mem/relations/normalize.d.ts +275 -0
- package/lib/engine/mem/relations/patterns.d.ts +67 -0
- package/lib/engine/mem/relations/statement.d.ts +88 -0
- package/lib/engine/mem/relations/types.d.ts +79 -0
- package/lib/engine/mem/runtime.d.ts +117 -90
- package/lib/engine/mem/storage/create.d.ts +42 -0
- package/lib/engine/mem/storage/errors.d.ts +65 -0
- package/lib/engine/mem/storage/index.d.ts +15 -0
- package/lib/engine/mem/storage/ports.d.ts +472 -0
- package/lib/engine/mem/storage/registry.d.ts +37 -0
- package/lib/engine/mem/storage/sqlite.d.ts +28 -0
- package/lib/engine/mem/store/common.d.ts +1 -1
- package/lib/engine/mem/store/entity_leg.d.ts +59 -2
- package/lib/engine/mem/store/entity_sweep.d.ts +4 -3
- package/lib/engine/mem/store/explicit_relations.d.ts +52 -0
- package/lib/engine/mem/store/facts_restore.d.ts +124 -0
- package/lib/engine/mem/store/floors.d.ts +4 -1
- package/lib/engine/mem/store/graph_reader.d.ts +146 -0
- package/lib/engine/mem/store/hybrid.d.ts +50 -13
- package/lib/engine/mem/store/kinds.d.ts +47 -0
- package/lib/engine/mem/store/legs.d.ts +24 -29
- package/lib/engine/mem/store/lexical.d.ts +36 -45
- package/lib/engine/mem/store/memory.d.ts +486 -82
- package/lib/engine/mem/store/recall_policy.d.ts +49 -0
- package/lib/engine/mem/store/relation_backfill.d.ts +210 -0
- package/lib/engine/mem/store/relation_planner.d.ts +191 -0
- package/lib/engine/mem/store/secret_guard.d.ts +0 -1
- package/lib/engine/mem/store/self_query.d.ts +4 -2
- package/lib/engine/mem/store/time_filter.d.ts +121 -0
- package/lib/engine/mem/store/titles.d.ts +52 -0
- package/lib/engine/mem/store/vector_repair.d.ts +44 -54
- package/lib/engine/mem-contract/config.d.ts +44 -38
- package/lib/engine/mem-contract/text.d.ts +1 -1
- package/lib/engine/mem-contract/tools.d.ts +184 -117
- package/lib/engine/mem-contract/types.d.ts +364 -288
- package/lib/index.js +13898 -11500
- package/lib/interface-version.json +1 -1
- package/lib/types/client/index.d.ts +2 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paging.d.ts +3 -12
- package/lib/types/client/paging.d.ts.map +1 -1
- package/lib/types/client/summarize.d.ts.map +1 -1
- package/lib/types/command.d.ts +61 -0
- package/lib/types/command.d.ts.map +1 -0
- package/lib/types/envinit-bootstrap.d.ts +1 -1
- package/lib/types/envinit.d.ts +2 -6
- package/lib/types/envinit.d.ts.map +1 -1
- package/lib/types/hints.d.ts +28 -24
- package/lib/types/hints.d.ts.map +1 -1
- package/lib/types/index.d.ts +21 -40
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/inject.d.ts +3 -2
- package/lib/types/inject.d.ts.map +1 -1
- package/lib/types/interface_gate.d.ts +9 -9
- package/lib/types/prompt.d.ts +17 -41
- package/lib/types/prompt.d.ts.map +1 -1
- package/lib/types/provision.d.ts +3 -0
- package/lib/types/provision.d.ts.map +1 -1
- package/lib/types/remote.d.ts +23 -9
- package/lib/types/remote.d.ts.map +1 -1
- package/lib/types/render.d.ts +3 -3
- package/lib/types/tool_schema.d.ts +4 -0
- package/lib/types/tool_schema.d.ts.map +1 -1
- package/lib/types/vectorMigration.d.ts +5 -7
- package/lib/types/vectorMigration.d.ts.map +1 -1
- package/package.json +16 -15
- package/lib/engine/mem/config/domains.d.ts +0 -20
- package/lib/engine/mem/db/dao/chunks.d.ts +0 -231
- package/lib/engine/mem/db/dao/documents.d.ts +0 -51
- package/lib/engine/mem/db/hash.d.ts +0 -2
- package/lib/engine/mem/db/knowledge.d.ts +0 -17
- package/lib/engine/mem/eval/index.d.ts +0 -4
- package/lib/engine/mem/git.d.ts +0 -73
- package/lib/engine/mem/router.d.ts +0 -51
- package/lib/engine/mem/store/doc_files.d.ts +0 -125
- package/lib/engine/mem/store/document_text.d.ts +0 -47
- package/lib/engine/mem/store/ingest_guard.d.ts +0 -25
- package/lib/engine/mem/store/knowledge.d.ts +0 -598
- package/lib/engine/mem/store/source_picker.d.ts +0 -43
- package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +0 -52
- package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +0 -10
- package/lib/engine/mem-retrieval/rerank.d.ts +0 -13
- package/lib/types/client/domains.d.ts +0 -53
- package/lib/types/client/domains.d.ts.map +0 -1
- package/lib/types/client/ingest.d.ts +0 -24
- package/lib/types/client/ingest.d.ts.map +0 -1
- package/lib/types/open.d.ts +0 -77
- package/lib/types/open.d.ts.map +0 -1
- package/lib/types/reconcile.d.ts +0 -64
- package/lib/types/reconcile.d.ts.map +0 -1
|
@@ -6,8 +6,48 @@
|
|
|
6
6
|
* would exceed SQLite's bind-parameter cap and, on the write path, silently disable
|
|
7
7
|
* contradiction detection (see `db/chunk.ts`).
|
|
8
8
|
*/
|
|
9
|
+
import type { MemoryKind } from '../../../mem-contract/index.js';
|
|
9
10
|
import type { Db } from '../port.js';
|
|
10
11
|
import type { ExtractedEntity } from '../../entities/extract.js';
|
|
12
|
+
import type { TimeSql } from '../../store/time_filter.js';
|
|
13
|
+
import { type SurfaceMatch } from '../../store/relation_planner.js';
|
|
14
|
+
import type { FactContainingPage, FactsContainingAllQuery } from '../../interfaces/repositories.js';
|
|
15
|
+
/**
|
|
16
|
+
* The two vocabulary scans behind {@link EntitiesDao.factEntitiesIn}, as constants because the
|
|
17
|
+
* benchmark (`scripts/bench-facts.mjs`) EXPLAINs them: their `EXISTS` subquery is DRIVEN BY THE
|
|
18
|
+
* PLANNER'S STATISTICS, so a timing without the plan that produced it is not comparable. With no
|
|
19
|
+
* `sqlite_stat1` SQLite drives the EXISTS from `facts` by `status` (every ACTIVE fact visited once
|
|
20
|
+
* per matched name); with full statistics it seeks `idx_fact_entities_entity` instead. The same
|
|
21
|
+
* arrangement as `facts.ts#FTS_SEARCH_SQL`, and for the same reason: the measured SQL must be the
|
|
22
|
+
* text production runs, not a copy that can drift.
|
|
23
|
+
*/
|
|
24
|
+
export declare const FACT_ENTITIES_IN_SQL = "SELECT n.name AS name\n FROM entities n\n WHERE instr(?, n.name) > 0\n AND EXISTS (\n SELECT 1\n FROM fact_entities fe\n JOIN facts fa ON fa.fact_id = fe.fact_id\n WHERE fe.entity_id = n.entity_id AND fa.status = 'active'\n )";
|
|
25
|
+
/** The alias half of {@link FACT_ENTITIES_IN_SQL} (aliases first, see `factEntitiesIn`). */
|
|
26
|
+
export declare const FACT_ENTITY_ALIASES_IN_SQL = "SELECT a.alias AS alias, n.name AS name\n FROM entity_aliases a\n JOIN entities n ON n.entity_id = a.entity_id\n WHERE instr(?, a.alias) > 0\n AND EXISTS (\n SELECT 1\n FROM fact_entities fe\n JOIN facts fa ON fa.fact_id = fe.fact_id\n WHERE fe.entity_id = a.entity_id AND fa.status = 'active'\n )";
|
|
27
|
+
/** One statement plus its positional binds, exactly as production runs it. */
|
|
28
|
+
export interface FactAndStatement {
|
|
29
|
+
sql: string;
|
|
30
|
+
params: unknown[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The two statements one fact-level AND page runs — the `total` count and the page itself — as text
|
|
34
|
+
* plus binds, built from the RESOLVED ids.
|
|
35
|
+
*
|
|
36
|
+
* A function (not a constant like {@link FACT_ENTITIES_IN_SQL}) because the placeholder count, the
|
|
37
|
+
* time clause and limit/offset are per-call, and EXPORTED for the same reason as that constant: the
|
|
38
|
+
* benchmark (`scripts/bench-facts.mjs`) EXPLAINs the statements production runs, so the recorded plan
|
|
39
|
+
* cannot drift from the timed query. The AND page's plan is statistics-dependent too — a timing
|
|
40
|
+
* without the plan that produced it is not comparable.
|
|
41
|
+
*
|
|
42
|
+
* Bind order is the text's order: entity ids, kind ×2, source ×2, time params, the AND cardinality,
|
|
43
|
+
* then (page only) limit/offset.
|
|
44
|
+
*/
|
|
45
|
+
export declare function factsContainingAllStatements(db: Db, query: Omit<FactsContainingAllQuery, 'time'> & {
|
|
46
|
+
time?: TimeSql;
|
|
47
|
+
}): {
|
|
48
|
+
total: FactAndStatement;
|
|
49
|
+
page: FactAndStatement;
|
|
50
|
+
} | null;
|
|
11
51
|
export declare class EntitiesDao {
|
|
12
52
|
private readonly db;
|
|
13
53
|
constructor(db: Db);
|
|
@@ -61,11 +101,6 @@ export declare class EntitiesDao {
|
|
|
61
101
|
* whole corpus. The hint makes the plan independent of ANALYZE timing.
|
|
62
102
|
*/
|
|
63
103
|
candidateFacts(factId: number, minShared: number, maxEntities: number): number[];
|
|
64
|
-
/** Active facts linked to `name`, optionally limited to one category (`related` leg). */
|
|
65
|
-
activeFactsForEntity(name: string, category?: string): number[];
|
|
66
|
-
/** Active facts linked to ALL of `names` (AND-join, `reason` leg). */
|
|
67
|
-
activeFactsForAllEntities(names: readonly string[]): number[];
|
|
68
|
-
/** Active facts sharing ANY of `names`, in one category (the entity leg). */
|
|
69
104
|
/**
|
|
70
105
|
* Candidate facts for an entity query, ordered by the leg's score and CAPPED.
|
|
71
106
|
*
|
|
@@ -82,10 +117,48 @@ export declare class EntitiesDao {
|
|
|
82
117
|
* deterministic tie-break among equal ratios. The caller still recomputes the score in JS for the
|
|
83
118
|
* survivors, so one formula stays in charge.
|
|
84
119
|
*/
|
|
85
|
-
candidateFactsForAnyEntity(names: readonly string[],
|
|
120
|
+
candidateFactsForAnyEntity(names: readonly string[], kind: MemoryKind | undefined, limit: number, queryWidth: number, widthCap: number,
|
|
86
121
|
/**
|
|
87
122
|
* P-08 provenance filter (`fact_sources.ref`). `EXISTS`, never a JOIN: a fact with several
|
|
88
123
|
* sources would fan out into several rows and eat the leg's `LIMIT`, silently shrinking recall.
|
|
89
124
|
*/
|
|
90
|
-
source?: string
|
|
125
|
+
source?: string,
|
|
126
|
+
/**
|
|
127
|
+
* The explicit `mem_recall.time` predicate (see `store/time_filter.ts`). It is part of the
|
|
128
|
+
* statement rather than a post-fusion filter for the same reason as P-08's `source`: the leg's
|
|
129
|
+
* `LIMIT` must apply WITHIN the window, or window-external facts eat the cap and the in-window
|
|
130
|
+
* ones never reach fusion.
|
|
131
|
+
*/
|
|
132
|
+
time?: TimeSql): number[];
|
|
133
|
+
/**
|
|
134
|
+
* The FACT-level entity vocabulary for the query text (docs/GRAPH_RECALL_AND_SEMANTICS.md §7.9).
|
|
135
|
+
*
|
|
136
|
+
* Canonical names and registered aliases are BOTH reported (alias-first is what `resolveNode` and
|
|
137
|
+
* `relations/normalize.ts` do, so an alias and its canonical form are one entity to the planner),
|
|
138
|
+
* and both are narrowed to entities linked to at least one ACTIVE fact. A name whose facts are all
|
|
139
|
+
* archived is not part of the live corpus: reporting it would form a plan that can only come back
|
|
140
|
+
* empty, which is the fuzzy fallback with extra work.
|
|
141
|
+
*
|
|
142
|
+
* NOT the graph reader's `entitiesIn`: that one requires an `edges` row, so a journal entry (which
|
|
143
|
+
* never has one) or a note whose extraction produced no relation would be invisible here — exactly
|
|
144
|
+
* the facts a fact-level AND must cover. The `kind`/`source`/`time` filters are deliberately not
|
|
145
|
+
* applied (the caller's page applies them, and an empty page falls back to fuzzy).
|
|
146
|
+
*/
|
|
147
|
+
factEntitiesIn(text: string): SurfaceMatch[];
|
|
148
|
+
/**
|
|
149
|
+
* One fact-level AND page (docs/GRAPH_RECALL_AND_SEMANTICS.md §7.9): ACTIVE facts that mention
|
|
150
|
+
* EVERY requested entity.
|
|
151
|
+
*
|
|
152
|
+
* The entity names are resolved to ids alias-first (`resolveNode`'s order), deduplicated, and an
|
|
153
|
+
* UNKNOWN name ends the query with an empty page: "all of these entities appear" cannot hold when
|
|
154
|
+
* one of them does not exist. The grouping is the AND itself — `COUNT(DISTINCT fe.entity_id)`
|
|
155
|
+
* equals the number of distinct entities — so a fact carrying some of them is not a candidate,
|
|
156
|
+
* unlike the entity leg's `minSupport`.
|
|
157
|
+
*
|
|
158
|
+
* `ORDER BY fact_id DESC` is the exact page's own convention (newest row first), and
|
|
159
|
+
* `total`/`offset`/`limit` are the FACT-level page, not an edge page.
|
|
160
|
+
*/
|
|
161
|
+
factsContainingAll(query: Omit<FactsContainingAllQuery, 'time'> & {
|
|
162
|
+
time?: TimeSql;
|
|
163
|
+
}): FactContainingPage;
|
|
91
164
|
}
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fact_sources` — where a memory fact came from (P-08).
|
|
3
|
-
*
|
|
4
|
-
* One row per `(fact_id, kind, ref)`. `kind` is a closed vocabulary (`session` / `kb_doc` /
|
|
5
|
-
* `tool` / `manual`) so a reader can trust what a reference IS; `ref` is opaque to this layer
|
|
6
|
-
* (a session id, a document id, a tool name, or a free-form note).
|
|
7
|
-
*
|
|
8
|
-
* The SQL lives here like every other aggregate. The store decides WHICH source a write carries
|
|
9
|
-
* (and refuses to invent one when the caller gave none); the DAO only records and reads rows.
|
|
10
|
-
*/
|
|
11
|
-
import type { FactSourceView } from '../../../mem-contract/index.js';
|
|
12
1
|
import type { Db } from '../port.js';
|
|
13
2
|
/**
|
|
14
3
|
* The kind axis is the CONTRACT's, not a second list: `FactSourceView['kind']` is what `admin detail`
|
|
@@ -16,7 +5,8 @@ import type { Db } from '../port.js';
|
|
|
16
5
|
* vocabulary" one thing. (Before, the array here and the union in `types.ts` were two independent
|
|
17
6
|
* spellings that happened to agree.)
|
|
18
7
|
*/
|
|
19
|
-
export type FactSourceKind
|
|
8
|
+
export type { FactSourceKind } from '../../interfaces/repositories.js';
|
|
9
|
+
import type { FactSourceKind } from '../../interfaces/repositories.js';
|
|
20
10
|
/**
|
|
21
11
|
* The closed `kind` vocabulary, spelled once so the store and the tests share it.
|
|
22
12
|
*
|
|
@@ -42,7 +32,7 @@ export interface FactSource {
|
|
|
42
32
|
* `kind:` prefix — `session:…` / `kb_doc:…` / `tool:…` — and anything else, INCLUDING the fallback
|
|
43
33
|
* kind's own name, is `manual` with the reference kept VERBATIM. No shape guessing: a bare
|
|
44
34
|
* `domain:source:title` becomes a `manual` source whose ref is exactly what the caller wrote, which
|
|
45
|
-
* is strictly more honest than inventing a
|
|
35
|
+
* is strictly more honest than inventing a provenance kind from how the string looks.
|
|
46
36
|
*
|
|
47
37
|
* The prefix table comes from {@link FACT_SOURCE_KINDS} (see {@link FACT_SOURCE_PREFIX_KINDS}) rather
|
|
48
38
|
* than from a hand-written alternation, which is what keeps the recognized set and the recorded set
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* the store ends up holding business rules only and no SQL. See DESIGN §19.
|
|
7
7
|
*/
|
|
8
8
|
import type { Db } from '../port.js';
|
|
9
|
+
import { type TimeRange, type TimeSql } from '../../store/time_filter.js';
|
|
10
|
+
import type { MemoryKind } from '../../../mem-contract/index.js';
|
|
9
11
|
/**
|
|
10
12
|
* Every `facts` column EXCEPT the two BLOBs (`hrr_vector` ~8 KB, `semantic_vector` ~2 KB per row)
|
|
11
13
|
* and the two DEPRECATED provenance columns (`mirror_source` / `mirror_target`, see below).
|
|
@@ -26,39 +28,8 @@ import type { Db } from '../port.js';
|
|
|
26
28
|
* `test/db_lifecycle.spec.ts` asserts this list against `PRAGMA table_info(facts)` MINUS those
|
|
27
29
|
* two deprecations, so a new column fails a test instead of silently vanishing from these reads.
|
|
28
30
|
*/
|
|
29
|
-
export declare const FACT_COLUMNS_NO_BLOB = "fact_id, content,
|
|
30
|
-
|
|
31
|
-
* The FTS leg's exact SQL, exported so the plan guard (`test/fts_plan.spec.ts`) and the bench
|
|
32
|
-
* control group EXPLAIN the production text itself instead of a copy that can silently drift.
|
|
33
|
-
*
|
|
34
|
-
* `NOT INDEXED` on the `facts` join is LOAD-BEARING, not decoration — this query is the one place
|
|
35
|
-
* where a healthy-looking covering index (`idx_facts_status_category (status, category)`) is the
|
|
36
|
-
* WRONG drive. Without planner statistics SQLite cannot know that `MATCH` is the selective access
|
|
37
|
-
* path, so it drives from `facts` and RE-RUNS the whole external-content MATCH once per row:
|
|
38
|
-
*
|
|
39
|
-
* bad : SEARCH fa USING COVERING INDEX idx_facts_status_category (status=?)
|
|
40
|
-
* | SCAN f VIRTUAL TABLE INDEX 0:=M1 <- the inner `=M1`
|
|
41
|
-
* good: SCAN f VIRTUAL TABLE INDEX 0:M1
|
|
42
|
-
* | SEARCH fa USING INTEGER PRIMARY KEY (rowid=?)
|
|
43
|
-
*
|
|
44
|
-
* Measured on the synthetic corpus at n=1000 (selective query, ~10 hits) in the fresh-session
|
|
45
|
-
* shape: 574 ms → 4.6 ms for the leg; the hint's `LIMIT 1` shape is the SAME statement, 568 ms
|
|
46
|
-
* → 1.9 ms.
|
|
47
|
-
*
|
|
48
|
-
* Why the missing statistics are not a startup-only blip: the open tick's `PRAGMA optimize` writes
|
|
49
|
-
* no stats for an EMPTY database, so on a fresh install the whole first session runs without them
|
|
50
|
-
* while the corpus grows in it; the next tick (≤60 min heartbeat) or a restart is what writes them.
|
|
51
|
-
* `bench-memory` used to hide this because it seeds, then REOPENS the runtime (whose open tick
|
|
52
|
-
* analyzes). Hence "ANALYZE facts at open when `sqlite_stat1` lacks a `facts` row and `facts` has
|
|
53
|
-
* rows" does NOT close the window either: at open the fresh database has zero rows (the guard is
|
|
54
|
-
* false), and once rows exist the next optimize is up to an hour away. `NOT INDEXED` pins the
|
|
55
|
-
* FTS-driven plan independently of `sqlite_stat1`, so there is no window at all. It cannot change
|
|
56
|
-
* results — only the join order — because the rowid join to `facts` is an INTEGER PRIMARY KEY
|
|
57
|
-
* lookup (still available under `NOT INDEXED`), and `status`/`category` stay residual filters on
|
|
58
|
-
* that one joined row, exactly as in the good plan above. Same spirit as §19 `page()`'s
|
|
59
|
-
* `INDEXED BY`, which pins the plan against `PRAGMA optimize` flipping it back.
|
|
60
|
-
*/
|
|
61
|
-
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 ?";
|
|
31
|
+
export declare const FACT_COLUMNS_NO_BLOB = "fact_id, content, kind, title, 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 valid_from, valid_to, valid_from_ms, valid_to_ms, assert_count, created_at, updated_at";
|
|
32
|
+
export declare const FTS_SEARCH_SQL: string;
|
|
62
33
|
/**
|
|
63
34
|
* {@link FTS_SEARCH_SQL} with the P-08 source predicate, as a SEPARATE statement rather than one
|
|
64
35
|
* more `(? IS NULL OR …)` branch in the shared text.
|
|
@@ -73,7 +44,106 @@ export declare const FTS_SEARCH_SQL = "SELECT f.rowid AS id, bm25(facts_fts) AS
|
|
|
73
44
|
* rows, and this leg is `ORDER BY bm25 … LIMIT cap` — the duplicates would eat cap slots and shrink
|
|
74
45
|
* recall behind the caller's back.
|
|
75
46
|
*/
|
|
76
|
-
export declare const FTS_SEARCH_SQL_FILTERED
|
|
47
|
+
export declare const FTS_SEARCH_SQL_FILTERED: string;
|
|
48
|
+
/**
|
|
49
|
+
* One row of a TITLE search (`mem_recall.titles`), as the DAO returns it.
|
|
50
|
+
*
|
|
51
|
+
* `title` is non-null here by construction (`title IS NOT NULL`); the store maps the row onto the
|
|
52
|
+
* contract's `TitleCandidate`, which is where `kind` is renamed to `memory_kind` and `valid_from` to
|
|
53
|
+
* `event_date`. `excerpt` is the content's first 200 characters, computed in SQL so a candidate page
|
|
54
|
+
* never drags whole documents through the driver.
|
|
55
|
+
*
|
|
56
|
+
* `matched_title` is the title text the query actually matched — the CURRENT title, or one of the
|
|
57
|
+
* aliases a corrected transaction used to carry (v17). It equals `title` unless the row was found
|
|
58
|
+
* through an old handle, which is exactly what the caller needs to know before it decides "same
|
|
59
|
+
* transaction or not".
|
|
60
|
+
*/
|
|
61
|
+
export interface TitleRow {
|
|
62
|
+
fact_id: number;
|
|
63
|
+
title: string;
|
|
64
|
+
matched_title: string;
|
|
65
|
+
kind: string;
|
|
66
|
+
status: string;
|
|
67
|
+
created_at: string | null;
|
|
68
|
+
updated_at: string | null;
|
|
69
|
+
valid_from: string | null;
|
|
70
|
+
excerpt: string | null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The filters BOTH title statements apply — one shape, so the FTS path and the LIKE fallback cannot
|
|
74
|
+
* drift into two meanings of `status`/`kind`/`time`.
|
|
75
|
+
*
|
|
76
|
+
* `status: 'all'` is the one value that adds no predicate (it is what `titles`'s documented
|
|
77
|
+
* "include the old revisions" means); `kind` undefined means no layer restriction — the STORE applies
|
|
78
|
+
* the action's `journal` default, because that is a product decision, not a SQL one.
|
|
79
|
+
*/
|
|
80
|
+
export interface TitleFilters {
|
|
81
|
+
kind?: MemoryKind;
|
|
82
|
+
status: 'active' | 'archived' | 'all';
|
|
83
|
+
time?: TimeSql;
|
|
84
|
+
}
|
|
85
|
+
export interface TitleQuery extends TitleFilters {
|
|
86
|
+
/** The normalized query, used for the exact/prefix comparison. */
|
|
87
|
+
query: string;
|
|
88
|
+
/** The query's keywords, used by `matched_terms` and by the LIKE fallback. */
|
|
89
|
+
terms: readonly string[];
|
|
90
|
+
limit: number;
|
|
91
|
+
}
|
|
92
|
+
/** The exact-title filter: the same `kind`/`status`/`time` axis, one literal query. */
|
|
93
|
+
export interface ExactTitleQuery extends TitleFilters {
|
|
94
|
+
query: string;
|
|
95
|
+
}
|
|
96
|
+
/** The exact-content filter (docs/RECALL_INTERFACE.md §2.2): one literal substring, active rows only. */
|
|
97
|
+
export interface ExactContentQuery {
|
|
98
|
+
/** The normalized literal a fact's content must CONTAIN. */
|
|
99
|
+
query: string;
|
|
100
|
+
kind?: MemoryKind;
|
|
101
|
+
source?: string;
|
|
102
|
+
time?: TimeSql;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* One SQL text WITH its bind values — the ONLY way the title statements leave this module.
|
|
106
|
+
*
|
|
107
|
+
* The caller used to bind `rank.params`, the MATCH expression, the filter values and the LIMIT by
|
|
108
|
+
* hand, which meant the parameter order was a comment instead of a fact (REVIEW-2026-10-24 P2). The
|
|
109
|
+
* builders below assemble the values in the order the `?` placeholders appear in the text they
|
|
110
|
+
* produce, so a statement and its params cannot come from two different shapes.
|
|
111
|
+
*/
|
|
112
|
+
export interface TitleStatement {
|
|
113
|
+
readonly sql: string;
|
|
114
|
+
readonly params: (string | number | null)[];
|
|
115
|
+
}
|
|
116
|
+
/** The status/kind/time predicate text plus its bind values, built by {@link titleFilter}. */
|
|
117
|
+
export interface TitleFilterSql {
|
|
118
|
+
readonly sql: string;
|
|
119
|
+
readonly params: (string | number | null)[];
|
|
120
|
+
}
|
|
121
|
+
/** What the fuzzy title statements need from their caller (everything but the SQL itself). */
|
|
122
|
+
export interface TitleFuzzyInput {
|
|
123
|
+
/** The normalized query (the `=`/prefix comparison). */
|
|
124
|
+
readonly query: string;
|
|
125
|
+
/** The query's keywords (the LIKE predicate and `matched_terms`). */
|
|
126
|
+
readonly terms: readonly string[];
|
|
127
|
+
/** The status/kind/time predicate, over the `fa.` alias both statements use for `facts`. */
|
|
128
|
+
readonly filter: TitleFilterSql;
|
|
129
|
+
readonly limit: number;
|
|
130
|
+
}
|
|
131
|
+
/** {@link TitleFuzzyInput} plus the FTS5 MATCH expression from `store/titles.ts#titleFtsQuery`. */
|
|
132
|
+
export interface TitleFtsSearchInput extends TitleFuzzyInput {
|
|
133
|
+
readonly fts: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The FTS title statement, exported so the plan guard (`test/journal_title.spec.ts`) EXPLAINs the
|
|
137
|
+
* production text (and binds the production params) instead of a copy — the same arrangement
|
|
138
|
+
* {@link FTS_SEARCH_SQL} uses.
|
|
139
|
+
*/
|
|
140
|
+
export declare function titleFtsSearchSql(input: TitleFtsSearchInput): TitleStatement;
|
|
141
|
+
/**
|
|
142
|
+
* The short-query fallback: a `title LIKE` scan instead of the FTS index, plus the same alias
|
|
143
|
+
* source — a corrected chain's 1–2 character old handle has to stay findable too, and the trigram
|
|
144
|
+
* table cannot express a run that short.
|
|
145
|
+
*/
|
|
146
|
+
export declare function titleLikeSearchSql(input: TitleFuzzyInput): TitleStatement;
|
|
77
147
|
export declare class FactsDao {
|
|
78
148
|
private readonly db;
|
|
79
149
|
constructor(db: Db);
|
|
@@ -110,12 +180,19 @@ export declare class FactsDao {
|
|
|
110
180
|
rowsByIds(ids: readonly number[]): Record<string, unknown>[];
|
|
111
181
|
/** `fact_id → content` for a hit set (never N+1). */
|
|
112
182
|
textsByIds(ids: readonly number[]): Map<number, string>;
|
|
113
|
-
/**
|
|
114
|
-
|
|
183
|
+
/**
|
|
184
|
+
* `fact_id →` the scalar fields a `RecallHit` renders (timestamps, validity, title) for a hit set.
|
|
185
|
+
*
|
|
186
|
+
* Renamed from `timesByIds` when `title` joined the projection: the read is the hit's META, not
|
|
187
|
+
* only its clocks. One statement for the whole hit set — never N+1 — and the title arrives here
|
|
188
|
+
* rather than in a second query because it is on the same row.
|
|
189
|
+
*/
|
|
190
|
+
hitMetaByIds(ids: readonly number[]): Map<number, {
|
|
115
191
|
created_at: string;
|
|
116
192
|
updated_at: string | null;
|
|
117
193
|
valid_from: string | null;
|
|
118
194
|
valid_to: string | null;
|
|
195
|
+
title: string | null;
|
|
119
196
|
}>;
|
|
120
197
|
/** Active rows that carry a persisted vector (index classification). */
|
|
121
198
|
activeVectorRows(): {
|
|
@@ -162,7 +239,7 @@ export declare class FactsDao {
|
|
|
162
239
|
*/
|
|
163
240
|
insertRevision(v: {
|
|
164
241
|
content: string;
|
|
165
|
-
|
|
242
|
+
kind: MemoryKind;
|
|
166
243
|
ttlDays: number;
|
|
167
244
|
supersedesId: number | null;
|
|
168
245
|
hrr: Uint8Array;
|
|
@@ -172,6 +249,12 @@ export declare class FactsDao {
|
|
|
172
249
|
pinnedAt: string | null;
|
|
173
250
|
windowAt: string;
|
|
174
251
|
createdAt: string;
|
|
252
|
+
/**
|
|
253
|
+
* The journal title of this revision (JOURNAL_TITLE_SEARCH §2.2). `null` = none (a note, a
|
|
254
|
+
* legacy journal, a CLI/import write) — and that is why the column is nullable. The FTS index
|
|
255
|
+
* follows through the `facts_title_ai` trigger; this statement never touches it by hand.
|
|
256
|
+
*/
|
|
257
|
+
title?: string | null;
|
|
175
258
|
/**
|
|
176
259
|
* P-13: the caller's event time / known end, written straight onto the new row. `null` means
|
|
177
260
|
* "unknown" and is what an omitted argument leaves (the column has no default on purpose).
|
|
@@ -180,6 +263,15 @@ export declare class FactsDao {
|
|
|
180
263
|
*/
|
|
181
264
|
validFrom?: string | null;
|
|
182
265
|
validTo?: string | null;
|
|
266
|
+
/**
|
|
267
|
+
* The INSTANTS the two text bounds name (`valid_from_ms`/`valid_to_ms`).
|
|
268
|
+
*
|
|
269
|
+
* Written in the same statement as the text, never computed later: the text is what a reader
|
|
270
|
+
* displays and the epoch is what an `event` window compares, so writing them apart would let the
|
|
271
|
+
* two describe different times. `null` = "no event time" (the axis then excludes the row, D5).
|
|
272
|
+
*/
|
|
273
|
+
validFromMs?: number | null;
|
|
274
|
+
validToMs?: number | null;
|
|
183
275
|
}): {
|
|
184
276
|
changes: number;
|
|
185
277
|
lastInsertRowid: number | bigint;
|
|
@@ -187,9 +279,15 @@ export declare class FactsDao {
|
|
|
187
279
|
/**
|
|
188
280
|
* P-10: land an explicitly given validity on a row that already exists (a duplicate `add` whose
|
|
189
281
|
* caller named `event_date` / `valid_until`). Omitted arguments keep what the row has, exactly
|
|
190
|
-
* like `
|
|
282
|
+
* like `kind`/`ttl_days` (see `add`'s doc).
|
|
191
283
|
*/
|
|
192
|
-
applyValidity(factId: number, validFrom:
|
|
284
|
+
applyValidity(factId: number, validFrom: {
|
|
285
|
+
text: string;
|
|
286
|
+
ms: number;
|
|
287
|
+
} | undefined, validTo: {
|
|
288
|
+
text: string;
|
|
289
|
+
ms: number;
|
|
290
|
+
} | undefined): void;
|
|
193
291
|
/** P-10: the row's assertion counter (`1` when the column is somehow NULL on a legacy row). */
|
|
194
292
|
assertCount(factId: number): number;
|
|
195
293
|
/**
|
|
@@ -206,7 +304,7 @@ export declare class FactsDao {
|
|
|
206
304
|
* contradiction verdict). `COALESCE` keeps an end the caller already declared (`valid_until`):
|
|
207
305
|
* overwriting it with the archival instant would replace a real answer with an administrative one.
|
|
208
306
|
*/
|
|
209
|
-
markValidTo(factId: number, at: string): void;
|
|
307
|
+
markValidTo(factId: number, at: string, atMs?: number | null): void;
|
|
210
308
|
/**
|
|
211
309
|
* P-07: "who superseded this fact", derived from the existing `supersedes_id` link — no
|
|
212
310
|
* redundant column. `MIN` is deterministic when several revisions named the same predecessor
|
|
@@ -240,7 +338,7 @@ export declare class FactsDao {
|
|
|
240
338
|
* `EXISTS`, never a JOIN on `fact_sources` (the {@link FTS_SEARCH_SQL_FILTERED} rule): the cap is
|
|
241
339
|
* `LIMIT`, and a multi-source fact would eat cap slots with duplicate rows.
|
|
242
340
|
*/
|
|
243
|
-
windowRows(fromDate: string, toDate: string,
|
|
341
|
+
windowRows(fromDate: string, toDate: string, kind: MemoryKind | undefined, cap: number, source?: string): number[];
|
|
244
342
|
findByContent(content: string): Record<string, unknown> | undefined;
|
|
245
343
|
/**
|
|
246
344
|
* ACTIVE facts whose entity/triple rows were produced by OLDER extraction rules.
|
|
@@ -254,10 +352,15 @@ export declare class FactsDao {
|
|
|
254
352
|
* sorted every matching row in a temp b-tree before applying the LIMIT — i.e. the budget
|
|
255
353
|
* bounded what was rebuilt but not what was read. This order also drains the oldest vintage
|
|
256
354
|
* first, which is the more useful one when several rule changes are pending.
|
|
355
|
+
*
|
|
356
|
+
* `kind` rides along because the sweep's edge write depends on it (STRUCTURED_RELATIONS §四: a
|
|
357
|
+
* journal gets no graph edges). Reading it here costs nothing — the row is already being decoded,
|
|
358
|
+
* and a per-fact lookup would be one more query per sweep item.
|
|
257
359
|
*/
|
|
258
360
|
staleEntityRows(version: number, limit: number): {
|
|
259
361
|
fact_id: number;
|
|
260
362
|
content: string;
|
|
363
|
+
kind: string;
|
|
261
364
|
}[];
|
|
262
365
|
countStaleEntities(version: number): number;
|
|
263
366
|
/** Stamp the rules that produced a batch of rows' entity/triple data. */
|
|
@@ -316,9 +419,47 @@ export declare class FactsDao {
|
|
|
316
419
|
*/
|
|
317
420
|
requeueConflictCheck(ids: readonly number[]): number;
|
|
318
421
|
/** Revive an archived row (R18/R19: refresh the idle clock, clear the archive bookkeeping). */
|
|
319
|
-
revive(factId: number,
|
|
320
|
-
/** Land explicit
|
|
321
|
-
applyValues(factId: number,
|
|
422
|
+
revive(factId: number, kind: MemoryKind, ttlDays: number, trust: number, clock: number): number;
|
|
423
|
+
/** Land explicit kind/TTL on a row that already exists (pure duplicate). */
|
|
424
|
+
applyValues(factId: number, kind: MemoryKind, ttlDays: number): void;
|
|
425
|
+
/**
|
|
426
|
+
* Land a title on a row that already exists — the ONE write behind every "backfill" path
|
|
427
|
+
* (JOURNAL_TITLE_SEARCH §11.2/§11.3): a duplicate `add` that names a title for an existing
|
|
428
|
+
* untitled row, and an update that merges into another row whose title is NULL.
|
|
429
|
+
*
|
|
430
|
+
* It MUST be an `UPDATE`, not a second `INSERT OR IGNORE`: the insert trigger only fires for a row
|
|
431
|
+
* that is actually inserted, so a backfill written that way would leave `facts_title_fts` holding
|
|
432
|
+
* the old (empty) title forever. `AFTER UPDATE OF title` is what moves the index here.
|
|
433
|
+
*/
|
|
434
|
+
setTitle(factId: number, title: string): void;
|
|
435
|
+
/**
|
|
436
|
+
* Record `title` as a handle `factId` USED to carry (v17, the controlled `retitle: true`).
|
|
437
|
+
*
|
|
438
|
+
* `INSERT OR IGNORE` because the alias is derived search state, not a log: a correction that lands
|
|
439
|
+
* on the same old handle twice (retitle A→B, retitle B→A, retitle A→B) must not fail on the
|
|
440
|
+
* `(fact_id, title)` primary key. The row belongs to the revision that NOW carries the title, which
|
|
441
|
+
* is what makes the old search find the ACTIVE revision rather than an archived predecessor.
|
|
442
|
+
*/
|
|
443
|
+
insertTitleAlias(factId: number, title: string): void;
|
|
444
|
+
/**
|
|
445
|
+
* Drop an alias row that has become the fact's CURRENT title (v17).
|
|
446
|
+
*
|
|
447
|
+
* Reached by a correction that goes BACK to an earlier handle (A → B, then B → A): the alias table
|
|
448
|
+
* exists to hold handles the row does not carry any more, and a row repeating the current title
|
|
449
|
+
* would be a second source of truth for the same string. `INSERT OR IGNORE` alone cannot express
|
|
450
|
+
* that (it adds, never removes), hence the explicit delete.
|
|
451
|
+
*/
|
|
452
|
+
dropTitleAlias(factId: number, title: string): void;
|
|
453
|
+
/**
|
|
454
|
+
* Carry a revision's aliases onto the revision that replaces it (v17).
|
|
455
|
+
*
|
|
456
|
+
* Aliases are chain-level knowledge — "this transaction was once called X" — stored per revision
|
|
457
|
+
* because that is the row a search joins. Without this copy, the second update after a correction
|
|
458
|
+
* would move the title onto a new row and strand every alias on the archived predecessor, so the
|
|
459
|
+
* old handle would stop finding the live transaction. `currentTitle` is excluded so the table never
|
|
460
|
+
* holds a row that merely repeats the current handle.
|
|
461
|
+
*/
|
|
462
|
+
copyTitleAliases(fromFactId: number, toFactId: number, currentTitle: string): void;
|
|
322
463
|
statusOf(factId: number): string | undefined;
|
|
323
464
|
/**
|
|
324
465
|
* Land a (re-)encoded vector, and RE-QUEUE the fact for the conflict check.
|
|
@@ -359,20 +500,35 @@ export declare class FactsDao {
|
|
|
359
500
|
* One page, newest first (the `list` action).
|
|
360
501
|
*
|
|
361
502
|
* TWO shapes, deliberately, because they want different indexes and the shared
|
|
362
|
-
* `(? IS NULL OR
|
|
503
|
+
* `(? IS NULL OR kind = ?)` form made the planner sort EVERY active row for the unfiltered
|
|
363
504
|
* one (measured 124 ms at 33k facts against 2.1 ms with an index that carries the order):
|
|
364
505
|
*
|
|
365
506
|
* - unfiltered: pinned to `idx_facts_created(status, created_at DESC)`. `INDEXED BY` is
|
|
366
507
|
* load-bearing, not decoration — the lifecycle tick runs `PRAGMA optimize`, and once
|
|
367
508
|
* `sqlite_stat1` exists the planner can switch this query back to `SCAN facts` + a temp
|
|
368
509
|
* b-tree (verified at 33k rows), silently undoing the fix at exactly the scale it matters.
|
|
369
|
-
* -
|
|
370
|
-
* only that
|
|
371
|
-
* index in order and filter per row (measured 9.8 ms for a
|
|
510
|
+
* - kind-filtered: left to the planner, which seeks `idx_facts_status_kind` and sorts
|
|
511
|
+
* only that kind's rows. Pinning the created-at index here would instead walk the whole
|
|
512
|
+
* index in order and filter per row (measured 9.8 ms for a kind with 17 rows at 33k),
|
|
372
513
|
* i.e. it would trade a fast path for a slow one to save an index we do not need.
|
|
373
514
|
*/
|
|
374
|
-
page(status: string,
|
|
375
|
-
|
|
515
|
+
page(status: string, kind: MemoryKind | undefined, limit: number, offset: number, source?: string): Record<string, unknown>[];
|
|
516
|
+
/**
|
|
517
|
+
* Run one of `page()`'s INDEXED BY-pinned statements, degrading to the unpinned form when the
|
|
518
|
+
* pinned index is missing.
|
|
519
|
+
*
|
|
520
|
+
* The `INDEXED BY` pins the PLAN, not the RESULT: without the hint the same rows come back, the
|
|
521
|
+
* planner just chooses for itself. `idx_facts_created` comes from migration 4 / the base DDL, and
|
|
522
|
+
* both create it idempotently — but a database whose migration was interrupted (or whose index was
|
|
523
|
+
* dropped out of band) at an already-current `user_version` has no pending step left to recreate
|
|
524
|
+
* it, and without this fallback the WHOLE list path errors out on "no such index". Degrading is
|
|
525
|
+
* preferred over an open-time self-check/rebuild: the migrations are the open path's only repair
|
|
526
|
+
* mechanism (each step idempotent), a missing index hurts exactly ONE query's plan choice, and
|
|
527
|
+
* rebuilding is a DDL write the open path should not take on for it. Only the "no such index"
|
|
528
|
+
* failure degrades — every other error still propagates.
|
|
529
|
+
*/
|
|
530
|
+
private pageByCreatedIndex;
|
|
531
|
+
countInStatus(status: string, kind?: MemoryKind, source?: string): number;
|
|
376
532
|
/** Every ACTIVE fact (the report's `active`). */
|
|
377
533
|
countActive(): number;
|
|
378
534
|
/** ACTIVE and permanent — pinned facts are exempt from decay (the report's `pinned`). */
|
|
@@ -432,7 +588,7 @@ export declare class FactsDao {
|
|
|
432
588
|
* facts with a cap of 200 it silently made 32.8k of them unreachable. Recency is the honest
|
|
433
589
|
* prior for a memory store and `idx_facts_created(status, created_at DESC)` already serves it.
|
|
434
590
|
*/
|
|
435
|
-
activeHrrRows(
|
|
591
|
+
activeHrrRows(kind?: MemoryKind, limit?: number, source?: string, time?: TimeSql): {
|
|
436
592
|
fact_id: number;
|
|
437
593
|
hrr_vector: Uint8Array;
|
|
438
594
|
}[];
|
|
@@ -444,12 +600,12 @@ export declare class FactsDao {
|
|
|
444
600
|
* which is the same information the HRR bundle encodes (its atoms ARE the entity names), so the
|
|
445
601
|
* narrowing removes the O(N) part rather than a meaningful part of the ranking.
|
|
446
602
|
*/
|
|
447
|
-
hrrRowsForFacts(ids: readonly number[],
|
|
603
|
+
hrrRowsForFacts(ids: readonly number[], kind?: MemoryKind, source?: string): {
|
|
448
604
|
fact_id: number;
|
|
449
605
|
hrr_vector: Uint8Array;
|
|
450
606
|
}[];
|
|
451
|
-
/** ACTIVE subset of `ids` in one
|
|
452
|
-
activeIdsIn(ids: readonly number[],
|
|
607
|
+
/** ACTIVE subset of `ids` in one kind/source (`null` = any) — the semantic leg's filter. */
|
|
608
|
+
activeIdsIn(ids: readonly number[], kind?: MemoryKind, source?: string, time?: TimeSql): number[];
|
|
453
609
|
/** `retrieval_count` + the "last used" clock (spec §2.5), batched. */
|
|
454
610
|
touchUsage(ids: readonly number[]): void;
|
|
455
611
|
/** Recall bonus not granted: only the settled trust moved. */
|
|
@@ -464,7 +620,7 @@ export declare class FactsDao {
|
|
|
464
620
|
* that JOIN from becoming the drive (see {@link FTS_SEARCH_SQL}, which carries the plans and
|
|
465
621
|
* measurements).
|
|
466
622
|
*/
|
|
467
|
-
ftsSearch(ftsQuery: string,
|
|
623
|
+
ftsSearch(ftsQuery: string, kind?: MemoryKind, limit?: number, source?: string, time?: TimeSql): {
|
|
468
624
|
id: number;
|
|
469
625
|
rank: number;
|
|
470
626
|
}[];
|
|
@@ -479,10 +635,109 @@ export declare class FactsDao {
|
|
|
479
635
|
* single-term query (all ranks 1) has a deterministic order. `ORDER BY rank` must read the SELECT
|
|
480
636
|
* alias, so the LIKE expressions are built once and bound twice — same fragment, same order.
|
|
481
637
|
*/
|
|
482
|
-
ftsSubstringSearch(terms: readonly string[],
|
|
638
|
+
ftsSubstringSearch(terms: readonly string[], kind?: MemoryKind, limit?: number, source?: string, time?: TimeSql): {
|
|
483
639
|
id: number;
|
|
484
640
|
rank: number;
|
|
485
641
|
}[];
|
|
642
|
+
/**
|
|
643
|
+
* One statement's worth of candidates, from the TITLE FTS index (JOURNAL_TITLE_SEARCH §11.4).
|
|
644
|
+
*
|
|
645
|
+
* The FTS table only PRODUCES candidates: `bm25` is the last, tie-breaking key, and the three keys
|
|
646
|
+
* above it are computed on `facts.title` itself (exact match, prefix, how many of the query's
|
|
647
|
+
* keywords the title contains). That split is the design's whole point — a title's bm25 score is
|
|
648
|
+
* not a content-relevance score, and the ordering the caller acts on ("is this the same
|
|
649
|
+
* transaction?") is about the title text, not about a corpus-wide term weight.
|
|
650
|
+
*
|
|
651
|
+
* `NOT INDEXED` on the `facts` join is the same load-bearing hint as {@link FTS_SEARCH_SQL}: without
|
|
652
|
+
* statistics the planner can drive from `facts` and re-run the external-content MATCH per row.
|
|
653
|
+
* `fa.title IS NOT NULL` keeps the empty-string rows FTS5 indexes for NULL out of the answer.
|
|
654
|
+
*
|
|
655
|
+
* Since v17 the same statement also consults `title_aliases` (see {@link titleAliasSource}): a
|
|
656
|
+
* corrected transaction must not become unfindable by the handle it used to carry, so the candidate
|
|
657
|
+
* set is the union of "the current title matches" and "an alias matches", deduped with the current
|
|
658
|
+
* title winning, and `matched_title` says which text the query actually hit.
|
|
659
|
+
*/
|
|
660
|
+
searchTitleRowsByFts(q: TitleQuery & {
|
|
661
|
+
fts: string;
|
|
662
|
+
}): TitleRow[];
|
|
663
|
+
/**
|
|
664
|
+
* The SHORT-QUERY fallback: a `title LIKE` scan over the same filters and the same ordering.
|
|
665
|
+
*
|
|
666
|
+
* Reached when the FTS table's tokenizer cannot express the query (a 1–2 character CJK run, a
|
|
667
|
+
* 1–2 character latin word, or a unicode61 table — see `store/titles.ts`). The predicate matches
|
|
668
|
+
* ANY keyword (not the whole query contiguously), exactly like the content leg's substring
|
|
669
|
+
* fallback; `matched_terms` is what ranks a row containing all keywords above one containing a
|
|
670
|
+
* single one. The alias source rides along for the same reason it does in the FTS statement.
|
|
671
|
+
*/
|
|
672
|
+
searchTitleRowsByLike(q: TitleQuery): TitleRow[];
|
|
673
|
+
/**
|
|
674
|
+
* The WHERE body of the content-exact statements: active facts whose content CONTAINS the query.
|
|
675
|
+
*
|
|
676
|
+
* `lower()` is SQLite's — ASCII-only case folding. Latin queries are therefore case-insensitive
|
|
677
|
+
* (`DSH` finds `dsh`) while CJK is compared as-is, which is exactly the rule; a non-ASCII accented
|
|
678
|
+
* letter stays case-sensitive, and that is stated in the spec's "拉丁部分大小写不敏感".
|
|
679
|
+
*/
|
|
680
|
+
/**
|
|
681
|
+
* How many active facts contain `q.query` (the exact page's `total`).
|
|
682
|
+
*/
|
|
683
|
+
countContentContaining(q: ExactContentQuery): number;
|
|
684
|
+
/** One page of active facts containing `q.query`, newest row first. */
|
|
685
|
+
contentContainingRows(q: ExactContentQuery & {
|
|
686
|
+
limit: number;
|
|
687
|
+
offset: number;
|
|
688
|
+
}): number[];
|
|
689
|
+
/**
|
|
690
|
+
* How many rows carry exactly `q.query` as their title — OR as one of their aliases (v17), under
|
|
691
|
+
* the same filters as the fuzzy path.
|
|
692
|
+
*
|
|
693
|
+
* Including the alias set here is what makes an old handle answer the EXACT stage instead of
|
|
694
|
+
* falling through to the fuzzy shortlist: "the title of this transaction used to be X" is exactly
|
|
695
|
+
* the same question as "a transaction titled X", and the caller cannot tell the two apart.
|
|
696
|
+
*/
|
|
697
|
+
countTitleEqual(q: ExactTitleQuery): number;
|
|
698
|
+
/**
|
|
699
|
+
* One page of title-equal rows.
|
|
700
|
+
*
|
|
701
|
+
* `=`, not `LIKE`: the contract is "title EQUALS the query" (case-sensitive, because title is an
|
|
702
|
+
* immutable handle), so no wildcard and no folding — `fa.title = ?` under SQLite's BINARY collation
|
|
703
|
+
* is exactly that comparison. An ALIAS that equals the query counts too (v17); `matched_title`
|
|
704
|
+
* carries the text that matched, while `title` stays the row's current handle.
|
|
705
|
+
*
|
|
706
|
+
* Bind order (the statement's, not a rule of thumb): the SELECT's `matched_title` expression first
|
|
707
|
+
* (query, query), then the filter, then the WHERE's equality pair (query, query), then the page.
|
|
708
|
+
*/
|
|
709
|
+
titleEqualRows(q: ExactTitleQuery & {
|
|
710
|
+
limit: number;
|
|
711
|
+
offset: number;
|
|
712
|
+
}): TitleRow[];
|
|
713
|
+
/**
|
|
714
|
+
* How many ACTIVE facts fall inside the window (the `mode:"time"` envelope's `total`).
|
|
715
|
+
*/
|
|
716
|
+
countTimeWindow(range: TimeRange, opts?: {
|
|
717
|
+
kind?: MemoryKind;
|
|
718
|
+
source?: string;
|
|
719
|
+
}): number;
|
|
720
|
+
/**
|
|
721
|
+
* One page of that window, newest first along the axis the caller filtered on.
|
|
722
|
+
*
|
|
723
|
+
* The order follows the SAME axis as the WHERE (one mapping in {@link timeAxisColumn}), so the
|
|
724
|
+
* first row is the newest one the window contains; `fact_id ASC` is the deterministic tie-break
|
|
725
|
+
* among equal instants (an import can produce several).
|
|
726
|
+
*/
|
|
727
|
+
timeWindowRows(range: TimeRange, opts: {
|
|
728
|
+
kind?: MemoryKind;
|
|
729
|
+
source?: string;
|
|
730
|
+
limit: number;
|
|
731
|
+
offset: number;
|
|
732
|
+
}): number[];
|
|
733
|
+
/**
|
|
734
|
+
* `mem_admin list` narrowed by a window: the same page shape and order as {@link page}, plus the
|
|
735
|
+
* caller's time predicate. Separate from `page` because that method pins two different indexes by
|
|
736
|
+
* hand and a window is exactly the shape where the indexed range should drive instead.
|
|
737
|
+
*/
|
|
738
|
+
pageTimeWindow(status: string, kind: MemoryKind | undefined, range: TimeRange, limit: number, offset: number, source?: string): Record<string, unknown>[];
|
|
739
|
+
/** How many rows `pageTimeWindow` would page through (the `admin list` `total`). */
|
|
740
|
+
countStatusTimeWindow(status: string, kind: MemoryKind | undefined, range: TimeRange, source?: string): number;
|
|
486
741
|
/**
|
|
487
742
|
* ①'s predicate: the active-day counter crossed an integer boundary since the last settle.
|
|
488
743
|
* Kept next to its four siblings so the tick's ORDER and the SQL stay in one place.
|