@omgbase/core 0.7.1 → 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.
Files changed (37) hide show
  1. package/dist/src/core/cursor.d.ts +13 -1
  2. package/dist/src/core/cursor.js +21 -2
  3. package/dist/src/core/paths.d.ts +10 -0
  4. package/dist/src/core/paths.js +34 -0
  5. package/dist/src/core/read/document.d.ts +1 -0
  6. package/dist/src/core/read/document.js +3 -2
  7. package/dist/src/core/read/reader.d.ts +6 -3
  8. package/dist/src/core/read/reader.js +31 -16
  9. package/dist/src/graph/history.js +16 -10
  10. package/dist/src/graph/link-health.d.ts +2 -1
  11. package/dist/src/graph/link-health.js +13 -7
  12. package/dist/src/index.d.ts +3 -1
  13. package/dist/src/index.js +3 -1
  14. package/dist/src/mcp/errors.js +1 -1
  15. package/dist/src/mcp/graph.js +50 -15
  16. package/dist/src/mcp/reference.d.ts +1 -1
  17. package/dist/src/mcp/reference.js +102 -33
  18. package/dist/src/mcp/server.js +32 -28
  19. package/dist/src/oqx-js/context.d.ts +2 -1
  20. package/dist/src/oqx-js/context.js +181 -49
  21. package/dist/src/oqx-js/planner.d.ts +8 -1
  22. package/dist/src/oqx-js/planner.js +22 -42
  23. package/dist/src/oqx-js/root-row.d.ts +21 -0
  24. package/dist/src/oqx-js/root-row.js +55 -0
  25. package/dist/src/oqx-js/run.d.ts +3 -1
  26. package/dist/src/oqx-js/run.js +128 -164
  27. package/dist/src/oqx-js/sql/scan.d.ts +27 -0
  28. package/dist/src/oqx-js/sql/scan.js +62 -0
  29. package/dist/src/oqx-js/sql/translate.d.ts +7 -0
  30. package/dist/src/oqx-js/sql/translate.js +42 -11
  31. package/dist/src/oqx-js/store-index.d.ts +20 -0
  32. package/dist/src/oqx-js/store-index.js +193 -0
  33. package/dist/src/spec-versions.d.ts +3 -3
  34. package/dist/src/spec-versions.js +2 -2
  35. package/dist/src/surface-paths.d.ts +33 -0
  36. package/dist/src/surface-paths.js +75 -0
  37. package/package.json +2 -2
