@omgbase/core 0.21.0 → 0.23.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.
Files changed (36) hide show
  1. package/dist/src/core/ids.d.ts +1 -1
  2. package/dist/src/core/read/document.d.ts +4 -1
  3. package/dist/src/core/read/document.js +18 -6
  4. package/dist/src/core/store/archive.d.ts +196 -0
  5. package/dist/src/core/store/archive.js +606 -0
  6. package/dist/src/core/store/gc.d.ts +25 -4
  7. package/dist/src/core/store/gc.js +43 -28
  8. package/dist/src/core/store/rebuild.d.ts +2 -0
  9. package/dist/src/core/store/rebuild.js +8 -2
  10. package/dist/src/core/store/retention.d.ts +138 -0
  11. package/dist/src/core/store/retention.js +409 -0
  12. package/dist/src/core/store/schema.d.ts +3 -2
  13. package/dist/src/core/store/schema.js +30 -4
  14. package/dist/src/core/store/size.d.ts +21 -2
  15. package/dist/src/core/store/size.js +24 -3
  16. package/dist/src/core/store/store.d.ts +1 -0
  17. package/dist/src/core/store/store.js +53 -1
  18. package/dist/src/graph/history.d.ts +2 -0
  19. package/dist/src/graph/history.js +8 -4
  20. package/dist/src/index.d.ts +3 -1
  21. package/dist/src/index.js +3 -0
  22. package/dist/src/mcp/errors.d.ts +1 -1
  23. package/dist/src/mcp/errors.js +4 -0
  24. package/dist/src/mcp/reference.d.ts +1 -1
  25. package/dist/src/mcp/reference.js +24 -6
  26. package/dist/src/mcp/server.js +2 -2
  27. package/dist/src/oqx-js/context.js +21 -5
  28. package/dist/src/oqx-js/dates.d.ts +105 -0
  29. package/dist/src/oqx-js/dates.js +427 -0
  30. package/dist/src/oqx-js/names.js +14 -3
  31. package/dist/src/oqx-js/planner.js +17 -2
  32. package/dist/src/oqx-js/run.d.ts +1 -0
  33. package/dist/src/oqx-js/run.js +66 -1
  34. package/dist/src/spec-versions.d.ts +4 -4
  35. package/dist/src/spec-versions.js +3 -3
  36. package/package.json +2 -2
