llmnav 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/docs/api.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +3 -3
- package/docs/configuration.md +1 -1
- package/docs/publishing.md +2 -2
- package/docs/quickstart.md +1 -1
- package/package.json +1 -1
- package/src/audit.js +65 -5
- package/src/boundaries.js +24 -3
- package/src/cli.js +23 -5
- package/src/index.d.ts +1 -1
- package/src/spec.js +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,28 @@ The npm package follows Semantic Versioning. The `llmnav/N` source protocol is v
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.2] — 2026-08-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* Added generated boundary signals for persistent dotted JSON filename protocols, versioned schema literals, and TypeScript Tauri invoke adapters.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* Stopped classifying high-fan-in domain contract hubs as broad utilities solely because they export many declarations.
|
|
18
|
+
* Made tag-driven publication create an idempotent GitHub Release after npm integrity verification or publication succeeds.
|
|
19
|
+
|
|
20
|
+
## [0.6.1] — 2026-08-10
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
* Added compact `audit --summary` output and repository-contained `audit --output <path>` reports.
|
|
25
|
+
* Added nested workspace package entrypoint discovery and Rust/Tauri command, module, and platform-lifecycle signals.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
* Excluded Bun and pnpm package-manager caches from default source discovery.
|
|
30
|
+
|
|
9
31
|
## [0.6.0] — 2026-08-10
|
|
10
32
|
|
|
11
33
|
### Added
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ LLMNav is not a documentation generator, an embedding database, or a reason to a
|
|
|
18
18
|
* Machine-readable changed-card, affected-boundary, and affected-catalog output
|
|
19
19
|
* Exported API and effective configuration contract fingerprints
|
|
20
20
|
* TypeScript and Go declaration enrichment with declaration-level body hashes
|
|
21
|
-
* Generated route, event, schema, migration, and command boundaries
|
|
21
|
+
* Generated artifact, route, event, schema, migration, runtime, and command boundaries
|
|
22
22
|
* SARIF 2.1.0 diagnostic output
|
|
23
23
|
* Optional deterministic card-range search shards for very large repositories
|
|
24
24
|
* Strict repository-local imports for generated definition and reference indexes
|
|
@@ -169,7 +169,7 @@ Line-comment cards require an explicit terminator and work with `//`, `#`, and `
|
|
|
169
169
|
| Command | Purpose |
|
|
170
170
|
| --- | --- |
|
|
171
171
|
| `llmnav init` | Create configuration, registry, schemas, agent instructions, and the initial cache |
|
|
172
|
-
| `llmnav audit` | Rank unannotated architectural boundary candidates
|
|
172
|
+
| `llmnav audit` | Rank unannotated architectural boundary candidates, with compact or file-backed output |
|
|
173
173
|
| `llmnav check` | Validate cards, relations, coverage rules, and registry state |
|
|
174
174
|
| `llmnav format` | Rewrite safe cards into canonical order and spacing |
|
|
175
175
|
| `llmnav generate` | Incrementally compile and transactionally commit generated artifacts |
|
|
@@ -250,7 +250,7 @@ Do not annotate trivial getters, generated files, obvious wrappers, every test f
|
|
|
250
250
|
|
|
251
251
|
## Current implementation boundary
|
|
252
252
|
|
|
253
|
-
Version 0.6 adds a deterministic coverage audit that prioritizes
|
|
253
|
+
Version 0.6 adds a deterministic coverage audit that prioritizes root and nested-package entrypoints, structural boundaries, persistent artifact filename protocols, versioned schema literals, Rust/Tauri runtime signals, TypeScript Tauri invoke adapters, and high fan-in modules. It suppresses declaration files, dependency caches, test support code, simple barrels, and low-fan-in broad utilities. It suggests narrow coverage rules but never writes cards or invents semantic roles. Use `llmnav audit --summary --json` for a compact automation result or `llmnav audit --json --output .llmnav/audit.json` to keep the full candidate report out of captured stdout.
|
|
254
254
|
|
|
255
255
|
LLMNav does not discover sibling repositories automatically and does not ship an MCP server, embedding database, hosted service, SCIP generator, or complete language-aware call graph. External tools may export the documented compact graph-input schema. Generated structure never writes derived edges into source cards.
|
|
256
256
|
|
package/docs/api.md
CHANGED
|
@@ -60,7 +60,7 @@ console.log(stats.parsedFiles, stats.reusedFiles);
|
|
|
60
60
|
|
|
61
61
|
Stat hints are an optimization, not deterministic output. The returned `fileState` contains only repository-relative generated data.
|
|
62
62
|
|
|
63
|
-
Attached symbol declarations expose generated `language`, `exported`, `visibility`, optional Go `receiver`, `endOffset`, and declaration `bodyHash` fields. Indexed cards also expose sorted route, event, schema, migration, and command `boundaries` with confidence and evidence.
|
|
63
|
+
Attached symbol declarations expose generated `language`, `exported`, `visibility`, optional Go `receiver`, `endOffset`, and declaration `bodyHash` fields. Indexed cards also expose sorted artifact, route, event, schema, migration, runtime, and command `boundaries` with confidence and evidence.
|
|
64
64
|
|
|
65
65
|
## Audit annotation coverage
|
|
66
66
|
|
package/docs/architecture.md
CHANGED
|
@@ -64,13 +64,13 @@ Each card carries three independent hashes.
|
|
|
64
64
|
|
|
65
65
|
Symbol attachment records the detected language, public/exported status, visibility, Go receiver when present, declaration span, and a declaration-level body hash. TypeScript and Go use dedicated deterministic declaration patterns; JavaScript, Rust, Python, and generic C-like declarations retain the compatible fallback patterns.
|
|
66
66
|
|
|
67
|
-
Generated cards may contain a sorted `boundaries` array. LLMNav detects `route`, `event`, `schema`, `migration`, and `command` boundaries from repository-relative paths
|
|
67
|
+
Generated cards may contain a sorted `boundaries` array. LLMNav detects `artifact`, `route`, `event`, `schema`, `migration`, `runtime`, and `command` boundaries from repository-relative paths, controlled semantic effects or risks, and narrow source evidence. Source evidence includes persistent dotted JSON filename builders, versioned schema literals, Tauri commands and invoke adapters, and platform lifecycle code. Each record includes `confidence` and explicit evidence. These hints are generated navigation data and are never copied into source comments.
|
|
68
68
|
|
|
69
69
|
Generation compares the previous and current primary indexes to emit `affectedBoundaries`. The report preserves the card change and hash dimensions while adding module IDs, structural boundary records, outbound relation targets, and reverse semantic dependents. It is returned through the API and `generate --json`; it is not stored in source cards.
|
|
70
70
|
|
|
71
71
|
## Annotation coverage audit
|
|
72
72
|
|
|
73
|
-
The read-only audit combines package entrypoints, public re-export reachability, generated structural boundaries, import fan-in, exported declaration counts, and source size. It lowers the priority of
|
|
73
|
+
The read-only audit combines package entrypoints, public re-export reachability, generated structural boundaries, import fan-in, exported declaration counts, and source size. It lowers the priority of named utilities and low-fan-in export hubs, declaration files, non-production support paths, and pure re-export barrels. High-fan-in domain contract hubs do not receive the broad-utility penalty merely because they export many declarations. Every candidate retains its score inputs and deterministic repository-relative path.
|
|
74
74
|
|
|
75
75
|
This analyzer deliberately stops before semantic generation. Structure can identify a file worth inspecting, but it cannot safely invent a durable ID, role, ownership statement, invariant, risk, or relation. Accepted boundaries become explicit source cards, path-specific coverage rules, and retrieval regression queries through normal review.
|
|
76
76
|
|
package/docs/cli.md
CHANGED
|
@@ -32,14 +32,14 @@ Creates project configuration, schema, registry, lexicon, evaluation file, agent
|
|
|
32
32
|
## `llmnav audit`
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
|
-
llmnav audit [--fail-on none|high|medium|low] [--json]
|
|
35
|
+
llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low] [--json]
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Reads the selected source set and ranks files that lack file or module cards. Signals include package entrypoints, public re-exports, generated command/route/schema/migration/event boundaries, import fan-in, exported declarations, and source size. Declaration files are omitted; test support code, broad utilities, and pure re-export barrels
|
|
38
|
+
Reads the selected source set and ranks files that lack file or module cards. Signals include root and nested-package entrypoints, public re-exports, generated command/route/schema/migration/event boundaries, Rust/Tauri runtime boundaries, import fan-in, exported declarations, and source size. Declaration files are omitted; dependency caches, test support code, broad utilities, and pure re-export barrels are excluded or penalized. Candidates whose penalties reduce their score to zero are omitted.
|
|
39
39
|
|
|
40
40
|
The command never modifies source, configuration, registries, or generated caches. Its output is advisory and exits with status 0 by default. `--fail-on high` fails only for high candidates; `medium` fails for high or medium; `low` fails for any candidate. Invalid thresholds exit with status 2.
|
|
41
41
|
|
|
42
|
-
JSON output uses schemaVersion 1, repository-relative paths, deterministic ordering, explainable `reasons` and `signals`, and a path-specific `suggestedCoverageRule` for high and medium candidates. Suggestions require human or agent review: LLMNav cannot infer a durable role, ownership boundary, invariant, or semantic ID from structure alone.
|
|
42
|
+
JSON output uses schemaVersion 1, repository-relative paths, deterministic ordering, explainable `reasons` and `signals`, and a path-specific `suggestedCoverageRule` for high and medium candidates. `--summary` omits candidate details. `--output <path>` writes the selected report inside the repository and emits only a compact confirmation envelope to stdout; escaping paths and symbolic-link traversal are rejected. Suggestions require human or agent review: LLMNav cannot infer a durable role, ownership boundary, invariant, or semantic ID from structure alone.
|
|
43
43
|
|
|
44
44
|
## `llmnav check`
|
|
45
45
|
|
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
|
|
package/docs/publishing.md
CHANGED
|
@@ -31,7 +31,7 @@ npm run release:check
|
|
|
31
31
|
|
|
32
32
|
## 3. Configure npm authentication
|
|
33
33
|
|
|
34
|
-
The supplied release workflow uses GitHub's OIDC token through npm Trusted Publishers. The GitHub source repository is private, so the workflow explicitly disables provenance; npm accepts provenance only from public source repositories. If the repository becomes public, enable provenance in both `package.json` and the workflow after a successful release check.
|
|
34
|
+
The supplied release workflow uses GitHub's OIDC token through npm Trusted Publishers. The GitHub source repository is private, so the workflow explicitly disables provenance; npm accepts provenance only from public source repositories. If the repository becomes public, enable provenance in both `package.json` and the workflow after a successful release check. The same tag workflow creates an idempotent GitHub Release only after the npm registry check or publication succeeds.
|
|
35
35
|
|
|
36
36
|
The workflow references a GitHub environment named `npm`. Create that environment for release protection, or remove the `environment` line when no environment gate is desired.
|
|
37
37
|
|
|
@@ -65,7 +65,7 @@ git tag v0.6.0
|
|
|
65
65
|
git push origin v0.6.0
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
The release workflow rejects a tag that does not match `package.json`. If the exact version is already present in npm, the workflow succeeds only when the registry tarball integrity matches the tagged package; a mismatched package fails closed.
|
|
68
|
+
The release workflow rejects a tag that does not match `package.json`. If the exact version is already present in npm, the workflow succeeds only when the registry tarball integrity matches the tagged package; a mismatched package fails closed. It then reuses an existing GitHub Release for the tag or creates one with generated release notes. Registry, GitHub API, and Release creation errors other than an expected missing Release fail the workflow.
|
|
69
69
|
|
|
70
70
|
## 6. Verify from a clean directory
|
|
71
71
|
|
package/docs/quickstart.md
CHANGED
|
@@ -19,7 +19,7 @@ Source roots and evaluation files must remain inside the repository. The generat
|
|
|
19
19
|
npx llmnav audit
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Start with high and medium candidates. The audit explains whether a file is a package entrypoint, public API, generated structural boundary, or high fan-in module. It does not write source or invent card contents. Declaration files and common non-production or utility shapes are suppressed so the result is a review queue, not a demand to annotate every file.
|
|
22
|
+
Start with high and medium candidates. The audit explains whether a file is a package entrypoint, public API, generated structural boundary, or high fan-in module. Narrow source signals also identify versioned schema literals, persistent dotted JSON filename protocols, and Tauri invoke adapters. It does not write source or invent card contents. Declaration files and common non-production or low-fan-in utility shapes are suppressed so the result is a review queue, not a demand to annotate every file.
|
|
23
23
|
|
|
24
24
|
After accepting a candidate, write its durable meaning by inspecting the source, add a narrow `coverageRules` entry for that exact boundary, and add a real task-language query to `.llmnav/eval/queries.jsonl`. Use `npx llmnav audit --fail-on high` in CI only after the initial review.
|
|
25
25
|
|
package/package.json
CHANGED
package/src/audit.js
CHANGED
|
@@ -47,8 +47,7 @@ export async function auditProject(root) {
|
|
|
47
47
|
for (const target of unique) importedBy.get(target)?.add(file);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
const entrypoints = collectPackageEntrypoints(packageJson, fileByPath);
|
|
50
|
+
const entrypoints = await collectWorkspacePackageEntrypoints(root, fileByPath);
|
|
52
51
|
const publicApiPaths = collectPublicApiPaths(entrypoints, fileByPath);
|
|
53
52
|
const candidates = [];
|
|
54
53
|
|
|
@@ -58,6 +57,7 @@ export async function auditProject(root) {
|
|
|
58
57
|
const boundaries = detectBoundaries({
|
|
59
58
|
relativePath: file,
|
|
60
59
|
card: { effect: [], risk: [] },
|
|
60
|
+
source: record.source ?? "",
|
|
61
61
|
}).map((boundary) => boundary.kind);
|
|
62
62
|
const exportedDeclarations = countExportedDeclarations(record.source ?? "", file);
|
|
63
63
|
const entrypoint = entrypoints.has(file);
|
|
@@ -66,7 +66,7 @@ export async function auditProject(root) {
|
|
|
66
66
|
const reexportBarrel = isReexportBarrel(record.source ?? "", file);
|
|
67
67
|
const basename = path.posix.basename(file, path.posix.extname(file)).toLowerCase();
|
|
68
68
|
const broadUtility = UTILITY_NAME_PATTERN.test(basename) ||
|
|
69
|
-
(exportedDeclarations >= 10 && !entrypoint && !publicApi && boundaries.length === 0);
|
|
69
|
+
(exportedDeclarations >= 10 && importers <= 1 && !entrypoint && !publicApi && boundaries.length === 0);
|
|
70
70
|
const nonProduction = NON_PRODUCTION_PATH_PATTERN.test(file);
|
|
71
71
|
const largeSource = record.sourceBytes >= LARGE_SOURCE_BYTES;
|
|
72
72
|
const hasSignal = entrypoint || publicApi || boundaries.length > 0 || importers > 0 || exportedDeclarations > 0 || largeSource;
|
|
@@ -174,14 +174,34 @@ function buildCoverageSuggestion(file) {
|
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
function
|
|
177
|
+
async function collectWorkspacePackageEntrypoints(root, fileByPath) {
|
|
178
|
+
const packageDirectories = new Set([""]);
|
|
179
|
+
for (const file of fileByPath.keys()) {
|
|
180
|
+
let directory = path.posix.dirname(file);
|
|
181
|
+
while (directory !== "." && directory !== "") {
|
|
182
|
+
packageDirectories.add(directory);
|
|
183
|
+
const parent = path.posix.dirname(directory);
|
|
184
|
+
if (parent === directory || parent === ".") break;
|
|
185
|
+
directory = parent;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const entrypoints = new Set();
|
|
189
|
+
for (const directory of [...packageDirectories].sort(compareText)) {
|
|
190
|
+
const packageJson = await readJsonSafe(path.join(root, directory, "package.json"), null);
|
|
191
|
+
if (!packageJson || typeof packageJson !== "object") continue;
|
|
192
|
+
for (const entrypoint of collectPackageEntrypoints(packageJson, fileByPath, directory)) entrypoints.add(entrypoint);
|
|
193
|
+
}
|
|
194
|
+
return entrypoints;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function collectPackageEntrypoints(packageJson, fileByPath, packageDirectory = "") {
|
|
178
198
|
const raw = [packageJson?.main, packageJson?.module, ...collectStringLeaves(packageJson?.bin), ...collectStringLeaves(packageJson?.exports)];
|
|
179
199
|
const entrypoints = new Set();
|
|
180
200
|
for (const value of raw) {
|
|
181
201
|
if (typeof value !== "string" || /\.d\.[cm]?ts$/u.test(value)) continue;
|
|
182
202
|
const normalized = normalizePackagePath(value);
|
|
183
203
|
if (!normalized) continue;
|
|
184
|
-
const resolved = resolveProjectPath(normalized, fileByPath);
|
|
204
|
+
const resolved = resolveProjectPath(path.posix.join(packageDirectory, normalized), fileByPath);
|
|
185
205
|
if (resolved) entrypoints.add(resolved);
|
|
186
206
|
}
|
|
187
207
|
return entrypoints;
|
|
@@ -237,12 +257,52 @@ function isReexportBarrel(source, file) {
|
|
|
237
257
|
}
|
|
238
258
|
|
|
239
259
|
function resolveLocalSpecifier(fromFile, specifier, fileByPath) {
|
|
260
|
+
if (/\.rs$/u.test(fromFile)) return resolveRustSpecifier(fromFile, specifier, fileByPath);
|
|
240
261
|
if (!specifier.startsWith(".")) return null;
|
|
241
262
|
const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), toPosix(specifier)));
|
|
242
263
|
if (base.startsWith("../") || base === "..") return null;
|
|
243
264
|
return resolveProjectPath(base, fileByPath);
|
|
244
265
|
}
|
|
245
266
|
|
|
267
|
+
function resolveRustSpecifier(fromFile, specifier, fileByPath) {
|
|
268
|
+
const normalized = specifier
|
|
269
|
+
.replace(/\s+as\s+.+$/u, "")
|
|
270
|
+
.replace(/::\{[\s\S]*$/u, "")
|
|
271
|
+
.replace(/::\*$/u, "")
|
|
272
|
+
.trim();
|
|
273
|
+
if (!normalized || /[{}(),]/u.test(normalized)) return null;
|
|
274
|
+
const parts = normalized.split("::").filter(Boolean);
|
|
275
|
+
const sourceIndex = fromFile.split("/").lastIndexOf("src");
|
|
276
|
+
const crateSource = sourceIndex >= 0 ? fromFile.split("/").slice(0, sourceIndex + 1).join("/") : path.posix.dirname(fromFile);
|
|
277
|
+
let base;
|
|
278
|
+
if (parts[0] === "crate") {
|
|
279
|
+
parts.shift();
|
|
280
|
+
base = crateSource;
|
|
281
|
+
} else if (parts[0] === "self") {
|
|
282
|
+
parts.shift();
|
|
283
|
+
base = rustModuleDirectory(fromFile);
|
|
284
|
+
} else if (parts[0] === "super") {
|
|
285
|
+
while (parts[0] === "super") {
|
|
286
|
+
parts.shift();
|
|
287
|
+
base = path.posix.dirname(base ?? rustModuleDirectory(fromFile));
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
base = rustModuleDirectory(fromFile);
|
|
291
|
+
}
|
|
292
|
+
if (parts.length === 0) return null;
|
|
293
|
+
const candidate = path.posix.join(base, ...parts);
|
|
294
|
+
return [
|
|
295
|
+
`${candidate}.rs`,
|
|
296
|
+
`${candidate}/mod.rs`,
|
|
297
|
+
].find((value) => fileByPath.has(value)) ?? null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function rustModuleDirectory(fromFile) {
|
|
301
|
+
const basename = path.posix.basename(fromFile);
|
|
302
|
+
if (basename === "lib.rs" || basename === "main.rs" || basename === "mod.rs") return path.posix.dirname(fromFile);
|
|
303
|
+
return path.posix.join(path.posix.dirname(fromFile), path.posix.basename(fromFile, ".rs"));
|
|
304
|
+
}
|
|
305
|
+
|
|
246
306
|
function resolveProjectPath(candidate, fileByPath) {
|
|
247
307
|
const values = [candidate];
|
|
248
308
|
const extension = path.posix.extname(candidate);
|
package/src/boundaries.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* llmnav/1 module
|
|
2
2
|
id=llmnav.structure.boundaries
|
|
3
|
-
role=Detect route, event, schema, migration, and command boundaries from generated local evidence.
|
|
3
|
+
role=Detect durable artifact, route, event, schema, migration, runtime, and command boundaries from generated local evidence.
|
|
4
4
|
owns=boundary kinds|confidence assignment|boundary evidence
|
|
5
5
|
excludes=framework execution|source annotations
|
|
6
|
-
search=boundary detection|route schema migration|command event
|
|
6
|
+
search=boundary detection|artifact route schema migration|command event runtime
|
|
7
7
|
rel=workflow>llmnav.index.generate
|
|
8
8
|
stability=architecture
|
|
9
9
|
*/
|
|
@@ -11,13 +11,14 @@ stability=architecture
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { compareText, toPosix } from "./util.js";
|
|
13
13
|
|
|
14
|
-
export const BOUNDARY_KINDS = Object.freeze(["command", "event", "migration", "route", "schema"]);
|
|
14
|
+
export const BOUNDARY_KINDS = Object.freeze(["artifact", "command", "event", "migration", "route", "runtime", "schema"]);
|
|
15
15
|
|
|
16
16
|
export function detectBoundaries(record) {
|
|
17
17
|
const relativePath = toPosix(record.relativePath).toLowerCase();
|
|
18
18
|
const basename = path.posix.basename(relativePath);
|
|
19
19
|
const effects = record.card.effect ?? [];
|
|
20
20
|
const risks = record.card.risk ?? [];
|
|
21
|
+
const source = record.source ?? "";
|
|
21
22
|
const boundaries = new Map();
|
|
22
23
|
const add = (kind, confidence, evidence) => {
|
|
23
24
|
const current = boundaries.get(kind);
|
|
@@ -39,12 +40,32 @@ export function detectBoundaries(record) {
|
|
|
39
40
|
/(?:schema\.(?:json|ya?ml)|\.(?:proto|graphql|gql))$/u.test(relativePath)) {
|
|
40
41
|
add("schema", "high", "path");
|
|
41
42
|
}
|
|
43
|
+
if (/\bschemaVersion\s*:\s*["'][^"'\r\n]+\/v\d+(?:\.\d+)*["']/u.test(source)) {
|
|
44
|
+
add("schema", "high", "versioned-schema-literal");
|
|
45
|
+
}
|
|
46
|
+
if (/\bexport\s+(?:async\s+)?function\s+[A-Za-z_$][\w$]*FileName\b/u.test(source) &&
|
|
47
|
+
/["'`][^"'`\r\n]*\.[a-z0-9-]+\.json["'`]/iu.test(source)) {
|
|
48
|
+
add("artifact", "medium", "persistent-json-filename");
|
|
49
|
+
}
|
|
42
50
|
if (risks.includes("migration") || /(?:^|\/)(?:migrations?|migrate)(?:\/|$)/u.test(relativePath)) {
|
|
43
51
|
add("migration", "high", risks.includes("migration") ? "risk" : "path");
|
|
44
52
|
}
|
|
45
53
|
if (/(?:^|\/)(?:commands?|cli|bin)(?:\/|$)/u.test(relativePath) || /(?:command|cmd)\.[^.]+$/u.test(basename)) {
|
|
46
54
|
add("command", "high", "path");
|
|
47
55
|
}
|
|
56
|
+
if (/\.rs$/u.test(relativePath) && /#\[tauri::command\]|tauri::generate_handler!/u.test(source)) {
|
|
57
|
+
add("command", "high", "tauri-command");
|
|
58
|
+
}
|
|
59
|
+
if (/\.[cm]?[jt]sx?$/u.test(relativePath) &&
|
|
60
|
+
/\bgetTauriInvoke\s*\(/u.test(source) &&
|
|
61
|
+
/\binvoke(?:\s*<[^;\r\n]+?>)?\s*\(/u.test(source)) {
|
|
62
|
+
add("command", "high", "tauri-invoke");
|
|
63
|
+
}
|
|
64
|
+
if (/\.rs$/u.test(relativePath) &&
|
|
65
|
+
/#\[cfg\((?:windows|unix|target_(?:os|family))/u.test(source) &&
|
|
66
|
+
/\b(?:Drop|shutdown|terminate|kill|process_group|job_object)\b/iu.test(source)) {
|
|
67
|
+
add("runtime", "medium", "platform-lifecycle");
|
|
68
|
+
}
|
|
48
69
|
|
|
49
70
|
return [...boundaries.values()].sort((left, right) => compareText(left.kind, right.kind));
|
|
50
71
|
}
|
package/src/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
SPEC_VERSION,
|
|
32
32
|
STABILITIES,
|
|
33
33
|
} from "./spec.js";
|
|
34
|
-
import { parseInteger } from "./util.js";
|
|
34
|
+
import { assertNoSymlinkTraversal, atomicWrite, parseInteger, relativePosix } from "./util.js";
|
|
35
35
|
import { diagnosticsToSarif } from "./sarif.js";
|
|
36
36
|
import { loadGraphInputs } from "./graph-input.js";
|
|
37
37
|
import { renderGraphNode } from "./graph.js";
|
|
@@ -40,7 +40,7 @@ import { loadPromptPrefixBundle } from "./prompt-bundle.js";
|
|
|
40
40
|
import { diagnosticsToEditor, getEditorIntegration } from "./editor.js";
|
|
41
41
|
import { AUDIT_PRIORITIES, auditHasFindings, auditProject } from "./audit.js";
|
|
42
42
|
|
|
43
|
-
const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file", "--fail-on"]);
|
|
43
|
+
const VALUE_OPTIONS = new Set(["--root", "--format", "--top", "--depth", "--budget", "--max-edges", "--agents", "--file", "--fail-on", "--output"]);
|
|
44
44
|
|
|
45
45
|
const COMMAND_OPTIONS = Object.freeze({
|
|
46
46
|
init: new Set(["--agents", "--package-scripts", "--force", "--root", "--json"]),
|
|
@@ -53,7 +53,7 @@ const COMMAND_OPTIONS = Object.freeze({
|
|
|
53
53
|
context: new Set(["--depth", "--budget", "--max-edges", "--root", "--json"]),
|
|
54
54
|
eval: new Set(["--file", "--top", "--root", "--json"]),
|
|
55
55
|
doctor: new Set(["--root", "--json"]),
|
|
56
|
-
audit: new Set(["--root", "--json", "--fail-on"]),
|
|
56
|
+
audit: new Set(["--root", "--json", "--summary", "--fail-on", "--output"]),
|
|
57
57
|
spec: new Set(["--root", "--json"]),
|
|
58
58
|
tools: new Set(["--json"]),
|
|
59
59
|
bundle: new Set(["--root", "--json"]),
|
|
@@ -300,10 +300,27 @@ async function runAudit(root, args, json) {
|
|
|
300
300
|
if (failOn !== "none" && !AUDIT_PRIORITIES.includes(failOn)) {
|
|
301
301
|
throw usageError(`audit --fail-on must be one of none, ${AUDIT_PRIORITIES.join(", ")}.`);
|
|
302
302
|
}
|
|
303
|
+
const outputOption = getOption(args, "--output");
|
|
304
|
+
const outputPath = outputOption ? path.resolve(root, outputOption) : null;
|
|
305
|
+
if (outputPath) {
|
|
306
|
+
try {
|
|
307
|
+
await assertNoSymlinkTraversal(root, outputPath, "audit output file");
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
303
312
|
const result = await auditProject(root);
|
|
304
313
|
const report = { ...result, failOn };
|
|
314
|
+
const selectedReport = hasFlag(args, "--summary")
|
|
315
|
+
? { schemaVersion: result.schemaVersion, repositoryId: result.repositoryId, summary: result.summary, failOn }
|
|
316
|
+
: report;
|
|
317
|
+
if (outputPath) {
|
|
318
|
+
await atomicWrite(outputPath, `${JSON.stringify(selectedReport, null, 2)}\n`);
|
|
319
|
+
}
|
|
305
320
|
if (json) {
|
|
306
|
-
console.log(JSON.stringify(
|
|
321
|
+
console.log(JSON.stringify(outputPath
|
|
322
|
+
? { schemaVersion: result.schemaVersion, repositoryId: result.repositoryId, summary: result.summary, failOn, output: relativePosix(root, outputPath) }
|
|
323
|
+
: selectedReport, null, 2));
|
|
307
324
|
} else {
|
|
308
325
|
const { summary } = result;
|
|
309
326
|
console.log(
|
|
@@ -314,6 +331,7 @@ async function runAudit(root, args, json) {
|
|
|
314
331
|
console.log(`${candidate.priority} ${candidate.path} score=${candidate.score} ${candidate.reasons.join(",")}`);
|
|
315
332
|
}
|
|
316
333
|
if (summary.low > 0) console.log(`${summary.low} low-priority candidate(s) are available in --json output.`);
|
|
334
|
+
if (outputPath) console.log(`wrote ${relativePosix(root, outputPath)}`);
|
|
317
335
|
}
|
|
318
336
|
return auditHasFindings(result, failOn) ? 1 : 0;
|
|
319
337
|
}
|
|
@@ -474,7 +492,7 @@ Usage
|
|
|
474
492
|
llmnav context <semantic-id> [--depth 1] [--budget 2500] [--max-edges 24]
|
|
475
493
|
llmnav eval [--file path] [--top 5]
|
|
476
494
|
llmnav doctor
|
|
477
|
-
llmnav audit [--fail-on none|high|medium|low]
|
|
495
|
+
llmnav audit [--summary] [--output path] [--fail-on none|high|medium|low]
|
|
478
496
|
llmnav spec
|
|
479
497
|
llmnav tools [--json]
|
|
480
498
|
llmnav bundle [--json]
|
package/src/index.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface Declaration {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export interface DetectedBoundary {
|
|
61
|
-
kind: "command" | "event" | "migration" | "route" | "schema";
|
|
61
|
+
kind: "artifact" | "command" | "event" | "migration" | "route" | "runtime" | "schema";
|
|
62
62
|
confidence: "high" | "medium";
|
|
63
63
|
evidence: string[];
|
|
64
64
|
}
|
package/src/spec.js
CHANGED
|
@@ -10,7 +10,7 @@ rel=workflow>llmnav.rules.validate
|
|
|
10
10
|
stability=contract
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export const PACKAGE_VERSION = "0.6.
|
|
13
|
+
export const PACKAGE_VERSION = "0.6.2";
|
|
14
14
|
export const SPEC_VERSION = "1";
|
|
15
15
|
|
|
16
16
|
export const SCOPES = Object.freeze(["file", "module", "symbol"]);
|
|
@@ -163,6 +163,8 @@ export const DEFAULT_EXCLUDED_DIRECTORIES = Object.freeze([
|
|
|
163
163
|
".astro",
|
|
164
164
|
".turbo",
|
|
165
165
|
".cache",
|
|
166
|
+
".bun-cache",
|
|
167
|
+
".pnpm-store",
|
|
166
168
|
"target",
|
|
167
169
|
"bin",
|
|
168
170
|
"obj",
|