llmnav 0.6.0 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ The npm package follows Semantic Versioning. The `llmnav/N` source protocol is v
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.1] — 2026-08-10
10
+
11
+ ### Added
12
+
13
+ * Added compact `audit --summary` output and repository-contained `audit --output <path>` reports.
14
+ * Added nested workspace package entrypoint discovery and Rust/Tauri command, module, and platform-lifecycle signals.
15
+
16
+ ### Fixed
17
+
18
+ * Excluded Bun and pnpm package-manager caches from default source discovery.
19
+
9
20
  ## [0.6.0] — 2026-08-10
10
21
 
11
22
  ### Added
package/README.md CHANGED
@@ -169,7 +169,7 @@ Line-comment cards require an explicit terminator and work with `//`, `#`, and `
169
169
  | Command | Purpose |
170
170
  | --- | --- |
171
171
  | `llmnav init` | Create configuration, registry, schemas, agent instructions, and the initial cache |
172
- | `llmnav audit` | Rank unannotated architectural boundary candidates without modifying source |
172
+ | `llmnav audit` | Rank unannotated architectural boundary candidates, with compact or file-backed output |
173
173
  | `llmnav check` | Validate cards, relations, coverage rules, and registry state |
174
174
  | `llmnav format` | Rewrite safe cards into canonical order and spacing |
175
175
  | `llmnav generate` | Incrementally compile and transactionally commit generated artifacts |
@@ -250,7 +250,7 @@ Do not annotate trivial getters, generated files, obvious wrappers, every test f
250
250
 
251
251
  ## Current implementation boundary
252
252
 
253
- Version 0.6 adds a deterministic coverage audit that prioritizes public entrypoints, structural boundaries, and high fan-in modules while suppressing declaration files, test support code, simple barrels, and broad utilities. It suggests narrow coverage rules but never writes cards or invents semantic roles.
253
+ Version 0.6 adds a deterministic coverage audit that prioritizes root and nested-package entrypoints, structural boundaries, Rust/Tauri runtime signals, and high fan-in modules while suppressing declaration files, dependency caches, test support code, simple barrels, and broad utilities. It suggests narrow coverage rules but never writes cards or invents semantic roles. Use `llmnav audit --summary --json` for a compact automation result or `llmnav audit --json --output .llmnav/audit.json` to keep the full candidate report out of captured stdout.
254
254
 
255
255
  LLMNav does not discover sibling repositories automatically and does not ship an MCP server, embedding database, hosted service, SCIP generator, or complete language-aware call graph. External tools may export the documented compact graph-input schema. Generated structure never writes derived edges into source cards.
256
256
 
package/docs/cli.md CHANGED
@@ -32,14 +32,14 @@ Creates project configuration, schema, registry, lexicon, evaluation file, agent
32
32
  ## `llmnav audit`
33
33
 
34
34
  ```sh
