latticesql 1.2.5 → 1.3.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 CHANGED
@@ -18,6 +18,8 @@ Every AI agent session starts cold — no memory of what happened yesterday, wha
18
18
  2. **Watches** for DB changes and re-renders automatically
19
19
  3. **Ingests** agent-written output back into the DB via the writeback pipeline
20
20
  4. **Manages** state with full CRUD, natural-key operations, seeding, and soft-delete
21
+ 5. **Optimizes** context with token budgets, relevance filtering, enrichment pipelines, and reward-scored memory
22
+ 6. **Searches** semantically via bring-your-own embeddings and cosine similarity
21
23
 
22
24
  Lattice has no opinions about your schema, your agents, or your file format. You define the tables. You control the rendering. Lattice runs the sync loop.
23
25
 
@@ -41,6 +43,9 @@ Lattice has no opinions about your schema, your agents, or your file format. You
41
43
  - [Render, sync, watch, and reconcile](#render-sync-watch-and-reconcile)
42
44
  - [Events](#events)
43
45
  - [Raw DB access](#raw-db-access)
46
+ - [Context optimization](#context-optimization-v13)
47
+ - [Semantic search](#semantic-search-v13)
48
+ - [Writeback validation](#writeback-validation-v13)
44
49
  - [Template rendering](#template-rendering)
45
50
  - [Built-in templates](#built-in-templates)
46
51
  - [Lifecycle hooks](#lifecycle-hooks)
@@ -1261,6 +1266,140 @@ const rows = db.db
1261
1266
 
1262
1267
  ---
1263
1268
 
1269
+ ### Context optimization (v1.3+)
1270
+
1271
+ Lattice provides several options on `TableDefinition` to optimize what gets rendered into context files.
1272
+
1273
+ #### Token budget
1274
+
1275
+ Limit the token count of rendered output. When content exceeds the budget, rows are pruned by priority:
1276
+
1277
+ ```typescript
1278
+ db.define('tickets', {
1279
+ columns: { id: 'TEXT PRIMARY KEY', title: 'TEXT', updated_at: 'TEXT' },
1280
+ render: (rows) => rows.map((r) => `- ${r.title}`).join('\n'),
1281
+ outputFile: 'TICKETS.md',
1282
+ tokenBudget: 4000, // max estimated tokens (~4 chars/token)
1283
+ prioritizeBy: 'updated_at', // keep most recent rows when pruning
1284
+ });
1285
+ ```
1286
+
1287
+ A truncation footer is appended: `[truncated: 47 of 123 rows rendered, ~3800 tokens]`
1288
+
1289
+ #### Relevance filtering
1290
+
1291
+ Dynamically filter rows based on a task context string:
1292
+
1293
+ ```typescript
1294
+ db.define('knowledge', {
1295
+ columns: { id: 'TEXT PRIMARY KEY', topic: 'TEXT', body: 'TEXT' },
1296
+ render: (rows) => rows.map((r) => `## ${r.topic}\n${r.body}`).join('\n\n'),
1297
+ outputFile: 'KNOWLEDGE.md',
1298
+ relevanceFilter: (row, ctx) =>
1299
+ ctx ? String(row.body).toLowerCase().includes(ctx.toLowerCase()) : true,
1300
+ });
1301
+
1302
+ // Set the current task context — only matching rows are rendered
1303
+ db.setTaskContext('deployment');
1304
+ await db.render('./context');
1305
+ ```
1306
+
1307
+ #### Enrichment pipeline
1308
+
1309
+ Transform rows between filtering and rendering — add computed fields, cluster, summarize:
1310
+
1311
+ ```typescript
1312
+ db.define('incidents', {
1313
+ columns: { id: 'TEXT PRIMARY KEY', severity: 'TEXT', title: 'TEXT', created_at: 'TEXT' },
1314
+ render: (rows) => JSON.stringify(rows, null, 2),
1315
+ outputFile: 'incidents.json',
1316
+ enrich: [
1317
+ (rows) => rows.map((r) => ({
1318
+ ...r,
1319
+ _age_hours: Math.round((Date.now() - new Date(r.created_at as string).getTime()) / 3600000),
1320
+ })),
1321
+ (rows) => rows.length > 100 ? [{ _summary: `${rows.length} incidents` }] : rows,
1322
+ ],
1323
+ });
1324
+ ```
1325
+
1326
+ #### Reward-scored memory
1327
+
1328
+ Track which data is useful. High-reward rows are prioritized in rendering; low-scoring rows can be auto-pruned:
1329
+
1330
+ ```typescript
1331
+ db.define('tips', {
1332
+ columns: { id: 'TEXT PRIMARY KEY', tip: 'TEXT', deleted_at: 'TEXT' },
1333
+ render: (rows) => rows.map((r) => `- ${r.tip}`).join('\n'),
1334
+ outputFile: 'TIPS.md',
1335
+ rewardTracking: true, // auto-adds _reward_total, _reward_count columns
1336
+ pruneBelow: 0.3, // soft-delete rows with reward < 0.3 (requires deleted_at column)
1337
+ });
1338
+
1339
+ await db.init();
1340
+ const id = await db.insert('tips', { tip: 'Use batch inserts for bulk data' });
1341
+
1342
+ // After the agent confirms this tip was useful:
1343
+ await db.reward('tips', id, { relevance: 0.9, accuracy: 1.0 });
1344
+ ```
1345
+
1346
+ ### Semantic search (v1.3+)
1347
+
1348
+ Enable embedding-based search on any table. Bring your own embedding function:
1349
+
1350
+ ```typescript
1351
+ db.define('docs', {
1352
+ columns: { id: 'TEXT PRIMARY KEY', title: 'TEXT', body: 'TEXT' },
1353
+ render: (rows) => rows.map((r) => `## ${r.title}\n${r.body}`).join('\n\n---\n\n'),
1354
+ outputFile: 'DOCS.md',
1355
+ embeddings: {
1356
+ fields: ['title', 'body'],
1357
+ embed: async (text) => {
1358
+ const res = await openai.embeddings.create({ input: text, model: 'text-embedding-3-small' });
1359
+ return res.data[0].embedding;
1360
+ },
1361
+ },
1362
+ });
1363
+
1364
+ await db.init();
1365
+ await db.insert('docs', { title: 'Deploy guide', body: 'How to deploy to production...' });
1366
+
1367
+ // Search by meaning, not keywords
1368
+ const results = await db.search('docs', 'ship to prod', { topK: 5, minScore: 0.7 });
1369
+ for (const { row, score } of results) {
1370
+ console.log(`${score.toFixed(2)} — ${row.title}`);
1371
+ }
1372
+ ```
1373
+
1374
+ Embeddings are stored in a companion SQLite table and cosine similarity is computed in JS — no external vector database required.
1375
+
1376
+ ### Writeback validation (v1.3+)
1377
+
1378
+ Validate agent-written data before persisting. Reject low-quality or hallucinated writes:
1379
+
1380
+ ```typescript
1381
+ db.defineWriteback({
1382
+ file: './agent-output/*.md',
1383
+ parse: (content, offset) => ({ entries: [content.slice(offset)], nextOffset: content.length }),
1384
+ persist: async (entry) => { /* save to DB */ },
1385
+ validate: async (entry) => {
1386
+ const text = entry as string;
1387
+ const hasRequiredFields = text.includes('## Title') && text.includes('## Body');
1388
+ return {
1389
+ pass: hasRequiredFields,
1390
+ score: hasRequiredFields ? 0.9 : 0.1,
1391
+ reason: hasRequiredFields ? undefined : 'Missing required sections',
1392
+ };
1393
+ },
1394
+ rejectBelow: 0.5,
1395
+ onReject: (entry, result) => {
1396
+ console.warn(`Rejected write: ${result.reason} (score: ${result.score})`);
1397
+ },
1398
+ });
1399
+ ```
1400
+
1401
+ ---
1402
+
1264
1403
  ## Template rendering
1265
1404
 
1266
1405
  ### Built-in templates
package/dist/cli.js CHANGED
@@ -646,6 +646,55 @@ var Sanitizer = class {
646
646
  import { join as join5, basename, isAbsolute, resolve as resolve3 } from "path";
647
647
  import { mkdirSync as mkdirSync3, existsSync as existsSync5, copyFileSync } from "fs";
648
648
 
649
+ // src/render/token-budget.ts
650
+ function estimateTokens(text) {
651
+ return Math.ceil(text.length / 4);
652
+ }
653
+ function applyTokenBudget(rows, renderFn, budget, prioritizeBy) {
654
+ const fullContent = renderFn(rows);
655
+ if (estimateTokens(fullContent) <= budget) return fullContent;
656
+ if (rows.length === 0) return fullContent;
657
+ const prioritized = [...rows];
658
+ if (typeof prioritizeBy === "function") {
659
+ prioritized.sort(prioritizeBy);
660
+ } else if (typeof prioritizeBy === "string") {
661
+ const col = prioritizeBy;
662
+ prioritized.sort((a, b) => {
663
+ const va = a[col];
664
+ const vb = b[col];
665
+ if (va == null && vb == null) return 0;
666
+ if (va == null) return 1;
667
+ if (vb == null) return -1;
668
+ if (va < vb) return 1;
669
+ if (va > vb) return -1;
670
+ return 0;
671
+ });
672
+ }
673
+ let lo = 0;
674
+ let hi = prioritized.length;
675
+ let bestContent = "";
676
+ let bestCount = 0;
677
+ while (lo < hi) {
678
+ const mid = Math.ceil((lo + hi) / 2);
679
+ const content = renderFn(prioritized.slice(0, mid));
680
+ if (estimateTokens(content) <= budget) {
681
+ bestContent = content;
682
+ bestCount = mid;
683
+ lo = mid;
684
+ if (lo === hi) break;
685
+ } else {
686
+ hi = mid - 1;
687
+ }
688
+ }
689
+ if (bestCount === 0) {
690
+ bestContent = renderFn([]);
691
+ }
692
+ const tokens = estimateTokens(bestContent);
693
+ return bestContent + `
694
+
695
+ [truncated: ${bestCount} of ${rows.length} rows rendered, ~${tokens} tokens]`;
696
+ }
697
+
649
698
  // src/render/entity-query.ts
650
699
  var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
651
700
  function effectiveFilters(opts) {
@@ -1073,9 +1122,11 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
1073
1122
  var RenderEngine = class {
1074
1123
  _schema;
1075
1124
  _adapter;
1076
- constructor(schema, adapter) {
1125
+ _getTaskContext;
1126
+ constructor(schema, adapter, getTaskContext) {
1077
1127
  this._schema = schema;
1078
1128
  this._adapter = adapter;
1129
+ this._getTaskContext = getTaskContext ?? (() => "");
1079
1130
  }
1080
1131
  async render(outputDir) {
1081
1132
  const start = Date.now();
@@ -1083,8 +1134,38 @@ var RenderEngine = class {
1083
1134
  const counters = { skipped: 0 };
1084
1135
  for (const [name, def] of this._schema.getTables()) {
1085
1136
  let rows = this._schema.queryTable(this._adapter, name);
1137
+ if (def.relevanceFilter) {
1138
+ const ctx = this._getTaskContext();
1139
+ rows = rows.filter((row) => def.relevanceFilter(row, ctx));
1140
+ }
1086
1141
  if (def.filter) rows = def.filter(rows);
1087
- const content = def.render(rows);
1142
+ if (def.rewardTracking) {
1143
+ if (def.pruneBelow !== void 0) {
1144
+ const threshold = def.pruneBelow;
1145
+ const toPrune = rows.filter(
1146
+ (r) => r._reward_count > 0 && r._reward_total < threshold
1147
+ );
1148
+ if (toPrune.length > 0) {
1149
+ for (const r of toPrune) {
1150
+ const pkCol = this._schema.getPrimaryKey(name)[0] ?? "id";
1151
+ this._adapter.run(
1152
+ `UPDATE "${name}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`,
1153
+ [r[pkCol]]
1154
+ );
1155
+ }
1156
+ rows = rows.filter(
1157
+ (r) => r._reward_count === 0 || r._reward_total >= threshold
1158
+ );
1159
+ }
1160
+ }
1161
+ if (!def.prioritizeBy) {
1162
+ rows.sort((a, b) => (b._reward_total ?? 0) - (a._reward_total ?? 0));
1163
+ }
1164
+ }
1165
+ if (def.enrich) {
1166
+ for (const fn of def.enrich) rows = fn(rows);
1167
+ }
1168
+ const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
1088
1169
  const filePath = join5(outputDir, def.outputFile);
1089
1170
  if (atomicWrite(filePath, content)) {
1090
1171
  filesWritten.push(filePath);
@@ -1488,6 +1569,14 @@ var WritebackPipeline = class {
1488
1569
  if (store.isSeen(filePath, key)) continue;
1489
1570
  store.markSeen(filePath, key);
1490
1571
  }
1572
+ if (def.validate) {
1573
+ const result = await def.validate(entry);
1574
+ const threshold = def.rejectBelow ?? 0;
1575
+ if (!result.pass || result.score < threshold) {
1576
+ def.onReject?.(entry, result);
1577
+ continue;
1578
+ }
1579
+ }
1491
1580
  await def.persist(entry, filePath);
1492
1581
  processed++;
1493
1582
  }
@@ -1666,6 +1755,73 @@ function resolveEncryptedColumns(encrypted, allColumns) {
1666
1755
  return new Set(allColumns.filter((c) => !SKIP_COLUMNS.has(c)));
1667
1756
  }
1668
1757
 
1758
+ // src/search/embeddings.ts
1759
+ var EMBEDDINGS_TABLE = "_lattice_embeddings";
1760
+ function ensureEmbeddingsTable(adapter) {
1761
+ adapter.run(`CREATE TABLE IF NOT EXISTS "${EMBEDDINGS_TABLE}" (
1762
+ "table_name" TEXT NOT NULL,
1763
+ "row_pk" TEXT NOT NULL,
1764
+ "embedding" TEXT NOT NULL,
1765
+ PRIMARY KEY ("table_name", "row_pk")
1766
+ )`);
1767
+ }
1768
+ async function storeEmbedding(adapter, table, pk, row, config) {
1769
+ const text = config.fields.map((f) => {
1770
+ const v = row[f];
1771
+ return v == null ? "" : String(v);
1772
+ }).filter((s) => s.length > 0).join(" ");
1773
+ if (text.length === 0) return;
1774
+ const vector = await config.embed(text);
1775
+ adapter.run(
1776
+ `INSERT OR REPLACE INTO "${EMBEDDINGS_TABLE}" ("table_name", "row_pk", "embedding") VALUES (?, ?, ?)`,
1777
+ [table, pk, JSON.stringify(vector)]
1778
+ );
1779
+ }
1780
+ function removeEmbedding(adapter, table, pk) {
1781
+ adapter.run(
1782
+ `DELETE FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ? AND "row_pk" = ?`,
1783
+ [table, pk]
1784
+ );
1785
+ }
1786
+ function cosineSimilarity(a, b) {
1787
+ const len = Math.min(a.length, b.length);
1788
+ let dot = 0;
1789
+ let magA = 0;
1790
+ let magB = 0;
1791
+ for (let i = 0; i < len; i++) {
1792
+ dot += a[i] * b[i];
1793
+ magA += a[i] * a[i];
1794
+ magB += b[i] * b[i];
1795
+ }
1796
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
1797
+ return denom === 0 ? 0 : dot / denom;
1798
+ }
1799
+ async function searchByEmbedding(adapter, table, queryText, config, topK, minScore, pkColumn = "id") {
1800
+ const queryVector = await config.embed(queryText);
1801
+ const stored = adapter.all(
1802
+ `SELECT "row_pk", "embedding" FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ?`,
1803
+ [table]
1804
+ );
1805
+ const scored = [];
1806
+ for (const entry of stored) {
1807
+ const vec = JSON.parse(entry.embedding);
1808
+ const score = cosineSimilarity(queryVector, vec);
1809
+ if (score >= minScore) {
1810
+ scored.push({ pk: entry.row_pk, score });
1811
+ }
1812
+ }
1813
+ scored.sort((a, b) => b.score - a.score);
1814
+ const topResults = scored.slice(0, topK);
1815
+ const results = [];
1816
+ for (const { pk, score } of topResults) {
1817
+ const row = adapter.get(`SELECT * FROM "${table}" WHERE "${pkColumn}" = ?`, [pk]);
1818
+ if (row) {
1819
+ results.push({ row, score });
1820
+ }
1821
+ }
1822
+ return results;
1823
+ }
1824
+
1669
1825
  // src/lattice.ts
1670
1826
  var Lattice = class {
1671
1827
  _adapter;
@@ -1684,6 +1840,8 @@ var Lattice = class {
1684
1840
  _encryptedTableColumns = /* @__PURE__ */ new Map();
1685
1841
  /** Raw encryption key passphrase from constructor options. */
1686
1842
  _encryptionKeyRaw;
1843
+ /** Current task context string for relevance filtering. */
1844
+ _taskContext = "";
1687
1845
  _auditHandlers = [];
1688
1846
  _renderHandlers = [];
1689
1847
  _writebackHandlers = [];
@@ -1710,7 +1868,7 @@ var Lattice = class {
1710
1868
  this._adapter = new SQLiteAdapter(dbPath, adapterOpts);
1711
1869
  this._schema = new SchemaManager();
1712
1870
  this._sanitizer = new Sanitizer(options.security);
1713
- this._render = new RenderEngine(this._schema, this._adapter);
1871
+ this._render = new RenderEngine(this._schema, this._adapter, () => this._taskContext);
1714
1872
  this._reverseSync = new ReverseSyncEngine(this._schema, this._adapter);
1715
1873
  this._loop = new SyncLoop(this._render);
1716
1874
  this._writeback = new WritebackPipeline();
@@ -1734,8 +1892,10 @@ var Lattice = class {
1734
1892
  // -------------------------------------------------------------------------
1735
1893
  define(table, def) {
1736
1894
  this._assertNotInit("define");
1895
+ const columns = def.rewardTracking ? { ...def.columns, _reward_total: "REAL DEFAULT 0", _reward_count: "INTEGER DEFAULT 0" } : def.columns;
1737
1896
  const compiledDef = {
1738
1897
  ...def,
1898
+ columns,
1739
1899
  render: def.render ? compileRender(
1740
1900
  def,
1741
1901
  table,
@@ -1781,6 +1941,10 @@ var Lattice = class {
1781
1941
  const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1782
1942
  this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1783
1943
  }
1944
+ const hasEmbeddings = [...this._schema.getTables().values()].some((d) => d.embeddings);
1945
+ if (hasEmbeddings) {
1946
+ ensureEmbeddingsTable(this._adapter);
1947
+ }
1784
1948
  this._setupEncryption();
1785
1949
  this._initialized = true;
1786
1950
  return Promise.resolve();
@@ -1810,6 +1974,21 @@ var Lattice = class {
1810
1974
  this._initialized = false;
1811
1975
  }
1812
1976
  // -------------------------------------------------------------------------
1977
+ // Task context (for relevance filtering)
1978
+ // -------------------------------------------------------------------------
1979
+ /**
1980
+ * Set the current task context string. Tables with a `relevanceFilter`
1981
+ * will use this value to filter rows before rendering.
1982
+ */
1983
+ setTaskContext(context) {
1984
+ this._taskContext = context;
1985
+ return this;
1986
+ }
1987
+ /** Return the current task context string. */
1988
+ getTaskContext() {
1989
+ return this._taskContext;
1990
+ }
1991
+ // -------------------------------------------------------------------------
1813
1992
  // Encryption helpers
1814
1993
  // -------------------------------------------------------------------------
1815
1994
  _setupEncryption() {
@@ -1884,6 +2063,7 @@ var Lattice = class {
1884
2063
  const pkValue = rawPk != null ? String(rawPk) : "";
1885
2064
  this._sanitizer.emitAudit(table, "insert", pkValue);
1886
2065
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
2066
+ this._syncEmbedding(table, "insert", rowWithPk, pkValue);
1887
2067
  return Promise.resolve(pkValue);
1888
2068
  }
1889
2069
  /**
@@ -1951,6 +2131,11 @@ var Lattice = class {
1951
2131
  const auditId = typeof id === "string" ? id : JSON.stringify(id);
1952
2132
  this._sanitizer.emitAudit(table, "update", auditId);
1953
2133
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
2134
+ const def = this._schema.getTables().get(table);
2135
+ if (def?.embeddings) {
2136
+ const fullRow = this._adapter.get(`SELECT * FROM "${table}" WHERE ${clause}`, pkParams);
2137
+ if (fullRow) this._syncEmbedding(table, "update", fullRow, auditId);
2138
+ }
1954
2139
  return Promise.resolve();
1955
2140
  }
1956
2141
  /**
@@ -1972,6 +2157,7 @@ var Lattice = class {
1972
2157
  const auditId = typeof id === "string" ? id : JSON.stringify(id);
1973
2158
  this._sanitizer.emitAudit(table, "delete", auditId);
1974
2159
  this._fireWriteHooks(table, "delete", { id: auditId }, auditId);
2160
+ this._syncEmbedding(table, "delete", {}, auditId);
1975
2161
  return Promise.resolve();
1976
2162
  }
1977
2163
  get(table, id) {
@@ -2355,6 +2541,65 @@ var Lattice = class {
2355
2541
  const ms = unit === "h" ? num * 36e5 : unit === "d" ? num * 864e5 : num * 6e4;
2356
2542
  return new Date(Date.now() - ms).toISOString();
2357
2543
  }
2544
+ // -------------------------------------------------------------------------
2545
+ // Reward tracking
2546
+ // -------------------------------------------------------------------------
2547
+ /**
2548
+ * Update reward scores for a row. The total reward is recalculated as
2549
+ * the running average across all reward calls. Requires `rewardTracking`
2550
+ * on the table definition.
2551
+ */
2552
+ reward(table, id, scores) {
2553
+ const notInit = this._notInitError();
2554
+ if (notInit) return notInit;
2555
+ const def = this._schema.getTables().get(table);
2556
+ if (!def?.rewardTracking) {
2557
+ return Promise.reject(
2558
+ new Error(`Table "${table}" does not have rewardTracking enabled`)
2559
+ );
2560
+ }
2561
+ const vals = Object.values(scores);
2562
+ if (vals.length === 0) return Promise.resolve();
2563
+ const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
2564
+ const { clause, params: pkParams } = this._pkWhere(table, id);
2565
+ this._adapter.run(
2566
+ `UPDATE "${table}" SET "_reward_total" = ("_reward_total" * "_reward_count" + ?) / ("_reward_count" + 1), "_reward_count" = "_reward_count" + 1 WHERE ${clause}`,
2567
+ [avg, ...pkParams]
2568
+ );
2569
+ return Promise.resolve();
2570
+ }
2571
+ // -------------------------------------------------------------------------
2572
+ // Semantic search
2573
+ // -------------------------------------------------------------------------
2574
+ /**
2575
+ * Search for rows by semantic similarity. Requires `embeddings` config
2576
+ * on the table definition.
2577
+ *
2578
+ * @param table - Table to search
2579
+ * @param query - Natural-language query text
2580
+ * @param opts - Search options (topK, minScore)
2581
+ * @returns Matching rows with similarity scores, sorted best-first.
2582
+ */
2583
+ async search(table, query, opts = {}) {
2584
+ const notInit = this._notInitError();
2585
+ if (notInit) return notInit;
2586
+ const def = this._schema.getTables().get(table);
2587
+ if (!def?.embeddings) {
2588
+ return Promise.reject(
2589
+ new Error(`Table "${table}" does not have embeddings configured`)
2590
+ );
2591
+ }
2592
+ const pkCol = this._schema.getPrimaryKey(table)[0] ?? "id";
2593
+ return searchByEmbedding(
2594
+ this._adapter,
2595
+ table,
2596
+ query,
2597
+ def.embeddings,
2598
+ opts.topK ?? 10,
2599
+ opts.minScore ?? 0,
2600
+ pkCol
2601
+ );
2602
+ }
2358
2603
  query(table, opts = {}) {
2359
2604
  const notInit = this._notInitError();
2360
2605
  if (notInit) return notInit;
@@ -2613,6 +2858,23 @@ var Lattice = class {
2613
2858
  }
2614
2859
  }
2615
2860
  }
2861
+ /**
2862
+ * Update or remove the embedding for a row.
2863
+ * No-op if the table doesn't have `embeddings` configured.
2864
+ */
2865
+ _syncEmbedding(table, op, row, pk) {
2866
+ const def = this._schema.getTables().get(table);
2867
+ if (!def?.embeddings) return;
2868
+ if (op === "delete") {
2869
+ removeEmbedding(this._adapter, table, pk);
2870
+ return;
2871
+ }
2872
+ storeEmbedding(this._adapter, table, pk, row, def.embeddings).catch((err) => {
2873
+ for (const h of this._errorHandlers) {
2874
+ h(err instanceof Error ? err : new Error(String(err)));
2875
+ }
2876
+ });
2877
+ }
2616
2878
  _notInitError() {
2617
2879
  if (!this._initialized) {
2618
2880
  return Promise.reject(