@cleartrip/frontguard 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +585 -245
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +31 -3
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import g, { stdin, stdout, cwd } from 'process';
|
|
|
4
4
|
import f from 'readline';
|
|
5
5
|
import * as tty from 'tty';
|
|
6
6
|
import { WriteStream } from 'tty';
|
|
7
|
-
import
|
|
7
|
+
import path5, { sep, normalize, delimiter, resolve, dirname } from 'path';
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
10
|
import { execFileSync, spawn } from 'child_process';
|
|
@@ -2399,7 +2399,7 @@ async function runMain(cmd, opts = {}) {
|
|
|
2399
2399
|
}
|
|
2400
2400
|
}
|
|
2401
2401
|
function packageRoot() {
|
|
2402
|
-
return
|
|
2402
|
+
return path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "..");
|
|
2403
2403
|
}
|
|
2404
2404
|
var CONFIG = `import { defineConfig } from '@cleartrip/frontguard'
|
|
2405
2405
|
|
|
@@ -2417,7 +2417,14 @@ export default defineConfig({
|
|
|
2417
2417
|
// },
|
|
2418
2418
|
|
|
2419
2419
|
// checks: {
|
|
2420
|
-
// bundle: { baselineRef: 'main', maxDeltaBytes: 50_000 },
|
|
2420
|
+
// bundle: { baselineRef: 'main', maxDeltaBytes: 50_000 },
|
|
2421
|
+
// coreWebVitals: { scanGlobs: ['src/**/*.{tsx,jsx}'] },
|
|
2422
|
+
// prSize: {
|
|
2423
|
+
// tiers: [
|
|
2424
|
+
// { minLines: 1000, severity: 'warn', message: 'Very large PR (\${lines} lines; \u2265 \${min})' },
|
|
2425
|
+
// { minLines: 500, severity: 'info', message: 'Consider splitting (\${lines} lines)' },
|
|
2426
|
+
// ],
|
|
2427
|
+
// },
|
|
2421
2428
|
// cycles: { enabled: true },
|
|
2422
2429
|
// deadCode: { enabled: true, gate: 'info' },
|
|
2423
2430
|
// // LLM: cloud keys in CI, or local Ollama (no API key) on dev/self-hosted runners:
|
|
@@ -2457,8 +2464,8 @@ If **Yes**, list tools and what they touched (helps reviewers run a stricter fir
|
|
|
2457
2464
|
`;
|
|
2458
2465
|
async function initFrontGuard(cwd) {
|
|
2459
2466
|
const root = packageRoot();
|
|
2460
|
-
const tplPath =
|
|
2461
|
-
const outPipeline =
|
|
2467
|
+
const tplPath = path5.join(root, "templates", "bitbucket-pipelines.yml");
|
|
2468
|
+
const outPipeline = path5.join(cwd, "bitbucket-pipelines.frontguard.example.yml");
|
|
2462
2469
|
try {
|
|
2463
2470
|
await fs.access(outPipeline);
|
|
2464
2471
|
} catch {
|
|
@@ -2473,13 +2480,13 @@ async function initFrontGuard(cwd) {
|
|
|
2473
2480
|
);
|
|
2474
2481
|
}
|
|
2475
2482
|
}
|
|
2476
|
-
const cfgPath =
|
|
2483
|
+
const cfgPath = path5.join(cwd, "frontguard.config.js");
|
|
2477
2484
|
try {
|
|
2478
2485
|
await fs.access(cfgPath);
|
|
2479
2486
|
} catch {
|
|
2480
2487
|
await fs.writeFile(cfgPath, CONFIG, "utf8");
|
|
2481
2488
|
}
|
|
2482
|
-
const tplPr =
|
|
2489
|
+
const tplPr = path5.join(cwd, "pull_request_template.md");
|
|
2483
2490
|
try {
|
|
2484
2491
|
await fs.access(tplPr);
|
|
2485
2492
|
} catch {
|
|
@@ -2654,13 +2661,13 @@ function parseNumstat(output) {
|
|
|
2654
2661
|
if (tab2 < 0) continue;
|
|
2655
2662
|
const aStr = t3.slice(0, tab);
|
|
2656
2663
|
const dStr = t3.slice(tab + 1, tab2);
|
|
2657
|
-
const
|
|
2664
|
+
const path18 = t3.slice(tab2 + 1);
|
|
2658
2665
|
const a3 = aStr === "-" ? 0 : Number(aStr);
|
|
2659
2666
|
const d3 = dStr === "-" ? 0 : Number(dStr);
|
|
2660
2667
|
if (!Number.isFinite(a3) || !Number.isFinite(d3)) continue;
|
|
2661
2668
|
additions += a3;
|
|
2662
2669
|
deletions += d3;
|
|
2663
|
-
if (
|
|
2670
|
+
if (path18) files.push(path18);
|
|
2664
2671
|
}
|
|
2665
2672
|
return { additions, deletions, files };
|
|
2666
2673
|
}
|
|
@@ -2817,7 +2824,7 @@ var defaultConfig = {
|
|
|
2817
2824
|
maxDeltaBytes: null,
|
|
2818
2825
|
maxTotalBytes: null
|
|
2819
2826
|
},
|
|
2820
|
-
|
|
2827
|
+
coreWebVitals: {
|
|
2821
2828
|
enabled: true,
|
|
2822
2829
|
gate: "warn",
|
|
2823
2830
|
scanGlobs: ["app/**/*.{tsx,jsx}", "pages/**/*.{tsx,jsx}", "src/**/*.{tsx,jsx}"],
|
|
@@ -2838,6 +2845,16 @@ var defaultConfig = {
|
|
|
2838
2845
|
}
|
|
2839
2846
|
};
|
|
2840
2847
|
|
|
2848
|
+
// src/config/migrate.ts
|
|
2849
|
+
function migrateLegacyConfigKeys(config) {
|
|
2850
|
+
const ch = config.checks;
|
|
2851
|
+
if (!ch) return;
|
|
2852
|
+
if ("cwv" in ch && !("coreWebVitals" in ch)) {
|
|
2853
|
+
ch.coreWebVitals = ch.cwv;
|
|
2854
|
+
delete ch.cwv;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2841
2858
|
// src/config/load.ts
|
|
2842
2859
|
var CONFIG_NAMES = [
|
|
2843
2860
|
"frontguard.config.js",
|
|
@@ -2860,7 +2877,7 @@ function stripExtends(c4) {
|
|
|
2860
2877
|
}
|
|
2861
2878
|
async function loadExtendsLayer(cwd, spec) {
|
|
2862
2879
|
if (!spec) return {};
|
|
2863
|
-
const req = createRequire(
|
|
2880
|
+
const req = createRequire(path5.join(cwd, "package.json"));
|
|
2864
2881
|
const specs = Array.isArray(spec) ? spec : [spec];
|
|
2865
2882
|
let merged = {};
|
|
2866
2883
|
for (const s3 of specs) {
|
|
@@ -2879,7 +2896,7 @@ async function loadExtendsLayer(cwd, spec) {
|
|
|
2879
2896
|
async function loadConfig(cwd) {
|
|
2880
2897
|
let userFile = null;
|
|
2881
2898
|
for (const name of CONFIG_NAMES) {
|
|
2882
|
-
const full =
|
|
2899
|
+
const full = path5.join(cwd, name);
|
|
2883
2900
|
if (!fs2.existsSync(full)) continue;
|
|
2884
2901
|
try {
|
|
2885
2902
|
const mod = await importConfig(full);
|
|
@@ -2891,6 +2908,8 @@ async function loadConfig(cwd) {
|
|
|
2891
2908
|
}
|
|
2892
2909
|
const extendsSpec = userFile?.extends;
|
|
2893
2910
|
const orgLayer = await loadExtendsLayer(cwd, extendsSpec);
|
|
2911
|
+
migrateLegacyConfigKeys(orgLayer);
|
|
2912
|
+
if (userFile) migrateLegacyConfigKeys(userFile);
|
|
2894
2913
|
const user = userFile ? stripExtends(userFile) : {};
|
|
2895
2914
|
const base = structuredClone(defaultConfig);
|
|
2896
2915
|
const withOrg = defu2(orgLayer, base);
|
|
@@ -2909,7 +2928,7 @@ function hasDep(deps, name) {
|
|
|
2909
2928
|
async function detectStack(cwd) {
|
|
2910
2929
|
let pkg = {};
|
|
2911
2930
|
try {
|
|
2912
|
-
const raw = await fs.readFile(
|
|
2931
|
+
const raw = await fs.readFile(path5.join(cwd, "package.json"), "utf8");
|
|
2913
2932
|
pkg = JSON.parse(raw);
|
|
2914
2933
|
} catch {
|
|
2915
2934
|
return {
|
|
@@ -2929,7 +2948,7 @@ async function detectStack(cwd) {
|
|
|
2929
2948
|
const isMonorepo = Boolean(pkg.workspaces);
|
|
2930
2949
|
let tsStrict = null;
|
|
2931
2950
|
try {
|
|
2932
|
-
const tsconfigPath =
|
|
2951
|
+
const tsconfigPath = path5.join(cwd, "tsconfig.json");
|
|
2933
2952
|
const tsRaw = await fs.readFile(tsconfigPath, "utf8");
|
|
2934
2953
|
const ts = JSON.parse(tsRaw);
|
|
2935
2954
|
if (typeof ts.compilerOptions?.strict === "boolean") {
|
|
@@ -2939,15 +2958,15 @@ async function detectStack(cwd) {
|
|
|
2939
2958
|
}
|
|
2940
2959
|
let pm = "unknown";
|
|
2941
2960
|
try {
|
|
2942
|
-
await fs.access(
|
|
2961
|
+
await fs.access(path5.join(cwd, "pnpm-lock.yaml"));
|
|
2943
2962
|
pm = "pnpm";
|
|
2944
2963
|
} catch {
|
|
2945
2964
|
try {
|
|
2946
|
-
await fs.access(
|
|
2965
|
+
await fs.access(path5.join(cwd, "yarn.lock"));
|
|
2947
2966
|
pm = "yarn";
|
|
2948
2967
|
} catch {
|
|
2949
2968
|
try {
|
|
2950
|
-
await fs.access(
|
|
2969
|
+
await fs.access(path5.join(cwd, "package-lock.json"));
|
|
2951
2970
|
pm = "npm";
|
|
2952
2971
|
} catch {
|
|
2953
2972
|
pm = "npm";
|
|
@@ -2977,6 +2996,65 @@ function formatStackOneLiner(s3) {
|
|
|
2977
2996
|
bits.push(`pkg: ${s3.packageManager}`);
|
|
2978
2997
|
return bits.join(" \xB7 ") || "unknown";
|
|
2979
2998
|
}
|
|
2999
|
+
function normalizePrPath(p2) {
|
|
3000
|
+
return p2.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
3001
|
+
}
|
|
3002
|
+
function hasPrFileList(pr) {
|
|
3003
|
+
return Boolean(pr?.files?.length);
|
|
3004
|
+
}
|
|
3005
|
+
function prPathSet(pr) {
|
|
3006
|
+
if (!hasPrFileList(pr)) return null;
|
|
3007
|
+
return new Set(pr.files.map(normalizePrPath));
|
|
3008
|
+
}
|
|
3009
|
+
function isPathInPrScope(relOrAbs, cwd, prSet) {
|
|
3010
|
+
const raw = relOrAbs.replace(/^file:\/\//, "");
|
|
3011
|
+
let rel = normalizePrPath(raw);
|
|
3012
|
+
if (path5.isAbsolute(raw)) {
|
|
3013
|
+
rel = normalizePrPath(path5.relative(cwd, raw));
|
|
3014
|
+
}
|
|
3015
|
+
if (prSet.has(rel)) return true;
|
|
3016
|
+
const trimmed = rel.replace(/^\.\//, "");
|
|
3017
|
+
if (prSet.has(trimmed)) return true;
|
|
3018
|
+
return false;
|
|
3019
|
+
}
|
|
3020
|
+
var ESLINT_EXT = /\.(js|cjs|mjs|jsx|ts|tsx)$/i;
|
|
3021
|
+
var PRETTIER_EXT = /\.(js|cjs|mjs|jsx|ts|tsx|json|md|css|scss|sass|less|yml|yaml|vue|svelte)$/i;
|
|
3022
|
+
var CYCLES_EXT = /\.(tsx?|jsx?|mjs|cjs|js)$/i;
|
|
3023
|
+
function filterPrFilesForEslint(pr) {
|
|
3024
|
+
if (!hasPrFileList(pr)) return null;
|
|
3025
|
+
return pr.files.map(normalizePrPath).filter((f4) => ESLINT_EXT.test(f4));
|
|
3026
|
+
}
|
|
3027
|
+
function filterPrFilesForPrettier(pr) {
|
|
3028
|
+
if (!hasPrFileList(pr)) return null;
|
|
3029
|
+
return pr.files.map(normalizePrPath).filter((f4) => PRETTIER_EXT.test(f4));
|
|
3030
|
+
}
|
|
3031
|
+
function filterPrFilesForMadge(pr) {
|
|
3032
|
+
if (!hasPrFileList(pr)) return null;
|
|
3033
|
+
return pr.files.map(normalizePrPath).filter((f4) => CYCLES_EXT.test(f4));
|
|
3034
|
+
}
|
|
3035
|
+
async function existingRepoPaths(cwd, rels) {
|
|
3036
|
+
const out = [];
|
|
3037
|
+
for (const rel of rels) {
|
|
3038
|
+
try {
|
|
3039
|
+
await fs.access(path5.join(cwd, rel));
|
|
3040
|
+
out.push(rel);
|
|
3041
|
+
} catch {
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
return out;
|
|
3045
|
+
}
|
|
3046
|
+
function filterTscOutputToPrFiles(output, cwd, prSet) {
|
|
3047
|
+
const lines = output.split("\n");
|
|
3048
|
+
const kept = [];
|
|
3049
|
+
for (const line of lines) {
|
|
3050
|
+
const m3 = /^(.+?)\(\d+,\d+\):\s/.exec(line);
|
|
3051
|
+
if (m3?.[1]) {
|
|
3052
|
+
if (isPathInPrScope(m3[1], cwd, prSet)) kept.push(line);
|
|
3053
|
+
continue;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
return kept.join("\n").trim();
|
|
3057
|
+
}
|
|
2980
3058
|
function stripFileUrl(p2) {
|
|
2981
3059
|
let s3 = p2.trim();
|
|
2982
3060
|
if (!/^file:/i.test(s3)) return s3;
|
|
@@ -2988,30 +3066,30 @@ function stripFileUrl(p2) {
|
|
|
2988
3066
|
return s3;
|
|
2989
3067
|
}
|
|
2990
3068
|
function isUnderDir(parent, child) {
|
|
2991
|
-
const rel =
|
|
2992
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
3069
|
+
const rel = path5.relative(parent, child);
|
|
3070
|
+
return rel === "" || !rel.startsWith("..") && !path5.isAbsolute(rel);
|
|
2993
3071
|
}
|
|
2994
3072
|
function toRepoRelativePath(cwd, filePath) {
|
|
2995
3073
|
if (!filePath?.trim()) return void 0;
|
|
2996
3074
|
const raw = stripFileUrl(filePath);
|
|
2997
|
-
const resolvedCwd =
|
|
2998
|
-
const absFile =
|
|
3075
|
+
const resolvedCwd = path5.resolve(cwd);
|
|
3076
|
+
const absFile = path5.isAbsolute(raw) ? path5.resolve(raw) : path5.resolve(resolvedCwd, raw);
|
|
2999
3077
|
if (!isUnderDir(resolvedCwd, absFile)) {
|
|
3000
3078
|
return raw.split(/[/\\]/g).join("/");
|
|
3001
3079
|
}
|
|
3002
|
-
let rel =
|
|
3080
|
+
let rel = path5.relative(resolvedCwd, absFile);
|
|
3003
3081
|
if (!rel || rel === ".") {
|
|
3004
|
-
return
|
|
3082
|
+
return path5.basename(absFile);
|
|
3005
3083
|
}
|
|
3006
|
-
return rel.split(
|
|
3084
|
+
return rel.split(path5.sep).join("/");
|
|
3007
3085
|
}
|
|
3008
3086
|
function stripRepoAbsolutePaths(cwd, text) {
|
|
3009
3087
|
if (!text || !cwd.trim()) return text;
|
|
3010
|
-
const resolvedCwd =
|
|
3088
|
+
const resolvedCwd = path5.resolve(cwd);
|
|
3011
3089
|
const asPosix = (s3) => s3.replace(/\\/g, "/");
|
|
3012
3090
|
const cwdPosix = asPosix(resolvedCwd);
|
|
3013
3091
|
let out = asPosix(text);
|
|
3014
|
-
const prefixes = [.../* @__PURE__ */ new Set([cwdPosix + "/", resolvedCwd +
|
|
3092
|
+
const prefixes = [.../* @__PURE__ */ new Set([cwdPosix + "/", resolvedCwd + path5.sep])].filter(
|
|
3015
3093
|
(p2) => p2.length > 1
|
|
3016
3094
|
);
|
|
3017
3095
|
for (const prefix of prefixes) {
|
|
@@ -3637,7 +3715,7 @@ async function pathExists(file) {
|
|
|
3637
3715
|
}
|
|
3638
3716
|
}
|
|
3639
3717
|
async function resolveBin(cwd, name) {
|
|
3640
|
-
const local =
|
|
3718
|
+
const local = path5.join(cwd, "node_modules", ".bin", name);
|
|
3641
3719
|
if (await pathExists(local)) return local;
|
|
3642
3720
|
const win = local + ".cmd";
|
|
3643
3721
|
if (await pathExists(win)) return win;
|
|
@@ -3693,7 +3771,7 @@ async function runNpx(cwd, args) {
|
|
|
3693
3771
|
// src/checks/eslint.ts
|
|
3694
3772
|
async function hasEslintDependency(cwd) {
|
|
3695
3773
|
try {
|
|
3696
|
-
const raw = await fs.readFile(
|
|
3774
|
+
const raw = await fs.readFile(path5.join(cwd, "package.json"), "utf8");
|
|
3697
3775
|
const p2 = JSON.parse(raw);
|
|
3698
3776
|
return Boolean(p2.devDependencies?.eslint || p2.dependencies?.eslint);
|
|
3699
3777
|
} catch {
|
|
@@ -3705,6 +3783,7 @@ async function hasEslintConfig(cwd) {
|
|
|
3705
3783
|
"eslint.config.js",
|
|
3706
3784
|
"eslint.config.mjs",
|
|
3707
3785
|
"eslint.config.cjs",
|
|
3786
|
+
"eslint.config.json",
|
|
3708
3787
|
".eslintrc",
|
|
3709
3788
|
".eslintrc.json",
|
|
3710
3789
|
".eslintrc.cjs",
|
|
@@ -3712,14 +3791,47 @@ async function hasEslintConfig(cwd) {
|
|
|
3712
3791
|
".eslintrc.yml"
|
|
3713
3792
|
];
|
|
3714
3793
|
for (const c4 of candidates) {
|
|
3715
|
-
if (await pathExists(
|
|
3794
|
+
if (await pathExists(path5.join(cwd, c4))) return true;
|
|
3716
3795
|
}
|
|
3717
3796
|
return false;
|
|
3718
3797
|
}
|
|
3719
3798
|
function meaningfulStderr(stderr) {
|
|
3720
3799
|
return stderr.split("\n").filter((l3) => l3.trim() && !/^npm warn\b/i.test(l3)).join("\n").trim();
|
|
3721
3800
|
}
|
|
3722
|
-
|
|
3801
|
+
var ESLINT_BATCH = 80;
|
|
3802
|
+
async function runEslintOnPaths(cwd, relPaths) {
|
|
3803
|
+
let worstExit = 0;
|
|
3804
|
+
const merged = [];
|
|
3805
|
+
let stderrAcc = "";
|
|
3806
|
+
for (let i3 = 0; i3 < relPaths.length; i3 += ESLINT_BATCH) {
|
|
3807
|
+
const batch = relPaths.slice(i3, i3 + ESLINT_BATCH);
|
|
3808
|
+
const args = [
|
|
3809
|
+
...batch,
|
|
3810
|
+
"--max-warnings",
|
|
3811
|
+
"0",
|
|
3812
|
+
"--no-error-on-unmatched-pattern",
|
|
3813
|
+
"-f",
|
|
3814
|
+
"json"
|
|
3815
|
+
];
|
|
3816
|
+
const { exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "eslint", args);
|
|
3817
|
+
worstExit = Math.max(worstExit, exitCode ?? 0);
|
|
3818
|
+
stderrAcc += stderr;
|
|
3819
|
+
const t3 = stdout2.trim();
|
|
3820
|
+
if (t3) {
|
|
3821
|
+
try {
|
|
3822
|
+
const rows = JSON.parse(t3);
|
|
3823
|
+
if (Array.isArray(rows)) merged.push(...rows);
|
|
3824
|
+
} catch {
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
return {
|
|
3829
|
+
exitCode: worstExit,
|
|
3830
|
+
stdout: JSON.stringify(merged),
|
|
3831
|
+
stderr: stderrAcc
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
async function runEslint(cwd, config, _stack, pr) {
|
|
3723
3835
|
const t0 = performance.now();
|
|
3724
3836
|
if (!config.checks.eslint.enabled) {
|
|
3725
3837
|
return {
|
|
@@ -3740,15 +3852,39 @@ async function runEslint(cwd, config, _stack) {
|
|
|
3740
3852
|
};
|
|
3741
3853
|
}
|
|
3742
3854
|
const glob = config.checks.eslint.glob ?? "**/*.{js,cjs,mjs,jsx,ts,tsx}";
|
|
3743
|
-
const
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3855
|
+
const prPaths = filterPrFilesForEslint(pr);
|
|
3856
|
+
let exitCode;
|
|
3857
|
+
let stdout2;
|
|
3858
|
+
let stderr;
|
|
3859
|
+
if (prPaths !== null) {
|
|
3860
|
+
if (prPaths.length === 0) {
|
|
3861
|
+
return {
|
|
3862
|
+
checkId: "eslint",
|
|
3863
|
+
findings: [],
|
|
3864
|
+
durationMs: Math.round(performance.now() - t0),
|
|
3865
|
+
skipped: "no lintable files in PR diff"
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
const existing = await existingRepoPaths(cwd, prPaths);
|
|
3869
|
+
if (existing.length === 0) {
|
|
3870
|
+
return {
|
|
3871
|
+
checkId: "eslint",
|
|
3872
|
+
findings: [],
|
|
3873
|
+
durationMs: Math.round(performance.now() - t0),
|
|
3874
|
+
skipped: hasPrFileList(pr) ? "no lintable files in PR diff (added/modified only)" : "no files"
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
({ exitCode, stdout: stdout2, stderr } = await runEslintOnPaths(cwd, existing));
|
|
3878
|
+
} else {
|
|
3879
|
+
({ exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "eslint", [
|
|
3880
|
+
glob,
|
|
3881
|
+
"--max-warnings",
|
|
3882
|
+
"0",
|
|
3883
|
+
"--no-error-on-unmatched-pattern",
|
|
3884
|
+
"-f",
|
|
3885
|
+
"json"
|
|
3886
|
+
]));
|
|
3887
|
+
}
|
|
3752
3888
|
const errText = meaningfulStderr(stderr);
|
|
3753
3889
|
const findings = [];
|
|
3754
3890
|
if (exitCode === 0) {
|
|
@@ -3814,7 +3950,25 @@ function truncate(s3, max) {
|
|
|
3814
3950
|
}
|
|
3815
3951
|
|
|
3816
3952
|
// src/checks/prettier.ts
|
|
3817
|
-
|
|
3953
|
+
var PRETTIER_BATCH = 100;
|
|
3954
|
+
async function runPrettierOnPaths(cwd, relPaths) {
|
|
3955
|
+
let worstExit = 0;
|
|
3956
|
+
let stdoutAcc = "";
|
|
3957
|
+
let stderrAcc = "";
|
|
3958
|
+
for (let i3 = 0; i3 < relPaths.length; i3 += PRETTIER_BATCH) {
|
|
3959
|
+
const batch = relPaths.slice(i3, i3 + PRETTIER_BATCH);
|
|
3960
|
+
const r4 = await runNpmBinary(cwd, "prettier", [
|
|
3961
|
+
"--check",
|
|
3962
|
+
"--ignore-unknown",
|
|
3963
|
+
...batch
|
|
3964
|
+
]);
|
|
3965
|
+
worstExit = Math.max(worstExit, r4.exitCode ?? 0);
|
|
3966
|
+
stdoutAcc += r4.stdout;
|
|
3967
|
+
stderrAcc += r4.stderr;
|
|
3968
|
+
}
|
|
3969
|
+
return { exitCode: worstExit, stdout: stdoutAcc, stderr: stderrAcc };
|
|
3970
|
+
}
|
|
3971
|
+
async function runPrettier(cwd, config, pr) {
|
|
3818
3972
|
const t0 = performance.now();
|
|
3819
3973
|
if (!config.checks.prettier.enabled) {
|
|
3820
3974
|
return {
|
|
@@ -3825,11 +3979,36 @@ async function runPrettier(cwd, config) {
|
|
|
3825
3979
|
};
|
|
3826
3980
|
}
|
|
3827
3981
|
const glob = config.checks.prettier.glob ?? "**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}";
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3982
|
+
let exitCode;
|
|
3983
|
+
let stdout2;
|
|
3984
|
+
let stderr;
|
|
3985
|
+
const prPaths = filterPrFilesForPrettier(pr);
|
|
3986
|
+
if (prPaths !== null) {
|
|
3987
|
+
if (prPaths.length === 0) {
|
|
3988
|
+
return {
|
|
3989
|
+
checkId: "prettier",
|
|
3990
|
+
findings: [],
|
|
3991
|
+
durationMs: Math.round(performance.now() - t0),
|
|
3992
|
+
skipped: "no formattable files in PR diff"
|
|
3993
|
+
};
|
|
3994
|
+
}
|
|
3995
|
+
const existing = await existingRepoPaths(cwd, prPaths);
|
|
3996
|
+
if (existing.length === 0) {
|
|
3997
|
+
return {
|
|
3998
|
+
checkId: "prettier",
|
|
3999
|
+
findings: [],
|
|
4000
|
+
durationMs: Math.round(performance.now() - t0),
|
|
4001
|
+
skipped: hasPrFileList(pr) ? "no formattable files in PR diff" : "no files"
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
({ exitCode, stdout: stdout2, stderr } = await runPrettierOnPaths(cwd, existing));
|
|
4005
|
+
} else {
|
|
4006
|
+
({ exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "prettier", [
|
|
4007
|
+
"--check",
|
|
4008
|
+
glob,
|
|
4009
|
+
"--ignore-unknown"
|
|
4010
|
+
]));
|
|
4011
|
+
}
|
|
3833
4012
|
const findings = [];
|
|
3834
4013
|
if (exitCode === 127 || /command not found|not recognized|ENOENT/i.test(stderr)) {
|
|
3835
4014
|
return {
|
|
@@ -3858,7 +4037,7 @@ function truncate2(s3, max) {
|
|
|
3858
4037
|
if (s3.length <= max) return s3;
|
|
3859
4038
|
return s3.slice(0, max) + "\u2026";
|
|
3860
4039
|
}
|
|
3861
|
-
async function runTypeScript(cwd, config, stack) {
|
|
4040
|
+
async function runTypeScript(cwd, config, stack, pr) {
|
|
3862
4041
|
const t0 = performance.now();
|
|
3863
4042
|
if (!config.checks.typescript.enabled) {
|
|
3864
4043
|
return {
|
|
@@ -3868,7 +4047,7 @@ async function runTypeScript(cwd, config, stack) {
|
|
|
3868
4047
|
skipped: "disabled in config"
|
|
3869
4048
|
};
|
|
3870
4049
|
}
|
|
3871
|
-
const hasTs = stack.hasTypeScript || await pathExists(
|
|
4050
|
+
const hasTs = stack.hasTypeScript || await pathExists(path5.join(cwd, "tsconfig.json"));
|
|
3872
4051
|
if (!hasTs) {
|
|
3873
4052
|
return {
|
|
3874
4053
|
checkId: "typescript",
|
|
@@ -3877,7 +4056,14 @@ async function runTypeScript(cwd, config, stack) {
|
|
|
3877
4056
|
skipped: "no TypeScript project detected"
|
|
3878
4057
|
};
|
|
3879
4058
|
}
|
|
3880
|
-
const
|
|
4059
|
+
const extra = config.checks.typescript.tscArgs ?? [];
|
|
4060
|
+
const hasProject = extra.some((a3) => a3 === "-p" || a3 === "--project");
|
|
4061
|
+
const tsconfigPath = path5.join(cwd, "tsconfig.json");
|
|
4062
|
+
const args = [
|
|
4063
|
+
"--noEmit",
|
|
4064
|
+
...hasProject || !await pathExists(tsconfigPath) ? [] : ["-p", "tsconfig.json"],
|
|
4065
|
+
...extra
|
|
4066
|
+
];
|
|
3881
4067
|
const { exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "tsc", args);
|
|
3882
4068
|
const findings = [];
|
|
3883
4069
|
if (exitCode === 127 || /command not found|not recognized|ENOENT/i.test(stderr)) {
|
|
@@ -3890,12 +4076,16 @@ async function runTypeScript(cwd, config, stack) {
|
|
|
3890
4076
|
}
|
|
3891
4077
|
if (exitCode !== 0) {
|
|
3892
4078
|
const out = [stdout2, stderr].filter(Boolean).join("\n").trim();
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
4079
|
+
const prS = prPathSet(pr);
|
|
4080
|
+
const scoped = prS && out ? filterTscOutputToPrFiles(out, cwd, prS) : out;
|
|
4081
|
+
if (scoped) {
|
|
4082
|
+
findings.push({
|
|
4083
|
+
id: "tsc",
|
|
4084
|
+
severity: "warn",
|
|
4085
|
+
message: prS ? "TypeScript: issues in PR-changed files (full project was typechecked)" : "TypeScript compiler reported diagnostics",
|
|
4086
|
+
detail: scoped ? truncate3(scoped, 8e3) : `exit ${exitCode}`
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
3899
4089
|
}
|
|
3900
4090
|
return {
|
|
3901
4091
|
checkId: "typescript",
|
|
@@ -3981,7 +4171,7 @@ async function runSecrets(cwd, config, pr) {
|
|
|
3981
4171
|
});
|
|
3982
4172
|
break;
|
|
3983
4173
|
}
|
|
3984
|
-
const full =
|
|
4174
|
+
const full = path5.join(cwd, rel);
|
|
3985
4175
|
let content;
|
|
3986
4176
|
try {
|
|
3987
4177
|
content = await fs.readFile(full, "utf8");
|
|
@@ -4007,7 +4197,7 @@ async function runSecrets(cwd, config, pr) {
|
|
|
4007
4197
|
};
|
|
4008
4198
|
}
|
|
4009
4199
|
function isProbablyTextFile(rel) {
|
|
4010
|
-
const ext =
|
|
4200
|
+
const ext = path5.extname(rel).toLowerCase();
|
|
4011
4201
|
return TEXT_EXT.has(ext);
|
|
4012
4202
|
}
|
|
4013
4203
|
|
|
@@ -4106,6 +4296,25 @@ function sectionMentioned(body, hint) {
|
|
|
4106
4296
|
}
|
|
4107
4297
|
|
|
4108
4298
|
// src/checks/pr-size.ts
|
|
4299
|
+
function expandMessage(template, lines, min) {
|
|
4300
|
+
return template.replaceAll("${lines}", String(lines)).replaceAll("${min}", String(min));
|
|
4301
|
+
}
|
|
4302
|
+
function defaultTiers(cfg) {
|
|
4303
|
+
return [
|
|
4304
|
+
{
|
|
4305
|
+
minLines: cfg.softBlockLines,
|
|
4306
|
+
severity: "warn",
|
|
4307
|
+
id: "pr-size-large",
|
|
4308
|
+
message: "PR is very large (${lines} lines changed; threshold ${min}). Consider splitting for review."
|
|
4309
|
+
},
|
|
4310
|
+
{
|
|
4311
|
+
minLines: cfg.warnLines,
|
|
4312
|
+
severity: "info",
|
|
4313
|
+
id: "pr-size-medium",
|
|
4314
|
+
message: "PR size is elevated (${lines} lines changed; threshold ${min})."
|
|
4315
|
+
}
|
|
4316
|
+
];
|
|
4317
|
+
}
|
|
4109
4318
|
function runPrSize(config, pr) {
|
|
4110
4319
|
const t0 = performance.now();
|
|
4111
4320
|
if (!config.checks.prSize.enabled) {
|
|
@@ -4124,21 +4333,26 @@ function runPrSize(config, pr) {
|
|
|
4124
4333
|
skipped: "no PR context (run in a Bitbucket pull-request pipeline so BITBUCKET_PR_* and git diff are available)"
|
|
4125
4334
|
};
|
|
4126
4335
|
}
|
|
4127
|
-
const findings = [];
|
|
4128
4336
|
const lines = pr.additions + pr.deletions;
|
|
4129
|
-
const
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
}
|
|
4137
|
-
|
|
4337
|
+
const cfg = config.checks.prSize;
|
|
4338
|
+
const rawTiers = cfg.tiers?.length ? cfg.tiers : defaultTiers(cfg);
|
|
4339
|
+
const sorted = [...rawTiers].sort((a3, b3) => b3.minLines - a3.minLines);
|
|
4340
|
+
const findings = [];
|
|
4341
|
+
let i3 = 0;
|
|
4342
|
+
for (const tier of sorted) {
|
|
4343
|
+
if (lines < tier.minLines) continue;
|
|
4344
|
+
const id = tier.id ?? `pr-size-tier-${i3}`;
|
|
4345
|
+
i3 += 1;
|
|
4346
|
+
const message = tier.message ? expandMessage(tier.message, lines, tier.minLines) : expandMessage(
|
|
4347
|
+
"PR has ${lines} lines changed (\u2265 ${min}).",
|
|
4348
|
+
lines,
|
|
4349
|
+
tier.minLines
|
|
4350
|
+
);
|
|
4138
4351
|
findings.push({
|
|
4139
|
-
id
|
|
4140
|
-
severity:
|
|
4141
|
-
message
|
|
4352
|
+
id,
|
|
4353
|
+
severity: tier.severity,
|
|
4354
|
+
message,
|
|
4355
|
+
detail: "Total = additions + deletions from the PR diff."
|
|
4142
4356
|
});
|
|
4143
4357
|
}
|
|
4144
4358
|
return {
|
|
@@ -4300,7 +4514,7 @@ async function runTsAnyDelta(cwd, config, stack) {
|
|
|
4300
4514
|
function gateSeverity2(g4) {
|
|
4301
4515
|
return g4 === "block" ? "block" : g4 === "info" ? "info" : "warn";
|
|
4302
4516
|
}
|
|
4303
|
-
async function runCycles(cwd, config, stack) {
|
|
4517
|
+
async function runCycles(cwd, config, stack, pr) {
|
|
4304
4518
|
const t0 = performance.now();
|
|
4305
4519
|
const cfg = config.checks.cycles;
|
|
4306
4520
|
if (!cfg.enabled || !stack.hasTypeScript) {
|
|
@@ -4313,12 +4527,12 @@ async function runCycles(cwd, config, stack) {
|
|
|
4313
4527
|
}
|
|
4314
4528
|
let entry = cfg.entries[0] ?? "src";
|
|
4315
4529
|
for (const e3 of cfg.entries) {
|
|
4316
|
-
if (await pathExists(
|
|
4530
|
+
if (await pathExists(path5.join(cwd, e3))) {
|
|
4317
4531
|
entry = e3;
|
|
4318
4532
|
break;
|
|
4319
4533
|
}
|
|
4320
4534
|
}
|
|
4321
|
-
if (!await pathExists(
|
|
4535
|
+
if (!await pathExists(path5.join(cwd, entry))) {
|
|
4322
4536
|
return {
|
|
4323
4537
|
checkId: "cycles",
|
|
4324
4538
|
findings: [],
|
|
@@ -4326,10 +4540,13 @@ async function runCycles(cwd, config, stack) {
|
|
|
4326
4540
|
skipped: `entry path not found (${entry})`
|
|
4327
4541
|
};
|
|
4328
4542
|
}
|
|
4543
|
+
const prMadge = filterPrFilesForMadge(pr);
|
|
4544
|
+
const prExisting = prMadge?.length ? await existingRepoPaths(cwd, prMadge) : [];
|
|
4545
|
+
const roots = prExisting.length > 0 ? prExisting : [entry];
|
|
4329
4546
|
const args = [
|
|
4330
4547
|
"-y",
|
|
4331
4548
|
"madge@6",
|
|
4332
|
-
|
|
4549
|
+
...roots,
|
|
4333
4550
|
"--extensions",
|
|
4334
4551
|
"ts,tsx,js,jsx",
|
|
4335
4552
|
"--circular",
|
|
@@ -4351,7 +4568,7 @@ async function runCycles(cwd, config, stack) {
|
|
|
4351
4568
|
findings.push({
|
|
4352
4569
|
id: "import-cycle",
|
|
4353
4570
|
severity: gateSeverity2(cfg.gate),
|
|
4354
|
-
message: "Circular dependencies detected (madge)",
|
|
4571
|
+
message: prExisting.length > 0 ? "Circular dependencies detected (madge, scoped to PR files)" : "Circular dependencies detected (madge)",
|
|
4355
4572
|
detail: truncate4(out || `exit ${exitCode}`, 12e3)
|
|
4356
4573
|
});
|
|
4357
4574
|
} else if (exitCode !== 0) {
|
|
@@ -4442,7 +4659,7 @@ async function sumGlobBytes(cwd, patterns) {
|
|
|
4442
4659
|
});
|
|
4443
4660
|
for (const rel of files) {
|
|
4444
4661
|
try {
|
|
4445
|
-
const st = await fs.stat(
|
|
4662
|
+
const st = await fs.stat(path5.join(cwd, rel));
|
|
4446
4663
|
total += st.size;
|
|
4447
4664
|
} catch {
|
|
4448
4665
|
}
|
|
@@ -4451,7 +4668,7 @@ async function sumGlobBytes(cwd, patterns) {
|
|
|
4451
4668
|
return total;
|
|
4452
4669
|
}
|
|
4453
4670
|
async function readBaseline(cwd, relPath, baseRef) {
|
|
4454
|
-
const disk =
|
|
4671
|
+
const disk = path5.join(cwd, relPath);
|
|
4455
4672
|
try {
|
|
4456
4673
|
const raw = await fs.readFile(disk, "utf8");
|
|
4457
4674
|
return JSON.parse(raw);
|
|
@@ -4495,7 +4712,7 @@ async function bundleBuildPrecheck(cwd, buildCommand) {
|
|
|
4495
4712
|
if (!script) return { run: true };
|
|
4496
4713
|
let scripts;
|
|
4497
4714
|
try {
|
|
4498
|
-
const raw = await fs.readFile(
|
|
4715
|
+
const raw = await fs.readFile(path5.join(cwd, "package.json"), "utf8");
|
|
4499
4716
|
const pkg = JSON.parse(raw);
|
|
4500
4717
|
scripts = pkg.scripts;
|
|
4501
4718
|
} catch {
|
|
@@ -4642,12 +4859,12 @@ async function runBundle(cwd, config, stack) {
|
|
|
4642
4859
|
function gateSeverity5(g4) {
|
|
4643
4860
|
return g4 === "block" ? "block" : g4 === "info" ? "info" : "warn";
|
|
4644
4861
|
}
|
|
4645
|
-
async function
|
|
4862
|
+
async function runCoreWebVitals(cwd, config, stack, pr) {
|
|
4646
4863
|
const t0 = performance.now();
|
|
4647
|
-
const cfg = config.checks.
|
|
4864
|
+
const cfg = config.checks.coreWebVitals;
|
|
4648
4865
|
if (!cfg.enabled) {
|
|
4649
4866
|
return {
|
|
4650
|
-
checkId: "
|
|
4867
|
+
checkId: "core-web-vitals",
|
|
4651
4868
|
findings: [],
|
|
4652
4869
|
durationMs: 0,
|
|
4653
4870
|
skipped: "disabled in config"
|
|
@@ -4655,7 +4872,7 @@ async function runCwv(cwd, config, stack, pr) {
|
|
|
4655
4872
|
}
|
|
4656
4873
|
if (stack.hasReactNative && !stack.hasNext) {
|
|
4657
4874
|
return {
|
|
4658
|
-
checkId: "
|
|
4875
|
+
checkId: "core-web-vitals",
|
|
4659
4876
|
findings: [],
|
|
4660
4877
|
durationMs: Math.round(performance.now() - t0),
|
|
4661
4878
|
skipped: "skipped for React Native"
|
|
@@ -4671,7 +4888,7 @@ async function runCwv(cwd, config, stack, pr) {
|
|
|
4671
4888
|
const findings = [];
|
|
4672
4889
|
const sev2 = gateSeverity5(cfg.gate);
|
|
4673
4890
|
for (const rel of toScan.slice(0, 400)) {
|
|
4674
|
-
const full =
|
|
4891
|
+
const full = path5.join(cwd, rel);
|
|
4675
4892
|
let text;
|
|
4676
4893
|
try {
|
|
4677
4894
|
text = await fs.readFile(full, "utf8");
|
|
@@ -4681,23 +4898,23 @@ async function runCwv(cwd, config, stack, pr) {
|
|
|
4681
4898
|
if (text.length > cfg.maxFileBytes) continue;
|
|
4682
4899
|
if (stack.hasNext && /<img\b/i.test(text) && !/from\s+['"]next\/image['"]/.test(text)) {
|
|
4683
4900
|
findings.push({
|
|
4684
|
-
id: "
|
|
4901
|
+
id: "core-web-vitals-img-tag",
|
|
4685
4902
|
severity: sev2,
|
|
4686
|
-
message: "Raw `<img>`
|
|
4903
|
+
message: "Raw `<img>` \u2014 prefer `next/image` for LCP-friendly delivery",
|
|
4687
4904
|
file: rel
|
|
4688
4905
|
});
|
|
4689
4906
|
}
|
|
4690
4907
|
if (/dangerouslySetInnerHTML/i.test(text)) {
|
|
4691
4908
|
findings.push({
|
|
4692
|
-
id: "
|
|
4909
|
+
id: "core-web-vitals-dsh",
|
|
4693
4910
|
severity: "warn",
|
|
4694
|
-
message: "`dangerouslySetInnerHTML` can
|
|
4911
|
+
message: "`dangerouslySetInnerHTML` can add main-thread work \u2014 validate necessity",
|
|
4695
4912
|
file: rel
|
|
4696
4913
|
});
|
|
4697
4914
|
}
|
|
4698
4915
|
}
|
|
4699
4916
|
return {
|
|
4700
|
-
checkId: "
|
|
4917
|
+
checkId: "core-web-vitals",
|
|
4701
4918
|
findings: dedupeFindings(findings).slice(0, 40),
|
|
4702
4919
|
durationMs: Math.round(performance.now() - t0)
|
|
4703
4920
|
};
|
|
@@ -4756,7 +4973,7 @@ async function runCustomRules(cwd, config, restrictToFiles) {
|
|
|
4756
4973
|
});
|
|
4757
4974
|
break;
|
|
4758
4975
|
}
|
|
4759
|
-
const full =
|
|
4976
|
+
const full = path5.join(cwd, rel);
|
|
4760
4977
|
let content;
|
|
4761
4978
|
try {
|
|
4762
4979
|
content = await fs.readFile(full, "utf8");
|
|
@@ -4872,7 +5089,7 @@ async function runAiAssistedStrict(cwd, config, pr) {
|
|
|
4872
5089
|
checkId: "ai-assisted-strict",
|
|
4873
5090
|
findings: [],
|
|
4874
5091
|
durationMs: Math.round(performance.now() - t0),
|
|
4875
|
-
skipped: "
|
|
5092
|
+
skipped: "no PR context (Bitbucket PR pipeline + BITBUCKET_PR_ID required)"
|
|
4876
5093
|
};
|
|
4877
5094
|
}
|
|
4878
5095
|
if (!pr.aiAssisted) {
|
|
@@ -4887,7 +5104,7 @@ async function runAiAssistedStrict(cwd, config, pr) {
|
|
|
4887
5104
|
const gate = cfg.gate;
|
|
4888
5105
|
const findings = [];
|
|
4889
5106
|
for (const rel of files) {
|
|
4890
|
-
const full =
|
|
5107
|
+
const full = path5.join(cwd, rel);
|
|
4891
5108
|
let content;
|
|
4892
5109
|
try {
|
|
4893
5110
|
content = await fs.readFile(full, "utf8");
|
|
@@ -4952,25 +5169,6 @@ function escapeHtml(s3) {
|
|
|
4952
5169
|
}
|
|
4953
5170
|
|
|
4954
5171
|
// src/report/html-report.ts
|
|
4955
|
-
function shieldUrl(label, message, color) {
|
|
4956
|
-
const q2 = new URLSearchParams({ label, message, color, style: "for-the-badge" });
|
|
4957
|
-
return `https://img.shields.io/static/v1?${q2}`;
|
|
4958
|
-
}
|
|
4959
|
-
function riskColor(risk) {
|
|
4960
|
-
if (risk === "LOW") return "brightgreen";
|
|
4961
|
-
if (risk === "MEDIUM") return "orange";
|
|
4962
|
-
return "red";
|
|
4963
|
-
}
|
|
4964
|
-
function modeColor(mode) {
|
|
4965
|
-
return mode === "enforce" ? "critical" : "blue";
|
|
4966
|
-
}
|
|
4967
|
-
function countColor(kind, n3) {
|
|
4968
|
-
if (kind === "block") return n3 === 0 ? "brightgreen" : "critical";
|
|
4969
|
-
if (kind === "info") return n3 === 0 ? "inactive" : "informational";
|
|
4970
|
-
if (n3 === 0) return "brightgreen";
|
|
4971
|
-
if (n3 <= 10) return "yellow";
|
|
4972
|
-
return "orange";
|
|
4973
|
-
}
|
|
4974
5172
|
function parseLineHint(detail) {
|
|
4975
5173
|
if (!detail) return 0;
|
|
4976
5174
|
const m3 = /^line\s+(\d+)/i.exec(detail.trim());
|
|
@@ -5002,13 +5200,20 @@ function formatDuration(ms) {
|
|
|
5002
5200
|
const r4 = s3 % 60;
|
|
5003
5201
|
return r4 ? `${m3}m ${r4}s` : `${m3}m`;
|
|
5004
5202
|
}
|
|
5203
|
+
function statusDot(r4) {
|
|
5204
|
+
if (r4.skipped) return '<span class="dot dot-skip" title="Skipped"></span>';
|
|
5205
|
+
if (r4.findings.length === 0) return '<span class="dot dot-ok" title="Clean"></span>';
|
|
5206
|
+
if (r4.findings.some((x3) => x3.severity === "block"))
|
|
5207
|
+
return '<span class="dot dot-block" title="Blocking"></span>';
|
|
5208
|
+
return '<span class="dot dot-warn" title="Issues"></span>';
|
|
5209
|
+
}
|
|
5005
5210
|
function renderFindingCard(cwd, r4, f4) {
|
|
5006
5211
|
const d3 = normalizeFinding(cwd, f4);
|
|
5007
5212
|
const sevClass = f4.severity === "block" ? "sev-block" : f4.severity === "warn" ? "sev-warn" : "sev-info";
|
|
5008
|
-
const fixBlock = f4.suggestedFix ? `<div class="suggested-fix"><
|
|
5213
|
+
const fixBlock = f4.suggestedFix ? `<div class="suggested-fix"><div class="fix-label">Suggested fix <span class="pill pill-llm">LLM</span></div><div class="fix-md">${escapeHtml(f4.suggestedFix.summary)}</div>${f4.suggestedFix.code ? `<pre class="code"><code>${escapeHtml(f4.suggestedFix.code)}</code></pre>` : ""}<p class="disclaimer">Suggestions are non-binding; review before applying.</p></div>` : "";
|
|
5009
5214
|
const hintRow = d3.detail && !d3.detail.includes("\n") && d3.detail.length <= 220 && !d3.detail.includes("|") ? `<tr><th>Hint</th><td>${escapeHtml(d3.detail)}</td></tr>` : "";
|
|
5010
5215
|
const detailFence = d3.detail && (d3.detail.includes("\n") || d3.detail.length > 220 || d3.detail.includes("|")) ? `<pre class="code"><code>${escapeHtml(d3.detail)}</code></pre>` : "";
|
|
5011
|
-
return `<article class="card ${sevClass}"><
|
|
5216
|
+
return `<article class="card ${sevClass}"><div class="card-title">${escapeHtml(d3.file ?? "\u2014")}</div><p class="card-msg">${escapeHtml(d3.message)}</p><table class="meta"><tr><th>Check</th><td><code>${escapeHtml(r4.checkId)}</code></td></tr><tr><th>Rule</th><td><code>${escapeHtml(f4.id)}</code></td></tr>${d3.file ? `<tr><th>File</th><td><code>${escapeHtml(d3.file)}</code></td></tr>` : ""}${hintRow}</table>${detailFence}${fixBlock}</article>`;
|
|
5012
5217
|
}
|
|
5013
5218
|
function buildHtmlReport(p2) {
|
|
5014
5219
|
const {
|
|
@@ -5024,21 +5229,11 @@ function buildHtmlReport(p2) {
|
|
|
5024
5229
|
lines,
|
|
5025
5230
|
llmAppendix
|
|
5026
5231
|
} = p2;
|
|
5027
|
-
const modeLabel = mode === "enforce" ? "
|
|
5028
|
-
const
|
|
5029
|
-
["risk", riskScore, riskColor(riskScore)],
|
|
5030
|
-
["mode", modeLabel, modeColor(mode)],
|
|
5031
|
-
["blocking", String(blocks), countColor("block", blocks)],
|
|
5032
|
-
["warnings", String(warns), countColor("warn", warns)],
|
|
5033
|
-
["info", String(infos), countColor("info", infos)]
|
|
5034
|
-
];
|
|
5035
|
-
const badgeImgs = badges.map(([l3, m3, c4]) => {
|
|
5036
|
-
const alt = `${l3}: ${m3}`;
|
|
5037
|
-
return `<img class="badge" src="${escapeHtml(shieldUrl(l3, m3, c4))}" alt="${escapeHtml(alt)}" loading="lazy" />`;
|
|
5038
|
-
}).join(" ");
|
|
5232
|
+
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
5233
|
+
const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
|
|
5039
5234
|
const checkRows = results.map((r4) => {
|
|
5040
|
-
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "
|
|
5041
|
-
return `<tr><td
|
|
5235
|
+
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
|
|
5236
|
+
return `<tr><td class="td-icon">${statusDot(r4)}</td><td><strong class="check-name">${escapeHtml(r4.checkId)}</strong></td><td class="td-status">${status}</td><td class="td-num">${r4.skipped ? "\u2014" : r4.findings.length}</td><td class="td-time">${formatDuration(r4.durationMs)}</td></tr>`;
|
|
5042
5237
|
}).join("\n");
|
|
5043
5238
|
const blockItems = sortFindings(
|
|
5044
5239
|
cwd,
|
|
@@ -5065,141 +5260,286 @@ function buildHtmlReport(p2) {
|
|
|
5065
5260
|
byCheck.set(item.r.checkId, list);
|
|
5066
5261
|
}
|
|
5067
5262
|
const checkOrder = [...byCheck.keys()].sort((a3, b3) => a3.localeCompare(b3));
|
|
5068
|
-
const blockingHtml = blockItems.length === 0 ? '<p class="
|
|
5263
|
+
const blockingHtml = blockItems.length === 0 ? '<p class="empty-state">No blocking findings.</p>' : blockItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5069
5264
|
let warningsHtml = "";
|
|
5070
5265
|
if (warnItems.length === 0) {
|
|
5071
|
-
warningsHtml = '<p class="
|
|
5266
|
+
warningsHtml = '<p class="empty-state">No warnings.</p>';
|
|
5072
5267
|
} else {
|
|
5073
5268
|
for (const cid of checkOrder) {
|
|
5074
5269
|
const group = sortFindings(cwd, byCheck.get(cid));
|
|
5075
5270
|
const cards = group.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5076
|
-
warningsHtml += `<details class="
|
|
5271
|
+
warningsHtml += `<details class="panel nested"><summary><span class="summary-title">${escapeHtml(cid)}</span><span class="summary-count">${group.length}</span></summary><div class="panel-body">${cards}</div></details>`;
|
|
5077
5272
|
}
|
|
5078
5273
|
}
|
|
5079
|
-
const infoHtml = infoItems.length === 0 ? '<p class="muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5080
|
-
const prBlock = pr && lines != null ? `<tr><th>
|
|
5081
|
-
const appendix = llmAppendix?.trim() ? `<section class="
|
|
5274
|
+
const infoHtml = infoItems.length === 0 ? '<p class="empty-state muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5275
|
+
const prBlock = pr && lines != null ? `<tr><th>Pull request</th><td>${lines} lines changed (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files</td></tr>` : "";
|
|
5276
|
+
const appendix = llmAppendix?.trim() ? `<section class="section"><h2 class="h2">Appendix</h2><pre class="md-raw">${escapeHtml(llmAppendix.trim())}</pre></section>` : "";
|
|
5082
5277
|
return `<!DOCTYPE html>
|
|
5083
5278
|
<html lang="en">
|
|
5084
5279
|
<head>
|
|
5085
5280
|
<meta charset="utf-8" />
|
|
5086
5281
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5087
|
-
<title>FrontGuard
|
|
5282
|
+
<title>FrontGuard \u2014 Report</title>
|
|
5088
5283
|
<style>
|
|
5089
5284
|
:root {
|
|
5090
|
-
--bg: #
|
|
5091
|
-
--
|
|
5092
|
-
--text: #
|
|
5093
|
-
--muted: #
|
|
5094
|
-
--border: #
|
|
5095
|
-
--
|
|
5096
|
-
--
|
|
5097
|
-
--
|
|
5098
|
-
--
|
|
5099
|
-
--
|
|
5285
|
+
--bg: #f8fafc;
|
|
5286
|
+
--surface: #ffffff;
|
|
5287
|
+
--text: #0f172a;
|
|
5288
|
+
--muted: #64748b;
|
|
5289
|
+
--border: #e2e8f0;
|
|
5290
|
+
--accent: #4f46e5;
|
|
5291
|
+
--accent-soft: #eef2ff;
|
|
5292
|
+
--block: #dc2626;
|
|
5293
|
+
--warn: #d97706;
|
|
5294
|
+
--info: #0284c7;
|
|
5295
|
+
--ok: #16a34a;
|
|
5296
|
+
--radius: 10px;
|
|
5297
|
+
--shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
|
|
5100
5298
|
}
|
|
5101
5299
|
* { box-sizing: border-box; }
|
|
5102
5300
|
body {
|
|
5103
|
-
margin: 0;
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
.
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
.
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
.
|
|
5301
|
+
margin: 0;
|
|
5302
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
5303
|
+
background: var(--bg);
|
|
5304
|
+
color: var(--text);
|
|
5305
|
+
line-height: 1.55;
|
|
5306
|
+
font-size: 15px;
|
|
5307
|
+
padding: 2rem clamp(1rem, 4vw, 3rem) 4rem;
|
|
5308
|
+
max-width: 920px;
|
|
5309
|
+
margin-left: auto;
|
|
5310
|
+
margin-right: auto;
|
|
5311
|
+
}
|
|
5312
|
+
.hero {
|
|
5313
|
+
margin-bottom: 2rem;
|
|
5314
|
+
}
|
|
5315
|
+
.brand {
|
|
5316
|
+
font-size: 0.75rem;
|
|
5317
|
+
font-weight: 600;
|
|
5318
|
+
letter-spacing: 0.12em;
|
|
5319
|
+
text-transform: uppercase;
|
|
5320
|
+
color: var(--muted);
|
|
5321
|
+
margin-bottom: 0.35rem;
|
|
5322
|
+
}
|
|
5323
|
+
h1 {
|
|
5324
|
+
font-size: 1.75rem;
|
|
5325
|
+
font-weight: 700;
|
|
5326
|
+
letter-spacing: -0.03em;
|
|
5327
|
+
margin: 0 0 1rem;
|
|
5328
|
+
color: var(--text);
|
|
5329
|
+
}
|
|
5330
|
+
.metrics {
|
|
5331
|
+
display: flex;
|
|
5332
|
+
flex-wrap: wrap;
|
|
5333
|
+
gap: 0.65rem;
|
|
5334
|
+
margin-bottom: 0.5rem;
|
|
5335
|
+
}
|
|
5336
|
+
.metric {
|
|
5337
|
+
background: var(--surface);
|
|
5338
|
+
border: 1px solid var(--border);
|
|
5339
|
+
border-radius: var(--radius);
|
|
5340
|
+
padding: 0.5rem 0.9rem;
|
|
5341
|
+
box-shadow: var(--shadow);
|
|
5342
|
+
display: flex;
|
|
5343
|
+
align-items: center;
|
|
5344
|
+
gap: 0.5rem;
|
|
5345
|
+
}
|
|
5346
|
+
.metric-label { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
5347
|
+
.metric-value { font-weight: 600; font-size: 0.95rem; }
|
|
5348
|
+
.risk-low { color: var(--ok); }
|
|
5349
|
+
.risk-med { color: var(--warn); }
|
|
5350
|
+
.risk-high { color: var(--block); }
|
|
5351
|
+
.section { margin-top: 2.25rem; }
|
|
5352
|
+
.h2 {
|
|
5353
|
+
font-size: 1rem;
|
|
5354
|
+
font-weight: 600;
|
|
5355
|
+
margin: 0 0 0.85rem;
|
|
5356
|
+
color: var(--text);
|
|
5357
|
+
letter-spacing: -0.02em;
|
|
5358
|
+
}
|
|
5359
|
+
.snapshot {
|
|
5360
|
+
width: 100%;
|
|
5361
|
+
border-collapse: collapse;
|
|
5362
|
+
font-size: 0.9rem;
|
|
5363
|
+
background: var(--surface);
|
|
5364
|
+
border-radius: var(--radius);
|
|
5365
|
+
overflow: hidden;
|
|
5366
|
+
border: 1px solid var(--border);
|
|
5367
|
+
box-shadow: var(--shadow);
|
|
5368
|
+
}
|
|
5369
|
+
.snapshot th, .snapshot td {
|
|
5370
|
+
padding: 0.65rem 1rem;
|
|
5371
|
+
text-align: left;
|
|
5372
|
+
border-bottom: 1px solid var(--border);
|
|
5373
|
+
}
|
|
5374
|
+
.snapshot tr:last-child th, .snapshot tr:last-child td { border-bottom: none; }
|
|
5375
|
+
.snapshot th {
|
|
5376
|
+
width: 9rem;
|
|
5377
|
+
color: var(--muted);
|
|
5378
|
+
font-weight: 500;
|
|
5379
|
+
background: #f1f5f9;
|
|
5380
|
+
}
|
|
5381
|
+
table.results {
|
|
5382
|
+
width: 100%;
|
|
5383
|
+
border-collapse: collapse;
|
|
5384
|
+
font-size: 0.875rem;
|
|
5385
|
+
background: var(--surface);
|
|
5386
|
+
border-radius: var(--radius);
|
|
5387
|
+
overflow: hidden;
|
|
5388
|
+
border: 1px solid var(--border);
|
|
5389
|
+
box-shadow: var(--shadow);
|
|
5390
|
+
}
|
|
5391
|
+
table.results th, table.results td {
|
|
5392
|
+
padding: 0.55rem 0.85rem;
|
|
5393
|
+
text-align: left;
|
|
5394
|
+
border-bottom: 1px solid var(--border);
|
|
5395
|
+
}
|
|
5396
|
+
table.results tr:last-child td { border-bottom: none; }
|
|
5397
|
+
table.results thead th {
|
|
5398
|
+
background: #f1f5f9;
|
|
5399
|
+
color: var(--muted);
|
|
5400
|
+
font-weight: 600;
|
|
5401
|
+
font-size: 0.72rem;
|
|
5402
|
+
text-transform: uppercase;
|
|
5403
|
+
letter-spacing: 0.04em;
|
|
5404
|
+
}
|
|
5405
|
+
.td-icon { width: 2rem; vertical-align: middle; }
|
|
5406
|
+
.td-num, .td-time { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
5407
|
+
.check-name { font-weight: 600; }
|
|
5408
|
+
.dot {
|
|
5409
|
+
display: inline-block;
|
|
5410
|
+
width: 8px;
|
|
5411
|
+
height: 8px;
|
|
5412
|
+
border-radius: 50%;
|
|
5413
|
+
}
|
|
5414
|
+
.dot-ok { background: var(--ok); }
|
|
5415
|
+
.dot-warn { background: var(--warn); }
|
|
5416
|
+
.dot-block { background: var(--block); }
|
|
5417
|
+
.dot-skip { background: #cbd5e1; }
|
|
5418
|
+
.panel {
|
|
5419
|
+
background: var(--surface);
|
|
5420
|
+
border: 1px solid var(--border);
|
|
5421
|
+
border-radius: var(--radius);
|
|
5422
|
+
margin-bottom: 0.65rem;
|
|
5423
|
+
box-shadow: var(--shadow);
|
|
5424
|
+
}
|
|
5425
|
+
.panel summary {
|
|
5426
|
+
cursor: pointer;
|
|
5427
|
+
padding: 0.85rem 1rem;
|
|
5428
|
+
list-style: none;
|
|
5429
|
+
display: flex;
|
|
5430
|
+
align-items: center;
|
|
5431
|
+
justify-content: space-between;
|
|
5432
|
+
font-weight: 600;
|
|
5433
|
+
font-size: 0.9rem;
|
|
5434
|
+
}
|
|
5435
|
+
.panel summary::-webkit-details-marker { display: none; }
|
|
5436
|
+
.panel[open] summary { border-bottom: 1px solid var(--border); }
|
|
5437
|
+
.panel-body { padding: 0.75rem 1rem 1rem; }
|
|
5438
|
+
.nested summary { font-weight: 500; color: var(--warn); }
|
|
5439
|
+
.summary-count {
|
|
5440
|
+
font-size: 0.8rem;
|
|
5441
|
+
font-weight: 500;
|
|
5442
|
+
color: var(--muted);
|
|
5443
|
+
background: #f1f5f9;
|
|
5444
|
+
padding: 0.15rem 0.5rem;
|
|
5445
|
+
border-radius: 999px;
|
|
5446
|
+
}
|
|
5133
5447
|
.card {
|
|
5134
|
-
border: 1px solid var(--border);
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5448
|
+
border: 1px solid var(--border);
|
|
5449
|
+
border-radius: 8px;
|
|
5450
|
+
padding: 1rem;
|
|
5451
|
+
margin-bottom: 0.65rem;
|
|
5452
|
+
background: #fafafa;
|
|
5453
|
+
}
|
|
5454
|
+
.card:last-child { margin-bottom: 0; }
|
|
5455
|
+
.card.sev-block { border-left: 3px solid var(--block); }
|
|
5456
|
+
.card.sev-warn { border-left: 3px solid var(--warn); }
|
|
5457
|
+
.card.sev-info { border-left: 3px solid var(--info); }
|
|
5458
|
+
.card-title { font-size: 0.8rem; font-weight: 600; color: var(--muted); margin-bottom: 0.35rem; }
|
|
5459
|
+
.card-msg { margin: 0 0 0.65rem; font-size: 0.9rem; }
|
|
5460
|
+
table.meta { width: 100%; font-size: 0.78rem; border-collapse: collapse; margin: 0.35rem 0 0; }
|
|
5461
|
+
table.meta th { text-align: left; color: var(--muted); width: 4.5rem; padding: 0.2rem 0.5rem 0.2rem 0; vertical-align: top; }
|
|
5142
5462
|
table.meta td { padding: 0.2rem 0; }
|
|
5463
|
+
table.meta code { font-size: 0.85em; background: #f1f5f9; padding: 0.1rem 0.35rem; border-radius: 4px; }
|
|
5143
5464
|
.muted { color: var(--muted); }
|
|
5144
|
-
.
|
|
5465
|
+
.empty-state { margin: 0; font-size: 0.9rem; color: var(--muted); }
|
|
5145
5466
|
pre.code {
|
|
5146
|
-
margin: 0.5rem 0 0;
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5467
|
+
margin: 0.5rem 0 0;
|
|
5468
|
+
padding: 0.75rem;
|
|
5469
|
+
background: #f1f5f9;
|
|
5470
|
+
border-radius: 6px;
|
|
5471
|
+
overflow: auto;
|
|
5472
|
+
font-size: 0.78rem;
|
|
5473
|
+
border: 1px solid var(--border);
|
|
5474
|
+
}
|
|
5475
|
+
pre.code code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }
|
|
5476
|
+
.suggested-fix { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px dashed var(--border); }
|
|
5477
|
+
.fix-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); margin-bottom: 0.35rem; }
|
|
5478
|
+
.pill-llm { background: var(--accent-soft); color: var(--accent); padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.65rem; }
|
|
5153
5479
|
.fix-md { font-size: 0.85rem; white-space: pre-wrap; margin: 0.25rem 0 0.5rem; }
|
|
5154
|
-
.tag {
|
|
5155
|
-
font-size: 0.65rem; background: var(--accent); color: var(--bg);
|
|
5156
|
-
padding: 0.1rem 0.35rem; border-radius: 4px; vertical-align: middle;
|
|
5157
|
-
}
|
|
5158
5480
|
.disclaimer { font-size: 0.72rem; color: var(--muted); margin: 0.5rem 0 0; }
|
|
5159
|
-
|
|
5160
|
-
white-space: pre-wrap;
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5481
|
+
pre.md-raw {
|
|
5482
|
+
white-space: pre-wrap;
|
|
5483
|
+
font-size: 0.85rem;
|
|
5484
|
+
background: var(--surface);
|
|
5485
|
+
padding: 1rem;
|
|
5486
|
+
border-radius: var(--radius);
|
|
5487
|
+
border: 1px solid var(--border);
|
|
5488
|
+
margin: 0;
|
|
5489
|
+
}
|
|
5490
|
+
footer {
|
|
5491
|
+
margin-top: 3rem;
|
|
5492
|
+
padding-top: 1.25rem;
|
|
5493
|
+
border-top: 1px solid var(--border);
|
|
5494
|
+
font-size: 0.8rem;
|
|
5495
|
+
color: var(--muted);
|
|
5496
|
+
}
|
|
5497
|
+
footer a { color: var(--accent); text-decoration: none; }
|
|
5498
|
+
footer a:hover { text-decoration: underline; }
|
|
5164
5499
|
</style>
|
|
5165
5500
|
</head>
|
|
5166
5501
|
<body>
|
|
5167
|
-
<
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
<h2>Check results</h2>
|
|
5179
|
-
<table class="results">
|
|
5180
|
-
<thead><tr><th></th><th>Check</th><th>Status</th><th>#</th><th>Time</th></tr></thead>
|
|
5181
|
-
<tbody>${checkRows}</tbody>
|
|
5182
|
-
</table>
|
|
5502
|
+
<header class="hero">
|
|
5503
|
+
<div class="brand">FrontGuard</div>
|
|
5504
|
+
<h1>Code review report</h1>
|
|
5505
|
+
<div class="metrics">
|
|
5506
|
+
<div class="metric"><span class="metric-label">Risk</span><span class="metric-value ${riskClass}">${riskScore}</span></div>
|
|
5507
|
+
<div class="metric"><span class="metric-label">Mode</span><span class="metric-value">${escapeHtml(modeLabel)}</span></div>
|
|
5508
|
+
<div class="metric"><span class="metric-label">Blocking</span><span class="metric-value">${blocks}</span></div>
|
|
5509
|
+
<div class="metric"><span class="metric-label">Warnings</span><span class="metric-value">${warns}</span></div>
|
|
5510
|
+
<div class="metric"><span class="metric-label">Info</span><span class="metric-value">${infos}</span></div>
|
|
5511
|
+
</div>
|
|
5512
|
+
</header>
|
|
5183
5513
|
|
|
5184
|
-
<
|
|
5185
|
-
<
|
|
5186
|
-
<
|
|
5187
|
-
|
|
5514
|
+
<section class="section">
|
|
5515
|
+
<h2 class="h2">Overview</h2>
|
|
5516
|
+
<table class="snapshot">
|
|
5517
|
+
<tr><th>Risk score</th><td><strong>${riskScore}</strong> <span class="muted">\u2014 heuristic</span></td></tr>
|
|
5518
|
+
<tr><th>Mode</th><td>${escapeHtml(modeLabel)}</td></tr>
|
|
5519
|
+
<tr><th>Stack</th><td>${escapeHtml(formatStackOneLiner(stack))}</td></tr>
|
|
5520
|
+
${prBlock}
|
|
5521
|
+
</table>
|
|
5522
|
+
</section>
|
|
5188
5523
|
|
|
5189
|
-
<
|
|
5190
|
-
<
|
|
5191
|
-
<
|
|
5192
|
-
|
|
5524
|
+
<section class="section">
|
|
5525
|
+
<h2 class="h2">Checks</h2>
|
|
5526
|
+
<table class="results">
|
|
5527
|
+
<thead><tr><th></th><th>Check</th><th>Status</th><th>#</th><th>Time</th></tr></thead>
|
|
5528
|
+
<tbody>${checkRows}</tbody>
|
|
5529
|
+
</table>
|
|
5530
|
+
</section>
|
|
5193
5531
|
|
|
5194
|
-
<
|
|
5195
|
-
<
|
|
5196
|
-
<div class="
|
|
5197
|
-
|
|
5532
|
+
<section class="section">
|
|
5533
|
+
<h2 class="h2">Findings</h2>
|
|
5534
|
+
<details class="panel"><summary>Blocking <span class="summary-count">${blocks}</span></summary><div class="panel-body">${blockingHtml}</div></details>
|
|
5535
|
+
<details class="panel"><summary>Warnings <span class="summary-count">${warns}</span></summary><div class="panel-body">${warningsHtml}</div></details>
|
|
5536
|
+
<details class="panel"><summary>Info <span class="summary-count">${infos}</span></summary><div class="panel-body">${infoHtml}</div></details>
|
|
5537
|
+
</section>
|
|
5198
5538
|
|
|
5199
5539
|
${appendix}
|
|
5200
5540
|
|
|
5201
5541
|
<footer>
|
|
5202
|
-
<p>
|
|
5542
|
+
<p>Static report \u2014 open in any browser. Generated by <strong>FrontGuard</strong>.</p>
|
|
5203
5543
|
</footer>
|
|
5204
5544
|
</body>
|
|
5205
5545
|
</html>`;
|
|
@@ -5645,10 +5985,10 @@ async function callOllamaChat(opts) {
|
|
|
5645
5985
|
|
|
5646
5986
|
// src/llm/finding-fixes.ts
|
|
5647
5987
|
async function safeReadRepoFile(cwd, rel, maxChars) {
|
|
5648
|
-
const root =
|
|
5649
|
-
const abs =
|
|
5650
|
-
const relToRoot =
|
|
5651
|
-
if (relToRoot.startsWith("..") ||
|
|
5988
|
+
const root = path5.resolve(cwd);
|
|
5989
|
+
const abs = path5.resolve(root, rel);
|
|
5990
|
+
const relToRoot = path5.relative(root, abs);
|
|
5991
|
+
if (relToRoot.startsWith("..") || path5.isAbsolute(relToRoot)) return null;
|
|
5652
5992
|
try {
|
|
5653
5993
|
let t3 = await fs.readFile(abs, "utf8");
|
|
5654
5994
|
if (t3.length > maxChars) {
|
|
@@ -5677,7 +6017,7 @@ async function enrichFindingsWithOllamaFixes(opts) {
|
|
|
5677
6017
|
}
|
|
5678
6018
|
let pkgSnippet = "";
|
|
5679
6019
|
try {
|
|
5680
|
-
const pj = await fs.readFile(
|
|
6020
|
+
const pj = await fs.readFile(path5.join(cwd, "package.json"), "utf8");
|
|
5681
6021
|
pkgSnippet = pj.slice(0, 4e3);
|
|
5682
6022
|
} catch {
|
|
5683
6023
|
pkgSnippet = "";
|
|
@@ -5761,7 +6101,7 @@ async function loadManualAppendix(opts) {
|
|
|
5761
6101
|
const envFile = process.env.FRONTGUARD_MANUAL_APPENDIX_FILE?.trim();
|
|
5762
6102
|
const resolvedPath = filePath?.trim() || envFile;
|
|
5763
6103
|
if (resolvedPath) {
|
|
5764
|
-
const abs =
|
|
6104
|
+
const abs = path5.isAbsolute(resolvedPath) ? resolvedPath : path5.join(cwd, resolvedPath);
|
|
5765
6105
|
try {
|
|
5766
6106
|
let text = await fs.readFile(abs, "utf8");
|
|
5767
6107
|
if (text.length > MAX_CHARS) {
|
|
@@ -5956,21 +6296,21 @@ async function runFrontGuard(opts) {
|
|
|
5956
6296
|
prettier,
|
|
5957
6297
|
typescript,
|
|
5958
6298
|
secrets,
|
|
5959
|
-
tsAnyDelta,
|
|
5960
6299
|
cycles,
|
|
5961
6300
|
deadCode,
|
|
5962
|
-
|
|
6301
|
+
coreWebVitals,
|
|
6302
|
+
tsAnyDelta,
|
|
5963
6303
|
customRules,
|
|
5964
6304
|
aiStrict
|
|
5965
6305
|
] = await Promise.all([
|
|
5966
|
-
runEslint(opts.cwd, config),
|
|
5967
|
-
runPrettier(opts.cwd, config),
|
|
5968
|
-
runTypeScript(opts.cwd, config, stack),
|
|
6306
|
+
runEslint(opts.cwd, config, stack, pr),
|
|
6307
|
+
runPrettier(opts.cwd, config, pr),
|
|
6308
|
+
runTypeScript(opts.cwd, config, stack, pr),
|
|
5969
6309
|
runSecrets(opts.cwd, config, pr),
|
|
5970
|
-
|
|
5971
|
-
runCycles(opts.cwd, config, stack),
|
|
6310
|
+
runCycles(opts.cwd, config, stack, pr),
|
|
5972
6311
|
runDeadCode(opts.cwd, config, stack, pr),
|
|
5973
|
-
|
|
6312
|
+
runCoreWebVitals(opts.cwd, config, stack, pr),
|
|
6313
|
+
runTsAnyDelta(opts.cwd, config, stack),
|
|
5974
6314
|
runCustomRules(opts.cwd, config, restrictFiles),
|
|
5975
6315
|
runAiAssistedStrict(opts.cwd, config, pr)
|
|
5976
6316
|
]);
|
|
@@ -5982,15 +6322,15 @@ async function runFrontGuard(opts) {
|
|
|
5982
6322
|
prettier,
|
|
5983
6323
|
typescript,
|
|
5984
6324
|
secrets,
|
|
5985
|
-
tsAnyDelta,
|
|
5986
6325
|
cycles,
|
|
5987
6326
|
deadCode,
|
|
5988
6327
|
bundle,
|
|
5989
|
-
|
|
5990
|
-
customRules,
|
|
6328
|
+
coreWebVitals,
|
|
5991
6329
|
aiStrict,
|
|
5992
6330
|
prHygiene,
|
|
5993
|
-
prSize
|
|
6331
|
+
prSize,
|
|
6332
|
+
tsAnyDelta,
|
|
6333
|
+
customRules
|
|
5994
6334
|
];
|
|
5995
6335
|
applyAiAssistedEscalation(results, pr, config);
|
|
5996
6336
|
results = await enrichFindingsWithOllamaFixes({
|
|
@@ -6021,7 +6361,7 @@ async function runFrontGuard(opts) {
|
|
|
6021
6361
|
}
|
|
6022
6362
|
if (opts.prCommentOut) {
|
|
6023
6363
|
const snippet = formatBitbucketPrSnippet(report);
|
|
6024
|
-
const abs =
|
|
6364
|
+
const abs = path5.isAbsolute(opts.prCommentOut) ? opts.prCommentOut : path5.join(opts.cwd, opts.prCommentOut);
|
|
6025
6365
|
await fs.writeFile(abs, snippet, "utf8");
|
|
6026
6366
|
g.stderr.write(
|
|
6027
6367
|
`
|