@gulu9527/code-trust 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -14,9 +14,30 @@ interface Issue {
14
14
  suggestion?: string;
15
15
  codeSnippet?: string;
16
16
  }
17
+ type IssueLifecycleStatus = 'new' | 'existing';
17
18
  interface ReportIssue extends Issue {
18
19
  fingerprint: string;
19
20
  fingerprintVersion: string;
21
+ lifecycle?: IssueLifecycleStatus;
22
+ }
23
+ interface FixedIssue {
24
+ ruleId: string;
25
+ severity: Severity;
26
+ category: RuleCategory;
27
+ file: string;
28
+ startLine: number;
29
+ endLine: number;
30
+ message: string;
31
+ fingerprint: string;
32
+ fingerprintVersion?: string;
33
+ }
34
+ interface LifecycleSummary {
35
+ newIssues: number;
36
+ existingIssues: number;
37
+ fixedIssues: number;
38
+ baselineUsed: boolean;
39
+ baselineCommit?: string;
40
+ baselineTimestamp?: string;
20
41
  }
21
42
  interface DimensionScore {
22
43
  score: number;
@@ -70,6 +91,8 @@ interface TrustReport {
70
91
  coverage: DimensionScore;
71
92
  };
72
93
  issues: ReportIssue[];
94
+ lifecycle?: LifecycleSummary;
95
+ fixedIssues?: FixedIssue[];
73
96
  }