@@ -0,0 +1,409 @@
1
+ import { referencePath } from "../paths.js";
2
+ export const LADDER_KEEPS = ["one per day", "one per week", "one per month", "one per year"];
3
+ /** The defaults (spec/store §7): a month hot, then one a day for a year, then one a month; 200 per document; api commits with a reason. */
4
+ export const DEFAULT_RETENTION = {
5
+ keep_days: 30,
6
+ ladder: [
7
+ { after_days: 30, keep: "one per day" },
8
+ { after_days: 365, keep: "one per month" },
9
+ ],
10
+ per_doc_max: 200,
11
+ keep_origins: ["api"],
12
+ overrides: [],
13
+ };
14
+ /** The settings key the policy lives under (`history.retention`, deep-merged over the defaults). */
15
+ export const RETENTION_SETTINGS_KEY = "history";
16
+ /** The repo-settings key naming the last segment this repo recorded (`history.last_segment`). */
17
+ export const LAST_SEGMENT_SETTINGS_KEY = "last_segment";
18
+ const DAY_MS = 86_400_000;
19
+ export class RetentionPolicyError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "RetentionPolicyError";
23
+ }
24
+ }
25
+ function isRecord(v) {
26
+ return typeof v === "object" && v !== null && !Array.isArray(v);
27
+ }
28
+ function nonNegativeInt(v, at) {
29
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 0)
30
+ throw new RetentionPolicyError(`${at} must be a non-negative integer`);
31
+ return v;
32
+ }
33
+ function parseLadder(v, at) {
34
+ if (!Array.isArray(v))
35
+ throw new RetentionPolicyError(`${at} must be an array of { after_days, keep }`);
36
+ const rungs = v.map((r, i) => {
37
+ if (!isRecord(r))
38
+ throw new RetentionPolicyError(`${at}[${i}] must be an object`);
39
+ const extra = Object.keys(r).filter((k) => k !== "after_days" && k !== "keep");
40
+ if (extra.length > 0)
41
+ throw new RetentionPolicyError(`${at}[${i}]: unknown key ${extra[0]}`);
42
+ if (!LADDER_KEEPS.includes(r.keep))
43
+ throw new RetentionPolicyError(`${at}[${i}].keep must be one of ${LADDER_KEEPS.map((k) => JSON.stringify(k)).join(", ")}`);
44
+ return { after_days: nonNegativeInt(r.after_days, `${at}[${i}].after_days`), keep: r.keep };
45
+ });
46
+ // Sorted by after_days; two rungs at the same age would be ambiguous.
47
+ rungs.sort((a, b) => a.after_days - b.after_days);
48
+ for (let i = 1; i < rungs.length; i++)
49
+ if (rungs[i].after_days === rungs[i - 1].after_days)
50
+ throw new RetentionPolicyError(`${at}: two rungs share after_days ${rungs[i].after_days}`);
51
+ return rungs;
52
+ }
53
+ function parseOrigins(v, at) {
54
+ if (!Array.isArray(v) || v.some((o) => typeof o !== "string"))
55
+ throw new RetentionPolicyError(`${at} must be an array of strings`);
56
+ return [...v];
57
+ }
58
+ const POLICY_KEYS = new Set(["keep_days", "ladder", "per_doc_max", "keep_origins", "overrides"]);
59
+ const OVERRIDE_KEYS = new Set(["glob", "keep_days", "ladder", "per_doc_max", "keep_origins"]);
60
+ /**
61
+ * A partial policy (a settings blob, a `--policy` argument) over `base`: every
62
+ * key given replaces the base's; a malformed value is `RetentionPolicyError`.
63
+ */
64
+ export function mergeRetentionPolicy(base, partial, at = "history.retention") {
65
+ if (partial === undefined || partial === null)
66
+ return { ...base, ladder: [...base.ladder], keep_origins: [...base.keep_origins], overrides: [...base.overrides] };
67
+ if (!isRecord(partial))
68
+ throw new RetentionPolicyError(`${at} must be an object`);
69
+ for (const k of Object.keys(partial))
70
+ if (!POLICY_KEYS.has(k))
71
+ throw new RetentionPolicyError(`${at}: unknown key ${k}`);
72
+ const out = { ...base, ladder: [...base.ladder], keep_origins: [...base.keep_origins], overrides: [...base.overrides] };
73
+ if ("keep_days" in partial)
74
+ out.keep_days = nonNegativeInt(partial.keep_days, `${at}.keep_days`);
75
+ if ("per_doc_max" in partial)
76
+ out.per_doc_max = nonNegativeInt(partial.per_doc_max, `${at}.per_doc_max`);
77
+ if ("ladder" in partial)
78
+ out.ladder = parseLadder(partial.ladder, `${at}.ladder`);
79
+ if ("keep_origins" in partial)
80
+ out.keep_origins = parseOrigins(partial.keep_origins, `${at}.keep_origins`);
81
+ if ("overrides" in partial) {
82
+ if (!Array.isArray(partial.overrides))
83
+ throw new RetentionPolicyError(`${at}.overrides must be an array`);
84
+ out.overrides = partial.overrides.map((o, i) => {
85
+ const here = `${at}.overrides[${i}]`;
86
+ if (!isRecord(o))
87
+ throw new RetentionPolicyError(`${here} must be an object`);
88
+ for (const k of Object.keys(o))
89
+ if (!OVERRIDE_KEYS.has(k))
90
+ throw new RetentionPolicyError(`${here}: unknown key ${k}`);
91
+ if (typeof o.glob !== "string" || o.glob === "")
92
+ throw new RetentionPolicyError(`${here}.glob must be a non-empty string`);
93
+ const r = { glob: o.glob };
94
+ if ("keep_days" in o)
95
+ r.keep_days = nonNegativeInt(o.keep_days, `${here}.keep_days`);
96
+ if ("per_doc_max" in o)
97
+ r.per_doc_max = nonNegativeInt(o.per_doc_max, `${here}.per_doc_max`);
98
+ if ("ladder" in o)
99
+ r.ladder = parseLadder(o.ladder, `${here}.ladder`);
100
+ if ("keep_origins" in o)
101
+ r.keep_origins = parseOrigins(o.keep_origins, `${here}.keep_origins`);
102
+ return r;
103
+ });
104
+ }
105
+ return out;
106
+ }
107
+ /** A settings blob as stored (`workspace_settings.settings`, `repos.settings`): an object, or `{}` for anything else. */
108
+ export function parseSettingsJson(json) {
109
+ if (!json)
110
+ return {};
111
+ try {
112
+ const v = JSON.parse(json);
113
+ return isRecord(v) ? v : {};
114
+ }
115
+ catch {
116
+ return {};
117
+ }
118
+ }
119
+ /**
120
+ * The repo's effective settings (`spec/sync` settings: the workspace layer
121
+ * with the repo's own deep-merged over it at the leaf). Inlined here rather
122
+ * than imported from `sync/settings.ts` — `core/` does not import its
123
+ * siblings — and kept to the rule that module states.
124
+ */
125
+ export function effectiveSettings(store, repoId) {
126
+ const ws = parseSettingsJson(store.db.prepare("SELECT settings FROM workspace_settings WHERE id = 0").get()?.settings);
127
+ const own = parseSettingsJson(store.db.prepare("SELECT settings FROM repos WHERE repo_id = ?").get(repoId)?.settings);
128
+ const merge = (base, over) => {
129
+ const out = { ...base };
130
+ for (const [k, v] of Object.entries(over)) {
131
+ const b = out[k];
132
+ out[k] = isRecord(b) && isRecord(v) ? merge(b, v) : v;
133
+ }
134
+ return out;
135
+ };
136
+ return merge(ws, own);
137
+ }
138
+ /** The repo's effective policy: the defaults, the settings' `history.retention` over them, then `override` (a CLI `--policy`). */
139
+ export function retentionPolicy(store, repoId, override) {
140
+ const settings = effectiveSettings(store, repoId);
141
+ const history = settings[RETENTION_SETTINGS_KEY];
142
+ const configured = isRecord(history) ? history.retention : undefined;
143
+ const base = mergeRetentionPolicy(DEFAULT_RETENTION, configured);
144
+ return override === undefined ? base : mergeRetentionPolicy(base, override, "policy");
145
+ }
146
+ /**
147
+ * An override glob against a reference path: `**` any run of characters,
148
+ * `*` any run without `/`, `?` one character, everything else literal; a glob
149
+ * without a leading `/` is rooted (`projects/**` ≡ `/projects/**`).
150
+ */
151
+ export function globToRegExp(glob) {
152
+ const g = glob.startsWith("/") ? glob : `/${glob}`;
153
+ let re = "^";
154
+ for (let i = 0; i < g.length; i++) {
155
+ const c = g[i];
156
+ if (c === "*") {
157
+ if (g[i + 1] === "*") {
158
+ re += ".*";
159
+ i++;
160
+ }
161
+ else
162
+ re += "[^/]*";
163
+ }
164
+ else if (c === "?")
165
+ re += "[^/]";
166
+ else
167
+ re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
168
+ }
169
+ return new RegExp(re + "$");
170
+ }
171
+ /** The policy for one document: the first matching override's keys over the base. */
172
+ export function policyFor(base, referencePathOfDoc) {
173
+ for (const o of base.overrides) {
174
+ if (!globToRegExp(o.glob).test(referencePathOfDoc))
175
+ continue;
176
+ return {
177
+ keep_days: o.keep_days ?? base.keep_days,
178
+ per_doc_max: o.per_doc_max ?? base.per_doc_max,
179
+ ladder: o.ladder ?? base.ladder,
180
+ keep_origins: o.keep_origins ?? base.keep_origins,
181
+ overrides: [],
182
+ };
183
+ }
184
+ return base;
185
+ }
186
+ /** Whole days between a commit's `ts` and `now` (negative when the commit is in the future). */
187
+ export function ageDays(ts, nowMs) {
188
+ return Math.floor((nowMs - Date.parse(ts)) / DAY_MS);
189
+ }
190
+ /** The period key of a timestamp under a rung's grain (UTC; a week starts on Monday). */
191
+ export function periodKey(ts, keep) {
192
+ const d = new Date(ts);
193
+ const y = d.getUTCFullYear();
194
+ const m = String(d.getUTCMonth() + 1).padStart(2, "0");
195
+ const day = String(d.getUTCDate()).padStart(2, "0");
196
+ switch (keep) {
197
+ case "one per day": return `${y}-${m}-${day}`;
198
+ case "one per month": return `${y}-${m}`;
199
+ case "one per year": return `${y}`;
200
+ case "one per week": {
201
+ // The Monday of the ISO week, as a date.
202
+ const monday = new Date(Date.UTC(y, d.getUTCMonth(), d.getUTCDate()));
203
+ const dow = (monday.getUTCDay() + 6) % 7; // Monday = 0
204
+ monday.setUTCDate(monday.getUTCDate() - dow);
205
+ return `${monday.getUTCFullYear()}-${String(monday.getUTCMonth() + 1).padStart(2, "0")}-${String(monday.getUTCDate()).padStart(2, "0")}`;
206
+ }
207
+ }
208
+ }
209
+ /**
210
+ * The sizes of the blobs and tree nodes of a store, and the walk that marks
211
+ * what a set of revisions reaches (spec/store §7: the GC mark, run twice — once
212
+ * from what stays, once from what goes — to cost a plan).
213
+ */
214
+ export class ReachMarker {
215
+ store;
216
+ treeNode;
217
+ blobSize;
218
+ treeSizes = new Map();
219
+ blobSizes = new Map();
220
+ constructor(store) {
221
+ this.store = store;
222
+ this.treeNode = store.db.prepare("SELECT entries FROM tree_nodes WHERE hash = ?");
223
+ this.blobSize = store.db.prepare("SELECT size FROM blobs WHERE hash = ?");
224
+ }
225
+ /** Mark every tree node and blob reachable from the given roots and frontmatter blobs into `trees`/`blobs` (hex). */
226
+ mark(revs, trees, blobs) {
227
+ const walk = (hashHex) => {
228
+ if (trees.has(hashHex))
229
+ return;
230
+ trees.add(hashHex);
231
+ const node = this.treeNode.get(Buffer.from(hashHex, "hex"));
232
+ if (!node)
233
+ return;
234
+ this.treeSizes.set(hashHex, Buffer.byteLength(node.entries));
235
+ for (const [, rawHashHex, childHashHex, , , triviaHashHex] of JSON.parse(node.entries)) {
236
+ blobs.add(rawHashHex);
237
+ if (triviaHashHex)
238
+ blobs.add(triviaHashHex);
239
+ if (childHashHex)
240
+ walk(childHashHex);
241
+ }
242
+ };
243
+ for (const r of revs) {
244
+ if (r.root_tree)
245
+ walk(r.root_tree.toString("hex"));
246
+ if (r.frontmatter_blob)
247
+ blobs.add(r.frontmatter_blob.toString("hex"));
248
+ }
249
+ }
250
+ /** Bytes of the given tree nodes (their `entries`) and blobs (`size`). */
251
+ bytesOf(trees, blobs) {
252
+ let n = 0;
253
+ for (const t of trees) {
254
+ let size = this.treeSizes.get(t);
255
+ if (size === undefined) {
256
+ const node = this.treeNode.get(Buffer.from(t, "hex"));
257
+ size = node ? Buffer.byteLength(node.entries) : 0;
258
+ this.treeSizes.set(t, size);
259
+ }
260
+ n += size;
261
+ }
262
+ for (const b of blobs) {
263
+ let size = this.blobSizes.get(b);
264
+ if (size === undefined) {
265
+ size = this.blobSize.get(Buffer.from(b, "hex"))?.size ?? 0;
266
+ this.blobSizes.set(b, size);
267
+ }
268
+ n += size;
269
+ }
270
+ return n;
271
+ }
272
+ }
273
+ /**
274
+ * The plan (spec/store §7 "Retention", the policy as an exact rule). Per
275
+ * document, over its live revisions oldest → newest: a revision is KEPT when
276
+ * any rule keeps it — it is the document's current revision (a tombstoned
277
+ * document's last revision is its current one), a `pending_materializations`
278
+ * row names it, it is younger than `keep_days`, it is among the newest
279
+ * `per_doc_max`, its commit's origin is in `keep_origins` with a non-empty
280
+ * reason, it is not before `before`, no ladder rung applies to its age (the
281
+ * gap below the first rung keeps everything), or it is the newest revision of
282
+ * its rung's period; every other revision is ARCHIVED. Bytes are the GC mark
283
+ * from the archived revisions minus the mark from every revision that stays
284
+ * live in the store (all repositories — blobs and tree nodes are shared).
285
+ */
286
+ export function evaluateRetention(store, repoId, opts = {}) {
287
+ const now = opts.now ?? new Date().toISOString();
288
+ const nowMs = Date.parse(now);
289
+ const before = opts.before ?? null;
290
+ const beforeMs = before === null ? null : Date.parse(before);
291
+ const base = opts.policy ?? retentionPolicy(store, repoId);
292
+ const db = store.db;
293
+ const docs = db
294
+ .prepare("SELECT doc_id, path, current_rev FROM docs WHERE repo_id = ? ORDER BY path")
295
+ .all(repoId);
296
+ const revStmt = db.prepare(`SELECT r.rev_id, r.doc_id, r.seq, r.root_tree, r.frontmatter_blob, r.commit_id, c.ts, c.origin, c.reason, r.archive
297
+ FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.doc_id = ? ORDER BY r.seq`);
298
+ const pinned = new Set(db.prepare("SELECT rev_id FROM pending_materializations WHERE repo_id = ? AND rev_id IS NOT NULL").all(repoId).map((r) => r.rev_id));
299
+ const archiveSet = new Set();
300
+ const perDoc = [];
301
+ let totalLive = 0;
302
+ for (const d of docs) {
303
+ const rows = revStmt.all(d.doc_id).filter((r) => r.archive === null);
304
+ totalLive += rows.length;
305
+ if (rows.length === 0)
306
+ continue;
307
+ const policy = policyFor(base, referencePath(d.path));
308
+ const kept = new Set();
309
+ // Rules 1–6: the per-revision keeps.
310
+ rows.forEach((r, i) => {
311
+ const rank = rows.length - i; // 1 = the newest
312
+ if (r.rev_id === d.current_rev)
313
+ kept.add(r.rev_id);
314
+ else if (pinned.has(r.rev_id))
315
+ kept.add(r.rev_id);
316
+ else if (ageDays(r.ts, nowMs) < policy.keep_days)
317
+ kept.add(r.rev_id);
318
+ else if (rank <= policy.per_doc_max)
319
+ kept.add(r.rev_id);
320
+ else if (policy.keep_origins.includes(r.origin) && r.reason !== null && r.reason !== "")
321
+ kept.add(r.rev_id);
322
+ else if (beforeMs !== null && Date.parse(r.ts) >= beforeMs)
323
+ kept.add(r.rev_id);
324
+ });
325
+ // Rule 7: the ladder. Each remaining revision belongs to the rung with the
326
+ // greatest `after_days` not above its age; the newest revision of each
327
+ // (rung, period) bucket — over every live revision of the document in that
328
+ // bucket, kept or not — is the bucket's representative and is kept.
329
+ const representative = new Map();
330
+ const bucketOf = new Map();
331
+ for (const r of rows) {
332
+ const age = ageDays(r.ts, nowMs);
333
+ if (age < policy.keep_days)
334
+ continue;
335
+ let rung;
336
+ for (const l of policy.ladder)
337
+ if (l.after_days <= age)
338
+ rung = l;
339
+ if (!rung) {
340
+ kept.add(r.rev_id); // the gap below the first rung: nothing says to thin it
341
+ continue;
342
+ }
343
+ const key = `${rung.after_days}|${rung.keep}|${periodKey(r.ts, rung.keep)}`;
344
+ bucketOf.set(r.rev_id, key);
345
+ const cur = representative.get(key);
346
+ if (!cur || r.seq > cur.seq)
347
+ representative.set(key, r);
348
+ }
349
+ for (const r of representative.values())
350
+ kept.add(r.rev_id);
351
+ const keep = [];
352
+ const archive = [];
353
+ for (const r of rows) {
354
+ if (kept.has(r.rev_id))
355
+ keep.push(r.rev_id);
356
+ else {
357
+ archive.push(r);
358
+ archiveSet.add(r.rev_id);
359
+ }
360
+ }
361
+ perDoc.push({ doc: d, live: rows, keep, archive });
362
+ }
363
+ // Costing: the mark from everything that stays live in the whole store.
364
+ const marker = new ReachMarker(store);
365
+ const keptTrees = new Set();
366
+ const keptBlobs = new Set();
367
+ const liveRevs = db.prepare("SELECT rev_id, root_tree, frontmatter_blob FROM revisions WHERE archive IS NULL").all();
368
+ marker.mark(liveRevs.filter((r) => !archiveSet.has(r.rev_id)), keptTrees, keptBlobs);
369
+ const unionTrees = new Set();
370
+ const unionBlobs = new Set();
371
+ const planned = [];
372
+ let keepTotal = 0;
373
+ for (const p of perDoc) {
374
+ keepTotal += p.keep.length;
375
+ if (p.archive.length === 0)
376
+ continue;
377
+ const trees = new Set();
378
+ const blobs = new Set();
379
+ marker.mark(p.archive, trees, blobs);
380
+ const onlyTrees = [...trees].filter((t) => !keptTrees.has(t));
381
+ const onlyBlobs = [...blobs].filter((b) => !keptBlobs.has(b));
382
+ for (const t of onlyTrees)
383
+ unionTrees.add(t);
384
+ for (const b of onlyBlobs)
385
+ unionBlobs.add(b);
386
+ planned.push({
387
+ id: p.doc.doc_id,
388
+ path: referencePath(p.doc.path),
389
+ revisions: p.live.length,
390
+ keep: p.keep,
391
+ archive: p.archive.map((r) => r.rev_id),
392
+ bytes: marker.bytesOf(onlyTrees, onlyBlobs),
393
+ });
394
+ }
395
+ return {
396
+ policy: base,
397
+ now,
398
+ before,
399
+ docs: planned,
400
+ totals: {
401
+ docs: planned.length,
402
+ revisions: totalLive,
403
+ keep: keepTotal,
404
+ archive: archiveSet.size,
405
+ bytes: marker.bytesOf(unionTrees, unionBlobs),
406
+ },
407
+ };
408
+ }
409
+ //# sourceMappingURL=retention.js.map
@@ -1,4 +1,4 @@
1
- export declare const SCHEMA_VERSION = 14;
1
+ export declare const SCHEMA_VERSION = 15;
2
2
  export declare const FILE_STATS_DDL = "\nCREATE TABLE IF NOT EXISTS file_stats (\n repo_id TEXT NOT NULL,\n path TEXT NOT NULL,\n mtime_ns INTEGER NOT NULL,\n size INTEGER NOT NULL,\n hash BLOB NOT NULL,\n PRIMARY KEY (repo_id, path)\n);\n";
