llmnav 0.8.0 → 0.9.0

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,12 @@ The npm package follows Semantic Versioning. The `llmnav/N` source protocol is v
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0] — 2026-08-15
10
+
11
+ ### Added
12
+
13
+ * Added `llmnav explain <file>` and `explainProjectFile` to report one file's navigation cards, module coverage, audit score evidence, reviewed disposition, and bounded next action without modifying source.
14
+
9
15
  ## [0.8.0] — 2026-08-15
10
16
 
11
17
  ### Added
package/README.md CHANGED
@@ -47,7 +47,7 @@ npx llmnav init --agents all --package-scripts
47
47
 
48
48
  Initialization is explicit. LLMNav never edits a consumer repository from an npm `postinstall` script. Use `--agents none` when only the machine-readable control directory is desired.
49
49
 
50
- Before writing cards, run `npx llmnav audit`. It ranks likely architectural boundaries and explains each score. The command never edits source and remains advisory unless `--fail-on high`, `medium`, or `low` is selected. Review the candidates: a high score is evidence to inspect a file, not permission to generate semantic meaning automatically. For a reviewed false positive or implementation detail, add its exact path and a concrete reason to `audit.dispositions`; stale decisions remain visible instead of becoming permanent hidden ignores.
50
+ Before writing cards, run `npx llmnav audit`. It ranks likely architectural boundaries and explains each score. Use `npx llmnav explain <file>` when you need to know why one file is carded, covered by another module card, ranked as a candidate, suppressed by a reviewed disposition, or omitted from the candidate list. Neither command edits source. Review the evidence: a high score is a reason to inspect a file, not permission to generate semantic meaning automatically. For a reviewed false positive or implementation detail, add its exact path and a concrete reason to `audit.dispositions`; stale decisions remain visible instead of becoming permanent hidden ignores.
51
51
 
52
52
  ## Add the first card
53
53
 
@@ -172,6 +172,7 @@ Line-comment cards require an explicit terminator and work with `//`, `#`, and `
172
172
  | --- | --- |
173
173
  | `llmnav init` | Create configuration, registry, schemas, agent instructions, and the initial cache |
174
174
  | `llmnav audit` | Rank unannotated architectural boundary candidates, with compact or file-backed output |
175
+ | `llmnav explain` | Explain one file's card coverage, audit score, disposition, and recommended next action |
175
176
  | `llmnav check` | Validate cards, relations, coverage rules, and registry state |
176
177
  | `llmnav format` | Rewrite safe cards into canonical order and spacing |
177
178
  | `llmnav generate` | Incrementally compile and transactionally commit generated artifacts |
@@ -49,7 +49,7 @@ The generated instruction tells an agent to:
49
49
  10. Run format, check, and generation after semantic changes.
50
50
  11. Fall back to broad search when no credible card is returned.
51
51
 
52
- When a host supports structured tool calls, `llmnav tools --json` returns four stable provider-neutral definitions in fixed order: `llmnav_query`, `llmnav_show`, `llmnav_context`, and `llmnav_check`. The schemas reject unknown fields and omit the repository root so the trusted host binds scope outside model-generated input.
52
+ When a host supports structured tool calls, `llmnav tools --json` returns five stable provider-neutral definitions in fixed order: `llmnav_query`, `llmnav_show`, `llmnav_context`, `llmnav_check`, and `llmnav_explain`. The new definition is appended so the existing stable tool prefix keeps its order. The schemas reject unknown fields and omit the repository root so the trusted host binds scope outside model-generated input.
53
53
 
54
54
  The protocol does not order an agent to trust a card over source code. It uses the card to choose what source to inspect.
55
55
 
@@ -99,7 +99,7 @@ Suggested contract:
99
99
  ```json
100
100
  {
101
101
  "schemaVersion": 1,
102
- "operations": ["llmnav_query", "llmnav_show", "llmnav_context", "llmnav_check"]
102
+ "operations": ["llmnav_query", "llmnav_show", "llmnav_context", "llmnav_check", "llmnav_explain"]
103
103
  }