74
97
  interface DiffFile {
75
98
  filePath: string;
@@ -91,6 +114,7 @@ interface ScanOptions {
91
114
  diff?: string;
92
115
  files?: string[];
93
116
  minScore?: number;
117
+ baseline?: string;
94
118
  format?: 'terminal' | 'json' | 'html';
95
119
  }
96
120
 
@@ -134,6 +158,11 @@ declare class ScanEngine {
134
158
  constructor(config: CodeTrustConfig, workDir?: string);
135
159
  scan(options: ScanOptions): Promise<TrustReport>;
136
160
  private scanFile;
161
+ private createSkippedResult;
162
+ private createErrorResult;
163
+ private readFileContent;
164
+ private extractAddedLines;
165
+ private runStructureAnalysis;
137
166
  private getScanCandidates;
138
167
  private getScanMode;
139
168
  private getDiffFiles;
@@ -142,6 +171,15 @@ declare class ScanEngine {
142
171
  private isTsJsFile;
143
172
  private attachFingerprints;
144
173
  private normalizeRelativePath;
174
+ private loadBaseline;
175
+ private parseBaselineIssues;
176
+ private parseBaselineIssue;
177
+ private isValidBaselineIssue;
178
+ private attachLifecycle;
179
+ private getFixedIssues;
180
+ private buildLifecycleSummary;
181
+ private isSeverity;
182
+ private isRuleCategory;
145
183
  private groupByDimension;
146
184
  }
147
185
 
@@ -196,6 +234,11 @@ declare class DiffParser {
196
234
  getStagedFiles(): Promise<DiffFile[]>;
197
235
  getDiffFromRef(ref: string): Promise<DiffFile[]>;
198
236
  getChangedFiles(): Promise<DiffFile[]>;
237
+ /**
238
+ * Merge two sets of diff files, deduplicating by file path.
239
+ * When a file appears in both, merge their hunks and combine stats.
240
+ */
241
+ private mergeDiffFiles;
199
242
  getLastCommitDiff(): Promise<DiffFile[]>;
200
243
  getCurrentCommitHash(): Promise<string | undefined>;
201
244
  getFileContent(filePath: string): Promise<string | undefined>;
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
  }
@@ -660,6 +691,9 @@ function analyzeFunctionNode(node) {
660
691
  function calculateCyclomaticComplexity(root) {
661
692
  let complexity = 1;
662
693
  walkAST(root, (n) => {
694
+ 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) {
695
+ return false;
696
+ }
663
697
  switch (n.type) {
664
698
  case AST_NODE_TYPES.IfStatement:
665
699
  case AST_NODE_TYPES.ConditionalExpression:
@@ -688,6 +722,9 @@ function calculateCognitiveComplexity(root) {
688
722
  const depthMap = /* @__PURE__ */ new WeakMap();
689
723
  depthMap.set(root, 0);
690
724
  walkAST(root, (n, parent) => {
725
+ 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) {
726
+ return false;
727
+ }
691
728
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
692
729
  const isNesting = isNestingNode(n);
693
730
  const depth = isNesting ? parentDepth + 1 : parentDepth;
@@ -706,6 +743,9 @@ function calculateMaxNestingDepth(root) {
706
743
  const depthMap = /* @__PURE__ */ new WeakMap();
707
744
  depthMap.set(root, 0);
708
745
  walkAST(root, (n, parent) => {
746
+ 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) {
747
+ return false;
748
+ }
709
749
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
710
750
  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
751
  const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
@@ -887,14 +927,19 @@ function stringifyCondition(node) {
887
927
  case AST_NODE_TYPES.Literal:
888
928
  return String(node.value);
889
929
  case AST_NODE_TYPES.BinaryExpression:
930
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
890
931
  case AST_NODE_TYPES.LogicalExpression:
891
932
  return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
892
933
  case AST_NODE_TYPES.UnaryExpression:
893
934
  return `${node.operator}${stringifyCondition(node.argument)}`;
894
935
  case AST_NODE_TYPES.MemberExpression:
895
936
  return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
896
- case AST_NODE_TYPES.CallExpression:
897
- return `${stringifyCondition(node.callee)}(...)`;
937
+ case AST_NODE_TYPES.CallExpression: {
938
+ const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
939
+ return `${stringifyCondition(node.callee)}(${args})`;
940
+ }
941
+ case AST_NODE_TYPES.ConditionalExpression:
942
+ return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
898
943
  default:
899
944
  return `[${node.type}]`;
900
945
  }
@@ -1055,9 +1100,11 @@ var securityRules = [
1055
1100
  const issues = [];
1056
1101
  const lines = context.fileContent.split("\n");
1057
1102
  for (let i = 0; i < lines.length; i++) {
1058
- const trimmed = lines[i].trim();
1103
+ const line = lines[i];
1104
+ const trimmed = line.trim();
1059
1105
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1060
- if (/\.(innerHTML|outerHTML)\s*=/.test(lines[i]) || /dangerouslySetInnerHTML/.test(lines[i])) {
1106
+ const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
1107
+ if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
1061
1108
  issues.push({
1062
1109
  ruleId: "security/dangerous-html",
1063
1110
  severity: "medium",
@@ -1585,6 +1632,9 @@ var missingAwaitRule = {
1585
1632
  const body = getFunctionBody(node);
1586
1633
  if (!body) return;
1587
1634
  walkAST(body, (inner, parent) => {
1635
+ if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1636
+ return;
1637
+ }
1588
1638
  if (inner !== body && isAsyncFunction(inner)) return false;
1589
1639
  if (inner.type !== AST_NODE_TYPES.CallExpression) return;
1590
1640
  if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
@@ -1594,6 +1644,9 @@ var missingAwaitRule = {
1594
1644
  if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
1595
1645
  if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
1596
1646
  if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
1647
+ if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1648
+ return;
1649
+ }
1597
1650
  const callName = getCallName(inner);
1598
1651
  if (!callName) return;
1599
1652
  if (!asyncFuncNames.has(callName)) return;
@@ -1805,9 +1858,28 @@ var typeCoercionRule = {
1805
1858
  var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
1806
1859
  -1,
1807
1860
  0,
1861
+ 0.1,
1862
+ 0.1,
1863
+ 0.15,
1864
+ 0.2,
1865
+ 0.2,
1866
+ 0.25,
1867
+ 0.3,
1868
+ 0.3,
1869
+ 0.5,
1808
1870
  1,
1809
1871
  2,
1872
+ 3,
1873
+ 4,
1874
+ 5,
1810
1875
  10,
1876
+ 15,
1877
+ 20,
1878
+ 30,
1879
+ 40,
1880
+ 50,
1881
+ 70,
1882
+ 90,
1811
1883
  100
1812
1884
  ]);
1813
1885
  var magicNumberRule = {
@@ -1836,6 +1908,7 @@ var magicNumberRule = {
1836
1908
  if (/^\s*(export\s+)?enum\s/.test(line)) continue;
1837
1909
  if (trimmed.startsWith("import ")) continue;
1838
1910
  if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
1911
+ if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
1839
1912
  const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
1840
1913
  const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
1841
1914
  let match;
@@ -1932,6 +2005,17 @@ var nestedTernaryRule = {
1932
2005
  // src/rules/builtin/duplicate-string.ts
1933
2006
  var MIN_STRING_LENGTH = 6;
1934
2007
  var MIN_OCCURRENCES = 3;
2008
+ var IGNORED_LITERALS = /* @__PURE__ */ new Set([
2009
+ "high",
2010
+ "medium",
2011
+ "low",
2012
+ "info",
2013
+ "logic",
2014
+ "security",
2015
+ "structure",
2016
+ "style",
2017
+ "coverage"
2018
+ ]);
1935
2019
  var duplicateStringRule = {
1936
2020
  id: "logic/duplicate-string",
1937
2021
  category: "logic",
@@ -1962,6 +2046,7 @@ var duplicateStringRule = {
1962
2046
  while ((match = stringRegex.exec(cleaned)) !== null) {
1963
2047
  const value = match[2];
1964
2048
  if (value.length < MIN_STRING_LENGTH) continue;
2049
+ if (IGNORED_LITERALS.has(value)) continue;
1965
2050
  if (value.includes("${")) continue;
1966
2051
  if (value.startsWith("http") || value.startsWith("/")) continue;
1967
2052
  if (value.startsWith("test") || value.startsWith("mock")) continue;
@@ -2231,13 +2316,28 @@ var promiseVoidRule = {
2231
2316
  /^save/,
2232
2317
  /^load/,
2233
2318
  /^send/,
2234
- /^delete/,
2235
2319
  /^update/,
2236
2320
  /^create/,
2237
2321
  /^connect/,
2238
2322
  /^disconnect/,
2239
2323
  /^init/
2240
2324
  ];
2325
+ const syncMethods = [
2326
+ "delete",
2327
+ // Map.delete(), Set.delete(), Object.delete() are synchronous
2328
+ "has",
2329
+ // Map.has(), Set.has() are synchronous
2330
+ "get",
2331
+ // Map.get() is synchronous
2332
+ "set",
2333
+ // Map.set() is synchronous (though some consider it potentially async)
2334
+ "keys",
2335
+ // Object.keys() is synchronous
2336
+ "values",
2337
+ // Object.values() is synchronous
2338
+ "entries"
2339
+ // Object.entries() is synchronous
2340
+ ];
2241
2341
  walkAST(ast, (node) => {
2242
2342
  if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
2243
2343
  const expr = node.expression;
@@ -2248,6 +2348,7 @@ var promiseVoidRule = {
2248
2348
  const isKnownAsync = asyncFnNames.has(fnName);
2249
2349
  const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
2250
2350
  const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
2351
+ if (syncMethods.includes(fnName)) return;
2251
2352
  if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
2252
2353
  const line = node.loc?.start.line ?? 0;
2253
2354
  if (line === 0) return;
@@ -2999,7 +3100,11 @@ var ScanEngine = class {
2999
3100
  }
3000
3101
  }
3001
3102
  const issuesWithFingerprints = this.attachFingerprints(allIssues);
3002
- const dimensions = this.groupByDimension(issuesWithFingerprints);
3103
+ const baseline = await this.loadBaseline(options.baseline);
3104
+ const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
3105
+ const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
3106
+ const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
3107
+ const dimensions = this.groupByDimension(issuesWithLifecycle);
3003
3108
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
3004
3109
  const grade = getGrade(overallScore);
3005
3110
  const commitHash = await this.diffParser.getCurrentCommitHash();
@@ -3013,7 +3118,7 @@ var ScanEngine = class {
3013
3118
  score: overallScore,
3014
3119
  grade,
3015
3120
  filesScanned,
3016
- issuesFound: issuesWithFingerprints.length
3121
+ issuesFound: issuesWithLifecycle.length
3017
3122
  },
3018
3123
  toolHealth: {
3019
3124
  rulesExecuted,
@@ -3026,70 +3131,75 @@ var ScanEngine = class {
3026
3131
  ruleFailures
3027
3132
  },
3028
3133
  dimensions,
3029
- issues: issuesWithFingerprints.sort((a, b) => {
3134
+ issues: issuesWithLifecycle.sort((a, b) => {
3030
3135
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
3031
3136
  return severityOrder[a.severity] - severityOrder[b.severity];
3032
- })
3137
+ }),
3138
+ lifecycle,
3139
+ fixedIssues
3033
3140
  };
3034
3141
  }
3035
3142
  async scanFile(diffFile) {
3036
3143
  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
- };
3144
+ return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
3051
3145
  }
3052
3146
  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
- };
3147
+ return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
3148
+ }
3149
+ const fileContent = await this.readFileContent(diffFile);
3150
+ if (!fileContent) {
3151
+ return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
3067
3152
  }
3153
+ const addedLines = this.extractAddedLines(diffFile);
3154
+ const ruleResult = this.ruleEngine.runWithDiagnostics({
3155
+ filePath: diffFile.filePath,
3156
+ fileContent,
3157
+ addedLines
3158
+ });
3159
+ const issues = [...ruleResult.issues];
3160
+ issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
3161
+ issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
3162
+ issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
3163
+ return {
3164
+ issues,
3165
+ ruleFailures: ruleResult.ruleFailures,
3166
+ rulesExecuted: ruleResult.rulesExecuted,
3167
+ rulesFailed: ruleResult.rulesFailed,
3168
+ scanErrors: [],
3169
+ scanned: true
3170
+ };
3171
+ }
3172
+ createSkippedResult(diffFile, type, message) {
3173
+ return {
3174
+ issues: [],
3175
+ ruleFailures: [],
3176
+ rulesExecuted: 0,
3177
+ rulesFailed: 0,
3178
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3179
+ scanned: false
3180
+ };
3181
+ }
3182
+ createErrorResult(diffFile, type, message) {
3183
+ return {
3184
+ issues: [],
3185
+ ruleFailures: [],
3186
+ rulesExecuted: 0,
3187
+ rulesFailed: 0,
3188
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3189
+ scanned: false
3190
+ };
3191
+ }
3192
+ async readFileContent(diffFile) {
3068
3193
  const filePath = resolve2(diffFile.filePath);
3069
- let fileContent;
3070
3194
  try {
3071
- fileContent = await readFile(filePath, "utf-8");
3195
+ return await readFile(filePath, "utf-8");
3072
3196
  } catch {
3073
3197
  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;
3198
+ return content ?? null;
3091
3199
  }
3092
- const addedLines = diffFile.hunks.flatMap((hunk) => {
3200
+ }
3201
+ extractAddedLines(diffFile) {
3202
+ return diffFile.hunks.flatMap((hunk) => {
3093
3203
  const lines = hunk.content.split("\n");
3094
3204
  const result = [];
3095
3205
  let currentLine = hunk.newStart;
@@ -3104,32 +3214,15 @@ var ScanEngine = class {
3104
3214
  }
3105
3215
  return result;
3106
3216
  });
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, {
3217
+ }
3218
+ runStructureAnalysis(fileContent, filePath) {
3219
+ return analyzeStructure(fileContent, filePath, {
3114
3220
  maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3115
3221
  maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3116
3222
  maxFunctionLength: this.config.thresholds["max-function-length"],
3117
3223
  maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3118
3224
  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
- };
3225
+ }).issues;
3133
3226
  }
3134
3227
  async getScanCandidates(options) {
3135
3228
  const scanMode = this.getScanMode(options);
@@ -3170,7 +3263,7 @@ var ScanEngine = class {
3170
3263
  }
3171
3264
  return "changed";
3172
3265
  }
3173
- async getDiffFiles(options) {
3266
+ getDiffFiles(options) {
3174
3267
  if (options.staged) {
3175
3268
  return this.diffParser.getStagedFiles();
3176
3269
  }
@@ -3251,6 +3344,96 @@ var ScanEngine = class {
3251
3344
  const relativePath = relative(process.cwd(), absolutePath) || filePath;
3252
3345
  return relativePath.split(sep).join("/");
3253
3346
  }
3347
+ async loadBaseline(baselinePath) {
3348
+ if (!baselinePath) {
3349
+ return void 0;
3350
+ }
3351
+ const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
3352
+ const parsed = JSON.parse(baselineContent);
3353
+ const issues = this.parseBaselineIssues(parsed.issues);
3354
+ return {
3355
+ issues,
3356
+ fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
3357
+ commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
3358
+ timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
3359
+ };
3360
+ }
3361
+ parseBaselineIssues(input) {
3362
+ if (!Array.isArray(input)) {
3363
+ return [];
3364
+ }
3365
+ return input.flatMap((item) => {
3366
+ const issue = this.parseBaselineIssue(item);
3367
+ return issue ? [issue] : [];
3368
+ });
3369
+ }
3370
+ parseBaselineIssue(input) {
3371
+ if (!input || typeof input !== "object") {
3372
+ return void 0;
3373
+ }
3374
+ const issue = input;
3375
+ if (!this.isValidBaselineIssue(issue)) {
3376
+ return void 0;
3377
+ }
3378
+ return {
3379
+ ruleId: issue.ruleId,
3380
+ severity: issue.severity,
3381
+ category: issue.category,
3382
+ file: issue.file,
3383
+ startLine: issue.startLine,
3384
+ endLine: issue.endLine,
3385
+ message: issue.message,
3386
+ fingerprint: issue.fingerprint,
3387
+ fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
3388
+ };
3389
+ }
3390
+ isValidBaselineIssue(issue) {
3391
+ 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";
3392
+ }
3393
+ attachLifecycle(issues, baseline) {
3394
+ if (!baseline) {
3395
+ return issues;
3396
+ }
3397
+ return issues.map((issue) => ({
3398
+ ...issue,
3399
+ lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
3400
+ }));
3401
+ }
3402
+ getFixedIssues(issues, baseline) {
3403
+ if (!baseline) {
3404
+ return [];
3405
+ }
3406
+ const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
3407
+ return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
3408
+ }
3409
+ buildLifecycleSummary(issues, fixedIssues, baseline) {
3410
+ if (!baseline) {
3411
+ return void 0;
3412
+ }
3413
+ let newIssues = 0;
3414
+ let existingIssues = 0;
3415
+ for (const issue of issues) {
3416
+ if (issue.lifecycle === "existing") {
3417
+ existingIssues++;
3418
+ } else {
3419
+ newIssues++;
3420
+ }
3421
+ }
3422
+ return {
3423
+ newIssues,
3424
+ existingIssues,
3425
+ fixedIssues: fixedIssues.length,
3426
+ baselineUsed: true,
3427
+ baselineCommit: baseline.commit,
3428
+ baselineTimestamp: baseline.timestamp
3429
+ };
3430
+ }
3431
+ isSeverity(value) {
3432
+ return value === "high" || value === "medium" || value === "low" || value === "info";
3433
+ }
3434
+ isRuleCategory(value) {
3435
+ return ["security", "logic", "structure", "style", "coverage"].includes(value);
3436
+ }
3254
3437
  groupByDimension(issues) {
3255
3438
  const categories = [
3256
3439
  "security",