@omgbase/core 0.11.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/core/ids.d.ts +4 -0
- package/dist/src/core/ids.js +17 -0
- package/dist/src/core/ingest.d.ts +39 -3
- package/dist/src/core/ingest.js +233 -202
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +28 -3
- package/dist/src/core/store/size.d.ts +76 -0
- package/dist/src/core/store/size.js +223 -0
- package/dist/src/core/store/store.d.ts +9 -0
- package/dist/src/core/store/store.js +11 -0
- package/dist/src/graph/history.js +15 -12
- package/dist/src/index.d.ts +12 -5
- package/dist/src/index.js +18 -7
- package/dist/src/mcp/arguments.d.ts +9 -2
- package/dist/src/mcp/arguments.js +15 -2
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +3 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +27 -6
- package/dist/src/mcp/server.d.ts +56 -1
- package/dist/src/mcp/server.js +365 -203
- package/dist/src/mutate/apply.d.ts +93 -12
- package/dist/src/mutate/apply.js +203 -113
- package/dist/src/mutate/canonical.d.ts +27 -0
- package/dist/src/mutate/canonical.js +138 -0
- package/dist/src/mutate/commit.d.ts +150 -0
- package/dist/src/mutate/commit.js +349 -0
- package/dist/src/mutate/doc-store.d.ts +3 -3
- package/dist/src/mutate/doc-store.js +21 -6
- package/dist/src/mutate/docs.d.ts +42 -87
- package/dist/src/mutate/docs.js +50 -204
- package/dist/src/mutate/effects.d.ts +363 -0
- package/dist/src/mutate/effects.js +1161 -0
- package/dist/src/mutate/materialize.d.ts +49 -0
- package/dist/src/mutate/materialize.js +174 -0
- package/dist/src/mutate/opset.d.ts +27 -2
- package/dist/src/mutate/opset.js +12 -5
- package/dist/src/mutate/plan-update.d.ts +1 -46
- package/dist/src/mutate/plan-update.js +19 -67
- package/dist/src/mutate/plan.d.ts +210 -0
- package/dist/src/mutate/plan.js +512 -0
- package/dist/src/mutate/select.d.ts +89 -0
- package/dist/src/mutate/select.js +879 -0
- package/dist/src/mutate/sugar.d.ts +135 -0
- package/dist/src/mutate/sugar.js +167 -0
- package/dist/src/mutate/tree.d.ts +4 -2
- package/dist/src/oqx-js/planner.js +4 -0
- package/dist/src/oqx-js/run.d.ts +28 -1
- package/dist/src/oqx-js/run.js +85 -22
- package/dist/src/spec-versions.d.ts +6 -6
- package/dist/src/spec-versions.js +5 -5
- package/dist/src/surface-paths.d.ts +17 -26
- package/dist/src/surface-paths.js +27 -22
- package/dist/src/sync/checkpoint.d.ts +5 -2
- package/dist/src/sync/checkpoint.js +10 -6
- package/dist/src/sync/freshness.d.ts +10 -7
- package/dist/src/sync/freshness.js +15 -17
- package/dist/src/sync/observe.d.ts +4 -1
- package/dist/src/sync/observe.js +56 -5
- package/dist/src/sync/recovery.d.ts +4 -0
- package/dist/src/sync/recovery.js +4 -1
- package/dist/src/sync/stat-cache.d.ts +7 -0
- package/dist/src/sync/stat-cache.js +22 -0
- package/dist/src/sync/tombstone.js +23 -19
- package/dist/src/sync/watcher.d.ts +2 -0
- package/dist/src/sync/watcher.js +4 -1
- package/dist/src/sync/writer-lock.d.ts +15 -0
- package/dist/src/sync/writer-lock.js +104 -17
- package/dist/src/version.d.ts +7 -2
- package/dist/src/version.js +5 -2
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { openSync, writeSync, fsyncSync, closeSync, readFileSync, existsSync, renameSync, unlinkSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { sha256 } from "../core/hash.js";
|
|
4
|
-
import { recordFileStat } from "../sync/
|
|
4
|
+
import { recordFileStat } from "../sync/stat-cache.js";
|
|
5
5
|
import { EngineError } from "../mcp/errors.js";
|
|
6
6
|
/** Filesystem-backed DocStore rooted at a working-tree directory (v1 default). */
|
|
7
7
|
export class FsDocStore {
|
|
@@ -22,8 +22,20 @@ export class FsDocStore {
|
|
|
22
22
|
write(path, bytes) {
|
|
23
23
|
const abs = this.abs(path);
|
|
24
24
|
mkdirSync(dirname(abs), { recursive: true });
|
|
25
|
+
// Temp file + rename for atomicity; the temp file is fsynced before the
|
|
26
|
+
// rename (store 14.0) so a power loss after the commit cannot leave a
|
|
27
|
+
// truncated file under the final name — the same loss class as SQLite's
|
|
28
|
+
// `synchronous = NORMAL`. No directory fsync: the rename itself may be lost,
|
|
29
|
+
// which the pending row then replays.
|
|
25
30
|
const tmp = `${abs}.omgtmp`;
|
|
26
|
-
|
|
31
|
+
const fd = openSync(tmp, "w");
|
|
32
|
+
try {
|
|
33
|
+
writeSync(fd, bytes);
|
|
34
|
+
fsyncSync(fd);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
closeSync(fd);
|
|
38
|
+
}
|
|
27
39
|
renameSync(tmp, abs);
|
|
28
40
|
}
|
|
29
41
|
rename(from, to) {
|
|
@@ -52,9 +64,12 @@ export class NullDocStore {
|
|
|
52
64
|
read() {
|
|
53
65
|
return null;
|
|
54
66
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
// Parameters spelled out so a subclass may override with the interface's
|
|
68
|
+
// signature (a zero-parameter base method makes `write(path, bytes)` an
|
|
69
|
+
// incompatible override under TS2416).
|
|
70
|
+
write(_path, _bytes) { }
|
|
71
|
+
rename(_from, _to) { }
|
|
72
|
+
remove(_path) { }
|
|
58
73
|
recordStat() { }
|
|
59
74
|
clearStat() { }
|
|
60
75
|
}
|
|
@@ -1,91 +1,46 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
2
|
import { type InboundLink } from "../graph/inbound-links.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export
|
|
26
|
-
docId: string;
|
|
27
|
-
path: string;
|
|
28
|
-
committed: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* With `dryRun`: the file changes the operation would make, keyed by
|
|
31
|
-
* repo-relative path — `before` the current bytes (`""` for a file that
|
|
32
|
-
* would be created), `after` the bytes the operation would leave (`""` for a
|
|
33
|
-
* file that would be removed). A rename is two entries: the old path
|
|
34
|
-
* emptied, the new path filled. Absent on a committed result.
|
|
35
|
-
*/
|
|
36
|
-
diffs?: Record<string, {
|
|
37
|
-
before: string;
|
|
38
|
-
after: string;
|
|
39
|
-
}>;
|
|
40
|
-
}
|
|
41
|
-
export interface DocMoveOptions {
|
|
42
|
-
/**
|
|
43
|
-
* Rewrite the inbound links that named the OLD path so they point at the new
|
|
44
|
-
* one — a destination-aware rewrite (anchors, link text, titles, and code
|
|
45
|
-
* spans preserved; absolute/relative style kept) applied as a CAS-checked
|
|
46
|
-
* `update` per source block in one follow-up api changeset. **Default `true`**
|
|
47
|
-
* (spec/mutate 1.3): a move means "same document, new place", so the authored
|
|
48
|
-
* links follow it; pass `false` for the rarer intent that the old path become
|
|
49
|
-
* unbound. Frontmatter-level relations are never rewritten (reported in
|
|
50
|
-
* `dangling`).
|
|
51
|
-
*/
|
|
52
|
-
retargetInbound?: boolean;
|
|
53
|
-
}
|
|
54
|
-
export interface DocMoveResult extends DocOpResult {
|
|
55
|
-
/**
|
|
56
|
-
* Inbound links still pointing at the OLD path after this call — the source
|
|
57
|
-
* blocks `links_stale` will now report (with the default retarget: only the
|
|
58
|
-
* frontmatter relations, which are never rewritten). Computed from the open
|
|
59
|
-
* edge index before the move.
|
|
60
|
-
*/
|
|
61
|
-
dangling: InboundLink[];
|
|
62
|
-
/**
|
|
63
|
-
* The source blocks rewritten and the docs re-ingested (empty lists when the
|
|
64
|
-
* rewrite ran but only frontmatter relations linked in); `null` when the
|
|
65
|
-
* retarget was opted out (`retargetInbound: false`) or nothing linked in.
|
|
66
|
-
*/
|
|
67
|
-
retargeted: {
|
|
68
|
-
blocks: string[];
|
|
69
|
-
docs: string[];
|
|
70
|
-
} | null;
|
|
71
|
-
}
|
|
72
|
-
/** docs_create: create a new document from complete file bytes (frontmatter incl.). */
|
|
73
|
-
export declare function docsCreate(store: Store, ctx: DocOpContext, path: string, markdown: string, frontmatter?: Record<string, unknown>): DocOpResult;
|
|
3
|
+
import type { Op } from "./apply.js";
|
|
4
|
+
import type { MutDoc } from "./tree.js";
|
|
5
|
+
/** spec/mutate §6: the storage form of a document path — leading `/` stripped, `\` → `/`. */
|
|
6
|
+
export declare function canonicalDocPath(path: string): string;
|
|
7
|
+
/** Compose a full document's bytes from optional frontmatter + body markdown
|
|
8
|
+
* (spec/mutate §6 `docs_create`): the body with a trailing `\n` ensured; a
|
|
9
|
+
* non-empty frontmatter mapping becomes `---\n<yaml>\n---\n` followed by a
|
|
10
|
+
* blank line unless the body already starts with one. The effect planner's
|
|
11
|
+
* `set frontmatter.<k>` composes through this same function (§11.3), so its
|
|
12
|
+
* bytes equal `docs_set_meta`'s for the same patch. */
|
|
13
|
+
export declare function composeFile(markdown: string, frontmatter?: Record<string, unknown>): string;
|
|
14
|
+
/**
|
|
15
|
+
* A document this request CREATES as the planner's working tree (spec/mutate
|
|
16
|
+
* §11.3, 3.0): `source` (the composed file) parsed per the path's format —
|
|
17
|
+
* the frontmatter block becomes `frontmatterRaw` (its raw + its trailing
|
|
18
|
+
* trivia, as `loadMutDoc` builds it), the bytes before it the leading trivia,
|
|
19
|
+
* every body block a `MutBlock` with its exact trivia and an id minted NOW in
|
|
20
|
+
* pre-order under the current minter (the symbolic one at plan time, so the
|
|
21
|
+
* ids are `$new.k`; the production one at execute, so the replay mints the
|
|
22
|
+
* same positions). `docId` is the plan's document symbol. The round-trip law
|
|
23
|
+
* makes `renderDoc` of the result equal `source` byte for byte.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseNewDoc(symbol: string, path: string, source: string): MutDoc;
|
|
74
26
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* follow-up changeset (spec/mutate 1.3); `retargetInbound: false` leaves them
|
|
82
|
-
* as written and the result lists them.
|
|
27
|
+
* The `retarget_inbound` plan of a move `fromPath → toRel`: one coalesced,
|
|
28
|
+
* CAS-pinned `update` per deepest hit block, the blocks and docs it touches,
|
|
29
|
+
* and the inbound links it leaves dangling. Pure over the store's current
|
|
30
|
+
* blocks — the move itself changes no block — so the effect planner's `move
|
|
31
|
+
* document` (§11.3) and the tool's `dangling`/`retargeted` report (computed
|
|
32
|
+
* before the commit) plan identically.
|
|
83
33
|
*/
|
|
84
|
-
export declare function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
34
|
+
export declare function planInboundRetarget(store: Store, docId: string, fromPath: string, toRel: string, inbound: InboundLink[]): {
|
|
35
|
+
ops: Op[];
|
|
36
|
+
blocks: string[];
|
|
37
|
+
docs: string[];
|
|
38
|
+
dangling: InboundLink[];
|
|
39
|
+
};
|
|
40
|
+
/** Split a file's bytes into (frontmatter object, body markdown) with
|
|
41
|
+
* `^---\r?\n([\s\S]*?)\r?\n---\r?\n?` (spec/mutate §6). Missing or malformed
|
|
42
|
+
* frontmatter yields an empty object and the whole content as body. */
|
|
43
|
+
export declare function splitFrontmatter(content: string): {
|
|
44
|
+
frontmatter: Record<string, unknown>;
|
|
45
|
+
body: string;
|
|
46
|
+
};
|
package/dist/src/mutate/docs.js
CHANGED
|
@@ -1,175 +1,72 @@
|
|
|
1
1
|
import { stringify as stringifyYaml, parse as parseYaml } from "yaml";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { makeReconcilingResolver } from "../sync/reconciling-ingest.js";
|
|
5
|
-
import { newCommit } from "../core/store/writers.js";
|
|
2
|
+
import { parseTree } from "../core/parse/tree.js";
|
|
3
|
+
import { adapterForPath } from "../format/index.js";
|
|
6
4
|
import { mintId } from "../core/ids.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
import { retargetLinksInRaw } from "../graph/inbound-links.js";
|
|
6
|
+
// The document operations' shared pieces (spec/mutate §6, 3.0). Through 2.x this
|
|
7
|
+
// module held `docsCreate` / `docsMove` / `docsDelete` / `docsSetMeta`, library
|
|
8
|
+
// functions that composed the internal commit effects themselves; since 3.0 the
|
|
9
|
+
// operations are the effect builders of `sugar.ts` (`create`, `move document`,
|
|
10
|
+
// `remove document`, `set`/`unset frontmatter.<k>`) planned and executed by the
|
|
11
|
+
// Effect layer (`plan.ts`), and what remains here is what the planner's
|
|
12
|
+
// lowerings share with them: the path canonicalization, the file composition,
|
|
13
|
+
// the parse of a created document, the inbound-link retarget plan and the
|
|
14
|
+
// frontmatter split.
|
|
17
15
|
function dirOf(path) {
|
|
18
16
|
return path.replace(/[^/]*$/, "");
|
|
19
17
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
function canonical(path) {
|
|
18
|
+
/** spec/mutate §6: the storage form of a document path — leading `/` stripped, `\` → `/`. */
|
|
19
|
+
export function canonicalDocPath(path) {
|
|
24
20
|
return path.replace(/^\/+/, "").split("\\").join("/");
|
|
25
21
|
}
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
/** Compose a full document's bytes from optional frontmatter + body markdown
|
|
23
|
+
* (spec/mutate §6 `docs_create`): the body with a trailing `\n` ensured; a
|
|
24
|
+
* non-empty frontmatter mapping becomes `---\n<yaml>\n---\n` followed by a
|
|
25
|
+
* blank line unless the body already starts with one. The effect planner's
|
|
26
|
+
* `set frontmatter.<k>` composes through this same function (§11.3), so its
|
|
27
|
+
* bytes equal `docs_set_meta`'s for the same patch. */
|
|
28
|
+
export function composeFile(markdown, frontmatter) {
|
|
28
29
|
const body = markdown.endsWith("\n") || markdown.length === 0 ? markdown : markdown + "\n";
|
|
29
30
|
if (!frontmatter || Object.keys(frontmatter).length === 0)
|
|
30
31
|
return body;
|
|
31
32
|
const yaml = stringifyYaml(frontmatter).replace(/\n$/, "");
|
|
32
33
|
return `---\n${yaml}\n---\n${body.startsWith("\n") ? body : "\n" + body}`;
|
|
33
34
|
}
|
|
34
|
-
/** docs_create: create a new document from complete file bytes (frontmatter incl.). */
|
|
35
|
-
export function docsCreate(store, ctx, path, markdown, frontmatter) {
|
|
36
|
-
const rel = canonical(path);
|
|
37
|
-
const existing = findDoc(store, { repoId: ctx.repoId, path: rel });
|
|
38
|
-
if (existing)
|
|
39
|
-
throw new MutationError("path_taken", `document already exists at ${rel}`);
|
|
40
|
-
const content = composeFile(markdown, frontmatter);
|
|
41
|
-
const docStore = resolveDocStore(ctx);
|
|
42
|
-
if (ctx.dryRun) {
|
|
43
|
-
if (docStore.exists(rel))
|
|
44
|
-
throw new MutationError("path_taken", `file already exists on disk at ${rel}`);
|
|
45
|
-
return { docId: mintId("d"), path: rel, committed: false, diffs: { [rel]: { before: "", after: content } } };
|
|
46
|
-
}
|
|
47
|
-
return underLock(ctx, () => {
|
|
48
|
-
if (docStore.exists(rel))
|
|
49
|
-
throw new MutationError("path_taken", `file already exists on disk at ${rel}`);
|
|
50
|
-
docStore.write(rel, content);
|
|
51
|
-
const ts = nowOr(ctx);
|
|
52
|
-
const res = ingestFile(store, ctx.repoId, rel, content, { ts, origin: "api", actor: ctx.actor ?? null, reason: `create ${rel}`, resolveIds: makeReconcilingResolver(store, ctx.repoId, { ts, path: rel }) });
|
|
53
|
-
if (ctx.omgbaseDir)
|
|
54
|
-
docStore.recordStat(store, ctx.repoId, rel, content);
|
|
55
|
-
return { docId: res.docId, path: rel, committed: true };
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
35
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
36
|
+
* A document this request CREATES as the planner's working tree (spec/mutate
|
|
37
|
+
* §11.3, 3.0): `source` (the composed file) parsed per the path's format —
|
|
38
|
+
* the frontmatter block becomes `frontmatterRaw` (its raw + its trailing
|
|
39
|
+
* trivia, as `loadMutDoc` builds it), the bytes before it the leading trivia,
|
|
40
|
+
* every body block a `MutBlock` with its exact trivia and an id minted NOW in
|
|
41
|
+
* pre-order under the current minter (the symbolic one at plan time, so the
|
|
42
|
+
* ids are `$new.k`; the production one at execute, so the replay mints the
|
|
43
|
+
* same positions). `docId` is the plan's document symbol. The round-trip law
|
|
44
|
+
* makes `renderDoc` of the result equal `source` byte for byte.
|
|
67
45
|
*/
|
|
68
|
-
export function
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
if (findDoc(store, { repoId: ctx.repoId, path: toRel }))
|
|
75
|
-
throw new MutationError("path_taken", `a document already exists at ${toRel}`);
|
|
76
|
-
// Who links here BY PATH, as written — before the path changes.
|
|
77
|
-
const inbound = inboundLinksTo(store, ctx.repoId, info.docId, info.path);
|
|
78
|
-
const docStore = resolveDocStore(ctx);
|
|
79
|
-
if (ctx.dryRun) {
|
|
80
|
-
if (docStore.exists(toRel))
|
|
81
|
-
throw new MutationError("path_taken", `file already exists on disk at ${toRel}`);
|
|
82
|
-
const content = docStore.read(info.path) ?? "";
|
|
83
|
-
const diffs = { [info.path]: { before: content, after: "" }, [toRel]: { before: "", after: content } };
|
|
84
|
-
const planned = { docId: info.docId, path: toRel, committed: false, diffs };
|
|
85
|
-
if (!retargetInbound || inbound.length === 0)
|
|
86
|
-
return { ...planned, dangling: inbound, retargeted: null };
|
|
87
|
-
const plan = planInboundRetarget(store, info.docId, info.path, toRel, inbound);
|
|
88
|
-
if (plan.ops.length > 0) {
|
|
89
|
-
const preview = apply(store, { ...applyCtx(ctx), ops: plan.ops, dryRun: true, origin: { actor: ctx.actor ?? "api", reason: `retarget inbound links ${info.path} -> ${toRel}` } });
|
|
90
|
-
Object.assign(diffs, preview.diffs ?? {});
|
|
91
|
-
}
|
|
92
|
-
return { ...planned, dangling: plan.dangling, retargeted: { blocks: plan.blocks, docs: plan.docs } };
|
|
93
|
-
}
|
|
94
|
-
const moved = underLock(ctx, () => {
|
|
95
|
-
if (docStore.exists(toRel))
|
|
96
|
-
throw new MutationError("path_taken", `file already exists on disk at ${toRel}`);
|
|
97
|
-
const content = docStore.read(info.path) ?? "";
|
|
98
|
-
if (docStore.exists(info.path))
|
|
99
|
-
docStore.rename(info.path, toRel);
|
|
100
|
-
else
|
|
101
|
-
docStore.write(toRel, content);
|
|
102
|
-
const ts = nowOr(ctx);
|
|
103
|
-
store.write((db) => {
|
|
104
|
-
// Update the document row's path + its open revisions' path pointer, and
|
|
105
|
-
// record an api commit noting the move.
|
|
106
|
-
const commit = newCommit(db, { repoId: ctx.repoId, ts, origin: "api", actor: ctx.actor ?? null, reason: `move ${info.path} -> ${toRel}` });
|
|
107
|
-
db.prepare("UPDATE docs SET path = ? WHERE doc_id = ?").run(toRel, info.docId);
|
|
108
|
-
db.prepare("UPDATE revisions SET path = ? WHERE doc_id = ? AND rev_id = ?").run(toRel, info.docId, info.currentRev);
|
|
109
|
-
void commit;
|
|
110
|
-
// Edge index. Every open edge from ANOTHER doc into this one resolved
|
|
111
|
-
// via the old path, so it dangles now → re-point it at the old path's
|
|
112
|
-
// phantom (in place, mirroring adoptPhantoms). A self-doc edge dangles
|
|
113
|
-
// only when the link names the path (`[x](/old.md#H)`); a pure fragment
|
|
114
|
-
// (`#H`) resolves to the doc regardless of path and is left alone — the
|
|
115
|
-
// inbound scan distinguishes them from the raw.
|
|
116
|
-
const phantom = `phantom:${info.path}`;
|
|
117
|
-
const affected = new Set();
|
|
118
|
-
const others = db.prepare("SELECT DISTINCT src_doc FROM edges WHERE dst_node = ? AND to_commit IS NULL AND src_doc != ?").all(info.docId, info.docId);
|
|
119
|
-
for (const r of others)
|
|
120
|
-
affected.add(r.src_doc);
|
|
121
|
-
db.prepare("UPDATE edges SET dst_node = ? WHERE dst_node = ? AND to_commit IS NULL AND src_doc != ?").run(phantom, info.docId, info.docId);
|
|
122
|
-
const selfEdge = db.prepare("UPDATE edges SET dst_node = ? WHERE dst_node = ? AND to_commit IS NULL AND src_doc = ? AND src_block = ? AND anchor IS ?");
|
|
123
|
-
for (const l of inbound) {
|
|
124
|
-
if (l.doc_id !== info.docId || l.block_id === null)
|
|
125
|
-
continue;
|
|
126
|
-
selfEdge.run(phantom, info.docId, info.docId, l.block_id, l.anchor);
|
|
127
|
-
affected.add(info.docId);
|
|
128
|
-
}
|
|
129
|
-
for (const d of affected)
|
|
130
|
-
rebuildDocEdges(db, d);
|
|
131
|
-
// Links already written against the NEW path resolve to this doc now.
|
|
132
|
-
adoptPhantoms(db, toRel, info.docId);
|
|
133
|
-
});
|
|
134
|
-
if (ctx.omgbaseDir) {
|
|
135
|
-
docStore.clearStat(store, ctx.repoId, info.path); // drop the old path's row
|
|
136
|
-
docStore.recordStat(store, ctx.repoId, toRel, content);
|
|
137
|
-
}
|
|
138
|
-
return { docId: info.docId, path: toRel, committed: true };
|
|
139
|
-
});
|
|
140
|
-
if (!retargetInbound || inbound.length === 0)
|
|
141
|
-
return { ...moved, dangling: inbound, retargeted: null };
|
|
142
|
-
// Rewrite the dangling links block by block (one coalesced update per block,
|
|
143
|
-
// CAS on the block's current hash) and apply as one changeset; the re-ingest
|
|
144
|
-
// re-extracts each source doc, whose links now resolve to the moved doc.
|
|
145
|
-
const plan = planInboundRetarget(store, info.docId, info.path, toRel, inbound);
|
|
146
|
-
if (plan.ops.length > 0) {
|
|
147
|
-
apply(store, {
|
|
148
|
-
...applyCtx(ctx),
|
|
149
|
-
ops: plan.ops,
|
|
150
|
-
origin: { actor: ctx.actor ?? "api", reason: `retarget inbound links ${info.path} -> ${toRel}` },
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return { ...moved, dangling: plan.dangling, retargeted: { blocks: plan.blocks, docs: plan.docs } };
|
|
154
|
-
}
|
|
155
|
-
/** The `ApplyRequest` fields a document operation's context supplies to a follow-up changeset. */
|
|
156
|
-
function applyCtx(ctx) {
|
|
46
|
+
export function parseNewDoc(symbol, path, source) {
|
|
47
|
+
const adapter = adapterForPath(path);
|
|
48
|
+
const tree = adapter ? adapter.parse(source) : parseTree(source);
|
|
49
|
+
const fm = tree.children[0]?.type === "frontmatter" ? tree.children[0] : null;
|
|
50
|
+
const rest = fm ? tree.children.slice(1) : tree.children;
|
|
51
|
+
const toMut = (b) => ({ id: mintId("b"), type: b.type, raw: b.raw, trivia: b.trivia, attrs: b.attrs, children: b.children.map(toMut) });
|
|
157
52
|
return {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
53
|
+
docId: symbol,
|
|
54
|
+
path,
|
|
55
|
+
format: adapter?.format ?? "markdown",
|
|
56
|
+
leadingTrivia: tree.leadingTrivia,
|
|
57
|
+
frontmatterRaw: fm ? fm.raw + fm.trivia : null,
|
|
58
|
+
children: rest.map(toMut),
|
|
163
59
|
};
|
|
164
60
|
}
|
|
165
61
|
/**
|
|
166
62
|
* The `retarget_inbound` plan of a move `fromPath → toRel`: one coalesced,
|
|
167
63
|
* CAS-pinned `update` per deepest hit block, the blocks and docs it touches,
|
|
168
64
|
* and the inbound links it leaves dangling. Pure over the store's current
|
|
169
|
-
* blocks — the move itself changes no block — so the
|
|
170
|
-
*
|
|
65
|
+
* blocks — the move itself changes no block — so the effect planner's `move
|
|
66
|
+
* document` (§11.3) and the tool's `dangling`/`retargeted` report (computed
|
|
67
|
+
* before the commit) plan identically.
|
|
171
68
|
*/
|
|
172
|
-
function planInboundRetarget(store, docId, fromPath, toRel, inbound) {
|
|
69
|
+
export function planInboundRetarget(store, docId, fromPath, toRel, inbound) {
|
|
173
70
|
const byBlock = new Map();
|
|
174
71
|
for (const l of inbound)
|
|
175
72
|
if (l.block_id !== null && !byBlock.has(l.block_id))
|
|
@@ -215,61 +112,10 @@ function planInboundRetarget(store, docId, fromPath, toRel, inbound) {
|
|
|
215
112
|
const dangling = inbound.filter((l) => l.block_id === null || !rewritten.has(l.block_id));
|
|
216
113
|
return { ops, blocks, docs: [...docs], dangling };
|
|
217
114
|
}
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
throw new MutationError("doc_missing", `no document ${docRef}`);
|
|
223
|
-
const docStore = resolveDocStore(ctx);
|
|
224
|
-
if (ctx.dryRun) {
|
|
225
|
-
return { docId: info.docId, path: info.path, committed: false, diffs: { [info.path]: { before: docStore.read(info.path) ?? "", after: "" } } };
|
|
226
|
-
}
|
|
227
|
-
return underLock(ctx, () => {
|
|
228
|
-
const ts = nowOr(ctx);
|
|
229
|
-
store.write((db) => {
|
|
230
|
-
const commit = newCommit(db, { repoId: ctx.repoId, ts, origin: "api", actor: ctx.actor ?? null, reason: `delete ${info.path}` });
|
|
231
|
-
// Tombstone the document and its live blocks; FTS rows drop with the blocks.
|
|
232
|
-
ftsDeleteDoc(db, info.docId);
|
|
233
|
-
db.prepare("UPDATE blocks SET deleted_commit = ? WHERE doc_id = ? AND deleted_commit IS NULL").run(commit.commitId, info.docId);
|
|
234
|
-
db.prepare("UPDATE docs SET deleted_commit = ? WHERE doc_id = ?").run(commit.commitId, info.docId);
|
|
235
|
-
});
|
|
236
|
-
docStore.remove(info.path);
|
|
237
|
-
if (ctx.omgbaseDir)
|
|
238
|
-
docStore.clearStat(store, ctx.repoId, info.path);
|
|
239
|
-
return { docId: info.docId, path: info.path, committed: true };
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
/** docs_set_meta: surgical frontmatter patch (set/unset keys), re-ingested. */
|
|
243
|
-
export function docsSetMeta(store, ctx, docRef, patch) {
|
|
244
|
-
const info = findDocByRef(store, ctx.repoId, docRef);
|
|
245
|
-
if (!info)
|
|
246
|
-
throw new MutationError("doc_missing", `no document ${docRef}`);
|
|
247
|
-
const docStore = resolveDocStore(ctx);
|
|
248
|
-
const patched = () => {
|
|
249
|
-
const original = docStore.read(info.path) ?? "";
|
|
250
|
-
const { frontmatter, body } = splitFrontmatter(original);
|
|
251
|
-
const merged = { ...frontmatter, ...(patch.set ?? {}) };
|
|
252
|
-
for (const k of patch.unset ?? [])
|
|
253
|
-
delete merged[k];
|
|
254
|
-
return { original, content: composeFile(body, merged) };
|
|
255
|
-
};
|
|
256
|
-
if (ctx.dryRun) {
|
|
257
|
-
const { original, content } = patched();
|
|
258
|
-
return { docId: info.docId, path: info.path, committed: false, diffs: { [info.path]: { before: original, after: content } } };
|
|
259
|
-
}
|
|
260
|
-
return underLock(ctx, () => {
|
|
261
|
-
const { content } = patched();
|
|
262
|
-
docStore.write(info.path, content);
|
|
263
|
-
const ts = nowOr(ctx);
|
|
264
|
-
const res = ingestFile(store, ctx.repoId, info.path, content, { ts, origin: "api", actor: ctx.actor ?? null, reason: `set_meta ${info.path}`, resolveIds: makeReconcilingResolver(store, ctx.repoId, { ts, path: info.path }) });
|
|
265
|
-
if (ctx.omgbaseDir)
|
|
266
|
-
docStore.recordStat(store, ctx.repoId, info.path, content);
|
|
267
|
-
return { docId: res.docId, path: info.path, committed: true };
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
// Split a file's bytes into (frontmatter object, body markdown). Missing or
|
|
271
|
-
// malformed frontmatter yields an empty object and the whole content as body.
|
|
272
|
-
function splitFrontmatter(content) {
|
|
115
|
+
/** Split a file's bytes into (frontmatter object, body markdown) with
|
|
116
|
+
* `^---\r?\n([\s\S]*?)\r?\n---\r?\n?` (spec/mutate §6). Missing or malformed
|
|
117
|
+
* frontmatter yields an empty object and the whole content as body. */
|
|
118
|
+
export function splitFrontmatter(content) {
|
|
273
119
|
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(content);
|
|
274
120
|
if (!m)
|
|
275
121
|
return { frontmatter: {}, body: content };
|