latticesql 0.7.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/README.md +214 -5
- package/dist/cli.js +186 -5
- package/dist/index.cjs +193 -51
- package/dist/index.d.cts +115 -6
- package/dist/index.d.ts +115 -6
- package/dist/index.js +193 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -356,13 +356,16 @@ db.defineMulti('agent-context', {
|
|
|
356
356
|
db.defineEntityContext(table: string, def: EntityContextDefinition): this
|
|
357
357
|
```
|
|
358
358
|
|
|
359
|
-
Generate a **parallel file-system tree** for an entity type — one subdirectory per row, one file per declared relationship, and an optional combined context file.
|
|
359
|
+
Generate a **parallel file-system tree** for an entity type — one subdirectory per row, one file per declared relationship, and an optional combined context file. Can be called before or after `init()`.
|
|
360
360
|
|
|
361
361
|
```typescript
|
|
362
362
|
db.defineEntityContext('agents', {
|
|
363
363
|
// Derive the subdirectory name for each entity
|
|
364
364
|
slug: (row) => row.slug as string,
|
|
365
365
|
|
|
366
|
+
// Default query options for all relationship sources (v0.6+)
|
|
367
|
+
sourceDefaults: { softDelete: true },
|
|
368
|
+
|
|
366
369
|
// Global index file listing all entities
|
|
367
370
|
index: {
|
|
368
371
|
outputFile: 'agents/AGENTS.md',
|
|
@@ -376,7 +379,8 @@ db.defineEntityContext('agents', {
|
|
|
376
379
|
render: ([r]) => `# ${r.name as string}\n\n${r.bio as string ?? ''}`,
|
|
377
380
|
},
|
|
378
381
|
'TASKS.md': {
|
|
379
|
-
source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id'
|
|
382
|
+
source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id',
|
|
383
|
+
orderBy: 'created_at', orderDir: 'desc', limit: 20 },
|
|
380
384
|
render: (rows) => rows.map((r) => `- ${r.title as string}`).join('\n'),
|
|
381
385
|
omitIfEmpty: true, // skip if no tasks
|
|
382
386
|
budget: 4000, // truncate at 4 000 chars
|
|
@@ -388,6 +392,7 @@ db.defineEntityContext('agents', {
|
|
|
388
392
|
localKey: 'agent_id',
|
|
389
393
|
remoteKey: 'skill_id',
|
|
390
394
|
remoteTable: 'skills',
|
|
395
|
+
orderBy: 'name', // softDelete inherited from sourceDefaults
|
|
391
396
|
},
|
|
392
397
|
render: (rows) => rows.map((r) => `- ${r.name as string}`).join('\n'),
|
|
393
398
|
omitIfEmpty: true,
|
|
@@ -423,11 +428,82 @@ context/
|
|
|
423
428
|
| Type | What it queries |
|
|
424
429
|
|---|---|
|
|
425
430
|
| `{ type: 'self' }` | The entity row itself |
|
|
426
|
-
| `{ type: 'hasMany', table, foreignKey,
|
|
427
|
-
| `{ type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable,
|
|
428
|
-
| `{ type: 'belongsTo', table, foreignKey,
|
|
431
|
+
| `{ type: 'hasMany', table, foreignKey, ... }` | Rows in `table` where `foreignKey = entityPk` |
|
|
432
|
+
| `{ type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable, ... }` | Remote rows via a junction table |
|
|
433
|
+
| `{ type: 'belongsTo', table, foreignKey, ... }` | Single parent row via FK on this entity (`null` FK → empty) |
|
|
434
|
+
| `{ type: 'enriched', include: { ... } }` | Entity row + related data attached as `_key` JSON fields (v0.7+) |
|
|
429
435
|
| `{ type: 'custom', query: (row, adapter) => Row[] }` | Fully custom synchronous query |
|
|
430
436
|
|
|
437
|
+
#### Source query options (v0.6+)
|
|
438
|
+
|
|
439
|
+
`hasMany`, `manyToMany`, and `belongsTo` sources accept optional query refinements:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
{
|
|
443
|
+
type: 'hasMany',
|
|
444
|
+
table: 'tasks',
|
|
445
|
+
foreignKey: 'agent_id',
|
|
446
|
+
// Query options (all optional):
|
|
447
|
+
softDelete: true, // exclude rows where deleted_at IS NULL
|
|
448
|
+
filters: [ // additional WHERE clauses (uses existing Filter type)
|
|
449
|
+
{ col: 'status', op: 'eq', val: 'active' },
|
|
450
|
+
],
|
|
451
|
+
orderBy: 'created_at', // ORDER BY column
|
|
452
|
+
orderDir: 'desc', // 'asc' (default) or 'desc'
|
|
453
|
+
limit: 20, // LIMIT N
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The `softDelete: true` shorthand is equivalent to `filters: [{ col: 'deleted_at', op: 'isNull' }]`.
|
|
458
|
+
|
|
459
|
+
#### sourceDefaults (v0.6+)
|
|
460
|
+
|
|
461
|
+
Set default query options for all relationship sources in an entity context:
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
db.defineEntityContext('agents', {
|
|
465
|
+
slug: (row) => row.slug as string,
|
|
466
|
+
sourceDefaults: { softDelete: true }, // applied to all hasMany/manyToMany/belongsTo
|
|
467
|
+
files: {
|
|
468
|
+
'TASKS.md': {
|
|
469
|
+
// softDelete: true is inherited from sourceDefaults
|
|
470
|
+
source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id', orderBy: 'created_at' },
|
|
471
|
+
render: (rows) => rows.map((r) => `- ${r.title as string}`).join('\n'),
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Per-file source options override defaults. `custom`, `self`, and `enriched` sources are unaffected.
|
|
478
|
+
|
|
479
|
+
#### Enriched source (v0.7+)
|
|
480
|
+
|
|
481
|
+
Starts with the entity's own row and attaches related data as JSON string fields. Each key in `include` becomes a `_key` field containing `JSON.stringify(resolvedRows)`.
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
'PROFILE.md': {
|
|
485
|
+
source: {
|
|
486
|
+
type: 'enriched',
|
|
487
|
+
include: {
|
|
488
|
+
// Declarative sub-lookups (support all query options)
|
|
489
|
+
skills: { type: 'manyToMany', junctionTable: 'agent_skills',
|
|
490
|
+
localKey: 'agent_id', remoteKey: 'skill_id',
|
|
491
|
+
remoteTable: 'skills', softDelete: true },
|
|
492
|
+
projects: { type: 'hasMany', table: 'projects', foreignKey: 'org_id',
|
|
493
|
+
softDelete: true, orderBy: 'name' },
|
|
494
|
+
// Custom sub-lookup for complex queries
|
|
495
|
+
stats: { type: 'custom', query: (row, adapter) =>
|
|
496
|
+
adapter.all('SELECT COUNT(*) as cnt FROM events WHERE actor_id = ?', [row.id]) },
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
render: ([row]) => {
|
|
500
|
+
const skills = JSON.parse(row._skills as string);
|
|
501
|
+
const projects = JSON.parse(row._projects as string);
|
|
502
|
+
return `# ${row.name}\n\nSkills: ${skills.length}\nProjects: ${projects.length}`;
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
431
507
|
See [docs/entity-context.md](./docs/entity-context.md) for the complete guide.
|
|
432
508
|
|
|
433
509
|
---
|
|
@@ -924,6 +1000,71 @@ db.define('tickets', {
|
|
|
924
1000
|
|
|
925
1001
|
---
|
|
926
1002
|
|
|
1003
|
+
## Markdown utilities (v0.6+)
|
|
1004
|
+
|
|
1005
|
+
Composable helper functions for building render functions. Use inside `render: (rows) => ...` callbacks to reduce boilerplate.
|
|
1006
|
+
|
|
1007
|
+
### `frontmatter(fields)`
|
|
1008
|
+
|
|
1009
|
+
Generate a YAML-style frontmatter block. Automatically includes `generated_at` with the current ISO timestamp.
|
|
1010
|
+
|
|
1011
|
+
```typescript
|
|
1012
|
+
import { frontmatter } from 'latticesql';
|
|
1013
|
+
|
|
1014
|
+
const header = frontmatter({ agent: 'Alice', skill_count: 5 });
|
|
1015
|
+
// ---
|
|
1016
|
+
// generated_at: "2026-03-27T..."
|
|
1017
|
+
// agent: "Alice"
|
|
1018
|
+
// skill_count: 5
|
|
1019
|
+
// ---
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
### `markdownTable(rows, columns)`
|
|
1023
|
+
|
|
1024
|
+
Generate a GitHub-Flavoured Markdown table from rows with explicit column configuration and optional per-cell formatters.
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
import { markdownTable } from 'latticesql';
|
|
1028
|
+
|
|
1029
|
+
const md = markdownTable(rows, [
|
|
1030
|
+
{ key: 'name', header: 'Name' },
|
|
1031
|
+
{ key: 'status', header: 'Status', format: (v) => String(v || '—') },
|
|
1032
|
+
{ key: 'name', header: 'Detail', format: (v, row) => `[view](${row.slug}/DETAIL.md)` },
|
|
1033
|
+
]);
|
|
1034
|
+
// | Name | Status | Detail |
|
|
1035
|
+
// | --- | --- | --- |
|
|
1036
|
+
// | Alice | active | [view](alice/DETAIL.md) |
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
Returns empty string for zero rows. The `format` callback receives `(cellValue, fullRow)`.
|
|
1040
|
+
|
|
1041
|
+
### `slugify(name)`
|
|
1042
|
+
|
|
1043
|
+
Generate a URL-safe slug from a display name — lowercases, strips diacritics, replaces non-alphanumeric runs with hyphens.
|
|
1044
|
+
|
|
1045
|
+
```typescript
|
|
1046
|
+
import { slugify } from 'latticesql';
|
|
1047
|
+
|
|
1048
|
+
slugify('My Agent Name'); // 'my-agent-name'
|
|
1049
|
+
slugify('Jose Garcia'); // 'jose-garcia'
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
### `truncate(content, maxChars, notice?)`
|
|
1053
|
+
|
|
1054
|
+
Truncate content at a character budget. Appends a notice when truncation occurs.
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
import { truncate } from 'latticesql';
|
|
1058
|
+
|
|
1059
|
+
const md = truncate(longContent, 4000);
|
|
1060
|
+
// Appends: "\n\n*[truncated — context budget exceeded]*"
|
|
1061
|
+
|
|
1062
|
+
const md2 = truncate(longContent, 4000, '\n\n[...truncated]');
|
|
1063
|
+
// Custom notice
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
---
|
|
1067
|
+
|
|
927
1068
|
## Entity context directories (v0.5+)
|
|
928
1069
|
|
|
929
1070
|
`defineEntityContext()` is the high-level API for per-entity file generation — the pattern where each entity type gets its own directory tree, with a separate file for each relationship type.
|
|
@@ -1071,6 +1212,74 @@ interface SessionWriteEntry {
|
|
|
1071
1212
|
|
|
1072
1213
|
The processor is responsible for applying the parsed entries to your DB and validating field names against your schema. The `parseSessionWrites` function is pure — no DB access, no side effects.
|
|
1073
1214
|
|
|
1215
|
+
### Full session parser (v0.5.2+)
|
|
1216
|
+
|
|
1217
|
+
For parsing **all** entry types (not just writes), use `parseSessionMD`:
|
|
1218
|
+
|
|
1219
|
+
```ts
|
|
1220
|
+
import { parseSessionMD, parseMarkdownEntries } from 'latticesql';
|
|
1221
|
+
|
|
1222
|
+
// Parse YAML-delimited entries (--- header --- body ===)
|
|
1223
|
+
const result = parseSessionMD(content, startOffset);
|
|
1224
|
+
// result.entries: SessionEntry[] — all types: event, learning, status, write, etc.
|
|
1225
|
+
// result.errors: ParseError[]
|
|
1226
|
+
// result.lastOffset: number — for incremental parsing
|
|
1227
|
+
|
|
1228
|
+
// Parse markdown heading entries (## timestamp — description)
|
|
1229
|
+
const mdResult = parseMarkdownEntries(content, 'agent-name', startOffset);
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
#### Configurable entry types (v0.5.5+)
|
|
1233
|
+
|
|
1234
|
+
By default, the parser validates against a built-in set of entry types. Override via `SessionParseOptions`:
|
|
1235
|
+
|
|
1236
|
+
```ts
|
|
1237
|
+
import { parseSessionMD, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES } from 'latticesql';
|
|
1238
|
+
|
|
1239
|
+
// Accept any type (no validation)
|
|
1240
|
+
parseSessionMD(content, 0, { validTypes: null });
|
|
1241
|
+
|
|
1242
|
+
// Custom type set
|
|
1243
|
+
parseSessionMD(content, 0, {
|
|
1244
|
+
validTypes: new Set(['alert', 'todo', 'write']),
|
|
1245
|
+
typeAliases: { warning: 'alert', task: 'todo' },
|
|
1246
|
+
});
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
### Read-only header (v0.5.5+)
|
|
1250
|
+
|
|
1251
|
+
All generated context files should carry a read-only header. Use the default or create a custom one:
|
|
1252
|
+
|
|
1253
|
+
```ts
|
|
1254
|
+
import { READ_ONLY_HEADER, createReadOnlyHeader } from 'latticesql';
|
|
1255
|
+
|
|
1256
|
+
// Default: "generated by Lattice"
|
|
1257
|
+
const header = READ_ONLY_HEADER;
|
|
1258
|
+
|
|
1259
|
+
// Custom generator name and docs reference
|
|
1260
|
+
const custom = createReadOnlyHeader({
|
|
1261
|
+
generator: 'my-sync-tool',
|
|
1262
|
+
docsRef: 'https://example.com/docs/sessions',
|
|
1263
|
+
});
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
### Write applicator (v0.5.2+)
|
|
1267
|
+
|
|
1268
|
+
Apply parsed write entries to a better-sqlite3 database with schema validation:
|
|
1269
|
+
|
|
1270
|
+
```ts
|
|
1271
|
+
import { applyWriteEntry } from 'latticesql';
|
|
1272
|
+
|
|
1273
|
+
const result = applyWriteEntry(db, writeEntry);
|
|
1274
|
+
if (result.ok) {
|
|
1275
|
+
console.log(`Applied to ${result.table}, record ${result.recordId}`);
|
|
1276
|
+
} else {
|
|
1277
|
+
console.error(result.reason);
|
|
1278
|
+
}
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
Validates table existence, field names against schema, and uses soft-delete when a `deleted_at` column exists.
|
|
1282
|
+
|
|
1074
1283
|
---
|
|
1075
1284
|
|
|
1076
1285
|
## YAML config (v0.4+)
|
package/dist/cli.js
CHANGED
|
@@ -666,9 +666,18 @@ function appendQueryOptions(baseSql, params, opts, tableAlias) {
|
|
|
666
666
|
break;
|
|
667
667
|
}
|
|
668
668
|
}
|
|
669
|
-
if (opts.orderBy
|
|
670
|
-
|
|
671
|
-
|
|
669
|
+
if (opts.orderBy) {
|
|
670
|
+
if (typeof opts.orderBy === "string") {
|
|
671
|
+
if (SAFE_COL_RE.test(opts.orderBy)) {
|
|
672
|
+
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
673
|
+
sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
const clauses = opts.orderBy.filter((spec) => SAFE_COL_RE.test(spec.col)).map((spec) => `${prefix}"${spec.col}" ${spec.dir === "desc" ? "DESC" : "ASC"}`);
|
|
677
|
+
if (clauses.length > 0) {
|
|
678
|
+
sql += ` ORDER BY ${clauses.join(", ")}`;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
672
681
|
}
|
|
673
682
|
if (opts.limit !== void 0 && opts.limit > 0) {
|
|
674
683
|
sql += ` LIMIT ${Math.floor(opts.limit)}`;
|
|
@@ -691,7 +700,19 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
|
691
700
|
const pkVal = entityRow[entityPk];
|
|
692
701
|
const remotePk = source.references ?? "id";
|
|
693
702
|
const params = [pkVal];
|
|
694
|
-
let
|
|
703
|
+
let selectCols = "r.*";
|
|
704
|
+
if (source.junctionColumns?.length) {
|
|
705
|
+
const jCols = source.junctionColumns.map((jc) => {
|
|
706
|
+
if (typeof jc === "string") {
|
|
707
|
+
if (!SAFE_COL_RE.test(jc)) return null;
|
|
708
|
+
return `j."${jc}"`;
|
|
709
|
+
}
|
|
710
|
+
if (!SAFE_COL_RE.test(jc.col) || !SAFE_COL_RE.test(jc.as)) return null;
|
|
711
|
+
return `j."${jc.col}" AS "${jc.as}"`;
|
|
712
|
+
}).filter(Boolean);
|
|
713
|
+
if (jCols.length > 0) selectCols += ", " + jCols.join(", ");
|
|
714
|
+
}
|
|
715
|
+
let sql = `SELECT ${selectCols} FROM "${source.remoteTable}" r
|
|
695
716
|
JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
|
|
696
717
|
WHERE j."${source.localKey}" = ?`;
|
|
697
718
|
sql = appendQueryOptions(sql, params, source, "r");
|
|
@@ -736,6 +757,165 @@ function truncateContent(content, budget) {
|
|
|
736
757
|
return content.slice(0, budget) + "\n\n*[truncated \u2014 context budget exceeded]*";
|
|
737
758
|
}
|
|
738
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
|
+
|
|
739
919
|
// src/lifecycle/cleanup.ts
|
|
740
920
|
import { join as join4 } from "path";
|
|
741
921
|
import { existsSync as existsSync4, readdirSync, unlinkSync, rmdirSync, statSync } from "fs";
|
|
@@ -945,7 +1125,8 @@ var RenderEngine = class {
|
|
|
945
1125
|
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
946
1126
|
const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
|
|
947
1127
|
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
948
|
-
const
|
|
1128
|
+
const renderFn = compileEntityRender(spec.render);
|
|
1129
|
+
const content = truncateContent(renderFn(rows), spec.budget);
|
|
949
1130
|
renderedFiles.set(filename, content);
|
|
950
1131
|
const filePath = join5(entityDir, filename);
|
|
951
1132
|
if (atomicWrite(filePath, content)) {
|