@omgbase/core 0.15.0 → 0.21.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 (47) hide show
  1. package/dist/src/core/ingest.d.ts +2 -0
  2. package/dist/src/core/ingest.js +13 -0
  3. package/dist/src/core/read/document.d.ts +9 -8
  4. package/dist/src/core/read/document.js +0 -2
  5. package/dist/src/core/store/gc.d.ts +13 -1
  6. package/dist/src/core/store/gc.js +24 -5
  7. package/dist/src/core/store/size.d.ts +79 -0
  8. package/dist/src/core/store/size.js +232 -0
  9. package/dist/src/index.d.ts +4 -1
  10. package/dist/src/index.js +5 -1
  11. package/dist/src/mcp/arguments.d.ts +10 -2
  12. package/dist/src/mcp/arguments.js +37 -3
  13. package/dist/src/mcp/errors.d.ts +1 -1
  14. package/dist/src/mcp/reference.d.ts +1 -1
  15. package/dist/src/mcp/reference.js +63 -10
  16. package/dist/src/mcp/server.d.ts +34 -1
  17. package/dist/src/mcp/server.js +158 -46
  18. package/dist/src/mutate/canonical.d.ts +27 -0
  19. package/dist/src/mutate/canonical.js +138 -0
  20. package/dist/src/mutate/effects.d.ts +58 -8
  21. package/dist/src/mutate/effects.js +312 -43
  22. package/dist/src/mutate/known-ids.d.ts +19 -0
  23. package/dist/src/mutate/known-ids.js +47 -7
  24. package/dist/src/mutate/ops.d.ts +1 -0
  25. package/dist/src/mutate/ops.js +20 -1
  26. package/dist/src/mutate/plan.d.ts +26 -16
  27. package/dist/src/mutate/plan.js +152 -112
  28. package/dist/src/mutate/select.d.ts +78 -0
  29. package/dist/src/mutate/select.js +903 -0
  30. package/dist/src/mutate/tree.d.ts +2 -2
  31. package/dist/src/oqx-js/context.d.ts +9 -2
  32. package/dist/src/oqx-js/context.js +74 -28
  33. package/dist/src/oqx-js/names.d.ts +40 -0
  34. package/dist/src/oqx-js/names.js +347 -0
  35. package/dist/src/oqx-js/planner.d.ts +3 -3
  36. package/dist/src/oqx-js/planner.js +17 -9
  37. package/dist/src/oqx-js/root-row.d.ts +15 -4
  38. package/dist/src/oqx-js/root-row.js +18 -10
  39. package/dist/src/oqx-js/run.d.ts +45 -1
  40. package/dist/src/oqx-js/run.js +114 -32
  41. package/dist/src/oqx-js/sql/translate.js +1 -1
  42. package/dist/src/spec-versions.d.ts +5 -5
  43. package/dist/src/spec-versions.js +4 -4
  44. package/dist/src/sync/observe.js +9 -3
  45. package/dist/src/version.d.ts +7 -2
  46. package/dist/src/version.js +5 -2
  47. package/package.json +2 -2
@@ -10,6 +10,8 @@ export interface DispositionRow {
10
10
  matcherV: string | null;
11
11
  detail: Record<string, unknown>;
12
12
  }
