Documentation

Full reference for latticesql. View source on GitHub · Changelog

Installation

bash
npm install latticesql

Requires Node.js 18+. Uses better-sqlite3 — no external database process needed.

Quick start

typescript
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:

typescript
const db = new Lattice({ config: './lattice.config.yml' });
await db.init();
// Tables and render functions are wired automatically from the config

YAML 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.

yaml
# 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 typeSQLite typeTypeScript type
uuidTEXTstring
textTEXTstring
integer / intINTEGERnumber
real / floatREALnumber
boolean / boolINTEGERboolean
datetime / dateTEXTstring
blobBLOBBuffer

API reference

Constructor

typescript
// 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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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 exit

CRUD operations

All CRUD methods return Promises. The filters array supports eq, ne, gt, gte, lt, lte, like, in, isNull, isNotNull.

typescript
// 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()

typescript
// 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

typescript
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

TypeWhat 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 generate

Generate TypeScript interface types, a SQL migration file, and (optionally) scaffold render output files from a lattice.config.yml.

FlagDefaultDescription
--config, -c <path>./lattice.config.ymlPath to the YAML config file
--out, -o <dir>./generatedOutput directory for generated files
--scaffoldoffCreate empty scaffold render output files
bash
npx lattice generate --config ./lattice.config.yml --out ./generated --scaffold
lattice render

One-shot context generation. Reads the config, connects to the database, and writes all entity context files.

FlagDefaultDescription
--config, -c <path>./lattice.config.ymlPath to the YAML config file
--output <dir>./contextOutput directory for rendered context files
bash
npx lattice render --config ./lattice.config.yml --output ./context
lattice reconcile

Render + orphan cleanup. Writes entity context directories then removes any orphaned entity directories and files no longer in the database.

FlagDefaultDescription
--config, -c <path>./lattice.config.ymlPath to the YAML config file
--output <dir>./contextOutput directory
--dry-runoffReport orphans but do not delete anything
--protected <csv>Comma-separated list of protected filenames
bash
npx lattice reconcile --output ./context --protected SESSION.md
lattice status

Dry-run reconcile — shows what would change without writing or deleting anything. Alias for lattice reconcile --dry-run.

FlagDefaultDescription
--config, -c <path>./lattice.config.ymlPath to the YAML config file
--output <dir>./contextOutput directory
bash
npx lattice status --output ./context
lattice watch

Starts a polling loop that re-renders entity context directories on each interval. Optionally runs orphan cleanup after each cycle.

FlagDefaultDescription
--config, -c <path>./lattice.config.ymlPath to the YAML config file
--output <dir>./contextOutput directory
--interval <ms>5000Poll interval in milliseconds
--cleanupoffEnable orphan cleanup after each render cycle
--protected <csv>Comma-separated list of protected filenames (requires --cleanup)
bash
npx lattice watch --output ./context --interval 3000 --cleanup --protected SESSION.md