@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/cli/index.js CHANGED
@@ -104,6 +104,7 @@ thresholds:
104
104
  min-score: 70
105
105
  max-function-length: 40
106
106
  max-cyclomatic-complexity: 10
107
+ max-cognitive-complexity: 20
107
108
  max-nesting-depth: 4
108
109
  max-params: 5
109
110
 
@@ -128,33 +129,64 @@ import { fileURLToPath } from "url";
128
129
 
129
130
  // src/parsers/diff.ts
130
131
  import simpleGit from "simple-git";
132
+ var GIT_DIFF_UNIFIED = "--unified=3";
133
+ var SHORT_HASH_LENGTH = 7;
131
134
  var DiffParser = class {
132
135
  git;
133
136
  constructor(workDir) {
134
137
  this.git = simpleGit(workDir);
135
138
  }
136
139
  async getStagedFiles() {
137
- const diffDetail = await this.git.diff(["--cached", "--unified=3"]);
140
+ const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
138
141
  return this.parseDiffOutput(diffDetail);
139
142
  }
140
143
  async getDiffFromRef(ref) {
141
- const diffDetail = await this.git.diff([ref, "--unified=3"]);
144
+ const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
142
145
  return this.parseDiffOutput(diffDetail);
143
146
  }
144
147
  async getChangedFiles() {
145
- const diffDetail = await this.git.diff(["--unified=3"]);
146
- const stagedDetail = await this.git.diff(["--cached", "--unified=3"]);
147
- const allDiff = diffDetail + "\n" + stagedDetail;
148
- return this.parseDiffOutput(allDiff);
148
+ const diffDetail = await this.git.diff([GIT_DIFF_UNIFIED]);
149
+ const stagedDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
150
+ const unstagedFiles = this.parseDiffOutput(diffDetail);
151
+ const stagedFiles = this.parseDiffOutput(stagedDetail);
152
+ return this.mergeDiffFiles(unstagedFiles, stagedFiles);
153
+ }
154
+ /**
155
+ * Merge two sets of diff files, deduplicating by file path.
156
+ * When a file appears in both, merge their hunks and combine stats.
157
+ */
158
+ mergeDiffFiles(unstaged, staged) {
159
+ const fileMap = /* @__PURE__ */ new Map();
160
+ for (const file of unstaged) {
161
+ fileMap.set(file.filePath, file);
162
+ }
163
+ for (const file of staged) {
164
+ const existing = fileMap.get(file.filePath);
165
+ if (existing) {
166
+ fileMap.set(file.filePath, {
167
+ ...existing,
168
+ // Combine additions/deletions
169
+ additions: existing.additions + file.additions,
170
+ deletions: existing.deletions + file.deletions,
171
+ // Merge hunks (preserve order: staged first, then unstaged)
172
+ hunks: [...file.hunks, ...existing.hunks],
173
+ // Status: if either is 'added', treat as added; otherwise keep modified
174
+ status: existing.status === "added" || file.status === "added" ? "added" : "modified"
175
+ });
176
+ } else {
177
+ fileMap.set(file.filePath, file);
178
+ }
179
+ }
180
+ return Array.from(fileMap.values());
149
181
  }
150
182
  async getLastCommitDiff() {
151
- const diffDetail = await this.git.diff(["HEAD~1", "HEAD", "--unified=3"]);
183
+ const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
152
184
  return this.parseDiffOutput(diffDetail);
153
185
  }
154
186
  async getCurrentCommitHash() {
155
187
  try {
156
188
  const hash = await this.git.revparse(["HEAD"]);
157
- return hash.trim().slice(0, 7);
189
+ return hash.trim().slice(0, SHORT_HASH_LENGTH);
158
190
  } catch {
159
191
  return void 0;
160
192
  }
@@ -180,9 +212,18 @@ var DiffParser = class {
180
212
  }
181
213
  parseFileDiff(fileDiff) {
182
214
  const lines = fileDiff.split("\n");
183
- const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
184
- if (!headerMatch) return null;
185
- const filePath = headerMatch[2];
215
+ let filePath = null;
216
+ for (const line of lines) {
217
+ if (line.startsWith("+++ b/")) {
218
+ filePath = line.slice(6);
219
+ break;
220
+ }
221
+ }
222
+ if (!filePath) {
223
+ const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
224
+ if (!headerMatch) return null;
225
+ filePath = headerMatch[2];
226
+ }
186
227
  let status = "modified";
187
228
  let additions = 0;
188
229
  let deletions = 0;
@@ -286,6 +327,144 @@ function t(en2, zh2) {
286
327
  return isZhLocale() ? zh2 : en2;
287
328
  }
288
329
 
330
+ // src/rules/brace-utils.ts
331
+ function countBracesInLine(line, startJ, initialDepth, inBlockComment) {
332
+ let depth = initialDepth;
333
+ let i = startJ;
334
+ while (i < line.length) {
335
+ if (inBlockComment.value) {
336
+ const closeIdx = line.indexOf("*/", i);
337
+ if (closeIdx === -1) return depth;
338
+ inBlockComment.value = false;
339
+ i = closeIdx + 2;
340
+ continue;
341
+ }
342
+ const ch = line[i];
343
+ if (ch === "/" && line[i + 1] === "/") return depth;
344
+ if (ch === "/" && line[i + 1] === "*") {
345
+ inBlockComment.value = true;
346
+ i += 2;
347
+ continue;
348
+ }
349
+ if (ch === "'" || ch === '"') {
350
+ i = skipStringLiteral(line, i, ch);
351
+ continue;
352
+ }
353
+ if (ch === "`") {
354
+ i = skipTemplateLiteral(line, i);
355
+ continue;
356
+ }
357
+ if (ch === "/" && i > 0) {
358
+ const prevNonSpace = findPrevNonSpace(line, i);
359
+ if (prevNonSpace !== -1 && "=(!|&:,;[{?+->~%^".includes(line[prevNonSpace])) {
360
+ i = skipRegexpLiteral(line, i);
361
+ continue;
362
+ }
363
+ }
364
+ if (ch === "{") depth++;
365
+ if (ch === "}") depth--;
366
+ i++;
367
+ }
368
+ return depth;
369
+ }
370
+ function skipStringLiteral(line, start, quote) {
371
+ let i = start + 1;
372
+ while (i < line.length) {
373
+ if (line[i] === "\\") {
374
+ i += 2;
375
+ continue;
376
+ }
377
+ if (line[i] === quote) return i + 1;
378
+ i++;
379
+ }
380
+ return i;
381
+ }
382
+ function skipTemplateLiteral(line, start) {
383
+ let i = start + 1;
384
+ while (i < line.length) {
385
+ if (line[i] === "\\") {
386
+ i += 2;
387
+ continue;
388
+ }
389
+ if (line[i] === "`") return i + 1;
390
+ i++;
391
+ }
392
+ return i;
393
+ }
394
+ function skipRegexpLiteral(line, start) {
395
+ let i = start + 1;
396
+ while (i < line.length) {
397
+ if (line[i] === "\\") {
398
+ i += 2;
399
+ continue;
400
+ }
401
+ if (line[i] === "/") return i + 1;
402
+ i++;
403
+ }
404
+ return i;
405
+ }
406
+ function findPrevNonSpace(line, pos) {
407
+ for (let i = pos - 1; i >= 0; i--) {
408
+ if (line[i] !== " " && line[i] !== " ") return i;
409
+ }
410
+ return -1;
411
+ }
412
+ function extractBraceBlock(lines, startLineIndex, startCol) {
413
+ let depth = 0;
414
+ let started = false;
415
+ let bodyStart = -1;
416
+ const blockComment = { value: false };
417
+ for (let i = startLineIndex; i < lines.length; i++) {
418
+ const startJ = i === startLineIndex ? startCol : 0;
419
+ const line = lines[i];
420
+ let j = startJ;
421
+ while (j < line.length) {
422
+ if (blockComment.value) {
423
+ const closeIdx = line.indexOf("*/", j);
424
+ if (closeIdx === -1) {
425
+ j = line.length;
426
+ continue;
427
+ }
428
+ blockComment.value = false;
429
+ j = closeIdx + 2;
430
+ continue;
431
+ }
432
+ const ch = line[j];
433
+ if (ch === "/" && line[j + 1] === "/") break;
434
+ if (ch === "/" && line[j + 1] === "*") {
435
+ blockComment.value = true;
436
+ j += 2;
437
+ continue;
438
+ }
439
+ if (ch === "'" || ch === '"') {
440
+ j = skipStringLiteral(line, j, ch);
441
+ continue;
442
+ }
443
+ if (ch === "`") {
444
+ j = skipTemplateLiteral(line, j);
445
+ continue;
446
+ }
447
+ if (ch === "{") {
448
+ depth++;
449
+ if (!started) {
450
+ started = true;
451
+ bodyStart = i;
452
+ }
453
+ } else if (ch === "}") {
454
+ depth--;
455
+ if (started && depth === 0) {
456
+ return {
457
+ bodyLines: lines.slice(bodyStart + 1, i),
458
+ endLine: i
459
+ };
460
+ }
461
+ }
462
+ j++;
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+
289
468
  // src/rules/builtin/unnecessary-try-catch.ts
290
469
  var unnecessaryTryCatchRule = {
291
470
  id: "logic/unnecessary-try-catch",
@@ -301,38 +480,53 @@ var unnecessaryTryCatchRule = {
301
480
  const line = lines[i];
302
481
  const trimmed = line.trim();
303
482
  if (trimmed.startsWith("try") && trimmed.includes("{")) {
304
- const tryBlock = extractBlock(lines, i);
483
+ const tryCol = line.indexOf("try");
484
+ const tryBlock = extractBraceBlock(lines, i, tryCol);
305
485
  if (tryBlock) {
306
- const { bodyLines, catchBodyLines, endLine } = tryBlock;
307
- const nonEmptyBody = bodyLines.filter((l) => l.trim().length > 0);
308
- const nonEmptyCatch = catchBodyLines.filter((l) => l.trim().length > 0);
309
- const isSimpleBody = nonEmptyBody.length <= 2;
310
- const isGenericCatch = nonEmptyCatch.length <= 2 && nonEmptyCatch.some(
311
- (l) => /console\.(log|error|warn)/.test(l) || /throw\s+(new\s+)?Error/.test(l) || l.trim() === ""
312
- );
313
- const bodyHasOnlyAssignments = nonEmptyBody.every(
314
- (l) => /^\s*(const|let|var)\s+/.test(l) || /^\s*\w+(\.\w+)*\s*=\s*/.test(l) || /^\s*return\s+/.test(l)
315
- );
316
- if (isSimpleBody && isGenericCatch && bodyHasOnlyAssignments) {
317
- issues.push({
318
- ruleId: "logic/unnecessary-try-catch",
319
- severity: "medium",
320
- category: "logic",
321
- file: context.filePath,
322
- startLine: i + 1,
323
- endLine: endLine + 1,
324
- message: t(
325
- "Unnecessary try-catch wrapping a simple statement with generic error handling. This is likely AI-hallucinated error handling.",
326
- "\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"
327
- ),
328
- suggestion: t(
329
- "Remove the try-catch block or add meaningful error recovery logic.",
330
- "\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u6DFB\u52A0\u6709\u610F\u4E49\u7684\u9519\u8BEF\u6062\u590D\u903B\u8F91\u3002"
331
- )
332
- });
486
+ const { bodyLines: tryBodyLines, endLine: tryEndLine } = tryBlock;
487
+ let catchLineIdx = -1;
488
+ for (let k = tryEndLine; k < Math.min(tryEndLine + 2, lines.length); k++) {
489
+ if (lines[k].includes("catch")) {
490
+ catchLineIdx = k;
491
+ break;
492
+ }
493
+ }
494
+ if (catchLineIdx !== -1) {
495
+ const catchCol = lines[catchLineIdx].indexOf("catch");
496
+ const catchBlock = extractBraceBlock(lines, catchLineIdx, catchCol);
497
+ if (catchBlock) {
498
+ const { bodyLines: catchBodyLines, endLine: catchEndLine } = catchBlock;
499
+ const nonEmptyBody = tryBodyLines.filter((l) => l.trim().length > 0);
500
+ const nonEmptyCatch = catchBodyLines.filter((l) => l.trim().length > 0);
501
+ const isSimpleBody = nonEmptyBody.length <= 2;
502
+ const isGenericCatch = nonEmptyCatch.length <= 2 && nonEmptyCatch.some(
503
+ (l) => /console\.(log|error|warn)/.test(l) || /throw\s+(new\s+)?Error/.test(l) || l.trim() === ""
504
+ );
505
+ const bodyHasOnlyAssignments = nonEmptyBody.every(
506
+ (l) => /^\s*(const|let|var)\s+/.test(l) || /^\s*\w+(\.\w+)*\s*=\s*/.test(l) || /^\s*return\s+/.test(l)
507
+ );
508
+ if (isSimpleBody && isGenericCatch && bodyHasOnlyAssignments) {
509
+ issues.push({
510
+ ruleId: "logic/unnecessary-try-catch",
511
+ severity: "medium",
512
+ category: "logic",
513
+ file: context.filePath,
514
+ startLine: i + 1,
515
+ endLine: catchEndLine + 1,
516
+ message: t(
517
+ "Unnecessary try-catch wrapping a simple statement with generic error handling. This is likely AI-hallucinated error handling.",
518
+ "\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"
519
+ ),
520
+ suggestion: t(
521
+ "Remove the try-catch block or add meaningful error recovery logic.",
522
+ "\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u6DFB\u52A0\u6709\u610F\u4E49\u7684\u9519\u8BEF\u6062\u590D\u903B\u8F91\u3002"
523
+ )
524
+ });
525
+ }
526
+ i = catchEndLine + 1;
527
+ continue;
528
+ }
333
529
  }
334
- i = endLine + 1;
335
- continue;
336
530
  }
337
531
  }
338
532
  i++;
@@ -340,55 +534,6 @@ var unnecessaryTryCatchRule = {
340
534
  return issues;
341
535
  }
342
536
  };
343
- function extractBlock(lines, tryLineIndex) {
344
- let braceCount = 0;
345
- let foundTryOpen = false;
346
- let tryBodyStart = -1;
347
- let tryBodyEnd = -1;
348
- let catchStart = -1;
349
- let catchBodyStart = -1;
350
- let catchBodyEnd = -1;
351
- for (let i = tryLineIndex; i < lines.length; i++) {
352
- const line = lines[i];
353
- for (const ch of line) {
354
- if (ch === "{") {
355
- braceCount++;
356
- if (!foundTryOpen) {
357
- foundTryOpen = true;
358
- tryBodyStart = i;
359
- }
360
- } else if (ch === "}") {
361
- braceCount--;
362
- if (braceCount === 0 && tryBodyEnd === -1) {
363
- tryBodyEnd = i;
364
- } else if (braceCount === 0 && catchBodyEnd === -1 && catchBodyStart !== -1) {
365
- catchBodyEnd = i;
366
- break;
367
- }
368
- }
369
- }
370
- if (tryBodyEnd !== -1 && catchStart === -1) {
371
- if (line.includes("catch")) {
372
- catchStart = i;
373
- }
374
- }
375
- if (catchStart !== -1 && catchBodyStart === -1 && line.includes("{")) {
376
- catchBodyStart = i;
377
- }
378
- if (catchBodyEnd !== -1) break;
379
- }
380
- if (tryBodyStart === -1 || tryBodyEnd === -1 || catchBodyStart === -1 || catchBodyEnd === -1) {
381
- return null;
382
- }
383
- const bodyLines = lines.slice(tryBodyStart + 1, tryBodyEnd);
384
- const catchBodyLines = lines.slice(catchBodyStart + 1, catchBodyEnd);
385
- return {
386
- bodyLines,
387
- catchStart,
388
- catchBodyLines,
389
- endLine: catchBodyEnd
390
- };
391
- }
392
537
 
393
538
  // src/rules/builtin/over-defensive.ts
394
539
  var overDefensiveRule = {
@@ -577,18 +722,10 @@ function detectCodeAfterReturn(context, lines, issues) {
577
722
  let braceDepth = 0;
578
723
  let lastReturnDepth = -1;
579
724
  let lastReturnLine = -1;
725
+ const blockComment = { value: false };
580
726
  for (let i = 0; i < lines.length; i++) {
581
727
  const trimmed = lines[i].trim();
582
- for (const ch of trimmed) {
583
- if (ch === "{") braceDepth++;
584
- if (ch === "}") {
585
- if (braceDepth === lastReturnDepth) {
586
- lastReturnDepth = -1;
587
- lastReturnLine = -1;
588
- }
589
- braceDepth--;
590
- }
591
- }
728
+ braceDepth = countBracesInLine(lines[i], 0, braceDepth, blockComment);
592
729
  if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
593
730
  const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
594
731
  if (endsOpen) continue;
@@ -614,6 +751,10 @@ function detectCodeAfterReturn(context, lines, issues) {
614
751
  lastReturnDepth = -1;
615
752
  lastReturnLine = -1;
616
753
  }
754
+ if (braceDepth < lastReturnDepth) {
755
+ lastReturnDepth = -1;
756
+ lastReturnLine = -1;
757
+ }
617
758
  }
618
759
  }
619
760
  function detectImmediateReassign(context, lines, issues) {
@@ -647,7 +788,21 @@ function detectImmediateReassign(context, lines, issues) {
647
788
  }
648
789
 
649
790
  // src/rules/fix-utils.ts
650
- function lineStartOffset(content, lineNumber) {
791
+ function buildLineOffsets(content) {
792
+ const offsets = [0];
793
+ for (let i = 0; i < content.length; i++) {
794
+ if (content[i] === "\n") {
795
+ offsets.push(i + 1);
796
+ }
797
+ }
798
+ return offsets;
799
+ }
800
+ function lineStartOffset(content, lineNumber, offsets) {
801
+ if (offsets) {
802
+ const idx = lineNumber - 1;
803
+ if (idx < 0 || idx >= offsets.length) return content.length;
804
+ return offsets[idx];
805
+ }
651
806
  let offset = 0;
652
807
  const lines = content.split("\n");
653
808
  for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
@@ -655,12 +810,13 @@ function lineStartOffset(content, lineNumber) {
655
810
  }
656
811
  return offset;
657
812
  }
658
- function lineRange(content, lineNumber) {
659
- const lines = content.split("\n");
813
+ function lineRange(content, lineNumber, offsets) {
814
+ const table = offsets ?? buildLineOffsets(content);
660
815
  const lineIndex = lineNumber - 1;
661
- if (lineIndex < 0 || lineIndex >= lines.length) return [0, 0];
662
- const start = lineStartOffset(content, lineNumber);
663
- const end = start + lines[lineIndex].length + (lineIndex < lines.length - 1 ? 1 : 0);
816
+ if (lineIndex < 0 || lineIndex >= table.length) return [0, 0];
817
+ const start = table[lineIndex];
818
+ const nextLineStart = lineIndex + 1 < table.length ? table[lineIndex + 1] : content.length;
819
+ const end = lineIndex + 1 < table.length ? nextLineStart : nextLineStart;
664
820
  return [start, end];
665
821
  }
666
822
 
@@ -726,12 +882,12 @@ function extractFunctions(parsed) {
726
882
  }
727
883
  function visitNode(root, functions) {
728
884
  const methodBodies = /* @__PURE__ */ new WeakSet();
729
- walkAST(root, (node) => {
885
+ walkAST(root, (node, parent) => {
730
886
  if (node.type === AST_NODE_TYPES.FunctionExpression && methodBodies.has(node)) {
731
887
  return false;
732
888
  }
733
889
  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) {
734
- const info = analyzeFunctionNode(node);
890
+ const info = analyzeFunctionNode(node, parent);
735
891
  if (info) functions.push(info);
736
892
  if (node.type === AST_NODE_TYPES.MethodDefinition) {
737
893
  methodBodies.add(node.value);
@@ -740,7 +896,7 @@ function visitNode(root, functions) {
740
896
  return;
741
897
  });
742
898
  }
743
- function analyzeFunctionNode(node) {
899
+ function analyzeFunctionNode(node, parent) {
744
900
  let name = "<anonymous>";
745
901
  let params = [];
746
902
  let body = null;
@@ -753,7 +909,11 @@ function analyzeFunctionNode(node) {
753
909
  params = node.params;
754
910
  body = node.body;
755
911
  } else if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
756
- name = "<arrow>";
912
+ if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier) {
913
+ name = parent.id.name;
914
+ } else {
915
+ name = "<arrow>";
916
+ }
757
917
  params = node.params;
758
918
  body = node.body;
759
919
  } else if (node.type === AST_NODE_TYPES.MethodDefinition) {
@@ -781,6 +941,9 @@ function analyzeFunctionNode(node) {
781
941
  function calculateCyclomaticComplexity(root) {
782
942
  let complexity = 1;
783
943
  walkAST(root, (n) => {
944
+ 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) {
945
+ return false;
946
+ }
784
947
  switch (n.type) {
785
948
  case AST_NODE_TYPES.IfStatement:
786
949
  case AST_NODE_TYPES.ConditionalExpression:
@@ -809,6 +972,9 @@ function calculateCognitiveComplexity(root) {
809
972
  const depthMap = /* @__PURE__ */ new WeakMap();
810
973
  depthMap.set(root, 0);
811
974
  walkAST(root, (n, parent) => {
975
+ 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) {
976
+ return false;
977
+ }
812
978
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
813
979
  const isNesting = isNestingNode(n);
814
980
  const depth = isNesting ? parentDepth + 1 : parentDepth;
@@ -827,6 +993,9 @@ function calculateMaxNestingDepth(root) {
827
993
  const depthMap = /* @__PURE__ */ new WeakMap();
828
994
  depthMap.set(root, 0);
829
995
  walkAST(root, (n, parent) => {
996
+ 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) {
997
+ return false;
998
+ }
830
999
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
831
1000
  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;
832
1001
  const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
@@ -934,7 +1103,24 @@ function collectDeclarationsAndReferences(root, declarations, references) {
934
1103
  kind: exportedNames.has(node.id.name) ? "export" : "local"
935
1104
  });
936
1105
  }
937
- if (node.type === AST_NODE_TYPES.Identifier && parentType !== "VariableDeclarator" && parentType !== "FunctionDeclaration") {
1106
+ if (node.type === AST_NODE_TYPES.ClassDeclaration && node.id) {
1107
+ declarations.set(node.id.name, {
1108
+ line: node.loc?.start.line ?? 0,
1109
+ kind: exportedNames.has(node.id.name) ? "export" : "local"
1110
+ });
1111
+ }
1112
+ const declarationParentTypes = /* @__PURE__ */ new Set([
1113
+ "VariableDeclarator",
1114
+ "FunctionDeclaration",
1115
+ "ClassDeclaration",
1116
+ "MethodDefinition",
1117
+ "TSEnumDeclaration",
1118
+ "TSEnumMember",
1119
+ "TSTypeAliasDeclaration",
1120
+ "TSInterfaceDeclaration",
1121
+ "TSModuleDeclaration"
1122
+ ]);
1123
+ if (node.type === AST_NODE_TYPES.Identifier && !declarationParentTypes.has(parentType ?? "")) {
938
1124
  references.add(node.name);
939
1125
  }
940
1126
  return;
@@ -1008,16 +1194,38 @@ function stringifyCondition(node) {
1008
1194
  case AST_NODE_TYPES.Literal:
1009
1195
  return String(node.value);
1010
1196
  case AST_NODE_TYPES.BinaryExpression:
1197
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
1011
1198
  case AST_NODE_TYPES.LogicalExpression:
1012
1199
  return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
1013
1200
  case AST_NODE_TYPES.UnaryExpression:
1014
1201
  return `${node.operator}${stringifyCondition(node.argument)}`;
1015
1202
  case AST_NODE_TYPES.MemberExpression:
1016
1203
  return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
1017
- case AST_NODE_TYPES.CallExpression:
1018
- return `${stringifyCondition(node.callee)}(...)`;
1204
+ case AST_NODE_TYPES.CallExpression: {
1205
+ const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
1206
+ return `${stringifyCondition(node.callee)}(${args})`;
1207
+ }
1208
+ case AST_NODE_TYPES.ConditionalExpression:
1209
+ return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
1210
+ case AST_NODE_TYPES.TemplateLiteral:
1211
+ return `\`template@${node.loc?.start.line}:${node.loc?.start.column}\``;
1212
+ case AST_NODE_TYPES.ArrayExpression:
1213
+ return `[${node.elements.map((e) => e ? stringifyCondition(e) : "empty").join(", ")}]`;
1214
+ case AST_NODE_TYPES.ObjectExpression:
1215
+ return `{obj@${node.loc?.start.line}:${node.loc?.start.column}}`;
1216
+ case AST_NODE_TYPES.AssignmentExpression:
1217
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
1218
+ case AST_NODE_TYPES.NewExpression:
1219
+ return `new ${stringifyCondition(node.callee)}(${node.arguments.map((a) => stringifyCondition(a)).join(", ")})`;
1220
+ case AST_NODE_TYPES.TSAsExpression:
1221
+ case AST_NODE_TYPES.TSNonNullExpression:
1222
+ return stringifyCondition(node.expression);
1223
+ case AST_NODE_TYPES.AwaitExpression:
1224
+ return `await ${stringifyCondition(node.argument)}`;
1225
+ case AST_NODE_TYPES.ChainExpression:
1226
+ return stringifyCondition(node.expression);
1019
1227
  default:
1020
- return `[${node.type}]`;
1228
+ return `[${node.type}@${node.loc?.start.line}:${node.loc?.start.column}]`;
1021
1229
  }
1022
1230
  }
1023
1231
  function truncate(s, maxLen) {
@@ -1176,9 +1384,11 @@ var securityRules = [
1176
1384
  const issues = [];
1177
1385
  const lines = context.fileContent.split("\n");
1178
1386
  for (let i = 0; i < lines.length; i++) {
1179
- const trimmed = lines[i].trim();
1387
+ const line = lines[i];
1388
+ const trimmed = line.trim();
1180
1389
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1181
- if (/\.(innerHTML|outerHTML)\s*=/.test(lines[i]) || /dangerouslySetInnerHTML/.test(lines[i])) {
1390
+ const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
1391
+ if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
1182
1392
  issues.push({
1183
1393
  ruleId: "security/dangerous-html",
1184
1394
  severity: "medium",
@@ -1227,7 +1437,8 @@ var emptyCatchRule = {
1227
1437
  const catchMatch = trimmed.match(/\bcatch\s*\(\s*(\w+)?\s*\)\s*\{/);
1228
1438
  if (!catchMatch) continue;
1229
1439
  const catchVarName = catchMatch[1] || "";
1230
- const blockContent = extractCatchBody(lines, i);
1440
+ const catchIdx = lines[i].indexOf("catch");
1441
+ const blockContent = extractBraceBlock(lines, i, catchIdx);
1231
1442
  if (!blockContent) continue;
1232
1443
  const { bodyLines, endLine } = blockContent;
1233
1444
  const meaningful = bodyLines.filter(
@@ -1277,37 +1488,6 @@ var emptyCatchRule = {
1277
1488
  return issues;
1278
1489
  }
1279
1490
  };
1280
- function extractCatchBody(lines, catchLineIndex) {
1281
- const catchLine = lines[catchLineIndex];
1282
- const catchIdx = catchLine.indexOf("catch");
1283
- if (catchIdx === -1) return null;
1284
- let braceCount = 0;
1285
- let started = false;
1286
- let bodyStart = -1;
1287
- for (let i = catchLineIndex; i < lines.length; i++) {
1288
- const line = lines[i];
1289
- const startJ = i === catchLineIndex ? catchIdx : 0;
1290
- for (let j = startJ; j < line.length; j++) {
1291
- const ch = line[j];
1292
- if (ch === "{") {
1293
- braceCount++;
1294
- if (!started) {
1295
- started = true;
1296
- bodyStart = i;
1297
- }
1298
- } else if (ch === "}") {
1299
- braceCount--;
1300
- if (started && braceCount === 0) {
1301
- return {
1302
- bodyLines: lines.slice(bodyStart + 1, i),
1303
- endLine: i
1304
- };
1305
- }
1306
- }
1307
- }
1308
- }
1309
- return null;
1310
- }
1311
1491
 
1312
1492
  // src/rules/builtin/identical-branches.ts
1313
1493
  var identicalBranchesRule = {
@@ -1654,10 +1834,25 @@ var unusedImportRule = {
1654
1834
  }
1655
1835
  return;
1656
1836
  });
1657
- const typeRefPattern = /\b([A-Z][A-Za-z0-9]*)\b/g;
1658
- let match;
1659
- while ((match = typeRefPattern.exec(context.fileContent)) !== null) {
1660
- references.add(match[1]);
1837
+ const codeLines = context.fileContent.split("\n");
1838
+ let inBlock = false;
1839
+ for (const codeLine of codeLines) {
1840
+ const trimmedCode = codeLine.trim();
1841
+ if (inBlock) {
1842
+ if (trimmedCode.includes("*/")) inBlock = false;
1843
+ continue;
1844
+ }
1845
+ if (trimmedCode.startsWith("/*")) {
1846
+ if (!trimmedCode.includes("*/")) inBlock = true;
1847
+ continue;
1848
+ }
1849
+ if (trimmedCode.startsWith("//") || trimmedCode.startsWith("*")) continue;
1850
+ const cleaned = codeLine.replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "").replace(/'(?:[^'\\]|\\.)*'/g, "").replace(/"(?:[^"\\]|\\.)*"/g, "").replace(/`(?:[^`\\]|\\.)*`/g, "");
1851
+ const typeRefPattern = /\b([A-Z][A-Za-z0-9]*)\b/g;
1852
+ let match;
1853
+ while ((match = typeRefPattern.exec(cleaned)) !== null) {
1854
+ references.add(match[1]);
1855
+ }
1661
1856
  }
1662
1857
  for (const imp of imports) {
1663
1858
  if (!references.has(imp.local)) {
@@ -1706,6 +1901,9 @@ var missingAwaitRule = {
1706
1901
  const body = getFunctionBody(node);
1707
1902
  if (!body) return;
1708
1903
  walkAST(body, (inner, parent) => {
1904
+ if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1905
+ return;
1906
+ }
1709
1907
  if (inner !== body && isAsyncFunction(inner)) return false;
1710
1908
  if (inner.type !== AST_NODE_TYPES.CallExpression) return;
1711
1909
  if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
@@ -1715,6 +1913,9 @@ var missingAwaitRule = {
1715
1913
  if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
1716
1914
  if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
1717
1915
  if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
1916
+ if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1917
+ return;
1918
+ }
1718
1919
  const callName = getCallName(inner);
1719
1920
  if (!callName) return;
1720
1921
  if (!asyncFuncNames.has(callName)) return;
@@ -1926,9 +2127,28 @@ var typeCoercionRule = {
1926
2127
  var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
1927
2128
  -1,
1928
2129
  0,
2130
+ 0.1,
2131
+ 0.1,
2132
+ 0.15,
2133
+ 0.2,
2134
+ 0.2,
2135
+ 0.25,
2136
+ 0.3,
2137
+ 0.3,
2138
+ 0.5,
1929
2139
  1,
1930
2140
  2,
2141
+ 3,
2142
+ 4,
2143
+ 5,
1931
2144
  10,
2145
+ 15,
2146
+ 20,
2147
+ 30,
2148
+ 40,
2149
+ 50,
2150
+ 70,
2151
+ 90,
1932
2152
  100
1933
2153
  ]);
1934
2154
  var magicNumberRule = {
@@ -1957,6 +2177,7 @@ var magicNumberRule = {
1957
2177
  if (/^\s*(export\s+)?enum\s/.test(line)) continue;
1958
2178
  if (trimmed.startsWith("import ")) continue;
1959
2179
  if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
2180
+ if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
1960
2181
  const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
1961
2182
  const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
1962
2183
  let match;
@@ -2053,6 +2274,17 @@ var nestedTernaryRule = {
2053
2274
  // src/rules/builtin/duplicate-string.ts
2054
2275
  var MIN_STRING_LENGTH = 6;
2055
2276
  var MIN_OCCURRENCES = 3;
2277
+ var IGNORED_LITERALS = /* @__PURE__ */ new Set([
2278
+ "high",
2279
+ "medium",
2280
+ "low",
2281
+ "info",
2282
+ "logic",
2283
+ "security",
2284
+ "structure",
2285
+ "style",
2286
+ "coverage"
2287
+ ]);
2056
2288
  var duplicateStringRule = {
2057
2289
  id: "logic/duplicate-string",
2058
2290
  category: "logic",
@@ -2083,6 +2315,7 @@ var duplicateStringRule = {
2083
2315
  while ((match = stringRegex.exec(cleaned)) !== null) {
2084
2316
  const value = match[2];
2085
2317
  if (value.length < MIN_STRING_LENGTH) continue;
2318
+ if (IGNORED_LITERALS.has(value)) continue;
2086
2319
  if (value.includes("${")) continue;
2087
2320
  if (value.startsWith("http") || value.startsWith("/")) continue;
2088
2321
  if (value.startsWith("test") || value.startsWith("mock")) continue;
@@ -2352,13 +2585,28 @@ var promiseVoidRule = {
2352
2585
  /^save/,
2353
2586
  /^load/,
2354
2587
  /^send/,
2355
- /^delete/,
2356
2588
  /^update/,
2357
2589
  /^create/,
2358
2590
  /^connect/,
2359
2591
  /^disconnect/,
2360
2592
  /^init/
2361
2593
  ];
2594
+ const syncMethods = [
2595
+ "delete",
2596
+ // Map.delete(), Set.delete(), Object.delete() are synchronous
2597
+ "has",
2598
+ // Map.has(), Set.has() are synchronous
2599
+ "get",
2600
+ // Map.get() is synchronous
2601
+ "set",
2602
+ // Map.set() is synchronous (though some consider it potentially async)
2603
+ "keys",
2604
+ // Object.keys() is synchronous
2605
+ "values",
2606
+ // Object.values() is synchronous
2607
+ "entries"
2608
+ // Object.entries() is synchronous
2609
+ ];
2362
2610
  walkAST(ast, (node) => {
2363
2611
  if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
2364
2612
  const expr = node.expression;
@@ -2369,6 +2617,7 @@ var promiseVoidRule = {
2369
2617
  const isKnownAsync = asyncFnNames.has(fnName);
2370
2618
  const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
2371
2619
  const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
2620
+ if (syncMethods.includes(fnName)) return;
2372
2621
  if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
2373
2622
  const line = node.loc?.start.line ?? 0;
2374
2623
  if (line === 0) return;
@@ -2729,13 +2978,20 @@ var SEVERITY_PENALTY = {
2729
2978
  low: 3,
2730
2979
  info: 0
2731
2980
  };
2981
+ var DIMINISHING_FACTOR = 0.7;
2732
2982
  function calculateDimensionScore(issues) {
2733
2983
  let score = 100;
2984
+ const severityCounts = {};
2734
2985
  for (const issue of issues) {
2735
- score -= SEVERITY_PENALTY[issue.severity] ?? 0;
2986
+ const base = SEVERITY_PENALTY[issue.severity] ?? 0;
2987
+ if (base === 0) continue;
2988
+ const n = severityCounts[issue.severity] ?? 0;
2989
+ severityCounts[issue.severity] = n + 1;
2990
+ const penalty = base * Math.pow(DIMINISHING_FACTOR, n);
2991
+ score -= penalty;
2736
2992
  }
2737
2993
  return {
2738
- score: Math.max(0, Math.min(100, score)),
2994
+ score: Math.round(Math.max(0, Math.min(100, score)) * 10) / 10,
2739
2995
  issues
2740
2996
  };
2741
2997
  }
@@ -3088,7 +3344,7 @@ var PKG_VERSION = (() => {
3088
3344
  }
3089
3345
  })();
3090
3346
  var REPORT_SCHEMA_VERSION = "1.0.0";
3091
- var FINGERPRINT_VERSION = "1";
3347
+ var FINGERPRINT_VERSION = "2";
3092
3348
  var ScanEngine = class {
3093
3349
  config;
3094
3350
  diffParser;
@@ -3120,7 +3376,11 @@ var ScanEngine = class {
3120
3376
  }
3121
3377
  }
3122
3378
  const issuesWithFingerprints = this.attachFingerprints(allIssues);
3123
- const dimensions = this.groupByDimension(issuesWithFingerprints);
3379
+ const baseline = await this.loadBaseline(options.baseline);
3380
+ const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
3381
+ const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
3382
+ const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
3383
+ const dimensions = this.groupByDimension(issuesWithLifecycle);
3124
3384
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
3125
3385
  const grade = getGrade(overallScore);
3126
3386
  const commitHash = await this.diffParser.getCurrentCommitHash();
@@ -3134,7 +3394,7 @@ var ScanEngine = class {
3134
3394
  score: overallScore,
3135
3395
  grade,
3136
3396
  filesScanned,
3137
- issuesFound: issuesWithFingerprints.length
3397
+ issuesFound: issuesWithLifecycle.length
3138
3398
  },
3139
3399
  toolHealth: {
3140
3400
  rulesExecuted,
@@ -3147,70 +3407,75 @@ var ScanEngine = class {
3147
3407
  ruleFailures
3148
3408
  },
3149
3409
  dimensions,
3150
- issues: issuesWithFingerprints.sort((a, b) => {
3410
+ issues: issuesWithLifecycle.sort((a, b) => {
3151
3411
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
3152
3412
  return severityOrder[a.severity] - severityOrder[b.severity];
3153
- })
3413
+ }),
3414
+ lifecycle,
3415
+ fixedIssues
3154
3416
  };
3155
3417
  }
3156
3418
  async scanFile(diffFile) {
3157
3419
  if (diffFile.status === "deleted") {
3158
- return {
3159
- issues: [],
3160
- ruleFailures: [],
3161
- rulesExecuted: 0,
3162
- rulesFailed: 0,
3163
- scanErrors: [
3164
- {
3165
- type: "deleted-file",
3166
- file: diffFile.filePath,
3167
- message: `Skipped deleted file: ${diffFile.filePath}`
3168
- }
3169
- ],
3170
- scanned: false
3171
- };
3420
+ return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
3172
3421
  }
3173
3422
  if (!this.isTsJsFile(diffFile.filePath)) {
3174
- return {
3175
- issues: [],
3176
- ruleFailures: [],
3177
- rulesExecuted: 0,
3178
- rulesFailed: 0,
3179
- scanErrors: [
3180
- {
3181
- type: "unsupported-file-type",
3182
- file: diffFile.filePath,
3183
- message: `Skipped unsupported file type: ${diffFile.filePath}`
3184
- }
3185
- ],
3186
- scanned: false
3187
- };
3423
+ return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
3188
3424
  }
3425
+ const fileContent = await this.readFileContent(diffFile);
3426
+ if (!fileContent) {
3427
+ return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
3428
+ }
3429
+ const addedLines = this.extractAddedLines(diffFile);
3430
+ const ruleResult = this.ruleEngine.runWithDiagnostics({
3431
+ filePath: diffFile.filePath,
3432
+ fileContent,
3433
+ addedLines
3434
+ });
3435
+ const issues = [...ruleResult.issues];
3436
+ issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
3437
+ issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
3438
+ issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
3439
+ return {
3440
+ issues,
3441
+ ruleFailures: ruleResult.ruleFailures,
3442
+ rulesExecuted: ruleResult.rulesExecuted,
3443
+ rulesFailed: ruleResult.rulesFailed,
3444
+ scanErrors: [],
3445
+ scanned: true
3446
+ };
3447
+ }
3448
+ createSkippedResult(diffFile, type, message) {
3449
+ return {
3450
+ issues: [],
3451
+ ruleFailures: [],
3452
+ rulesExecuted: 0,
3453
+ rulesFailed: 0,
3454
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3455
+ scanned: false
3456
+ };
3457
+ }
3458
+ createErrorResult(diffFile, type, message) {
3459
+ return {
3460
+ issues: [],
3461
+ ruleFailures: [],
3462
+ rulesExecuted: 0,
3463
+ rulesFailed: 0,
3464
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3465
+ scanned: false
3466
+ };
3467
+ }
3468
+ async readFileContent(diffFile) {
3189
3469
  const filePath = resolve2(diffFile.filePath);
3190
- let fileContent;
3191
3470
  try {
3192
- fileContent = await readFile(filePath, "utf-8");
3471
+ return await readFile(filePath, "utf-8");
3193
3472
  } catch {
3194
3473
  const content = await this.diffParser.getFileContent(diffFile.filePath);
3195
- if (!content) {
3196
- return {
3197
- issues: [],
3198
- ruleFailures: [],
3199
- rulesExecuted: 0,
3200
- rulesFailed: 0,
3201
- scanErrors: [
3202
- {
3203
- type: "missing-file-content",
3204
- file: diffFile.filePath,
3205
- message: `Unable to read file content for ${diffFile.filePath}`
3206
- }
3207
- ],
3208
- scanned: false
3209
- };
3210
- }
3211
- fileContent = content;
3474
+ return content ?? null;
3212
3475
  }
3213
- const addedLines = diffFile.hunks.flatMap((hunk) => {
3476
+ }
3477
+ extractAddedLines(diffFile) {
3478
+ return diffFile.hunks.flatMap((hunk) => {
3214
3479
  const lines = hunk.content.split("\n");
3215
3480
  const result = [];
3216
3481
  let currentLine = hunk.newStart;
@@ -3225,32 +3490,15 @@ var ScanEngine = class {
3225
3490
  }
3226
3491
  return result;
3227
3492
  });
3228
- const ruleResult = this.ruleEngine.runWithDiagnostics({
3229
- filePath: diffFile.filePath,
3230
- fileContent,
3231
- addedLines
3232
- });
3233
- const issues = [...ruleResult.issues];
3234
- const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
3493
+ }
3494
+ runStructureAnalysis(fileContent, filePath) {
3495
+ return analyzeStructure(fileContent, filePath, {
3235
3496
  maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3236
3497
  maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3237
3498
  maxFunctionLength: this.config.thresholds["max-function-length"],
3238
3499
  maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3239
3500
  maxParamCount: this.config.thresholds["max-params"]
3240
- });
3241
- issues.push(...structureResult.issues);
3242
- const styleResult = analyzeStyle(fileContent, diffFile.filePath);
3243
- issues.push(...styleResult.issues);
3244
- const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
3245
- issues.push(...coverageResult.issues);
3246
- return {
3247
- issues,
3248
- ruleFailures: ruleResult.ruleFailures,
3249
- rulesExecuted: ruleResult.rulesExecuted,
3250
- rulesFailed: ruleResult.rulesFailed,
3251
- scanErrors: [],
3252
- scanned: true
3253
- };
3501
+ }).issues;
3254
3502
  }
3255
3503
  async getScanCandidates(options) {
3256
3504
  const scanMode = this.getScanMode(options);
@@ -3291,7 +3539,7 @@ var ScanEngine = class {
3291
3539
  }
3292
3540
  return "changed";
3293
3541
  }
3294
- async getDiffFiles(options) {
3542
+ getDiffFiles(options) {
3295
3543
  if (options.staged) {
3296
3544
  return this.diffParser.getStagedFiles();
3297
3545
  }
@@ -3348,13 +3596,14 @@ var ScanEngine = class {
3348
3596
  const occurrenceCounts = /* @__PURE__ */ new Map();
3349
3597
  return issues.map((issue) => {
3350
3598
  const normalizedFile = this.normalizeRelativePath(issue.file);
3351
- const locationComponent = `${issue.startLine}:${issue.endLine}`;
3599
+ const contentSource = issue.codeSnippet ? issue.codeSnippet : `${issue.startLine}:${issue.endLine}`;
3600
+ const contentDigest = createHash("sha256").update(contentSource).digest("hex").slice(0, 16);
3352
3601
  const baseKey = [
3353
3602
  issue.ruleId,
3354
3603
  normalizedFile,
3355
3604
  issue.category,
3356
3605
  issue.severity,
3357
- locationComponent
3606
+ contentDigest
3358
3607
  ].join("|");
3359
3608
  const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
3360
3609
  occurrenceCounts.set(baseKey, occurrenceIndex + 1);
@@ -3372,18 +3621,110 @@ var ScanEngine = class {
3372
3621
  const relativePath = relative(process.cwd(), absolutePath) || filePath;
3373
3622
  return relativePath.split(sep).join("/");
3374
3623
  }
3624
+ async loadBaseline(baselinePath) {
3625
+ if (!baselinePath) {
3626
+ return void 0;
3627
+ }
3628
+ const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
3629
+ const parsed = JSON.parse(baselineContent);
3630
+ const issues = this.parseBaselineIssues(parsed.issues);
3631
+ return {
3632
+ issues,
3633
+ fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
3634
+ commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
3635
+ timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
3636
+ };
3637
+ }
3638
+ parseBaselineIssues(input) {
3639
+ if (!Array.isArray(input)) {
3640
+ return [];
3641
+ }
3642
+ return input.flatMap((item) => {
3643
+ const issue = this.parseBaselineIssue(item);
3644
+ return issue ? [issue] : [];
3645
+ });
3646
+ }
3647
+ parseBaselineIssue(input) {
3648
+ if (!input || typeof input !== "object") {
3649
+ return void 0;
3650
+ }
3651
+ const issue = input;
3652
+ if (!this.isValidBaselineIssue(issue)) {
3653
+ return void 0;
3654
+ }
3655
+ return {
3656
+ ruleId: issue.ruleId,
3657
+ severity: issue.severity,
3658
+ category: issue.category,
3659
+ file: issue.file,
3660
+ startLine: issue.startLine,
3661
+ endLine: issue.endLine,
3662
+ message: issue.message,
3663
+ fingerprint: issue.fingerprint,
3664
+ fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
3665
+ };
3666
+ }
3667
+ isValidBaselineIssue(issue) {
3668
+ 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";
3669
+ }
3670
+ attachLifecycle(issues, baseline) {
3671
+ if (!baseline) {
3672
+ return issues;
3673
+ }
3674
+ return issues.map((issue) => ({
3675
+ ...issue,
3676
+ lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
3677
+ }));
3678
+ }
3679
+ getFixedIssues(issues, baseline) {
3680
+ if (!baseline) {
3681
+ return [];
3682
+ }
3683
+ const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
3684
+ return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
3685
+ }
3686
+ buildLifecycleSummary(issues, fixedIssues, baseline) {
3687
+ if (!baseline) {
3688
+ return void 0;
3689
+ }
3690
+ let newIssues = 0;
3691
+ let existingIssues = 0;
3692
+ for (const issue of issues) {
3693
+ if (issue.lifecycle === "existing") {
3694
+ existingIssues++;
3695
+ } else {
3696
+ newIssues++;
3697
+ }
3698
+ }
3699
+ return {
3700
+ newIssues,
3701
+ existingIssues,
3702
+ fixedIssues: fixedIssues.length,
3703
+ baselineUsed: true,
3704
+ baselineCommit: baseline.commit,
3705
+ baselineTimestamp: baseline.timestamp
3706
+ };
3707
+ }
3708
+ isSeverity(value) {
3709
+ return value === "high" || value === "medium" || value === "low" || value === "info";
3710
+ }
3711
+ isRuleCategory(value) {
3712
+ return ["security", "logic", "structure", "style", "coverage"].includes(value);
3713
+ }
3375
3714
  groupByDimension(issues) {
3376
- const categories = [
3377
- "security",
3378
- "logic",
3379
- "structure",
3380
- "style",
3381
- "coverage"
3382
- ];
3715
+ const buckets = {
3716
+ security: [],
3717
+ logic: [],
3718
+ structure: [],
3719
+ style: [],
3720
+ coverage: []
3721
+ };
3722
+ for (const issue of issues) {
3723
+ buckets[issue.category]?.push(issue);
3724
+ }
3383
3725
  const grouped = {};
3384
- for (const cat of categories) {
3385
- const catIssues = issues.filter((issue) => issue.category === cat);
3386
- grouped[cat] = calculateDimensionScore(catIssues);
3726
+ for (const cat of Object.keys(buckets)) {
3727
+ grouped[cat] = calculateDimensionScore(buckets[cat]);
3387
3728
  }
3388
3729
  return grouped;
3389
3730
  }
@@ -3406,7 +3747,9 @@ var en = {
3406
3747
  healthHeader: "Tool Health",
3407
3748
  rulesFailed: "Failed rules: {{count}}",
3408
3749
  filesSkipped: "Skipped files: {{count}}",
3409
- filesExcluded: "Excluded files: {{count}}"
3750
+ filesExcluded: "Excluded files: {{count}}",
3751
+ lifecycleHeader: "Lifecycle",
3752
+ lifecycleSummary: "New: {{new}} Existing: {{existing}} Fixed: {{fixed}}"
3410
3753
  };
3411
3754
  var zh = {
3412
3755
  reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
@@ -3422,7 +3765,9 @@ var zh = {
3422
3765
  healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
3423
3766
  rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
3424
3767
  filesSkipped: "\u8DF3\u8FC7\u6587\u4EF6\u6570\uFF1A{{count}}",
3425
- filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}"
3768
+ filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}",
3769
+ lifecycleHeader: "\u751F\u547D\u5468\u671F",
3770
+ lifecycleSummary: "\u65B0\u589E\uFF1A{{new}} \u5DF2\u5B58\u5728\uFF1A{{existing}} \u5DF2\u4FEE\u590D\uFF1A{{fixed}}"
3426
3771
  };
3427
3772
  function renderTerminalReport(report) {
3428
3773
  const isZh = isZhLocale();
@@ -3490,6 +3835,11 @@ function renderTerminalReport(report) {
3490
3835
  }
3491
3836
  lines.push("");
3492
3837
  }
3838
+ if (report.lifecycle) {
3839
+ lines.push(pc.bold(t2.lifecycleHeader));
3840
+ lines.push(` ${t2.lifecycleSummary.replace("{{new}}", String(report.lifecycle.newIssues)).replace("{{existing}}", String(report.lifecycle.existingIssues)).replace("{{fixed}}", String(report.lifecycle.fixedIssues))}`);
3841
+ lines.push("");
3842
+ }
3493
3843
  if (report.issues.length > 0) {
3494
3844
  lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
3495
3845
  lines.push("");
@@ -3562,23 +3912,31 @@ function renderJsonReport(report) {
3562
3912
  overall: report.overall,
3563
3913
  toolHealth: report.toolHealth,
3564
3914
  dimensions: report.dimensions,
3565
- issues: report.issues
3915
+ issues: report.issues,
3916
+ lifecycle: report.lifecycle,
3917
+ fixedIssues: report.fixedIssues
3566
3918
  };
3567
3919
  return JSON.stringify(payload, null, 2);
3568
3920
  }
3569
3921
 
3570
3922
  // src/cli/commands/scan.ts
3571
3923
  function createScanCommand() {
3572
- const cmd = new Command("scan").description("Run the primary live trust analysis command").argument("[files...]", "Specific files to scan").option("--staged", "Scan only git staged files").option("--diff <ref>", "Scan diff against a git ref (e.g. HEAD~1, origin/main)").option("--format <format>", "Output format: terminal, json", "terminal").option("--min-score <score>", "Minimum trust score threshold", "0").action(async (files, opts) => {
3924
+ const cmd = new Command("scan").description("Run the primary live trust analysis command").argument("[files...]", "Specific files to scan").option("--staged", "Scan only git staged files").option("--diff <ref>", "Scan diff against a git ref (e.g. HEAD~1, origin/main)").option("--format <format>", "Output format: terminal, json", "terminal").option("--min-score <score>", "Minimum trust score threshold", "0").option("--baseline <path>", "Compare current findings against a prior CodeTrust JSON report").action(async (files, opts) => {
3573
3925
  try {
3574
3926
  const config = await loadConfig();
3575
3927
  const engine = new ScanEngine(config);
3928
+ const parsedMinScore = parseInt(opts.minScore, 10);
3929
+ if (isNaN(parsedMinScore)) {
3930
+ console.error(`Error: Invalid --min-score value "${opts.minScore}". Must be a number.`);
3931
+ process.exit(1);
3932
+ }
3576
3933
  const scanOptions = {
3577
3934
  staged: opts.staged,
3578
3935
  diff: opts.diff,
3579
3936
  files: files.length > 0 ? files : void 0,
3580
3937
  format: opts.format,
3581
- minScore: parseInt(opts.minScore, 10)
3938
+ minScore: parsedMinScore,
3939
+ baseline: opts.baseline
3582
3940
  };
3583
3941
  const report = await engine.scan(scanOptions);
3584
3942
  if (opts.format === "json") {