@omgbase/core 0.4.0 → 0.4.1

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.
@@ -2,17 +2,48 @@ export type IdPrefix = "d" | "b" | "c" | "r" | "x" | "col" | "cp" | "e" | "rp" |
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;
5
- /** Install a replacement minter (null restores the CSPRNG default). */
5
+ /**
6
+ * An in-use oracle (spec/store §2.1): does `id` name a row of the prefix's
7
+ * table(s)? A `Store` registers one when it opens and unregisters it on close.
8
+ * `col`/`v` have no table; an oracle answers false for them.
9
+ */
10
+ export type IdOracle = (prefix: IdPrefix, id: string) => boolean;
11
+ /**
12
+ * Consecutive rejections after which a mint gives up. The CSPRNG cannot hit
13
+ * this (32⁷ candidates against at most millions in use); only a minter that
14
+ * can never produce a fresh id — a broken fixture minter — does, and it must
15
+ * fail loudly rather than spin forever.
16
+ */
17
+ export declare const MINT_GIVE_UP_AFTER = 1000;
18
+ /** Install a replacement minter (null restores the CSPRNG default). Starts a fresh issued set. */
6
19
  export declare function setIdMinter(minter: IdMinter | null): void;
7
- /** Run `body` with `minter` installed, restoring the previous minter afterwards (also on throw). */
20
+ /** Run `body` with `minter` installed, restoring the previous minter (and its issued set) afterwards (also on throw). */
8
21
  export declare function withIdMinter<T>(minter: IdMinter, body: () => T): T;
22
+ /**
23
+ * Register an in-use oracle (spec/store §2.1); returns the unregister handle.
24
+ * Every open `Store` holds one, so a mint is checked against every database
25
+ * this process has open — a false "in use" from another store costs one redraw.
26
+ */
27
+ export declare function registerIdOracle(oracle: IdOracle): () => void;
9
28
  /**
10
29
  * The fixture minter (spec/store §2.2): a sequential per-prefix counter —
11
30
  * `d_0, d_1, …`, `b_0, …`, each prefix counting from 0 independently. A fresh
12
31
  * instance per case resets every counter.
13
32
  */
14
33
  export declare function sequentialMinter(): IdMinter;
15
- /** Mint an ID with the given prefix. Collision checking is the store's job. */
34
+ /**
35
+ * The repeating fixture minter (spec/store §9.4, `"minter": "repeat"`): the
36
+ * sequential counter whose every id is offered twice — `rp_0, rp_0, b_0, b_0,
37
+ * b_1, b_1, …`. The store rejects the second offering as in use and asks
38
+ * again, so a case run under it projects exactly as under `sequentialMinter`.
39
+ */
40
+ export declare function repeatingMinter(): IdMinter;
41
+ /**
42
+ * Mint an id with the given prefix (spec/store §2.1 "Uniqueness at mint"):
43
+ * draw a candidate — from the installed minter, else the CSPRNG — and redraw
44
+ * while it is in use (issued in this process, or present in a registered
45
+ * store). Gives up after `MINT_GIVE_UP_AFTER` consecutive rejections.
46
+ */
16
47
  export declare function mintId(prefix: IdPrefix): string;
17
48
  export declare function isValidId(id: string, prefix?: IdPrefix): boolean;
18
49
  export declare function prefixOf(id: string): string | null;
@@ -8,6 +8,13 @@ export function randomSuffix(len = ID_LEN) {
8
8
  s += ALPHABET[randomInt(ALPHABET.length)];
9
9
  return s;
10
10
  }
11
+ /**
12
+ * Consecutive rejections after which a mint gives up. The CSPRNG cannot hit
13
+ * this (32⁷ candidates against at most millions in use); only a minter that
14
+ * can never produce a fresh id — a broken fixture minter — does, and it must
15
+ * fail loudly rather than spin forever.
16
+ */
17
+ export const MINT_GIVE_UP_AFTER = 1_000;
11
18
  // The installed minter, or null for production (CSPRNG). spec/store §2.2: the
12
19
  // store's tree hashes contain block ids, so a fixture runner installs a
13
20
  // deterministic per-prefix counter here to make ids comparable across
