@omgbase/core 0.1.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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/cli/eval-matcher.d.ts +13 -0
  3. package/dist/src/cli/eval-matcher.js +36 -0
  4. package/dist/src/core/attach.d.ts +5 -0
  5. package/dist/src/core/attach.js +38 -0
  6. package/dist/src/core/cursor.d.ts +11 -0
  7. package/dist/src/core/cursor.js +35 -0
  8. package/dist/src/core/hash.d.ts +23 -0
  9. package/dist/src/core/hash.js +82 -0
  10. package/dist/src/core/ids.d.ts +6 -0
  11. package/dist/src/core/ids.js +26 -0
  12. package/dist/src/core/index.d.ts +1 -0
  13. package/dist/src/core/index.js +2 -0
  14. package/dist/src/core/ingest.d.ts +43 -0
  15. package/dist/src/core/ingest.js +287 -0
  16. package/dist/src/core/order-key.d.ts +8 -0
  17. package/dist/src/core/order-key.js +72 -0
  18. package/dist/src/core/parse/parse.d.ts +3 -0
  19. package/dist/src/core/parse/parse.js +104 -0
  20. package/dist/src/core/parse/render.d.ts +3 -0
  21. package/dist/src/core/parse/render.js +20 -0
  22. package/dist/src/core/parse/tree.d.ts +8 -0
  23. package/dist/src/core/parse/tree.js +43 -0
  24. package/dist/src/core/parse/types.d.ts +52 -0
  25. package/dist/src/core/parse/types.js +9 -0
  26. package/dist/src/core/read/document.d.ts +107 -0
  27. package/dist/src/core/read/document.js +191 -0
  28. package/dist/src/core/read/nodes.d.ts +47 -0
  29. package/dist/src/core/read/nodes.js +102 -0
  30. package/dist/src/core/read/outline.d.ts +12 -0
  31. package/dist/src/core/read/outline.js +76 -0
  32. package/dist/src/core/read/reader.d.ts +125 -0
  33. package/dist/src/core/read/reader.js +206 -0
  34. package/dist/src/core/read/refs.d.ts +15 -0
  35. package/dist/src/core/read/refs.js +51 -0
  36. package/dist/src/core/store/edges.d.ts +26 -0
  37. package/dist/src/core/store/edges.js +101 -0
  38. package/dist/src/core/store/fts.d.ts +10 -0
  39. package/dist/src/core/store/fts.js +22 -0
  40. package/dist/src/core/store/gc.d.ts +18 -0
  41. package/dist/src/core/store/gc.js +69 -0
  42. package/dist/src/core/store/nodes.d.ts +5 -0
  43. package/dist/src/core/store/nodes.js +57 -0
  44. package/dist/src/core/store/properties.d.ts +38 -0
  45. package/dist/src/core/store/properties.js +177 -0
  46. package/dist/src/core/store/rebuild.d.ts +3 -0
  47. package/dist/src/core/store/rebuild.js +29 -0
  48. package/dist/src/core/store/schema.d.ts +9 -0
  49. package/dist/src/core/store/schema.js +386 -0
  50. package/dist/src/core/store/sections.d.ts +2 -0
  51. package/dist/src/core/store/sections.js +29 -0
  52. package/dist/src/core/store/store.d.ts +24 -0
  53. package/dist/src/core/store/store.js +164 -0
  54. package/dist/src/core/store/writers.d.ts +52 -0
  55. package/dist/src/core/store/writers.js +89 -0
  56. package/dist/src/core/vec.d.ts +10 -0
  57. package/dist/src/core/vec.js +33 -0
  58. package/dist/src/format/adapter.d.ts +97 -0
  59. package/dist/src/format/adapter.js +14 -0
  60. package/dist/src/format/index.d.ts +6 -0
  61. package/dist/src/format/index.js +15 -0
  62. package/dist/src/format/json.d.ts +3 -0
  63. package/dist/src/format/json.js +324 -0
  64. package/dist/src/format/markdown.d.ts +5 -0
  65. package/dist/src/format/markdown.js +217 -0
  66. package/dist/src/format/registry.d.ts +6 -0
  67. package/dist/src/format/registry.js +24 -0
  68. package/dist/src/format/yaml.d.ts +5 -0
  69. package/dist/src/format/yaml.js +413 -0
  70. package/dist/src/graph/extract.d.ts +38 -0
  71. package/dist/src/graph/extract.js +242 -0
  72. package/dist/src/graph/history.d.ts +103 -0
  73. package/dist/src/graph/history.js +210 -0
  74. package/dist/src/graph/inbound-links.d.ts +33 -0
  75. package/dist/src/graph/inbound-links.js +130 -0
  76. package/dist/src/graph/link-destinations.d.ts +22 -0
  77. package/dist/src/graph/link-destinations.js +84 -0
  78. package/dist/src/graph/link-health.d.ts +79 -0
  79. package/dist/src/graph/link-health.js +146 -0
  80. package/dist/src/graph/links.d.ts +23 -0
  81. package/dist/src/graph/links.js +42 -0
  82. package/dist/src/index.d.ts +60 -0
  83. package/dist/src/index.js +70 -0
  84. package/dist/src/mcp/errors.d.ts +17 -0
  85. package/dist/src/mcp/errors.js +18 -0
  86. package/dist/src/mcp/graph.d.ts +63 -0
  87. package/dist/src/mcp/graph.js +212 -0
  88. package/dist/src/mcp/reference.d.ts +1 -0
  89. package/dist/src/mcp/reference.js +227 -0
  90. package/dist/src/mcp/server.d.ts +30 -0
  91. package/dist/src/mcp/server.js +1076 -0
  92. package/dist/src/mcp/stdio.d.ts +13 -0
  93. package/dist/src/mcp/stdio.js +24 -0
  94. package/dist/src/migrate/mrplex.d.ts +27 -0
  95. package/dist/src/migrate/mrplex.js +29 -0
  96. package/dist/src/mutate/apply.d.ts +83 -0
  97. package/dist/src/mutate/apply.js +302 -0
  98. package/dist/src/mutate/doc-store.d.ts +49 -0
  99. package/dist/src/mutate/doc-store.js +71 -0
  100. package/dist/src/mutate/docs.d.ts +60 -0
  101. package/dist/src/mutate/docs.js +232 -0
  102. package/dist/src/mutate/known-ids.d.ts +12 -0
  103. package/dist/src/mutate/known-ids.js +154 -0
  104. package/dist/src/mutate/load.d.ts +3 -0
  105. package/dist/src/mutate/load.js +66 -0
  106. package/dist/src/mutate/lower.d.ts +32 -0
  107. package/dist/src/mutate/lower.js +232 -0
  108. package/dist/src/mutate/macros.d.ts +91 -0
  109. package/dist/src/mutate/macros.js +230 -0
  110. package/dist/src/mutate/ops.d.ts +40 -0
  111. package/dist/src/mutate/ops.js +513 -0
  112. package/dist/src/mutate/opset.d.ts +92 -0
  113. package/dist/src/mutate/opset.js +79 -0
  114. package/dist/src/mutate/plan-update.d.ts +51 -0
  115. package/dist/src/mutate/plan-update.js +187 -0
  116. package/dist/src/mutate/tree.d.ts +46 -0
  117. package/dist/src/mutate/tree.js +167 -0
  118. package/dist/src/oqx/run.d.ts +1 -0
  119. package/dist/src/oqx/run.js +6 -0
  120. package/dist/src/oqx-js/context.d.ts +23 -0
  121. package/dist/src/oqx-js/context.js +564 -0
  122. package/dist/src/oqx-js/planner.d.ts +10 -0
  123. package/dist/src/oqx-js/planner.js +91 -0
  124. package/dist/src/oqx-js/run.d.ts +41 -0
  125. package/dist/src/oqx-js/run.js +332 -0
  126. package/dist/src/oqx-js/sql/translate.d.ts +27 -0
  127. package/dist/src/oqx-js/sql/translate.js +290 -0
  128. package/dist/src/reconcile/crossdoc.d.ts +34 -0
  129. package/dist/src/reconcile/crossdoc.js +76 -0
  130. package/dist/src/reconcile/eval/generator.d.ts +21 -0
  131. package/dist/src/reconcile/eval/generator.js +129 -0
  132. package/dist/src/reconcile/eval/metrics.d.ts +32 -0
  133. package/dist/src/reconcile/eval/metrics.js +74 -0
  134. package/dist/src/reconcile/flatten.d.ts +12 -0
  135. package/dist/src/reconcile/flatten.js +37 -0
  136. package/dist/src/reconcile/phase5.d.ts +8 -0
  137. package/dist/src/reconcile/phase5.js +167 -0
  138. package/dist/src/reconcile/phase6a.d.ts +2 -0
  139. package/dist/src/reconcile/phase6a.js +159 -0
  140. package/dist/src/reconcile/phases.d.ts +28 -0
  141. package/dist/src/reconcile/phases.js +0 -0
  142. package/dist/src/reconcile/reconcile.d.ts +21 -0
  143. package/dist/src/reconcile/reconcile.js +115 -0
  144. package/dist/src/reconcile/similarity.d.ts +7 -0
  145. package/dist/src/reconcile/similarity.js +41 -0
  146. package/dist/src/reconcile/types.d.ts +46 -0
  147. package/dist/src/reconcile/types.js +19 -0
  148. package/dist/src/search/cel/ast.d.ts +74 -0
  149. package/dist/src/search/cel/ast.js +3 -0
  150. package/dist/src/search/cel/compile.d.ts +60 -0
  151. package/dist/src/search/cel/compile.js +836 -0
  152. package/dist/src/search/cel/lexer.d.ts +9 -0
  153. package/dist/src/search/cel/lexer.js +139 -0
  154. package/dist/src/search/cel/parser.d.ts +7 -0
  155. package/dist/src/search/cel/parser.js +219 -0
  156. package/dist/src/search/drain.d.ts +39 -0
  157. package/dist/src/search/drain.js +97 -0
  158. package/dist/src/search/embeddings.d.ts +131 -0
  159. package/dist/src/search/embeddings.js +263 -0
  160. package/dist/src/search/external.d.ts +13 -0
  161. package/dist/src/search/external.js +177 -0
  162. package/dist/src/search/fts-query.d.ts +1 -0
  163. package/dist/src/search/fts-query.js +64 -0
  164. package/dist/src/search/provider.d.ts +17 -0
  165. package/dist/src/search/provider.js +13 -0
  166. package/dist/src/search/resolve.d.ts +18 -0
  167. package/dist/src/search/resolve.js +23 -0
  168. package/dist/src/search/rrf.d.ts +36 -0
  169. package/dist/src/search/rrf.js +84 -0
  170. package/dist/src/search/tasks.d.ts +12 -0
  171. package/dist/src/search/tasks.js +131 -0
  172. package/dist/src/search/text.d.ts +16 -0
  173. package/dist/src/search/text.js +21 -0
  174. package/dist/src/search/vector.d.ts +18 -0
  175. package/dist/src/search/vector.js +49 -0
  176. package/dist/src/sync/admin.d.ts +59 -0
  177. package/dist/src/sync/admin.js +53 -0
  178. package/dist/src/sync/attach.d.ts +13 -0
  179. package/dist/src/sync/attach.js +29 -0
  180. package/dist/src/sync/checkpoint.d.ts +21 -0
  181. package/dist/src/sync/checkpoint.js +52 -0
  182. package/dist/src/sync/driver.d.ts +25 -0
  183. package/dist/src/sync/driver.js +80 -0
  184. package/dist/src/sync/external-source.d.ts +14 -0
  185. package/dist/src/sync/external-source.js +137 -0
  186. package/dist/src/sync/freshness.d.ts +42 -0
  187. package/dist/src/sync/freshness.js +152 -0
  188. package/dist/src/sync/fs-util.d.ts +12 -0
  189. package/dist/src/sync/fs-util.js +55 -0
  190. package/dist/src/sync/git-heuristics.d.ts +6 -0
  191. package/dist/src/sync/git-heuristics.js +16 -0
  192. package/dist/src/sync/observe.d.ts +63 -0
  193. package/dist/src/sync/observe.js +84 -0
  194. package/dist/src/sync/plugin.d.ts +54 -0
  195. package/dist/src/sync/plugin.js +12 -0
  196. package/dist/src/sync/reconciling-ingest.d.ts +12 -0
  197. package/dist/src/sync/reconciling-ingest.js +209 -0
  198. package/dist/src/sync/recovery.d.ts +6 -0
  199. package/dist/src/sync/recovery.js +34 -0
  200. package/dist/src/sync/settings.d.ts +18 -0
  201. package/dist/src/sync/settings.js +58 -0
  202. package/dist/src/sync/sources.d.ts +37 -0
  203. package/dist/src/sync/sources.js +75 -0
  204. package/dist/src/sync/tombstone.d.ts +9 -0
  205. package/dist/src/sync/tombstone.js +55 -0
  206. package/dist/src/sync/watch-lease.d.ts +14 -0
  207. package/dist/src/sync/watch-lease.js +91 -0
  208. package/dist/src/sync/watcher.d.ts +19 -0
  209. package/dist/src/sync/watcher.js +34 -0
  210. package/dist/src/sync/workspace.d.ts +34 -0
  211. package/dist/src/sync/workspace.js +120 -0
  212. package/dist/src/sync/writer-lock.d.ts +16 -0
  213. package/dist/src/sync/writer-lock.js +121 -0
  214. package/package.json +80 -0
