@omgbase/core 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/src/core/ingest.d.ts +2 -0
  2. package/dist/src/core/ingest.js +13 -0
  3. package/dist/src/core/read/document.d.ts +9 -8
  4. package/dist/src/core/read/document.js +0 -2
  5. package/dist/src/core/store/gc.d.ts +13 -1
  6. package/dist/src/core/store/gc.js +24 -5
  7. package/dist/src/core/store/size.d.ts +4 -1
  8. package/dist/src/core/store/size.js +11 -2
  9. package/dist/src/mcp/arguments.d.ts +10 -2
  10. package/dist/src/mcp/arguments.js +37 -3
  11. package/dist/src/mcp/reference.d.ts +1 -1
  12. package/dist/src/mcp/reference.js +36 -4
  13. package/dist/src/mcp/server.js +50 -23
  14. package/dist/src/mutate/effects.d.ts +23 -5
  15. package/dist/src/mutate/effects.js +111 -22
  16. package/dist/src/mutate/known-ids.d.ts +19 -0
  17. package/dist/src/mutate/known-ids.js +47 -7
  18. package/dist/src/mutate/ops.d.ts +1 -0
  19. package/dist/src/mutate/ops.js +20 -1
  20. package/dist/src/mutate/select.d.ts +0 -11
  21. package/dist/src/mutate/select.js +33 -9
  22. package/dist/src/oqx-js/context.d.ts +9 -2
  23. package/dist/src/oqx-js/context.js +74 -28
  24. package/dist/src/oqx-js/names.d.ts +40 -0
  25. package/dist/src/oqx-js/names.js +347 -0
  26. package/dist/src/oqx-js/planner.d.ts +3 -3
  27. package/dist/src/oqx-js/planner.js +13 -9
  28. package/dist/src/oqx-js/root-row.d.ts +15 -4
  29. package/dist/src/oqx-js/root-row.js +18 -10
  30. package/dist/src/oqx-js/run.d.ts +18 -1
  31. package/dist/src/oqx-js/run.js +31 -12
  32. package/dist/src/oqx-js/sql/translate.js +1 -1
  33. package/dist/src/spec-versions.d.ts +5 -5
  34. package/dist/src/spec-versions.js +4 -4
  35. package/dist/src/sync/observe.js +9 -3
  36. package/package.json +2 -2
