@gulu9527/code-trust 0.3.0 → 0.3.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/index.js CHANGED
@@ -7,33 +7,64 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  // src/parsers/diff.ts
9
9
  import simpleGit from "simple-git";
10
+ var GIT_DIFF_UNIFIED = "--unified=3";
11
+ var SHORT_HASH_LENGTH = 7;
10
12
  var DiffParser = class {
11
13
  git;
12
14
  constructor(workDir) {
13
15
  this.git = simpleGit(workDir);
14
16
  }
15
17
  async getStagedFiles() {
16
- const diffDetail = await this.git.diff(["--cached", "--unified=3"]);
18
+ const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
17
19
  return this.parseDiffOutput(diffDetail);
18
20
  }
19
21
  async getDiffFromRef(ref) {
20
- const diffDetail = await this.git.diff([ref, "--unified=3"]);
22
+ const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
21
23
  return this.parseDiffOutput(diffDetail);
22
24
  }
23
25
  async getChangedFiles() {
24
- const diffDetail = await this.git.diff(["--unified=3"]);
25
- const stagedDetail = await this.git.diff(["--cached", "--unified=3"]);
26
- const allDiff = diffDetail + "\n" + stagedDetail;
27
- return this.parseDiffOutput(allDiff);
26
+ const diffDetail = await this.git.diff([GIT_DIFF_UNIFIED]);
27
+ const stagedDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
28
+ const unstagedFiles = this.parseDiffOutput(diffDetail);
29
+ const stagedFiles = this.parseDiffOutput(stagedDetail);
30
+ return this.mergeDiffFiles(unstagedFiles, stagedFiles);
31
+ }
32
+ /**
33
+ * Merge two sets of diff files, deduplicating by file path.
34
+ * When a file appears in both, merge their hunks and combine stats.
35
+ */
36
+ mergeDiffFiles(unstaged, staged) {
37
+ const fileMap = /* @__PURE__ */ new Map();
38
+ for (const file of unstaged) {
39
+ fileMap.set(file.filePath, file);
40
+ }
41
+ for (const file of staged) {
42
+ const existing = fileMap.get(file.filePath);
43
+ if (existing) {
44
+ fileMap.set(file.filePath, {
45
+ ...existing,
46
+ // Combine additions/deletions
47
+ additions: existing.additions + file.additions,
48
+ deletions: existing.deletions + file.deletions,
49
+ // Merge hunks (preserve order: staged first, then unstaged)
50
+ hunks: [...file.hunks, ...existing.hunks],
51
+ // Status: if either is 'added', treat as added; otherwise keep modified
52
+ status: existing.status === "added" || file.status === "added" ? "added" : "modified"
53
+ });
54
+ } else {
55
+ fileMap.set(file.filePath, file);
56
+ }
57
+ }
58
+ return Array.from(fileMap.values());
28
59
  }
29
60
  async getLastCommitDiff() {
30
- const diffDetail = await this.git.diff(["HEAD~1", "HEAD", "--unified=3"]);
61
+ const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
31
62
  return this.parseDiffOutput(diffDetail);
32
63
  }
33
64
  async getCurrentCommitHash() {
34
65
  try {
35
66
  const hash = await this.git.revparse(["HEAD"]);
36
- return hash.trim().slice(0, 7);
67
+ return hash.trim().slice(0, SHORT_HASH_LENGTH);
37
68
  } catch {
38
69
  return void 0;
39
70
  }
@@ -59,9 +90,18 @@ var DiffParser = class {
59
90
  }
60
91
  parseFileDiff(fileDiff) {
61
92
  const lines = fileDiff.split("\n");
62
- const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
63
- if (!headerMatch) return null;
64
- const filePath = headerMatch[2];
93
+ let filePath = null;
94
+ for (const line of lines) {
95
+ if (line.startsWith("+++ b/")) {
96
+ filePath = line.slice(6);
97
+ break;
98
+ }
99
+ }
100
+ if (!filePath) {
101
+ const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
102
+ if (!headerMatch) return null;
103
+ filePath = headerMatch[2];
104
+ }
65
105
  let status = "modified";
66
106
  let additions = 0;
67
107
  let deletions = 0;
@@ -165,6 +205,144 @@ function t(en, zh) {
165
205
  return isZhLocale() ? zh : en;
166
206
  }
167
207
 
208
+ // src/rules/brace-utils.ts
209
+ function countBracesInLine(line, startJ, initialDepth, inBlockComment) {
210
+ let depth = initialDepth;
211
+ let i = startJ;
212
+ while (i < line.length) {
213
+ if (inBlockComment.value) {
214
+ const closeIdx = line.indexOf("*/", i);
215
+ if (closeIdx === -1) return depth;
216
+ inBlockComment.value = false;
217
+ i = closeIdx + 2;
218
+ continue;
219
+ }
220
+ const ch = line[i];
221
+ if (ch === "/" && line[i + 1] === "/") return depth;
222
+ if (ch === "/" && line[i + 1] === "*") {
223
+ inBlockComment.value = true;
224
+ i += 2;
225
+ continue;
226
+ }
227
+ if (ch === "'" || ch === '"') {
228
+ i = skipStringLiteral(line, i, ch);
229
+ continue;
230
+ }
231
+ if (ch === "`") {
232
+ i = skipTemplateLiteral(line, i);
233
+ continue;
234
+ }
235
+ if (ch === "/" && i > 0) {
236
+ const prevNonSpace = findPrevNonSpace(line, i);
237
+ if (prevNonSpace !== -1 && "=(!|&:,;[{?+->~%^".includes(line[prevNonSpace])) {
238
+ i = skipRegexpLiteral(line, i);
239
+ continue;
240
+ }
241
+ }
242
+ if (ch === "{") depth++;
243
+ if (ch === "}") depth--;
244
+ i++;
245
+ }
246
+ return depth;
247
+ }
248
+ function skipStringLiteral(line, start, quote) {
249
+ let i = start + 1;
250
+ while (i < line.length) {
251
+ if (line[i] === "\\") {
252
+ i += 2;
253
+ continue;
254
+ }
255
+ if (line[i] === quote) return i + 1;
256
+ i++;
257
+ }
258
+ return i;
259
+ }
260
+ function skipTemplateLiteral(line, start) {
261
+ let i = start + 1;
262
+ while (i < line.length) {
263
+ if (line[i] === "\\") {
264
+ i += 2;
265
+ continue;
266
+ }
267
+ if (line[i] === "`") return i + 1;
268
+ i++;
269
+ }
270
+ return i;
271
+ }
272
+ function skipRegexpLiteral(line, start) {
273
+ let i = start + 1;
274
+ while (i < line.length) {
275
+ if (line[i] === "\\") {
276
+ i += 2;
277
+ continue;
278
+ }
279
+ if (line[i] === "/") return i + 1;
280
+ i++;
281
+ }
282
+ return i;
283
+ }
284
+ function findPrevNonSpace(line, pos) {
285
+ for (let i = pos - 1; i >= 0; i--) {
286
+ if (line[i] !== " " && line[i] !== " ") return i;
287
+ }
288
+ return -1;
289
+ }
290
+ function extractBraceBlock(lines, startLineIndex, startCol) {
291
+ let depth = 0;
292
+ let started = false;
293
+ let bodyStart = -1;
294
+ const blockComment = { value: false };
295
+ for (let i = startLineIndex; i < lines.length; i++) {
296
+ const startJ = i === startLineIndex ? startCol : 0;
297
+ const line = lines[i];
298
+ let j = startJ;
299
+ while (j < line.length) {
300
+ if (blockComment.value) {
301
+ const closeIdx = line.indexOf("*/", j);
302
+ if (closeIdx === -1) {
303
+ j = line.length;
304
+ continue;
305
+ }
306
+ blockComment.value = false;
307
+ j = closeIdx + 2;
308
+ continue;
309
+ }
310
+ const ch = line[j];
311
+ if (ch === "/" && line[j + 1] === "/") break;
312
+ if (ch === "/" && line[j + 1] === "*") {
313
+ blockComment.value = true;
314
+ j += 2;
315
+ continue;
316
+ }
317
+ if (ch === "'" || ch === '"') {
318
+ j = skipStringLiteral(line, j, ch);
319
+ continue;
320
+ }
321
+ if (ch === "`") {
322
+ j = skipTemplateLiteral(line, j);
323
+ continue;
324
+ }
325
+ if (ch === "{") {
326
+ depth++;
327
+ if (!started) {
328
+ started = true;
329
+ bodyStart = i;
330
+ }
331
+ } else if (ch === "}") {
332
+ depth--;
333
+ if (started && depth === 0) {
334
+ return {
335
+ bodyLines: lines.slice(bodyStart + 1, i),
336
+ endLine: i
337
+ };
338
+ }
339
+ }
340
+ j++;
341
+ }
342
+ }
343
+ return null;
344
+ }
345
+
168
346
  // src/rules/builtin/unnecessary-try-catch.ts
169
347
  var unnecessaryTryCatchRule = {
170
348
  id: "logic/unnecessary-try-catch",
@@ -180,38 +358,53 @@ var unnecessaryTryCatchRule = {
180
358
  const line = lines[i];
181
359
  const trimmed = line.trim();
182
360
  if (trimmed.startsWith("try") && trimmed.includes("{")) {
183
- const tryBlock = extractBlock(lines, i);
361
+ const tryCol = line.indexOf("try");
362
+ const tryBlock = extractBraceBlock(lines, i, tryCol);
184
363
  if (tryBlock) {
185
- const { bodyLines, catchBodyLines, endLine } = tryBlock;
186
- const nonEmptyBody = bodyLines.filter((l) => l.trim().length > 0);
187
- const nonEmptyCatch = catchBodyLines.filter((l) => l.trim().length > 0);
188
- const isSimpleBody = nonEmptyBody.length <= 2;
189
- const isGenericCatch = nonEmptyCatch.length <= 2 && nonEmptyCatch.some(
190
- (l) => /console\.(log|error|warn)/.test(l) || /throw\s+(new\s+)?Error/.test(l) || l.trim() === ""
191
- );
192
- const bodyHasOnlyAssignments = nonEmptyBody.every(
193
- (l) => /^\s*(const|let|var)\s+/.test(l) || /^\s*\w+(\.\w+)*\s*=\s*/.test(l) || /^\s*return\s+/.test(l)
194
- );
195
- if (isSimpleBody && isGenericCatch && bodyHasOnlyAssignments) {
196
- issues.push({
197
- ruleId: "logic/unnecessary-try-catch",
198
- severity: "medium",
199
- category: "logic",
200
- file: context.filePath,
201
- startLine: i + 1,
202
- endLine: endLine + 1,
203
- message: t(
204
- "Unnecessary try-catch wrapping a simple statement with generic error handling. This is likely AI-hallucinated error handling.",
205
- "\u4E0D\u5FC5\u8981\u7684 try-catch \u5305\u88F9\u4E86\u7B80\u5355\u8BED\u53E5\uFF0Ccatch \u4E2D\u53EA\u6709\u901A\u7528\u7684\u9519\u8BEF\u65E5\u5FD7\u3002\u8FD9\u5F88\u53EF\u80FD\u662F AI \u5E7B\u89C9\u751F\u6210\u7684\u9519\u8BEF\u5904\u7406\u3002"
206
- ),
207
- suggestion: t(
208
- "Remove the try-catch block or add meaningful error recovery logic.",
209
- "\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u6DFB\u52A0\u6709\u610F\u4E49\u7684\u9519\u8BEF\u6062\u590D\u903B\u8F91\u3002"
210
- )
211
- });
364
+ const { bodyLines: tryBodyLines, endLine: tryEndLine } = tryBlock;
365
+ let catchLineIdx = -1;
366
+ for (let k = tryEndLine; k < Math.min(tryEndLine + 2, lines.length); k++) {
367
+ if (lines[k].includes("catch")) {
368
+ catchLineIdx = k;
369
+ break;
370
+ }
371
+ }
372
+ if (catchLineIdx !== -1) {
373
+ const catchCol = lines[catchLineIdx].indexOf("catch");
374
+ const catchBlock = extractBraceBlock(lines, catchLineIdx, catchCol);
375
+ if (catchBlock) {
376
+ const { bodyLines: catchBodyLines, endLine: catchEndLine } = catchBlock;
377
+ const nonEmptyBody = tryBodyLines.filter((l) => l.trim().length > 0);
378
+ const nonEmptyCatch = catchBodyLines.filter((l) => l.trim().length > 0);
379
+ const isSimpleBody = nonEmptyBody.length <= 2;
380
+ const isGenericCatch = nonEmptyCatch.length <= 2 && nonEmptyCatch.some(
381
+ (l) => /console\.(log|error|warn)/.test(l) || /throw\s+(new\s+)?Error/.test(l) || l.trim() === ""
382
+ );
383
+ const bodyHasOnlyAssignments = nonEmptyBody.every(
384
+ (l) => /^\s*(const|let|var)\s+/.test(l) || /^\s*\w+(\.\w+)*\s*=\s*/.test(l) || /^\s*return\s+/.test(l)
385
+ );
386
+ if (isSimpleBody && isGenericCatch && bodyHasOnlyAssignments) {
387
+ issues.push({
388
+ ruleId: "logic/unnecessary-try-catch",
389
+ severity: "medium",
390
+ category: "logic",
391
+ file: context.filePath,
392
+ startLine: i + 1,
393
+ endLine: catchEndLine + 1,
394
+ message: t(
395
+ "Unnecessary try-catch wrapping a simple statement with generic error handling. This is likely AI-hallucinated error handling.",
396
+ "\u4E0D\u5FC5\u8981\u7684 try-catch \u5305\u88F9\u4E86\u7B80\u5355\u8BED\u53E5\uFF0Ccatch \u4E2D\u53EA\u6709\u901A\u7528\u7684\u9519\u8BEF\u65E5\u5FD7\u3002\u8FD9\u5F88\u53EF\u80FD\u662F AI \u5E7B\u89C9\u751F\u6210\u7684\u9519\u8BEF\u5904\u7406\u3002"
397
+ ),
398
+ suggestion: t(
399
+ "Remove the try-catch block or add meaningful error recovery logic.",
400
+ "\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u6DFB\u52A0\u6709\u610F\u4E49\u7684\u9519\u8BEF\u6062\u590D\u903B\u8F91\u3002"
401
+ )
402
+ });
403
+ }
404
+ i = catchEndLine + 1;
405
+ continue;
406
+ }
212
407
  }
213
- i = endLine + 1;
214
- continue;
215
408
  }
216
409
  }
217
410
  i++;
@@ -219,55 +412,6 @@ var unnecessaryTryCatchRule = {
219
412
  return issues;
220
413
  }
221
414
  };
222
- function extractBlock(lines, tryLineIndex) {
223
- let braceCount = 0;
224
- let foundTryOpen = false;
225
- let tryBodyStart = -1;
226
- let tryBodyEnd = -1;
227
- let catchStart = -1;
228
- let catchBodyStart = -1;
229
- let catchBodyEnd = -1;
230
- for (let i = tryLineIndex; i < lines.length; i++) {
231
- const line = lines[i];
232
- for (const ch of line) {
233
- if (ch === "{") {
234
- braceCount++;
235
- if (!foundTryOpen) {
236
- foundTryOpen = true;
237
- tryBodyStart = i;
238
- }
239
- } else if (ch === "}") {
240
- braceCount--;
241
- if (braceCount === 0 && tryBodyEnd === -1) {
242
- tryBodyEnd = i;
243
- } else if (braceCount === 0 && catchBodyEnd === -1 && catchBodyStart !== -1) {
244
- catchBodyEnd = i;
245
- break;
246
- }
247
- }
248
- }
249
- if (tryBodyEnd !== -1 && catchStart === -1) {
250
- if (line.includes("catch")) {
251
- catchStart = i;
252
- }
253
- }
254
- if (catchStart !== -1 && catchBodyStart === -1 && line.includes("{")) {
255
- catchBodyStart = i;
256
- }
257
- if (catchBodyEnd !== -1) break;
258
- }
259
- if (tryBodyStart === -1 || tryBodyEnd === -1 || catchBodyStart === -1 || catchBodyEnd === -1) {
260
- return null;
261
- }
262
- const bodyLines = lines.slice(tryBodyStart + 1, tryBodyEnd);
263
- const catchBodyLines = lines.slice(catchBodyStart + 1, catchBodyEnd);
264
- return {
265
- bodyLines,
266
- catchStart,
267
- catchBodyLines,
268
- endLine: catchBodyEnd
269
- };
270
- }
271
415
 
272
416
  // src/rules/builtin/over-defensive.ts
273
417
  var overDefensiveRule = {
@@ -456,18 +600,10 @@ function detectCodeAfterReturn(context, lines, issues) {
456
600
  let braceDepth = 0;
457
601
  let lastReturnDepth = -1;
458
602
  let lastReturnLine = -1;
603
+ const blockComment = { value: false };
459
604
  for (let i = 0; i < lines.length; i++) {
460
605
  const trimmed = lines[i].trim();
461
- for (const ch of trimmed) {
462
- if (ch === "{") braceDepth++;
463
- if (ch === "}") {
464
- if (braceDepth === lastReturnDepth) {
465
- lastReturnDepth = -1;
466
- lastReturnLine = -1;
467
- }
468
- braceDepth--;
469
- }
470
- }
606
+ braceDepth = countBracesInLine(lines[i], 0, braceDepth, blockComment);
471
607
  if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
472
608
  const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
473
609
  if (endsOpen) continue;
@@ -493,6 +629,10 @@ function detectCodeAfterReturn(context, lines, issues) {
493
629
  lastReturnDepth = -1;
494
630
  lastReturnLine = -1;
495
631
  }
632
+ if (braceDepth < lastReturnDepth) {
633
+ lastReturnDepth = -1;
634
+ lastReturnLine = -1;
635
+ }
496
636
  }
497
637
  }
498
638
  function detectImmediateReassign(context, lines, issues) {
@@ -526,7 +666,21 @@ function detectImmediateReassign(context, lines, issues) {
526
666
  }
527
667
 
528
668
  // src/rules/fix-utils.ts
529
- function lineStartOffset(content, lineNumber) {
669
+ function buildLineOffsets(content) {
670
+ const offsets = [0];
671
+ for (let i = 0; i < content.length; i++) {
672
+ if (content[i] === "\n") {
673
+ offsets.push(i + 1);
674
+ }
675
+ }
676
+ return offsets;
677
+ }
678
+ function lineStartOffset(content, lineNumber, offsets) {
679
+ if (offsets) {
680
+ const idx = lineNumber - 1;
681
+ if (idx < 0 || idx >= offsets.length) return content.length;
682
+ return offsets[idx];
683
+ }
530
684
  let offset = 0;
531
685
  const lines = content.split("\n");
532
686
  for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
@@ -534,12 +688,13 @@ function lineStartOffset(content, lineNumber) {
534
688
  }
535
689
  return offset;
536
690
  }
537
- function lineRange(content, lineNumber) {
538
- const lines = content.split("\n");
691
+ function lineRange(content, lineNumber, offsets) {
692
+ const table = offsets ?? buildLineOffsets(content);
539
693
  const lineIndex = lineNumber - 1;
540
- if (lineIndex < 0 || lineIndex >= lines.length) return [0, 0];
541
- const start = lineStartOffset(content, lineNumber);
542
- const end = start + lines[lineIndex].length + (lineIndex < lines.length - 1 ? 1 : 0);
694
+ if (lineIndex < 0 || lineIndex >= table.length) return [0, 0];
695
+ const start = table[lineIndex];
696
+ const nextLineStart = lineIndex + 1 < table.length ? table[lineIndex + 1] : content.length;
697
+ const end = lineIndex + 1 < table.length ? nextLineStart : nextLineStart;
543
698
  return [start, end];
544
699
  }
545
700
 
@@ -605,12 +760,12 @@ function extractFunctions(parsed) {
605
760
  }
606
761
  function visitNode(root, functions) {
607
762
  const methodBodies = /* @__PURE__ */ new WeakSet();
608
- walkAST(root, (node) => {
763
+ walkAST(root, (node, parent) => {
609
764
  if (node.type === AST_NODE_TYPES.FunctionExpression && methodBodies.has(node)) {
610
765
  return false;
611
766
  }
612
767
  if (node.type === AST_NODE_TYPES.FunctionDeclaration || node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.MethodDefinition) {
613
- const info = analyzeFunctionNode(node);
768
+ const info = analyzeFunctionNode(node, parent);
614
769
  if (info) functions.push(info);
615
770
  if (node.type === AST_NODE_TYPES.MethodDefinition) {
616
771
  methodBodies.add(node.value);
@@ -619,7 +774,7 @@ function visitNode(root, functions) {
619
774
  return;
620
775
  });
621
776
  }
622
- function analyzeFunctionNode(node) {
777
+ function analyzeFunctionNode(node, parent) {
623
778
  let name = "<anonymous>";
624
779
  let params = [];
625
780
  let body = null;
@@ -632,7 +787,11 @@ function analyzeFunctionNode(node) {
632
787
  params = node.params;
633
788
  body = node.body;
634
789
  } else if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
635
- name = "<arrow>";
790
+ if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier) {
791
+ name = parent.id.name;
792
+ } else {
793
+ name = "<arrow>";
794
+ }
636
795
  params = node.params;
637
796
  body = node.body;
638
797
  } else if (node.type === AST_NODE_TYPES.MethodDefinition) {
@@ -660,6 +819,9 @@ function analyzeFunctionNode(node) {
660
819
  function calculateCyclomaticComplexity(root) {
661
820
  let complexity = 1;
662
821
  walkAST(root, (n) => {
822
+ if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
823
+ return false;
824
+ }
663
825
  switch (n.type) {
664
826
  case AST_NODE_TYPES.IfStatement:
665
827
  case AST_NODE_TYPES.ConditionalExpression:
@@ -688,6 +850,9 @@ function calculateCognitiveComplexity(root) {
688
850
  const depthMap = /* @__PURE__ */ new WeakMap();
689
851
  depthMap.set(root, 0);
690
852
  walkAST(root, (n, parent) => {
853
+ if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
854
+ return false;
855
+ }
691
856
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
692
857
  const isNesting = isNestingNode(n);
693
858
  const depth = isNesting ? parentDepth + 1 : parentDepth;
@@ -706,6 +871,9 @@ function calculateMaxNestingDepth(root) {
706
871
  const depthMap = /* @__PURE__ */ new WeakMap();
707
872
  depthMap.set(root, 0);
708
873
  walkAST(root, (n, parent) => {
874
+ if (n.type === AST_NODE_TYPES.FunctionDeclaration || n.type === AST_NODE_TYPES.FunctionExpression || n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.MethodDefinition) {
875
+ return false;
876
+ }
709
877
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
710
878
  const isNesting = n.type === AST_NODE_TYPES.IfStatement || n.type === AST_NODE_TYPES.ForStatement || n.type === AST_NODE_TYPES.ForInStatement || n.type === AST_NODE_TYPES.ForOfStatement || n.type === AST_NODE_TYPES.WhileStatement || n.type === AST_NODE_TYPES.DoWhileStatement || n.type === AST_NODE_TYPES.SwitchStatement || n.type === AST_NODE_TYPES.TryStatement;
711
879
  const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
@@ -813,7 +981,24 @@ function collectDeclarationsAndReferences(root, declarations, references) {
813
981
  kind: exportedNames.has(node.id.name) ? "export" : "local"
814
982
  });
815
983
  }
816
- if (node.type === AST_NODE_TYPES.Identifier && parentType !== "VariableDeclarator" && parentType !== "FunctionDeclaration") {
984
+ if (node.type === AST_NODE_TYPES.ClassDeclaration && node.id) {
985
+ declarations.set(node.id.name, {
986
+ line: node.loc?.start.line ?? 0,
987
+ kind: exportedNames.has(node.id.name) ? "export" : "local"
988
+ });
989
+ }
990
+ const declarationParentTypes = /* @__PURE__ */ new Set([
991
+ "VariableDeclarator",
992
+ "FunctionDeclaration",
993
+ "ClassDeclaration",
994
+ "MethodDefinition",
995
+ "TSEnumDeclaration",
996
+ "TSEnumMember",
997
+ "TSTypeAliasDeclaration",
998
+ "TSInterfaceDeclaration",
999
+ "TSModuleDeclaration"
1000
+ ]);
1001
+ if (node.type === AST_NODE_TYPES.Identifier && !declarationParentTypes.has(parentType ?? "")) {
817
1002
  references.add(node.name);
818
1003
  }
819
1004
  return;
@@ -887,16 +1072,38 @@ function stringifyCondition(node) {
887
1072
  case AST_NODE_TYPES.Literal:
888
1073
  return String(node.value);
889
1074
  case AST_NODE_TYPES.BinaryExpression:
1075
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
890
1076
  case AST_NODE_TYPES.LogicalExpression:
891
1077
  return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
892
1078
  case AST_NODE_TYPES.UnaryExpression:
893
1079
  return `${node.operator}${stringifyCondition(node.argument)}`;
894
1080
  case AST_NODE_TYPES.MemberExpression:
895
1081
  return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
896
- case AST_NODE_TYPES.CallExpression:
897
- return `${stringifyCondition(node.callee)}(...)`;
1082
+ case AST_NODE_TYPES.CallExpression: {
1083
+ const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
1084
+ return `${stringifyCondition(node.callee)}(${args})`;
1085
+ }
1086
+ case AST_NODE_TYPES.ConditionalExpression:
1087
+ return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
1088
+ case AST_NODE_TYPES.TemplateLiteral:
1089
+ return `\`template@${node.loc?.start.line}:${node.loc?.start.column}\``;
1090
+ case AST_NODE_TYPES.ArrayExpression:
1091
+ return `[${node.elements.map((e) => e ? stringifyCondition(e) : "empty").join(", ")}]`;
1092
+ case AST_NODE_TYPES.ObjectExpression:
1093
+ return `{obj@${node.loc?.start.line}:${node.loc?.start.column}}`;
1094
+ case AST_NODE_TYPES.AssignmentExpression:
1095
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
1096
+ case AST_NODE_TYPES.NewExpression:
1097
+ return `new ${stringifyCondition(node.callee)}(${node.arguments.map((a) => stringifyCondition(a)).join(", ")})`;
1098
+ case AST_NODE_TYPES.TSAsExpression:
1099
+ case AST_NODE_TYPES.TSNonNullExpression:
1100
+ return stringifyCondition(node.expression);
1101
+ case AST_NODE_TYPES.AwaitExpression:
1102
+ return `await ${stringifyCondition(node.argument)}`;
1103
+ case AST_NODE_TYPES.ChainExpression:
1104
+ return stringifyCondition(node.expression);
898
1105
  default:
899
- return `[${node.type}]`;
1106
+ return `[${node.type}@${node.loc?.start.line}:${node.loc?.start.column}]`;
900
1107
  }
901
1108
  }
902
1109
  function truncate(s, maxLen) {
@@ -1055,9 +1262,11 @@ var securityRules = [
1055
1262
  const issues = [];
1056
1263
  const lines = context.fileContent.split("\n");
1057
1264
  for (let i = 0; i < lines.length; i++) {
1058
- const trimmed = lines[i].trim();
1265
+ const line = lines[i];
1266
+ const trimmed = line.trim();
1059
1267
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1060
- if (/\.(innerHTML|outerHTML)\s*=/.test(lines[i]) || /dangerouslySetInnerHTML/.test(lines[i])) {
1268
+ const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
1269
+ if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
1061
1270
  issues.push({
1062
1271
  ruleId: "security/dangerous-html",
1063
1272
  severity: "medium",
@@ -1106,7 +1315,8 @@ var emptyCatchRule = {
1106
1315
  const catchMatch = trimmed.match(/\bcatch\s*\(\s*(\w+)?\s*\)\s*\{/);
1107
1316
  if (!catchMatch) continue;
1108
1317
  const catchVarName = catchMatch[1] || "";
1109
- const blockContent = extractCatchBody(lines, i);
1318
+ const catchIdx = lines[i].indexOf("catch");
1319
+ const blockContent = extractBraceBlock(lines, i, catchIdx);
1110
1320
  if (!blockContent) continue;
1111
1321
  const { bodyLines, endLine } = blockContent;
1112
1322
  const meaningful = bodyLines.filter(
@@ -1156,37 +1366,6 @@ var emptyCatchRule = {
1156
1366
  return issues;
1157
1367
  }
1158
1368
  };
1159
- function extractCatchBody(lines, catchLineIndex) {
1160
- const catchLine = lines[catchLineIndex];
1161
- const catchIdx = catchLine.indexOf("catch");
1162
- if (catchIdx === -1) return null;
1163
- let braceCount = 0;
1164
- let started = false;
1165
- let bodyStart = -1;
1166
- for (let i = catchLineIndex; i < lines.length; i++) {
1167
- const line = lines[i];
1168
- const startJ = i === catchLineIndex ? catchIdx : 0;
1169
- for (let j = startJ; j < line.length; j++) {
1170
- const ch = line[j];
1171
- if (ch === "{") {
1172
- braceCount++;
1173
- if (!started) {
1174
- started = true;
1175
- bodyStart = i;
1176
- }
1177
- } else if (ch === "}") {
1178
- braceCount--;
1179
- if (started && braceCount === 0) {
1180
- return {
1181
- bodyLines: lines.slice(bodyStart + 1, i),
1182
- endLine: i
1183
- };
1184
- }
1185
- }
1186
- }
1187
- }
1188
- return null;
1189
- }
1190
1369
 
1191
1370
  // src/rules/builtin/identical-branches.ts
1192
1371
  var identicalBranchesRule = {
@@ -1533,10 +1712,25 @@ var unusedImportRule = {
1533
1712
  }
1534
1713
  return;
1535
1714
  });
1536
- const typeRefPattern = /\b([A-Z][A-Za-z0-9]*)\b/g;
1537
- let match;
1538
- while ((match = typeRefPattern.exec(context.fileContent)) !== null) {
1539
- references.add(match[1]);
1715
+ const codeLines = context.fileContent.split("\n");
1716
+ let inBlock = false;
1717
+ for (const codeLine of codeLines) {
1718
+ const trimmedCode = codeLine.trim();
1719
+ if (inBlock) {
1720
+ if (trimmedCode.includes("*/")) inBlock = false;
1721
+ continue;
1722
+ }
1723
+ if (trimmedCode.startsWith("/*")) {
1724
+ if (!trimmedCode.includes("*/")) inBlock = true;
1725
+ continue;
1726
+ }
1727
+ if (trimmedCode.startsWith("//") || trimmedCode.startsWith("*")) continue;
1728
+ const cleaned = codeLine.replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "").replace(/'(?:[^'\\]|\\.)*'/g, "").replace(/"(?:[^"\\]|\\.)*"/g, "").replace(/`(?:[^`\\]|\\.)*`/g, "");
1729
+ const typeRefPattern = /\b([A-Z][A-Za-z0-9]*)\b/g;
1730
+ let match;
1731
+ while ((match = typeRefPattern.exec(cleaned)) !== null) {
1732
+ references.add(match[1]);
1733
+ }
1540
1734
  }
1541
1735
  for (const imp of imports) {
1542
1736
  if (!references.has(imp.local)) {
@@ -1585,6 +1779,9 @@ var missingAwaitRule = {
1585
1779
  const body = getFunctionBody(node);
1586
1780
  if (!body) return;
1587
1781
  walkAST(body, (inner, parent) => {
1782
+ if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1783
+ return;
1784
+ }
1588
1785
  if (inner !== body && isAsyncFunction(inner)) return false;
1589
1786
  if (inner.type !== AST_NODE_TYPES.CallExpression) return;
1590
1787
  if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
@@ -1594,6 +1791,9 @@ var missingAwaitRule = {
1594
1791
  if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
1595
1792
  if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
1596
1793
  if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
1794
+ if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1795
+ return;
1796
+ }
1597
1797
  const callName = getCallName(inner);
1598
1798
  if (!callName) return;
1599
1799
  if (!asyncFuncNames.has(callName)) return;
@@ -1805,9 +2005,28 @@ var typeCoercionRule = {
1805
2005
  var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
1806
2006
  -1,
1807
2007
  0,
2008
+ 0.1,
2009
+ 0.1,
2010
+ 0.15,
2011
+ 0.2,
2012
+ 0.2,
2013
+ 0.25,
2014
+ 0.3,
2015
+ 0.3,
2016
+ 0.5,
1808
2017
  1,
1809
2018
  2,
2019
+ 3,
2020
+ 4,
2021
+ 5,
1810
2022
  10,
2023
+ 15,
2024
+ 20,
2025
+ 30,
2026
+ 40,
2027
+ 50,
2028
+ 70,
2029
+ 90,
1811
2030
  100
1812
2031
  ]);
1813
2032
  var magicNumberRule = {
@@ -1836,6 +2055,7 @@ var magicNumberRule = {
1836
2055
  if (/^\s*(export\s+)?enum\s/.test(line)) continue;
1837
2056
  if (trimmed.startsWith("import ")) continue;
1838
2057
  if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
2058
+ if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
1839
2059
  const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
1840
2060
  const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
1841
2061
  let match;
@@ -1932,6 +2152,17 @@ var nestedTernaryRule = {
1932
2152
  // src/rules/builtin/duplicate-string.ts
1933
2153
  var MIN_STRING_LENGTH = 6;
1934
2154
  var MIN_OCCURRENCES = 3;
2155
+ var IGNORED_LITERALS = /* @__PURE__ */ new Set([
2156
+ "high",
2157
+ "medium",
2158
+ "low",
2159
+ "info",
2160
+ "logic",
2161
+ "security",
2162
+ "structure",
2163
+ "style",
2164
+ "coverage"
2165
+ ]);
1935
2166
  var duplicateStringRule = {
1936
2167
  id: "logic/duplicate-string",
1937
2168
  category: "logic",
@@ -1962,6 +2193,7 @@ var duplicateStringRule = {
1962
2193
  while ((match = stringRegex.exec(cleaned)) !== null) {
1963
2194
  const value = match[2];
1964
2195
  if (value.length < MIN_STRING_LENGTH) continue;
2196
+ if (IGNORED_LITERALS.has(value)) continue;
1965
2197
  if (value.includes("${")) continue;
1966
2198
  if (value.startsWith("http") || value.startsWith("/")) continue;
1967
2199
  if (value.startsWith("test") || value.startsWith("mock")) continue;
@@ -2231,13 +2463,28 @@ var promiseVoidRule = {
2231
2463
  /^save/,
2232
2464
  /^load/,
2233
2465
  /^send/,
2234
- /^delete/,
2235
2466
  /^update/,
2236
2467
  /^create/,
2237
2468
  /^connect/,
2238
2469
  /^disconnect/,
2239
2470
  /^init/
2240
2471
  ];
2472
+ const syncMethods = [
2473
+ "delete",
2474
+ // Map.delete(), Set.delete(), Object.delete() are synchronous
2475
+ "has",
2476
+ // Map.has(), Set.has() are synchronous
2477
+ "get",
2478
+ // Map.get() is synchronous
2479
+ "set",
2480
+ // Map.set() is synchronous (though some consider it potentially async)
2481
+ "keys",
2482
+ // Object.keys() is synchronous
2483
+ "values",
2484
+ // Object.values() is synchronous
2485
+ "entries"
2486
+ // Object.entries() is synchronous
2487
+ ];
2241
2488
  walkAST(ast, (node) => {
2242
2489
  if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
2243
2490
  const expr = node.expression;
@@ -2248,6 +2495,7 @@ var promiseVoidRule = {
2248
2495
  const isKnownAsync = asyncFnNames.has(fnName);
2249
2496
  const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
2250
2497
  const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
2498
+ if (syncMethods.includes(fnName)) return;
2251
2499
  if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
2252
2500
  const line = node.loc?.start.line ?? 0;
2253
2501
  if (line === 0) return;
@@ -2608,13 +2856,20 @@ var SEVERITY_PENALTY = {
2608
2856
  low: 3,
2609
2857
  info: 0
2610
2858
  };
2859
+ var DIMINISHING_FACTOR = 0.7;
2611
2860
  function calculateDimensionScore(issues) {
2612
2861
  let score = 100;
2862
+ const severityCounts = {};
2613
2863
  for (const issue of issues) {
2614
- score -= SEVERITY_PENALTY[issue.severity] ?? 0;
2864
+ const base = SEVERITY_PENALTY[issue.severity] ?? 0;
2865
+ if (base === 0) continue;
2866
+ const n = severityCounts[issue.severity] ?? 0;
2867
+ severityCounts[issue.severity] = n + 1;
2868
+ const penalty = base * Math.pow(DIMINISHING_FACTOR, n);
2869
+ score -= penalty;
2615
2870
  }
2616
2871
  return {
2617
- score: Math.max(0, Math.min(100, score)),
2872
+ score: Math.round(Math.max(0, Math.min(100, score)) * 10) / 10,
2618
2873
  issues
2619
2874
  };
2620
2875
  }
@@ -2967,7 +3222,7 @@ var PKG_VERSION = (() => {
2967
3222
  }
2968
3223
  })();
2969
3224
  var REPORT_SCHEMA_VERSION = "1.0.0";
2970
- var FINGERPRINT_VERSION = "1";
3225
+ var FINGERPRINT_VERSION = "2";
2971
3226
  var ScanEngine = class {
2972
3227
  config;
2973
3228
  diffParser;
@@ -2999,7 +3254,11 @@ var ScanEngine = class {
2999
3254
  }
3000
3255
  }
3001
3256
  const issuesWithFingerprints = this.attachFingerprints(allIssues);
3002
- const dimensions = this.groupByDimension(issuesWithFingerprints);
3257
+ const baseline = await this.loadBaseline(options.baseline);
3258
+ const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
3259
+ const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
3260
+ const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
3261
+ const dimensions = this.groupByDimension(issuesWithLifecycle);
3003
3262
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
3004
3263
  const grade = getGrade(overallScore);
3005
3264
  const commitHash = await this.diffParser.getCurrentCommitHash();
@@ -3013,7 +3272,7 @@ var ScanEngine = class {
3013
3272
  score: overallScore,
3014
3273
  grade,
3015
3274
  filesScanned,
3016
- issuesFound: issuesWithFingerprints.length
3275
+ issuesFound: issuesWithLifecycle.length
3017
3276
  },
3018
3277
  toolHealth: {
3019
3278
  rulesExecuted,
@@ -3026,70 +3285,75 @@ var ScanEngine = class {
3026
3285
  ruleFailures
3027
3286
  },
3028
3287
  dimensions,
3029
- issues: issuesWithFingerprints.sort((a, b) => {
3288
+ issues: issuesWithLifecycle.sort((a, b) => {
3030
3289
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
3031
3290
  return severityOrder[a.severity] - severityOrder[b.severity];
3032
- })
3291
+ }),
3292
+ lifecycle,
3293
+ fixedIssues
3033
3294
  };
3034
3295
  }
3035
3296
  async scanFile(diffFile) {
3036
3297
  if (diffFile.status === "deleted") {
3037
- return {
3038
- issues: [],
3039
- ruleFailures: [],
3040
- rulesExecuted: 0,
3041
- rulesFailed: 0,
3042
- scanErrors: [
3043
- {
3044
- type: "deleted-file",
3045
- file: diffFile.filePath,
3046
- message: `Skipped deleted file: ${diffFile.filePath}`
3047
- }
3048
- ],
3049
- scanned: false
3050
- };
3298
+ return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
3051
3299
  }
3052
3300
  if (!this.isTsJsFile(diffFile.filePath)) {
3053
- return {
3054
- issues: [],
3055
- ruleFailures: [],
3056
- rulesExecuted: 0,
3057
- rulesFailed: 0,
3058
- scanErrors: [
3059
- {
3060
- type: "unsupported-file-type",
3061
- file: diffFile.filePath,
3062
- message: `Skipped unsupported file type: ${diffFile.filePath}`
3063
- }
3064
- ],
3065
- scanned: false
3066
- };
3301
+ return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
3302
+ }
3303
+ const fileContent = await this.readFileContent(diffFile);
3304
+ if (!fileContent) {
3305
+ return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
3067
3306
  }
3307
+ const addedLines = this.extractAddedLines(diffFile);
3308
+ const ruleResult = this.ruleEngine.runWithDiagnostics({
3309
+ filePath: diffFile.filePath,
3310
+ fileContent,
3311
+ addedLines
3312
+ });
3313
+ const issues = [...ruleResult.issues];
3314
+ issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
3315
+ issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
3316
+ issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
3317
+ return {
3318
+ issues,
3319
+ ruleFailures: ruleResult.ruleFailures,
3320
+ rulesExecuted: ruleResult.rulesExecuted,
3321
+ rulesFailed: ruleResult.rulesFailed,
3322
+ scanErrors: [],
3323
+ scanned: true
3324
+ };
3325
+ }
3326
+ createSkippedResult(diffFile, type, message) {
3327
+ return {
3328
+ issues: [],
3329
+ ruleFailures: [],
3330
+ rulesExecuted: 0,
3331
+ rulesFailed: 0,
3332
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3333
+ scanned: false
3334
+ };
3335
+ }
3336
+ createErrorResult(diffFile, type, message) {
3337
+ return {
3338
+ issues: [],
3339
+ ruleFailures: [],
3340
+ rulesExecuted: 0,
3341
+ rulesFailed: 0,
3342
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3343
+ scanned: false
3344
+ };
3345
+ }
3346
+ async readFileContent(diffFile) {
3068
3347
  const filePath = resolve2(diffFile.filePath);
3069
- let fileContent;
3070
3348
  try {
3071
- fileContent = await readFile(filePath, "utf-8");
3349
+ return await readFile(filePath, "utf-8");
3072
3350
  } catch {
3073
3351
  const content = await this.diffParser.getFileContent(diffFile.filePath);
3074
- if (!content) {
3075
- return {
3076
- issues: [],
3077
- ruleFailures: [],
3078
- rulesExecuted: 0,
3079
- rulesFailed: 0,
3080
- scanErrors: [
3081
- {
3082
- type: "missing-file-content",
3083
- file: diffFile.filePath,
3084
- message: `Unable to read file content for ${diffFile.filePath}`
3085
- }
3086
- ],
3087
- scanned: false
3088
- };
3089
- }
3090
- fileContent = content;
3352
+ return content ?? null;
3091
3353
  }
3092
- const addedLines = diffFile.hunks.flatMap((hunk) => {
3354
+ }
3355
+ extractAddedLines(diffFile) {
3356
+ return diffFile.hunks.flatMap((hunk) => {
3093
3357
  const lines = hunk.content.split("\n");
3094
3358
  const result = [];
3095
3359
  let currentLine = hunk.newStart;
@@ -3104,32 +3368,15 @@ var ScanEngine = class {
3104
3368
  }
3105
3369
  return result;
3106
3370
  });
3107
- const ruleResult = this.ruleEngine.runWithDiagnostics({
3108
- filePath: diffFile.filePath,
3109
- fileContent,
3110
- addedLines
3111
- });
3112
- const issues = [...ruleResult.issues];
3113
- const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
3371
+ }
3372
+ runStructureAnalysis(fileContent, filePath) {
3373
+ return analyzeStructure(fileContent, filePath, {
3114
3374
  maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3115
3375
  maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3116
3376
  maxFunctionLength: this.config.thresholds["max-function-length"],
3117
3377
  maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3118
3378
  maxParamCount: this.config.thresholds["max-params"]
3119
- });
3120
- issues.push(...structureResult.issues);
3121
- const styleResult = analyzeStyle(fileContent, diffFile.filePath);
3122
- issues.push(...styleResult.issues);
3123
- const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
3124
- issues.push(...coverageResult.issues);
3125
- return {
3126
- issues,
3127
- ruleFailures: ruleResult.ruleFailures,
3128
- rulesExecuted: ruleResult.rulesExecuted,
3129
- rulesFailed: ruleResult.rulesFailed,
3130
- scanErrors: [],
3131
- scanned: true
3132
- };
3379
+ }).issues;
3133
3380
  }
3134
3381
  async getScanCandidates(options) {
3135
3382
  const scanMode = this.getScanMode(options);
@@ -3170,7 +3417,7 @@ var ScanEngine = class {
3170
3417
  }
3171
3418
  return "changed";
3172
3419
  }
3173
- async getDiffFiles(options) {
3420
+ getDiffFiles(options) {
3174
3421
  if (options.staged) {
3175
3422
  return this.diffParser.getStagedFiles();
3176
3423
  }
@@ -3227,13 +3474,14 @@ var ScanEngine = class {
3227
3474
  const occurrenceCounts = /* @__PURE__ */ new Map();
3228
3475
  return issues.map((issue) => {
3229
3476
  const normalizedFile = this.normalizeRelativePath(issue.file);
3230
- const locationComponent = `${issue.startLine}:${issue.endLine}`;
3477
+ const contentSource = issue.codeSnippet ? issue.codeSnippet : `${issue.startLine}:${issue.endLine}`;
3478
+ const contentDigest = createHash("sha256").update(contentSource).digest("hex").slice(0, 16);
3231
3479
  const baseKey = [
3232
3480
  issue.ruleId,
3233
3481
  normalizedFile,
3234
3482
  issue.category,
3235
3483
  issue.severity,
3236
- locationComponent
3484
+ contentDigest
3237
3485
  ].join("|");
3238
3486
  const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
3239
3487
  occurrenceCounts.set(baseKey, occurrenceIndex + 1);
@@ -3251,18 +3499,110 @@ var ScanEngine = class {
3251
3499
  const relativePath = relative(process.cwd(), absolutePath) || filePath;
3252
3500
  return relativePath.split(sep).join("/");
3253
3501
  }
3502
+ async loadBaseline(baselinePath) {
3503
+ if (!baselinePath) {
3504
+ return void 0;
3505
+ }
3506
+ const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
3507
+ const parsed = JSON.parse(baselineContent);
3508
+ const issues = this.parseBaselineIssues(parsed.issues);
3509
+ return {
3510
+ issues,
3511
+ fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
3512
+ commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
3513
+ timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
3514
+ };
3515
+ }
3516
+ parseBaselineIssues(input) {
3517
+ if (!Array.isArray(input)) {
3518
+ return [];
3519
+ }
3520
+ return input.flatMap((item) => {
3521
+ const issue = this.parseBaselineIssue(item);
3522
+ return issue ? [issue] : [];
3523
+ });
3524
+ }
3525
+ parseBaselineIssue(input) {
3526
+ if (!input || typeof input !== "object") {
3527
+ return void 0;
3528
+ }
3529
+ const issue = input;
3530
+ if (!this.isValidBaselineIssue(issue)) {
3531
+ return void 0;
3532
+ }
3533
+ return {
3534
+ ruleId: issue.ruleId,
3535
+ severity: issue.severity,
3536
+ category: issue.category,
3537
+ file: issue.file,
3538
+ startLine: issue.startLine,
3539
+ endLine: issue.endLine,
3540
+ message: issue.message,
3541
+ fingerprint: issue.fingerprint,
3542
+ fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
3543
+ };
3544
+ }
3545
+ isValidBaselineIssue(issue) {
3546
+ return typeof issue.ruleId === "string" && this.isSeverity(issue.severity) && this.isRuleCategory(issue.category) && typeof issue.file === "string" && typeof issue.startLine === "number" && typeof issue.endLine === "number" && typeof issue.message === "string" && typeof issue.fingerprint === "string";
3547
+ }
3548
+ attachLifecycle(issues, baseline) {
3549
+ if (!baseline) {
3550
+ return issues;
3551
+ }
3552
+ return issues.map((issue) => ({
3553
+ ...issue,
3554
+ lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
3555
+ }));
3556
+ }
3557
+ getFixedIssues(issues, baseline) {
3558
+ if (!baseline) {
3559
+ return [];
3560
+ }
3561
+ const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
3562
+ return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
3563
+ }
3564
+ buildLifecycleSummary(issues, fixedIssues, baseline) {
3565
+ if (!baseline) {
3566
+ return void 0;
3567
+ }
3568
+ let newIssues = 0;
3569
+ let existingIssues = 0;
3570
+ for (const issue of issues) {
3571
+ if (issue.lifecycle === "existing") {
3572
+ existingIssues++;
3573
+ } else {
3574
+ newIssues++;
3575
+ }
3576
+ }
3577
+ return {
3578
+ newIssues,
3579
+ existingIssues,
3580
+ fixedIssues: fixedIssues.length,
3581
+ baselineUsed: true,
3582
+ baselineCommit: baseline.commit,
3583
+ baselineTimestamp: baseline.timestamp
3584
+ };
3585
+ }
3586
+ isSeverity(value) {
3587
+ return value === "high" || value === "medium" || value === "low" || value === "info";
3588
+ }
3589
+ isRuleCategory(value) {
3590
+ return ["security", "logic", "structure", "style", "coverage"].includes(value);
3591
+ }
3254
3592
  groupByDimension(issues) {
3255
- const categories = [
3256
- "security",
3257
- "logic",
3258
- "structure",
3259
- "style",
3260
- "coverage"
3261
- ];
3593
+ const buckets = {
3594
+ security: [],
3595
+ logic: [],
3596
+ structure: [],
3597
+ style: [],
3598
+ coverage: []
3599
+ };
3600
+ for (const issue of issues) {
3601
+ buckets[issue.category]?.push(issue);
3602
+ }
3262
3603
  const grouped = {};
3263
- for (const cat of categories) {
3264
- const catIssues = issues.filter((issue) => issue.category === cat);
3265
- grouped[cat] = calculateDimensionScore(catIssues);
3604
+ for (const cat of Object.keys(buckets)) {
3605
+ grouped[cat] = calculateDimensionScore(buckets[cat]);
3266
3606
  }
3267
3607
  return grouped;
3268
3608
  }
@@ -3366,6 +3706,7 @@ thresholds:
3366
3706
  min-score: 70
3367
3707
  max-function-length: 40
3368
3708
  max-cyclomatic-complexity: 10
3709
+ max-cognitive-complexity: 20
3369
3710
  max-nesting-depth: 4
3370
3711
  max-params: 5
3371
3712