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.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  InMemoryStateStore: () => InMemoryStateStore,
36
36
  Lattice: () => Lattice,
37
37
  READ_ONLY_HEADER: () => READ_ONLY_HEADER,
38
+ applyTokenBudget: () => applyTokenBudget,
38
39
  applyWriteEntry: () => applyWriteEntry,
39
40
  autoUpdate: () => autoUpdate,
40
41
  contentHash: () => contentHash,
@@ -44,6 +45,7 @@ __export(index_exports, {
44
45
  deriveKey: () => deriveKey,
45
46
  encrypt: () => encrypt,
46
47
  entityFileNames: () => entityFileNames,
48
+ estimateTokens: () => estimateTokens,
47
49
  fixSchemaConflicts: () => fixSchemaConflicts,
48
50
  frontmatter: () => frontmatter,
49
51
  generateEntryId: () => generateEntryId,
@@ -87,7 +89,16 @@ function atomicWrite(filePath, content) {
87
89
  if (currentHash === newHash) return false;
88
90
  const tmp = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `lattice-${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`);
89
91
  (0, import_node_fs.writeFileSync)(tmp, content, "utf8");
90
- (0, import_node_fs.renameSync)(tmp, filePath);
92
+ try {
93
+ (0, import_node_fs.renameSync)(tmp, filePath);
94
+ } catch (err) {
95
+ if (err.code === "EXDEV") {
96
+ (0, import_node_fs.copyFileSync)(tmp, filePath);
97
+ (0, import_node_fs.unlinkSync)(tmp);
98
+ } else {
99
+ throw err;
100
+ }
101
+ }
91
102
  return true;
92
103
  }
93
104
  function existingHash(filePath) {
@@ -399,6 +410,55 @@ var Sanitizer = class {
399
410
  var import_node_path4 = require("path");
400
411
  var import_node_fs4 = require("fs");
401
412
 
413
+ // src/render/token-budget.ts
414
+ function estimateTokens(text) {
415
+ return Math.ceil(text.length / 4);
416
+ }
417
+ function applyTokenBudget(rows, renderFn, budget, prioritizeBy) {
418
+ const fullContent = renderFn(rows);
419
+ if (estimateTokens(fullContent) <= budget) return fullContent;
420
+ if (rows.length === 0) return fullContent;
421
+ const prioritized = [...rows];
422
+ if (typeof prioritizeBy === "function") {
423
+ prioritized.sort(prioritizeBy);
424
+ } else if (typeof prioritizeBy === "string") {
425
+ const col = prioritizeBy;
426
+ prioritized.sort((a, b) => {
427
+ const va = a[col];
428
+ const vb = b[col];
429
+ if (va == null && vb == null) return 0;
430
+ if (va == null) return 1;
431
+ if (vb == null) return -1;
432
+ if (va < vb) return 1;
433
+ if (va > vb) return -1;
434
+ return 0;
435
+ });
436
+ }
437
+ let lo = 0;
438
+ let hi = prioritized.length;
439
+ let bestContent = "";
440
+ let bestCount = 0;
441
+ while (lo < hi) {
442
+ const mid = Math.ceil((lo + hi) / 2);
443
+ const content = renderFn(prioritized.slice(0, mid));
444
+ if (estimateTokens(content) <= budget) {
445
+ bestContent = content;
446
+ bestCount = mid;
447
+ lo = mid;
448
+ if (lo === hi) break;
449
+ } else {
450
+ hi = mid - 1;
451
+ }
452
+ }
453
+ if (bestCount === 0) {
454
+ bestContent = renderFn([]);
455
+ }
456
+ const tokens = estimateTokens(bestContent);
457
+ return bestContent + `
458
+
459
+ [truncated: ${bestCount} of ${rows.length} rows rendered, ~${tokens} tokens]`;
460
+ }
461
+
402
462
  // src/render/entity-query.ts
403
463
  var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
404
464
  function effectiveFilters(opts) {
@@ -833,9 +893,11 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
833
893
  var RenderEngine = class {
834
894
  _schema;
835
895
  _adapter;
836
- constructor(schema, adapter) {
896
+ _getTaskContext;
897
+ constructor(schema, adapter, getTaskContext) {
837
898
  this._schema = schema;
838
899
  this._adapter = adapter;
900
+ this._getTaskContext = getTaskContext ?? (() => "");
839
901
  }
840
902
  async render(outputDir) {
841
903
  const start = Date.now();
@@ -843,8 +905,38 @@ var RenderEngine = class {
843
905
  const counters = { skipped: 0 };
844
906
  for (const [name, def] of this._schema.getTables()) {
845
907
  let rows = this._schema.queryTable(this._adapter, name);
908
+ if (def.relevanceFilter) {
909
+ const ctx = this._getTaskContext();
910
+ rows = rows.filter((row) => def.relevanceFilter(row, ctx));
911
+ }
846
912
  if (def.filter) rows = def.filter(rows);
847
- const content = def.render(rows);
913
+ if (def.rewardTracking) {
914
+ if (def.pruneBelow !== void 0) {
915
+ const threshold = def.pruneBelow;
916
+ const toPrune = rows.filter(
917
+ (r) => r._reward_count > 0 && r._reward_total < threshold
918
+ );
919
+ if (toPrune.length > 0) {
920
+ for (const r of toPrune) {
921
+ const pkCol = this._schema.getPrimaryKey(name)[0] ?? "id";
922
+ this._adapter.run(
923
+ `UPDATE "${name}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`,
924
+ [r[pkCol]]
925
+ );
926
+ }
927
+ rows = rows.filter(
928
+ (r) => r._reward_count === 0 || r._reward_total >= threshold
929
+ );
930
+ }
931
+ }
932
+ if (!def.prioritizeBy) {
933
+ rows.sort((a, b) => (b._reward_total ?? 0) - (a._reward_total ?? 0));
934
+ }
935
+ }
936
+ if (def.enrich) {
937
+ for (const fn of def.enrich) rows = fn(rows);
938
+ }
939
+ const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
848
940
  const filePath = (0, import_node_path4.join)(outputDir, def.outputFile);
849
941
  if (atomicWrite(filePath, content)) {
850
942
  filesWritten.push(filePath);
@@ -1304,6 +1396,14 @@ var WritebackPipeline = class {
1304
1396
  if (store.isSeen(filePath, key)) continue;
1305
1397
  store.markSeen(filePath, key);
1306
1398
  }
1399
+ if (def.validate) {
1400
+ const result = await def.validate(entry);
1401
+ const threshold = def.rejectBelow ?? 0;
1402
+ if (!result.pass || result.score < threshold) {
1403
+ def.onReject?.(entry, result);
1404
+ continue;
1405
+ }
1406
+ }
1307
1407
  await def.persist(entry, filePath);
1308
1408
  processed++;
1309
1409
  }
@@ -1710,6 +1810,73 @@ function resolveEncryptedColumns(encrypted, allColumns) {
1710
1810
  return new Set(allColumns.filter((c) => !SKIP_COLUMNS.has(c)));
1711
1811
  }
1712
1812
 
1813
+ // src/search/embeddings.ts
1814
+ var EMBEDDINGS_TABLE = "_lattice_embeddings";
1815
+ function ensureEmbeddingsTable(adapter) {
1816
+ adapter.run(`CREATE TABLE IF NOT EXISTS "${EMBEDDINGS_TABLE}" (
1817
+ "table_name" TEXT NOT NULL,
1818
+ "row_pk" TEXT NOT NULL,
1819
+ "embedding" TEXT NOT NULL,
1820
+ PRIMARY KEY ("table_name", "row_pk")
1821
+ )`);
1822
+ }
1823
+ async function storeEmbedding(adapter, table, pk, row, config) {
1824
+ const text = config.fields.map((f) => {
1825
+ const v = row[f];
1826
+ return v == null ? "" : String(v);
1827
+ }).filter((s) => s.length > 0).join(" ");
1828
+ if (text.length === 0) return;
1829
+ const vector = await config.embed(text);
1830
+ adapter.run(
1831
+ `INSERT OR REPLACE INTO "${EMBEDDINGS_TABLE}" ("table_name", "row_pk", "embedding") VALUES (?, ?, ?)`,
1832
+ [table, pk, JSON.stringify(vector)]
1833
+ );
1834
+ }
1835
+ function removeEmbedding(adapter, table, pk) {
1836
+ adapter.run(
1837
+ `DELETE FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ? AND "row_pk" = ?`,
1838
+ [table, pk]
1839
+ );
1840
+ }
1841
+ function cosineSimilarity(a, b) {
1842
+ const len = Math.min(a.length, b.length);
1843
+ let dot = 0;
1844
+ let magA = 0;
1845
+ let magB = 0;
1846
+ for (let i = 0; i < len; i++) {
1847
+ dot += a[i] * b[i];
1848
+ magA += a[i] * a[i];
1849
+ magB += b[i] * b[i];
1850
+ }
1851
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
1852
+ return denom === 0 ? 0 : dot / denom;
1853
+ }
1854
+ async function searchByEmbedding(adapter, table, queryText, config, topK, minScore, pkColumn = "id") {
1855
+ const queryVector = await config.embed(queryText);
1856
+ const stored = adapter.all(
1857
+ `SELECT "row_pk", "embedding" FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ?`,
1858
+ [table]
1859
+ );
1860
+ const scored = [];
1861
+ for (const entry of stored) {
1862
+ const vec = JSON.parse(entry.embedding);
1863
+ const score = cosineSimilarity(queryVector, vec);
1864
+ if (score >= minScore) {
1865
+ scored.push({ pk: entry.row_pk, score });
1866
+ }
1867
+ }
1868
+ scored.sort((a, b) => b.score - a.score);
1869
+ const topResults = scored.slice(0, topK);
1870
+ const results = [];
1871
+ for (const { pk, score } of topResults) {
1872
+ const row = adapter.get(`SELECT * FROM "${table}" WHERE "${pkColumn}" = ?`, [pk]);
1873
+ if (row) {
1874
+ results.push({ row, score });
1875
+ }
1876
+ }
1877
+ return results;
1878
+ }
1879
+
1713
1880
  // src/lattice.ts
1714
1881
  var Lattice = class {
1715
1882
  _adapter;
@@ -1728,6 +1895,8 @@ var Lattice = class {
1728
1895
  _encryptedTableColumns = /* @__PURE__ */ new Map();
1729
1896
  /** Raw encryption key passphrase from constructor options. */
1730
1897
  _encryptionKeyRaw;
1898
+ /** Current task context string for relevance filtering. */
1899
+ _taskContext = "";
1731
1900
  _auditHandlers = [];
1732
1901
  _renderHandlers = [];
1733
1902
  _writebackHandlers = [];
@@ -1754,7 +1923,7 @@ var Lattice = class {
1754
1923
  this._adapter = new SQLiteAdapter(dbPath, adapterOpts);
1755
1924
  this._schema = new SchemaManager();
1756
1925
  this._sanitizer = new Sanitizer(options.security);
1757
- this._render = new RenderEngine(this._schema, this._adapter);
1926
+ this._render = new RenderEngine(this._schema, this._adapter, () => this._taskContext);
1758
1927
  this._reverseSync = new ReverseSyncEngine(this._schema, this._adapter);
1759
1928
  this._loop = new SyncLoop(this._render);
1760
1929
  this._writeback = new WritebackPipeline();
@@ -1778,8 +1947,10 @@ var Lattice = class {
1778
1947
  // -------------------------------------------------------------------------
1779
1948
  define(table, def) {
1780
1949
  this._assertNotInit("define");
1950
+ const columns = def.rewardTracking ? { ...def.columns, _reward_total: "REAL DEFAULT 0", _reward_count: "INTEGER DEFAULT 0" } : def.columns;
1781
1951
  const compiledDef = {
1782
1952
  ...def,
1953
+ columns,
1783
1954
  render: def.render ? compileRender(
1784
1955
  def,
1785
1956
  table,
@@ -1825,6 +1996,10 @@ var Lattice = class {
1825
1996
  const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1826
1997
  this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1827
1998
  }
1999
+ const hasEmbeddings = [...this._schema.getTables().values()].some((d) => d.embeddings);
2000
+ if (hasEmbeddings) {
2001
+ ensureEmbeddingsTable(this._adapter);
2002
+ }
1828
2003
  this._setupEncryption();
1829
2004
  this._initialized = true;
1830
2005
  return Promise.resolve();
@@ -1854,6 +2029,21 @@ var Lattice = class {
1854
2029
  this._initialized = false;
1855
2030
  }
1856
2031
  // -------------------------------------------------------------------------
2032
+ // Task context (for relevance filtering)
2033
+ // -------------------------------------------------------------------------
2034
+ /**
2035
+ * Set the current task context string. Tables with a `relevanceFilter`
2036
+ * will use this value to filter rows before rendering.
2037
+ */
2038
+ setTaskContext(context) {
2039
+ this._taskContext = context;
2040
+ return this;
2041
+ }
2042
+ /** Return the current task context string. */
2043
+ getTaskContext() {
2044
+ return this._taskContext;
2045
+ }
2046
+ // -------------------------------------------------------------------------
1857
2047
  // Encryption helpers
1858
2048
  // -------------------------------------------------------------------------
1859
2049
  _setupEncryption() {
@@ -1928,6 +2118,7 @@ var Lattice = class {
1928
2118
  const pkValue = rawPk != null ? String(rawPk) : "";
1929
2119
  this._sanitizer.emitAudit(table, "insert", pkValue);
1930
2120
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
2121
+ this._syncEmbedding(table, "insert", rowWithPk, pkValue);
1931
2122
  return Promise.resolve(pkValue);
1932
2123
  }
1933
2124
  /**
@@ -1995,6 +2186,11 @@ var Lattice = class {
1995
2186
  const auditId = typeof id === "string" ? id : JSON.stringify(id);
1996
2187
  this._sanitizer.emitAudit(table, "update", auditId);
1997
2188
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
2189
+ const def = this._schema.getTables().get(table);
2190
+ if (def?.embeddings) {
2191
+ const fullRow = this._adapter.get(`SELECT * FROM "${table}" WHERE ${clause}`, pkParams);
2192
+ if (fullRow) this._syncEmbedding(table, "update", fullRow, auditId);
2193
+ }
1998
2194
  return Promise.resolve();
1999
2195
  }
2000
2196
  /**
@@ -2016,6 +2212,7 @@ var Lattice = class {
2016
2212
  const auditId = typeof id === "string" ? id : JSON.stringify(id);
2017
2213
  this._sanitizer.emitAudit(table, "delete", auditId);
2018
2214
  this._fireWriteHooks(table, "delete", { id: auditId }, auditId);
2215
+ this._syncEmbedding(table, "delete", {}, auditId);
2019
2216
  return Promise.resolve();
2020
2217
  }
2021
2218
  get(table, id) {
@@ -2399,6 +2596,65 @@ var Lattice = class {
2399
2596
  const ms = unit === "h" ? num * 36e5 : unit === "d" ? num * 864e5 : num * 6e4;
2400
2597
  return new Date(Date.now() - ms).toISOString();
2401
2598
  }
2599
+ // -------------------------------------------------------------------------
2600
+ // Reward tracking
2601
+ // -------------------------------------------------------------------------
2602
+ /**
2603
+ * Update reward scores for a row. The total reward is recalculated as
2604
+ * the running average across all reward calls. Requires `rewardTracking`
2605
+ * on the table definition.
2606
+ */
2607
+ reward(table, id, scores) {
2608
+ const notInit = this._notInitError();
2609
+ if (notInit) return notInit;
2610
+ const def = this._schema.getTables().get(table);
2611
+ if (!def?.rewardTracking) {
2612
+ return Promise.reject(
2613
+ new Error(`Table "${table}" does not have rewardTracking enabled`)
2614
+ );
2615
+ }
2616
+ const vals = Object.values(scores);
2617
+ if (vals.length === 0) return Promise.resolve();
2618
+ const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
2619
+ const { clause, params: pkParams } = this._pkWhere(table, id);
2620
+ this._adapter.run(
2621
+ `UPDATE "${table}" SET "_reward_total" = ("_reward_total" * "_reward_count" + ?) / ("_reward_count" + 1), "_reward_count" = "_reward_count" + 1 WHERE ${clause}`,
2622
+ [avg, ...pkParams]
2623
+ );
2624
+ return Promise.resolve();
2625
+ }
2626
+ // -------------------------------------------------------------------------
2627
+ // Semantic search
2628
+ // -------------------------------------------------------------------------
2629
+ /**
2630
+ * Search for rows by semantic similarity. Requires `embeddings` config
2631
+ * on the table definition.
2632
+ *
2633
+ * @param table - Table to search
2634
+ * @param query - Natural-language query text
2635
+ * @param opts - Search options (topK, minScore)
2636
+ * @returns Matching rows with similarity scores, sorted best-first.
2637
+ */
2638
+ async search(table, query, opts = {}) {
2639
+ const notInit = this._notInitError();
2640
+ if (notInit) return notInit;
2641
+ const def = this._schema.getTables().get(table);
2642
+ if (!def?.embeddings) {
2643
+ return Promise.reject(
2644
+ new Error(`Table "${table}" does not have embeddings configured`)
2645
+ );
2646
+ }
2647
+ const pkCol = this._schema.getPrimaryKey(table)[0] ?? "id";
2648
+ return searchByEmbedding(
2649
+ this._adapter,
2650
+ table,
2651
+ query,
2652
+ def.embeddings,
2653
+ opts.topK ?? 10,
2654
+ opts.minScore ?? 0,
2655
+ pkCol
2656
+ );
2657
+ }
2402
2658
  query(table, opts = {}) {
2403
2659
  const notInit = this._notInitError();
2404
2660
  if (notInit) return notInit;
@@ -2657,6 +2913,23 @@ var Lattice = class {
2657
2913
  }
2658
2914
  }
2659
2915
  }
2916
+ /**
2917
+ * Update or remove the embedding for a row.
2918
+ * No-op if the table doesn't have `embeddings` configured.
2919
+ */
2920
+ _syncEmbedding(table, op, row, pk) {
2921
+ const def = this._schema.getTables().get(table);
2922
+ if (!def?.embeddings) return;
2923
+ if (op === "delete") {
2924
+ removeEmbedding(this._adapter, table, pk);
2925
+ return;
2926
+ }
2927
+ storeEmbedding(this._adapter, table, pk, row, def.embeddings).catch((err) => {
2928
+ for (const h of this._errorHandlers) {
2929
+ h(err instanceof Error ? err : new Error(String(err)));
2930
+ }
2931
+ });
2932
+ }
2660
2933
  _notInitError() {
2661
2934
  if (!this._initialized) {
2662
2935
  return Promise.reject(
@@ -3198,6 +3471,7 @@ async function autoUpdate(opts) {
3198
3471
  InMemoryStateStore,
3199
3472
  Lattice,
3200
3473
  READ_ONLY_HEADER,
3474
+ applyTokenBudget,
3201
3475
  applyWriteEntry,
3202
3476
  autoUpdate,
3203
3477
  contentHash,
@@ -3207,6 +3481,7 @@ async function autoUpdate(opts) {
3207
3481
  deriveKey,
3208
3482
  encrypt,
3209
3483
  entityFileNames,
3484
+ estimateTokens,
3210
3485
  fixSchemaConflicts,
3211
3486
  frontmatter,
3212
3487
  generateEntryId,
package/dist/index.d.cts CHANGED
@@ -812,6 +812,90 @@ interface TableDefinition {
812
812
  * ```
813
813
  */
814
814
  relations?: Record<string, Relation>;
815
+ /**
816
+ * Enable semantic search for this table via embeddings.
817
+ *
818
+ * When configured, Lattice computes and stores vector embeddings for
819
+ * the specified text fields. Use `lattice.search(table, query, opts)`
820
+ * to retrieve rows by semantic similarity.
821
+ *
822
+ * The `embed` function is called to generate vectors — bring your own
823
+ * embedding model (OpenAI, local model, etc.).
824
+ *
825
+ * @example
826
+ * ```ts
827
+ * embeddings: {
828
+ * fields: ['title', 'body'],
829
+ * embed: async (text) => openai.embeddings.create({ input: text, model: 'text-embedding-3-small' }).then(r => r.data[0].embedding),
830
+ * }
831
+ * ```
832
+ */
833
+ embeddings?: EmbeddingsConfig;
834
+ /**
835
+ * Enable reward tracking for this table. When `true`, Lattice
836
+ * auto-adds `_reward_total REAL DEFAULT 0` and `_reward_count INTEGER
837
+ * DEFAULT 0` columns. Rows are sorted by `_reward_total DESC` before
838
+ * rendering (unless overridden by `prioritizeBy`).
839
+ *
840
+ * Use `lattice.reward(table, id, scores)` to update reward values.
841
+ */
842
+ rewardTracking?: boolean;
843
+ /**
844
+ * When `rewardTracking` is enabled, automatically soft-delete rows
845
+ * whose `_reward_total` falls below this threshold during rendering.
846
+ * Requires a `deleted_at` column on the table. Default: no pruning.
847
+ */
848
+ pruneBelow?: number;
849
+ /**
850
+ * Pipeline of enrichment functions applied to rows after query and
851
+ * filtering but before rendering. Each function receives the row
852
+ * array and returns a (possibly transformed) row array.
853
+ *
854
+ * Use enrichment hooks to cluster, annotate, summarize, or
855
+ * cross-reference rows without modifying the underlying data.
856
+ *
857
+ * @example
858
+ * ```ts
859
+ * enrich: [
860
+ * (rows) => rows.map(r => ({ ...r, _age: Date.now() - new Date(r.created_at as string).getTime() })),
861
+ * (rows) => rows.length > 50 ? [{ summary: `${rows.length} items` }] : rows,
862
+ * ]
863
+ * ```
864
+ */
865
+ enrich?: ((rows: Row[]) => Row[])[];
866
+ /**
867
+ * Dynamic filter that scores rows against the current task context
868
+ * (set via `lattice.setTaskContext()`). Called before `filter` and
869
+ * before rendering. Only rows for which the function returns `true`
870
+ * are included.
871
+ *
872
+ * The second argument is the current task-context string (empty string
873
+ * when none has been set).
874
+ *
875
+ * @example
876
+ * ```ts
877
+ * relevanceFilter: (row, ctx) =>
878
+ * ctx ? String(row.body).toLowerCase().includes(ctx.toLowerCase()) : true
879
+ * ```
880
+ */
881
+ relevanceFilter?: (row: Row, taskContext: string) => boolean;
882
+ /**
883
+ * Maximum estimated token count for the rendered output.
884
+ * When the rendered content exceeds this budget, rows are pruned
885
+ * (lowest-priority first) and re-rendered with a truncation notice.
886
+ *
887
+ * Token count is estimated at ~4 characters per token.
888
+ */
889
+ tokenBudget?: number;
890
+ /**
891
+ * Controls row priority when `tokenBudget` forces pruning.
892
+ *
893
+ * - `string` — column name to sort by descending (highest value = highest priority).
894
+ * - `(a, b) => number` — custom comparator (positive = a has higher priority).
895
+ *
896
+ * When omitted, rows at the end of the query result are pruned first.
897
+ */
898
+ prioritizeBy?: string | ((a: Row, b: Row) => number);
815
899
  }
816
900
  interface MultiTableDefinition {
817
901
  /** Returns the "anchor" entities — one output file is produced per anchor */
@@ -823,6 +907,58 @@ interface MultiTableDefinition {
823
907
  /** Additional table names to query and pass into render */
824
908
  tables?: string[];
825
909
  }
910
+ /**
911
+ * Configuration for embedding-based semantic search on a table.
912
+ */
913
+ interface EmbeddingsConfig {
914
+ /** Column names whose values are concatenated and embedded. */
915
+ fields: string[];
916
+ /**
917
+ * Function that converts text into a numeric vector.
918
+ * Bring your own model — Lattice does not bundle an embedding provider.
919
+ */
920
+ embed: (text: string) => Promise<number[]>;
921
+ }
922
+ /**
923
+ * Options for `Lattice.search()`.
924
+ */
925
+ interface SearchOptions {
926
+ /** Maximum number of results to return. Default: 10. */
927
+ topK?: number;
928
+ /**
929
+ * Minimum cosine similarity threshold (0–1). Results below this
930
+ * score are excluded. Default: 0.
931
+ */
932
+ minScore?: number;
933
+ }
934
+ /**
935
+ * A single search result returned by `Lattice.search()`.
936
+ */
937
+ interface SearchResult {
938
+ /** The matched row from the source table. */
939
+ row: Row;
940
+ /** Cosine similarity score (0–1). */
941
+ score: number;
942
+ }
943
+ /**
944
+ * Dimension scores passed to `Lattice.reward()`.
945
+ * Values should be between 0 and 1. The total reward is the average
946
+ * of all provided dimension scores, accumulated over multiple calls.
947
+ */
948
+ interface RewardScores {
949
+ [dimension: string]: number;
950
+ }
951
+ /**
952
+ * Result of a writeback validation check.
953
+ */
954
+ interface WritebackValidationResult {
955
+ /** Whether the entry passed validation. */
956
+ pass: boolean;
957
+ /** Overall quality score (0–1). */
958
+ score: number;
959
+ /** Human-readable reason when validation fails. */
960
+ reason?: string;
961
+ }
826
962
  interface WritebackDefinition {
827
963
  /** Path or glob to agent-written files */
828
964
  file: string;
@@ -843,6 +979,25 @@ interface WritebackDefinition {
843
979
  stateStore?: WritebackStateStore;
844
980
  /** Called after entries are processed for a file. Useful for archival. */
845
981
  onArchive?: (filePath: string) => void;
982
+ /**
983
+ * Optional validation hook. Called before `persist` for each entry.
984
+ * Return `{ pass: true, score }` to allow the write, or
985
+ * `{ pass: false, score, reason }` to reject it.
986
+ *
987
+ * When omitted, all parsed entries are persisted without validation.
988
+ */
989
+ validate?: (entry: unknown) => WritebackValidationResult | Promise<WritebackValidationResult>;
990
+ /**
991
+ * Minimum score threshold. Entries with `score < rejectBelow` are
992
+ * automatically rejected even if `validate` returns `pass: true`.
993
+ * Only meaningful when `validate` is set. Default: 0 (no threshold).
994
+ */
995
+ rejectBelow?: number;
996
+ /**
997
+ * Called for each entry that fails validation.
998
+ * Useful for logging or auditing rejected writes.
999
+ */
1000
+ onReject?: (entry: unknown, result: WritebackValidationResult) => void;
846
1001
  }
847
1002
  interface QueryOptions {
848
1003
  /**
@@ -1154,6 +1309,8 @@ declare class Lattice {
1154
1309
  private readonly _encryptedTableColumns;
1155
1310
  /** Raw encryption key passphrase from constructor options. */
1156
1311
  private readonly _encryptionKeyRaw?;
1312
+ /** Current task context string for relevance filtering. */
1313
+ private _taskContext;
1157
1314
  private readonly _auditHandlers;
1158
1315
  private readonly _renderHandlers;
1159
1316
  private readonly _writebackHandlers;
@@ -1178,6 +1335,13 @@ declare class Lattice {
1178
1335
  */
1179
1336
  migrate(migrations: Migration[]): Promise<void>;
1180
1337
  close(): void;
1338
+ /**
1339
+ * Set the current task context string. Tables with a `relevanceFilter`
1340
+ * will use this value to filter rows before rendering.
1341
+ */
1342
+ setTaskContext(context: string): this;
1343
+ /** Return the current task context string. */
1344
+ getTaskContext(): string;
1181
1345
  private _setupEncryption;
1182
1346
  /** Encrypt applicable columns in a row before writing. Returns a new row. */
1183
1347
  private _encryptRow;
@@ -1258,6 +1422,22 @@ declare class Lattice {
1258
1422
  buildReport(config: ReportConfig): Promise<ReportResult>;
1259
1423
  /** Parse duration shorthand ('8h', '24h', '7d') into ISO timestamp. */
1260
1424
  private _resolveSince;
1425
+ /**
1426
+ * Update reward scores for a row. The total reward is recalculated as
1427
+ * the running average across all reward calls. Requires `rewardTracking`
1428
+ * on the table definition.
1429
+ */
1430
+ reward(table: string, id: PkLookup, scores: RewardScores): Promise<void>;
1431
+ /**
1432
+ * Search for rows by semantic similarity. Requires `embeddings` config
1433
+ * on the table definition.
1434
+ *
1435
+ * @param table - Table to search
1436
+ * @param query - Natural-language query text
1437
+ * @param opts - Search options (topK, minScore)
1438
+ * @returns Matching rows with similarity scores, sorted best-first.
1439
+ */
1440
+ search(table: string, query: string, opts?: SearchOptions): Promise<SearchResult[]>;
1261
1441
  query(table: string, opts?: QueryOptions): Promise<Row[]>;
1262
1442
  count(table: string, opts?: CountOptions): Promise<number>;
1263
1443
  render(outputDir: string): Promise<RenderResult>;
@@ -1297,6 +1477,11 @@ declare class Lattice {
1297
1477
  private _buildFilters;
1298
1478
  /** Returns a rejected Promise if not initialized; null if ready. */
1299
1479
  private _fireWriteHooks;
1480
+ /**
1481
+ * Update or remove the embedding for a row.
1482
+ * No-op if the table doesn't have `embeddings` configured.
1483
+ */
1484
+ private _syncEmbedding;
1300
1485
  private _notInitError;
1301
1486
  /**
1302
1487
  * Returns a rejected Promise if any of the given column names are not present
@@ -1512,6 +1697,25 @@ declare function parseConfigString(yamlContent: string, configDir: string): Pars
1512
1697
 
1513
1698
  declare function contentHash(content: string): string;
1514
1699
 
1700
+ /**
1701
+ * Estimate the number of tokens in a string using a fast heuristic.
1702
+ * Uses ~4 characters per token, a reasonable approximation for English
1703
+ * text with typical LLM tokenizers (BPE/ByteBPE).
1704
+ *
1705
+ * Users who need exact counts should pre-process with their own tokenizer
1706
+ * and use the `filter` hook to manually limit rows.
1707
+ */
1708
+ declare function estimateTokens(text: string): number;
1709
+ /**
1710
+ * Apply a token budget to rendered output.
1711
+ * If the full render exceeds the budget, rows are sorted by priority,
1712
+ * pruned via binary search, and re-rendered. A truncation footer is
1713
+ * appended so the consuming agent knows context was limited.
1714
+ *
1715
+ * @returns Final content string, possibly with truncation footer.
1716
+ */
1717
+ declare function applyTokenBudget(rows: Row[], renderFn: (rows: Row[]) => string, budget: number, prioritizeBy?: string | ((a: Row, b: Row) => number)): string;
1718
+
1515
1719
  /**
1516
1720
  * Fix legacy schema conflicts before Lattice init().
1517
1721
  *
@@ -1827,4 +2031,4 @@ declare function autoUpdate(opts?: {
1827
2031
  quiet?: boolean;
1828
2032
  }): Promise<AutoUpdateResult>;
1829
2033
 
1830
- export { type ApplyWriteResult, type AuditEvent, type AutoUpdateResult, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileManifestInfo, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, type Filter, type FilterOp, type HasManyRelation, type HasManySource, InMemoryStateStore, type InitOptions, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type LinkOptions, type ManyToManySource, type MarkdownTableColumn, type Migration, type MultiTableDefinition, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type ReportConfig, type ReportResult, type ReportSection, type ReportSectionResult, type ReverseSyncError, type ReverseSyncResult, type ReverseSyncUpdate, type Row, type SecurityOptions, type SeedConfig, type SeedLinkSpec, type SeedResult, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type UpsertByNaturalKeyOptions, type WatchOptions, type WriteHook, type WriteHookContext, type WritebackDefinition, type WritebackStateStore, applyWriteEntry, autoUpdate, contentHash, createReadOnlyHeader, createSQLiteStateStore, decrypt, deriveKey, encrypt, entityFileNames, fixSchemaConflicts, frontmatter, generateEntryId, generateWriteEntryId, isEncrypted, isV1EntityFiles, manifestPath, markdownTable, normalizeEntityFiles, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, slugify, truncate, validateEntryId, writeManifest };
2034
+ export { type ApplyWriteResult, type AuditEvent, type AutoUpdateResult, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type EmbeddingsConfig, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileManifestInfo, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, type Filter, type FilterOp, type HasManyRelation, type HasManySource, InMemoryStateStore, type InitOptions, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type LinkOptions, type ManyToManySource, type MarkdownTableColumn, type Migration, type MultiTableDefinition, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type ReportConfig, type ReportResult, type ReportSection, type ReportSectionResult, type ReverseSyncError, type ReverseSyncResult, type ReverseSyncUpdate, type RewardScores, type Row, type SearchOptions, type SearchResult, type SecurityOptions, type SeedConfig, type SeedLinkSpec, type SeedResult, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type UpsertByNaturalKeyOptions, type WatchOptions, type WriteHook, type WriteHookContext, type WritebackDefinition, type WritebackStateStore, type WritebackValidationResult, applyTokenBudget, applyWriteEntry, autoUpdate, contentHash, createReadOnlyHeader, createSQLiteStateStore, decrypt, deriveKey, encrypt, entityFileNames, estimateTokens, fixSchemaConflicts, frontmatter, generateEntryId, generateWriteEntryId, isEncrypted, isV1EntityFiles, manifestPath, markdownTable, normalizeEntityFiles, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, slugify, truncate, validateEntryId, writeManifest };