@omgbase/core 0.28.0 → 0.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +7 -5
  2. package/dist/src/core/read/document.d.ts +11 -10
  3. package/dist/src/core/read/document.js +12 -11
  4. package/dist/src/core/read/nodes.d.ts +10 -10
  5. package/dist/src/core/read/nodes.js +11 -11
  6. package/dist/src/core/read/reader.d.ts +37 -24
  7. package/dist/src/core/read/reader.js +44 -31
  8. package/dist/src/graph/history.d.ts +25 -22
  9. package/dist/src/graph/history.js +17 -17
  10. package/dist/src/graph/inbound-links.d.ts +7 -7
  11. package/dist/src/graph/inbound-links.js +7 -7
  12. package/dist/src/graph/link-health.d.ts +9 -9
  13. package/dist/src/graph/link-health.js +9 -9
  14. package/dist/src/graph/links.d.ts +3 -3
  15. package/dist/src/graph/links.js +4 -4
  16. package/dist/src/mcp/arguments.d.ts +10 -3
  17. package/dist/src/mcp/arguments.js +32 -9
  18. package/dist/src/mcp/errors.d.ts +1 -1
  19. package/dist/src/mcp/graph.d.ts +22 -22
  20. package/dist/src/mcp/graph.js +54 -47
  21. package/dist/src/mcp/reference.d.ts +29 -1
  22. package/dist/src/mcp/reference.js +398 -255
  23. package/dist/src/mcp/server.d.ts +9 -0
  24. package/dist/src/mcp/server.js +170 -108
  25. package/dist/src/mutate/apply.d.ts +6 -6
  26. package/dist/src/mutate/apply.js +1 -1
  27. package/dist/src/mutate/canonical.d.ts +1 -1
  28. package/dist/src/mutate/canonical.js +2 -2
  29. package/dist/src/mutate/commit.d.ts +3 -3
  30. package/dist/src/mutate/commit.js +4 -4
  31. package/dist/src/mutate/docs.d.ts +1 -1
  32. package/dist/src/mutate/docs.js +8 -8
  33. package/dist/src/mutate/effects.d.ts +17 -17
  34. package/dist/src/mutate/effects.js +113 -113
  35. package/dist/src/mutate/macros.d.ts +5 -5
  36. package/dist/src/mutate/macros.js +1 -1
  37. package/dist/src/mutate/materialize.js +2 -2
  38. package/dist/src/mutate/ops.js +2 -2
  39. package/dist/src/mutate/opset.d.ts +7 -7
  40. package/dist/src/mutate/plan-update.js +2 -2
  41. package/dist/src/mutate/plan.d.ts +8 -8
  42. package/dist/src/mutate/plan.js +25 -25
  43. package/dist/src/mutate/select.d.ts +4 -4
  44. package/dist/src/mutate/select.js +57 -56
  45. package/dist/src/mutate/sugar.d.ts +10 -10
  46. package/dist/src/mutate/sugar.js +21 -21
  47. package/dist/src/oqx-js/context.d.ts +8 -4
  48. package/dist/src/oqx-js/context.js +206 -189
  49. package/dist/src/oqx-js/names.d.ts +28 -17
  50. package/dist/src/oqx-js/names.js +115 -78
  51. package/dist/src/oqx-js/planner.d.ts +4 -8
  52. package/dist/src/oqx-js/planner.js +6 -10
  53. package/dist/src/oqx-js/root-row.d.ts +6 -5
  54. package/dist/src/oqx-js/root-row.js +15 -16
  55. package/dist/src/oqx-js/run.d.ts +18 -17
  56. package/dist/src/oqx-js/run.js +57 -54
  57. package/dist/src/oqx-js/sql/translate.d.ts +5 -5
  58. package/dist/src/oqx-js/sql/translate.js +162 -161
  59. package/dist/src/oqx-js/store-index.d.ts +1 -1
  60. package/dist/src/oqx-js/store-index.js +28 -30
  61. package/dist/src/search/external.d.ts +10 -0
  62. package/dist/src/search/external.js +20 -2
  63. package/dist/src/search/resolve.d.ts +2 -2
  64. package/dist/src/search/resolve.js +1 -1
  65. package/dist/src/search/rrf.js +2 -2
  66. package/dist/src/search/text.d.ts +5 -5
  67. package/dist/src/search/text.js +2 -2
  68. package/dist/src/spec-versions.d.ts +7 -7
  69. package/dist/src/spec-versions.js +6 -6
  70. package/dist/src/surface-paths.d.ts +18 -17
  71. package/dist/src/surface-paths.js +23 -21
  72. package/dist/src/sync/admin.d.ts +2 -2
  73. package/dist/src/sync/admin.js +1 -1
  74. package/package.json +2 -2
@@ -10,16 +10,16 @@ export type OpsetKind = "doc_update";
10
10
  export type PlanDisposition = "same" | "edited" | "moved" | "edited_moved" | "inserted" | "deleted" | "split_from" | "merged_into" | "copied_from" | "resurrected" | "bulk_rewrite" | "retiled";
11
11
  /** State the opset was planned against; applying against different state
12
12
  * fails. The document is named by its row attributes (spec/surface §1.2,
13
- * 3.0): `id`, `path`, and `hash` — its current hash at plan time;
13
+ * 5.0 "underscore = engine"): `_id`, `_path`, and `_hash` — its current hash at plan time;
14
14
  * `baseRevision` names a role and keeps its name. */