3
3
  export declare const PROPERTIES_DDL = "\nCREATE TABLE IF NOT EXISTS properties (\n prop_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT, -- NULL = document-scoped (frontmatter, computed)\n source TEXT NOT NULL CHECK (source IN ('frontmatter','inline','computed')),\n key TEXT NOT NULL, -- dotted, flattened: \"layer\",\"meta.owner\"; computed carry $: \"$title\"\n card TEXT NOT NULL CHECK (card IN ('scalar','list')),\n ord INTEGER NOT NULL DEFAULT 0,\n val_text TEXT,\n val_num REAL,\n val_bool INTEGER,\n val_json TEXT,\n type TEXT NOT NULL CHECK (type IN ('string','number','bool','null','json')),\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_props_doc ON properties(doc_id) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_text ON properties(repo_id, key, val_text) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_num ON properties(repo_id, key, val_num) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_src_key ON properties(repo_id, source, key) WHERE deleted_commit IS NULL;\n";
4
4
  export declare const SYNC_DDL = "\nCREATE TABLE IF NOT EXISTS adapters (\n name TEXT PRIMARY KEY, -- workspace-unique adapter name (e.g. \"fs\")\n command TEXT NOT NULL, -- argv[0] to spawn (e.g. \"omgbase-fs-adapter\")\n args TEXT NOT NULL DEFAULT '[]' -- fixed leading args (JSON string[])\n);\n\nCREATE TABLE IF NOT EXISTS sources (\n source_id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE, -- workspace-unique source name\n adapter TEXT NOT NULL REFERENCES adapters(name),\n config TEXT NOT NULL DEFAULT '{}', -- JSON object \u2192 rendered to flags (13 \u00A73.1)\n env TEXT NOT NULL DEFAULT '{}' -- JSON object \u2192 spawn env (secrets; 13 \u00A73.2)\n);\n\nCREATE TABLE IF NOT EXISTS attachments (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n source_id TEXT NOT NULL REFERENCES sources(source_id),\n PRIMARY KEY (repo_id, source_id)\n);\n\nCREATE TABLE IF NOT EXISTS sync_state (\n repo_id TEXT NOT NULL,\n source_id TEXT NOT NULL,\n path TEXT NOT NULL, -- '' reserved for the attachment-level cursor row\n revision TEXT, -- last-observed source revision for this path\n cursor TEXT, -- last poll/webhook cursor (attachment-level row)\n PRIMARY KEY (repo_id, source_id, path)\n);\n";
