@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.
@@ -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 text(`${doc}.path`);
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 text(`${doc}.path`);
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 text(`${doc}.path`);
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 text(`${doc}.path`);
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
- if (NON_PROPERTY_NAMES[target].has(e.name))
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 text(`${doc}.path`);
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 (`$repo.docs`, `$repo.edges`,
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
- return typeof v === "string" ? this.column(v) : [];
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.15";
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.8.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.15.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",