@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
@@ -1,9 +1,13 @@
1
- // Collect the set of tree-node hashes reachable from every revision root, and
2
- // the blob hashes referenced by those nodes (raw + trivia) plus frontmatter.
3
- function markReachable(store) {
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. Because
39
- * revisions are append-only and never pruned in v1, nothing is actually
40
- * unreachable unless revision pruning ran first — so that half is a no-op in a
41
- * default v1 repo, exactly as specified. Rebuild-equivalence is preserved.
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
- let treeNodesSwept = 0;
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
- db.prepare("DELETE FROM block_changes").run();
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;