@@ -0,0 +1,72 @@
1
+ // Fractional order keys (02 §5.3). Base-62; key_between(a,b) returns a key
2
+ // strictly between its arguments. Keys sort lexicographically among siblings
3
+ // and are never exposed via the API (ordinals are).
4
+ //
5
+ // v1 scope: ingest appends sequentially, so keyBetween(last, null) is the hot
6
+ // path. A midpoint algorithm covers arbitrary insertion for the mutation stage.
7
+ const DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
8
+ const BASE = DIGITS.length; // 62
9
+ function digitVal(c) {
10
+ const v = DIGITS.indexOf(c);
11
+ if (v < 0)
12
+ throw new Error(`invalid order-key digit: ${c}`);
13
+ return v;
14
+ }
15
+ /**
16
+ * Return a key strictly between a and b (exclusive). null bounds mean
17
+ * open-ended (a=null → before b; b=null → after a). Amortized O(1) growth for
18
+ * sequential appends.
19
+ */
20
+ export function keyBetween(a, b) {
21
+ if (a !== null && b !== null && a >= b) {
22
+ throw new Error(`keyBetween: a must be < b (got ${a}, ${b})`);
23
+ }
24
+ if (a === null && b === null)
25
+ return DIGITS[Math.floor(BASE / 2)]; // midpoint digit
26
+ if (b === null) {
27
+ // append after a: increment the last digit, or extend.
28
+ const last = a.charCodeAt(a.length - 1);
29
+ const lastDigit = digitVal(a[a.length - 1]);
30
+ if (lastDigit + 1 < BASE) {
31
+ return a.slice(0, -1) + DIGITS[lastDigit + 1];
32
+ }
33
+ void last;
34
+ return a + DIGITS[Math.floor(BASE / 2)]; // extend deeper
35
+ }
36
+ if (a === null) {
37
+ // before b: midpoint between "" and b.
38
+ const firstDigit = digitVal(b[0]);
39
+ if (firstDigit > 0)
40
+ return DIGITS[Math.floor(firstDigit / 2)];
41
+ return "0" + keyBetween(null, b.slice(1) || null);
42
+ }
43
+ // both present: find midpoint digit-by-digit.
44
+ let i = 0;
45
+ let prefix = "";
46
+ for (;;) {
47
+ const da = i < a.length ? digitVal(a[i]) : 0;
48
+ const db = i < b.length ? digitVal(b[i]) : BASE;
49
+ if (da === db) {
50
+ prefix += DIGITS[da];
51
+ i++;
52
+ continue;
53
+ }
54
+ if (db - da > 1) {
55
+ return prefix + DIGITS[da + Math.floor((db - da) / 2)];
56
+ }
57
+ // adjacent digits: take a's digit and recurse deeper on the fractional part.
58
+ prefix += DIGITS[da];
59
+ return prefix + keyBetween(a.slice(i + 1) || null, null);
60
+ }
61
+ }
62
+ /** Generate n sequential append keys starting from scratch. */
63
+ export function sequentialKeys(n) {
64
+ const keys = [];
65
+ let prev = null;
66
+ for (let i = 0; i < n; i++) {
67
+ prev = keyBetween(prev, null);
68
+ keys.push(prev);
69
+ }
70
+ return keys;
71
+ }
72
+ //# sourceMappingURL=order-key.js.map
@@ -0,0 +1,3 @@
1
+ import type { RawBlock } from "./types.js";
2
+ /** Parse Markdown source into a flat list of top-level blocks with byte spans. */
3
+ export declare function parseBlocks(source: string): RawBlock[];
@@ -0,0 +1,104 @@
1
+ import { fromMarkdown } from "mdast-util-from-markdown";
2
+ import { gfm } from "micromark-extension-gfm";
3
+ import { gfmFromMarkdown } from "mdast-util-gfm";
4
+ import { frontmatter } from "micromark-extension-frontmatter";
5
+ import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
6
+ import { normalizeVisibleText } from "../hash.js";
7
+ // Nodes whose children nest as blocks (03 §1: parser nesting only).
8
+ // Everything else at block level maps to a leaf block; unknown → opaque.
9
+ function mapType(node) {
10
+ switch (node.type) {
11
+ case "yaml":
12
+ return "frontmatter";
13
+ case "heading":
14
+ return "heading";
15
+ case "paragraph":
16
+ return "paragraph";
17
+ case "list":
18
+ return "list";
19
+ case "listItem":
20
+ return node.checked === null || node.checked === undefined ? "list_item" : "task";
21
+ case "blockquote":
22
+ return "blockquote";
23
+ case "code":
24
+ return "code_fence";
25
+ case "table":
26
+ return "table";
27
+ case "tableRow":
28
+ return "table_row";
29
+ case "thematicBreak":
30
+ return "thematic_break";
31
+ case "html":
32
+ return "html_block";
33
+ default:
34
+ return null; // opaque fallback
35
+ }
36
+ }
37
+ function attrsFor(node) {
38
+ switch (node.type) {
39
+ case "heading":
40
+ return { level: node.depth };
41
+ case "list": {
42
+ const attrs = { ordered: node.ordered ?? false };
43
+ if (node.start !== null && node.start !== undefined)
44
+ attrs.start = node.start;
45
+ return attrs;
46
+ }
47
+ case "listItem":
48
+ return node.checked === null || node.checked === undefined ? {} : { checked: node.checked };
49
+ case "code": {
50
+ const attrs = {};
51
+ if (node.lang)
52
+ attrs.lang = node.lang;
53
+ if (node.meta)
54
+ attrs.info = node.meta;
55
+ return attrs;
56
+ }
57
+ default:
58
+ return {};
59
+ }
60
+ }
61
+ // Which mdast container types expose block-level children we descend into.
62
+ const CONTAINER_TYPES = new Set(["list", "listItem", "blockquote", "table"]);
63
+ function childrenOf(node) {
64
+ if (!CONTAINER_TYPES.has(node.type))
65
+ return [];
66
+ const kids = node.children;
67
+ return Array.isArray(kids) ? kids : [];
68
+ }
69
+ function buildBlock(node, source) {
70
+ const start = node.position?.start.offset;
71
+ const end = node.position?.end.offset;
72
+ if (start === undefined || end === undefined) {
73
+ throw new Error(`node ${node.type} missing position offsets`);
74
+ }
75
+ const raw = source.slice(start, end);
76
+ const mapped = mapType(node);
77
+ const type = mapped ?? "opaque";
78
+ let children = mapped === null ? [] : childrenOf(node).map((c) => buildBlock(c, source));
79
+ // Fold a tight list item's lone paragraph: the item block carries the text
80
+ // directly (frozen outline format, 06 §6). Loose/multi-block items keep them.
81
+ if ((type === "list_item" || type === "task") && children.length === 1 && children[0].type === "paragraph") {
82
+ children = [];
83
+ }
84
+ return {
85
+ type,
86
+ span: { start, end },
87
+ raw,
88
+ text: normalizeVisibleText(raw, type),
89
+ attrs: mapped === null ? {} : attrsFor(node),
90
+ children,
91
+ trivia: "",
92
+ anchors: [],
93
+ outLinks: [],
94
+ };
95
+ }
96
+ /** Parse Markdown source into a flat list of top-level blocks with byte spans. */
97
+ export function parseBlocks(source) {
98
+ const tree = fromMarkdown(source, {
99
+ extensions: [gfm(), frontmatter(["yaml"])],
100
+ mdastExtensions: [gfmFromMarkdown(), frontmatterFromMarkdown(["yaml"])],
101
+ });
102
+ return tree.children.map((c) => buildBlock(c, source));
103
+ }
104
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1,3 @@
1
+ import type { BlockTree, RawBlock } from "./types.js";
2
+ export declare function render(tree: BlockTree): string;
3
+ export declare function serializeNew(block: RawBlock): string;
@@ -0,0 +1,20 @@
1
+ // Splice renderer — 03 §2.2. Untouched blocks emit their retained raw bytes
2
+ // verbatim; only dirty blocks (op-supplied markdown) are serialized. The
3
+ // renderer MUST NOT re-serialize untouched content from an AST (README
4
+ // invariant #1 — remark-stringify is banned by lint).
5
+ export function render(tree) {
6
+ const out = [tree.leadingTrivia];
7
+ for (const block of tree.children) {
8
+ out.push(block.dirty ? serializeNew(block) : block.raw);
9
+ out.push(block.trivia);
10
+ }
11
+ return out.join("");
12
+ }
13
+ // Minimal normalization only (03 §2.2): the op supplies the block's markdown as
14
+ // `raw`; we do not reformat it. Separation between siblings is owned by trivia,
15
+ // so serialization here is the identity on the supplied text. (Container-aware
16
+ // indentation and blank-line insertion arrive with the mutation kernel.)
17
+ export function serializeNew(block) {
18
+ return block.raw;
19
+ }
20
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1,8 @@
1
+ import type { BlockTree } from "./types.js";
2
+ export declare function parseTree(source: string): BlockTree;
3
+ /**
4
+ * Coverage assertion (03 §2.1 invariant #2): every byte of the source is owned
5
+ * by exactly one top-level block's raw or trivia — no gaps, no overlaps.
6
+ * Returns true iff the tree tiles the source exactly.
7
+ */
8
+ export declare function assertFullCoverage(tree: BlockTree): boolean;
@@ -0,0 +1,43 @@
1
+ import { parseBlocks } from "./parse.js";
2
+ // Trivia attachment (03 §2.3): inter-block bytes attach to the PRECEDING block
3
+ // (trailing-attach). Bytes before the first block are document-leading trivia.
4
+ //
5
+ // As-built scope: trivia is tiled at the top level, which is what the splice
6
+ // renderer (03 §2.2) walks. `leadingTrivia + Σ(block.raw + block.trivia)`
7
+ // reconstructs the source byte-for-byte. Nested children retain their `raw`
8
+ // (a substring of the parent's raw) for structure/identity; splicing *inside* a
9
+ // container is deferred to the mutation stage and does not affect round-trip.
10
+ export function parseTree(source) {
11
+ const children = parseBlocks(source);
12
+ const leadingTrivia = children.length === 0 ? source : source.slice(0, children[0].span.start);
13
+ for (let i = 0; i < children.length; i++) {
14
+ const block = children[i];
15
+ const next = children[i + 1];
16
+ const triviaEnd = next ? next.span.start : source.length;
17
+ block.trivia = source.slice(block.span.end, triviaEnd);
18
+ }
19
+ return { source, leadingTrivia, children };
20
+ }
21
+ /**
22
+ * Coverage assertion (03 §2.1 invariant #2): every byte of the source is owned
23
+ * by exactly one top-level block's raw or trivia — no gaps, no overlaps.
24
+ * Returns true iff the tree tiles the source exactly.
25
+ */
26
+ export function assertFullCoverage(tree) {
27
+ let cursor = 0;
28
+ if (tree.leadingTrivia !== tree.source.slice(0, tree.leadingTrivia.length))
29
+ return false;
30
+ cursor = tree.leadingTrivia.length;
31
+ for (const block of tree.children) {
32
+ if (block.span.start !== cursor)
33
+ return false; // gap or overlap
34
+ if (block.raw !== tree.source.slice(block.span.start, block.span.end))
35
+ return false;
36
+ cursor = block.span.end;
37
+ if (block.trivia !== tree.source.slice(cursor, cursor + block.trivia.length))
38
+ return false;
39
+ cursor += block.trivia.length;
40
+ }
41
+ return cursor === tree.source.length;
42
+ }
43
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1,52 @@
1
+ /** General block kind — format-qualified string (e.g. "md:heading", "yaml:mapping_entry"). */
2
+ export type BlockKind = string;
3
+ /** Markdown-specific block types. Subset of BlockKind used by the markdown parser. */
4
+ export type BlockType = "heading" | "paragraph" | "list" | "list_item" | "task" | "blockquote" | "code_fence" | "table" | "table_row" | "thematic_break" | "html_block" | "frontmatter" | "opaque";
5
+ /** Half-open [start, end) offsets into the source string. */
6
+ export interface Span {
7
+ start: number;
8
+ end: number;
9
+ }
10
+ export interface RawBlock {
11
+ type: BlockKind;
12
+ span: Span;
13
+ /** Exact source slice for this block's content (excludes trailing trivia). */
14
+ raw: string;
15
+ /** Normalized visible text (02 §5.2 — refined in Stage 1). */
16
+ text: string;
17
+ /** Typed attributes: level, ordered, checked, lang, info, … */
18
+ attrs: Record<string, unknown>;
19
+ /** Parser nesting only (lists, list items, blockquotes, tables). */
20
+ children: RawBlock[];
21
+ /** Trailing inter-block trivia attached to this block (03 §2.3). */
22
+ trivia: string;
23
+ /**
24
+ * Set by mutation ops: this block's `raw` is op-supplied markdown and must be
25
+ * serialized (03 §2.2) rather than spliced verbatim. Absent/false = untouched.
26
+ */
27
+ dirty?: boolean;
28
+ /** Authored ^block-ref anchors (Stage 4). */
29
+ anchors: string[];
30
+ /** Extracted outgoing links for edge extraction (Stage 4). */
31
+ outLinks: ExtractedLink[];
32
+ /**
33
+ * Assigned block id, populated ONLY for node projection (ingest zips the
34
+ * assigned tree onto the parsed blocks before calling projectNodes) so an
35
+ * adapter can anchor each node to its own block. Absent during parse/reconcile.
36
+ */
37
+ blockId?: string;
38
+ }
39
+ export interface ExtractedLink {
40
+ kind: "link" | "wikilink" | "image" | "inline_field" | "url";
41
+ target: string;
42
+ anchor?: string;
43
+ field?: string;
44
+ }
45
+ export interface BlockTree {
46
+ /** The decoded source string this tree was parsed from. */
47
+ source: string;
48
+ /** Bytes before the first block (document-leading trivia, 03 §2.3). */
49
+ leadingTrivia: string;
50
+ /** Top-level blocks in document order (flat containment). */
51
+ children: RawBlock[];
52
+ }
@@ -0,0 +1,9 @@
1
+ // Block model — architecture §3.3, reconciliation-spec §1.
2
+ //
3
+ // As-built note: the design (03 §1) types spans as byte offsets and `raw` as
4
+ // Uint8Array. We work in string space instead — mdast/micromark positions are
5
+ // offsets into the decoded source string, and byte-identity round-trip follows
6
+ // from deterministic UTF-8 encoding of equal strings. Hashing (02 §5) encodes
7
+ // UTF-8 at hash time. This keeps offset handling robust and simple.
8
+ export {};
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,107 @@
1
+ import type { Database } from "better-sqlite3";
2
+ import type { Store } from "../store/store.js";
3
+ export interface DocsReadResult {
4
+ path: string;
5
+ docId: string;
6
+ rev: string | null;
7
+ /** Properties grouped by source: { frontmatter, inline, computed }. */
8
+ properties: Record<string, Record<string, unknown>>;
9
+ content: string;
10
+ /** Block ids in document order, for follow-up edits (include_ids). */
11
+ ids?: string[];
12
+ /**
13
+ * Block id → current content hash (raw-bytes hash, hex) for every id in
14
+ * `ids` (include_ids). This is the exact value the raw `apply` kernel wants in
15
+ * an update/split/merge/remove op's `expect.content_hash`, so a single read
16
+ * yields both the stable ids AND the CAS tokens to mutate them — no follow-up
17
+ * nodes_get_many(resolution:"full") hydration round trip.
18
+ */
19
+ hashes?: Record<string, string>;
20
+ /**
21
+ * Block id → parent block id (null = top level) for every id in `ids`
22
+ * (include_ids). `ids` is a flat pre-order walk, so without this a caller
23
+ * cannot tell a top-level block from a nested one (a list from its items)
24
+ * without a second outline read — this makes "the top-level blocks of these
25
+ * sections" a local filter.
26
+ */
27
+ parents?: Record<string, string | null>;
28
+ }
29
+ export interface DocsReadOptions {
30
+ includeIds?: boolean;
31
+ }
32
+ /**
33
+ * Reconstruct a document's full file bytes from its ordered top-level blocks.
34
+ * Takes a raw Database handle so it can run inside an ingest transaction (the
35
+ * convergence check reloads through this) as well as from a Store.
36
+ */
37
+ export declare function reconstructContent(db: Database, docId: string): string | null;
38
+ /** Read a whole document by id: full file bytes + properties. Null if missing. */
39
+ export declare function docsRead(store: Store, docId: string, opts?: DocsReadOptions): DocsReadResult | null;
40
+ /** Max doc refs honored per docsReadMany call (matches nodesGetMany's cap). */
41
+ export declare const MANY_DOCS_CAP = 100;
42
+ export interface DocsReadManyError {
43
+ /** The original ref (id or path) that failed to resolve. */
44
+ ref: string;
45
+ /** Stable error code — currently always doc_not_found (unresolvable ref). */
46
+ error: "doc_not_found";
47
+ }
48
+ export interface DocsReadManyResult {
49
+ /** Full reads, one per resolved (deduped) ref, same shape as docsRead. */
50
+ items: DocsReadResult[];
51
+ /** Per-ref misses; present but empty when every ref resolved. */
52
+ errors: DocsReadManyError[];
53
+ /** True when refs exceeded the cap or the token budget cut the batch short. */
54
+ truncated: boolean;
55
+ }
56
+ /**
57
+ * Read many whole documents by ref (id or path) in one call. Found docs land in
58
+ * `items` (each a full docsRead projection); unresolvable refs land in `errors`
59
+ * without failing the call. Duplicate refs collapse first-seen. The raw list is
60
+ * capped at MANY_DOCS_CAP (excess ⇒ truncated); an optional token budget stops
61
+ * the batch early (also ⇒ truncated).
62
+ */
63
+ export declare function docsReadMany(store: Store, repoId: string, refs: string[], opts?: {
64
+ includeIds?: boolean;
65
+ budgetTokens?: number;
66
+ }): DocsReadManyResult;
67
+ export interface DocsReadAtResult {
68
+ path: string;
69
+ docId: string;
70
+ /** The revision travelled to (the argument, echoed for confirmation). */
71
+ rev: string;
72
+ content: string;
73
+ /** True when the reconstructed bytes' sha256 matched the revision's stored
74
+ * rendered_hash — a byte-faithful round-trip. False flags the fidelity gap
75
+ * described below (doc-level trivia may differ for an old revision). */
76
+ renderedHashMatch: boolean;
77
+ /**
78
+ * Properties as they are CURRENTLY (not at `rev`): the properties table is
79
+ * current-state only (properties-table), so historical property values are
80
+ * not recoverable. Included as best-effort context, flagged by
81
+ * `propertiesAreCurrent: true`.
82
+ */
83
+ properties: Record<string, Record<string, unknown>>;
84
+ propertiesAreCurrent: true;
85
+ }
86
+ /**
87
+ * Whole-document TIME-TRAVEL read: reconstruct a document's full file bytes AS
88
+ * OF a past revision `revId`, using the SAME assembly order as
89
+ * `reconstructContent` — leading_trivia + frontmatter (from THAT revision's
90
+ * frontmatter_blob) + frontmatter_trivia + Σ(top-level block raw + its trivia)
91
+ * IN ORDER — but sourced from the revision's Merkle `root_tree` instead of the
92
+ * live `blocks` table. Returns null when the doc is missing, or when `revId`
93
+ * does not name a revision of this document (so the caller can raise a clean
94
+ * not-found error).
95
+ *
96
+ * FIDELITY CAVEAT: `leading_trivia` and `frontmatter_trivia` are stored ONLY on
97
+ * the current `docs` row, NOT per-revision (see schema.ts — the `docs` row is
98
+ * document-level current state). So for the CURRENT revision they are exact and
99
+ * `renderedHashMatch` is true; for a PAST revision they may be wrong if the
100
+ * document's leading/frontmatter trivia changed since — in practice these rarely
101
+ * change, so we use the current doc-row values as a documented best-effort. The
102
+ * block bytes themselves ARE per-revision and always faithful. `renderedHashMatch`
103
+ * is the honest signal: true ⇒ the reconstruction is byte-for-byte the file at
104
+ * that revision; false ⇒ it differs only by those doc-level trivia bytes (block
105
+ * content still reconstructs correctly).
106
+ */
107
+ export declare function readDocumentAtRevision(store: Store, docId: string, revId: string): DocsReadAtResult | null;
@@ -0,0 +1,191 @@
1
+ import { findDoc, findDocByRef, loadDocBlocks } from "./reader.js";
2
+ import { docPropertiesGrouped } from "../store/properties.js";
3
+ import { sha256 } from "../hash.js";
4
+ function blob(db, hash) {
5
+ const row = db.prepare("SELECT bytes FROM blobs WHERE hash = ?").get(hash);
6
+ return row ? row.bytes.toString("utf8") : "";
7
+ }
8
+ /**
9
+ * Reconstruct a document's full file bytes from its ordered top-level blocks.
10
+ * Takes a raw Database handle so it can run inside an ingest transaction (the
11
+ * convergence check reloads through this) as well as from a Store.
12
+ */
13
+ export function reconstructContent(db, docId) {
14
+ const doc = db
15
+ .prepare("SELECT leading_trivia, frontmatter_trivia, current_rev FROM docs WHERE doc_id = ? AND deleted_commit IS NULL")
16
+ .get(docId);
17
+ if (!doc)
18
+ return null;
19
+ const out = [doc.leading_trivia];
20
+ if (doc.current_rev) {
21
+ const rev = db.prepare("SELECT frontmatter_blob FROM revisions WHERE rev_id = ?").get(doc.current_rev);
22
+ // Emit the frontmatter block followed by its exact stored separator. Ingest
23
+ // always captures frontmatter_trivia whenever a frontmatter block exists, so
24
+ // frontmatter_blob present ⇒ frontmatter_trivia is a real string (possibly
25
+ // ""); the ?? "" is just a type guard.
26
+ if (rev?.frontmatter_blob)
27
+ out.push(blob(db, rev.frontmatter_blob) + (doc.frontmatter_trivia ?? ""));
28
+ }
29
+ // Top-level blocks only: a container's `raw` already contains its nested
30
+ // children verbatim (03 §2.3), so walking roots reconstructs the whole body.
31
+ const rows = db
32
+ .prepare(`SELECT raw_hash, trivia_hash FROM blocks
33
+ WHERE doc_id = ? AND parent_block IS NULL AND deleted_commit IS NULL
34
+ ORDER BY order_key`)
35
+ .all(docId);
36
+ for (const r of rows) {
37
+ out.push(blob(db, r.raw_hash));
38
+ if (r.trivia_hash)
39
+ out.push(blob(db, r.trivia_hash));
40
+ }
41
+ return out.join("");
42
+ }
43
+ /** Read a whole document by id: full file bytes + properties. Null if missing. */
44
+ export function docsRead(store, docId, opts = {}) {
45
+ const info = findDoc(store, { docId });
46
+ if (!info)
47
+ return null;
48
+ const content = reconstructContent(store.db, docId);
49
+ if (content === null)
50
+ return null;
51
+ const result = {
52
+ path: info.path,
53
+ docId: info.docId,
54
+ rev: info.currentRev,
55
+ properties: docPropertiesGrouped(store.db, docId),
56
+ content,
57
+ };
58
+ if (opts.includeIds) {
59
+ const ids = [];
60
+ const hashes = {};
61
+ const parents = {};
62
+ const collect = (nodes) => {
63
+ for (const n of nodes) {
64
+ ids.push(n.blockId);
65
+ // The block's raw-bytes hash and parent are already loaded on the node,
66
+ // so exposing them costs nothing beyond the id walk.
67
+ hashes[n.blockId] = n.rawHashHex;
68
+ parents[n.blockId] = n.parentBlock;
69
+ collect(n.children);
70
+ }
71
+ };
72
+ collect(loadDocBlocks(store, docId));
73
+ result.ids = ids;
74
+ result.hashes = hashes;
75
+ result.parents = parents;
76
+ }
77
+ return result;
78
+ }
79
+ // Batch whole-document read (the hydrate half of query→hydrate). The batch
80
+ // analog of docsRead: given several doc refs (each an id OR a path, via the
81
+ // shared findDocByRef dispatch), it returns `items` (the full reads, identical
82
+ // in shape to docsRead) alongside `errors` (per-ref misses). A miss NEVER fails
83
+ // the whole call — mirrors nodes_getMany's tolerance of unknown ids, but made
84
+ // explicit as an errors array so a caller can tell which refs didn't resolve.
85
+ // Duplicate refs collapse first-seen (a repeated ref yields one item). Capped
86
+ // at MANY_DOCS_CAP refs per call (matching nodesGetMany's 100), with the same
87
+ // budget-token truncation and `truncated` flag.
88
+ /** Max doc refs honored per docsReadMany call (matches nodesGetMany's cap). */
89
+ export const MANY_DOCS_CAP = 100;
90
+ /**
91
+ * Read many whole documents by ref (id or path) in one call. Found docs land in
92
+ * `items` (each a full docsRead projection); unresolvable refs land in `errors`
93
+ * without failing the call. Duplicate refs collapse first-seen. The raw list is
94
+ * capped at MANY_DOCS_CAP (excess ⇒ truncated); an optional token budget stops
95
+ * the batch early (also ⇒ truncated).
96
+ */
97
+ export function docsReadMany(store, repoId, refs, opts = {}) {
98
+ const capped = refs.slice(0, MANY_DOCS_CAP);
99
+ let truncated = refs.length > MANY_DOCS_CAP;
100
+ const budget = opts.budgetTokens ?? Infinity;
101
+ const readOpts = opts.includeIds ? { includeIds: true } : {};
102
+ const items = [];
103
+ const errors = [];
104
+ const seen = new Set();
105
+ let tokens = 0;
106
+ for (const ref of capped) {
107
+ // First-seen wins: a repeated ref (same literal string) is collapsed.
108
+ if (seen.has(ref))
109
+ continue;
110
+ seen.add(ref);
111
+ const info = findDocByRef(store, repoId, ref);
112
+ // Unresolvable ref (or a d_-shaped id with no doc) → a per-ref miss, never
113
+ // a thrown error. reconstructContent returning null is the same miss.
114
+ const read = info ? docsRead(store, info.docId, readOpts) : null;
115
+ if (!read) {
116
+ errors.push({ ref, error: "doc_not_found" });
117
+ continue;
118
+ }
119
+ const cost = Math.ceil(JSON.stringify(read).length / 4);
120
+ if (tokens + cost > budget) {
121
+ truncated = true;
122
+ break;
123
+ }
124
+ tokens += cost;
125
+ items.push(read);
126
+ }
127
+ return { items, errors, truncated };
128
+ }
129
+ /**
130
+ * Whole-document TIME-TRAVEL read: reconstruct a document's full file bytes AS
131
+ * OF a past revision `revId`, using the SAME assembly order as
132
+ * `reconstructContent` — leading_trivia + frontmatter (from THAT revision's
133
+ * frontmatter_blob) + frontmatter_trivia + Σ(top-level block raw + its trivia)
134
+ * IN ORDER — but sourced from the revision's Merkle `root_tree` instead of the
135
+ * live `blocks` table. Returns null when the doc is missing, or when `revId`
136
+ * does not name a revision of this document (so the caller can raise a clean
137
+ * not-found error).
138
+ *
139
+ * FIDELITY CAVEAT: `leading_trivia` and `frontmatter_trivia` are stored ONLY on
140
+ * the current `docs` row, NOT per-revision (see schema.ts — the `docs` row is
141
+ * document-level current state). So for the CURRENT revision they are exact and
142
+ * `renderedHashMatch` is true; for a PAST revision they may be wrong if the
143
+ * document's leading/frontmatter trivia changed since — in practice these rarely
144
+ * change, so we use the current doc-row values as a documented best-effort. The
145
+ * block bytes themselves ARE per-revision and always faithful. `renderedHashMatch`
146
+ * is the honest signal: true ⇒ the reconstruction is byte-for-byte the file at
147
+ * that revision; false ⇒ it differs only by those doc-level trivia bytes (block
148
+ * content still reconstructs correctly).
149
+ */
150
+ export function readDocumentAtRevision(store, docId, revId) {
151
+ const db = store.db;
152
+ const doc = db
153
+ .prepare("SELECT path, leading_trivia, frontmatter_trivia FROM docs WHERE doc_id = ? AND deleted_commit IS NULL")
154
+ .get(docId);
155
+ if (!doc)
156
+ return null;
157
+ const rev = db.prepare("SELECT root_tree, frontmatter_blob, rendered_hash, path FROM revisions WHERE rev_id = ? AND doc_id = ?").get(revId, docId);
158
+ if (!rev)
159
+ return null;
160
+ const out = [doc.leading_trivia];
161
+ // Frontmatter blob for THIS revision (per-revision), then the doc-level
162
+ // separator (best-effort — see fidelity caveat).
163
+ if (rev.frontmatter_blob)
164
+ out.push(blob(db, rev.frontmatter_blob) + (doc.frontmatter_trivia ?? ""));
165
+ // Top-level blocks in tree order, from THIS revision's Merkle root tree node.
166
+ // A container's raw already holds its children verbatim, so we deliberately do
167
+ // NOT recurse into childTreeHashHex (mirrors reconstructContent's
168
+ // parent_block IS NULL walk). An absent tree node yields empty body bytes.
169
+ const node = db.prepare("SELECT entries FROM tree_nodes WHERE hash = ?").get(rev.root_tree);
170
+ if (node) {
171
+ const entries = JSON.parse(node.entries);
172
+ for (const [, rawHashHex, , , , triviaHashHex] of entries) {
173
+ out.push(blob(db, Buffer.from(rawHashHex, "hex")));
174
+ if (triviaHashHex)
175
+ out.push(blob(db, Buffer.from(triviaHashHex, "hex")));
176
+ }
177
+ }
178
+ const content = out.join("");
179
+ const renderedHashMatch = sha256(content).equals(rev.rendered_hash);
180
+ return {
181
+ // The revision records the path as of that revision (docs may be moved).
182
+ path: rev.path,
183
+ docId,
184
+ rev: revId,
185
+ content,
186
+ renderedHashMatch,
187
+ properties: docPropertiesGrouped(db, docId),
188
+ propertiesAreCurrent: true,
189
+ };
190
+ }
191
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1,47 @@
1
+ import type { Store } from "../store/store.js";
2
+ export type Resolution = "skeleton" | "outline" | "text" | "raw" | "full";
3
+ export interface GetNode {
4
+ id: string;
5
+ type: string;
6
+ label?: string;
7
+ text?: string;
8
+ raw?: string;
9
+ /** The block's raw_hash (hex) — the value update/split expect as content_hash. */
10
+ content_hash?: string;
11
+ attrs?: Record<string, unknown>;
12
+ placement?: {
13
+ parent: string | null;
14
+ ordinal: number;
15
+ depth: number;
16
+ };
17
+ children?: GetNode[];
18
+ }
19
+ export interface NodesGetOptions {
20
+ resolution?: Resolution;
21
+ include?: ("children" | "ancestors")[];
22
+ }
23
+ /** Fetch a single block subtree at a resolution. Returns null if missing. */
24
+ export declare function nodesGet(store: Store, docId: string, blockId: string, opts?: NodesGetOptions): GetNode | null;
25
+ export interface NodesGetManyResult {
26
+ nodes: GetNode[];
27
+ truncated: boolean;
28
+ /**
29
+ * Requested ids (within the first 100) that name NO live block — or, when a
30
+ * `docId` scope is given, no live block in THAT document. Ids dropped by the
31
+ * 100-id cap or the token budget are NOT listed here; they are reported by
32
+ * `truncated` instead. Never silently dropped.
33
+ */
34
+ unresolved: string[];
35
+ }
36
+ /**
37
+ * Fetch up to 100 blocks by id, with budget truncation (06 §3). Block ids are
38
+ * globally unique, so `docId` is an optional SCOPE, not a requirement: with
39
+ * `null` each id is resolved to its owning document (`blocks.doc_id`), the ids
40
+ * are grouped by doc, each doc's forest is loaded once, and the nodes come back
41
+ * in request order. With a `docId`, ids from other documents count as
42
+ * `unresolved` (the caller asked for that doc's blocks).
43
+ */
44
+ export declare function nodesGetMany(store: Store, docId: string | null, blockIds: string[], opts?: {
45
+ resolution?: Resolution;
46
+ budgetTokens?: number;
47
+ }): NodesGetManyResult;