@anokye-labs/kbexplorer-engine 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/README.md +77 -0
- package/dist/build-manifest-BT0sY84J.d.ts +239 -0
- package/dist/build-manifest-BkgZUL0E.d.cts +239 -0
- package/dist/chunk-ASLGNOYV.js +220 -0
- package/dist/chunk-BA526FDQ.js +16 -0
- package/dist/chunk-DJ2DBEEK.js +424 -0
- package/dist/chunk-FJ4GTN4U.js +973 -0
- package/dist/chunk-HXAS3BSD.js +187 -0
- package/dist/chunk-IWEURXYH.js +273 -0
- package/dist/chunk-JNQVSNLC.js +55 -0
- package/dist/chunk-MW4BLXVK.js +72 -0
- package/dist/chunk-NOGBKE7C.js +351 -0
- package/dist/chunk-XVI6CBSX.js +412 -0
- package/dist/chunk-YTMIERYM.js +67 -0
- package/dist/github-api-source-KV5HZARH.js +4 -0
- package/dist/index.cjs +6218 -0
- package/dist/index.d.cts +1396 -0
- package/dist/index.d.ts +1396 -0
- package/dist/index.js +2892 -0
- package/dist/node-wasm-VSXBOCPD.js +9 -0
- package/dist/plugin-loader-K4VU6ODV.js +2 -0
- package/dist/repo-data-0JvFdLGv.d.cts +461 -0
- package/dist/repo-data-0JvFdLGv.d.ts +461 -0
- package/dist/sources.cjs +1191 -0
- package/dist/sources.d.cts +194 -0
- package/dist/sources.d.ts +194 -0
- package/dist/sources.js +357 -0
- package/dist/sqlite-graph-store-NH2NHL2B.js +2 -0
- package/dist/sqlite-runtime-CysPjjzX.d.cts +96 -0
- package/dist/sqlite-runtime-CysPjjzX.d.ts +96 -0
- package/dist/store-orchestrator-337XWIXO.js +5 -0
- package/dist/store.cjs +1112 -0
- package/dist/store.d.cts +37 -0
- package/dist/store.d.ts +37 -0
- package/dist/store.js +7 -0
- package/package.json +70 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { assignIdentity } from './chunk-JNQVSNLC.js';
|
|
2
|
+
import { checkProviderCompatibility, PROVIDER_API_VERSION } from '@anokye-labs/kbexplorer-core';
|
|
3
|
+
import { isAbsolute, resolve, sep } from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
|
|
6
|
+
// src/providers/wikipedia-provider.ts
|
|
7
|
+
var WikipediaProvider = class {
|
|
8
|
+
id;
|
|
9
|
+
name;
|
|
10
|
+
dependencies = [];
|
|
11
|
+
articles;
|
|
12
|
+
defaultCluster;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.id = `wikipedia-${config.name?.replace(/\s+/g, "-").toLowerCase() ?? "default"}`;
|
|
15
|
+
this.name = config.name ?? "Wikipedia";
|
|
16
|
+
this.defaultCluster = config.cluster ?? "reference";
|
|
17
|
+
this.articles = config.options?.articles ?? [];
|
|
18
|
+
}
|
|
19
|
+
async resolve(_config, _existingNodes) {
|
|
20
|
+
const nodes = [];
|
|
21
|
+
const fetches = this.articles.map(async (article) => {
|
|
22
|
+
try {
|
|
23
|
+
const encoded = encodeURIComponent(article.title.replace(/\s+/g, "_"));
|
|
24
|
+
const resp = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encoded}`);
|
|
25
|
+
if (!resp.ok) return null;
|
|
26
|
+
const data = await resp.json();
|
|
27
|
+
return { article, data };
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const results = await Promise.all(fetches);
|
|
33
|
+
for (const result of results) {
|
|
34
|
+
if (!result) continue;
|
|
35
|
+
const { article, data } = result;
|
|
36
|
+
const nodeId = article.id ?? `wiki-${data.title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
|
37
|
+
const connections = (article.connections ?? []).map((to) => ({
|
|
38
|
+
to,
|
|
39
|
+
description: `Referenced by ${data.title}`
|
|
40
|
+
}));
|
|
41
|
+
const content = `<p>${data.extract_html ?? data.extract}</p>
|
|
42
|
+
<p><a href="${data.content_urls.desktop.page}" target="_blank">Read on Wikipedia \u2192</a></p>`;
|
|
43
|
+
const rawContent = `${data.extract}
|
|
44
|
+
|
|
45
|
+
[Read on Wikipedia \u2192](${data.content_urls.desktop.page})`;
|
|
46
|
+
const node = {
|
|
47
|
+
id: nodeId,
|
|
48
|
+
title: data.title,
|
|
49
|
+
cluster: article.cluster ?? this.defaultCluster,
|
|
50
|
+
content,
|
|
51
|
+
rawContent,
|
|
52
|
+
emoji: "Globe",
|
|
53
|
+
connections,
|
|
54
|
+
source: { type: "external", provider: this.id },
|
|
55
|
+
provider: this.id
|
|
56
|
+
};
|
|
57
|
+
const identity = assignIdentity(node);
|
|
58
|
+
if (identity !== void 0) node.identity = identity;
|
|
59
|
+
nodes.push(node);
|
|
60
|
+
}
|
|
61
|
+
return { nodes, edges: [] };
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/providers/orgchart-provider.ts
|
|
66
|
+
var OrgChartProvider = class {
|
|
67
|
+
id;
|
|
68
|
+
name;
|
|
69
|
+
dependencies = [];
|
|
70
|
+
people;
|
|
71
|
+
defaultCluster;
|
|
72
|
+
constructor(config) {
|
|
73
|
+
this.id = `orgchart-${config.name?.replace(/\s+/g, "-").toLowerCase() ?? "default"}`;
|
|
74
|
+
this.name = config.name ?? "Org Chart";
|
|
75
|
+
this.defaultCluster = config.cluster ?? "team";
|
|
76
|
+
this.people = config.options?.people ?? [];
|
|
77
|
+
}
|
|
78
|
+
async resolve(_config, _existingNodes) {
|
|
79
|
+
const nodes = [];
|
|
80
|
+
for (const person of this.people) {
|
|
81
|
+
const connections = [];
|
|
82
|
+
for (const managerId of person.reports ?? []) {
|
|
83
|
+
connections.push({
|
|
84
|
+
to: `org-${managerId}`,
|
|
85
|
+
description: `Reports to`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
for (const target of person.connections ?? []) {
|
|
89
|
+
connections.push({
|
|
90
|
+
to: target,
|
|
91
|
+
description: `Owns`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const role = person.role ?? "";
|
|
95
|
+
const content = `<h2>${person.name}</h2><p><strong>${role}</strong></p>`;
|
|
96
|
+
const rawContent = `## ${person.name}
|
|
97
|
+
|
|
98
|
+
**${role}**`;
|
|
99
|
+
const node = {
|
|
100
|
+
id: `org-${person.id}`,
|
|
101
|
+
title: person.name,
|
|
102
|
+
cluster: this.defaultCluster,
|
|
103
|
+
content,
|
|
104
|
+
rawContent,
|
|
105
|
+
emoji: person.emoji ?? "Person",
|
|
106
|
+
connections,
|
|
107
|
+
source: { type: "external", provider: this.id },
|
|
108
|
+
provider: this.id
|
|
109
|
+
};
|
|
110
|
+
const identity = assignIdentity(node);
|
|
111
|
+
if (identity !== void 0) node.identity = identity;
|
|
112
|
+
nodes.push(node);
|
|
113
|
+
}
|
|
114
|
+
return { nodes, edges: [] };
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/plugin-loader.ts
|
|
119
|
+
var HOST_CONTRACT = {
|
|
120
|
+
apiVersion: PROVIDER_API_VERSION,
|
|
121
|
+
capabilities: ["graph:nodes", "graph:edges"]
|
|
122
|
+
};
|
|
123
|
+
function resolveImportBaseUrl(importBaseUrl) {
|
|
124
|
+
if (importBaseUrl instanceof URL) {
|
|
125
|
+
const normalizedBaseUrl = new URL(importBaseUrl.href);
|
|
126
|
+
if (normalizedBaseUrl.protocol === "file:" && !normalizedBaseUrl.pathname.endsWith("/")) {
|
|
127
|
+
normalizedBaseUrl.pathname = `${normalizedBaseUrl.pathname}/`;
|
|
128
|
+
}
|
|
129
|
+
return normalizedBaseUrl;
|
|
130
|
+
}
|
|
131
|
+
if (importBaseUrl.startsWith("file:")) {
|
|
132
|
+
const normalizedBaseUrl = new URL(importBaseUrl);
|
|
133
|
+
if (normalizedBaseUrl.protocol === "file:" && !normalizedBaseUrl.pathname.endsWith("/")) {
|
|
134
|
+
normalizedBaseUrl.pathname = `${normalizedBaseUrl.pathname}/`;
|
|
135
|
+
}
|
|
136
|
+
return normalizedBaseUrl;
|
|
137
|
+
}
|
|
138
|
+
const absolutePath = isAbsolute(importBaseUrl) ? importBaseUrl : resolve(importBaseUrl);
|
|
139
|
+
const normalizedPath = absolutePath.endsWith(sep) ? absolutePath : `${absolutePath}${sep}`;
|
|
140
|
+
return pathToFileURL(normalizedPath);
|
|
141
|
+
}
|
|
142
|
+
function classifySpecifier(specifier) {
|
|
143
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) return "local";
|
|
144
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(specifier) || specifier.startsWith("/") || specifier.startsWith("\\")) {
|
|
145
|
+
return "rejected";
|
|
146
|
+
}
|
|
147
|
+
return "bare";
|
|
148
|
+
}
|
|
149
|
+
function adaptCoreProvider(provider) {
|
|
150
|
+
return {
|
|
151
|
+
id: provider.id,
|
|
152
|
+
name: provider.name,
|
|
153
|
+
dependencies: provider.dependencies ?? [],
|
|
154
|
+
async resolve(config, existingNodes) {
|
|
155
|
+
const { nodes, edges } = await provider.resolve({ config, existingNodes });
|
|
156
|
+
return { nodes, edges };
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function loadModuleProvider(config, options) {
|
|
161
|
+
const specifier = config.module;
|
|
162
|
+
if (!specifier) return null;
|
|
163
|
+
if (classifySpecifier(specifier) === "rejected") {
|
|
164
|
+
console.warn(
|
|
165
|
+
`[kbexplorer] Provider module "${specifier}" is not a local (\`./\`, \`../\`) or bare npm package specifier; absolute/URL specifiers are not supported (no remote code execution). Skipping.`
|
|
166
|
+
);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const importTarget = classifySpecifier(specifier) === "local" && options?.importBaseUrl ? new URL(specifier, resolveImportBaseUrl(options.importBaseUrl)).href : specifier;
|
|
170
|
+
try {
|
|
171
|
+
const mod = await import(
|
|
172
|
+
/* @vite-ignore */
|
|
173
|
+
importTarget
|
|
174
|
+
);
|
|
175
|
+
const factory = mod.default;
|
|
176
|
+
if (typeof factory !== "function") {
|
|
177
|
+
console.warn(
|
|
178
|
+
`[kbexplorer] Provider module "${specifier}" has no default-export factory (use defineProvider()).`
|
|
179
|
+
);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const compat = checkProviderCompatibility(mod, HOST_CONTRACT);
|
|
183
|
+
if (!compat.compatible) {
|
|
184
|
+
console.warn(
|
|
185
|
+
`[kbexplorer] Provider module "${specifier}" ${compat.reason}. Skipping.`
|
|
186
|
+
);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return adaptCoreProvider(factory(config));
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
192
|
+
console.warn(`[kbexplorer] Failed to load provider module "${specifier}": ${message}`);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function loadExternalProviders(configs, options) {
|
|
197
|
+
const providers = [];
|
|
198
|
+
for (const config of configs) {
|
|
199
|
+
if (config.module) {
|
|
200
|
+
const provider = await loadModuleProvider(config, options);
|
|
201
|
+
if (provider) providers.push(provider);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
switch (config.type) {
|
|
205
|
+
case "wikipedia":
|
|
206
|
+
providers.push(new WikipediaProvider(config));
|
|
207
|
+
break;
|
|
208
|
+
case "orgchart":
|
|
209
|
+
providers.push(new OrgChartProvider(config));
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
console.warn(
|
|
213
|
+
`[kbexplorer] Provider "${config.name ?? config.type}" is not a built-in type and declares no "module" specifier; skipping.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return providers;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { OrgChartProvider, WikipediaProvider, loadExternalProviders };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/store/config.ts
|
|
2
|
+
function resolveGraphStoreOptions(env) {
|
|
3
|
+
const engineEnv = env ?? {};
|
|
4
|
+
const raw = engineEnv.VITE_KB_GRAPH_STORE;
|
|
5
|
+
const value = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
|
6
|
+
if (!value || value === "off" || value === "false" || value === "0") {
|
|
7
|
+
return { mode: "off" };
|
|
8
|
+
}
|
|
9
|
+
if (value === "sqlite") return { mode: "sqlite" };
|
|
10
|
+
throw new Error(`Unsupported VITE_KB_GRAPH_STORE value: ${String(raw)}`);
|
|
11
|
+
}
|
|
12
|
+
function isGraphStoreEnabled(env) {
|
|
13
|
+
return resolveGraphStoreOptions(env).mode === "sqlite";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { isGraphStoreEnabled, resolveGraphStoreOptions };
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { filterAccessWithheld, extractIssueRefs, renderSafeMarkdown, splitIntoSections } from './chunk-FJ4GTN4U.js';
|
|
2
|
+
|
|
3
|
+
// src/edge-weights.ts
|
|
4
|
+
var EDGE_TYPE_WEIGHTS = {
|
|
5
|
+
contains: 5,
|
|
6
|
+
derived_from: 3,
|
|
7
|
+
imports: 2,
|
|
8
|
+
references: 2,
|
|
9
|
+
frontmatter: 1.5,
|
|
10
|
+
cross_references: 1.5,
|
|
11
|
+
modifies: 1,
|
|
12
|
+
closes: 2,
|
|
13
|
+
mentions: 0.5,
|
|
14
|
+
related: 0.3
|
|
15
|
+
};
|
|
16
|
+
function getEdgeWeight(type) {
|
|
17
|
+
return EDGE_TYPE_WEIGHTS[type ?? "related"] ?? 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/node-types/registry.ts
|
|
21
|
+
var registry = /* @__PURE__ */ new Map();
|
|
22
|
+
var BUILT_IN_NODE_TYPES = [
|
|
23
|
+
{ id: "authored", layer: "content", label: "Authored" },
|
|
24
|
+
{ id: "readme", layer: "content", label: "README" },
|
|
25
|
+
{ id: "derived", layer: "content", label: "Derived" },
|
|
26
|
+
{ id: "section", layer: "content", label: "Section" },
|
|
27
|
+
{ id: "structured", layer: "content", label: "Structured" },
|
|
28
|
+
{ id: "issue", layer: "work", label: "Issue" },
|
|
29
|
+
{ id: "pull_request", layer: "work", label: "Pull Request" },
|
|
30
|
+
{ id: "commit", layer: "work", label: "Commit" },
|
|
31
|
+
{ id: "branch", layer: "work", label: "Branch" },
|
|
32
|
+
{ id: "workflow", layer: "work", label: "Workflow" },
|
|
33
|
+
{ id: "repository", layer: "work", label: "Repository" },
|
|
34
|
+
{ id: "release", layer: "work", label: "Release", cluster: "releases", description: "A GitHub release (tag, name, release notes)." },
|
|
35
|
+
// Person nodes derived from GitHub activity (#235)
|
|
36
|
+
{ id: "person", layer: "work", label: "Person", cluster: "person", description: "A person derived from GitHub activity or a content-model descriptor." },
|
|
37
|
+
{ id: "file", layer: "file", label: "File" },
|
|
38
|
+
{ id: "external", layer: "file", label: "External" }
|
|
39
|
+
];
|
|
40
|
+
function registerType(def) {
|
|
41
|
+
registry.set(def.id, def);
|
|
42
|
+
}
|
|
43
|
+
function resolveType(id) {
|
|
44
|
+
if (!id) return void 0;
|
|
45
|
+
return registry.get(id);
|
|
46
|
+
}
|
|
47
|
+
function hasType(id) {
|
|
48
|
+
return registry.has(id);
|
|
49
|
+
}
|
|
50
|
+
function getRegisteredTypes() {
|
|
51
|
+
return [...registry.values()];
|
|
52
|
+
}
|
|
53
|
+
function registerBuiltInNodeTypes() {
|
|
54
|
+
for (const def of BUILT_IN_NODE_TYPES) {
|
|
55
|
+
if (!registry.has(def.id)) registry.set(def.id, def);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function resetNodeTypeRegistry() {
|
|
59
|
+
registry.clear();
|
|
60
|
+
registerBuiltInNodeTypes();
|
|
61
|
+
}
|
|
62
|
+
function resolveNodeLayer(node) {
|
|
63
|
+
const byEntity = node.entityType ? registry.get(node.entityType) : void 0;
|
|
64
|
+
if (byEntity?.layer) return byEntity.layer;
|
|
65
|
+
const bySource = registry.get(node.source.type);
|
|
66
|
+
if (bySource?.layer) return bySource.layer;
|
|
67
|
+
return "file";
|
|
68
|
+
}
|
|
69
|
+
function resolveTypeCluster(node) {
|
|
70
|
+
const byEntity = node.entityType ? registry.get(node.entityType) : void 0;
|
|
71
|
+
if (byEntity?.cluster) return byEntity.cluster;
|
|
72
|
+
return registry.get(node.source.type)?.cluster;
|
|
73
|
+
}
|
|
74
|
+
registerBuiltInNodeTypes();
|
|
75
|
+
|
|
76
|
+
// src/graph.ts
|
|
77
|
+
function buildGraph(nodes, clusters) {
|
|
78
|
+
nodes = filterAccessWithheld(nodes);
|
|
79
|
+
for (const node of nodes) {
|
|
80
|
+
node.layer = resolveNodeLayer(node);
|
|
81
|
+
}
|
|
82
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
83
|
+
for (const n of nodes) {
|
|
84
|
+
const prev = nodeMap.get(n.id);
|
|
85
|
+
if (prev && (prev.provider ?? "(none)") !== (n.provider ?? "(none)")) {
|
|
86
|
+
console.warn(
|
|
87
|
+
`[kbexplorer] cross-provider id collision: "${n.id}" is produced by provider "${prev.provider ?? "(none)"}" and provider "${n.provider ?? "(none)"}" \u2014 edge/related resolution will last-win on the latter. Give one a distinct id.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
nodeMap.set(n.id, n);
|
|
91
|
+
}
|
|
92
|
+
const edges = buildEdges(nodes, nodeMap);
|
|
93
|
+
const connected = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const e of edges) {
|
|
95
|
+
connected.add(e.from);
|
|
96
|
+
connected.add(e.to);
|
|
97
|
+
}
|
|
98
|
+
const orphans = nodes.filter((n) => !connected.has(n.id));
|
|
99
|
+
if (orphans.length > 0) {
|
|
100
|
+
const degrees = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const n of nodes) degrees.set(n.id, 0);
|
|
102
|
+
for (const e of edges) {
|
|
103
|
+
degrees.set(e.from, (degrees.get(e.from) ?? 0) + 1);
|
|
104
|
+
degrees.set(e.to, (degrees.get(e.to) ?? 0) + 1);
|
|
105
|
+
}
|
|
106
|
+
let hubId = nodes[0]?.id;
|
|
107
|
+
let hubDeg = 0;
|
|
108
|
+
for (const [id, deg] of degrees) {
|
|
109
|
+
if (deg > hubDeg) {
|
|
110
|
+
hubDeg = deg;
|
|
111
|
+
hubId = id;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const orphan of orphans) {
|
|
115
|
+
const sibling = nodes.find((n) => n.id !== orphan.id && n.cluster === orphan.cluster && connected.has(n.id));
|
|
116
|
+
const targetId = sibling?.id ?? hubId;
|
|
117
|
+
if (targetId && targetId !== orphan.id) {
|
|
118
|
+
edges.push({ from: targetId, to: orphan.id, type: "related", description: "Related", source: "inferred", weight: EDGE_TYPE_WEIGHTS.related });
|
|
119
|
+
connected.add(orphan.id);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const related = computeRelated(nodes, edges);
|
|
124
|
+
return { nodes, edges, clusters, related };
|
|
125
|
+
}
|
|
126
|
+
function buildEdges(nodes, nodeMap) {
|
|
127
|
+
const edgeSet = /* @__PURE__ */ new Map();
|
|
128
|
+
const addEdge = (edge) => {
|
|
129
|
+
const key = edgeKey(edge);
|
|
130
|
+
if (!edgeSet.has(key)) {
|
|
131
|
+
edgeSet.set(key, edge);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
for (const node of nodes) {
|
|
135
|
+
for (const conn of node.connections) {
|
|
136
|
+
if (nodeMap.has(conn.to)) {
|
|
137
|
+
const edgeType = conn.type ?? "references";
|
|
138
|
+
addEdge({
|
|
139
|
+
from: node.id,
|
|
140
|
+
to: conn.to,
|
|
141
|
+
type: edgeType,
|
|
142
|
+
description: conn.description,
|
|
143
|
+
source: conn.source ?? "frontmatter",
|
|
144
|
+
weight: conn.weight ?? getEdgeWeight(edgeType),
|
|
145
|
+
...conn.relation ? { relation: conn.relation } : {}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (node.parent && nodeMap.has(node.parent)) {
|
|
150
|
+
addEdge({
|
|
151
|
+
from: node.parent,
|
|
152
|
+
to: node.id,
|
|
153
|
+
type: "contains",
|
|
154
|
+
description: "Contains",
|
|
155
|
+
source: "inferred",
|
|
156
|
+
weight: EDGE_TYPE_WEIGHTS.contains
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [...edgeSet.values()];
|
|
161
|
+
}
|
|
162
|
+
function edgeKey(edge) {
|
|
163
|
+
return `${edge.from}\0${edge.to}\0${edge.type}\0${edge.relation ?? ""}`;
|
|
164
|
+
}
|
|
165
|
+
function computeRelated(nodes, edges) {
|
|
166
|
+
const adj = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const node of nodes) {
|
|
168
|
+
adj.set(node.id, /* @__PURE__ */ new Map());
|
|
169
|
+
}
|
|
170
|
+
for (const edge of edges) {
|
|
171
|
+
const fwd = adj.get(edge.from);
|
|
172
|
+
const rev = adj.get(edge.to);
|
|
173
|
+
if (fwd && (!fwd.has(edge.to) || edge.weight > (fwd.get(edge.to) ?? 0))) {
|
|
174
|
+
fwd.set(edge.to, edge.weight);
|
|
175
|
+
}
|
|
176
|
+
if (rev && (!rev.has(edge.from) || edge.weight > (rev.get(edge.from) ?? 0))) {
|
|
177
|
+
rev.set(edge.from, edge.weight);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const degree = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const [id, neighbors] of adj) {
|
|
182
|
+
degree.set(id, neighbors.size);
|
|
183
|
+
}
|
|
184
|
+
const related = {};
|
|
185
|
+
for (const [id, neighbors] of adj) {
|
|
186
|
+
related[id] = [...neighbors.entries()].sort((a, b) => {
|
|
187
|
+
const weightDiff = b[1] - a[1];
|
|
188
|
+
if (Math.abs(weightDiff) > 0.01) return weightDiff;
|
|
189
|
+
return (degree.get(b[0]) ?? 0) - (degree.get(a[0]) ?? 0);
|
|
190
|
+
}).map(([neighborId]) => neighborId).slice(0, 12);
|
|
191
|
+
}
|
|
192
|
+
return related;
|
|
193
|
+
}
|
|
194
|
+
function getNodeDegrees(graph) {
|
|
195
|
+
const degrees = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const node of graph.nodes) {
|
|
197
|
+
degrees.set(node.id, 0);
|
|
198
|
+
}
|
|
199
|
+
for (const edge of graph.edges) {
|
|
200
|
+
degrees.set(edge.from, (degrees.get(edge.from) ?? 0) + 1);
|
|
201
|
+
degrees.set(edge.to, (degrees.get(edge.to) ?? 0) + 1);
|
|
202
|
+
}
|
|
203
|
+
return degrees;
|
|
204
|
+
}
|
|
205
|
+
function getHubNodeId(graph) {
|
|
206
|
+
if (graph.nodes.some((n) => n.id === "home")) return "home";
|
|
207
|
+
if (graph.nodes.some((n) => n.id === "readme")) return "readme";
|
|
208
|
+
if (graph.nodes.some((n) => n.id === "overview")) return "overview";
|
|
209
|
+
const degrees = getNodeDegrees(graph);
|
|
210
|
+
let bestId = null;
|
|
211
|
+
let bestDeg = -1;
|
|
212
|
+
for (const [id, deg] of degrees) {
|
|
213
|
+
if (deg > bestDeg) {
|
|
214
|
+
bestDeg = deg;
|
|
215
|
+
bestId = id;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return bestId;
|
|
219
|
+
}
|
|
220
|
+
function getEdgeDescription(graph, from, to) {
|
|
221
|
+
return graph.edges.find(
|
|
222
|
+
(e) => e.from === from && e.to === to || e.from === to && e.to === from
|
|
223
|
+
)?.description;
|
|
224
|
+
}
|
|
225
|
+
var MAX_VISIBLE_NODES = 40;
|
|
226
|
+
var MAX_VISIBLE_EDGES = 80;
|
|
227
|
+
function trimGraphToLimits(graph, currentNodeId, maxNodes = MAX_VISIBLE_NODES, maxEdges = MAX_VISIBLE_EDGES) {
|
|
228
|
+
const totalNodes = graph.nodes.length;
|
|
229
|
+
const totalEdges = graph.edges.length;
|
|
230
|
+
if (totalNodes <= maxNodes && totalEdges <= maxEdges) {
|
|
231
|
+
return { graph, trimmed: false, totalNodes, totalEdges };
|
|
232
|
+
}
|
|
233
|
+
const degree = /* @__PURE__ */ new Map();
|
|
234
|
+
for (const n of graph.nodes) degree.set(n.id, 0);
|
|
235
|
+
for (const e of graph.edges) {
|
|
236
|
+
degree.set(e.from, (degree.get(e.from) ?? 0) + 1);
|
|
237
|
+
degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
|
|
238
|
+
}
|
|
239
|
+
let hubId = null;
|
|
240
|
+
let hubDeg = -1;
|
|
241
|
+
for (const [id, d] of degree) {
|
|
242
|
+
if (d > hubDeg) {
|
|
243
|
+
hubId = id;
|
|
244
|
+
hubDeg = d;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (graph.nodes.some((n) => n.id === "home")) hubId = "home";
|
|
248
|
+
else if (graph.nodes.some((n) => n.id === "readme")) hubId = "readme";
|
|
249
|
+
else if (graph.nodes.some((n) => n.id === "overview")) hubId = "overview";
|
|
250
|
+
const kept = /* @__PURE__ */ new Set();
|
|
251
|
+
if (hubId) kept.add(hubId);
|
|
252
|
+
if (currentNodeId && degree.has(currentNodeId)) kept.add(currentNodeId);
|
|
253
|
+
if (graph.nodes.some((n) => n.id === "readme")) kept.add("readme");
|
|
254
|
+
if (currentNodeId) {
|
|
255
|
+
const neighbors = [];
|
|
256
|
+
for (const e of graph.edges) {
|
|
257
|
+
if (e.from === currentNodeId && degree.has(e.to)) neighbors.push({ id: e.to, deg: degree.get(e.to) });
|
|
258
|
+
if (e.to === currentNodeId && degree.has(e.from)) neighbors.push({ id: e.from, deg: degree.get(e.from) });
|
|
259
|
+
}
|
|
260
|
+
neighbors.sort((a, b) => b.deg - a.deg);
|
|
261
|
+
const neighborBudget = Math.min(Math.floor(maxNodes * 0.3), neighbors.length);
|
|
262
|
+
for (let i = 0; i < neighborBudget; i++) kept.add(neighbors[i].id);
|
|
263
|
+
}
|
|
264
|
+
const clusters = new Set(graph.nodes.map((n) => n.cluster).filter(Boolean));
|
|
265
|
+
for (const cid of clusters) {
|
|
266
|
+
if ([...kept].some((id) => graph.nodes.find((n) => n.id === id)?.cluster === cid)) continue;
|
|
267
|
+
const best = graph.nodes.filter((n) => n.cluster === cid).sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0))[0];
|
|
268
|
+
if (best && kept.size < maxNodes) kept.add(best.id);
|
|
269
|
+
}
|
|
270
|
+
const externalNodes = graph.nodes.filter((n) => n.source.type === "external");
|
|
271
|
+
if (externalNodes.length > 0) {
|
|
272
|
+
const externalBudget = Math.max(2, Math.floor(maxNodes * 0.2));
|
|
273
|
+
const externalToAdd = externalNodes.filter((n) => !kept.has(n.id)).sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0));
|
|
274
|
+
for (const n of externalToAdd) {
|
|
275
|
+
if (kept.size >= maxNodes) break;
|
|
276
|
+
const externalKept = [...kept].filter(
|
|
277
|
+
(id) => graph.nodes.find((nd) => nd.id === id)?.source.type === "external"
|
|
278
|
+
).length;
|
|
279
|
+
if (externalKept >= externalBudget) break;
|
|
280
|
+
kept.add(n.id);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const byDegree = [...graph.nodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0));
|
|
284
|
+
for (const n of byDegree) {
|
|
285
|
+
if (kept.size >= maxNodes) break;
|
|
286
|
+
kept.add(n.id);
|
|
287
|
+
}
|
|
288
|
+
const nodes = graph.nodes.filter((n) => kept.has(n.id));
|
|
289
|
+
const edges = graph.edges.filter((e) => kept.has(e.from) && kept.has(e.to));
|
|
290
|
+
const nodeIdSet = new Set(nodes.map((n) => n.id));
|
|
291
|
+
const related = {};
|
|
292
|
+
for (const id of nodeIdSet) {
|
|
293
|
+
const r = (graph.related[id] ?? []).filter((rid) => nodeIdSet.has(rid));
|
|
294
|
+
if (r.length > 0) related[id] = r;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
graph: { nodes, edges, clusters: graph.clusters, related },
|
|
298
|
+
trimmed: true,
|
|
299
|
+
totalNodes,
|
|
300
|
+
totalEdges
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/transforms.ts
|
|
305
|
+
function selectIssueNodes(nodes) {
|
|
306
|
+
return nodes.filter((n) => n.source.type === "issue");
|
|
307
|
+
}
|
|
308
|
+
function selectDirNodes(nodes) {
|
|
309
|
+
return nodes.filter((n) => n.provider === "files");
|
|
310
|
+
}
|
|
311
|
+
var readmeTransform = {
|
|
312
|
+
name: "readme",
|
|
313
|
+
apply(nodes, ctx) {
|
|
314
|
+
if (!ctx.readme) return nodes;
|
|
315
|
+
const readme = ctx.readme;
|
|
316
|
+
const issueNodes = selectIssueNodes(nodes);
|
|
317
|
+
const dirNodes = selectDirNodes(nodes);
|
|
318
|
+
const readmeConns = [];
|
|
319
|
+
const lower = readme.toLowerCase();
|
|
320
|
+
const issueRefs = extractIssueRefs(readme);
|
|
321
|
+
for (const num of issueRefs) {
|
|
322
|
+
const id = `issue-${num}`;
|
|
323
|
+
if (issueNodes.some((n) => n.id === id)) {
|
|
324
|
+
readmeConns.push({ to: id, description: `References #${num}` });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const node of issueNodes) {
|
|
328
|
+
if (readmeConns.some((c) => c.to === node.id)) continue;
|
|
329
|
+
const titleWords = node.title.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
|
|
330
|
+
if (titleWords.length === 0) continue;
|
|
331
|
+
const matchCount = titleWords.filter((w) => lower.includes(w)).length;
|
|
332
|
+
if (matchCount >= Math.ceil(titleWords.length * 0.6)) {
|
|
333
|
+
readmeConns.push({ to: node.id, description: "Mentions" });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const dir of dirNodes) {
|
|
337
|
+
const dirName = dir.title.replace(/\/$/, "");
|
|
338
|
+
if (lower.includes(`${dirName}/`) || lower.includes(`\`${dirName}\``)) {
|
|
339
|
+
readmeConns.push({ to: dir.id, description: `References ${dirName}/` });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
readmeConns.push({ to: "repo-root", description: "Documents" });
|
|
343
|
+
const readmeConnectedTo = new Set(readmeConns.map((c) => c.to));
|
|
344
|
+
for (const m of readme.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) {
|
|
345
|
+
const target = m[2].trim();
|
|
346
|
+
if (target.startsWith("http") || target.startsWith("#") || target.startsWith("/")) continue;
|
|
347
|
+
if (target.match(/\.(png|jpg|jpeg|gif|svg|webp|md)$/i)) continue;
|
|
348
|
+
if (readmeConnectedTo.has(target)) continue;
|
|
349
|
+
readmeConns.push({ to: target, description: m[1] });
|
|
350
|
+
readmeConnectedTo.add(target);
|
|
351
|
+
}
|
|
352
|
+
const html = renderSafeMarkdown(readme);
|
|
353
|
+
nodes.push({
|
|
354
|
+
id: "readme",
|
|
355
|
+
title: "README",
|
|
356
|
+
cluster: "docs",
|
|
357
|
+
content: html,
|
|
358
|
+
rawContent: readme,
|
|
359
|
+
emoji: "Document",
|
|
360
|
+
parent: "repo-root",
|
|
361
|
+
identity: "urn:content:readme",
|
|
362
|
+
connections: readmeConns,
|
|
363
|
+
source: { type: "readme" }
|
|
364
|
+
});
|
|
365
|
+
return nodes;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
var issueDirectoryLinkTransform = {
|
|
369
|
+
name: "issue-directory-link",
|
|
370
|
+
apply(nodes) {
|
|
371
|
+
const issueNodes = selectIssueNodes(nodes);
|
|
372
|
+
const dirNodes = selectDirNodes(nodes);
|
|
373
|
+
const dirNames = dirNodes.map((d) => d.title.replace(/\/$/, ""));
|
|
374
|
+
for (const node of issueNodes) {
|
|
375
|
+
for (let i = 0; i < dirNames.length; i++) {
|
|
376
|
+
const dir = dirNames[i];
|
|
377
|
+
if (node.rawContent && (node.rawContent.includes(`${dir}/`) || node.rawContent.includes(`\`${dir}\``) || node.rawContent.toLowerCase().includes(dir.toLowerCase()))) {
|
|
378
|
+
node.connections.push({ to: dirNodes[i].id, description: `References ${dir}/` });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return nodes;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var issueSplitTransform = {
|
|
386
|
+
name: "issue-split",
|
|
387
|
+
apply(nodes) {
|
|
388
|
+
const issueNodes = selectIssueNodes(nodes);
|
|
389
|
+
const dirNodes = selectDirNodes(nodes);
|
|
390
|
+
const expandedIssues = [];
|
|
391
|
+
for (const node of issueNodes) {
|
|
392
|
+
const sectionNodes = splitIntoSections(
|
|
393
|
+
node.id,
|
|
394
|
+
node.title,
|
|
395
|
+
node.rawContent,
|
|
396
|
+
node.cluster,
|
|
397
|
+
node.emoji ?? "Pin",
|
|
398
|
+
node.source,
|
|
399
|
+
[...issueNodes, ...dirNodes]
|
|
400
|
+
);
|
|
401
|
+
if (sectionNodes.length > 0) {
|
|
402
|
+
const idx = nodes.indexOf(node);
|
|
403
|
+
if (idx >= 0) nodes.splice(idx, 1);
|
|
404
|
+
expandedIssues.push(...sectionNodes);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
nodes.push(...expandedIssues);
|
|
408
|
+
return nodes;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
var DEFAULT_TRANSFORMS = [
|
|
412
|
+
readmeTransform,
|
|
413
|
+
issueDirectoryLinkTransform,
|
|
414
|
+
issueSplitTransform
|
|
415
|
+
];
|
|
416
|
+
function applyTransforms(nodes, ctx, transforms = DEFAULT_TRANSFORMS) {
|
|
417
|
+
let current = nodes;
|
|
418
|
+
for (const transform of transforms) {
|
|
419
|
+
current = transform.apply(current, ctx);
|
|
420
|
+
}
|
|
421
|
+
return current;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export { DEFAULT_TRANSFORMS, EDGE_TYPE_WEIGHTS, MAX_VISIBLE_EDGES, MAX_VISIBLE_NODES, applyTransforms, buildGraph, getEdgeDescription, getEdgeWeight, getHubNodeId, getNodeDegrees, getRegisteredTypes, hasType, issueDirectoryLinkTransform, issueSplitTransform, readmeTransform, registerBuiltInNodeTypes, registerType, resetNodeTypeRegistry, resolveNodeLayer, resolveType, resolveTypeCluster, trimGraphToLimits };
|