@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.
- package/LICENSE +21 -0
- package/dist/src/cli/eval-matcher.d.ts +13 -0
- package/dist/src/cli/eval-matcher.js +36 -0
- package/dist/src/core/attach.d.ts +5 -0
- package/dist/src/core/attach.js +38 -0
- package/dist/src/core/cursor.d.ts +11 -0
- package/dist/src/core/cursor.js +35 -0
- package/dist/src/core/hash.d.ts +23 -0
- package/dist/src/core/hash.js +82 -0
- package/dist/src/core/ids.d.ts +6 -0
- package/dist/src/core/ids.js +26 -0
- package/dist/src/core/index.d.ts +1 -0
- package/dist/src/core/index.js +2 -0
- package/dist/src/core/ingest.d.ts +43 -0
- package/dist/src/core/ingest.js +287 -0
- package/dist/src/core/order-key.d.ts +8 -0
- package/dist/src/core/order-key.js +72 -0
- package/dist/src/core/parse/parse.d.ts +3 -0
- package/dist/src/core/parse/parse.js +104 -0
- package/dist/src/core/parse/render.d.ts +3 -0
- package/dist/src/core/parse/render.js +20 -0
- package/dist/src/core/parse/tree.d.ts +8 -0
- package/dist/src/core/parse/tree.js +43 -0
- package/dist/src/core/parse/types.d.ts +52 -0
- package/dist/src/core/parse/types.js +9 -0
- package/dist/src/core/read/document.d.ts +107 -0
- package/dist/src/core/read/document.js +191 -0
- package/dist/src/core/read/nodes.d.ts +47 -0
- package/dist/src/core/read/nodes.js +102 -0
- package/dist/src/core/read/outline.d.ts +12 -0
- package/dist/src/core/read/outline.js +76 -0
- package/dist/src/core/read/reader.d.ts +125 -0
- package/dist/src/core/read/reader.js +206 -0
- package/dist/src/core/read/refs.d.ts +15 -0
- package/dist/src/core/read/refs.js +51 -0
- package/dist/src/core/store/edges.d.ts +26 -0
- package/dist/src/core/store/edges.js +101 -0
- package/dist/src/core/store/fts.d.ts +10 -0
- package/dist/src/core/store/fts.js +22 -0
- package/dist/src/core/store/gc.d.ts +18 -0
- package/dist/src/core/store/gc.js +69 -0
- package/dist/src/core/store/nodes.d.ts +5 -0
- package/dist/src/core/store/nodes.js +57 -0
- package/dist/src/core/store/properties.d.ts +38 -0
- package/dist/src/core/store/properties.js +177 -0
- package/dist/src/core/store/rebuild.d.ts +3 -0
- package/dist/src/core/store/rebuild.js +29 -0
- package/dist/src/core/store/schema.d.ts +9 -0
- package/dist/src/core/store/schema.js +386 -0
- package/dist/src/core/store/sections.d.ts +2 -0
- package/dist/src/core/store/sections.js +29 -0
- package/dist/src/core/store/store.d.ts +24 -0
- package/dist/src/core/store/store.js +164 -0
- package/dist/src/core/store/writers.d.ts +52 -0
- package/dist/src/core/store/writers.js +89 -0
- package/dist/src/core/vec.d.ts +10 -0
- package/dist/src/core/vec.js +33 -0
- package/dist/src/format/adapter.d.ts +97 -0
- package/dist/src/format/adapter.js +14 -0
- package/dist/src/format/index.d.ts +6 -0
- package/dist/src/format/index.js +15 -0
- package/dist/src/format/json.d.ts +3 -0
- package/dist/src/format/json.js +324 -0
- package/dist/src/format/markdown.d.ts +5 -0
- package/dist/src/format/markdown.js +217 -0
- package/dist/src/format/registry.d.ts +6 -0
- package/dist/src/format/registry.js +24 -0
- package/dist/src/format/yaml.d.ts +5 -0
- package/dist/src/format/yaml.js +413 -0
- package/dist/src/graph/extract.d.ts +38 -0
- package/dist/src/graph/extract.js +242 -0
- package/dist/src/graph/history.d.ts +103 -0
- package/dist/src/graph/history.js +210 -0
- package/dist/src/graph/inbound-links.d.ts +33 -0
- package/dist/src/graph/inbound-links.js +130 -0
- package/dist/src/graph/link-destinations.d.ts +22 -0
- package/dist/src/graph/link-destinations.js +84 -0
- package/dist/src/graph/link-health.d.ts +79 -0
- package/dist/src/graph/link-health.js +146 -0
- package/dist/src/graph/links.d.ts +23 -0
- package/dist/src/graph/links.js +42 -0
- package/dist/src/index.d.ts +60 -0
- package/dist/src/index.js +70 -0
- package/dist/src/mcp/errors.d.ts +17 -0
- package/dist/src/mcp/errors.js +18 -0
- package/dist/src/mcp/graph.d.ts +63 -0
- package/dist/src/mcp/graph.js +212 -0
- package/dist/src/mcp/reference.d.ts +1 -0
- package/dist/src/mcp/reference.js +227 -0
- package/dist/src/mcp/server.d.ts +30 -0
- package/dist/src/mcp/server.js +1076 -0
- package/dist/src/mcp/stdio.d.ts +13 -0
- package/dist/src/mcp/stdio.js +24 -0
- package/dist/src/migrate/mrplex.d.ts +27 -0
- package/dist/src/migrate/mrplex.js +29 -0
- package/dist/src/mutate/apply.d.ts +83 -0
- package/dist/src/mutate/apply.js +302 -0
- package/dist/src/mutate/doc-store.d.ts +49 -0
- package/dist/src/mutate/doc-store.js +71 -0
- package/dist/src/mutate/docs.d.ts +60 -0
- package/dist/src/mutate/docs.js +232 -0
- package/dist/src/mutate/known-ids.d.ts +12 -0
- package/dist/src/mutate/known-ids.js +154 -0
- package/dist/src/mutate/load.d.ts +3 -0
- package/dist/src/mutate/load.js +66 -0
- package/dist/src/mutate/lower.d.ts +32 -0
- package/dist/src/mutate/lower.js +232 -0
- package/dist/src/mutate/macros.d.ts +91 -0
- package/dist/src/mutate/macros.js +230 -0
- package/dist/src/mutate/ops.d.ts +40 -0
- package/dist/src/mutate/ops.js +513 -0
- package/dist/src/mutate/opset.d.ts +92 -0
- package/dist/src/mutate/opset.js +79 -0
- package/dist/src/mutate/plan-update.d.ts +51 -0
- package/dist/src/mutate/plan-update.js +187 -0
- package/dist/src/mutate/tree.d.ts +46 -0
- package/dist/src/mutate/tree.js +167 -0
- package/dist/src/oqx/run.d.ts +1 -0
- package/dist/src/oqx/run.js +6 -0
- package/dist/src/oqx-js/context.d.ts +23 -0
- package/dist/src/oqx-js/context.js +564 -0
- package/dist/src/oqx-js/planner.d.ts +10 -0
- package/dist/src/oqx-js/planner.js +91 -0
- package/dist/src/oqx-js/run.d.ts +41 -0
- package/dist/src/oqx-js/run.js +332 -0
- package/dist/src/oqx-js/sql/translate.d.ts +27 -0
- package/dist/src/oqx-js/sql/translate.js +290 -0
- package/dist/src/reconcile/crossdoc.d.ts +34 -0
- package/dist/src/reconcile/crossdoc.js +76 -0
- package/dist/src/reconcile/eval/generator.d.ts +21 -0
- package/dist/src/reconcile/eval/generator.js +129 -0
- package/dist/src/reconcile/eval/metrics.d.ts +32 -0
- package/dist/src/reconcile/eval/metrics.js +74 -0
- package/dist/src/reconcile/flatten.d.ts +12 -0
- package/dist/src/reconcile/flatten.js +37 -0
- package/dist/src/reconcile/phase5.d.ts +8 -0
- package/dist/src/reconcile/phase5.js +167 -0
- package/dist/src/reconcile/phase6a.d.ts +2 -0
- package/dist/src/reconcile/phase6a.js +159 -0
- package/dist/src/reconcile/phases.d.ts +28 -0
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.d.ts +21 -0
- package/dist/src/reconcile/reconcile.js +115 -0
- package/dist/src/reconcile/similarity.d.ts +7 -0
- package/dist/src/reconcile/similarity.js +41 -0
- package/dist/src/reconcile/types.d.ts +46 -0
- package/dist/src/reconcile/types.js +19 -0
- package/dist/src/search/cel/ast.d.ts +74 -0
- package/dist/src/search/cel/ast.js +3 -0
- package/dist/src/search/cel/compile.d.ts +60 -0
- package/dist/src/search/cel/compile.js +836 -0
- package/dist/src/search/cel/lexer.d.ts +9 -0
- package/dist/src/search/cel/lexer.js +139 -0
- package/dist/src/search/cel/parser.d.ts +7 -0
- package/dist/src/search/cel/parser.js +219 -0
- package/dist/src/search/drain.d.ts +39 -0
- package/dist/src/search/drain.js +97 -0
- package/dist/src/search/embeddings.d.ts +131 -0
- package/dist/src/search/embeddings.js +263 -0
- package/dist/src/search/external.d.ts +13 -0
- package/dist/src/search/external.js +177 -0
- package/dist/src/search/fts-query.d.ts +1 -0
- package/dist/src/search/fts-query.js +64 -0
- package/dist/src/search/provider.d.ts +17 -0
- package/dist/src/search/provider.js +13 -0
- package/dist/src/search/resolve.d.ts +18 -0
- package/dist/src/search/resolve.js +23 -0
- package/dist/src/search/rrf.d.ts +36 -0
- package/dist/src/search/rrf.js +84 -0
- package/dist/src/search/tasks.d.ts +12 -0
- package/dist/src/search/tasks.js +131 -0
- package/dist/src/search/text.d.ts +16 -0
- package/dist/src/search/text.js +21 -0
- package/dist/src/search/vector.d.ts +18 -0
- package/dist/src/search/vector.js +49 -0
- package/dist/src/sync/admin.d.ts +59 -0
- package/dist/src/sync/admin.js +53 -0
- package/dist/src/sync/attach.d.ts +13 -0
- package/dist/src/sync/attach.js +29 -0
- package/dist/src/sync/checkpoint.d.ts +21 -0
- package/dist/src/sync/checkpoint.js +52 -0
- package/dist/src/sync/driver.d.ts +25 -0
- package/dist/src/sync/driver.js +80 -0
- package/dist/src/sync/external-source.d.ts +14 -0
- package/dist/src/sync/external-source.js +137 -0
- package/dist/src/sync/freshness.d.ts +42 -0
- package/dist/src/sync/freshness.js +152 -0
- package/dist/src/sync/fs-util.d.ts +12 -0
- package/dist/src/sync/fs-util.js +55 -0
- package/dist/src/sync/git-heuristics.d.ts +6 -0
- package/dist/src/sync/git-heuristics.js +16 -0
- package/dist/src/sync/observe.d.ts +63 -0
- package/dist/src/sync/observe.js +84 -0
- package/dist/src/sync/plugin.d.ts +54 -0
- package/dist/src/sync/plugin.js +12 -0
- package/dist/src/sync/reconciling-ingest.d.ts +12 -0
- package/dist/src/sync/reconciling-ingest.js +209 -0
- package/dist/src/sync/recovery.d.ts +6 -0
- package/dist/src/sync/recovery.js +34 -0
- package/dist/src/sync/settings.d.ts +18 -0
- package/dist/src/sync/settings.js +58 -0
- package/dist/src/sync/sources.d.ts +37 -0
- package/dist/src/sync/sources.js +75 -0
- package/dist/src/sync/tombstone.d.ts +9 -0
- package/dist/src/sync/tombstone.js +55 -0
- package/dist/src/sync/watch-lease.d.ts +14 -0
- package/dist/src/sync/watch-lease.js +91 -0
- package/dist/src/sync/watcher.d.ts +19 -0
- package/dist/src/sync/watcher.js +34 -0
- package/dist/src/sync/workspace.d.ts +34 -0
- package/dist/src/sync/workspace.js +120 -0
- package/dist/src/sync/writer-lock.d.ts +16 -0
- package/dist/src/sync/writer-lock.js +121 -0
- 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,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,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;
|