@omgbase/core 0.21.0 → 0.24.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 (39) 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 +49 -11
  26. package/dist/src/mcp/server.js +2 -2
  27. package/dist/src/mutate/select.js +5 -1
  28. package/dist/src/oqx-js/context.js +39 -8
  29. package/dist/src/oqx-js/dates.d.ts +105 -0
  30. package/dist/src/oqx-js/dates.js +427 -0
  31. package/dist/src/oqx-js/names.js +14 -3
  32. package/dist/src/oqx-js/planner.js +21 -2
  33. package/dist/src/oqx-js/run.d.ts +1 -0
  34. package/dist/src/oqx-js/run.js +82 -2
  35. package/dist/src/oqx-js/sql/translate.js +12 -5
  36. package/dist/src/oqx-js/store-index.js +1 -1
  37. package/dist/src/spec-versions.d.ts +4 -4
  38. package/dist/src/spec-versions.js +3 -3
  39. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- export type IdPrefix = "d" | "b" | "c" | "r" | "x" | "col" | "cp" | "e" | "rp" | "v" | "src";
1
+ export type IdPrefix = "d" | "b" | "c" | "r" | "x" | "col" | "cp" | "e" | "rp" | "v" | "src" | "s";
2
2
  export declare function randomSuffix(len?: number): string;
3
3
  /** A replacement id generator (spec/store §2.2): given a prefix, return the whole id. */
4
4
  export type IdMinter = (prefix: IdPrefix) => string;
@@ -106,7 +106,10 @@ export interface DocsReadAtResult {
106
106
  * IN ORDER — but sourced from the revision's Merkle `root_tree` instead of the
107
107
  * live `blocks` table. Returns null when the doc is missing, or when `revId`
108
108
  * does not name a revision of this document (so the caller can raise a clean
109
- * not-found error).
109
+ * not-found error). An ARCHIVED revision (spec/store §7 "Retention", 15.0)
110
+ * reads through its segment, attached on demand; when the segment is not
111
+ * recorded or not on disk this throws `RevisionArchived` (the surface's
112
+ * `revision_archived`).
110
113
  *
111
114
  * FIDELITY CAVEAT: `leading_trivia` and `frontmatter_trivia` are stored ONLY on
