latticesql 4.0.1 → 4.2.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.
@@ -0,0 +1,202 @@
1
+ # Retrieval, query & data primitives (v4.1)
2
+
3
+ latticesql 4.1 turns the library into a measurable, production-grade retrieval and
4
+ data substrate. Everything here is **additive and opt-in** — absent the opt-in,
5
+ `query()` / `count()` / `search()` behave byte-identically to 4.0. Every primitive
6
+ ships with unit (`:memory:` SQLite) + integration (real Postgres) + dialect-parity
7
+ tests.
8
+
9
+ ## Measurable retrieval
10
+
11
+ ### `evaluateRetrieval(queries, retriever, opts?)`
12
+
13
+ Standard IR metrics over **any** ranked retriever — `(query) => rankedRowIds`, so
14
+ it grades semantic, full-text, hybrid, graph, or an external service.
15
+
16
+ ```ts
17
+ const summary = await db.evaluateRetrieval(
18
+ [{ query: 'budget', relevant: ['doc1', 'doc7'] }],
19
+ async (q) => (await db.search('docs', q, { topK: 10 })).map((r) => String(r.row.id)),
20
+ { k: 10, ks: [1, 5, 10] },
21
+ );
22
+ // summary.precisionAtK / recallAtK / mrr / ndcgAtK / map (+ perQuery, byK)
23
+ ```
24
+
25
+ `detectRetrievalRegressions(baseline, candidate, tolerance)` turns it into a CI
26
+ gate — a retrieval change that lowers any metric past tolerance fails the build.
27
+
28
+ > **v4.2 — the gate can actually fail.** The golden corpus is now ~20 docs with
29
+ > deliberate cross-topic lexical overlap, so the real `search()` scores
30
+ > good-but-imperfect; the committed baseline is **generated** by running the real
31
+ > search (`npm run eval:baseline`) and is sub-perfect (`mrr ≈ 0.92`,
32
+ > `ndcg@3 ≈ 0.94`), never hand-authored. `npm run eval:gate` evaluates the current
33
+ > `search()` against that baseline and exits non-zero on any metric dropping past
34
+ > tolerance; it runs as a required CI step, and a suite test asserts the baseline
35
+ > still has headroom (`mrr < 1`) so the gate can't silently go blind.
36
+
37
+ ### `lattice doctor` / `diagnoseRetrieval(opts?)`
38
+
39
+ Read-only health: per-table FTS + embedding coverage (soft-deleted rows excluded),
40
+ extension availability (FTS5, sqlite-vec, pgvector, pg_trgm), and severity-ranked
41
+ issues. `lattice doctor [--json]` exits non-zero on any error (deploy gate).
42
+
43
+ ### `benchmarkRetrieval(opts?)` / `checkSlos(report, slos)`
44
+
45
+ Reproducible p50/p95/p99 latency for filtered query, FTS, vector, and aggregate,
46
+ plus ingest throughput + peak memory — on both dialects, at a configurable scale
47
+ (`LATTICE_BENCH_ROWS/QUERIES/DIM`). Ships in the package so buyers reproduce the
48
+ numbers; wire `checkSlos` as a CI SLO gate.
49
+
50
+ > **v4.2 — honest vector timing + an advisory SLO gate.** A Postgres integration
51
+ > test runs the benchmark against a real pgvector cluster and asserts the harness
52
+ > built the **native index before** the vector timing loop
53
+ > (`report.vectorIndexed === true`), so `vector.p95` reflects the indexed path,
54
+ > not the O(n) in-process scan; where pgvector is unavailable the test skips with a
55
+ > clear message rather than passing green-by-construction. `npm run slo:gate` runs
56
+ > the real benchmark at a committed scale and checks observed p95 latencies against
57
+ > committed thresholds — it is **advisory, never build-blocking** (shared CI
58
+ > runners are too latency-noisy to gate a merge on), and the output marks whether
59
+ > `vector.p95` reflects a native index or the in-process scan.
60
+
61
+ ## Better search
62
+
63
+ ### Chunked + contextual embeddings
64
+
65
+ ```ts
66
+ db.define('docs', {
67
+ columns: { id: 'TEXT PRIMARY KEY', title: 'TEXT', body: 'TEXT' },
68
+ embeddings: {
69
+ fields: ['title', 'body'],
70
+ embed: myEmbedder,
71
+ chunker: semanticChunker({ maxChars: 1000, overlap: 100 }),
72
+ contextPrefix: (row) => String(row.title), // prepended to every chunk
73
+ modelId: 'text-embedding-3-small',
74
+ },
75
+ });
76
+ ```
77
+
78
+ Each row is embedded as several boundary-aware chunks → higher precision@k and
79
+ fewer tokens to a correct answer. `search()` returns the best-matching chunk
80
+ (`chunkIndex` + `matchedContent`), excludes soft-deleted rows, and throws
81
+ `EmbeddingDimensionMismatchError` if the model dimension changed without a re-embed.
82
+ `refreshEmbeddings(table, opts)` backfills missing / re-embeds stale / sweeps orphans.
83
+
84
+ ### Indexed vector search
85
+
86
+ ```ts
87
+ await db.buildVectorIndex('docs'); // pgvector HNSW (PG) / sqlite-vec (SQLite)
88
+ ```
89
+
90
+ Opt-in per-table approximate-nearest-neighbor index built from the stored vectors;
91
+ `search()` uses it automatically when present, else the in-process scan (which
92
+ `doctor` reports). Requires the extension server-side (pgvector) or loaded
93
+ (sqlite-vec).
94
+
95
+ > **v4.2 — bounded retrieval reads.** `search()` / `hybridSearch()` clamp the
96
+ > caller's `topK` (`clampTopK`, `SEARCH_TOPK_MAX = 1000`) **before** the indexed
97
+ > arm over-fetches `topK * N` candidates, so a single large `topK` can't fan out
98
+ > into a whole-table read. For a table with **no** native index, the in-process
99
+ > cosine scan can be capped per-table with `embeddings.maxScanChunks`: when the
100
+ > scan would read more than that many stored chunk vectors it throws
101
+ > `EmbeddingScanTooLargeError` (telling you to add a pgvector index or raise the
102
+ > cap) rather than load them all into memory. It is **off by default** (unbounded
103
+ > scan — the historical behavior) and is **never silently truncated**, because a
104
+ > partial cosine scan would return incomplete, wrong results.
105
+
106
+ ### Hybrid search + ranking + reranker
107
+
108
+ ```ts
109
+ const results = await db.hybridSearch('docs', 'q4 budget', {
110
+ topK: 10,
111
+ ranking: {
112
+ recency: { column: 'created_at', halfLifeDays: 30, weight: 1 },
113
+ reward: { weight: 0.5 },
114
+ },
115
+ reranker: myCrossEncoder, // optional; graceful fallback on failure
116
+ });
117
+ // each result carries .explain { rrf, vectorRank/Score, ftsRank/Score, rankingBoost, rerankerScore }
118
+ ```
119
+
120
+ Reciprocal Rank Fusion (k=60) of the vector + full-text arms. `lattice search
121
+ "<q>" --table <t> --explain` shows the score breakdown. Full-text is now
122
+ relevance-ranked (`ts_rank` / `bm25`).
123
+
124
+ ### Graph-augmented retrieval
125
+
126
+ ```ts
127
+ await db.addEdge({ srcTable: 'docs', srcId: 'a', dstTable: 'docs', dstId: 'b', type: 'cites' });
128
+ await db.extractEdges({ srcTable: 'docs', fkColumn: 'parent_id', dstTable: 'docs' }); // zero-LLM
129
+ const results = await db.graphSearch('docs', 'q', { anchors: [{ table: 'docs', id: 'a' }] });
130
+ ```
131
+
132
+ A typed-edge graph (`__lattice_edges`) with bounded BFS (`traverseGraph`, depth ≤ 5,
133
+ node-capped) and adjacency boosting — relationship-aware retrieval that lifts rows
134
+ connected to your current-context entities.
135
+
136
+ ## Query primitives
137
+
138
+ ```ts
139
+ // Bounded reads — guard against unbounded full-table loads
140
+ await db.query('t', { maxRows: 1000 }); // throws BoundedReadError if more match
141
+ new Lattice(path, { defaultMaxRows: 1000 }); // global default
142
+
143
+ // Projection — return only the columns you need
144
+ await db.query('t', { projection: ['id', 'name'] });
145
+
146
+ // OR/AND groups + jsonPath
147
+ await db.query('t', {
148
+ filters: [
149
+ { col: 'status', op: 'eq', val: 'open' },
150
+ {
151
+ or: [
152
+ { col: 'priority', op: 'gte', val: 3 },
153
+ { col: 'pinned', op: 'eq', val: true },
154
+ ],
155
+ },
156
+ { col: 'meta', jsonPath: 'tier', op: 'eq', val: 'gold' },
157
+ ],
158
+ });
159
+
160
+ // SQL-side aggregation
161
+ await db.aggregate('orders', {
162
+ groupBy: ['status'],
163
+ aggregates: [
164
+ { fn: 'count', as: 'n' },
165
+ { fn: 'sum', col: 'total', as: 'revenue' },
166
+ ],
167
+ having: [{ aggregate: 'n', op: 'gt', val: 10 }],
168
+ });
169
+
170
+ // Keyset pagination — fast arbitrarily deep
171
+ const page = await db.queryPage('t', { orderBy: 'created_at', limit: 50, cursor });
172
+
173
+ // distinctOn — one row per group; include — batched relation expansion
174
+ await db.query('events', { distinctOn: 'user_id', orderBy: 'ts', orderDir: 'desc' });
175
+ await db.query('posts', { include: ['author'] }); // belongsTo → row; hasMany → array
176
+ ```
177
+
178
+ ## Governance, reliability, computed columns, cloud files
179
+
180
+ ```ts
181
+ // Immutable provenance + trust gate
182
+ db.define('docs', { columns: {...}, provenance: true, trust: true });
183
+ await db.verifyRow('docs', id, 'alice'); // markRowForReview / rowsNeedingReview / verifiedRows
184
+
185
+ // Durable retry + online resumable migrations
186
+ await withRetry(() => db.insert(...)); // idempotent ops only
187
+ await applyChunkedMigration(db.adapter, { id, table, apply, batchSize: 1000 });
188
+
189
+ // Computed columns + materialized rollups
190
+ db.define('people', { columns: {...}, computed: {
191
+ full_name: { deps: ['first', 'last'], compute: (r) => `${r.first} ${r.last}` },
192
+ }});
193
+ db.define('posts', { columns: {...}, materializedRollups: {
194
+ comment_count: { sourceTable: 'comments', foreignKey: 'post_id', fn: 'count' },
195
+ }});
196
+
197
+ // Keyless cloud file-byte access (Postgres cloud)
198
+ await db.enableCloudFilePresigning({ bucket, region, accessKey, secretKey });
199
+ // members fetch bytes with zero config; the owner key never leaves the database.
200
+ ```
201
+
202
+ See [CHANGELOG.md](../CHANGELOG.md) for the full 4.1 list.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "4.0.1",
3
+ "version": "4.2.0",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,13 +31,16 @@
31
31
  "scripts": {
32
32
  "build": "tsup",
33
33
  "typecheck": "tsc --noEmit",
34
- "lint": "eslint src tests",
35
- "lint:fix": "eslint src tests --fix",
34
+ "lint": "eslint src tests scripts",
35
+ "lint:fix": "eslint src tests scripts --fix",
36
36
  "format": "prettier --write .",
37
37
  "format:check": "prettier --check .",
38
38
  "check:generic": "bash scripts/check-generic.sh",
39
39
  "test": "vitest run",
40
40
  "test:watch": "vitest",
41
+ "eval:baseline": "vite-node scripts/eval-baseline.ts",
42
+ "eval:gate": "vite-node scripts/eval-gate.ts",
43
+ "slo:gate": "vite-node scripts/slo-gate.ts",
41
44
  "test:coverage": "vitest run --coverage",
42
45
  "test:e2e": "playwright test",
43
46
  "docs": "typedoc --out docs-generated src/index.ts",
@@ -65,9 +68,12 @@
65
68
  },
66
69
  "optionalDependencies": {
67
70
  "@aws-sdk/client-s3": "^3.1067.0",
71
+ "exceljs": "^4.4.0",
68
72
  "pg": "^8.11.0",
73
+ "pgvector": "^0.2.0",
69
74
  "playwright": "^1.48.0",
70
- "sharp": "^0.33.5"
75
+ "sharp": "^0.33.5",
76
+ "sqlite-vec": "^0.1.6"
71
77
  },
72
78
  "devDependencies": {
73
79
  "@eslint/js": "^9.0.0",