@omgbase/core 0.19.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/src/core/ids.d.ts +1 -1
  2. package/dist/src/core/ingest.d.ts +2 -0
  3. package/dist/src/core/ingest.js +13 -0
  4. package/dist/src/core/read/document.d.ts +13 -9
  5. package/dist/src/core/read/document.js +18 -8
  6. package/dist/src/core/store/archive.d.ts +196 -0
  7. package/dist/src/core/store/archive.js +606 -0
  8. package/dist/src/core/store/gc.d.ts +37 -4
  9. package/dist/src/core/store/gc.js +62 -28
  10. package/dist/src/core/store/rebuild.d.ts +2 -0
  11. package/dist/src/core/store/rebuild.js +8 -2
  12. package/dist/src/core/store/retention.d.ts +138 -0
  13. package/dist/src/core/store/retention.js +409 -0
  14. package/dist/src/core/store/schema.d.ts +3 -2
  15. package/dist/src/core/store/schema.js +30 -4
  16. package/dist/src/core/store/size.d.ts +24 -2
  17. package/dist/src/core/store/size.js +33 -3
  18. package/dist/src/core/store/store.d.ts +1 -0
  19. package/dist/src/core/store/store.js +53 -1
  20. package/dist/src/graph/history.d.ts +2 -0
  21. package/dist/src/graph/history.js +8 -4
  22. package/dist/src/index.d.ts +3 -1
  23. package/dist/src/index.js +3 -0
  24. package/dist/src/mcp/arguments.d.ts +10 -2
  25. package/dist/src/mcp/arguments.js +37 -3
  26. package/dist/src/mcp/errors.d.ts +1 -1
  27. package/dist/src/mcp/errors.js +4 -0
  28. package/dist/src/mcp/reference.d.ts +1 -1
  29. package/dist/src/mcp/reference.js +58 -8
  30. package/dist/src/mcp/server.js +51 -24
  31. package/dist/src/mutate/effects.d.ts +23 -5
  32. package/dist/src/mutate/effects.js +111 -22
  33. package/dist/src/mutate/known-ids.d.ts +19 -0
  34. package/dist/src/mutate/known-ids.js +47 -7
  35. package/dist/src/mutate/ops.d.ts +1 -0
  36. package/dist/src/mutate/ops.js +20 -1
  37. package/dist/src/mutate/select.d.ts +0 -11
  38. package/dist/src/mutate/select.js +33 -9
  39. package/dist/src/oqx-js/context.d.ts +9 -2
  40. package/dist/src/oqx-js/context.js +92 -30
  41. package/dist/src/oqx-js/dates.d.ts +105 -0
  42. package/dist/src/oqx-js/dates.js +427 -0
  43. package/dist/src/oqx-js/names.d.ts +40 -0
  44. package/dist/src/oqx-js/names.js +358 -0
  45. package/dist/src/oqx-js/planner.d.ts +3 -3
  46. package/dist/src/oqx-js/planner.js +30 -11
  47. package/dist/src/oqx-js/root-row.d.ts +15 -4
  48. package/dist/src/oqx-js/root-row.js +18 -10
  49. package/dist/src/oqx-js/run.d.ts +19 -1
  50. package/dist/src/oqx-js/run.js +97 -13
  51. package/dist/src/oqx-js/sql/translate.js +1 -1
  52. package/dist/src/spec-versions.d.ts +5 -5
  53. package/dist/src/spec-versions.js +4 -4
  54. package/dist/src/sync/observe.js +9 -3
  55. package/package.json +2 -2
