@omgbase/core 0.15.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,8 +16,8 @@ export interface MutDoc {
16
16
  frontmatterRaw: string | null;
17
17
  children: MutBlock[];
18
18
  }
19
- /** The codes spec/mutate §8 lists (2.3 adds `conflicting_effects`, `unsupported_plan_version`, `plan_mismatch`); `filter_invalid` is the planner's shape error, spec/surface §4's envelope. */
20
- export type MutationErrorCode = "stale_expectation" | "block_missing" | "target_missing" | "parent_missing" | "doc_missing" | "cycle_move" | "not_contiguous" | "type_mismatch" | "path_taken" | "node_not_editable" | "stale_plan" | "plan_not_convergent" | "sync_conflict" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "filter_invalid";
19
+ /** The codes spec/mutate §8 lists (2.3 adds `conflicting_effects`, `unsupported_plan_version`, `plan_mismatch`; 3.1 `ambiguous_target`, `selection_too_large` — §11.8); `filter_invalid` is the planner's shape error, spec/surface §4's envelope. */
20
+ export type MutationErrorCode = "stale_expectation" | "block_missing" | "target_missing" | "parent_missing" | "doc_missing" | "cycle_move" | "not_contiguous" | "type_mismatch" | "path_taken" | "node_not_editable" | "stale_plan" | "plan_not_convergent" | "sync_conflict" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "ambiguous_target" | "selection_too_large" | "filter_invalid";
21
21
  export declare class MutationError extends Error {
22
22
  code: MutationErrorCode | string;
23
23
  data: Record<string, unknown>;
@@ -125,6 +125,10 @@ function exprMayRaise(e, target, root) {
125
125
  return (e.lo !== null && exprMayRaise(e.lo, target, root)) || (e.hi !== null && exprMayRaise(e.hi, target, root));
126
126
  // A tuple (oqx 0.20) is never pushed down; it raises iff an element does.
127
127
  case "tuple": return e.items.some((x) => exprMayRaise(x, target, root));
128
+ // A template literal (oqx 0.21) is never pushed down; interpolating a list or
129
+ // a record is an eval error only the data decides, so with any interpolation
130
+ // it may raise.
131
+ case "template": return e.parts.some((p) => typeof p !== "string");
128
132
  case "lit":
129
133
  case "binding": return false;
130
134
  }
@@ -1,5 +1,6 @@
1
- import type { Query, Expr } from "@omgbase/oqx";
1
+ import type { Query, Expr, SelectItem } from "@omgbase/oqx";
2
2
  import type { Store } from "../core/store/store.js";
3
+ import { FilterInvalid } from "../search/cel/parser.js";
3
4
  import type { SemanticVec } from "../search/cel/compile.js";
4
5
  export type OqxConsumer = "collect" | "count" | "exists" | "none" | "first" | "single";
5
6
  export type EmbedQuery = (text: string) => Promise<{
@@ -18,6 +19,10 @@ export interface OqxHit {
18
19
  [k: string]: unknown;
19
20
  }
20
21
  export interface OqxResult {
22
+ /** `{ id, path, ...projection }` per hit — or, under `OqxOptions.bare`, the
23
+ * projection alone (`id`/`path` present only when the query projected them;
24
+ * typed as `OqxHit` for the one hit shape every consumer handles, a bare
25
+ * caller knows it asked). */
21
26
  hits: OqxHit[];
22
27
  truncated: boolean;
23
28
  cursor: string | null;
@@ -35,6 +40,17 @@ export interface OqxResult {
35
40
  export interface OqxOptions {
36
41
  limit?: number;
37
42
  cursor?: string | null;
43
+ /** Return each hit as its PROJECTION only — no automatic `id`/`path`
44
+ * members (spec/surface §1.4, 4.2): `select title, era from docs` yields
45
+ * `{ title, era }` rows. Needs a `select` (without a projection a hit is
46
+ * only its envelope) and is refused with `values` (already the bare-values
47
+ * form); both are `filter_invalid` before the engine runs. A projected
48
+ * `id`/`path` is the intrinsic as everywhere (`select id` → `{ id }`), and
49
+ * with no envelope there is nothing to collide with (`select path: layer`
50
+ * is fine). Paging, `distinct`, `truncated` and `cursor` are unchanged —
51
+ * the runner keeps identity internally and strips the envelope last. The
52
+ * scalar consumers (`count`/`exists`/`none`) have no hits and ignore it. */
53
+ bare?: boolean;
38
54
  semanticVectors?: Map<string, SemanticVec>;
39
55
  /** Force the pure in-memory engine (skip tier-3 pushdown). Internal — used by
40
56
  * the differential conformance suite to prove planned == in-memory. */
@@ -43,7 +59,18 @@ export interface OqxOptions {
43
59
  export declare function rootPathLiterals(e: Expr): Expr;
44
60
  export declare function checkRootSpellings(q: Query): void;
45
61
  export declare function collectSemanticPhrases(source: string): string[];
62
+ export declare function reservedAlias(items: readonly SelectItem[]): FilterInvalid | null;
63
+ export declare const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` \u2014 without a projection a hit is only its envelope";
64
+ export declare const BARE_WITH_VALUES_MESSAGE = "`bare` is redundant with `values` \u2014 `values` is already the bare-values form; drop one";
46
65
  export declare function oqxRun(store: Store, repoId: string, source: string, opts?: OqxOptions): OqxResult;
66
+ /**
67
+ * The AST entry (4.1): run an already parsed query — the source path above
68
+ * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
69
+ * its slot items to the RAW tree so a user alias a template reads is inlined
70
+ * by the alias resolution here. Errors are the library's own (`OqxError`);
71
+ * `oqxRun` normalizes them, a caller of this entry does the same.
72
+ */
73
+ export declare function oqxRunQuery(store: Store, repoId: string, raw: Query, opts?: OqxOptions): OqxResult;
47
74
  export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;
48
75
  /** Parse an OQX source into the `@omgbase/oqx` AST (re-exported for callers). */
49
76
  export declare function parseOqx(source: string): Query;
@@ -128,6 +128,21 @@ export function collectSemanticPhrases(source) {
128
128
  }
129
129
  const ID_ITEM = build.field("__oqx_id", build.ident("id"));
130
130
  const PATH_ITEM = build.field("__oqx_path", build.ident("path"));
131
+ // The runner's own projection names (`__oqx_id`, `__oqx_path`, `__oqx_self`,
132
+ // `__oqx_value`) and the selector compiler's slots (`__omg_sel_<t>_<slot>`,
133
+ // mutate/select.ts) share two reserved prefixes. A user alias under either would
134
+ // shadow an injected item (`select __oqx_id: 1` overwrote the hit's `id` through
135
+ // 4.0) or collide with a slot, so it is refused before anything runs — over the
136
+ // query's own top-level items, the only ones the runner injects beside.
137
+ const RESERVED_ALIAS_PREFIXES = ["__oqx_", "__omg_"];
138
+ export function reservedAlias(items) {
139
+ for (const item of items) {
140
+ const prefix = RESERVED_ALIAS_PREFIXES.find((p) => item.name.startsWith(p));
141
+ if (prefix !== undefined)
142
+ return new FilterInvalid(`projection \`${item.name}\` uses the reserved prefix \`${prefix}\` — choose another alias`, "OQX");
143
+ }
144
+ return null;
145
+ }
131
146
  // Result projections may not overwrite the envelope (spec/surface §1.4, 3.0):
132
147
  // a projection whose output key is `id` or `path` is allowed only when it IS
133
148
  // the envelope's own selection — the bare attribute of the same name, which
@@ -213,33 +228,56 @@ function renderValue(v) {
213
228
  out[k] = renderValue(x);
214
229
  return out;
215
230
  }
216
- function toHit(row) {
231
+ function shapeRow(row) {
217
232
  const o = row;
218
233
  const { __oqx_id, __oqx_path, __oqx_self, ...rest } = o;
219
234
  if (SELF_KEY in o && !rowRef(__oqx_self))
220
235
  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) };
236
+ return { id: String(__oqx_id), path: String(__oqx_path ?? ""), proj: renderValue(rest) };
237
+ }
238
+ // The envelope keys lead; a user projection of `id`/`path` collapses onto
239
+ // them in place (the same value, read by the same attribute).
240
+ function toHit(s) {
241
+ return { id: s.id, path: s.path, ...s.proj };
242
+ }
243
+ // `bare` (4.2): the projection alone. A projected `id`/`path` is an ordinary
244
+ // column here — the intrinsic the attribute reads, under the key the query
245
+ // gave it — and nothing is injected beside it.
246
+ function toBareHit(s) {
247
+ return s.proj;
224
248
  }
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) {
249
+ // Top-level `select distinct`: dedup hits by their USER projection — every
250
+ // field except the envelope's `id`/`path`, which a projection of the attribute
251
+ // collapses onto (so `select distinct id` dedups on nothing but the empty
252
+ // projection, as it always has); under `bare` there is no envelope, so a
253
+ // projected `id`/`path` IS part of the projection and counts. Keeps the first
254
+ // (so the retained hit carries a real identity). Order-preserving.
255
+ function dedupByProjection(rows, bare) {
229
256
  const seen = new Set();
230
257
  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]]));
