@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
|
@@ -8,7 +8,9 @@ import { type LinkRepairCount, type RetargetHit } from "./macros.js";
|
|
|
8
8
|
import type { At, Expect } from "./ops.js";
|
|
9
9
|
import type { IdentityPreview } from "./opset.js";
|
|
10
10
|
import { MutationError } from "./tree.js";
|
|
11
|
-
/** What an effect addresses. `section` is a derived range (ADR-005), never a stored id.
|
|
11
|
+
/** What an effect addresses. `section` is a derived range (ADR-005), never a stored id.
|
|
12
|
+
* `symbol` (3.2, §11.8 "Sugar"): a bare `"$new.n"` target whose kind — document or block — the
|
|
13
|
+
* symbol table decides when the effect is resolved; it never reaches a resolved effect. */
|
|
12
14
|
export type EffectTarget = {
|
|
13
15
|
kind: "document";
|
|
14
16
|
id?: string;
|
|
@@ -22,6 +24,9 @@ export type EffectTarget = {
|
|
|
22
24
|
} | {
|
|
23
25
|
kind: "section";
|
|
24
26
|
heading_id: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "symbol";
|
|
29
|
+
id: string;
|
|
25
30
|
};
|
|
26
31
|
export type EffectAt = At;
|
|
27
32
|
/** Guards: the kernel's two CAS fields plus the document's `current_rev` at plan time. */
|
|
@@ -169,6 +174,23 @@ export type ResolvedEffect = {
|
|
|
169
174
|
};
|
|
170
175
|
/** The argument-shape error of the surface, thrown from the planner: `filter_invalid { reason, hint: "arguments" }`. */
|
|
171
176
|
export declare function effectInvalid(message: string): MutationError;
|
|
177
|
+
/** The `-> ` leader (3.2, §11.8 "Sugar"): a string beginning exactly so is an OQX expression, the rest of the string. */
|
|
178
|
+
export declare const EXPRESSION_LEADER = "-> ";
|
|
179
|
+
/** A path-looking string: contains `/` or ends `.md` (the bare document target of §11.8 "Sugar"). */
|
|
180
|
+
export declare function looksLikePath(s: string): boolean;
|
|
181
|
+
/** A bare id of the three target kinds (§11.8 "Sugar"). */
|
|
182
|
+
export declare function isTargetId(s: string): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* A bare string where a target object is expected (3.2, §11.8 "Sugar"): a
|
|
185
|
+
* path-looking string → `{ kind: "document", path }`; a `b_`/`d_`/`n_` id →
|
|
186
|
+
* that kind; `$new.n` → `{ kind: "symbol", id }` (its kind is the symbol
|
|
187
|
+
* table's at resolution); a `#` locator → `filter_invalid` with a `fix`;
|
|
188
|
+
* anything else is no target. A `-> ` leader never reaches here from the
|
|
189
|
+
* planner (`select.ts` desugars it first); as a library input it is refused
|
|
190
|
+
* the same way, naming the planner.
|
|
191
|
+
*/
|
|
192
|
+
export declare function bareTarget(ref: string, here: string): EffectTarget;
|
|
193
|
+
export declare function validateTarget(v: unknown, here: string, kinds: readonly EffectTarget["kind"][]): EffectTarget;
|
|
172
194
|
/** The shape check of one effect (§11.1). `i` names it in the message (`effects[i]`). */
|
|
173
195
|
export declare function validateEffect(v: unknown, i: number): Effect;
|
|
174
196
|
/** The whole list; a non-array or an empty list is a shape error too. */
|
|
@@ -304,6 +326,8 @@ export interface LoweringContext {
|
|
|
304
326
|
canonical: (v: unknown) => string;
|
|
305
327
|
/** the resolved effects kept (dropped duplicates excluded) */
|
|
306
328
|
resolved: ResolvedEffect[];
|
|
329
|
+
/** `preview.summary` (3.3, §11.9): one line in words per resolved effect, in order */
|
|
330
|
+
summary: string[];
|
|
307
331
|
}
|
|
308
332
|
/** Resolve a document target: an existing document's `{ id, path }`, or a `$new.n` document this plan creates. */
|
|
309
333
|
export declare function resolveDocumentTarget(ctx: LoweringContext, t: Extract<EffectTarget, {
|
|
@@ -311,8 +335,16 @@ export declare function resolveDocumentTarget(ctx: LoweringContext, t: Extract<E
|
|
|
311
335
|
}>): Extract<ResolvedTarget, {
|
|
312
336
|
kind: "document";
|
|
313
337
|
}>;
|
|
314
|
-
/**
|
|
315
|
-
export declare function
|
|
338
|
+
/** An existing document's `{ id, path }` for a literal document target (`doc_missing`; `filter_invalid` when `id` and `path` disagree). */
|
|
339
|
+
export declare function findDocumentTarget(store: Store, repoId: string, t: Extract<EffectTarget, {
|
|
340
|
+
kind: "document";
|
|
341
|
+
}>): Extract<ResolvedTarget, {
|
|
342
|
+
kind: "document";
|
|
343
|
+
}>;
|
|
344
|
+
/** `1 block` / `2 blocks` (an `s` on every noun of the table). */
|
|
345
|
+
export declare function plural(n: number, noun: string): string;
|
|
346
|
+
/** Resolve one validated effect's targets (§11.4): documents to `{ id, path }`, defaults made explicit; `i` names the effect in messages. */
|
|
347
|
+
export declare function resolveEffect(ctx: LoweringContext, e: Effect, i?: number): ResolvedEffect;
|
|
316
348
|
/** Shift a kernel op's `$n.ids[i]` placeholders by `offset` (an opset planned alone, spliced into a longer op list). */
|
|
317
349
|
export declare function shiftPlaceholders(op: Op, offset: number): Op;
|
|
318
350
|
/**
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { findDoc } from "../core/read/reader.js";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeVisibleText } from "../core/hash.js";
|
|
3
|
+
import { referencePath } from "../core/paths.js";
|
|
4
|
+
import { isValidId, withIdMinter } from "../core/ids.js";
|
|
3
5
|
import { inboundLinksTo } from "../graph/inbound-links.js";
|
|
4
6
|
import { readDocBase } from "./commit.js";
|
|
5
7
|
import { canonicalDocPath, composeFile, parseNewDoc, planInboundRetarget, splitFrontmatter } from "./docs.js";
|
|
@@ -39,9 +41,56 @@ function optionalString(e, key, here) {
|
|
|
39
41
|
throw effectInvalid(`${here}: \`${key}\` must be a string`);
|
|
40
42
|
return v;
|
|
41
43
|
}
|
|
42
|
-
|
|
44
|
+
/** The `-> ` leader (3.2, §11.8 "Sugar"): a string beginning exactly so is an OQX expression, the rest of the string. */
|
|
45
|
+
export const EXPRESSION_LEADER = "-> ";
|
|
46
|
+
/** A path-looking string: contains `/` or ends `.md` (the bare document target of §11.8 "Sugar"). */
|
|
47
|
+
export function looksLikePath(s) {
|
|
48
|
+
return s.includes("/") || s.endsWith(".md");
|
|
49
|
+
}
|
|
50
|
+
/** A node id: `n_` + 12 hex (content-hash derived, `core/store/nodes.ts`; the `resolveRef` rule). */
|
|
51
|
+
const NODE_ID_RE = /^n_[0-9a-f]{12}$/;
|
|
52
|
+
/** A bare id of the three target kinds (§11.8 "Sugar"). */
|
|
53
|
+
export function isTargetId(s) {
|
|
54
|
+
return isValidId(s, "b") || isValidId(s, "d") || NODE_ID_RE.test(s);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A bare string where a target object is expected (3.2, §11.8 "Sugar"): a
|
|
58
|
+
* path-looking string → `{ kind: "document", path }`; a `b_`/`d_`/`n_` id →
|
|
59
|
+
* that kind; `$new.n` → `{ kind: "symbol", id }` (its kind is the symbol
|
|
60
|
+
* table's at resolution); a `#` locator → `filter_invalid` with a `fix`;
|
|
61
|
+
* anything else is no target. A `-> ` leader never reaches here from the
|
|
62
|
+
* planner (`select.ts` desugars it first); as a library input it is refused
|
|
63
|
+
* the same way, naming the planner.
|
|
64
|
+
*/
|
|
65
|
+
export function bareTarget(ref, here) {
|
|
66
|
+
if (ref.startsWith(EXPRESSION_LEADER)) {
|
|
67
|
+
const reason = `${here}: a \`-> \` expression target is bound by the planner (\`plan\` / \`apply\` / \`execute\`), not by the shape check`;
|
|
68
|
+
throw new MutationError("filter_invalid", reason, { reason, hint: "selection" });
|
|
69
|
+
}
|
|
70
|
+
if (ref.includes("#")) {
|
|
71
|
+
const reason = `${here}: ${JSON.stringify(ref)} is a locator, not a target — a bare target is a document path, a \`b_\`/\`d_\`/\`n_\` id or \`$new.n\``;
|
|
72
|
+
throw new MutationError("filter_invalid", reason, { reason, hint: "arguments", fix: "resolve the locator to an id first: `blocks_resolve { locator }`, or a `query` whose hit is the block" });
|
|
73
|
+
}
|
|
74
|
+
if (isNewSymbol(ref))
|
|
75
|
+
return { kind: "symbol", id: ref };
|
|
76
|
+
if (isValidId(ref, "b"))
|
|
77
|
+
return { kind: "block", id: ref };
|
|
78
|
+
if (isValidId(ref, "d"))
|
|
79
|
+
return { kind: "document", id: ref };
|
|
80
|
+
if (NODE_ID_RE.test(ref))
|
|
81
|
+
return { kind: "node", id: ref };
|
|
82
|
+
if (looksLikePath(ref))
|
|
83
|
+
return { kind: "document", path: ref };
|
|
84
|
+
throw effectInvalid(`${here}: a target is { kind: …, … } or a bare document path, a \`b_\`/\`d_\`/\`n_\` id or \`$new.n\` (got ${JSON.stringify(ref)})`);
|
|
85
|
+
}
|
|
86
|
+
export function validateTarget(v, here, kinds) {
|
|
87
|
+
if (typeof v === "string")
|
|
88
|
+
v = bareTarget(v, here);
|
|
43
89
|
if (!isRecord(v) || typeof v.kind !== "string")
|
|
44
90
|
throw effectInvalid(`${here}: a target is { kind: ${kinds.map((k) => `"${k}"`).join(" | ")}, … }`);
|
|
91
|
+
// A bare `$new.n` defers its kind check to resolution (`resolveEffect`); the `kinds` are the slot's.
|
|
92
|
+
if (v.kind === "symbol" && typeof v.id === "string" && isNewSymbol(v.id) && Object.keys(v).length === 2)
|
|
93
|
+
return { kind: "symbol", id: v.id };
|
|
45
94
|
if (!kinds.includes(v.kind))
|
|
46
95
|
throw effectInvalid(`${here}: \`kind\` must be one of ${kinds.join(", ")} (got "${v.kind}")`);
|
|
47
96
|
switch (v.kind) {
|
|
@@ -147,7 +196,8 @@ export function validateEffect(v, i) {
|
|
|
147
196
|
const field = requireString(v, "field", here);
|
|
148
197
|
if (!("value" in v))
|
|
149
198
|
throw effectInvalid(`${here}: \`value\` is required`);
|
|
150
|
-
|
|
199
|
+
if (target.kind !== "symbol")
|
|
200
|
+
checkField(target.kind, field, here, "set");
|
|
151
201
|
const expect = validateExpect(v.expect, here);
|
|
152
202
|
return { op: "set", target, field, value: v.value, ...(expect !== undefined ? { expect } : {}) };
|
|
153
203
|
}
|
|
@@ -155,7 +205,8 @@ export function validateEffect(v, i) {
|
|
|
155
205
|
onlyKeys(v, ["op", "target", "field", "expect"], here);
|
|
156
206
|
const target = validateTarget(v.target, `${here}.target`, ["document"]);
|
|
157
207
|
const field = requireString(v, "field", here);
|
|
158
|
-
|
|
208
|
+
if (target.kind !== "symbol")
|
|
209
|
+
checkField(target.kind, field, here, "unset");
|
|
159
210
|
const expect = validateExpect(v.expect, here);
|
|
160
211
|
return { op: "unset", target, field, ...(expect !== undefined ? { expect } : {}) };
|
|
161
212
|
}
|
|
@@ -337,10 +388,14 @@ export function resolveDocumentTarget(ctx, t) {
|
|
|
337
388
|
throw effectInvalid(`id ${JSON.stringify(t.id)} and path ${JSON.stringify(t.path)} name different documents`);
|
|
338
389
|
return { kind: "document", id: t.id, path: created.path };
|
|
339
390
|
}
|
|
340
|
-
|
|
341
|
-
|
|
391
|
+
return findDocumentTarget(ctx.store, ctx.repoId, t);
|
|
392
|
+
}
|
|
393
|
+
/** An existing document's `{ id, path }` for a literal document target (`doc_missing`; `filter_invalid` when `id` and `path` disagree). */
|
|
394
|
+
export function findDocumentTarget(store, repoId, t) {
|
|
395
|
+
const byId = t.id !== undefined ? findDoc(store, { docId: t.id }) : null;
|
|
396
|
+
if (t.id !== undefined && (!byId || byId.repoId !== repoId))
|
|
342
397
|
throw docMissing(t.id);
|
|
343
|
-
const byPath = t.path !== undefined ? findDoc(
|
|
398
|
+
const byPath = t.path !== undefined ? findDoc(store, { repoId, path: t.path }) : null;
|
|
344
399
|
if (t.path !== undefined && !byPath)
|
|
345
400
|
throw docMissing(t.path);
|
|
346
401
|
if (byId && byPath && byId.docId !== byPath.docId)
|
|
@@ -348,6 +403,15 @@ export function resolveDocumentTarget(ctx, t) {
|
|
|
348
403
|
const info = (byId ?? byPath);
|
|
349
404
|
return { kind: "document", id: info.docId, path: info.path };
|
|
350
405
|
}
|
|
406
|
+
/** A bare `$new.n` target (3.2): the kind the symbol table recorded, checked against the slot's admitted kinds. */
|
|
407
|
+
function resolveSymbolTarget(ctx, symbol, kinds, here) {
|
|
408
|
+
const kind = ctx.symbols.kindOf(symbol);
|
|
409
|
+
if (kind === undefined)
|
|
410
|
+
throw new MutationError("target_missing", `${symbol} is not created by an earlier effect`, { symbol });
|
|
411
|
+
if (!kinds.includes(kind))
|
|
412
|
+
throw effectInvalid(`${here}: \`kind\` must be one of ${kinds.join(", ")} (got "${kind}")`);
|
|
413
|
+
return { kind, id: symbol };
|
|
414
|
+
}
|
|
351
415
|
function symbolMissing(ctx, symbol, wanted) {
|
|
352
416
|
const kind = ctx.symbols.kindOf(symbol);
|
|
353
417
|
if (kind === undefined)
|
|
@@ -393,6 +457,80 @@ function blockInfo(ctx, id) {
|
|
|
393
457
|
throw new MutationError("block_missing", `block ${id} not found`, { block: id });
|
|
394
458
|
return { docId: row.doc_id, hash: row.h, block: null };
|
|
395
459
|
}
|
|
460
|
+
// ---- the summary (§11.9, 3.3) ---------------------------------------------------------
|
|
461
|
+
//
|
|
462
|
+
// Channel 3 of the teaching surfaces: the plan is a worked example of itself.
|
|
463
|
+
// One line in words per resolved effect — `preview.summary`, which the Receipt
|
|
464
|
+
// repeats as its `summary` and the CLI prints under the commit confirmation.
|
|
465
|
+
// Deterministic and byte-identical across engines (the wording table of §11.9):
|
|
466
|
+
// a document by its rooted path, a heading by its text in quotes, any other
|
|
467
|
+
// block by its type and id, counts with their nouns pluralized by an `s`.
|
|
468
|
+
/** `1 block` / `2 blocks` (an `s` on every noun of the table). */
|
|
469
|
+
export function plural(n, noun) {
|
|
470
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
471
|
+
}
|
|
472
|
+
/** The rooted path of a document the plan addresses — a created document's from its `create`. */
|
|
473
|
+
function docPathOf(ctx, docId) {
|
|
474
|
+
if (isNewSymbol(docId)) {
|
|
475
|
+
const created = ctx.creates.find((c) => c.symbol === docId);
|
|
476
|
+
if (created)
|
|
477
|
+
return referencePath(created.path);
|
|
478
|
+
}
|
|
479
|
+
const info = findDoc(ctx.store, { docId });
|
|
480
|
+
return referencePath(info?.path ?? docId);
|
|
481
|
+
}
|
|
482
|
+
/** A block's type and visible text: the live row when it has one, else the planner's tree (a `$new.k`). */
|
|
483
|
+
function blockDescriptor(ctx, id) {
|
|
484
|
+
if (!isNewSymbol(id)) {
|
|
485
|
+
const row = ctx.store.db.prepare("SELECT type, text FROM blocks WHERE block_id = ? AND deleted_commit IS NULL").get(id);
|
|
486
|
+
if (row)
|
|
487
|
+
return { type: row.type, text: row.text };
|
|
488
|
+
}
|
|
489
|
+
const inTree = locateLoaded(ctx, id);
|
|
490
|
+
if (!inTree)
|
|
491
|
+
return null;
|
|
492
|
+
return { type: inTree.block.type, text: normalizeVisibleText(inTree.block.raw, inTree.block.type) };
|
|
493
|
+
}
|
|
494
|
+
/** The name of a block in a summary line: `heading "Text"` for a heading, else `<type> <id>` (`paragraph b_12`, `task b_9`). */
|
|
495
|
+
function blockName(ctx, id) {
|
|
496
|
+
const d = blockDescriptor(ctx, id);
|
|
497
|
+
if (d === null)
|
|
498
|
+
return `block ${id}`;
|
|
499
|
+
return d.type === "heading" ? `heading ${ctx.canonical(d.text)}` : `${d.type} ${id}`;
|
|
500
|
+
}
|
|
501
|
+
/** A section by its heading's text: `section "Risks"`. */
|
|
502
|
+
function sectionName(ctx, headingId) {
|
|
503
|
+
const d = blockDescriptor(ctx, headingId);
|
|
504
|
+
return `section ${ctx.canonical(d?.text ?? headingId)}`;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The placement half of an `insert` / `move blocks` line: `<verb> the end|start
|
|
508
|
+
* of <container> [in <path>]` — the container a document (its path; no `in`), a
|
|
509
|
+
* section (`section "Risks"`) or a block (its name) — or `before|after <block>
|
|
510
|
+
* in <path>`. `verb` is `at` for an insert, `to` for a move.
|
|
511
|
+
*/
|
|
512
|
+
function placeWords(ctx, into, at, verb) {
|
|
513
|
+
const path = docPathOf(ctx, targetDoc(ctx, into));
|
|
514
|
+
if (at !== "start" && at !== "end") {
|
|
515
|
+
const anchor = "before" in at ? at.before : at.after;
|
|
516
|
+
return `${"before" in at ? "before" : "after"} ${blockName(ctx, anchor)} in ${path}`;
|
|
517
|
+
}
|
|
518
|
+
const edge = `${verb} the ${at}`;
|
|
519
|
+
switch (into.kind) {
|
|
520
|
+
case "document":
|
|
521
|
+
return `${edge} of ${path}`;
|
|
522
|
+
case "section":
|
|
523
|
+
return `${edge} of ${sectionName(ctx, into.heading_id)} in ${path}`;
|
|
524
|
+
case "block":
|
|
525
|
+
return `${edge} of ${blockName(ctx, into.id)} in ${path}`;
|
|
526
|
+
case "node":
|
|
527
|
+
return `${edge} of node ${into.id} in ${path}`;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/** The document of a block list's first block (a kernel `remove`/`merge` names blocks of one document). */
|
|
531
|
+
function docOfBlocks(ctx, ids) {
|
|
532
|
+
return docPathOf(ctx, blockInfo(ctx, ids[0]).docId);
|
|
533
|
+
}
|
|
396
534
|
/** Every descendant id of a block: the planner's tree when loaded, else the store's live rows. */
|
|
397
535
|
function descendantsOf(ctx, id) {
|
|
398
536
|
const inTree = locateLoaded(ctx, id);
|
|
@@ -535,30 +673,46 @@ function describe(e) {
|
|
|
535
673
|
function targetName(t) {
|
|
536
674
|
return t.kind === "section" ? `section ${t.heading_id}` : `${t.kind} ${t.id}`;
|
|
537
675
|
}
|
|
538
|
-
/**
|
|
539
|
-
|
|
540
|
-
|
|
676
|
+
/** The target kinds each slot admits (§11.1); `validateTarget` checks a literal, `resolveEffect` a bare `$new.n`. */
|
|
677
|
+
const PLACEMENT_KINDS = ["document", "section", "block"];
|
|
678
|
+
const DOCUMENT_KIND = ["document"];
|
|
679
|
+
const BLOCK_KIND = ["block"];
|
|
680
|
+
const SET_KINDS = ["document", "block", "node"];
|
|
681
|
+
/** Resolve one validated effect's targets (§11.4): documents to `{ id, path }`, defaults made explicit; `i` names the effect in messages. */
|
|
682
|
+
export function resolveEffect(ctx, e, i = 0) {
|
|
683
|
+
const here = `effects[${i}]`;
|
|
684
|
+
const target = (t, kinds, member) => {
|
|
685
|
+
const literal = t.kind === "symbol" ? resolveSymbolTarget(ctx, t.id, kinds, `${here}.${member}`) : t;
|
|
686
|
+
return literal.kind === "document" ? resolveDocumentTarget(ctx, literal) : literal;
|
|
687
|
+
};
|
|
688
|
+
const targets = (list) => list.map((t, k) => target(t, BLOCK_KIND, `targets[${k}]`));
|
|
541
689
|
switch (e.op) {
|
|
542
690
|
case "create":
|
|
543
691
|
return { op: "create", path: canonicalDocPath(e.path), markdown: e.markdown, ...(e.frontmatter !== undefined ? { frontmatter: e.frontmatter } : {}) };
|
|
544
692
|
case "insert":
|
|
545
|
-
return { op: "insert", into: target(e.into), at: e.at ?? "end", markdown: e.markdown, ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
693
|
+
return { op: "insert", into: target(e.into, PLACEMENT_KINDS, "into"), at: e.at ?? "end", markdown: e.markdown, ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
546
694
|
case "set":
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
695
|
+
case "unset": {
|
|
696
|
+
const t = target(e.target, e.op === "set" ? SET_KINDS : DOCUMENT_KIND, "target");
|
|
697
|
+
// A bare `$new.n` deferred its `field` check (§11.2) to here, where its kind is known.
|
|
698
|
+
if (e.target.kind === "symbol")
|
|
699
|
+
checkField(t.kind, e.field, here, e.op);
|
|
700
|
+
if (e.op === "set")
|
|
701
|
+
return { op: "set", target: t, field: e.field, value: e.value, ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
702
|
+
return { op: "unset", target: t, field: e.field, ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
703
|
+
}
|
|
550
704
|
case "move":
|
|
551
705
|
if ("targets" in e)
|
|
552
|
-
return { op: "move", targets: e.targets
|
|
553
|
-
return { op: "move", target: target(e.target), to: canonicalDocPath(e.to), retarget_inbound: e.retarget_inbound !== false };
|
|
706
|
+
return { op: "move", targets: targets(e.targets), into: target(e.into, PLACEMENT_KINDS, "into"), at: e.at ?? "end", ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
707
|
+
return { op: "move", target: target(e.target, DOCUMENT_KIND, "target"), to: canonicalDocPath(e.to), retarget_inbound: e.retarget_inbound !== false };
|
|
554
708
|
case "remove":
|
|
555
709
|
if ("targets" in e)
|
|
556
|
-
return { op: "remove", targets: e.targets
|
|
557
|
-
return { op: "remove", target: target(e.target) };
|
|
710
|
+
return { op: "remove", targets: targets(e.targets), ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
711
|
+
return { op: "remove", target: target(e.target, DOCUMENT_KIND, "target") };
|
|
558
712
|
case "split":
|
|
559
|
-
return { op: "split", target: target(e.target), at: [...e.at], ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
713
|
+
return { op: "split", target: target(e.target, BLOCK_KIND, "target"), at: [...e.at], ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
560
714
|
case "merge":
|
|
561
|
-
return { op: "merge", targets: e.targets
|
|
715
|
+
return { op: "merge", targets: targets(e.targets), separator: e.separator ?? " ", ...(e.expect !== undefined ? { expect: e.expect } : {}) };
|
|
562
716
|
case "retarget":
|
|
563
717
|
return { op: "retarget", repairs: e.repairs.map((r) => ({ from: r.from, to: r.to })), ...(e.path_glob !== undefined ? { path_glob: e.path_glob } : {}) };
|
|
564
718
|
}
|
|
@@ -640,7 +794,7 @@ export function shiftPlaceholders(op, offset) {
|
|
|
640
794
|
* resolved effect, or null when it was dropped.
|
|
641
795
|
*/
|
|
642
796
|
export function lowerEffect(ctx, i, effect) {
|
|
643
|
-
const e = resolveEffect(ctx, effect);
|
|
797
|
+
const e = resolveEffect(ctx, effect, i);
|
|
644
798
|
const info = focusOf(ctx, e);
|
|
645
799
|
if (!ctx.tracker.check(i, info, describe(e)))
|
|
646
800
|
return null;
|
|
@@ -755,9 +909,11 @@ function applyLowering(ctx, i, e) {
|
|
|
755
909
|
// document, so every later effect addresses it and its blocks like any other.
|
|
756
910
|
const symbol = ctx.symbols.alloc("document");
|
|
757
911
|
const source = composeFile(e.markdown, e.frontmatter);
|
|
758
|
-
|
|
912
|
+
const tree = parseNewDoc(symbol, e.path, source);
|
|
913
|
+
ctx.planner.adoptNew(symbol, tree);
|
|
759
914
|
ctx.creates.push({ symbol, path: e.path, source, before_op: ctx.ops.length });
|
|
760
915
|
ctx.absentPaths.push(e.path);
|
|
916
|
+
ctx.summary.push(`create ${referencePath(e.path)} (${plural(tree.children.length, "block")})`);
|
|
761
917
|
return;
|
|
762
918
|
}
|
|
763
919
|
case "insert": {
|
|
@@ -766,7 +922,9 @@ function applyLowering(ctx, i, e) {
|
|
|
766
922
|
recordParentPin(ctx, doc, e.into, e.expect);
|
|
767
923
|
const to = placement(ctx, e.into, e.at);
|
|
768
924
|
const expect = kernelExpect(e.expect);
|
|
925
|
+
const where = placeWords(ctx, e.into, e.at, "at");
|
|
769
926
|
pushOp(ctx, { op: "insert", ...(e.into.kind === "document" ? { doc } : {}), to, markdown: e.markdown, ...(expect !== undefined ? { expect } : {}) });
|
|
927
|
+
ctx.summary.push(`insert ${plural(ctx.planner.results[ctx.planner.results.length - 1].ids.length, "block")} ${where}`);
|
|
770
928
|
return;
|
|
771
929
|
}
|
|
772
930
|
case "set":
|
|
@@ -780,6 +938,7 @@ function applyLowering(ctx, i, e) {
|
|
|
780
938
|
if (isNewSymbol(docId))
|
|
781
939
|
throw new MutationError("target_missing", `${docId} is a document this plan creates; its content is the create's markdown`, { symbol: docId });
|
|
782
940
|
lowerContent(ctx, docId, e.value);
|
|
941
|
+
ctx.summary.push(`set content of ${docPathOf(ctx, docId)}`);
|
|
783
942
|
return;
|
|
784
943
|
}
|
|
785
944
|
// `frontmatter.<key>` — on a created document too (3.0): its tree is loaded, the patch is a head like any other.
|
|
@@ -791,6 +950,7 @@ function applyLowering(ctx, i, e) {
|
|
|
791
950
|
else
|
|
792
951
|
delete merged[key];
|
|
793
952
|
patchFrontmatter(ctx, docId, merged, body);
|
|
953
|
+
ctx.summary.push(e.op === "set" ? `set ${e.field} = ${ctx.canonical(e.value)} in ${docPathOf(ctx, docId)}` : `unset ${e.field} in ${docPathOf(ctx, docId)}`);
|
|
794
954
|
return;
|
|
795
955
|
}
|
|
796
956
|
if (e.op !== "set")
|
|
@@ -800,15 +960,18 @@ function applyLowering(ctx, i, e) {
|
|
|
800
960
|
const { docId } = blockInfo(ctx, id);
|
|
801
961
|
checkRevision(ctx, docId, e.expect);
|
|
802
962
|
const hash = pin(ctx, id, e.expect?.content_hash);
|
|
963
|
+
const name = blockName(ctx, id);
|
|
803
964
|
if (e.field === "markdown") {
|
|
804
965
|
if (typeof e.value !== "string")
|
|
805
966
|
throw effectInvalid("`markdown` takes a string value");
|
|
806
967
|
pushOp(ctx, { op: "update", block: kernelBlockId(ctx, id), markdown: e.value, expect: kernelExpect(e.expect, hash) });
|
|
968
|
+
ctx.summary.push(`set markdown of ${name} in ${docPathOf(ctx, docId)}`);
|
|
807
969
|
}
|
|
808
970
|
else {
|
|
809
971
|
if (typeof e.value !== "boolean")
|
|
810
972
|
throw effectInvalid("`attrs.checked` takes a boolean value");
|
|
811
973
|
pushOp(ctx, { op: "update", block: kernelBlockId(ctx, id), attrs: { checked: e.value }, expect: kernelExpect(e.expect, hash) });
|
|
974
|
+
ctx.summary.push(`${e.value ? "check" : "uncheck"} ${name} in ${docPathOf(ctx, docId)}`);
|
|
812
975
|
}
|
|
813
976
|
return;
|
|
814
977
|
}
|
|
@@ -822,6 +985,7 @@ function applyLowering(ctx, i, e) {
|
|
|
822
985
|
const hash = pin(ctx, blockId, e.expect?.content_hash ?? u.expect?.content_hash);
|
|
823
986
|
pushOp(ctx, { ...u, expect: { ...(u.expect ?? {}), content_hash: hash } });
|
|
824
987
|
}
|
|
988
|
+
ctx.summary.push(`set ${e.field} = ${ctx.canonical(e.value)} on node ${node.id} in ${docPathOf(ctx, docId)}`);
|
|
825
989
|
return;
|
|
826
990
|
}
|
|
827
991
|
case "move": {
|
|
@@ -832,9 +996,12 @@ function applyLowering(ctx, i, e) {
|
|
|
832
996
|
recordParentPin(ctx, dst, e.into, e.expect);
|
|
833
997
|
const to = placement(ctx, e.into, e.at);
|
|
834
998
|
const expect = kernelExpect(e.expect);
|
|
999
|
+
const what = ids.length === 1 ? blockName(ctx, ids[0]) : plural(ids.length, "block");
|
|
1000
|
+
const where = placeWords(ctx, e.into, e.at, "to");
|
|
835
1001
|
// `doc` names the destination when `into` is a document (§2.3, 3.0): a
|
|
836
1002
|
// top-level start/end would otherwise mean the SOURCE document's.
|
|
837
1003
|
pushOp(ctx, { op: "move", blocks: ids.map((id) => kernelBlockId(ctx, id)), to, ...(e.into.kind === "document" ? { doc: dst } : {}), ...(expect !== undefined ? { expect } : {}) });
|
|
1004
|
+
ctx.summary.push(`move ${what} ${where}`);
|
|
838
1005
|
return;
|
|
839
1006
|
}
|
|
840
1007
|
const t = docTarget(e.target);
|
|
@@ -854,6 +1021,7 @@ function applyLowering(ctx, i, e) {
|
|
|
854
1021
|
ctx.docBases.set(t.id, base);
|
|
855
1022
|
const inbound = inboundLinksTo(ctx.store, ctx.repoId, t.id, t.path);
|
|
856
1023
|
let retargeted = null;
|
|
1024
|
+
let links = "";
|
|
857
1025
|
if (e.retarget_inbound && inbound.length > 0) {
|
|
858
1026
|
const plan = planInboundRetarget(ctx.store, t.id, t.path, e.to, inbound);
|
|
859
1027
|
for (const op of plan.ops) {
|
|
@@ -862,8 +1030,10 @@ function applyLowering(ctx, i, e) {
|
|
|
862
1030
|
pushOp(ctx, u);
|
|
863
1031
|
}
|
|
864
1032
|
retargeted = { blocks: plan.blocks, docs: plan.docs };
|
|
1033
|
+
links = ` (retarget ${plural(inbound.length - plan.dangling.length, "link")} in ${plural(plan.docs.length, "document")})`;
|
|
865
1034
|
}
|
|
866
1035
|
ctx.moves.push({ id: t.id, from: t.path, to: e.to, currentRev: base.currentRev, inbound, retargeted });
|
|
1036
|
+
ctx.summary.push(`move ${referencePath(t.path)} → ${referencePath(e.to)}${links}`);
|
|
867
1037
|
return;
|
|
868
1038
|
}
|
|
869
1039
|
case "remove": {
|
|
@@ -875,7 +1045,9 @@ function applyLowering(ctx, i, e) {
|
|
|
875
1045
|
const hash = pin(ctx, id, given?.content_hash);
|
|
876
1046
|
expect[kernelBlockId(ctx, id)] = { ...(given ?? {}), content_hash: hash };
|
|
877
1047
|
}
|
|
1048
|
+
const line = `remove ${ids.length === 1 ? blockName(ctx, ids[0]) : plural(ids.length, "block")} from ${docOfBlocks(ctx, ids)}`;
|
|
878
1049
|
pushOp(ctx, { op: "remove", blocks: ids.map((id) => kernelBlockId(ctx, id)), expect });
|
|
1050
|
+
ctx.summary.push(line);
|
|
879
1051
|
return;
|
|
880
1052
|
}
|
|
881
1053
|
const t = docTarget(e.target);
|
|
@@ -887,6 +1059,7 @@ function applyLowering(ctx, i, e) {
|
|
|
887
1059
|
if (!ctx.docBases.has(t.id))
|
|
888
1060
|
ctx.docBases.set(t.id, base);
|
|
889
1061
|
ctx.tombstones.push({ id: t.id, path: t.path, currentRev: base.currentRev });
|
|
1062
|
+
ctx.summary.push(`remove ${referencePath(t.path)}`);
|
|
890
1063
|
return;
|
|
891
1064
|
}
|
|
892
1065
|
case "split": {
|
|
@@ -894,7 +1067,9 @@ function applyLowering(ctx, i, e) {
|
|
|
894
1067
|
const { docId } = blockInfo(ctx, id);
|
|
895
1068
|
checkRevision(ctx, docId, e.expect);
|
|
896
1069
|
const hash = pin(ctx, id, e.expect?.content_hash);
|
|
1070
|
+
const name = blockName(ctx, id);
|
|
897
1071
|
pushOp(ctx, { op: "split", block: kernelBlockId(ctx, id), at: [...e.at], expect: kernelExpect(e.expect, hash) });
|
|
1072
|
+
ctx.summary.push(`split ${name} at offset${e.at.length === 1 ? "" : "s"} ${e.at.join(", ")} in ${docPathOf(ctx, docId)}`);
|
|
898
1073
|
return;
|
|
899
1074
|
}
|
|
900
1075
|
case "merge": {
|
|
@@ -905,7 +1080,9 @@ function applyLowering(ctx, i, e) {
|
|
|
905
1080
|
const hash = pin(ctx, id, given?.content_hash);
|
|
906
1081
|
expect[kernelBlockId(ctx, id)] = { ...(given ?? {}), content_hash: hash };
|
|
907
1082
|
}
|
|
1083
|
+
const line = `merge ${plural(ids.length, "block")} into ${blockName(ctx, ids[0])} in ${docOfBlocks(ctx, ids)}`;
|
|
908
1084
|
pushOp(ctx, { op: "merge", blocks: ids.map((id) => kernelBlockId(ctx, id)), separator: e.separator, expect });
|
|
1085
|
+
ctx.summary.push(line);
|
|
909
1086
|
return;
|
|
910
1087
|
}
|
|
911
1088
|
case "retarget": {
|
|
@@ -916,6 +1093,9 @@ function applyLowering(ctx, i, e) {
|
|
|
916
1093
|
pin(ctx, u.block, u.expect?.content_hash);
|
|
917
1094
|
pushOp(ctx, u);
|
|
918
1095
|
}
|
|
1096
|
+
const docs = new Set(plan.hits.map((h) => h.path)).size;
|
|
1097
|
+
const pair = e.repairs.length === 1 ? `${e.repairs[0].from} → ${e.repairs[0].to}` : `over ${plural(e.repairs.length, "pair")}`;
|
|
1098
|
+
ctx.summary.push(`retarget ${plural(plan.hits.length, "link")} ${pair} in ${plural(docs, "document")}`);
|
|
919
1099
|
return;
|
|
920
1100
|
}
|
|
921
1101
|
}
|
|
@@ -3,9 +3,10 @@ import { type ApplyRevision, type Op, type OpResult } from "./apply.js";
|
|
|
3
3
|
import { type DocStore } from "./doc-store.js";
|
|
4
4
|
import { type CreateEntry, type ResolvedEffect, type RetargetPreview } from "./effects.js";
|
|
5
5
|
import type { IdentityPreview } from "./opset.js";
|
|
6
|
+
import { type Selection } from "./select.js";
|
|
6
7
|
export declare const PLAN_VERSION: 1;
|
|
7
8
|
/** The lowering + conflict rules version (§11.4; the `matcher_v` precedent). */
|
|
8
|
-
export declare const PLANNER_V: "
|
|
9
|
+
export declare const PLANNER_V: "p3";
|
|
9
10
|
/** A kernel op as the plan spells it on the wire: the fixture spelling (`child_ids`). */
|
|
10
11
|
export type WireOp = Exclude<Op, {
|
|
11
12
|
op: "update";
|
|
@@ -77,6 +78,8 @@ export interface PlanPreview {
|
|
|
77
78
|
identity?: Record<string, IdentityPreview>;
|
|
78
79
|
/** (3.0) per `retarget` effect: its hits and pair counts; present when a plan has one */
|
|
79
80
|
retarget?: RetargetPreview[];
|
|
81
|
+
/** (3.3, §11.9) one line in words per resolved effect, in order — the plan as a worked example; `[]` for a zero-hit selection */
|
|
82
|
+
summary: string[];
|
|
80
83
|
}
|
|
81
84
|
/** The executable core the digest covers. */
|
|
82
85
|
export interface PlanCore {
|
|
@@ -89,10 +92,15 @@ export interface PlanCore {
|
|
|
89
92
|
symbol: string;
|
|
90
93
|
kind: "block" | "document";
|
|
91
94
|
}[];
|
|
95
|
+
/** (3.1, §11.8) the selector, its templates and the hits it bound — present only for a selector plan */
|
|
96
|
+
selection?: Selection;
|
|
92
97
|
}
|
|
93
98
|
export interface Plan extends PlanCore {
|
|
94
99
|
plan_id: string;
|
|
95
100
|
diagnostics: string[];
|
|
101
|
+
/** (3.2, §11.8 "Sugar") a plain request's effects AS WRITTEN when one carried a `-> ` / `{ oqx }` expression;
|
|
102
|
+
* not digested (the core already holds the concrete effects), recorded in the intent record as `templates` */
|
|
103
|
+
templates?: unknown[];
|
|
96
104
|
/** excluded from the digest; omittable on echo */
|
|
97
105
|
preview?: PlanPreview;
|
|
98
106
|
}
|
|
@@ -104,19 +112,11 @@ export interface Receipt {
|
|
|
104
112
|
/** `$new.n` → the minted id */
|
|
105
113
|
new_ids: Record<string, string>;
|
|
106
114
|
committed: true;
|
|
115
|
+
/** (3.3, §11.9) the plan's `preview.summary` — what was committed, in words (`[]` when the plan was handed back without its preview) */
|
|
116
|
+
summary: string[];
|
|
107
117
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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. */
|
|
118
|
+
export { canonicalJson } from "./canonical.js";
|
|
119
|
+
/** The executable core of a plan — the six digested keys (seven with `selection`), nothing else. */
|
|
120
120
|
export declare function planCore(p: PlanCore): PlanCore;
|
|
121
121
|
/** `plan_id = hex(sha256(canonical(core)))` (§11.5). */
|
|
122
122
|
export declare function planDigest(core: PlanCore): string;
|
|
@@ -126,8 +126,12 @@ export interface PlanEffectsRequest {
|
|
|
126
126
|
rootPath?: string;
|
|
127
127
|
docStore?: DocStore;
|
|
128
128
|
omgbaseDir?: string;
|
|
129
|
-
/** The effects as given (validated here: a shape error is `filter_invalid`). */
|
|
129
|
+
/** The effects as given (validated here: a shape error is `filter_invalid`); with `select`, the templates (§11.8). */
|
|
130
130
|
effects: unknown;
|
|
131
|
+
/** (3.1) an OQX selector: its hits × `effects` (templates) are the effects planned */
|
|
132
|
+
select?: unknown;
|
|
133
|
+
/** (3.1) `{ max }`: the bound on the selection (`selection_too_large` beyond it); the implicit ceiling without it */
|
|
134
|
+
expect?: unknown;
|
|
131
135
|
}
|
|
132
136
|
/** The kernel op as the plan carries it (`childIds` → `child_ids`). */
|
|
133
137
|
export declare function wireOp(o: Op): WireOp;
|
|
@@ -165,10 +169,12 @@ export declare function admitPlan(input: unknown): Plan;
|
|
|
165
169
|
export declare function executePlan(store: Store, req: ExecuteRequest & {
|
|
166
170
|
plan: unknown;
|
|
167
171
|
}): Receipt;
|
|
168
|
-
/** `execute { plan_id, effects }` (§11.7): re-plan; the digest must equal `plan_id`, else `plan_mismatch`. */
|
|
172
|
+
/** `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`. */
|
|
169
173
|
export declare function executeByDigest(store: Store, req: ExecuteRequest & {
|
|
170
174
|
planId: string;
|
|
171
175
|
effects: unknown;
|
|
176
|
+
select?: unknown;
|
|
177
|
+
expect?: unknown;
|
|
172
178
|
}): Receipt;
|
|
173
179
|
/**
|
|
174
180
|
* `apply { effects }` with both halves kept (the sugar's seam — `sugar.ts`,
|
|
@@ -179,14 +185,18 @@ export declare function executeByDigest(store: Store, req: ExecuteRequest & {
|
|
|
179
185
|
*/
|
|
180
186
|
export declare function applyEffectsDetailed(store: Store, req: ExecuteRequest & {
|
|
181
187
|
effects: unknown;
|
|
188
|
+
select?: unknown;
|
|
189
|
+
expect?: unknown;
|
|
182
190
|
dryRun?: boolean;
|
|
183
191
|
}): {
|
|
184
192
|
plan: Plan;
|
|
185
193
|
receipt?: Receipt;
|
|
186
194
|
};
|
|
187
|
-
/** `apply { effects }` (§11.7): plan + execute under one lock and one transaction; `dryRun` returns the Plan. */
|
|
195
|
+
/** `apply { effects, select?, expect? }` (§11.7): plan + execute under one lock and one transaction; `dryRun` returns the Plan. */
|
|
188
196
|
export declare function applyEffects(store: Store, req: ExecuteRequest & {
|
|
189
197
|
effects: unknown;
|
|
198
|
+
select?: unknown;
|
|
199
|
+
expect?: unknown;
|
|
190
200
|
dryRun?: boolean;
|
|
191
201
|
}): Plan | Receipt;
|
|
192
202
|
/** 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`. */
|