15
15
  export interface OpsetPrecondition {
16
16
  /** doc id the plan targets */
17
- id: string;
18
- path: string;
17
+ _id: string;
18
+ _path: string;
19
19
  /** the document's current revision id at plan time (null for a doc with no committed revision yet) */
20
20
  baseRevision: string | null;
21
- /** sha256 hex of the document's current rendered bytes (docs.file_hash) at plan time — its `hash` */
22
- hash: string;
21
+ /** sha256 hex of the document's current rendered bytes (docs.file_hash) at plan time — its `_hash` */
22
+ _hash: string;
23
23
  }
24
24
  /** One kernel op plus the identity consequence the planner attributes to it. */
25
25
  export interface PlanOp {
@@ -54,8 +54,8 @@ export interface Opset {
54
54
  version: 1;
55
55
  kind: OpsetKind;
56
56
  target: {
57
- id: string;
58
- path: string;
57
+ _id: string;
58
+ _path: string;
59
59
  };
60
60
  precondition: OpsetPrecondition;
61
61
  /** matcher version that produced the identity decisions (reproducibility) */
@@ -86,8 +86,8 @@ export function planUpdate(store, repoId, rootPath, docRef, content, opts = {})
86
86
  return {
87
87
  version: 1,
88
88
  kind: "doc_update",
89
- target: { id: docId, path: path },
90
- precondition: { id: docId, path: path, baseRevision: info.currentRev, hash: baseContentHash },
89
+ target: { _id: docId, _path: path },
90
+ precondition: { _id: docId, _path: path, baseRevision: info.currentRev, _hash: baseContentHash },
91
91
  matcherV: config.matcherV,
92
92
  ops,
93
93
  ...(fmChanged ? { frontmatter: { raw: targetFm } } : {}),
@@ -24,18 +24,18 @@ export type WireOp = Exclude<Op, {
24
24
  };
25
25
  /** The lowered form `execute` replays (§11.4): every key present, `[]` when empty. */
26
26
  export interface PlanOpset {
27
- /** `create` → the composed file, its document symbol and the index of the first kernel op planned after it (3.0; a later `set frontmatter.<k>` on the symbol is a head below) */
27
+ /** `create` → the composed file, its document symbol and the index of the first kernel op planned after it (3.0; a later `set _frontmatter.<k>` on the symbol is a head below) */
28
28
  creates: CreateEntry[];
29
29
  /** `move document` → the row/file rename (its retarget updates are in `ops`) */
30
30
  moves: {
31
- id: string;
31
+ _id: string;
32
32
  from: string;
33
33
  to: string;
34
34
  }[];
35
35
  /** `remove document` */
36
36
  tombstones: {
37
- id: string;
38
- path: string;
37
+ _id: string;
38
+ _path: string;
39
39
  }[];
40
40
  /** per document, the file head — everything before the first block (frontmatter incl. fences and separator) */
41
41
  set_frontmatter: {
@@ -48,14 +48,14 @@ export interface PlanOpset {
48
48
  export interface PlanPreconditions {
49
49
  /** the row triples (`spec/mutate` §4 step 5) of every document the plan touches, loaded ones first in load order */
50
50
  docs: {
51
- id: string;
52
- path: string;
51
+ _id: string;
52
+ _path: string;
53
53
  base_revision: string | null;
54
- hash: string | null;
54
+ _hash: string | null;
55
55
  }[];
56
56
  /** every block content hash the plan pins (caller-given or pinned from the live row), first-pin order */
57
57
  blocks: {
58
- id: string;
58
+ _id: string;
59
59
  content_hash: string;
60
60
  }[];
61
61
  /** the caller's destination-parent CAS pins */
@@ -116,19 +116,19 @@ export function planEffects(store, req) {
116
116
  const changeset = planner.finish();
117
117
  const opset = {
118
118
  creates: ctx.creates,
119
- moves: ctx.moves.map((m) => ({ id: m.id, from: m.from, to: m.to })),
120
- tombstones: ctx.tombstones.map((t) => ({ id: t.id, path: t.path })),
119
+ moves: ctx.moves.map((m) => ({ _id: m.id, from: m.from, to: m.to })),
120
+ tombstones: ctx.tombstones.map((t) => ({ _id: t.id, _path: t.path })),
121
121
  set_frontmatter: [...ctx.heads].map(([doc, raw]) => ({ doc, raw })),
122
122
  ops: ctx.ops.map(wireOp),
123
123
  };
124
- const docs = changeset.preconditions.map((p) => ({ id: p.id, path: p.path, base_revision: p.currentRev, hash: p.fileHash }));
124
+ const docs = changeset.preconditions.map((p) => ({ _id: p.id, _path: p.path, base_revision: p.currentRev, _hash: p.fileHash }));
125
125
  for (const [id, base] of ctx.docBases) {
126
- if (!docs.some((d) => d.id === id))
127
- docs.push({ id, path: base.path, base_revision: base.currentRev, hash: base.fileHash });
126
+ if (!docs.some((d) => d._id === id))
127
+ docs.push({ _id: id, _path: base.path, base_revision: base.currentRev, _hash: base.fileHash });
128
128
  }
129
129
  const preconditions = {
130
130
  docs,
131
- blocks: [...ctx.pins].map(([id, content_hash]) => ({ id, content_hash })),
131
+ blocks: [...ctx.pins].map(([id, content_hash]) => ({ _id: id, content_hash })),
132
132
  parents: ctx.parents,
133
133
  absent_paths: ctx.absentPaths,
134
134
  };
@@ -151,9 +151,9 @@ export function planEffects(store, req) {
151
151
  for (const t of ctx.tombstones)
152
152
  diffs[t.path] = { before: docStore.read(t.path) ?? "", after: "" };
153
153
  for (const r of revisions) {
154
- const moved = ctx.moves.find((m) => m.id === r.id);
154
+ const moved = ctx.moves.find((m) => m.id === r._id);
155
155
  if (moved)
156
- r.path = moved.to;
156
+ r._path = moved.to;
157
157
  }
158
158
  const preview = { results: changeset.results, revisions, diffs, summary: ctx.summary };
159
159
  if (ctx.identity.size > 0)
@@ -171,7 +171,7 @@ function requestOf(req) {
171
171
  /**
172
172
  * `conflicting_effects`: keep the later effect — the request minus the earlier
173
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
174
+ * later hit (`from <root> where _id == …`, `expect.max: 1`) and a same-hit
175
175
  * collision drops the earlier template. `stale_plan` at plan time (an
176
176
  * `expect.revision` that no longer holds): the same request with the current
177
177
  * revision in that guard. `target_missing { symbol }` for a `$new.n` no earlier
@@ -191,8 +191,8 @@ function planningFix(err, i, req, origins, symbols) {
191
191
  const ob = origins[b];
192
192
  if (oa === undefined || ob === undefined || oa.template_index === undefined)
193
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 } });
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
196
  return fixRequest({ ...requestOf(req), effects: effects.filter((_, k) => k !== oa.template_index) });
197
197
  }
198
198
  if (err.code === "stale_plan" && isRecord(err.data.current) && typeof err.data.current.revision === "string") {
@@ -386,14 +386,14 @@ function underLock(req, fn) {
386
386
  */
387
387
  function executeAdmitted(store, req, plan, docStore) {
388
388
  for (const d of plan.preconditions.docs) {
389
- const row = readDocBase(store.db, d.id);
389
+ const row = readDocBase(store.db, d._id);
390
390
  if (!row)
391
- throw new MutationError("doc_missing", `doc ${d.id} no longer exists`, { doc: d.id });
392
- if (row.path !== d.path || row.currentRev !== d.base_revision || row.fileHash !== d.hash) {
393
- throw new MutationError("stale_plan", `document ${d.id} changed since the plan was made; re-plan`, {
394
- doc: d.id,
391
+ throw new MutationError("doc_missing", `doc ${d._id} no longer exists`, { doc: d._id });
392
+ if (row.path !== d._path || row.currentRev !== d.base_revision || row.fileHash !== d._hash) {
393
+ throw new MutationError("stale_plan", `document ${d._id} changed since the plan was made; re-plan`, {
394
+ doc: d._id,
395
395
  expected_revision: d.base_revision,
396
- current: { revision: row.currentRev, hash: row.fileHash },
396
+ current: { revision: row.currentRev, _hash: row.fileHash },
397
397
  retriable: true,
398
398
  fix: replanFix(plan),
399
399
  });
@@ -418,7 +418,7 @@ function executeAdmitted(store, req, plan, docStore) {
418
418
  }),
419
419
  };
420
420
  const stat = req.omgbaseDir !== undefined;
421
- const moved = new Map(plan.opset.moves.map((m) => [m.id, m]));
421
+ const moved = new Map(plan.opset.moves.map((m) => [m._id, m]));
422
422
  const blockSymbols = plan.new_ids.filter((s) => s.kind === "block").map((s) => s.symbol);
423
423
  const planner = new ChangesetPlanner(store, req.repoId, req.omgbaseDir);
424
424
  const { effects, outcome } = observeMints((prefix, id) => {
@@ -459,8 +459,8 @@ function executeAdmitted(store, req, plan, docStore) {
459
459
  const effects = [];
460
460
  for (const m of plan.opset.moves) {
461
461
  const bytes = docStore.read(m.from) ?? "";
462
- const rev = readDocBase(store.db, m.id)?.currentRev ?? null;
463
- effects.push({ kind: "move_path", docId: m.id, from: m.from, to: m.to, currentRev: rev, inbound: inboundLinksTo(store, req.repoId, m.id, m.from), bytes });
462
+ const rev = readDocBase(store.db, m._id)?.currentRev ?? null;
463
+ effects.push({ kind: "move_path", docId: m._id, from: m.from, to: m.to, currentRev: rev, inbound: inboundLinksTo(store, req.repoId, m._id, m.from), bytes });
464
464
  if (stat)
465
465
  effects.push({ kind: "clear_stat", path: m.from }, { kind: "record_stat", path: m.to, bytes });
466
466
  }
@@ -477,10 +477,10 @@ function executeAdmitted(store, req, plan, docStore) {
477
477
  effects.push(e);
478
478
  }
479
479
  for (const t of plan.opset.tombstones) {
480
- const rev = readDocBase(store.db, t.id)?.currentRev ?? null;
481
- effects.push({ kind: "tombstone", docId: t.id, path: t.path, currentRev: rev, pool: false });
480
+ const rev = readDocBase(store.db, t._id)?.currentRev ?? null;
481
+ effects.push({ kind: "tombstone", docId: t._id, path: t._path, currentRev: rev, pool: false });
482
482
  if (stat)
483
- effects.push({ kind: "clear_stat", path: t.path });
483
+ effects.push({ kind: "clear_stat", path: t._path });
484
484
  }
485
485
  const outcome = commitChangeset(store, req.repoId, head, effects, ts, { docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}) });
486
486
  return { effects, outcome };
@@ -491,9 +491,9 @@ function executeAdmitted(store, req, plan, docStore) {
491
491
  let j = 0;
492
492
  for (const e of effects) {
493
493
  if (e.kind === "ingest_reconciled")
494
- revisions.push({ id: outcome.ingests[j++].docId, path: e.path, hash: sha256(e.source).toString("hex") });
494
+ revisions.push({ _id: outcome.ingests[j++].docId, _path: e.path, _hash: sha256(e.source).toString("hex") });
495
495
  else if (e.kind === "ingest_known")
496
- revisions.push({ id: outcome.ingests[j++].docId, path: e.path, hash: sha256(e.rendered).toString("hex") });
496
+ revisions.push({ _id: outcome.ingests[j++].docId, _path: e.path, _hash: sha256(e.rendered).toString("hex") });
497
497
  }
498
498
  // `$new.n` → minted id: the i-th block symbol is the i-th `b` the replay
499
499
  // minted (all before the transaction, in the plan's order — the aliases
@@ -22,8 +22,8 @@ export interface Selection {
22
22
  export interface EffectOrigin {
23
23
  template_index?: number;
24
24
  row: {
25
- id: string;
26
- path: string;
25
+ _id: string;
26
+ _path: string;
27
27
  };
28
28
  }
29
29
  export interface CompiledSelection {
@@ -56,8 +56,8 @@ export declare function desugarLeader(v: string, here: string): unknown;
56
56
  * Desugar one effect or template as written (§11.8 "Sugar"), before the shape
57
57
  * check and the admission: in every slot that admits an expression — the
58
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
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
61
  * beginning `-> ` becomes `{ oqx }` (or folds to its literal); a `-> ` string
62
62
  * anywhere else (`expect`, `field`, `op`, a section's `heading_id`, …) is
63
63
  * refused. Every other string is literal data, with no escaping and no `${`
@@ -42,9 +42,9 @@ function isRecord(v) {
42
42
  function isOqx(v) {
43
43
  return isRecord(v) && Object.keys(v).length === 1 && typeof v.oqx === "string";
44
44
  }
45
- /** A row rendered as a value by the runner (`{ id, path }` and nothing else, spec/surface §1.4). */
45
+ /** A row rendered as a value by the runner (`{ _id, _path }` and nothing else, spec/surface §1.4). */
46
46
  function isRowRef(v) {
47
- return isRecord(v) && Object.keys(v).length === 2 && typeof v.id === "string" && typeof v.path === "string";
47
+ return isRecord(v) && Object.keys(v).length === 2 && typeof v._id === "string" && typeof v._path === "string";
48
48
  }
49
49
  function describeValue(v) {
50
50
  if (v === null || v === undefined)
@@ -63,8 +63,8 @@ function describeValue(v) {
63
63
  // OQX has no expression entry point: each `{ oqx: e }` is parsed through this
64
64
  // wrapper and the one select item taken. A top-level item is scope depth 1 —
65
65
  // exactly the hit's scope — so `^` and `N^` validate as they will evaluate.
66
- const WRAPPER_PREFIX = "select __omg_x: ";
67
- const WRAPPER_SUFFIX = " from docs";
66
+ const WRAPPER_PREFIX = "docs { select __omg_x: ";
67
+ const WRAPPER_SUFFIX = " }";
68
68
  const OFFSET_RE = /\(at offset (\d+)\)/;
69
69
  /** Parse one expression; a parse error's offset is shifted back by the wrapper and forward by `shift` (the `-> ` leader's 3). */
70
70
  function parseExpression(src, here, shift = 0) {
@@ -80,7 +80,8 @@ function parseExpression(src, here, shift = 0) {
80
80
  throw selectionInvalid(`${here}: ${message}`);
81
81
  }
82
82
  // The whole source must have been one expression: a query smuggled into the
83
- // slot (`x from blocks where …`) parses, but not as one item over `docs`.
83
+ // slot (`x where … }` closing the wrapper early does not; `x }` + `{ … }`
84
+ // cannot) parses, but not as one item over `docs`.
84
85
  const one = q.select.length === 1 && q.source.kind === "ident" && q.source.name === "docs" && q.from.length === 0 && q.where === null
85
86
  && q.orderBy === null && q.follow === null && q.frontier === null && q.limit === null && q.offset === null && !q.distinct && !q.values && q.consumer === "collect";
86
87
  if (!one)
@@ -144,8 +145,8 @@ function refuseLeader(v, here, path, fix) {
144
145
  * Desugar one effect or template as written (§11.8 "Sugar"), before the shape
145
146
  * check and the admission: in every slot that admits an expression — the
146
147
  * strings `markdown`, `to`, `path`, `separator`, `at.before` / `at.after`,
147
- * the JSON `value`, each top-level `create.frontmatter` value, a target's `id`
148
- * / `path`, and a whole target (`into`, `target`, `targets[k]`) — a string
148
+ * the JSON `value`, each top-level `create.frontmatter` value, a target's `_id`
149
+ * / `_path`, and a whole target (`into`, `target`, `targets[k]`) — a string
149
150
  * beginning `-> ` becomes `{ oqx }` (or folds to its literal); a `-> ` string
150
151
  * anywhere else (`expect`, `field`, `op`, a section's `heading_id`, …) is
151
152
  * refused. Every other string is literal data, with no escaping and no `${`
@@ -174,11 +175,11 @@ export function desugarEffect(raw, index, request) {
174
175
  if (isLeader(t))
175
176
  out[key] = desugarLeader(t, `${here}.${key}`);
176
177
  else if (isRecord(t)) {
177
- for (const member of ["id", "path"])
178
+ for (const member of ["_id", "_path"])
178
179
  if (isLeader(t[member]))
179
180
  t[member] = desugarLeader(t[member], `${here}.${key}.${member}`);
180
181
  for (const k of Object.keys(t))
181
- if (k !== "id" && k !== "path")
182
+ if (k !== "_id" && k !== "_path")
182
183
  refuse(t[k], `${here}.${key}.${k}`, [key, k]);
183
184
  }
184
185
  };
@@ -191,10 +192,10 @@ export function desugarEffect(raw, index, request) {
191
192
  if (isLeader(t))
192
193
  list[k] = desugarLeader(t, `${here}.targets[${k}]`);
193
194
  else if (isRecord(t)) {
194
- if (isLeader(t.id))
195
- t.id = desugarLeader(t.id, `${here}.targets[${k}].id`);
195
+ if (isLeader(t._id))
196
+ t._id = desugarLeader(t._id, `${here}.targets[${k}]._id`);
196
197
  for (const m of Object.keys(t))
197
- if (m !== "id")
198
+ if (m !== "_id")
198
199
  refuse(t[m], `${here}.targets[${k}].${m}`, ["targets", String(k), m]);
199
200
  }
200
201
  });
@@ -328,7 +329,7 @@ function admitTemplate(raw, index, root, plain, fix) {
328
329
  probe[key] = null;
329
330
  slots.push({ name: key, kind: "json", here: `${here}.${key}`, path: [key], expression: src, item: expr(src, key), put: (e, v) => { e[key] = v; } });
330
331
  };
331
- // A target member: `{ kind, id: { oqx } }` or `{ kind: "document", path: { oqx } }`, or (3.2) the whole target
332
+ // A target member: `{ kind, _id: { oqx } }` or `{ kind: "document", _path: { oqx } }`, or (3.2) the whole target
332
333
  // `{ oqx }` — the identity the value names; absent → the hit when `defaultable` (never on a plain effect).
333
334
  // `probeKind` is a kind the slot admits, standing in for the whole-target expression's unknown kind.
334
335
  const targetSlot = (key, defaultable, probeKind, primary) => {
@@ -337,7 +338,7 @@ function admitTemplate(raw, index, root, plain, fix) {
337
338
  if (t === undefined) {
338
339
  if (!defaultable || plain)
339
340
  return null;
340
- probe[key] = { kind: HIT_KIND[root], id: "$sel" };
341
+ probe[key] = { kind: HIT_KIND[root], _id: "$sel" };
341
342
  return { key, list: false };
342
343
  }
343
344
  if (isOqx(t)) {
@@ -345,19 +346,19 @@ function admitTemplate(raw, index, root, plain, fix) {
345
346
  throw needsSelector(`${here}.${key}`);
346
347
  const src = t.oqx;
347
348
  delete base[key];
348
- probe[key] = { kind: probeKind, id: "$sel" };
349
+ probe[key] = { kind: probeKind, _id: "$sel" };
349
350
  slots.push({ name: key, kind: "target", here: `${here}.${key}`, path: [key], expression: src, item: expr(src, key), put: (e, v) => { e[key] = v; } });
350
351
  return null;
351
352
  }
352
353
  if (isRecord(t)) {
353
- const idX = isOqx(t.id);
354
- const pathX = isOqx(t.path);
354
+ const idX = isOqx(t._id);
355
+ const pathX = isOqx(t._path);
355
356
  if (idX && pathX)
356
- throw selectionInvalid(`${here}.${key}: give \`id\` or \`path\` as an expression, not both`);
357
+ throw selectionInvalid(`${here}.${key}: give \`_id\` or \`_path\` as an expression, not both`);
357
358
  if (idX || pathX) {
358
359
  if (plain && primary)
359
360
  throw needsSelector(`${here}.${key}`);
360
- const member = idX ? "id" : "path";
361
+ const member = idX ? "_id" : "_path";
361
362
  const src = t[member].oqx;
362
363
  const literal = { ...t };
363
364
  delete literal[member];
@@ -369,12 +370,12 @@ function admitTemplate(raw, index, root, plain, fix) {
369
370
  });
370
371
  }
371
372
  for (const k of Object.keys(t))
372
- if (k !== "id" && k !== "path" && containsOqx(t[k]))
373
+ if (k !== "_id" && k !== "_path" && containsOqx(t[k]))
373
374
  throw selectionInvalid(`${here}.${key}.${k}: \`{ oqx }\` is not allowed here`);
374
375
  }
375
376
  return null;
376
377
  };
377
- // A `targets` list: each element `{ kind: "block", id: { oqx } }` (or, 3.2, a whole `{ oqx }`) yields one block;
378
+ // A `targets` list: each element `{ kind: "block", _id: { oqx } }` (or, 3.2, a whole `{ oqx }`) yields one block;
378
379
  // absent → `[the hit]` when `defaultable`. The list is always the primary target of its op.
379
380
  const targetsSlot = (defaultable) => {
380
381
  slotKeys.add("targets");
@@ -382,7 +383,7 @@ function admitTemplate(raw, index, root, plain, fix) {
382
383
  if (list === undefined) {
383
384
  if (!defaultable || plain)
384
385
  return null;
385
- probe.targets = [{ kind: HIT_KIND[root], id: "$sel" }];
386
+ probe.targets = [{ kind: HIT_KIND[root], _id: "$sel" }];
386
387
  return { key: "targets", list: true };
387
388
  }
388
389
  if (Array.isArray(list)) {
@@ -392,7 +393,7 @@ function admitTemplate(raw, index, root, plain, fix) {
392
393
  throw needsSelector(`${here}.targets[${k}]`);
393
394
  const src = t.oqx;
394
395
  base.targets[k] = {};
395
- probe.targets[k] = { kind: "block", id: "$sel" };
396
+ probe.targets[k] = { kind: "block", _id: "$sel" };
396
397
  slots.push({
397
398
  name: `targets_${k}`, kind: "target", here: `${here}.targets[${k}]`, path: ["targets", String(k)], expression: src, item: expr(src, `targets[${k}]`),
398
399
  put: (e, v) => { e.targets[k] = v; },
@@ -401,23 +402,23 @@ function admitTemplate(raw, index, root, plain, fix) {
401
402
  }
402
403
  if (!isRecord(t))
403
404
  return;
404
- if (isOqx(t.path))
405
- throw selectionInvalid(`${here}.targets[${k}]: a block target has \`id\`, not \`path\``);
406
- if (isOqx(t.id)) {
405
+ if (isOqx(t._path))
406
+ throw selectionInvalid(`${here}.targets[${k}]: a block target has \`_id\`, not \`_path\``);
407
+ if (isOqx(t._id)) {
407
408
  if (plain)
408
- throw needsSelector(`${here}.targets[${k}].id`);
409
- const src = t.id.oqx;
409
+ throw needsSelector(`${here}.targets[${k}]._id`);
410
+ const src = t._id.oqx;
410
411
  const literal = { ...t };
411
- delete literal.id;
412
+ delete literal._id;
412
413
  base.targets[k] = literal;
413
- probe.targets[k].id = "$sel";
414
+ probe.targets[k]._id = "$sel";
414
415
  slots.push({
415
- name: `targets_${k}`, kind: "identity", here: `${here}.targets[${k}].id`, path: ["targets", String(k), "id"], expression: src, item: expr(src, `targets[${k}].id`),
416
- put: (e, v) => { const arr = e.targets; arr[k] = { ...arr[k], ...identityMember("id", v) }; },
416
+ name: `targets_${k}`, kind: "identity", here: `${here}.targets[${k}]._id`, path: ["targets", String(k), "_id"], expression: src, item: expr(src, `targets[${k}]._id`),
417
+ put: (e, v) => { const arr = e.targets; arr[k] = { ...arr[k], ...identityMember("_id", v) }; },
417
418
  });
418
419
  }
419
420
  for (const key of Object.keys(t))
420
- if (key !== "id" && containsOqx(t[key]))
421
+ if (key !== "_id" && containsOqx(t[key]))
421
422
  throw selectionInvalid(`${here}.targets[${k}].${key}: \`{ oqx }\` is not allowed here`);
422
423
  });
423
424
  }
@@ -505,10 +506,10 @@ function admitTemplate(raw, index, root, plain, fix) {
505
506
  validateEffect(probe, index, fix);
506
507
  return { index, base, slots, defaultTarget };
507
508
  }
508
- /** The `id`/`path` member a bound identity value supplies: a string as given, a row ref by its id. */
509
+ /** The `_id`/`_path` member a bound identity value supplies: a string as given, a row ref by its id. */
509
510
  function identityMember(member, v) {
510
511
  if (isRowRef(v))
511
- return { id: v.id };
512
+ return { _id: v._id };
512
513
  return { [member]: v };
513
514
  }
514
515
  // ---- the request ---------------------------------------------------------------------
@@ -570,7 +571,7 @@ function admitSelector(select, request) {
570
571
  throw selectionInvalid("select: `revisions` rows have no effect target — select docs, blocks or nodes", rootFix(select, raw.source, request));
571
572
  }
572
573
  if (root === null)
573
- throw selectionInvalid("select: a selector scans a root collection — `from docs`, `from blocks` or `from nodes`", rootFix(select, raw.source, request));
574
+ throw selectionInvalid("select: a selector scans a root collection — `docs { … }`, `blocks { … }` or `nodes { … }`", rootFix(select, raw.source, request));
574
575
  if (root === "links")
575
576
  throw selectionInvalid("select: `links` rows have no effect target — select docs, blocks or nodes", rootFix(select, raw.source, request));
576
577
  if (raw.follow !== null)
@@ -624,7 +625,7 @@ export function compileSelection(store, repoId, select, expectRaw, templatesRaw)
624
625
  }
625
626
  // Row → effects: hits in result order × templates in order (k = h·T + t). A
626
627
  // selector with `flatten` (oqx 0.25; §11.8) fans out per FLATTENED row: each
627
- // hit is the parent row (its `id`/`path`), the slot items read the bound name
628
+ // hit is the parent row (its `_id`/`_path`), the slot items read the bound name
628
629
  // (`-> tag`) in the flattened scope, and `expect.max` — the page bound above —
629
630
  // counts the flattened hits.
630
631
  const effects = [];
@@ -632,7 +633,7 @@ export function compileSelection(store, repoId, select, expectRaw, templatesRaw)
632
633
  hits.forEach((hit) => {
633
634
  for (const t of templates) {
634
635
  const k = effects.length;
635
- const row = { id: hit.id, path: hit.path };
636
+ const row = { _id: hit._id, _path: hit._path };
636
637
  const origin = { template_index: t.index, row };
637
638
  effects.push(bind(hit, t, k, origin, root, request));
638
639
  origins.push(origin);
@@ -643,7 +644,7 @@ export function compileSelection(store, repoId, select, expectRaw, templatesRaw)
643
644
  ...(expect !== undefined ? { expect } : {}),
644
645
  templates: templatesRaw,
645
646
  cardinality: hits.length,
646
- identities: hits.map((h) => h.id),
647
+ identities: hits.map((h) => h._id),
647
648
  };
648
649
  return { effects, selection, origins };
649
650
  }
@@ -682,20 +683,20 @@ function slotName(t, slot) {
682
683
  */
683
684
  function ambiguousFix(request, t, s, hit, root, list) {
684
685
  const head = list[0];
685
- const first = isRowRef(head) ? head.id : head;
686
+ const first = isRowRef(head) ? head._id : head;
686
687
  if (typeof first !== "string")
687
688
  return {};
688
689
  const effects = withMember(request.effects, [String(t.index), ...s.path], first);
689
690
  if (request.select === undefined)
690
691
  return { fix: fixRequest({ effects }) };
691
- return { fix: fixRequest({ select: narrowedTo(root, hit.id), effects, expect: { max: 1 } }) };
692
+ return { fix: fixRequest({ select: narrowedTo(root, hit._id), effects, expect: { max: 1 } }) };
692
693
  }
693
694
  /** One hit × one template → the concrete effect (the binding table of §11.8). */
694
695
  function bind(hit, t, k, origin, root, request) {
695
696
  const e = clone(t.base);
696
697
  const data = { effect_index: k, ...(origin.template_index !== undefined ? { template_index: origin.template_index } : {}), row: origin.row };
697
698
  if (t.defaultTarget !== null) {
698
- const target = { kind: HIT_KIND[root], id: hit.id };
699
+ const target = { kind: HIT_KIND[root], _id: hit._id };
699
700
  e[t.defaultTarget.key] = t.defaultTarget.list ? [target] : target;
700
701
  }
701
702
  for (const s of t.slots) {
@@ -705,10 +706,10 @@ function bind(hit, t, k, origin, root, request) {
705
706
  case "identity":
706
707
  case "target": {
707
708
  if (absent || (Array.isArray(v) && v.length === 0)) {
708
- throw new MutationError("target_missing", `${s.here}: the expression yielded no identity for row ${hit.id}`, { ...data, expression: s.expression });
709
+ throw new MutationError("target_missing", `${s.here}: the expression yielded no identity for row ${hit._id}`, { ...data, expression: s.expression });
709
710
  }
710
711
  if (Array.isArray(v)) {
711
- throw new MutationError("ambiguous_target", `${s.here}: the expression yielded ${v.length} identities for row ${hit.id}; a target resolves to exactly one`, { ...data, expression: s.expression, count: v.length, ...ambiguousFix(request, t, s, hit, root, v) });
712
+ throw new MutationError("ambiguous_target", `${s.here}: the expression yielded ${v.length} identities for row ${hit._id}; a target resolves to exactly one`, { ...data, expression: s.expression, count: v.length, ...ambiguousFix(request, t, s, hit, root, v) });
712
713
  }
713
714
  if (typeof v !== "string" && !isRowRef(v))
714
715
  throw selectionInvalid(`${s.here}: the expression yielded ${describeValue(v)}, not an identity (a string or a row)`, { ...data, expression: s.expression });
@@ -717,7 +718,7 @@ function bind(hit, t, k, origin, root, request) {
717
718
  break;
718
719
  }
719
720
  // (3.2) a whole target: a row by its id's prefix, a string as a bare target (a path → document, an id → its kind).
720
- const ref = isRowRef(v) ? v.id : v;
721
+ const ref = isRowRef(v) ? v._id : v;
721
722
  if (!isRowRef(v) && !looksLikePath(ref) && !isTargetId(ref) && !isNewSymbol(ref)) {
722
723
  throw selectionInvalid(`${s.here}: the expression yielded ${describeValue(v)}, not an identity (a document path, an id or a row)`, { ...data, expression: s.expression });
723
724
  }
@@ -726,14 +727,14 @@ function bind(hit, t, k, origin, root, request) {
726
727
  }
727
728
  case "string":
728
729
  if (absent)
729
- throw selectionInvalid(`${s.here}: the expression yielded absent for row ${hit.id}`, { ...data, expression: s.expression });
730
+ throw selectionInvalid(`${s.here}: the expression yielded absent for row ${hit._id}`, { ...data, expression: s.expression });
730
731
  if (typeof v !== "string")
731
732
  throw selectionInvalid(`${s.here}: the expression yielded ${describeValue(v)}, expected a string`, { ...data, expression: s.expression });
732
733
  s.put(e, v);
733
734
  break;
734
735
  case "json":
735
736
  if (absent)
736
- throw selectionInvalid(`${s.here}: the expression yielded absent for row ${hit.id}${s.name === "value" ? " — use `unset` to remove a key" : ""}`, { ...data, expression: s.expression });
737
+ throw selectionInvalid(`${s.here}: the expression yielded absent for row ${hit._id}${s.name === "value" ? " — use `unset` to remove a key" : ""}`, { ...data, expression: s.expression });
737
738
  s.put(e, v);
738
739
  break;
739
740
  }
@@ -813,25 +814,25 @@ function scopeOf(store, repoId, e, i) {
813
814
  validateEffect(e, i); // an unknown op (or `retarget`): the shape check refuses it
814
815
  throw effectInvalid(`${here}: \`op\` ${JSON.stringify(e.op)} is not row-shaped`);
815
816
  }
816
- if (isOqx(v) || (isRecord(v) && (isOqx(v.id) || isOqx(v.path))))
817
+ if (isOqx(v) || (isRecord(v) && (isOqx(v._id) || isOqx(v._path))))
817
818
  throw needsSelector(`${here}.${key}`);
818
819
  const t = validateTarget(v, `${here}.${key}`, kinds);
819
820
  switch (t.kind) {
820
821
  case "symbol":
821
- throw selectionInvalid(`${here}.${key}: ${t.id} has no row yet — it is created by this plan; use a literal target or a selector`);
822
+ throw selectionInvalid(`${here}.${key}: ${t._id} has no row yet — it is created by this plan; use a literal target or a selector`);
822
823
  case "document": {
823
824
  const d = findDocumentTarget(store, repoId, t);
824
- return { root: "docs", id: d.id, missing: () => new MutationError("doc_missing", `doc ${d.id} not found`, { doc: d.id }) };
825
+ return { root: "docs", id: d._id, missing: () => new MutationError("doc_missing", `doc ${d._id} not found`, { doc: d._id }) };
825
826
  }
826
827
  case "block":
827
828
  return {
828
- root: "blocks", id: t.id,
829
- missing: () => (placement ? new MutationError("parent_missing", `parent ${t.id} not found`) : new MutationError("block_missing", `block ${t.id} not found`, { block: t.id })),
829
+ root: "blocks", id: t._id,
830
+ missing: () => (placement ? new MutationError("parent_missing", `parent ${t._id} not found`) : new MutationError("block_missing", `block ${t._id} not found`, { block: t._id })),
830
831
  };
831
832
  case "section":
832
833
  return { root: "blocks", id: t.heading_id, missing: () => new MutationError("parent_missing", `parent ${t.heading_id} not found`) };
833
834
  case "node":
834
- return { root: "nodes", id: t.id, missing: () => new MutationError("block_missing", `node ${t.id} not found`, {}) };
835
+ return { root: "nodes", id: t._id, missing: () => new MutationError("block_missing", `node ${t._id} not found`, {}) };
835
836
  }
836
837
  }
837
838
  /**
@@ -839,7 +840,7 @@ function scopeOf(store, repoId, e, i) {
839
840
  * then — when nothing is an expression — the shape check as before (3.0
840
841
  * byte-identical). Otherwise each effect with expressions is admitted as a
841
842
  * template over its own primary target's row: the effects are grouped by that
842
- * row's identity, one synthetic query per identity — `from <root> where id ==
843
+ * row's identity, one synthetic query per identity — `from <root> where _id ==
843
844
  * "<id>"` with the slot items appended, named `__omg_sel_<effectIndex>_<slot>`
844
845
  * — runs, and the row binds the slots exactly as a selector's hit would. The
845
846
  * concrete effects feed the lowering unchanged; `templates` is the list as
@@ -893,7 +894,7 @@ export function compilePlain(store, repoId, raw) {
893
894
  g.members.push(t);
894
895
  });
895
896
  for (const g of scopes.values()) {
896
- const q = parse(`from ${g.root} where id == ${JSON.stringify(g.id)}`);
897
+ const q = parse(`from ${g.root} where _id == ${JSON.stringify(g.id)}`);
897
898
  const items = [];
898
899
  for (const t of g.members)
899
900
  for (const s of t.slots)
@@ -903,7 +904,7 @@ export function compilePlain(store, repoId, raw) {
903
904
  if (hit === undefined)
904
905
  throw g.missing();
905
906
  for (const t of g.members) {
906
- const origin = { row: { id: hit.id, path: hit.path } };
907
+ const origin = { row: { _id: hit._id, _path: hit._path } };
907
908
  effects[t.index] = bind(hit, t, t.index, origin, g.root, request);
908
909
  origins[t.index] = origin;
909
910
  }