@@ -15,21 +22,47 @@ export function randomSuffix(len = ID_LEN) {
15
22
  // the reconcile module's phase 7 mints), commits, revisions, repos, sources —
16
23
  // goes through `mintId`, so one seam covers them all.
17
24
  let installed = null;
18
- /** Install a replacement minter (null restores the CSPRNG default). */
25
+ // Ids issued by the production minter in this process: a candidate already
26
+ // here is redrawn without asking any oracle. Bounded by the number of ids the
27
+ // process mints (a 360k-block ingest is a few tens of MB at most).
28
+ const productionIssued = new Set();
29
+ // Ids issued while a replacement minter is installed. A *fresh* set per
30
+ // install: a fixture case runs on a fresh database, so what the previous case
31
+ // minted must not reject this one's `d_0` — sequential ids stay byte-identical
32
+ // across cases. The repeating minter's second offering of an id is caught
33
+ // here (the row may not be written yet, so the oracle alone would miss it).
34
+ let installedIssued = new Set();
35
+ const oracles = new Set();
36
+ /** Install a replacement minter (null restores the CSPRNG default). Starts a fresh issued set. */
19
37
  export function setIdMinter(minter) {
20
38
  installed = minter;
39
+ installedIssued = new Set();
21
40
  }
22
- /** Run `body` with `minter` installed, restoring the previous minter afterwards (also on throw). */
41
+ /** Run `body` with `minter` installed, restoring the previous minter (and its issued set) afterwards (also on throw). */
23
42
  export function withIdMinter(minter, body) {
24
43
  const previous = installed;
44
+ const previousIssued = installedIssued;
25
45
  installed = minter;
46
+ installedIssued = new Set();
26
47
  try {
27
48
  return body();
28
49
  }
29
50
  finally {
30
51
  installed = previous;
52
+ installedIssued = previousIssued;
31
53
  }
32
54
  }
55
+ /**
56
+ * Register an in-use oracle (spec/store §2.1); returns the unregister handle.
57
+ * Every open `Store` holds one, so a mint is checked against every database
58
+ * this process has open — a false "in use" from another store costs one redraw.
59
+ */
60
+ export function registerIdOracle(oracle) {
61
+ oracles.add(oracle);
62
+ return () => {
63
+ oracles.delete(oracle);
64
+ };
65
+ }
33
66
  /**
34
67
  * The fixture minter (spec/store §2.2): a sequential per-prefix counter —
35
68
  * `d_0, d_1, …`, `b_0, …`, each prefix counting from 0 independently. A fresh
@@ -43,11 +76,51 @@ export function sequentialMinter() {
43
76
  return `${prefix}_${n}`;
44
77
  };
45
78
  }
46
- /** Mint an ID with the given prefix. Collision checking is the store's job. */
79
+ /**
80
+ * The repeating fixture minter (spec/store §9.4, `"minter": "repeat"`): the
81
+ * sequential counter whose every id is offered twice — `rp_0, rp_0, b_0, b_0,
82
+ * b_1, b_1, …`. The store rejects the second offering as in use and asks
83
+ * again, so a case run under it projects exactly as under `sequentialMinter`.
84
+ */
85
+ export function repeatingMinter() {
86
+ const next = sequentialMinter();
87
+ const pending = new Map();
88
+ return (prefix) => {
89
+ const again = pending.get(prefix);
90
+ if (again !== undefined) {
91
+ pending.delete(prefix);
92
+ return again;
93
+ }
94
+ const id = next(prefix);
95
+ pending.set(prefix, id);
96
+ return id;
97
+ };
98
+ }
99
+ function inUse(prefix, id, issued) {
100
+ if (issued.has(id))
101
+ return true;
102
+ for (const oracle of oracles)
103
+ if (oracle(prefix, id))
104
+ return true;
105
+ return false;
106
+ }
107
+ /**
108
+ * Mint an id with the given prefix (spec/store §2.1 "Uniqueness at mint"):
109
+ * draw a candidate — from the installed minter, else the CSPRNG — and redraw
110
+ * while it is in use (issued in this process, or present in a registered
111
+ * store). Gives up after `MINT_GIVE_UP_AFTER` consecutive rejections.
112
+ */
47
113
  export function mintId(prefix) {
48
- if (installed)
49
- return installed(prefix);
50
- return `${prefix}_${randomSuffix()}`;
114
+ const minter = installed;
115
+ const issued = minter ? installedIssued : productionIssued;
116
+ for (let rejected = 0; rejected < MINT_GIVE_UP_AFTER; rejected++) {
117
+ const candidate = minter ? minter(prefix) : `${prefix}_${randomSuffix()}`;
118
+ if (inUse(prefix, candidate, issued))
119
+ continue;
120
+ issued.add(candidate);
121
+ return candidate;
122
+ }
123
+ throw new Error(`mintId(${prefix}): ${MINT_GIVE_UP_AFTER} consecutive candidates were already in use — the ${minter ? "installed" : "production"} minter cannot produce a fresh id`);
51
124
  }
52
125
  // Production ids carry exactly ID_LEN suffix characters; the fixture minter
53
126
  // (spec/store §2.2) mints `d_0`, `b_12`, … — one to seven alphabet characters.
@@ -5,6 +5,15 @@ import type { Database } from "better-sqlite3";
5
5
  * it never changes the answer).
6
6
  */
7
7
  export declare const LIVE_LEAF_SQL = "b.deleted_commit IS NULL AND NOT EXISTS (SELECT 1 FROM blocks c WHERE c.doc_id = b.doc_id AND c.parent_block = b.block_id AND c.deleted_commit IS NULL)";
