@omgbase/core 0.3.0 → 0.4.1
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/dist/src/core/ids.d.ts +34 -3
- package/dist/src/core/ids.js +79 -6
- package/dist/src/core/ingest.js +15 -12
- package/dist/src/core/read/document.js +6 -1
- package/dist/src/core/read/nodes.js +5 -1
- package/dist/src/core/store/fts.d.ts +47 -7
- package/dist/src/core/store/fts.js +73 -9
- package/dist/src/core/store/rebuild.js +5 -4
- package/dist/src/core/store/store.d.ts +4 -0
- package/dist/src/core/store/store.js +40 -1
- package/dist/src/format/index.d.ts +12 -1
- package/dist/src/format/index.js +14 -1
- package/dist/src/format/markdown.d.ts +7 -0
- package/dist/src/format/markdown.js +7 -0
- package/dist/src/graph/history.d.ts +21 -0
- package/dist/src/graph/history.js +48 -6
- package/dist/src/graph/links.d.ts +2 -0
- package/dist/src/graph/links.js +22 -11
- package/dist/src/index.d.ts +8 -6
- package/dist/src/index.js +7 -5
- package/dist/src/mcp/errors.d.ts +8 -0
- package/dist/src/mcp/errors.js +17 -3
- package/dist/src/mcp/server.js +50 -57
- package/dist/src/mutate/apply.js +12 -4
- package/dist/src/mutate/doc-store.js +4 -1
- package/dist/src/mutate/docs.d.ts +19 -0
- package/dist/src/mutate/docs.js +64 -16
- package/dist/src/mutate/ops.js +25 -13
- package/dist/src/oqx-js/context.d.ts +11 -0
- package/dist/src/oqx-js/context.js +16 -0
- package/dist/src/oqx-js/planner.d.ts +20 -1
- package/dist/src/oqx-js/planner.js +95 -2
- package/dist/src/oqx-js/run.js +27 -2
- package/dist/src/oqx-js/sql/translate.d.ts +94 -1
- package/dist/src/oqx-js/sql/translate.js +263 -79
- package/dist/src/search/tasks.d.ts +15 -1
- package/dist/src/search/tasks.js +31 -3
- package/dist/src/sync/admin.d.ts +13 -0
- package/dist/src/sync/admin.js +11 -0
- package/dist/src/sync/external-source.js +20 -7
- package/dist/src/sync/plugin.d.ts +30 -5
- package/dist/src/sync/plugin.js +13 -1
- package/dist/src/sync/watcher.d.ts +2 -0
- package/dist/src/sync/watcher.js +10 -2
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { findDocByRef } from "../core/read/reader.js";
|
|
2
2
|
import { isValidId } from "../core/ids.js";
|
|
3
|
+
import { EngineError } from "../mcp/errors.js";
|
|
3
4
|
/** history_node: a block's biography from block_changes + commits (06 §3). */
|
|
4
5
|
export function historyNode(store, blockId, opts = {}) {
|
|
5
6
|
const limit = opts.limit ?? 100;
|
|
@@ -78,6 +79,25 @@ export function diffUnified(store, docId, fromRev, toRev) {
|
|
|
78
79
|
const rendered = (rev) => [...blocksAtRevision(store, docId, rev).values()].join("\n");
|
|
79
80
|
return unifiedDiff(rendered(fromRev), rendered(toRev));
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* `diff_unified` over a resolved document: `to` defaults to the current revision,
|
|
84
|
+
* `from` to the one before it (or the current one again when there is only one:
|
|
85
|
+
* an empty diff). Shared by the MCP tool and `omg diff` so the shape cannot
|
|
86
|
+
* drift. `label` is how the caller named the document, for the error message.
|
|
87
|
+
* A document with no revision at all is `target_missing`; an unknown `rev` is
|
|
88
|
+
* `RevisionNotFound` (→ `target_missing` through `errorBody`).
|
|
89
|
+
*/
|
|
90
|
+
export function docDiffUnified(store, docId, opts = {}) {
|
|
91
|
+
const revs = store.db
|
|
92
|
+
.prepare("SELECT rev_id FROM revisions WHERE doc_id = ? ORDER BY seq DESC LIMIT 2")
|
|
93
|
+
.all(docId);
|
|
94
|
+
const to = opts.toRev ?? revs[0]?.rev_id;
|
|
95
|
+
const from = opts.fromRev ?? revs[1]?.rev_id ?? revs[0]?.rev_id;
|
|
96
|
+
if (!to || !from)
|
|
97
|
+
throw new EngineError("target_missing", `no revisions to diff for ${opts.label ?? docId}`);
|
|
98
|
+
const path = store.db.prepare("SELECT path FROM docs WHERE doc_id = ?").get(docId)?.path ?? "";
|
|
99
|
+
return { doc: docId, path, from, to, diff: diffUnified(store, docId, from, to) };
|
|
100
|
+
}
|
|
81
101
|
// ---- unified diff (spec/surface §3) ------------------------------------------
|
|
82
102
|
// A line-grain unified diff with a deterministic Myers script, so both engines
|
|
83
103
|
// (this reference and crates/omgbase-surface/src/history.rs) produce the same
|
|
@@ -232,14 +252,19 @@ export function changesSince(store, repoId, opts = {}) {
|
|
|
232
252
|
params.push(opts.origin);
|
|
233
253
|
params.push(limit + 1);
|
|
234
254
|
const commits = store.db
|
|
235
|
-
.prepare(`SELECT commit_id, seq, ts, origin, actor FROM commits WHERE repo_id = ? AND seq > ? ${originClause} ORDER BY seq LIMIT ?`)
|
|
255
|
+
.prepare(`SELECT commit_id, seq, ts, origin, actor, reason FROM commits WHERE repo_id = ? AND seq > ? ${originClause} ORDER BY seq LIMIT ?`)
|
|
236
256
|
.all(...params);
|
|
237
257
|
const truncated = commits.length > limit;
|
|
238
258
|
const page = commits.slice(0, limit);
|
|
239
259
|
const digests = page.map((c) => {
|
|
240
260
|
const revs = store.db.prepare("SELECT r.doc_id AS doc, r.path AS path, r.rendered_hash AS rendered_hash FROM revisions r WHERE r.commit_id = ?").all(c.commit_id).map((r) => ({ doc: r.doc, path: r.path, contentHash: r.rendered_hash.toString("hex") }));
|
|
241
261
|
const dispCounts = store.db.prepare("SELECT kind, count(*) n FROM dispositions WHERE commit_id = ? GROUP BY kind").all(c.commit_id);
|
|
242
|
-
|
|
262
|
+
// A deletion or a move writes no revision: the tombstone names the commit
|
|
263
|
+
// (`docs.deleted_commit`), a move only its reason (`move <from> -> <to>`).
|
|
264
|
+
const deleted = revs.length === 0
|
|
265
|
+
? store.db.prepare("SELECT path FROM docs WHERE deleted_commit = ? ORDER BY path").all(c.commit_id).map((r) => r.path)
|
|
266
|
+
: [];
|
|
267
|
+
const summary = renderSummary(c.origin, c.actor, revs, dispCounts, { deleted, moved: revs.length === 0 ? parseMove(c.reason) : null });
|
|
243
268
|
return { commit: c.commit_id, seq: c.seq, ts: c.ts, origin: c.origin, actor: c.actor, summary, revisions: revs };
|
|
244
269
|
});
|
|
245
270
|
const nextCursor = page.length > 0 ? page[page.length - 1].seq : cursor;
|
|
@@ -333,12 +358,29 @@ function resolveDocRow(store, repoId, ref, includeDeleted) {
|
|
|
333
358
|
: store.db.prepare("SELECT doc_id, path, current_rev, deleted_commit FROM docs WHERE repo_id = ? AND path = ?");
|
|
334
359
|
return (asId ? stmt.get(ref) : stmt.get(repoId, ref));
|
|
335
360
|
}
|
|
336
|
-
|
|
337
|
-
|
|
361
|
+
/** The `reason` a document move records (`mutate/docs.ts`): `move <from> -> <to>`. */
|
|
362
|
+
function parseMove(reason) {
|
|
363
|
+
const m = reason === null ? null : /^move (.+) -> (.+)$/.exec(reason);
|
|
364
|
+
return m ? { from: m[1], to: m[2] } : null;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* One line per commit: `<origin>[(<actor>)]: <paths> — <n> <kind>, …`. A
|
|
368
|
+
* commit that touched no revision — a document deletion (observed or api) or a
|
|
369
|
+
* move — says so instead: `deleted <path>` / `moved <from> → <to>`
|
|
370
|
+
* (spec/cli §6 `log`; the summary is unpinned by spec/surface).
|
|
371
|
+
*/
|
|
372
|
+
function renderSummary(origin, actor, revs, disp, extra = { deleted: [], moved: null }) {
|
|
373
|
+
let subject = revs.map((r) => r.path).join(", ");
|
|
374
|
+
if (revs.length === 0) {
|
|
375
|
+
if (extra.deleted.length > 0)
|
|
376
|
+
subject = `deleted ${extra.deleted.join(", ")}`;
|
|
377
|
+
else if (extra.moved)
|
|
378
|
+
subject = `moved ${extra.moved.from} → ${extra.moved.to}`;
|
|
379
|
+
}
|
|
338
380
|
const parts = disp.filter((d) => d.kind !== "same").map((d) => `${d.n} ${d.kind}`);
|
|
339
381
|
const detail = parts.length > 0 ? ` — ${parts.join(", ")}` : "";
|
|
340
382
|
if (origin === "api" || origin === "import")
|
|
341
|
-
return `${origin}(${actor ?? "?"}): ${
|
|
342
|
-
return `observed: ${
|
|
383
|
+
return `${origin}(${actor ?? "?"}): ${subject}${detail}`;
|
|
384
|
+
return `observed: ${subject}${detail}`;
|
|
343
385
|
}
|
|
344
386
|
//# sourceMappingURL=history.js.map
|
|
@@ -8,6 +8,8 @@ export interface LinkEdge {
|
|
|
8
8
|
count: number;
|
|
9
9
|
/** sample source block ids (out-edges only) */
|
|
10
10
|
samples?: string[];
|
|
11
|
+
/** block-grain (`blocks: true`): the source block of this edge (null for a frontmatter edge) */
|
|
12
|
+
block?: string | null;
|
|
11
13
|
}
|
|
12
14
|
export interface LinksResult {
|
|
13
15
|
out: LinkEdge[];
|
package/dist/src/graph/links.js
CHANGED
|
@@ -8,12 +8,12 @@ export function docLinks(store, docId, opts = {}) {
|
|
|
8
8
|
if (dir === "out" || dir === "both") {
|
|
9
9
|
if (opts.blocks) {
|
|
10
10
|
const rows = store.db
|
|
11
|
-
.prepare(`SELECT predicate, dst_node, dst_kind FROM edges
|
|
12
|
-
WHERE src_doc = ? AND to_commit IS NULL ORDER BY predicate, dst_node`)
|
|
11
|
+
.prepare(`SELECT predicate, dst_node, dst_kind, src_block FROM edges
|
|
12
|
+
WHERE src_doc = ? AND to_commit IS NULL ORDER BY predicate, dst_node, src_block`)
|
|
13
13
|
.all(docId);
|
|
14
14
|
for (const r of rows)
|
|
15
15
|
if (inPred(r.predicate))
|
|
16
|
-
out.push({ predicate: r.predicate, node: r.dst_node, kind: r.dst_kind, count: 1 });
|
|
16
|
+
out.push({ predicate: r.predicate, node: r.dst_node, kind: r.dst_kind, count: 1, block: r.src_block });
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
19
|
const rows = store.db
|
|
@@ -28,14 +28,25 @@ export function docLinks(store, docId, opts = {}) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
if (dir === "in" || dir === "both") {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
if (opts.blocks) {
|
|
32
|
+
const rows = store.db
|
|
33
|
+
.prepare(`SELECT predicate, src_doc, src_block FROM edges
|
|
34
|
+
WHERE dst_node = ? AND to_commit IS NULL ORDER BY predicate, src_doc, src_block`)
|
|
35
|
+
.all(docId);
|
|
36
|
+
for (const r of rows)
|
|
37
|
+
if (inPred(r.predicate))
|
|
38
|
+
inbound.push({ predicate: r.predicate, node: r.src_doc, kind: "document", count: 1, block: r.src_block });
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const rows = store.db
|
|
42
|
+
.prepare(`SELECT predicate, src_doc, count(*) AS cnt FROM edges
|
|
43
|
+
WHERE dst_node = ? AND to_commit IS NULL
|
|
44
|
+
GROUP BY predicate, src_doc ORDER BY predicate, src_doc`)
|
|
45
|
+
.all(docId);
|
|
46
|
+
for (const r of rows)
|
|
47
|
+
if (inPred(r.predicate))
|
|
48
|
+
inbound.push({ predicate: r.predicate, node: r.src_doc, kind: "document", count: r.cnt });
|
|
49
|
+
}
|
|
39
50
|
}
|
|
40
51
|
return { out, in: inbound };
|
|
41
52
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export { ingestDirectory, type IngestDirResult } from "./sync/attach.js";
|
|
|
19
19
|
export { ensureRepo } from "./core/attach.js";
|
|
20
20
|
export { walkMarkdown, walkMarkdownAsync, nodeFs, type SyncFs, type FileStat } from "./sync/fs-util.js";
|
|
21
21
|
export { Watcher, type WatcherOptions } from "./sync/watcher.js";
|
|
22
|
-
export type { SyncSource, SourceCapabilities, SourceIdentity, SourceEntry, SourceItem, SourceWatch, WatchListener, } from "./sync/plugin.js";
|
|
22
|
+
export type { SyncSource, SourceCapabilities, SourceIdentity, SourceEntry, SourceItem, SourceWatch, WatchEvent, WatchListener, } from "./sync/plugin.js";
|
|
23
|
+
export { awaitReady, WATCH_READY_PATIENCE_MS } from "./sync/plugin.js";
|
|
23
24
|
export { createExternalSource, type ExternalSourceSpec } from "./sync/external-source.js";
|
|
24
25
|
export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, type AdapterRow, type SourceRow, } from "./sync/sources.js";
|
|
25
26
|
export { reconcileChanges, attachSource } from "./sync/driver.js";
|
|
@@ -27,7 +28,7 @@ export { observeFile, observeMany, observeOne, observeDelete, observeBatch, type
|
|
|
27
28
|
export { buildServer, type ServerContext } from "./mcp/server.js";
|
|
28
29
|
export { serveStdio, type ServeStdioHandle } from "./mcp/stdio.js";
|
|
29
30
|
export { processCheckpoint, type CheckpointResult } from "./sync/checkpoint.js";
|
|
30
|
-
export { reposStatus, syncStatus, type RepoStatus, type SyncStatus, type DiskStatus } from "./sync/admin.js";
|
|
31
|
+
export { reposStatus, syncStatus, reposList, type RepoStatus, type SyncStatus, type DiskStatus, type ReposList } from "./sync/admin.js";
|
|
31
32
|
export { docsOutline, type OutlineResult, type OutlineOptions } from "./core/read/outline.js";
|
|
32
33
|
export { docsRead, reconstructContent, type DocsReadResult, type DocsReadOptions } from "./core/read/document.js";
|
|
33
34
|
export { nodesGet, nodesGetMany, type GetNode, type Resolution } from "./core/read/nodes.js";
|
|
@@ -37,18 +38,18 @@ export { resolveRef, type ResolvedRef } from "./core/read/refs.js";
|
|
|
37
38
|
export { resolve, type ResolveHit, type ResolveInput } from "./search/resolve.js";
|
|
38
39
|
export { oqxRun, oqxRunAsync, collectSemanticPhrases, parseOqx, type OqxResult, type OqxHit, type OqxOptions, type EmbedQuery } from "./oqx/run.js";
|
|
39
40
|
export { EmbeddingWorker, SemanticUnavailable, contextPrefix, shouldEmbed, estimateTokens, type EmbeddingProvider, type EmbedTask, type DocEmbedTask, type DocEmbedBlockRef, type DocEmbedMethod, type DocVectorRow, } from "./search/embeddings.js";
|
|
40
|
-
export { buildEmbedTasks, buildDocEmbedTasks } from "./search/tasks.js";
|
|
41
|
+
export { buildEmbedTasks, buildDocEmbedTasks, staleEmbedCount } from "./search/tasks.js";
|
|
41
42
|
export { EmbedDrainer, type DrainerOptions } from "./search/drain.js";
|
|
42
43
|
export { embeddingSettings, type EmbeddingSettings } from "./search/provider.js";
|
|
43
44
|
export { createExternalProvider, type ExternalProvider } from "./search/external.js";
|
|
44
45
|
export { hybridSearch, type HybridHit, type HybridInput } from "./search/rrf.js";
|
|
45
46
|
export { vectorSearch, docVectorSearch, type VectorHit, type DocVectorHit } from "./search/vector.js";
|
|
46
|
-
export { historyNode, diffUnified, unifiedDiff, changesSince, type NodeChange, type CommitDigest } from "./graph/history.js";
|
|
47
|
+
export { historyNode, diffUnified, docDiffUnified, diffBlocks, unifiedDiff, changesSince, RevisionNotFound, type NodeChange, type CommitDigest, type DocDiffUnified, type DiffEntry } from "./graph/history.js";
|
|
47
48
|
export { docLinks, type LinksResult, type LinkEdge, type LinksOptions } from "./graph/links.js";
|
|
48
49
|
export { FilterInvalid } from "./search/cel/parser.js";
|
|
49
|
-
export { EngineError, type ErrorCode } from "./mcp/errors.js";
|
|
50
|
+
export { EngineError, errorBody, type ErrorCode, type ErrorBody } from "./mcp/errors.js";
|
|
50
51
|
export { MutationError } from "./mutate/tree.js";
|
|
51
|
-
export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter, type IdMinter, type IdPrefix } from "./core/ids.js";
|
|
52
|
+
export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter, repeatingMinter, registerIdOracle, type IdMinter, type IdOracle, type IdPrefix } from "./core/ids.js";
|
|
52
53
|
export { pinClock, withClock } from "./core/clock.js";
|
|
53
54
|
export { sha256 } from "./core/hash.js";
|
|
54
55
|
export { ingestFile, type IngestResult } from "./core/ingest.js";
|
|
@@ -62,3 +63,4 @@ export { planUpdate, applyOpset, docsUpdate, type PlanUpdateOptions, type ApplyO
|
|
|
62
63
|
export { opsetKernelOps, renderOpsetPlan, serializeOpset, parseOpset, type Opset, type PlanOp, type OpsetSummary, type OpsetPrecondition, type PlanDisposition, } from "./mutate/opset.js";
|
|
63
64
|
export { rebuildIndex, type RebuildTarget } from "./core/store/rebuild.js";
|
|
64
65
|
export { runGc, type GcResult } from "./core/store/gc.js";
|
|
66
|
+
export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
|
package/dist/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export { ingestDirectory } from "./sync/attach.js";
|
|
|
20
20
|
export { ensureRepo } from "./core/attach.js";
|
|
21
21
|
export { walkMarkdown, walkMarkdownAsync, nodeFs } from "./sync/fs-util.js";
|
|
22
22
|
export { Watcher } from "./sync/watcher.js";
|
|
23
|
+
export { awaitReady, WATCH_READY_PATIENCE_MS } from "./sync/plugin.js";
|
|
23
24
|
export { createExternalSource } from "./sync/external-source.js";
|
|
24
25
|
export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, } from "./sync/sources.js";
|
|
25
26
|
export { reconcileChanges, attachSource } from "./sync/driver.js";
|
|
@@ -27,7 +28,7 @@ export { observeFile, observeMany, observeOne, observeDelete, observeBatch } fro
|
|
|
27
28
|
export { buildServer } from "./mcp/server.js";
|
|
28
29
|
export { serveStdio } from "./mcp/stdio.js";
|
|
29
30
|
export { processCheckpoint } from "./sync/checkpoint.js";
|
|
30
|
-
export { reposStatus, syncStatus } from "./sync/admin.js";
|
|
31
|
+
export { reposStatus, syncStatus, reposList } from "./sync/admin.js";
|
|
31
32
|
// Read surface consumed by CLI read commands (11 §5.2–5.5).
|
|
32
33
|
export { docsOutline } from "./core/read/outline.js";
|
|
33
34
|
export { docsRead, reconstructContent } from "./core/read/document.js";
|
|
@@ -42,18 +43,18 @@ export { oqxRun, oqxRunAsync, collectSemanticPhrases, parseOqx } from "./oqx/run
|
|
|
42
43
|
// Embeddings + semantic retrieval (05 §5–6). The provider is a plugin loaded by
|
|
43
44
|
// package name; core carries no ML dependency.
|
|
44
45
|
export { EmbeddingWorker, SemanticUnavailable, contextPrefix, shouldEmbed, estimateTokens, } from "./search/embeddings.js";
|
|
45
|
-
export { buildEmbedTasks, buildDocEmbedTasks } from "./search/tasks.js";
|
|
46
|
+
export { buildEmbedTasks, buildDocEmbedTasks, staleEmbedCount } from "./search/tasks.js";
|
|
46
47
|
export { EmbedDrainer } from "./search/drain.js";
|
|
47
48
|
export { embeddingSettings } from "./search/provider.js";
|
|
48
49
|
export { createExternalProvider } from "./search/external.js";
|
|
49
50
|
export { hybridSearch } from "./search/rrf.js";
|
|
50
51
|
export { vectorSearch, docVectorSearch } from "./search/vector.js";
|
|
51
|
-
export { historyNode, diffUnified, unifiedDiff, changesSince } from "./graph/history.js";
|
|
52
|
+
export { historyNode, diffUnified, docDiffUnified, diffBlocks, unifiedDiff, changesSince, RevisionNotFound } from "./graph/history.js";
|
|
52
53
|
export { docLinks } from "./graph/links.js";
|
|
53
54
|
export { FilterInvalid } from "./search/cel/parser.js";
|
|
54
|
-
export { EngineError } from "./mcp/errors.js";
|
|
55
|
+
export { EngineError, errorBody } from "./mcp/errors.js";
|
|
55
56
|
export { MutationError } from "./mutate/tree.js";
|
|
56
|
-
export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter } from "./core/ids.js";
|
|
57
|
+
export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter, repeatingMinter, registerIdOracle } from "./core/ids.js";
|
|
57
58
|
// The clock seam (spec/surface §7.1): `omg mcp` pins the process clock under OMGBASE_SPEC_CLOCK.
|
|
58
59
|
export { pinClock, withClock } from "./core/clock.js";
|
|
59
60
|
export { sha256 } from "./core/hash.js";
|
|
@@ -72,4 +73,5 @@ export { planUpdate, applyOpset, docsUpdate, } from "./mutate/plan-update.js";
|
|
|
72
73
|
export { opsetKernelOps, renderOpsetPlan, serializeOpset, parseOpset, } from "./mutate/opset.js";
|
|
73
74
|
export { rebuildIndex } from "./core/store/rebuild.js";
|
|
74
75
|
export { runGc } from "./core/store/gc.js";
|
|
76
|
+
export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
|
|
75
77
|
//# sourceMappingURL=index.js.map
|
package/dist/src/mcp/errors.d.ts
CHANGED
|
@@ -15,3 +15,11 @@ export declare class EngineError extends Error {
|
|
|
15
15
|
});
|
|
16
16
|
body(): EngineErrorBody;
|
|
17
17
|
}
|
|
18
|
+
/** The rendered body of any thrown error (`error` is the code). */
|
|
19
|
+
export interface ErrorBody {
|
|
20
|
+
error: string;
|
|
21
|
+
message: string;
|
|
22
|
+
data?: unknown;
|
|
23
|
+
retriable: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function errorBody(err: unknown): ErrorBody;
|
package/dist/src/mcp/errors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { FilterInvalid } from "../search/cel/parser.js";
|
|
2
|
+
import { CursorInvalid } from "../core/cursor.js";
|
|
3
|
+
import { MutationError } from "../mutate/tree.js";
|
|
4
|
+
import { RevisionNotFound } from "../graph/history.js";
|
|
4
5
|
export class EngineError extends Error {
|
|
5
6
|
code;
|
|
6
7
|
data;
|
|
@@ -15,4 +16,17 @@ export class EngineError extends Error {
|
|
|
15
16
|
return { error: this.code, message: this.message, data: this.data, retriable: this.retriable };
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
export function errorBody(err) {
|
|
20
|
+
if (err instanceof EngineError)
|
|
21
|
+
return { error: err.code, message: err.message, ...(err.data !== undefined ? { data: err.data } : {}), retriable: err.retriable };
|
|
22
|
+
if (err instanceof FilterInvalid)
|
|
23
|
+
return { error: "filter_invalid", message: err.message, data: { reason: err.reason, hint: err.hint }, retriable: false };
|
|
24
|
+
if (err instanceof CursorInvalid)
|
|
25
|
+
return { error: "filter_invalid", message: err.message, data: { reason: `cursor was not issued by ${err.surface}`, hint: "resume only with a `cursor` returned by a truncated page of the same tool" }, retriable: false };
|
|
26
|
+
if (err instanceof MutationError)
|
|
27
|
+
return { error: err.code, message: err.message, data: err.data, retriable: Boolean(err.data.retriable) };
|
|
28
|
+
if (err instanceof RevisionNotFound)
|
|
29
|
+
return { error: "target_missing", message: err.message, data: { doc: err.docId, rev: err.rev }, retriable: false };
|
|
30
|
+
return { error: "repo_not_found", message: String(err), retriable: false };
|
|
31
|
+
}
|
|
18
32
|
//# sourceMappingURL=errors.js.map
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -4,7 +4,6 @@ import { docsOutline } from "../core/read/outline.js";
|
|
|
4
4
|
import { docsRead, docsReadMany, MANY_DOCS_CAP, readDocumentAtRevision } from "../core/read/document.js";
|
|
5
5
|
import { nodesGet, nodesGetMany } from "../core/read/nodes.js";
|
|
6
6
|
import { findDoc, findDocByRef, docsList, docsTree, DOCS_LIST_DEFAULT_LIMIT, DOCS_TREE_DEFAULT_LIMIT } from "../core/read/reader.js";
|
|
7
|
-
import { CursorInvalid } from "../core/cursor.js";
|
|
8
7
|
import { resolveRef } from "../core/read/refs.js";
|
|
9
8
|
import { isValidId } from "../core/ids.js";
|
|
10
9
|
import { VERSION } from "../core/index.js";
|
|
@@ -12,18 +11,16 @@ import { normalizeText, normalizeVisibleText } from "../core/hash.js";
|
|
|
12
11
|
import { oqxRunAsync, collectSemanticPhrases } from "../oqx/run.js";
|
|
13
12
|
import { graphNeighborhood } from "./graph.js";
|
|
14
13
|
import { textSearch } from "../search/text.js";
|
|
15
|
-
import {
|
|
16
|
-
import { EngineError } from "./errors.js";
|
|
14
|
+
import { EngineError, errorBody } from "./errors.js";
|
|
17
15
|
import { apply } from "../mutate/apply.js";
|
|
18
|
-
import { MutationError } from "../mutate/tree.js";
|
|
19
16
|
import { tasksComplete, sectionsAppend, docsAppend, linksRetarget, linksRepair, nodeSet } from "../mutate/macros.js";
|
|
20
17
|
import { docsCreate, docsMove, docsDelete, docsSetMeta } from "../mutate/docs.js";
|
|
21
18
|
import { planUpdate, docsUpdate } from "../mutate/plan-update.js";
|
|
22
19
|
import { renderOpsetPlan } from "../mutate/opset.js";
|
|
23
|
-
import { historyNode, diffBlocks,
|
|
20
|
+
import { historyNode, diffBlocks, docDiffUnified, changesSince, docHistory } from "../graph/history.js";
|
|
24
21
|
import { linksStale, linksStaleSummary } from "../graph/link-health.js";
|
|
25
22
|
import { resolve as resolveThing } from "../search/resolve.js";
|
|
26
|
-
import { reposStatus, syncStatus } from "../sync/admin.js";
|
|
23
|
+
import { reposStatus, syncStatus, reposList } from "../sync/admin.js";
|
|
27
24
|
import { observeFile, observeMany, observeDelete } from "../sync/observe.js";
|
|
28
25
|
import { QUERY_SYNTAX } from "./reference.js";
|
|
29
26
|
// Kernel-op schemas (04 §1) published as the `apply` tool's op grammar. Giving
|
|
@@ -45,6 +42,15 @@ const toSchema = z.object({
|
|
|
45
42
|
]),
|
|
46
43
|
at: atSchema,
|
|
47
44
|
});
|
|
45
|
+
// blocks_insert / blocks_move carry the op-level expectation of spec/mutate §1.2:
|
|
46
|
+
// only `parent_children_hash` (the destination parent's live direct child ids
|
|
47
|
+
// joined by "," and sha256-hexed) has a meaning there — there is no target block
|
|
48
|
+
// whose content_hash could be checked — so the schema names that one key alone
|
|
49
|
+
// (an accidental `content_hash` is dropped, not an error; the kernel would
|
|
50
|
+
// ignore it anyway).
|
|
51
|
+
const parentExpectSchema = z.object({
|
|
52
|
+
parent_children_hash: z.string().optional(),
|
|
53
|
+
});
|
|
48
54
|
const expectSchema = z.object({
|
|
49
55
|
content_hash: z.string().optional(),
|
|
50
56
|
parent_children_hash: z.string().optional(),
|
|
@@ -61,20 +67,9 @@ function ok(payload) {
|
|
|
61
67
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
62
68
|
}
|
|
63
69
|
function fail(err) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
else if (err instanceof FilterInvalid)
|
|
68
|
-
body = { error: "filter_invalid", message: err.message, data: { reason: err.reason, hint: err.hint }, retriable: false };
|
|
69
|
-
else if (err instanceof CursorInvalid)
|
|
70
|
-
body = { error: "filter_invalid", message: err.message, data: { reason: `cursor was not issued by ${err.surface}`, hint: "resume only with a `cursor` returned by a truncated page of the same tool" }, retriable: false };
|
|
71
|
-
else if (err instanceof MutationError)
|
|
72
|
-
body = { error: err.code, message: err.message, data: err.data, retriable: Boolean(err.data.retriable) };
|
|
73
|
-
else if (err instanceof RevisionNotFound)
|
|
74
|
-
body = { error: "target_missing", message: err.message, data: { doc: err.docId, rev: err.rev }, retriable: false };
|
|
75
|
-
else
|
|
76
|
-
body = { error: "repo_not_found", message: String(err), retriable: false };
|
|
77
|
-
return { content: [{ type: "text", text: JSON.stringify(body) }], isError: true };
|
|
70
|
+
// One mapping for every client (errors.ts `errorBody`): the CLI renders the
|
|
71
|
+
// same body, so a code can never differ between `omg` and `omg mcp`.
|
|
72
|
+
return { content: [{ type: "text", text: JSON.stringify(errorBody(err)) }], isError: true };
|
|
78
73
|
}
|
|
79
74
|
export function buildServer(ctx) {
|
|
80
75
|
const server = new McpServer({ name: "omgbase", version: VERSION });
|
|
@@ -279,7 +274,7 @@ export function buildServer(ctx) {
|
|
|
279
274
|
}
|
|
280
275
|
});
|
|
281
276
|
server.registerTool("docs_get_many", {
|
|
282
|
-
description: "Batch whole-document read — the hydrate half of query→hydrate. The plural of docs_read: pass `docs`, a list of refs (each a doc id OR a path, same id-or-path symmetry docs_read accepts in its `doc` field), and get back one full read per ref. Returns `{ items, errors, truncated }`: each found doc is a full docs_read projection (`content` = complete file bytes verbatim, `properties` grouped by source, plus `path`/`docId`/`rev`, and — with include_ids:true — the document's ordered block `ids`, a `hashes` {block id → content hash} map for raw-`apply` CAS pinning, and a `parents` {block id → parent block id | null} map so top-level vs nested blocks are distinguishable without an outline read); a ref that resolves to no live document lands in `errors` as {ref, error:\"doc_not_found\"} WITHOUT failing the call, so one bad ref never sinks the batch. Duplicate refs collapse first-seen (a repeated ref yields a single item). Capped at " + MANY_DOCS_CAP + " refs per call; excess refs are dropped and `truncated` is set. Pass budget_tokens to cap total hydrated size — the batch stops early and flags `truncated` when the next doc would exceed it. Use this after query/text_search/resolve to pull N whole docs in ONE round-trip instead of N serial docs_read calls; for a single doc use docs_read, and for lean structure-only orientation use docs_outline.",
|
|
277
|
+
description: "Batch whole-document read — the hydrate half of query→hydrate. The plural of docs_read: pass `docs`, a list of refs (each a doc id OR a path, same id-or-path symmetry docs_read accepts in its `doc` field), and get back one full read per ref. Returns `{ items, errors, truncated }`: each found doc is a full docs_read projection (`content` = complete file bytes verbatim, `properties` grouped by source, plus `path`/`docId`/`rev`, and — with include_ids:true — the document's ordered block `ids`, a `hashes` {block id → content hash} map for raw-`apply` CAS pinning, and a `parents` {block id → parent block id | null} map so top-level vs nested blocks are distinguishable without an outline read); a ref that resolves to no live document lands in `errors` as {ref, error:\"doc_not_found\"} WITHOUT failing the call, so one bad ref never sinks the batch. Duplicate refs collapse first-seen (a repeated ref yields a single item). Capped at " + MANY_DOCS_CAP + " refs per call; excess refs are dropped and `truncated` is set. Pass budget_tokens to cap total hydrated size — the batch stops early and flags `truncated` when the next doc would exceed it; the FIRST doc always ships even when it alone exceeds the budget (the same at-least-one-row contract as docs_list/docs_tree, so a budgeted caller always makes progress). Use this after query/text_search/resolve to pull N whole docs in ONE round-trip instead of N serial docs_read calls; for a single doc use docs_read, and for lean structure-only orientation use docs_outline.",
|
|
283
278
|
inputSchema: {
|
|
284
279
|
docs: z.array(z.string()),
|
|
285
280
|
include_ids: z.boolean().optional(),
|
|
@@ -322,7 +317,7 @@ export function buildServer(ctx) {
|
|
|
322
317
|
}
|
|
323
318
|
});
|
|
324
319
|
server.registerTool("nodes_get_many", {
|
|
325
|
-
description: "Fetch up to 100 blocks by id (in request order) with budget truncation. Block ids are globally unique, so `ids` may span ANY number of documents — each id is resolved to its owning doc server-side; `doc`/`path` is an optional SCOPE (ids from other docs then count as unresolved), not a requirement. Result carries `nodes`, `truncated` (cap or `budget_tokens` hit), and `unresolved` — the requested ids that name no live block (never silently dropped).",
|
|
320
|
+
description: "Fetch up to 100 blocks by id (in request order) with budget truncation. Block ids are globally unique, so `ids` may span ANY number of documents — each id is resolved to its owning doc server-side; `doc`/`path` is an optional SCOPE (ids from other docs then count as unresolved), not a requirement. Result carries `nodes`, `truncated` (cap or `budget_tokens` hit — the first resolved node always ships even when it alone exceeds the budget, so a budgeted caller always makes progress), and `unresolved` — the requested ids that name no live block (never silently dropped).",
|
|
326
321
|
inputSchema: {
|
|
327
322
|
doc: z.string().optional(),
|
|
328
323
|
path: z.string().optional(),
|
|
@@ -540,16 +535,18 @@ export function buildServer(ctx) {
|
|
|
540
535
|
// local: the resolution that used to live in the CLI now lives next to the
|
|
541
536
|
// store, so a remote client needs no local database.
|
|
542
537
|
server.registerTool("blocks_insert", {
|
|
543
|
-
description: "Insert new block(s) parsed from `markdown` under a parent, at a position. `to` names the parent: a BLOCK ref (id or locator) nests the new blocks under that block, or a DOCUMENT ref (doc id or path) places them at the document's top level — so appending a new section to a document is `{ to: \"<path>\", markdown: \"## New\\n\\n…\" }` (the positional generalization of docs_append). `at` places among the parent's children: \"end\" (default) / \"start\" / {before|after: <block ref>}. Expands to one insert op through the kernel writer.",
|
|
544
|
-
inputSchema: { to: z.string(), markdown: z.string(), at: atSchema.optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
538
|
+
description: "Insert new block(s) parsed from `markdown` under a parent, at a position. `to` names the parent: a BLOCK ref (id or locator) nests the new blocks under that block — it must be a CONTAINER (a list, list item, task, blockquote or table); a leaf such as a heading or paragraph fails `type_mismatch` (spec/mutate §1.1) — use sections_append for \"under this heading\" or `at: {after: <ref>}` for \"right after this block\"; or a DOCUMENT ref (doc id or path) places them at the document's top level — so appending a new section to a document is `{ to: \"<path>\", markdown: \"## New\\n\\n…\" }` (the positional generalization of docs_append). `at` places among the parent's children: \"end\" (default) / \"start\" / {before|after: <block ref>}. Optional `expect.parent_children_hash` is the destination-parent CAS: the parent's CURRENT direct child ids (the document's top-level ids for a document `to`) joined by \",\" and sha256-hexed — compute it from docs_read include_ids (`ids` filtered by `parents`) — and the insert fails `stale_expectation` (with `data.current.parent_children_hash`) if the siblings changed under you; `content_hash` has no meaning here (there is no target block) and is not accepted. Expands to one insert op through the kernel writer.",
|
|
539
|
+
inputSchema: { to: z.string(), markdown: z.string(), at: atSchema.optional(), expect: parentExpectSchema.optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
545
540
|
}, async (args) => {
|
|
546
541
|
try {
|
|
547
542
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
548
543
|
const root = requireRoot(rootPath);
|
|
549
544
|
const { parent, docId } = resolveParentRef(repoId, args.to);
|
|
550
545
|
const to = { parent, at: resolveAt(repoId, args.at) };
|
|
551
|
-
// A document parent carries no block to infer the doc from; pin it.
|
|
552
|
-
|
|
546
|
+
// A document parent carries no block to infer the doc from; pin it. The
|
|
547
|
+
// caller's expect rides through as the op-level expectation (spec/mutate
|
|
548
|
+
// §1.2: checked against the destination parent before the op mutates).
|
|
549
|
+
const ops = [{ op: "insert", ...(typeof parent === "object" ? { doc: docId } : {}), to, markdown: args.markdown, ...(args.expect ? { expect: args.expect } : {}) }];
|
|
553
550
|
return applyOps(repoId, root, ops, "blocks_insert", args.dry_run);
|
|
554
551
|
}
|
|
555
552
|
catch (e) {
|
|
@@ -583,8 +580,8 @@ export function buildServer(ctx) {
|
|
|
583
580
|
}
|
|
584
581
|
});
|
|
585
582
|
server.registerTool("blocks_move", {
|
|
586
|
-
description: "Move block(s) under a new parent at a position. `blocks` are block refs; `to` (the parent) is a block ref, or the blocks' OWN document (id or path) to move them to its top level — moving to ANOTHER document's root is not expressible (target_missing): anchor on a block in that document with `at` {before|after} instead. `at` is \"end\"/\"start\"/{before|after: <ref>}. One move op through the kernel writer.",
|
|
587
|
-
inputSchema: { blocks: z.array(z.string()), to: z.string(), at: atSchema.optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
583
|
+
description: "Move block(s) under a new parent at a position. `blocks` are block refs; `to` (the parent) is a block ref (a container — a leaf parent is `type_mismatch`, as for blocks_insert), or the blocks' OWN document (id or path) to move them to its top level — moving to ANOTHER document's root is not expressible (target_missing): anchor on a block in that document with `at` {before|after} instead. `at` is \"end\"/\"start\"/{before|after: <ref>}. Optional `expect.parent_children_hash` is the destination-parent CAS (as blocks_insert: the destination parent's CURRENT direct child ids joined by \",\", sha256 hex), checked once for the whole run before anything moves — `stale_expectation` with `data.current.parent_children_hash` if the destination's children changed; `content_hash` is meaningless here and not accepted. One move op through the kernel writer.",
|
|
584
|
+
inputSchema: { blocks: z.array(z.string()), to: z.string(), at: atSchema.optional(), expect: parentExpectSchema.optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
588
585
|
}, async (args) => {
|
|
589
586
|
try {
|
|
590
587
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
@@ -597,7 +594,7 @@ export function buildServer(ctx) {
|
|
|
597
594
|
throw new EngineError("target_missing", `blocks_move cannot target another document's root (${args.to}); anchor on a block in that document with at.before/at.after`, { data: { to: args.to } });
|
|
598
595
|
}
|
|
599
596
|
const to = { parent, at: resolveAt(repoId, args.at) };
|
|
600
|
-
const ops = [{ op: "move", blocks, to }];
|
|
597
|
+
const ops = [{ op: "move", blocks, to, ...(args.expect ? { expect: args.expect } : {}) }];
|
|
601
598
|
return applyOps(repoId, root, ops, "blocks_move", args.dry_run);
|
|
602
599
|
}
|
|
603
600
|
catch (e) {
|
|
@@ -791,53 +788,56 @@ export function buildServer(ctx) {
|
|
|
791
788
|
// Doc-level operations (06 §API). The MCP server serializes writes in-process,
|
|
792
789
|
// so these pass no omgbaseDir (the flock is for cross-process CLI writers);
|
|
793
790
|
// MCP-originated writes are actor agent:mcp.
|
|
794
|
-
|
|
791
|
+
// `dry_run` validates and returns the per-file `diffs` without writing
|
|
792
|
+
// (`DocOpContext.dryRun`), the docs-verb peer of `apply`'s preview.
|
|
793
|
+
const docCtx = (scope, dryRun) => {
|
|
795
794
|
if (!scope.rootPath)
|
|
796
795
|
throw new EngineError("repo_not_found", "repo has no filesystem source; mutation disabled");
|
|
797
|
-
return { repoId: scope.repoId, rootPath: scope.rootPath, actor: "agent:mcp" };
|
|
796
|
+
return { repoId: scope.repoId, rootPath: scope.rootPath, actor: "agent:mcp", ...(dryRun ? { dryRun: true } : {}) };
|
|
798
797
|
};
|
|
798
|
+
const docResult = (res, dryRun) => (dryRun ? ok(res) : okMutated(res));
|
|
799
799
|
server.registerTool("docs_create", {
|
|
800
|
-
description: "Create a new document at `path` from complete file bytes (`markdown`), with optional structured `frontmatter`. Fails path_taken if it already exists.",
|
|
801
|
-
inputSchema: { path: z.string(), markdown: z.string(), frontmatter: z.record(z.string(), z.unknown()).optional(), ...REPO_ARG },
|
|
800
|
+
description: "Create a new document at `path` from complete file bytes (`markdown`), with optional structured `frontmatter`. Fails path_taken if it already exists. dry_run:true validates and returns the would-be file under `diffs` (committed:false) without writing.",
|
|
801
|
+
inputSchema: { path: z.string(), markdown: z.string(), frontmatter: z.record(z.string(), z.unknown()).optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
802
802
|
}, async (args) => {
|
|
803
803
|
try {
|
|
804
|
-
return
|
|
804
|
+
return docResult(docsCreate(store, docCtx(repoScope(args.repo), args.dry_run), args.path, args.markdown, args.frontmatter), args.dry_run);
|
|
805
805
|
}
|
|
806
806
|
catch (e) {
|
|
807
807
|
return fail(e);
|
|
808
808
|
}
|
|
809
809
|
});
|
|
810
810
|
server.registerTool("docs_move", {
|
|
811
|
-
description: "Rename a document to a new repo-relative path; block identity and history are preserved. Fails path_taken if the destination exists. Links follow the PATH, not the identity: inbound links written against the old path now dangle (their edges become `phantom:<old path>`, so links_stale reports them), and links already written against the new path start resolving to this doc. The result lists those `dangling` inbound links ({doc, path, block, target, anchor, field?} per occurrence; block null = a frontmatter relation). Pass `retarget_inbound:true` to rewrite them in the same call — a destination-aware rewrite (anchors/link text/code spans preserved, absolute vs relative style kept) applied as one CAS-checked changeset, after which `dangling` holds only what could not be rewritten (frontmatter relations) and `retargeted` lists the touched blocks/docs.",
|
|
812
|
-
inputSchema: { doc: z.string(), to_path: z.string(), retarget_inbound: z.boolean().optional(), ...REPO_ARG },
|
|
811
|
+
description: "Rename a document to a new repo-relative path; block identity and history are preserved. Fails path_taken if the destination exists. Links follow the PATH, not the identity: inbound links written against the old path now dangle (their edges become `phantom:<old path>`, so links_stale reports them), and links already written against the new path start resolving to this doc. The result lists those `dangling` inbound links ({doc, path, block, target, anchor, field?} per occurrence; block null = a frontmatter relation). Pass `retarget_inbound:true` to rewrite them in the same call — a destination-aware rewrite (anchors/link text/code spans preserved, absolute vs relative style kept) applied as one CAS-checked changeset, after which `dangling` holds only what could not be rewritten (frontmatter relations) and `retargeted` lists the touched blocks/docs. dry_run:true validates and returns the plan — `dangling`, `retargeted`, and the per-file `diffs` (the old path emptied, the new path filled, plus any rewritten sources) — without renaming or committing.",
|
|
812
|
+
inputSchema: { doc: z.string(), to_path: z.string(), retarget_inbound: z.boolean().optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
813
813
|
}, async (args) => {
|
|
814
814
|
try {
|
|
815
|
-
return
|
|
815
|
+
return docResult(docsMove(store, docCtx(repoScope(args.repo), args.dry_run), args.doc, args.to_path, { retargetInbound: args.retarget_inbound === true }), args.dry_run);
|
|
816
816
|
}
|
|
817
817
|
catch (e) {
|
|
818
818
|
return fail(e);
|
|
819
819
|
}
|
|
820
820
|
});
|
|
821
821
|
server.registerTool("docs_delete", {
|
|
822
|
-
description: "Delete a document: tombstone it and its live blocks (resurrection-poolable) and remove the file. Requires the explicit doc id/path.",
|
|
823
|
-
inputSchema: { doc: z.string(), ...REPO_ARG },
|
|
822
|
+
description: "Delete a document: tombstone it and its live blocks (resurrection-poolable) and remove the file. Requires the explicit doc id/path. dry_run:true validates and returns the file's removal under `diffs` without deleting.",
|
|
823
|
+
inputSchema: { doc: z.string(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
824
824
|
}, async (args) => {
|
|
825
825
|
try {
|
|
826
|
-
return
|
|
826
|
+
return docResult(docsDelete(store, docCtx(repoScope(args.repo), args.dry_run), args.doc), args.dry_run);
|
|
827
827
|
}
|
|
828
828
|
catch (e) {
|
|
829
829
|
return fail(e);
|
|
830
830
|
}
|
|
831
831
|
});
|
|
832
832
|
server.registerTool("docs_set_meta", {
|
|
833
|
-
description: "Surgical frontmatter patch: set the given keys and/or unset named keys, re-ingesting the document. Other frontmatter is preserved.",
|
|
834
|
-
inputSchema: { doc: z.string(), set: z.record(z.string(), z.unknown()).optional(), unset: z.array(z.string()).optional(), ...REPO_ARG },
|
|
833
|
+
description: "Surgical frontmatter patch: set the given keys and/or unset named keys, re-ingesting the document. Other frontmatter is preserved. dry_run:true validates and returns the rewritten file under `diffs` without writing.",
|
|
834
|
+
inputSchema: { doc: z.string(), set: z.record(z.string(), z.unknown()).optional(), unset: z.array(z.string()).optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
835
835
|
}, async (args) => {
|
|
836
836
|
try {
|
|
837
|
-
return
|
|
837
|
+
return docResult(docsSetMeta(store, docCtx(repoScope(args.repo), args.dry_run), args.doc, {
|
|
838
838
|
...(args.set ? { set: args.set } : {}),
|
|
839
839
|
...(args.unset ? { unset: args.unset } : {}),
|
|
840
|
-
}));
|
|
840
|
+
}), args.dry_run);
|
|
841
841
|
}
|
|
842
842
|
catch (e) {
|
|
843
843
|
return fail(e);
|
|
@@ -945,15 +945,11 @@ export function buildServer(ctx) {
|
|
|
945
945
|
try {
|
|
946
946
|
const { repoId } = repoScope(args.repo);
|
|
947
947
|
const docId = resolveDocId(repoId, { doc: args.doc });
|
|
948
|
-
|
|
949
|
-
.
|
|
950
|
-
.
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
if (!toRev || !fromRev)
|
|
954
|
-
throw new EngineError("target_missing", `no revisions to diff for ${JSON.stringify(args.doc)}`);
|
|
955
|
-
const path = store.db.prepare("SELECT path FROM docs WHERE doc_id = ?").get(docId)?.path ?? "";
|
|
956
|
-
return ok({ doc: docId, path, from: fromRev, to: toRev, diff: diffUnified(store, docId, fromRev, toRev) });
|
|
948
|
+
return ok(docDiffUnified(store, docId, {
|
|
949
|
+
...(args.from_rev !== undefined ? { fromRev: args.from_rev } : {}),
|
|
950
|
+
...(args.to_rev !== undefined ? { toRev: args.to_rev } : {}),
|
|
951
|
+
label: JSON.stringify(args.doc),
|
|
952
|
+
}));
|
|
957
953
|
}
|
|
958
954
|
catch (e) {
|
|
959
955
|
return fail(e);
|
|
@@ -1067,10 +1063,7 @@ export function buildServer(ctx) {
|
|
|
1067
1063
|
inputSchema: {},
|
|
1068
1064
|
}, async () => {
|
|
1069
1065
|
try {
|
|
1070
|
-
|
|
1071
|
-
.prepare("SELECT r.slug AS slug, MAX(CASE WHEN s.adapter = 'fs' THEN 1 ELSE 0 END) AS has_fs FROM repos r LEFT JOIN attachments a ON a.repo_id = r.repo_id LEFT JOIN sources s ON s.source_id = a.source_id GROUP BY r.repo_id, r.slug ORDER BY r.slug")
|
|
1072
|
-
.all();
|
|
1073
|
-
return ok({ repos: rows.map((r) => ({ slug: r.slug, hasSource: r.has_fs === 1 })) });
|
|
1066
|
+
return ok(reposList(store));
|
|
1074
1067
|
}
|
|
1075
1068
|
catch (e) {
|
|
1076
1069
|
return fail(e);
|
package/dist/src/mutate/apply.js
CHANGED
|
@@ -9,6 +9,7 @@ import { renderDoc, markContainerDirty, MutationError } from "./tree.js";
|
|
|
9
9
|
import { opInsert, opUpdate, opMove, opRemove, opSplit, opMerge, healTopLevelSeams, checkParentChildrenHash } from "./ops.js";
|
|
10
10
|
import { withWriterLock } from "../sync/writer-lock.js";
|
|
11
11
|
import { resolveDocStore } from "./doc-store.js";
|
|
12
|
+
import { isContainerBlock } from "../format/index.js";
|
|
12
13
|
// Resolve "$n.ids[i]" placeholders in a string against prior op results.
|
|
13
14
|
function resolvePlaceholder(value, results) {
|
|
14
15
|
const m = /^\$(\d+)\.ids\[(\d+)\]$/.exec(value);
|
|
@@ -248,9 +249,11 @@ function crossDocMove(src, dst, blockIds, to, opIndex, expect) {
|
|
|
248
249
|
throw new MutationError("block_missing", `block ${id} not found in source doc`, { op_index: opIndex });
|
|
249
250
|
moving.push(found.block);
|
|
250
251
|
}
|
|
251
|
-
// §1.
|
|
252
|
+
// §1.1 container rule + §1.2 destination CAS (in `dst`), resolved once before
|
|
253
|
+
// either tree moves so a leaf destination errors with both trees untouched.
|
|
254
|
+
const pre = resolveDstTarget(dst, to, opIndex);
|
|
252
255
|
if (expect?.parent_children_hash !== undefined)
|
|
253
|
-
checkParentChildrenHash(
|
|
256
|
+
checkParentChildrenHash(pre.siblings, expect, opIndex);
|
|
254
257
|
// remove from their source lists (marking each emptied owner dirty)
|
|
255
258
|
for (const id of blockIds) {
|
|
256
259
|
const f = locateMut(src, id);
|
|
@@ -260,7 +263,7 @@ function crossDocMove(src, dst, blockIds, to, opIndex, expect) {
|
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
265
|
// Insert into dst at the resolved target; the destination owner re-renders.
|
|
263
|
-
const target = resolveDstTarget(dst, to);
|
|
266
|
+
const target = resolveDstTarget(dst, to, opIndex);
|
|
264
267
|
target.siblings.splice(target.index, 0, ...moving);
|
|
265
268
|
markContainerDirty(dst, target.siblings);
|
|
266
269
|
// Heal both documents' top-level seams (spec/mutate §2.3): a block that was
|
|
@@ -283,7 +286,7 @@ function locateMut(doc, blockId) {
|
|
|
283
286
|
};
|
|
284
287
|
return search(doc.children);
|
|
285
288
|
}
|
|
286
|
-
function resolveDstTarget(dst, to) {
|
|
289
|
+
function resolveDstTarget(dst, to, opIndex = 0) {
|
|
287
290
|
if (typeof to.parent === "object" && "doc" in to.parent) {
|
|
288
291
|
return { siblings: dst.children, index: resolveAt(dst.children, to.at) };
|
|
289
292
|
}
|
|
@@ -291,6 +294,11 @@ function resolveDstTarget(dst, to) {
|
|
|
291
294
|
const f = locateMut(dst, to.parent);
|
|
292
295
|
if (!f)
|
|
293
296
|
throw new MutationError("parent_missing", `parent ${to.parent} not found in dest`);
|
|
297
|
+
// spec/mutate §1.1 (1.2): a leaf destination would be re-rendered from its
|
|
298
|
+
// children and lose its own text — the same rule as the in-document ops.
|
|
299
|
+
if (!isContainerBlock(dst.format, f.block)) {
|
|
300
|
+
throw new MutationError("type_mismatch", `${to.parent} is a ${f.block.type}, not a container; place relative to it with at.before/at.after or append to its section`, { op_index: opIndex, block: to.parent, type: f.block.type });
|
|
301
|
+
}
|
|
294
302
|
return { siblings: f.block.children, index: resolveAt(f.block.children, to.at) };
|
|
295
303
|
}
|
|
296
304
|
// heading/section: insert into top-level list at the anchor.
|
|
@@ -2,6 +2,7 @@ import { writeFileSync, readFileSync, existsSync, renameSync, unlinkSync, mkdirS
|
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { sha256 } from "../core/hash.js";
|
|
4
4
|
import { recordFileStat } from "../sync/freshness.js";
|
|
5
|
+
import { EngineError } from "../mcp/errors.js";
|
|
5
6
|
/** Filesystem-backed DocStore rooted at a working-tree directory (v1 default). */
|
|
6
7
|
export class FsDocStore {
|
|
7
8
|
root;
|
|
@@ -66,6 +67,8 @@ export function resolveDocStore(req) {
|
|
|
66
67
|
return req.docStore;
|
|
67
68
|
if (req.rootPath != null)
|
|
68
69
|
return new FsDocStore(req.rootPath);
|
|
69
|
-
|
|
70
|
+
// Typed, so every client renders the same `repo_not_found` the MCP server's
|
|
71
|
+
// pre-checks emit (spec/surface §4; spec/cli §9 "sourceless writes").
|
|
72
|
+
throw new EngineError("repo_not_found", "repo has no filesystem source; mutation disabled");
|
|
70
73
|
}
|
|
71
74
|
//# sourceMappingURL=doc-store.js.map
|