@omgbase/core 0.11.1 → 0.15.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/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 +9 -5
- package/dist/src/index.js +14 -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/server.d.ts +22 -0
- package/dist/src/mcp/server.js +262 -185
- package/dist/src/mutate/apply.d.ts +93 -12
- package/dist/src/mutate/apply.js +203 -113
- 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 +331 -0
- package/dist/src/mutate/effects.js +981 -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 +200 -0
- package/dist/src/mutate/plan.js +472 -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/spec-versions.d.ts +5 -5
- 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/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Store } from "../core/store/store.js";
|
|
2
|
+
import { type InboundLink } from "../graph/inbound-links.js";
|
|
3
|
+
import { type DocStore } from "./doc-store.js";
|
|
4
|
+
import type { Effect, EffectAt, EffectExpect, EffectTarget } from "./effects.js";
|
|
5
|
+
import { type ExecuteRequest, type Plan, type Receipt } from "./plan.js";
|
|
6
|
+
/** What a builder returns: the ordered effects and the commit reason they carry (absent → the caller's `reason` argument). */
|
|
7
|
+
export interface Sugar {
|
|
8
|
+
effects: Effect[];
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
/** A live document as the builders address it: its id and its storage-form path (the reasons spell the path). */
|
|
12
|
+
export interface SugarDoc {
|
|
13
|
+
id: string;
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
/** A caller's guard as an effect guard: absent stays absent, an empty object stays an empty guard (the planner then pins what it needs). */
|
|
17
|
+
export declare function guardOf(e: {
|
|
18
|
+
content_hash?: string | undefined;
|
|
19
|
+
parent_children_hash?: string | undefined;
|
|
20
|
+
} | undefined): {
|
|
21
|
+
expect?: EffectExpect;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The pre-check a `create` / `move document` caller runs before planning
|
|
25
|
+
* (spec/surface §4 "The document operations", §9): `path_taken` exactly as the
|
|
26
|
+
* 2.x library operations raised it — the caller's message (`document already
|
|
27
|
+
* exists at <rel>` for a create, `a document already exists at <rel>` for a
|
|
28
|
+
* move), then `file already exists on disk at <rel>` — with NO `data`, so the
|
|
29
|
+
* planner's own `path_taken { path }` never fires. Shared by the MCP tools and
|
|
30
|
+
* the `omg` verbs (the Rust port mirrors it).
|
|
31
|
+
*/
|
|
32
|
+
export declare function requireAbsentPath(store: Store, repoId: string, where: {
|
|
33
|
+
rootPath?: string;
|
|
34
|
+
docStore?: DocStore;
|
|
35
|
+
}, rel: string, takenMessage: string): void;
|
|
36
|
+
/** `docs_create` / `omg new`: one `create` at the canonical path; reason `create <rel>`. */
|
|
37
|
+
export declare function createEffects(path: string, markdown: string, frontmatter?: Record<string, unknown>): Sugar;
|
|
38
|
+
/** `docs_move` / `omg mv`: one `move { target: document, to, retarget_inbound }`; reason `move <from> -> <to>`. */
|
|
39
|
+
export declare function moveDocEffects(doc: SugarDoc, toPath: string, retargetInbound?: boolean): Sugar;
|
|
40
|
+
/** `docs_delete` / `omg rm --doc`: one `remove { target: document }`; reason `delete <path>`. */
|
|
41
|
+
export declare function deleteDocEffects(doc: SugarDoc): Sugar;
|
|
42
|
+
/**
|
|
43
|
+
* `docs_set_meta` / `omg meta`: one `set`/`unset frontmatter.<k>` per key, `unset`
|
|
44
|
+
* winning a key named in both (the legacy merge spread `set` then deleted `unset`);
|
|
45
|
+
* reason `set_meta <path>`. An empty patch yields no effect — the caller refuses
|
|
46
|
+
* it (`filter_invalid` "pass `set` and/or `unset`", spec/surface §9).
|
|
47
|
+
*/
|
|
48
|
+
export declare function setMetaEffects(doc: SugarDoc, set: Record<string, unknown> | undefined, unset: readonly string[] | undefined): Sugar;
|
|
49
|
+
/** `docs_update` / `omg update <doc>`: one `set { document, content }`; the reason is the caller's. */
|
|
50
|
+
export declare function updateContentEffects(doc: Pick<SugarDoc, "id">, content: string, reason?: string): Sugar;
|
|
51
|
+
/** `blocks_insert` / `omg insert`: `insert into <document|block> at …`; the caller's `expect` is the destination-parent CAS. */
|
|
52
|
+
export declare function insertEffects(into: EffectTarget, at: EffectAt, markdown: string, expect?: {
|
|
53
|
+
parent_children_hash?: string | undefined;
|
|
54
|
+
}): Sugar;
|
|
55
|
+
/**
|
|
56
|
+
* `blocks_update` / `omg update <block>` / `omg edit`: `set markdown` and/or `set
|
|
57
|
+
* attrs.checked`. The caller's `expect` guards the FIRST effect (the content CAS
|
|
58
|
+
* is checked once, against the bytes the caller saw); a second effect is pinned
|
|
59
|
+
* by the planner to the block as the first left it. Nothing to set → no effect
|
|
60
|
+
* (the caller refuses it).
|
|
61
|
+
*/
|
|
62
|
+
export declare function updateBlockEffects(id: string, patch: {
|
|
63
|
+
markdown?: string | undefined;
|
|
64
|
+
checked?: boolean | undefined;
|
|
65
|
+
expect?: {
|
|
66
|
+
content_hash?: string | undefined;
|
|
67
|
+
parent_children_hash?: string | undefined;
|
|
68
|
+
} | undefined;
|
|
69
|
+
}): Sugar;
|
|
70
|
+
/** `blocks_move` / `omg move`: one `move { targets, into, at, expect }`. */
|
|
71
|
+
export declare function moveBlocksEffects(ids: readonly string[], into: EffectTarget, at: EffectAt, expect?: {
|
|
72
|
+
parent_children_hash?: string | undefined;
|
|
73
|
+
}): Sugar;
|
|
74
|
+
/** `blocks_remove` / `omg rm`: one `remove { targets }` (the planner pins each block). */
|
|
75
|
+
export declare function removeBlocksEffects(ids: readonly string[]): Sugar;
|
|
76
|
+
/** `blocks_split` / `omg split`: one `split { target, at }` (the planner pins the content CAS). */
|
|
77
|
+
export declare function splitEffects(id: string, at: readonly number[]): Sugar;
|
|
78
|
+
/** `blocks_merge` / `omg merge`: one `merge { targets, separator? }`. */
|
|
79
|
+
export declare function mergeEffects(ids: readonly string[], separator?: string): Sugar;
|
|
80
|
+
/** `blocks_check` / `omg done`: N × `set attrs.checked` (a block named twice is one write — the planner drops the duplicate). */
|
|
81
|
+
export declare function checkEffects(ids: readonly string[], checked: boolean): Sugar;
|
|
82
|
+
/** `nodes_set` / `omg node set`: one `set { node, field: prop, value }`. */
|
|
83
|
+
export declare function nodeSetEffects(nodeId: string, prop: string, value: string): Sugar;
|
|
84
|
+
/** `blocks_append` / `omg append`: `insert at end` into a heading's section or a container block (the caller decided which by the block's kind). */
|
|
85
|
+
export declare function appendEffects(into: EffectTarget, text: string): Sugar;
|
|
86
|
+
/** `docs_append`: `insert at end` into the document's top level or a resolved section. */
|
|
87
|
+
export declare function docsAppendEffects(into: EffectTarget, text: string): Sugar;
|
|
88
|
+
/** `links_retarget` / `links_repair` / `omg retarget --apply`: one `retarget { repairs, path_glob? }` under the calling tool's name. */
|
|
89
|
+
export declare function retargetEffects(repairs: readonly {
|
|
90
|
+
from: string;
|
|
91
|
+
to: string;
|
|
92
|
+
}[], pathGlob: string | undefined, reason: "links_retarget" | "links_repair"): Sugar;
|
|
93
|
+
/** The request half a caller supplies: where to write and under whom (`ExecuteRequest` minus the origin's reason, which the sugar carries). */
|
|
94
|
+
export type SugarRequest = Omit<ExecuteRequest, "origin"> & {
|
|
95
|
+
actor: string | null;
|
|
96
|
+
reason?: string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Plan + execute a builder's effects as one request (`applyEffectsDetailed`: one
|
|
100
|
+
* lock, one transaction) under `actor` and the sugar's reason — `request.reason`
|
|
101
|
+
* wins when given (a `--reason`, a `docs_update` argument) — and report any
|
|
102
|
+
* planner failure without its `effect_index`. `dryRun` plans only (the CLI's
|
|
103
|
+
* `--dry-run`, spec/cli §3.6: every dry run is a plan and burns no id).
|
|
104
|
+
*/
|
|
105
|
+
export declare function applySugar(store: Store, request: SugarRequest & {
|
|
106
|
+
dryRun?: boolean;
|
|
107
|
+
}, sugar: Sugar): {
|
|
108
|
+
plan: Plan;
|
|
109
|
+
receipt?: Receipt;
|
|
110
|
+
};
|
|
111
|
+
/** The document-operation result the `omg` document verbs and the fixture `docs` step record (`spec/mutate` §9): `{ id, path, committed }`, the path in the storage form. */
|
|
112
|
+
export interface DocOpOutcome {
|
|
113
|
+
id: string;
|
|
114
|
+
path: string;
|
|
115
|
+
committed: true;
|
|
116
|
+
}
|
|
117
|
+
/** `docs_create`'s document is the receipt's first revision (`new_ids["$new.0"]` is the same id); every other operation's is the one addressed. */
|
|
118
|
+
export declare function createdDoc(receipt: Receipt, path: string): DocOpOutcome;
|
|
119
|
+
export declare function docOutcome(doc: SugarDoc, path?: string): DocOpOutcome;
|
|
120
|
+
/** What a move reports about the inbound links (spec/mutate §6 `docs_move`; the `retargeted`/`dangling` extras of the tool and the verb). */
|
|
121
|
+
export interface MoveLinks {
|
|
122
|
+
/** inbound links still naming the OLD path after the move — every link when the retarget was opted out, else the frontmatter relations a move never rewrites */
|
|
123
|
+
dangling: InboundLink[];
|
|
124
|
+
/** the source blocks rewritten and their documents; `null` when the retarget was opted out or nothing linked in */
|
|
125
|
+
retargeted: {
|
|
126
|
+
blocks: string[];
|
|
127
|
+
docs: string[];
|
|
128
|
+
} | null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Who links to the document BY PATH as written, and what the move's retarget
|
|
132
|
+
* rewrites — computed BEFORE the move from the current blocks (`planInboundRetarget`
|
|
133
|
+
* is pure, so it equals what the planner lowers for the same `move` effect).
|
|
134
|
+
*/
|
|
135
|
+
export declare function moveLinks(store: Store, repoId: string, doc: SugarDoc, toPath: string, retargetInbound?: boolean): MoveLinks;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { findDoc } from "../core/read/reader.js";
|
|
2
|
+
import { inboundLinksTo } from "../graph/inbound-links.js";
|
|
3
|
+
import { canonicalDocPath, planInboundRetarget } from "./docs.js";
|
|
4
|
+
import { resolveDocStore } from "./doc-store.js";
|
|
5
|
+
import { MutationError } from "./tree.js";
|
|
6
|
+
import { applyEffectsDetailed, stripEffectIndex } from "./plan.js";
|
|
7
|
+
const documentTarget = (id) => ({ kind: "document", id });
|
|
8
|
+
const blockTarget = (id) => ({ kind: "block", id });
|
|
9
|
+
/** A caller's guard as an effect guard: absent stays absent, an empty object stays an empty guard (the planner then pins what it needs). */
|
|
10
|
+
export function guardOf(e) {
|
|
11
|
+
if (!e)
|
|
12
|
+
return {};
|
|
13
|
+
const expect = {};
|
|
14
|
+
if (e.content_hash !== undefined)
|
|
15
|
+
expect.content_hash = e.content_hash;
|
|
16
|
+
if (e.parent_children_hash !== undefined)
|
|
17
|
+
expect.parent_children_hash = e.parent_children_hash;
|
|
18
|
+
return { expect };
|
|
19
|
+
}
|
|
20
|
+
// ---- the document operations (spec/mutate §6) ---------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* The pre-check a `create` / `move document` caller runs before planning
|
|
23
|
+
* (spec/surface §4 "The document operations", §9): `path_taken` exactly as the
|
|
24
|
+
* 2.x library operations raised it — the caller's message (`document already
|
|
25
|
+
* exists at <rel>` for a create, `a document already exists at <rel>` for a
|
|
26
|
+
* move), then `file already exists on disk at <rel>` — with NO `data`, so the
|
|
27
|
+
* planner's own `path_taken { path }` never fires. Shared by the MCP tools and
|
|
28
|
+
* the `omg` verbs (the Rust port mirrors it).
|
|
29
|
+
*/
|
|
30
|
+
export function requireAbsentPath(store, repoId, where, rel, takenMessage) {
|
|
31
|
+
if (findDoc(store, { repoId, path: rel }))
|
|
32
|
+
throw new MutationError("path_taken", takenMessage);
|
|
33
|
+
if (resolveDocStore(where).exists(rel))
|
|
34
|
+
throw new MutationError("path_taken", `file already exists on disk at ${rel}`);
|
|
35
|
+
}
|
|
36
|
+
/** `docs_create` / `omg new`: one `create` at the canonical path; reason `create <rel>`. */
|
|
37
|
+
export function createEffects(path, markdown, frontmatter) {
|
|
38
|
+
const rel = canonicalDocPath(path);
|
|
39
|
+
return { effects: [{ op: "create", path: rel, markdown, ...(frontmatter !== undefined ? { frontmatter } : {}) }], reason: `create ${rel}` };
|
|
40
|
+
}
|
|
41
|
+
/** `docs_move` / `omg mv`: one `move { target: document, to, retarget_inbound }`; reason `move <from> -> <to>`. */
|
|
42
|
+
export function moveDocEffects(doc, toPath, retargetInbound = true) {
|
|
43
|
+
const to = canonicalDocPath(toPath);
|
|
44
|
+
return { effects: [{ op: "move", target: documentTarget(doc.id), to, ...(retargetInbound ? {} : { retarget_inbound: false }) }], reason: `move ${doc.path} -> ${to}` };
|
|
45
|
+
}
|
|
46
|
+
/** `docs_delete` / `omg rm --doc`: one `remove { target: document }`; reason `delete <path>`. */
|
|
47
|
+
export function deleteDocEffects(doc) {
|
|
48
|
+
return { effects: [{ op: "remove", target: documentTarget(doc.id) }], reason: `delete ${doc.path}` };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `docs_set_meta` / `omg meta`: one `set`/`unset frontmatter.<k>` per key, `unset`
|
|
52
|
+
* winning a key named in both (the legacy merge spread `set` then deleted `unset`);
|
|
53
|
+
* reason `set_meta <path>`. An empty patch yields no effect — the caller refuses
|
|
54
|
+
* it (`filter_invalid` "pass `set` and/or `unset`", spec/surface §9).
|
|
55
|
+
*/
|
|
56
|
+
export function setMetaEffects(doc, set, unset) {
|
|
57
|
+
const gone = unset ?? [];
|
|
58
|
+
const target = documentTarget(doc.id);
|
|
59
|
+
const effects = [
|
|
60
|
+
...Object.entries(set ?? {}).filter(([k]) => !gone.includes(k)).map(([k, value]) => ({ op: "set", target, field: `frontmatter.${k}`, value })),
|
|
61
|
+
...gone.map((k) => ({ op: "unset", target, field: `frontmatter.${k}` })),
|
|
62
|
+
];
|
|
63
|
+
return { effects, reason: `set_meta ${doc.path}` };
|
|
64
|
+
}
|
|
65
|
+
/** `docs_update` / `omg update <doc>`: one `set { document, content }`; the reason is the caller's. */
|
|
66
|
+
export function updateContentEffects(doc, content, reason) {
|
|
67
|
+
return { effects: [{ op: "set", target: documentTarget(doc.id), field: "content", value: content }], ...(reason !== undefined ? { reason } : {}) };
|
|
68
|
+
}
|
|
69
|
+
// ---- the block-level tools (spec/surface §4) ----------------------------------------------
|
|
70
|
+
/** `blocks_insert` / `omg insert`: `insert into <document|block> at …`; the caller's `expect` is the destination-parent CAS. */
|
|
71
|
+
export function insertEffects(into, at, markdown, expect) {
|
|
72
|
+
return { effects: [{ op: "insert", into, at, markdown, ...guardOf(expect) }], reason: "blocks_insert" };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `blocks_update` / `omg update <block>` / `omg edit`: `set markdown` and/or `set
|
|
76
|
+
* attrs.checked`. The caller's `expect` guards the FIRST effect (the content CAS
|
|
77
|
+
* is checked once, against the bytes the caller saw); a second effect is pinned
|
|
78
|
+
* by the planner to the block as the first left it. Nothing to set → no effect
|
|
79
|
+
* (the caller refuses it).
|
|
80
|
+
*/
|
|
81
|
+
export function updateBlockEffects(id, patch) {
|
|
82
|
+
const target = blockTarget(id);
|
|
83
|
+
const expect = guardOf(patch.expect);
|
|
84
|
+
const effects = [];
|
|
85
|
+
if (patch.markdown !== undefined)
|
|
86
|
+
effects.push({ op: "set", target, field: "markdown", value: patch.markdown, ...expect });
|
|
87
|
+
if (patch.checked !== undefined)
|
|
88
|
+
effects.push({ op: "set", target, field: "attrs.checked", value: patch.checked, ...(effects.length === 0 ? expect : {}) });
|
|
89
|
+
return { effects, reason: "blocks_update" };
|
|
90
|
+
}
|
|
91
|
+
/** `blocks_move` / `omg move`: one `move { targets, into, at, expect }`. */
|
|
92
|
+
export function moveBlocksEffects(ids, into, at, expect) {
|
|
93
|
+
return { effects: [{ op: "move", targets: ids.map(blockTarget), into, at, ...guardOf(expect) }], reason: "blocks_move" };
|
|
94
|
+
}
|
|
95
|
+
/** `blocks_remove` / `omg rm`: one `remove { targets }` (the planner pins each block). */
|
|
96
|
+
export function removeBlocksEffects(ids) {
|
|
97
|
+
return { effects: [{ op: "remove", targets: ids.map(blockTarget) }], reason: "blocks_remove" };
|
|
98
|
+
}
|
|
99
|
+
/** `blocks_split` / `omg split`: one `split { target, at }` (the planner pins the content CAS). */
|
|
100
|
+
export function splitEffects(id, at) {
|
|
101
|
+
return { effects: [{ op: "split", target: blockTarget(id), at: [...at] }], reason: "blocks_split" };
|
|
102
|
+
}
|
|
103
|
+
/** `blocks_merge` / `omg merge`: one `merge { targets, separator? }`. */
|
|
104
|
+
export function mergeEffects(ids, separator) {
|
|
105
|
+
return { effects: [{ op: "merge", targets: ids.map(blockTarget), ...(separator !== undefined ? { separator } : {}) }], reason: "blocks_merge" };
|
|
106
|
+
}
|
|
107
|
+
/** `blocks_check` / `omg done`: N × `set attrs.checked` (a block named twice is one write — the planner drops the duplicate). */
|
|
108
|
+
export function checkEffects(ids, checked) {
|
|
109
|
+
return { effects: ids.map((id) => ({ op: "set", target: blockTarget(id), field: "attrs.checked", value: checked })), reason: "blocks_check" };
|
|
110
|
+
}
|
|
111
|
+
/** `nodes_set` / `omg node set`: one `set { node, field: prop, value }`. */
|
|
112
|
+
export function nodeSetEffects(nodeId, prop, value) {
|
|
113
|
+
return { effects: [{ op: "set", target: { kind: "node", id: nodeId }, field: prop, value }], reason: "nodes_set" };
|
|
114
|
+
}
|
|
115
|
+
/** `blocks_append` / `omg append`: `insert at end` into a heading's section or a container block (the caller decided which by the block's kind). */
|
|
116
|
+
export function appendEffects(into, text) {
|
|
117
|
+
return { effects: [{ op: "insert", into, at: "end", markdown: text }], reason: "blocks_append" };
|
|
118
|
+
}
|
|
119
|
+
/** `docs_append`: `insert at end` into the document's top level or a resolved section. */
|
|
120
|
+
export function docsAppendEffects(into, text) {
|
|
121
|
+
return { effects: [{ op: "insert", into, at: "end", markdown: text }], reason: "docs_append" };
|
|
122
|
+
}
|
|
123
|
+
/** `links_retarget` / `links_repair` / `omg retarget --apply`: one `retarget { repairs, path_glob? }` under the calling tool's name. */
|
|
124
|
+
export function retargetEffects(repairs, pathGlob, reason) {
|
|
125
|
+
return { effects: [{ op: "retarget", repairs: repairs.map((r) => ({ from: r.from, to: r.to })), ...(pathGlob !== undefined ? { path_glob: pathGlob } : {}) }], reason };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Plan + execute a builder's effects as one request (`applyEffectsDetailed`: one
|
|
129
|
+
* lock, one transaction) under `actor` and the sugar's reason — `request.reason`
|
|
130
|
+
* wins when given (a `--reason`, a `docs_update` argument) — and report any
|
|
131
|
+
* planner failure without its `effect_index`. `dryRun` plans only (the CLI's
|
|
132
|
+
* `--dry-run`, spec/cli §3.6: every dry run is a plan and burns no id).
|
|
133
|
+
*/
|
|
134
|
+
export function applySugar(store, request, sugar) {
|
|
135
|
+
const { actor, reason, dryRun, ...rest } = request;
|
|
136
|
+
const chosen = reason ?? sugar.reason;
|
|
137
|
+
try {
|
|
138
|
+
return applyEffectsDetailed(store, { ...rest, effects: sugar.effects, origin: { actor, ...(chosen !== undefined ? { reason: chosen } : {}) }, ...(dryRun ? { dryRun: true } : {}) });
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
throw stripEffectIndex(e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** `docs_create`'s document is the receipt's first revision (`new_ids["$new.0"]` is the same id); every other operation's is the one addressed. */
|
|
145
|
+
export function createdDoc(receipt, path) {
|
|
146
|
+
const id = receipt.new_ids["$new.0"] ?? receipt.revisions[0]?.id;
|
|
147
|
+
if (id === undefined)
|
|
148
|
+
throw new Error("createdDoc: the receipt names no created document");
|
|
149
|
+
return { id, path, committed: true };
|
|
150
|
+
}
|
|
151
|
+
export function docOutcome(doc, path = doc.path) {
|
|
152
|
+
return { id: doc.id, path, committed: true };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Who links to the document BY PATH as written, and what the move's retarget
|
|
156
|
+
* rewrites — computed BEFORE the move from the current blocks (`planInboundRetarget`
|
|
157
|
+
* is pure, so it equals what the planner lowers for the same `move` effect).
|
|
158
|
+
*/
|
|
159
|
+
export function moveLinks(store, repoId, doc, toPath, retargetInbound = true) {
|
|
160
|
+
const to = canonicalDocPath(toPath);
|
|
161
|
+
const inbound = inboundLinksTo(store, repoId, doc.id, doc.path);
|
|
162
|
+
if (!retargetInbound || inbound.length === 0)
|
|
163
|
+
return { dangling: inbound, retargeted: null };
|
|
164
|
+
const plan = planInboundRetarget(store, doc.id, doc.path, to, inbound);
|
|
165
|
+
return { dangling: plan.dangling, retargeted: { blocks: plan.blocks, docs: plan.docs } };
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=sugar.js.map
|
|
@@ -16,10 +16,12 @@ export interface MutDoc {
|
|
|
16
16
|
frontmatterRaw: string | null;
|
|
17
17
|
children: MutBlock[];
|
|
18
18
|
}
|
|
19
|
+
/** The codes spec/mutate §8 lists (2.3 adds `conflicting_effects`, `unsupported_plan_version`, `plan_mismatch`); `filter_invalid` is the planner's shape error, spec/surface §4's envelope. */
|
|
20
|
+
export type MutationErrorCode = "stale_expectation" | "block_missing" | "target_missing" | "parent_missing" | "doc_missing" | "cycle_move" | "not_contiguous" | "type_mismatch" | "path_taken" | "node_not_editable" | "stale_plan" | "plan_not_convergent" | "sync_conflict" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "filter_invalid";
|
|
19
21
|
export declare class MutationError extends Error {
|
|
20
|
-
code: string;
|
|
22
|
+
code: MutationErrorCode | string;
|
|
21
23
|
data: Record<string, unknown>;
|
|
22
|
-
constructor(code: string, message: string, data?: Record<string, unknown>);
|
|
24
|
+
constructor(code: MutationErrorCode | string, message: string, data?: Record<string, unknown>);
|
|
23
25
|
}
|
|
24
26
|
/** Find a block by id anywhere in the doc; returns the block + its parent list. */
|
|
25
27
|
export declare function locate(doc: MutDoc, blockId: string): {
|
|
@@ -2,13 +2,13 @@ export declare const SPEC_VERSIONS: {
|
|
|
2
2
|
readonly oqx: "0.20";
|
|
3
3
|
readonly format: "0.2";
|
|
4
4
|
readonly reconcile: "2.3";
|
|
5
|
-
readonly store: "
|
|
5
|
+
readonly store: "14.0";
|
|
6
6
|
readonly properties: "1.1";
|
|
7
7
|
readonly graph: "1.1";
|
|
8
8
|
readonly search: "2.0";
|
|
9
|
-
readonly mutate: "
|
|
10
|
-
readonly sync: "2.
|
|
11
|
-
readonly surface: "
|
|
12
|
-
readonly cli: "
|
|
9
|
+
readonly mutate: "3.0";
|
|
10
|
+
readonly sync: "2.2";
|
|
11
|
+
readonly surface: "4.0";
|
|
12
|
+
readonly cli: "4.0";
|
|
13
13
|
};
|
|
14
14
|
export type SpecName = keyof typeof SPEC_VERSIONS;
|
|
@@ -13,13 +13,13 @@ export const SPEC_VERSIONS = {
|
|
|
13
13
|
oqx: LANGUAGE_VERSION,
|
|
14
14
|
format: "0.2",
|
|
15
15
|
reconcile: "2.3",
|
|
16
|
-
store: "
|
|
16
|
+
store: "14.0",
|
|
17
17
|
properties: "1.1",
|
|
18
18
|
graph: "1.1",
|
|
19
19
|
search: "2.0",
|
|
20
|
-
mutate: "
|
|
21
|
-
sync: "2.
|
|
22
|
-
surface: "
|
|
23
|
-
cli: "
|
|
20
|
+
mutate: "3.0",
|
|
21
|
+
sync: "2.2",
|
|
22
|
+
surface: "4.0",
|
|
23
|
+
cli: "4.0",
|
|
24
24
|
};
|
|
25
25
|
//# sourceMappingURL=spec-versions.js.map
|
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
import type { ApplyResult } from "./mutate/apply.js";
|
|
2
|
-
import type { DocOpResult, DocMoveResult } from "./mutate/docs.js";
|
|
3
2
|
import type { RetargetHit } from "./mutate/macros.js";
|
|
4
|
-
import type {
|
|
5
|
-
import type { DocsUpdateResult } from "./mutate/plan-update.js";
|
|
3
|
+
import type { Plan, Receipt } from "./mutate/plan.js";
|
|
6
4
|
import type { InboundLink } from "./graph/inbound-links.js";
|
|
7
5
|
import type { ObserveResult, ObserveDeleteResult } from "./sync/observe.js";
|
|
8
6
|
import type { SweepResult } from "./sync/freshness.js";
|
|
9
7
|
import type { TextSearchResult } from "./search/text.js";
|
|
10
8
|
import type { ResolveHit } from "./search/resolve.js";
|
|
11
|
-
/** `spec/mutate` §4's result: `revisions[].path` and the dry-run `diffs` keys. */
|
|
9
|
+
/** `spec/mutate` §4's kernel result (the library's `apply`; no tool returns it since 4.0): `revisions[].path` and the dry-run `diffs` keys. */
|
|
12
10
|
export declare function surfaceApplyResult<T extends ApplyResult>(res: T): T;
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* dry-run `diffs` re-keyed; every other key (`committed`, `deleted`, …) as is. */
|
|
17
|
-
export type SurfaceDocOpResult<T extends DocOpResult> = Omit<T, "docId" | "path"> & {
|
|
18
|
-
id: string;
|
|
19
|
-
path: string;
|
|
20
|
-
};
|
|
21
|
-
/** `docs_create` / `docs_delete` / `docs_set_meta`. */
|
|
22
|
-
export declare function surfaceDocOpResult<T extends DocOpResult>(res: T): SurfaceDocOpResult<T>;
|
|
23
|
-
/** An inbound link on the wire (`docs_move.dangling[]`): an edge row under the
|
|
24
|
-
* `edges` target's names (spec/surface §1.2) — `doc_id`, `path` (rooted),
|
|
25
|
-
* `block_id`, `target` (as authored), `anchor`, `field?`. */
|
|
11
|
+
/** An inbound link on the wire (`docs_move.dangling[]`, `omg mv`): a link row
|
|
12
|
+
* under the `links` target's names (spec/surface §1.2) — `doc_id`, `path`
|
|
13
|
+
* (rooted), `block_id`, `target` (as authored), `anchor`, `field?`. */
|
|
26
14
|
export interface SurfaceInboundLink {
|
|
27
15
|
doc_id: string;
|
|
28
16
|
path: string;
|
|
@@ -32,11 +20,6 @@ export interface SurfaceInboundLink {
|
|
|
32
20
|
field?: string;
|
|
33
21
|
}
|
|
34
22
|
export declare function surfaceInboundLink(l: InboundLink): SurfaceInboundLink;
|
|
35
|
-
/** `docs_move`: the document-op fields plus each dangling link (its `target`
|
|
36
|
-
* stays as authored — that is what the link says). */
|
|
37
|
-
export declare function surfaceDocMoveResult(res: DocMoveResult): Omit<SurfaceDocOpResult<DocMoveResult>, "dangling"> & {
|
|
38
|
-
dangling: SurfaceInboundLink[];
|
|
39
|
-
};
|
|
40
23
|
/** An observe outcome on the wire (spec/surface §4, 3.0): `spec/store` §5's
|
|
41
24
|
* `{ docId, path, … }` (unchanged there) renamed to the document's row
|
|
42
25
|
* attributes `id`/`path`, the path rooted. */
|
|
@@ -58,9 +41,17 @@ export declare function surfaceTextSearch(res: TextSearchResult): TextSearchResu
|
|
|
58
41
|
export declare function surfaceResolveHits(hits: ResolveHit[]): ResolveHit[];
|
|
59
42
|
/** `links_retarget` / `links_repair` hits (`path` is the block's document). */
|
|
60
43
|
export declare function surfaceRetargetHits(hits: RetargetHit[]): RetargetHit[];
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
/**
|
|
45
|
+
* A Plan on the wire (spec/surface §4, 3.1): the executable core — `effects`,
|
|
46
|
+
* `opset`, `preconditions`, `new_ids`, `plan_id` — is returned VERBATIM in the
|
|
47
|
+
* storage form, because `plan_id` digests exactly those bytes and the client
|
|
48
|
+
* hands the object back to `execute` (spec/mutate §11.5; the one result the
|
|
49
|
+
* path rule exempts, like `dangling[].target`). Only the un-digested `preview`
|
|
50
|
+
* is rooted: its `revisions[].path`, `diffs` keys and `retarget[].hits[].path`
|
|
51
|
+
* (3.0; `identity` is keyed by document id and carries no path).
|
|
52
|
+
*/
|
|
53
|
+
export declare function surfacePlan(plan: Plan): Plan;
|
|
54
|
+
/** A Receipt on the wire: `revisions[].path` rooted (the same shape as an apply revision). */
|
|
55
|
+
export declare function surfaceReceipt(receipt: Receipt): Receipt;
|
|
65
56
|
/** `omg sync`'s one-shot sweep: the ingested / suppressed / deleted / conflicted paths. */
|
|
66
57
|
export declare function surfaceSweepResult(res: SweepResult): SweepResult;
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
// Row names (spec/surface §1.2, 3.0): a result that renders a document, block
|
|
12
12
|
// or edge row spells it the way the query binding does — `id`, `path`,
|
|
13
13
|
// `hash`, `raw`, `updated_at`, plain `doc_id`/`block_id` foreign keys. The
|
|
14
|
-
// shapes other specs own under their own names — `spec/
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// shapes other specs own under their own names — `spec/store`'s observe
|
|
15
|
+
// outcome, a move's dangling inbound links — are re-shaped HERE too
|
|
16
|
+
// (`SurfaceObserveResult`, `surfaceInboundLink`, …).
|
|
17
17
|
//
|
|
18
18
|
// Both the MCP server and the `omg` verbs call these, so the two clients
|
|
19
19
|
// cannot drift. Each function returns a new value; the input is not mutated.
|
|
20
20
|
import { referencePath, referenceKeyed } from "./core/paths.js";
|
|
21
|
-
/** `spec/mutate` §4's result: `revisions[].path` and the dry-run `diffs` keys. */
|
|
21
|
+
/** `spec/mutate` §4's kernel result (the library's `apply`; no tool returns it since 4.0): `revisions[].path` and the dry-run `diffs` keys. */
|
|
22
22
|
export function surfaceApplyResult(res) {
|
|
23
23
|
return {
|
|
24
24
|
...res,
|
|
@@ -26,20 +26,10 @@ export function surfaceApplyResult(res) {
|
|
|
26
26
|
...(res.diffs ? { diffs: referenceKeyed(res.diffs) } : {}),
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
/** `docs_create` / `docs_delete` / `docs_set_meta`. */
|
|
30
|
-
export function surfaceDocOpResult(res) {
|
|
31
|
-
const { docId, path, ...rest } = res;
|
|
32
|
-
return { id: docId, path: referencePath(path), ...rest, ...(res.diffs ? { diffs: referenceKeyed(res.diffs) } : {}) };
|
|
33
|
-
}
|
|
34
29
|
export function surfaceInboundLink(l) {
|
|
35
30
|
const { path, ...rest } = l;
|
|
36
31
|
return { doc_id: rest.doc_id, path: referencePath(path), block_id: rest.block_id, target: rest.target, anchor: rest.anchor, ...(rest.field !== undefined ? { field: rest.field } : {}) };
|
|
37
32
|
}
|
|
38
|
-
/** `docs_move`: the document-op fields plus each dangling link (its `target`
|
|
39
|
-
* stays as authored — that is what the link says). */
|
|
40
|
-
export function surfaceDocMoveResult(res) {
|
|
41
|
-
return { ...surfaceDocOpResult(res), dangling: res.dangling.map(surfaceInboundLink) };
|
|
42
|
-
}
|
|
43
33
|
/** `observe` / `observe_many` outcomes. */
|
|
44
34
|
export function surfaceObserveResult(res) {
|
|
45
35
|
const { docId, path, ...rest } = res;
|
|
@@ -66,17 +56,31 @@ export function surfaceResolveHits(hits) {
|
|
|
66
56
|
export function surfaceRetargetHits(hits) {
|
|
67
57
|
return hits.map((h) => ({ ...h, path: referencePath(h.path) }));
|
|
68
58
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
59
|
+
/**
|
|
60
|
+
* A Plan on the wire (spec/surface §4, 3.1): the executable core — `effects`,
|
|
61
|
+
* `opset`, `preconditions`, `new_ids`, `plan_id` — is returned VERBATIM in the
|
|
62
|
+
* storage form, because `plan_id` digests exactly those bytes and the client
|
|
63
|
+
* hands the object back to `execute` (spec/mutate §11.5; the one result the
|
|
64
|
+
* path rule exempts, like `dangling[].target`). Only the un-digested `preview`
|
|
65
|
+
* is rooted: its `revisions[].path`, `diffs` keys and `retarget[].hits[].path`
|
|
66
|
+
* (3.0; `identity` is keyed by document id and carries no path).
|
|
67
|
+
*/
|
|
68
|
+
export function surfacePlan(plan) {
|
|
69
|
+
if (!plan.preview)
|
|
70
|
+
return plan;
|
|
71
71
|
return {
|
|
72
|
-
...
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
...plan,
|
|
73
|
+
preview: {
|
|
74
|
+
...plan.preview,
|
|
75
|
+
revisions: plan.preview.revisions.map((r) => ({ ...r, path: referencePath(r.path) })),
|
|
76
|
+
diffs: referenceKeyed(plan.preview.diffs),
|
|
77
|
+
...(plan.preview.retarget !== undefined ? { retarget: plan.preview.retarget.map((r) => ({ ...r, hits: surfaceRetargetHits(r.hits) })) } : {}),
|
|
78
|
+
},
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
|
-
/** `
|
|
78
|
-
export function
|
|
79
|
-
return {
|
|
81
|
+
/** A Receipt on the wire: `revisions[].path` rooted (the same shape as an apply revision). */
|
|
82
|
+
export function surfaceReceipt(receipt) {
|
|
83
|
+
return { ...receipt, revisions: receipt.revisions.map((r) => ({ ...r, path: referencePath(r.path) })) };
|
|
80
84
|
}
|
|
81
85
|
/** `omg sync`'s one-shot sweep: the ingested / suppressed / deleted / conflicted paths. */
|
|
82
86
|
export function surfaceSweepResult(res) {
|
|
@@ -86,6 +90,7 @@ export function surfaceSweepResult(res) {
|
|
|
86
90
|
suppressed: res.suppressed.map(referencePath),
|
|
87
91
|
deleted: res.deleted.map(referencePath),
|
|
88
92
|
conflicted: res.conflicted.map(referencePath),
|
|
93
|
+
materialized: res.materialized.map(referencePath),
|
|
89
94
|
};
|
|
90
95
|
}
|
|
91
96
|
//# sourceMappingURL=surface-paths.js.map
|
|
@@ -29,8 +29,11 @@ export declare function processCheckpoint(store: Store, repoId: string, rootPath
|
|
|
29
29
|
}): CheckpointResult;
|
|
30
30
|
/**
|
|
31
31
|
* Turn a batch's outcomes into the checkpoint row + result, then sweep the
|
|
32
|
-
* resurrection pool
|
|
33
|
-
*
|
|
32
|
+
* resurrection pool — the row and the sweep in ONE transaction (spec/sync §4.1),
|
|
33
|
+
* so a checkpoint is never recorded without its sweep or swept without its row
|
|
34
|
+
* (the member commits before it are one transaction each, as before). Shared
|
|
35
|
+
* with the external-source driver (driver.ts), which obtains its bytes
|
|
36
|
+
* asynchronously but records its checkpoint the same way.
|
|
34
37
|
*/
|
|
35
38
|
export declare function finishCheckpoint(store: Store, repoId: string, outcomes: BatchOutcome[], opts: {
|
|
36
39
|
ts: string;
|
|
@@ -20,8 +20,11 @@ export function processCheckpoint(store, repoId, rootPath, changes, opts = {}) {
|
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Turn a batch's outcomes into the checkpoint row + result, then sweep the
|
|
23
|
-
* resurrection pool
|
|
24
|
-
*
|
|
23
|
+
* resurrection pool — the row and the sweep in ONE transaction (spec/sync §4.1),
|
|
24
|
+
* so a checkpoint is never recorded without its sweep or swept without its row
|
|
25
|
+
* (the member commits before it are one transaction each, as before). Shared
|
|
26
|
+
* with the external-source driver (driver.ts), which obtains its bytes
|
|
27
|
+
* asynchronously but records its checkpoint the same way.
|
|
25
28
|
*/
|
|
26
29
|
export function finishCheckpoint(store, repoId, outcomes, opts) {
|
|
27
30
|
const checkpointId = mintId("cp");
|
|
@@ -45,10 +48,11 @@ export function finishCheckpoint(store, repoId, outcomes, opts) {
|
|
|
45
48
|
ingested.push(o.path);
|
|
46
49
|
fileEntries.push([o.path, o.oldHashHex, o.newHashHex]);
|
|
47
50
|
}
|
|
48
|
-
store.db
|
|
49
|
-
.prepare("INSERT INTO checkpoints (id, repo_id, ts, files, git_head) VALUES (?, ?, ?, ?, ?)")
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
store.write((db) => {
|
|
52
|
+
db.prepare("INSERT INTO checkpoints (id, repo_id, ts, files, git_head) VALUES (?, ?, ?, ?, ?)")
|
|
53
|
+
.run(checkpointId, repoId, opts.ts, JSON.stringify(fileEntries), opts.gitHead);
|
|
54
|
+
sweepResurrectionPool(store, opts.ts);
|
|
55
|
+
});
|
|
52
56
|
return { checkpointId, ingested, suppressed, deleted, conflicted };
|
|
53
57
|
}
|
|
54
58
|
//# sourceMappingURL=checkpoint.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
2
|
import { type CheckpointResult } from "./checkpoint.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { type SyncFs } from "./fs-util.js";
|
|
4
|
+
import { type DocStore } from "../mutate/doc-store.js";
|
|
5
|
+
export { recordStat, recordFileStat } from "./stat-cache.js";
|
|
4
6
|
/** One `file_stats` row (spec/sync §4.3 "the repo's cache rows"). */
|
|
5
7
|
export interface StatCacheRow {
|
|
6
8
|
path: string;
|
|
@@ -44,22 +46,23 @@ export interface SweepResult extends CheckpointResult {
|
|
|
44
46
|
scanned: number;
|
|
45
47
|
candidates: number;
|
|
46
48
|
changed: boolean;
|
|
49
|
+
/** step 0 (spec/sync §4.3, 2.2): the paths whose pending materialization rows were replayed before the walk. */
|
|
50
|
+
materialized: string[];
|
|
47
51
|
}
|
|
48
|
-
/** Write a file's stat cache row from a stat (`INSERT … ON CONFLICT DO UPDATE`); a null stat (the
|
|
49
|
-
* file vanished) deletes the row. The DB half of `record_file_stat` (spec/sync §4.3 step 4). */
|
|
50
|
-
export declare function recordStat(store: Store, repoId: string, path: string, st: FileStat | null, hash: Buffer): void;
|
|
51
|
-
/** Record/refresh a file's stat cache row from its absolute path. Call after any ingest of the file. */
|
|
52
|
-
export declare function recordFileStat(store: Store, repoId: string, path: string, abs: string, hash: Buffer): void;
|
|
53
52
|
/**
|
|
54
53
|
* Run the freshness sweep for a repo (spec/sync §4.3). Returns the checkpoint
|
|
55
54
|
* result plus scan counters. Idempotent; safe alongside a live watcher
|
|
56
55
|
* (hash-based echo suppression makes a double ingest a no-op). `opts.ts` pins
|
|
57
|
-
* the batch timestamp (default now); `opts.fs` the filesystem seam
|
|
56
|
+
* the batch timestamp (default now); `opts.fs` the filesystem seam;
|
|
57
|
+
* `opts.docStore` the write target step 0 materializes through (default: the
|
|
58
|
+
* filesystem store at `rootPath` — the fixtures' in-memory filesystem never
|
|
59
|
+
* holds a pending row, so the default is never exercised there).
|
|
58
60
|
*/
|
|
59
61
|
export declare function freshnessSweep(store: Store, repoId: string, rootPath: string, opts?: {
|
|
60
62
|
ts?: string;
|
|
61
63
|
gitHead?: string | null;
|
|
62
64
|
fs?: SyncFs;
|
|
65
|
+
docStore?: DocStore;
|
|
63
66
|
}): SweepResult;
|
|
64
67
|
/**
|
|
65
68
|
* Counts of ways the DB disagrees with the current filesystem. All fields are
|