latticesql 0.5.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/LICENSE +182 -0
- package/README.md +1360 -0
- package/dist/cli.js +1880 -0
- package/dist/index.cjs +1514 -0
- package/dist/index.d.cts +869 -0
- package/dist/index.d.ts +869 -0
- package/dist/index.js +1472 -0
- package/package.json +79 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
interface LatticeManifest {
|
|
4
|
+
version: 1;
|
|
5
|
+
generated_at: string;
|
|
6
|
+
entityContexts: Record<string, EntityContextManifestEntry>;
|
|
7
|
+
}
|
|
8
|
+
interface EntityContextManifestEntry {
|
|
9
|
+
directoryRoot: string;
|
|
10
|
+
indexFile?: string;
|
|
11
|
+
declaredFiles: string[];
|
|
12
|
+
protectedFiles: string[];
|
|
13
|
+
/** Key = slug, value = filenames actually written (omitIfEmpty files may be absent) */
|
|
14
|
+
entities: Record<string, string[]>;
|
|
15
|
+
}
|
|
16
|
+
declare function manifestPath(outputDir: string): string;
|
|
17
|
+
declare function readManifest(outputDir: string): LatticeManifest | null;
|
|
18
|
+
declare function writeManifest(outputDir: string, manifest: LatticeManifest): void;
|
|
19
|
+
|
|
20
|
+
/** Pluggable storage backend interface */
|
|
21
|
+
interface StorageAdapter {
|
|
22
|
+
/** Execute a statement with no return value */
|
|
23
|
+
run(sql: string, params?: unknown[]): void;
|
|
24
|
+
/** Execute a statement and return one row or undefined */
|
|
25
|
+
get(sql: string, params?: unknown[]): Row | undefined;
|
|
26
|
+
/** Execute a statement and return all rows */
|
|
27
|
+
all(sql: string, params?: unknown[]): Row[];
|
|
28
|
+
/** Prepare and cache a statement for repeated execution */
|
|
29
|
+
prepare(sql: string): PreparedStatement;
|
|
30
|
+
/** Open the connection */
|
|
31
|
+
open(): void;
|
|
32
|
+
/** Close the connection */
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
interface PreparedStatement {
|
|
36
|
+
run(...params: unknown[]): {
|
|
37
|
+
changes: number;
|
|
38
|
+
lastInsertRowid: number | bigint;
|
|
39
|
+
};
|
|
40
|
+
get(...params: unknown[]): Row | undefined;
|
|
41
|
+
all(...params: unknown[]): Row[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Yield the entity row itself as a single-element array.
|
|
46
|
+
* Use for the primary entity file (e.g. `AGENT.md`).
|
|
47
|
+
*/
|
|
48
|
+
interface SelfSource {
|
|
49
|
+
type: 'self';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Query rows from another table where a foreign key on that table points back
|
|
53
|
+
* to this entity (e.g. all tasks where `tasks.agent_id = agent.id`).
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id' }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
interface HasManySource {
|
|
61
|
+
type: 'hasMany';
|
|
62
|
+
/** The related table to query */
|
|
63
|
+
table: string;
|
|
64
|
+
/** Column on the RELATED table that holds the FK pointing to this entity */
|
|
65
|
+
foreignKey: string;
|
|
66
|
+
/**
|
|
67
|
+
* Column on THIS entity's table that is referenced.
|
|
68
|
+
* Defaults to the entity table's first primary key column.
|
|
69
|
+
*/
|
|
70
|
+
references?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Query rows from a remote table via a junction table
|
|
74
|
+
* (e.g. skills for an agent via `agent_skills`).
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* source: {
|
|
79
|
+
* type: 'manyToMany',
|
|
80
|
+
* junctionTable: 'agent_skills',
|
|
81
|
+
* localKey: 'agent_id', // FK in junction → this entity
|
|
82
|
+
* remoteKey: 'skill_id', // FK in junction → remote entity
|
|
83
|
+
* remoteTable: 'skills',
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
interface ManyToManySource {
|
|
88
|
+
type: 'manyToMany';
|
|
89
|
+
/** The junction / association table */
|
|
90
|
+
junctionTable: string;
|
|
91
|
+
/** Column in the junction table that points to THIS entity */
|
|
92
|
+
localKey: string;
|
|
93
|
+
/** Column in the junction table that points to the REMOTE entity */
|
|
94
|
+
remoteKey: string;
|
|
95
|
+
/** The remote table to JOIN and return rows from */
|
|
96
|
+
remoteTable: string;
|
|
97
|
+
/**
|
|
98
|
+
* Primary key column on `remoteTable` that `remoteKey` references.
|
|
99
|
+
* Defaults to `'id'`.
|
|
100
|
+
*/
|
|
101
|
+
references?: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Query the single row that this entity belongs to via a foreign key on
|
|
105
|
+
* THIS entity's table (e.g. the team a bot belongs to via `bot.team_id`).
|
|
106
|
+
*
|
|
107
|
+
* Returns `[]` when the FK column is NULL; returns `[row]` when found.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* source: { type: 'belongsTo', table: 'teams', foreignKey: 'team_id' }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
interface BelongsToSource {
|
|
115
|
+
type: 'belongsTo';
|
|
116
|
+
/** The related table to look up */
|
|
117
|
+
table: string;
|
|
118
|
+
/** Column on THIS entity's table that holds the FK */
|
|
119
|
+
foreignKey: string;
|
|
120
|
+
/**
|
|
121
|
+
* Column on the RELATED table being referenced.
|
|
122
|
+
* Defaults to `'id'`.
|
|
123
|
+
*/
|
|
124
|
+
references?: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Fully custom query — caller receives the entity row and the raw SQLite
|
|
128
|
+
* adapter, returns whatever rows they need. Use a closure to capture any
|
|
129
|
+
* additional context (other table names, filter conditions, etc.).
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* source: {
|
|
134
|
+
* type: 'custom',
|
|
135
|
+
* query: (row, adapter) =>
|
|
136
|
+
* adapter.all('SELECT * FROM events WHERE actor_id = ? ORDER BY ts DESC LIMIT 20', [row.id]),
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
interface CustomSource {
|
|
141
|
+
type: 'custom';
|
|
142
|
+
query: (row: Row, adapter: StorageAdapter) => Row[];
|
|
143
|
+
}
|
|
144
|
+
/** Union of all supported source types for {@link EntityFileSpec}. */
|
|
145
|
+
type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsToSource | CustomSource;
|
|
146
|
+
/**
|
|
147
|
+
* Specification for a single file generated inside an entity's directory.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* 'SKILLS.md': {
|
|
152
|
+
* source: { type: 'manyToMany', junctionTable: 'agent_skills', localKey: 'agent_id',
|
|
153
|
+
* remoteKey: 'skill_id', remoteTable: 'skills' },
|
|
154
|
+
* render: (rows) => `# Skills\n\n${rows.map(r => `- ${r.name}`).join('\n')}`,
|
|
155
|
+
* omitIfEmpty: true,
|
|
156
|
+
* budget: 2000,
|
|
157
|
+
* }
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
interface EntityFileSpec {
|
|
161
|
+
/** Determines what rows are passed to {@link render}. */
|
|
162
|
+
source: EntityFileSource;
|
|
163
|
+
/**
|
|
164
|
+
* Converts the resolved rows into the file's markdown content.
|
|
165
|
+
* For `self` sources, `rows` is always a single-element array.
|
|
166
|
+
*/
|
|
167
|
+
render: (rows: Row[]) => string;
|
|
168
|
+
/**
|
|
169
|
+
* Maximum number of characters allowed in the rendered output.
|
|
170
|
+
* Content exceeding this limit is truncated with a notice appended.
|
|
171
|
+
*/
|
|
172
|
+
budget?: number;
|
|
173
|
+
/**
|
|
174
|
+
* When `true`, skip writing this file if the source returns zero rows.
|
|
175
|
+
* Defaults to `false`.
|
|
176
|
+
*/
|
|
177
|
+
omitIfEmpty?: boolean;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Defines the parallel file-system structure for one entity type.
|
|
181
|
+
*
|
|
182
|
+
* Lattice uses this to generate:
|
|
183
|
+
* - An optional global index file listing all entities
|
|
184
|
+
* - A per-entity subdirectory with one file per declared {@link files} entry
|
|
185
|
+
* - An optional combined context file (CONTEXT.md) concatenating all files
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* db.defineEntityContext('agents', {
|
|
190
|
+
* slug: (row) => row.slug as string,
|
|
191
|
+
* index: { outputFile: 'agents/AGENTS.md', render: (rows) => `# Agents\n...` },
|
|
192
|
+
* files: {
|
|
193
|
+
* 'AGENT.md': { source: { type: 'self' }, render: ([r]) => `# ${r.name}` },
|
|
194
|
+
* 'SKILLS.md': { source: { type: 'manyToMany', ... }, render, omitIfEmpty: true },
|
|
195
|
+
* },
|
|
196
|
+
* combined: { outputFile: 'CONTEXT.md' },
|
|
197
|
+
* });
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
interface EntityContextDefinition {
|
|
201
|
+
/**
|
|
202
|
+
* Derives the directory slug for this entity from its row.
|
|
203
|
+
* Used as the subdirectory name under {@link directoryRoot}.
|
|
204
|
+
*
|
|
205
|
+
* @example `(row) => row.slug as string`
|
|
206
|
+
*/
|
|
207
|
+
slug: (row: Row) => string;
|
|
208
|
+
/**
|
|
209
|
+
* Optional global index file written once per render cycle (not per entity).
|
|
210
|
+
* Lists all entities of this type.
|
|
211
|
+
*/
|
|
212
|
+
index?: {
|
|
213
|
+
/** Path relative to the `outputDir` passed to `render()` / `watch()` */
|
|
214
|
+
outputFile: string;
|
|
215
|
+
render: (rows: Row[]) => string;
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Files written inside each entity's directory.
|
|
219
|
+
* Keys are filenames (e.g. `'AGENT.md'`); values define the source and renderer.
|
|
220
|
+
* Files are written in iteration order.
|
|
221
|
+
*/
|
|
222
|
+
files: Record<string, EntityFileSpec>;
|
|
223
|
+
/**
|
|
224
|
+
* Optional combined context file inside each entity's directory.
|
|
225
|
+
* Lattice concatenates all per-entity files with `\n\n---\n\n` dividers.
|
|
226
|
+
* Files listed in `exclude` are omitted from the combined output.
|
|
227
|
+
*/
|
|
228
|
+
combined?: {
|
|
229
|
+
/** Filename for the combined file (e.g. `'CONTEXT.md'`) */
|
|
230
|
+
outputFile: string;
|
|
231
|
+
/** Filenames to exclude from the combined output */
|
|
232
|
+
exclude?: string[];
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Override the per-entity directory path relative to `outputDir`.
|
|
236
|
+
* Defaults to `'{directoryRoot}/{slug}'`.
|
|
237
|
+
*
|
|
238
|
+
* @example `(row) => \`custom-dir/${row.slug as string}/\``
|
|
239
|
+
*/
|
|
240
|
+
directory?: (row: Row) => string;
|
|
241
|
+
/**
|
|
242
|
+
* Top-level directory owned by this entity context.
|
|
243
|
+
* Used by `reconcile()` to scan for orphaned subdirectories.
|
|
244
|
+
* Defaults to the table name.
|
|
245
|
+
*/
|
|
246
|
+
directoryRoot?: string;
|
|
247
|
+
/**
|
|
248
|
+
* Files inside each entity's directory that Lattice must never delete
|
|
249
|
+
* during cleanup or reconciliation (e.g. agent-writable files like `SESSION.md`).
|
|
250
|
+
* Defaults to `[]`.
|
|
251
|
+
*/
|
|
252
|
+
protectedFiles?: string[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
interface CleanupOptions {
|
|
256
|
+
/** Remove entity directories whose slug is no longer in the DB. Default: true. */
|
|
257
|
+
removeOrphanedDirectories?: boolean;
|
|
258
|
+
/** Remove files inside entity dirs that are no longer declared. Default: true. */
|
|
259
|
+
removeOrphanedFiles?: boolean;
|
|
260
|
+
/** Additional globally protected files (merged with per-entity protectedFiles). */
|
|
261
|
+
protectedFiles?: string[];
|
|
262
|
+
/** Report orphans but do not delete anything. */
|
|
263
|
+
dryRun?: boolean;
|
|
264
|
+
/** Called for each orphan before removal (or instead of removal in dryRun mode). */
|
|
265
|
+
onOrphan?: (path: string, kind: 'directory' | 'file') => void;
|
|
266
|
+
}
|
|
267
|
+
interface CleanupResult {
|
|
268
|
+
directoriesRemoved: string[];
|
|
269
|
+
filesRemoved: string[];
|
|
270
|
+
/** Directories with user files that were left in place. */
|
|
271
|
+
directoriesSkipped: string[];
|
|
272
|
+
warnings: string[];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type Row = Record<string, unknown>;
|
|
276
|
+
interface LatticeOptions {
|
|
277
|
+
wal?: boolean;
|
|
278
|
+
busyTimeout?: number;
|
|
279
|
+
security?: SecurityOptions;
|
|
280
|
+
}
|
|
281
|
+
interface SecurityOptions {
|
|
282
|
+
sanitize?: boolean;
|
|
283
|
+
auditTables?: string[];
|
|
284
|
+
fieldLimits?: Record<string, number>;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The primary key of a table. Either a single column name (string) or an
|
|
288
|
+
* ordered list of column names for composite keys.
|
|
289
|
+
*
|
|
290
|
+
* Defaults to `'id'` when omitted from a TableDefinition. When the default
|
|
291
|
+
* `'id'` is used and the `id` field is absent on insert, a UUID v4 is
|
|
292
|
+
* generated automatically. For custom single or composite keys the caller
|
|
293
|
+
* must supply all PK column values.
|
|
294
|
+
*/
|
|
295
|
+
type PrimaryKey = string | string[];
|
|
296
|
+
/**
|
|
297
|
+
* A foreign-key relationship where THIS table holds the FK pointing to another
|
|
298
|
+
* table (e.g. `comment.post_id → posts.id`).
|
|
299
|
+
*/
|
|
300
|
+
interface BelongsToRelation {
|
|
301
|
+
type: 'belongsTo';
|
|
302
|
+
/** The related table */
|
|
303
|
+
table: string;
|
|
304
|
+
/** Column on THIS table that holds the foreign key */
|
|
305
|
+
foreignKey: string;
|
|
306
|
+
/**
|
|
307
|
+
* Column on the RELATED table being referenced.
|
|
308
|
+
* Defaults to that table's first primary key column.
|
|
309
|
+
*/
|
|
310
|
+
references?: string;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* A relationship where ANOTHER table holds the FK pointing back to this table
|
|
314
|
+
* (e.g. `posts.id ← comments.post_id`).
|
|
315
|
+
*/
|
|
316
|
+
interface HasManyRelation {
|
|
317
|
+
type: 'hasMany';
|
|
318
|
+
/** The related table */
|
|
319
|
+
table: string;
|
|
320
|
+
/** Column on the RELATED table that points back to this table */
|
|
321
|
+
foreignKey: string;
|
|
322
|
+
/**
|
|
323
|
+
* Column on THIS table being referenced.
|
|
324
|
+
* Defaults to this table's first primary key column.
|
|
325
|
+
*/
|
|
326
|
+
references?: string;
|
|
327
|
+
}
|
|
328
|
+
/** A declared relationship between two tables. */
|
|
329
|
+
type Relation = BelongsToRelation | HasManyRelation;
|
|
330
|
+
/**
|
|
331
|
+
* Comparison operators available in a {@link Filter}.
|
|
332
|
+
*
|
|
333
|
+
* - `eq` / `ne` — equality / inequality
|
|
334
|
+
* - `gt` / `gte` / `lt` / `lte` — numeric or lexicographic comparison
|
|
335
|
+
* - `like` — SQL LIKE pattern (`%` is the wildcard)
|
|
336
|
+
* - `in` — column value is one of a list
|
|
337
|
+
* - `isNull` / `isNotNull` — NULL checks (no `val` needed)
|
|
338
|
+
*/
|
|
339
|
+
type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'in' | 'isNull' | 'isNotNull';
|
|
340
|
+
/**
|
|
341
|
+
* A single filter clause with an explicit operator.
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```ts
|
|
345
|
+
* { col: 'score', op: 'gte', val: 80 }
|
|
346
|
+
* { col: 'deleted_at', op: 'isNull' }
|
|
347
|
+
* { col: 'status', op: 'in', val: ['open', 'pending'] }
|
|
348
|
+
* { col: 'name', op: 'like', val: 'A%' }
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
interface Filter {
|
|
352
|
+
/** Column name to filter on */
|
|
353
|
+
col: string;
|
|
354
|
+
/** Comparison operator */
|
|
355
|
+
op: FilterOp;
|
|
356
|
+
/**
|
|
357
|
+
* Operand value. Not required for `isNull` / `isNotNull`.
|
|
358
|
+
* For `in`, must be an array.
|
|
359
|
+
*/
|
|
360
|
+
val?: unknown;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Names of the four built-in render templates.
|
|
364
|
+
*
|
|
365
|
+
* - `default-list` — one bullet per row (supports `formatRow` hook)
|
|
366
|
+
* - `default-table` — GitHub-flavoured Markdown table
|
|
367
|
+
* - `default-detail` — section per row with all fields (supports `formatRow` hook)
|
|
368
|
+
* - `default-json` — `JSON.stringify(rows, null, 2)`
|
|
369
|
+
*/
|
|
370
|
+
type BuiltinTemplateName = 'default-list' | 'default-table' | 'default-detail' | 'default-json';
|
|
371
|
+
/**
|
|
372
|
+
* Lifecycle hooks that customise a built-in template.
|
|
373
|
+
*
|
|
374
|
+
* - `beforeRender(rows)` — transform or filter the row array before rendering.
|
|
375
|
+
* - `formatRow` — control how each row is serialised to a string.
|
|
376
|
+
* Can be a plain function or a `{{field}}` interpolation template string.
|
|
377
|
+
* Supported by `default-list` and `default-detail`.
|
|
378
|
+
* `belongsTo` relation fields are available as `{{relationName.field}}`.
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```ts
|
|
382
|
+
* hooks: {
|
|
383
|
+
* beforeRender: (rows) => rows.filter(r => r.active),
|
|
384
|
+
* formatRow: '{{title}} — {{status}}',
|
|
385
|
+
* }
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
interface RenderHooks {
|
|
389
|
+
beforeRender?: (rows: Row[]) => Row[];
|
|
390
|
+
formatRow?: ((row: Row) => string) | string;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Use a built-in template, optionally with lifecycle hooks.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* render: { template: 'default-list', hooks: { formatRow: '- {{title}} ({{status}})' } }
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
interface TemplateRenderSpec {
|
|
401
|
+
template: BuiltinTemplateName;
|
|
402
|
+
hooks?: RenderHooks;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The accepted value for `TableDefinition.render`:
|
|
406
|
+
*
|
|
407
|
+
* - A plain `(rows: Row[]) => string` function — full control, unchanged from v0.1/v0.2.
|
|
408
|
+
* - A `BuiltinTemplateName` string — use a built-in template with default settings.
|
|
409
|
+
* - A `TemplateRenderSpec` object — use a built-in template with lifecycle hooks.
|
|
410
|
+
*/
|
|
411
|
+
type RenderSpec = ((rows: Row[]) => string) | BuiltinTemplateName | TemplateRenderSpec;
|
|
412
|
+
interface TableDefinition {
|
|
413
|
+
/** Column name → SQLite type spec (e.g. `'TEXT PRIMARY KEY'`) */
|
|
414
|
+
columns: Record<string, string>;
|
|
415
|
+
/**
|
|
416
|
+
* How to render DB rows into text content for the context file.
|
|
417
|
+
*
|
|
418
|
+
* - Pass a `(rows: Row[]) => string` function for full control (v0.1/v0.2 behaviour).
|
|
419
|
+
* - Pass a `BuiltinTemplateName` string (`'default-list'`, `'default-table'`,
|
|
420
|
+
* `'default-detail'`, `'default-json'`) to use a built-in template.
|
|
421
|
+
* - Pass a `TemplateRenderSpec` to use a built-in template with lifecycle hooks.
|
|
422
|
+
*/
|
|
423
|
+
render: RenderSpec;
|
|
424
|
+
/** Output path relative to the outputDir passed to render/watch */
|
|
425
|
+
outputFile: string;
|
|
426
|
+
/** Optional pre-filter applied before render */
|
|
427
|
+
filter?: (rows: Row[]) => Row[];
|
|
428
|
+
/**
|
|
429
|
+
* Primary key column name or names.
|
|
430
|
+
*
|
|
431
|
+
* - Omit (or `'id'`): default behaviour — UUID auto-generated on insert when absent.
|
|
432
|
+
* - Custom string (e.g. `'slug'`): caller must supply the value on every insert.
|
|
433
|
+
* - Array (e.g. `['org_id', 'seq']`): composite key — caller must supply all columns.
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* ```ts
|
|
437
|
+
* primaryKey: 'slug'
|
|
438
|
+
* primaryKey: ['tenant_id', 'ticket_id']
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
primaryKey?: PrimaryKey;
|
|
442
|
+
/**
|
|
443
|
+
* Optional table-level SQL constraints appended after the column list.
|
|
444
|
+
* Required for composite primary keys and multi-column unique constraints,
|
|
445
|
+
* which cannot be expressed in the per-column `columns` spec.
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* ```ts
|
|
449
|
+
* tableConstraints: ['PRIMARY KEY (tenant_id, seq)']
|
|
450
|
+
* tableConstraints: ['PRIMARY KEY (a, b)', 'UNIQUE (email, org_id)']
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
tableConstraints?: string[];
|
|
454
|
+
/**
|
|
455
|
+
* Declared relationships to other registered tables.
|
|
456
|
+
* Stored as metadata in v0.2; used by template rendering in v0.3+.
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* relations: {
|
|
461
|
+
* author: { type: 'belongsTo', table: 'users', foreignKey: 'author_id' },
|
|
462
|
+
* comments: { type: 'hasMany', table: 'comments', foreignKey: 'post_id' },
|
|
463
|
+
* }
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
relations?: Record<string, Relation>;
|
|
467
|
+
}
|
|
468
|
+
interface MultiTableDefinition {
|
|
469
|
+
/** Returns the "anchor" entities — one output file is produced per anchor */
|
|
470
|
+
keys: () => Promise<Row[]>;
|
|
471
|
+
/** Derive the output file path from the anchor entity */
|
|
472
|
+
outputFile: (key: Row) => string;
|
|
473
|
+
/** Transform an anchor entity + related table data into text content */
|
|
474
|
+
render: (key: Row, tables: Record<string, Row[]>) => string;
|
|
475
|
+
/** Additional table names to query and pass into render */
|
|
476
|
+
tables?: string[];
|
|
477
|
+
}
|
|
478
|
+
interface WritebackDefinition {
|
|
479
|
+
/** Path or glob to agent-written files */
|
|
480
|
+
file: string;
|
|
481
|
+
/** Parse new file content starting at fromOffset; return entries and next offset */
|
|
482
|
+
parse: (content: string, fromOffset: number) => {
|
|
483
|
+
entries: unknown[];
|
|
484
|
+
nextOffset: number;
|
|
485
|
+
};
|
|
486
|
+
/** Persist a single parsed entry; called exactly once per unique dedupeKey */
|
|
487
|
+
persist: (entry: unknown, filePath: string) => Promise<void>;
|
|
488
|
+
/** Optional dedup key — if omitted, every entry is processed */
|
|
489
|
+
dedupeKey?: (entry: unknown) => string;
|
|
490
|
+
}
|
|
491
|
+
interface QueryOptions {
|
|
492
|
+
/**
|
|
493
|
+
* Equality filters — shorthand for `filters: [{ col, op: 'eq', val }]`.
|
|
494
|
+
* Fully backward compatible with pre-v0.2 usage.
|
|
495
|
+
*/
|
|
496
|
+
where?: Record<string, unknown>;
|
|
497
|
+
/**
|
|
498
|
+
* Advanced filter clauses with full operator support.
|
|
499
|
+
* Combined with `where` using AND.
|
|
500
|
+
*
|
|
501
|
+
* @example
|
|
502
|
+
* ```ts
|
|
503
|
+
* filters: [
|
|
504
|
+
* { col: 'priority', op: 'gte', val: 3 },
|
|
505
|
+
* { col: 'deleted_at', op: 'isNull' },
|
|
506
|
+
* { col: 'tag', op: 'in', val: ['bug', 'feature'] },
|
|
507
|
+
* ]
|
|
508
|
+
* ```
|
|
509
|
+
*/
|
|
510
|
+
filters?: Filter[];
|
|
511
|
+
orderBy?: string;
|
|
512
|
+
orderDir?: 'asc' | 'desc';
|
|
513
|
+
limit?: number;
|
|
514
|
+
offset?: number;
|
|
515
|
+
}
|
|
516
|
+
interface CountOptions {
|
|
517
|
+
/** Equality filters (same as QueryOptions.where) */
|
|
518
|
+
where?: Record<string, unknown>;
|
|
519
|
+
/** Advanced filter clauses (same as QueryOptions.filters) */
|
|
520
|
+
filters?: Filter[];
|
|
521
|
+
}
|
|
522
|
+
interface InitOptions {
|
|
523
|
+
migrations?: Migration[];
|
|
524
|
+
}
|
|
525
|
+
interface Migration {
|
|
526
|
+
version: number;
|
|
527
|
+
sql: string;
|
|
528
|
+
}
|
|
529
|
+
interface WatchOptions {
|
|
530
|
+
/** Poll interval in milliseconds (default: 5000) */
|
|
531
|
+
interval?: number;
|
|
532
|
+
onRender?: (result: RenderResult) => void;
|
|
533
|
+
onError?: (err: Error) => void;
|
|
534
|
+
/**
|
|
535
|
+
* If set, runs orphan cleanup after each render cycle using the previous manifest.
|
|
536
|
+
* Safe to enable in long-running daemons — never removes protectedFiles.
|
|
537
|
+
*/
|
|
538
|
+
cleanup?: CleanupOptions;
|
|
539
|
+
/** Called after each cleanup cycle (only when cleanup option is set). */
|
|
540
|
+
onCleanup?: (result: CleanupResult) => void;
|
|
541
|
+
}
|
|
542
|
+
interface RenderResult {
|
|
543
|
+
filesWritten: string[];
|
|
544
|
+
filesSkipped: number;
|
|
545
|
+
durationMs: number;
|
|
546
|
+
}
|
|
547
|
+
interface SyncResult extends RenderResult {
|
|
548
|
+
writebackProcessed: number;
|
|
549
|
+
}
|
|
550
|
+
type StopFn = () => void;
|
|
551
|
+
interface AuditEvent {
|
|
552
|
+
table: string;
|
|
553
|
+
operation: 'insert' | 'update' | 'delete';
|
|
554
|
+
id: string;
|
|
555
|
+
timestamp: string;
|
|
556
|
+
}
|
|
557
|
+
interface ReconcileOptions {
|
|
558
|
+
/** Remove entity directories whose slug is no longer in the DB. Default: true. */
|
|
559
|
+
removeOrphanedDirectories?: boolean;
|
|
560
|
+
/** Remove files inside entity dirs that are no longer declared. Default: true. */
|
|
561
|
+
removeOrphanedFiles?: boolean;
|
|
562
|
+
/** Additional globally protected files. */
|
|
563
|
+
protectedFiles?: string[];
|
|
564
|
+
/** Report orphans but do not delete anything. */
|
|
565
|
+
dryRun?: boolean;
|
|
566
|
+
/** Called for each orphan before removal. */
|
|
567
|
+
onOrphan?: (path: string, kind: 'directory' | 'file') => void;
|
|
568
|
+
}
|
|
569
|
+
interface ReconcileResult extends RenderResult {
|
|
570
|
+
cleanup: CleanupResult;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Initialise Lattice from a YAML config file instead of an explicit path.
|
|
575
|
+
*
|
|
576
|
+
* @example
|
|
577
|
+
* ```ts
|
|
578
|
+
* const db = new Lattice({ config: './lattice.config.yml' });
|
|
579
|
+
* await db.init();
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
interface LatticeConfigInput {
|
|
583
|
+
/** Path to `lattice.config.yml` (absolute or relative to `process.cwd()`) */
|
|
584
|
+
config: string;
|
|
585
|
+
/** Optional Lattice runtime options */
|
|
586
|
+
options?: LatticeOptions;
|
|
587
|
+
}
|
|
588
|
+
type EventHandler<T> = (data: T) => void;
|
|
589
|
+
/**
|
|
590
|
+
* A primary key lookup value.
|
|
591
|
+
* - `string` — the value of the table's single PK column (backward compatible).
|
|
592
|
+
* - `Record<string, unknown>` — column → value map for composite PKs.
|
|
593
|
+
*/
|
|
594
|
+
type PkLookup = string | Record<string, unknown>;
|
|
595
|
+
declare class Lattice {
|
|
596
|
+
private readonly _adapter;
|
|
597
|
+
private readonly _schema;
|
|
598
|
+
private readonly _sanitizer;
|
|
599
|
+
private readonly _render;
|
|
600
|
+
private readonly _loop;
|
|
601
|
+
private readonly _writeback;
|
|
602
|
+
private _initialized;
|
|
603
|
+
/** Cache of actual table columns (from PRAGMA), populated after init(). */
|
|
604
|
+
private readonly _columnCache;
|
|
605
|
+
private readonly _auditHandlers;
|
|
606
|
+
private readonly _renderHandlers;
|
|
607
|
+
private readonly _writebackHandlers;
|
|
608
|
+
private readonly _errorHandlers;
|
|
609
|
+
constructor(pathOrConfig: string | LatticeConfigInput, options?: LatticeOptions);
|
|
610
|
+
define(table: string, def: TableDefinition): this;
|
|
611
|
+
defineMulti(name: string, def: MultiTableDefinition): this;
|
|
612
|
+
defineEntityContext(table: string, def: EntityContextDefinition): this;
|
|
613
|
+
defineWriteback(def: WritebackDefinition): this;
|
|
614
|
+
init(options?: InitOptions): Promise<void>;
|
|
615
|
+
close(): void;
|
|
616
|
+
insert(table: string, row: Row): Promise<string>;
|
|
617
|
+
upsert(table: string, row: Row): Promise<string>;
|
|
618
|
+
upsertBy(table: string, col: string, val: unknown, row: Row): Promise<string>;
|
|
619
|
+
update(table: string, id: PkLookup, row: Partial<Row>): Promise<void>;
|
|
620
|
+
delete(table: string, id: PkLookup): Promise<void>;
|
|
621
|
+
get(table: string, id: PkLookup): Promise<Row | null>;
|
|
622
|
+
query(table: string, opts?: QueryOptions): Promise<Row[]>;
|
|
623
|
+
count(table: string, opts?: CountOptions): Promise<number>;
|
|
624
|
+
render(outputDir: string): Promise<RenderResult>;
|
|
625
|
+
sync(outputDir: string): Promise<SyncResult>;
|
|
626
|
+
reconcile(outputDir: string, options?: ReconcileOptions): Promise<ReconcileResult>;
|
|
627
|
+
watch(outputDir: string, opts?: WatchOptions): Promise<StopFn>;
|
|
628
|
+
on(event: 'audit', handler: EventHandler<AuditEvent>): this;
|
|
629
|
+
on(event: 'render', handler: EventHandler<RenderResult>): this;
|
|
630
|
+
on(event: 'writeback', handler: EventHandler<{
|
|
631
|
+
filePath: string;
|
|
632
|
+
entriesProcessed: number;
|
|
633
|
+
}>): this;
|
|
634
|
+
on(event: 'error', handler: EventHandler<Error>): this;
|
|
635
|
+
get db(): Database.Database;
|
|
636
|
+
/**
|
|
637
|
+
* Filter a sanitized row to only include columns that actually exist in the
|
|
638
|
+
* table (verified via PRAGMA after init). Unregistered tables (accessed
|
|
639
|
+
* through the raw `.db` handle) are passed through unchanged.
|
|
640
|
+
*
|
|
641
|
+
* This is a defence-in-depth guard: column names from caller-supplied `row`
|
|
642
|
+
* objects are interpolated into SQL, so stripping unknown keys eliminates
|
|
643
|
+
* any theoretical injection vector from crafted object keys.
|
|
644
|
+
*/
|
|
645
|
+
private _filterToSchemaColumns;
|
|
646
|
+
/**
|
|
647
|
+
* Build the WHERE clause and params for a PK lookup.
|
|
648
|
+
* - `string` → matches against the table's first PK column.
|
|
649
|
+
* - `Record` → matches every PK column; all must be present in the object.
|
|
650
|
+
*/
|
|
651
|
+
private _pkWhere;
|
|
652
|
+
/**
|
|
653
|
+
* Convert Filter objects into SQL clause strings and bound params.
|
|
654
|
+
* An `in` filter with an empty array is silently ignored (produces no clause).
|
|
655
|
+
*/
|
|
656
|
+
private _buildFilters;
|
|
657
|
+
/** Returns a rejected Promise if not initialized; null if ready. */
|
|
658
|
+
private _notInitError;
|
|
659
|
+
/**
|
|
660
|
+
* Returns a rejected Promise if any of the given column names are not present
|
|
661
|
+
* in the table's schema; null if all columns are valid.
|
|
662
|
+
*
|
|
663
|
+
* Applied on the read path (query/count) to validate WHERE and filter column
|
|
664
|
+
* names before they are interpolated into SQL. The write path strips unknown
|
|
665
|
+
* columns via _filterToSchemaColumns; the read path rejects instead to avoid
|
|
666
|
+
* silently discarding intended filter conditions.
|
|
667
|
+
*
|
|
668
|
+
* Unregistered tables (accessed via the raw `.db` handle) are passed through.
|
|
669
|
+
*/
|
|
670
|
+
private _invalidColumnError;
|
|
671
|
+
private _assertNotInit;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Scalar types recognised in `lattice.config.yml` field definitions.
|
|
676
|
+
*
|
|
677
|
+
* | YAML type | SQLite type | TypeScript type |
|
|
678
|
+
* | ----------- | ----------- | --------------- |
|
|
679
|
+
* | `uuid` | TEXT | string |
|
|
680
|
+
* | `text` | TEXT | string |
|
|
681
|
+
* | `integer` | INTEGER | number |
|
|
682
|
+
* | `int` | INTEGER | number |
|
|
683
|
+
* | `real` | REAL | number |
|
|
684
|
+
* | `float` | REAL | number |
|
|
685
|
+
* | `boolean` | INTEGER | boolean |
|
|
686
|
+
* | `bool` | INTEGER | boolean |
|
|
687
|
+
* | `datetime` | TEXT | string |
|
|
688
|
+
* | `date` | TEXT | string |
|
|
689
|
+
* | `blob` | BLOB | Buffer |
|
|
690
|
+
*/
|
|
691
|
+
type LatticeFieldType = 'uuid' | 'text' | 'integer' | 'int' | 'real' | 'float' | 'boolean' | 'bool' | 'datetime' | 'date' | 'blob';
|
|
692
|
+
/**
|
|
693
|
+
* A single field (column) definition in a `lattice.config.yml` entity.
|
|
694
|
+
*
|
|
695
|
+
* @example
|
|
696
|
+
* ```yaml
|
|
697
|
+
* id: { type: uuid, primaryKey: true }
|
|
698
|
+
* title: { type: text, required: true }
|
|
699
|
+
* status: { type: text, default: open }
|
|
700
|
+
* assignee_id: { type: uuid, ref: user }
|
|
701
|
+
* score: { type: integer, default: 0 }
|
|
702
|
+
* ```
|
|
703
|
+
*/
|
|
704
|
+
interface LatticeFieldDef {
|
|
705
|
+
/** Column data type */
|
|
706
|
+
type: LatticeFieldType;
|
|
707
|
+
/** Mark this column as the table's primary key */
|
|
708
|
+
primaryKey?: boolean;
|
|
709
|
+
/** Column is NOT NULL */
|
|
710
|
+
required?: boolean;
|
|
711
|
+
/** SQL DEFAULT value */
|
|
712
|
+
default?: string | number | boolean;
|
|
713
|
+
/**
|
|
714
|
+
* Foreign-key reference to another entity (table name).
|
|
715
|
+
* Creates a `belongsTo` relation automatically.
|
|
716
|
+
* The relation name is derived from the field name — `_id` suffix is stripped
|
|
717
|
+
* (e.g. `assignee_id: { ref: user }` → relation name `assignee`).
|
|
718
|
+
*/
|
|
719
|
+
ref?: string;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Inline render spec inside YAML — a flat object alternative to `TemplateRenderSpec`.
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* ```yaml
|
|
726
|
+
* render:
|
|
727
|
+
* template: default-list
|
|
728
|
+
* formatRow: "{{title}} — {{status}}"
|
|
729
|
+
* ```
|
|
730
|
+
*/
|
|
731
|
+
interface LatticeEntityRenderSpec {
|
|
732
|
+
template: string;
|
|
733
|
+
formatRow?: string;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* A single entity (table) definition in `lattice.config.yml`.
|
|
737
|
+
*
|
|
738
|
+
* @example
|
|
739
|
+
* ```yaml
|
|
740
|
+
* ticket:
|
|
741
|
+
* fields:
|
|
742
|
+
* id: { type: uuid, primaryKey: true }
|
|
743
|
+
* title: { type: text, required: true }
|
|
744
|
+
* render: default-list
|
|
745
|
+
* outputFile: context/TICKETS.md
|
|
746
|
+
* ```
|
|
747
|
+
*/
|
|
748
|
+
interface LatticeEntityDef {
|
|
749
|
+
/** Column definitions */
|
|
750
|
+
fields: Record<string, LatticeFieldDef>;
|
|
751
|
+
/**
|
|
752
|
+
* How to render rows into context text.
|
|
753
|
+
* Accepts the same forms as `TableDefinition.render`:
|
|
754
|
+
* - A `BuiltinTemplateName` string (e.g. `default-list`)
|
|
755
|
+
* - A `{ template, formatRow }` object for hooks
|
|
756
|
+
*/
|
|
757
|
+
render?: string | LatticeEntityRenderSpec;
|
|
758
|
+
/** Render output file path (relative to the config file directory) */
|
|
759
|
+
outputFile: string;
|
|
760
|
+
/**
|
|
761
|
+
* Optional explicit primary key override.
|
|
762
|
+
* If omitted, the field with `primaryKey: true` is used.
|
|
763
|
+
* Accepts a single column name or an array for composite keys.
|
|
764
|
+
*/
|
|
765
|
+
primaryKey?: string | string[];
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* The top-level `lattice.config.yml` document.
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* ```yaml
|
|
772
|
+
* db: ./data/app.db
|
|
773
|
+
* entities:
|
|
774
|
+
* ticket:
|
|
775
|
+
* fields:
|
|
776
|
+
* id: { type: uuid, primaryKey: true }
|
|
777
|
+
* title: { type: text, required: true }
|
|
778
|
+
* render: default-list
|
|
779
|
+
* outputFile: context/TICKETS.md
|
|
780
|
+
* ```
|
|
781
|
+
*/
|
|
782
|
+
interface LatticeConfig {
|
|
783
|
+
/** Path to the SQLite database file (relative to the config file) */
|
|
784
|
+
db: string;
|
|
785
|
+
/** Entity (table) definitions */
|
|
786
|
+
entities: Record<string, LatticeEntityDef>;
|
|
787
|
+
/** Entity context directory definitions */
|
|
788
|
+
entityContexts?: Record<string, LatticeEntityContextDef>;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Source spec in YAML config — either the shorthand string 'self' or an object.
|
|
792
|
+
*/
|
|
793
|
+
type LatticeEntityContextSourceDef = 'self' | {
|
|
794
|
+
type: 'hasMany';
|
|
795
|
+
table: string;
|
|
796
|
+
foreignKey: string;
|
|
797
|
+
references?: string;
|
|
798
|
+
} | {
|
|
799
|
+
type: 'manyToMany';
|
|
800
|
+
junctionTable: string;
|
|
801
|
+
localKey: string;
|
|
802
|
+
remoteKey: string;
|
|
803
|
+
remoteTable: string;
|
|
804
|
+
references?: string;
|
|
805
|
+
} | {
|
|
806
|
+
type: 'belongsTo';
|
|
807
|
+
table: string;
|
|
808
|
+
foreignKey: string;
|
|
809
|
+
references?: string;
|
|
810
|
+
};
|
|
811
|
+
/** A single per-entity file spec in YAML config */
|
|
812
|
+
interface LatticeEntityContextFileDef {
|
|
813
|
+
source: LatticeEntityContextSourceDef;
|
|
814
|
+
template: string;
|
|
815
|
+
budget?: number;
|
|
816
|
+
omitIfEmpty?: boolean;
|
|
817
|
+
}
|
|
818
|
+
/** Entity context definition in YAML config */
|
|
819
|
+
interface LatticeEntityContextDef {
|
|
820
|
+
slug: string;
|
|
821
|
+
directoryRoot?: string;
|
|
822
|
+
protectedFiles?: string[];
|
|
823
|
+
index?: {
|
|
824
|
+
outputFile: string;
|
|
825
|
+
render: string;
|
|
826
|
+
};
|
|
827
|
+
files: Record<string, LatticeEntityContextFileDef>;
|
|
828
|
+
combined?: {
|
|
829
|
+
outputFile: string;
|
|
830
|
+
exclude?: string[];
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Output of a successful config parse — ready to hand to Lattice. */
|
|
835
|
+
interface ParsedConfig {
|
|
836
|
+
/** Absolute path to the SQLite database file */
|
|
837
|
+
dbPath: string;
|
|
838
|
+
/** Table definitions in declaration order */
|
|
839
|
+
tables: readonly {
|
|
840
|
+
name: string;
|
|
841
|
+
definition: TableDefinition;
|
|
842
|
+
}[];
|
|
843
|
+
/** Entity context definitions in declaration order */
|
|
844
|
+
entityContexts: readonly {
|
|
845
|
+
table: string;
|
|
846
|
+
definition: EntityContextDefinition;
|
|
847
|
+
}[];
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Read, parse, and validate a `lattice.config.yml` file.
|
|
851
|
+
*
|
|
852
|
+
* Paths inside the config (e.g. `db`, `outputFile`) are resolved relative to
|
|
853
|
+
* the config file's directory.
|
|
854
|
+
*
|
|
855
|
+
* @throws If the file cannot be read, the YAML is malformed, or required
|
|
856
|
+
* keys are missing.
|
|
857
|
+
*/
|
|
858
|
+
declare function parseConfigFile(configPath: string): ParsedConfig;
|
|
859
|
+
/**
|
|
860
|
+
* Parse and validate a raw YAML string as a Lattice config.
|
|
861
|
+
*
|
|
862
|
+
* `configDir` is used to resolve relative `db` and `outputFile` paths.
|
|
863
|
+
* Typically this should be the directory that contains `lattice.config.yml`.
|
|
864
|
+
*
|
|
865
|
+
* Useful for testing without touching the filesystem.
|
|
866
|
+
*/
|
|
867
|
+
declare function parseConfigString(yamlContent: string, configDir: string): ParsedConfig;
|
|
868
|
+
|
|
869
|
+
export { 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 ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, manifestPath, parseConfigFile, parseConfigString, readManifest, writeManifest };
|