@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,2892 @@
|
|
|
1
|
+
export { buildManifest, globToRegex } from './chunk-MW4BLXVK.js';
|
|
2
|
+
import { resolveGraphStoreOptions } from './chunk-BA526FDQ.js';
|
|
3
|
+
export { OrgChartProvider, WikipediaProvider, loadExternalProviders } from './chunk-ASLGNOYV.js';
|
|
4
|
+
import { loadNodeMap } from './chunk-XVI6CBSX.js';
|
|
5
|
+
export { DEFAULT_STRUCTURED_CONTENT_PATH, extractImportPaths, hasExplicitStructuredContentPath, loadNodeMap, normalizeRepoRelativeDir, resolveImportPath, resolveStructuredContentPath } from './chunk-XVI6CBSX.js';
|
|
6
|
+
import { DEFAULT_TRANSFORMS, registerType, buildGraph, applyTransforms } from './chunk-DJ2DBEEK.js';
|
|
7
|
+
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 } from './chunk-DJ2DBEEK.js';
|
|
8
|
+
import { parseMarkdownFile, renderSafeMarkdown, treeToNodes, issueToNode, extractIssueRefs, extractClusters } from './chunk-FJ4GTN4U.js';
|
|
9
|
+
export { DEFAULT_CONFIG, extractClusters, extractIssueRefs, fetchCommits, fetchFile, fetchFiles, fetchIssues, fetchPullRequests, fetchReleases, fetchTree, filterAccessWithheld, isAccessWithheld, issueToNode, loadAuthoredContent, loadConfig, loadRepoContent, parseAccessLabel, parseMarkdownFile, renderSafeMarkdown, splitIntoSections, treeToNodes } from './chunk-FJ4GTN4U.js';
|
|
10
|
+
import { assignIdentity, urnIdentity } from './chunk-JNQVSNLC.js';
|
|
11
|
+
export { assignIdentity, buildIdentityIndex, shareIdentity, urnBody, urnIdentity } from './chunk-JNQVSNLC.js';
|
|
12
|
+
import { buildProviderResultCacheKey } from './chunk-HXAS3BSD.js';
|
|
13
|
+
import yaml from 'yaml';
|
|
14
|
+
import { stripScheme, buildPersonAddress, buildJsonLd } from '@anokye-labs/kbexplorer-core';
|
|
15
|
+
import { ingestRichMarkdown } from '@anokye-labs/kbexplorer-provider-rich-markdown/lib';
|
|
16
|
+
|
|
17
|
+
// src/query.ts
|
|
18
|
+
function getNode(graph, id) {
|
|
19
|
+
return graph.nodes.find((n) => n.id === id);
|
|
20
|
+
}
|
|
21
|
+
function findNodes(graph, predicate) {
|
|
22
|
+
return graph.nodes.filter(predicate);
|
|
23
|
+
}
|
|
24
|
+
function edgeTypeMatcher(edgeType) {
|
|
25
|
+
if (edgeType === void 0) return () => true;
|
|
26
|
+
const set = new Set(Array.isArray(edgeType) ? edgeType : [edgeType]);
|
|
27
|
+
return (type) => set.has(type);
|
|
28
|
+
}
|
|
29
|
+
function neighbors(graph, id, options = {}) {
|
|
30
|
+
const direction = options.direction ?? "both";
|
|
31
|
+
const matchesType = edgeTypeMatcher(options.edgeType);
|
|
32
|
+
const nodeById = indexNodes(graph);
|
|
33
|
+
if (!nodeById.has(id)) return [];
|
|
34
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35
|
+
const out = [];
|
|
36
|
+
const consider = (neighborId) => {
|
|
37
|
+
if (neighborId === id || seen.has(neighborId)) return;
|
|
38
|
+
const node = nodeById.get(neighborId);
|
|
39
|
+
if (!node) return;
|
|
40
|
+
seen.add(neighborId);
|
|
41
|
+
out.push(node);
|
|
42
|
+
};
|
|
43
|
+
for (const edge of graph.edges) {
|
|
44
|
+
if (!matchesType(edge.type)) continue;
|
|
45
|
+
if ((direction === "out" || direction === "both") && edge.from === id) consider(edge.to);
|
|
46
|
+
if ((direction === "in" || direction === "both") && edge.to === id) consider(edge.from);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function related(graph, id) {
|
|
51
|
+
const nodeById = indexNodes(graph);
|
|
52
|
+
const relatedIds = graph.related[id] ?? [];
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const relatedId of relatedIds) {
|
|
55
|
+
const node = nodeById.get(relatedId);
|
|
56
|
+
if (node) out.push(node);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function subgraph(graph, seeds, options = {}) {
|
|
61
|
+
const radius = options.radius ?? 1;
|
|
62
|
+
const direction = options.direction ?? "both";
|
|
63
|
+
const nodeById = indexNodes(graph);
|
|
64
|
+
const adjacency = buildAdjacency(graph.edges, direction);
|
|
65
|
+
const kept = /* @__PURE__ */ new Set();
|
|
66
|
+
let frontier = [];
|
|
67
|
+
for (const seed of Array.isArray(seeds) ? seeds : [seeds]) {
|
|
68
|
+
if (nodeById.has(seed) && !kept.has(seed)) {
|
|
69
|
+
kept.add(seed);
|
|
70
|
+
frontier.push(seed);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (let hop = 0; hop < radius && frontier.length > 0; hop++) {
|
|
74
|
+
const next = [];
|
|
75
|
+
for (const current of frontier) {
|
|
76
|
+
for (const neighborId of adjacency.get(current) ?? []) {
|
|
77
|
+
if (!kept.has(neighborId)) {
|
|
78
|
+
kept.add(neighborId);
|
|
79
|
+
next.push(neighborId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
frontier = next;
|
|
84
|
+
}
|
|
85
|
+
const nodes = graph.nodes.filter((n) => kept.has(n.id));
|
|
86
|
+
const edges = graph.edges.filter((e) => kept.has(e.from) && kept.has(e.to));
|
|
87
|
+
const usedClusters = new Set(nodes.map((n) => n.cluster));
|
|
88
|
+
const clusters = graph.clusters.filter((c) => usedClusters.has(c.id));
|
|
89
|
+
const relatedOut = {};
|
|
90
|
+
for (const node of nodes) {
|
|
91
|
+
const filtered = (graph.related[node.id] ?? []).filter((rid) => kept.has(rid));
|
|
92
|
+
if (filtered.length > 0) relatedOut[node.id] = filtered;
|
|
93
|
+
}
|
|
94
|
+
return { nodes, edges, clusters, related: relatedOut };
|
|
95
|
+
}
|
|
96
|
+
function shortestPath(graph, from, to, options = {}) {
|
|
97
|
+
const direction = options.direction ?? "both";
|
|
98
|
+
const nodeById = indexNodes(graph);
|
|
99
|
+
if (!nodeById.has(from) || !nodeById.has(to)) return null;
|
|
100
|
+
if (from === to) return [from];
|
|
101
|
+
const adjacency = buildAdjacency(graph.edges, direction);
|
|
102
|
+
const previous = /* @__PURE__ */ new Map();
|
|
103
|
+
const visited = /* @__PURE__ */ new Set([from]);
|
|
104
|
+
const queue = [from];
|
|
105
|
+
while (queue.length > 0) {
|
|
106
|
+
const current = queue.shift();
|
|
107
|
+
for (const neighborId of adjacency.get(current) ?? []) {
|
|
108
|
+
if (visited.has(neighborId)) continue;
|
|
109
|
+
visited.add(neighborId);
|
|
110
|
+
previous.set(neighborId, current);
|
|
111
|
+
if (neighborId === to) {
|
|
112
|
+
return reconstructPath(previous, from, to);
|
|
113
|
+
}
|
|
114
|
+
queue.push(neighborId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
function indexNodes(graph) {
|
|
120
|
+
const byId = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const node of graph.nodes) byId.set(node.id, node);
|
|
122
|
+
return byId;
|
|
123
|
+
}
|
|
124
|
+
function buildAdjacency(edges, direction) {
|
|
125
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
126
|
+
const link = (a, b) => {
|
|
127
|
+
let set = adjacency.get(a);
|
|
128
|
+
if (!set) {
|
|
129
|
+
set = /* @__PURE__ */ new Set();
|
|
130
|
+
adjacency.set(a, set);
|
|
131
|
+
}
|
|
132
|
+
set.add(b);
|
|
133
|
+
};
|
|
134
|
+
for (const edge of edges) {
|
|
135
|
+
if (direction === "out" || direction === "both") link(edge.from, edge.to);
|
|
136
|
+
if (direction === "in" || direction === "both") link(edge.to, edge.from);
|
|
137
|
+
}
|
|
138
|
+
return adjacency;
|
|
139
|
+
}
|
|
140
|
+
function reconstructPath(previous, from, to) {
|
|
141
|
+
const path = [to];
|
|
142
|
+
let cursor = to;
|
|
143
|
+
while (cursor !== from) {
|
|
144
|
+
const prev = previous.get(cursor);
|
|
145
|
+
if (prev === void 0) return [];
|
|
146
|
+
path.push(prev);
|
|
147
|
+
cursor = prev;
|
|
148
|
+
}
|
|
149
|
+
path.reverse();
|
|
150
|
+
return path;
|
|
151
|
+
}
|
|
152
|
+
function globToRegex2(pattern) {
|
|
153
|
+
let re = "";
|
|
154
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
155
|
+
const c = pattern[i];
|
|
156
|
+
if (c === "*" && pattern[i + 1] === "*") {
|
|
157
|
+
re += ".*";
|
|
158
|
+
i += 1;
|
|
159
|
+
if (pattern[i + 1] === "/") i += 1;
|
|
160
|
+
} else if (c === "*") {
|
|
161
|
+
re += "[^/]*";
|
|
162
|
+
} else if (c === "?") {
|
|
163
|
+
re += "[^/]";
|
|
164
|
+
} else if (".+^${}()|[]\\".includes(c)) {
|
|
165
|
+
re += "\\" + c;
|
|
166
|
+
} else {
|
|
167
|
+
re += c;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return new RegExp(`^${re}$`);
|
|
171
|
+
}
|
|
172
|
+
function matchesAnyGlob(path, globs) {
|
|
173
|
+
if (globs === void 0) return true;
|
|
174
|
+
const list = Array.isArray(globs) ? globs : [globs];
|
|
175
|
+
return list.some((g) => globToRegex2(g).test(path));
|
|
176
|
+
}
|
|
177
|
+
function fileName(path) {
|
|
178
|
+
return path.split("/").pop() ?? path;
|
|
179
|
+
}
|
|
180
|
+
function baseName(path) {
|
|
181
|
+
const name = fileName(path);
|
|
182
|
+
const dot = name.lastIndexOf(".");
|
|
183
|
+
return dot > 0 ? name.substring(0, dot) : name;
|
|
184
|
+
}
|
|
185
|
+
function extOf(path) {
|
|
186
|
+
const name = fileName(path);
|
|
187
|
+
const dot = name.lastIndexOf(".");
|
|
188
|
+
return dot >= 0 ? name.substring(dot + 1).toLowerCase() : "";
|
|
189
|
+
}
|
|
190
|
+
function slugify(value) {
|
|
191
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
192
|
+
}
|
|
193
|
+
function getByPath(obj, path) {
|
|
194
|
+
let cur = obj;
|
|
195
|
+
for (const key2 of path.split(".")) {
|
|
196
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
197
|
+
cur = cur[key2];
|
|
198
|
+
}
|
|
199
|
+
return cur;
|
|
200
|
+
}
|
|
201
|
+
function isPlainObject(v) {
|
|
202
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
203
|
+
}
|
|
204
|
+
function parseStructuredContent(file) {
|
|
205
|
+
const ext = extOf(file.path);
|
|
206
|
+
const raw = file.content;
|
|
207
|
+
if (!raw || !raw.trim()) return null;
|
|
208
|
+
const tryParse = (format) => {
|
|
209
|
+
try {
|
|
210
|
+
const parsed = format === "json" ? JSON.parse(raw) : yaml.parse(raw);
|
|
211
|
+
if (isPlainObject(parsed) || Array.isArray(parsed)) {
|
|
212
|
+
return { format, data: parsed };
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
};
|
|
218
|
+
if (ext === "json" || ext === "jsonld") return tryParse("json");
|
|
219
|
+
if (ext === "yml" || ext === "yaml") return tryParse("yaml");
|
|
220
|
+
return tryParse("yaml");
|
|
221
|
+
}
|
|
222
|
+
function matchRule(file, data, map) {
|
|
223
|
+
for (const rule of map.rules ?? []) {
|
|
224
|
+
if (!matchesAnyGlob(file.path, rule.glob)) continue;
|
|
225
|
+
if (rule.shape?.length) {
|
|
226
|
+
if (!isPlainObject(data)) continue;
|
|
227
|
+
const hasAll = rule.shape.every((k) => k in data);
|
|
228
|
+
if (!hasAll) continue;
|
|
229
|
+
}
|
|
230
|
+
return rule;
|
|
231
|
+
}
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
function deriveId(file, options) {
|
|
235
|
+
if (options?.id) return options.id;
|
|
236
|
+
const prefix = options?.idPrefix ?? "cfg";
|
|
237
|
+
return `${prefix}-${slugify(file.path)}`;
|
|
238
|
+
}
|
|
239
|
+
function structuredSource(entityType, ref) {
|
|
240
|
+
return { type: "structured", entityType, ref };
|
|
241
|
+
}
|
|
242
|
+
function makeNode(args) {
|
|
243
|
+
const identity = urnIdentity("structured", args.ref);
|
|
244
|
+
const dataBag = isPlainObject(args.data) ? args.data : { items: args.data };
|
|
245
|
+
return {
|
|
246
|
+
id: args.id,
|
|
247
|
+
title: args.title,
|
|
248
|
+
cluster: args.cluster,
|
|
249
|
+
content: "",
|
|
250
|
+
rawContent: "",
|
|
251
|
+
emoji: args.emoji,
|
|
252
|
+
display: "entity",
|
|
253
|
+
connections: (args.edges ?? []).map((e) => ({
|
|
254
|
+
to: e.to,
|
|
255
|
+
description: e.description ?? "Structural",
|
|
256
|
+
source: "inferred",
|
|
257
|
+
...e.type !== void 0 ? { type: e.type } : {},
|
|
258
|
+
...e.relation !== void 0 ? { relation: e.relation } : {}
|
|
259
|
+
})),
|
|
260
|
+
identity,
|
|
261
|
+
derived: true,
|
|
262
|
+
source: structuredSource(args.entityType, args.ref),
|
|
263
|
+
entityType: args.entityType,
|
|
264
|
+
data: dataBag,
|
|
265
|
+
jsonld: buildJsonLd({ id: args.id, identity }, args.ldType, args.ldProps ?? {})
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function titleFromData(data, rule, file) {
|
|
269
|
+
if (rule?.titleFrom) {
|
|
270
|
+
const v = getByPath(data, rule.titleFrom);
|
|
271
|
+
if (typeof v === "string" && v.trim()) return v;
|
|
272
|
+
}
|
|
273
|
+
if (isPlainObject(data)) {
|
|
274
|
+
const name = data.name ?? data.title;
|
|
275
|
+
if (typeof name === "string" && name.trim()) return name;
|
|
276
|
+
}
|
|
277
|
+
return baseName(file.path);
|
|
278
|
+
}
|
|
279
|
+
function buildMappedNode(file, data, rule, options) {
|
|
280
|
+
const entityType = rule.entityType ?? slugify(rule.type);
|
|
281
|
+
const ldProps = {};
|
|
282
|
+
if (rule.fields) {
|
|
283
|
+
for (const [out, src] of Object.entries(rule.fields)) {
|
|
284
|
+
const v = getByPath(data, src);
|
|
285
|
+
if (v !== void 0) ldProps[out] = v;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return makeNode({
|
|
289
|
+
id: deriveId(file, options),
|
|
290
|
+
title: titleFromData(data, rule, file),
|
|
291
|
+
cluster: rule.cluster ?? options?.cluster ?? "infra",
|
|
292
|
+
emoji: rule.emoji ?? "DocumentData",
|
|
293
|
+
entityType,
|
|
294
|
+
ldType: rule.type,
|
|
295
|
+
data,
|
|
296
|
+
ref: file.path,
|
|
297
|
+
ldProps,
|
|
298
|
+
...rule.edges !== void 0 ? { edges: rule.edges } : {}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function inferType(data) {
|
|
302
|
+
if (isPlainObject(data)) {
|
|
303
|
+
const has = (k) => k in data;
|
|
304
|
+
if (has("on") && has("jobs")) return { ldType: "Workflow", entityType: "workflow", emoji: "Flow" };
|
|
305
|
+
if (has("runs")) return { ldType: "SoftwareApplication", entityType: "github-action", emoji: "PuzzlePiece" };
|
|
306
|
+
if (has("version") && has("updates")) return { ldType: "DependabotConfig", entityType: "dependabot-config", emoji: "ArrowSync" };
|
|
307
|
+
if (has("inputs") || has("outputs")) return { ldType: "ParameterisedConfig", entityType: "structured-config", emoji: "Options" };
|
|
308
|
+
}
|
|
309
|
+
return { ldType: "StructuredConfig", entityType: "structured-config", emoji: "DocumentData" };
|
|
310
|
+
}
|
|
311
|
+
function inferStructuredNode(file, parsed, options) {
|
|
312
|
+
const result = parsed ?? parseStructuredContent(file);
|
|
313
|
+
if (!result) return null;
|
|
314
|
+
const { ldType, entityType, emoji } = inferType(result.data);
|
|
315
|
+
return makeNode({
|
|
316
|
+
id: deriveId(file, options),
|
|
317
|
+
title: titleFromData(result.data, void 0, file),
|
|
318
|
+
cluster: options?.cluster ?? "infra",
|
|
319
|
+
emoji,
|
|
320
|
+
entityType,
|
|
321
|
+
ldType,
|
|
322
|
+
data: result.data,
|
|
323
|
+
ref: file.path
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function applyStructuredNodeMap(file, map, options) {
|
|
327
|
+
const parsed = parseStructuredContent(file);
|
|
328
|
+
if (!parsed) return null;
|
|
329
|
+
const rule = map ? matchRule(file, parsed.data, map) : void 0;
|
|
330
|
+
if (rule) return buildMappedNode(file, parsed.data, rule, options);
|
|
331
|
+
return inferStructuredNode(file, parsed, options);
|
|
332
|
+
}
|
|
333
|
+
function parseStructuredNodeMap(raw) {
|
|
334
|
+
if (!raw || !raw.trim()) return { rules: [] };
|
|
335
|
+
try {
|
|
336
|
+
const parsed = yaml.parse(raw);
|
|
337
|
+
const rules = Array.isArray(parsed?.rules) ? parsed.rules.filter((r) => r && typeof r.type === "string") : [];
|
|
338
|
+
return { rules };
|
|
339
|
+
} catch {
|
|
340
|
+
return { rules: [] };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function reconstructSource(node, formatOverride) {
|
|
344
|
+
const data = node.data ?? {};
|
|
345
|
+
let format = formatOverride ?? "yaml";
|
|
346
|
+
if (!formatOverride && node.source.type === "structured" && node.source.ref) {
|
|
347
|
+
const ext = extOf(node.source.ref);
|
|
348
|
+
if (ext === "json" || ext === "jsonld") format = "json";
|
|
349
|
+
}
|
|
350
|
+
const payload = isPlainObject(data) && Object.keys(data).length === 1 && Array.isArray(data.items) ? data.items : data;
|
|
351
|
+
return format === "json" ? JSON.stringify(payload, null, 2) : yaml.stringify(payload);
|
|
352
|
+
}
|
|
353
|
+
function repoCoordsFromConfig(config) {
|
|
354
|
+
const { owner, repo, branch } = config.source;
|
|
355
|
+
return { owner, repo, branch: branch && branch.trim() ? branch : "main" };
|
|
356
|
+
}
|
|
357
|
+
function canEditSource(node) {
|
|
358
|
+
const f = node.sourceFile;
|
|
359
|
+
return !!f && typeof f.path === "string" && f.path.trim().length > 0 && typeof f.raw === "string" && // Validate the runtime `format` too: cached/loaded data could carry a
|
|
360
|
+
// missing or unknown format, which would later crash the editor
|
|
361
|
+
// (`format.toUpperCase()`) or mis-dispatch validation. An invalid shape
|
|
362
|
+
// simply exposes no affordance — a safe no-op.
|
|
363
|
+
(f.format === "yaml" || f.format === "json");
|
|
364
|
+
}
|
|
365
|
+
function resolveSourceFile(node) {
|
|
366
|
+
return canEditSource(node) ? node.sourceFile : null;
|
|
367
|
+
}
|
|
368
|
+
function normalizeNewlines(text) {
|
|
369
|
+
return text.replace(/\r\n/g, "\n");
|
|
370
|
+
}
|
|
371
|
+
function validateSourceContent(raw, format) {
|
|
372
|
+
if (raw.trim().length === 0) {
|
|
373
|
+
return { ok: false, error: "Source file is empty." };
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
if (format === "json") {
|
|
377
|
+
JSON.parse(raw);
|
|
378
|
+
} else if (format === "yaml") {
|
|
379
|
+
yaml.parse(raw);
|
|
380
|
+
}
|
|
381
|
+
return { ok: true };
|
|
382
|
+
} catch (err) {
|
|
383
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
384
|
+
return { ok: false, error: message };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
var GITHUB_WEB = "https://github.com";
|
|
388
|
+
function encodeRepoPath(path) {
|
|
389
|
+
return path.split("/").filter((seg) => seg.length > 0).map((seg) => encodeURIComponent(seg)).join("/");
|
|
390
|
+
}
|
|
391
|
+
function buildEditUrl(coords, path) {
|
|
392
|
+
const { owner, repo, branch } = coords;
|
|
393
|
+
return `${GITHUB_WEB}/${owner}/${repo}/edit/${encodeURIComponent(branch)}/${encodeRepoPath(path)}`;
|
|
394
|
+
}
|
|
395
|
+
function buildNewFileUrl(coords, path, content) {
|
|
396
|
+
const { owner, repo, branch } = coords;
|
|
397
|
+
const params = new URLSearchParams({ filename: path, value: content });
|
|
398
|
+
return `${GITHUB_WEB}/${owner}/${repo}/new/${encodeURIComponent(branch)}?${params.toString()}`;
|
|
399
|
+
}
|
|
400
|
+
function buildHandoffUrl(coords, path, content, exists) {
|
|
401
|
+
return exists ? buildEditUrl(coords, path) : buildNewFileUrl(coords, path, content);
|
|
402
|
+
}
|
|
403
|
+
function splitLines(text) {
|
|
404
|
+
const lines = text.split("\n");
|
|
405
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
406
|
+
return lines;
|
|
407
|
+
}
|
|
408
|
+
var MAX_DIFF_LINES = 5e3;
|
|
409
|
+
function diffLines(a, b) {
|
|
410
|
+
const m = a.length;
|
|
411
|
+
const n = b.length;
|
|
412
|
+
if (m > MAX_DIFF_LINES || n > MAX_DIFF_LINES) {
|
|
413
|
+
const coarse = [];
|
|
414
|
+
for (const line of a) coarse.push({ type: "-", line });
|
|
415
|
+
for (const line of b) coarse.push({ type: "+", line });
|
|
416
|
+
return coarse;
|
|
417
|
+
}
|
|
418
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
419
|
+
for (let i2 = m - 1; i2 >= 0; i2--) {
|
|
420
|
+
for (let j2 = n - 1; j2 >= 0; j2--) {
|
|
421
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const ops = [];
|
|
425
|
+
let i = 0;
|
|
426
|
+
let j = 0;
|
|
427
|
+
while (i < m && j < n) {
|
|
428
|
+
if (a[i] === b[j]) {
|
|
429
|
+
ops.push({ type: " ", line: a[i] });
|
|
430
|
+
i++;
|
|
431
|
+
j++;
|
|
432
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
433
|
+
ops.push({ type: "-", line: a[i] });
|
|
434
|
+
i++;
|
|
435
|
+
} else {
|
|
436
|
+
ops.push({ type: "+", line: b[j] });
|
|
437
|
+
j++;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
while (i < m) ops.push({ type: "-", line: a[i++] });
|
|
441
|
+
while (j < n) ops.push({ type: "+", line: b[j++] });
|
|
442
|
+
return ops;
|
|
443
|
+
}
|
|
444
|
+
function buildUnifiedDiff(path, oldText, newText, context = 3, isNew = false) {
|
|
445
|
+
if (oldText === newText) return "";
|
|
446
|
+
const a = splitLines(oldText);
|
|
447
|
+
const b = splitLines(newText);
|
|
448
|
+
const ops = diffLines(a, b);
|
|
449
|
+
const n = ops.length;
|
|
450
|
+
const oldAt = new Array(n);
|
|
451
|
+
const newAt = new Array(n);
|
|
452
|
+
let oldNo = 1;
|
|
453
|
+
let newNo = 1;
|
|
454
|
+
for (let k = 0; k < n; k++) {
|
|
455
|
+
oldAt[k] = oldNo;
|
|
456
|
+
newAt[k] = newNo;
|
|
457
|
+
if (ops[k].type === " ") {
|
|
458
|
+
oldNo++;
|
|
459
|
+
newNo++;
|
|
460
|
+
} else if (ops[k].type === "-") {
|
|
461
|
+
oldNo++;
|
|
462
|
+
} else {
|
|
463
|
+
newNo++;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const ranges = [];
|
|
467
|
+
let i = 0;
|
|
468
|
+
while (i < n) {
|
|
469
|
+
if (ops[i].type === " ") {
|
|
470
|
+
i++;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
let end = i + 1;
|
|
474
|
+
let gap = 0;
|
|
475
|
+
let j = i + 1;
|
|
476
|
+
while (j < n) {
|
|
477
|
+
if (ops[j].type !== " ") {
|
|
478
|
+
end = j + 1;
|
|
479
|
+
gap = 0;
|
|
480
|
+
} else {
|
|
481
|
+
gap++;
|
|
482
|
+
if (gap > 2 * context) break;
|
|
483
|
+
}
|
|
484
|
+
j++;
|
|
485
|
+
}
|
|
486
|
+
ranges.push([Math.max(0, i - context), Math.min(n, end + context)]);
|
|
487
|
+
i = j;
|
|
488
|
+
}
|
|
489
|
+
const merged = [];
|
|
490
|
+
for (const r of ranges) {
|
|
491
|
+
const last = merged[merged.length - 1];
|
|
492
|
+
if (last && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);
|
|
493
|
+
else merged.push([r[0], r[1]]);
|
|
494
|
+
}
|
|
495
|
+
const hunks = [];
|
|
496
|
+
for (const [s, e] of merged) {
|
|
497
|
+
let oldCount = 0;
|
|
498
|
+
let newCount = 0;
|
|
499
|
+
const body = [];
|
|
500
|
+
for (let k = s; k < e; k++) {
|
|
501
|
+
const o = ops[k];
|
|
502
|
+
if (o.type === " ") {
|
|
503
|
+
oldCount++;
|
|
504
|
+
newCount++;
|
|
505
|
+
body.push(` ${o.line}`);
|
|
506
|
+
} else if (o.type === "-") {
|
|
507
|
+
oldCount++;
|
|
508
|
+
body.push(`-${o.line}`);
|
|
509
|
+
} else {
|
|
510
|
+
newCount++;
|
|
511
|
+
body.push(`+${o.line}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const oldStart = oldCount === 0 ? oldAt[s] - 1 : oldAt[s];
|
|
515
|
+
const newStart = newCount === 0 ? newAt[s] - 1 : newAt[s];
|
|
516
|
+
hunks.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@
|
|
517
|
+
${body.join("\n")}`);
|
|
518
|
+
}
|
|
519
|
+
const fromPath = isNew ? "/dev/null" : `a/${path}`;
|
|
520
|
+
const newFileLine = isNew ? "new file mode 100644\n" : "";
|
|
521
|
+
const header = `diff --git a/${path} b/${path}
|
|
522
|
+
${newFileLine}--- ${fromPath}
|
|
523
|
+
+++ b/${path}
|
|
524
|
+
`;
|
|
525
|
+
return `${header}${hunks.join("\n")}
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
function patchFilename(path) {
|
|
529
|
+
const base = path.split("/").filter(Boolean).pop() ?? "source";
|
|
530
|
+
return `${base}.patch`;
|
|
531
|
+
}
|
|
532
|
+
function buildSourceEditHandoff(coords, file, newContent, exists = true) {
|
|
533
|
+
const base = normalizeNewlines(file.raw);
|
|
534
|
+
const next = normalizeNewlines(newContent);
|
|
535
|
+
return {
|
|
536
|
+
changed: next !== base,
|
|
537
|
+
exists,
|
|
538
|
+
url: buildHandoffUrl(coords, file.path, next, exists),
|
|
539
|
+
editUrl: buildEditUrl(coords, file.path),
|
|
540
|
+
newFileUrl: buildNewFileUrl(coords, file.path, next),
|
|
541
|
+
patch: buildUnifiedDiff(file.path, base, next, 3, !exists),
|
|
542
|
+
patchName: patchFilename(file.path)
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/providers.ts
|
|
547
|
+
var ProviderRegistry = class {
|
|
548
|
+
providers = /* @__PURE__ */ new Map();
|
|
549
|
+
register(provider) {
|
|
550
|
+
this.providers.set(provider.id, provider);
|
|
551
|
+
}
|
|
552
|
+
/** Get providers in dependency-safe execution order */
|
|
553
|
+
getExecutionOrder() {
|
|
554
|
+
const visited = /* @__PURE__ */ new Set();
|
|
555
|
+
const order = [];
|
|
556
|
+
const visit = (id) => {
|
|
557
|
+
if (visited.has(id)) return;
|
|
558
|
+
visited.add(id);
|
|
559
|
+
const p = this.providers.get(id);
|
|
560
|
+
if (!p) return;
|
|
561
|
+
for (const dep of p.dependencies ?? []) visit(dep);
|
|
562
|
+
order.push(p);
|
|
563
|
+
};
|
|
564
|
+
for (const id of this.providers.keys()) visit(id);
|
|
565
|
+
return order;
|
|
566
|
+
}
|
|
567
|
+
get(id) {
|
|
568
|
+
return this.providers.get(id);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// src/providers/rich-markdown/detect.ts
|
|
573
|
+
var FRONTMATTER_RE = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
574
|
+
var DISPLAY_RE = /^\s*display\s*:\s*(?:"([^"]*)"|'([^']*)'|([^\s#]+))/m;
|
|
575
|
+
function readFrontmatterDisplay(raw) {
|
|
576
|
+
const fm = FRONTMATTER_RE.exec(raw);
|
|
577
|
+
if (!fm) return void 0;
|
|
578
|
+
const m = DISPLAY_RE.exec(fm[1] ?? "");
|
|
579
|
+
if (!m) return void 0;
|
|
580
|
+
const value = (m[1] ?? m[2] ?? m[3] ?? "").trim();
|
|
581
|
+
return value || void 0;
|
|
582
|
+
}
|
|
583
|
+
function isRichAuthoredMarkdown(raw) {
|
|
584
|
+
return readFrontmatterDisplay(raw) === "rich-markdown";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/providers/authored-provider.ts
|
|
588
|
+
var AuthoredProvider = class {
|
|
589
|
+
id = "authored";
|
|
590
|
+
name = "Authored Content";
|
|
591
|
+
dependencies = [];
|
|
592
|
+
authoredContent;
|
|
593
|
+
nodemapRaw;
|
|
594
|
+
nodemapFiles;
|
|
595
|
+
nodemapDirs;
|
|
596
|
+
listFiles;
|
|
597
|
+
constructor(authoredContent, nodemapRaw, nodemapFiles, nodemapDirs, listFiles) {
|
|
598
|
+
this.authoredContent = authoredContent;
|
|
599
|
+
this.nodemapRaw = nodemapRaw;
|
|
600
|
+
this.nodemapFiles = nodemapFiles;
|
|
601
|
+
this.nodemapDirs = nodemapDirs;
|
|
602
|
+
this.listFiles = listFiles;
|
|
603
|
+
}
|
|
604
|
+
async resolve(_config, _existingNodes) {
|
|
605
|
+
const nodes = [];
|
|
606
|
+
for (const [path, raw] of Object.entries(this.authoredContent)) {
|
|
607
|
+
if (isRichAuthoredMarkdown(raw)) continue;
|
|
608
|
+
try {
|
|
609
|
+
const node = parseMarkdownFile(path, raw);
|
|
610
|
+
node.provider = "authored";
|
|
611
|
+
if (!node.identity) {
|
|
612
|
+
node.identity = `urn:content:${node.id}`;
|
|
613
|
+
}
|
|
614
|
+
nodes.push(node);
|
|
615
|
+
} catch {
|
|
616
|
+
console.warn(`[AuthoredProvider] Failed to parse ${path}, skipping`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (this.nodemapRaw) {
|
|
620
|
+
const readFile = async (path) => this.nodemapFiles?.[path] ?? null;
|
|
621
|
+
const listDirectory = this.nodemapDirs ? async (dir) => this.nodemapDirs[dir] ?? [] : void 0;
|
|
622
|
+
const nodemapNodes = await loadNodeMap(
|
|
623
|
+
this.nodemapRaw,
|
|
624
|
+
readFile,
|
|
625
|
+
this.listFiles,
|
|
626
|
+
listDirectory
|
|
627
|
+
);
|
|
628
|
+
for (const node of nodemapNodes) {
|
|
629
|
+
node.provider = "authored";
|
|
630
|
+
if (!node.identity) {
|
|
631
|
+
node.identity = assignIdentity(node) ?? `urn:content:${node.id}`;
|
|
632
|
+
}
|
|
633
|
+
nodes.push(node);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return { nodes, edges: [] };
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
var PROVIDER_ID = "authored-rich-markdown";
|
|
640
|
+
var LEADING_FRONTMATTER_RE = /^\uFEFF?---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
|
641
|
+
function stripLeadingFrontmatter(raw) {
|
|
642
|
+
return raw.replace(LEADING_FRONTMATTER_RE, "");
|
|
643
|
+
}
|
|
644
|
+
function toTemplateBlock(block) {
|
|
645
|
+
const out = { kind: block.lang, source: block.content };
|
|
646
|
+
if (typeof block.contentHash === "string") out.hash = block.contentHash;
|
|
647
|
+
if (block.span && Number.isFinite(block.span.start) && Number.isFinite(block.span.end)) {
|
|
648
|
+
out.range = { start: block.span.start, end: block.span.end };
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
function localIdOf(ingested) {
|
|
653
|
+
return ingested.id === ingested.identity ? stripScheme(ingested.id) : ingested.id;
|
|
654
|
+
}
|
|
655
|
+
function adaptIngestedNode(ingested) {
|
|
656
|
+
const { richMarkdown: pkgRichMarkdown, ...frontmatter } = ingested.data;
|
|
657
|
+
const blocks = (pkgRichMarkdown.blocks ?? []).map(toTemplateBlock);
|
|
658
|
+
const fmCluster = typeof frontmatter.cluster === "string" ? frontmatter.cluster.trim() : "";
|
|
659
|
+
const cluster = fmCluster || ingested.cluster;
|
|
660
|
+
const body = stripLeadingFrontmatter(ingested.rawContent);
|
|
661
|
+
const node = {
|
|
662
|
+
id: localIdOf(ingested),
|
|
663
|
+
title: ingested.title,
|
|
664
|
+
cluster,
|
|
665
|
+
// The pure lib leaves `content` empty; render the body exactly as the engine
|
|
666
|
+
// renders any node so ProseContent finds the same fences at runtime.
|
|
667
|
+
content: renderSafeMarkdown(body),
|
|
668
|
+
rawContent: body,
|
|
669
|
+
display: "rich-markdown",
|
|
670
|
+
connections: ingested.connections ?? [],
|
|
671
|
+
source: ingested.source,
|
|
672
|
+
provider: PROVIDER_ID,
|
|
673
|
+
data: {
|
|
674
|
+
...frontmatter,
|
|
675
|
+
richMarkdown: { frontmatter, blocks }
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
const identity = assignIdentity(node);
|
|
679
|
+
if (identity !== void 0) node.identity = identity;
|
|
680
|
+
if (ingested.emoji != null) node.emoji = ingested.emoji;
|
|
681
|
+
if (ingested.parent != null) node.parent = ingested.parent;
|
|
682
|
+
if (ingested.entityType != null) node.entityType = ingested.entityType;
|
|
683
|
+
if (ingested.jsonld != null) {
|
|
684
|
+
node.jsonld = {
|
|
685
|
+
...ingested.jsonld,
|
|
686
|
+
"@id": node.identity ?? node.id
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
if (ingested.sourceFile != null) node.sourceFile = ingested.sourceFile;
|
|
690
|
+
return node;
|
|
691
|
+
}
|
|
692
|
+
var AuthoredRichMarkdownProvider = class {
|
|
693
|
+
id = PROVIDER_ID;
|
|
694
|
+
name = "Authored Rich-Markdown";
|
|
695
|
+
dependencies = [];
|
|
696
|
+
authoredContent;
|
|
697
|
+
constructor(authoredContent) {
|
|
698
|
+
this.authoredContent = authoredContent;
|
|
699
|
+
}
|
|
700
|
+
async resolve(_config, _existingNodes) {
|
|
701
|
+
const nodes = [];
|
|
702
|
+
const edges = [];
|
|
703
|
+
for (const [path, raw] of Object.entries(this.authoredContent)) {
|
|
704
|
+
if (!isRichAuthoredMarkdown(raw)) continue;
|
|
705
|
+
try {
|
|
706
|
+
const fragment = ingestRichMarkdown({
|
|
707
|
+
content: raw,
|
|
708
|
+
path,
|
|
709
|
+
cluster: "docs",
|
|
710
|
+
providerId: PROVIDER_ID
|
|
711
|
+
});
|
|
712
|
+
const idRemap = /* @__PURE__ */ new Map();
|
|
713
|
+
const adapted = [];
|
|
714
|
+
for (const ingested of fragment.nodes) {
|
|
715
|
+
const node = adaptIngestedNode(ingested);
|
|
716
|
+
idRemap.set(ingested.id, node.id);
|
|
717
|
+
adapted.push(node);
|
|
718
|
+
}
|
|
719
|
+
for (const node of adapted) {
|
|
720
|
+
if (node.connections.length > 0) {
|
|
721
|
+
node.connections = node.connections.map((conn) => ({
|
|
722
|
+
...conn,
|
|
723
|
+
to: idRemap.get(conn.to) ?? conn.to
|
|
724
|
+
}));
|
|
725
|
+
}
|
|
726
|
+
nodes.push(node);
|
|
727
|
+
}
|
|
728
|
+
for (const edge of fragment.edges) {
|
|
729
|
+
const e = edge;
|
|
730
|
+
edges.push({
|
|
731
|
+
...e,
|
|
732
|
+
from: idRemap.get(e.from) ?? e.from,
|
|
733
|
+
to: idRemap.get(e.to) ?? e.to
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
console.warn(`[AuthoredRichMarkdownProvider] Failed to ingest ${path}, skipping`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return { nodes, edges };
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
var SCHEMA_PATHS = {
|
|
744
|
+
teamops: "teamops.yaml",
|
|
745
|
+
conventions: "schema/conventions.yaml",
|
|
746
|
+
edges: "schema/edges.yaml",
|
|
747
|
+
lifecycle: "schema/lifecycle.yaml",
|
|
748
|
+
context: "index/context.jsonld",
|
|
749
|
+
/**
|
|
750
|
+
* Optional cross-repo vocabulary / synonym overlay (#153): a JSON-LD
|
|
751
|
+
* `@context` mapping per-repo alias terms → a canonical kind. Absent in most
|
|
752
|
+
* repos, in which case the synonym layer is a safe no-op.
|
|
753
|
+
*/
|
|
754
|
+
vocabulary: "index/vocabulary.jsonld"
|
|
755
|
+
};
|
|
756
|
+
function hasContentModelSource(source) {
|
|
757
|
+
if (!source) return false;
|
|
758
|
+
const f = source.files;
|
|
759
|
+
return typeof f[SCHEMA_PATHS.teamops] === "string" && typeof f[SCHEMA_PATHS.context] === "string";
|
|
760
|
+
}
|
|
761
|
+
function parseYaml(raw) {
|
|
762
|
+
if (raw == null) return void 0;
|
|
763
|
+
try {
|
|
764
|
+
return yaml.parse(raw);
|
|
765
|
+
} catch {
|
|
766
|
+
return void 0;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function parseTeamOps(raw, diags) {
|
|
770
|
+
const doc = parseYaml(raw) ?? {};
|
|
771
|
+
const identity = doc.identity ?? doc;
|
|
772
|
+
const orgsRaw = doc.orgs ?? identity.orgs ?? [];
|
|
773
|
+
const orgs = orgsRaw.map((o) => ({
|
|
774
|
+
id: String(o.id ?? ""),
|
|
775
|
+
name: o.name != null ? String(o.name) : void 0,
|
|
776
|
+
default: o.default === true
|
|
777
|
+
}));
|
|
778
|
+
const authority = String(identity.authority ?? "");
|
|
779
|
+
let defaultOrg = String(identity.defaultOrg ?? identity.org ?? orgs.find((o) => o.default)?.id ?? "");
|
|
780
|
+
if (!authority) diags.push({ level: "error", code: "missing-authority", message: "teamops.yaml has no identity.authority" });
|
|
781
|
+
if (!defaultOrg && orgs.length > 0) defaultOrg = orgs[0].id;
|
|
782
|
+
return { authority, defaultOrg, orgs };
|
|
783
|
+
}
|
|
784
|
+
function parseConventions(raw, diags) {
|
|
785
|
+
const doc = parseYaml(raw) ?? {};
|
|
786
|
+
const kindsRaw = doc.kinds ?? {};
|
|
787
|
+
const kinds = {};
|
|
788
|
+
for (const [kind, c] of Object.entries(kindsRaw)) {
|
|
789
|
+
if (!c || typeof c !== "object") continue;
|
|
790
|
+
kinds[kind] = {
|
|
791
|
+
kind,
|
|
792
|
+
path: String(c.path ?? kind),
|
|
793
|
+
orgScoped: c.orgScoped === true,
|
|
794
|
+
aliasField: c.aliasField != null ? String(c.aliasField) : void 0,
|
|
795
|
+
passthrough: Array.isArray(c.passthrough) ? c.passthrough.map(String) : void 0,
|
|
796
|
+
companionExt: c.companionExt != null ? String(c.companionExt) : void 0
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (Object.keys(kinds).length === 0) {
|
|
800
|
+
diags.push({ level: "warn", code: "no-kinds", message: "conventions.yaml declares no kinds" });
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
typeField: String(doc.typeField ?? "@type"),
|
|
804
|
+
idField: String(doc.idField ?? "id"),
|
|
805
|
+
kinds
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function parseEdges(raw) {
|
|
809
|
+
const doc = parseYaml(raw) ?? {};
|
|
810
|
+
const edges = Array.isArray(doc.edges) ? doc.edges : [];
|
|
811
|
+
const derived = Array.isArray(doc.derived) ? doc.derived : [];
|
|
812
|
+
const deprecated = Array.isArray(doc.deprecated) ? doc.deprecated : [];
|
|
813
|
+
for (const d of deprecated) d.relation = "deprecated";
|
|
814
|
+
return { edges, derived, deprecated };
|
|
815
|
+
}
|
|
816
|
+
function parseLifecycle(raw) {
|
|
817
|
+
const doc = parseYaml(raw) ?? {};
|
|
818
|
+
const bandsRaw = doc.bands ?? {};
|
|
819
|
+
const bands = {};
|
|
820
|
+
for (const [band, kinds] of Object.entries(bandsRaw)) {
|
|
821
|
+
bands[band] = Array.isArray(kinds) ? kinds.map(String) : [];
|
|
822
|
+
}
|
|
823
|
+
return { bands };
|
|
824
|
+
}
|
|
825
|
+
function normalizeBase(base) {
|
|
826
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
827
|
+
}
|
|
828
|
+
function parseContext(raw, diags) {
|
|
829
|
+
let doc = {};
|
|
830
|
+
if (raw != null) {
|
|
831
|
+
try {
|
|
832
|
+
doc = JSON.parse(raw);
|
|
833
|
+
} catch {
|
|
834
|
+
diags.push({ level: "error", code: "bad-context", message: "index/context.jsonld is not valid JSON" });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const ctx = doc["@context"] ?? doc;
|
|
838
|
+
const prefixes = {};
|
|
839
|
+
let base;
|
|
840
|
+
for (const [key2, value] of Object.entries(ctx)) {
|
|
841
|
+
if (key2 === "@base") {
|
|
842
|
+
base = normalizeBase(String(value));
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (key2.startsWith("@")) continue;
|
|
846
|
+
const iri = typeof value === "string" ? value : value && typeof value === "object" ? String(value["@id"] ?? "") : "";
|
|
847
|
+
if (iri) prefixes[key2] = normalizeBase(iri);
|
|
848
|
+
}
|
|
849
|
+
if (Object.keys(prefixes).length === 0) {
|
|
850
|
+
diags.push({ level: "error", code: "no-prefixes", message: "context.jsonld declares no CURIE prefixes" });
|
|
851
|
+
}
|
|
852
|
+
return { base, prefixes };
|
|
853
|
+
}
|
|
854
|
+
function aliasesFromContext(doc) {
|
|
855
|
+
const ctx = doc["@context"] ?? doc;
|
|
856
|
+
const aliases = {};
|
|
857
|
+
if (!ctx || typeof ctx !== "object" || Array.isArray(ctx)) return aliases;
|
|
858
|
+
for (const [term, value] of Object.entries(ctx)) {
|
|
859
|
+
if (term.startsWith("@")) continue;
|
|
860
|
+
const canonical = typeof value === "string" ? value : value && typeof value === "object" ? String(value["@id"] ?? "") : "";
|
|
861
|
+
const a = term.trim();
|
|
862
|
+
const c = canonical.trim();
|
|
863
|
+
if (!a || !c || a === c) continue;
|
|
864
|
+
aliases[a] = c;
|
|
865
|
+
}
|
|
866
|
+
return aliases;
|
|
867
|
+
}
|
|
868
|
+
function parseVocabularyDoc(raw, diags, sourceLabel = SCHEMA_PATHS.vocabulary) {
|
|
869
|
+
if (raw == null) return {};
|
|
870
|
+
let doc;
|
|
871
|
+
try {
|
|
872
|
+
doc = JSON.parse(raw);
|
|
873
|
+
} catch {
|
|
874
|
+
diags.push({ level: "error", code: "bad-vocabulary", message: `${sourceLabel} is not valid JSON` });
|
|
875
|
+
return {};
|
|
876
|
+
}
|
|
877
|
+
return aliasesFromContext(doc);
|
|
878
|
+
}
|
|
879
|
+
function overlayAliases(overlay, diags) {
|
|
880
|
+
if (overlay == null) return {};
|
|
881
|
+
if (typeof overlay === "string") return parseVocabularyDoc(overlay, diags, "vocabulary overlay");
|
|
882
|
+
return { ...overlay.aliases };
|
|
883
|
+
}
|
|
884
|
+
function canonicalKind(schema, term) {
|
|
885
|
+
return schema.vocabulary.aliases[term] ?? term;
|
|
886
|
+
}
|
|
887
|
+
function readContentModelSchema(source, overlay) {
|
|
888
|
+
const diagnostics = [];
|
|
889
|
+
const f = source.files;
|
|
890
|
+
const aliases = {
|
|
891
|
+
...parseVocabularyDoc(f[SCHEMA_PATHS.vocabulary], diagnostics),
|
|
892
|
+
...overlayAliases(overlay, diagnostics)
|
|
893
|
+
};
|
|
894
|
+
const schema = {
|
|
895
|
+
teamops: parseTeamOps(f[SCHEMA_PATHS.teamops], diagnostics),
|
|
896
|
+
conventions: parseConventions(f[SCHEMA_PATHS.conventions], diagnostics),
|
|
897
|
+
edges: parseEdges(f[SCHEMA_PATHS.edges]),
|
|
898
|
+
lifecycle: parseLifecycle(f[SCHEMA_PATHS.lifecycle]),
|
|
899
|
+
context: parseContext(f[SCHEMA_PATHS.context], diagnostics),
|
|
900
|
+
vocabulary: { aliases }
|
|
901
|
+
};
|
|
902
|
+
return { schema, diagnostics };
|
|
903
|
+
}
|
|
904
|
+
function isOrgScoped(schema, kind) {
|
|
905
|
+
return schema.conventions.kinds[kind]?.orgScoped === true;
|
|
906
|
+
}
|
|
907
|
+
function urnLocalId(urn) {
|
|
908
|
+
return stripScheme(urn);
|
|
909
|
+
}
|
|
910
|
+
function buildUrn(schema, kind, id, org, diagnostics) {
|
|
911
|
+
const base = schema.context.prefixes[kind];
|
|
912
|
+
if (!base) {
|
|
913
|
+
diagnostics?.push({ level: "error", code: "unknown-prefix", message: `No URN base in context for kind "${kind}"`, ref: `${kind}:${id}` });
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
if (isOrgScoped(schema, kind)) {
|
|
917
|
+
const resolvedOrg = org ?? schema.teamops.defaultOrg;
|
|
918
|
+
return `${base}${resolvedOrg}/${id}`;
|
|
919
|
+
}
|
|
920
|
+
return `${base}${id}`;
|
|
921
|
+
}
|
|
922
|
+
function resolveCurie(schema, curie, opts = {}) {
|
|
923
|
+
const value = curie.trim();
|
|
924
|
+
const idx = value.indexOf(":");
|
|
925
|
+
if (idx < 0) {
|
|
926
|
+
opts.diagnostics?.push({ level: "warn", code: "not-a-curie", message: `"${curie}" is not a CURIE`, ref: curie });
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
const local = value.slice(idx + 1);
|
|
930
|
+
if (local.startsWith("//")) return value;
|
|
931
|
+
const prefix = value.slice(0, idx);
|
|
932
|
+
if (!schema.context.prefixes[prefix]) {
|
|
933
|
+
opts.diagnostics?.push({ level: "warn", code: "unknown-prefix", message: `Unknown CURIE prefix "${prefix}"`, ref: curie });
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
return buildUrn(schema, prefix, local, opts.org, opts.diagnostics);
|
|
937
|
+
}
|
|
938
|
+
function lifecycleBand(schema, kind) {
|
|
939
|
+
for (const [band, kinds] of Object.entries(schema.lifecycle.bands)) {
|
|
940
|
+
if (kinds.includes(kind)) return band;
|
|
941
|
+
}
|
|
942
|
+
return void 0;
|
|
943
|
+
}
|
|
944
|
+
function getConvention(schema, kind) {
|
|
945
|
+
return schema.conventions.kinds[kind];
|
|
946
|
+
}
|
|
947
|
+
var CONTENT_MODEL_PROVIDER = "content-model";
|
|
948
|
+
var NUL = "\0";
|
|
949
|
+
var key = (kind, value) => `${kind}${NUL}${value}`;
|
|
950
|
+
function humanize(s) {
|
|
951
|
+
return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
952
|
+
}
|
|
953
|
+
function refOf(v) {
|
|
954
|
+
if (v == null) return null;
|
|
955
|
+
if (typeof v === "object") {
|
|
956
|
+
const id = v.id;
|
|
957
|
+
return typeof id === "string" && id.trim() ? id.trim() : null;
|
|
958
|
+
}
|
|
959
|
+
const s = String(v).trim();
|
|
960
|
+
return s || null;
|
|
961
|
+
}
|
|
962
|
+
function isSchemaPath(path) {
|
|
963
|
+
if (path === SCHEMA_PATHS.teamops) return true;
|
|
964
|
+
const top = path.split("/")[0];
|
|
965
|
+
return top === "schema" || top === "index";
|
|
966
|
+
}
|
|
967
|
+
var YAML_RE = /\.ya?ml$/i;
|
|
968
|
+
function joinPath(root, path) {
|
|
969
|
+
const trimmedRoot = root.replace(/\/+$/, "");
|
|
970
|
+
return trimmedRoot ? `${trimmedRoot}/${path}` : path;
|
|
971
|
+
}
|
|
972
|
+
function sourceFormat(path) {
|
|
973
|
+
return /\.json$/i.test(path) ? "json" : "yaml";
|
|
974
|
+
}
|
|
975
|
+
function parseYaml2(raw) {
|
|
976
|
+
try {
|
|
977
|
+
const doc = yaml.parse(raw);
|
|
978
|
+
return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : null;
|
|
979
|
+
} catch {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function detectOrg(schema, kind, path, pathRoot) {
|
|
984
|
+
if (!schema.conventions.kinds[kind]?.orgScoped) return void 0;
|
|
985
|
+
const prefix = `${pathRoot}/`;
|
|
986
|
+
const rel = path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
|
987
|
+
const segments = rel.split("/");
|
|
988
|
+
return segments.length > 1 ? segments[0] : schema.teamops.defaultOrg;
|
|
989
|
+
}
|
|
990
|
+
function companionBody(files, entityPath, ext) {
|
|
991
|
+
if (!ext) return void 0;
|
|
992
|
+
const companion = entityPath.replace(YAML_RE, ext);
|
|
993
|
+
return companion !== entityPath ? files[companion] : void 0;
|
|
994
|
+
}
|
|
995
|
+
function walkEntities(schema, source, diagnostics) {
|
|
996
|
+
const { typeField, idField } = schema.conventions;
|
|
997
|
+
const entries = [];
|
|
998
|
+
const byKindId = /* @__PURE__ */ new Map();
|
|
999
|
+
const byKindAlias = /* @__PURE__ */ new Map();
|
|
1000
|
+
for (const [path, raw] of Object.entries(source.files)) {
|
|
1001
|
+
if (isSchemaPath(path) || !YAML_RE.test(path)) continue;
|
|
1002
|
+
const record = parseYaml2(raw);
|
|
1003
|
+
if (!record) {
|
|
1004
|
+
diagnostics.push({ level: "warn", code: "unparsable-entity", message: `Could not parse entity file`, ref: path });
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const declaredType = record[typeField] != null ? String(record[typeField]) : "";
|
|
1008
|
+
if (!declaredType) {
|
|
1009
|
+
diagnostics.push({ level: "warn", code: "missing-type", message: `Entity has no ${typeField}`, ref: path });
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
const kind = canonicalKind(schema, declaredType);
|
|
1013
|
+
const nativeType = kind !== declaredType ? declaredType : void 0;
|
|
1014
|
+
const convention = getConvention(schema, kind);
|
|
1015
|
+
if (!convention) {
|
|
1016
|
+
diagnostics.push({ level: "warn", code: "unknown-kind", message: `No convention for kind "${kind}"`, ref: path });
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
const id = record[idField] != null ? String(record[idField]) : "";
|
|
1020
|
+
if (!id) {
|
|
1021
|
+
diagnostics.push({ level: "warn", code: "missing-id", message: `Entity has no ${idField}`, ref: path });
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const org = detectOrg(schema, kind, path, convention.path);
|
|
1025
|
+
const urn = buildUrn(schema, kind, id, org, diagnostics);
|
|
1026
|
+
if (!urn) continue;
|
|
1027
|
+
const entry = {
|
|
1028
|
+
kind,
|
|
1029
|
+
id,
|
|
1030
|
+
org,
|
|
1031
|
+
urn,
|
|
1032
|
+
record,
|
|
1033
|
+
nativeType,
|
|
1034
|
+
body: companionBody(source.files, path, convention.companionExt),
|
|
1035
|
+
path,
|
|
1036
|
+
raw
|
|
1037
|
+
};
|
|
1038
|
+
entries.push(entry);
|
|
1039
|
+
byKindId.set(key(kind, id), entry);
|
|
1040
|
+
if (convention.aliasField) {
|
|
1041
|
+
const alias = record[convention.aliasField];
|
|
1042
|
+
if (alias != null) byKindAlias.set(key(kind, String(alias)), entry);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
entries.sort((a, b) => a.urn < b.urn ? -1 : a.urn > b.urn ? 1 : 0);
|
|
1046
|
+
return { entries, byKindId, byKindAlias };
|
|
1047
|
+
}
|
|
1048
|
+
function ldContextOf(schema) {
|
|
1049
|
+
const ctx = { ...schema.context.prefixes };
|
|
1050
|
+
if (schema.context.base) ctx["@base"] = schema.context.base;
|
|
1051
|
+
return Object.keys(ctx).length > 0 ? ctx : "https://schema.org";
|
|
1052
|
+
}
|
|
1053
|
+
function emitNode(schema, entry, ldContext, root) {
|
|
1054
|
+
const { kind, id, urn, record, body, nativeType } = entry;
|
|
1055
|
+
const title = String(record.name ?? record.title ?? id);
|
|
1056
|
+
const data = { ...record };
|
|
1057
|
+
const band = lifecycleBand(schema, kind);
|
|
1058
|
+
const ldData = { ...data };
|
|
1059
|
+
if (band) ldData.lifecycle = band;
|
|
1060
|
+
if (nativeType) ldData.nativeType = nativeType;
|
|
1061
|
+
const content = body ? renderSafeMarkdown(body) : "";
|
|
1062
|
+
const localId = urnLocalId(urn);
|
|
1063
|
+
const node = {
|
|
1064
|
+
id: localId,
|
|
1065
|
+
title,
|
|
1066
|
+
cluster: kind,
|
|
1067
|
+
content,
|
|
1068
|
+
rawContent: body ?? "",
|
|
1069
|
+
display: "entity",
|
|
1070
|
+
connections: [],
|
|
1071
|
+
derived: true,
|
|
1072
|
+
source: { type: "structured", entityType: kind, ref: id },
|
|
1073
|
+
entityType: kind,
|
|
1074
|
+
provider: CONTENT_MODEL_PROVIDER,
|
|
1075
|
+
data,
|
|
1076
|
+
jsonld: buildJsonLd({ id: localId, identity: urn }, kind, ldData, ldContext),
|
|
1077
|
+
// Pointer to the underlying source-of-truth file so the in-app editor can
|
|
1078
|
+
// edit the real entity file and hand the change off to GitHub as a PR
|
|
1079
|
+
// (F5 — #152). The path is repo-relative (root + entry path).
|
|
1080
|
+
sourceFile: { path: joinPath(root, entry.path), raw: entry.raw, format: sourceFormat(entry.path) }
|
|
1081
|
+
};
|
|
1082
|
+
const identity = assignIdentity(node);
|
|
1083
|
+
if (identity !== void 0) node.identity = identity;
|
|
1084
|
+
return node;
|
|
1085
|
+
}
|
|
1086
|
+
var EdgeResolver = class {
|
|
1087
|
+
edges = [];
|
|
1088
|
+
edgeKeys = /* @__PURE__ */ new Set();
|
|
1089
|
+
connKeys = /* @__PURE__ */ new Set();
|
|
1090
|
+
stubs = /* @__PURE__ */ new Map();
|
|
1091
|
+
schema;
|
|
1092
|
+
index;
|
|
1093
|
+
nodeByUrn;
|
|
1094
|
+
ldContext;
|
|
1095
|
+
diagnostics;
|
|
1096
|
+
constructor(schema, index, nodeByUrn, ldContext, diagnostics) {
|
|
1097
|
+
this.schema = schema;
|
|
1098
|
+
this.index = index;
|
|
1099
|
+
this.nodeByUrn = nodeByUrn;
|
|
1100
|
+
this.ldContext = ldContext;
|
|
1101
|
+
this.diagnostics = diagnostics;
|
|
1102
|
+
}
|
|
1103
|
+
/** Resolve a reference to a target URN, lazily creating a stub when missing. */
|
|
1104
|
+
resolve(targetKind, ref, mode) {
|
|
1105
|
+
const map = mode === "alias" ? this.index.byKindAlias : this.index.byKindId;
|
|
1106
|
+
const hit = map.get(key(targetKind, ref));
|
|
1107
|
+
if (hit) return hit.urn;
|
|
1108
|
+
return this.stub(targetKind, ref);
|
|
1109
|
+
}
|
|
1110
|
+
/** Lookup-only resolution (no stub creation) — used for derived grouping. */
|
|
1111
|
+
lookup(targetKind, ref, mode) {
|
|
1112
|
+
const map = mode === "alias" ? this.index.byKindAlias : this.index.byKindId;
|
|
1113
|
+
return map.get(key(targetKind, ref))?.urn;
|
|
1114
|
+
}
|
|
1115
|
+
stub(targetKind, ref) {
|
|
1116
|
+
const urn = buildUrn(this.schema, targetKind, ref) ?? `kg://unresolved/${targetKind}/${ref}`;
|
|
1117
|
+
if (!this.stubs.has(urn)) {
|
|
1118
|
+
const data = { id: ref, unresolved: true };
|
|
1119
|
+
const localId = urnLocalId(urn);
|
|
1120
|
+
const node = {
|
|
1121
|
+
id: localId,
|
|
1122
|
+
title: ref,
|
|
1123
|
+
cluster: targetKind,
|
|
1124
|
+
content: "",
|
|
1125
|
+
rawContent: "",
|
|
1126
|
+
display: "entity",
|
|
1127
|
+
connections: [],
|
|
1128
|
+
derived: true,
|
|
1129
|
+
source: { type: "structured", entityType: targetKind, ref },
|
|
1130
|
+
entityType: targetKind,
|
|
1131
|
+
provider: CONTENT_MODEL_PROVIDER,
|
|
1132
|
+
data,
|
|
1133
|
+
jsonld: buildJsonLd({ id: localId, identity: urn }, targetKind, data, this.ldContext)
|
|
1134
|
+
};
|
|
1135
|
+
const identity = assignIdentity(node);
|
|
1136
|
+
if (identity !== void 0) node.identity = identity;
|
|
1137
|
+
this.stubs.set(urn, node);
|
|
1138
|
+
this.nodeByUrn.set(urn, node);
|
|
1139
|
+
this.diagnostics.push({
|
|
1140
|
+
level: "warn",
|
|
1141
|
+
code: "unresolved-ref",
|
|
1142
|
+
message: `Unresolved ${targetKind} reference "${ref}"`,
|
|
1143
|
+
ref: urn
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
return urn;
|
|
1147
|
+
}
|
|
1148
|
+
addEdge(from, to, relation, description) {
|
|
1149
|
+
if (from === to) return;
|
|
1150
|
+
const fromId = urnLocalId(from);
|
|
1151
|
+
const toId = urnLocalId(to);
|
|
1152
|
+
const k = `${from}${NUL}${to}${NUL}${relation}`;
|
|
1153
|
+
if (!this.edgeKeys.has(k)) {
|
|
1154
|
+
this.edgeKeys.add(k);
|
|
1155
|
+
this.edges.push({ from: fromId, to: toId, type: "related", relation, description, source: "inferred", weight: 1 });
|
|
1156
|
+
}
|
|
1157
|
+
const ck = `${from}${NUL}${to}${NUL}${relation}`;
|
|
1158
|
+
if (!this.connKeys.has(ck)) {
|
|
1159
|
+
this.connKeys.add(ck);
|
|
1160
|
+
const conn = { to: toId, type: "related", description, source: "inferred", relation };
|
|
1161
|
+
this.nodeByUrn.get(from)?.connections.push(conn);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
/** Apply a single FK rule to one source entry. */
|
|
1165
|
+
applyRule(entry, rule, relationOverride) {
|
|
1166
|
+
const raw = entry.record[rule.field];
|
|
1167
|
+
if (raw == null) return;
|
|
1168
|
+
if (rule.fk === "composite") {
|
|
1169
|
+
const parts = String(raw).split(":").map((p) => p.trim());
|
|
1170
|
+
const legs = rule.composite ?? [];
|
|
1171
|
+
parts.forEach((part, i) => {
|
|
1172
|
+
const leg = legs[i];
|
|
1173
|
+
if (!leg) {
|
|
1174
|
+
this.diagnostics.push({
|
|
1175
|
+
level: "warn",
|
|
1176
|
+
code: "composite-arity",
|
|
1177
|
+
message: `Composite FK "${rule.id}" has no leg for part ${i} ("${part}")`,
|
|
1178
|
+
ref: entry.urn
|
|
1179
|
+
});
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const to = this.resolve(leg.to, part, "id");
|
|
1183
|
+
this.addEdge(entry.urn, to, leg.relation, rule.description ?? humanize(leg.relation));
|
|
1184
|
+
});
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const relation = relationOverride ?? rule.relation;
|
|
1188
|
+
const mode = rule.fk === "alias" ? "alias" : "id";
|
|
1189
|
+
const values = rule.fk === "array" ? Array.isArray(raw) ? raw : [raw] : [raw];
|
|
1190
|
+
for (const v of values) {
|
|
1191
|
+
if (v == null) continue;
|
|
1192
|
+
const ref = refOf(v);
|
|
1193
|
+
if (ref == null) {
|
|
1194
|
+
this.diagnostics.push({
|
|
1195
|
+
level: "warn",
|
|
1196
|
+
code: "bad-ref-shape",
|
|
1197
|
+
message: `FK "${rule.id}" entry is an object without a string "id" \u2014 expected an id string (or { id: ... })`,
|
|
1198
|
+
ref: entry.urn
|
|
1199
|
+
});
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
const to = this.resolve(rule.to ?? "", ref, mode);
|
|
1203
|
+
this.addEdge(entry.urn, to, relation, rule.description ?? humanize(relation));
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/** Pass 4: resolve all FK rules (and deprecated rules, tagged `deprecated`). */
|
|
1207
|
+
resolveForeignKeys() {
|
|
1208
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
1209
|
+
for (const e of this.index.entries) {
|
|
1210
|
+
(byKind.get(e.kind) ?? byKind.set(e.kind, []).get(e.kind)).push(e);
|
|
1211
|
+
}
|
|
1212
|
+
const run = (rules, deprecated) => {
|
|
1213
|
+
for (const rule of rules) {
|
|
1214
|
+
for (const entry of byKind.get(rule.from) ?? []) {
|
|
1215
|
+
this.applyRule(entry, rule, deprecated ? "deprecated" : void 0);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
run(this.schema.edges.edges, false);
|
|
1220
|
+
run(this.schema.edges.deprecated, true);
|
|
1221
|
+
}
|
|
1222
|
+
/** Pass 5: compute derived `shared-target` edges (deduped, undirected). */
|
|
1223
|
+
resolveDerived() {
|
|
1224
|
+
for (const rule of this.schema.edges.derived) {
|
|
1225
|
+
if (rule.type !== "shared-target") continue;
|
|
1226
|
+
const via = this.schema.edges.edges.find((e) => e.id === rule.via);
|
|
1227
|
+
if (!via || via.fk === "composite" || !via.to) {
|
|
1228
|
+
this.diagnostics.push({
|
|
1229
|
+
level: "warn",
|
|
1230
|
+
code: "bad-derived",
|
|
1231
|
+
message: `Derived rule "${rule.id}" references unusable FK "${rule.via}"`
|
|
1232
|
+
});
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
const mode = via.fk === "alias" ? "alias" : "id";
|
|
1236
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1237
|
+
for (const entry of this.index.entries) {
|
|
1238
|
+
if (entry.kind !== via.from) continue;
|
|
1239
|
+
const raw = entry.record[via.field];
|
|
1240
|
+
if (raw == null) continue;
|
|
1241
|
+
const refs = via.fk === "array" ? Array.isArray(raw) ? raw : [raw] : [raw];
|
|
1242
|
+
for (const r of refs) {
|
|
1243
|
+
const ref = refOf(r);
|
|
1244
|
+
if (ref == null) continue;
|
|
1245
|
+
const targetUrn = this.lookup(via.to, ref, mode);
|
|
1246
|
+
if (!targetUrn) continue;
|
|
1247
|
+
(groups.get(targetUrn) ?? groups.set(targetUrn, []).get(targetUrn)).push(entry.urn);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
for (const members of groups.values()) {
|
|
1251
|
+
const unique = [...new Set(members)].sort();
|
|
1252
|
+
for (let i = 0; i < unique.length; i++) {
|
|
1253
|
+
for (let j = i + 1; j < unique.length; j++) {
|
|
1254
|
+
this.addEdge(unique[i], unique[j], rule.relation, rule.description ?? humanize(rule.relation));
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
function buildContentModel(source, vocabularyOverlay) {
|
|
1262
|
+
if (!hasContentModelSource(source)) {
|
|
1263
|
+
return { nodes: [], edges: [], diagnostics: [] };
|
|
1264
|
+
}
|
|
1265
|
+
const src = source;
|
|
1266
|
+
const { schema, diagnostics } = readContentModelSchema(src, vocabularyOverlay);
|
|
1267
|
+
const ldContext = ldContextOf(schema);
|
|
1268
|
+
const index = walkEntities(schema, src, diagnostics);
|
|
1269
|
+
const nodeByUrn = /* @__PURE__ */ new Map();
|
|
1270
|
+
for (const entry of index.entries) {
|
|
1271
|
+
nodeByUrn.set(entry.urn, emitNode(schema, entry, ldContext, src.root));
|
|
1272
|
+
}
|
|
1273
|
+
const resolver = new EdgeResolver(schema, index, nodeByUrn, ldContext, diagnostics);
|
|
1274
|
+
resolver.resolveForeignKeys();
|
|
1275
|
+
resolver.resolveDerived();
|
|
1276
|
+
const nodes = [...nodeByUrn.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1277
|
+
const edges = resolver.edges.slice().sort((a, b) => {
|
|
1278
|
+
const ka = `${a.from}${NUL}${a.to}${NUL}${a.relation}`;
|
|
1279
|
+
const kb = `${b.from}${NUL}${b.to}${NUL}${b.relation}`;
|
|
1280
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
1281
|
+
});
|
|
1282
|
+
return { nodes, edges, diagnostics };
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// src/content-model/register.ts
|
|
1286
|
+
var CONTENT_MODEL_KINDS = [
|
|
1287
|
+
{ id: "person", label: "Person", layer: "work", relations: ["reports-to"], viewer: "person", description: "An individual in the org." },
|
|
1288
|
+
{ id: "squad", label: "Squad", layer: "work", relations: ["leads", "staffs", "structural", "deprecated"], viewer: "squad", description: "A squad that staffs people, is led by a DRI, and delivers a workstream." },
|
|
1289
|
+
{ id: "workstream", label: "Workstream", layer: "work", relations: ["structural"], viewer: "workstream", description: "A stream of work aligned to a priority." },
|
|
1290
|
+
{ id: "mission", label: "Mission", layer: "work", relations: ["structural"], viewer: "mission", description: "A time-boxed mission assigned to a cycle + squad." },
|
|
1291
|
+
{ id: "priority", label: "Priority", layer: "work", relations: [], viewer: "priority", description: "A ranked organizational priority." },
|
|
1292
|
+
{ id: "cycle", label: "Cycle", layer: "work", relations: [], viewer: "cycle", description: "A planning cycle (time box)." },
|
|
1293
|
+
{ id: "org", label: "Org", layer: "work", relations: [], viewer: "org", description: "An organization with a charter." },
|
|
1294
|
+
// Work-graph organizational-layer descriptor kinds (#233)
|
|
1295
|
+
{ id: "team", label: "Team", layer: "work", relations: ["leads", "staffs", "owns"], viewer: "team", description: "A team that leads people and owns workstreams." },
|
|
1296
|
+
{ id: "system-of-record", label: "System of Record", layer: "work", relations: ["tracked-in"], viewer: "system-of-record", description: "An external system that tracks a workstream (e.g. an ADO board or GitHub repo)." },
|
|
1297
|
+
// Services-monorepo core kinds (#275)
|
|
1298
|
+
{ id: "service", label: "Service", layer: "work", relations: ["owned-by", "tracked-in"], viewer: "service", description: "A deployable service owned by a team, with a ServiceTree id and catalog-info path." },
|
|
1299
|
+
{ id: "decision", label: "Decision", layer: "work", relations: ["decided-by", "affects"], viewer: "decision", description: "An architecture decision record (ADR) \u2014 deciders, status, context, and the work it affects." }
|
|
1300
|
+
];
|
|
1301
|
+
function registerContentModelTypes() {
|
|
1302
|
+
for (const k of CONTENT_MODEL_KINDS) {
|
|
1303
|
+
registerType({
|
|
1304
|
+
id: k.id,
|
|
1305
|
+
label: k.label,
|
|
1306
|
+
layer: k.layer,
|
|
1307
|
+
cluster: k.id,
|
|
1308
|
+
relations: k.relations,
|
|
1309
|
+
viewer: k.viewer,
|
|
1310
|
+
description: k.description
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/providers/content-model-provider.ts
|
|
1316
|
+
var ContentModelProvider = class {
|
|
1317
|
+
id = CONTENT_MODEL_PROVIDER;
|
|
1318
|
+
name = "Content Model";
|
|
1319
|
+
dependencies = [];
|
|
1320
|
+
source;
|
|
1321
|
+
/**
|
|
1322
|
+
* Optional cross-repo synonym overlay (#153) supplied independently of the
|
|
1323
|
+
* source's own files — the shared vocabulary layer. Null/absent leaves the
|
|
1324
|
+
* synonym layer a safe no-op.
|
|
1325
|
+
*/
|
|
1326
|
+
vocabularyOverlay;
|
|
1327
|
+
constructor(source, vocabularyOverlay) {
|
|
1328
|
+
this.source = source ?? null;
|
|
1329
|
+
this.vocabularyOverlay = vocabularyOverlay ?? null;
|
|
1330
|
+
}
|
|
1331
|
+
async resolve(_config, _existingNodes) {
|
|
1332
|
+
if (!hasContentModelSource(this.source)) {
|
|
1333
|
+
return { nodes: [], edges: [] };
|
|
1334
|
+
}
|
|
1335
|
+
registerContentModelTypes();
|
|
1336
|
+
const { nodes } = buildContentModel(this.source, this.vocabularyOverlay);
|
|
1337
|
+
const anchored = nodes.map((n) => ({
|
|
1338
|
+
...n,
|
|
1339
|
+
cluster: "teamops",
|
|
1340
|
+
connections: [
|
|
1341
|
+
...n.connections,
|
|
1342
|
+
{
|
|
1343
|
+
to: "repo-meta",
|
|
1344
|
+
type: "references",
|
|
1345
|
+
relation: "tracked-in",
|
|
1346
|
+
description: "Tracked in this repository",
|
|
1347
|
+
source: "inferred"
|
|
1348
|
+
}
|
|
1349
|
+
]
|
|
1350
|
+
}));
|
|
1351
|
+
return { nodes: anchored, edges: [] };
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
// src/providers/files-provider.ts
|
|
1356
|
+
var FilesProvider = class {
|
|
1357
|
+
id = "files";
|
|
1358
|
+
name = "File System";
|
|
1359
|
+
dependencies = [];
|
|
1360
|
+
treeItems;
|
|
1361
|
+
repoName;
|
|
1362
|
+
excludePaths;
|
|
1363
|
+
constructor(treeItems, repoName, excludePaths) {
|
|
1364
|
+
this.treeItems = treeItems;
|
|
1365
|
+
this.repoName = repoName;
|
|
1366
|
+
this.excludePaths = excludePaths;
|
|
1367
|
+
}
|
|
1368
|
+
async resolve(_config, _existingNodes) {
|
|
1369
|
+
const nodes = treeToNodes(this.treeItems, this.repoName, this.excludePaths);
|
|
1370
|
+
for (const node of nodes) {
|
|
1371
|
+
node.provider = "files";
|
|
1372
|
+
if (!node.identity) {
|
|
1373
|
+
const identity = assignIdentity(node);
|
|
1374
|
+
if (identity !== void 0) node.identity = identity;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return { nodes, edges: [] };
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
var DEFAULT_MIN_ACTIVE_ITEMS = 1;
|
|
1381
|
+
var PersonProvider = class {
|
|
1382
|
+
id = "person";
|
|
1383
|
+
name = "People";
|
|
1384
|
+
/**
|
|
1385
|
+
* Run after work so we can match existing nodes; run after content-model
|
|
1386
|
+
* so we can link to descriptor people.
|
|
1387
|
+
*/
|
|
1388
|
+
dependencies = ["work", "content-model"];
|
|
1389
|
+
issues;
|
|
1390
|
+
pullRequests;
|
|
1391
|
+
constructor(issues, pullRequests) {
|
|
1392
|
+
this.issues = issues;
|
|
1393
|
+
this.pullRequests = pullRequests;
|
|
1394
|
+
}
|
|
1395
|
+
async resolve(config, existingNodes) {
|
|
1396
|
+
const minActive = config.people?.minActiveItems ?? DEFAULT_MIN_ACTIVE_ITEMS;
|
|
1397
|
+
const byLogin = /* @__PURE__ */ new Map();
|
|
1398
|
+
function ensureLogin(login) {
|
|
1399
|
+
let d = byLogin.get(login);
|
|
1400
|
+
if (!d) {
|
|
1401
|
+
d = { login, assignedIssues: [], authoredIssues: [], assignedPRs: [], authoredPRs: [] };
|
|
1402
|
+
byLogin.set(login, d);
|
|
1403
|
+
}
|
|
1404
|
+
return d;
|
|
1405
|
+
}
|
|
1406
|
+
for (const issue of this.issues) {
|
|
1407
|
+
if (issue.state !== "open") continue;
|
|
1408
|
+
for (const a of issue.assignees ?? []) {
|
|
1409
|
+
ensureLogin(a.login).assignedIssues.push(issue);
|
|
1410
|
+
}
|
|
1411
|
+
if (issue.user?.login) {
|
|
1412
|
+
ensureLogin(issue.user.login).authoredIssues.push(issue);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
for (const pr of this.pullRequests) {
|
|
1416
|
+
if (pr.state !== "open") continue;
|
|
1417
|
+
for (const a of pr.assignees ?? []) {
|
|
1418
|
+
ensureLogin(a.login).assignedPRs.push(pr);
|
|
1419
|
+
}
|
|
1420
|
+
if (pr.user?.login) {
|
|
1421
|
+
ensureLogin(pr.user.login).authoredPRs.push(pr);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const qualifying = [...byLogin.values()].filter((d) => {
|
|
1425
|
+
const activeCount = d.assignedIssues.length + d.authoredIssues.length + d.assignedPRs.length + d.authoredPRs.length;
|
|
1426
|
+
return activeCount >= minActive;
|
|
1427
|
+
});
|
|
1428
|
+
if (qualifying.length === 0) return { nodes: [], edges: [] };
|
|
1429
|
+
const descriptorByAlias = /* @__PURE__ */ new Map();
|
|
1430
|
+
for (const n of existingNodes) {
|
|
1431
|
+
if (n.entityType === "person" || n.source.type === "structured" && n.source.entityType === "person") {
|
|
1432
|
+
const alias = n.data?.alias;
|
|
1433
|
+
if (alias) descriptorByAlias.set(alias.toLowerCase(), n);
|
|
1434
|
+
const descId = n.data?.id;
|
|
1435
|
+
if (descId) descriptorByAlias.set(descId.toLowerCase(), n);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const nodes = [];
|
|
1439
|
+
const edges = [];
|
|
1440
|
+
for (const d of qualifying) {
|
|
1441
|
+
const loginLower = d.login.toLowerCase();
|
|
1442
|
+
const descriptor = descriptorByAlias.get(loginLower);
|
|
1443
|
+
const connections = [];
|
|
1444
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1445
|
+
const addConn = (to, relation) => {
|
|
1446
|
+
if (!seen.has(to)) {
|
|
1447
|
+
connections.push({
|
|
1448
|
+
to,
|
|
1449
|
+
relation,
|
|
1450
|
+
description: relation === "assigned-to" ? "Assigned to" : "Authored",
|
|
1451
|
+
type: "related",
|
|
1452
|
+
source: "inferred"
|
|
1453
|
+
});
|
|
1454
|
+
seen.add(to);
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
for (const issue of d.assignedIssues) addConn(`issue-${issue.number}`, "assigned-to");
|
|
1458
|
+
for (const issue of d.authoredIssues) addConn(`issue-${issue.number}`, "authored");
|
|
1459
|
+
for (const pr of d.assignedPRs) addConn(`pr-${pr.number}`, "assigned-to");
|
|
1460
|
+
for (const pr of d.authoredPRs) addConn(`pr-${pr.number}`, "authored");
|
|
1461
|
+
if (descriptor) {
|
|
1462
|
+
const witness = {
|
|
1463
|
+
kind: "github",
|
|
1464
|
+
href: urnIdentity("person", d.login),
|
|
1465
|
+
resourceKind: "person"
|
|
1466
|
+
};
|
|
1467
|
+
const refs = descriptor.linkedRefs ?? [];
|
|
1468
|
+
if (!refs.some((r) => r.href === witness.href)) {
|
|
1469
|
+
descriptor.linkedRefs = [...refs, witness];
|
|
1470
|
+
}
|
|
1471
|
+
const uniqueIssuesD = [
|
|
1472
|
+
...d.assignedIssues,
|
|
1473
|
+
...d.authoredIssues.filter((i) => !d.assignedIssues.some((a) => a.number === i.number))
|
|
1474
|
+
];
|
|
1475
|
+
const uniquePRsD = [
|
|
1476
|
+
...d.assignedPRs,
|
|
1477
|
+
...d.authoredPRs.filter((pr) => !d.assignedPRs.some((a) => a.number === pr.number))
|
|
1478
|
+
];
|
|
1479
|
+
descriptor.data = {
|
|
1480
|
+
...descriptor.data ?? {},
|
|
1481
|
+
login: d.login,
|
|
1482
|
+
activeIssues: uniqueIssuesD.map((i) => ({ number: i.number, title: i.title })),
|
|
1483
|
+
activePRs: uniquePRsD.map((pr) => ({ number: pr.number, title: pr.title })),
|
|
1484
|
+
activeIssueCount: uniqueIssuesD.length,
|
|
1485
|
+
activePRCount: uniquePRsD.length
|
|
1486
|
+
};
|
|
1487
|
+
for (const conn of connections) {
|
|
1488
|
+
const alreadyPresent = descriptor.connections.some((c) => c.to === conn.to);
|
|
1489
|
+
if (!alreadyPresent) descriptor.connections.push(conn);
|
|
1490
|
+
}
|
|
1491
|
+
for (const conn of connections) {
|
|
1492
|
+
edges.push({
|
|
1493
|
+
from: descriptor.id,
|
|
1494
|
+
to: conn.to,
|
|
1495
|
+
type: "related",
|
|
1496
|
+
...conn.relation ? { relation: conn.relation } : {},
|
|
1497
|
+
description: conn.description ?? "",
|
|
1498
|
+
source: "inferred",
|
|
1499
|
+
weight: 1.5
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
const uniqueIssues = [
|
|
1505
|
+
...d.assignedIssues,
|
|
1506
|
+
...d.authoredIssues.filter((i) => !d.assignedIssues.some((a) => a.number === i.number))
|
|
1507
|
+
];
|
|
1508
|
+
const uniquePRs = [
|
|
1509
|
+
...d.assignedPRs,
|
|
1510
|
+
...d.authoredPRs.filter((p) => !d.assignedPRs.some((a) => a.number === p.number))
|
|
1511
|
+
];
|
|
1512
|
+
const issueLines = uniqueIssues.map(
|
|
1513
|
+
(i) => `- [#${i.number}: ${i.title}](issue-${i.number}) *(issue)*`
|
|
1514
|
+
);
|
|
1515
|
+
const prLines = uniquePRs.map(
|
|
1516
|
+
(p) => `- [#${p.number}: ${p.title}](pr-${p.number}) *(PR)*`
|
|
1517
|
+
);
|
|
1518
|
+
const allLines = [...issueLines, ...prLines];
|
|
1519
|
+
const rawContent = [
|
|
1520
|
+
`## @${d.login}`,
|
|
1521
|
+
"",
|
|
1522
|
+
`**GitHub login:** \`${d.login}\``,
|
|
1523
|
+
"",
|
|
1524
|
+
`### Active work (${allLines.length} item${allLines.length !== 1 ? "s" : ""})`,
|
|
1525
|
+
"",
|
|
1526
|
+
...allLines
|
|
1527
|
+
].join("\n");
|
|
1528
|
+
const content = renderSafeMarkdown(rawContent);
|
|
1529
|
+
const nodeId = `person-${d.login}`;
|
|
1530
|
+
const personNode = {
|
|
1531
|
+
id: nodeId,
|
|
1532
|
+
title: `@${d.login}`,
|
|
1533
|
+
cluster: "work",
|
|
1534
|
+
content,
|
|
1535
|
+
rawContent,
|
|
1536
|
+
emoji: "Person",
|
|
1537
|
+
display: "entity",
|
|
1538
|
+
connections,
|
|
1539
|
+
source: { type: "person", login: d.login, linked: false },
|
|
1540
|
+
provider: "person",
|
|
1541
|
+
entityType: "person",
|
|
1542
|
+
derived: true,
|
|
1543
|
+
// Core v0.3.0 identity/link substrate (#445 / AF-013): alongside the
|
|
1544
|
+
// legacy source witness, point at the person's canonical, host-neutral
|
|
1545
|
+
// address (`buildPersonAddress(login)` — the alias-based `kg://` form
|
|
1546
|
+
// cli/directory providers mint). kbexplorer-cli's edge-mint resolves
|
|
1547
|
+
// linkedRef hrefs against node identities, so this is what makes
|
|
1548
|
+
// template person nodes visible to its identity-linking at all.
|
|
1549
|
+
linkedRefs: [
|
|
1550
|
+
{
|
|
1551
|
+
kind: "github",
|
|
1552
|
+
href: buildPersonAddress(d.login),
|
|
1553
|
+
resourceKind: "person"
|
|
1554
|
+
}
|
|
1555
|
+
],
|
|
1556
|
+
data: {
|
|
1557
|
+
login: d.login,
|
|
1558
|
+
activeIssues: uniqueIssues.map((i) => ({ number: i.number, title: i.title })),
|
|
1559
|
+
activePRs: uniquePRs.map((p) => ({ number: p.number, title: p.title })),
|
|
1560
|
+
activeIssueCount: uniqueIssues.length,
|
|
1561
|
+
activePRCount: uniquePRs.length
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
const identity = assignIdentity(personNode);
|
|
1565
|
+
if (identity !== void 0) personNode.identity = identity;
|
|
1566
|
+
nodes.push(personNode);
|
|
1567
|
+
for (const conn of connections) {
|
|
1568
|
+
edges.push({
|
|
1569
|
+
from: nodeId,
|
|
1570
|
+
to: conn.to,
|
|
1571
|
+
type: "related",
|
|
1572
|
+
...conn.relation ? { relation: conn.relation } : {},
|
|
1573
|
+
description: conn.description ?? "",
|
|
1574
|
+
source: "inferred",
|
|
1575
|
+
weight: 1.5
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return { nodes, edges };
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
var REPO_NODE_ID = "repo-meta";
|
|
1583
|
+
var STRUCTURAL_CLUSTER = "infra";
|
|
1584
|
+
function registerStructuralTypes() {
|
|
1585
|
+
registerType({ id: "workflow", label: "Workflow", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], viewer: "workflow", description: "A GitHub Actions workflow." });
|
|
1586
|
+
registerType({ id: "github-action", label: "Action", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], viewer: "github-action", description: "A composite or JS GitHub Action." });
|
|
1587
|
+
registerType({ id: "skill", label: "Skill", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], viewer: "skill", description: "A Copilot / agent skill (SKILL.md): when-to-use triggers + guidance." });
|
|
1588
|
+
registerType({ id: "issue-template", label: "Issue Template", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], description: "A GitHub issue template / form." });
|
|
1589
|
+
registerType({ id: "pr-template", label: "PR Template", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], description: "A pull-request template." });
|
|
1590
|
+
registerType({ id: "codeowners", label: "CODEOWNERS", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], description: "Code ownership rules." });
|
|
1591
|
+
registerType({ id: "dependabot-config", label: "Dependabot", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], description: "Dependabot update configuration." });
|
|
1592
|
+
registerType({ id: "funding-config", label: "Funding", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], description: "Repository funding links." });
|
|
1593
|
+
registerType({ id: "github-config", label: "Repo Config", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], description: "Generic repository configuration file." });
|
|
1594
|
+
registerType({ id: "structured-config", label: "Config", layer: "work", cluster: STRUCTURAL_CLUSTER, relations: ["structural"], description: "Heuristically-typed structured config." });
|
|
1595
|
+
}
|
|
1596
|
+
function fileName2(path) {
|
|
1597
|
+
return path.split("/").pop() ?? path;
|
|
1598
|
+
}
|
|
1599
|
+
function isWorkflow(path) {
|
|
1600
|
+
return /^\.github\/workflows\/[^/]+\.ya?ml$/i.test(path);
|
|
1601
|
+
}
|
|
1602
|
+
function isAction(path) {
|
|
1603
|
+
return /(^|\/)action\.ya?ml$/i.test(path);
|
|
1604
|
+
}
|
|
1605
|
+
function isSkill(path) {
|
|
1606
|
+
return /^\.github\/skills\/.+\/SKILL\.md$/i.test(path) || /(^|\/)[^/]+\.skill\.md$/i.test(path);
|
|
1607
|
+
}
|
|
1608
|
+
function isIssueTemplate(path) {
|
|
1609
|
+
return /^\.github\/ISSUE_TEMPLATE\/.+/i.test(path) && !/\/config\.ya?ml$/i.test(path);
|
|
1610
|
+
}
|
|
1611
|
+
function isIssueTemplateConfig(path) {
|
|
1612
|
+
return /^\.github\/ISSUE_TEMPLATE\/config\.ya?ml$/i.test(path);
|
|
1613
|
+
}
|
|
1614
|
+
function isPrTemplate(path) {
|
|
1615
|
+
return /^\.github\/(PULL_REQUEST_TEMPLATE|pull_request_template)\.md$/i.test(path) || /^\.github\/PULL_REQUEST_TEMPLATE\/.+\.md$/i.test(path);
|
|
1616
|
+
}
|
|
1617
|
+
function isCodeowners(path) {
|
|
1618
|
+
return /(^|\/)CODEOWNERS$/.test(path);
|
|
1619
|
+
}
|
|
1620
|
+
function isDependabot(path) {
|
|
1621
|
+
return /^\.github\/dependabot\.ya?ml$/i.test(path);
|
|
1622
|
+
}
|
|
1623
|
+
function isFunding(path) {
|
|
1624
|
+
return /^\.github\/FUNDING\.ya?ml$/i.test(path);
|
|
1625
|
+
}
|
|
1626
|
+
function structuralConnection(to, description) {
|
|
1627
|
+
return { to, type: "contains", relation: "structural", description, source: "inferred", weight: 5 };
|
|
1628
|
+
}
|
|
1629
|
+
function buildStructuralNode(args) {
|
|
1630
|
+
return {
|
|
1631
|
+
id: args.id,
|
|
1632
|
+
title: args.title,
|
|
1633
|
+
cluster: STRUCTURAL_CLUSTER,
|
|
1634
|
+
content: args.content ?? "",
|
|
1635
|
+
rawContent: args.rawContent ?? "",
|
|
1636
|
+
emoji: args.emoji,
|
|
1637
|
+
...args.display !== void 0 ? { display: args.display } : {},
|
|
1638
|
+
connections: [structuralConnection(args.repoNodeId, `${args.title} configures the repository`)],
|
|
1639
|
+
identity: args.identity,
|
|
1640
|
+
derived: true,
|
|
1641
|
+
source: args.source,
|
|
1642
|
+
provider: "structural",
|
|
1643
|
+
entityType: args.entityType,
|
|
1644
|
+
...args.data !== void 0 ? { data: args.data } : {},
|
|
1645
|
+
jsonld: buildJsonLd({ id: args.id, identity: args.identity }, args.ldType, args.ldProps ?? {})
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function parseFrontmatter(raw) {
|
|
1649
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
1650
|
+
if (!match) return { data: {}, body: raw };
|
|
1651
|
+
try {
|
|
1652
|
+
const data = yaml.parse(match[1] ?? "");
|
|
1653
|
+
return { data: data ?? {}, body: match[2] ?? "" };
|
|
1654
|
+
} catch {
|
|
1655
|
+
return { data: {}, body: raw };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function safeYaml(content) {
|
|
1659
|
+
try {
|
|
1660
|
+
const parsed = yaml.parse(content);
|
|
1661
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1662
|
+
} catch {
|
|
1663
|
+
return {};
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function parseCodeowners(content) {
|
|
1667
|
+
const rules = [];
|
|
1668
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1669
|
+
const trimmed = line.trim();
|
|
1670
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1671
|
+
const parts = trimmed.split(/\s+/);
|
|
1672
|
+
const pattern = parts.shift();
|
|
1673
|
+
if (!pattern) continue;
|
|
1674
|
+
rules.push({ pattern, owners: parts });
|
|
1675
|
+
}
|
|
1676
|
+
return rules;
|
|
1677
|
+
}
|
|
1678
|
+
function buildWorkflowNode(path, content, repoNodeId) {
|
|
1679
|
+
const data = safeYaml(content);
|
|
1680
|
+
const name = typeof data.name === "string" && data.name || fileName2(path);
|
|
1681
|
+
return buildStructuralNode({
|
|
1682
|
+
id: `gh-workflow-${slugify(fileName2(path))}`,
|
|
1683
|
+
title: name,
|
|
1684
|
+
entityType: "workflow",
|
|
1685
|
+
ldType: "Workflow",
|
|
1686
|
+
source: { type: "workflow", path },
|
|
1687
|
+
identity: urnIdentity("structural", path),
|
|
1688
|
+
emoji: "Flow",
|
|
1689
|
+
data,
|
|
1690
|
+
ldProps: { name },
|
|
1691
|
+
display: "entity",
|
|
1692
|
+
repoNodeId
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
function buildActionNode(path, content, repoNodeId) {
|
|
1696
|
+
const data = safeYaml(content);
|
|
1697
|
+
const name = typeof data.name === "string" && data.name || fileName2(path);
|
|
1698
|
+
return buildStructuralNode({
|
|
1699
|
+
id: `gh-action-${slugify(path)}`,
|
|
1700
|
+
title: name,
|
|
1701
|
+
entityType: "github-action",
|
|
1702
|
+
ldType: "SoftwareApplication",
|
|
1703
|
+
source: { type: "structured", entityType: "github-action", ref: path },
|
|
1704
|
+
identity: urnIdentity("structural", path),
|
|
1705
|
+
emoji: "PuzzlePiece",
|
|
1706
|
+
data,
|
|
1707
|
+
ldProps: { name },
|
|
1708
|
+
display: "entity",
|
|
1709
|
+
repoNodeId
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
function buildSkillNode(path, content, repoNodeId) {
|
|
1713
|
+
const { data, body } = parseFrontmatter(content);
|
|
1714
|
+
const defaultName = /\/SKILL\.md$/i.test(path) ? path.split("/").slice(-2, -1)[0] ?? fileName2(path) : fileName2(path).replace(/\.skill\.md$/i, "");
|
|
1715
|
+
const name = typeof data.name === "string" && data.name || defaultName;
|
|
1716
|
+
const html = renderSafeMarkdown(body);
|
|
1717
|
+
const ldProps = { name };
|
|
1718
|
+
if (typeof data.version === "string") ldProps.version = data.version;
|
|
1719
|
+
return buildStructuralNode({
|
|
1720
|
+
id: `gh-skill-${slugify(name)}`,
|
|
1721
|
+
title: name,
|
|
1722
|
+
entityType: "skill",
|
|
1723
|
+
ldType: "HowTo",
|
|
1724
|
+
source: { type: "structured", entityType: "skill", ref: path },
|
|
1725
|
+
identity: urnIdentity("structural", path),
|
|
1726
|
+
emoji: "BookOpenLightbulb",
|
|
1727
|
+
data,
|
|
1728
|
+
ldProps,
|
|
1729
|
+
content: html,
|
|
1730
|
+
rawContent: body,
|
|
1731
|
+
display: "entity",
|
|
1732
|
+
repoNodeId
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
function buildMarkdownTemplateNode(path, content, entityType, ldType, emoji, repoNodeId) {
|
|
1736
|
+
const { data, body } = parseFrontmatter(content);
|
|
1737
|
+
const title = typeof data.name === "string" && data.name || typeof data.about === "string" && data.about || fileName2(path);
|
|
1738
|
+
const hasData = Object.keys(data).length > 0;
|
|
1739
|
+
const html = renderSafeMarkdown(body);
|
|
1740
|
+
return buildStructuralNode({
|
|
1741
|
+
id: `gh-${entityType}-${slugify(fileName2(path))}`,
|
|
1742
|
+
title,
|
|
1743
|
+
entityType,
|
|
1744
|
+
ldType,
|
|
1745
|
+
source: { type: "structured", entityType, ref: path },
|
|
1746
|
+
identity: urnIdentity("structural", path),
|
|
1747
|
+
emoji,
|
|
1748
|
+
data: hasData ? data : void 0,
|
|
1749
|
+
ldProps: typeof data.name === "string" ? { name: data.name } : {},
|
|
1750
|
+
content: html,
|
|
1751
|
+
rawContent: body,
|
|
1752
|
+
display: hasData ? "entity" : void 0,
|
|
1753
|
+
repoNodeId
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
function buildYamlFormNode(path, content, repoNodeId) {
|
|
1757
|
+
const data = safeYaml(content);
|
|
1758
|
+
const name = typeof data.name === "string" && data.name || fileName2(path);
|
|
1759
|
+
return buildStructuralNode({
|
|
1760
|
+
id: `gh-issue-template-${slugify(fileName2(path))}`,
|
|
1761
|
+
title: name,
|
|
1762
|
+
entityType: "issue-template",
|
|
1763
|
+
ldType: "CreativeWork",
|
|
1764
|
+
source: { type: "structured", entityType: "issue-template", ref: path },
|
|
1765
|
+
identity: urnIdentity("structural", path),
|
|
1766
|
+
emoji: "TextBulletListSquare",
|
|
1767
|
+
data,
|
|
1768
|
+
ldProps: { name },
|
|
1769
|
+
display: "entity",
|
|
1770
|
+
repoNodeId
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
function buildCodeownersNode(path, content, repoNodeId) {
|
|
1774
|
+
const rules = parseCodeowners(content);
|
|
1775
|
+
return buildStructuralNode({
|
|
1776
|
+
id: `gh-codeowners-${slugify(path)}`,
|
|
1777
|
+
title: "CODEOWNERS",
|
|
1778
|
+
entityType: "codeowners",
|
|
1779
|
+
ldType: "StructuredConfig",
|
|
1780
|
+
source: { type: "structured", entityType: "codeowners", ref: path },
|
|
1781
|
+
identity: urnIdentity("structural", path),
|
|
1782
|
+
emoji: "People",
|
|
1783
|
+
data: { rules },
|
|
1784
|
+
ldProps: { ruleCount: rules.length },
|
|
1785
|
+
display: "entity",
|
|
1786
|
+
repoNodeId
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
function buildDependabotNode(path, content, repoNodeId) {
|
|
1790
|
+
const data = safeYaml(content);
|
|
1791
|
+
return buildStructuralNode({
|
|
1792
|
+
id: `gh-dependabot-${slugify(fileName2(path))}`,
|
|
1793
|
+
title: "Dependabot",
|
|
1794
|
+
entityType: "dependabot-config",
|
|
1795
|
+
ldType: "DependabotConfig",
|
|
1796
|
+
source: { type: "structured", entityType: "dependabot-config", ref: path },
|
|
1797
|
+
identity: urnIdentity("structural", path),
|
|
1798
|
+
emoji: "ArrowSync",
|
|
1799
|
+
data,
|
|
1800
|
+
ldProps: typeof data.version !== "undefined" ? { schemaVersion: data.version } : {},
|
|
1801
|
+
display: "entity",
|
|
1802
|
+
repoNodeId
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
function buildFundingNode(path, content, repoNodeId) {
|
|
1806
|
+
const data = safeYaml(content);
|
|
1807
|
+
return buildStructuralNode({
|
|
1808
|
+
id: `gh-funding-${slugify(fileName2(path))}`,
|
|
1809
|
+
title: "Funding",
|
|
1810
|
+
entityType: "funding-config",
|
|
1811
|
+
ldType: "StructuredConfig",
|
|
1812
|
+
source: { type: "structured", entityType: "funding-config", ref: path },
|
|
1813
|
+
identity: urnIdentity("structural", path),
|
|
1814
|
+
emoji: "Heart",
|
|
1815
|
+
data,
|
|
1816
|
+
display: "entity",
|
|
1817
|
+
repoNodeId
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
function isMarkdown(path) {
|
|
1821
|
+
return /\.m*md$/i.test(path) || /\.markdown$/i.test(path);
|
|
1822
|
+
}
|
|
1823
|
+
function buildGenericConfigNode(path, content, map, repoNodeId) {
|
|
1824
|
+
const mapped = applyStructuredNodeMap({ path, content }, map, {
|
|
1825
|
+
id: `gh-config-${slugify(path)}`,
|
|
1826
|
+
cluster: STRUCTURAL_CLUSTER
|
|
1827
|
+
});
|
|
1828
|
+
if (mapped) {
|
|
1829
|
+
mapped.provider = "structural";
|
|
1830
|
+
mapped.derived = true;
|
|
1831
|
+
if (!mapped.connections.some((c) => c.to === repoNodeId)) {
|
|
1832
|
+
mapped.connections.push(structuralConnection(repoNodeId, `${mapped.title} configures the repository`));
|
|
1833
|
+
}
|
|
1834
|
+
return mapped;
|
|
1835
|
+
}
|
|
1836
|
+
if (isMarkdown(path)) {
|
|
1837
|
+
const { data, body } = parseFrontmatter(content);
|
|
1838
|
+
const html = renderSafeMarkdown(body);
|
|
1839
|
+
const hasData = Object.keys(data).length > 0;
|
|
1840
|
+
return buildStructuralNode({
|
|
1841
|
+
id: `gh-doc-${slugify(fileName2(path))}`,
|
|
1842
|
+
title: typeof data.name === "string" && data.name || fileName2(path),
|
|
1843
|
+
entityType: "github-config",
|
|
1844
|
+
ldType: "CreativeWork",
|
|
1845
|
+
source: { type: "structured", entityType: "github-config", ref: path },
|
|
1846
|
+
identity: urnIdentity("structural", path),
|
|
1847
|
+
emoji: "Document",
|
|
1848
|
+
data: hasData ? data : void 0,
|
|
1849
|
+
content: html,
|
|
1850
|
+
rawContent: body,
|
|
1851
|
+
display: hasData ? "entity" : void 0,
|
|
1852
|
+
repoNodeId
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
function buildStructuralFileNode(path, content, map, repoNodeId = REPO_NODE_ID) {
|
|
1858
|
+
if (typeof content !== "string") return null;
|
|
1859
|
+
if (isWorkflow(path)) return buildWorkflowNode(path, content, repoNodeId);
|
|
1860
|
+
if (isAction(path)) return buildActionNode(path, content, repoNodeId);
|
|
1861
|
+
if (isSkill(path)) return buildSkillNode(path, content, repoNodeId);
|
|
1862
|
+
if (isDependabot(path)) return buildDependabotNode(path, content, repoNodeId);
|
|
1863
|
+
if (isFunding(path)) return buildFundingNode(path, content, repoNodeId);
|
|
1864
|
+
if (isCodeowners(path)) return buildCodeownersNode(path, content, repoNodeId);
|
|
1865
|
+
if (isIssueTemplateConfig(path)) return buildYamlFormNode(path, content, repoNodeId);
|
|
1866
|
+
if (isIssueTemplate(path)) {
|
|
1867
|
+
return isMarkdown(path) ? buildMarkdownTemplateNode(path, content, "issue-template", "CreativeWork", "TextBulletListSquare", repoNodeId) : buildYamlFormNode(path, content, repoNodeId);
|
|
1868
|
+
}
|
|
1869
|
+
if (isPrTemplate(path)) {
|
|
1870
|
+
return buildMarkdownTemplateNode(path, content, "pr-template", "CreativeWork", "BranchRequest", repoNodeId);
|
|
1871
|
+
}
|
|
1872
|
+
return buildGenericConfigNode(path, content, map, repoNodeId);
|
|
1873
|
+
}
|
|
1874
|
+
var StructuralProvider = class {
|
|
1875
|
+
id = "structural";
|
|
1876
|
+
name = "Repo Structure";
|
|
1877
|
+
dependencies = ["work"];
|
|
1878
|
+
structuralFiles;
|
|
1879
|
+
structuredNodeMapRaw;
|
|
1880
|
+
repoNodeId;
|
|
1881
|
+
constructor(structuralFiles = {}, structuredNodeMapRaw = null, repoNodeId = REPO_NODE_ID) {
|
|
1882
|
+
this.structuralFiles = structuralFiles;
|
|
1883
|
+
this.structuredNodeMapRaw = structuredNodeMapRaw;
|
|
1884
|
+
this.repoNodeId = repoNodeId;
|
|
1885
|
+
}
|
|
1886
|
+
async resolve(_config, _existingNodes) {
|
|
1887
|
+
const entries = Object.entries(this.structuralFiles ?? {});
|
|
1888
|
+
if (entries.length === 0) return { nodes: [], edges: [] };
|
|
1889
|
+
registerStructuralTypes();
|
|
1890
|
+
const map = parseStructuredNodeMap(this.structuredNodeMapRaw);
|
|
1891
|
+
const nodes = [];
|
|
1892
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1893
|
+
for (const [path, content] of entries.sort(([a], [b]) => a.localeCompare(b))) {
|
|
1894
|
+
const node = buildStructuralFileNode(path, content, map, this.repoNodeId);
|
|
1895
|
+
if (!node) continue;
|
|
1896
|
+
let id = node.id;
|
|
1897
|
+
let n = 2;
|
|
1898
|
+
while (seen.has(id)) id = `${node.id}-${n++}`;
|
|
1899
|
+
node.id = id;
|
|
1900
|
+
seen.add(id);
|
|
1901
|
+
nodes.push(node);
|
|
1902
|
+
}
|
|
1903
|
+
return { nodes, edges: [] };
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
// src/providers/work-provider.ts
|
|
1908
|
+
var DATE_FORMAT = { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" };
|
|
1909
|
+
var SHORT_DATE_FORMAT = { month: "short", day: "numeric", timeZone: "UTC" };
|
|
1910
|
+
var WorkProvider = class {
|
|
1911
|
+
id = "work";
|
|
1912
|
+
name = "Work Items";
|
|
1913
|
+
dependencies = [];
|
|
1914
|
+
issues;
|
|
1915
|
+
pullRequests;
|
|
1916
|
+
commits;
|
|
1917
|
+
branches;
|
|
1918
|
+
repoMetadata;
|
|
1919
|
+
releases;
|
|
1920
|
+
constructor(issues, pullRequests, commits, branches = [], repoMetadata = null, releases = []) {
|
|
1921
|
+
this.issues = issues;
|
|
1922
|
+
this.pullRequests = pullRequests;
|
|
1923
|
+
this.commits = commits;
|
|
1924
|
+
this.branches = branches;
|
|
1925
|
+
this.repoMetadata = repoMetadata;
|
|
1926
|
+
this.releases = releases;
|
|
1927
|
+
}
|
|
1928
|
+
async resolve(_config, _existingNodes) {
|
|
1929
|
+
const nodes = [];
|
|
1930
|
+
const knownIssueNumbers = new Set(this.issues.map((i) => i.number));
|
|
1931
|
+
const knownPrNumbers = new Set(this.pullRequests.map((p) => p.number));
|
|
1932
|
+
const repoNodeId = this.repoMetadata ? "repo-meta" : void 0;
|
|
1933
|
+
for (const issue of this.issues) {
|
|
1934
|
+
const node = issueToNode(issue, {
|
|
1935
|
+
knownIssueNumbers,
|
|
1936
|
+
knownPrNumbers,
|
|
1937
|
+
...repoNodeId !== void 0 ? { repoNodeId } : {}
|
|
1938
|
+
});
|
|
1939
|
+
node.provider = "work";
|
|
1940
|
+
const identity = assignIdentity(node);
|
|
1941
|
+
if (identity !== void 0) node.identity = identity;
|
|
1942
|
+
nodes.push(node);
|
|
1943
|
+
}
|
|
1944
|
+
for (const pr of this.pullRequests) {
|
|
1945
|
+
const body = pr.body ?? "";
|
|
1946
|
+
const remappedBody = body.replace(/https?:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/g, (_m, num) => `issue-${num}`).replace(/https?:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/g, (_m, num) => `pr-${num}`);
|
|
1947
|
+
const refs = extractIssueRefs(body);
|
|
1948
|
+
const stateEmoji = pr.state === "open" ? "\u{1F7E2}" : pr.state === "merged" ? "\u{1F7E3}" : "\u{1F534}";
|
|
1949
|
+
const labelBadges = pr.labels?.map((l) => `\`${l.name}\``).join(" ") ?? "";
|
|
1950
|
+
const created = new Date(pr.created_at).toLocaleDateString("en-US", DATE_FORMAT);
|
|
1951
|
+
const updated = new Date(pr.updated_at).toLocaleDateString("en-US", DATE_FORMAT);
|
|
1952
|
+
const metaLines = [
|
|
1953
|
+
`${stateEmoji} **${(pr.state || "closed").toUpperCase()}** \xB7 PR #${pr.number}`,
|
|
1954
|
+
labelBadges ? `Labels: ${labelBadges}` : "",
|
|
1955
|
+
`Created: ${created} \xB7 Updated: ${updated}`,
|
|
1956
|
+
`[View on GitHub \u2197](${pr.html_url})`
|
|
1957
|
+
].filter(Boolean).join("\n\n");
|
|
1958
|
+
const fullContent = `${metaLines}
|
|
1959
|
+
|
|
1960
|
+
---
|
|
1961
|
+
|
|
1962
|
+
${remappedBody}`;
|
|
1963
|
+
const html = renderSafeMarkdown(fullContent);
|
|
1964
|
+
const connections = [];
|
|
1965
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1966
|
+
for (const n of refs) {
|
|
1967
|
+
if (n === pr.number) continue;
|
|
1968
|
+
if (knownIssueNumbers.has(n)) {
|
|
1969
|
+
const to = `issue-${n}`;
|
|
1970
|
+
if (!seen.has(to)) {
|
|
1971
|
+
connections.push({ to, description: `References #${n}` });
|
|
1972
|
+
seen.add(to);
|
|
1973
|
+
}
|
|
1974
|
+
} else if (knownPrNumbers.has(n)) {
|
|
1975
|
+
const to = `pr-${n}`;
|
|
1976
|
+
if (!seen.has(to)) {
|
|
1977
|
+
connections.push({ to, description: `References #${n}` });
|
|
1978
|
+
seen.add(to);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (pr.head_branch) {
|
|
1983
|
+
connections.push({ to: `branch-${pr.head_branch}`, description: `Branch: ${pr.head_branch}` });
|
|
1984
|
+
}
|
|
1985
|
+
if (repoNodeId) {
|
|
1986
|
+
connections.push({
|
|
1987
|
+
to: repoNodeId,
|
|
1988
|
+
type: "contains",
|
|
1989
|
+
relation: "tracked-in",
|
|
1990
|
+
description: "Tracked in repository",
|
|
1991
|
+
source: "inferred"
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
const prNode = {
|
|
1995
|
+
id: `pr-${pr.number}`,
|
|
1996
|
+
title: pr.title,
|
|
1997
|
+
cluster: "work",
|
|
1998
|
+
content: html,
|
|
1999
|
+
rawContent: fullContent,
|
|
2000
|
+
emoji: "BranchFork",
|
|
2001
|
+
connections,
|
|
2002
|
+
source: { type: "pull_request", number: pr.number, state: pr.state },
|
|
2003
|
+
provider: "work"
|
|
2004
|
+
};
|
|
2005
|
+
if (repoNodeId) prNode.parent = repoNodeId;
|
|
2006
|
+
const prIdentity = assignIdentity(prNode);
|
|
2007
|
+
if (prIdentity !== void 0) prNode.identity = prIdentity;
|
|
2008
|
+
nodes.push(prNode);
|
|
2009
|
+
}
|
|
2010
|
+
if (this.commits.length > 0) {
|
|
2011
|
+
const commitList = this.commits.slice(0, 30).map((c) => {
|
|
2012
|
+
const msg = c.commit.message.split("\n")[0];
|
|
2013
|
+
const date = new Date(c.commit.author.date).toLocaleDateString("en-US", SHORT_DATE_FORMAT);
|
|
2014
|
+
const refs = extractIssueRefs(c.commit.message);
|
|
2015
|
+
const refLinks = refs.map((n) => `[#${n}](issue-${n})`).join(" ");
|
|
2016
|
+
return `- \`${c.sha.substring(0, 7)}\` ${msg}${refLinks ? " \u2014 " + refLinks : ""} *(${date})*`;
|
|
2017
|
+
}).join("\n");
|
|
2018
|
+
const commitConnections = [];
|
|
2019
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
2020
|
+
for (const c of this.commits) {
|
|
2021
|
+
for (const n of extractIssueRefs(c.commit.message)) {
|
|
2022
|
+
if (knownIssueNumbers.has(n)) {
|
|
2023
|
+
const to = `issue-${n}`;
|
|
2024
|
+
if (!seenRefs.has(to)) {
|
|
2025
|
+
commitConnections.push({ to, description: `Commit references #${n}` });
|
|
2026
|
+
seenRefs.add(to);
|
|
2027
|
+
}
|
|
2028
|
+
} else if (knownPrNumbers.has(n)) {
|
|
2029
|
+
const to = `pr-${n}`;
|
|
2030
|
+
if (!seenRefs.has(to)) {
|
|
2031
|
+
commitConnections.push({ to, description: `Commit references #${n}` });
|
|
2032
|
+
seenRefs.add(to);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (repoNodeId) {
|
|
2038
|
+
commitConnections.push({
|
|
2039
|
+
to: repoNodeId,
|
|
2040
|
+
type: "contains",
|
|
2041
|
+
relation: "tracked-in",
|
|
2042
|
+
description: "Commits in repository",
|
|
2043
|
+
source: "inferred"
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
const commitContent = `## Recent Commits
|
|
2047
|
+
|
|
2048
|
+
${this.commits.length} commits \xB7 ${this.commits[0]?.commit.author.name ?? "unknown"}
|
|
2049
|
+
|
|
2050
|
+
${commitList}`;
|
|
2051
|
+
const commitHtml = renderSafeMarkdown(commitContent);
|
|
2052
|
+
nodes.push({
|
|
2053
|
+
id: "commits",
|
|
2054
|
+
title: "Recent Commits",
|
|
2055
|
+
cluster: "work",
|
|
2056
|
+
content: commitHtml,
|
|
2057
|
+
rawContent: commitContent,
|
|
2058
|
+
emoji: "History",
|
|
2059
|
+
connections: commitConnections,
|
|
2060
|
+
source: { type: "commit", sha: "summary" },
|
|
2061
|
+
provider: "work",
|
|
2062
|
+
...repoNodeId ? { parent: repoNodeId } : {}
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
if (this.repoMetadata) {
|
|
2066
|
+
const meta = this.repoMetadata;
|
|
2067
|
+
const langList = meta.languages.sort((a, b) => b.size - a.size).slice(0, 10).map((l) => `- **${l.name}** (${Math.round(l.size / 1024)}KB)`).join("\n");
|
|
2068
|
+
const topicBadges = meta.topics.map((t) => `\`${t}\``).join(" ");
|
|
2069
|
+
const repoContent = [
|
|
2070
|
+
`${meta.private ? "\u{1F512} Private" : "\u{1F310} Public"} \xB7 \u2B50 ${meta.stargazers_count} \xB7 \u{1F374} ${meta.forks_count}`,
|
|
2071
|
+
meta.description ? `
|
|
2072
|
+
${meta.description}` : "",
|
|
2073
|
+
topicBadges ? `
|
|
2074
|
+
|
|
2075
|
+
Topics: ${topicBadges}` : "",
|
|
2076
|
+
`
|
|
2077
|
+
|
|
2078
|
+
## Languages
|
|
2079
|
+
|
|
2080
|
+
${langList || "No language data"}`,
|
|
2081
|
+
`
|
|
2082
|
+
|
|
2083
|
+
Default branch: \`${meta.default_branch}\``,
|
|
2084
|
+
`
|
|
2085
|
+
|
|
2086
|
+
[View on GitHub \u2197](${meta.html_url})`
|
|
2087
|
+
].join("");
|
|
2088
|
+
const repoHtml = renderSafeMarkdown(repoContent);
|
|
2089
|
+
const repoConns = [
|
|
2090
|
+
{ to: "readme", description: "README" },
|
|
2091
|
+
{ to: `branch-${meta.default_branch}`, description: `Default branch` },
|
|
2092
|
+
// Tie the GitHub-side repo node to the file-tree root so the source
|
|
2093
|
+
// tree and the repository are one navigable cluster (the two nodes
|
|
2094
|
+
// describe the same repo; without this they float independently).
|
|
2095
|
+
{ to: "repo-root", description: "Source tree", type: "contains", source: "inferred" }
|
|
2096
|
+
];
|
|
2097
|
+
nodes.push({
|
|
2098
|
+
id: "repo-meta",
|
|
2099
|
+
title: meta.name,
|
|
2100
|
+
cluster: "infra",
|
|
2101
|
+
content: repoHtml,
|
|
2102
|
+
rawContent: repoContent,
|
|
2103
|
+
emoji: "Organization",
|
|
2104
|
+
display: "repository",
|
|
2105
|
+
...meta.owner.avatar_url ? { image: meta.owner.avatar_url } : {},
|
|
2106
|
+
connections: repoConns,
|
|
2107
|
+
source: { type: "repository", owner: meta.owner.login, repo: meta.name },
|
|
2108
|
+
provider: "work"
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
for (const branch of this.branches) {
|
|
2112
|
+
const protectedBadge = branch.protected ? "\u{1F6E1}\uFE0F Protected" : "";
|
|
2113
|
+
const isDefault = this.repoMetadata?.default_branch === branch.name;
|
|
2114
|
+
const branchContent = [
|
|
2115
|
+
`${isDefault ? "**Default branch**" : "Branch"} \xB7 \`${branch.name}\``,
|
|
2116
|
+
protectedBadge ? ` \xB7 ${protectedBadge}` : ""
|
|
2117
|
+
].join("");
|
|
2118
|
+
const branchHtml = renderSafeMarkdown(branchContent);
|
|
2119
|
+
const branchConns = [];
|
|
2120
|
+
if (repoNodeId) {
|
|
2121
|
+
branchConns.push({
|
|
2122
|
+
to: repoNodeId,
|
|
2123
|
+
type: "contains",
|
|
2124
|
+
relation: "tracked-in",
|
|
2125
|
+
description: isDefault ? "Default branch of repository" : "Branch in repository",
|
|
2126
|
+
source: "inferred"
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
nodes.push({
|
|
2130
|
+
id: `branch-${branch.name}`,
|
|
2131
|
+
title: branch.name,
|
|
2132
|
+
// Branches are a structural property of the repository — keep them in
|
|
2133
|
+
// the same cluster so the legend doesn't split branch-isms (the old
|
|
2134
|
+
// code put default in `infra` and the rest in `pull-request` which was
|
|
2135
|
+
// visually inconsistent).
|
|
2136
|
+
cluster: "infra",
|
|
2137
|
+
content: branchHtml,
|
|
2138
|
+
rawContent: branchContent,
|
|
2139
|
+
emoji: branch.protected ? "ShieldCheckmark" : "Branch",
|
|
2140
|
+
connections: branchConns,
|
|
2141
|
+
source: { type: "branch", name: branch.name, protected: branch.protected },
|
|
2142
|
+
provider: "work",
|
|
2143
|
+
...repoNodeId ? { parent: repoNodeId } : {}
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
for (const release of this.releases) {
|
|
2147
|
+
const tag = release.tag_name;
|
|
2148
|
+
const body = release.body ?? "";
|
|
2149
|
+
const prereleaseBadge = release.prerelease ? "\u{1F536} **PRE-RELEASE**" : "\u{1F7E2} **RELEASE**";
|
|
2150
|
+
const pubDate = release.published_at ? new Date(release.published_at).toLocaleDateString("en-US", DATE_FORMAT) : "";
|
|
2151
|
+
const metaLines = [
|
|
2152
|
+
`${prereleaseBadge} \xB7 \`${tag}\``,
|
|
2153
|
+
pubDate ? `Published: ${pubDate}` : "",
|
|
2154
|
+
`[View on GitHub \u2197](${release.html_url})`
|
|
2155
|
+
].filter(Boolean).join("\n\n");
|
|
2156
|
+
const fullContent = `${metaLines}
|
|
2157
|
+
|
|
2158
|
+
---
|
|
2159
|
+
|
|
2160
|
+
${body}`;
|
|
2161
|
+
const html = renderSafeMarkdown(fullContent);
|
|
2162
|
+
const connections = [];
|
|
2163
|
+
if (this.repoMetadata) {
|
|
2164
|
+
connections.push({ to: "repo-meta", description: "Repository", type: "contains" });
|
|
2165
|
+
}
|
|
2166
|
+
const refs = extractIssueRefs(body);
|
|
2167
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
2168
|
+
for (const n of refs) {
|
|
2169
|
+
if (knownPrNumbers.has(n)) {
|
|
2170
|
+
const prTo = `pr-${n}`;
|
|
2171
|
+
if (!seenRefs.has(prTo)) {
|
|
2172
|
+
connections.push({ to: prTo, description: `Ships PR #${n}`, type: "ships" });
|
|
2173
|
+
seenRefs.add(prTo);
|
|
2174
|
+
}
|
|
2175
|
+
} else if (knownIssueNumbers.has(n)) {
|
|
2176
|
+
const issueTo = `issue-${n}`;
|
|
2177
|
+
if (!seenRefs.has(issueTo)) {
|
|
2178
|
+
connections.push({ to: issueTo, description: `Closes #${n}`, type: "closes" });
|
|
2179
|
+
seenRefs.add(issueTo);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
const releaseNode = {
|
|
2184
|
+
id: `release-${tag}`,
|
|
2185
|
+
title: release.name || tag,
|
|
2186
|
+
cluster: "work",
|
|
2187
|
+
content: html,
|
|
2188
|
+
rawContent: fullContent,
|
|
2189
|
+
emoji: release.prerelease ? "Beaker" : "Rocket",
|
|
2190
|
+
connections,
|
|
2191
|
+
source: { type: "release", tag, prerelease: release.prerelease },
|
|
2192
|
+
provider: "work",
|
|
2193
|
+
data: {
|
|
2194
|
+
tag_name: tag,
|
|
2195
|
+
html_url: release.html_url,
|
|
2196
|
+
published_at: release.published_at,
|
|
2197
|
+
prerelease: release.prerelease
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
const releaseIdentity = assignIdentity(releaseNode);
|
|
2201
|
+
if (releaseIdentity !== void 0) releaseNode.identity = releaseIdentity;
|
|
2202
|
+
nodes.push(releaseNode);
|
|
2203
|
+
}
|
|
2204
|
+
return { nodes, edges: [] };
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
|
|
2208
|
+
// src/orchestrator.ts
|
|
2209
|
+
async function collectProviderNodes(registry, config) {
|
|
2210
|
+
const providers = registry.getExecutionOrder();
|
|
2211
|
+
const allNodes = [];
|
|
2212
|
+
for (const provider of providers) {
|
|
2213
|
+
const result = await provider.resolve(config, allNodes);
|
|
2214
|
+
allNodes.push(...result.nodes);
|
|
2215
|
+
}
|
|
2216
|
+
return allNodes;
|
|
2217
|
+
}
|
|
2218
|
+
async function orchestrate(registry, config) {
|
|
2219
|
+
const allNodes = await collectProviderNodes(registry, config);
|
|
2220
|
+
const clusters = extractClusters(allNodes, config);
|
|
2221
|
+
return buildGraph(allNodes, clusters);
|
|
2222
|
+
}
|
|
2223
|
+
async function orchestrateWithTransforms(registry, config, ctx, transforms = DEFAULT_TRANSFORMS) {
|
|
2224
|
+
const allNodes = await collectProviderNodes(registry, config);
|
|
2225
|
+
const transformed = applyTransforms(allNodes, ctx, transforms);
|
|
2226
|
+
const clusters = extractClusters(transformed, config);
|
|
2227
|
+
return buildGraph(transformed, clusters);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/loader.ts
|
|
2231
|
+
function registerProviders(registry, data) {
|
|
2232
|
+
if (data.tree.length > 0) {
|
|
2233
|
+
registry.register(new FilesProvider(data.tree, data.repo));
|
|
2234
|
+
}
|
|
2235
|
+
if (Object.keys(data.authoredContent).length > 0 || data.nodemapRaw) {
|
|
2236
|
+
registry.register(new AuthoredProvider(
|
|
2237
|
+
data.authoredContent,
|
|
2238
|
+
data.nodemapRaw,
|
|
2239
|
+
data.nodemapFiles,
|
|
2240
|
+
data.nodemapDirs,
|
|
2241
|
+
data.listFiles
|
|
2242
|
+
));
|
|
2243
|
+
}
|
|
2244
|
+
if (Object.keys(data.authoredContent).length > 0) {
|
|
2245
|
+
registry.register(new AuthoredRichMarkdownProvider(data.authoredContent));
|
|
2246
|
+
}
|
|
2247
|
+
const workPRs = data.pullRequests.map((pr) => ({
|
|
2248
|
+
number: pr.number,
|
|
2249
|
+
title: pr.title,
|
|
2250
|
+
body: pr.body,
|
|
2251
|
+
state: pr.state,
|
|
2252
|
+
labels: pr.labels,
|
|
2253
|
+
html_url: pr.html_url,
|
|
2254
|
+
created_at: pr.created_at,
|
|
2255
|
+
updated_at: pr.updated_at,
|
|
2256
|
+
...pr.head_branch !== void 0 ? { head_branch: pr.head_branch } : {},
|
|
2257
|
+
...pr.user !== void 0 ? { user: pr.user } : {}
|
|
2258
|
+
}));
|
|
2259
|
+
registry.register(new WorkProvider(
|
|
2260
|
+
data.issues,
|
|
2261
|
+
workPRs,
|
|
2262
|
+
data.commits,
|
|
2263
|
+
data.branches,
|
|
2264
|
+
data.repoMetadata,
|
|
2265
|
+
data.releases
|
|
2266
|
+
));
|
|
2267
|
+
registry.register(new PersonProvider(
|
|
2268
|
+
data.issues,
|
|
2269
|
+
data.pullRequests.map((pr) => ({
|
|
2270
|
+
number: pr.number,
|
|
2271
|
+
title: pr.title,
|
|
2272
|
+
state: pr.state,
|
|
2273
|
+
html_url: pr.html_url,
|
|
2274
|
+
...pr.user !== void 0 ? { user: pr.user } : {},
|
|
2275
|
+
...pr.assignees !== void 0 ? { assignees: pr.assignees } : {}
|
|
2276
|
+
}))
|
|
2277
|
+
));
|
|
2278
|
+
if (Object.keys(data.structuralFiles).length > 0) {
|
|
2279
|
+
registry.register(new StructuralProvider(data.structuralFiles, data.structuredNodeMapRaw));
|
|
2280
|
+
}
|
|
2281
|
+
registry.register(new ContentModelProvider(data.contentModel));
|
|
2282
|
+
}
|
|
2283
|
+
async function loadKnowledgeBase(arg1, arg2, arg3, arg4) {
|
|
2284
|
+
if (isRepoSource(arg1)) {
|
|
2285
|
+
return loadFromSource(arg1, arg2, arg3, arg4);
|
|
2286
|
+
}
|
|
2287
|
+
const config = arg1;
|
|
2288
|
+
const options = arg2 ?? {};
|
|
2289
|
+
const source = options.source ?? await defaultSourceFor(config);
|
|
2290
|
+
const positionalOptions = {};
|
|
2291
|
+
if (options.importBaseUrl !== void 0) positionalOptions.importBaseUrl = options.importBaseUrl;
|
|
2292
|
+
if (options.graphStore !== void 0) positionalOptions.graphStore = options.graphStore;
|
|
2293
|
+
const { graph } = await loadFromSource(source, config, options.env, positionalOptions);
|
|
2294
|
+
return graph;
|
|
2295
|
+
}
|
|
2296
|
+
function isRepoSource(value) {
|
|
2297
|
+
return typeof value.getRepoData === "function";
|
|
2298
|
+
}
|
|
2299
|
+
async function defaultSourceFor(config) {
|
|
2300
|
+
const { GitHubApiSource } = await import('./github-api-source-KV5HZARH.js');
|
|
2301
|
+
return new GitHubApiSource(config.source);
|
|
2302
|
+
}
|
|
2303
|
+
async function loadFromSource(source, config, env, options) {
|
|
2304
|
+
const data = await source.getRepoData();
|
|
2305
|
+
const registry = new ProviderRegistry();
|
|
2306
|
+
registerProviders(registry, data);
|
|
2307
|
+
if (config.providers && config.providers.length > 0) {
|
|
2308
|
+
const { loadExternalProviders: loadExternalProviders2 } = await import('./plugin-loader-K4VU6ODV.js');
|
|
2309
|
+
const externals = await loadExternalProviders2(
|
|
2310
|
+
config.providers,
|
|
2311
|
+
options?.importBaseUrl !== void 0 ? { importBaseUrl: options.importBaseUrl } : void 0
|
|
2312
|
+
);
|
|
2313
|
+
for (const p of externals) registry.register(p);
|
|
2314
|
+
}
|
|
2315
|
+
const storeOptions = resolveGraphStoreOptions(env);
|
|
2316
|
+
if (storeOptions.mode === "sqlite") {
|
|
2317
|
+
const [
|
|
2318
|
+
{ SQLiteGraphStore },
|
|
2319
|
+
{ orchestrateWithProviderResultStore }
|
|
2320
|
+
] = await Promise.all([
|
|
2321
|
+
import('./sqlite-graph-store-NH2NHL2B.js'),
|
|
2322
|
+
import('./store-orchestrator-337XWIXO.js')
|
|
2323
|
+
]);
|
|
2324
|
+
const store = await SQLiteGraphStore.create(options?.graphStore?.byteStore, options?.graphStore?.locateFile);
|
|
2325
|
+
const graph2 = await orchestrateWithProviderResultStore(
|
|
2326
|
+
registry,
|
|
2327
|
+
config,
|
|
2328
|
+
{ readme: data.readme },
|
|
2329
|
+
store,
|
|
2330
|
+
(providerId, previousContentHash) => buildProviderResultCacheKey(source, config, data, providerId, previousContentHash)
|
|
2331
|
+
);
|
|
2332
|
+
return { graph: graph2, config };
|
|
2333
|
+
}
|
|
2334
|
+
const graph = await orchestrateWithTransforms(registry, config, { readme: data.readme });
|
|
2335
|
+
return { graph, config };
|
|
2336
|
+
}
|
|
2337
|
+
function parseContentFrontmatter(raw) {
|
|
2338
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
2339
|
+
if (!match) return { meta: {}, body: raw };
|
|
2340
|
+
try {
|
|
2341
|
+
const parsed = yaml.parse(match[1]);
|
|
2342
|
+
return { meta: parsed ?? {}, body: match[2] ?? "" };
|
|
2343
|
+
} catch {
|
|
2344
|
+
return { meta: {}, body: raw };
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
function stripCode(body) {
|
|
2348
|
+
let stripped = body.replace(/```[\s\S]*?```/g, "");
|
|
2349
|
+
stripped = stripped.replace(/`[^`]+`/g, "");
|
|
2350
|
+
return stripped;
|
|
2351
|
+
}
|
|
2352
|
+
function extractInlineLinks(body) {
|
|
2353
|
+
const clean = stripCode(body);
|
|
2354
|
+
const links = [];
|
|
2355
|
+
const re = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
2356
|
+
let m;
|
|
2357
|
+
while ((m = re.exec(clean)) !== null) {
|
|
2358
|
+
const target = m[2];
|
|
2359
|
+
if (target.startsWith("http://") || target.startsWith("https://")) continue;
|
|
2360
|
+
if (target.startsWith("#")) continue;
|
|
2361
|
+
if (target.startsWith("mailto:")) continue;
|
|
2362
|
+
links.push(target);
|
|
2363
|
+
}
|
|
2364
|
+
return links;
|
|
2365
|
+
}
|
|
2366
|
+
function parseAuthoredEntries(authoredContent) {
|
|
2367
|
+
const entries = [];
|
|
2368
|
+
for (const [path, raw] of Object.entries(authoredContent)) {
|
|
2369
|
+
const { meta, body } = parseContentFrontmatter(raw);
|
|
2370
|
+
const id = typeof meta.id === "string" ? meta.id : void 0;
|
|
2371
|
+
if (!id) continue;
|
|
2372
|
+
entries.push({
|
|
2373
|
+
path,
|
|
2374
|
+
id,
|
|
2375
|
+
title: typeof meta.title === "string" ? meta.title : id,
|
|
2376
|
+
cluster: typeof meta.cluster === "string" ? meta.cluster : null,
|
|
2377
|
+
body,
|
|
2378
|
+
links: extractInlineLinks(body)
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
return entries;
|
|
2382
|
+
}
|
|
2383
|
+
function parseConfigClusters(configRaw) {
|
|
2384
|
+
if (!configRaw) return {};
|
|
2385
|
+
try {
|
|
2386
|
+
const parsed = yaml.parse(configRaw);
|
|
2387
|
+
return parsed?.clusters ?? {};
|
|
2388
|
+
} catch {
|
|
2389
|
+
return {};
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/validate-graph.ts
|
|
2394
|
+
function parseNodemap(raw) {
|
|
2395
|
+
try {
|
|
2396
|
+
return yaml.parse(raw) ?? null;
|
|
2397
|
+
} catch {
|
|
2398
|
+
return null;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
function validateGraph(input) {
|
|
2402
|
+
const entries = parseAuthoredEntries(input.authoredContent);
|
|
2403
|
+
const definedClusters = new Set(Object.keys(parseConfigClusters(input.configRaw)));
|
|
2404
|
+
const issueCount = input.issues?.length ?? 0;
|
|
2405
|
+
const nodeIds = new Set(entries.map((e) => e.id));
|
|
2406
|
+
const findings = [];
|
|
2407
|
+
for (const entry of entries) {
|
|
2408
|
+
for (const target of entry.links) {
|
|
2409
|
+
if (nodeIds.has(target)) continue;
|
|
2410
|
+
const isGithubRef = target.startsWith("issue-") || target.startsWith("pr-");
|
|
2411
|
+
if (isGithubRef && issueCount === 0) {
|
|
2412
|
+
findings.push({
|
|
2413
|
+
rule: "missing-github-link",
|
|
2414
|
+
severity: "warning",
|
|
2415
|
+
message: `${entry.id} \u2192 ${target} (no GitHub data available)`,
|
|
2416
|
+
nodeId: entry.id,
|
|
2417
|
+
target
|
|
2418
|
+
});
|
|
2419
|
+
} else {
|
|
2420
|
+
findings.push({
|
|
2421
|
+
rule: "broken-inline-link",
|
|
2422
|
+
severity: "error",
|
|
2423
|
+
message: `${entry.id} \u2192 ${target}`,
|
|
2424
|
+
nodeId: entry.id,
|
|
2425
|
+
target
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const idCounts = /* @__PURE__ */ new Map();
|
|
2431
|
+
for (const entry of entries) idCounts.set(entry.id, (idCounts.get(entry.id) ?? 0) + 1);
|
|
2432
|
+
for (const [id, count] of idCounts) {
|
|
2433
|
+
if (count > 1) {
|
|
2434
|
+
findings.push({
|
|
2435
|
+
rule: "duplicate-id",
|
|
2436
|
+
severity: "error",
|
|
2437
|
+
message: `duplicate ID "${id}" (${count} files)`,
|
|
2438
|
+
nodeId: id
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
const incomingCount = /* @__PURE__ */ new Map();
|
|
2443
|
+
for (const entry of entries) incomingCount.set(entry.id, 0);
|
|
2444
|
+
for (const entry of entries) {
|
|
2445
|
+
for (const target of entry.links) {
|
|
2446
|
+
if (incomingCount.has(target)) incomingCount.set(target, incomingCount.get(target) + 1);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
for (const [id, count] of incomingCount) {
|
|
2450
|
+
if (count === 0) {
|
|
2451
|
+
findings.push({
|
|
2452
|
+
rule: "orphan-node",
|
|
2453
|
+
severity: "warning",
|
|
2454
|
+
message: `"${id}" has no incoming links`,
|
|
2455
|
+
nodeId: id
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
for (const entry of entries) {
|
|
2460
|
+
if (entry.cluster && !definedClusters.has(entry.cluster)) {
|
|
2461
|
+
findings.push({
|
|
2462
|
+
rule: "invalid-cluster",
|
|
2463
|
+
severity: "error",
|
|
2464
|
+
message: `"${entry.id}" \u2192 cluster "${entry.cluster}"`,
|
|
2465
|
+
nodeId: entry.id,
|
|
2466
|
+
target: entry.cluster
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (input.nodemapRaw) {
|
|
2471
|
+
const nodemap = parseNodemap(input.nodemapRaw);
|
|
2472
|
+
const knownPaths = new Set(Object.keys(input.authoredContent));
|
|
2473
|
+
for (const item of input.tree ?? []) knownPaths.add(item.path);
|
|
2474
|
+
for (const node of nodemap?.nodes ?? []) {
|
|
2475
|
+
const candidates = [];
|
|
2476
|
+
if (node.file) candidates.push(node.file);
|
|
2477
|
+
if (node.directory) candidates.push(node.directory);
|
|
2478
|
+
if (node.files) candidates.push(...node.files);
|
|
2479
|
+
for (const path of candidates) {
|
|
2480
|
+
if (!knownPaths.has(path)) {
|
|
2481
|
+
findings.push({
|
|
2482
|
+
rule: "missing-nodemap-path",
|
|
2483
|
+
severity: "error",
|
|
2484
|
+
message: `"${node.id}" \u2192 ${path}`,
|
|
2485
|
+
nodeId: node.id,
|
|
2486
|
+
target: path
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
for (const entry of entries) {
|
|
2493
|
+
if (!entry.body.trim()) {
|
|
2494
|
+
findings.push({
|
|
2495
|
+
rule: "empty-content",
|
|
2496
|
+
severity: "warning",
|
|
2497
|
+
message: `"${entry.id}" has empty content`,
|
|
2498
|
+
nodeId: entry.id
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
const errorCount = findings.filter((f) => f.severity === "error").length;
|
|
2503
|
+
const warningCount = findings.filter((f) => f.severity === "warning").length;
|
|
2504
|
+
return {
|
|
2505
|
+
ok: errorCount === 0,
|
|
2506
|
+
errorCount,
|
|
2507
|
+
warningCount,
|
|
2508
|
+
findings,
|
|
2509
|
+
summary: { contentCount: entries.length, issueCount }
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// src/assess-graph.ts
|
|
2514
|
+
var LIMITS = {
|
|
2515
|
+
nodesPerView: 40,
|
|
2516
|
+
edgesPerView: 80,
|
|
2517
|
+
maxClusters: 8,
|
|
2518
|
+
highOutDegree: 15
|
|
2519
|
+
};
|
|
2520
|
+
var MIN_SCORES = {
|
|
2521
|
+
connectivity: 50,
|
|
2522
|
+
clusterBalance: 30,
|
|
2523
|
+
density: 30,
|
|
2524
|
+
bidirectionality: 20,
|
|
2525
|
+
contentDepth: 60
|
|
2526
|
+
};
|
|
2527
|
+
function clamp100(v) {
|
|
2528
|
+
return Math.max(0, Math.min(100, Math.round(v)));
|
|
2529
|
+
}
|
|
2530
|
+
function bfsDistances(adjacency, startId) {
|
|
2531
|
+
const distances = /* @__PURE__ */ new Map([[startId, 0]]);
|
|
2532
|
+
const queue = [startId];
|
|
2533
|
+
while (queue.length > 0) {
|
|
2534
|
+
const current = queue.shift();
|
|
2535
|
+
const d = distances.get(current);
|
|
2536
|
+
for (const neighbour of adjacency.get(current) ?? []) {
|
|
2537
|
+
if (!distances.has(neighbour)) {
|
|
2538
|
+
distances.set(neighbour, d + 1);
|
|
2539
|
+
queue.push(neighbour);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return distances;
|
|
2544
|
+
}
|
|
2545
|
+
function assessGraph(input, options = {}) {
|
|
2546
|
+
const entries = parseAuthoredEntries(input.authoredContent);
|
|
2547
|
+
const nodeIds = new Set(entries.map((e) => e.id));
|
|
2548
|
+
const edges = [];
|
|
2549
|
+
for (const entry of entries) {
|
|
2550
|
+
for (const target of entry.links) {
|
|
2551
|
+
if (nodeIds.has(target)) edges.push({ from: entry.id, to: target });
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
const clusterMap = /* @__PURE__ */ new Map();
|
|
2555
|
+
for (const entry of entries) {
|
|
2556
|
+
if (entry.cluster) {
|
|
2557
|
+
if (!clusterMap.has(entry.cluster)) clusterMap.set(entry.cluster, []);
|
|
2558
|
+
clusterMap.get(entry.cluster).push(entry.id);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
const nodeCount = entries.length;
|
|
2562
|
+
const edgeCount = edges.length;
|
|
2563
|
+
const clusterCount = clusterMap.size;
|
|
2564
|
+
const outDegree = /* @__PURE__ */ new Map();
|
|
2565
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
2566
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
2567
|
+
for (const id of nodeIds) {
|
|
2568
|
+
outDegree.set(id, 0);
|
|
2569
|
+
inDegree.set(id, 0);
|
|
2570
|
+
adjacency.set(id, /* @__PURE__ */ new Set());
|
|
2571
|
+
}
|
|
2572
|
+
for (const e of edges) {
|
|
2573
|
+
outDegree.set(e.from, (outDegree.get(e.from) ?? 0) + 1);
|
|
2574
|
+
inDegree.set(e.to, (inDegree.get(e.to) ?? 0) + 1);
|
|
2575
|
+
adjacency.get(e.from).add(e.to);
|
|
2576
|
+
adjacency.get(e.to).add(e.from);
|
|
2577
|
+
}
|
|
2578
|
+
const nodeCountConstraint = {
|
|
2579
|
+
value: nodeCount,
|
|
2580
|
+
limit: LIMITS.nodesPerView,
|
|
2581
|
+
ok: nodeCount <= LIMITS.nodesPerView
|
|
2582
|
+
};
|
|
2583
|
+
const edgeCountConstraint = {
|
|
2584
|
+
value: edgeCount,
|
|
2585
|
+
limit: LIMITS.edgesPerView,
|
|
2586
|
+
ok: edgeCount <= LIMITS.edgesPerView
|
|
2587
|
+
};
|
|
2588
|
+
const clusterCountConstraint = {
|
|
2589
|
+
value: clusterCount,
|
|
2590
|
+
limit: LIMITS.maxClusters,
|
|
2591
|
+
ok: clusterCount <= LIMITS.maxClusters
|
|
2592
|
+
};
|
|
2593
|
+
const orphans = [];
|
|
2594
|
+
for (const entry of entries) {
|
|
2595
|
+
if ((inDegree.get(entry.id) ?? 0) === 0) orphans.push(entry.id);
|
|
2596
|
+
}
|
|
2597
|
+
let hubId = entries[0]?.id ?? null;
|
|
2598
|
+
let hubDegree = 0;
|
|
2599
|
+
for (const entry of entries) {
|
|
2600
|
+
const total = (outDegree.get(entry.id) ?? 0) + (inDegree.get(entry.id) ?? 0);
|
|
2601
|
+
if (total > hubDegree) {
|
|
2602
|
+
hubDegree = total;
|
|
2603
|
+
hubId = entry.id;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
let maxHops = 0;
|
|
2607
|
+
const unreachable = [];
|
|
2608
|
+
const distances = hubId !== null ? bfsDistances(adjacency, hubId) : /* @__PURE__ */ new Map();
|
|
2609
|
+
for (const id of nodeIds) {
|
|
2610
|
+
if (!distances.has(id)) unreachable.push(id);
|
|
2611
|
+
else maxHops = Math.max(maxHops, distances.get(id));
|
|
2612
|
+
}
|
|
2613
|
+
const avgLinksPerNode = nodeCount > 0 ? edgeCount / nodeCount : 0;
|
|
2614
|
+
let connectivityScore;
|
|
2615
|
+
if (avgLinksPerNode >= 4 && avgLinksPerNode <= 8) {
|
|
2616
|
+
connectivityScore = 100;
|
|
2617
|
+
} else if (avgLinksPerNode < 4) {
|
|
2618
|
+
connectivityScore = clamp100(avgLinksPerNode / 4 * 100);
|
|
2619
|
+
} else {
|
|
2620
|
+
connectivityScore = clamp100(100 - (avgLinksPerNode - 8) / 8 * 50);
|
|
2621
|
+
}
|
|
2622
|
+
const clusterSizes = [...clusterMap.values()].map((members) => members.length);
|
|
2623
|
+
const clusterBalanceApplicable = clusterSizes.length > 1;
|
|
2624
|
+
let clusterBalanceScore = 100;
|
|
2625
|
+
let clusterStdDev = null;
|
|
2626
|
+
if (clusterBalanceApplicable) {
|
|
2627
|
+
const mean = clusterSizes.reduce((a, b) => a + b, 0) / clusterSizes.length;
|
|
2628
|
+
const variance = clusterSizes.reduce((s, v) => s + (v - mean) ** 2, 0) / clusterSizes.length;
|
|
2629
|
+
clusterStdDev = Math.sqrt(variance);
|
|
2630
|
+
clusterBalanceScore = clamp100(100 - clusterStdDev * 15);
|
|
2631
|
+
}
|
|
2632
|
+
const maxEdges = nodeCount > 1 ? nodeCount * (nodeCount - 1) / 2 : 1;
|
|
2633
|
+
const density = edgeCount / maxEdges;
|
|
2634
|
+
let densityScore;
|
|
2635
|
+
if (density >= 0.1 && density <= 0.3) {
|
|
2636
|
+
densityScore = 100;
|
|
2637
|
+
} else if (density < 0.1) {
|
|
2638
|
+
densityScore = clamp100(density / 0.1 * 100);
|
|
2639
|
+
} else {
|
|
2640
|
+
densityScore = clamp100(100 - (density - 0.3) / 0.7 * 100);
|
|
2641
|
+
}
|
|
2642
|
+
const edgeSet = new Set(edges.map((e) => `${e.from}\u2192${e.to}`));
|
|
2643
|
+
let reciprocalCount = 0;
|
|
2644
|
+
for (const e of edges) {
|
|
2645
|
+
if (edgeSet.has(`${e.to}\u2192${e.from}`)) reciprocalCount++;
|
|
2646
|
+
}
|
|
2647
|
+
const bidirectionalPct = edgeCount > 0 ? reciprocalCount / edgeCount * 100 : 0;
|
|
2648
|
+
const bidirectionalityScore = clamp100(bidirectionalPct);
|
|
2649
|
+
const avgContentLength = nodeCount > 0 ? entries.reduce((sum, e) => sum + e.body.length, 0) / nodeCount : 0;
|
|
2650
|
+
const contentDepthScore = clamp100(avgContentLength / 1e3 * 100);
|
|
2651
|
+
const scores = {
|
|
2652
|
+
connectivity: connectivityScore,
|
|
2653
|
+
clusterBalance: clusterBalanceScore,
|
|
2654
|
+
density: densityScore,
|
|
2655
|
+
bidirectionality: bidirectionalityScore,
|
|
2656
|
+
contentDepth: contentDepthScore
|
|
2657
|
+
};
|
|
2658
|
+
const suggestions = [];
|
|
2659
|
+
for (const id of orphans) {
|
|
2660
|
+
suggestions.push(`Node "${id}" has 0 incoming links \u2014 add a reference from a parent node`);
|
|
2661
|
+
}
|
|
2662
|
+
for (const [cluster, members] of clusterMap) {
|
|
2663
|
+
if (members.length > LIMITS.maxClusters + 1) {
|
|
2664
|
+
suggestions.push(`Cluster "${cluster}" has ${members.length} nodes \u2014 consider splitting into sub-clusters`);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
for (const entry of entries) {
|
|
2668
|
+
const out = outDegree.get(entry.id) ?? 0;
|
|
2669
|
+
if (out >= LIMITS.highOutDegree) {
|
|
2670
|
+
suggestions.push(`Node "${entry.id}" has ${out} outgoing links \u2014 consider splitting into focused sub-nodes`);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
const titleMap = /* @__PURE__ */ new Map();
|
|
2674
|
+
for (const entry of entries) {
|
|
2675
|
+
const t = (entry.title || "").toLowerCase();
|
|
2676
|
+
if (!titleMap.has(t)) titleMap.set(t, []);
|
|
2677
|
+
titleMap.get(t).push(entry.id);
|
|
2678
|
+
}
|
|
2679
|
+
for (const [title, ids] of titleMap) {
|
|
2680
|
+
if (ids.length > 1) {
|
|
2681
|
+
suggestions.push(`Nodes ${ids.join(" and ")} have identical title "${title}" \u2014 possible duplicate`);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (edgeCount > LIMITS.edgesPerView) {
|
|
2685
|
+
suggestions.push(`Edge count ${edgeCount} exceeds ${LIMITS.edgesPerView}-edge readability limit \u2014 use layer views`);
|
|
2686
|
+
}
|
|
2687
|
+
if (nodeCount > LIMITS.nodesPerView) {
|
|
2688
|
+
suggestions.push(`Node count ${nodeCount} exceeds ${LIMITS.nodesPerView}-node readability limit \u2014 use layer views`);
|
|
2689
|
+
}
|
|
2690
|
+
const result = {
|
|
2691
|
+
summary: { nodeCount, edgeCount, clusterCount },
|
|
2692
|
+
constraints: {
|
|
2693
|
+
nodeCount: nodeCountConstraint,
|
|
2694
|
+
edgeCount: edgeCountConstraint,
|
|
2695
|
+
clusterCount: clusterCountConstraint,
|
|
2696
|
+
orphanNodes: orphans,
|
|
2697
|
+
hubReachability: { hubId, maxHops, unreachable }
|
|
2698
|
+
},
|
|
2699
|
+
scores,
|
|
2700
|
+
scoreDetails: {
|
|
2701
|
+
avgLinksPerNode,
|
|
2702
|
+
clusterSizes,
|
|
2703
|
+
clusterStdDev,
|
|
2704
|
+
clusterBalanceApplicable,
|
|
2705
|
+
density,
|
|
2706
|
+
bidirectionalPct,
|
|
2707
|
+
avgContentLength
|
|
2708
|
+
},
|
|
2709
|
+
suggestions
|
|
2710
|
+
};
|
|
2711
|
+
if (options.gate) {
|
|
2712
|
+
const failures = [];
|
|
2713
|
+
for (const metric of Object.keys(MIN_SCORES)) {
|
|
2714
|
+
const minimum = MIN_SCORES[metric];
|
|
2715
|
+
const actual = scores[metric];
|
|
2716
|
+
if (actual < minimum) failures.push({ metric, actual, minimum });
|
|
2717
|
+
}
|
|
2718
|
+
result.gate = { pass: failures.length === 0, failures };
|
|
2719
|
+
}
|
|
2720
|
+
return result;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// src/catalogue/derive-needs.ts
|
|
2724
|
+
var AUTHORED_FRONTMATTER = /^authored:\s*true/m;
|
|
2725
|
+
function toDeriveNeedsNode(node) {
|
|
2726
|
+
const out = { id: node.id };
|
|
2727
|
+
if (node.title !== void 0) out.title = node.title;
|
|
2728
|
+
if (node.cluster !== void 0) out.cluster = node.cluster;
|
|
2729
|
+
if (node.file !== void 0) out.file = node.file;
|
|
2730
|
+
if (node.prompt !== void 0) out.prompt = node.prompt;
|
|
2731
|
+
if (node.edgeHints !== void 0) out.edgeHints = node.edgeHints;
|
|
2732
|
+
return out;
|
|
2733
|
+
}
|
|
2734
|
+
function deriveNeeds(catalogue, contentFiles) {
|
|
2735
|
+
const nodes = catalogue.nodes ?? [];
|
|
2736
|
+
const authored = [];
|
|
2737
|
+
const needsGeneration = [];
|
|
2738
|
+
for (const node of nodes) {
|
|
2739
|
+
if (node.authored) {
|
|
2740
|
+
authored.push(node);
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
if (node.derived) {
|
|
2744
|
+
const raw = contentFiles[node.id];
|
|
2745
|
+
if (raw !== void 0 && AUTHORED_FRONTMATTER.test(raw)) {
|
|
2746
|
+
authored.push(node);
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
needsGeneration.push(node);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
total: nodes.length,
|
|
2754
|
+
authored: authored.length,
|
|
2755
|
+
derived: needsGeneration.length,
|
|
2756
|
+
nodes: needsGeneration.map(toDeriveNeedsNode)
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
// src/catalogue/compare-content.ts
|
|
2761
|
+
var MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
2762
|
+
var CLUSTER_FRONTMATTER = /^cluster:\s*(.+)$/m;
|
|
2763
|
+
var LINK_DIFF_THRESHOLD = 3;
|
|
2764
|
+
function compareContent(catalogue, contentFiles) {
|
|
2765
|
+
const nodes = catalogue.nodes ?? [];
|
|
2766
|
+
const contentIds = Object.keys(contentFiles);
|
|
2767
|
+
const catalogueIds = /* @__PURE__ */ new Set();
|
|
2768
|
+
const authoredNodes = [];
|
|
2769
|
+
const derivedCurrent = [];
|
|
2770
|
+
const missingNodes = [];
|
|
2771
|
+
const clusterChanges = [];
|
|
2772
|
+
const linkDiffs = [];
|
|
2773
|
+
for (const node of nodes) {
|
|
2774
|
+
catalogueIds.add(node.id);
|
|
2775
|
+
const raw = contentFiles[node.id];
|
|
2776
|
+
const fileExists = raw !== void 0;
|
|
2777
|
+
if (node.authored) {
|
|
2778
|
+
if (fileExists) {
|
|
2779
|
+
authoredNodes.push(node);
|
|
2780
|
+
} else {
|
|
2781
|
+
missingNodes.push(node);
|
|
2782
|
+
}
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
if (node.derived) {
|
|
2786
|
+
if (fileExists) {
|
|
2787
|
+
derivedCurrent.push(node);
|
|
2788
|
+
const clusterMatch = raw.match(CLUSTER_FRONTMATTER);
|
|
2789
|
+
const fileCluster = clusterMatch?.[1]?.trim();
|
|
2790
|
+
if (fileCluster !== void 0 && fileCluster !== node.cluster) {
|
|
2791
|
+
clusterChanges.push({ id: node.id, from: fileCluster, to: String(node.cluster ?? "") });
|
|
2792
|
+
}
|
|
2793
|
+
const hintCount = (node.edgeHints ?? []).length;
|
|
2794
|
+
const linkMatches = raw.match(MARKDOWN_LINK) ?? [];
|
|
2795
|
+
const diff = Math.abs(hintCount - linkMatches.length);
|
|
2796
|
+
if (diff > LINK_DIFF_THRESHOLD) {
|
|
2797
|
+
linkDiffs.push({ id: node.id, catalogue: hintCount, file: linkMatches.length });
|
|
2798
|
+
}
|
|
2799
|
+
} else {
|
|
2800
|
+
missingNodes.push(node);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
const extraFiles = contentIds.filter((id) => !catalogueIds.has(id) && id !== "catalogue");
|
|
2805
|
+
return {
|
|
2806
|
+
totalNodes: nodes.length,
|
|
2807
|
+
totalContentFiles: contentIds.length,
|
|
2808
|
+
authoredNodes,
|
|
2809
|
+
derivedCurrent,
|
|
2810
|
+
missingNodes,
|
|
2811
|
+
extraFiles,
|
|
2812
|
+
clusterChanges,
|
|
2813
|
+
linkDiffs
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/catalogue/enrich-from-manifest.ts
|
|
2818
|
+
var SNIPPET_LENGTH = 200;
|
|
2819
|
+
var MAX_RELATED_ISSUES = 5;
|
|
2820
|
+
var MAX_RELATED_PRS = 5;
|
|
2821
|
+
var MAX_RECENT_COMMITS = 8;
|
|
2822
|
+
var MIN_TITLE_LENGTH_FOR_COMMIT_MATCH = 5;
|
|
2823
|
+
function fileBaseName(file) {
|
|
2824
|
+
const slash = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"));
|
|
2825
|
+
const base = slash >= 0 ? file.slice(slash + 1) : file;
|
|
2826
|
+
return base.replace(/\.\w+$/, "");
|
|
2827
|
+
}
|
|
2828
|
+
function snippetOf(body) {
|
|
2829
|
+
return (body ?? "").substring(0, SNIPPET_LENGTH).replace(/\n/g, " ");
|
|
2830
|
+
}
|
|
2831
|
+
function enrichFromManifest(catalogue, manifest) {
|
|
2832
|
+
const issues = manifest.issues ?? [];
|
|
2833
|
+
const prs = manifest.pullRequests ?? [];
|
|
2834
|
+
const commits = manifest.commits ?? [];
|
|
2835
|
+
const enrichedNodes = (catalogue.nodes ?? []).map((node) => {
|
|
2836
|
+
const file = node.file ?? "";
|
|
2837
|
+
const fileBase = fileBaseName(file);
|
|
2838
|
+
const titleLower = (node.title ?? "").toLowerCase();
|
|
2839
|
+
const idLower = (node.id ?? "").toLowerCase();
|
|
2840
|
+
const relatedIssues = [];
|
|
2841
|
+
for (const iss of issues) {
|
|
2842
|
+
const body = (iss.body ?? "").toLowerCase();
|
|
2843
|
+
const title = (iss.title ?? "").toLowerCase();
|
|
2844
|
+
if (file && (body.includes(file) || body.includes(fileBase)) || titleLower && (body.includes(titleLower) || title.includes(titleLower)) || idLower && body.includes(idLower)) {
|
|
2845
|
+
relatedIssues.push({ number: iss.number, title: iss.title, state: iss.state, snippet: snippetOf(iss.body) });
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
const relatedPRs = [];
|
|
2849
|
+
for (const pr of prs) {
|
|
2850
|
+
const body = (pr.body ?? "").toLowerCase();
|
|
2851
|
+
const title = (pr.title ?? "").toLowerCase();
|
|
2852
|
+
if (file && (body.includes(file) || body.includes(fileBase)) || titleLower && (body.includes(titleLower) || title.includes(titleLower))) {
|
|
2853
|
+
relatedPRs.push({ number: pr.number, title: pr.title, state: pr.state, snippet: snippetOf(pr.body) });
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
const recentCommits = [];
|
|
2857
|
+
for (const c of commits) {
|
|
2858
|
+
const msg = (c.commit?.message ?? "").toLowerCase();
|
|
2859
|
+
if (file && msg.includes(fileBase) || titleLower.length > MIN_TITLE_LENGTH_FOR_COMMIT_MATCH && msg.includes(titleLower)) {
|
|
2860
|
+
recentCommits.push({ sha: c.sha?.substring(0, 7), message: c.commit?.message?.split("\n")[0] ?? "" });
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
return {
|
|
2864
|
+
...node,
|
|
2865
|
+
relatedIssues: relatedIssues.slice(0, MAX_RELATED_ISSUES),
|
|
2866
|
+
relatedPRs: relatedPRs.slice(0, MAX_RELATED_PRS),
|
|
2867
|
+
recentCommits: recentCommits.slice(0, MAX_RECENT_COMMITS)
|
|
2868
|
+
};
|
|
2869
|
+
});
|
|
2870
|
+
let nodesWithIssues = 0;
|
|
2871
|
+
let nodesWithPRs = 0;
|
|
2872
|
+
let nodesWithCommits = 0;
|
|
2873
|
+
for (const n of enrichedNodes) {
|
|
2874
|
+
if (n.relatedIssues.length > 0) nodesWithIssues++;
|
|
2875
|
+
if (n.relatedPRs.length > 0) nodesWithPRs++;
|
|
2876
|
+
if (n.recentCommits.length > 0) nodesWithCommits++;
|
|
2877
|
+
}
|
|
2878
|
+
return {
|
|
2879
|
+
catalogue: { ...catalogue, nodes: enrichedNodes },
|
|
2880
|
+
summary: {
|
|
2881
|
+
issueCount: issues.length,
|
|
2882
|
+
prCount: prs.length,
|
|
2883
|
+
commitCount: commits.length,
|
|
2884
|
+
totalNodes: enrichedNodes.length,
|
|
2885
|
+
nodesWithIssues,
|
|
2886
|
+
nodesWithPRs,
|
|
2887
|
+
nodesWithCommits
|
|
2888
|
+
}
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
export { AuthoredProvider, AuthoredRichMarkdownProvider, CONTENT_MODEL_KINDS, CONTENT_MODEL_PROVIDER, ContentModelProvider, FilesProvider, PersonProvider, ProviderRegistry, SCHEMA_PATHS, StructuralProvider, WorkProvider, adaptIngestedNode, applyStructuredNodeMap, assessGraph, buildContentModel, buildEditUrl, buildHandoffUrl, buildNewFileUrl, buildSourceEditHandoff, buildStructuralFileNode, buildUnifiedDiff, buildUrn, canEditSource, canonicalKind, collectProviderNodes, compareContent, deriveNeeds, encodeRepoPath, enrichFromManifest, findNodes, getConvention, getNode, hasContentModelSource, inferStructuredNode, isOrgScoped, lifecycleBand, loadKnowledgeBase, neighbors, normalizeNewlines, orchestrate, orchestrateWithTransforms, parseCodeowners, parseStructuredContent, parseStructuredNodeMap, patchFilename, readContentModelSchema, reconstructSource, registerContentModelTypes, registerProviders, registerStructuralTypes, related, repoCoordsFromConfig, resolveCurie, resolveSourceFile, shortestPath, slugify, subgraph, urnLocalId, validateGraph, validateSourceContent };
|