latticesql 0.16.0 → 0.16.2

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.
package/README.md CHANGED
@@ -376,15 +376,21 @@ db.defineEntityContext('agents', {
376
376
  // Files inside each entity's directory
377
377
  files: {
378
378
  'AGENT.md': {
379
- source: { type: 'self' }, // entity's own row
380
- render: ([r]) => `# ${r.name as string}\n\n${r.bio as string ?? ''}`,
379
+ source: { type: 'self' }, // entity's own row
380
+ render: ([r]) => `# ${r.name as string}\n\n${(r.bio as string) ?? ''}`,
381
381
  },
382
382
  'TASKS.md': {
383
- source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id',
384
- orderBy: 'created_at', orderDir: 'desc', limit: 20 },
383
+ source: {
384
+ type: 'hasMany',
385
+ table: 'tasks',
386
+ foreignKey: 'agent_id',
387
+ orderBy: 'created_at',
388
+ orderDir: 'desc',
389
+ limit: 20,
390
+ },
385
391
  render: (rows) => rows.map((r) => `- ${r.title as string}`).join('\n'),
386
- omitIfEmpty: true, // skip if no tasks
387
- budget: 4000, // truncate at 4 000 chars
392
+ omitIfEmpty: true, // skip if no tasks
393
+ budget: 4000, // truncate at 4 000 chars
388
394
  },
389
395
  'SKILLS.md': {
390
396
  source: {
@@ -393,7 +399,7 @@ db.defineEntityContext('agents', {
393
399
  localKey: 'agent_id',
394
400
  remoteKey: 'skill_id',
395
401
  remoteTable: 'skills',
396
- orderBy: 'name', // softDelete inherited from sourceDefaults
402
+ orderBy: 'name', // softDelete inherited from sourceDefaults
397
403
  },
398
404
  render: (rows) => rows.map((r) => `- ${r.name as string}`).join('\n'),
399
405
  omitIfEmpty: true,
@@ -426,14 +432,14 @@ context/
426
432
 
427
433
  **Source types:**
428
434
 
429
- | Type | What it queries |
430
- |---|---|
431
- | `{ type: 'self' }` | The entity row itself |
432
- | `{ type: 'hasMany', table, foreignKey, ... }` | Rows in `table` where `foreignKey = entityPk` |
433
- | `{ type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable, ... }` | Remote rows via a junction table |
434
- | `{ type: 'belongsTo', table, foreignKey, ... }` | Single parent row via FK on this entity (`null` FK → empty) |
435
- | `{ type: 'enriched', include: { ... } }` | Entity row + related data attached as `_key` JSON fields (v0.7+) |
436
- | `{ type: 'custom', query: (row, adapter) => Row[] }` | Fully custom synchronous query |
435
+ | Type | What it queries |
436
+ | ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
437
+ | `{ type: 'self' }` | The entity row itself |
438
+ | `{ type: 'hasMany', table, foreignKey, ... }` | Rows in `table` where `foreignKey = entityPk` |
439
+ | `{ type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable, ... }` | Remote rows via a junction table |
440
+ | `{ type: 'belongsTo', table, foreignKey, ... }` | Single parent row via FK on this entity (`null` FK → empty) |
441
+ | `{ type: 'enriched', include: { ... } }` | Entity row + related data attached as `_key` JSON fields (v0.7+) |
442
+ | `{ type: 'custom', query: (row, adapter) => Row[] }` | Fully custom synchronous query |
437
443
 
438
444
  #### Source query options (v0.6+)
439
445
 
@@ -502,7 +508,7 @@ Set default query options for all relationship sources in an entity context:
502
508
  ```typescript
503
509
  db.defineEntityContext('agents', {
504
510
  slug: (row) => row.slug as string,
505
- sourceDefaults: { softDelete: true }, // applied to all hasMany/manyToMany/belongsTo
511
+ sourceDefaults: { softDelete: true }, // applied to all hasMany/manyToMany/belongsTo
506
512
  files: {
507
513
  'TASKS.md': {
508
514
  // softDelete: true is inherited from sourceDefaults
@@ -548,6 +554,7 @@ Starts with the entity's own row and attaches related data as JSON string fields
548
554
  `EntityFileSpec.render` accepts declarative template objects in addition to functions. Three built-in templates:
549
555
 
550
556
  **entity-table** — heading + GFM table:
557
+
551
558
  ```typescript
552
559
  render: {
553
560
  template: 'entity-table',
@@ -562,6 +569,7 @@ render: {
562
569
  ```
563
570
 
564
571
  **entity-profile** — heading + field-value pairs + enriched JSON sections:
572
+
565
573
  ```typescript
566
574
  render: {
567
575
  template: 'entity-profile',
@@ -581,6 +589,7 @@ render: {
581
589
  ```
582
590
 
583
591
  **entity-sections** — per-row sections with metadata + body:
592
+
584
593
  ```typescript
585
594
  render: {
586
595
  template: 'entity-sections',
@@ -615,7 +624,7 @@ Register a post-write lifecycle hook that fires after `insert()`, `update()`, or
615
624
  db.defineWriteHook({
616
625
  table: 'agents',
617
626
  on: ['insert', 'update'],
618
- watchColumns: ['team_id', 'division'], // only fire when these change
627
+ watchColumns: ['team_id', 'division'], // only fire when these change
619
628
  handler: (ctx) => {
620
629
  // ctx.table, ctx.op, ctx.row, ctx.pk, ctx.changedColumns
621
630
  console.log(`${ctx.op} on ${ctx.table}: ${ctx.pk}`);
@@ -626,12 +635,12 @@ db.defineWriteHook({
626
635
 
627
636
  **Options:**
628
637
 
629
- | Field | Type | Description |
630
- |---|---|---|
631
- | `table` | `string` | Table to watch |
632
- | `on` | `Array<'insert' \| 'update' \| 'delete'>` | Operations that trigger the hook |
633
- | `watchColumns` | `string[]` (optional) | Only fire on update when these columns changed |
634
- | `handler` | `(ctx: WriteHookContext) => void` | Synchronous handler |
638
+ | Field | Type | Description |
639
+ | -------------- | ----------------------------------------- | ---------------------------------------------- |
640
+ | `table` | `string` | Table to watch |
641
+ | `on` | `Array<'insert' \| 'update' \| 'delete'>` | Operations that trigger the hook |
642
+ | `watchColumns` | `string[]` (optional) | Only fire on update when these columns changed |
643
+ | `handler` | `(ctx: WriteHookContext) => void` | Synchronous handler |
635
644
 
636
645
  Hook errors are caught and routed to error handlers — they never crash the caller. Multiple hooks per table are supported.
637
646
 
@@ -677,9 +686,16 @@ Methods that work on **any table** — including tables created via raw DDL (not
677
686
 
678
687
  ```typescript
679
688
  // Upsert by natural key (not just UUID). Auto-handles org_id, updated_at, deleted_at.
680
- const id = await db.upsertByNaturalKey('agents', 'name', 'Alice', {
681
- role: 'engineer', status: 'active',
682
- }, { sourceFile: 'agents.md', orgId: 'org-1' });
689
+ const id = await db.upsertByNaturalKey(
690
+ 'agents',
691
+ 'name',
692
+ 'Alice',
693
+ {
694
+ role: 'engineer',
695
+ status: 'active',
696
+ },
697
+ { sourceFile: 'agents.md', orgId: 'org-1' },
698
+ );
683
699
 
684
700
  // Sparse update — only writes non-null fields.
685
701
  await db.enrichByNaturalKey('agents', 'name', 'Alice', { title: 'Senior Engineer' });
@@ -700,7 +716,11 @@ const alice = await db.getByNaturalKey('agents', 'name', 'Alice');
700
716
  await db.link('agent_skills', { agent_id: 'a1', skill_id: 's1', proficiency: 'expert' });
701
717
 
702
718
  // Link with upsert (INSERT OR REPLACE — updates existing)
703
- await db.link('agent_projects', { agent_id: 'a1', project_id: 'p1', role: 'lead' }, { upsert: true });
719
+ await db.link(
720
+ 'agent_projects',
721
+ { agent_id: 'a1', project_id: 'p1', role: 'lead' },
722
+ { upsert: true },
723
+ );
704
724
 
705
725
  // Unlink (DELETE matching rows)
706
726
  await db.unlink('agent_projects', { agent_id: 'a1', project_id: 'p1' });
@@ -748,11 +768,19 @@ Declarative report builder — queries data within a time window, groups into se
748
768
 
749
769
  ```typescript
750
770
  const report = await db.buildReport({
751
- since: '8h', // or '24h', '7d', or ISO timestamp
771
+ since: '8h', // or '24h', '7d', or ISO timestamp
752
772
  sections: [
753
- { name: 'tasks', query: { table: 'tasks', orderBy: 'created_at', orderDir: 'desc' }, format: 'count_and_list' },
773
+ {
774
+ name: 'tasks',
775
+ query: { table: 'tasks', orderBy: 'created_at', orderDir: 'desc' },
776
+ format: 'count_and_list',
777
+ },
754
778
  { name: 'events', query: { table: 'activity', groupBy: 'type' }, format: 'counts' },
755
- { name: 'alerts', query: { table: 'activity', filters: [{ col: 'severity', op: 'lte', val: 2 }] }, format: 'list' },
779
+ {
780
+ name: 'alerts',
781
+ query: { table: 'activity', filters: [{ col: 'severity', op: 'lte', val: 2 }] },
782
+ format: 'list',
783
+ },
756
784
  ],
757
785
  });
758
786
 
@@ -771,11 +799,13 @@ import { createSQLiteStateStore } from 'latticesql';
771
799
 
772
800
  db.defineWriteback({
773
801
  file: './agents/*/SESSION.md',
774
- stateStore: createSQLiteStateStore(db.db), // persists offsets in SQLite
802
+ stateStore: createSQLiteStateStore(db.db), // persists offsets in SQLite
775
803
  parse: (content, offset) => myParser(content, offset),
776
- persist: async (entry, filePath) => { /* ... */ },
804
+ persist: async (entry, filePath) => {
805
+ /* ... */
806
+ },
777
807
  dedupeKey: (entry) => entry.id,
778
- onArchive: (filePath) => archiveFile(filePath), // lifecycle hook
808
+ onArchive: (filePath) => archiveFile(filePath), // lifecycle hook
779
809
  });
780
810
  ```
781
811
 
@@ -1046,9 +1076,9 @@ stop();
1046
1076
  const stop = await db.watch('./context', {
1047
1077
  interval: 10_000,
1048
1078
  cleanup: {
1049
- removeOrphanedDirectories: true, // delete dirs for deleted entities
1050
- removeOrphanedFiles: true, // delete stale relationship files
1051
- protectedFiles: ['SESSION.md'], // never delete these
1079
+ removeOrphanedDirectories: true, // delete dirs for deleted entities
1080
+ removeOrphanedFiles: true, // delete stale relationship files
1081
+ protectedFiles: ['SESSION.md'], // never delete these
1052
1082
  dryRun: false,
1053
1083
  },
1054
1084
  onCleanup: (r) => {
@@ -1074,15 +1104,15 @@ const result = await db.reconcile('./context', {
1074
1104
  removeOrphanedDirectories: true,
1075
1105
  removeOrphanedFiles: true,
1076
1106
  protectedFiles: ['SESSION.md'],
1077
- reverseSync: true, // default; set false to skip, 'dry-run' to preview
1078
- dryRun: false, // set true to preview without deleting
1107
+ reverseSync: true, // default; set false to skip, 'dry-run' to preview
1108
+ dryRun: false, // set true to preview without deleting
1079
1109
  onOrphan: (path, kind) => console.log(`would remove ${kind}: ${path}`),
1080
1110
  });
1081
1111
 
1082
- console.log(result.filesWritten); // files written this cycle
1083
- console.log(result.cleanup.directoriesRemoved); // orphaned dirs removed
1084
- console.log(result.cleanup.warnings); // dirs left in place (user files)
1085
- console.log(result.reverseSync); // { filesScanned, filesChanged, updatesApplied, errors }
1112
+ console.log(result.filesWritten); // files written this cycle
1113
+ console.log(result.cleanup.directoriesRemoved); // orphaned dirs removed
1114
+ console.log(result.cleanup.warnings); // dirs left in place (user files)
1115
+ console.log(result.reverseSync); // { filesScanned, filesChanged, updatesApplied, errors }
1086
1116
  ```
1087
1117
 
1088
1118
  `ReconcileResult` extends `RenderResult` with `cleanup` and `reverseSync` fields:
@@ -1278,9 +1308,9 @@ Generate a GitHub-Flavoured Markdown table from rows with explicit column config
1278
1308
  import { markdownTable } from 'latticesql';
1279
1309
 
1280
1310
  const md = markdownTable(rows, [
1281
- { key: 'name', header: 'Name' },
1311
+ { key: 'name', header: 'Name' },
1282
1312
  { key: 'status', header: 'Status', format: (v) => String(v || '—') },
1283
- { key: 'name', header: 'Detail', format: (v, row) => `[view](${row.slug}/DETAIL.md)` },
1313
+ { key: 'name', header: 'Detail', format: (v, row) => `[view](${row.slug}/DETAIL.md)` },
1284
1314
  ]);
1285
1315
  // | Name | Status | Detail |
1286
1316
  // | --- | --- | --- |
@@ -1296,8 +1326,8 @@ Generate a URL-safe slug from a display name — lowercases, strips diacritics,
1296
1326
  ```typescript
1297
1327
  import { slugify } from 'latticesql';
1298
1328
 
1299
- slugify('My Agent Name'); // 'my-agent-name'
1300
- slugify('Jose Garcia'); // 'jose-garcia'
1329
+ slugify('My Agent Name'); // 'my-agent-name'
1330
+ slugify('Jose Garcia'); // 'jose-garcia'
1301
1331
  ```
1302
1332
 
1303
1333
  ### `truncate(content, maxChars, notice?)`
@@ -1332,7 +1362,7 @@ db.defineEntityContext('projects', {
1332
1362
  files: {
1333
1363
  'PROJECT.md': {
1334
1364
  source: { type: 'self' },
1335
- render: ([r]) => `# ${r.name as string}\n\n${r.description as string ?? ''}`,
1365
+ render: ([r]) => `# ${r.name as string}\n\n${(r.description as string) ?? ''}`,
1336
1366
  },
1337
1367
  },
1338
1368
  });
@@ -1358,7 +1388,7 @@ await db.delete('projects', 'old-id');
1358
1388
 
1359
1389
  const result = await db.reconcile('./ctx', {
1360
1390
  removeOrphanedDirectories: true,
1361
- protectedFiles: ['NOTES.md'], // agents wrote these — keep them
1391
+ protectedFiles: ['NOTES.md'], // agents wrote these — keep them
1362
1392
  });
1363
1393
  // result.cleanup.directoriesRemoved → ['/.../ctx/projects/old-project']
1364
1394
  ```
@@ -1373,7 +1403,9 @@ Declare files that agents write inside entity directories. Lattice will never de
1373
1403
  db.defineEntityContext('agents', {
1374
1404
  slug: (r) => r.slug as string,
1375
1405
  protectedFiles: ['SESSION.md', 'NOTES.md'],
1376
- files: { /* ... */ },
1406
+ files: {
1407
+ /* ... */
1408
+ },
1377
1409
  });
1378
1410
  ```
1379
1411
 
@@ -1417,18 +1449,18 @@ target: agent-id-here
1417
1449
  reason: Updating status after deployment completed.
1418
1450
  ---
1419
1451
  status: active
1420
- last_task: piut-deploy
1452
+ last_task: api-deploy
1421
1453
  ===
1422
1454
  ```
1423
1455
 
1424
- | Header | Required | Description |
1425
- |--------|----------|-------------|
1426
- | `type` | Yes | Must be `write` |
1427
- | `timestamp` | Yes | ISO 8601 |
1428
- | `op` | Yes | `create`, `update`, or `delete` |
1429
- | `table` | Yes | Target table name |
1430
- | `target` | For update/delete | Record primary key |
1431
- | `reason` | Encouraged | Human-readable reason (audit trail) |
1456
+ | Header | Required | Description |
1457
+ | ----------- | ----------------- | ----------------------------------- |
1458
+ | `type` | Yes | Must be `write` |
1459
+ | `timestamp` | Yes | ISO 8601 |
1460
+ | `op` | Yes | `create`, `update`, or `delete` |
1461
+ | `table` | Yes | Target table name |
1462
+ | `target` | For update/delete | Record primary key |
1463
+ | `reason` | Encouraged | Human-readable reason (audit trail) |
1432
1464
 
1433
1465
  **Body**: `key: value` pairs — one field per line. Field names are validated against the table schema before any write is applied.
1434
1466
 
@@ -1449,15 +1481,16 @@ for (const entry of result.entries) {
1449
1481
  ```
1450
1482
 
1451
1483
  **`SessionWriteEntry`:**
1484
+
1452
1485
  ```ts
1453
1486
  interface SessionWriteEntry {
1454
- id: string; // content-addressed ID
1455
- timestamp: string; // ISO 8601
1487
+ id: string; // content-addressed ID
1488
+ timestamp: string; // ISO 8601
1456
1489
  op: 'create' | 'update' | 'delete';
1457
1490
  table: string;
1458
- target?: string; // required for update/delete
1491
+ target?: string; // required for update/delete
1459
1492
  reason?: string;
1460
- fields: Record<string, string>; // empty for delete
1493
+ fields: Record<string, string>; // empty for delete
1461
1494
  }
1462
1495
  ```
1463
1496
 
package/dist/cli.js CHANGED
@@ -47,13 +47,13 @@ function buildParsedConfig(raw, sourceName, configDir) {
47
47
  const dbPath = resolve(configDir, config.db);
48
48
  const tables = [];
49
49
  for (const [entityName, entityDef] of Object.entries(config.entities)) {
50
- const definition = entityToTableDef(entityName, entityDef, configDir);
50
+ const definition = entityToTableDef(entityName, entityDef);
51
51
  tables.push({ name: entityName, definition });
52
52
  }
53
53
  const entityContexts = parseEntityContexts(config.entityContexts);
54
54
  return { dbPath, tables, entityContexts };
55
55
  }
56
- function entityToTableDef(entityName, entity, configDir) {
56
+ function entityToTableDef(entityName, entity) {
57
57
  const rawFields = entity.fields;
58
58
  if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
59
59
  throw new Error(`Lattice: entity "${entityName}" must have a "fields" object`);
@@ -77,7 +77,7 @@ function entityToTableDef(entityName, entity, configDir) {
77
77
  }
78
78
  const primaryKey = entity.primaryKey ?? pkFromField;
79
79
  const render = parseEntityRender(entity.render);
80
- const outputFile = resolve(configDir, entity.outputFile);
80
+ const outputFile = entity.outputFile;
81
81
  return {
82
82
  columns,
83
83
  render,
@@ -528,7 +528,9 @@ var SchemaManager = class {
528
528
  if (this._entityContexts.has(name)) {
529
529
  const cols = adapter.all(`PRAGMA table_info("${name}")`);
530
530
  const hasDeletedAt = cols.some((c) => c.name === "deleted_at");
531
- return adapter.all(`SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`);
531
+ return adapter.all(
532
+ `SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`
533
+ );
532
534
  }
533
535
  throw new Error(`Unknown table: "${name}"`);
534
536
  }
@@ -1100,7 +1102,14 @@ var RenderEngine = class {
1100
1102
  const slugs = new Set(rows.map((row) => def.slug(row)));
1101
1103
  currentSlugsByTable.set(table, slugs);
1102
1104
  }
1103
- return cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, prevManifest, options, newManifest);
1105
+ return cleanupEntityContexts(
1106
+ outputDir,
1107
+ entityContexts,
1108
+ currentSlugsByTable,
1109
+ prevManifest,
1110
+ options,
1111
+ newManifest
1112
+ );
1104
1113
  }
1105
1114
  /**
1106
1115
  * Render all entity context definitions.
@@ -1215,7 +1224,6 @@ var ReverseSyncEngine = class {
1215
1224
  }
1216
1225
  }
1217
1226
  if (reverseSyncFiles.size === 0) continue;
1218
- const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
1219
1227
  const allRows = this._schema.queryTable(this._adapter, table);
1220
1228
  const slugToRow = /* @__PURE__ */ new Map();
1221
1229
  for (const row of allRows) {
@@ -1247,7 +1255,7 @@ var ReverseSyncEngine = class {
1247
1255
  const updates = reverseSyncFn(currentContent, entityRow);
1248
1256
  if (updates.length === 0) continue;
1249
1257
  if (!dryRun) {
1250
- this._applyUpdates(updates, entityPk);
1258
+ this._applyUpdates(updates);
1251
1259
  }
1252
1260
  result.updatesApplied += updates.length;
1253
1261
  } catch (err) {
@@ -1266,7 +1274,7 @@ var ReverseSyncEngine = class {
1266
1274
  * Each update is an independent UPDATE statement.
1267
1275
  * Wrapped in a transaction for atomicity.
1268
1276
  */
1269
- _applyUpdates(updates, _defaultPk) {
1277
+ _applyUpdates(updates) {
1270
1278
  this._adapter.run("BEGIN");
1271
1279
  try {
1272
1280
  for (const update of updates) {
@@ -1283,10 +1291,7 @@ var ReverseSyncEngine = class {
1283
1291
  const setClause = setCols.map((c) => `"${c}" = ?`).join(", ");
1284
1292
  const whereClause = pkCols.map((c) => `"${c}" = ?`).join(" AND ");
1285
1293
  const sql = `UPDATE "${update.table}" SET ${setClause} WHERE ${whereClause}`;
1286
- const params = [
1287
- ...setCols.map((c) => update.set[c]),
1288
- ...pkCols.map((c) => update.pk[c])
1289
- ];
1294
+ const params = [...setCols.map((c) => update.set[c]), ...pkCols.map((c) => update.pk[c])];
1290
1295
  this._adapter.run(sql, params);
1291
1296
  }
1292
1297
  this._adapter.run("COMMIT");
@@ -1356,8 +1361,12 @@ var InMemoryStateStore = class {
1356
1361
  return this._seen.get(filePath)?.has(key) ?? false;
1357
1362
  }
1358
1363
  markSeen(filePath, key) {
1359
- if (!this._seen.has(filePath)) this._seen.set(filePath, /* @__PURE__ */ new Set());
1360
- this._seen.get(filePath).add(key);
1364
+ let seenSet = this._seen.get(filePath);
1365
+ if (!seenSet) {
1366
+ seenSet = /* @__PURE__ */ new Set();
1367
+ this._seen.set(filePath, seenSet);
1368
+ }
1369
+ seenSet.add(key);
1361
1370
  }
1362
1371
  };
1363
1372
 
@@ -1774,19 +1783,32 @@ var Lattice = class {
1774
1783
  const entries = Object.entries(withConventions).filter(([k]) => k !== "id");
1775
1784
  if (entries.length === 0) return Promise.resolve(existing.id);
1776
1785
  const setCols = entries.map(([k]) => `"${k}" = ?`).join(", ");
1777
- this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [...entries.map(([, v]) => v), existing.id]);
1778
- this._fireWriteHooks(table, "update", withConventions, existing.id, Object.keys(sanitized));
1786
+ this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
1787
+ ...entries.map(([, v]) => v),
1788
+ existing.id
1789
+ ]);
1790
+ this._fireWriteHooks(
1791
+ table,
1792
+ "update",
1793
+ withConventions,
1794
+ existing.id,
1795
+ Object.keys(sanitized)
1796
+ );
1779
1797
  return Promise.resolve(existing.id);
1780
1798
  }
1781
1799
  const id = sanitized.id ?? uuidv4();
1782
1800
  const insertData = { ...withConventions, id, [naturalKeyCol]: naturalKeyVal };
1783
1801
  if (opts?.orgId && cols.has("org_id") && !insertData.org_id) insertData.org_id = opts.orgId;
1784
1802
  if (cols.has("deleted_at")) insertData.deleted_at = null;
1785
- if (cols.has("created_at") && !insertData.created_at) insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
1803
+ if (cols.has("created_at") && !insertData.created_at)
1804
+ insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
1786
1805
  const filtered = this._filterToSchemaColumns(table, insertData);
1787
1806
  const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
1788
1807
  const placeholders = Object.keys(filtered).map(() => "?").join(", ");
1789
- this._adapter.run(`INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`, Object.values(filtered));
1808
+ this._adapter.run(
1809
+ `INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`,
1810
+ Object.values(filtered)
1811
+ );
1790
1812
  this._fireWriteHooks(table, "insert", filtered, id);
1791
1813
  return Promise.resolve(id);
1792
1814
  }
@@ -1803,14 +1825,25 @@ var Lattice = class {
1803
1825
  );
1804
1826
  if (!existing) return Promise.resolve(false);
1805
1827
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(data));
1806
- const entries = Object.entries(sanitized).filter(([k, v]) => v !== null && v !== void 0 && k !== "id");
1828
+ const entries = Object.entries(sanitized).filter(
1829
+ ([k, v]) => v !== null && v !== void 0 && k !== "id"
1830
+ );
1807
1831
  if (entries.length === 0) return Promise.resolve(true);
1808
1832
  const cols = this._ensureColumnCache(table);
1809
1833
  const withTs = [...entries];
1810
1834
  if (cols.has("updated_at")) withTs.push(["updated_at", (/* @__PURE__ */ new Date()).toISOString()]);
1811
1835
  const setCols = withTs.map(([k]) => `"${k}" = ?`).join(", ");
1812
- this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [...withTs.map(([, v]) => v), existing.id]);
1813
- this._fireWriteHooks(table, "update", Object.fromEntries(entries), existing.id, entries.map(([k]) => k));
1836
+ this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
1837
+ ...withTs.map(([, v]) => v),
1838
+ existing.id
1839
+ ]);
1840
+ this._fireWriteHooks(
1841
+ table,
1842
+ "update",
1843
+ Object.fromEntries(entries),
1844
+ existing.id,
1845
+ entries.map(([k]) => k)
1846
+ );
1814
1847
  return Promise.resolve(true);
1815
1848
  }
1816
1849
  /**
@@ -1888,7 +1921,10 @@ var Lattice = class {
1888
1921
  const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
1889
1922
  const placeholders = Object.keys(filtered).map(() => "?").join(", ");
1890
1923
  const verb = opts?.upsert ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
1891
- this._adapter.run(`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`, Object.values(filtered));
1924
+ this._adapter.run(
1925
+ `${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
1926
+ Object.values(filtered)
1927
+ );
1892
1928
  return Promise.resolve();
1893
1929
  }
1894
1930
  /**
@@ -1900,7 +1936,10 @@ var Lattice = class {
1900
1936
  const entries = Object.entries(conditions);
1901
1937
  if (entries.length === 0) return Promise.resolve();
1902
1938
  const where = entries.map(([k]) => `"${k}" = ?`).join(" AND ");
1903
- this._adapter.run(`DELETE FROM "${junctionTable}" WHERE ${where}`, entries.map(([, v]) => v));
1939
+ this._adapter.run(
1940
+ `DELETE FROM "${junctionTable}" WHERE ${where}`,
1941
+ entries.map(([, v]) => v)
1942
+ );
1904
1943
  return Promise.resolve();
1905
1944
  }
1906
1945
  // -------------------------------------------------------------------------
@@ -1927,7 +1966,13 @@ var Lattice = class {
1927
1966
  if (config.sourceFile) upsertOpts.sourceFile = config.sourceFile;
1928
1967
  if (config.sourceHash) upsertOpts.sourceHash = config.sourceHash;
1929
1968
  if (config.orgId) upsertOpts.orgId = config.orgId;
1930
- await this.upsertByNaturalKey(config.table, config.naturalKey, naturalKeyVal, record, upsertOpts);
1969
+ await this.upsertByNaturalKey(
1970
+ config.table,
1971
+ config.naturalKey,
1972
+ naturalKeyVal,
1973
+ record,
1974
+ upsertOpts
1975
+ );
1931
1976
  upserted++;
1932
1977
  if (config.linkTo) {
1933
1978
  const recordId = await this.getByNaturalKey(config.table, config.naturalKey, naturalKeyVal);
@@ -1952,7 +1997,12 @@ var Lattice = class {
1952
1997
  }
1953
1998
  }
1954
1999
  if (config.softDeleteMissing && config.sourceFile && keys.length > 0) {
1955
- softDeleted = await this.softDeleteMissing(config.table, config.naturalKey, config.sourceFile, keys);
2000
+ softDeleted = await this.softDeleteMissing(
2001
+ config.table,
2002
+ config.naturalKey,
2003
+ config.sourceFile,
2004
+ keys
2005
+ );
1956
2006
  }
1957
2007
  return { upserted, linked, softDeleted };
1958
2008
  }
@@ -2036,7 +2086,10 @@ var Lattice = class {
2036
2086
  const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2037
2087
  const orderBy = section.query.orderBy ? ` ORDER BY "${section.query.orderBy}" ${section.query.orderDir === "desc" ? "DESC" : "ASC"}` : "";
2038
2088
  const limit = section.query.limit ? ` LIMIT ${String(section.query.limit)}` : "";
2039
- const rows = this._adapter.all(`SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`, params);
2089
+ const rows = this._adapter.all(
2090
+ `SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`,
2091
+ params
2092
+ );
2040
2093
  if (rows.length > 0) allEmpty = false;
2041
2094
  let formatted = "";
2042
2095
  if (section.format === "custom" && section.customFormat) {
@@ -2359,9 +2412,7 @@ var Lattice = class {
2359
2412
  if (!known) return null;
2360
2413
  for (const col of cols) {
2361
2414
  if (!known.has(col)) {
2362
- return Promise.reject(
2363
- new Error(`Lattice: unknown column "${col}" in table "${table}"`)
2364
- );
2415
+ return Promise.reject(new Error(`Lattice: unknown column "${col}" in table "${table}"`));
2365
2416
  }
2366
2417
  }
2367
2418
  return null;
package/dist/index.cjs CHANGED
@@ -276,7 +276,9 @@ var SchemaManager = class {
276
276
  if (this._entityContexts.has(name)) {
277
277
  const cols = adapter.all(`PRAGMA table_info("${name}")`);
278
278
  const hasDeletedAt = cols.some((c) => c.name === "deleted_at");
279
- return adapter.all(`SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`);
279
+ return adapter.all(
280
+ `SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`
281
+ );
280
282
  }
281
283
  throw new Error(`Unknown table: "${name}"`);
282
284
  }
@@ -855,7 +857,14 @@ var RenderEngine = class {
855
857
  const slugs = new Set(rows.map((row) => def.slug(row)));
856
858
  currentSlugsByTable.set(table, slugs);
857
859
  }
858
- return cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, prevManifest, options, newManifest);
860
+ return cleanupEntityContexts(
861
+ outputDir,
862
+ entityContexts,
863
+ currentSlugsByTable,
864
+ prevManifest,
865
+ options,
866
+ newManifest
867
+ );
859
868
  }
860
869
  /**
861
870
  * Render all entity context definitions.
@@ -970,7 +979,6 @@ var ReverseSyncEngine = class {
970
979
  }
971
980
  }
972
981
  if (reverseSyncFiles.size === 0) continue;
973
- const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
974
982
  const allRows = this._schema.queryTable(this._adapter, table);
975
983
  const slugToRow = /* @__PURE__ */ new Map();
976
984
  for (const row of allRows) {
@@ -1002,7 +1010,7 @@ var ReverseSyncEngine = class {
1002
1010
  const updates = reverseSyncFn(currentContent, entityRow);
1003
1011
  if (updates.length === 0) continue;
1004
1012
  if (!dryRun) {
1005
- this._applyUpdates(updates, entityPk);
1013
+ this._applyUpdates(updates);
1006
1014
  }
1007
1015
  result.updatesApplied += updates.length;
1008
1016
  } catch (err) {
@@ -1021,7 +1029,7 @@ var ReverseSyncEngine = class {
1021
1029
  * Each update is an independent UPDATE statement.
1022
1030
  * Wrapped in a transaction for atomicity.
1023
1031
  */
1024
- _applyUpdates(updates, _defaultPk) {
1032
+ _applyUpdates(updates) {
1025
1033
  this._adapter.run("BEGIN");
1026
1034
  try {
1027
1035
  for (const update of updates) {
@@ -1038,10 +1046,7 @@ var ReverseSyncEngine = class {
1038
1046
  const setClause = setCols.map((c) => `"${c}" = ?`).join(", ");
1039
1047
  const whereClause = pkCols.map((c) => `"${c}" = ?`).join(" AND ");
1040
1048
  const sql = `UPDATE "${update.table}" SET ${setClause} WHERE ${whereClause}`;
1041
- const params = [
1042
- ...setCols.map((c) => update.set[c]),
1043
- ...pkCols.map((c) => update.pk[c])
1044
- ];
1049
+ const params = [...setCols.map((c) => update.set[c]), ...pkCols.map((c) => update.pk[c])];
1045
1050
  this._adapter.run(sql, params);
1046
1051
  }
1047
1052
  this._adapter.run("COMMIT");
@@ -1111,8 +1116,12 @@ var InMemoryStateStore = class {
1111
1116
  return this._seen.get(filePath)?.has(key) ?? false;
1112
1117
  }
1113
1118
  markSeen(filePath, key) {
1114
- if (!this._seen.has(filePath)) this._seen.set(filePath, /* @__PURE__ */ new Set());
1115
- this._seen.get(filePath).add(key);
1119
+ let seenSet = this._seen.get(filePath);
1120
+ if (!seenSet) {
1121
+ seenSet = /* @__PURE__ */ new Set();
1122
+ this._seen.set(filePath, seenSet);
1123
+ }
1124
+ seenSet.add(key);
1116
1125
  }
1117
1126
  };
1118
1127
  var SQLiteStateStore = class {
@@ -1151,11 +1160,13 @@ var SQLiteStateStore = class {
1151
1160
  }
1152
1161
  setOffset(filePath, offset, size) {
1153
1162
  this._init();
1154
- this._db.prepare(`
1163
+ this._db.prepare(
1164
+ `
1155
1165
  INSERT INTO _lattice_writeback_offset (file_path, byte_offset, file_size, updated_at)
1156
1166
  VALUES (?, ?, ?, datetime('now'))
1157
1167
  ON CONFLICT(file_path) DO UPDATE SET byte_offset = ?, file_size = ?, updated_at = datetime('now')
1158
- `).run(filePath, offset, size, offset, size);
1168
+ `
1169
+ ).run(filePath, offset, size, offset, size);
1159
1170
  }
1160
1171
  isSeen(filePath, key) {
1161
1172
  this._init();
@@ -1396,13 +1407,13 @@ function buildParsedConfig(raw, sourceName, configDir) {
1396
1407
  const dbPath = (0, import_node_path7.resolve)(configDir, config.db);
1397
1408
  const tables = [];
1398
1409
  for (const [entityName, entityDef] of Object.entries(config.entities)) {
1399
- const definition = entityToTableDef(entityName, entityDef, configDir);
1410
+ const definition = entityToTableDef(entityName, entityDef);
1400
1411
  tables.push({ name: entityName, definition });
1401
1412
  }
1402
1413
  const entityContexts = parseEntityContexts(config.entityContexts);
1403
1414
  return { dbPath, tables, entityContexts };
1404
1415
  }
1405
- function entityToTableDef(entityName, entity, configDir) {
1416
+ function entityToTableDef(entityName, entity) {
1406
1417
  const rawFields = entity.fields;
1407
1418
  if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
1408
1419
  throw new Error(`Lattice: entity "${entityName}" must have a "fields" object`);
@@ -1426,7 +1437,7 @@ function entityToTableDef(entityName, entity, configDir) {
1426
1437
  }
1427
1438
  const primaryKey = entity.primaryKey ?? pkFromField;
1428
1439
  const render = parseEntityRender(entity.render);
1429
- const outputFile = (0, import_node_path7.resolve)(configDir, entity.outputFile);
1440
+ const outputFile = entity.outputFile;
1430
1441
  return {
1431
1442
  columns,
1432
1443
  render,
@@ -1808,19 +1819,32 @@ var Lattice = class {
1808
1819
  const entries = Object.entries(withConventions).filter(([k]) => k !== "id");
1809
1820
  if (entries.length === 0) return Promise.resolve(existing.id);
1810
1821
  const setCols = entries.map(([k]) => `"${k}" = ?`).join(", ");
1811
- this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [...entries.map(([, v]) => v), existing.id]);
1812
- this._fireWriteHooks(table, "update", withConventions, existing.id, Object.keys(sanitized));
1822
+ this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
1823
+ ...entries.map(([, v]) => v),
1824
+ existing.id
1825
+ ]);
1826
+ this._fireWriteHooks(
1827
+ table,
1828
+ "update",
1829
+ withConventions,
1830
+ existing.id,
1831
+ Object.keys(sanitized)
1832
+ );
1813
1833
  return Promise.resolve(existing.id);
1814
1834
  }
1815
1835
  const id = sanitized.id ?? (0, import_uuid.v4)();
1816
1836
  const insertData = { ...withConventions, id, [naturalKeyCol]: naturalKeyVal };
1817
1837
  if (opts?.orgId && cols.has("org_id") && !insertData.org_id) insertData.org_id = opts.orgId;
1818
1838
  if (cols.has("deleted_at")) insertData.deleted_at = null;
1819
- if (cols.has("created_at") && !insertData.created_at) insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
1839
+ if (cols.has("created_at") && !insertData.created_at)
1840
+ insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
1820
1841
  const filtered = this._filterToSchemaColumns(table, insertData);
1821
1842
  const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
1822
1843
  const placeholders = Object.keys(filtered).map(() => "?").join(", ");
1823
- this._adapter.run(`INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`, Object.values(filtered));
1844
+ this._adapter.run(
1845
+ `INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`,
1846
+ Object.values(filtered)
1847
+ );
1824
1848
  this._fireWriteHooks(table, "insert", filtered, id);
1825
1849
  return Promise.resolve(id);
1826
1850
  }
@@ -1837,14 +1861,25 @@ var Lattice = class {
1837
1861
  );
1838
1862
  if (!existing) return Promise.resolve(false);
1839
1863
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(data));
1840
- const entries = Object.entries(sanitized).filter(([k, v]) => v !== null && v !== void 0 && k !== "id");
1864
+ const entries = Object.entries(sanitized).filter(
1865
+ ([k, v]) => v !== null && v !== void 0 && k !== "id"
1866
+ );
1841
1867
  if (entries.length === 0) return Promise.resolve(true);
1842
1868
  const cols = this._ensureColumnCache(table);
1843
1869
  const withTs = [...entries];
1844
1870
  if (cols.has("updated_at")) withTs.push(["updated_at", (/* @__PURE__ */ new Date()).toISOString()]);
1845
1871
  const setCols = withTs.map(([k]) => `"${k}" = ?`).join(", ");
1846
- this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [...withTs.map(([, v]) => v), existing.id]);
1847
- this._fireWriteHooks(table, "update", Object.fromEntries(entries), existing.id, entries.map(([k]) => k));
1872
+ this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
1873
+ ...withTs.map(([, v]) => v),
1874
+ existing.id
1875
+ ]);
1876
+ this._fireWriteHooks(
1877
+ table,
1878
+ "update",
1879
+ Object.fromEntries(entries),
1880
+ existing.id,
1881
+ entries.map(([k]) => k)
1882
+ );
1848
1883
  return Promise.resolve(true);
1849
1884
  }
1850
1885
  /**
@@ -1922,7 +1957,10 @@ var Lattice = class {
1922
1957
  const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
1923
1958
  const placeholders = Object.keys(filtered).map(() => "?").join(", ");
1924
1959
  const verb = opts?.upsert ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
1925
- this._adapter.run(`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`, Object.values(filtered));
1960
+ this._adapter.run(
1961
+ `${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
1962
+ Object.values(filtered)
1963
+ );
1926
1964
  return Promise.resolve();
1927
1965
  }
1928
1966
  /**
@@ -1934,7 +1972,10 @@ var Lattice = class {
1934
1972
  const entries = Object.entries(conditions);
1935
1973
  if (entries.length === 0) return Promise.resolve();
1936
1974
  const where = entries.map(([k]) => `"${k}" = ?`).join(" AND ");
1937
- this._adapter.run(`DELETE FROM "${junctionTable}" WHERE ${where}`, entries.map(([, v]) => v));
1975
+ this._adapter.run(
1976
+ `DELETE FROM "${junctionTable}" WHERE ${where}`,
1977
+ entries.map(([, v]) => v)
1978
+ );
1938
1979
  return Promise.resolve();
1939
1980
  }
1940
1981
  // -------------------------------------------------------------------------
@@ -1961,7 +2002,13 @@ var Lattice = class {
1961
2002
  if (config.sourceFile) upsertOpts.sourceFile = config.sourceFile;
1962
2003
  if (config.sourceHash) upsertOpts.sourceHash = config.sourceHash;
1963
2004
  if (config.orgId) upsertOpts.orgId = config.orgId;
1964
- await this.upsertByNaturalKey(config.table, config.naturalKey, naturalKeyVal, record, upsertOpts);
2005
+ await this.upsertByNaturalKey(
2006
+ config.table,
2007
+ config.naturalKey,
2008
+ naturalKeyVal,
2009
+ record,
2010
+ upsertOpts
2011
+ );
1965
2012
  upserted++;
1966
2013
  if (config.linkTo) {
1967
2014
  const recordId = await this.getByNaturalKey(config.table, config.naturalKey, naturalKeyVal);
@@ -1986,7 +2033,12 @@ var Lattice = class {
1986
2033
  }
1987
2034
  }
1988
2035
  if (config.softDeleteMissing && config.sourceFile && keys.length > 0) {
1989
- softDeleted = await this.softDeleteMissing(config.table, config.naturalKey, config.sourceFile, keys);
2036
+ softDeleted = await this.softDeleteMissing(
2037
+ config.table,
2038
+ config.naturalKey,
2039
+ config.sourceFile,
2040
+ keys
2041
+ );
1990
2042
  }
1991
2043
  return { upserted, linked, softDeleted };
1992
2044
  }
@@ -2070,7 +2122,10 @@ var Lattice = class {
2070
2122
  const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2071
2123
  const orderBy = section.query.orderBy ? ` ORDER BY "${section.query.orderBy}" ${section.query.orderDir === "desc" ? "DESC" : "ASC"}` : "";
2072
2124
  const limit = section.query.limit ? ` LIMIT ${String(section.query.limit)}` : "";
2073
- const rows = this._adapter.all(`SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`, params);
2125
+ const rows = this._adapter.all(
2126
+ `SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`,
2127
+ params
2128
+ );
2074
2129
  if (rows.length > 0) allEmpty = false;
2075
2130
  let formatted = "";
2076
2131
  if (section.format === "custom" && section.customFormat) {
@@ -2393,9 +2448,7 @@ var Lattice = class {
2393
2448
  if (!known) return null;
2394
2449
  for (const col of cols) {
2395
2450
  if (!known.has(col)) {
2396
- return Promise.reject(
2397
- new Error(`Lattice: unknown column "${col}" in table "${table}"`)
2398
- );
2451
+ return Promise.reject(new Error(`Lattice: unknown column "${col}" in table "${table}"`));
2399
2452
  }
2400
2453
  }
2401
2454
  return null;
@@ -2479,7 +2532,9 @@ function parseBlock(block) {
2479
2532
  return { error: { line, message: "Missing required field: op" } };
2480
2533
  }
2481
2534
  if (rawOp !== "create" && rawOp !== "update" && rawOp !== "delete") {
2482
- return { error: { line, message: `Invalid op: "${rawOp}". Must be create, update, or delete` } };
2535
+ return {
2536
+ error: { line, message: `Invalid op: "${rawOp}". Must be create, update, or delete` }
2537
+ };
2483
2538
  }
2484
2539
  const op = rawOp;
2485
2540
  const table = header.table;
@@ -2487,7 +2542,9 @@ function parseBlock(block) {
2487
2542
  return { error: { line, message: "Missing required field: table" } };
2488
2543
  }
2489
2544
  if (!TABLE_NAME_RE.test(table)) {
2490
- return { error: { line, message: `Invalid table name: "${table}". Only [a-zA-Z0-9_] allowed` } };
2545
+ return {
2546
+ error: { line, message: `Invalid table name: "${table}". Only [a-zA-Z0-9_] allowed` }
2547
+ };
2491
2548
  }
2492
2549
  const target = header.target || void 0;
2493
2550
  if ((op === "update" || op === "delete") && !target) {
@@ -2629,7 +2686,12 @@ function parseSessionMD(content, startOffset = 0, options) {
2629
2686
  writeFields[key] = (m[2] ?? "").trim();
2630
2687
  }
2631
2688
  }
2632
- const newEntry = { id: entryId, type: resolvedType, timestamp: headers.timestamp, body };
2689
+ const newEntry = {
2690
+ id: entryId,
2691
+ type: resolvedType,
2692
+ timestamp: headers.timestamp,
2693
+ body
2694
+ };
2633
2695
  if (headers.project) newEntry.project = headers.project;
2634
2696
  if (headers.task) newEntry.task = headers.task;
2635
2697
  if (tags) newEntry.tags = tags;
@@ -2765,7 +2827,9 @@ function applyWriteEntry(db, entry) {
2765
2827
  const allFields = { ...fields, id };
2766
2828
  const cols = Object.keys(allFields).map((c) => `"${c}"`).join(", ");
2767
2829
  const placeholders = Object.keys(allFields).map(() => "?").join(", ");
2768
- db.prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(...Object.values(allFields));
2830
+ db.prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(
2831
+ ...Object.values(allFields)
2832
+ );
2769
2833
  recordId = id;
2770
2834
  } else if (op === "update") {
2771
2835
  if (!target) {
@@ -2773,7 +2837,10 @@ function applyWriteEntry(db, entry) {
2773
2837
  }
2774
2838
  const pkCol = columnRows.find((r) => r.name === "id") ? "id" : columnRows[0]?.name ?? "id";
2775
2839
  const setCols = Object.keys(fields).map((c) => `"${c}" = ?`).join(", ");
2776
- db.prepare(`UPDATE "${table}" SET ${setCols} WHERE "${pkCol}" = ?`).run(...Object.values(fields), target);
2840
+ db.prepare(`UPDATE "${table}" SET ${setCols} WHERE "${pkCol}" = ?`).run(
2841
+ ...Object.values(fields),
2842
+ target
2843
+ );
2777
2844
  recordId = target;
2778
2845
  } else {
2779
2846
  if (!target) {
@@ -2781,7 +2848,9 @@ function applyWriteEntry(db, entry) {
2781
2848
  }
2782
2849
  const pkCol = columnRows.find((r) => r.name === "id") ? "id" : columnRows[0]?.name ?? "id";
2783
2850
  if (knownColumns.has("deleted_at")) {
2784
- db.prepare(`UPDATE "${table}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`).run(target);
2851
+ db.prepare(`UPDATE "${table}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`).run(
2852
+ target
2853
+ );
2785
2854
  } else {
2786
2855
  db.prepare(`DELETE FROM "${table}" WHERE "${pkCol}" = ?`).run(target);
2787
2856
  }
package/dist/index.d.cts CHANGED
@@ -1403,8 +1403,9 @@ interface ParsedConfig {
1403
1403
  /**
1404
1404
  * Read, parse, and validate a `lattice.config.yml` file.
1405
1405
  *
1406
- * Paths inside the config (e.g. `db`, `outputFile`) are resolved relative to
1407
- * the config file's directory.
1406
+ * The `db` path is resolved relative to the config file's directory.
1407
+ * `outputFile` values are kept as-is (they are relative to the `outputDir`
1408
+ * passed to `render()`, not to the config file location).
1408
1409
  *
1409
1410
  * @throws If the file cannot be read, the YAML is malformed, or required
1410
1411
  * keys are missing.
@@ -1413,7 +1414,8 @@ declare function parseConfigFile(configPath: string): ParsedConfig;
1413
1414
  /**
1414
1415
  * Parse and validate a raw YAML string as a Lattice config.
1415
1416
  *
1416
- * `configDir` is used to resolve relative `db` and `outputFile` paths.
1417
+ * `configDir` is used to resolve the `db` path. `outputFile` values are kept
1418
+ * as-is (relative to the `outputDir` passed to `render()`).
1417
1419
  * Typically this should be the directory that contains `lattice.config.yml`.
1418
1420
  *
1419
1421
  * Useful for testing without touching the filesystem.
@@ -1588,7 +1590,7 @@ declare const DEFAULT_TYPE_ALIASES: Readonly<Record<string, string>>;
1588
1590
  * Each entry looks like:
1589
1591
  * ```
1590
1592
  * ---
1591
- * id: 2026-03-12T15:30:42Z-cortex-a1b2c3
1593
+ * id: 2026-03-12T15:30:42Z-agent1-a1b2c3
1592
1594
  * type: event
1593
1595
  * timestamp: 2026-03-12T15:30:42Z
1594
1596
  * ---
package/dist/index.d.ts CHANGED
@@ -1403,8 +1403,9 @@ interface ParsedConfig {
1403
1403
  /**
1404
1404
  * Read, parse, and validate a `lattice.config.yml` file.
1405
1405
  *
1406
- * Paths inside the config (e.g. `db`, `outputFile`) are resolved relative to
1407
- * the config file's directory.
1406
+ * The `db` path is resolved relative to the config file's directory.
1407
+ * `outputFile` values are kept as-is (they are relative to the `outputDir`
1408
+ * passed to `render()`, not to the config file location).
1408
1409
  *
1409
1410
  * @throws If the file cannot be read, the YAML is malformed, or required
1410
1411
  * keys are missing.
@@ -1413,7 +1414,8 @@ declare function parseConfigFile(configPath: string): ParsedConfig;
1413
1414
  /**
1414
1415
  * Parse and validate a raw YAML string as a Lattice config.
1415
1416
  *
1416
- * `configDir` is used to resolve relative `db` and `outputFile` paths.
1417
+ * `configDir` is used to resolve the `db` path. `outputFile` values are kept
1418
+ * as-is (relative to the `outputDir` passed to `render()`).
1417
1419
  * Typically this should be the directory that contains `lattice.config.yml`.
1418
1420
  *
1419
1421
  * Useful for testing without touching the filesystem.
@@ -1588,7 +1590,7 @@ declare const DEFAULT_TYPE_ALIASES: Readonly<Record<string, string>>;
1588
1590
  * Each entry looks like:
1589
1591
  * ```
1590
1592
  * ---
1591
- * id: 2026-03-12T15:30:42Z-cortex-a1b2c3
1593
+ * id: 2026-03-12T15:30:42Z-agent1-a1b2c3
1592
1594
  * type: event
1593
1595
  * timestamp: 2026-03-12T15:30:42Z
1594
1596
  * ---
package/dist/index.js CHANGED
@@ -214,7 +214,9 @@ var SchemaManager = class {
214
214
  if (this._entityContexts.has(name)) {
215
215
  const cols = adapter.all(`PRAGMA table_info("${name}")`);
216
216
  const hasDeletedAt = cols.some((c) => c.name === "deleted_at");
217
- return adapter.all(`SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`);
217
+ return adapter.all(
218
+ `SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`
219
+ );
218
220
  }
219
221
  throw new Error(`Unknown table: "${name}"`);
220
222
  }
@@ -793,7 +795,14 @@ var RenderEngine = class {
793
795
  const slugs = new Set(rows.map((row) => def.slug(row)));
794
796
  currentSlugsByTable.set(table, slugs);
795
797
  }
796
- return cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, prevManifest, options, newManifest);
798
+ return cleanupEntityContexts(
799
+ outputDir,
800
+ entityContexts,
801
+ currentSlugsByTable,
802
+ prevManifest,
803
+ options,
804
+ newManifest
805
+ );
797
806
  }
798
807
  /**
799
808
  * Render all entity context definitions.
@@ -908,7 +917,6 @@ var ReverseSyncEngine = class {
908
917
  }
909
918
  }
910
919
  if (reverseSyncFiles.size === 0) continue;
911
- const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
912
920
  const allRows = this._schema.queryTable(this._adapter, table);
913
921
  const slugToRow = /* @__PURE__ */ new Map();
914
922
  for (const row of allRows) {
@@ -940,7 +948,7 @@ var ReverseSyncEngine = class {
940
948
  const updates = reverseSyncFn(currentContent, entityRow);
941
949
  if (updates.length === 0) continue;
942
950
  if (!dryRun) {
943
- this._applyUpdates(updates, entityPk);
951
+ this._applyUpdates(updates);
944
952
  }
945
953
  result.updatesApplied += updates.length;
946
954
  } catch (err) {
@@ -959,7 +967,7 @@ var ReverseSyncEngine = class {
959
967
  * Each update is an independent UPDATE statement.
960
968
  * Wrapped in a transaction for atomicity.
961
969
  */
962
- _applyUpdates(updates, _defaultPk) {
970
+ _applyUpdates(updates) {
963
971
  this._adapter.run("BEGIN");
964
972
  try {
965
973
  for (const update of updates) {
@@ -976,10 +984,7 @@ var ReverseSyncEngine = class {
976
984
  const setClause = setCols.map((c) => `"${c}" = ?`).join(", ");
977
985
  const whereClause = pkCols.map((c) => `"${c}" = ?`).join(" AND ");
978
986
  const sql = `UPDATE "${update.table}" SET ${setClause} WHERE ${whereClause}`;
979
- const params = [
980
- ...setCols.map((c) => update.set[c]),
981
- ...pkCols.map((c) => update.pk[c])
982
- ];
987
+ const params = [...setCols.map((c) => update.set[c]), ...pkCols.map((c) => update.pk[c])];
983
988
  this._adapter.run(sql, params);
984
989
  }
985
990
  this._adapter.run("COMMIT");
@@ -1049,8 +1054,12 @@ var InMemoryStateStore = class {
1049
1054
  return this._seen.get(filePath)?.has(key) ?? false;
1050
1055
  }
1051
1056
  markSeen(filePath, key) {
1052
- if (!this._seen.has(filePath)) this._seen.set(filePath, /* @__PURE__ */ new Set());
1053
- this._seen.get(filePath).add(key);
1057
+ let seenSet = this._seen.get(filePath);
1058
+ if (!seenSet) {
1059
+ seenSet = /* @__PURE__ */ new Set();
1060
+ this._seen.set(filePath, seenSet);
1061
+ }
1062
+ seenSet.add(key);
1054
1063
  }
1055
1064
  };
1056
1065
  var SQLiteStateStore = class {
@@ -1089,11 +1098,13 @@ var SQLiteStateStore = class {
1089
1098
  }
1090
1099
  setOffset(filePath, offset, size) {
1091
1100
  this._init();
1092
- this._db.prepare(`
1101
+ this._db.prepare(
1102
+ `
1093
1103
  INSERT INTO _lattice_writeback_offset (file_path, byte_offset, file_size, updated_at)
1094
1104
  VALUES (?, ?, ?, datetime('now'))
1095
1105
  ON CONFLICT(file_path) DO UPDATE SET byte_offset = ?, file_size = ?, updated_at = datetime('now')
1096
- `).run(filePath, offset, size, offset, size);
1106
+ `
1107
+ ).run(filePath, offset, size, offset, size);
1097
1108
  }
1098
1109
  isSeen(filePath, key) {
1099
1110
  this._init();
@@ -1334,13 +1345,13 @@ function buildParsedConfig(raw, sourceName, configDir) {
1334
1345
  const dbPath = resolve(configDir, config.db);
1335
1346
  const tables = [];
1336
1347
  for (const [entityName, entityDef] of Object.entries(config.entities)) {
1337
- const definition = entityToTableDef(entityName, entityDef, configDir);
1348
+ const definition = entityToTableDef(entityName, entityDef);
1338
1349
  tables.push({ name: entityName, definition });
1339
1350
  }
1340
1351
  const entityContexts = parseEntityContexts(config.entityContexts);
1341
1352
  return { dbPath, tables, entityContexts };
1342
1353
  }
1343
- function entityToTableDef(entityName, entity, configDir) {
1354
+ function entityToTableDef(entityName, entity) {
1344
1355
  const rawFields = entity.fields;
1345
1356
  if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
1346
1357
  throw new Error(`Lattice: entity "${entityName}" must have a "fields" object`);
@@ -1364,7 +1375,7 @@ function entityToTableDef(entityName, entity, configDir) {
1364
1375
  }
1365
1376
  const primaryKey = entity.primaryKey ?? pkFromField;
1366
1377
  const render = parseEntityRender(entity.render);
1367
- const outputFile = resolve(configDir, entity.outputFile);
1378
+ const outputFile = entity.outputFile;
1368
1379
  return {
1369
1380
  columns,
1370
1381
  render,
@@ -1746,19 +1757,32 @@ var Lattice = class {
1746
1757
  const entries = Object.entries(withConventions).filter(([k]) => k !== "id");
1747
1758
  if (entries.length === 0) return Promise.resolve(existing.id);
1748
1759
  const setCols = entries.map(([k]) => `"${k}" = ?`).join(", ");
1749
- this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [...entries.map(([, v]) => v), existing.id]);
1750
- this._fireWriteHooks(table, "update", withConventions, existing.id, Object.keys(sanitized));
1760
+ this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
1761
+ ...entries.map(([, v]) => v),
1762
+ existing.id
1763
+ ]);
1764
+ this._fireWriteHooks(
1765
+ table,
1766
+ "update",
1767
+ withConventions,
1768
+ existing.id,
1769
+ Object.keys(sanitized)
1770
+ );
1751
1771
  return Promise.resolve(existing.id);
1752
1772
  }
1753
1773
  const id = sanitized.id ?? uuidv4();
1754
1774
  const insertData = { ...withConventions, id, [naturalKeyCol]: naturalKeyVal };
1755
1775
  if (opts?.orgId && cols.has("org_id") && !insertData.org_id) insertData.org_id = opts.orgId;
1756
1776
  if (cols.has("deleted_at")) insertData.deleted_at = null;
1757
- if (cols.has("created_at") && !insertData.created_at) insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
1777
+ if (cols.has("created_at") && !insertData.created_at)
1778
+ insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
1758
1779
  const filtered = this._filterToSchemaColumns(table, insertData);
1759
1780
  const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
1760
1781
  const placeholders = Object.keys(filtered).map(() => "?").join(", ");
1761
- this._adapter.run(`INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`, Object.values(filtered));
1782
+ this._adapter.run(
1783
+ `INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`,
1784
+ Object.values(filtered)
1785
+ );
1762
1786
  this._fireWriteHooks(table, "insert", filtered, id);
1763
1787
  return Promise.resolve(id);
1764
1788
  }
@@ -1775,14 +1799,25 @@ var Lattice = class {
1775
1799
  );
1776
1800
  if (!existing) return Promise.resolve(false);
1777
1801
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(data));
1778
- const entries = Object.entries(sanitized).filter(([k, v]) => v !== null && v !== void 0 && k !== "id");
1802
+ const entries = Object.entries(sanitized).filter(
1803
+ ([k, v]) => v !== null && v !== void 0 && k !== "id"
1804
+ );
1779
1805
  if (entries.length === 0) return Promise.resolve(true);
1780
1806
  const cols = this._ensureColumnCache(table);
1781
1807
  const withTs = [...entries];
1782
1808
  if (cols.has("updated_at")) withTs.push(["updated_at", (/* @__PURE__ */ new Date()).toISOString()]);
1783
1809
  const setCols = withTs.map(([k]) => `"${k}" = ?`).join(", ");
1784
- this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [...withTs.map(([, v]) => v), existing.id]);
1785
- this._fireWriteHooks(table, "update", Object.fromEntries(entries), existing.id, entries.map(([k]) => k));
1810
+ this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
1811
+ ...withTs.map(([, v]) => v),
1812
+ existing.id
1813
+ ]);
1814
+ this._fireWriteHooks(
1815
+ table,
1816
+ "update",
1817
+ Object.fromEntries(entries),
1818
+ existing.id,
1819
+ entries.map(([k]) => k)
1820
+ );
1786
1821
  return Promise.resolve(true);
1787
1822
  }
1788
1823
  /**
@@ -1860,7 +1895,10 @@ var Lattice = class {
1860
1895
  const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
1861
1896
  const placeholders = Object.keys(filtered).map(() => "?").join(", ");
1862
1897
  const verb = opts?.upsert ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
1863
- this._adapter.run(`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`, Object.values(filtered));
1898
+ this._adapter.run(
1899
+ `${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
1900
+ Object.values(filtered)
1901
+ );
1864
1902
  return Promise.resolve();
1865
1903
  }
1866
1904
  /**
@@ -1872,7 +1910,10 @@ var Lattice = class {
1872
1910
  const entries = Object.entries(conditions);
1873
1911
  if (entries.length === 0) return Promise.resolve();
1874
1912
  const where = entries.map(([k]) => `"${k}" = ?`).join(" AND ");
1875
- this._adapter.run(`DELETE FROM "${junctionTable}" WHERE ${where}`, entries.map(([, v]) => v));
1913
+ this._adapter.run(
1914
+ `DELETE FROM "${junctionTable}" WHERE ${where}`,
1915
+ entries.map(([, v]) => v)
1916
+ );
1876
1917
  return Promise.resolve();
1877
1918
  }
1878
1919
  // -------------------------------------------------------------------------
@@ -1899,7 +1940,13 @@ var Lattice = class {
1899
1940
  if (config.sourceFile) upsertOpts.sourceFile = config.sourceFile;
1900
1941
  if (config.sourceHash) upsertOpts.sourceHash = config.sourceHash;
1901
1942
  if (config.orgId) upsertOpts.orgId = config.orgId;
1902
- await this.upsertByNaturalKey(config.table, config.naturalKey, naturalKeyVal, record, upsertOpts);
1943
+ await this.upsertByNaturalKey(
1944
+ config.table,
1945
+ config.naturalKey,
1946
+ naturalKeyVal,
1947
+ record,
1948
+ upsertOpts
1949
+ );
1903
1950
  upserted++;
1904
1951
  if (config.linkTo) {
1905
1952
  const recordId = await this.getByNaturalKey(config.table, config.naturalKey, naturalKeyVal);
@@ -1924,7 +1971,12 @@ var Lattice = class {
1924
1971
  }
1925
1972
  }
1926
1973
  if (config.softDeleteMissing && config.sourceFile && keys.length > 0) {
1927
- softDeleted = await this.softDeleteMissing(config.table, config.naturalKey, config.sourceFile, keys);
1974
+ softDeleted = await this.softDeleteMissing(
1975
+ config.table,
1976
+ config.naturalKey,
1977
+ config.sourceFile,
1978
+ keys
1979
+ );
1928
1980
  }
1929
1981
  return { upserted, linked, softDeleted };
1930
1982
  }
@@ -2008,7 +2060,10 @@ var Lattice = class {
2008
2060
  const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
2009
2061
  const orderBy = section.query.orderBy ? ` ORDER BY "${section.query.orderBy}" ${section.query.orderDir === "desc" ? "DESC" : "ASC"}` : "";
2010
2062
  const limit = section.query.limit ? ` LIMIT ${String(section.query.limit)}` : "";
2011
- const rows = this._adapter.all(`SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`, params);
2063
+ const rows = this._adapter.all(
2064
+ `SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`,
2065
+ params
2066
+ );
2012
2067
  if (rows.length > 0) allEmpty = false;
2013
2068
  let formatted = "";
2014
2069
  if (section.format === "custom" && section.customFormat) {
@@ -2331,9 +2386,7 @@ var Lattice = class {
2331
2386
  if (!known) return null;
2332
2387
  for (const col of cols) {
2333
2388
  if (!known.has(col)) {
2334
- return Promise.reject(
2335
- new Error(`Lattice: unknown column "${col}" in table "${table}"`)
2336
- );
2389
+ return Promise.reject(new Error(`Lattice: unknown column "${col}" in table "${table}"`));
2337
2390
  }
2338
2391
  }
2339
2392
  return null;
@@ -2417,7 +2470,9 @@ function parseBlock(block) {
2417
2470
  return { error: { line, message: "Missing required field: op" } };
2418
2471
  }
2419
2472
  if (rawOp !== "create" && rawOp !== "update" && rawOp !== "delete") {
2420
- return { error: { line, message: `Invalid op: "${rawOp}". Must be create, update, or delete` } };
2473
+ return {
2474
+ error: { line, message: `Invalid op: "${rawOp}". Must be create, update, or delete` }
2475
+ };
2421
2476
  }
2422
2477
  const op = rawOp;
2423
2478
  const table = header.table;
@@ -2425,7 +2480,9 @@ function parseBlock(block) {
2425
2480
  return { error: { line, message: "Missing required field: table" } };
2426
2481
  }
2427
2482
  if (!TABLE_NAME_RE.test(table)) {
2428
- return { error: { line, message: `Invalid table name: "${table}". Only [a-zA-Z0-9_] allowed` } };
2483
+ return {
2484
+ error: { line, message: `Invalid table name: "${table}". Only [a-zA-Z0-9_] allowed` }
2485
+ };
2429
2486
  }
2430
2487
  const target = header.target || void 0;
2431
2488
  if ((op === "update" || op === "delete") && !target) {
@@ -2567,7 +2624,12 @@ function parseSessionMD(content, startOffset = 0, options) {
2567
2624
  writeFields[key] = (m[2] ?? "").trim();
2568
2625
  }
2569
2626
  }
2570
- const newEntry = { id: entryId, type: resolvedType, timestamp: headers.timestamp, body };
2627
+ const newEntry = {
2628
+ id: entryId,
2629
+ type: resolvedType,
2630
+ timestamp: headers.timestamp,
2631
+ body
2632
+ };
2571
2633
  if (headers.project) newEntry.project = headers.project;
2572
2634
  if (headers.task) newEntry.task = headers.task;
2573
2635
  if (tags) newEntry.tags = tags;
@@ -2703,7 +2765,9 @@ function applyWriteEntry(db, entry) {
2703
2765
  const allFields = { ...fields, id };
2704
2766
  const cols = Object.keys(allFields).map((c) => `"${c}"`).join(", ");
2705
2767
  const placeholders = Object.keys(allFields).map(() => "?").join(", ");
2706
- db.prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(...Object.values(allFields));
2768
+ db.prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(
2769
+ ...Object.values(allFields)
2770
+ );
2707
2771
  recordId = id;
2708
2772
  } else if (op === "update") {
2709
2773
  if (!target) {
@@ -2711,7 +2775,10 @@ function applyWriteEntry(db, entry) {
2711
2775
  }
2712
2776
  const pkCol = columnRows.find((r) => r.name === "id") ? "id" : columnRows[0]?.name ?? "id";
2713
2777
  const setCols = Object.keys(fields).map((c) => `"${c}" = ?`).join(", ");
2714
- db.prepare(`UPDATE "${table}" SET ${setCols} WHERE "${pkCol}" = ?`).run(...Object.values(fields), target);
2778
+ db.prepare(`UPDATE "${table}" SET ${setCols} WHERE "${pkCol}" = ?`).run(
2779
+ ...Object.values(fields),
2780
+ target
2781
+ );
2715
2782
  recordId = target;
2716
2783
  } else {
2717
2784
  if (!target) {
@@ -2719,7 +2786,9 @@ function applyWriteEntry(db, entry) {
2719
2786
  }
2720
2787
  const pkCol = columnRows.find((r) => r.name === "id") ? "id" : columnRows[0]?.name ?? "id";
2721
2788
  if (knownColumns.has("deleted_at")) {
2722
- db.prepare(`UPDATE "${table}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`).run(target);
2789
+ db.prepare(`UPDATE "${table}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`).run(
2790
+ target
2791
+ );
2723
2792
  } else {
2724
2793
  db.prepare(`DELETE FROM "${table}" WHERE "${pkCol}" = ?`).run(target);
2725
2794
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Persistent structured memory for AI agent systems — SQLite ↔ LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",