@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.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/dist/src/core/ids.d.ts +4 -0
  3. package/dist/src/core/ids.js +17 -0
  4. package/dist/src/core/ingest.d.ts +39 -3
  5. package/dist/src/core/ingest.js +233 -202
  6. package/dist/src/core/store/schema.d.ts +3 -2
  7. package/dist/src/core/store/schema.js +28 -3
  8. package/dist/src/core/store/store.d.ts +9 -0
  9. package/dist/src/core/store/store.js +11 -0
  10. package/dist/src/graph/history.js +15 -12
  11. package/dist/src/index.d.ts +9 -5
  12. package/dist/src/index.js +14 -7
  13. package/dist/src/mcp/arguments.d.ts +9 -2
  14. package/dist/src/mcp/arguments.js +15 -2
  15. package/dist/src/mcp/errors.d.ts +1 -1
  16. package/dist/src/mcp/errors.js +3 -0
  17. package/dist/src/mcp/server.d.ts +22 -0
  18. package/dist/src/mcp/server.js +262 -185
  19. package/dist/src/mutate/apply.d.ts +93 -12
  20. package/dist/src/mutate/apply.js +203 -113
  21. package/dist/src/mutate/commit.d.ts +150 -0
  22. package/dist/src/mutate/commit.js +349 -0
  23. package/dist/src/mutate/doc-store.d.ts +3 -3
  24. package/dist/src/mutate/doc-store.js +21 -6
  25. package/dist/src/mutate/docs.d.ts +42 -87
  26. package/dist/src/mutate/docs.js +50 -204
  27. package/dist/src/mutate/effects.d.ts +331 -0
  28. package/dist/src/mutate/effects.js +981 -0
  29. package/dist/src/mutate/materialize.d.ts +49 -0
  30. package/dist/src/mutate/materialize.js +174 -0
  31. package/dist/src/mutate/opset.d.ts +27 -2
  32. package/dist/src/mutate/opset.js +12 -5
  33. package/dist/src/mutate/plan-update.d.ts +1 -46
  34. package/dist/src/mutate/plan-update.js +19 -67
  35. package/dist/src/mutate/plan.d.ts +200 -0
  36. package/dist/src/mutate/plan.js +472 -0
  37. package/dist/src/mutate/sugar.d.ts +135 -0
  38. package/dist/src/mutate/sugar.js +167 -0
  39. package/dist/src/mutate/tree.d.ts +4 -2
  40. package/dist/src/spec-versions.d.ts +5 -5
  41. package/dist/src/spec-versions.js +5 -5
  42. package/dist/src/surface-paths.d.ts +17 -26
  43. package/dist/src/surface-paths.js +27 -22
  44. package/dist/src/sync/checkpoint.d.ts +5 -2
  45. package/dist/src/sync/checkpoint.js +10 -6
  46. package/dist/src/sync/freshness.d.ts +10 -7
  47. package/dist/src/sync/freshness.js +15 -17
  48. package/dist/src/sync/observe.d.ts +4 -1
  49. package/dist/src/sync/observe.js +56 -5
  50. package/dist/src/sync/recovery.d.ts +4 -0
  51. package/dist/src/sync/recovery.js +4 -1
  52. package/dist/src/sync/stat-cache.d.ts +7 -0
  53. package/dist/src/sync/stat-cache.js +22 -0
  54. package/dist/src/sync/tombstone.js +23 -19
  55. package/dist/src/sync/watcher.d.ts +2 -0
  56. package/dist/src/sync/watcher.js +4 -1
  57. package/dist/src/sync/writer-lock.d.ts +15 -0
  58. package/dist/src/sync/writer-lock.js +104 -17
  59. package/package.json +1 -1
