@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/cli/index.js CHANGED
@@ -128,33 +128,64 @@ import { fileURLToPath } from "url";
128
128
 
129
129
  // src/parsers/diff.ts
130
130
  import simpleGit from "simple-git";
131
+ var GIT_DIFF_UNIFIED = "--unified=3";
132
+ var SHORT_HASH_LENGTH = 7;
131
133
  var DiffParser = class {
132
134
  git;
133
135
  constructor(workDir) {
134
136
  this.git = simpleGit(workDir);
135
137
  }
136
138
  async getStagedFiles() {
137
- const diffDetail = await this.git.diff(["--cached", "--unified=3"]);
139
+ const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
138
140
  return this.parseDiffOutput(diffDetail);
139
141
  }
140
142
  async getDiffFromRef(ref) {
141
- const diffDetail = await this.git.diff([ref, "--unified=3"]);
143
+ const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
142
144
  return this.parseDiffOutput(diffDetail);
143
145
  }
144
146
  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);
147
+ const diffDetail = await this.git.diff([GIT_DIFF_UNIFIED]);
148
+ const stagedDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
149
+ const unstagedFiles = this.parseDiffOutput(diffDetail);
150
+ const stagedFiles = this.parseDiffOutput(stagedDetail);
151
+ return this.mergeDiffFiles(unstagedFiles, stagedFiles);
152
+ }
153
+ /**
154
+ * Merge two sets of diff files, deduplicating by file path.
155
+ * When a file appears in both, merge their hunks and combine stats.
156
+ */
157
+ mergeDiffFiles(unstaged, staged) {
158
+ const fileMap = /* @__PURE__ */ new Map();
159
+ for (const file of unstaged) {
160
+ fileMap.set(file.filePath, file);
161
+ }
162
+ for (const file of staged) {
163
+ const existing = fileMap.get(file.filePath);
164
+ if (existing) {
165
+ fileMap.set(file.filePath, {
166
+ ...existing,
167
+ // Combine additions/deletions
168
+ additions: existing.additions + file.additions,
169
+ deletions: existing.deletions + file.deletions,
170
+ // Merge hunks (preserve order: staged first, then unstaged)
171
+ hunks: [...file.hunks, ...existing.hunks],
172
+ // Status: if either is 'added', treat as added; otherwise keep modified
173
+ status: existing.status === "added" || file.status === "added" ? "added" : "modified"
174
+ });
175
+ } else {
176
+ fileMap.set(file.filePath, file);
177
+ }
178
+ }
179
+ return Array.from(fileMap.values());
149
180
  }
150
181
  async getLastCommitDiff() {
151
- const diffDetail = await this.git.diff(["HEAD~1", "HEAD", "--unified=3"]);
182
+ const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
152
183
  return this.parseDiffOutput(diffDetail);
153
184
  }
