latticesql 3.0.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,331 @@
1
+ # Architecture Overview
2
+
3
+ How `latticesql` is structured internally and the design decisions behind it.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [High-level picture](#high-level-picture)
10
+ - [Module breakdown](#module-breakdown)
11
+ - [Data flow](#data-flow)
12
+ - [Design decisions](#design-decisions)
13
+ - [Package structure](#package-structure)
14
+
15
+ > **v0.5 additions** are called out inline below. They cover entity context directories, lifecycle management, the manifest, and the `reconcile()` method.
16
+
17
+ ---
18
+
19
+ ## High-level picture
20
+
21
+ ```
22
+ ┌─────────────────────────────────────┐
23
+ │ Lattice │
24
+ │ (public facade) │
25
+ │ │
26
+ │ defineEntityContext() reconcile() │ ← v0.5
27
+ └──────────────────┬──────────────────┘
28
+
29
+ ┌────────────────────────────┼──────────────────────────┐
30
+ │ │ │
31
+ ┌─────────▼──────────┐ ┌────────────▼──────────┐ ┌───────────▼──────────┐
32
+ │ SchemaManager │ │ SQLiteAdapter │ │ WritebackPipeline │
33
+ │ │ │ │ │ │
34
+ │ • define(table) │ │ • open() / close() │ │ • define(def) │
35
+ │ • defineEntityCtx() │ │ • run() / all() / get()│ │ • process() │
36
+ │ • getPrimaryKey() │ │ • WAL mode │ │ • file watching │
37
+ │ • getRelations() │ │ • busy timeout │ │ • dedup │
38
+ │ • applySchema() │ └────────────────────────┘ └──────────────────────┘
39
+ │ • applyMigrations() │
40
+ └─────────────────────┘
41
+
42
+ ┌─────────▼──────────┐ ┌────────────────────────┐
43
+ │ RenderEngine │ │ SyncLoop │
44
+ │ │ │ │
45
+ │ • render(outputDir) │◄──│ • watch(outputDir) │
46
+ │ • resolveRelations()│ │ • setInterval polling │
47
+ │ • writeFiles() │ │ • cleanup on each tick │ ← v0.5
48
+ │ • _renderEntityCtxs │ │ • StopFn returned │
49
+ │ • writeManifest() │ └─────────────────────────┘
50
+ │ • cleanup() │ ← v0.5
51
+ └─────────────────────┘
52
+
53
+ ┌─────────▼──────────┐ ┌────────────────────────┐ ┌──────────────────────┐
54
+ │ RenderTemplates │ │ Sanitizer │ │ Lifecycle (v0.5) │
55
+ │ │ │ │ │ │
56
+ │ • compileRender() │ │ • sanitizeRow() │ │ • readManifest() │
57
+ │ • default-list │ │ • null-byte strip │ │ • writeManifest() │
58
+ │ • default-table │ │ • field length limits │ │ • manifestPath() │
59
+ │ • default-detail │ │ • audit event emission │ │ • cleanupEntityCtxs()│
60
+ │ • default-json │ └─────────────────────────┘ └──────────────────────┘
61
+ │ • interpolate() │
62
+ └─────────────────────┘
63
+
64
+ ┌─────────▼──────────┐
65
+ │ EntityQuery (v0.5) │
66
+ │ │
67
+ │ • resolveSource() │
68
+ │ • self / hasMany │
69
+ │ • manyToMany │
70
+ │ • belongsTo │
71
+ │ • custom │
72
+ │ • truncateContent() │
73
+ └─────────────────────┘
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Module breakdown
79
+
80
+ ### `Lattice` — public facade (`src/lattice.ts`)
81
+
82
+ The single public class. It wires together all internal modules and exposes the public API. Key responsibilities:
83
+
84
+ - Accepts `string | LatticeConfigInput` in the constructor, normalising both forms to a `dbPath` + table definitions
85
+ - Enforces the `define → init → CRUD/sync` lifecycle (throws if called out of order)
86
+ - Owns the event handler arrays and dispatches to them
87
+ - Delegates every operation to an internal module
88
+
89
+ ### `SQLiteAdapter` — database layer (`src/db/sqlite.ts`)
90
+
91
+ A thin wrapper around `better-sqlite3`. Responsibilities:
92
+
93
+ - `open()` — opens the connection, optionally enabling WAL mode and setting busy timeout
94
+ - `close()` — closes the connection
95
+ - `run(sql, params)` — executes a DML statement (INSERT, UPDATE, DELETE)
96
+ - `all(sql, params)` — returns `Row[]`
97
+ - `get(sql, params)` — returns one `Row | undefined`
98
+ - Exposes `.db` for the escape hatch
99
+
100
+ The adapter is synchronous — `better-sqlite3` is a synchronous binding. All Lattice methods return Promises for a consistent async API surface, but they resolve synchronously.
101
+
102
+ ### `SchemaManager` — schema registry (`src/schema/manager.ts`)
103
+
104
+ Holds all registered table and multi-table definitions. Responsibilities:
105
+
106
+ - `define(table, compiledDef)` — stores a `CompiledTableDef` (render is always a function)
107
+ - `defineMulti(name, def)` — stores a multi-table view definition
108
+ - `getPrimaryKey(table)` — returns the PK column array for a table
109
+ - `getRelations(table)` — returns the relations map for a table
110
+ - `applySchema(adapter)` — emits `CREATE TABLE IF NOT EXISTS` for every registered table
111
+ - `applyMigrations(adapter, migrations)` — creates `_lattice_migrations` and runs pending versions
112
+
113
+ `CompiledTableDef` differs from `TableDefinition` in that the `render` field is always a compiled `(rows: Row[]) => string` function. The compilation happens once in `Lattice.define()` via `compileRender()`.
114
+
115
+ ### `RenderEngine` — sync to files (`src/render/engine.ts`)
116
+
117
+ Executes the render cycle. Responsibilities:
118
+
119
+ - `render(outputDir)` — iterates all registered tables, multi-table views, and entity context definitions; renders each to a string; writes to the appropriate output file (skipping unchanged content)
120
+ - `resolveRelations(table, rows)` — for `belongsTo` relations referenced in template strings, joins to the related table in-process
121
+ - `_renderEntityContexts(outputDir)` — (v0.5) renders all `defineEntityContext()` definitions; returns `Record<string, EntityContextManifestEntry>` and writes the manifest
122
+ - `cleanup(outputDir, prevManifest, options, newManifest?)` — (v0.5) builds current slug sets from the DB and calls `cleanupEntityContexts()`
123
+ - Returns `RenderResult` with file paths and timing
124
+
125
+ File writes are skipped when the new content equals the existing file content — important for keeping LLM context file mtimes stable.
126
+
127
+ ### `EntityQuery` — entity source resolver (`src/render/entity-query.ts`) _(v0.5)_
128
+
129
+ Contains the synchronous row-resolution logic for entity context directories. Responsibilities:
130
+
131
+ - `resolveEntitySource(source, entityRow, entityPk, adapter)` — dispatches to the correct SQL query based on the source type (`self`, `hasMany`, `manyToMany`, `belongsTo`, `custom`)
132
+ - `truncateContent(content, budget?)` — applies the per-file character budget and appends the truncation notice
133
+
134
+ ### `Lifecycle` — manifest and cleanup (`src/lifecycle/`) _(v0.5)_
135
+
136
+ Two modules:
137
+
138
+ - `manifest.ts` — `readManifest()`, `writeManifest()`, `manifestPath()` and the `LatticeManifest` / `EntityContextManifestEntry` types
139
+ - `cleanup.ts` — `cleanupEntityContexts()` and the `CleanupOptions` / `CleanupResult` types
140
+
141
+ The manifest (`{outputDir}/.lattice/manifest.json`) is the single source of truth for what Lattice generated. It is written atomically after every render cycle that includes entity contexts and read by the cleanup step to determine orphans.
142
+
143
+ ### `SyncLoop` — polling loop (`src/sync/loop.ts`)
144
+
145
+ Wraps `RenderEngine` in a `setInterval` polling loop. Responsibilities:
146
+
147
+ - `watch(outputDir, opts)` — starts the loop, returns a `StopFn`
148
+ - Reads the previous manifest before each render cycle when `opts.cleanup` is set (v0.5)
149
+ - Calls `engine.cleanup()` after each render cycle when `opts.cleanup` is set (v0.5)
150
+ - Calls `onRender`, `onError`, and `onCleanup` callbacks per cycle
151
+
152
+ ### `WritebackPipeline` — agent-to-db writes (`src/writeback/pipeline.ts`)
153
+
154
+ Monitors agent-written files and ingests new entries into the database. Responsibilities:
155
+
156
+ - `define(def)` — registers a writeback definition
157
+ - `process()` — reads registered files from their last-read offset, calls `parse()`, deduplicates, and calls `persist()` for each new entry
158
+
159
+ ### `RenderTemplates` — built-in templates (`src/render/templates.ts`)
160
+
161
+ Contains the four built-in template implementations and the template compilation logic. Responsibilities:
162
+
163
+ - `compileRender(def, table, schema, adapter)` — converts a `RenderSpec` into a `(rows: Row[]) => string` function. Called once at `define()` time
164
+ - `interpolate(template, row, relations)` — replaces `{{field}}` and `{{rel.field}}` tokens
165
+ - `renderList`, `renderTable`, `renderDetail`, `renderJson` — the four built-in renderers
166
+
167
+ ### `Sanitizer` — input safety (`src/security/sanitize.ts`)
168
+
169
+ Applied to every row before it reaches the database. Responsibilities:
170
+
171
+ - Strip null bytes from string values
172
+ - Apply field length limits (`SecurityOptions.fieldLimits`)
173
+ - Emit `AuditEvent` on each write operation
174
+
175
+ ### Config layer (`src/config/`)
176
+
177
+ Two modules:
178
+
179
+ - `types.ts` — TypeScript types for `LatticeConfig`, `LatticeEntityDef`, `LatticeFieldDef`, `LatticeEntityRenderSpec`
180
+ - `parser.ts` — `parseConfigFile()` and `parseConfigString()`: read YAML, validate, convert to `ParsedConfig` (array of `{ name, TableDefinition }`)
181
+
182
+ ### Codegen layer (`src/codegen/`)
183
+
184
+ - `generate.ts` — `generateTypes()`, `generateMigration()`, `generateAll()`: string generators for `types.ts` and `migration.sql`
185
+
186
+ ### CLI (`src/cli.ts`)
187
+
188
+ Standalone entry point compiled to `dist/cli.js` with a `#!/usr/bin/env node` shebang. Uses no external CLI framework — just manual `process.argv` parsing. Calls `generateAll()` and logs results.
189
+
190
+ ---
191
+
192
+ ## Data flow
193
+
194
+ ### Insert flow
195
+
196
+ ```
197
+ db.insert(table, row)
198
+ → Sanitizer.sanitizeRow(row)
199
+ → resolve PK (auto-UUID if default 'id' and absent)
200
+ → SQLiteAdapter.run(INSERT INTO ...)
201
+ → Sanitizer.emitAudit(table, 'insert', id)
202
+ → emit 'audit' event to handlers
203
+ → return Promise.resolve(pkValue)
204
+ ```
205
+
206
+ ### Render flow
207
+
208
+ ```
209
+ db.render(outputDir) / SyncLoop (watch)
210
+ → RenderEngine.render(outputDir)
211
+ → for each registered table:
212
+ → SQLiteAdapter.all(SELECT * FROM table)
213
+ → apply table.filter (if defined)
214
+ → apply hooks.beforeRender (if defined)
215
+ → resolve belongsTo relations (for template interpolation)
216
+ → call compiled render function(rows)
217
+ → compare output to existing file content
218
+ → write file if changed
219
+ → for each multi-table view:
220
+ → call def.keys() for anchor rows
221
+ → for each anchor: query tables, call def.render(key, tables)
222
+ → write files
223
+ → _renderEntityContexts(outputDir): ← v0.5
224
+ → for each entity context definition:
225
+ → render index file (if defined)
226
+ → for each entity row:
227
+ → derive slug → entity subdirectory
228
+ → for each EntityFileSpec:
229
+ → resolveEntitySource(source, row, pk, adapter)
230
+ → call spec.render(rows)
231
+ → apply budget truncation (if defined)
232
+ → skip write if omitIfEmpty and rows empty
233
+ → write file if content changed
234
+ → write combined file (if defined)
235
+ → writeManifest(outputDir, manifest)
236
+ → return RenderResult
237
+ ```
238
+
239
+ ### Reconcile flow _(v0.5)_
240
+
241
+ ```
242
+ db.reconcile(outputDir, options)
243
+ → prevManifest = readManifest(outputDir) ← read BEFORE render
244
+ → RenderEngine.render(outputDir) ← writes new manifest
245
+ → newManifest = readManifest(outputDir)
246
+ → cleanupEntityContexts(
247
+ outputDir,
248
+ entityContexts,
249
+ currentSlugsByTable, ← fresh DB query
250
+ prevManifest,
251
+ options,
252
+ newManifest ← used to detect omitIfEmpty-skipped files
253
+ )
254
+ → return ReconcileResult { ...renderResult, cleanup: CleanupResult }
255
+ ```
256
+
257
+ ### Sync flow
258
+
259
+ ```
260
+ db.sync(outputDir)
261
+ → RenderEngine.render(outputDir) ← same as render()
262
+ → WritebackPipeline.process() ← read agent files, ingest entries
263
+ → return SyncResult
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Design decisions
269
+
270
+ **Synchronous SQLite, async API surface.** `better-sqlite3` is synchronous. All Lattice methods still return `Promise<T>` — this allows callers to use `await` and keeps the API contract stable if an async adapter is ever added. The promises resolve in the same tick.
271
+
272
+ **Compile at define-time, not render-time.** `compileRender()` converts a `RenderSpec` (which can be a string, object, or function) into a single `(rows: Row[]) => string` function when `define()` is called. This ensures zero per-cycle overhead for template dispatch.
273
+
274
+ **No ORM, no query builder.** Lattice does not attempt to abstract SQL. The `columns` spec is a raw SQLite type string. Advanced queries use `db.db` (escape hatch). This keeps the library small and avoids the impedance mismatch that plagues ORMs.
275
+
276
+ **Config form is a thin wrapper over `define()`.** `new Lattice({ config })` calls `parseConfigFile()` then loops over the result calling `this.define()`. The config form is not a separate code path — it just automates the manual `define()` calls.
277
+
278
+ **Files are skipped when content is unchanged.** `RenderEngine` compares the rendered string to the file's current content before writing. This prevents unnecessary filesystem writes and keeps file modification times stable (important for LLM context systems that watch mtimes).
279
+
280
+ **Relation resolution happens in-process.** When a `belongsTo` relation is referenced in a `{{rel.field}}` token, Lattice issues a `SELECT` for each row via the adapter. This is intentionally simple — N+1 for N rows. For tables with thousands of rows this could be slow, but Lattice is designed for small-to-medium context tables (dozens to hundreds of rows), not analytics workloads.
281
+
282
+ **Manifest-driven cleanup.** Lattice never scans the output directory for files it does not recognise. Instead it only removes files and directories it previously recorded in the manifest. This means files created by agents or other tools in entity directories are never touched — unless they happen to match a filename Lattice manages, which is why `protectedFiles` exists as an escape valve.
283
+
284
+ **Render before cleanup.** `reconcile()` always runs the render cycle first, writing a new manifest, before running cleanup. This ensures the cleanup step has both the previous state (what to remove) and the current state (what is still being written) and can correctly detect `omitIfEmpty` files that were skipped this cycle but existed before.
285
+
286
+ ---
287
+
288
+ ## Package structure
289
+
290
+ ```
291
+ src/
292
+ ├── index.ts # Public exports
293
+ ├── lattice.ts # Lattice class (public facade)
294
+ ├── types.ts # All public TypeScript types
295
+ ├── cli.ts # CLI entry point
296
+ ├── config/
297
+ │ ├── types.ts # YAML config schema types
298
+ │ └── parser.ts # parseConfigFile / parseConfigString
299
+ ├── codegen/
300
+ │ └── generate.ts # generateTypes / generateMigration / generateAll
301
+ ├── db/
302
+ │ └── sqlite.ts # SQLiteAdapter
303
+ ├── schema/
304
+ │ └── manager.ts # SchemaManager
305
+ ├── render/
306
+ │ ├── engine.ts # RenderEngine (+ entity context rendering, v0.5)
307
+ │ ├── templates.ts # Built-in templates + compileRender + interpolate
308
+ │ └── entity-query.ts # resolveEntitySource + truncateContent (v0.5)
309
+ ├── lifecycle/ # v0.5
310
+ │ ├── index.ts # Barrel export
311
+ │ ├── manifest.ts # readManifest / writeManifest / manifestPath
312
+ │ └── cleanup.ts # cleanupEntityContexts + CleanupOptions/Result
313
+ ├── sync/
314
+ │ └── loop.ts # SyncLoop (+ cleanup integration, v0.5)
315
+ ├── writeback/
316
+ │ └── pipeline.ts # WritebackPipeline
317
+ └── security/
318
+ └── sanitize.ts # Sanitizer
319
+
320
+ tests/
321
+ ├── unit/
322
+ │ ├── config.test.ts # parseConfigFile / parseConfigString
323
+ │ ├── codegen.test.ts # generateTypes / generateMigration + integration
324
+ │ ├── lattice.test.ts # Core CRUD / query / render tests
325
+ │ └── entity-query.test.ts # resolveEntitySource unit tests (v0.5)
326
+ ├── integration/
327
+ │ ├── entity-context.test.ts # defineEntityContext() flow (v0.5)
328
+ │ └── lifecycle.test.ts # reconcile() + cleanup (v0.5)
329
+ └── fixtures/
330
+ └── lattice.config.yml
331
+ ```
@@ -0,0 +1,138 @@
1
+ # The AI assistant & Context Constructor (2.0+)
2
+
3
+ `lattice gui` ships an optional assistant rail. It is **GUI-only** and inert
4
+ until you configure a credential — the `latticesql` library API is unchanged.
5
+
6
+ ## Connect Claude
7
+
8
+ Open **Settings → User → Assistant** and paste an Anthropic API key, or set
9
+ `ANTHROPIC_API_KEY` in the environment. Keys are stored encrypted in the native
10
+ `secrets` entity; the env var is a fallback. That's all the chat and the
11
+ Context Constructor need.
12
+
13
+ A **"Connect your Claude subscription"** link (Authorization-Code + PKCE) appears
14
+ only when all four `ANTHROPIC_OAUTH_*` values are configured (see
15
+ [`.env.example`](../.env.example)); otherwise the panel shows a dormant hint.
16
+ Use a fixed GUI port so the redirect URI is stable: `lattice gui --port 4317`.
17
+
18
+ ## Chat
19
+
20
+ The rail runs a Claude tool-calling loop streamed over SSE. The model can list,
21
+ read, **full-text search**, create, update, link, delete tables, and revert in
22
+ the active database. **Every edit goes through the same audited, undoable
23
+ mutation path as a manual edit** — it appears in the activity feed and the
24
+ version history and can be reverted.
25
+
26
+ The **top search box hands your query to the assistant**: type and press Enter
27
+ and the query is submitted as a chat turn, which the assistant answers using its
28
+ `search` (and read) tools rather than a plain text match. The assistant never
29
+ sees the conversation-storage or `secrets` tables (search and `list_entities`
30
+ both exclude them).
31
+
32
+ When the assistant points you at a specific record — ask it to "link me to" or
33
+ "open" one — it renders a **clickable object pill** inline in its answer
34
+ (emitted as `[label](lattice://<table>/<id>)`). Clicking the pill opens that row
35
+ in the GUI via the same mode-aware navigator the activity feed uses; it links the
36
+ user-facing record (the contract/person/etc.) rather than an internal `files` id,
37
+ and only ids it actually retrieved.
38
+
39
+ **Deleting a table is guarded + reversible.** The `delete_entity` tool refuses
40
+ built-in tables, tables another table links to, and tables you don't own. An
41
+ **empty** table is soft-deleted immediately; a **non-empty** one is **not**
42
+ deleted until you decide what happens to the data — the tool reports the row
43
+ count and the assistant asks, then you choose `delete_data` (soft-delete the rows
44
+ too) or `move_to` another table. The physical table + rows are kept (no hard
45
+ drop), so the whole thing is revertible from version history.
46
+
47
+ Conversations persist in the native `chat_threads` / `chat_messages` entities;
48
+ use the thread switcher to revisit them. A new thread is **named from a short AI
49
+ summary** of its first exchange (e.g. "Adding New Notes About Cheese"). The
50
+ assistant's **data changes are saved with each turn and replayed as activity
51
+ cards** when you reopen the conversation — collapsed by type (e.g. "Deleted 19
52
+ tables", "Removed 49 rows across 9 tables"), with the operation's icon. Reads
53
+ (list / get / search) change nothing, so they produce no card; only data changes
54
+ appear. The activity feed is scoped to the open conversation rather than a global
55
+ workspace log.
56
+
57
+ The assistant **remembers what it read across turns.** Earlier tool calls and
58
+ their results (including row ids) are replayed into the model's context, so a
59
+ follow-up like "now update that row" reuses the id it just listed instead of
60
+ guessing one. Replay is bounded to the recent turns within a size budget and is
61
+ secret-redacted; set `LATTICE_CHAT_REHYDRATE=false` to disable it. Reads are also
62
+ deterministically ordered, so listing the same table twice returns the same rows.
63
+
64
+ The assistant **knows the record you're looking at.** When a file or row detail is
65
+ open, the chat passes that record (table + id) as context, so "delete this file",
66
+ "summarize this", or "share this row" act on it directly instead of asking which
67
+ one. It's a hint only — every action still goes through the same permission-gated
68
+ tools, so it can't reach a record you couldn't otherwise touch.
69
+
70
+ The assistant can also **answer questions about Lattice itself.** Ask "what is
71
+ private mode?" or "how do I invite a member?" and it calls the `lattice_help` tool,
72
+ which searches Lattice's own documentation (these `docs/*.md` files — the single
73
+ canonical source, shipped in the npm package) and answers from it rather than
74
+ guessing or searching your data.
75
+
76
+ ## The Context Constructor (file & text ingest)
77
+
78
+ Drag files onto the rail, click the paperclip, or paste text (or a URL). For each
79
+ source:
80
+
81
+ 1. **Referenced, not copied.** The source becomes a native `files` row that
82
+ points at the original; bytes are not moved into Lattice.
83
+ 2. **Extracted.** Plain text/markdown/code is read directly; documents
84
+ (PDF, Word `.docx`/`.doc`, PowerPoint `.pptx`, Excel `.xlsx`, OpenDocument
85
+ `.odt`/`.ods`/`.odp`, EPUB, RTF) are parsed **natively in-process** — no
86
+ external CLI; **images are described by Claude vision**; **scanned/image-only
87
+ PDFs** with no text layer fall back to Claude's native PDF read; a pasted
88
+ **bare URL is crawled** for readable text (and the URL preserved on the row as
89
+ a `cloud_ref`). Legacy binary `.xls`/`.ppt` (pre-2007) and any other binary
90
+ are still referenced and marked `extraction_status='skipped'`. The parsers
91
+ ship as optional dependencies, so a document just skips (rather than failing)
92
+ if its parser isn't installed.
93
+ 3. **Summarized** with Claude Haiku (the description fills in).
94
+ 4. **Organized.** The text is classified against your existing records, and for
95
+ each match the file is **linked** — **auto-creating the `files_<entity>` junction
96
+ table when none exists yet**. When a source fits **nothing** (and aggressiveness
97
+ is high), a new native `notes` object is **created** for it, linked back via
98
+ `source_file_id`. New objects, enrichment, links, and junctions are all
99
+ reversible via the version history.
100
+
101
+ ### Library API
102
+
103
+ The same intelligence is a first-class, GUI-independent API (inert without an LLM
104
+ client): `organizeSource`, `describeImage`, `crawlUrl`, `enrichKnowledge`, and the
105
+ `summarizeText` / `classifyLinks` primitives — all importable from `latticesql`.
106
+ `sharp` + `file-type` are optional, lazily-loaded deps; the crawler uses `jsdom` +
107
+ `@mozilla/readability`.
108
+
109
+ A transient **"Analyzing…"** row shows while ingest runs; the add/enrich/link
110
+ events stream into the feed as the server materializes them.
111
+
112
+ ## Inference Aggressiveness
113
+
114
+ A single **Conservative ↔ Aggressive** slider (Settings → Assistant) tunes how
115
+ much the assistant extrapolates. It maps to the model sampling temperature, how
116
+ liberally the ingest classifier proposes links, and whether ingest auto-creates a
117
+ missing junction (gated at ≥ 0.25) versus only suggesting it. Default: balanced
118
+ (0.5). Settable via `PUT /api/assistant/aggressiveness { "value": 0..1 }`. This
119
+ is a **user preference** (machine-local `~/.lattice/preferences.json`), not a
120
+ workspace secret — it persists across workspaces and never appears in a
121
+ workspace's Secrets object.
122
+
123
+ ## Voice (optional)
124
+
125
+ Set `OPENAI_API_KEY` (Whisper) or `ELEVENLABS_API_KEY` to enable the composer
126
+ mic; choose the provider in the Assistant settings (also a machine-local user
127
+ preference, not a workspace secret). When no microphone is available the mic
128
+ button is shown disabled with a tooltip rather than erroring. **While a note is
129
+ recording or transcribing, the composer is read-only** — it shows a
130
+ "Listening… / Transcribing…" placeholder and the Send button is disabled — and
131
+ the transcript is inserted when you stop.
132
+
133
+ ## Cloud
134
+
135
+ The assistant runs against local SQLite and any `postgres://` connection, including
136
+ a Lattice cloud. On a cloud it connects as your own scoped role, so its reads and
137
+ writes are confined by Postgres Row-Level Security to the rows you may see — see
138
+ [cloud.md](cloud.md).