latticesql 3.1.0 → 3.2.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 +4 -0
- package/dist/cli.js +1996 -878
- package/dist/index.cjs +352 -187
- package/dist/index.d.cts +15 -20
- package/dist/index.d.ts +15 -20
- package/dist/index.js +352 -187
- 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,338 @@
|
|
|
1
|
+
# Template Rendering
|
|
2
|
+
|
|
3
|
+
How Lattice turns database rows into LLM context files.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [Built-in templates](#built-in-templates)
|
|
11
|
+
- [`default-list`](#default-list)
|
|
12
|
+
- [`default-table`](#default-table)
|
|
13
|
+
- [`default-detail`](#default-detail)
|
|
14
|
+
- [`default-json`](#default-json)
|
|
15
|
+
- [Render hooks](#render-hooks)
|
|
16
|
+
- [`beforeRender`](#beforerender)
|
|
17
|
+
- [`formatRow`](#formatrow)
|
|
18
|
+
- [Interpolation syntax](#interpolation-syntax)
|
|
19
|
+
- [Relationship data in templates](#relationship-data-in-templates)
|
|
20
|
+
- [Custom render functions](#custom-render-functions)
|
|
21
|
+
- [Choosing a template](#choosing-a-template)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
Every registered table has a `render` spec that controls how its rows become text. Lattice compiles the spec at `define()` time, so the heavy work happens once — each sync cycle just calls the pre-compiled function.
|
|
28
|
+
|
|
29
|
+
The three forms of `render`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// 1. Built-in template name — simplest
|
|
33
|
+
render: 'default-list'
|
|
34
|
+
|
|
35
|
+
// 2. Template with hooks — add pre-filter or row formatting
|
|
36
|
+
render: {
|
|
37
|
+
template: 'default-list',
|
|
38
|
+
hooks: {
|
|
39
|
+
beforeRender: (rows) => rows.filter(r => r.status !== 'archived'),
|
|
40
|
+
formatRow: '{{title}} [{{status}}]',
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Custom function — full control
|
|
45
|
+
render: (rows) => rows.map(r => `## ${r.title}\n${r.body}`).join('\n\n')
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
In YAML config, the forms map to:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
render: default-list # form 1
|
|
52
|
+
|
|
53
|
+
render:
|
|
54
|
+
template: default-list
|
|
55
|
+
formatRow: "{{title}} [{{status}}]" # form 2 (string formatRow only in YAML)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Built-in templates
|
|
61
|
+
|
|
62
|
+
### `default-list`
|
|
63
|
+
|
|
64
|
+
Renders rows as a Markdown bulleted list. Ideal for compact overviews.
|
|
65
|
+
|
|
66
|
+
**Default output** (no `formatRow`):
|
|
67
|
+
|
|
68
|
+
```markdown
|
|
69
|
+
# tasks
|
|
70
|
+
|
|
71
|
+
- id: task-1 | title: Fix login bug | status: open
|
|
72
|
+
- id: task-2 | title: Write tests | status: done
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Fields are joined with `|` separators. Field names and values are shown as `key: value` pairs.
|
|
76
|
+
|
|
77
|
+
**With `formatRow`:**
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
render: {
|
|
81
|
+
template: 'default-list',
|
|
82
|
+
hooks: { formatRow: '{{title}} [{{status}}]' },
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```markdown
|
|
87
|
+
# tasks
|
|
88
|
+
|
|
89
|
+
- Fix login bug [open]
|
|
90
|
+
- Write tests [done]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### `default-table`
|
|
96
|
+
|
|
97
|
+
Renders rows as a GitHub-flavoured Markdown table. Good for structured data with many fields.
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
# users
|
|
101
|
+
|
|
102
|
+
| id | name | email | role |
|
|
103
|
+
| --- | ----- | ----------------- | ------ |
|
|
104
|
+
| u-1 | Alice | alice@example.com | admin |
|
|
105
|
+
| u-2 | Bob | bob@example.com | member |
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Column headers are derived from the first row's keys. Does not support `formatRow`.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### `default-detail`
|
|
113
|
+
|
|
114
|
+
Renders each row as a separate Markdown section. Best for entities with many fields where table layout would be cramped.
|
|
115
|
+
|
|
116
|
+
**Default output:**
|
|
117
|
+
|
|
118
|
+
```markdown
|
|
119
|
+
# agents
|
|
120
|
+
|
|
121
|
+
## agent-1
|
|
122
|
+
|
|
123
|
+
- id: agent-1
|
|
124
|
+
- name: Craft
|
|
125
|
+
- role: Software architect
|
|
126
|
+
|
|
127
|
+
## agent-2
|
|
128
|
+
|
|
129
|
+
- id: agent-2
|
|
130
|
+
- name: Audit
|
|
131
|
+
- role: Code reviewer
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**With `formatRow`:**
|
|
135
|
+
|
|
136
|
+
Each item in the section's list is rendered using `formatRow` instead of the default `key: value` format.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### `default-json`
|
|
141
|
+
|
|
142
|
+
Renders all rows as a JSON array. Useful when downstream tools consume structured data.
|
|
143
|
+
|
|
144
|
+
````markdown
|
|
145
|
+
# tasks
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
[
|
|
149
|
+
{ "id": "task-1", "title": "Fix login bug", "status": "open" },
|
|
150
|
+
{ "id": "task-2", "title": "Write tests", "status": "done" }
|
|
151
|
+
]
|
|
152
|
+
```
|
|
153
|
+
````
|
|
154
|
+
|
|
155
|
+
````
|
|
156
|
+
|
|
157
|
+
No formatting hooks apply to `default-json`. If you need to transform the data, use `beforeRender` or a custom render function.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Render hooks
|
|
162
|
+
|
|
163
|
+
Hooks let you customise built-in template behaviour without writing a full custom render function.
|
|
164
|
+
|
|
165
|
+
### `beforeRender`
|
|
166
|
+
|
|
167
|
+
`beforeRender(rows: Row[]): Row[]` — transform or filter the row array **before** any rendering occurs.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
render: {
|
|
171
|
+
template: 'default-list',
|
|
172
|
+
hooks: {
|
|
173
|
+
beforeRender: (rows) => rows
|
|
174
|
+
.filter(r => r.status !== 'archived')
|
|
175
|
+
.sort((a, b) => Number(b.priority) - Number(a.priority)),
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
````
|
|
179
|
+
|
|
180
|
+
Use cases:
|
|
181
|
+
|
|
182
|
+
- Exclude archived / deleted rows
|
|
183
|
+
- Sort rows by a computed field
|
|
184
|
+
- Add computed properties (e.g. format a date)
|
|
185
|
+
- Limit to the N most-recent rows
|
|
186
|
+
|
|
187
|
+
`beforeRender` runs before `formatRow`. The array it returns is what gets formatted.
|
|
188
|
+
|
|
189
|
+
### `formatRow`
|
|
190
|
+
|
|
191
|
+
`formatRow` controls how each row is serialised to a string. It is supported by `default-list` and `default-detail`. It is **not** supported by `default-table` or `default-json`.
|
|
192
|
+
|
|
193
|
+
Two forms:
|
|
194
|
+
|
|
195
|
+
**Function:**
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
hooks: {
|
|
199
|
+
formatRow: (row) => `${row.title} — assigned to ${row.assignee_id ?? 'unassigned'}`,
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Template string:**
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
hooks: {
|
|
207
|
+
formatRow: '{{title}} — {{status}}',
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
In YAML config, only the template string form is supported:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
render:
|
|
215
|
+
template: default-list
|
|
216
|
+
formatRow: '{{title}} — {{status}}'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Interpolation syntax
|
|
222
|
+
|
|
223
|
+
Template strings in `formatRow` (and anywhere Lattice uses `{{...}}` substitution) follow these rules:
|
|
224
|
+
|
|
225
|
+
- `{{fieldName}}` — replaced with `String(row[fieldName])` or `''` if the field is missing or null
|
|
226
|
+
- `{{relationName.fieldName}}` — resolved by joining the current row to a related table via a `belongsTo` relation (see below)
|
|
227
|
+
- Delimiters are `{{` and `}}` — no spaces inside
|
|
228
|
+
- Unknown tokens are replaced with an empty string (no error thrown)
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
"{{title}}" → "Fix login bug"
|
|
234
|
+
"{{title}} [{{status}}]" → "Fix login bug [open]"
|
|
235
|
+
"{{assignee.name}} → {{title}}" → "Alice → Fix login bug"
|
|
236
|
+
"P{{priority}}: {{title}}" → "P3: Fix login bug"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Relationship data in templates
|
|
242
|
+
|
|
243
|
+
When a table has a `belongsTo` relation declared (via `relations` or `ref` in YAML config), Lattice can resolve relation fields inside `{{...}}` templates.
|
|
244
|
+
|
|
245
|
+
To use relation data, the relation must be declared:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
db.define('tickets', {
|
|
249
|
+
columns: {
|
|
250
|
+
id: 'TEXT PRIMARY KEY',
|
|
251
|
+
title: 'TEXT NOT NULL',
|
|
252
|
+
assignee_id: 'TEXT',
|
|
253
|
+
},
|
|
254
|
+
relations: {
|
|
255
|
+
assignee: { type: 'belongsTo', table: 'users', foreignKey: 'assignee_id' },
|
|
256
|
+
},
|
|
257
|
+
render: {
|
|
258
|
+
template: 'default-list',
|
|
259
|
+
hooks: { formatRow: '{{title}} → {{assignee.name}}' },
|
|
260
|
+
},
|
|
261
|
+
outputFile: 'context/TICKETS.md',
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
When rendering, Lattice:
|
|
266
|
+
|
|
267
|
+
1. Executes `SELECT * FROM users WHERE id = assignee_id` for each ticket
|
|
268
|
+
2. Makes all `users` columns available as `{{assignee.<column>}}`
|
|
269
|
+
|
|
270
|
+
In YAML config, `ref: user` automatically creates the `belongsTo` relation:
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
entities:
|
|
274
|
+
ticket:
|
|
275
|
+
fields:
|
|
276
|
+
id: { type: uuid, primaryKey: true }
|
|
277
|
+
title: { type: text, required: true }
|
|
278
|
+
assignee_id: { type: uuid, ref: user }
|
|
279
|
+
render:
|
|
280
|
+
template: default-list
|
|
281
|
+
formatRow: '{{title}} → {{assignee.name}}'
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Limitations:**
|
|
285
|
+
|
|
286
|
+
- Only `belongsTo` relations are resolved in templates (the table holding the FK)
|
|
287
|
+
- `hasMany` relations are not resolved in `{{...}}` interpolation; use a custom render function or `defineMulti` instead
|
|
288
|
+
- Only one level of nesting is supported (`{{assignee.name}}`, not `{{assignee.org.name}}`)
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Custom render functions
|
|
293
|
+
|
|
294
|
+
For full control, pass a `(rows: Row[]) => string` function directly:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
db.define('changelog', {
|
|
298
|
+
columns: {
|
|
299
|
+
id: 'TEXT PRIMARY KEY',
|
|
300
|
+
version: 'TEXT NOT NULL',
|
|
301
|
+
notes: 'TEXT',
|
|
302
|
+
date: 'TEXT',
|
|
303
|
+
},
|
|
304
|
+
render: (rows) => {
|
|
305
|
+
const sorted = [...rows].sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
|
306
|
+
return sorted
|
|
307
|
+
.map((r) => `## v${r.version} — ${r.date}\n\n${r.notes ?? '_No notes_'}`)
|
|
308
|
+
.join('\n\n---\n\n');
|
|
309
|
+
},
|
|
310
|
+
outputFile: 'context/CHANGELOG.md',
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Custom functions:
|
|
315
|
+
|
|
316
|
+
- Receive the full `Row[]` array (after any `filter` defined on the table)
|
|
317
|
+
- Must return a string
|
|
318
|
+
- Have no access to relation data (join manually via `db.query()` if needed)
|
|
319
|
+
- Are compiled once at `define()` time and called on every sync cycle
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Choosing a template
|
|
324
|
+
|
|
325
|
+
| Template | Best for |
|
|
326
|
+
| ---------------- | ------------------------------------------------------------------------ |
|
|
327
|
+
| `default-list` | Compact overviews with `formatRow` control; good for lists the LLM reads |
|
|
328
|
+
| `default-table` | Structured data with uniform fields; easy to scan |
|
|
329
|
+
| `default-detail` | Rich entities with many fields; one section per row |
|
|
330
|
+
| `default-json` | Downstream tool consumption; structured data handoff |
|
|
331
|
+
| Custom function | Any format not covered above; multi-table joins; complex Markdown |
|
|
332
|
+
|
|
333
|
+
General guidance:
|
|
334
|
+
|
|
335
|
+
- Use `default-list` + `formatRow` for most agent context files — it's compact and readable
|
|
336
|
+
- Use `default-table` for reference data (users, tags, config settings)
|
|
337
|
+
- Use `default-json` when another system (not a human / LLM) reads the output
|
|
338
|
+
- Use a custom function for hierarchical documents or when you need JOIN data
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Workspaces & auto-render
|
|
2
|
+
|
|
3
|
+
Lattice 1.16 introduces a single, discoverable on-disk home — the **`.lattice`
|
|
4
|
+
root** — that holds machine-local config, a workspace registry, each
|
|
5
|
+
workspace's database, and the rendered SQL→markdown context. It's entirely
|
|
6
|
+
opt-in: a bare `new Lattice(path)` is unaffected and pays no overhead.
|
|
7
|
+
|
|
8
|
+
## The `.lattice` root
|
|
9
|
+
|
|
10
|
+
A root is the first ancestor directory containing `.lattice/.config/`, or the
|
|
11
|
+
path in the `LATTICE_ROOT` environment variable. Layout:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
.lattice/
|
|
15
|
+
├── .config/ # machine-local: registry, keys, preferences
|
|
16
|
+
│ └── registry.json # the workspace registry (see below)
|
|
17
|
+
└── Workspaces/
|
|
18
|
+
└── <Workspace Name>/
|
|
19
|
+
├── Data/ # database.db (local) + content-addressed blobs
|
|
20
|
+
├── Context/ # rendered SQL→markdown bridge output
|
|
21
|
+
└── workspace.yml # this workspace's config
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `ensureLatticeRoot(startDir?)` — resolve (creating if needed) the root.
|
|
25
|
+
- The root marker is the `.config/` directory; there is no manifest file.
|
|
26
|
+
|
|
27
|
+
## Workspaces
|
|
28
|
+
|
|
29
|
+
A **workspace** is one database plus its rendered context, registered under the
|
|
30
|
+
root. Each has a stable UUID `id` (survives renames), a `displayName`, a
|
|
31
|
+
filesystem-safe `dir`, a `db` target (`./Data/database.db` or a
|
|
32
|
+
`postgres://…` URL), and a `kind` (`local` | `cloud`).
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Lattice, ensureLatticeRoot, addWorkspace } from 'latticesql';
|
|
36
|
+
|
|
37
|
+
const root = ensureLatticeRoot();
|
|
38
|
+
const ws = addWorkspace(root, { displayName: 'Research' });
|
|
39
|
+
const db = await Lattice.openWorkspace({ root, workspaceId: ws.id });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Registry helpers (all in the package root export):
|
|
43
|
+
|
|
44
|
+
| Function | Purpose |
|
|
45
|
+
| ------------------------------------------------------- | ------------------------------------------- |
|
|
46
|
+
| `addWorkspace(root, { displayName, db?, makeActive? })` | Scaffold + register a workspace. |
|
|
47
|
+
| `listWorkspaces(root)` | All registered workspaces. |
|
|
48
|
+
| `getWorkspace(root, id)` / `getActiveWorkspace(root)` | Look up by id / the active one. |
|
|
49
|
+
| `setActiveWorkspace(root, id)` | Change the active workspace. |
|
|
50
|
+
| `resolveWorkspacePaths(root, ws)` | `{ dir, configPath, dataDir, contextDir }`. |
|
|
51
|
+
|
|
52
|
+
`Lattice.openWorkspace({ root?, workspaceId?, autoRender? })` opens the active
|
|
53
|
+
(or named) workspace, applies the canonical context layout for tables without
|
|
54
|
+
an explicit one, runs `init()`, and — unless `autoRender: false` — enables
|
|
55
|
+
auto-render and writes the initial `Context/` tree.
|
|
56
|
+
|
|
57
|
+
## Auto-render (SQL → markdown)
|
|
58
|
+
|
|
59
|
+
`enableAutoRender(outputDir)` debounces a re-render on every
|
|
60
|
+
insert/update/delete, coalescing bursts into one render and skipping unchanged
|
|
61
|
+
files via the manifest hash-diff. Workspaces enable it by default, so the
|
|
62
|
+
`Context/` tree is always current and there is never a "no rendered context"
|
|
63
|
+
state.
|
|
64
|
+
|
|
65
|
+
A bare `new Lattice(path)` does **not** auto-render (`_scheduleAutoRender`
|
|
66
|
+
early-returns when no output dir is set) — call `render(dir)` / `reconcile(dir)`
|
|
67
|
+
manually, or opt in with `enableAutoRender(dir)`.
|
|
68
|
+
|
|
69
|
+
The canonical `Context/` layout is DB-aligned and zero-config: table → folder,
|
|
70
|
+
row → subfolder, `<ENTITY>.md` plus relation rollups, derived from the schema
|
|
71
|
+
via `deriveCanonicalContexts`.
|
|
72
|
+
|
|
73
|
+
## CLI
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
lattice init # scaffold a root + default workspace, render the tree
|
|
77
|
+
lattice workspace list # list workspaces
|
|
78
|
+
lattice workspace create <name>
|
|
79
|
+
lattice workspace use <name>
|
|
80
|
+
lattice gui # opens the active workspace when a root is present
|
|
81
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latticesql",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"lattice": "./dist/cli.js"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist"
|
|
25
|
+
"dist",
|
|
26
|
+
"docs"
|
|
26
27
|
],
|
|
27
28
|
"engines": {
|
|
28
29
|
"node": ">=18"
|