Documentation
Full reference for latticesql. View source on GitHub · Changelog
Installation
npm install latticesqlRequires Node.js 18+. Uses better-sqlite3 — no external database process needed.
Quick start
import { Lattice } from 'latticesql';
const db = new Lattice('./state.db');
db.define('tasks', {
columns: {
id: 'TEXT PRIMARY KEY',
title: 'TEXT NOT NULL',
status: 'TEXT DEFAULT "open"',
due: 'TEXT',
},
render(rows) {
const open = rows.filter((r) => r.status === 'open');
return (
`# Open Tasks (${open.length})\n\n` +
open.map((r) => `- [ ] ${r.title}${r.due ? ` — due ${r.due}` : ''}`).join('\n')
);
},
outputFile: 'TASKS.md',
});
await db.init();
await db.insert('tasks', { title: 'Write docs', status: 'open' });
await db.insert('tasks', { title: 'Ship v1', status: 'open', due: '2026-04-01' });
// Render DB → context files
await db.render('./context');
// Writes: context/TASKS.md
// Watch for changes
const stop = await db.watch('./context', { interval: 5000 });
// ...
stop();
db.close();The YAML config form (v0.4+) lets you declare your schema in a file instead of code:
const db = new Lattice({ config: './lattice.config.yml' });
await db.init();
// Tables and render functions are wired automatically from the configYAML config (v0.4+)
Define your entire schema in a lattice.config.yml file. Lattice reads it at construction time, creates all tables on init(), and wires render functions automatically.
# lattice.config.yml
db: ./data/app.db
entities:
user:
fields:
id: { type: uuid, primaryKey: true }
name: { type: text, required: true }
email: { type: text }
role: { type: text, default: member }
render: default-table
outputFile: context/USERS.md
task:
fields:
id: { type: uuid, primaryKey: true }
title: { type: text, required: true }
status: { type: text, default: open }
priority: { type: integer, default: 1 }
assignee_id: { type: uuid, ref: user }
render:
template: default-list
formatRow: '{{title}} ({{status}}) — {{assignee.name}}'
outputFile: context/TASKS.md| YAML type | SQLite type | TypeScript type |
|---|---|---|
| uuid | TEXT | string |
| text | TEXT | string |
| integer / int | INTEGER | number |
| real / float | REAL | number |
| boolean / bool | INTEGER | boolean |
| datetime / date | TEXT | string |
| blob | BLOB | Buffer |
API reference
Constructor
// String path
new Lattice('./app.db')
// In-memory (tests)
new Lattice(':memory:')
// From YAML config
new Lattice({ config: './lattice.config.yml' })
// With options
new Lattice('./app.db', {
wal: true,
busyTimeout: 10_000,
security: {
sanitize: true,
auditTables: ['users', 'credentials'],
fieldLimits: { notes: 50_000, bio: 2_000 },
},
})LatticeOptions supports wal (WAL journal mode, default true), busyTimeout (default 5000 ms), and security (sanitize, auditTables, fieldLimits).
define()
Register a table. Must be called before init(). Returns this for chaining.
db.define('agents', {
columns: {
id: 'TEXT PRIMARY KEY',
name: 'TEXT NOT NULL',
persona: 'TEXT',
active: 'INTEGER DEFAULT 1',
},
render(rows) {
return rows
.filter((r) => r.active)
.map((r) => `## ${r.name}\n\n${r.persona ?? ''}`)
.join('\n\n---\n\n');
},
outputFile: 'AGENTS.md',
});The render field accepts a function, a built-in template name ('default-list', 'default-table', 'default-detail', 'default-json'), or a template spec with hooks.
defineMulti()
Produces one output file per anchor entity. Useful for per-agent or per-project context files where you need full control over queries.
db.defineMulti('agent-context', {
// One file per active agent
keys: () => db.query('agents', { where: { active: 1 } }),
outputFile: (agent) => `agents/${agent.slug}/CONTEXT.md`,
tables: ['tasks', 'notes'],
render(agent, { tasks, notes }) {
const myTasks = tasks.filter((t) => t.assigned_to === agent.id);
const myNotes = notes.filter((n) => n.agent_id === agent.id);
return [
`# ${agent.name} — context`,
'',
'## Pending tasks',
myTasks.map((t) => `- ${t.title}`).join('\n') || '_none_',
'',
'## Notes',
myNotes.map((n) => `- ${n.body}`).join('\n') || '_none_',
].join('\n');
},
});defineEntityContext() — v0.5+
Generate a parallel file-system tree for an entity type — one subdirectory per row, one file per declared relationship, and an optional combined context file. Lattice handles all querying, directory creation, hash-skip deduplication, and orphan cleanup.
db.defineEntityContext('agents', {
slug: (row) => row.slug as string,
index: {
outputFile: 'agents/AGENTS.md',
render: (rows) => `# Agents\n\n${rows.map((r) => `- ${r.name}`).join('\n')}`,
},
files: {
'AGENT.md': {
source: { type: 'self' },
render: ([r]) => `# ${r.name}\n\n${r.bio ?? ''}`,
},
'TASKS.md': {
source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id' },
render: (rows) => rows.map((r) => `- ${r.title}`).join('\n'),
omitIfEmpty: true,
budget: 4000,
},
'SKILLS.md': {
source: {
type: 'manyToMany',
junctionTable: 'agent_skills',
localKey: 'agent_id',
remoteKey: 'skill_id',
remoteTable: 'skills',
},
render: (rows) => rows.map((r) => `- ${r.name}`).join('\n'),
omitIfEmpty: true,
},
},
combined: { outputFile: 'CONTEXT.md', exclude: [] },
protectedFiles: ['SESSION.md'],
});defineWriteback()
Register an agent-output file for parsing and DB ingestion. Lattice tracks file offsets and handles rotation (truncation) automatically.
db.defineWriteback({
file: './context/agents/*/SESSION.md',
parse(content, fromOffset) {
const newContent = content.slice(fromOffset);
const entries = parseMarkdownItems(newContent);
return { entries, nextOffset: content.length };
},
async persist(entry, filePath) {
await db.insert('events', {
source_file: filePath,
...(entry as Row),
});
},
dedupeKey: (entry) => (entry as { id: string }).id,
});init() / close()
init() opens the SQLite file, runs CREATE TABLE IF NOT EXISTS for all defined tables, and applies any migrations. Must be called once before CRUD or render operations.
await db.init({
migrations: [
{ version: 1, sql: 'ALTER TABLE tasks ADD COLUMN due_date TEXT' },
{ version: 2, sql: 'ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0' },
],
});
// Migrations are idempotent — each version is applied exactly once.
// Safe to call init() on every process start.
db.close(); // call on process exitCRUD operations
All CRUD methods return Promises. The filters array supports eq, ne, gt, gte, lt, lte, like, in, isNull, isNotNull.
// Insert — returns the generated id
const id = await db.insert('tasks', { title: 'Write docs', status: 'open' });
// Upsert by PK
await db.upsert('tasks', { id: 'task-001', title: 'Updated', status: 'done' });
// Upsert by arbitrary column
await db.upsertBy('users', 'email', 'alice@example.com', { name: 'Alice' });
// Update specific fields
await db.update('tasks', 'task-001', { status: 'done' });
// Get by PK
const task = await db.get('tasks', 'task-001');
// Query with filters
const open = await db.query('tasks', {
where: { status: 'open' },
filters: [{ col: 'priority', op: 'gte', val: 3 }],
orderBy: 'created_at',
orderDir: 'desc',
limit: 20,
});
// Count
const n = await db.count('tasks', { where: { status: 'open' } });
// Delete
await db.delete('tasks', 'task-001');render() / sync() / watch() / reconcile()
// One-shot render
const result = await db.render('./context');
// { filesWritten: ['context/TASKS.md'], filesSkipped: 2, durationMs: 12 }
// render() + writeback pipeline
const syncResult = await db.sync('./context');
// Watch with polling
const stop = await db.watch('./context', {
interval: 5_000,
onRender: (r) => console.log('rendered', r.filesWritten.length, 'files'),
onError: (e) => console.error(e.message),
cleanup: {
removeOrphanedDirectories: true,
removeOrphanedFiles: true,
protectedFiles: ['SESSION.md'],
},
});
// One-shot render + orphan cleanup
const reconcileResult = await db.reconcile('./context', {
removeOrphanedDirectories: true,
removeOrphanedFiles: true,
protectedFiles: ['SESSION.md'],
dryRun: false,
});Files are written atomically (write to temp, rename). Files whose content has not changed since the last render are skipped.
Events
db.on('audit', ({ table, operation, id, timestamp }) => {
auditLog.write({ table, operation, id, timestamp });
});
db.on('render', ({ filesWritten, filesSkipped, durationMs }) => {
console.log(`Rendered ${filesWritten.length} files in ${durationMs}ms`);
});
db.on('writeback', ({ filePath, entriesProcessed }) => {
console.log(`Processed ${entriesProcessed} entries from ${filePath}`);
});
db.on('error', (err: Error) => {
console.error('Lattice error:', err.message);
});audit events fire on every insert/update/delete for tables listed in security.auditTables. Use them to build an audit log.
Entity context directories (v0.5+)
defineEntityContext() is the high-level API for per-entity file generation. Declare the structure — which tables to pull, how to render them, what budget to enforce — and Lattice handles all the querying, directory creation, hash-skip deduplication, and orphan cleanup.
Source types
| Type | What it queries |
|---|---|
| { type: 'self' } | The entity row itself |
| { type: 'hasMany', table, foreignKey } | Rows in table where foreignKey = entityPk |
| { type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable } | Remote rows via a junction table |
| { type: 'belongsTo', table, foreignKey } | Single parent row via FK on this entity |
| { type: 'custom', query: (row, adapter) => Row[] } | Fully custom synchronous query |
Orphan cleanup
When you delete an entity from the database, the old directory becomes an orphan. Use reconcile() to clean it up. Lattice writes a .lattice/manifest.json inside the output directory after every render cycle — this is what reconcile() uses to know which directories it owns.
Declare protectedFiles — files agents write — and Lattice will never delete them during cleanup. If an entity is deleted and its directory still contains protected files, Lattice removes only its own managed files and leaves the directory with a warning.
CLI reference
The lattice CLI is bundled with the package. Available via npx lattice or as a script in package.json.
lattice generateGenerate TypeScript interface types, a SQL migration file, and (optionally) scaffold render output files from a lattice.config.yml.
| Flag | Default | Description |
|---|---|---|
| --config, -c <path> | ./lattice.config.yml | Path to the YAML config file |
| --out, -o <dir> | ./generated | Output directory for generated files |
| --scaffold | off | Create empty scaffold render output files |
npx lattice generate --config ./lattice.config.yml --out ./generated --scaffoldlattice renderOne-shot context generation. Reads the config, connects to the database, and writes all entity context files.
| Flag | Default | Description |
|---|---|---|
| --config, -c <path> | ./lattice.config.yml | Path to the YAML config file |
| --output <dir> | ./context | Output directory for rendered context files |
npx lattice render --config ./lattice.config.yml --output ./contextlattice reconcileRender + orphan cleanup. Writes entity context directories then removes any orphaned entity directories and files no longer in the database.
| Flag | Default | Description |
|---|---|---|
| --config, -c <path> | ./lattice.config.yml | Path to the YAML config file |
| --output <dir> | ./context | Output directory |
| --dry-run | off | Report orphans but do not delete anything |
| --protected <csv> | — | Comma-separated list of protected filenames |
npx lattice reconcile --output ./context --protected SESSION.mdlattice statusDry-run reconcile — shows what would change without writing or deleting anything. Alias for lattice reconcile --dry-run.
| Flag | Default | Description |
|---|---|---|
| --config, -c <path> | ./lattice.config.yml | Path to the YAML config file |
| --output <dir> | ./context | Output directory |
npx lattice status --output ./contextlattice watchStarts a polling loop that re-renders entity context directories on each interval. Optionally runs orphan cleanup after each cycle.
| Flag | Default | Description |
|---|---|---|
| --config, -c <path> | ./lattice.config.yml | Path to the YAML config file |
| --output <dir> | ./context | Output directory |
| --interval <ms> | 5000 | Poll interval in milliseconds |
| --cleanup | off | Enable orphan cleanup after each render cycle |
| --protected <csv> | — | Comma-separated list of protected filenames (requires --cleanup) |
npx lattice watch --output ./context --interval 3000 --cleanup --protected SESSION.md