latticesql 0.8.0 → 0.9.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
@@ -757,6 +757,165 @@ function truncateContent(content, budget) {
757
757
  return content.slice(0, budget) + "\n\n*[truncated \u2014 context budget exceeded]*";
758
758
  }
759
759
 
760
+ // src/render/markdown.ts
761
+ function frontmatter(fields) {
762
+ const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
763
+ for (const [key, val] of Object.entries(fields)) {
764
+ lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
765
+ }
766
+ return `---
767
+ ${lines.join("\n")}
768
+ ---
769
+
770
+ `;
771
+ }
772
+ function markdownTable(rows, columns) {
773
+ if (rows.length === 0 || columns.length === 0) return "";
774
+ const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
775
+ const separator = "| " + columns.map(() => "---").join(" | ") + " |";
776
+ const body = rows.map((row) => {
777
+ const cells = columns.map((col) => {
778
+ const raw = row[col.key];
779
+ return col.format ? col.format(raw, row) : String(raw ?? "");
780
+ });
781
+ return "| " + cells.join(" | ") + " |";
782
+ });
783
+ return [header, separator, ...body].join("\n") + "\n";
784
+ }
785
+
786
+ // src/session/constants.ts
787
+ function createReadOnlyHeader(options) {
788
+ const generator = options?.generator ?? "Lattice";
789
+ const docsRef = options?.docsRef ?? "the Lattice documentation";
790
+ return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
791
+ To update data in Lattice: write entries to SESSION.md in this directory.
792
+ Format: type: write | op: create/update/delete | table: <name> | target: <id>
793
+ See ${docsRef} for the SESSION.md format spec. -->
794
+
795
+ `;
796
+ }
797
+ var READ_ONLY_HEADER = createReadOnlyHeader();
798
+
799
+ // src/render/entity-templates.ts
800
+ var DEFAULT_HEADER = createReadOnlyHeader();
801
+ function compileEntityRender(spec) {
802
+ if (typeof spec === "function") return spec;
803
+ return compileTemplate(spec);
804
+ }
805
+ function compileTemplate(tmpl) {
806
+ switch (tmpl.template) {
807
+ case "entity-table":
808
+ return compileEntityTable(tmpl);
809
+ case "entity-profile":
810
+ return compileEntityProfile(tmpl);
811
+ case "entity-sections":
812
+ return compileEntitySections(tmpl);
813
+ }
814
+ }
815
+ function compileEntityTable(tmpl) {
816
+ return (rows) => {
817
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
818
+ let md = DEFAULT_HEADER;
819
+ md += frontmatter(tmpl.frontmatter ?? {});
820
+ md += `# ${tmpl.heading}
821
+
822
+ `;
823
+ if (data.length === 0) {
824
+ md += tmpl.emptyMessage ?? "*No data.*\n";
825
+ } else {
826
+ md += markdownTable(data, tmpl.columns);
827
+ }
828
+ return md;
829
+ };
830
+ }
831
+ function compileEntityProfile(tmpl) {
832
+ return (rows) => {
833
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
834
+ const r = data[0];
835
+ if (!r) return "";
836
+ let md = DEFAULT_HEADER;
837
+ if (tmpl.frontmatter) {
838
+ const fm = typeof tmpl.frontmatter === "function" ? tmpl.frontmatter(r) : tmpl.frontmatter;
839
+ md += frontmatter(fm);
840
+ } else {
841
+ md += frontmatter({});
842
+ }
843
+ const heading = typeof tmpl.heading === "function" ? tmpl.heading(r) : tmpl.heading;
844
+ md += `# ${heading}
845
+
846
+ `;
847
+ for (const field of tmpl.fields) {
848
+ const val = r[field.key];
849
+ if (val === null || val === void 0) continue;
850
+ const formatted = field.format ? field.format(val, r) : String(val);
851
+ if (formatted) {
852
+ md += `**${field.label}:** ${formatted}
853
+ `;
854
+ }
855
+ }
856
+ if (tmpl.sections) {
857
+ for (const section of tmpl.sections) {
858
+ const rawJson = r[`_${section.key}`];
859
+ if (!rawJson) continue;
860
+ if (section.condition && !section.condition(r)) continue;
861
+ const items = JSON.parse(rawJson);
862
+ if (items.length === 0) continue;
863
+ const sectionHeading = typeof section.heading === "function" ? section.heading(r) : section.heading;
864
+ md += `
865
+ ## ${sectionHeading}
866
+
867
+ `;
868
+ if (section.render === "table" && section.columns) {
869
+ md += markdownTable(items, section.columns);
870
+ } else if (section.render === "list" && section.formatItem) {
871
+ for (const item of items) {
872
+ md += `- ${section.formatItem(item)}
873
+ `;
874
+ }
875
+ } else if (typeof section.render === "function") {
876
+ md += section.render(items);
877
+ }
878
+ }
879
+ }
880
+ return md;
881
+ };
882
+ }
883
+ function compileEntitySections(tmpl) {
884
+ return (rows) => {
885
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
886
+ let md = DEFAULT_HEADER;
887
+ md += frontmatter(tmpl.frontmatter ?? {});
888
+ md += `# ${tmpl.heading}
889
+
890
+ `;
891
+ if (data.length === 0) {
892
+ md += tmpl.emptyMessage ?? "*No data.*\n";
893
+ return md;
894
+ }
895
+ for (const row of data) {
896
+ md += `## ${tmpl.perRow.heading(row)}
897
+ `;
898
+ if (tmpl.perRow.metadata?.length) {
899
+ const parts = tmpl.perRow.metadata.map((m) => {
900
+ const val = row[m.key];
901
+ const formatted = m.format ? m.format(val) : String(val ?? "");
902
+ return `**${m.label}:** ${formatted}`;
903
+ }).filter(Boolean);
904
+ if (parts.length > 0) {
905
+ md += parts.join(" | ") + "\n";
906
+ }
907
+ }
908
+ if (tmpl.perRow.body) {
909
+ md += `
910
+ ${tmpl.perRow.body(row)}
911
+ `;
912
+ }
913
+ md += "\n";
914
+ }
915
+ return md;
916
+ };
917
+ }
918
+
760
919
  // src/lifecycle/cleanup.ts
761
920
  import { join as join4 } from "path";
762
921
  import { existsSync as existsSync4, readdirSync, unlinkSync, rmdirSync, statSync } from "fs";
@@ -966,7 +1125,8 @@ var RenderEngine = class {
966
1125
  const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
967
1126
  const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
968
1127
  if (spec.omitIfEmpty && rows.length === 0) continue;
969
- const content = truncateContent(spec.render(rows), spec.budget);
1128
+ const renderFn = compileEntityRender(spec.render);
1129
+ const content = truncateContent(renderFn(rows), spec.budget);
970
1130
  renderedFiles.set(filename, content);
971
1131
  const filePath = join5(entityDir, filename);
972
1132
  if (atomicWrite(filePath, content)) {
package/dist/index.cjs CHANGED
@@ -499,6 +499,172 @@ function truncateContent(content, budget) {
499
499
  return content.slice(0, budget) + "\n\n*[truncated \u2014 context budget exceeded]*";
500
500
  }
501
501
 
502
+ // src/render/markdown.ts
503
+ function frontmatter(fields) {
504
+ const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
505
+ for (const [key, val] of Object.entries(fields)) {
506
+ lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
507
+ }
508
+ return `---
509
+ ${lines.join("\n")}
510
+ ---
511
+
512
+ `;
513
+ }
514
+ function markdownTable(rows, columns) {
515
+ if (rows.length === 0 || columns.length === 0) return "";
516
+ const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
517
+ const separator = "| " + columns.map(() => "---").join(" | ") + " |";
518
+ const body = rows.map((row) => {
519
+ const cells = columns.map((col) => {
520
+ const raw = row[col.key];
521
+ return col.format ? col.format(raw, row) : String(raw ?? "");
522
+ });
523
+ return "| " + cells.join(" | ") + " |";
524
+ });
525
+ return [header, separator, ...body].join("\n") + "\n";
526
+ }
527
+ function slugify(name) {
528
+ return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
529
+ }
530
+ function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
531
+ if (content.length <= maxChars) return content;
532
+ return content.slice(0, maxChars) + notice;
533
+ }
534
+
535
+ // src/session/constants.ts
536
+ function createReadOnlyHeader(options) {
537
+ const generator = options?.generator ?? "Lattice";
538
+ const docsRef = options?.docsRef ?? "the Lattice documentation";
539
+ return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
540
+ To update data in Lattice: write entries to SESSION.md in this directory.
541
+ Format: type: write | op: create/update/delete | table: <name> | target: <id>
542
+ See ${docsRef} for the SESSION.md format spec. -->
543
+
544
+ `;
545
+ }
546
+ var READ_ONLY_HEADER = createReadOnlyHeader();
547
+
548
+ // src/render/entity-templates.ts
549
+ var DEFAULT_HEADER = createReadOnlyHeader();
550
+ function compileEntityRender(spec) {
551
+ if (typeof spec === "function") return spec;
552
+ return compileTemplate(spec);
553
+ }
554
+ function compileTemplate(tmpl) {
555
+ switch (tmpl.template) {
556
+ case "entity-table":
557
+ return compileEntityTable(tmpl);
558
+ case "entity-profile":
559
+ return compileEntityProfile(tmpl);
560
+ case "entity-sections":
561
+ return compileEntitySections(tmpl);
562
+ }
563
+ }
564
+ function compileEntityTable(tmpl) {
565
+ return (rows) => {
566
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
567
+ let md = DEFAULT_HEADER;
568
+ md += frontmatter(tmpl.frontmatter ?? {});
569
+ md += `# ${tmpl.heading}
570
+
571
+ `;
572
+ if (data.length === 0) {
573
+ md += tmpl.emptyMessage ?? "*No data.*\n";
574
+ } else {
575
+ md += markdownTable(data, tmpl.columns);
576
+ }
577
+ return md;
578
+ };
579
+ }
580
+ function compileEntityProfile(tmpl) {
581
+ return (rows) => {
582
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
583
+ const r = data[0];
584
+ if (!r) return "";
585
+ let md = DEFAULT_HEADER;
586
+ if (tmpl.frontmatter) {
587
+ const fm = typeof tmpl.frontmatter === "function" ? tmpl.frontmatter(r) : tmpl.frontmatter;
588
+ md += frontmatter(fm);
589
+ } else {
590
+ md += frontmatter({});
591
+ }
592
+ const heading = typeof tmpl.heading === "function" ? tmpl.heading(r) : tmpl.heading;
593
+ md += `# ${heading}
594
+
595
+ `;
596
+ for (const field of tmpl.fields) {
597
+ const val = r[field.key];
598
+ if (val === null || val === void 0) continue;
599
+ const formatted = field.format ? field.format(val, r) : String(val);
600
+ if (formatted) {
601
+ md += `**${field.label}:** ${formatted}
602
+ `;
603
+ }
604
+ }
605
+ if (tmpl.sections) {
606
+ for (const section of tmpl.sections) {
607
+ const rawJson = r[`_${section.key}`];
608
+ if (!rawJson) continue;
609
+ if (section.condition && !section.condition(r)) continue;
610
+ const items = JSON.parse(rawJson);
611
+ if (items.length === 0) continue;
612
+ const sectionHeading = typeof section.heading === "function" ? section.heading(r) : section.heading;
613
+ md += `
614
+ ## ${sectionHeading}
615
+
616
+ `;
617
+ if (section.render === "table" && section.columns) {
618
+ md += markdownTable(items, section.columns);
619
+ } else if (section.render === "list" && section.formatItem) {
620
+ for (const item of items) {
621
+ md += `- ${section.formatItem(item)}
622
+ `;
623
+ }
624
+ } else if (typeof section.render === "function") {
625
+ md += section.render(items);
626
+ }
627
+ }
628
+ }
629
+ return md;
630
+ };
631
+ }
632
+ function compileEntitySections(tmpl) {
633
+ return (rows) => {
634
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
635
+ let md = DEFAULT_HEADER;
636
+ md += frontmatter(tmpl.frontmatter ?? {});
637
+ md += `# ${tmpl.heading}
638
+
639
+ `;
640
+ if (data.length === 0) {
641
+ md += tmpl.emptyMessage ?? "*No data.*\n";
642
+ return md;
643
+ }
644
+ for (const row of data) {
645
+ md += `## ${tmpl.perRow.heading(row)}
646
+ `;
647
+ if (tmpl.perRow.metadata?.length) {
648
+ const parts = tmpl.perRow.metadata.map((m) => {
649
+ const val = row[m.key];
650
+ const formatted = m.format ? m.format(val) : String(val ?? "");
651
+ return `**${m.label}:** ${formatted}`;
652
+ }).filter(Boolean);
653
+ if (parts.length > 0) {
654
+ md += parts.join(" | ") + "\n";
655
+ }
656
+ }
657
+ if (tmpl.perRow.body) {
658
+ md += `
659
+ ${tmpl.perRow.body(row)}
660
+ `;
661
+ }
662
+ md += "\n";
663
+ }
664
+ return md;
665
+ };
666
+ }
667
+
502
668
  // src/lifecycle/cleanup.ts