@@ -6,5 +6,6 @@ export declare const WORKSPACE_SETTINGS_DDL = "\nCREATE TABLE IF NOT EXISTS work
6
6
  export declare const NODES_DDL = "\nCREATE TABLE IF NOT EXISTS nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT,\n kind TEXT NOT NULL,\n name TEXT,\n value TEXT,\n span_start INTEGER,\n span_end INTEGER,\n attrs TEXT NOT NULL DEFAULT '{}'\n);\nCREATE INDEX IF NOT EXISTS idx_nodes_doc ON nodes(doc_id);\nCREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name) WHERE name IS NOT NULL;\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n name, value, content='nodes', content_rowid='rowid', tokenize='porter unicode61'\n);\n";
7
7
  export declare const DOC_EMBEDDINGS_DDL = "\nCREATE TABLE IF NOT EXISTS doc_embeddings (\n doc_id TEXT NOT NULL,\n model TEXT NOT NULL,\n input_hash BLOB NOT NULL,\n method TEXT NOT NULL CHECK (method IN ('whole','pooled')),\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (doc_id, model)\n);\n";
8
8
  export declare const PENDING_MATERIALIZATIONS_DDL = "\nCREATE TABLE IF NOT EXISTS pending_materializations (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL, -- where the file must end up\n kind TEXT NOT NULL CHECK (kind IN ('write','rename','remove')),\n from_path TEXT, -- rename: the old path\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n rev_id TEXT REFERENCES revisions(rev_id), -- write/rename: the revision the bytes render (NULL for remove)\n rendered_hash BLOB NOT NULL, -- the bytes expected at path (remove: the bytes it is safe to unlink)\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n PRIMARY KEY (repo_id, path)\n);\n";
9
+ export declare const ARCHIVES_DDL = "\nCREATE TABLE IF NOT EXISTS archives (\n segment_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL,\n seq_lo INTEGER NOT NULL,\n seq_hi INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n revision_count INTEGER NOT NULL,\n bytes INTEGER NOT NULL\n);\n";
9
10
  export declare const MIGRATIONS: Record<number, string>;
