@cleartrip/frontguard 0.3.5 → 1.0.0

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