8
+ /**
9
+ * How many rows the FTS index actually holds. `SELECT count(*) FROM blocks_fts`
10
+ * is answered from the external-content table (`blocks`, tombstones included),
11
+ * so the count comes from the index's own `_docsize` shadow table — one row per
12
+ * indexed row. `doctor` compares it to the live leaf count (spec/search §1.1).
13
+ */
14
+ export declare function ftsIndexedRowCount(db: Database): number;
15
+ /** The number of live leaf blocks — the rows the FTS index must hold (spec/search §1.1). */
16
+ export declare function liveLeafCount(db: Database): number;
8
17
  /** Remove a document's live leaf rows from the FTS index (call before deleting
9
18
  * or tombstoning blocks). Only live leaves are indexed (see ftsIndexDoc), so the
10
19
  * delete must target the SAME set: an already-tombstoned block or a container has
@@ -4,6 +4,19 @@
4
4
  * it never changes the answer).
5
5
  */
6
6
  export const LIVE_LEAF_SQL = "b.deleted_commit IS NULL AND NOT EXISTS (SELECT 1 FROM blocks c WHERE c.doc_id = b.doc_id AND c.parent_block = b.block_id AND c.deleted_commit IS NULL)";
7
+ /**
8
+ * How many rows the FTS index actually holds. `SELECT count(*) FROM blocks_fts`
9
+ * is answered from the external-content table (`blocks`, tombstones included),
10
+ * so the count comes from the index's own `_docsize` shadow table — one row per
11
+ * indexed row. `doctor` compares it to the live leaf count (spec/search §1.1).
12
+ */
13
+ export function ftsIndexedRowCount(db) {
14
+ return db.prepare("SELECT count(*) c FROM blocks_fts_docsize").get().c;
15
+ }
16
+ /** The number of live leaf blocks — the rows the FTS index must hold (spec/search §1.1). */
17
+ export function liveLeafCount(db) {
18
+ return db.prepare(`SELECT count(*) c FROM blocks b WHERE ${LIVE_LEAF_SQL}`).get().c;
19
+ }
7
20
  /** Remove a document's live leaf rows from the FTS index (call before deleting
8
21
  * or tombstoning blocks). Only live leaves are indexed (see ftsIndexDoc), so the
9
22
  * delete must target the SAME set: an already-tombstoned block or a container has
@@ -5,7 +5,11 @@ export interface StoreOptions {
5
5
  }
6
6
  export declare class Store {
7
7
  readonly db: Database.Database;
8
+ private readonly unregisterOracle;
9
+ private readonly inUseStmts;
8
10
  constructor(opts: StoreOptions);
11
+ /** spec/store §2.1: does `id` name a row of the prefix's table(s)? */
12
+ private idInUse;
9
13
  private migrate;
10
14
  private migrateV3;
11
15
  private migrateV4;