10
- export declare const DDL = "\n-- ---- durable tables (02 \u00A73) --------------------------------------------------\nCREATE TABLE IF NOT EXISTS repos (\n repo_id TEXT PRIMARY KEY,\n slug TEXT NOT NULL UNIQUE,\n settings TEXT NOT NULL DEFAULT '{}'\n);\n-- A repo owns identity + history, NOT a filesystem (ADR-014). Where its bytes\n-- come from is a sources row (adapter=fs, config.root) joined via attachments;\n-- the former repos.root_path column was removed in schema v13.\n\nCREATE TABLE IF NOT EXISTS docs (\n doc_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL,\n format TEXT NOT NULL DEFAULT 'markdown',\n current_rev TEXT,\n file_hash BLOB,\n conflicted INTEGER NOT NULL DEFAULT 0,\n leading_trivia TEXT NOT NULL DEFAULT '',\n frontmatter_trivia TEXT, -- separator between frontmatter and first body block (NULL = no frontmatter)\n deleted_commit TEXT,\n UNIQUE (repo_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS blocks (\n block_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n parent_block TEXT,\n order_key TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n depth INTEGER NOT NULL,\n ancestor_path TEXT NOT NULL,\n type TEXT NOT NULL,\n attrs TEXT NOT NULL DEFAULT '{}',\n text TEXT NOT NULL,\n raw_hash BLOB NOT NULL,\n norm_hash BLOB NOT NULL,\n trivia_hash BLOB, -- trailing trivia blob hash (NULL = no trivia)\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_blocks_doc ON blocks(doc_id, parent_block, order_key) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_blocks_type ON blocks(repo_id, type) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_blocks_rawhash ON blocks(raw_hash);\nCREATE INDEX IF NOT EXISTS idx_blocks_ancestor ON blocks(doc_id, ancestor_path);\n\nCREATE TABLE IF NOT EXISTS blobs (\n hash BLOB PRIMARY KEY,\n bytes BLOB NOT NULL,\n size INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS tree_nodes (\n hash BLOB PRIMARY KEY,\n entries TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS revisions (\n rev_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n seq INTEGER NOT NULL,\n root_tree BLOB NOT NULL REFERENCES tree_nodes(hash),\n frontmatter_blob BLOB REFERENCES blobs(hash),\n rendered_hash BLOB NOT NULL,\n path TEXT NOT NULL,\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n UNIQUE (doc_id, seq)\n);\n\nCREATE TABLE IF NOT EXISTS commits (\n commit_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n seq INTEGER NOT NULL,\n ts TEXT NOT NULL,\n origin TEXT NOT NULL CHECK (origin IN ('api','observed','import','projection')),\n actor TEXT,\n reason TEXT,\n checkpoint_id TEXT,\n ops TEXT,\n UNIQUE (repo_id, seq)\n);\n\nCREATE TABLE IF NOT EXISTS dispositions (\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n block_id TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN\n ('same','edited','moved','edited_moved','inserted','deleted',\n 'split_from','merged_into','copied_from','resurrected','bulk_rewrite')),\n confidence REAL,\n reason TEXT,\n matcher_v TEXT,\n detail TEXT NOT NULL DEFAULT '{}',\n PRIMARY KEY (commit_id, block_id, kind)\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n edge_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n src_doc TEXT NOT NULL,\n src_block TEXT,\n src_field TEXT,\n predicate TEXT NOT NULL,\n dst_kind TEXT NOT NULL CHECK (dst_kind IN ('document','block','external','collection')),\n dst_node TEXT NOT NULL,\n anchor TEXT,\n provenance TEXT NOT NULL CHECK (provenance IN ('link','frontmatter','inline_field','projected',\n 'yaml_ref','yaml_schema','yaml_extends','json_ref','json_schema')),\n via_node TEXT,\n from_commit TEXT NOT NULL,\n to_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_doc, predicate) WHERE to_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_node, predicate) WHERE to_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_edges_temporal ON edges(from_commit, to_commit);\n\nCREATE TABLE IF NOT EXISTS external_nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n uri TEXT NOT NULL,\n title TEXT,\n UNIQUE (repo_id, uri)\n);\n\nCREATE TABLE IF NOT EXISTS collections (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n name TEXT NOT NULL,\n spec TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS checkpoints (\n id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n ts TEXT NOT NULL,\n files TEXT NOT NULL,\n git_head TEXT\n);\n\nCREATE TABLE IF NOT EXISTS resurrection_pool (\n block_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n raw_hash BLOB NOT NULL,\n norm_hash BLOB NOT NULL,\n type TEXT NOT NULL,\n deleted_commit TEXT NOT NULL,\n expires_ts TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS pending_materializations (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL, -- where the file must end up\n kind TEXT NOT NULL CHECK (kind IN ('write','rename','remove')),\n from_path TEXT, -- rename: the old path\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n rev_id TEXT REFERENCES revisions(rev_id), -- write/rename: the revision the bytes render (NULL for remove)\n rendered_hash BLOB NOT NULL, -- the bytes expected at path (remove: the bytes it is safe to unlink)\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n PRIMARY KEY (repo_id, path)\n);\n\n-- ---- derived tables (02 \u00A74; rebuildable) ------------------------------------\nCREATE TABLE IF NOT EXISTS sections (\n heading_block TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n level INTEGER NOT NULL,\n first_ordinal INTEGER NOT NULL,\n last_ordinal INTEGER NOT NULL,\n PRIMARY KEY (doc_id, heading_block)\n);\n\nCREATE TABLE IF NOT EXISTS doc_edges (\n src_doc TEXT NOT NULL,\n predicate TEXT NOT NULL,\n dst_node TEXT NOT NULL,\n dst_kind TEXT NOT NULL,\n count INTEGER NOT NULL,\n samples TEXT NOT NULL,\n PRIMARY KEY (src_doc, predicate, dst_node)\n);\n\nCREATE TABLE IF NOT EXISTS block_changes (\n block_id TEXT NOT NULL,\n commit_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n PRIMARY KEY (block_id, commit_id, kind)\n);\n\nCREATE TABLE IF NOT EXISTS inferred_edges (\n src_node TEXT NOT NULL,\n dst_node TEXT NOT NULL,\n predicate TEXT NOT NULL,\n method TEXT NOT NULL,\n score REAL NOT NULL,\n model_v TEXT NOT NULL,\n computed_at TEXT NOT NULL,\n PRIMARY KEY (src_node, dst_node, predicate, method)\n);\n\nCREATE TABLE IF NOT EXISTS embeddings (\n content_hash BLOB NOT NULL,\n ctx_hash BLOB NOT NULL,\n model TEXT NOT NULL,\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (content_hash, ctx_hash, model)\n);\n\n\nCREATE TABLE IF NOT EXISTS doc_embeddings (\n doc_id TEXT NOT NULL,\n model TEXT NOT NULL,\n input_hash BLOB NOT NULL,\n method TEXT NOT NULL CHECK (method IN ('whole','pooled')),\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (doc_id, model)\n);\n\n\nCREATE VIRTUAL TABLE IF NOT EXISTS blocks_fts USING fts5(\n text, content='blocks', content_rowid='rowid', tokenize='porter unicode61'\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT,\n kind TEXT NOT NULL,\n name TEXT,\n value TEXT,\n span_start INTEGER,\n span_end INTEGER,\n attrs TEXT NOT NULL DEFAULT '{}'\n);\nCREATE INDEX IF NOT EXISTS idx_nodes_doc ON nodes(doc_id);\nCREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name) WHERE name IS NOT NULL;\n\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n name, value, content='nodes', content_rowid='rowid', tokenize='porter unicode61'\n);\n\n\nCREATE TABLE IF NOT EXISTS file_stats (\n repo_id TEXT NOT NULL,\n path TEXT NOT NULL,\n mtime_ns INTEGER NOT NULL,\n size INTEGER NOT NULL,\n hash BLOB NOT NULL,\n PRIMARY KEY (repo_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS properties (\n prop_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT, -- NULL = document-scoped (frontmatter, computed)\n source TEXT NOT NULL CHECK (source IN ('frontmatter','inline','computed')),\n key TEXT NOT NULL, -- dotted, flattened: \"layer\",\"meta.owner\"; computed carry $: \"$title\"\n card TEXT NOT NULL CHECK (card IN ('scalar','list')),\n ord INTEGER NOT NULL DEFAULT 0,\n val_text TEXT,\n val_num REAL,\n val_bool INTEGER,\n val_json TEXT,\n type TEXT NOT NULL CHECK (type IN ('string','number','bool','null','json')),\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_props_doc ON properties(doc_id) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_text ON properties(repo_id, key, val_text) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_num ON properties(repo_id, key, val_num) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_src_key ON properties(repo_id, source, key) WHERE deleted_commit IS NULL;\n\n\nCREATE TABLE IF NOT EXISTS adapters (\n name TEXT PRIMARY KEY, -- workspace-unique adapter name (e.g. \"fs\")\n command TEXT NOT NULL, -- argv[0] to spawn (e.g. \"omgbase-fs-adapter\")\n args TEXT NOT NULL DEFAULT '[]' -- fixed leading args (JSON string[])\n);\n\nCREATE TABLE IF NOT EXISTS sources (\n source_id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE, -- workspace-unique source name\n adapter TEXT NOT NULL REFERENCES adapters(name),\n config TEXT NOT NULL DEFAULT '{}', -- JSON object \u2192 rendered to flags (13 \u00A73.1)\n env TEXT NOT NULL DEFAULT '{}' -- JSON object \u2192 spawn env (secrets; 13 \u00A73.2)\n);\n\nCREATE TABLE IF NOT EXISTS attachments (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n source_id TEXT NOT NULL REFERENCES sources(source_id),\n PRIMARY KEY (repo_id, source_id)\n);\n\nCREATE TABLE IF NOT EXISTS sync_state (\n repo_id TEXT NOT NULL,\n source_id TEXT NOT NULL,\n path TEXT NOT NULL, -- '' reserved for the attachment-level cursor row\n revision TEXT, -- last-observed source revision for this path\n cursor TEXT, -- last poll/webhook cursor (attachment-level row)\n PRIMARY KEY (repo_id, source_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS workspace_settings (\n id INTEGER PRIMARY KEY CHECK (id = 0),\n settings TEXT NOT NULL DEFAULT '{}'\n);\nINSERT OR IGNORE INTO workspace_settings (id, settings) VALUES (0, '{}');\n\n";
11
+ export declare const DDL = "\n-- ---- durable tables (02 \u00A73) --------------------------------------------------\nCREATE TABLE IF NOT EXISTS repos (\n repo_id TEXT PRIMARY KEY,\n slug TEXT NOT NULL UNIQUE,\n settings TEXT NOT NULL DEFAULT '{}'\n);\n-- A repo owns identity + history, NOT a filesystem (ADR-014). Where its bytes\n-- come from is a sources row (adapter=fs, config.root) joined via attachments;\n-- the former repos.root_path column was removed in schema v13.\n\nCREATE TABLE IF NOT EXISTS docs (\n doc_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL,\n format TEXT NOT NULL DEFAULT 'markdown',\n current_rev TEXT,\n file_hash BLOB,\n conflicted INTEGER NOT NULL DEFAULT 0,\n leading_trivia TEXT NOT NULL DEFAULT '',\n frontmatter_trivia TEXT, -- separator between frontmatter and first body block (NULL = no frontmatter)\n deleted_commit TEXT,\n UNIQUE (repo_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS blocks (\n block_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n parent_block TEXT,\n order_key TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n depth INTEGER NOT NULL,\n ancestor_path TEXT NOT NULL,\n type TEXT NOT NULL,\n attrs TEXT NOT NULL DEFAULT '{}',\n text TEXT NOT NULL,\n raw_hash BLOB NOT NULL,\n norm_hash BLOB NOT NULL,\n trivia_hash BLOB, -- trailing trivia blob hash (NULL = no trivia)\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_blocks_doc ON blocks(doc_id, parent_block, order_key) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_blocks_type ON blocks(repo_id, type) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_blocks_rawhash ON blocks(raw_hash);\nCREATE INDEX IF NOT EXISTS idx_blocks_ancestor ON blocks(doc_id, ancestor_path);\n\nCREATE TABLE IF NOT EXISTS blobs (\n hash BLOB PRIMARY KEY,\n bytes BLOB NOT NULL,\n size INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS tree_nodes (\n hash BLOB PRIMARY KEY,\n entries TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS revisions (\n rev_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n seq INTEGER NOT NULL,\n root_tree BLOB, -- a tree_nodes.hash; NULL once archived (spec/store \u00A77 \"Retention\", 15.0)\n frontmatter_blob BLOB, -- a blobs.hash, or NULL (no frontmatter, or archived)\n rendered_hash BLOB NOT NULL,\n path TEXT NOT NULL,\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n archive TEXT, -- the archive segment holding the bodies; NULL = live (15.0)\n UNIQUE (doc_id, seq)\n);\n\nCREATE TABLE IF NOT EXISTS commits (\n commit_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n seq INTEGER NOT NULL,\n ts TEXT NOT NULL,\n origin TEXT NOT NULL CHECK (origin IN ('api','observed','import','projection')),\n actor TEXT,\n reason TEXT,\n checkpoint_id TEXT,\n ops TEXT,\n UNIQUE (repo_id, seq)\n);\n\nCREATE TABLE IF NOT EXISTS dispositions (\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n block_id TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN\n ('same','edited','moved','edited_moved','inserted','deleted',\n 'split_from','merged_into','copied_from','resurrected','bulk_rewrite')),\n confidence REAL,\n reason TEXT,\n matcher_v TEXT,\n detail TEXT NOT NULL DEFAULT '{}',\n PRIMARY KEY (commit_id, block_id, kind)\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n edge_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n src_doc TEXT NOT NULL,\n src_block TEXT,\n src_field TEXT,\n predicate TEXT NOT NULL,\n dst_kind TEXT NOT NULL CHECK (dst_kind IN ('document','block','external','collection')),\n dst_node TEXT NOT NULL,\n anchor TEXT,\n provenance TEXT NOT NULL CHECK (provenance IN ('link','frontmatter','inline_field','projected',\n 'yaml_ref','yaml_schema','yaml_extends','json_ref','json_schema')),\n via_node TEXT,\n from_commit TEXT NOT NULL,\n to_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_doc, predicate) WHERE to_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_node, predicate) WHERE to_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_edges_temporal ON edges(from_commit, to_commit);\n\nCREATE TABLE IF NOT EXISTS external_nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n uri TEXT NOT NULL,\n title TEXT,\n UNIQUE (repo_id, uri)\n);\n\nCREATE TABLE IF NOT EXISTS collections (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n name TEXT NOT NULL,\n spec TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS checkpoints (\n id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n ts TEXT NOT NULL,\n files TEXT NOT NULL,\n git_head TEXT\n);\n\nCREATE TABLE IF NOT EXISTS resurrection_pool (\n block_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n raw_hash BLOB NOT NULL,\n norm_hash BLOB NOT NULL,\n type TEXT NOT NULL,\n deleted_commit TEXT NOT NULL,\n expires_ts TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS pending_materializations (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL, -- where the file must end up\n kind TEXT NOT NULL CHECK (kind IN ('write','rename','remove')),\n from_path TEXT, -- rename: the old path\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n rev_id TEXT REFERENCES revisions(rev_id), -- write/rename: the revision the bytes render (NULL for remove)\n rendered_hash BLOB NOT NULL, -- the bytes expected at path (remove: the bytes it is safe to unlink)\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n PRIMARY KEY (repo_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS archives (\n segment_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL,\n seq_lo INTEGER NOT NULL,\n seq_hi INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n revision_count INTEGER NOT NULL,\n bytes INTEGER NOT NULL\n);\n\n-- ---- derived tables (02 \u00A74; rebuildable) ------------------------------------\nCREATE TABLE IF NOT EXISTS sections (\n heading_block TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n level INTEGER NOT NULL,\n first_ordinal INTEGER NOT NULL,\n last_ordinal INTEGER NOT NULL,\n PRIMARY KEY (doc_id, heading_block)\n);\n\nCREATE TABLE IF NOT EXISTS doc_edges (\n src_doc TEXT NOT NULL,\n predicate TEXT NOT NULL,\n dst_node TEXT NOT NULL,\n dst_kind TEXT NOT NULL,\n count INTEGER NOT NULL,\n samples TEXT NOT NULL,\n PRIMARY KEY (src_doc, predicate, dst_node)\n);\n\nCREATE TABLE IF NOT EXISTS block_changes (\n block_id TEXT NOT NULL,\n commit_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n PRIMARY KEY (block_id, commit_id, kind)\n);\n\nCREATE TABLE IF NOT EXISTS inferred_edges (\n src_node TEXT NOT NULL,\n dst_node TEXT NOT NULL,\n predicate TEXT NOT NULL,\n method TEXT NOT NULL,\n score REAL NOT NULL,\n model_v TEXT NOT NULL,\n computed_at TEXT NOT NULL,\n PRIMARY KEY (src_node, dst_node, predicate, method)\n);\n\nCREATE TABLE IF NOT EXISTS embeddings (\n content_hash BLOB NOT NULL,\n ctx_hash BLOB NOT NULL,\n model TEXT NOT NULL,\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (content_hash, ctx_hash, model)\n);\n\n\nCREATE TABLE IF NOT EXISTS doc_embeddings (\n doc_id TEXT NOT NULL,\n model TEXT NOT NULL,\n input_hash BLOB NOT NULL,\n method TEXT NOT NULL CHECK (method IN ('whole','pooled')),\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (doc_id, model)\n);\n\n\nCREATE VIRTUAL TABLE IF NOT EXISTS blocks_fts USING fts5(\n text, content='blocks', content_rowid='rowid', tokenize='porter unicode61'\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT,\n kind TEXT NOT NULL,\n name TEXT,\n value TEXT,\n span_start INTEGER,\n span_end INTEGER,\n attrs TEXT NOT NULL DEFAULT '{}'\n);\nCREATE INDEX IF NOT EXISTS idx_nodes_doc ON nodes(doc_id);\nCREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name) WHERE name IS NOT NULL;\n\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n name, value, content='nodes', content_rowid='rowid', tokenize='porter unicode61'\n);\n\n\nCREATE TABLE IF NOT EXISTS file_stats (\n repo_id TEXT NOT NULL,\n path TEXT NOT NULL,\n mtime_ns INTEGER NOT NULL,\n size INTEGER NOT NULL,\n hash BLOB NOT NULL,\n PRIMARY KEY (repo_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS properties (\n prop_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT, -- NULL = document-scoped (frontmatter, computed)\n source TEXT NOT NULL CHECK (source IN ('frontmatter','inline','computed')),\n key TEXT NOT NULL, -- dotted, flattened: \"layer\",\"meta.owner\"; computed carry $: \"$title\"\n card TEXT NOT NULL CHECK (card IN ('scalar','list')),\n ord INTEGER NOT NULL DEFAULT 0,\n val_text TEXT,\n val_num REAL,\n val_bool INTEGER,\n val_json TEXT,\n type TEXT NOT NULL CHECK (type IN ('string','number','bool','null','json')),\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_props_doc ON properties(doc_id) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_text ON properties(repo_id, key, val_text) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_num ON properties(repo_id, key, val_num) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_src_key ON properties(repo_id, source, key) WHERE deleted_commit IS NULL;\n\n\nCREATE TABLE IF NOT EXISTS adapters (\n name TEXT PRIMARY KEY, -- workspace-unique adapter name (e.g. \"fs\")\n command TEXT NOT NULL, -- argv[0] to spawn (e.g. \"omgbase-fs-adapter\")\n args TEXT NOT NULL DEFAULT '[]' -- fixed leading args (JSON string[])\n);\n\nCREATE TABLE IF NOT EXISTS sources (\n source_id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE, -- workspace-unique source name\n adapter TEXT NOT NULL REFERENCES adapters(name),\n config TEXT NOT NULL DEFAULT '{}', -- JSON object \u2192 rendered to flags (13 \u00A73.1)\n env TEXT NOT NULL DEFAULT '{}' -- JSON object \u2192 spawn env (secrets; 13 \u00A73.2)\n);\n\nCREATE TABLE IF NOT EXISTS attachments (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n source_id TEXT NOT NULL REFERENCES sources(source_id),\n PRIMARY KEY (repo_id, source_id)\n);\n\nCREATE TABLE IF NOT EXISTS sync_state (\n repo_id TEXT NOT NULL,\n source_id TEXT NOT NULL,\n path TEXT NOT NULL, -- '' reserved for the attachment-level cursor row\n revision TEXT, -- last-observed source revision for this path\n cursor TEXT, -- last poll/webhook cursor (attachment-level row)\n PRIMARY KEY (repo_id, source_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS workspace_settings (\n id INTEGER PRIMARY KEY CHECK (id = 0),\n settings TEXT NOT NULL DEFAULT '{}'\n);\nINSERT OR IGNORE INTO workspace_settings (id, settings) VALUES (0, '{}');\n\n";
@@ -1,7 +1,7 @@
1
1
  // Full DDL — data-model §3 (durable) + §4 (derived). SQLite dialect.
