llmnav 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +7 -3
- package/ROADMAP.md +11 -0
- package/docs/agent-integration.md +6 -2
- package/docs/api.md +15 -0
- package/docs/architecture.md +7 -1
- package/docs/ci.md +5 -1
- package/docs/cli.md +13 -1
- package/docs/configuration.md +3 -1
- package/docs/faq.md +7 -3
- package/docs/migration.md +1 -1
- package/docs/publishing.md +2 -2
- package/docs/quickstart.md +10 -0
- package/examples/provider-neutral-host.mjs +11 -0
- package/package.json +1 -1
- package/src/agents.js +2 -0
- package/src/audit.js +342 -0
- package/src/boundaries.js +10 -1
- package/src/cli.js +49 -2
- package/src/config.js +11 -0
- package/src/declaration.js +11 -0
- package/src/doctor.js +11 -0
- package/src/files.js +11 -0
- package/src/formatter.js +11 -0
- package/src/index.d.ts +45 -0
- package/src/index.js +1 -0
- package/src/initializer.js +1 -0
- package/src/project.js +12 -0
- package/src/registry.js +11 -0
- package/src/spec.js +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ The npm package follows Semantic Versioning. The `llmnav/N` source protocol is v
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.1] — 2026-08-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* Added compact `audit --summary` output and repository-contained `audit --output <path>` reports.
|
|
14
|
+
* Added nested workspace package entrypoint discovery and Rust/Tauri command, module, and platform-lifecycle signals.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* Excluded Bun and pnpm package-manager caches from default source discovery.
|
|
19
|
+
|
|
20
|
+
## [0.6.0] — 2026-08-10
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
* Added semantic boundaries for configuration, source discovery and formatting, project scanning, declaration extraction, diagnostics, ID lifecycle, and the public protocol specification, with targeted coverage and retrieval regression queries.
|
|
25
|
+
* Added a deterministic, read-only `audit` command and public API that ranks likely missing module boundaries, explains every signal, suggests narrow coverage rules, and supports opt-in CI thresholds.
|
|
26
|
+
* Added generated agent guidance and an `llmnav:audit` package script so new repositories review coverage after initialization and structural changes.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
* Suppressed declaration files and reduced false positives from tests, fixtures, benchmarks, broad utilities, and re-export barrels.
|
|
31
|
+
|
|
9
32
|
## [0.5.2] — 2026-08-09
|
|
10
33
|
|
|
11
34
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ It adds compact, stable metadata to a small number of architectural and behavior
|
|
|
6
6
|
|
|
7
7
|
LLMNav is not a documentation generator, an embedding database, or a reason to annotate every function. It is a zero-runtime-dependency Node.js CLI and ESM library for reducing broad repository scans, irrelevant context, stale hand-written links, repeated card tokenization, and avoidable cache invalidation.
|
|
8
8
|
|
|
9
|
-
## What v0.
|
|
9
|
+
## What v0.6 provides
|
|
10
10
|
|
|
11
11
|
* The backward-compatible `llmnav/1` source comment specification
|
|
12
12
|
* A parser and data-loss-resistant canonical formatter
|
|
@@ -31,6 +31,7 @@ LLMNav is not a documentation generator, an embedding database, or a reason to a
|
|
|
31
31
|
* Multilingual alias routing and CJK n-gram retrieval
|
|
32
32
|
* Search regression tests with Recall@1, Recall@5, and MRR
|
|
33
33
|
* Managed instructions for AGENTS.md, Claude Code, GitHub Copilot, and Cursor
|
|
34
|
+
* A deterministic, read-only annotation coverage audit with explicit CI thresholds
|
|
34
35
|
* Linux and Windows CI gates plus npm pack installation smoke tests
|
|
35
36
|
|
|
36
37
|
The package supports Node.js 22 or newer, uses ESM, performs no network requests, and has no runtime dependencies.
|
|
@@ -44,6 +45,8 @@ npx llmnav init --agents all --package-scripts
|
|
|
44
45
|
|
|
45
46
|
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.
|
|
46
47
|
|
|
48
|
+
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.
|
|
49
|
+
|
|
47
50
|
## Add the first card
|
|
48
51
|
|
|
49
52
|
```ts
|
|
@@ -166,6 +169,7 @@ Line-comment cards require an explicit terminator and work with `//`, `#`, and `
|
|
|
166
169
|
| Command | Purpose |
|
|
167
170
|
| --- | --- |
|
|
168
171
|
| `llmnav init` | Create configuration, registry, schemas, agent instructions, and the initial cache |
|
|
172
|
+
| `llmnav audit` | Rank unannotated architectural boundary candidates, with compact or file-backed output |
|
|
169
173
|
| `llmnav check` | Validate cards, relations, coverage rules, and registry state |
|
|
170
174
|
| `llmnav format` | Rewrite safe cards into canonical order and spacing |
|
|
171
175
|
| `llmnav generate` | Incrementally compile and transactionally commit generated artifacts |
|
|
@@ -246,7 +250,7 @@ Do not annotate trivial getters, generated files, obvious wrappers, every test f
|
|
|
246
250
|
|
|
247
251
|
## Current implementation boundary
|
|
248
252
|
|
|
249
|
-
Version 0.
|
|
253
|
+
Version 0.6 adds a deterministic coverage audit that prioritizes root and nested-package entrypoints, structural boundaries, Rust/Tauri runtime signals, and high fan-in modules while suppressing declaration files, dependency caches, test support code, simple barrels, and broad utilities. It suggests narrow coverage rules but never writes cards or invents semantic roles. Use `llmnav audit --summary --json` for a compact automation result or `llmnav audit --json --output .llmnav/audit.json` to keep the full candidate report out of captured stdout.
|
|
250
254
|
|
|
251
255
|
LLMNav does not discover sibling repositories automatically and does not ship an MCP server, embedding database, hosted service, SCIP generator, or complete language-aware call graph. External tools may export the documented compact graph-input schema. Generated structure never writes derived edges into source cards.
|
|
252
256
|
|
|
@@ -287,7 +291,7 @@ The project uses the Node.js standard library and built-in test runner. There is
|
|
|
287
291
|
|
|
288
292
|
## Status
|
|
289
293
|
|
|
290
|
-
LLMNav is an experimental protocol and a usable v0.
|
|
294
|
+
LLMNav is an experimental protocol and a usable v0.6 CLI. The source format remains `llmnav/1`; npm package changes and source-grammar changes are versioned independently.
|
|
291
295
|
|
|
292
296
|
## License
|
|
293
297
|
|
package/ROADMAP.md
CHANGED
|
@@ -62,6 +62,17 @@ Implemented:
|
|
|
62
62
|
* editor integrations and diagnostics
|
|
63
63
|
* provider-neutral integration examples
|
|
64
64
|
|
|
65
|
+
## 0.6 — annotation coverage audit
|
|
66
|
+
|
|
67
|
+
Implemented:
|
|
68
|
+
|
|
69
|
+
* deterministic read-only discovery of unannotated architectural boundary candidates
|
|
70
|
+
* explainable priority signals for package entrypoints, public APIs, structural boundaries, fan-in, exports, and source size
|
|
71
|
+
* false-positive penalties for declaration files, test support code, broad utilities, and re-export barrels
|
|
72
|
+
* opt-in `--fail-on high|medium|low` CI enforcement
|
|
73
|
+
* path-specific coverage-rule suggestions without automatic source annotation
|
|
74
|
+
* audit guidance in generated agent instructions and package scripts
|
|
75
|
+
|
|
65
76
|
## 1.0 criteria
|
|
66
77
|
|
|
67
78
|
The source grammar and generated formats will be declared stable only after use across multiple TypeScript, Go, Rust, Python, and mixed-language repositories. A 1.0 release requires migration tooling, documented compatibility guarantees, benchmark fixtures with published methodology, sustained Windows and Linux verification, and no unresolved high-severity parser or transaction ambiguity.
|
|
@@ -44,8 +44,10 @@ The generated instruction tells an agent to:
|
|
|
44
44
|
5. Keep IDs stable across moves and renames.
|
|
45
45
|
6. Update semantic fields only when meaning changes.
|
|
46
46
|
7. Never hand-maintain call, import, reference, implementation, export, or override relations.
|
|
47
|
-
8. Run
|
|
48
|
-
9.
|
|
47
|
+
8. Run `llmnav audit` after initialization and structural boundary changes, review high and medium candidates, and never annotate automatically.
|
|
48
|
+
9. Encode accepted boundaries in narrow coverage rules and retrieval queries.
|
|
49
|
+
10. Run format, check, and generation after semantic changes.
|
|
50
|
+
11. Fall back to broad search when no credible card is returned.
|
|
49
51
|
|
|
50
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.
|
|
51
53
|
|
|
@@ -112,3 +114,5 @@ An agent should not stop when a repository is partially annotated.
|
|
|
112
114
|
When `query` returns no credible result, the correct fallback is normal symbol search, grep, or language-server navigation. After completing the task, the agent may propose a new card only when the missed boundary satisfies the repository's annotation policy.
|
|
113
115
|
|
|
114
116
|
Search failure alone is not permission to annotate every function sharing a keyword.
|
|
117
|
+
|
|
118
|
+
Audit priority is also not permission to invent a card. It is a deterministic prompt to inspect likely boundaries; durable ownership and invariants still come from the current source and repository contracts.
|
package/docs/api.md
CHANGED
|
@@ -62,6 +62,21 @@ Stat hints are an optimization, not deterministic output. The returned `fileStat
|
|
|
62
62
|
|
|
63
63
|
Attached symbol declarations expose generated `language`, `exported`, `visibility`, optional Go `receiver`, `endOffset`, and declaration `bodyHash` fields. Indexed cards also expose sorted route, event, schema, migration, and command `boundaries` with confidence and evidence.
|
|
64
64
|
|
|
65
|
+
## Audit annotation coverage
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { auditHasFindings, auditProject } from "llmnav";
|
|
69
|
+
|
|
70
|
+
const result = await auditProject(process.cwd());
|
|
71
|
+
for (const candidate of result.candidates) {
|
|
72
|
+
console.log(candidate.priority, candidate.path, candidate.reasons);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (auditHasFindings(result, "high")) process.exitCode = 1;
|
|
76
|
+
```
|
|
77
|
+
|
|
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 the suggested coverage rule into automatic source annotation.
|
|
79
|
+
|
|
65
80
|
## Generate incrementally and transactionally
|
|
66
81
|
|
|
67
82
|
```js
|
package/docs/architecture.md
CHANGED
|
@@ -36,7 +36,7 @@ Generated paths, signatures, callers, references, timestamps, and hashes are nev
|
|
|
36
36
|
{
|
|
37
37
|
"schemaVersion": 1,
|
|
38
38
|
"specVersion": "1",
|
|
39
|
-
"generatedBy": "llmnav@0.
|
|
39
|
+
"generatedBy": "llmnav@0.6.0",
|
|
40
40
|
"repositoryId": "example",
|
|
41
41
|
"contractFingerprints": {
|
|
42
42
|
"schemaVersion": 1,
|
|
@@ -68,6 +68,12 @@ Generated cards may contain a sorted `boundaries` array. LLMNav detects `route`,
|
|
|
68
68
|
|
|
69
69
|
Generation compares the previous and current primary indexes to emit `affectedBoundaries`. The report preserves the card change and hash dimensions while adding module IDs, structural boundary records, outbound relation targets, and reverse semantic dependents. It is returned through the API and `generate --json`; it is not stored in source cards.
|
|
70
70
|
|
|
71
|
+
## Annotation coverage audit
|
|
72
|
+
|
|
73
|
+
The read-only audit combines package entrypoints, public re-export reachability, generated structural boundaries, import fan-in, exported declaration counts, and source size. It lowers the priority of broad utilities, declaration files, non-production support paths, and pure re-export barrels. Every candidate retains its score inputs and deterministic repository-relative path.
|
|
74
|
+
|
|
75
|
+
This analyzer deliberately stops before semantic generation. Structure can identify a file worth inspecting, but it cannot safely invent a durable ID, role, ownership statement, invariant, risk, or relation. Accepted boundaries become explicit source cards, path-specific coverage rules, and retrieval regression queries through normal review.
|
|
76
|
+
|
|
71
77
|
## Deterministic inverted index
|
|
72
78
|
|
|
73
79
|
`.llmnav/cache/search-index.json` schemaVersion 2 with `compact-v1` encoding stores a sorted card-ID table, normalized phrase documents, a sorted token dictionary, and posting lists.
|
package/docs/ci.md
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Consumer repository gates
|
|
4
4
|
|
|
5
|
-
A practical LLMNav gate has
|
|
5
|
+
A practical LLMNav gate has five stages after the repository has reviewed its initial audit baseline.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npx llmnav format --check
|
|
9
|
+
npx llmnav audit --fail-on high
|
|
9
10
|
npx llmnav check --format github
|
|
10
11
|
npx llmnav generate --full --check
|
|
11
12
|
npx llmnav eval
|
|
@@ -13,6 +14,8 @@ npx llmnav eval
|
|
|
13
14
|
|
|
14
15
|
`format --check` rejects non-canonical source-card serialization.
|
|
15
16
|
|
|
17
|
+
`audit --fail-on high` rejects newly exposed high-priority unannotated boundaries. It does not modify source and should not be enabled until the initial candidate list has been reviewed. Medium and low candidates remain advisory by default to avoid turning heuristic scores into blanket annotation policy.
|
|
18
|
+
|
|
16
19
|
`check` rejects invalid meaning, unresolved or retired semantic relations, malformed registry state, missing configured coverage, path escape, and declaration attachment failures.
|
|
17
20
|
|
|
18
21
|
Use `npx llmnav check --format sarif > llmnav.sarif` when a code-scanning system accepts SARIF 2.1.0 rather than GitHub workflow annotations. SARIF output is deterministic for the same ordered diagnostics and uses repository-relative paths.
|
|
@@ -81,6 +84,7 @@ jobs:
|
|
|
81
84
|
cache: npm
|
|
82
85
|
- run: npm ci
|
|
83
86
|
- run: npx llmnav format --check
|
|
87
|
+
- run: npx llmnav audit --fail-on high
|
|
84
88
|
- run: npx llmnav check --format github
|
|
85
89
|
- run: npx llmnav generate --full --check
|
|
86
90
|
- run: npx llmnav eval
|
package/docs/cli.md
CHANGED
|
@@ -25,10 +25,22 @@ Creates project configuration, schema, registry, lexicon, evaluation file, agent
|
|
|
25
25
|
|
|
26
26
|
`--agents` accepts `agents`, `claude`, `copilot`, `cursor`, a comma-separated combination, `all`, or `none`. The default is `agents`.
|
|
27
27
|
|
|
28
|
-
`--package-scripts` adds `llmnav:check`, `llmnav:format`, `llmnav:generate`, and `llmnav:eval` to an existing package.json.
|
|
28
|
+
`--package-scripts` adds `llmnav:audit`, `llmnav:check`, `llmnav:format`, `llmnav:generate`, and `llmnav:eval` to an existing package.json. The audit script gates only high-priority candidates.
|
|
29
29
|
|
|
30
30
|
`--force` refreshes configuration templates, schema, managed agent blocks, and `.llmnav/.gitignore`. It never resets semantic ID history, catalog order, aliases, evaluation cases, or source cards.
|
|
31
31
|
|
|
32
|
+
## `llmnav audit`
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low] [--json]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Reads the selected source set and ranks files that lack file or module cards. Signals include root and nested-package entrypoints, public re-exports, generated command/route/schema/migration/event boundaries, Rust/Tauri runtime boundaries, import fan-in, exported declarations, and source size. Declaration files are omitted; dependency caches, test support code, broad utilities, and pure re-export barrels are excluded or penalized. Candidates whose penalties reduce their score to zero are omitted.
|
|
39
|
+
|
|
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
|
+
|
|
42
|
+
JSON output uses schemaVersion 1, repository-relative paths, deterministic ordering, explainable `reasons` and `signals`, and a path-specific `suggestedCoverageRule` for high and medium candidates. `--summary` omits candidate details. `--output <path>` writes the selected report inside the repository and emits only a compact confirmation envelope to stdout; escaping paths and symbolic-link traversal are rejected. Suggestions require human or agent review: LLMNav cannot infer a durable role, ownership boundary, invariant, or semantic ID from structure alone.
|
|
43
|
+
|
|
32
44
|
## `llmnav check`
|
|
33
45
|
|
|
34
46
|
```sh
|
package/docs/configuration.md
CHANGED
|
@@ -71,7 +71,7 @@ Every source root must remain inside the repository and cannot be a symbolic lin
|
|
|
71
71
|
|
|
72
72
|
`includeExtensions` is an allowlist. LLMNav does not scan Markdown or YAML by default because documentation examples routinely contain card syntax.
|
|
73
73
|
|
|
74
|
-
`excludeDirectories` matches directory names at any depth. `excludeFiles` uses repository-relative globs with `*`, `?`, and `**`.
|
|
74
|
+
`excludeDirectories` matches directory names at any depth. Defaults exclude dependency and tool output including `node_modules`, `.bun-cache`, `.pnpm-store`, `.cache`, build directories, and language targets. `excludeFiles` uses repository-relative globs with `*`, `?`, and `**`.
|
|
75
75
|
|
|
76
76
|
## Coverage rules
|
|
77
77
|
|
|
@@ -88,6 +88,8 @@ Coverage is opt-in and path-specific.
|
|
|
88
88
|
|
|
89
89
|
A coverage rule checks files already selected by `sourceRoots` and extension filters. Its fields are closed and `requiredFields` must name valid LLMNav keys. It should target a real architectural boundary, never an entire source tree.
|
|
90
90
|
|
|
91
|
+
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
|
+
|
|
91
93
|
## Lint profile
|
|
92
94
|
|
|
93
95
|
Byte and field-count limits prevent semantic cards from becoming mini-documents. `maxSemanticRatio` produces a warning after the scanned source exceeds `minimumSourceBytesForRatio`.
|
package/docs/faq.md
CHANGED
|
@@ -8,9 +8,13 @@ No. That makes retrieval noisier and creates a second copy of the program in pro
|
|
|
8
8
|
|
|
9
9
|
JSDoc and docstrings target generated human documentation and language tooling. They do not define durable semantic IDs, controlled effects and risks, append-only catalog order, relation validation, deterministic search artifacts, or search regression gates. They can coexist with LLMNav cards.
|
|
10
10
|
|
|
11
|
-
## Does v0.
|
|
11
|
+
## Does v0.6 change the `llmnav/1` syntax?
|
|
12
12
|
|
|
13
|
-
No. The source grammar remains `llmnav/1`. v0.
|
|
13
|
+
No. The source grammar remains `llmnav/1`. v0.6 adds an independently versioned annotation-audit result beside the existing schemaVersion 1 `index.json` and v0.5 integration contracts.
|
|
14
|
+
|
|
15
|
+
## Does `llmnav audit` write comments automatically?
|
|
16
|
+
|
|
17
|
+
No. It produces an explainable review queue from structural evidence and never edits source. A candidate can still be intentionally cardless; only a reviewer can define its durable ID, ownership, exclusions, and invariants.
|
|
14
18
|
|
|
15
19
|
## Does every query still tokenize every card?
|
|
16
20
|
|
|
@@ -40,7 +44,7 @@ The files are deterministic agent context. Committing them makes drift reviewabl
|
|
|
40
44
|
|
|
41
45
|
## Does LLMNav execute project code?
|
|
42
46
|
|
|
43
|
-
No. Version 0.
|
|
47
|
+
No. Version 0.6 reads text and configured repository-local JSON files, then writes generated artifacts or explicitly requested card formatting. The audit path is read-only. Its tool dispatcher remains local and has no plugin loader, install hook, or network request.
|
|
44
48
|
|
|
45
49
|
## Can cards be written in Korean?
|
|
46
50
|
|
package/docs/migration.md
CHANGED
|
@@ -7,7 +7,7 @@ No source-card migration is required. Keep every `llmnav/1` comment and the exis
|
|
|
7
7
|
Upgrade and regenerate:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install --save-dev llmnav@^0.
|
|
10
|
+
npm install --save-dev llmnav@^0.6.0
|
|
11
11
|
npx llmnav init --agents all
|
|
12
12
|
npx llmnav generate
|
|
13
13
|
npx llmnav doctor
|
package/docs/publishing.md
CHANGED
|
@@ -61,8 +61,8 @@ npm publish --provenance=false --access public
|
|
|
61
61
|
Automated publication:
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
|
-
git tag v0.
|
|
65
|
-
git push origin v0.
|
|
64
|
+
git tag v0.6.0
|
|
65
|
+
git push origin v0.6.0
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
The release workflow rejects a tag that does not match `package.json`. If the exact version is already present in npm, the workflow succeeds only when the registry tarball integrity matches the tagged package; a mismatched package fails closed.
|
package/docs/quickstart.md
CHANGED
|
@@ -13,6 +13,16 @@ Review `.llmnav/config.json` before annotating code. The default scans the repos
|
|
|
13
13
|
|
|
14
14
|
Source roots and evaluation files must remain inside the repository. The generated cache directory must remain below `.llmnav/`; parent traversal and symbolic-link control directories are rejected.
|
|
15
15
|
|
|
16
|
+
## Audit before annotating
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx llmnav audit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Start with high and medium candidates. The audit explains whether a file is a package entrypoint, public API, generated structural boundary, or high fan-in module. It does not write source or invent card contents. Declaration files and common non-production or utility shapes are suppressed so the result is a review queue, not a demand to annotate every file.
|
|
23
|
+
|
|
24
|
+
After accepting a candidate, write its durable meaning by inspecting the source, add a narrow `coverageRules` entry for that exact boundary, and add a real task-language query to `.llmnav/eval/queries.jsonl`. Use `npx llmnav audit --fail-on high` in CI only after the initial review.
|
|
25
|
+
|
|
16
26
|
## Annotate a module boundary
|
|
17
27
|
|
|
18
28
|
Choose a boundary that an agent is likely to search for by behavior rather than by current symbol name.
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.agent.provider-neutral-host
|
|
3
|
+
role=Demonstrate a provider-neutral host that binds one trusted repository root to reusable LLMNav tools and prompt partitions.
|
|
4
|
+
owns=host-side root binding|project session lifecycle|prompt partition selection
|
|
5
|
+
excludes=model provider client|authentication|model-selected repository roots
|
|
6
|
+
search=provider neutral host|tool call adapter|project session example
|
|
7
|
+
invariant=The host supplies the repository root; model-generated tool input cannot replace it.
|
|
8
|
+
rel=workflow>llmnav.agent.protocol
|
|
9
|
+
stability=contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import {
|
|
2
13
|
executeAgentOperation,
|
|
3
14
|
createProjectSession,
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -30,6 +30,8 @@ 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.
|
|
34
|
+
|
|
33
35
|
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.
|
|
34
36
|
${END}`;
|
|
35
37
|
|
package/src/audit.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.audit.coverage
|
|
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
|
|
5
|
+
excludes=automatic source annotation|semantic role generation
|
|
6
|
+
search=llmnav audit|missing module cards|coverage suggestions
|
|
7
|
+
invariant=Audit output is deterministic, repository-relative, and advisory unless an explicit fail threshold is selected.
|
|
8
|
+
rel=workflow>llmnav.project.scan
|
|
9
|
+
stability=contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { detectBoundaries } from "./boundaries.js";
|
|
14
|
+
import { scanProject } from "./project.js";
|
|
15
|
+
import { compareText, readJsonSafe, toPosix } from "./util.js";
|
|
16
|
+
|
|
17
|
+
export const AUDIT_SCHEMA_VERSION = 1;
|
|
18
|
+
export const AUDIT_PRIORITIES = Object.freeze(["high", "medium", "low"]);
|
|
19
|
+
|
|
20
|
+
const SOURCE_EXTENSIONS = Object.freeze([
|
|
21
|
+
".astro", ".c", ".cc", ".cjs", ".cpp", ".cs", ".cts", ".dart", ".go", ".h", ".hpp", ".java",
|
|
22
|
+
".js", ".jsx", ".kt", ".kts", ".mjs", ".mts", ".php", ".py", ".rb", ".rs", ".svelte", ".swift",
|
|
23
|
+
".ts", ".tsx", ".vue",
|
|
24
|
+
]);
|
|
25
|
+
const UTILITY_NAME_PATTERN = /^(?:common|helpers?|shared|utils?)$/u;
|
|
26
|
+
const NON_PRODUCTION_PATH_PATTERN = /(?:^|\/)(?:__tests__|benchmarks?|fixtures?|tests?)(?:\/|$)|\.(?:spec|test)\.[^/]+$/u;
|
|
27
|
+
const LARGE_SOURCE_BYTES = 12_000;
|
|
28
|
+
|
|
29
|
+
export async function auditProject(root) {
|
|
30
|
+
const project = await scanProject(root);
|
|
31
|
+
const fileByPath = new Map(
|
|
32
|
+
project.fileRecords.map((record) => [toPosix(record.relativePath), record]),
|
|
33
|
+
);
|
|
34
|
+
const moduleCardPaths = new Set(
|
|
35
|
+
project.records
|
|
36
|
+
.filter((record) => record.card.scope === "file" || record.card.scope === "module")
|
|
37
|
+
.map((record) => toPosix(record.relativePath)),
|
|
38
|
+
);
|
|
39
|
+
const importsByPath = new Map();
|
|
40
|
+
const importedBy = new Map([...fileByPath.keys()].map((file) => [file, new Set()]));
|
|
41
|
+
for (const [file, record] of fileByPath) {
|
|
42
|
+
const resolved = record.imports
|
|
43
|
+
.map((specifier) => resolveLocalSpecifier(file, specifier, fileByPath))
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
const unique = [...new Set(resolved)].sort(compareText);
|
|
46
|
+
importsByPath.set(file, unique);
|
|
47
|
+
for (const target of unique) importedBy.get(target)?.add(file);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const entrypoints = await collectWorkspacePackageEntrypoints(root, fileByPath);
|
|
51
|
+
const publicApiPaths = collectPublicApiPaths(entrypoints, fileByPath);
|
|
52
|
+
const candidates = [];
|
|
53
|
+
|
|
54
|
+
for (const [file, record] of [...fileByPath.entries()].sort(([left], [right]) => compareText(left, right))) {
|
|
55
|
+
if (moduleCardPaths.has(file)) continue;
|
|
56
|
+
if (/\.d\.[cm]?ts$/u.test(file)) continue;
|
|
57
|
+
const boundaries = detectBoundaries({
|
|
58
|
+
relativePath: file,
|
|
59
|
+
card: { effect: [], risk: [] },
|
|
60
|
+
source: record.source ?? "",
|
|
61
|
+
}).map((boundary) => boundary.kind);
|
|
62
|
+
const exportedDeclarations = countExportedDeclarations(record.source ?? "", file);
|
|
63
|
+
const entrypoint = entrypoints.has(file);
|
|
64
|
+
const publicApi = publicApiPaths.has(file) && !entrypoint;
|
|
65
|
+
const importers = importedBy.get(file)?.size ?? 0;
|
|
66
|
+
const reexportBarrel = isReexportBarrel(record.source ?? "", file);
|
|
67
|
+
const basename = path.posix.basename(file, path.posix.extname(file)).toLowerCase();
|
|
68
|
+
const broadUtility = UTILITY_NAME_PATTERN.test(basename) ||
|
|
69
|
+
(exportedDeclarations >= 10 && !entrypoint && !publicApi && boundaries.length === 0);
|
|
70
|
+
const nonProduction = NON_PRODUCTION_PATH_PATTERN.test(file);
|
|
71
|
+
const largeSource = record.sourceBytes >= LARGE_SOURCE_BYTES;
|
|
72
|
+
const hasSignal = entrypoint || publicApi || boundaries.length > 0 || importers > 0 || exportedDeclarations > 0 || largeSource;
|
|
73
|
+
if (!hasSignal) continue;
|
|
74
|
+
|
|
75
|
+
let score = 0;
|
|
76
|
+
const reasons = ["missing-module-card"];
|
|
77
|
+
if (entrypoint) {
|
|
78
|
+
score += 50;
|
|
79
|
+
reasons.push("package-entrypoint");
|
|
80
|
+
}
|
|
81
|
+
if (publicApi) {
|
|
82
|
+
score += 45;
|
|
83
|
+
reasons.push("public-api-reexport");
|
|
84
|
+
}
|
|
85
|
+
for (const boundary of boundaries) {
|
|
86
|
+
score += boundary === "command" ? 40 : 30;
|
|
87
|
+
reasons.push(`${boundary}-boundary`);
|
|
88
|
+
}
|
|
89
|
+
if (importers > 0) {
|
|
90
|
+
score += Math.min(30, importers * 5);
|
|
91
|
+
reasons.push(`imported-by:${importers}`);
|
|
92
|
+
}
|
|
93
|
+
if (exportedDeclarations > 0) {
|
|
94
|
+
score += 10;
|
|
95
|
+
reasons.push(`exported-declarations:${exportedDeclarations}`);
|
|
96
|
+
}
|
|
97
|
+
if (largeSource) {
|
|
98
|
+
score += 10;
|
|
99
|
+
reasons.push("large-source");
|
|
100
|
+
}
|
|
101
|
+
if (broadUtility) {
|
|
102
|
+
score -= 30;
|
|
103
|
+
reasons.push("broad-utility-penalty");
|
|
104
|
+
}
|
|
105
|
+
if (nonProduction) {
|
|
106
|
+
score -= 40;
|
|
107
|
+
reasons.push("non-production-penalty");
|
|
108
|
+
}
|
|
109
|
+
if (reexportBarrel) {
|
|
110
|
+
score -= 60;
|
|
111
|
+
reasons.push("reexport-barrel-penalty");
|
|
112
|
+
}
|
|
113
|
+
score = Math.max(0, score);
|
|
114
|
+
if (score === 0) continue;
|
|
115
|
+
const priority = score >= 60 ? "high" : score >= 30 ? "medium" : "low";
|
|
116
|
+
candidates.push({
|
|
117
|
+
path: file,
|
|
118
|
+
priority,
|
|
119
|
+
score,
|
|
120
|
+
reasons,
|
|
121
|
+
signals: {
|
|
122
|
+
packageEntrypoint: entrypoint,
|
|
123
|
+
publicApi,
|
|
124
|
+
exportedDeclarations,
|
|
125
|
+
importedBy: importers,
|
|
126
|
+
boundaries,
|
|
127
|
+
largeSource,
|
|
128
|
+
broadUtility,
|
|
129
|
+
nonProduction,
|
|
130
|
+
reexportBarrel,
|
|
131
|
+
},
|
|
132
|
+
suggestedCoverageRule: priority === "low" ? null : buildCoverageSuggestion(file),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
candidates.sort((left, right) =>
|
|
137
|
+
priorityRank(left.priority) - priorityRank(right.priority) ||
|
|
138
|
+
right.score - left.score ||
|
|
139
|
+
compareText(left.path, right.path));
|
|
140
|
+
const summary = {
|
|
141
|
+
analyzedFiles: fileByPath.size,
|
|
142
|
+
cardedFiles: moduleCardPaths.size,
|
|
143
|
+
filesWithoutModuleCards: fileByPath.size - moduleCardPaths.size,
|
|
144
|
+
candidates: candidates.length,
|
|
145
|
+
high: candidates.filter((candidate) => candidate.priority === "high").length,
|
|
146
|
+
medium: candidates.filter((candidate) => candidate.priority === "medium").length,
|
|
147
|
+
low: candidates.filter((candidate) => candidate.priority === "low").length,
|
|
148
|
+
coverageSuggestions: candidates.filter((candidate) => candidate.suggestedCoverageRule !== null).length,
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
schemaVersion: AUDIT_SCHEMA_VERSION,
|
|
152
|
+
repositoryId: project.config.repositoryId,
|
|
153
|
+
summary,
|
|
154
|
+
candidates,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function auditHasFindings(result, minimumPriority = "none") {
|
|
159
|
+
if (minimumPriority === "none") return false;
|
|
160
|
+
if (!AUDIT_PRIORITIES.includes(minimumPriority)) {
|
|
161
|
+
throw new TypeError(`Unknown audit priority: ${minimumPriority}.`);
|
|
162
|
+
}
|
|
163
|
+
const threshold = priorityRank(minimumPriority);
|
|
164
|
+
return result.candidates.some((candidate) => priorityRank(candidate.priority) <= threshold);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildCoverageSuggestion(file) {
|
|
168
|
+
const stem = path.posix.basename(file, path.posix.extname(file)).replace(/[^a-z0-9]+/giu, "-").toLowerCase();
|
|
169
|
+
return {
|
|
170
|
+
name: `${stem} module boundary`,
|
|
171
|
+
match: [file],
|
|
172
|
+
scope: "module",
|
|
173
|
+
requiredFields: ["owns", "search"],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function collectWorkspacePackageEntrypoints(root, fileByPath) {
|
|
178
|
+
const packageDirectories = new Set([""]);
|
|
179
|
+
for (const file of fileByPath.keys()) {
|
|
180
|
+
let directory = path.posix.dirname(file);
|
|
181
|
+
while (directory !== "." && directory !== "") {
|
|
182
|
+
packageDirectories.add(directory);
|
|
183
|
+
const parent = path.posix.dirname(directory);
|
|
184
|
+
if (parent === directory || parent === ".") break;
|
|
185
|
+
directory = parent;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const entrypoints = new Set();
|
|
189
|
+
for (const directory of [...packageDirectories].sort(compareText)) {
|
|
190
|
+
const packageJson = await readJsonSafe(path.join(root, directory, "package.json"), null);
|
|
191
|
+
if (!packageJson || typeof packageJson !== "object") continue;
|
|
192
|
+
for (const entrypoint of collectPackageEntrypoints(packageJson, fileByPath, directory)) entrypoints.add(entrypoint);
|
|
193
|
+
}
|
|
194
|
+
return entrypoints;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function collectPackageEntrypoints(packageJson, fileByPath, packageDirectory = "") {
|
|
198
|
+
const raw = [packageJson?.main, packageJson?.module, ...collectStringLeaves(packageJson?.bin), ...collectStringLeaves(packageJson?.exports)];
|
|
199
|
+
const entrypoints = new Set();
|
|
200
|
+
for (const value of raw) {
|
|
201
|
+
if (typeof value !== "string" || /\.d\.[cm]?ts$/u.test(value)) continue;
|
|
202
|
+
const normalized = normalizePackagePath(value);
|
|
203
|
+
if (!normalized) continue;
|
|
204
|
+
const resolved = resolveProjectPath(path.posix.join(packageDirectory, normalized), fileByPath);
|
|
205
|
+
if (resolved) entrypoints.add(resolved);
|
|
206
|
+
}
|
|
207
|
+
return entrypoints;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function collectStringLeaves(value, key = "") {
|
|
211
|
+
if (key === "types") return [];
|
|
212
|
+
if (typeof value === "string") return [value];
|
|
213
|
+
if (Array.isArray(value)) return value.flatMap((item) => collectStringLeaves(item));
|
|
214
|
+
if (!value || typeof value !== "object") return [];
|
|
215
|
+
return Object.entries(value).flatMap(([childKey, child]) => collectStringLeaves(child, childKey));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizePackagePath(value) {
|
|
219
|
+
const normalized = toPosix(value).replace(/^\.\//u, "");
|
|
220
|
+
if (!normalized || normalized.startsWith("/") || normalized.split("/").includes("..")) return null;
|
|
221
|
+
return path.posix.normalize(normalized);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function collectPublicApiPaths(entrypoints, fileByPath) {
|
|
225
|
+
const publicPaths = new Set(entrypoints);
|
|
226
|
+
const queue = [...entrypoints].sort(compareText);
|
|
227
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
228
|
+
const file = queue[index];
|
|
229
|
+
const source = fileByPath.get(file)?.source ?? "";
|
|
230
|
+
for (const specifier of extractReexportSpecifiers(source)) {
|
|
231
|
+
const target = resolveLocalSpecifier(file, specifier, fileByPath);
|
|
232
|
+
if (!target || publicPaths.has(target)) continue;
|
|
233
|
+
publicPaths.add(target);
|
|
234
|
+
queue.push(target);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return publicPaths;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function extractReexportSpecifiers(source) {
|
|
241
|
+
const values = [];
|
|
242
|
+
const pattern = /^\s*export\s+(?:\*(?:\s+as\s+[A-Za-z_$][\w$]*)?|\{[^}]*\})\s+from\s+["']([^"']+)["']\s*;?/gmu;
|
|
243
|
+
for (const match of source.matchAll(pattern)) values.push(match[1]);
|
|
244
|
+
return [...new Set(values)].sort(compareText);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isReexportBarrel(source, file) {
|
|
248
|
+
if (!/\.[cm]?[jt]sx?$/u.test(file)) return false;
|
|
249
|
+
const reexports = extractReexportSpecifiers(source);
|
|
250
|
+
if (reexports.length === 0) return false;
|
|
251
|
+
const withoutReexports = source
|
|
252
|
+
.replace(/^\s*export\s+(?:\*(?:\s+as\s+[A-Za-z_$][\w$]*)?|\{[^}]*\})\s+from\s+["'][^"']+["']\s*;?/gmu, "")
|
|
253
|
+
.replace(/^\s*\/\/.*$/gmu, "")
|
|
254
|
+
.replace(/\/\*[\s\S]*?\*\//gu, "")
|
|
255
|
+
.trim();
|
|
256
|
+
return withoutReexports === "";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolveLocalSpecifier(fromFile, specifier, fileByPath) {
|
|
260
|
+
if (/\.rs$/u.test(fromFile)) return resolveRustSpecifier(fromFile, specifier, fileByPath);
|
|
261
|
+
if (!specifier.startsWith(".")) return null;
|
|
262
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), toPosix(specifier)));
|
|
263
|
+
if (base.startsWith("../") || base === "..") return null;
|
|
264
|
+
return resolveProjectPath(base, fileByPath);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveRustSpecifier(fromFile, specifier, fileByPath) {
|
|
268
|
+
const normalized = specifier
|
|
269
|
+
.replace(/\s+as\s+.+$/u, "")
|
|
270
|
+
.replace(/::\{[\s\S]*$/u, "")
|
|
271
|
+
.replace(/::\*$/u, "")
|
|
272
|
+
.trim();
|
|
273
|
+
if (!normalized || /[{}(),]/u.test(normalized)) return null;
|
|
274
|
+
const parts = normalized.split("::").filter(Boolean);
|
|
275
|
+
const sourceIndex = fromFile.split("/").lastIndexOf("src");
|
|
276
|
+
const crateSource = sourceIndex >= 0 ? fromFile.split("/").slice(0, sourceIndex + 1).join("/") : path.posix.dirname(fromFile);
|
|
277
|
+
let base;
|
|
278
|
+
if (parts[0] === "crate") {
|
|
279
|
+
parts.shift();
|
|
280
|
+
base = crateSource;
|
|
281
|
+
} else if (parts[0] === "self") {
|
|
282
|
+
parts.shift();
|
|
283
|
+
base = rustModuleDirectory(fromFile);
|
|
284
|
+
} else if (parts[0] === "super") {
|
|
285
|
+
while (parts[0] === "super") {
|
|
286
|
+
parts.shift();
|
|
287
|
+
base = path.posix.dirname(base ?? rustModuleDirectory(fromFile));
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
base = rustModuleDirectory(fromFile);
|
|
291
|
+
}
|
|
292
|
+
if (parts.length === 0) return null;
|
|
293
|
+
const candidate = path.posix.join(base, ...parts);
|
|
294
|
+
return [
|
|
295
|
+
`${candidate}.rs`,
|
|
296
|
+
`${candidate}/mod.rs`,
|
|
297
|
+
].find((value) => fileByPath.has(value)) ?? null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function rustModuleDirectory(fromFile) {
|
|
301
|
+
const basename = path.posix.basename(fromFile);
|
|
302
|
+
if (basename === "lib.rs" || basename === "main.rs" || basename === "mod.rs") return path.posix.dirname(fromFile);
|
|
303
|
+
return path.posix.join(path.posix.dirname(fromFile), path.posix.basename(fromFile, ".rs"));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function resolveProjectPath(candidate, fileByPath) {
|
|
307
|
+
const values = [candidate];
|
|
308
|
+
const extension = path.posix.extname(candidate);
|
|
309
|
+
if (extension) {
|
|
310
|
+
const stem = candidate.slice(0, -extension.length);
|
|
311
|
+
for (const nextExtension of SOURCE_EXTENSIONS) values.push(`${stem}${nextExtension}`);
|
|
312
|
+
} else {
|
|
313
|
+
for (const nextExtension of SOURCE_EXTENSIONS) {
|
|
314
|
+
values.push(`${candidate}${nextExtension}`);
|
|
315
|
+
values.push(`${candidate}/index${nextExtension}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return values.find((value) => fileByPath.has(value)) ?? null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function countExportedDeclarations(source, file) {
|
|
322
|
+
const extension = path.posix.extname(file).toLowerCase();
|
|
323
|
+
if ([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".svelte", ".astro", ".vue"].includes(extension)) {
|
|
324
|
+
const declarations = source.match(/^\s*export\s+(?:default\s+)?(?:declare\s+)?(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\b/gmu) ?? [];
|
|
325
|
+
const localLists = source.match(/^\s*export\s*\{[^}]*\}\s*;?$/gmu) ?? [];
|
|
326
|
+
const reexports = extractReexportSpecifiers(source).length;
|
|
327
|
+
return declarations.length + localLists.length + reexports;
|
|
328
|
+
}
|
|
329
|
+
if (extension === ".go") {
|
|
330
|
+
return [...source.matchAll(/^\s*(?:func\s+(?:\([^)]*\)\s*)?|type\s+|var\s+|const\s+)([A-Z][A-Za-z0-9_]*)\b/gmu)].length;
|
|
331
|
+
}
|
|
332
|
+
if (extension === ".rs") return [...source.matchAll(/^\s*pub(?:\([^)]*\))?\s+(?:async\s+)?(?:fn|struct|enum|trait|type|const|static|mod)\b/gmu)].length;
|
|
333
|
+
if (extension === ".py") return [...source.matchAll(/^\s*(?:async\s+def|def|class)\s+([A-Za-z][A-Za-z0-9_]*)\b/gmu)].filter((match) => !match[1].startsWith("_")).length;
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function priorityRank(priority) {
|
|
338
|
+
if (priority === "high") return 0;
|
|
339
|
+
if (priority === "medium") return 1;
|
|
340
|
+
if (priority === "low") return 2;
|
|
341
|
+
return 3;
|
|
342
|
+
}
|
package/src/boundaries.js
CHANGED
|
@@ -11,13 +11,14 @@ stability=architecture
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { compareText, toPosix } from "./util.js";
|
|
13
13
|
|
|
14
|
-
export const BOUNDARY_KINDS = Object.freeze(["command", "event", "migration", "route", "schema"]);
|
|
14
|
+
export const BOUNDARY_KINDS = Object.freeze(["command", "event", "migration", "route", "runtime", "schema"]);
|
|
15
15
|
|
|
16
16
|
export function detectBoundaries(record) {
|
|
17
17
|
const relativePath = toPosix(record.relativePath).toLowerCase();
|
|
18
18
|
const basename = path.posix.basename(relativePath);
|
|
19
19
|
const effects = record.card.effect ?? [];
|
|
20
20
|
const risks = record.card.risk ?? [];
|
|
21
|
+
const source = record.source ?? "";
|
|
21
22
|
const boundaries = new Map();
|
|
22
23
|
const add = (kind, confidence, evidence) => {
|
|
23
24
|
const current = boundaries.get(kind);
|
|
@@ -45,6 +46,14 @@ export function detectBoundaries(record) {
|
|
|
45
46
|
if (/(?:^|\/)(?:commands?|cli|bin)(?:\/|$)/u.test(relativePath) || /(?:command|cmd)\.[^.]+$/u.test(basename)) {
|
|
46
47
|
add("command", "high", "path");
|
|
47
48
|
}
|
|
49
|
+
if (/\.rs$/u.test(relativePath) && /#\[tauri::command\]|tauri::generate_handler!/u.test(source)) {
|
|
50
|
+
add("command", "high", "tauri-command");
|
|
51
|
+
}
|
|
52
|
+
if (/\.rs$/u.test(relativePath) &&
|
|
53
|
+
/#\[cfg\((?:windows|unix|target_(?:os|family))/u.test(source) &&
|
|
54
|
+
/\b(?:Drop|shutdown|terminate|kill|process_group|job_object)\b/iu.test(source)) {
|
|
55
|
+
add("runtime", "medium", "platform-lifecycle");
|
|
56
|
+
}
|
|
48
57
|
|
|
49
58
|
return [...boundaries.values()].sort((left, right) => compareText(left.kind, right.kind));
|
|
50
59
|
}
|
package/src/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ rel=workflow>llmnav.project.initialize
|
|
|
8
8
|
rel=workflow>llmnav.rules.validate
|
|
9
9
|
rel=workflow>llmnav.search.query
|
|
10
10
|
rel=workflow>llmnav.eval.measure
|
|
11
|
+
rel=workflow>llmnav.audit.coverage
|
|
11
12
|
stability=architecture
|
|
12
13
|
*/
|
|
13
14
|
|
|
@@ -30,15 +31,16 @@ import {
|
|
|
30
31
|
SPEC_VERSION,
|
|
31
32
|
STABILITIES,
|
|
32
33
|
} from "./spec.js";
|
|
33
|
-
import { parseInteger } from "./util.js";
|
|
34
|
+
import { assertNoSymlinkTraversal, atomicWrite, parseInteger, relativePosix } from "./util.js";
|
|
34
35
|
import { diagnosticsToSarif } from "./sarif.js";
|
|
35
36
|
import { loadGraphInputs } from "./graph-input.js";
|
|
36
37
|
import { renderGraphNode } from "./graph.js";
|
|
37
38
|
import { getAgentToolDefinitions } from "./agent-protocol.js";
|
|
38
39
|
import { loadPromptPrefixBundle } from "./prompt-bundle.js";
|
|
39
40
|
import { diagnosticsToEditor, getEditorIntegration } from "./editor.js";
|
|
41
|
+
import { AUDIT_PRIORITIES, auditHasFindings, auditProject } from "./audit.js";
|
|
40
42
|
|
|
41
|
-
const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file"]);
|
|
43
|
+
const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file", "--fail-on", "--output"]);
|
|
42
44
|
|
|
43
45
|
const COMMAND_OPTIONS = Object.freeze({
|
|
44
46
|
init: new Set(["--agents", "--package-scripts", "--force", "--root", "--json"]),
|
|
@@ -51,6 +53,7 @@ const COMMAND_OPTIONS = Object.freeze({
|
|
|
51
53
|
context: new Set(["--depth", "--budget", "--max-edges", "--root", "--json"]),
|
|
52
54
|
eval: new Set(["--file", "--top", "--root", "--json"]),
|
|
53
55
|
doctor: new Set(["--root", "--json"]),
|
|
56
|
+
audit: new Set(["--root", "--json", "--summary", "--fail-on", "--output"]),
|
|
54
57
|
spec: new Set(["--root", "--json"]),
|
|
55
58
|
tools: new Set(["--json"]),
|
|
56
59
|
bundle: new Set(["--root", "--json"]),
|
|
@@ -97,6 +100,8 @@ export async function runCli(argv) {
|
|
|
97
100
|
return runEval(root, args, json);
|
|
98
101
|
case "doctor":
|
|
99
102
|
return runDoctor(root, json);
|
|
103
|
+
case "audit":
|
|
104
|
+
return runAudit(root, args, json);
|
|
100
105
|
case "spec":
|
|
101
106
|
return runSpec(json);
|
|
102
107
|
case "bundle":
|
|
@@ -290,6 +295,47 @@ async function runDoctor(root, json) {
|
|
|
290
295
|
return result.ok ? 0 : 1;
|
|
291
296
|
}
|
|
292
297
|
|
|
298
|
+
async function runAudit(root, args, json) {
|
|
299
|
+
const failOn = getOption(args, "--fail-on") ?? "none";
|
|
300
|
+
if (failOn !== "none" && !AUDIT_PRIORITIES.includes(failOn)) {
|
|
301
|
+
throw usageError(`audit --fail-on must be one of none, ${AUDIT_PRIORITIES.join(", ")}.`);
|
|
302
|
+
}
|
|
303
|
+
const outputOption = getOption(args, "--output");
|
|
304
|
+
const outputPath = outputOption ? path.resolve(root, outputOption) : null;
|
|
305
|
+
if (outputPath) {
|
|
306
|
+
try {
|
|
307
|
+
await assertNoSymlinkTraversal(root, outputPath, "audit output file");
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const result = await auditProject(root);
|
|
313
|
+
const report = { ...result, failOn };
|
|
314
|
+
const selectedReport = hasFlag(args, "--summary")
|
|
315
|
+
? { schemaVersion: result.schemaVersion, repositoryId: result.repositoryId, summary: result.summary, failOn }
|
|
316
|
+
: report;
|
|
317
|
+
if (outputPath) {
|
|
318
|
+
await atomicWrite(outputPath, `${JSON.stringify(selectedReport, null, 2)}\n`);
|
|
319
|
+
}
|
|
320
|
+
if (json) {
|
|
321
|
+
console.log(JSON.stringify(outputPath
|
|
322
|
+
? { schemaVersion: result.schemaVersion, repositoryId: result.repositoryId, summary: result.summary, failOn, output: relativePosix(root, outputPath) }
|
|
323
|
+
: selectedReport, null, 2));
|
|
324
|
+
} else {
|
|
325
|
+
const { summary } = result;
|
|
326
|
+
console.log(
|
|
327
|
+
`audit files=${summary.analyzedFiles} carded=${summary.cardedFiles} candidates=${summary.candidates} ` +
|
|
328
|
+
`high=${summary.high} medium=${summary.medium} low=${summary.low}`,
|
|
329
|
+
);
|
|
330
|
+
for (const candidate of result.candidates.filter((item) => item.priority !== "low")) {
|
|
331
|
+
console.log(`${candidate.priority} ${candidate.path} score=${candidate.score} ${candidate.reasons.join(",")}`);
|
|
332
|
+
}
|
|
333
|
+
if (summary.low > 0) console.log(`${summary.low} low-priority candidate(s) are available in --json output.`);
|
|
334
|
+
if (outputPath) console.log(`wrote ${relativePosix(root, outputPath)}`);
|
|
335
|
+
}
|
|
336
|
+
return auditHasFindings(result, failOn) ? 1 : 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
293
339
|
function runSpec(json) {
|
|
294
340
|
const spec = {
|
|
295
341
|
specVersion: SPEC_VERSION,
|
|
@@ -446,6 +492,7 @@ Usage
|
|
|
446
492
|
llmnav context <semantic-id> [--depth 1] [--budget 2500] [--max-edges 24]
|
|
447
493
|
llmnav eval [--file path] [--top 5]
|
|
448
494
|
llmnav doctor
|
|
495
|
+
llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low]
|
|
449
496
|
llmnav spec
|
|
450
497
|
llmnav tools [--json]
|
|
451
498
|
llmnav bundle [--json]
|
package/src/config.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.config.load
|
|
3
|
+
role=Load, merge, and validate repository configuration before project operations observe it.
|
|
4
|
+
owns=configuration loading|default merging|configuration schema validation
|
|
5
|
+
excludes=source-card validation|schema file generation
|
|
6
|
+
search=load llmnav config|configuration validation|coverage rules
|
|
7
|
+
invariant=Unknown configuration keys and escaping project paths are rejected before use.
|
|
8
|
+
rel=workflow>llmnav.rules.validate
|
|
9
|
+
stability=contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import path from "node:path";
|
|
2
13
|
import {
|
|
3
14
|
ALLOWED_KEYS,
|
package/src/declaration.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.declaration.extract
|
|
3
|
+
role=Attach symbol cards to language declarations and derive structural metadata without executing source.
|
|
4
|
+
owns=declaration attachment|signature extraction|import extraction
|
|
5
|
+
excludes=semantic card parsing|type-system resolution
|
|
6
|
+
search=declaration extraction|symbol attachment|body hash
|
|
7
|
+
invariant=String literals and comments do not create false declarations or imports.
|
|
8
|
+
rel=workflow>llmnav.project.scan
|
|
9
|
+
stability=architecture
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import path from "node:path";
|
|
2
13
|
import { lineAtOffset, normalizeNewlines, sha256 } from "./util.js";
|
|
3
14
|
|
package/src/doctor.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.diagnostics.doctor
|
|
3
|
+
role=Diagnose project configuration, generated cache integrity, and recoverable generation state.
|
|
4
|
+
owns=project health checks|cache integrity checks|release readiness diagnostics
|
|
5
|
+
excludes=source formatting|package publication
|
|
6
|
+
search=llmnav doctor|project health|cache integrity
|
|
7
|
+
invariant=Only interrupted generation recovery may mutate state; all other health checks are read-only.
|
|
8
|
+
rel=workflow>llmnav.index.generate
|
|
9
|
+
stability=contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import path from "node:path";
|
|
2
13
|
import { loadConfig } from "./config.js";
|
|
3
14
|
import { generateProject } from "./generator.js";
|
package/src/files.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.source.discover
|
|
3
|
+
role=Locate ancestor LLMNav roots and collect project-contained source files without following symlinks.
|
|
4
|
+
owns=project root discovery|source traversal|include and exclude filtering
|
|
5
|
+
excludes=source parsing|generated artifact selection
|
|
6
|
+
search=source discovery|project root|file traversal
|
|
7
|
+
invariant=Selected source paths remain inside the repository and symbolic links are never traversed.
|
|
8
|
+
rel=workflow>llmnav.project.scan
|
|
9
|
+
stability=contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import { lstat, readdir, stat } from "node:fs/promises";
|
|
2
13
|
import path from "node:path";
|
|
3
14
|
import { compareText, matchesAnyGlob, relativePosix, toPosix } from "./util.js";
|
package/src/formatter.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.source.format
|
|
3
|
+
role=Canonicalize LLMNav source blocks across selected files with atomic writes and check-only reporting.
|
|
4
|
+
owns=source card formatting|format check mode|atomic source rewrite
|
|
5
|
+
excludes=semantic validation|generated cache formatting
|
|
6
|
+
search=llmnav format|canonical comments|source rewrite
|
|
7
|
+
invariant=Check mode reports non-canonical files without writing source files.
|
|
8
|
+
rel=workflow>llmnav.syntax.parse
|
|
9
|
+
stability=contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import { readFile } from "node:fs/promises";
|
|
2
13
|
import { loadConfig } from "./config.js";
|
|
3
14
|
import { collectSourceFiles } from "./files.js";
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type LlmnavScope = "file" | "module" | "symbol";
|
|
2
2
|
export type LlmnavStability = "architecture" | "contract" | "implementation";
|
|
3
3
|
export type DiagnosticSeverity = "error" | "warning" | "info";
|
|
4
|
+
export type AuditPriority = "high" | "medium" | "low";
|
|
4
5
|
|
|
5
6
|
export interface LlmnavCard {
|
|
6
7
|
scope: LlmnavScope;
|
|
@@ -71,6 +72,46 @@ export interface Diagnostic {
|
|
|
71
72
|
column: number;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
export interface AuditCandidate {
|
|
76
|
+
path: string;
|
|
77
|
+
priority: AuditPriority;
|
|
78
|
+
score: number;
|
|
79
|
+
reasons: string[];
|
|
80
|
+
signals: {
|
|
81
|
+
packageEntrypoint: boolean;
|
|
82
|
+
publicApi: boolean;
|
|
83
|
+
exportedDeclarations: number;
|
|
84
|
+
importedBy: number;
|
|
85
|
+
boundaries: DetectedBoundary["kind"][];
|
|
86
|
+
largeSource: boolean;
|
|
87
|
+
broadUtility: boolean;
|
|
88
|
+
nonProduction: boolean;
|
|
89
|
+
reexportBarrel: boolean;
|
|
90
|
+
};
|
|
91
|
+
suggestedCoverageRule: null | {
|
|
92
|
+
name: string;
|
|
93
|
+
match: [string];
|
|
94
|
+
scope: "module";
|
|
95
|
+
requiredFields: ["owns", "search"];
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface AuditResult {
|
|
100
|
+
schemaVersion: 1;
|
|
101
|
+
repositoryId: string;
|
|
102
|
+
summary: {
|
|
103
|
+
analyzedFiles: number;
|
|
104
|
+
cardedFiles: number;
|
|
105
|
+
filesWithoutModuleCards: number;
|
|
106
|
+
candidates: number;
|
|
107
|
+
high: number;
|
|
108
|
+
medium: number;
|
|
109
|
+
low: number;
|
|
110
|
+
coverageSuggestions: number;
|
|
111
|
+
};
|
|
112
|
+
candidates: AuditCandidate[];
|
|
113
|
+
}
|
|
114
|
+
|
|
74
115
|
export interface IndexedLocation {
|
|
75
116
|
path: string;
|
|
76
117
|
startLine: number;
|
|
@@ -541,6 +582,8 @@ export interface EvaluationResult {
|
|
|
541
582
|
}
|
|
542
583
|
|
|
543
584
|
export const AGENT_PROTOCOL: string;
|
|
585
|
+
export const AUDIT_PRIORITIES: readonly AuditPriority[];
|
|
586
|
+
export const AUDIT_SCHEMA_VERSION: 1;
|
|
544
587
|
export const AGENT_OPERATION_SCHEMA_VERSION: 1;
|
|
545
588
|
export const AGENT_TOOL_SCHEMA_VERSION: 1;
|
|
546
589
|
export const BOUNDARY_KINDS: readonly DetectedBoundary["kind"][];
|
|
@@ -615,6 +658,8 @@ export function describeAffectedBoundaries(changedCards: ChangedCardRecord[], co
|
|
|
615
658
|
export function describeAffectedCatalogs(changedFiles: string[], cacheDirectory: string, config: LlmnavConfig, previousIndex: LlmnavIndex | null, currentIndex: LlmnavIndex): AffectedCatalogRecord[];
|
|
616
659
|
export function loadConfig(root: string): Promise<{ config: LlmnavConfig; configPath: string }>;
|
|
617
660
|
export function validateConfig(config: LlmnavConfig, configPath?: string): void;
|
|
661
|
+
export function auditProject(root: string): Promise<AuditResult>;
|
|
662
|
+
export function auditHasFindings(result: AuditResult, minimumPriority?: AuditPriority | "none"): boolean;
|
|
618
663
|
export function findAttachedDeclaration(source: string, block: LlmnavBlock, filePath: string): Declaration | null;
|
|
619
664
|
export function extractImports(source: string, filePath: string): string[];
|
|
620
665
|
export function doctorProject(root: string): Promise<{ ok: boolean; checks: Array<{ name: string; ok: boolean; message: string }> }>;
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,7 @@ 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
18
|
export { BOUNDARY_KINDS, detectBoundaries } from "./boundaries.js";
|
|
18
19
|
export {
|
|
19
20
|
diagnosticsToEditor,
|
package/src/initializer.js
CHANGED
|
@@ -121,6 +121,7 @@ async function addPackageScripts(root) {
|
|
|
121
121
|
const parsed = JSON.parse(text);
|
|
122
122
|
parsed.scripts ??= {};
|
|
123
123
|
const desired = {
|
|
124
|
+
"llmnav:audit": "llmnav audit --fail-on high",
|
|
124
125
|
"llmnav:check": "llmnav check",
|
|
125
126
|
"llmnav:format": "llmnav format",
|
|
126
127
|
"llmnav:generate": "llmnav generate",
|
package/src/project.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.project.scan
|
|
3
|
+
role=Scan project sources into semantic card and file records with declarations, imports, and content hashes.
|
|
4
|
+
owns=project scan|card record assembly|source metrics
|
|
5
|
+
excludes=semantic validation|cache generation
|
|
6
|
+
search=project scan|card records|source metadata
|
|
7
|
+
invariant=Generated paths and structural metadata remain derived data and are never written into source cards.
|
|
8
|
+
rel=workflow>llmnav.declaration.extract
|
|
9
|
+
rel=workflow>llmnav.syntax.parse
|
|
10
|
+
stability=architecture
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
import { readFile } from "node:fs/promises";
|
|
2
14
|
import { loadConfig } from "./config.js";
|
|
3
15
|
import { findAttachedDeclaration, extractImports } from "./declaration.js";
|
package/src/registry.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.registry.ids
|
|
3
|
+
role=Validate and evolve semantic ID lifecycle records across active, redirect, replaced, and retired states.
|
|
4
|
+
owns=semantic ID registry|redirect resolution|replacement lifecycle
|
|
5
|
+
excludes=source card parsing|search ranking
|
|
6
|
+
search=semantic id registry|redirected id|retired id
|
|
7
|
+
invariant=Registry resolution never silently chooses among multiple replacement targets.
|
|
8
|
+
rel=workflow>llmnav.index.generate
|
|
9
|
+
stability=contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import path from "node:path";
|
|
2
13
|
import { ID_PATTERN } from "./spec.js";
|
|
3
14
|
import { atomicWrite, parseJsonLines, readText } from "./util.js";
|
package/src/spec.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/* llmnav/1 module
|
|
2
|
+
id=llmnav.spec.contract
|
|
3
|
+
role=Define the public LLMNav/1 vocabulary, limits, relation kinds, and default configuration.
|
|
4
|
+
owns=protocol vocabulary|controlled field values|default configuration
|
|
5
|
+
excludes=configuration overrides|source parsing
|
|
6
|
+
search=llmnav specification|protocol constants|default config
|
|
7
|
+
invariant=Public field, effect, risk, and relation vocabularies remain deterministic and versioned.
|
|
8
|
+
rel=workflow>llmnav.config.load
|
|
9
|
+
rel=workflow>llmnav.rules.validate
|
|
10
|
+
stability=contract
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const PACKAGE_VERSION = "0.6.1";
|
|
2
14
|
export const SPEC_VERSION = "1";
|
|
3
15
|
|
|
4
16
|
export const SCOPES = Object.freeze(["file", "module", "symbol"]);
|
|
@@ -151,6 +163,8 @@ export const DEFAULT_EXCLUDED_DIRECTORIES = Object.freeze([
|
|
|
151
163
|
".astro",
|
|
152
164
|
".turbo",
|
|
153
165
|
".cache",
|
|
166
|
+
".bun-cache",
|
|
167
|
+
".pnpm-store",
|
|
154
168
|
"target",
|
|
155
169
|
"bin",
|
|
156
170
|
"obj",
|