@omgbase/core 0.27.1 → 0.29.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/read/document.d.ts +11 -10
- package/dist/src/core/read/document.js +12 -11
- package/dist/src/core/read/nodes.d.ts +10 -10
- package/dist/src/core/read/nodes.js +11 -11
- package/dist/src/core/read/reader.d.ts +37 -24
- package/dist/src/core/read/reader.js +44 -31
- package/dist/src/graph/history.d.ts +25 -22
- package/dist/src/graph/history.js +17 -17
- package/dist/src/graph/inbound-links.d.ts +7 -7
- package/dist/src/graph/inbound-links.js +7 -7
- package/dist/src/graph/link-health.d.ts +9 -9
- package/dist/src/graph/link-health.js +9 -9
- package/dist/src/graph/links.d.ts +3 -3
- package/dist/src/graph/links.js +4 -4
- package/dist/src/mcp/arguments.d.ts +10 -3
- package/dist/src/mcp/arguments.js +32 -9
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/graph.d.ts +22 -22
- package/dist/src/mcp/graph.js +53 -46
- package/dist/src/mcp/reference.d.ts +29 -1
- package/dist/src/mcp/reference.js +350 -222
- package/dist/src/mcp/server.d.ts +9 -0
- package/dist/src/mcp/server.js +169 -107
- package/dist/src/mutate/apply.d.ts +6 -6
- package/dist/src/mutate/apply.js +1 -1
- package/dist/src/mutate/canonical.d.ts +1 -1
- package/dist/src/mutate/canonical.js +2 -2
- package/dist/src/mutate/commit.d.ts +3 -3
- package/dist/src/mutate/commit.js +4 -4
- package/dist/src/mutate/docs.d.ts +1 -1
- package/dist/src/mutate/docs.js +8 -8
- package/dist/src/mutate/effects.d.ts +17 -17
- package/dist/src/mutate/effects.js +113 -113
- package/dist/src/mutate/macros.d.ts +5 -5
- package/dist/src/mutate/macros.js +1 -1
- package/dist/src/mutate/materialize.js +2 -2
- package/dist/src/mutate/ops.js +2 -2
- package/dist/src/mutate/opset.d.ts +7 -7
- package/dist/src/mutate/plan-update.js +2 -2
- package/dist/src/mutate/plan.d.ts +8 -8
- package/dist/src/mutate/plan.js +25 -25
- package/dist/src/mutate/select.d.ts +4 -4
- package/dist/src/mutate/select.js +52 -52
- package/dist/src/mutate/sugar.d.ts +10 -10
- package/dist/src/mutate/sugar.js +21 -21
- package/dist/src/oqx-js/context.d.ts +8 -4
- package/dist/src/oqx-js/context.js +206 -189
- package/dist/src/oqx-js/names.d.ts +28 -17
- package/dist/src/oqx-js/names.js +115 -78
- package/dist/src/oqx-js/planner.d.ts +4 -8
- package/dist/src/oqx-js/planner.js +6 -10
- package/dist/src/oqx-js/root-row.d.ts +6 -5
- package/dist/src/oqx-js/root-row.js +15 -16
- package/dist/src/oqx-js/run.d.ts +18 -17
- package/dist/src/oqx-js/run.js +57 -54
- package/dist/src/oqx-js/sql/translate.d.ts +5 -5
- package/dist/src/oqx-js/sql/translate.js +162 -161
- package/dist/src/oqx-js/store-index.d.ts +1 -1
- package/dist/src/oqx-js/store-index.js +28 -30
- package/dist/src/search/resolve.d.ts +2 -2
- package/dist/src/search/resolve.js +1 -1
- package/dist/src/search/rrf.js +2 -2
- package/dist/src/search/text.d.ts +5 -5
- package/dist/src/search/text.js +2 -2
- package/dist/src/spec-versions.d.ts +6 -6
- package/dist/src/spec-versions.js +6 -6
- package/dist/src/surface-paths.d.ts +18 -17
- package/dist/src/surface-paths.js +23 -21
- package/dist/src/sync/admin.d.ts +2 -2
- package/dist/src/sync/admin.js +1 -1
- package/package.json +1 -1
|
@@ -2,14 +2,14 @@ import type { Store } from "../core/store/store.js";
|
|
|
2
2
|
export type StaleReason = "dangling_doc" | "dangling_block" | "broken_anchor";
|
|
3
3
|
export interface StaleLink {
|
|
4
4
|
/** source document id */
|
|
5
|
-
|
|
5
|
+
_doc_id: string;
|
|
6
6
|
/** source document path in the reference form */
|
|
7
|
-
|
|
7
|
+
_path: string;
|
|
8
8
|
/** source block id (null for frontmatter-level edges) */
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
_block_id: string | null;
|
|
10
|
+
_predicate: string;
|
|
11
|
+
_provenance: string;
|
|
12
|
+
_dst_kind: string;
|
|
13
13
|
/** the canonical target path in the reference form (phantom prefix stripped,
|
|
14
14
|
* rooted), e.g. "/guides/old.md" — spec/surface §1 "Paths" */
|
|
15
15
|
target: string;
|
|
@@ -17,7 +17,7 @@ export interface StaleLink {
|
|
|
17
17
|
* — paste-ready as `links_repair.from`. null when it can't be recovered (frontmatter
|
|
18
18
|
* edges have no block; a projected node no longer matches). */
|
|
19
19
|
authored: string | null;
|
|
20
|
-
|
|
20
|
+
_anchor: string | null;
|
|
21
21
|
reason: StaleReason;
|
|
22
22
|
}
|
|
23
23
|
/** links_stale summary mode: counts only, no per-row output. */
|
|
@@ -29,9 +29,9 @@ export interface LinkHealthSummary {
|
|
|
29
29
|
target: string;
|
|
30
30
|
count: number;
|
|
31
31
|
}[];
|
|
32
|
-
/** source documents carrying dangling links, most first (ties by path); `
|
|
32
|
+
/** source documents carrying dangling links, most first (ties by path); `_path` the reference form */
|
|
33
33
|
bySource: {
|
|
34
|
-
|
|
34
|
+
_path: string;
|
|
35
35
|
count: number;
|
|
36
36
|
}[];
|
|
37
37
|
externalCount: number;
|
|
@@ -88,18 +88,18 @@ export function linksStale(store, repoId, opts = {}) {
|
|
|
88
88
|
const stale = page.map((r) => {
|
|
89
89
|
// Strip the "phantom:" prefix to expose the canonical path.
|
|
90
90
|
const target = r.dstNode.slice("phantom:".length);
|
|
91
|
-
// The surface speaks the reference form: `
|
|
91
|
+
// The surface speaks the reference form: `_path` and `target` rooted;
|
|
92
92
|
// `authored` stays exactly as written in the source.
|
|
93
93
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
_doc_id: r.srcDoc,
|
|
95
|
+
_path: referencePath(r.srcPath),
|
|
96
|
+
_block_id: r.srcBlock,
|
|
97
|
+
_predicate: r.predicate,
|
|
98
|
+
_provenance: r.provenance,
|
|
99
|
+
_dst_kind: r.dstKind,
|
|
100
100
|
target: referencePath(target),
|
|
101
101
|
authored: authoredFor(r.srcBlock, r.srcPath, target, r.anchor),
|
|
102
|
-
|
|
102
|
+
_anchor: r.anchor,
|
|
103
103
|
// All phantom targets are dangling DOCUMENTS in this data model (cross-doc
|
|
104
104
|
// block refs collapse to document phantoms carrying an anchor). We tag rows
|
|
105
105
|
// that also carry an anchor as such, but the miss is still a missing doc.
|
|
@@ -140,7 +140,7 @@ export function linksStaleSummary(store, repoId, opts = {}) {
|
|
|
140
140
|
const bySource = store.db
|
|
141
141
|
.prepare(`SELECT d.path AS srcPath, count(*) AS count ${phantom} GROUP BY d.path ORDER BY count DESC, d.path`)
|
|
142
142
|
.all(...params)
|
|
143
|
-
.map((r) => ({
|
|
143
|
+
.map((r) => ({ _path: referencePath(r.srcPath), count: r.count }));
|
|
144
144
|
return {
|
|
145
145
|
staleCount: byTarget.reduce((n, t) => n + t.count, 0),
|
|
146
146
|
byTarget,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
2
|
export interface LinkEdge {
|
|
3
|
-
|
|
3
|
+
_predicate: string;
|
|
4
4
|
/** the other endpoint (dst for out-edges, src for in-edges): a doc id, an external node id or `phantom:<path>` */
|
|
5
|
-
|
|
5
|
+
_doc_id: string;
|
|
6
6
|
kind: string;
|
|
7
7
|
/** how many underlying block-grain edges this doc-grain row rolls up */
|
|
8
8
|
count: number;
|
|
9
9
|
/** sample source block ids (out-edges only) */
|
|
10
10
|
samples?: string[];
|
|
11
11
|
/** block-grain (`blocks: true`): the source block of this edge (null for a frontmatter edge) */
|
|
12
|
-
|
|
12
|
+
_block_id?: string | null;
|
|
13
13
|
}
|
|
14
14
|
export interface LinksResult {
|
|
15
15
|
out: LinkEdge[];
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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 };
|
|
@@ -31,10 +31,17 @@ export declare function jsonArrayString(s: string): {
|
|
|
31
31
|
* arguments with any `undefined` members dropped; throws the first failure.
|
|
32
32
|
*/
|
|
33
33
|
export declare function validateArguments(tool: string, shape: ArgShape, raw: unknown): Record<string, unknown>;
|
|
34
|
+
/** Whether `key` is a bare engine-field name whose `_` spelling the tool takes. */
|
|
35
|
+
export declare function bareEngineArgument(key: string, shape: ArgShape): boolean;
|
|
36
|
+
/** The call with every bare engine-field argument respelled to its `_` form (the `fix` of the no-tolerance error). */
|
|
37
|
+
export declare function respellEngineArguments(args: Record<string, unknown>, shape: ArgShape): Record<string, unknown>;
|
|
34
38
|
/**
|
|
35
|
-
* What to say about an argument name the tool does not take.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* What to say about an argument name the tool does not take. A bare engine
|
|
40
|
+
* field (`id`, `path`, `ids`) where the tool takes its `_` spelling is the
|
|
41
|
+
* no-tolerance error of 5.0 (spec/surface §4 "Arguments": the bare namespace is
|
|
42
|
+
* the user's, an engine field is matched by its exact name). The names 3.0
|
|
43
|
+
* retired are called out with their replacement (`doc` → `_id`/`_path`,
|
|
44
|
+
* `block`/`node` → `_id`, `heading` → `section`, `markdown` → `text`) when the
|
|
38
45
|
* tool has the replacement, and so are the inputs 4.0 retired (spec/surface
|
|
39
46
|
* §4; ADR-021's retirement release): `ops` on a tool that takes `effects`
|
|
40
47
|
* (the kernel ops are a library/fixture contract), `dry_run` on any tool (the
|
|
@@ -77,8 +77,13 @@ export function validateArguments(tool, shape, raw) {
|
|
|
77
77
|
throw argumentError(tool, "arguments must be an object", shape);
|
|
78
78
|
const args = raw;
|
|
79
79
|
for (const key of Object.keys(args).sort(cmpBytes)) {
|
|
80
|
-
if (
|
|
81
|
-
|
|
80
|
+
if (key in shape)
|
|
81
|
+
continue;
|
|
82
|
+
// (5.0) A bare engine-field name where the tool takes its `_` spelling:
|
|
83
|
+
// the error carries the corrected call (every bare `id`/`path`/`ids` the
|
|
84
|
+
// call holds respelled at once), never a silent acceptance.
|
|
85
|
+
const fix = bareEngineArgument(key, shape) ? canonicalJson(respellEngineArguments(args, shape)) : undefined;
|
|
86
|
+
throw argumentError(tool, unknownArgumentMessage(key, shape), shape, fix);
|
|
82
87
|
}
|
|
83
88
|
const out = {};
|
|
84
89
|
for (const key of Object.keys(shape).sort(cmpBytes)) {
|
|
@@ -135,10 +140,26 @@ function issueMessage(key, schema, issues) {
|
|
|
135
140
|
const expected = { string: "a string", number: "an integer", boolean: "a boolean", object: "an object", array: "an array" };
|
|
136
141
|
return `\`${at}\` must be ${issue.expected !== undefined ? (expected[issue.expected] ?? issue.expected) : "valid"}`;
|
|
137
142
|
}
|
|
143
|
+
/** The bare engine-field names a tool may be called with by mistake (spec/surface §4, 5.0): each has its `_` spelling in the shape. */
|
|
144
|
+
const BARE_ENGINE_ARGUMENTS = ["id", "path", "ids", "paths"];
|
|
145
|
+
/** Whether `key` is a bare engine-field name whose `_` spelling the tool takes. */
|
|
146
|
+
export function bareEngineArgument(key, shape) {
|
|
147
|
+
return BARE_ENGINE_ARGUMENTS.includes(key) && `_${key}` in shape;
|
|
148
|
+
}
|
|
149
|
+
/** The call with every bare engine-field argument respelled to its `_` form (the `fix` of the no-tolerance error). */
|
|
150
|
+
export function respellEngineArguments(args, shape) {
|
|
151
|
+
const out = {};
|
|
152
|
+
for (const [k, v] of Object.entries(args))
|
|
153
|
+
out[bareEngineArgument(k, shape) ? `_${k}` : k] = v;
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
138
156
|
/**
|
|
139
|
-
* What to say about an argument name the tool does not take.
|
|
140
|
-
*
|
|
141
|
-
*
|
|
157
|
+
* What to say about an argument name the tool does not take. A bare engine
|
|
158
|
+
* field (`id`, `path`, `ids`) where the tool takes its `_` spelling is the
|
|
159
|
+
* no-tolerance error of 5.0 (spec/surface §4 "Arguments": the bare namespace is
|
|
160
|
+
* the user's, an engine field is matched by its exact name). The names 3.0
|
|
161
|
+
* retired are called out with their replacement (`doc` → `_id`/`_path`,
|
|
162
|
+
* `block`/`node` → `_id`, `heading` → `section`, `markdown` → `text`) when the
|
|
142
163
|
* tool has the replacement, and so are the inputs 4.0 retired (spec/surface
|
|
143
164
|
* §4; ADR-021's retirement release): `ops` on a tool that takes `effects`
|
|
144
165
|
* (the kernel ops are a library/fixture contract), `dry_run` on any tool (the
|
|
@@ -148,10 +169,12 @@ function issueMessage(key, schema, issues) {
|
|
|
148
169
|
export function unknownArgumentMessage(key, shape) {
|
|
149
170
|
const has = (n) => n in shape;
|
|
150
171
|
const retired = (replacement) => `\`${key}\` is no longer an argument — pass ${replacement}`;
|
|
151
|
-
if (key
|
|
152
|
-
return
|
|
153
|
-
if (
|
|
154
|
-
return retired("`
|
|
172
|
+
if (bareEngineArgument(key, shape))
|
|
173
|
+
return `\`${key}\` is not an argument — the engine field is \`_${key}\` (the bare namespace is the user's); pass \`_${key}\``;
|
|
174
|
+
if (key === "doc" && has("_id") && has("_path"))
|
|
175
|
+
return retired("`_id` or `_path`");
|
|
176
|
+
if ((key === "block" || key === "node") && has("_id"))
|
|
177
|
+
return retired("`_id`");
|
|
155
178
|
if (key === "heading" && has("section"))
|
|
156
179
|
return retired("`section` (the heading's text), or blocks_append with the heading's `id`");
|
|
157
180
|
if (key === "markdown" && has("text"))
|
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" | "provider_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved" | "block_not_container" | "stale_plan" | "plan_not_convergent" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "ambiguous_target" | "selection_too_large" | "revision_archived";
|
|
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" | "stale_plan" | "plan_not_convergent" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "ambiguous_target" | "selection_too_large" | "revision_archived" | "plan_unknown";
|
|
2
2
|
export interface EngineErrorBody {
|
|
3
3
|
error: ErrorCode;
|
|
4
4
|
message: string;
|
package/dist/src/mcp/graph.d.ts
CHANGED
|
@@ -11,35 +11,35 @@ 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
19
|
/** A traversed edge: a row of the `links` target under its own field names
|
|
20
|
-
* (spec/surface §1.2,
|
|
21
|
-
* `
|
|
22
|
-
* `
|
|
23
|
-
* the result carries. */
|
|
20
|
+
* (spec/surface §1.2, 5.0 — `_id` the edge, `_doc_id`/`_block_id` the source,
|
|
21
|
+
* `_via_id`, `_dst_id` the destination node, `_dst_path`/`_uri` its path or
|
|
22
|
+
* URI, `_field` the authoring key). No rename table: what the walk projects is
|
|
23
|
+
* what the result carries. */
|
|
24
24
|
export interface GraphEdge {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
_id: string;
|
|
26
|
+
_doc_id: string;
|
|
27
|
+
_block_id: string | null;
|
|
28
|
+
_via_id: string | null;
|
|
29
|
+
_dst_id: string;
|
|
30
|
+
_dst_path: string | null;
|
|
31
|
+
_uri: string | null;
|
|
32
|
+
_dst_kind: string;
|
|
33
|
+
_predicate: string;
|
|
34
|
+
_provenance: string;
|
|
35
|
+
_anchor: string | null;
|
|
36
|
+
_field: string | null;
|
|
37
37
|
}
|
|
38
|
-
/** A reached document: `
|
|
38
|
+
/** A reached document: `_id`/`_path` (the row's own names, as a `query` hit)
|
|
39
39
|
* + walk metadata + any requested projections. */
|
|
40
40
|
export interface GraphDoc {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
_id: string;
|
|
42
|
+
_path: string;
|
|
43
43
|
/** minimum hop distance from a root (root = 0). */
|
|
44
44
|
degree: number;
|
|
45
45
|
/** true when this node sits on the outer boundary (its min degree == degrees). */
|
|
@@ -53,8 +53,8 @@ export interface GraphResult {
|
|
|
53
53
|
documents: GraphDoc[];
|
|
54
54
|
links: GraphEdge[];
|
|
55
55
|
frontier: {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
_id: string;
|
|
57
|
+
_path: string;
|
|
58
58
|
degree: number;
|
|
59
59
|
}[];
|
|
60
60
|
truncated: boolean;
|
package/dist/src/mcp/graph.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
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
|
-
// collect { from
|
|
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
7
|
// exact walk the `query` tool exposes over the link rows),
|
|
8
8
|
// runs it through the shared runner (oqxRunAsync), and then only SHAPES the
|
|
9
9
|
// walk's own output (its `$depth`/`$stop` intrinsics and projected edges) into
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
// bound is `follow`'s; nothing here re-implements BFS/DFS.
|
|
12
12
|
//
|
|
13
13
|
// Mapping (args → follow query):
|
|
14
|
-
// roots → the seed `where
|
|
14
|
+
// roots → the seed `where _id == … or …` (refs resolved to doc ids first; literals in the AST, never spliced text)
|
|
15
15
|
// degrees → `follow … collect { … depth degrees+1 }` (seed is $depth 1, so N hops = depth N+1)
|
|
16
|
-
// direction → `follow
|
|
17
|
-
// | `follow
|
|
16
|
+
// direction → `follow _out_links collect { from _dst }` (the linked documents)
|
|
17
|
+
// | `follow _in_links collect { from _doc }` (the linking documents)
|
|
18
18
|
// | both (two walks, unioned)
|
|
19
19
|
// predicate → restricts the neighborhood to docs reachable via that predicate
|
|
20
20
|
// (recomputed over the walk's collected edges; no `via` clause)
|
|
21
21
|
// select → extra doc projections spliced into the walk's `select`
|
|
22
22
|
// max_documents → a post-walk cap on the distinct document set
|
|
23
23
|
//
|
|
24
|
-
// Edges are surfaced by projecting the reached doc's `
|
|
24
|
+
// Edges are surfaced by projecting the reached doc's `_out_links`/`_in_links`
|
|
25
25
|
// in the SAME walk (a select-position collect), then keeping the induced-subgraph
|
|
26
26
|
// edges (both endpoints in the neighborhood) plus dangling stubs to external/
|
|
27
27
|
// phantom targets — so external (`x_…`) and phantom endpoints are never dropped,
|
|
@@ -41,8 +41,8 @@ function cmpBytes(a, b) {
|
|
|
41
41
|
// Build the extra `select` items for caller projections. Each expression gets a
|
|
42
42
|
// stable internal alias (_u0, _u1, …) so it can never collide with a reserved
|
|
43
43
|
// word or the macro's own aliases; the clean output name is derived separately.
|
|
44
|
-
// A bare-name selection (`layer`, `
|
|
45
|
-
// `query` hit projects it (spec/surface §1.4,
|
|
44
|
+
// A bare-name selection (`layer`, `_title`) is its own output key, exactly as a
|
|
45
|
+
// `query` hit projects it (spec/surface §1.4, 5.0) — so `_id`/`_path` collapse
|
|
46
46
|
// onto the envelope keys (the engine reads the same value) and any other name
|
|
47
47
|
// is an ordinary column, deduped by suffix. Anything else (an expression) is
|
|
48
48
|
// `sel_<i>`, so a projection can never overwrite the envelope here.
|
|
@@ -51,16 +51,16 @@ function buildUserSelect(select) {
|
|
|
51
51
|
const outNames = [];
|
|
52
52
|
if (!select || select.length === 0)
|
|
53
53
|
return { items, outNames };
|
|
54
|
-
const used = new Set(["
|
|
54
|
+
const used = new Set(["_id", "_path"]);
|
|
55
55
|
select.forEach((expr, i) => {
|
|
56
56
|
const trimmed = expr.trim();
|
|
57
57
|
if (!trimmed)
|
|
58
58
|
return;
|
|
59
59
|
const m = /^(\$?[A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
60
60
|
let name = m ? m[1] : `sel_${i}`;
|
|
61
|
-
// The envelope keys are the row's own identity: an `
|
|
61
|
+
// The envelope keys are the row's own identity: an `_id`/`_path` selection
|
|
62
62
|
// lands on them rather than beside them (the engine reads the same value).
|
|
63
|
-
if (name !== "
|
|
63
|
+
if (name !== "_id" && name !== "_path") {
|
|
64
64
|
while (used.has(name))
|
|
65
65
|
name = `${name}_${i}`;
|
|
66
66
|
used.add(name);
|
|
@@ -86,40 +86,47 @@ function userItems(src, alias) {
|
|
|
86
86
|
}
|
|
87
87
|
return q.select;
|
|
88
88
|
}
|
|
89
|
-
// The edge fields projected from
|
|
90
|
-
// target's own names, projected under themselves (spec/surface §1.2,
|
|
89
|
+
// The edge fields projected from `_out_links`/`_in_links` — the `links`
|
|
90
|
+
// target's own names, projected under themselves (spec/surface §1.2, 5.0: no
|
|
91
91
|
// rename between the language and the result) — identical for both directions
|
|
92
92
|
// so a single filter handles either scan (an edge is a directional fact
|
|
93
93
|
// regardless of which endpoint the walk reached it from).
|
|
94
94
|
const EDGE_FIELDS = [
|
|
95
|
-
"
|
|
96
|
-
"
|
|
95
|
+
"_id", "_doc_id", "_block_id", "_via_id", "_dst_id", "_dst_path", "_uri",
|
|
96
|
+
"_dst_kind", "_predicate", "_provenance", "_anchor", "_field",
|
|
97
97
|
];
|
|
98
|
+
// The macro's own aliases: `__oqx_`/`__omg_` (two underscores) are the runner's
|
|
99
|
+
// and the selector compiler's reserved prefixes; a single underscore is the
|
|
100
|
+
// engine prefix of 5.0, which an ALIAS may use freely (an alias is never a row
|
|
101
|
+
// read), and keeps these off any authored key.
|
|
102
|
+
const DEPTH_ALIAS = "_depth";
|
|
103
|
+
const STOP_ALIAS = "_stop";
|
|
104
|
+
const LINKS_ALIAS = "_links";
|
|
98
105
|
// The walk, as an AST (spec/oqx/AST.md §7) printed canonically — never spliced
|
|
99
106
|
// strings: the seed ids are literals, the caller's projections parsed items.
|
|
100
107
|
//
|
|
101
|
-
// select _depth: $depth, _stop: $stop, _links:
|
|
102
|
-
// from docs where
|
|
108
|
+
// select _depth: $depth, _stop: $stop, _links: _<dir>_links collect { <edge fields> } <user items>
|
|
109
|
+
// from docs where _id == <root> or … follow distinct _<dir>_links collect { from <_dst | _doc> depth <depth> }
|
|
103
110
|
//
|
|
104
111
|
// The destination block walks the link rows to the documents at their other
|
|
105
112
|
// end (spec/surface §1.2, 3.0 follow-up: there are no `out`/`in` relations —
|
|
106
|
-
// `
|
|
107
|
-
// absent `
|
|
113
|
+
// `_dst` is a link's destination document, `_doc` its source; a `from` over an
|
|
114
|
+
// absent `_dst` — an external or phantom target — contributes no successor).
|
|
108
115
|
function buildQuery(rootIds, dir, depth, userSelect) {
|
|
109
|
-
const edges = build.op(build.
|
|
116
|
+
const edges = build.op(build.ident(`_${dir}_links`), "collect", build.subquery({
|
|
110
117
|
select: EDGE_FIELDS.map((name) => build.field(name, build.ident(name))),
|
|
111
118
|
}));
|
|
112
|
-
const seed = rootIds.map((id) => build.scalar(build.binary("==", build.ident("
|
|
119
|
+
const seed = rootIds.map((id) => build.scalar(build.binary("==", build.ident("_id"), build.lit(id))));
|
|
113
120
|
const q = build.query(build.ident("docs"), {
|
|
114
121
|
select: [
|
|
115
|
-
build.field(
|
|
116
|
-
build.field(
|
|
117
|
-
build.collect(
|
|
122
|
+
build.field(DEPTH_ALIAS, build.ident("$depth")),
|
|
123
|
+
build.field(STOP_ALIAS, build.ident("$stop")),
|
|
124
|
+
build.collect(LINKS_ALIAS, edges),
|
|
118
125
|
...userSelect,
|
|
119
126
|
],
|
|
120
127
|
where: seed.length === 1 ? seed[0] : build.or(seed),
|
|
121
128
|
// `depth` is the destination block's own clause (oqx 0.20).
|
|
122
|
-
follow: build.follow([build.op(build.ident(
|
|
129
|
+
follow: build.follow([build.op(build.ident(`_${dir}_links`), "collect", build.subquery({ from: [build.ident(dir === "out" ? "_dst" : "_doc")], depth }))], { distinct: true }),
|
|
123
130
|
});
|
|
124
131
|
return print(q);
|
|
125
132
|
}
|
|
@@ -163,26 +170,26 @@ export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
|
163
170
|
if (res.truncated)
|
|
164
171
|
queryTruncated = true;
|
|
165
172
|
for (const hit of res.hits) {
|
|
166
|
-
const hopDepth = Number(hit
|
|
173
|
+
const hopDepth = Number(hit[DEPTH_ALIAS]); // $depth: seed = 1
|
|
167
174
|
const degree = hopDepth - 1; // root = 0
|
|
168
|
-
// The walk's hits carry `
|
|
175
|
+
// The walk's hits carry `_id`/`_path` (spec/surface §1.4, 5.0) and so
|
|
169
176
|
// does `graph`'s document: one notation for a document everywhere.
|
|
170
|
-
const prev = docMap.get(hit.
|
|
177
|
+
const prev = docMap.get(hit._id);
|
|
171
178
|
if (!prev || degree < prev.degree) {
|
|
172
|
-
const doc = {
|
|
179
|
+
const doc = { _id: hit._id, _path: hit._path, degree, frontier: false };
|
|
173
180
|
outNames.forEach((name, i) => {
|
|
174
|
-
// An `
|
|
175
|
-
if (name !== undefined && name !== "
|
|
181
|
+
// An `_id`/`_path` selection is the envelope key itself (same value).
|
|
182
|
+
if (name !== undefined && name !== "_id" && name !== "_path")
|
|
176
183
|
doc[name] = hit[`_u${i}`] ?? null;
|
|
177
184
|
});
|
|
178
|
-
docMap.set(hit.
|
|
185
|
+
docMap.set(hit._id, doc);
|
|
179
186
|
}
|
|
180
187
|
// Gather this node's edges; dedup by edge id (an a→b edge is reachable as
|
|
181
188
|
// a's out-edge AND b's in-edge in "both" mode).
|
|
182
|
-
const raw = hit
|
|
189
|
+
const raw = hit[LINKS_ALIAS] ?? [];
|
|
183
190
|
for (const e of raw)
|
|
184
|
-
if (!edgeMap.has(e.
|
|
185
|
-
edgeMap.set(e.
|
|
191
|
+
if (!edgeMap.has(e._id))
|
|
192
|
+
edgeMap.set(e._id, e);
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
// Predicate filter restricts the NEIGHBORHOOD, not just the edge list: a doc is
|
|
@@ -196,12 +203,12 @@ export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
|
196
203
|
const adj = new Map();
|
|
197
204
|
const link = (from, to) => { (adj.get(from) ?? adj.set(from, []).get(from)).push(to); };
|
|
198
205
|
for (const e of edgeMap.values()) {
|
|
199
|
-
if (e.
|
|
206
|
+
if (e._predicate !== P)
|
|
200
207
|
continue;
|
|
201
208
|
if (dirs.includes("out"))
|
|
202
|
-
link(e.
|
|
209
|
+
link(e._doc_id, e._dst_id); // forward
|
|
203
210
|
if (dirs.includes("in"))
|
|
204
|
-
link(e.
|
|
211
|
+
link(e._dst_id, e._doc_id); // backward
|
|
205
212
|
}
|
|
206
213
|
const depthOf = new Map(rootIds.map((id) => [id, 0]));
|
|
207
214
|
let wave = [...rootIds];
|
|
@@ -230,10 +237,10 @@ export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
|
230
237
|
docMap.set(id, d);
|
|
231
238
|
}
|
|
232
239
|
// Cap the distinct document set (nearest-first), then bound edges to it.
|
|
233
|
-
const allDocs = [...docMap.values()].sort((a, b) => a.degree - b.degree || cmpBytes(a.
|
|
240
|
+
const allDocs = [...docMap.values()].sort((a, b) => a.degree - b.degree || cmpBytes(a._path, b._path));
|
|
234
241
|
const capped = allDocs.length > maxDocuments;
|
|
235
242
|
const documents = allDocs.slice(0, maxDocuments);
|
|
236
|
-
const reached = new Set(documents.map((d) => d.
|
|
243
|
+
const reached = new Set(documents.map((d) => d._id));
|
|
237
244
|
const truncated = queryTruncated || capped;
|
|
238
245
|
// Mark the outer boundary: a node whose MIN degree equals the requested reach
|
|
239
246
|
// was only ever touched at the edge of the walk (never expanded past). At
|
|
@@ -242,7 +249,7 @@ export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
|
242
249
|
for (const d of documents) {
|
|
243
250
|
d.frontier = d.degree === effectiveDegrees;
|
|
244
251
|
if (d.frontier)
|
|
245
|
-
frontier.push({
|
|
252
|
+
frontier.push({ _id: d._id, _path: d._path, degree: d.degree });
|
|
246
253
|
}
|
|
247
254
|
// Keep the induced-subgraph edges (both endpoints in the neighborhood) plus
|
|
248
255
|
// dangling stubs to external/phantom targets, so external (x_…) and phantom
|
|
@@ -251,15 +258,15 @@ export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
|
251
258
|
// predicate-reachable docs above).
|
|
252
259
|
const edges = [];
|
|
253
260
|
for (const e of edgeMap.values()) {
|
|
254
|
-
if (args.predicate && e.
|
|
261
|
+
if (args.predicate && e._predicate !== args.predicate)
|
|
255
262
|
continue;
|
|
256
|
-
const srcIn = reached.has(e.
|
|
257
|
-
const dstDangling = e.
|
|
258
|
-
const dstIn = reached.has(e.
|
|
263
|
+
const srcIn = reached.has(e._doc_id);
|
|
264
|
+
const dstDangling = e._dst_kind === "external" || (e._dst_kind === "document" && e._dst_path === null);
|
|
265
|
+
const dstIn = reached.has(e._dst_id) || dstDangling;
|
|
259
266
|
if (srcIn && dstIn)
|
|
260
267
|
edges.push(e);
|
|
261
268
|
}
|
|
262
|
-
edges.sort((a, b) => cmpBytes(a.
|
|
269
|
+
edges.sort((a, b) => cmpBytes(a._doc_id, b._doc_id) || cmpBytes(a._id, b._id));
|
|
263
270
|
return {
|
|
264
271
|
roots: rootIds,
|
|
265
272
|
degrees: effectiveDegrees,
|