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/cli.js ADDED
@@ -0,0 +1,459 @@
1
+ /* llmnav/1 module
2
+ id=llmnav.cli.dispatch
3
+ role=Expose initialization, validation, generation, search, evaluation, and diagnostics through one CLI.
4
+ owns=command dispatch|terminal output|exit codes
5
+ excludes=semantic rule definitions|index persistence format
6
+ search=llmnav cli|command line|automation interface
7
+ rel=workflow>llmnav.project.initialize
8
+ rel=workflow>llmnav.rules.validate
9
+ rel=workflow>llmnav.search.query
10
+ rel=workflow>llmnav.eval.measure
11
+ stability=architecture
12
+ */
13
+
14
+ import path from "node:path";
15
+ import { findProjectRoot } from "./files.js";
16
+ import { initializeProject } from "./initializer.js";
17
+ import { scanProject } from "./project.js";
18
+ import { countDiagnostics, validateProject } from "./validator.js";
19
+ import { formatProject } from "./formatter.js";
20
+ import { generateProject, renderCompactCard } from "./generator.js";
21
+ import { buildContext, queryProject, showProjectCard } from "./search.js";
22
+ import { evaluateProject } from "./evaluation.js";
23
+ import { doctorProject } from "./doctor.js";
24
+ import {
25
+ EFFECT_KINDS,
26
+ KEY_ORDER,
27
+ PACKAGE_VERSION,
28
+ RELATION_KINDS,
29
+ RISK_KINDS,
30
+ SPEC_VERSION,
31
+ STABILITIES,
32
+ } from "./spec.js";
33
+ import { parseInteger } from "./util.js";
34
+ import { diagnosticsToSarif } from "./sarif.js";
35
+ import { loadGraphInputs } from "./graph-input.js";
36
+ import { renderGraphNode } from "./graph.js";
37
+ import { getAgentToolDefinitions } from "./agent-protocol.js";
38
+ import { loadPromptPrefixBundle } from "./prompt-bundle.js";
39
+ import { diagnosticsToEditor, getEditorIntegration } from "./editor.js";
40
+
41
+ const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file"]);
42
+
43
+ const COMMAND_OPTIONS = Object.freeze({
44
+ init: new Set(["--agents", "--package-scripts", "--force", "--root", "--json"]),
45
+ check: new Set(["--format", "--root", "--json"]),
46
+ format: new Set(["--check", "--root", "--json"]),
47
+ generate: new Set(["--check", "--verify", "--full", "--root", "--json"]),
48
+ index: new Set(["--check", "--verify", "--full", "--root", "--json"]),
49
+ query: new Set(["--top", "--root", "--json"]),
50
+ show: new Set(["--root", "--json"]),
51
+ context: new Set(["--depth", "--budget", "--max-edges", "--root", "--json"]),
52
+ eval: new Set(["--file", "--top", "--root", "--json"]),
53
+ doctor: new Set(["--root", "--json"]),
54
+ spec: new Set(["--root", "--json"]),
55
+ tools: new Set(["--json"]),
56
+ bundle: new Set(["--root", "--json"]),
57
+ editor: new Set(["--json"]),
58
+ help: new Set(["--root", "--json"]),
59
+ });
60
+
61
+ export async function runCli(argv) {
62
+ if (argv.length === 0 || hasFlag(argv, "--help") || hasFlag(argv, "-h")) {
63
+ printHelp();
64
+ return 0;
65
+ }
66
+ if (hasFlag(argv, "--version") || hasFlag(argv, "-v") || argv[0] === "version") {
67
+ console.log(PACKAGE_VERSION);
68
+ return 0;
69
+ }
70
+
71
+ const command = argv[0];
72
+ const args = argv.slice(1);
73
+ validateOptions(command, args);
74
+ const json = hasFlag(args, "--json");
75
+ if (command === "tools") return runTools(json);
76
+ if (command === "editor") return runEditor(args);
77
+ const rootOption = getOption(args, "--root");
78
+ const root = rootOption ? path.resolve(rootOption) : await findProjectRoot(process.cwd());
79
+
80
+ switch (command) {
81
+ case "init":
82
+ return runInit(root, args, json);
83
+ case "check":
84
+ return runCheck(root, args, json);
85
+ case "format":
86
+ return runFormat(root, args, json);
87
+ case "generate":
88
+ case "index":
89
+ return runGenerate(root, args, json);
90
+ case "query":
91
+ return runQuery(root, args, json);
92
+ case "show":
93
+ return runShow(root, args, json);
94
+ case "context":
95
+ return runContext(root, args, json);
96
+ case "eval":
97
+ return runEval(root, args, json);
98
+ case "doctor":
99
+ return runDoctor(root, json);
100
+ case "spec":
101
+ return runSpec(json);
102
+ case "bundle":
103
+ return runBundle(root, json);
104
+ case "help":
105
+ printHelp();
106
+ return 0;
107
+ default:
108
+ console.error(`Unknown command ${JSON.stringify(command)}. Run llmnav --help.`);
109
+ return 2;
110
+ }
111
+ }
112
+
113
+ async function runInit(root, args, json) {
114
+ const agents = (getOption(args, "--agents") ?? "agents").split(",");
115
+ const result = await initializeProject(root, {
116
+ force: hasFlag(args, "--force"),
117
+ packageScripts: hasFlag(args, "--package-scripts"),
118
+ agents,
119
+ });
120
+ if (json) {
121
+ console.log(JSON.stringify({ ok: result.ok, root, changed: result.changed }, null, 2));
122
+ } else {
123
+ console.log(`Initialized LLMNav in ${root}`);
124
+ if (result.changed.length === 0) console.log("No files changed.");
125
+ for (const file of result.changed) console.log(` wrote ${file}`);
126
+ if (!result.ok) printDiagnostics(result.generated.diagnostics, "text");
127
+ }
128
+ return result.ok ? 0 : 1;
129
+ }
130
+
131
+ async function runCheck(root, args, json) {
132
+ const format = json ? "json" : getOption(args, "--format") ?? "text";
133
+ if (!["text", "json", "github", "sarif", "editor"].includes(format)) throw usageError(`Unknown diagnostic format ${JSON.stringify(format)}.`);
134
+ const paths = getPositionals(args);
135
+ const project = await scanProject(root, { paths });
136
+ const graphInputs = await loadGraphInputs(root, project.config);
137
+ const diagnostics = [...validateProject(project), ...graphInputs.diagnostics].sort(compareCliDiagnostics);
138
+ const counts = countDiagnostics(diagnostics);
139
+ const ok = counts.error === 0;
140
+ if (format === "json") {
141
+ console.log(JSON.stringify({ ok, counts, diagnostics }, null, 2));
142
+ } else if (format === "sarif") {
143
+ console.log(JSON.stringify(diagnosticsToSarif(diagnostics), null, 2));
144
+ } else if (format === "editor") {
145
+ console.log(JSON.stringify(diagnosticsToEditor(diagnostics), null, 2));
146
+ } else {
147
+ printDiagnostics(diagnostics, format);
148
+ if (format === "text") {
149
+ console.log(`${counts.error} error(s), ${counts.warning} warning(s), ${project.records.length} card(s)`);
150
+ }
151
+ }
152
+ return ok ? 0 : 1;
153
+ }
154
+
155
+ async function runFormat(root, args, json) {
156
+ const check = hasFlag(args, "--check");
157
+ const result = await formatProject(root, { check, paths: getPositionals(args) });
158
+ if (json) {
159
+ console.log(JSON.stringify(result, null, 2));
160
+ } else {
161
+ for (const error of result.errors) console.error(`${error.file}:${error.line}:1 error LNV010 ${error.message}`);
162
+ if (result.changedFiles.length === 0 && result.errors.length === 0) {
163
+ console.log("All LLMNav blocks are canonical.");
164
+ } else {
165
+ for (const file of result.changedFiles) console.log(`${check ? "would format" : "formatted"} ${file}`);
166
+ }
167
+ }
168
+ return result.ok ? 0 : 1;
169
+ }
170
+
171
+ async function runGenerate(root, args, json) {
172
+ const check = hasFlag(args, "--check") || hasFlag(args, "--verify");
173
+ const result = await generateProject(root, { check, incremental: !hasFlag(args, "--full") });
174
+ if (json) {
175
+ console.log(
176
+ JSON.stringify(
177
+ {
178
+ ok: result.ok,
179
+ changedFiles: result.changedFiles,
180
+ changedCards: result.changedCards,
181
+ affectedBoundaries: result.affectedBoundaries,
182
+ affectedCatalogs: result.affectedCatalogs,
183
+ incremental: result.incremental,
184
+ transaction: result.transaction,
185
+ diagnostics: result.diagnostics,
186
+ },
187
+ null,
188
+ 2,
189
+ ),
190
+ );
191
+ } else {
192
+ if (result.diagnostics.length > 0) printDiagnostics(result.diagnostics, "text");
193
+ if (result.changedFiles.length === 0 && result.ok) {
194
+ console.log(check ? "Generated LLMNav files are current." : "Generated LLMNav index.");
195
+ } else {
196
+ for (const file of result.changedFiles) console.log(`${check ? "stale" : "generated"} ${file}`);
197
+ }
198
+ }
199
+ return result.ok ? 0 : 1;
200
+ }
201
+
202
+ async function runQuery(root, args, json) {
203
+ const query = getPositionals(args).join(" ").trim();
204
+ if (!query) {
205
+ console.error("query requires search text.");
206
+ return 2;
207
+ }
208
+ const top = parseCliIntegerOption(args, "--top", 5);
209
+ const results = await queryProject(root, query, { top });
210
+ if (json) {
211
+ console.log(JSON.stringify(results, null, 2));
212
+ } else if (results.length === 0) {
213
+ console.log("No credible LLMNav candidates found.");
214
+ } else {
215
+ for (const [index, result] of results.entries()) {
216
+ const symbol = result.location?.symbol ? `#${result.location.symbol}` : "";
217
+ const line = result.location?.declarationLine ?? result.location?.startLine ?? 1;
218
+ console.log(`${index + 1}. @${result.id} score=${result.score}`);
219
+ console.log(` ${result.role}`);
220
+ console.log(` ${result.location?.path ?? "unknown"}${symbol}:${line}`);
221
+ console.log(` why ${result.reasons.join(", ")}`);
222
+ }
223
+ }
224
+ return 0;
225
+ }
226
+
227
+ async function runShow(root, args, json) {
228
+ const id = getPositionals(args)[0];
229
+ if (!id) {
230
+ console.error("show requires a semantic ID.");
231
+ return 2;
232
+ }
233
+ const result = await showProjectCard(root, id);
234
+ if (!result.card && !result.node) {
235
+ if (json) console.log(JSON.stringify(result, null, 2));
236
+ else console.error(`Unknown or inactive semantic ID ${id}.`);
237
+ return 1;
238
+ }
239
+ if (json) console.log(JSON.stringify(result, null, 2));
240
+ else {
241
+ const resolvedId = result.card?.id ?? result.node?.key;
242
+ if (result.resolvedFrom) console.log(`resolved ${id} -> ${resolvedId}\n`);
243
+ console.log(result.card ? renderCompactCard(result.card) : renderGraphNode(result.node));
244
+ }
245
+ return 0;
246
+ }
247
+
248
+ async function runContext(root, args, json) {
249
+ const id = getPositionals(args)[0];
250
+ if (!id) {
251
+ console.error("context requires a semantic ID.");
252
+ return 2;
253
+ }
254
+ const result = await buildContext(root, id, {
255
+ depth: parseCliIntegerOption(args, "--depth", 1),
256
+ budget: parseCliIntegerOption(args, "--budget", 2500),
257
+ maxEdges: parseCliIntegerOption(args, "--max-edges", 24),
258
+ });
259
+ if (json) console.log(JSON.stringify(result, null, 2));
260
+ else console.log(result.text);
261
+ return 0;
262
+ }
263
+
264
+ async function runEval(root, args, json) {
265
+ const result = await evaluateProject(root, {
266
+ top: parseCliIntegerOption(args, "--top", 5),
267
+ file: getOption(args, "--file"),
268
+ });
269
+ if (json) {
270
+ console.log(JSON.stringify(result, null, 2));
271
+ } else {
272
+ for (const error of result.errors) console.error(error);
273
+ for (const item of result.cases.filter((testCase) => !testCase.passAt5)) {
274
+ console.error(`miss ${JSON.stringify(item.query)} expected=${item.expected.join(",")} actual=${item.actual.join(",")}`);
275
+ }
276
+ console.log(`cases ${result.metrics.total}`);
277
+ console.log(`recall@1 ${(result.metrics.recallAt1 * 100).toFixed(1)}%`);
278
+ console.log(`recall@5 ${(result.metrics.recallAt5 * 100).toFixed(1)}%`);
279
+ console.log(`MRR ${result.metrics.meanReciprocalRank.toFixed(3)}`);
280
+ }
281
+ return result.ok ? 0 : 1;
282
+ }
283
+
284
+ async function runDoctor(root, json) {
285
+ const result = await doctorProject(root);
286
+ if (json) console.log(JSON.stringify(result, null, 2));
287
+ else {
288
+ for (const check of result.checks) console.log(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}`);
289
+ }
290
+ return result.ok ? 0 : 1;
291
+ }
292
+
293
+ function runSpec(json) {
294
+ const spec = {
295
+ specVersion: SPEC_VERSION,
296
+ keyOrder: KEY_ORDER,
297
+ stabilities: STABILITIES,
298
+ effects: EFFECT_KINDS,
299
+ risks: RISK_KINDS,
300
+ relations: RELATION_KINDS,
301
+ };
302
+ if (json) console.log(JSON.stringify(spec, null, 2));
303
+ else {
304
+ console.log(`LLMNav/${SPEC_VERSION}`);
305
+ console.log(`keys ${KEY_ORDER.join(" ")}`);
306
+ console.log(`stability ${STABILITIES.join(" ")}`);
307
+ console.log(`effects ${EFFECT_KINDS.join(" ")}`);
308
+ console.log(`risks ${RISK_KINDS.join(" ")}`);
309
+ console.log(`relations ${RELATION_KINDS.join(" ")}`);
310
+ }
311
+ return 0;
312
+ }
313
+
314
+ function runTools(json) {
315
+ const definitions = getAgentToolDefinitions();
316
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, tools: definitions }, null, 2));
317
+ else {
318
+ for (const definition of definitions) console.log(`${definition.name}\t${definition.description}`);
319
+ }
320
+ return 0;
321
+ }
322
+
323
+ async function runBundle(root, json) {
324
+ const bundle = await loadPromptPrefixBundle(root);
325
+ if (json) console.log(JSON.stringify(bundle, null, 2));
326
+ else {
327
+ console.log(`llmnav-prompt-bundle/${bundle.schemaVersion} repository=${bundle.repositoryId} hash=${bundle.bundleHash}`);
328
+ for (const partition of bundle.partitions) {
329
+ console.log(`${partition.id}\tscope=${partition.cacheScope}\ttokens~${partition.estimatedTokens}\tsha256=${partition.contentHash}`);
330
+ }
331
+ }
332
+ return 0;
333
+ }
334
+
335
+ function runEditor(args) {
336
+ const name = getPositionals(args)[0];
337
+ if (!name) throw usageError("editor requires an integration name.");
338
+ console.log(JSON.stringify(getEditorIntegration(name), null, 2));
339
+ return 0;
340
+ }
341
+
342
+ function printDiagnostics(diagnostics, format) {
343
+ for (const item of diagnostics) {
344
+ if (format === "github") {
345
+ const command = item.severity === "error" ? "error" : item.severity === "warning" ? "warning" : "notice";
346
+ console.log(
347
+ `::${command} file=${escapeWorkflow(item.file)},line=${item.line},col=${item.column},title=${item.code}::${escapeWorkflow(item.message)}`,
348
+ );
349
+ } else {
350
+ console.log(`${item.file}:${item.line}:${item.column} ${item.severity} ${item.code} ${item.message}`);
351
+ }
352
+ }
353
+ }
354
+
355
+ function compareCliDiagnostics(left, right) {
356
+ return left.file.localeCompare(right.file, "en") || left.line - right.line || left.column - right.column ||
357
+ left.code.localeCompare(right.code, "en") || left.message.localeCompare(right.message, "en");
358
+ }
359
+
360
+
361
+
362
+ function parseCliIntegerOption(args, name, fallback) {
363
+ const value = getOption(args, name);
364
+ if (value === undefined) return fallback;
365
+ if (!/^-?\d+$/u.test(value)) throw usageError(`Option ${name} requires an integer.`);
366
+ return parseInteger(value, fallback);
367
+ }
368
+
369
+ function usageError(message) {
370
+ const error = new Error(message);
371
+ error.exitCode = 2;
372
+ return error;
373
+ }
374
+
375
+ function validateOptions(command, args) {
376
+ const allowed = COMMAND_OPTIONS[command];
377
+ if (!allowed) return;
378
+ for (let index = 0; index < args.length; index += 1) {
379
+ const argument = args[index];
380
+ if (argument === "--") break;
381
+ if (!argument.startsWith("--")) continue;
382
+ const separator = argument.indexOf("=");
383
+ const name = separator >= 0 ? argument.slice(0, separator) : argument;
384
+ if (!allowed.has(name)) throw usageError(`Unknown option ${name} for command ${command}.`);
385
+ if (VALUE_OPTIONS.has(name)) {
386
+ const inlineValue = separator >= 0 ? argument.slice(separator + 1) : null;
387
+ if (inlineValue !== null) {
388
+ if (!inlineValue) throw usageError(`Option ${name} requires a value.`);
389
+ } else {
390
+ const next = args[index + 1];
391
+ if (next === undefined || next.startsWith("--")) throw usageError(`Option ${name} requires a value.`);
392
+ index += 1;
393
+ }
394
+ } else if (separator >= 0) {
395
+ throw usageError(`Flag ${name} does not accept a value.`);
396
+ }
397
+ }
398
+ }
399
+
400
+ function getPositionals(args) {
401
+ const values = [];
402
+ for (let index = 0; index < args.length; index += 1) {
403
+ const argument = args[index];
404
+ if (argument === "--") {
405
+ values.push(...args.slice(index + 1));
406
+ break;
407
+ }
408
+ if (argument.startsWith("--") || argument === "-h" || argument === "-v") {
409
+ const name = argument.split("=", 1)[0];
410
+ if (!argument.includes("=") && VALUE_OPTIONS.has(name)) index += 1;
411
+ continue;
412
+ }
413
+ values.push(argument);
414
+ }
415
+ return values;
416
+ }
417
+
418
+ function getOption(args, name) {
419
+ const exact = args.indexOf(name);
420
+ if (exact >= 0) return args[exact + 1];
421
+ const prefix = `${name}=`;
422
+ const inline = args.find((argument) => argument.startsWith(prefix));
423
+ return inline?.slice(prefix.length);
424
+ }
425
+
426
+ function hasFlag(args, name) {
427
+ return args.includes(name);
428
+ }
429
+
430
+ function escapeWorkflow(value) {
431
+ return String(value).replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A").replaceAll(":", "%3A").replaceAll(",", "%2C");
432
+ }
433
+
434
+ function printHelp() {
435
+ console.log(`llmnav ${PACKAGE_VERSION}
436
+
437
+ Deterministic semantic navigation for LLM coding agents.
438
+
439
+ Usage
440
+ llmnav init [--agents all|agents,claude,copilot,cursor] [--package-scripts]
441
+ llmnav check [paths...] [--format text|json|github|sarif|editor]
442
+ llmnav format [paths...] [--check]
443
+ llmnav generate [--check] [--full]
444
+ llmnav query "<task>" [--top 5]
445
+ llmnav show <semantic-id>
446
+ llmnav context <semantic-id> [--depth 1] [--budget 2500] [--max-edges 24]
447
+ llmnav eval [--file path] [--top 5]
448
+ llmnav doctor
449
+ llmnav spec
450
+ llmnav tools [--json]
451
+ llmnav bundle [--json]
452
+ llmnav editor vscode
453
+
454
+ Global options
455
+ --root <path> Project root
456
+ --json Machine-readable output
457
+ --help Show help
458
+ --version Show version`);
459
+ }