@@ -0,0 +1,200 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ import { type ApplyRevision, type Op, type OpResult } from "./apply.js";
3
+ import { type DocStore } from "./doc-store.js";
4
+ import { type CreateEntry, type ResolvedEffect, type RetargetPreview } from "./effects.js";
5
+ import type { IdentityPreview } from "./opset.js";
6
+ export declare const PLAN_VERSION: 1;
7
+ /** The lowering + conflict rules version (§11.4; the `matcher_v` precedent). */
8
+ export declare const PLANNER_V: "p2";
9
+ /** A kernel op as the plan spells it on the wire: the fixture spelling (`child_ids`). */
10
+ export type WireOp = Exclude<Op, {
11
+ op: "update";
12
+ }> | {
13
+ op: "update";
14
+ block: string;
15
+ markdown?: string;
16
+ attrs?: Record<string, unknown>;
17
+ expect?: {
18
+ content_hash?: string;
19
+ parent_children_hash?: string;
20
+ };
21
+ trivia?: string;
22
+ child_ids?: Record<string, string>;
23
+ };
24
+ /** The lowered form `execute` replays (§11.4): every key present, `[]` when empty. */
25
+ export interface PlanOpset {
26
+ /** `create` → the composed file, its document symbol and the index of the first kernel op planned after it (3.0; a later `set frontmatter.<k>` on the symbol is a head below) */
27
+ creates: CreateEntry[];
28
+ /** `move document` → the row/file rename (its retarget updates are in `ops`) */
29
+ moves: {
30
+ id: string;
31
+ from: string;
32
+ to: string;
33
+ }[];
34
+ /** `remove document` */
35
+ tombstones: {
36
+ id: string;
37
+ path: string;
38
+ }[];
39
+ /** per document, the file head — everything before the first block (frontmatter incl. fences and separator) */
40
+ set_frontmatter: {
41
+ doc: string;
42
+ raw: string;
43
+ }[];
44
+ /** the kernel ops, in order; new structure is referenced by kernel placeholders `$k.ids[j]` */
45
+ ops: WireOp[];
46
+ }
47
+ export interface PlanPreconditions {
48
+ /** the row triples (`spec/mutate` §4 step 5) of every document the plan touches, loaded ones first in load order */
49
+ docs: {
50
+ id: string;
51
+ path: string;
52
+ base_revision: string | null;
53
+ hash: string | null;
54
+ }[];
55
+ /** every block content hash the plan pins (caller-given or pinned from the live row), first-pin order */
56
+ blocks: {
57
+ id: string;
58
+ content_hash: string;
59
+ }[];
60
+ /** the caller's destination-parent CAS pins */
61
+ parents: {
62
+ doc: string;
63
+ parent: string | null;
64
+ parent_children_hash: string;
65
+ }[];
66
+ /** paths that must have no live document and no file: `create` paths and move destinations */
67
+ absent_paths: string[];
68
+ }
69
+ export interface PlanPreview {
70
+ results: OpResult[];
71
+ revisions: ApplyRevision[];
72
+ diffs: Record<string, {
73
+ before: string;
74
+ after: string;
75
+ }>;
76
+ /** (3.0) per `set content` document: the whole-document planner's identity consequences; present when a plan has one */
77
+ identity?: Record<string, IdentityPreview>;
78
+ /** (3.0) per `retarget` effect: its hits and pair counts; present when a plan has one */
79
+ retarget?: RetargetPreview[];
80
+ }
81
+ /** The executable core the digest covers. */
82
+ export interface PlanCore {
83
+ version: typeof PLAN_VERSION;
84
+ planner_v: string;
85
+ effects: ResolvedEffect[];
86
+ opset: PlanOpset;
87
+ preconditions: PlanPreconditions;
88
+ new_ids: {
89
+ symbol: string;
90
+ kind: "block" | "document";
91
+ }[];
92
+ }
93
+ export interface Plan extends PlanCore {
94
+ plan_id: string;
95
+ diagnostics: string[];
96
+ /** excluded from the digest; omittable on echo */
97
+ preview?: PlanPreview;
98
+ }
99
+ export interface Receipt {
100
+ plan_id: string;
101
+ /** the one `commits` row (`null` when the plan had nothing to commit) */
102
+ commit: string | null;
103
+ revisions: ApplyRevision[];
104
+ /** `$new.n` → the minted id */
105
+ new_ids: Record<string, string>;
106
+ committed: true;
107
+ }
108
+ /**
109
+ * Canonical JSON (§11.5): no whitespace; object keys sorted by their UTF-8
110
+ * bytes; members whose value is `undefined` omitted (an optional key absent is
111
+ * absent, never `null`; a JSON `null` value stays `null`); strings escaped only
112
+ * as JSON requires — `"`, `\`, and U+0000–U+001F (`\b \f \n \r \t`, else
113
+ * `\u00xx` lower-case) — everything else literal UTF-8; a number that is
114
+ * integral with |n| < 2^53 printed as a plain integer (`-0` as `0`), any other
115
+ * finite number by ECMAScript `Number::toString` (shortest round-trip digits);
116
+ * `true`/`false`/`null`.
117
+ */
118
+ export declare function canonicalJson(v: unknown): string;
119
+ /** The executable core of a plan — the six digested keys, nothing else. */
120
+ export declare function planCore(p: PlanCore): PlanCore;
121
+ /** `plan_id = hex(sha256(canonical(core)))` (§11.5). */
122
+ export declare function planDigest(core: PlanCore): string;
123
+ export interface PlanEffectsRequest {
124
+ repoId: string;
125
+ /** Working-tree root for the default doc store (the `path_taken` disk check; the move/remove previews). */
126
+ rootPath?: string;
127
+ docStore?: DocStore;
128
+ omgbaseDir?: string;
129
+ /** The effects as given (validated here: a shape error is `filter_invalid`). */
130
+ effects: unknown;
131
+ }
132
+ /** The kernel op as the plan carries it (`childIds` → `child_ids`). */
133
+ export declare function wireOp(o: Op): WireOp;
134
+ /** A plan's op back to the kernel's spelling. */
135
+ export declare function kernelOp(o: WireOp): Op;
136
+ /**
137
+ * Lower an ordered effect list into a frozen Plan (§11.6): under the symbolic
138
+ * minter, resolve and lower each effect in order (conflicts by focus kind,
139
+ * identical duplicates dropped with a diagnostic), render the touched documents,
140
+ * freeze the preconditions, digest the core. Reads only; mints no real id.
141
+ */
142
+ export declare function planEffects(store: Store, req: PlanEffectsRequest): Plan;
143
+ export interface ExecuteRequest {
144
+ repoId: string;
145
+ rootPath?: string;
146
+ docStore?: DocStore;
147
+ omgbaseDir?: string;
148
+ /** the commit's `actor` (null when the caller has none — a fixture step without one) and `reason` */
149
+ origin: {
150
+ actor: string | null;
151
+ reason?: string;
152
+ };
153
+ /** Commit timestamp (RFC 3339 UTC); defaults to now (see `ApplyRequest.ts`). */
154
+ ts?: string;
155
+ }
156
+ /**
157
+ * The plan form's admission (§11.7): `version` first — anything but 1 is
158
+ * `unsupported_plan_version { version, supported: [1] }` — then the core's keys
159
+ * must be present (`filter_invalid`), then the recomputed digest must equal the
160
+ * embedded `plan_id` (`plan_mismatch { expected, actual }`). `preview` and
161
+ * `diagnostics` are ignored.
162
+ */
163
+ export declare function admitPlan(input: unknown): Plan;
164
+ /** `execute { plan }` (§11.7): admit, then commit exactly that plan. */
165
+ export declare function executePlan(store: Store, req: ExecuteRequest & {
166
+ plan: unknown;
167
+ }): Receipt;
168
+ /** `execute { plan_id, effects }` (§11.7): re-plan; the digest must equal `plan_id`, else `plan_mismatch`. */
169
+ export declare function executeByDigest(store: Store, req: ExecuteRequest & {
170
+ planId: string;
171
+ effects: unknown;
172
+ }): Receipt;
173
+ /**
174
+ * `apply { effects }` with both halves kept (the sugar's seam — `sugar.ts`,
175
+ * the `omg` verbs, the whole-document update): the Plan always, and the
176
+ * Receipt when it committed (`dryRun` plans only — the CLI's `--dry-run`,
177
+ * spec/cli §3.6). Plan + execute run under one lock and one transaction,
178
+ * exactly as `applyEffects`.
179
+ */
180
+ export declare function applyEffectsDetailed(store: Store, req: ExecuteRequest & {
181
+ effects: unknown;
182
+ dryRun?: boolean;
183
+ }): {
184
+ plan: Plan;
185
+ receipt?: Receipt;
186
+ };
187
+ /** `apply { effects }` (§11.7): plan + execute under one lock and one transaction; `dryRun` returns the Plan. */
188
+ export declare function applyEffects(store: Store, req: ExecuteRequest & {
189
+ effects: unknown;
190
+ dryRun?: boolean;
191
+ }): Plan | Receipt;
192
+ /** Every string that is a key of `ids` replaced by its value, recursively (arrays and plain objects) — a plan's `$new.n` realized from a receipt's `new_ids`. */
193
+ export declare function substituteSymbols<T>(value: T, ids: Record<string, string>): T;
194
+ /**
195
+ * A planner error as the sugar reports it (`sugar.ts` `applySugar`): the
196
+ * `effect_index` the planner adds (§11.6) is dropped — the tool or verb built
197
+ * the effects itself, so the index names nothing the caller sent and the
198
+ * kernel's data (`op_index`, `block`, `current`, …) stays as pinned.
199
+ */
200
+ export declare function stripEffectIndex(err: unknown): unknown;
@@ -0,0 +1,472 @@
1
+ import { sha256 } from "../core/hash.js";
2
+ import { observeMints, withIdMinter } from "../core/ids.js";
3
+ import { findDoc } from "../core/read/reader.js";
4
+ import { inboundLinksTo } from "../graph/inbound-links.js";
5
+ import { withWriterLock } from "../sync/writer-lock.js";
6
+ import { ChangesetPlanner } from "./apply.js";
7
+ import { commitChangeset, readDocBase } from "./commit.js";
8
+ import { resolveDocStore } from "./doc-store.js";
9
+ import { parseNewDoc } from "./docs.js";
10
+ import { ConflictTracker, SymbolTable, isNewSymbol, lowerEffect, symbolicMinter, validateEffects, effectInvalid, } from "./effects.js";
11
+ import { MutationError } from "./tree.js";
12
+ // The Plan (spec/mutate §11.4–§11.7, 2.3): the frozen, executable, digest-named
13
+ // result of lowering an ordered effect list. `plan` writes nothing; `execute`
14
+ // commits exactly a plan — its preconditions re-validated inside the
15
+ // transaction by `commit.ts` — and `apply({ effects })` is plan + execute under
16
+ // one writer lock and one transaction. New identities are symbolic (`$new.n`,
17
+ // minted at execute, mapped in the receipt); a plan's identity is the sha256 of
18
+ // the canonical JSON of its executable core.
19
+ export const PLAN_VERSION = 1;
20
+ /** The lowering + conflict rules version (§11.4; the `matcher_v` precedent). */
21
+ export const PLANNER_V = "p2";
22
+ // ---- canonical JSON and the digest (§11.5) ----------------------------------------------
23
+ function cmpUtf8(a, b) {
24
+ return Buffer.compare(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
25
+ }
26
+ // A lone surrogate has no UTF-8 encoding: it cannot be digested (nor sent as JSON text).
27
+ function isWellFormed(s) {
28
+ for (let i = 0; i < s.length; i++) {
29
+ const c = s.charCodeAt(i);
30
+ if (c >= 0xd800 && c <= 0xdbff) {
31
+ const n = s.charCodeAt(i + 1);
32
+ if (!(n >= 0xdc00 && n <= 0xdfff))
33
+ return false;
34
+ i++;
35
+ }
36
+ else if (c >= 0xdc00 && c <= 0xdfff)
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+ function canonicalString(s) {
42
+ if (!isWellFormed(s))
43
+ throw effectInvalid("strings must be well-formed Unicode (a lone surrogate cannot be digested)");
44
+ let out = '"';
45
+ for (let i = 0; i < s.length; i++) {
46
+ const c = s.charCodeAt(i);
47
+ switch (c) {
48
+ case 0x22:
49
+ out += '\\"';
50
+ break;
51
+ case 0x5c:
52
+ out += "\\\\";
53
+ break;
54
+ case 0x08:
55
+ out += "\\b";
56
+ break;
57
+ case 0x0c:
58
+ out += "\\f";
59
+ break;
60
+ case 0x0a:
61
+ out += "\\n";
62
+ break;
63
+ case 0x0d:
64
+ out += "\\r";
65
+ break;
66
+ case 0x09:
67
+ out += "\\t";
68
+ break;
69
+ default:
70
+ out += c < 0x20 ? `\\u${c.toString(16).padStart(4, "0")}` : s[i];
71
+ }
72
+ }
73
+ return out + '"';
74
+ }
75
+ function canonicalNumber(n) {
76
+ if (!Number.isFinite(n))
77
+ throw effectInvalid("a number in a plan must be finite");
78
+ if (Number.isInteger(n) && Math.abs(n) < 2 ** 53)
79
+ return String(n === 0 ? 0 : n);
80
+ return String(n);
81
+ }
82
+ /**
83
+ * Canonical JSON (§11.5): no whitespace; object keys sorted by their UTF-8
84
+ * bytes; members whose value is `undefined` omitted (an optional key absent is
85
+ * absent, never `null`; a JSON `null` value stays `null`); strings escaped only
86
+ * as JSON requires — `"`, `\`, and U+0000–U+001F (`\b \f \n \r \t`, else
87
+ * `\u00xx` lower-case) — everything else literal UTF-8; a number that is
88
+ * integral with |n| < 2^53 printed as a plain integer (`-0` as `0`), any other
89
+ * finite number by ECMAScript `Number::toString` (shortest round-trip digits);
90
+ * `true`/`false`/`null`.
91
+ */
92
+ export function canonicalJson(v) {
93
+ if (v === null)
94
+ return "null";
95
+ switch (typeof v) {
96
+ case "boolean":
97
+ return v ? "true" : "false";
98
+ case "number":
99
+ return canonicalNumber(v);
100
+ case "string":
101
+ return canonicalString(v);
102
+ case "object": {
103
+ if (Array.isArray(v))
104
+ return `[${v.map((x) => canonicalJson(x === undefined ? null : x)).join(",")}]`;
105
+ const o = v;
106
+ const keys = Object.keys(o).filter((k) => o[k] !== undefined).sort(cmpUtf8);
107
+ return `{${keys.map((k) => `${canonicalString(k)}:${canonicalJson(o[k])}`).join(",")}}`;
108
+ }
109
+ default:
110
+ throw effectInvalid(`a ${typeof v} cannot be digested`);
111
+ }
112
+ }
113
+ /** The executable core of a plan — the six digested keys, nothing else. */
114
+ export function planCore(p) {
115
+ return { version: p.version, planner_v: p.planner_v, effects: p.effects, opset: p.opset, preconditions: p.preconditions, new_ids: p.new_ids };
116
+ }
117
+ /** `plan_id = hex(sha256(canonical(core)))` (§11.5). */
118
+ export function planDigest(core) {
119
+ return sha256(canonicalJson(planCore(core))).toString("hex");
120
+ }
121
+ /** The kernel op as the plan carries it (`childIds` → `child_ids`). */
122
+ export function wireOp(o) {
123
+ if (o.op === "update") {
124
+ const { childIds, ...rest } = o;
125
+ return { ...rest, ...(childIds !== undefined ? { child_ids: childIds } : {}) };
126
+ }
127
+ return o;
128
+ }
129
+ /** A plan's op back to the kernel's spelling. */
130
+ export function kernelOp(o) {
131
+ if (o.op === "update") {
132
+ const { child_ids, ...rest } = o;
133
+ return { ...rest, ...(child_ids !== undefined ? { childIds: child_ids } : {}) };
134
+ }
135
+ return o;
136
+ }
137
+ /**
138
+ * Lower an ordered effect list into a frozen Plan (§11.6): under the symbolic
139
+ * minter, resolve and lower each effect in order (conflicts by focus kind,
140
+ * identical duplicates dropped with a diagnostic), render the touched documents,
141
+ * freeze the preconditions, digest the core. Reads only; mints no real id.
142
+ */
143
+ export function planEffects(store, req) {
144
+ const effects = validateEffects(req.effects);
145
+ const docStore = resolveDocStore(req);
146
+ const symbols = new SymbolTable();
147
+ return withIdMinter(symbolicMinter(symbols), () => {
148
+ const planner = new ChangesetPlanner(store, req.repoId, req.omgbaseDir);
149
+ const ctx = {
150
+ store, repoId: req.repoId, docStore, planner, symbols,
151
+ blockSymbols: new Map(), creates: [], identity: new Map(), retargets: [], moves: [], tombstones: [], heads: new Map(), ops: [],
152
+ pins: new Map(), parents: [], absentPaths: [], docBases: new Map(),
153
+ tracker: new ConflictTracker(canonicalJson), canonical: canonicalJson, resolved: [],
154
+ };
155
+ effects.forEach((e, i) => {
156
+ try {
157
+ lowerEffect(ctx, i, e);
158
+ }
159
+ catch (err) {
160
+ // Every failure names the effect it arose from (the kernel's own data — `op_index`, `block` — stays).
161
+ if (err instanceof MutationError && err.data.effect_index === undefined)
162
+ err.data = { ...err.data, effect_index: i };
163
+ throw err;
164
+ }
165
+ });
166
+ const changeset = planner.finish();
167
+ const opset = {
168
+ creates: ctx.creates,
169
+ moves: ctx.moves.map((m) => ({ id: m.id, from: m.from, to: m.to })),
170
+ tombstones: ctx.tombstones.map((t) => ({ id: t.id, path: t.path })),
171
+ set_frontmatter: [...ctx.heads].map(([doc, raw]) => ({ doc, raw })),
172
+ ops: ctx.ops.map(wireOp),
173
+ };
174
+ const docs = changeset.preconditions.map((p) => ({ id: p.id, path: p.path, base_revision: p.currentRev, hash: p.fileHash }));
175
+ for (const [id, base] of ctx.docBases) {
176
+ if (!docs.some((d) => d.id === id))
177
+ docs.push({ id, path: base.path, base_revision: base.currentRev, hash: base.fileHash });
178
+ }
179
+ const preconditions = {
180
+ docs,
181
+ blocks: [...ctx.pins].map(([id, content_hash]) => ({ id, content_hash })),
182
+ parents: ctx.parents,
183
+ absent_paths: ctx.absentPaths,
184
+ };
185
+ const core = { version: PLAN_VERSION, planner_v: PLANNER_V, effects: ctx.resolved, opset, preconditions, new_ids: symbols.entries };
186
+ const plan_id = planDigest(core);
187
+ // The preview (not digested): the kernel results, every revision the commit
188
+ // would record — a created document's under its symbol, from its adopted
189
+ // tree like any loaded document's — and the per-file diffs: a create
190
+ // `"" → bytes`, a move two entries (its retargeted bytes land at the new
191
+ // path), a removal `bytes → ""`.
192
+ const diffs = { ...changeset.diffs };
193
+ const revisions = [...changeset.revisions];
194
+ for (const m of ctx.moves) {
195
+ const bytes = docStore.read(m.from) ?? "";
196
+ const own = diffs[m.from];
197
+ delete diffs[m.from];
198
+ diffs[m.from] = { before: bytes, after: "" };
199
+ diffs[m.to] = { before: "", after: own?.after ?? bytes };
200
+ }
201
+ for (const t of ctx.tombstones)
202
+ diffs[t.path] = { before: docStore.read(t.path) ?? "", after: "" };
203
+ for (const r of revisions) {
204
+ const moved = ctx.moves.find((m) => m.id === r.id);
205
+ if (moved)
206
+ r.path = moved.to;
207
+ }
208
+ const preview = { results: changeset.results, revisions, diffs };
209
+ if (ctx.identity.size > 0)
210
+ preview.identity = Object.fromEntries(ctx.identity);
211
+ if (ctx.retargets.length > 0)
212
+ preview.retarget = ctx.retargets;
213
+ return { ...core, plan_id, diagnostics: ctx.tracker.diagnostics, preview };
214
+ });
215
+ }
216
+ function isRecord(v) {
217
+ return typeof v === "object" && v !== null && !Array.isArray(v);
218
+ }
219
+ /**
220
+ * The plan form's admission (§11.7): `version` first — anything but 1 is
221
+ * `unsupported_plan_version { version, supported: [1] }` — then the core's keys
222
+ * must be present (`filter_invalid`), then the recomputed digest must equal the
223
+ * embedded `plan_id` (`plan_mismatch { expected, actual }`). `preview` and
224
+ * `diagnostics` are ignored.
225
+ */
226
+ export function admitPlan(input) {
227
+ if (!isRecord(input))
228
+ throw effectInvalid("`plan` must be an object");
229
+ if (input.version !== PLAN_VERSION) {
230
+ throw new MutationError("unsupported_plan_version", `plan version ${JSON.stringify(input.version)} is not supported (supported: ${PLAN_VERSION})`, { version: input.version ?? null, supported: [PLAN_VERSION] });
231
+ }
232
+ for (const k of ["planner_v", "effects", "opset", "preconditions", "new_ids", "plan_id"]) {
233
+ if (!(k in input))
234
+ throw effectInvalid(`\`plan.${k}\` is required`);
235
+ }
236
+ if (typeof input.planner_v !== "string")
237
+ throw effectInvalid("`plan.planner_v` must be a string");
238
+ if (!Array.isArray(input.effects))
239
+ throw effectInvalid("`plan.effects` must be an array");
240
+ if (!isRecord(input.opset))
241
+ throw effectInvalid("`plan.opset` must be an object");
242
+ for (const k of ["creates", "moves", "tombstones", "set_frontmatter", "ops"]) {
243
+ if (!Array.isArray(input.opset[k]))
244
+ throw effectInvalid(`\`plan.opset.${k}\` must be an array`);
245
+ }
246
+ for (const c of input.opset.creates) {
247
+ if (!isRecord(c) || typeof c.symbol !== "string" || typeof c.path !== "string" || typeof c.source !== "string" || typeof c.before_op !== "number") {
248
+ throw effectInvalid("`plan.opset.creates[]` entries are { symbol, path, source, before_op }");
249
+ }
250
+ }
251
+ if (!isRecord(input.preconditions))
252
+ throw effectInvalid("`plan.preconditions` must be an object");
253
+ for (const k of ["docs", "blocks", "parents", "absent_paths"]) {
254
+ if (!Array.isArray(input.preconditions[k]))
255
+ throw effectInvalid(`\`plan.preconditions.${k}\` must be an array`);
256
+ }
257
+ if (!Array.isArray(input.new_ids))
258
+ throw effectInvalid("`plan.new_ids` must be an array");
259
+ if (typeof input.plan_id !== "string")
260
+ throw effectInvalid("`plan.plan_id` must be a string");
261
+ const plan = input;
262
+ const actual = planDigest(plan);
263
+ if (actual !== plan.plan_id) {
264
+ throw new MutationError("plan_mismatch", "the plan's digest does not match its plan_id; it was edited or produced by a different planner — re-plan", { expected: plan.plan_id, actual });
265
+ }
266
+ return plan;
267
+ }
268
+ /** `execute { plan }` (§11.7): admit, then commit exactly that plan. */
269
+ export function executePlan(store, req) {
270
+ const plan = admitPlan(req.plan);
271
+ const docStore = resolveDocStore(req);
272
+ return underLock(req, () => executeAdmitted(store, req, plan, docStore));
273
+ }
274
+ /** `execute { plan_id, effects }` (§11.7): re-plan; the digest must equal `plan_id`, else `plan_mismatch`. */
275
+ export function executeByDigest(store, req) {
276
+ const docStore = resolveDocStore(req);
277
+ return underLock(req, () => {
278
+ const plan = planEffects(store, { repoId: req.repoId, docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}), effects: req.effects });
279
+ if (plan.plan_id !== req.planId) {
280
+ throw new MutationError("plan_mismatch", "the effects or the documents they target changed since the plan was made; re-plan", { expected: req.planId, actual: plan.plan_id });
281
+ }
282
+ return executeAdmitted(store, req, plan, docStore);
283
+ });
284
+ }
285
+ /**
286
+ * `apply { effects }` with both halves kept (the sugar's seam — `sugar.ts`,
287
+ * the `omg` verbs, the whole-document update): the Plan always, and the
288
+ * Receipt when it committed (`dryRun` plans only — the CLI's `--dry-run`,
289
+ * spec/cli §3.6). Plan + execute run under one lock and one transaction,
290
+ * exactly as `applyEffects`.
291
+ */
292
+ export function applyEffectsDetailed(store, req) {
293
+ const docStore = resolveDocStore(req);
294
+ const planIt = () => planEffects(store, { repoId: req.repoId, docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}), effects: req.effects });
295
+ if (req.dryRun)
296
+ return { plan: planIt() };
297
+ return underLock(req, () => {
298
+ const plan = planIt();
299
+ return { plan, receipt: executeAdmitted(store, req, plan, docStore) };
300
+ });
301
+ }
302
+ /** `apply { effects }` (§11.7): plan + execute under one lock and one transaction; `dryRun` returns the Plan. */
303
+ export function applyEffects(store, req) {
304
+ const { plan, receipt } = applyEffectsDetailed(store, req);
305
+ return receipt ?? plan;
306
+ }
307
+ // ---- symbols on the wire ----------------------------------------------------------------
308
+ /** Every string that is a key of `ids` replaced by its value, recursively (arrays and plain objects) — a plan's `$new.n` realized from a receipt's `new_ids`. */
309
+ export function substituteSymbols(value, ids) {
310
+ if (typeof value === "string")
311
+ return (Object.hasOwn(ids, value) ? ids[value] : value);
312
+ if (Array.isArray(value))
313
+ return value.map((x) => substituteSymbols(x, ids));
314
+ if (typeof value === "object" && value !== null) {
315
+ const out = {};
316
+ for (const [k, v] of Object.entries(value))
317
+ out[k] = substituteSymbols(v, ids);
318
+ return out;
319
+ }
320
+ return value;
321
+ }
322
+ /**
323
+ * A planner error as the sugar reports it (`sugar.ts` `applySugar`): the
324
+ * `effect_index` the planner adds (§11.6) is dropped — the tool or verb built
325
+ * the effects itself, so the index names nothing the caller sent and the
326
+ * kernel's data (`op_index`, `block`, `current`, …) stays as pinned.
327
+ */
328
+ export function stripEffectIndex(err) {
329
+ if (err instanceof MutationError && Object.hasOwn(err.data, "effect_index")) {
330
+ const { effect_index: _dropped, ...rest } = err.data;
331
+ err.data = rest;
332
+ }
333
+ return err;
334
+ }
335
+ function underLock(req, fn) {
336
+ return req.omgbaseDir ? withWriterLock(req.omgbaseDir, fn) : fn();
337
+ }
338
+ /**
339
+ * Commit an admitted plan: the preconditions pre-checked (`stale_plan` /
340
+ * `doc_missing` / `path_taken`, outside the transaction, so a stale plan fails
341
+ * before any id is minted), the opset replayed under the production minter
342
+ * (`b` ids minted here, before the transaction — as every request's are; a
343
+ * created document is re-adopted from its bytes at its `before_op` position,
344
+ * so the replay mints in the plan's order and the i-th `b` IS the i-th block
345
+ * symbol — the alias the `$new.k` in the ops resolve through), the internal
346
+ * effects built in order — moves, the rendered documents (created ones as
347
+ * `ingest_known { base: null }`; a moved document's revision lands at its new
348
+ * path), tombstones — and `commitChangeset` re-validates every triple inside
349
+ * `BEGIN IMMEDIATE`. The commit row's `ops` is the intent record
350
+ * `canonical({ v: 1, plan_id, effects })`.
351
+ */
352
+ function executeAdmitted(store, req, plan, docStore) {
353
+ for (const d of plan.preconditions.docs) {
354
+ const row = readDocBase(store.db, d.id);
355
+ if (!row)
356
+ throw new MutationError("doc_missing", `doc ${d.id} no longer exists`, { doc: d.id });
357
+ if (row.path !== d.path || row.currentRev !== d.base_revision || row.fileHash !== d.hash) {
358
+ throw new MutationError("stale_plan", `document ${d.id} changed since the plan was made; re-plan`, {
359
+ doc: d.id,
360
+ expected_revision: d.base_revision,
361
+ current: { revision: row.currentRev, hash: row.fileHash },
362
+ retriable: true,
363
+ });
364
+ }
365
+ }
366
+ for (const path of plan.preconditions.absent_paths) {
367
+ if (findDoc(store, { repoId: req.repoId, path }))
368
+ throw new MutationError("path_taken", `document already exists at ${path}`, { path });
369
+ if (docStore.exists(path))
370
+ throw new MutationError("path_taken", `file already exists on disk at ${path}`, { path });
371
+ }
372
+ const minted = { b: [], d: [] };
373
+ const ts = req.ts ?? new Date().toISOString();
374
+ const head = {
375
+ origin: "api",
376
+ actor: req.origin.actor,
377
+ reason: req.origin.reason ?? null,
378
+ ops: canonicalJson({ v: 1, plan_id: plan.plan_id, effects: plan.effects }),
379
+ };
380
+ const stat = req.omgbaseDir !== undefined;
381
+ const moved = new Map(plan.opset.moves.map((m) => [m.id, m]));
382
+ const blockSymbols = plan.new_ids.filter((s) => s.kind === "block").map((s) => s.symbol);
383
+ const planner = new ChangesetPlanner(store, req.repoId, req.omgbaseDir);
384
+ const { effects, outcome } = observeMints((prefix, id) => {
385
+ if (prefix === "b") {
386
+ // The i-th `b` the replay mints is the i-th block symbol (§11.7): alias it
387
+ // so an op naming a created document's block by its symbol resolves.
388
+ const symbol = blockSymbols[minted.b.length];
389
+ minted.b.push(id);
390
+ if (symbol !== undefined)
391
+ planner.alias(symbol, id);
392
+ }
393
+ else if (prefix === "d")
394
+ minted.d.push(id);
395
+ }, () => {
396
+ const heads = plan.opset.set_frontmatter;
397
+ for (const h of heads)
398
+ if (!isNewSymbol(h.doc))
399
+ planner.setHead(h.doc, h.raw);
400
+ // Re-adopt each created document where the plan did (`before_op`), its
401
+ // head applied once its tree is loaded; the ops between in order.
402
+ const creates = plan.opset.creates;
403
+ let ci = 0;
404
+ const adoptUpTo = (opIndex) => {
405
+ while (ci < creates.length && creates[ci].before_op <= opIndex) {
406
+ const c = creates[ci++];
407
+ planner.adoptNew(c.symbol, parseNewDoc(c.symbol, c.path, c.source));
408
+ for (const h of heads)
409
+ if (h.doc === c.symbol)
410
+ planner.setHead(c.symbol, h.raw);
411
+ }
412
+ };
413
+ plan.opset.ops.forEach((op, i) => {
414
+ adoptUpTo(i);
415
+ planner.push(kernelOp(op));
416
+ });
417
+ adoptUpTo(plan.opset.ops.length);
418
+ const changeset = planner.finish();
419
+ const effects = [];
420
+ for (const m of plan.opset.moves) {
421
+ const bytes = docStore.read(m.from) ?? "";
422
+ const rev = readDocBase(store.db, m.id)?.currentRev ?? null;
423
+ effects.push({ kind: "move_path", docId: m.id, from: m.from, to: m.to, currentRev: rev, inbound: inboundLinksTo(store, req.repoId, m.id, m.from), bytes });
424
+ if (stat)
425
+ effects.push({ kind: "clear_stat", path: m.from }, { kind: "record_stat", path: m.to, bytes });
426
+ }
427
+ for (const e of changeset.effects) {
428
+ // The moved document is loaded under its OLD path (the row changes
429
+ // inside the transaction); its rewritten bytes belong at the new path.
430
+ if (e.kind === "ingest_known" && moved.has(e.docId))
431
+ effects.push({ ...e, path: moved.get(e.docId).to });
432
+ else if (e.kind === "record_stat") {
433
+ const m = plan.opset.moves.find((x) => x.from === e.path);
434
+ effects.push(m ? { ...e, path: m.to } : e);
435
+ }
436
+ else
437
+ effects.push(e);
438
+ }
439
+ for (const t of plan.opset.tombstones) {
440
+ const rev = readDocBase(store.db, t.id)?.currentRev ?? null;
441
+ effects.push({ kind: "tombstone", docId: t.id, path: t.path, currentRev: rev, pool: false });
442
+ if (stat)
443
+ effects.push({ kind: "clear_stat", path: t.path });
444
+ }
445
+ const outcome = commitChangeset(store, req.repoId, head, effects, ts, { docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}) });
446
+ return { effects, outcome };
447
+ });
448
+ // The receipt's revisions, in effect order; the ingest results pair with the
449
+ // ingest effects in order (a created document's id is the one its ingest minted).
450
+ const revisions = [];
451
+ let j = 0;
452
+ for (const e of effects) {
453
+ if (e.kind === "ingest_reconciled")
454
+ revisions.push({ id: outcome.ingests[j++].docId, path: e.path, hash: sha256(e.source).toString("hex") });
455
+ else if (e.kind === "ingest_known")
456
+ revisions.push({ id: outcome.ingests[j++].docId, path: e.path, hash: sha256(e.rendered).toString("hex") });
457
+ }
458
+ // `$new.n` → minted id: the i-th block symbol is the i-th `b` the replay
459
+ // minted (all before the transaction, in the plan's order — the aliases
460
+ // above), the i-th document symbol the i-th `d` the created documents'
461
+ // ingests minted inside it (load order = effect order).
462
+ const newIds = {};
463
+ let bi = 0;
464
+ let di = 0;
465
+ for (const s of plan.new_ids) {
466
+ const id = s.kind === "block" ? minted.b[bi++] : minted.d[di++];
467
+ if (id !== undefined)
468
+ newIds[s.symbol] = id;
469
+ }
470
+ return { plan_id: plan.plan_id, commit: outcome.commitId, revisions, new_ids: newIds, committed: true };
471
+ }
472
+ //# sourceMappingURL=plan.js.map