104
104
  ```
105
105
 
package/docs/api.md CHANGED
@@ -65,7 +65,7 @@ Attached symbol declarations expose generated `language`, `exported`, `visibilit
65
65
  ## Audit annotation coverage
66
66
 
67
67
  ```js
68
- import { auditHasFindings, auditProject } from "llmnav";
68
+ import { auditHasFindings, auditProject, explainProjectFile } from "llmnav";
69
69
 
70
70
  const result = await auditProject(process.cwd());
71
71
  for (const candidate of result.candidates) {
@@ -73,9 +73,12 @@ for (const candidate of result.candidates) {
73
73
  }
74
74
 
75
75
  if (auditHasFindings(result, "high")) process.exitCode = 1;
76
+
77
+ const explanation = await explainProjectFile(process.cwd(), "src/runtime.ts");
78
+ console.log(explanation.status, explanation.candidate?.score, explanation.recommendation.action);
76
79
  ```
77
80
 
78
- `auditProject` is read-only and returns schemaVersion 1 data with repository-relative paths and deterministic ordering. Candidate signals are structural heuristics, not generated semantic meaning. Exact reviewed decisions from `audit.dispositions` are returned separately with `suppressed` or `stale` status; `auditHasFindings` considers only active candidates. Consumers should review high and medium candidates before adding a card and should never turn the suggested coverage rule into automatic source annotation.
81
+ `auditProject` is read-only and returns schemaVersion 1 data with repository-relative paths and deterministic ordering. Candidate signals are structural heuristics, not generated semantic meaning. Exact reviewed decisions from `audit.dispositions` are returned separately with `suppressed` or `stale` status; `auditHasFindings` considers only active candidates. `explainProjectFile` applies the same analysis to one path, preserving candidate score evidence and package-level coverage without mutating source. Consumers should review high and medium candidates before adding a card and should never turn a suggested coverage rule or recommendation into automatic source annotation.
79
82
 
80
83
  ## Generate incrementally and transactionally
81
84
 
package/docs/cli.md CHANGED
@@ -41,6 +41,16 @@ The command never modifies source, configuration, registries, or generated cache
41
41
 