2
2
  // Kept as one string so migrations and rebuild-index can apply it verbatim.
3
3
  // No dialect-specific SQL leaks above the store module (02 §8).
4
- export const SCHEMA_VERSION = 14;
4
+ export const SCHEMA_VERSION = 15;
5
5
  // file_stats (02 §4; derived, rebuildable by a full re-stat) backs the CLI
6
6
  // freshness sweep (11 §3.3): (mtime_ns, size) cheap-change detection so a
7
7
  // one-shot command re-ingests only files that changed on disk since last ingest.
@@ -160,10 +160,33 @@ CREATE TABLE IF NOT EXISTS pending_materializations (
160
160
  PRIMARY KEY (repo_id, path)
161
161
  );
162
162
  `;
163
+ // archives (spec/store §7 "Retention", store 15.0). The registry of archive
164
+ // segments — the one-file-per-export SQLite databases `omg history export`
165
+ // writes (`spec/store/archive.sql`) that hold the bodies of the revisions the
166
+ // retention policy squashed: `segment_id` (a minted `s_` id, the manifest's),
167
+ // `path` (the file as recorded; `attach` re-records it), `seq_lo`/`seq_hi`
168
+ // (the `commits.seq` span of the archived commits), `created_at`,
169
+ // `revision_count`, `bytes` (the file's size when recorded). A revision's
170
+ // `archive` column names its segment whether or not the row is here: `detach`
171
+ // deletes the row, the pointer stays, and a read of such a revision is
172
+ // `revision_archived`. Rows are written by the squash transaction and by
173
+ // `attach`; never by a commit.
174
+ export const ARCHIVES_DDL = /* sql */ `
175
+ CREATE TABLE IF NOT EXISTS archives (
176
+ segment_id TEXT PRIMARY KEY,
177
+ repo_id TEXT NOT NULL REFERENCES repos(repo_id),
178
+ path TEXT NOT NULL,
179
+ seq_lo INTEGER NOT NULL,
180
+ seq_hi INTEGER NOT NULL,
181
+ created_at TEXT NOT NULL,
182
+ revision_count INTEGER NOT NULL,
183
+ bytes INTEGER NOT NULL
184
+ );
185
+ `;
163
186
  // Additive migrations keyed by the version they upgrade TO. Each runs inside a
164
187
  // transaction. Only forward, idempotent DDL (CREATE ... IF NOT EXISTS) — no
165
188
  // destructive changes. store.ts applies these in order for an older db.
166
- // Migrations 3, 4, 6, 7, 9, 11, 13 are programmatic — see store.ts.
189
+ // Migrations 3, 4, 6, 7, 9, 11, 13, 15 are programmatic — see store.ts.
167
190
  export const MIGRATIONS = {
168
191
  2: FILE_STATS_DDL,
169
192
  3: "", // handled programmatically in store.ts
@@ -178,6 +201,7 @@ export const MIGRATIONS = {
178
201
  12: DOC_EMBEDDINGS_DDL, // doc-grain semantic vectors
179
202
  13: "", // drop repos.root_path (migrate → fs sources), handled programmatically in store.ts
180
203
  14: PENDING_MATERIALIZATIONS_DDL, // the api path's recorded file intent (spec/store §5.7)
204
+ 15: "", // revisions.root_tree/frontmatter_blob nullable + `archive`, create `archives` (spec/store §7 "Retention"), handled programmatically in store.ts
181
205
  };
182
206
  export const DDL = /* sql */ `
