@cleartrip/frontguard 0.2.1 → 0.2.2

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
@@ -2425,6 +2425,8 @@ export default defineConfig({
2425
2425
  // { minLines: 500, severity: 'info', message: 'Consider splitting (\${lines} lines)' },
2426
2426
  // ],
2427
2427
  // },
2428
+ // // AI strict: only // @frontguard-ai:start \u2026 :end (or // written by AI: start \u2026 :end) in PR files
2429
+ // // aiAssistedReview: { strictScanMode: 'decorator' },
2428
2430
  // cycles: { enabled: true },
2429
2431
  // deadCode: { enabled: true, gate: 'info' },
2430
2432
  // // LLM: cloud keys in CI, or local Ollama (no API key) on dev/self-hosted runners:
@@ -2789,6 +2791,7 @@ var defaultConfig = {
2789
2791
  aiAssistedReview: {
2790
2792
  enabled: true,
2791
2793
  gate: "warn",
2794
+ strictScanMode: "both",
2792
2795
  escalate: {
2793
2796
  secretFindingsToBlock: true,
2794
2797
  tsAnyDeltaToBlock: true
@@ -5010,6 +5013,115 @@ async function runCustomRules(cwd, config, restrictToFiles) {
5010
5013
  durationMs: Math.round(performance.now() - t0)
5011
5014
  };
5012
5015
  }
5016
+
5017
+ // src/lib/ai-decorators.ts
5018
+ var FILE_SCAN_HEAD_LINES = 40;
5019
+ function commentInner(line) {
5020
+ const t3 = line.trim();
5021
+ const mLine = /^\/\/\s*(.*)$/.exec(t3);
5022
+ if (mLine) return mLine[1]?.trim() ?? "";
5023
+ const mBlock = /^\/\*\s*(.*?)\s*\*\/\s*$/.exec(t3);
5024
+ if (mBlock) return mBlock[1]?.trim() ?? "";
5025
+ return null;
5026
+ }
5027
+ function lineMarkerKind(line) {
5028
+ const inner = commentInner(line);
5029
+ if (!inner) return null;
5030
+ if (/@(?:frontguard-ai|ai-written)\s*:\s*file\b/i.test(inner) || /^written\s+by\s+ai\s*:?\s*file\b/i.test(inner))
5031
+ return "file";
5032
+ if (/@(?:frontguard-ai|ai-written)\s*:\s*start\b/i.test(inner) || /^written\s+by\s+ai\s*:?\s*start\b/i.test(inner))
5033
+ return "start";
5034
+ if (/@(?:frontguard-ai|ai-written)\s*:\s*end\b/i.test(inner) || /^written\s+by\s+ai\s*:?\s*end\b/i.test(inner))
5035
+ return "end";
5036
+ return null;
5037
+ }
5038
+ function isAiFileDirectiveLine(line) {
5039
+ return lineMarkerKind(line) === "file";
5040
+ }
5041
+ function parseAiMarkedRegions(source, fileHint) {
5042
+ const lines = source.split(/\r?\n/);
5043
+ const parseWarnings = [];
5044
+ const regions = [];
5045
+ let headNonEmpty = 0;
5046
+ for (let i3 = 0; i3 < lines.length && headNonEmpty < FILE_SCAN_HEAD_LINES; i3++) {
5047
+ if (!lines[i3]?.trim()) continue;
5048
+ headNonEmpty++;
5049
+ if (isAiFileDirectiveLine(lines[i3] ?? "")) {
5050
+ const bodyLines = lines.filter((_4, idx) => idx !== i3);
5051
+ regions.push({
5052
+ startLine: 1,
5053
+ endLine: lines.length,
5054
+ text: bodyLines.join("\n")
5055
+ });
5056
+ return { regions, parseWarnings };
5057
+ }
5058
+ }
5059
+ let open = false;
5060
+ let contentStart = null;
5061
+ const buf = [];
5062
+ for (let i3 = 0; i3 < lines.length; i3++) {
5063
+ const line = lines[i3] ?? "";
5064
+ const kind = lineMarkerKind(line);
5065
+ if (kind === "file") {
5066
+ parseWarnings.push(`${fileHint}:${i3 + 1}: @file directive ignored (only honored in first ${FILE_SCAN_HEAD_LINES} non-empty lines)`);
5067
+ if (open) buf.push(line);
5068
+ continue;
5069
+ }
5070
+ if (kind === "start") {
5071
+ if (open) {
5072
+ parseWarnings.push(`${fileHint}:${i3 + 1}: nested AI:start ignored (flatten your regions)`);
5073
+ buf.push(line);
5074
+ continue;
5075
+ }
5076
+ open = true;
5077
+ contentStart = i3 + 2;
5078
+ buf.length = 0;
5079
+ continue;
5080
+ }
5081
+ if (kind === "end") {
5082
+ if (!open) {
5083
+ parseWarnings.push(`${fileHint}:${i3 + 1}: stray AI:end`);
5084
+ continue;
5085
+ }
5086
+ open = false;
5087
+ if (contentStart !== null) {
5088
+ regions.push({
5089
+ startLine: contentStart,
5090
+ endLine: i3,
5091
+ text: buf.join("\n")
5092
+ });
5093
+ }
5094
+ contentStart = null;
5095
+ buf.length = 0;
5096
+ continue;
5097
+ }
5098
+ if (open) buf.push(line);
5099
+ }
5100
+ if (open && contentStart !== null) {
5101
+ parseWarnings.push(`${fileHint}: unclosed AI:start \u2014 treating region as ending at EOF`);
5102
+ regions.push({
5103
+ startLine: contentStart,
5104
+ endLine: lines.length,
5105
+ text: buf.join("\n")
5106
+ });
5107
+ }
5108
+ return { regions, parseWarnings };
5109
+ }
5110
+ function matchLineNumbersInRegion(regionText, regionStartLine, re) {
5111
+ const flags = re.flags.includes("g") ? re.flags : `${re.flags}g`;
5112
+ const g4 = new RegExp(re.source, flags);
5113
+ const out = [];
5114
+ let m3;
5115
+ const text = regionText;
5116
+ while ((m3 = g4.exec(text)) !== null) {
5117
+ const lineInRegion = text.slice(0, m3.index).split("\n").length;
5118
+ out.push(regionStartLine + lineInRegion - 1);
5119
+ if (m3.index === g4.lastIndex) g4.lastIndex++;
5120
+ }
5121
+ return out;
5122
+ }
5123
+
5124
+ // src/checks/ai-assisted-strict.ts
5013
5125
  function sev(gate) {
5014
5126
  return gate === "block" ? "block" : gate === "info" ? "info" : "warn";
5015
5127
  }
@@ -5073,6 +5185,20 @@ var PATTERNS2 = [
5073
5185
  message: "Possible dynamic SQL/string \u2014 ensure parameterization, not string concat."
5074
5186
  }
5075
5187
  ];
5188
+ function scanRegion(rel, regionText, regionStartLine, gate, tag, findings) {
5189
+ for (const { id, re, message, forceBlock } of PATTERNS2) {
5190
+ const lines = matchLineNumbersInRegion(regionText, regionStartLine, re);
5191
+ for (const line of lines) {
5192
+ findings.push({
5193
+ id,
5194
+ severity: forceBlock ? "block" : sev(gate),
5195
+ message: `${tag} ${message}`,
5196
+ file: rel,
5197
+ detail: `line ${line}`
5198
+ });
5199
+ }
5200
+ }
5201
+ }
5076
5202
  async function runAiAssistedStrict(cwd, config, pr) {
5077
5203
  const t0 = performance.now();
5078
5204
  const cfg = config.checks.aiAssistedReview;
@@ -5092,35 +5218,62 @@ async function runAiAssistedStrict(cwd, config, pr) {
5092
5218
  skipped: "no PR context (Bitbucket PR pipeline + BITBUCKET_PR_ID required)"
5093
5219
  };
5094
5220
  }
5095
- if (!pr.aiAssisted) {
5221
+ const mode = cfg.strictScanMode ?? "both";
5222
+ const gate = cfg.gate;
5223
+ const files = (pr.files ?? []).filter((f4) => CODE_EXT.test(f4)).slice(0, 150);
5224
+ const byRel = /* @__PURE__ */ new Map();
5225
+ let anyDecoratorInPr = false;
5226
+ for (const rel of files) {
5227
+ const full = path5.join(cwd, rel);
5228
+ try {
5229
+ const content = await fs.readFile(full, "utf8");
5230
+ if (content.length > 5e5) continue;
5231
+ const parsed = parseAiMarkedRegions(content, rel);
5232
+ byRel.set(rel, { content, parsed });
5233
+ if (parsed.regions.length > 0) anyDecoratorInPr = true;
5234
+ } catch {
5235
+ continue;
5236
+ }
5237
+ }
5238
+ if (mode === "decorator" && !anyDecoratorInPr) {
5096
5239
  return {
5097
5240
  checkId: "ai-assisted-strict",
5098
5241
  findings: [],
5099
5242
  durationMs: Math.round(performance.now() - t0),
5100
- skipped: "PR does not indicate AI-assisted code"
5243
+ skipped: "strictScanMode=decorator \u2014 no `@frontguard-ai:start` / `written by AI: start` regions in PR files"
5101
5244
  };
5102
5245
  }
5103
- const files = (pr.files ?? []).filter((f4) => CODE_EXT.test(f4)).slice(0, 150);
5104
- const gate = cfg.gate;
5246
+ if (mode === "pr-disclosure" && !pr.aiAssisted) {
5247
+ return {
5248
+ checkId: "ai-assisted-strict",
5249
+ findings: [],
5250
+ durationMs: Math.round(performance.now() - t0),
5251
+ skipped: "strictScanMode=pr-disclosure \u2014 PR does not indicate AI-assisted code"
5252
+ };
5253
+ }
5254
+ const useWholeFileFallback = (mode === "pr-disclosure" || mode === "both") && Boolean(pr.aiAssisted);
5105
5255
  const findings = [];
5106
5256
  for (const rel of files) {
5107
- const full = path5.join(cwd, rel);
5108
- let content;
5109
- try {
5110
- content = await fs.readFile(full, "utf8");
5111
- } catch {
5112
- continue;
5113
- }
5114
- if (content.length > 5e5) continue;
5115
- for (const { id, re, message, forceBlock } of PATTERNS2) {
5116
- if (!re.test(content)) continue;
5257
+ const entry = byRel.get(rel);
5258
+ if (!entry) continue;
5259
+ const { content, parsed } = entry;
5260
+ for (const w3 of parsed.parseWarnings) {
5117
5261
  findings.push({
5118
- id,
5119
- severity: forceBlock ? "block" : sev(gate),
5120
- message: `[AI-assisted strict] ${message}`,
5262
+ id: "ai-decorator-parse",
5263
+ severity: "info",
5264
+ message: `[AI markers] ${w3}`,
5121
5265
  file: rel
5122
5266
  });
5123
5267
  }
5268
+ if (parsed.regions.length > 0) {
5269
+ for (const r4 of parsed.regions) {
5270
+ scanRegion(rel, r4.text, r4.startLine, gate, "[AI-marked code]", findings);
5271
+ }
5272
+ continue;
5273
+ }
5274
+ if (useWholeFileFallback) {
5275
+ scanRegion(rel, content, 1, gate, "[AI-assisted strict]", findings);
5276
+ }
5124
5277
  }
5125
5278
  return {
5126
5279
  checkId: "ai-assisted-strict",
@@ -5132,7 +5285,7 @@ function dedupe(f4) {
5132
5285
  const s3 = /* @__PURE__ */ new Set();
5133
5286
  const out = [];
5134
5287
  for (const x3 of f4) {
5135
- const k3 = `${x3.id}:${x3.file ?? ""}`;
5288
+ const k3 = `${x3.id}:${x3.file ?? ""}:${x3.detail ?? ""}`;
5136
5289
  if (s3.has(k3)) continue;
5137
5290
  s3.add(k3);
5138
5291
  out.push(x3);