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 +139 -0
- package/dist/cli.js +265 -3
- package/dist/index.cjs +269 -3
- package/dist/index.d.cts +205 -1
- package/dist/index.d.ts +205 -1
- package/dist/index.js +267 -3
- package/package.json +2 -2
package/dist/index.d.ts
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 };
|
package/dist/index.js
CHANGED
|
@@ -331,6 +331,55 @@ var Sanitizer = class {
|
|
|
331
331
|
import { join as join4, basename, isAbsolute, resolve } from "path";
|
|
332
332
|
import { mkdirSync as mkdirSync2, existsSync as existsSync4, copyFileSync } from "fs";
|
|
333
333
|
|
|
334
|
+
// src/render/token-budget.ts
|
|
335
|
+
function estimateTokens(text) {
|
|
336
|
+
return Math.ceil(text.length / 4);
|
|
337
|
+
}
|
|
338
|
+
function applyTokenBudget(rows, renderFn, budget, prioritizeBy) {
|
|
339
|
+
const fullContent = renderFn(rows);
|
|
340
|
+
if (estimateTokens(fullContent) <= budget) return fullContent;
|
|
341
|
+
if (rows.length === 0) return fullContent;
|
|
342
|
+
const prioritized = [...rows];
|
|
343
|
+
if (typeof prioritizeBy === "function") {
|
|
344
|
+
prioritized.sort(prioritizeBy);
|
|
345
|
+
} else if (typeof prioritizeBy === "string") {
|
|
346
|
+
const col = prioritizeBy;
|
|
347
|
+
prioritized.sort((a, b) => {
|
|
348
|
+
const va = a[col];
|
|
349
|
+
const vb = b[col];
|
|
350
|
+
if (va == null && vb == null) return 0;
|
|
351
|
+
if (va == null) return 1;
|
|
352
|
+
if (vb == null) return -1;
|
|
353
|
+
if (va < vb) return 1;
|
|
354
|
+
if (va > vb) return -1;
|
|
355
|
+
return 0;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
let lo = 0;
|
|
359
|
+
let hi = prioritized.length;
|
|
360
|
+
let bestContent = "";
|
|
361
|
+
let bestCount = 0;
|
|
362
|
+
while (lo < hi) {
|
|
363
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
364
|
+
const content = renderFn(prioritized.slice(0, mid));
|
|
365
|
+
if (estimateTokens(content) <= budget) {
|
|
366
|
+
bestContent = content;
|
|
367
|
+
bestCount = mid;
|
|
368
|
+
lo = mid;
|
|
369
|
+
if (lo === hi) break;
|
|
370
|
+
} else {
|
|
371
|
+
hi = mid - 1;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (bestCount === 0) {
|
|
375
|
+
bestContent = renderFn([]);
|
|
376
|
+
}
|
|
377
|
+
const tokens = estimateTokens(bestContent);
|
|
378
|
+
return bestContent + `
|
|
379
|
+
|
|
380
|
+
[truncated: ${bestCount} of ${rows.length} rows rendered, ~${tokens} tokens]`;
|
|
381
|
+
}
|
|
382
|
+
|
|
334
383
|
// src/render/entity-query.ts
|
|
335
384
|
var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
|
|
336
385
|
function effectiveFilters(opts) {
|
|
@@ -765,9 +814,11 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
|
|
|
765
814
|
var RenderEngine = class {
|
|
766
815
|
_schema;
|
|
767
816
|
_adapter;
|
|
768
|
-
|
|
817
|
+
_getTaskContext;
|
|
818
|
+
constructor(schema, adapter, getTaskContext) {
|
|
769
819
|
this._schema = schema;
|
|
770
820
|
this._adapter = adapter;
|
|
821
|
+
this._getTaskContext = getTaskContext ?? (() => "");
|
|
771
822
|
}
|
|
772
823
|
async render(outputDir) {
|
|
773
824
|
const start = Date.now();
|
|
@@ -775,8 +826,38 @@ var RenderEngine = class {
|
|
|
775
826
|
const counters = { skipped: 0 };
|
|
776
827
|
for (const [name, def] of this._schema.getTables()) {
|
|
777
828
|
let rows = this._schema.queryTable(this._adapter, name);
|
|
829
|
+
if (def.relevanceFilter) {
|
|
830
|
+
const ctx = this._getTaskContext();
|
|
831
|
+
rows = rows.filter((row) => def.relevanceFilter(row, ctx));
|
|
832
|
+
}
|
|
778
833
|
if (def.filter) rows = def.filter(rows);
|
|
779
|
-
|
|
834
|
+
if (def.rewardTracking) {
|
|
835
|
+
if (def.pruneBelow !== void 0) {
|
|
836
|
+
const threshold = def.pruneBelow;
|
|
837
|
+
const toPrune = rows.filter(
|
|
838
|
+
(r) => r._reward_count > 0 && r._reward_total < threshold
|
|
839
|
+
);
|
|
840
|
+
if (toPrune.length > 0) {
|
|
841
|
+
for (const r of toPrune) {
|
|
842
|
+
const pkCol = this._schema.getPrimaryKey(name)[0] ?? "id";
|
|
843
|
+
this._adapter.run(
|
|
844
|
+
`UPDATE "${name}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`,
|
|
845
|
+
[r[pkCol]]
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
rows = rows.filter(
|
|
849
|
+
(r) => r._reward_count === 0 || r._reward_total >= threshold
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (!def.prioritizeBy) {
|
|
854
|
+
rows.sort((a, b) => (b._reward_total ?? 0) - (a._reward_total ?? 0));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (def.enrich) {
|
|
858
|
+
for (const fn of def.enrich) rows = fn(rows);
|
|
859
|
+
}
|
|
860
|
+
const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
|
|
780
861
|
const filePath = join4(outputDir, def.outputFile);
|
|
781
862
|
if (atomicWrite(filePath, content)) {
|
|
782
863
|
filesWritten.push(filePath);
|
|
@@ -1236,6 +1317,14 @@ var WritebackPipeline = class {
|
|
|
1236
1317
|
if (store.isSeen(filePath, key)) continue;
|
|
1237
1318
|
store.markSeen(filePath, key);
|
|
1238
1319
|
}
|
|
1320
|
+
if (def.validate) {
|
|
1321
|
+
const result = await def.validate(entry);
|
|
1322
|
+
const threshold = def.rejectBelow ?? 0;
|
|
1323
|
+
if (!result.pass || result.score < threshold) {
|
|
1324
|
+
def.onReject?.(entry, result);
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1239
1328
|
await def.persist(entry, filePath);
|
|
1240
1329
|
processed++;
|
|
1241
1330
|
}
|
|
@@ -1642,6 +1731,73 @@ function resolveEncryptedColumns(encrypted, allColumns) {
|
|
|
1642
1731
|
return new Set(allColumns.filter((c) => !SKIP_COLUMNS.has(c)));
|
|
1643
1732
|
}
|
|
1644
1733
|
|
|
1734
|
+
// src/search/embeddings.ts
|
|
1735
|
+
var EMBEDDINGS_TABLE = "_lattice_embeddings";
|
|
1736
|
+
function ensureEmbeddingsTable(adapter) {
|
|
1737
|
+
adapter.run(`CREATE TABLE IF NOT EXISTS "${EMBEDDINGS_TABLE}" (
|
|
1738
|
+
"table_name" TEXT NOT NULL,
|
|
1739
|
+
"row_pk" TEXT NOT NULL,
|
|
1740
|
+
"embedding" TEXT NOT NULL,
|
|
1741
|
+
PRIMARY KEY ("table_name", "row_pk")
|
|
1742
|
+
)`);
|
|
1743
|
+
}
|
|
1744
|
+
async function storeEmbedding(adapter, table, pk, row, config) {
|
|
1745
|
+
const text = config.fields.map((f) => {
|
|
1746
|
+
const v = row[f];
|
|
1747
|
+
return v == null ? "" : String(v);
|
|
1748
|
+
}).filter((s) => s.length > 0).join(" ");
|
|
1749
|
+
if (text.length === 0) return;
|
|
1750
|
+
const vector = await config.embed(text);
|
|
1751
|
+
adapter.run(
|
|
1752
|
+
`INSERT OR REPLACE INTO "${EMBEDDINGS_TABLE}" ("table_name", "row_pk", "embedding") VALUES (?, ?, ?)`,
|
|
1753
|
+
[table, pk, JSON.stringify(vector)]
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
function removeEmbedding(adapter, table, pk) {
|
|
1757
|
+
adapter.run(
|
|
1758
|
+
`DELETE FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ? AND "row_pk" = ?`,
|
|
1759
|
+
[table, pk]
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
function cosineSimilarity(a, b) {
|
|
1763
|
+
const len = Math.min(a.length, b.length);
|
|
1764
|
+
let dot = 0;
|
|
1765
|
+
let magA = 0;
|
|
1766
|
+
let magB = 0;
|
|
1767
|
+
for (let i = 0; i < len; i++) {
|
|
1768
|
+
dot += a[i] * b[i];
|
|
1769
|
+
magA += a[i] * a[i];
|
|
1770
|
+
magB += b[i] * b[i];
|
|
1771
|
+
}
|
|
1772
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
1773
|
+
return denom === 0 ? 0 : dot / denom;
|
|
1774
|
+
}
|
|
1775
|
+
async function searchByEmbedding(adapter, table, queryText, config, topK, minScore, pkColumn = "id") {
|
|
1776
|
+
const queryVector = await config.embed(queryText);
|
|
1777
|
+
const stored = adapter.all(
|
|
1778
|
+
`SELECT "row_pk", "embedding" FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ?`,
|
|
1779
|
+
[table]
|
|
1780
|
+
);
|
|
1781
|
+
const scored = [];
|
|
1782
|
+
for (const entry of stored) {
|
|
1783
|
+
const vec = JSON.parse(entry.embedding);
|
|
1784
|
+
const score = cosineSimilarity(queryVector, vec);
|
|
1785
|
+
if (score >= minScore) {
|
|
1786
|
+
scored.push({ pk: entry.row_pk, score });
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1790
|
+
const topResults = scored.slice(0, topK);
|
|
1791
|
+
const results = [];
|
|
1792
|
+
for (const { pk, score } of topResults) {
|
|
1793
|
+
const row = adapter.get(`SELECT * FROM "${table}" WHERE "${pkColumn}" = ?`, [pk]);
|
|
1794
|
+
if (row) {
|
|
1795
|
+
results.push({ row, score });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return results;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1645
1801
|
// src/lattice.ts
|
|
1646
1802
|
var Lattice = class {
|
|
1647
1803
|
_adapter;
|
|
@@ -1660,6 +1816,8 @@ var Lattice = class {
|
|
|
1660
1816
|
_encryptedTableColumns = /* @__PURE__ */ new Map();
|
|
1661
1817
|
/** Raw encryption key passphrase from constructor options. */
|
|
1662
1818
|
_encryptionKeyRaw;
|
|
1819
|
+
/** Current task context string for relevance filtering. */
|
|
1820
|
+
_taskContext = "";
|
|
1663
1821
|
_auditHandlers = [];
|
|
1664
1822
|
_renderHandlers = [];
|
|
1665
1823
|
_writebackHandlers = [];
|
|
@@ -1686,7 +1844,7 @@ var Lattice = class {
|
|
|
1686
1844
|
this._adapter = new SQLiteAdapter(dbPath, adapterOpts);
|
|
1687
1845
|
this._schema = new SchemaManager();
|
|
1688
1846
|
this._sanitizer = new Sanitizer(options.security);
|
|
1689
|
-
this._render = new RenderEngine(this._schema, this._adapter);
|
|
1847
|
+
this._render = new RenderEngine(this._schema, this._adapter, () => this._taskContext);
|
|
1690
1848
|
this._reverseSync = new ReverseSyncEngine(this._schema, this._adapter);
|
|
1691
1849
|
this._loop = new SyncLoop(this._render);
|
|
1692
1850
|
this._writeback = new WritebackPipeline();
|
|
@@ -1710,8 +1868,10 @@ var Lattice = class {
|
|
|
1710
1868
|
// -------------------------------------------------------------------------
|
|
1711
1869
|
define(table, def) {
|
|
1712
1870
|
this._assertNotInit("define");
|
|
1871
|
+
const columns = def.rewardTracking ? { ...def.columns, _reward_total: "REAL DEFAULT 0", _reward_count: "INTEGER DEFAULT 0" } : def.columns;
|
|
1713
1872
|
const compiledDef = {
|
|
1714
1873
|
...def,
|
|
1874
|
+
columns,
|
|
1715
1875
|
render: def.render ? compileRender(
|
|
1716
1876
|
def,
|
|
1717
1877
|
table,
|
|
@@ -1757,6 +1917,10 @@ var Lattice = class {
|
|
|
1757
1917
|
const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
|
|
1758
1918
|
this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
|
|
1759
1919
|
}
|
|
1920
|
+
const hasEmbeddings = [...this._schema.getTables().values()].some((d) => d.embeddings);
|
|
1921
|
+
if (hasEmbeddings) {
|
|
1922
|
+
ensureEmbeddingsTable(this._adapter);
|
|
1923
|
+
}
|
|
1760
1924
|
this._setupEncryption();
|
|
1761
1925
|
this._initialized = true;
|
|
1762
1926
|
return Promise.resolve();
|
|
@@ -1786,6 +1950,21 @@ var Lattice = class {
|
|
|
1786
1950
|
this._initialized = false;
|
|
1787
1951
|
}
|
|
1788
1952
|
// -------------------------------------------------------------------------
|
|
1953
|
+
// Task context (for relevance filtering)
|
|
1954
|
+
// -------------------------------------------------------------------------
|
|
1955
|
+
/**
|
|
1956
|
+
* Set the current task context string. Tables with a `relevanceFilter`
|
|
1957
|
+
* will use this value to filter rows before rendering.
|
|
1958
|
+
*/
|
|
1959
|
+
setTaskContext(context) {
|
|
1960
|
+
this._taskContext = context;
|
|
1961
|
+
return this;
|
|
1962
|
+
}
|
|
1963
|
+
/** Return the current task context string. */
|
|
1964
|
+
getTaskContext() {
|
|
1965
|
+
return this._taskContext;
|
|
1966
|
+
}
|
|
1967
|
+
// -------------------------------------------------------------------------
|
|
1789
1968
|
// Encryption helpers
|
|
1790
1969
|
// -------------------------------------------------------------------------
|
|
1791
1970
|
_setupEncryption() {
|
|
@@ -1860,6 +2039,7 @@ var Lattice = class {
|
|
|
1860
2039
|
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
1861
2040
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
1862
2041
|
this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
2042
|
+
this._syncEmbedding(table, "insert", rowWithPk, pkValue);
|
|
1863
2043
|
return Promise.resolve(pkValue);
|
|
1864
2044
|
}
|
|
1865
2045
|
/**
|
|
@@ -1927,6 +2107,11 @@ var Lattice = class {
|
|
|
1927
2107
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1928
2108
|
this._sanitizer.emitAudit(table, "update", auditId);
|
|
1929
2109
|
this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
2110
|
+
const def = this._schema.getTables().get(table);
|
|
2111
|
+
if (def?.embeddings) {
|
|
2112
|
+
const fullRow = this._adapter.get(`SELECT * FROM "${table}" WHERE ${clause}`, pkParams);
|
|
2113
|
+
if (fullRow) this._syncEmbedding(table, "update", fullRow, auditId);
|
|
2114
|
+
}
|
|
1930
2115
|
return Promise.resolve();
|
|
1931
2116
|
}
|
|
1932
2117
|
/**
|
|
@@ -1948,6 +2133,7 @@ var Lattice = class {
|
|
|
1948
2133
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1949
2134
|
this._sanitizer.emitAudit(table, "delete", auditId);
|
|
1950
2135
|
this._fireWriteHooks(table, "delete", { id: auditId }, auditId);
|
|
2136
|
+
this._syncEmbedding(table, "delete", {}, auditId);
|
|
1951
2137
|
return Promise.resolve();
|
|
1952
2138
|
}
|
|
1953
2139
|
get(table, id) {
|
|
@@ -2331,6 +2517,65 @@ var Lattice = class {
|
|
|
2331
2517
|
const ms = unit === "h" ? num * 36e5 : unit === "d" ? num * 864e5 : num * 6e4;
|
|
2332
2518
|
return new Date(Date.now() - ms).toISOString();
|
|
2333
2519
|
}
|
|
2520
|
+
// -------------------------------------------------------------------------
|
|
2521
|
+
// Reward tracking
|
|
2522
|
+
// -------------------------------------------------------------------------
|
|
2523
|
+
/**
|
|
2524
|
+
* Update reward scores for a row. The total reward is recalculated as
|
|
2525
|
+
* the running average across all reward calls. Requires `rewardTracking`
|
|
2526
|
+
* on the table definition.
|
|
2527
|
+
*/
|
|
2528
|
+
reward(table, id, scores) {
|
|
2529
|
+
const notInit = this._notInitError();
|
|
2530
|
+
if (notInit) return notInit;
|
|
2531
|
+
const def = this._schema.getTables().get(table);
|
|
2532
|
+
if (!def?.rewardTracking) {
|
|
2533
|
+
return Promise.reject(
|
|
2534
|
+
new Error(`Table "${table}" does not have rewardTracking enabled`)
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
const vals = Object.values(scores);
|
|
2538
|
+
if (vals.length === 0) return Promise.resolve();
|
|
2539
|
+
const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
2540
|
+
const { clause, params: pkParams } = this._pkWhere(table, id);
|
|
2541
|
+
this._adapter.run(
|
|
2542
|
+
`UPDATE "${table}" SET "_reward_total" = ("_reward_total" * "_reward_count" + ?) / ("_reward_count" + 1), "_reward_count" = "_reward_count" + 1 WHERE ${clause}`,
|
|
2543
|
+
[avg, ...pkParams]
|
|
2544
|
+
);
|
|
2545
|
+
return Promise.resolve();
|
|
2546
|
+
}
|
|
2547
|
+
// -------------------------------------------------------------------------
|
|
2548
|
+
// Semantic search
|
|
2549
|
+
// -------------------------------------------------------------------------
|
|
2550
|
+
/**
|
|
2551
|
+
* Search for rows by semantic similarity. Requires `embeddings` config
|
|
2552
|
+
* on the table definition.
|
|
2553
|
+
*
|
|
2554
|
+
* @param table - Table to search
|
|
2555
|
+
* @param query - Natural-language query text
|
|
2556
|
+
* @param opts - Search options (topK, minScore)
|
|
2557
|
+
* @returns Matching rows with similarity scores, sorted best-first.
|
|
2558
|
+
*/
|
|
2559
|
+
async search(table, query, opts = {}) {
|
|
2560
|
+
const notInit = this._notInitError();
|
|
2561
|
+
if (notInit) return notInit;
|
|
2562
|
+
const def = this._schema.getTables().get(table);
|
|
2563
|
+
if (!def?.embeddings) {
|
|
2564
|
+
return Promise.reject(
|
|
2565
|
+
new Error(`Table "${table}" does not have embeddings configured`)
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
const pkCol = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
2569
|
+
return searchByEmbedding(
|
|
2570
|
+
this._adapter,
|
|
2571
|
+
table,
|
|
2572
|
+
query,
|
|
2573
|
+
def.embeddings,
|
|
2574
|
+
opts.topK ?? 10,
|
|
2575
|
+
opts.minScore ?? 0,
|
|
2576
|
+
pkCol
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2334
2579
|
query(table, opts = {}) {
|
|
2335
2580
|
const notInit = this._notInitError();
|
|
2336
2581
|
if (notInit) return notInit;
|
|
@@ -2589,6 +2834,23 @@ var Lattice = class {
|
|
|
2589
2834
|
}
|
|
2590
2835
|
}
|
|
2591
2836
|
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Update or remove the embedding for a row.
|
|
2839
|
+
* No-op if the table doesn't have `embeddings` configured.
|
|
2840
|
+
*/
|
|
2841
|
+
_syncEmbedding(table, op, row, pk) {
|
|
2842
|
+
const def = this._schema.getTables().get(table);
|
|
2843
|
+
if (!def?.embeddings) return;
|
|
2844
|
+
if (op === "delete") {
|
|
2845
|
+
removeEmbedding(this._adapter, table, pk);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
storeEmbedding(this._adapter, table, pk, row, def.embeddings).catch((err) => {
|
|
2849
|
+
for (const h of this._errorHandlers) {
|
|
2850
|
+
h(err instanceof Error ? err : new Error(String(err)));
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2592
2854
|
_notInitError() {
|
|
2593
2855
|
if (!this._initialized) {
|
|
2594
2856
|
return Promise.reject(
|
|
@@ -3129,6 +3391,7 @@ export {
|
|
|
3129
3391
|
InMemoryStateStore,
|
|
3130
3392
|
Lattice,
|
|
3131
3393
|
READ_ONLY_HEADER,
|
|
3394
|
+
applyTokenBudget,
|
|
3132
3395
|
applyWriteEntry,
|
|
3133
3396
|
autoUpdate,
|
|
3134
3397
|
contentHash,
|
|
@@ -3138,6 +3401,7 @@ export {
|
|
|
3138
3401
|
deriveKey,
|
|
3139
3402
|
encrypt,
|
|
3140
3403
|
entityFileNames,
|
|
3404
|
+
estimateTokens,
|
|
3141
3405
|
fixSchemaConflicts,
|
|
3142
3406
|
frontmatter,
|
|
3143
3407
|
generateEntryId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latticesql",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Persistent structured memory for AI agent systems — SQLite ↔ LLM context bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"better-sqlite3": "^12.8.0",
|
|
46
|
-
"latticesql": "^1.2.
|
|
46
|
+
"latticesql": "^1.2.5",
|
|
47
47
|
"uuid": "^13.0.0",
|
|
48
48
|
"yaml": "^2.8.3"
|
|
49
49
|
},
|