llmnav 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +294 -0
  4. package/ROADMAP.md +71 -0
  5. package/bin/llmnav.js +16 -0
  6. package/docs/agent-integration.md +114 -0
  7. package/docs/api.md +290 -0
  8. package/docs/architecture.md +286 -0
  9. package/docs/benchmarking.md +164 -0
  10. package/docs/ci.md +196 -0
  11. package/docs/cli.md +233 -0
  12. package/docs/configuration.md +117 -0
  13. package/docs/editor-integration.md +29 -0
  14. package/docs/faq.md +59 -0
  15. package/docs/graph.md +92 -0
  16. package/docs/language-examples.md +130 -0
  17. package/docs/migration.md +130 -0
  18. package/docs/performance-v0.2.md +42 -0
  19. package/docs/provider-neutral-integration.md +66 -0
  20. package/docs/publishing.md +86 -0
  21. package/docs/quickstart.md +139 -0
  22. package/docs/research.md +31 -0
  23. package/docs/spec.md +424 -0
  24. package/examples/provider-neutral-host.d.mts +17 -0
  25. package/examples/provider-neutral-host.mjs +40 -0
  26. package/package.json +79 -0
  27. package/schema/config.schema.json +296 -0
  28. package/src/agent-protocol.js +117 -0
  29. package/src/agent-tools.js +61 -0
  30. package/src/agents.js +127 -0
  31. package/src/boundaries.js +50 -0
  32. package/src/changes.js +168 -0
  33. package/src/cli.js +459 -0
  34. package/src/config.js +305 -0
  35. package/src/contracts.js +70 -0
  36. package/src/declaration.js +334 -0
  37. package/src/doctor.js +124 -0
  38. package/src/editor.js +107 -0
  39. package/src/evaluation.js +67 -0
  40. package/src/files.js +81 -0
  41. package/src/formatter.js +23 -0
  42. package/src/generator.js +528 -0
  43. package/src/graph-input.js +157 -0
  44. package/src/graph.js +403 -0
  45. package/src/incremental.js +262 -0
  46. package/src/index.d.ts +673 -0
  47. package/src/index.js +115 -0
  48. package/src/initializer.js +137 -0
  49. package/src/inverted-index.js +350 -0
  50. package/src/parser.js +449 -0
  51. package/src/project.js +65 -0
  52. package/src/prompt-bundle.js +108 -0
  53. package/src/registry.js +107 -0
  54. package/src/sarif.js +70 -0
  55. package/src/search-shards.js +75 -0
  56. package/src/search.js +636 -0
  57. package/src/spec.d.ts +27 -0
  58. package/src/spec.js +237 -0
  59. package/src/tokenizer.js +37 -0
  60. package/src/transaction.js +557 -0
  61. package/src/util.js +256 -0
  62. package/src/validator.js +635 -0
  63. package/templates/file-card.txt +8 -0
  64. package/templates/lexicon.json +7 -0
  65. package/templates/line-card.txt +9 -0
  66. package/templates/module-card.txt +9 -0
  67. package/templates/queries.jsonl +1 -0
  68. package/templates/symbol-card.txt +10 -0
