latticesql 3.1.0 → 3.2.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/README.md +4 -0
- package/dist/cli.js +2174 -1016
- package/dist/index.cjs +393 -190
- package/dist/index.d.cts +15 -20
- package/dist/index.d.ts +15 -20
- package/dist/index.js +393 -190
- package/docs/api-reference.md +1370 -0
- package/docs/architecture.md +331 -0
- package/docs/assistant.md +138 -0
- package/docs/cli.md +515 -0
- package/docs/cloud.md +675 -0
- package/docs/collaboration.md +85 -0
- package/docs/configuration.md +416 -0
- package/docs/entity-context.md +510 -0
- package/docs/examples/agent-system.md +313 -0
- package/docs/examples/cms.md +366 -0
- package/docs/examples/ticket-tracker.md +313 -0
- package/docs/migrations.md +272 -0
- package/docs/templates.md +338 -0
- package/docs/workspaces.md +81 -0
- package/package.json +3 -2
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for all public classes, methods, and types exported by `latticesql`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Class: `Lattice`](#class-lattice)
|
|
10
|
+
- [Constructor](#constructor)
|
|
11
|
+
- [Setup Methods](#setup-methods)
|
|
12
|
+
- [CRUD Methods](#crud-methods)
|
|
13
|
+
- [Sync Methods](#sync-methods)
|
|
14
|
+
- [Events](#events)
|
|
15
|
+
- [Escape Hatch](#escape-hatch)
|
|
16
|
+
- [Functions](#functions)
|
|
17
|
+
- [`parseConfigFile()`](#parseconfigfile)
|
|
18
|
+
- [`parseConfigString()`](#parseconfigstring)
|
|
19
|
+
- [Types](#types)
|
|
20
|
+
- [`Row`](#row)
|
|
21
|
+
- [`LatticeOptions`](#latticeoptions)
|
|
22
|
+
- [`SecurityOptions`](#securityoptions)
|
|
23
|
+
- [`TableDefinition`](#tabledefinition)
|
|
24
|
+
- [`MultiTableDefinition`](#multitabledefinition)
|
|
25
|
+
- [`WritebackDefinition`](#writebackdefinition)
|
|
26
|
+
- [`QueryOptions`](#queryoptions)
|
|
27
|
+
- [`CountOptions`](#countoptions)
|
|
28
|
+
- [`Filter` and `FilterOp`](#filter-and-filterop)
|
|
29
|
+
- [`InitOptions` and `Migration`](#initoptions-and-migration)
|
|
30
|
+
- [`WatchOptions`](#watchoptions)
|
|
31
|
+
- [`RenderResult` and `SyncResult`](#renderresult-and-syncresult)
|
|
32
|
+
- [`AuditEvent`](#auditevent)
|
|
33
|
+
- [`PkLookup`](#pklookup)
|
|
34
|
+
- [`PrimaryKey`](#primarykey)
|
|
35
|
+
- [`Relation` types](#relation-types)
|
|
36
|
+
- [Render types](#render-types)
|
|
37
|
+
- [Config types](#config-types)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Class: `Lattice`
|
|
42
|
+
|
|
43
|
+
The main entry point. Manages a SQLite database, registered table schemas, sync/render cycle, and optional writeback pipeline.
|
|
44
|
+
|
|
45
|
+
### Constructor
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
new Lattice(path: string, options?: LatticeOptions): Lattice
|
|
49
|
+
new Lattice(config: LatticeConfigInput, options?: LatticeOptions): Lattice
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Form 1 — explicit path:**
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const db = new Lattice('./data/app.db');
|
|
56
|
+
const db = new Lattice(':memory:');
|
|
57
|
+
const db = new Lattice('./data/app.db', { wal: true, busyTimeout: 5000 });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Form 2 — YAML config:**
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const db = new Lattice({ config: './lattice.config.yml' });
|
|
64
|
+
const db = new Lattice({ config: './lattice.config.yml', options: { wal: true } });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
When using the config form, Lattice reads the YAML file, resolves the `db` path relative to the config file, and calls `define()` for every entity automatically. No manual `define()` calls are needed.
|
|
68
|
+
|
|
69
|
+
**Parameters:**
|
|
70
|
+
|
|
71
|
+
| Parameter | Type | Description |
|
|
72
|
+
| --------- | -------------------- | ------------------------------------------------------------------ |
|
|
73
|
+
| `path` | `string` | Path to the SQLite file, or `':memory:'` for an in-memory database |
|
|
74
|
+
| `config` | `LatticeConfigInput` | Object with a `config` path to a `lattice.config.yml` file |
|
|
75
|
+
| `options` | `LatticeOptions` | Optional runtime configuration |
|
|
76
|
+
|
|
77
|
+
**`LatticeOptions`:**
|
|
78
|
+
|
|
79
|
+
| Option | Type | Default | Description |
|
|
80
|
+
| ------------------ | ----------------- | ------- | -------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `wal` | `boolean` | `false` | Enable WAL journal mode (recommended for concurrent read/write) |
|
|
82
|
+
| `busyTimeout` | `number` | – | SQLite busy timeout in milliseconds |
|
|
83
|
+
| `renderSkipsEmpty` | `boolean` | `false` | On `render()`, skip the full-table read + file write for tables registered without a `render` spec |
|
|
84
|
+
| `security` | `SecurityOptions` | – | Input sanitization and audit options |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Setup Methods
|
|
89
|
+
|
|
90
|
+
#### `define(table, definition): this`
|
|
91
|
+
|
|
92
|
+
Register a table schema before calling `init()`. Returns `this` for chaining.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
db.define('tasks', {
|
|
96
|
+
columns: {
|
|
97
|
+
id: 'TEXT PRIMARY KEY',
|
|
98
|
+
title: 'TEXT NOT NULL',
|
|
99
|
+
status: "TEXT DEFAULT 'open'",
|
|
100
|
+
},
|
|
101
|
+
render: 'default-list',
|
|
102
|
+
outputFile: 'context/TASKS.md',
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Must be called **before** `init()`. Throws if called after `init()`.
|
|
107
|
+
|
|
108
|
+
**`TableDefinition`** fields:
|
|
109
|
+
|
|
110
|
+
| Field | Type | Required | Description |
|
|
111
|
+
| ------------------ | ---------------------------- | -------- | ------------------------------------------------------------------ |
|
|
112
|
+
| `columns` | `Record<string, string>` | yes | Column name → SQLite column spec |
|
|
113
|
+
| `render` | `RenderSpec` | yes | How to render rows into context text |
|
|
114
|
+
| `outputFile` | `string` | yes | Output file path (relative to `outputDir` in `render()`/`watch()`) |
|
|
115
|
+
| `filter` | `(rows: Row[]) => Row[]` | no | Pre-filter applied before render |
|
|
116
|
+
| `primaryKey` | `PrimaryKey` | no | Primary key column(s); defaults to `'id'` |
|
|
117
|
+
| `tableConstraints` | `string[]` | no | Table-level SQL constraints (e.g. composite PK) |
|
|
118
|
+
| `relations` | `Record<string, Relation>` | no | Declared foreign-key relationships |
|
|
119
|
+
| `embeddings` | `EmbeddingsConfig` | no | Enable semantic search via embeddings (v1.3+) |
|
|
120
|
+
| `rewardTracking` | `boolean` | no | Auto-add `_reward_total`/`_reward_count` columns (v1.3+) |
|
|
121
|
+
| `pruneBelow` | `number` | no | Soft-delete rows with reward below threshold (v1.3+) |
|
|
122
|
+
| `enrich` | `((rows: Row[]) => Row[])[]` | no | Row transform pipeline before rendering (v1.3+) |
|
|
123
|
+
| `relevanceFilter` | `(row, ctx) => boolean` | no | Filter by task context before rendering (v1.3+) |
|
|
124
|
+
| `tokenBudget` | `number` | no | Max estimated tokens for rendered output (v1.3+) |
|
|
125
|
+
| `prioritizeBy` | `string \| comparator` | no | Row priority when token budget prunes (v1.3+) |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
#### `defineMulti(name, definition): this`
|
|
130
|
+
|
|
131
|
+
Register a multi-table view that produces one output file per "anchor" row.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
db.defineMulti('agent-context', {
|
|
135
|
+
keys: async () => db.query('agents'),
|
|
136
|
+
outputFile: (agent) => `agents/${agent.slug}/CONTEXT.md`,
|
|
137
|
+
render: (agent, tables) => {
|
|
138
|
+
const tasks = tables.tasks ?? [];
|
|
139
|
+
return `# ${agent.name}\n\n${tasks.map((t) => `- ${t.title}`).join('\n')}`;
|
|
140
|
+
},
|
|
141
|
+
tables: ['tasks'],
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**`MultiTableDefinition`** fields:
|
|
146
|
+
|
|
147
|
+
| Field | Type | Description |
|
|
148
|
+
| ------------ | ----------------------------------------------------- | ------------------------------------------------------ |
|
|
149
|
+
| `keys` | `() => Promise<Row[]>` | Returns the anchor rows; one output file per row |
|
|
150
|
+
| `outputFile` | `(key: Row) => string` | Derive output file path from an anchor row |
|
|
151
|
+
| `render` | `(key: Row, tables: Record<string, Row[]>) => string` | Produce the file content |
|
|
152
|
+
| `tables` | `string[]` | Additional table names to query and pass into `render` |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
#### `defineEntityContext(table, definition): this`
|
|
157
|
+
|
|
158
|
+
Register an entity context definition for a table. Must be called **before** `init()`. Returns `this` for chaining.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
db.defineEntityContext('agent', {
|
|
162
|
+
slug: (row) => row.slug as string,
|
|
163
|
+
index: {
|
|
164
|
+
outputFile: 'AGENTS.md',
|
|
165
|
+
render: (rows) => rows.map((r) => `- [${r.name}](${r.slug}/)`).join('\n'),
|
|
166
|
+
},
|
|
167
|
+
files: [
|
|
168
|
+
{
|
|
169
|
+
filename: 'AGENT.md',
|
|
170
|
+
source: { type: 'self' },
|
|
171
|
+
render: (rows) => `# ${rows[0]?.name}\n\n${rows[0]?.bio ?? ''}`,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
filename: 'TASKS.md',
|
|
175
|
+
source: { type: 'hasMany', table: 'task', foreignKey: 'agent_id' },
|
|
176
|
+
render: (rows) => rows.map((r) => `- [ ] ${r.title}`).join('\n'),
|
|
177
|
+
omitIfEmpty: true,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
filename: 'SKILLS.md',
|
|
181
|
+
source: {
|
|
182
|
+
type: 'manyToMany',
|
|
183
|
+
junctionTable: 'agent_skill',
|
|
184
|
+
localKey: 'agent_id',
|
|
185
|
+
remoteKey: 'skill_id',
|
|
186
|
+
remoteTable: 'skill',
|
|
187
|
+
},
|
|
188
|
+
render: (rows) => rows.map((r) => `- ${r.name}`).join('\n'),
|
|
189
|
+
omitIfEmpty: true,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
combined: {
|
|
193
|
+
outputFile: 'CONTEXT.md',
|
|
194
|
+
},
|
|
195
|
+
directoryRoot: 'agents',
|
|
196
|
+
protectedFiles: ['SESSION.md'],
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**`EntityContextDefinition`** fields:
|
|
201
|
+
|
|
202
|
+
| Field | Type | Required | Description |
|
|
203
|
+
| ---------------- | -------------------------- | -------- | ------------------------------------------------------------------------------- |
|
|
204
|
+
| `slug` | `(row: Row) => string` | yes | Derive the per-entity directory name from the entity row |
|
|
205
|
+
| `index` | `{ outputFile, render }` | no | A single index file written at the `directoryRoot` level listing all entities |
|
|
206
|
+
| `files` | `EntityFileSpec[]` | yes | One or more per-entity files to generate inside each entity's subdirectory |
|
|
207
|
+
| `combined` | `{ outputFile, exclude? }` | no | Concatenate all rendered files into a single combined file per entity |
|
|
208
|
+
| `directory` | `(row: Row) => string` | no | Override the default `{directoryRoot}/{slug}` directory path for an entity |
|
|
209
|
+
| `directoryRoot` | `string` | no | Root directory Lattice owns; defaults to the table name. Used by orphan cleanup |
|
|
210
|
+
| `protectedFiles` | `string[]` | no | Filenames Lattice must never delete during orphan cleanup (e.g. `'SESSION.md'`) |
|
|
211
|
+
|
|
212
|
+
**`EntityFileSpec`** fields:
|
|
213
|
+
|
|
214
|
+
| Field | Type | Required | Description |
|
|
215
|
+
| ------------- | ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
216
|
+
| `filename` | `string` | yes | Output filename within the entity's subdirectory |
|
|
217
|
+
| `source` | `EntitySource` | yes | How to query rows for this file (see source types below) |
|
|
218
|
+
| `render` | `(rows: Row[]) => string` | yes | Render resolved rows to a string |
|
|
219
|
+
| `budget` | `number` | no | Max character count; truncated with a notice if exceeded |
|
|
220
|
+
| `omitIfEmpty` | `boolean` | no | Skip writing the file if the source returns zero rows |
|
|
221
|
+
| `reverseSync` | `(content: string, entityRow: Row) => ReverseSyncUpdate[]` | no | Parse external file edits back into DB updates (v0.16+). See [Reverse-Sync](./entity-context.md#reverse-sync-v016). |
|
|
222
|
+
|
|
223
|
+
**Source types:**
|
|
224
|
+
|
|
225
|
+
| Type | Required fields | Description |
|
|
226
|
+
| ------------ | ------------------------------------------------------- | ------------------------------------------------------ |
|
|
227
|
+
| `self` | _(none)_ | The entity row itself (always exactly one row) |
|
|
228
|
+
| `hasMany` | `table`, `foreignKey` | Rows on a related table where `foreignKey = entity.PK` |
|
|
229
|
+
| `manyToMany` | `junctionTable`, `localKey`, `remoteKey`, `remoteTable` | Rows from a remote table via a junction table |
|
|
230
|
+
| `belongsTo` | `table`, `foreignKey` | Single parent row: `related.PK = entity.foreignKey` |
|
|
231
|
+
| `custom` | `query: (row, adapter) => Row[]` | Fully custom synchronous query |
|
|
232
|
+
|
|
233
|
+
All source types accept an optional `references` field to override the default primary key column.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
#### `defineWriteback(definition): this`
|
|
238
|
+
|
|
239
|
+
Register a writeback pipeline: watch an agent-written file for new entries and persist them to the database.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
db.defineWriteback({
|
|
243
|
+
file: './context/INBOX.md',
|
|
244
|
+
parse: (content, fromOffset) => {
|
|
245
|
+
const newContent = content.slice(fromOffset);
|
|
246
|
+
const entries = newContent
|
|
247
|
+
.split('\n')
|
|
248
|
+
.filter((l) => l.startsWith('- '))
|
|
249
|
+
.map((l) => ({ text: l.slice(2) }));
|
|
250
|
+
return { entries, nextOffset: content.length };
|
|
251
|
+
},
|
|
252
|
+
persist: async (entry) => {
|
|
253
|
+
await db.insert('notes', { text: (entry as { text: string }).text });
|
|
254
|
+
},
|
|
255
|
+
dedupeKey: (entry) => (entry as { text: string }).text,
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**`WritebackDefinition`** fields:
|
|
260
|
+
|
|
261
|
+
| Field | Type | Description |
|
|
262
|
+
| ----------- | -------------------------------------------------- | --------------------------------------------------------------------- |
|
|
263
|
+
| `file` | `string` | Path or glob to the agent-written file(s) |
|
|
264
|
+
| `parse` | `(content, fromOffset) => { entries, nextOffset }` | Parse new content from the last-read offset |
|
|
265
|
+
| `persist` | `(entry, filePath) => Promise<void>` | Persist one parsed entry |
|
|
266
|
+
| `dedupeKey` | `(entry) => string` | Optional dedup key; entries with the same key are processed only once |
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
#### `init(options?): Promise<void>`
|
|
271
|
+
|
|
272
|
+
Open the database, apply schema (`CREATE TABLE IF NOT EXISTS` for all registered tables), and optionally run migrations.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
await db.init();
|
|
276
|
+
|
|
277
|
+
// With migrations:
|
|
278
|
+
await db.init({
|
|
279
|
+
migrations: [
|
|
280
|
+
{ version: 1, sql: 'ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 1' },
|
|
281
|
+
{ version: 2, sql: 'CREATE INDEX idx_tasks_status ON tasks (status)' },
|
|
282
|
+
],
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Must be called **once** before any CRUD or sync methods. Throws if called a second time.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
#### `defineLate(table, definition): Promise<this>`
|
|
291
|
+
|
|
292
|
+
Register a table **after** `init()` — creates it (`CREATE TABLE IF NOT EXISTS`) and adds it to the live schema registry. Idempotent: registering an already-registered table is a no-op. Use `define()` during setup; use `defineLate()` to add a table at runtime (e.g. a GUI/assistant-created object) without reopening.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
await db.defineLate('invoices', { columns: { id: 'TEXT PRIMARY KEY', total: 'REAL' } });
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
#### `unregisterTable(table): this` (v2.1+)
|
|
301
|
+
|
|
302
|
+
The inverse of `defineLate()`. Removes a table from the live schema registry so it stops being listed/queryable, **without** a reopen and **without** dropping the physical SQL table or its rows (the data is kept, so the removal can be reverted by re-registering). A no-op if the table isn't registered. Used by the GUI's soft table-delete.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
db.unregisterTable('invoices'); // forgets the registration; the SQL table + rows remain
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
#### `close(): void`
|
|
311
|
+
|
|
312
|
+
Close the underlying SQLite connection. After calling `close()`, CRUD and sync methods will reject.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
db.close();
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### CRUD Methods
|
|
321
|
+
|
|
322
|
+
All CRUD methods return `Promise<T>` but resolve synchronously (better-sqlite3 is synchronous under the hood). Calling any CRUD method before `init()` returns a rejected promise.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
#### `insert(table, row): Promise<string>`
|
|
327
|
+
|
|
328
|
+
Insert a new row. Returns the primary key value as a string.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const id = await db.insert('tasks', { title: 'Fix the bug', status: 'open' });
|
|
332
|
+
// → '3f2a1b...' (auto-generated UUID)
|
|
333
|
+
|
|
334
|
+
// Provide your own ID:
|
|
335
|
+
await db.insert('tasks', { id: 'task-1', title: 'Fix the bug' });
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
- If `primaryKey` is the default `'id'` and `id` is absent, a UUID v4 is auto-generated.
|
|
339
|
+
- For custom or composite PKs, all PK column values must be provided.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
#### `upsert(table, row): Promise<string>`
|
|
344
|
+
|
|
345
|
+
Insert or update using `INSERT ... ON CONFLICT DO UPDATE`. Returns the primary key value.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
await db.upsert('settings', { key: 'theme', value: 'dark' });
|
|
349
|
+
// Updates if 'theme' already exists, otherwise inserts.
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
PK columns are excluded from the `UPDATE SET` clause — only non-PK fields are overwritten.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
#### `upsertBy(table, col, val, row): Promise<string>`
|
|
357
|
+
|
|
358
|
+
Insert or update based on an arbitrary column (not the PK). Returns the PK value.
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
await db.upsertBy('users', 'email', 'alice@example.com', { name: 'Alice', role: 'admin' });
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Looks up an existing row by `col = val`. If found, calls `update()`; otherwise calls `insert()`.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
#### `update(table, id, row): Promise<void>`
|
|
369
|
+
|
|
370
|
+
Update one row identified by its primary key.
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
await db.update('tasks', 'task-1', { status: 'done' });
|
|
374
|
+
|
|
375
|
+
// Composite PK:
|
|
376
|
+
await db.update('line_items', { order_id: 'ord-1', seq: 2 }, { qty: 5 });
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
#### `delete(table, id): Promise<void>`
|
|
382
|
+
|
|
383
|
+
Delete one row identified by its primary key.
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
await db.delete('tasks', 'task-1');
|
|
387
|
+
|
|
388
|
+
// Composite PK:
|
|
389
|
+
await db.delete('line_items', { order_id: 'ord-1', seq: 2 });
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
#### `get(table, id): Promise<Row | null>`
|
|
395
|
+
|
|
396
|
+
Fetch one row by primary key. Returns `null` if not found.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
const task = await db.get('tasks', 'task-1');
|
|
400
|
+
if (task) {
|
|
401
|
+
console.log(task.title);
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
#### `query(table, options?): Promise<Row[]>`
|
|
408
|
+
|
|
409
|
+
Query rows with optional filtering, ordering, and pagination.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
// All rows:
|
|
413
|
+
const tasks = await db.query('tasks');
|
|
414
|
+
|
|
415
|
+
// With equality filter:
|
|
416
|
+
const open = await db.query('tasks', { where: { status: 'open' } });
|
|
417
|
+
|
|
418
|
+
// Advanced filters:
|
|
419
|
+
const highPriority = await db.query('tasks', {
|
|
420
|
+
filters: [
|
|
421
|
+
{ col: 'priority', op: 'gte', val: 3 },
|
|
422
|
+
{ col: 'deleted_at', op: 'isNull' },
|
|
423
|
+
],
|
|
424
|
+
orderBy: 'created_at',
|
|
425
|
+
orderDir: 'desc',
|
|
426
|
+
limit: 20,
|
|
427
|
+
offset: 0,
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**`QueryOptions`:**
|
|
432
|
+
|
|
433
|
+
| Option | Type | Description |
|
|
434
|
+
| ---------- | ------------------------- | --------------------------------- |
|
|
435
|
+
| `where` | `Record<string, unknown>` | Equality filter shorthand |
|
|
436
|
+
| `filters` | `Filter[]` | Advanced filter clauses |
|
|
437
|
+
| `orderBy` | `string` | Column to sort by |
|
|
438
|
+
| `orderDir` | `'asc' \| 'desc'` | Sort direction (default: `'asc'`) |
|
|
439
|
+
| `limit` | `number` | Max rows to return |
|
|
440
|
+
| `offset` | `number` | Skip N rows |
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
#### `count(table, options?): Promise<number>`
|
|
445
|
+
|
|
446
|
+
Count rows matching optional filters.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
const total = await db.count('tasks');
|
|
450
|
+
const open = await db.count('tasks', { where: { status: 'open' } });
|
|
451
|
+
const highPriority = await db.count('tasks', {
|
|
452
|
+
filters: [{ col: 'priority', op: 'gte', val: 3 }],
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Accepts the same `where` and `filters` options as `query()`.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
### Context & Search Methods (v1.3+)
|
|
461
|
+
|
|
462
|
+
#### `setTaskContext(context): this`
|
|
463
|
+
|
|
464
|
+
Set the current task context string. Tables with a `relevanceFilter` use this value to filter rows before rendering.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
db.setTaskContext('deployment issues');
|
|
468
|
+
await db.render('./context'); // only relevant rows rendered
|
|
469
|
+
db.setTaskContext(''); // clear — all rows rendered
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### `getTaskContext(): string`
|
|
473
|
+
|
|
474
|
+
Return the current task context string.
|
|
475
|
+
|
|
476
|
+
#### `reward(table, id, scores): Promise<void>`
|
|
477
|
+
|
|
478
|
+
Update reward scores for a row. Requires `rewardTracking: true` on the table. The total reward is the running average across all `reward()` calls.
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
await db.reward('knowledge', rowId, { relevance: 0.9, accuracy: 1.0 });
|
|
482
|
+
// _reward_total = avg(0.9, 1.0) = 0.95, _reward_count = 1
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**`RewardScores`**: `Record<string, number>` — dimension names are arbitrary; values should be 0–1.
|
|
486
|
+
|
|
487
|
+
#### `search(table, query, options?): Promise<SearchResult[]>`
|
|
488
|
+
|
|
489
|
+
Search for rows by semantic similarity. Requires `embeddings` on the table definition.
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
const results = await db.search('docs', 'deploy to production', { topK: 5, minScore: 0.7 });
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**`SearchOptions`**:
|
|
496
|
+
|
|
497
|
+
| Field | Type | Default | Description |
|
|
498
|
+
| ---------- | -------- | ------- | ----------------------------------- |
|
|
499
|
+
| `topK` | `number` | `10` | Max results to return |
|
|
500
|
+
| `minScore` | `number` | `0` | Minimum cosine similarity threshold |
|
|
501
|
+
|
|
502
|
+
**`SearchResult`**: `{ row: Row, score: number }`
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### Sync Methods
|
|
507
|
+
|
|
508
|
+
#### `render(outputDir): Promise<RenderResult>`
|
|
509
|
+
|
|
510
|
+
Render all registered tables (and multi-table views) to their output files once.
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
const result = await db.render('./context');
|
|
514
|
+
console.log(`Wrote ${result.filesWritten.length} files in ${result.durationMs}ms`);
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**`RenderResult`:**
|
|
518
|
+
|
|
519
|
+
| Field | Type | Description |
|
|
520
|
+
| -------------- | ---------- | ------------------------------------------ |
|
|
521
|
+
| `filesWritten` | `string[]` | Absolute paths of files written |
|
|
522
|
+
| `filesSkipped` | `number` | Count of files skipped (content unchanged) |
|
|
523
|
+
| `durationMs` | `number` | Render duration in milliseconds |
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
#### `sync(outputDir): Promise<SyncResult>`
|
|
528
|
+
|
|
529
|
+
Render all output files **and** process any pending writeback entries.
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
const result = await db.sync('./context');
|
|
533
|
+
console.log(
|
|
534
|
+
`Wrote ${result.filesWritten.length} files, processed ${result.writebackProcessed} writeback entries`,
|
|
535
|
+
);
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**`SyncResult`** extends `RenderResult` with:
|
|
539
|
+
|
|
540
|
+
| Field | Type | Description |
|
|
541
|
+
| -------------------- | -------- | --------------------------------------------------- |
|
|
542
|
+
| `writebackProcessed` | `number` | Number of writeback entries processed in this cycle |
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
#### `reconcile(outputDir, options?): Promise<ReconcileResult>`
|
|
547
|
+
|
|
548
|
+
Run a full render cycle and then clean up orphaned files and directories produced by previous cycles. The recommended one-shot method when you want both rendering and lifecycle management.
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
const result = await db.reconcile('./context', {
|
|
552
|
+
removeOrphanedDirectories: true,
|
|
553
|
+
removeOrphanedFiles: true,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
console.log(`Wrote ${result.filesWritten.length} files`);
|
|
557
|
+
console.log(`Removed ${result.cleanup.directoriesRemoved} stale directories`);
|
|
558
|
+
console.log(`Removed ${result.cleanup.filesRemoved} stale files`);
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`reconcile()` reads the manifest written by the **previous** render cycle before rendering, then compares old and new manifests to detect what was generated before but not now. Order of operations:
|
|
562
|
+
|
|
563
|
+
1. Read previous `.lattice/manifest.json` (if it exists)
|
|
564
|
+
2. Run a full render cycle (writes a new manifest)
|
|
565
|
+
3. Compare old vs. new manifest to identify orphans
|
|
566
|
+
4. Delete orphaned directories / files according to `options`
|
|
567
|
+
5. Return `ReconcileResult`
|
|
568
|
+
|
|
569
|
+
**`ReconcileOptions`** (all optional):
|
|
570
|
+
|
|
571
|
+
| Option | Type | Default | Description |
|
|
572
|
+
| --------------------------- | -------------------------------------- | ------- | ------------------------------------------------------------------- |
|
|
573
|
+
| `removeOrphanedDirectories` | `boolean` | `false` | Delete directories for entities no longer in the database |
|
|
574
|
+
| `removeOrphanedFiles` | `boolean` | `false` | Delete files within surviving directories that were not re-rendered |
|
|
575
|
+
| `protectedFiles` | `string[]` | `[]` | Filenames to never delete (merged with per-definition protections) |
|
|
576
|
+
| `dryRun` | `boolean` | `false` | Report what would be deleted without deleting anything |
|
|
577
|
+
| `onOrphan` | `(path: string, kind: string) => void` | – | Called for each orphaned path before it is deleted |
|
|
578
|
+
|
|
579
|
+
**`ReconcileResult`** extends `RenderResult` with:
|
|
580
|
+
|
|
581
|
+
| Field | Type | Description |
|
|
582
|
+
| --------- | --------------- | ---------------------------------- |
|
|
583
|
+
| `cleanup` | `CleanupResult` | Orphan cleanup summary (see below) |
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
#### `watch(outputDir, options?): Promise<StopFn>`
|
|
588
|
+
|
|
589
|
+
Start a polling sync loop. Returns a `StopFn` to stop it.
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
const stop = await db.watch('./context', {
|
|
593
|
+
interval: 10_000,
|
|
594
|
+
onRender: (result) => console.log('Rendered:', result.filesWritten),
|
|
595
|
+
onError: (err) => console.error('Watch error:', err),
|
|
596
|
+
cleanup: {
|
|
597
|
+
removeOrphanedDirectories: true,
|
|
598
|
+
removeOrphanedFiles: true,
|
|
599
|
+
},
|
|
600
|
+
onCleanup: (result) => {
|
|
601
|
+
if (result.directoriesRemoved > 0 || result.filesRemoved > 0) {
|
|
602
|
+
console.log(`Cleaned up ${result.directoriesRemoved} dirs, ${result.filesRemoved} files`);
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Later:
|
|
608
|
+
stop();
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**`WatchOptions`:**
|
|
612
|
+
|
|
613
|
+
| Option | Type | Default | Description |
|
|
614
|
+
| ----------- | --------------------------------- | ------- | -------------------------------------------------------------- |
|
|
615
|
+
| `interval` | `number` | `5000` | Poll interval in milliseconds |
|
|
616
|
+
| `onRender` | `(result: RenderResult) => void` | – | Called after each successful render cycle |
|
|
617
|
+
| `onError` | `(err: Error) => void` | – | Called on render errors |
|
|
618
|
+
| `cleanup` | `CleanupOptions` | – | If set, orphan cleanup runs after each render cycle |
|
|
619
|
+
| `onCleanup` | `(result: CleanupResult) => void` | – | Called after each cleanup cycle (requires `cleanup` to be set) |
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
### Events
|
|
624
|
+
|
|
625
|
+
#### `on(event, handler): this`
|
|
626
|
+
|
|
627
|
+
Subscribe to lifecycle events. Returns `this` for chaining.
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
db.on('audit', (event) => {
|
|
631
|
+
console.log(`${event.operation} on ${event.table} — id: ${event.id}`);
|
|
632
|
+
})
|
|
633
|
+
.on('render', (result) => {
|
|
634
|
+
console.log(`Render complete: ${result.filesWritten.length} files`);
|
|
635
|
+
})
|
|
636
|
+
.on('error', (err) => {
|
|
637
|
+
console.error('Lattice error:', err);
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Available events:**
|
|
642
|
+
|
|
643
|
+
| Event | Handler type | Fires when |
|
|
644
|
+
| ------------- | ---------------------------------------------------------------- | -------------------------------- |
|
|
645
|
+
| `'audit'` | `(event: AuditEvent) => void` | Any insert/update/delete |
|
|
646
|
+
| `'render'` | `(result: RenderResult) => void` | After a render cycle |
|
|
647
|
+
| `'writeback'` | `(data: { filePath: string; entriesProcessed: number }) => void` | After writeback processing |
|
|
648
|
+
| `'error'` | `(err: Error) => void` | On uncaught errors in watch/sync |
|
|
649
|
+
|
|
650
|
+
**`AuditEvent`:**
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
interface AuditEvent {
|
|
654
|
+
table: string;
|
|
655
|
+
operation: 'insert' | 'update' | 'delete';
|
|
656
|
+
id: string;
|
|
657
|
+
timestamp: string; // ISO 8601
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
### Escape Hatch
|
|
664
|
+
|
|
665
|
+
#### `db.db: Database.Database`
|
|
666
|
+
|
|
667
|
+
Direct access to the underlying `better-sqlite3` database instance for raw SQL queries not covered by the Lattice API.
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
const stmt = db.db.prepare('SELECT COUNT(*) FROM tasks WHERE assignee_id = ?');
|
|
671
|
+
const result = stmt.get('user-1') as { 'COUNT(*)': number };
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Use sparingly — raw queries bypass Lattice's sanitization and audit pipeline.
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## Functions
|
|
679
|
+
|
|
680
|
+
### `parseConfigFile()`
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
function parseConfigFile(configPath: string): ParsedConfig;
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Read a `lattice.config.yml` file, validate it, and return a `ParsedConfig` with resolved paths and compiled `TableDefinition` objects ready to pass to `define()`.
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { parseConfigFile } from 'latticesql';
|
|
690
|
+
|
|
691
|
+
const { dbPath, tables } = parseConfigFile('./lattice.config.yml');
|
|
692
|
+
const db = new Lattice(dbPath);
|
|
693
|
+
for (const { name, definition } of tables) {
|
|
694
|
+
db.define(name, definition);
|
|
695
|
+
}
|
|
696
|
+
await db.init();
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Throws on:
|
|
700
|
+
|
|
701
|
+
- File not found or unreadable
|
|
702
|
+
- YAML parse error
|
|
703
|
+
- Missing `db` key
|
|
704
|
+
- Missing `entities` key
|
|
705
|
+
- Entity with no `fields` object
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
### `parseConfigString()`
|
|
710
|
+
|
|
711
|
+
```ts
|
|
712
|
+
function parseConfigString(yamlContent: string, configDir: string): ParsedConfig;
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Parse a raw YAML string instead of reading a file. `configDir` is used to resolve relative paths for `db` and `outputFile`.
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
import { parseConfigString } from 'latticesql';
|
|
719
|
+
|
|
720
|
+
const yaml = `
|
|
721
|
+
db: ./data/app.db
|
|
722
|
+
entities:
|
|
723
|
+
note:
|
|
724
|
+
fields:
|
|
725
|
+
id: { type: uuid, primaryKey: true }
|
|
726
|
+
body: { type: text }
|
|
727
|
+
render: default-list
|
|
728
|
+
outputFile: context/NOTES.md
|
|
729
|
+
`;
|
|
730
|
+
|
|
731
|
+
const { dbPath, tables } = parseConfigString(yaml, process.cwd());
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
### `readManifest(outputDir)`
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
function readManifest(outputDir: string): LatticeManifest | null;
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
Read the Lattice manifest from `{outputDir}/.lattice/manifest.json`. Returns `null` on first run (no manifest yet).
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
import { readManifest } from 'latticesql';
|
|
746
|
+
|
|
747
|
+
const manifest = readManifest('./context');
|
|
748
|
+
if (manifest) {
|
|
749
|
+
console.log('Last generated:', manifest.generated_at);
|
|
750
|
+
for (const [table, entry] of Object.entries(manifest.entityContexts)) {
|
|
751
|
+
console.log(`${table}: ${Object.keys(entry.entities).length} entities`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
### `writeManifest(outputDir, manifest)`
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
function writeManifest(outputDir: string, manifest: LatticeManifest): void;
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Write the manifest atomically. Called automatically by `render()`, `sync()`, and `reconcile()` — you rarely need to call this directly.
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
### `manifestPath(outputDir)`
|
|
769
|
+
|
|
770
|
+
```ts
|
|
771
|
+
function manifestPath(outputDir: string): string;
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
Return the path where Lattice writes its manifest: `{outputDir}/.lattice/manifest.json`.
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
## Types
|
|
779
|
+
|
|
780
|
+
### `Row`
|
|
781
|
+
|
|
782
|
+
```ts
|
|
783
|
+
type Row = Record<string, unknown>;
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
A generic database row — column name to value.
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
### `LatticeOptions`
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
interface LatticeOptions {
|
|
794
|
+
wal?: boolean;
|
|
795
|
+
busyTimeout?: number;
|
|
796
|
+
renderSkipsEmpty?: boolean; // v1.16.5+ — skip read + write for spec-less tables on render()
|
|
797
|
+
security?: SecurityOptions;
|
|
798
|
+
encryptionKey?: string; // v0.18+ — master key for at-rest encryption
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
| Field | Type | Description |
|
|
803
|
+
| ------------------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
804
|
+
| `wal` | `boolean` | Enable WAL mode (default: `true`) |
|
|
805
|
+
| `busyTimeout` | `number` | SQLite busy timeout in ms |
|
|
806
|
+
| `renderSkipsEmpty` | `boolean` | When `true`, `render()` skips the full-table read and file write for tables registered without a `render` spec (they would only emit an empty `.schema-only/<table>.md`). Default `false` — original behavior. Tables with an explicit `render`/`outputFile` are unaffected. |
|
|
807
|
+
| `security` | `SecurityOptions` | Sanitization and audit options |
|
|
808
|
+
| `encryptionKey` | `string` | Master key for at-rest encryption. Required when any entity context has `encrypted: true`. Derived via scrypt before use. |
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
### `SecurityOptions`
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
interface SecurityOptions {
|
|
816
|
+
sanitize?: boolean;
|
|
817
|
+
auditTables?: string[];
|
|
818
|
+
fieldLimits?: Record<string, number>;
|
|
819
|
+
}
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
| Option | Description |
|
|
823
|
+
| ------------- | ------------------------------------------------------------------------- |
|
|
824
|
+
| `sanitize` | Enable input sanitization (strip null bytes, HTML-encode dangerous chars) |
|
|
825
|
+
| `auditTables` | Table names to emit `audit` events for (empty = all tables) |
|
|
826
|
+
| `fieldLimits` | Maximum string length per column name |
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
### `TableDefinition`
|
|
831
|
+
|
|
832
|
+
See [Setup Methods — `define()`](#definetable-definition-this) for the full field reference.
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
### `MultiTableDefinition`
|
|
837
|
+
|
|
838
|
+
See [Setup Methods — `defineMulti()`](#definemultiname-definition-this).
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
842
|
+
### `WritebackDefinition`
|
|
843
|
+
|
|
844
|
+
See [Setup Methods — `defineWriteback()`](#definewritebackdefinition-this).
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
### `QueryOptions`
|
|
849
|
+
|
|
850
|
+
See [CRUD Methods — `query()`](#querytable-options-promiserow).
|
|
851
|
+
|
|
852
|
+
---
|
|
853
|
+
|
|
854
|
+
### `CountOptions`
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
interface CountOptions {
|
|
858
|
+
where?: Record<string, unknown>;
|
|
859
|
+
filters?: Filter[];
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
### `Filter` and `FilterOp`
|
|
866
|
+
|
|
867
|
+
```ts
|
|
868
|
+
type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'in' | 'isNull' | 'isNotNull';
|
|
869
|
+
|
|
870
|
+
interface Filter {
|
|
871
|
+
col: string;
|
|
872
|
+
op: FilterOp;
|
|
873
|
+
val?: unknown;
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
**Operator reference:**
|
|
878
|
+
|
|
879
|
+
| Operator | SQL equivalent | `val` type |
|
|
880
|
+
| ----------- | -------------------- | ------------------------- |
|
|
881
|
+
| `eq` | `col = ?` | any scalar |
|
|
882
|
+
| `ne` | `col != ?` | any scalar |
|
|
883
|
+
| `gt` | `col > ?` | number or string |
|
|
884
|
+
| `gte` | `col >= ?` | number or string |
|
|
885
|
+
| `lt` | `col < ?` | number or string |
|
|
886
|
+
| `lte` | `col <= ?` | number or string |
|
|
887
|
+
| `like` | `col LIKE ?` | string with `%` wildcards |
|
|
888
|
+
| `in` | `col IN (?, ?, ...)` | `unknown[]` |
|
|
889
|
+
| `isNull` | `col IS NULL` | _(not used)_ |
|
|
890
|
+
| `isNotNull` | `col IS NOT NULL` | _(not used)_ |
|
|
891
|
+
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
### `InitOptions` and `Migration`
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
interface InitOptions {
|
|
898
|
+
migrations?: Migration[];
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
interface Migration {
|
|
902
|
+
version: number;
|
|
903
|
+
sql: string;
|
|
904
|
+
}
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
Migrations are applied in order of `version`. Each version is tracked in the `_lattice_migrations` table — a version is only applied once, even across restarts.
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
### `WatchOptions`
|
|
912
|
+
|
|
913
|
+
```ts
|
|
914
|
+
interface WatchOptions {
|
|
915
|
+
interval?: number; // ms, default 5000
|
|
916
|
+
onRender?: (result: RenderResult) => void;
|
|
917
|
+
onError?: (err: Error) => void;
|
|
918
|
+
cleanup?: CleanupOptions;
|
|
919
|
+
onCleanup?: (result: CleanupResult) => void;
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
### `RenderResult` and `SyncResult`
|
|
926
|
+
|
|
927
|
+
```ts
|
|
928
|
+
interface RenderResult {
|
|
929
|
+
filesWritten: string[];
|
|
930
|
+
filesSkipped: number;
|
|
931
|
+
durationMs: number;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
interface SyncResult extends RenderResult {
|
|
935
|
+
writebackProcessed: number;
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
### `AuditEvent`
|
|
942
|
+
|
|
943
|
+
```ts
|
|
944
|
+
interface AuditEvent {
|
|
945
|
+
table: string;
|
|
946
|
+
operation: 'insert' | 'update' | 'delete';
|
|
947
|
+
id: string;
|
|
948
|
+
timestamp: string; // ISO 8601
|
|
949
|
+
}
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
### `PkLookup`
|
|
955
|
+
|
|
956
|
+
```ts
|
|
957
|
+
type PkLookup = string | Record<string, unknown>;
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
Used by `get()`, `update()`, and `delete()` to identify a row:
|
|
961
|
+
|
|
962
|
+
- `string` — value of the single PK column
|
|
963
|
+
- `Record<string, unknown>` — column-to-value map for composite PKs
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
### `PrimaryKey`
|
|
968
|
+
|
|
969
|
+
```ts
|
|
970
|
+
type PrimaryKey = string | string[];
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
The primary key of a table. A string for single-column PKs; an array for composite PKs.
|
|
974
|
+
|
|
975
|
+
---
|
|
976
|
+
|
|
977
|
+
### `Relation` types
|
|
978
|
+
|
|
979
|
+
```ts
|
|
980
|
+
interface BelongsToRelation {
|
|
981
|
+
type: 'belongsTo';
|
|
982
|
+
table: string; // related table name
|
|
983
|
+
foreignKey: string; // FK column on THIS table
|
|
984
|
+
references?: string; // PK column on the related table (default: its first PK)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
interface HasManyRelation {
|
|
988
|
+
type: 'hasMany';
|
|
989
|
+
table: string; // related table name
|
|
990
|
+
foreignKey: string; // FK column on the RELATED table
|
|
991
|
+
references?: string; // PK column on THIS table (default: its first PK)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
type Relation = BelongsToRelation | HasManyRelation;
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
---
|
|
998
|
+
|
|
999
|
+
### Render types
|
|
1000
|
+
|
|
1001
|
+
```ts
|
|
1002
|
+
type BuiltinTemplateName = 'default-list' | 'default-table' | 'default-detail' | 'default-json';
|
|
1003
|
+
|
|
1004
|
+
interface RenderHooks {
|
|
1005
|
+
beforeRender?: (rows: Row[]) => Row[];
|
|
1006
|
+
formatRow?: ((row: Row) => string) | string;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
interface TemplateRenderSpec {
|
|
1010
|
+
template: BuiltinTemplateName;
|
|
1011
|
+
hooks?: RenderHooks;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
type RenderSpec = ((rows: Row[]) => string) | BuiltinTemplateName | TemplateRenderSpec;
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
See [Template Rendering](./templates.md) for the complete guide.
|
|
1018
|
+
|
|
1019
|
+
---
|
|
1020
|
+
|
|
1021
|
+
### Config types
|
|
1022
|
+
|
|
1023
|
+
```ts
|
|
1024
|
+
type LatticeFieldType =
|
|
1025
|
+
| 'uuid'
|
|
1026
|
+
| 'text'
|
|
1027
|
+
| 'integer'
|
|
1028
|
+
| 'int'
|
|
1029
|
+
| 'real'
|
|
1030
|
+
| 'float'
|
|
1031
|
+
| 'boolean'
|
|
1032
|
+
| 'bool'
|
|
1033
|
+
| 'datetime'
|
|
1034
|
+
| 'date'
|
|
1035
|
+
| 'blob';
|
|
1036
|
+
|
|
1037
|
+
interface LatticeFieldDef {
|
|
1038
|
+
type: LatticeFieldType;
|
|
1039
|
+
primaryKey?: boolean;
|
|
1040
|
+
required?: boolean;
|
|
1041
|
+
default?: string | number | boolean;
|
|
1042
|
+
ref?: string;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
interface LatticeEntityRenderSpec {
|
|
1046
|
+
template: string;
|
|
1047
|
+
formatRow?: string;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
interface LatticeEntityDef {
|
|
1051
|
+
fields: Record<string, LatticeFieldDef>;
|
|
1052
|
+
render?: string | LatticeEntityRenderSpec;
|
|
1053
|
+
outputFile: string;
|
|
1054
|
+
primaryKey?: string | string[];
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
interface LatticeConfig {
|
|
1058
|
+
db: string;
|
|
1059
|
+
entities: Record<string, LatticeEntityDef>;
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
See [Configuration Guide](./configuration.md) for the complete YAML reference.
|
|
1064
|
+
|
|
1065
|
+
---
|
|
1066
|
+
|
|
1067
|
+
### `ParsedConfig`
|
|
1068
|
+
|
|
1069
|
+
```ts
|
|
1070
|
+
interface ParsedConfig {
|
|
1071
|
+
dbPath: string;
|
|
1072
|
+
tables: ReadonlyArray<{ name: string; definition: TableDefinition }>;
|
|
1073
|
+
}
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
Returned by `parseConfigFile()` and `parseConfigString()`.
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
### `LatticeConfigInput`
|
|
1081
|
+
|
|
1082
|
+
```ts
|
|
1083
|
+
interface LatticeConfigInput {
|
|
1084
|
+
config: string;
|
|
1085
|
+
options?: LatticeOptions;
|
|
1086
|
+
}
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
The object form of the `Lattice` constructor when initialising from a YAML config file.
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
### `StopFn`
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
type StopFn = () => void;
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
Returned by `watch()`. Call it to stop the polling loop.
|
|
1100
|
+
|
|
1101
|
+
---
|
|
1102
|
+
|
|
1103
|
+
### Entity Context types
|
|
1104
|
+
|
|
1105
|
+
#### `EntityContextDefinition`
|
|
1106
|
+
|
|
1107
|
+
```ts
|
|
1108
|
+
interface EntityContextDefinition {
|
|
1109
|
+
slug: (row: Row) => string;
|
|
1110
|
+
index?: {
|
|
1111
|
+
outputFile: string;
|
|
1112
|
+
render: (rows: Row[]) => string;
|
|
1113
|
+
};
|
|
1114
|
+
files: EntityFileSpec[];
|
|
1115
|
+
combined?: {
|
|
1116
|
+
outputFile: string;
|
|
1117
|
+
exclude?: string[];
|
|
1118
|
+
};
|
|
1119
|
+
directory?: (row: Row) => string;
|
|
1120
|
+
directoryRoot?: string;
|
|
1121
|
+
protectedFiles?: string[];
|
|
1122
|
+
protected?: boolean; // v0.18+
|
|
1123
|
+
encrypted?: boolean | { columns: string[] }; // v0.18+
|
|
1124
|
+
}
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
**`protected`** _(v0.18+)_ — When `true`, this entity's data is never rendered into other entities' context files. Sources referencing a protected table from a different entity context return empty results. Within the same protected table, sources return only the current entity's own row. The entity's own files are still rendered normally.
|
|
1128
|
+
|
|
1129
|
+
**`encrypted`** _(v0.18+)_ — Enable at-rest encryption for this entity's table. Requires `encryptionKey` in `LatticeOptions`. Set to `true` to encrypt all text columns (except `id`, timestamps), or `{ columns: ['value', 'notes'] }` for specific columns. Values are stored as `enc:<base64(iv+tag+ciphertext)>` using AES-256-GCM and transparently decrypted on read.
|
|
1130
|
+
|
|
1131
|
+
#### `EntityFileSpec`
|
|
1132
|
+
|
|
1133
|
+
```ts
|
|
1134
|
+
interface EntityFileSpec {
|
|
1135
|
+
filename: string;
|
|
1136
|
+
source: EntitySource;
|
|
1137
|
+
render: (rows: Row[]) => string;
|
|
1138
|
+
budget?: number;
|
|
1139
|
+
omitIfEmpty?: boolean;
|
|
1140
|
+
}
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
#### `EntitySource`
|
|
1144
|
+
|
|
1145
|
+
```ts
|
|
1146
|
+
type EntitySource = SelfSource | HasManySource | ManyToManySource | BelongsToSource | CustomSource;
|
|
1147
|
+
|
|
1148
|
+
interface SelfSource {
|
|
1149
|
+
type: 'self';
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
interface HasManySource {
|
|
1153
|
+
type: 'hasMany';
|
|
1154
|
+
table: string;
|
|
1155
|
+
foreignKey: string;
|
|
1156
|
+
references?: string;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
interface ManyToManySource {
|
|
1160
|
+
type: 'manyToMany';
|
|
1161
|
+
junctionTable: string;
|
|
1162
|
+
localKey: string;
|
|
1163
|
+
remoteKey: string;
|
|
1164
|
+
remoteTable: string;
|
|
1165
|
+
references?: string;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
interface BelongsToSource {
|
|
1169
|
+
type: 'belongsTo';
|
|
1170
|
+
table: string;
|
|
1171
|
+
foreignKey: string;
|
|
1172
|
+
references?: string;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
interface CustomSource {
|
|
1176
|
+
type: 'custom';
|
|
1177
|
+
query: (row: Row, adapter: StorageAdapter) => Row[];
|
|
1178
|
+
}
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
---
|
|
1182
|
+
|
|
1183
|
+
### Lifecycle types
|
|
1184
|
+
|
|
1185
|
+
#### `CleanupOptions`
|
|
1186
|
+
|
|
1187
|
+
```ts
|
|
1188
|
+
interface CleanupOptions {
|
|
1189
|
+
removeOrphanedDirectories?: boolean;
|
|
1190
|
+
removeOrphanedFiles?: boolean;
|
|
1191
|
+
protectedFiles?: string[];
|
|
1192
|
+
dryRun?: boolean;
|
|
1193
|
+
onOrphan?: (path: string, kind: 'directory' | 'file') => void;
|
|
1194
|
+
}
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
#### `CleanupResult`
|
|
1198
|
+
|
|
1199
|
+
```ts
|
|
1200
|
+
interface CleanupResult {
|
|
1201
|
+
directoriesRemoved: number;
|
|
1202
|
+
filesRemoved: number;
|
|
1203
|
+
directoriesSkipped: number;
|
|
1204
|
+
warnings: string[];
|
|
1205
|
+
}
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
#### `ReconcileOptions`
|
|
1209
|
+
|
|
1210
|
+
Extends `CleanupOptions` with reverse-sync control:
|
|
1211
|
+
|
|
1212
|
+
```ts
|
|
1213
|
+
interface ReconcileOptions extends CleanupOptions {
|
|
1214
|
+
reverseSync?: boolean | 'dry-run'; // default: true
|
|
1215
|
+
}
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
| Value | Behavior |
|
|
1219
|
+
| ---------------- | -------------------------------------------------------------------- |
|
|
1220
|
+
| `true` (default) | Detect external file edits and sync them back to DB before rendering |
|
|
1221
|
+
| `'dry-run'` | Detect and count changes, but do not modify the database |
|
|
1222
|
+
| `false` | Skip reverse-sync entirely |
|
|
1223
|
+
|
|
1224
|
+
#### `ReconcileResult`
|
|
1225
|
+
|
|
1226
|
+
```ts
|
|
1227
|
+
interface ReconcileResult extends RenderResult {
|
|
1228
|
+
cleanup: CleanupResult;
|
|
1229
|
+
reverseSync: ReverseSyncResult | null;
|
|
1230
|
+
}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
#### `ReverseSyncResult`
|
|
1234
|
+
|
|
1235
|
+
```ts
|
|
1236
|
+
interface ReverseSyncResult {
|
|
1237
|
+
filesScanned: number; // Files with reverseSync checked for changes
|
|
1238
|
+
filesChanged: number; // Files that had been modified since last render
|
|
1239
|
+
updatesApplied: number; // Total DB updates applied
|
|
1240
|
+
errors: ReverseSyncError[];
|
|
1241
|
+
}
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
#### `ReverseSyncUpdate`
|
|
1245
|
+
|
|
1246
|
+
```ts
|
|
1247
|
+
interface ReverseSyncUpdate {
|
|
1248
|
+
table: string;
|
|
1249
|
+
pk: Record<string, unknown>;
|
|
1250
|
+
set: Record<string, unknown>;
|
|
1251
|
+
}
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
#### `ReverseSyncError`
|
|
1255
|
+
|
|
1256
|
+
```ts
|
|
1257
|
+
interface ReverseSyncError {
|
|
1258
|
+
file: string; // Absolute path to the file
|
|
1259
|
+
error: string; // Error description
|
|
1260
|
+
}
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
---
|
|
1264
|
+
|
|
1265
|
+
### Manifest types and functions
|
|
1266
|
+
|
|
1267
|
+
#### `LatticeManifest`
|
|
1268
|
+
|
|
1269
|
+
```ts
|
|
1270
|
+
interface LatticeManifest {
|
|
1271
|
+
version: 1 | 2;
|
|
1272
|
+
generated_at: string; // ISO 8601
|
|
1273
|
+
entityContexts: Record<string, EntityContextManifestEntry>;
|
|
1274
|
+
}
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
Written to `.lattice/manifest.json` inside `outputDir` after every render cycle that includes entity contexts. The manifest is the authoritative record of what Lattice generated — it enables safe orphan cleanup and reverse-sync change detection across restarts.
|
|
1278
|
+
|
|
1279
|
+
v2 (0.16+) adds per-file content hashes. v1 manifests are auto-migrated.
|
|
1280
|
+
|
|
1281
|
+
#### `EntityContextManifestEntry`
|
|
1282
|
+
|
|
1283
|
+
```ts
|
|
1284
|
+
interface EntityContextManifestEntry {
|
|
1285
|
+
directoryRoot: string;
|
|
1286
|
+
indexFile?: string;
|
|
1287
|
+
declaredFiles: string[];
|
|
1288
|
+
protectedFiles: string[];
|
|
1289
|
+
entities: Record<string, Record<string, EntityFileManifestInfo> | string[]>;
|
|
1290
|
+
}
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
#### `EntityFileManifestInfo`
|
|
1294
|
+
|
|
1295
|
+
```ts
|
|
1296
|
+
interface EntityFileManifestInfo {
|
|
1297
|
+
hash: string; // SHA-256 hex digest of last-rendered content
|
|
1298
|
+
}
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
#### `readManifest(outputDir)`
|
|
1302
|
+
|
|
1303
|
+
```ts
|
|
1304
|
+
function readManifest(outputDir: string): LatticeManifest | null;
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
Read `.lattice/manifest.json` from `outputDir`. Returns `null` if the file does not exist (first run).
|
|
1308
|
+
|
|
1309
|
+
#### `writeManifest(outputDir, manifest)`
|
|
1310
|
+
|
|
1311
|
+
```ts
|
|
1312
|
+
function writeManifest(outputDir: string, manifest: LatticeManifest): void;
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
Write the manifest atomically (`.tmp` → rename). Called automatically by `render()` and `reconcile()` when entity contexts are registered.
|
|
1316
|
+
|
|
1317
|
+
#### `manifestPath(outputDir)`
|
|
1318
|
+
|
|
1319
|
+
```ts
|
|
1320
|
+
function manifestPath(outputDir: string): string;
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
Return the path to the manifest file: `{outputDir}/.lattice/manifest.json`.
|
|
1324
|
+
|
|
1325
|
+
---
|
|
1326
|
+
|
|
1327
|
+
### Internal exports (for testing)
|
|
1328
|
+
|
|
1329
|
+
The following are exported from `latticesql` to support integration testing but are not part of the stable public API:
|
|
1330
|
+
|
|
1331
|
+
```ts
|
|
1332
|
+
function resolveEntitySource(
|
|
1333
|
+
source: EntitySource,
|
|
1334
|
+
entityRow: Row,
|
|
1335
|
+
entityPk: string | string[],
|
|
1336
|
+
adapter: StorageAdapter,
|
|
1337
|
+
): Row[];
|
|
1338
|
+
|
|
1339
|
+
function truncateContent(content: string, budget?: number): string;
|
|
1340
|
+
|
|
1341
|
+
function cleanupEntityContexts(
|
|
1342
|
+
outputDir: string,
|
|
1343
|
+
entityContexts: Record<string, EntityContextDefinition & { table: string }>,
|
|
1344
|
+
currentSlugsByTable: Record<string, Set<string>>,
|
|
1345
|
+
manifest: LatticeManifest | null,
|
|
1346
|
+
options: CleanupOptions,
|
|
1347
|
+
newManifest?: LatticeManifest,
|
|
1348
|
+
): CleanupResult;
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
### Full-text search (1.16)
|
|
1352
|
+
|
|
1353
|
+
```ts
|
|
1354
|
+
function fullTextSearch(
|
|
1355
|
+
adapter: StorageAdapter,
|
|
1356
|
+
tables: string[],
|
|
1357
|
+
opts: { query: string; limitPerTable?: number; textColumns?: Record<string, string[]> },
|
|
1358
|
+
): Promise<FtsResult>; // { query, groups: [{ table, count, more, hits: [{ id, snippet }] }] }
|
|
1359
|
+
|
|
1360
|
+
function ensureFtsIndex(adapter: StorageAdapter, table: string, cols: string[]): Promise<void>;
|
|
1361
|
+
function hasFtsIndex(adapter: StorageAdapter, table: string): Promise<boolean>;
|
|
1362
|
+
function ftsTableName(table: string): string; // "__lattice_fts_<table>"
|
|
1363
|
+
function autoFtsColumns(cols: string[]): string[]; // text cols minus ids/bookkeeping
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
A table opts into an inverted index (SQLite FTS5 / Postgres `tsvector` + GIN)
|
|
1367
|
+
by declaring `fts: { fields?: string[] }` on its `TableDefinition`; tables
|
|
1368
|
+
without `fts` use a LIKE fallback. Indexes are created only for opt-in tables,
|
|
1369
|
+
so a bare consumer pays zero write-path overhead. See `docs/workspaces.md` and
|
|
1370
|
+
`docs/collaboration.md` for the workspace model and multiplayer cloud editing.
|