503
669
  var import_node_path3 = require("path");
504
670
  var import_node_fs3 = require("fs");
@@ -708,7 +874,8 @@ var RenderEngine = class {
708
874
  const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
709
875
  const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
710
876
  if (spec.omitIfEmpty && rows.length === 0) continue;
711
- const content = truncateContent(spec.render(rows), spec.budget);
877
+ const renderFn = compileEntityRender(spec.render);
878
+ const content = truncateContent(renderFn(rows), spec.budget);
712
879
  renderedFiles.set(filename, content);
713
880
  const filePath = (0, import_node_path4.join)(entityDir, filename);
714
881
  if (atomicWrite(filePath, content)) {
@@ -1641,39 +1808,6 @@ var Lattice = class {
1641
1808
  }
1642
1809
  };
1643
1810
 
1644
- // src/render/markdown.ts
1645
- function frontmatter(fields) {
1646
- const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
1647
- for (const [key, val] of Object.entries(fields)) {
1648
- lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
1649
- }
1650
- return `---
1651
- ${lines.join("\n")}
1652
- ---
1653
-
1654
- `;
1655
- }
1656
- function markdownTable(rows, columns) {
1657
- if (rows.length === 0 || columns.length === 0) return "";
1658
- const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
1659
- const separator = "| " + columns.map(() => "---").join(" | ") + " |";
1660
- const body = rows.map((row) => {
1661
- const cells = columns.map((col) => {
1662
- const raw = row[col.key];
1663
- return col.format ? col.format(raw, row) : String(raw ?? "");
1664
- });
1665
- return "| " + cells.join(" | ") + " |";
1666
- });
1667
- return [header, separator, ...body].join("\n") + "\n";
1668
- }
1669
- function slugify(name) {
1670
- return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1671
- }
1672
- function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
1673
- if (content.length <= maxChars) return content;
1674
- return content.slice(0, maxChars) + notice;
1675
- }
1676
-
1677
1811
  // src/session/parser.ts
