@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
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The mark (spec/store §7): every tree node reachable from a live revision's
|
|
3
|
+
* `root_tree` (walking `child_tree_hash_hex`) and every blob those nodes name
|
|
4
|
+
* (raw + trivia) plus every live `frontmatter_blob`. An archived revision
|
|
5
|
+
* (`root_tree IS NULL`, 15.0) contributes nothing — its bodies live in its
|
|
6
|
+
* segment.
|
|
7
|
+
*/
|
|
8
|
+
export function markReachable(store) {
|
|
4
9
|
const trees = new Set();
|
|
5
10
|
const blobs = new Set();
|
|
6
|
-
// Frontmatter blobs referenced by revisions.
|
|
7
11
|
const fmRows = store.db.prepare("SELECT DISTINCT frontmatter_blob FROM revisions WHERE frontmatter_blob IS NOT NULL").all();
|
|
8
12
|
for (const r of fmRows)
|
|
9
13
|
blobs.add(r.frontmatter_blob.toString("hex"));
|
|
@@ -24,21 +28,50 @@ function markReachable(store) {
|
|
|
24
28
|
walk(childHashHex);
|
|
25
29
|
}
|
|
26
30
|
};
|
|
27
|
-
const roots = store.db.prepare("SELECT DISTINCT root_tree FROM revisions").all();
|
|
31
|
+
const roots = store.db.prepare("SELECT DISTINCT root_tree FROM revisions WHERE root_tree IS NOT NULL").all();
|
|
28
32
|
for (const r of roots)
|
|
29
33
|
walk(r.root_tree.toString("hex"));
|
|
30
34
|
return { trees, blobs };
|
|
31
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The sweep: delete every `tree_nodes` and `blobs` row the mark did not reach
|
|
38
|
+
* (count only when `dry`). Runs in the caller's transaction — `runGc` and the
|
|
39
|
+
* retention squash both call it.
|
|
40
|
+
*/
|
|
41
|
+
export function sweepUnreachable(store, dry = false) {
|
|
42
|
+
const db = store.db;
|
|
43
|
+
const { trees, blobs } = markReachable(store);
|
|
44
|
+
let treeNodesSwept = 0;
|
|
45
|
+
const allTrees = db.prepare("SELECT hash FROM tree_nodes").all();
|
|
46
|
+
const delTree = db.prepare("DELETE FROM tree_nodes WHERE hash = ?");
|
|
47
|
+
for (const t of allTrees)
|
|
48
|
+
if (!trees.has(t.hash.toString("hex"))) {
|
|
49
|
+
if (!dry)
|
|
50
|
+
delTree.run(t.hash);
|
|
51
|
+
treeNodesSwept++;
|
|
52
|
+
}
|
|
53
|
+
let blobsSwept = 0;
|
|
54
|
+
const allBlobs = db.prepare("SELECT hash FROM blobs").all();
|
|
55
|
+
const delBlob = db.prepare("DELETE FROM blobs WHERE hash = ?");
|
|
56
|
+
for (const b of allBlobs)
|
|
57
|
+
if (!blobs.has(b.hash.toString("hex"))) {
|
|
58
|
+
if (!dry)
|
|
59
|
+
delBlob.run(b.hash);
|
|
60
|
+
blobsSwept++;
|
|
61
|
+
}
|
|
62
|
+
return { blobsSwept, treeNodesSwept };
|
|
63
|
+
}
|
|
32
64
|
/** The `same` rows a store through 14.1 persisted (spec/store §7, 14.2). */
|
|
33
65
|
export function countSameDispositions(store) {
|
|
34
66
|
return store.db.prepare("SELECT count(*) AS c FROM dispositions WHERE kind = 'same'").get().c;
|
|
35
67
|
}
|
|
36
68
|
/**
|
|
37
69
|
* Mark-and-sweep GC (flag-gated; enabled must be true to run — v1.0 ships it
|
|
38
|
-
* off). Removes blobs/tree_nodes unreachable from any revision root.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
70
|
+
* off). Removes blobs/tree_nodes unreachable from any live revision root. In a
|
|
71
|
+
* store that never archived, nothing is unreachable — revisions are append-only
|
|
72
|
+
* — so that half is a no-op; after a retention squash (15.0) the squash has
|
|
73
|
+
* already swept, so it is a no-op again unless a crash interrupted one.
|
|
74
|
+
* Rebuild-equivalence is preserved.
|
|
42
75
|
*
|
|
43
76
|
* spec/store §7 (14.2): the same sweep deletes every `same` row of
|
|
44
77
|
* `dispositions` and `block_changes` — what a 14.1 store wrote for every
|
|
@@ -51,26 +84,8 @@ export function runGc(store, opts = {}) {
|
|
|
51
84
|
if (!opts.enabled)
|
|
52
85
|
return { blobsSwept: 0, treeNodesSwept: 0, sameDispositionsSwept: 0 };
|
|
53
86
|
return store.write((db) => {
|
|
54
|
-
const { trees, blobs } = markReachable(store);
|
|
55
87
|
const dry = Boolean(opts.dryRun);
|
|
56
|
-
|
|
57
|
-
const allTrees = db.prepare("SELECT hash FROM tree_nodes").all();
|
|
58
|
-
const delTree = db.prepare("DELETE FROM tree_nodes WHERE hash = ?");
|
|
59
|
-
for (const t of allTrees)
|
|
60
|
-
if (!trees.has(t.hash.toString("hex"))) {
|
|
61
|
-
if (!dry)
|
|
62
|
-
delTree.run(t.hash);
|
|
63
|
-
treeNodesSwept++;
|
|
64
|
-
}
|
|
65
|
-
let blobsSwept = 0;
|
|
66
|
-
const allBlobs = db.prepare("SELECT hash FROM blobs").all();
|
|
67
|
-
const delBlob = db.prepare("DELETE FROM blobs WHERE hash = ?");
|
|
68
|
-
for (const b of allBlobs)
|
|
69
|
-
if (!blobs.has(b.hash.toString("hex"))) {
|
|
70
|
-
if (!dry)
|
|
71
|
-
delBlob.run(b.hash);
|
|
72
|
-
blobsSwept++;
|
|
73
|
-
}
|
|
88
|
+
const { blobsSwept, treeNodesSwept } = sweepUnreachable(store, dry);
|
|
74
89
|
const sameDispositionsSwept = countSameDispositions(store);
|
|
75
90
|
if (!dry && sameDispositionsSwept > 0) {
|
|
76
91
|
db.prepare("DELETE FROM block_changes WHERE kind = 'same'").run();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { Store } from "./store.js";
|
|
2
2
|
export type RebuildTarget = "sections" | "edges" | "fts" | "block_changes" | "all";
|
|
3
|
+
/** The commit ids every revision of which is archived (spec/store §7 "Retention", 15.0): a commit with at least one revision and no live one. */
|
|
4
|
+
export declare const ARCHIVED_COMMITS_SQL = "SELECT c.commit_id FROM commits c WHERE EXISTS (SELECT 1 FROM revisions r WHERE r.commit_id = c.commit_id) AND NOT EXISTS (SELECT 1 FROM revisions r WHERE r.commit_id = c.commit_id AND r.archive IS NULL)";
|
|
3
5
|
export declare function rebuildIndex(store: Store, target?: RebuildTarget): void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { rebuildSections } from "./sections.js";
|
|
2
2
|
import { rebuildDocEdges } from "./edges.js";
|
|
3
3
|
import { ftsRebuild } from "./fts.js";
|
|
4
|
+
/** The commit ids every revision of which is archived (spec/store §7 "Retention", 15.0): a commit with at least one revision and no live one. */
|
|
5
|
+
export const ARCHIVED_COMMITS_SQL = "SELECT c.commit_id FROM commits c WHERE EXISTS (SELECT 1 FROM revisions r WHERE r.commit_id = c.commit_id) AND NOT EXISTS (SELECT 1 FROM revisions r WHERE r.commit_id = c.commit_id AND r.archive IS NULL)";
|
|
4
6
|
export function rebuildIndex(store, target = "all") {
|
|
5
7
|
store.write((db) => {
|
|
6
8
|
const docIds = db.prepare("SELECT doc_id FROM docs WHERE deleted_commit IS NULL").all().map((r) => r.doc_id);
|
|
@@ -21,8 +23,12 @@ export function rebuildIndex(store, target = "all") {
|
|
|
21
23
|
}
|
|
22
24
|
if (target === "block_changes" || target === "all") {
|
|
23
25
|
// block_changes is a projection of dispositions across commits: rebuild
|
|
24
|
-
// it verbatim from the dispositions table (which is durable)
|
|
25
|
-
|
|
26
|
+
// it verbatim from the dispositions table (which is durable) — for every
|
|
27
|
+
// commit that still has dispositions. A wholly archived commit (spec/store
|
|
28
|
+
// §7 "Retention", 15.0: every revision of it carries `archive`) has none;
|
|
29
|
+
// its surviving block_changes rows are each block's earliest, kept for the
|
|
30
|
+
// mint oracle, and are left exactly as they are.
|
|
31
|
+
db.exec(`DELETE FROM block_changes WHERE commit_id NOT IN (${ARCHIVED_COMMITS_SQL})`);
|
|
26
32
|
db.exec("INSERT OR IGNORE INTO block_changes (block_id, commit_id, kind) SELECT block_id, commit_id, kind FROM dispositions");
|
|
27
33
|
}
|
|
28
34
|
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
/** `one per <period>`: the ladder's thinning grain. */
|
|
3
|
+
export type LadderKeep = "one per day" | "one per week" | "one per month" | "one per year";
|
|
4
|
+
export declare const LADDER_KEEPS: readonly LadderKeep[];
|
|
5
|
+
export interface LadderRung {
|
|
6
|
+
/** The rung applies to a revision whose age in whole days is at least this. */
|
|
7
|
+
after_days: number;
|
|
8
|
+
keep: LadderKeep;
|
|
9
|
+
}
|
|
10
|
+
/** A per-path override: the keys given replace the base policy's for the documents the glob matches. */
|
|
11
|
+
export interface RetentionOverride {
|
|
12
|
+
glob: string;
|
|
13
|
+
keep_days?: number;
|
|
14
|
+
per_doc_max?: number;
|
|
15
|
+
ladder?: LadderRung[];
|
|
16
|
+
keep_origins?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface RetentionPolicy {
|
|
19
|
+
/** Every revision younger than this many days is kept. */
|
|
20
|
+
keep_days: number;
|
|
21
|
+
/** Beyond `keep_days`, one revision per period per rung (sorted by `after_days`). */
|
|
22
|
+
ladder: LadderRung[];
|
|
23
|
+
/** The newest N revisions of a document are kept regardless of age. */
|
|
24
|
+
per_doc_max: number;
|
|
25
|
+
/** A revision whose commit has one of these origins AND a non-empty reason is kept. */
|
|
26
|
+
keep_origins: string[];
|
|
27
|
+
overrides: RetentionOverride[];
|
|
28
|
+
}
|
|
29
|
+
/** 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. */
|
|
30
|
+
export declare const DEFAULT_RETENTION: RetentionPolicy;
|
|
31
|
+
/** The settings key the policy lives under (`history.retention`, deep-merged over the defaults). */
|
|
32
|
+
export declare const RETENTION_SETTINGS_KEY = "history";
|
|
33
|
+
/** The repo-settings key naming the last segment this repo recorded (`history.last_segment`). */
|
|
34
|
+
export declare const LAST_SEGMENT_SETTINGS_KEY = "last_segment";
|
|
35
|
+
export declare class RetentionPolicyError extends Error {
|
|
36
|
+
constructor(message: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A partial policy (a settings blob, a `--policy` argument) over `base`: every
|
|
40
|
+
* key given replaces the base's; a malformed value is `RetentionPolicyError`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mergeRetentionPolicy(base: RetentionPolicy, partial: unknown, at?: string): RetentionPolicy;
|
|
43
|
+
/** A settings blob as stored (`workspace_settings.settings`, `repos.settings`): an object, or `{}` for anything else. */
|
|
44
|
+
export declare function parseSettingsJson(json: string | undefined | null): Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* The repo's effective settings (`spec/sync` settings: the workspace layer
|
|
47
|
+
* with the repo's own deep-merged over it at the leaf). Inlined here rather
|
|
48
|
+
* than imported from `sync/settings.ts` — `core/` does not import its
|
|
49
|
+
* siblings — and kept to the rule that module states.
|
|
50
|
+
*/
|
|
51
|
+
export declare function effectiveSettings(store: Store, repoId: string): Record<string, unknown>;
|
|
52
|
+
/** The repo's effective policy: the defaults, the settings' `history.retention` over them, then `override` (a CLI `--policy`). */
|
|
53
|
+
export declare function retentionPolicy(store: Store, repoId: string, override?: unknown): RetentionPolicy;
|
|
54
|
+
/**
|
|
55
|
+
* An override glob against a reference path: `**` any run of characters,
|
|
56
|
+
* `*` any run without `/`, `?` one character, everything else literal; a glob
|
|
57
|
+
* without a leading `/` is rooted (`projects/**` ≡ `/projects/**`).
|
|
58
|
+
*/
|
|
59
|
+
export declare function globToRegExp(glob: string): RegExp;
|
|
60
|
+
/** The policy for one document: the first matching override's keys over the base. */
|
|
61
|
+
export declare function policyFor(base: RetentionPolicy, referencePathOfDoc: string): RetentionPolicy;
|
|
62
|
+
/** Whole days between a commit's `ts` and `now` (negative when the commit is in the future). */
|
|
63
|
+
export declare function ageDays(ts: string, nowMs: number): number;
|
|
64
|
+
/** The period key of a timestamp under a rung's grain (UTC; a week starts on Monday). */
|
|
65
|
+
export declare function periodKey(ts: string, keep: LadderKeep): string;
|
|
66
|
+
export interface RetentionDocPlan {
|
|
67
|
+
id: string;
|
|
68
|
+
/** The document's current path, reference form. */
|
|
69
|
+
path: string;
|
|
70
|
+
/** Live (non-archived) revisions of the document before the plan. */
|
|
71
|
+
revisions: number;
|
|
72
|
+
/** Revision ids kept, oldest first. */
|
|
73
|
+
keep: string[];
|
|
74
|
+
/** Revision ids to archive, oldest first. */
|
|
75
|
+
archive: string[];
|
|
76
|
+
/** Bytes of blobs and tree nodes reachable from the archived revisions and from no kept revision of the store. */
|
|
77
|
+
bytes: number;
|
|
78
|
+
}
|
|
79
|
+
export interface RetentionTotals {
|
|
80
|
+
/** Documents with at least one revision to archive. */
|
|
81
|
+
docs: number;
|
|
82
|
+
/** Live revisions of the repository before the plan. */
|
|
83
|
+
revisions: number;
|
|
84
|
+
keep: number;
|
|
85
|
+
archive: number;
|
|
86
|
+
/** Bytes reachable only from the archived revisions, over their union. */
|
|
87
|
+
bytes: number;
|
|
88
|
+
}
|
|
89
|
+
export interface RetentionPlan {
|
|
90
|
+
policy: RetentionPolicy;
|
|
91
|
+
now: string;
|
|
92
|
+
before: string | null;
|
|
93
|
+
/** Only the documents with something to archive, by path. */
|
|
94
|
+
docs: RetentionDocPlan[];
|
|
95
|
+
totals: RetentionTotals;
|
|
96
|
+
}
|
|
97
|
+
export interface EvaluateOptions {
|
|
98
|
+
/** The clock; `new Date()` when absent (the spec seam pins it). */
|
|
99
|
+
now?: string;
|
|
100
|
+
/** Only revisions committed strictly before this instant are candidates. */
|
|
101
|
+
before?: string;
|
|
102
|
+
/** The policy in effect; `retentionPolicy(store, repoId)` when absent. */
|
|
103
|
+
policy?: RetentionPolicy;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The sizes of the blobs and tree nodes of a store, and the walk that marks
|
|
107
|
+
* what a set of revisions reaches (spec/store §7: the GC mark, run twice — once
|
|
108
|
+
* from what stays, once from what goes — to cost a plan).
|
|
109
|
+
*/
|
|
110
|
+
export declare class ReachMarker {
|
|
111
|
+
private readonly store;
|
|
112
|
+
private readonly treeNode;
|
|
113
|
+
private readonly blobSize;
|
|
114
|
+
private readonly treeSizes;
|
|
115
|
+
private readonly blobSizes;
|
|
116
|
+
constructor(store: Store);
|
|
117
|
+
/** Mark every tree node and blob reachable from the given roots and frontmatter blobs into `trees`/`blobs` (hex). */
|
|
118
|
+
mark(revs: {
|
|
119
|
+
root_tree: Buffer | null;
|
|
120
|
+
frontmatter_blob: Buffer | null;
|
|
121
|
+
}[], trees: Set<string>, blobs: Set<string>): void;
|
|
122
|
+
/** Bytes of the given tree nodes (their `entries`) and blobs (`size`). */
|
|
123
|
+
bytesOf(trees: Iterable<string>, blobs: Iterable<string>): number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The plan (spec/store §7 "Retention", the policy as an exact rule). Per
|
|
127
|
+
* document, over its live revisions oldest → newest: a revision is KEPT when
|
|
128
|
+
* any rule keeps it — it is the document's current revision (a tombstoned
|
|
129
|
+
* document's last revision is its current one), a `pending_materializations`
|
|
130
|
+
* row names it, it is younger than `keep_days`, it is among the newest
|
|
131
|
+
* `per_doc_max`, its commit's origin is in `keep_origins` with a non-empty
|
|
132
|
+
* reason, it is not before `before`, no ladder rung applies to its age (the
|
|
133
|
+
* gap below the first rung keeps everything), or it is the newest revision of
|
|
134
|
+
* its rung's period; every other revision is ARCHIVED. Bytes are the GC mark
|
|
135
|
+
* from the archived revisions minus the mark from every revision that stays
|
|
136
|
+
* live in the store (all repositories — blobs and tree nodes are shared).
|
|
137
|
+
*/
|
|
138
|
+
export declare function evaluateRetention(store: Store, repoId: string, opts?: EvaluateOptions): RetentionPlan;
|