@omgbase/core 0.15.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 (47) 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 +79 -0
  8. package/dist/src/core/store/size.js +232 -0
  9. package/dist/src/index.d.ts +4 -1
  10. package/dist/src/index.js +5 -1
  11. package/dist/src/mcp/arguments.d.ts +10 -2
  12. package/dist/src/mcp/arguments.js +37 -3
  13. package/dist/src/mcp/errors.d.ts +1 -1
  14. package/dist/src/mcp/reference.d.ts +1 -1
  15. package/dist/src/mcp/reference.js +63 -10
  16. package/dist/src/mcp/server.d.ts +34 -1
  17. package/dist/src/mcp/server.js +158 -46
  18. package/dist/src/mutate/canonical.d.ts +27 -0
  19. package/dist/src/mutate/canonical.js +138 -0
  20. package/dist/src/mutate/effects.d.ts +58 -8
  21. package/dist/src/mutate/effects.js +312 -43
  22. package/dist/src/mutate/known-ids.d.ts +19 -0
  23. package/dist/src/mutate/known-ids.js +47 -7
  24. package/dist/src/mutate/ops.d.ts +1 -0
  25. package/dist/src/mutate/ops.js +20 -1
  26. package/dist/src/mutate/plan.d.ts +26 -16
  27. package/dist/src/mutate/plan.js +152 -112
  28. package/dist/src/mutate/select.d.ts +78 -0
  29. package/dist/src/mutate/select.js +903 -0
  30. package/dist/src/mutate/tree.d.ts +2 -2
  31. package/dist/src/oqx-js/context.d.ts +9 -2
  32. package/dist/src/oqx-js/context.js +74 -28
  33. package/dist/src/oqx-js/names.d.ts +40 -0
  34. package/dist/src/oqx-js/names.js +347 -0
  35. package/dist/src/oqx-js/planner.d.ts +3 -3
  36. package/dist/src/oqx-js/planner.js +17 -9
  37. package/dist/src/oqx-js/root-row.d.ts +15 -4
  38. package/dist/src/oqx-js/root-row.js +18 -10
  39. package/dist/src/oqx-js/run.d.ts +45 -1
  40. package/dist/src/oqx-js/run.js +114 -32
  41. package/dist/src/oqx-js/sql/translate.js +1 -1
  42. package/dist/src/spec-versions.d.ts +5 -5
  43. package/dist/src/spec-versions.js +4 -4
  44. package/dist/src/sync/observe.js +9 -3
  45. package/dist/src/version.d.ts +7 -2
  46. package/dist/src/version.js +5 -2
  47. package/package.json +2 -2
@@ -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);
@@ -125,6 +129,10 @@ function exprMayRaise(e, target, root) {
125
129
  return (e.lo !== null && exprMayRaise(e.lo, target, root)) || (e.hi !== null && exprMayRaise(e.hi, target, root));
126
130
  // A tuple (oqx 0.20) is never pushed down; it raises iff an element does.
127
131
  case "tuple": return e.items.some((x) => exprMayRaise(x, target, root));
132
+ // A template literal (oqx 0.21) is never pushed down; interpolating a list or
133
+ // a record is an eval error only the data decides, so with any interpolation
134
+ // it may raise.
135
+ case "template": return e.parts.some((p) => typeof p !== "string");
128
136
  case "lit":
129
137
  case "binding": return false;
130
138
  }
@@ -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,5 +1,7 @@
1
- import type { Query, Expr } from "@omgbase/oqx";
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";
4
+ import { FilterInvalid } from "../search/cel/parser.js";
3
5
  import type { SemanticVec } from "../search/cel/compile.js";
4
6
  export type OqxConsumer = "collect" | "count" | "exists" | "none" | "first" | "single";
