@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
@@ -6,8 +6,10 @@
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";
12
+ import { DATE_FUNCTIONS, DATE_METHODS, dateFunction, dateMethod } from "./dates.js";
11
13
  import { FilterInvalid } from "../search/cel/parser.js";
12
14
  import { float32ToBlob } from "../core/vec.js";
13
15
  import { encodeCursor as encodeKeyset, decodePathCursor } from "../core/cursor.js";
@@ -72,8 +74,8 @@ export function rootPathLiterals(e) {
72
74
  export function checkRootSpellings(q) {
73
75
  // A top-level `from <name>` that is no collection (`from edges`, the links
74
76
  // 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))
77
+ // refused, naming the five collections (root-row.ts).
78
+ if (q.source.kind === "ident" && !ROOTS.has(q.source.name))
77
79
  throw new FilterInvalid(unknownTargetMessage(q.source.name), "OQX");
78
80
  visit(q, {
79
81
  enter(node, ctx) {
@@ -84,14 +86,38 @@ export function checkRootSpellings(q) {
84
86
  if (node.kind === "ident" && ctx.depth >= 1 && (node.name === "docs" || node.name === "links")) {
85
87
  throw new FilterInvalid(bareTargetMessage(node.name, ctx.depth), "OQX");
86
88
  }
87
- if (node.kind === "outer" && TARGETS.has(node.name) && node.levels > ctx.depth) {
89
+ if (node.kind === "outer" && ROOTS.has(node.name) && node.levels > ctx.depth) {
88
90
  throw new FilterInvalid(pastRootMessage(node.name, node.levels), "OQX");
89
91
  }
90
92
  },
91
93
  });
92
94
  }
95
+ // Dates over literals fold to literals (spec/surface §1.3 "Dates and
96
+ // durations", 4.8): `date("today")`, `dur("30 days")`, and a date method whose
97
+ // receiver and arguments are literals (`date("today").minus(dur("30 days"))`,
98
+ // folded inside-out since `transform` maps children first) become the string /
99
+ // number / null they evaluate to — by the SAME functions the context runs, so
100
+ // nothing changes in meaning — before the planner sees the tree. So `where
101
+ // updated_at < date("today")` is a text-vs-text comparison the translator
102
+ // pushes down (a call is otherwise an unconditional decline), and a named point
103
+ // is resolved ONCE per query. A call with the wrong arity is left in place: it
104
+ // raises when evaluated, and an empty source never evaluates it (spec/oqx §11).
105
+ const isLit = (e) => e.kind === "lit";
106
+ export function foldDates(e) {
107
+ if (e.kind !== "call" || !e.args.every(isLit))
108
+ return e;
109
+ const args = e.args.map((a) => a.value);
110
+ if (e.recv === null) {
111
+ if (!Object.hasOwn(DATE_FUNCTIONS, e.name) || DATE_FUNCTIONS[e.name] !== args.length)
112
+ return e;
113
+ return build.lit(dateFunction(e.name, args).value);
114
+ }
115
+ if (!isLit(e.recv) || !Object.hasOwn(DATE_METHODS, e.name) || DATE_METHODS[e.name] !== args.length)
116
+ return e;
117
+ return build.lit(dateMethod(e.name, e.recv.value, args).value);
118
+ }
93
119
  function rewriteQuery(q) {
94
- return transform(q, (e) => (e.kind === "call" && e.recv === null && ROW_FNS.has(e.name) ? { ...e, recv: build.ident("$self") } : rootPathLiterals(e)));
120
+ return transform(q, (e) => (e.kind === "call" && e.recv === null && ROW_FNS.has(e.name) ? { ...e, recv: build.ident("$self") } : foldDates(rootPathLiterals(e))));
95
121
  }
96
122
  // A top-level `limit`/`offset` on the collect path is applied by the runner (see
97
123
  // oqxRunInner), so it must be a plain number literal here — there is no row to
@@ -165,8 +191,9 @@ function envelopeCollision(items) {
165
191
  return null;
166
192
  }
167
193
  // A hit is a store row (spec/surface §1.4, 1.5). The engine's top-level rows
168
- // are store rows by construction when the source is a bare root scan and
169
- // nothing re-projects them; a `follow` destination (`follow before` over a
194
+ // are store rows by construction when the source is a bare root scan (one of
195
+ // the five collections, `revisions` included — the planner's `rootTarget` is
196
+ // narrower: the four it can plan) and nothing re-projects them; a `follow` destination (`follow before` over a
170
197
  // frontmatter list of paths), a `from E` re-projection or any other source can
171
198
  // reach a scalar, which the injected `id`/`path` reads would render as the
172
199
  // junk hit `{ id: "undefined", path: "" }`. For those queries the row itself is
@@ -177,7 +204,7 @@ function envelopeCollision(items) {
177
204
  const SELF_KEY = "__oqx_self";
178
205
  const SELF_ITEM = build.field(SELF_KEY, build.ident("$it"));
179
206
  function mayReachNonRows(q) {
180
- return q.follow !== null || q.from.length > 0 || rootTarget(q.source) === null;
207
+ return q.follow !== null || q.from.length > 0 || !(q.source.kind === "ident" && ROOTS.has(q.source.name));
181
208
  }
182
209
  function describeValue(v) {
183
210
  if (v === null || v === undefined)
@@ -195,7 +222,7 @@ function describeValue(v) {
195
222
  return "an object";
196
223
  }
197
224
  function notAStoreRow(v) {
198
- 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");
225
+ 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");
199
226
  }
200
227
  // The reserved key a top-level `values` projection's single item is renamed to,
201
228
  // so it rides through id/path injection, keyset paging, and distinct as an
@@ -294,19 +321,36 @@ function oqxRunInner(store, repoId, source, opts) {
294
321
  const reserved = reservedAlias(raw.select);
295
322
  if (reserved)
296
323
  throw reserved;
297
- return oqxRunQuery(store, repoId, raw, opts);
324
+ // The source entry is every read: a tier-3 name on an open kind is a hint.
325
+ return oqxRunQuery(store, repoId, raw, { unknownNames: "hint", ...opts });
298
326
  }
299
327
  /**
300
328
  * The AST entry (4.1): run an already parsed query — the source path above
301
329
  * after `parse`, and the selector compiler (`mutate/select.ts`), which appends
302
330
  * its slot items to the RAW tree so a user alias a template reads is inlined
303
331
  * by the alias resolution here. Errors are the library's own (`OqxError`);
304
- * `oqxRun` normalizes them, a caller of this entry does the same.
332
+ * `oqxRun` normalizes them, a caller of this entry does the same. Unknown
333
+ * names (4.6) default to `"refuse"` here: this entry's caller is the write
334
+ * selector, where a name no row carries would select every row; the source
335
+ * entry passes `"hint"`.
305
336
  */
306
337
  export function oqxRunQuery(store, repoId, raw, opts = {}) {
338
+ checkRootSpellings(raw);
339
+ // Unknown names (spec/surface §1.5): judged over the RAW tree — before alias
340
+ // resolution and the runner's own items — so an alias is never mistaken for
341
+ // a row property and the verdict never depends on the rows. The selector
342
+ // compiler's slot items (`__omg_sel_*`, the templates' expressions) are not
343
+ // the selector: an expression reading an absent property there is
344
+ // spec/mutate's `target_missing` / absent value, not an unknown name.
345
+ const own = { ...raw, select: raw.select.filter((it) => !RESERVED_ALIAS_PREFIXES.some((p) => it.name.startsWith(p))) };
346
+ const hints = checkNames(store, repoId, own, opts.unknownNames ?? "refuse");
347
+ const result = runResolved(store, repoId, raw, opts);
348
+ // `hints` is the last member, present only when there is something to say.
349
+ return hints.length === 0 ? result : { ...result, hints };
350
+ }
351
+ function runResolved(store, repoId, raw, opts) {
307
352
  // The query's `select` aliases are resolved HERE, once, before the runner
308
353
  // renames/injects items (an engine evaluates the query it is given).
309
- checkRootSpellings(raw);
310
354
  const parsed = rewriteQuery(resolveAliases(raw));
311
355
  const consumer = parsed.consumer;
312
356
  const ctxOpts = opts.semanticVectors ? { semanticVectors: opts.semanticVectors } : {};
@@ -328,6 +372,18 @@ export function oqxRunQuery(store, repoId, raw, opts = {}) {
328
372
  const res = engine.run(parsed, []);
329
373
  return { hits: [], truncated: false, cursor: null, consumer: "none", none: res.consumer === "none" ? res.none : true };
330
374
  }
375
+ // A grouped query (oqx 0.24, SEMANTICS §24) yields GROUPS, not store rows:
376
+ // the envelope does not apply — there is no `id`/`path` to inject and no
377
+ // keyset to page by — so a hit is the group's projection as the query wrote
378
+ // it (`{ layer, n }`), exactly what `bare` yields for a row query; `bare` is
379
+ // accepted and changes nothing. Rows that surface as values (`rows collect {
380
+ // }` with an empty projection, `collect(doc)`) render as `{ id, path }` refs
381
+ // like every row-as-value. Paging is by position: the query's own
382
+ // `limit`/`offset` define the set, the tool's `limit` takes the first page,
383
+ // `truncated` says whether more groups follow, the cursor is always null (as
384
+ // under a custom `order by`). `distinct` cannot occur (a parse error).
385
+ if (parsed.groupBy)
386
+ return runGrouped(engine, parsed, consumer, opts);
331
387
  // collect / first / single: inject id + path so every hit carries them. A
332
388
  // top-level `select distinct` is applied HERE, not in the engine: the injected
333
389
  // id/path are unique per row and would defeat the engine's projection dedup, so
@@ -398,6 +454,34 @@ export function oqxRunQuery(store, repoId, raw, opts = {}) {
398
454
  return { hits: [], truncated, cursor, consumer: "collect", values: page.map((s) => s.proj[VALUE_KEY]) };
399
455
  return { hits: page.map(render), truncated, cursor, consumer: "collect" };
400
456
  }
457
+ // The collect/first/single path of a grouped query (see `runResolved`).
458
+ function runGrouped(engine, parsed, consumer, opts) {
459
+ const topValues = parsed.values;
460
+ // A `values` projection rides as a record under VALUE_KEY and is peeled off
461
+ // the page, as on the row path.
462
+ const select = topValues ? [{ ...parsed.select[0], name: VALUE_KEY }] : parsed.select;
463
+ const { limit: topLimit, offset: topOffset } = parsed;
464
+ const base = consumer === "collect" ? { ...parsed, limit: null, offset: null } : parsed;
465
+ const res = engine.run({ ...base, values: false, select }, []);
466
+ const project = (row) => renderValue(row);
467
+ if (consumer === "first" || consumer === "single") {
468
+ const row = res.consumer === "first" || res.consumer === "single" ? res.row : null;
469
+ if (topValues)
470
+ return { hits: [], truncated: false, cursor: null, consumer, values: row == null ? [] : [project(row)[VALUE_KEY]] };
471
+ return { hits: row == null ? [] : [project(row)], truncated: false, cursor: null, consumer };
472
+ }
473
+ let groups = (res.consumer === "collect" ? res.rows : []).map(project);
474
+ const offset = constBound(topOffset, "offset") ?? 0;
475
+ const limit = constBound(topLimit, "limit");
476
+ if (offset || limit != null)
477
+ groups = groups.slice(offset, limit == null ? undefined : offset + limit);
478
+ const cap = opts.limit ?? 50;
479
+ const truncated = groups.length > cap;
480
+ const page = groups.slice(0, cap);
481
+ if (topValues)
482
+ return { hits: [], truncated, cursor: null, consumer: "collect", values: page.map((g) => g[VALUE_KEY]) };
483
+ return { hits: page, truncated, cursor: null, consumer: "collect" };
484
+ }
401
485
  export async function oqxRunAsync(store, repoId, source, opts = {}, embedQuery) {
402
486
  const phrases = collectSemanticPhrases(source);
403
487
  if (phrases.length === 0)
@@ -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.21";
2
+ readonly oqx: "0.24";
3
3
  readonly format: "0.2";
4
4
  readonly reconcile: "2.3";
5
- readonly store: "14.1";
5
+ readonly store: "15.0";
6
6
  readonly properties: "1.1";
7
7
  readonly graph: "1.1";
8
8
  readonly search: "2.0";
9
- readonly mutate: "3.3";
9
+ readonly mutate: "3.5";
10
10
  readonly sync: "2.2";
11
- readonly surface: "4.4";
12
- readonly cli: "4.3";
11
+ readonly surface: "4.9";
12
+ readonly cli: "4.6";
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.1",
16
+ store: "15.0",
17
17
  properties: "1.1",
18
18
  graph: "1.1",
19
19
  search: "2.0",
20
- mutate: "3.3",
20
+ mutate: "3.5",
21
21
  sync: "2.2",
22
- surface: "4.4",
23
- cli: "4.3",
22
+ surface: "4.9",
23
+ cli: "4.6",
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omgbase/core",
3
- "version": "0.19.0",
3
+ "version": "0.23.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.21.0",
58
+ "@omgbase/oqx": "^0.24.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",