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 +93 -15
- package/dist/index.cjs +171 -29
- package/dist/index.d.cts +166 -15
- package/dist/index.d.ts +166 -15
- package/dist/index.js +164 -29
- package/package.json +3 -2
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
625
|
-
|
|
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
|
-
|
|
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
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
if (
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
949
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
949
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
if (
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|