@schlessera/brain-ui-server 0.6.3 → 0.7.1
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/app.d.ts.map +1 -1
- package/dist/app.js +2 -0
- package/dist/app.js.map +1 -1
- package/dist/graph/reader.d.ts +71 -0
- package/dist/graph/reader.d.ts.map +1 -0
- package/dist/graph/reader.js +522 -0
- package/dist/graph/reader.js.map +1 -0
- package/dist/routes/graph.d.ts +256 -0
- package/dist/routes/graph.d.ts.map +1 -0
- package/dist/routes/graph.js +100 -0
- package/dist/routes/graph.js.map +1 -0
- package/package.json +3 -3
- package/src/app.ts +2 -0
- package/src/graph/reader.ts +667 -0
- package/src/routes/graph.ts +112 -0
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAoB1E,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAqBD,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB;;;EAyFvD"}
|
package/dist/app.js
CHANGED
|
@@ -11,6 +11,7 @@ import { filesRoutes } from "./routes/files.js";
|
|
|
11
11
|
import { createRenderRoutes } from "./routes/render.js";
|
|
12
12
|
import { providerRoutes } from "./routes/providers.js";
|
|
13
13
|
import { modelRoutes } from "./routes/models.js";
|
|
14
|
+
import { graphRoutes } from "./routes/graph.js";
|
|
14
15
|
import { resolveAuthMode, assertAuthConfig, authGuard, authRoutes, isWsAuthorized, } from "./middleware/auth.js";
|
|
15
16
|
import { passkeyPublicRoutes, passkeyManagementRoutes, passwordLoginDisabled, assertPasskeyConfig, } from "./middleware/passkeys.js";
|
|
16
17
|
import { configureDb } from "./db/client.js";
|
|
@@ -91,6 +92,7 @@ export function createApp(options = {}) {
|
|
|
91
92
|
app.route("/api", createRenderRoutes(options.renderer));
|
|
92
93
|
app.route("/api", providerRoutes);
|
|
93
94
|
app.route("/api", modelRoutes);
|
|
95
|
+
app.route("/api", graphRoutes);
|
|
94
96
|
// WebSocket endpoint. Browsers can't set headers on the WS handshake, so the
|
|
95
97
|
// upgrade authenticates via the session cookie (or IP/proxy header) INSIDE
|
|
96
98
|
// the handler — no header-modifying middleware may sit on this route.
|
package/dist/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read side of the knowledge graph.
|
|
3
|
+
*
|
|
4
|
+
* brain.db is opened read-only per request (the keyterm-builder precedent):
|
|
5
|
+
* WAL makes concurrent readers cheap, and a short-lived handle means a
|
|
6
|
+
* `brain sync` swapping the file underneath us can never leave the server
|
|
7
|
+
* holding a stale page cache. ui-server has no dependency on core, so the
|
|
8
|
+
* schema below is consumed as the documented direct-read contract, not as
|
|
9
|
+
* imported types.
|
|
10
|
+
*
|
|
11
|
+
* Everything derived (metrics, communities, layout, root distances) is
|
|
12
|
+
* precomputed by the CLI. What stays here is per-request work over the raw
|
|
13
|
+
* `links` table: ego extraction and BFS from an arbitrary root, both of which
|
|
14
|
+
* are too interactive to shell out for.
|
|
15
|
+
*/
|
|
16
|
+
import type { GraphMaintenanceResponse, GraphMetaResponse, GraphSubgraphResponse } from "@schlessera/brain-ui-sdk/protocol";
|
|
17
|
+
/** brain.db schema that first carries the graph tables. */
|
|
18
|
+
export declare const MIN_SCHEMA_VERSION = 8;
|
|
19
|
+
/** Response caps. Over them, the lowest-pagerank nodes/edges are dropped. */
|
|
20
|
+
export declare const MAX_NODES = 5000;
|
|
21
|
+
export declare const MAX_EDGES = 20000;
|
|
22
|
+
/** A neighborhood is meant to stay readable, so it caps well below MAX_NODES. */
|
|
23
|
+
export declare const MAX_NEIGHBORHOOD_NODES = 1500;
|
|
24
|
+
export declare const MAX_NEIGHBORHOOD_DEPTH = 3;
|
|
25
|
+
export declare const MAX_DISCOVERY_DEPTH = 8;
|
|
26
|
+
export type GraphDirection = "in" | "out" | "both";
|
|
27
|
+
export type GraphUnavailableReason = "schema" | "not_computed";
|
|
28
|
+
/** No graph to read: the repo predates schema v8, or nothing has computed one. */
|
|
29
|
+
export declare class GraphUnavailableError extends Error {
|
|
30
|
+
readonly reason: GraphUnavailableReason;
|
|
31
|
+
constructor(reason: GraphUnavailableReason);
|
|
32
|
+
}
|
|
33
|
+
/** A center/root path that matches no indexed markdown document. */
|
|
34
|
+
export declare class GraphNotFoundError extends Error {
|
|
35
|
+
readonly path: string;
|
|
36
|
+
constructor(path: string);
|
|
37
|
+
}
|
|
38
|
+
export interface GraphReadOptions {
|
|
39
|
+
/** Defaults to the repo the rest of the server reads (BRAIN_PATH). */
|
|
40
|
+
brainPath?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ClustersOptions extends GraphReadOptions {
|
|
43
|
+
community?: number;
|
|
44
|
+
includeIsolates?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface NeighborhoodOptions extends GraphReadOptions {
|
|
47
|
+
center: string;
|
|
48
|
+
depth?: number;
|
|
49
|
+
direction?: GraphDirection;
|
|
50
|
+
}
|
|
51
|
+
export interface DiscoveryOptions extends GraphReadOptions {
|
|
52
|
+
/** Omitted → the precomputed default root, virtual node included. */
|
|
53
|
+
root?: string;
|
|
54
|
+
direction?: "out" | "both";
|
|
55
|
+
maxDepth?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface MaintenanceOptions extends GraphReadOptions {
|
|
58
|
+
staleDays?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const DEFAULT_STALE_DAYS = 180;
|
|
61
|
+
export declare function getGraphMeta(opts?: GraphReadOptions): GraphMetaResponse;
|
|
62
|
+
export declare function getClusters(opts?: ClustersOptions): GraphSubgraphResponse;
|
|
63
|
+
/**
|
|
64
|
+
* Deliberately exempt from the schema gate: an ego graph needs nothing but the
|
|
65
|
+
* `links` table, so the local mode stays useful on a repo that has never seen a
|
|
66
|
+
* graph precompute.
|
|
67
|
+
*/
|
|
68
|
+
export declare function getNeighborhood(opts: NeighborhoodOptions): GraphSubgraphResponse;
|
|
69
|
+
export declare function getDiscovery(opts?: DiscoveryOptions): GraphSubgraphResponse;
|
|
70
|
+
export declare function getMaintenance(opts?: MaintenanceOptions): GraphMaintenanceResponse;
|
|
71
|
+
//# sourceMappingURL=reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../src/graph/reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,KAAK,EAGV,wBAAwB,EACxB,iBAAiB,EAEjB,qBAAqB,EACtB,MAAM,mCAAmC,CAAC;AAG3C,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,6EAA6E;AAC7E,eAAO,MAAM,SAAS,OAAO,CAAC;AAC9B,eAAO,MAAM,SAAS,QAAS,CAAC;AAChC,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC;AACnD,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,cAAc,CAAC;AAE/D,kFAAkF;AAClF,qBAAa,qBAAsB,SAAQ,KAAK;IAClC,QAAQ,CAAC,MAAM,EAAE,sBAAsB;gBAA9B,MAAM,EAAE,sBAAsB;CAIpD;AAED,oEAAoE;AACpE,qBAAa,kBAAmB,SAAQ,KAAK;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAIlC;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,gBAAgB;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAmB,SAAQ,gBAAgB;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAkVtC,wBAAgB,YAAY,CAAC,IAAI,GAAE,gBAAqB,GAAG,iBAAiB,CAkC3E;AAqBD,wBAAgB,WAAW,CAAC,IAAI,GAAE,eAAoB,GAAG,qBAAqB,CA0B7E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,qBAAqB,CAehF;AAED,wBAAgB,YAAY,CAAC,IAAI,GAAE,gBAAqB,GAAG,qBAAqB,CA2E/E;AAED,wBAAgB,cAAc,CAAC,IAAI,GAAE,kBAAuB,GAAG,wBAAwB,CAuDtF"}
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read side of the knowledge graph.
|
|
3
|
+
*
|
|
4
|
+
* brain.db is opened read-only per request (the keyterm-builder precedent):
|
|
5
|
+
* WAL makes concurrent readers cheap, and a short-lived handle means a
|
|
6
|
+
* `brain sync` swapping the file underneath us can never leave the server
|
|
7
|
+
* holding a stale page cache. ui-server has no dependency on core, so the
|
|
8
|
+
* schema below is consumed as the documented direct-read contract, not as
|
|
9
|
+
* imported types.
|
|
10
|
+
*
|
|
11
|
+
* Everything derived (metrics, communities, layout, root distances) is
|
|
12
|
+
* precomputed by the CLI. What stays here is per-request work over the raw
|
|
13
|
+
* `links` table: ego extraction and BFS from an arbitrary root, both of which
|
|
14
|
+
* are too interactive to shell out for.
|
|
15
|
+
*/
|
|
16
|
+
import { Database } from "bun:sqlite";
|
|
17
|
+
import { existsSync } from "fs";
|
|
18
|
+
import { basename, join } from "path";
|
|
19
|
+
import { getBrainRoot } from "../files/walker.js";
|
|
20
|
+
/** brain.db schema that first carries the graph tables. */
|
|
21
|
+
export const MIN_SCHEMA_VERSION = 8;
|
|
22
|
+
/** Response caps. Over them, the lowest-pagerank nodes/edges are dropped. */
|
|
23
|
+
export const MAX_NODES = 5000;
|
|
24
|
+
export const MAX_EDGES = 20_000;
|
|
25
|
+
/** A neighborhood is meant to stay readable, so it caps well below MAX_NODES. */
|
|
26
|
+
export const MAX_NEIGHBORHOOD_NODES = 1500;
|
|
27
|
+
export const MAX_NEIGHBORHOOD_DEPTH = 3;
|
|
28
|
+
export const MAX_DISCOVERY_DEPTH = 8;
|
|
29
|
+
/** No graph to read: the repo predates schema v8, or nothing has computed one. */
|
|
30
|
+
export class GraphUnavailableError extends Error {
|
|
31
|
+
reason;
|
|
32
|
+
constructor(reason) {
|
|
33
|
+
super(`graph_unavailable: ${reason}`);
|
|
34
|
+
this.reason = reason;
|
|
35
|
+
this.name = "GraphUnavailableError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** A center/root path that matches no indexed markdown document. */
|
|
39
|
+
export class GraphNotFoundError extends Error {
|
|
40
|
+
path;
|
|
41
|
+
constructor(path) {
|
|
42
|
+
super(`not_found: ${path}`);
|
|
43
|
+
this.path = path;
|
|
44
|
+
this.name = "GraphNotFoundError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export const DEFAULT_STALE_DAYS = 180;
|
|
48
|
+
// --- db access -------------------------------------------------------------
|
|
49
|
+
function openDb(brainPath) {
|
|
50
|
+
const dbPath = join(brainPath ?? getBrainRoot(), "brain.db");
|
|
51
|
+
if (!existsSync(dbPath))
|
|
52
|
+
throw new GraphUnavailableError("schema");
|
|
53
|
+
return new Database(dbPath, { readonly: true });
|
|
54
|
+
}
|
|
55
|
+
function withDb(brainPath, fn) {
|
|
56
|
+
const db = openDb(brainPath);
|
|
57
|
+
try {
|
|
58
|
+
return fn(db);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
db.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const GRAPH_TABLES = ["graph_metrics", "graph_communities", "graph_root_distances", "graph_layouts"];
|
|
65
|
+
/**
|
|
66
|
+
* A db can claim schema 8 in metadata and still lack the tables (a hand-copied
|
|
67
|
+
* file, an interrupted migration), so presence is checked rather than trusted.
|
|
68
|
+
*/
|
|
69
|
+
function hasGraphTables(db) {
|
|
70
|
+
const rows = db
|
|
71
|
+
.query(`SELECT name FROM sqlite_master WHERE type = 'table'
|
|
72
|
+
AND name IN ('graph_metrics', 'graph_communities', 'graph_root_distances', 'graph_layouts')`)
|
|
73
|
+
.all();
|
|
74
|
+
return rows.length === GRAPH_TABLES.length;
|
|
75
|
+
}
|
|
76
|
+
function readMetadata(db) {
|
|
77
|
+
try {
|
|
78
|
+
const rows = db.query("SELECT key, value FROM index_metadata").all();
|
|
79
|
+
return new Map(rows.map((r) => [r.key, r.value]));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return new Map();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function readSchemaVersion(meta) {
|
|
86
|
+
const parsed = Number.parseInt(meta.get("schema_version") ?? "", 10);
|
|
87
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
88
|
+
}
|
|
89
|
+
function requireComputedGraph(db, meta) {
|
|
90
|
+
if (readSchemaVersion(meta) < MIN_SCHEMA_VERSION || !hasGraphTables(db)) {
|
|
91
|
+
throw new GraphUnavailableError("schema");
|
|
92
|
+
}
|
|
93
|
+
if (!meta.get("graph_computed_at"))
|
|
94
|
+
throw new GraphUnavailableError("not_computed");
|
|
95
|
+
}
|
|
96
|
+
/** Only markdown notes are graph nodes; other assets carry no wiki links. */
|
|
97
|
+
const MARKDOWN_ONLY = "COALESCE(d.asset_type, 'markdown') = 'markdown'";
|
|
98
|
+
/**
|
|
99
|
+
* The analytical columns come from tables that do not exist below schema v8,
|
|
100
|
+
* so the select degrades to literals — that is what keeps the neighborhood
|
|
101
|
+
* endpoint working against an older repo.
|
|
102
|
+
*/
|
|
103
|
+
function nodeSelect(withGraphTables, extraColumns = "") {
|
|
104
|
+
const base = `d.id AS id, d.path AS path, d.title AS title, d.type AS type${extraColumns}`;
|
|
105
|
+
if (!withGraphTables) {
|
|
106
|
+
return `SELECT ${base},
|
|
107
|
+
0 AS in_degree, 0 AS out_degree,
|
|
108
|
+
NULL AS community, NULL AS pagerank, NULL AS x, NULL AS y
|
|
109
|
+
FROM documents d`;
|
|
110
|
+
}
|
|
111
|
+
return `SELECT ${base},
|
|
112
|
+
COALESCE(m.in_degree, 0) AS in_degree, COALESCE(m.out_degree, 0) AS out_degree,
|
|
113
|
+
m.community AS community, m.pagerank AS pagerank, gl.x AS x, gl.y AS y
|
|
114
|
+
FROM documents d
|
|
115
|
+
LEFT JOIN graph_metrics m ON m.document_id = d.id
|
|
116
|
+
LEFT JOIN graph_layouts gl ON gl.document_id = d.id AND gl.mode = 'clusters'`;
|
|
117
|
+
}
|
|
118
|
+
function toNode(row, distance) {
|
|
119
|
+
const node = {
|
|
120
|
+
id: row.id,
|
|
121
|
+
path: row.path,
|
|
122
|
+
title: row.title,
|
|
123
|
+
type: row.type,
|
|
124
|
+
inDegree: row.in_degree ?? 0,
|
|
125
|
+
outDegree: row.out_degree ?? 0,
|
|
126
|
+
};
|
|
127
|
+
if (row.community != null)
|
|
128
|
+
node.community = row.community;
|
|
129
|
+
if (row.pagerank != null)
|
|
130
|
+
node.pagerank = row.pagerank;
|
|
131
|
+
if (row.x != null && row.y != null) {
|
|
132
|
+
node.x = row.x;
|
|
133
|
+
node.y = row.y;
|
|
134
|
+
}
|
|
135
|
+
if (distance !== undefined)
|
|
136
|
+
node.distance = distance;
|
|
137
|
+
return node;
|
|
138
|
+
}
|
|
139
|
+
/** Chunked so a large id set never runs into SQLite's bound-parameter limit. */
|
|
140
|
+
function loadNodesByIds(db, ids, withGraphTables) {
|
|
141
|
+
const out = [];
|
|
142
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
143
|
+
const chunk = ids.slice(i, i + 500);
|
|
144
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
145
|
+
const rows = db
|
|
146
|
+
.query(`${nodeSelect(withGraphTables)} WHERE ${MARKDOWN_ONLY} AND d.id IN (${placeholders})`)
|
|
147
|
+
.all(...chunk);
|
|
148
|
+
out.push(...rows);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
function lookupDocumentId(db, path) {
|
|
153
|
+
const normalized = path.replace(/^\.?\//, "");
|
|
154
|
+
const row = db
|
|
155
|
+
.query(`SELECT d.id AS id FROM documents d WHERE ${MARKDOWN_ONLY} AND d.path = ?`)
|
|
156
|
+
.get(normalized);
|
|
157
|
+
if (!row)
|
|
158
|
+
throw new GraphNotFoundError(path);
|
|
159
|
+
return row.id;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The one definition of an edge, shared by the adjacency load and the meta
|
|
163
|
+
* count so /api/graph/meta agrees with `brain graph stats`: one edge per
|
|
164
|
+
* distinct (source, target) DOCUMENT pair — two wiki links that resolve to the
|
|
165
|
+
* same note are one edge — self-links dropped, and both ends markdown, since
|
|
166
|
+
* non-markdown assets are not graph nodes.
|
|
167
|
+
*/
|
|
168
|
+
const DISTINCT_EDGES_SQL = `SELECT DISTINCT l.source_id AS source, l.target_id AS target
|
|
169
|
+
FROM links l
|
|
170
|
+
JOIN documents d ON d.id = l.source_id AND ${MARKDOWN_ONLY}
|
|
171
|
+
JOIN documents t ON t.id = l.target_id AND COALESCE(t.asset_type, 'markdown') = 'markdown'
|
|
172
|
+
WHERE l.target_id IS NOT NULL AND l.target_id <> l.source_id`;
|
|
173
|
+
/** The whole link table as an edge list plus adjacency maps in both directions. */
|
|
174
|
+
function loadLinkStructure(db) {
|
|
175
|
+
const edges = db.query(DISTINCT_EDGES_SQL).all();
|
|
176
|
+
const out = new Map();
|
|
177
|
+
const inbound = new Map();
|
|
178
|
+
for (const edge of edges) {
|
|
179
|
+
push(out, edge.source, edge.target);
|
|
180
|
+
push(inbound, edge.target, edge.source);
|
|
181
|
+
}
|
|
182
|
+
return { edges, out, in: inbound };
|
|
183
|
+
}
|
|
184
|
+
function push(map, key, value) {
|
|
185
|
+
const existing = map.get(key);
|
|
186
|
+
if (existing)
|
|
187
|
+
existing.push(value);
|
|
188
|
+
else
|
|
189
|
+
map.set(key, [value]);
|
|
190
|
+
}
|
|
191
|
+
/** Breadth-first walk, level by level, stopping at `maxDepth` or `limit` nodes. */
|
|
192
|
+
function bfs(structure, start, direction, maxDepth, limit) {
|
|
193
|
+
const distances = new Map([[start, 0]]);
|
|
194
|
+
let frontier = [start];
|
|
195
|
+
let depth = 0;
|
|
196
|
+
let truncated = false;
|
|
197
|
+
while (frontier.length > 0 && depth < maxDepth && !truncated) {
|
|
198
|
+
depth++;
|
|
199
|
+
const next = [];
|
|
200
|
+
for (const id of frontier) {
|
|
201
|
+
const neighbors = [];
|
|
202
|
+
if (direction !== "in")
|
|
203
|
+
neighbors.push(...(structure.out.get(id) ?? []));
|
|
204
|
+
if (direction !== "out")
|
|
205
|
+
neighbors.push(...(structure.in.get(id) ?? []));
|
|
206
|
+
for (const neighbor of neighbors) {
|
|
207
|
+
if (distances.has(neighbor))
|
|
208
|
+
continue;
|
|
209
|
+
if (distances.size >= limit) {
|
|
210
|
+
truncated = true;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
distances.set(neighbor, depth);
|
|
214
|
+
next.push(neighbor);
|
|
215
|
+
}
|
|
216
|
+
if (truncated)
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
frontier = next;
|
|
220
|
+
}
|
|
221
|
+
return { distances, truncated };
|
|
222
|
+
}
|
|
223
|
+
// --- caps ------------------------------------------------------------------
|
|
224
|
+
/**
|
|
225
|
+
* Trims a subgraph to the response caps, keeping the highest-pagerank nodes
|
|
226
|
+
* (a virtual root always survives — dropping the thing the view is rooted on
|
|
227
|
+
* would be nonsense). Edges are then filtered to the surviving node set, which
|
|
228
|
+
* is also how a whole-corpus edge list is narrowed to the requested subgraph.
|
|
229
|
+
*/
|
|
230
|
+
function applyCaps(nodes, edges, maxNodes) {
|
|
231
|
+
let truncated = false;
|
|
232
|
+
let keptNodes = nodes;
|
|
233
|
+
if (nodes.length > maxNodes) {
|
|
234
|
+
truncated = true;
|
|
235
|
+
keptNodes = [...nodes]
|
|
236
|
+
.sort((a, b) => Number(b.virtual ?? false) - Number(a.virtual ?? false) || (b.pagerank ?? 0) - (a.pagerank ?? 0))
|
|
237
|
+
.slice(0, maxNodes);
|
|
238
|
+
}
|
|
239
|
+
const ids = new Set(keptNodes.map((n) => n.id));
|
|
240
|
+
let keptEdges = edges.filter((e) => ids.has(e.source) && ids.has(e.target));
|
|
241
|
+
if (keptEdges.length > MAX_EDGES) {
|
|
242
|
+
truncated = true;
|
|
243
|
+
const rank = new Map(keptNodes.map((n) => [n.id, n.pagerank ?? 0]));
|
|
244
|
+
keptEdges = keptEdges
|
|
245
|
+
.map((edge) => ({ edge, weight: Math.min(rank.get(edge.source) ?? 0, rank.get(edge.target) ?? 0) }))
|
|
246
|
+
.sort((a, b) => b.weight - a.weight)
|
|
247
|
+
.slice(0, MAX_EDGES)
|
|
248
|
+
.map((entry) => entry.edge);
|
|
249
|
+
}
|
|
250
|
+
return { nodes: keptNodes, edges: keptEdges, truncated };
|
|
251
|
+
}
|
|
252
|
+
// --- meta ------------------------------------------------------------------
|
|
253
|
+
function countGraph(db) {
|
|
254
|
+
try {
|
|
255
|
+
const edges = db.query(`SELECT COUNT(*) AS c FROM (${DISTINCT_EDGES_SQL})`).get();
|
|
256
|
+
return { nodeCount: countMarkdownDocs(db), edgeCount: edges?.c ?? 0 };
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return { nodeCount: 0, edgeCount: 0 };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Whether the precompute resolved a root at all.
|
|
264
|
+
*
|
|
265
|
+
* The key is deleted and rewritten each run, so its ABSENCE is the only signal
|
|
266
|
+
* that there is no root — an empty `graph_root_distances` does not mean the
|
|
267
|
+
* same thing. A named root whose links all failed to resolve is written with
|
|
268
|
+
* zero distance rows, and in that state every note really is unreachable.
|
|
269
|
+
*/
|
|
270
|
+
function hasResolvedRoot(meta) {
|
|
271
|
+
return Boolean(meta.get("graph_root"));
|
|
272
|
+
}
|
|
273
|
+
function countMarkdownDocs(db) {
|
|
274
|
+
return (db.query(`SELECT COUNT(*) AS c FROM documents d WHERE ${MARKDOWN_ONLY}`).get()?.c ?? 0);
|
|
275
|
+
}
|
|
276
|
+
function parseDefaultRoot(meta) {
|
|
277
|
+
const raw = meta.get("graph_root");
|
|
278
|
+
if (!raw)
|
|
279
|
+
return null;
|
|
280
|
+
if (raw.startsWith("virtual:"))
|
|
281
|
+
return { path: raw.slice("virtual:".length), virtual: true };
|
|
282
|
+
return { path: raw, virtual: false };
|
|
283
|
+
}
|
|
284
|
+
function parseRootLinks(meta) {
|
|
285
|
+
const raw = meta.get("graph_root_links");
|
|
286
|
+
if (!raw)
|
|
287
|
+
return [];
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse(raw);
|
|
290
|
+
return Array.isArray(parsed) ? parsed.filter((id) => Number.isFinite(id)) : [];
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function readCommunities(db) {
|
|
297
|
+
const rows = db
|
|
298
|
+
.query("SELECT community, size, label, top_terms FROM graph_communities ORDER BY size DESC, community ASC")
|
|
299
|
+
.all();
|
|
300
|
+
return rows.map((row) => {
|
|
301
|
+
let topTerms = [];
|
|
302
|
+
try {
|
|
303
|
+
const parsed = row.top_terms ? JSON.parse(row.top_terms) : [];
|
|
304
|
+
if (Array.isArray(parsed))
|
|
305
|
+
topTerms = parsed.filter((t) => typeof t === "string");
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
topTerms = [];
|
|
309
|
+
}
|
|
310
|
+
return { community: row.community, size: row.size, label: row.label ?? null, topTerms };
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* An older CLI indexing a v8 repo leaves the graph tables untouched rather than
|
|
315
|
+
* broken, so freshness is a comparison against the newest index run, not a
|
|
316
|
+
* structural check.
|
|
317
|
+
*/
|
|
318
|
+
function isStale(db, computedAt) {
|
|
319
|
+
const row = db.query("SELECT MAX(indexed_at) AS newest FROM documents").get();
|
|
320
|
+
if (!row?.newest)
|
|
321
|
+
return false;
|
|
322
|
+
const computed = Date.parse(computedAt);
|
|
323
|
+
const indexed = Date.parse(row.newest);
|
|
324
|
+
if (Number.isNaN(computed) || Number.isNaN(indexed))
|
|
325
|
+
return false;
|
|
326
|
+
return computed < indexed;
|
|
327
|
+
}
|
|
328
|
+
export function getGraphMeta(opts = {}) {
|
|
329
|
+
let db;
|
|
330
|
+
try {
|
|
331
|
+
db = openDb(opts.brainPath);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// No brain.db at all — indistinguishable, from the client's side, from a
|
|
335
|
+
// repo whose schema is too old to carry a graph.
|
|
336
|
+
return unavailable("schema", 0, { nodeCount: 0, edgeCount: 0 });
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const meta = readMetadata(db);
|
|
340
|
+
const schemaVersion = readSchemaVersion(meta);
|
|
341
|
+
const counts = countGraph(db);
|
|
342
|
+
if (schemaVersion < MIN_SCHEMA_VERSION || !hasGraphTables(db)) {
|
|
343
|
+
return unavailable("schema", schemaVersion, counts);
|
|
344
|
+
}
|
|
345
|
+
const computedAt = meta.get("graph_computed_at") ?? null;
|
|
346
|
+
if (!computedAt)
|
|
347
|
+
return unavailable("not_computed", schemaVersion, counts);
|
|
348
|
+
const response = {
|
|
349
|
+
available: true,
|
|
350
|
+
schemaVersion,
|
|
351
|
+
computedAt,
|
|
352
|
+
stale: isStale(db, computedAt),
|
|
353
|
+
...counts,
|
|
354
|
+
communities: readCommunities(db),
|
|
355
|
+
defaultRoot: parseDefaultRoot(meta),
|
|
356
|
+
};
|
|
357
|
+
if (meta.get("graph_layout_skipped") === "1")
|
|
358
|
+
response.layoutSkipped = true;
|
|
359
|
+
return response;
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
db.close();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function unavailable(reason, schemaVersion, counts) {
|
|
366
|
+
return {
|
|
367
|
+
available: false,
|
|
368
|
+
reason,
|
|
369
|
+
schemaVersion,
|
|
370
|
+
computedAt: null,
|
|
371
|
+
stale: false,
|
|
372
|
+
...counts,
|
|
373
|
+
communities: [],
|
|
374
|
+
defaultRoot: null,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// --- modes -----------------------------------------------------------------
|
|
378
|
+
export function getClusters(opts = {}) {
|
|
379
|
+
return withDb(opts.brainPath, (db) => {
|
|
380
|
+
requireComputedGraph(db, readMetadata(db));
|
|
381
|
+
const filters = [MARKDOWN_ONLY];
|
|
382
|
+
const params = [];
|
|
383
|
+
if (opts.community !== undefined) {
|
|
384
|
+
filters.push("m.community = ?");
|
|
385
|
+
params.push(opts.community);
|
|
386
|
+
}
|
|
387
|
+
if (!opts.includeIsolates) {
|
|
388
|
+
filters.push("(COALESCE(m.in_degree, 0) + COALESCE(m.out_degree, 0)) > 0");
|
|
389
|
+
}
|
|
390
|
+
const rows = db
|
|
391
|
+
.query(`${nodeSelect(true)} WHERE ${filters.join(" AND ")} ORDER BY m.pagerank DESC, d.id ASC`)
|
|
392
|
+
.all(...params);
|
|
393
|
+
return applyCaps(rows.map((row) => toNode(row)), loadLinkStructure(db).edges, MAX_NODES);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Deliberately exempt from the schema gate: an ego graph needs nothing but the
|
|
398
|
+
* `links` table, so the local mode stays useful on a repo that has never seen a
|
|
399
|
+
* graph precompute.
|
|
400
|
+
*/
|
|
401
|
+
export function getNeighborhood(opts) {
|
|
402
|
+
const depth = clamp(opts.depth ?? 1, 1, MAX_NEIGHBORHOOD_DEPTH);
|
|
403
|
+
const direction = opts.direction ?? "both";
|
|
404
|
+
return withDb(opts.brainPath, (db) => {
|
|
405
|
+
const centerId = lookupDocumentId(db, opts.center);
|
|
406
|
+
const withGraphTables = hasGraphTables(db);
|
|
407
|
+
const structure = loadLinkStructure(db);
|
|
408
|
+
const { distances, truncated } = bfs(structure, centerId, direction, depth, MAX_NEIGHBORHOOD_NODES);
|
|
409
|
+
const rows = loadNodesByIds(db, [...distances.keys()], withGraphTables);
|
|
410
|
+
const nodes = rows.map((row) => toNode(row, distances.get(row.id)));
|
|
411
|
+
const capped = applyCaps(nodes, structure.edges, MAX_NEIGHBORHOOD_NODES);
|
|
412
|
+
return { ...capped, truncated: capped.truncated || truncated };
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
export function getDiscovery(opts = {}) {
|
|
416
|
+
const maxDepth = clamp(opts.maxDepth ?? MAX_DISCOVERY_DEPTH, 1, MAX_DISCOVERY_DEPTH);
|
|
417
|
+
const direction = opts.direction ?? "out";
|
|
418
|
+
return withDb(opts.brainPath, (db) => {
|
|
419
|
+
const meta = readMetadata(db);
|
|
420
|
+
requireComputedGraph(db, meta);
|
|
421
|
+
const structure = loadLinkStructure(db);
|
|
422
|
+
const total = countMarkdownDocs(db);
|
|
423
|
+
if (opts.root) {
|
|
424
|
+
const rootId = lookupDocumentId(db, opts.root);
|
|
425
|
+
const { distances, truncated } = bfs(structure, rootId, direction, maxDepth, MAX_NODES);
|
|
426
|
+
const rows = loadNodesByIds(db, [...distances.keys()], true);
|
|
427
|
+
const capped = applyCaps(rows.map((row) => toNode(row, distances.get(row.id))), structure.edges, MAX_NODES);
|
|
428
|
+
// Reach is a property of the graph, not of the scene, so it is measured
|
|
429
|
+
// by a second walk with no depth window. A BFS can neither exceed the
|
|
430
|
+
// node count in levels nor in nodes, so `total` bounds both exactly.
|
|
431
|
+
const reachable = bfs(structure, rootId, direction, total, total).distances.size;
|
|
432
|
+
return {
|
|
433
|
+
...capped,
|
|
434
|
+
truncated: capped.truncated || truncated,
|
|
435
|
+
reachableCount: reachable,
|
|
436
|
+
unreachableCount: Math.max(0, total - reachable),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
// Default root: the distances were computed at index time, so `direction`
|
|
440
|
+
// does not apply here — the precomputed walk is the one that was run.
|
|
441
|
+
const distanceRows = db
|
|
442
|
+
.query("SELECT document_id, distance FROM graph_root_distances WHERE distance <= ?")
|
|
443
|
+
.all(maxDepth);
|
|
444
|
+
const distances = new Map(distanceRows.map((row) => [row.document_id, row.distance]));
|
|
445
|
+
const rows = loadNodesByIds(db, [...distances.keys()], true);
|
|
446
|
+
const nodes = rows.map((row) => toNode(row, distances.get(row.id)));
|
|
447
|
+
const root = parseDefaultRoot(meta);
|
|
448
|
+
const edges = [...structure.edges];
|
|
449
|
+
if (root?.virtual) {
|
|
450
|
+
// The entry file is excluded from indexing, so it has no documents row:
|
|
451
|
+
// the node standing in for it is synthesized here, at id 0.
|
|
452
|
+
const rootLinks = parseRootLinks(meta).filter((id) => distances.has(id));
|
|
453
|
+
nodes.unshift({
|
|
454
|
+
id: 0,
|
|
455
|
+
path: root.path,
|
|
456
|
+
title: basename(root.path),
|
|
457
|
+
type: "root",
|
|
458
|
+
inDegree: 0,
|
|
459
|
+
outDegree: rootLinks.length,
|
|
460
|
+
distance: 0,
|
|
461
|
+
virtual: true,
|
|
462
|
+
});
|
|
463
|
+
edges.unshift(...rootLinks.map((id) => ({ source: 0, target: id })));
|
|
464
|
+
}
|
|
465
|
+
const response = applyCaps(nodes, edges, MAX_NODES);
|
|
466
|
+
if (hasResolvedRoot(meta)) {
|
|
467
|
+
// Every distance row is one reached DOCUMENT — a virtual root is none, so
|
|
468
|
+
// this needs no adjustment for the synthesized node. Counted over the
|
|
469
|
+
// whole table, not the depth-limited scene above.
|
|
470
|
+
const reachable = db.query("SELECT COUNT(*) AS c FROM graph_root_distances").get()?.c ?? 0;
|
|
471
|
+
response.reachableCount = reachable;
|
|
472
|
+
response.unreachableCount = Math.max(0, total - reachable);
|
|
473
|
+
}
|
|
474
|
+
return response;
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
export function getMaintenance(opts = {}) {
|
|
478
|
+
const staleDays = clamp(opts.staleDays ?? DEFAULT_STALE_DAYS, 1, 3650);
|
|
479
|
+
return withDb(opts.brainPath, (db) => {
|
|
480
|
+
const meta = readMetadata(db);
|
|
481
|
+
requireComputedGraph(db, meta);
|
|
482
|
+
const orphans = db
|
|
483
|
+
.query(`${nodeSelect(true)} WHERE ${MARKDOWN_ONLY}
|
|
484
|
+
AND COALESCE(m.in_degree, 0) = 0 AND COALESCE(m.out_degree, 0) = 0
|
|
485
|
+
ORDER BY d.updated DESC, d.id ASC LIMIT ?`)
|
|
486
|
+
.all(MAX_NODES)
|
|
487
|
+
.map((row) => toNode(row));
|
|
488
|
+
// Keyed on whether a root was RESOLVED, not on whether any note has a
|
|
489
|
+
// distance: a named root that reached nothing leaves the distances empty,
|
|
490
|
+
// and then the whole corpus genuinely is unreachable. Only the absence of a
|
|
491
|
+
// root makes the finding meaningless.
|
|
492
|
+
const unreachable = !hasResolvedRoot(meta)
|
|
493
|
+
? []
|
|
494
|
+
: db
|
|
495
|
+
.query(`${nodeSelect(true)}
|
|
496
|
+
LEFT JOIN graph_root_distances rd ON rd.document_id = d.id
|
|
497
|
+
WHERE ${MARKDOWN_ONLY} AND rd.document_id IS NULL
|
|
498
|
+
ORDER BY m.pagerank DESC, d.id ASC LIMIT ?`)
|
|
499
|
+
.all(MAX_NODES)
|
|
500
|
+
.map((row) => toNode(row));
|
|
501
|
+
const brokenLinks = db
|
|
502
|
+
.query(`SELECT d.path AS sourcePath, l.target AS target
|
|
503
|
+
FROM links l JOIN documents d ON d.id = l.source_id
|
|
504
|
+
WHERE l.target_id IS NULL
|
|
505
|
+
ORDER BY d.path ASC, l.target ASC LIMIT ?`)
|
|
506
|
+
.all(MAX_NODES);
|
|
507
|
+
// `documents.updated` is a date, `indexed_at` a timestamp — compare on the
|
|
508
|
+
// date prefix so a mix of both formats still sorts correctly.
|
|
509
|
+
const cutoff = new Date(Date.now() - staleDays * 86_400_000).toISOString().slice(0, 10);
|
|
510
|
+
const stale = db
|
|
511
|
+
.query(`${nodeSelect(true, ", d.updated AS updated")}
|
|
512
|
+
WHERE ${MARKDOWN_ONLY} AND substr(d.updated, 1, 10) < ?
|
|
513
|
+
ORDER BY d.updated ASC, d.id ASC LIMIT ?`)
|
|
514
|
+
.all(cutoff, MAX_NODES)
|
|
515
|
+
.map((row) => ({ ...toNode(row), updated: row.updated }));
|
|
516
|
+
return { orphans, unreachable, brokenLinks, stale, staleDays };
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function clamp(value, min, max) {
|
|
520
|
+
return Math.min(max, Math.max(min, value));
|
|
521
|
+
}
|
|
522
|
+
//# sourceMappingURL=reader.js.map
|