@@ -0,0 +1,157 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.graph.import
3
+ role=Load and validate optional generated definition and reference indexes without executing repository code.
4
+ owns=graph import schema|input normalization|graph input diagnostics
5
+ excludes=graph ranking|source mutation
6
+ search=definition index import|reference index import|graph input validation
7
+ rel=workflow>llmnav.index.generate
8
+ stability=architecture
9
+ */
10
+
11
+ import path from "node:path";
12
+ import { ID_PATTERN, CROSS_REPO_ID_PATTERN } from "./spec.js";
13
+ import { assertNoSymlinkTraversal, compareText, readText, sha256, toPosix } from "./util.js";
14
+ import { diagnostic } from "./validator.js";
15
+
16
+ export const GRAPH_INPUT_SCHEMA_VERSION = 1;
17
+
18
+ export async function loadGraphInputs(root, config) {
19
+ const indexes = [];
20
+ const diagnostics = [];
21
+ for (const relativePath of [...config.graph.indexFiles].sort(compareText)) {
22
+ const file = toPosix(relativePath);
23
+ const absolutePath = path.join(root, file);
24
+ try {
25
+ await assertNoSymlinkTraversal(root, absolutePath, file);
26
+ const text = await readText(absolutePath, null);
27
+ if (text === null) throw new Error("file does not exist");
28
+ const parsed = JSON.parse(text);
29
+ indexes.push(normalizeGraphInput(parsed, file, sha256(text)));
30
+ } catch (error) {
31
+ diagnostics.push(
32
+ diagnostic(
33
+ "error",
34
+ "LNV014",
35
+ `Invalid graph index: ${error instanceof Error ? error.message : String(error)}`,
36
+ file,
37
+ 1,
38
+ ),
39
+ );
40
+ }
41
+ }
42
+ return { indexes, diagnostics };
43
+ }
44
+
45
+ export function normalizeGraphInput(value, file = "<graph-index>", contentHash = null) {
46
+ assertObject(value, "graph index");
47
+ assertKeys(value, new Set(["schemaVersion", "repositoryId", "generator", "definitions", "references"]), "graph index");
48
+ if (value.schemaVersion !== GRAPH_INPUT_SCHEMA_VERSION) throw new Error("schemaVersion must be 1");
49
+ if (!/^[a-z][a-z0-9-]{0,63}$/u.test(value.repositoryId ?? "")) {
50
+ throw new Error("repositoryId must match ^[a-z][a-z0-9-]{0,63}$");
51
+ }
52
+ if (value.generator !== undefined && (typeof value.generator !== "string" || !value.generator.trim())) {
53
+ throw new Error("generator must be a non-empty string when present");
54
+ }
55
+ if (!Array.isArray(value.definitions)) throw new Error("definitions must be an array");
56
+ if (!Array.isArray(value.references)) throw new Error("references must be an array");
57
+
58
+ const definitions = value.definitions.map((item, index) => normalizeDefinition(item, index, value.repositoryId));
59
+ const definitionIds = new Set();
60
+ for (const definition of definitions) {
61
+ if (definitionIds.has(definition.id)) throw new Error(`definitions contains duplicate ID ${definition.id}`);
62
+ definitionIds.add(definition.id);
63
+ }
64
+ const references = value.references.map((item, index) => normalizeReference(item, index, value.repositoryId));
65
+ definitions.sort((left, right) => compareText(left.id, right.id));
66
+ references.sort(compareReferences);
67
+
68
+ return {
69
+ file: toPosix(file),
70
+ contentHash,
71
+ schemaVersion: GRAPH_INPUT_SCHEMA_VERSION,
72
+ repositoryId: value.repositoryId,
73
+ generator: value.generator?.trim() ?? null,
74
+ definitions,
75
+ references,
76
+ };
77
+ }
78
+
79
+ function normalizeDefinition(value, index, repositoryId) {
80
+ const name = `definitions[${index}]`;
81
+ assertObject(value, name);
82
+ assertKeys(value, new Set(["id", "symbol", "path", "line", "kind"]), name);
83
+ const id = normalizeSemanticKey(value.id, repositoryId, `${name}.id`);
84
+ if (typeof value.symbol !== "string" || !value.symbol.trim()) throw new Error(`${name}.symbol must be a non-empty string`);
85
+ const normalizedPath = normalizeSourcePath(value.path, `${name}.path`);
86
+ if (value.line !== undefined && (!Number.isInteger(value.line) || value.line < 1)) {
87
+ throw new Error(`${name}.line must be a positive integer when present`);
88
+ }
89
+ if (value.kind !== undefined && (typeof value.kind !== "string" || !value.kind.trim())) {
90
+ throw new Error(`${name}.kind must be a non-empty string when present`);
91
+ }
92
+ return {
93
+ id,
94
+ symbol: value.symbol.trim(),
95
+ path: normalizedPath,
96
+ line: value.line ?? null,
97
+ kind: value.kind?.trim() ?? null,
98
+ };
99
+ }
100
+
101
+ function normalizeReference(value, index, repositoryId) {
102
+ const name = `references[${index}]`;
103
+ assertObject(value, name);
104
+ assertKeys(value, new Set(["from", "to", "kind", "path", "line", "confidence"]), name);
105
+ const from = normalizeSemanticKey(value.from, repositoryId, `${name}.from`);
106
+ const to = normalizeSemanticKey(value.to, repositoryId, `${name}.to`);
107
+ if (typeof value.kind !== "string" || !/^[a-z][a-z0-9-]*$/u.test(value.kind)) {
108
+ throw new Error(`${name}.kind must be a controlled lower-case identifier`);
109
+ }
110
+ const normalizedPath = value.path === undefined ? null : normalizeSourcePath(value.path, `${name}.path`);
111
+ if (value.line !== undefined && (!Number.isInteger(value.line) || value.line < 1)) {
112
+ throw new Error(`${name}.line must be a positive integer when present`);
113
+ }
114
+ const confidence = value.confidence ?? 0.8;
115
+ if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
116
+ throw new Error(`${name}.confidence must be a number from 0 to 1`);
117
+ }
118
+ return {
119
+ from,
120
+ to,
121
+ kind: value.kind,
122
+ path: normalizedPath,
123
+ line: value.line ?? null,
124
+ confidence,
125
+ };
126
+ }
127
+
128
+ function normalizeSemanticKey(value, repositoryId, name) {
129
+ if (typeof value !== "string") throw new Error(`${name} must be a semantic ID`);
130
+ const normalized = value.trim();
131
+ if (ID_PATTERN.test(normalized)) return `${repositoryId}/${normalized}`;
132
+ if (CROSS_REPO_ID_PATTERN.test(normalized)) return normalized;
133
+ throw new Error(`${name} must be a local or repository-qualified semantic ID`);
134
+ }
135
+
136
+ function normalizeSourcePath(value, name) {
137
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${name} must be a non-empty relative path`);
138
+ if (value.includes("\\") || /^(?:[A-Za-z]:|\/|~\/)/u.test(value) || value.split("/").includes("..") || /\p{Cc}/u.test(value)) {
139
+ throw new Error(`${name} must be a safe forward-slash relative path`);
140
+ }
141
+ return toPosix(value);
142
+ }
143
+
144
+ function assertObject(value, name) {
145
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object`);
146
+ }
147
+
148
+ function assertKeys(value, allowed, name) {
149
+ for (const key of Object.keys(value)) {
150
+ if (!allowed.has(key)) throw new Error(`${name} contains unknown property ${JSON.stringify(key)}`);
151
+ }
152
+ }
153
+
154
+ function compareReferences(left, right) {
155
+ return compareText(left.from, right.from) || compareText(left.to, right.to) || compareText(left.kind, right.kind) ||
156
+ compareText(left.path ?? "", right.path ?? "") || (left.line ?? 0) - (right.line ?? 0);
157
+ }
package/src/graph.js ADDED
@@ -0,0 +1,403 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.graph.generate
3
+ role=Build deterministic repository graphs with content-addressed partitions and resolve qualified IDs across workspace nodes.
4
+ owns=graph schema|edge normalization|local import resolution|workspace ID resolution|graph partition invalidation
5
+ excludes=query scoring|workspace file discovery
6
+ search=repository graph|cross repository ID|workspace resolution|edge provenance|graph confidence|incremental graph cache
7
+ rel=workflow>llmnav.graph.import
8
+ rel=workflow>llmnav.index.generate
9
+ stability=architecture
10
+ */
11
+
12
+ import path from "node:path";
13
+ import { compareText, sha256, stableJson, stableStringify, toPosix } from "./util.js";
14
+
15
+ export const GRAPH_SCHEMA_VERSION = 1;
16
+ export const GRAPH_STATE_SCHEMA_VERSION = 1;
17
+ const IMPORT_EXTENSIONS = Object.freeze([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".go", ".rs", ".py"]);
18
+
19
+ export function buildRepositoryGraph(project, index) {
20
+ return buildRepositoryGraphIncremental(project, index).graph;
21
+ }
22
+
23
+ export function buildRepositoryGraphIncremental(project, index, previousState = null) {
24
+ const repositoryId = index.repositoryId;
25
+ const cardsByPath = groupCardsByPath(index.cards);
26
+ const resolutionHash = sha256(stableJson(
27
+ [...cardsByPath.entries()]
28
+ .sort(([left], [right]) => compareText(left, right))
29
+ .map(([file, cards]) => [file, cards.map((card) => card.id).sort(compareText)]),
30
+ ));
31
+ const previousPartitions = compatibleGraphState(previousState, repositoryId)
32
+ ? new Map(previousState.partitions.map((partition) => [partition.key, partition]))
33
+ : new Map();
34
+ const partitions = [];
35
+ let reusedPartitions = 0;
36
+ let rebuiltPartitions = 0;
37
+
38
+ for (const card of index.cards) {
39
+ const key = `card:${qualifyId(card.id, repositoryId)}`;
40
+ const inputHash = sha256(stableJson({
41
+ resolutionHash,
42
+ id: card.id,
43
+ role: card.role,
44
+ rel: card.rel ?? [],
45
+ imports: card.imports ?? [],
46
+ location: card.location,
47
+ }));
48
+ const previous = previousPartitions.get(key);
49
+ if (previous?.inputHash === inputHash) {
50
+ partitions.push(previous);
51
+ reusedPartitions += 1;
52
+ } else {
53
+ partitions.push(buildCardPartition(key, inputHash, card, repositoryId, cardsByPath));
54
+ rebuiltPartitions += 1;
55
+ }
56
+ }
57
+
58
+ for (const imported of project.graphInputs ?? []) {
59
+ const key = `input:${toPosix(imported.file)}`;
60
+ const inputHash = imported.contentHash || sha256(stableJson(imported));
61
+ const previous = previousPartitions.get(key);
62
+ if (previous?.inputHash === inputHash) {
63
+ partitions.push(previous);
64
+ reusedPartitions += 1;
65
+ } else {
66
+ partitions.push(buildImportedPartition(key, inputHash, imported, repositoryId));
67
+ rebuiltPartitions += 1;
68
+ }
69
+ }
70
+
71
+ partitions.sort((left, right) => compareText(left.key, right.key));
72
+ const nodes = new Map();
73
+ const edges = new Map();
74
+ for (const partition of partitions) {
75
+ for (const node of partition.nodes) mergeNode(nodes, node.key, node, repositoryId);
76
+ for (const edge of partition.edges) addEdge(edges, nodes, edge, repositoryId);
77
+ }
78
+
79
+ const serializedNodes = [...nodes.values()].map(finalizeNode).sort((left, right) => compareText(left.key, right.key));
80
+ const serializedEdges = [...edges.values()].sort(compareEdges);
81
+ const sourceHash = sha256(stableJson({ nodes: serializedNodes, edges: serializedEdges }));
82
+ const graph = {
83
+ schemaVersion: GRAPH_SCHEMA_VERSION,
84
+ repositoryId,
85
+ sourceHash,
86
+ nodes: serializedNodes,
87
+ edges: serializedEdges,
88
+ stats: {
89
+ nodeCount: serializedNodes.length,
90
+ edgeCount: serializedEdges.length,
91
+ unresolvedNodeCount: serializedNodes.filter((node) => node.unresolved).length,
92
+ importedIndexCount: project.graphInputs?.length ?? 0,
93
+ },
94
+ };
95
+ const state = {
96
+ schemaVersion: GRAPH_STATE_SCHEMA_VERSION,
97
+ repositoryId,
98
+ resolutionHash,
99
+ partitions,
100
+ };
101
+ return {
102
+ graph,
103
+ state,
104
+ stats: {
105
+ totalPartitions: partitions.length,
106
+ reusedPartitions,
107
+ rebuiltPartitions,
108
+ removedPartitions: [...previousPartitions.keys()].filter((key) => !partitions.some((item) => item.key === key)).length,
109
+ },
110
+ };
111
+ }
112
+
113
+ export function renderRepositoryGraph(graph) {
114
+ return stableStringify(graph);
115
+ }
116
+
117
+ export function renderGraphState(state) {
118
+ return stableStringify(state);
119
+ }
120
+
121
+ export function compatibleGraphState(state, repositoryId = undefined) {
122
+ const structurallyValid = Boolean(
123
+ state &&
124
+ state.schemaVersion === GRAPH_STATE_SCHEMA_VERSION &&
125
+ typeof state.repositoryId === "string" &&
126
+ typeof state.resolutionHash === "string" &&
127
+ Array.isArray(state.partitions) &&
128
+ state.partitions.every((partition) =>
129
+ partition &&
130
+ typeof partition.key === "string" &&
131
+ typeof partition.inputHash === "string" &&
132
+ typeof partition.outputHash === "string" &&
133
+ Array.isArray(partition.nodes) &&
134
+ Array.isArray(partition.edges) &&
135
+ partition.nodes.every((node) => node && typeof node.key === "string" && Array.isArray(node.definitions)) &&
136
+ partition.edges.every(isReusableEdge) &&
137
+ partition.outputHash === sha256(stableJson({ nodes: partition.nodes, edges: partition.edges }))
138
+ ) &&
139
+ (repositoryId === undefined || state.repositoryId === repositoryId),
140
+ );
141
+ if (!structurallyValid) return false;
142
+ return new Set(state.partitions.map((partition) => partition.key)).size === state.partitions.length;
143
+ }
144
+
145
+ export function isCompatibleRepositoryGraph(graph, repositoryId = undefined) {
146
+ return Boolean(
147
+ graph &&
148
+ graph.schemaVersion === GRAPH_SCHEMA_VERSION &&
149
+ typeof graph.repositoryId === "string" &&
150
+ typeof graph.sourceHash === "string" &&
151
+ Array.isArray(graph.nodes) &&
152
+ Array.isArray(graph.edges) &&
153
+ graph.edges.every((edge) =>
154
+ edge &&
155
+ typeof edge.id === "string" &&
156
+ typeof edge.from === "string" &&
157
+ typeof edge.to === "string" &&
158
+ typeof edge.kind === "string" &&
159
+ typeof edge.confidence === "number" &&
160
+ edge.confidence >= 0 &&
161
+ edge.confidence <= 1 &&
162
+ edge.provenance &&
163
+ typeof edge.provenance.type === "string"
164
+ ) &&
165
+ (repositoryId === undefined || graph.repositoryId === repositoryId),
166
+ );
167
+ }
168
+
169
+ export function resolveGraphNode(graph, id, localRepositoryId = graph?.repositoryId) {
170
+ if (!isCompatibleRepositoryGraph(graph)) return { state: "missing", id, candidates: [], node: null };
171
+ const value = String(id).trim();
172
+ const byKey = new Map(graph.nodes.map((node) => [node.key, node]));
173
+ if (value.includes("/")) {
174
+ const node = byKey.get(value) ?? null;
175
+ return node
176
+ ? { state: "resolved", id: value, candidates: [value], node }
177
+ : { state: "missing", id: value, candidates: [], node: null };
178
+ }
179
+ const localKey = `${localRepositoryId}/${value}`;
180
+ if (byKey.has(localKey)) return { state: "resolved", id: localKey, candidates: [localKey], node: byKey.get(localKey) };
181
+ const candidates = graph.nodes.filter((node) => node.semanticId === value).map((node) => node.key).sort(compareText);
182
+ if (candidates.length === 1) return { state: "resolved", id: candidates[0], candidates, node: byKey.get(candidates[0]) };
183
+ if (candidates.length > 1) return { state: "ambiguous", id: value, candidates, node: null };
184
+ return { state: "missing", id: value, candidates: [], node: null };
185
+ }
186
+
187
+ export function renderGraphNode(node) {
188
+ const lines = [`@${node.key}`];
189
+ if (node.role) lines.push(`role ${node.role}`);
190
+ if (node.external) lines.push("external true");
191
+ if (node.unresolved) lines.push("unresolved true");
192
+ for (const definition of node.definitions ?? []) {
193
+ const location = `${definition.path}${definition.line ? `:${definition.line}` : ""}`;
194
+ lines.push(`def ${definition.symbol} ${location}${definition.kind ? ` kind=${definition.kind}` : ""}`);
195
+ }
196
+ return lines.join("\n");
197
+ }
198
+
199
+ function buildCardPartition(partitionKey, inputHash, card, repositoryId, cardsByPath) {
200
+ const nodes = new Map();
201
+ const edges = new Map();
202
+ const key = qualifyId(card.id, repositoryId);
203
+ mergeNode(nodes, key, {
204
+ role: card.role,
205
+ location: card.location,
206
+ definitions: card.location?.symbol ? [{
207
+ symbol: card.location.symbol,
208
+ path: card.location.path,
209
+ line: card.location.declarationLine,
210
+ kind: card.location.kind,
211
+ provenance: { type: "llmnav-index", source: ".llmnav/cache/index.json", generator: null },
212
+ }] : [],
213
+ }, repositoryId);
214
+
215
+ for (const relation of card.rel ?? []) {
216
+ const separator = relation.indexOf(">");
217
+ if (separator <= 0) continue;
218
+ addEdge(edges, nodes, {
219
+ from: key,
220
+ to: qualifyId(relation.slice(separator + 1), repositoryId),
221
+ kind: relation.slice(0, separator),
222
+ confidence: 1,
223
+ provenance: {
224
+ type: "source-card",
225
+ source: card.location.path,
226
+ path: card.location.path,
227
+ line: card.location.startLine,
228
+ generator: null,
229
+ },
230
+ }, repositoryId);
231
+ }
232
+
233
+ for (const specifier of card.imports ?? []) {
234
+ for (const target of resolveLocalImport(card.location.path, specifier, cardsByPath)) {
235
+ addEdge(edges, nodes, {
236
+ from: key,
237
+ to: qualifyId(target.id, repositoryId),
238
+ kind: "imports",
239
+ confidence: 0.85,
240
+ provenance: {
241
+ type: "local-import",
242
+ source: card.location.path,
243
+ path: card.location.path,
244
+ line: card.location.declarationLine,
245
+ generator: "llmnav",
246
+ },
247
+ }, repositoryId);
248
+ }
249
+ }
250
+ return serializePartition(partitionKey, inputHash, nodes, edges);
251
+ }
252
+
253
+ function buildImportedPartition(partitionKey, inputHash, imported, repositoryId) {
254
+ const nodes = new Map();
255
+ const edges = new Map();
256
+ for (const definition of imported.definitions) {
257
+ mergeNode(nodes, definition.id, {
258
+ definitions: [{
259
+ symbol: definition.symbol,
260
+ path: definition.path,
261
+ line: definition.line,
262
+ kind: definition.kind,
263
+ provenance: {
264
+ type: "generated-index",
265
+ source: imported.file,
266
+ generator: imported.generator,
267
+ },
268
+ }],
269
+ }, repositoryId);
270
+ }
271
+ for (const reference of imported.references) {
272
+ addEdge(edges, nodes, {
273
+ from: reference.from,
274
+ to: reference.to,
275
+ kind: reference.kind,
276
+ confidence: reference.confidence,
277
+ provenance: {
278
+ type: "generated-index",
279
+ source: imported.file,
280
+ path: reference.path,
281
+ line: reference.line,
282
+ generator: imported.generator,
283
+ },
284
+ }, repositoryId);
285
+ }
286
+ return serializePartition(partitionKey, inputHash, nodes, edges);
287
+ }
288
+
289
+ function serializePartition(key, inputHash, nodes, edges) {
290
+ const serializedNodes = [...nodes.values()].sort((left, right) => compareText(left.key, right.key));
291
+ const serializedEdges = [...edges.values()].sort(compareEdges);
292
+ return {
293
+ key,
294
+ inputHash,
295
+ outputHash: sha256(stableJson({ nodes: serializedNodes, edges: serializedEdges })),
296
+ nodes: serializedNodes,
297
+ edges: serializedEdges,
298
+ };
299
+ }
300
+
301
+ function isReusableEdge(edge) {
302
+ return Boolean(
303
+ edge &&
304
+ typeof edge.from === "string" &&
305
+ typeof edge.to === "string" &&
306
+ typeof edge.kind === "string" &&
307
+ typeof edge.confidence === "number" &&
308
+ edge.confidence >= 0 &&
309
+ edge.confidence <= 1 &&
310
+ edge.provenance &&
311
+ typeof edge.provenance.type === "string" &&
312
+ typeof edge.provenance.source === "string",
313
+ );
314
+ }
315
+
316
+ function addEdge(edges, nodes, edge, localRepositoryId) {
317
+ mergeNode(nodes, edge.from, {}, localRepositoryId);
318
+ mergeNode(nodes, edge.to, {}, localRepositoryId);
319
+ const normalized = {
320
+ from: edge.from,
321
+ to: edge.to,
322
+ kind: edge.kind,
323
+ confidence: Number(edge.confidence),
324
+ provenance: {
325
+ type: edge.provenance.type,
326
+ source: toPosix(edge.provenance.source),
327
+ path: edge.provenance.path ? toPosix(edge.provenance.path) : null,
328
+ line: edge.provenance.line ?? null,
329
+ generator: edge.provenance.generator ?? null,
330
+ },
331
+ };
332
+ const identity = stableJson(normalized);
333
+ if (edges.has(identity)) return;
334
+ edges.set(identity, { id: sha256(identity), ...normalized });
335
+ }
336
+
337
+ function mergeNode(nodes, key, input, localRepositoryId) {
338
+ const separator = key.indexOf("/");
339
+ const repositoryId = separator >= 0 ? key.slice(0, separator) : localRepositoryId;
340
+ const semanticId = separator >= 0 ? key.slice(separator + 1) : key;
341
+ const current = nodes.get(key) ?? {
342
+ key,
343
+ repositoryId,
344
+ semanticId,
345
+ role: null,
346
+ location: null,
347
+ definitions: [],
348
+ external: repositoryId !== localRepositoryId,
349
+ };
350
+ if (input.role) current.role = input.role;
351
+ if (input.location) current.location = input.location;
352
+ current.definitions.push(...(input.definitions ?? []));
353
+ nodes.set(key, current);
354
+ }
355
+
356
+ function finalizeNode(node) {
357
+ const definitions = [...new Map(node.definitions.map((definition) => [stableJson(definition), definition])).values()]
358
+ .sort((left, right) => compareText(left.path, right.path) || (left.line ?? 0) - (right.line ?? 0) || compareText(left.symbol, right.symbol));
359
+ return {
360
+ ...node,
361
+ unresolved: !node.role && definitions.length === 0,
362
+ definitions,
363
+ };
364
+ }
365
+
366
+ function qualifyId(id, repositoryId) {
367
+ return String(id).includes("/") ? String(id) : `${repositoryId}/${id}`;
368
+ }
369
+
370
+ function groupCardsByPath(cards) {
371
+ const output = new Map();
372
+ for (const card of cards) {
373
+ const file = toPosix(card.location.path);
374
+ const records = output.get(file) ?? [];
375
+ records.push(card);
376
+ output.set(file, records);
377
+ }
378
+ return output;
379
+ }
380
+
381
+ function resolveLocalImport(sourcePath, specifier, cardsByPath) {
382
+ if (!specifier.startsWith(".")) return [];
383
+ const base = path.posix.normalize(path.posix.join(path.posix.dirname(toPosix(sourcePath)), specifier));
384
+ if (base.startsWith("../")) return [];
385
+ const candidates = new Set([base]);
386
+ const extension = path.posix.extname(base);
387
+ if (!extension) {
388
+ for (const item of IMPORT_EXTENSIONS) {
389
+ candidates.add(`${base}${item}`);
390
+ candidates.add(`${base}/index${item}`);
391
+ }
392
+ } else if ([".js", ".jsx", ".mjs", ".cjs"].includes(extension)) {
393
+ const stem = base.slice(0, -extension.length);
394
+ for (const item of [".ts", ".tsx", ".mts", ".cts"]) candidates.add(`${stem}${item}`);
395
+ }
396
+ return [...candidates].flatMap((candidate) => cardsByPath.get(candidate) ?? []);
397
+ }
398
+
399
+ function compareEdges(left, right) {
400
+ return compareText(left.from, right.from) || compareText(left.to, right.to) || compareText(left.kind, right.kind) ||
401
+ compareText(left.provenance.type, right.provenance.type) || compareText(left.provenance.source, right.provenance.source) ||
402
+ (left.provenance.line ?? 0) - (right.provenance.line ?? 0);
403
+ }