@@ -0,0 +1,347 @@
1
+ // Unknown names (spec/surface §1.5, 4.6). A query that projects, filters or
2
+ // orders by a name no row of the scope's kind can carry — `select path, target
3
+ // from links` (links have `dst_path`), `where preview != ""` on blocks — used
4
+ // to run silently: an absent column in every row, a predicate true or false of
5
+ // every row. This pass judges every name the query reads (`@omgbase/oqx`'s
6
+ // `references`, AST.md §5b) against the VOCABULARY of the row kind it is read
7
+ // from, statically — before any row is read, so the verdict never depends on
8
+ // the query's own filter — and either records a hint or refuses the query.
9
+ //
10
+ // The three tiers of a name at a scope whose kind is known:
11
+ // 1. an engine attribute, handle or relation of the kind — known;
12
+ // 2. an authored property at least one live object of the kind in this
13
+ // repository carries (docs: a frontmatter or inline key — `stage` on three
14
+ // processes is enough; blocks and nodes: a key of a live row's `attrs`,
15
+ // the bare-name read of those kinds) — known, however partial;
16
+ // 3. a name no object of the kind carries — the complaint.
17
+ // Severity: `links` has a closed row shape (no authored data reaches a link
18
+ // row), so a tier-3 name there is `filter_invalid`; docs, blocks and nodes are
19
+ // open — on a read the result carries one hint per offending name and the rows
20
+ // still come back; a write selector (the AST entry, `unknownNames: "refuse"`)
21
+ // is refused, since a name no row carries would select every row. A repository
22
+ // with no live object of the kind has no vocabulary: the check is skipped for
23
+ // that kind. The vocabulary is read once per (repository, kind) and cached on
24
+ // the repository's head commit (`MAX(commits.seq)`), the one thing every
25
+ // property or attrs change moves.
26
+ import { references } from "@omgbase/oqx";
27
+ import { ATTRIBUTES, SOURCE_HANDLES } from "./context.js";
28
+ import { ROOTS } from "./root-row.js";
29
+ import { FROM, guards } from "./sql/scan.js";
30
+ import { FilterInvalid } from "../search/cel/parser.js";
31
+ /** The relations of each kind and the kind of the rows they yield (spec/surface §1.2). */
32
+ export const RELATIONS = {
33
+ docs: { blocks: "blocks", nodes: "nodes", out_links: "links", in_links: "links", revisions: "revisions" },
34
+ blocks: { children: "blocks", nodes: "nodes", out_links: "links", section: "nodes" },
35
+ nodes: { blocks: "blocks", subsections: "nodes", children: "nodes" },
36
+ links: {},
37
+ revisions: {},
38
+ };
39
+ /** The self and reach-through handles of each kind and the kind of the row they reach (§1.2). */
40
+ export const HANDLES = {
41
+ docs: { doc: "docs" },
42
+ blocks: { block: "blocks", doc: "docs" },
43
+ nodes: { section: "nodes", doc: "docs", block: "blocks" },
44
+ links: { doc: "docs", dst: "docs" },
45
+ revisions: { doc: "docs" },
46
+ };
47
+ /** The kinds whose row shape is closed: no authored data reaches a row by bare name. */
48
+ const CLOSED = new Set(["links", "revisions"]);
49
+ const NOUN = { docs: "document", blocks: "block", nodes: "node", links: "link", revisions: "revision" };
50
+ const CACHE = new WeakMap();
51
+ function headSeq(store, repoId) {
52
+ const row = store.db.prepare("SELECT COALESCE(MAX(seq), 0) AS seq FROM commits WHERE repo_id = ?").get(repoId);
53
+ return row?.seq ?? 0;
54
+ }
55
+ function repoCache(store, repoId) {
56
+ let repos = CACHE.get(store);
57
+ if (!repos)
58
+ CACHE.set(store, (repos = new Map()));
59
+ const head = headSeq(store, repoId);
60
+ let c = repos.get(repoId);
61
+ if (!c || c.head !== head)
62
+ repos.set(repoId, (c = { head, vocab: {} }));
63
+ return c;
64
+ }
65
+ /** The authored vocabulary of a kind: docs — every live frontmatter or inline
66
+ * key (the merged read a bare name performs); blocks and nodes — every key a
67
+ * live row's `attrs` holds (the bare-name read of those kinds). Computed keys
68
+ * (`$title`, `$tags`) are the intrinsics `title`/`tags`, not authored keys. */
69
+ function authoredKeys(store, repoId, target) {
70
+ let sql;
71
+ switch (target) {
72
+ case "docs":
73
+ sql = "SELECT DISTINCT key FROM properties WHERE repo_id = ? AND deleted_commit IS NULL AND source IN ('frontmatter', 'inline') ORDER BY key";
74
+ return store.db.prepare(sql).all(repoId).map((r) => r.key);
75
+ case "blocks":
76
+ case "nodes": {
77
+ const g = guards(target, repoId);
78
+ const alias = target === "blocks" ? "b" : "n";
79
+ sql = `SELECT DISTINCT j.key AS key FROM ${FROM[target]}, json_each(${alias}.attrs) j WHERE ${g.sql} ORDER BY key`;
80
+ return store.db.prepare(sql).all(...g.params).map((r) => r.key);
81
+ }
82
+ case "links":
83
+ case "revisions": return [];
84
+ }
85
+ }
86
+ function present(store, repoId, target) {
87
+ // A closed kind has no authored vocabulary to be absent; the question never reaches it.
88
+ if (target === "revisions")
89
+ return true;
90
+ const g = guards(target, repoId);
91
+ return store.db.prepare(`SELECT 1 FROM ${FROM[target]} WHERE ${g.sql} LIMIT 1`).get(...g.params) !== undefined;
92
+ }
93
+ /** The vocabulary of a kind in a repository (cached on the head commit; the SQL runs only when a name is not intrinsic). */
94
+ export function vocabulary(store, repoId, target) {
95
+ const v = vocabularyOf(store, repoId, target);
96
+ return { present: v.present, keys: v.list };
97
+ }
98
+ function vocabularyOf(store, repoId, target) {
99
+ const c = repoCache(store, repoId);
100
+ let v = c.vocab[target];
101
+ if (v)
102
+ return v;
103
+ const list = authoredKeys(store, repoId, target);
104
+ const keys = new Set();
105
+ for (const k of list) {
106
+ const segs = k.split(".");
107
+ for (let i = 1; i <= segs.length; i++)
108
+ keys.add(segs.slice(0, i).join("."));
109
+ }
110
+ v = { present: list.length > 0 || present(store, repoId, target), keys, list };
111
+ c.vocab[target] = v;
112
+ return v;
113
+ }
114
+ /** The kind of the rows an expression yields when read on rows of `on` (`"root"`: the root scope), or null when unknown. */
115
+ function kindOfExpr(e, on, readDepth, byDepth) {
116
+ switch (e.kind) {
117
+ case "ident": return kindOfPath(on, [e.name]);
118
+ case "outer": {
119
+ const k = readDepth - e.levels;
120
+ return kindOfPath(k <= 0 ? "root" : byDepth.get(k) ?? null, [e.name]);
121
+ }
122
+ case "member": {
123
+ const path = dotted(e);
124
+ if (path === null)
125
+ return null;
126
+ const head = path.head;
127
+ const base = head.kind === "ident" ? on : (readDepth - head.levels <= 0 ? "root" : byDepth.get(readDepth - head.levels) ?? null);
128
+ return kindOfPath(base, [head.name, ...path.rest]);
129
+ }
130
+ // `refs(x)` resolves document references to live document rows (§1.3).
131
+ case "call": return e.recv === null && e.name === "refs" ? "docs" : null;
132
+ // A value-position block with no projection passes its receiver's rows through (then its own `from`).
133
+ case "op": {
134
+ if (e.sub.select.some((it) => it.name !== "$key") || e.sub.values)
135
+ return null;
136
+ let k = kindOfExpr(e.receiver, on, readDepth, byDepth);
137
+ for (const f of e.sub.from)
138
+ k = k === null ? null : kindOfExpr(f, k, readDepth + 1, byDepth);
139
+ return k;
140
+ }
141
+ default: return null; // a literal, a binding, a tuple (mixed), a template, an operator
142
+ }
143
+ }
144
+ /** A dotted navigation headed by a name. */
145
+ function dotted(e) {
146
+ if (e.kind === "ident" || e.kind === "outer")
147
+ return { head: e, rest: [] };
148
+ if (e.kind !== "member")
149
+ return null;
150
+ const inner = dotted(e.recv);
151
+ return inner === null ? null : { head: inner.head, rest: [...inner.rest, e.name] };
152
+ }
153
+ /** Follow a dotted path from a row kind through handles and relations (`doc.blocks` on a link → blocks). */
154
+ function kindOfPath(on, path) {
155
+ let cur = on;
156
+ for (let i = 0; i < path.length; i++) {
157
+ const seg = path[i];
158
+ if (cur === null)
159
+ return null;
160
+ if (cur === "root") {
161
+ if (seg === "$it")
162
+ continue;
163
+ if (!ROOTS.has(seg))
164
+ return null;
165
+ cur = seg;
166
+ continue;
167
+ }
168
+ if (seg === "$it" && i === 0)
169
+ continue;
170
+ const next = HANDLES[cur][seg] ?? RELATIONS[cur][seg];
171
+ if (next === undefined)
172
+ return null;
173
+ cur = next;
174
+ }
175
+ return cur === "root" ? null : cur;
176
+ }
177
+ /** The kind of the frontier rows of a walk: the seeds' kind when every destination yields rows of that same kind, else unknown. */
178
+ function kindOfFrontier(seed, f, readDepth, byDepth) {
179
+ if (seed === null)
180
+ return null;
181
+ // A destination block is a value-position `op`: its receiver on the frontier row, then its own `from`.
182
+ for (const d of f.destinations)
183
+ if (kindOfExpr(d, seed, readDepth, byDepth) !== seed)
184
+ return null;
185
+ return seed;
186
+ }
187
+ /** The kind of the rows a scope holds, from its row chain (AST.md §5b); null when the chain reaches a value no kind describes. */
188
+ export function kindOfScope(steps) {
189
+ if (steps.length === 0)
190
+ return null; // the root scope: root names are judged elsewhere
191
+ const byDepth = new Map([[0, "root"]]);
192
+ let cur = "root";
193
+ let readDepth = 0;
194
+ for (const step of steps) {
195
+ const e = step.expr;
196
+ if (e.kind === "follow")
197
+ cur = kindOfFrontier(cur === "root" ? null : cur, e, readDepth, byDepth);
198
+ else
199
+ cur = kindOfExpr(e, cur, readDepth, byDepth);
200
+ byDepth.set(step.depth, cur);
201
+ readDepth = step.depth;
202
+ }
203
+ return cur === "root" ? null : cur;
204
+ }
205
+ /** Judge a dotted name read on rows of `target`; null = known. */
206
+ function judge(store, repoId, target, path) {
207
+ const [head, ...rest] = path;
208
+ // An engine attribute wins on collision; what follows it (`attrs.checked`, `tags.size`) is the value's own navigation.
209
+ if (ATTRIBUTES[target].has(head))
210
+ return null;
211
+ // The source handles reach the authored data under them.
212
+ if (target === "docs" && SOURCE_HANDLES.has(head))
213
+ return rest.length === 0 ? null : judgeAuthored(store, repoId, "docs", rest, head);
214
+ const via = HANDLES[target][head] ?? RELATIONS[target][head];
215
+ if (via !== undefined)
216
+ return rest.length === 0 ? null : judge(store, repoId, via, rest);
217
+ // A bare target name on a row is the context's own loud error (root-row.ts); not doubled here.
218
+ if (ROOTS.has(head))
219
+ return null;
220
+ return judgeAuthored(store, repoId, target, path);
221
+ }
222
+ function judgeAuthored(store, repoId, target, path, handle) {
223
+ const name = path.join(".");
224
+ if (CLOSED.has(target))
225
+ return { target, name, closed: true };
226
+ const v = vocabularyOf(store, repoId, target);
227
+ if (!v.present)
228
+ return null; // no object of the kind: no vocabulary to judge by
229
+ if (v.keys.has(name))
230
+ return null;
231
+ return { target, name, closed: false, ...(handle !== undefined ? { handle } : {}) };
232
+ }
233
+ // ---- messages --------------------------------------------------------------
234
+ /** The names a kind knows, for the nearest match: attributes, handles, relations, the authored keys. */
235
+ function candidates(store, repoId, target) {
236
+ const out = [...ATTRIBUTES[target], ...Object.keys(HANDLES[target]), ...(target === "docs" ? [...SOURCE_HANDLES] : []), ...Object.keys(RELATIONS[target])];
237
+ if (!CLOSED.has(target))
238
+ out.push(...vocabularyOf(store, repoId, target).list);
239
+ return out;
240
+ }
241
+ /** Optimal string alignment distance (Damerau–Levenshtein with adjacent transpositions, each edit 1). */
242
+ export function editDistance(a, b) {
243
+ const s = Array.from(a), t = Array.from(b);
244
+ const m = s.length, n = t.length;
245
+ const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
246
+ for (let i = 0; i <= m; i++)
247
+ d[i][0] = i;
248
+ for (let j = 0; j <= n; j++)
249
+ d[0][j] = j;
250
+ for (let i = 1; i <= m; i++) {
251
+ for (let j = 1; j <= n; j++) {
252
+ const cost = s[i - 1] === t[j - 1] ? 0 : 1;
253
+ let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
254
+ if (i > 1 && j > 1 && s[i - 1] === t[j - 2] && s[i - 2] === t[j - 1])
255
+ v = Math.min(v, d[i - 2][j - 2] + 1);
256
+ d[i][j] = v;
257
+ }
258
+ }
259
+ return d[m][n];
260
+ }
261
+ /**
262
+ * The nearest match of `name` among `candidates` (§1.5): case-insensitive; a
263
+ * candidate qualifies when its distance is ≤ 2, or when one of the two is a
264
+ * substring of the other and the shorter has at least three characters; the
265
+ * lowest distance wins, ties broken by the shorter candidate, then
266
+ * alphabetically. Null when nothing qualifies.
267
+ */
268
+ export function nearest(name, candidates) {
269
+ const needle = name.toLowerCase();
270
+ // A name under three characters is within distance 2 of nearly anything, so
271
+ // only a substring match (which needs three characters anyway) could qualify.
272
+ if (needle.length < 3)
273
+ return null;
274
+ let best = null;
275
+ for (const c of new Set(candidates)) {
276
+ const hay = c.toLowerCase();
277
+ if (hay === needle)
278
+ continue;
279
+ const d = editDistance(needle, hay);
280
+ const shorter = needle.length <= hay.length ? needle : hay;
281
+ const longer = shorter === needle ? hay : needle;
282
+ const sub = shorter.length >= 3 && longer.includes(shorter);
283
+ if (!(d <= 2 || sub))
284
+ continue;
285
+ // One typo (distance 1) wins first; then a substring match outranks a
286
+ // distance-2 match (`via` → `via_id`, not `id`; `dst_pth` → `dst_path`, not
287
+ // `dst`); within a class the lowest distance, then the shorter candidate,
288
+ // then alphabetical.
289
+ const rank = (x) => (x.d === 1 ? 0 : x.sub ? 1 : 2);
290
+ const better = best === null ||
291
+ rank({ d, sub }) < rank(best) ||
292
+ (rank({ d, sub }) === rank(best) && (d < best.d || (d === best.d && (c.length < best.c.length || (c.length === best.c.length && c < best.c)))));
293
+ if (better)
294
+ best = { c, d, sub };
295
+ }
296
+ return best === null ? null : best.c;
297
+ }
298
+ function suggestion(name, candidates) {
299
+ const c = nearest(name, candidates);
300
+ return c === null ? "" : `; did you mean \`${c}\`?`;
301
+ }
302
+ /** The error of a closed kind: `` `target` is not a field of links (fields: id, path, …); did you mean `…`? ``. */
303
+ export function fieldErrorMessage(store, repoId, target, name) {
304
+ return `\`${name}\` is not a field of ${target} (fields: ${[...ATTRIBUTES[target]].join(", ")})${suggestion(name, candidates(store, repoId, target))}`;
305
+ }
306
+ /** The hint of an open kind: `` `preview` is not a field of blocks and no block in this repository carries a property by that name; did you mean `…`? ``.
307
+ * Under a source handle the name is an authored lookup by construction, so the
308
+ * hint says only that: `` no document in this repository carries a property `path` (`data.path`); did you mean `…`? ``,
309
+ * the nearest match drawn from the authored keys alone. */
310
+ export function propertyHintMessage(store, repoId, target, name, handle) {
311
+ if (handle !== undefined) {
312
+ return `no ${NOUN[target]} in this repository carries a property \`${name}\` (\`${handle}.${name}\`)${suggestion(name, vocabularyOf(store, repoId, target).list)}`;
313
+ }
314
+ return `\`${name}\` is not a field of ${target} and no ${NOUN[target]} in this repository carries a property by that name${suggestion(name, candidates(store, repoId, target))}`;
315
+ }
316
+ // ---- the pass --------------------------------------------------------------
317
+ /**
318
+ * Judge every name the query reads. Returns the hints (one per offending name
319
+ * of an open kind, first-reference order, deduplicated) under `"hint"`; throws
320
+ * `FilterInvalid` for a tier-3 name on a closed kind in either mode, and for
321
+ * one on an open kind under `"refuse"`.
322
+ */
323
+ export function checkNames(store, repoId, raw, mode) {
324
+ const hints = [];
325
+ const seen = new Set();
326
+ for (const ref of references(raw)) {
327
+ if (ref.scope.steps.length === 0)
328
+ continue; // a root name
329
+ const kind = kindOfScope(ref.scope.steps);
330
+ if (kind === null)
331
+ continue;
332
+ const verdict = judge(store, repoId, kind, ref.name.split("."));
333
+ if (verdict === null)
334
+ continue;
335
+ if (verdict.closed)
336
+ throw new FilterInvalid(fieldErrorMessage(store, repoId, verdict.target, verdict.name), "OQX");
337
+ const message = propertyHintMessage(store, repoId, verdict.target, verdict.name, verdict.handle);
338
+ if (mode === "refuse")
339
+ throw new FilterInvalid(message, "OQX");
340
+ if (!seen.has(message)) {
341
+ seen.add(message);
342
+ hints.push(message);
343
+ }
344
+ }
345
+ return hints;
346
+ }
347
+ //# sourceMappingURL=names.js.map
@@ -15,9 +15,9 @@ export declare function rootTarget(source: Expr): Target | null;
15
15
  * • a bare reserved docs basename (`id path updated_at hash raw`): as
