@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.
Files changed (4) hide show
  1. package/README.md +221 -193
  2. package/dist/cli.js +677 -560
  3. package/dist/cli.js.map +1 -1
  4. 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
- // \u2500\u2500\u2500 Mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
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
- // \u2500\u2500\u2500 Shared org config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2413
- // Inherit from an installed npm package; fields are deep-merged.
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\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
2417
- // Inline pattern checks \u2014 \`check(file)\` returns true when violated.
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: 'info',
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
- // \u2500\u2500\u2500 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
2433
- // Runs ESLint using your project config on PR-scoped files.
2434
- // Default ignores frontguard.config.{mjs,js,cjs} (tool config, not app code).
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
- // ignorePatterns: [
2439
- // '**/frontguard.config.mjs',
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\u2500 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
2446
- // Checks that files match Prettier formatting.
2447
- // Default ignores the same FrontGuard config filenames as ESLint.
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\u2500 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\u2500\u2500\u2500
2459
- // Runs \`tsc --noEmit\` to surface type errors before merge.
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: ['--noEmit'], // extra tsc flags
2462
+ // tscArgs: [], // extra flags, e.g. ['--strict']
2463
2463
  // },
2464
2464
 
2465
- // \u2500\u2500\u2500 Secrets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 changed files for patterns that look like leaked secrets.
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\u2500 PR 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
2470
- // Validates PR metadata: description length, sections, AI disclosure.
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', // 'info' | 'warn' | 'block'
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\u2500 PR 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
2481
- // Flags oversized PRs based on total changed lines.
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
- // warnLines: 400,
2485
- // softBlockLines: 800,
2486
- // // Custom tiers (overrides warnLines/softBlockLines when set):
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: 'Consider splitting (\${lines} lines)' },
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\u2500 TS any delta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2494
- // Counts new \`any\` usage introduced in the diff vs merge-base.
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', // 'info' | 'warn' | 'block'
2498
- // baseRef: 'main', // fallback when BITBUCKET_PR_DESTINATION_BRANCH unset
2499
- // maxAdded: 0, // 0 = report only; >0 = trigger gate when exceeded
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\u2500 Circular dependencies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2503
- // Runs \`madge\` to find import cycles in your source.
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'], // entry directories for madge
2508
- // extraArgs: [],
2513
+ // entries: ['src'], // directories to analyse
2514
+ // extraArgs: [], // extra flags forwarded to madge
2509
2515
  // },
2510
2516
 
