@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.
- package/dist/src/core/ids.d.ts +1 -1
- package/dist/src/core/ingest.d.ts +2 -0
- package/dist/src/core/ingest.js +13 -0
- package/dist/src/core/read/document.d.ts +13 -9
- package/dist/src/core/read/document.js +18 -8
- package/dist/src/core/store/archive.d.ts +196 -0
- package/dist/src/core/store/archive.js +606 -0
- package/dist/src/core/store/gc.d.ts +37 -4
- package/dist/src/core/store/gc.js +62 -28
- package/dist/src/core/store/rebuild.d.ts +2 -0
- package/dist/src/core/store/rebuild.js +8 -2
- package/dist/src/core/store/retention.d.ts +138 -0
- package/dist/src/core/store/retention.js +409 -0
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +30 -4
- package/dist/src/core/store/size.d.ts +24 -2
- package/dist/src/core/store/size.js +33 -3
- package/dist/src/core/store/store.d.ts +1 -0
- package/dist/src/core/store/store.js +53 -1
- package/dist/src/graph/history.d.ts +2 -0
- package/dist/src/graph/history.js +8 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/mcp/arguments.d.ts +10 -2
- package/dist/src/mcp/arguments.js +37 -3
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +4 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +58 -8
- package/dist/src/mcp/server.js +51 -24
- package/dist/src/mutate/effects.d.ts +23 -5
- package/dist/src/mutate/effects.js +111 -22
- package/dist/src/mutate/known-ids.d.ts +19 -0
- package/dist/src/mutate/known-ids.js +47 -7
- package/dist/src/mutate/ops.d.ts +1 -0
- package/dist/src/mutate/ops.js +20 -1
- package/dist/src/mutate/select.d.ts +0 -11
- package/dist/src/mutate/select.js +33 -9
- package/dist/src/oqx-js/context.d.ts +9 -2
- package/dist/src/oqx-js/context.js +92 -30
- package/dist/src/oqx-js/dates.d.ts +105 -0
- package/dist/src/oqx-js/dates.js +427 -0
- package/dist/src/oqx-js/names.d.ts +40 -0
- package/dist/src/oqx-js/names.js +358 -0
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +30 -11
- package/dist/src/oqx-js/root-row.d.ts +15 -4
- package/dist/src/oqx-js/root-row.js +18 -10
- package/dist/src/oqx-js/run.d.ts +19 -1
- package/dist/src/oqx-js/run.js +97 -13
- package/dist/src/oqx-js/sql/translate.js +1 -1
- package/dist/src/spec-versions.d.ts +5 -5
- package/dist/src/spec-versions.js +4 -4
- package/dist/src/sync/observe.js +9 -3
- package/package.json +2 -2
package/dist/src/oqx-js/run.js
CHANGED
|
@@ -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
|
|
10
|
-
import { REPO_REMOVED_MESSAGE,
|
|
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
|
|
76
|
-
if (q.source.kind === "ident" && !
|
|
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" &&
|
|
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
|
|
169
|
-
//
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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.
|
|
2
|
+
readonly oqx: "0.24";
|
|
3
3
|
readonly format: "0.2";
|
|
4
4
|
readonly reconcile: "2.3";
|
|
5
|
-
readonly store: "
|
|
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.
|
|
9
|
+
readonly mutate: "3.5";
|
|
10
10
|
readonly sync: "2.2";
|
|
11
|
-
readonly surface: "4.
|
|
12
|
-
readonly cli: "4.
|
|
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: "
|
|
16
|
+
store: "15.0",
|
|
17
17
|
properties: "1.1",
|
|
18
18
|
graph: "1.1",
|
|
19
19
|
search: "2.0",
|
|
20
|
-
mutate: "3.
|
|
20
|
+
mutate: "3.5",
|
|
21
21
|
sync: "2.2",
|
|
22
|
-
surface: "4.
|
|
23
|
-
cli: "4.
|
|
22
|
+
surface: "4.9",
|
|
23
|
+
cli: "4.6",
|
|
24
24
|
};
|
|
25
25
|
//# sourceMappingURL=spec-versions.js.map
|
package/dist/src/sync/observe.js
CHANGED
|
@@ -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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
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.
|
|
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",
|