258
+ for (const s of rows) {
259
+ const keys = Object.keys(s.proj).filter((k) => bare || !ENVELOPE_KEYS.has(k)).sort();
260
+ const key = JSON.stringify(keys.map((k) => [k, s.proj[k]]));
236
261
  if (seen.has(key))
237
262
  continue;
238
263
  seen.add(key);
239
- out.push(h);
264
+ out.push(s);
240
265
  }
241
266
  return out;
242
267
  }
268
+ // `bare` is examined where hits are built (spec/surface §1.4, 4.2): without a
269
+ // projection a hit is only its envelope, and `values` is already the
270
+ // bare-values form — both refused before the engine runs, over the resolved
271
+ // query, so the error never depends on the rows.
272
+ export const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` — without a projection a hit is only its envelope";
273
+ export const BARE_WITH_VALUES_MESSAGE = "`bare` is redundant with `values` — `values` is already the bare-values form; drop one";
274
+ function bareMisuse(parsed) {
275
+ if (parsed.values)
276
+ return new FilterInvalid(BARE_WITH_VALUES_MESSAGE, "OQX");
277
+ if (parsed.select.length === 0)
278
+ return new FilterInvalid(BARE_NEEDS_SELECT_MESSAGE, "OQX");
279
+ return null;
280
+ }
243
281
  export function oqxRun(store, repoId, source, opts = {}) {
244
282
  try {
245
283
  return oqxRunInner(store, repoId, source, opts);
@@ -252,9 +290,22 @@ export function oqxRun(store, repoId, source, opts = {}) {
252
290
  }
253
291
  }
254
292
  function oqxRunInner(store, repoId, source, opts) {
293
+ const raw = parse(source);
294
+ const reserved = reservedAlias(raw.select);
295
+ if (reserved)
296
+ throw reserved;
297
+ return oqxRunQuery(store, repoId, raw, opts);
298
+ }
299
+ /**
300
+ * The AST entry (4.1): run an already parsed query — the source path above
301
+ * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
302
+ * its slot items to the RAW tree so a user alias a template reads is inlined
303
+ * by the alias resolution here. Errors are the library's own (`OqxError`);
304
+ * `oqxRun` normalizes them, a caller of this entry does the same.
305
+ */
306
+ export function oqxRunQuery(store, repoId, raw, opts = {}) {
255
307
  // The query's `select` aliases are resolved HERE, once, before the runner
256
308
  // renames/injects items (an engine evaluates the query it is given).
257
- const raw = parse(source);
258
309
  checkRootSpellings(raw);
259
310
  const parsed = rewriteQuery(resolveAliases(raw));
260
311
  const consumer = parsed.consumer;
@@ -289,11 +340,21 @@ function oqxRunInner(store, repoId, source, opts) {
289
340
  // the final page below and returned as `values` with `hits` empty.
290
341
  const topValues = parsed.values;
291
342
  const userSelect = topValues ? [{ ...parsed.select[0], name: VALUE_KEY }] : parsed.select;
292
- if (!topValues) {
343
+ // `bare` (4.2): the hits are the projection alone — refused without one and
344
+ // with `values`; with no envelope there is no collision to refuse either, a
345
+ // projected `id`/`path` is simply a column.
346
+ const bare = opts.bare === true;
347
+ if (bare) {
348
+ const misuse = bareMisuse(parsed);
349
+ if (misuse)
350
+ throw misuse;
351
+ }
352
+ else if (!topValues) {
293
353
  const collision = envelopeCollision(userSelect);
294
354
  if (collision)
295
355
  throw collision;
296
356
  }
357
+ const render = bare ? toBareHit : toHit;
297
358
  // On the collect path a top-level `limit`/`offset` is ALSO taken out of the
298
359
  // engine query and applied here, after the runner's own distinct — the engine
299
360
  // would otherwise bound the raw rows before dedup (`select distinct type
