llmnav 0.7.4 → 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 +12 -0
- package/README.md +4 -3
- package/docs/agent-integration.md +2 -2
- package/docs/api.md +5 -2
- package/docs/cli.md +11 -1
- package/docs/configuration.md +22 -0
- package/docs/quickstart.md +1 -1
- package/package.json +1 -1
- package/schema/config.schema.json +28 -0
- package/src/agent-protocol.js +6 -0
- package/src/agent-tools.js +3 -0
- package/src/agents.js +3 -1
- package/src/audit.js +129 -9
- package/src/cli.js +48 -2
- package/src/config.js +41 -0
- package/src/index.d.ts +32 -2
- package/src/index.js +8 -1
- package/src/spec.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ 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
|
+
|
|
15
|
+
## [0.8.0] — 2026-08-15
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
* Added exact-path audit dispositions with mandatory review reasons, deterministic suppression, and stale-decision reporting so intentional non-cards do not reappear without becoming hidden permanent ignores.
|
|
20
|
+
|
|
9
21
|
## [0.7.4] — 2026-08-14
|
|
10
22
|
|
|
11
23
|
### Fixed
|
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 |
|
|
@@ -253,7 +254,7 @@ Do not annotate trivial getters, generated files, obvious wrappers, every test f
|
|
|
253
254
|
|
|
254
255
|
## Current implementation boundary
|
|
255
256
|
|
|
256
|
-
|
|
257
|
+
The deterministic coverage audit 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. Exact reviewed dispositions suppress known non-boundaries while stale dispositions expose obsolete decisions. 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.
|
|
257
258
|
|
|
258
259
|
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.
|
|
259
260
|
|
|
@@ -294,7 +295,7 @@ The project uses the Node.js standard library and built-in test runner. There is
|
|
|
294
295
|
|
|
295
296
|
## Status
|
|
296
297
|
|
|
297
|
-
LLMNav is an experimental protocol and a usable v0.
|
|
298
|
+
LLMNav is an experimental protocol and a usable v0.8 CLI. The source format remains `llmnav/1`; npm package changes and source-grammar changes are versioned independently.
|
|
298
299
|
|
|
299
300
|
## License
|
|
300
301
|
|
|
@@ -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. 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
|
@@ -39,7 +39,17 @@ Reads the selected source set and ranks files that lack file or module cards. Si
|
|
|
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. `--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.
|
|
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
|
+
|
|
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.
|
|
43
53
|
|
|
44
54
|
## `llmnav check`
|
|
45
55
|
|
package/docs/configuration.md
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"excludeDirectories": ["node_modules", "dist", "target"],
|
|
15
15
|
"excludeFiles": ["*.generated.*", "*.gen.*"],
|
|
16
16
|
"coverageRules": [],
|
|
17
|
+
"audit": {
|
|
18
|
+
"dispositions": []
|
|
19
|
+
},
|
|
17
20
|
"graph": {
|
|
18
21
|
"indexFiles": []
|
|
19
22
|
},
|
|
@@ -90,6 +93,25 @@ A coverage rule checks files already selected by `sourceRoots` and extension fil
|
|
|
90
93
|
|
|
91
94
|
Run `llmnav audit` to obtain path-specific candidate rules. A suggestion is not applied automatically and should be accepted only after the file's durable responsibility is confirmed. This separation prevents structural heuristics from creating vague or stale semantic cards.
|
|
92
95
|
|
|
96
|
+
## Audit dispositions
|
|
97
|
+
|
|
98
|
+
When a candidate has been reviewed and intentionally does not need a semantic card, record that decision instead of repeatedly ignoring it:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"audit": {
|
|
103
|
+
"dispositions": [
|
|
104
|
+
{
|
|
105
|
+
"path": "src/adapters/local-cache.ts",
|
|
106
|
+
"reason": "This private adapter mirrors its carded module contract and owns no durable policy."
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Each disposition targets one exact normalized repository-relative path. Globs, trailing slashes, backslashes, duplicate paths, and reasons shorter than 12 characters are rejected. A matching candidate is omitted from the active queue and reported as `suppressed`. If the file disappears, gains card coverage, or stops qualifying as a candidate, the disposition is reported as `stale` with a machine-readable reason so obsolete decisions remain visible.
|
|
114
|
+
|
|
93
115
|
## Lint profile
|
|
94
116
|
|
|
95
117
|
Byte and field-count limits prevent semantic cards from becoming mini-documents. `maxSemanticRatio` produces a warning after the scanned source exceeds `minimumSourceBytesForRatio`.
|
package/docs/quickstart.md
CHANGED
|
@@ -23,7 +23,7 @@ Start with high and medium candidates. The audit explains whether a file is a pa
|
|
|
23
23
|
|
|
24
24
|
In Go repositories, candidates represent packages. Full imports below a repository-contained `go.mod` contribute package fan-in, `cmd/*/main.go` is treated as a command entrypoint, and `_test.go` files do not create separate production candidates. Put one `module` card on a durable package representative; do not repeat the same package role on every Go file.
|
|
25
25
|
|
|
26
|
-
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.
|
|
26
|
+
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`. If review confirms that a candidate owns no durable navigation responsibility, add its exact path and a concrete reason to `audit.dispositions`. Never use a glob to dismiss a directory. The audit suppresses the reviewed candidate but reports the entry as stale when the file or its classification changes. Use `npx llmnav audit --fail-on high` in CI only after the initial review.
|
|
27
27
|
|
|
28
28
|
## Annotate a module boundary
|
|
29
29
|
|
package/package.json
CHANGED
|
@@ -101,6 +101,34 @@
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
|
+
"audit": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"additionalProperties": false,
|
|
107
|
+
"properties": {
|
|
108
|
+
"dispositions": {
|
|
109
|
+
"type": "array",
|
|
110
|
+
"items": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"required": [
|
|
113
|
+
"path",
|
|
114
|
+
"reason"
|
|
115
|
+
],
|
|
116
|
+
"additionalProperties": false,
|
|
117
|
+
"properties": {
|
|
118
|
+
"path": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"pattern": "^(?!/|[A-Za-z]:|.*(?:^|/)\\.\\.(?:/|$)|.*[\\\\*?\\[\\]{}])[^/]+(?:/[^/]+)*$"
|
|
121
|
+
},
|
|
122
|
+
"reason": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"minLength": 12,
|
|
125
|
+
"maxLength": 280
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
104
132
|
"graph": {
|
|
105
133
|
"type": "object",
|
|
106
134
|
"additionalProperties": false,
|
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
|
@@ -30,7 +30,9 @@ Keep an existing LLMNav ID when a symbol or file is renamed or moved. Change \`r
|
|
|
30
30
|
|
|
31
31
|
Do not add hand-maintained \`calls\`, \`imports\`, \`references\`, \`implements\`, \`exports\`, or \`overrides\` relations. Do not put paths, line numbers, commit hashes, timestamps, callers, or current signatures in LLMNav source comments.
|
|
32
32
|
|
|
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.
|
|
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
|
+
|
|
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.
|
|
34
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}`;
|
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]),
|
|
@@ -162,22 +222,82 @@ export async function auditProject(root) {
|
|
|
162
222
|
priorityRank(left.priority) - priorityRank(right.priority) ||
|
|
163
223
|
right.score - left.score ||
|
|
164
224
|
compareText(left.path, right.path));
|
|
225
|
+
const candidateByPath = new Map(candidates.map((candidate) => [candidate.path, candidate]));
|
|
226
|
+
const dispositions = [...project.config.audit.dispositions]
|
|
227
|
+
.sort((left, right) => compareText(left.path, right.path))
|
|
228
|
+
.map((disposition) => {
|
|
229
|
+
const candidate = candidateByPath.get(disposition.path);
|
|
230
|
+
if (candidate) {
|
|
231
|
+
candidateByPath.delete(disposition.path);
|
|
232
|
+
return {
|
|
233
|
+
path: disposition.path,
|
|
234
|
+
reason: disposition.reason,
|
|
235
|
+
status: "suppressed",
|
|
236
|
+
priority: candidate.priority,
|
|
237
|
+
score: candidate.score,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
path: disposition.path,
|
|
242
|
+
reason: disposition.reason,
|
|
243
|
+
status: "stale",
|
|
244
|
+
staleReason: classifyStaleDisposition(disposition.path, fileByPath, exactCardPaths, coveredModules),
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
const activeCandidates = candidates.filter((candidate) => candidateByPath.has(candidate.path));
|
|
165
248
|
const summary = {
|
|
166
249
|
analyzedFiles: fileByPath.size,
|
|
167
250
|
cardedFiles: cardedFilePaths.size,
|
|
168
251
|
filesWithoutModuleCards: fileByPath.size - cardedFilePaths.size,
|
|
169
|
-
candidates:
|
|
170
|
-
high:
|
|
171
|
-
medium:
|
|
172
|
-
low:
|
|
173
|
-
coverageSuggestions:
|
|
252
|
+
candidates: activeCandidates.length,
|
|
253
|
+
high: activeCandidates.filter((candidate) => candidate.priority === "high").length,
|
|
254
|
+
medium: activeCandidates.filter((candidate) => candidate.priority === "medium").length,
|
|
255
|
+
low: activeCandidates.filter((candidate) => candidate.priority === "low").length,
|
|
256
|
+
coverageSuggestions: activeCandidates.filter((candidate) => candidate.suggestedCoverageRule !== null).length,
|
|
257
|
+
suppressedCandidates: dispositions.filter((disposition) => disposition.status === "suppressed").length,
|
|
258
|
+
staleDispositions: dispositions.filter((disposition) => disposition.status === "stale").length,
|
|
174
259
|
};
|
|
175
|
-
|
|
260
|
+
const result = {
|
|
176
261
|
schemaVersion: AUDIT_SCHEMA_VERSION,
|
|
177
262
|
repositoryId: project.config.repositoryId,
|
|
178
263
|
summary,
|
|
179
|
-
candidates,
|
|
264
|
+
candidates: activeCandidates,
|
|
265
|
+
dispositions,
|
|
180
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
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function classifyStaleDisposition(file, fileByPath, exactCardPaths, coveredModules) {
|
|
298
|
+
if (!fileByPath.has(file)) return "file-not-scanned";
|
|
299
|
+
if (exactCardPaths.has(file) || coveredModules.has(moduleKeyForFile(file))) return "already-carded";
|
|
300
|
+
return "not-a-candidate";
|
|
181
301
|
}
|
|
182
302
|
|
|
183
303
|
function groupFilesByModule(fileByPath) {
|
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":
|
|
@@ -349,17 +352,59 @@ async function runAudit(root, args, json) {
|
|
|
349
352
|
const { summary } = result;
|
|
350
353
|
console.log(
|
|
351
354
|
`audit files=${summary.analyzedFiles} carded=${summary.cardedFiles} candidates=${summary.candidates} ` +
|
|
352
|
-
`high=${summary.high} medium=${summary.medium} low=${summary.low}
|
|
355
|
+
`high=${summary.high} medium=${summary.medium} low=${summary.low} ` +
|
|
356
|
+
`suppressed=${summary.suppressedCandidates} stale-dispositions=${summary.staleDispositions}`,
|
|
353
357
|
);
|
|
354
358
|
for (const candidate of result.candidates.filter((item) => item.priority !== "low")) {
|
|
355
359
|
console.log(`${candidate.priority} ${candidate.path} score=${candidate.score} ${candidate.reasons.join(",")}`);
|
|
356
360
|
}
|
|
357
361
|
if (summary.low > 0) console.log(`${summary.low} low-priority candidate(s) are available in --json output.`);
|
|
362
|
+
for (const disposition of result.dispositions.filter((item) => item.status === "stale")) {
|
|
363
|
+
console.log(`stale ${disposition.path} ${disposition.staleReason}: ${disposition.reason}`);
|
|
364
|
+
}
|
|
358
365
|
if (outputPath) console.log(`wrote ${relativePosix(root, outputPath)}`);
|
|
359
366
|
}
|
|
360
367
|
return auditHasFindings(result, failOn) ? 1 : 0;
|
|
361
368
|
}
|
|
362
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
|
+
|
|
363
408
|
function runSpec(json) {
|
|
364
409
|
const spec = {
|
|
365
410
|
specVersion: SPEC_VERSION,
|
|
@@ -518,6 +563,7 @@ Usage
|
|
|
518
563
|
llmnav doctor
|
|
519
564
|
llmnav migrate [--check|--write]
|
|
520
565
|
llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low]
|
|
566
|
+
llmnav explain <file>
|
|
521
567
|
llmnav spec
|
|
522
568
|
llmnav tools [--json]
|
|
523
569
|
llmnav bundle [--json]
|
package/src/config.js
CHANGED
|
@@ -30,7 +30,9 @@ const LINT_KEYS = new Set(Object.keys(DEFAULT_CONFIG.lint));
|
|
|
30
30
|
const GENERATION_KEYS = new Set(Object.keys(DEFAULT_CONFIG.generation));
|
|
31
31
|
const EVALUATION_KEYS = new Set(Object.keys(DEFAULT_CONFIG.evaluation));
|
|
32
32
|
const GRAPH_KEYS = new Set(Object.keys(DEFAULT_CONFIG.graph));
|
|
33
|
+
const AUDIT_KEYS = new Set(Object.keys(DEFAULT_CONFIG.audit));
|
|
33
34
|
const COVERAGE_KEYS = new Set(["name", "match", "scope", "requiredFields"]);
|
|
35
|
+
const AUDIT_DISPOSITION_KEYS = new Set(["path", "reason"]);
|
|
34
36
|
|
|
35
37
|
export async function loadConfig(root) {
|
|
36
38
|
const configPath = path.join(root, ".llmnav", "config.json");
|
|
@@ -72,6 +74,7 @@ export function validateConfig(config, configPath = ".llmnav/config.json") {
|
|
|
72
74
|
validateStringArray(config.excludeDirectories, "excludeDirectories", problems, { unique: true });
|
|
73
75
|
validateStringArray(config.excludeFiles, "excludeFiles", problems, { unique: true });
|
|
74
76
|
validateCoverageRules(config.coverageRules, problems);
|
|
77
|
+
validateAudit(config.audit, problems);
|
|
75
78
|
validateGraph(config.graph, problems);
|
|
76
79
|
|
|
77
80
|
validateLint(config.lint, problems);
|
|
@@ -83,6 +86,44 @@ export function validateConfig(config, configPath = ".llmnav/config.json") {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
function validateAudit(audit, problems) {
|
|
90
|
+
if (!isObject(audit)) {
|
|
91
|
+
problems.push("audit must be an object");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
validateObjectKeys(audit, AUDIT_KEYS, "audit", problems);
|
|
95
|
+
if (!Array.isArray(audit.dispositions)) {
|
|
96
|
+
problems.push("audit.dispositions must be an array");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const seenPaths = new Set();
|
|
100
|
+
for (const [index, disposition] of audit.dispositions.entries()) {
|
|
101
|
+
const name = `audit.dispositions[${index}]`;
|
|
102
|
+
if (!isObject(disposition)) {
|
|
103
|
+
problems.push(`${name} must be an object`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
validateObjectKeys(disposition, AUDIT_DISPOSITION_KEYS, name, problems);
|
|
107
|
+
const pathProblem = validateProjectRelativePath(disposition.path);
|
|
108
|
+
if (pathProblem) {
|
|
109
|
+
problems.push(`${name}.path ${pathProblem}`);
|
|
110
|
+
} else {
|
|
111
|
+
const normalizedPath = path.posix.normalize(disposition.path);
|
|
112
|
+
if (normalizedPath !== disposition.path || disposition.path.endsWith("/") || /[*?[\]{}]/u.test(disposition.path)) {
|
|
113
|
+
problems.push(`${name}.path must be one exact normalized repository-relative path without glob syntax`);
|
|
114
|
+
}
|
|
115
|
+
const comparisonPath = normalizedPath.normalize("NFKC").toLocaleLowerCase("en-US");
|
|
116
|
+
if (seenPaths.has(comparisonPath)) problems.push(`${name}.path duplicates an earlier disposition`);
|
|
117
|
+
seenPaths.add(comparisonPath);
|
|
118
|
+
}
|
|
119
|
+
if (typeof disposition.reason !== "string" || disposition.reason.trim().length < 12) {
|
|
120
|
+
problems.push(`${name}.reason must explain the review decision in at least 12 characters`);
|
|
121
|
+
} else if (disposition.reason.length > 280) {
|
|
122
|
+
problems.push(`${name}.reason must not exceed 280 characters`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
86
127
|
function validateGraph(graph, problems) {
|
|
87
128
|
if (!isObject(graph)) {
|
|
88
129
|
problems.push("graph must be an object");
|
package/src/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type LlmnavScope = "file" | "module" | "symbol";
|
|
|
2
2
|
export type LlmnavStability = "architecture" | "contract" | "implementation";
|
|
3
3
|
export type DiagnosticSeverity = "error" | "warning" | "info";
|
|
4
4
|
export type AuditPriority = "high" | "medium" | "low";
|
|
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";
|
|
5
7
|
|
|
6
8
|
export interface LlmnavCard {
|
|
7
9
|
scope: LlmnavScope;
|
|
@@ -108,8 +110,31 @@ export interface AuditResult {
|
|
|
108
110
|
medium: number;
|
|
109
111
|
low: number;
|
|
110
112
|
coverageSuggestions: number;
|
|
113
|
+
suppressedCandidates: number;
|
|
114
|
+
staleDispositions: number;
|
|
111
115
|
};
|
|
112
116
|
candidates: AuditCandidate[];
|
|
117
|
+
dispositions: Array<
|
|
118
|
+
| { path: string; reason: string; status: "suppressed"; priority: AuditPriority; score: number }
|
|
119
|
+
| { path: string; reason: string; status: "stale"; staleReason: AuditDispositionStaleReason }
|
|
120
|
+
>;
|
|
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
|
+
};
|
|
113
138
|
}
|
|
114
139
|
|
|
115
140
|
export interface IndexedLocation {
|
|
@@ -247,6 +272,9 @@ export interface LlmnavConfig {
|
|
|
247
272
|
excludeDirectories: string[];
|
|
248
273
|
excludeFiles: string[];
|
|
249
274
|
coverageRules: Array<Record<string, unknown>>;
|
|
275
|
+
audit: {
|
|
276
|
+
dispositions: Array<{ path: string; reason: string }>;
|
|
277
|
+
};
|
|
250
278
|
graph: {
|
|
251
279
|
indexFiles: string[];
|
|
252
280
|
};
|
|
@@ -287,7 +315,7 @@ export interface LlmnavConfig {
|
|
|
287
315
|
|
|
288
316
|
export interface AgentToolDefinition {
|
|
289
317
|
schemaVersion: 1;
|
|
290
|
-
name: "llmnav_query" | "llmnav_show" | "llmnav_context" | "llmnav_check";
|
|
318
|
+
name: "llmnav_query" | "llmnav_show" | "llmnav_context" | "llmnav_check" | "llmnav_explain";
|
|
291
319
|
description: string;
|
|
292
320
|
inputSchema: {
|
|
293
321
|
type: "object";
|
|
@@ -299,7 +327,7 @@ export interface AgentToolDefinition {
|
|
|
299
327
|
|
|
300
328
|
export interface AgentOperationResult<T = unknown> {
|
|
301
329
|
schemaVersion: 1;
|
|
302
|
-
operation: "query" | "show" | "context" | "check" | "unknown";
|
|
330
|
+
operation: "query" | "show" | "context" | "check" | "explain" | "unknown";
|
|
303
331
|
ok: boolean;
|
|
304
332
|
data: T | null;
|
|
305
333
|
error: { code: string; message: string } | null;
|
|
@@ -611,6 +639,7 @@ export interface EvaluationResult {
|
|
|
611
639
|
export const AGENT_PROTOCOL: string;
|
|
612
640
|
export const AUDIT_PRIORITIES: readonly AuditPriority[];
|
|
613
641
|
export const AUDIT_SCHEMA_VERSION: 1;
|
|
642
|
+
export const FILE_EXPLANATION_SCHEMA_VERSION: 1;
|
|
614
643
|
export const AGENT_OPERATION_SCHEMA_VERSION: 1;
|
|
615
644
|
export const AGENT_TOOL_SCHEMA_VERSION: 1;
|
|
616
645
|
export const BOUNDARY_KINDS: readonly DetectedBoundary["kind"][];
|
|
@@ -688,6 +717,7 @@ export function loadConfig(root: string): Promise<{ config: LlmnavConfig; config
|
|
|
688
717
|
export function validateConfig(config: LlmnavConfig, configPath?: string): void;
|
|
689
718
|
export function auditProject(root: string): Promise<AuditResult>;
|
|
690
719
|
export function auditHasFindings(result: AuditResult, minimumPriority?: AuditPriority | "none"): boolean;
|
|
720
|
+
export function explainProjectFile(root: string, file: string): Promise<FileExplanationResult>;
|
|
691
721
|
export function findAttachedDeclaration(source: string, block: LlmnavBlock, filePath: string): Declaration | null;
|
|
692
722
|
export function extractImports(source: string, filePath: string): string[];
|
|
693
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"]);
|
|
@@ -208,6 +208,9 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
208
208
|
excludeDirectories: [...DEFAULT_EXCLUDED_DIRECTORIES],
|
|
209
209
|
excludeFiles: ["*.min.js", "*.bundle.js", "*.generated.*", "*.gen.*"],
|
|
210
210
|
coverageRules: [],
|
|
211
|
+
audit: {
|
|
212
|
+
dispositions: [],
|
|
213
|
+
},
|
|
211
214
|
graph: {
|
|
212
215
|
indexFiles: [],
|
|
213
216
|
},
|