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/util.js ADDED
@@ -0,0 +1,256 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ export function normalizeNewlines(value) {
6
+ return value.replace(/\r\n?/gu, "\n");
7
+ }
8
+
9
+ export function detectNewline(value) {
10
+ return value.includes("\r\n") ? "\r\n" : "\n";
11
+ }
12
+
13
+ export function splitPipe(value) {
14
+ return value
15
+ .split("|")
16
+ .map((item) => item.trim())
17
+ .filter(Boolean);
18
+ }
19
+
20
+ export function unique(values) {
21
+ return [...new Set(values)];
22
+ }
23
+
24
+ export function stableStringify(value, space = 2) {
25
+ return `${JSON.stringify(sortObject(value), null, space)}\n`;
26
+ }
27
+
28
+ export function stableJson(value) {
29
+ return JSON.stringify(sortObject(value));
30
+ }
31
+
32
+ export function compareText(left, right) {
33
+ const leftText = String(left);
34
+ const rightText = String(right);
35
+ if (leftText < rightText) return -1;
36
+ if (leftText > rightText) return 1;
37
+ return 0;
38
+ }
39
+
40
+ export function sortObject(value) {
41
+ if (Array.isArray(value)) {
42
+ return value.map(sortObject);
43
+ }
44
+ if (value && typeof value === "object") {
45
+ return Object.fromEntries(
46
+ Object.entries(value)
47
+ .sort(([left], [right]) => compareText(left, right))
48
+ .map(([key, child]) => [key, sortObject(child)]),
49
+ );
50
+ }
51
+ return value;
52
+ }
53
+
54
+ export function sha256(value) {
55
+ return createHash("sha256").update(value).digest("hex");
56
+ }
57
+
58
+ export function toPosix(value) {
59
+ return String(value).replace(/\\/gu, "/");
60
+ }
61
+
62
+ export function projectRelativePath(value, label = value) {
63
+ const normalized = toPosix(value);
64
+ if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) {
65
+ throw new Error(`${label} must be a non-empty project-relative path.`);
66
+ }
67
+ if (normalized.split("/").includes("..")) {
68
+ throw new Error(`${label} contains parent-directory traversal.`);
69
+ }
70
+ const canonical = path.posix.normalize(normalized).replace(/^\.\//u, "");
71
+ if (!canonical || canonical === ".") throw new Error(`${label} must name a path below the project root.`);
72
+ return canonical;
73
+ }
74
+
75
+ export function relativePosix(root, absolutePath) {
76
+ return toPosix(path.relative(root, absolutePath));
77
+ }
78
+
79
+ export function lineAtOffset(source, offset) {
80
+ let line = 1;
81
+ for (let index = 0; index < offset; index += 1) {
82
+ if (source.charCodeAt(index) === 10) line += 1;
83
+ }
84
+ return line;
85
+ }
86
+
87
+ export function offsetAtLine(source, targetLine) {
88
+ if (targetLine <= 1) return 0;
89
+ let line = 1;
90
+ for (let index = 0; index < source.length; index += 1) {
91
+ if (source.charCodeAt(index) === 10) {
92
+ line += 1;
93
+ if (line === targetLine) return index + 1;
94
+ }
95
+ }
96
+ return source.length;
97
+ }
98
+
99
+ export async function readText(filePath, fallback = undefined) {
100
+ try {
101
+ return await readFile(filePath, "utf8");
102
+ } catch (error) {
103
+ if (fallback !== undefined && error && typeof error === "object" && error.code === "ENOENT") {
104
+ return fallback;
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ export async function readJson(filePath, fallback = undefined) {
111
+ const text = await readText(filePath, fallback === undefined ? undefined : "");
112
+ if (text === "" && fallback !== undefined) return structuredClone(fallback);
113
+ return JSON.parse(text);
114
+ }
115
+
116
+ export async function readJsonSafe(filePath, fallback = null) {
117
+ try {
118
+ return await readJson(filePath, fallback);
119
+ } catch (error) {
120
+ if (error instanceof SyntaxError) return structuredClone(fallback);
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ export async function atomicWrite(filePath, content) {
126
+ await mkdir(path.dirname(filePath), { recursive: true });
127
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
128
+ await writeFile(temporaryPath, content, "utf8");
129
+ try {
130
+ await rename(temporaryPath, filePath);
131
+ } catch (error) {
132
+ await rm(temporaryPath, { force: true });
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ export async function assertNoSymlinkTraversal(root, targetPath, label = targetPath) {
138
+ const rootPath = path.resolve(root);
139
+ const target = path.resolve(targetPath);
140
+ const relative = path.relative(rootPath, target);
141
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
142
+ throw new Error(`${label} escapes the repository root.`);
143
+ }
144
+
145
+ let current = rootPath;
146
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
147
+ current = path.join(current, segment);
148
+ try {
149
+ const details = await lstat(current);
150
+ if (details.isSymbolicLink()) throw new Error(`${label} traverses symbolic link ${path.relative(rootPath, current)}.`);
151
+ } catch (error) {
152
+ if (error && typeof error === "object" && error.code === "ENOENT") return;
153
+ throw error;
154
+ }
155
+ }
156
+ }
157
+
158
+ export function deepMerge(base, override) {
159
+ if (Array.isArray(base) || Array.isArray(override)) {
160
+ return override === undefined ? structuredClone(base) : structuredClone(override);
161
+ }
162
+ if (base && override && typeof base === "object" && typeof override === "object") {
163
+ const result = structuredClone(base);
164
+ for (const [key, value] of Object.entries(override)) {
165
+ result[key] = key in result ? deepMerge(result[key], value) : structuredClone(value);
166
+ }
167
+ return result;
168
+ }
169
+ return override === undefined ? structuredClone(base) : structuredClone(override);
170
+ }
171
+
172
+ export function parseInteger(value, fallback) {
173
+ if (value === undefined) return fallback;
174
+ const parsed = Number.parseInt(value, 10);
175
+ return Number.isFinite(parsed) ? parsed : fallback;
176
+ }
177
+
178
+ export function approximateTokens(value) {
179
+ const ascii = [...value].filter((character) => character.codePointAt(0) < 128).length;
180
+ const nonAscii = value.length - ascii;
181
+ return Math.max(1, Math.ceil(ascii / 4 + nonAscii / 1.8));
182
+ }
183
+
184
+ export function truncateToTokenBudget(value, budget) {
185
+ if (approximateTokens(value) <= budget) return value;
186
+ let low = 0;
187
+ let high = value.length;
188
+ while (low < high) {
189
+ const middle = Math.ceil((low + high) / 2);
190
+ if (approximateTokens(value.slice(0, middle)) <= budget) low = middle;
191
+ else high = middle - 1;
192
+ }
193
+ return `${value.slice(0, low).trimEnd()}\n…`;
194
+ }
195
+
196
+ export function escapeRegExp(value) {
197
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
198
+ }
199
+
200
+ export function globToRegExp(glob) {
201
+ const normalized = toPosix(glob);
202
+ let pattern = "^";
203
+ for (let index = 0; index < normalized.length; index += 1) {
204
+ const character = normalized[index];
205
+ const next = normalized[index + 1];
206
+ if (character === "*" && next === "*") {
207
+ const after = normalized[index + 2];
208
+ if (after === "/") {
209
+ pattern += "(?:.*/)?";
210
+ index += 2;
211
+ } else {
212
+ pattern += ".*";
213
+ index += 1;
214
+ }
215
+ } else if (character === "*") {
216
+ pattern += "[^/]*";
217
+ } else if (character === "?") {
218
+ pattern += "[^/]";
219
+ } else {
220
+ pattern += escapeRegExp(character);
221
+ }
222
+ }
223
+ pattern += "$";
224
+ return new RegExp(pattern, "u");
225
+ }
226
+
227
+ export function matchesAnyGlob(relativePath, patterns) {
228
+ const normalized = toPosix(relativePath);
229
+ return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
230
+ }
231
+
232
+ export function parseJsonLines(text, sourceName = "JSONL") {
233
+ const records = [];
234
+ const errors = [];
235
+ for (const [index, rawLine] of normalizeNewlines(text).split("\n").entries()) {
236
+ const line = rawLine.trim();
237
+ if (!line || line.startsWith("#")) continue;
238
+ try {
239
+ records.push(JSON.parse(line));
240
+ } catch (error) {
241
+ errors.push(`${sourceName}:${index + 1}: ${error instanceof Error ? error.message : String(error)}`);
242
+ }
243
+ }
244
+ return { records, errors };
245
+ }
246
+
247
+ export function asArray(value) {
248
+ if (value === undefined || value === null) return [];
249
+ return Array.isArray(value) ? value : [value];
250
+ }
251
+
252
+ export function formatBytes(bytes) {
253
+ if (bytes < 1024) return `${bytes} B`;
254
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
255
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
256
+ }