@omgbase/core 0.19.0 → 0.23.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/ids.d.ts +1 -1
- package/dist/src/core/ingest.d.ts +2 -0
- package/dist/src/core/ingest.js +13 -0
- package/dist/src/core/read/document.d.ts +13 -9
- package/dist/src/core/read/document.js +18 -8
- package/dist/src/core/store/archive.d.ts +196 -0
- package/dist/src/core/store/archive.js +606 -0
- package/dist/src/core/store/gc.d.ts +37 -4
- package/dist/src/core/store/gc.js +62 -28
- package/dist/src/core/store/rebuild.d.ts +2 -0
- package/dist/src/core/store/rebuild.js +8 -2
- package/dist/src/core/store/retention.d.ts +138 -0
- package/dist/src/core/store/retention.js +409 -0
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +30 -4
- package/dist/src/core/store/size.d.ts +24 -2
- package/dist/src/core/store/size.js +33 -3
- package/dist/src/core/store/store.d.ts +1 -0
- package/dist/src/core/store/store.js +53 -1
- package/dist/src/graph/history.d.ts +2 -0
- package/dist/src/graph/history.js +8 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/mcp/arguments.d.ts +10 -2
- package/dist/src/mcp/arguments.js +37 -3
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +4 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +58 -8
- package/dist/src/mcp/server.js +51 -24
- package/dist/src/mutate/effects.d.ts +23 -5
- package/dist/src/mutate/effects.js +111 -22
- package/dist/src/mutate/known-ids.d.ts +19 -0
- package/dist/src/mutate/known-ids.js +47 -7
- package/dist/src/mutate/ops.d.ts +1 -0
- package/dist/src/mutate/ops.js +20 -1
- package/dist/src/mutate/select.d.ts +0 -11
- package/dist/src/mutate/select.js +33 -9
- package/dist/src/oqx-js/context.d.ts +9 -2
- package/dist/src/oqx-js/context.js +92 -30
- package/dist/src/oqx-js/dates.d.ts +105 -0
- package/dist/src/oqx-js/dates.js +427 -0
- package/dist/src/oqx-js/names.d.ts +40 -0
- package/dist/src/oqx-js/names.js +358 -0
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +30 -11
- package/dist/src/oqx-js/root-row.d.ts +15 -4
- package/dist/src/oqx-js/root-row.js +18 -10
- package/dist/src/oqx-js/run.d.ts +19 -1
- package/dist/src/oqx-js/run.js +97 -13
- package/dist/src/oqx-js/sql/translate.js +1 -1
- package/dist/src/spec-versions.d.ts +5 -5
- package/dist/src/spec-versions.js +4 -4
- package/dist/src/sync/observe.js +9 -3
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@ import { canonicalDocPath, composeFile, parseNewDoc, planInboundRetarget, splitF
|
|
|
8
8
|
import { linksRepair, nodeSet } from "./macros.js";
|
|
9
9
|
import { planUpdate } from "./plan-update.js";
|
|
10
10
|
import { MutationError, rawHashHex, renderDoc } from "./tree.js";
|
|
11
|
+
import { fixRequest, withMember } from "./canonical.js";
|
|
11
12
|
export const EFFECT_OPS = ["create", "insert", "set", "unset", "move", "remove", "split", "merge", "retarget"];
|
|
12
13
|
/** `$new.<n>`: an identity an earlier effect of the same request creates (§11.1). */
|
|
13
14
|
export const NEW_SYMBOL_RE = /^\$new\.(0|[1-9][0-9]*)$/;
|
|
@@ -18,10 +19,86 @@ export function isNewSymbol(s) {
|
|
|
18
19
|
function isRecord(v) {
|
|
19
20
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
20
21
|
}
|
|
21
|
-
/** The argument-shape error of the surface, thrown from the planner: `filter_invalid { reason, hint: "arguments" }
|
|
22
|
-
export function effectInvalid(message) {
|
|
23
|
-
return new MutationError("filter_invalid", message, { reason: message, hint: "arguments" });
|
|
22
|
+
/** The argument-shape error of the surface, thrown from the planner: `filter_invalid { reason, hint: "arguments" }` — with a `fix` (3.4, §8) when the shape check can spell the corrected call. */
|
|
23
|
+
export function effectInvalid(message, fix) {
|
|
24
|
+
return new MutationError("filter_invalid", message, { reason: message, hint: "arguments", ...(fix !== undefined ? { fix } : {}) });
|
|
24
25
|
}
|
|
26
|
+
function fixerFor(ctx, i, v) {
|
|
27
|
+
if (ctx === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const list = Array.isArray(ctx.request.effects) ? ctx.request.effects : undefined;
|
|
30
|
+
const written = list !== undefined && isRecord(list[i]) ? list[i] : v;
|
|
31
|
+
return {
|
|
32
|
+
written,
|
|
33
|
+
build: (effect, expect) => fixRequest({
|
|
34
|
+
...ctx.request,
|
|
35
|
+
effects: withMember(ctx.request.effects, [String(i)], effect),
|
|
36
|
+
...(expect !== undefined ? { expect: expect.value } : {}),
|
|
37
|
+
}),
|
|
38
|
+
parentOf: ctx.parentOf,
|
|
39
|
+
request: ctx.request,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** A record whose every value is a string. */
|
|
43
|
+
function isStringRecord(v) {
|
|
44
|
+
return isRecord(v) && Object.values(v).every((x) => typeof x === "string");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* §8 (3.4): an effect-level `expect.max` — the bound belongs to the request's
|
|
48
|
+
* top-level `expect`, beside `select`. The fix drops it from the effect (the
|
|
49
|
+
* rest of that `expect` stays; an emptied `expect` goes) and, when the request
|
|
50
|
+
* has a `select`, moves it to the top level unless a top-level `expect.max` is
|
|
51
|
+
* already there (then the top-level one is kept as written); without a
|
|
52
|
+
* `select` a top-level `expect` would itself be refused, so the fix only drops it.
|
|
53
|
+
*/
|
|
54
|
+
function effectLevelMax(v, here, f) {
|
|
55
|
+
const message = `${here}: \`expect.max\` bounds a selection — it is the request's top-level \`expect\`, beside \`select\`, not an effect's`;
|
|
56
|
+
if (f === undefined)
|
|
57
|
+
return effectInvalid(message);
|
|
58
|
+
const { max, ...rest } = v;
|
|
59
|
+
const effect = { ...f.written, expect: Object.keys(rest).length > 0 ? rest : undefined };
|
|
60
|
+
if (f.request.select === undefined)
|
|
61
|
+
return effectInvalid(message, f.build(effect));
|
|
62
|
+
const top = isRecord(f.request.expect) ? f.request.expect : {};
|
|
63
|
+
return effectInvalid(message, f.build(effect, { value: "max" in top ? top : { ...top, max } }));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* §8 (3.4): `at` given as the bare string `"after"` / `"before"` with a block
|
|
67
|
+
* `into` — "right after block X" is `into: X's parent, at: { after: X }`. The
|
|
68
|
+
* fix needs the block's live row (its parent, or its document when it is top-
|
|
69
|
+
* level, spelled as the rooted path); a block the store does not know gets no fix.
|
|
70
|
+
*/
|
|
71
|
+
function bareAnchor(at, into, here, f) {
|
|
72
|
+
const message = `${here}: \`at\` must be "start", "end", { before: <block id> } or { after: <block id> } — "${at} block X" is into: X's parent (a block, or the document when X is top-level), at: { ${at}: X }`;
|
|
73
|
+
if (f === undefined || into.kind !== "block")
|
|
74
|
+
return effectInvalid(message);
|
|
75
|
+
const row = f.parentOf(into.id);
|
|
76
|
+
if (row === null)
|
|
77
|
+
return effectInvalid(message);
|
|
78
|
+
const parent = row.parent === null ? referencePath(row.path) : { kind: "block", id: row.parent };
|
|
79
|
+
return effectInvalid(message, f.build({ ...f.written, into: parent, at: { [at]: into.id } }));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* §8 (3.4): a plural-target `expect` written as `{ content_hash: { <id>: <hash>, … } }`
|
|
83
|
+
* (every key a guard name, every value a map of strings) — the fix inverts the
|
|
84
|
+
* nesting to `{ <id>: { content_hash: <hash> }, … }`.
|
|
85
|
+
*/
|
|
86
|
+
function invertedExpectMap(v, here, f) {
|
|
87
|
+
const keys = Object.keys(v);
|
|
88
|
+
if (keys.length === 0 || !keys.every((k) => k === "content_hash" || k === "parent_children_hash") || !Object.values(v).every(isStringRecord))
|
|
89
|
+
return null;
|
|
90
|
+
const message = `${here}: a plural-target \`expect\` is keyed by block id — { <block id>: { content_hash?, parent_children_hash? }, … }`;
|
|
91
|
+
if (f === undefined)
|
|
92
|
+
return effectInvalid(message);
|
|
93
|
+
const inverted = {};
|
|
94
|
+
for (const [guard, byId] of Object.entries(v)) {
|
|
95
|
+
for (const [id, hash] of Object.entries(byId))
|
|
96
|
+
inverted[id] = { ...inverted[id], [guard]: hash };
|
|
97
|
+
}
|
|
98
|
+
return effectInvalid(message, f.build({ ...f.written, expect: inverted }));
|
|
99
|
+
}
|
|
100
|
+
/** §8 (3.4): the block-field misspellings the fix corrects to `markdown`. */
|
|
101
|
+
const BLOCK_FIELD_ALIASES = new Set(["text", "content", "body", "raw"]);
|
|
25
102
|
function onlyKeys(e, allowed, here) {
|
|
26
103
|
const extra = Object.keys(e).filter((k) => !allowed.includes(k));
|
|
27
104
|
if (extra.length > 0)
|
|
@@ -118,7 +195,7 @@ function validateTargets(v, here) {
|
|
|
118
195
|
throw effectInvalid(`${here}: \`targets\` must be a non-empty array of block targets`);
|
|
119
196
|
return v.map((t, i) => validateTarget(t, `${here}.targets[${i}]`, ["block"]));
|
|
120
197
|
}
|
|
121
|
-
function validateAt(v, here) {
|
|
198
|
+
function validateAt(v, here, into, f) {
|
|
122
199
|
if (v === undefined)
|
|
123
200
|
return undefined;
|
|
124
201
|
if (v === "start" || v === "end")
|
|
@@ -129,13 +206,17 @@ function validateAt(v, here) {
|
|
|
129
206
|
if (typeof v.after === "string")
|
|
130
207
|
return { after: v.after };
|
|
131
208
|
}
|
|
209
|
+
if ((v === "before" || v === "after") && into !== undefined)
|
|
210
|
+
throw bareAnchor(v, into, here, f);
|
|
132
211
|
throw effectInvalid(`${here}: \`at\` must be "start", "end", { before: <block id> } or { after: <block id> }`);
|
|
133
212
|
}
|
|
134
|
-
function validateExpect(v, here) {
|
|
213
|
+
function validateExpect(v, here, f) {
|
|
135
214
|
if (v === undefined)
|
|
136
215
|
return undefined;
|
|
137
216
|
if (!isRecord(v))
|
|
138
217
|
throw effectInvalid(`${here}: \`expect\` must be an object`);
|
|
218
|
+
if ("max" in v)
|
|
219
|
+
throw effectLevelMax(v, here, f);
|
|
139
220
|
onlyKeys(v, ["content_hash", "parent_children_hash", "revision"], `${here}.expect`);
|
|
140
221
|
const out = {};
|
|
141
222
|
for (const k of ["content_hash", "parent_children_hash", "revision"]) {
|
|
@@ -145,11 +226,16 @@ function validateExpect(v, here) {
|
|
|
145
226
|
}
|
|
146
227
|
return out;
|
|
147
228
|
}
|
|
148
|
-
function validateExpectMap(v, here) {
|
|
229
|
+
function validateExpectMap(v, here, f) {
|
|
149
230
|
if (v === undefined)
|
|
150
231
|
return undefined;
|
|
151
232
|
if (!isRecord(v))
|
|
152
233
|
throw effectInvalid(`${here}: \`expect\` must be an object keyed by block id`);
|
|
234
|
+
if ("max" in v)
|
|
235
|
+
throw effectLevelMax(v, here, f);
|
|
236
|
+
const inverted = invertedExpectMap(v, here, f);
|
|
237
|
+
if (inverted !== null)
|
|
238
|
+
throw inverted;
|
|
153
239
|
const out = {};
|
|
154
240
|
for (const [id, x] of Object.entries(v)) {
|
|
155
241
|
if (!isRecord(x))
|
|
@@ -166,13 +252,14 @@ function validateExpectMap(v, here) {
|
|
|
166
252
|
}
|
|
167
253
|
return out;
|
|
168
254
|
}
|
|
169
|
-
/** The shape check of one effect (§11.1). `i` names it in the message (`effects[i]`). */
|
|
170
|
-
export function validateEffect(v, i) {
|
|
255
|
+
/** The shape check of one effect (§11.1). `i` names it in the message (`effects[i]`); `fix` (3.4) lets the shape errors of §8 carry their corrected call. */
|
|
256
|
+
export function validateEffect(v, i, fix) {
|
|
171
257
|
const here = `effects[${i}]`;
|
|
172
258
|
if (!isRecord(v) || typeof v.op !== "string")
|
|
173
259
|
throw effectInvalid(`${here}: an effect is an object whose \`op\` is one of ${EFFECT_OPS.join("/")}`);
|
|
174
260
|
if (!EFFECT_OPS.includes(v.op))
|
|
175
261
|
throw effectInvalid(`${here}: \`op\` must be one of ${EFFECT_OPS.join("/")} (got "${v.op}")`);
|
|
262
|
+
const f = fixerFor(fix, i, v);
|
|
176
263
|
switch (v.op) {
|
|
177
264
|
case "create": {
|
|
178
265
|
onlyKeys(v, ["op", "path", "markdown", "frontmatter"], here);
|
|
@@ -185,9 +272,9 @@ export function validateEffect(v, i) {
|
|
|
185
272
|
case "insert": {
|
|
186
273
|
onlyKeys(v, ["op", "into", "at", "markdown", "expect"], here);
|
|
187
274
|
const into = validateTarget(v.into, `${here}.into`, ["document", "section", "block"]);
|
|
188
|
-
const at = validateAt(v.at, here);
|
|
275
|
+
const at = validateAt(v.at, here, into, f);
|
|
189
276
|
const markdown = requireString(v, "markdown", here);
|
|
190
|
-
const expect = validateExpect(v.expect, here);
|
|
277
|
+
const expect = validateExpect(v.expect, here, f);
|
|
191
278
|
return { op: "insert", into, ...(at !== undefined ? { at } : {}), markdown, ...(expect !== undefined ? { expect } : {}) };
|
|
192
279
|
}
|
|
193
280
|
case "set": {
|
|
@@ -197,8 +284,8 @@ export function validateEffect(v, i) {
|
|
|
197
284
|
if (!("value" in v))
|
|
198
285
|
throw effectInvalid(`${here}: \`value\` is required`);
|
|
199
286
|
if (target.kind !== "symbol")
|
|
200
|
-
checkField(target.kind, field, here, "set");
|
|
201
|
-
const expect = validateExpect(v.expect, here);
|
|
287
|
+
checkField(target.kind, field, here, "set", f);
|
|
288
|
+
const expect = validateExpect(v.expect, here, f);
|
|
202
289
|
return { op: "set", target, field, value: v.value, ...(expect !== undefined ? { expect } : {}) };
|
|
203
290
|
}
|
|
204
291
|
case "unset": {
|
|
@@ -207,7 +294,7 @@ export function validateEffect(v, i) {
|
|
|
207
294
|
const field = requireString(v, "field", here);
|
|
208
295
|
if (target.kind !== "symbol")
|
|
209
296
|
checkField(target.kind, field, here, "unset");
|
|
210
|
-
const expect = validateExpect(v.expect, here);
|
|
297
|
+
const expect = validateExpect(v.expect, here, f);
|
|
211
298
|
return { op: "unset", target, field, ...(expect !== undefined ? { expect } : {}) };
|
|
212
299
|
}
|
|
213
300
|
case "move": {
|
|
@@ -215,8 +302,8 @@ export function validateEffect(v, i) {
|
|
|
215
302
|
onlyKeys(v, ["op", "targets", "into", "at", "expect"], here);
|
|
216
303
|
const targets = validateTargets(v.targets, here);
|
|
217
304
|
const into = validateTarget(v.into, `${here}.into`, ["document", "section", "block"]);
|
|
218
|
-
const at = validateAt(v.at, here);
|
|
219
|
-
const expect = validateExpect(v.expect, here);
|
|
305
|
+
const at = validateAt(v.at, here, into, f);
|
|
306
|
+
const expect = validateExpect(v.expect, here, f);
|
|
220
307
|
return { op: "move", targets, into, ...(at !== undefined ? { at } : {}), ...(expect !== undefined ? { expect } : {}) };
|
|
221
308
|
}
|
|
222
309
|
onlyKeys(v, ["op", "target", "to", "retarget_inbound"], here);
|
|
@@ -230,7 +317,7 @@ export function validateEffect(v, i) {
|
|
|
230
317
|
if ("targets" in v) {
|
|
231
318
|
onlyKeys(v, ["op", "targets", "expect"], here);
|
|
232
319
|
const targets = validateTargets(v.targets, here);
|
|
233
|
-
const expect = validateExpectMap(v.expect, here);
|
|
320
|
+
const expect = validateExpectMap(v.expect, here, f);
|
|
234
321
|
return { op: "remove", targets, ...(expect !== undefined ? { expect } : {}) };
|
|
235
322
|
}
|
|
236
323
|
onlyKeys(v, ["op", "target"], here);
|
|
@@ -241,14 +328,14 @@ export function validateEffect(v, i) {
|
|
|
241
328
|
const target = validateTarget(v.target, `${here}.target`, ["block"]);
|
|
242
329
|
if (!Array.isArray(v.at) || v.at.some((n) => typeof n !== "number" || !Number.isInteger(n) || n < 0))
|
|
243
330
|
throw effectInvalid(`${here}: \`at\` must be an array of byte offsets`);
|
|
244
|
-
const expect = validateExpect(v.expect, here);
|
|
331
|
+
const expect = validateExpect(v.expect, here, f);
|
|
245
332
|
return { op: "split", target, at: v.at, ...(expect !== undefined ? { expect } : {}) };
|
|
246
333
|
}
|
|
247
334
|
case "merge": {
|
|
248
335
|
onlyKeys(v, ["op", "targets", "separator", "expect"], here);
|
|
249
336
|
const targets = validateTargets(v.targets, here);
|
|
250
337
|
const separator = optionalString(v, "separator", here);
|
|
251
|
-
const expect = validateExpectMap(v.expect, here);
|
|
338
|
+
const expect = validateExpectMap(v.expect, here, f);
|
|
252
339
|
return { op: "merge", targets, ...(separator !== undefined ? { separator } : {}), ...(expect !== undefined ? { expect } : {}) };
|
|
253
340
|
}
|
|
254
341
|
case "retarget": {
|
|
@@ -267,16 +354,16 @@ export function validateEffect(v, i) {
|
|
|
267
354
|
}
|
|
268
355
|
}
|
|
269
356
|
/** The whole list; a non-array or an empty list is a shape error too. */
|
|
270
|
-
export function validateEffects(v) {
|
|
357
|
+
export function validateEffects(v, fix) {
|
|
271
358
|
if (!Array.isArray(v))
|
|
272
359
|
throw effectInvalid("`effects` must be an array of effects");
|
|
273
360
|
if (v.length === 0)
|
|
274
361
|
throw effectInvalid("`effects` must name at least one effect");
|
|
275
|
-
return v.map((e, i) => validateEffect(e, i));
|
|
362
|
+
return v.map((e, i) => validateEffect(e, i, fix));
|
|
276
363
|
}
|
|
277
364
|
// §11.2: the `field` spellings per target kind.
|
|
278
365
|
export const FRONTMATTER_FIELD = /^frontmatter\.(.+)$/;
|
|
279
|
-
function checkField(kind, field, here, op) {
|
|
366
|
+
function checkField(kind, field, here, op, f) {
|
|
280
367
|
if (kind === "document") {
|
|
281
368
|
if (FRONTMATTER_FIELD.test(field) || (op === "set" && field === "content"))
|
|
282
369
|
return;
|
|
@@ -285,7 +372,9 @@ function checkField(kind, field, here, op) {
|
|
|
285
372
|
if (kind === "block") {
|
|
286
373
|
if (field === "markdown" || field === "attrs.checked")
|
|
287
374
|
return;
|
|
288
|
-
|
|
375
|
+
// (3.4, §8) `text` / `content` / `body` / `raw` on a block: the fix spells `markdown`.
|
|
376
|
+
const fix = f !== undefined && BLOCK_FIELD_ALIASES.has(field) ? f.build({ ...f.written, field: "markdown" }) : undefined;
|
|
377
|
+
throw effectInvalid(`${here}: a block's \`field\` is \`markdown\` or \`attrs.checked\` (got "${field}")`, fix);
|
|
289
378
|
}
|
|
290
379
|
// node: the adapter's editable props (`node_not_editable` names them when unknown)
|
|
291
380
|
if (field === "" || field.includes("."))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
|
+
import type { TreeInputBlock } from "../core/store/writers.js";
|
|
2
3
|
import type { IdResolver } from "../core/ingest.js";
|
|
3
4
|
import type { MutDoc } from "./tree.js";
|
|
4
5
|
/**
|
|
@@ -10,3 +11,21 @@ import type { MutDoc } from "./tree.js";
|
|
|
10
11
|
export declare function makeKnownIdResolver(store: Store, repoId: string, doc: MutDoc, priorIds: Set<string>, opts?: {
|
|
11
12
|
path?: string;
|
|
12
13
|
}): IdResolver;
|
|
14
|
+
/** A carried block's live row, as spec/reconcile §2's carry classification reads it. */
|
|
15
|
+
interface PriorRow {
|
|
16
|
+
parent: string | null;
|
|
17
|
+
ordinal: number;
|
|
18
|
+
rawHashHex: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* spec/reconcile §2 "Classifying a carry", applied to the api path's known ids:
|
|
22
|
+
* `moved` = the parent or the sibling index differs from the live row,
|
|
23
|
+
* `content_equal` = the raw bytes hash the same; `same` / `moved` / `edited` /
|
|
24
|
+
* `edited_moved` follow. An id with no prior row (new to this document — a
|
|
25
|
+
* mint or a cross-document arrival) is `inserted`. Pre-order over the tree.
|
|
26
|
+
*/
|
|
27
|
+
export declare function classifyCarries(blocks: TreeInputBlock[], prior: Map<string, PriorRow>, priorIds: Set<string>): {
|
|
28
|
+
id: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
}[];
|
|
31
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mintId } from "../core/ids.js";
|
|
2
|
+
import { sha256 } from "../core/hash.js";
|
|
2
3
|
import { extractFromBlock, extractFromFrontmatter, resolveRelativePath } from "../graph/extract.js";
|
|
3
4
|
import { resolveExternal, resolveDocPath } from "../core/store/edges.js";
|
|
4
5
|
import { adapterForPath } from "../format/index.js";
|
|
@@ -79,12 +80,19 @@ export function makeKnownIdResolver(store, repoId, doc, priorIds, opts = {}) {
|
|
|
79
80
|
const assigned = assignFromMut(rest, byKey);
|
|
80
81
|
const nowIds = collectIds(assigned);
|
|
81
82
|
const nowSet = new Set(nowIds);
|
|
82
|
-
// Dispositions are INTENT, not inference
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
// Dispositions are INTENT, not inference (confidence 1.0, reason `api`): an
|
|
84
|
+
// id new this commit is `inserted`; a prior id no longer present is
|
|
85
|
+
// `deleted` (pooled, no row); a carried id is classified exactly as
|
|
86
|
+
// spec/reconcile §2 classifies a carry — `same` when its raw bytes, parent
|
|
87
|
+
// and sibling index are those of its live row, else `edited` / `moved` /
|
|
88
|
+
// `edited_moved` — so a block the changeset did not touch is `same` and
|
|
89
|
+
// (spec/store §5.4 step 10, 14.2) never persisted: a one-paragraph edit
|
|
90
|
+
// moves that paragraph's `updated_at`, not its siblings'. Through store
|
|
91
|
+
// 14.1 every carried block was stamped `edited`.
|
|
92
|
+
const prior = docId === null ? new Map() : priorRows(db, docId);
|
|
93
|
+
const dispositions = classifyCarries(assigned, prior, priorIds).map((c) => ({
|
|
94
|
+
blockId: c.id,
|
|
95
|
+
kind: c.kind,
|
|
88
96
|
confidence: 1,
|
|
89
97
|
reason: "api",
|
|
90
98
|
matcherV: null,
|
|
@@ -121,10 +129,42 @@ export function makeKnownIdResolver(store, repoId, doc, priorIds, opts = {}) {
|
|
|
121
129
|
}
|
|
122
130
|
return out;
|
|
123
131
|
};
|
|
124
|
-
void docId;
|
|
125
132
|
return { assigned, dispositions, deleted, crossDocIds, extractEdges };
|
|
126
133
|
};
|
|
127
134
|
}
|
|
135
|
+
function priorRows(db, docId) {
|
|
136
|
+
const rows = db
|
|
137
|
+
.prepare("SELECT block_id, parent_block, ordinal, raw_hash FROM blocks WHERE doc_id = ? AND deleted_commit IS NULL")
|
|
138
|
+
.all(docId);
|
|
139
|
+
return new Map(rows.map((r) => [r.block_id, { parent: r.parent_block, ordinal: r.ordinal, rawHashHex: r.raw_hash.toString("hex") }]));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* spec/reconcile §2 "Classifying a carry", applied to the api path's known ids:
|
|
143
|
+
* `moved` = the parent or the sibling index differs from the live row,
|
|
144
|
+
* `content_equal` = the raw bytes hash the same; `same` / `moved` / `edited` /
|
|
145
|
+
* `edited_moved` follow. An id with no prior row (new to this document — a
|
|
146
|
+
* mint or a cross-document arrival) is `inserted`. Pre-order over the tree.
|
|
147
|
+
*/
|
|
148
|
+
export function classifyCarries(blocks, prior, priorIds) {
|
|
149
|
+
const out = [];
|
|
150
|
+
const walk = (list, parent) => {
|
|
151
|
+
list.forEach((b, index) => {
|
|
152
|
+
const row = priorIds.has(b.blockId) ? prior.get(b.blockId) : undefined;
|
|
153
|
+
if (!row) {
|
|
154
|
+
out.push({ id: b.blockId, kind: "inserted" });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const moved = row.parent !== parent || row.ordinal !== index;
|
|
158
|
+
const contentEqual = row.rawHashHex === sha256(b.raw).toString("hex");
|
|
159
|
+
out.push({ id: b.blockId, kind: contentEqual ? (moved ? "moved" : "same") : moved ? "edited_moved" : "edited" });
|
|
160
|
+
}
|
|
161
|
+
if (b.children.length > 0)
|
|
162
|
+
walk(b.children, b.blockId);
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
walk(blocks, null);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
128
168
|
// Edge resolution — mirrors reconciling-ingest's resolvers (block-anchor targets
|
|
129
169
|
// resolve to the document node in v1; the anchor is preserved on the edge).
|
|
130
170
|
function resolveEdge(db, repoId, srcDoc, e, docDir) {
|
package/dist/src/mutate/ops.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare function opRemove(doc: MutDoc, blockIds: string[], opIndex: numbe
|
|
|
35
35
|
export declare function opSplit(doc: MutDoc, blockId: string, at: number[], opIndex: number, expect?: Expect): {
|
|
36
36
|
ids: string[];
|
|
37
37
|
};
|
|
38
|
+
export declare function trimCutEdges(pieces: readonly string[]): string[];
|
|
38
39
|
export declare function opMerge(doc: MutDoc, blockIds: string[], opIndex: number, separator?: string, expectPer?: Record<string, Expect>): {
|
|
39
40
|
ids: string[];
|
|
40
41
|
mergedInto: string[];
|
package/dist/src/mutate/ops.js
CHANGED
|
@@ -505,7 +505,7 @@ export function opSplit(doc, blockId, at, opIndex, expect) {
|
|
|
505
505
|
const pieces = [];
|
|
506
506
|
for (let i = 1; i < cuts.length; i++)
|
|
507
507
|
pieces.push(raw.slice(cuts[i - 1], cuts[i]));
|
|
508
|
-
const nonEmpty = pieces.filter((p) => p.trim().length > 0);
|
|
508
|
+
const nonEmpty = trimCutEdges(pieces).filter((p) => p.trim().length > 0);
|
|
509
509
|
if (nonEmpty.length < 2)
|
|
510
510
|
throw new MutationError("type_mismatch", "split must yield ≥2 non-empty fragments");
|
|
511
511
|
// First fragment carries the id (authored intent, 04 §1); rest minted.
|
|
@@ -533,6 +533,25 @@ export function opSplit(doc, blockId, at, opIndex, expect) {
|
|
|
533
533
|
markContainerDirty(doc, found.siblings);
|
|
534
534
|
return { ids: [blockId, ...newBlocks.map((b) => b.id)] };
|
|
535
535
|
}
|
|
536
|
+
// spec/mutate §2.5 (3.5): the pieces of a split are trimmed at the CUT edges
|
|
537
|
+
// only — every piece but the last loses its trailing whitespace, every piece
|
|
538
|
+
// but the first its leading whitespace — so an offset on either side of a
|
|
539
|
+
// space (or inside the run, a `\n`, a hard break's ` \n`) yields the same
|
|
540
|
+
// pieces and no piece ends in a stray space. The raw's own start and end are
|
|
541
|
+
// not cut edges and stay as authored (a last piece keeps the raw's trailing
|
|
542
|
+
// spaces). The whitespace set is the format's trim set (`spec/format` §4.1
|
|
543
|
+
// step 4) — exactly what JavaScript's `trimStart`/`trimEnd` remove.
|
|
544
|
+
export function trimCutEdges(pieces) {
|
|
545
|
+
const last = pieces.length - 1;
|
|
546
|
+
return pieces.map((p, i) => {
|
|
547
|
+
let out = p;
|
|
548
|
+
if (i < last)
|
|
549
|
+
out = out.trimEnd();
|
|
550
|
+
if (i > 0)
|
|
551
|
+
out = out.trimStart();
|
|
552
|
+
return out;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
536
555
|
export function opMerge(doc, blockIds, opIndex, separator = " ", expectPer) {
|
|
537
556
|
if (blockIds.length < 2)
|
|
538
557
|
throw new MutationError("not_contiguous", "merge requires ≥2 blocks");
|
|
@@ -75,15 +75,4 @@ export declare function desugarEffect(raw: unknown, index: number, request?: Fix
|
|
|
75
75
|
export declare function compileSelection(store: Store, repoId: string, select: unknown, expectRaw: unknown, templatesRaw: unknown): CompiledSelection;
|
|
76
76
|
/** The query a selector request compiles to (admission + rewrite, before any row is read) — `print` it to see what runs. */
|
|
77
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
78
|
export declare function compilePlain(store: Store, repoId: string, raw: unknown): CompiledPlain;
|
|
@@ -294,7 +294,7 @@ function containsOqx(v) {
|
|
|
294
294
|
* defaults to a hit, and its primary target may not be an expression (the
|
|
295
295
|
* primary target IS the row the expressions read).
|
|
296
296
|
*/
|
|
297
|
-
function admitTemplate(raw, index, root, plain) {
|
|
297
|
+
function admitTemplate(raw, index, root, plain, fix) {
|
|
298
298
|
const here = `effects[${index}]`;
|
|
299
299
|
if (!isRecord(raw) || typeof raw.op !== "string")
|
|
300
300
|
throw selectionInvalid(`${here}: a template is an effect object whose \`op\` is one of ${EFFECT_OPS.filter((o) => o !== "retarget").join("/")}`);
|
|
@@ -501,8 +501,8 @@ function admitTemplate(raw, index, root, plain) {
|
|
|
501
501
|
if (containsOqx(raw[key]))
|
|
502
502
|
throw selectionInvalid(`${here}.${key}: \`{ oqx }\` is not allowed here`);
|
|
503
503
|
}
|
|
504
|
-
// The shape, with a probe in every slot (the §11.1 messages, naming `effects[i]
|
|
505
|
-
validateEffect(probe, index);
|
|
504
|
+
// The shape, with a probe in every slot (the §11.1 messages, naming `effects[i]`; the §8 shape fixes rewrite the template as written).
|
|
505
|
+
validateEffect(probe, index, fix);
|
|
506
506
|
return { index, base, slots, defaultTarget };
|
|
507
507
|
}
|
|
508
508
|
/** The `id`/`path` member a bound identity value supplies: a string as given, a row ref by its id. */
|
|
@@ -527,7 +527,7 @@ function validateExpect(v) {
|
|
|
527
527
|
/**
|
|
528
528
|
* The fix of a refused selector source (3.3): the same request over an
|
|
529
529
|
* admitted root — `links` becomes `blocks` (a link lives in a block), any other
|
|
530
|
-
* source `docs` — spliced over the source's span of the written query.
|
|
530
|
+
* source (`revisions` included) `docs` — spliced over the source's span of the written query.
|
|
531
531
|
*/
|
|
532
532
|
function rootFix(select, source, request) {
|
|
533
533
|
if (request === undefined)
|
|
@@ -560,6 +560,9 @@ function admitSelector(select, request) {
|
|
|
560
560
|
if (raw.values)
|
|
561
561
|
throw selectionInvalid("select: a selector yields rows, not `values`");
|
|
562
562
|
const root = rootTarget(raw.source);
|
|
563
|
+
if (root === null && raw.source.kind === "ident" && raw.source.name === "revisions") {
|
|
564
|
+
throw selectionInvalid("select: `revisions` rows have no effect target — select docs, blocks or nodes", rootFix(select, raw.source, request));
|
|
565
|
+
}
|
|
563
566
|
if (root === null)
|
|
564
567
|
throw selectionInvalid("select: a selector scans a root collection — `from docs`, `from blocks` or `from nodes`", rootFix(select, raw.source, request));
|
|
565
568
|
if (root === "links")
|
|
@@ -583,6 +586,10 @@ function runQuery(store, repoId, q, limit) {
|
|
|
583
586
|
catch (e) {
|
|
584
587
|
if (e instanceof OqxError)
|
|
585
588
|
throw new FilterInvalid(e.message, "OQX");
|
|
589
|
+
// An unknown name (spec/surface §1.5) is refused before any row is read; a
|
|
590
|
+
// selector's refusal wears the admission prefix and hint like every other.
|
|
591
|
+
if (e instanceof FilterInvalid)
|
|
592
|
+
throw selectionInvalid(`select: ${e.message}`);
|
|
586
593
|
throw e;
|
|
587
594
|
}
|
|
588
595
|
}
|
|
@@ -639,7 +646,9 @@ function admitRequest(select, templatesRaw, request) {
|
|
|
639
646
|
if (templatesRaw.length === 0)
|
|
640
647
|
throw selectionInvalid("`effects` must name at least one template");
|
|
641
648
|
const { raw, root } = admitSelector(select, request);
|
|
642
|
-
|
|
649
|
+
// Under a selector a template's `into` is bound per hit, so the anchor fix (a parent lookup) does not apply: `parentOf` finds nothing.
|
|
650
|
+
const fix = request === undefined ? undefined : { request, parentOf: () => null };
|
|
651
|
+
const templates = templatesRaw.map((t, i) => admitTemplate(desugarEffect(t, i, request), i, root, false, fix));
|
|
643
652
|
// The rewrite: the slot items, named `__omg_sel_<t>_<slot>`, appended after
|
|
644
653
|
// the user's items on the raw tree (so a user alias resolves into them).
|
|
645
654
|
const items = [];
|
|
@@ -826,28 +835,43 @@ function scopeOf(store, repoId, e, i) {
|
|
|
826
835
|
* concrete effects feed the lowering unchanged; `templates` is the list as
|
|
827
836
|
* written, for the intent record.
|
|
828
837
|
*/
|
|
838
|
+
/**
|
|
839
|
+
* The live-row lookup behind the anchor fix (§8, 3.4): a block's parent id
|
|
840
|
+
* (`null` at the top level) and its document's path, from the `blocks` row
|
|
841
|
+
* that is not deleted; `null` for an id the store does not know (a `$new.n`,
|
|
842
|
+
* a typo) — then there is no fix.
|
|
843
|
+
*/
|
|
844
|
+
function parentLookup(store) {
|
|
845
|
+
return (id) => {
|
|
846
|
+
const row = store.db
|
|
847
|
+
.prepare("SELECT b.parent_block AS parent, d.path AS path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ? AND b.deleted_commit IS NULL")
|
|
848
|
+
.get(id);
|
|
849
|
+
return row === undefined ? null : { parent: row.parent, path: row.path };
|
|
850
|
+
};
|
|
851
|
+
}
|
|
829
852
|
export function compilePlain(store, repoId, raw) {
|
|
830
853
|
if (!Array.isArray(raw))
|
|
831
854
|
throw effectInvalid("`effects` must be an array of effects");
|
|
832
855
|
if (raw.length === 0)
|
|
833
856
|
throw effectInvalid("`effects` must name at least one effect");
|
|
834
857
|
const request = { effects: raw };
|
|
858
|
+
const fix = { request, parentOf: parentLookup(store) };
|
|
835
859
|
const desugared = raw.map((e, i) => desugarEffect(e, i, request));
|
|
836
860
|
if (!desugared.some(containsOqx))
|
|
837
|
-
return { effects: desugared.map((e, i) => validateEffect(e, i)), origins: [] };
|
|
861
|
+
return { effects: desugared.map((e, i) => validateEffect(e, i, fix)), origins: [] };
|
|
838
862
|
const effects = [];
|
|
839
863
|
const origins = [];
|
|
840
864
|
const scopes = new Map();
|
|
841
865
|
desugared.forEach((e, i) => {
|
|
842
866
|
if (!containsOqx(e) || !isRecord(e)) {
|
|
843
|
-
effects[i] = validateEffect(e, i);
|
|
867
|
+
effects[i] = validateEffect(e, i, fix);
|
|
844
868
|
return;
|
|
845
869
|
}
|
|
846
870
|
const scope = scopeOf(store, repoId, e, i);
|
|
847
|
-
const t = admitTemplate(e, i, scope.root, true);
|
|
871
|
+
const t = admitTemplate(e, i, scope.root, true, fix);
|
|
848
872
|
if (t.slots.length === 0) {
|
|
849
873
|
// `{ oqx }` only inside data (a nested `value`): literal, as for a template.
|
|
850
|
-
effects[i] = validateEffect(e, i);
|
|
874
|
+
effects[i] = validateEffect(e, i, fix);
|
|
851
875
|
return;
|
|
852
876
|
}
|
|
853
877
|
const key = `${scope.root}:${scope.id}`;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { DataContext } from "@omgbase/oqx";
|
|
2
|
+
import { type Root } from "./root-row.js";
|
|
2
3
|
import type { Store } from "../core/store/store.js";
|
|
3
4
|
import type { SemanticVec } from "../search/cel/compile.js";
|
|
4
5
|
export type Target = "docs" | "blocks" | "nodes" | "links";
|
|
6
|
+
/** Every row kind the context tags (spec/surface §1.1): the four targets and
|
|
7
|
+
* `revisions` (4.7) — the history target, whose rows are the `revisions` ×
|
|
8
|
+
* `commits` join. `Target` stays the four kinds the unknown-names pass judges
|
|
9
|
+
* (names.ts keys its tables by it); everything that tags, scans or renders a
|
|
10
|
+
* row is keyed by `Kind`. */
|
|
11
|
+
export type Kind = Root;
|
|
5
12
|
declare const TARGET: unique symbol;
|
|
6
13
|
/** The engine attributes per kind (spec/surface §1.2 table). Exported so the
|
|
7
14
|
* translator's tests pin the two against each other. */
|
|
8
|
-
export declare const ATTRIBUTES: Readonly<Record<
|
|
15
|
+
export declare const ATTRIBUTES: Readonly<Record<Kind, ReadonlySet<string>>>;
|
|
9
16
|
/** The source handles of a document (§1.2): `data` is the merged authored view
|
|
10
17
|
* (frontmatter + inline), the other two one source each. */
|
|
11
18
|
export declare const SOURCE_HANDLES: ReadonlySet<string>;
|
|
@@ -31,7 +38,7 @@ export interface StoreContextOptions {
|
|
|
31
38
|
export declare function makeStoreContext(store: Store, repoId: string, opts?: StoreContextOptions): DataContext;
|
|
32
39
|
/** Tag SQL rows with their target so the store context resolves them (used by the
|
|
33
40
|
* tier-3 planner to hand produced rows back for residual evaluation). */
|
|
34
|
-
export declare function tagRows(rows: Record<string, unknown>[], target:
|
|
41
|
+
export declare function tagRows(rows: Record<string, unknown>[], target: Kind): unknown[];
|
|
35
42
|
/**
|
|
36
43
|
* A store row surfacing as a VALUE in a result (spec/surface §1.4, rows as
|
|
37
44
|
* values, 1.2) is rendered as `{ id, path }` (3.0) — its id column and its
|