@cleartrip/frontguard 0.3.5 → 1.0.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/README.md +222 -177
- package/dist/cli.js +823 -521
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +29 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -6
package/dist/cli.js
CHANGED
|
@@ -2402,186 +2402,209 @@ async function runMain(cmd, opts = {}) {
|
|
|
2402
2402
|
}
|
|
2403
2403
|
var CONFIG_TEMPLATE = `import { defineConfig } from '@cleartrip/frontguard'
|
|
2404
2404
|
|
|
2405
|
+
/**
|
|
2406
|
+
* FrontGuard configuration.
|
|
2407
|
+
*
|
|
2408
|
+
* Severity levels: 'info' = note only | 'warn' = advisory | 'block' = fails CI in enforce mode
|
|
2409
|
+
* Gate modes: 'warn' (default) = advisory only | 'enforce' = exit 1 on any 'block' finding
|
|
2410
|
+
*
|
|
2411
|
+
* Run \`frontguard run\` locally to test your config before pushing.
|
|
2412
|
+
*/
|
|
2405
2413
|
export default defineConfig({
|
|
2406
|
-
//
|
|
2407
|
-
// 'warn' \u2014 findings are advisory; CI always passes (default).
|
|
2408
|
-
// 'enforce' \u2014 CI exits non-zero when any finding has severity 'block'.
|
|
2409
|
-
// Same as running \`frontguard run --enforce\`.
|
|
2414
|
+
// CI gate: 'warn' = never fails CI (default) | 'enforce' = fails CI on block-severity findings
|
|
2410
2415
|
mode: 'warn',
|
|
2411
2416
|
|
|
2412
|
-
//
|
|
2413
|
-
//
|
|
2414
|
-
// extends: '@your-org/frontguard-config/base',
|
|
2417
|
+
// Inherit defaults from a shared org config package (optional).
|
|
2418
|
+
// extends: '@your-org/frontguard-config',
|
|
2415
2419
|
|
|
2416
|
-
// \u2500\u2500
|
|
2417
|
-
//
|
|
2420
|
+
// \u2500\u2500 Custom rules \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2421
|
+
// Write your own checks as simple functions. Return true = rule violated.
|
|
2422
|
+
//
|
|
2418
2423
|
// rules: {
|
|
2419
|
-
// 'no-inline-style': {
|
|
2420
|
-
// severity: 'warn',
|
|
2421
|
-
// message: 'Avoid inline style objects in JSX',
|
|
2422
|
-
// check: (file) => /\\.tsx$/.test(file.path) && file.content.includes('style={{'),
|
|
2423
|
-
// },
|
|
2424
2424
|
// 'no-console-log': {
|
|
2425
|
-
// severity: '
|
|
2425
|
+
// severity: 'warn',
|
|
2426
2426
|
// message: 'Remove console.log before merging',
|
|
2427
2427
|
// check: (file) => /\\.(ts|tsx|js|jsx)$/.test(file.path) && /console\\.log\\(/.test(file.content),
|
|
2428
2428
|
// },
|
|
2429
|
+
// 'no-inline-styles': {
|
|
2430
|
+
// severity: 'info',
|
|
2431
|
+
// message: 'Prefer CSS classes over inline style objects',
|
|
2432
|
+
// check: (file) => /\\.tsx$/.test(file.path) && file.content.includes('style={{'),
|
|
2433
|
+
// },
|
|
2429
2434
|
// },
|
|
2430
2435
|
|
|
2431
2436
|
checks: {
|
|
2432
|
-
|
|
2433
|
-
//
|
|
2434
|
-
//
|
|
2437
|
+
|
|
2438
|
+
// \u2500\u2500 Linting (ESLint) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2439
|
+
// Uses your existing ESLint config. Runs on PR-changed files only when in CI.
|
|
2440
|
+
//
|
|
2435
2441
|
// eslint: {
|
|
2436
2442
|
// enabled: true,
|
|
2437
2443
|
// glob: '**/*.{js,cjs,mjs,jsx,ts,tsx}',
|
|
2438
|
-
//
|
|
2439
|
-
//
|
|
2440
|
-
// '**/frontguard.config.js',
|
|
2441
|
-
// '**/frontguard.config.cjs',
|
|
2442
|
-
// ],
|
|
2444
|
+
// // Files to skip (picomatch patterns). Defaults exclude frontguard.config.* files.
|
|
2445
|
+
// ignorePatterns: ['**/frontguard.config.mjs', '**/frontguard.config.js', '**/frontguard.config.cjs'],
|
|
2443
2446
|
// },
|
|
2444
2447
|
|
|
2445
|
-
// \u2500\u2500
|
|
2446
|
-
// Checks that files match Prettier
|
|
2447
|
-
//
|
|
2448
|
+
// \u2500\u2500 Formatting (Prettier) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2449
|
+
// Checks that files match your Prettier config.
|
|
2450
|
+
//
|
|
2448
2451
|
// prettier: {
|
|
2449
2452
|
// enabled: true,
|
|
2450
2453
|
// glob: '**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}',
|
|
2451
|
-
// ignorePatterns: [
|
|
2452
|
-
// '**/frontguard.config.mjs',
|
|
2453
|
-
// '**/frontguard.config.js',
|
|
2454
|
-
// '**/frontguard.config.cjs',
|
|
2455
|
-
// ],
|
|
2454
|
+
// ignorePatterns: ['**/frontguard.config.mjs', '**/frontguard.config.js', '**/frontguard.config.cjs'],
|
|
2456
2455
|
// },
|
|
2457
2456
|
|
|
2458
|
-
// \u2500\u2500
|
|
2459
|
-
// Runs \`tsc --noEmit
|
|
2457
|
+
// \u2500\u2500 Type checking (TypeScript) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2458
|
+
// Runs \`tsc --noEmit\`. Requires a tsconfig.json in the project root.
|
|
2459
|
+
//
|
|
2460
2460
|
// typescript: {
|
|
2461
2461
|
// enabled: true,
|
|
2462
|
-
// tscArgs: [
|
|
2462
|
+
// tscArgs: [], // extra flags, e.g. ['--strict']
|
|
2463
2463
|
// },
|
|
2464
2464
|
|
|
2465
|
-
// \u2500\u2500
|
|
2466
|
-
// Scans
|
|
2465
|
+
// \u2500\u2500 Secret detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2466
|
+
// Scans for leaked API keys, tokens, and private keys using regex patterns.
|
|
2467
|
+
//
|
|
2467
2468
|
// secrets: { enabled: true },
|
|
2468
2469
|
|
|
2469
|
-
// \u2500\u2500
|
|
2470
|
-
//
|
|
2470
|
+
// \u2500\u2500 PR description quality \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2471
|
+
// Checks that the PR body meets your team's standards.
|
|
2472
|
+
//
|
|
2471
2473
|
// prHygiene: {
|
|
2472
2474
|
// enabled: true,
|
|
2473
|
-
// minBodyLength: 80,
|
|
2474
|
-
// requireSections: false,
|
|
2475
|
-
// sectionHints: ['what', 'why', 'test', 'screenshot'],
|
|
2476
|
-
// requireAiDisclosureSection: true,
|
|
2477
|
-
// gateWhenAiDisclosureAmbiguous: 'warn',
|
|
2475
|
+
// minBodyLength: 80, // minimum characters in PR description
|
|
2476
|
+
// requireSections: false, // set true to require section headers
|
|
2477
|
+
// sectionHints: ['what', 'why', 'test', 'screenshot'], // expected section keywords
|
|
2478
|
+
// requireAiDisclosureSection: true, // expect an ## AI disclosure section
|
|
2479
|
+
// gateWhenAiDisclosureAmbiguous: 'warn', // 'info' | 'warn' | 'block'
|
|
2478
2480
|
// },
|
|
2479
2481
|
|
|
2480
|
-
// \u2500\u2500
|
|
2481
|
-
// Flags
|
|
2482
|
+
// \u2500\u2500 PR size limits \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2483
|
+
// Flags PRs that are too large to review effectively.
|
|
2484
|
+
//
|
|
2482
2485
|
// prSize: {
|
|
2483
2486
|
// enabled: true,
|
|
2484
|
-
//
|
|
2485
|
-
//
|
|
2486
|
-
// //
|
|
2487
|
+
// // Simple mode: two thresholds
|
|
2488
|
+
// warnLines: 400, // warn above this many changed lines
|
|
2489
|
+
// softBlockLines: 800, // block above this many changed lines
|
|
2490
|
+
// // Advanced mode: custom tiers (overrides warnLines/softBlockLines when set)
|
|
2487
2491
|
// // tiers: [
|
|
2488
|
-
// // { minLines: 1000, severity: 'block', message: 'PR too large (\${lines} lines)' },
|
|
2489
|
-
// // { minLines: 500, severity: 'warn', message: '
|
|
2492
|
+
// // { minLines: 1000, severity: 'block', message: 'PR too large (\${lines} lines). Split it up.' },
|
|
2493
|
+
// // { minLines: 500, severity: 'warn', message: 'Large PR (\${lines} lines). Consider splitting.' },
|
|
2490
2494
|
// // ],
|
|
2491
2495
|
// },
|
|
2492
2496
|
|
|
2493
|
-
// \u2500\u2500
|
|
2494
|
-
// Counts new \`any\`
|
|
2497
|
+
// \u2500\u2500 TypeScript \`any\` usage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2498
|
+
// Counts new \`any\` types introduced in this diff. Helps prevent type safety erosion.
|
|
2499
|
+
//
|
|
2495
2500
|
// tsAnyDelta: {
|
|
2496
2501
|
// enabled: true,
|
|
2497
|
-
// gate: 'warn',
|
|
2498
|
-
// baseRef: 'main',
|
|
2499
|
-
// maxAdded: 0,
|
|
2502
|
+
// gate: 'warn', // severity when maxAdded is exceeded: 'info' | 'warn' | 'block'
|
|
2503
|
+
// baseRef: 'main', // branch to compare against (overridden by BITBUCKET_PR_DESTINATION_BRANCH)
|
|
2504
|
+
// maxAdded: 0, // 0 = report the count; >0 = trigger gate only when exceeded
|
|
2500
2505
|
// },
|
|
2501
2506
|
|
|
2502
|
-
// \u2500\u2500
|
|
2503
|
-
//
|
|
2507
|
+
// \u2500\u2500 Circular import detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2508
|
+
// Uses \`madge\` to find circular dependencies in your source tree.
|
|
2509
|
+
//
|
|
2504
2510
|
// cycles: {
|
|
2505
2511
|
// enabled: true,
|
|
2506
2512
|
// gate: 'warn',
|
|
2507
|
-
// entries: ['src'],
|
|
2508
|
-
// extraArgs: [],
|
|
2513
|
+
// entries: ['src'], // directories to analyse
|
|
2514
|
+
// extraArgs: [], // extra flags forwarded to madge
|
|
2509
2515
|
// },
|
|
2510
2516
|
|
|
2511
|
-
// \u2500\u2500
|
|
2512
|
-
//
|
|
2517
|
+
// \u2500\u2500 Unused exports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2518
|
+
// Uses \`ts-prune\` to surface exports that are never imported.
|
|
2519
|
+
//
|
|
2513
2520
|
// deadCode: {
|
|
2514
2521
|
// enabled: true,
|
|
2515
|
-
// gate: 'info',
|
|
2516
|
-
//
|
|
2517
|
-
// maxReportLines: 80,
|
|
2522
|
+
// gate: 'info', // usually kept as info to avoid noise
|
|
2523
|
+
// maxReportLines: 80, // cap output length
|
|
2518
2524
|
// },
|
|
2519
2525
|
|
|
2520
|
-
// \u2500\u2500
|
|
2521
|
-
//
|
|
2526
|
+
// \u2500\u2500 Bundle size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2527
|
+
// Builds your project and measures the output size against a committed baseline.
|
|
2522
2528
|
//
|
|
2523
|
-
//
|
|
2524
|
-
// 'auto' \u2014 auto-
|
|
2525
|
-
// 'next' \u2014
|
|
2526
|
-
// 'vite' \u2014
|
|
2527
|
-
// 'cra' \u2014
|
|
2528
|
-
// 'glob' \u2014
|
|
2529
|
-
// 'custom' \u2014
|
|
2529
|
+
// Strategy options:
|
|
2530
|
+
// 'auto' \u2014 auto-detects from package.json (Next.js \u2192 next, Vite \u2192 vite, CRA \u2192 cra, else \u2192 glob)
|
|
2531
|
+
// 'next' \u2014 reads "First Load JS shared by all" from next build output
|
|
2532
|
+
// 'vite' \u2014 sums dist/assets/*.js
|
|
2533
|
+
// 'cra' \u2014 reads gzipped JS from react-scripts build output
|
|
2534
|
+
// 'glob' \u2014 sums all files matching measureGlobs
|
|
2535
|
+
// 'custom' \u2014 runs bundleSizeCommand and reads a number (bytes) from stdout
|
|
2530
2536
|
//
|
|
2531
2537
|
// bundle: {
|
|
2532
2538
|
// enabled: true,
|
|
2533
2539
|
// gate: 'warn',
|
|
2534
|
-
//
|
|
2535
|
-
//
|
|
2536
|
-
//
|
|
2537
|
-
// bundleSizeCommand: null, // only for strategy 'custom', e.g. 'node scripts/bundle-size.js'
|
|
2540
|
+
// buildCommand: 'npm run build', // command to build your project
|
|
2541
|
+
// bundleSizeStrategy: 'auto', // see above
|
|
2542
|
+
// bundleSizeCommand: null, // only needed when strategy is 'custom'
|
|
2538
2543
|
// measureGlobs: ['dist/**/*', 'build/static/**/*', '.next/static/**/*'],
|
|
2539
|
-
// baselinePath: '.frontguard/bundle-baseline.json',
|
|
2540
|
-
// baselineRef: 'main',
|
|
2541
|
-
// maxDeltaBytes: 50_000,
|
|
2542
|
-
// maxTotalBytes: null,
|
|
2544
|
+
// baselinePath: '.frontguard/bundle-baseline.json', // committed baseline file
|
|
2545
|
+
// baselineRef: 'main', // git ref to read baseline from if file missing
|
|
2546
|
+
// maxDeltaBytes: 50_000, // max allowed growth vs baseline (null = no limit)
|
|
2547
|
+
// maxTotalBytes: null, // absolute size cap (null = no limit)
|
|
2548
|
+
// runBuild: true, // set false to skip build and measure existing files
|
|
2543
2549
|
// },
|
|
2544
2550
|
|
|
2545
|
-
// \u2500\u2500
|
|
2546
|
-
// Static
|
|
2551
|
+
// \u2500\u2500 Core Web Vitals hints \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2552
|
+
// Static analysis of JSX/TSX for patterns that harm LCP, CLS, or FID.
|
|
2553
|
+
// Not a substitute for real field data (Lighthouse, CrUX).
|
|
2554
|
+
//
|
|
2547
2555
|
// coreWebVitals: {
|
|
2548
2556
|
// enabled: true,
|
|
2549
2557
|
// gate: 'warn',
|
|
2550
2558
|
// scanGlobs: ['app/**/*.{tsx,jsx}', 'pages/**/*.{tsx,jsx}', 'src/**/*.{tsx,jsx}'],
|
|
2551
|
-
// maxFileBytes: 400_000,
|
|
2552
2559
|
// },
|
|
2553
2560
|
|
|
2554
|
-
// \u2500\u2500
|
|
2555
|
-
//
|
|
2556
|
-
//
|
|
2561
|
+
// \u2500\u2500 React Native hygiene \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2562
|
+
// Runs only when react-native is in your dependencies. Checks Metro config,
|
|
2563
|
+
// optionally runs align-deps and react-native doctor, and hints on native file changes.
|
|
2564
|
+
//
|
|
2565
|
+
// reactNative: {
|
|
2557
2566
|
// enabled: true,
|
|
2567
|
+
// gate: 'info',
|
|
2568
|
+
// requireMetroConfig: true, // warn when metro.config.* is missing
|
|
2569
|
+
// runAlignDeps: false, // run @rnx-kit/align-deps (needs network/registry)
|
|
2570
|
+
// alignDepsArgs: ['--requirements', 'react-native'],
|
|
2571
|
+
// runDoctor: false, // run react-native doctor (slow, macOS-heavy)
|
|
2572
|
+
// swiftLintOnChangedSwift: true, // run SwiftLint when PR touches .swift files
|
|
2573
|
+
// hintAndroidNativeOnPr: true, // remind to run Android Lint when .kt/.java change
|
|
2574
|
+
// },
|
|
2575
|
+
|
|
2576
|
+
// \u2500\u2500 AI-assisted strict checks (off by default) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2577
|
+
// When enabled: scans code marked with @frontguard-ai comments (or AI-disclosed PRs)
|
|
2578
|
+
// for risky patterns \u2014 eval(), dangerouslySetInnerHTML, @ts-ignore, empty catch, etc.
|
|
2579
|
+
//
|
|
2580
|
+
// aiAssistedReview: {
|
|
2581
|
+
// enabled: false,
|
|
2558
2582
|
// gate: 'warn',
|
|
2559
|
-
// // 'decorator'
|
|
2560
|
-
// // 'pr-disclosure'
|
|
2561
|
-
// // 'both'
|
|
2583
|
+
// // 'decorator' \u2014 only scan regions marked with @frontguard-ai:start / :end
|
|
2584
|
+
// // 'pr-disclosure' \u2014 scan all touched files when PR discloses AI use
|
|
2585
|
+
// // 'both' \u2014 decorator regions when present; PR disclosure as fallback
|
|
2562
2586
|
// strictScanMode: 'both',
|
|
2563
2587
|
// escalate: {
|
|
2564
|
-
// secretFindingsToBlock: true,
|
|
2565
|
-
// tsAnyDeltaToBlock: true,
|
|
2588
|
+
// secretFindingsToBlock: true, // promote secret findings to 'block' in AI PRs
|
|
2589
|
+
// tsAnyDeltaToBlock: true, // promote any-delta findings to 'block' in AI PRs
|
|
2566
2590
|
// },
|
|
2567
2591
|
// },
|
|
2568
2592
|
|
|
2569
|
-
// \u2500\u2500
|
|
2570
|
-
//
|
|
2571
|
-
//
|
|
2572
|
-
//
|
|
2593
|
+
// \u2500\u2500 LLM-assisted review (off by default) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2594
|
+
// Attaches an automated diff review from an LLM to the report.
|
|
2595
|
+
// Supports OpenAI, Anthropic, or a local Ollama instance.
|
|
2596
|
+
//
|
|
2573
2597
|
// llm: {
|
|
2574
|
-
// enabled:
|
|
2575
|
-
// provider: 'ollama',
|
|
2576
|
-
// model: 'llama3.2',
|
|
2577
|
-
//
|
|
2578
|
-
//
|
|
2598
|
+
// enabled: false,
|
|
2599
|
+
// provider: 'ollama', // 'openai' | 'anthropic' | 'ollama'
|
|
2600
|
+
// model: 'llama3.2', // model name
|
|
2601
|
+
// apiKeyEnv: 'OPENAI_API_KEY', // env var holding the API key (cloud providers only)
|
|
2602
|
+
// ollamaUrl: 'http://127.0.0.1:11434', // Ollama base URL
|
|
2579
2603
|
// maxDiffChars: 48_000,
|
|
2580
2604
|
// timeoutMs: 60_000,
|
|
2581
|
-
// perFindingFixes: false,
|
|
2582
|
-
// maxFixSuggestions: 12,
|
|
2583
|
-
// maxFileContextChars: 24_000,
|
|
2605
|
+
// perFindingFixes: false, // ask for fix suggestions per finding (slower)
|
|
2584
2606
|
// },
|
|
2607
|
+
|
|
2585
2608
|
},
|
|
2586
2609
|
})
|
|
2587
2610
|
`;
|
|
@@ -2852,8 +2875,8 @@ function mdTableCell(s3) {
|
|
|
2852
2875
|
function statusForPrTable(r4) {
|
|
2853
2876
|
const n3 = r4.findings.length;
|
|
2854
2877
|
const blocks = r4.findings.filter((f4) => f4.severity === "block").length;
|
|
2855
|
-
if (blocks > 0) return `${n3} issue
|
|
2856
|
-
return `${n3} issue
|
|
2878
|
+
if (blocks > 0) return `${n3} issue${n3 === 1 ? "" : "s"}, ${blocks} blocking`;
|
|
2879
|
+
return `${n3} issue${n3 === 1 ? "" : "s"}`;
|
|
2857
2880
|
}
|
|
2858
2881
|
function checkNeedsRow(r4) {
|
|
2859
2882
|
return !r4.skipped && r4.findings.length > 0;
|
|
@@ -2869,21 +2892,21 @@ function formatBitbucketPrSnippet(report) {
|
|
|
2869
2892
|
const reportUrl = publicReport || FRONTGUARD_REPORT_URL_PLACEHOLDER;
|
|
2870
2893
|
const { riskScore, results } = report;
|
|
2871
2894
|
const lines = [];
|
|
2872
|
-
|
|
2895
|
+
const riskEmoji2 = riskScore === "HIGH" ? "\u{1F534}" : riskScore === "MEDIUM" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
2896
|
+
lines.push(`**FrontGuard** \xB7 ${riskEmoji2} ${riskScore} risk`);
|
|
2873
2897
|
lines.push("");
|
|
2874
2898
|
const failing = results.filter(checkNeedsRow);
|
|
2875
2899
|
if (failing.length > 0) {
|
|
2876
|
-
lines.push("| Check
|
|
2877
|
-
lines.push("|
|
|
2900
|
+
lines.push("| Check | Issues |");
|
|
2901
|
+
lines.push("| :-- | :-- |");
|
|
2878
2902
|
for (const r4 of failing) {
|
|
2879
|
-
lines.push(`|
|
|
2903
|
+
lines.push(`| \`${mdTableCell(r4.checkId)}\` | ${mdTableCell(statusForPrTable(r4))} |`);
|
|
2880
2904
|
}
|
|
2881
2905
|
} else {
|
|
2882
|
-
lines.push("_All checks passed
|
|
2906
|
+
lines.push("_All checks passed._");
|
|
2883
2907
|
}
|
|
2884
2908
|
lines.push("");
|
|
2885
2909
|
lines.push(DETAILED_REPORT_LINE);
|
|
2886
|
-
lines.push("");
|
|
2887
2910
|
lines.push(reportUrl.endsWith("\n") ? reportUrl.slice(0, -1) : reportUrl);
|
|
2888
2911
|
lines.push("");
|
|
2889
2912
|
return lines.join("\n");
|
|
@@ -2971,13 +2994,13 @@ function parseNumstat(output) {
|
|
|
2971
2994
|
if (tab2 < 0) continue;
|
|
2972
2995
|
const aStr = t3.slice(0, tab);
|
|
2973
2996
|
const dStr = t3.slice(tab + 1, tab2);
|
|
2974
|
-
const
|
|
2997
|
+
const path21 = t3.slice(tab2 + 1);
|
|
2975
2998
|
const a3 = aStr === "-" ? 0 : Number(aStr);
|
|
2976
2999
|
const d3 = dStr === "-" ? 0 : Number(dStr);
|
|
2977
3000
|
if (!Number.isFinite(a3) || !Number.isFinite(d3)) continue;
|
|
2978
3001
|
additions += a3;
|
|
2979
3002
|
deletions += d3;
|
|
2980
|
-
if (
|
|
3003
|
+
if (path21) files.push(path21);
|
|
2981
3004
|
}
|
|
2982
3005
|
return { additions, deletions, files };
|
|
2983
3006
|
}
|
|
@@ -3220,6 +3243,16 @@ var defaultConfig = {
|
|
|
3220
3243
|
maxDeltaBytes: null,
|
|
3221
3244
|
maxTotalBytes: null
|
|
3222
3245
|
},
|
|
3246
|
+
reactNative: {
|
|
3247
|
+
enabled: true,
|
|
3248
|
+
gate: "info",
|
|
3249
|
+
requireMetroConfig: true,
|
|
3250
|
+
runAlignDeps: false,
|
|
3251
|
+
alignDepsArgs: ["--requirements", "react-native"],
|
|
3252
|
+
runDoctor: false,
|
|
3253
|
+
swiftLintOnChangedSwift: true,
|
|
3254
|
+
hintAndroidNativeOnPr: true
|
|
3255
|
+
},
|
|
3223
3256
|
coreWebVitals: {
|
|
3224
3257
|
enabled: true,
|
|
3225
3258
|
gate: "warn",
|
|
@@ -4319,13 +4352,13 @@ async function runEslint(cwd, config, _stack, pr) {
|
|
|
4319
4352
|
try {
|
|
4320
4353
|
const rows = JSON.parse(stdout2);
|
|
4321
4354
|
for (const row of rows) {
|
|
4322
|
-
const
|
|
4355
|
+
const relFile2 = toRepoRelativePath(cwd, row.filePath);
|
|
4323
4356
|
for (const m3 of row.messages) {
|
|
4324
4357
|
findings.push({
|
|
4325
4358
|
id: `eslint-${m3.ruleId ?? "unknown"}`,
|
|
4326
4359
|
severity: "warn",
|
|
4327
4360
|
message: m3.message,
|
|
4328
|
-
file:
|
|
4361
|
+
file: relFile2,
|
|
4329
4362
|
detail: m3.line ? `line ${m3.line}` : void 0
|
|
4330
4363
|
});
|
|
4331
4364
|
}
|
|
@@ -5512,6 +5545,211 @@ function dedupeFindings(f4) {
|
|
|
5512
5545
|
}
|
|
5513
5546
|
return out;
|
|
5514
5547
|
}
|
|
5548
|
+
function gateSeverity6(g4) {
|
|
5549
|
+
return g4 === "block" ? "block" : g4 === "info" ? "info" : "warn";
|
|
5550
|
+
}
|
|
5551
|
+
var METRO_CANDIDATES = [
|
|
5552
|
+
"metro.config.js",
|
|
5553
|
+
"metro.config.mjs",
|
|
5554
|
+
"metro.config.cjs",
|
|
5555
|
+
"metro.config.ts"
|
|
5556
|
+
];
|
|
5557
|
+
var SWIFTLINT_MAX_FINDINGS = 40;
|
|
5558
|
+
function prSwiftFiles(pr) {
|
|
5559
|
+
if (!hasPrFileList(pr)) return [];
|
|
5560
|
+
return pr.files.map(normalizePrPath).filter((f4) => /\.swift$/i.test(f4));
|
|
5561
|
+
}
|
|
5562
|
+
function prTouchesAndroidNative(pr) {
|
|
5563
|
+
if (!hasPrFileList(pr)) return false;
|
|
5564
|
+
return pr.files.some((f4) => {
|
|
5565
|
+
const n3 = normalizePrPath(f4);
|
|
5566
|
+
return /(^|\/)android\/.*\.(kt|kts|java)$/i.test(n3);
|
|
5567
|
+
});
|
|
5568
|
+
}
|
|
5569
|
+
function prTouchesObjc(pr) {
|
|
5570
|
+
if (!hasPrFileList(pr)) return false;
|
|
5571
|
+
return pr.files.some((f4) => /\.(m|mm)$/i.test(normalizePrPath(f4)));
|
|
5572
|
+
}
|
|
5573
|
+
function truncate5(s3, max) {
|
|
5574
|
+
if (s3.length <= max) return s3;
|
|
5575
|
+
return s3.slice(0, max) + "\u2026";
|
|
5576
|
+
}
|
|
5577
|
+
function parseSwiftLintJson(raw) {
|
|
5578
|
+
try {
|
|
5579
|
+
const j2 = JSON.parse(raw);
|
|
5580
|
+
if (Array.isArray(j2)) return j2;
|
|
5581
|
+
if (j2 && typeof j2 === "object" && Array.isArray(j2.violations)) {
|
|
5582
|
+
return j2.violations;
|
|
5583
|
+
}
|
|
5584
|
+
} catch {
|
|
5585
|
+
}
|
|
5586
|
+
return [];
|
|
5587
|
+
}
|
|
5588
|
+
function relFile(cwd, file) {
|
|
5589
|
+
if (!file) return void 0;
|
|
5590
|
+
if (path6.isAbsolute(file)) {
|
|
5591
|
+
try {
|
|
5592
|
+
return normalizePrPath(path6.relative(cwd, file));
|
|
5593
|
+
} catch {
|
|
5594
|
+
return file;
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
return normalizePrPath(file);
|
|
5598
|
+
}
|
|
5599
|
+
async function runReactNative(cwd, config, stack, pr) {
|
|
5600
|
+
const t0 = performance.now();
|
|
5601
|
+
const cfg = config.checks.reactNative;
|
|
5602
|
+
if (!stack.hasReactNative) {
|
|
5603
|
+
return {
|
|
5604
|
+
checkId: "react-native",
|
|
5605
|
+
findings: [],
|
|
5606
|
+
durationMs: Math.round(performance.now() - t0),
|
|
5607
|
+
skipped: "not a React Native project"
|
|
5608
|
+
};
|
|
5609
|
+
}
|
|
5610
|
+
if (!cfg.enabled) {
|
|
5611
|
+
return {
|
|
5612
|
+
checkId: "react-native",
|
|
5613
|
+
findings: [],
|
|
5614
|
+
durationMs: 0,
|
|
5615
|
+
skipped: "disabled in config"
|
|
5616
|
+
};
|
|
5617
|
+
}
|
|
5618
|
+
const findings = [];
|
|
5619
|
+
const sev2 = gateSeverity6(cfg.gate);
|
|
5620
|
+
if (cfg.requireMetroConfig) {
|
|
5621
|
+
let found = false;
|
|
5622
|
+
for (const name of METRO_CANDIDATES) {
|
|
5623
|
+
if (await pathExists(path6.join(cwd, name))) {
|
|
5624
|
+
found = true;
|
|
5625
|
+
break;
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
if (!found) {
|
|
5629
|
+
findings.push({
|
|
5630
|
+
id: "rn-metro-config-missing",
|
|
5631
|
+
severity: sev2,
|
|
5632
|
+
message: "No Metro config found (expected metro.config.js|mjs|cjs|ts at project root)",
|
|
5633
|
+
detail: "Metro bundles JavaScript for React Native. Expo and RN templates normally include metro.config.js."
|
|
5634
|
+
});
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
if (cfg.runAlignDeps) {
|
|
5638
|
+
const r4 = await runNpx(cwd, ["--yes", "@rnx-kit/align-deps", ...cfg.alignDepsArgs]);
|
|
5639
|
+
if ((r4.exitCode ?? 0) !== 0) {
|
|
5640
|
+
findings.push({
|
|
5641
|
+
id: "rn-align-deps-failed",
|
|
5642
|
+
severity: sev2,
|
|
5643
|
+
message: "@rnx-kit/align-deps reported dependency mismatches",
|
|
5644
|
+
detail: truncate5(
|
|
5645
|
+
[r4.stdout, r4.stderr].filter(Boolean).join("\n") || `exit ${r4.exitCode}`,
|
|
5646
|
+
8e3
|
|
5647
|
+
)
|
|
5648
|
+
});
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
if (cfg.runDoctor) {
|
|
5652
|
+
const r4 = await runNpmBinary(cwd, "react-native", ["doctor"]);
|
|
5653
|
+
if (r4.exitCode === 127 || /not recognized|command not found|ENOENT/i.test(r4.stderr)) {
|
|
5654
|
+
findings.push({
|
|
5655
|
+
id: "rn-doctor-skipped",
|
|
5656
|
+
severity: "info",
|
|
5657
|
+
message: "react-native doctor not run (CLI binary not found in this project)",
|
|
5658
|
+
detail: "Install dependencies so `node_modules/.bin/react-native` exists, or only enable runDoctor on agents with the RN CLI."
|
|
5659
|
+
});
|
|
5660
|
+
} else if ((r4.exitCode ?? 0) !== 0) {
|
|
5661
|
+
findings.push({
|
|
5662
|
+
id: "rn-doctor-issues",
|
|
5663
|
+
severity: sev2,
|
|
5664
|
+
message: "react-native doctor exited with errors",
|
|
5665
|
+
detail: truncate5([r4.stdout, r4.stderr].filter(Boolean).join("\n"), 8e3)
|
|
5666
|
+
});
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
const swifts = prSwiftFiles(pr);
|
|
5670
|
+
if (cfg.swiftLintOnChangedSwift && swifts.length > 0) {
|
|
5671
|
+
const existing = await existingRepoPaths(cwd, swifts);
|
|
5672
|
+
if (existing.length > 0) {
|
|
5673
|
+
let ver;
|
|
5674
|
+
try {
|
|
5675
|
+
ver = await W2("swiftlint", ["version"], { nodeOptions: { cwd } });
|
|
5676
|
+
} catch {
|
|
5677
|
+
ver = { exitCode: 1, stdout: "", stderr: "" };
|
|
5678
|
+
}
|
|
5679
|
+
if ((ver.exitCode ?? 0) !== 0) {
|
|
5680
|
+
findings.push({
|
|
5681
|
+
id: "rn-swiftlint-unavailable",
|
|
5682
|
+
severity: "info",
|
|
5683
|
+
message: "PR changes Swift files but SwiftLint is not on PATH",
|
|
5684
|
+
detail: `Touched: ${existing.slice(0, 12).join(", ")}${existing.length > 12 ? "\u2026" : ""}`
|
|
5685
|
+
});
|
|
5686
|
+
} else {
|
|
5687
|
+
let r4;
|
|
5688
|
+
try {
|
|
5689
|
+
r4 = await W2("swiftlint", ["lint", "--reporter", "json", "--quiet", ...existing], {
|
|
5690
|
+
nodeOptions: { cwd }
|
|
5691
|
+
});
|
|
5692
|
+
} catch (e3) {
|
|
5693
|
+
r4 = {
|
|
5694
|
+
exitCode: 1,
|
|
5695
|
+
stdout: "",
|
|
5696
|
+
stderr: e3 instanceof Error ? e3.message : String(e3)
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
5699
|
+
const out = (r4.stdout ?? "").trim();
|
|
5700
|
+
const violations = out ? parseSwiftLintJson(out) : [];
|
|
5701
|
+
if (violations.length > 0) {
|
|
5702
|
+
const slice = violations.slice(0, SWIFTLINT_MAX_FINDINGS);
|
|
5703
|
+
for (const v3 of slice) {
|
|
5704
|
+
const isErr = (v3.severity ?? "").toLowerCase() === "error";
|
|
5705
|
+
findings.push({
|
|
5706
|
+
id: `rn-swiftlint-${(v3.rule_id ?? "rule").replace(/[^a-z0-9-]/gi, "-")}`,
|
|
5707
|
+
severity: isErr ? sev2 : "warn",
|
|
5708
|
+
message: v3.reason ?? "SwiftLint violation",
|
|
5709
|
+
file: relFile(cwd, v3.file),
|
|
5710
|
+
detail: v3.line != null ? `line ${v3.line}` : void 0
|
|
5711
|
+
});
|
|
5712
|
+
}
|
|
5713
|
+
if (violations.length > SWIFTLINT_MAX_FINDINGS) {
|
|
5714
|
+
findings.push({
|
|
5715
|
+
id: "rn-swiftlint-truncated",
|
|
5716
|
+
severity: "info",
|
|
5717
|
+
message: `SwiftLint: ${violations.length - SWIFTLINT_MAX_FINDINGS} more violation(s) not listed`
|
|
5718
|
+
});
|
|
5719
|
+
}
|
|
5720
|
+
} else if ((r4.exitCode ?? 0) !== 0) {
|
|
5721
|
+
findings.push({
|
|
5722
|
+
id: "rn-swiftlint-failed",
|
|
5723
|
+
severity: sev2,
|
|
5724
|
+
message: "SwiftLint failed",
|
|
5725
|
+
detail: truncate5([out, r4.stderr].filter(Boolean).join("\n") || `exit ${r4.exitCode}`, 4e3)
|
|
5726
|
+
});
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
if (cfg.hintAndroidNativeOnPr && prTouchesAndroidNative(pr)) {
|
|
5732
|
+
findings.push({
|
|
5733
|
+
id: "rn-android-native-hint",
|
|
5734
|
+
severity: "info",
|
|
5735
|
+
message: "PR touches Android Kotlin/Java under android/",
|
|
5736
|
+
detail: "Consider Android Lint (`./gradlew :app:lint`) or Detekt in CI when native sources change."
|
|
5737
|
+
});
|
|
5738
|
+
}
|
|
5739
|
+
if (hasPrFileList(pr) && prTouchesObjc(pr)) {
|
|
5740
|
+
findings.push({
|
|
5741
|
+
id: "rn-ios-objc-hint",
|
|
5742
|
+
severity: "info",
|
|
5743
|
+
message: "PR touches Objective-C (.m / .mm) sources",
|
|
5744
|
+
detail: "Consider clang-format or Xcode Analyze on changed native files."
|
|
5745
|
+
});
|
|
5746
|
+
}
|
|
5747
|
+
return {
|
|
5748
|
+
checkId: "react-native",
|
|
5749
|
+
findings,
|
|
5750
|
+
durationMs: Math.round(performance.now() - t0)
|
|
5751
|
+
};
|
|
5752
|
+
}
|
|
5515
5753
|
var DEFAULT_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs}";
|
|
5516
5754
|
async function runCustomRules(cwd, config, restrictToFiles) {
|
|
5517
5755
|
const t0 = performance.now();
|
|
@@ -6106,6 +6344,7 @@ var CHECK_DESCRIPTIONS = {
|
|
|
6106
6344
|
cycles: "Runs madge for circular dependencies on TypeScript/JavaScript entry points. Import cycles can cause brittle builds and load order bugs.",
|
|
6107
6345
|
"dead-code": "Runs ts-prune to find unused exports in the TypeScript project. Helps trim dead surface area.",
|
|
6108
6346
|
bundle: "Measures bundle size using a stack-aware strategy (Next.js build output, Vite/CRA file sizes, or custom command) and compares to a checked-in baseline.",
|
|
6347
|
+
"react-native": "When react-native is a dependency: Metro config presence, optional @rnx-kit/align-deps and react-native doctor, SwiftLint on changed Swift in PRs, and light hints for Android/iOS native edits.",
|
|
6109
6348
|
"core-web-vitals": "Static hints in JSX/TSX related to Core Web Vitals (e.g. LCP-friendly images, main-thread hygiene). Not a substitute for real field metrics.",
|
|
6110
6349
|
"ai-assisted-strict": "Under development \u2014 off by default. When enabled: scans @frontguard-ai regions or AI-disclosed PR files for risky patterns (eval, XSS sinks, etc.) and AST heuristics.",
|
|
6111
6350
|
"pr-hygiene": "Validates PR metadata when CI provides PR context: description length, checklist items, and similar hygiene rules from config.",
|
|
@@ -6142,438 +6381,519 @@ function sortFindings(cwd, items) {
|
|
|
6142
6381
|
});
|
|
6143
6382
|
}
|
|
6144
6383
|
function formatDuration(ms) {
|
|
6145
|
-
if (ms < 1e3) return `${ms}
|
|
6384
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
6146
6385
|
const s3 = Math.round(ms / 1e3);
|
|
6147
6386
|
if (s3 < 60) return `${s3}s`;
|
|
6148
6387
|
const m3 = Math.floor(s3 / 60);
|
|
6149
6388
|
const r4 = s3 % 60;
|
|
6150
6389
|
return r4 ? `${m3}m ${r4}s` : `${m3}m`;
|
|
6151
6390
|
}
|
|
6152
|
-
function
|
|
6153
|
-
if (r4.skipped) return '<span class="
|
|
6154
|
-
if (r4.findings.length === 0) return '<span class="
|
|
6391
|
+
function checkStatusIcon(r4) {
|
|
6392
|
+
if (r4.skipped) return '<span class="status-icon skip" title="Skipped">\u2013</span>';
|
|
6393
|
+
if (r4.findings.length === 0) return '<span class="status-icon pass" title="Passed">\u2713</span>';
|
|
6155
6394
|
if (r4.findings.some((x3) => x3.severity === "block"))
|
|
6156
|
-
return '<span class="
|
|
6157
|
-
return '<span class="
|
|
6158
|
-
}
|
|
6159
|
-
var CHECKS_TABLE_STYLES = `
|
|
6160
|
-
table.results {
|
|
6161
|
-
width: 100%;
|
|
6162
|
-
border-collapse: collapse;
|
|
6163
|
-
font-size: 0.875rem;
|
|
6164
|
-
background: var(--surface);
|
|
6165
|
-
border-radius: var(--radius);
|
|
6166
|
-
overflow: hidden;
|
|
6167
|
-
border: 1px solid var(--border);
|
|
6168
|
-
box-shadow: var(--shadow);
|
|
6169
|
-
}
|
|
6170
|
-
table.results th, table.results td {
|
|
6171
|
-
padding: 0.55rem 0.85rem;
|
|
6172
|
-
text-align: left;
|
|
6173
|
-
border-bottom: 1px solid var(--border);
|
|
6174
|
-
}
|
|
6175
|
-
table.results tr:last-child td { border-bottom: none; }
|
|
6176
|
-
table.results thead th {
|
|
6177
|
-
background: #f1f5f9;
|
|
6178
|
-
color: var(--muted);
|
|
6179
|
-
font-weight: 600;
|
|
6180
|
-
font-size: 0.72rem;
|
|
6181
|
-
text-transform: uppercase;
|
|
6182
|
-
letter-spacing: 0.04em;
|
|
6183
|
-
}
|
|
6184
|
-
.td-icon { width: 2rem; vertical-align: middle; }
|
|
6185
|
-
.td-check { vertical-align: middle; }
|
|
6186
|
-
.td-num, .td-time { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
6187
|
-
.check-title-cell {
|
|
6188
|
-
display: inline-flex;
|
|
6189
|
-
align-items: center;
|
|
6190
|
-
gap: 0.35rem;
|
|
6191
|
-
flex-wrap: nowrap;
|
|
6192
|
-
}
|
|
6193
|
-
.check-name { font-weight: 600; }
|
|
6194
|
-
.check-info-wrap {
|
|
6195
|
-
position: relative;
|
|
6196
|
-
display: inline-flex;
|
|
6197
|
-
align-items: center;
|
|
6198
|
-
flex-shrink: 0;
|
|
6199
|
-
}
|
|
6200
|
-
.check-info {
|
|
6201
|
-
display: inline-flex;
|
|
6202
|
-
align-items: center;
|
|
6203
|
-
justify-content: center;
|
|
6204
|
-
width: 1.125rem;
|
|
6205
|
-
height: 1.125rem;
|
|
6206
|
-
padding: 0;
|
|
6207
|
-
margin: 0;
|
|
6208
|
-
border: 1px solid var(--border);
|
|
6209
|
-
border-radius: 50%;
|
|
6210
|
-
background: #f1f5f9;
|
|
6211
|
-
color: var(--muted);
|
|
6212
|
-
font-size: 0.62rem;
|
|
6213
|
-
font-weight: 700;
|
|
6214
|
-
font-style: normal;
|
|
6215
|
-
line-height: 1;
|
|
6216
|
-
cursor: help;
|
|
6217
|
-
flex-shrink: 0;
|
|
6218
|
-
}
|
|
6219
|
-
.check-info:hover,
|
|
6220
|
-
.check-info:focus-visible {
|
|
6221
|
-
border-color: var(--accent);
|
|
6222
|
-
color: var(--accent);
|
|
6223
|
-
background: var(--accent-soft);
|
|
6224
|
-
outline: none;
|
|
6225
|
-
}
|
|
6226
|
-
.check-tooltip {
|
|
6227
|
-
position: absolute;
|
|
6228
|
-
left: 50%;
|
|
6229
|
-
bottom: calc(100% + 8px);
|
|
6230
|
-
transform: translateX(-50%);
|
|
6231
|
-
min-width: 12rem;
|
|
6232
|
-
max-width: min(22rem, 86vw);
|
|
6233
|
-
padding: 0.55rem 0.65rem;
|
|
6234
|
-
background: var(--text);
|
|
6235
|
-
color: #f8fafc;
|
|
6236
|
-
font-size: 0.78rem;
|
|
6237
|
-
font-weight: 400;
|
|
6238
|
-
line-height: 1.45;
|
|
6239
|
-
border-radius: 6px;
|
|
6240
|
-
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
|
|
6241
|
-
z-index: 50;
|
|
6242
|
-
opacity: 0;
|
|
6243
|
-
visibility: hidden;
|
|
6244
|
-
pointer-events: none;
|
|
6245
|
-
transition: opacity 0.12s ease, visibility 0.12s ease;
|
|
6246
|
-
text-align: left;
|
|
6247
|
-
}
|
|
6248
|
-
.check-info-wrap:hover .check-tooltip,
|
|
6249
|
-
.check-info-wrap:focus-within .check-tooltip {
|
|
6250
|
-
opacity: 1;
|
|
6251
|
-
visibility: visible;
|
|
6252
|
-
}
|
|
6253
|
-
.check-tooltip::after {
|
|
6254
|
-
content: '';
|
|
6255
|
-
position: absolute;
|
|
6256
|
-
top: 100%;
|
|
6257
|
-
left: 50%;
|
|
6258
|
-
margin-left: -6px;
|
|
6259
|
-
border: 6px solid transparent;
|
|
6260
|
-
border-top-color: var(--text);
|
|
6261
|
-
}
|
|
6262
|
-
.dot {
|
|
6263
|
-
display: inline-block;
|
|
6264
|
-
width: 8px;
|
|
6265
|
-
height: 8px;
|
|
6266
|
-
border-radius: 50%;
|
|
6267
|
-
}
|
|
6268
|
-
.dot-ok { background: var(--ok); }
|
|
6269
|
-
.dot-warn { background: var(--warn); }
|
|
6270
|
-
.dot-block { background: var(--block); }
|
|
6271
|
-
.dot-skip { background: #cbd5e1; }
|
|
6272
|
-
`;
|
|
6273
|
-
function renderCheckTableRows(results) {
|
|
6274
|
-
return results.map((r4) => {
|
|
6275
|
-
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
|
|
6276
|
-
const help = escapeHtml(getCheckDescription(r4.checkId));
|
|
6277
|
-
const ariaWhat = escapeHtml(`What does the ${r4.checkId} check do?`);
|
|
6278
|
-
const checkTitle = `<span class="check-title-cell"><strong class="check-name">${escapeHtml(r4.checkId)}</strong><span class="check-info-wrap"><button type="button" class="check-info" title="${help}" aria-label="${ariaWhat}">i</button><span class="check-tooltip" role="tooltip">${help}</span></span></span>`;
|
|
6279
|
-
return `<tr><td class="td-icon">${statusDot(r4)}</td><td class="td-check">${checkTitle}</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>`;
|
|
6280
|
-
}).join("\n");
|
|
6395
|
+
return '<span class="status-icon block" title="Blocking">\u2715</span>';
|
|
6396
|
+
return '<span class="status-icon warn" title="Warnings">!</span>';
|
|
6281
6397
|
}
|
|
6282
6398
|
function renderFindingCard(cwd, r4, f4) {
|
|
6283
6399
|
const d3 = normalizeFinding(cwd, f4);
|
|
6284
6400
|
const sevClass = f4.severity === "block" ? "sev-block" : f4.severity === "warn" ? "sev-warn" : "sev-info";
|
|
6285
|
-
const
|
|
6286
|
-
const
|
|
6287
|
-
|
|
6288
|
-
|
|
6401
|
+
const sevLabel = f4.severity === "block" ? "blocking" : f4.severity === "warn" ? "warning" : "info";
|
|
6402
|
+
const meta = [];
|
|
6403
|
+
meta.push(`<span class="tag tag-check">${escapeHtml(r4.checkId)}</span>`);
|
|
6404
|
+
meta.push(`<span class="tag tag-rule">${escapeHtml(f4.id)}</span>`);
|
|
6405
|
+
if (d3.file) meta.push(`<span class="tag tag-file">${escapeHtml(d3.file)}</span>`);
|
|
6406
|
+
const hintHtml = d3.detail && !d3.detail.includes("\n") && d3.detail.length <= 240 && !d3.detail.includes("|") ? `<p class="card-hint">${escapeHtml(d3.detail)}</p>` : "";
|
|
6407
|
+
const detailHtml = d3.detail && (d3.detail.includes("\n") || d3.detail.length > 240 || d3.detail.includes("|")) ? `<pre class="code-block"><code>${escapeHtml(d3.detail)}</code></pre>` : "";
|
|
6408
|
+
const fixHtml = f4.suggestedFix ? `<div class="fix-box"><div class="fix-header">Suggested fix <span class="badge badge-llm">LLM</span></div><p class="fix-summary">${escapeHtml(f4.suggestedFix.summary)}</p>${f4.suggestedFix.code ? `<pre class="code-block"><code>${escapeHtml(f4.suggestedFix.code)}</code></pre>` : ""}<p class="fix-disclaimer">Non-binding \u2014 review before applying.</p></div>` : "";
|
|
6409
|
+
return `<div class="card ${sevClass}"><div class="card-header"><span class="sev-badge ${sevClass}">${sevLabel}</span><p class="card-message">${escapeHtml(d3.message)}</p></div><div class="card-meta">${meta.join("")}</div>${hintHtml}${detailHtml}${fixHtml}</div>`;
|
|
6410
|
+
}
|
|
6411
|
+
function renderCheckTableRows(_cwd, results) {
|
|
6412
|
+
return results.map((r4) => {
|
|
6413
|
+
const icon = checkStatusIcon(r4);
|
|
6414
|
+
const statusText = r4.skipped ? `<span class="status-text muted">Skipped \u2014 ${escapeHtml(r4.skipped)}</span>` : r4.findings.length === 0 ? '<span class="status-text ok">Passed</span>' : r4.findings.some((f4) => f4.severity === "block") ? `<span class="status-text danger">${r4.findings.length} issue${r4.findings.length === 1 ? "" : "s"}</span>` : `<span class="status-text caution">${r4.findings.length} issue${r4.findings.length === 1 ? "" : "s"}</span>`;
|
|
6415
|
+
const help = escapeHtml(getCheckDescription(r4.checkId));
|
|
6416
|
+
const countCell = r4.skipped ? "\u2014" : String(r4.findings.length);
|
|
6417
|
+
const timeCell = formatDuration(r4.durationMs);
|
|
6418
|
+
return `<tr><td class="col-icon">${icon}</td><td class="col-check"><span class="check-name">${escapeHtml(r4.checkId)}</span><span class="check-tooltip">${help}</span></td><td class="col-status">${statusText}</td><td class="col-count">${countCell}</td><td class="col-time">${timeCell}</td></tr>`;
|
|
6419
|
+
}).join("\n");
|
|
6289
6420
|
}
|
|
6290
6421
|
function buildHtmlReport(p2) {
|
|
6291
|
-
const {
|
|
6292
|
-
cwd,
|
|
6293
|
-
riskScore,
|
|
6294
|
-
mode,
|
|
6295
|
-
stack,
|
|
6296
|
-
pr,
|
|
6297
|
-
results,
|
|
6298
|
-
warns,
|
|
6299
|
-
infos,
|
|
6300
|
-
blocks,
|
|
6301
|
-
lines,
|
|
6302
|
-
llmAppendix
|
|
6303
|
-
} = p2;
|
|
6304
|
-
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
6422
|
+
const { cwd, riskScore, mode, stack, pr, results, warns, infos, blocks, lines, llmAppendix } = p2;
|
|
6305
6423
|
const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
|
|
6306
|
-
const
|
|
6307
|
-
const
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
);
|
|
6313
|
-
const warnItems = sortFindings(
|
|
6314
|
-
cwd,
|
|
6315
|
-
results.flatMap(
|
|
6316
|
-
(r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))
|
|
6317
|
-
)
|
|
6318
|
-
);
|
|
6319
|
-
const infoItems = sortFindings(
|
|
6320
|
-
cwd,
|
|
6321
|
-
results.flatMap(
|
|
6322
|
-
(r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))
|
|
6323
|
-
)
|
|
6324
|
-
);
|
|
6424
|
+
const riskIcon = riskScore === "LOW" ? "\u2713" : riskScore === "MEDIUM" ? "!" : "\u2715";
|
|
6425
|
+
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
6426
|
+
const checkRows = renderCheckTableRows(cwd, results);
|
|
6427
|
+
const blockItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))));
|
|
6428
|
+
const warnItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))));
|
|
6429
|
+
const infoItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))));
|
|
6325
6430
|
const byCheck = /* @__PURE__ */ new Map();
|
|
6326
6431
|
for (const item of warnItems) {
|
|
6327
6432
|
const list = byCheck.get(item.r.checkId) ?? [];
|
|
6328
6433
|
list.push(item);
|
|
6329
6434
|
byCheck.set(item.r.checkId, list);
|
|
6330
6435
|
}
|
|
6331
|
-
const
|
|
6332
|
-
const
|
|
6333
|
-
let
|
|
6436
|
+
const warnCheckOrder = [...byCheck.keys()].sort();
|
|
6437
|
+
const blockHtml = blockItems.length === 0 ? '<p class="empty-note">No blocking findings.</p>' : blockItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
6438
|
+
let warnsHtml = "";
|
|
6334
6439
|
if (warnItems.length === 0) {
|
|
6335
|
-
|
|
6440
|
+
warnsHtml = '<p class="empty-note">No warnings.</p>';
|
|
6336
6441
|
} else {
|
|
6337
|
-
for (const cid of
|
|
6442
|
+
for (const cid of warnCheckOrder) {
|
|
6338
6443
|
const group = sortFindings(cwd, byCheck.get(cid));
|
|
6339
|
-
|
|
6340
|
-
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>`;
|
|
6444
|
+
warnsHtml += `<div class="group"><div class="group-label"><span class="group-name">${escapeHtml(cid)}</span><span class="group-count">${group.length}</span></div>${group.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n")}</div>`;
|
|
6341
6445
|
}
|
|
6342
6446
|
}
|
|
6343
|
-
const infoHtml = infoItems.length === 0 ? '<p class="empty-
|
|
6344
|
-
const
|
|
6345
|
-
const
|
|
6447
|
+
const infoHtml = infoItems.length === 0 ? '<p class="empty-note muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
6448
|
+
const prRow = pr && lines != null ? `<div class="meta-row"><span class="meta-label">PR size</span><span class="meta-value">${lines} lines (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files</span></div>` : "";
|
|
6449
|
+
const appendixHtml = llmAppendix?.trim() ? `<section class="section"><h2 class="section-title">Appendix</h2><pre class="raw-pre">${escapeHtml(llmAppendix.trim())}</pre></section>` : "";
|
|
6450
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16) + " UTC";
|
|
6346
6451
|
return `<!DOCTYPE html>
|
|
6347
6452
|
<html lang="en">
|
|
6348
6453
|
<head>
|
|
6349
6454
|
<meta charset="utf-8" />
|
|
6350
6455
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6351
|
-
<title>FrontGuard
|
|
6456
|
+
<title>FrontGuard Report</title>
|
|
6352
6457
|
<style>
|
|
6353
6458
|
:root {
|
|
6354
6459
|
--bg: #f8fafc;
|
|
6355
6460
|
--surface: #ffffff;
|
|
6356
|
-
--
|
|
6357
|
-
--muted: #64748b;
|
|
6461
|
+
--surface-alt: #f1f5f9;
|
|
6358
6462
|
--border: #e2e8f0;
|
|
6359
|
-
--
|
|
6360
|
-
--
|
|
6361
|
-
--
|
|
6362
|
-
--
|
|
6363
|
-
--
|
|
6364
|
-
--
|
|
6463
|
+
--border-light: #f1f5f9;
|
|
6464
|
+
--text: #0f172a;
|
|
6465
|
+
--text-secondary: #475569;
|
|
6466
|
+
--muted: #94a3b8;
|
|
6467
|
+
--accent: #6366f1;
|
|
6468
|
+
--c-block: #dc2626;
|
|
6469
|
+
--c-block-bg: #fef2f2;
|
|
6470
|
+
--c-block-border: #fecaca;
|
|
6471
|
+
--c-warn: #d97706;
|
|
6472
|
+
--c-warn-bg: #fffbeb;
|
|
6473
|
+
--c-warn-border: #fde68a;
|
|
6474
|
+
--c-info: #2563eb;
|
|
6475
|
+
--c-info-bg: #eff6ff;
|
|
6476
|
+
--c-info-border: #bfdbfe;
|
|
6477
|
+
--c-ok: #16a34a;
|
|
6478
|
+
--c-ok-bg: #f0fdf4;
|
|
6479
|
+
--radius-sm: 6px;
|
|
6365
6480
|
--radius: 10px;
|
|
6366
|
-
--
|
|
6481
|
+
--radius-lg: 14px;
|
|
6482
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,.05);
|
|
6483
|
+
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
|
|
6484
|
+
font-size: 15px;
|
|
6367
6485
|
}
|
|
6368
|
-
|
|
6486
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
6369
6487
|
body {
|
|
6370
|
-
|
|
6371
|
-
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
6488
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
6372
6489
|
background: var(--bg);
|
|
6373
6490
|
color: var(--text);
|
|
6374
|
-
line-height: 1.
|
|
6375
|
-
|
|
6376
|
-
padding: 2rem clamp(1rem, 4vw, 3rem) 4rem;
|
|
6377
|
-
max-width: 920px;
|
|
6378
|
-
margin-left: auto;
|
|
6379
|
-
margin-right: auto;
|
|
6380
|
-
}
|
|
6381
|
-
.hero {
|
|
6382
|
-
margin-bottom: 2rem;
|
|
6491
|
+
line-height: 1.6;
|
|
6492
|
+
padding: 2.5rem clamp(1rem, 5vw, 3rem) 5rem;
|
|
6383
6493
|
}
|
|
6494
|
+
.wrap { max-width: 860px; margin: 0 auto; }
|
|
6495
|
+
|
|
6496
|
+
/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6497
|
+
.header { margin-bottom: 2.5rem; }
|
|
6384
6498
|
.brand {
|
|
6385
|
-
font-size: 0.
|
|
6386
|
-
font-weight:
|
|
6499
|
+
font-size: 0.7rem;
|
|
6500
|
+
font-weight: 700;
|
|
6387
6501
|
letter-spacing: 0.12em;
|
|
6388
6502
|
text-transform: uppercase;
|
|
6389
6503
|
color: var(--muted);
|
|
6390
|
-
margin-bottom: 0.
|
|
6504
|
+
margin-bottom: 0.75rem;
|
|
6391
6505
|
}
|
|
6392
|
-
|
|
6393
|
-
font-size: 1.
|
|
6506
|
+
.page-title {
|
|
6507
|
+
font-size: 1.6rem;
|
|
6394
6508
|
font-weight: 700;
|
|
6395
6509
|
letter-spacing: -0.03em;
|
|
6396
|
-
margin: 0 0 1rem;
|
|
6397
6510
|
color: var(--text);
|
|
6511
|
+
margin-bottom: 1.25rem;
|
|
6512
|
+
line-height: 1.2;
|
|
6513
|
+
}
|
|
6514
|
+
|
|
6515
|
+
/* \u2500\u2500 Risk pill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6516
|
+
.risk-pill {
|
|
6517
|
+
display: inline-flex;
|
|
6518
|
+
align-items: center;
|
|
6519
|
+
gap: 0.45rem;
|
|
6520
|
+
padding: 0.35rem 0.8rem;
|
|
6521
|
+
border-radius: 999px;
|
|
6522
|
+
font-size: 0.82rem;
|
|
6523
|
+
font-weight: 700;
|
|
6524
|
+
letter-spacing: 0.02em;
|
|
6525
|
+
border: 1.5px solid;
|
|
6398
6526
|
}
|
|
6399
|
-
.
|
|
6527
|
+
.risk-low { color: var(--c-ok); background: var(--c-ok-bg); border-color: #bbf7d0; }
|
|
6528
|
+
.risk-med { color: var(--c-warn); background: var(--c-warn-bg); border-color: var(--c-warn-border); }
|
|
6529
|
+
.risk-high { color: var(--c-block); background: var(--c-block-bg); border-color: var(--c-block-border); }
|
|
6530
|
+
.risk-icon { font-size: 0.75rem; }
|
|
6531
|
+
|
|
6532
|
+
/* \u2500\u2500 Stat chips \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6533
|
+
.stats {
|
|
6400
6534
|
display: flex;
|
|
6401
6535
|
flex-wrap: wrap;
|
|
6402
|
-
gap: 0.
|
|
6403
|
-
margin-
|
|
6536
|
+
gap: 0.5rem;
|
|
6537
|
+
margin-top: 1rem;
|
|
6538
|
+
align-items: center;
|
|
6404
6539
|
}
|
|
6405
|
-
.
|
|
6540
|
+
.stat {
|
|
6541
|
+
display: flex;
|
|
6542
|
+
align-items: center;
|
|
6543
|
+
gap: 0.35rem;
|
|
6544
|
+
padding: 0.3rem 0.65rem;
|
|
6545
|
+
background: var(--surface);
|
|
6546
|
+
border: 1px solid var(--border);
|
|
6547
|
+
border-radius: var(--radius-sm);
|
|
6548
|
+
font-size: 0.78rem;
|
|
6549
|
+
box-shadow: var(--shadow-sm);
|
|
6550
|
+
}
|
|
6551
|
+
.stat-label { color: var(--text-secondary); }
|
|
6552
|
+
.stat-value { font-weight: 600; color: var(--text); }
|
|
6553
|
+
.stat-value.danger { color: var(--c-block); }
|
|
6554
|
+
.stat-value.caution { color: var(--c-warn); }
|
|
6555
|
+
.stat-value.ok { color: var(--c-ok); }
|
|
6556
|
+
|
|
6557
|
+
/* \u2500\u2500 Meta table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6558
|
+
.meta-box {
|
|
6406
6559
|
background: var(--surface);
|
|
6407
6560
|
border: 1px solid var(--border);
|
|
6408
6561
|
border-radius: var(--radius);
|
|
6409
|
-
|
|
6410
|
-
|
|
6562
|
+
box-shadow: var(--shadow-sm);
|
|
6563
|
+
overflow: hidden;
|
|
6564
|
+
margin-bottom: 2rem;
|
|
6565
|
+
}
|
|
6566
|
+
.meta-row {
|
|
6411
6567
|
display: flex;
|
|
6412
|
-
align-items:
|
|
6413
|
-
gap:
|
|
6568
|
+
align-items: baseline;
|
|
6569
|
+
gap: 1rem;
|
|
6570
|
+
padding: 0.6rem 1rem;
|
|
6571
|
+
border-bottom: 1px solid var(--border-light);
|
|
6572
|
+
font-size: 0.875rem;
|
|
6414
6573
|
}
|
|
6415
|
-
.
|
|
6416
|
-
.
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
font-size: 1rem;
|
|
6423
|
-
font-weight: 600;
|
|
6424
|
-
margin: 0 0 0.85rem;
|
|
6425
|
-
color: var(--text);
|
|
6426
|
-
letter-spacing: -0.02em;
|
|
6574
|
+
.meta-row:last-child { border-bottom: none; }
|
|
6575
|
+
.meta-label {
|
|
6576
|
+
flex-shrink: 0;
|
|
6577
|
+
width: 7rem;
|
|
6578
|
+
color: var(--text-secondary);
|
|
6579
|
+
font-size: 0.78rem;
|
|
6580
|
+
font-weight: 500;
|
|
6427
6581
|
}
|
|
6428
|
-
.
|
|
6582
|
+
.meta-value { color: var(--text); }
|
|
6583
|
+
|
|
6584
|
+
/* \u2500\u2500 Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6585
|
+
.section { margin-top: 2.5rem; }
|
|
6586
|
+
.section-title {
|
|
6587
|
+
font-size: 0.8rem;
|
|
6588
|
+
font-weight: 700;
|
|
6589
|
+
letter-spacing: 0.06em;
|
|
6590
|
+
text-transform: uppercase;
|
|
6591
|
+
color: var(--text-secondary);
|
|
6592
|
+
margin-bottom: 0.75rem;
|
|
6593
|
+
}
|
|
6594
|
+
|
|
6595
|
+
/* \u2500\u2500 Checks table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6596
|
+
.checks-table {
|
|
6429
6597
|
width: 100%;
|
|
6430
6598
|
border-collapse: collapse;
|
|
6431
|
-
font-size: 0.9rem;
|
|
6432
6599
|
background: var(--surface);
|
|
6600
|
+
border: 1px solid var(--border);
|
|
6433
6601
|
border-radius: var(--radius);
|
|
6434
6602
|
overflow: hidden;
|
|
6435
|
-
|
|
6436
|
-
|
|
6603
|
+
box-shadow: var(--shadow-sm);
|
|
6604
|
+
font-size: 0.855rem;
|
|
6437
6605
|
}
|
|
6438
|
-
.
|
|
6439
|
-
padding: 0.
|
|
6606
|
+
.checks-table thead th {
|
|
6607
|
+
padding: 0.5rem 0.75rem;
|
|
6440
6608
|
text-align: left;
|
|
6609
|
+
font-size: 0.7rem;
|
|
6610
|
+
font-weight: 600;
|
|
6611
|
+
letter-spacing: 0.06em;
|
|
6612
|
+
text-transform: uppercase;
|
|
6613
|
+
color: var(--muted);
|
|
6614
|
+
background: var(--surface-alt);
|
|
6441
6615
|
border-bottom: 1px solid var(--border);
|
|
6442
6616
|
}
|
|
6443
|
-
.
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6617
|
+
.checks-table tbody td {
|
|
6618
|
+
padding: 0.55rem 0.75rem;
|
|
6619
|
+
border-bottom: 1px solid var(--border-light);
|
|
6620
|
+
vertical-align: middle;
|
|
6621
|
+
}
|
|
6622
|
+
.checks-table tbody tr:last-child td { border-bottom: none; }
|
|
6623
|
+
.checks-table tbody tr:hover { background: #fafbfc; }
|
|
6624
|
+
.col-icon { width: 2.25rem; }
|
|
6625
|
+
.col-count, .col-time { color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
|
|
6626
|
+
.col-time { width: 5.5rem; }
|
|
6627
|
+
.col-count { width: 2.5rem; }
|
|
6628
|
+
|
|
6629
|
+
/* Status icons */
|
|
6630
|
+
.status-icon {
|
|
6631
|
+
display: inline-flex;
|
|
6632
|
+
align-items: center;
|
|
6633
|
+
justify-content: center;
|
|
6634
|
+
width: 1.3rem;
|
|
6635
|
+
height: 1.3rem;
|
|
6636
|
+
border-radius: 50%;
|
|
6637
|
+
font-size: 0.65rem;
|
|
6638
|
+
font-weight: 800;
|
|
6639
|
+
line-height: 1;
|
|
6449
6640
|
}
|
|
6450
|
-
|
|
6451
|
-
.
|
|
6641
|
+
.status-icon.pass { background: var(--c-ok-bg); color: var(--c-ok); border: 1.5px solid #bbf7d0; }
|
|
6642
|
+
.status-icon.block { background: var(--c-block-bg); color: var(--c-block); border: 1.5px solid var(--c-block-border); }
|
|
6643
|
+
.status-icon.warn { background: var(--c-warn-bg); color: var(--c-warn); border: 1.5px solid var(--c-warn-border); }
|
|
6644
|
+
.status-icon.skip { background: var(--surface-alt); color: var(--muted); border: 1.5px solid var(--border); font-size: 0.9rem; }
|
|
6645
|
+
|
|
6646
|
+
/* Check name + tooltip */
|
|
6647
|
+
.check-name { font-weight: 600; color: var(--text); cursor: default; }
|
|
6648
|
+
.check-tooltip {
|
|
6649
|
+
display: block;
|
|
6650
|
+
font-size: 0.75rem;
|
|
6651
|
+
color: var(--text-secondary);
|
|
6652
|
+
margin-top: 0.1rem;
|
|
6653
|
+
font-weight: 400;
|
|
6654
|
+
line-height: 1.4;
|
|
6655
|
+
}
|
|
6656
|
+
.status-text { font-weight: 500; }
|
|
6657
|
+
.status-text.ok { color: var(--c-ok); }
|
|
6658
|
+
.status-text.danger { color: var(--c-block); }
|
|
6659
|
+
.status-text.caution { color: var(--c-warn); }
|
|
6660
|
+
.status-text.muted { color: var(--muted); font-weight: 400; font-size: 0.8rem; }
|
|
6661
|
+
|
|
6662
|
+
/* \u2500\u2500 Findings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6663
|
+
.accordion {
|
|
6452
6664
|
background: var(--surface);
|
|
6453
6665
|
border: 1px solid var(--border);
|
|
6454
6666
|
border-radius: var(--radius);
|
|
6667
|
+
box-shadow: var(--shadow-sm);
|
|
6455
6668
|
margin-bottom: 0.65rem;
|
|
6456
|
-
|
|
6669
|
+
overflow: hidden;
|
|
6457
6670
|
}
|
|
6458
|
-
.
|
|
6459
|
-
cursor: pointer;
|
|
6460
|
-
padding: 0.85rem 1rem;
|
|
6461
|
-
list-style: none;
|
|
6671
|
+
.accordion-header {
|
|
6462
6672
|
display: flex;
|
|
6463
6673
|
align-items: center;
|
|
6464
6674
|
justify-content: space-between;
|
|
6675
|
+
padding: 0.8rem 1rem;
|
|
6676
|
+
cursor: pointer;
|
|
6677
|
+
user-select: none;
|
|
6678
|
+
list-style: none;
|
|
6465
6679
|
font-weight: 600;
|
|
6466
6680
|
font-size: 0.9rem;
|
|
6467
6681
|
}
|
|
6468
|
-
.
|
|
6469
|
-
|
|
6470
|
-
.
|
|
6471
|
-
.
|
|
6472
|
-
|
|
6473
|
-
font-
|
|
6474
|
-
|
|
6475
|
-
color: var(--muted);
|
|
6476
|
-
background: #f1f5f9;
|
|
6477
|
-
padding: 0.15rem 0.5rem;
|
|
6682
|
+
.accordion-header::-webkit-details-marker { display: none; }
|
|
6683
|
+
details[open] > .accordion-header { border-bottom: 1px solid var(--border-light); }
|
|
6684
|
+
.accordion-label { display: flex; align-items: center; gap: 0.6rem; }
|
|
6685
|
+
.acc-count {
|
|
6686
|
+
font-size: 0.72rem;
|
|
6687
|
+
font-weight: 600;
|
|
6688
|
+
padding: 0.15rem 0.55rem;
|
|
6478
6689
|
border-radius: 999px;
|
|
6690
|
+
background: var(--surface-alt);
|
|
6691
|
+
color: var(--text-secondary);
|
|
6692
|
+
border: 1px solid var(--border);
|
|
6479
6693
|
}
|
|
6694
|
+
.acc-count.danger { background: var(--c-block-bg); color: var(--c-block); border-color: var(--c-block-border); }
|
|
6695
|
+
.acc-count.caution { background: var(--c-warn-bg); color: var(--c-warn); border-color: var(--c-warn-border); }
|
|
6696
|
+
.accordion-body { padding: 0.85rem; display: flex; flex-direction: column; gap: 0.6rem; }
|
|
6697
|
+
|
|
6698
|
+
/* Warning groups */
|
|
6699
|
+
.group { margin-bottom: 0.25rem; }
|
|
6700
|
+
.group-label {
|
|
6701
|
+
display: flex;
|
|
6702
|
+
align-items: center;
|
|
6703
|
+
gap: 0.5rem;
|
|
6704
|
+
padding: 0.4rem 0;
|
|
6705
|
+
margin-bottom: 0.45rem;
|
|
6706
|
+
}
|
|
6707
|
+
.group-name { font-size: 0.78rem; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
6708
|
+
.group-count { font-size: 0.72rem; color: var(--muted); background: var(--surface-alt); padding: 0.1rem 0.45rem; border-radius: 999px; border: 1px solid var(--border); }
|
|
6709
|
+
|
|
6710
|
+
/* \u2500\u2500 Finding cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6480
6711
|
.card {
|
|
6481
6712
|
border: 1px solid var(--border);
|
|
6482
|
-
border-radius:
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
background: #fafafa;
|
|
6713
|
+
border-radius: var(--radius-sm);
|
|
6714
|
+
background: var(--surface);
|
|
6715
|
+
padding: 0.85rem 1rem;
|
|
6486
6716
|
}
|
|
6487
|
-
.card
|
|
6488
|
-
.card.sev-
|
|
6489
|
-
.card.sev-
|
|
6490
|
-
.card
|
|
6491
|
-
.
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6717
|
+
.card.sev-block { border-left: 3px solid var(--c-block); background: var(--c-block-bg); border-top-color: var(--c-block-border); border-right-color: var(--c-block-border); border-bottom-color: var(--c-block-border); }
|
|
6718
|
+
.card.sev-warn { border-left: 3px solid var(--c-warn); }
|
|
6719
|
+
.card.sev-info { border-left: 3px solid var(--c-info); }
|
|
6720
|
+
.card-header { display: flex; align-items: flex-start; gap: 0.6rem; margin-bottom: 0.55rem; }
|
|
6721
|
+
.sev-badge {
|
|
6722
|
+
flex-shrink: 0;
|
|
6723
|
+
font-size: 0.65rem;
|
|
6724
|
+
font-weight: 700;
|
|
6725
|
+
letter-spacing: 0.05em;
|
|
6726
|
+
text-transform: uppercase;
|
|
6727
|
+
padding: 0.15rem 0.45rem;
|
|
6728
|
+
border-radius: 4px;
|
|
6729
|
+
margin-top: 0.15rem;
|
|
6730
|
+
}
|
|
6731
|
+
.sev-badge.sev-block { background: var(--c-block-bg); color: var(--c-block); border: 1px solid var(--c-block-border); }
|
|
6732
|
+
.sev-badge.sev-warn { background: var(--c-warn-bg); color: var(--c-warn); border: 1px solid var(--c-warn-border); }
|
|
6733
|
+
.sev-badge.sev-info { background: var(--c-info-bg); color: var(--c-info); border: 1px solid var(--c-info-border); }
|
|
6734
|
+
.card-message { font-size: 0.88rem; line-height: 1.5; color: var(--text); }
|
|
6735
|
+
.card-meta { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
|
6736
|
+
.tag {
|
|
6737
|
+
font-size: 0.72rem;
|
|
6738
|
+
padding: 0.1rem 0.45rem;
|
|
6739
|
+
border-radius: 4px;
|
|
6740
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6741
|
+
white-space: nowrap;
|
|
6742
|
+
overflow: hidden;
|
|
6743
|
+
text-overflow: ellipsis;
|
|
6744
|
+
max-width: 40ch;
|
|
6745
|
+
}
|
|
6746
|
+
.tag-check { background: #f0f0ff; color: #4338ca; border: 1px solid #e0e7ff; }
|
|
6747
|
+
.tag-rule { background: var(--surface-alt); color: var(--text-secondary); border: 1px solid var(--border); }
|
|
6748
|
+
.tag-file { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; max-width: 55ch; }
|
|
6749
|
+
.card-hint { font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.5rem; line-height: 1.45; }
|
|
6750
|
+
|
|
6751
|
+
/* Code blocks */
|
|
6752
|
+
.code-block {
|
|
6753
|
+
margin-top: 0.6rem;
|
|
6754
|
+
padding: 0.65rem 0.85rem;
|
|
6755
|
+
background: #f8fafc;
|
|
6506
6756
|
border: 1px solid var(--border);
|
|
6757
|
+
border-radius: var(--radius-sm);
|
|
6758
|
+
overflow-x: auto;
|
|
6759
|
+
font-size: 0.75rem;
|
|
6760
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6761
|
+
line-height: 1.55;
|
|
6762
|
+
white-space: pre;
|
|
6763
|
+
color: var(--text);
|
|
6507
6764
|
}
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
.fix-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6765
|
+
|
|
6766
|
+
/* Fix box */
|
|
6767
|
+
.fix-box {
|
|
6768
|
+
margin-top: 0.75rem;
|
|
6769
|
+
padding: 0.7rem 0.85rem;
|
|
6770
|
+
background: #fafafa;
|
|
6771
|
+
border: 1px dashed var(--border);
|
|
6772
|
+
border-radius: var(--radius-sm);
|
|
6773
|
+
}
|
|
6774
|
+
.fix-header {
|
|
6775
|
+
font-size: 0.7rem;
|
|
6776
|
+
font-weight: 700;
|
|
6777
|
+
text-transform: uppercase;
|
|
6778
|
+
letter-spacing: 0.06em;
|
|
6779
|
+
color: var(--accent);
|
|
6780
|
+
margin-bottom: 0.4rem;
|
|
6781
|
+
display: flex;
|
|
6782
|
+
align-items: center;
|
|
6783
|
+
gap: 0.4rem;
|
|
6784
|
+
}
|
|
6785
|
+
.badge-llm {
|
|
6786
|
+
font-size: 0.6rem;
|
|
6787
|
+
padding: 0.1rem 0.35rem;
|
|
6788
|
+
background: #eef2ff;
|
|
6789
|
+
color: var(--accent);
|
|
6790
|
+
border-radius: 4px;
|
|
6791
|
+
border: 1px solid #e0e7ff;
|
|
6792
|
+
}
|
|
6793
|
+
.fix-summary { font-size: 0.82rem; color: var(--text); white-space: pre-wrap; }
|
|
6794
|
+
.fix-disclaimer { font-size: 0.7rem; color: var(--muted); margin-top: 0.5rem; }
|
|
6795
|
+
|
|
6796
|
+
/* Misc */
|
|
6797
|
+
.empty-note { font-size: 0.875rem; color: var(--muted); padding: 0.25rem 0; }
|
|
6798
|
+
.muted { color: var(--muted); }
|
|
6799
|
+
.raw-pre {
|
|
6515
6800
|
white-space: pre-wrap;
|
|
6516
|
-
font-size: 0.
|
|
6801
|
+
font-size: 0.82rem;
|
|
6517
6802
|
background: var(--surface);
|
|
6518
6803
|
padding: 1rem;
|
|
6519
6804
|
border-radius: var(--radius);
|
|
6520
6805
|
border: 1px solid var(--border);
|
|
6521
|
-
|
|
6806
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6807
|
+
line-height: 1.55;
|
|
6522
6808
|
}
|
|
6523
|
-
|
|
6524
|
-
|
|
6809
|
+
|
|
6810
|
+
/* \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6811
|
+
.footer {
|
|
6812
|
+
margin-top: 3.5rem;
|
|
6525
6813
|
padding-top: 1.25rem;
|
|
6526
6814
|
border-top: 1px solid var(--border);
|
|
6527
|
-
font-size: 0.
|
|
6815
|
+
font-size: 0.77rem;
|
|
6528
6816
|
color: var(--muted);
|
|
6817
|
+
display: flex;
|
|
6818
|
+
align-items: center;
|
|
6819
|
+
justify-content: space-between;
|
|
6820
|
+
flex-wrap: wrap;
|
|
6821
|
+
gap: 0.5rem;
|
|
6529
6822
|
}
|
|
6530
|
-
footer a { color: var(--accent); text-decoration: none; }
|
|
6531
|
-
footer a:hover { text-decoration: underline; }
|
|
6532
6823
|
</style>
|
|
6533
6824
|
</head>
|
|
6534
6825
|
<body>
|
|
6535
|
-
|
|
6826
|
+
<div class="wrap">
|
|
6827
|
+
|
|
6828
|
+
<header class="header">
|
|
6536
6829
|
<div class="brand">FrontGuard</div>
|
|
6537
|
-
<h1>Code review report</h1>
|
|
6538
|
-
<
|
|
6539
|
-
|
|
6540
|
-
<div class="
|
|
6541
|
-
<div class="
|
|
6542
|
-
<div class="
|
|
6543
|
-
<div class="
|
|
6830
|
+
<h1 class="page-title">Code review report</h1>
|
|
6831
|
+
<span class="risk-pill ${riskClass}"><span class="risk-icon">${riskIcon}</span> ${riskScore} risk</span>
|
|
6832
|
+
<div class="stats">
|
|
6833
|
+
<div class="stat"><span class="stat-label">Blocking</span><span class="stat-value ${blocks > 0 ? "danger" : "ok"}">${blocks}</span></div>
|
|
6834
|
+
<div class="stat"><span class="stat-label">Warnings</span><span class="stat-value ${warns > 0 ? "caution" : "ok"}">${warns}</span></div>
|
|
6835
|
+
<div class="stat"><span class="stat-label">Info</span><span class="stat-value">${infos}</span></div>
|
|
6836
|
+
<div class="stat"><span class="stat-label">Mode</span><span class="stat-value">${escapeHtml(modeLabel)}</span></div>
|
|
6544
6837
|
</div>
|
|
6545
6838
|
</header>
|
|
6546
6839
|
|
|
6547
|
-
<
|
|
6548
|
-
<
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
<tr><th>Stack</th><td>${escapeHtml(formatStackOneLiner(stack))}</td></tr>
|
|
6553
|
-
${prBlock}
|
|
6554
|
-
</table>
|
|
6555
|
-
</section>
|
|
6840
|
+
<div class="meta-box">
|
|
6841
|
+
<div class="meta-row"><span class="meta-label">Stack</span><span class="meta-value">${escapeHtml(formatStackOneLiner(stack))}</span></div>
|
|
6842
|
+
${prRow}
|
|
6843
|
+
<div class="meta-row"><span class="meta-label">Gate mode</span><span class="meta-value">${escapeHtml(modeLabel)}</span></div>
|
|
6844
|
+
</div>
|
|
6556
6845
|
|
|
6557
6846
|
<section class="section">
|
|
6558
|
-
<h2 class="
|
|
6559
|
-
<table class="
|
|
6560
|
-
<thead
|
|
6561
|
-
|
|
6847
|
+
<h2 class="section-title">Checks</h2>
|
|
6848
|
+
<table class="checks-table">
|
|
6849
|
+
<thead>
|
|
6850
|
+
<tr>
|
|
6851
|
+
<th class="col-icon"></th>
|
|
6852
|
+
<th>Check</th>
|
|
6853
|
+
<th>Status</th>
|
|
6854
|
+
<th class="col-count">#</th>
|
|
6855
|
+
<th class="col-time">Time</th>
|
|
6856
|
+
</tr>
|
|
6857
|
+
</thead>
|
|
6858
|
+
<tbody>
|
|
6859
|
+
${checkRows}
|
|
6860
|
+
</tbody>
|
|
6562
6861
|
</table>
|
|
6563
6862
|
</section>
|
|
6564
6863
|
|
|
6565
6864
|
<section class="section">
|
|
6566
|
-
<h2 class="
|
|
6567
|
-
|
|
6568
|
-
<details class="
|
|
6569
|
-
|
|
6865
|
+
<h2 class="section-title">Findings</h2>
|
|
6866
|
+
|
|
6867
|
+
<details class="accordion" ${blocks > 0 ? "open" : ""}>
|
|
6868
|
+
<summary class="accordion-header">
|
|
6869
|
+
<span class="accordion-label">Blocking <span class="acc-count ${blocks > 0 ? "danger" : ""}">${blocks}</span></span>
|
|
6870
|
+
</summary>
|
|
6871
|
+
<div class="accordion-body">${blockHtml}</div>
|
|
6872
|
+
</details>
|
|
6873
|
+
|
|
6874
|
+
<details class="accordion" ${warns > 0 && blocks === 0 ? "open" : ""}>
|
|
6875
|
+
<summary class="accordion-header">
|
|
6876
|
+
<span class="accordion-label">Warnings <span class="acc-count ${warns > 0 ? "caution" : ""}">${warns}</span></span>
|
|
6877
|
+
</summary>
|
|
6878
|
+
<div class="accordion-body">${warnsHtml}</div>
|
|
6879
|
+
</details>
|
|
6880
|
+
|
|
6881
|
+
<details class="accordion">
|
|
6882
|
+
<summary class="accordion-header">
|
|
6883
|
+
<span class="accordion-label">Info <span class="acc-count">${infos}</span></span>
|
|
6884
|
+
</summary>
|
|
6885
|
+
<div class="accordion-body">${infoHtml}</div>
|
|
6886
|
+
</details>
|
|
6570
6887
|
</section>
|
|
6571
6888
|
|
|
6572
|
-
${
|
|
6889
|
+
${appendixHtml}
|
|
6573
6890
|
|
|
6574
|
-
<footer>
|
|
6575
|
-
<
|
|
6891
|
+
<footer class="footer">
|
|
6892
|
+
<span>Generated by <strong>FrontGuard</strong> \xB7 ${ts2}</span>
|
|
6893
|
+
<span>Open in any browser \xB7 static report</span>
|
|
6576
6894
|
</footer>
|
|
6895
|
+
|
|
6896
|
+
</div>
|
|
6577
6897
|
</body>
|
|
6578
6898
|
</html>`;
|
|
6579
6899
|
}
|
|
@@ -6772,76 +7092,52 @@ function formatMarkdown(p2) {
|
|
|
6772
7092
|
if (lineA !== lineB) return lineA - lineB;
|
|
6773
7093
|
return a3.f.message.localeCompare(b3.f.message);
|
|
6774
7094
|
});
|
|
6775
|
-
sb.push("##
|
|
7095
|
+
sb.push("## FrontGuard report");
|
|
6776
7096
|
sb.push("");
|
|
6777
7097
|
const modeLabel = mode === "enforce" ? "enforce" : "warn only";
|
|
6778
7098
|
const badgeLine = [
|
|
6779
|
-
mdShield("Risk
|
|
6780
|
-
mdShield("
|
|
7099
|
+
mdShield("Risk", "risk", riskScore, riskShieldColor(riskScore)),
|
|
7100
|
+
mdShield("Mode", "mode", modeLabel, modeShieldColor(mode)),
|
|
6781
7101
|
mdShield("Blocking", "blocking", String(blocks), countShieldColor("block", blocks)),
|
|
6782
7102
|
mdShield("Warnings", "warnings", String(warns), countShieldColor("warn", warns)),
|
|
6783
|
-
mdShield("Info", "info
|
|
7103
|
+
mdShield("Info", "info", String(infos), countShieldColor("info", infos))
|
|
6784
7104
|
].join(" ");
|
|
6785
7105
|
sb.push(badgeLine);
|
|
6786
7106
|
sb.push("");
|
|
6787
|
-
sb.push("---");
|
|
6788
|
-
sb.push("");
|
|
6789
|
-
sb.push("### \u{1F4CC} Snapshot");
|
|
6790
|
-
sb.push("");
|
|
6791
7107
|
sb.push("| | |");
|
|
6792
7108
|
sb.push("|:--|:--|");
|
|
6793
|
-
sb.push(`|
|
|
6794
|
-
sb.push(
|
|
6795
|
-
|
|
6796
|
-
);
|
|
6797
|
-
sb.push(`| **Stack detected** | ${formatStackOneLiner(stack)} |`);
|
|
7109
|
+
sb.push(`| Risk | ${riskEmoji(riskScore)} **${riskScore}** |`);
|
|
7110
|
+
sb.push(`| Mode | ${mode === "enforce" ? "\u{1F512} Enforce" : "Warn only"} |`);
|
|
7111
|
+
sb.push(`| Stack | ${formatStackOneLiner(stack)} |`);
|
|
6798
7112
|
if (pr && lines != null) {
|
|
6799
|
-
sb.push(
|
|
6800
|
-
`| **PR size** | \u{1F4CF} **${lines}** LOC ( +${pr.additions} / \u2212${pr.deletions} ) \xB7 **${pr.changedFiles}** files |`
|
|
6801
|
-
);
|
|
7113
|
+
sb.push(`| PR size | ${lines} lines (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files |`);
|
|
6802
7114
|
}
|
|
6803
7115
|
sb.push("");
|
|
6804
7116
|
if (showAiAssistedBanner && pr?.aiAssisted) {
|
|
6805
7117
|
sb.push(
|
|
6806
|
-
">
|
|
7118
|
+
"> **AI-assisted PR** \u2014 stricter static checks active (security, secrets, `any` deltas may escalate)."
|
|
6807
7119
|
);
|
|
6808
7120
|
sb.push("");
|
|
6809
7121
|
}
|
|
6810
|
-
sb.push("> **How to read this report**");
|
|
6811
|
-
sb.push(">");
|
|
6812
|
-
sb.push("> | Symbol | Meaning |");
|
|
6813
|
-
sb.push("> |:--|:--|");
|
|
6814
|
-
sb.push("> | \u{1F7E2} | Check passed or skipped cleanly |");
|
|
6815
|
-
sb.push("> | \u{1F7E1} | Warnings only \u2014 review recommended |");
|
|
6816
|
-
sb.push("> | \u{1F534} | Blocking (`block`) severity present |");
|
|
6817
|
-
sb.push("> | \u23ED\uFE0F | Check skipped (see reason in table) |");
|
|
6818
|
-
sb.push(">");
|
|
6819
|
-
sb.push(
|
|
6820
|
-
"> Paths in findings are **relative to the repo root**. Each issue below has a small field table and optional detail."
|
|
6821
|
-
);
|
|
6822
|
-
sb.push("");
|
|
6823
7122
|
sb.push("---");
|
|
6824
7123
|
sb.push("");
|
|
6825
|
-
sb.push("###
|
|
7124
|
+
sb.push("### Checks");
|
|
6826
7125
|
sb.push("");
|
|
6827
|
-
sb.push("| | Check |
|
|
6828
|
-
sb.push("
|
|
7126
|
+
sb.push("| | Check | Issues | Time |");
|
|
7127
|
+
sb.push("|:--:|:--|:-:|--:|");
|
|
6829
7128
|
for (const r4 of results) {
|
|
6830
7129
|
const he2 = healthEmojiForCheck(r4);
|
|
6831
7130
|
let status;
|
|
6832
7131
|
if (r4.skipped) {
|
|
6833
7132
|
const why = r4.skipped.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
6834
|
-
const short = why.length >
|
|
6835
|
-
status = `\
|
|
7133
|
+
const short = why.length > 80 ? `${why.slice(0, 77)}\u2026` : why;
|
|
7134
|
+
status = `\u2014 _${short}_`;
|
|
6836
7135
|
} else if (r4.findings.length === 0) {
|
|
6837
|
-
status = "\
|
|
7136
|
+
status = "\u2014";
|
|
6838
7137
|
} else {
|
|
6839
|
-
status =
|
|
7138
|
+
status = String(r4.findings.length);
|
|
6840
7139
|
}
|
|
6841
|
-
|
|
6842
|
-
sb.push(
|
|
6843
|
-
`| ${he2} | **${r4.checkId}** | ${status} | **${nFind}** | ${formatDuration2(r4.durationMs)} |`
|
|
6844
|
-
);
|
|
7140
|
+
sb.push(`| ${he2} | \`${r4.checkId}\` | ${status} | ${formatDuration2(r4.durationMs)} |`);
|
|
6845
7141
|
}
|
|
6846
7142
|
sb.push("");
|
|
6847
7143
|
const blockFindings = sortWithCwd(
|
|
@@ -6849,10 +7145,10 @@ function formatMarkdown(p2) {
|
|
|
6849
7145
|
(r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))
|
|
6850
7146
|
)
|
|
6851
7147
|
);
|
|
6852
|
-
sb.push(`###
|
|
7148
|
+
sb.push(`### Blocking \u2014 ${blocks} issue${blocks === 1 ? "" : "s"}`);
|
|
6853
7149
|
sb.push("");
|
|
6854
7150
|
if (blockFindings.length === 0) {
|
|
6855
|
-
sb.push("
|
|
7151
|
+
sb.push("_No blocking findings._");
|
|
6856
7152
|
} else {
|
|
6857
7153
|
for (const { r: r4, f: f4 } of blockFindings) {
|
|
6858
7154
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -6860,11 +7156,11 @@ function formatMarkdown(p2) {
|
|
|
6860
7156
|
sb.push("");
|
|
6861
7157
|
sb.push(`#### ${findingTitleLine(d3.file, d3.message)}`);
|
|
6862
7158
|
sb.push("");
|
|
6863
|
-
sb.push(`|
|
|
7159
|
+
sb.push(`| | |`);
|
|
6864
7160
|
sb.push(`|:--|:--|`);
|
|
6865
|
-
sb.push(`|
|
|
6866
|
-
sb.push(`|
|
|
6867
|
-
if (d3.file) sb.push(`|
|
|
7161
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7162
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7163
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
6868
7164
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
6869
7165
|
appendSuggestedFix(sb, cwd, f4);
|
|
6870
7166
|
sb.push("");
|
|
@@ -6876,12 +7172,10 @@ function formatMarkdown(p2) {
|
|
|
6876
7172
|
(r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))
|
|
6877
7173
|
)
|
|
6878
7174
|
);
|
|
6879
|
-
sb.push(
|
|
6880
|
-
`### \u26A0\uFE0F Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"} (by check)`
|
|
6881
|
-
);
|
|
7175
|
+
sb.push(`### Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"}`);
|
|
6882
7176
|
sb.push("");
|
|
6883
7177
|
if (warnFindings.length === 0) {
|
|
6884
|
-
sb.push("
|
|
7178
|
+
sb.push("_No warnings._");
|
|
6885
7179
|
} else {
|
|
6886
7180
|
const byCheck = /* @__PURE__ */ new Map();
|
|
6887
7181
|
for (const item of warnFindings) {
|
|
@@ -6892,7 +7186,7 @@ function formatMarkdown(p2) {
|
|
|
6892
7186
|
const checkOrder = [...byCheck.keys()].sort((a3, b3) => a3.localeCompare(b3));
|
|
6893
7187
|
for (const checkId of checkOrder) {
|
|
6894
7188
|
const group = sortWithCwd(byCheck.get(checkId));
|
|
6895
|
-
sb.push(`####
|
|
7189
|
+
sb.push(`#### \`${checkId}\` \xB7 ${group.length} finding${group.length === 1 ? "" : "s"}`);
|
|
6896
7190
|
sb.push("");
|
|
6897
7191
|
for (const { r: r4, f: f4 } of group) {
|
|
6898
7192
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -6900,11 +7194,11 @@ function formatMarkdown(p2) {
|
|
|
6900
7194
|
sb.push("");
|
|
6901
7195
|
sb.push(`##### ${findingTitleLine(d3.file, d3.message)}`);
|
|
6902
7196
|
sb.push("");
|
|
6903
|
-
sb.push(`|
|
|
7197
|
+
sb.push(`| | |`);
|
|
6904
7198
|
sb.push(`|:--|:--|`);
|
|
6905
|
-
sb.push(`|
|
|
6906
|
-
sb.push(`|
|
|
6907
|
-
if (d3.file) sb.push(`|
|
|
7199
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7200
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7201
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
6908
7202
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
6909
7203
|
appendSuggestedFix(sb, cwd, f4);
|
|
6910
7204
|
sb.push("");
|
|
@@ -6917,10 +7211,10 @@ function formatMarkdown(p2) {
|
|
|
6917
7211
|
(r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))
|
|
6918
7212
|
)
|
|
6919
7213
|
);
|
|
6920
|
-
sb.push(`###
|
|
7214
|
+
sb.push(`### Info \u2014 ${infos} item${infos === 1 ? "" : "s"}`);
|
|
6921
7215
|
sb.push("");
|
|
6922
7216
|
if (infoFindings.length === 0) {
|
|
6923
|
-
sb.push("
|
|
7217
|
+
sb.push("_No info notes._");
|
|
6924
7218
|
} else {
|
|
6925
7219
|
for (const { r: r4, f: f4 } of infoFindings) {
|
|
6926
7220
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -6928,11 +7222,11 @@ function formatMarkdown(p2) {
|
|
|
6928
7222
|
sb.push("");
|
|
6929
7223
|
sb.push(`#### ${findingTitleLine(d3.file, d3.message)}`);
|
|
6930
7224
|
sb.push("");
|
|
6931
|
-
sb.push(`|
|
|
7225
|
+
sb.push(`| | |`);
|
|
6932
7226
|
sb.push(`|:--|:--|`);
|
|
6933
|
-
sb.push(`|
|
|
6934
|
-
sb.push(`|
|
|
6935
|
-
if (d3.file) sb.push(`|
|
|
7227
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7228
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7229
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
6936
7230
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
6937
7231
|
appendSuggestedFix(sb, cwd, f4);
|
|
6938
7232
|
sb.push("");
|
|
@@ -6940,7 +7234,7 @@ function formatMarkdown(p2) {
|
|
|
6940
7234
|
}
|
|
6941
7235
|
sb.push("");
|
|
6942
7236
|
if (llmAppendix?.trim()) {
|
|
6943
|
-
sb.push("###
|
|
7237
|
+
sb.push("### Appendix");
|
|
6944
7238
|
sb.push("");
|
|
6945
7239
|
sb.push(llmAppendix.trim());
|
|
6946
7240
|
sb.push("");
|
|
@@ -6948,40 +7242,45 @@ function formatMarkdown(p2) {
|
|
|
6948
7242
|
sb.push("---");
|
|
6949
7243
|
sb.push("");
|
|
6950
7244
|
sb.push(
|
|
6951
|
-
|
|
6952
|
-
"Generated by",
|
|
6953
|
-
"report",
|
|
6954
|
-
"FrontGuard",
|
|
6955
|
-
"blueviolet"
|
|
6956
|
-
)
|
|
6957
|
-
);
|
|
6958
|
-
sb.push("");
|
|
6959
|
-
sb.push(
|
|
6960
|
-
"_Configure checks in `frontguard.config.mjs` (or `.cjs` / `.js`) \xB7 [Shields.io](https://shields.io) badge images load in Bitbucket PR comments (HTML tags like `<details>` are not supported there)._"
|
|
7245
|
+
"_Report by [FrontGuard](https://github.com/flipkart-incubator/frontguard) \xB7 Configure in `frontguard.config.mjs`_"
|
|
6961
7246
|
);
|
|
6962
7247
|
return sb.join("\n");
|
|
6963
7248
|
}
|
|
6964
7249
|
function formatConsole(p2) {
|
|
6965
7250
|
const { riskScore, mode, stack, pr, results, warns, infos, blocks } = p2;
|
|
7251
|
+
const W3 = 56;
|
|
7252
|
+
const pad = (s3, n3) => s3.slice(0, n3).padEnd(n3);
|
|
7253
|
+
const row = (label, val) => `\u2502 ${import_picocolors.default.dim(label.padEnd(7))} ${pad(val, W3 - 10)} \u2502`;
|
|
7254
|
+
const riskColored = riskScore === "HIGH" ? import_picocolors.default.red(import_picocolors.default.bold(riskScore)) : riskScore === "MEDIUM" ? import_picocolors.default.yellow(import_picocolors.default.bold(riskScore)) : import_picocolors.default.green(import_picocolors.default.bold(riskScore));
|
|
7255
|
+
const stackStr = [
|
|
7256
|
+
stack.hasNext && "Next.js",
|
|
7257
|
+
stack.hasReactNative && "React Native",
|
|
7258
|
+
!stack.hasNext && !stack.hasReactNative && stack.hasReact && "React",
|
|
7259
|
+
stack.hasTypeScript ? "TypeScript" : "JavaScript",
|
|
7260
|
+
stack.isMonorepo && "monorepo"
|
|
7261
|
+
].filter(Boolean).join(" \xB7 ");
|
|
6966
7262
|
const lines = [];
|
|
6967
|
-
|
|
6968
|
-
lines.push(
|
|
6969
|
-
lines.push(
|
|
6970
|
-
lines.push(
|
|
6971
|
-
|
|
6972
|
-
);
|
|
7263
|
+
const border = "\u2500".repeat(W3);
|
|
7264
|
+
lines.push(import_picocolors.default.dim(`\u250C${border}\u2510`));
|
|
7265
|
+
lines.push(row("Risk", riskColored));
|
|
7266
|
+
lines.push(row("Mode", mode === "enforce" ? import_picocolors.default.yellow("enforce") : "warn only"));
|
|
7267
|
+
lines.push(row("Stack", stackStr));
|
|
6973
7268
|
if (pr) {
|
|
6974
|
-
const sz = `${pr.additions + pr.deletions} lines
|
|
6975
|
-
lines.push(
|
|
7269
|
+
const sz = `${pr.additions + pr.deletions} lines +${pr.additions} / -${pr.deletions} \xB7 ${pr.changedFiles} files`;
|
|
7270
|
+
lines.push(row("PR", sz));
|
|
6976
7271
|
}
|
|
6977
|
-
lines.push(
|
|
6978
|
-
const statusLine = blocks > 0 ? import_picocolors.default.red(`\
|
|
6979
|
-
lines.push(`\u2502 ${statusLine
|
|
7272
|
+
lines.push(import_picocolors.default.dim(`\u251C${border}\u2524`));
|
|
7273
|
+
const statusLine = blocks > 0 ? import_picocolors.default.red(`\u2715 ${blocks} blocking \xB7 ${warns} warnings \xB7 ${infos} info`) : warns > 0 ? import_picocolors.default.yellow(`! ${warns} warnings \xB7 ${infos} info`) : import_picocolors.default.green("\u2713 All checks passed");
|
|
7274
|
+
lines.push(`\u2502 ${pad(statusLine, W3 - 2)} \u2502`);
|
|
7275
|
+
lines.push(import_picocolors.default.dim(`\u251C${border}\u2524`));
|
|
6980
7276
|
for (const r4 of results) {
|
|
6981
|
-
const
|
|
6982
|
-
|
|
7277
|
+
const icon = r4.skipped ? import_picocolors.default.dim("\u2013") : r4.findings.length === 0 ? import_picocolors.default.green("\u2713") : r4.findings.some((f4) => f4.severity === "block") ? import_picocolors.default.red("\u2715") : import_picocolors.default.yellow("!");
|
|
7278
|
+
const countStr = r4.skipped ? import_picocolors.default.dim("skipped") : r4.findings.length === 0 ? import_picocolors.default.dim("pass") : r4.findings.length === 1 ? `${r4.findings.length} issue` : `${r4.findings.length} issues`;
|
|
7279
|
+
const name = r4.checkId.padEnd(22);
|
|
7280
|
+
const label = ` ${icon} ${name} ${countStr}`;
|
|
7281
|
+
lines.push(`\u2502${pad(label, W3 + 1)}\u2502`);
|
|
6983
7282
|
}
|
|
6984
|
-
lines.push(import_picocolors.default.
|
|
7283
|
+
lines.push(import_picocolors.default.dim(`\u2514${border}\u2518`));
|
|
6985
7284
|
return lines.join("\n");
|
|
6986
7285
|
}
|
|
6987
7286
|
|
|
@@ -7385,6 +7684,7 @@ async function runFrontGuard(opts) {
|
|
|
7385
7684
|
cycles,
|
|
7386
7685
|
deadCode,
|
|
7387
7686
|
coreWebVitals,
|
|
7687
|
+
reactNative,
|
|
7388
7688
|
tsAnyDelta,
|
|
7389
7689
|
customRules,
|
|
7390
7690
|
aiStrict
|
|
@@ -7396,6 +7696,7 @@ async function runFrontGuard(opts) {
|
|
|
7396
7696
|
runCycles(opts.cwd, config, stack, pr),
|
|
7397
7697
|
runDeadCode(opts.cwd, config, stack, pr),
|
|
7398
7698
|
runCoreWebVitals(opts.cwd, config, stack, pr),
|
|
7699
|
+
runReactNative(opts.cwd, config, stack, pr),
|
|
7399
7700
|
runTsAnyDelta(opts.cwd, config, stack),
|
|
7400
7701
|
runCustomRules(opts.cwd, config, restrictFiles),
|
|
7401
7702
|
runAiAssistedStrict(opts.cwd, config, pr)
|
|
@@ -7412,6 +7713,7 @@ async function runFrontGuard(opts) {
|
|
|
7412
7713
|
deadCode,
|
|
7413
7714
|
bundle,
|
|
7414
7715
|
coreWebVitals,
|
|
7716
|
+
reactNative,
|
|
7415
7717
|
aiStrict,
|
|
7416
7718
|
prHygiene,
|
|
7417
7719
|
prSize,
|