@cleartrip/frontguard 0.3.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +221 -193
  2. package/dist/cli.js +600 -530
  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
  `;
@@ -2865,8 +2875,8 @@ function mdTableCell(s3) {
2865
2875
  function statusForPrTable(r4) {
2866
2876
  const n3 = r4.findings.length;
2867
2877
  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)`;
2878
+ if (blocks > 0) return `${n3} issue${n3 === 1 ? "" : "s"}, ${blocks} blocking`;
2879
+ return `${n3} issue${n3 === 1 ? "" : "s"}`;
2870
2880
  }
2871
2881
  function checkNeedsRow(r4) {
2872
2882
  return !r4.skipped && r4.findings.length > 0;
@@ -2882,21 +2892,21 @@ function formatBitbucketPrSnippet(report) {
2882
2892
  const reportUrl = publicReport || FRONTGUARD_REPORT_URL_PLACEHOLDER;
2883
2893
  const { riskScore, results } = report;
2884
2894
  const lines = [];
2885
- lines.push(`#### RISK score: ${riskScore}`);
2895
+ const riskEmoji2 = riskScore === "HIGH" ? "\u{1F534}" : riskScore === "MEDIUM" ? "\u{1F7E1}" : "\u{1F7E2}";
2896
+ lines.push(`**FrontGuard** \xB7 ${riskEmoji2} ${riskScore} risk`);
2886
2897
  lines.push("");
2887
2898
  const failing = results.filter(checkNeedsRow);
2888
2899
  if (failing.length > 0) {
2889
- lines.push("| Check name | Status |");
2890
- lines.push("| --- | --- |");
2900
+ lines.push("| Check | Issues |");
2901
+ lines.push("| :-- | :-- |");
2891
2902
  for (const r4 of failing) {
2892
- lines.push(`| ${mdTableCell(r4.checkId)} | ${mdTableCell(statusForPrTable(r4))} |`);
2903
+ lines.push(`| \`${mdTableCell(r4.checkId)}\` | ${mdTableCell(statusForPrTable(r4))} |`);
2893
2904
  }
2894
2905
  } else {
2895
- lines.push("_All checks passed for this run._");
2906
+ lines.push("_All checks passed._");
2896
2907
  }
2897
2908
  lines.push("");
2898
2909
  lines.push(DETAILED_REPORT_LINE);
2899
- lines.push("");
2900
2910
  lines.push(reportUrl.endsWith("\n") ? reportUrl.slice(0, -1) : reportUrl);
2901
2911
  lines.push("");
2902
2912
  return lines.join("\n");
@@ -6371,438 +6381,519 @@ function sortFindings(cwd, items) {
6371
6381
  });
6372
6382
  }
6373
6383
  function formatDuration(ms) {
6374
- if (ms < 1e3) return `${ms} ms`;
6384
+ if (ms < 1e3) return `${ms}ms`;
6375
6385
  const s3 = Math.round(ms / 1e3);
6376
6386
  if (s3 < 60) return `${s3}s`;
6377
6387
  const m3 = Math.floor(s3 / 60);
6378
6388
  const r4 = s3 % 60;
6379
6389
  return r4 ? `${m3}m ${r4}s` : `${m3}m`;
6380
6390
  }
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>';
6391
+ function checkStatusIcon(r4) {
6392
+ if (r4.skipped) return '<span class="status-icon skip" title="Skipped">\u2013</span>';
6393
+ if (r4.findings.length === 0) return '<span class="status-icon pass" title="Passed">\u2713</span>';
6384
6394
  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");
6395
+ return '<span class="status-icon block" title="Blocking">\u2715</span>';
6396
+ return '<span class="status-icon warn" title="Warnings">!</span>';
6510
6397
  }
