@omgbase/core 0.10.0 → 0.11.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/README.md +10 -10
- package/dist/src/core/read/document.d.ts +18 -5
- package/dist/src/core/read/document.js +21 -7
- package/dist/src/core/read/nodes.d.ts +7 -7
- package/dist/src/core/read/nodes.js +5 -3
- package/dist/src/core/read/reader.d.ts +11 -7
- package/dist/src/core/read/reader.js +8 -10
- package/dist/src/format/adapter.d.ts +1 -1
- package/dist/src/graph/history.d.ts +20 -14
- package/dist/src/graph/history.js +12 -12
- package/dist/src/graph/inbound-links.d.ts +4 -4
- package/dist/src/graph/inbound-links.js +4 -4
- package/dist/src/graph/link-health.d.ts +8 -8
- package/dist/src/graph/link-health.js +6 -6
- package/dist/src/graph/links.d.ts +3 -3
- package/dist/src/graph/links.js +4 -4
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/mcp/arguments.d.ts +27 -0
- package/dist/src/mcp/arguments.js +147 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/graph.d.ts +16 -9
- package/dist/src/mcp/graph.js +58 -35
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +215 -121
- package/dist/src/mcp/server.d.ts +9 -1
- package/dist/src/mcp/server.js +359 -268
- package/dist/src/mutate/apply.d.ts +11 -4
- package/dist/src/mutate/apply.js +3 -2
- package/dist/src/mutate/docs.js +7 -7
- package/dist/src/mutate/macros.d.ts +4 -1
- package/dist/src/mutate/macros.js +1 -1
- package/dist/src/mutate/ops.js +2 -2
- package/dist/src/mutate/opset.d.ts +8 -5
- package/dist/src/mutate/plan-update.js +9 -8
- package/dist/src/oqx-js/context.d.ts +17 -4
- package/dist/src/oqx-js/context.js +268 -131
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +14 -15
- package/dist/src/oqx-js/root-row.d.ts +10 -3
- package/dist/src/oqx-js/root-row.js +18 -8
- package/dist/src/oqx-js/run.d.ts +6 -0
- package/dist/src/oqx-js/run.js +71 -24
- package/dist/src/oqx-js/sql/scan.js +7 -7
- package/dist/src/oqx-js/sql/translate.d.ts +12 -9
- package/dist/src/oqx-js/sql/translate.js +183 -75
- package/dist/src/oqx-js/store-index.d.ts +1 -1
- package/dist/src/oqx-js/store-index.js +28 -26
- package/dist/src/search/cel/ast.d.ts +1 -1
- package/dist/src/search/cel/compile.d.ts +1 -1
- package/dist/src/search/cel/compile.js +43 -70
- package/dist/src/search/cel/parser.js +1 -1
- package/dist/src/search/resolve.d.ts +2 -0
- package/dist/src/search/rrf.js +2 -2
- package/dist/src/search/text.d.ts +2 -2
- package/dist/src/search/text.js +1 -1
- package/dist/src/spec-versions.d.ts +6 -6
- package/dist/src/spec-versions.js +5 -5
- package/dist/src/surface-paths.d.ts +41 -8
- package/dist/src/surface-paths.js +34 -18
- package/dist/src/sync/admin.d.ts +2 -1
- package/dist/src/sync/admin.js +1 -1
- package/package.json +2 -2
package/dist/src/graph/links.js
CHANGED
|
@@ -13,7 +13,7 @@ export function docLinks(store, docId, opts = {}) {
|
|
|
13
13
|
.all(docId);
|
|
14
14
|
for (const r of rows)
|
|
15
15
|
if (inPred(r.predicate))
|
|
16
|
-
out.push({ predicate: r.predicate,
|
|
16
|
+
out.push({ predicate: r.predicate, doc_id: r.dst_node, kind: r.dst_kind, count: 1, block_id: r.src_block });
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
19
|
const rows = store.db
|
|
@@ -23,7 +23,7 @@ export function docLinks(store, docId, opts = {}) {
|
|
|
23
23
|
for (const r of rows) {
|
|
24
24
|
if (!inPred(r.predicate))
|
|
25
25
|
continue;
|
|
26
|
-
out.push({ predicate: r.predicate,
|
|
26
|
+
out.push({ predicate: r.predicate, doc_id: r.dst_node, kind: r.dst_kind, count: r.count, samples: JSON.parse(r.samples) });
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -35,7 +35,7 @@ export function docLinks(store, docId, opts = {}) {
|
|
|
35
35
|
.all(docId);
|
|
36
36
|
for (const r of rows)
|
|
37
37
|
if (inPred(r.predicate))
|
|
38
|
-
inbound.push({ predicate: r.predicate,
|
|
38
|
+
inbound.push({ predicate: r.predicate, doc_id: r.src_doc, kind: "document", count: 1, block_id: r.src_block });
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
41
|
const rows = store.db
|
|
@@ -45,7 +45,7 @@ export function docLinks(store, docId, opts = {}) {
|
|
|
45
45
|
.all(docId);
|
|
46
46
|
for (const r of rows)
|
|
47
47
|
if (inPred(r.predicate))
|
|
48
|
-
inbound.push({ predicate: r.predicate,
|
|
48
|
+
inbound.push({ predicate: r.predicate, doc_id: r.src_doc, kind: "document", count: r.cnt });
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
return { out, in: inbound };
|
package/dist/src/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type { FormatAdapter, AdapterEdge, ProjectedNode, EmbeddingChunk, Reconci
|
|
|
5
5
|
export { AdapterCapability } from "./format/adapter.js";
|
|
6
6
|
export { registerAdapter, adapterForFormat, adapterForPath, registeredFormats, detectFormat } from "./format/registry.js";
|
|
7
7
|
export { markdownAdapter, MARKDOWN_FORMAT } from "./format/markdown.js";
|
|
8
|
+
export { isContainerBlock } from "./format/index.js";
|
|
8
9
|
export { yamlAdapter, YAML_FORMAT } from "./format/yaml.js";
|
|
9
10
|
export { jsonAdapter, JSON_FORMAT } from "./format/json.js";
|
|
10
11
|
export { writeDocNodes, deleteDocNodes } from "./core/store/nodes.js";
|
|
@@ -34,7 +35,7 @@ export { reposStatus, syncStatus, reposList, type RepoStatus, type SyncStatus, t
|
|
|
34
35
|
export { docsOutline, type OutlineResult, type OutlineOptions } from "./core/read/outline.js";
|
|
35
36
|
export { docsRead, reconstructContent, type DocsReadResult, type DocsReadOptions } from "./core/read/document.js";
|
|
36
37
|
export { nodesGet, nodesGetMany, type GetNode, type Resolution } from "./core/read/nodes.js";
|
|
37
|
-
export { findDoc, loadDocBlocks, blockRaw, docsList, docsTree, normalizeTreePrefix, DOCS_LIST_DEFAULT_LIMIT, DOCS_TREE_DEFAULT_LIMIT, type DocInfo, type BlockNode, type DocListRow, type DocListPage, type DocListOptions, type DocTreeEntry, type DocTreePage, type DocTreeOptions, } from "./core/read/reader.js";
|
|
38
|
+
export { findDoc, findDocByRef, loadDocBlocks, blockRaw, docsList, docsTree, normalizeTreePrefix, DOCS_LIST_DEFAULT_LIMIT, DOCS_TREE_DEFAULT_LIMIT, type DocInfo, type BlockNode, type DocListRow, type DocListPage, type DocListOptions, type DocTreeEntry, type DocTreePage, type DocTreeOptions, } from "./core/read/reader.js";
|
|
38
39
|
export { CursorInvalid, encodeCursor, decodeCursor, decodePathCursor } from "./core/cursor.js";
|
|
39
40
|
export { referencePath, storagePath, referencePathOrNull, referenceKeyed } from "./core/paths.js";
|
|
40
41
|
export * from "./surface-paths.js";
|
package/dist/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { SPEC_VERSIONS } from "./spec-versions.js";
|
|
|
4
4
|
export { AdapterCapability } from "./format/adapter.js";
|
|
5
5
|
export { registerAdapter, adapterForFormat, adapterForPath, registeredFormats, detectFormat } from "./format/registry.js";
|
|
6
6
|
export { markdownAdapter, MARKDOWN_FORMAT } from "./format/markdown.js";
|
|
7
|
+
export { isContainerBlock } from "./format/index.js";
|
|
7
8
|
export { yamlAdapter, YAML_FORMAT } from "./format/yaml.js";
|
|
8
9
|
export { jsonAdapter, JSON_FORMAT } from "./format/json.js";
|
|
9
10
|
export { writeDocNodes, deleteDocNodes } from "./core/store/nodes.js";
|
|
@@ -35,7 +36,7 @@ export { reposStatus, syncStatus, reposList } from "./sync/admin.js";
|
|
|
35
36
|
export { docsOutline } from "./core/read/outline.js";
|
|
36
37
|
export { docsRead, reconstructContent } from "./core/read/document.js";
|
|
37
38
|
export { nodesGet, nodesGetMany } from "./core/read/nodes.js";
|
|
38
|
-
export { findDoc, loadDocBlocks, blockRaw, docsList, docsTree, normalizeTreePrefix, DOCS_LIST_DEFAULT_LIMIT, DOCS_TREE_DEFAULT_LIMIT, } from "./core/read/reader.js";
|
|
39
|
+
export { findDoc, findDocByRef, loadDocBlocks, blockRaw, docsList, docsTree, normalizeTreePrefix, DOCS_LIST_DEFAULT_LIMIT, DOCS_TREE_DEFAULT_LIMIT, } from "./core/read/reader.js";
|
|
39
40
|
export { CursorInvalid, encodeCursor, decodeCursor, decodePathCursor } from "./core/cursor.js";
|
|
40
41
|
export { referencePath, storagePath, referencePathOrNull, referenceKeyed } from "./core/paths.js";
|
|
41
42
|
export * from "./surface-paths.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { EngineError } from "./errors.js";
|
|
3
|
+
/** One tool's argument shape: the zod raw shape it was registered with. */
|
|
4
|
+
export type ArgShape = Record<string, z.ZodTypeAny>;
|
|
5
|
+
/** The `(tool takes …)` tail: names in bytewise order, `?` on the optional ones. */
|
|
6
|
+
export declare function argumentsHint(tool: string, shape: ArgShape): string;
|
|
7
|
+
/** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape. */
|
|
8
|
+
export declare function argumentError(tool: string, message: string, shape: ArgShape): EngineError;
|
|
9
|
+
/**
|
|
10
|
+
* Validate `raw` against `shape`: unknown names first (in bytewise order), then
|
|
11
|
+
* each declared argument (bytewise order) for presence and type. Returns the
|
|
12
|
+
* arguments with any `undefined` members dropped; throws the first failure.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateArguments(tool: string, shape: ArgShape, raw: unknown): Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* What to say about an argument name the tool does not take. The names 3.0
|
|
17
|
+
* retired are called out with their replacement (`doc` → `id`/`path`,
|
|
18
|
+
* `block`/`node` → `id`, `heading` → `section`,
|
|
19
|
+
* `markdown` → `text`) when the tool has the replacement; otherwise the
|
|
20
|
+
* nearest valid name (Levenshtein ≤ 2, or one a prefix of the other).
|
|
21
|
+
*/
|
|
22
|
+
export declare function unknownArgumentMessage(key: string, shape: ArgShape): string;
|
|
23
|
+
/** The nearest candidate: Levenshtein distance ≤ 2, or a prefix relation either way; ties → the bytewise-first. */
|
|
24
|
+
export declare function nearest(name: string, candidates: readonly string[]): string | null;
|
|
25
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
26
|
+
/** The expected-type phrase of an argument, as the Rust catalog spells it from its JSON schema. */
|
|
27
|
+
export declare function phraseFor(schema: z.ZodTypeAny): string;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { EngineError } from "./errors.js";
|
|
3
|
+
/** The `(tool takes …)` tail: names in bytewise order, `?` on the optional ones. */
|
|
4
|
+
export function argumentsHint(tool, shape) {
|
|
5
|
+
const names = Object.keys(shape).sort(cmpBytes);
|
|
6
|
+
if (names.length === 0)
|
|
7
|
+
return `${tool} takes no arguments`;
|
|
8
|
+
return `${tool} takes ${names.map((n) => (isOptional(shape[n]) ? `${n}?` : n)).join(", ")}`;
|
|
9
|
+
}
|
|
10
|
+
/** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape. */
|
|
11
|
+
export function argumentError(tool, message, shape) {
|
|
12
|
+
const full = `${message} (${argumentsHint(tool, shape)})`;
|
|
13
|
+
return new EngineError("filter_invalid", full, { data: { reason: full, hint: "arguments" } });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validate `raw` against `shape`: unknown names first (in bytewise order), then
|
|
17
|
+
* each declared argument (bytewise order) for presence and type. Returns the
|
|
18
|
+
* arguments with any `undefined` members dropped; throws the first failure.
|
|
19
|
+
*/
|
|
20
|
+
export function validateArguments(tool, shape, raw) {
|
|
21
|
+
if (raw === undefined || raw === null)
|
|
22
|
+
raw = {};
|
|
23
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
24
|
+
throw argumentError(tool, "arguments must be an object", shape);
|
|
25
|
+
const args = raw;
|
|
26
|
+
for (const key of Object.keys(args).sort(cmpBytes)) {
|
|
27
|
+
if (!(key in shape))
|
|
28
|
+
throw argumentError(tool, unknownArgumentMessage(key, shape), shape);
|
|
29
|
+
}
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const key of Object.keys(shape).sort(cmpBytes)) {
|
|
32
|
+
const schema = shape[key];
|
|
33
|
+
const value = args[key];
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
if (!isOptional(schema))
|
|
36
|
+
throw argumentError(tool, `\`${key}\` is required`, shape);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const parsed = schema.safeParse(value);
|
|
40
|
+
if (!parsed.success)
|
|
41
|
+
throw argumentError(tool, issueMessage(key, schema, parsed.error.issues), shape);
|
|
42
|
+
// The value as sent, not zod's copy: the schemas here coerce nothing, and
|
|
43
|
+
// the kernel-op grammar (`apply.ops`) keeps keys the schema does not name.
|
|
44
|
+
out[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/** The first failure, spelled for the argument: a top-level type mismatch is
|
|
49
|
+
* "`key` must be <phrase>"; a failure inside a nested shape (`ops[0].block`)
|
|
50
|
+
* names the path — "is required" when the member is absent, else "must be
|
|
51
|
+
* <zod's expected type>". Nested wording is the reference's own (the Rust
|
|
52
|
+
* catalog checks nested shapes in its handlers); the top-level form is pinned. */
|
|
53
|
+
function issueMessage(key, schema, issues) {
|
|
54
|
+
const issue = issues[0];
|
|
55
|
+
if (!issue || issue.path.length === 0)
|
|
56
|
+
return `\`${key}\` must be ${phraseFor(schema)}`;
|
|
57
|
+
const at = key + issue.path.map((p) => (typeof p === "number" ? `[${p}]` : `.${String(p)}`)).join("");
|
|
58
|
+
if (issue.code === "invalid_type" && issue.input === undefined)
|
|
59
|
+
return `\`${at}\` is required`;
|
|
60
|
+
const expected = { string: "a string", number: "an integer", boolean: "a boolean", object: "an object", array: "an array" };
|
|
61
|
+
return `\`${at}\` must be ${issue.expected !== undefined ? (expected[issue.expected] ?? issue.expected) : "valid"}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What to say about an argument name the tool does not take. The names 3.0
|
|
65
|
+
* retired are called out with their replacement (`doc` → `id`/`path`,
|
|
66
|
+
* `block`/`node` → `id`, `heading` → `section`,
|
|
67
|
+
* `markdown` → `text`) when the tool has the replacement; otherwise the
|
|
68
|
+
* nearest valid name (Levenshtein ≤ 2, or one a prefix of the other).
|
|
69
|
+
*/
|
|
70
|
+
export function unknownArgumentMessage(key, shape) {
|
|
71
|
+
const has = (n) => n in shape;
|
|
72
|
+
const retired = (replacement) => `\`${key}\` is no longer an argument — pass ${replacement}`;
|
|
73
|
+
if (key === "doc" && has("id") && has("path"))
|
|
74
|
+
return retired("`id` or `path`");
|
|
75
|
+
if ((key === "block" || key === "node") && has("id"))
|
|
76
|
+
return retired("`id`");
|
|
77
|
+
if (key === "heading" && has("section"))
|
|
78
|
+
return retired("`section` (the heading's text), or blocks_append with the heading's `id`");
|
|
79
|
+
if (key === "markdown" && has("text"))
|
|
80
|
+
return retired("`text`");
|
|
81
|
+
const near = nearest(key, Object.keys(shape));
|
|
82
|
+
return near === null ? `unknown argument \`${key}\`` : `unknown argument \`${key}\`; did you mean \`${near}\`?`;
|
|
83
|
+
}
|
|
84
|
+
/** The nearest candidate: Levenshtein distance ≤ 2, or a prefix relation either way; ties → the bytewise-first. */
|
|
85
|
+
export function nearest(name, candidates) {
|
|
86
|
+
let best = null;
|
|
87
|
+
for (const c of [...candidates].sort(cmpBytes)) {
|
|
88
|
+
const d = levenshtein(name, c);
|
|
89
|
+
const prefix = name.length > 0 && c.length > 0 && (c.startsWith(name) || name.startsWith(c));
|
|
90
|
+
if (d > 2 && !prefix)
|
|
91
|
+
continue;
|
|
92
|
+
const score = prefix && d > 2 ? 3 : d;
|
|
93
|
+
if (best === null || score < best.score)
|
|
94
|
+
best = { name: c, score };
|
|
95
|
+
}
|
|
96
|
+
return best?.name ?? null;
|
|
97
|
+
}
|
|
98
|
+
export function levenshtein(a, b) {
|
|
99
|
+
const m = a.length;
|
|
100
|
+
const n = b.length;
|
|
101
|
+
if (m === 0)
|
|
102
|
+
return n;
|
|
103
|
+
if (n === 0)
|
|
104
|
+
return m;
|
|
105
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
106
|
+
for (let i = 1; i <= m; i++) {
|
|
107
|
+
const cur = [i];
|
|
108
|
+
for (let j = 1; j <= n; j++) {
|
|
109
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
110
|
+
}
|
|
111
|
+
prev = cur;
|
|
112
|
+
}
|
|
113
|
+
return prev[n];
|
|
114
|
+
}
|
|
115
|
+
function cmpBytes(a, b) {
|
|
116
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
117
|
+
}
|
|
118
|
+
function defOf(schema) {
|
|
119
|
+
return schema._zod.def;
|
|
120
|
+
}
|
|
121
|
+
function isOptional(schema) {
|
|
122
|
+
const d = defOf(schema);
|
|
123
|
+
return d.type === "optional" || (d.type === "nullable" && isOptional(d.innerType));
|
|
124
|
+
}
|
|
125
|
+
function unwrap(schema) {
|
|
126
|
+
const d = defOf(schema);
|
|
127
|
+
return d.type === "optional" || d.type === "nullable" ? unwrap(d.innerType) : schema;
|
|
128
|
+
}
|
|
129
|
+
/** The expected-type phrase of an argument, as the Rust catalog spells it from its JSON schema. */
|
|
130
|
+
export function phraseFor(schema) {
|
|
131
|
+
const d = defOf(unwrap(schema));
|
|
132
|
+
switch (d.type) {
|
|
133
|
+
case "string": return "a string";
|
|
134
|
+
case "number": return "an integer";
|
|
135
|
+
case "boolean": return "a boolean";
|
|
136
|
+
case "object":
|
|
137
|
+
case "record": return "an object";
|
|
138
|
+
case "enum": return `one of ${Object.values(d.entries ?? {}).map(String).join(", ")}`;
|
|
139
|
+
case "array": {
|
|
140
|
+
const inner = defOf(unwrap(d.element)).type;
|
|
141
|
+
return `an array of ${inner === "string" ? "strings" : inner === "number" ? "integers" : "objects"}`;
|
|
142
|
+
}
|
|
143
|
+
case "union": return "\"start\", \"end\", {before} or {after}";
|
|
144
|
+
default: return "valid";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=arguments.js.map
|
package/dist/src/mcp/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ErrorCode = "stale_expectation" | "parent_missing" | "target_missing" | "block_missing" | "doc_missing" | "cycle_move" | "opaque_block" | "not_contiguous" | "type_mismatch" | "conflicted_document" | "path_taken" | "create_conflict" | "ambiguous_locator" | "ambiguous_heading" | "filter_invalid" | "budget_exceeded" | "
|
|
1
|
+
export type ErrorCode = "stale_expectation" | "parent_missing" | "target_missing" | "block_missing" | "doc_missing" | "cycle_move" | "opaque_block" | "not_contiguous" | "type_mismatch" | "conflicted_document" | "path_taken" | "create_conflict" | "ambiguous_locator" | "ambiguous_heading" | "filter_invalid" | "budget_exceeded" | "provider_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved" | "block_not_container";
|
|
2
2
|
export interface EngineErrorBody {
|
|
3
3
|
error: ErrorCode;
|
|
4
4
|
message: string;
|
package/dist/src/mcp/graph.d.ts
CHANGED
|
@@ -11,25 +11,32 @@ export interface GraphArgs {
|
|
|
11
11
|
/** restrict the neighborhood to documents reachable from a root via edges of
|
|
12
12
|
* this predicate, within `degrees` (both the docs and the edges are filtered). */
|
|
13
13
|
predicate?: string | undefined;
|
|
14
|
-
/** extra document projections (OQX select expressions, e.g. "layer", "
|
|
14
|
+
/** extra document projections (OQX select expressions, e.g. "layer", "path"). */
|
|
15
15
|
select?: string[] | undefined;
|
|
16
16
|
/** cap on the distinct documents returned (default 200). */
|
|
17
17
|
max_documents?: number | undefined;
|
|
18
18
|
}
|
|
19
|
-
/** A traversed edge
|
|
19
|
+
/** A traversed edge: a row of the `links` target under its own field names
|
|
20
|
+
* (spec/surface §1.2, 3.0 — `id` the edge, `doc_id`/`block_id` the source,
|
|
21
|
+
* `via_id`, `dst_id` the destination node, `dst_path`/`uri` its path or URI,
|
|
22
|
+
* `field` the authoring key). No rename table: what the walk projects is what
|
|
23
|
+
* the result carries. */
|
|
20
24
|
export interface GraphEdge {
|
|
21
25
|
id: string;
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
doc_id: string;
|
|
27
|
+
block_id: string | null;
|
|
28
|
+
via_id: string | null;
|
|
29
|
+
dst_id: string;
|
|
24
30
|
dst_path: string | null;
|
|
25
|
-
|
|
31
|
+
uri: string | null;
|
|
26
32
|
dst_kind: string;
|
|
27
33
|
predicate: string;
|
|
28
34
|
provenance: string;
|
|
29
35
|
anchor: string | null;
|
|
30
|
-
|
|
36
|
+
field: string | null;
|
|
31
37
|
}
|
|
32
|
-
/** A reached document: id
|
|
38
|
+
/** A reached document: `id`/`path` (the row's own names, as a `query` hit)
|
|
39
|
+
* + walk metadata + any requested projections. */
|
|
33
40
|
export interface GraphDoc {
|
|
34
41
|
id: string;
|
|
35
42
|
path: string;
|
|
@@ -44,7 +51,7 @@ export interface GraphResult {
|
|
|
44
51
|
degrees: number;
|
|
45
52
|
direction: GraphDirection;
|
|
46
53
|
documents: GraphDoc[];
|
|
47
|
-
|
|
54
|
+
links: GraphEdge[];
|
|
48
55
|
frontier: {
|
|
49
56
|
id: string;
|
|
50
57
|
path: string;
|
|
@@ -57,7 +64,7 @@ export interface GraphResult {
|
|
|
57
64
|
/**
|
|
58
65
|
* Run the `graph` neighborhood macro: compile the args into OQX `follow`
|
|
59
66
|
* query(ies), execute via the shared runner, and shape the walk output into
|
|
60
|
-
* { documents,
|
|
67
|
+
* { documents, links, frontier }. Throws EngineError (doc_missing/target_missing)
|
|
61
68
|
* for an empty or unresolvable roots list — never a silent empty result.
|
|
62
69
|
*/
|
|
63
70
|
export declare function graphNeighborhood(store: Store, repoId: string, args: GraphArgs, embedQuery?: EmbedQuery): Promise<GraphResult>;
|
package/dist/src/mcp/graph.js
CHANGED
|
@@ -2,27 +2,30 @@
|
|
|
2
2
|
//
|
|
3
3
|
// ADR-006: OQX `follow` is the ONE traversal engine; the removed
|
|
4
4
|
// graph_traverse/path/subgraph tools are not resurrected. This macro owns NO
|
|
5
|
-
// graph-walking logic — it compiles its arguments into an OQX `follow
|
|
6
|
-
// / `
|
|
5
|
+
// graph-walking logic — it compiles its arguments into an OQX `follow out_links
|
|
6
|
+
// collect { from dst }` / `follow in_links collect { from doc }` query (the
|
|
7
|
+
// exact walk the `query` tool exposes over the link rows),
|
|
7
8
|
// runs it through the shared runner (oqxRunAsync), and then only SHAPES the
|
|
8
9
|
// walk's own output (its `$depth`/`$stop` intrinsics and projected edges) into
|
|
9
|
-
// the { documents,
|
|
10
|
+
// the { documents, links, frontier } envelope. Every hop, cycle-guard and depth
|
|
10
11
|
// bound is `follow`'s; nothing here re-implements BFS/DFS.
|
|
11
12
|
//
|
|
12
13
|
// Mapping (args → follow query):
|
|
13
|
-
// roots → the seed `where
|
|
14
|
-
// degrees → `follow … { depth degrees+1 }` (seed is $depth 1, so N hops = depth N+1)
|
|
15
|
-
// direction → `follow
|
|
14
|
+
// roots → the seed `where id == … or …` (refs resolved to doc ids first; literals in the AST, never spliced text)
|
|
15
|
+
// degrees → `follow … collect { … depth degrees+1 }` (seed is $depth 1, so N hops = depth N+1)
|
|
16
|
+
// direction → `follow out_links collect { from dst }` (the linked documents)
|
|
17
|
+
// | `follow in_links collect { from doc }` (the linking documents)
|
|
18
|
+
// | both (two walks, unioned)
|
|
16
19
|
// predicate → restricts the neighborhood to docs reachable via that predicate
|
|
17
20
|
// (recomputed over the walk's collected edges; no `via` clause)
|
|
18
21
|
// select → extra doc projections spliced into the walk's `select`
|
|
19
22
|
// max_documents → a post-walk cap on the distinct document set
|
|
20
23
|
//
|
|
21
|
-
// Edges are surfaced by projecting the reached doc's `doc.
|
|
24
|
+
// Edges are surfaced by projecting the reached doc's `doc.out_links`/`doc.in_links`
|
|
22
25
|
// in the SAME walk (a select-position collect), then keeping the induced-subgraph
|
|
23
26
|
// edges (both endpoints in the neighborhood) plus dangling stubs to external/
|
|
24
27
|
// phantom targets — so external (`x_…`) and phantom endpoints are never dropped,
|
|
25
|
-
// exactly as `from
|
|
28
|
+
// exactly as `from links` surfaces them.
|
|
26
29
|
import { build, parse, print, OqxError } from "@omgbase/oqx";
|
|
27
30
|
import { findDocByRef } from "../core/read/reader.js";
|
|
28
31
|
import { oqxRunAsync } from "../oqx/run.js";
|
|
@@ -38,21 +41,30 @@ function cmpBytes(a, b) {
|
|
|
38
41
|
// Build the extra `select` items for caller projections. Each expression gets a
|
|
39
42
|
// stable internal alias (_u0, _u1, …) so it can never collide with a reserved
|
|
40
43
|
// word or the macro's own aliases; the clean output name is derived separately.
|
|
44
|
+
// A bare-name selection (`layer`, `title`) is its own output key, exactly as a
|
|
45
|
+
// `query` hit projects it (spec/surface §1.4, 3.0) — so `id`/`path` collapse
|
|
46
|
+
// onto the envelope keys (the engine reads the same value) and any other name
|
|
47
|
+
// is an ordinary column, deduped by suffix. Anything else (an expression) is
|
|
48
|
+
// `sel_<i>`, so a projection can never overwrite the envelope here.
|
|
41
49
|
function buildUserSelect(select) {
|
|
42
50
|
const items = [];
|
|
43
51
|
const outNames = [];
|
|
44
52
|
if (!select || select.length === 0)
|
|
45
53
|
return { items, outNames };
|
|
46
|
-
const used = new Set();
|
|
54
|
+
const used = new Set(["id", "path"]);
|
|
47
55
|
select.forEach((expr, i) => {
|
|
48
56
|
const trimmed = expr.trim();
|
|
49
57
|
if (!trimmed)
|
|
50
58
|
return;
|
|
51
|
-
const m =
|
|
59
|
+
const m = /^(\$?[A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
52
60
|
let name = m ? m[1] : `sel_${i}`;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
// The envelope keys are the row's own identity: an `id`/`path` selection
|
|
62
|
+
// lands on them rather than beside them (the engine reads the same value).
|
|
63
|
+
if (name !== "id" && name !== "path") {
|
|
64
|
+
while (used.has(name))
|
|
65
|
+
name = `${name}_${i}`;
|
|
66
|
+
used.add(name);
|
|
67
|
+
}
|
|
56
68
|
outNames[i] = name;
|
|
57
69
|
items.push(...userItems(trimmed, `_u${i}`));
|
|
58
70
|
});
|
|
@@ -74,39 +86,47 @@ function userItems(src, alias) {
|
|
|
74
86
|
}
|
|
75
87
|
return q.select;
|
|
76
88
|
}
|
|
77
|
-
// The edge fields projected from doc.
|
|
78
|
-
//
|
|
79
|
-
//
|
|
89
|
+
// The edge fields projected from doc.out_links/doc.in_links — the `links`
|
|
90
|
+
// target's own names, projected under themselves (spec/surface §1.2, 3.0: no
|
|
91
|
+
// rename between the language and the result) — identical for both directions
|
|
92
|
+
// so a single filter handles either scan (an edge is a directional fact
|
|
93
|
+
// regardless of which endpoint the walk reached it from).
|
|
80
94
|
const EDGE_FIELDS = [
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
"id", "doc_id", "block_id", "via_id", "dst_id", "dst_path", "uri",
|
|
96
|
+
"dst_kind", "predicate", "provenance", "anchor", "field",
|
|
83
97
|
];
|
|
84
98
|
// The walk, as an AST (spec/oqx/AST.md §7) printed canonically — never spliced
|
|
85
99
|
// strings: the seed ids are literals, the caller's projections parsed items.
|
|
86
100
|
//
|
|
87
|
-
// select _depth: $depth, _stop: $stop,
|
|
88
|
-
// from docs where
|
|
101
|
+
// select _depth: $depth, _stop: $stop, _links: doc.<dir>_links collect { <edge fields> } <user items>
|
|
102
|
+
// from docs where id == <root> or … follow distinct <dir>_links collect { from <dst | doc> depth <depth> }
|
|
103
|
+
//
|
|
104
|
+
// The destination block walks the link rows to the documents at their other
|
|
105
|
+
// end (spec/surface §1.2, 3.0 follow-up: there are no `out`/`in` relations —
|
|
106
|
+
// `dst` is a link's destination document, `doc` its source; a `from` over an
|
|
107
|
+
// absent `dst` — an external or phantom target — contributes no successor).
|
|
89
108
|
function buildQuery(rootIds, dir, depth, userSelect) {
|
|
90
|
-
const edges = build.op(build.path("doc", `${dir}
|
|
91
|
-
select: EDGE_FIELDS.map((
|
|
109
|
+
const edges = build.op(build.path("doc", `${dir}_links`), "collect", build.subquery({
|
|
110
|
+
select: EDGE_FIELDS.map((name) => build.field(name, build.ident(name))),
|
|
92
111
|
}));
|
|
93
|
-
const seed = rootIds.map((id) => build.scalar(build.binary("==", build.ident("
|
|
112
|
+
const seed = rootIds.map((id) => build.scalar(build.binary("==", build.ident("id"), build.lit(id))));
|
|
94
113
|
const q = build.query(build.ident("docs"), {
|
|
95
114
|
select: [
|
|
96
115
|
build.field("_depth", build.ident("$depth")),
|
|
97
116
|
build.field("_stop", build.ident("$stop")),
|
|
98
|
-
build.collect("
|
|
117
|
+
build.collect("_links", edges),
|
|
99
118
|
...userSelect,
|
|
100
119
|
],
|
|
101
120
|
where: seed.length === 1 ? seed[0] : build.or(seed),
|
|
102
|
-
|
|
121
|
+
// `depth` is the destination block's own clause (oqx 0.20).
|
|
122
|
+
follow: build.follow([build.op(build.ident(`${dir}_links`), "collect", build.subquery({ from: [build.ident(dir === "out" ? "dst" : "doc")], depth }))], { distinct: true }),
|
|
103
123
|
});
|
|
104
124
|
return print(q);
|
|
105
125
|
}
|
|
106
126
|
/**
|
|
107
127
|
* Run the `graph` neighborhood macro: compile the args into OQX `follow`
|
|
108
128
|
* query(ies), execute via the shared runner, and shape the walk output into
|
|
109
|
-
* { documents,
|
|
129
|
+
* { documents, links, frontier }. Throws EngineError (doc_missing/target_missing)
|
|
110
130
|
* for an empty or unresolvable roots list — never a silent empty result.
|
|
111
131
|
*/
|
|
112
132
|
export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
@@ -145,18 +165,21 @@ export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
|
145
165
|
for (const hit of res.hits) {
|
|
146
166
|
const hopDepth = Number(hit._depth); // $depth: seed = 1
|
|
147
167
|
const degree = hopDepth - 1; // root = 0
|
|
168
|
+
// The walk's hits carry `id`/`path` (spec/surface §1.4, 3.0) and so
|
|
169
|
+
// does `graph`'s document: one notation for a document everywhere.
|
|
148
170
|
const prev = docMap.get(hit.id);
|
|
149
171
|
if (!prev || degree < prev.degree) {
|
|
150
172
|
const doc = { id: hit.id, path: hit.path, degree, frontier: false };
|
|
151
173
|
outNames.forEach((name, i) => {
|
|
152
|
-
|
|
174
|
+
// An `id`/`path` selection is the envelope key itself (same value).
|
|
175
|
+
if (name !== undefined && name !== "id" && name !== "path")
|
|
153
176
|
doc[name] = hit[`_u${i}`] ?? null;
|
|
154
177
|
});
|
|
155
178
|
docMap.set(hit.id, doc);
|
|
156
179
|
}
|
|
157
180
|
// Gather this node's edges; dedup by edge id (an a→b edge is reachable as
|
|
158
181
|
// a's out-edge AND b's in-edge in "both" mode).
|
|
159
|
-
const raw = hit.
|
|
182
|
+
const raw = hit._links ?? [];
|
|
160
183
|
for (const e of raw)
|
|
161
184
|
if (!edgeMap.has(e.id))
|
|
162
185
|
edgeMap.set(e.id, e);
|
|
@@ -176,9 +199,9 @@ export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
|
176
199
|
if (e.predicate !== P)
|
|
177
200
|
continue;
|
|
178
201
|
if (dirs.includes("out"))
|
|
179
|
-
link(e.
|
|
202
|
+
link(e.doc_id, e.dst_id); // forward
|
|
180
203
|
if (dirs.includes("in"))
|
|
181
|
-
link(e.
|
|
204
|
+
link(e.dst_id, e.doc_id); // backward
|
|
182
205
|
}
|
|
183
206
|
const depthOf = new Map(rootIds.map((id) => [id, 0]));
|
|
184
207
|
let wave = [...rootIds];
|
|
@@ -223,26 +246,26 @@ export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
|
223
246
|
}
|
|
224
247
|
// Keep the induced-subgraph edges (both endpoints in the neighborhood) plus
|
|
225
248
|
// dangling stubs to external/phantom targets, so external (x_…) and phantom
|
|
226
|
-
// endpoints survive the way `from
|
|
249
|
+
// endpoints survive the way `from links` surfaces them. A `predicate` filter
|
|
227
250
|
// keeps only that predicate's edges (the neighborhood was already restricted to
|
|
228
251
|
// predicate-reachable docs above).
|
|
229
252
|
const edges = [];
|
|
230
253
|
for (const e of edgeMap.values()) {
|
|
231
254
|
if (args.predicate && e.predicate !== args.predicate)
|
|
232
255
|
continue;
|
|
233
|
-
const srcIn = reached.has(e.
|
|
256
|
+
const srcIn = reached.has(e.doc_id);
|
|
234
257
|
const dstDangling = e.dst_kind === "external" || (e.dst_kind === "document" && e.dst_path === null);
|
|
235
|
-
const dstIn = reached.has(e.
|
|
258
|
+
const dstIn = reached.has(e.dst_id) || dstDangling;
|
|
236
259
|
if (srcIn && dstIn)
|
|
237
260
|
edges.push(e);
|
|
238
261
|
}
|
|
239
|
-
edges.sort((a, b) => cmpBytes(a.
|
|
262
|
+
edges.sort((a, b) => cmpBytes(a.doc_id, b.doc_id) || cmpBytes(a.id, b.id));
|
|
240
263
|
return {
|
|
241
264
|
roots: rootIds,
|
|
242
265
|
degrees: effectiveDegrees,
|
|
243
266
|
direction,
|
|
244
267
|
documents,
|
|
245
|
-
edges,
|
|
268
|
+
links: edges,
|
|
246
269
|
frontier,
|
|
247
270
|
truncated,
|
|
248
271
|
queries,
|