183
207
  -- ---- durable tables (02 §3) --------------------------------------------------
@@ -242,11 +266,12 @@ CREATE TABLE IF NOT EXISTS revisions (
242
266
  rev_id TEXT PRIMARY KEY,
243
267
  doc_id TEXT NOT NULL REFERENCES docs(doc_id),
244
268
  seq INTEGER NOT NULL,
245
- root_tree BLOB NOT NULL REFERENCES tree_nodes(hash),
246
- frontmatter_blob BLOB REFERENCES blobs(hash),
269
+ root_tree BLOB, -- a tree_nodes.hash; NULL once archived (spec/store §7 "Retention", 15.0)
270
+ frontmatter_blob BLOB, -- a blobs.hash, or NULL (no frontmatter, or archived)
247
271
  rendered_hash BLOB NOT NULL,
248
272
  path TEXT NOT NULL,
249
273
  commit_id TEXT NOT NULL REFERENCES commits(commit_id),
274
+ archive TEXT, -- the archive segment holding the bodies; NULL = live (15.0)
250
275
  UNIQUE (doc_id, seq)
251
276
  );
252
277
 
@@ -330,6 +355,7 @@ CREATE TABLE IF NOT EXISTS resurrection_pool (
330
355
  expires_ts TEXT NOT NULL
331
356
  );