6511
6398
  function renderFindingCard(cwd, r4, f4) {
6512
6399
  const d3 = normalizeFinding(cwd, f4);
6513
6400
  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>`;
6401
+ const sevLabel = f4.severity === "block" ? "blocking" : f4.severity === "warn" ? "warning" : "info";
6402
+ const meta = [];
6403
+ meta.push(`<span class="tag tag-check">${escapeHtml(r4.checkId)}</span>`);
6404
+ meta.push(`<span class="tag tag-rule">${escapeHtml(f4.id)}</span>`);
6405
+ if (d3.file) meta.push(`<span class="tag tag-file">${escapeHtml(d3.file)}</span>`);
6406
+ const hintHtml = d3.detail && !d3.detail.includes("\n") && d3.detail.length <= 240 && !d3.detail.includes("|") ? `<p class="card-hint">${escapeHtml(d3.detail)}</p>` : "";
6407
+ const detailHtml = d3.detail && (d3.detail.includes("\n") || d3.detail.length > 240 || d3.detail.includes("|")) ? `<pre class="code-block"><code>${escapeHtml(d3.detail)}</code></pre>` : "";
6408
+ const fixHtml = f4.suggestedFix ? `<div class="fix-box"><div class="fix-header">Suggested fix <span class="badge badge-llm">LLM</span></div><p class="fix-summary">${escapeHtml(f4.suggestedFix.summary)}</p>${f4.suggestedFix.code ? `<pre class="code-block"><code>${escapeHtml(f4.suggestedFix.code)}</code></pre>` : ""}<p class="fix-disclaimer">Non-binding \u2014 review before applying.</p></div>` : "";
6409
+ return `<div class="card ${sevClass}"><div class="card-header"><span class="sev-badge ${sevClass}">${sevLabel}</span><p class="card-message">${escapeHtml(d3.message)}</p></div><div class="card-meta">${meta.join("")}</div>${hintHtml}${detailHtml}${fixHtml}</div>`;
6410
+ }
6411
+ function renderCheckTableRows(_cwd, results) {
6412
+ return results.map((r4) => {
6413
+ const icon = checkStatusIcon(r4);
6414
+ const statusText = r4.skipped ? `<span class="status-text muted">Skipped \u2014 ${escapeHtml(r4.skipped)}</span>` : r4.findings.length === 0 ? '<span class="status-text ok">Passed</span>' : r4.findings.some((f4) => f4.severity === "block") ? `<span class="status-text danger">${r4.findings.length} issue${r4.findings.length === 1 ? "" : "s"}</span>` : `<span class="status-text caution">${r4.findings.length} issue${r4.findings.length === 1 ? "" : "s"}</span>`;
6415
+ const help = escapeHtml(getCheckDescription(r4.checkId));
6416
+ const countCell = r4.skipped ? "\u2014" : String(r4.findings.length);
6417
+ const timeCell = formatDuration(r4.durationMs);
6418
+ return `<tr><td class="col-icon">${icon}</td><td class="col-check"><span class="check-name">${escapeHtml(r4.checkId)}</span><span class="check-tooltip">${help}</span></td><td class="col-status">${statusText}</td><td class="col-count">${countCell}</td><td class="col-time">${timeCell}</td></tr>`;
6419
+ }).join("\n");
6518
6420
  }
6519
6421
  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";
6422
+ const { cwd, riskScore, mode, stack, pr, results, warns, infos, blocks, lines, llmAppendix } = p2;
6534
6423
  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
- );
6424
+ const riskIcon = riskScore === "LOW" ? "\u2713" : riskScore === "MEDIUM" ? "!" : "\u2715";
6425
+ const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
6426
+ const checkRows = renderCheckTableRows(cwd, results);
6427
+ const blockItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))));
6428
+ const warnItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))));
6429
+ const infoItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))));
6554
6430
  const byCheck = /* @__PURE__ */ new Map();
6555
6431
  for (const item of warnItems) {
6556
6432
  const list = byCheck.get(item.r.checkId) ?? [];
6557
6433
  list.push(item);
6558
6434
  byCheck.set(item.r.checkId, list);
6559
6435
  }
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 = "";
6436
+ const warnCheckOrder = [...byCheck.keys()].sort();
6437
+ const blockHtml = blockItems.length === 0 ? '<p class="empty-note">No blocking findings.</p>' : blockItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
6438
+ let warnsHtml = "";
6563
6439
  if (warnItems.length === 0) {
6564
- warningsHtml = '<p class="empty-state">No warnings.</p>';
6440
+ warnsHtml = '<p class="empty-note">No warnings.</p>';
6565
6441
  } else {
6566
- for (const cid of checkOrder) {
6442
+ for (const cid of warnCheckOrder) {
6567
6443
  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>`;
6444
+ warnsHtml += `<div class="group"><div class="group-label"><span class="group-name">${escapeHtml(cid)}</span><span class="group-count">${group.length}</span></div>${group.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n")}</div>`;
6570
6445
  }
