@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.
- package/dist/src/core/ids.d.ts +1 -1
- package/dist/src/core/read/document.d.ts +4 -1
- package/dist/src/core/read/document.js +18 -6
- package/dist/src/core/store/archive.d.ts +196 -0
- package/dist/src/core/store/archive.js +606 -0
- package/dist/src/core/store/gc.d.ts +25 -4
- package/dist/src/core/store/gc.js +43 -28
- package/dist/src/core/store/rebuild.d.ts +2 -0
- package/dist/src/core/store/rebuild.js +8 -2
- package/dist/src/core/store/retention.d.ts +138 -0
- package/dist/src/core/store/retention.js +409 -0
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +30 -4
- package/dist/src/core/store/size.d.ts +21 -2
- package/dist/src/core/store/size.js +24 -3
- package/dist/src/core/store/store.d.ts +1 -0
- package/dist/src/core/store/store.js +53 -1
- package/dist/src/graph/history.d.ts +2 -0
- package/dist/src/graph/history.js +8 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +4 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +24 -6
- package/dist/src/mcp/server.js +2 -2
- package/dist/src/oqx-js/context.js +21 -5
- package/dist/src/oqx-js/dates.d.ts +105 -0
- package/dist/src/oqx-js/dates.js +427 -0
- package/dist/src/oqx-js/names.js +14 -3
- package/dist/src/oqx-js/planner.js +17 -2
- package/dist/src/oqx-js/run.d.ts +1 -0
- package/dist/src/oqx-js/run.js +66 -1
- package/dist/src/spec-versions.d.ts +4 -4
- package/dist/src/spec-versions.js +3 -3
- package/package.json +2 -2
package/dist/src/core/ids.d.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
205
|
+
out.push(blobAt(Buffer.from(rawHashHex, "hex")));
|
|
194
206
|
if (triviaHashHex)
|
|
195
|
-
out.push(
|
|
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;
|