@@ -0,0 +1,193 @@
1
+ // Store-backed equality indexes for the OQX engine (`DataContext.indexFor`):
2
+ // when a nested block's receiver is a ROOT SCAN (`^docs`, `^edges`,
3
+ // a bare `docs` at the root scope) and its correlated or constant equality is on
4
+ // a column or property SQLite can look up — `where $path == ^^$path`,
5
+ // `where customer == ^$path`, `where $dst == ^$id`, `where type == "order"` —
6
+ // the probe runs ONE prepared statement against the store's indexes instead of
7
+ // materializing every row of the target and hashing it. The rows come back
8
+ // exactly as the root scan constructs them (same columns, same `__path`, same
9
+ // target tag, same `(path, id)` order) under the same live-row and repo guards
10
+ // (`sql/scan.ts`), so every intrinsic and relation keeps working downstream and
11
+ // the result cannot differ from the scan — only the work does.
12
+ //
13
+ // Typed equality (OQX SEMANTICS §5) is preserved by probing per VALUE KIND:
14
+ //
15
+ // • a text column (`doc_id`, `path`, `type`, `kind`, `name`, `src_doc`, …)
16
+ // matches a string probe by `= ?` (TEXT = TEXT, exact); a number, boolean,
17
+ // object or any other non-string probe matches NO row (the column holds
18
+ // strings or NULL and `5 == "5"` is false); an absent probe (`null`,
19
+ // `undefined`: `name == null`) is answered by the in-memory fallback below;
20
+ // • a document property (`customer`, `$title`) matches a string probe only on
21
+ // a `type = 'string'` row (`val_text = ?`), a number probe only on a
22
+ // `type = 'number'` row (`val_num = ?`), a boolean only on a `type = 'bool'`
23
+ // row (`val_bool = ?`), each under the scalar-in-scope rule the in-memory
24
+ // `docProp` applies (exactly one live row for the key on the document and it
25
+ // is `card = 'scalar'`; a list-valued, repeated or nested key is an array or
26
+ // object in memory and equals no scalar). An object/array probe equals
27
+ // nothing (decoded values are fresh objects; §5 compares them by
28
+ // reference), `NaN` equals nothing; an absent probe (`key == null`: a `null`
29
+ // scalar OR a document lacking the key) is an anti-join SQLite has no index
30
+ // for, so it is answered by the fallback.
31
+ //
32
+ // The fallback — a probe value no statement covers — reads the collection once
33
+ // (the same lazy root scan the engine would have walked) and filters it with
34
+ // the context's own `get`, under `semantics.equals`: the engine's own hash
35
+ // index could do no better, and the answer is by construction the scan's.
36
+ //
37
+ // `lookup(value)` (positions into the collection) is implemented for the seam's
38
+ // contract but the engine never calls it for these indexes: it probes
39
+ // `lookupRows` first and so never reads the root whole.
40
+ import { semantics } from "@omgbase/oqx";
41
+ import { NON_PROPERTY_NAMES, RESERVED_DOC_BASENAMES } from "./sql/translate.js";
42
+ import { COLS, FROM, FROM_BY_DOC, ORDER, guards, guardsByDoc } from "./sql/scan.js";
43
+ import { storagePath } from "../core/paths.js";
44
+ // ---- which paths SQLite answers ------------------------------------------------
45
+ /** The indexed TEXT column (or an equivalent indexed test) a one-segment local
46
+ * path reads, per target — the paths a probe pushes to SQLite. `?` is the
47
+ * probe value. Each is served by a primary key, a `UNIQUE`, or an index of
48
+ * `spec/store/schema.sql`: docs `doc_id` (PK) and `(repo_id, path)`; blocks
49
+ * `block_id` (PK), `idx_blocks_doc (doc_id, …)`, `idx_blocks_type (repo_id,
50
+ * type)`; nodes `node_id` (PK), `idx_nodes_doc`, `idx_nodes_kind`,
51
+ * `idx_nodes_name`; edges `edge_id` (PK), `idx_edges_src (src_doc, …)`,
52
+ * `idx_edges_dst (dst_node, …)`; `$path` on a joined target reaches the docs
53
+ * `(repo_id, path)` key and then the target's `doc_id`/`src_doc` index. */
54
+ const COLUMN_PROBES = {
55
+ docs: { $id: "d.doc_id = ?", $path: "d.path = ?" },
56
+ blocks: { $id: "b.block_id = ?", $doc: "b.doc_id = ?", type: "b.type = ?", $path: "d.path = ?" },
57
+ nodes: { $id: "n.node_id = ?", $node_id: "n.node_id = ?", $doc_id: "n.doc_id = ?", kind: "n.kind = ?", name: "n.name = ?", $path: "d.path = ?" },
58
+ edges: {
59
+ $id: "e.edge_id = ?", $src: "e.src_doc = ?", $dst: "e.dst_node = ?", $path: "d.path = ?",
60
+ // `$dst_path` is the destination document's path (deleted or not — the
61
+ // intrinsic reads `docs` by id without a liveness guard). A destination is
62
+ // resolved within the edge's repo (spec/graph §3), where `(repo_id, path)`
63
+ // is UNIQUE, so the probe is that one document; `?r` is the repo id.
64
+ $dst_path: "e.dst_node IN (SELECT doc_id FROM docs WHERE repo_id = ?r AND path = ?)",
65
+ },
66
+ };
67
+ /** How a one-segment path on `target` is probed, or null when SQLite has no
68
+ * index for it (the engine then builds its own over the materialized scan). */
69
+ function probeFor(target, path) {
70
+ if (path.length !== 1)
71
+ return null;
72
+ const key = path[0];
73
+ const column = COLUMN_PROBES[target][key];
74
+ if (column)
75
+ return { kind: "column", sql: column };
76
+ if (target !== "docs")
77
+ return null;
78
+ // A docs property: any bare name the context reads through `docProp` — not a
79
+ // relation / handle (`out`, `frontmatter`, …), not a reserved basename (a loud
80
+ // error in memory, left to the scan to raise), not the `format` column (not
81
+ // indexed), and of the intrinsics only `$title` (a computed scalar property).
82
+ if (key === "$title")
83
+ return { kind: "property", key: "$title", source: "computed" };
84
+ if (key.startsWith("$") || key === "format")
85
+ return null;
86
+ if (NON_PROPERTY_NAMES.docs.has(key) || RESERVED_DOC_BASENAMES.has(key))
87
+ return null;
88
+ return { kind: "property", key, source: null };
89
+ }
90
+ // ---- the index ---------------------------------------------------------------------
91
+ class StoreIndex {
92
+ target;
93
+ path;
94
+ probe;
95
+ deps;
96
+ statements = new Map();
97
+ positions = null;
98
+ constructor(target, path, probe, deps) {
99
+ this.target = target;
100
+ this.path = path;
101
+ this.probe = probe;
102
+ this.deps = deps;
103
+ }
104
+ lookupRows(value) {
105
+ const v = value === undefined ? null : value;
106
+ if (v === null)
107
+ return this.fallback(null);
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
+ }
118
+ switch (typeof v) {
119
+ case "string": return this.property("p.type = 'string' AND p.val_text = ?", v);
120
+ case "number": return Number.isNaN(v) ? [] : this.property("p.type = 'number' AND p.val_num = ?", v);
121
+ case "boolean": return this.property("p.type = 'bool' AND p.val_bool = ?", v ? 1 : 0);
122
+ default: return []; // object, array, range, bigint, symbol: equals no decoded scalar
123
+ }
124
+ }
125
+ lookup(value) {
126
+ if (!this.positions) {
127
+ this.positions = new Map();
128
+ this.deps.rows().forEach((r, i) => this.positions.set(this.deps.identity(r), i));
129
+ }
130
+ const out = [];
131
+ for (const r of this.lookupRows(value)) {
132
+ const pos = this.positions.get(this.deps.identity(r));
133
+ if (pos !== undefined)
134
+ out.push(pos);
135
+ }
136
+ return out.sort((a, b) => a - b);
137
+ }
138
+ // A `$path` probe on a joined target drives from the document (`FROM_BY_DOC`),
139
+ // every other column probe from the target's own index. `?r` in a probe is
140
+ // the repo id, bound ahead of the value.
141
+ column(value) {
142
+ const t = this.target;
143
+ const probe = this.probe.sql;
144
+ const byDoc = t !== "docs" && this.path === "$path";
145
+ const g = byDoc ? guardsByDoc(t, this.deps.repoId) : guards(t, this.deps.repoId);
146
+ const from = byDoc ? FROM_BY_DOC[t] : FROM[t];
147
+ const params = probe.includes("?r") ? [this.deps.repoId, value] : [value];
148
+ const sql = `SELECT ${COLS[t]} FROM ${from} WHERE ${g.sql} AND ${probe.replace("?r", "?")} ORDER BY ${ORDER[t]}`;
149
+ return this.run(sql, [...g.params, ...params]);
150
+ }
151
+ // The scalar-in-scope rule as SQL: the document has exactly one live row for
152
+ // the key (within the source, for a sourced read) and that row is a scalar of
153
+ // the probe's type with the probe's value. The loop is driven from the
154
+ // properties index (`CROSS JOIN`: `idx_props_key_text` / `idx_props_key_num`,
155
+ // `(repo_id, key, val_*)`) and reaches the document by primary key; the count
156
+ // runs per candidate only, and with it at one no document repeats.
157
+ property(typed, bound) {
158
+ const { key, source } = this.probe;
159
+ const src = source === null ? "" : " AND p.source = ?";
160
+ const src2 = source === null ? "" : " AND p2.source = ?";
161
+ const g = guards("docs", this.deps.repoId);
162
+ const sql = `SELECT d.* FROM properties p CROSS JOIN docs d ON d.doc_id = p.doc_id
163
+ WHERE p.repo_id = ? AND p.key = ? AND ${typed} AND p.card = 'scalar' AND p.deleted_commit IS NULL${src}
164
+ AND ${g.sql}
165
+ AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = d.doc_id AND p2.key = ? AND p2.deleted_commit IS NULL${src2}) = 1
166
+ ORDER BY ${ORDER.docs}`;
167
+ const params = [this.deps.repoId, key, bound, ...(source === null ? [] : [source]), ...g.params, key, ...(source === null ? [] : [source])];
168
+ return this.run(sql, params);
169
+ }
170
+ run(sql, params) {
171
+ let stmt = this.statements.get(sql);
172
+ if (!stmt)
173
+ this.statements.set(sql, (stmt = this.deps.db.prepare(sql)));
174
+ return this.deps.tag(stmt.all(...params), this.target);
175
+ }
176
+ // The scan's own answer for a probe no statement covers.
177
+ fallback(value) {
178
+ return this.deps.rows().filter((r) => semantics.equals(this.deps.get(r, this.path), value));
179
+ }
180
+ }
181
+ /** A store-backed `RowIndex` for the root scan of `target` on `path`, or
182
+ * `undefined` when SQLite has no index for the path (per-target table above;
183
+ * multi-segment paths; `format`, edge `predicate`/`src_field`, `$tags`, …). */
184
+ export function storeIndexFor(target, path, deps) {
185
+ const probe = probeFor(target, path);
186
+ return probe ? new StoreIndex(target, path[0], probe, deps) : undefined;
187
+ }
188
+ /** The paths `storeIndexFor` answers from SQLite, per target (for tests and docs). */
189
+ export function indexablePaths(target) {
190
+ const cols = Object.keys(COLUMN_PROBES[target]);
191
+ return target === "docs" ? [...cols, "$title", "<property key>"] : cols;
192
+ }
193
+ //# sourceMappingURL=store-index.js.map
@@ -1,5 +1,5 @@
1
1
  export declare const SPEC_VERSIONS: {
2
- readonly oqx: "0.14";
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: "1.4";
12
- readonly cli: "1.2";
11
+ readonly surface: "2.0";
12
+ readonly cli: "2.0";
13
13
  };
14
14
  export type SpecName = keyof typeof SPEC_VERSIONS;
@@ -19,7 +19,7 @@ export const SPEC_VERSIONS = {
19
19
  search: "1.2",
20
20
  mutate: "1.3",
21
21
  sync: "1.3",
22
- surface: "1.4",
23
- cli: "1.2",
22
+ surface: "2.0",
23
+ cli: "2.0",
24
24
  };
25
25
  //# sourceMappingURL=spec-versions.js.map
@@ -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.7.1",
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.14.1",
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",