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 +6 -0
- package/README.md +2 -1
- package/docs/agent-integration.md +2 -2
- package/docs/api.md +5 -2
- package/docs/cli.md +10 -0
- package/package.json +1 -1
- package/src/agent-protocol.js +6 -0
- package/src/agent-tools.js +3 -0
- package/src/agents.js +2 -0
- package/src/audit.js +91 -3
- package/src/cli.js +43 -1
- package/src/index.d.ts +22 -2
- package/src/index.js +8 -1
- package/src/spec.js +1 -1
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.
|
|
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
|
|
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
|
|
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
package/src/agent-protocol.js
CHANGED
|
@@ -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);
|
package/src/agent-tools.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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.
|
|
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"]);
|