@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,513 @@
|
|
|
1
|
+
import { mintId } from "../core/ids.js";
|
|
2
|
+
import { parseTree } from "../core/parse/tree.js";
|
|
3
|
+
import { adapterForFormat } from "../format/index.js";
|
|
4
|
+
import { MutationError, locate, rawHashHex, parentChildrenHash, markContainerDirty, ownerOf } from "./tree.js";
|
|
5
|
+
function defaultTrivia(format) {
|
|
6
|
+
if (format === "json")
|
|
7
|
+
return ",\n";
|
|
8
|
+
if (format === "yaml")
|
|
9
|
+
return "\n";
|
|
10
|
+
return "\n\n";
|
|
11
|
+
}
|
|
12
|
+
// Parse op-supplied content into MutBlocks (ids minted). Uses the format's
|
|
13
|
+
// adapter when available; falls back to markdown. The parsed blocks are CLEAN:
|
|
14
|
+
// a freshly parsed block's raw is the byte-exact source for its whole subtree,
|
|
15
|
+
// so renderDoc must emit it verbatim. Marking it dirty would make renderBlock
|
|
16
|
+
// rebuild the container from its children, which cannot reproduce a blockquote
|
|
17
|
+
// (`> ` markers live only in the container raw) or a table (the delimiter row
|
|
18
|
+
// is not a child node) — the op that splices these blocks in is responsible
|
|
19
|
+
// for invalidating the CONTAINER it lands in (markContainerDirty).
|
|
20
|
+
function parseContentToBlocks(content, format) {
|
|
21
|
+
const adapter = adapterForFormat(format);
|
|
22
|
+
const normalized = content.endsWith("\n") ? content : content + "\n";
|
|
23
|
+
const tree = adapter ? adapter.parse(normalized) : parseTree(normalized);
|
|
24
|
+
const trivia = defaultTrivia(format);
|
|
25
|
+
const toMut = (b) => ({
|
|
26
|
+
id: mintId("b"),
|
|
27
|
+
type: b.type,
|
|
28
|
+
raw: b.raw,
|
|
29
|
+
trivia: b.trivia || trivia,
|
|
30
|
+
attrs: b.attrs,
|
|
31
|
+
children: b.children.map(toMut),
|
|
32
|
+
dirty: false,
|
|
33
|
+
});
|
|
34
|
+
return tree.children.filter((b) => b.type !== "frontmatter").map(toMut);
|
|
35
|
+
}
|
|
36
|
+
// Resolve a To into a target sibling list + insertion index.
|
|
37
|
+
function resolveTarget(doc, to) {
|
|
38
|
+
let siblings;
|
|
39
|
+
if (to.parent === null || (typeof to.parent === "object" && "doc" in to.parent)) {
|
|
40
|
+
siblings = doc.children;
|
|
41
|
+
}
|
|
42
|
+
else if (typeof to.parent === "object" && "heading" in to.parent) {
|
|
43
|
+
// scope:section — resolve the section's range at top level: insert before
|
|
44
|
+
// the next peer/higher heading (04 §1.1). Section blocks live at top level.
|
|
45
|
+
return resolveSection(doc, to.parent.heading, to.at);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const found = locate(doc, to.parent);
|
|
49
|
+
if (!found)
|
|
50
|
+
throw new MutationError("parent_missing", `parent ${String(to.parent)} not found`);
|
|
51
|
+
siblings = found.block.children;
|
|
52
|
+
}
|
|
53
|
+
return { siblings, index: resolveIndex(siblings, to.at) };
|
|
54
|
+
}
|
|
55
|
+
function resolveIndex(siblings, at) {
|
|
56
|
+
if (at === "start")
|
|
57
|
+
return 0;
|
|
58
|
+
if (at === "end")
|
|
59
|
+
return siblings.length;
|
|
60
|
+
if ("before" in at) {
|
|
61
|
+
const i = siblings.findIndex((b) => b.id === at.before);
|
|
62
|
+
if (i < 0)
|
|
63
|
+
throw new MutationError("target_missing", `anchor ${at.before} not found`);
|
|
64
|
+
return i;
|
|
65
|
+
}
|
|
66
|
+
const i = siblings.findIndex((b) => b.id === at.after);
|
|
67
|
+
if (i < 0)
|
|
68
|
+
throw new MutationError("target_missing", `anchor ${at.after} not found`);
|
|
69
|
+
return i + 1;
|
|
70
|
+
}
|
|
71
|
+
// A section runs from its heading to the block before the next heading of equal
|
|
72
|
+
// or higher level. Blocks are top-level (flat containment, 01 §3.2).
|
|
73
|
+
function resolveSection(doc, headingId, at) {
|
|
74
|
+
const hIdx = doc.children.findIndex((b) => b.id === headingId);
|
|
75
|
+
if (hIdx < 0)
|
|
76
|
+
throw new MutationError("target_missing", `heading ${headingId} not found`);
|
|
77
|
+
const level = Number(doc.children[hIdx].attrs.level ?? 1);
|
|
78
|
+
let end = doc.children.length;
|
|
79
|
+
for (let i = hIdx + 1; i < doc.children.length; i++) {
|
|
80
|
+
const b = doc.children[i];
|
|
81
|
+
if (b.type === "heading" && Number(b.attrs.level ?? 1) <= level) {
|
|
82
|
+
end = i;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (at === "end")
|
|
87
|
+
return { siblings: doc.children, index: end };
|
|
88
|
+
if (at === "start")
|
|
89
|
+
return { siblings: doc.children, index: hIdx + 1 };
|
|
90
|
+
return { siblings: doc.children, index: resolveIndex(doc.children, at) };
|
|
91
|
+
}
|
|
92
|
+
function checkContentHash(block, expect, opIndex) {
|
|
93
|
+
if (!expect?.content_hash) {
|
|
94
|
+
// Hand back the current hash + bytes so the caller can retry immediately
|
|
95
|
+
// (same actionable shape as a hash MISMATCH below) instead of a separate
|
|
96
|
+
// hydration read — an agent may hold fresh ids yet lack the CAS token.
|
|
97
|
+
throw new MutationError("stale_expectation", "expect.content_hash required", {
|
|
98
|
+
op_index: opIndex,
|
|
99
|
+
block: block.id,
|
|
100
|
+
current: { content_hash: rawHashHex(block.raw), markdown: block.raw },
|
|
101
|
+
retriable: true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const current = rawHashHex(block.raw);
|
|
105
|
+
if (current !== expect.content_hash) {
|
|
106
|
+
throw new MutationError("stale_expectation", "content hash mismatch", {
|
|
107
|
+
op_index: opIndex,
|
|
108
|
+
block: block.id,
|
|
109
|
+
expected_content_hash: expect.content_hash,
|
|
110
|
+
current: { content_hash: current, markdown: block.raw },
|
|
111
|
+
retriable: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Assign known ids onto a freshly-parsed subtree by positional key (parentKey +
|
|
116
|
+
// '/' + index), the same convention flatten()/known-ids use. Keys absent from
|
|
117
|
+
// the map leave the block's minted id in place (a genuinely new child).
|
|
118
|
+
function assignChildIds(children, childIds, parentKey) {
|
|
119
|
+
children.forEach((c, i) => {
|
|
120
|
+
const key = `${parentKey}/${i}`;
|
|
121
|
+
const id = childIds[key];
|
|
122
|
+
if (id)
|
|
123
|
+
c.id = id;
|
|
124
|
+
if (c.children.length > 0)
|
|
125
|
+
assignChildIds(c.children, childIds, key);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Wrap a non-list parsed block as a list_item (marker prepended) so it can be
|
|
129
|
+
// inserted into a list. The marker is normalized on render (renderItem strips
|
|
130
|
+
// and re-applies it, renumbering ordered lists), so `- ` here is just a seed.
|
|
131
|
+
function wrapAsItem(b) {
|
|
132
|
+
return { id: mintId("b"), type: "list_item", raw: `- ${b.raw}`, trivia: "", attrs: {}, children: [], dirty: true };
|
|
133
|
+
}
|
|
134
|
+
// Mark a block and its whole subtree clean (dirty=false), so renderDoc emits the
|
|
135
|
+
// block's retained raw verbatim rather than rebuilding it from children.
|
|
136
|
+
function markSubtreeClean(b) {
|
|
137
|
+
b.dirty = false;
|
|
138
|
+
for (const c of b.children)
|
|
139
|
+
markSubtreeClean(c);
|
|
140
|
+
}
|
|
141
|
+
// ---- the six ops ------------------------------------------------------------
|
|
142
|
+
export function opInsert(doc, to, markdown) {
|
|
143
|
+
const { siblings, index } = resolveTarget(doc, to);
|
|
144
|
+
let blocks = parseContentToBlocks(markdown, doc.format);
|
|
145
|
+
// Inserting into a list: the content must land as list ITEMS, not a nested
|
|
146
|
+
// list. Markdown `- x` parses to a `list` wrapping a `list_item`; unwrap it to
|
|
147
|
+
// its items (a bare block is wrapped as one item). Then mark the list dirty so
|
|
148
|
+
// it re-renders from its new items. (04 §1 / lists_insert_item.)
|
|
149
|
+
const container = ownerOf(doc, siblings);
|
|
150
|
+
if (container?.type === "list" && doc.format === "markdown") {
|
|
151
|
+
blocks = blocks.flatMap((b) => (b.type === "list" ? b.children : [wrapAsItem(b)]));
|
|
152
|
+
siblings.splice(index, 0, ...blocks);
|
|
153
|
+
container.dirty = true;
|
|
154
|
+
return { ids: blocks.map((b) => b.id) };
|
|
155
|
+
}
|
|
156
|
+
// Trivia is document tiling rendered independently of a block's raw, so the
|
|
157
|
+
// separators fixed up here set no `dirty` flag — no block content changed.
|
|
158
|
+
// Only the top-level list is rendered from trivia (nested containers are
|
|
159
|
+
// re-emitted from children with single-newline joins, so their trivia is
|
|
160
|
+
// inert); confine separator normalization to that list.
|
|
161
|
+
if (siblings === doc.children) {
|
|
162
|
+
const sep = defaultTrivia(doc.format);
|
|
163
|
+
// Separator BEFORE the run: the block now preceding it must end at a real
|
|
164
|
+
// block boundary, or the two render jammed together — e.g. appending a
|
|
165
|
+
// heading at the end of a section whose last paragraph carried only a
|
|
166
|
+
// single trailing "\n" glued "…text\n## Heading" with no blank line.
|
|
167
|
+
if (index > 0) {
|
|
168
|
+
const prev = siblings[index - 1];
|
|
169
|
+
if (!separatesBlocks(prev.trivia, doc.format))
|
|
170
|
+
prev.trivia = sep;
|
|
171
|
+
}
|
|
172
|
+
// Separator AFTER the run: a mid-document insert leaves a following block,
|
|
173
|
+
// and the parsed run's last block ends in a lone "\n" — bump it too.
|
|
174
|
+
if (blocks.length > 0 && index < siblings.length) {
|
|
175
|
+
const last = blocks[blocks.length - 1];
|
|
176
|
+
if (!separatesBlocks(last.trivia, doc.format))
|
|
177
|
+
last.trivia = sep;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
siblings.splice(index, 0, ...blocks);
|
|
181
|
+
// A nested insert (into a blockquote/table/list item) changes the owning
|
|
182
|
+
// container's children, so its cached raw is stale — rebuild it from children.
|
|
183
|
+
if (siblings !== doc.children)
|
|
184
|
+
markContainerDirty(doc, siblings);
|
|
185
|
+
return { ids: blocks.map((b) => b.id) };
|
|
186
|
+
}
|
|
187
|
+
// Does this trailing trivia hold a real block boundary? Markdown block-level
|
|
188
|
+
// content needs a blank line between blocks (a lone "\n" renders as a soft
|
|
189
|
+
// continuation, jamming an ATX heading onto the previous paragraph); other
|
|
190
|
+
// formats only need the separator to be non-empty.
|
|
191
|
+
function separatesBlocks(trivia, format) {
|
|
192
|
+
return format === "markdown" ? trivia.includes("\n\n") : trivia !== "";
|
|
193
|
+
}
|
|
194
|
+
export function opUpdate(doc, blockId, opIndex, markdown, attrs, expect, trivia, childIds) {
|
|
195
|
+
const found = locate(doc, blockId);
|
|
196
|
+
if (!found)
|
|
197
|
+
throw new MutationError("block_missing", `block ${blockId} not found`, { op_index: opIndex, block: blockId });
|
|
198
|
+
// Content CAS is required only for a content edit (`markdown`). Attr- and
|
|
199
|
+
// trivia-only updates change no block content, so they follow the placement
|
|
200
|
+
// convention (no content CAS); a supplied `expect.content_hash` is still
|
|
201
|
+
// honored when present (CAS honesty, README invariant 6). This lets the
|
|
202
|
+
// whole-doc planner set exact trailing trivia after structural ops without
|
|
203
|
+
// juggling post-op content hashes across the changeset.
|
|
204
|
+
if (markdown !== undefined || expect?.content_hash !== undefined)
|
|
205
|
+
checkContentHash(found.block, expect, opIndex);
|
|
206
|
+
// Blocks minted from MULTI-block content: the target takes the first parsed
|
|
207
|
+
// block (keeping its id); the rest become fresh siblings right after it.
|
|
208
|
+
const extra = [];
|
|
209
|
+
if (markdown !== undefined) {
|
|
210
|
+
if (doc.format === "markdown" && found.block.type === "list_item") {
|
|
211
|
+
// A list item can't be re-parsed as a standalone block (`- x` parses to a
|
|
212
|
+
// list, `x` loses the marker). Unwrap: accept a list (its first item
|
|
213
|
+
// replaces this one, further items become following siblings) or a bare
|
|
214
|
+
// block (wrap it), keep this block a list_item, and mark the containing
|
|
215
|
+
// list dirty so it re-renders from its items.
|
|
216
|
+
const parsed = parseContentToBlocks(markdown, doc.format);
|
|
217
|
+
if (parsed.length !== 1) {
|
|
218
|
+
throw new MutationError("type_mismatch", "list-item update content must be ONE list (`- a\\n- b`: first item replaces, the rest follow as siblings) or ONE bare block", {
|
|
219
|
+
op_index: opIndex, block: blockId, hint: "to put mixed content under an item, update the item then blocks_insert the rest with `to` = the item",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const only = parsed[0];
|
|
223
|
+
if (only.type === "list") {
|
|
224
|
+
if (only.children.length === 0)
|
|
225
|
+
throw new MutationError("type_mismatch", "list-item update must yield at least one item", { op_index: opIndex, block: blockId });
|
|
226
|
+
found.block.raw = only.children[0].raw;
|
|
227
|
+
found.block.children = only.children[0].children;
|
|
228
|
+
extra.push(...only.children.slice(1));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
found.block.raw = `- ${only.raw}`;
|
|
232
|
+
found.block.children = [];
|
|
233
|
+
}
|
|
234
|
+
found.block.dirty = true;
|
|
235
|
+
markContainerDirty(doc, found.siblings);
|
|
236
|
+
}
|
|
237
|
+
else if (doc.format === "markdown") {
|
|
238
|
+
const parsed = parseContentToBlocks(markdown, doc.format);
|
|
239
|
+
if (parsed.length === 0)
|
|
240
|
+
throw new MutationError("type_mismatch", "update content must contain at least one block", { op_index: opIndex, block: blockId });
|
|
241
|
+
if (parsed.length > 1 && childIds !== undefined)
|
|
242
|
+
throw new MutationError("type_mismatch", "update with childIds must be a single block", { op_index: opIndex, block: blockId });
|
|
243
|
+
extra.push(...parsed.slice(1));
|
|
244
|
+
const nb = parsed[0];
|
|
245
|
+
found.block.raw = nb.raw;
|
|
246
|
+
found.block.type = nb.type;
|
|
247
|
+
found.block.attrs = nb.attrs;
|
|
248
|
+
found.block.children = nb.children;
|
|
249
|
+
// Thread known child identities onto the re-parsed subtree, positionally
|
|
250
|
+
// (mirroring flatten()/known-ids keys, relative to this block's children).
|
|
251
|
+
// A whole-container update (e.g. a list) re-parses its children, minting
|
|
252
|
+
// fresh ids by default; supplying childIds lets the whole-document update
|
|
253
|
+
// planner preserve reconcile-carried identity for the container's items
|
|
254
|
+
// (nested identity) — the commit re-parse threads exactly these ids.
|
|
255
|
+
if (childIds !== undefined && found.block.children.length > 0) {
|
|
256
|
+
assignChildIds(found.block.children, childIds, "");
|
|
257
|
+
}
|
|
258
|
+
// Render the block VERBATIM from its op-supplied raw (the exact target
|
|
259
|
+
// bytes) rather than rebuilding from children — the splice renderer's
|
|
260
|
+
// child rebuild can't reproduce nested/loose list formatting, blockquote
|
|
261
|
+
// markers, or a table's delimiter row. The re-parsed children remain
|
|
262
|
+
// (clean) only to carry ids for the commit re-parse. The block's OWN raw
|
|
263
|
+
// is fresh, so it is not dirty; what is stale is the raw of the container
|
|
264
|
+
// it lives in (a nested update), which is invalidated instead.
|
|
265
|
+
markSubtreeClean(found.block);
|
|
266
|
+
markContainerDirty(doc, found.siblings);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Non-markdown: raw swap preserves the block's type and attrs. Fragments
|
|
270
|
+
// like JSON properties aren't valid standalone documents, so re-parsing
|
|
271
|
+
// would corrupt the block kind (e.g. json:property → json:scalar).
|
|
272
|
+
found.block.raw = markdown;
|
|
273
|
+
found.block.children = [];
|
|
274
|
+
found.block.dirty = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (attrs) {
|
|
278
|
+
found.block.attrs = { ...found.block.attrs, ...attrs };
|
|
279
|
+
// attr-only edits (e.g. task checkbox) re-render the marker: mark dirty and
|
|
280
|
+
// rewrite the raw checkbox if present.
|
|
281
|
+
if ("checked" in attrs && (found.block.type === "task" || found.block.type === "list_item")) {
|
|
282
|
+
found.block.raw = found.block.raw.replace(/\[[ xX]\]/, attrs.checked ? "[x]" : "[ ]");
|
|
283
|
+
found.block.type = "task";
|
|
284
|
+
found.block.dirty = true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Trailing trivia (positional tiling): set verbatim. No content change, no
|
|
288
|
+
// dirty flag — renderDoc emits a top-level block's trivia unconditionally.
|
|
289
|
+
if (trivia !== undefined)
|
|
290
|
+
found.block.trivia = trivia;
|
|
291
|
+
if (extra.length > 0) {
|
|
292
|
+
// Splice the surplus blocks in after the target. The target's trailing
|
|
293
|
+
// trivia is the document's tiling AFTER the whole run, so it moves to the
|
|
294
|
+
// last new block; every seam inside the run must separate blocks (top level
|
|
295
|
+
// only — nested containers re-render from their children).
|
|
296
|
+
const tail = found.block.trivia;
|
|
297
|
+
if (found.siblings === doc.children) {
|
|
298
|
+
const sep = defaultTrivia(doc.format);
|
|
299
|
+
if (!separatesBlocks(found.block.trivia, doc.format))
|
|
300
|
+
found.block.trivia = sep;
|
|
301
|
+
for (const b of extra.slice(0, -1))
|
|
302
|
+
if (!separatesBlocks(b.trivia, doc.format))
|
|
303
|
+
b.trivia = sep;
|
|
304
|
+
}
|
|
305
|
+
extra[extra.length - 1].trivia = tail;
|
|
306
|
+
found.siblings.splice(found.index + 1, 0, ...extra);
|
|
307
|
+
markContainerDirty(doc, found.siblings);
|
|
308
|
+
}
|
|
309
|
+
return { ids: [blockId, ...extra.map((b) => b.id)] };
|
|
310
|
+
}
|
|
311
|
+
export function opMove(doc, blockIds, to, opIndex) {
|
|
312
|
+
if (blockIds.length === 0)
|
|
313
|
+
throw new MutationError("not_contiguous", "move requires ≥1 block");
|
|
314
|
+
// Locate all; assert contiguous siblings.
|
|
315
|
+
const located = blockIds.map((id) => {
|
|
316
|
+
const f = locate(doc, id);
|
|
317
|
+
if (!f)
|
|
318
|
+
throw new MutationError("block_missing", `block ${id} not found`, { op_index: opIndex, block: id });
|
|
319
|
+
return f;
|
|
320
|
+
});
|
|
321
|
+
const siblings = located[0].siblings;
|
|
322
|
+
const indices = located.map((l) => l.index).sort((a, b) => a - b);
|
|
323
|
+
for (let i = 1; i < indices.length; i++) {
|
|
324
|
+
if (located[i].siblings !== siblings || indices[i] !== indices[i - 1] + 1) {
|
|
325
|
+
throw new MutationError("not_contiguous", "move blocks must be a contiguous sibling run", { op_index: opIndex });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// cycle_move: target parent must not be inside the moved subtree.
|
|
329
|
+
const movedIds = new Set();
|
|
330
|
+
const collect = (b) => { movedIds.add(b.id); b.children.forEach(collect); };
|
|
331
|
+
located.forEach((l) => collect(l.block));
|
|
332
|
+
if (typeof to.parent === "string" && movedIds.has(to.parent)) {
|
|
333
|
+
throw new MutationError("cycle_move", "target is inside the moved subtree", { op_index: opIndex });
|
|
334
|
+
}
|
|
335
|
+
// Extract (preserve order) then re-insert at the target.
|
|
336
|
+
const moving = located.map((l) => l.block);
|
|
337
|
+
for (const m of moving) {
|
|
338
|
+
const idx = siblings.indexOf(m);
|
|
339
|
+
if (idx >= 0)
|
|
340
|
+
siblings.splice(idx, 1);
|
|
341
|
+
}
|
|
342
|
+
const { siblings: dstSiblings, index } = resolveTarget(doc, to);
|
|
343
|
+
dstSiblings.splice(index, 0, ...moving);
|
|
344
|
+
// A move within/into a nested container invalidates that container's cached
|
|
345
|
+
// raw (and the source's) — mark them so they re-render from children. No-op
|
|
346
|
+
// for the top-level list, which renders directly and is healed below.
|
|
347
|
+
markContainerDirty(doc, siblings);
|
|
348
|
+
markContainerDirty(doc, dstSiblings);
|
|
349
|
+
// Heal top-level seams so relocated blocks never render jammed together.
|
|
350
|
+
// Trivia is positional tiling (03 §2.2), not owned by the block: a block that
|
|
351
|
+
// was last in the source list carried a lone "\n" that, once interior, would
|
|
352
|
+
// soft-merge with its new neighbour (two paragraphs → one). Both the source
|
|
353
|
+
// gap and the destination seam are re-tiled. Only the top-level list renders
|
|
354
|
+
// from trivia (nested containers re-emit from children), so confine to it.
|
|
355
|
+
if (siblings === doc.children)
|
|
356
|
+
healTopLevelSeams(doc.children, doc.format);
|
|
357
|
+
if (dstSiblings === doc.children && dstSiblings !== siblings)
|
|
358
|
+
healTopLevelSeams(doc.children, doc.format);
|
|
359
|
+
return { ids: blockIds };
|
|
360
|
+
}
|
|
361
|
+
// Ensure every non-last top-level block ends at a real block boundary. Leaves
|
|
362
|
+
// already-separating trivia untouched (idempotent, byte-preserving where the
|
|
363
|
+
// document is already well-formed) and never touches the last block's trailing
|
|
364
|
+
// trivia (that is the document's trailing bytes). Exact target trivia is the
|
|
365
|
+
// planner's job via `update { trivia }`; this only guarantees well-formedness.
|
|
366
|
+
function healTopLevelSeams(children, format) {
|
|
367
|
+
const sep = defaultTrivia(format);
|
|
368
|
+
for (let i = 0; i < children.length - 1; i++) {
|
|
369
|
+
const b = children[i];
|
|
370
|
+
if (!separatesBlocks(b.trivia, format))
|
|
371
|
+
b.trivia = sep;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
export function opRemove(doc, blockIds, opIndex, expectPer) {
|
|
375
|
+
// Pass 1 — validate while every block is still in place: a genuinely unknown
|
|
376
|
+
// id is block_missing, and each CAS expectation is checked against the block's
|
|
377
|
+
// current bytes. Then collapse the set to its TOP-MOST members: a block whose
|
|
378
|
+
// ancestor is also being removed goes with that ancestor's subtree. A caller
|
|
379
|
+
// holding a flat id list (docs_read include_ids) can hand over a container
|
|
380
|
+
// and its children together without the child's lookup failing after the
|
|
381
|
+
// container has already taken it. Duplicates collapse too.
|
|
382
|
+
const set = new Set(blockIds);
|
|
383
|
+
const tops = [];
|
|
384
|
+
const seen = new Set();
|
|
385
|
+
for (const id of blockIds) {
|
|
386
|
+
if (seen.has(id))
|
|
387
|
+
continue;
|
|
388
|
+
seen.add(id);
|
|
389
|
+
const found = locate(doc, id);
|
|
390
|
+
if (!found) {
|
|
391
|
+
throw new MutationError("block_missing", `block ${id} not found in this document`, {
|
|
392
|
+
op_index: opIndex,
|
|
393
|
+
block: id,
|
|
394
|
+
hint: "the id is not a live block of the targeted document — it may have been removed by an earlier op in this changeset (removing a block removes its whole subtree), or never existed",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (expectPer?.[id])
|
|
398
|
+
checkContentHash(found.block, expectPer[id], opIndex);
|
|
399
|
+
if (!hasAncestorIn(doc, found.siblings, set))
|
|
400
|
+
tops.push(id);
|
|
401
|
+
}
|
|
402
|
+
// Pass 2 — remove the top-most blocks; `removed` reports every block that
|
|
403
|
+
// actually left the tree (each subtree walked), so the caller sees the
|
|
404
|
+
// collapsed descendants too.
|
|
405
|
+
const removed = [];
|
|
406
|
+
for (const id of tops) {
|
|
407
|
+
const found = locate(doc, id);
|
|
408
|
+
const collect = (b) => { removed.push(b.id); b.children.forEach(collect); };
|
|
409
|
+
collect(found.block);
|
|
410
|
+
found.siblings.splice(found.index, 1);
|
|
411
|
+
// Removing a nested block invalidates its container's cached raw; a container
|
|
412
|
+
// emptied by the removal is itself removed (an empty list would render blank).
|
|
413
|
+
pruneOrDirty(doc, found.siblings);
|
|
414
|
+
}
|
|
415
|
+
return { ids: blockIds, removed };
|
|
416
|
+
}
|
|
417
|
+
// Does any block owning `siblings` (walking up to the document root) have an id
|
|
418
|
+
// in `set`? I.e. is a block found in `siblings` a descendant of a block in `set`.
|
|
419
|
+
function hasAncestorIn(doc, siblings, set) {
|
|
420
|
+
let cur = siblings;
|
|
421
|
+
for (;;) {
|
|
422
|
+
const owner = ownerOf(doc, cur);
|
|
423
|
+
if (!owner)
|
|
424
|
+
return false;
|
|
425
|
+
if (set.has(owner.id))
|
|
426
|
+
return true;
|
|
427
|
+
const f = locate(doc, owner.id);
|
|
428
|
+
if (!f)
|
|
429
|
+
return false;
|
|
430
|
+
cur = f.siblings;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// After removing from `siblings`: if that list is now empty, remove the empty
|
|
434
|
+
// container from its own parent (recursing); otherwise mark it dirty so it
|
|
435
|
+
// re-renders from its remaining children.
|
|
436
|
+
function pruneOrDirty(doc, siblings) {
|
|
437
|
+
const owner = ownerOf(doc, siblings);
|
|
438
|
+
if (!owner)
|
|
439
|
+
return; // top-level list: renderDoc renders it directly
|
|
440
|
+
if (owner.children.length === 0) {
|
|
441
|
+
const f = locate(doc, owner.id);
|
|
442
|
+
if (f) {
|
|
443
|
+
f.siblings.splice(f.index, 1);
|
|
444
|
+
pruneOrDirty(doc, f.siblings);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
owner.dirty = true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
export function opSplit(doc, blockId, at, opIndex, expect) {
|
|
452
|
+
const found = locate(doc, blockId);
|
|
453
|
+
if (!found)
|
|
454
|
+
throw new MutationError("block_missing", `block ${blockId} not found`, { op_index: opIndex, block: blockId });
|
|
455
|
+
checkContentHash(found.block, expect, opIndex);
|
|
456
|
+
const raw = found.block.raw;
|
|
457
|
+
const cuts = [0, ...at, raw.length].sort((a, b) => a - b);
|
|
458
|
+
const pieces = [];
|
|
459
|
+
for (let i = 1; i < cuts.length; i++)
|
|
460
|
+
pieces.push(raw.slice(cuts[i - 1], cuts[i]));
|
|
461
|
+
const nonEmpty = pieces.filter((p) => p.trim().length > 0);
|
|
462
|
+
if (nonEmpty.length < 2)
|
|
463
|
+
throw new MutationError("type_mismatch", "split must yield ≥2 non-empty fragments");
|
|
464
|
+
// First fragment carries the id (authored intent, 04 §1); rest minted.
|
|
465
|
+
found.block.raw = nonEmpty[0];
|
|
466
|
+
found.block.dirty = true;
|
|
467
|
+
const newBlocks = nonEmpty.slice(1).map((p) => ({
|
|
468
|
+
id: mintId("b"), type: found.block.type, raw: p, trivia: "\n\n", attrs: { ...found.block.attrs }, children: [], dirty: true,
|
|
469
|
+
}));
|
|
470
|
+
found.siblings.splice(found.index + 1, 0, ...newBlocks);
|
|
471
|
+
return { ids: [blockId, ...newBlocks.map((b) => b.id)] };
|
|
472
|
+
}
|
|
473
|
+
export function opMerge(doc, blockIds, opIndex, separator = " ", expectPer) {
|
|
474
|
+
if (blockIds.length < 2)
|
|
475
|
+
throw new MutationError("not_contiguous", "merge requires ≥2 blocks");
|
|
476
|
+
const located = blockIds.map((id) => {
|
|
477
|
+
const f = locate(doc, id);
|
|
478
|
+
if (!f)
|
|
479
|
+
throw new MutationError("block_missing", `block ${id} not found`, { op_index: opIndex, block: id });
|
|
480
|
+
return f;
|
|
481
|
+
});
|
|
482
|
+
const siblings = located[0].siblings;
|
|
483
|
+
const type = located[0].block.type;
|
|
484
|
+
const indices = located.map((l) => l.index).sort((a, b) => a - b);
|
|
485
|
+
for (let i = 0; i < located.length; i++) {
|
|
486
|
+
if (located[i].siblings !== siblings)
|
|
487
|
+
throw new MutationError("not_contiguous", "merge blocks must share a parent", { op_index: opIndex });
|
|
488
|
+
if (located[i].block.type !== type)
|
|
489
|
+
throw new MutationError("type_mismatch", "merge blocks must share a type", { op_index: opIndex });
|
|
490
|
+
if (expectPer?.[blockIds[i]])
|
|
491
|
+
checkContentHash(located[i].block, expectPer[blockIds[i]], opIndex);
|
|
492
|
+
}
|
|
493
|
+
for (let i = 1; i < indices.length; i++)
|
|
494
|
+
if (indices[i] !== indices[i - 1] + 1)
|
|
495
|
+
throw new MutationError("not_contiguous", "merge blocks must be contiguous", { op_index: opIndex });
|
|
496
|
+
const ordered = indices.map((i) => siblings[i]);
|
|
497
|
+
const first = ordered[0];
|
|
498
|
+
first.raw = ordered.map((b) => b.raw).join(separator);
|
|
499
|
+
first.dirty = true;
|
|
500
|
+
const mergedInto = ordered.slice(1).map((b) => b.id);
|
|
501
|
+
// remove the others (highest index first)
|
|
502
|
+
for (let i = indices.length - 1; i >= 1; i--)
|
|
503
|
+
siblings.splice(indices[i], 1);
|
|
504
|
+
return { ids: [first.id], mergedInto };
|
|
505
|
+
}
|
|
506
|
+
export function checkParentChildrenHash(doc, parentId, expected, opIndex) {
|
|
507
|
+
const list = parentId ? locate(doc, parentId)?.block.children ?? doc.children : doc.children;
|
|
508
|
+
const current = parentChildrenHash(list);
|
|
509
|
+
if (current !== expected) {
|
|
510
|
+
throw new MutationError("stale_expectation", "parent_children_hash mismatch", { op_index: opIndex, current: { parent_children_hash: current }, retriable: true });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
//# sourceMappingURL=ops.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Op } from "./apply.js";
|
|
2
|
+
export type OpsetKind = "doc_update";
|
|
3
|
+
/**
|
|
4
|
+
* The identity consequence of one planned op, in the reconciler's vocabulary
|
|
5
|
+
* (03 §4). Kept as a string so mutate/ carries no runtime dependency on
|
|
6
|
+
* reconcile/; the values mirror reconcile/types.ts DispositionKind plus the
|
|
7
|
+
* synthetic `retiled` for a trailing-trivia-only fixup the planner emits to
|
|
8
|
+
* reproduce the proposed bytes exactly.
|
|
9
|
+
*/
|
|
10
|
+
export type PlanDisposition = "same" | "edited" | "moved" | "edited_moved" | "inserted" | "deleted" | "split_from" | "merged_into" | "copied_from" | "resurrected" | "bulk_rewrite" | "retiled";
|
|
11
|
+
/** State the opset was planned against; applying against different state fails. */
|
|
12
|
+
export interface OpsetPrecondition {
|
|
13
|
+
/** doc id the plan targets */
|
|
14
|
+
doc: string;
|
|
15
|
+
path: string;
|
|
16
|
+
/** the document's current revision id at plan time (null for a doc with no committed revision yet) */
|
|
17
|
+
baseRevision: string | null;
|
|
18
|
+
/** sha256 hex of the document's current rendered bytes (docs.file_hash) at plan time */
|
|
19
|
+
baseContentHash: string;
|
|
20
|
+
}
|
|
21
|
+
/** One kernel op plus the identity consequence the planner attributes to it. */
|
|
22
|
+
export interface PlanOp {
|
|
23
|
+
/** the executable kernel op (unchanged semantics) */
|
|
24
|
+
op: Op;
|
|
25
|
+
/** identity disposition this op realizes (planning metadata) */
|
|
26
|
+
disposition: PlanDisposition;
|
|
27
|
+
/**
|
|
28
|
+
* subject block ids for human/agent inspection: the carried id(s) an
|
|
29
|
+
* update/move/remove acts on, the survivor for merge, the origin for split.
|
|
30
|
+
* Empty for an insert of genuinely new structure (its id is minted at apply).
|
|
31
|
+
*/
|
|
32
|
+
blocks: string[];
|
|
33
|
+
confidence: number | null;
|
|
34
|
+
reason: string | null;
|
|
35
|
+
detail?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
/** Identity accounting over the reconciliation (for dry-run summaries). */
|
|
38
|
+
export interface OpsetSummary {
|
|
39
|
+
/** blocks carried with unchanged content and position */
|
|
40
|
+
preserved: number;
|
|
41
|
+
updated: number;
|
|
42
|
+
moved: number;
|
|
43
|
+
created: number;
|
|
44
|
+
removed: number;
|
|
45
|
+
split: number;
|
|
46
|
+
merged: number;
|
|
47
|
+
/** low-confidence carries / recorded near-misses the matcher flagged */
|
|
48
|
+
ambiguous: number;
|
|
49
|
+
}
|
|
50
|
+
export interface Opset {
|
|
51
|
+
version: 1;
|
|
52
|
+
kind: OpsetKind;
|
|
53
|
+
target: {
|
|
54
|
+
doc: string;
|
|
55
|
+
path: string;
|
|
56
|
+
};
|
|
57
|
+
precondition: OpsetPrecondition;
|
|
58
|
+
/** matcher version that produced the identity decisions (reproducibility) */
|
|
59
|
+
matcherV: string;
|
|
60
|
+
/** the planned ops, each annotated with its identity consequence */
|
|
61
|
+
ops: PlanOp[];
|
|
62
|
+
/**
|
|
63
|
+
* Document-level frontmatter change, when the proposed content changes it.
|
|
64
|
+
* `raw` is the new frontmatter incl. fences + trailing separator (null drops
|
|
65
|
+
* it). Frontmatter is a materialization unit, not a block op; apply sets it
|
|
66
|
+
* via ApplyRequest.setFrontmatter.
|
|
67
|
+
*/
|
|
68
|
+
frontmatter?: {
|
|
69
|
+
raw: string | null;
|
|
70
|
+
};
|
|
71
|
+
summary: OpsetSummary;
|
|
72
|
+
/**
|
|
73
|
+
* Verified by the planner: replaying `ops` from the base state renders to
|
|
74
|
+
* exactly the proposed content. When false, `apply(opset)` refuses (a plan
|
|
75
|
+
* that cannot reproduce the intended bytes is never applied silently); the
|
|
76
|
+
* caller inspects `diagnostics` and re-plans or edits.
|
|
77
|
+
*/
|
|
78
|
+
converges: boolean;
|
|
79
|
+
diagnostics: string[];
|
|
80
|
+
}
|
|
81
|
+
/** The bare kernel ops, in order — what `apply` actually replays. */
|
|
82
|
+
export declare function opsetKernelOps(opset: Opset): Op[];
|
|
83
|
+
/** Compute the summary from the planned ops + carried/ambiguous counts. */
|
|
84
|
+
export declare function summarizeOps(ops: PlanOp[], counts: {
|
|
85
|
+
preserved: number;
|
|
86
|
+
ambiguous: number;
|
|
87
|
+
}): OpsetSummary;
|
|
88
|
+
/** Serialize an opset to a stable JSON string (ops are plain JSON). */
|
|
89
|
+
export declare function serializeOpset(opset: Opset): string;
|
|
90
|
+
/** Parse a serialized opset, validating the version tag. */
|
|
91
|
+
export declare function parseOpset(text: string): Opset;
|
|
92
|
+
export declare function renderOpsetPlan(opset: Opset): string;
|