42
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. Reviewed `audit.dispositions` appear separately as `suppressed` or `stale`; only active candidates participate in `--fail-on`. Stale reasons are `file-not-scanned`, `already-carded`, or `not-a-candidate`. `--summary` omits candidate and disposition details but retains their counters. `--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
+ ## `llmnav explain`
45
+
46
+ ```sh
47
+ llmnav explain <file> [--json]
48
+ ```
49
+
50
+ Explains one repository-relative or repository-contained absolute file path without modifying the repository. The result distinguishes active candidates, reviewed suppressions, direct cards, package-level module coverage, files with no ranked audit signal, files excluded from the scanned source set, and stale dispositions. Candidate results retain the audit's representative path, priority, score, reasons, signals, and suggested coverage rule; this matters for Go packages, where the requested file and representative candidate can differ.
51
+
52
+ The recommendation is deliberately bounded. It may ask for card review, disposition review, stale-disposition cleanup, or no action, but it never invents a semantic ID or writes a source comment. A file outside the repository or an invalid positional argument exits with status 2. A valid path that is not in the scanned source set returns its explanation and exits with status 1.
53
+
44
54
  ## `llmnav check`
45
55
 
46
56
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llmnav",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A deterministic semantic navigation layer for LLM coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,7 @@ excludes=provider SDK transport|repository discovery|source mutation
6
6
  search=agent tool schema|provider neutral tools|tool dispatcher|agent operation protocol
7
7
  rel=workflow>llmnav.search.query
8
8
  rel=workflow>llmnav.rules.validate
9
+ rel=workflow>llmnav.audit.coverage
9
10
  stability=contract
10
11
  */
11
12
 
@@ -14,6 +15,7 @@ import { scanProject } from "./project.js";
14
15
  import { buildContext, queryProject, showProjectCard } from "./search.js";
15
16
  import { countDiagnostics, validateProject } from "./validator.js";
16
17
  import { getAgentToolDefinitions } from "./agent-tools.js";
18
+ import { explainProjectFile } from "./audit.js";
17
19
 
18
20
  export { AGENT_TOOL_SCHEMA_VERSION, getAgentToolDefinitions } from "./agent-tools.js";
19
21
  export const AGENT_OPERATION_SCHEMA_VERSION = 1;
@@ -25,6 +27,7 @@ const OPERATIONS = new Map([
25
27
  ["llmnav_show", "show"],
26
28
  ["llmnav_context", "context"],
27
29
  ["llmnav_check", "check"],
30
+ ["llmnav_explain", "explain"],
28
31
  ]);
29
32
 
30
33
  export async function executeAgentOperation(root, name, input = {}, options = {}) {
@@ -58,6 +61,9 @@ export async function executeAgentOperation(root, name, input = {}, options = {}
58
61
  ? options.session.context(input.id.trim(), contextOptions)
59
62
  : await buildContext(root, input.id.trim(), contextOptions));
60
63
  }
64
+ if (operation === "explain") {
65
+ return success(operation, await explainProjectFile(root, input.file.trim()));
66
+ }
61
67
  const project = await scanProject(root, { paths: input.paths ?? [] });
62
68
  const graphInputs = await loadGraphInputs(root, project.config);
63
69
  const diagnostics = [...validateProject(project), ...graphInputs.diagnostics].sort(compareDiagnostics);
@@ -32,6 +32,9 @@ const DEFINITIONS = [
32
32
  default: [],
33
33
  },
34
34
  }),
35
+ tool("llmnav_explain", "Explain one file's card coverage, audit evidence, disposition, and recommended next action.", {
36
+ file: stringProperty("Repository-relative or repository-contained absolute file path."),
37
+ }, ["file"]),
35
38
  ];
36
39
 
37
40
  export function getAgentToolDefinitions() {
package/src/agents.js CHANGED
@@ -32,6 +32,8 @@ Do not add hand-maintained \`calls\`, \`imports\`, \`references\`, \`implements\
32
32
 
33
33
  After initialization and whenever public entrypoints, commands, routes, schemas, migrations, or high fan-in modules change, run \`npm exec -- llmnav audit\`. Review high and medium candidates; never add cards automatically. Add a module card only after confirming a durable responsibility, then encode the accepted boundary in a path-specific \`coverageRules\` entry and add a representative retrieval query. When a reviewed candidate has no durable navigation responsibility, record its exact path and a concrete reason in \`audit.dispositions\`; never use a glob or broad directory suppression.
34
34
 
35
+ When deciding whether one specific file needs a card, run \`npm exec -- llmnav explain <file>\`. Use its coverage, score, and disposition evidence to guide review, but never turn its recommendation into automatic source annotation.
36
+
35
37
  After semantic changes, run \`npm exec -- llmnav format\`, \`npm exec -- llmnav check\`, and \`npm exec -- llmnav generate\`. Use broad text search only when LLMNav returns no credible candidate.
36
38
  ${END}`;
37
39
 