35
- llmnav audit [--fail-on none|high|medium|low] [--json]
35
+ llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low] [--json]
36
36
  ```
37
37
 
38
- Reads the selected source set and ranks files that lack file or module cards. Signals include package entrypoints, public re-exports, generated command/route/schema/migration/event boundaries, import fan-in, exported declarations, and source size. Declaration files are omitted; test support code, broad utilities, and pure re-export barrels receive penalties. Candidates whose penalties reduce their score to zero are omitted.
38
+ Reads the selected source set and ranks files that lack file or module cards. Signals include root and nested-package entrypoints, public re-exports, generated command/route/schema/migration/event boundaries, Rust/Tauri runtime boundaries, import fan-in, exported declarations, and source size. Declaration files are omitted; dependency caches, test support code, broad utilities, and pure re-export barrels are excluded or penalized. Candidates whose penalties reduce their score to zero are omitted.
39
39
 
40
40
  The command never modifies source, configuration, registries, or generated caches. Its output is advisory and exits with status 0 by default. `--fail-on high` fails only for high candidates; `medium` fails for high or medium; `low` fails for any candidate. Invalid thresholds exit with status 2.
41
41
 
42
- JSON output uses schemaVersion 1, repository-relative paths, deterministic ordering, explainable `reasons` and `signals`, and a path-specific `suggestedCoverageRule` for high and medium candidates. Suggestions require human or agent review: LLMNav cannot infer a durable role, ownership boundary, invariant, or semantic ID from structure alone.
42
+ JSON output uses schemaVersion 1, repository-relative paths, deterministic ordering, explainable `reasons` and `signals`, and a path-specific `suggestedCoverageRule` for high and medium candidates. `--summary` omits candidate details. `--output <path>` writes the selected report inside the repository and emits only a compact confirmation envelope to stdout; escaping paths and symbolic-link traversal are rejected. Suggestions require human or agent review: LLMNav cannot infer a durable role, ownership boundary, invariant, or semantic ID from structure alone.
43
43
 
44
44
  ## `llmnav check`
45
45
 
@@ -71,7 +71,7 @@ Every source root must remain inside the repository and cannot be a symbolic lin
71
71
 
72
72
  `includeExtensions` is an allowlist. LLMNav does not scan Markdown or YAML by default because documentation examples routinely contain card syntax.
73
73
 
74
- `excludeDirectories` matches directory names at any depth. `excludeFiles` uses repository-relative globs with `*`, `?`, and `**`.
74
+ `excludeDirectories` matches directory names at any depth. Defaults exclude dependency and tool output including `node_modules`, `.bun-cache`, `.pnpm-store`, `.cache`, build directories, and language targets. `excludeFiles` uses repository-relative globs with `*`, `?`, and `**`.
75
75
 
76
76
  ## Coverage rules
77
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llmnav",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A deterministic semantic navigation layer for LLM coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/audit.js CHANGED
@@ -47,8 +47,7 @@ export async function auditProject(root) {
47
47
  for (const target of unique) importedBy.get(target)?.add(file);
48
48
  }
49
49
 
50
- const packageJson = await readJsonSafe(path.join(root, "package.json"), {});
51
- const entrypoints = collectPackageEntrypoints(packageJson, fileByPath);
50
+ const entrypoints = await collectWorkspacePackageEntrypoints(root, fileByPath);
52
51
  const publicApiPaths = collectPublicApiPaths(entrypoints, fileByPath);
53
52
  const candidates = [];
54
53
 
@@ -58,6 +57,7 @@ export async function auditProject(root) {
58
57
  const boundaries = detectBoundaries({
59
58
  relativePath: file,
60
59
  card: { effect: [], risk: [] },
60
+ source: record.source ?? "",
61
61
  }).map((boundary) => boundary.kind);
62
62
  const exportedDeclarations = countExportedDeclarations(record.source ?? "", file);
63
63
  const entrypoint = entrypoints.has(file);
@@ -174,14 +174,34 @@ function buildCoverageSuggestion(file) {
174
174
  };
175
175
  }
176
176
 
177
- function collectPackageEntrypoints(packageJson, fileByPath) {
177
+ async function collectWorkspacePackageEntrypoints(root, fileByPath) {
178
+ const packageDirectories = new Set([""]);
179
+ for (const file of fileByPath.keys()) {
180
+ let directory = path.posix.dirname(file);
181
+ while (directory !== "." && directory !== "") {
182
+ packageDirectories.add(directory);
183
+ const parent = path.posix.dirname(directory);
184
+ if (parent === directory || parent === ".") break;
185
+ directory = parent;
186
+ }
187
+ }
188
+ const entrypoints = new Set();
189
+ for (const directory of [...packageDirectories].sort(compareText)) {
190
+ const packageJson = await readJsonSafe(path.join(root, directory, "package.json"), null);
191
+ if (!packageJson || typeof packageJson !== "object") continue;
192
+ for (const entrypoint of collectPackageEntrypoints(packageJson, fileByPath, directory)) entrypoints.add(entrypoint);
193
+ }
194
+ return entrypoints;
195
+ }
196
+
197
+ function collectPackageEntrypoints(packageJson, fileByPath, packageDirectory = "") {
178
198
  const raw = [packageJson?.main, packageJson?.module, ...collectStringLeaves(packageJson?.bin), ...collectStringLeaves(packageJson?.exports)];
179
199
  const entrypoints = new Set();
180
200
  for (const value of raw) {
181
201
  if (typeof value !== "string" || /\.d\.[cm]?ts$/u.test(value)) continue;
182
202
  const normalized = normalizePackagePath(value);
183
203
  if (!normalized) continue;
184
- const resolved = resolveProjectPath(normalized, fileByPath);
204
+ const resolved = resolveProjectPath(path.posix.join(packageDirectory, normalized), fileByPath);
185
205
  if (resolved) entrypoints.add(resolved);
186
206
  }
187
207
  return entrypoints;
@@ -237,12 +257,52 @@ function isReexportBarrel(source, file) {
237
257
  }
238
258
 
239
259
  function resolveLocalSpecifier(fromFile, specifier, fileByPath) {
260
+ if (/\.rs$/u.test(fromFile)) return resolveRustSpecifier(fromFile, specifier, fileByPath);
240
261
  if (!specifier.startsWith(".")) return null;
241
262
  const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), toPosix(specifier)));
242
263
  if (base.startsWith("../") || base === "..") return null;
243
264
  return resolveProjectPath(base, fileByPath);
244
265
  }
245
266
 
267
+ function resolveRustSpecifier(fromFile, specifier, fileByPath) {
268
+ const normalized = specifier
269
+ .replace(/\s+as\s+.+$/u, "")
270
+ .replace(/::\{[\s\S]*$/u, "")
271
+ .replace(/::\*$/u, "")
272
+ .trim();
273
+ if (!normalized || /[{}(),]/u.test(normalized)) return null;
274
+ const parts = normalized.split("::").filter(Boolean);
275
+ const sourceIndex = fromFile.split("/").lastIndexOf("src");
276
+ const crateSource = sourceIndex >= 0 ? fromFile.split("/").slice(0, sourceIndex + 1).join("/") : path.posix.dirname(fromFile);
277
+ let base;
278
+ if (parts[0] === "crate") {
279
+ parts.shift();
280
+ base = crateSource;
281
+ } else if (parts[0] === "self") {
282
+ parts.shift();
283
+ base = rustModuleDirectory(fromFile);
284
+ } else if (parts[0] === "super") {
285
+ while (parts[0] === "super") {
286
+ parts.shift();
287
+ base = path.posix.dirname(base ?? rustModuleDirectory(fromFile));
288
+ }
289
+ } else {
290
+ base = rustModuleDirectory(fromFile);
291
+ }
292
+ if (parts.length === 0) return null;
293
+ const candidate = path.posix.join(base, ...parts);
294
+ return [
295
+ `${candidate}.rs`,
296
+ `${candidate}/mod.rs`,
297
+ ].find((value) => fileByPath.has(value)) ?? null;
298
+ }
299
+
300
+ function rustModuleDirectory(fromFile) {
301
+ const basename = path.posix.basename(fromFile);
302
+ if (basename === "lib.rs" || basename === "main.rs" || basename === "mod.rs") return path.posix.dirname(fromFile);
303
+ return path.posix.join(path.posix.dirname(fromFile), path.posix.basename(fromFile, ".rs"));
304
+ }
305
+
246
306
  function resolveProjectPath(candidate, fileByPath) {
247
307
  const values = [candidate];
248
308
  const extension = path.posix.extname(candidate);
package/src/boundaries.js CHANGED
@@ -11,13 +11,14 @@ stability=architecture
11
11
  import path from "node:path";
12
12
  import { compareText, toPosix } from "./util.js";
13
13
 
14
- export const BOUNDARY_KINDS = Object.freeze(["command", "event", "migration", "route", "schema"]);
14
+ export const BOUNDARY_KINDS = Object.freeze(["command", "event", "migration", "route", "runtime", "schema"]);
15
15
 
16
16
  export function detectBoundaries(record) {
17
17
  const relativePath = toPosix(record.relativePath).toLowerCase();
18
18
  const basename = path.posix.basename(relativePath);
19
19
  const effects = record.card.effect ?? [];
20
20
  const risks = record.card.risk ?? [];
21
+ const source = record.source ?? "";
21
22
  const boundaries = new Map();
22
23
  const add = (kind, confidence, evidence) => {
23
24
  const current = boundaries.get(kind);
@@ -45,6 +46,14 @@ export function detectBoundaries(record) {
45
46
  if (/(?:^|\/)(?:commands?|cli|bin)(?:\/|$)/u.test(relativePath) || /(?:command|cmd)\.[^.]+$/u.test(basename)) {
46
47
  add("command", "high", "path");
47
48
  }
49
+ if (/\.rs$/u.test(relativePath) && /#\[tauri::command\]|tauri::generate_handler!/u.test(source)) {
50
+ add("command", "high", "tauri-command");
51
+ }
52
+ if (/\.rs$/u.test(relativePath) &&
53
+ /#\[cfg\((?:windows|unix|target_(?:os|family))/u.test(source) &&
54
+ /\b(?:Drop|shutdown|terminate|kill|process_group|job_object)\b/iu.test(source)) {
55
+ add("runtime", "medium", "platform-lifecycle");
56
+ }
48
57
 
49
58
  return [...boundaries.values()].sort((left, right) => compareText(left.kind, right.kind));
50
59
  }
package/src/cli.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  SPEC_VERSION,
32
32
  STABILITIES,
33
33
  } from "./spec.js";
34
- import { parseInteger } from "./util.js";
34
+ import { assertNoSymlinkTraversal, atomicWrite, parseInteger, relativePosix } from "./util.js";
35
35
  import { diagnosticsToSarif } from "./sarif.js";
36
36
  import { loadGraphInputs } from "./graph-input.js";
37
37
  import { renderGraphNode } from "./graph.js";
@@ -40,7 +40,7 @@ import { loadPromptPrefixBundle } from "./prompt-bundle.js";
40
40
  import { diagnosticsToEditor, getEditorIntegration } from "./editor.js";
41
41
  import { AUDIT_PRIORITIES, auditHasFindings, auditProject } from "./audit.js";
42
42
 
43
- const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file", "--fail-on"]);
43
+ const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file", "--fail-on", "--output"]);
44
44
 
45
45
  const COMMAND_OPTIONS = Object.freeze({
46
46
  init: new Set(["--agents", "--package-scripts", "--force", "--root", "--json"]),
@@ -53,7 +53,7 @@ const COMMAND_OPTIONS = Object.freeze({
53
53
  context: new Set(["--depth", "--budget", "--max-edges", "--root", "--json"]),
54
54
  eval: new Set(["--file", "--top", "--root", "--json"]),
55
55
  doctor: new Set(["--root", "--json"]),
56
- audit: new Set(["--root", "--json", "--fail-on"]),
56
+ audit: new Set(["--root", "--json", "--summary", "--fail-on", "--output"]),
57
57
  spec: new Set(["--root", "--json"]),
58
58
  tools: new Set(["--json"]),
59
59
  bundle: new Set(["--root", "--json"]),
@@ -300,10 +300,27 @@ async function runAudit(root, args, json) {
300
300
  if (failOn !== "none" && !AUDIT_PRIORITIES.includes(failOn)) {
301
301
  throw usageError(`audit --fail-on must be one of none, ${AUDIT_PRIORITIES.join(", ")}.`);
302
302
  }
303
+ const outputOption = getOption(args, "--output");
304
+ const outputPath = outputOption ? path.resolve(root, outputOption) : null;
305
+ if (outputPath) {
306
+ try {
307
+ await assertNoSymlinkTraversal(root, outputPath, "audit output file");
308
+ } catch (error) {
309
+ throw usageError(error instanceof Error ? error.message : String(error));
310
+ }
311
+ }
303
312
  const result = await auditProject(root);
304
313
  const report = { ...result, failOn };
314
+ const selectedReport = hasFlag(args, "--summary")
315
+ ? { schemaVersion: result.schemaVersion, repositoryId: result.repositoryId, summary: result.summary, failOn }
316
+ : report;
317
+ if (outputPath) {
318
+ await atomicWrite(outputPath, `${JSON.stringify(selectedReport, null, 2)}\n`);
319
+ }
305
320
  if (json) {
306
- console.log(JSON.stringify(report, null, 2));
321
+ console.log(JSON.stringify(outputPath
322
+ ? { schemaVersion: result.schemaVersion, repositoryId: result.repositoryId, summary: result.summary, failOn, output: relativePosix(root, outputPath) }
323
+ : selectedReport, null, 2));
307
324
  } else {
308
325
  const { summary } = result;
309
326
  console.log(
@@ -314,6 +331,7 @@ async function runAudit(root, args, json) {
314
331
  console.log(`${candidate.priority} ${candidate.path} score=${candidate.score} ${candidate.reasons.join(",")}`);
315
332
  }
316
333
  if (summary.low > 0) console.log(`${summary.low} low-priority candidate(s) are available in --json output.`);
334
+ if (outputPath) console.log(`wrote ${relativePosix(root, outputPath)}`);
317
335
  }
318
336
  return auditHasFindings(result, failOn) ? 1 : 0;
319
337
  }
@@ -474,7 +492,7 @@ Usage
474
492
  llmnav context <semantic-id> [--depth 1] [--budget 2500] [--max-edges 24]
475
493
  llmnav eval [--file path] [--top 5]
476
494
  llmnav doctor
477
- llmnav audit [--fail-on none|high|medium|low]
495
+ llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low]
478
496
  llmnav spec
479
497
  llmnav tools [--json]
480
498
  llmnav bundle [--json]
package/src/spec.js CHANGED
@@ -10,7 +10,7 @@ rel=workflow>llmnav.rules.validate
10
10
  stability=contract
11
11
  */
12
12
 
13
- export const PACKAGE_VERSION = "0.6.0";
13
+ export const PACKAGE_VERSION = "0.6.1";
14
14
  export const SPEC_VERSION = "1";
15
15
 
16
16
  export const SCOPES = Object.freeze(["file", "module", "symbol"]);
@@ -163,6 +163,8 @@ export const DEFAULT_EXCLUDED_DIRECTORIES = Object.freeze([
163
163
  ".astro",
164
164
  ".turbo",
165
165
  ".cache",
166
+ ".bun-cache",
167
+ ".pnpm-store",
166
168
  "target",
167
169
  "bin",
168
170
  "obj",