llmnav 0.6.0 → 0.6.2

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,28 @@ The npm package follows Semantic Versioning. The `llmnav/N` source protocol is v
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.2] — 2026-08-11
10
+
11
+ ### Added
12
+
13
+ * Added generated boundary signals for persistent dotted JSON filename protocols, versioned schema literals, and TypeScript Tauri invoke adapters.
14
+
15
+ ### Fixed
16
+
17
+ * Stopped classifying high-fan-in domain contract hubs as broad utilities solely because they export many declarations.
18
+ * Made tag-driven publication create an idempotent GitHub Release after npm integrity verification or publication succeeds.
19
+
20
+ ## [0.6.1] — 2026-08-10
21
+
22
+ ### Added
23
+
24
+ * Added compact `audit --summary` output and repository-contained `audit --output <path>` reports.
25
+ * Added nested workspace package entrypoint discovery and Rust/Tauri command, module, and platform-lifecycle signals.
26
+
27
+ ### Fixed
28
+
29
+ * Excluded Bun and pnpm package-manager caches from default source discovery.
30
+
9
31
  ## [0.6.0] — 2026-08-10
10
32
 
11
33
  ### Added
package/README.md CHANGED
@@ -18,7 +18,7 @@ LLMNav is not a documentation generator, an embedding database, or a reason to a
18
18
  * Machine-readable changed-card, affected-boundary, and affected-catalog output
19
19
  * Exported API and effective configuration contract fingerprints
20
20
  * TypeScript and Go declaration enrichment with declaration-level body hashes
21
- * Generated route, event, schema, migration, and command boundaries
21
+ * Generated artifact, route, event, schema, migration, runtime, and command boundaries
22
22
  * SARIF 2.1.0 diagnostic output
23
23
  * Optional deterministic card-range search shards for very large repositories
24
24
  * Strict repository-local imports for generated definition and reference indexes
@@ -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, persistent artifact filename protocols, versioned schema literals, Rust/Tauri runtime signals, TypeScript Tauri invoke adapters, and high fan-in modules. It suppresses declaration files, dependency caches, test support code, simple barrels, and low-fan-in 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/api.md CHANGED
@@ -60,7 +60,7 @@ console.log(stats.parsedFiles, stats.reusedFiles);
60
60
 
61
61
  Stat hints are an optimization, not deterministic output. The returned `fileState` contains only repository-relative generated data.
62
62
 
63
- Attached symbol declarations expose generated `language`, `exported`, `visibility`, optional Go `receiver`, `endOffset`, and declaration `bodyHash` fields. Indexed cards also expose sorted route, event, schema, migration, and command `boundaries` with confidence and evidence.
63
+ Attached symbol declarations expose generated `language`, `exported`, `visibility`, optional Go `receiver`, `endOffset`, and declaration `bodyHash` fields. Indexed cards also expose sorted artifact, route, event, schema, migration, runtime, and command `boundaries` with confidence and evidence.
64
64
 
65
65
  ## Audit annotation coverage
66
66
 
@@ -64,13 +64,13 @@ Each card carries three independent hashes.
64
64
 
65
65
  Symbol attachment records the detected language, public/exported status, visibility, Go receiver when present, declaration span, and a declaration-level body hash. TypeScript and Go use dedicated deterministic declaration patterns; JavaScript, Rust, Python, and generic C-like declarations retain the compatible fallback patterns.
66
66
 
67
- Generated cards may contain a sorted `boundaries` array. LLMNav detects `route`, `event`, `schema`, `migration`, and `command` boundaries from repository-relative paths and controlled semantic effects or risks. Each record includes `confidence` and explicit evidence such as `path`, `effect`, or `risk`. These hints are generated navigation data and are never copied into source comments.
67
+ Generated cards may contain a sorted `boundaries` array. LLMNav detects `artifact`, `route`, `event`, `schema`, `migration`, `runtime`, and `command` boundaries from repository-relative paths, controlled semantic effects or risks, and narrow source evidence. Source evidence includes persistent dotted JSON filename builders, versioned schema literals, Tauri commands and invoke adapters, and platform lifecycle code. Each record includes `confidence` and explicit evidence. These hints are generated navigation data and are never copied into source comments.
68
68
 
