@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 +171 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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: "
|
|
5243
|
+
skipped: "strictScanMode=decorator \u2014 no `@frontguard-ai:start` / `written by AI: start` regions in PR files"
|
|
5101
5244
|
};
|
|
5102
5245
|
}
|
|
5103
|
-
|
|
5104
|
-
|
|
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
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
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:
|
|
5120
|
-
message: `[AI
|
|
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);
|