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
package/src/search.js ADDED
@@ -0,0 +1,636 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.search.query
3
+ role=Rank cards from task language and pack confidence-weighted repository graph context.
4
+ owns=query ranking|graph ranking|multilingual aliases|context expansion
5
+ excludes=embedding generation|source mutation
6
+ search=semantic code search|graph-aware ranking|agent navigation|multilingual query
7
+ rel=workflow>llmnav.index.generate
8
+ rel=workflow>llmnav.index.inverted
9
+ rel=workflow>llmnav.graph.generate
10
+ stability=architecture
11
+ */
12
+
13
+ import path from "node:path";
14
+ import { loadConfig } from "./config.js";
15
+ import { resolveRegistryId, loadRegistry } from "./registry.js";
16
+ import { renderCompactCard } from "./generator.js";
17
+ import { approximateTokens, compareText, readJsonSafe, readText, sha256, toPosix, truncateToTokenBudget } from "./util.js";
18
+ import {
19
+ buildInvertedIndex,
20
+ isCompatibleSearchIndex,
21
+ SEARCH_FIELD_ORDER,
22
+ SEARCH_FIELD_WEIGHTS,
23
+ verifySearchIndex,
24
+ } from "./inverted-index.js";
25
+ import { normalizeSearchText, tokenize } from "./tokenizer.js";
26
+ import { recoverGenerationTransaction } from "./transaction.js";
27
+ import { isCompatibleRepositoryGraph, renderGraphNode, resolveGraphNode } from "./graph.js";
28
+
29
+ const preparedIndexCache = new WeakMap();
30
+ const preparedSearchIndexCache = new WeakMap();
31
+
32
+ export async function loadSearchData(root) {
33
+ const { config } = await loadConfig(root);
34
+ await recoverGenerationTransaction(root, { cacheDirectory: config.generation.cacheDirectory });
35
+ const cacheRoot = path.join(root, config.generation.cacheDirectory);
36
+ const index = await readJsonSafe(path.join(cacheRoot, "index.json"), null);
37
+ if (!index) throw new Error("No generated index found. Run `llmnav generate` first.");
38
+ const lexicon = await readJsonSafe(path.join(root, ".llmnav", "lexicon.json"), { version: 1, aliases: {} });
39
+ const manifest = await readJsonSafe(path.join(cacheRoot, "manifest.json"), null);
40
+ const graphRelative = `${toPosix(config.generation.cacheDirectory).replace(/\/+$/u, "")}/graph.json`;
41
+ const graphText = await readText(path.join(cacheRoot, "graph.json"), null);
42
+ let graph = null;
43
+ if (graphText !== null) {
44
+ try {
45
+ graph = JSON.parse(graphText);
46
+ } catch {
47
+ graph = null;
48
+ }
49
+ }
50
+ const graphMatches = Boolean(
51
+ graphText !== null &&
52
+ manifest?.files?.[graphRelative] === sha256(graphText) &&
53
+ isCompatibleRepositoryGraph(graph, index.repositoryId),
54
+ );
55
+ if (!graphMatches) graph = null;
56
+ const searchRelative = `${toPosix(config.generation.cacheDirectory).replace(/\/+$/u, "")}/search-index.json`;
57
+ const searchText = await readText(path.join(cacheRoot, "search-index.json"), null);
58
+ let searchIndex = null;
59
+ if (searchText !== null) {
60
+ try {
61
+ searchIndex = JSON.parse(searchText);
62
+ } catch {
63
+ searchIndex = null;
64
+ }
65
+ }
66
+ const manifestMatches = Boolean(
67
+ searchText !== null &&
68
+ manifest?.searchCardSetHash &&
69
+ searchIndex?.cardSetHash === manifest.searchCardSetHash &&
70
+ searchIndex?.documentCount === index.cards.length &&
71
+ manifest.files?.[searchRelative] === sha256(searchText),
72
+ );
73
+ if (!isCompatibleSearchIndex(searchIndex, index.repositoryId) || (!manifestMatches && !verifySearchIndex(index, searchIndex))) {
74
+ searchIndex = buildInvertedIndex(index).searchIndex;
75
+ }
76
+ return { index, lexicon, searchIndex, graph };
77
+ }
78
+
79
+ export async function queryProject(root, query, options = {}) {
80
+ const { index, lexicon, searchIndex, graph } = await loadSearchData(root);
81
+ return queryPreparedIndex(index, searchIndex, query, { ...options, lexicon, graph });
82
+ }
83
+
84
+ export async function createProjectSession(root) {
85
+ let snapshot = await loadProjectSnapshot(root);
86
+ const session = {
87
+ root,
88
+ query(query, options = {}) {
89
+ return queryPreparedIndex(snapshot.index, snapshot.searchIndex, query, {
90
+ ...options,
91
+ lexicon: snapshot.lexicon,
92
+ graph: snapshot.graph,
93
+ });
94
+ },
95
+ show(id) {
96
+ return showSnapshotCard(snapshot, id);
97
+ },
98
+ context(id, options = {}) {
99
+ return buildSnapshotContext(snapshot, id, options);
100
+ },
101
+ async refresh() {
102
+ snapshot = await loadProjectSnapshot(root);
103
+ return session;
104
+ },
105
+ };
106
+ return session;
107
+ }
108
+
109
+ async function loadProjectSnapshot(root) {
110
+ const { index, lexicon, searchIndex, graph } = await loadSearchData(root);
111
+ const registry = await loadRegistry(root);
112
+ return { index, lexicon, searchIndex, graph, registry };
113
+ }
114
+
115
+ export function queryIndex(index, query, options = {}) {
116
+ let searchIndex = options.invertedIndex;
117
+ if (!searchIndex) {
118
+ searchIndex = preparedIndexCache.get(index);
119
+ if (!searchIndex) {
120
+ searchIndex = buildInvertedIndex(index).searchIndex;
121
+ preparedIndexCache.set(index, searchIndex);
122
+ }
123
+ }
124
+ return queryPreparedIndex(index, searchIndex, query, options);
125
+ }
126
+
127
+ export function queryPreparedIndex(index, searchIndex, query, options = {}) {
128
+ const top = boundedInteger(options.top, 5, 1, 100);
129
+ const lexicon = options.lexicon ?? { aliases: {} };
130
+ const metrics = options.metrics ?? null;
131
+ const normalizedQuery = normalizeSearchText(query);
132
+ const queryTokens = tokenize(query);
133
+ const aliases = Object.entries(lexicon.aliases ?? {});
134
+ const aliasTargets = new Set();
135
+ const aliasReasons = new Map();
136
+ const byId = new Map(index.cards.map((card) => [card.id, card]));
137
+ const cardOrder = new Map(index.cards.map((card, cardIndex) => [card.id, cardIndex]));
138
+ const resultsById = new Map();
139
+
140
+ if (metrics) {
141
+ metrics.queryTokens = queryTokens.length;
142
+ metrics.documentTokenizations = 0;
143
+ metrics.postingVisits = 0;
144
+ metrics.phraseDocumentsScanned = 0;
145
+ metrics.idDocumentsScanned = 0;
146
+ metrics.graphEdgesVisited = 0;
147
+ }
148
+
149
+ for (const [alias, targetValue] of aliases) {
150
+ const normalizedAlias = normalizeSearchText(alias);
151
+ if (!normalizedAlias || !normalizedQuery.includes(normalizedAlias)) continue;
152
+ const targets = Array.isArray(targetValue) ? targetValue : [targetValue];
153
+ for (const target of targets) {
154
+ aliasTargets.add(target);
155
+ const reasons = aliasReasons.get(target) ?? [];
156
+ reasons.push(`alias=${JSON.stringify(alias)}`);
157
+ aliasReasons.set(target, reasons);
158
+ }
159
+ }
160
+
161
+ for (const card of index.cards) {
162
+ if (metrics) metrics.idDocumentsScanned += 1;
163
+ if (normalizeSearchText(card.id) === normalizedQuery) {
164
+ addScore(resultsById, card, 1000, "exact semantic ID");
165
+ } else if (normalizeSearchText(card.id).includes(normalizedQuery) && normalizedQuery.length > 2) {
166
+ addScore(resultsById, card, 100, "semantic ID phrase");
167
+ }
168
+ if (aliasTargets.has(card.id)) {
169
+ addScore(resultsById, card, 500, aliasReasons.get(card.id) ?? []);
170
+ }
171
+ }
172
+
173
+ const preparedSearchIndex = prepareCompactSearchIndex(searchIndex);
174
+ const documentCount = index.cards.length;
175
+ for (const token of queryTokens) {
176
+ const tokenIndex = preparedSearchIndex.tokenIndex.get(token);
177
+ const tokenPostings = tokenIndex === undefined ? [] : (searchIndex.postings[tokenIndex] ?? []);
178
+ const frequency = tokenPostings.length;
179
+ const idf = Math.log(1 + (documentCount + 1) / (frequency + 1));
180
+ for (const [cardIndex, vector] of tokenPostings) {
181
+ if (metrics) metrics.postingVisits += 1;
182
+ const id = searchIndex.cardIds[cardIndex];
183
+ const card = byId.get(id);
184
+ if (!card) continue;
185
+ let tokenScore = 0;
186
+ for (let vectorIndex = 0; vectorIndex < vector.length; vectorIndex += 2) {
187
+ const field = SEARCH_FIELD_ORDER[vector[vectorIndex]];
188
+ const count = vector[vectorIndex + 1] ?? 0;
189
+ if (field) tokenScore += count * SEARCH_FIELD_WEIGHTS[field] * idf;
190
+ }
191
+ if (tokenScore > 0) addScore(resultsById, card, tokenScore, `token=${token}`);
192
+ }
193
+ }
194
+
195
+ if (normalizedQuery.length >= 4) {
196
+ for (const [cardIndex, document] of searchIndex.documents.entries()) {
197
+ if (metrics) metrics.phraseDocumentsScanned += 1;
198
+ const phrases = Array.isArray(document) && Array.isArray(document[1]) ? document[1] : [];
199
+ if (!phrases.some((field) => field.includes(normalizedQuery))) continue;
200
+ const card = byId.get(searchIndex.cardIds[cardIndex]);
201
+ if (card) addScore(resultsById, card, 40, "exact phrase");
202
+ }
203
+ }
204
+
205
+ const results = [...resultsById.values()].filter((result) => result.score > 0);
206
+ const seeds = [...results]
207
+ .sort((left, right) => right.score - left.score || (cardOrder.get(left.card.id) ?? 0) - (cardOrder.get(right.card.id) ?? 0))
208
+ .slice(0, 3);
209
+ if (isCompatibleRepositoryGraph(options.graph, index.repositoryId)) {
210
+ applyGraphBonuses(index, options.graph, seeds, byId, resultsById, metrics);
211
+ } else {
212
+ applyLegacyRelationBonuses(seeds, byId, resultsById);
213
+ }
214
+
215
+ return [...resultsById.values()]
216
+ .filter((result) => result.score > 0)
217
+ .sort((left, right) => right.score - left.score || compareText(left.card.id, right.card.id))
218
+ .slice(0, top)
219
+ .map((result) => ({
220
+ id: result.card.id,
221
+ score: Number(result.score.toFixed(3)),
222
+ reasons: [...new Set(result.reasons)].slice(0, 6),
223
+ role: result.card.role,
224
+ location: result.card.location,
225
+ card: result.card,
226
+ }));
227
+ }
228
+
229
+ function prepareCompactSearchIndex(searchIndex) {
230
+ let prepared = preparedSearchIndexCache.get(searchIndex);
231
+ if (prepared) return prepared;
232
+ prepared = {
233
+ tokenIndex: new Map((searchIndex.tokens ?? []).map((token, index) => [token, index])),
234
+ };
235
+ preparedSearchIndexCache.set(searchIndex, prepared);
236
+ return prepared;
237
+ }
238
+
239
+ export function queryIndexLegacy(index, query, options = {}) {
240
+ const top = boundedInteger(options.top, 5, 1, 100);
241
+ const lexicon = options.lexicon ?? { aliases: {} };
242
+ const normalizedQuery = normalizeSearchText(query);
243
+ const queryTokens = tokenize(query);
244
+ const aliases = Object.entries(lexicon.aliases ?? {});
245
+ const aliasTargets = new Set();
246
+ const aliasReasons = new Map();
247
+
248
+ for (const [alias, targetValue] of aliases) {
249
+ const normalizedAlias = normalizeSearchText(alias);
250
+ if (!normalizedAlias || !normalizedQuery.includes(normalizedAlias)) continue;
251
+ const targets = Array.isArray(targetValue) ? targetValue : [targetValue];
252
+ for (const target of targets) {
253
+ aliasTargets.add(target);
254
+ const reasons = aliasReasons.get(target) ?? [];
255
+ reasons.push(`alias=${JSON.stringify(alias)}`);
256
+ aliasReasons.set(target, reasons);
257
+ }
258
+ }
259
+
260
+ const documents = index.cards.map((card) => buildLegacyDocument(card));
261
+ const documentFrequency = new Map();
262
+ for (const document of documents) {
263
+ for (const token of new Set(document.allTokens)) {
264
+ documentFrequency.set(token, (documentFrequency.get(token) ?? 0) + 1);
265
+ }
266
+ }
267
+
268
+ const results = [];
269
+ for (const [cardIndex, card] of index.cards.entries()) {
270
+ const document = documents[cardIndex];
271
+ let score = 0;
272
+ const reasons = [];
273
+
274
+ if (normalizeSearchText(card.id) === normalizedQuery) {
275
+ score += 1000;
276
+ reasons.push("exact semantic ID");
277
+ } else if (normalizeSearchText(card.id).includes(normalizedQuery) && normalizedQuery.length > 2) {
278
+ score += 100;
279
+ reasons.push("semantic ID phrase");
280
+ }
281
+
282
+ if (aliasTargets.has(card.id)) {
283
+ score += 500;
284
+ reasons.push(...(aliasReasons.get(card.id) ?? []));
285
+ }
286
+
287
+ for (const token of queryTokens) {
288
+ const frequency = documentFrequency.get(token) ?? 0;
289
+ const idf = Math.log(1 + (index.cards.length + 1) / (frequency + 1));
290
+ let tokenScore = 0;
291
+ for (const [field, weight] of Object.entries(SEARCH_FIELD_WEIGHTS)) {
292
+ const count = document.fields[field]?.filter((item) => item === token).length ?? 0;
293
+ tokenScore += count * weight * idf;
294
+ }
295
+ if (tokenScore > 0) {
296
+ score += tokenScore;
297
+ reasons.push(`token=${token}`);
298
+ }
299
+ }
300
+
301
+ const phraseFields = [card.role, ...(card.search ?? []), ...(card.invariant ?? []), ...(card.owns ?? [])]
302
+ .filter(Boolean)
303
+ .map(normalizeSearchText);
304
+ if (normalizedQuery.length >= 4 && phraseFields.some((field) => field.includes(normalizedQuery))) {
305
+ score += 40;
306
+ reasons.push("exact phrase");
307
+ }
308
+
309
+ if (score > 0) results.push({ card, score, reasons: [...new Set(reasons)].slice(0, 6) });
310
+ }
311
+
312
+ const byId = new Map(results.map((result) => [result.card.id, result]));
313
+ const seeds = [...results].sort((left, right) => right.score - left.score).slice(0, 3);
314
+ for (const seed of seeds) {
315
+ for (const relation of seed.card.rel ?? []) {
316
+ const separator = relation.indexOf(">");
317
+ if (separator <= 0) continue;
318
+ const target = relation.slice(separator + 1);
319
+ const targetCard = index.cards.find((card) => card.id === target);
320
+ if (!targetCard) continue;
321
+ const existing = byId.get(target);
322
+ const bonus = seed.score * 0.08;
323
+ if (existing) {
324
+ existing.score += bonus;
325
+ existing.reasons.push(`related-from=${seed.card.id}`);
326
+ } else {
327
+ const result = { card: targetCard, score: bonus, reasons: [`related-from=${seed.card.id}`] };
328
+ results.push(result);
329
+ byId.set(target, result);
330
+ }
331
+ }
332
+ }
333
+
334
+ return results
335
+ .sort((left, right) => right.score - left.score || compareText(left.card.id, right.card.id))
336
+ .slice(0, top)
337
+ .map((result) => ({
338
+ id: result.card.id,
339
+ score: Number(result.score.toFixed(3)),
340
+ reasons: [...new Set(result.reasons)].slice(0, 6),
341
+ role: result.card.role,
342
+ location: result.card.location,
343
+ card: result.card,
344
+ }));
345
+ }
346
+
347
+ export async function showProjectCard(root, id) {
348
+ return showSnapshotCard(await loadProjectSnapshot(root), id);
349
+ }
350
+
351
+ function showSnapshotCard(snapshot, id) {
352
+ const { index, graph, registry } = snapshot;
353
+ const direct = index.cards.find((card) => card.id === id);
354
+ if (direct) return { card: direct, node: null, resolvedFrom: null };
355
+ if (!String(id).includes("/")) {
356
+ const resolved = resolveRegistryId(registry, id);
357
+ if (resolved.state === "active") {
358
+ const card = index.cards.find((item) => item.id === resolved.id) ?? null;
359
+ if (card) return { card, node: null, resolvedFrom: resolved };
360
+ }
361
+ if (resolved.state === "ambiguous" || resolved.state === "cycle") {
362
+ return { card: null, node: null, resolvedFrom: resolved };
363
+ }
364
+ }
365
+ const graphResolution = resolveGraphNode(graph, id, index.repositoryId);
366
+ if (graphResolution.state !== "resolved") return { card: null, node: null, resolvedFrom: graphResolution };
367
+ const localId = localSemanticId(graphResolution.node.key, index.repositoryId);
368
+ const card = localId ? index.cards.find((item) => item.id === localId) ?? null : null;
369
+ return { card, node: card ? null : graphResolution.node, resolvedFrom: graphResolution };
370
+ }
371
+
372
+ export async function buildContext(root, id, options = {}) {
373
+ return buildSnapshotContext(await loadProjectSnapshot(root), id, options);
374
+ }
375
+
376
+ function buildSnapshotContext(snapshot, id, options = {}) {
377
+ const { index, graph, registry } = snapshot;
378
+ const depth = boundedInteger(options.depth, 1, 0, 8);
379
+ const budget = boundedInteger(options.budget, 2500, 128, 100000);
380
+ const maxEdges = boundedInteger(options.maxEdges, 24, 0, 1000);
381
+ const direct = index.cards.find((card) => card.id === id);
382
+ let rootId = direct?.id ?? null;
383
+ if (!rootId && !String(id).includes("/")) {
384
+ const resolved = resolveRegistryId(registry, id);
385
+ if (resolved.state === "active" && index.cards.some((card) => card.id === resolved.id)) rootId = resolved.id;
386
+ if (resolved.state === "ambiguous") {
387
+ throw new Error(`Ambiguous replaced semantic ID ${id}; choose one of ${resolved.candidates.join(", ")}.`);
388
+ }
389
+ if (resolved.state === "cycle") throw new Error(`Registry cycle prevents resolving semantic ID ${id}.`);
390
+ }
391
+ if (isCompatibleRepositoryGraph(graph, index.repositoryId)) {
392
+ const resolution = rootId
393
+ ? resolveGraphNode(graph, `${index.repositoryId}/${rootId}`, index.repositoryId)
394
+ : resolveGraphNode(graph, id, index.repositoryId);
395
+ if (resolution.state === "ambiguous") {
396
+ throw new Error(`Ambiguous semantic ID ${id}; qualify one of ${resolution.candidates.join(", ")}.`);
397
+ }
398
+ if (resolution.state !== "resolved") throw new Error(`Unknown or inactive semantic ID ${id}.`);
399
+ return buildGraphContext(index, graph, resolution.node, { depth, budget, maxEdges });
400
+ }
401
+ if (!rootId) throw new Error(`Unknown or inactive semantic ID ${id}.`);
402
+ return buildLegacyContext(index, rootId, { depth, budget, maxEdges });
403
+ }
404
+
405
+ function buildGraphContext(index, graph, rootNode, options) {
406
+ const { depth, budget, maxEdges } = options;
407
+ const byId = new Map(index.cards.map((card) => [card.id, card]));
408
+ const nodesByKey = new Map(graph.nodes.map((node) => [node.key, node]));
409
+ const graphAdjacency = buildGraphAdjacency(graph);
410
+ const queue = [{ key: rootNode.key, depth: 0 }];
411
+ const visited = new Set();
412
+ const selected = [];
413
+ const selectedEdges = [];
414
+ const seenEdges = new Set();
415
+ while (queue.length > 0) {
416
+ const current = queue.shift();
417
+ if (!current || visited.has(current.key)) continue;
418
+ visited.add(current.key);
419
+ const node = nodesByKey.get(current.key);
420
+ if (!node) continue;
421
+ const localId = localSemanticId(node.key, index.repositoryId);
422
+ selected.push({ node, card: localId ? byId.get(localId) ?? null : null });
423
+ if (current.depth >= depth) continue;
424
+ for (const entry of graphAdjacency.get(current.key) ?? []) {
425
+ if (selectedEdges.length >= maxEdges) break;
426
+ if (!seenEdges.has(entry.edge.id)) {
427
+ selectedEdges.push(entry.edge);
428
+ seenEdges.add(entry.edge.id);
429
+ }
430
+ queue.push({ key: entry.neighbor, depth: current.depth + 1 });
431
+ }
432
+ }
433
+
434
+ const rootOutputId = contextNodeId(rootNode, index.repositoryId);
435
+ let output = `llmnav-context/1 root=${rootOutputId} depth=${depth}\n`;
436
+ const included = [];
437
+ for (const item of selected) {
438
+ const rendered = `${item.card ? renderCompactCard(item.card) : renderGraphNode(item.node)}\n\n`;
439
+ if (approximateTokens(output + rendered) > budget) {
440
+ if (included.length === 0) {
441
+ output = truncateToTokenBudget(output + rendered, budget);
442
+ included.push(contextNodeId(item.node, index.repositoryId));
443
+ }
444
+ break;
445
+ }
446
+ output += rendered;
447
+ included.push(contextNodeId(item.node, index.repositoryId));
448
+ }
449
+ const includedEdges = [];
450
+ for (const edge of selectedEdges) {
451
+ const rendered = renderGraphEdge(edge);
452
+ if (approximateTokens(output + rendered) > budget) break;
453
+ output += rendered;
454
+ includedEdges.push(edge.id);
455
+ }
456
+ return {
457
+ id: rootOutputId,
458
+ depth,
459
+ budget,
460
+ maxEdges,
461
+ included,
462
+ includedEdges,
463
+ text: truncateToTokenBudget(output.trimEnd(), budget),
464
+ };
465
+ }
466
+
467
+ function buildLegacyContext(index, rootId, options) {
468
+ const { depth, budget, maxEdges } = options;
469
+ const start = index.cards.find((card) => card.id === rootId);
470
+ if (!start) throw new Error(`No indexed source card exists for semantic ID ${rootId}.`);
471
+ const byId = new Map(index.cards.map((card) => [card.id, card]));
472
+ const reverse = new Map();
473
+ for (const card of index.cards) {
474
+ for (const relation of card.rel ?? []) {
475
+ const target = relation.slice(relation.indexOf(">") + 1);
476
+ const list = reverse.get(target) ?? [];
477
+ list.push(card.id);
478
+ reverse.set(target, list);
479
+ }
480
+ }
481
+
482
+ const queue = [{ id: rootId, depth: 0 }];
483
+ const visited = new Set();
484
+ const selected = [];
485
+ while (queue.length > 0) {
486
+ const current = queue.shift();
487
+ if (!current || visited.has(current.id)) continue;
488
+ visited.add(current.id);
489
+ const card = byId.get(current.id);
490
+ if (!card) continue;
491
+ selected.push(card);
492
+ if (current.depth >= depth) continue;
493
+ for (const relation of card.rel ?? []) {
494
+ const target = relation.slice(relation.indexOf(">") + 1);
495
+ queue.push({ id: target, depth: current.depth + 1 });
496
+ }
497
+ for (const source of reverse.get(card.id) ?? []) queue.push({ id: source, depth: current.depth + 1 });
498
+ }
499
+
500
+ const header = `llmnav-context/1 root=${rootId} depth=${depth}\n`;
501
+ let output = header;
502
+ const included = [];
503
+ for (const card of selected) {
504
+ const rendered = `${renderCompactCard(card)}\n\n`;
505
+ if (approximateTokens(output + rendered) > budget) {
506
+ if (included.length === 0) {
507
+ output = truncateToTokenBudget(output + rendered, budget);
508
+ included.push(card.id);
509
+ }
510
+ break;
511
+ }
512
+ output += rendered;
513
+ included.push(card.id);
514
+ }
515
+ return {
516
+ id: rootId,
517
+ depth,
518
+ budget,
519
+ maxEdges,
520
+ included,
521
+ includedEdges: [],
522
+ text: truncateToTokenBudget(output.trimEnd(), budget),
523
+ };
524
+ }
525
+
526
+ function renderGraphEdge(edge) {
527
+ return `graph ${edge.from} -[${edge.kind} confidence=${edge.confidence.toFixed(2)} provenance=${edge.provenance.type}]-> ${edge.to}\n`;
528
+ }
529
+
530
+ function contextNodeId(node, localRepositoryId) {
531
+ return node.repositoryId === localRepositoryId ? node.semanticId : node.key;
532
+ }
533
+
534
+ function applyGraphBonuses(index, graph, seeds, byId, resultsById, metrics) {
535
+ const adjacency = buildGraphAdjacency(graph);
536
+ for (const seed of seeds) {
537
+ const key = `${index.repositoryId}/${seed.card.id}`;
538
+ for (const entry of adjacency.get(key) ?? []) {
539
+ if (metrics) metrics.graphEdgesVisited += 1;
540
+ const target = localSemanticId(entry.neighbor, index.repositoryId);
541
+ const targetCard = target ? byId.get(target) : null;
542
+ if (!targetCard) continue;
543
+ const directionWeight = entry.direction === "out" ? 1 : 0.6;
544
+ const bonus = seed.score * 0.08 * edgeConfidence(entry.edge) * directionWeight;
545
+ if (bonus <= 0) continue;
546
+ addScore(
547
+ resultsById,
548
+ targetCard,
549
+ bonus,
550
+ `graph-${entry.direction}:${entry.edge.kind}@${edgeConfidence(entry.edge).toFixed(2)}`,
551
+ );
552
+ }
553
+ }
554
+ }
555
+
556
+ function applyLegacyRelationBonuses(seeds, byId, resultsById) {
557
+ for (const seed of seeds) {
558
+ for (const relation of seed.card.rel ?? []) {
559
+ const separator = relation.indexOf(">");
560
+ if (separator <= 0) continue;
561
+ const target = relation.slice(separator + 1);
562
+ const targetCard = byId.get(target);
563
+ if (targetCard) addScore(resultsById, targetCard, seed.score * 0.08, `related-from=${seed.card.id}`);
564
+ }
565
+ }
566
+ }
567
+
568
+ function buildGraphAdjacency(graph) {
569
+ const adjacency = new Map();
570
+ for (const edge of [...graph.edges].sort(compareGraphEdgesForTraversal)) {
571
+ appendGraphNeighbor(adjacency, edge.from, { neighbor: edge.to, direction: "out", edge });
572
+ appendGraphNeighbor(adjacency, edge.to, { neighbor: edge.from, direction: "in", edge });
573
+ }
574
+ for (const entries of adjacency.values()) {
575
+ entries.sort((left, right) =>
576
+ edgeConfidence(right.edge) - edgeConfidence(left.edge) ||
577
+ compareText(left.edge.kind, right.edge.kind) ||
578
+ compareText(left.neighbor, right.neighbor) ||
579
+ compareText(left.edge.id, right.edge.id),
580
+ );
581
+ }
582
+ return adjacency;
583
+ }
584
+
585
+ function appendGraphNeighbor(adjacency, key, entry) {
586
+ const values = adjacency.get(key) ?? [];
587
+ values.push(entry);
588
+ adjacency.set(key, values);
589
+ }
590
+
591
+ function localSemanticId(key, repositoryId) {
592
+ const prefix = `${repositoryId}/`;
593
+ return key.startsWith(prefix) ? key.slice(prefix.length) : null;
594
+ }
595
+
596
+ function edgeConfidence(edge) {
597
+ return Math.max(0, Math.min(1, Number(edge.confidence) || 0));
598
+ }
599
+
600
+ function compareGraphEdgesForTraversal(left, right) {
601
+ return compareText(left.from, right.from) || compareText(left.to, right.to) || compareText(left.kind, right.kind) ||
602
+ compareText(left.id, right.id);
603
+ }
604
+
605
+ function addScore(resultsById, card, score, reasons) {
606
+ const existing = resultsById.get(card.id) ?? { card, score: 0, reasons: [] };
607
+ existing.score += score;
608
+ if (Array.isArray(reasons)) existing.reasons.push(...reasons);
609
+ else existing.reasons.push(reasons);
610
+ resultsById.set(card.id, existing);
611
+ }
612
+
613
+ function boundedInteger(value, fallback, minimum, maximum) {
614
+ const parsed = Number.isInteger(value) ? value : Number.parseInt(String(value ?? ""), 10);
615
+ if (!Number.isFinite(parsed)) return fallback;
616
+ return Math.min(maximum, Math.max(minimum, parsed));
617
+ }
618
+
619
+ function buildLegacyDocument(card) {
620
+ const fields = {
621
+ id: tokenize(card.id.replaceAll(".", " ")),
622
+ role: tokenize(card.role),
623
+ search: tokenize((card.search ?? []).join(" ")),
624
+ owns: tokenize((card.owns ?? []).join(" ")),
625
+ excludes: tokenize((card.excludes ?? []).join(" ")),
626
+ invariant: tokenize((card.invariant ?? []).join(" ")),
627
+ effect: tokenize((card.effect ?? []).join(" ")),
628
+ risk: tokenize((card.risk ?? []).join(" ")),
629
+ symbol: tokenize(card.location?.symbol ?? ""),
630
+ path: tokenize(card.location?.path ?? ""),
631
+ signature: tokenize(card.location?.signature ?? ""),
632
+ };
633
+ return { fields, allTokens: Object.values(fields).flat() };
634
+ }
635
+
636
+ export { tokenize } from "./tokenizer.js";
package/src/spec.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { LlmnavConfig, LlmnavScope, LlmnavStability } from "./index.js";
2
+
3
+ export const PACKAGE_VERSION: string;
4
+ export const SPEC_VERSION: string;
5
+ export const SCOPES: readonly LlmnavScope[];
6
+ export const STABILITIES: readonly LlmnavStability[];
7
+ export const KEY_ORDER: readonly string[];
8
+ export const REQUIRED_KEYS: readonly string[];
9
+ export const REPEATABLE_KEYS: readonly string[];
10
+ export const LIST_KEYS: readonly string[];
11
+ export const SCALAR_KEYS: readonly string[];
12
+ export const ALLOWED_KEYS: readonly string[];
13
+ export const EFFECT_KINDS_WITH_ARGUMENT: readonly string[];
14
+ export const EFFECT_KINDS_WITHOUT_ARGUMENT: readonly string[];
15
+ export const EFFECT_KINDS: readonly string[];
16
+ export const RISK_KINDS: readonly string[];
17
+ export const STRICT_RISKS: readonly string[];
18
+ export const RELATION_KINDS: readonly string[];
19
+ export const FORBIDDEN_STRUCTURE_RELATIONS: readonly string[];
20
+ export const FORBIDDEN_VOLATILE_KEYS: readonly string[];
21
+ export const DEFAULT_INCLUDE_EXTENSIONS: readonly string[];
22
+ export const DEFAULT_EXCLUDED_DIRECTORIES: readonly string[];
23
+ export const DEFAULT_GENERIC_SEARCH_TERMS: readonly string[];
24
+ export const DEFAULT_VAGUE_ROLE_WORDS: readonly string[];
25
+ export const ID_PATTERN: RegExp;
26
+ export const CROSS_REPO_ID_PATTERN: RegExp;
27
+ export const DEFAULT_CONFIG: Readonly<LlmnavConfig>;