@vibe-agent-toolkit/cli 0.1.41-rc.7 → 0.1.41-rc.8
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/dist/bin/vat +24 -1
- package/dist/bin/vat.js +24 -1
- package/dist/bin/vat.js.map +1 -1
- package/dist/bin.js +12 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/agent/list.d.ts +11 -0
- package/dist/commands/agent/list.d.ts.map +1 -1
- package/dist/commands/agent/list.js +30 -11
- package/dist/commands/agent/list.js.map +1 -1
- package/dist/commands/audit.d.ts +34 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +220 -88
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/build.d.ts +14 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +89 -24
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/claude/marketplace/validate.d.ts +97 -0
- package/dist/commands/claude/marketplace/validate.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/validate.js +154 -40
- package/dist/commands/claude/marketplace/validate.js.map +1 -1
- package/dist/commands/claude/org/stubs.d.ts +9 -1
- package/dist/commands/claude/org/stubs.d.ts.map +1 -1
- package/dist/commands/claude/org/stubs.js +11 -3
- package/dist/commands/claude/org/stubs.js.map +1 -1
- package/dist/commands/claude/plugin/build.d.ts +25 -2
- package/dist/commands/claude/plugin/build.d.ts.map +1 -1
- package/dist/commands/claude/plugin/build.js +99 -10
- package/dist/commands/claude/plugin/build.js.map +1 -1
- package/dist/commands/claude/plugin/plugin-json-merge.d.ts.map +1 -1
- package/dist/commands/claude/plugin/plugin-json-merge.js +15 -0
- package/dist/commands/claude/plugin/plugin-json-merge.js.map +1 -1
- package/dist/commands/corpus/report.d.ts +22 -4
- package/dist/commands/corpus/report.d.ts.map +1 -1
- package/dist/commands/corpus/report.js +1 -0
- package/dist/commands/corpus/report.js.map +1 -1
- package/dist/commands/corpus/runner.d.ts +16 -1
- package/dist/commands/corpus/runner.d.ts.map +1 -1
- package/dist/commands/corpus/runner.js +40 -22
- package/dist/commands/corpus/runner.js.map +1 -1
- package/dist/commands/inventory.d.ts +2 -0
- package/dist/commands/inventory.d.ts.map +1 -1
- package/dist/commands/inventory.js +45 -4
- package/dist/commands/inventory.js.map +1 -1
- package/dist/commands/phase-utils.d.ts +183 -14
- package/dist/commands/phase-utils.d.ts.map +1 -1
- package/dist/commands/phase-utils.js +294 -29
- package/dist/commands/phase-utils.js.map +1 -1
- package/dist/commands/rag/query-command.d.ts +40 -0
- package/dist/commands/rag/query-command.d.ts.map +1 -1
- package/dist/commands/rag/query-command.js +47 -20
- package/dist/commands/rag/query-command.js.map +1 -1
- package/dist/commands/resources/index.d.ts.map +1 -1
- package/dist/commands/resources/index.js +27 -9
- package/dist/commands/resources/index.js.map +1 -1
- package/dist/commands/resources/scan.d.ts +37 -0
- package/dist/commands/resources/scan.d.ts.map +1 -1
- package/dist/commands/resources/scan.js +47 -32
- package/dist/commands/resources/scan.js.map +1 -1
- package/dist/commands/resources/validate.d.ts +136 -1
- package/dist/commands/resources/validate.d.ts.map +1 -1
- package/dist/commands/resources/validate.js +105 -66
- package/dist/commands/resources/validate.js.map +1 -1
- package/dist/commands/skill/review.d.ts.map +1 -1
- package/dist/commands/skill/review.js +7 -3
- package/dist/commands/skill/review.js.map +1 -1
- package/dist/commands/skill/test/run.d.ts +1 -1
- package/dist/commands/skill/test/run.d.ts.map +1 -1
- package/dist/commands/skill/test/run.js +8 -6
- package/dist/commands/skill/test/run.js.map +1 -1
- package/dist/commands/skills/build.d.ts +96 -7
- package/dist/commands/skills/build.d.ts.map +1 -1
- package/dist/commands/skills/build.js +277 -60
- package/dist/commands/skills/build.js.map +1 -1
- package/dist/commands/skills/command-helpers.d.ts +6 -10
- package/dist/commands/skills/command-helpers.d.ts.map +1 -1
- package/dist/commands/skills/command-helpers.js +6 -22
- package/dist/commands/skills/command-helpers.js.map +1 -1
- package/dist/commands/skills/list.d.ts +18 -0
- package/dist/commands/skills/list.d.ts.map +1 -1
- package/dist/commands/skills/list.js +41 -15
- package/dist/commands/skills/list.js.map +1 -1
- package/dist/commands/skills/package.d.ts +15 -0
- package/dist/commands/skills/package.d.ts.map +1 -1
- package/dist/commands/skills/package.js +30 -20
- package/dist/commands/skills/package.js.map +1 -1
- package/dist/commands/skills/validate-command.d.ts.map +1 -1
- package/dist/commands/skills/validate-command.js +22 -6
- package/dist/commands/skills/validate-command.js.map +1 -1
- package/dist/commands/skills/validate.d.ts +44 -3
- package/dist/commands/skills/validate.d.ts.map +1 -1
- package/dist/commands/skills/validate.js +180 -46
- package/dist/commands/skills/validate.js.map +1 -1
- package/dist/commands/validate.d.ts +18 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +73 -66
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.d.ts +38 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +203 -83
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/skill-resolution/index.d.ts +1 -1
- package/dist/skill-resolution/index.d.ts.map +1 -1
- package/dist/skill-resolution/index.js +1 -1
- package/dist/skill-resolution/index.js.map +1 -1
- package/dist/skill-resolution/packaging-config.d.ts +15 -2
- package/dist/skill-resolution/packaging-config.d.ts.map +1 -1
- package/dist/skill-resolution/packaging-config.js +39 -2
- package/dist/skill-resolution/packaging-config.js.map +1 -1
- package/dist/utils/command-error.d.ts +34 -1
- package/dist/utils/command-error.d.ts.map +1 -1
- package/dist/utils/command-error.js +32 -1
- package/dist/utils/command-error.js.map +1 -1
- package/dist/utils/help-loader.d.ts +6 -2
- package/dist/utils/help-loader.d.ts.map +1 -1
- package/dist/utils/help-loader.js +9 -16
- package/dist/utils/help-loader.js.map +1 -1
- package/dist/utils/issue-rendering.d.ts +88 -0
- package/dist/utils/issue-rendering.d.ts.map +1 -1
- package/dist/utils/issue-rendering.js +94 -0
- package/dist/utils/issue-rendering.js.map +1 -1
- package/dist/utils/output.d.ts +54 -5
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +111 -17
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/relativize-paths.d.ts +45 -0
- package/dist/utils/relativize-paths.d.ts.map +1 -0
- package/dist/utils/relativize-paths.js +67 -0
- package/dist/utils/relativize-paths.js.map +1 -0
- package/dist/utils/resource-loader.d.ts +21 -2
- package/dist/utils/resource-loader.d.ts.map +1 -1
- package/dist/utils/resource-loader.js +85 -11
- package/dist/utils/resource-loader.js.map +1 -1
- package/dist/utils/skill-packaging-config.d.ts +22 -1
- package/dist/utils/skill-packaging-config.d.ts.map +1 -1
- package/dist/utils/skill-packaging-config.js +26 -0
- package/dist/utils/skill-packaging-config.js.map +1 -1
- package/dist/utils/verdict-helpers.d.ts +4 -3
- package/dist/utils/verdict-helpers.d.ts.map +1 -1
- package/dist/utils/verdict-helpers.js +6 -13
- package/dist/utils/verdict-helpers.js.map +1 -1
- package/docs/rag.md +9 -4
- package/docs/resources.md +30 -14
- package/docs/skills.md +9 -5
- package/package.json +12 -12
package/dist/commands/audit.js
CHANGED
|
@@ -11,12 +11,13 @@ import { detectFormat } from '@vibe-agent-toolkit/discovery';
|
|
|
11
11
|
import { findProjectRoot, gitFindRoot, GitTracker, isAbsolutePath, isGitUrl, issueLocation, parseGitUrl, resetProjectRootCaches, safePath, } from '@vibe-agent-toolkit/utils';
|
|
12
12
|
import { Command } from 'commander';
|
|
13
13
|
import picomatch from 'picomatch';
|
|
14
|
-
import { resetSkillDiscoveryCache, resolveSkillPackagingConfig, stripValidationAllowForDisplay, } from '../skill-resolution/packaging-config.js';
|
|
14
|
+
import { resetSkillDiscoveryCache, resolveProjectDeclaredEvalSuites, resolveSkillPackagingConfig, stripValidationAllowForDisplay, } from '../skill-resolution/packaging-config.js';
|
|
15
15
|
import { handleCommandError } from '../utils/command-error.js';
|
|
16
16
|
import { ConfigLoadError, loadConfig, resetLoadedConfigCache, } from '../utils/config-loader.js';
|
|
17
|
-
import {
|
|
17
|
+
import { formatIssueLines, formatSeverityBreakdown, issuesToRenderAtVerbosity, } from '../utils/issue-rendering.js';
|
|
18
18
|
import { createLogger } from '../utils/logger.js';
|
|
19
19
|
import { writeYamlOutput } from '../utils/output.js';
|
|
20
|
+
import { relativizePathEntries } from '../utils/relativize-paths.js';
|
|
20
21
|
import { mergeSkillPackagingConfig } from '../utils/skill-packaging-config.js';
|
|
21
22
|
import { renderSkillQualityFooter } from '../utils/skill-quality-footer.js';
|
|
22
23
|
import { computeConfigVerdicts } from '../utils/verdict-helpers.js';
|
|
@@ -168,15 +169,19 @@ async function validateSingleSkill(skillPath, options, logger, locationRoot, isV
|
|
|
168
169
|
const skillConfig = fullConfig === null ? null : stripValidationAllowForDisplay(fullConfig);
|
|
169
170
|
if (skillConfig !== null) {
|
|
170
171
|
logger.debug(` Using config-aware validation for: ${skillPath}`);
|
|
171
|
-
const { gitTracker } = await resolveScanContext(safePath.resolve(skillPath), locationRoot);
|
|
172
|
+
const { gitTracker } = await resolveScanContext(safePath.resolve(skillPath), locationRoot, logger);
|
|
172
173
|
// Key the shared registry on the same project root the validator derives,
|
|
173
174
|
// so every skill under one root parses the corpus once for the whole run
|
|
174
175
|
// instead of once each.
|
|
175
176
|
const skillDir = safePath.resolve(skillPath, '..');
|
|
176
177
|
const projectRoot = findProjectRoot(skillDir) ?? skillDir;
|
|
178
|
+
// The project's declared eval suites, memoized per config root, so this lane
|
|
179
|
+
// predicts the same bundle the packager produces: no skill's report counts a
|
|
180
|
+
// SIBLING skill's eval suite as an ordinary bundled file.
|
|
177
181
|
const sharedCtx = {
|
|
178
182
|
registry: await crawlAndResolveRegistry(projectRoot),
|
|
179
183
|
locationRoot,
|
|
184
|
+
projectSkills: await resolveProjectDeclaredEvalSuites(skillPath),
|
|
180
185
|
};
|
|
181
186
|
if (gitTracker !== null) {
|
|
182
187
|
sharedCtx.gitTracker = gitTracker;
|
|
@@ -368,7 +373,7 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
368
373
|
}
|
|
369
374
|
// Run compatibility analysis if --compat flag is set
|
|
370
375
|
const compatMap = options.compat
|
|
371
|
-
? await runCompatAnalysis(results, logger) // --settings not supported in --user mode
|
|
376
|
+
? await runCompatAnalysis(results, logger, scanRoot) // --settings not supported in --user mode
|
|
372
377
|
: undefined;
|
|
373
378
|
const verbose = options.verbose ?? false;
|
|
374
379
|
const skillResults = results.filter((r) => SKILL_RESULT_TYPES.has(r.type));
|
|
@@ -539,7 +544,7 @@ export async function buildAuditReport(scanPath, options, startTime, logger) {
|
|
|
539
544
|
const vatContextForCompat = await buildVATProjectContext(deriveConfigRoot(scanPath), logger);
|
|
540
545
|
// Run compatibility analysis if --compat flag is set
|
|
541
546
|
const compatMap = options.compat
|
|
542
|
-
? await runCompatAnalysis(results, logger, effectiveSettings, vatContextForCompat)
|
|
547
|
+
? await runCompatAnalysis(results, logger, scanRoot, effectiveSettings, vatContextForCompat)
|
|
543
548
|
: undefined;
|
|
544
549
|
const verbose = options.verbose ?? false;
|
|
545
550
|
return {
|
|
@@ -570,7 +575,7 @@ async function runAuditAtPath(scanPath, options, overrides = {}) {
|
|
|
570
575
|
if (verbose) {
|
|
571
576
|
renderVerboseEvidence(results, document.root, logger);
|
|
572
577
|
}
|
|
573
|
-
handleAuditResults(results, document, logger);
|
|
578
|
+
handleAuditResults(results, document, logger, verbose);
|
|
574
579
|
logger.debug(`Audit complete in ${Date.now() - startTime}ms`);
|
|
575
580
|
}
|
|
576
581
|
async function runUrlAudit(rawInput, options) {
|
|
@@ -813,14 +818,11 @@ export async function getValidationResults(scanPath, recursive, options, logger,
|
|
|
813
818
|
const stat = await fsp.stat(scanPath);
|
|
814
819
|
if (stat.isDirectory()) {
|
|
815
820
|
logger.debug('Scanning directory for resources');
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
? { ...options, exclude: [...(options.exclude ?? []), ...configExcludes] }
|
|
822
|
-
: options;
|
|
823
|
-
return scanDirectory(scanPath, recursive, mergedOptions, logger, await resolveScanContext(scanPath, locationRoot));
|
|
821
|
+
// The project's `resources.exclude` is applied by the scan context,
|
|
822
|
+
// on the project-root basis its globs were written against — NOT
|
|
823
|
+
// merged into `options.exclude`, whose patterns are relative to the
|
|
824
|
+
// directory the operator named. See resolveProjectExcludes.
|
|
825
|
+
return scanDirectory(scanPath, recursive, options, logger, await resolveScanContext(scanPath, locationRoot, logger));
|
|
824
826
|
}
|
|
825
827
|
}
|
|
826
828
|
catch {
|
|
@@ -895,6 +897,11 @@ function resolveConfigTargetsForPlugin(pluginDir, vatContext) {
|
|
|
895
897
|
* Non-plugin results are skipped silently.
|
|
896
898
|
* When effectiveSettings is provided, also runs settings conflict detection.
|
|
897
899
|
*
|
|
900
|
+
* `locationRoot` is the invocation scan root: the ONE base every emitted
|
|
901
|
+
* evidence `location.file` is relative to. One audit run spans many plugin
|
|
902
|
+
* directories, so anchoring evidence at each plugin would mix coordinate
|
|
903
|
+
* systems in a single document.
|
|
904
|
+
*
|
|
898
905
|
* When vatContext is provided, config-layer `targets` declarations are
|
|
899
906
|
* threaded through to the analyzer so `vat audit .` inside a VAT project
|
|
900
907
|
* matches `vat skills validate` verdicts. See
|
|
@@ -902,7 +909,7 @@ function resolveConfigTargetsForPlugin(pluginDir, vatContext) {
|
|
|
902
909
|
*
|
|
903
910
|
* @internal Exported for integration testing only — not part of the public CLI API.
|
|
904
911
|
*/
|
|
905
|
-
export async function runCompatAnalysis(results, logger, effectiveSettings, vatContext = null) {
|
|
912
|
+
export async function runCompatAnalysis(results, logger, locationRoot, effectiveSettings, vatContext = null) {
|
|
906
913
|
const compatMap = new Map();
|
|
907
914
|
for (const result of results) {
|
|
908
915
|
if (result.type !== RESOURCE_TYPE_CLAUDE_PLUGIN)
|
|
@@ -913,7 +920,7 @@ export async function runCompatAnalysis(results, logger, effectiveSettings, vatC
|
|
|
913
920
|
const analyzeOptions = configTargets === undefined
|
|
914
921
|
? undefined
|
|
915
922
|
: { configTargets };
|
|
916
|
-
const compat = await analyzeCompatibility(result.path, analyzeOptions);
|
|
923
|
+
const compat = await analyzeCompatibility(result.path, locationRoot, analyzeOptions);
|
|
917
924
|
// Settings conflict detection (when --settings flag is used)
|
|
918
925
|
if (effectiveSettings === undefined) {
|
|
919
926
|
compatMap.set(result.path, compat);
|
|
@@ -994,34 +1001,22 @@ function applyVerboseFilter(results, verbose) {
|
|
|
994
1001
|
});
|
|
995
1002
|
}
|
|
996
1003
|
/**
|
|
997
|
-
*
|
|
1004
|
+
* Nested carriers inside a file entry that publish their own `path`.
|
|
998
1005
|
*
|
|
999
|
-
* `
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
* `location` is the opposite: it cannot be re-based here because a relative
|
|
1004
|
-
* string whose base was never stated is not recoverable, which is exactly why
|
|
1005
|
-
* the base is threaded down to the producers instead.
|
|
1006
|
+
* `linkedFiles[].path` sat absolute in every document while every sibling
|
|
1007
|
+
* `path` and `location` beside it was root-relative — 532 of them in a single
|
|
1008
|
+
* `vat audit --user` run. Naming the carrier here is what re-bases it; a new
|
|
1009
|
+
* carrier that is not named keeps shipping absolute paths.
|
|
1006
1010
|
*/
|
|
1007
|
-
|
|
1008
|
-
return entries.map((entry) => {
|
|
1009
|
-
const relative = issueLocation(entry.path, root);
|
|
1010
|
-
// Pointing audit AT a single resource makes that resource the root, which
|
|
1011
|
-
// relativizes to the empty string. Spell it `.` — the POSIX name for "the
|
|
1012
|
-
// root itself" — so no consumer has to special-case an empty path, and
|
|
1013
|
-
// `join(root, path)` still resolves.
|
|
1014
|
-
return { ...entry, path: relative === '' ? '.' : relative };
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1011
|
+
const NESTED_PATH_CARRIERS = ['linkedFiles'];
|
|
1017
1012
|
function calculateSummary(results, startTime, compatMap, verbose, root) {
|
|
1018
|
-
const base = buildBaseSummary(results, startTime);
|
|
1019
|
-
const withCompat = applyCompatMap(
|
|
1013
|
+
const { entries, ...base } = buildBaseSummary(results, startTime);
|
|
1014
|
+
const withCompat = applyCompatMap(entries, compatMap);
|
|
1020
1015
|
return {
|
|
1021
1016
|
// Stated once, first, and the only absolute path in the document.
|
|
1022
1017
|
root,
|
|
1023
1018
|
...base,
|
|
1024
|
-
files:
|
|
1019
|
+
files: relativizePathEntries(applyVerboseFilter(withCompat, verbose), root, NESTED_PATH_CARRIERS),
|
|
1025
1020
|
};
|
|
1026
1021
|
}
|
|
1027
1022
|
/**
|
|
@@ -1088,7 +1083,7 @@ function renderVerboseEvidence(results, root, logger) {
|
|
|
1088
1083
|
}
|
|
1089
1084
|
}
|
|
1090
1085
|
}
|
|
1091
|
-
function handleAuditResults(results, summary, logger) {
|
|
1086
|
+
function handleAuditResults(results, summary, logger, verbose) {
|
|
1092
1087
|
const { filesWithErrors: errorCount, filesWithWarnings: warningCount, filesPassed: successCount, } = summary.summary;
|
|
1093
1088
|
// Report settings conflicts (advisory, non-blocking)
|
|
1094
1089
|
const totalSettingsConflicts = (summary.files ?? []).reduce((sum, f) => {
|
|
@@ -1100,10 +1095,12 @@ function handleAuditResults(results, summary, logger) {
|
|
|
1100
1095
|
// Audit is advisory only — always exit 0 for validation results.
|
|
1101
1096
|
// Use vat skills validate for gated validation (exit 1 on errors).
|
|
1102
1097
|
if (errorCount > 0) {
|
|
1103
|
-
|
|
1098
|
+
logger.error(`Audit failed: ${errorCount} file(s) with errors`);
|
|
1099
|
+
logFindingsForStatus(results, 'error', summary.root, logger.error.bind(logger), verbose);
|
|
1104
1100
|
}
|
|
1105
1101
|
else if (warningCount > 0) {
|
|
1106
|
-
|
|
1102
|
+
logger.info(`Audit passed with warnings: ${warningCount} file(s)`);
|
|
1103
|
+
logFindingsForStatus(results, 'warning', summary.root, logger.info.bind(logger), verbose);
|
|
1107
1104
|
}
|
|
1108
1105
|
else {
|
|
1109
1106
|
// `success` means "nothing actionable", NOT "nothing to see" — name the
|
|
@@ -1135,34 +1132,62 @@ function renderAuditFooter(results, logger) {
|
|
|
1135
1132
|
}
|
|
1136
1133
|
renderSkillQualityFooter(logger, hasSkillFindings, emittedCodes);
|
|
1137
1134
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1135
|
+
/**
|
|
1136
|
+
* The stderr findings report for a set of audit results — the HUMAN channel.
|
|
1137
|
+
*
|
|
1138
|
+
* Deliberately NOT the YAML document. `vat audit` publishes a machine-readable
|
|
1139
|
+
* report on stdout that CI and tooling parse, and that document carries every
|
|
1140
|
+
* finding at every verbosity; this stream is read by a person, and on a 90-skill
|
|
1141
|
+
* corpus it printed 26,480 lines, 3,480 of them occurrences of a single
|
|
1142
|
+
* high-cardinality warning code. The two channels answer different questions, so
|
|
1143
|
+
* only this one collapses.
|
|
1144
|
+
*
|
|
1145
|
+
* Which findings render in full is not decided here — it is
|
|
1146
|
+
* {@link issuesToRenderAtVerbosity}, shared with the other findings-reporting
|
|
1147
|
+
* verbs so `vat audit`, `vat skills validate` and `vat skills build` cannot drift
|
|
1148
|
+
* onto three answers again. Errors always render; warnings and info collapse
|
|
1149
|
+
* unless `verbose`; allow-listed findings never render.
|
|
1150
|
+
*
|
|
1151
|
+
* The severity breakdown on each file's heading is load-bearing, not decoration.
|
|
1152
|
+
* For a warning-only file at default verbosity that heading is the ENTIRE report
|
|
1153
|
+
* for that file, so dropping the counts would turn the collapse into silence —
|
|
1154
|
+
* the failure direction `issue-rendering.ts` exists to prevent. The trailing
|
|
1155
|
+
* line names the collapsed total and both ways to see it, for the same reason.
|
|
1156
|
+
*
|
|
1157
|
+
* Pure: takes results, returns lines. It reads `issues` and never rewrites it,
|
|
1158
|
+
* because `runAuditAtPath` builds the YAML document from the very same array.
|
|
1159
|
+
*
|
|
1160
|
+
* @internal Exported for unit testing only — not part of the public CLI API.
|
|
1161
|
+
*/
|
|
1162
|
+
export function formatAuditFindingsLines(results, root, verbose) {
|
|
1163
|
+
const lines = [];
|
|
1164
|
+
let collapsed = 0;
|
|
1165
|
+
for (const result of results) {
|
|
1166
|
+
const rendered = issuesToRenderAtVerbosity(result.issues, verbose);
|
|
1167
|
+
// What --verbose WOULD show, minus what this verbosity shows. Derived from
|
|
1168
|
+
// the policy rather than re-stating it, so the count cannot contradict it.
|
|
1169
|
+
collapsed += issuesToRenderAtVerbosity(result.issues, true).length - rendered.length;
|
|
1170
|
+
lines.push(`\n${issueLocation(result.path, root)} — ${formatSeverityBreakdown(countBySeverity(result.issues))}:`);
|
|
1171
|
+
for (const issue of rendered) {
|
|
1172
|
+
lines.push(...formatIssueLines(issue, ' '));
|
|
1173
|
+
}
|
|
1145
1174
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
const warningResults = results.filter((r) => r.status === 'warning');
|
|
1150
|
-
for (const result of warningResults) {
|
|
1151
|
-
logger.info(`\n${issueLocation(result.path, root)}:`);
|
|
1152
|
-
const warningIssues = result.issues.filter((i) => i.severity === 'warning');
|
|
1153
|
-
logIssues(warningIssues, logger.info.bind(logger));
|
|
1175
|
+
if (collapsed > 0) {
|
|
1176
|
+
lines.push(`\n${collapsed} warning/info finding(s) not shown — re-run with --verbose, ` +
|
|
1177
|
+
'or read the YAML report on stdout, which always lists every finding.');
|
|
1154
1178
|
}
|
|
1179
|
+
return lines;
|
|
1155
1180
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1181
|
+
/**
|
|
1182
|
+
* Write one status class's findings to stderr beneath the run banner.
|
|
1183
|
+
*
|
|
1184
|
+
* The banner counts FILES and the blocks below count FINDINGS; both are needed,
|
|
1185
|
+
* and `logFn` picks the channel the calling branch already established.
|
|
1186
|
+
*/
|
|
1187
|
+
function logFindingsForStatus(results, status, root, logFn, verbose) {
|
|
1188
|
+
const selected = results.filter((r) => r.status === status);
|
|
1189
|
+
for (const line of formatAuditFindingsLines(selected, root, verbose)) {
|
|
1190
|
+
logFn(line);
|
|
1166
1191
|
}
|
|
1167
1192
|
}
|
|
1168
1193
|
/**
|
|
@@ -1198,9 +1223,12 @@ async function handleFileEntry(entry, fullPath, options, logger, scanCtx) {
|
|
|
1198
1223
|
logger.debug(` Using config-aware validation for: ${fullPath}`);
|
|
1199
1224
|
// Thread the per-scan tracker into packaging validation so gitignore
|
|
1200
1225
|
// checks in the link-graph walk stay O(1).
|
|
1226
|
+
// `projectSkills`: same project-wide test-input rule as the lane above,
|
|
1227
|
+
// resolved through the same per-config-root memo.
|
|
1228
|
+
const projectSkills = await resolveProjectDeclaredEvalSuites(fullPath);
|
|
1201
1229
|
const sharedCtx = scanCtx.gitTracker === null
|
|
1202
|
-
? { locationRoot }
|
|
1203
|
-
: { gitTracker: scanCtx.gitTracker, locationRoot };
|
|
1230
|
+
? { locationRoot, projectSkills }
|
|
1231
|
+
: { gitTracker: scanCtx.gitTracker, locationRoot, projectSkills };
|
|
1204
1232
|
const packagingResult = await validateSkillForPackaging(fullPath, skillConfig, 'source', sharedCtx);
|
|
1205
1233
|
return packagingResultToValidationResult(fullPath, packagingResult, skillConfig.targets, locationRoot);
|
|
1206
1234
|
}
|
|
@@ -1237,6 +1265,52 @@ async function handleDirectoryEntry(fullPath, recursive, options, logger, baseDi
|
|
|
1237
1265
|
}
|
|
1238
1266
|
return results;
|
|
1239
1267
|
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Compile the governing project's `resources.exclude` for a scan.
|
|
1270
|
+
*
|
|
1271
|
+
* The config is found by walking UP from the scan directory ({@link findProjectRoot}),
|
|
1272
|
+
* not by looking in it. Looking only in the scan directory is how a path argument
|
|
1273
|
+
* silently voided every exclude the project had declared: `vat audit
|
|
1274
|
+
* packages/x/resources/skills/` found no config there, so a package that excludes
|
|
1275
|
+
* its deliberately-broken eval fixtures had them audited as production skills the
|
|
1276
|
+
* moment anyone named a subdirectory. Commit 8a466b0c fixed the same defect for
|
|
1277
|
+
* `vat resources validate|scan` and `vat rag index`; this is the lane it missed.
|
|
1278
|
+
* The rule is the same one that commit established: a path argument says WHICH
|
|
1279
|
+
* tree to audit, and `exclude` applies either way.
|
|
1280
|
+
*
|
|
1281
|
+
* With one deliberate exception, mirroring the gitignore rule in
|
|
1282
|
+
* {@link resolveScanContext}: when the operator points the scan AT an excluded
|
|
1283
|
+
* tree, their explicit intent wins and the excludes are dropped for that run.
|
|
1284
|
+
* Otherwise naming an excluded directory would report `filesScanned: 0,
|
|
1285
|
+
* status: success` — a green run that scanned nothing.
|
|
1286
|
+
*/
|
|
1287
|
+
function resolveProjectExcludes(scanDir, logger) {
|
|
1288
|
+
const projectRoot = findProjectRoot(scanDir);
|
|
1289
|
+
if (projectRoot === null)
|
|
1290
|
+
return null;
|
|
1291
|
+
let patterns;
|
|
1292
|
+
try {
|
|
1293
|
+
patterns = loadConfig(projectRoot)?.resources?.exclude ?? [];
|
|
1294
|
+
}
|
|
1295
|
+
catch (err) {
|
|
1296
|
+
// Audit is a bulk linter over trees it does not own; a broken governing
|
|
1297
|
+
// config must not abort the scan. Say so rather than dropping it silently.
|
|
1298
|
+
logger.debug(`Ignoring unreadable config at ${projectRoot}: ${String(err)}`);
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
if (patterns.length === 0)
|
|
1302
|
+
return null;
|
|
1303
|
+
// dot:true so excludes like `**/.cache/*` match through dotfile dirs;
|
|
1304
|
+
// without it the exclude silently never fires.
|
|
1305
|
+
const isMatch = picomatch([...patterns], { dot: true });
|
|
1306
|
+
const scanRelToProject = safePath.relative(projectRoot, safePath.resolve(scanDir)).replaceAll('\\', '/');
|
|
1307
|
+
if (scanRelToProject !== '' && isExcludedPath(isMatch, scanRelToProject, true)) {
|
|
1308
|
+
logger.debug(`Scan root ${scanRelToProject} is excluded by the config at ${projectRoot}; ` +
|
|
1309
|
+
'auditing it anyway because it was named explicitly');
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
return { isMatch, base: projectRoot, label: 'excluded by config' };
|
|
1313
|
+
}
|
|
1240
1314
|
/** Cache of (gitRoot → initialized GitTracker) to avoid re-spawning ls-files. */
|
|
1241
1315
|
const gitTrackerCache = new Map();
|
|
1242
1316
|
/**
|
|
@@ -1264,10 +1338,27 @@ async function getOrCreateInventoryRegistry(projectRoot) {
|
|
|
1264
1338
|
/**
|
|
1265
1339
|
* `extractClaudePluginInventory` for a plugin directory, handing it the shared
|
|
1266
1340
|
* inventory registry so each skill it walks does not re-parse the corpus.
|
|
1341
|
+
*
|
|
1342
|
+
* A PROVIDER, not an awaited registry, and only when there is a project root — the same
|
|
1343
|
+
* two rules `vat inventory`'s `linkRegistryProviderFor` follows, for the same two reasons:
|
|
1344
|
+
*
|
|
1345
|
+
* 1. **Unresolved.** `extractClaudePluginInventory` promises "never throws — all failures
|
|
1346
|
+
* surface via parseErrors[]", and that promise holds only while the crawl runs inside
|
|
1347
|
+
* `walkLinkedFiles`'s try/catch. Awaiting it here moved the crawl outside that catch,
|
|
1348
|
+
* so one unreadable markdown file anywhere under the root aborted the WHOLE audit with
|
|
1349
|
+
* `status: error` and exit code 2 instead of degrading three link walks. The extractor
|
|
1350
|
+
* memoizes the provider and caches even a rejection, so the failed crawl is still
|
|
1351
|
+
* attempted once, not once per skill.
|
|
1352
|
+
* 2. **Only a project root.** Reuse is gated on exact `registry.baseDir === projectRoot`
|
|
1353
|
+
* equality against the root `walkLinkedFiles` derives per skill
|
|
1354
|
+
* (`findProjectRoot(dirname(skillMd)) ?? dirname(skillMd)`). With no project root that
|
|
1355
|
+
* per-skill fallback is each skill's OWN directory, so a registry rooted at the plugin
|
|
1356
|
+
* matches nothing: it would be crawled, compared, discarded, and re-crawled per skill.
|
|
1357
|
+
* No root, no provider.
|
|
1267
1358
|
*/
|
|
1268
1359
|
async function pluginInventoryAt(dir) {
|
|
1269
|
-
const
|
|
1270
|
-
return extractClaudePluginInventory(dir,
|
|
1360
|
+
const projectRoot = findProjectRoot(dir);
|
|
1361
|
+
return extractClaudePluginInventory(dir, projectRoot === null ? undefined : () => getOrCreateInventoryRegistry(projectRoot));
|
|
1271
1362
|
}
|
|
1272
1363
|
/**
|
|
1273
1364
|
* Reset all module-level audit caches. Must be called at the start of every
|
|
@@ -1303,18 +1394,19 @@ async function getOrCreateGitTracker(gitRoot) {
|
|
|
1303
1394
|
* `dist/`), returns `{ gitRoot: null, gitTracker: null }` to disable gitignore
|
|
1304
1395
|
* filtering — the user's explicit intent takes precedence over .gitignore.
|
|
1305
1396
|
*/
|
|
1306
|
-
async function resolveScanContext(dirPath, locationRoot) {
|
|
1397
|
+
async function resolveScanContext(dirPath, locationRoot, logger) {
|
|
1398
|
+
const projectExcludes = resolveProjectExcludes(dirPath, logger);
|
|
1307
1399
|
const detectedRoot = gitFindRoot(dirPath);
|
|
1308
1400
|
if (detectedRoot === null) {
|
|
1309
|
-
return { gitRoot: null, gitTracker: null, locationRoot };
|
|
1401
|
+
return { gitRoot: null, gitTracker: null, locationRoot, projectExcludes };
|
|
1310
1402
|
}
|
|
1311
1403
|
const tracker = await getOrCreateGitTracker(detectedRoot);
|
|
1312
1404
|
// If the scan root itself is gitignored (e.g. user targeted `dist/`), disable
|
|
1313
1405
|
// gitignore filtering so the user's explicit intent wins.
|
|
1314
1406
|
if (tracker.isIgnoredByActiveSet(dirPath)) {
|
|
1315
|
-
return { gitRoot: null, gitTracker: null, locationRoot };
|
|
1407
|
+
return { gitRoot: null, gitTracker: null, locationRoot, projectExcludes };
|
|
1316
1408
|
}
|
|
1317
|
-
return { gitRoot: detectedRoot, gitTracker: tracker, locationRoot };
|
|
1409
|
+
return { gitRoot: detectedRoot, gitTracker: tracker, locationRoot, projectExcludes };
|
|
1318
1410
|
}
|
|
1319
1411
|
/**
|
|
1320
1412
|
* Build the gitignore exclusion map for a set of entry paths.
|
|
@@ -1335,7 +1427,7 @@ function buildGitIgnoreMap(entryPaths, gitTracker, includeArtifacts) {
|
|
|
1335
1427
|
* Check whether a directory entry should be skipped during scanning.
|
|
1336
1428
|
* Returns a reason string for debug logging, or null if the entry should be kept.
|
|
1337
1429
|
*/
|
|
1338
|
-
function getSkipReason(entry, fullPath,
|
|
1430
|
+
function getSkipReason(entry, fullPath, gitIgnoredMap, excludeMatchers) {
|
|
1339
1431
|
// Always skip .git directory (not reported by git check-ignore)
|
|
1340
1432
|
if (entry.isDirectory() && entry.name === '.git') {
|
|
1341
1433
|
return '.git';
|
|
@@ -1344,11 +1436,11 @@ function getSkipReason(entry, fullPath, resolvedBaseDir, gitIgnoredMap, isMatch)
|
|
|
1344
1436
|
if (gitIgnoredMap !== null && gitIgnoredMap.get(fullPath) === true) {
|
|
1345
1437
|
return `gitignored: ${entry.name}`;
|
|
1346
1438
|
}
|
|
1347
|
-
//
|
|
1348
|
-
|
|
1349
|
-
const relativePath = safePath.relative(
|
|
1350
|
-
if (isExcludedPath(isMatch, relativePath, entry.isDirectory())) {
|
|
1351
|
-
return
|
|
1439
|
+
// Each matcher answers against ITS OWN base — see {@link ExcludeMatcher}.
|
|
1440
|
+
for (const matcher of excludeMatchers) {
|
|
1441
|
+
const relativePath = safePath.relative(matcher.base, fullPath).replaceAll('\\', '/');
|
|
1442
|
+
if (isExcludedPath(matcher.isMatch, relativePath, entry.isDirectory())) {
|
|
1443
|
+
return `${matcher.label}: ${relativePath}`;
|
|
1352
1444
|
}
|
|
1353
1445
|
}
|
|
1354
1446
|
return null;
|
|
@@ -1381,14 +1473,26 @@ async function scanDirectory(dirPath, recursive, options, logger, scanCtx, baseD
|
|
|
1381
1473
|
// Compile picomatch for user-supplied --exclude patterns.
|
|
1382
1474
|
// dot:true so excludes like `**/private/*` match through dotfile dirs
|
|
1383
1475
|
// (`.claude/.../private/x`); without it the exclude silently never fires.
|
|
1384
|
-
|
|
1476
|
+
// These are relative to the scan base; the config's excludes carry their own
|
|
1477
|
+
// (project-root) base on the matcher — see {@link ExcludeMatcher}.
|
|
1478
|
+
const excludeMatchers = [];
|
|
1479
|
+
if (userExcludes.length > 0) {
|
|
1480
|
+
excludeMatchers.push({
|
|
1481
|
+
isMatch: picomatch(userExcludes, { dot: true }),
|
|
1482
|
+
base: resolvedBaseDir,
|
|
1483
|
+
label: 'excluded',
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
if (resolvedScanCtx.projectExcludes !== null) {
|
|
1487
|
+
excludeMatchers.push(resolvedScanCtx.projectExcludes);
|
|
1488
|
+
}
|
|
1385
1489
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
1386
1490
|
// Build full paths and batch-check gitignore status
|
|
1387
1491
|
const entryPaths = entries.map(e => safePath.join(dirPath, e.name));
|
|
1388
1492
|
const gitIgnoredMap = buildGitIgnoreMap(entryPaths, resolvedScanCtx.gitTracker, options.includeArtifacts ?? false);
|
|
1389
1493
|
for (const entry of entries) {
|
|
1390
1494
|
const fullPath = safePath.join(dirPath, entry.name);
|
|
1391
|
-
const skipReason = getSkipReason(entry, fullPath,
|
|
1495
|
+
const skipReason = getSkipReason(entry, fullPath, gitIgnoredMap, excludeMatchers);
|
|
1392
1496
|
if (skipReason !== null) {
|
|
1393
1497
|
logger.debug(`Excluding path: ${skipReason}`);
|
|
1394
1498
|
continue;
|
|
@@ -1441,17 +1545,45 @@ function countFilesByStatus(results) {
|
|
|
1441
1545
|
};
|
|
1442
1546
|
}
|
|
1443
1547
|
/**
|
|
1444
|
-
* Build base summary structure (used by both flat and hierarchical)
|
|
1548
|
+
* Build base summary structure (used by both flat and hierarchical), together
|
|
1549
|
+
* with the entries the document will publish.
|
|
1445
1550
|
*
|
|
1446
1551
|
* `issueCounts` is named exactly as it is on every individual file entry, and
|
|
1447
1552
|
* means the same thing at both levels: findings, by severity. `summary` only ever
|
|
1448
1553
|
* counts files.
|
|
1554
|
+
*
|
|
1555
|
+
* The header total and every per-file total come from ONE traversal of the
|
|
1556
|
+
* final issue set, and the entries carrying those per-file totals are returned
|
|
1557
|
+
* from the same pass. Before that, the header was derived from the issue
|
|
1558
|
+
* records while each file's `issueCounts` was whatever its producer had
|
|
1559
|
+
* published — and a producer that appends findings after publishing (or never
|
|
1560
|
+
* sets the counts at all: `validatePlugin` writes `status` and `summary` but
|
|
1561
|
+
* not `issueCounts`) left the two disagreeing inside one document. A real
|
|
1562
|
+
* `vat audit --user` run said 55/422/504 in the header over per-file counts
|
|
1563
|
+
* summing to 55/360/405, with 91 of 614 entries declaring `{0,0,0}` above the
|
|
1564
|
+
* findings they listed. Re-deriving here means the header and the sum are the
|
|
1565
|
+
* same arithmetic on the same array, so they cannot drift again no matter what
|
|
1566
|
+
* a producer forgets.
|
|
1567
|
+
*
|
|
1568
|
+
* `linkedFiles[].issues` are deliberately NOT added: every linked finding is
|
|
1569
|
+
* also present in its owner's `issues`, so the nested list is a VIEW of records
|
|
1570
|
+
* already counted, not extra records. `audit-report-coherence.integration.test.ts`
|
|
1571
|
+
* pins that subset relation, because counting once is only correct while it holds.
|
|
1449
1572
|
*/
|
|
1450
1573
|
function buildBaseSummary(results, startTime) {
|
|
1574
|
+
const issueCounts = { errors: 0, warnings: 0, info: 0 };
|
|
1575
|
+
const entries = results.map((result) => {
|
|
1576
|
+
const counts = countBySeverity(result.issues);
|
|
1577
|
+
issueCounts.errors += counts.errors;
|
|
1578
|
+
issueCounts.warnings += counts.warnings;
|
|
1579
|
+
issueCounts.info += counts.info;
|
|
1580
|
+
return { ...result, issueCounts: counts };
|
|
1581
|
+
});
|
|
1451
1582
|
return {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1583
|
+
entries,
|
|
1584
|
+
status: calculateOverallStatus(entries),
|
|
1585
|
+
summary: countFilesByStatus(entries),
|
|
1586
|
+
issueCounts,
|
|
1455
1587
|
duration: `${Date.now() - startTime}ms`,
|
|
1456
1588
|
};
|
|
1457
1589
|
}
|
|
@@ -1459,8 +1591,8 @@ function buildBaseSummary(results, startTime) {
|
|
|
1459
1591
|
* Calculate summary for hierarchical output
|
|
1460
1592
|
*/
|
|
1461
1593
|
function calculateHierarchicalSummary(results, hierarchical, startTime, compatMap, verbose, root) {
|
|
1462
|
-
const base = buildBaseSummary(results, startTime);
|
|
1463
|
-
const withCompat = applyCompatMap(
|
|
1594
|
+
const { entries, ...base } = buildBaseSummary(results, startTime);
|
|
1595
|
+
const withCompat = applyCompatMap(entries, compatMap);
|
|
1464
1596
|
return {
|
|
1465
1597
|
root,
|
|
1466
1598
|
...base,
|
|
@@ -1471,7 +1603,7 @@ function calculateHierarchicalSummary(results, hierarchical, startTime, compatMa
|
|
|
1471
1603
|
standalonePlugins: hierarchical.standalonePlugins.length,
|
|
1472
1604
|
standaloneSkills: hierarchical.standaloneSkills.length,
|
|
1473
1605
|
},
|
|
1474
|
-
files:
|
|
1606
|
+
files: relativizePathEntries(applyVerboseFilter(withCompat, verbose), root, NESTED_PATH_CARRIERS),
|
|
1475
1607
|
hierarchical,
|
|
1476
1608
|
};
|
|
1477
1609
|
}
|