69
69
  Generation compares the previous and current primary indexes to emit `affectedBoundaries`. The report preserves the card change and hash dimensions while adding module IDs, structural boundary records, outbound relation targets, and reverse semantic dependents. It is returned through the API and `generate --json`; it is not stored in source cards.
70
70
 
71
71
  ## Annotation coverage audit
72
72
 
73
- The read-only audit combines package entrypoints, public re-export reachability, generated structural boundaries, import fan-in, exported declaration counts, and source size. It lowers the priority of broad utilities, declaration files, non-production support paths, and pure re-export barrels. Every candidate retains its score inputs and deterministic repository-relative path.
73
+ The read-only audit combines package entrypoints, public re-export reachability, generated structural boundaries, import fan-in, exported declaration counts, and source size. It lowers the priority of named utilities and low-fan-in export hubs, declaration files, non-production support paths, and pure re-export barrels. High-fan-in domain contract hubs do not receive the broad-utility penalty merely because they export many declarations. Every candidate retains its score inputs and deterministic repository-relative path.
74
74
 
75
75
  This analyzer deliberately stops before semantic generation. Structure can identify a file worth inspecting, but it cannot safely invent a durable ID, role, ownership statement, invariant, risk, or relation. Accepted boundaries become explicit source cards, path-specific coverage rules, and retrieval regression queries through normal review.
76
76
 
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
 
@@ -31,7 +31,7 @@ npm run release:check
31
31
 
32
32
  ## 3. Configure npm authentication
33
33
 
34
- The supplied release workflow uses GitHub's OIDC token through npm Trusted Publishers. The GitHub source repository is private, so the workflow explicitly disables provenance; npm accepts provenance only from public source repositories. If the repository becomes public, enable provenance in both `package.json` and the workflow after a successful release check.
34
+ The supplied release workflow uses GitHub's OIDC token through npm Trusted Publishers. The GitHub source repository is private, so the workflow explicitly disables provenance; npm accepts provenance only from public source repositories. If the repository becomes public, enable provenance in both `package.json` and the workflow after a successful release check. The same tag workflow creates an idempotent GitHub Release only after the npm registry check or publication succeeds.
35
35
 
36
36
  The workflow references a GitHub environment named `npm`. Create that environment for release protection, or remove the `environment` line when no environment gate is desired.
37
37
 
@@ -65,7 +65,7 @@ git tag v0.6.0
65
65
  git push origin v0.6.0
66
66
  ```
67
67
 
68
- The release workflow rejects a tag that does not match `package.json`. If the exact version is already present in npm, the workflow succeeds only when the registry tarball integrity matches the tagged package; a mismatched package fails closed.
68
+ The release workflow rejects a tag that does not match `package.json`. If the exact version is already present in npm, the workflow succeeds only when the registry tarball integrity matches the tagged package; a mismatched package fails closed. It then reuses an existing GitHub Release for the tag or creates one with generated release notes. Registry, GitHub API, and Release creation errors other than an expected missing Release fail the workflow.
69
69
 
70
70
  ## 6. Verify from a clean directory
71
71
 
@@ -19,7 +19,7 @@ Source roots and evaluation files must remain inside the repository. The generat
19
19
  npx llmnav audit
20
20
  ```
21
21
 
22
- Start with high and medium candidates. The audit explains whether a file is a package entrypoint, public API, generated structural boundary, or high fan-in module. It does not write source or invent card contents. Declaration files and common non-production or utility shapes are suppressed so the result is a review queue, not a demand to annotate every file.
22
+ Start with high and medium candidates. The audit explains whether a file is a package entrypoint, public API, generated structural boundary, or high fan-in module. Narrow source signals also identify versioned schema literals, persistent dotted JSON filename protocols, and Tauri invoke adapters. It does not write source or invent card contents. Declaration files and common non-production or low-fan-in utility shapes are suppressed so the result is a review queue, not a demand to annotate every file.
23
23
 