1678
1812
  var import_node_crypto3 = require("crypto");
1679
1813
  function generateWriteEntryId(timestamp, agentName, op, table, target) {
@@ -2058,19 +2192,6 @@ function applyWriteEntry(db, entry) {
2058
2192
  return { ok: false, reason: `DB error: ${message}` };
2059
2193
  }
2060
2194
  }
2061
-
2062
- // src/session/constants.ts
2063
- function createReadOnlyHeader(options) {
2064
- const generator = options?.generator ?? "Lattice";
2065
- const docsRef = options?.docsRef ?? "the Lattice documentation";
2066
- return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
2067
- To update data in Lattice: write entries to SESSION.md in this directory.
2068
- Format: type: write | op: create/update/delete | table: <name> | target: <id>
2069
- See ${docsRef} for the SESSION.md format spec. -->
2070
-
2071
- `;
2072
- }
2073
- var READ_ONLY_HEADER = createReadOnlyHeader();
2074
2195
  // Annotate the CommonJS export names for ESM import in node:
2075
2196
  0 && (module.exports = {
2076
2197
  DEFAULT_ENTRY_TYPES,
package/dist/index.d.cts CHANGED
@@ -266,14 +266,89 @@ type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsT
266
266
  * }
267
267
  * ```
268
268
  */
269
+ /** Column spec for entity-table template (reuses MarkdownTableColumn). */
270
+ interface EntityTableColumn {
271
+ key: string;
272
+ header: string;
273
+ format?: (val: unknown, row: Row) => string;
274
+ }
275
+ /**
276
+ * Render a heading + GFM table. Auto-prepends read-only header + frontmatter.
277
+ */
278
+ interface EntityTableTemplate {
279
+ template: 'entity-table';
280
+ heading: string;
281
+ columns: EntityTableColumn[];
282
+ emptyMessage?: string;
283
+ frontmatter?: Record<string, string | number | boolean>;
284
+ beforeRender?: (rows: Row[]) => Row[];
285
+ }
286
+ /**
287
+ * Field spec for entity-profile template.
288
+ */
289
+ interface EntityProfileField {
290
+ key: string;
291
+ label: string;
292
+ format?: (val: unknown, row: Row) => string;
293
+ }
294
+ /**
295
+ * Section spec for entity-profile template (renders enriched JSON arrays).
296
+ */
297
+ interface EntityProfileSection {
298
+ /** Key of the enriched field (e.g. 'agents' → reads row._agents). */
299
+ key: string;
300
+ heading: string | ((row: Row) => string);
301
+ condition?: (row: Row) => boolean;
302
+ render: 'table' | 'list' | ((items: Row[]) => string);
303
+ columns?: EntityTableColumn[];
304
+ formatItem?: (item: Row) => string;
305
+ }
306
+ /**
307
+ * Render entity profile: heading + field-value pairs + optional enriched sections.
308
+ */
309
+ interface EntityProfileTemplate {
310
+ template: 'entity-profile';
311
+ heading: string | ((row: Row) => string);
312
+ fields: EntityProfileField[];
313
+ sections?: EntityProfileSection[];
314
+ frontmatter?: Record<string, string | number | boolean> | ((row: Row) => Record<string, string | number | boolean>);
315
+ beforeRender?: (rows: Row[]) => Row[];
316
+ }
317
+ /**
318
+ * Per-row section spec for entity-sections template.
319
+ */
320
+ interface EntitySectionPerRow {
321
+ heading: (row: Row) => string;
322
+ metadata?: Array<{
323
+ key: string;
324
+ label: string;
325
+ format?: (val: unknown) => string;
326
+ }>;
327
+ body?: (row: Row) => string;
328
+ }
329
+ /**
330
+ * Render per-row sections: heading + metadata key-value + body text per row.
331
+ */
332
+ interface EntitySectionsTemplate {
333
+ template: 'entity-sections';
334
+ heading: string;
335
+ perRow: EntitySectionPerRow;
336
+ emptyMessage?: string;
337
+ frontmatter?: Record<string, string | number | boolean>;
338
+ beforeRender?: (rows: Row[]) => Row[];
339
+ }
340
+ /** Union of all entity render template types. */
341
+ type EntityRenderTemplate = EntityTableTemplate | EntityProfileTemplate | EntitySectionsTemplate;
342
+ /** Accepted values for EntityFileSpec.render — function or template object. */
343
+ type EntityRenderSpec = ((rows: Row[]) => string) | EntityRenderTemplate;
269
344
  interface EntityFileSpec {
270
345
  /** Determines what rows are passed to {@link render}. */
271
346
  source: EntityFileSource;
272
347
  /**
273
348
  * Converts the resolved rows into the file's markdown content.
274
- * For `self` sources, `rows` is always a single-element array.
349
+ * Accepts a function `(rows) => string` or a declarative template object.
275
350
  */
276
- render: (rows: Row[]) => string;
351
+ render: EntityRenderSpec;
277
352
  /**
278
353
  * Maximum number of characters allowed in the rendered output.
279
354
  * Content exceeding this limit is truncated with a notice appended.
@@ -1230,4 +1305,4 @@ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
1230
1305
  */
1231
1306
  declare const READ_ONLY_HEADER: string;
1232
1307
 
1233
- 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 EnrichedSource, type EnrichmentLookup, 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 OrderBySpec, 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 };
1308
+ 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 EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, 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 OrderBySpec, 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
@@ -266,14 +266,89 @@ type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsT
266
266
  * }
