eslint-plugin-ai-guard 1.1.14 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +534 -125
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +218 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +208 -31
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -3
package/dist/cli/index.js
CHANGED
|
@@ -382,13 +382,53 @@ var RULE_CATEGORY = {
|
|
|
382
382
|
"ai-guard/no-duplicate-logic-block": "AI Patterns",
|
|
383
383
|
"ai-guard/no-dead-branch": "AI Patterns"
|
|
384
384
|
};
|
|
385
|
+
var CONFIDENCE_TIER = {
|
|
386
|
+
// High confidence — near-zero false positive rate
|
|
387
|
+
"ai-guard/no-hardcoded-secret": "high",
|
|
388
|
+
"ai-guard/no-eval-dynamic": "high",
|
|
389
|
+
"ai-guard/no-floating-promise": "high",
|
|
390
|
+
"ai-guard/no-empty-catch": "high",
|
|
391
|
+
// Medium confidence — context matters, usually valid
|
|
392
|
+
"ai-guard/no-sql-string-concat": "medium",
|
|
393
|
+
"ai-guard/no-await-in-loop": "medium",
|
|
394
|
+
"ai-guard/require-auth-middleware": "medium",
|
|
395
|
+
"ai-guard/require-authz-check": "medium",
|
|
396
|
+
"ai-guard/no-catch-log-rethrow": "medium",
|
|
397
|
+
"ai-guard/no-catch-without-use": "medium",
|
|
398
|
+
"ai-guard/no-unsafe-deserialize": "medium",
|
|
399
|
+
// Low confidence — suggestions, may be intentional
|
|
400
|
+
"ai-guard/no-async-without-await": "low",
|
|
401
|
+
"ai-guard/no-async-array-callback": "low",
|
|
402
|
+
"ai-guard/no-dead-branch": "low",
|
|
403
|
+
"ai-guard/no-broad-exception": "low",
|
|
404
|
+
"ai-guard/no-console-in-handler": "low",
|
|
405
|
+
"ai-guard/no-duplicate-logic-block": "low",
|
|
406
|
+
"ai-guard/no-redundant-await": "low"
|
|
407
|
+
};
|
|
408
|
+
var ECOSYSTEM_FIX_HINTS = {
|
|
409
|
+
"react-hooks/exhaustive-deps": "npm install --save-dev eslint-plugin-react-hooks",
|
|
410
|
+
"react-hooks/rules-of-hooks": "npm install --save-dev eslint-plugin-react-hooks",
|
|
411
|
+
"@typescript-eslint/": "npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser",
|
|
412
|
+
"import/": "npm install --save-dev eslint-plugin-import",
|
|
413
|
+
"jsx-a11y/": "npm install --save-dev eslint-plugin-jsx-a11y",
|
|
414
|
+
"prettier/": "npm install --save-dev eslint-plugin-prettier"
|
|
415
|
+
};
|
|
416
|
+
function getEcosystemFix(ruleId) {
|
|
417
|
+
if (!ruleId) return null;
|
|
418
|
+
for (const [prefix, fix] of Object.entries(ECOSYSTEM_FIX_HINTS)) {
|
|
419
|
+
if (ruleId.startsWith(prefix)) return fix;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
385
423
|
var PREFIX = {
|
|
386
424
|
success: import_chalk.default.green("\u2714"),
|
|
387
425
|
error: import_chalk.default.red("\u2716"),
|
|
388
426
|
warn: import_chalk.default.yellow("\u26A0"),
|
|
389
427
|
info: import_chalk.default.cyan("\u2139"),
|
|
390
428
|
section: import_chalk.default.bold.white,
|
|
391
|
-
bullet: import_chalk.default.gray("\u2022")
|
|
429
|
+
bullet: import_chalk.default.gray("\u2022"),
|
|
430
|
+
ecosystem: import_chalk.default.yellow("\u26A0"),
|
|
431
|
+
parser: import_chalk.default.magenta("\u26A1")
|
|
392
432
|
};
|
|
393
433
|
var log = {
|
|
394
434
|
info(msg) {
|
|
@@ -408,6 +448,14 @@ var log = {
|
|
|
408
448
|
console.log(import_chalk.default.bold.cyan(` \u2500\u2500 ${title} \u2500\u2500`));
|
|
409
449
|
console.log("");
|
|
410
450
|
},
|
|
451
|
+
/**
|
|
452
|
+
* Ecosystem section — distinct styling to communicate it's NOT ai-guard output
|
|
453
|
+
*/
|
|
454
|
+
ecosystemSection(title) {
|
|
455
|
+
console.log("");
|
|
456
|
+
console.log(import_chalk.default.bold.yellow(` \u2500\u2500 ${title} \u2500\u2500`) + import_chalk.default.dim(" (not ai-guard findings)"));
|
|
457
|
+
console.log("");
|
|
458
|
+
},
|
|
411
459
|
rule(ruleName, count) {
|
|
412
460
|
console.log(
|
|
413
461
|
` ${PREFIX.bullet} ${import_chalk.default.yellow(ruleName)} ${import_chalk.default.gray(`(${count} issue${count !== 1 ? "s" : ""})`)}`
|
|
@@ -424,9 +472,36 @@ var log = {
|
|
|
424
472
|
` ${import_chalk.default.gray(`${line}:${col}`).padEnd(12)} ${icon} ${import_chalk.default.white(msg)}`
|
|
425
473
|
);
|
|
426
474
|
},
|
|
475
|
+
/**
|
|
476
|
+
* Print an ESLint ecosystem issue (missing rule definition, foreign plugin rule, etc.)
|
|
477
|
+
* with a clear disclaimer that this is NOT an ai-guard finding.
|
|
478
|
+
*/
|
|
479
|
+
ecosystemIssue(issue, count = 1) {
|
|
480
|
+
const ruleLabel = issue.ruleId ? import_chalk.default.yellow(issue.ruleId) : import_chalk.default.gray("(no rule id)");
|
|
481
|
+
const typeLabel = issue.type === "missing-rule" ? import_chalk.default.dim("[missing rule definition]") : issue.type === "config-error" ? import_chalk.default.dim("[config error]") : import_chalk.default.dim("[foreign rule]");
|
|
482
|
+
const countLabel = count > 1 ? import_chalk.default.gray(` \xD7 ${count}`) : "";
|
|
483
|
+
console.log(` ${PREFIX.ecosystem} ${ruleLabel} ${typeLabel}${countLabel}`);
|
|
484
|
+
console.log(` ${import_chalk.default.gray("File:")} ${import_chalk.default.dim(issue.filePath)}${issue.line > 0 ? import_chalk.default.dim(`:${issue.line}`) : ""}`);
|
|
485
|
+
console.log(` ${import_chalk.default.gray("Msg:")} ${import_chalk.default.dim(issue.message.slice(0, 120))}`);
|
|
486
|
+
const fix = getEcosystemFix(issue.ruleId);
|
|
487
|
+
if (fix) {
|
|
488
|
+
console.log(` ${import_chalk.default.cyan("\u2192 Fix:")} ${import_chalk.default.yellow(fix)}`);
|
|
489
|
+
}
|
|
490
|
+
console.log(
|
|
491
|
+
import_chalk.default.dim(` Note: This comes from your ESLint config, not from ai-guard.`)
|
|
492
|
+
);
|
|
493
|
+
console.log("");
|
|
494
|
+
},
|
|
495
|
+
/**
|
|
496
|
+
* Print a parser/syntax error — clearly NOT an ai-guard finding.
|
|
497
|
+
*/
|
|
498
|
+
parserError(pe) {
|
|
499
|
+
console.log(` ${PREFIX.parser} ${import_chalk.default.magenta("Parse failure")} ${import_chalk.default.dim(pe.filePath)}${pe.line > 0 ? import_chalk.default.dim(`:${pe.line}`) : ""}`);
|
|
500
|
+
console.log(` ${import_chalk.default.dim(pe.message.slice(0, 120))}`);
|
|
501
|
+
console.log("");
|
|
502
|
+
},
|
|
427
503
|
/**
|
|
428
504
|
* Print a category summary line with icon, label, and count.
|
|
429
|
-
* e.g. 🔴 Security 3 errors · 0 warnings
|
|
430
505
|
*/
|
|
431
506
|
category(icon, label, errors, warnings) {
|
|
432
507
|
const parts = [];
|
|
@@ -437,7 +512,7 @@ var log = {
|
|
|
437
512
|
console.log(` ${icon} ${import_chalk.default.bold.white(labelPadded)} ${counts}`);
|
|
438
513
|
},
|
|
439
514
|
/**
|
|
440
|
-
* Print the scan stats line
|
|
515
|
+
* Print the scan stats line.
|
|
441
516
|
*/
|
|
442
517
|
scanStats(filesScanned, filesWithIssues, durationMs, preset) {
|
|
443
518
|
console.log(
|
|
@@ -547,9 +622,27 @@ var DEFAULT_IGNORE_PATTERNS = [
|
|
|
547
622
|
];
|
|
548
623
|
function isSkippablePatternError(error) {
|
|
549
624
|
const msg = error.message.toLowerCase();
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
625
|
+
return msg.includes("no files") || msg.includes("no files matching") || msg.includes("ignored") || msg.includes("all files matched by") || msg.includes("are ignored") || msg.includes("was ignored") || msg.includes("file ignored");
|
|
626
|
+
}
|
|
627
|
+
function classifyMessage(msg) {
|
|
628
|
+
if (msg.fatal === true) return "parser-error";
|
|
629
|
+
if (msg.ruleId === null && (msg.message.startsWith("Parsing error") || msg.message.includes("Unexpected token") || msg.message.includes("SyntaxError"))) {
|
|
630
|
+
return "parser-error";
|
|
631
|
+
}
|
|
632
|
+
if (msg.ruleId && msg.ruleId.startsWith("ai-guard/")) {
|
|
633
|
+
return "ai-guard";
|
|
634
|
+
}
|
|
635
|
+
return "ecosystem";
|
|
636
|
+
}
|
|
637
|
+
function classifyEcosystemType(msg) {
|
|
638
|
+
const lower = msg.message.toLowerCase();
|
|
639
|
+
if (lower.includes("definition for rule") && lower.includes("was not found")) {
|
|
640
|
+
return "missing-rule";
|
|
641
|
+
}
|
|
642
|
+
if (lower.includes("configuration") || lower.includes("config")) {
|
|
643
|
+
return "config-error";
|
|
644
|
+
}
|
|
645
|
+
return "foreign-rule";
|
|
553
646
|
}
|
|
554
647
|
async function loadPluginModuleFromCwd(cwd) {
|
|
555
648
|
const { createRequire: createRequire2 } = await import("module");
|
|
@@ -577,13 +670,22 @@ async function loadPluginModuleFromCwd(cwd) {
|
|
|
577
670
|
);
|
|
578
671
|
}
|
|
579
672
|
async function runEslint(options) {
|
|
673
|
+
const overallStart = Date.now();
|
|
674
|
+
const timing = {
|
|
675
|
+
pluginLoadMs: 0,
|
|
676
|
+
parserLoadMs: 0,
|
|
677
|
+
lintMs: 0,
|
|
678
|
+
processMs: 0
|
|
679
|
+
};
|
|
580
680
|
const { ESLint } = await import("eslint").catch(() => {
|
|
581
681
|
throw new Error(
|
|
582
682
|
"ESLint is not installed. Run: npm install --save-dev eslint"
|
|
583
683
|
);
|
|
584
684
|
});
|
|
685
|
+
const pluginStart = Date.now();
|
|
585
686
|
const rawPlugin = await loadPluginModuleFromCwd(process.cwd());
|
|
586
687
|
const plugin = normalizePlugin(rawPlugin);
|
|
688
|
+
timing.pluginLoadMs = Date.now() - pluginStart;
|
|
587
689
|
const rules = getRules(options.preset);
|
|
588
690
|
const resolvedTargetPath = import_path.default.resolve(options.targetPath);
|
|
589
691
|
if (!import_fs.default.existsSync(resolvedTargetPath)) {
|
|
@@ -592,18 +694,9 @@ async function runEslint(options) {
|
|
|
592
694
|
const targetStat = import_fs.default.statSync(resolvedTargetPath);
|
|
593
695
|
const isSingleFileTarget = targetStat.isFile();
|
|
594
696
|
const eslintCwd = isSingleFileTarget ? import_path.default.dirname(resolvedTargetPath) : resolvedTargetPath;
|
|
595
|
-
const
|
|
596
|
-
const JS_TS_FILES = [
|
|
597
|
-
"**/*.js",
|
|
598
|
-
"**/*.jsx",
|
|
599
|
-
"**/*.ts",
|
|
600
|
-
"**/*.tsx",
|
|
601
|
-
"**/*.mts",
|
|
602
|
-
"**/*.cts",
|
|
603
|
-
"**/*.mjs",
|
|
604
|
-
"**/*.cjs"
|
|
605
|
-
];
|
|
697
|
+
const parserStart = Date.now();
|
|
606
698
|
let tsParser = null;
|
|
699
|
+
let tsParserAvailable = false;
|
|
607
700
|
try {
|
|
608
701
|
try {
|
|
609
702
|
tsParser = require(import_path.default.join(
|
|
@@ -615,10 +708,23 @@ async function runEslint(options) {
|
|
|
615
708
|
} catch {
|
|
616
709
|
tsParser = require("@typescript-eslint/parser");
|
|
617
710
|
}
|
|
711
|
+
tsParserAvailable = true;
|
|
618
712
|
} catch {
|
|
713
|
+
log.debug("@typescript-eslint/parser not found \u2014 TypeScript files will use espree fallback");
|
|
619
714
|
}
|
|
715
|
+
timing.parserLoadMs = Date.now() - parserStart;
|
|
716
|
+
const JS_TS_FILES = [
|
|
717
|
+
"**/*.js",
|
|
718
|
+
"**/*.jsx",
|
|
719
|
+
"**/*.ts",
|
|
720
|
+
"**/*.tsx",
|
|
721
|
+
"**/*.mts",
|
|
722
|
+
"**/*.cts",
|
|
723
|
+
"**/*.mjs",
|
|
724
|
+
"**/*.cjs"
|
|
725
|
+
];
|
|
620
726
|
const configBlocks = [
|
|
621
|
-
// JS/JSX files — default espree parser
|
|
727
|
+
// JS/JSX files — default espree parser with JSX support
|
|
622
728
|
{
|
|
623
729
|
files: ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"],
|
|
624
730
|
plugins: { "ai-guard": plugin },
|
|
@@ -641,9 +747,9 @@ async function runEslint(options) {
|
|
|
641
747
|
languageOptions: {
|
|
642
748
|
parser: tsParser,
|
|
643
749
|
parserOptions: {
|
|
644
|
-
// project: true would be needed for type-aware rules — skip for speed
|
|
645
750
|
ecmaVersion: "latest",
|
|
646
|
-
sourceType: "module"
|
|
751
|
+
sourceType: "module",
|
|
752
|
+
ecmaFeatures: { jsx: true }
|
|
647
753
|
}
|
|
648
754
|
},
|
|
649
755
|
rules
|
|
@@ -652,6 +758,13 @@ async function runEslint(options) {
|
|
|
652
758
|
configBlocks.splice(1, 0, {
|
|
653
759
|
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
|
654
760
|
plugins: { "ai-guard": plugin },
|
|
761
|
+
languageOptions: {
|
|
762
|
+
parserOptions: {
|
|
763
|
+
ecmaVersion: "latest",
|
|
764
|
+
sourceType: "module",
|
|
765
|
+
ecmaFeatures: { jsx: true }
|
|
766
|
+
}
|
|
767
|
+
},
|
|
655
768
|
rules
|
|
656
769
|
});
|
|
657
770
|
}
|
|
@@ -661,50 +774,107 @@ async function runEslint(options) {
|
|
|
661
774
|
overrideConfig: configBlocks
|
|
662
775
|
});
|
|
663
776
|
const patterns = isSingleFileTarget ? [import_path.default.basename(resolvedTargetPath)] : JS_TS_FILES;
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
777
|
+
const lintStart = Date.now();
|
|
778
|
+
let rawResults;
|
|
779
|
+
try {
|
|
780
|
+
rawResults = await eslint.lintFiles(patterns);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
783
|
+
if (isSkippablePatternError(error)) {
|
|
784
|
+
log.debug("Single-call lintFiles failed with no-files, falling back to per-pattern");
|
|
785
|
+
const perPatternResults = await Promise.all(
|
|
786
|
+
patterns.map(async (pattern) => {
|
|
787
|
+
try {
|
|
788
|
+
return await eslint.lintFiles([pattern]);
|
|
789
|
+
} catch (patternErr) {
|
|
790
|
+
const patternError = patternErr instanceof Error ? patternErr : new Error(String(patternErr));
|
|
791
|
+
if (isSkippablePatternError(patternError)) {
|
|
792
|
+
log.debug(`Skipping pattern '${pattern}' (no lintable files)`);
|
|
793
|
+
return [];
|
|
794
|
+
}
|
|
795
|
+
throw patternError;
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
);
|
|
799
|
+
rawResults = perPatternResults.flat();
|
|
800
|
+
} else {
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
timing.lintMs = Date.now() - lintStart;
|
|
805
|
+
const processStart = Date.now();
|
|
680
806
|
const files = [];
|
|
807
|
+
const ecosystemIssues = [];
|
|
808
|
+
const parserErrors = [];
|
|
681
809
|
const ruleBreakdown = /* @__PURE__ */ new Map();
|
|
682
810
|
let totalErrors = 0;
|
|
683
811
|
let totalWarnings = 0;
|
|
684
812
|
const filesScanned = rawResults.length;
|
|
685
813
|
for (const result of rawResults) {
|
|
686
814
|
if (result.messages.length === 0) continue;
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
815
|
+
const relPath = import_path.default.relative(process.cwd(), result.filePath);
|
|
816
|
+
const aiGuardIssues = [];
|
|
817
|
+
let fileErrors = 0;
|
|
818
|
+
let fileWarnings = 0;
|
|
819
|
+
for (const m of result.messages) {
|
|
820
|
+
const source = classifyMessage(m);
|
|
821
|
+
if (source === "parser-error") {
|
|
822
|
+
parserErrors.push({
|
|
823
|
+
filePath: relPath,
|
|
824
|
+
message: m.message,
|
|
825
|
+
line: m.line ?? 0
|
|
826
|
+
});
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (source === "ecosystem") {
|
|
830
|
+
ecosystemIssues.push({
|
|
831
|
+
type: classifyEcosystemType(m),
|
|
832
|
+
ruleId: m.ruleId,
|
|
833
|
+
message: m.message,
|
|
834
|
+
filePath: relPath,
|
|
835
|
+
line: m.line ?? 0,
|
|
836
|
+
column: m.column ?? 0
|
|
837
|
+
});
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
const issue = {
|
|
841
|
+
ruleId: m.ruleId ?? "unknown",
|
|
842
|
+
severity: m.severity,
|
|
843
|
+
message: m.message,
|
|
844
|
+
line: m.line,
|
|
845
|
+
column: m.column
|
|
846
|
+
};
|
|
847
|
+
aiGuardIssues.push(issue);
|
|
848
|
+
if (m.severity === 2) {
|
|
849
|
+
fileErrors++;
|
|
850
|
+
totalErrors++;
|
|
851
|
+
} else {
|
|
852
|
+
fileWarnings++;
|
|
853
|
+
totalWarnings++;
|
|
854
|
+
}
|
|
699
855
|
ruleBreakdown.set(
|
|
700
856
|
issue.ruleId,
|
|
701
857
|
(ruleBreakdown.get(issue.ruleId) ?? 0) + 1
|
|
702
858
|
);
|
|
703
859
|
}
|
|
704
|
-
|
|
705
|
-
|
|
860
|
+
if (aiGuardIssues.length > 0) {
|
|
861
|
+
files.push({
|
|
862
|
+
filePath: relPath,
|
|
863
|
+
issues: aiGuardIssues,
|
|
864
|
+
errorCount: fileErrors,
|
|
865
|
+
warningCount: fileWarnings
|
|
866
|
+
});
|
|
867
|
+
}
|
|
706
868
|
}
|
|
707
869
|
const topFiles = [...files].sort((a, b) => b.issues.length - a.issues.length).slice(0, 10).map((f) => ({ path: f.filePath, count: f.issues.length }));
|
|
870
|
+
timing.processMs = Date.now() - processStart;
|
|
871
|
+
if (options.debugTiming) {
|
|
872
|
+
log.debug(`Plugin load: ${timing.pluginLoadMs}ms`);
|
|
873
|
+
log.debug(`Parser load: ${timing.parserLoadMs}ms`);
|
|
874
|
+
log.debug(`Lint: ${timing.lintMs}ms`);
|
|
875
|
+
log.debug(`Process: ${timing.processMs}ms`);
|
|
876
|
+
log.debug(`Total: ${Date.now() - overallStart}ms`);
|
|
877
|
+
}
|
|
708
878
|
return {
|
|
709
879
|
files,
|
|
710
880
|
totalErrors,
|
|
@@ -713,7 +883,11 @@ async function runEslint(options) {
|
|
|
713
883
|
filesScanned,
|
|
714
884
|
ruleBreakdown,
|
|
715
885
|
topFiles,
|
|
716
|
-
durationMs
|
|
886
|
+
durationMs: Date.now() - overallStart,
|
|
887
|
+
timing,
|
|
888
|
+
ecosystemIssues,
|
|
889
|
+
parserErrors,
|
|
890
|
+
tsParserAvailable
|
|
717
891
|
};
|
|
718
892
|
}
|
|
719
893
|
|
|
@@ -723,7 +897,7 @@ function registerRunCommand(program2) {
|
|
|
723
897
|
"--max-warnings <n>",
|
|
724
898
|
"Fail with exit code 1 if warnings exceed this count",
|
|
725
899
|
(value) => Number.parseInt(value, 10)
|
|
726
|
-
).action(async (opts) => {
|
|
900
|
+
).option("--verbose", "Show all issues \u2014 disable grouping of repeated warnings").option("--debug-timing", "Print per-phase timing diagnostics").action(async (opts) => {
|
|
727
901
|
if (opts.maxWarnings !== void 0 && (!Number.isInteger(opts.maxWarnings) || opts.maxWarnings < 0)) {
|
|
728
902
|
log.blank();
|
|
729
903
|
log.error("--max-warnings must be a non-negative integer.");
|
|
@@ -743,7 +917,11 @@ function registerRunCommand(program2) {
|
|
|
743
917
|
const spinner = opts.json ? null : (0, import_ora.default)({ text: "Scanning\u2026", color: "cyan" }).start();
|
|
744
918
|
let result;
|
|
745
919
|
try {
|
|
746
|
-
result = await runEslint({
|
|
920
|
+
result = await runEslint({
|
|
921
|
+
preset,
|
|
922
|
+
targetPath: opts.path,
|
|
923
|
+
debugTiming: opts.debugTiming
|
|
924
|
+
});
|
|
747
925
|
spinner?.stop();
|
|
748
926
|
} catch (err) {
|
|
749
927
|
spinner?.stop();
|
|
@@ -775,17 +953,26 @@ function registerRunCommand(program2) {
|
|
|
775
953
|
totalWarnings: result.totalWarnings,
|
|
776
954
|
totalIssues: result.totalIssues,
|
|
777
955
|
durationMs: result.durationMs,
|
|
956
|
+
timing: result.timing,
|
|
778
957
|
ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
|
|
779
958
|
topFiles: result.topFiles,
|
|
780
|
-
files: result.files
|
|
959
|
+
files: result.files,
|
|
960
|
+
ecosystemIssues: result.ecosystemIssues,
|
|
961
|
+
parserErrors: result.parserErrors,
|
|
962
|
+
tsParserAvailable: result.tsParserAvailable
|
|
781
963
|
};
|
|
782
964
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
783
965
|
process.exit(getRunExitCode(result, opts.maxWarnings));
|
|
784
966
|
return;
|
|
785
967
|
}
|
|
786
968
|
log.scanStats(result.filesScanned, result.files.length, result.durationMs, preset);
|
|
969
|
+
if (!result.tsParserAvailable) {
|
|
970
|
+
log.print(
|
|
971
|
+
import_chalk2.default.dim(` \u2139 TypeScript parser not found \u2014 .ts/.tsx files use espree fallback`) + import_chalk2.default.dim(` (install @typescript-eslint/parser for full TS support)`)
|
|
972
|
+
);
|
|
973
|
+
}
|
|
787
974
|
log.blank();
|
|
788
|
-
if (result.totalIssues === 0) {
|
|
975
|
+
if (result.totalIssues === 0 && result.ecosystemIssues.length === 0 && result.parserErrors.length === 0) {
|
|
789
976
|
log.print(
|
|
790
977
|
` ${import_chalk2.default.green("\u2714")} ${import_chalk2.default.bold.green("No AI issues found")} ${import_chalk2.default.gray(`\u2014 ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""} scanned, all clean`)}`
|
|
791
978
|
);
|
|
@@ -793,80 +980,117 @@ function registerRunCommand(program2) {
|
|
|
793
980
|
process.exit(0);
|
|
794
981
|
return;
|
|
795
982
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
983
|
+
if (result.totalIssues === 0) {
|
|
984
|
+
log.print(
|
|
985
|
+
` ${import_chalk2.default.green("\u2714")} ${import_chalk2.default.bold.green("No AI issues found")} ${import_chalk2.default.gray(`\u2014 ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""} scanned, all clean`)}`
|
|
986
|
+
);
|
|
987
|
+
log.blank();
|
|
988
|
+
}
|
|
989
|
+
if (result.totalIssues > 0) {
|
|
990
|
+
log.section("Summary by Category");
|
|
991
|
+
const categoryErrors = {};
|
|
992
|
+
const categoryWarnings = {};
|
|
993
|
+
for (const file of result.files) {
|
|
994
|
+
for (const issue of file.issues) {
|
|
995
|
+
const cat = RULE_CATEGORY[issue.ruleId] ?? "Other";
|
|
996
|
+
if (issue.severity === 2) {
|
|
997
|
+
categoryErrors[cat] = (categoryErrors[cat] ?? 0) + 1;
|
|
998
|
+
} else {
|
|
999
|
+
categoryWarnings[cat] = (categoryWarnings[cat] ?? 0) + 1;
|
|
1000
|
+
}
|
|
806
1001
|
}
|
|
807
1002
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
(cat) => (categoryErrors[cat] ?? 0) + (categoryWarnings[cat] ?? 0) > 0
|
|
816
|
-
);
|
|
817
|
-
sortedCategories.sort((a, b) => {
|
|
818
|
-
const ai = CATEGORY_ORDER.indexOf(a);
|
|
819
|
-
const bi = CATEGORY_ORDER.indexOf(b);
|
|
820
|
-
if (ai !== -1 && bi !== -1) return ai - bi;
|
|
821
|
-
if (ai !== -1) return -1;
|
|
822
|
-
if (bi !== -1) return 1;
|
|
823
|
-
return a.localeCompare(b);
|
|
824
|
-
});
|
|
825
|
-
for (const cat of sortedCategories) {
|
|
826
|
-
const icon = CATEGORY_ICONS[cat] ?? "\u26AA";
|
|
827
|
-
log.category(icon, cat, categoryErrors[cat] ?? 0, categoryWarnings[cat] ?? 0);
|
|
828
|
-
}
|
|
829
|
-
log.blank();
|
|
830
|
-
log.print(
|
|
831
|
-
` ${import_chalk2.default.bold("Total:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`
|
|
832
|
-
);
|
|
833
|
-
log.blank();
|
|
834
|
-
if (result.ruleBreakdown.size > 0) {
|
|
835
|
-
log.section("By Rule");
|
|
836
|
-
const sorted = [...result.ruleBreakdown.entries()].sort(
|
|
837
|
-
(a, b) => b[1] - a[1]
|
|
1003
|
+
const allCategories = /* @__PURE__ */ new Set([
|
|
1004
|
+
...CATEGORY_ORDER,
|
|
1005
|
+
...Object.keys(categoryErrors),
|
|
1006
|
+
...Object.keys(categoryWarnings)
|
|
1007
|
+
]);
|
|
1008
|
+
const sortedCategories = [...allCategories].filter(
|
|
1009
|
+
(cat) => (categoryErrors[cat] ?? 0) + (categoryWarnings[cat] ?? 0) > 0
|
|
838
1010
|
);
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
1011
|
+
sortedCategories.sort((a, b) => {
|
|
1012
|
+
const ai = CATEGORY_ORDER.indexOf(a);
|
|
1013
|
+
const bi = CATEGORY_ORDER.indexOf(b);
|
|
1014
|
+
if (ai !== -1 && bi !== -1) return ai - bi;
|
|
1015
|
+
if (ai !== -1) return -1;
|
|
1016
|
+
if (bi !== -1) return 1;
|
|
1017
|
+
return a.localeCompare(b);
|
|
1018
|
+
});
|
|
1019
|
+
for (const cat of sortedCategories) {
|
|
1020
|
+
const icon = CATEGORY_ICONS[cat] ?? "\u26AA";
|
|
1021
|
+
log.category(icon, cat, categoryErrors[cat] ?? 0, categoryWarnings[cat] ?? 0);
|
|
1022
|
+
}
|
|
1023
|
+
log.blank();
|
|
1024
|
+
log.print(
|
|
1025
|
+
` ${import_chalk2.default.bold("Total:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`
|
|
1026
|
+
);
|
|
1027
|
+
log.blank();
|
|
1028
|
+
if (result.ruleBreakdown.size > 0) {
|
|
1029
|
+
log.section("By Rule");
|
|
1030
|
+
const sorted = [...result.ruleBreakdown.entries()].sort(
|
|
1031
|
+
(a, b) => b[1] - a[1]
|
|
843
1032
|
);
|
|
1033
|
+
for (const [rule, count] of sorted) {
|
|
1034
|
+
const shortRule = rule.replace(/^ai-guard\//, "");
|
|
1035
|
+
const tier = CONFIDENCE_TIER[rule];
|
|
1036
|
+
const tierLabel = tier === "high" ? import_chalk2.default.red.dim("[high]") : tier === "medium" ? import_chalk2.default.yellow.dim("[medium]") : import_chalk2.default.gray.dim("[low]");
|
|
1037
|
+
log.print(
|
|
1038
|
+
` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.yellow(shortRule)} ${tierLabel}${import_chalk2.default.gray(":")} ${import_chalk2.default.white(String(count))}`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
log.blank();
|
|
1042
|
+
}
|
|
1043
|
+
if (result.topFiles.length > 0) {
|
|
1044
|
+
log.section("Top Files");
|
|
1045
|
+
for (const { path: fp, count } of result.topFiles) {
|
|
1046
|
+
log.print(
|
|
1047
|
+
` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.white(fp)} ${import_chalk2.default.gray(`(${count})`)}`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
log.blank();
|
|
844
1051
|
}
|
|
1052
|
+
log.section("Issues by File");
|
|
845
1053
|
log.blank();
|
|
1054
|
+
const verbose = opts.verbose ?? false;
|
|
1055
|
+
renderIssuesByFile(result, verbose);
|
|
846
1056
|
}
|
|
847
|
-
if (result.
|
|
848
|
-
log.
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1057
|
+
if (result.ecosystemIssues.length > 0) {
|
|
1058
|
+
log.ecosystemSection("ESLint Config Issues");
|
|
1059
|
+
log.print(
|
|
1060
|
+
import_chalk2.default.dim(` These issues come from your project's ESLint config, not from ai-guard.`)
|
|
1061
|
+
);
|
|
1062
|
+
log.blank();
|
|
1063
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1064
|
+
for (const issue of result.ecosystemIssues) {
|
|
1065
|
+
const key = `${issue.ruleId ?? "null"}::${issue.message}`;
|
|
1066
|
+
const existing = seen.get(key);
|
|
1067
|
+
if (existing) {
|
|
1068
|
+
existing.count++;
|
|
1069
|
+
} else {
|
|
1070
|
+
seen.set(key, { issue, count: 1 });
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
for (const { issue, count } of seen.values()) {
|
|
1074
|
+
log.ecosystemIssue(issue, count);
|
|
853
1075
|
}
|
|
854
1076
|
log.blank();
|
|
855
1077
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
for (const file of result.files) {
|
|
1078
|
+
if (result.parserErrors.length > 0) {
|
|
1079
|
+
log.ecosystemSection("Parser Errors");
|
|
859
1080
|
log.print(
|
|
860
|
-
|
|
861
|
-
`(${file.errorCount} error${file.errorCount !== 1 ? "s" : ""}, ${file.warningCount} warning${file.warningCount !== 1 ? "s" : ""})`
|
|
862
|
-
)
|
|
1081
|
+
import_chalk2.default.dim(` These files could not be parsed \u2014 not ai-guard findings.`)
|
|
863
1082
|
);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1083
|
+
log.blank();
|
|
1084
|
+
for (const pe of result.parserErrors) {
|
|
1085
|
+
log.parserError(pe);
|
|
1086
|
+
}
|
|
1087
|
+
if (!result.tsParserAvailable) {
|
|
1088
|
+
log.blank();
|
|
868
1089
|
log.print(
|
|
869
|
-
`
|
|
1090
|
+
` ${import_chalk2.default.cyan("\u2192 Fix:")} ${import_chalk2.default.yellow("npm install --save-dev @typescript-eslint/parser")}`
|
|
1091
|
+
);
|
|
1092
|
+
log.print(
|
|
1093
|
+
import_chalk2.default.dim(` TypeScript parser resolves most .ts/.tsx parse failures.`)
|
|
870
1094
|
);
|
|
871
1095
|
}
|
|
872
1096
|
log.blank();
|
|
@@ -874,13 +1098,83 @@ function registerRunCommand(program2) {
|
|
|
874
1098
|
log.divider();
|
|
875
1099
|
log.blank();
|
|
876
1100
|
log.section("Next Steps");
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1101
|
+
if (result.totalIssues > 0) {
|
|
1102
|
+
log.info(`Run ${import_chalk2.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
|
|
1103
|
+
log.info(`Run ${import_chalk2.default.cyan("ai-guard report")} to generate a shareable HTML report`);
|
|
1104
|
+
log.info(`Run ${import_chalk2.default.cyan("ai-guard ignore")} to suppress dist/build noise`);
|
|
1105
|
+
}
|
|
1106
|
+
if (result.ecosystemIssues.length > 0) {
|
|
1107
|
+
log.info(`Run ${import_chalk2.default.cyan("ai-guard doctor")} to diagnose ESLint config issues`);
|
|
1108
|
+
}
|
|
1109
|
+
if (!opts.verbose && hasGroupedWarnings(result)) {
|
|
1110
|
+
log.info(`Run with ${import_chalk2.default.cyan("--verbose")} to expand grouped warnings`);
|
|
1111
|
+
}
|
|
880
1112
|
log.blank();
|
|
881
1113
|
process.exit(getRunExitCode(result, opts.maxWarnings));
|
|
882
1114
|
});
|
|
883
1115
|
}
|
|
1116
|
+
var GROUP_THRESHOLD = 4;
|
|
1117
|
+
function hasGroupedWarnings(result) {
|
|
1118
|
+
for (const file of result.files) {
|
|
1119
|
+
const ruleCounts = /* @__PURE__ */ new Map();
|
|
1120
|
+
for (const issue of file.issues) {
|
|
1121
|
+
ruleCounts.set(issue.ruleId, (ruleCounts.get(issue.ruleId) ?? 0) + 1);
|
|
1122
|
+
}
|
|
1123
|
+
for (const count of ruleCounts.values()) {
|
|
1124
|
+
if (count >= GROUP_THRESHOLD) return true;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
function renderIssuesByFile(result, verbose) {
|
|
1130
|
+
for (const file of result.files) {
|
|
1131
|
+
log.print(
|
|
1132
|
+
` ${import_chalk2.default.bold.white(file.filePath)} ` + import_chalk2.default.gray(
|
|
1133
|
+
`(${file.errorCount} error${file.errorCount !== 1 ? "s" : ""}, ${file.warningCount} warning${file.warningCount !== 1 ? "s" : ""})`
|
|
1134
|
+
)
|
|
1135
|
+
);
|
|
1136
|
+
if (!verbose) {
|
|
1137
|
+
const ruleCounts = /* @__PURE__ */ new Map();
|
|
1138
|
+
for (const issue of file.issues) {
|
|
1139
|
+
const arr = ruleCounts.get(issue.ruleId) ?? [];
|
|
1140
|
+
arr.push(issue);
|
|
1141
|
+
ruleCounts.set(issue.ruleId, arr);
|
|
1142
|
+
}
|
|
1143
|
+
for (const issues of ruleCounts.values()) {
|
|
1144
|
+
const tier = CONFIDENCE_TIER[issues[0].ruleId] ?? "low";
|
|
1145
|
+
if (tier === "low" && issues.length >= GROUP_THRESHOLD) {
|
|
1146
|
+
const sev = issues[0].severity === 2 ? import_chalk2.default.red("error") : import_chalk2.default.yellow(" warn");
|
|
1147
|
+
const shortRule = import_chalk2.default.gray(issues[0].ruleId.replace(/^ai-guard\//, ""));
|
|
1148
|
+
log.print(
|
|
1149
|
+
` ${sev} ${import_chalk2.default.white(issues[0].message)} ${shortRule}`
|
|
1150
|
+
);
|
|
1151
|
+
log.print(
|
|
1152
|
+
import_chalk2.default.dim(` \u21B3 ${issues.length} occurrences \u2014 run with --verbose to expand`)
|
|
1153
|
+
);
|
|
1154
|
+
} else {
|
|
1155
|
+
for (const issue of issues) {
|
|
1156
|
+
renderSingleIssue(issue);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
} else {
|
|
1161
|
+
for (const issue of file.issues) {
|
|
1162
|
+
renderSingleIssue(issue);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
log.blank();
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function renderSingleIssue(issue) {
|
|
1169
|
+
const sev = issue.severity === 2 ? import_chalk2.default.red("error") : import_chalk2.default.yellow(" warn");
|
|
1170
|
+
const loc = import_chalk2.default.gray(`${String(issue.line)}:${String(issue.column)}`).padEnd(12);
|
|
1171
|
+
const ruleShort = import_chalk2.default.gray(issue.ruleId.replace(/^ai-guard\//, ""));
|
|
1172
|
+
const tier = CONFIDENCE_TIER[issue.ruleId];
|
|
1173
|
+
const tierTag = tier === "high" ? "" : tier === "medium" ? "" : import_chalk2.default.dim(" [low]");
|
|
1174
|
+
log.print(
|
|
1175
|
+
` ${loc} ${sev} ${import_chalk2.default.white(issue.message)}${tierTag} ${ruleShort}`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
884
1178
|
function getRunExitCode(result, maxWarnings) {
|
|
885
1179
|
if (result.totalErrors > 0) return 1;
|
|
886
1180
|
if (maxWarnings !== void 0 && result.totalWarnings > maxWarnings) return 1;
|
|
@@ -1315,6 +1609,7 @@ function registerInitCommand(program2) {
|
|
|
1315
1609
|
var import_chalk4 = __toESM(require("chalk"));
|
|
1316
1610
|
var import_path5 = __toESM(require("path"));
|
|
1317
1611
|
init_config_manager();
|
|
1612
|
+
var import_meta = {};
|
|
1318
1613
|
async function probeConfigLoad(configPath, cwd) {
|
|
1319
1614
|
try {
|
|
1320
1615
|
const { ESLint } = await import("eslint");
|
|
@@ -1425,6 +1720,73 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1425
1720
|
note: probe.ok ? void 0 : "Run: ai-guard init --dry-run to preview a fresh config"
|
|
1426
1721
|
});
|
|
1427
1722
|
}
|
|
1723
|
+
{
|
|
1724
|
+
let utilsOk = false;
|
|
1725
|
+
let utilsVersion = null;
|
|
1726
|
+
try {
|
|
1727
|
+
const { createRequire: createRequire2 } = await import("module");
|
|
1728
|
+
const req = createRequire2(import_path5.default.join(cwd, "package.json"));
|
|
1729
|
+
const pkgPath = req.resolve("@typescript-eslint/utils/package.json");
|
|
1730
|
+
const pkg = JSON.parse(
|
|
1731
|
+
(await import("fs")).readFileSync(pkgPath, "utf-8")
|
|
1732
|
+
);
|
|
1733
|
+
utilsVersion = pkg.version ?? null;
|
|
1734
|
+
utilsOk = true;
|
|
1735
|
+
} catch {
|
|
1736
|
+
}
|
|
1737
|
+
if (!utilsOk) {
|
|
1738
|
+
try {
|
|
1739
|
+
const selfDir = import_path5.default.dirname(
|
|
1740
|
+
new URL(import_meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")
|
|
1741
|
+
);
|
|
1742
|
+
const pkgPath = import_path5.default.join(selfDir, "../../node_modules/@typescript-eslint/utils/package.json");
|
|
1743
|
+
if ((await import("fs")).existsSync(pkgPath)) {
|
|
1744
|
+
utilsOk = true;
|
|
1745
|
+
}
|
|
1746
|
+
} catch {
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
checks.push({
|
|
1750
|
+
label: "@typescript-eslint/utils available at runtime",
|
|
1751
|
+
pass: utilsOk,
|
|
1752
|
+
detail: utilsOk ? `Found${utilsVersion ? ` v${utilsVersion}` : ""}` : "Not found \u2014 rule engine may crash on fresh installs",
|
|
1753
|
+
fix: utilsOk ? void 0 : "npm install @typescript-eslint/utils",
|
|
1754
|
+
note: utilsOk ? void 0 : "This is a runtime dependency of eslint-plugin-ai-guard rules."
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
{
|
|
1758
|
+
let parserOk = false;
|
|
1759
|
+
let parserVersion = null;
|
|
1760
|
+
try {
|
|
1761
|
+
const { createRequire: createRequire2 } = await import("module");
|
|
1762
|
+
const req = createRequire2(import_path5.default.join(cwd, "package.json"));
|
|
1763
|
+
const pkgPath = req.resolve("@typescript-eslint/parser/package.json");
|
|
1764
|
+
const pkg = JSON.parse(
|
|
1765
|
+
(await import("fs")).readFileSync(pkgPath, "utf-8")
|
|
1766
|
+
);
|
|
1767
|
+
parserVersion = pkg.version ?? null;
|
|
1768
|
+
parserOk = true;
|
|
1769
|
+
} catch {
|
|
1770
|
+
}
|
|
1771
|
+
const hasTsFiles = (() => {
|
|
1772
|
+
try {
|
|
1773
|
+
const { readdirSync } = require("fs");
|
|
1774
|
+
const files = readdirSync(cwd);
|
|
1775
|
+
return files.some((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
1776
|
+
} catch {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
})();
|
|
1780
|
+
if (!parserOk && hasTsFiles) {
|
|
1781
|
+
checks.push({
|
|
1782
|
+
label: "@typescript-eslint/parser installed (TypeScript project)",
|
|
1783
|
+
pass: false,
|
|
1784
|
+
detail: ".ts/.tsx files detected but TypeScript parser not found",
|
|
1785
|
+
fix: "npm install --save-dev @typescript-eslint/parser",
|
|
1786
|
+
note: "TypeScript files will use espree fallback without this \u2014 some rules may not work correctly."
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1428
1790
|
log.section("Diagnostics");
|
|
1429
1791
|
let allPassed = true;
|
|
1430
1792
|
for (const check of checks) {
|
|
@@ -2419,6 +2781,37 @@ function buildReportData(result, preset, scannedPath, cwd) {
|
|
|
2419
2781
|
example
|
|
2420
2782
|
};
|
|
2421
2783
|
});
|
|
2784
|
+
const CONFIDENCE_ASSIGNMENT = {
|
|
2785
|
+
"ai-guard/no-hardcoded-secret": "high",
|
|
2786
|
+
"ai-guard/no-eval-dynamic": "high",
|
|
2787
|
+
"ai-guard/no-floating-promise": "high",
|
|
2788
|
+
"ai-guard/no-empty-catch": "high",
|
|
2789
|
+
"ai-guard/no-sql-string-concat": "medium",
|
|
2790
|
+
"ai-guard/no-await-in-loop": "medium",
|
|
2791
|
+
"ai-guard/require-auth-middleware": "medium",
|
|
2792
|
+
"ai-guard/require-authz-check": "medium",
|
|
2793
|
+
"ai-guard/no-catch-log-rethrow": "medium",
|
|
2794
|
+
"ai-guard/no-catch-without-use": "medium",
|
|
2795
|
+
"ai-guard/no-unsafe-deserialize": "medium",
|
|
2796
|
+
"ai-guard/no-async-without-await": "low",
|
|
2797
|
+
"ai-guard/no-async-array-callback": "low",
|
|
2798
|
+
"ai-guard/no-dead-branch": "low",
|
|
2799
|
+
"ai-guard/no-broad-exception": "low",
|
|
2800
|
+
"ai-guard/no-console-in-handler": "low",
|
|
2801
|
+
"ai-guard/no-duplicate-logic-block": "low",
|
|
2802
|
+
"ai-guard/no-redundant-await": "low"
|
|
2803
|
+
};
|
|
2804
|
+
let highCount = 0;
|
|
2805
|
+
let mediumCount = 0;
|
|
2806
|
+
let lowCount = 0;
|
|
2807
|
+
for (const [rule, count] of result.ruleBreakdown.entries()) {
|
|
2808
|
+
const tier = CONFIDENCE_ASSIGNMENT[rule] ?? "low";
|
|
2809
|
+
if (tier === "high") highCount += count;
|
|
2810
|
+
else if (tier === "medium") mediumCount += count;
|
|
2811
|
+
else lowCount += count;
|
|
2812
|
+
}
|
|
2813
|
+
const ecosystemCount = result.ecosystemIssues?.length ?? 0;
|
|
2814
|
+
const parserErrorCount = result.parserErrors?.length ?? 0;
|
|
2422
2815
|
return {
|
|
2423
2816
|
projectName,
|
|
2424
2817
|
version,
|
|
@@ -2433,7 +2826,12 @@ function buildReportData(result, preset, scannedPath, cwd) {
|
|
|
2433
2826
|
ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
|
|
2434
2827
|
topFiles,
|
|
2435
2828
|
topRules,
|
|
2436
|
-
categoryBreakdown
|
|
2829
|
+
categoryBreakdown,
|
|
2830
|
+
highCount,
|
|
2831
|
+
mediumCount,
|
|
2832
|
+
lowCount,
|
|
2833
|
+
ecosystemCount,
|
|
2834
|
+
parserErrorCount
|
|
2437
2835
|
};
|
|
2438
2836
|
}
|
|
2439
2837
|
function generateHtml(data) {
|
|
@@ -2533,12 +2931,23 @@ function generateHtml(data) {
|
|
|
2533
2931
|
<p style="color:#6b7280;font-size:0.85rem;margin-top:0.25rem;">Scanned: <code style="color:#94a3b8;">${escHtml(data.scannedPath)}</code> \xB7 Preset: <code style="color:#94a3b8;">${escHtml(data.preset)}</code> \xB7 ${data.durationMs}ms</p>
|
|
2534
2932
|
</div>
|
|
2535
2933
|
<div style="text-align:center;">
|
|
2536
|
-
<div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.
|
|
2537
|
-
<div style="
|
|
2538
|
-
<div style="
|
|
2539
|
-
<span style="
|
|
2540
|
-
<span style="font-size:0.
|
|
2934
|
+
<div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.75rem;letter-spacing:0.05em;">SIGNAL CONFIDENCE</div>
|
|
2935
|
+
<div style="display:flex;flex-direction:column;gap:0.4rem;">
|
|
2936
|
+
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
2937
|
+
<span style="width:8px;height:8px;border-radius:50%;background:#ef4444;display:inline-block;"></span>
|
|
2938
|
+
<span style="font-size:0.8rem;color:#f87171;font-weight:600;">${data.highCount} high-confidence</span>
|
|
2939
|
+
</div>
|
|
2940
|
+
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
2941
|
+
<span style="width:8px;height:8px;border-radius:50%;background:#f59e0b;display:inline-block;"></span>
|
|
2942
|
+
<span style="font-size:0.8rem;color:#fbbf24;font-weight:600;">${data.mediumCount} medium-confidence</span>
|
|
2943
|
+
</div>
|
|
2944
|
+
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
2945
|
+
<span style="width:8px;height:8px;border-radius:50%;background:#6b7280;display:inline-block;"></span>
|
|
2946
|
+
<span style="font-size:0.8rem;color:#9ca3af;font-weight:600;">${data.lowCount} suggestions</span>
|
|
2541
2947
|
</div>
|
|
2948
|
+
${data.ecosystemCount > 0 ? `<div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.25rem;">
|
|
2949
|
+
<span style="font-size:0.75rem;color:#4b5563;">\u26A0 ${data.ecosystemCount} config issue${data.ecosystemCount !== 1 ? "s" : ""} (not ai-guard)</span>
|
|
2950
|
+
</div>` : ""}
|
|
2542
2951
|
</div>
|
|
2543
2952
|
</div>
|
|
2544
2953
|
</div>
|