2511
- // \u2500\u2500\u2500 Dead code \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2512
- // Runs \`ts-prune\` to find unused exports in the TypeScript project.
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
- // extraArgs: [],
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\u2500 React Native (when react-native is a dependency) \u2500\u2500\u2500\u2500\u2500\u2500
2521
- // Metro config, optional @rnx-kit/align-deps / doctor, SwiftLint on PR .swift, native hints.
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
- // bundleSizeStrategy options:
2537
- // 'auto' \u2014 auto-detect from package.json (Next\u2192next, Vite\u2192vite, CRA\u2192cra, else glob)
2538
- // 'next' \u2014 parse "First Load JS shared by all" from \`next build\` stdout
2539
- // 'vite' \u2014 sum dist/assets/*.js after \`vite build\`
2540
- // 'cra' \u2014 parse gzipped JS total from \`react-scripts build\` stdout
2541
- // 'glob' \u2014 sum raw bytes under measureGlobs (generic fallback)
2542
- // 'custom' \u2014 run bundleSizeCommand, read a single number (bytes) from stdout
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
- // runBuild: true, // false = skip build, measure existing files
2548
- // buildCommand: 'npm run build', // your production build command
2549
- // bundleSizeStrategy: 'auto',
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, // max allowed growth vs baseline; null = no limit
2555
- // maxTotalBytes: null, // absolute cap; null = no limit
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\u2500 Core Web Vitals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2559
- // Static hints for LCP-friendly images, main-thread hygiene, etc.
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\u2500 AI-assisted review (UNDER DEVELOPMENT \u2014 off by default) \u2500
2568
- // Static heuristics on @frontguard-ai regions or AI-disclosed PRs. Not used in CI until you enable.
2569
- // aiAssistedReview: {
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' \u2014 only scan @frontguard-ai:start \u2026 :end regions
2573
- // // 'pr-disclosure' \u2014 only when PR body discloses AI use
2574
- // // 'both' \u2014 scan decorators when present; otherwise PR disclosure triggers full scan
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, // promote secret findings to 'block' in AI PRs
2578
- // tsAnyDeltaToBlock: true, // promote any-delta findings to 'block' in AI PRs
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\u2500 LLM review (UNDER DEVELOPMENT \u2014 off by default) \u2500\u2500\u2500\u2500\u2500\u2500\u2500
2583
- // When enabled: loads .cursor/rules, AGENTS.md for prompt context. Cloud keys or Ollama.
2584
- // ollama: ollama serve && ollama pull llama3.2
2585
- // paste: frontguard run --append ./.frontguard/review-notes.md
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: true,
2588
- // provider: 'ollama', // 'openai' | 'anthropic' | 'ollama'
2589
- // model: 'llama3.2',
2590
- // ollamaUrl: 'http://127.0.0.1:11434',
2591
- // apiKeyEnv: 'OPENAI_API_KEY', // env var name for OpenAI/Anthropic
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, // ask model for per-finding fix hints (slow)
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
- var FRONTGUARD_STEP_YAML = ` - step:
2622
- name: FrontGuard \u2014 report + PR comment
2623
- caches:
2624
- - node
2625
- artifacts:
2626
- - frontguard-report.html
2627
- - frontguard-report.md
2628
- - frontguard-pr-comment.partial.md
2629
- script:
2630
- - corepack enable
2631
- - yarn install --immutable || yarn install
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, FRONTGUARD_STEP_YAML, ...after].join("\n");
2796
+ return [...before, stepYaml, ...after].join("\n");
2742
2797
  }
2743
2798
  const result = [...lines];
2744
- result.splice(prIdx + 1, 0, " '**':", FRONTGUARD_STEP_YAML);
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
- FRONTGUARD_STEP_YAML
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
- FRONTGUARD_STEP_YAML,
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("merged FrontGuard step into bitbucket-pipelines.yml pull-requests");
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
- FRONTGUARD_STEP_YAML,
2898
+ stepYaml,
2842
2899
  ""
2843
2900
  ].join("\n"),
2844
2901
  "utf8"
2845
2902
  );
2846
- actions.push("created bitbucket-pipelines.yml with FrontGuard step");
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(s), ${blocks} blocking`;
2869
- return `${n3} issue(s)`;
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
- lines.push(`#### RISK score: ${riskScore}`);
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 name | Status |");
2890
- lines.push("| --- | --- |");
2947
+ lines.push("| Check | Issues |");
2948
+ lines.push("| :-- | :-- |");
2891
2949
  for (const r4 of failing) {
2892
- lines.push(`| ${mdTableCell(r4.checkId)} | ${mdTableCell(statusForPrTable(r4))} |`);
2950
+ lines.push(`| \`${mdTableCell(r4.checkId)}\` | ${mdTableCell(statusForPrTable(r4))} |`);
2893
2951
  }