267
267
  * ```
268
268
  */
269
+ /** Column spec for entity-table template (reuses MarkdownTableColumn). */
270
+ interface EntityTableColumn {
271
+ key: string;
272
+ header: string;
273
+ format?: (val: unknown, row: Row) => string;
274
+ }
275
+ /**
276
+ * Render a heading + GFM table. Auto-prepends read-only header + frontmatter.
277
+ */
278
+ interface EntityTableTemplate {
279
+ template: 'entity-table';
280
+ heading: string;
281
+ columns: EntityTableColumn[];
282
+ emptyMessage?: string;
283
+ frontmatter?: Record<string, string | number | boolean>;
284
+ beforeRender?: (rows: Row[]) => Row[];
285
+ }
286
+ /**
287
+ * Field spec for entity-profile template.
288
+ */
289
+ interface EntityProfileField {
290
+ key: string;
291
+ label: string;
292
+ format?: (val: unknown, row: Row) => string;
293
+ }
294
+ /**
295
+ * Section spec for entity-profile template (renders enriched JSON arrays).
296
+ */
297
+ interface EntityProfileSection {
298
+ /** Key of the enriched field (e.g. 'agents' → reads row._agents). */
299
+ key: string;
300
+ heading: string | ((row: Row) => string);
301
+ condition?: (row: Row) => boolean;
302
+ render: 'table' | 'list' | ((items: Row[]) => string);
303
+ columns?: EntityTableColumn[];
304
+ formatItem?: (item: Row) => string;
305
+ }
306
+ /**
307
+ * Render entity profile: heading + field-value pairs + optional enriched sections.
308
+ */
309
+ interface EntityProfileTemplate {
310
+ template: 'entity-profile';
311
+ heading: string | ((row: Row) => string);
312
+ fields: EntityProfileField[];
313
+ sections?: EntityProfileSection[];
314
+ frontmatter?: Record<string, string | number | boolean> | ((row: Row) => Record<string, string | number | boolean>);
315
+ beforeRender?: (rows: Row[]) => Row[];
316
+ }
317
+ /**
318
+ * Per-row section spec for entity-sections template.
319
+ */
320
+ interface EntitySectionPerRow {
321
+ heading: (row: Row) => string;
322
+ metadata?: Array<{
323
+ key: string;
324
+ label: string;
325
+ format?: (val: unknown) => string;
326
+ }>;
327
+ body?: (row: Row) => string;
328
+ }
329
+ /**
330
+ * Render per-row sections: heading + metadata key-value + body text per row.
331
+ */
332
+ interface EntitySectionsTemplate {
333
+ template: 'entity-sections';
334
+ heading: string;
335
+ perRow: EntitySectionPerRow;
336
+ emptyMessage?: string;
337
+ frontmatter?: Record<string, string | number | boolean>;
338
+ beforeRender?: (rows: Row[]) => Row[];
339
+ }
340
+ /** Union of all entity render template types. */
341
+ type EntityRenderTemplate = EntityTableTemplate | EntityProfileTemplate | EntitySectionsTemplate;
342
+ /** Accepted values for EntityFileSpec.render — function or template object. */
343
+ type EntityRenderSpec = ((rows: Row[]) => string) | EntityRenderTemplate;
269
344
  interface EntityFileSpec {
270
345
  /** Determines what rows are passed to {@link render}. */
271
346
  source: EntityFileSource;
272
347
  /**
273
348
  * Converts the resolved rows into the file's markdown content.
274
- * For `self` sources, `rows` is always a single-element array.
349
+ * Accepts a function `(rows) => string` or a declarative template object.
275
350
  */
276
- render: (rows: Row[]) => string;
351
+ render: EntityRenderSpec;
277
352
  /**
278
353
  * Maximum number of characters allowed in the rendered output.
279
354
  * Content exceeding this limit is truncated with a notice appended.
@@ -1230,4 +1305,4 @@ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
1230
1305
  */
1231
1306
  declare const READ_ONLY_HEADER: string;
1232
1307
 
1233
- 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 EnrichedSource, type EnrichmentLookup, 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 OrderBySpec, 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 };
1308
+ 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 EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, 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 OrderBySpec, 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
@@ -443,6 +443,172 @@ function truncateContent(content, budget) {
443
443
  return content.slice(0, budget) + "\n\n*[truncated \u2014 context budget exceeded]*";
444
444
  }
445
445
 
446
+ // src/render/markdown.ts
447
+ function frontmatter(fields) {
448
+ const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
449
+ for (const [key, val] of Object.entries(fields)) {
450
+ lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
451
+ }
452
+ return `---
453
+ ${lines.join("\n")}
454
+ ---
455
+
456
+ `;
457
+ }
458
+ function markdownTable(rows, columns) {
459
+ if (rows.length === 0 || columns.length === 0) return "";
460
+ const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
461
+ const separator = "| " + columns.map(() => "---").join(" | ") + " |";
462
+ const body = rows.map((row) => {
463
+ const cells = columns.map((col) => {
464
+ const raw = row[col.key];
465
+ return col.format ? col.format(raw, row) : String(raw ?? "");
466
+ });
467
+ return "| " + cells.join(" | ") + " |";
468
+ });
469
+ return [header, separator, ...body].join("\n") + "\n";
470
+ }
471
+ function slugify(name) {
472
+ return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
473
+ }
474
+ function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
475
+ if (content.length <= maxChars) return content;
476
+ return content.slice(0, maxChars) + notice;
477
+ }
478
+
479
+ // src/session/constants.ts
480
+ function createReadOnlyHeader(options) {
481
+ const generator = options?.generator ?? "Lattice";
482
+ const docsRef = options?.docsRef ?? "the Lattice documentation";
483
+ return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
484
+ To update data in Lattice: write entries to SESSION.md in this directory.
485
+ Format: type: write | op: create/update/delete | table: <name> | target: <id>
486
+ See ${docsRef} for the SESSION.md format spec. -->
487
+
488
+ `;
489
+ }
490
+ var READ_ONLY_HEADER = createReadOnlyHeader();
491
+
492
+ // src/render/entity-templates.ts
493
+ var DEFAULT_HEADER = createReadOnlyHeader();
494
+ function compileEntityRender(spec) {
495
+ if (typeof spec === "function") return spec;
496
+ return compileTemplate(spec);
497
+ }
498
+ function compileTemplate(tmpl) {
499
+ switch (tmpl.template) {
500
+ case "entity-table":
501
+ return compileEntityTable(tmpl);
502
+ case "entity-profile":
503
+ return compileEntityProfile(tmpl);
504
+ case "entity-sections":
505
+ return compileEntitySections(tmpl);
506
+ }
507
+ }
508
+ function compileEntityTable(tmpl) {
509
+ return (rows) => {
510
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
511
+ let md = DEFAULT_HEADER;
512
+ md += frontmatter(tmpl.frontmatter ?? {});
513
+ md += `# ${tmpl.heading}
514
+
515
+ `;
516
+ if (data.length === 0) {
517
+ md += tmpl.emptyMessage ?? "*No data.*\n";
518
+ } else {
519
+ md += markdownTable(data, tmpl.columns);
520
+ }
521
+ return md;
522
+ };
523
+ }
524
+ function compileEntityProfile(tmpl) {
525
+ return (rows) => {
526
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
527
+ const r = data[0];
528
+ if (!r) return "";
529
+ let md = DEFAULT_HEADER;
530
+ if (tmpl.frontmatter) {
531
+ const fm = typeof tmpl.frontmatter === "function" ? tmpl.frontmatter(r) : tmpl.frontmatter;
532
+ md += frontmatter(fm);
533
+ } else {
534
+ md += frontmatter({});
535
+ }
536
+ const heading = typeof tmpl.heading === "function" ? tmpl.heading(r) : tmpl.heading;
537
+ md += `# ${heading}
538
+
539
+ `;
540
+ for (const field of tmpl.fields) {
541
+ const val = r[field.key];
542
+ if (val === null || val === void 0) continue;
543
+ const formatted = field.format ? field.format(val, r) : String(val);
544
+ if (formatted) {
545
+ md += `**${field.label}:** ${formatted}
546
+ `;
547
+ }
548
+ }
549
+ if (tmpl.sections) {
550
+ for (const section of tmpl.sections) {
551
+ const rawJson = r[`_${section.key}`];
552
+ if (!rawJson) continue;
553
+ if (section.condition && !section.condition(r)) continue;
554
+ const items = JSON.parse(rawJson);
555
+ if (items.length === 0) continue;
556
+ const sectionHeading = typeof section.heading === "function" ? section.heading(r) : section.heading;
557
+ md += `
558
+ ## ${sectionHeading}
559
+
560
+ `;
561
+ if (section.render === "table" && section.columns) {
562
+ md += markdownTable(items, section.columns);
563
+ } else if (section.render === "list" && section.formatItem) {
564
+ for (const item of items) {
565
+ md += `- ${section.formatItem(item)}
566
+ `;
567
+ }
568
+ } else if (typeof section.render === "function") {
569
+ md += section.render(items);
570
+ }
571
+ }
572
+ }
573
+ return md;
574
+ };
575
+ }
576
+ function compileEntitySections(tmpl) {
577
+ return (rows) => {
578
+ const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
579
+ let md = DEFAULT_HEADER;
580
+ md += frontmatter(tmpl.frontmatter ?? {});
581
+ md += `# ${tmpl.heading}
582
+
583
+ `;
584
+ if (data.length === 0) {
585
+ md += tmpl.emptyMessage ?? "*No data.*\n";
586
+ return md;
587
+ }
588
+ for (const row of data) {
589
+ md += `## ${tmpl.perRow.heading(row)}
590
+ `;
591
+ if (tmpl.perRow.metadata?.length) {
592
+ const parts = tmpl.perRow.metadata.map((m) => {
593
+ const val = row[m.key];
594
+ const formatted = m.format ? m.format(val) : String(val ?? "");
595
+ return `**${m.label}:** ${formatted}`;
596
+ }).filter(Boolean);
597
+ if (parts.length > 0) {
598
+ md += parts.join(" | ") + "\n";
599
+ }
600
+ }
601
+ if (tmpl.perRow.body) {
602
+ md += `
603
+ ${tmpl.perRow.body(row)}
604
+ `;
605
+ }
606
+ md += "\n";
607
+ }
608
+ return md;
609
+ };
610
+ }
611
+
446
612
  // src/lifecycle/cleanup.ts
