@cleartrip/frontguard 0.3.6 → 1.0.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/README.md +221 -193
- package/dist/cli.js +677 -560
- package/dist/cli.js.map +1 -1
- package/package.json +11 -6
package/dist/cli.js
CHANGED
|
@@ -2402,199 +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
|
-
//
|
|
2522
|
-
// reactNative: {
|
|
2523
|
-
// enabled: true,
|
|
2524
|
-
// gate: 'info',
|
|
2525
|
-
// requireMetroConfig: true,
|
|
2526
|
-
// runAlignDeps: false, // npx @rnx-kit/align-deps (registry access)
|
|
2527
|
-
// alignDepsArgs: ['--requirements', 'react-native'],
|
|
2528
|
-
// runDoctor: false, // local react-native doctor (slow / env-specific)
|
|
2529
|
-
// swiftLintOnChangedSwift: true,
|
|
2530
|
-
// hintAndroidNativeOnPr: true,
|
|
2531
|
-
// },
|
|
2532
|
-
|
|
2533
|
-
// \u2500\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
|
|
2534
|
-
// Measures bundle after build and compares to a baseline.
|
|
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.
|
|
2535
2528
|
//
|
|
2536
|
-
//
|
|
2537
|
-
// 'auto' \u2014 auto-
|
|
2538
|
-
// 'next' \u2014
|
|
2539
|
-
// 'vite' \u2014
|
|
2540
|
-
// 'cra' \u2014
|
|
2541
|
-
// 'glob' \u2014
|
|
2542
|
-
// '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
|
|
2543
2536
|
//
|
|
2544
2537
|
// bundle: {
|
|
2545
2538
|
// enabled: true,
|
|
2546
2539
|
// gate: 'warn',
|
|
2547
|
-
//
|
|
2548
|
-
//
|
|
2549
|
-
//
|
|
2550
|
-
// 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'
|
|
2551
2543
|
// measureGlobs: ['dist/**/*', 'build/static/**/*', '.next/static/**/*'],
|
|
2552
|
-
// baselinePath: '.frontguard/bundle-baseline.json',
|
|
2553
|
-
// baselineRef: 'main',
|
|
2554
|
-
// maxDeltaBytes: 50_000,
|
|
2555
|
-
// 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
|
|
2556
2549
|
// },
|
|
2557
2550
|
|
|
2558
|
-
// \u2500\u2500
|
|
2559
|
-
// 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
|
+
//
|
|
2560
2555
|
// coreWebVitals: {
|
|
2561
2556
|
// enabled: true,
|
|
2562
2557
|
// gate: 'warn',
|
|
2563
2558
|
// scanGlobs: ['app/**/*.{tsx,jsx}', 'pages/**/*.{tsx,jsx}', 'src/**/*.{tsx,jsx}'],
|
|
2564
|
-
// maxFileBytes: 400_000,
|
|
2565
2559
|
// },
|
|
2566
2560
|
|
|
2567
|
-
// \u2500\u2500
|
|
2568
|
-
//
|
|
2569
|
-
//
|
|
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: {
|
|
2570
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,
|
|
2571
2582
|
// gate: 'warn',
|
|
2572
|
-
// // 'decorator'
|
|
2573
|
-
// // 'pr-disclosure'
|
|
2574
|
-
// // '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
|
|
2575
2586
|
// strictScanMode: 'both',
|
|
2576
2587
|
// escalate: {
|
|
2577
|
-
// secretFindingsToBlock: true,
|
|
2578
|
-
// 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
|
|
2579
2590
|
// },
|
|
2580
2591
|
// },
|
|
2581
2592
|
|
|
2582
|
-
// \u2500\u2500
|
|
2583
|
-
//
|
|
2584
|
-
//
|
|
2585
|
-
//
|
|
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
|
+
//
|
|
2586
2597
|
// llm: {
|
|
2587
|
-
// enabled:
|
|
2588
|
-
// provider: 'ollama',
|
|
2589
|
-
// model: 'llama3.2',
|
|
2590
|
-
//
|
|
2591
|
-
//
|
|
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
|
|
2592
2603
|
// maxDiffChars: 48_000,
|
|
2593
2604
|
// timeoutMs: 60_000,
|
|
2594
|
-
// perFindingFixes: false,
|
|
2595
|
-
// maxFixSuggestions: 12,
|
|
2596
|
-
// maxFileContextChars: 24_000,
|
|
2605
|
+
// perFindingFixes: false, // ask for fix suggestions per finding (slower)
|
|
2597
2606
|
// },
|
|
2607
|
+
|
|
2598
2608
|
},
|
|
2599
2609
|
})
|
|
2600
2610
|
`;
|
|
@@ -2618,23 +2628,18 @@ If **Yes**, list tools and what they touched (helps reviewers run a stricter fir
|
|
|
2618
2628
|
## AI assistance (optional detail)
|
|
2619
2629
|
- [ ] I have reviewed every AI-suggested line for security, auth, and product correctness
|
|
2620
2630
|
`;
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
-
|
|
2633
|
-
yarn frontguard run --markdown \\
|
|
2634
|
-
--htmlOut frontguard-report.html \\
|
|
2635
|
-
--prCommentOut frontguard-pr-comment.partial.md \\
|
|
2636
|
-
> frontguard-report.md
|
|
2637
|
-
- test -n "\${BITBUCKET_ACCESS_TOKEN:-}" || { echo "Missing secured var BITBUCKET_ACCESS_TOKEN"; exit 1; }
|
|
2631
|
+
async function detectPackageManager(cwd) {
|
|
2632
|
+
const [hasYarnLock, hasPnpmLock, hasNpmLock] = await Promise.all([
|
|
2633
|
+
fileExists(path6.join(cwd, "yarn.lock")),
|
|
2634
|
+
fileExists(path6.join(cwd, "pnpm-lock.yaml")),
|
|
2635
|
+
fileExists(path6.join(cwd, "package-lock.json"))
|
|
2636
|
+
]);
|
|
2637
|
+
if (hasYarnLock) return "yarn";
|
|
2638
|
+
if (hasPnpmLock) return "pnpm";
|
|
2639
|
+
if (hasNpmLock) return "npm";
|
|
2640
|
+
return "npm";
|
|
2641
|
+
}
|
|
2642
|
+
var UPLOAD_AND_COMMENT_SCRIPT = ` - test -n "\${BITBUCKET_ACCESS_TOKEN:-}" || { echo "Missing secured var BITBUCKET_ACCESS_TOKEN"; exit 1; }
|
|
2638
2643
|
- |
|
|
2639
2644
|
python3 << 'PY'
|
|
2640
2645
|
import json
|
|
@@ -2690,20 +2695,70 @@ var FRONTGUARD_STEP_YAML = ` - step:
|
|
|
2690
2695
|
--header 'Content-Type: application/json' \\
|
|
2691
2696
|
--header "Authorization: Bearer \${BITBUCKET_ACCESS_TOKEN}" \\
|
|
2692
2697
|
--data @frontguard-payload.json`;
|
|
2698
|
+
function buildFrontGuardStepYaml(pm) {
|
|
2699
|
+
const installLines = [];
|
|
2700
|
+
const runLine = [];
|
|
2701
|
+
if (pm === "yarn") {
|
|
2702
|
+
installLines.push(
|
|
2703
|
+
" - corepack enable",
|
|
2704
|
+
" - yarn install --immutable || YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install"
|
|
2705
|
+
);
|
|
2706
|
+
runLine.push(
|
|
2707
|
+
" - |",
|
|
2708
|
+
" yarn frontguard run --markdown \\",
|
|
2709
|
+
" --htmlOut frontguard-report.html \\",
|
|
2710
|
+
" --prCommentOut frontguard-pr-comment.partial.md \\",
|
|
2711
|
+
" > frontguard-report.md"
|
|
2712
|
+
);
|
|
2713
|
+
} else if (pm === "pnpm") {
|
|
2714
|
+
installLines.push(
|
|
2715
|
+
" - corepack enable",
|
|
2716
|
+
" - pnpm install --frozen-lockfile || pnpm install"
|
|
2717
|
+
);
|
|
2718
|
+
runLine.push(
|
|
2719
|
+
" - |",
|
|
2720
|
+
" pnpm exec frontguard run --markdown \\",
|
|
2721
|
+
" --htmlOut frontguard-report.html \\",
|
|
2722
|
+
" --prCommentOut frontguard-pr-comment.partial.md \\",
|
|
2723
|
+
" > frontguard-report.md"
|
|
2724
|
+
);
|
|
2725
|
+
} else {
|
|
2726
|
+
installLines.push(" - npm ci || npm install");
|
|
2727
|
+
runLine.push(
|
|
2728
|
+
" - |",
|
|
2729
|
+
" npx frontguard run --markdown \\",
|
|
2730
|
+
" --htmlOut frontguard-report.html \\",
|
|
2731
|
+
" --prCommentOut frontguard-pr-comment.partial.md \\",
|
|
2732
|
+
" > frontguard-report.md"
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
return ` - step:
|
|
2736
|
+
name: FrontGuard \u2014 report + PR comment
|
|
2737
|
+
caches:
|
|
2738
|
+
- node
|
|
2739
|
+
artifacts:
|
|
2740
|
+
- frontguard-report.html
|
|
2741
|
+
- frontguard-report.md
|
|
2742
|
+
- frontguard-pr-comment.partial.md
|
|
2743
|
+
script:
|
|
2744
|
+
${installLines.join("\n")}
|
|
2745
|
+
${runLine.join("\n")}
|
|
2746
|
+
${UPLOAD_AND_COMMENT_SCRIPT}`;
|
|
2747
|
+
}
|
|
2693
2748
|
var FRONTGUARD_MARKER = "FrontGuard";
|
|
2694
2749
|
function hasFrontGuardStep(content) {
|
|
2695
2750
|
return content.includes(FRONTGUARD_MARKER);
|
|
2696
2751
|
}
|
|
2697
|
-
function mergePipelineStep(existing) {
|
|
2752
|
+
function mergePipelineStep(existing, stepYaml) {
|
|
2698
2753
|
if (hasFrontGuardStep(existing)) {
|
|
2699
2754
|
return existing;
|
|
2700
2755
|
}
|
|
2701
2756
|
const lines = existing.split("\n");
|
|
2702
2757
|
const prSectionIdx = findPullRequestsSection(lines);
|
|
2703
2758
|
if (prSectionIdx !== null) {
|
|
2704
|
-
return injectStepIntoPrSection(lines, prSectionIdx);
|
|
2759
|
+
return injectStepIntoPrSection(lines, prSectionIdx, stepYaml);
|
|
2705
2760
|
}
|
|
2706
|
-
return appendPrSection(lines);
|
|
2761
|
+
return appendPrSection(lines, stepYaml);
|
|
2707
2762
|
}
|
|
2708
2763
|
function findPullRequestsSection(lines) {
|
|
2709
2764
|
for (let i3 = 0; i3 < lines.length; i3++) {
|
|
@@ -2732,19 +2787,19 @@ function findLastStepEnd(lines, afterBranch) {
|
|
|
2732
2787
|
}
|
|
2733
2788
|
return lastContentLine;
|
|
2734
2789
|
}
|
|
2735
|
-
function injectStepIntoPrSection(lines, prIdx) {
|
|
2790
|
+
function injectStepIntoPrSection(lines, prIdx, stepYaml) {
|
|
2736
2791
|
const branchIdx = findBranchPattern(lines, prIdx);
|
|
2737
2792
|
if (branchIdx !== null) {
|
|
2738
2793
|
const insertAfter = findLastStepEnd(lines, branchIdx);
|
|
2739
2794
|
const before = lines.slice(0, insertAfter + 1);
|
|
2740
2795
|
const after = lines.slice(insertAfter + 1);
|
|
2741
|
-
return [...before,
|
|
2796
|
+
return [...before, stepYaml, ...after].join("\n");
|
|
2742
2797
|
}
|
|
2743
2798
|
const result = [...lines];
|
|
2744
|
-
result.splice(prIdx + 1, 0, " '**':",
|
|
2799
|
+
result.splice(prIdx + 1, 0, " '**':", stepYaml);
|
|
2745
2800
|
return result.join("\n");
|
|
2746
2801
|
}
|
|
2747
|
-
function appendPrSection(lines) {
|
|
2802
|
+
function appendPrSection(lines, stepYaml) {
|
|
2748
2803
|
const pipelinesIdx = lines.findIndex((l3) => /^pipelines:\s*$/.test(l3));
|
|
2749
2804
|
if (pipelinesIdx === -1) {
|
|
2750
2805
|
return [
|
|
@@ -2753,7 +2808,7 @@ function appendPrSection(lines) {
|
|
|
2753
2808
|
"pipelines:",
|
|
2754
2809
|
" pull-requests:",
|
|
2755
2810
|
" '**':",
|
|
2756
|
-
|
|
2811
|
+
stepYaml
|
|
2757
2812
|
].join("\n");
|
|
2758
2813
|
}
|
|
2759
2814
|
let insertAt = pipelinesIdx + 1;
|
|
@@ -2777,7 +2832,7 @@ function appendPrSection(lines) {
|
|
|
2777
2832
|
...before,
|
|
2778
2833
|
" pull-requests:",
|
|
2779
2834
|
" '**':",
|
|
2780
|
-
|
|
2835
|
+
stepYaml,
|
|
2781
2836
|
...after
|
|
2782
2837
|
].join("\n");
|
|
2783
2838
|
}
|
|
@@ -2794,6 +2849,7 @@ function findEndOfLastSection(lines, pipelinesIdx) {
|
|
|
2794
2849
|
}
|
|
2795
2850
|
async function initFrontGuard(cwd) {
|
|
2796
2851
|
const actions = [];
|
|
2852
|
+
const pm = await detectPackageManager(cwd);
|
|
2797
2853
|
const cfgCandidates = [
|
|
2798
2854
|
"frontguard.config.mjs",
|
|
2799
2855
|
"frontguard.config.js",
|
|
@@ -2816,15 +2872,16 @@ async function initFrontGuard(cwd) {
|
|
|
2816
2872
|
} else {
|
|
2817
2873
|
actions.push("pull_request_template.md already exists (skipped)");
|
|
2818
2874
|
}
|
|
2875
|
+
const stepYaml = buildFrontGuardStepYaml(pm);
|
|
2819
2876
|
const pipelinePath = path6.join(cwd, "bitbucket-pipelines.yml");
|
|
2820
2877
|
if (await fileExists(pipelinePath)) {
|
|
2821
2878
|
const existing = await fs.readFile(pipelinePath, "utf8");
|
|
2822
2879
|
if (hasFrontGuardStep(existing)) {
|
|
2823
2880
|
actions.push("bitbucket-pipelines.yml already has FrontGuard step (skipped)");
|
|
2824
2881
|
} else {
|
|
2825
|
-
const merged = mergePipelineStep(existing);
|
|
2882
|
+
const merged = mergePipelineStep(existing, stepYaml);
|
|
2826
2883
|
await fs.writeFile(pipelinePath, merged, "utf8");
|
|
2827
|
-
actions.push(
|
|
2884
|
+
actions.push(`merged FrontGuard step into bitbucket-pipelines.yml (${pm})`);
|
|
2828
2885
|
}
|
|
2829
2886
|
} else {
|
|
2830
2887
|
await fs.writeFile(
|
|
@@ -2838,12 +2895,12 @@ async function initFrontGuard(cwd) {
|
|
|
2838
2895
|
"pipelines:",
|
|
2839
2896
|
" pull-requests:",
|
|
2840
2897
|
" '**':",
|
|
2841
|
-
|
|
2898
|
+
stepYaml,
|
|
2842
2899
|
""
|
|
2843
2900
|
].join("\n"),
|
|
2844
2901
|
"utf8"
|
|
2845
2902
|
);
|
|
2846
|
-
actions.push(
|
|
2903
|
+
actions.push(`created bitbucket-pipelines.yml with FrontGuard step (${pm})`);
|
|
2847
2904
|
}
|
|
2848
2905
|
return actions;
|
|
2849
2906
|
}
|
|
@@ -2865,8 +2922,8 @@ function mdTableCell(s3) {
|
|
|
2865
2922
|
function statusForPrTable(r4) {
|
|
2866
2923
|
const n3 = r4.findings.length;
|
|
2867
2924
|
const blocks = r4.findings.filter((f4) => f4.severity === "block").length;
|
|
2868
|
-
if (blocks > 0) return `${n3} issue
|
|
2869
|
-
return `${n3} issue
|
|
2925
|
+
if (blocks > 0) return `${n3} issue${n3 === 1 ? "" : "s"}, ${blocks} blocking`;
|
|
2926
|
+
return `${n3} issue${n3 === 1 ? "" : "s"}`;
|
|
2870
2927
|
}
|
|
2871
2928
|
function checkNeedsRow(r4) {
|
|
2872
2929
|
return !r4.skipped && r4.findings.length > 0;
|
|
@@ -2882,21 +2939,21 @@ function formatBitbucketPrSnippet(report) {
|
|
|
2882
2939
|
const reportUrl = publicReport || FRONTGUARD_REPORT_URL_PLACEHOLDER;
|
|
2883
2940
|
const { riskScore, results } = report;
|
|
2884
2941
|
const lines = [];
|
|
2885
|
-
|
|
2942
|
+
const riskEmoji2 = riskScore === "HIGH" ? "\u{1F534}" : riskScore === "MEDIUM" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
2943
|
+
lines.push(`**FrontGuard** \xB7 ${riskEmoji2} ${riskScore} risk`);
|
|
2886
2944
|
lines.push("");
|
|
2887
2945
|
const failing = results.filter(checkNeedsRow);
|
|
2888
2946
|
if (failing.length > 0) {
|
|
2889
|
-
lines.push("| Check
|
|
2890
|
-
lines.push("|
|
|
2947
|
+
lines.push("| Check | Issues |");
|
|
2948
|
+
lines.push("| :-- | :-- |");
|
|
2891
2949
|
for (const r4 of failing) {
|
|
2892
|
-
lines.push(`|
|
|
2950
|
+
lines.push(`| \`${mdTableCell(r4.checkId)}\` | ${mdTableCell(statusForPrTable(r4))} |`);
|
|
2893
2951
|
}
|
|
2894
2952
|
} else {
|
|
2895
|
-
lines.push("_All checks passed
|
|
2953
|
+
lines.push("_All checks passed._");
|
|
2896
2954
|
}
|
|
2897
2955
|
lines.push("");
|
|
2898
2956
|
lines.push(DETAILED_REPORT_LINE);
|
|
2899
|
-
lines.push("");
|
|
2900
2957
|
lines.push(reportUrl.endsWith("\n") ? reportUrl.slice(0, -1) : reportUrl);
|
|
2901
2958
|
lines.push("");
|
|
2902
2959
|
return lines.join("\n");
|
|
@@ -6371,438 +6428,519 @@ function sortFindings(cwd, items) {
|
|
|
6371
6428
|
});
|
|
6372
6429
|
}
|
|
6373
6430
|
function formatDuration(ms) {
|
|
6374
|
-
if (ms < 1e3) return `${ms}
|
|
6431
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
6375
6432
|
const s3 = Math.round(ms / 1e3);
|
|
6376
6433
|
if (s3 < 60) return `${s3}s`;
|
|
6377
6434
|
const m3 = Math.floor(s3 / 60);
|
|
6378
6435
|
const r4 = s3 % 60;
|
|
6379
6436
|
return r4 ? `${m3}m ${r4}s` : `${m3}m`;
|
|
6380
6437
|
}
|
|
6381
|
-
function
|
|
6382
|
-
if (r4.skipped) return '<span class="
|
|
6383
|
-
if (r4.findings.length === 0) return '<span class="
|
|
6438
|
+
function checkStatusIcon(r4) {
|
|
6439
|
+
if (r4.skipped) return '<span class="status-icon skip" title="Skipped">\u2013</span>';
|
|
6440
|
+
if (r4.findings.length === 0) return '<span class="status-icon pass" title="Passed">\u2713</span>';
|
|
6384
6441
|
if (r4.findings.some((x3) => x3.severity === "block"))
|
|
6385
|
-
return '<span class="
|
|
6386
|
-
return '<span class="
|
|
6387
|
-
}
|
|
6388
|
-
var CHECKS_TABLE_STYLES = `
|
|
6389
|
-
table.results {
|
|
6390
|
-
width: 100%;
|
|
6391
|
-
border-collapse: collapse;
|
|
6392
|
-
font-size: 0.875rem;
|
|
6393
|
-
background: var(--surface);
|
|
6394
|
-
border-radius: var(--radius);
|
|
6395
|
-
overflow: hidden;
|
|
6396
|
-
border: 1px solid var(--border);
|
|
6397
|
-
box-shadow: var(--shadow);
|
|
6398
|
-
}
|
|
6399
|
-
table.results th, table.results td {
|
|
6400
|
-
padding: 0.55rem 0.85rem;
|
|
6401
|
-
text-align: left;
|
|
6402
|
-
border-bottom: 1px solid var(--border);
|
|
6403
|
-
}
|
|
6404
|
-
table.results tr:last-child td { border-bottom: none; }
|
|
6405
|
-
table.results thead th {
|
|
6406
|
-
background: #f1f5f9;
|
|
6407
|
-
color: var(--muted);
|
|
6408
|
-
font-weight: 600;
|
|
6409
|
-
font-size: 0.72rem;
|
|
6410
|
-
text-transform: uppercase;
|
|
6411
|
-
letter-spacing: 0.04em;
|
|
6412
|
-
}
|
|
6413
|
-
.td-icon { width: 2rem; vertical-align: middle; }
|
|
6414
|
-
.td-check { vertical-align: middle; }
|
|
6415
|
-
.td-num, .td-time { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
6416
|
-
.check-title-cell {
|
|
6417
|
-
display: inline-flex;
|
|
6418
|
-
align-items: center;
|
|
6419
|
-
gap: 0.35rem;
|
|
6420
|
-
flex-wrap: nowrap;
|
|
6421
|
-
}
|
|
6422
|
-
.check-name { font-weight: 600; }
|
|
6423
|
-
.check-info-wrap {
|
|
6424
|
-
position: relative;
|
|
6425
|
-
display: inline-flex;
|
|
6426
|
-
align-items: center;
|
|
6427
|
-
flex-shrink: 0;
|
|
6428
|
-
}
|
|
6429
|
-
.check-info {
|
|
6430
|
-
display: inline-flex;
|
|
6431
|
-
align-items: center;
|
|
6432
|
-
justify-content: center;
|
|
6433
|
-
width: 1.125rem;
|
|
6434
|
-
height: 1.125rem;
|
|
6435
|
-
padding: 0;
|
|
6436
|
-
margin: 0;
|
|
6437
|
-
border: 1px solid var(--border);
|
|
6438
|
-
border-radius: 50%;
|
|
6439
|
-
background: #f1f5f9;
|
|
6440
|
-
color: var(--muted);
|
|
6441
|
-
font-size: 0.62rem;
|
|
6442
|
-
font-weight: 700;
|
|
6443
|
-
font-style: normal;
|
|
6444
|
-
line-height: 1;
|
|
6445
|
-
cursor: help;
|
|
6446
|
-
flex-shrink: 0;
|
|
6447
|
-
}
|
|
6448
|
-
.check-info:hover,
|
|
6449
|
-
.check-info:focus-visible {
|
|
6450
|
-
border-color: var(--accent);
|
|
6451
|
-
color: var(--accent);
|
|
6452
|
-
background: var(--accent-soft);
|
|
6453
|
-
outline: none;
|
|
6454
|
-
}
|
|
6455
|
-
.check-tooltip {
|
|
6456
|
-
position: absolute;
|
|
6457
|
-
left: 50%;
|
|
6458
|
-
bottom: calc(100% + 8px);
|
|
6459
|
-
transform: translateX(-50%);
|
|
6460
|
-
min-width: 12rem;
|
|
6461
|
-
max-width: min(22rem, 86vw);
|
|
6462
|
-
padding: 0.55rem 0.65rem;
|
|
6463
|
-
background: var(--text);
|
|
6464
|
-
color: #f8fafc;
|
|
6465
|
-
font-size: 0.78rem;
|
|
6466
|
-
font-weight: 400;
|
|
6467
|
-
line-height: 1.45;
|
|
6468
|
-
border-radius: 6px;
|
|
6469
|
-
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
|
|
6470
|
-
z-index: 50;
|
|
6471
|
-
opacity: 0;
|
|
6472
|
-
visibility: hidden;
|
|
6473
|
-
pointer-events: none;
|
|
6474
|
-
transition: opacity 0.12s ease, visibility 0.12s ease;
|
|
6475
|
-
text-align: left;
|
|
6476
|
-
}
|
|
6477
|
-
.check-info-wrap:hover .check-tooltip,
|
|
6478
|
-
.check-info-wrap:focus-within .check-tooltip {
|
|
6479
|
-
opacity: 1;
|
|
6480
|
-
visibility: visible;
|
|
6481
|
-
}
|
|
6482
|
-
.check-tooltip::after {
|
|
6483
|
-
content: '';
|
|
6484
|
-
position: absolute;
|
|
6485
|
-
top: 100%;
|
|
6486
|
-
left: 50%;
|
|
6487
|
-
margin-left: -6px;
|
|
6488
|
-
border: 6px solid transparent;
|
|
6489
|
-
border-top-color: var(--text);
|
|
6490
|
-
}
|
|
6491
|
-
.dot {
|
|
6492
|
-
display: inline-block;
|
|
6493
|
-
width: 8px;
|
|
6494
|
-
height: 8px;
|
|
6495
|
-
border-radius: 50%;
|
|
6496
|
-
}
|
|
6497
|
-
.dot-ok { background: var(--ok); }
|
|
6498
|
-
.dot-warn { background: var(--warn); }
|
|
6499
|
-
.dot-block { background: var(--block); }
|
|
6500
|
-
.dot-skip { background: #cbd5e1; }
|
|
6501
|
-
`;
|
|
6502
|
-
function renderCheckTableRows(results) {
|
|
6503
|
-
return results.map((r4) => {
|
|
6504
|
-
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
|
|
6505
|
-
const help = escapeHtml(getCheckDescription(r4.checkId));
|
|
6506
|
-
const ariaWhat = escapeHtml(`What does the ${r4.checkId} check do?`);
|
|
6507
|
-
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>`;
|
|
6508
|
-
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>`;
|
|
6509
|
-
}).join("\n");
|
|
6442
|
+
return '<span class="status-icon block" title="Blocking">\u2715</span>';
|
|
6443
|
+
return '<span class="status-icon warn" title="Warnings">!</span>';
|
|
6510
6444
|
}
|
|
6511
6445
|
function renderFindingCard(cwd, r4, f4) {
|
|
6512
6446
|
const d3 = normalizeFinding(cwd, f4);
|
|
6513
6447
|
const sevClass = f4.severity === "block" ? "sev-block" : f4.severity === "warn" ? "sev-warn" : "sev-info";
|
|
6514
|
-
const
|
|
6515
|
-
const
|
|
6516
|
-
|
|
6517
|
-
|
|
6448
|
+
const sevLabel = f4.severity === "block" ? "blocking" : f4.severity === "warn" ? "warning" : "info";
|
|
6449
|
+
const meta = [];
|
|
6450
|
+
meta.push(`<span class="tag tag-check">${escapeHtml(r4.checkId)}</span>`);
|
|
6451
|
+
meta.push(`<span class="tag tag-rule">${escapeHtml(f4.id)}</span>`);
|
|
6452
|
+
if (d3.file) meta.push(`<span class="tag tag-file">${escapeHtml(d3.file)}</span>`);
|
|
6453
|
+
const hintHtml = d3.detail && !d3.detail.includes("\n") && d3.detail.length <= 240 && !d3.detail.includes("|") ? `<p class="card-hint">${escapeHtml(d3.detail)}</p>` : "";
|
|
6454
|
+
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>` : "";
|
|
6455
|
+
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>` : "";
|
|
6456
|
+
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>`;
|
|
6457
|
+
}
|
|
6458
|
+
function renderCheckTableRows(_cwd, results) {
|
|
6459
|
+
return results.map((r4) => {
|
|
6460
|
+
const icon = checkStatusIcon(r4);
|
|
6461
|
+
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>`;
|
|
6462
|
+
const help = escapeHtml(getCheckDescription(r4.checkId));
|
|
6463
|
+
const countCell = r4.skipped ? "\u2014" : String(r4.findings.length);
|
|
6464
|
+
const timeCell = formatDuration(r4.durationMs);
|
|
6465
|
+
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>`;
|
|
6466
|
+
}).join("\n");
|
|
6518
6467
|
}
|
|
6519
6468
|
function buildHtmlReport(p2) {
|
|
6520
|
-
const {
|
|
6521
|
-
cwd,
|
|
6522
|
-
riskScore,
|
|
6523
|
-
mode,
|
|
6524
|
-
stack,
|
|
6525
|
-
pr,
|
|
6526
|
-
results,
|
|
6527
|
-
warns,
|
|
6528
|
-
infos,
|
|
6529
|
-
blocks,
|
|
6530
|
-
lines,
|
|
6531
|
-
llmAppendix
|
|
6532
|
-
} = p2;
|
|
6533
|
-
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
6469
|
+
const { cwd, riskScore, mode, stack, pr, results, warns, infos, blocks, lines, llmAppendix } = p2;
|
|
6534
6470
|
const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
|
|
6535
|
-
const
|
|
6536
|
-
const
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
);
|
|
6542
|
-
const warnItems = sortFindings(
|
|
6543
|
-
cwd,
|
|
6544
|
-
results.flatMap(
|
|
6545
|
-
(r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))
|
|
6546
|
-
)
|
|
6547
|
-
);
|
|
6548
|
-
const infoItems = sortFindings(
|
|
6549
|
-
cwd,
|
|
6550
|
-
results.flatMap(
|
|
6551
|
-
(r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))
|
|
6552
|
-
)
|
|
6553
|
-
);
|
|
6471
|
+
const riskIcon = riskScore === "LOW" ? "\u2713" : riskScore === "MEDIUM" ? "!" : "\u2715";
|
|
6472
|
+
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
6473
|
+
const checkRows = renderCheckTableRows(cwd, results);
|
|
6474
|
+
const blockItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))));
|
|
6475
|
+
const warnItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))));
|
|
6476
|
+
const infoItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))));
|
|
6554
6477
|
const byCheck = /* @__PURE__ */ new Map();
|
|
6555
6478
|
for (const item of warnItems) {
|
|
6556
6479
|
const list = byCheck.get(item.r.checkId) ?? [];
|
|
6557
6480
|
list.push(item);
|
|
6558
6481
|
byCheck.set(item.r.checkId, list);
|
|
6559
6482
|
}
|
|
6560
|
-
const
|
|
6561
|
-
const
|
|
6562
|
-
let
|
|
6483
|
+
const warnCheckOrder = [...byCheck.keys()].sort();
|
|
6484
|
+
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");
|
|
6485
|
+
let warnsHtml = "";
|
|
6563
6486
|
if (warnItems.length === 0) {
|
|
6564
|
-
|
|
6487
|
+
warnsHtml = '<p class="empty-note">No warnings.</p>';
|
|
6565
6488
|
} else {
|
|
6566
|
-
for (const cid of
|
|
6489
|
+
for (const cid of warnCheckOrder) {
|
|
6567
6490
|
const group = sortFindings(cwd, byCheck.get(cid));
|
|
6568
|
-
|
|
6569
|
-
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>`;
|
|
6491
|
+
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>`;
|
|
6570
6492
|
}
|
|
6571
6493
|
}
|
|
6572
|
-
const infoHtml = infoItems.length === 0 ? '<p class="empty-
|
|
6573
|
-
const
|
|
6574
|
-
const
|
|
6494
|
+
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");
|
|
6495
|
+
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>` : "";
|
|
6496
|
+
const appendixHtml = llmAppendix?.trim() ? `<section class="section"><h2 class="section-title">Appendix</h2><pre class="raw-pre">${escapeHtml(llmAppendix.trim())}</pre></section>` : "";
|
|
6497
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16) + " UTC";
|
|
6575
6498
|
return `<!DOCTYPE html>
|
|
6576
6499
|
<html lang="en">
|
|
6577
6500
|
<head>
|
|
6578
6501
|
<meta charset="utf-8" />
|
|
6579
6502
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6580
|
-
<title>FrontGuard
|
|
6503
|
+
<title>FrontGuard Report</title>
|
|
6581
6504
|
<style>
|
|
6582
6505
|
:root {
|
|
6583
6506
|
--bg: #f8fafc;
|
|
6584
6507
|
--surface: #ffffff;
|
|
6585
|
-
--
|
|
6586
|
-
--muted: #64748b;
|
|
6508
|
+
--surface-alt: #f1f5f9;
|
|
6587
6509
|
--border: #e2e8f0;
|
|
6588
|
-
--
|
|
6589
|
-
--
|
|
6590
|
-
--
|
|
6591
|
-
--
|
|
6592
|
-
--
|
|
6593
|
-
--
|
|
6510
|
+
--border-light: #f1f5f9;
|
|
6511
|
+
--text: #0f172a;
|
|
6512
|
+
--text-secondary: #475569;
|
|
6513
|
+
--muted: #94a3b8;
|
|
6514
|
+
--accent: #6366f1;
|
|
6515
|
+
--c-block: #dc2626;
|
|
6516
|
+
--c-block-bg: #fef2f2;
|
|
6517
|
+
--c-block-border: #fecaca;
|
|
6518
|
+
--c-warn: #d97706;
|
|
6519
|
+
--c-warn-bg: #fffbeb;
|
|
6520
|
+
--c-warn-border: #fde68a;
|
|
6521
|
+
--c-info: #2563eb;
|
|
6522
|
+
--c-info-bg: #eff6ff;
|
|
6523
|
+
--c-info-border: #bfdbfe;
|
|
6524
|
+
--c-ok: #16a34a;
|
|
6525
|
+
--c-ok-bg: #f0fdf4;
|
|
6526
|
+
--radius-sm: 6px;
|
|
6594
6527
|
--radius: 10px;
|
|
6595
|
-
--
|
|
6528
|
+
--radius-lg: 14px;
|
|
6529
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,.05);
|
|
6530
|
+
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
|
|
6531
|
+
font-size: 15px;
|
|
6596
6532
|
}
|
|
6597
|
-
|
|
6533
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
6598
6534
|
body {
|
|
6599
|
-
|
|
6600
|
-
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
6535
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
6601
6536
|
background: var(--bg);
|
|
6602
6537
|
color: var(--text);
|
|
6603
|
-
line-height: 1.
|
|
6604
|
-
|
|
6605
|
-
padding: 2rem clamp(1rem, 4vw, 3rem) 4rem;
|
|
6606
|
-
max-width: 920px;
|
|
6607
|
-
margin-left: auto;
|
|
6608
|
-
margin-right: auto;
|
|
6609
|
-
}
|
|
6610
|
-
.hero {
|
|
6611
|
-
margin-bottom: 2rem;
|
|
6538
|
+
line-height: 1.6;
|
|
6539
|
+
padding: 2.5rem clamp(1rem, 5vw, 3rem) 5rem;
|
|
6612
6540
|
}
|
|
6541
|
+
.wrap { max-width: 860px; margin: 0 auto; }
|
|
6542
|
+
|
|
6543
|
+
/* \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 */
|
|
6544
|
+
.header { margin-bottom: 2.5rem; }
|
|
6613
6545
|
.brand {
|
|
6614
|
-
font-size: 0.
|
|
6615
|
-
font-weight:
|
|
6546
|
+
font-size: 0.7rem;
|
|
6547
|
+
font-weight: 700;
|
|
6616
6548
|
letter-spacing: 0.12em;
|
|
6617
6549
|
text-transform: uppercase;
|
|
6618
6550
|
color: var(--muted);
|
|
6619
|
-
margin-bottom: 0.
|
|
6551
|
+
margin-bottom: 0.75rem;
|
|
6620
6552
|
}
|
|
6621
|
-
|
|
6622
|
-
font-size: 1.
|
|
6553
|
+
.page-title {
|
|
6554
|
+
font-size: 1.6rem;
|
|
6623
6555
|
font-weight: 700;
|
|
6624
6556
|
letter-spacing: -0.03em;
|
|
6625
|
-
margin: 0 0 1rem;
|
|
6626
6557
|
color: var(--text);
|
|
6558
|
+
margin-bottom: 1.25rem;
|
|
6559
|
+
line-height: 1.2;
|
|
6627
6560
|
}
|
|
6628
|
-
|
|
6561
|
+
|
|
6562
|
+
/* \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 */
|
|
6563
|
+
.risk-pill {
|
|
6564
|
+
display: inline-flex;
|
|
6565
|
+
align-items: center;
|
|
6566
|
+
gap: 0.45rem;
|
|
6567
|
+
padding: 0.35rem 0.8rem;
|
|
6568
|
+
border-radius: 999px;
|
|
6569
|
+
font-size: 0.82rem;
|
|
6570
|
+
font-weight: 700;
|
|
6571
|
+
letter-spacing: 0.02em;
|
|
6572
|
+
border: 1.5px solid;
|
|
6573
|
+
}
|
|
6574
|
+
.risk-low { color: var(--c-ok); background: var(--c-ok-bg); border-color: #bbf7d0; }
|
|
6575
|
+
.risk-med { color: var(--c-warn); background: var(--c-warn-bg); border-color: var(--c-warn-border); }
|
|
6576
|
+
.risk-high { color: var(--c-block); background: var(--c-block-bg); border-color: var(--c-block-border); }
|
|
6577
|
+
.risk-icon { font-size: 0.75rem; }
|
|
6578
|
+
|
|
6579
|
+
/* \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 */
|
|
6580
|
+
.stats {
|
|
6629
6581
|
display: flex;
|
|
6630
6582
|
flex-wrap: wrap;
|
|
6631
|
-
gap: 0.
|
|
6632
|
-
margin-
|
|
6583
|
+
gap: 0.5rem;
|
|
6584
|
+
margin-top: 1rem;
|
|
6585
|
+
align-items: center;
|
|
6633
6586
|
}
|
|
6634
|
-
.
|
|
6587
|
+
.stat {
|
|
6588
|
+
display: flex;
|
|
6589
|
+
align-items: center;
|
|
6590
|
+
gap: 0.35rem;
|
|
6591
|
+
padding: 0.3rem 0.65rem;
|
|
6592
|
+
background: var(--surface);
|
|
6593
|
+
border: 1px solid var(--border);
|
|
6594
|
+
border-radius: var(--radius-sm);
|
|
6595
|
+
font-size: 0.78rem;
|
|
6596
|
+
box-shadow: var(--shadow-sm);
|
|
6597
|
+
}
|
|
6598
|
+
.stat-label { color: var(--text-secondary); }
|
|
6599
|
+
.stat-value { font-weight: 600; color: var(--text); }
|
|
6600
|
+
.stat-value.danger { color: var(--c-block); }
|
|
6601
|
+
.stat-value.caution { color: var(--c-warn); }
|
|
6602
|
+
.stat-value.ok { color: var(--c-ok); }
|
|
6603
|
+
|
|
6604
|
+
/* \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 */
|
|
6605
|
+
.meta-box {
|
|
6635
6606
|
background: var(--surface);
|
|
6636
6607
|
border: 1px solid var(--border);
|
|
6637
6608
|
border-radius: var(--radius);
|
|
6638
|
-
|
|
6639
|
-
|
|
6609
|
+
box-shadow: var(--shadow-sm);
|
|
6610
|
+
overflow: hidden;
|
|
6611
|
+
margin-bottom: 2rem;
|
|
6612
|
+
}
|
|
6613
|
+
.meta-row {
|
|
6640
6614
|
display: flex;
|
|
6641
|
-
align-items:
|
|
6642
|
-
gap:
|
|
6615
|
+
align-items: baseline;
|
|
6616
|
+
gap: 1rem;
|
|
6617
|
+
padding: 0.6rem 1rem;
|
|
6618
|
+
border-bottom: 1px solid var(--border-light);
|
|
6619
|
+
font-size: 0.875rem;
|
|
6643
6620
|
}
|
|
6644
|
-
.
|
|
6645
|
-
.
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6621
|
+
.meta-row:last-child { border-bottom: none; }
|
|
6622
|
+
.meta-label {
|
|
6623
|
+
flex-shrink: 0;
|
|
6624
|
+
width: 7rem;
|
|
6625
|
+
color: var(--text-secondary);
|
|
6626
|
+
font-size: 0.78rem;
|
|
6627
|
+
font-weight: 500;
|
|
6628
|
+
}
|
|
6629
|
+
.meta-value { color: var(--text); }
|
|
6630
|
+
|
|
6631
|
+
/* \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 */
|
|
6632
|
+
.section { margin-top: 2.5rem; }
|
|
6633
|
+
.section-title {
|
|
6634
|
+
font-size: 0.8rem;
|
|
6635
|
+
font-weight: 700;
|
|
6636
|
+
letter-spacing: 0.06em;
|
|
6637
|
+
text-transform: uppercase;
|
|
6638
|
+
color: var(--text-secondary);
|
|
6639
|
+
margin-bottom: 0.75rem;
|
|
6656
6640
|
}
|
|
6657
|
-
|
|
6641
|
+
|
|
6642
|
+
/* \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 */
|
|
6643
|
+
.checks-table {
|
|
6658
6644
|
width: 100%;
|
|
6659
6645
|
border-collapse: collapse;
|
|
6660
|
-
font-size: 0.9rem;
|
|
6661
6646
|
background: var(--surface);
|
|
6647
|
+
border: 1px solid var(--border);
|
|
6662
6648
|
border-radius: var(--radius);
|
|
6663
6649
|
overflow: hidden;
|
|
6664
|
-
|
|
6665
|
-
|
|
6650
|
+
box-shadow: var(--shadow-sm);
|
|
6651
|
+
font-size: 0.855rem;
|
|
6666
6652
|
}
|
|
6667
|
-
.
|
|
6668
|
-
padding: 0.
|
|
6653
|
+
.checks-table thead th {
|
|
6654
|
+
padding: 0.5rem 0.75rem;
|
|
6669
6655
|
text-align: left;
|
|
6656
|
+
font-size: 0.7rem;
|
|
6657
|
+
font-weight: 600;
|
|
6658
|
+
letter-spacing: 0.06em;
|
|
6659
|
+
text-transform: uppercase;
|
|
6660
|
+
color: var(--muted);
|
|
6661
|
+
background: var(--surface-alt);
|
|
6670
6662
|
border-bottom: 1px solid var(--border);
|
|
6671
6663
|
}
|
|
6672
|
-
.
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6664
|
+
.checks-table tbody td {
|
|
6665
|
+
padding: 0.55rem 0.75rem;
|
|
6666
|
+
border-bottom: 1px solid var(--border-light);
|
|
6667
|
+
vertical-align: middle;
|
|
6668
|
+
}
|
|
6669
|
+
.checks-table tbody tr:last-child td { border-bottom: none; }
|
|
6670
|
+
.checks-table tbody tr:hover { background: #fafbfc; }
|
|
6671
|
+
.col-icon { width: 2.25rem; }
|
|
6672
|
+
.col-count, .col-time { color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
|
|
6673
|
+
.col-time { width: 5.5rem; }
|
|
6674
|
+
.col-count { width: 2.5rem; }
|
|
6675
|
+
|
|
6676
|
+
/* Status icons */
|
|
6677
|
+
.status-icon {
|
|
6678
|
+
display: inline-flex;
|
|
6679
|
+
align-items: center;
|
|
6680
|
+
justify-content: center;
|
|
6681
|
+
width: 1.3rem;
|
|
6682
|
+
height: 1.3rem;
|
|
6683
|
+
border-radius: 50%;
|
|
6684
|
+
font-size: 0.65rem;
|
|
6685
|
+
font-weight: 800;
|
|
6686
|
+
line-height: 1;
|
|
6678
6687
|
}
|
|
6679
|
-
|
|
6680
|
-
.
|
|
6688
|
+
.status-icon.pass { background: var(--c-ok-bg); color: var(--c-ok); border: 1.5px solid #bbf7d0; }
|
|
6689
|
+
.status-icon.block { background: var(--c-block-bg); color: var(--c-block); border: 1.5px solid var(--c-block-border); }
|
|
6690
|
+
.status-icon.warn { background: var(--c-warn-bg); color: var(--c-warn); border: 1.5px solid var(--c-warn-border); }
|
|
6691
|
+
.status-icon.skip { background: var(--surface-alt); color: var(--muted); border: 1.5px solid var(--border); font-size: 0.9rem; }
|
|
6692
|
+
|
|
6693
|
+
/* Check name + tooltip */
|
|
6694
|
+
.check-name { font-weight: 600; color: var(--text); cursor: default; }
|
|
6695
|
+
.check-tooltip {
|
|
6696
|
+
display: block;
|
|
6697
|
+
font-size: 0.75rem;
|
|
6698
|
+
color: var(--text-secondary);
|
|
6699
|
+
margin-top: 0.1rem;
|
|
6700
|
+
font-weight: 400;
|
|
6701
|
+
line-height: 1.4;
|
|
6702
|
+
}
|
|
6703
|
+
.status-text { font-weight: 500; }
|
|
6704
|
+
.status-text.ok { color: var(--c-ok); }
|
|
6705
|
+
.status-text.danger { color: var(--c-block); }
|
|
6706
|
+
.status-text.caution { color: var(--c-warn); }
|
|
6707
|
+
.status-text.muted { color: var(--muted); font-weight: 400; font-size: 0.8rem; }
|
|
6708
|
+
|
|
6709
|
+
/* \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 */
|
|
6710
|
+
.accordion {
|
|
6681
6711
|
background: var(--surface);
|
|
6682
6712
|
border: 1px solid var(--border);
|
|
6683
6713
|
border-radius: var(--radius);
|
|
6714
|
+
box-shadow: var(--shadow-sm);
|
|
6684
6715
|
margin-bottom: 0.65rem;
|
|
6685
|
-
|
|
6716
|
+
overflow: hidden;
|
|
6686
6717
|
}
|
|
6687
|
-
.
|
|
6688
|
-
cursor: pointer;
|
|
6689
|
-
padding: 0.85rem 1rem;
|
|
6690
|
-
list-style: none;
|
|
6718
|
+
.accordion-header {
|
|
6691
6719
|
display: flex;
|
|
6692
6720
|
align-items: center;
|
|
6693
6721
|
justify-content: space-between;
|
|
6722
|
+
padding: 0.8rem 1rem;
|
|
6723
|
+
cursor: pointer;
|
|
6724
|
+
user-select: none;
|
|
6725
|
+
list-style: none;
|
|
6694
6726
|
font-weight: 600;
|
|
6695
6727
|
font-size: 0.9rem;
|
|
6696
6728
|
}
|
|
6697
|
-
.
|
|
6698
|
-
|
|
6699
|
-
.
|
|
6700
|
-
.
|
|
6701
|
-
|
|
6702
|
-
font-
|
|
6703
|
-
|
|
6704
|
-
color: var(--muted);
|
|
6705
|
-
background: #f1f5f9;
|
|
6706
|
-
padding: 0.15rem 0.5rem;
|
|
6729
|
+
.accordion-header::-webkit-details-marker { display: none; }
|
|
6730
|
+
details[open] > .accordion-header { border-bottom: 1px solid var(--border-light); }
|
|
6731
|
+
.accordion-label { display: flex; align-items: center; gap: 0.6rem; }
|
|
6732
|
+
.acc-count {
|
|
6733
|
+
font-size: 0.72rem;
|
|
6734
|
+
font-weight: 600;
|
|
6735
|
+
padding: 0.15rem 0.55rem;
|
|
6707
6736
|
border-radius: 999px;
|
|
6737
|
+
background: var(--surface-alt);
|
|
6738
|
+
color: var(--text-secondary);
|
|
6739
|
+
border: 1px solid var(--border);
|
|
6740
|
+
}
|
|
6741
|
+
.acc-count.danger { background: var(--c-block-bg); color: var(--c-block); border-color: var(--c-block-border); }
|
|
6742
|
+
.acc-count.caution { background: var(--c-warn-bg); color: var(--c-warn); border-color: var(--c-warn-border); }
|
|
6743
|
+
.accordion-body { padding: 0.85rem; display: flex; flex-direction: column; gap: 0.6rem; }
|
|
6744
|
+
|
|
6745
|
+
/* Warning groups */
|
|
6746
|
+
.group { margin-bottom: 0.25rem; }
|
|
6747
|
+
.group-label {
|
|
6748
|
+
display: flex;
|
|
6749
|
+
align-items: center;
|
|
6750
|
+
gap: 0.5rem;
|
|
6751
|
+
padding: 0.4rem 0;
|
|
6752
|
+
margin-bottom: 0.45rem;
|
|
6708
6753
|
}
|
|
6754
|
+
.group-name { font-size: 0.78rem; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
6755
|
+
.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); }
|
|
6756
|
+
|
|
6757
|
+
/* \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 */
|
|
6709
6758
|
.card {
|
|
6710
6759
|
border: 1px solid var(--border);
|
|
6711
|
-
border-radius:
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
background: #fafafa;
|
|
6760
|
+
border-radius: var(--radius-sm);
|
|
6761
|
+
background: var(--surface);
|
|
6762
|
+
padding: 0.85rem 1rem;
|
|
6715
6763
|
}
|
|
6716
|
-
.card
|
|
6717
|
-
.card.sev-
|
|
6718
|
-
.card.sev-
|
|
6719
|
-
.card
|
|
6720
|
-
.
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6764
|
+
.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); }
|
|
6765
|
+
.card.sev-warn { border-left: 3px solid var(--c-warn); }
|
|
6766
|
+
.card.sev-info { border-left: 3px solid var(--c-info); }
|
|
6767
|
+
.card-header { display: flex; align-items: flex-start; gap: 0.6rem; margin-bottom: 0.55rem; }
|
|
6768
|
+
.sev-badge {
|
|
6769
|
+
flex-shrink: 0;
|
|
6770
|
+
font-size: 0.65rem;
|
|
6771
|
+
font-weight: 700;
|
|
6772
|
+
letter-spacing: 0.05em;
|
|
6773
|
+
text-transform: uppercase;
|
|
6774
|
+
padding: 0.15rem 0.45rem;
|
|
6775
|
+
border-radius: 4px;
|
|
6776
|
+
margin-top: 0.15rem;
|
|
6777
|
+
}
|
|
6778
|
+
.sev-badge.sev-block { background: var(--c-block-bg); color: var(--c-block); border: 1px solid var(--c-block-border); }
|
|
6779
|
+
.sev-badge.sev-warn { background: var(--c-warn-bg); color: var(--c-warn); border: 1px solid var(--c-warn-border); }
|
|
6780
|
+
.sev-badge.sev-info { background: var(--c-info-bg); color: var(--c-info); border: 1px solid var(--c-info-border); }
|
|
6781
|
+
.card-message { font-size: 0.88rem; line-height: 1.5; color: var(--text); }
|
|
6782
|
+
.card-meta { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
|
6783
|
+
.tag {
|
|
6784
|
+
font-size: 0.72rem;
|
|
6785
|
+
padding: 0.1rem 0.45rem;
|
|
6786
|
+
border-radius: 4px;
|
|
6787
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6788
|
+
white-space: nowrap;
|
|
6789
|
+
overflow: hidden;
|
|
6790
|
+
text-overflow: ellipsis;
|
|
6791
|
+
max-width: 40ch;
|
|
6792
|
+
}
|
|
6793
|
+
.tag-check { background: #f0f0ff; color: #4338ca; border: 1px solid #e0e7ff; }
|
|
6794
|
+
.tag-rule { background: var(--surface-alt); color: var(--text-secondary); border: 1px solid var(--border); }
|
|
6795
|
+
.tag-file { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; max-width: 55ch; }
|
|
6796
|
+
.card-hint { font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.5rem; line-height: 1.45; }
|
|
6797
|
+
|
|
6798
|
+
/* Code blocks */
|
|
6799
|
+
.code-block {
|
|
6800
|
+
margin-top: 0.6rem;
|
|
6801
|
+
padding: 0.65rem 0.85rem;
|
|
6802
|
+
background: #f8fafc;
|
|
6735
6803
|
border: 1px solid var(--border);
|
|
6804
|
+
border-radius: var(--radius-sm);
|
|
6805
|
+
overflow-x: auto;
|
|
6806
|
+
font-size: 0.75rem;
|
|
6807
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6808
|
+
line-height: 1.55;
|
|
6809
|
+
white-space: pre;
|
|
6810
|
+
color: var(--text);
|
|
6811
|
+
}
|
|
6812
|
+
|
|
6813
|
+
/* Fix box */
|
|
6814
|
+
.fix-box {
|
|
6815
|
+
margin-top: 0.75rem;
|
|
6816
|
+
padding: 0.7rem 0.85rem;
|
|
6817
|
+
background: #fafafa;
|
|
6818
|
+
border: 1px dashed var(--border);
|
|
6819
|
+
border-radius: var(--radius-sm);
|
|
6736
6820
|
}
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6821
|
+
.fix-header {
|
|
6822
|
+
font-size: 0.7rem;
|
|
6823
|
+
font-weight: 700;
|
|
6824
|
+
text-transform: uppercase;
|
|
6825
|
+
letter-spacing: 0.06em;
|
|
6826
|
+
color: var(--accent);
|
|
6827
|
+
margin-bottom: 0.4rem;
|
|
6828
|
+
display: flex;
|
|
6829
|
+
align-items: center;
|
|
6830
|
+
gap: 0.4rem;
|
|
6831
|
+
}
|
|
6832
|
+
.badge-llm {
|
|
6833
|
+
font-size: 0.6rem;
|
|
6834
|
+
padding: 0.1rem 0.35rem;
|
|
6835
|
+
background: #eef2ff;
|
|
6836
|
+
color: var(--accent);
|
|
6837
|
+
border-radius: 4px;
|
|
6838
|
+
border: 1px solid #e0e7ff;
|
|
6839
|
+
}
|
|
6840
|
+
.fix-summary { font-size: 0.82rem; color: var(--text); white-space: pre-wrap; }
|
|
6841
|
+
.fix-disclaimer { font-size: 0.7rem; color: var(--muted); margin-top: 0.5rem; }
|
|
6842
|
+
|
|
6843
|
+
/* Misc */
|
|
6844
|
+
.empty-note { font-size: 0.875rem; color: var(--muted); padding: 0.25rem 0; }
|
|
6845
|
+
.muted { color: var(--muted); }
|
|
6846
|
+
.raw-pre {
|
|
6744
6847
|
white-space: pre-wrap;
|
|
6745
|
-
font-size: 0.
|
|
6848
|
+
font-size: 0.82rem;
|
|
6746
6849
|
background: var(--surface);
|
|
6747
6850
|
padding: 1rem;
|
|
6748
6851
|
border-radius: var(--radius);
|
|
6749
6852
|
border: 1px solid var(--border);
|
|
6750
|
-
|
|
6853
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6854
|
+
line-height: 1.55;
|
|
6751
6855
|
}
|
|
6752
|
-
|
|
6753
|
-
|
|
6856
|
+
|
|
6857
|
+
/* \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 */
|
|
6858
|
+
.footer {
|
|
6859
|
+
margin-top: 3.5rem;
|
|
6754
6860
|
padding-top: 1.25rem;
|
|
6755
6861
|
border-top: 1px solid var(--border);
|
|
6756
|
-
font-size: 0.
|
|
6862
|
+
font-size: 0.77rem;
|
|
6757
6863
|
color: var(--muted);
|
|
6864
|
+
display: flex;
|
|
6865
|
+
align-items: center;
|
|
6866
|
+
justify-content: space-between;
|
|
6867
|
+
flex-wrap: wrap;
|
|
6868
|
+
gap: 0.5rem;
|
|
6758
6869
|
}
|
|
6759
|
-
footer a { color: var(--accent); text-decoration: none; }
|
|
6760
|
-
footer a:hover { text-decoration: underline; }
|
|
6761
6870
|
</style>
|
|
6762
6871
|
</head>
|
|
6763
6872
|
<body>
|
|
6764
|
-
|
|
6873
|
+
<div class="wrap">
|
|
6874
|
+
|
|
6875
|
+
<header class="header">
|
|
6765
6876
|
<div class="brand">FrontGuard</div>
|
|
6766
|
-
<h1>Code review report</h1>
|
|
6767
|
-
<
|
|
6768
|
-
|
|
6769
|
-
<div class="
|
|
6770
|
-
<div class="
|
|
6771
|
-
<div class="
|
|
6772
|
-
<div class="
|
|
6877
|
+
<h1 class="page-title">Code review report</h1>
|
|
6878
|
+
<span class="risk-pill ${riskClass}"><span class="risk-icon">${riskIcon}</span> ${riskScore} risk</span>
|
|
6879
|
+
<div class="stats">
|
|
6880
|
+
<div class="stat"><span class="stat-label">Blocking</span><span class="stat-value ${blocks > 0 ? "danger" : "ok"}">${blocks}</span></div>
|
|
6881
|
+
<div class="stat"><span class="stat-label">Warnings</span><span class="stat-value ${warns > 0 ? "caution" : "ok"}">${warns}</span></div>
|
|
6882
|
+
<div class="stat"><span class="stat-label">Info</span><span class="stat-value">${infos}</span></div>
|
|
6883
|
+
<div class="stat"><span class="stat-label">Mode</span><span class="stat-value">${escapeHtml(modeLabel)}</span></div>
|
|
6773
6884
|
</div>
|
|
6774
6885
|
</header>
|
|
6775
6886
|
|
|
6776
|
-
<
|
|
6777
|
-
<
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
<tr><th>Stack</th><td>${escapeHtml(formatStackOneLiner(stack))}</td></tr>
|
|
6782
|
-
${prBlock}
|
|
6783
|
-
</table>
|
|
6784
|
-
</section>
|
|
6887
|
+
<div class="meta-box">
|
|
6888
|
+
<div class="meta-row"><span class="meta-label">Stack</span><span class="meta-value">${escapeHtml(formatStackOneLiner(stack))}</span></div>
|
|
6889
|
+
${prRow}
|
|
6890
|
+
<div class="meta-row"><span class="meta-label">Gate mode</span><span class="meta-value">${escapeHtml(modeLabel)}</span></div>
|
|
6891
|
+
</div>
|
|
6785
6892
|
|
|
6786
6893
|
<section class="section">
|
|
6787
|
-
<h2 class="
|
|
6788
|
-
<table class="
|
|
6789
|
-
<thead
|
|
6790
|
-
|
|
6894
|
+
<h2 class="section-title">Checks</h2>
|
|
6895
|
+
<table class="checks-table">
|
|
6896
|
+
<thead>
|
|
6897
|
+
<tr>
|
|
6898
|
+
<th class="col-icon"></th>
|
|
6899
|
+
<th>Check</th>
|
|
6900
|
+
<th>Status</th>
|
|
6901
|
+
<th class="col-count">#</th>
|
|
6902
|
+
<th class="col-time">Time</th>
|
|
6903
|
+
</tr>
|
|
6904
|
+
</thead>
|
|
6905
|
+
<tbody>
|
|
6906
|
+
${checkRows}
|
|
6907
|
+
</tbody>
|
|
6791
6908
|
</table>
|
|
6792
6909
|
</section>
|
|
6793
6910
|
|
|
6794
6911
|
<section class="section">
|
|
6795
|
-
<h2 class="
|
|
6796
|
-
|
|
6797
|
-
<details class="
|
|
6798
|
-
|
|
6912
|
+
<h2 class="section-title">Findings</h2>
|
|
6913
|
+
|
|
6914
|
+
<details class="accordion" ${blocks > 0 ? "open" : ""}>
|
|
6915
|
+
<summary class="accordion-header">
|
|
6916
|
+
<span class="accordion-label">Blocking <span class="acc-count ${blocks > 0 ? "danger" : ""}">${blocks}</span></span>
|
|
6917
|
+
</summary>
|
|
6918
|
+
<div class="accordion-body">${blockHtml}</div>
|
|
6919
|
+
</details>
|
|
6920
|
+
|
|
6921
|
+
<details class="accordion" ${warns > 0 && blocks === 0 ? "open" : ""}>
|
|
6922
|
+
<summary class="accordion-header">
|
|
6923
|
+
<span class="accordion-label">Warnings <span class="acc-count ${warns > 0 ? "caution" : ""}">${warns}</span></span>
|
|
6924
|
+
</summary>
|
|
6925
|
+
<div class="accordion-body">${warnsHtml}</div>
|
|
6926
|
+
</details>
|
|
6927
|
+
|
|
6928
|
+
<details class="accordion">
|
|
6929
|
+
<summary class="accordion-header">
|
|
6930
|
+
<span class="accordion-label">Info <span class="acc-count">${infos}</span></span>
|
|
6931
|
+
</summary>
|
|
6932
|
+
<div class="accordion-body">${infoHtml}</div>
|
|
6933
|
+
</details>
|
|
6799
6934
|
</section>
|
|
6800
6935
|
|
|
6801
|
-
${
|
|
6936
|
+
${appendixHtml}
|
|
6802
6937
|
|
|
6803
|
-
<footer>
|
|
6804
|
-
<
|
|
6938
|
+
<footer class="footer">
|
|
6939
|
+
<span>Generated by <strong>FrontGuard</strong> \xB7 ${ts2}</span>
|
|
6940
|
+
<span>Open in any browser \xB7 static report</span>
|
|
6805
6941
|
</footer>
|
|
6942
|
+
|
|
6943
|
+
</div>
|
|
6806
6944
|
</body>
|
|
6807
6945
|
</html>`;
|
|
6808
6946
|
}
|
|
@@ -7001,76 +7139,52 @@ function formatMarkdown(p2) {
|
|
|
7001
7139
|
if (lineA !== lineB) return lineA - lineB;
|
|
7002
7140
|
return a3.f.message.localeCompare(b3.f.message);
|
|
7003
7141
|
});
|
|
7004
|
-
sb.push("##
|
|
7142
|
+
sb.push("## FrontGuard report");
|
|
7005
7143
|
sb.push("");
|
|
7006
7144
|
const modeLabel = mode === "enforce" ? "enforce" : "warn only";
|
|
7007
7145
|
const badgeLine = [
|
|
7008
|
-
mdShield("Risk
|
|
7009
|
-
mdShield("
|
|
7146
|
+
mdShield("Risk", "risk", riskScore, riskShieldColor(riskScore)),
|
|
7147
|
+
mdShield("Mode", "mode", modeLabel, modeShieldColor(mode)),
|
|
7010
7148
|
mdShield("Blocking", "blocking", String(blocks), countShieldColor("block", blocks)),
|
|
7011
7149
|
mdShield("Warnings", "warnings", String(warns), countShieldColor("warn", warns)),
|
|
7012
|
-
mdShield("Info", "info
|
|
7150
|
+
mdShield("Info", "info", String(infos), countShieldColor("info", infos))
|
|
7013
7151
|
].join(" ");
|
|
7014
7152
|
sb.push(badgeLine);
|
|
7015
7153
|
sb.push("");
|
|
7016
|
-
sb.push("---");
|
|
7017
|
-
sb.push("");
|
|
7018
|
-
sb.push("### \u{1F4CC} Snapshot");
|
|
7019
|
-
sb.push("");
|
|
7020
7154
|
sb.push("| | |");
|
|
7021
7155
|
sb.push("|:--|:--|");
|
|
7022
|
-
sb.push(`|
|
|
7023
|
-
sb.push(
|
|
7024
|
-
|
|
7025
|
-
);
|
|
7026
|
-
sb.push(`| **Stack detected** | ${formatStackOneLiner(stack)} |`);
|
|
7156
|
+
sb.push(`| Risk | ${riskEmoji(riskScore)} **${riskScore}** |`);
|
|
7157
|
+
sb.push(`| Mode | ${mode === "enforce" ? "\u{1F512} Enforce" : "Warn only"} |`);
|
|
7158
|
+
sb.push(`| Stack | ${formatStackOneLiner(stack)} |`);
|
|
7027
7159
|
if (pr && lines != null) {
|
|
7028
|
-
sb.push(
|
|
7029
|
-
`| **PR size** | \u{1F4CF} **${lines}** LOC ( +${pr.additions} / \u2212${pr.deletions} ) \xB7 **${pr.changedFiles}** files |`
|
|
7030
|
-
);
|
|
7160
|
+
sb.push(`| PR size | ${lines} lines (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files |`);
|
|
7031
7161
|
}
|
|
7032
7162
|
sb.push("");
|
|
7033
7163
|
if (showAiAssistedBanner && pr?.aiAssisted) {
|
|
7034
7164
|
sb.push(
|
|
7035
|
-
">
|
|
7165
|
+
"> **AI-assisted PR** \u2014 stricter static checks active (security, secrets, `any` deltas may escalate)."
|
|
7036
7166
|
);
|
|
7037
7167
|
sb.push("");
|
|
7038
7168
|
}
|
|
7039
|
-
sb.push("> **How to read this report**");
|
|
7040
|
-
sb.push(">");
|
|
7041
|
-
sb.push("> | Symbol | Meaning |");
|
|
7042
|
-
sb.push("> |:--|:--|");
|
|
7043
|
-
sb.push("> | \u{1F7E2} | Check passed or skipped cleanly |");
|
|
7044
|
-
sb.push("> | \u{1F7E1} | Warnings only \u2014 review recommended |");
|
|
7045
|
-
sb.push("> | \u{1F534} | Blocking (`block`) severity present |");
|
|
7046
|
-
sb.push("> | \u23ED\uFE0F | Check skipped (see reason in table) |");
|
|
7047
|
-
sb.push(">");
|
|
7048
|
-
sb.push(
|
|
7049
|
-
"> Paths in findings are **relative to the repo root**. Each issue below has a small field table and optional detail."
|
|
7050
|
-
);
|
|
7051
|
-
sb.push("");
|
|
7052
7169
|
sb.push("---");
|
|
7053
7170
|
sb.push("");
|
|
7054
|
-
sb.push("###
|
|
7171
|
+
sb.push("### Checks");
|
|
7055
7172
|
sb.push("");
|
|
7056
|
-
sb.push("| | Check |
|
|
7057
|
-
sb.push("
|
|
7173
|
+
sb.push("| | Check | Issues | Time |");
|
|
7174
|
+
sb.push("|:--:|:--|:-:|--:|");
|
|
7058
7175
|
for (const r4 of results) {
|
|
7059
7176
|
const he2 = healthEmojiForCheck(r4);
|
|
7060
7177
|
let status;
|
|
7061
7178
|
if (r4.skipped) {
|
|
7062
7179
|
const why = r4.skipped.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
7063
|
-
const short = why.length >
|
|
7064
|
-
status = `\
|
|
7180
|
+
const short = why.length > 80 ? `${why.slice(0, 77)}\u2026` : why;
|
|
7181
|
+
status = `\u2014 _${short}_`;
|
|
7065
7182
|
} else if (r4.findings.length === 0) {
|
|
7066
|
-
status = "\
|
|
7183
|
+
status = "\u2014";
|
|
7067
7184
|
} else {
|
|
7068
|
-
status =
|
|
7185
|
+
status = String(r4.findings.length);
|
|
7069
7186
|
}
|
|
7070
|
-
|
|
7071
|
-
sb.push(
|
|
7072
|
-
`| ${he2} | **${r4.checkId}** | ${status} | **${nFind}** | ${formatDuration2(r4.durationMs)} |`
|
|
7073
|
-
);
|
|
7187
|
+
sb.push(`| ${he2} | \`${r4.checkId}\` | ${status} | ${formatDuration2(r4.durationMs)} |`);
|
|
7074
7188
|
}
|
|
7075
7189
|
sb.push("");
|
|
7076
7190
|
const blockFindings = sortWithCwd(
|
|
@@ -7078,10 +7192,10 @@ function formatMarkdown(p2) {
|
|
|
7078
7192
|
(r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))
|
|
7079
7193
|
)
|
|
7080
7194
|
);
|
|
7081
|
-
sb.push(`###
|
|
7195
|
+
sb.push(`### Blocking \u2014 ${blocks} issue${blocks === 1 ? "" : "s"}`);
|
|
7082
7196
|
sb.push("");
|
|
7083
7197
|
if (blockFindings.length === 0) {
|
|
7084
|
-
sb.push("
|
|
7198
|
+
sb.push("_No blocking findings._");
|
|
7085
7199
|
} else {
|
|
7086
7200
|
for (const { r: r4, f: f4 } of blockFindings) {
|
|
7087
7201
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -7089,11 +7203,11 @@ function formatMarkdown(p2) {
|
|
|
7089
7203
|
sb.push("");
|
|
7090
7204
|
sb.push(`#### ${findingTitleLine(d3.file, d3.message)}`);
|
|
7091
7205
|
sb.push("");
|
|
7092
|
-
sb.push(`|
|
|
7206
|
+
sb.push(`| | |`);
|
|
7093
7207
|
sb.push(`|:--|:--|`);
|
|
7094
|
-
sb.push(`|
|
|
7095
|
-
sb.push(`|
|
|
7096
|
-
if (d3.file) sb.push(`|
|
|
7208
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7209
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7210
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
7097
7211
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
7098
7212
|
appendSuggestedFix(sb, cwd, f4);
|
|
7099
7213
|
sb.push("");
|
|
@@ -7105,12 +7219,10 @@ function formatMarkdown(p2) {
|
|
|
7105
7219
|
(r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))
|
|
7106
7220
|
)
|
|
7107
7221
|
);
|
|
7108
|
-
sb.push(
|
|
7109
|
-
`### \u26A0\uFE0F Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"} (by check)`
|
|
7110
|
-
);
|
|
7222
|
+
sb.push(`### Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"}`);
|
|
7111
7223
|
sb.push("");
|
|
7112
7224
|
if (warnFindings.length === 0) {
|
|
7113
|
-
sb.push("
|
|
7225
|
+
sb.push("_No warnings._");
|
|
7114
7226
|
} else {
|
|
7115
7227
|
const byCheck = /* @__PURE__ */ new Map();
|
|
7116
7228
|
for (const item of warnFindings) {
|
|
@@ -7121,7 +7233,7 @@ function formatMarkdown(p2) {
|
|
|
7121
7233
|
const checkOrder = [...byCheck.keys()].sort((a3, b3) => a3.localeCompare(b3));
|
|
7122
7234
|
for (const checkId of checkOrder) {
|
|
7123
7235
|
const group = sortWithCwd(byCheck.get(checkId));
|
|
7124
|
-
sb.push(`####
|
|
7236
|
+
sb.push(`#### \`${checkId}\` \xB7 ${group.length} finding${group.length === 1 ? "" : "s"}`);
|
|
7125
7237
|
sb.push("");
|
|
7126
7238
|
for (const { r: r4, f: f4 } of group) {
|
|
7127
7239
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -7129,11 +7241,11 @@ function formatMarkdown(p2) {
|
|
|
7129
7241
|
sb.push("");
|
|
7130
7242
|
sb.push(`##### ${findingTitleLine(d3.file, d3.message)}`);
|
|
7131
7243
|
sb.push("");
|
|
7132
|
-
sb.push(`|
|
|
7244
|
+
sb.push(`| | |`);
|
|
7133
7245
|
sb.push(`|:--|:--|`);
|
|
7134
|
-
sb.push(`|
|
|
7135
|
-
sb.push(`|
|
|
7136
|
-
if (d3.file) sb.push(`|
|
|
7246
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7247
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7248
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
7137
7249
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
7138
7250
|
appendSuggestedFix(sb, cwd, f4);
|
|
7139
7251
|
sb.push("");
|
|
@@ -7146,10 +7258,10 @@ function formatMarkdown(p2) {
|
|
|
7146
7258
|
(r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))
|
|
7147
7259
|
)
|
|
7148
7260
|
);
|
|
7149
|
-
sb.push(`###
|
|
7261
|
+
sb.push(`### Info \u2014 ${infos} item${infos === 1 ? "" : "s"}`);
|
|
7150
7262
|
sb.push("");
|
|
7151
7263
|
if (infoFindings.length === 0) {
|
|
7152
|
-
sb.push("
|
|
7264
|
+
sb.push("_No info notes._");
|
|
7153
7265
|
} else {
|
|
7154
7266
|
for (const { r: r4, f: f4 } of infoFindings) {
|
|
7155
7267
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -7157,11 +7269,11 @@ function formatMarkdown(p2) {
|
|
|
7157
7269
|
sb.push("");
|
|
7158
7270
|
sb.push(`#### ${findingTitleLine(d3.file, d3.message)}`);
|
|
7159
7271
|
sb.push("");
|
|
7160
|
-
sb.push(`|
|
|
7272
|
+
sb.push(`| | |`);
|
|
7161
7273
|
sb.push(`|:--|:--|`);
|
|
7162
|
-
sb.push(`|
|
|
7163
|
-
sb.push(`|
|
|
7164
|
-
if (d3.file) sb.push(`|
|
|
7274
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7275
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7276
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
7165
7277
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
7166
7278
|
appendSuggestedFix(sb, cwd, f4);
|
|
7167
7279
|
sb.push("");
|
|
@@ -7169,7 +7281,7 @@ function formatMarkdown(p2) {
|
|
|
7169
7281
|
}
|
|
7170
7282
|
sb.push("");
|
|
7171
7283
|
if (llmAppendix?.trim()) {
|
|
7172
|
-
sb.push("###
|
|
7284
|
+
sb.push("### Appendix");
|
|
7173
7285
|
sb.push("");
|
|
7174
7286
|
sb.push(llmAppendix.trim());
|
|
7175
7287
|
sb.push("");
|
|
@@ -7177,40 +7289,45 @@ function formatMarkdown(p2) {
|
|
|
7177
7289
|
sb.push("---");
|
|
7178
7290
|
sb.push("");
|
|
7179
7291
|
sb.push(
|
|
7180
|
-
|
|
7181
|
-
"Generated by",
|
|
7182
|
-
"report",
|
|
7183
|
-
"FrontGuard",
|
|
7184
|
-
"blueviolet"
|
|
7185
|
-
)
|
|
7186
|
-
);
|
|
7187
|
-
sb.push("");
|
|
7188
|
-
sb.push(
|
|
7189
|
-
"_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)._"
|
|
7292
|
+
"_Report by [FrontGuard](https://github.com/flipkart-incubator/frontguard) \xB7 Configure in `frontguard.config.mjs`_"
|
|
7190
7293
|
);
|
|
7191
7294
|
return sb.join("\n");
|
|
7192
7295
|
}
|
|
7193
7296
|
function formatConsole(p2) {
|
|
7194
7297
|
const { riskScore, mode, stack, pr, results, warns, infos, blocks } = p2;
|
|
7298
|
+
const W3 = 56;
|
|
7299
|
+
const pad = (s3, n3) => s3.slice(0, n3).padEnd(n3);
|
|
7300
|
+
const row = (label, val) => `\u2502 ${import_picocolors.default.dim(label.padEnd(7))} ${pad(val, W3 - 10)} \u2502`;
|
|
7301
|
+
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));
|
|
7302
|
+
const stackStr = [
|
|
7303
|
+
stack.hasNext && "Next.js",
|
|
7304
|
+
stack.hasReactNative && "React Native",
|
|
7305
|
+
!stack.hasNext && !stack.hasReactNative && stack.hasReact && "React",
|
|
7306
|
+
stack.hasTypeScript ? "TypeScript" : "JavaScript",
|
|
7307
|
+
stack.isMonorepo && "monorepo"
|
|
7308
|
+
].filter(Boolean).join(" \xB7 ");
|
|
7195
7309
|
const lines = [];
|
|
7196
|
-
|
|
7197
|
-
lines.push(
|
|
7198
|
-
lines.push(
|
|
7199
|
-
lines.push(
|
|
7200
|
-
|
|
7201
|
-
);
|
|
7310
|
+
const border = "\u2500".repeat(W3);
|
|
7311
|
+
lines.push(import_picocolors.default.dim(`\u250C${border}\u2510`));
|
|
7312
|
+
lines.push(row("Risk", riskColored));
|
|
7313
|
+
lines.push(row("Mode", mode === "enforce" ? import_picocolors.default.yellow("enforce") : "warn only"));
|
|
7314
|
+
lines.push(row("Stack", stackStr));
|
|
7202
7315
|
if (pr) {
|
|
7203
|
-
const sz = `${pr.additions + pr.deletions} lines
|
|
7204
|
-
lines.push(
|
|
7316
|
+
const sz = `${pr.additions + pr.deletions} lines +${pr.additions} / -${pr.deletions} \xB7 ${pr.changedFiles} files`;
|
|
7317
|
+
lines.push(row("PR", sz));
|
|
7205
7318
|
}
|
|
7206
|
-
lines.push(
|
|
7207
|
-
const statusLine = blocks > 0 ? import_picocolors.default.red(`\
|
|
7208
|
-
lines.push(`\u2502 ${statusLine
|
|
7319
|
+
lines.push(import_picocolors.default.dim(`\u251C${border}\u2524`));
|
|
7320
|
+
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");
|
|
7321
|
+
lines.push(`\u2502 ${pad(statusLine, W3 - 2)} \u2502`);
|
|
7322
|
+
lines.push(import_picocolors.default.dim(`\u251C${border}\u2524`));
|
|
7209
7323
|
for (const r4 of results) {
|
|
7210
|
-
const
|
|
7211
|
-
|
|
7324
|
+
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("!");
|
|
7325
|
+
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`;
|
|
7326
|
+
const name = r4.checkId.padEnd(22);
|
|
7327
|
+
const label = ` ${icon} ${name} ${countStr}`;
|
|
7328
|
+
lines.push(`\u2502${pad(label, W3 + 1)}\u2502`);
|
|
7212
7329
|
}
|
|
7213
|
-
lines.push(import_picocolors.default.
|
|
7330
|
+
lines.push(import_picocolors.default.dim(`\u2514${border}\u2518`));
|
|
7214
7331
|
return lines.join("\n");
|
|
7215
7332
|
}
|
|
7216
7333
|
|