154
185
  async getCurrentCommitHash() {
155
186
  try {
156
187
  const hash = await this.git.revparse(["HEAD"]);
157
- return hash.trim().slice(0, 7);
188
+ return hash.trim().slice(0, SHORT_HASH_LENGTH);
158
189
  } catch {
159
190
  return void 0;
160
191
  }
@@ -781,6 +812,9 @@ function analyzeFunctionNode(node) {
781
812
  function calculateCyclomaticComplexity(root) {
782
813
  let complexity = 1;
783
814
  walkAST(root, (n) => {
815
+ 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) {
816
+ return false;
817
+ }
784
818
  switch (n.type) {
785
819
  case AST_NODE_TYPES.IfStatement:
786
820
  case AST_NODE_TYPES.ConditionalExpression:
@@ -809,6 +843,9 @@ function calculateCognitiveComplexity(root) {
809
843
  const depthMap = /* @__PURE__ */ new WeakMap();
810
844
  depthMap.set(root, 0);
811
845
  walkAST(root, (n, parent) => {
846
+ 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) {
847
+ return false;
848
+ }
812
849
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
813
850
  const isNesting = isNestingNode(n);
814
851
  const depth = isNesting ? parentDepth + 1 : parentDepth;
@@ -827,6 +864,9 @@ function calculateMaxNestingDepth(root) {
827
864
  const depthMap = /* @__PURE__ */ new WeakMap();
828
865
  depthMap.set(root, 0);
829
866
  walkAST(root, (n, parent) => {
867
+ 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) {
868
+ return false;
869
+ }
830
870
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
831
871
  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
872
  const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
@@ -1008,14 +1048,19 @@ function stringifyCondition(node) {
1008
1048
  case AST_NODE_TYPES.Literal:
1009
1049
  return String(node.value);
1010
1050
  case AST_NODE_TYPES.BinaryExpression:
1051
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
1011
1052
  case AST_NODE_TYPES.LogicalExpression:
1012
1053
  return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
1013
1054
  case AST_NODE_TYPES.UnaryExpression:
1014
1055
  return `${node.operator}${stringifyCondition(node.argument)}`;
1015
1056
  case AST_NODE_TYPES.MemberExpression:
1016
1057
  return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
1017
- case AST_NODE_TYPES.CallExpression:
1018
- return `${stringifyCondition(node.callee)}(...)`;
1058
+ case AST_NODE_TYPES.CallExpression: {
1059
+ const args = node.arguments.map((arg) => stringifyCondition(arg)).join(", ");
1060
+ return `${stringifyCondition(node.callee)}(${args})`;
1061
+ }
1062
+ case AST_NODE_TYPES.ConditionalExpression:
1063
+ return `${stringifyCondition(node.test)} ? ${stringifyCondition(node.consequent)} : ${stringifyCondition(node.alternate)}`;
1019
1064
  default:
1020
1065
  return `[${node.type}]`;
1021
1066
  }
@@ -1176,9 +1221,11 @@ var securityRules = [
1176
1221
  const issues = [];
1177
1222
  const lines = context.fileContent.split("\n");
1178
1223
  for (let i = 0; i < lines.length; i++) {
1179
- const trimmed = lines[i].trim();
1224
+ const line = lines[i];
1225
+ const trimmed = line.trim();
1180
1226
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1181
- if (/\.(innerHTML|outerHTML)\s*=/.test(lines[i]) || /dangerouslySetInnerHTML/.test(lines[i])) {
1227
+ const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "").replace(/\/[^/]+\/[dgimsuvy]*/g, '""');
1228
+ if (/\.(innerHTML|outerHTML)\s*=/.test(cleaned) || /dangerouslySetInnerHTML/.test(cleaned)) {
1182
1229
  issues.push({
1183
1230
  ruleId: "security/dangerous-html",
1184
1231
  severity: "medium",
@@ -1706,6 +1753,9 @@ var missingAwaitRule = {
1706
1753
  const body = getFunctionBody(node);
1707
1754
  if (!body) return;
1708
1755
  walkAST(body, (inner, parent) => {
1756
+ if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1757
+ return;
1758
+ }
1709
1759
  if (inner !== body && isAsyncFunction(inner)) return false;
1710
1760
  if (inner.type !== AST_NODE_TYPES.CallExpression) return;
1711
1761
  if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
@@ -1715,6 +1765,9 @@ var missingAwaitRule = {
1715
1765
  if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
1716
1766
  if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
1717
1767
  if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
1768
+ if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1769
+ return;
1770
+ }
1718
1771
  const callName = getCallName(inner);
1719
1772
  if (!callName) return;
1720
1773
  if (!asyncFuncNames.has(callName)) return;
@@ -1926,9 +1979,28 @@ var typeCoercionRule = {
1926
1979
  var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
1927
1980
  -1,
1928
1981
  0,
1982
+ 0.1,
1983
+ 0.1,
1984
+ 0.15,
1985
+ 0.2,
1986
+ 0.2,
1987
+ 0.25,
1988
+ 0.3,
1989
+ 0.3,
1990
+ 0.5,
1929
1991
  1,
1930
1992
  2,
1993
+ 3,
1994
+ 4,
1995
+ 5,
1931
1996
  10,
1997
+ 15,
1998
+ 20,
1999
+ 30,
2000
+ 40,
2001
+ 50,
2002
+ 70,
2003
+ 90,
1932
2004
  100
1933
2005
  ]);
1934
2006
  var magicNumberRule = {
@@ -1957,6 +2029,7 @@ var magicNumberRule = {
1957
2029
  if (/^\s*(export\s+)?enum\s/.test(line)) continue;
1958
2030
  if (trimmed.startsWith("import ")) continue;
1959
2031
  if (/^\s*return\s+[0-9]+\s*;?\s*$/.test(line)) continue;
2032
+ if (/^\s*['"]?[-\w]+['"]?\s*:\s*-?\d+\.?\d*(?:e[+-]?\d+)?\s*,?\s*$/.test(trimmed)) continue;
1960
2033
  const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
1961
2034
  const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
1962
2035
  let match;
@@ -2053,6 +2126,17 @@ var nestedTernaryRule = {
2053
2126
  // src/rules/builtin/duplicate-string.ts
2054
2127
  var MIN_STRING_LENGTH = 6;
2055
2128
  var MIN_OCCURRENCES = 3;
2129
+ var IGNORED_LITERALS = /* @__PURE__ */ new Set([
2130
+ "high",
2131
+ "medium",
2132
+ "low",
2133
+ "info",
2134
+ "logic",
2135
+ "security",
2136
+ "structure",
2137
+ "style",
2138
+ "coverage"
2139
+ ]);
2056
2140
  var duplicateStringRule = {
2057
2141
  id: "logic/duplicate-string",
2058
2142
  category: "logic",
@@ -2083,6 +2167,7 @@ var duplicateStringRule = {
2083
2167
  while ((match = stringRegex.exec(cleaned)) !== null) {
2084
2168
  const value = match[2];
2085
2169
  if (value.length < MIN_STRING_LENGTH) continue;
2170
+ if (IGNORED_LITERALS.has(value)) continue;
2086
2171
  if (value.includes("${")) continue;
2087
2172
  if (value.startsWith("http") || value.startsWith("/")) continue;
2088
2173
  if (value.startsWith("test") || value.startsWith("mock")) continue;
@@ -2352,13 +2437,28 @@ var promiseVoidRule = {
2352
2437
  /^save/,
2353
2438
  /^load/,
2354
2439
  /^send/,
2355
- /^delete/,
2356
2440
  /^update/,
2357
2441
  /^create/,
2358
2442
  /^connect/,
2359
2443
  /^disconnect/,
2360
2444
  /^init/
2361
2445
  ];
2446
+ const syncMethods = [
2447
+ "delete",
2448
+ // Map.delete(), Set.delete(), Object.delete() are synchronous
2449
+ "has",
2450
+ // Map.has(), Set.has() are synchronous
2451
+ "get",
2452
+ // Map.get() is synchronous
2453
+ "set",
2454
+ // Map.set() is synchronous (though some consider it potentially async)
2455
+ "keys",
2456
+ // Object.keys() is synchronous
2457
+ "values",
2458
+ // Object.values() is synchronous
2459
+ "entries"
2460
+ // Object.entries() is synchronous
2461
+ ];
2362
2462
  walkAST(ast, (node) => {
2363
2463
  if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
2364
2464
  const expr = node.expression;
@@ -2369,6 +2469,7 @@ var promiseVoidRule = {
2369
2469
  const isKnownAsync = asyncFnNames.has(fnName);
2370
2470
  const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
2371
2471
  const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
2472
+ if (syncMethods.includes(fnName)) return;
2372
2473
  if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
2373
2474
  const line = node.loc?.start.line ?? 0;
2374
2475
  if (line === 0) return;
@@ -3120,7 +3221,11 @@ var ScanEngine = class {
3120
3221
  }
3121
3222
  }
3122
3223
  const issuesWithFingerprints = this.attachFingerprints(allIssues);
3123
- const dimensions = this.groupByDimension(issuesWithFingerprints);
3224
+ const baseline = await this.loadBaseline(options.baseline);
3225
+ const issuesWithLifecycle = this.attachLifecycle(issuesWithFingerprints, baseline);
3226
+ const fixedIssues = this.getFixedIssues(issuesWithLifecycle, baseline);
3227
+ const lifecycle = this.buildLifecycleSummary(issuesWithLifecycle, fixedIssues, baseline);
3228
+ const dimensions = this.groupByDimension(issuesWithLifecycle);
3124
3229
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
3125
3230
  const grade = getGrade(overallScore);
3126
3231
  const commitHash = await this.diffParser.getCurrentCommitHash();
@@ -3134,7 +3239,7 @@ var ScanEngine = class {
3134
3239
  score: overallScore,
3135
3240
  grade,
3136
3241
  filesScanned,
3137
- issuesFound: issuesWithFingerprints.length
3242
+ issuesFound: issuesWithLifecycle.length
3138
3243
  },
3139
3244
  toolHealth: {
3140
3245
  rulesExecuted,
@@ -3147,70 +3252,75 @@ var ScanEngine = class {
3147
3252
  ruleFailures
3148
3253
  },
3149
3254
  dimensions,
3150
- issues: issuesWithFingerprints.sort((a, b) => {
3255
+ issues: issuesWithLifecycle.sort((a, b) => {
3151
3256
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
3152
3257
  return severityOrder[a.severity] - severityOrder[b.severity];
3153
- })
3258
+ }),
3259
+ lifecycle,
3260
+ fixedIssues
3154
3261
  };
3155
3262
  }
3156
3263
  async scanFile(diffFile) {
3157
3264
  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
- };
3265
+ return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
3172
3266
  }
3173
3267
  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
- };
3268
+ return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
3188
3269
  }
3270
+ const fileContent = await this.readFileContent(diffFile);
3271
+ if (!fileContent) {
3272
+ return this.createErrorResult(diffFile, "missing-file-content", `Unable to read file content for ${diffFile.filePath}`);
3273
+ }
3274
+ const addedLines = this.extractAddedLines(diffFile);
3275
+ const ruleResult = this.ruleEngine.runWithDiagnostics({
3276
+ filePath: diffFile.filePath,
3277
+ fileContent,
3278
+ addedLines
3279
+ });
3280
+ const issues = [...ruleResult.issues];
3281
+ issues.push(...this.runStructureAnalysis(fileContent, diffFile.filePath));
3282
+ issues.push(...analyzeStyle(fileContent, diffFile.filePath).issues);
3283
+ issues.push(...analyzeCoverage(fileContent, diffFile.filePath).issues);
3284
+ return {
3285
+ issues,
3286
+ ruleFailures: ruleResult.ruleFailures,
3287
+ rulesExecuted: ruleResult.rulesExecuted,
3288
+ rulesFailed: ruleResult.rulesFailed,
3289
+ scanErrors: [],
3290
+ scanned: true
3291
+ };
3292
+ }
3293
+ createSkippedResult(diffFile, type, message) {
3294
+ return {
3295
+ issues: [],
3296
+ ruleFailures: [],
3297
+ rulesExecuted: 0,
3298
+ rulesFailed: 0,
3299
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3300
+ scanned: false
3301
+ };
3302
+ }
3303
+ createErrorResult(diffFile, type, message) {
3304
+ return {
3305
+ issues: [],
3306
+ ruleFailures: [],
3307
+ rulesExecuted: 0,
3308
+ rulesFailed: 0,
3309
+ scanErrors: [{ type, file: diffFile.filePath, message }],
3310
+ scanned: false
3311
+ };
3312
+ }
3313
+ async readFileContent(diffFile) {
3189
3314
  const filePath = resolve2(diffFile.filePath);
3190
- let fileContent;
3191
3315
  try {
3192
- fileContent = await readFile(filePath, "utf-8");
3316
+ return await readFile(filePath, "utf-8");
3193
3317
  } catch {
3194
3318
  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;
3319
+ return content ?? null;
3212
3320
  }
3213
- const addedLines = diffFile.hunks.flatMap((hunk) => {
3321
+ }
3322
+ extractAddedLines(diffFile) {
3323
+ return diffFile.hunks.flatMap((hunk) => {
3214
3324
  const lines = hunk.content.split("\n");
3215
3325
  const result = [];
3216
3326
  let currentLine = hunk.newStart;
@@ -3225,32 +3335,15 @@ var ScanEngine = class {
3225
3335
  }
3226
3336
  return result;
3227
3337
  });
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, {
3338
+ }
3339
+ runStructureAnalysis(fileContent, filePath) {
3340
+ return analyzeStructure(fileContent, filePath, {
3235
3341
  maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3236
3342
  maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3237
3343
  maxFunctionLength: this.config.thresholds["max-function-length"],
3238
3344
  maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3239
3345
  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
- };
3346
+ }).issues;
3254
3347
  }
3255
3348
  async getScanCandidates(options) {
3256
3349
  const scanMode = this.getScanMode(options);
@@ -3291,7 +3384,7 @@ var ScanEngine = class {
3291
3384
  }
3292
3385
  return "changed";
3293
3386
  }
3294
- async getDiffFiles(options) {
3387
+ getDiffFiles(options) {
3295
3388
  if (options.staged) {
3296
3389
  return this.diffParser.getStagedFiles();
3297
3390
  }
@@ -3372,6 +3465,96 @@ var ScanEngine = class {
3372
3465
  const relativePath = relative(process.cwd(), absolutePath) || filePath;
3373
3466
  return relativePath.split(sep).join("/");
3374
3467
  }
3468
+ async loadBaseline(baselinePath) {
3469
+ if (!baselinePath) {
3470
+ return void 0;
3471
+ }
3472
+ const baselineContent = await readFile(resolve2(baselinePath), "utf-8");
3473
+ const parsed = JSON.parse(baselineContent);
3474
+ const issues = this.parseBaselineIssues(parsed.issues);
3475
+ return {
3476
+ issues,
3477
+ fingerprintSet: new Set(issues.map((issue) => issue.fingerprint)),
3478
+ commit: typeof parsed.commit === "string" ? parsed.commit : void 0,
3479
+ timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
3480
+ };
3481
+ }
3482
+ parseBaselineIssues(input) {
3483
+ if (!Array.isArray(input)) {
3484
+ return [];
3485
+ }
3486
+ return input.flatMap((item) => {
3487
+ const issue = this.parseBaselineIssue(item);
3488
+ return issue ? [issue] : [];
3489
+ });
3490
+ }
3491
+ parseBaselineIssue(input) {
3492
+ if (!input || typeof input !== "object") {
3493
+ return void 0;
3494
+ }
3495
+ const issue = input;
3496
+ if (!this.isValidBaselineIssue(issue)) {
3497
+ return void 0;
3498
+ }
3499
+ return {
3500
+ ruleId: issue.ruleId,
3501
+ severity: issue.severity,
3502
+ category: issue.category,
3503
+ file: issue.file,
3504
+ startLine: issue.startLine,
3505
+ endLine: issue.endLine,
3506
+ message: issue.message,
3507
+ fingerprint: issue.fingerprint,
3508
+ fingerprintVersion: typeof issue.fingerprintVersion === "string" ? issue.fingerprintVersion : void 0
3509
+ };
3510
+ }
3511
+ isValidBaselineIssue(issue) {
3512
+ 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";
3513
+ }
3514
+ attachLifecycle(issues, baseline) {
3515
+ if (!baseline) {
3516
+ return issues;
3517
+ }
3518
+ return issues.map((issue) => ({
3519
+ ...issue,
3520
+ lifecycle: baseline.fingerprintSet.has(issue.fingerprint) ? "existing" : "new"
3521
+ }));
3522
+ }
3523
+ getFixedIssues(issues, baseline) {
3524
+ if (!baseline) {
3525
+ return [];
3526
+ }
3527
+ const currentFingerprints = new Set(issues.map((issue) => issue.fingerprint));
3528
+ return baseline.issues.filter((issue) => !currentFingerprints.has(issue.fingerprint));
3529
+ }
3530
+ buildLifecycleSummary(issues, fixedIssues, baseline) {
3531
+ if (!baseline) {
3532
+ return void 0;
3533
+ }
3534
+ let newIssues = 0;
3535
+ let existingIssues = 0;
3536
+ for (const issue of issues) {
3537
+ if (issue.lifecycle === "existing") {
3538
+ existingIssues++;
3539
+ } else {
3540
+ newIssues++;
3541
+ }
3542
+ }
3543
+ return {
3544
+ newIssues,
3545
+ existingIssues,
3546
+ fixedIssues: fixedIssues.length,
3547
+ baselineUsed: true,
3548
+ baselineCommit: baseline.commit,
3549
+ baselineTimestamp: baseline.timestamp
3550
+ };
3551
+ }
3552
+ isSeverity(value) {
3553
+ return value === "high" || value === "medium" || value === "low" || value === "info";
3554
+ }
3555
+ isRuleCategory(value) {
3556
+ return ["security", "logic", "structure", "style", "coverage"].includes(value);
3557
+ }
3375
3558
  groupByDimension(issues) {
3376
3559
  const categories = [
3377
3560
  "security",
@@ -3406,7 +3589,9 @@ var en = {
3406
3589
  healthHeader: "Tool Health",
3407
3590
  rulesFailed: "Failed rules: {{count}}",
3408
3591
  filesSkipped: "Skipped files: {{count}}",
3409
- filesExcluded: "Excluded files: {{count}}"
3592
+ filesExcluded: "Excluded files: {{count}}",
3593
+ lifecycleHeader: "Lifecycle",
3594
+ lifecycleSummary: "New: {{new}} Existing: {{existing}} Fixed: {{fixed}}"
3410
3595
  };
3411
3596
  var zh = {
3412
3597
  reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
@@ -3422,7 +3607,9 @@ var zh = {
3422
3607
  healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
3423
3608
  rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
3424
3609
  filesSkipped: "\u8DF3\u8FC7\u6587\u4EF6\u6570\uFF1A{{count}}",
3425
- filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}"
3610
+ filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}",
3611
+ lifecycleHeader: "\u751F\u547D\u5468\u671F",
3612
+ lifecycleSummary: "\u65B0\u589E\uFF1A{{new}} \u5DF2\u5B58\u5728\uFF1A{{existing}} \u5DF2\u4FEE\u590D\uFF1A{{fixed}}"
3426
3613
  };
3427
3614
  function renderTerminalReport(report) {
3428
3615
  const isZh = isZhLocale();
@@ -3490,6 +3677,11 @@ function renderTerminalReport(report) {
3490
3677
  }
3491
3678
  lines.push("");
3492
3679
  }
3680
+ if (report.lifecycle) {
3681
+ lines.push(pc.bold(t2.lifecycleHeader));
3682
+ lines.push(` ${t2.lifecycleSummary.replace("{{new}}", String(report.lifecycle.newIssues)).replace("{{existing}}", String(report.lifecycle.existingIssues)).replace("{{fixed}}", String(report.lifecycle.fixedIssues))}`);
3683
+ lines.push("");
3684
+ }
3493
3685
  if (report.issues.length > 0) {
3494
3686
  lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
3495
3687
  lines.push("");
@@ -3562,14 +3754,16 @@ function renderJsonReport(report) {
3562
3754
  overall: report.overall,
3563
3755
  toolHealth: report.toolHealth,
3564
3756
  dimensions: report.dimensions,
3565
- issues: report.issues
3757
+ issues: report.issues,
3758
+ lifecycle: report.lifecycle,
3759
+ fixedIssues: report.fixedIssues
3566
3760
  };
3567
3761
  return JSON.stringify(payload, null, 2);
3568
3762
  }
3569
3763
 
3570
3764
  // src/cli/commands/scan.ts
3571
3765
  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) => {
3766
+ 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
3767
  try {
3574
3768
  const config = await loadConfig();
3575
3769
  const engine = new ScanEngine(config);
@@ -3578,7 +3772,8 @@ function createScanCommand() {
3578
3772
  diff: opts.diff,
3579
3773
  files: files.length > 0 ? files : void 0,
3580
3774
  format: opts.format,
3581
- minScore: parseInt(opts.minScore, 10)
3775
+ minScore: parseInt(opts.minScore, 10),
3776
+ baseline: opts.baseline
3582
3777
  };
3583
3778
  const report = await engine.scan(scanOptions);
3584
3779
  if (opts.format === "json") {