@@ -307,13 +368,13 @@ function oqxRunInner(store, repoId, source, opts) {
307
368
  if (consumer === "first" || consumer === "single") {
308
369
  const row = res.consumer === "first" || res.consumer === "single" ? res.row : null;
309
370
  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 };
371
+ return { hits: [], truncated: false, cursor: null, consumer, values: row == null ? [] : [shapeRow(row).proj[VALUE_KEY]] };
372
+ return { hits: row == null ? [] : [render(shapeRow(row))], truncated: false, cursor: null, consumer };
312
373
  }
313
374
  // collect: keyset pagination on (path, id) when the order is the default.
314
- let rows = (res.consumer === "collect" ? res.rows : []).map(toHit);
375
+ let rows = (res.consumer === "collect" ? res.rows : []).map(shapeRow);
315
376
  if (topDistinct)
316
- rows = dedupHitsByProjection(rows);
377
+ rows = dedupByProjection(rows, bare);
317
378
  // The query's own bound defines the result SET; the `limit`/`cursor` options
318
379
  // then page within it.
319
380
  const offset = constBound(topOffset, "offset") ?? 0;
@@ -331,9 +392,11 @@ function oqxRunInner(store, repoId, source, opts) {
331
392
  page = page.slice(0, cap);
332
393
  const last = page[page.length - 1];
333
394
  const cursor = truncated && last && !custom ? encodeCursor(last.path, last.id) : null;
395
+ // The envelope is stripped LAST (`bare`): the page and the cursor above ran
396
+ // on identity, so a bare result pages exactly like the enveloped one.
334
397
  if (topValues)
335
- return { hits: [], truncated, cursor, consumer: "collect", values: page.map((h) => h[VALUE_KEY]) };
336
- return { hits: page, truncated, cursor, consumer: "collect" };
398
+ return { hits: [], truncated, cursor, consumer: "collect", values: page.map((s) => s.proj[VALUE_KEY]) };
399
+ return { hits: page.map(render), truncated, cursor, consumer: "collect" };
337
400
  }