5
7
  export type EmbedQuery = (text: string) => Promise<{
@@ -18,6 +20,10 @@ export interface OqxHit {
18
20
  [k: string]: unknown;
19
21
  }
20
22
  export interface OqxResult {
23
+ /** `{ id, path, ...projection }` per hit — or, under `OqxOptions.bare`, the
24
+ * projection alone (`id`/`path` present only when the query projected them;
25
+ * typed as `OqxHit` for the one hit shape every consumer handles, a bare
26
+ * caller knows it asked). */
21
27
  hits: OqxHit[];
22
28
  truncated: boolean;
23
29
  cursor: string | null;
@@ -31,19 +37,57 @@ export interface OqxResult {
31
37
  * empty). Paged/deduped exactly like hits — `truncated`/`cursor` apply. For
32
38
  * first/single it holds zero or one value. */
33
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[];
34
46
  }
35
47
  export interface OqxOptions {
36
48
  limit?: number;
37
49
  cursor?: string | null;
50
+ /** Return each hit as its PROJECTION only — no automatic `id`/`path`
51
+ * members (spec/surface §1.4, 4.2): `select title, era from docs` yields
52
+ * `{ title, era }` rows. Needs a `select` (without a projection a hit is
53
+ * only its envelope) and is refused with `values` (already the bare-values
54
+ * form); both are `filter_invalid` before the engine runs. A projected
55
+ * `id`/`path` is the intrinsic as everywhere (`select id` → `{ id }`), and
56
+ * with no envelope there is nothing to collide with (`select path: layer`
57
+ * is fine). Paging, `distinct`, `truncated` and `cursor` are unchanged —
58
+ * the runner keeps identity internally and strips the envelope last. The
59
+ * scalar consumers (`count`/`exists`/`none`) have no hits and ignore it. */
60
+ bare?: boolean;
38
61
  semanticVectors?: Map<string, SemanticVec>;
39
62
  /** Force the pure in-memory engine (skip tier-3 pushdown). Internal — used by
40
63
  * the differential conformance suite to prove planned == in-memory. */
41
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;
42
72
  }
43
73
  export declare function rootPathLiterals(e: Expr): Expr;
44
74
  export declare function checkRootSpellings(q: Query): void;
45
75
  export declare function collectSemanticPhrases(source: string): string[];
76
+ export declare function reservedAlias(items: readonly SelectItem[]): FilterInvalid | null;
77
+ export declare const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` \u2014 without a projection a hit is only its envelope";
78
+ export declare const BARE_WITH_VALUES_MESSAGE = "`bare` is redundant with `values` \u2014 `values` is already the bare-values form; drop one";
46
79
  export declare function oqxRun(store: Store, repoId: string, source: string, opts?: OqxOptions): OqxResult;
80
+ /**
81
+ * The AST entry (4.1): run an already parsed query — the source path above
82
+ * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
83
+ * its slot items to the RAW tree so a user alias a template reads is inlined
84
+ * by the alias resolution here. Errors are the library's own (`OqxError`);
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"`.
89
+ */
90
+ export declare function oqxRunQuery(store: Store, repoId: string, raw: Query, opts?: OqxOptions): OqxResult;
47
91
  export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;
48
92
  /** Parse an OQX source into the `@omgbase/oqx` AST (re-exported for callers). */
49
93
  export declare function parseOqx(source: string): Query;
@@ -6,8 +6,9 @@
6
6
  // in-tree compiler, so every caller and the corpus are unchanged.
7
7
  import { parse, PlannedEngine, InMemoryEngine, OqxError, resolveAliases, transform, visit, build, semantics } from "@omgbase/oqx";
8
8
  import { makeStoreContext, rowRef } from "./context.js";
9
- import { SQLiteQueryPlanner, rootTarget } from "./planner.js";
10
- import { REPO_REMOVED_MESSAGE, TARGETS, bareTargetMessage, pastRootMessage, unknownTargetMessage } from "./root-row.js";
9
+ import { SQLiteQueryPlanner } from "./planner.js";
10
+ import { REPO_REMOVED_MESSAGE, ROOTS, bareTargetMessage, pastRootMessage, unknownTargetMessage } from "./root-row.js";
11
+ import { checkNames } from "./names.js";
11
12
  import { FilterInvalid } from "../search/cel/parser.js";
12
13
  import { float32ToBlob } from "../core/vec.js";
13
14
  import { encodeCursor as encodeKeyset, decodePathCursor } from "../core/cursor.js";
@@ -72,8 +73,8 @@ export function rootPathLiterals(e) {
72
73
  export function checkRootSpellings(q) {
73
74
  // A top-level `from <name>` that is no collection (`from edges`, the links
74
75
  // target's name before 3.0) would scan an absent root and come back empty:
75
- // refused, naming the four collections (root-row.ts).
76
- if (q.source.kind === "ident" && !TARGETS.has(q.source.name))
76
+ // refused, naming the five collections (root-row.ts).
77
+ if (q.source.kind === "ident" && !ROOTS.has(q.source.name))
77
78
  throw new FilterInvalid(unknownTargetMessage(q.source.name), "OQX");
78
79
  visit(q, {
79
80
  enter(node, ctx) {
@@ -84,7 +85,7 @@ export function checkRootSpellings(q) {
84
85
  if (node.kind === "ident" && ctx.depth >= 1 && (node.name === "docs" || node.name === "links")) {
85
86
  throw new FilterInvalid(bareTargetMessage(node.name, ctx.depth), "OQX");
86
87
  }
87
- if (node.kind === "outer" && TARGETS.has(node.name) && node.levels > ctx.depth) {
88
+ if (node.kind === "outer" && ROOTS.has(node.name) && node.levels > ctx.depth) {
88
89
  throw new FilterInvalid(pastRootMessage(node.name, node.levels), "OQX");
89
90
  }
90
91
  },
@@ -128,6 +129,21 @@ export function collectSemanticPhrases(source) {
128
129
  }
129
130
  const ID_ITEM = build.field("__oqx_id", build.ident("id"));
130
131
  const PATH_ITEM = build.field("__oqx_path", build.ident("path"));
132
+ // The runner's own projection names (`__oqx_id`, `__oqx_path`, `__oqx_self`,
133
+ // `__oqx_value`) and the selector compiler's slots (`__omg_sel_<t>_<slot>`,
134
+ // mutate/select.ts) share two reserved prefixes. A user alias under either would
135
+ // shadow an injected item (`select __oqx_id: 1` overwrote the hit's `id` through
136
+ // 4.0) or collide with a slot, so it is refused before anything runs — over the
137
+ // query's own top-level items, the only ones the runner injects beside.
138
+ const RESERVED_ALIAS_PREFIXES = ["__oqx_", "__omg_"];
139
+ export function reservedAlias(items) {
140
+ for (const item of items) {
141
+ const prefix = RESERVED_ALIAS_PREFIXES.find((p) => item.name.startsWith(p));
142
+ if (prefix !== undefined)
143
+ return new FilterInvalid(`projection \`${item.name}\` uses the reserved prefix \`${prefix}\` — choose another alias`, "OQX");
144
+ }
145
+ return null;
146
+ }
131
147
  // Result projections may not overwrite the envelope (spec/surface §1.4, 3.0):
132
148
  // a projection whose output key is `id` or `path` is allowed only when it IS
133
149
  // the envelope's own selection — the bare attribute of the same name, which
@@ -150,8 +166,9 @@ function envelopeCollision(items) {
150
166
  return null;
151
167
  }
152
168
  // A hit is a store row (spec/surface §1.4, 1.5). The engine's top-level rows
153
- // are store rows by construction when the source is a bare root scan and
154
- // nothing re-projects them; a `follow` destination (`follow before` over a
169
+ // are store rows by construction when the source is a bare root scan (one of
170
+ // the five collections, `revisions` included — the planner's `rootTarget` is
171
+ // narrower: the four it can plan) and nothing re-projects them; a `follow` destination (`follow before` over a
155
172
  // frontmatter list of paths), a `from E` re-projection or any other source can
156
173
  // reach a scalar, which the injected `id`/`path` reads would render as the
157
174
  // junk hit `{ id: "undefined", path: "" }`. For those queries the row itself is
@@ -162,7 +179,7 @@ function envelopeCollision(items) {
162
179
  const SELF_KEY = "__oqx_self";
163
180
  const SELF_ITEM = build.field(SELF_KEY, build.ident("$it"));
164
181
  function mayReachNonRows(q) {
165
- return q.follow !== null || q.from.length > 0 || rootTarget(q.source) === null;
182
+ return q.follow !== null || q.from.length > 0 || !(q.source.kind === "ident" && ROOTS.has(q.source.name));
166
183
  }
167
184
  function describeValue(v) {
168
185
  if (v === null || v === undefined)
@@ -180,7 +197,7 @@ function describeValue(v) {
180
197
  return "an object";
181
198
  }
182
199
  function notAStoreRow(v) {
183
- return new FilterInvalid(`a hit must be a document, block, node or link row — the query reached ${describeValue(v)}; to follow document references held in a property use refs(<field>)`, "OQX");
200
+ return new FilterInvalid(`a hit must be a document, block, node, link or revision row — the query reached ${describeValue(v)}; to follow document references held in a property use refs(<field>)`, "OQX");
184
201
  }
185
202
  // The reserved key a top-level `values` projection's single item is renamed to,
186
203
  // so it rides through id/path injection, keyset paging, and distinct as an
@@ -213,33 +230,56 @@ function renderValue(v) {
213
230
  out[k] = renderValue(x);
214
231
  return out;
215
232
  }
216
- function toHit(row) {
233
+ function shapeRow(row) {
217
234
  const o = row;
218
235
  const { __oqx_id, __oqx_path, __oqx_self, ...rest } = o;
219
236
  if (SELF_KEY in o && !rowRef(__oqx_self))
220
237
  throw notAStoreRow(__oqx_self);
221
- // The envelope keys lead; a user projection of `id`/`path` collapses onto
222
- // them in place (the same value, read by the same attribute).
223
- return { id: String(__oqx_id), path: String(__oqx_path ?? ""), ...renderValue(rest) };
238
+ return { id: String(__oqx_id), path: String(__oqx_path ?? ""), proj: renderValue(rest) };
224
239
  }
225
- // Top-level `select distinct`: dedup hits by their USER projection (every field
226
- // except the envelope's `id`/`path`), keeping the first (so the retained hit
227
- // carries a real identity). Order-preserving.
228
- function dedupHitsByProjection(hits) {
240
+ // The envelope keys lead; a user projection of `id`/`path` collapses onto
241
+ // them in place (the same value, read by the same attribute).
242
+ function toHit(s) {
243
+ return { id: s.id, path: s.path, ...s.proj };
244
+ }
245
+ // `bare` (4.2): the projection alone. A projected `id`/`path` is an ordinary
246
+ // column here — the intrinsic the attribute reads, under the key the query
247
+ // gave it — and nothing is injected beside it.
248
+ function toBareHit(s) {
249
+ return s.proj;
250
+ }
251
+ // Top-level `select distinct`: dedup hits by their USER projection — every
252
+ // field except the envelope's `id`/`path`, which a projection of the attribute
253
+ // collapses onto (so `select distinct id` dedups on nothing but the empty
254
+ // projection, as it always has); under `bare` there is no envelope, so a
255
+ // projected `id`/`path` IS part of the projection and counts. Keeps the first
256
+ // (so the retained hit carries a real identity). Order-preserving.
257
+ function dedupByProjection(rows, bare) {
229
258
  const seen = new Set();
230
259
  const out = [];
231
- for (const h of hits) {
232
- const { id: _id, path: _path, ...proj } = h;
233
- void _id;
234
- void _path;
235
- const key = JSON.stringify(Object.keys(proj).sort().map((k) => [k, proj[k]]));
260
+ for (const s of rows) {
261
+ const keys = Object.keys(s.proj).filter((k) => bare || !ENVELOPE_KEYS.has(k)).sort();
262
+ const key = JSON.stringify(keys.map((k) => [k, s.proj[k]]));
236
263
  if (seen.has(key))
237
264
  continue;
238
265
  seen.add(key);
239
- out.push(h);
266
+ out.push(s);
240
267
  }
241
268
  return out;
242
269
  }
270
+ // `bare` is examined where hits are built (spec/surface §1.4, 4.2): without a
271
+ // projection a hit is only its envelope, and `values` is already the
272
+ // bare-values form — both refused before the engine runs, over the resolved
273
+ // query, so the error never depends on the rows.
274
+ export const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` — without a projection a hit is only its envelope";
275
+ export const BARE_WITH_VALUES_MESSAGE = "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
276
+ function bareMisuse(parsed) {
277
+ if (parsed.values)
278
+ return new FilterInvalid(BARE_WITH_VALUES_MESSAGE, "OQX");
279
+ if (parsed.select.length === 0)
280
+ return new FilterInvalid(BARE_NEEDS_SELECT_MESSAGE, "OQX");
281
+ return null;
282
+ }
243
283
  export function oqxRun(store, repoId, source, opts = {}) {
244
284
  try {
245
285
  return oqxRunInner(store, repoId, source, opts);
@@ -252,10 +292,40 @@ export function oqxRun(store, repoId, source, opts = {}) {
252
292
  }
253
293
  }
254
294
  function oqxRunInner(store, repoId, source, opts) {
255
- // The query's `select` aliases are resolved HERE, once, before the runner
256
- // renames/injects items (an engine evaluates the query it is given).
257
295
  const raw = parse(source);
296
+ const reserved = reservedAlias(raw.select);
297
+ if (reserved)
298
+ throw reserved;
299
+ // The source entry is every read: a tier-3 name on an open kind is a hint.
300
+ return oqxRunQuery(store, repoId, raw, { unknownNames: "hint", ...opts });
301
+ }
302
+ /**
303
+ * The AST entry (4.1): run an already parsed query — the source path above
304
+ * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
305
+ * its slot items to the RAW tree so a user alias a template reads is inlined
306
+ * by the alias resolution here. Errors are the library's own (`OqxError`);
307
+ * `oqxRun` normalizes them, a caller of this entry does the same. Unknown
308
+ * names (4.6) default to `"refuse"` here: this entry's caller is the write
309
+ * selector, where a name no row carries would select every row; the source
310
+ * entry passes `"hint"`.
311
+ */
312
+ export function oqxRunQuery(store, repoId, raw, opts = {}) {
258
313
  checkRootSpellings(raw);
314
+ // Unknown names (spec/surface §1.5): judged over the RAW tree — before alias
315
+ // resolution and the runner's own items — so an alias is never mistaken for
316
+ // a row property and the verdict never depends on the rows. The selector
317
+ // compiler's slot items (`__omg_sel_*`, the templates' expressions) are not
318
+ // the selector: an expression reading an absent property there is
319
+ // spec/mutate's `target_missing` / absent value, not an unknown name.
320
+ const own = { ...raw, select: raw.select.filter((it) => !RESERVED_ALIAS_PREFIXES.some((p) => it.name.startsWith(p))) };
321
+ const hints = checkNames(store, repoId, own, opts.unknownNames ?? "refuse");
322
+ const result = runResolved(store, repoId, raw, opts);
323
+ // `hints` is the last member, present only when there is something to say.
324
+ return hints.length === 0 ? result : { ...result, hints };
325
+ }
326
+ function runResolved(store, repoId, raw, opts) {
327
+ // The query's `select` aliases are resolved HERE, once, before the runner
328
+ // renames/injects items (an engine evaluates the query it is given).
259
329
  const parsed = rewriteQuery(resolveAliases(raw));
260
330
  const consumer = parsed.consumer;
261
331
  const ctxOpts = opts.semanticVectors ? { semanticVectors: opts.semanticVectors } : {};
@@ -289,11 +359,21 @@ function oqxRunInner(store, repoId, source, opts) {
289
359
  // the final page below and returned as `values` with `hits` empty.
290
360
  const topValues = parsed.values;
291
361
  const userSelect = topValues ? [{ ...parsed.select[0], name: VALUE_KEY }] : parsed.select;
292
- if (!topValues) {
362
+ // `bare` (4.2): the hits are the projection alone — refused without one and
363
+ // with `values`; with no envelope there is no collision to refuse either, a
364
+ // projected `id`/`path` is simply a column.
365
+ const bare = opts.bare === true;
366
+ if (bare) {
367
+ const misuse = bareMisuse(parsed);
368
+ if (misuse)
369
+ throw misuse;
370
+ }
371
+ else if (!topValues) {
293
372
  const collision = envelopeCollision(userSelect);
294
373
  if (collision)
295
374
  throw collision;
296
375
  }
376
+ const render = bare ? toBareHit : toHit;
297
377
  // On the collect path a top-level `limit`/`offset` is ALSO taken out of the
298
378
  // engine query and applied here, after the runner's own distinct — the engine
299
379
  // would otherwise bound the raw rows before dedup (`select distinct type
@@ -307,13 +387,13 @@ function oqxRunInner(store, repoId, source, opts) {
307
387
  if (consumer === "first" || consumer === "single") {
308
388
  const row = res.consumer === "first" || res.consumer === "single" ? res.row : null;
309
389
  if (topValues)
310
- return { hits: [], truncated: false, cursor: null, consumer, values: row == null ? [] : [toHit(row)[VALUE_KEY]] };
311
- return { hits: row == null ? [] : [toHit(row)], truncated: false, cursor: null, consumer };
390
+ return { hits: [], truncated: false, cursor: null, consumer, values: row == null ? [] : [shapeRow(row).proj[VALUE_KEY]] };
391
+ return { hits: row == null ? [] : [render(shapeRow(row))], truncated: false, cursor: null, consumer };
312
392
  }
313
393
  // collect: keyset pagination on (path, id) when the order is the default.
314
- let rows = (res.consumer === "collect" ? res.rows : []).map(toHit);
394
+ let rows = (res.consumer === "collect" ? res.rows : []).map(shapeRow);
315
395
  if (topDistinct)
316
- rows = dedupHitsByProjection(rows);
396
+ rows = dedupByProjection(rows, bare);
317
397
  // The query's own bound defines the result SET; the `limit`/`cursor` options
318
398
  // then page within it.
319
399
  const offset = constBound(topOffset, "offset") ?? 0;
@@ -331,9 +411,11 @@ function oqxRunInner(store, repoId, source, opts) {
331
411
  page = page.slice(0, cap);
332
412
  const last = page[page.length - 1];
333
413
  const cursor = truncated && last && !custom ? encodeCursor(last.path, last.id) : null;
414
+ // The envelope is stripped LAST (`bare`): the page and the cursor above ran
415
+ // on identity, so a bare result pages exactly like the enveloped one.
334
416
  if (topValues)
335
- return { hits: [], truncated, cursor, consumer: "collect", values: page.map((h) => h[VALUE_KEY]) };
336
- return { hits: page, truncated, cursor, consumer: "collect" };
417
+ return { hits: [], truncated, cursor, consumer: "collect", values: page.map((s) => s.proj[VALUE_KEY]) };
418
+ return { hits: page.map(render), truncated, cursor, consumer: "collect" };
337
419
  }
338
420
  export async function oqxRunAsync(store, repoId, source, opts = {}, embedQuery) {
339
421
  const phrases = collectSemanticPhrases(source);
@@ -53,7 +53,7 @@ import { TARGETS } from "../root-row.js";
53
53
  * `translate.test.ts` asserts this set against the store context.
54
54
  */
55
55
  export const NON_PROPERTY_NAMES = {
56
- docs: new Set(["doc", "nodes", "blocks", "out_links", "in_links", "data", "frontmatter", "inline"]),
56
+ docs: new Set(["doc", "nodes", "blocks", "out_links", "in_links", "revisions", "data", "frontmatter", "inline"]),
57
57
  blocks: new Set(["block", "doc", "children", "nodes", "out_links", "section", "attrs"]),
58
58
  nodes: new Set(["section", "doc", "block", "blocks", "subsections", "children", "attrs"]),
59
59
  links: new Set(["doc", "dst"]),
@@ -1,14 +1,14 @@
1
1
  export declare const SPEC_VERSIONS: {
2
- readonly oqx: "0.20";
2
+ readonly oqx: "0.23";
3
3
  readonly format: "0.2";
4
4
  readonly reconcile: "2.3";
5
- readonly store: "14.0";
5
+ readonly store: "14.2";
6
6
  readonly properties: "1.1";
7
7
  readonly graph: "1.1";
8
8
  readonly search: "2.0";
9
- readonly mutate: "3.0";
9
+ readonly mutate: "3.5";
10
10
  readonly sync: "2.2";
11
- readonly surface: "4.0";
12
- readonly cli: "4.0";
11
+ readonly surface: "4.7";
12
+ readonly cli: "4.5";
13
13
  };
14
14
  export type SpecName = keyof typeof SPEC_VERSIONS;
@@ -13,13 +13,13 @@ export const SPEC_VERSIONS = {
13
13
  oqx: LANGUAGE_VERSION,
14
14
  format: "0.2",
15
15
  reconcile: "2.3",
16
- store: "14.0",
16
+ store: "14.2",
17
17
  properties: "1.1",
18
18
  graph: "1.1",
19
19
  search: "2.0",
20
- mutate: "3.0",
20
+ mutate: "3.5",
21
21
  sync: "2.2",
22
- surface: "4.0",
23
- cli: "4.0",
22
+ surface: "4.7",
23
+ cli: "4.5",
24
24
  };
25
25
  //# sourceMappingURL=spec-versions.js.map
@@ -189,9 +189,15 @@ function commitPrepared(store, repoId, prepared, ts, oldHashHex, newHashHex) {
189
189
  const conflicted = hasConflictMarkers(content);
190
190
  const resolveIds = makeReconcilingResolver(store, repoId, { ts, path, prepared });
191
191
  const res = ingestFile(store, repoId, path, content, { ts, origin: "observed", resolveIds, parsed: prepared.tree, conflicted });
192
- const rows = store.db
193
- .prepare("SELECT kind, count(*) n FROM dispositions WHERE commit_id = ? GROUP BY kind ORDER BY kind")
194
- .all(res.commitId);
192
+ // The outcome counts every verdict the matcher produced, `same` included —
193
+ // it describes what the matcher saw; the `dispositions` table describes
194
+ // what changed (spec/store §5.4 step 10, 14.2: `same` is not persisted), so
195
+ // the counts come from the in-memory result, not the table. Kinds in
196
+ // bytewise order, as the table query listed them through 14.1.
197
+ const counts = new Map();
198
+ for (const d of prepared.result.dispositions)
199
+ counts.set(d.kind, (counts.get(d.kind) ?? 0) + 1);
200
+ const rows = [...counts].map(([kind, n]) => ({ kind, n })).sort((a, b) => (a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0));
195
201
  return {
196
202
  docId: res.docId,
197
203
  path,
@@ -1,5 +1,6 @@
1
1
  import type { Store } from "./core/store/store.js";
2
2
  import { SPEC_VERSIONS } from "./spec-versions.js";
3
+ import type { ToolsProfile } from "./mcp/server.js";
3
4
  export interface HostInfo {
4
5
  /** the serving binary's own version — `omgbase`'s package.json (what `--version` prints) */
5
6
  version: string;
@@ -25,6 +26,8 @@ export interface VersionInfo {
25
26
  runtime: string;
26
27
  commit: string | null;
27
28
  built: string | null;
29
+ /** the catalog profile the server serves (spec/surface §4 "Profiles", 4.3): `omg version` itself reports `full` */
30
+ profile: ToolsProfile;
28
31
  }
29
32
  /** The omgbase packages the reference binary is built from (spec/surface §4). */
30
33
  export declare const COMPONENT_PACKAGES: readonly ["omgbase", "@omgbase/core", "@omgbase/oqx", "@omgbase/sync", "@omgbase/fs-adapter"];
@@ -34,6 +37,8 @@ export declare function nearestPackageVersion(fromUrl: string, name: string): st
34
37
  export declare function coreVersion(): string;
35
38
  /**
36
39
  * The `version` result. `store` supplies `schema`; `host` the binary's own
37
- * version, build info and resolution anchor (see the module comment).
40
+ * version, build info and resolution anchor (see the module comment);
41
+ * `profile` the catalog profile served (the MCP server's `--tools`; the CLI's
42
+ * own `version` verb is the full library and says `full`).
38
43
  */
39
- export declare function versionInfo(store?: Store | null, host?: HostInfo): VersionInfo;
44
+ export declare function versionInfo(store?: Store | null, host?: HostInfo, profile?: ToolsProfile): VersionInfo;
@@ -73,9 +73,11 @@ export function coreVersion() {
73
73
  }
74
74
  /**
75
75
  * The `version` result. `store` supplies `schema`; `host` the binary's own
76
- * version, build info and resolution anchor (see the module comment).
76
+ * version, build info and resolution anchor (see the module comment);
77
+ * `profile` the catalog profile served (the MCP server's `--tools`; the CLI's
78
+ * own `version` verb is the full library and says `full`).
77
79
  */
78
- export function versionInfo(store, host) {
80
+ export function versionInfo(store, host, profile = "full") {
79
81
  const anchors = [...(host?.resolveFrom ? [host.resolveFrom] : []), import.meta.url];
80
82
  const components = {};
81
83
  for (const name of COMPONENT_PACKAGES) {
@@ -98,6 +100,7 @@ export function versionInfo(store, host) {
98
100
  runtime: `node ${process.versions.node}`,
99
101
  commit: host?.commit ?? null,
100
102
  built: host?.built ?? null,
103
+ profile,
101
104
  };
102
105
  }
103
106
  //# sourceMappingURL=version.js.map