@@ -0,0 +1,358 @@
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
+ // At a group scope `rows` yields the grouped rows (their kind); a key is a
169
+ // value the language built, not a row of any kind.
170
+ if (typeof cur === "object") {
171
+ if (seg !== "rows" || i !== 0)
172
+ return null;
173
+ cur = cur.group;
174
+ continue;
175
+ }
176
+ if (seg === "$it" && i === 0)
177
+ continue;
178
+ const next = HANDLES[cur][seg] ?? RELATIONS[cur][seg];
179
+ if (next === undefined)
180
+ return null;
181
+ cur = next;
182
+ }
183
+ return cur === "root" || typeof cur === "object" ? null : cur;
184
+ }
185
+ /** The kind of the frontier rows of a walk: the seeds' kind when every destination yields rows of that same kind, else unknown. */
186
+ function kindOfFrontier(seed, f, readDepth, byDepth) {
187
+ if (seed === null)
188
+ return null;
189
+ // A destination block is a value-position `op`: its receiver on the frontier row, then its own `from`.
190
+ for (const d of f.destinations)
191
+ if (kindOfExpr(d, seed, readDepth, byDepth) !== seed)
192
+ return null;
193
+ return seed;
194
+ }
195
+ /** 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. */
196
+ export function kindOfScope(steps) {
197
+ if (steps.length === 0)
198
+ return null; // the root scope: root names are judged elsewhere
199
+ const byDepth = new Map([[0, "root"]]);
200
+ let cur = "root";
201
+ let readDepth = 0;
202
+ for (const step of steps) {
203
+ const e = step.expr;
204
+ if (e.kind === "follow")
205
+ cur = kindOfFrontier(cur === "root" || typeof cur === "object" ? null : cur, e, readDepth, byDepth);
206
+ // A `group by` step (oqx 0.24): the rows at this depth become groups of them.
207
+ else if (e.kind === "groupBy")
208
+ cur = { group: cur === "root" || typeof cur === "object" ? null : cur };
209
+ else
210
+ cur = kindOfExpr(e, cur, readDepth, byDepth);
211
+ byDepth.set(step.depth, cur);
212
+ readDepth = step.depth;
213
+ }
214
+ return cur === "root" || typeof cur === "object" ? null : cur;
215
+ }
216
+ /** Judge a dotted name read on rows of `target`; null = known. */
217
+ function judge(store, repoId, target, path) {
218
+ const [head, ...rest] = path;
219
+ // An engine attribute wins on collision; what follows it (`attrs.checked`, `tags.size`) is the value's own navigation.
220
+ if (ATTRIBUTES[target].has(head))
221
+ return null;
222
+ // The source handles reach the authored data under them.
223
+ if (target === "docs" && SOURCE_HANDLES.has(head))
224
+ return rest.length === 0 ? null : judgeAuthored(store, repoId, "docs", rest, head);
225
+ const via = HANDLES[target][head] ?? RELATIONS[target][head];
226
+ if (via !== undefined)
227
+ return rest.length === 0 ? null : judge(store, repoId, via, rest);
228
+ // A bare target name on a row is the context's own loud error (root-row.ts); not doubled here.
229
+ if (ROOTS.has(head))
230
+ return null;
231
+ return judgeAuthored(store, repoId, target, path);
232
+ }
233
+ function judgeAuthored(store, repoId, target, path, handle) {
234
+ const name = path.join(".");
235
+ if (CLOSED.has(target))
236
+ return { target, name, closed: true };
237
+ const v = vocabularyOf(store, repoId, target);
238
+ if (!v.present)
239
+ return null; // no object of the kind: no vocabulary to judge by
240
+ if (v.keys.has(name))
241
+ return null;
242
+ return { target, name, closed: false, ...(handle !== undefined ? { handle } : {}) };
243
+ }
244
+ // ---- messages --------------------------------------------------------------
245
+ /** The names a kind knows, for the nearest match: attributes, handles, relations, the authored keys. */
246
+ function candidates(store, repoId, target) {
247
+ const out = [...ATTRIBUTES[target], ...Object.keys(HANDLES[target]), ...(target === "docs" ? [...SOURCE_HANDLES] : []), ...Object.keys(RELATIONS[target])];
248
+ if (!CLOSED.has(target))
249
+ out.push(...vocabularyOf(store, repoId, target).list);
250
+ return out;
251
+ }
252
+ /** Optimal string alignment distance (Damerau–Levenshtein with adjacent transpositions, each edit 1). */
253
+ export function editDistance(a, b) {
254
+ const s = Array.from(a), t = Array.from(b);
255
+ const m = s.length, n = t.length;
256
+ const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
257
+ for (let i = 0; i <= m; i++)
258
+ d[i][0] = i;
259
+ for (let j = 0; j <= n; j++)
260
+ d[0][j] = j;
261
+ for (let i = 1; i <= m; i++) {
262
+ for (let j = 1; j <= n; j++) {
263
+ const cost = s[i - 1] === t[j - 1] ? 0 : 1;
264
+ let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
265
+ if (i > 1 && j > 1 && s[i - 1] === t[j - 2] && s[i - 2] === t[j - 1])
266
+ v = Math.min(v, d[i - 2][j - 2] + 1);
267
+ d[i][j] = v;
268
+ }
269
+ }
270
+ return d[m][n];
271
+ }
272
+ /**
273
+ * The nearest match of `name` among `candidates` (§1.5): case-insensitive; a
274
+ * candidate qualifies when its distance is ≤ 2, or when one of the two is a
275
+ * substring of the other and the shorter has at least three characters; the
276
+ * lowest distance wins, ties broken by the shorter candidate, then
277
+ * alphabetically. Null when nothing qualifies.
278
+ */
279
+ export function nearest(name, candidates) {
280
+ const needle = name.toLowerCase();
281
+ // A name under three characters is within distance 2 of nearly anything, so
282
+ // only a substring match (which needs three characters anyway) could qualify.
283
+ if (needle.length < 3)
284
+ return null;
285
+ let best = null;
286
+ for (const c of new Set(candidates)) {
287
+ const hay = c.toLowerCase();
288
+ if (hay === needle)
289
+ continue;
290
+ const d = editDistance(needle, hay);
291
+ const shorter = needle.length <= hay.length ? needle : hay;
292
+ const longer = shorter === needle ? hay : needle;
293
+ const sub = shorter.length >= 3 && longer.includes(shorter);
294
+ if (!(d <= 2 || sub))
295
+ continue;
296
+ // One typo (distance 1) wins first; then a substring match outranks a
297
+ // distance-2 match (`via` → `via_id`, not `id`; `dst_pth` → `dst_path`, not
298
+ // `dst`); within a class the lowest distance, then the shorter candidate,
299
+ // then alphabetical.
300
+ const rank = (x) => (x.d === 1 ? 0 : x.sub ? 1 : 2);
301
+ const better = best === null ||
302
+ rank({ d, sub }) < rank(best) ||
303
+ (rank({ d, sub }) === rank(best) && (d < best.d || (d === best.d && (c.length < best.c.length || (c.length === best.c.length && c < best.c)))));
304
+ if (better)
305
+ best = { c, d, sub };
306
+ }
307
+ return best === null ? null : best.c;
308
+ }
309
+ function suggestion(name, candidates) {
310
+ const c = nearest(name, candidates);
311
+ return c === null ? "" : `; did you mean \`${c}\`?`;
312
+ }
313
+ /** The error of a closed kind: `` `target` is not a field of links (fields: id, path, …); did you mean `…`? ``. */
314
+ export function fieldErrorMessage(store, repoId, target, name) {
315
+ return `\`${name}\` is not a field of ${target} (fields: ${[...ATTRIBUTES[target]].join(", ")})${suggestion(name, candidates(store, repoId, target))}`;
316
+ }
317
+ /** 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 `…`? ``.
318
+ * Under a source handle the name is an authored lookup by construction, so the
319
+ * hint says only that: `` no document in this repository carries a property `path` (`data.path`); did you mean `…`? ``,
320
+ * the nearest match drawn from the authored keys alone. */
321
+ export function propertyHintMessage(store, repoId, target, name, handle) {
322
+ if (handle !== undefined) {
323
+ return `no ${NOUN[target]} in this repository carries a property \`${name}\` (\`${handle}.${name}\`)${suggestion(name, vocabularyOf(store, repoId, target).list)}`;
324
+ }
325
+ 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))}`;
326
+ }
327
+ // ---- the pass --------------------------------------------------------------
328
+ /**
329
+ * Judge every name the query reads. Returns the hints (one per offending name
330
+ * of an open kind, first-reference order, deduplicated) under `"hint"`; throws
331
+ * `FilterInvalid` for a tier-3 name on a closed kind in either mode, and for
332
+ * one on an open kind under `"refuse"`.
333
+ */
334
+ export function checkNames(store, repoId, raw, mode) {
335
+ const hints = [];
336
+ const seen = new Set();
337
+ for (const ref of references(raw)) {
338
+ if (ref.scope.steps.length === 0)
339
+ continue; // a root name
340
+ const kind = kindOfScope(ref.scope.steps);
341
+ if (kind === null)
342
+ continue;
343
+ const verdict = judge(store, repoId, kind, ref.name.split("."));
344
+ if (verdict === null)
345
+ continue;
346
+ if (verdict.closed)
347
+ throw new FilterInvalid(fieldErrorMessage(store, repoId, verdict.target, verdict.name), "OQX");
348
+ const message = propertyHintMessage(store, repoId, verdict.target, verdict.name, verdict.handle);
349
+ if (mode === "refuse")
350
+ throw new FilterInvalid(message, "OQX");
351
+ if (!seen.has(message)) {
352
+ seen.add(message);
353
+ hints.push(message);
354
+ }
355
+ }
356
+ return hints;
357
+ }
358
+ //# 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`)
@@ -90,6 +91,12 @@ function subMayRaise(s, target) {
90
91
  return true;
91
92
  if (s.follow && followMayRaise(s.follow, target))
92
93
  return true;
94
+ // A grouped block (oqx 0.24) evaluates its keys, `having` and its aggregates
95
+ // (`sum` over a string raises) per group.
96
+ if (s.groupBy?.keys.some((k) => exprMayRaise(k.expr, target, false)))
97
+ return true;
98
+ if (s.having && whereMayRaise(s.having, target, false))
99
+ return true;
93
100
  if (s.limit && exprMayRaise(s.limit, target, false))
94
101
  return true;
95
102
  if (s.offset && exprMayRaise(s.offset, target, false))
@@ -107,9 +114,9 @@ function exprMayRaise(e, target, root) {
107
114
  case "call": return true;
108
115
  case "outer": return true;
109
116
  case "ident":
110
- return TARGETS.has(e.name) && (!root || !isTargetRelationOf(target, e.name));
117
+ return ROOTS.has(e.name) && (!root || !isTargetRelationOf(target, e.name));
111
118
  case "member":
112
- if (TARGETS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
119
+ if (ROOTS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
113
120
  return true;
114
121
  return exprMayRaise(e.recv, target, root);
115
122
  case "unary": return exprMayRaise(e.expr, target, root);
@@ -117,7 +124,10 @@ function exprMayRaise(e, target, root) {
117
124
  // read anything its block reads.
118
125
  case "required": return true;
119
126
  case "op": return opMayRaise(e, target, root);
127
+ // A regex operator (oqx 0.22) is a `matches` call: an invalid pattern
128
+ // raises, exactly as the call would.
120
129
  case "binary":
130
+ return isRegexOp(e.op) || exprMayRaise(e.left, target, root) || exprMayRaise(e.right, target, root);
121
131
  case "logical":
122
132
  case "in":
123
133
  return exprMayRaise(e.left, target, root) || exprMayRaise(e.right, target, root);
@@ -129,6 +139,9 @@ function exprMayRaise(e, target, root) {
129
139
  // a record is an eval error only the data decides, so with any interpolation
130
140
  // it may raise.
131
141
  case "template": return e.parts.some((p) => typeof p !== "string");
142
+ // An aggregate (oqx 0.24) raises on a non-number under `sum`/`avg`, which
143
+ // only the data decides; a grouped query is never planned anyway (below).
144
+ case "aggregate": return true;
132
145
  case "lit":
133
146
  case "binding": return false;
134
147
  }
@@ -144,8 +157,14 @@ export class SQLiteQueryPlanner {
144
157
  this.ctxOpts = ctxOpts;
145
158
  }
146
159
  plan(query, params) {
147
- // Only simple top-level scans: no follow, no `from E` re-projection.
148
- if (query.follow || query.from.length > 0)
160
+ // Only simple top-level scans: no follow, no `from E` re-projection, no
161
+ // `group by` (oqx 0.24). A grouped query is declined whole: its `where`
162
+ // conjuncts could be pushed (grouping happens after them, so the residual
163
+ // would group the produced rows correctly), but a pushed conjunct that
164
+ // empties the scan would hide an eval error an aggregate or `having` raises
165
+ // in memory (`sum` over a string) — decline (c) extended to the post-group
166
+ // clauses. SQL `GROUP BY` pushdown is a later round.
167
+ if (query.follow || query.from.length > 0 || query.groupBy)
149
168
  return null;
150
169
  const target = rootTarget(query.source);
151
170
  if (!target)
@@ -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,9 +62,17 @@ 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;
75
+ export declare function foldDates(e: Expr): Expr;
61
76
  export declare function collectSemanticPhrases(source: string): string[];
62
77
  export declare function reservedAlias(items: readonly SelectItem[]): FilterInvalid | null;
63
78
  export declare const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` \u2014 without a projection a hit is only its envelope";
@@ -68,7 +83,10 @@ export declare function oqxRun(store: Store, repoId: string, source: string, opt
68
83
  * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
69
84
  * its slot items to the RAW tree so a user alias a template reads is inlined
70
85
  * by the alias resolution here. Errors are the library's own (`OqxError`);
71
- * `oqxRun` normalizes them, a caller of this entry does the same.
86
+ * `oqxRun` normalizes them, a caller of this entry does the same. Unknown
87
+ * names (4.6) default to `"refuse"` here: this entry's caller is the write
88
+ * selector, where a name no row carries would select every row; the source
89
+ * entry passes `"hint"`.
72
90
  */
73
91
  export declare function oqxRunQuery(store: Store, repoId: string, raw: Query, opts?: OqxOptions): OqxResult;
74
92
  export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;