latticesql 0.5.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,869 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ interface LatticeManifest {
4
+ version: 1;
5
+ generated_at: string;
6
+ entityContexts: Record<string, EntityContextManifestEntry>;
7
+ }
8
+ interface EntityContextManifestEntry {
9
+ directoryRoot: string;
10
+ indexFile?: string;
11
+ declaredFiles: string[];
12
+ protectedFiles: string[];
13
+ /** Key = slug, value = filenames actually written (omitIfEmpty files may be absent) */
14
+ entities: Record<string, string[]>;
15
+ }
16
+ declare function manifestPath(outputDir: string): string;
17
+ declare function readManifest(outputDir: string): LatticeManifest | null;
18
+ declare function writeManifest(outputDir: string, manifest: LatticeManifest): void;
19
+
20
+ /** Pluggable storage backend interface */
21
+ interface StorageAdapter {
22
+ /** Execute a statement with no return value */
23
+ run(sql: string, params?: unknown[]): void;
24
+ /** Execute a statement and return one row or undefined */
25
+ get(sql: string, params?: unknown[]): Row | undefined;
26
+ /** Execute a statement and return all rows */
27
+ all(sql: string, params?: unknown[]): Row[];
28
+ /** Prepare and cache a statement for repeated execution */
29
+ prepare(sql: string): PreparedStatement;
30
+ /** Open the connection */
31
+ open(): void;
32
+ /** Close the connection */
33
+ close(): void;
34
+ }
35
+ interface PreparedStatement {
36
+ run(...params: unknown[]): {
37
+ changes: number;
38
+ lastInsertRowid: number | bigint;
39
+ };
40
+ get(...params: unknown[]): Row | undefined;
41
+ all(...params: unknown[]): Row[];
42
+ }
43
+
44
+ /**
45
+ * Yield the entity row itself as a single-element array.
46
+ * Use for the primary entity file (e.g. `AGENT.md`).
47
+ */
48
+ interface SelfSource {
49
+ type: 'self';
50
+ }
51
+ /**
52
+ * Query rows from another table where a foreign key on that table points back
53
+ * to this entity (e.g. all tasks where `tasks.agent_id = agent.id`).
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id' }
58
+ * ```
59
+ */
60
+ interface HasManySource {
61
+ type: 'hasMany';
62
+ /** The related table to query */
63
+ table: string;
64
+ /** Column on the RELATED table that holds the FK pointing to this entity */
65
+ foreignKey: string;
66
+ /**
67
+ * Column on THIS entity's table that is referenced.
68
+ * Defaults to the entity table's first primary key column.
69
+ */
70
+ references?: string;
71
+ }
72
+ /**
73
+ * Query rows from a remote table via a junction table
74
+ * (e.g. skills for an agent via `agent_skills`).
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * source: {
79
+ * type: 'manyToMany',
80
+ * junctionTable: 'agent_skills',
81
+ * localKey: 'agent_id', // FK in junction → this entity
82
+ * remoteKey: 'skill_id', // FK in junction → remote entity
83
+ * remoteTable: 'skills',
84
+ * }
85
+ * ```
86
+ */
87
+ interface ManyToManySource {
88
+ type: 'manyToMany';
89
+ /** The junction / association table */
90
+ junctionTable: string;
91
+ /** Column in the junction table that points to THIS entity */
92
+ localKey: string;
93
+ /** Column in the junction table that points to the REMOTE entity */
94
+ remoteKey: string;
95
+ /** The remote table to JOIN and return rows from */
96
+ remoteTable: string;
97
+ /**
98
+ * Primary key column on `remoteTable` that `remoteKey` references.
99
+ * Defaults to `'id'`.
100
+ */
101
+ references?: string;
102
+ }
103
+ /**
104
+ * Query the single row that this entity belongs to via a foreign key on
105
+ * THIS entity's table (e.g. the team a bot belongs to via `bot.team_id`).
106
+ *
107
+ * Returns `[]` when the FK column is NULL; returns `[row]` when found.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * source: { type: 'belongsTo', table: 'teams', foreignKey: 'team_id' }
112
+ * ```
113
+ */
114
+ interface BelongsToSource {
115
+ type: 'belongsTo';
116
+ /** The related table to look up */
117
+ table: string;
118
+ /** Column on THIS entity's table that holds the FK */
119
+ foreignKey: string;
120
+ /**
121
+ * Column on the RELATED table being referenced.
122
+ * Defaults to `'id'`.
123
+ */
124
+ references?: string;
125
+ }
126
+ /**
127
+ * Fully custom query — caller receives the entity row and the raw SQLite
128
+ * adapter, returns whatever rows they need. Use a closure to capture any
129
+ * additional context (other table names, filter conditions, etc.).
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * source: {
134
+ * type: 'custom',
135
+ * query: (row, adapter) =>
136
+ * adapter.all('SELECT * FROM events WHERE actor_id = ? ORDER BY ts DESC LIMIT 20', [row.id]),
137
+ * }
138
+ * ```
139
+ */
140
+ interface CustomSource {
141
+ type: 'custom';
142
+ query: (row: Row, adapter: StorageAdapter) => Row[];
143
+ }
144
+ /** Union of all supported source types for {@link EntityFileSpec}. */
145
+ type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsToSource | CustomSource;
146
+ /**
147
+ * Specification for a single file generated inside an entity's directory.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * 'SKILLS.md': {
152
+ * source: { type: 'manyToMany', junctionTable: 'agent_skills', localKey: 'agent_id',
153
+ * remoteKey: 'skill_id', remoteTable: 'skills' },
154
+ * render: (rows) => `# Skills\n\n${rows.map(r => `- ${r.name}`).join('\n')}`,
155
+ * omitIfEmpty: true,
156
+ * budget: 2000,
157
+ * }
158
+ * ```
159
+ */
160
+ interface EntityFileSpec {
161
+ /** Determines what rows are passed to {@link render}. */
162
+ source: EntityFileSource;
163
+ /**
164
+ * Converts the resolved rows into the file's markdown content.
165
+ * For `self` sources, `rows` is always a single-element array.
166
+ */
167
+ render: (rows: Row[]) => string;
168
+ /**
169
+ * Maximum number of characters allowed in the rendered output.
170
+ * Content exceeding this limit is truncated with a notice appended.
171
+ */
172
+ budget?: number;
173
+ /**
174
+ * When `true`, skip writing this file if the source returns zero rows.
175
+ * Defaults to `false`.
176
+ */
177
+ omitIfEmpty?: boolean;
178
+ }
179
+ /**
180
+ * Defines the parallel file-system structure for one entity type.
181
+ *
182
+ * Lattice uses this to generate:
183
+ * - An optional global index file listing all entities
184
+ * - A per-entity subdirectory with one file per declared {@link files} entry
185
+ * - An optional combined context file (CONTEXT.md) concatenating all files
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * db.defineEntityContext('agents', {
190
+ * slug: (row) => row.slug as string,
191
+ * index: { outputFile: 'agents/AGENTS.md', render: (rows) => `# Agents\n...` },
192
+ * files: {
193
+ * 'AGENT.md': { source: { type: 'self' }, render: ([r]) => `# ${r.name}` },
194
+ * 'SKILLS.md': { source: { type: 'manyToMany', ... }, render, omitIfEmpty: true },
195
+ * },
196
+ * combined: { outputFile: 'CONTEXT.md' },
197
+ * });
198
+ * ```
199
+ */
200
+ interface EntityContextDefinition {
201
+ /**
202
+ * Derives the directory slug for this entity from its row.
203
+ * Used as the subdirectory name under {@link directoryRoot}.
204
+ *
205
+ * @example `(row) => row.slug as string`
206
+ */
207
+ slug: (row: Row) => string;
208
+ /**
209
+ * Optional global index file written once per render cycle (not per entity).
210
+ * Lists all entities of this type.
211
+ */
212
+ index?: {
213
+ /** Path relative to the `outputDir` passed to `render()` / `watch()` */
214
+ outputFile: string;
215
+ render: (rows: Row[]) => string;
216
+ };
217
+ /**
218
+ * Files written inside each entity's directory.
219
+ * Keys are filenames (e.g. `'AGENT.md'`); values define the source and renderer.
220
+ * Files are written in iteration order.
221
+ */
222
+ files: Record<string, EntityFileSpec>;
223
+ /**
224
+ * Optional combined context file inside each entity's directory.
225
+ * Lattice concatenates all per-entity files with `\n\n---\n\n` dividers.
226
+ * Files listed in `exclude` are omitted from the combined output.
227
+ */
228
+ combined?: {
229
+ /** Filename for the combined file (e.g. `'CONTEXT.md'`) */
230
+ outputFile: string;
231
+ /** Filenames to exclude from the combined output */
232
+ exclude?: string[];
233
+ };
234
+ /**
235
+ * Override the per-entity directory path relative to `outputDir`.
236
+ * Defaults to `'{directoryRoot}/{slug}'`.
237
+ *
238
+ * @example `(row) => \`custom-dir/${row.slug as string}/\``
239
+ */
240
+ directory?: (row: Row) => string;
241
+ /**
242
+ * Top-level directory owned by this entity context.
243
+ * Used by `reconcile()` to scan for orphaned subdirectories.
244
+ * Defaults to the table name.
245
+ */
246
+ directoryRoot?: string;
247
+ /**
248
+ * Files inside each entity's directory that Lattice must never delete
249
+ * during cleanup or reconciliation (e.g. agent-writable files like `SESSION.md`).
250
+ * Defaults to `[]`.
251
+ */
252
+ protectedFiles?: string[];
253
+ }
254
+
255
+ interface CleanupOptions {
256
+ /** Remove entity directories whose slug is no longer in the DB. Default: true. */
257
+ removeOrphanedDirectories?: boolean;
258
+ /** Remove files inside entity dirs that are no longer declared. Default: true. */
259
+ removeOrphanedFiles?: boolean;
260
+ /** Additional globally protected files (merged with per-entity protectedFiles). */
261
+ protectedFiles?: string[];
262
+ /** Report orphans but do not delete anything. */
263
+ dryRun?: boolean;
264
+ /** Called for each orphan before removal (or instead of removal in dryRun mode). */
265
+ onOrphan?: (path: string, kind: 'directory' | 'file') => void;
266
+ }
267
+ interface CleanupResult {
268
+ directoriesRemoved: string[];
269
+ filesRemoved: string[];
270
+ /** Directories with user files that were left in place. */
271
+ directoriesSkipped: string[];
272
+ warnings: string[];
273
+ }
274
+
275
+ type Row = Record<string, unknown>;
276
+ interface LatticeOptions {
277
+ wal?: boolean;
278
+ busyTimeout?: number;
279
+ security?: SecurityOptions;
280
+ }
281
+ interface SecurityOptions {
282
+ sanitize?: boolean;
283
+ auditTables?: string[];
284
+ fieldLimits?: Record<string, number>;
285
+ }
286
+ /**
287
+ * The primary key of a table. Either a single column name (string) or an
288
+ * ordered list of column names for composite keys.
289
+ *
290
+ * Defaults to `'id'` when omitted from a TableDefinition. When the default
291
+ * `'id'` is used and the `id` field is absent on insert, a UUID v4 is
292
+ * generated automatically. For custom single or composite keys the caller
293
+ * must supply all PK column values.
294
+ */
295
+ type PrimaryKey = string | string[];
296
+ /**
297
+ * A foreign-key relationship where THIS table holds the FK pointing to another
298
+ * table (e.g. `comment.post_id → posts.id`).
299
+ */
300
+ interface BelongsToRelation {
301
+ type: 'belongsTo';
302
+ /** The related table */
303
+ table: string;
304
+ /** Column on THIS table that holds the foreign key */
305
+ foreignKey: string;
306
+ /**
307
+ * Column on the RELATED table being referenced.
308
+ * Defaults to that table's first primary key column.
309
+ */
310
+ references?: string;
311
+ }
312
+ /**
313
+ * A relationship where ANOTHER table holds the FK pointing back to this table
314
+ * (e.g. `posts.id ← comments.post_id`).
315
+ */
316
+ interface HasManyRelation {
317
+ type: 'hasMany';
318
+ /** The related table */
319
+ table: string;
320
+ /** Column on the RELATED table that points back to this table */
321
+ foreignKey: string;
322
+ /**
323
+ * Column on THIS table being referenced.
324
+ * Defaults to this table's first primary key column.
325
+ */
326
+ references?: string;
327
+ }
328
+ /** A declared relationship between two tables. */
329
+ type Relation = BelongsToRelation | HasManyRelation;
330
+ /**
331
+ * Comparison operators available in a {@link Filter}.
332
+ *
333
+ * - `eq` / `ne` — equality / inequality
334
+ * - `gt` / `gte` / `lt` / `lte` — numeric or lexicographic comparison
335
+ * - `like` — SQL LIKE pattern (`%` is the wildcard)
336
+ * - `in` — column value is one of a list
337
+ * - `isNull` / `isNotNull` — NULL checks (no `val` needed)
338
+ */
339
+ type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'in' | 'isNull' | 'isNotNull';
340
+ /**
341
+ * A single filter clause with an explicit operator.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * { col: 'score', op: 'gte', val: 80 }
346
+ * { col: 'deleted_at', op: 'isNull' }
347
+ * { col: 'status', op: 'in', val: ['open', 'pending'] }
348
+ * { col: 'name', op: 'like', val: 'A%' }
349
+ * ```
350
+ */
351
+ interface Filter {
352
+ /** Column name to filter on */
353
+ col: string;
354
+ /** Comparison operator */
355
+ op: FilterOp;
356
+ /**
357
+ * Operand value. Not required for `isNull` / `isNotNull`.
358
+ * For `in`, must be an array.
359
+ */
360
+ val?: unknown;
361
+ }
362
+ /**
363
+ * Names of the four built-in render templates.
364
+ *
365
+ * - `default-list` — one bullet per row (supports `formatRow` hook)
366
+ * - `default-table` — GitHub-flavoured Markdown table
367
+ * - `default-detail` — section per row with all fields (supports `formatRow` hook)
368
+ * - `default-json` — `JSON.stringify(rows, null, 2)`
369
+ */
370
+ type BuiltinTemplateName = 'default-list' | 'default-table' | 'default-detail' | 'default-json';
371
+ /**
372
+ * Lifecycle hooks that customise a built-in template.
373
+ *
374
+ * - `beforeRender(rows)` — transform or filter the row array before rendering.
375
+ * - `formatRow` — control how each row is serialised to a string.
376
+ * Can be a plain function or a `{{field}}` interpolation template string.
377
+ * Supported by `default-list` and `default-detail`.
378
+ * `belongsTo` relation fields are available as `{{relationName.field}}`.
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * hooks: {
383
+ * beforeRender: (rows) => rows.filter(r => r.active),
384
+ * formatRow: '{{title}} — {{status}}',
385
+ * }
386
+ * ```
387
+ */
388
+ interface RenderHooks {
389
+ beforeRender?: (rows: Row[]) => Row[];
390
+ formatRow?: ((row: Row) => string) | string;
391
+ }
392
+ /**
393
+ * Use a built-in template, optionally with lifecycle hooks.
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * render: { template: 'default-list', hooks: { formatRow: '- {{title}} ({{status}})' } }
398
+ * ```
399
+ */
400
+ interface TemplateRenderSpec {
401
+ template: BuiltinTemplateName;
402
+ hooks?: RenderHooks;
403
+ }
404
+ /**
405
+ * The accepted value for `TableDefinition.render`:
406
+ *
407
+ * - A plain `(rows: Row[]) => string` function — full control, unchanged from v0.1/v0.2.
408
+ * - A `BuiltinTemplateName` string — use a built-in template with default settings.
409
+ * - A `TemplateRenderSpec` object — use a built-in template with lifecycle hooks.
410
+ */
411
+ type RenderSpec = ((rows: Row[]) => string) | BuiltinTemplateName | TemplateRenderSpec;
412
+ interface TableDefinition {
413
+ /** Column name → SQLite type spec (e.g. `'TEXT PRIMARY KEY'`) */
414
+ columns: Record<string, string>;
415
+ /**
416
+ * How to render DB rows into text content for the context file.
417
+ *
418
+ * - Pass a `(rows: Row[]) => string` function for full control (v0.1/v0.2 behaviour).
419
+ * - Pass a `BuiltinTemplateName` string (`'default-list'`, `'default-table'`,
420
+ * `'default-detail'`, `'default-json'`) to use a built-in template.
421
+ * - Pass a `TemplateRenderSpec` to use a built-in template with lifecycle hooks.
422
+ */
423
+ render: RenderSpec;
424
+ /** Output path relative to the outputDir passed to render/watch */
425
+ outputFile: string;
426
+ /** Optional pre-filter applied before render */
427
+ filter?: (rows: Row[]) => Row[];
428
+ /**
429
+ * Primary key column name or names.
430
+ *
431
+ * - Omit (or `'id'`): default behaviour — UUID auto-generated on insert when absent.
432
+ * - Custom string (e.g. `'slug'`): caller must supply the value on every insert.
433
+ * - Array (e.g. `['org_id', 'seq']`): composite key — caller must supply all columns.
434
+ *
435
+ * @example
436
+ * ```ts
437
+ * primaryKey: 'slug'
438
+ * primaryKey: ['tenant_id', 'ticket_id']
439
+ * ```
440
+ */
441
+ primaryKey?: PrimaryKey;
442
+ /**
443
+ * Optional table-level SQL constraints appended after the column list.
444
+ * Required for composite primary keys and multi-column unique constraints,
445
+ * which cannot be expressed in the per-column `columns` spec.
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * tableConstraints: ['PRIMARY KEY (tenant_id, seq)']
450
+ * tableConstraints: ['PRIMARY KEY (a, b)', 'UNIQUE (email, org_id)']
451
+ * ```
452
+ */
453
+ tableConstraints?: string[];
454
+ /**
455
+ * Declared relationships to other registered tables.
456
+ * Stored as metadata in v0.2; used by template rendering in v0.3+.
457
+ *
458
+ * @example
459
+ * ```ts
460
+ * relations: {
461
+ * author: { type: 'belongsTo', table: 'users', foreignKey: 'author_id' },
462
+ * comments: { type: 'hasMany', table: 'comments', foreignKey: 'post_id' },
463
+ * }
464
+ * ```
465
+ */
466
+ relations?: Record<string, Relation>;
467
+ }
468
+ interface MultiTableDefinition {
469
+ /** Returns the "anchor" entities — one output file is produced per anchor */
470
+ keys: () => Promise<Row[]>;
471
+ /** Derive the output file path from the anchor entity */
472
+ outputFile: (key: Row) => string;
473
+ /** Transform an anchor entity + related table data into text content */
474
+ render: (key: Row, tables: Record<string, Row[]>) => string;
475
+ /** Additional table names to query and pass into render */
476
+ tables?: string[];
477
+ }
478
+ interface WritebackDefinition {
479
+ /** Path or glob to agent-written files */
480
+ file: string;
481
+ /** Parse new file content starting at fromOffset; return entries and next offset */
482
+ parse: (content: string, fromOffset: number) => {
483
+ entries: unknown[];
484
+ nextOffset: number;
485
+ };
486
+ /** Persist a single parsed entry; called exactly once per unique dedupeKey */
487
+ persist: (entry: unknown, filePath: string) => Promise<void>;
488
+ /** Optional dedup key — if omitted, every entry is processed */
489
+ dedupeKey?: (entry: unknown) => string;
490
+ }
491
+ interface QueryOptions {
492
+ /**
493
+ * Equality filters — shorthand for `filters: [{ col, op: 'eq', val }]`.
494
+ * Fully backward compatible with pre-v0.2 usage.
495
+ */
496
+ where?: Record<string, unknown>;
497
+ /**
498
+ * Advanced filter clauses with full operator support.
499
+ * Combined with `where` using AND.
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * filters: [
504
+ * { col: 'priority', op: 'gte', val: 3 },
505
+ * { col: 'deleted_at', op: 'isNull' },
506
+ * { col: 'tag', op: 'in', val: ['bug', 'feature'] },
507
+ * ]
508
+ * ```
509
+ */
510
+ filters?: Filter[];
511
+ orderBy?: string;
512
+ orderDir?: 'asc' | 'desc';
513
+ limit?: number;
514
+ offset?: number;
515
+ }
516
+ interface CountOptions {
517
+ /** Equality filters (same as QueryOptions.where) */
518
+ where?: Record<string, unknown>;
519
+ /** Advanced filter clauses (same as QueryOptions.filters) */
520
+ filters?: Filter[];
521
+ }
522
+ interface InitOptions {
523
+ migrations?: Migration[];
524
+ }
525
+ interface Migration {
526
+ version: number;
527
+ sql: string;
528
+ }
529
+ interface WatchOptions {
530
+ /** Poll interval in milliseconds (default: 5000) */
531
+ interval?: number;
532
+ onRender?: (result: RenderResult) => void;
533
+ onError?: (err: Error) => void;
534
+ /**
535
+ * If set, runs orphan cleanup after each render cycle using the previous manifest.
536
+ * Safe to enable in long-running daemons — never removes protectedFiles.
537
+ */
538
+ cleanup?: CleanupOptions;
539
+ /** Called after each cleanup cycle (only when cleanup option is set). */
540
+ onCleanup?: (result: CleanupResult) => void;
541
+ }
542
+ interface RenderResult {
543
+ filesWritten: string[];
544
+ filesSkipped: number;
545
+ durationMs: number;
546
+ }
547
+ interface SyncResult extends RenderResult {
548
+ writebackProcessed: number;
549
+ }
550
+ type StopFn = () => void;
551
+ interface AuditEvent {
552
+ table: string;
553
+ operation: 'insert' | 'update' | 'delete';
554
+ id: string;
555
+ timestamp: string;
556
+ }
557
+ interface ReconcileOptions {
558
+ /** Remove entity directories whose slug is no longer in the DB. Default: true. */
559
+ removeOrphanedDirectories?: boolean;
560
+ /** Remove files inside entity dirs that are no longer declared. Default: true. */
561
+ removeOrphanedFiles?: boolean;
562
+ /** Additional globally protected files. */
563
+ protectedFiles?: string[];
564
+ /** Report orphans but do not delete anything. */
565
+ dryRun?: boolean;
566
+ /** Called for each orphan before removal. */
567
+ onOrphan?: (path: string, kind: 'directory' | 'file') => void;
568
+ }
569
+ interface ReconcileResult extends RenderResult {
570
+ cleanup: CleanupResult;
571
+ }
572
+
573
+ /**
574
+ * Initialise Lattice from a YAML config file instead of an explicit path.
575
+ *
576
+ * @example
577
+ * ```ts
578
+ * const db = new Lattice({ config: './lattice.config.yml' });
579
+ * await db.init();
580
+ * ```
581
+ */
582
+ interface LatticeConfigInput {
583
+ /** Path to `lattice.config.yml` (absolute or relative to `process.cwd()`) */
584
+ config: string;
585
+ /** Optional Lattice runtime options */
586
+ options?: LatticeOptions;
587
+ }
588
+ type EventHandler<T> = (data: T) => void;
589
+ /**
590
+ * A primary key lookup value.
591
+ * - `string` — the value of the table's single PK column (backward compatible).
592
+ * - `Record<string, unknown>` — column → value map for composite PKs.
593
+ */
594
+ type PkLookup = string | Record<string, unknown>;
595
+ declare class Lattice {
596
+ private readonly _adapter;
597
+ private readonly _schema;
598
+ private readonly _sanitizer;
599
+ private readonly _render;
600
+ private readonly _loop;
601
+ private readonly _writeback;
602
+ private _initialized;
603
+ /** Cache of actual table columns (from PRAGMA), populated after init(). */
604
+ private readonly _columnCache;
605
+ private readonly _auditHandlers;
606
+ private readonly _renderHandlers;
607
+ private readonly _writebackHandlers;
608
+ private readonly _errorHandlers;
609
+ constructor(pathOrConfig: string | LatticeConfigInput, options?: LatticeOptions);
610
+ define(table: string, def: TableDefinition): this;
611
+ defineMulti(name: string, def: MultiTableDefinition): this;
612
+ defineEntityContext(table: string, def: EntityContextDefinition): this;
613
+ defineWriteback(def: WritebackDefinition): this;
614
+ init(options?: InitOptions): Promise<void>;
615
+ close(): void;
616
+ insert(table: string, row: Row): Promise<string>;
617
+ upsert(table: string, row: Row): Promise<string>;
618
+ upsertBy(table: string, col: string, val: unknown, row: Row): Promise<string>;
619
+ update(table: string, id: PkLookup, row: Partial<Row>): Promise<void>;
620
+ delete(table: string, id: PkLookup): Promise<void>;
621
+ get(table: string, id: PkLookup): Promise<Row | null>;
622
+ query(table: string, opts?: QueryOptions): Promise<Row[]>;
623
+ count(table: string, opts?: CountOptions): Promise<number>;
624
+ render(outputDir: string): Promise<RenderResult>;
625
+ sync(outputDir: string): Promise<SyncResult>;
626
+ reconcile(outputDir: string, options?: ReconcileOptions): Promise<ReconcileResult>;
627
+ watch(outputDir: string, opts?: WatchOptions): Promise<StopFn>;
628
+ on(event: 'audit', handler: EventHandler<AuditEvent>): this;
629
+ on(event: 'render', handler: EventHandler<RenderResult>): this;
630
+ on(event: 'writeback', handler: EventHandler<{
631
+ filePath: string;
632
+ entriesProcessed: number;
633
+ }>): this;
634
+ on(event: 'error', handler: EventHandler<Error>): this;
635
+ get db(): Database.Database;
636
+ /**
637
+ * Filter a sanitized row to only include columns that actually exist in the
638
+ * table (verified via PRAGMA after init). Unregistered tables (accessed
639
+ * through the raw `.db` handle) are passed through unchanged.
640
+ *
641
+ * This is a defence-in-depth guard: column names from caller-supplied `row`
642
+ * objects are interpolated into SQL, so stripping unknown keys eliminates
643
+ * any theoretical injection vector from crafted object keys.
644
+ */
645
+ private _filterToSchemaColumns;
646
+ /**
647
+ * Build the WHERE clause and params for a PK lookup.
648
+ * - `string` → matches against the table's first PK column.
649
+ * - `Record` → matches every PK column; all must be present in the object.
650
+ */
651
+ private _pkWhere;
652
+ /**
653
+ * Convert Filter objects into SQL clause strings and bound params.
654
+ * An `in` filter with an empty array is silently ignored (produces no clause).
655
+ */
656
+ private _buildFilters;
657
+ /** Returns a rejected Promise if not initialized; null if ready. */
658
+ private _notInitError;
659
+ /**
660
+ * Returns a rejected Promise if any of the given column names are not present
661
+ * in the table's schema; null if all columns are valid.
662
+ *
663
+ * Applied on the read path (query/count) to validate WHERE and filter column
664
+ * names before they are interpolated into SQL. The write path strips unknown
665
+ * columns via _filterToSchemaColumns; the read path rejects instead to avoid
666
+ * silently discarding intended filter conditions.
667
+ *
668
+ * Unregistered tables (accessed via the raw `.db` handle) are passed through.
669
+ */
670
+ private _invalidColumnError;
671
+ private _assertNotInit;
672
+ }
673
+
674
+ /**
675
+ * Scalar types recognised in `lattice.config.yml` field definitions.
676
+ *
677
+ * | YAML type | SQLite type | TypeScript type |
678
+ * | ----------- | ----------- | --------------- |
679
+ * | `uuid` | TEXT | string |
680
+ * | `text` | TEXT | string |
681
+ * | `integer` | INTEGER | number |
682
+ * | `int` | INTEGER | number |
683
+ * | `real` | REAL | number |
684
+ * | `float` | REAL | number |
685
+ * | `boolean` | INTEGER | boolean |
686
+ * | `bool` | INTEGER | boolean |
687
+ * | `datetime` | TEXT | string |
688
+ * | `date` | TEXT | string |
689
+ * | `blob` | BLOB | Buffer |
690
+ */
691
+ type LatticeFieldType = 'uuid' | 'text' | 'integer' | 'int' | 'real' | 'float' | 'boolean' | 'bool' | 'datetime' | 'date' | 'blob';
692
+ /**
693
+ * A single field (column) definition in a `lattice.config.yml` entity.
694
+ *
695
+ * @example
696
+ * ```yaml
697
+ * id: { type: uuid, primaryKey: true }
698
+ * title: { type: text, required: true }
699
+ * status: { type: text, default: open }
700
+ * assignee_id: { type: uuid, ref: user }
701
+ * score: { type: integer, default: 0 }
702
+ * ```
703
+ */
704
+ interface LatticeFieldDef {
705
+ /** Column data type */
706
+ type: LatticeFieldType;
707
+ /** Mark this column as the table's primary key */
708
+ primaryKey?: boolean;
709
+ /** Column is NOT NULL */
710
+ required?: boolean;
711
+ /** SQL DEFAULT value */
712
+ default?: string | number | boolean;
713
+ /**
714
+ * Foreign-key reference to another entity (table name).
715
+ * Creates a `belongsTo` relation automatically.
716
+ * The relation name is derived from the field name — `_id` suffix is stripped
717
+ * (e.g. `assignee_id: { ref: user }` → relation name `assignee`).
718
+ */
719
+ ref?: string;
720
+ }
721
+ /**
722
+ * Inline render spec inside YAML — a flat object alternative to `TemplateRenderSpec`.
723
+ *
724
+ * @example
725
+ * ```yaml
726
+ * render:
727
+ * template: default-list
728
+ * formatRow: "{{title}} — {{status}}"
729
+ * ```
730
+ */
731
+ interface LatticeEntityRenderSpec {
732
+ template: string;
733
+ formatRow?: string;
734
+ }
735
+ /**
736
+ * A single entity (table) definition in `lattice.config.yml`.
737
+ *
738
+ * @example
739
+ * ```yaml
740
+ * ticket:
741
+ * fields:
742
+ * id: { type: uuid, primaryKey: true }
743
+ * title: { type: text, required: true }
744
+ * render: default-list
745
+ * outputFile: context/TICKETS.md
746
+ * ```
747
+ */
748
+ interface LatticeEntityDef {
749
+ /** Column definitions */
750
+ fields: Record<string, LatticeFieldDef>;
751
+ /**
752
+ * How to render rows into context text.
753
+ * Accepts the same forms as `TableDefinition.render`:
754
+ * - A `BuiltinTemplateName` string (e.g. `default-list`)
755
+ * - A `{ template, formatRow }` object for hooks
756
+ */
757
+ render?: string | LatticeEntityRenderSpec;
758
+ /** Render output file path (relative to the config file directory) */
759
+ outputFile: string;
760
+ /**
761
+ * Optional explicit primary key override.
762
+ * If omitted, the field with `primaryKey: true` is used.
763
+ * Accepts a single column name or an array for composite keys.
764
+ */
765
+ primaryKey?: string | string[];
766
+ }
767
+ /**
768
+ * The top-level `lattice.config.yml` document.
769
+ *
770
+ * @example
771
+ * ```yaml
772
+ * db: ./data/app.db
773
+ * entities:
774
+ * ticket:
775
+ * fields:
776
+ * id: { type: uuid, primaryKey: true }
777
+ * title: { type: text, required: true }
778
+ * render: default-list
779
+ * outputFile: context/TICKETS.md
780
+ * ```
781
+ */
782
+ interface LatticeConfig {
783
+ /** Path to the SQLite database file (relative to the config file) */
784
+ db: string;
785
+ /** Entity (table) definitions */
786
+ entities: Record<string, LatticeEntityDef>;
787
+ /** Entity context directory definitions */
788
+ entityContexts?: Record<string, LatticeEntityContextDef>;
789
+ }
790
+ /**
791
+ * Source spec in YAML config — either the shorthand string 'self' or an object.
792
+ */
793
+ type LatticeEntityContextSourceDef = 'self' | {
794
+ type: 'hasMany';
795
+ table: string;
796
+ foreignKey: string;
797
+ references?: string;
798
+ } | {
799
+ type: 'manyToMany';
800
+ junctionTable: string;
801
+ localKey: string;
802
+ remoteKey: string;
803
+ remoteTable: string;
804
+ references?: string;
805
+ } | {
806
+ type: 'belongsTo';
807
+ table: string;
808
+ foreignKey: string;
809
+ references?: string;
810
+ };
811
+ /** A single per-entity file spec in YAML config */
812
+ interface LatticeEntityContextFileDef {
813
+ source: LatticeEntityContextSourceDef;
814
+ template: string;
815
+ budget?: number;
816
+ omitIfEmpty?: boolean;
817
+ }
818
+ /** Entity context definition in YAML config */
819
+ interface LatticeEntityContextDef {
820
+ slug: string;
821
+ directoryRoot?: string;
822
+ protectedFiles?: string[];
823
+ index?: {
824
+ outputFile: string;
825
+ render: string;
826
+ };
827
+ files: Record<string, LatticeEntityContextFileDef>;
828
+ combined?: {
829
+ outputFile: string;
830
+ exclude?: string[];
831
+ };
832
+ }
833
+
834
+ /** Output of a successful config parse — ready to hand to Lattice. */
835
+ interface ParsedConfig {
836
+ /** Absolute path to the SQLite database file */
837
+ dbPath: string;
838
+ /** Table definitions in declaration order */
839
+ tables: readonly {
840
+ name: string;
841
+ definition: TableDefinition;
842
+ }[];
843
+ /** Entity context definitions in declaration order */
844
+ entityContexts: readonly {
845
+ table: string;
846
+ definition: EntityContextDefinition;
847
+ }[];
848
+ }
849
+ /**
850
+ * Read, parse, and validate a `lattice.config.yml` file.
851
+ *
852
+ * Paths inside the config (e.g. `db`, `outputFile`) are resolved relative to
853
+ * the config file's directory.
854
+ *
855
+ * @throws If the file cannot be read, the YAML is malformed, or required
856
+ * keys are missing.
857
+ */
858
+ declare function parseConfigFile(configPath: string): ParsedConfig;
859
+ /**
860
+ * Parse and validate a raw YAML string as a Lattice config.
861
+ *
862
+ * `configDir` is used to resolve relative `db` and `outputFile` paths.
863
+ * Typically this should be the directory that contains `lattice.config.yml`.
864
+ *
865
+ * Useful for testing without touching the filesystem.
866
+ */
867
+ declare function parseConfigString(yamlContent: string, configDir: string): ParsedConfig;
868
+
869
+ export { type AuditEvent, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileSource, type EntityFileSpec, type Filter, type FilterOp, type HasManyRelation, type HasManySource, type InitOptions, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type ManyToManySource, type Migration, type MultiTableDefinition, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, manifestPath, parseConfigFile, parseConfigString, readManifest, writeManifest };