@omgbase/core 0.1.0 → 0.3.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 (84) hide show
  1. package/README.md +172 -0
  2. package/dist/src/cli/eval-matcher.d.ts +6 -1
  3. package/dist/src/cli/eval-matcher.js +14 -2
  4. package/dist/src/core/clock.d.ts +9 -0
  5. package/dist/src/core/clock.js +46 -0
  6. package/dist/src/core/hash.d.ts +25 -1
  7. package/dist/src/core/hash.js +110 -10
  8. package/dist/src/core/ids.d.ts +12 -0
  9. package/dist/src/core/ids.js +43 -1
  10. package/dist/src/core/ingest.d.ts +22 -2
  11. package/dist/src/core/ingest.js +92 -18
  12. package/dist/src/core/parse/parse.d.ts +1 -1
  13. package/dist/src/core/parse/parse.js +31 -9
  14. package/dist/src/core/store/nodes.d.ts +13 -0
  15. package/dist/src/core/store/nodes.js +24 -0
  16. package/dist/src/core/store/writers.d.ts +6 -0
  17. package/dist/src/core/store/writers.js +1 -0
  18. package/dist/src/core/utf8.d.ts +29 -0
  19. package/dist/src/core/utf8.js +96 -0
  20. package/dist/src/format/adapter.d.ts +7 -1
  21. package/dist/src/format/markdown.js +36 -41
  22. package/dist/src/graph/extract.d.ts +41 -1
  23. package/dist/src/graph/extract.js +59 -1
  24. package/dist/src/graph/history.d.ts +39 -1
  25. package/dist/src/graph/history.js +154 -20
  26. package/dist/src/index.d.ts +10 -6
  27. package/dist/src/index.js +11 -6
  28. package/dist/src/mcp/graph.js +8 -4
  29. package/dist/src/mcp/reference.d.ts +1 -1
  30. package/dist/src/mcp/reference.js +17 -6
  31. package/dist/src/mcp/server.js +15 -10
  32. package/dist/src/mutate/apply.d.ts +9 -0
  33. package/dist/src/mutate/apply.js +24 -11
  34. package/dist/src/mutate/docs.d.ts +2 -0
  35. package/dist/src/mutate/docs.js +10 -6
  36. package/dist/src/mutate/known-ids.js +11 -3
  37. package/dist/src/mutate/macros.js +9 -2
  38. package/dist/src/mutate/ops.d.ts +5 -4
  39. package/dist/src/mutate/ops.js +62 -15
  40. package/dist/src/mutate/plan-update.d.ts +7 -1
  41. package/dist/src/mutate/plan-update.js +3 -1
  42. package/dist/src/oqx-js/context.js +57 -7
  43. package/dist/src/reconcile/eval/generator.d.ts +24 -1
  44. package/dist/src/reconcile/eval/generator.js +248 -0
  45. package/dist/src/reconcile/eval/metrics.d.ts +33 -9
  46. package/dist/src/reconcile/eval/metrics.js +55 -32
  47. package/dist/src/reconcile/flatten.js +7 -5
  48. package/dist/src/reconcile/phase5.d.ts +2 -1
  49. package/dist/src/reconcile/phase5.js +27 -10
  50. package/dist/src/reconcile/phase6a.js +17 -5
  51. package/dist/src/reconcile/phases.d.ts +44 -6
  52. package/dist/src/reconcile/phases.js +0 -0
  53. package/dist/src/reconcile/reconcile.js +16 -9
  54. package/dist/src/reconcile/types.d.ts +4 -1
  55. package/dist/src/reconcile/types.js +11 -2
  56. package/dist/src/search/external.d.ts +10 -0
  57. package/dist/src/search/external.js +18 -1
  58. package/dist/src/search/provider.d.ts +7 -3
  59. package/dist/src/search/provider.js +8 -7
  60. package/dist/src/search/rrf.js +4 -0
  61. package/dist/src/search/tasks.d.ts +28 -0
  62. package/dist/src/search/tasks.js +36 -23
  63. package/dist/src/search/vector.js +30 -14
  64. package/dist/src/sync/attach.d.ts +7 -2
  65. package/dist/src/sync/attach.js +10 -8
  66. package/dist/src/sync/checkpoint.d.ts +18 -1
  67. package/dist/src/sync/checkpoint.js +31 -29
  68. package/dist/src/sync/driver.d.ts +15 -4
  69. package/dist/src/sync/driver.js +18 -43
  70. package/dist/src/sync/external-source.js +7 -1
  71. package/dist/src/sync/freshness.d.ts +73 -15
  72. package/dist/src/sync/freshness.js +128 -97
  73. package/dist/src/sync/fs-util.d.ts +19 -0
  74. package/dist/src/sync/fs-util.js +24 -1
  75. package/dist/src/sync/observe.d.ts +53 -5
  76. package/dist/src/sync/observe.js +162 -21
  77. package/dist/src/sync/reconciling-ingest.d.ts +42 -2
  78. package/dist/src/sync/reconciling-ingest.js +87 -13
  79. package/dist/src/sync/recovery.d.ts +5 -1
  80. package/dist/src/sync/recovery.js +8 -8
  81. package/dist/src/sync/workspace.d.ts +11 -3
  82. package/dist/src/sync/workspace.js +39 -30
  83. package/dist/src/sync/writer-lock.js +16 -26
  84. package/package.json +3 -3
@@ -2,8 +2,7 @@
2
2
  import { AdapterCapability } from "./adapter.js";
3
3
  import { parseTree, assertFullCoverage } from "../core/parse/tree.js";
