@omgbase/core 0.8.0 → 0.10.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/cursor.d.ts +13 -1
- package/dist/src/core/cursor.js +21 -2
- package/dist/src/core/paths.d.ts +10 -0
- package/dist/src/core/paths.js +34 -0
- package/dist/src/core/read/document.d.ts +1 -0
- package/dist/src/core/read/document.js +3 -2
- package/dist/src/core/read/reader.d.ts +6 -3
- package/dist/src/core/read/reader.js +31 -16
- package/dist/src/graph/history.js +16 -10
- package/dist/src/graph/link-health.d.ts +2 -1
- package/dist/src/graph/link-health.js +13 -7
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -1
- package/dist/src/mcp/errors.js +1 -1
- package/dist/src/mcp/graph.js +50 -15
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +93 -26
- package/dist/src/mcp/server.js +32 -28
- package/dist/src/oqx-js/context.d.ts +2 -1
- package/dist/src/oqx-js/context.js +93 -29
- package/dist/src/oqx-js/planner.d.ts +8 -1
- package/dist/src/oqx-js/planner.js +21 -8
- package/dist/src/oqx-js/root-row.d.ts +21 -0
- package/dist/src/oqx-js/root-row.js +55 -0
- package/dist/src/oqx-js/run.d.ts +3 -1
- package/dist/src/oqx-js/run.js +127 -163
- package/dist/src/oqx-js/sql/translate.d.ts +7 -0
- package/dist/src/oqx-js/sql/translate.js +42 -11
- package/dist/src/oqx-js/store-index.js +12 -3
- package/dist/src/spec-versions.d.ts +3 -3
- package/dist/src/spec-versions.js +2 -2
- package/dist/src/surface-paths.d.ts +33 -0
- package/dist/src/surface-paths.js +75 -0
- package/package.json +2 -2
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
// translated (that is the only context `partitionPushable` pushes into). `||`,
|
|
40
40
|
// `!`, `in`, `matches` (needs a regexp UDF), and bare content-property routing
|
|
41
41
|
// are deliberately declined here and left residual for now.
|
|
42
|
+
import { storagePath } from "../../core/paths.js";
|
|
43
|
+
import { TARGETS } from "../root-row.js";
|
|
42
44
|
// ---- names that are never property reads ------------------------------------
|
|
43
45
|
/**
|
|
44
46
|
* Per target, the bare identifiers (and `doc.<k>` keys, via the docs set) that
|
|
@@ -59,7 +61,15 @@ export const NON_PROPERTY_NAMES = {
|
|
|
59
61
|
// A `$`-namespaced intrinsic → a param-free SQL scalar, per target, with its kind.
|
|
60
62
|
// Anything not mapped (e.g. docs `$body`, reconstructed; `$title`/`$tags`,
|
|
61
63
|
// computed) returns null → residual. `$updated_at`/`$dst_path`/`$dst_uri` are
|
|
62
|
-
// correlated subqueries.
|
|
64
|
+
// correlated subqueries. `$path` and `$dst_path` are the REFERENCE form on the
|
|
65
|
+
// surface (spec/surface §1 "Paths", 2.0) — `'/' || <storage column>` in SQL —
|
|
66
|
+
// so every comparison, `startsWith`, `contains`, `lower()`… sees exactly the
|
|
67
|
+
// string the in-memory intrinsic yields; `pathColumn` keeps the bare indexed
|
|
68
|
+
// column at hand for the equality fast path.
|
|
69
|
+
/** The rooted path read over a storage path column. */
|
|
70
|
+
export function pathColumn(col) {
|
|
71
|
+
return { sql: `('/' || ${col})`, params: [], kind: "text", pathCol: col };
|
|
72
|
+
}
|
|
63
73
|
function intrinsicSql(name, ctx) {
|
|
64
74
|
const { self, doc, target } = ctx;
|
|
65
75
|
const text = (sql) => ({ sql, params: [], kind: "text" });
|
|
@@ -67,7 +77,7 @@ function intrinsicSql(name, ctx) {
|
|
|
67
77
|
if (target === "docs")
|
|
68
78
|
switch (name) {
|
|
69
79
|
case "$id": return text(`${self}.doc_id`);
|
|
70
|
-
case "$path": return
|
|
80
|
+
case "$path": return pathColumn(`${doc}.path`);
|
|
71
81
|
case "$content_hash": return text(`lower(hex(${self}.file_hash))`);
|
|
72
82
|
case "$updated_at": return text(`(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ${self}.current_rev)`);
|
|
73
83
|
}
|
|
@@ -75,7 +85,7 @@ function intrinsicSql(name, ctx) {
|
|
|
75
85
|
switch (name) {
|
|
76
86
|
case "$id": return text(`${self}.block_id`);
|
|
77
87
|
case "$doc": return text(`${self}.doc_id`);
|
|
78
|
-
case "$path": return
|
|
88
|
+
case "$path": return pathColumn(`${doc}.path`);
|
|
79
89
|
case "$ordinal": return int(`${self}.ordinal`);
|
|
80
90
|
case "$depth": return int(`${self}.depth`);
|
|
81
91
|
case "$body": return text(`${self}.text`);
|
|
@@ -87,7 +97,7 @@ function intrinsicSql(name, ctx) {
|
|
|
87
97
|
case "$node_id": return text(`${self}.node_id`);
|
|
88
98
|
case "$doc_id": return text(`${self}.doc_id`);
|
|
89
99
|
case "$block_id": return text(`${self}.block_id`);
|
|
90
|
-
case "$path": return
|
|
100
|
+
case "$path": return pathColumn(`${doc}.path`);
|
|
91
101
|
}
|
|
92
102
|
else if (target === "edges")
|
|
93
103
|
switch (name) {
|
|
@@ -97,8 +107,8 @@ function intrinsicSql(name, ctx) {
|
|
|
97
107
|
case "$src_block": return text(`${self}.src_block`);
|
|
98
108
|
case "$via": return text(`${self}.via_node`);
|
|
99
109
|
case "$from_commit": return text(`${self}.from_commit`);
|
|
100
|
-
case "$path": return
|
|
101
|
-
case "$dst_path": return text(`(SELECT dd.path FROM docs dd WHERE dd.doc_id = ${self}.dst_node)`);
|
|
110
|
+
case "$path": return pathColumn(`${doc}.path`);
|
|
111
|
+
case "$dst_path": return text(`(SELECT '/' || dd.path FROM docs dd WHERE dd.doc_id = ${self}.dst_node)`);
|
|
102
112
|
case "$dst_uri": return text(`(SELECT xn.uri FROM external_nodes xn WHERE xn.node_id = ${self}.dst_node)`);
|
|
103
113
|
}
|
|
104
114
|
return null;
|
|
@@ -177,8 +187,9 @@ export function translateOperand(e, ctx) {
|
|
|
177
187
|
if (e.name.startsWith("$"))
|
|
178
188
|
return intrinsicSql(e.name, ctx);
|
|
179
189
|
// A relation, reach-through, source handle or the whole attrs object is not a
|
|
180
|
-
// scalar read (decline (b))
|
|
181
|
-
|
|
190
|
+
// scalar read (decline (b)); a bare target name is a relation or a loud
|
|
191
|
+
// error (root-row.ts), never a property.
|
|
192
|
+
if (NON_PROPERTY_NAMES[target].has(e.name) || TARGETS.has(e.name))
|
|
182
193
|
return null;
|
|
183
194
|
// Bare field per target.
|
|
184
195
|
if (target === "docs") {
|
|
@@ -224,15 +235,15 @@ export function translateOperand(e, ctx) {
|
|
|
224
235
|
return null;
|
|
225
236
|
const k = rest[0];
|
|
226
237
|
if (k === "$path")
|
|
227
|
-
return
|
|
238
|
+
return pathColumn(`${doc}.path`);
|
|
228
239
|
if (k === "format")
|
|
229
240
|
return text(`${doc}.format`);
|
|
230
241
|
if (k.startsWith("$"))
|
|
231
242
|
return null;
|
|
232
243
|
if (RESERVED_DOC_BASENAMES.has(k))
|
|
233
244
|
return null;
|
|
234
|
-
if (NON_PROPERTY_NAMES.docs.has(k))
|
|
235
|
-
return null; // `doc.nodes`, `doc.frontmatter`, … (decline (b))
|
|
245
|
+
if (NON_PROPERTY_NAMES.docs.has(k) || TARGETS.has(k))
|
|
246
|
+
return null; // `doc.nodes`, `doc.frontmatter`, `doc.docs` … (decline (b))
|
|
236
247
|
return propScalar(doc, k);
|
|
237
248
|
}
|
|
238
249
|
// block.type / block.text reach-through from a node.
|
|
@@ -372,6 +383,9 @@ function translateComparison(e, ctx) {
|
|
|
372
383
|
const r = translateOperand(e.right, ctx);
|
|
373
384
|
if (!l || !r)
|
|
374
385
|
return null;
|
|
386
|
+
const fast = pathEquality(e.op, l, r);
|
|
387
|
+
if (fast)
|
|
388
|
+
return fast;
|
|
375
389
|
if (plainComparable(e.op, l.kind, r.kind)) {
|
|
376
390
|
// `==`/`!=` → null-safe IS / IS NOT (absence-normalized equality, faithful in
|
|
377
391
|
// any context). Relational ops → plain SQL: a NULL operand yields NULL, which
|
|
@@ -387,6 +401,23 @@ function translateComparison(e, ctx) {
|
|
|
387
401
|
return side === "right" ? typedComparison(e.op, l, r) : typedComparison(FLIP[e.op] ?? e.op, r, l);
|
|
388
402
|
}
|
|
389
403
|
const FLIP = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" };
|
|
404
|
+
/**
|
|
405
|
+
* The path equality fast path: `$path == "/a.md"` (or `!=`, either side) against
|
|
406
|
+
* a rooted text constant is `d.path IS ?` with the constant's storage form, so
|
|
407
|
+
* the `(repo_id, path)` index serves it; `'/' || d.path IS ?` — what the general
|
|
408
|
+
* form would emit — is the same predicate without the index. Only a constant
|
|
409
|
+
* that IS rooted qualifies (run.ts roots every literal compared with a path
|
|
410
|
+
* read, so that is every literal); a bare binding stays on the general form,
|
|
411
|
+
* where `'/' || d.path` can never equal it — exactly the in-memory answer.
|
|
412
|
+
*/
|
|
413
|
+
function pathEquality(op, l, r) {
|
|
414
|
+
if (op !== "==" && op !== "!=")
|
|
415
|
+
return null;
|
|
416
|
+
const [col, konst] = l.pathCol ? [l, r] : r.pathCol ? [r, l] : [null, null];
|
|
417
|
+
if (!col || !konst || konst.kind !== "text" || typeof konst.value !== "string" || !konst.value.startsWith("/"))
|
|
418
|
+
return null;
|
|
419
|
+
return { sql: `(${col.pathCol} ${IS_OP[op]} ?)`, params: [storagePath(konst.value)] };
|
|
420
|
+
}
|
|
390
421
|
/**
|
|
391
422
|
* The typed push (spec/surface §1, 1.2 patch): `read <op> constant` where the
|
|
392
423
|
* read is a JSON or property read and the constant a bool or num literal/binding.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Store-backed equality indexes for the OQX engine (`DataContext.indexFor`):
|
|
2
|
-
// when a nested block's receiver is a ROOT SCAN (
|
|
2
|
+
// when a nested block's receiver is a ROOT SCAN (`^docs`, `^edges`,
|
|
3
3
|
// a bare `docs` at the root scope) and its correlated or constant equality is on
|
|
4
4
|
// a column or property SQLite can look up — `where $path == ^^$path`,
|
|
5
5
|
// `where customer == ^$path`, `where $dst == ^$id`, `where type == "order"` —
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
import { semantics } from "@omgbase/oqx";
|
|
41
41
|
import { NON_PROPERTY_NAMES, RESERVED_DOC_BASENAMES } from "./sql/translate.js";
|
|
42
42
|
import { COLS, FROM, FROM_BY_DOC, ORDER, guards, guardsByDoc } from "./sql/scan.js";
|
|
43
|
+
import { storagePath } from "../core/paths.js";
|
|
43
44
|
// ---- which paths SQLite answers ------------------------------------------------
|
|
44
45
|
/** The indexed TEXT column (or an equivalent indexed test) a one-segment local
|
|
45
46
|
* path reads, per target — the paths a probe pushes to SQLite. `?` is the
|
|
@@ -104,8 +105,16 @@ class StoreIndex {
|
|
|
104
105
|
const v = value === undefined ? null : value;
|
|
105
106
|
if (v === null)
|
|
106
107
|
return this.fallback(null);
|
|
107
|
-
if (this.probe.kind === "column")
|
|
108
|
-
|
|
108
|
+
if (this.probe.kind === "column") {
|
|
109
|
+
if (typeof v !== "string")
|
|
110
|
+
return [];
|
|
111
|
+
// `$path` / `$dst_path` are the reference form in memory (`/a.md`,
|
|
112
|
+
// spec/surface §1 "Paths"); the column holds the storage form. A rooted
|
|
113
|
+
// probe is de-rooted; a bare one can equal no rooted path — no row.
|
|
114
|
+
if (this.path === "$path" || this.path === "$dst_path")
|
|
115
|
+
return v.startsWith("/") ? this.column(storagePath(v)) : [];
|
|
116
|
+
return this.column(v);
|
|
117
|
+
}
|
|
109
118
|
switch (typeof v) {
|
|
110
119
|
case "string": return this.property("p.type = 'string' AND p.val_text = ?", v);
|
|
111
120
|
case "number": return Number.isNaN(v) ? [] : this.property("p.type = 'number' AND p.val_num = ?", v);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const SPEC_VERSIONS: {
|
|
2
|
-
readonly oqx: "0.
|
|
2
|
+
readonly oqx: "0.18";
|
|
3
3
|
readonly format: "0.2";
|
|
4
4
|
readonly reconcile: "2.3";
|
|
5
5
|
readonly store: "13.5";
|
|
@@ -8,7 +8,7 @@ export declare const SPEC_VERSIONS: {
|
|
|
8
8
|
readonly search: "1.2";
|
|
9
9
|
readonly mutate: "1.3";
|
|
10
10
|
readonly sync: "1.3";
|
|
11
|
-
readonly surface: "
|
|
12
|
-
readonly cli: "
|
|
11
|
+
readonly surface: "2.0";
|
|
12
|
+
readonly cli: "2.0";
|
|
13
13
|
};
|
|
14
14
|
export type SpecName = keyof typeof SPEC_VERSIONS;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ApplyResult } from "./mutate/apply.js";
|
|
2
|
+
import type { DocOpResult, DocMoveResult } from "./mutate/docs.js";
|
|
3
|
+
import type { RetargetHit } from "./mutate/macros.js";
|
|
4
|
+
import type { Opset } from "./mutate/opset.js";
|
|
5
|
+
import type { DocsUpdateResult } from "./mutate/plan-update.js";
|
|
6
|
+
import type { ObserveResult, ObserveDeleteResult } from "./sync/observe.js";
|
|
7
|
+
import type { SweepResult } from "./sync/freshness.js";
|
|
8
|
+
import type { TextSearchResult } from "./search/text.js";
|
|
9
|
+
import type { ResolveHit } from "./search/resolve.js";
|
|
10
|
+
/** `spec/mutate` §4's result: `revisions[].path` and the dry-run `diffs` keys. */
|
|
11
|
+
export declare function surfaceApplyResult<T extends ApplyResult>(res: T): T;
|
|
12
|
+
/** `docs_create` / `docs_delete` / `docs_set_meta`: `path` and the `diffs` keys. */
|
|
13
|
+
export declare function surfaceDocOpResult<T extends DocOpResult>(res: T): T;
|
|
14
|
+
/** `docs_move`: the document-op fields plus each dangling link's source `path`
|
|
15
|
+
* (its `target` stays as authored — that is what the link says). */
|
|
16
|
+
export declare function surfaceDocMoveResult(res: DocMoveResult): DocMoveResult;
|
|
17
|
+
/** `observe` / `observe_many` outcomes. */
|
|
18
|
+
export declare function surfaceObserveResult<T extends ObserveResult>(res: T): T;
|
|
19
|
+
/** `observe_delete`. */
|
|
20
|
+
export declare function surfaceObserveDeleteResult(res: ObserveDeleteResult): ObserveDeleteResult;
|
|
21
|
+
/** `text_search` hits. */
|
|
22
|
+
export declare function surfaceTextSearch(res: TextSearchResult): TextSearchResult;
|
|
23
|
+
/** `resolve` hits: the locator is `<path>#<type>[<ordinal>]`; its path half is
|
|
24
|
+
* rooted (a locator without `#` names no path and is left alone). */
|
|
25
|
+
export declare function surfaceResolveHits(hits: ResolveHit[]): ResolveHit[];
|
|
26
|
+
/** `links_retarget` / `links_repair` hits (`path` is the block's document). */
|
|
27
|
+
export declare function surfaceRetargetHits(hits: RetargetHit[]): RetargetHit[];
|
|
28
|
+
/** A whole-document update plan: `target.path` and `precondition.path`. */
|
|
29
|
+
export declare function surfaceOpset(opset: Opset): Opset;
|
|
30
|
+
/** `docs_update`: the opset and, when committed, the apply result. */
|
|
31
|
+
export declare function surfaceDocsUpdate(res: DocsUpdateResult): DocsUpdateResult;
|
|
32
|
+
/** `omg sync`'s one-shot sweep: the ingested / suppressed / deleted / conflicted paths. */
|
|
33
|
+
export declare function surfaceSweepResult(res: SweepResult): SweepResult;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The surface's path rule applied at its boundary (spec/surface §1 "Paths",
|
|
2
|
+
// 2.0): every path a tool or verb returns is `/`-rooted. The surface-owned
|
|
3
|
+
// readers (`core/read`, `graph/history`, `graph/link-health`, the query
|
|
4
|
+
// binding) root their own output; the results that belong to another spec —
|
|
5
|
+
// `spec/mutate`'s apply and document-operation results and opsets,
|
|
6
|
+
// `spec/store`'s observe outcomes, `spec/search`'s hits, `spec/sync`'s sweep —
|
|
7
|
+
// keep their storage-form paths in the library (their own fixtures pin them)
|
|
8
|
+
// and are re-shaped HERE, by the MCP server and the `omg` verbs alike, so the
|
|
9
|
+
// two clients cannot drift. Each function returns a new value; the input is
|
|
10
|
+
// not mutated.
|
|
11
|
+
import { referencePath, referenceKeyed } from "./core/paths.js";
|
|
12
|
+
/** `spec/mutate` §4's result: `revisions[].path` and the dry-run `diffs` keys. */
|
|
13
|
+
export function surfaceApplyResult(res) {
|
|
14
|
+
return {
|
|
15
|
+
...res,
|
|
16
|
+
revisions: res.revisions.map((r) => ({ ...r, path: referencePath(r.path) })),
|
|
17
|
+
...(res.diffs ? { diffs: referenceKeyed(res.diffs) } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** `docs_create` / `docs_delete` / `docs_set_meta`: `path` and the `diffs` keys. */
|
|
21
|
+
export function surfaceDocOpResult(res) {
|
|
22
|
+
return { ...res, path: referencePath(res.path), ...(res.diffs ? { diffs: referenceKeyed(res.diffs) } : {}) };
|
|
23
|
+
}
|
|
24
|
+
/** `docs_move`: the document-op fields plus each dangling link's source `path`
|
|
25
|
+
* (its `target` stays as authored — that is what the link says). */
|
|
26
|
+
export function surfaceDocMoveResult(res) {
|
|
27
|
+
return { ...surfaceDocOpResult(res), dangling: res.dangling.map((l) => ({ ...l, path: referencePath(l.path) })) };
|
|
28
|
+
}
|
|
29
|
+
/** `observe` / `observe_many` outcomes. */
|
|
30
|
+
export function surfaceObserveResult(res) {
|
|
31
|
+
return { ...res, path: referencePath(res.path) };
|
|
32
|
+
}
|
|
33
|
+
/** `observe_delete`. */
|
|
34
|
+
export function surfaceObserveDeleteResult(res) {
|
|
35
|
+
return { ...res, path: referencePath(res.path) };
|
|
36
|
+
}
|
|
37
|
+
/** `text_search` hits. */
|
|
38
|
+
export function surfaceTextSearch(res) {
|
|
39
|
+
return { ...res, hits: res.hits.map((h) => ({ ...h, path: referencePath(h.path) })) };
|
|
40
|
+
}
|
|
41
|
+
/** `resolve` hits: the locator is `<path>#<type>[<ordinal>]`; its path half is
|
|
42
|
+
* rooted (a locator without `#` names no path and is left alone). */
|
|
43
|
+
export function surfaceResolveHits(hits) {
|
|
44
|
+
return hits.map((h) => {
|
|
45
|
+
const i = h.locator.indexOf("#");
|
|
46
|
+
return i < 0 ? h : { ...h, locator: referencePath(h.locator.slice(0, i)) + h.locator.slice(i) };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** `links_retarget` / `links_repair` hits (`path` is the block's document). */
|
|
50
|
+
export function surfaceRetargetHits(hits) {
|
|
51
|
+
return hits.map((h) => ({ ...h, path: referencePath(h.path) }));
|
|
52
|
+
}
|
|
53
|
+
/** A whole-document update plan: `target.path` and `precondition.path`. */
|
|
54
|
+
export function surfaceOpset(opset) {
|
|
55
|
+
return {
|
|
56
|
+
...opset,
|
|
57
|
+
target: { ...opset.target, path: referencePath(opset.target.path) },
|
|
58
|
+
precondition: { ...opset.precondition, path: referencePath(opset.precondition.path) },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** `docs_update`: the opset and, when committed, the apply result. */
|
|
62
|
+
export function surfaceDocsUpdate(res) {
|
|
63
|
+
return { opset: surfaceOpset(res.opset), result: res.result ? surfaceApplyResult(res.result) : null };
|
|
64
|
+
}
|
|
65
|
+
/** `omg sync`'s one-shot sweep: the ingested / suppressed / deleted / conflicted paths. */
|
|
66
|
+
export function surfaceSweepResult(res) {
|
|
67
|
+
return {
|
|
68
|
+
...res,
|
|
69
|
+
ingested: res.ingested.map(referencePath),
|
|
70
|
+
suppressed: res.suppressed.map(referencePath),
|
|
71
|
+
deleted: res.deleted.map(referencePath),
|
|
72
|
+
conflicted: res.conflicted.map(referencePath),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=surface-paths.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omgbase/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.18.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",
|