@cleartrip/frontguard 0.2.8 → 0.3.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/index.d.ts CHANGED
@@ -58,8 +58,9 @@ interface FrontGuardConfig {
58
58
  gateWhenAiDisclosureAmbiguous: CheckGate;
59
59
  };
60
60
  /**
61
- * Stricter heuristics on AI-generated code.
62
- * Prefer marking regions with `// @frontguard-ai:start` `// @frontguard-ai:end` (or `// written by AI: start` … `end`).
61
+ * **Under development — disabled by default.** Stricter static heuristics on AI-generated code
62
+ * (decorator regions or PR disclosure). When enabled, also drives escalation in `ai-escalation.ts`.
63
+ * Prefer marking regions with `// @frontguard-ai:start` … `// @frontguard-ai:end`.
63
64
  * See `strictScanMode` for interaction with PR AI disclosure.
64
65
  */
65
66
  aiAssistedReview: {
@@ -121,25 +122,41 @@ interface FrontGuardConfig {
121
122
  maxReportLines: number;
122
123
  };
123
124
  /**
124
- * Static bundle size: optionally run `buildCommand`, then sum bytes under `measureGlobs`.
125
- * For default `npm run build`, the run is skipped (info) when `package.json` has no matching script — use `buildCommand` / `runBuild: false` for non-standard repos.
125
+ * Bundle size: run `buildCommand`, extract the size metric, compare to a baseline from the base branch.
126
+ *
127
+ * **`bundleSizeStrategy`** selects how to read the bundle size:
128
+ * - `'auto'` — pick from detected stack (Next→next, Vite→vite, CRA→cra, else glob).
129
+ * - `'next'` — parse `next build` stdout for "First Load JS shared by all" (kB).
130
+ * - `'vite'` — sum `dist/assets/*.js` (or `measureGlobs`) after `vite build`.
131
+ * - `'cra'` — parse `react-scripts build` stdout for the gzipped JS total.
132
+ * - `'glob'` — sum raw bytes under `measureGlobs` (generic fallback).
133
+ * - `'custom'` — run `bundleSizeCommand`, read a single number (bytes) from stdout.
126
134
  */
127
135
  bundle: {
128
136
  enabled: boolean;
129
137
  gate: CheckGate;
130
- /** When false, only measures existing files (no build). Set false for libs/backends with no web bundle. */
138
+ /** When false, only measures existing files (no build). */
131
139
  runBuild: boolean;
132
- /** E.g. `npm run build`, `pnpm run vite-build`, or any command that produces artifacts under measureGlobs */
140
+ /** E.g. `npm run build`, `yarn build:prod`, or any command that produces output. */
133
141
  buildCommand: string;
142
+ /** How to extract the size number after build. See JSDoc above. */
143
+ bundleSizeStrategy: 'auto' | 'next' | 'vite' | 'cra' | 'glob' | 'custom';
144
+ /**
145
+ * Command that prints a single number (bytes) to stdout. Used only when `bundleSizeStrategy: 'custom'`.
146
+ * E.g. `"node scripts/bundle-size.js"`.
147
+ */
148
+ bundleSizeCommand: string | null;
149
+ /** Globs for `'glob'` and `'vite'` strategies. */
134
150
  measureGlobs: string[];
135
151
  baselinePath: string;
136
152
  /**
137
153
  * Git ref used to read `baselinePath` from the base branch when the file is not on disk.
138
- * Bitbucket PR pipelines set `BITBUCKET_PR_DESTINATION_BRANCH`, which is preferred for resolving the ref.
139
- * Separate from `tsAnyDelta.baseRef` so bundle can track a release branch.
154
+ * Bitbucket PR pipelines set `BITBUCKET_PR_DESTINATION_BRANCH`, which is preferred.
140
155
  */
141
156
  baselineRef: string;
157
+ /** Max allowed growth vs baseline in bytes; null = no limit. */
142
158
  maxDeltaBytes: number | null;
159
+ /** Absolute cap in bytes; null = no limit. */
143
160
  maxTotalBytes: number | null;
144
161
  };
145
162
  /**
@@ -152,6 +169,9 @@ interface FrontGuardConfig {
152
169
  scanGlobs: string[];
153
170
  maxFileBytes: number;
154
171
  };
172
+ /**
173
+ * **Under development — disabled by default.** Optional automated review / fix hints via OpenAI, Anthropic, or Ollama.
174
+ */
155
175
  llm: {
156
176
  enabled: boolean;
157
177
  provider: 'openai' | 'anthropic' | 'ollama';
package/dist/index.js CHANGED
@@ -75,7 +75,7 @@ var defaultConfig = {
75
75
  gateWhenAiDisclosureAmbiguous: "warn"
76
76
  },
77
77
  aiAssistedReview: {
78
- enabled: true,
78
+ enabled: false,
79
79
  gate: "warn",
80
80
  strictScanMode: "both",
81
81
  escalate: {
@@ -107,6 +107,8 @@ var defaultConfig = {
107
107
  gate: "warn",
108
108
  runBuild: true,
109
109
  buildCommand: "npm run build",
110
+ bundleSizeStrategy: "auto",
111
+ bundleSizeCommand: null,
110
112
  measureGlobs: ["dist/**/*", "build/static/**/*", ".next/static/**/*"],
111
113
  baselinePath: ".frontguard/bundle-baseline.json",
112
114
  baselineRef: "main",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../node_modules/defu/dist/defu.mjs","../src/config/defaults.ts","../src/config/migrate.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["function isPlainObject(value) {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) {\n return false;\n }\n if (Symbol.iterator in value) {\n return false;\n }\n if (Symbol.toStringTag in value) {\n return Object.prototype.toString.call(value) === \"[object Module]\";\n }\n return true;\n}\n\nfunction _defu(baseObject, defaults, namespace = \".\", merger) {\n if (!isPlainObject(defaults)) {\n return _defu(baseObject, {}, namespace, merger);\n }\n const object = { ...defaults };\n for (const key of Object.keys(baseObject)) {\n if (key === \"__proto__\" || key === \"constructor\") {\n continue;\n }\n const value = baseObject[key];\n if (value === null || value === void 0) {\n continue;\n }\n if (merger && merger(object, key, value, namespace)) {\n continue;\n }\n if (Array.isArray(value) && Array.isArray(object[key])) {\n object[key] = [...value, ...object[key]];\n } else if (isPlainObject(value) && isPlainObject(object[key])) {\n object[key] = _defu(\n value,\n object[key],\n (namespace ? `${namespace}.` : \"\") + key.toString(),\n merger\n );\n } else {\n object[key] = value;\n }\n }\n return object;\n}\nfunction createDefu(merger) {\n return (...arguments_) => (\n // eslint-disable-next-line unicorn/no-array-reduce\n arguments_.reduce((p, c) => _defu(p, c, \"\", merger), {})\n );\n}\nconst defu = createDefu();\nconst defuFn = createDefu((object, key, currentValue) => {\n if (object[key] !== void 0 && typeof currentValue === \"function\") {\n object[key] = currentValue(object[key]);\n return true;\n }\n});\nconst defuArrayFn = createDefu((object, key, currentValue) => {\n if (Array.isArray(object[key]) && typeof currentValue === \"function\") {\n object[key] = currentValue(object[key]);\n return true;\n }\n});\n\nexport { createDefu, defu as default, defu, defuArrayFn, defuFn };\n","import type { FrontGuardConfig } from './schema.js'\n\nexport const defaultConfig: FrontGuardConfig = {\n mode: 'warn',\n rules: {},\n checks: {\n eslint: { enabled: true, glob: '**/*.{js,cjs,mjs,jsx,ts,tsx}' },\n prettier: {\n enabled: true,\n glob: '**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}',\n },\n typescript: { enabled: true },\n secrets: { enabled: true },\n prHygiene: {\n enabled: true,\n minBodyLength: 80,\n requireSections: false,\n sectionHints: ['what', 'why', 'test', 'how to test', 'screenshot'],\n requireAiDisclosureSection: true,\n gateWhenAiDisclosureAmbiguous: 'warn',\n },\n aiAssistedReview: {\n enabled: true,\n gate: 'warn',\n strictScanMode: 'both',\n escalate: {\n secretFindingsToBlock: true,\n tsAnyDeltaToBlock: true,\n },\n },\n prSize: { enabled: true, warnLines: 400, softBlockLines: 800 },\n\n tsAnyDelta: {\n enabled: true,\n gate: 'warn',\n baseRef: 'main',\n maxAdded: 0,\n },\n cycles: {\n enabled: true,\n gate: 'warn',\n entries: ['src'],\n extraArgs: [],\n },\n deadCode: {\n enabled: true,\n gate: 'info',\n extraArgs: [],\n maxReportLines: 80,\n },\n bundle: {\n enabled: true,\n gate: 'warn',\n runBuild: true,\n buildCommand: 'npm run build',\n measureGlobs: ['dist/**/*', 'build/static/**/*', '.next/static/**/*'],\n baselinePath: '.frontguard/bundle-baseline.json',\n baselineRef: 'main',\n maxDeltaBytes: null,\n maxTotalBytes: null,\n },\n coreWebVitals: {\n enabled: true,\n gate: 'warn',\n scanGlobs: ['app/**/*.{tsx,jsx}', 'pages/**/*.{tsx,jsx}', 'src/**/*.{tsx,jsx}'],\n maxFileBytes: 400_000,\n },\n llm: {\n enabled: false,\n provider: 'openai',\n model: 'gpt-4o-mini',\n apiKeyEnv: 'OPENAI_API_KEY',\n maxDiffChars: 48_000,\n timeoutMs: 60_000,\n ollamaUrl: 'http://127.0.0.1:11434',\n perFindingFixes: false,\n maxFixSuggestions: 12,\n maxFileContextChars: 24_000,\n },\n },\n}\n","import type { FrontGuardConfig } from './schema.js'\n\n/** Normalize legacy config keys before merging with defaults. */\nexport function migrateLegacyConfigKeys(config: Partial<FrontGuardConfig>): void {\n const ch = config.checks as Record<string, unknown> | undefined\n if (!ch) return\n if ('cwv' in ch && !('coreWebVitals' in ch)) {\n ch.coreWebVitals = ch.cwv\n delete ch.cwv\n }\n}\n","import defu from 'defu'\nimport type { FrontGuardConfig } from './config/schema.js'\nimport { defaultConfig } from './config/defaults.js'\nimport { migrateLegacyConfigKeys } from './config/migrate.js'\n\nexport type {\n FrontGuardConfig,\n GuardMode,\n CheckGate,\n RulesMap,\n CustomRuleDefinition,\n RuleFileContext,\n PrSizeTier,\n} from './config/schema.js'\nexport { defaultConfig } from './config/defaults.js'\n\n/**\n * Define repo-level config; shallow-merged over built-in defaults.\n * Org packages can be referenced via `extends` in config file.\n */\nexport function defineConfig(partial: Partial<FrontGuardConfig>): FrontGuardConfig {\n migrateLegacyConfigKeys(partial)\n return defu(partial, defaultConfig) as FrontGuardConfig\n}\n"]}
1
+ {"version":3,"sources":["../node_modules/defu/dist/defu.mjs","../src/config/defaults.ts","../src/config/migrate.ts","../src/index.ts"],"names":[],"mappings":";AAAA,SAAS,cAAc,KAAA,EAAO;AAC5B,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,OAAO,KAAA,KAAU,QAAA,EAAU;AAC/C,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,cAAA,CAAe,KAAK,CAAA;AAC7C,EAAA,IAAI,SAAA,KAAc,QAAQ,SAAA,KAAc,MAAA,CAAO,aAAa,MAAA,CAAO,cAAA,CAAe,SAAS,CAAA,KAAM,IAAA,EAAM;AACrG,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,MAAA,CAAO,YAAY,KAAA,EAAO;AAC5B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,MAAA,CAAO,eAAe,KAAA,EAAO;AAC/B,IAAA,OAAO,MAAA,CAAO,SAAA,CAAU,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA,KAAM,iBAAA;AAAA,EACnD;AACA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,KAAA,CAAM,UAAA,EAAY,QAAA,EAAU,SAAA,GAAY,KAAK,MAAA,EAAQ;AAC5D,EAAA,IAAI,CAAC,aAAA,CAAc,QAAQ,CAAA,EAAG;AAC5B,IAAA,OAAO,KAAA,CAAM,UAAA,EAAY,EAAC,EAAG,WAAW,MAAM,CAAA;AAAA,EAChD;AACA,EAAA,MAAM,MAAA,GAAS,EAAE,GAAG,QAAA,EAAS;AAC7B,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA,EAAG;AACzC,IAAA,IAAI,GAAA,KAAQ,WAAA,IAAe,GAAA,KAAQ,aAAA,EAAe;AAChD,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,WAAW,GAAG,CAAA;AAC5B,IAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAQ;AACtC,MAAA;AAAA,IACF;AACA,IAAA,IAAI,UAAU,MAAA,CAAO,MAAA,EAAQ,GAAA,EAAK,KAAA,EAAO,SAAS,CAAA,EAAG;AACnD,MAAA;AAAA,IACF;AACA,IAAA,IAAI,KAAA,CAAM,QAAQ,KAAK,CAAA,IAAK,MAAM,OAAA,CAAQ,MAAA,CAAO,GAAG,CAAC,CAAA,EAAG;AACtD,MAAA,MAAA,CAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,IACzC,CAAA,MAAA,IAAW,cAAc,KAAK,CAAA,IAAK,cAAc,MAAA,CAAO,GAAG,CAAC,CAAA,EAAG;AAC7D,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,KAAA;AAAA,QACZ,KAAA;AAAA,QACA,OAAO,GAAG,CAAA;AAAA,QAAA,CACT,YAAY,CAAA,EAAG,SAAS,CAAA,CAAA,CAAA,GAAM,EAAA,IAAM,IAAI,QAAA,EAAS;AAAA,QAClD;AAAA,OACF;AAAA,IACF,CAAA,MAAO;AACL,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,KAAA;AAAA,IAChB;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AACA,SAAS,WAAW,MAAA,EAAQ;AAC1B,EAAA,OAAO,CAAA,GAAI,UAAA;AAAA;AAAA,IAET,UAAA,CAAW,MAAA,CAAO,CAAC,CAAA,EAAG,CAAA,KAAM,KAAA,CAAM,CAAA,EAAG,CAAA,EAAG,EAAA,EAAI,MAAM,CAAA,EAAG,EAAE;AAAA,GAAA;AAE3D;AACA,IAAM,OAAO,UAAA,EAAW;;;ACpDjB,IAAM,aAAA,GAAkC;AAAA,EAC7C,IAAA,EAAM,MAAA;AAAA,EACN,OAAO,EAAC;AAAA,EACR,MAAA,EAAQ;AAAA,IACN,MAAA,EAAQ,EAAE,OAAA,EAAS,IAAA,EAAM,MAAM,8BAAA,EAA+B;AAAA,IAC9D,QAAA,EAAU;AAAA,MACR,OAAA,EAAS,IAAA;AAAA,MACT,IAAA,EAAM;AAAA,KACR;AAAA,IACA,UAAA,EAAY,EAAE,OAAA,EAAS,IAAA,EAAK;AAAA,IAC5B,OAAA,EAAS,EAAE,OAAA,EAAS,IAAA,EAAK;AAAA,IACzB,SAAA,EAAW;AAAA,MACT,OAAA,EAAS,IAAA;AAAA,MACT,aAAA,EAAe,EAAA;AAAA,MACf,eAAA,EAAiB,KAAA;AAAA,MACjB,cAAc,CAAC,MAAA,EAAQ,KAAA,EAAO,MAAA,EAAQ,eAAe,YAAY,CAAA;AAAA,MACjE,0BAAA,EAA4B,IAAA;AAAA,MAC5B,6BAAA,EAA+B;AAAA,KACjC;AAAA,IACA,gBAAA,EAAkB;AAAA,MAChB,OAAA,EAAS,KAAA;AAAA,MACT,IAAA,EAAM,MAAA;AAAA,MACN,cAAA,EAAgB,MAAA;AAAA,MAChB,QAAA,EAAU;AAAA,QACR,qBAAA,EAAuB,IAAA;AAAA,QACvB,iBAAA,EAAmB;AAAA;AACrB,KACF;AAAA,IACA,QAAQ,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,GAAA,EAAK,gBAAgB,GAAA,EAAI;AAAA,IAE7D,UAAA,EAAY;AAAA,MACV,OAAA,EAAS,IAAA;AAAA,MACT,IAAA,EAAM,MAAA;AAAA,MACN,OAAA,EAAS,MAAA;AAAA,MACT,QAAA,EAAU;AAAA,KACZ;AAAA,IACA,MAAA,EAAQ;AAAA,MACN,OAAA,EAAS,IAAA;AAAA,MACT,IAAA,EAAM,MAAA;AAAA,MACN,OAAA,EAAS,CAAC,KAAK,CAAA;AAAA,MACf,WAAW;AAAC,KACd;AAAA,IACA,QAAA,EAAU;AAAA,MACR,OAAA,EAAS,IAAA;AAAA,MACT,IAAA,EAAM,MAAA;AAAA,MACN,WAAW,EAAC;AAAA,MACZ,cAAA,EAAgB;AAAA,KAClB;AAAA,IACA,MAAA,EAAQ;AAAA,MACN,OAAA,EAAS,IAAA;AAAA,MACT,IAAA,EAAM,MAAA;AAAA,MACN,QAAA,EAAU,IAAA;AAAA,MACV,YAAA,EAAc,eAAA;AAAA,MACd,kBAAA,EAAoB,MAAA;AAAA,MACpB,iBAAA,EAAmB,IAAA;AAAA,MACnB,YAAA,EAAc,CAAC,WAAA,EAAa,mBAAA,EAAqB,mBAAmB,CAAA;AAAA,MACpE,YAAA,EAAc,kCAAA;AAAA,MACd,WAAA,EAAa,MAAA;AAAA,MACb,aAAA,EAAe,IAAA;AAAA,MACf,aAAA,EAAe;AAAA,KACjB;AAAA,IACA,aAAA,EAAe;AAAA,MACb,OAAA,EAAS,IAAA;AAAA,MACT,IAAA,EAAM,MAAA;AAAA,MACN,SAAA,EAAW,CAAC,oBAAA,EAAsB,sBAAA,EAAwB,oBAAoB,CAAA;AAAA,MAC9E,YAAA,EAAc;AAAA,KAChB;AAAA,IACA,GAAA,EAAK;AAAA,MACH,OAAA,EAAS,KAAA;AAAA,MACT,QAAA,EAAU,QAAA;AAAA,MACV,KAAA,EAAO,aAAA;AAAA,MACP,SAAA,EAAW,gBAAA;AAAA,MACX,YAAA,EAAc,IAAA;AAAA,MACd,SAAA,EAAW,GAAA;AAAA,MACX,SAAA,EAAW,wBAAA;AAAA,MACX,eAAA,EAAiB,KAAA;AAAA,MACjB,iBAAA,EAAmB,EAAA;AAAA,MACnB,mBAAA,EAAqB;AAAA;AACvB;AAEJ;;;AC/EO,SAAS,wBAAwB,MAAA,EAAyC;AAC/E,EAAA,MAAM,KAAK,MAAA,CAAO,MAAA;AAClB,EAAA,IAAI,CAAC,EAAA,EAAI;AACT,EAAA,IAAI,KAAA,IAAS,EAAA,IAAM,EAAE,eAAA,IAAmB,EAAA,CAAA,EAAK;AAC3C,IAAA,EAAA,CAAG,gBAAgB,EAAA,CAAG,GAAA;AACtB,IAAA,OAAO,EAAA,CAAG,GAAA;AAAA,EACZ;AACF;;;ACUO,SAAS,aAAa,OAAA,EAAsD;AACjF,EAAA,uBAAA,CAAwB,OAAO,CAAA;AAC/B,EAAA,OAAO,IAAA,CAAK,SAAS,aAAa,CAAA;AACpC","file":"index.js","sourcesContent":["function isPlainObject(value) {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) {\n return false;\n }\n if (Symbol.iterator in value) {\n return false;\n }\n if (Symbol.toStringTag in value) {\n return Object.prototype.toString.call(value) === \"[object Module]\";\n }\n return true;\n}\n\nfunction _defu(baseObject, defaults, namespace = \".\", merger) {\n if (!isPlainObject(defaults)) {\n return _defu(baseObject, {}, namespace, merger);\n }\n const object = { ...defaults };\n for (const key of Object.keys(baseObject)) {\n if (key === \"__proto__\" || key === \"constructor\") {\n continue;\n }\n const value = baseObject[key];\n if (value === null || value === void 0) {\n continue;\n }\n if (merger && merger(object, key, value, namespace)) {\n continue;\n }\n if (Array.isArray(value) && Array.isArray(object[key])) {\n object[key] = [...value, ...object[key]];\n } else if (isPlainObject(value) && isPlainObject(object[key])) {\n object[key] = _defu(\n value,\n object[key],\n (namespace ? `${namespace}.` : \"\") + key.toString(),\n merger\n );\n } else {\n object[key] = value;\n }\n }\n return object;\n}\nfunction createDefu(merger) {\n return (...arguments_) => (\n // eslint-disable-next-line unicorn/no-array-reduce\n arguments_.reduce((p, c) => _defu(p, c, \"\", merger), {})\n );\n}\nconst defu = createDefu();\nconst defuFn = createDefu((object, key, currentValue) => {\n if (object[key] !== void 0 && typeof currentValue === \"function\") {\n object[key] = currentValue(object[key]);\n return true;\n }\n});\nconst defuArrayFn = createDefu((object, key, currentValue) => {\n if (Array.isArray(object[key]) && typeof currentValue === \"function\") {\n object[key] = currentValue(object[key]);\n return true;\n }\n});\n\nexport { createDefu, defu as default, defu, defuArrayFn, defuFn };\n","import type { FrontGuardConfig } from './schema.js'\n\nexport const defaultConfig: FrontGuardConfig = {\n mode: 'warn',\n rules: {},\n checks: {\n eslint: { enabled: true, glob: '**/*.{js,cjs,mjs,jsx,ts,tsx}' },\n prettier: {\n enabled: true,\n glob: '**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}',\n },\n typescript: { enabled: true },\n secrets: { enabled: true },\n prHygiene: {\n enabled: true,\n minBodyLength: 80,\n requireSections: false,\n sectionHints: ['what', 'why', 'test', 'how to test', 'screenshot'],\n requireAiDisclosureSection: true,\n gateWhenAiDisclosureAmbiguous: 'warn',\n },\n aiAssistedReview: {\n enabled: false,\n gate: 'warn',\n strictScanMode: 'both',\n escalate: {\n secretFindingsToBlock: true,\n tsAnyDeltaToBlock: true,\n },\n },\n prSize: { enabled: true, warnLines: 400, softBlockLines: 800 },\n\n tsAnyDelta: {\n enabled: true,\n gate: 'warn',\n baseRef: 'main',\n maxAdded: 0,\n },\n cycles: {\n enabled: true,\n gate: 'warn',\n entries: ['src'],\n extraArgs: [],\n },\n deadCode: {\n enabled: true,\n gate: 'info',\n extraArgs: [],\n maxReportLines: 80,\n },\n bundle: {\n enabled: true,\n gate: 'warn',\n runBuild: true,\n buildCommand: 'npm run build',\n bundleSizeStrategy: 'auto',\n bundleSizeCommand: null,\n measureGlobs: ['dist/**/*', 'build/static/**/*', '.next/static/**/*'],\n baselinePath: '.frontguard/bundle-baseline.json',\n baselineRef: 'main',\n maxDeltaBytes: null,\n maxTotalBytes: null,\n },\n coreWebVitals: {\n enabled: true,\n gate: 'warn',\n scanGlobs: ['app/**/*.{tsx,jsx}', 'pages/**/*.{tsx,jsx}', 'src/**/*.{tsx,jsx}'],\n maxFileBytes: 400_000,\n },\n llm: {\n enabled: false,\n provider: 'openai',\n model: 'gpt-4o-mini',\n apiKeyEnv: 'OPENAI_API_KEY',\n maxDiffChars: 48_000,\n timeoutMs: 60_000,\n ollamaUrl: 'http://127.0.0.1:11434',\n perFindingFixes: false,\n maxFixSuggestions: 12,\n maxFileContextChars: 24_000,\n },\n },\n}\n","import type { FrontGuardConfig } from './schema.js'\n\n/** Normalize legacy config keys before merging with defaults. */\nexport function migrateLegacyConfigKeys(config: Partial<FrontGuardConfig>): void {\n const ch = config.checks as Record<string, unknown> | undefined\n if (!ch) return\n if ('cwv' in ch && !('coreWebVitals' in ch)) {\n ch.coreWebVitals = ch.cwv\n delete ch.cwv\n }\n}\n","import defu from 'defu'\nimport type { FrontGuardConfig } from './config/schema.js'\nimport { defaultConfig } from './config/defaults.js'\nimport { migrateLegacyConfigKeys } from './config/migrate.js'\n\nexport type {\n FrontGuardConfig,\n GuardMode,\n CheckGate,\n RulesMap,\n CustomRuleDefinition,\n RuleFileContext,\n PrSizeTier,\n} from './config/schema.js'\nexport { defaultConfig } from './config/defaults.js'\n\n/**\n * Define repo-level config; shallow-merged over built-in defaults.\n * Org packages can be referenced via `extends` in config file.\n */\nexport function defineConfig(partial: Partial<FrontGuardConfig>): FrontGuardConfig {\n migrateLegacyConfigKeys(partial)\n return defu(partial, defaultConfig) as FrontGuardConfig\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cleartrip/frontguard",
3
- "version": "0.2.8",
4
- "description": "Org-wide frontend PR guardrails: linting, hygiene, any-delta, cycles, dead code, bundle/CWV hints, custom rules, optional LLM brief",
3
+ "version": "0.3.0",
4
+ "description": "Org-wide frontend PR guardrails for Bitbucket: lint, types, secrets, bundle, PR hygiene, and more (AI-assisted / LLM layers WIP, off by default)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "frontguard": "./dist/cli.js"
@@ -26,7 +26,6 @@
26
26
  "build": "tsup",
27
27
  "dev": "tsup --watch",
28
28
  "typecheck": "tsc --noEmit",
29
- "test": "npm run build && tsx --test src/lib/bitbucket-checks-image-md.test.ts src/runner/checks-snapshot-png.integration.test.ts",
30
29
  "format": "prettier --write .",
31
30
  "prepack": "npm run build"
32
31
  },
@@ -39,7 +38,6 @@
39
38
  ],
40
39
  "license": "MIT",
41
40
  "dependencies": {
42
- "@resvg/resvg-js": "^2.6.2",
43
41
  "fast-glob": "^3.3.3",
44
42
  "typescript": "^5.8.2"
45
43
  },
@@ -50,7 +48,6 @@
50
48
  "picocolors": "^1.1.1",
51
49
  "prettier": "^3.5.3",
52
50
  "tinyexec": "^1.0.1",
53
- "tsup": "^8.4.0",
54
- "tsx": "^4.21.0"
51
+ "tsup": "^8.4.0"
55
52
  }
56
53
  }
@@ -1,16 +1,11 @@
1
- # FrontGuard — PR comment: checks screenshot (inline) + FreeKit full report
1
+ # FrontGuard — reference pipeline (Bitbucket Cloud)
2
2
  #
3
- # PNG matches the HTML snapshot (Overview + Checks). If Bitbucket shows a broken/empty image,
4
- # the host may strip data: URLs use pipeline artifact frontguard-checks.png or HTTPS + FRONTGUARD_CHECKS_IMAGE_URL.
5
- # Raster: @resvg/resvg-js (no browser).
6
- # • The PNG is embedded in the PR comment as a small base64 image (no Bitbucket Downloads / extra tokens).
7
- # • Full HTML report is uploaded to FreeKit; the comment links to that URL for details.
3
+ # This is the FULL pipeline that `frontguard init` generates.
4
+ # In most cases, run `frontguard init` instead of copying this manually
5
+ # it merges the FrontGuard step into your existing bitbucket-pipelines.yml.
8
6
  #
9
- # Secured variable: BITBUCKET_ACCESS_TOKEN must be allowed to post pull request comments.
10
- #
11
- # Optional: FREEKIT_BASE_URL — override FreeKit API host (default https://freekit.dev).
12
- #
13
- # Deeper clone avoids shallow-git errors (e.g. merge-base / parent rev) for diff-based checks.
7
+ # Required secured variable: BITBUCKET_ACCESS_TOKEN (PR comment permission)
8
+ # Optional: FREEKIT_BASE_URL — override the report hosting endpoint
14
9
 
15
10
  image: node:20
16
11
 
@@ -27,30 +22,24 @@ pipelines:
27
22
  artifacts:
28
23
  - frontguard-report.html
29
24
  - frontguard-report.md
30
- - frontguard-checks.html
31
- - frontguard-checks.png
25
+ - frontguard-pr-comment.partial.md
32
26
  script:
33
27
  - corepack enable
34
- - apt-get update && apt-get install -y --no-install-recommends fonts-dejavu-core fonts-liberation
35
28
  - yarn install --immutable || yarn install
36
29
  - |
37
30
  yarn frontguard run --markdown \
38
31
  --htmlOut frontguard-report.html \
39
- --checksSnapshotOut frontguard-checks.html \
40
- --checksPngOut frontguard-checks.png \
32
+ --prCommentOut frontguard-pr-comment.partial.md \
41
33
  > frontguard-report.md
34
+ - test -n "${BITBUCKET_ACCESS_TOKEN:-}" || { echo "Missing secured var BITBUCKET_ACCESS_TOKEN"; exit 1; }
42
35
  - |
43
36
  python3 << 'PY'
44
- import base64
45
37
  import json
46
38
  import os
47
39
  from urllib.error import HTTPError
48
40
  from urllib.request import Request, urlopen
49
41
 
50
- DETAILED = "For detailed check analysis, please open the full interactive report:"
51
- # Keep in sync with src/lib/bitbucket-checks-image-md.ts (markdown line length cap).
52
- MAX_IMAGE_LINE = 300_000
53
- PNG_SIG = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
42
+ PLACEHOLDER = "__FRONTGUARD_REPORT_URL__"
54
43
 
55
44
  base = os.environ.get("FREEKIT_BASE_URL", "https://freekit.dev").rstrip("/")
56
45
  with open("frontguard-report.html", encoding="utf-8") as f:
@@ -78,27 +67,20 @@ pipelines:
78
67
  if not report_url:
79
68
  raise SystemExit(f"FreeKit: missing data.url in {json.dumps(parsed)[:2000]}")
80
69
 
81
- with open("frontguard-checks.png", "rb") as f:
82
- raw = f.read()
83
- if len(raw) < 8 or raw[:8] != PNG_SIG:
84
- raise SystemExit("frontguard-checks.png is missing or not a valid PNG file.")
85
- b64 = base64.standard_b64encode(raw).decode("ascii")
86
- # ASCII alt text; standard Base64 — matches FrontGuard pngBufferToBitbucketImageMarkdownLine.
87
- img_line = f"![FrontGuard checks summary](data:image/png;base64,{b64})"
88
- if len(img_line) > MAX_IMAGE_LINE:
70
+ with open("frontguard-pr-comment.partial.md", encoding="utf-8") as f:
71
+ body = f.read()
72
+ if PLACEHOLDER not in body:
89
73
  raise SystemExit(
90
- f"Checks PNG too large for inline comment ({len(raw)} bytes, line {len(img_line)} chars). "
91
- "Shrink the table or host the PNG and link it; see bitbucket-checks-image-md.ts limits."
74
+ f"Expected {PLACEHOLDER!r} in frontguard-pr-comment.partial.md regenerate with current FrontGuard."
92
75
  )
76
+ body = body.replace(PLACEHOLDER, report_url, 1)
93
77
 
94
- body = f"{img_line}\n\n{DETAILED}\n{report_url}\n"
95
78
  with open("frontguard-pr-comment.md", "w", encoding="utf-8") as out:
96
79
  out.write(body)
97
80
  with open("frontguard-payload.json", "w", encoding="utf-8") as out:
98
81
  json.dump({"content": {"raw": body}}, out, ensure_ascii=False)
99
82
  PY
100
83
  - |
101
- test -n "${BITBUCKET_ACCESS_TOKEN:-}" || { echo "Missing secured var BITBUCKET_ACCESS_TOKEN"; exit 1; }
102
84
  curl --silent --show-error --fail --request POST \
103
85
  --url "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_FULL_NAME}/pullrequests/${BITBUCKET_PR_ID}/comments" \
104
86
  --header 'Accept: application/json' \
@@ -1 +0,0 @@
1
- # Deprecated: use `templates/bitbucket-pipelines.yml` — it now uses `--checksPngOut` + inline PNG.
@@ -1,33 +0,0 @@
1
- # FreeKit.dev + FrontGuard (Bitbucket Pipelines)
2
-
3
- The Bitbucket template (`templates/bitbucket-pipelines.yml`) runs FrontGuard with **`--checksPngOut`** (SVG → PNG via **`@resvg/resvg-js`**, no headless browser), uploads the full HTML to **FreeKit**, and posts a PR comment that **inlines** the checks PNG as `data:image/png;base64,...` plus the FreeKit URL. You only need **`BITBUCKET_ACCESS_TOKEN`** with permission to **comment on pull requests** — no Repository Downloads or `repository:write`.
4
-
5
- The same step uploads `frontguard-report.html` to FreeKit’s public API:
6
-
7
- - **Docs:** [FreeKit API](https://freekit.dev/docs)
8
- - **Endpoint:** `POST https://freekit.dev/api/v1/sites` with JSON `{"html": "<full report html>"}`
9
- - **Response:** `data.url` like `https://freekit.dev/s/<siteId>` — this URL is included in the PR comment below the checks screenshot caption.
10
-
11
- Optional env (advanced): **`FREEKIT_BASE_URL`** — override the API host (e.g. self‑hosted instance per FreeKit docs).
12
-
13
- ## PR checks (`pr-hygiene`, `pr-size`)
14
-
15
- On **Bitbucket Pipelines** pull-request builds, FrontGuard reads **`BITBUCKET_PR_ID`** (and related branch env vars) and computes diff size with **`git diff <destination>...HEAD --numstat`**, so **`pr-size` runs** when the destination ref exists in the clone.
16
-
17
- - **`pr-hygiene`** needs the PR description text. Bitbucket does not expose the body in default env vars — set **`FRONTGUARD_PR_BODY`** in the pipeline (e.g. fetch from the [Bitbucket PR API](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/) in a prior step and `export` it) if you want body-length / section / AI-disclosure rules to apply.
18
- - **Shallow clones:** If `git` cannot resolve `BITBUCKET_PR_DESTINATION_BRANCH`, add a **`git fetch`** of the base branch so `pr-size` can measure the PR (see Bitbucket “clone depth” docs).
19
-
20
- ## Things to know
21
-
22
- - **Public:** Anyone with the link can open the report. Do not use for highly sensitive content unless you add FreeKit **password** protection via their API (requires passing `password` in the JSON — not in this template).
23
- - **Third party:** Uptime, limits, and terms are those of [FreeKit](https://freekit.dev); not controlled by Cleartrip/FrontGuard.
24
- - **Limits:** Per FreeKit docs — e.g. HTML payload size caps, **rate limits** (creates per minute per IP), possible `429`. Large reports or busy runners may need retries or another host.
25
- - **No delete token in CI:** Each run creates a **new** site URL. Old FreeKit URLs may still work until you delete them with the `deleteToken` returned in the API response (this template does not store it).
26
-
27
- ## Checks table image in PR comments
28
-
29
- The default pipeline uses **`--checksPngOut`** so FrontGuard writes **`frontguard-checks.png`** (raster from the checks table). A short Python step base64-embeds that image in the PR comment (no extra hosting). If the PNG is too large for Bitbucket, raise the limit in the pipeline script cautiously or host the image and set **`FRONTGUARD_CHECKS_IMAGE_URL`** with **`--prCommentOut`** instead.
30
-
31
- ## Compared to Surge
32
-
33
- Surge credentials (`SURGE_LOGIN` / `SURGE_TOKEN`) are **not** required for FreeKit’s open POST flow. If you remove Surge, you can delete any old Surge-related secured variables from the repository.