latticesql 1.2.6 → 1.3.1

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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { join as join2 } from "path";
6
6
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
7
7
 
8
8
  // src/render/writer.ts
9
- import { writeFileSync, mkdirSync, renameSync, existsSync, readFileSync } from "fs";
9
+ import { writeFileSync, mkdirSync, renameSync, copyFileSync, unlinkSync, existsSync, readFileSync } from "fs";
10
10
  import { createHash } from "crypto";
11
11
  import { dirname, join } from "path";
12
12
  import { tmpdir } from "os";
@@ -19,7 +19,16 @@ function atomicWrite(filePath, content) {
19
19
  if (currentHash === newHash) return false;
20
20
  const tmp = join(tmpdir(), `lattice-${randomBytes(8).toString("hex")}.tmp`);
21
21
  writeFileSync(tmp, content, "utf8");
22
- renameSync(tmp, filePath);
22
+ try {
23
+ renameSync(tmp, filePath);
24
+ } catch (err) {
25
+ if (err.code === "EXDEV") {
26
+ copyFileSync(tmp, filePath);
27
+ unlinkSync(tmp);
28
+ } else {
29
+ throw err;
30
+ }
31
+ }
23
32
  return true;
24
33
  }
25
34
  function existingHash(filePath) {
@@ -329,7 +338,56 @@ var Sanitizer = class {
329
338
 
330
339
  // src/render/engine.ts
331
340
  import { join as join4, basename, isAbsolute, resolve } from "path";
332
- import { mkdirSync as mkdirSync2, existsSync as existsSync4, copyFileSync } from "fs";
341
+ import { mkdirSync as mkdirSync2, existsSync as existsSync4, copyFileSync as copyFileSync2 } from "fs";
342
+
343
+ // src/render/token-budget.ts
344
+ function estimateTokens(text) {
345
+ return Math.ceil(text.length / 4);
346
+ }
347
+ function applyTokenBudget(rows, renderFn, budget, prioritizeBy) {
348
+ const fullContent = renderFn(rows);
349
+ if (estimateTokens(fullContent) <= budget) return fullContent;
350
+ if (rows.length === 0) return fullContent;
351
+ const prioritized = [...rows];
352
+ if (typeof prioritizeBy === "function") {
353
+ prioritized.sort(prioritizeBy);
354
+ } else if (typeof prioritizeBy === "string") {
355
+ const col = prioritizeBy;
356
+ prioritized.sort((a, b) => {
357
+ const va = a[col];
358
+ const vb = b[col];
359
+ if (va == null && vb == null) return 0;
360
+ if (va == null) return 1;
361
+ if (vb == null) return -1;
362
+ if (va < vb) return 1;
363
+ if (va > vb) return -1;
364
+ return 0;
365
+ });
366
+ }
367
+ let lo = 0;
368
+ let hi = prioritized.length;
369
+ let bestContent = "";
370
+ let bestCount = 0;
371
+ while (lo < hi) {
372
+ const mid = Math.ceil((lo + hi) / 2);
373
+ const content = renderFn(prioritized.slice(0, mid));
374
+ if (estimateTokens(content) <= budget) {
375
+ bestContent = content;
376
+ bestCount = mid;
377
+ lo = mid;
378
+ if (lo === hi) break;
379
+ } else {
380
+ hi = mid - 1;
381
+ }
382
+ }
383
+ if (bestCount === 0) {
384
+ bestContent = renderFn([]);
385
+ }
386
+ const tokens = estimateTokens(bestContent);
387
+ return bestContent + `
388
+
389
+ [truncated: ${bestCount} of ${rows.length} rows rendered, ~${tokens} tokens]`;
390
+ }
333
391
 
334
392
  // src/render/entity-query.ts
335
393
  var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
@@ -664,7 +722,7 @@ ${tmpl.perRow.body(row)}
664
722
 
665
723
  // src/lifecycle/cleanup.ts
666
724
  import { join as join3 } from "path";
667
- import { existsSync as existsSync3, readdirSync, unlinkSync, rmdirSync, statSync } from "fs";
725
+ import { existsSync as existsSync3, readdirSync, unlinkSync as unlinkSync2, rmdirSync, statSync } from "fs";
668
726
  function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, manifest, options = {}, newManifest) {
669
727
  const result = {
670
728
  directoriesRemoved: [],
@@ -706,7 +764,7 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
706
764
  if (globalProtected.has(filename)) continue;
707
765
  const filePath = join3(entityDir, filename);
708
766
  if (!existsSync3(filePath)) continue;
709
- if (!options.dryRun) unlinkSync(filePath);
767
+ if (!options.dryRun) unlinkSync2(filePath);
710
768
  options.onOrphan?.(filePath, "file");
711
769
  result.filesRemoved.push(filePath);
712
770
  }
@@ -751,7 +809,7 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
751
809
  if (globalProtected.has(filename)) continue;
752
810
  const filePath = join3(entityDir, filename);
753
811
  if (!existsSync3(filePath)) continue;
754
- if (!options.dryRun) unlinkSync(filePath);
812
+ if (!options.dryRun) unlinkSync2(filePath);
755
813
  options.onOrphan?.(filePath, "file");
756
814
  result.filesRemoved.push(filePath);
757
815
  }
@@ -765,9 +823,11 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
765
823
  var RenderEngine = class {
766
824
  _schema;
767
825
  _adapter;
768
- constructor(schema, adapter) {
826
+ _getTaskContext;
827
+ constructor(schema, adapter, getTaskContext) {
769
828
  this._schema = schema;
770
829
  this._adapter = adapter;
830
+ this._getTaskContext = getTaskContext ?? (() => "");
771
831
  }
772
832
  async render(outputDir) {
773
833
  const start = Date.now();
@@ -775,8 +835,38 @@ var RenderEngine = class {
775
835
  const counters = { skipped: 0 };
776
836
  for (const [name, def] of this._schema.getTables()) {
777
837
  let rows = this._schema.queryTable(this._adapter, name);
838
+ if (def.relevanceFilter) {
839
+ const ctx = this._getTaskContext();
840
+ rows = rows.filter((row) => def.relevanceFilter(row, ctx));
841
+ }
778
842
  if (def.filter) rows = def.filter(rows);
779
- const content = def.render(rows);
843
+ if (def.rewardTracking) {
844
+ if (def.pruneBelow !== void 0) {
845
+ const threshold = def.pruneBelow;
846
+ const toPrune = rows.filter(
847
+ (r) => r._reward_count > 0 && r._reward_total < threshold
848
+ );
849
+ if (toPrune.length > 0) {
850
+ for (const r of toPrune) {
851
+ const pkCol = this._schema.getPrimaryKey(name)[0] ?? "id";
852
+ this._adapter.run(
853
+ `UPDATE "${name}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`,
854
+ [r[pkCol]]
855
+ );
856
+ }
857
+ rows = rows.filter(
858
+ (r) => r._reward_count === 0 || r._reward_total >= threshold
859
+ );
860
+ }
861
+ }
862
+ if (!def.prioritizeBy) {
863
+ rows.sort((a, b) => (b._reward_total ?? 0) - (a._reward_total ?? 0));
864
+ }
865
+ }
866
+ if (def.enrich) {
867
+ for (const fn of def.enrich) rows = fn(rows);
868
+ }
869
+ const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
780
870
  const filePath = join4(outputDir, def.outputFile);
781
871
  if (atomicWrite(filePath, content)) {
782
872
  filesWritten.push(filePath);
@@ -895,7 +985,7 @@ var RenderEngine = class {
895
985
  const destPath = join4(entityDir, basename(absPath));
896
986
  if (!existsSync4(destPath)) {
897
987
  try {
898
- copyFileSync(absPath, destPath);
988
+ copyFileSync2(absPath, destPath);
899
989
  filesWritten.push(destPath);
900
990
  } catch {
901
991
  }
@@ -1236,6 +1326,14 @@ var WritebackPipeline = class {
1236
1326
  if (store.isSeen(filePath, key)) continue;
1237
1327
  store.markSeen(filePath, key);
1238
1328
  }
1329
+ if (def.validate) {
1330
+ const result = await def.validate(entry);
1331
+ const threshold = def.rejectBelow ?? 0;
1332
+ if (!result.pass || result.score < threshold) {
1333
+ def.onReject?.(entry, result);
1334
+ continue;
1335
+ }
1336
+ }
1239
1337
  await def.persist(entry, filePath);
1240
1338
  processed++;
1241
1339
  }
@@ -1642,6 +1740,73 @@ function resolveEncryptedColumns(encrypted, allColumns) {
1642
1740
  return new Set(allColumns.filter((c) => !SKIP_COLUMNS.has(c)));
1643
1741
  }
1644
1742
 
1743
+ // src/search/embeddings.ts
1744
+ var EMBEDDINGS_TABLE = "_lattice_embeddings";
1745
+ function ensureEmbeddingsTable(adapter) {
1746
+ adapter.run(`CREATE TABLE IF NOT EXISTS "${EMBEDDINGS_TABLE}" (
1747
+ "table_name" TEXT NOT NULL,
1748
+ "row_pk" TEXT NOT NULL,
1749
+ "embedding" TEXT NOT NULL,
1750
+ PRIMARY KEY ("table_name", "row_pk")
1751
+ )`);
1752
+ }
1753
+ async function storeEmbedding(adapter, table, pk, row, config) {
1754
+ const text = config.fields.map((f) => {
1755
+ const v = row[f];
1756
+ return v == null ? "" : String(v);
1757
+ }).filter((s) => s.length > 0).join(" ");
1758
+ if (text.length === 0) return;
1759
+ const vector = await config.embed(text);
1760
+ adapter.run(
1761
+ `INSERT OR REPLACE INTO "${EMBEDDINGS_TABLE}" ("table_name", "row_pk", "embedding") VALUES (?, ?, ?)`,
1762
+ [table, pk, JSON.stringify(vector)]
1763
+ );
1764
+ }
1765
+ function removeEmbedding(adapter, table, pk) {
1766
+ adapter.run(
1767
+ `DELETE FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ? AND "row_pk" = ?`,
1768
+ [table, pk]
1769
+ );
1770
+ }
1771
+ function cosineSimilarity(a, b) {
1772
+ const len = Math.min(a.length, b.length);
1773
+ let dot = 0;
1774
+ let magA = 0;
1775
+ let magB = 0;
1776
+ for (let i = 0; i < len; i++) {
1777
+ dot += a[i] * b[i];
1778
+ magA += a[i] * a[i];
1779
+ magB += b[i] * b[i];
1780
+ }
1781
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
1782
+ return denom === 0 ? 0 : dot / denom;
1783
+ }
1784
+ async function searchByEmbedding(adapter, table, queryText, config, topK, minScore, pkColumn = "id") {
1785
+ const queryVector = await config.embed(queryText);
1786
+ const stored = adapter.all(
1787
+ `SELECT "row_pk", "embedding" FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ?`,
1788
+ [table]
1789
+ );
1790
+ const scored = [];
1791
+ for (const entry of stored) {
1792
+ const vec = JSON.parse(entry.embedding);
1793
+ const score = cosineSimilarity(queryVector, vec);
1794
+ if (score >= minScore) {
1795
+ scored.push({ pk: entry.row_pk, score });
1796
+ }
1797
+ }
1798
+ scored.sort((a, b) => b.score - a.score);
1799
+ const topResults = scored.slice(0, topK);
1800
+ const results = [];
1801
+ for (const { pk, score } of topResults) {
1802
+ const row = adapter.get(`SELECT * FROM "${table}" WHERE "${pkColumn}" = ?`, [pk]);
1803
+ if (row) {
1804
+ results.push({ row, score });
1805
+ }
1806
+ }
1807
+ return results;
1808
+ }
1809
+
1645
1810
  // src/lattice.ts
1646
1811
  var Lattice = class {
1647
1812
  _adapter;
@@ -1660,6 +1825,8 @@ var Lattice = class {
1660
1825
  _encryptedTableColumns = /* @__PURE__ */ new Map();
1661
1826
  /** Raw encryption key passphrase from constructor options. */
1662
1827
  _encryptionKeyRaw;
1828
+ /** Current task context string for relevance filtering. */
1829
+ _taskContext = "";
1663
1830
  _auditHandlers = [];
1664
1831
  _renderHandlers = [];
1665
1832
  _writebackHandlers = [];
@@ -1686,7 +1853,7 @@ var Lattice = class {
1686
1853
  this._adapter = new SQLiteAdapter(dbPath, adapterOpts);
1687
1854
  this._schema = new SchemaManager();
1688
1855
  this._sanitizer = new Sanitizer(options.security);
1689
- this._render = new RenderEngine(this._schema, this._adapter);
1856
+ this._render = new RenderEngine(this._schema, this._adapter, () => this._taskContext);
1690
1857
  this._reverseSync = new ReverseSyncEngine(this._schema, this._adapter);
1691
1858
  this._loop = new SyncLoop(this._render);
1692
1859
  this._writeback = new WritebackPipeline();
@@ -1710,8 +1877,10 @@ var Lattice = class {
1710
1877
  // -------------------------------------------------------------------------
1711
1878
  define(table, def) {
1712
1879
  this._assertNotInit("define");
1880
+ const columns = def.rewardTracking ? { ...def.columns, _reward_total: "REAL DEFAULT 0", _reward_count: "INTEGER DEFAULT 0" } : def.columns;
1713
1881
  const compiledDef = {
1714
1882
  ...def,
1883
+ columns,
1715
1884
  render: def.render ? compileRender(
1716
1885
  def,
1717
1886
  table,
@@ -1757,6 +1926,10 @@ var Lattice = class {
1757
1926
  const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1758
1927
  this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1759
1928
  }
1929
+ const hasEmbeddings = [...this._schema.getTables().values()].some((d) => d.embeddings);
1930
+ if (hasEmbeddings) {
1931
+ ensureEmbeddingsTable(this._adapter);
1932
+ }
1760
1933
  this._setupEncryption();
1761
1934
  this._initialized = true;
1762
1935
  return Promise.resolve();
@@ -1786,6 +1959,21 @@ var Lattice = class {
1786
1959
  this._initialized = false;
1787
1960
  }
1788
1961
  // -------------------------------------------------------------------------
1962
+ // Task context (for relevance filtering)
1963
+ // -------------------------------------------------------------------------
1964
+ /**
1965
+ * Set the current task context string. Tables with a `relevanceFilter`
1966
+ * will use this value to filter rows before rendering.
1967
+ */
1968
+ setTaskContext(context) {
1969
+ this._taskContext = context;
1970
+ return this;
1971
+ }
1972
+ /** Return the current task context string. */
1973
+ getTaskContext() {
1974
+ return this._taskContext;
1975
+ }
1976
+ // -------------------------------------------------------------------------
1789
1977
  // Encryption helpers
1790
1978
  // -------------------------------------------------------------------------
1791
1979
  _setupEncryption() {
@@ -1860,6 +2048,7 @@ var Lattice = class {
1860
2048
  const pkValue = rawPk != null ? String(rawPk) : "";
1861
2049
  this._sanitizer.emitAudit(table, "insert", pkValue);
1862
2050
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
2051
+ this._syncEmbedding(table, "insert", rowWithPk, pkValue);
1863
2052
  return Promise.resolve(pkValue);
1864
2053
  }
1865
2054
  /**
@@ -1927,6 +2116,11 @@ var Lattice = class {
1927
2116
  const auditId = typeof id === "string" ? id : JSON.stringify(id);
1928
2117
  this._sanitizer.emitAudit(table, "update", auditId);
1929
2118
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
2119
+ const def = this._schema.getTables().get(table);
2120
+ if (def?.embeddings) {
2121
+ const fullRow = this._adapter.get(`SELECT * FROM "${table}" WHERE ${clause}`, pkParams);
2122
+ if (fullRow) this._syncEmbedding(table, "update", fullRow, auditId);
2123
+ }
1930
2124
  return Promise.resolve();
1931
2125
  }
1932
2126
  /**
@@ -1948,6 +2142,7 @@ var Lattice = class {
1948
2142
  const auditId = typeof id === "string" ? id : JSON.stringify(id);
1949
2143
  this._sanitizer.emitAudit(table, "delete", auditId);
1950
2144
  this._fireWriteHooks(table, "delete", { id: auditId }, auditId);
2145
+ this._syncEmbedding(table, "delete", {}, auditId);
1951
2146
  return Promise.resolve();
1952
2147
  }
1953
2148
  get(table, id) {
@@ -2331,6 +2526,65 @@ var Lattice = class {
2331
2526
  const ms = unit === "h" ? num * 36e5 : unit === "d" ? num * 864e5 : num * 6e4;
2332
2527
  return new Date(Date.now() - ms).toISOString();
2333
2528
  }
2529
+ // -------------------------------------------------------------------------
2530
+ // Reward tracking
2531
+ // -------------------------------------------------------------------------
2532
+ /**
2533
+ * Update reward scores for a row. The total reward is recalculated as
2534
+ * the running average across all reward calls. Requires `rewardTracking`
2535
+ * on the table definition.
2536
+ */
2537
+ reward(table, id, scores) {
2538
+ const notInit = this._notInitError();
2539
+ if (notInit) return notInit;
2540
+ const def = this._schema.getTables().get(table);
2541
+ if (!def?.rewardTracking) {
2542
+ return Promise.reject(
2543
+ new Error(`Table "${table}" does not have rewardTracking enabled`)
2544
+ );
2545
+ }
2546
+ const vals = Object.values(scores);
2547
+ if (vals.length === 0) return Promise.resolve();
2548
+ const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
2549
+ const { clause, params: pkParams } = this._pkWhere(table, id);
2550
+ this._adapter.run(
2551
+ `UPDATE "${table}" SET "_reward_total" = ("_reward_total" * "_reward_count" + ?) / ("_reward_count" + 1), "_reward_count" = "_reward_count" + 1 WHERE ${clause}`,
2552
+ [avg, ...pkParams]
2553
+ );
2554
+ return Promise.resolve();
2555
+ }
2556
+ // -------------------------------------------------------------------------
2557
+ // Semantic search
2558
+ // -------------------------------------------------------------------------
2559
+ /**
2560
+ * Search for rows by semantic similarity. Requires `embeddings` config
2561
+ * on the table definition.
2562
+ *
2563
+ * @param table - Table to search
2564
+ * @param query - Natural-language query text
2565
+ * @param opts - Search options (topK, minScore)
2566
+ * @returns Matching rows with similarity scores, sorted best-first.
2567
+ */
2568
+ async search(table, query, opts = {}) {
2569
+ const notInit = this._notInitError();
2570
+ if (notInit) return notInit;
2571
+ const def = this._schema.getTables().get(table);
2572
+ if (!def?.embeddings) {
2573
+ return Promise.reject(
2574
+ new Error(`Table "${table}" does not have embeddings configured`)
2575
+ );
2576
+ }
2577
+ const pkCol = this._schema.getPrimaryKey(table)[0] ?? "id";
2578
+ return searchByEmbedding(
2579
+ this._adapter,
2580
+ table,
2581
+ query,
2582
+ def.embeddings,
2583
+ opts.topK ?? 10,
2584
+ opts.minScore ?? 0,
2585
+ pkCol
2586
+ );
2587
+ }
2334
2588
  query(table, opts = {}) {
2335
2589
  const notInit = this._notInitError();
2336
2590
  if (notInit) return notInit;
@@ -2589,6 +2843,23 @@ var Lattice = class {
2589
2843
  }
2590
2844
  }
2591
2845
  }
2846
+ /**
2847
+ * Update or remove the embedding for a row.
2848
+ * No-op if the table doesn't have `embeddings` configured.
2849
+ */
2850
+ _syncEmbedding(table, op, row, pk) {
2851
+ const def = this._schema.getTables().get(table);
2852
+ if (!def?.embeddings) return;
2853
+ if (op === "delete") {
2854
+ removeEmbedding(this._adapter, table, pk);
2855
+ return;
2856
+ }
2857
+ storeEmbedding(this._adapter, table, pk, row, def.embeddings).catch((err) => {
2858
+ for (const h of this._errorHandlers) {
2859
+ h(err instanceof Error ? err : new Error(String(err)));
2860
+ }
2861
+ });
2862
+ }
2592
2863
  _notInitError() {
2593
2864
  if (!this._initialized) {
2594
2865
  return Promise.reject(
@@ -3129,6 +3400,7 @@ export {
3129
3400
  InMemoryStateStore,
3130
3401
  Lattice,
3131
3402
  READ_ONLY_HEADER,
3403
+ applyTokenBudget,
3132
3404
  applyWriteEntry,
3133
3405
  autoUpdate,
3134
3406
  contentHash,
@@ -3138,6 +3410,7 @@ export {
3138
3410
  deriveKey,
3139
3411
  encrypt,
3140
3412
  entityFileNames,
3413
+ estimateTokens,
3141
3414
  fixSchemaConflicts,
3142
3415
  frontmatter,
3143
3416
  generateEntryId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "1.2.6",
3
+ "version": "1.3.1",
4
4
  "description": "Persistent structured memory for AI agent systems — SQLite ↔ LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",