@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,232 @@
|
|
|
1
|
+
import { rawHashHex } from "./tree.js";
|
|
2
|
+
function collectMutIds(list, out = new Set()) {
|
|
3
|
+
for (const b of list) {
|
|
4
|
+
out.add(b.id);
|
|
5
|
+
collectMutIds(b.children, out);
|
|
6
|
+
}
|
|
7
|
+
return out;
|
|
8
|
+
}
|
|
9
|
+
function collectTargetIds(list, out = new Set()) {
|
|
10
|
+
for (const b of list) {
|
|
11
|
+
out.add(b.id);
|
|
12
|
+
collectTargetIds(b.children, out);
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
// Build the proposed tree annotated with assigned ids + carry flags, keying
|
|
17
|
+
// exactly as flatten()/assignFromMut() do (parentKey + '/' + index).
|
|
18
|
+
function buildTarget(blocks, assignment, oldIds, parentKey) {
|
|
19
|
+
return blocks.map((b, index) => {
|
|
20
|
+
const key = `${parentKey ?? ""}/${index}`;
|
|
21
|
+
const id = assignment.get(key) ?? key; // key fallback should not happen
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
key,
|
|
25
|
+
type: b.type,
|
|
26
|
+
raw: b.raw,
|
|
27
|
+
trivia: b.trivia,
|
|
28
|
+
children: buildTarget(b.children, assignment, oldIds, key),
|
|
29
|
+
carried: oldIds.has(id),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// Longest common subsequence of two id lists (indices into a). Standard DP;
|
|
34
|
+
// used to leave already-ordered survivors in place and move only the rest.
|
|
35
|
+
function lcs(a, b) {
|
|
36
|
+
const n = a.length, m = b.length;
|
|
37
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
38
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
39
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
40
|
+
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const keep = new Set();
|
|
44
|
+
let i = 0, j = 0;
|
|
45
|
+
while (i < n && j < m) {
|
|
46
|
+
if (a[i] === b[j]) {
|
|
47
|
+
keep.add(a[i]);
|
|
48
|
+
i++;
|
|
49
|
+
j++;
|
|
50
|
+
}
|
|
51
|
+
else if (dp[i + 1][j] >= dp[i][j + 1])
|
|
52
|
+
i++;
|
|
53
|
+
else
|
|
54
|
+
j++;
|
|
55
|
+
}
|
|
56
|
+
return keep;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Lower a reconciled whole-document update to a kernel opset (top-level
|
|
60
|
+
* granularity). Preserves identity for every top-level block carried unchanged
|
|
61
|
+
* and correctly realizes add / remove / reorder / content-edit at the top
|
|
62
|
+
* level. A carried container whose content changed is updated as a whole (its
|
|
63
|
+
* children re-minted); recursion to preserve nested identity is a later
|
|
64
|
+
* refinement (verification keeps this honest either way).
|
|
65
|
+
*/
|
|
66
|
+
export function lowerTopLevel(oldDoc, rest, assignment, dispositions) {
|
|
67
|
+
const docId = oldDoc.docId;
|
|
68
|
+
const oldIds = collectMutIds(oldDoc.children);
|
|
69
|
+
const target = buildTarget(rest, assignment, oldIds, null);
|
|
70
|
+
const targetIdSet = collectTargetIds(target);
|
|
71
|
+
const dispBy = new Map(dispositions.map((d) => [d.blockId, d]));
|
|
72
|
+
const oldById = new Map();
|
|
73
|
+
{
|
|
74
|
+
const walk = (l) => { for (const b of l) {
|
|
75
|
+
oldById.set(b.id, b);
|
|
76
|
+
walk(b.children);
|
|
77
|
+
} };
|
|
78
|
+
walk(oldDoc.children);
|
|
79
|
+
}
|
|
80
|
+
const oldTopIds = oldDoc.children.map((b) => b.id);
|
|
81
|
+
const oldTopSet = new Set(oldTopIds);
|
|
82
|
+
const ops = [];
|
|
83
|
+
let preserved = 0;
|
|
84
|
+
let ambiguous = 0;
|
|
85
|
+
for (const d of dispositions)
|
|
86
|
+
if (d.detail && Array.isArray(d.detail.nearMisses))
|
|
87
|
+
ambiguous++;
|
|
88
|
+
const meta = (id) => {
|
|
89
|
+
const d = dispBy.get(id);
|
|
90
|
+
return { confidence: d?.confidence ?? null, reason: d?.reason ?? null, ...(d?.detail ? { detail: d.detail } : {}) };
|
|
91
|
+
};
|
|
92
|
+
// 1. Removes: current top-level blocks gone from the proposed TOP LEVEL. An id
|
|
93
|
+
// the reconciler carried into a nested position (a paragraph that became a
|
|
94
|
+
// blockquote's child) is still removed here: the container it now lives in is
|
|
95
|
+
// inserted fresh below, and an insert cannot carry ids, so leaving the old
|
|
96
|
+
// top-level block in place would duplicate its bytes.
|
|
97
|
+
const targetTopIds = new Set(target.map((t) => t.id));
|
|
98
|
+
for (const ob of oldDoc.children) {
|
|
99
|
+
if (!targetTopIds.has(ob.id)) {
|
|
100
|
+
ops.push({
|
|
101
|
+
op: { op: "remove", blocks: [ob.id], expect: { [ob.id]: { content_hash: rawHashHex(ob.raw) } } },
|
|
102
|
+
disposition: "deleted", blocks: [ob.id], confidence: null, reason: "tombstone",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 2. Content updates: a carried top-level block whose raw changed. A carried
|
|
107
|
+
// container (list/blockquote) is updated as a whole — its raw carries the
|
|
108
|
+
// children — but `childIds` threads the reconcile-carried identity of the
|
|
109
|
+
// items onto the re-parsed subtree, so a within-container edit/reorder/insert
|
|
110
|
+
// preserves the identity of every item that was NOT itself changed (only
|
|
111
|
+
// genuinely new items mint). Nested identity, at no convergence cost.
|
|
112
|
+
for (const t of target) {
|
|
113
|
+
if (!t.carried)
|
|
114
|
+
continue;
|
|
115
|
+
const ob = oldById.get(t.id);
|
|
116
|
+
if (!ob)
|
|
117
|
+
continue; // carried an id that lives nested in old — handled by move below
|
|
118
|
+
if (rawHashHex(ob.raw) !== rawHashHex(t.raw)) {
|
|
119
|
+
const m = meta(t.id);
|
|
120
|
+
const childIds = t.children.length > 0 ? carriedChildIdMap(t, oldIds) : {};
|
|
121
|
+
ops.push({
|
|
122
|
+
op: { op: "update", block: t.id, markdown: t.raw, expect: { content_hash: rawHashHex(ob.raw) }, ...(Object.keys(childIds).length > 0 ? { childIds } : {}) },
|
|
123
|
+
disposition: "edited", blocks: [t.id], confidence: m.confidence, reason: m.reason, ...(m.detail ? { detail: m.detail } : {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 3. Placement: realize the proposed top-level order. Leave the LCS of
|
|
128
|
+
// survivors in place; move the rest and insert new blocks, each anchored after
|
|
129
|
+
// the preceding proposed block (a minted-id placeholder for a fresh insert).
|
|
130
|
+
const keptTop = target.filter((t) => t.carried && oldTopSet.has(t.id)).map((t) => t.id);
|
|
131
|
+
const survivorsInOldOrder = oldTopIds.filter((id) => targetIdSet.has(id) && keptTop.includes(id));
|
|
132
|
+
const stable = lcs(survivorsInOldOrder, keptTop);
|
|
133
|
+
let prevRef = null; // null = document start
|
|
134
|
+
const refOf = []; // parallel to target, the id/placeholder to anchor after
|
|
135
|
+
const placed = []; // parallel to target: realized by insert/move (seam-healing ops)
|
|
136
|
+
target.forEach((t, i) => {
|
|
137
|
+
const anchorAt = prevRef === null ? "start" : { after: prevRef };
|
|
138
|
+
placed[i] = !t.carried || !oldTopSet.has(t.id) || !stable.has(t.id);
|
|
139
|
+
if (!t.carried || !oldTopSet.has(t.id)) {
|
|
140
|
+
// Insert new (or a block not previously top-level) at this position.
|
|
141
|
+
const opIndex = ops.length;
|
|
142
|
+
ops.push({
|
|
143
|
+
op: { op: "insert", doc: docId, to: { parent: { doc: true }, at: anchorAt }, markdown: t.raw },
|
|
144
|
+
disposition: dispositionForNew(dispBy.get(t.id)?.kind), blocks: [], confidence: dispBy.get(t.id)?.confidence ?? null, reason: dispBy.get(t.id)?.reason ?? null,
|
|
145
|
+
});
|
|
146
|
+
prevRef = `$${opIndex}.ids[0]`;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (!stable.has(t.id)) {
|
|
150
|
+
const m = meta(t.id);
|
|
151
|
+
ops.push({
|
|
152
|
+
op: { op: "move", blocks: [t.id], to: { parent: { doc: true }, at: anchorAt } },
|
|
153
|
+
disposition: "moved", blocks: [t.id], confidence: m.confidence, reason: m.reason, ...(m.detail ? { detail: m.detail } : {}),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
prevRef = t.id;
|
|
157
|
+
}
|
|
158
|
+
refOf[i] = { ref: prevRef };
|
|
159
|
+
});
|
|
160
|
+
// 4. Trivia: set exact trailing trivia for proposed top-level blocks that are
|
|
161
|
+
// inserted, moved, or whose trivia differs from the current block's — so the
|
|
162
|
+
// committed bytes equal the proposed content exactly. A block ANCHORING an
|
|
163
|
+
// insert/move is retiled too: those ops heal the seam (bump a lone "\n" to a
|
|
164
|
+
// blank line) so hand-authored ops never jam blocks, but the proposed content
|
|
165
|
+
// may legitimately want the tight seam (a blockquote directly under a heading).
|
|
166
|
+
target.forEach((t, i) => {
|
|
167
|
+
const ob = t.carried ? oldById.get(t.id) : undefined;
|
|
168
|
+
const wasTop = ob && oldTopSet.has(t.id);
|
|
169
|
+
const moved = wasTop && !stable.has(t.id);
|
|
170
|
+
const anchorsPlacement = placed[i + 1] === true;
|
|
171
|
+
const needs = !wasTop || moved || anchorsPlacement || (ob && ob.trivia !== t.trivia);
|
|
172
|
+
if (!needs)
|
|
173
|
+
return;
|
|
174
|
+
ops.push({
|
|
175
|
+
op: { op: "update", block: refOf[i].ref, trivia: t.trivia },
|
|
176
|
+
disposition: "retiled", blocks: wasTop ? [t.id] : [], confidence: null, reason: null,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
// preserved = carried top-level blocks with unchanged content AND position.
|
|
180
|
+
for (const t of target) {
|
|
181
|
+
if (t.carried && oldById.get(t.id) && rawHashHex(oldById.get(t.id).raw) === rawHashHex(t.raw) && stable.has(t.id))
|
|
182
|
+
preserved++;
|
|
183
|
+
}
|
|
184
|
+
return { ops, preserved, ambiguous };
|
|
185
|
+
}
|
|
186
|
+
// Positional-key → id map for a container's carried descendants (keys relative
|
|
187
|
+
// to the container's own children, e.g. "/0", "/1", "/0/0"). Only carried ids
|
|
188
|
+
// (present in the current tree) are threaded; genuinely-new positions are
|
|
189
|
+
// omitted so the op stays deterministic and opUpdate mints those fresh.
|
|
190
|
+
function carriedChildIdMap(container, oldIds) {
|
|
191
|
+
const out = {};
|
|
192
|
+
const walk = (children, parentKey) => {
|
|
193
|
+
children.forEach((c, i) => {
|
|
194
|
+
const key = `${parentKey}/${i}`;
|
|
195
|
+
if (oldIds.has(c.id))
|
|
196
|
+
out[key] = c.id;
|
|
197
|
+
if (c.children.length > 0)
|
|
198
|
+
walk(c.children, key);
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
walk(container.children, "");
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
function dispositionForNew(kind) {
|
|
205
|
+
if (kind === "copied_from")
|
|
206
|
+
return "copied_from";
|
|
207
|
+
if (kind === "resurrected")
|
|
208
|
+
return "resurrected";
|
|
209
|
+
if (kind === "split_from")
|
|
210
|
+
return "split_from";
|
|
211
|
+
return "inserted";
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Guaranteed fallback: replace the whole body. Remove every current top-level
|
|
215
|
+
* block, then insert the entire proposed body as one op. Loses all block
|
|
216
|
+
* identity but always reproduces the exact bytes (verification still checks).
|
|
217
|
+
* Used only when no identity-preserving lowering converges.
|
|
218
|
+
*/
|
|
219
|
+
export function lowerReplace(oldDoc, content) {
|
|
220
|
+
const ops = [];
|
|
221
|
+
for (const ob of oldDoc.children) {
|
|
222
|
+
ops.push({ op: { op: "remove", blocks: [ob.id] }, disposition: "deleted", blocks: [ob.id], confidence: null, reason: "tombstone" });
|
|
223
|
+
}
|
|
224
|
+
ops.push({ op: { op: "insert", doc: oldDoc.docId, to: { parent: { doc: true }, at: "end" }, markdown: bodyOf(content) }, disposition: "bulk_rewrite", blocks: [], confidence: null, reason: null });
|
|
225
|
+
return { ops, preserved: 0, ambiguous: 0 };
|
|
226
|
+
}
|
|
227
|
+
// Strip a leading YAML frontmatter block from raw content for a body-only insert.
|
|
228
|
+
function bodyOf(content) {
|
|
229
|
+
const m = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(content);
|
|
230
|
+
return m ? content.slice(m[0].length).replace(/^\s*\n/, "") : content;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=lower.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Store } from "../core/store/store.js";
|
|
2
|
+
import type { Op } from "./apply.js";
|
|
3
|
+
import type { To } from "./ops.js";
|
|
4
|
+
/** tasks_complete: update(attrs:{checked:true}) per block. */
|
|
5
|
+
export declare function tasksComplete(store: Store, blocks: string[]): Op[];
|
|
6
|
+
/** sections_append: insert markdown at the end of a heading's section. */
|
|
7
|
+
export declare function sectionsAppend(heading: string, markdown: string): Op[];
|
|
8
|
+
/**
|
|
9
|
+
* docs_append: insert markdown at the END of a whole document — the
|
|
10
|
+
* document-root peer of sections_append (which appends within a heading's
|
|
11
|
+
* section). ADDITIVE, not a whole-body replace: it parses `markdown` into new
|
|
12
|
+
* blocks and inserts them as fresh top-level blocks after the document's
|
|
13
|
+
* existing ones, so every existing block keeps its stable `b_` id. Expands to a
|
|
14
|
+
* single insert op targeting the document top level (`{ doc: true }`, `at:
|
|
15
|
+
* "end"`) with an explicit `doc` — the kernel then threads the new blocks in
|
|
16
|
+
* without touching any prior block. The doc must already exist; resolving the
|
|
17
|
+
* ref is the caller's job (a missing doc is doc_missing — creation belongs to
|
|
18
|
+
* docs_create, never here).
|
|
19
|
+
*/
|
|
20
|
+
export declare function docsAppend(docId: string, markdown: string): Op[];
|
|
21
|
+
/** sections_rename: update the heading block's markdown to a new title. */
|
|
22
|
+
export declare function sectionsRename(store: Store, heading: string, title: string): Op[];
|
|
23
|
+
/** sections_move: move a heading's whole section range to a new location. */
|
|
24
|
+
export declare function sectionsMove(store: Store, heading: string, to: To): Op[];
|
|
25
|
+
/** lists_insert_item: insert a list item at a position relative to a list/item. */
|
|
26
|
+
export declare function listsInsertItem(anchor: string, at: "start" | "end" | {
|
|
27
|
+
before: string;
|
|
28
|
+
} | {
|
|
29
|
+
after: string;
|
|
30
|
+
}, markdown: string): Op[];
|
|
31
|
+
export interface RetargetHit {
|
|
32
|
+
block: string;
|
|
33
|
+
/** repo-relative path of the block's document */
|
|
34
|
+
path: string;
|
|
35
|
+
oldRaw: string;
|
|
36
|
+
newRaw: string;
|
|
37
|
+
}
|
|
38
|
+
/** node_set: surgically set one editable property of a node (node-editability).
|
|
39
|
+
* Resolves the node → its block + owning doc's format adapter, runs the adapter's
|
|
40
|
+
* registered editor for (kind, prop) against the block's current raw + the node's
|
|
41
|
+
* span, and returns a single kernel `update` op (markdown or attrs) with a CAS
|
|
42
|
+
* pin. The node stays a read-only projection; the write goes through the block.
|
|
43
|
+
* Throws node_not_editable when no editor is registered for (kind, prop). */
|
|
44
|
+
export declare function nodeSet(store: Store, nodeId: string, prop: string, value: string): Op[];
|
|
45
|
+
/** The editable property names registered for a (format, kind), for discovery. */
|
|
46
|
+
export declare function editablePropsFor(format: string, kind: string): string[];
|
|
47
|
+
/** links_retarget: the single-pair form of linksRepair (delegates). */
|
|
48
|
+
export declare function linksRetarget(store: Store, repoId: string, fromTarget: string, toTarget: string, opts?: LinkRepairOptions): LinkRepairPlan;
|
|
49
|
+
/** A single from→to link-destination rewrite. */
|
|
50
|
+
export interface LinkRepair {
|
|
51
|
+
from: string;
|
|
52
|
+
to: string;
|
|
53
|
+
}
|
|
54
|
+
export interface LinkRepairOptions {
|
|
55
|
+
/** scope the SOURCE docs by a path glob (`journal/*`), mirroring linksStale. */
|
|
56
|
+
pathGlob?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Per-pair tally: how many link destinations `from` matched (across all hits). */
|
|
59
|
+
export interface LinkRepairCount extends LinkRepair {
|
|
60
|
+
hits: number;
|
|
61
|
+
}
|
|
62
|
+
export interface LinkRepairPlan {
|
|
63
|
+
/** kernel update ops, one per rewritten TOP-MOST block */
|
|
64
|
+
ops: Op[];
|
|
65
|
+
/** the rewritten blocks (dry-run preview) */
|
|
66
|
+
hits: RetargetHit[];
|
|
67
|
+
/** each input pair with its destination-match count (0 = nothing to fix) */
|
|
68
|
+
pairs: LinkRepairCount[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* links_repair: bulk stale-link repair. Given a batch of {from,to} destination
|
|
72
|
+
* rewrites, produce the kernel update ops plus the hits for dry-run preview.
|
|
73
|
+
* Repairs compose within ONE changeset so an agent can fix many dangling
|
|
74
|
+
* targets (surfaced by linksStale) at once.
|
|
75
|
+
*
|
|
76
|
+
* Scope: this rewrites LINK DESTINATIONS, not substrings. A destination is
|
|
77
|
+
* rewritten only when `from` is the WHOLE destination of a Markdown link /
|
|
78
|
+
* image, a wikilink, or a bare-path inline field (`key:: /path`) — ignoring the
|
|
79
|
+
* leading `/`, and allowing a trailing `#heading`/`^ref` fragment which is
|
|
80
|
+
* re-appended. Prose mentions, inline code, and `code_fence` blocks are never
|
|
81
|
+
* touched, and a `from` that is merely a suffix/prefix of a longer path does
|
|
82
|
+
* not match it. Each destination takes the FIRST matching pair (no chaining).
|
|
83
|
+
* Frontmatter links are not blocks and are out of reach here (use docs_set_meta).
|
|
84
|
+
*
|
|
85
|
+
* Coalescing: a block hit by several pairs gets a single update op (one CAS pin
|
|
86
|
+
* per block). Hits collapse to their TOP-MOST block: a list and its list items
|
|
87
|
+
* both carry the same bytes, and updating the container re-parses (re-mints)
|
|
88
|
+
* its children, so a second op on the child would fail block_missing and sink
|
|
89
|
+
* the whole changeset. Mirrors opRemove's top-most collapse. No FS/network I/O.
|
|
90
|
+
*/
|
|
91
|
+
export declare function linksRepair(store: Store, repoId: string, repairs: LinkRepair[], opts?: LinkRepairOptions): LinkRepairPlan;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { adapterForFormat } from "../format/registry.js";
|
|
2
|
+
import { MutationError } from "./tree.js";
|
|
3
|
+
import { globClause } from "../graph/link-health.js";
|
|
4
|
+
import { rewriteLinkDestinations, splitDestination } from "../graph/link-destinations.js";
|
|
5
|
+
// Server-side macros (04 §3). Each expands deterministically to kernel ops —
|
|
6
|
+
// no policy judgment (that belongs in the agent). The expansion is returned so
|
|
7
|
+
// callers see the kernel ops that ran. Macros compose within one changeset.
|
|
8
|
+
function rawHashOfBlock(store, blockId) {
|
|
9
|
+
const row = store.db.prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_commit IS NULL").get(blockId);
|
|
10
|
+
return row?.h ?? null;
|
|
11
|
+
}
|
|
12
|
+
function rawOfBlock(store, blockId) {
|
|
13
|
+
const row = store.db.prepare("SELECT b.bytes FROM blocks bl JOIN blobs b ON b.hash = bl.raw_hash WHERE bl.block_id = ? AND bl.deleted_commit IS NULL").get(blockId);
|
|
14
|
+
return row?.bytes.toString("utf8") ?? null;
|
|
15
|
+
}
|
|
16
|
+
/** tasks_complete: update(attrs:{checked:true}) per block. */
|
|
17
|
+
export function tasksComplete(store, blocks) {
|
|
18
|
+
return blocks.map((block) => {
|
|
19
|
+
const hash = rawHashOfBlock(store, block);
|
|
20
|
+
return { op: "update", block, attrs: { checked: true }, ...(hash ? { expect: { content_hash: hash } } : {}) };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** sections_append: insert markdown at the end of a heading's section. */
|
|
24
|
+
export function sectionsAppend(heading, markdown) {
|
|
25
|
+
const to = { parent: { heading, scope: "section" }, at: "end" };
|
|
26
|
+
return [{ op: "insert", to, markdown }];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* docs_append: insert markdown at the END of a whole document — the
|
|
30
|
+
* document-root peer of sections_append (which appends within a heading's
|
|
31
|
+
* section). ADDITIVE, not a whole-body replace: it parses `markdown` into new
|
|
32
|
+
* blocks and inserts them as fresh top-level blocks after the document's
|
|
33
|
+
* existing ones, so every existing block keeps its stable `b_` id. Expands to a
|
|
34
|
+
* single insert op targeting the document top level (`{ doc: true }`, `at:
|
|
35
|
+
* "end"`) with an explicit `doc` — the kernel then threads the new blocks in
|
|
36
|
+
* without touching any prior block. The doc must already exist; resolving the
|
|
37
|
+
* ref is the caller's job (a missing doc is doc_missing — creation belongs to
|
|
38
|
+
* docs_create, never here).
|
|
39
|
+
*/
|
|
40
|
+
export function docsAppend(docId, markdown) {
|
|
41
|
+
const to = { parent: { doc: true }, at: "end" };
|
|
42
|
+
return [{ op: "insert", doc: docId, to, markdown }];
|
|
43
|
+
}
|
|
44
|
+
/** sections_rename: update the heading block's markdown to a new title. */
|
|
45
|
+
export function sectionsRename(store, heading, title) {
|
|
46
|
+
const raw = rawOfBlock(store, heading) ?? "";
|
|
47
|
+
const level = (/^(#{1,6})\s/.exec(raw)?.[1] ?? "#").length;
|
|
48
|
+
const hash = rawHashOfBlock(store, heading);
|
|
49
|
+
const markdown = `${"#".repeat(level)} ${title}`;
|
|
50
|
+
return [{ op: "update", block: heading, markdown, ...(hash ? { expect: { content_hash: hash } } : {}) }];
|
|
51
|
+
}
|
|
52
|
+
/** sections_move: move a heading's whole section range to a new location. */
|
|
53
|
+
export function sectionsMove(store, heading, to) {
|
|
54
|
+
// The section range is the heading + blocks until the next peer/higher
|
|
55
|
+
// heading. We resolve the contiguous top-level run of ids from the store.
|
|
56
|
+
const docRow = store.db.prepare("SELECT doc_id, ordinal, json_extract(attrs,'$.level') level FROM blocks WHERE block_id = ?").get(heading);
|
|
57
|
+
if (!docRow)
|
|
58
|
+
return [];
|
|
59
|
+
const level = docRow.level ?? 1;
|
|
60
|
+
const tops = store.db.prepare("SELECT block_id, ordinal, type, json_extract(attrs,'$.level') level FROM blocks WHERE doc_id = ? AND parent_block IS NULL AND deleted_commit IS NULL ORDER BY ordinal").all(docRow.doc_id);
|
|
61
|
+
const startIdx = tops.findIndex((t) => t.block_id === heading);
|
|
62
|
+
let end = tops.length;
|
|
63
|
+
for (let i = startIdx + 1; i < tops.length; i++) {
|
|
64
|
+
if (tops[i].type === "heading" && (tops[i].level ?? 1) <= level) {
|
|
65
|
+
end = i;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const run = tops.slice(startIdx, end).map((t) => t.block_id);
|
|
70
|
+
return [{ op: "move", blocks: run, to }];
|
|
71
|
+
}
|
|
72
|
+
/** lists_insert_item: insert a list item at a position relative to a list/item. */
|
|
73
|
+
export function listsInsertItem(anchor, at, markdown) {
|
|
74
|
+
const item = markdown.trimStart().startsWith("- ") ? markdown : `- ${markdown}`;
|
|
75
|
+
return [{ op: "insert", to: { parent: anchor, at }, markdown: item }];
|
|
76
|
+
}
|
|
77
|
+
/** node_set: surgically set one editable property of a node (node-editability).
|
|
78
|
+
* Resolves the node → its block + owning doc's format adapter, runs the adapter's
|
|
79
|
+
* registered editor for (kind, prop) against the block's current raw + the node's
|
|
80
|
+
* span, and returns a single kernel `update` op (markdown or attrs) with a CAS
|
|
81
|
+
* pin. The node stays a read-only projection; the write goes through the block.
|
|
82
|
+
* Throws node_not_editable when no editor is registered for (kind, prop). */
|
|
83
|
+
export function nodeSet(store, nodeId, prop, value) {
|
|
84
|
+
const node = store.db.prepare(`SELECT n.kind, n.name, n.value, n.attrs, n.span_start, n.span_end, n.block_id,
|
|
85
|
+
d.format AS format
|
|
86
|
+
FROM nodes n JOIN docs d ON d.doc_id = n.doc_id
|
|
87
|
+
WHERE n.node_id = ?`).get(nodeId);
|
|
88
|
+
if (!node)
|
|
89
|
+
throw new MutationError("block_missing", `node ${nodeId} not found`, {});
|
|
90
|
+
if (!node.block_id)
|
|
91
|
+
throw new MutationError("node_not_editable", `node ${nodeId} is not anchored to a block`, {});
|
|
92
|
+
const adapter = adapterForFormat(node.format);
|
|
93
|
+
const editor = adapter?.nodeEditors?.[node.kind]?.[prop];
|
|
94
|
+
if (!editor) {
|
|
95
|
+
throw new MutationError("node_not_editable", `no editor for ${node.kind}.${prop}`, {
|
|
96
|
+
kind: node.kind, prop, editable: editablePropsFor(node.format, node.kind),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const blockRaw = rawOfBlock(store, node.block_id);
|
|
100
|
+
if (blockRaw === null)
|
|
101
|
+
throw new MutationError("block_missing", `block ${node.block_id} not found`, {});
|
|
102
|
+
const hash = rawHashOfBlock(store, node.block_id);
|
|
103
|
+
const result = editor({
|
|
104
|
+
blockRaw,
|
|
105
|
+
span: node.span_start !== null && node.span_end !== null ? { start: node.span_start, end: node.span_end } : null,
|
|
106
|
+
node: {
|
|
107
|
+
kind: node.kind,
|
|
108
|
+
...(node.name !== null ? { name: node.name } : {}),
|
|
109
|
+
...(node.value !== null ? { value: node.value } : {}),
|
|
110
|
+
attrs: JSON.parse(node.attrs || "{}"),
|
|
111
|
+
},
|
|
112
|
+
}, value);
|
|
113
|
+
const expect = hash ? { expect: { content_hash: hash } } : {};
|
|
114
|
+
if ("markdown" in result) {
|
|
115
|
+
return [{ op: "update", block: node.block_id, markdown: result.markdown, ...expect }];
|
|
116
|
+
}
|
|
117
|
+
return [{ op: "update", block: node.block_id, attrs: result.attrs, ...expect }];
|
|
118
|
+
}
|
|
119
|
+
/** The editable property names registered for a (format, kind), for discovery. */
|
|
120
|
+
export function editablePropsFor(format, kind) {
|
|
121
|
+
const editors = adapterForFormat(format)?.nodeEditors?.[kind];
|
|
122
|
+
return editors ? Object.keys(editors) : [];
|
|
123
|
+
}
|
|
124
|
+
/** links_retarget: the single-pair form of linksRepair (delegates). */
|
|
125
|
+
export function linksRetarget(store, repoId, fromTarget, toTarget, opts = {}) {
|
|
126
|
+
return linksRepair(store, repoId, [{ from: fromTarget, to: toTarget }], opts);
|
|
127
|
+
}
|
|
128
|
+
const stripSlash = (s) => s.replace(/^\//, "");
|
|
129
|
+
// Does destination `dest` (as authored) name the same link target as `from`?
|
|
130
|
+
// Whole-destination match first (lets `from` carry its own #fragment), then the
|
|
131
|
+
// path part alone (the fragment rides along). Both sides compare with the
|
|
132
|
+
// leading `/` dropped, so `from` may be given either as authored (`/b.md`) or as
|
|
133
|
+
// links_stale reports `target` (`b.md`). Returns the fragment to re-append.
|
|
134
|
+
function destinationMatch(dest, from) {
|
|
135
|
+
if (stripSlash(dest) === stripSlash(from))
|
|
136
|
+
return { fragment: "" };
|
|
137
|
+
const { path, fragment } = splitDestination(dest);
|
|
138
|
+
if (fragment !== "" && stripSlash(path) === stripSlash(from))
|
|
139
|
+
return { fragment };
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* links_repair: bulk stale-link repair. Given a batch of {from,to} destination
|
|
144
|
+
* rewrites, produce the kernel update ops plus the hits for dry-run preview.
|
|
145
|
+
* Repairs compose within ONE changeset so an agent can fix many dangling
|
|
146
|
+
* targets (surfaced by linksStale) at once.
|
|
147
|
+
*
|
|
148
|
+
* Scope: this rewrites LINK DESTINATIONS, not substrings. A destination is
|
|
149
|
+
* rewritten only when `from` is the WHOLE destination of a Markdown link /
|
|
150
|
+
* image, a wikilink, or a bare-path inline field (`key:: /path`) — ignoring the
|
|
151
|
+
* leading `/`, and allowing a trailing `#heading`/`^ref` fragment which is
|
|
152
|
+
* re-appended. Prose mentions, inline code, and `code_fence` blocks are never
|
|
153
|
+
* touched, and a `from` that is merely a suffix/prefix of a longer path does
|
|
154
|
+
* not match it. Each destination takes the FIRST matching pair (no chaining).
|
|
155
|
+
* Frontmatter links are not blocks and are out of reach here (use docs_set_meta).
|
|
156
|
+
*
|
|
157
|
+
* Coalescing: a block hit by several pairs gets a single update op (one CAS pin
|
|
158
|
+
* per block). Hits collapse to their TOP-MOST block: a list and its list items
|
|
159
|
+
* both carry the same bytes, and updating the container re-parses (re-mints)
|
|
160
|
+
* its children, so a second op on the child would fail block_missing and sink
|
|
161
|
+
* the whole changeset. Mirrors opRemove's top-most collapse. No FS/network I/O.
|
|
162
|
+
*/
|
|
163
|
+
export function linksRepair(store, repoId, repairs, opts = {}) {
|
|
164
|
+
const pairs = repairs.map((r) => ({ from: r.from, to: r.to, hits: 0 }));
|
|
165
|
+
const effective = pairs.filter((r) => stripSlash(r.from) !== "" && r.from !== r.to);
|
|
166
|
+
if (effective.length === 0)
|
|
167
|
+
return { ops: [], hits: [], pairs };
|
|
168
|
+
// Candidate blocks: live, not a code fence, whose bytes contain the slash-less
|
|
169
|
+
// form of some `from` (a cheap prefilter; the destination scan below decides).
|
|
170
|
+
const params = [];
|
|
171
|
+
let globSql = "";
|
|
172
|
+
if (opts.pathGlob) {
|
|
173
|
+
const { clause, param } = globClause("d.path", opts.pathGlob);
|
|
174
|
+
globSql = `AND ${clause}`;
|
|
175
|
+
params.push(param);
|
|
176
|
+
}
|
|
177
|
+
const stmt = store.db.prepare(`SELECT bl.block_id, bl.parent_block, d.path, b.bytes
|
|
178
|
+
FROM blocks bl
|
|
179
|
+
JOIN blobs b ON b.hash = bl.raw_hash
|
|
180
|
+
JOIN docs d ON d.doc_id = bl.doc_id
|
|
181
|
+
WHERE bl.repo_id = ? AND bl.deleted_commit IS NULL AND d.deleted_commit IS NULL
|
|
182
|
+
AND bl.type != 'code_fence' AND instr(b.bytes, ?) > 0 ${globSql}`);
|
|
183
|
+
const candidates = new Map();
|
|
184
|
+
for (const key of new Set(effective.map((r) => stripSlash(r.from)))) {
|
|
185
|
+
const rows = stmt.all(repoId, key, ...params);
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
if (!candidates.has(row.block_id))
|
|
188
|
+
candidates.set(row.block_id, { parent: row.parent_block, path: row.path, raw: row.bytes.toString("utf8") });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Rewrite each candidate's destinations; keep the blocks that changed.
|
|
192
|
+
const changed = new Map();
|
|
193
|
+
for (const [blockId, c] of candidates) {
|
|
194
|
+
const tally = effective.map(() => 0);
|
|
195
|
+
const newRaw = rewriteLinkDestinations(c.raw, (dest) => {
|
|
196
|
+
for (let i = 0; i < effective.length; i++) {
|
|
197
|
+
const m = destinationMatch(dest, effective[i].from);
|
|
198
|
+
if (m) {
|
|
199
|
+
tally[i]++;
|
|
200
|
+
return effective[i].to + m.fragment;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
});
|
|
205
|
+
if (newRaw !== c.raw)
|
|
206
|
+
changed.set(blockId, { parent: c.parent, path: c.path, oldRaw: c.raw, newRaw, tally });
|
|
207
|
+
}
|
|
208
|
+
// Collapse to top-most blocks: drop a hit whose ancestor is also a hit.
|
|
209
|
+
const parentOf = store.db.prepare("SELECT parent_block FROM blocks WHERE block_id = ? AND deleted_commit IS NULL");
|
|
210
|
+
const hasChangedAncestor = (parent) => {
|
|
211
|
+
for (let p = parent; p !== null;) {
|
|
212
|
+
if (changed.has(p))
|
|
213
|
+
return true;
|
|
214
|
+
p = parentOf.get(p)?.parent_block ?? null;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
};
|
|
218
|
+
const ops = [];
|
|
219
|
+
const hits = [];
|
|
220
|
+
for (const [blockId, c] of changed) {
|
|
221
|
+
if (hasChangedAncestor(c.parent))
|
|
222
|
+
continue;
|
|
223
|
+
c.tally.forEach((n, i) => { effective[i].hits += n; });
|
|
224
|
+
const hash = rawHashOfBlock(store, blockId);
|
|
225
|
+
ops.push({ op: "update", block: blockId, markdown: c.newRaw, ...(hash ? { expect: { content_hash: hash } } : {}) });
|
|
226
|
+
hits.push({ block: blockId, path: c.path, oldRaw: c.oldRaw, newRaw: c.newRaw });
|
|
227
|
+
}
|
|
228
|
+
return { ops, hits, pairs };
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=macros.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type MutDoc } from "./tree.js";
|
|
2
|
+
export type At = "start" | "end" | {
|
|
3
|
+
before: string;
|
|
4
|
+
} | {
|
|
5
|
+
after: string;
|
|
6
|
+
};
|
|
7
|
+
export interface To {
|
|
8
|
+
parent: string | {
|
|
9
|
+
doc: true;
|
|
10
|
+
} | {
|
|
11
|
+
heading: string;
|
|
12
|
+
scope: "section";
|
|
13
|
+
};
|
|
14
|
+
at: At;
|
|
15
|
+
}
|
|
16
|
+
export interface Expect {
|
|
17
|
+
content_hash?: string;
|
|
18
|
+
parent_children_hash?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function opInsert(doc: MutDoc, to: To, markdown: string): {
|
|
21
|
+
ids: string[];
|
|
22
|
+
};
|
|
23
|
+
export declare function opUpdate(doc: MutDoc, blockId: string, opIndex: number, markdown?: string, attrs?: Record<string, unknown>, expect?: Expect, trivia?: string, childIds?: Record<string, string>): {
|
|
24
|
+
ids: string[];
|
|
25
|
+
};
|
|
26
|
+
export declare function opMove(doc: MutDoc, blockIds: string[], to: To, opIndex: number): {
|
|
27
|
+
ids: string[];
|
|
28
|
+
};
|
|
29
|
+
export declare function opRemove(doc: MutDoc, blockIds: string[], opIndex: number, expectPer?: Record<string, Expect>): {
|
|
30
|
+
ids: string[];
|
|
31
|
+
removed: string[];
|
|
32
|
+
};
|
|
33
|
+
export declare function opSplit(doc: MutDoc, blockId: string, at: number[], opIndex: number, expect?: Expect): {
|
|
34
|
+
ids: string[];
|
|
35
|
+
};
|
|
36
|
+
export declare function opMerge(doc: MutDoc, blockIds: string[], opIndex: number, separator?: string, expectPer?: Record<string, Expect>): {
|
|
37
|
+
ids: string[];
|
|
38
|
+
mergedInto: string[];
|
|
39
|
+
};
|
|
40
|
+
export declare function checkParentChildrenHash(doc: MutDoc, parentId: string | null, expected: string, opIndex: number): void;
|