338
401
  export async function oqxRunAsync(store, repoId, source, opts = {}, embedQuery) {
339
402
  const phrases = collectSemanticPhrases(source);
@@ -1,14 +1,14 @@
1
1
  export declare const SPEC_VERSIONS: {
2
- readonly oqx: "0.20";
2
+ readonly oqx: "0.21";
3
3
  readonly format: "0.2";
4
4
  readonly reconcile: "2.3";
5
- readonly store: "14.0";
5
+ readonly store: "14.1";
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.3";
10
10
  readonly sync: "2.2";
11
- readonly surface: "4.0";
12
- readonly cli: "4.0";
11
+ readonly surface: "4.4";
12
+ readonly cli: "4.3";
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.1",
17
17
  properties: "1.1",
18
18
  graph: "1.1",
19
19
  search: "2.0",
20
- mutate: "3.0",
20
+ mutate: "3.3",
21
21
  sync: "2.2",
22
- surface: "4.0",
23
- cli: "4.0",
22
+ surface: "4.4",
23
+ cli: "4.3",
24
24
  };
25
25
  //# sourceMappingURL=spec-versions.js.map
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omgbase/core",
3
- "version": "0.15.0",
3
+ "version": "0.19.0",
4
4
  "description": "Open Markdown Graph Base — embedded engine",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@modelcontextprotocol/sdk": "^1.30.0",
58
- "@omgbase/oqx": "^0.20.0",
58
+ "@omgbase/oqx": "^0.21.0",
59
59
  "better-sqlite3": "^13.0.3",
60
60
  "mdast-util-from-markdown": "^2.0.2",
61
61
  "mdast-util-frontmatter": "^2.0.1",