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.
@@ -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.1.0",
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"