16
16
  * an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
17
17
  * on any target at any depth (the reach-through row is a doc)
18
- * • a bare target name (`docs blocks nodes links`, root-row.ts) read as a
19
- * property: as an `ident` at the ROOT scope when it is not a relation of
20
- * the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
18
+ * • a bare root name (`docs blocks nodes links revisions`, root-row.ts) read
19
+ * as a property: as an `ident` at the ROOT scope when it is not a relation
20
+ * of the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
21
21
  * as an `ident` at any depth below (the rows there are unknown), or as
22
22
  * `<head>.<name>` unless the head is a reach-through whose row has that
23
23
  * relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
@@ -14,15 +14,16 @@
14
14
  // `[]` planned, `filter_invalid` in memory), so when any residual conjunct could
15
15
  // raise — a function or method call, a `single` block, a `^`-escaped name, or a
16
16
  // bare reserved docs basename — the WHOLE query runs unplanned.
17
- import { ROWS_ROOT, partitionPushable, residualQuery } from "@omgbase/oqx";
17
+ import { ROWS_ROOT, isRegexOp, partitionPushable, residualQuery } from "@omgbase/oqx";
18
18
  import { makeStoreContext, tagRows } from "./context.js";
19
19
  import { translatePredicate } from "./sql/translate.js";