24
24
  After accepting a candidate, write its durable meaning by inspecting the source, add a narrow `coverageRules` entry for that exact boundary, and add a real task-language query to `.llmnav/eval/queries.jsonl`. Use `npx llmnav audit --fail-on high` in CI only after the initial review.
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llmnav",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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);
@@ -66,7 +66,7 @@ export async function auditProject(root) {
66
66
  const reexportBarrel = isReexportBarrel(record.source ?? "", file);
67
67
  const basename = path.posix.basename(file, path.posix.extname(file)).toLowerCase();
68
68
  const broadUtility = UTILITY_NAME_PATTERN.test(basename) ||
69
- (exportedDeclarations >= 10 && !entrypoint && !publicApi && boundaries.length === 0);
69
+ (exportedDeclarations >= 10 && importers <= 1 && !entrypoint && !publicApi && boundaries.length === 0);
70
70
  const nonProduction = NON_PRODUCTION_PATH_PATTERN.test(file);
71
71
  const largeSource = record.sourceBytes >= LARGE_SOURCE_BYTES;
72
72
  const hasSignal = entrypoint || publicApi || boundaries.length > 0 || importers > 0 || exportedDeclarations > 0 || largeSource;
@@ -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
@@ -1,9 +1,9 @@
1
1
  /* llmnav/1 module
2
2
  id=llmnav.structure.boundaries
3
- role=Detect route, event, schema, migration, and command boundaries from generated local evidence.
3
+ role=Detect durable artifact, route, event, schema, migration, runtime, and command boundaries from generated local evidence.
4
4
  owns=boundary kinds|confidence assignment|boundary evidence
5
5
  excludes=framework execution|source annotations
6
- search=boundary detection|route schema migration|command event
6
+ search=boundary detection|artifact route schema migration|command event runtime
7
7
  rel=workflow>llmnav.index.generate
8
8
  stability=architecture
9
9
  */
@@ -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(["artifact", "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);
@@ -39,12 +40,32 @@ export function detectBoundaries(record) {
39
40
  /(?:schema\.(?:json|ya?ml)|\.(?:proto|graphql|gql))$/u.test(relativePath)) {
40
41
  add("schema", "high", "path");
41
42
  }
43
+ if (/\bschemaVersion\s*:\s*["'][^"'\r\n]+\/v\d+(?:\.\d+)*["']/u.test(source)) {
44
+ add("schema", "high", "versioned-schema-literal");
45
+ }
46
+ if (/\bexport\s+(?:async\s+)?function\s+[A-Za-z_$][\w$]*FileName\b/u.test(source) &&
47
+ /["'`][^"'`\r\n]*\.[a-z0-9-]+\.json["'`]/iu.test(source)) {
48
+ add("artifact", "medium", "persistent-json-filename");
49
+ }
42
50
  if (risks.includes("migration") || /(?:^|\/)(?:migrations?|migrate)(?:\/|$)/u.test(relativePath)) {
43
51
  add("migration", "high", risks.includes("migration") ? "risk" : "path");
44
52
  }
45
53
  if (/(?:^|\/)(?:commands?|cli|bin)(?:\/|$)/u.test(relativePath) || /(?:command|cmd)\.[^.]+$/u.test(basename)) {
46
54
  add("command", "high", "path");
47
55
  }
56
+ if (/\.rs$/u.test(relativePath) && /#\[tauri::command\]|tauri::generate_handler!/u.test(source)) {
57
+ add("command", "high", "tauri-command");
58
+ }
59
+ if (/\.[cm]?[jt]sx?$/u.test(relativePath) &&
60
+ /\bgetTauriInvoke\s*\(/u.test(source) &&
61
+ /\binvoke(?:\s*<[^;\r\n]+?>)?\s*\(/u.test(source)) {
62
+ add("command", "high", "tauri-invoke");
63
+ }
64
+ if (/\.rs$/u.test(relativePath) &&
65
+ /#\[cfg\((?:windows|unix|target_(?:os|family))/u.test(source) &&
66
+ /\b(?:Drop|shutdown|terminate|kill|process_group|job_object)\b/iu.test(source)) {
67
+ add("runtime", "medium", "platform-lifecycle");
68
+ }
48
69
 
49
70
  return [...boundaries.values()].sort((left, right) => compareText(left.kind, right.kind));
50
71
  }
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/index.d.ts CHANGED
@@ -58,7 +58,7 @@ export interface Declaration {
58
58
  }
59
59
 
60
60
  export interface DetectedBoundary {
61
- kind: "command" | "event" | "migration" | "route" | "schema";
61
+ kind: "artifact" | "command" | "event" | "migration" | "route" | "runtime" | "schema";
62
62
  confidence: "high" | "medium";
63
63
  evidence: string[];
64
64
  }
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.2";
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",