112
115
  * the current `docs` row, NOT per-revision (see schema.ts — the `docs` row is
@@ -2,6 +2,7 @@ import { findDoc, findDocByRef, loadDocBlocks } from "./reader.js";
2
2
  import { docPropertiesGrouped } from "../store/properties.js";
3
3
  import { sha256 } from "../hash.js";
4
4
  import { referencePath } from "../paths.js";
5
+ import { revisionBody } from "../store/archive.js";
5
6
  function blob(db, hash) {
6
7
  const row = db.prepare("SELECT bytes FROM blobs WHERE hash = ?").get(hash);
7
8
  return row ? row.bytes.toString("utf8") : "";
@@ -154,7 +155,10 @@ export function docsReadMany(store, repoId, refs, opts = {}) {
154
155
  * IN ORDER — but sourced from the revision's Merkle `root_tree` instead of the
155
156
  * live `blocks` table. Returns null when the doc is missing, or when `revId`
156
157
  * does not name a revision of this document (so the caller can raise a clean
157
- * not-found error).
158
+ * not-found error). An ARCHIVED revision (spec/store §7 "Retention", 15.0)
159
+ * reads through its segment, attached on demand; when the segment is not
160
+ * recorded or not on disk this throws `RevisionArchived` (the surface's
161
+ * `revision_archived`).
158
162
  *
159
163
  * FIDELITY CAVEAT: `leading_trivia` and `frontmatter_trivia` are stored ONLY on
160
164
  * the current `docs` row, NOT per-revision (see schema.ts — the `docs` row is
@@ -174,25 +178,33 @@ export function readDocumentAtRevision(store, docId, revId) {
174
178
  .get(docId);
175
179
  if (!doc)
176
180
  return null;
177
- const rev = db.prepare("SELECT root_tree, frontmatter_blob, rendered_hash, path FROM revisions WHERE rev_id = ? AND doc_id = ?").get(revId, docId);
181
+ // The revision's bodies: the store's for a live revision, its segment's for
182
+ // an archived one (spec/store §6.2 "through a segment", 15.0 — `prefix` is
183
+ // the attached alias; an unavailable segment throws RevisionArchived).
184
+ const rev = revisionBody(store, revId, docId);
178
185
  if (!rev)
179
186
  return null;
187
+ const at = (table) => `${rev.prefix}${table}`;
188
+ const blobAt = (hash) => {
189
+ const row = db.prepare(`SELECT bytes FROM ${at("blobs")} WHERE hash = ?`).get(hash);
190
+ return row ? row.bytes.toString("utf8") : "";
191
+ };
180
192
  const out = [doc.leading_trivia];
181
193
  // Frontmatter blob for THIS revision (per-revision), then the doc-level
182
194
  // separator (best-effort — see fidelity caveat).
183
195
  if (rev.frontmatter_blob)
184
- out.push(blob(db, rev.frontmatter_blob) + (doc.frontmatter_trivia ?? ""));
196
+ out.push(blobAt(rev.frontmatter_blob) + (doc.frontmatter_trivia ?? ""));
185
197
  // Top-level blocks in tree order, from THIS revision's Merkle root tree node.
186
198
  // A container's raw already holds its children verbatim, so we deliberately do
187
199
  // NOT recurse into childTreeHashHex (mirrors reconstructContent's
188
200
  // parent_block IS NULL walk). An absent tree node yields empty body bytes.
189
- const node = db.prepare("SELECT entries FROM tree_nodes WHERE hash = ?").get(rev.root_tree);
201
+ const node = db.prepare(`SELECT entries FROM ${at("tree_nodes")} WHERE hash = ?`).get(rev.root_tree);
190
202
  if (node) {
191
203
  const entries = JSON.parse(node.entries);
192
204
  for (const [, rawHashHex, , , , triviaHashHex] of entries) {
193
- out.push(blob(db, Buffer.from(rawHashHex, "hex")));
205
+ out.push(blobAt(Buffer.from(rawHashHex, "hex")));
194
206
  if (triviaHashHex)
195
- out.push(blob(db, Buffer.from(triviaHashHex, "hex")));
207
+ out.push(blobAt(Buffer.from(triviaHashHex, "hex")));
196
208
  }
197
209
  }
198
210
  const content = out.join("");
@@ -0,0 +1,196 @@
1
+ import type { Store } from "./store.js";
2
+ import { type EvaluateOptions, type RetentionPlan, type RetentionPolicy } from "./retention.js";
3
+ /** spec/store/archive.sql, verbatim — the segment's DDL. */
4
+ export declare const ARCHIVE_DDL = "-- ---- an archive segment (spec/store \u00A77 \"Retention\", 15.0) --------------------\n-- One file per `history export`, never appended: the bodies of the revisions\n-- the retention policy squashed, self-contained (every blob and tree node\n-- reachable from them), plus copies of their revision, commit, disposition and\n-- block-change rows, and one manifest row. The same DDL subset as schema.sql\n-- minus the foreign keys (a segment holds no repos or docs rows).\nCREATE TABLE IF NOT EXISTS manifest (\n segment_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n created_at TEXT NOT NULL,\n policy TEXT NOT NULL,\n seq_lo INTEGER NOT NULL,\n seq_hi INTEGER NOT NULL,\n revision_count INTEGER NOT NULL,\n previous_segment TEXT,\n schema_user_version INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS revisions (\n rev_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n root_tree BLOB NOT NULL,\n frontmatter_blob BLOB,\n rendered_hash BLOB NOT NULL,\n path TEXT NOT NULL,\n commit_id TEXT NOT NULL,\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,\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);\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 dispositions (\n commit_id TEXT NOT NULL,\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 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";
5
+ /** The file suffix every segment carries. */
6
+ export declare const ARCHIVE_SUFFIX = ".omgarchive.db";
7
+ /** The default directory under `.omgbase/`. */
8
+ export declare const ARCHIVE_DIR = "archives";
9
+ /** `<repo_id>-<seq_lo>-<seq_hi>-<segment_id>.omgarchive.db` */
10
+ export declare function segmentFileName(repoId: string, seqLo: number, seqHi: number, segmentId: string): string;
11
+ /** The parts a segment file name carries, or null when the name is not one. */
12
+ export declare function parseSegmentFileName(name: string): {
13
+ repoId: string;
14
+ seqLo: number;
15
+ seqHi: number;
16
+ segmentId: string;
17
+ } | null;
18
+ export interface Manifest {
19
+ segment_id: string;
20
+ repo_id: string;
21
+ created_at: string;
22
+ policy: RetentionPolicy;
23
+ seq_lo: number;
24
+ seq_hi: number;
25
+ revision_count: number;
26
+ previous_segment: string | null;
27
+ schema_user_version: number;
28
+ }
29
+ /** A read of an archived revision whose segment is not recorded, or recorded but not on disk (spec/surface: `revision_archived`). */
30
+ export declare class RevisionArchived extends Error {
31
+ readonly rev: string;
32
+ readonly archive: string;
33
+ readonly path: string | null;
34
+ constructor(rev: string, archive: string, path: string | null);
35
+ }
36
+ /** Not an archive segment, or one of another repository. */
37
+ export declare class NotASegment extends Error {
38
+ readonly path: string;
39
+ constructor(path: string, detail: string);
40
+ }
41
+ /** Open a segment read-only and return its manifest; `NotASegment` when the file is not one. */
42
+ export declare function readManifest(path: string): Manifest;
43
+ export interface ArchiveRow {
44
+ segment_id: string;
45
+ repo_id: string;
46
+ path: string;
47
+ seq_lo: number;
48
+ seq_hi: number;
49
+ created_at: string;
50
+ revision_count: number;
51
+ bytes: number;
52
+ }
53
+ export interface AttachedSegment {
54
+ segment_id: string;
55
+ path: string;
56
+ seq_lo: number;
57
+ seq_hi: number;
58
+ revision_count: number;
59
+ bytes: number;
60
+ }
61
+ export interface AttachResult {
62
+ attached: AttachedSegment[];
63
+ /** Files in a directory that are not this repository's segments. */
64
+ skipped: number;
65
+ }
66
+ /**
67
+ * `omg history attach <file|dir>`: record a segment (idempotent — a known
68
+ * segment's path is re-recorded) or every `.omgarchive.db` of a directory
69
+ * whose manifest `repo_id` is this repository's. A single file of another
70
+ * repository or a non-segment is `NotASegment`; in a directory they are skipped.
71
+ */
72
+ export declare function attachArchive(store: Store, repoId: string, target: string): AttachResult;
73
+ /**
74
+ * `omg history detach <segment_id|path>`: forget a segment. The revisions it
75
+ * holds keep their `archive` pointer; reading one is `revision_archived` until
76
+ * the segment is attached again. Returns the id, or null when nothing matched.
77
+ */
78
+ export declare function detachArchive(store: Store, repoId: string, ref: string): string | null;
79
+ /**
80
+ * A segment's state (spec/store §7 "Retention"): recorded and on disk
81
+ * (`attached`), recorded but not on disk (`missing`); or unrecorded under the
82
+ * archives directory — `pending` (complete, its revisions still live: the
83
+ * crash rule finishes it on the next `export --prune`), `detached` (complete
84
+ * and squashed: `attach` brings it back), `stale` (some of its revisions live,
85
+ * some not: it belongs to another store or predates a different squash).
86
+ */
87
+ export type ArchiveState = "attached" | "missing" | "pending" | "detached" | "stale";
88
+ export interface ArchiveListing extends AttachedSegment {
89
+ created_at: string;
90
+ state: ArchiveState;
91
+ }
92
+ /**
93
+ * `omg history list`: the repository's recorded segments by `seq_lo` then id,
94
+ * followed by the unrecorded complete segments found in `dest`, by file name.
95
+ */
96
+ export declare function listArchives(store: Store, repoId: string, opts?: {
97
+ dest?: string;
98
+ }): ArchiveListing[];
99
+ export interface UnrecordedSegment {
100
+ path: string;
101
+ state: "pending" | "detached" | "stale";
102
+ }
103
+ /**
104
+ * The complete segments of this repository under `dest` that no `archives`
105
+ * row records, by file name, each classified by its revisions: all live →
106
+ * `pending` (the crash rule's input), all archived → `detached`, else `stale`.
107
+ */
108
+ export declare function unrecordedSegments(store: Store, repoId: string, dest: string): UnrecordedSegment[];
109
+ export declare function archiveAlias(segmentId: string): string;
110
+ /**
111
+ * The table prefix a revision's bodies are read under: `""` for a live
112
+ * revision, `"<alias>."` for an archived one whose segment is attached on
113
+ * demand (and verified to be the segment it claims). `RevisionArchived` when
114
+ * the segment is not recorded or not on disk.
115
+ */
116
+ export declare function archivePrefix(store: Store, rev: {
117
+ rev_id: string;
118
+ archive: string | null;
119
+ }): string;
120
+ export interface RevisionBody {
121
+ rev_id: string;
122
+ doc_id: string;
123
+ path: string;
124
+ rendered_hash: Buffer;
125
+ root_tree: Buffer;
126
+ frontmatter_blob: Buffer | null;
127
+ /** `""` or `"<alias>."` — where `tree_nodes` and `blobs` for this revision live. */
128
+ prefix: string;
129
+ archived: boolean;
130
+ }
131
+ /**
132
+ * A revision's row with its bodies resolved: a live revision's from the store,
133
+ * an archived one's from its segment (attached on demand). `undefined` when
134
+ * `revId` is not a revision (of `docId`, when given); `RevisionArchived` when
135
+ * its segment is unavailable.
136
+ */
137
+ export declare function revisionBody(store: Store, revId: string, docId?: string): RevisionBody | undefined;
138
+ export interface SegmentInfo {
139
+ id: string;
140
+ path: string;
141
+ seq_lo: number;
142
+ seq_hi: number;
143
+ created_at: string;
144
+ revision_count: number;
145
+ /** The file's size on disk. */
146
+ bytes: number;
147
+ previous_segment: string | null;
148
+ }
149
+ export interface SquashResult {
150
+ revisions: number;
151
+ /** Commits every revision of which is archived: their dispositions and block changes moved. */
152
+ commits: number;
153
+ dispositions: number;
154
+ block_changes: number;
155
+ }
156
+ export interface ExportResult {
157
+ plan: RetentionPlan;
158
+ /** null when nothing was to archive (no file written). */
159
+ segment: SegmentInfo | null;
160
+ /** An unrecorded complete segment was finished instead of writing a new one (the crash rule). */
161
+ reused: boolean;
162
+ /** null without `prune`. */
163
+ squashed: SquashResult | null;
164
+ /** null without `prune`. */
165
+ gc: {
166
+ blobs: number;
167
+ tree_nodes: number;
168
+ } | null;
169
+ }
170
+ export interface ExportOptions extends EvaluateOptions {
171
+ /** The directory the segment is written to (created when missing). */
172
+ dest: string;
173
+ /** Squash the store after the segment is durable. */
174
+ prune: boolean;
175
+ }
176
+ /** An unrecorded segment under `dest` names both live and non-live revisions (it predates another squash, or belongs elsewhere). */
177
+ export declare class StaleSegment extends Error {
178
+ readonly path: string;
179
+ readonly rev: string;
180
+ constructor(path: string, rev: string);
181
+ }
182
+ /**
183
+ * `omg history export [--prune]`: evaluate the policy, write ONE new segment
184
+ * for the revisions to archive (or finish an unrecorded one — the crash
185
+ * rule), and with `prune` squash the store. Nothing to archive → no file,
186
+ * `segment: null`.
187
+ */
188
+ export declare function exportHistory(store: Store, repoId: string, opts: ExportOptions): ExportResult;
189
+ /** The archived-history figures the size report carries (spec/store §7 "Size", 15.0). */
190
+ export declare function archivedSummary(store: Store): {
191
+ revisions: number;
192
+ segments: number;
193
+ bytes: number;
194
+ };
195
+ /** The file name of a segment path, for messages and fixtures. */
196
+ export declare function segmentBasename(path: string): string;