package/src/audit.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /* llmnav/1 module
2
2
  id=llmnav.audit.coverage
3
3
  role=Identify high-value source modules that lack semantic navigation boundaries without modifying source.
4
- owns=annotation coverage audit|candidate prioritization|coverage rule suggestions
4
+ owns=annotation coverage audit|candidate prioritization|file explanation|coverage rule suggestions
5
5
  excludes=automatic source annotation|semantic role generation
6
- search=llmnav audit|missing module cards|coverage suggestions
6
+ search=llmnav audit|missing module cards|explain missing annotation|coverage suggestions
7
7
  invariant=Audit output is deterministic, repository-relative, and advisory unless an explicit fail threshold is selected.
8
8
  rel=workflow>llmnav.project.scan
9
9
  stability=contract
@@ -17,6 +17,7 @@ import { compareText, readJsonSafe, toPosix } from "./util.js";
17
17
 
18
18
  export const AUDIT_SCHEMA_VERSION = 1;
19
19
  export const AUDIT_PRIORITIES = Object.freeze(["high", "medium", "low"]);
20
+ export const FILE_EXPLANATION_SCHEMA_VERSION = 1;
20
21
 
21
22
  const SOURCE_EXTENSIONS = Object.freeze([
22
23
  ".astro", ".c", ".cc", ".cjs", ".cpp", ".cs", ".cts", ".dart", ".go", ".h", ".hpp", ".java",
@@ -28,6 +29,65 @@ const NON_PRODUCTION_PATH_PATTERN = /(?:^|\/)(?:__tests__|benchmarks?|fixtures?|
28
29
  const LARGE_SOURCE_BYTES = 12_000;
29
30
 
30
31
  export async function auditProject(root) {
32
+ return (await analyzeAuditProject(root)).result;
33
+ }
34
+
35
+ export async function explainProjectFile(root, inputPath) {
36
+ const file = normalizeExplanationPath(root, inputPath);
37
+ const analysis = await analyzeAuditProject(root);
38
+ const { project, fileByPath, candidates, result } = analysis;
39
+ const moduleKey = fileByPath.has(file) ? moduleKeyForFile(file) : null;
40
+ const navigationCards = project.records
41
+ .filter((record) => toPosix(record.relativePath) === file)
42
+ .map((record) => ({ id: record.card.id, scope: record.card.scope, path: file }))
43
+ .sort((left, right) => compareText(left.id, right.id));
44
+ const coverageCards = moduleKey === null ? [] : project.records
45
+ .filter((record) => {
46
+ const cardPath = toPosix(record.relativePath);
47
+ if (record.card.scope === "file") return cardPath === file;
48
+ return record.card.scope === "module" && moduleKeyForFile(cardPath) === moduleKey;
49
+ })
50
+ .map((record) => ({ id: record.card.id, scope: record.card.scope, path: toPosix(record.relativePath) }))
51
+ .sort((left, right) => compareText(left.id, right.id));
52
+ const candidate = moduleKey === null ? null : candidates.find((item) => moduleKeyForFile(item.path) === moduleKey) ?? null;
53
+ const dispositionPath = candidate?.path ?? file;
54
+ const disposition = result.dispositions.find((item) => item.path === dispositionPath) ??
55
+ result.dispositions.find((item) => item.path === file) ?? null;
56
+
57
+ if (disposition?.status === "stale") {
58
+ return buildFileExplanation(result.repositoryId, file, moduleKey, "stale-disposition", navigationCards, coverageCards, candidate, disposition, [
59
+ `stale-disposition:${disposition.staleReason}`,
60
+ ], "remove-or-review-disposition", "Remove or update the stale exact-path disposition after reviewing the current file state.");
61
+ }
62
+ if (!fileByPath.has(file)) {
63
+ return buildFileExplanation(result.repositoryId, file, null, "not-scanned", [], [], null, disposition, ["file-not-scanned"],
64
+ "check-scan-configuration", "Check that the file exists under a source root and is not excluded by extension, directory, or file rules.");
65
+ }
66
+ if (coverageCards.length > 0) {
67
+ const status = coverageCards.some((card) => card.path === file) ? "carded" : "covered";
68
+ return buildFileExplanation(result.repositoryId, file, moduleKey, status, navigationCards, coverageCards, null, disposition,
69
+ [status === "carded" ? "file-has-coverage-card" : "module-covered-by-card"],
70
+ "keep-current-coverage", "No additional file or module card is needed unless this file gains a separate durable responsibility.");
71
+ }
72
+ if (candidate && disposition?.status === "suppressed") {
73
+ return buildFileExplanation(result.repositoryId, file, moduleKey, "suppressed", navigationCards, coverageCards, candidate, disposition,
74
+ ["reviewed-exact-path-disposition", ...candidate.reasons], "keep-or-review-disposition",
75
+ "Keep the disposition while its reason remains true; remove it if the file gains a durable navigation responsibility.");
76
+ }
77
+ if (candidate) {
78
+ const action = candidate.priority === "low" ? "review-or-disposition" : "review-card";
79
+ const message = candidate.priority === "low"
80
+ ? "Review the low-priority signal, but do not add a card unless the file owns a durable navigation responsibility."
81
+ : "Review the candidate and either add one durable card with exact coverage or record an exact-path disposition with a concrete reason.";
82
+ return buildFileExplanation(result.repositoryId, file, moduleKey, "candidate", navigationCards, coverageCards, candidate, null,
83
+ candidate.reasons, action, message);
84
+ }
85
+ const reasons = /\.d\.[cm]?ts$/u.test(file) ? ["declaration-file"] : ["no-ranked-audit-signal"];
86
+ return buildFileExplanation(result.repositoryId, file, moduleKey, "not-candidate", navigationCards, coverageCards, null, disposition, reasons,
87
+ "no-card-needed", "Do not add a card solely for coverage; revisit only if the file gains a durable responsibility or stronger structural signals.");
88
+ }
89
+
90
+ async function analyzeAuditProject(root) {
31
91
  const project = await scanProject(root);
32
92
  const fileByPath = new Map(
33
93
  project.fileRecords.map((record) => [toPosix(record.relativePath), record]),
@@ -197,13 +257,41 @@ export async function auditProject(root) {
197
257
  suppressedCandidates: dispositions.filter((disposition) => disposition.status === "suppressed").length,
198
258
  staleDispositions: dispositions.filter((disposition) => disposition.status === "stale").length,
199
259
  };
200
- return {
260
+ const result = {
201
261
  schemaVersion: AUDIT_SCHEMA_VERSION,
202
262
  repositoryId: project.config.repositoryId,
203
263
  summary,
204
264
  candidates: activeCandidates,
205
265
  dispositions,
206
266
  };
267
+ return { project, fileByPath, candidates, result };
268
+ }
269
+
270
+ function normalizeExplanationPath(root, inputPath) {
271
+ if (typeof inputPath !== "string" || inputPath.trim() === "") throw new TypeError("explain requires one file path.");
272
+ const rootPath = path.resolve(root);
273
+ const absolutePath = path.resolve(rootPath, inputPath);
274
+ const relativePath = path.relative(rootPath, absolutePath);
275
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
276
+ throw new RangeError("explain path must name one file inside the repository root.");
277
+ }
278
+ return toPosix(relativePath);
279
+ }
280
+
281
+ function buildFileExplanation(repositoryId, file, moduleKey, status, navigationCards, coverageCards, candidate, disposition, reasons, action, message) {
282
+ return {
283
+ schemaVersion: FILE_EXPLANATION_SCHEMA_VERSION,
284
+ repositoryId,
285
+ path: file,
286
+ moduleKey,
287
+ status,
288
+ navigationCards,
289
+ coverageCards,
290
+ candidate,
291
+ disposition,
292
+ reasons,
293
+ recommendation: { action, message },
294
+ };
207
295
  }
208
296
 
209
297
  function classifyStaleDisposition(file, fileByPath, exactCardPaths, coveredModules) {
package/src/cli.js CHANGED
@@ -38,7 +38,7 @@ import { renderGraphNode } from "./graph.js";
38
38
  import { getAgentToolDefinitions } from "./agent-protocol.js";
39
39
  import { loadPromptPrefixBundle } from "./prompt-bundle.js";
40
40
  import { diagnosticsToEditor, getEditorIntegration } from "./editor.js";
41
- import { AUDIT_PRIORITIES, auditHasFindings, auditProject } from "./audit.js";
41
+ import { AUDIT_PRIORITIES, auditHasFindings, auditProject, explainProjectFile } from "./audit.js";
42
42
  import { migrateProject } from "./migration.js";
43
43
 
44
44
  const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file", "--fail-on", "--output"]);
@@ -56,6 +56,7 @@ const COMMAND_OPTIONS = Object.freeze({
56
56
  doctor: new Set(["--root", "--json"]),
57
57
  migrate: new Set(["--check", "--write", "--root", "--json"]),
58
58
  audit: new Set(["--root", "--json", "--summary", "--fail-on", "--output"]),
59
+ explain: new Set(["--root", "--json"]),
59
60
  spec: new Set(["--root", "--json"]),
60
61
  tools: new Set(["--json"]),
61
62
  bundle: new Set(["--root", "--json"]),
@@ -106,6 +107,8 @@ export async function runCli(argv) {
106
107
  return runMigrate(root, args, json);
107
108
  case "audit":
108
109
  return runAudit(root, args, json);
110
+ case "explain":
111
+ return runExplain(root, args, json);
109
112
  case "spec":
110
113
  return runSpec(json);
111
114
  case "bundle":
@@ -364,6 +367,44 @@ async function runAudit(root, args, json) {
364
367
  return auditHasFindings(result, failOn) ? 1 : 0;
365
368
  }
366
369
 
370
+ async function runExplain(root, args, json) {
371
+ const files = getPositionals(args);
372
+ if (files.length !== 1) throw usageError("explain requires exactly one file path.");
373
+ let result;
374
+ try {
375
+ result = await explainProjectFile(root, files[0]);
376
+ } catch (error) {
377
+ if ((error instanceof TypeError || error instanceof RangeError) && /^explain (?:requires|path)/u.test(error.message)) {
378
+ throw usageError(error.message);
379
+ }
380
+ throw error;
381
+ }
382
+ if (json) {
383
+ console.log(JSON.stringify(result, null, 2));
384
+ } else {
385
+ console.log(`file ${result.path}`);
386
+ console.log(`status ${result.status}`);
387
+ if (result.moduleKey) console.log(`module ${result.moduleKey}`);
388
+ for (const card of result.navigationCards) console.log(`card ${card.scope} @${card.id} ${card.path}`);
389
+ for (const card of result.coverageCards) {
390
+ if (!result.navigationCards.some((item) => item.id === card.id && item.path === card.path)) {
391
+ console.log(`coverage ${card.scope} @${card.id} ${card.path}`);
392
+ }
393
+ }
394
+ if (result.candidate) {
395
+ console.log(`candidate ${result.candidate.priority} ${result.candidate.path} score=${result.candidate.score}`);
396
+ console.log(`why ${result.candidate.reasons.join(",")}`);
397
+ const signals = Object.entries(result.candidate.signals)
398
+ .filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value))
399
+ .map(([name, value]) => `${name}=${Array.isArray(value) ? value.join("|") : value}`);
400
+ if (signals.length > 0) console.log(`signals ${signals.join(",")}`);
401
+ }
402
+ if (result.disposition) console.log(`disposition ${result.disposition.status}: ${result.disposition.reason}`);
403
+ console.log(`next ${result.recommendation.action}: ${result.recommendation.message}`);
404
+ }
405
+ return result.status === "not-scanned" ? 1 : 0;
406
+ }
407
+
367
408
  function runSpec(json) {
368
409
  const spec = {
369
410
  specVersion: SPEC_VERSION,
@@ -522,6 +563,7 @@ Usage
522
563
  llmnav doctor
523
564
  llmnav migrate [--check|--write]
524
565
  llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low]
566
+ llmnav explain <file>
525
567
  llmnav spec
526
568
  llmnav tools [--json]
527
569
  llmnav bundle [--json]
package/src/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export type LlmnavStability = "architecture" | "contract" | "implementation";
3
3
  export type DiagnosticSeverity = "error" | "warning" | "info";
4
4
  export type AuditPriority = "high" | "medium" | "low";
5
5
  export type AuditDispositionStaleReason = "file-not-scanned" | "already-carded" | "not-a-candidate";
6
+ export type FileExplanationStatus = "candidate" | "suppressed" | "carded" | "covered" | "not-candidate" | "not-scanned" | "stale-disposition";
6
7
 
7
8
  export interface LlmnavCard {
8
9
  scope: LlmnavScope;
@@ -119,6 +120,23 @@ export interface AuditResult {
119
120
  >;
120
121
  }
121
122
 
123
+ export interface FileExplanationResult {
124
+ schemaVersion: 1;
125
+ repositoryId: string;
126
+ path: string;
127
+ moduleKey: string | null;
128
+ status: FileExplanationStatus;
129
+ navigationCards: Array<{ id: string; scope: LlmnavScope; path: string }>;
130
+ coverageCards: Array<{ id: string; scope: "file" | "module"; path: string }>;
131
+ candidate: AuditCandidate | null;
132
+ disposition: AuditResult["dispositions"][number] | null;
133
+ reasons: string[];
134
+ recommendation: {
135
+ action: "review-card" | "review-or-disposition" | "keep-or-review-disposition" | "keep-current-coverage" | "no-card-needed" | "check-scan-configuration" | "remove-or-review-disposition";
136
+ message: string;
137
+ };
138
+ }
139
+
122
140
  export interface IndexedLocation {
123
141
  path: string;
124
142
  startLine: number;
@@ -297,7 +315,7 @@ export interface LlmnavConfig {
297
315
 
298
316
  export interface AgentToolDefinition {
299
317
  schemaVersion: 1;
300
- name: "llmnav_query" | "llmnav_show" | "llmnav_context" | "llmnav_check";
318
+ name: "llmnav_query" | "llmnav_show" | "llmnav_context" | "llmnav_check" | "llmnav_explain";
301
319
  description: string;
302
320
  inputSchema: {
303
321
  type: "object";
@@ -309,7 +327,7 @@ export interface AgentToolDefinition {
309
327
 
310
328
  export interface AgentOperationResult<T = unknown> {
311
329
  schemaVersion: 1;
312
- operation: "query" | "show" | "context" | "check" | "unknown";
330
+ operation: "query" | "show" | "context" | "check" | "explain" | "unknown";
313
331
  ok: boolean;
314
332
  data: T | null;
315
333
  error: { code: string; message: string } | null;
@@ -621,6 +639,7 @@ export interface EvaluationResult {
621
639
  export const AGENT_PROTOCOL: string;
622
640
  export const AUDIT_PRIORITIES: readonly AuditPriority[];
623
641
  export const AUDIT_SCHEMA_VERSION: 1;
642
+ export const FILE_EXPLANATION_SCHEMA_VERSION: 1;
624
643
  export const AGENT_OPERATION_SCHEMA_VERSION: 1;
625
644
  export const AGENT_TOOL_SCHEMA_VERSION: 1;
626
645
  export const BOUNDARY_KINDS: readonly DetectedBoundary["kind"][];
@@ -698,6 +717,7 @@ export function loadConfig(root: string): Promise<{ config: LlmnavConfig; config
698
717
  export function validateConfig(config: LlmnavConfig, configPath?: string): void;
699
718
  export function auditProject(root: string): Promise<AuditResult>;
700
719
  export function auditHasFindings(result: AuditResult, minimumPriority?: AuditPriority | "none"): boolean;
720
+ export function explainProjectFile(root: string, file: string): Promise<FileExplanationResult>;
701
721
  export function findAttachedDeclaration(source: string, block: LlmnavBlock, filePath: string): Declaration | null;
702
722
  export function extractImports(source: string, filePath: string): string[];
703
723
  export function doctorProject(root: string): Promise<{ ok: boolean; checks: Array<{ name: string; ok: boolean; message: string }> }>;
package/src/index.js CHANGED
@@ -14,7 +14,14 @@ export {
14
14
  } from "./prompt-bundle.js";
15
15
  export { compareCardIndexes, describeAffectedBoundaries, describeAffectedCatalogs } from "./changes.js";
16
16
  export { loadConfig, validateConfig } from "./config.js";
17
- export { AUDIT_PRIORITIES, AUDIT_SCHEMA_VERSION, auditHasFindings, auditProject } from "./audit.js";
17
+ export {
18
+ AUDIT_PRIORITIES,
19
+ AUDIT_SCHEMA_VERSION,
20
+ FILE_EXPLANATION_SCHEMA_VERSION,
21
+ auditHasFindings,
22
+ auditProject,
23
+ explainProjectFile,
24
+ } from "./audit.js";
18
25
  export { BOUNDARY_KINDS, detectBoundaries } from "./boundaries.js";
19
26
  export {
20
27
  diagnosticsToEditor,
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.8.0";
13
+ export const PACKAGE_VERSION = "0.9.0";
14
14
  export const SPEC_VERSION = "1";
15
15
 
16
16
  export const SCOPES = Object.freeze(["file", "module", "symbol"]);