@@ -3,9 +3,28 @@ import { mkdirSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
  import { DDL, SCHEMA_VERSION, MIGRATIONS, SYNC_DDL } from "./schema.js";
5
5
  import { cosineBytes } from "../vec.js";
6
- import { mintId } from "../ids.js";
6
+ import { mintId, registerIdOracle } from "../ids.js";
7
+ // spec/store §2.1 "Uniqueness at mint": the table(s) and id column a minted
8
+ // id of each prefix must not already name a row of. A block id lives on in
9
+ // history after its `blocks` row is gone, hence `block_changes` and
10
+ // `resurrection_pool`. `col` and `v` have no table and no check.
11
+ const ID_TABLES = {
12
+ d: [["docs", "doc_id"]],
13
+ b: [["blocks", "block_id"], ["block_changes", "block_id"], ["resurrection_pool", "block_id"]],
14
+ c: [["commits", "commit_id"]],
15
+ r: [["revisions", "rev_id"]],
16
+ x: [["external_nodes", "node_id"]],
17
+ e: [["edges", "edge_id"]],
18
+ cp: [["checkpoints", "id"]],
19
+ rp: [["repos", "repo_id"]],
20
+ src: [["sources", "source_id"]],
21
+ };
7
22
  export class Store {
8
23
  db;
24
+ // The in-use oracle this store registered with `mintId` (spec/store §2.1);
25
+ // `close()` calls it to unregister. Prepared statements cached per prefix.
26
+ unregisterOracle;
27
+ inUseStmts = new Map();
9
28
  constructor(opts) {
10
29
  if (opts.path !== ":memory:")
11
30
  mkdirSync(dirname(opts.path), { recursive: true });
@@ -17,6 +36,24 @@ export class Store {
17
36
  // scalar. Deterministic (same inputs → same score) so SQLite may cache it.
18
37
  this.db.function("cosine", { deterministic: true }, (a, b) => cosineBytes(a, b));
19
38
  this.migrate();
39
+ // Registered after the migration so every table the oracle reads exists.
40
+ // (A migration's own mints — v13's `src_` per repo — are checked only
41
+ // against the process's issued set; they land in tables that were empty.)
42
+ this.unregisterOracle = registerIdOracle((prefix, id) => this.idInUse(prefix, id));
43
+ }
44
+ /** spec/store §2.1: does `id` name a row of the prefix's table(s)? */
45
+ idInUse(prefix, id) {
46
+ if (!this.db.open)
47
+ return false;
48
+ let stmts = this.inUseStmts.get(prefix);
49
+ if (!stmts) {
50
+ stmts = (ID_TABLES[prefix] ?? []).map(([table, column]) => this.db.prepare(`SELECT 1 FROM ${table} WHERE ${column} = ? LIMIT 1`));
51
+ this.inUseStmts.set(prefix, stmts);
52
+ }
53
+ for (const stmt of stmts)
54
+ if (stmt.get(id) !== undefined)
55
+ return true;
56
+ return false;
20
57
  }
21
58
  migrate() {
22
59
  const current = this.db.pragma("user_version", { simple: true });
@@ -158,6 +195,8 @@ export class Store {
158
195
  return this.db.pragma(name, { simple: true });
159
196
  }
160
197
  close() {
198
+ this.unregisterOracle();
199
+ this.inUseStmts.clear();
161
200
  this.db.close();
162
201
  }
163
202
  }
@@ -1,6 +1,17 @@
1
1
  export type { FormatAdapter, AdapterEdge, ProjectedNode, EmbeddingChunk, ReconcileHints, } from "./adapter.js";
2
2
  export { AdapterCapability } from "./adapter.js";
3
3
  export { registerAdapter, adapterForFormat, adapterForPath, registeredFormats, detectFormat, } from "./registry.js";
4
- export { markdownAdapter, MARKDOWN_FORMAT } from "./markdown.js";
4
+ export { markdownAdapter, MARKDOWN_FORMAT, MARKDOWN_CONTAINER_KINDS } from "./markdown.js";
5
5
  export { yamlAdapter, YAML_FORMAT } from "./yaml.js";
6
6
  export { jsonAdapter, JSON_FORMAT } from "./json.js";
7
+ /**
8
+ * May this block take children (spec/mutate §1.1)? Markdown decides by kind —
9
+ * the parser only ever nests under `list`, `list_item`, `task`, `blockquote`
10
+ * and `table`, so an empty `children` on a heading or paragraph is structural,
11
+ * not "an empty container". The other formats have no kind table for this yet:
12
+ * a block that has children is a container, a childless one a leaf.
13
+ */
14
+ export declare function isContainerBlock(format: string, block: {
15
+ type: string;
16
+ children: readonly unknown[];
17
+ }): boolean;
@@ -1,9 +1,22 @@
1
1
  // Format adapter system. Importing this module registers the built-in adapters.
2
2
  export { AdapterCapability } from "./adapter.js";
3
3
  export { registerAdapter, adapterForFormat, adapterForPath, registeredFormats, detectFormat, } from "./registry.js";
4
- export { markdownAdapter, MARKDOWN_FORMAT } from "./markdown.js";
4
+ export { markdownAdapter, MARKDOWN_FORMAT, MARKDOWN_CONTAINER_KINDS } from "./markdown.js";
5
5
  export { yamlAdapter, YAML_FORMAT } from "./yaml.js";
6
6
  export { jsonAdapter, JSON_FORMAT } from "./json.js";
7
+ import { MARKDOWN_FORMAT, MARKDOWN_CONTAINER_KINDS } from "./markdown.js";
8
+ /**
9
+ * May this block take children (spec/mutate §1.1)? Markdown decides by kind —
10
+ * the parser only ever nests under `list`, `list_item`, `task`, `blockquote`
11
+ * and `table`, so an empty `children` on a heading or paragraph is structural,
12
+ * not "an empty container". The other formats have no kind table for this yet:
13
+ * a block that has children is a container, a childless one a leaf.
14
+ */
15
+ export function isContainerBlock(format, block) {
16
+ if (format === MARKDOWN_FORMAT)
17
+ return MARKDOWN_CONTAINER_KINDS.has(block.type);
18
+ return block.children.length > 0;
19
+ }
7
20
  // Register built-in adapters.
8
21
  import { registerAdapter } from "./registry.js";
9
22
  import { markdownAdapter } from "./markdown.js";
@@ -1,5 +1,12 @@
1
1
  import { type FormatAdapter } from "./adapter.js";
2
2
  import { assertFullCoverage } from "../core/parse/tree.js";
3
3
  export declare const MARKDOWN_FORMAT = "markdown";
4
+ /**
5
+ * The Markdown block kinds whose `children` the parser produces (spec/format;
6
+ * `core/parse/parse.ts` CONTAINER_TYPES on the mdast side: list → list_item /
7
+ * task, blockquote, table → table_row). Every other kind is a leaf: its raw IS
8
+ * its whole content, so nothing may be placed inside it (spec/mutate §1.1).
9
+ */
10
+ export declare const MARKDOWN_CONTAINER_KINDS: ReadonlySet<string>;
4
11
  export declare const markdownAdapter: FormatAdapter;
5
12
  export { assertFullCoverage };
@@ -4,6 +4,13 @@ import { parseTree, assertFullCoverage } from "../core/parse/tree.js";
4
4
  import { render } from "../core/parse/render.js";
5
5
  import { extractFromBlock, extractFromFrontmatter, ownText, scanText, INLINE_FIELD_BRACKETED, INLINE_FIELD_LINE } from "../graph/extract.js";
6
6
  export const MARKDOWN_FORMAT = "markdown";
7
+ /**
8
+ * The Markdown block kinds whose `children` the parser produces (spec/format;
9
+ * `core/parse/parse.ts` CONTAINER_TYPES on the mdast side: list → list_item /
10
+ * task, blockquote, table → table_row). Every other kind is a leaf: its raw IS
11
+ * its whole content, so nothing may be placed inside it (spec/mutate §1.1).
12
+ */
13
+ export const MARKDOWN_CONTAINER_KINDS = new Set(["list", "list_item", "task", "blockquote", "table"]);
7
14
  function toAdapterEdge(e) {
8
15
  return {
9
16
  srcBlock: e.srcBlock,
@@ -28,6 +28,27 @@ export declare function diffBlocks(store: Store, docId: string, fromRev: string,
28
28
  /** `diff_unified` (spec/surface §3): a unified diff of two revisions' rendered
29
29
  * texts — each revision's live raws joined by `\n` — via {@link unifiedDiff}. */
30
30
  export declare function diffUnified(store: Store, docId: string, fromRev: string, toRev: string): string;
31
+ /** The `diff_unified` tool's result (spec/surface §3): the resolved revision pair and the diff. */
32
+ export interface DocDiffUnified {
33
+ doc: string;
34
+ path: string;
35
+ from: string;
36
+ to: string;
37
+ diff: string;
38
+ }
39
+ /**
40
+ * `diff_unified` over a resolved document: `to` defaults to the current revision,
41
+ * `from` to the one before it (or the current one again when there is only one:
42
+ * an empty diff). Shared by the MCP tool and `omg diff` so the shape cannot
43
+ * drift. `label` is how the caller named the document, for the error message.
44
+ * A document with no revision at all is `target_missing`; an unknown `rev` is
45
+ * `RevisionNotFound` (→ `target_missing` through `errorBody`).
46
+ */
47
+ export declare function docDiffUnified(store: Store, docId: string, opts?: {
48
+ fromRev?: string;
49
+ toRev?: string;
50
+ label?: string;
51
+ }): DocDiffUnified;
31
52
  /** How each text becomes lines: `split("\n")` exactly — no trimming, no
32
53
  * dropping of a trailing empty element (a raw ending in `\n` yields one) — with
33
54
  * a single special case: the empty text has *no* lines (an empty file is zero
@@ -1,5 +1,6 @@
1
1
  import { findDocByRef } from "../core/read/reader.js";
2
2
  import { isValidId } from "../core/ids.js";
3
+ import { EngineError } from "../mcp/errors.js";
3
4
  /** history_node: a block's biography from block_changes + commits (06 §3). */
4
5
  export function historyNode(store, blockId, opts = {}) {
5
6
  const limit = opts.limit ?? 100;
@@ -78,6 +79,25 @@ export function diffUnified(store, docId, fromRev, toRev) {
78
79
  const rendered = (rev) => [...blocksAtRevision(store, docId, rev).values()].join("\n");
79
80
  return unifiedDiff(rendered(fromRev), rendered(toRev));
80
81
  }
82
+ /**
83
+ * `diff_unified` over a resolved document: `to` defaults to the current revision,
84
+ * `from` to the one before it (or the current one again when there is only one:
85
+ * an empty diff). Shared by the MCP tool and `omg diff` so the shape cannot
86
+ * drift. `label` is how the caller named the document, for the error message.
87
+ * A document with no revision at all is `target_missing`; an unknown `rev` is
88
+ * `RevisionNotFound` (→ `target_missing` through `errorBody`).
89
+ */
90
+ export function docDiffUnified(store, docId, opts = {}) {
91
+ const revs = store.db
92
+ .prepare("SELECT rev_id FROM revisions WHERE doc_id = ? ORDER BY seq DESC LIMIT 2")
93
+ .all(docId);
94
+ const to = opts.toRev ?? revs[0]?.rev_id;
95
+ const from = opts.fromRev ?? revs[1]?.rev_id ?? revs[0]?.rev_id;
96
+ if (!to || !from)
97
+ throw new EngineError("target_missing", `no revisions to diff for ${opts.label ?? docId}`);
98
+ const path = store.db.prepare("SELECT path FROM docs WHERE doc_id = ?").get(docId)?.path ?? "";
99
+ return { doc: docId, path, from, to, diff: diffUnified(store, docId, from, to) };
100
+ }
81
101
  // ---- unified diff (spec/surface §3) ------------------------------------------
82
102
  // A line-grain unified diff with a deterministic Myers script, so both engines
83
103
  // (this reference and crates/omgbase-surface/src/history.rs) produce the same
@@ -232,14 +252,19 @@ export function changesSince(store, repoId, opts = {}) {
232
252
  params.push(opts.origin);
233
253
  params.push(limit + 1);
234
254
  const commits = store.db
235
- .prepare(`SELECT commit_id, seq, ts, origin, actor FROM commits WHERE repo_id = ? AND seq > ? ${originClause} ORDER BY seq LIMIT ?`)
255
+ .prepare(`SELECT commit_id, seq, ts, origin, actor, reason FROM commits WHERE repo_id = ? AND seq > ? ${originClause} ORDER BY seq LIMIT ?`)
236
256
  .all(...params);
237
257
  const truncated = commits.length > limit;
238
258
  const page = commits.slice(0, limit);
239
259
  const digests = page.map((c) => {
240
260
  const revs = store.db.prepare("SELECT r.doc_id AS doc, r.path AS path, r.rendered_hash AS rendered_hash FROM revisions r WHERE r.commit_id = ?").all(c.commit_id).map((r) => ({ doc: r.doc, path: r.path, contentHash: r.rendered_hash.toString("hex") }));
241
261
  const dispCounts = store.db.prepare("SELECT kind, count(*) n FROM dispositions WHERE commit_id = ? GROUP BY kind").all(c.commit_id);
242
- const summary = renderSummary(c.origin, c.actor, revs, dispCounts);
262
+ // A deletion or a move writes no revision: the tombstone names the commit
263
+ // (`docs.deleted_commit`), a move only its reason (`move <from> -> <to>`).
264
+ const deleted = revs.length === 0
265
+ ? store.db.prepare("SELECT path FROM docs WHERE deleted_commit = ? ORDER BY path").all(c.commit_id).map((r) => r.path)
266
+ : [];
267
+ const summary = renderSummary(c.origin, c.actor, revs, dispCounts, { deleted, moved: revs.length === 0 ? parseMove(c.reason) : null });
243
268
  return { commit: c.commit_id, seq: c.seq, ts: c.ts, origin: c.origin, actor: c.actor, summary, revisions: revs };
244
269
  });
245
270
  const nextCursor = page.length > 0 ? page[page.length - 1].seq : cursor;
@@ -333,12 +358,29 @@ function resolveDocRow(store, repoId, ref, includeDeleted) {
333
358
  : store.db.prepare("SELECT doc_id, path, current_rev, deleted_commit FROM docs WHERE repo_id = ? AND path = ?");
334
359
  return (asId ? stmt.get(ref) : stmt.get(repoId, ref));
335
360
  }
336
- function renderSummary(origin, actor, revs, disp) {
337
- const paths = revs.map((r) => r.path).join(", ");
361
+ /** The `reason` a document move records (`mutate/docs.ts`): `move <from> -> <to>`. */
362
+ function parseMove(reason) {
363
+ const m = reason === null ? null : /^move (.+) -> (.+)$/.exec(reason);
364
+ return m ? { from: m[1], to: m[2] } : null;
365
+ }
366
+ /**
367
+ * One line per commit: `<origin>[(<actor>)]: <paths> — <n> <kind>, …`. A
368
+ * commit that touched no revision — a document deletion (observed or api) or a
369
+ * move — says so instead: `deleted <path>` / `moved <from> → <to>`
370
+ * (spec/cli §6 `log`; the summary is unpinned by spec/surface).
371
+ */
372
+ function renderSummary(origin, actor, revs, disp, extra = { deleted: [], moved: null }) {
373
+ let subject = revs.map((r) => r.path).join(", ");
374
+ if (revs.length === 0) {
375
+ if (extra.deleted.length > 0)
376
+ subject = `deleted ${extra.deleted.join(", ")}`;
377
+ else if (extra.moved)
378
+ subject = `moved ${extra.moved.from} → ${extra.moved.to}`;
379
+ }
338
380
  const parts = disp.filter((d) => d.kind !== "same").map((d) => `${d.n} ${d.kind}`);
339
381
  const detail = parts.length > 0 ? ` — ${parts.join(", ")}` : "";
340
382
  if (origin === "api" || origin === "import")
341
- return `${origin}(${actor ?? "?"}): ${paths}${detail}`;
342
- return `observed: ${paths}${detail}`;
383
+ return `${origin}(${actor ?? "?"}): ${subject}${detail}`;
384
+ return `observed: ${subject}${detail}`;
343
385
  }
344
386
  //# sourceMappingURL=history.js.map
@@ -8,6 +8,8 @@ export interface LinkEdge {
8
8
  count: number;
9
9
  /** sample source block ids (out-edges only) */
10
10
  samples?: string[];
11
+ /** block-grain (`blocks: true`): the source block of this edge (null for a frontmatter edge) */
12
+ block?: string | null;
11
13
  }
12
14
  export interface LinksResult {
13
15
  out: LinkEdge[];
@@ -8,12 +8,12 @@ export function docLinks(store, docId, opts = {}) {
8
8
  if (dir === "out" || dir === "both") {
9
9
  if (opts.blocks) {
10
10
  const rows = store.db
11
- .prepare(`SELECT predicate, dst_node, dst_kind FROM edges
12
- WHERE src_doc = ? AND to_commit IS NULL ORDER BY predicate, dst_node`)
11
+ .prepare(`SELECT predicate, dst_node, dst_kind, src_block FROM edges
12
+ WHERE src_doc = ? AND to_commit IS NULL ORDER BY predicate, dst_node, src_block`)
13
13
  .all(docId);
14
14
  for (const r of rows)
15
15
  if (inPred(r.predicate))
16
- out.push({ predicate: r.predicate, node: r.dst_node, kind: r.dst_kind, count: 1 });
16
+ out.push({ predicate: r.predicate, node: r.dst_node, kind: r.dst_kind, count: 1, block: r.src_block });
17
17
  }
18
18
  else {
19
19
  const rows = store.db
@@ -28,14 +28,25 @@ export function docLinks(store, docId, opts = {}) {
28
28
  }
29
29
  }
30
30
  if (dir === "in" || dir === "both") {
31
- const rows = store.db
32
- .prepare(`SELECT predicate, src_doc, count(*) AS cnt FROM edges
33
- WHERE dst_node = ? AND to_commit IS NULL
34
- GROUP BY predicate, src_doc ORDER BY predicate, src_doc`)
35
- .all(docId);
36
- for (const r of rows)
37
- if (inPred(r.predicate))
38
- inbound.push({ predicate: r.predicate, node: r.src_doc, kind: "document", count: r.cnt });
31
+ if (opts.blocks) {
32
+ const rows = store.db
33
+ .prepare(`SELECT predicate, src_doc, src_block FROM edges
34
+ WHERE dst_node = ? AND to_commit IS NULL ORDER BY predicate, src_doc, src_block`)
35
+ .all(docId);
36
+ for (const r of rows)
37
+ if (inPred(r.predicate))
38
+ inbound.push({ predicate: r.predicate, node: r.src_doc, kind: "document", count: 1, block: r.src_block });
39
+ }
40
+ else {
41
+ const rows = store.db
42
+ .prepare(`SELECT predicate, src_doc, count(*) AS cnt FROM edges
43
+ WHERE dst_node = ? AND to_commit IS NULL
44
+ GROUP BY predicate, src_doc ORDER BY predicate, src_doc`)
45
+ .all(docId);
46
+ for (const r of rows)
47
+ if (inPred(r.predicate))
48
+ inbound.push({ predicate: r.predicate, node: r.src_doc, kind: "document", count: r.cnt });
49
+ }
39
50
  }
40
51
  return { out, in: inbound };
41
52
  }
@@ -28,7 +28,7 @@ export { observeFile, observeMany, observeOne, observeDelete, observeBatch, type
28
28
  export { buildServer, type ServerContext } from "./mcp/server.js";
29
29
  export { serveStdio, type ServeStdioHandle } from "./mcp/stdio.js";
30
30
  export { processCheckpoint, type CheckpointResult } from "./sync/checkpoint.js";
31
- export { reposStatus, syncStatus, type RepoStatus, type SyncStatus, type DiskStatus } from "./sync/admin.js";
31
+ export { reposStatus, syncStatus, reposList, type RepoStatus, type SyncStatus, type DiskStatus, type ReposList } from "./sync/admin.js";
32
32
  export { docsOutline, type OutlineResult, type OutlineOptions } from "./core/read/outline.js";
33
33
  export { docsRead, reconstructContent, type DocsReadResult, type DocsReadOptions } from "./core/read/document.js";
34
34
  export { nodesGet, nodesGetMany, type GetNode, type Resolution } from "./core/read/nodes.js";
@@ -38,18 +38,18 @@ export { resolveRef, type ResolvedRef } from "./core/read/refs.js";
38
38
  export { resolve, type ResolveHit, type ResolveInput } from "./search/resolve.js";
39
39
  export { oqxRun, oqxRunAsync, collectSemanticPhrases, parseOqx, type OqxResult, type OqxHit, type OqxOptions, type EmbedQuery } from "./oqx/run.js";
40
40
  export { EmbeddingWorker, SemanticUnavailable, contextPrefix, shouldEmbed, estimateTokens, type EmbeddingProvider, type EmbedTask, type DocEmbedTask, type DocEmbedBlockRef, type DocEmbedMethod, type DocVectorRow, } from "./search/embeddings.js";
41
- export { buildEmbedTasks, buildDocEmbedTasks } from "./search/tasks.js";
41
+ export { buildEmbedTasks, buildDocEmbedTasks, staleEmbedCount } from "./search/tasks.js";
42
42
  export { EmbedDrainer, type DrainerOptions } from "./search/drain.js";
43
43
  export { embeddingSettings, type EmbeddingSettings } from "./search/provider.js";
44
44
  export { createExternalProvider, type ExternalProvider } from "./search/external.js";
45
45
  export { hybridSearch, type HybridHit, type HybridInput } from "./search/rrf.js";
46
46
  export { vectorSearch, docVectorSearch, type VectorHit, type DocVectorHit } from "./search/vector.js";
47
- export { historyNode, diffUnified, unifiedDiff, changesSince, type NodeChange, type CommitDigest } from "./graph/history.js";
47
+ export { historyNode, diffUnified, docDiffUnified, diffBlocks, unifiedDiff, changesSince, RevisionNotFound, type NodeChange, type CommitDigest, type DocDiffUnified, type DiffEntry } from "./graph/history.js";
48
48
  export { docLinks, type LinksResult, type LinkEdge, type LinksOptions } from "./graph/links.js";
49
49
  export { FilterInvalid } from "./search/cel/parser.js";
50
- export { EngineError, type ErrorCode } from "./mcp/errors.js";
50
+ export { EngineError, errorBody, type ErrorCode, type ErrorBody } from "./mcp/errors.js";
51
51
  export { MutationError } from "./mutate/tree.js";
52
- export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter, type IdMinter, type IdPrefix } from "./core/ids.js";
52
+ export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter, repeatingMinter, registerIdOracle, type IdMinter, type IdOracle, type IdPrefix } from "./core/ids.js";
53
53
  export { pinClock, withClock } from "./core/clock.js";
54
54
  export { sha256 } from "./core/hash.js";
55
55
  export { ingestFile, type IngestResult } from "./core/ingest.js";
@@ -63,3 +63,4 @@ export { planUpdate, applyOpset, docsUpdate, type PlanUpdateOptions, type ApplyO
63
63
  export { opsetKernelOps, renderOpsetPlan, serializeOpset, parseOpset, type Opset, type PlanOp, type OpsetSummary, type OpsetPrecondition, type PlanDisposition, } from "./mutate/opset.js";
64
64
  export { rebuildIndex, type RebuildTarget } from "./core/store/rebuild.js";
65
65
  export { runGc, type GcResult } from "./core/store/gc.js";
66
+ export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
package/dist/src/index.js CHANGED
@@ -28,7 +28,7 @@ export { observeFile, observeMany, observeOne, observeDelete, observeBatch } fro
28
28
  export { buildServer } from "./mcp/server.js";
29
29
  export { serveStdio } from "./mcp/stdio.js";
30
30
  export { processCheckpoint } from "./sync/checkpoint.js";
31
- export { reposStatus, syncStatus } from "./sync/admin.js";
31
+ export { reposStatus, syncStatus, reposList } from "./sync/admin.js";
32
32
  // Read surface consumed by CLI read commands (11 §5.2–5.5).
33
33
  export { docsOutline } from "./core/read/outline.js";
34
34
  export { docsRead, reconstructContent } from "./core/read/document.js";
@@ -43,18 +43,18 @@ export { oqxRun, oqxRunAsync, collectSemanticPhrases, parseOqx } from "./oqx/run
43
43
  // Embeddings + semantic retrieval (05 §5–6). The provider is a plugin loaded by
44
44
  // package name; core carries no ML dependency.
45
45
  export { EmbeddingWorker, SemanticUnavailable, contextPrefix, shouldEmbed, estimateTokens, } from "./search/embeddings.js";
46
- export { buildEmbedTasks, buildDocEmbedTasks } from "./search/tasks.js";
46
+ export { buildEmbedTasks, buildDocEmbedTasks, staleEmbedCount } from "./search/tasks.js";
47
47
  export { EmbedDrainer } from "./search/drain.js";
48
48
  export { embeddingSettings } from "./search/provider.js";
49
49
  export { createExternalProvider } from "./search/external.js";
50
50
  export { hybridSearch } from "./search/rrf.js";
51
51
  export { vectorSearch, docVectorSearch } from "./search/vector.js";
52
- export { historyNode, diffUnified, unifiedDiff, changesSince } from "./graph/history.js";
52
+ export { historyNode, diffUnified, docDiffUnified, diffBlocks, unifiedDiff, changesSince, RevisionNotFound } from "./graph/history.js";
53
53
  export { docLinks } from "./graph/links.js";
54
54
  export { FilterInvalid } from "./search/cel/parser.js";
55
- export { EngineError } from "./mcp/errors.js";
55
+ export { EngineError, errorBody } from "./mcp/errors.js";
56
56
  export { MutationError } from "./mutate/tree.js";
57
- export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter } from "./core/ids.js";
57
+ export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter, repeatingMinter, registerIdOracle } from "./core/ids.js";
58
58
  // The clock seam (spec/surface §7.1): `omg mcp` pins the process clock under OMGBASE_SPEC_CLOCK.
59
59
  export { pinClock, withClock } from "./core/clock.js";
60
60
  export { sha256 } from "./core/hash.js";
@@ -73,4 +73,5 @@ export { planUpdate, applyOpset, docsUpdate, } from "./mutate/plan-update.js";
73
73
  export { opsetKernelOps, renderOpsetPlan, serializeOpset, parseOpset, } from "./mutate/opset.js";
74
74
  export { rebuildIndex } from "./core/store/rebuild.js";
75
75
  export { runGc } from "./core/store/gc.js";
76
+ export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
76
77
  //# sourceMappingURL=index.js.map
@@ -15,3 +15,11 @@ export declare class EngineError extends Error {
15
15
  });
16
16
  body(): EngineErrorBody;
17
17
  }
18
+ /** The rendered body of any thrown error (`error` is the code). */
19
+ export interface ErrorBody {
20
+ error: string;
21
+ message: string;
22
+ data?: unknown;
23
+ retriable: boolean;
24
+ }
25
+ export declare function errorBody(err: unknown): ErrorBody;