447
613
  import { join as join3 } from "path";
448
614
  import { existsSync as existsSync3, readdirSync, unlinkSync, rmdirSync, statSync } from "fs";
@@ -652,7 +818,8 @@ var RenderEngine = class {
652
818
  const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
653
819
  const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
654
820
  if (spec.omitIfEmpty && rows.length === 0) continue;
655
- const content = truncateContent(spec.render(rows), spec.budget);
821
+ const renderFn = compileEntityRender(spec.render);
822
+ const content = truncateContent(renderFn(rows), spec.budget);
656
823
  renderedFiles.set(filename, content);
657
824
  const filePath = join4(entityDir, filename);
658
825
  if (atomicWrite(filePath, content)) {
@@ -1585,39 +1752,6 @@ var Lattice = class {
1585
1752
  }
1586
1753
  };
1587
1754
 
1588
- // src/render/markdown.ts
1589
- function frontmatter(fields) {
1590
- const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
1591
- for (const [key, val] of Object.entries(fields)) {
1592
- lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
1593
- }
1594
- return `---
1595
- ${lines.join("\n")}
1596
- ---
1597
-
1598
- `;
1599
- }
1600
- function markdownTable(rows, columns) {
1601
- if (rows.length === 0 || columns.length === 0) return "";
1602
- const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
1603
- const separator = "| " + columns.map(() => "---").join(" | ") + " |";
1604
- const body = rows.map((row) => {
1605
- const cells = columns.map((col) => {
1606
- const raw = row[col.key];
1607
- return col.format ? col.format(raw, row) : String(raw ?? "");
1608
- });
1609
- return "| " + cells.join(" | ") + " |";
1610
- });
1611
- return [header, separator, ...body].join("\n") + "\n";
1612
- }
1613
- function slugify(name) {
1614
- return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1615
- }
1616
- function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
1617
- if (content.length <= maxChars) return content;
1618
- return content.slice(0, maxChars) + notice;
1619
- }
1620
-
1621
1755
  // src/session/parser.ts
1622
1756
  import { createHash as createHash2 } from "crypto";
1623
1757
  function generateWriteEntryId(timestamp, agentName, op, table, target) {
@@ -2002,19 +2136,6 @@ function applyWriteEntry(db, entry) {
2002
2136
  return { ok: false, reason: `DB error: ${message}` };
2003
2137
  }
2004
2138
  }
2005
-
2006
- // src/session/constants.ts
2007
- function createReadOnlyHeader(options) {
2008
- const generator = options?.generator ?? "Lattice";
2009
- const docsRef = options?.docsRef ?? "the Lattice documentation";
2010
- return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
2011
- To update data in Lattice: write entries to SESSION.md in this directory.
2012
- Format: type: write | op: create/update/delete | table: <name> | target: <id>
2013
- See ${docsRef} for the SESSION.md format spec. -->
2014
-
2015
- `;
2016
- }
2017
- var READ_ONLY_HEADER = createReadOnlyHeader();
2018
2139
  export {
2019
2140
  DEFAULT_ENTRY_TYPES,
2020
2141
  DEFAULT_TYPE_ALIASES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",