20
20
  import { ALIAS, COLS, FROM, ORDER, guards } from "./sql/scan.js";
21
- import { REACH_THROUGH, TARGETS, isTargetRelationOf } from "./root-row.js";
21
+ import { REACH_THROUGH, ROOTS, TARGETS, isTargetRelationOf } from "./root-row.js";
22
22
  // The root collection a query scans, if it is a bare `docs|blocks|nodes|links`
23
23
  // source at the root scope (else null — not a pushable shape; a nested `^docs`
24
- // is a block's receiver, never the query's source). run.ts also asks: a bare
25
- // root scan's rows are store rows by construction.
24
+ // is a block's receiver, never the query's source). `revisions` (4.7) is a root
25
+ // the planner never plans — it is not a `Target` here — so a revisions scan
26
+ // always runs in memory.
26
27
  export function rootTarget(source) {
27
28
  if (source.kind === "ident" && TARGETS.has(source.name))
28
29
  return source.name;
@@ -41,9 +42,9 @@ export function rootTarget(source) {
41
42
  * • a bare reserved docs basename (`id path updated_at hash raw`): as
42
43
  * an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
43
44
  * on any target at any depth (the reach-through row is a doc)
44
- * • a bare target name (`docs blocks nodes links`, root-row.ts) read as a
45
- * property: as an `ident` at the ROOT scope when it is not a relation of
46
- * the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
45
+ * • a bare root name (`docs blocks nodes links revisions`, root-row.ts) read
46
+ * as a property: as an `ident` at the ROOT scope when it is not a relation
47
+ * of the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
47
48
  * as an `ident` at any depth below (the rows there are unknown), or as
48
49
  * `<head>.<name>` unless the head is a reach-through whose row has that
49
50
  * relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
@@ -107,9 +108,9 @@ function exprMayRaise(e, target, root) {
107
108
  case "call": return true;
108
109
  case "outer": return true;
109
110
  case "ident":
110
- return TARGETS.has(e.name) && (!root || !isTargetRelationOf(target, e.name));
111
+ return ROOTS.has(e.name) && (!root || !isTargetRelationOf(target, e.name));
111
112
  case "member":
112
- if (TARGETS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
113
+ if (ROOTS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
113
114
  return true;
114
115
  return exprMayRaise(e.recv, target, root);
115
116
  case "unary": return exprMayRaise(e.expr, target, root);
@@ -117,7 +118,10 @@ function exprMayRaise(e, target, root) {
117
118
  // read anything its block reads.
118
119
  case "required": return true;
119
120
  case "op": return opMayRaise(e, target, root);
121
+ // A regex operator (oqx 0.22) is a `matches` call: an invalid pattern
122
+ // raises, exactly as the call would.
120
123
  case "binary":
124
+ return isRegexOp(e.op) || exprMayRaise(e.left, target, root) || exprMayRaise(e.right, target, root);
121
125
  case "logical":
122
126
  case "in":
123
127
  return exprMayRaise(e.left, target, root) || exprMayRaise(e.right, target, root);
@@ -1,7 +1,18 @@
1
1
  export type Target = "docs" | "blocks" | "nodes" | "links";
2
+ /** Every root collection (spec/surface §1.1): the four row kinds the
3
+ * unknown-names pass judges (`TARGETS`, names.ts) plus `revisions` (4.7), the
4
+ * history target — one row per revision of the repository's documents. */
5
+ export type Root = Target | "revisions";
6
+ /** The four row kinds whose names the unknown-names pass judges (names.ts keys
7
+ * its vocabulary tables by them); `revisions` joins when that pass learns the
8
+ * kind. Every OTHER root-collection test uses `ROOTS`. */
2
9
  export declare const TARGETS: ReadonlySet<string>;
3
- /** The target names that are a relation of each target's rows (§1.2). */
4
- export declare const TARGET_RELATIONS: Readonly<Record<Target, ReadonlySet<string>>>;
10
+ /** The root collections, in the root object's entry order (§1.1). */
11
+ export declare const ROOT_NAMES: readonly Root[];
12
+ export declare const ROOTS: ReadonlySet<string>;
13
+ /** The root names that are a relation of each kind's rows (§1.2): `revisions`
14
+ * on a document is its revision list (4.7). */
15
+ export declare const TARGET_RELATIONS: Readonly<Record<Root, ReadonlySet<string>>>;
5
16
  /** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`, and a link's `dst.x`). */
6
17
  export declare const REACH_THROUGH: Readonly<Record<string, Target>>;
7
18
  export declare const REPO_REMOVED_MESSAGE = "`$repo` was removed in surface 2.0 \u2014 reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^id` / `0^id`";
@@ -19,10 +30,10 @@ export declare function bareTargetMessage(name: string, depth: number | null): s
19
30
  export declare function pastRootMessage(name: string, levels: number): string;
20
31
  /**
21
32
  * The unknown-target error (3.0 name pass): a top-level `from <name>` whose name
22
- * is not one of the four collections would read an absent root and return an
33
+ * is not one of the five collections would read an absent root and return an
23
34
  * empty result — refused instead, naming the collections; `edges`, the links
24
35
  * target's name before 3.0, gets its new name spelled out.
25
36
  */
26
37
  export declare function unknownTargetMessage(name: string): string;
27
38
  /** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
28
- export declare function isTargetRelationOf(target: Target | undefined, name: string): boolean;
39
+ export declare function isTargetRelationOf(target: Root | undefined, name: string): boolean;
@@ -1,7 +1,7 @@
1
1
  // The root row (spec/surface §1.1, 2.0; spec/oqx 0.18). The repository is OQX's
2
2
  // root row: from a top-level row `^docs` reaches the documents (one caret per
3
3
  // enclosing block, or the absolute `0^docs`), `^id` the repository id, and the
4
- // root object (`^$it`) exposes the four collections. Two spellings are refused
4
+ // root object (`^$it`) exposes the five collections (`revisions` since 4.7). Two spellings are refused
5
5
  // as `filter_invalid`, with the messages below, in both engines:
6
6
  //
7
7
  // • `$repo` (removed in 2.0) anywhere — bare, as a member head (`$repo.docs`),
@@ -11,22 +11,30 @@
11
11
  // • a bare target name at scope depth ≥ 1 (inside a block, or in a top-level
12
12
  // row's clauses), where a bare name reads a property of the current row,
13
13
  // which has none: `docs` and `links` are no row's relation, so they are
14
- // refused statically by the same walk; `blocks` and `nodes` ARE relations
15
- // of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
16
- // `section.blocks`), so the context raises the same error at read time on a
17
- // row that lacks the relation (context.ts). A frontmatter key named after a
14
+ // refused statically by the same walk; `blocks`, `nodes` and `revisions`
15
+ // ARE relations of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
16
+ // `section.blocks`, `docs.revisions`), so the context raises the same error
17
+ // at read time on a row that lacks the relation (context.ts). A frontmatter key named after a
18
18
  // target is unreachable by bare name (use `frontmatter.<k>`).
19
+ /** The four row kinds whose names the unknown-names pass judges (names.ts keys
20
+ * its vocabulary tables by them); `revisions` joins when that pass learns the
21
+ * kind. Every OTHER root-collection test uses `ROOTS`. */
19
22
  export const TARGETS = new Set(["docs", "blocks", "nodes", "links"]);
20
- /** The target names that are a relation of each target's rows (§1.2). */
23
+ /** The root collections, in the root object's entry order (§1.1). */
24
+ export const ROOT_NAMES = ["docs", "blocks", "nodes", "links", "revisions"];
25
+ export const ROOTS = new Set(ROOT_NAMES);
26
+ /** The root names that are a relation of each kind's rows (§1.2): `revisions`
27
+ * on a document is its revision list (4.7). */
21
28
  export const TARGET_RELATIONS = {
22
- docs: new Set(["nodes", "blocks"]),
29
+ docs: new Set(["nodes", "blocks", "revisions"]),
23
30
  blocks: new Set(["nodes"]),
24
31
  nodes: new Set(["blocks"]),
25
32
  links: new Set(),
33
+ revisions: new Set(),
26
34
  };
27
35
  /** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`, and a link's `dst.x`). */
28
36
  export const REACH_THROUGH = { doc: "docs", block: "blocks", section: "nodes", dst: "docs" };
29
- const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", links: "links" };
37
+ const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", links: "links", revisions: "revisions" };
30
38
  export const REPO_REMOVED_MESSAGE = "`$repo` was removed in surface 2.0 — reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^id` / `0^id`";
31
39
  /**
32
40
  * The bare-target error. `depth` is the scope depth the name was read at when
@@ -50,13 +58,13 @@ export function pastRootMessage(name, levels) {
50
58
  }
51
59
  /**
52
60
  * The unknown-target error (3.0 name pass): a top-level `from <name>` whose name
53
- * is not one of the four collections would read an absent root and return an
61
+ * is not one of the five collections would read an absent root and return an
54
62
  * empty result — refused instead, naming the collections; `edges`, the links
55
63
  * target's name before 3.0, gets its new name spelled out.
56
64
  */
57
65
  export function unknownTargetMessage(name) {
58
66
  const was = name === "edges" ? " (`edges` was renamed `links` in surface 3.0)" : "";
59
- return `\`${name}\` is not a target — the repository's collections are \`docs\`, \`blocks\`, \`nodes\`, \`links\`${was}`;
67
+ return `\`${name}\` is not a target — the repository's collections are ${ROOT_NAMES.map((r) => `\`${r}\``).join(", ")}${was}`;
60
68
  }
61
69
  /** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
62
70
  export function isTargetRelationOf(target, name) {
@@ -1,4 +1,5 @@
1
1
  import type { Query, Expr, SelectItem } from "@omgbase/oqx";
2
+ import { type UnknownNames } from "./names.js";
2
3
  import type { Store } from "../core/store/store.js";
3
4
  import { FilterInvalid } from "../search/cel/parser.js";
4
5
  import type { SemanticVec } from "../search/cel/compile.js";
@@ -36,6 +37,12 @@ export interface OqxResult {
36
37
  * empty). Paged/deduped exactly like hits — `truncated`/`cursor` apply. For
37
38
  * first/single it holds zero or one value. */
38
39
  values?: unknown[];
40
+ /** Unknown names (spec/surface §1.5, 4.6): one line per name the query reads
41
+ * that is neither a field of its row kind nor a property any object of that
42
+ * kind in the repository carries — the rows are still returned. First-
43
+ * reference order, deduplicated; absent when there is none (never `[]`).
44
+ * Last member of the result, after everything above. */
45
+ hints?: string[];
39
46
  }
40
47
  export interface OqxOptions {
41
48
  limit?: number;
@@ -55,6 +62,13 @@ export interface OqxOptions {
55
62
  /** Force the pure in-memory engine (skip tier-3 pushdown). Internal — used by
56
63
  * the differential conformance suite to prove planned == in-memory. */
57
64
  plan?: boolean;
65
+ /** What a tier-3 name on an open kind does (spec/surface §1.5, 4.6; a tier-3
66
+ * name on `links` is `filter_invalid` either way): `"hint"` records it in
67
+ * `hints` and runs the query — the default of the source entry (`oqxRun`,
68
+ * every read); `"refuse"` throws `filter_invalid` before any row is read —
69
+ * the default of the AST entry (`oqxRunQuery`), whose caller is the write
70
+ * selector compiler, where a name no row carries would select every row. */
71
+ unknownNames?: UnknownNames;
58
72
  }
59
73
  export declare function rootPathLiterals(e: Expr): Expr;
60
74
  export declare function checkRootSpellings(q: Query): void;
@@ -68,7 +82,10 @@ export declare function oqxRun(store: Store, repoId: string, source: string, opt
68
82
  * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
69
83
  * its slot items to the RAW tree so a user alias a template reads is inlined
70
84
  * by the alias resolution here. Errors are the library's own (`OqxError`);
71
- * `oqxRun` normalizes them, a caller of this entry does the same.
85
+ * `oqxRun` normalizes them, a caller of this entry does the same. Unknown
86
+ * names (4.6) default to `"refuse"` here: this entry's caller is the write
87
+ * selector, where a name no row carries would select every row; the source
88
+ * entry passes `"hint"`.
72
89
  */
73
90
  export declare function oqxRunQuery(store: Store, repoId: string, raw: Query, opts?: OqxOptions): OqxResult;
74
91
  export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;