13
+ /** spec/store §5.4 step 10 (14.2): which verdicts the store persists — every kind but `same`. */
14
+ export declare function persistsDisposition(kind: string): boolean;
13
15
  export interface ResolvedEdgeRow {
14
16
  srcDoc: string;
15
17
  srcBlock: string | null;
@@ -13,6 +13,10 @@ import { flattenFrontmatter, flattenComputed, writeDocProperties } from "./store
13
13
  import { maintainEdges, adoptPhantoms } from "./store/edges.js";
14
14
  import { parse as parseYaml } from "yaml";
15
15
  import { adapterForPath } from "../format/index.js";
16
+ /** spec/store §5.4 step 10 (14.2): which verdicts the store persists — every kind but `same`. */
17
+ export function persistsDisposition(kind) {
18
+ return kind !== "same";
19
+ }
16
20
  /** The parse step of ingest, exported so a batch caller can parse + reconcile
17
21
  * every document of a checkpoint BEFORE committing any (the cross-document
18
22
  * phase needs all per-doc results first) and hand the tree back via
@@ -279,10 +283,19 @@ export function ingestInto(db, repoId, path, content, opts) {
279
283
  const dispositions = resolved.dispositions.length > 0 || opts.resolveIds
280
284
  ? resolved.dispositions
281
285
  : rows.map((r) => ({ blockId: r.blockId, kind: "inserted", confidence: null, reason: null, matcherV: null, detail: {} }));
286
+ // spec/store §5.4 step 10 (14.2): a `same` verdict is not persisted. The
287
+ // matcher (and the known-id resolver) still produce one per untouched block
288
+ // — the outcome counts them — but the table records what CHANGED, so a
289
+ // block's `updated_at` (MAX commit ts over its `block_changes`) and its
290
+ // `blocks_history` are the block's own changes, not its siblings'. Every
291
+ // other kind — the first revision's `inserted` rows the mint oracle (§2.1)
292
+ // depends on included — is written as before.
282
293
  const insDisp = db.prepare(`INSERT OR IGNORE INTO dispositions (commit_id, block_id, kind, confidence, reason, matcher_v, detail)
283
294
  VALUES (?, ?, ?, ?, ?, ?, ?)`);
284
295
  const bc = db.prepare("INSERT OR IGNORE INTO block_changes (block_id, commit_id, kind) VALUES (?, ?, ?)");
285
296
  for (const d of dispositions) {
297
+ if (!persistsDisposition(d.kind))
298
+ continue;
286
299
  insDisp.run(commitId, d.blockId, d.kind, d.confidence, d.reason, d.matcherV, JSON.stringify(d.detail));
287
300
  bc.run(d.blockId, commitId, d.kind);
288
301
  }
@@ -76,6 +76,15 @@ export declare function docsReadMany(store: Store, repoId: string, refs: string[
76
76
  includeIds?: boolean;
77
77
  budgetTokens?: number;
78
78
  }): DocsReadManyResult;
79
+ /**
80
+ * `docs_read { at }` / `docs_read_at` (spec/surface §2, 4.7): the document AS
81
+ * OF a revision. No `properties`: the properties table is current-state only
82
+ * (`spec/properties`), and recomputing the bag from the reconstructed bytes is
83
+ * not yet a one-call affair of the format layer (the inline fields come out of
84
+ * the ingest's node projection) — so the time-travel read carries the bytes
85
+ * and nothing it would have to guess at (through 4.6 `docs_read_at` returned
86
+ * the CURRENT bag flagged `propertiesAreCurrent: true`).
87
+ */
79
88
  export interface DocsReadAtResult {
80
89
  id: string;
81
90
  /** The path as of that revision, in the reference form. */
@@ -88,14 +97,6 @@ export interface DocsReadAtResult {
88
97
  * rendered_hash — a byte-faithful round-trip. False flags the fidelity gap
89
98
  * described below (doc-level trivia may differ for an old revision). */
90
99
  renderedHashMatch: boolean;
91
- /**
92
- * Properties as they are CURRENTLY (not at `rev`): the properties table is
93
- * current-state only (properties-table), so historical property values are
94
- * not recoverable. Included as best-effort context, flagged by
95
- * `propertiesAreCurrent: true`.
96
- */
97
- properties: Record<string, Record<string, unknown>>;
98
- propertiesAreCurrent: true;
99
100
  }
100
101
  /**
101
102
  * Whole-document TIME-TRAVEL read: reconstruct a document's full file bytes AS
@@ -204,8 +204,6 @@ export function readDocumentAtRevision(store, docId, revId) {
204
204
  rev: revId,
205
205
  raw: content,
206
206
  renderedHashMatch,
207
- properties: surfaceProperties(docPropertiesGrouped(db, docId)),
208
- propertiesAreCurrent: true,
209
207
  };
210
208
  }
211
209
  //# sourceMappingURL=document.js.map
@@ -2,16 +2,28 @@ import type { Store } from "./store.js";
2
2
  export interface GcResult {
3
3
  blobsSwept: number;
4
4
  treeNodesSwept: number;
5
+ /** spec/store §7 (14.2): `same` rows a 14.1 store wrote into `dispositions`/`block_changes` — history nothing reads. */
6
+ sameDispositionsSwept: number;
5
7
  }
8
+ /** The `same` rows a store through 14.1 persisted (spec/store §7, 14.2). */
9
+ export declare function countSameDispositions(store: Store): number;
6
10
  /**
7
11
  * Mark-and-sweep GC (flag-gated; enabled must be true to run — v1.0 ships it
8
12
  * off). Removes blobs/tree_nodes unreachable from any revision root. Because
9
13
  * revisions are append-only and never pruned in v1, nothing is actually
10
- * unreachable unless revision pruning ran first — so this is a no-op in a
14
+ * unreachable unless revision pruning ran first — so that half is a no-op in a
11
15
  * default v1 repo, exactly as specified. Rebuild-equivalence is preserved.
16
+ *
17
+ * spec/store §7 (14.2): the same sweep deletes every `same` row of
18
+ * `dispositions` and `block_changes` — what a 14.1 store wrote for every
19
+ * untouched block of every commit, and nothing reads (`updated_at` and
20
+ * `blocks_history` are the block's own changes now). Not a migration: a
21
+ * data-only change needs no `user_version`, and opening a database must never
22
+ * bulk-delete. `dryRun` counts everything and deletes nothing.
12
23
  */
13
24
  export declare function runGc(store: Store, opts?: {
14
25
  enabled?: boolean;
26
+ dryRun?: boolean;
15
27
  }): GcResult;
16
28
  /** Resurrection-pool expiry sweep (02 §7): drop rows past their expires_ts.
17
29
  * Runs routinely (lazy, at checkpoint time). */
@@ -29,24 +29,37 @@ function markReachable(store) {
29
29
  walk(r.root_tree.toString("hex"));
30
30
  return { trees, blobs };
31
31
  }
32
+ /** The `same` rows a store through 14.1 persisted (spec/store §7, 14.2). */
33
+ export function countSameDispositions(store) {
34
+ return store.db.prepare("SELECT count(*) AS c FROM dispositions WHERE kind = 'same'").get().c;
35
+ }
32
36
  /**
33
37
  * Mark-and-sweep GC (flag-gated; enabled must be true to run — v1.0 ships it
34
38
  * off). Removes blobs/tree_nodes unreachable from any revision root. Because
35
39
  * revisions are append-only and never pruned in v1, nothing is actually
36
- * unreachable unless revision pruning ran first — so this is a no-op in a
40
+ * unreachable unless revision pruning ran first — so that half is a no-op in a
37
41
  * default v1 repo, exactly as specified. Rebuild-equivalence is preserved.
42
+ *
43
+ * spec/store §7 (14.2): the same sweep deletes every `same` row of
44
+ * `dispositions` and `block_changes` — what a 14.1 store wrote for every
45
+ * untouched block of every commit, and nothing reads (`updated_at` and
46
+ * `blocks_history` are the block's own changes now). Not a migration: a
47
+ * data-only change needs no `user_version`, and opening a database must never
48
+ * bulk-delete. `dryRun` counts everything and deletes nothing.
38
49
  */
39
50
  export function runGc(store, opts = {}) {
40
51
  if (!opts.enabled)
41
- return { blobsSwept: 0, treeNodesSwept: 0 };
52
+ return { blobsSwept: 0, treeNodesSwept: 0, sameDispositionsSwept: 0 };
42
53
  return store.write((db) => {
43
54
  const { trees, blobs } = markReachable(store);
55
+ const dry = Boolean(opts.dryRun);
44
56
  let treeNodesSwept = 0;
45
57
  const allTrees = db.prepare("SELECT hash FROM tree_nodes").all();
46
58
  const delTree = db.prepare("DELETE FROM tree_nodes WHERE hash = ?");
47
59
  for (const t of allTrees)
48
60
  if (!trees.has(t.hash.toString("hex"))) {
49
- delTree.run(t.hash);
61
+ if (!dry)
62
+ delTree.run(t.hash);
50
63
  treeNodesSwept++;
51
64
  }
52
65
  let blobsSwept = 0;
@@ -54,10 +67,16 @@ export function runGc(store, opts = {}) {
54
67
  const delBlob = db.prepare("DELETE FROM blobs WHERE hash = ?");
55
68
  for (const b of allBlobs)
56
69
  if (!blobs.has(b.hash.toString("hex"))) {
57
- delBlob.run(b.hash);
70
+ if (!dry)
71
+ delBlob.run(b.hash);
58
72
  blobsSwept++;
59
73
  }
60
- return { blobsSwept, treeNodesSwept };
74
+ const sameDispositionsSwept = countSameDispositions(store);
75
+ if (!dry && sameDispositionsSwept > 0) {
76
+ db.prepare("DELETE FROM block_changes WHERE kind = 'same'").run();
77
+ db.prepare("DELETE FROM dispositions WHERE kind = 'same'").run();
78
+ }
79
+ return { blobsSwept, treeNodesSwept, sameDispositionsSwept };
61
80
  });
62
81
  }
63
82
  /** Resurrection-pool expiry sweep (02 §7): drop rows past their expires_ts.
@@ -0,0 +1,79 @@
1
+ import type { Store } from "./store.js";
2
+ export declare const SIZE_CATEGORIES: readonly ["content", "history", "search", "sync"];
3
+ export type SizeCategory = (typeof SIZE_CATEGORIES)[number];
4
+ /** spec/store §7: the category of every table `schema.sql` creates. */
5
+ export declare const TABLE_CATEGORY: Readonly<Record<string, SizeCategory>>;
6
+ /** The category of a table: the spec's assignment, `sync` for anything else. */
7
+ export declare function categoryOf(table: string): SizeCategory;
8
+ export interface SizeTable {
9
+ name: string;
10
+ /** Pages of the table and its indexes (and shadow tables), in bytes; null without dbstat. */
11
+ bytes: number | null;
12
+ /** `bytes` over the total, one decimal; null without dbstat. */
13
+ share: number | null;
14
+ rows: number;
15
+ /** The bytes of the table's indexes alone (included in `bytes`); null without dbstat. */
16
+ indexes: number | null;
17
+ }
18
+ export interface SizeCategoryReport {
19
+ name: SizeCategory;
20
+ bytes: number | null;
21
+ share: number | null;
22
+ rows: number;
23
+ /** By bytes desc then name (rows desc then name without dbstat). */
24
+ tables: SizeTable[];
25
+ }
26
+ export interface SizeFile {
27
+ path: string;
28
+ /** The main file's bytes on disk (`page_count * page_size` for an in-memory database). */
29
+ bytes: number;
30
+ /** The `-wal` file's bytes on disk, 0 when absent. */
31
+ wal_bytes: number;
32
+ page_size: number;
33
+ page_count: number;
34
+ freelist_count: number;
35
+ /** `freelist_count * page_size`: what `VACUUM` returns to the filesystem. */
36
+ reclaimable: number;
37
+ }
38
+ export interface SizeReport {
39
+ file: SizeFile;
40
+ /** Always the four categories, in the fixed order content, history, search, sync. */
41
+ categories: SizeCategoryReport[];
42
+ /** spec/store §7 (14.2): `dispositions` rows with `kind = 'same'` — what a store through 14.1 wrote and `omg gc` removes. */
43
+ same_dispositions: number;
44
+ advice: string[];
45
+ /** Present only when the SQLite build lacks `dbstat` (row counts only). */
46
+ detail?: "dbstat unavailable";
47
+ }
48
+ /** The advice thresholds (spec/store §7). */
49
+ export declare const VACUUM_ADVICE_MIN_BYTES: number;
50
+ export declare const VACUUM_ADVICE_MIN_SHARE = 10;
51
+ export declare const HISTORY_ADVICE_MIN_SHARE = 50;
52
+ export declare const SAME_ADVICE_MIN_SHARE = 25;
53
+ export declare const EMBEDDINGS_ADVICE_MIN_SHARE = 25;
54
+ /**
55
+ * `<n> B` under 1024, else one decimal of KB/MB/GB/TB (1024-based), the tenths
56
+ * rounded half up — `4096` → `4.0 KB`, `13734297` → `13.1 MB`. Both engines
57
+ * format from the integer tenths so the bytes never differ by a rounding mode.
58
+ */
59
+ export declare function humanBytes(n: number): string;
60
+ /** A share as `<n>.<d>%` from its one-decimal value (`48.2` → `48.2%`, `50` → `50.0%`). */
61
+ export declare function formatShare(share: number): string;
62
+ /** `bytes / total` in percent, one decimal, rounded half up; 0 when the total is 0. */
63
+ export declare function shareOf(bytes: number, total: number): number;
64
+ /** 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[];
66
+ /** The size report of an open store (spec/store §7 "Size"). */
67
+ export declare function sizeReport(store: Store): SizeReport;
68
+ export interface VacuumResult {
69
+ /** `freelist_count * page_size` before the `VACUUM`: the bytes it returned. */
70
+ reclaimable: number;
71
+ }
72
+ /**
73
+ * `VACUUM` the database (spec/store §7). Not a transaction's work — SQLite
74
+ * refuses a VACUUM inside one — so it runs on the bare connection; returns the
75
+ * free-page bytes measured before, which is what the file shrinks by.
76
+ */
77
+ export declare function vacuum(store: Store): VacuumResult;
78
+ /** `freelist_count * page_size`: the bytes a `VACUUM` would return now. */
79
+ export declare function reclaimableBytes(store: Store): number;
@@ -0,0 +1,232 @@
1
+ import { statSync } from "node:fs";
2
+ // The size report (spec/store §7 "Size", 14.1): what the database is made of,
3
+ // table by table through SQLite's `dbstat` virtual table, grouped into the four
4
+ // categories a reader reasons about — the live state (`content`), the
5
+ // append-only record (`history`), the derived indexes (`search`) and the
6
+ // workspace bookkeeping (`sync`) — plus the file's page accounting and the
7
+ // deterministic advice `omg doctor --size` prints. The categorization is the
8
+ // spec's: every table of `schema.sql` is assigned, an index counts toward its
9
+ // table, an FTS5 virtual table's shadow tables fold into it, and anything the
10
+ // spec does not name (`sqlite_schema`, `sqlite_stat1`, a table from a newer
11
+ // schema) is `sync`.
12
+ export const SIZE_CATEGORIES = ["content", "history", "search", "sync"];
13
+ /** spec/store §7: the category of every table `schema.sql` creates. */
14
+ export const TABLE_CATEGORY = {
15
+ docs: "content",
16
+ blocks: "content",
17
+ sections: "content",
18
+ nodes: "content",
19
+ edges: "content",
20
+ doc_edges: "content",
21
+ inferred_edges: "content",
22
+ external_nodes: "content",
23
+ properties: "content",
24
+ collections: "content",
25
+ blobs: "history",
26
+ tree_nodes: "history",
27
+ revisions: "history",
28
+ commits: "history",
29
+ dispositions: "history",
30
+ block_changes: "history",
31
+ resurrection_pool: "history",
32
+ pending_materializations: "history",
33
+ blocks_fts: "search",
34
+ nodes_fts: "search",
35
+ embeddings: "search",
36
+ doc_embeddings: "search",
37
+ repos: "sync",
38
+ sources: "sync",
39
+ adapters: "sync",
40
+ attachments: "sync",
41
+ checkpoints: "sync",
42
+ file_stats: "sync",
43
+ sync_state: "sync",
44
+ workspace_settings: "sync",
45
+ };
46
+ /** The category of a table: the spec's assignment, `sync` for anything else. */
47
+ export function categoryOf(table) {
48
+ return TABLE_CATEGORY[table] ?? "sync";
49
+ }
50
+ /** The advice thresholds (spec/store §7). */
51
+ export const VACUUM_ADVICE_MIN_BYTES = 1024 * 1024;
52
+ export const VACUUM_ADVICE_MIN_SHARE = 10;
53
+ export const HISTORY_ADVICE_MIN_SHARE = 50;
54
+ export const SAME_ADVICE_MIN_SHARE = 25;
55
+ export const EMBEDDINGS_ADVICE_MIN_SHARE = 25;
56
+ /**
57
+ * `<n> B` under 1024, else one decimal of KB/MB/GB/TB (1024-based), the tenths
58
+ * rounded half up — `4096` → `4.0 KB`, `13734297` → `13.1 MB`. Both engines
59
+ * format from the integer tenths so the bytes never differ by a rounding mode.
60
+ */
61
+ export function humanBytes(n) {
62
+ if (n < 1024)
63
+ return `${n} B`;
64
+ const units = ["KB", "MB", "GB", "TB"];
65
+ let v = n / 1024;
66
+ let u = 0;
67
+ while (v >= 1024 && u < units.length - 1) {
68
+ v /= 1024;
69
+ u++;
70
+ }
71
+ const tenths = Math.round(v * 10);
72
+ return `${Math.floor(tenths / 10)}.${tenths % 10} ${units[u]}`;
73
+ }
74
+ /** A share as `<n>.<d>%` from its one-decimal value (`48.2` → `48.2%`, `50` → `50.0%`). */
75
+ export function formatShare(share) {
76
+ const tenths = Math.round(share * 10);
77
+ return `${Math.floor(tenths / 10)}.${tenths % 10}%`;
78
+ }
79
+ /** `bytes / total` in percent, one decimal, rounded half up; 0 when the total is 0. */
80
+ export function shareOf(bytes, total) {
81
+ if (total <= 0)
82
+ return 0;
83
+ return Math.round((bytes * 1000) / total) / 10;
84
+ }
85
+ /** spec/store §7: the advice lines, in this order; `nothing to reclaim` when none applies. */
86
+ export function sizeAdvice(file, categories, sameDispositions = 0) {
87
+ const advice = [];
88
+ const logical = file.page_count * file.page_size;
89
+ if (file.reclaimable > VACUUM_ADVICE_MIN_BYTES && file.reclaimable * 100 > logical * VACUUM_ADVICE_MIN_SHARE) {
90
+ advice.push(`run \`omg gc --vacuum\` to return ${humanBytes(file.reclaimable)} of free pages to the filesystem`);
91
+ }
92
+ const history = categories.find((c) => c.name === "history");
93
+ if (history && history.share !== null && history.share > HISTORY_ADVICE_MIN_SHARE) {
94
+ advice.push(`history dominates (${formatShare(history.share)}): blobs, tree nodes and dispositions grow with every commit — enable gc (\`omg config set gc.enabled true\`) and run \`omg gc --vacuum\``);
95
+ }
96
+ // 14.2: `same` rows are what a 14.1 store wrote for every untouched block;
97
+ // past a quarter of the table they are worth a sweep (the share is over rows,
98
+ // so it holds with or without dbstat).
99
+ const dispositionRows = history?.tables.find((t) => t.name === "dispositions")?.rows ?? 0;
100
+ if (sameDispositions > 0 && sameDispositions * 100 > dispositionRows * SAME_ADVICE_MIN_SHARE) {
101
+ advice.push(`${sameDispositions} same dispositions are history nothing reads (store 14.2): \`omg gc\` removes them, \`--vacuum\` reclaims the pages`);
102
+ }
103
+ const search = categories.find((c) => c.name === "search");
104
+ if (search && search.bytes !== null) {
105
+ const total = categories.reduce((n, c) => n + (c.bytes ?? 0), 0);
106
+ const embBytes = search.tables.filter((t) => t.name === "embeddings" || t.name === "doc_embeddings").reduce((n, t) => n + (t.bytes ?? 0), 0);
107
+ const embShare = shareOf(embBytes, total);
108
+ if (embShare > EMBEDDINGS_ADVICE_MIN_SHARE) {
109
+ advice.push(`embeddings are ${formatShare(embShare)} of the database: a smaller model (\`embedding.model\` / \`embedding.dim\`) or \`omg embed drain --prune\` after a model switch shrinks them`);
110
+ }
111
+ }
112
+ if (advice.length === 0)
113
+ advice.push("nothing to reclaim");
114
+ return advice;
115
+ }
116
+ /** The `dbstat` bytes per b-tree name, or null when the build lacks the virtual table. */
117
+ function dbstatBytes(store) {
118
+ try {
119
+ const rows = store.db.prepare("SELECT name, sum(pgsize) AS bytes FROM dbstat GROUP BY name").all();
120
+ return new Map(rows.map((r) => [r.name, Number(r.bytes)]));
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ /** The size report of an open store (spec/store §7 "Size"). */
127
+ export function sizeReport(store) {
128
+ const db = store.db;
129
+ const path = db.name === "" ? ":memory:" : db.name;
130
+ const page_size = db.pragma("page_size", { simple: true });
131
+ const page_count = db.pragma("page_count", { simple: true });
132
+ const freelist_count = db.pragma("freelist_count", { simple: true });
133
+ const onDisk = path !== ":memory:";
134
+ const fileBytes = (p) => {
135
+ try {
136
+ return statSync(p).size;
137
+ }
138
+ catch {
139
+ return 0;
140
+ }
141
+ };
142
+ const file = {
143
+ path,
144
+ bytes: onDisk ? fileBytes(path) : page_count * page_size,
145
+ wal_bytes: onDisk ? fileBytes(`${path}-wal`) : 0,
146
+ page_size,
147
+ page_count,
148
+ freelist_count,
149
+ reclaimable: freelist_count * page_size,
150
+ };
151
+ const master = db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_master").all();
152
+ const virtuals = master.filter((r) => r.type === "table" && /^CREATE VIRTUAL TABLE/i.test(r.sql ?? "")).map((r) => r.name);
153
+ // A shadow table (`blocks_fts_data`, …) folds into its virtual table.
154
+ const ownerOf = (name) => virtuals.find((v) => name.startsWith(`${v}_`)) ?? name;
155
+ const stat = dbstatBytes(store);
156
+ // Every table: sqlite_master's (shadow tables folded into their owner), plus
157
+ // the schema table itself, which sqlite_master does not list but dbstat does.
158
+ const tableNames = new Set(["sqlite_schema"]);
159
+ for (const r of master)
160
+ if (r.type === "table")
161
+ tableNames.add(ownerOf(r.name));
162
+ if (stat)
163
+ for (const name of stat.keys())
164
+ if (!master.some((r) => r.name === name && r.type === "index"))
165
+ tableNames.add(ownerOf(name));
166
+ const indexBytes = new Map();
167
+ const tableBytes = new Map();
168
+ if (stat) {
169
+ for (const [name, bytes] of stat) {
170
+ const idx = master.find((r) => r.type === "index" && r.name === name);
171
+ if (idx) {
172
+ indexBytes.set(idx.tbl_name, (indexBytes.get(idx.tbl_name) ?? 0) + bytes);
173
+ tableBytes.set(idx.tbl_name, (tableBytes.get(idx.tbl_name) ?? 0) + bytes);
174
+ }
175
+ else {
176
+ const owner = ownerOf(name);
177
+ tableBytes.set(owner, (tableBytes.get(owner) ?? 0) + bytes);
178
+ }
179
+ }
180
+ }
181
+ const total = stat ? [...stat.values()].reduce((n, b) => n + b, 0) : 0;
182
+ const countRows = (name) => {
183
+ // An external-content FTS5 table answers `count(*)` from its content table;
184
+ // the rows it indexes are its `_docsize` shadow's.
185
+ const from = virtuals.includes(name) ? `${name}_docsize` : name;
186
+ try {
187
+ return db.prepare(`SELECT count(*) AS c FROM "${from.replace(/"/g, '""')}"`).get().c;
188
+ }
189
+ catch {
190
+ return 0;
191
+ }
192
+ };
193
+ const byCategory = new Map(SIZE_CATEGORIES.map((c) => [c, []]));
194
+ for (const name of tableNames) {
195
+ const bytes = stat ? (tableBytes.get(name) ?? 0) : null;
196
+ byCategory.get(categoryOf(name)).push({
197
+ name,
198
+ bytes,
199
+ share: bytes === null ? null : shareOf(bytes, total),
200
+ rows: countRows(name),
201
+ indexes: stat ? (indexBytes.get(name) ?? 0) : null,
202
+ });
203
+ }
204
+ const byBytesThenName = (a, b) => (b.bytes ?? b.rows) - (a.bytes ?? a.rows) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
205
+ const categories = SIZE_CATEGORIES.map((name) => {
206
+ const tables = byCategory.get(name).sort(byBytesThenName);
207
+ const bytes = stat ? tables.reduce((n, t) => n + (t.bytes ?? 0), 0) : null;
208
+ return { name, bytes, share: bytes === null ? null : shareOf(bytes, total), rows: tables.reduce((n, t) => n + t.rows, 0), tables };
209
+ });
210
+ const same_dispositions = db.prepare("SELECT count(*) AS c FROM dispositions WHERE kind = 'same'").get().c;
211
+ const report = { file, categories, same_dispositions, advice: sizeAdvice(file, categories, same_dispositions) };
212
+ if (!stat)
213
+ report.detail = "dbstat unavailable";
214
+ return report;
215
+ }
216
+ /**
217
+ * `VACUUM` the database (spec/store §7). Not a transaction's work — SQLite
218
+ * refuses a VACUUM inside one — so it runs on the bare connection; returns the
219
+ * free-page bytes measured before, which is what the file shrinks by.
220
+ */
221
+ export function vacuum(store) {
222
+ const reclaimable = reclaimableBytes(store);
223
+ store.db.exec("VACUUM");
224
+ return { reclaimable };
225
+ }
226
+ /** `freelist_count * page_size`: the bytes a `VACUUM` would return now. */
227
+ export function reclaimableBytes(store) {
228
+ const page_size = store.db.pragma("page_size", { simple: true });
229
+ const freelist_count = store.db.pragma("freelist_count", { simple: true });
230
+ return freelist_count * page_size;
231
+ }
232
+ //# sourceMappingURL=size.js.map
@@ -28,7 +28,7 @@ export { createExternalSource, type ExternalSourceSpec } from "./sync/external-s
28
28
  export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, type AdapterRow, type SourceRow, } from "./sync/sources.js";
29
29
  export { reconcileChanges, attachSource } from "./sync/driver.js";
30
30
  export { observeFile, observeMany, observeOne, observeDelete, observeBatch, type ObserveResult, type ObserveOneResult, type ObserveDeleteResult, type BatchItem, type BatchOutcome } from "./sync/observe.js";
31
- export { buildServer, WRITE_TOOLS, type ServerContext } from "./mcp/server.js";
31
+ export { buildServer, toolDescriptions, toolNames, unknownTool, UNKNOWN_TOOL_CODE, WRITE_TOOLS, MINIMAL_TOOLS, PROFILES, TOOLS_PROFILES, parseToolsProfile, type ServerContext, type ToolsProfile } from "./mcp/server.js";
32
32
  export { serveStdio, type ServeStdioHandle } from "./mcp/stdio.js";
33
33
  export { processCheckpoint, type CheckpointResult } from "./sync/checkpoint.js";
34
34
  export { reposStatus, syncStatus, reposList, type RepoStatus, type SyncStatus, type DiskStatus, type ReposList } from "./sync/admin.js";
@@ -67,9 +67,12 @@ export { canonicalDocPath, composeFile, splitFrontmatter, planInboundRetarget }
67
67
  export { createEffects, moveDocEffects, deleteDocEffects, setMetaEffects, updateContentEffects, insertEffects, updateBlockEffects, moveBlocksEffects, removeBlocksEffects, splitEffects, mergeEffects, checkEffects, nodeSetEffects, appendEffects, docsAppendEffects, retargetEffects, guardOf, requireAbsentPath, applySugar, createdDoc, docOutcome, moveLinks, type Sugar, type SugarDoc, type SugarRequest, type DocOpOutcome, type MoveLinks, } from "./mutate/sugar.js";
68
68
  export { planEffects, executePlan, executeByDigest, applyEffects, applyEffectsDetailed, substituteSymbols, admitPlan, canonicalJson, planDigest, PLAN_VERSION, PLANNER_V, type Plan, type PlanCore, type PlanOpset, type PlanPreconditions, type PlanPreview, type Receipt, type PlanEffectsRequest, type ExecuteRequest, } from "./mutate/plan.js";
69
69
  export { validateEffects, EFFECT_OPS, type Effect, type EffectTarget, type EffectExpect, type ResolvedEffect, type CreateEntry, type RetargetPreview } from "./mutate/effects.js";
70
+ export { compileSelection, selectionQuery, SELECTION_CEILING, type Selection, type SelectionExpect, type CompiledSelection } from "./mutate/select.js";
71
+ export { oqxRunQuery, reservedAlias } from "./oqx-js/run.js";
70
72
  export { inboundLinksTo, retargetLinksInRaw, type InboundLink } from "./graph/inbound-links.js";
71
73
  export { planUpdate, type PlanUpdateOptions } from "./mutate/plan-update.js";
72
74
  export { opsetKernelOps, renderOpsetPlan, renderIdentityPlan, type IdentityPreview, serializeOpset, parseOpset, type Opset, type PlanOp, type OpsetSummary, type OpsetPrecondition, type PlanDisposition, } from "./mutate/opset.js";
73
75
  export { rebuildIndex, type RebuildTarget } from "./core/store/rebuild.js";
74
76
  export { runGc, type GcResult } from "./core/store/gc.js";
77
+ export { sizeReport, sizeAdvice, vacuum, reclaimableBytes, humanBytes, formatShare, shareOf, categoryOf, TABLE_CATEGORY, SIZE_CATEGORIES, type SizeReport, type SizeCategory, type SizeCategoryReport, type SizeTable, type SizeFile, type VacuumResult, } from "./core/store/size.js";
75
78
  export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
package/dist/src/index.js CHANGED
@@ -28,7 +28,7 @@ export { createExternalSource } from "./sync/external-source.js";
28
28
  export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, } from "./sync/sources.js";
29
29
  export { reconcileChanges, attachSource } from "./sync/driver.js";
30
30
  export { observeFile, observeMany, observeOne, observeDelete, observeBatch } from "./sync/observe.js";
31
- export { buildServer, WRITE_TOOLS } from "./mcp/server.js";
31
+ export { buildServer, toolDescriptions, toolNames, unknownTool, UNKNOWN_TOOL_CODE, WRITE_TOOLS, MINIMAL_TOOLS, PROFILES, TOOLS_PROFILES, parseToolsProfile } from "./mcp/server.js";
32
32
  export { serveStdio } from "./mcp/stdio.js";
33
33
  export { processCheckpoint } from "./sync/checkpoint.js";
34
34
  export { reposStatus, syncStatus, reposList } from "./sync/admin.js";
@@ -78,6 +78,9 @@ export { createEffects, moveDocEffects, deleteDocEffects, setMetaEffects, update
78
78
  // The Effect layer (ADR-021, spec/mutate §11): plan / execute / apply { effects } and the plan's shapes.
79
79
  export { planEffects, executePlan, executeByDigest, applyEffects, applyEffectsDetailed, substituteSymbols, admitPlan, canonicalJson, planDigest, PLAN_VERSION, PLANNER_V, } from "./mutate/plan.js";
80
80
  export { validateEffects, EFFECT_OPS } from "./mutate/effects.js";
81
+ // Query-directed planning (spec/mutate §11.8, 3.1): the selector binding behind `plan`/`apply`/`execute { select }`.
82
+ export { compileSelection, selectionQuery, SELECTION_CEILING } from "./mutate/select.js";
83
+ export { oqxRunQuery, reservedAlias } from "./oqx-js/run.js";
81
84
  // Inbound-link discovery + destination-aware rewrite (docs_move's dangling report / retarget_inbound).
82
85
  export { inboundLinksTo, retargetLinksInRaw } from "./graph/inbound-links.js";
83
86
  // Whole-document update: reconcile → opset (the `set content` effect's internal planner since 3.0; `preview.identity` surfaces it).
@@ -85,5 +88,6 @@ export { planUpdate } from "./mutate/plan-update.js";
85
88
  export { opsetKernelOps, renderOpsetPlan, renderIdentityPlan, serializeOpset, parseOpset, } from "./mutate/opset.js";
86
89
  export { rebuildIndex } from "./core/store/rebuild.js";
87
90
  export { runGc } from "./core/store/gc.js";
91
+ export { sizeReport, sizeAdvice, vacuum, reclaimableBytes, humanBytes, formatShare, shareOf, categoryOf, TABLE_CATEGORY, SIZE_CATEGORIES, } from "./core/store/size.js";
88
92
  export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
89
93
  //# sourceMappingURL=index.js.map
@@ -4,8 +4,16 @@ import { EngineError } from "./errors.js";
4
4
  export type ArgShape = Record<string, z.ZodTypeAny>;
5
5
  /** The `(tool takes …)` tail: names in bytewise order, `?` on the optional ones. */
6
6
  export declare function argumentsHint(tool: string, shape: ArgShape): string;
7
- /** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape. */
8
- export declare function argumentError(tool: string, message: string, shape: ArgShape): EngineError;
7
+ /** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape — and a `fix` (4.5, spec/surface §4) when the corrected call can be spelled. */
8
+ export declare function argumentError(tool: string, message: string, shape: ArgShape, fix?: string): EngineError;
9
+ /** The schema of a string-list argument: an array of strings, or one bare string (normalized to `[s]` by `validateArguments`). */
10
+ export declare function stringList(opts?: {
11
+ min?: number;
12
+ }): z.ZodType<string[]>;
13
+ /** Is this argument a string list (through `optional`/`nullable`)? */
14
+ export declare function isStringList(schema: z.ZodTypeAny): boolean;
15
+ /** The parts of a comma-joined string: split on `,`, trimmed, empty parts dropped. */
16
+ export declare function commaSplit(s: string): string[];
9
17
  /**
10
18
  * Validate `raw` against `shape`: unknown names first (in bytewise order), then
11
19
  * each declared argument (bytewise order) for presence and type. Returns the
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { EngineError } from "./errors.js";
3
+ import { canonicalJson } from "../mutate/canonical.js";
3
4
  /** The `(tool takes …)` tail: names in bytewise order, `?` on the optional ones. */
4
5
  export function argumentsHint(tool, shape) {
5
6
  const names = Object.keys(shape).sort(cmpBytes);
@@ -7,10 +8,33 @@ export function argumentsHint(tool, shape) {
7
8
  return `${tool} takes no arguments`;
8
9
  return `${tool} takes ${names.map((n) => (isOptional(shape[n]) ? `${n}?` : n)).join(", ")}`;
9
10
  }
10
- /** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape. */
11
- export function argumentError(tool, message, shape) {
11
+ /** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape — and a `fix` (4.5, spec/surface §4) when the corrected call can be spelled. */
12
+ export function argumentError(tool, message, shape, fix) {
12
13
  const full = `${message} (${argumentsHint(tool, shape)})`;
13
- return new EngineError("filter_invalid", full, { data: { reason: full, hint: "arguments" } });
14
+ return new EngineError("filter_invalid", full, { data: { reason: full, hint: "arguments", ...(fix !== undefined ? { fix } : {}) } });
15
+ }
16
+ // ---- string lists (spec/surface §4 "Arguments", 4.5) ---------------------------------
17
+ //
18
+ // Every list-of-strings argument (`docs`, `ids`, `roots`, `blocks`, `select`,
19
+ // `unset`) also takes ONE bare string as a one-element list — the first guess
20
+ // of a caller who has one ref. A comma-joined string is still "must be an array
21
+ // of strings", but that error carries a `fix`: the same call with the string
22
+ // split on commas (each part trimmed, empty parts dropped), canonical JSON.
23
+ const STRING_LISTS = new WeakSet();
24
+ /** The schema of a string-list argument: an array of strings, or one bare string (normalized to `[s]` by `validateArguments`). */
25
+ export function stringList(opts = {}) {
26
+ const list = opts.min !== undefined ? z.array(z.string()).min(opts.min) : z.array(z.string());
27
+ const schema = z.union([list, z.string()]);
28
+ STRING_LISTS.add(schema);
29
+ return schema;
30
+ }
31
+ /** Is this argument a string list (through `optional`/`nullable`)? */
32
+ export function isStringList(schema) {
33
+ return STRING_LISTS.has(unwrap(schema));
34
+ }
35
+ /** The parts of a comma-joined string: split on `,`, trimmed, empty parts dropped. */
36
+ export function commaSplit(s) {
37
+ return s.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
14
38
  }
15
39
  /**
16
40
  * Validate `raw` against `shape`: unknown names first (in bytewise order), then
@@ -36,6 +60,14 @@ export function validateArguments(tool, shape, raw) {
36
60
  throw argumentError(tool, `\`${key}\` is required`, shape);
37
61
  continue;
38
62
  }
63
+ if (isStringList(schema) && typeof value === "string") {
64
+ // (4.5) one bare string is a one-element list; a comma-joined one is the
65
+ // array error with the split list as its `fix`.
66
+ if (value.includes(","))
67
+ throw argumentError(tool, `\`${key}\` must be an array of strings`, shape, canonicalJson({ ...args, [key]: commaSplit(value) }));
68
+ out[key] = [value];
69
+ continue;
70
+ }
39
71
  const parsed = schema.safeParse(value);
40
72
  if (!parsed.success)
41
73
  throw argumentError(tool, issueMessage(key, schema, parsed.error.issues), shape);
@@ -141,6 +173,8 @@ function unwrap(schema) {
141
173
  }
142
174
  /** The expected-type phrase of an argument, as the Rust catalog spells it from its JSON schema. */
143
175
  export function phraseFor(schema) {
176
+ if (isStringList(schema))
177
+ return "an array of strings";
144
178
  const d = defOf(unwrap(schema));
145
179
  switch (d.type) {
146
180
  case "string": return "a string";
@@ -1,4 +1,4 @@
1
- export type ErrorCode = "stale_expectation" | "parent_missing" | "target_missing" | "block_missing" | "doc_missing" | "cycle_move" | "opaque_block" | "not_contiguous" | "type_mismatch" | "conflicted_document" | "path_taken" | "create_conflict" | "ambiguous_locator" | "ambiguous_heading" | "filter_invalid" | "budget_exceeded" | "provider_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved" | "block_not_container" | "stale_plan" | "plan_not_convergent" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch";
1
+ export type ErrorCode = "stale_expectation" | "parent_missing" | "target_missing" | "block_missing" | "doc_missing" | "cycle_move" | "opaque_block" | "not_contiguous" | "type_mismatch" | "conflicted_document" | "path_taken" | "create_conflict" | "ambiguous_locator" | "ambiguous_heading" | "filter_invalid" | "budget_exceeded" | "provider_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved" | "block_not_container" | "stale_plan" | "plan_not_convergent" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "ambiguous_target" | "selection_too_large";
2
2
  export interface EngineErrorBody {
3
3
  error: ErrorCode;
4
4
  message: string;