6571
6446
  }
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>` : "";
6447
+ const infoHtml = infoItems.length === 0 ? '<p class="empty-note muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
6448
+ const prRow = pr && lines != null ? `<div class="meta-row"><span class="meta-label">PR size</span><span class="meta-value">${lines} lines (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files</span></div>` : "";
6449
+ const appendixHtml = llmAppendix?.trim() ? `<section class="section"><h2 class="section-title">Appendix</h2><pre class="raw-pre">${escapeHtml(llmAppendix.trim())}</pre></section>` : "";
6450
+ const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16) + " UTC";
6575
6451
  return `<!DOCTYPE html>
6576
6452
  <html lang="en">
6577
6453
  <head>
6578
6454
  <meta charset="utf-8" />
6579
6455
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6580
- <title>FrontGuard \u2014 Report</title>
6456
+ <title>FrontGuard Report</title>
6581
6457
  <style>
6582
6458
  :root {
6583
6459
  --bg: #f8fafc;
6584
6460
  --surface: #ffffff;
6585
- --text: #0f172a;
6586
- --muted: #64748b;
6461
+ --surface-alt: #f1f5f9;
6587
6462
  --border: #e2e8f0;
6588
- --accent: #4f46e5;
6589
- --accent-soft: #eef2ff;
6590
- --block: #dc2626;
6591
- --warn: #d97706;
6592
- --info: #0284c7;
6593
- --ok: #16a34a;
6463
+ --border-light: #f1f5f9;
6464
+ --text: #0f172a;
6465
+ --text-secondary: #475569;
6466
+ --muted: #94a3b8;
6467
+ --accent: #6366f1;
6468
+ --c-block: #dc2626;
6469
+ --c-block-bg: #fef2f2;
6470
+ --c-block-border: #fecaca;
6471
+ --c-warn: #d97706;
6472
+ --c-warn-bg: #fffbeb;
6473
+ --c-warn-border: #fde68a;
6474
+ --c-info: #2563eb;
6475
+ --c-info-bg: #eff6ff;
6476
+ --c-info-border: #bfdbfe;
6477
+ --c-ok: #16a34a;
6478
+ --c-ok-bg: #f0fdf4;
6479
+ --radius-sm: 6px;
6594
6480
  --radius: 10px;
6595
- --shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
6481
+ --radius-lg: 14px;
6482
+ --shadow-sm: 0 1px 2px rgba(0,0,0,.05);
6483
+ --shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
6484
+ font-size: 15px;
6596
6485
  }
6597
- * { box-sizing: border-box; }
6486
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
6598
6487
  body {
6599
- margin: 0;
6600
- font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
6488
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
6601
6489
  background: var(--bg);
6602
6490
  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;
6491
+ line-height: 1.6;
6492
+ padding: 2.5rem clamp(1rem, 5vw, 3rem) 5rem;
6612
6493
  }
6494
+ .wrap { max-width: 860px; margin: 0 auto; }
6495
+
6496
+ /* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6497
+ .header { margin-bottom: 2.5rem; }
6613
6498
  .brand {
6614
- font-size: 0.75rem;
6615
- font-weight: 600;
6499
+ font-size: 0.7rem;
6500
+ font-weight: 700;
6616
6501
  letter-spacing: 0.12em;
6617
6502
  text-transform: uppercase;
6618
6503
  color: var(--muted);
6619
- margin-bottom: 0.35rem;
6504
+ margin-bottom: 0.75rem;
6620
6505
  }
6621
- h1 {
6622
- font-size: 1.75rem;
6506
+ .page-title {
6507
+ font-size: 1.6rem;
6623
6508
  font-weight: 700;
6624
6509
  letter-spacing: -0.03em;
6625
- margin: 0 0 1rem;
6626
6510
  color: var(--text);
6511
+ margin-bottom: 1.25rem;
6512
+ line-height: 1.2;
6513
+ }
6514
+
6515
+ /* \u2500\u2500 Risk pill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6516
+ .risk-pill {
6517
+ display: inline-flex;
6518
+ align-items: center;
6519
+ gap: 0.45rem;
6520
+ padding: 0.35rem 0.8rem;
6521
+ border-radius: 999px;
6522
+ font-size: 0.82rem;
6523
+ font-weight: 700;
6524
+ letter-spacing: 0.02em;
6525
+ border: 1.5px solid;
6627
6526
  }
6628
- .metrics {
6527
+ .risk-low { color: var(--c-ok); background: var(--c-ok-bg); border-color: #bbf7d0; }
6528
+ .risk-med { color: var(--c-warn); background: var(--c-warn-bg); border-color: var(--c-warn-border); }
6529
+ .risk-high { color: var(--c-block); background: var(--c-block-bg); border-color: var(--c-block-border); }
6530
+ .risk-icon { font-size: 0.75rem; }
6531
+
6532
+ /* \u2500\u2500 Stat chips \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6533
+ .stats {
6629
6534
  display: flex;
6630
6535
  flex-wrap: wrap;
6631
- gap: 0.65rem;
6632
- margin-bottom: 0.5rem;
6536
+ gap: 0.5rem;
6537
+ margin-top: 1rem;
6538
+ align-items: center;
6633
6539
  }
6634
- .metric {
6540
+ .stat {
6541
+ display: flex;
6542
+ align-items: center;
6543
+ gap: 0.35rem;
6544
+ padding: 0.3rem 0.65rem;
6545
+ background: var(--surface);
6546
+ border: 1px solid var(--border);
6547
+ border-radius: var(--radius-sm);
6548
+ font-size: 0.78rem;
6549
+ box-shadow: var(--shadow-sm);
6550
+ }
6551
+ .stat-label { color: var(--text-secondary); }
6552
+ .stat-value { font-weight: 600; color: var(--text); }
6553
+ .stat-value.danger { color: var(--c-block); }
6554
+ .stat-value.caution { color: var(--c-warn); }
6555
+ .stat-value.ok { color: var(--c-ok); }
6556
+
6557
+ /* \u2500\u2500 Meta table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6558
+ .meta-box {
6635
6559
  background: var(--surface);
6636
6560
  border: 1px solid var(--border);
6637
6561
  border-radius: var(--radius);
6638
- padding: 0.5rem 0.9rem;
6639
- box-shadow: var(--shadow);
6562
+ box-shadow: var(--shadow-sm);
6563
+ overflow: hidden;
6564
+ margin-bottom: 2rem;
6565
+ }
6566
+ .meta-row {
6640
6567
  display: flex;
6641
- align-items: center;
6642
- gap: 0.5rem;
6568
+ align-items: baseline;
6569
+ gap: 1rem;
6570
+ padding: 0.6rem 1rem;
6571
+ border-bottom: 1px solid var(--border-light);
6572
+ font-size: 0.875rem;
6643
6573
  }
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;
6574
+ .meta-row:last-child { border-bottom: none; }
6575
+ .meta-label {
6576
+ flex-shrink: 0;
6577
+ width: 7rem;
6578
+ color: var(--text-secondary);
6579
+ font-size: 0.78rem;
6580
+ font-weight: 500;
6656
6581
  }
6657
- .snapshot {
6582
+ .meta-value { color: var(--text); }
6583
+
6584
+ /* \u2500\u2500 Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6585
+ .section { margin-top: 2.5rem; }
6586
+ .section-title {
6587
+ font-size: 0.8rem;
6588
+ font-weight: 700;
6589
+ letter-spacing: 0.06em;
6590
+ text-transform: uppercase;
6591
+ color: var(--text-secondary);
6592
+ margin-bottom: 0.75rem;
6593
+ }
6594
+
6595
+ /* \u2500\u2500 Checks table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6596
+ .checks-table {
6658
6597
  width: 100%;
6659
6598
  border-collapse: collapse;
6660
- font-size: 0.9rem;
6661
6599
  background: var(--surface);
6600
+ border: 1px solid var(--border);
6662
6601
  border-radius: var(--radius);
6663
6602
  overflow: hidden;
6664
- border: 1px solid var(--border);
6665
- box-shadow: var(--shadow);
6603
+ box-shadow: var(--shadow-sm);
6604
+ font-size: 0.855rem;
6666
6605
  }
6667
- .snapshot th, .snapshot td {
6668
- padding: 0.65rem 1rem;
6606
+ .checks-table thead th {
6607
+ padding: 0.5rem 0.75rem;
6669
6608
  text-align: left;
6609
+ font-size: 0.7rem;
6610
+ font-weight: 600;
6611
+ letter-spacing: 0.06em;
6612
+ text-transform: uppercase;
6613
+ color: var(--muted);
6614
+ background: var(--surface-alt);
6670
6615
  border-bottom: 1px solid var(--border);
6671
6616
  }
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;
6617
+ .checks-table tbody td {
6618
+ padding: 0.55rem 0.75rem;
6619
+ border-bottom: 1px solid var(--border-light);
6620
+ vertical-align: middle;
6621
+ }
6622
+ .checks-table tbody tr:last-child td { border-bottom: none; }
6623
+ .checks-table tbody tr:hover { background: #fafbfc; }
6624
+ .col-icon { width: 2.25rem; }
6625
+ .col-count, .col-time { color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
6626
+ .col-time { width: 5.5rem; }
6627
+ .col-count { width: 2.5rem; }
6628
+
6629
+ /* Status icons */
6630
+ .status-icon {
6631
+ display: inline-flex;
6632
+ align-items: center;
6633
+ justify-content: center;
6634
+ width: 1.3rem;
6635
+ height: 1.3rem;
6636
+ border-radius: 50%;
6637
+ font-size: 0.65rem;
6638
+ font-weight: 800;
6639
+ line-height: 1;
6640
+ }
6641
+ .status-icon.pass { background: var(--c-ok-bg); color: var(--c-ok); border: 1.5px solid #bbf7d0; }
6642
+ .status-icon.block { background: var(--c-block-bg); color: var(--c-block); border: 1.5px solid var(--c-block-border); }
6643
+ .status-icon.warn { background: var(--c-warn-bg); color: var(--c-warn); border: 1.5px solid var(--c-warn-border); }
6644
+ .status-icon.skip { background: var(--surface-alt); color: var(--muted); border: 1.5px solid var(--border); font-size: 0.9rem; }
6645
+
6646
+ /* Check name + tooltip */
6647
+ .check-name { font-weight: 600; color: var(--text); cursor: default; }
6648
+ .check-tooltip {
6649
+ display: block;
6650
+ font-size: 0.75rem;
6651
+ color: var(--text-secondary);
6652
+ margin-top: 0.1rem;
6653
+ font-weight: 400;
6654
+ line-height: 1.4;
6678
6655
  }
6679
- ${CHECKS_TABLE_STYLES}
6680
- .panel {
6656
+ .status-text { font-weight: 500; }
6657
+ .status-text.ok { color: var(--c-ok); }
6658
+ .status-text.danger { color: var(--c-block); }
6659
+ .status-text.caution { color: var(--c-warn); }
6660
+ .status-text.muted { color: var(--muted); font-weight: 400; font-size: 0.8rem; }
6661
+
6662
+ /* \u2500\u2500 Findings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6663
+ .accordion {
6681
6664
  background: var(--surface);
6682
6665
  border: 1px solid var(--border);
6683
6666
  border-radius: var(--radius);
6667
+ box-shadow: var(--shadow-sm);
6684
6668
  margin-bottom: 0.65rem;
6685
- box-shadow: var(--shadow);
6669
+ overflow: hidden;
6686
6670
  }
6687
- .panel summary {
6688
- cursor: pointer;
6689
- padding: 0.85rem 1rem;
6690
- list-style: none;
6671
+ .accordion-header {
6691
6672
  display: flex;
6692
6673
  align-items: center;
6693
6674
  justify-content: space-between;
6675
+ padding: 0.8rem 1rem;
6676
+ cursor: pointer;
6677
+ user-select: none;
6678
+ list-style: none;
6694
6679
  font-weight: 600;
6695
6680
  font-size: 0.9rem;
6696
6681
  }
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;
6682
+ .accordion-header::-webkit-details-marker { display: none; }
6683
+ details[open] > .accordion-header { border-bottom: 1px solid var(--border-light); }
6684
+ .accordion-label { display: flex; align-items: center; gap: 0.6rem; }
6685
+ .acc-count {
6686
+ font-size: 0.72rem;
6687
+ font-weight: 600;
6688
+ padding: 0.15rem 0.55rem;
6707
6689
  border-radius: 999px;
6690
+ background: var(--surface-alt);
6691
+ color: var(--text-secondary);
6692
+ border: 1px solid var(--border);
6693
+ }
6694
+ .acc-count.danger { background: var(--c-block-bg); color: var(--c-block); border-color: var(--c-block-border); }
6695
+ .acc-count.caution { background: var(--c-warn-bg); color: var(--c-warn); border-color: var(--c-warn-border); }
6696
+ .accordion-body { padding: 0.85rem; display: flex; flex-direction: column; gap: 0.6rem; }
6697
+
6698
+ /* Warning groups */
6699
+ .group { margin-bottom: 0.25rem; }
6700
+ .group-label {
6701
+ display: flex;
6702
+ align-items: center;
6703
+ gap: 0.5rem;
6704
+ padding: 0.4rem 0;
6705
+ margin-bottom: 0.45rem;
6708
6706
  }
6707
+ .group-name { font-size: 0.78rem; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }
6708
+ .group-count { font-size: 0.72rem; color: var(--muted); background: var(--surface-alt); padding: 0.1rem 0.45rem; border-radius: 999px; border: 1px solid var(--border); }
6709
+
6710
+ /* \u2500\u2500 Finding cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6709
6711
  .card {
6710
6712
  border: 1px solid var(--border);
6711
- border-radius: 8px;
6712
- padding: 1rem;
6713
- margin-bottom: 0.65rem;
6714
- background: #fafafa;
6713
+ border-radius: var(--radius-sm);
6714
+ background: var(--surface);
6715
+ padding: 0.85rem 1rem;
6715
6716
  }
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;
6717
+ .card.sev-block { border-left: 3px solid var(--c-block); background: var(--c-block-bg); border-top-color: var(--c-block-border); border-right-color: var(--c-block-border); border-bottom-color: var(--c-block-border); }
6718
+ .card.sev-warn { border-left: 3px solid var(--c-warn); }
6719
+ .card.sev-info { border-left: 3px solid var(--c-info); }
6720
+ .card-header { display: flex; align-items: flex-start; gap: 0.6rem; margin-bottom: 0.55rem; }
6721
+ .sev-badge {
6722
+ flex-shrink: 0;
6723
+ font-size: 0.65rem;
6724
+ font-weight: 700;
6725
+ letter-spacing: 0.05em;
6726
+ text-transform: uppercase;
6727
+ padding: 0.15rem 0.45rem;
6728
+ border-radius: 4px;
6729
+ margin-top: 0.15rem;
6730
+ }
6731
+ .sev-badge.sev-block { background: var(--c-block-bg); color: var(--c-block); border: 1px solid var(--c-block-border); }
6732
+ .sev-badge.sev-warn { background: var(--c-warn-bg); color: var(--c-warn); border: 1px solid var(--c-warn-border); }
6733
+ .sev-badge.sev-info { background: var(--c-info-bg); color: var(--c-info); border: 1px solid var(--c-info-border); }
6734
+ .card-message { font-size: 0.88rem; line-height: 1.5; color: var(--text); }
6735
+ .card-meta { display: flex; flex-wrap: wrap; gap: 0.35rem; }
6736
+ .tag {
6737
+ font-size: 0.72rem;
6738
+ padding: 0.1rem 0.45rem;
6739
+ border-radius: 4px;
6740
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
6741
+ white-space: nowrap;
6742
+ overflow: hidden;
6743
+ text-overflow: ellipsis;
6744
+ max-width: 40ch;
6745
+ }
6746
+ .tag-check { background: #f0f0ff; color: #4338ca; border: 1px solid #e0e7ff; }
6747
+ .tag-rule { background: var(--surface-alt); color: var(--text-secondary); border: 1px solid var(--border); }
6748
+ .tag-file { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; max-width: 55ch; }
6749
+ .card-hint { font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.5rem; line-height: 1.45; }
6750
+
6751
+ /* Code blocks */
6752
+ .code-block {
6753
+ margin-top: 0.6rem;
6754
+ padding: 0.65rem 0.85rem;
6755
+ background: #f8fafc;
6735
6756
  border: 1px solid var(--border);
6757
+ border-radius: var(--radius-sm);
6758
+ overflow-x: auto;
6759
+ font-size: 0.75rem;
6760
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
6761
+ line-height: 1.55;
6762
+ white-space: pre;
6763
+ color: var(--text);
6764
+ }
6765
+
6766
+ /* Fix box */
6767
+ .fix-box {
6768
+ margin-top: 0.75rem;
6769
+ padding: 0.7rem 0.85rem;
6770
+ background: #fafafa;
6771
+ border: 1px dashed var(--border);
6772
+ border-radius: var(--radius-sm);
6736
6773
  }
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 {
6774
+ .fix-header {
6775
+ font-size: 0.7rem;
6776
+ font-weight: 700;
6777
+ text-transform: uppercase;
6778
+ letter-spacing: 0.06em;
6779
+ color: var(--accent);
6780
+ margin-bottom: 0.4rem;
6781
+ display: flex;
6782
+ align-items: center;
6783
+ gap: 0.4rem;
6784
+ }
6785
+ .badge-llm {
6786
+ font-size: 0.6rem;
6787
+ padding: 0.1rem 0.35rem;
6788
+ background: #eef2ff;
6789
+ color: var(--accent);
6790
+ border-radius: 4px;
6791
+ border: 1px solid #e0e7ff;
6792
+ }
6793
+ .fix-summary { font-size: 0.82rem; color: var(--text); white-space: pre-wrap; }
6794
+ .fix-disclaimer { font-size: 0.7rem; color: var(--muted); margin-top: 0.5rem; }
6795
+
6796
+ /* Misc */
6797
+ .empty-note { font-size: 0.875rem; color: var(--muted); padding: 0.25rem 0; }
6798
+ .muted { color: var(--muted); }
6799
+ .raw-pre {
6744
6800
  white-space: pre-wrap;
6745
- font-size: 0.85rem;
6801
+ font-size: 0.82rem;
6746
6802
  background: var(--surface);
6747
6803
  padding: 1rem;
6748
6804
  border-radius: var(--radius);
6749
6805
  border: 1px solid var(--border);
6750
- margin: 0;
6806
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
6807
+ line-height: 1.55;
6751
6808
  }
6752
- footer {
6753
- margin-top: 3rem;
6809
+
6810
+ /* \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
6811
+ .footer {
6812
+ margin-top: 3.5rem;
6754
6813
  padding-top: 1.25rem;
6755
6814
  border-top: 1px solid var(--border);
6756
- font-size: 0.8rem;
6815
+ font-size: 0.77rem;
6757
6816
  color: var(--muted);
6817
+ display: flex;
6818
+ align-items: center;
6819
+ justify-content: space-between;
6820
+ flex-wrap: wrap;
6821
+ gap: 0.5rem;
6758
6822
  }
6759
- footer a { color: var(--accent); text-decoration: none; }
6760
- footer a:hover { text-decoration: underline; }
6761
6823
  </style>
6762
6824
  </head>
6763
6825
  <body>
6764
- <header class="hero">
6826
+ <div class="wrap">
6827
+
6828
+ <header class="header">
6765
6829
  <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>
6830
+ <h1 class="page-title">Code review report</h1>
6831
+ <span class="risk-pill ${riskClass}"><span class="risk-icon">${riskIcon}</span> ${riskScore} risk</span>
6832
+ <div class="stats">
6833
+ <div class="stat"><span class="stat-label">Blocking</span><span class="stat-value ${blocks > 0 ? "danger" : "ok"}">${blocks}</span></div>
6834
+ <div class="stat"><span class="stat-label">Warnings</span><span class="stat-value ${warns > 0 ? "caution" : "ok"}">${warns}</span></div>
6835
+ <div class="stat"><span class="stat-label">Info</span><span class="stat-value">${infos}</span></div>
6836
+ <div class="stat"><span class="stat-label">Mode</span><span class="stat-value">${escapeHtml(modeLabel)}</span></div>
6773
6837
  </div>
6774
6838
  </header>
6775
6839
 
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>
6840
+ <div class="meta-box">
6841
+ <div class="meta-row"><span class="meta-label">Stack</span><span class="meta-value">${escapeHtml(formatStackOneLiner(stack))}</span></div>
6842
+ ${prRow}
6843
+ <div class="meta-row"><span class="meta-label">Gate mode</span><span class="meta-value">${escapeHtml(modeLabel)}</span></div>
6844
+ </div>
6785
6845
 
6786
6846
  <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>
6847
+ <h2 class="section-title">Checks</h2>
6848
+ <table class="checks-table">
6849
+ <thead>
6850
+ <tr>
6851
+ <th class="col-icon"></th>
6852
+ <th>Check</th>
6853
+ <th>Status</th>
6854
+ <th class="col-count">#</th>
6855
+ <th class="col-time">Time</th>
6856
+ </tr>
6857
+ </thead>
6858
+ <tbody>
6859
+ ${checkRows}
6860
+ </tbody>
6791
6861
  </table>
6792
6862
  </section>
6793
6863
 
6794
6864
  <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>
6865
+ <h2 class="section-title">Findings</h2>
6866
+
6867
+ <details class="accordion" ${blocks > 0 ? "open" : ""}>
6868
+ <summary class="accordion-header">
6869
+ <span class="accordion-label">Blocking <span class="acc-count ${blocks > 0 ? "danger" : ""}">${blocks}</span></span>
6870
+ </summary>
6871
+ <div class="accordion-body">${blockHtml}</div>
6872
+ </details>
6873
+
6874
+ <details class="accordion" ${warns > 0 && blocks === 0 ? "open" : ""}>
6875
+ <summary class="accordion-header">
6876
+ <span class="accordion-label">Warnings <span class="acc-count ${warns > 0 ? "caution" : ""}">${warns}</span></span>
6877
+ </summary>
6878
+ <div class="accordion-body">${warnsHtml}</div>
6879
+ </details>
6880
+
6881
+ <details class="accordion">
6882
+ <summary class="accordion-header">
6883
+ <span class="accordion-label">Info <span class="acc-count">${infos}</span></span>
6884
+ </summary>
6885
+ <div class="accordion-body">${infoHtml}</div>
6886
+ </details>
6799
6887
  </section>
6800
6888
 
6801
- ${appendix}
6889
+ ${appendixHtml}
6802
6890
 
6803
- <footer>
6804
- <p>Static report \u2014 open in any browser. Generated by <strong>FrontGuard</strong>.</p>
6891
+ <footer class="footer">
6892
+ <span>Generated by <strong>FrontGuard</strong> \xB7 ${ts2}</span>
6893
+ <span>Open in any browser \xB7 static report</span>
6805
6894
  </footer>
6895
+
6896
+ </div>
6806
6897
  </body>
6807
6898
  </html>`;
6808
6899
  }
@@ -7001,76 +7092,52 @@ function formatMarkdown(p2) {
7001
7092
  if (lineA !== lineB) return lineA - lineB;
7002
7093
  return a3.f.message.localeCompare(b3.f.message);
7003
7094
  });
7004
- sb.push("## \u2728 FrontGuard review brief");
7095
+ sb.push("## FrontGuard report");
7005
7096
  sb.push("");
7006
7097
  const modeLabel = mode === "enforce" ? "enforce" : "warn only";
7007
7098
  const badgeLine = [
7008
- mdShield("Risk level", "risk", riskScore, riskShieldColor(riskScore)),
7009
- mdShield("CI mode", "mode", modeLabel, modeShieldColor(mode)),
7099
+ mdShield("Risk", "risk", riskScore, riskShieldColor(riskScore)),
7100
+ mdShield("Mode", "mode", modeLabel, modeShieldColor(mode)),
7010
7101
  mdShield("Blocking", "blocking", String(blocks), countShieldColor("block", blocks)),
7011
7102
  mdShield("Warnings", "warnings", String(warns), countShieldColor("warn", warns)),
7012
- mdShield("Info", "info notes", String(infos), countShieldColor("info", infos))
7103
+ mdShield("Info", "info", String(infos), countShieldColor("info", infos))
7013
7104
  ].join(" ");
7014
7105
  sb.push(badgeLine);
7015
7106
  sb.push("");
7016
- sb.push("---");
7017
- sb.push("");
7018
- sb.push("### \u{1F4CC} Snapshot");
7019
- sb.push("");
7020
7107
  sb.push("| | |");
7021
7108
  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)} |`);
7109
+ sb.push(`| Risk | ${riskEmoji(riskScore)} **${riskScore}** |`);
7110
+ sb.push(`| Mode | ${mode === "enforce" ? "\u{1F512} Enforce" : "Warn only"} |`);
7111
+ sb.push(`| Stack | ${formatStackOneLiner(stack)} |`);
7027
7112
  if (pr && lines != null) {
7028
- sb.push(
7029
- `| **PR size** | \u{1F4CF} **${lines}** LOC ( +${pr.additions} / \u2212${pr.deletions} ) \xB7 **${pr.changedFiles}** files |`
7030
- );
7113
+ sb.push(`| PR size | ${lines} lines (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files |`);
7031
7114
  }
7032
7115
  sb.push("");
7033
7116
  if (showAiAssistedBanner && pr?.aiAssisted) {
7034
7117
  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."
7118
+ "> **AI-assisted PR** \u2014 stricter static checks active (security, secrets, `any` deltas may escalate)."
7036
7119
  );
7037
7120
  sb.push("");
7038
7121
  }
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
7122
  sb.push("---");
7053
7123
  sb.push("");
7054
- sb.push("### \u{1F4CB} Check results");
7124
+ sb.push("### Checks");
7055
7125
  sb.push("");
7056
- sb.push("| | Check | Status | Findings | Duration |");
7057
- sb.push("|:--:|:--|:--|:-:|--:|");
7126
+ sb.push("| | Check | Issues | Time |");
7127
+ sb.push("|:--:|:--|:-:|--:|");
7058
7128
  for (const r4 of results) {
7059
7129
  const he2 = healthEmojiForCheck(r4);
7060
7130
  let status;
7061
7131
  if (r4.skipped) {
7062
7132
  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}`;
7133
+ const short = why.length > 80 ? `${why.slice(0, 77)}\u2026` : why;
7134
+ status = `\u2014 _${short}_`;
7065
7135
  } else if (r4.findings.length === 0) {
7066
- status = "\u2705 **Clean**";
7136
+ status = "\u2014";
7067
7137
  } else {
7068
- status = "\u26A0\uFE0F **Issues**";
7138
+ status = String(r4.findings.length);
7069
7139
  }
7070
- const nFind = r4.skipped ? "\u2014" : String(r4.findings.length);
7071
- sb.push(
7072
- `| ${he2} | **${r4.checkId}** | ${status} | **${nFind}** | ${formatDuration2(r4.durationMs)} |`
7073
- );
7140
+ sb.push(`| ${he2} | \`${r4.checkId}\` | ${status} | ${formatDuration2(r4.durationMs)} |`);
7074
7141
  }
7075
7142
  sb.push("");
7076
7143
  const blockFindings = sortWithCwd(
@@ -7078,10 +7145,10 @@ function formatMarkdown(p2) {
7078
7145
  (r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))
7079
7146
  )
7080
7147
  );
7081
- sb.push(`### \u{1F6D1} Blocking \u2014 ${blocks} issue${blocks === 1 ? "" : "s"}`);
7148
+ sb.push(`### Blocking \u2014 ${blocks} issue${blocks === 1 ? "" : "s"}`);
7082
7149
  sb.push("");
7083
7150
  if (blockFindings.length === 0) {
7084
- sb.push("*\u2705 No blocking findings \u2014 nothing mapped to severity `block`.*");
7151
+ sb.push("_No blocking findings._");
7085
7152
  } else {
7086
7153
  for (const { r: r4, f: f4 } of blockFindings) {
7087
7154
  const d3 = normalizeFindingDisplay(cwd, f4);
@@ -7089,11 +7156,11 @@ function formatMarkdown(p2) {
7089
7156
  sb.push("");
7090
7157
  sb.push(`#### ${findingTitleLine(d3.file, d3.message)}`);
7091
7158
  sb.push("");
7092
- sb.push(`| Field | Value |`);
7159
+ sb.push(`| | |`);
7093
7160
  sb.push(`|:--|:--|`);
7094
- sb.push(`| **Check** | \`${r4.checkId}\` |`);
7095
- sb.push(`| **Rule / id** | \`${f4.id}\` |`);
7096
- if (d3.file) sb.push(`| **File** | \`${d3.file}\` |`);
7161
+ sb.push(`| Check | \`${r4.checkId}\` |`);
7162
+ sb.push(`| Rule | \`${f4.id}\` |`);
7163
+ if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
7097
7164
  appendDetailAfterTable(sb, cwd, d3.detail);
7098
7165
  appendSuggestedFix(sb, cwd, f4);
7099
7166
  sb.push("");
@@ -7105,12 +7172,10 @@ function formatMarkdown(p2) {
7105
7172
  (r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))
7106
7173
  )
7107
7174
  );
7108
- sb.push(
7109
- `### \u26A0\uFE0F Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"} (by check)`
7110
- );
7175
+ sb.push(`### Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"}`);
7111
7176
  sb.push("");
7112
7177
  if (warnFindings.length === 0) {
7113
- sb.push("*\u{1F389} No warnings \u2014 nice work.*");
7178
+ sb.push("_No warnings._");
7114
7179
  } else {
7115
7180
  const byCheck = /* @__PURE__ */ new Map();
7116
7181
  for (const item of warnFindings) {
@@ -7121,7 +7186,7 @@ function formatMarkdown(p2) {
7121
7186
  const checkOrder = [...byCheck.keys()].sort((a3, b3) => a3.localeCompare(b3));
7122
7187
  for (const checkId of checkOrder) {
7123
7188
  const group = sortWithCwd(byCheck.get(checkId));
7124
- sb.push(`#### \u{1F4C2} \`${checkId}\` \xB7 ${group.length} finding${group.length === 1 ? "" : "s"}`);
7189
+ sb.push(`#### \`${checkId}\` \xB7 ${group.length} finding${group.length === 1 ? "" : "s"}`);
7125
7190
  sb.push("");
7126
7191
  for (const { r: r4, f: f4 } of group) {
7127
7192
  const d3 = normalizeFindingDisplay(cwd, f4);
@@ -7129,11 +7194,11 @@ function formatMarkdown(p2) {
7129
7194
  sb.push("");
7130
7195
  sb.push(`##### ${findingTitleLine(d3.file, d3.message)}`);
7131
7196
  sb.push("");
7132
- sb.push(`| Field | Value |`);
7197
+ sb.push(`| | |`);
7133
7198
  sb.push(`|:--|:--|`);
7134
- sb.push(`| **Check** | \`${r4.checkId}\` |`);
7135
- sb.push(`| **Rule / id** | \`${f4.id}\` |`);
7136
- if (d3.file) sb.push(`| **File** | \`${d3.file}\` |`);
7199
+ sb.push(`| Check | \`${r4.checkId}\` |`);
7200
+ sb.push(`| Rule | \`${f4.id}\` |`);
7201
+ if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
7137
7202
  appendDetailAfterTable(sb, cwd, d3.detail);
7138
7203
  appendSuggestedFix(sb, cwd, f4);
7139
7204
  sb.push("");
@@ -7146,10 +7211,10 @@ function formatMarkdown(p2) {
7146
7211
  (r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))
7147
7212
  )
7148
7213
  );
7149
- sb.push(`### \u2139\uFE0F Info & notes \u2014 ${infos} item${infos === 1 ? "" : "s"}`);
7214
+ sb.push(`### Info \u2014 ${infos} item${infos === 1 ? "" : "s"}`);
7150
7215
  sb.push("");
7151
7216
  if (infoFindings.length === 0) {
7152
- sb.push("*No info-level notes.*");
7217
+ sb.push("_No info notes._");
7153
7218
  } else {
7154
7219
  for (const { r: r4, f: f4 } of infoFindings) {
7155
7220
  const d3 = normalizeFindingDisplay(cwd, f4);
@@ -7157,11 +7222,11 @@ function formatMarkdown(p2) {
7157
7222
  sb.push("");
7158
7223
  sb.push(`#### ${findingTitleLine(d3.file, d3.message)}`);
7159
7224
  sb.push("");
7160
- sb.push(`| Field | Value |`);
7225
+ sb.push(`| | |`);
7161
7226
  sb.push(`|:--|:--|`);
7162
- sb.push(`| **Check** | \`${r4.checkId}\` |`);
7163
- sb.push(`| **Rule / id** | \`${f4.id}\` |`);
7164
- if (d3.file) sb.push(`| **File** | \`${d3.file}\` |`);
7227
+ sb.push(`| Check | \`${r4.checkId}\` |`);
7228
+ sb.push(`| Rule | \`${f4.id}\` |`);
7229
+ if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
7165
7230
  appendDetailAfterTable(sb, cwd, d3.detail);
7166
7231
  appendSuggestedFix(sb, cwd, f4);
7167
7232
  sb.push("");
@@ -7169,7 +7234,7 @@ function formatMarkdown(p2) {
7169
7234
  }
7170
7235
  sb.push("");
7171
7236
  if (llmAppendix?.trim()) {
7172
- sb.push("### \u{1F916} AI / manual appendix");
7237
+ sb.push("### Appendix");
7173
7238
  sb.push("");
7174
7239
  sb.push(llmAppendix.trim());
7175
7240
  sb.push("");
@@ -7177,40 +7242,45 @@ function formatMarkdown(p2) {
7177
7242
  sb.push("---");
7178
7243
  sb.push("");
7179
7244
  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)._"
7245
+ "_Report by [FrontGuard](https://github.com/flipkart-incubator/frontguard) \xB7 Configure in `frontguard.config.mjs`_"
7190
7246
  );
7191
7247
  return sb.join("\n");
7192
7248
  }
7193
7249
  function formatConsole(p2) {
7194
7250
  const { riskScore, mode, stack, pr, results, warns, infos, blocks } = p2;
7251
+ const W3 = 56;
7252
+ const pad = (s3, n3) => s3.slice(0, n3).padEnd(n3);
7253
+ const row = (label, val) => `\u2502 ${import_picocolors.default.dim(label.padEnd(7))} ${pad(val, W3 - 10)} \u2502`;
7254
+ const riskColored = riskScore === "HIGH" ? import_picocolors.default.red(import_picocolors.default.bold(riskScore)) : riskScore === "MEDIUM" ? import_picocolors.default.yellow(import_picocolors.default.bold(riskScore)) : import_picocolors.default.green(import_picocolors.default.bold(riskScore));
7255
+ const stackStr = [
7256
+ stack.hasNext && "Next.js",
7257
+ stack.hasReactNative && "React Native",
7258
+ !stack.hasNext && !stack.hasReactNative && stack.hasReact && "React",
7259
+ stack.hasTypeScript ? "TypeScript" : "JavaScript",
7260
+ stack.isMonorepo && "monorepo"
7261
+ ].filter(Boolean).join(" \xB7 ");
7195
7262
  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
- );
7263
+ const border = "\u2500".repeat(W3);
7264
+ lines.push(import_picocolors.default.dim(`\u250C${border}\u2510`));
7265
+ lines.push(row("Risk", riskColored));
7266
+ lines.push(row("Mode", mode === "enforce" ? import_picocolors.default.yellow("enforce") : "warn only"));
7267
+ lines.push(row("Stack", stackStr));
7202
7268
  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`);
7269
+ const sz = `${pr.additions + pr.deletions} lines +${pr.additions} / -${pr.deletions} \xB7 ${pr.changedFiles} files`;
7270
+ lines.push(row("PR", sz));
7205
7271
  }
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");
7272
+ lines.push(import_picocolors.default.dim(`\u251C${border}\u2524`));
7273
+ const statusLine = blocks > 0 ? import_picocolors.default.red(`\u2715 ${blocks} blocking \xB7 ${warns} warnings \xB7 ${infos} info`) : warns > 0 ? import_picocolors.default.yellow(`! ${warns} warnings \xB7 ${infos} info`) : import_picocolors.default.green("\u2713 All checks passed");
7274
+ lines.push(`\u2502 ${pad(statusLine, W3 - 2)} \u2502`);
7275
+ lines.push(import_picocolors.default.dim(`\u251C${border}\u2524`));
7209
7276
  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`);
7277
+ const icon = r4.skipped ? import_picocolors.default.dim("\u2013") : r4.findings.length === 0 ? import_picocolors.default.green("\u2713") : r4.findings.some((f4) => f4.severity === "block") ? import_picocolors.default.red("\u2715") : import_picocolors.default.yellow("!");
7278
+ const countStr = r4.skipped ? import_picocolors.default.dim("skipped") : r4.findings.length === 0 ? import_picocolors.default.dim("pass") : r4.findings.length === 1 ? `${r4.findings.length} issue` : `${r4.findings.length} issues`;
7279
+ const name = r4.checkId.padEnd(22);
7280
+ const label = ` ${icon} ${name} ${countStr}`;
7281
+ lines.push(`\u2502${pad(label, W3 + 1)}\u2502`);
7212
7282
  }
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"));
7283
+ lines.push(import_picocolors.default.dim(`\u2514${border}\u2518`));
7214
7284
  return lines.join("\n");
7215
7285
  }
7216
7286