2894
2952
  } else {
2895
- lines.push("_All checks passed for this run._");
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} 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 statusDot(r4) {
6382
- if (r4.skipped) return '<span class="dot dot-skip" title="Skipped"></span>';
6383
- if (r4.findings.length === 0) return '<span class="dot dot-ok" title="Clean"></span>';
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="dot dot-block" title="Blocking"></span>';
6386
- return '<span class="dot dot-warn" title="Issues"></span>';
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 fixBlock = f4.suggestedFix ? `<div class="suggested-fix"><div class="fix-label">Suggested fix <span class="pill pill-llm">LLM</span></div><div class="fix-md">${escapeHtml(f4.suggestedFix.summary)}</div>${f4.suggestedFix.code ? `<pre class="code"><code>${escapeHtml(f4.suggestedFix.code)}</code></pre>` : ""}<p class="disclaimer">Suggestions are non-binding; review before applying.</p></div>` : "";
6515
- const hintRow = d3.detail && !d3.detail.includes("\n") && d3.detail.length <= 220 && !d3.detail.includes("|") ? `<tr><th>Hint</th><td>${escapeHtml(d3.detail)}</td></tr>` : "";
6516
- const detailFence = d3.detail && (d3.detail.includes("\n") || d3.detail.length > 220 || d3.detail.includes("|")) ? `<pre class="code"><code>${escapeHtml(d3.detail)}</code></pre>` : "";
6517
- return `<article class="card ${sevClass}"><div class="card-title">${escapeHtml(d3.file ?? "\u2014")}</div><p class="card-msg">${escapeHtml(d3.message)}</p><table class="meta"><tr><th>Check</th><td><code>${escapeHtml(r4.checkId)}</code></td></tr><tr><th>Rule</th><td><code>${escapeHtml(f4.id)}</code></td></tr>${d3.file ? `<tr><th>File</th><td><code>${escapeHtml(d3.file)}</code></td></tr>` : ""}${hintRow}</table>${detailFence}${fixBlock}</article>`;
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 checkRows = renderCheckTableRows(results);
6536
- const blockItems = sortFindings(
6537
- cwd,
6538
- results.flatMap(
6539
- (r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))
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 checkOrder = [...byCheck.keys()].sort((a3, b3) => a3.localeCompare(b3));
6561
- const blockingHtml = blockItems.length === 0 ? '<p class="empty-state">No blocking findings.</p>' : blockItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
6562
- let warningsHtml = "";
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
- warningsHtml = '<p class="empty-state">No warnings.</p>';
6487
+ warnsHtml = '<p class="empty-note">No warnings.</p>';
6565
6488
  } else {
6566
- for (const cid of checkOrder) {
6489
+ for (const cid of warnCheckOrder) {
6567
6490
  const group = sortFindings(cwd, byCheck.get(cid));
6568
- const cards = group.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
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-state muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
6573
- const prBlock = pr && lines != null ? `<tr><th>Pull request</th><td>${lines} lines changed (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files</td></tr>` : "";
6574
- const appendix = llmAppendix?.trim() ? `<section class="section"><h2 class="h2">Appendix</h2><pre class="md-raw">${escapeHtml(llmAppendix.trim())}</pre></section>` : "";
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 \u2014 Report</title>
6503
+ <title>FrontGuard Report</title>
6581
6504
  <style>
6582
6505
  :root {
6583
6506
  --bg: #f8fafc;
6584
6507
  --surface: #ffffff;
6585
- --text: #0f172a;
6586
- --muted: #64748b;
6508
+ --surface-alt: #f1f5f9;
6587
6509
  --border: #e2e8f0;
6588
- --accent: #4f46e5;
6589
- --accent-soft: #eef2ff;
6590
- --block: #dc2626;
6591
- --warn: #d97706;
6592
- --info: #0284c7;
6593
- --ok: #16a34a;
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
- --shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
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
- * { box-sizing: border-box; }
6533
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
6598
6534
  body {
6599
- margin: 0;
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.55;
6604
- font-size: 15px;
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.75rem;
6615
- font-weight: 600;
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.35rem;
6551
+ margin-bottom: 0.75rem;
6620
6552
  }
6621
- h1 {
6622
- font-size: 1.75rem;
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
- .metrics {
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.65rem;
6632
- margin-bottom: 0.5rem;
6583
+ gap: 0.5rem;
6584
+ margin-top: 1rem;
6585
+ align-items: center;
6633
6586
  }
6634
- .metric {
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
- padding: 0.5rem 0.9rem;
6639
- box-shadow: var(--shadow);
6609
+ box-shadow: var(--shadow-sm);
6610
+ overflow: hidden;
6611
+ margin-bottom: 2rem;
6612
+ }
6613
+ .meta-row {
6640
6614
  display: flex;
6641
- align-items: center;
6642
- gap: 0.5rem;
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
- .metric-label { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
6645
- .metric-value { font-weight: 600; font-size: 0.95rem; }
6646
- .risk-low { color: var(--ok); }
6647
- .risk-med { color: var(--warn); }
6648
- .risk-high { color: var(--block); }
6649
- .section { margin-top: 2.25rem; }
6650
- .h2 {
6651
- font-size: 1rem;
6652
- font-weight: 600;
6653
- margin: 0 0 0.85rem;
6654
- color: var(--text);
6655
- letter-spacing: -0.02em;
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
- .snapshot {
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
- border: 1px solid var(--border);
6665
- box-shadow: var(--shadow);
6650
+ box-shadow: var(--shadow-sm);
6651
+ font-size: 0.855rem;
6666
6652
  }
6667
- .snapshot th, .snapshot td {
6668
- padding: 0.65rem 1rem;
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
- .snapshot tr:last-child th, .snapshot tr:last-child td { border-bottom: none; }
6673
- .snapshot th {
6674
- width: 9rem;
6675
- color: var(--muted);
6676
- font-weight: 500;
6677
- background: #f1f5f9;
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
- ${CHECKS_TABLE_STYLES}
6680
- .panel {
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
- box-shadow: var(--shadow);
6716
+ overflow: hidden;
6686
6717
  }
6687
- .panel summary {
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
- .panel summary::-webkit-details-marker { display: none; }
6698
- .panel[open] summary { border-bottom: 1px solid var(--border); }
6699
- .panel-body { padding: 0.75rem 1rem 1rem; }
6700
- .nested summary { font-weight: 500; color: var(--warn); }
6701
- .summary-count {
6702
- font-size: 0.8rem;
6703
- font-weight: 500;
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: 8px;
6712
- padding: 1rem;
6713
- margin-bottom: 0.65rem;
6714
- background: #fafafa;
6760
+ border-radius: var(--radius-sm);
6761
+ background: var(--surface);
6762
+ padding: 0.85rem 1rem;
6715
6763
  }
6716
- .card:last-child { margin-bottom: 0; }
6717
- .card.sev-block { border-left: 3px solid var(--block); }
6718
- .card.sev-warn { border-left: 3px solid var(--warn); }
6719
- .card.sev-info { border-left: 3px solid var(--info); }
6720
- .card-title { font-size: 0.8rem; font-weight: 600; color: var(--muted); margin-bottom: 0.35rem; }
6721
- .card-msg { margin: 0 0 0.65rem; font-size: 0.9rem; }
6722
- table.meta { width: 100%; font-size: 0.78rem; border-collapse: collapse; margin: 0.35rem 0 0; }
6723
- table.meta th { text-align: left; color: var(--muted); width: 4.5rem; padding: 0.2rem 0.5rem 0.2rem 0; vertical-align: top; }
6724
- table.meta td { padding: 0.2rem 0; }
6725
- table.meta code { font-size: 0.85em; background: #f1f5f9; padding: 0.1rem 0.35rem; border-radius: 4px; }
6726
- .muted { color: var(--muted); }
6727
- .empty-state { margin: 0; font-size: 0.9rem; color: var(--muted); }
6728
- pre.code {
6729
- margin: 0.5rem 0 0;
6730
- padding: 0.75rem;
6731
- background: #f1f5f9;
6732
- border-radius: 6px;
6733
- overflow: auto;
6734
- font-size: 0.78rem;
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
- pre.code code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }
6738
- .suggested-fix { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px dashed var(--border); }
6739
- .fix-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); margin-bottom: 0.35rem; }
6740
- .pill-llm { background: var(--accent-soft); color: var(--accent); padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.65rem; }
6741
- .fix-md { font-size: 0.85rem; white-space: pre-wrap; margin: 0.25rem 0 0.5rem; }
6742
- .disclaimer { font-size: 0.72rem; color: var(--muted); margin: 0.5rem 0 0; }
6743
- pre.md-raw {
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.85rem;
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
- margin: 0;
6853
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
6854
+ line-height: 1.55;
6751
6855
  }
6752
- footer {
6753
- margin-top: 3rem;
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.8rem;
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
- <header class="hero">
6873
+ <div class="wrap">
6874
+
6875
+ <header class="header">
6765
6876
  <div class="brand">FrontGuard</div>
6766
- <h1>Code review report</h1>
6767
- <div class="metrics">
6768
- <div class="metric"><span class="metric-label">Risk</span><span class="metric-value ${riskClass}">${riskScore}</span></div>
6769
- <div class="metric"><span class="metric-label">Mode</span><span class="metric-value">${escapeHtml(modeLabel)}</span></div>
6770
- <div class="metric"><span class="metric-label">Blocking</span><span class="metric-value">${blocks}</span></div>
6771
- <div class="metric"><span class="metric-label">Warnings</span><span class="metric-value">${warns}</span></div>
6772
- <div class="metric"><span class="metric-label">Info</span><span class="metric-value">${infos}</span></div>
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
- <section class="section">
6777
- <h2 class="h2">Overview</h2>
6778
- <table class="snapshot">
6779
- <tr><th>Risk score</th><td><strong>${riskScore}</strong> <span class="muted">\u2014 heuristic</span></td></tr>
6780
- <tr><th>Mode</th><td>${escapeHtml(modeLabel)}</td></tr>
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="h2">Checks</h2>
6788
- <table class="results">
6789
- <thead><tr><th></th><th>Check</th><th>Status</th><th>#</th><th>Time</th></tr></thead>
6790
- <tbody>${checkRows}</tbody>
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="h2">Findings</h2>
6796
- <details class="panel"><summary>Blocking <span class="summary-count">${blocks}</span></summary><div class="panel-body">${blockingHtml}</div></details>
6797
- <details class="panel"><summary>Warnings <span class="summary-count">${warns}</span></summary><div class="panel-body">${warningsHtml}</div></details>
6798
- <details class="panel"><summary>Info <span class="summary-count">${infos}</span></summary><div class="panel-body">${infoHtml}</div></details>
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
- ${appendix}
6936
+ ${appendixHtml}
6802
6937
 
6803
- <footer>
6804
- <p>Static report \u2014 open in any browser. Generated by <strong>FrontGuard</strong>.</p>
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("## \u2728 FrontGuard review brief");
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 level", "risk", riskScore, riskShieldColor(riskScore)),
7009
- mdShield("CI mode", "mode", modeLabel, modeShieldColor(mode)),
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 notes", String(infos), countShieldColor("info", infos))
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(`| **Composite risk** | ${riskEmoji(riskScore)} **${riskScore}** \u2014 heuristic from blocks, warnings, and PR size. |`);
7023
- sb.push(
7024
- `| **Gate mode** | ${mode === "enforce" ? "\u{1F512} **Enforce** \u2014 CI fails when a `block` finding is present." : "\u{1F6C8} **Warn only** \u2014 findings are advisory unless you use `--enforce`."} |`
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
- "> **\u{1F916} AI-assisted PR** \u2014 Stricter static checks run on changed files (security / footguns; secrets & `any` deltas may escalate). This does not replace human review for behavior or product rules."
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("### \u{1F4CB} Check results");
7171
+ sb.push("### Checks");
7055
7172
  sb.push("");
7056
- sb.push("| | Check | Status | Findings | Duration |");
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 > 120 ? `${why.slice(0, 117)}\u2026` : why;
7064
- status = `\u23ED\uFE0F **Skipped** \u2014 ${short}`;
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 = "\u2705 **Clean**";
7183
+ status = "\u2014";
7067
7184
  } else {
7068
- status = "\u26A0\uFE0F **Issues**";
7185
+ status = String(r4.findings.length);
7069
7186
  }
7070
- const nFind = r4.skipped ? "\u2014" : String(r4.findings.length);
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(`### \u{1F6D1} Blocking \u2014 ${blocks} issue${blocks === 1 ? "" : "s"}`);
7195
+ sb.push(`### Blocking \u2014 ${blocks} issue${blocks === 1 ? "" : "s"}`);
7082
7196
  sb.push("");
7083
7197
  if (blockFindings.length === 0) {
7084
- sb.push("*\u2705 No blocking findings \u2014 nothing mapped to severity `block`.*");
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(`| Field | Value |`);
7206
+ sb.push(`| | |`);
7093
7207
  sb.push(`|:--|:--|`);
7094
- sb.push(`| **Check** | \`${r4.checkId}\` |`);
7095
- sb.push(`| **Rule / id** | \`${f4.id}\` |`);
7096
- if (d3.file) sb.push(`| **File** | \`${d3.file}\` |`);
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("*\u{1F389} No warnings \u2014 nice work.*");
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(`#### \u{1F4C2} \`${checkId}\` \xB7 ${group.length} finding${group.length === 1 ? "" : "s"}`);
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(`| Field | Value |`);
7244
+ sb.push(`| | |`);
7133
7245
  sb.push(`|:--|:--|`);
7134
- sb.push(`| **Check** | \`${r4.checkId}\` |`);
7135
- sb.push(`| **Rule / id** | \`${f4.id}\` |`);
7136
- if (d3.file) sb.push(`| **File** | \`${d3.file}\` |`);
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(`### \u2139\uFE0F Info & notes \u2014 ${infos} item${infos === 1 ? "" : "s"}`);
7261
+ sb.push(`### Info \u2014 ${infos} item${infos === 1 ? "" : "s"}`);
7150
7262
  sb.push("");
7151
7263
  if (infoFindings.length === 0) {
7152
- sb.push("*No info-level notes.*");
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(`| Field | Value |`);
7272
+ sb.push(`| | |`);
7161
7273
  sb.push(`|:--|:--|`);
7162
- sb.push(`| **Check** | \`${r4.checkId}\` |`);
7163
- sb.push(`| **Rule / id** | \`${f4.id}\` |`);
7164
- if (d3.file) sb.push(`| **File** | \`${d3.file}\` |`);
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("### \u{1F916} AI / manual appendix");
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
- mdShield(
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
- lines.push(import_picocolors.default.bold("\u250C\u2500 FrontGuard review brief \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
7197
- lines.push(`\u2502 ${import_picocolors.default.dim("Risk:")} ${riskScore.padEnd(43)} \u2502`);
7198
- lines.push(`\u2502 ${import_picocolors.default.dim("Mode:")} ${mode.padEnd(42)} \u2502`);
7199
- lines.push(
7200
- `\u2502 ${import_picocolors.default.dim("Stack:")} ${(stack.hasNext ? "Next.js " : "") + (stack.hasReactNative ? "RN " : "") + (stack.hasReact ? "React " : "") + (stack.hasTypeScript ? "TS" : "JS")}`.padEnd(56).slice(0, 56) + " \u2502"
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 (+${pr.additions}/-${pr.deletions}) \xB7 ${pr.changedFiles} files`;
7204
- lines.push(`\u2502 ${import_picocolors.default.dim("PR:")} ${sz.slice(0, 49).padEnd(49)} \u2502`);
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("\u2502 " + "".padEnd(53) + " \u2502");
7207
- const statusLine = blocks > 0 ? import_picocolors.default.red(`\u2716 ${blocks} blocking`) : warns === 0 && infos === 0 ? import_picocolors.default.green("\u2713 No findings") : import_picocolors.default.yellow(`\u26A0 ${warns} warnings \xB7 ${infos} info`);
7208
- lines.push(`\u2502 ${statusLine}`.padEnd(64).slice(0, 64) + " \u2502");
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 label = r4.skipped ? import_picocolors.default.dim(` ${r4.checkId}: skipped`) : ` ${r4.checkId}: ${r4.findings.length} issues`;
7211
- lines.push(`\u2502${label.slice(0, 54).padEnd(54)}\u2502`);
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.bold("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
7330
+ lines.push(import_picocolors.default.dim(`\u2514${border}\u2518`));
7214
7331
  return lines.join("\n");
7215
7332
  }
7216
7333