4
4
  import { render } from "../core/parse/render.js";
5
- import { extractFromBlock, extractFromFrontmatter, maskCode } from "../graph/extract.js";
6
- import { parse as parseYaml } from "yaml";
5
+ import { extractFromBlock, extractFromFrontmatter, ownText, scanText, INLINE_FIELD_BRACKETED, INLINE_FIELD_LINE } from "../graph/extract.js";
7
6
  export const MARKDOWN_FORMAT = "markdown";
8
7
  function toAdapterEdge(e) {
9
8
  return {
@@ -23,7 +22,6 @@ export const markdownAdapter = {
23
22
  AdapterCapability.Parse,
24
23
  AdapterCapability.Render,
25
24
  AdapterCapability.ExtractEdges,
26
- AdapterCapability.ExtractMetadata,
27
25
  AdapterCapability.ComputeProperties,
28
26
  AdapterCapability.StructuralMutation,
29
27
  AdapterCapability.ProjectNodes,
@@ -37,14 +35,15 @@ export const markdownAdapter = {
37
35
  extractEdges(blocks, metadata) {
38
36
  const edges = [];
39
37
  // Walk every block (recursing into list/blockquote/table nesting) and scan
40
- // its raw for links. srcBlock is the block's OWN assigned id — the caller
41
- // passes id-assigned blocks (blockId zipped on), mirroring projectNodes; a
42
- // block-grain edge without it is useless, so fall back to "" only for the
43
- // pre-identity callers. extractFromBlock returns [] for link-free blocks, so
44
- // no per-block gate is needed (matches the non-adapter fallback path).
38
+ // its OWN text for links (spec/graph §1: the raw with the children blanked,
39
+ // so a link in an item is the item's edge only). srcBlock is the block's
40
+ // OWN assigned id — the caller passes id-assigned blocks (blockId zipped
41
+ // on), mirroring projectNodes; a block-grain edge without it is useless, so
42
+ // fall back to "" only for the pre-identity callers. extractFromBlock
43
+ // returns [] for link-free blocks, so no per-block gate is needed.
45
44
  const walk = (list) => {
46
45
  for (const block of list) {
47
- for (const e of extractFromBlock(block.blockId ?? "", block.type, block.raw)) {
46
+ for (const e of extractFromBlock(block.blockId ?? "", block.type, ownText(block))) {
48
47
  edges.push(toAdapterEdge(e));
49
48
  }
50
49
  if (block.children.length > 0)
@@ -58,24 +57,17 @@ export const markdownAdapter = {
58
57
  }
59
58
  return edges;
60
59
  },
61
- extractMetadata(source) {
62
- const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(source);
63
- if (!match)
64
- return null;
65
- try {
66
- const parsed = parseYaml(match[1]);
67
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
68
- ? parsed
69
- : null;
70
- }
71
- catch {
72
- return null;
73
- }
74
- },
60
+ // No `extractMetadata`: Markdown frontmatter is the `frontmatter` block of the
61
+ // parsed tree and nothing else (spec/properties §3.1), so ingest parses that
62
+ // block's YAML itself. The regex-over-the-source form this adapter used to
63
+ // carry disagreed with the block model — `---\nfoo: 1\n---bar\n` has no
64
+ // frontmatter block yet matched, and a fence containing a line that starts
65
+ // with `---` was cut short (spec/properties §8 "Fixed").
75
66
  projectNodes(blocks) {
76
67
  const nodes = [];
77
- // Byte span of a regex match within the block's raw bytes (node-locates the
78
- // feature inside its block; disambiguates multiple same-kind nodes — 13/nodes).
68
+ // Span of a regex match within the block's raw, as string indices (ingest
69
+ // converts to bytes at the store boundary — spec/graph §2.3). It locates the
70
+ // feature inside its block and disambiguates same-kind nodes (13/nodes).
79
71
  const span = (m) => ({
80
72
  spanStart: m.index ?? 0,
81
73
  spanEnd: (m.index ?? 0) + m[0].length,
@@ -85,12 +77,13 @@ export const markdownAdapter = {
85
77
  // Anchor to the block's OWN assigned id (zipped on by ingest); fall back
86
78
  // to the parent's id, then "root", for pre-identity/parse-time callers.
87
79
  const id = b.blockId || parentId || "root";
88
- // Code is not prose (extraction_version x2): a code_fence block projects
89
- // no link/anchor/field nodes, and inline code spans are masked before the
90
- // regex scans below. Same rule as graph/extract.ts so the md:link /
91
- // md:wikilink nodes and the `references` edges agree. maskCode is
92
- // length-preserving, so spans still index the ORIGINAL raw.
93
- const scan = b.type === "code_fence" ? "" : maskCode(b.raw);
80
+ // The block's own text (spec/graph §1): children blanked, so a feature
81
+ // belongs to the innermost block only; then code masked — a code_fence
82
+ // block projects no link/anchor/field nodes, and inline code spans are
83
+ // blanked (extraction_version x2). Same rule as graph/extract.ts so the
84
+ // md:link / md:wikilink nodes and the `references` edges agree. Both
85
+ // masks are length-preserving, so spans still index the ORIGINAL raw.
86
+ const scan = scanText(b);
94
87
  // Links: [text](target)
95
88
  for (const m of scan.matchAll(/\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
96
89
  nodes.push({ kind: "md:link", name: m[1], value: m[2], blockId: id, ...span(m) });
@@ -109,20 +102,22 @@ export const markdownAdapter = {
109
102
  for (const m of scan.matchAll(anchorRe)) {
110
103
  nodes.push({ kind: "md:anchor", name: m[1], blockId: id, ...span(m) });
111
104
  }
112
- // Inline fields (dataview forms). Two shapes, so a multi-word value is
113
- // captured whole instead of truncated at the first space:
114
- // line form `key:: value` — key at line start, value to end of line
105
+ // Inline fields (dataview forms; the patterns are spec/properties §3.2's,
106
+ // shared with the properties scanner). Two shapes, so a multi-word value
107
+ // is captured whole instead of truncated at the first space:
108
+ // line form `key:: value` — key at line start (after blanks and, since
109
+ // 1.1, an optional list marker + task checkbox), value to
110
+ // end of line; span from the key to the trimmed value's end
115
111
  // bracketed `[key:: value]` / `(key:: value)` — value ends at closer
116
- // Bracketed matches first; the line form is anchored to a line start
117
- // (leading whitespace only), so a `[key:: …]`/`(key:: …)` sitting on its
118
- // own line is not double-counted (a bracket is not `[a-z]`).
119
- for (const m of scan.matchAll(/[[(]([a-z][a-z0-9_]*)::[ \t]*([^\]\n)]*?)[ \t]*[\])]/gi)) {
112
+ // Bracketed matches first; a `[key:: …]`/`(key:: …)` sitting on its own
113
+ // line is not double-counted (a bracket is not `[a-z]`).
114
+ for (const m of scan.matchAll(INLINE_FIELD_BRACKETED)) {
120
115
  nodes.push({ kind: "md:inline_field", name: m[1], value: m[2], blockId: id, ...span(m) });
121
116
  }
122
- for (const m of scan.matchAll(/^[ \t]*([a-z][a-z0-9_]*)::[ \t]*([^\n]*?)[ \t]*$/gim)) {
123
- const start = (m.index ?? 0) + (m[0].match(/^[ \t]*/)?.[0].length ?? 0);
117
+ for (const m of scan.matchAll(INLINE_FIELD_LINE)) {
118
+ const start = (m.index ?? 0) + m[1].length;
124
119
  const end = (m.index ?? 0) + m[0].replace(/[ \t]+$/, "").length;
125
- nodes.push({ kind: "md:inline_field", name: m[1], value: m[2], blockId: id, spanStart: start, spanEnd: end });
120
+ nodes.push({ kind: "md:inline_field", name: m[2], value: m[3], blockId: id, spanStart: start, spanEnd: end });
126
121
  }
127
122
  if (b.children.length > 0)
128
123
  walk(b.children, id);
@@ -11,6 +11,42 @@ export interface ExtractedEdge {
11
11
  anchor: string | null;
12
12
  provenance: Provenance;
13
13
  }
14
+ /** Bracketed form: `[key:: value]` / `(key:: value)` anywhere in prose; the value
15
+ * runs to the first closer (either bracket), trimmed of spaces and tabs. */
16
+ export declare const INLINE_FIELD_BRACKETED: RegExp;
17
+ /** Line form (1.1): a key at the start of a line — after blanks and optionally
18
+ * a list marker (`-`/`*`/`+`/`1.`/`1)`) and a task checkbox — with the value
19
+ * to the end of the line, trimmed. Group 1 is the prefix (so a span can start
20
+ * at the key), 2 the key as written, 3 the trimmed value. A bracketed field on
21
+ * its own line is not also a line-form match: `[`/`(` is not `[a-z]`. */
22
+ export declare const INLINE_FIELD_LINE: RegExp;
23
+ interface SpannedBlock {
24
+ raw: string;
25
+ span: {
26
+ start: number;
27
+ end: number;
28
+ };
29
+ children: SpannedBlock[];
30
+ }
31
+ /**
32
+ * A block's own text: its `raw` with the span of every DIRECT child blanked to
33
+ * spaces (newlines kept, like `maskCode`, so line structure survives). A child
34
+ * sits at `child.span.start - block.span.start` in the parent's raw (spans are
35
+ * source offsets; spec/format §1 inv. 3 puts a child inside its parent). That
36
+ * offset is trusted only when the parent's raw actually reads `child.raw`
37
+ * there; otherwise — callers that hand in blocks without real spans (a tree
38
+ * rebuilt from store rows carries `{0,0}`), which the invariant makes impossible
39
+ * for a parsed tree — the child is located as the first occurrence of its raw
40
+ * at or after the previous child's end, and skipped when not found. Length-
41
+ * preserving: offsets into the result are offsets into `raw`.
42
+ */
43
+ export declare function ownText(block: SpannedBlock): string;
44
+ /** What the scanners see for a block: nothing for a `code_fence`, else
45
+ * `maskCode(ownText(block))` — own text first (a child's fence is blanked
46
+ * either way), code second. Same length as `raw`. */
47
+ export declare function scanText(block: SpannedBlock & {
48
+ type: string;
49
+ }): string;
14
50
  /**
15
51
  * Resolve a `./foo` / `../bar` destination against the source document's
16
52
  * directory (`docDir` = path with the filename stripped, e.g. "a/b/" or "").
@@ -33,6 +69,10 @@ export declare function normalizeUri(uri: string): string;
33
69
  */
34
70
  export declare function maskCode(raw: string): string;
35
71
  /** Extract edges from a single block's raw markdown. Code is skipped: a
36
- * `code_fence` block yields nothing, and inline code spans are masked. */
72
+ * `code_fence` block yields nothing, and inline code spans are masked. Pass the
73
+ * block's own text (`ownText`) for a container so its children's features are
74
+ * not counted again here — the adapter does; a caller with only a stored raw
75
+ * (no children) passes it as is. */
37
76
  export declare function extractFromBlock(blockId: string, blockType: string, rawInput: string): ExtractedEdge[];
38
77
  export declare function extractFromFrontmatter(fm: Record<string, unknown>): ExtractedEdge[];
78
+ export {};
@@ -10,6 +10,12 @@
10
10
  // (and links_stale no longer reports them as dangling). There is no persisted
11
11
  // extraction version — an existing repo re-extracts a document the next time
12
12
  // that document is ingested (checkpoint/observe/apply), not via rebuild-index.
13
+ //
14
+ // spec/properties 1.1 + spec/graph 1.1: every scanner runs over the block's
15
+ // OWN text — `maskCode(ownText(block))`, the raw with each direct child's span
16
+ // blanked — so a feature inside a list item belongs to the item (the innermost
17
+ // block that contains it), never also to the list; and the inline-field line
18
+ // form admits a list marker and a task checkbox (`- key:: v`, `- [ ] due:: x`).
13
19
  export const EXTRACTION_VERSION = "x2";
14
20
  // --- link/inline scanners (operate on a block's raw markdown) ----------------
15
21
  const MD_LINK = /(!?)\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g; // [t](dest) / ![alt](dest)
@@ -17,6 +23,55 @@ const WIKILINK = /(!?)\[\[([^\]]+)\]\]/g; // [[note]] / [[note#H]] / [[note^ref]
17
23
  const BARE_URL = /(?<![("[])\bhttps?:\/\/[^\s)>\]]+/g;
18
24
  const AUTOLINK = /<(https?:\/\/[^>]+)>/g;
19
25
  const INLINE_FIELD = /(?:^|\s)([a-z][a-z0-9_]*)::\s*(\[\[[^\]]+\]\]|\/[^\s]+|https?:\/\/[^\s]+)/gi;
26
+ // --- inline-field forms (spec/properties §3.2; shared with md:inline_field) ---
27
+ /** Bracketed form: `[key:: value]` / `(key:: value)` anywhere in prose; the value
28
+ * runs to the first closer (either bracket), trimmed of spaces and tabs. */
29
+ export const INLINE_FIELD_BRACKETED = /[[(]([a-z][a-z0-9_]*)::[ \t]*([^\]\n)]*?)[ \t]*[\])]/gi;
30
+ /** Line form (1.1): a key at the start of a line — after blanks and optionally
31
+ * a list marker (`-`/`*`/`+`/`1.`/`1)`) and a task checkbox — with the value
32
+ * to the end of the line, trimmed. Group 1 is the prefix (so a span can start
33
+ * at the key), 2 the key as written, 3 the trimmed value. A bracketed field on
34
+ * its own line is not also a line-form match: `[`/`(` is not `[a-z]`. */
35
+ export const INLINE_FIELD_LINE = /^([ \t]*(?:(?:[-*+]|\d{1,9}[.)])[ \t]+(?:\[[ xX]\][ \t]+)?)?)([a-z][a-z0-9_]*)::[ \t]*([^\n]*?)[ \t]*$/gim;
36
+ /**
37
+ * A block's own text: its `raw` with the span of every DIRECT child blanked to
38
+ * spaces (newlines kept, like `maskCode`, so line structure survives). A child
39
+ * sits at `child.span.start - block.span.start` in the parent's raw (spans are
40
+ * source offsets; spec/format §1 inv. 3 puts a child inside its parent). That
41
+ * offset is trusted only when the parent's raw actually reads `child.raw`
42
+ * there; otherwise — callers that hand in blocks without real spans (a tree
43
+ * rebuilt from store rows carries `{0,0}`), which the invariant makes impossible
44
+ * for a parsed tree — the child is located as the first occurrence of its raw
45
+ * at or after the previous child's end, and skipped when not found. Length-
46
+ * preserving: offsets into the result are offsets into `raw`.
47
+ */
48
+ export function ownText(block) {
49
+ if (block.children.length === 0)
50
+ return block.raw;
51
+ const raw = block.raw;
52
+ let out = raw;
53
+ let cursor = 0;
54
+ for (const child of block.children) {
55
+ const len = child.raw.length;
56
+ if (len === 0)
57
+ continue;
58
+ let at = child.span.start - block.span.start;
59
+ if (!(at >= cursor && at + len <= raw.length && raw.startsWith(child.raw, at))) {
60
+ at = raw.indexOf(child.raw, cursor);
61
+ if (at < 0)
62
+ continue;
63
+ }
64
+ out = out.slice(0, at) + blank(child.raw) + out.slice(at + len);
65
+ cursor = at + len;
66
+ }
67
+ return out;
68
+ }
69
+ /** What the scanners see for a block: nothing for a `code_fence`, else
70
+ * `maskCode(ownText(block))` — own text first (a child's fence is blanked
71
+ * either way), code second. Same length as `raw`. */
72
+ export function scanText(block) {
73
+ return block.type === "code_fence" ? "" : maskCode(ownText(block));
74
+ }
20
75
  function splitAnchor(target) {
21
76
  const hashIdx = target.indexOf("#");
22
77
  const caretIdx = target.indexOf("^");
@@ -161,7 +216,10 @@ export function maskCode(raw) {
161
216
  return out;
162
217
  }
163
218
  /** Extract edges from a single block's raw markdown. Code is skipped: a
164
- * `code_fence` block yields nothing, and inline code spans are masked. */
219
+ * `code_fence` block yields nothing, and inline code spans are masked. Pass the
220
+ * block's own text (`ownText`) for a container so its children's features are
221
+ * not counted again here — the adapter does; a caller with only a stored raw
222
+ * (no children) passes it as is. */
165
223
  export function extractFromBlock(blockId, blockType, rawInput) {
166
224
  const edges = [];
167
225
  if (blockType === "code_fence")
@@ -12,6 +12,12 @@ export interface NodeChange {
12
12
  export declare function historyNode(store: Store, blockId: string, opts?: {
13
13
  limit?: number;
14
14
  }): NodeChange[];
15
+ /** A `rev` that names no revision of the document (spec/surface §3: both sides of a diff must exist). The MCP layer maps it to `target_missing` with `{ doc, rev }`. */
16
+ export declare class RevisionNotFound extends Error {
17
+ readonly docId: string;
18
+ readonly rev: string;
19
+ constructor(docId: string, rev: string);
20
+ }
15
21
  export interface DiffEntry {
16
22
  kind: "added" | "removed" | "changed" | "unchanged";
17
23
  blockId: string;
@@ -19,8 +25,40 @@ export interface DiffEntry {
19
25
  after?: string;
20
26
  }
21
27
  export declare function diffBlocks(store: Store, docId: string, fromRev: string, toRev: string): DiffEntry[];
22
- /** Unified textual diff (line-based) between two revisions' rendered files. */
28
+ /** `diff_unified` (spec/surface §3): a unified diff of two revisions' rendered
29
+ * texts — each revision's live raws joined by `\n` — via {@link unifiedDiff}. */
23
30
  export declare function diffUnified(store: Store, docId: string, fromRev: string, toRev: string): string;
31
+ /** How each text becomes lines: `split("\n")` exactly — no trimming, no
32
+ * dropping of a trailing empty element (a raw ending in `\n` yields one) — with
33
+ * a single special case: the empty text has *no* lines (an empty file is zero
34
+ * lines, not one empty line), so a diff from/to nothing is `@@ -0,0 +1,n @@`. */
35
+ export declare function diffLines(text: string): string[];
36
+ /** One step of the edit script: `keep` consumes a line from both sides,
37
+ * `delete` one from the old text, `insert` one from the new text. */
38
+ export interface EditOp {
39
+ kind: "keep" | "delete" | "insert";
40
+ line: string;
41
+ }
42
+ /**
43
+ * Myers' O(ND) shortest edit script (forward, with a per-`d` trace for the
44
+ * backtrack). `V[k]` is the furthest x on diagonal `k = x - y` reachable with
45
+ * `d` edits. The canonical tie rule, identical in both engines: at each step
46
+ * take the diagonal from `k+1` (moving down — an insertion of `b[y]`) when
47
+ * `k == -d || (k != d && V[k-1] < V[k+1])`, else from `k-1` (moving right —
48
+ * a deletion of `a[x]`). On a tie (`V[k-1] == V[k+1]`) that is the deletion.
49
+ */
50
+ export declare function myersScript(a: string[], b: string[]): EditOp[];
51
+ /**
52
+ * The unified diff of two texts (spec/surface §3): hunks of `DIFF_CONTEXT` (3)
53
+ * lines of context; a change group extends to include the next change when
54
+ * fewer than `2 * DIFF_CONTEXT + 1` unchanged lines separate them (the two
55
+ * contexts touch or overlap). Each hunk is `@@ -a,b +c,d @@` (1-based start
56
+ * and length; a length of 1 is written as the start alone; a length of 0 as
57
+ * `a,0` with `a` the line before the insertion point, `0` at the very top)
58
+ * followed by its lines prefixed `-`, `+` or a space with nothing after the
59
+ * sign; hunks joined by `\n`; no file header; identical texts → `""`.
60
+ */
61
+ export declare function unifiedDiff(oldText: string, newText: string): string;
24
62
  export interface CommitDigest {
25
63
  commit: string;
26
64
  seq: number;
@@ -14,6 +14,17 @@ export function historyNode(store, blockId, opts = {}) {
14
14
  LIMIT ?`)
15
15
  .all(blockId, limit);
16
16
  }
17
+ /** A `rev` that names no revision of the document (spec/surface §3: both sides of a diff must exist). The MCP layer maps it to `target_missing` with `{ doc, rev }`. */
18
+ export class RevisionNotFound extends Error {
19
+ docId;
20
+ rev;
21
+ constructor(docId, rev) {
22
+ super(`no revision ${JSON.stringify(rev)} for document ${docId}`);
23
+ this.docId = docId;
24
+ this.rev = rev;
25
+ this.name = "RevisionNotFound";
26
+ }
27
+ }
17
28
  // Block-grain diff between two revisions of a document: compare block id → raw.
18
29
  export function diffBlocks(store, docId, fromRev, toRev) {
19
30
  const at = (rev) => blocksAtRevision(store, docId, rev);
@@ -41,9 +52,9 @@ export function diffBlocks(store, docId, fromRev, toRev) {
41
52
  // in core/read/document.ts readDocumentAtRevision.
42
53
  function blocksAtRevision(store, docId, revId) {
43
54
  const rev = store.db.prepare("SELECT root_tree FROM revisions WHERE rev_id = ? AND doc_id = ?").get(revId, docId);
44
- const out = new Map();
45
55
  if (!rev)
46
- return out;
56
+ throw new RevisionNotFound(docId, revId);
57
+ const out = new Map();
47
58
  const blob = store.db.prepare("SELECT bytes FROM blobs WHERE hash = ?");
48
59
  const treeNode = store.db.prepare("SELECT entries FROM tree_nodes WHERE hash = ?");
49
60
  const walk = (treeHashHex) => {
@@ -61,26 +72,149 @@ function blocksAtRevision(store, docId, revId) {
61
72
  walk(rev.root_tree.toString("hex"));
62
73
  return out;
63
74
  }
64
- /** Unified textual diff (line-based) between two revisions' rendered files. */
75
+ /** `diff_unified` (spec/surface §3): a unified diff of two revisions' rendered
76
+ * texts — each revision's live raws joined by `\n` — via {@link unifiedDiff}. */
65
77
  export function diffUnified(store, docId, fromRev, toRev) {
66
- const rendered = (rev) => {
67
- const map = blocksAtRevision(store, docId, rev);
68
- return [...map.values()].join("\n");
69
- };
70
- const a = rendered(fromRev).split("\n");
71
- const b = rendered(toRev).split("\n");
72
- // Minimal line diff (not a full Myers; sufficient for digests/preview).
73
- const out = [];
74
- const max = Math.max(a.length, b.length);
75
- for (let i = 0; i < max; i++) {
76
- if (a[i] === b[i])
77
- continue;
78
- if (a[i] !== undefined)
79
- out.push(`- ${a[i]}`);
80
- if (b[i] !== undefined)
81
- out.push(`+ ${b[i]}`);
78
+ const rendered = (rev) => [...blocksAtRevision(store, docId, rev).values()].join("\n");
79
+ return unifiedDiff(rendered(fromRev), rendered(toRev));
80
+ }
81
+ // ---- unified diff (spec/surface §3) ------------------------------------------
82
+ // A line-grain unified diff with a deterministic Myers script, so both engines
83
+ // (this reference and crates/omgbase-surface/src/history.rs) produce the same
84
+ // bytes for the same two texts. Kept pure so it can be pinned by unit tests.
85
+ /** How each text becomes lines: `split("\n")` exactly — no trimming, no
86
+ * dropping of a trailing empty element (a raw ending in `\n` yields one) — with
87
+ * a single special case: the empty text has *no* lines (an empty file is zero
88
+ * lines, not one empty line), so a diff from/to nothing is `@@ -0,0 +1,n @@`. */
89
+ export function diffLines(text) {
90
+ return text === "" ? [] : text.split("\n");
91
+ }
92
+ const DIFF_CONTEXT = 3;
93
+ /**
94
+ * Myers' O(ND) shortest edit script (forward, with a per-`d` trace for the
95
+ * backtrack). `V[k]` is the furthest x on diagonal `k = x - y` reachable with
96
+ * `d` edits. The canonical tie rule, identical in both engines: at each step
97
+ * take the diagonal from `k+1` (moving down — an insertion of `b[y]`) when
98
+ * `k == -d || (k != d && V[k-1] < V[k+1])`, else from `k-1` (moving right —
99
+ * a deletion of `a[x]`). On a tie (`V[k-1] == V[k+1]`) that is the deletion.
100
+ */
101
+ export function myersScript(a, b) {
102
+ const n = a.length;
103
+ const m = b.length;
104
+ const max = n + m;
105
+ // V is indexed by k ∈ [-max-1, max+1]; `off` maps it onto a plain array.
106
+ const off = max + 1;
107
+ const v = new Array(2 * max + 3).fill(0);
108
+ const trace = [];
109
+ let found = false;
110
+ for (let d = 0; d <= max && !found; d++) {
111
+ trace.push(v.slice());
112
+ for (let k = -d; k <= d; k += 2) {
113
+ let x;
114
+ if (k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]))
115
+ x = v[off + k + 1];
116
+ else
117
+ x = v[off + k - 1] + 1;
118
+ let y = x - k;
119
+ while (x < n && y < m && a[x] === b[y]) {
120
+ x++;
121
+ y++;
122
+ }
123
+ v[off + k] = x;
124
+ if (x >= n && y >= m) {
125
+ found = true;
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ // Backtrack from (n, m) through the trace, emitting ops newest-first.
131
+ const ops = [];
132
+ let x = n;
133
+ let y = m;
134
+ for (let d = trace.length - 1; d >= 0; d--) {
135
+ const vd = trace[d];
136
+ const k = x - y;
137
+ const prevK = k === -d || (k !== d && vd[off + k - 1] < vd[off + k + 1]) ? k + 1 : k - 1;
138
+ const prevX = vd[off + prevK];
139
+ const prevY = prevX - prevK;
140
+ while (x > prevX && y > prevY) {
141
+ x--;
142
+ y--;
143
+ ops.push({ kind: "keep", line: a[x] });
144
+ }
145
+ if (d > 0) {
146
+ if (x === prevX)
147
+ ops.push({ kind: "insert", line: b[prevY] });
148
+ else
149
+ ops.push({ kind: "delete", line: a[prevX] });
150
+ }
151
+ x = prevX;
152
+ y = prevY;
153
+ }
154
+ ops.reverse();
155
+ return ops;
156
+ }
157
+ /**
158
+ * The unified diff of two texts (spec/surface §3): hunks of `DIFF_CONTEXT` (3)
159
+ * lines of context; a change group extends to include the next change when
160
+ * fewer than `2 * DIFF_CONTEXT + 1` unchanged lines separate them (the two
161
+ * contexts touch or overlap). Each hunk is `@@ -a,b +c,d @@` (1-based start
162
+ * and length; a length of 1 is written as the start alone; a length of 0 as
163
+ * `a,0` with `a` the line before the insertion point, `0` at the very top)
164
+ * followed by its lines prefixed `-`, `+` or a space with nothing after the
165
+ * sign; hunks joined by `\n`; no file header; identical texts → `""`.
166
+ */
167
+ export function unifiedDiff(oldText, newText) {
168
+ const ops = myersScript(diffLines(oldText), diffLines(newText));
169
+ // Old/new line counts consumed before each op (0-based positions).
170
+ const oldPos = new Array(ops.length + 1);
171
+ const newPos = new Array(ops.length + 1);
172
+ let o = 0;
173
+ let nn = 0;
174
+ for (let i = 0; i < ops.length; i++) {
175
+ oldPos[i] = o;
176
+ newPos[i] = nn;
177
+ if (ops[i].kind !== "insert")
178
+ o++;
179
+ if (ops[i].kind !== "delete")
180
+ nn++;
181
+ }
182
+ oldPos[ops.length] = o;
183
+ newPos[ops.length] = nn;
184
+ const changes = [];
185
+ for (let i = 0; i < ops.length; i++)
186
+ if (ops[i].kind !== "keep")
187
+ changes.push(i);
188
+ if (changes.length === 0)
189
+ return "";
190
+ const hunks = [];
191
+ let g = 0;
192
+ while (g < changes.length) {
193
+ const first = changes[g];
194
+ let last = first;
195
+ // Merge rule: the next change joins this hunk iff the unchanged lines
196
+ // between them number at most 2 * DIFF_CONTEXT.
197
+ while (g + 1 < changes.length && changes[g + 1] - last - 1 <= 2 * DIFF_CONTEXT) {
198
+ g++;
199
+ last = changes[g];
200
+ }
201
+ g++;
202
+ const start = Math.max(0, first - DIFF_CONTEXT);
203
+ const end = Math.min(ops.length - 1, last + DIFF_CONTEXT);
204
+ const oldLen = oldPos[end + 1] - oldPos[start];
205
+ const newLen = newPos[end + 1] - newPos[start];
206
+ const range = (pos, len) => {
207
+ const startLine = len === 0 ? pos : pos + 1;
208
+ return len === 1 ? `${startLine}` : `${startLine},${len}`;
209
+ };
210
+ const lines = [`@@ -${range(oldPos[start], oldLen)} +${range(newPos[start], newLen)} @@`];
211
+ for (let i = start; i <= end; i++) {
212
+ const op = ops[i];
213
+ lines.push((op.kind === "keep" ? " " : op.kind === "delete" ? "-" : "+") + op.line);
214
+ }
215
+ hunks.push(lines.join("\n"));
82
216
  }
83
- return out.join("\n");
217
+ return hunks.join("\n");
84
218
  }
85
219
  /** changes_since: commit digests after a cursor (repo commit seq) — the change
86
220
  * feed (06 §3). `seq` is a dense per-REPO total order, so a cursor is only
@@ -9,20 +9,21 @@ export { writeDocNodes, deleteDocNodes } from "./core/store/nodes.js";
9
9
  export { docPropertiesMerged, docPropertiesGrouped, writeDocProperties, flattenFrontmatter } from "./core/store/properties.js";
10
10
  import "./format/index.js";
11
11
  export { Store, type StoreOptions } from "./core/store/store.js";
12
- export { Workspace, RepoSelectionError, type RepoRow } from "./sync/workspace.js";
12
+ export { Workspace, RepoSelectionError, selectRepo, type RepoRow } from "./sync/workspace.js";
13
13
  export { resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, deepMerge, type Settings, } from "./sync/settings.js";
14
- export { freshnessSweep, rebuildFileStats, recordFileStat, detectDiskDrift, type SweepResult, type DiskDrift } from "./sync/freshness.js";
14
+ export { freshnessSweep, rebuildFileStats, recordFileStat, recordStat, detectDiskDrift, sweepPlan, sweepCandidates, loadStatCache, snapshotOf, type SweepResult, type DiskDrift, type SweepPlan, type StatCacheRow, type SnapshotEntry, } from "./sync/freshness.js";
15
+ export { recoverRepo, type RecoveryResult } from "./sync/recovery.js";
15
16
  export { withWriterLock, writerLockFree, WriterLockTimeout } from "./sync/writer-lock.js";
16
17
  export { WatchLease, watchLeaseLive } from "./sync/watch-lease.js";
17
18
  export { ingestDirectory, type IngestDirResult } from "./sync/attach.js";
18
19
  export { ensureRepo } from "./core/attach.js";
19
- export { walkMarkdown, walkMarkdownAsync } from "./sync/fs-util.js";
20
+ export { walkMarkdown, walkMarkdownAsync, nodeFs, type SyncFs, type FileStat } from "./sync/fs-util.js";
20
21
  export { Watcher, type WatcherOptions } from "./sync/watcher.js";
21
22
  export type { SyncSource, SourceCapabilities, SourceIdentity, SourceEntry, SourceItem, SourceWatch, WatchListener, } from "./sync/plugin.js";
22
23
  export { createExternalSource, type ExternalSourceSpec } from "./sync/external-source.js";
23
24
  export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, type AdapterRow, type SourceRow, } from "./sync/sources.js";
24
25
  export { reconcileChanges, attachSource } from "./sync/driver.js";
25
- export { observeFile, observeMany, observeOne, observeDelete, type ObserveResult, type ObserveOneResult, type ObserveDeleteResult } from "./sync/observe.js";
26
+ export { observeFile, observeMany, observeOne, observeDelete, observeBatch, type ObserveResult, type ObserveOneResult, type ObserveDeleteResult, type BatchItem, type BatchOutcome } from "./sync/observe.js";
26
27
  export { buildServer, type ServerContext } from "./mcp/server.js";
27
28
  export { serveStdio, type ServeStdioHandle } from "./mcp/stdio.js";
28
29
  export { processCheckpoint, type CheckpointResult } from "./sync/checkpoint.js";
@@ -42,12 +43,15 @@ export { embeddingSettings, type EmbeddingSettings } from "./search/provider.js"
42
43
  export { createExternalProvider, type ExternalProvider } from "./search/external.js";
43
44
  export { hybridSearch, type HybridHit, type HybridInput } from "./search/rrf.js";
44
45
  export { vectorSearch, docVectorSearch, type VectorHit, type DocVectorHit } from "./search/vector.js";
45
- export { historyNode, diffUnified, changesSince, type NodeChange, type CommitDigest } from "./graph/history.js";
46
+ export { historyNode, diffUnified, unifiedDiff, changesSince, type NodeChange, type CommitDigest } from "./graph/history.js";
46
47
  export { docLinks, type LinksResult, type LinkEdge, type LinksOptions } from "./graph/links.js";
47
48
  export { FilterInvalid } from "./search/cel/parser.js";
48
49
  export { EngineError, type ErrorCode } from "./mcp/errors.js";
49
50
  export { MutationError } from "./mutate/tree.js";
50
- export { isValidId, prefixOf } from "./core/ids.js";
51
+ export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter, type IdMinter, type IdPrefix } from "./core/ids.js";
52
+ export { pinClock, withClock } from "./core/clock.js";
53
+ export { sha256 } from "./core/hash.js";
54
+ export { ingestFile, type IngestResult } from "./core/ingest.js";
51
55
  export { apply, type ApplyRequest, type ApplyResult, type Op } from "./mutate/apply.js";
52
56
  export { type DocStore, FsDocStore, NullDocStore, resolveDocStore } from "./mutate/doc-store.js";
53
57
  export { type To, type At, type Expect } from "./mutate/ops.js";
package/dist/src/index.js CHANGED
@@ -10,19 +10,20 @@ export { docPropertiesMerged, docPropertiesGrouped, writeDocProperties, flattenF
10
10
  import "./format/index.js";
11
11
  // Workspace + sync surface used by the CLI (second client, 11 §1).
12
12
  export { Store } from "./core/store/store.js";
13
- export { Workspace, RepoSelectionError } from "./sync/workspace.js";
13
+ export { Workspace, RepoSelectionError, selectRepo } from "./sync/workspace.js";
14
14
  export { resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, deepMerge, } from "./sync/settings.js";
15
- export { freshnessSweep, rebuildFileStats, recordFileStat, detectDiskDrift } from "./sync/freshness.js";
15
+ export { freshnessSweep, rebuildFileStats, recordFileStat, recordStat, detectDiskDrift, sweepPlan, sweepCandidates, loadStatCache, snapshotOf, } from "./sync/freshness.js";
16
+ export { recoverRepo } from "./sync/recovery.js";
16
17
  export { withWriterLock, writerLockFree, WriterLockTimeout } from "./sync/writer-lock.js";
17
18
  export { WatchLease, watchLeaseLive } from "./sync/watch-lease.js";
18
19
  export { ingestDirectory } from "./sync/attach.js";
19
20
  export { ensureRepo } from "./core/attach.js";
20
- export { walkMarkdown, walkMarkdownAsync } from "./sync/fs-util.js";
21
+ export { walkMarkdown, walkMarkdownAsync, nodeFs } from "./sync/fs-util.js";
21
22
  export { Watcher } from "./sync/watcher.js";
22
23
  export { createExternalSource } from "./sync/external-source.js";
23
24
  export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, } from "./sync/sources.js";
24
25
  export { reconcileChanges, attachSource } from "./sync/driver.js";
25
- export { observeFile, observeMany, observeOne, observeDelete } from "./sync/observe.js";
26
+ export { observeFile, observeMany, observeOne, observeDelete, observeBatch } from "./sync/observe.js";
26
27
  export { buildServer } from "./mcp/server.js";
27
28
  export { serveStdio } from "./mcp/stdio.js";
28
29
  export { processCheckpoint } from "./sync/checkpoint.js";
@@ -47,12 +48,16 @@ export { embeddingSettings } from "./search/provider.js";
47
48
  export { createExternalProvider } from "./search/external.js";
48
49
  export { hybridSearch } from "./search/rrf.js";
49
50
  export { vectorSearch, docVectorSearch } from "./search/vector.js";
50
- export { historyNode, diffUnified, changesSince } from "./graph/history.js";
51
+ export { historyNode, diffUnified, unifiedDiff, changesSince } from "./graph/history.js";
51
52
  export { docLinks } from "./graph/links.js";
52
53
  export { FilterInvalid } from "./search/cel/parser.js";
53
54
  export { EngineError } from "./mcp/errors.js";
54
55
  export { MutationError } from "./mutate/tree.js";
55
- export { isValidId, prefixOf } from "./core/ids.js";
56
+ export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter } from "./core/ids.js";
57
+ // The clock seam (spec/surface §7.1): `omg mcp` pins the process clock under OMGBASE_SPEC_CLOCK.
58
+ export { pinClock, withClock } from "./core/clock.js";
59
+ export { sha256 } from "./core/hash.js";
60
+ export { ingestFile } from "./core/ingest.js";
56
61
  // Mutation surface consumed by CLI write commands (11 §5.6).
57
62
  export { apply } from "./mutate/apply.js";
58
63
  export { FsDocStore, NullDocStore, resolveDocStore } from "./mutate/doc-store.js";