latticesql 0.5.4 → 0.6.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.
package/dist/cli.js CHANGED
@@ -606,6 +606,75 @@ import { join as join5 } from "path";
606
606
  import { mkdirSync as mkdirSync3 } from "fs";
607
607
 
608
608
  // src/render/entity-query.ts
609
+ var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
610
+ function effectiveFilters(opts) {
611
+ const filters = opts.filters ? [...opts.filters] : [];
612
+ if (opts.softDelete && !filters.some((f) => f.col === "deleted_at")) {
613
+ filters.unshift({ col: "deleted_at", op: "isNull" });
614
+ }
615
+ return filters;
616
+ }
617
+ function appendQueryOptions(baseSql, params, opts, tableAlias) {
618
+ let sql = baseSql;
619
+ const prefix = tableAlias ? `${tableAlias}.` : "";
620
+ for (const f of effectiveFilters(opts)) {
621
+ if (!SAFE_COL_RE.test(f.col)) continue;
622
+ switch (f.op) {
623
+ case "eq":
624
+ sql += ` AND ${prefix}"${f.col}" = ?`;
625
+ params.push(f.val);
626
+ break;
627
+ case "ne":
628
+ sql += ` AND ${prefix}"${f.col}" != ?`;
629
+ params.push(f.val);
630
+ break;
631
+ case "gt":
632
+ sql += ` AND ${prefix}"${f.col}" > ?`;
633
+ params.push(f.val);
634
+ break;
635
+ case "gte":
636
+ sql += ` AND ${prefix}"${f.col}" >= ?`;
637
+ params.push(f.val);
638
+ break;
639
+ case "lt":
640
+ sql += ` AND ${prefix}"${f.col}" < ?`;
641
+ params.push(f.val);
642
+ break;
643
+ case "lte":
644
+ sql += ` AND ${prefix}"${f.col}" <= ?`;
645
+ params.push(f.val);
646
+ break;
647
+ case "like":
648
+ sql += ` AND ${prefix}"${f.col}" LIKE ?`;
649
+ params.push(f.val);
650
+ break;
651
+ case "in": {
652
+ const arr = f.val;
653
+ if (arr.length === 0) {
654
+ sql += " AND 0";
655
+ } else {
656
+ sql += ` AND ${prefix}"${f.col}" IN (${arr.map(() => "?").join(", ")})`;
657
+ params.push(...arr);
658
+ }
659
+ break;
660
+ }
661
+ case "isNull":
662
+ sql += ` AND ${prefix}"${f.col}" IS NULL`;
663
+ break;
664
+ case "isNotNull":
665
+ sql += ` AND ${prefix}"${f.col}" IS NOT NULL`;
666
+ break;
667
+ }
668
+ }
669
+ if (opts.orderBy && SAFE_COL_RE.test(opts.orderBy)) {
670
+ const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
671
+ sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
672
+ }
673
+ if (opts.limit !== void 0 && opts.limit > 0) {
674
+ sql += ` LIMIT ${Math.floor(opts.limit)}`;
675
+ }
676
+ return sql;
677
+ }
609
678
  function resolveEntitySource(source, entityRow, entityPk, adapter) {
610
679
  switch (source.type) {
611
680
  case "self":
@@ -613,29 +682,37 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
613
682
  case "hasMany": {
614
683
  const ref = source.references ?? entityPk;
615
684
  const pkVal = entityRow[ref];
616
- return adapter.all(
617
- `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`,
618
- [pkVal]
619
- );
685
+ const params = [pkVal];
686
+ let sql = `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`;
687
+ sql = appendQueryOptions(sql, params, source);
688
+ return adapter.all(sql, params);
620
689
  }
621
690
  case "manyToMany": {
622
691
  const pkVal = entityRow[entityPk];
623
692
  const remotePk = source.references ?? "id";
624
- return adapter.all(
625
- `SELECT r.* FROM "${source.remoteTable}" r
693
+ const params = [pkVal];
694
+ let sql = `SELECT r.* FROM "${source.remoteTable}" r
626
695
  JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
627
- WHERE j."${source.localKey}" = ?`,
628
- [pkVal]
629
- );
696
+ WHERE j."${source.localKey}" = ?`;
697
+ sql = appendQueryOptions(sql, params, source, "r");
698
+ return adapter.all(sql, params);
630
699
  }
631
700
  case "belongsTo": {
632
701
  const fkVal = entityRow[source.foreignKey];
633
702
  if (fkVal == null) return [];
634
- const related = adapter.get(
635
- `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
636
- [fkVal]
637
- );
638
- return related ? [related] : [];
703
+ const hasOptions = source.filters?.length || source.softDelete || source.orderBy || source.limit;
704
+ if (!hasOptions) {
705
+ const related = adapter.get(
706
+ `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
707
+ [fkVal]
708
+ );
709
+ return related ? [related] : [];
710
+ }
711
+ const params = [fkVal];
712
+ let sql = `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`;
713
+ sql = appendQueryOptions(sql, params, source);
714
+ const rows = adapter.all(sql, params);
715
+ return rows.length > 0 ? [rows[0]] : [];
639
716
  }
640
717
  case "custom":
641
718
  return source.query(entityRow, adapter);
@@ -851,7 +928,8 @@ var RenderEngine = class {
851
928
  mkdirSync3(entityDir, { recursive: true });
852
929
  const renderedFiles = /* @__PURE__ */ new Map();
853
930
  for (const [filename, spec] of Object.entries(def.files)) {
854
- const rows = resolveEntitySource(spec.source, entityRow, entityPk, this._adapter);
931
+ const source = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" ? { ...def.sourceDefaults, ...spec.source } : spec.source;
932
+ const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
855
933
  if (spec.omitIfEmpty && rows.length === 0) continue;
856
934
  const content = truncateContent(spec.render(rows), spec.budget);
857
935
  renderedFiles.set(filename, content);
package/dist/index.cjs CHANGED
@@ -30,18 +30,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ DEFAULT_ENTRY_TYPES: () => DEFAULT_ENTRY_TYPES,
34
+ DEFAULT_TYPE_ALIASES: () => DEFAULT_TYPE_ALIASES,
33
35
  Lattice: () => Lattice,
34
36
  READ_ONLY_HEADER: () => READ_ONLY_HEADER,
35
37
  applyWriteEntry: () => applyWriteEntry,
38
+ createReadOnlyHeader: () => createReadOnlyHeader,
39
+ frontmatter: () => frontmatter,
36
40
  generateEntryId: () => generateEntryId,
37
41
  generateWriteEntryId: () => generateWriteEntryId,
38
42
  manifestPath: () => manifestPath,
43
+ markdownTable: () => markdownTable,
39
44
  parseConfigFile: () => parseConfigFile,
40
45
  parseConfigString: () => parseConfigString,
41
46
  parseMarkdownEntries: () => parseMarkdownEntries,
42
47
  parseSessionMD: () => parseSessionMD,
43
48
  parseSessionWrites: () => parseSessionWrites,
44
49
  readManifest: () => readManifest,
50
+ slugify: () => slugify,
51
+ truncate: () => truncate,
45
52
  validateEntryId: () => validateEntryId,
46
53
  writeManifest: () => writeManifest
47
54
  });
@@ -341,6 +348,75 @@ var import_node_path4 = require("path");
341
348
  var import_node_fs4 = require("fs");
342
349
 
343
350
  // src/render/entity-query.ts
351
+ var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
352
+ function effectiveFilters(opts) {
353
+ const filters = opts.filters ? [...opts.filters] : [];
354
+ if (opts.softDelete && !filters.some((f) => f.col === "deleted_at")) {
355
+ filters.unshift({ col: "deleted_at", op: "isNull" });
356
+ }
357
+ return filters;
358
+ }
359
+ function appendQueryOptions(baseSql, params, opts, tableAlias) {
360
+ let sql = baseSql;
361
+ const prefix = tableAlias ? `${tableAlias}.` : "";
362
+ for (const f of effectiveFilters(opts)) {
363
+ if (!SAFE_COL_RE.test(f.col)) continue;
364
+ switch (f.op) {
365
+ case "eq":
366
+ sql += ` AND ${prefix}"${f.col}" = ?`;
367
+ params.push(f.val);
368
+ break;
369
+ case "ne":
370
+ sql += ` AND ${prefix}"${f.col}" != ?`;
371
+ params.push(f.val);
372
+ break;
373
+ case "gt":
374
+ sql += ` AND ${prefix}"${f.col}" > ?`;
375
+ params.push(f.val);
376
+ break;
377
+ case "gte":
378
+ sql += ` AND ${prefix}"${f.col}" >= ?`;
379
+ params.push(f.val);
380
+ break;
381
+ case "lt":
382
+ sql += ` AND ${prefix}"${f.col}" < ?`;
383
+ params.push(f.val);
384
+ break;
385
+ case "lte":
386
+ sql += ` AND ${prefix}"${f.col}" <= ?`;
387
+ params.push(f.val);
388
+ break;
389
+ case "like":
390
+ sql += ` AND ${prefix}"${f.col}" LIKE ?`;
391
+ params.push(f.val);
392
+ break;
393
+ case "in": {
394
+ const arr = f.val;
395
+ if (arr.length === 0) {
396
+ sql += " AND 0";
397
+ } else {
398
+ sql += ` AND ${prefix}"${f.col}" IN (${arr.map(() => "?").join(", ")})`;
399
+ params.push(...arr);
400
+ }
401
+ break;
402
+ }
403
+ case "isNull":
404
+ sql += ` AND ${prefix}"${f.col}" IS NULL`;
405
+ break;
406
+ case "isNotNull":
407
+ sql += ` AND ${prefix}"${f.col}" IS NOT NULL`;
408
+ break;
409
+ }
410
+ }
411
+ if (opts.orderBy && SAFE_COL_RE.test(opts.orderBy)) {
412
+ const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
413
+ sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
414
+ }
415
+ if (opts.limit !== void 0 && opts.limit > 0) {
416
+ sql += ` LIMIT ${Math.floor(opts.limit)}`;
417
+ }
418
+ return sql;
419
+ }
344
420
  function resolveEntitySource(source, entityRow, entityPk, adapter) {
345
421
  switch (source.type) {
346
422
  case "self":
@@ -348,29 +424,37 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
348
424
  case "hasMany": {
349
425
  const ref = source.references ?? entityPk;
350
426
  const pkVal = entityRow[ref];
351
- return adapter.all(
352
- `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`,
353
- [pkVal]
354
- );
427
+ const params = [pkVal];
428
+ let sql = `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`;
429
+ sql = appendQueryOptions(sql, params, source);
430
+ return adapter.all(sql, params);
355
431
  }
356
432
  case "manyToMany": {
357
433
  const pkVal = entityRow[entityPk];
358
434
  const remotePk = source.references ?? "id";
359
- return adapter.all(
360
- `SELECT r.* FROM "${source.remoteTable}" r
435
+ const params = [pkVal];
436
+ let sql = `SELECT r.* FROM "${source.remoteTable}" r
361
437
  JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
362
- WHERE j."${source.localKey}" = ?`,
363
- [pkVal]
364
- );
438
+ WHERE j."${source.localKey}" = ?`;
439
+ sql = appendQueryOptions(sql, params, source, "r");
440
+ return adapter.all(sql, params);
365
441
  }
366
442
  case "belongsTo": {
367
443
  const fkVal = entityRow[source.foreignKey];
368
444
  if (fkVal == null) return [];
369
- const related = adapter.get(
370
- `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
371
- [fkVal]
372
- );
373
- return related ? [related] : [];
445
+ const hasOptions = source.filters?.length || source.softDelete || source.orderBy || source.limit;
446
+ if (!hasOptions) {
447
+ const related = adapter.get(
448
+ `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
449
+ [fkVal]
450
+ );
451
+ return related ? [related] : [];
452
+ }
453
+ const params = [fkVal];
454
+ let sql = `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`;
455
+ sql = appendQueryOptions(sql, params, source);
456
+ const rows = adapter.all(sql, params);
457
+ return rows.length > 0 ? [rows[0]] : [];
374
458
  }
375
459
  case "custom":
376
460
  return source.query(entityRow, adapter);
@@ -586,7 +670,8 @@ var RenderEngine = class {
586
670
  (0, import_node_fs4.mkdirSync)(entityDir, { recursive: true });
587
671
  const renderedFiles = /* @__PURE__ */ new Map();
588
672
  for (const [filename, spec] of Object.entries(def.files)) {
589
- const rows = resolveEntitySource(spec.source, entityRow, entityPk, this._adapter);
673
+ const source = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" ? { ...def.sourceDefaults, ...spec.source } : spec.source;
674
+ const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
590
675
  if (spec.omitIfEmpty && rows.length === 0) continue;
591
676
  const content = truncateContent(spec.render(rows), spec.budget);
592
677
  renderedFiles.set(filename, content);
@@ -1521,6 +1606,39 @@ var Lattice = class {
1521
1606
  }
1522
1607
  };
1523
1608
 
1609
+ // src/render/markdown.ts
1610
+ function frontmatter(fields) {
1611
+ const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
1612
+ for (const [key, val] of Object.entries(fields)) {
1613
+ lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
1614
+ }
1615
+ return `---
1616
+ ${lines.join("\n")}
1617
+ ---
1618
+
1619
+ `;
1620
+ }
1621
+ function markdownTable(rows, columns) {
1622
+ if (rows.length === 0 || columns.length === 0) return "";
1623
+ const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
1624
+ const separator = "| " + columns.map(() => "---").join(" | ") + " |";
1625
+ const body = rows.map((row) => {
1626
+ const cells = columns.map((col) => {
1627
+ const raw = row[col.key];
1628
+ return col.format ? col.format(raw, row) : String(raw ?? "");
1629
+ });
1630
+ return "| " + cells.join(" | ") + " |";
1631
+ });
1632
+ return [header, separator, ...body].join("\n") + "\n";
1633
+ }
1634
+ function slugify(name) {
1635
+ return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1636
+ }
1637
+ function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
1638
+ if (content.length <= maxChars) return content;
1639
+ return content.slice(0, maxChars) + notice;
1640
+ }
1641
+
1524
1642
  // src/session/parser.ts
1525
1643
  var import_node_crypto3 = require("crypto");
1526
1644
  function generateWriteEntryId(timestamp, agentName, op, table, target) {
@@ -1634,7 +1752,7 @@ function parseBlock(block) {
1634
1752
 
1635
1753
  // src/session/entries.ts
1636
1754
  var import_node_crypto4 = require("crypto");
1637
- var VALID_TYPES = /* @__PURE__ */ new Set([
1755
+ var DEFAULT_ENTRY_TYPES = /* @__PURE__ */ new Set([
1638
1756
  "event",
1639
1757
  "learning",
1640
1758
  "status",
@@ -1644,7 +1762,7 @@ var VALID_TYPES = /* @__PURE__ */ new Set([
1644
1762
  "handoff",
1645
1763
  "write"
1646
1764
  ]);
1647
- var TYPE_ALIASES = {
1765
+ var DEFAULT_TYPE_ALIASES = {
1648
1766
  task_completion: "event",
1649
1767
  completion: "event",
1650
1768
  heartbeat: "status",
@@ -1654,7 +1772,7 @@ var TYPE_ALIASES = {
1654
1772
  note: "event"
1655
1773
  };
1656
1774
  var FIELD_NAME_RE2 = /^[a-zA-Z0-9_]+$/;
1657
- function parseSessionMD(content, startOffset = 0) {
1775
+ function parseSessionMD(content, startOffset = 0, options) {
1658
1776
  const entries = [];
1659
1777
  const errors = [];
1660
1778
  const text = content.slice(startOffset);
@@ -1710,7 +1828,7 @@ function parseSessionMD(content, startOffset = 0) {
1710
1828
  }
1711
1829
  const body = bodyLines.join("\n").trim();
1712
1830
  const rawType = headers["type"] ?? "";
1713
- const resolvedType = normalizeType(rawType);
1831
+ const resolvedType = normalizeType(rawType, options);
1714
1832
  if (!resolvedType) {
1715
1833
  errors.push({ line: entryStartLine + 1, message: `Unknown entry type: ${rawType}` });
1716
1834
  continue;
@@ -1760,7 +1878,7 @@ function parseSessionMD(content, startOffset = 0) {
1760
1878
  }
1761
1879
  return { entries, errors, lastOffset: currentByteOffset };
1762
1880
  }
1763
- function parseMarkdownEntries(content, agentName, startOffset = 0) {
1881
+ function parseMarkdownEntries(content, agentName, startOffset = 0, options) {
1764
1882
  const entries = [];
1765
1883
  const errors = [];
1766
1884
  const text = content.slice(startOffset);
@@ -1802,7 +1920,7 @@ function parseMarkdownEntries(content, agentName, startOffset = 0) {
1802
1920
  continue;
1803
1921
  }
1804
1922
  const rawType = bodyType ?? start.headingType ?? "event";
1805
- const resolvedType = normalizeType(rawType) ?? "event";
1923
+ const resolvedType = normalizeType(rawType, options) ?? "event";
1806
1924
  const id = generateEntryId(start.timestamp, agentName, body);
1807
1925
  entries.push({
1808
1926
  id,
@@ -1825,13 +1943,25 @@ function validateEntryId(id, body) {
1825
1943
  const expectedHash = (0, import_node_crypto4.createHash)("sha256").update(body).digest("hex").slice(0, 6);
1826
1944
  return hash === expectedHash;
1827
1945
  }
1828
- function normalizeType(raw) {
1946
+ function normalizeType(raw, options) {
1829
1947
  const lower = raw.toLowerCase().trim();
1830
- if (VALID_TYPES.has(lower)) return lower;
1831
- const normalized = lower.replace(/-/g, "_");
1832
- if (TYPE_ALIASES[normalized]) return TYPE_ALIASES[normalized];
1833
- for (const alias of Object.keys(TYPE_ALIASES)) {
1834
- if (normalized.startsWith(alias)) return TYPE_ALIASES[alias];
1948
+ if (!lower) return null;
1949
+ const validTypes = options?.validTypes === void 0 ? DEFAULT_ENTRY_TYPES : options.validTypes;
1950
+ const aliases = options?.typeAliases === void 0 ? DEFAULT_TYPE_ALIASES : options.typeAliases;
1951
+ if (validTypes === null) {
1952
+ if (aliases) {
1953
+ const normalized = lower.replace(/-/g, "_");
1954
+ if (aliases[normalized]) return aliases[normalized];
1955
+ }
1956
+ return lower;
1957
+ }
1958
+ if (validTypes.has(lower)) return lower;
1959
+ if (aliases) {
1960
+ const normalized = lower.replace(/-/g, "_");
1961
+ if (aliases[normalized]) return aliases[normalized];
1962
+ for (const alias of Object.keys(aliases)) {
1963
+ if (normalized.startsWith(alias)) return aliases[alias];
1964
+ }
1835
1965
  }
1836
1966
  return null;
1837
1967
  }
@@ -1895,26 +2025,38 @@ function applyWriteEntry(db, entry) {
1895
2025
  }
1896
2026
 
1897
2027
  // src/session/constants.ts
1898
- var READ_ONLY_HEADER = `<!-- READ ONLY \u2014 generated by lattice-sync. Do not edit directly.
2028
+ function createReadOnlyHeader(options) {
2029
+ const generator = options?.generator ?? "Lattice";
2030
+ const docsRef = options?.docsRef ?? "the Lattice documentation";
2031
+ return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
1899
2032
  To update data in Lattice: write entries to SESSION.md in this directory.
1900
2033
  Format: type: write | op: create/update/delete | table: <name> | target: <id>
1901
- See agents/shared/SESSION-FORMAT.md for the full spec. -->
2034
+ See ${docsRef} for the SESSION.md format spec. -->
1902
2035
 
1903
2036
  `;
2037
+ }
2038
+ var READ_ONLY_HEADER = createReadOnlyHeader();
1904
2039
  // Annotate the CommonJS export names for ESM import in node:
1905
2040
  0 && (module.exports = {
2041
+ DEFAULT_ENTRY_TYPES,
2042
+ DEFAULT_TYPE_ALIASES,
1906
2043
  Lattice,
1907
2044
  READ_ONLY_HEADER,
1908
2045
  applyWriteEntry,
2046
+ createReadOnlyHeader,
2047
+ frontmatter,
1909
2048
  generateEntryId,
1910
2049
  generateWriteEntryId,
1911
2050
  manifestPath,
2051
+ markdownTable,
1912
2052
  parseConfigFile,
1913
2053
  parseConfigString,
1914
2054
  parseMarkdownEntries,
1915
2055
  parseSessionMD,
1916
2056
  parseSessionWrites,
1917
2057
  readManifest,
2058
+ slugify,
2059
+ truncate,
1918
2060
  validateEntryId,
1919
2061
  writeManifest
1920
2062
  });
package/dist/index.d.cts CHANGED
@@ -41,6 +41,32 @@ interface PreparedStatement {
41
41
  all(...params: unknown[]): Row[];
42
42
  }
43
43
 
44
+ /**
45
+ * Optional query refinements shared by `hasMany`, `manyToMany`, and
46
+ * `belongsTo` sources. All fields are additive — omitting them preserves
47
+ * the v0.5 behaviour (bare `SELECT *`).
48
+ */
49
+ interface SourceQueryOptions {
50
+ /**
51
+ * Additional WHERE clauses applied after the relationship join condition.
52
+ * Uses the existing {@link Filter} type.
53
+ *
54
+ * @example `filters: [{ col: 'status', op: 'eq', val: 'active' }]`
55
+ */
56
+ filters?: Filter[];
57
+ /**
58
+ * Shorthand for `filters: [{ col: 'deleted_at', op: 'isNull' }]`.
59
+ * When `true`, soft-deleted rows are excluded. If both `softDelete`
60
+ * and an explicit `deleted_at` filter are present, the explicit filter wins.
61
+ */
62
+ softDelete?: boolean;
63
+ /** Column to ORDER BY. Validated against `[a-zA-Z0-9_]`. */
64
+ orderBy?: string;
65
+ /** Sort direction. Defaults to `'asc'`. */
66
+ orderDir?: 'asc' | 'desc';
67
+ /** Maximum number of rows to return. */
68
+ limit?: number;
69
+ }
44
70
  /**
45
71
  * Yield the entity row itself as a single-element array.
46
72
  * Use for the primary entity file (e.g. `AGENT.md`).
@@ -57,7 +83,7 @@ interface SelfSource {
57
83
  * source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id' }
58
84
  * ```
59
85
  */
60
- interface HasManySource {
86
+ interface HasManySource extends SourceQueryOptions {
61
87
  type: 'hasMany';
62
88
  /** The related table to query */
63
89
  table: string;
@@ -84,7 +110,7 @@ interface HasManySource {
84
110
  * }
85
111
  * ```
86
112
  */
87
- interface ManyToManySource {
113
+ interface ManyToManySource extends SourceQueryOptions {
88
114
  type: 'manyToMany';
89
115
  /** The junction / association table */
90
116
  junctionTable: string;
@@ -111,7 +137,7 @@ interface ManyToManySource {
111
137
  * source: { type: 'belongsTo', table: 'teams', foreignKey: 'team_id' }
112
138
  * ```
113
139
  */
114
- interface BelongsToSource {
140
+ interface BelongsToSource extends SourceQueryOptions {
115
141
  type: 'belongsTo';
116
142
  /** The related table to look up */
117
143
  table: string;
@@ -250,6 +276,17 @@ interface EntityContextDefinition {
250
276
  * Defaults to `[]`.
251
277
  */
252
278
  protectedFiles?: string[];
279
+ /**
280
+ * Default query options merged into every `hasMany`, `manyToMany`, and
281
+ * `belongsTo` source in this context. Per-file source options override
282
+ * these defaults. `custom` and `self` sources are unaffected.
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * sourceDefaults: { softDelete: true } // exclude soft-deleted rows everywhere
287
+ * ```
288
+ */
289
+ sourceDefaults?: SourceQueryOptions;
253
290
  }
254
291
 
255
292
  interface CleanupOptions {
@@ -866,6 +903,69 @@ declare function parseConfigFile(configPath: string): ParsedConfig;
866
903
  */
867
904
  declare function parseConfigString(yamlContent: string, configDir: string): ParsedConfig;
868
905
 
906
+ /**
907
+ * Column definition for {@link markdownTable}.
908
+ */
909
+ interface MarkdownTableColumn {
910
+ /** Row property to read (e.g. `'name'`, `'status'`). */
911
+ key: string;
912
+ /** Column header text displayed in the table. */
913
+ header: string;
914
+ /**
915
+ * Optional per-cell formatter. Receives the raw cell value and the full
916
+ * row so formatters can derive display values from multiple fields.
917
+ *
918
+ * @example `(val) => String(val ?? '—')`
919
+ * @example `(_, row) => \`[\${row.name}](\${row.slug}/DETAIL.md)\``
920
+ */
921
+ format?: (val: unknown, row: Row) => string;
922
+ }
923
+ /**
924
+ * Generate a YAML-style frontmatter block.
925
+ * Automatically includes `generated_at` with the current ISO timestamp.
926
+ *
927
+ * @example
928
+ * ```ts
929
+ * frontmatter({ agent: 'Alice', skill_count: 5 })
930
+ * // "---\ngenerated_at: 2026-03-27T...\nagent: Alice\nskill_count: 5\n---\n\n"
931
+ * ```
932
+ */
933
+ declare function frontmatter(fields: Record<string, string | number | boolean>): string;
934
+ /**
935
+ * Generate a GitHub-Flavoured Markdown table from rows with explicit column
936
+ * configuration. Returns an empty string when `rows` is empty.
937
+ *
938
+ * @example
939
+ * ```ts
940
+ * markdownTable(rows, [
941
+ * { key: 'name', header: 'Name' },
942
+ * { key: 'status', header: 'Status', format: (v) => String(v ?? '—') },
943
+ * ])
944
+ * ```
945
+ */
946
+ declare function markdownTable(rows: Row[], columns: MarkdownTableColumn[]): string;
947
+ /**
948
+ * Generate a URL-safe slug from a display name.
949
+ *
950
+ * - Lowercases, strips diacritics, replaces non-alphanumeric runs with `-`,
951
+ * and trims leading/trailing hyphens.
952
+ *
953
+ * @example `slugify('My Agent Name') // 'my-agent-name'`
954
+ * @example `slugify('José García') // 'jose-garcia'`
955
+ */
956
+ declare function slugify(name: string): string;
957
+ /**
958
+ * Truncate content at a character budget.
959
+ *
960
+ * When `content.length > maxChars`, slices to `maxChars` and appends `notice`.
961
+ * Returns `content` unchanged when the budget is not exceeded.
962
+ *
963
+ * @param content - The rendered content to truncate
964
+ * @param maxChars - Maximum character count
965
+ * @param notice - Appended after truncation (default: standard budget notice)
966
+ */
967
+ declare function truncate(content: string, maxChars: number, notice?: string): string;
968
+
869
969
  type SessionWriteOp = 'create' | 'update' | 'delete';
870
970
  interface SessionWriteEntry {
871
971
  id: string;
@@ -896,9 +996,10 @@ declare function generateWriteEntryId(timestamp: string, agentName: string, op:
896
996
  declare function parseSessionWrites(content: string): SessionWriteParseResult;
897
997
 
898
998
  /**
899
- * A single parsed SESSION.md entry. Covers all entry types:
900
- * event, learning, status, correction, discovery, metric, handoff, write.
999
+ * A single parsed SESSION.md entry.
901
1000
  *
1001
+ * The `type` field holds the resolved entry type (from the built-in set or
1002
+ * custom types supplied via {@link SessionParseOptions}).
902
1003
  * When `type === 'write'`, the op/table/target/reason/fields fields are set.
903
1004
  */
904
1005
  interface SessionEntry {
@@ -929,6 +1030,39 @@ interface ParseResult {
929
1030
  /** Byte offset after the last fully parsed entry — used for incremental parsing. */
930
1031
  lastOffset: number;
931
1032
  }
1033
+ /**
1034
+ * Options for {@link parseSessionMD} and {@link parseMarkdownEntries}.
1035
+ *
1036
+ * All fields are optional — omitting them preserves the default behaviour
1037
+ * (built-in type set + built-in aliases), so existing callers are unaffected.
1038
+ */
1039
+ interface SessionParseOptions {
1040
+ /**
1041
+ * Set of valid entry type names.
1042
+ * - Omit (or `undefined`) → use {@link DEFAULT_ENTRY_TYPES}.
1043
+ * - `null` → accept **any** type string without validation.
1044
+ * - Provide a custom `Set<string>` to restrict to your own taxonomy.
1045
+ */
1046
+ validTypes?: Set<string> | null;
1047
+ /**
1048
+ * Map of non-standard type names to their canonical form.
1049
+ * - Omit (or `undefined`) → use {@link DEFAULT_TYPE_ALIASES}.
1050
+ * - `null` → disable alias resolution.
1051
+ * - Provide a custom `Record<string, string>` for your own aliases.
1052
+ */
1053
+ typeAliases?: Record<string, string> | null;
1054
+ }
1055
+ /**
1056
+ * Default set of valid entry types shipped with latticesql.
1057
+ * Suitable for LLM-agent context systems; override via {@link SessionParseOptions.validTypes}.
1058
+ */
1059
+ declare const DEFAULT_ENTRY_TYPES: ReadonlySet<string>;
1060
+ /**
1061
+ * Default type aliases shipped with latticesql.
1062
+ * Maps commonly-seen alternative names to their canonical type.
1063
+ * Override via {@link SessionParseOptions.typeAliases}.
1064
+ */
1065
+ declare const DEFAULT_TYPE_ALIASES: Readonly<Record<string, string>>;
932
1066
  /**
933
1067
  * Parse SESSION.md YAML-delimited entries starting at `startOffset` bytes.
934
1068
  *
@@ -942,15 +1076,18 @@ interface ParseResult {
942
1076
  * Entry body text here.
943
1077
  * ===
944
1078
  * ```
1079
+ *
1080
+ * Pass {@link SessionParseOptions} to customise which entry types are accepted
1081
+ * and how aliases are resolved. Defaults match the built-in type set.
945
1082
  */
946
- declare function parseSessionMD(content: string, startOffset?: number): ParseResult;
1083
+ declare function parseSessionMD(content: string, startOffset?: number, options?: SessionParseOptions): ParseResult;
947
1084
  /**
948
- * Parse free-form Markdown SESSION.md entries. Agents sometimes write entries
949
- * as `## {timestamp} — {description}` headings rather than YAML blocks.
1085
+ * Parse free-form Markdown SESSION.md entries written as
1086
+ * `## {timestamp} — {description}` headings rather than YAML blocks.
950
1087
  *
951
1088
  * Runs alongside `parseSessionMD`; the two parsers are merged by caller.
952
1089
  */
953
- declare function parseMarkdownEntries(content: string, agentName: string, startOffset?: number): ParseResult;
1090
+ declare function parseMarkdownEntries(content: string, agentName: string, startOffset?: number, options?: SessionParseOptions): ParseResult;
954
1091
  /**
955
1092
  * Generate a content-addressed entry ID.
956
1093
  * Format: `{timestamp}-{agentName}-{6-char-sha256-prefix}`
@@ -988,12 +1125,26 @@ type ApplyWriteResult = {
988
1125
  declare function applyWriteEntry(db: Database.Database, entry: SessionWriteEntry): ApplyWriteResult;
989
1126
 
990
1127
  /**
991
- * Read-only header prepended to all Lattice-generated context files.
1128
+ * Options for {@link createReadOnlyHeader}.
1129
+ */
1130
+ interface ReadOnlyHeaderOptions {
1131
+ /** Name shown as the generator (default: `"Lattice"`). */
1132
+ generator?: string;
1133
+ /** Where to find the SESSION.md format spec (default: `"the Lattice documentation"`). */
1134
+ docsRef?: string;
1135
+ }
1136
+ /**
1137
+ * Build a read-only header for Lattice-generated context files.
1138
+ *
1139
+ * The header tells consumers (human or LLM) that the file is auto-generated
1140
+ * and that writes should go through SESSION.md instead.
1141
+ */
1142
+ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
1143
+ /**
1144
+ * Default read-only header prepended to all Lattice-generated context files.
992
1145
  *
993
- * Directs agents to SESSION.md for writes rather than editing generated files
994
- * directly. Include at the top of every rendered markdown context file so
995
- * agents see it at the start of their context window.
1146
+ * For a customised header, use {@link createReadOnlyHeader} instead.
996
1147
  */
997
- declare const READ_ONLY_HEADER = "<!-- READ ONLY \u2014 generated by lattice-sync. Do not edit directly.\n To update data in Lattice: write entries to SESSION.md in this directory.\n Format: type: write | op: create/update/delete | table: <name> | target: <id>\n See agents/shared/SESSION-FORMAT.md for the full spec. -->\n\n";
1148
+ declare const READ_ONLY_HEADER: string;
998
1149
 
999
- export { type ApplyWriteResult, 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 ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type SessionEntry, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, applyWriteEntry, generateEntryId, generateWriteEntryId, manifestPath, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, validateEntryId, writeManifest };
1150
+ export { type ApplyWriteResult, type AuditEvent, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, 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 MarkdownTableColumn, type Migration, type MultiTableDefinition, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, applyWriteEntry, createReadOnlyHeader, frontmatter, generateEntryId, generateWriteEntryId, manifestPath, markdownTable, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, slugify, truncate, validateEntryId, writeManifest };
package/dist/index.d.ts CHANGED
@@ -41,6 +41,32 @@ interface PreparedStatement {
41
41
  all(...params: unknown[]): Row[];
42
42
  }
43
43
 
44
+ /**
45
+ * Optional query refinements shared by `hasMany`, `manyToMany`, and
46
+ * `belongsTo` sources. All fields are additive — omitting them preserves
47
+ * the v0.5 behaviour (bare `SELECT *`).
48
+ */
49
+ interface SourceQueryOptions {
50
+ /**
51
+ * Additional WHERE clauses applied after the relationship join condition.
52
+ * Uses the existing {@link Filter} type.
53
+ *
54
+ * @example `filters: [{ col: 'status', op: 'eq', val: 'active' }]`
55
+ */
56
+ filters?: Filter[];
57
+ /**
58
+ * Shorthand for `filters: [{ col: 'deleted_at', op: 'isNull' }]`.
59
+ * When `true`, soft-deleted rows are excluded. If both `softDelete`
60
+ * and an explicit `deleted_at` filter are present, the explicit filter wins.
61
+ */
62
+ softDelete?: boolean;
63
+ /** Column to ORDER BY. Validated against `[a-zA-Z0-9_]`. */
64
+ orderBy?: string;
65
+ /** Sort direction. Defaults to `'asc'`. */
66
+ orderDir?: 'asc' | 'desc';
67
+ /** Maximum number of rows to return. */
68
+ limit?: number;
69
+ }
44
70
  /**
45
71
  * Yield the entity row itself as a single-element array.
46
72
  * Use for the primary entity file (e.g. `AGENT.md`).
@@ -57,7 +83,7 @@ interface SelfSource {
57
83
  * source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id' }
58
84
  * ```
59
85
  */
60
- interface HasManySource {
86
+ interface HasManySource extends SourceQueryOptions {
61
87
  type: 'hasMany';
62
88
  /** The related table to query */
63
89
  table: string;
@@ -84,7 +110,7 @@ interface HasManySource {
84
110
  * }
85
111
  * ```
86
112
  */
87
- interface ManyToManySource {
113
+ interface ManyToManySource extends SourceQueryOptions {
88
114
  type: 'manyToMany';
89
115
  /** The junction / association table */
90
116
  junctionTable: string;
@@ -111,7 +137,7 @@ interface ManyToManySource {
111
137
  * source: { type: 'belongsTo', table: 'teams', foreignKey: 'team_id' }
112
138
  * ```
113
139
  */
114
- interface BelongsToSource {
140
+ interface BelongsToSource extends SourceQueryOptions {
115
141
  type: 'belongsTo';
116
142
  /** The related table to look up */
117
143
  table: string;
@@ -250,6 +276,17 @@ interface EntityContextDefinition {
250
276
  * Defaults to `[]`.
251
277
  */
252
278
  protectedFiles?: string[];
279
+ /**
280
+ * Default query options merged into every `hasMany`, `manyToMany`, and
281
+ * `belongsTo` source in this context. Per-file source options override
282
+ * these defaults. `custom` and `self` sources are unaffected.
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * sourceDefaults: { softDelete: true } // exclude soft-deleted rows everywhere
287
+ * ```
288
+ */
289
+ sourceDefaults?: SourceQueryOptions;
253
290
  }
254
291
 
255
292
  interface CleanupOptions {
@@ -866,6 +903,69 @@ declare function parseConfigFile(configPath: string): ParsedConfig;
866
903
  */
867
904
  declare function parseConfigString(yamlContent: string, configDir: string): ParsedConfig;
868
905
 
906
+ /**
907
+ * Column definition for {@link markdownTable}.
908
+ */
909
+ interface MarkdownTableColumn {
910
+ /** Row property to read (e.g. `'name'`, `'status'`). */
911
+ key: string;
912
+ /** Column header text displayed in the table. */
913
+ header: string;
914
+ /**
915
+ * Optional per-cell formatter. Receives the raw cell value and the full
916
+ * row so formatters can derive display values from multiple fields.
917
+ *
918
+ * @example `(val) => String(val ?? '—')`
919
+ * @example `(_, row) => \`[\${row.name}](\${row.slug}/DETAIL.md)\``
920
+ */
921
+ format?: (val: unknown, row: Row) => string;
922
+ }
923
+ /**
924
+ * Generate a YAML-style frontmatter block.
925
+ * Automatically includes `generated_at` with the current ISO timestamp.
926
+ *
927
+ * @example
928
+ * ```ts
929
+ * frontmatter({ agent: 'Alice', skill_count: 5 })
930
+ * // "---\ngenerated_at: 2026-03-27T...\nagent: Alice\nskill_count: 5\n---\n\n"
931
+ * ```
932
+ */
933
+ declare function frontmatter(fields: Record<string, string | number | boolean>): string;
934
+ /**
935
+ * Generate a GitHub-Flavoured Markdown table from rows with explicit column
936
+ * configuration. Returns an empty string when `rows` is empty.
937
+ *
938
+ * @example
939
+ * ```ts
940
+ * markdownTable(rows, [
941
+ * { key: 'name', header: 'Name' },
942
+ * { key: 'status', header: 'Status', format: (v) => String(v ?? '—') },
943
+ * ])
944
+ * ```
945
+ */
946
+ declare function markdownTable(rows: Row[], columns: MarkdownTableColumn[]): string;
947
+ /**
948
+ * Generate a URL-safe slug from a display name.
949
+ *
950
+ * - Lowercases, strips diacritics, replaces non-alphanumeric runs with `-`,
951
+ * and trims leading/trailing hyphens.
952
+ *
953
+ * @example `slugify('My Agent Name') // 'my-agent-name'`
954
+ * @example `slugify('José García') // 'jose-garcia'`
955
+ */
956
+ declare function slugify(name: string): string;
957
+ /**
958
+ * Truncate content at a character budget.
959
+ *
960
+ * When `content.length > maxChars`, slices to `maxChars` and appends `notice`.
961
+ * Returns `content` unchanged when the budget is not exceeded.
962
+ *
963
+ * @param content - The rendered content to truncate
964
+ * @param maxChars - Maximum character count
965
+ * @param notice - Appended after truncation (default: standard budget notice)
966
+ */
967
+ declare function truncate(content: string, maxChars: number, notice?: string): string;
968
+
869
969
  type SessionWriteOp = 'create' | 'update' | 'delete';
870
970
  interface SessionWriteEntry {
871
971
  id: string;
@@ -896,9 +996,10 @@ declare function generateWriteEntryId(timestamp: string, agentName: string, op:
896
996
  declare function parseSessionWrites(content: string): SessionWriteParseResult;
897
997
 
898
998
  /**
899
- * A single parsed SESSION.md entry. Covers all entry types:
900
- * event, learning, status, correction, discovery, metric, handoff, write.
999
+ * A single parsed SESSION.md entry.
901
1000
  *
1001
+ * The `type` field holds the resolved entry type (from the built-in set or
1002
+ * custom types supplied via {@link SessionParseOptions}).
902
1003
  * When `type === 'write'`, the op/table/target/reason/fields fields are set.
903
1004
  */
904
1005
  interface SessionEntry {
@@ -929,6 +1030,39 @@ interface ParseResult {
929
1030
  /** Byte offset after the last fully parsed entry — used for incremental parsing. */
930
1031
  lastOffset: number;
931
1032
  }
1033
+ /**
1034
+ * Options for {@link parseSessionMD} and {@link parseMarkdownEntries}.
1035
+ *
1036
+ * All fields are optional — omitting them preserves the default behaviour
1037
+ * (built-in type set + built-in aliases), so existing callers are unaffected.
1038
+ */
1039
+ interface SessionParseOptions {
1040
+ /**
1041
+ * Set of valid entry type names.
1042
+ * - Omit (or `undefined`) → use {@link DEFAULT_ENTRY_TYPES}.
1043
+ * - `null` → accept **any** type string without validation.
1044
+ * - Provide a custom `Set<string>` to restrict to your own taxonomy.
1045
+ */
1046
+ validTypes?: Set<string> | null;
1047
+ /**
1048
+ * Map of non-standard type names to their canonical form.
1049
+ * - Omit (or `undefined`) → use {@link DEFAULT_TYPE_ALIASES}.
1050
+ * - `null` → disable alias resolution.
1051
+ * - Provide a custom `Record<string, string>` for your own aliases.
1052
+ */
1053
+ typeAliases?: Record<string, string> | null;
1054
+ }
1055
+ /**
1056
+ * Default set of valid entry types shipped with latticesql.
1057
+ * Suitable for LLM-agent context systems; override via {@link SessionParseOptions.validTypes}.
1058
+ */
1059
+ declare const DEFAULT_ENTRY_TYPES: ReadonlySet<string>;
1060
+ /**
1061
+ * Default type aliases shipped with latticesql.
1062
+ * Maps commonly-seen alternative names to their canonical type.
1063
+ * Override via {@link SessionParseOptions.typeAliases}.
1064
+ */
1065
+ declare const DEFAULT_TYPE_ALIASES: Readonly<Record<string, string>>;
932
1066
  /**
933
1067
  * Parse SESSION.md YAML-delimited entries starting at `startOffset` bytes.
934
1068
  *
@@ -942,15 +1076,18 @@ interface ParseResult {
942
1076
  * Entry body text here.
943
1077
  * ===
944
1078
  * ```
1079
+ *
1080
+ * Pass {@link SessionParseOptions} to customise which entry types are accepted
1081
+ * and how aliases are resolved. Defaults match the built-in type set.
945
1082
  */
946
- declare function parseSessionMD(content: string, startOffset?: number): ParseResult;
1083
+ declare function parseSessionMD(content: string, startOffset?: number, options?: SessionParseOptions): ParseResult;
947
1084
  /**
948
- * Parse free-form Markdown SESSION.md entries. Agents sometimes write entries
949
- * as `## {timestamp} — {description}` headings rather than YAML blocks.
1085
+ * Parse free-form Markdown SESSION.md entries written as
1086
+ * `## {timestamp} — {description}` headings rather than YAML blocks.
950
1087
  *
951
1088
  * Runs alongside `parseSessionMD`; the two parsers are merged by caller.
952
1089
  */
953
- declare function parseMarkdownEntries(content: string, agentName: string, startOffset?: number): ParseResult;
1090
+ declare function parseMarkdownEntries(content: string, agentName: string, startOffset?: number, options?: SessionParseOptions): ParseResult;
954
1091
  /**
955
1092
  * Generate a content-addressed entry ID.
956
1093
  * Format: `{timestamp}-{agentName}-{6-char-sha256-prefix}`
@@ -988,12 +1125,26 @@ type ApplyWriteResult = {
988
1125
  declare function applyWriteEntry(db: Database.Database, entry: SessionWriteEntry): ApplyWriteResult;
989
1126
 
990
1127
  /**
991
- * Read-only header prepended to all Lattice-generated context files.
1128
+ * Options for {@link createReadOnlyHeader}.
1129
+ */
1130
+ interface ReadOnlyHeaderOptions {
1131
+ /** Name shown as the generator (default: `"Lattice"`). */
1132
+ generator?: string;
1133
+ /** Where to find the SESSION.md format spec (default: `"the Lattice documentation"`). */
1134
+ docsRef?: string;
1135
+ }
1136
+ /**
1137
+ * Build a read-only header for Lattice-generated context files.
1138
+ *
1139
+ * The header tells consumers (human or LLM) that the file is auto-generated
1140
+ * and that writes should go through SESSION.md instead.
1141
+ */
1142
+ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
1143
+ /**
1144
+ * Default read-only header prepended to all Lattice-generated context files.
992
1145
  *
993
- * Directs agents to SESSION.md for writes rather than editing generated files
994
- * directly. Include at the top of every rendered markdown context file so
995
- * agents see it at the start of their context window.
1146
+ * For a customised header, use {@link createReadOnlyHeader} instead.
996
1147
  */
997
- declare const READ_ONLY_HEADER = "<!-- READ ONLY \u2014 generated by lattice-sync. Do not edit directly.\n To update data in Lattice: write entries to SESSION.md in this directory.\n Format: type: write | op: create/update/delete | table: <name> | target: <id>\n See agents/shared/SESSION-FORMAT.md for the full spec. -->\n\n";
1148
+ declare const READ_ONLY_HEADER: string;
998
1149
 
999
- export { type ApplyWriteResult, 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 ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type SessionEntry, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, applyWriteEntry, generateEntryId, generateWriteEntryId, manifestPath, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, validateEntryId, writeManifest };
1150
+ export { type ApplyWriteResult, type AuditEvent, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, 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 MarkdownTableColumn, type Migration, type MultiTableDefinition, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, applyWriteEntry, createReadOnlyHeader, frontmatter, generateEntryId, generateWriteEntryId, manifestPath, markdownTable, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, slugify, truncate, validateEntryId, writeManifest };
package/dist/index.js CHANGED
@@ -292,6 +292,75 @@ import { join as join4 } from "path";
292
292
  import { mkdirSync as mkdirSync2 } from "fs";
293
293
 
294
294
  // src/render/entity-query.ts
295
+ var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
296
+ function effectiveFilters(opts) {
297
+ const filters = opts.filters ? [...opts.filters] : [];
298
+ if (opts.softDelete && !filters.some((f) => f.col === "deleted_at")) {
299
+ filters.unshift({ col: "deleted_at", op: "isNull" });
300
+ }
301
+ return filters;
302
+ }
303
+ function appendQueryOptions(baseSql, params, opts, tableAlias) {
304
+ let sql = baseSql;
305
+ const prefix = tableAlias ? `${tableAlias}.` : "";
306
+ for (const f of effectiveFilters(opts)) {
307
+ if (!SAFE_COL_RE.test(f.col)) continue;
308
+ switch (f.op) {
309
+ case "eq":
310
+ sql += ` AND ${prefix}"${f.col}" = ?`;
311
+ params.push(f.val);
312
+ break;
313
+ case "ne":
314
+ sql += ` AND ${prefix}"${f.col}" != ?`;
315
+ params.push(f.val);
316
+ break;
317
+ case "gt":
318
+ sql += ` AND ${prefix}"${f.col}" > ?`;
319
+ params.push(f.val);
320
+ break;
321
+ case "gte":
322
+ sql += ` AND ${prefix}"${f.col}" >= ?`;
323
+ params.push(f.val);
324
+ break;
325
+ case "lt":
326
+ sql += ` AND ${prefix}"${f.col}" < ?`;
327
+ params.push(f.val);
328
+ break;
329
+ case "lte":
330
+ sql += ` AND ${prefix}"${f.col}" <= ?`;
331
+ params.push(f.val);
332
+ break;
333
+ case "like":
334
+ sql += ` AND ${prefix}"${f.col}" LIKE ?`;
335
+ params.push(f.val);
336
+ break;
337
+ case "in": {
338
+ const arr = f.val;
339
+ if (arr.length === 0) {
340
+ sql += " AND 0";
341
+ } else {
342
+ sql += ` AND ${prefix}"${f.col}" IN (${arr.map(() => "?").join(", ")})`;
343
+ params.push(...arr);
344
+ }
345
+ break;
346
+ }
347
+ case "isNull":
348
+ sql += ` AND ${prefix}"${f.col}" IS NULL`;
349
+ break;
350
+ case "isNotNull":
351
+ sql += ` AND ${prefix}"${f.col}" IS NOT NULL`;
352
+ break;
353
+ }
354
+ }
355
+ if (opts.orderBy && SAFE_COL_RE.test(opts.orderBy)) {
356
+ const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
357
+ sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
358
+ }
359
+ if (opts.limit !== void 0 && opts.limit > 0) {
360
+ sql += ` LIMIT ${Math.floor(opts.limit)}`;
361
+ }
362
+ return sql;
363
+ }
295
364
  function resolveEntitySource(source, entityRow, entityPk, adapter) {
296
365
  switch (source.type) {
297
366
  case "self":
@@ -299,29 +368,37 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
299
368
  case "hasMany": {
300
369
  const ref = source.references ?? entityPk;
301
370
  const pkVal = entityRow[ref];
302
- return adapter.all(
303
- `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`,
304
- [pkVal]
305
- );
371
+ const params = [pkVal];
372
+ let sql = `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`;
373
+ sql = appendQueryOptions(sql, params, source);
374
+ return adapter.all(sql, params);
306
375
  }
307
376
  case "manyToMany": {
308
377
  const pkVal = entityRow[entityPk];
309
378
  const remotePk = source.references ?? "id";
310
- return adapter.all(
311
- `SELECT r.* FROM "${source.remoteTable}" r
379
+ const params = [pkVal];
380
+ let sql = `SELECT r.* FROM "${source.remoteTable}" r
312
381
  JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
313
- WHERE j."${source.localKey}" = ?`,
314
- [pkVal]
315
- );
382
+ WHERE j."${source.localKey}" = ?`;
383
+ sql = appendQueryOptions(sql, params, source, "r");
384
+ return adapter.all(sql, params);
316
385
  }
317
386
  case "belongsTo": {
318
387
  const fkVal = entityRow[source.foreignKey];
319
388
  if (fkVal == null) return [];
320
- const related = adapter.get(
321
- `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
322
- [fkVal]
323
- );
324
- return related ? [related] : [];
389
+ const hasOptions = source.filters?.length || source.softDelete || source.orderBy || source.limit;
390
+ if (!hasOptions) {
391
+ const related = adapter.get(
392
+ `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
393
+ [fkVal]
394
+ );
395
+ return related ? [related] : [];
396
+ }
397
+ const params = [fkVal];
398
+ let sql = `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`;
399
+ sql = appendQueryOptions(sql, params, source);
400
+ const rows = adapter.all(sql, params);
401
+ return rows.length > 0 ? [rows[0]] : [];
325
402
  }
326
403
  case "custom":
327
404
  return source.query(entityRow, adapter);
@@ -537,7 +614,8 @@ var RenderEngine = class {
537
614
  mkdirSync2(entityDir, { recursive: true });
538
615
  const renderedFiles = /* @__PURE__ */ new Map();
539
616
  for (const [filename, spec] of Object.entries(def.files)) {
540
- const rows = resolveEntitySource(spec.source, entityRow, entityPk, this._adapter);
617
+ const source = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" ? { ...def.sourceDefaults, ...spec.source } : spec.source;
618
+ const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
541
619
  if (spec.omitIfEmpty && rows.length === 0) continue;
542
620
  const content = truncateContent(spec.render(rows), spec.budget);
543
621
  renderedFiles.set(filename, content);
@@ -1472,6 +1550,39 @@ var Lattice = class {
1472
1550
  }
1473
1551
  };
1474
1552
 
1553
+ // src/render/markdown.ts
1554
+ function frontmatter(fields) {
1555
+ const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
1556
+ for (const [key, val] of Object.entries(fields)) {
1557
+ lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
1558
+ }
1559
+ return `---
1560
+ ${lines.join("\n")}
1561
+ ---
1562
+
1563
+ `;
1564
+ }
1565
+ function markdownTable(rows, columns) {
1566
+ if (rows.length === 0 || columns.length === 0) return "";
1567
+ const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
1568
+ const separator = "| " + columns.map(() => "---").join(" | ") + " |";
1569
+ const body = rows.map((row) => {
1570
+ const cells = columns.map((col) => {
1571
+ const raw = row[col.key];
1572
+ return col.format ? col.format(raw, row) : String(raw ?? "");
1573
+ });
1574
+ return "| " + cells.join(" | ") + " |";
1575
+ });
1576
+ return [header, separator, ...body].join("\n") + "\n";
1577
+ }
1578
+ function slugify(name) {
1579
+ return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1580
+ }
1581
+ function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
1582
+ if (content.length <= maxChars) return content;
1583
+ return content.slice(0, maxChars) + notice;
1584
+ }
1585
+
1475
1586
  // src/session/parser.ts
1476
1587
  import { createHash as createHash2 } from "crypto";
1477
1588
  function generateWriteEntryId(timestamp, agentName, op, table, target) {
@@ -1585,7 +1696,7 @@ function parseBlock(block) {
1585
1696
 
1586
1697
  // src/session/entries.ts
1587
1698
  import { createHash as createHash3 } from "crypto";
1588
- var VALID_TYPES = /* @__PURE__ */ new Set([
1699
+ var DEFAULT_ENTRY_TYPES = /* @__PURE__ */ new Set([
1589
1700
  "event",
1590
1701
  "learning",
1591
1702
  "status",
@@ -1595,7 +1706,7 @@ var VALID_TYPES = /* @__PURE__ */ new Set([
1595
1706
  "handoff",
1596
1707
  "write"
1597
1708
  ]);
1598
- var TYPE_ALIASES = {
1709
+ var DEFAULT_TYPE_ALIASES = {
1599
1710
  task_completion: "event",
1600
1711
  completion: "event",
1601
1712
  heartbeat: "status",
@@ -1605,7 +1716,7 @@ var TYPE_ALIASES = {
1605
1716
  note: "event"
1606
1717
  };
1607
1718
  var FIELD_NAME_RE2 = /^[a-zA-Z0-9_]+$/;
1608
- function parseSessionMD(content, startOffset = 0) {
1719
+ function parseSessionMD(content, startOffset = 0, options) {
1609
1720
  const entries = [];
1610
1721
  const errors = [];
1611
1722
  const text = content.slice(startOffset);
@@ -1661,7 +1772,7 @@ function parseSessionMD(content, startOffset = 0) {
1661
1772
  }
1662
1773
  const body = bodyLines.join("\n").trim();
1663
1774
  const rawType = headers["type"] ?? "";
1664
- const resolvedType = normalizeType(rawType);
1775
+ const resolvedType = normalizeType(rawType, options);
1665
1776
  if (!resolvedType) {
1666
1777
  errors.push({ line: entryStartLine + 1, message: `Unknown entry type: ${rawType}` });
1667
1778
  continue;
@@ -1711,7 +1822,7 @@ function parseSessionMD(content, startOffset = 0) {
1711
1822
  }
1712
1823
  return { entries, errors, lastOffset: currentByteOffset };
1713
1824
  }
1714
- function parseMarkdownEntries(content, agentName, startOffset = 0) {
1825
+ function parseMarkdownEntries(content, agentName, startOffset = 0, options) {
1715
1826
  const entries = [];
1716
1827
  const errors = [];
1717
1828
  const text = content.slice(startOffset);
@@ -1753,7 +1864,7 @@ function parseMarkdownEntries(content, agentName, startOffset = 0) {
1753
1864
  continue;
1754
1865
  }
1755
1866
  const rawType = bodyType ?? start.headingType ?? "event";
1756
- const resolvedType = normalizeType(rawType) ?? "event";
1867
+ const resolvedType = normalizeType(rawType, options) ?? "event";
1757
1868
  const id = generateEntryId(start.timestamp, agentName, body);
1758
1869
  entries.push({
1759
1870
  id,
@@ -1776,13 +1887,25 @@ function validateEntryId(id, body) {
1776
1887
  const expectedHash = createHash3("sha256").update(body).digest("hex").slice(0, 6);
1777
1888
  return hash === expectedHash;
1778
1889
  }
1779
- function normalizeType(raw) {
1890
+ function normalizeType(raw, options) {
1780
1891
  const lower = raw.toLowerCase().trim();
1781
- if (VALID_TYPES.has(lower)) return lower;
1782
- const normalized = lower.replace(/-/g, "_");
1783
- if (TYPE_ALIASES[normalized]) return TYPE_ALIASES[normalized];
1784
- for (const alias of Object.keys(TYPE_ALIASES)) {
1785
- if (normalized.startsWith(alias)) return TYPE_ALIASES[alias];
1892
+ if (!lower) return null;
1893
+ const validTypes = options?.validTypes === void 0 ? DEFAULT_ENTRY_TYPES : options.validTypes;
1894
+ const aliases = options?.typeAliases === void 0 ? DEFAULT_TYPE_ALIASES : options.typeAliases;
1895
+ if (validTypes === null) {
1896
+ if (aliases) {
1897
+ const normalized = lower.replace(/-/g, "_");
1898
+ if (aliases[normalized]) return aliases[normalized];
1899
+ }
1900
+ return lower;
1901
+ }
1902
+ if (validTypes.has(lower)) return lower;
1903
+ if (aliases) {
1904
+ const normalized = lower.replace(/-/g, "_");
1905
+ if (aliases[normalized]) return aliases[normalized];
1906
+ for (const alias of Object.keys(aliases)) {
1907
+ if (normalized.startsWith(alias)) return aliases[alias];
1908
+ }
1786
1909
  }
1787
1910
  return null;
1788
1911
  }
@@ -1846,25 +1969,37 @@ function applyWriteEntry(db, entry) {
1846
1969
  }
1847
1970
 
1848
1971
  // src/session/constants.ts
1849
- var READ_ONLY_HEADER = `<!-- READ ONLY \u2014 generated by lattice-sync. Do not edit directly.
1972
+ function createReadOnlyHeader(options) {
1973
+ const generator = options?.generator ?? "Lattice";
1974
+ const docsRef = options?.docsRef ?? "the Lattice documentation";
1975
+ return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
1850
1976
  To update data in Lattice: write entries to SESSION.md in this directory.
1851
1977
  Format: type: write | op: create/update/delete | table: <name> | target: <id>
1852
- See agents/shared/SESSION-FORMAT.md for the full spec. -->
1978
+ See ${docsRef} for the SESSION.md format spec. -->
1853
1979
 
1854
1980
  `;
1981
+ }
1982
+ var READ_ONLY_HEADER = createReadOnlyHeader();
1855
1983
  export {
1984
+ DEFAULT_ENTRY_TYPES,
1985
+ DEFAULT_TYPE_ALIASES,
1856
1986
  Lattice,
1857
1987
  READ_ONLY_HEADER,
1858
1988
  applyWriteEntry,
1989
+ createReadOnlyHeader,
1990
+ frontmatter,
1859
1991
  generateEntryId,
1860
1992
  generateWriteEntryId,
1861
1993
  manifestPath,
1994
+ markdownTable,
1862
1995
  parseConfigFile,
1863
1996
  parseConfigString,
1864
1997
  parseMarkdownEntries,
1865
1998
  parseSessionMD,
1866
1999
  parseSessionWrites,
1867
2000
  readManifest,
2001
+ slugify,
2002
+ truncate,
1868
2003
  validateEntryId,
1869
2004
  writeManifest
1870
2005
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Persistent structured memory for AI agent systems — SQLite ↔ LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,11 +34,12 @@
34
34
  "lint:fix": "eslint src tests --fix",
35
35
  "format": "prettier --write .",
36
36
  "format:check": "prettier --check .",
37
+ "check:generic": "bash scripts/check-generic.sh",
37
38
  "test": "vitest run",
38
39
  "test:watch": "vitest",
39
40
  "test:coverage": "vitest run --coverage",
40
41
  "docs": "typedoc --out docs-generated src/index.ts",
41
- "prepublishOnly": "npm run build && npm run typecheck && npm test"
42
+ "prepublishOnly": "npm run check:generic && npm run build && npm run typecheck && npm test"
42
43
  },
43
44
  "dependencies": {
44
45
  "better-sqlite3": "^12.8.0",