@omgbase/core 0.1.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/LICENSE +21 -0
- package/dist/src/cli/eval-matcher.d.ts +13 -0
- package/dist/src/cli/eval-matcher.js +36 -0
- package/dist/src/core/attach.d.ts +5 -0
- package/dist/src/core/attach.js +38 -0
- package/dist/src/core/cursor.d.ts +11 -0
- package/dist/src/core/cursor.js +35 -0
- package/dist/src/core/hash.d.ts +23 -0
- package/dist/src/core/hash.js +82 -0
- package/dist/src/core/ids.d.ts +6 -0
- package/dist/src/core/ids.js +26 -0
- package/dist/src/core/index.d.ts +1 -0
- package/dist/src/core/index.js +2 -0
- package/dist/src/core/ingest.d.ts +43 -0
- package/dist/src/core/ingest.js +287 -0
- package/dist/src/core/order-key.d.ts +8 -0
- package/dist/src/core/order-key.js +72 -0
- package/dist/src/core/parse/parse.d.ts +3 -0
- package/dist/src/core/parse/parse.js +104 -0
- package/dist/src/core/parse/render.d.ts +3 -0
- package/dist/src/core/parse/render.js +20 -0
- package/dist/src/core/parse/tree.d.ts +8 -0
- package/dist/src/core/parse/tree.js +43 -0
- package/dist/src/core/parse/types.d.ts +52 -0
- package/dist/src/core/parse/types.js +9 -0
- package/dist/src/core/read/document.d.ts +107 -0
- package/dist/src/core/read/document.js +191 -0
- package/dist/src/core/read/nodes.d.ts +47 -0
- package/dist/src/core/read/nodes.js +102 -0
- package/dist/src/core/read/outline.d.ts +12 -0
- package/dist/src/core/read/outline.js +76 -0
- package/dist/src/core/read/reader.d.ts +125 -0
- package/dist/src/core/read/reader.js +206 -0
- package/dist/src/core/read/refs.d.ts +15 -0
- package/dist/src/core/read/refs.js +51 -0
- package/dist/src/core/store/edges.d.ts +26 -0
- package/dist/src/core/store/edges.js +101 -0
- package/dist/src/core/store/fts.d.ts +10 -0
- package/dist/src/core/store/fts.js +22 -0
- package/dist/src/core/store/gc.d.ts +18 -0
- package/dist/src/core/store/gc.js +69 -0
- package/dist/src/core/store/nodes.d.ts +5 -0
- package/dist/src/core/store/nodes.js +57 -0
- package/dist/src/core/store/properties.d.ts +38 -0
- package/dist/src/core/store/properties.js +177 -0
- package/dist/src/core/store/rebuild.d.ts +3 -0
- package/dist/src/core/store/rebuild.js +29 -0
- package/dist/src/core/store/schema.d.ts +9 -0
- package/dist/src/core/store/schema.js +386 -0
- package/dist/src/core/store/sections.d.ts +2 -0
- package/dist/src/core/store/sections.js +29 -0
- package/dist/src/core/store/store.d.ts +24 -0
- package/dist/src/core/store/store.js +164 -0
- package/dist/src/core/store/writers.d.ts +52 -0
- package/dist/src/core/store/writers.js +89 -0
- package/dist/src/core/vec.d.ts +10 -0
- package/dist/src/core/vec.js +33 -0
- package/dist/src/format/adapter.d.ts +97 -0
- package/dist/src/format/adapter.js +14 -0
- package/dist/src/format/index.d.ts +6 -0
- package/dist/src/format/index.js +15 -0
- package/dist/src/format/json.d.ts +3 -0
- package/dist/src/format/json.js +324 -0
- package/dist/src/format/markdown.d.ts +5 -0
- package/dist/src/format/markdown.js +217 -0
- package/dist/src/format/registry.d.ts +6 -0
- package/dist/src/format/registry.js +24 -0
- package/dist/src/format/yaml.d.ts +5 -0
- package/dist/src/format/yaml.js +413 -0
- package/dist/src/graph/extract.d.ts +38 -0
- package/dist/src/graph/extract.js +242 -0
- package/dist/src/graph/history.d.ts +103 -0
- package/dist/src/graph/history.js +210 -0
- package/dist/src/graph/inbound-links.d.ts +33 -0
- package/dist/src/graph/inbound-links.js +130 -0
- package/dist/src/graph/link-destinations.d.ts +22 -0
- package/dist/src/graph/link-destinations.js +84 -0
- package/dist/src/graph/link-health.d.ts +79 -0
- package/dist/src/graph/link-health.js +146 -0
- package/dist/src/graph/links.d.ts +23 -0
- package/dist/src/graph/links.js +42 -0
- package/dist/src/index.d.ts +60 -0
- package/dist/src/index.js +70 -0
- package/dist/src/mcp/errors.d.ts +17 -0
- package/dist/src/mcp/errors.js +18 -0
- package/dist/src/mcp/graph.d.ts +63 -0
- package/dist/src/mcp/graph.js +212 -0
- package/dist/src/mcp/reference.d.ts +1 -0
- package/dist/src/mcp/reference.js +227 -0
- package/dist/src/mcp/server.d.ts +30 -0
- package/dist/src/mcp/server.js +1076 -0
- package/dist/src/mcp/stdio.d.ts +13 -0
- package/dist/src/mcp/stdio.js +24 -0
- package/dist/src/migrate/mrplex.d.ts +27 -0
- package/dist/src/migrate/mrplex.js +29 -0
- package/dist/src/mutate/apply.d.ts +83 -0
- package/dist/src/mutate/apply.js +302 -0
- package/dist/src/mutate/doc-store.d.ts +49 -0
- package/dist/src/mutate/doc-store.js +71 -0
- package/dist/src/mutate/docs.d.ts +60 -0
- package/dist/src/mutate/docs.js +232 -0
- package/dist/src/mutate/known-ids.d.ts +12 -0
- package/dist/src/mutate/known-ids.js +154 -0
- package/dist/src/mutate/load.d.ts +3 -0
- package/dist/src/mutate/load.js +66 -0
- package/dist/src/mutate/lower.d.ts +32 -0
- package/dist/src/mutate/lower.js +232 -0
- package/dist/src/mutate/macros.d.ts +91 -0
- package/dist/src/mutate/macros.js +230 -0
- package/dist/src/mutate/ops.d.ts +40 -0
- package/dist/src/mutate/ops.js +513 -0
- package/dist/src/mutate/opset.d.ts +92 -0
- package/dist/src/mutate/opset.js +79 -0
- package/dist/src/mutate/plan-update.d.ts +51 -0
- package/dist/src/mutate/plan-update.js +187 -0
- package/dist/src/mutate/tree.d.ts +46 -0
- package/dist/src/mutate/tree.js +167 -0
- package/dist/src/oqx/run.d.ts +1 -0
- package/dist/src/oqx/run.js +6 -0
- package/dist/src/oqx-js/context.d.ts +23 -0
- package/dist/src/oqx-js/context.js +564 -0
- package/dist/src/oqx-js/planner.d.ts +10 -0
- package/dist/src/oqx-js/planner.js +91 -0
- package/dist/src/oqx-js/run.d.ts +41 -0
- package/dist/src/oqx-js/run.js +332 -0
- package/dist/src/oqx-js/sql/translate.d.ts +27 -0
- package/dist/src/oqx-js/sql/translate.js +290 -0
- package/dist/src/reconcile/crossdoc.d.ts +34 -0
- package/dist/src/reconcile/crossdoc.js +76 -0
- package/dist/src/reconcile/eval/generator.d.ts +21 -0
- package/dist/src/reconcile/eval/generator.js +129 -0
- package/dist/src/reconcile/eval/metrics.d.ts +32 -0
- package/dist/src/reconcile/eval/metrics.js +74 -0
- package/dist/src/reconcile/flatten.d.ts +12 -0
- package/dist/src/reconcile/flatten.js +37 -0
- package/dist/src/reconcile/phase5.d.ts +8 -0
- package/dist/src/reconcile/phase5.js +167 -0
- package/dist/src/reconcile/phase6a.d.ts +2 -0
- package/dist/src/reconcile/phase6a.js +159 -0
- package/dist/src/reconcile/phases.d.ts +28 -0
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.d.ts +21 -0
- package/dist/src/reconcile/reconcile.js +115 -0
- package/dist/src/reconcile/similarity.d.ts +7 -0
- package/dist/src/reconcile/similarity.js +41 -0
- package/dist/src/reconcile/types.d.ts +46 -0
- package/dist/src/reconcile/types.js +19 -0
- package/dist/src/search/cel/ast.d.ts +74 -0
- package/dist/src/search/cel/ast.js +3 -0
- package/dist/src/search/cel/compile.d.ts +60 -0
- package/dist/src/search/cel/compile.js +836 -0
- package/dist/src/search/cel/lexer.d.ts +9 -0
- package/dist/src/search/cel/lexer.js +139 -0
- package/dist/src/search/cel/parser.d.ts +7 -0
- package/dist/src/search/cel/parser.js +219 -0
- package/dist/src/search/drain.d.ts +39 -0
- package/dist/src/search/drain.js +97 -0
- package/dist/src/search/embeddings.d.ts +131 -0
- package/dist/src/search/embeddings.js +263 -0
- package/dist/src/search/external.d.ts +13 -0
- package/dist/src/search/external.js +177 -0
- package/dist/src/search/fts-query.d.ts +1 -0
- package/dist/src/search/fts-query.js +64 -0
- package/dist/src/search/provider.d.ts +17 -0
- package/dist/src/search/provider.js +13 -0
- package/dist/src/search/resolve.d.ts +18 -0
- package/dist/src/search/resolve.js +23 -0
- package/dist/src/search/rrf.d.ts +36 -0
- package/dist/src/search/rrf.js +84 -0
- package/dist/src/search/tasks.d.ts +12 -0
- package/dist/src/search/tasks.js +131 -0
- package/dist/src/search/text.d.ts +16 -0
- package/dist/src/search/text.js +21 -0
- package/dist/src/search/vector.d.ts +18 -0
- package/dist/src/search/vector.js +49 -0
- package/dist/src/sync/admin.d.ts +59 -0
- package/dist/src/sync/admin.js +53 -0
- package/dist/src/sync/attach.d.ts +13 -0
- package/dist/src/sync/attach.js +29 -0
- package/dist/src/sync/checkpoint.d.ts +21 -0
- package/dist/src/sync/checkpoint.js +52 -0
- package/dist/src/sync/driver.d.ts +25 -0
- package/dist/src/sync/driver.js +80 -0
- package/dist/src/sync/external-source.d.ts +14 -0
- package/dist/src/sync/external-source.js +137 -0
- package/dist/src/sync/freshness.d.ts +42 -0
- package/dist/src/sync/freshness.js +152 -0
- package/dist/src/sync/fs-util.d.ts +12 -0
- package/dist/src/sync/fs-util.js +55 -0
- package/dist/src/sync/git-heuristics.d.ts +6 -0
- package/dist/src/sync/git-heuristics.js +16 -0
- package/dist/src/sync/observe.d.ts +63 -0
- package/dist/src/sync/observe.js +84 -0
- package/dist/src/sync/plugin.d.ts +54 -0
- package/dist/src/sync/plugin.js +12 -0
- package/dist/src/sync/reconciling-ingest.d.ts +12 -0
- package/dist/src/sync/reconciling-ingest.js +209 -0
- package/dist/src/sync/recovery.d.ts +6 -0
- package/dist/src/sync/recovery.js +34 -0
- package/dist/src/sync/settings.d.ts +18 -0
- package/dist/src/sync/settings.js +58 -0
- package/dist/src/sync/sources.d.ts +37 -0
- package/dist/src/sync/sources.js +75 -0
- package/dist/src/sync/tombstone.d.ts +9 -0
- package/dist/src/sync/tombstone.js +55 -0
- package/dist/src/sync/watch-lease.d.ts +14 -0
- package/dist/src/sync/watch-lease.js +91 -0
- package/dist/src/sync/watcher.d.ts +19 -0
- package/dist/src/sync/watcher.js +34 -0
- package/dist/src/sync/workspace.d.ts +34 -0
- package/dist/src/sync/workspace.js +120 -0
- package/dist/src/sync/writer-lock.d.ts +16 -0
- package/dist/src/sync/writer-lock.js +121 -0
- package/package.json +80 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export { VERSION } from "./core/index.js";
|
|
2
|
+
export { AdapterCapability } from "./format/adapter.js";
|
|
3
|
+
export { registerAdapter, adapterForFormat, adapterForPath, registeredFormats, detectFormat } from "./format/registry.js";
|
|
4
|
+
export { markdownAdapter, MARKDOWN_FORMAT } from "./format/markdown.js";
|
|
5
|
+
export { yamlAdapter, YAML_FORMAT } from "./format/yaml.js";
|
|
6
|
+
export { jsonAdapter, JSON_FORMAT } from "./format/json.js";
|
|
7
|
+
export { writeDocNodes, deleteDocNodes } from "./core/store/nodes.js";
|
|
8
|
+
export { docPropertiesMerged, docPropertiesGrouped, writeDocProperties, flattenFrontmatter } from "./core/store/properties.js";
|
|
9
|
+
// Side-effect: registers built-in adapters.
|
|
10
|
+
import "./format/index.js";
|
|
11
|
+
// Workspace + sync surface used by the CLI (second client, 11 §1).
|
|
12
|
+
export { Store } from "./core/store/store.js";
|
|
13
|
+
export { Workspace, RepoSelectionError } from "./sync/workspace.js";
|
|
14
|
+
export { resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, deepMerge, } from "./sync/settings.js";
|
|
15
|
+
export { freshnessSweep, rebuildFileStats, recordFileStat, detectDiskDrift } from "./sync/freshness.js";
|
|
16
|
+
export { withWriterLock, writerLockFree, WriterLockTimeout } from "./sync/writer-lock.js";
|
|
17
|
+
export { WatchLease, watchLeaseLive } from "./sync/watch-lease.js";
|
|
18
|
+
export { ingestDirectory } from "./sync/attach.js";
|
|
19
|
+
export { ensureRepo } from "./core/attach.js";
|
|
20
|
+
export { walkMarkdown, walkMarkdownAsync } from "./sync/fs-util.js";
|
|
21
|
+
export { Watcher } from "./sync/watcher.js";
|
|
22
|
+
export { createExternalSource } from "./sync/external-source.js";
|
|
23
|
+
export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, } from "./sync/sources.js";
|
|
24
|
+
export { reconcileChanges, attachSource } from "./sync/driver.js";
|
|
25
|
+
export { observeFile, observeMany, observeOne, observeDelete } from "./sync/observe.js";
|
|
26
|
+
export { buildServer } from "./mcp/server.js";
|
|
27
|
+
export { serveStdio } from "./mcp/stdio.js";
|
|
28
|
+
export { processCheckpoint } from "./sync/checkpoint.js";
|
|
29
|
+
export { reposStatus, syncStatus } from "./sync/admin.js";
|
|
30
|
+
// Read surface consumed by CLI read commands (11 §5.2–5.5).
|
|
31
|
+
export { docsOutline } from "./core/read/outline.js";
|
|
32
|
+
export { docsRead, reconstructContent } from "./core/read/document.js";
|
|
33
|
+
export { nodesGet, nodesGetMany } from "./core/read/nodes.js";
|
|
34
|
+
export { findDoc, loadDocBlocks, blockRaw, docsList, docsTree, normalizeTreePrefix, DOCS_LIST_DEFAULT_LIMIT, DOCS_TREE_DEFAULT_LIMIT, } from "./core/read/reader.js";
|
|
35
|
+
export { CursorInvalid, encodeCursor, decodeCursor } from "./core/cursor.js";
|
|
36
|
+
export { resolveRef } from "./core/read/refs.js";
|
|
37
|
+
export { resolve } from "./search/resolve.js";
|
|
38
|
+
// OQX — omgbase Query eXpressions (unified composable query language, slice 1:
|
|
39
|
+
// structural navigation over docs/blocks/nodes). Coexists with query().
|
|
40
|
+
export { oqxRun, oqxRunAsync, collectSemanticPhrases, parseOqx } from "./oqx/run.js";
|
|
41
|
+
// Embeddings + semantic retrieval (05 §5–6). The provider is a plugin loaded by
|
|
42
|
+
// package name; core carries no ML dependency.
|
|
43
|
+
export { EmbeddingWorker, SemanticUnavailable, contextPrefix, shouldEmbed, estimateTokens, } from "./search/embeddings.js";
|
|
44
|
+
export { buildEmbedTasks, buildDocEmbedTasks } from "./search/tasks.js";
|
|
45
|
+
export { EmbedDrainer } from "./search/drain.js";
|
|
46
|
+
export { embeddingSettings } from "./search/provider.js";
|
|
47
|
+
export { createExternalProvider } from "./search/external.js";
|
|
48
|
+
export { hybridSearch } from "./search/rrf.js";
|
|
49
|
+
export { vectorSearch, docVectorSearch } from "./search/vector.js";
|
|
50
|
+
export { historyNode, diffUnified, changesSince } from "./graph/history.js";
|
|
51
|
+
export { docLinks } from "./graph/links.js";
|
|
52
|
+
export { FilterInvalid } from "./search/cel/parser.js";
|
|
53
|
+
export { EngineError } from "./mcp/errors.js";
|
|
54
|
+
export { MutationError } from "./mutate/tree.js";
|
|
55
|
+
export { isValidId, prefixOf } from "./core/ids.js";
|
|
56
|
+
// Mutation surface consumed by CLI write commands (11 §5.6).
|
|
57
|
+
export { apply } from "./mutate/apply.js";
|
|
58
|
+
export { FsDocStore, NullDocStore, resolveDocStore } from "./mutate/doc-store.js";
|
|
59
|
+
export {} from "./mutate/ops.js";
|
|
60
|
+
export { tasksComplete, sectionsAppend, sectionsRename, sectionsMove, listsInsertItem, linksRetarget, nodeSet, editablePropsFor, } from "./mutate/macros.js";
|
|
61
|
+
// Admin/maintenance (11 §5.9). Graph traversal is OQX `follow` now (see oqx/).
|
|
62
|
+
export { docsCreate, docsMove, docsDelete, docsSetMeta, } from "./mutate/docs.js";
|
|
63
|
+
// Inbound-link discovery + destination-aware rewrite (docs_move's dangling report / retarget_inbound).
|
|
64
|
+
export { inboundLinksTo, retargetLinksInRaw } from "./graph/inbound-links.js";
|
|
65
|
+
// Whole-document update: reconcile → opset → inspect/apply.
|
|
66
|
+
export { planUpdate, applyOpset, docsUpdate, } from "./mutate/plan-update.js";
|
|
67
|
+
export { opsetKernelOps, renderOpsetPlan, serializeOpset, parseOpset, } from "./mutate/opset.js";
|
|
68
|
+
export { rebuildIndex } from "./core/store/rebuild.js";
|
|
69
|
+
export { runGc } from "./core/store/gc.js";
|
|
70
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
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" | "semantic_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved";
|
|
2
|
+
export interface EngineErrorBody {
|
|
3
|
+
error: ErrorCode;
|
|
4
|
+
message: string;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
retriable: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class EngineError extends Error {
|
|
9
|
+
code: ErrorCode;
|
|
10
|
+
data: unknown;
|
|
11
|
+
retriable: boolean;
|
|
12
|
+
constructor(code: ErrorCode, message: string, opts?: {
|
|
13
|
+
data?: unknown;
|
|
14
|
+
retriable?: boolean;
|
|
15
|
+
});
|
|
16
|
+
body(): EngineErrorBody;
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Error codes (06 §5). Shape: { error, message, data?, retriable }. Codes are
|
|
2
|
+
// stable; prose is free to improve. FilterInvalid from the query compiler maps
|
|
3
|
+
// to filter_invalid with its reason/hint in data.
|
|
4
|
+
export class EngineError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
data;
|
|
7
|
+
retriable;
|
|
8
|
+
constructor(code, message, opts = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.data = opts.data;
|
|
12
|
+
this.retriable = opts.retriable ?? false;
|
|
13
|
+
}
|
|
14
|
+
body() {
|
|
15
|
+
return { error: this.code, message: this.message, data: this.data, retriable: this.retriable };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Store } from "../core/store/store.js";
|
|
2
|
+
import { type EmbedQuery } from "../oqx/run.js";
|
|
3
|
+
export type GraphDirection = "in" | "out" | "both";
|
|
4
|
+
export interface GraphArgs {
|
|
5
|
+
/** one or more document refs — paths and/or ids. Root nodes are depth 0. */
|
|
6
|
+
roots: string[];
|
|
7
|
+
/** max hop depth from a root (root = 0). Default 1; clamped so degrees+1 ≤ 8. */
|
|
8
|
+
degrees?: number | undefined;
|
|
9
|
+
/** which edges to follow: outgoing links, backlinks, or both. Default "both". */
|
|
10
|
+
direction?: GraphDirection | undefined;
|
|
11
|
+
/** restrict the neighborhood to documents reachable from a root via edges of
|
|
12
|
+
* this predicate, within `degrees` (both the docs and the edges are filtered). */
|
|
13
|
+
predicate?: string | undefined;
|
|
14
|
+
/** extra document projections (OQX select expressions, e.g. "layer", "$path"). */
|
|
15
|
+
select?: string[] | undefined;
|
|
16
|
+
/** cap on the distinct documents returned (default 200). */
|
|
17
|
+
max_documents?: number | undefined;
|
|
18
|
+
}
|
|
19
|
+
/** A traversed edge with full provenance (mirrors the `edges` target's rows). */
|
|
20
|
+
export interface GraphEdge {
|
|
21
|
+
id: string;
|
|
22
|
+
src: string;
|
|
23
|
+
dst: string;
|
|
24
|
+
dst_path: string | null;
|
|
25
|
+
dst_uri: string | null;
|
|
26
|
+
dst_kind: string;
|
|
27
|
+
predicate: string;
|
|
28
|
+
provenance: string;
|
|
29
|
+
anchor: string | null;
|
|
30
|
+
src_field: string | null;
|
|
31
|
+
}
|
|
32
|
+
/** A reached document: id/path + walk metadata + any requested projections. */
|
|
33
|
+
export interface GraphDoc {
|
|
34
|
+
id: string;
|
|
35
|
+
path: string;
|
|
36
|
+
/** minimum hop distance from a root (root = 0). */
|
|
37
|
+
degree: number;
|
|
38
|
+
/** true when this node sits on the outer boundary (its min degree == degrees). */
|
|
39
|
+
frontier: boolean;
|
|
40
|
+
[k: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
export interface GraphResult {
|
|
43
|
+
roots: string[];
|
|
44
|
+
degrees: number;
|
|
45
|
+
direction: GraphDirection;
|
|
46
|
+
documents: GraphDoc[];
|
|
47
|
+
edges: GraphEdge[];
|
|
48
|
+
frontier: {
|
|
49
|
+
id: string;
|
|
50
|
+
path: string;
|
|
51
|
+
degree: number;
|
|
52
|
+
}[];
|
|
53
|
+
truncated: boolean;
|
|
54
|
+
/** the exact OQX `follow` query/queries this macro generated — it delegates. */
|
|
55
|
+
queries: string[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run the `graph` neighborhood macro: compile the args into OQX `follow`
|
|
59
|
+
* query(ies), execute via the shared runner, and shape the walk output into
|
|
60
|
+
* { documents, edges, frontier }. Throws EngineError (doc_missing/target_missing)
|
|
61
|
+
* for an empty or unresolvable roots list — never a silent empty result.
|
|
62
|
+
*/
|
|
63
|
+
export declare function graphNeighborhood(store: Store, repoId: string, args: GraphArgs, embedQuery?: EmbedQuery): Promise<GraphResult>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// `graph` — a one-call neighborhood macro OVER OQX `follow`.
|
|
2
|
+
//
|
|
3
|
+
// ADR-006: OQX `follow` is the ONE traversal engine; the removed
|
|
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 doc.out`
|
|
6
|
+
// / `doc.in` query (the exact recursive-CTE walk the `query` tool exposes),
|
|
7
|
+
// runs it through the shared runner (oqxRunAsync), and then only SHAPES the
|
|
8
|
+
// walk's own output (its `$depth`/`$stop` intrinsics and projected edges) into
|
|
9
|
+
// the { documents, edges, frontier } envelope. Every hop, cycle-guard and depth
|
|
10
|
+
// bound is `follow`'s; nothing here re-implements BFS/DFS.
|
|
11
|
+
//
|
|
12
|
+
// Mapping (args → follow query):
|
|
13
|
+
// roots → the seed `where $id == … || …` (refs resolved to doc ids first)
|
|
14
|
+
// degrees → `follow … { depth degrees+1 }` (seed is $depth 1, so N hops = depth N+1)
|
|
15
|
+
// direction → `follow doc.out` | `doc.in` | both (two walks, unioned)
|
|
16
|
+
// predicate → restricts the neighborhood to docs reachable via that predicate
|
|
17
|
+
// (recomputed over the walk's collected edges; no `via` clause)
|
|
18
|
+
// select → extra doc projections spliced into the walk's `select`
|
|
19
|
+
// max_documents → a post-walk cap on the distinct document set
|
|
20
|
+
//
|
|
21
|
+
// Edges are surfaced by projecting the reached doc's `doc.out_edges`/`doc.in_edges`
|
|
22
|
+
// in the SAME walk (a select-position collect), then keeping the induced-subgraph
|
|
23
|
+
// edges (both endpoints in the neighborhood) plus dangling stubs to external/
|
|
24
|
+
// phantom targets — so external (`x_…`) and phantom endpoints are never dropped,
|
|
25
|
+
// exactly as `from edges` surfaces them.
|
|
26
|
+
import { findDocByRef } from "../core/read/reader.js";
|
|
27
|
+
import { oqxRunAsync } from "../oqx/run.js";
|
|
28
|
+
import { EngineError } from "./errors.js";
|
|
29
|
+
const DEFAULT_DEGREES = 1;
|
|
30
|
+
const DEFAULT_MAX_DOCUMENTS = 200;
|
|
31
|
+
const MAX_DEPTH = 8; // OQX follow depth cap (1..8)
|
|
32
|
+
// Build the extra `select` items for caller projections. Each expression gets a
|
|
33
|
+
// stable internal alias (_u0, _u1, …) so it can never collide with a reserved
|
|
34
|
+
// word or the macro's own aliases; the clean output name is derived separately.
|
|
35
|
+
function buildUserSelect(select) {
|
|
36
|
+
if (!select || select.length === 0)
|
|
37
|
+
return { clause: "", outNames: [] };
|
|
38
|
+
const items = [];
|
|
39
|
+
const outNames = [];
|
|
40
|
+
const used = new Set();
|
|
41
|
+
select.forEach((expr, i) => {
|
|
42
|
+
const trimmed = expr.trim();
|
|
43
|
+
if (!trimmed)
|
|
44
|
+
return;
|
|
45
|
+
const m = /^\$?([A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
46
|
+
let name = m ? m[1] : `sel_${i}`;
|
|
47
|
+
while (used.has(name))
|
|
48
|
+
name = `${name}_${i}`;
|
|
49
|
+
used.add(name);
|
|
50
|
+
outNames[i] = name;
|
|
51
|
+
items.push(`_u${i}: ${trimmed}`);
|
|
52
|
+
});
|
|
53
|
+
return { clause: items.length ? `, ${items.join(", ")}` : "", outNames };
|
|
54
|
+
}
|
|
55
|
+
// The edge fields projected from doc.out_edges/doc.in_edges — identical for both
|
|
56
|
+
// directions so a single filter handles either scan (an edge is a directional
|
|
57
|
+
// src→dst fact regardless of which endpoint the walk reached it from).
|
|
58
|
+
const EDGE_COLLECT = "{ id: $id, src: $src, dst: $dst, dst_path: $dst_path, dst_uri: $dst_uri, " +
|
|
59
|
+
"dst_kind: dst_kind, predicate: predicate, provenance: provenance, anchor: anchor, src_field: src_field }";
|
|
60
|
+
function buildQuery(seed, dir, depth, userSelect) {
|
|
61
|
+
const edgesRel = dir === "out" ? "doc.out_edges" : "doc.in_edges";
|
|
62
|
+
return (`from docs where ${seed} ` +
|
|
63
|
+
`select _depth: $depth, _stop: $stop, _edges: ${edgesRel} collect ${EDGE_COLLECT}${userSelect} ` +
|
|
64
|
+
`follow distinct doc.${dir} { depth ${depth} }`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run the `graph` neighborhood macro: compile the args into OQX `follow`
|
|
68
|
+
* query(ies), execute via the shared runner, and shape the walk output into
|
|
69
|
+
* { documents, edges, frontier }. Throws EngineError (doc_missing/target_missing)
|
|
70
|
+
* for an empty or unresolvable roots list — never a silent empty result.
|
|
71
|
+
*/
|
|
72
|
+
export async function graphNeighborhood(store, repoId, args, embedQuery) {
|
|
73
|
+
if (!args.roots || args.roots.length === 0) {
|
|
74
|
+
throw new EngineError("target_missing", "graph requires at least one root (path or id)");
|
|
75
|
+
}
|
|
76
|
+
// Resolve every root ref to a concrete doc id up front — the seed is built
|
|
77
|
+
// from ids only, so no caller-supplied string is interpolated into the query.
|
|
78
|
+
const rootIds = [];
|
|
79
|
+
for (const ref of args.roots) {
|
|
80
|
+
const info = findDocByRef(store, repoId, ref);
|
|
81
|
+
if (!info)
|
|
82
|
+
throw new EngineError("doc_missing", `no document for ${JSON.stringify(ref)}`, { data: { root: ref } });
|
|
83
|
+
if (!rootIds.includes(info.docId))
|
|
84
|
+
rootIds.push(info.docId);
|
|
85
|
+
}
|
|
86
|
+
const degrees = Math.max(0, Math.floor(args.degrees ?? DEFAULT_DEGREES));
|
|
87
|
+
const depth = Math.min(MAX_DEPTH, degrees + 1); // seed = $depth 1 ⇒ N hops = depth N+1
|
|
88
|
+
const effectiveDegrees = depth - 1; // after the depth-cap clamp
|
|
89
|
+
const direction = args.direction ?? "both";
|
|
90
|
+
const maxDocuments = Math.max(1, Math.floor(args.max_documents ?? DEFAULT_MAX_DOCUMENTS));
|
|
91
|
+
const dirs = direction === "both" ? ["out", "in"] : [direction];
|
|
92
|
+
const seed = rootIds.map((id) => `$id == ${JSON.stringify(id)}`).join(" || ");
|
|
93
|
+
const { clause: userSelect, outNames } = buildUserSelect(args.select);
|
|
94
|
+
const queries = [];
|
|
95
|
+
const docMap = new Map();
|
|
96
|
+
const edgeMap = new Map();
|
|
97
|
+
let queryTruncated = false;
|
|
98
|
+
for (const dir of dirs) {
|
|
99
|
+
const q = buildQuery(seed, dir, depth, userSelect);
|
|
100
|
+
queries.push(q);
|
|
101
|
+
// Fetch one more than the cap to detect truncation of a single scan; `follow
|
|
102
|
+
// distinct` yields one row per reached node so the row count == node count.
|
|
103
|
+
const res = await oqxRunAsync(store, repoId, q, { limit: maxDocuments + 1 }, embedQuery);
|
|
104
|
+
if (res.truncated)
|
|
105
|
+
queryTruncated = true;
|
|
106
|
+
for (const hit of res.hits) {
|
|
107
|
+
const hopDepth = Number(hit._depth); // $depth: seed = 1
|
|
108
|
+
const degree = hopDepth - 1; // root = 0
|
|
109
|
+
const prev = docMap.get(hit.id);
|
|
110
|
+
if (!prev || degree < prev.degree) {
|
|
111
|
+
const doc = { id: hit.id, path: hit.path, degree, frontier: false };
|
|
112
|
+
outNames.forEach((name, i) => {
|
|
113
|
+
if (name !== undefined)
|
|
114
|
+
doc[name] = hit[`_u${i}`] ?? null;
|
|
115
|
+
});
|
|
116
|
+
docMap.set(hit.id, doc);
|
|
117
|
+
}
|
|
118
|
+
// Gather this node's edges; dedup by edge id (an a→b edge is reachable as
|
|
119
|
+
// a's out-edge AND b's in-edge in "both" mode).
|
|
120
|
+
const raw = hit._edges ?? [];
|
|
121
|
+
for (const e of raw)
|
|
122
|
+
if (!edgeMap.has(e.id))
|
|
123
|
+
edgeMap.set(e.id, e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Predicate filter restricts the NEIGHBORHOOD, not just the edge list: a doc is
|
|
127
|
+
// in-neighborhood only if reachable from a root by edges of that predicate
|
|
128
|
+
// within `degrees`. The unrestricted walk already collected every edge in the
|
|
129
|
+
// wider neighborhood (and any predicate-P path of length ≤ degrees stays inside
|
|
130
|
+
// it, since each prefix is reachable at ≤ that depth), so we recompute
|
|
131
|
+
// reachability over the P-filtered edges rather than issuing more queries.
|
|
132
|
+
if (args.predicate) {
|
|
133
|
+
const P = args.predicate;
|
|
134
|
+
const adj = new Map();
|
|
135
|
+
const link = (from, to) => { (adj.get(from) ?? adj.set(from, []).get(from)).push(to); };
|
|
136
|
+
for (const e of edgeMap.values()) {
|
|
137
|
+
if (e.predicate !== P)
|
|
138
|
+
continue;
|
|
139
|
+
if (dirs.includes("out"))
|
|
140
|
+
link(e.src, e.dst); // forward
|
|
141
|
+
if (dirs.includes("in"))
|
|
142
|
+
link(e.dst, e.src); // backward
|
|
143
|
+
}
|
|
144
|
+
const depthOf = new Map(rootIds.map((id) => [id, 0]));
|
|
145
|
+
let wave = [...rootIds];
|
|
146
|
+
for (let lvl = 1; lvl <= effectiveDegrees && wave.length > 0; lvl++) {
|
|
147
|
+
const next = [];
|
|
148
|
+
for (const from of wave) {
|
|
149
|
+
for (const to of adj.get(from) ?? []) {
|
|
150
|
+
// Only traverse to real reached documents (external/phantom targets stay
|
|
151
|
+
// edge stubs, handled below — never in-neighborhood nodes).
|
|
152
|
+
if (docMap.has(to) && !depthOf.has(to)) {
|
|
153
|
+
depthOf.set(to, lvl);
|
|
154
|
+
next.push(to);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
wave = next;
|
|
159
|
+
}
|
|
160
|
+
const restricted = new Map();
|
|
161
|
+
for (const [id, d] of depthOf) {
|
|
162
|
+
const orig = docMap.get(id);
|
|
163
|
+
if (orig)
|
|
164
|
+
restricted.set(id, { ...orig, degree: d });
|
|
165
|
+
}
|
|
166
|
+
docMap.clear();
|
|
167
|
+
for (const [id, d] of restricted)
|
|
168
|
+
docMap.set(id, d);
|
|
169
|
+
}
|
|
170
|
+
// Cap the distinct document set (nearest-first), then bound edges to it.
|
|
171
|
+
const allDocs = [...docMap.values()].sort((a, b) => a.degree - b.degree || a.path.localeCompare(b.path));
|
|
172
|
+
const capped = allDocs.length > maxDocuments;
|
|
173
|
+
const documents = allDocs.slice(0, maxDocuments);
|
|
174
|
+
const reached = new Set(documents.map((d) => d.id));
|
|
175
|
+
const truncated = queryTruncated || capped;
|
|
176
|
+
// Mark the outer boundary: a node whose MIN degree equals the requested reach
|
|
177
|
+
// was only ever touched at the edge of the walk (never expanded past). At
|
|
178
|
+
// degrees 0 the roots themselves are the boundary.
|
|
179
|
+
const frontier = [];
|
|
180
|
+
for (const d of documents) {
|
|
181
|
+
d.frontier = d.degree === effectiveDegrees;
|
|
182
|
+
if (d.frontier)
|
|
183
|
+
frontier.push({ id: d.id, path: d.path, degree: d.degree });
|
|
184
|
+
}
|
|
185
|
+
// Keep the induced-subgraph edges (both endpoints in the neighborhood) plus
|
|
186
|
+
// dangling stubs to external/phantom targets, so external (x_…) and phantom
|
|
187
|
+
// endpoints survive the way `from edges` surfaces them. A `predicate` filter
|
|
188
|
+
// keeps only that predicate's edges (the neighborhood was already restricted to
|
|
189
|
+
// predicate-reachable docs above).
|
|
190
|
+
const edges = [];
|
|
191
|
+
for (const e of edgeMap.values()) {
|
|
192
|
+
if (args.predicate && e.predicate !== args.predicate)
|
|
193
|
+
continue;
|
|
194
|
+
const srcIn = reached.has(e.src);
|
|
195
|
+
const dstDangling = e.dst_kind === "external" || (e.dst_kind === "document" && e.dst_path === null);
|
|
196
|
+
const dstIn = reached.has(e.dst) || dstDangling;
|
|
197
|
+
if (srcIn && dstIn)
|
|
198
|
+
edges.push(e);
|
|
199
|
+
}
|
|
200
|
+
edges.sort((a, b) => (a.src === b.src ? a.id.localeCompare(b.id) : a.src.localeCompare(b.src)));
|
|
201
|
+
return {
|
|
202
|
+
roots: rootIds,
|
|
203
|
+
degrees: effectiveDegrees,
|
|
204
|
+
direction,
|
|
205
|
+
documents,
|
|
206
|
+
edges,
|
|
207
|
+
frontier,
|
|
208
|
+
truncated,
|
|
209
|
+
queries,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const QUERY_SYNTAX = "# query \u2014 OQX syntax reference\n\nThe `query` tool takes ONE plain OQX string (+ optional `limit`/`cursor`). There\nis no {from, filter, order} envelope: every concern is a clause of the string.\n\n from <target> [where <pred>] [select <items>] [order by <expr> [asc|desc], \u2026]\n [limit N] [offset N] [follow <relation> [{ \u2026 }]]\n $repo.<target> count|exists|none|first|single { <block> } (scalar/one-row form)\n\nResults are LEAN hits \u2014 {id, path} + whatever `select` projects (or `values`,\n`count`, `exists`, `none`). Hydrate full content by id via nodes_get/docs_read.\n\n## Targets & field namespaces\n\nThe sigil rule: `$`-prefixed names are engine intrinsics; BARE identifiers are\nyour content. On docs a bare first segment that collides with an intrinsic base\nname (id/path/repo/updated_at/content_hash/body) is REJECTED with a \"did you mean\n$X?\" hint (bare `path` would silently read an absent frontmatter key). Force the\nproperty with `frontmatter.<k>`. `title`/`tags` are NOT reserved: `$title`/`$tags`\nare computed and never shadow an authored `title:`/`tags:` key.\n\ndocs:\n - bare identifier = a document PROPERTY (frontmatter + inline `key:: value`,\n unioned; nested via dots: meta.owner). YAML/JSON docs\n expose the parsed object's keys the same way.\n - source-scoped = frontmatter.<k> / inline.<k>; the whole bag as a collection\n via entries(frontmatter) / entries(inline) (see entries()).\n - computed = $title (first H1), $tags (body #hashtags), format\n (the doc's format \u2014 \"markdown\"/\"yaml\"/\"json\"; a bare\n computed field, present on EVERY doc, so has(format) is\n always true and `format == \"yaml\"` is the filter).\n - intrinsics = $id, $path, $updated_at (ISO-8601, sorts chronologically),\n $body (whole file), $content_hash.\n - relations = nodes, blocks, doc.out / doc.in (citation graph),\n doc.out_edges / doc.in_edges (a doc's edges as rows).\n\nblocks:\n - bare fields = type, text, and every attrs key FLATTENED onto the row\n (bare `checked` reads attrs.checked; `attrs.<k>` still\n works; type/text win on collision; absent \u21D2 false).\n - intrinsics = $id, $doc, $path, $ordinal, $depth, $updated_at, $body\n (the block text), $content_hash (the block's OWN raw hash \u2014\n the value update/split expect).\n - reach-through = doc.<key> / doc.$path / doc.format = the CONTAINING doc.\n - relations = block.children, block.nodes, block.out_edges, section.\n\nnodes:\n - bare fields = kind (md:task / md:link / md:section / yaml:\u2026), name,\n value, + attrs keys flattened (checked, level).\n - intrinsics = $id (=$node_id), $doc_id, $block_id, $path.\n - reach-through = doc.<key>, block.type / block.text.\n - relations = section.blocks, section.children, section.subsections.\n\nedges: (the authored link graph as rows \u2014 one per open edge)\n - bare fields = predicate (\"references\"/\"embeds\"/freeform), provenance\n (\"link\"/\"frontmatter\"/\"inline_field\"/\u2026), dst_kind\n (\"document\"/\"external\"/\"collection\"), anchor, src_field\n - intrinsics = $id, $src, $dst, $dst_path (target doc path; NULL when\n dangling/external), $dst_uri (external URL; NULL\n otherwise), $src_block, $via, $from_commit; $path and\n doc.<key> reach the SOURCE document.\n - text()/semantic() are N/A on edges.\n\n## Scoping (ADR-015): bare names are LOCAL\n\n name the CURRENT row only \u2014 never falls through to an enclosing row\n or the repository; an absent field is simply absent.\n ^name one scope OUT (the enclosing row's fields/intrinsics/lifts);\n ^^name two scopes out. `^$path` = the enclosing row's path.\n $repo.<t> the root scan \u2014 $repo.docs / $repo.nodes / $repo.blocks /\n $repo.edges \u2014 available from ANY depth (uncorrelated until you\n add a ^ predicate). $repo.$id = the repository id.\n $value the current item itself (a row, or each ELEMENT when the\n receiver is a list property); ^$value = the enclosing row.\n $key inside an entries(x) block: the entry's key.\n\n## Scalar semantics (@omgbase/oqx)\n\n comparisons == != < <= > >= strict & typed (5 == \"5\" is false); either\n side may be a field (field-vs-field OK).\n absence null and a missing field are the same \"absent\" value; two absents\n are EQUAL, so `absent != \"x\"` is TRUE and `absent == null` is TRUE.\n Relational (< <= > >=) on an absent operand \u2192 FALSE. Bare `f` in\n boolean position uses JS truthiness (absent/false/0/\"\" \u21D2 false;\n NOTE: \"false\", [] and {} are truthy). has(f) = explicit presence.\n boolean && || ! grouping ( \u2026 )\n arithmetic + - * / % (+ concatenates if either side is a string)\n membership x in y array \u2192 typed membership; string \u2192 substring;\n object \u2192 key exists; range \u2192 coverage. Any list\n property works directly: \"pricing\" in tags. list(f)\n coerces scalar-or-list (absent \u2192 []) \u2014 use it when a\n key is sometimes scalar, sometimes a list.\n ranges lo..hi (inclusive) lo...hi (exclusive hi) ..hi lo..\n era in 800..1680; works over ISO dates. range(prop) reads a\n range-shaped STRING property as an interval: \"2026-01-15\" in range(window).\n strings x.contains(\"s\") x.startsWith(\"p\") x.endsWith(\"s\") x.matches(\"^re$\")\n (real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE \u2014\n fold with .lower(): $title.lower().contains(\"aurora\").\n functions has(f) size(x) list(x) entries(x) range(s)\n literals \"str\" 'str' 123 1.5 true false null\n determinism no now()/clock/random. Lists/structs are not literals; ternary is\n not supported. Errors surface as filter_invalid.\n scalars vs lists a list-authored key never == a scalar (tags == \"x\" is false\n when tags is a list) \u2014 use `\"x\" in tags` / `in list(tags)`.\n\n## Domain predicates (omgbase)\n\n text(\"terms\") FTS5 PRUNING predicate (docs match via their blocks, nodes\n via the node index); composes anywhere, incl. nested scopes.\n semantic(\"phrase\") embedding cosine SCORE (docs/blocks): threshold it\n (semantic(\"x\") > 0.6), project it, or `order by semantic(\"x\")\n desc` for top-K. Needs a provider; else semantic_unavailable.\n blocks target only:\n under(id_or_heading) block in the containment subtree / section range\n under_heading(\"Launch\") an ancestor section heading contains the text (ci)\n under_kind(type[, name]) an ancestor block has that type (name: its text\n contains / its key equals name)\n within(\"path\" | \"d_..\") containing doc = id, exact path, or path glob (*)\n yaml_path(\"a.b\") YAML docs: the block at that key path\n json_pointer(\"#/a/b\") JSON docs: the block at that pointer\n has_edge(\"pred\"[, target]) an open authored edge with that predicate leaves here\n has_anchor() block carries an authored ^ref\n parent_type() == \"x\" parent block's type (must be compared)\n child_count() > 0 number of direct children (must be compared)\n\n## Nested queries & consumers\n\n <receiver> collect|exists|none|count|first|single { <block> }\n receiver = a relation (nodes, blocks, doc.out_edges, section.blocks, \u2026), a\n list property (tags), entries(x), or a root ($repo.docs).\n block = [where \u2026] [select \u2026] [order by \u2026] [limit N] [offset N]; a leading\n predicate is an implicit where, a leading name/alias list an\n implicit select.\n exists \u22651 row nodes exists { where kind == \"md:task\" && !checked }\n none 0 rows nodes none { where kind == \"md:task\" && !checked } (\"every task done\")\n count a number nodes count { where kind == \"md:task\" } >= 2\n collect rows (default) \u2014 in select for nested results; in where with a lift\n first / single zero-or-one (single errors on >1) \u2014 lookups in select\n distinct on any consumer: dedup by projected value \u2014 nodes count distinct { select kind }\n limit/offset bound the block's rows AFTER where/order/distinct, BEFORE the\n consumer reduces: nodes exists { offset 1 } = at least two;\n first { \u2026 offset 1 } = the second.\n lift `^name:` in a where-collect binds values outward:\n from docs where nodes collect { ^open: value where kind == \"md:task\" && !checked } select $path, open\n joins $repo.<t> exists { where slug == ^ref } semi-join; !\u2026 exists anti-join;\n $repo.nodes single { where kind == \"person\" && attrs.id == ^owner_id } lookup;\n x in ^keys membership over a lifted set.\n\n## select \u2014 projection\n\n select a, alias: expr, nested: nodes collect { \u2026 }\n bare key \u2192 the property (scalar-authored \u2192 scalar; list \u2192 array); $-intrinsics;\n a call/arithmetic needs an alias (n: size(tags)) unless in values mode.\n \"$body\" whole file bytes (docs) \u2014 docs_read is cheaper for one doc.\n distinct select distinct type (dedup hits by projected value)\n values select <ONE item> values \u2192 bare values instead of records:\n from docs select distinct type values \u2192 values: [\"hub\",\"lab-note\",\u2026]\n tags: tags collect { $value values } \u2192 a plain array\n h: nodes first { name values where kind == \"md:section\" order by first_ordinal }\n entries(x) a record as a collection ($key / $value per entry), key order:\n select fm: entries(frontmatter) collect { k: $key, v: $value }\n where entries(frontmatter) exists { where $key == \"era\" && $value > 1600 }\n entries(inline), entries(attrs), a nested map, or a list (index keys)\n $semantic_score is not a field \u2014 project semantic(\"\u2026\") under an alias instead.\n\n## order by, limit/offset, pagination\n\n order by era desc, $path asc absent values sort LAST (asc); ties \u2192 (path, id)\n limit N / offset N define the result SET (integer literals);\n the tool's limit/cursor then page WITHIN it\n cursor opaque keyset on (path,id); a custom order by\n disables it (you get the top page + truncated)\n\n## Graph traversal (follow) and edges\n\n from docs where <seed> follow doc.out docs the seed links TO\n from docs where <seed> follow doc.in backlinks\n follow doc.out { depth 3 } bound (1..8, default 8)\n follow doc.out { where layer == \"canon\" } keep only matching successors\n follow doc.out { frontier type == \"practitioner\" } cut, keeping the frontier row\n follow distinct doc.out / follow doc.out { by group } identity control\n Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|\n depth|cycle), $leaf/$frontier, $ordinal \u2014 usable in select/order by and the\n top-level post-walk where, NOT in the follow-local where/frontier.\n Type-preserving relations: doc.out/doc.in, block.children, section.children,\n section.subsections.\n from edges where $dst_path == \"notes/x.md\" inbound edges to a doc, as rows\n from edges where predicate == \"depends_on\" select $src, $dst_path\n from docs where !doc.in_edges exists { } orphans (nothing links in)\n (The `graph` tool is a convenience wrapper that compiles to a follow query.)\n\n## semantic grain\n\nfrom docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.\ntext()/where prune the candidate set; only order by reweights.\n\n## Examples\n\n from docs where layer == \"working\"\n from docs where $path.startsWith(\"guides/\") && $updated_at >= \"2026-08-01\"\n from docs where \"pricing\" in list(tags) select layer, tags\n from docs where inline.owner == \"alice\"\n from docs where $title.lower().contains(\"q3 plan\")\n from docs where format == \"yaml\"\n from docs where type == \"practitioner\" order by era desc limit 2\n from docs where nodes none { where kind == \"md:task\" && !checked }\n from docs where tags exists { where $value == \"tria-prima\" }\n from docs where $path == \"x.md\" select fm: entries(frontmatter) collect { k: $key, v: $value }\n from docs select owner_id, owner: $repo.nodes single { where kind == \"person\" && attrs.id == ^owner_id }\n from blocks where type == \"task\" && !checked && under_heading(\"Launch\") && doc.layer == \"working\"\n from blocks where type == \"paragraph\" && has_edge(\"references\", \"d_92aaaaa\")\n from blocks order by semantic(\"identity preservation across edits\") desc limit 10\n from edges where dst_kind == \"external\" select $dst_uri\n $repo.docs count { where layer == \"canon\" }\n\nCommon mistake: bare `path.startsWith(...)` meant the intrinsic \u2014 it fails loud\nwith a hint; write `$path.startsWith(...)`. Another: `where tags == \"x\"` on a\nlist-valued key is false \u2014 write `\"x\" in tags`.\n";
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Agent-facing syntax reference, surfaced as the query_syntax MCP tool. Agents
|
|
2
|
+
// lean on it heavily before writing a non-trivial query. It is a condensed,
|
|
3
|
+
// example-led distillation of docs/query-language.md — enough to compose a
|
|
4
|
+
// correct call without reading the normative spec. Kept in one place so the
|
|
5
|
+
// tool handler stays thin. Semantics are the @omgbase/oqx contract (ADR-013);
|
|
6
|
+
// when this text and the corpus (corpus/oqx/alchemy.test.ts) disagree, the
|
|
7
|
+
// corpus wins.
|
|
8
|
+
export const QUERY_SYNTAX = `# query — OQX syntax reference
|
|
9
|
+
|
|
10
|
+
The \`query\` tool takes ONE plain OQX string (+ optional \`limit\`/\`cursor\`). There
|
|
11
|
+
is no {from, filter, order} envelope: every concern is a clause of the string.
|
|
12
|
+
|
|
13
|
+
from <target> [where <pred>] [select <items>] [order by <expr> [asc|desc], …]
|
|
14
|
+
[limit N] [offset N] [follow <relation> [{ … }]]
|
|
15
|
+
$repo.<target> count|exists|none|first|single { <block> } (scalar/one-row form)
|
|
16
|
+
|
|
17
|
+
Results are LEAN hits — {id, path} + whatever \`select\` projects (or \`values\`,
|
|
18
|
+
\`count\`, \`exists\`, \`none\`). Hydrate full content by id via nodes_get/docs_read.
|
|
19
|
+
|
|
20
|
+
## Targets & field namespaces
|
|
21
|
+
|
|
22
|
+
The sigil rule: \`$\`-prefixed names are engine intrinsics; BARE identifiers are
|
|
23
|
+
your content. On docs a bare first segment that collides with an intrinsic base
|
|
24
|
+
name (id/path/repo/updated_at/content_hash/body) is REJECTED with a "did you mean
|
|
25
|
+
$X?" hint (bare \`path\` would silently read an absent frontmatter key). Force the
|
|
26
|
+
property with \`frontmatter.<k>\`. \`title\`/\`tags\` are NOT reserved: \`$title\`/\`$tags\`
|
|
27
|
+
are computed and never shadow an authored \`title:\`/\`tags:\` key.
|
|
28
|
+
|
|
29
|
+
docs:
|
|
30
|
+
- bare identifier = a document PROPERTY (frontmatter + inline \`key:: value\`,
|
|
31
|
+
unioned; nested via dots: meta.owner). YAML/JSON docs
|
|
32
|
+
expose the parsed object's keys the same way.
|
|
33
|
+
- source-scoped = frontmatter.<k> / inline.<k>; the whole bag as a collection
|
|
34
|
+
via entries(frontmatter) / entries(inline) (see entries()).
|
|
35
|
+
- computed = $title (first H1), $tags (body #hashtags), format
|
|
36
|
+
(the doc's format — "markdown"/"yaml"/"json"; a bare
|
|
37
|
+
computed field, present on EVERY doc, so has(format) is
|
|
38
|
+
always true and \`format == "yaml"\` is the filter).
|
|
39
|
+
- intrinsics = $id, $path, $updated_at (ISO-8601, sorts chronologically),
|
|
40
|
+
$body (whole file), $content_hash.
|
|
41
|
+
- relations = nodes, blocks, doc.out / doc.in (citation graph),
|
|
42
|
+
doc.out_edges / doc.in_edges (a doc's edges as rows).
|
|
43
|
+
|
|
44
|
+
blocks:
|
|
45
|
+
- bare fields = type, text, and every attrs key FLATTENED onto the row
|
|
46
|
+
(bare \`checked\` reads attrs.checked; \`attrs.<k>\` still
|
|
47
|
+
works; type/text win on collision; absent ⇒ false).
|
|
48
|
+
- intrinsics = $id, $doc, $path, $ordinal, $depth, $updated_at, $body
|
|
49
|
+
(the block text), $content_hash (the block's OWN raw hash —
|
|
50
|
+
the value update/split expect).
|
|
51
|
+
- reach-through = doc.<key> / doc.$path / doc.format = the CONTAINING doc.
|
|
52
|
+
- relations = block.children, block.nodes, block.out_edges, section.
|
|
53
|
+
|
|
54
|
+
nodes:
|
|
55
|
+
- bare fields = kind (md:task / md:link / md:section / yaml:…), name,
|
|
56
|
+
value, + attrs keys flattened (checked, level).
|
|
57
|
+
- intrinsics = $id (=$node_id), $doc_id, $block_id, $path.
|
|
58
|
+
- reach-through = doc.<key>, block.type / block.text.
|
|
59
|
+
- relations = section.blocks, section.children, section.subsections.
|
|
60
|
+
|
|
61
|
+
edges: (the authored link graph as rows — one per open edge)
|
|
62
|
+
- bare fields = predicate ("references"/"embeds"/freeform), provenance
|
|
63
|
+
("link"/"frontmatter"/"inline_field"/…), dst_kind
|
|
64
|
+
("document"/"external"/"collection"), anchor, src_field
|
|
65
|
+
- intrinsics = $id, $src, $dst, $dst_path (target doc path; NULL when
|
|
66
|
+
dangling/external), $dst_uri (external URL; NULL
|
|
67
|
+
otherwise), $src_block, $via, $from_commit; $path and
|
|
68
|
+
doc.<key> reach the SOURCE document.
|
|
69
|
+
- text()/semantic() are N/A on edges.
|
|
70
|
+
|
|
71
|
+
## Scoping (ADR-015): bare names are LOCAL
|
|
72
|
+
|
|
73
|
+
name the CURRENT row only — never falls through to an enclosing row
|
|
74
|
+
or the repository; an absent field is simply absent.
|
|
75
|
+
^name one scope OUT (the enclosing row's fields/intrinsics/lifts);
|
|
76
|
+
^^name two scopes out. \`^$path\` = the enclosing row's path.
|
|
77
|
+
$repo.<t> the root scan — $repo.docs / $repo.nodes / $repo.blocks /
|
|
78
|
+
$repo.edges — available from ANY depth (uncorrelated until you
|
|
79
|
+
add a ^ predicate). $repo.$id = the repository id.
|
|
80
|
+
$value the current item itself (a row, or each ELEMENT when the
|
|
81
|
+
receiver is a list property); ^$value = the enclosing row.
|
|
82
|
+
$key inside an entries(x) block: the entry's key.
|
|
83
|
+
|
|
84
|
+
## Scalar semantics (@omgbase/oqx)
|
|
85
|
+
|
|
86
|
+
comparisons == != < <= > >= strict & typed (5 == "5" is false); either
|
|
87
|
+
side may be a field (field-vs-field OK).
|
|
88
|
+
absence null and a missing field are the same "absent" value; two absents
|
|
89
|
+
are EQUAL, so \`absent != "x"\` is TRUE and \`absent == null\` is TRUE.
|
|
90
|
+
Relational (< <= > >=) on an absent operand → FALSE. Bare \`f\` in
|
|
91
|
+
boolean position uses JS truthiness (absent/false/0/"" ⇒ false;
|
|
92
|
+
NOTE: "false", [] and {} are truthy). has(f) = explicit presence.
|
|
93
|
+
boolean && || ! grouping ( … )
|
|
94
|
+
arithmetic + - * / % (+ concatenates if either side is a string)
|
|
95
|
+
membership x in y array → typed membership; string → substring;
|
|
96
|
+
object → key exists; range → coverage. Any list
|
|
97
|
+
property works directly: "pricing" in tags. list(f)
|
|
98
|
+
coerces scalar-or-list (absent → []) — use it when a
|
|
99
|
+
key is sometimes scalar, sometimes a list.
|
|
100
|
+
ranges lo..hi (inclusive) lo...hi (exclusive hi) ..hi lo..
|
|
101
|
+
era in 800..1680; works over ISO dates. range(prop) reads a
|
|
102
|
+
range-shaped STRING property as an interval: "2026-01-15" in range(window).
|
|
103
|
+
strings x.contains("s") x.startsWith("p") x.endsWith("s") x.matches("^re$")
|
|
104
|
+
(real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE —
|
|
105
|
+
fold with .lower(): $title.lower().contains("aurora").
|
|
106
|
+
functions has(f) size(x) list(x) entries(x) range(s)
|
|
107
|
+
literals "str" 'str' 123 1.5 true false null
|
|
108
|
+
determinism no now()/clock/random. Lists/structs are not literals; ternary is
|
|
109
|
+
not supported. Errors surface as filter_invalid.
|
|
110
|
+
scalars vs lists a list-authored key never == a scalar (tags == "x" is false
|
|
111
|
+
when tags is a list) — use \`"x" in tags\` / \`in list(tags)\`.
|
|
112
|
+
|
|
113
|
+
## Domain predicates (omgbase)
|
|
114
|
+
|
|
115
|
+
text("terms") FTS5 PRUNING predicate (docs match via their blocks, nodes
|
|
116
|
+
via the node index); composes anywhere, incl. nested scopes.
|
|
117
|
+
semantic("phrase") embedding cosine SCORE (docs/blocks): threshold it
|
|
118
|
+
(semantic("x") > 0.6), project it, or \`order by semantic("x")
|
|
119
|
+
desc\` for top-K. Needs a provider; else semantic_unavailable.
|
|
120
|
+
blocks target only:
|
|
121
|
+
under(id_or_heading) block in the containment subtree / section range
|
|
122
|
+
under_heading("Launch") an ancestor section heading contains the text (ci)
|
|
123
|
+
under_kind(type[, name]) an ancestor block has that type (name: its text
|
|
124
|
+
contains / its key equals name)
|
|
125
|
+
within("path" | "d_..") containing doc = id, exact path, or path glob (*)
|
|
126
|
+
yaml_path("a.b") YAML docs: the block at that key path
|
|
127
|
+
json_pointer("#/a/b") JSON docs: the block at that pointer
|
|
128
|
+
has_edge("pred"[, target]) an open authored edge with that predicate leaves here
|
|
129
|
+
has_anchor() block carries an authored ^ref
|
|
130
|
+
parent_type() == "x" parent block's type (must be compared)
|
|
131
|
+
child_count() > 0 number of direct children (must be compared)
|
|
132
|
+
|
|
133
|
+
## Nested queries & consumers
|
|
134
|
+
|
|
135
|
+
<receiver> collect|exists|none|count|first|single { <block> }
|
|
136
|
+
receiver = a relation (nodes, blocks, doc.out_edges, section.blocks, …), a
|
|
137
|
+
list property (tags), entries(x), or a root ($repo.docs).
|
|
138
|
+
block = [where …] [select …] [order by …] [limit N] [offset N]; a leading
|
|
139
|
+
predicate is an implicit where, a leading name/alias list an
|
|
140
|
+
implicit select.
|
|
141
|
+
exists ≥1 row nodes exists { where kind == "md:task" && !checked }
|
|
142
|
+
none 0 rows nodes none { where kind == "md:task" && !checked } ("every task done")
|
|
143
|
+
count a number nodes count { where kind == "md:task" } >= 2
|
|
144
|
+
collect rows (default) — in select for nested results; in where with a lift
|
|
145
|
+
first / single zero-or-one (single errors on >1) — lookups in select
|
|
146
|
+
distinct on any consumer: dedup by projected value — nodes count distinct { select kind }
|
|
147
|
+
limit/offset bound the block's rows AFTER where/order/distinct, BEFORE the
|
|
148
|
+
consumer reduces: nodes exists { offset 1 } = at least two;
|
|
149
|
+
first { … offset 1 } = the second.
|
|
150
|
+
lift \`^name:\` in a where-collect binds values outward:
|
|
151
|
+
from docs where nodes collect { ^open: value where kind == "md:task" && !checked } select $path, open
|
|
152
|
+
joins $repo.<t> exists { where slug == ^ref } semi-join; !… exists anti-join;
|
|
153
|
+
$repo.nodes single { where kind == "person" && attrs.id == ^owner_id } lookup;
|
|
154
|
+
x in ^keys membership over a lifted set.
|
|
155
|
+
|
|
156
|
+
## select — projection
|
|
157
|
+
|
|
158
|
+
select a, alias: expr, nested: nodes collect { … }
|
|
159
|
+
bare key → the property (scalar-authored → scalar; list → array); $-intrinsics;
|
|
160
|
+
a call/arithmetic needs an alias (n: size(tags)) unless in values mode.
|
|
161
|
+
"$body" whole file bytes (docs) — docs_read is cheaper for one doc.
|
|
162
|
+
distinct select distinct type (dedup hits by projected value)
|
|
163
|
+
values select <ONE item> values → bare values instead of records:
|
|
164
|
+
from docs select distinct type values → values: ["hub","lab-note",…]
|
|
165
|
+
tags: tags collect { $value values } → a plain array
|
|
166
|
+
h: nodes first { name values where kind == "md:section" order by first_ordinal }
|
|
167
|
+
entries(x) a record as a collection ($key / $value per entry), key order:
|
|
168
|
+
select fm: entries(frontmatter) collect { k: $key, v: $value }
|
|
169
|
+
where entries(frontmatter) exists { where $key == "era" && $value > 1600 }
|
|
170
|
+
entries(inline), entries(attrs), a nested map, or a list (index keys)
|
|
171
|
+
$semantic_score is not a field — project semantic("…") under an alias instead.
|
|
172
|
+
|
|
173
|
+
## order by, limit/offset, pagination
|
|
174
|
+
|
|
175
|
+
order by era desc, $path asc absent values sort LAST (asc); ties → (path, id)
|
|
176
|
+
limit N / offset N define the result SET (integer literals);
|
|
177
|
+
the tool's limit/cursor then page WITHIN it
|
|
178
|
+
cursor opaque keyset on (path,id); a custom order by
|
|
179
|
+
disables it (you get the top page + truncated)
|
|
180
|
+
|
|
181
|
+
## Graph traversal (follow) and edges
|
|
182
|
+
|
|
183
|
+
from docs where <seed> follow doc.out docs the seed links TO
|
|
184
|
+
from docs where <seed> follow doc.in backlinks
|
|
185
|
+
follow doc.out { depth 3 } bound (1..8, default 8)
|
|
186
|
+
follow doc.out { where layer == "canon" } keep only matching successors
|
|
187
|
+
follow doc.out { frontier type == "practitioner" } cut, keeping the frontier row
|
|
188
|
+
follow distinct doc.out / follow doc.out { by group } identity control
|
|
189
|
+
Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|
|
|
190
|
+
depth|cycle), $leaf/$frontier, $ordinal — usable in select/order by and the
|
|
191
|
+
top-level post-walk where, NOT in the follow-local where/frontier.
|
|
192
|
+
Type-preserving relations: doc.out/doc.in, block.children, section.children,
|
|
193
|
+
section.subsections.
|
|
194
|
+
from edges where $dst_path == "notes/x.md" inbound edges to a doc, as rows
|
|
195
|
+
from edges where predicate == "depends_on" select $src, $dst_path
|
|
196
|
+
from docs where !doc.in_edges exists { } orphans (nothing links in)
|
|
197
|
+
(The \`graph\` tool is a convenience wrapper that compiles to a follow query.)
|
|
198
|
+
|
|
199
|
+
## semantic grain
|
|
200
|
+
|
|
201
|
+
from docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.
|
|
202
|
+
text()/where prune the candidate set; only order by reweights.
|
|
203
|
+
|
|
204
|
+
## Examples
|
|
205
|
+
|
|
206
|
+
from docs where layer == "working"
|
|
207
|
+
from docs where $path.startsWith("guides/") && $updated_at >= "2026-08-01"
|
|
208
|
+
from docs where "pricing" in list(tags) select layer, tags
|
|
209
|
+
from docs where inline.owner == "alice"
|
|
210
|
+
from docs where $title.lower().contains("q3 plan")
|
|
211
|
+
from docs where format == "yaml"
|
|
212
|
+
from docs where type == "practitioner" order by era desc limit 2
|
|
213
|
+
from docs where nodes none { where kind == "md:task" && !checked }
|
|
214
|
+
from docs where tags exists { where $value == "tria-prima" }
|
|
215
|
+
from docs where $path == "x.md" select fm: entries(frontmatter) collect { k: $key, v: $value }
|
|
216
|
+
from docs select owner_id, owner: $repo.nodes single { where kind == "person" && attrs.id == ^owner_id }
|
|
217
|
+
from blocks where type == "task" && !checked && under_heading("Launch") && doc.layer == "working"
|
|
218
|
+
from blocks where type == "paragraph" && has_edge("references", "d_92aaaaa")
|
|
219
|
+
from blocks order by semantic("identity preservation across edits") desc limit 10
|
|
220
|
+
from edges where dst_kind == "external" select $dst_uri
|
|
221
|
+
$repo.docs count { where layer == "canon" }
|
|
222
|
+
|
|
223
|
+
Common mistake: bare \`path.startsWith(...)\` meant the intrinsic — it fails loud
|
|
224
|
+
with a hint; write \`$path.startsWith(...)\`. Another: \`where tags == "x"\` on a
|
|
225
|
+
list-valued key is false — write \`"x" in tags\`.
|
|
226
|
+
`;
|
|
227
|
+
//# sourceMappingURL=reference.js.map
|