332
357
  ${PENDING_MATERIALIZATIONS_DDL}
358
+ ${ARCHIVES_DDL}
333
359
  -- ---- derived tables (02 §4; rebuildable) ------------------------------------
334
360
  CREATE TABLE IF NOT EXISTS sections (
335
361
  heading_block TEXT NOT NULL,
@@ -35,12 +35,27 @@ export interface SizeFile {
35
35
  /** `freelist_count * page_size`: what `VACUUM` returns to the filesystem. */
36
36
  reclaimable: number;
37
37
  }
38
+ export interface SizeArchived {
39
+ /** `revisions` rows whose bodies live in a segment (`archive IS NOT NULL`). */
40
+ revisions: number;
41
+ /** Recorded segments (`archives` rows). */
42
+ segments: number;
43
+ /** The recorded segments' bytes on disk (a missing file counts 0). */
44
+ bytes: number;
45
+ }
46
+ /** What the retention policy would archive now (spec/store §7 "Retention"), summed over the store's repositories. */
47
+ export interface SizeRetention {
48
+ revisions: number;
49
+ bytes: number;
50
+ }
38
51
  export interface SizeReport {
39
52
  file: SizeFile;
40
53
  /** Always the four categories, in the fixed order content, history, search, sync. */
41
54
  categories: SizeCategoryReport[];
42
55
  /** spec/store §7 (14.2): `dispositions` rows with `kind = 'same'` — what a store through 14.1 wrote and `omg gc` removes. */
43
56
  same_dispositions: number;
57
+ /** spec/store §7 "Retention" (15.0): archived revisions, recorded segments, and the segments' bytes on disk. */
58
+ archived: SizeArchived;
44
59
  advice: string[];
45
60
  /** Present only when the SQLite build lacks `dbstat` (row counts only). */
46
61
  detail?: "dbstat unavailable";
@@ -51,6 +66,8 @@ export declare const VACUUM_ADVICE_MIN_SHARE = 10;
51
66
  export declare const HISTORY_ADVICE_MIN_SHARE = 50;
52
67
  export declare const SAME_ADVICE_MIN_SHARE = 25;
53
68
  export declare const EMBEDDINGS_ADVICE_MIN_SHARE = 25;
69
+ /** The retention advice fires when the plan's bytes exceed this share of the `history` category (15.0). */
70
+ export declare const RETENTION_ADVICE_MIN_SHARE = 10;
54
71
  /**
55
72
  * `<n> B` under 1024, else one decimal of KB/MB/GB/TB (1024-based), the tenths
56
73
  * rounded half up — `4096` → `4.0 KB`, `13734297` → `13.1 MB`. Both engines
@@ -62,9 +79,11 @@ export declare function formatShare(share: number): string;
62
79
  /** `bytes / total` in percent, one decimal, rounded half up; 0 when the total is 0. */
63
80
  export declare function shareOf(bytes: number, total: number): number;
64
81
  /** spec/store §7: the advice lines, in this order; `nothing to reclaim` when none applies. */
65
- export declare function sizeAdvice(file: SizeFile, categories: SizeCategoryReport[], sameDispositions?: number): string[];
82
+ export declare function sizeAdvice(file: SizeFile, categories: SizeCategoryReport[], sameDispositions?: number, retention?: SizeRetention): string[];
66
83
  /** The size report of an open store (spec/store §7 "Size"). */
67
- export declare function sizeReport(store: Store): SizeReport;
84
+ export declare function sizeReport(store: Store, opts?: {
85
+ now?: string;
86
+ }): SizeReport;
68
87
  export interface VacuumResult {
69
88
  /** `freelist_count * page_size` before the `VACUUM`: the bytes it returned. */
70
89
  reclaimable: number;