@omgbase/core 0.15.0 → 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/dist/src/core/store/size.d.ts +76 -0
- package/dist/src/core/store/size.js +223 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +5 -1
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +27 -6
- package/dist/src/mcp/server.d.ts +34 -1
- package/dist/src/mcp/server.js +113 -28
- package/dist/src/mutate/canonical.d.ts +27 -0
- package/dist/src/mutate/canonical.js +138 -0
- package/dist/src/mutate/effects.d.ts +35 -3
- package/dist/src/mutate/effects.js +202 -22
- package/dist/src/mutate/plan.d.ts +26 -16
- package/dist/src/mutate/plan.js +152 -112
- package/dist/src/mutate/select.d.ts +89 -0
- package/dist/src/mutate/select.js +879 -0
- package/dist/src/mutate/tree.d.ts +2 -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 +5 -5
- package/dist/src/spec-versions.js +4 -4
- package/dist/src/version.d.ts +7 -2
- package/dist/src/version.js +5 -2
- package/package.json +2 -2
package/dist/src/mutate/plan.js
CHANGED
|
@@ -7,112 +7,30 @@ import { ChangesetPlanner } from "./apply.js";
|
|
|
7
7
|
import { commitChangeset, readDocBase } from "./commit.js";
|
|
8
8
|
import { resolveDocStore } from "./doc-store.js";
|
|
9
9
|
import { parseNewDoc } from "./docs.js";
|
|
10
|
-
import { ConflictTracker, SymbolTable, isNewSymbol, lowerEffect, symbolicMinter,
|
|
10
|
+
import { ConflictTracker, SymbolTable, isNewSymbol, lowerEffect, symbolicMinter, effectInvalid, } from "./effects.js";
|
|
11
|
+
import { compilePlain, compileSelection, selectionInvalid } from "./select.js";
|
|
11
12
|
import { MutationError } from "./tree.js";
|
|
13
|
+
import { canonicalJson, fixRequest, rootOfId, narrowedTo } from "./canonical.js";
|
|
12
14
|
// The Plan (spec/mutate §11.4–§11.7, 2.3): the frozen, executable, digest-named
|
|
13
15
|
// result of lowering an ordered effect list. `plan` writes nothing; `execute`
|
|
14
16
|
// commits exactly a plan — its preconditions re-validated inside the
|
|
15
17
|
// transaction by `commit.ts` — and `apply({ effects })` is plan + execute under
|
|
16
18
|
// one writer lock and one transaction. New identities are symbolic (`$new.n`,
|
|
17
19
|
// minted at execute, mapped in the receipt); a plan's identity is the sha256 of
|
|
18
|
-
// the canonical JSON of its executable core.
|
|
20
|
+
// the canonical JSON of its executable core. Since 3.1 (§11.8) a request may
|
|
21
|
+
// carry an OQX `select` whose hits, bound to the `effects` as templates, ARE
|
|
22
|
+
// the effects (`select.ts`); the frozen `selection` is the core's seventh member.
|
|
19
23
|
export const PLAN_VERSION = 1;
|
|
20
24
|
/** The lowering + conflict rules version (§11.4; the `matcher_v` precedent). */
|
|
21
|
-
export const PLANNER_V = "
|
|
25
|
+
export const PLANNER_V = "p3";
|
|
22
26
|
// ---- canonical JSON and the digest (§11.5) ----------------------------------------------
|
|
23
|
-
|
|
24
|
-
|
|
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. */
|
|
27
|
+
export { canonicalJson } from "./canonical.js";
|
|
28
|
+
/** The executable core of a plan — the six digested keys (seven with `selection`), nothing else. */
|
|
114
29
|
export function planCore(p) {
|
|
115
|
-
return {
|
|
30
|
+
return {
|
|
31
|
+
version: p.version, planner_v: p.planner_v, effects: p.effects, opset: p.opset, preconditions: p.preconditions, new_ids: p.new_ids,
|
|
32
|
+
...(p.selection !== undefined ? { selection: p.selection } : {}),
|
|
33
|
+
};
|
|
116
34
|
}
|
|
117
35
|
/** `plan_id = hex(sha256(canonical(core)))` (§11.5). */
|
|
118
36
|
export function planDigest(core) {
|
|
@@ -141,7 +59,28 @@ export function kernelOp(o) {
|
|
|
141
59
|
* freeze the preconditions, digest the core. Reads only; mints no real id.
|
|
142
60
|
*/
|
|
143
61
|
export function planEffects(store, req) {
|
|
144
|
-
|
|
62
|
+
// A selector request (§11.8): the templates compile into the concrete effects
|
|
63
|
+
// — hits × templates — and the frozen `selection` joins the core; an empty
|
|
64
|
+
// selection is an empty plan (the non-empty rule of §11.1 is the templates').
|
|
65
|
+
let effects;
|
|
66
|
+
let selection;
|
|
67
|
+
let templates;
|
|
68
|
+
let origins = [];
|
|
69
|
+
if (req.select !== undefined || req.expect !== undefined) {
|
|
70
|
+
if (req.select === undefined)
|
|
71
|
+
throw selectionInvalid("`expect` bounds a selection — pass `select` with it");
|
|
72
|
+
const compiled = compileSelection(store, req.repoId, req.select, req.expect, req.effects);
|
|
73
|
+
effects = compiled.effects;
|
|
74
|
+
selection = compiled.selection;
|
|
75
|
+
origins = compiled.origins;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// (3.2) the `-> ` sugar and expressions over each effect's own target row; without any, the shape check alone.
|
|
79
|
+
const compiled = compilePlain(store, req.repoId, req.effects);
|
|
80
|
+
effects = compiled.effects;
|
|
81
|
+
templates = compiled.templates;
|
|
82
|
+
origins = compiled.origins;
|
|
83
|
+
}
|
|
145
84
|
const docStore = resolveDocStore(req);
|
|
146
85
|
const symbols = new SymbolTable();
|
|
147
86
|
return withIdMinter(symbolicMinter(symbols), () => {
|
|
@@ -150,16 +89,27 @@ export function planEffects(store, req) {
|
|
|
150
89
|
store, repoId: req.repoId, docStore, planner, symbols,
|
|
151
90
|
blockSymbols: new Map(), creates: [], identity: new Map(), retargets: [], moves: [], tombstones: [], heads: new Map(), ops: [],
|
|
152
91
|
pins: new Map(), parents: [], absentPaths: [], docBases: new Map(),
|
|
153
|
-
tracker: new ConflictTracker(canonicalJson), canonical: canonicalJson, resolved: [],
|
|
92
|
+
tracker: new ConflictTracker(canonicalJson), canonical: canonicalJson, resolved: [], summary: [],
|
|
154
93
|
};
|
|
155
94
|
effects.forEach((e, i) => {
|
|
156
95
|
try {
|
|
157
96
|
lowerEffect(ctx, i, e);
|
|
158
97
|
}
|
|
159
98
|
catch (err) {
|
|
160
|
-
// Every failure names the effect it arose from (the kernel's own data — `op_index`, `block` — stays)
|
|
161
|
-
|
|
162
|
-
|
|
99
|
+
// Every failure names the effect it arose from (the kernel's own data — `op_index`, `block` — stays);
|
|
100
|
+
// an expanded effect's names its template and the hit it was bound to as well (§11.8).
|
|
101
|
+
if (err instanceof MutationError) {
|
|
102
|
+
if (err.data.effect_index === undefined)
|
|
103
|
+
err.data = { ...err.data, effect_index: i };
|
|
104
|
+
const origin = origins[i];
|
|
105
|
+
if (origin !== undefined && err.data.row === undefined) {
|
|
106
|
+
err.data = { ...err.data, ...(origin.template_index !== undefined ? { template_index: origin.template_index } : {}), row: origin.row };
|
|
107
|
+
}
|
|
108
|
+
// (3.3) the corrected example, built from the request as given (§8 "fix").
|
|
109
|
+
const fix = planningFix(err, i, req, origins, symbols);
|
|
110
|
+
if (fix !== undefined && err.data.fix === undefined)
|
|
111
|
+
err.data = { ...err.data, fix };
|
|
112
|
+
}
|
|
163
113
|
throw err;
|
|
164
114
|
}
|
|
165
115
|
});
|
|
@@ -182,7 +132,7 @@ export function planEffects(store, req) {
|
|
|
182
132
|
parents: ctx.parents,
|
|
183
133
|
absent_paths: ctx.absentPaths,
|
|
184
134
|
};
|
|
185
|
-
const core = { version: PLAN_VERSION, planner_v: PLANNER_V, effects: ctx.resolved, opset, preconditions, new_ids: symbols.entries };
|
|
135
|
+
const core = { version: PLAN_VERSION, planner_v: PLANNER_V, effects: ctx.resolved, opset, preconditions, new_ids: symbols.entries, ...(selection !== undefined ? { selection } : {}) };
|
|
186
136
|
const plan_id = planDigest(core);
|
|
187
137
|
// The preview (not digested): the kernel results, every revision the commit
|
|
188
138
|
// would record — a created document's under its symbol, from its adopted
|
|
@@ -205,14 +155,73 @@ export function planEffects(store, req) {
|
|
|
205
155
|
if (moved)
|
|
206
156
|
r.path = moved.to;
|
|
207
157
|
}
|
|
208
|
-
const preview = { results: changeset.results, revisions, diffs };
|
|
158
|
+
const preview = { results: changeset.results, revisions, diffs, summary: ctx.summary };
|
|
209
159
|
if (ctx.identity.size > 0)
|
|
210
160
|
preview.identity = Object.fromEntries(ctx.identity);
|
|
211
161
|
if (ctx.retargets.length > 0)
|
|
212
162
|
preview.retarget = ctx.retargets;
|
|
213
|
-
return { ...core, plan_id, diagnostics: ctx.tracker.diagnostics, preview };
|
|
163
|
+
return { ...core, plan_id, diagnostics: ctx.tracker.diagnostics, ...(templates !== undefined ? { templates } : {}), preview };
|
|
214
164
|
});
|
|
215
165
|
}
|
|
166
|
+
// ---- the corrected examples of planning errors (§8 "fix", 3.3) ------------------------------
|
|
167
|
+
/** The request as the caller gave it, for a `fix`: `select`/`expect` only when present. */
|
|
168
|
+
function requestOf(req) {
|
|
169
|
+
return { effects: req.effects, ...(req.select !== undefined ? { select: req.select } : {}), ...(req.expect !== undefined ? { expect: req.expect } : {}) };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* `conflicting_effects`: keep the later effect — the request minus the earlier
|
|
173
|
+
* one; under a selector, a cross-hit collision narrows the selector to the
|
|
174
|
+
* later hit (`from <root> where id == …`, `expect.max: 1`) and a same-hit
|
|
175
|
+
* collision drops the earlier template. `stale_plan` at plan time (an
|
|
176
|
+
* `expect.revision` that no longer holds): the same request with the current
|
|
177
|
+
* revision in that guard. `target_missing { symbol }` for a `$new.n` no earlier
|
|
178
|
+
* effect created: the next `create` of a plain request moved before the effect
|
|
179
|
+
* that uses it, the symbol renamed to the one that create will allocate.
|
|
180
|
+
*/
|
|
181
|
+
function planningFix(err, i, req, origins, symbols) {
|
|
182
|
+
const effects = Array.isArray(req.effects) ? req.effects : null;
|
|
183
|
+
if (effects === null)
|
|
184
|
+
return undefined;
|
|
185
|
+
const selector = req.select !== undefined;
|
|
186
|
+
if (err.code === "conflicting_effects" && Array.isArray(err.data.effects)) {
|
|
187
|
+
const [a, b] = err.data.effects;
|
|
188
|
+
if (!selector)
|
|
189
|
+
return fixRequest({ effects: effects.filter((_, k) => k !== a) });
|
|
190
|
+
const oa = origins[a];
|
|
191
|
+
const ob = origins[b];
|
|
192
|
+
if (oa === undefined || ob === undefined || oa.template_index === undefined)
|
|
193
|
+
return undefined;
|
|
194
|
+
if (oa.row.id !== ob.row.id)
|
|
195
|
+
return fixRequest({ select: narrowedTo(rootOfId(ob.row.id), ob.row.id), effects, expect: { max: 1 } });
|
|
196
|
+
return fixRequest({ ...requestOf(req), effects: effects.filter((_, k) => k !== oa.template_index) });
|
|
197
|
+
}
|
|
198
|
+
if (err.code === "stale_plan" && isRecord(err.data.current) && typeof err.data.current.revision === "string") {
|
|
199
|
+
const t = selector ? origins[i]?.template_index : i;
|
|
200
|
+
if (t === undefined || !isRecord(effects[t]))
|
|
201
|
+
return undefined;
|
|
202
|
+
const e = effects[t];
|
|
203
|
+
const expect = { ...(isRecord(e.expect) ? e.expect : {}), revision: err.data.current.revision };
|
|
204
|
+
return fixRequest({ ...requestOf(req), effects: effects.map((x, k) => (k === t ? { ...e, expect } : x)) });
|
|
205
|
+
}
|
|
206
|
+
if (err.code === "target_missing" && typeof err.data.symbol === "string" && !selector && symbols.kindOf(err.data.symbol) === undefined) {
|
|
207
|
+
const c = effects.findIndex((e, k) => k > i && isRecord(e) && e.op === "create");
|
|
208
|
+
if (c < 0)
|
|
209
|
+
return undefined;
|
|
210
|
+
const symbol = `$new.${symbols.entries.length}`;
|
|
211
|
+
const reordered = effects.filter((_, k) => k !== c);
|
|
212
|
+
reordered.splice(i, 0, effects[c]);
|
|
213
|
+
return fixRequest({ effects: reordered.map((e, k) => (k === i + 1 ? substituteSymbols(e, { [err.data.symbol]: symbol }) : e)) });
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
/** The `fix` of a plan that cannot be executed as it stands (`stale_plan`, `plan_mismatch`): re-plan and commit in one call — `apply` over the plan's selector form when it has one, else over its concrete effects (none when the plan has no effects: `apply` takes at least one). */
|
|
218
|
+
function replanFix(plan) {
|
|
219
|
+
if (plan.selection !== undefined) {
|
|
220
|
+
const sel = plan.selection;
|
|
221
|
+
return fixRequest({ select: sel.query, effects: sel.templates, expect: sel.expect ?? { max: sel.cardinality } });
|
|
222
|
+
}
|
|
223
|
+
return Array.isArray(plan.effects) && plan.effects.length > 0 ? fixRequest({ effects: plan.effects }) : undefined;
|
|
224
|
+
}
|
|
216
225
|
function isRecord(v) {
|
|
217
226
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
218
227
|
}
|
|
@@ -256,12 +265,24 @@ export function admitPlan(input) {
|
|
|
256
265
|
}
|
|
257
266
|
if (!Array.isArray(input.new_ids))
|
|
258
267
|
throw effectInvalid("`plan.new_ids` must be an array");
|
|
268
|
+
if (input.selection !== undefined) {
|
|
269
|
+
// (3.1) the optional seventh member: its shape, so the digest below covers what it says.
|
|
270
|
+
const sel = input.selection;
|
|
271
|
+
if (!isRecord(sel) || typeof sel.query !== "string" || !Array.isArray(sel.templates) || typeof sel.cardinality !== "number" || !Array.isArray(sel.identities)) {
|
|
272
|
+
throw effectInvalid("`plan.selection` is { query, expect?, templates, cardinality, identities }");
|
|
273
|
+
}
|
|
274
|
+
if (sel.expect !== undefined && (!isRecord(sel.expect) || typeof sel.expect.max !== "number"))
|
|
275
|
+
throw effectInvalid("`plan.selection.expect` is { max }");
|
|
276
|
+
}
|
|
259
277
|
if (typeof input.plan_id !== "string")
|
|
260
278
|
throw effectInvalid("`plan.plan_id` must be a string");
|
|
279
|
+
// (3.2) the written effects ride outside the digest, like `diagnostics`; their shape is all that is checked.
|
|
280
|
+
if (input.templates !== undefined && !Array.isArray(input.templates))
|
|
281
|
+
throw effectInvalid("`plan.templates` must be an array");
|
|
261
282
|
const plan = input;
|
|
262
283
|
const actual = planDigest(plan);
|
|
263
284
|
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 });
|
|
285
|
+
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, fix: replanFix(plan) });
|
|
265
286
|
}
|
|
266
287
|
return plan;
|
|
267
288
|
}
|
|
@@ -271,13 +292,17 @@ export function executePlan(store, req) {
|
|
|
271
292
|
const docStore = resolveDocStore(req);
|
|
272
293
|
return underLock(req, () => executeAdmitted(store, req, plan, docStore));
|
|
273
294
|
}
|
|
274
|
-
/**
|
|
295
|
+
/** The selector half of a request, threaded to the planner as given (absent keys stay absent). */
|
|
296
|
+
function selectorOf(req) {
|
|
297
|
+
return { ...(req.select !== undefined ? { select: req.select } : {}), ...(req.expect !== undefined ? { expect: req.expect } : {}) };
|
|
298
|
+
}
|
|
299
|
+
/** `execute { plan_id, effects, select?, expect? }` (§11.7): re-plan (with the selector when one is given, §11.8); the digest must equal `plan_id`, else `plan_mismatch`. */
|
|
275
300
|
export function executeByDigest(store, req) {
|
|
276
301
|
const docStore = resolveDocStore(req);
|
|
277
302
|
return underLock(req, () => {
|
|
278
|
-
const plan = planEffects(store, { repoId: req.repoId, docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}), effects: req.effects });
|
|
303
|
+
const plan = planEffects(store, { repoId: req.repoId, docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}), effects: req.effects, ...selectorOf(req) });
|
|
279
304
|
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 });
|
|
305
|
+
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, fix: fixRequest(requestOf(req)) });
|
|
281
306
|
}
|
|
282
307
|
return executeAdmitted(store, req, plan, docStore);
|
|
283
308
|
});
|
|
@@ -291,15 +316,23 @@ export function executeByDigest(store, req) {
|
|
|
291
316
|
*/
|
|
292
317
|
export function applyEffectsDetailed(store, req) {
|
|
293
318
|
const docStore = resolveDocStore(req);
|
|
294
|
-
const planIt = () => planEffects(store, { repoId: req.repoId, docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}), effects: req.effects });
|
|
319
|
+
const planIt = () => planEffects(store, { repoId: req.repoId, docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}), effects: req.effects, ...selectorOf(req) });
|
|
295
320
|
if (req.dryRun)
|
|
296
321
|
return { plan: planIt() };
|
|
322
|
+
// One-shot `apply` with a selector must bound it (§11.8): the caller never saw
|
|
323
|
+
// the hits. The fix (3.3) is the same call bounded at the selection's actual
|
|
324
|
+
// size — the selector compiles (reads only) to count it; a selection the
|
|
325
|
+
// compile itself refuses reports its own error and fix.
|
|
326
|
+
if (req.select !== undefined && req.expect === undefined) {
|
|
327
|
+
const { selection } = compileSelection(store, req.repoId, req.select, undefined, req.effects);
|
|
328
|
+
throw selectionInvalid("`expect.max` is required on `apply` with `select` — bound the selection, or preview it with `plan`", { fix: fixRequest({ select: req.select, effects: req.effects, expect: { max: selection.cardinality } }) });
|
|
329
|
+
}
|
|
297
330
|
return underLock(req, () => {
|
|
298
331
|
const plan = planIt();
|
|
299
332
|
return { plan, receipt: executeAdmitted(store, req, plan, docStore) };
|
|
300
333
|
});
|
|
301
334
|
}
|
|
302
|
-
/** `apply { effects }` (§11.7): plan + execute under one lock and one transaction; `dryRun` returns the Plan. */
|
|
335
|
+
/** `apply { effects, select?, expect? }` (§11.7): plan + execute under one lock and one transaction; `dryRun` returns the Plan. */
|
|
303
336
|
export function applyEffects(store, req) {
|
|
304
337
|
const { plan, receipt } = applyEffectsDetailed(store, req);
|
|
305
338
|
return receipt ?? plan;
|
|
@@ -347,7 +380,9 @@ function underLock(req, fn) {
|
|
|
347
380
|
* `ingest_known { base: null }`; a moved document's revision lands at its new
|
|
348
381
|
* path), tombstones — and `commitChangeset` re-validates every triple inside
|
|
349
382
|
* `BEGIN IMMEDIATE`. The commit row's `ops` is the intent record
|
|
350
|
-
* `canonical({ v: 1, plan_id, effects })
|
|
383
|
+
* `canonical({ v: 1, plan_id, effects, selection?, templates? })` (§11.8: the
|
|
384
|
+
* selector, its templates and the hits it bound ride along when the plan had
|
|
385
|
+
* one; a plain request's written effects when one carried an expression, 3.2).
|
|
351
386
|
*/
|
|
352
387
|
function executeAdmitted(store, req, plan, docStore) {
|
|
353
388
|
for (const d of plan.preconditions.docs) {
|
|
@@ -360,6 +395,7 @@ function executeAdmitted(store, req, plan, docStore) {
|
|
|
360
395
|
expected_revision: d.base_revision,
|
|
361
396
|
current: { revision: row.currentRev, hash: row.fileHash },
|
|
362
397
|
retriable: true,
|
|
398
|
+
fix: replanFix(plan),
|
|
363
399
|
});
|
|
364
400
|
}
|
|
365
401
|
}
|
|
@@ -375,7 +411,11 @@ function executeAdmitted(store, req, plan, docStore) {
|
|
|
375
411
|
origin: "api",
|
|
376
412
|
actor: req.origin.actor,
|
|
377
413
|
reason: req.origin.reason ?? null,
|
|
378
|
-
ops: canonicalJson({
|
|
414
|
+
ops: canonicalJson({
|
|
415
|
+
v: 1, plan_id: plan.plan_id, effects: plan.effects,
|
|
416
|
+
...(plan.selection !== undefined ? { selection: plan.selection } : {}),
|
|
417
|
+
...(plan.templates !== undefined ? { templates: plan.templates } : {}),
|
|
418
|
+
}),
|
|
379
419
|
};
|
|
380
420
|
const stat = req.omgbaseDir !== undefined;
|
|
381
421
|
const moved = new Map(plan.opset.moves.map((m) => [m.id, m]));
|
|
@@ -467,6 +507,6 @@ function executeAdmitted(store, req, plan, docStore) {
|
|
|
467
507
|
if (id !== undefined)
|
|
468
508
|
newIds[s.symbol] = id;
|
|
469
509
|
}
|
|
470
|
-
return { plan_id: plan.plan_id, commit: outcome.commitId, revisions, new_ids: newIds, committed: true };
|
|
510
|
+
return { plan_id: plan.plan_id, commit: outcome.commitId, revisions, new_ids: newIds, committed: true, summary: plan.preview?.summary ?? [] };
|
|
471
511
|
}
|
|
472
512
|
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type Query } from "@omgbase/oqx";
|
|
2
|
+
import type { Store } from "../core/store/store.js";
|
|
3
|
+
import { type Effect } from "./effects.js";
|
|
4
|
+
import { MutationError } from "./tree.js";
|
|
5
|
+
import { type FixRequest } from "./canonical.js";
|
|
6
|
+
/** The implicit bound on a selection without `expect.max` (hits beyond it are `selection_too_large`). */
|
|
7
|
+
export declare const SELECTION_CEILING = 1000;
|
|
8
|
+
export interface SelectionExpect {
|
|
9
|
+
max: number;
|
|
10
|
+
}
|
|
11
|
+
/** The seventh digested member of a plan's core (§11.4): the selector and its templates AS GIVEN, the hits it bound. */
|
|
12
|
+
export interface Selection {
|
|
13
|
+
query: string;
|
|
14
|
+
expect?: SelectionExpect;
|
|
15
|
+
templates: unknown[];
|
|
16
|
+
cardinality: number;
|
|
17
|
+
/** the hit ids, result order */
|
|
18
|
+
identities: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Where an expanded effect came from (every error from one carries both, §11.8); a plain effect's
|
|
21
|
+
* expressions (3.2) have a row but no template. */
|
|
22
|
+
export interface EffectOrigin {
|
|
23
|
+
template_index?: number;
|
|
24
|
+
row: {
|
|
25
|
+
id: string;
|
|
26
|
+
path: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface CompiledSelection {
|
|
30
|
+
/** the concrete, validated effects: hits × templates */
|
|
31
|
+
effects: Effect[];
|
|
32
|
+
selection: Selection;
|
|
33
|
+
/** expanded index → its origin */
|
|
34
|
+
origins: EffectOrigin[];
|
|
35
|
+
}
|
|
36
|
+
/** (3.2) A plain request compiled: the concrete effects and, when any effect carried an expression, the list as written. */
|
|
37
|
+
export interface CompiledPlain {
|
|
38
|
+
effects: Effect[];
|
|
39
|
+
/** the effects AS WRITTEN — present only when at least one slot was an expression (the intent record's `templates`) */
|
|
40
|
+
templates?: unknown[];
|
|
41
|
+
/** effect index → the row its expressions were read from (sparse; literal effects have none) */
|
|
42
|
+
origins: (EffectOrigin | undefined)[];
|
|
43
|
+
}
|
|
44
|
+
/** The admission pass's refusal: `filter_invalid { reason, hint: "selection" }`. */
|
|
45
|
+
export declare function selectionInvalid(message: string, data?: Record<string, unknown>): MutationError;
|
|
46
|
+
/**
|
|
47
|
+
* `"-> <expr>"` → `{ oqx: "<expr>" }` — or, when the expression is a bare
|
|
48
|
+
* string literal or a template literal without interpolation, the literal
|
|
49
|
+
* itself (constant folding: `-> "x"` and `"x"` plan identically, and
|
|
50
|
+
* `-> "-> note"` is how a literal that begins with the leader is spelled).
|
|
51
|
+
* A parse error is `filter_invalid { hint: "selection" }` with its offset
|
|
52
|
+
* counted from the start of the written string (past the leader).
|
|
53
|
+
*/
|
|
54
|
+
export declare function desugarLeader(v: string, here: string): unknown;
|
|
55
|
+
/**
|
|
56
|
+
* Desugar one effect or template as written (§11.8 "Sugar"), before the shape
|
|
57
|
+
* check and the admission: in every slot that admits an expression — the
|
|
58
|
+
* strings `markdown`, `to`, `path`, `separator`, `at.before` / `at.after`,
|
|
59
|
+
* the JSON `value`, each top-level `create.frontmatter` value, a target's `id`
|
|
60
|
+
* / `path`, and a whole target (`into`, `target`, `targets[k]`) — a string
|
|
61
|
+
* beginning `-> ` becomes `{ oqx }` (or folds to its literal); a `-> ` string
|
|
62
|
+
* anywhere else (`expect`, `field`, `op`, a section's `heading_id`, …) is
|
|
63
|
+
* refused. Every other string is literal data, with no escaping and no `${`
|
|
64
|
+
* scanning; a string nested inside a `value` or a frontmatter value is data
|
|
65
|
+
* (the slot is the top-level value, as for `{ oqx }`). Shape problems are the
|
|
66
|
+
* shape check's.
|
|
67
|
+
*/
|
|
68
|
+
export declare function desugarEffect(raw: unknown, index: number, request?: FixRequest): unknown;
|
|
69
|
+
/**
|
|
70
|
+
* Compile `{ select, expect?, effects: templates }` into the concrete effects
|
|
71
|
+
* (§11.8): admit statically, rewrite, run once over one snapshot with
|
|
72
|
+
* `limit = bound + 1`, refuse a selection beyond the bound, bind every hit to
|
|
73
|
+
* every template. Reads only.
|
|
74
|
+
*/
|
|
75
|
+
export declare function compileSelection(store: Store, repoId: string, select: unknown, expectRaw: unknown, templatesRaw: unknown): CompiledSelection;
|
|
76
|
+
/** The query a selector request compiles to (admission + rewrite, before any row is read) — `print` it to see what runs. */
|
|
77
|
+
export declare function selectionQuery(select: unknown, templates: unknown): Query;
|
|
78
|
+
/**
|
|
79
|
+
* Compile a plain `effects` list (no selector): desugar the `-> ` leaders,
|
|
80
|
+
* then — when nothing is an expression — the shape check as before (3.0
|
|
81
|
+
* byte-identical). Otherwise each effect with expressions is admitted as a
|
|
82
|
+
* template over its own primary target's row: the effects are grouped by that
|
|
83
|
+
* row's identity, one synthetic query per identity — `from <root> where id ==
|
|
84
|
+
* "<id>"` with the slot items appended, named `__omg_sel_<effectIndex>_<slot>`
|
|
85
|
+
* — runs, and the row binds the slots exactly as a selector's hit would. The
|
|
86
|
+
* concrete effects feed the lowering unchanged; `templates` is the list as
|
|
87
|
+
* written, for the intent record.
|
|
88
|
+
*/
|
|
89
|
+
export declare function compilePlain(store: Store, repoId: string, raw: unknown): CompiledPlain;
|