@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.
- package/README.md +172 -0
- package/dist/src/cli/eval-matcher.d.ts +6 -1
- package/dist/src/cli/eval-matcher.js +14 -2
- package/dist/src/core/clock.d.ts +9 -0
- package/dist/src/core/clock.js +46 -0
- package/dist/src/core/hash.d.ts +25 -1
- package/dist/src/core/hash.js +110 -10
- package/dist/src/core/ids.d.ts +12 -0
- package/dist/src/core/ids.js +43 -1
- package/dist/src/core/ingest.d.ts +22 -2
- package/dist/src/core/ingest.js +92 -18
- package/dist/src/core/parse/parse.d.ts +1 -1
- package/dist/src/core/parse/parse.js +31 -9
- package/dist/src/core/store/nodes.d.ts +13 -0
- package/dist/src/core/store/nodes.js +24 -0
- package/dist/src/core/store/writers.d.ts +6 -0
- package/dist/src/core/store/writers.js +1 -0
- package/dist/src/core/utf8.d.ts +29 -0
- package/dist/src/core/utf8.js +96 -0
- package/dist/src/format/adapter.d.ts +7 -1
- package/dist/src/format/markdown.js +36 -41
- package/dist/src/graph/extract.d.ts +41 -1
- package/dist/src/graph/extract.js +59 -1
- package/dist/src/graph/history.d.ts +39 -1
- package/dist/src/graph/history.js +154 -20
- package/dist/src/index.d.ts +10 -6
- package/dist/src/index.js +11 -6
- package/dist/src/mcp/graph.js +8 -4
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +17 -6
- package/dist/src/mcp/server.js +15 -10
- package/dist/src/mutate/apply.d.ts +9 -0
- package/dist/src/mutate/apply.js +24 -11
- package/dist/src/mutate/docs.d.ts +2 -0
- package/dist/src/mutate/docs.js +10 -6
- package/dist/src/mutate/known-ids.js +11 -3
- package/dist/src/mutate/macros.js +9 -2
- package/dist/src/mutate/ops.d.ts +5 -4
- package/dist/src/mutate/ops.js +62 -15
- package/dist/src/mutate/plan-update.d.ts +7 -1
- package/dist/src/mutate/plan-update.js +3 -1
- package/dist/src/oqx-js/context.js +57 -7
- package/dist/src/reconcile/eval/generator.d.ts +24 -1
- package/dist/src/reconcile/eval/generator.js +248 -0
- package/dist/src/reconcile/eval/metrics.d.ts +33 -9
- package/dist/src/reconcile/eval/metrics.js +55 -32
- package/dist/src/reconcile/flatten.js +7 -5
- package/dist/src/reconcile/phase5.d.ts +2 -1
- package/dist/src/reconcile/phase5.js +27 -10
- package/dist/src/reconcile/phase6a.js +17 -5
- package/dist/src/reconcile/phases.d.ts +44 -6
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.js +16 -9
- package/dist/src/reconcile/types.d.ts +4 -1
- package/dist/src/reconcile/types.js +11 -2
- package/dist/src/search/external.d.ts +10 -0
- package/dist/src/search/external.js +18 -1
- package/dist/src/search/provider.d.ts +7 -3
- package/dist/src/search/provider.js +8 -7
- package/dist/src/search/rrf.js +4 -0
- package/dist/src/search/tasks.d.ts +28 -0
- package/dist/src/search/tasks.js +36 -23
- package/dist/src/search/vector.js +30 -14
- package/dist/src/sync/attach.d.ts +7 -2
- package/dist/src/sync/attach.js +10 -8
- package/dist/src/sync/checkpoint.d.ts +18 -1
- package/dist/src/sync/checkpoint.js +31 -29
- package/dist/src/sync/driver.d.ts +15 -4
- package/dist/src/sync/driver.js +18 -43
- package/dist/src/sync/external-source.js +7 -1
- package/dist/src/sync/freshness.d.ts +73 -15
- package/dist/src/sync/freshness.js +128 -97
- package/dist/src/sync/fs-util.d.ts +19 -0
- package/dist/src/sync/fs-util.js +24 -1
- package/dist/src/sync/observe.d.ts +53 -5
- package/dist/src/sync/observe.js +162 -21
- package/dist/src/sync/reconciling-ingest.d.ts +42 -2
- package/dist/src/sync/reconciling-ingest.js +87 -13
- package/dist/src/sync/recovery.d.ts +5 -1
- package/dist/src/sync/recovery.js +8 -8
- package/dist/src/sync/workspace.d.ts +11 -3
- package/dist/src/sync/workspace.js +39 -30
- package/dist/src/sync/writer-lock.js +16 -26
- package/package.json +3 -3
package/dist/src/core/ingest.js
CHANGED
|
@@ -3,24 +3,36 @@ import { parseTree } from "./parse/tree.js";
|
|
|
3
3
|
import { render } from "./parse/render.js";
|
|
4
4
|
import { reconstructContent } from "./read/document.js";
|
|
5
5
|
import { assignIds, writeBlockTree, putBlob, newCommit, writeRevision } from "./store/writers.js";
|
|
6
|
-
import { sha256,
|
|
6
|
+
import { sha256, childQuoteDepth, visibleText } from "./hash.js";
|
|
7
7
|
import { mintId } from "./ids.js";
|
|
8
8
|
import { keyBetween } from "./order-key.js";
|
|
9
9
|
import { ftsDeleteDoc, ftsIndexDoc } from "./store/fts.js";
|
|
10
10
|
import { rebuildSections } from "./store/sections.js";
|
|
11
|
-
import { writeDocNodes, projectSectionNodes } from "./store/nodes.js";
|
|
11
|
+
import { writeDocNodes, projectSectionNodes, toByteSpans } from "./store/nodes.js";
|
|
12
12
|
import { flattenFrontmatter, flattenComputed, writeDocProperties } from "./store/properties.js";
|
|
13
13
|
import { maintainEdges, adoptPhantoms } from "./store/edges.js";
|
|
14
14
|
import { parse as parseYaml } from "yaml";
|
|
15
15
|
import { adapterForPath } from "../format/index.js";
|
|
16
|
+
/** The parse step of ingest, exported so a batch caller can parse + reconcile
|
|
17
|
+
* every document of a checkpoint BEFORE committing any (the cross-document
|
|
18
|
+
* phase needs all per-doc results first) and hand the tree back via
|
|
19
|
+
* `opts.parsed`. Frontmatter is split off: `rest` is what the IdResolver sees. */
|
|
20
|
+
export function parseForIngest(path, content) {
|
|
21
|
+
const adapter = adapterForPath(path);
|
|
22
|
+
const tree = adapter ? adapter.parse(content) : parseTree(content);
|
|
23
|
+
return { tree, ...extractFrontmatter(tree.children) };
|
|
24
|
+
}
|
|
16
25
|
function extractFrontmatter(blocks) {
|
|
17
26
|
if (blocks.length > 0 && blocks[0].type === "frontmatter") {
|
|
18
27
|
return { fmBlock: blocks[0], rest: blocks.slice(1) };
|
|
19
28
|
}
|
|
20
29
|
return { fmBlock: null, rest: blocks };
|
|
21
30
|
}
|
|
22
|
-
// Parse a frontmatter block's raw (incl. --- fences) into a JSON-safe object
|
|
23
|
-
//
|
|
31
|
+
// Parse a frontmatter block's raw (incl. --- fences) into a JSON-safe object:
|
|
32
|
+
// the YAML text is the raw minus its first line (the opening `---`) and its last
|
|
33
|
+
// line (the closing `---`, with any trailing whitespace); `\n` or `\r\n` endings
|
|
34
|
+
// (spec/properties §3.1). Malformed YAML, or a document that is not a mapping,
|
|
35
|
+
// yields {} — frontmatter is queryable metadata, not load-bearing.
|
|
24
36
|
function parseFrontmatter(fmBlock) {
|
|
25
37
|
if (!fmBlock)
|
|
26
38
|
return {};
|
|
@@ -35,12 +47,14 @@ function parseFrontmatter(fmBlock) {
|
|
|
35
47
|
return {};
|
|
36
48
|
}
|
|
37
49
|
}
|
|
38
|
-
|
|
50
|
+
// `quoteDepth` is the number of blockquote ancestors — the §4.1 text rule needs
|
|
51
|
+
// it (spec/format), and containers derive text from their children.
|
|
52
|
+
function flatten(blocks, parent, depth, ancestorPath, out, quoteDepth = 0) {
|
|
39
53
|
let prevKey = null;
|
|
40
54
|
blocks.forEach((b, ordinal) => {
|
|
41
55
|
const orderKey = keyBetween(prevKey, null);
|
|
42
56
|
prevKey = orderKey;
|
|
43
|
-
const visible =
|
|
57
|
+
const visible = visibleText(b, quoteDepth);
|
|
44
58
|
out.push({
|
|
45
59
|
blockId: b.blockId,
|
|
46
60
|
parentBlock: parent,
|
|
@@ -56,7 +70,7 @@ function flatten(blocks, parent, depth, ancestorPath, out) {
|
|
|
56
70
|
triviaHash: b.trivia.length > 0 ? sha256(b.trivia) : null,
|
|
57
71
|
});
|
|
58
72
|
if (b.children.length > 0) {
|
|
59
|
-
flatten(b.children, b.blockId, depth + 1, `${ancestorPath}${b.blockId}/`, out);
|
|
73
|
+
flatten(b.children, b.blockId, depth + 1, `${ancestorPath}${b.blockId}/`, out, childQuoteDepth(b.type, quoteDepth));
|
|
60
74
|
}
|
|
61
75
|
});
|
|
62
76
|
}
|
|
@@ -67,7 +81,7 @@ export function ingestFile(store, repoId, path, content, opts = {}) {
|
|
|
67
81
|
const adapter = adapterForPath(path);
|
|
68
82
|
const format = opts.format ?? adapter?.format ?? "markdown";
|
|
69
83
|
return store.write((db) => {
|
|
70
|
-
const tree = adapter ? adapter.parse(content) : parseTree(content);
|
|
84
|
+
const tree = opts.parsed ?? (adapter ? adapter.parse(content) : parseTree(content));
|
|
71
85
|
const { fmBlock, rest } = extractFrontmatter(tree.children);
|
|
72
86
|
// Frontmatter blob (markdown-specific) preserved for revision history.
|
|
73
87
|
const fmBlobHex = fmBlock ? putBlob(db, fmBlock.raw) : null;
|
|
@@ -76,24 +90,38 @@ export function ingestFile(store, repoId, path, content, opts = {}) {
|
|
|
76
90
|
// reconstructs byte-for-byte instead of assuming a canonical blank line.
|
|
77
91
|
// NULL when there is no frontmatter.
|
|
78
92
|
const fmTrivia = fmBlock ? fmBlock.trivia : null;
|
|
79
|
-
// Document property bag:
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
// Document property bag: whole-document metadata for the formats whose
|
|
94
|
+
// adapter extracts one (YAML, JSON); for Markdown — whose adapter has no
|
|
95
|
+
// `extractMetadata` — the YAML of the `frontmatter` block and nothing else
|
|
96
|
+
// (spec/properties §3.1: a document whose first block is not `frontmatter`
|
|
97
|
+
// has no frontmatter rows, whatever else its bytes resemble). Flattened into
|
|
98
|
+
// the properties table below (source=frontmatter); also drives frontmatter
|
|
99
|
+
// edge extraction.
|
|
100
|
+
const metadata = adapter?.extractMetadata
|
|
101
|
+
? (adapter.extractMetadata(content) ?? {})
|
|
84
102
|
: parseFrontmatter(fmBlock);
|
|
85
103
|
// Upsert the document row.
|
|
86
|
-
let doc = db.prepare("SELECT doc_id FROM docs WHERE repo_id = ? AND path = ?").get(repoId, path);
|
|
104
|
+
let doc = db.prepare("SELECT doc_id, deleted_commit FROM docs WHERE repo_id = ? AND path = ?").get(repoId, path);
|
|
87
105
|
const docId = doc?.doc_id ?? mintId("d");
|
|
88
106
|
const isNew = !doc;
|
|
89
107
|
if (!doc) {
|
|
90
108
|
db.prepare("INSERT INTO docs (doc_id, repo_id, path, format, leading_trivia, frontmatter_trivia) VALUES (?, ?, ?, ?, ?, ?)").run(docId, repoId, path, format, tree.leadingTrivia, fmTrivia);
|
|
91
|
-
doc = { doc_id: docId };
|
|
109
|
+
doc = { doc_id: docId, deleted_commit: null };
|
|
92
110
|
// Adopt phantom edges that pointed at this path so backlinks re-point.
|
|
93
111
|
adoptPhantoms(db, path, docId);
|
|
94
112
|
}
|
|
95
113
|
else {
|
|
96
|
-
|
|
114
|
+
// A tombstoned row still owns the path's identity (spec/store §5.6
|
|
115
|
+
// "Re-creation"): bytes observed again at the path revive it — the row is
|
|
116
|
+
// live again, so reads that filter `deleted_commit IS NULL` see it and the
|
|
117
|
+
// next identical observation echo-gates instead of re-ingesting.
|
|
118
|
+
const revived = doc.deleted_commit !== null;
|
|
119
|
+
db.prepare("UPDATE docs SET format = ?, leading_trivia = ?, frontmatter_trivia = ?, deleted_commit = NULL WHERE doc_id = ?").run(format, tree.leadingTrivia, fmTrivia, docId);
|
|
120
|
+
// spec/graph §3.5: adoption happens whenever a row BECOMES live, so a
|
|
121
|
+
// revived tombstone also collects the phantom edges minted at its path
|
|
122
|
+
// while it was gone (§8 "Fixed — a revived document adopted no phantoms").
|
|
123
|
+
if (revived)
|
|
124
|
+
adoptPhantoms(db, path, docId);
|
|
97
125
|
}
|
|
98
126
|
// Assign block ids via the resolver when supplied (it reconciles against
|
|
99
127
|
// the prior revision — for a new doc that's an empty old tree, so all blocks
|
|
@@ -103,7 +131,7 @@ export function ingestFile(store, repoId, path, content, opts = {}) {
|
|
|
103
131
|
: { assigned: assignIds(rest), dispositions: [], deleted: [] };
|
|
104
132
|
const assigned = resolved.assigned;
|
|
105
133
|
const rootTreeHex = writeBlockTree(db, assigned);
|
|
106
|
-
const commit = newCommit(db, { repoId, ts, origin });
|
|
134
|
+
const commit = newCommit(db, { repoId, ts, origin, actor: opts.actor ?? null, reason: opts.reason ?? null });
|
|
107
135
|
const renderedHash = sha256(content);
|
|
108
136
|
const rev = writeRevision(db, {
|
|
109
137
|
docId,
|
|
@@ -122,6 +150,14 @@ export function ingestFile(store, repoId, path, content, opts = {}) {
|
|
|
122
150
|
for (const id of resolved.deleted)
|
|
123
151
|
pool.run(commit.commitId, expires, id, docId);
|
|
124
152
|
}
|
|
153
|
+
// Ids arriving from ANOTHER document — a cross-document move whose source
|
|
154
|
+
// commits later in this checkpoint (or was tombstoned in it), or a
|
|
155
|
+
// resurrection out of a tombstoned doc whose rows still sit in `blocks` —
|
|
156
|
+
// must not collide on the block_id primary key. Evict the foreign row (and
|
|
157
|
+
// its FTS entry when live) and drop any pool row: the id is live here now.
|
|
158
|
+
const incoming = [...(resolved.crossDocIds ?? []), ...(resolved.consumedPool ?? [])];
|
|
159
|
+
if (incoming.length > 0)
|
|
160
|
+
evictForeignBlockRows(db, docId, incoming);
|
|
125
161
|
// Refresh current-state blocks (clear + repopulate with carried/minted ids).
|
|
126
162
|
// FTS is external-content: delete old index rows before dropping blocks.
|
|
127
163
|
ftsDeleteDoc(db, docId);
|
|
@@ -155,7 +191,12 @@ export function ingestFile(store, repoId, path, content, opts = {}) {
|
|
|
155
191
|
...n,
|
|
156
192
|
blockId: n.blockId && idSet.has(n.blockId) ? n.blockId : "",
|
|
157
193
|
}));
|
|
158
|
-
|
|
194
|
+
// Adapters record spans as string indices; the store keeps byte offsets
|
|
195
|
+
// into the block's UTF-8 raw (spec/graph §2.3), so convert here — the one
|
|
196
|
+
// place that has both the nodes and the raws they index.
|
|
197
|
+
const rawById = collectRaws(assigned);
|
|
198
|
+
const byteSpanned = toByteSpans(withIds, (id) => rawById.get(id));
|
|
199
|
+
writeDocNodes(db, repoId, docId, [...byteSpanned, ...sectionNodes]);
|
|
159
200
|
// Inline properties (key:: value) → property rows (source=inline). The
|
|
160
201
|
// authored shape within the inline source drives `card`: a key that occurs
|
|
161
202
|
// exactly once in the document is `scalar` (so `element == "fire"` can
|
|
@@ -246,6 +287,27 @@ export function ingestFile(store, repoId, path, content, opts = {}) {
|
|
|
246
287
|
};
|
|
247
288
|
});
|
|
248
289
|
}
|
|
290
|
+
// Remove another document's `blocks` row for each id in `ids` (a cross-doc move
|
|
291
|
+
// or a resurrection re-homes the id into `docId`). A live foreign row still has
|
|
292
|
+
// an FTS entry (external-content index: delete with the original text); a
|
|
293
|
+
// tombstoned one does not. The source document, if it commits later in the same
|
|
294
|
+
// checkpoint, rewrites its rows wholesale and never lists a carried id as
|
|
295
|
+
// deleted, so nothing there depends on the evicted row.
|
|
296
|
+
function evictForeignBlockRows(db, docId, ids) {
|
|
297
|
+
const sel = db.prepare("SELECT rowid, text, deleted_commit FROM blocks WHERE block_id = ? AND doc_id != ?");
|
|
298
|
+
const ftsDel = db.prepare("INSERT INTO blocks_fts(blocks_fts, rowid, text) VALUES('delete', ?, ?)");
|
|
299
|
+
const del = db.prepare("DELETE FROM blocks WHERE block_id = ? AND doc_id != ?");
|
|
300
|
+
const unpool = db.prepare("DELETE FROM resurrection_pool WHERE block_id = ?");
|
|
301
|
+
for (const id of ids) {
|
|
302
|
+
const row = sel.get(id, docId);
|
|
303
|
+
if (row) {
|
|
304
|
+
if (row.deleted_commit === null)
|
|
305
|
+
ftsDel.run(row.rowid, row.text);
|
|
306
|
+
del.run(id, docId);
|
|
307
|
+
}
|
|
308
|
+
unpool.run(id);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
249
311
|
// Zip assigned block ids onto the parsed RawBlocks for node projection. The
|
|
250
312
|
// parsed `rest` and `assigned` trees are the same blocks in the same order and
|
|
251
313
|
// nesting (assigned is a 1:1 id-carrying projection of rest), so we walk them in
|
|
@@ -272,6 +334,18 @@ function typedInlineValue(raw) {
|
|
|
272
334
|
return { valText: null, valNum: Number(v), valBool: null, valJson: null, type: "number" };
|
|
273
335
|
return { valText: v, valNum: null, valBool: null, valJson: null, type: "string" };
|
|
274
336
|
}
|
|
337
|
+
function collectRaws(blocks) {
|
|
338
|
+
const raws = new Map();
|
|
339
|
+
const walk = (list) => {
|
|
340
|
+
for (const b of list) {
|
|
341
|
+
raws.set(b.blockId, b.raw);
|
|
342
|
+
if (b.children.length > 0)
|
|
343
|
+
walk(b.children);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
walk(blocks);
|
|
347
|
+
return raws;
|
|
348
|
+
}
|
|
275
349
|
function collectBlockIds(blocks) {
|
|
276
350
|
const ids = new Set();
|
|
277
351
|
const walk = (list) => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { RawBlock } from "./types.js";
|
|
2
|
-
/** Parse Markdown source into a flat list of top-level blocks with
|
|
2
|
+
/** Parse Markdown source into a flat list of top-level blocks with spans into `source`. */
|
|
3
3
|
export declare function parseBlocks(source: string): RawBlock[];
|
|
@@ -3,7 +3,7 @@ import { gfm } from "micromark-extension-gfm";
|
|
|
3
3
|
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
4
4
|
import { frontmatter } from "micromark-extension-frontmatter";
|
|
5
5
|
import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
|
|
6
|
-
import {
|
|
6
|
+
import { childQuoteDepth, visibleText } from "../hash.js";
|
|
7
7
|
// Nodes whose children nest as blocks (03 §1: parser nesting only).
|
|
8
8
|
// Everything else at block level maps to a leaf block; unknown → opaque.
|
|
9
9
|
function mapType(node) {
|
|
@@ -66,16 +66,30 @@ function childrenOf(node) {
|
|
|
66
66
|
const kids = node.children;
|
|
67
67
|
return Array.isArray(kids) ? kids : [];
|
|
68
68
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
function isLineEnding(code) {
|
|
70
|
+
return code === 0x0a /* \n */ || code === 0x0d /* \r */;
|
|
71
|
+
}
|
|
72
|
+
// `quoteDepth` = number of blockquote ancestors, which the §4.1 text rule needs
|
|
73
|
+
// because a nested block's raw keeps the `> ` prefixes of its continuation lines.
|
|
74
|
+
function buildBlock(node, source, shift, quoteDepth) {
|
|
75
|
+
const rawStart = node.position?.start.offset;
|
|
76
|
+
const rawEnd = node.position?.end.offset;
|
|
77
|
+
if (rawStart === undefined || rawEnd === undefined) {
|
|
73
78
|
throw new Error(`node ${node.type} missing position offsets`);
|
|
74
79
|
}
|
|
80
|
+
const start = rawStart + shift;
|
|
81
|
+
let end = rawEnd + shift;
|
|
82
|
+
// Spans exclude the terminating line ending (spec/format §1 inv. 4). micromark
|
|
83
|
+
// already ends most blocks before it, but a construct that runs to EOF without
|
|
84
|
+
// a closer (an unclosed fence, an unclosed HTML comment, and the list items
|
|
85
|
+
// holding one) is handed every trailing line ending, blank lines included.
|
|
86
|
+
// Those bytes belong to the trivia that follows the block.
|
|
87
|
+
while (end > start && isLineEnding(source.charCodeAt(end - 1)))
|
|
88
|
+
end--;
|
|
75
89
|
const raw = source.slice(start, end);
|
|
76
90
|
const mapped = mapType(node);
|
|
77
91
|
const type = mapped ?? "opaque";
|
|
78
|
-
let children = mapped === null ? [] : childrenOf(node).map((c) => buildBlock(c, source));
|
|
92
|
+
let children = mapped === null ? [] : childrenOf(node).map((c) => buildBlock(c, source, shift, childQuoteDepth(type, quoteDepth)));
|
|
79
93
|
// Fold a tight list item's lone paragraph: the item block carries the text
|
|
80
94
|
// directly (frozen outline format, 06 §6). Loose/multi-block items keep them.
|
|
81
95
|
if ((type === "list_item" || type === "task") && children.length === 1 && children[0].type === "paragraph") {
|
|
@@ -85,7 +99,7 @@ function buildBlock(node, source) {
|
|
|
85
99
|
type,
|
|
86
100
|
span: { start, end },
|
|
87
101
|
raw,
|
|
88
|
-
text:
|
|
102
|
+
text: visibleText({ type, raw, children }, quoteDepth),
|
|
89
103
|
attrs: mapped === null ? {} : attrsFor(node),
|
|
90
104
|
children,
|
|
91
105
|
trivia: "",
|
|
@@ -93,12 +107,20 @@ function buildBlock(node, source) {
|
|
|
93
107
|
outLinks: [],
|
|
94
108
|
};
|
|
95
109
|
}
|
|
96
|
-
|
|
110
|
+
// micromark's preprocessor (micromark/lib/preprocess.js) drops exactly one
|
|
111
|
+
// leading U+FEFF before tokenizing and never emits it as a chunk, so every
|
|
112
|
+
// position it reports is an index into the stripped string. Spans must index
|
|
113
|
+
// the true source (spec/format §1 inv. 6: the BOM is leading trivia), so all
|
|
114
|
+
// offsets move by one code unit when — and only when — the source starts with
|
|
115
|
+
// a BOM. A second BOM, or one anywhere else, is ordinary content and unshifted.
|
|
116
|
+
const BOM = 0xfeff;
|
|
117
|
+
/** Parse Markdown source into a flat list of top-level blocks with spans into `source`. */
|
|
97
118
|
export function parseBlocks(source) {
|
|
98
119
|
const tree = fromMarkdown(source, {
|
|
99
120
|
extensions: [gfm(), frontmatter(["yaml"])],
|
|
100
121
|
mdastExtensions: [gfmFromMarkdown(), frontmatterFromMarkdown(["yaml"])],
|
|
101
122
|
});
|
|
102
|
-
|
|
123
|
+
const shift = source.charCodeAt(0) === BOM ? 1 : 0;
|
|
124
|
+
return tree.children.map((c) => buildBlock(c, source, shift, 0));
|
|
103
125
|
}
|
|
104
126
|
//# sourceMappingURL=parse.js.map
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { Database } from "better-sqlite3";
|
|
2
2
|
import type { ProjectedNode } from "../../format/adapter.js";
|
|
3
3
|
export declare function projectSectionNodes(db: Database, docId: string): ProjectedNode[];
|
|
4
|
+
/**
|
|
5
|
+
* Convert adapter-projected spans (JavaScript string indices — UTF-16 code
|
|
6
|
+
* units, spec/graph §2.3) to byte offsets into the block's UTF-8 `raw`, which is
|
|
7
|
+
* what the `nodes` table stores (spec/graph §8 "Fixed"). `rawOf` returns the raw
|
|
8
|
+
* of a block by id; a node whose block raw is unknown keeps no span rather than a
|
|
9
|
+
* wrong one. Nodes without a span (sections) pass through untouched.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toByteSpans(projected: ProjectedNode[], rawOf: (blockId: string) => string | undefined): ProjectedNode[];
|
|
4
12
|
export declare function deleteDocNodes(db: Database, docId: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Replace a document's `nodes` rows (and their FTS rows) with `projected`, in
|
|
15
|
+
* order; `node_id` is the spec/graph §2.3 hash over (doc, block, kind, ordinal).
|
|
16
|
+
* Spans are stored as given — callers convert to bytes first (`toByteSpans`).
|
|
17
|
+
*/
|
|
5
18
|
export declare function writeDocNodes(db: Database, repoId: string, docId: string, projected: ProjectedNode[]): number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hashHex } from "../hash.js";
|
|
2
|
+
import { codeUnitSpanToBytes } from "../utf8.js";
|
|
2
3
|
function mintNodeId(docId, blockId, kind, ordinal) {
|
|
3
4
|
return "n_" + hashHex(`${docId}|${blockId}|${kind}|${ordinal}`).slice(0, 12);
|
|
4
5
|
}
|
|
@@ -27,6 +28,24 @@ export function projectSectionNodes(db, docId) {
|
|
|
27
28
|
attrs: { level: r.level, first_ordinal: r.first_ordinal, last_ordinal: r.last_ordinal },
|
|
28
29
|
}));
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert adapter-projected spans (JavaScript string indices — UTF-16 code
|
|
33
|
+
* units, spec/graph §2.3) to byte offsets into the block's UTF-8 `raw`, which is
|
|
34
|
+
* what the `nodes` table stores (spec/graph §8 "Fixed"). `rawOf` returns the raw
|
|
35
|
+
* of a block by id; a node whose block raw is unknown keeps no span rather than a
|
|
36
|
+
* wrong one. Nodes without a span (sections) pass through untouched.
|
|
37
|
+
*/
|
|
38
|
+
export function toByteSpans(projected, rawOf) {
|
|
39
|
+
return projected.map((p) => {
|
|
40
|
+
if (p.spanStart === undefined || p.spanEnd === undefined)
|
|
41
|
+
return p;
|
|
42
|
+
const raw = rawOf(p.blockId);
|
|
43
|
+
if (raw === undefined)
|
|
44
|
+
return { ...p, spanStart: undefined, spanEnd: undefined };
|
|
45
|
+
const { start, end } = codeUnitSpanToBytes(raw, p.spanStart, p.spanEnd);
|
|
46
|
+
return { ...p, spanStart: start, spanEnd: end };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
30
49
|
export function deleteDocNodes(db, docId) {
|
|
31
50
|
const rows = db.prepare("SELECT rowid, name, value FROM nodes WHERE doc_id = ?").all(docId);
|
|
32
51
|
for (const r of rows) {
|
|
@@ -34,6 +53,11 @@ export function deleteDocNodes(db, docId) {
|
|
|
34
53
|
}
|
|
35
54
|
db.prepare("DELETE FROM nodes WHERE doc_id = ?").run(docId);
|
|
36
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Replace a document's `nodes` rows (and their FTS rows) with `projected`, in
|
|
58
|
+
* order; `node_id` is the spec/graph §2.3 hash over (doc, block, kind, ordinal).
|
|
59
|
+
* Spans are stored as given — callers convert to bytes first (`toByteSpans`).
|
|
60
|
+
*/
|
|
37
61
|
export function writeDocNodes(db, repoId, docId, projected) {
|
|
38
62
|
deleteDocNodes(db, docId);
|
|
39
63
|
const insert = db.prepare(`INSERT INTO nodes (node_id, repo_id, doc_id, block_id, kind, name, value, span_start, span_end, attrs)
|
|
@@ -9,6 +9,12 @@ export interface TreeInputBlock {
|
|
|
9
9
|
trivia: string;
|
|
10
10
|
attrs: Record<string, unknown>;
|
|
11
11
|
children: TreeInputBlock[];
|
|
12
|
+
/** The parsed block's source span, when the tree came from a parse — what
|
|
13
|
+
* `ownText` (spec/properties §3.2) uses to blank a container's children. */
|
|
14
|
+
span?: {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
};
|
|
12
18
|
}
|
|
13
19
|
/** Assign fresh minted ids to a parsed block tree (Stage-1 re-mint path). */
|
|
14
20
|
export declare function assignIds(blocks: RawBlock[]): TreeInputBlock[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte offset of every code-unit index of `s` (length + 1 entries, so the
|
|
3
|
+
* end-of-string index resolves too). One pass over the string; a surrogate
|
|
4
|
+
* pair contributes 4 bytes at the index after the pair, and the index between
|
|
5
|
+
* its two halves maps to the pair's start (no span ever lands there).
|
|
6
|
+
*/
|
|
7
|
+
export declare function byteOffsetTable(s: string): Uint32Array;
|
|
8
|
+
/** The UTF-8 byte offset of code-unit index `index` of `s` (clamped to the string). */
|
|
9
|
+
export declare function byteOffsetOf(s: string, index: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Convert a `[start, end)` code-unit span of `s` to a byte span. Both ends are
|
|
12
|
+
* clamped to the string; `start <= end` is preserved.
|
|
13
|
+
*/
|
|
14
|
+
export declare function codeUnitSpanToBytes(s: string, start: number, end: number): {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The code-unit index whose byte offset is `byte` in `s`. A byte offset that
|
|
20
|
+
* falls inside a multi-byte sequence (never produced by this module, but a
|
|
21
|
+
* foreign writer could) rounds down to the start of that character; an offset
|
|
22
|
+
* past the end clamps to `s.length`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function codeUnitIndexOf(s: string, byte: number): number;
|
|
25
|
+
/** Convert a `[start, end)` byte span of `s` back to a code-unit span (inverse of `codeUnitSpanToBytes`). */
|
|
26
|
+
export declare function byteSpanToCodeUnits(s: string, start: number, end: number): {
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// UTF-16 code-unit index ⇄ UTF-8 byte offset (spec/format §1 inv. 5, spec/graph
|
|
2
|
+
// §2.3). JavaScript strings index code units; every language-neutral offset the
|
|
3
|
+
// store persists is a byte offset into the UTF-8 encoding of the same text, so
|
|
4
|
+
// the reference converts at the store boundary: node spans are converted to
|
|
5
|
+
// bytes before `nodes` rows are written, and back to code units before an
|
|
6
|
+
// editor slices a block's raw. Both directions are exact for well-formed text;
|
|
7
|
+
// a lone surrogate counts as the 3 bytes `Buffer.from(s, "utf8")` emits for
|
|
8
|
+
// U+FFFD, so the table always agrees with the bytes actually stored.
|
|
9
|
+
/**
|
|
10
|
+
* Byte offset of every code-unit index of `s` (length + 1 entries, so the
|
|
11
|
+
* end-of-string index resolves too). One pass over the string; a surrogate
|
|
12
|
+
* pair contributes 4 bytes at the index after the pair, and the index between
|
|
13
|
+
* its two halves maps to the pair's start (no span ever lands there).
|
|
14
|
+
*/
|
|
15
|
+
export function byteOffsetTable(s) {
|
|
16
|
+
const table = new Uint32Array(s.length + 1);
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < s.length) {
|
|
20
|
+
table[i] = bytes;
|
|
21
|
+
const c = s.charCodeAt(i);
|
|
22
|
+
if (c < 0x80) {
|
|
23
|
+
bytes += 1;
|
|
24
|
+
i += 1;
|
|
25
|
+
}
|
|
26
|
+
else if (c < 0x800) {
|
|
27
|
+
bytes += 2;
|
|
28
|
+
i += 1;
|
|
29
|
+
}
|
|
30
|
+
else if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) {
|
|
31
|
+
const d = s.charCodeAt(i + 1);
|
|
32
|
+
if (d >= 0xdc00 && d <= 0xdfff) {
|
|
33
|
+
table[i + 1] = bytes;
|
|
34
|
+
bytes += 4;
|
|
35
|
+
i += 2;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
bytes += 3; // lone high surrogate → U+FFFD
|
|
39
|
+
i += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
bytes += 3; // BMP ≥ U+0800, or a lone surrogate → U+FFFD
|
|
44
|
+
i += 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
table[s.length] = bytes;
|
|
48
|
+
return table;
|
|
49
|
+
}
|
|
50
|
+
/** The UTF-8 byte offset of code-unit index `index` of `s` (clamped to the string). */
|
|
51
|
+
export function byteOffsetOf(s, index) {
|
|
52
|
+
return byteOffsetTable(s)[Math.max(0, Math.min(index, s.length))];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Convert a `[start, end)` code-unit span of `s` to a byte span. Both ends are
|
|
56
|
+
* clamped to the string; `start <= end` is preserved.
|
|
57
|
+
*/
|
|
58
|
+
export function codeUnitSpanToBytes(s, start, end) {
|
|
59
|
+
const table = byteOffsetTable(s);
|
|
60
|
+
const clamp = (i) => Math.max(0, Math.min(i, s.length));
|
|
61
|
+
return { start: table[clamp(start)], end: table[clamp(Math.max(start, end))] };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The code-unit index whose byte offset is `byte` in `s`. A byte offset that
|
|
65
|
+
* falls inside a multi-byte sequence (never produced by this module, but a
|
|
66
|
+
* foreign writer could) rounds down to the start of that character; an offset
|
|
67
|
+
* past the end clamps to `s.length`.
|
|
68
|
+
*/
|
|
69
|
+
export function codeUnitIndexOf(s, byte) {
|
|
70
|
+
const table = byteOffsetTable(s);
|
|
71
|
+
// `table` is non-decreasing; find the greatest index whose offset ≤ byte.
|
|
72
|
+
let lo = 0;
|
|
73
|
+
let hi = s.length;
|
|
74
|
+
while (lo < hi) {
|
|
75
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
76
|
+
if (table[mid] <= byte)
|
|
77
|
+
lo = mid;
|
|
78
|
+
else
|
|
79
|
+
hi = mid - 1;
|
|
80
|
+
}
|
|
81
|
+
// The index between the halves of a surrogate pair shares the pair's start
|
|
82
|
+
// offset; step back to the high surrogate so a slice never splits the pair.
|
|
83
|
+
if (lo > 0 && lo < s.length) {
|
|
84
|
+
const c = s.charCodeAt(lo);
|
|
85
|
+
if (c >= 0xdc00 && c <= 0xdfff && table[lo] === table[lo - 1])
|
|
86
|
+
lo -= 1;
|
|
87
|
+
}
|
|
88
|
+
return lo;
|
|
89
|
+
}
|
|
90
|
+
/** Convert a `[start, end)` byte span of `s` back to a code-unit span (inverse of `codeUnitSpanToBytes`). */
|
|
91
|
+
export function byteSpanToCodeUnits(s, start, end) {
|
|
92
|
+
const a = codeUnitIndexOf(s, start);
|
|
93
|
+
const b = codeUnitIndexOf(s, Math.max(start, end));
|
|
94
|
+
return { start: a, end: Math.max(a, b) };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=utf8.js.map
|
|
@@ -14,6 +14,12 @@ export interface ProjectedNode {
|
|
|
14
14
|
name?: string | undefined;
|
|
15
15
|
value?: string | undefined;
|
|
16
16
|
blockId: string;
|
|
17
|
+
/**
|
|
18
|
+
* `[spanStart, spanEnd)` of the feature within the block's raw, as JavaScript
|
|
19
|
+
* string indices (UTF-16 code units). Ingest converts to UTF-8 byte offsets
|
|
20
|
+
* before the `nodes` row is written (spec/graph §2.3), and `node_set`
|
|
21
|
+
* converts back before an editor slices the raw.
|
|
22
|
+
*/
|
|
17
23
|
spanStart?: number | undefined;
|
|
18
24
|
spanEnd?: number | undefined;
|
|
19
25
|
attrs?: Record<string, unknown> | undefined;
|
|
@@ -27,7 +33,7 @@ export interface EmbeddingChunk {
|
|
|
27
33
|
export interface NodeEditContext {
|
|
28
34
|
/** The block's current raw bytes (what the edit rewrites). */
|
|
29
35
|
blockRaw: string;
|
|
30
|
-
/** The node's
|
|
36
|
+
/** The node's span within blockRaw as string indices — the stored byte span converted back (null when unrecorded). */
|
|
31
37
|
span: {
|
|
32
38
|
start: number;
|
|
33
39
|
end: number;
|