@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,6 +1,8 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
|
+
import { type MutDoc, type MutBlock } from "./tree.js";
|
|
2
3
|
import { type To, type Expect } from "./ops.js";
|
|
3
4
|
import { type DocStore } from "./doc-store.js";
|
|
5
|
+
import { type DocBase, type Effect } from "./commit.js";
|
|
4
6
|
export type Op = {
|
|
5
7
|
op: "insert";
|
|
6
8
|
doc?: string;
|
|
@@ -19,6 +21,7 @@ export type Op = {
|
|
|
19
21
|
op: "move";
|
|
20
22
|
blocks: string[];
|
|
21
23
|
to: To;
|
|
24
|
+
doc?: string;
|
|
22
25
|
expect?: Expect;
|
|
23
26
|
} | {
|
|
24
27
|
op: "remove";
|
|
@@ -54,18 +57,6 @@ export interface ApplyRequest {
|
|
|
54
57
|
* contexts (tests) where the in-process serialization of Store.write suffices.
|
|
55
58
|
*/
|
|
56
59
|
omgbaseDir?: string;
|
|
57
|
-
/**
|
|
58
|
-
* Document-level frontmatter overrides (doc id → new frontmatter raw incl.
|
|
59
|
-
* fences + trailing separator, or null to drop it). Frontmatter is a
|
|
60
|
-
* document-level materialization unit (rendered before the block tree), not a
|
|
61
|
-
* block the six ops address; the whole-document update planner sets it here
|
|
62
|
-
* when the proposed content changes the frontmatter. The doc is force-loaded
|
|
63
|
-
* so a frontmatter-only change still commits.
|
|
64
|
-
*/
|
|
65
|
-
setFrontmatter?: {
|
|
66
|
-
doc: string;
|
|
67
|
-
raw: string | null;
|
|
68
|
-
}[];
|
|
69
60
|
/**
|
|
70
61
|
* Commit timestamp (RFC 3339 UTC, spec/store §2.4) stamped on every commit this
|
|
71
62
|
* request records — the `api` commits and any `observed` commit a file-CAS
|
|
@@ -96,4 +87,94 @@ export interface ApplyResult {
|
|
|
96
87
|
}>;
|
|
97
88
|
committed: boolean;
|
|
98
89
|
}
|
|
90
|
+
/** A planned changeset: the op results, the per-document revisions and diffs
|
|
91
|
+
* the dry run returns, and the effects the commit runs (spec/mutate §4
|
|
92
|
+
* steps 1–4; `commit.ts` is step 5). Nothing has been written. */
|
|
93
|
+
export interface ChangesetPlan {
|
|
94
|
+
results: OpResult[];
|
|
95
|
+
revisions: ApplyRevision[];
|
|
96
|
+
diffs: Record<string, {
|
|
97
|
+
before: string;
|
|
98
|
+
after: string;
|
|
99
|
+
}>;
|
|
100
|
+
/** Per loaded document in load order: its `ingest_known`, then (in a
|
|
101
|
+
* workspace) its `record_stat`. */
|
|
102
|
+
effects: Effect[];
|
|
103
|
+
/** Per loaded document in load order: the row triple it was loaded against
|
|
104
|
+
* (spec/mutate §4 step 5, 2.2) — what the commit re-validates and what a
|
|
105
|
+
* frozen plan will carry. Each `ingest_known` effect carries its own as
|
|
106
|
+
* `base`. */
|
|
107
|
+
preconditions: ({
|
|
108
|
+
id: string;
|
|
109
|
+
} & DocBase)[];
|
|
110
|
+
}
|
|
111
|
+
/** spec/mutate §4 steps 1–4: resolve, load, apply the ops in memory, render. */
|
|
112
|
+
export declare function planChangeset(store: Store, req: Omit<ApplyRequest, "dryRun" | "ts">): ChangesetPlan;
|
|
113
|
+
/**
|
|
114
|
+
* The op loop of spec/mutate §4 steps 1–4 as an incremental planner: `push`
|
|
115
|
+
* applies one op to the in-memory trees (loading a document at first touch and
|
|
116
|
+
* remembering its pre-mutation render, live ids and row triple), `finish`
|
|
117
|
+
* renders every loaded document into the `ChangesetPlan`. `planChangeset` is
|
|
118
|
+
* `push` over a whole op list; the effect planner (`plan.ts`, 2.3) pushes an
|
|
119
|
+
* effect's lowered ops one effect at a time so it can read the ids each op
|
|
120
|
+
* minted (`results`) before lowering the next effect, and it sets a document's
|
|
121
|
+
* file head (`setHead`) for a frontmatter effect.
|
|
122
|
+
*/
|
|
123
|
+
export declare class ChangesetPlanner {
|
|
124
|
+
private readonly store;
|
|
125
|
+
private readonly repoId;
|
|
126
|
+
private readonly omgbaseDir?;
|
|
127
|
+
/** Per op, in push order: the ids each op reported (`"$n.ids[i]"` placeholders resolve against these). */
|
|
128
|
+
readonly results: OpResult[];
|
|
129
|
+
/** `$new.n` → the id minted for it at execute (spec/mutate §11.7, 3.0): a block of a document
|
|
130
|
+
* this request creates is named by its symbol in the plan's ops, and resolved here. Empty at
|
|
131
|
+
* plan time, when the symbols ARE the tree's ids. */
|
|
132
|
+
private readonly aliases;
|
|
133
|
+
private readonly loaded;
|
|
134
|
+
private readonly before;
|
|
135
|
+
private readonly priorIds;
|
|
136
|
+
private readonly bases;
|
|
137
|
+
constructor(store: Store, repoId: string, omgbaseDir?: string | undefined);
|
|
138
|
+
/** The loaded document (loading it at first touch); unknown or tombstoned → `doc_missing`. */
|
|
139
|
+
ensureDoc(docId: string): MutDoc;
|
|
140
|
+
/**
|
|
141
|
+
* spec/mutate §11.3 (3.0): register a document this request CREATES — parsed
|
|
142
|
+
* from its composed bytes under the current minter — as a loaded document
|
|
143
|
+
* with no row behind it: no triple (`baseOf` is undefined, it is absent from
|
|
144
|
+
* `preconditions`), an empty `before` render, no prior ids (every block is
|
|
145
|
+
* `inserted / api`). `symbol` is its document symbol (`$new.n`), the id every
|
|
146
|
+
* later op addresses it by; its commit effect is an `ingest_known` with
|
|
147
|
+
* `base: null`, guarded by `path_taken` instead of the triple.
|
|
148
|
+
*/
|
|
149
|
+
adoptNew(symbol: string, doc: MutDoc): void;
|
|
150
|
+
/** Name a minted id for a `$new.n` symbol the plan's ops use (execute's replay). */
|
|
151
|
+
alias(symbol: string, id: string): void;
|
|
152
|
+
/** An op's string field: an alias (`$new.n` at execute), a kernel placeholder, or the id itself. */
|
|
153
|
+
private resolveId;
|
|
154
|
+
private resolveTo;
|
|
155
|
+
/** A per-block `expect` map re-keyed by the resolved ids (its keys may be placeholders or symbols). */
|
|
156
|
+
private resolveExpectMap;
|
|
157
|
+
/** `insert.doc` / `move.doc`: a document this request created (its symbol) passes through; else `resolveDocRef`. */
|
|
158
|
+
private resolveDocArg;
|
|
159
|
+
/** Is the document loaded (touched by an earlier op or head change)? */
|
|
160
|
+
isLoaded(docId: string): boolean;
|
|
161
|
+
/** The loaded document's row triple (after `ensureDoc`). */
|
|
162
|
+
baseOf(docId: string): DocBase | undefined;
|
|
163
|
+
/** Resolve a document reference (an id passes through, a path resolves; else `doc_missing`). */
|
|
164
|
+
resolveDoc(ref: string): string;
|
|
165
|
+
/** Locate a block in the LOADED documents only (an id an earlier op minted included); never loads one. */
|
|
166
|
+
locateLoaded(blockId: string): {
|
|
167
|
+
docId: string;
|
|
168
|
+
block: MutBlock;
|
|
169
|
+
siblings: MutBlock[];
|
|
170
|
+
index: number;
|
|
171
|
+
} | null;
|
|
172
|
+
/** spec/mutate §11.3 (2.3): replace everything before the first block — the
|
|
173
|
+
* leading trivia AND the frontmatter — with `raw` (the file head). */
|
|
174
|
+
setHead(docId: string, raw: string): void;
|
|
175
|
+
/** Apply one op (spec/mutate §2); `op_index` is its position in `results`. */
|
|
176
|
+
push(rawOp: Op): OpResult;
|
|
177
|
+
/** Step 4: render every loaded document; the plan's effects, revisions, diffs and preconditions in load order. */
|
|
178
|
+
finish(): ChangesetPlan;
|
|
179
|
+
}
|
|
99
180
|
export declare function apply(store: Store, req: ApplyRequest): ApplyResult;
|
package/dist/src/mutate/apply.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { sha256 } from "../core/hash.js";
|
|
2
2
|
import { isValidId } from "../core/ids.js";
|
|
3
3
|
import { findDocByRef } from "../core/read/reader.js";
|
|
4
|
-
import { ingestFile } from "../core/ingest.js";
|
|
5
|
-
import { makeReconcilingResolver } from "../sync/reconciling-ingest.js";
|
|
6
|
-
import { makeKnownIdResolver } from "./known-ids.js";
|
|
7
4
|
import { loadMutDoc } from "./load.js";
|
|
8
5
|
import { renderDoc, markContainerDirty, MutationError } from "./tree.js";
|
|
9
6
|
import { opInsert, opUpdate, opMove, opRemove, opSplit, opMerge, healTopLevelSeams, checkParentChildrenHash } from "./ops.js";
|
|
10
7
|
import { withWriterLock } from "../sync/writer-lock.js";
|
|
11
8
|
import { resolveDocStore } from "./doc-store.js";
|
|
9
|
+
import { commitChangeset, readDocBase } from "./commit.js";
|
|
12
10
|
import { isContainerBlock } from "../format/index.js";
|
|
13
11
|
// Resolve "$n.ids[i]" placeholders in a string against prior op results.
|
|
14
12
|
function resolvePlaceholder(value, results) {
|
|
@@ -22,17 +20,6 @@ function resolvePlaceholder(value, results) {
|
|
|
22
20
|
throw new MutationError("target_missing", `placeholder ${value} did not resolve`);
|
|
23
21
|
return id;
|
|
24
22
|
}
|
|
25
|
-
function resolveTo(to, results) {
|
|
26
|
-
const parent = typeof to.parent === "string" ? resolvePlaceholder(to.parent, results) : to.parent;
|
|
27
|
-
let at = to.at;
|
|
28
|
-
if (typeof at === "object") {
|
|
29
|
-
if ("before" in at)
|
|
30
|
-
at = { before: resolvePlaceholder(at.before, results) };
|
|
31
|
-
else if ("after" in at)
|
|
32
|
-
at = { after: resolvePlaceholder(at.after, results) };
|
|
33
|
-
}
|
|
34
|
-
return { parent, at };
|
|
35
|
-
}
|
|
36
23
|
// Resolve an insert op's `doc` field, which may be a minted doc id (d_...) or a
|
|
37
24
|
// document path — the same id-or-path symmetry the read/graph surfaces expose
|
|
38
25
|
// (shared via findDocByRef; see core/read/reader.ts). A valid d_ id passes
|
|
@@ -53,156 +40,259 @@ function docIdForBlock(store, repoId, blockId) {
|
|
|
53
40
|
const row = store.db.prepare("SELECT doc_id FROM blocks WHERE block_id = ? AND deleted_commit IS NULL").get(blockId);
|
|
54
41
|
return row?.doc_id ?? null;
|
|
55
42
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
const
|
|
43
|
+
/** spec/mutate §4 steps 1–4: resolve, load, apply the ops in memory, render. */
|
|
44
|
+
export function planChangeset(store, req) {
|
|
45
|
+
// The kernel request (spec/mutate §4, 3.0): the ops alone — a document's file
|
|
46
|
+
// head (frontmatter) is the effect planner's `setHead`, never a request field.
|
|
47
|
+
const planner = new ChangesetPlanner(store, req.repoId, req.omgbaseDir);
|
|
48
|
+
for (const op of req.ops)
|
|
49
|
+
planner.push(op);
|
|
50
|
+
return planner.finish();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The op loop of spec/mutate §4 steps 1–4 as an incremental planner: `push`
|
|
54
|
+
* applies one op to the in-memory trees (loading a document at first touch and
|
|
55
|
+
* remembering its pre-mutation render, live ids and row triple), `finish`
|
|
56
|
+
* renders every loaded document into the `ChangesetPlan`. `planChangeset` is
|
|
57
|
+
* `push` over a whole op list; the effect planner (`plan.ts`, 2.3) pushes an
|
|
58
|
+
* effect's lowered ops one effect at a time so it can read the ids each op
|
|
59
|
+
* minted (`results`) before lowering the next effect, and it sets a document's
|
|
60
|
+
* file head (`setHead`) for a frontmatter effect.
|
|
61
|
+
*/
|
|
62
|
+
export class ChangesetPlanner {
|
|
63
|
+
store;
|
|
64
|
+
repoId;
|
|
65
|
+
omgbaseDir;
|
|
66
|
+
/** Per op, in push order: the ids each op reported (`"$n.ids[i]"` placeholders resolve against these). */
|
|
67
|
+
results = [];
|
|
68
|
+
/** `$new.n` → the id minted for it at execute (spec/mutate §11.7, 3.0): a block of a document
|
|
69
|
+
* this request creates is named by its symbol in the plan's ops, and resolved here. Empty at
|
|
70
|
+
* plan time, when the symbols ARE the tree's ids. */
|
|
71
|
+
aliases = new Map();
|
|
72
|
+
loaded = new Map();
|
|
73
|
+
before = new Map();
|
|
62
74
|
// Block ids present in each doc BEFORE any op — used to label intent
|
|
63
75
|
// dispositions (edited vs inserted) on the known-id commit path.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
priorIds = new Map();
|
|
77
|
+
// The row triple `(path, current_rev, file_hash)` each document is loaded
|
|
78
|
+
// against — the commit's precondition (spec/mutate §4 step 5, 2.2).
|
|
79
|
+
bases = new Map();
|
|
80
|
+
constructor(store, repoId, omgbaseDir) {
|
|
81
|
+
this.store = store;
|
|
82
|
+
this.repoId = repoId;
|
|
83
|
+
this.omgbaseDir = omgbaseDir;
|
|
84
|
+
}
|
|
85
|
+
/** The loaded document (loading it at first touch); unknown or tombstoned → `doc_missing`. */
|
|
86
|
+
ensureDoc(docId) {
|
|
87
|
+
let d = this.loaded.get(docId);
|
|
68
88
|
if (!d) {
|
|
69
|
-
const
|
|
70
|
-
|
|
89
|
+
const base = readDocBase(this.store.db, docId);
|
|
90
|
+
const md = base ? loadMutDoc(this.store.db, docId) : null;
|
|
91
|
+
if (!base || !md)
|
|
71
92
|
throw new MutationError("doc_missing", `doc ${docId} not found`);
|
|
72
93
|
d = md;
|
|
73
|
-
loaded.set(docId, d);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
this.loaded.set(docId, d);
|
|
95
|
+
this.bases.set(docId, base);
|
|
96
|
+
this.before.set(docId, renderDoc(d));
|
|
97
|
+
const ids = this.store.db.prepare("SELECT block_id FROM blocks WHERE doc_id = ? AND deleted_commit IS NULL").all(docId);
|
|
98
|
+
this.priorIds.set(docId, new Set(ids.map((r) => r.block_id)));
|
|
77
99
|
}
|
|
78
100
|
return d;
|
|
79
|
-
};
|
|
80
|
-
// Document-level frontmatter overrides, before the op loop (so a change with
|
|
81
|
-
// no body ops still loads + commits the doc). Applied to the loaded MutDoc;
|
|
82
|
-
// renderDoc emits frontmatterRaw verbatim and the commit re-ingest re-derives
|
|
83
|
-
// properties/edges from the rendered bytes.
|
|
84
|
-
for (const fm of req.setFrontmatter ?? []) {
|
|
85
|
-
// ensureDoc captures `before` (pre-mutation render) at load time.
|
|
86
|
-
ensureDoc(fm.doc).frontmatterRaw = fm.raw;
|
|
87
101
|
}
|
|
88
|
-
|
|
102
|
+
/**
|
|
103
|
+
* spec/mutate §11.3 (3.0): register a document this request CREATES — parsed
|
|
104
|
+
* from its composed bytes under the current minter — as a loaded document
|
|
105
|
+
* with no row behind it: no triple (`baseOf` is undefined, it is absent from
|
|
106
|
+
* `preconditions`), an empty `before` render, no prior ids (every block is
|
|
107
|
+
* `inserted / api`). `symbol` is its document symbol (`$new.n`), the id every
|
|
108
|
+
* later op addresses it by; its commit effect is an `ingest_known` with
|
|
109
|
+
* `base: null`, guarded by `path_taken` instead of the triple.
|
|
110
|
+
*/
|
|
111
|
+
adoptNew(symbol, doc) {
|
|
112
|
+
if (this.loaded.has(symbol))
|
|
113
|
+
throw new Error(`adoptNew: ${symbol} is already loaded`);
|
|
114
|
+
this.loaded.set(symbol, doc);
|
|
115
|
+
this.before.set(symbol, "");
|
|
116
|
+
this.priorIds.set(symbol, new Set());
|
|
117
|
+
}
|
|
118
|
+
/** Name a minted id for a `$new.n` symbol the plan's ops use (execute's replay). */
|
|
119
|
+
alias(symbol, id) {
|
|
120
|
+
this.aliases.set(symbol, id);
|
|
121
|
+
}
|
|
122
|
+
/** An op's string field: an alias (`$new.n` at execute), a kernel placeholder, or the id itself. */
|
|
123
|
+
resolveId(value) {
|
|
124
|
+
return this.aliases.get(value) ?? resolvePlaceholder(value, this.results);
|
|
125
|
+
}
|
|
126
|
+
resolveTo(to) {
|
|
127
|
+
const parent = typeof to.parent === "string"
|
|
128
|
+
? this.resolveId(to.parent)
|
|
129
|
+
: typeof to.parent === "object" && to.parent !== null && "heading" in to.parent
|
|
130
|
+
? { ...to.parent, heading: this.resolveId(to.parent.heading) }
|
|
131
|
+
: to.parent;
|
|
132
|
+
let at = to.at;
|
|
133
|
+
if (typeof at === "object") {
|
|
134
|
+
if ("before" in at)
|
|
135
|
+
at = { before: this.resolveId(at.before) };
|
|
136
|
+
else if ("after" in at)
|
|
137
|
+
at = { after: this.resolveId(at.after) };
|
|
138
|
+
}
|
|
139
|
+
return { parent, at };
|
|
140
|
+
}
|
|
141
|
+
/** A per-block `expect` map re-keyed by the resolved ids (its keys may be placeholders or symbols). */
|
|
142
|
+
resolveExpectMap(expect) {
|
|
143
|
+
if (!expect)
|
|
144
|
+
return undefined;
|
|
145
|
+
const out = {};
|
|
146
|
+
for (const [k, v] of Object.entries(expect))
|
|
147
|
+
out[this.resolveId(k)] = v;
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
/** `insert.doc` / `move.doc`: a document this request created (its symbol) passes through; else `resolveDocRef`. */
|
|
151
|
+
resolveDocArg(ref) {
|
|
152
|
+
return this.loaded.has(ref) ? ref : resolveDocRef(this.store, this.repoId, ref);
|
|
153
|
+
}
|
|
154
|
+
/** Is the document loaded (touched by an earlier op or head change)? */
|
|
155
|
+
isLoaded(docId) {
|
|
156
|
+
return this.loaded.has(docId);
|
|
157
|
+
}
|
|
158
|
+
/** The loaded document's row triple (after `ensureDoc`). */
|
|
159
|
+
baseOf(docId) {
|
|
160
|
+
return this.bases.get(docId);
|
|
161
|
+
}
|
|
162
|
+
/** Resolve a document reference (an id passes through, a path resolves; else `doc_missing`). */
|
|
163
|
+
resolveDoc(ref) {
|
|
164
|
+
return resolveDocRef(this.store, this.repoId, ref);
|
|
165
|
+
}
|
|
166
|
+
/** Locate a block in the LOADED documents only (an id an earlier op minted included); never loads one. */
|
|
167
|
+
locateLoaded(blockId) {
|
|
168
|
+
for (const [docId, d] of this.loaded) {
|
|
169
|
+
const f = locateMut(d, blockId);
|
|
170
|
+
if (f)
|
|
171
|
+
return { docId, ...f };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
/** spec/mutate §11.3 (2.3): replace everything before the first block — the
|
|
176
|
+
* leading trivia AND the frontmatter — with `raw` (the file head). */
|
|
177
|
+
setHead(docId, raw) {
|
|
178
|
+
const d = this.ensureDoc(docId);
|
|
179
|
+
d.leadingTrivia = "";
|
|
180
|
+
d.frontmatterRaw = raw;
|
|
181
|
+
}
|
|
182
|
+
/** Apply one op (spec/mutate §2); `op_index` is its position in `results`. */
|
|
183
|
+
push(rawOp) {
|
|
184
|
+
const { store, repoId, loaded, results } = this;
|
|
185
|
+
void results;
|
|
186
|
+
const i = results.length;
|
|
187
|
+
const ensureDoc = (docId) => this.ensureDoc(docId);
|
|
188
|
+
let result;
|
|
89
189
|
switch (rawOp.op) {
|
|
90
190
|
case "insert": {
|
|
91
|
-
const to = resolveTo(rawOp.to
|
|
191
|
+
const to = this.resolveTo(rawOp.to);
|
|
92
192
|
const docId = rawOp.doc !== undefined
|
|
93
|
-
?
|
|
94
|
-
: parentDoc(store,
|
|
193
|
+
? this.resolveDocArg(rawOp.doc)
|
|
194
|
+
: parentDoc(store, repoId, to, loaded);
|
|
95
195
|
const d = ensureDoc(docId);
|
|
96
|
-
|
|
196
|
+
result = opInsert(d, to, rawOp.markdown, rawOp.expect, i);
|
|
97
197
|
break;
|
|
98
198
|
}
|
|
99
199
|
case "update": {
|
|
100
|
-
const block =
|
|
101
|
-
const docId = docIdForBlockLoaded(loaded, block) ?? docIdForBlock(store,
|
|
200
|
+
const block = this.resolveId(rawOp.block);
|
|
201
|
+
const docId = docIdForBlockLoaded(loaded, block) ?? docIdForBlock(store, repoId, block);
|
|
102
202
|
if (!docId)
|
|
103
203
|
throw new MutationError("block_missing", `block ${block} not found`, { op_index: i });
|
|
104
|
-
|
|
204
|
+
result = opUpdate(ensureDoc(docId), block, i, rawOp.markdown, rawOp.attrs, rawOp.expect, rawOp.trivia, rawOp.childIds);
|
|
105
205
|
break;
|
|
106
206
|
}
|
|
107
207
|
case "move": {
|
|
108
|
-
const blocks = rawOp.blocks.map((b) =>
|
|
109
|
-
const to = resolveTo(rawOp.to
|
|
110
|
-
const srcDocId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store,
|
|
208
|
+
const blocks = rawOp.blocks.map((b) => this.resolveId(b));
|
|
209
|
+
const to = this.resolveTo(rawOp.to);
|
|
210
|
+
const srcDocId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, repoId, blocks[0]);
|
|
111
211
|
if (!srcDocId)
|
|
112
212
|
throw new MutationError("block_missing", `block ${blocks[0]} not found`, { op_index: i });
|
|
113
|
-
// {doc:true} with a non-anchor `at` (start/end) means the
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
213
|
+
// {doc:true} with a non-anchor `at` (start/end) means the top level of
|
|
214
|
+
// `doc` when given (spec/mutate §2.3, 3.0: the destination document of a
|
|
215
|
+
// cross-document move to a top-level start/end), else of the source
|
|
216
|
+
// doc; an anchored `at` infers the destination doc from the anchor.
|
|
217
|
+
const topLevel = typeof to.parent === "object" && "doc" in to.parent && typeof to.at === "string";
|
|
218
|
+
const dstDocId = topLevel ? (rawOp.doc !== undefined ? this.resolveDocArg(rawOp.doc) : srcDocId) : parentDoc(store, repoId, to, loaded);
|
|
117
219
|
if (dstDocId === srcDocId) {
|
|
118
|
-
|
|
220
|
+
result = opMove(ensureDoc(srcDocId), blocks, to, i, rawOp.expect);
|
|
119
221
|
}
|
|
120
222
|
else {
|
|
121
223
|
// Cross-document move (04 §1): extract from source, insert into target.
|
|
122
|
-
|
|
224
|
+
result = crossDocMove(ensureDoc(srcDocId), ensureDoc(dstDocId), blocks, to, i, rawOp.expect);
|
|
123
225
|
}
|
|
124
226
|
break;
|
|
125
227
|
}
|
|
126
228
|
case "remove": {
|
|
127
|
-
const blocks = rawOp.blocks.map((b) =>
|
|
128
|
-
const docId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store,
|
|
229
|
+
const blocks = rawOp.blocks.map((b) => this.resolveId(b));
|
|
230
|
+
const docId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, repoId, blocks[0]);
|
|
129
231
|
if (!docId)
|
|
130
232
|
throw new MutationError("block_missing", `block ${blocks[0]} not found`, { op_index: i });
|
|
131
|
-
|
|
233
|
+
result = opRemove(ensureDoc(docId), blocks, i, this.resolveExpectMap(rawOp.expect));
|
|
132
234
|
break;
|
|
133
235
|
}
|
|
134
236
|
case "split": {
|
|
135
|
-
const block =
|
|
136
|
-
const docId = docIdForBlockLoaded(loaded, block) ?? docIdForBlock(store,
|
|
237
|
+
const block = this.resolveId(rawOp.block);
|
|
238
|
+
const docId = docIdForBlockLoaded(loaded, block) ?? docIdForBlock(store, repoId, block);
|
|
137
239
|
if (!docId)
|
|
138
240
|
throw new MutationError("block_missing", `block ${block} not found`, { op_index: i });
|
|
139
|
-
|
|
241
|
+
result = opSplit(ensureDoc(docId), block, rawOp.at, i, rawOp.expect);
|
|
140
242
|
break;
|
|
141
243
|
}
|
|
142
244
|
case "merge": {
|
|
143
|
-
const blocks = rawOp.blocks.map((b) =>
|
|
144
|
-
const docId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store,
|
|
245
|
+
const blocks = rawOp.blocks.map((b) => this.resolveId(b));
|
|
246
|
+
const docId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, repoId, blocks[0]);
|
|
145
247
|
if (!docId)
|
|
146
248
|
throw new MutationError("block_missing", `block ${blocks[0]} not found`, { op_index: i });
|
|
147
|
-
|
|
249
|
+
result = opMerge(ensureDoc(docId), blocks, i, rawOp.separator, this.resolveExpectMap(rawOp.expect));
|
|
148
250
|
break;
|
|
149
251
|
}
|
|
150
252
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
253
|
+
results.push(result);
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
/** Step 4: render every loaded document; the plan's effects, revisions, diffs and preconditions in load order. */
|
|
257
|
+
finish() {
|
|
258
|
+
const revisions = [];
|
|
259
|
+
const diffs = {};
|
|
260
|
+
const effects = [];
|
|
261
|
+
const preconditions = [];
|
|
262
|
+
for (const [docId, d] of this.loaded) {
|
|
263
|
+
const after = renderDoc(d);
|
|
264
|
+
// An adopted document (`adoptNew`) has no triple: it is absent from the
|
|
265
|
+
// preconditions and its ingest is guarded by `path_taken` (`base: null`).
|
|
266
|
+
const base = this.bases.get(docId) ?? null;
|
|
267
|
+
diffs[d.path] = { before: this.before.get(docId) ?? "", after };
|
|
268
|
+
revisions.push({ id: docId, path: d.path, hash: sha256(after).toString("hex") });
|
|
269
|
+
if (base)
|
|
270
|
+
preconditions.push({ id: docId, ...base });
|
|
271
|
+
effects.push({ kind: "ingest_known", docId, path: d.path, rendered: after, mutDoc: d, priorIds: this.priorIds.get(docId) ?? new Set(), base });
|
|
272
|
+
// Keep the freshness cache warm so this engine write isn't re-hashed by a
|
|
273
|
+
// later sweep (echo suppression already covers correctness; this avoids work).
|
|
274
|
+
if (this.omgbaseDir)
|
|
275
|
+
effects.push({ kind: "record_stat", path: d.path, bytes: after });
|
|
276
|
+
}
|
|
277
|
+
return { results: this.results, revisions, diffs, effects, preconditions };
|
|
158
278
|
}
|
|
279
|
+
}
|
|
280
|
+
export function apply(store, req) {
|
|
281
|
+
const { results, revisions, diffs, effects } = planChangeset(store, req);
|
|
159
282
|
if (req.dryRun) {
|
|
160
283
|
return { results, revisions, diffs, committed: false };
|
|
161
284
|
}
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
285
|
+
// Commit (spec/mutate §4 step 5): one transaction validating every touched
|
|
286
|
+
// document, one `api` commit and a revision per document plus the file
|
|
287
|
+
// intent, then the rendered files materialized — under the cross-process
|
|
288
|
+
// writer lock when a workspace dir is supplied (spec/sync §7); otherwise the
|
|
289
|
+
// Store's in-process serialization suffices.
|
|
167
290
|
const docStore = resolveDocStore(req);
|
|
291
|
+
const ts = req.ts ?? new Date().toISOString();
|
|
292
|
+
const head = { origin: "api", actor: req.origin.actor, reason: req.origin.reason ?? null };
|
|
293
|
+
const targets = { docStore, ...(req.omgbaseDir ? { omgbaseDir: req.omgbaseDir } : {}) };
|
|
168
294
|
const commitPhase = () => {
|
|
169
|
-
|
|
170
|
-
for (const [docId, d] of loaded) {
|
|
171
|
-
const rendered = renderDoc(d);
|
|
172
|
-
// File-CAS: on-disk bytes must equal the revision we computed against.
|
|
173
|
-
const current = store.db.prepare("SELECT file_hash FROM docs WHERE doc_id = ?").get(docId);
|
|
174
|
-
const onDiskContent = docStore.read(d.path);
|
|
175
|
-
if (onDiskContent !== null && current?.file_hash) {
|
|
176
|
-
const onDisk = sha256(onDiskContent);
|
|
177
|
-
if (!onDisk.equals(current.file_hash)) {
|
|
178
|
-
// A human edit landed first: ingest it, then the caller must retry.
|
|
179
|
-
ingestFile(store, req.repoId, d.path, onDiskContent, { ts, resolveIds: makeReconcilingResolver(store, req.repoId, { ts, path: d.path }) });
|
|
180
|
-
if (req.omgbaseDir)
|
|
181
|
-
docStore.recordStat(store, req.repoId, d.path, onDiskContent);
|
|
182
|
-
throw new MutationError("sync_conflict", `file ${d.path} changed on disk; re-ingested — retry`, { retriable: true });
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
// Atomic write through the DocStore (no-op for a headless NullDocStore).
|
|
186
|
-
docStore.write(d.path, rendered);
|
|
187
|
-
// Commit (INTENT path): persist the ops' tree with its KNOWN block ids —
|
|
188
|
-
// never re-derive identity from the rendered bytes. The mutated MutDoc `d`
|
|
189
|
-
// already carries deterministic ids (opUpdate keeps a block's id in place,
|
|
190
|
-
// untouched blocks keep theirs, new blocks carry the op-minted id), so the
|
|
191
|
-
// known-id resolver threads them onto the re-parsed tree and records intent
|
|
192
|
-
// dispositions (confidence 1.0). Re-reconciling here would gamble block —
|
|
193
|
-
// and thus node — identity on the probabilistic matcher (node-editability).
|
|
194
|
-
ingestFile(store, req.repoId, d.path, rendered, {
|
|
195
|
-
ts,
|
|
196
|
-
origin: "api",
|
|
197
|
-
actor: req.origin.actor,
|
|
198
|
-
reason: req.origin.reason ?? null,
|
|
199
|
-
resolveIds: makeKnownIdResolver(store, req.repoId, d, priorIds.get(docId) ?? new Set(), { path: d.path }),
|
|
200
|
-
});
|
|
201
|
-
// Keep the freshness cache warm so this engine write isn't re-hashed by a
|
|
202
|
-
// later sweep (echo suppression already covers correctness; this avoids work).
|
|
203
|
-
if (req.omgbaseDir)
|
|
204
|
-
docStore.recordStat(store, req.repoId, d.path, rendered);
|
|
205
|
-
}
|
|
295
|
+
commitChangeset(store, req.repoId, head, effects, ts, targets);
|
|
206
296
|
};
|
|
207
297
|
if (req.omgbaseDir)
|
|
208
298
|
withWriterLock(req.omgbaseDir, commitPhase);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON (§11.5): no whitespace; object keys sorted by their UTF-8
|
|
3
|
+
* bytes; members whose value is `undefined` omitted (an optional key absent is
|
|
4
|
+
* absent, never `null`; a JSON `null` value stays `null`); strings escaped only
|
|
5
|
+
* as JSON requires — `"`, `\`, and U+0000–U+001F (`\b \f \n \r \t`, else
|
|
6
|
+
* `\u00xx` lower-case) — everything else literal UTF-8; a number that is
|
|
7
|
+
* integral with |n| < 2^53 printed as a plain integer (`-0` as `0`), any other
|
|
8
|
+
* finite number by ECMAScript `Number::toString` (shortest round-trip digits);
|
|
9
|
+
* `true`/`false`/`null`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function canonicalJson(v: unknown): string;
|
|
12
|
+
/** A `plan` / `apply` / `execute` request as the caller gave it: `select`, `effects`, `expect` (each only when present). */
|
|
13
|
+
export interface FixRequest {
|
|
14
|
+
select?: unknown;
|
|
15
|
+
effects: unknown;
|
|
16
|
+
expect?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** The `fix` for a request to the SAME tool: the corrected arguments, canonical JSON. */
|
|
19
|
+
export declare function fixRequest(req: FixRequest): string;
|
|
20
|
+
/** The `fix` naming another tool: `<tool> <canonical args>`. */
|
|
21
|
+
export declare function fixTool(tool: string, args: unknown): string;
|
|
22
|
+
/** The selector that names exactly one row: the request narrowed to `from <root> where id == "<id>"` with `expect.max: 1` (the fix of a cross-hit collision or an ambiguous target under a selector). */
|
|
23
|
+
export declare function narrowedTo(root: "docs" | "blocks" | "nodes", id: string): string;
|
|
24
|
+
/** The root a row id belongs to, by its prefix (`d_` / `b_` / `n_`). */
|
|
25
|
+
export declare function rootOfId(id: string): "docs" | "blocks" | "nodes";
|
|
26
|
+
/** `value` with the member at `path` (keys and array indices) replaced; a copy, the input untouched. */
|
|
27
|
+
export declare function withMember(value: unknown, path: readonly string[], replacement: unknown): unknown;
|