@gulu9527/code-trust 0.2.1 → 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
@@ -120,40 +120,72 @@ detection:
120
120
  }
121
121
 
122
122
  // src/core/engine.ts
123
+ import { createHash } from "crypto";
123
124
  import { readFile } from "fs/promises";
124
- import { readFileSync, existsSync as existsSync3 } from "fs";
125
- import { resolve as resolve2, dirname as dirname3 } from "path";
125
+ import { readFileSync } from "fs";
126
+ import { resolve as resolve2, dirname as dirname3, relative, sep } from "path";
126
127
  import { fileURLToPath } from "url";
127
128
 
128
129
  // src/parsers/diff.ts
129
130
  import simpleGit from "simple-git";
131
+ var GIT_DIFF_UNIFIED = "--unified=3";
132
+ var SHORT_HASH_LENGTH = 7;
130
133
  var DiffParser = class {
131
134
  git;
132
135
  constructor(workDir) {
133
136
  this.git = simpleGit(workDir);
134
137
  }
135
138
  async getStagedFiles() {
136
- const diffDetail = await this.git.diff(["--cached", "--unified=3"]);
139
+ const diffDetail = await this.git.diff(["--cached", GIT_DIFF_UNIFIED]);
137
140
  return this.parseDiffOutput(diffDetail);
138
141
  }
139
142
  async getDiffFromRef(ref) {
140
- const diffDetail = await this.git.diff([ref, "--unified=3"]);
143
+ const diffDetail = await this.git.diff([ref, GIT_DIFF_UNIFIED]);
141
144
  return this.parseDiffOutput(diffDetail);
142
145
  }
143
146
  async getChangedFiles() {
144
- const diffDetail = await this.git.diff(["--unified=3"]);
145
- const stagedDetail = await this.git.diff(["--cached", "--unified=3"]);
146
- const allDiff = diffDetail + "\n" + stagedDetail;
147
- 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());
148
180
  }
149
181
  async getLastCommitDiff() {
150
- const diffDetail = await this.git.diff(["HEAD~1", "HEAD", "--unified=3"]);
182
+ const diffDetail = await this.git.diff(["HEAD~1", "HEAD", GIT_DIFF_UNIFIED]);
151
183
  return this.parseDiffOutput(diffDetail);
152
184
  }
153
185
  async getCurrentCommitHash() {
154
186
  try {
155
187
  const hash = await this.git.revparse(["HEAD"]);
156
- return hash.trim().slice(0, 7);
188
+ return hash.trim().slice(0, SHORT_HASH_LENGTH);
157
189
  } catch {
158
190
  return void 0;
159
191
  }
@@ -589,7 +621,7 @@ function detectCodeAfterReturn(context, lines, issues) {
589
621
  }
590
622
  }
591
623
  if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
592
- const endsOpen = /[{(\[,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
624
+ const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
593
625
  if (endsOpen) continue;
594
626
  lastReturnDepth = braceDepth;
595
627
  lastReturnLine = i;
@@ -780,6 +812,9 @@ function analyzeFunctionNode(node) {
780
812
  function calculateCyclomaticComplexity(root) {
781
813
  let complexity = 1;
782
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
+ }
783
818
  switch (n.type) {
784
819
  case AST_NODE_TYPES.IfStatement:
785
820
  case AST_NODE_TYPES.ConditionalExpression:
@@ -808,6 +843,9 @@ function calculateCognitiveComplexity(root) {
808
843
  const depthMap = /* @__PURE__ */ new WeakMap();
809
844
  depthMap.set(root, 0);
810
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
+ }
811
849
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
812
850
  const isNesting = isNestingNode(n);
813
851
  const depth = isNesting ? parentDepth + 1 : parentDepth;
@@ -826,6 +864,9 @@ function calculateMaxNestingDepth(root) {
826
864
  const depthMap = /* @__PURE__ */ new WeakMap();
827
865
  depthMap.set(root, 0);
828
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
+ }
829
870
  const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
830
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;
831
872
  const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
@@ -1007,14 +1048,19 @@ function stringifyCondition(node) {
1007
1048
  case AST_NODE_TYPES.Literal:
1008
1049
  return String(node.value);
1009
1050
  case AST_NODE_TYPES.BinaryExpression:
1051
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
1010
1052
  case AST_NODE_TYPES.LogicalExpression:
1011
1053
  return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
1012
1054
  case AST_NODE_TYPES.UnaryExpression:
1013
1055
  return `${node.operator}${stringifyCondition(node.argument)}`;
1014
1056
  case AST_NODE_TYPES.MemberExpression:
1015
1057
  return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
1016
- case AST_NODE_TYPES.CallExpression:
1017
- 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)}`;
1018
1064
  default:
1019
1065
  return `[${node.type}]`;
1020
1066
  }
@@ -1024,6 +1070,10 @@ function truncate(s, maxLen) {
1024
1070
  }
1025
1071
 
1026
1072
  // src/rules/builtin/security.ts
1073
+ var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
1074
+ var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
1075
+ var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
1076
+ var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
1027
1077
  var securityRules = [
1028
1078
  {
1029
1079
  id: "security/hardcoded-secret",
@@ -1036,7 +1086,7 @@ var securityRules = [
1036
1086
  const lines = context.fileContent.split("\n");
1037
1087
  const secretPatterns = [
1038
1088
  // API keys / tokens
1039
- { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_\-]{16,}['"`]/i, label: "API key" },
1089
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
1040
1090
  { pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
1041
1091
  // AWS
1042
1092
  { pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
@@ -1089,15 +1139,17 @@ var securityRules = [
1089
1139
  const issues = [];
1090
1140
  const lines = context.fileContent.split("\n");
1091
1141
  for (let i = 0; i < lines.length; i++) {
1092
- const trimmed = lines[i].trim();
1093
- if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1142
+ const line = lines[i];
1143
+ const trimmed = line.trim();
1144
+ if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
1145
+ const sanitizedLine = stripQuotedStrings(line);
1094
1146
  const evalPatterns = [
1095
- { pattern: /\beval\s*\(/, label: "eval()" },
1096
- { pattern: /new\s+Function\s*\(/, label: "new Function()" },
1097
- { pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string" }
1147
+ { pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
1148
+ { pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
1149
+ { pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
1098
1150
  ];
1099
- for (const { pattern, label } of evalPatterns) {
1100
- if (pattern.test(lines[i])) {
1151
+ for (const { pattern, label, source } of evalPatterns) {
1152
+ if (pattern.test(source)) {
1101
1153
  issues.push({
1102
1154
  ruleId: "security/eval-usage",
1103
1155
  severity: "high",
@@ -1130,29 +1182,30 @@ var securityRules = [
1130
1182
  check(context) {
1131
1183
  const issues = [];
1132
1184
  const lines = context.fileContent.split("\n");
1185
+ const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
1133
1186
  for (let i = 0; i < lines.length; i++) {
1134
- const trimmed = lines[i].trim();
1135
- if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1136
- const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
1137
- if (sqlKeywords.test(lines[i])) {
1138
- if (/\$\{[^}]+\}/.test(lines[i]) || /['"]\s*\+\s*\w+/.test(lines[i])) {
1139
- issues.push({
1140
- ruleId: "security/sql-injection",
1141
- severity: "high",
1142
- category: "security",
1143
- file: context.filePath,
1144
- startLine: i + 1,
1145
- endLine: i + 1,
1146
- message: t(
1147
- "Potential SQL injection \u2014 string interpolation in SQL query.",
1148
- "\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
1149
- ),
1150
- suggestion: t(
1151
- "Use parameterized queries or prepared statements instead.",
1152
- "\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
1153
- )
1154
- });
1155
- }
1187
+ const line = lines[i];
1188
+ const trimmed = line.trim();
1189
+ if (isCommentLine(trimmed)) continue;
1190
+ const hasSqlKeyword = sqlKeywords.test(line);
1191
+ const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
1192
+ if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
1193
+ issues.push({
1194
+ ruleId: "security/sql-injection",
1195
+ severity: "high",
1196
+ category: "security",
1197
+ file: context.filePath,
1198
+ startLine: i + 1,
1199
+ endLine: i + 1,
1200
+ message: t(
1201
+ "Potential SQL injection \u2014 string interpolation in SQL query.",
1202
+ "\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
1203
+ ),
1204
+ suggestion: t(
1205
+ "Use parameterized queries or prepared statements instead.",
1206
+ "\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
1207
+ )
1208
+ });
1156
1209
  }
1157
1210
  }
1158
1211
  return issues;
@@ -1168,9 +1221,11 @@ var securityRules = [
1168
1221
  const issues = [];
1169
1222
  const lines = context.fileContent.split("\n");
1170
1223
  for (let i = 0; i < lines.length; i++) {
1171
- const trimmed = lines[i].trim();
1224
+ const line = lines[i];
1225
+ const trimmed = line.trim();
1172
1226
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1173
- 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)) {
1174
1229
  issues.push({
1175
1230
  ruleId: "security/dangerous-html",
1176
1231
  severity: "medium",
@@ -1698,6 +1753,9 @@ var missingAwaitRule = {
1698
1753
  const body = getFunctionBody(node);
1699
1754
  if (!body) return;
1700
1755
  walkAST(body, (inner, parent) => {
1756
+ if (inner.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1757
+ return;
1758
+ }
1701
1759
  if (inner !== body && isAsyncFunction(inner)) return false;
1702
1760
  if (inner.type !== AST_NODE_TYPES.CallExpression) return;
1703
1761
  if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
@@ -1707,6 +1765,9 @@ var missingAwaitRule = {
1707
1765
  if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
1708
1766
  if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
1709
1767
  if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
1768
+ if (parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1769
+ return;
1770
+ }
1710
1771
  const callName = getCallName(inner);
1711
1772
  if (!callName) return;
1712
1773
  if (!asyncFuncNames.has(callName)) return;
@@ -1918,9 +1979,28 @@ var typeCoercionRule = {
1918
1979
  var ALLOWED_NUMBERS = /* @__PURE__ */ new Set([
1919
1980
  -1,
1920
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,
1921
1991
  1,
1922
1992
  2,
1993
+ 3,
1994
+ 4,
1995
+ 5,
1923
1996
  10,
1997
+ 15,
1998
+ 20,
1999
+ 30,
2000
+ 40,
2001
+ 50,
2002
+ 70,
2003
+ 90,
1924
2004
  100
1925
2005
  ]);
1926
2006
  var magicNumberRule = {
@@ -1949,6 +2029,7 @@ var magicNumberRule = {
1949
2029
  if (/^\s*(export\s+)?enum\s/.test(line)) continue;
1950
2030
  if (trimmed.startsWith("import ")) continue;
1951
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;
1952
2033
  const cleaned = line.replace(/(['"`])(?:(?!\1|\\).|\\.)*\1/g, '""').replace(/\/\/.*$/, "");
1953
2034
  const numRegex = /(?<![.\w])(-?\d+\.?\d*(?:e[+-]?\d+)?)\b/gi;
1954
2035
  let match;
@@ -2045,6 +2126,17 @@ var nestedTernaryRule = {
2045
2126
  // src/rules/builtin/duplicate-string.ts
2046
2127
  var MIN_STRING_LENGTH = 6;
2047
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
+ ]);
2048
2140
  var duplicateStringRule = {
2049
2141
  id: "logic/duplicate-string",
2050
2142
  category: "logic",
@@ -2075,6 +2167,7 @@ var duplicateStringRule = {
2075
2167
  while ((match = stringRegex.exec(cleaned)) !== null) {
2076
2168
  const value = match[2];
2077
2169
  if (value.length < MIN_STRING_LENGTH) continue;
2170
+ if (IGNORED_LITERALS.has(value)) continue;
2078
2171
  if (value.includes("${")) continue;
2079
2172
  if (value.startsWith("http") || value.startsWith("/")) continue;
2080
2173
  if (value.startsWith("test") || value.startsWith("mock")) continue;
@@ -2344,13 +2437,28 @@ var promiseVoidRule = {
2344
2437
  /^save/,
2345
2438
  /^load/,
2346
2439
  /^send/,
2347
- /^delete/,
2348
2440
  /^update/,
2349
2441
  /^create/,
2350
2442
  /^connect/,
2351
2443
  /^disconnect/,
2352
2444
  /^init/
2353
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
+ ];
2354
2462
  walkAST(ast, (node) => {
2355
2463
  if (node.type !== AST_NODE_TYPES.ExpressionStatement) return;
2356
2464
  const expr = node.expression;
@@ -2361,6 +2469,7 @@ var promiseVoidRule = {
2361
2469
  const isKnownAsync = asyncFnNames.has(fnName);
2362
2470
  const matchesPattern = commonAsyncPatterns.some((p) => p.test(fnName));
2363
2471
  const endsWithAsync = fnName.endsWith("Async") || fnName.endsWith("async");
2472
+ if (syncMethods.includes(fnName)) return;
2364
2473
  if (!isKnownAsync && !matchesPattern && !endsWithAsync) return;
2365
2474
  const line = node.loc?.start.line ?? 0;
2366
2475
  if (line === 0) return;
@@ -2436,12 +2545,7 @@ var noReassignParamRule = {
2436
2545
  }
2437
2546
  }
2438
2547
  if (paramNames.size === 0) return;
2439
- let body = null;
2440
- if (node.type === AST_NODE_TYPES.MethodDefinition) {
2441
- body = node.value;
2442
- } else {
2443
- body = node;
2444
- }
2548
+ const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
2445
2549
  if (!body || !("body" in body)) return;
2446
2550
  const fnBody = body.body;
2447
2551
  if (!fnBody) return;
@@ -2678,15 +2782,33 @@ var RuleEngine = class {
2678
2782
  );
2679
2783
  }
2680
2784
  run(context) {
2785
+ return this.runWithDiagnostics(context).issues;
2786
+ }
2787
+ runWithDiagnostics(context) {
2681
2788
  const allIssues = [];
2789
+ const ruleFailures = [];
2790
+ let rulesExecuted = 0;
2791
+ let rulesFailed = 0;
2682
2792
  for (const rule of this.rules) {
2793
+ rulesExecuted++;
2683
2794
  try {
2684
2795
  const issues = rule.check(context);
2685
2796
  allIssues.push(...issues);
2686
- } catch (_err) {
2797
+ } catch (err) {
2798
+ rulesFailed++;
2799
+ ruleFailures.push({
2800
+ ruleId: rule.id,
2801
+ file: context.filePath,
2802
+ message: err instanceof Error ? err.message : "Unknown rule execution failure"
2803
+ });
2687
2804
  }
2688
2805
  }
2689
- return allIssues;
2806
+ return {
2807
+ issues: allIssues,
2808
+ rulesExecuted,
2809
+ rulesFailed,
2810
+ ruleFailures
2811
+ };
2690
2812
  }
2691
2813
  getRules() {
2692
2814
  return [...this.rules];
@@ -3066,6 +3188,8 @@ var PKG_VERSION = (() => {
3066
3188
  return "0.1.0";
3067
3189
  }
3068
3190
  })();
3191
+ var REPORT_SCHEMA_VERSION = "1.0.0";
3192
+ var FINGERPRINT_VERSION = "1";
3069
3193
  var ScanEngine = class {
3070
3194
  config;
3071
3195
  diffParser;
@@ -3076,81 +3200,191 @@ var ScanEngine = class {
3076
3200
  this.ruleEngine = new RuleEngine(config);
3077
3201
  }
3078
3202
  async scan(options) {
3079
- const diffFiles = await this.getDiffFiles(options);
3203
+ const selection = await this.getScanCandidates(options);
3080
3204
  const allIssues = [];
3081
- for (const diffFile of diffFiles) {
3082
- if (diffFile.status === "deleted") continue;
3083
- const filePath = resolve2(diffFile.filePath);
3084
- let fileContent;
3085
- try {
3086
- if (existsSync3(filePath)) {
3087
- fileContent = await readFile(filePath, "utf-8");
3088
- } else {
3089
- const content = await this.diffParser.getFileContent(diffFile.filePath);
3090
- if (!content) continue;
3091
- fileContent = content;
3092
- }
3093
- } catch {
3094
- continue;
3095
- }
3096
- const addedLines = diffFile.hunks.flatMap((hunk) => {
3097
- const lines = hunk.content.split("\n");
3098
- const result = [];
3099
- let currentLine = hunk.newStart;
3100
- for (const line of lines) {
3101
- if (line.startsWith("+")) {
3102
- result.push({ lineNumber: currentLine, content: line.slice(1) });
3103
- currentLine++;
3104
- } else if (line.startsWith("-")) {
3105
- } else {
3106
- currentLine++;
3107
- }
3108
- }
3109
- return result;
3110
- });
3111
- const issues = this.ruleEngine.run({
3112
- filePath: diffFile.filePath,
3113
- fileContent,
3114
- addedLines
3115
- });
3116
- allIssues.push(...issues);
3117
- if (this.isTsJsFile(diffFile.filePath)) {
3118
- const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
3119
- maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3120
- maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3121
- maxFunctionLength: this.config.thresholds["max-function-length"],
3122
- maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3123
- maxParamCount: this.config.thresholds["max-params"]
3124
- });
3125
- allIssues.push(...structureResult.issues);
3126
- const styleResult = analyzeStyle(fileContent, diffFile.filePath);
3127
- allIssues.push(...styleResult.issues);
3128
- const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
3129
- allIssues.push(...coverageResult.issues);
3205
+ const scanErrors = [];
3206
+ const ruleFailures = [];
3207
+ let rulesExecuted = 0;
3208
+ let rulesFailed = 0;
3209
+ let filesScanned = 0;
3210
+ const results = await Promise.all(
3211
+ selection.candidates.map((diffFile) => this.scanFile(diffFile))
3212
+ );
3213
+ for (const result of results) {
3214
+ allIssues.push(...result.issues);
3215
+ ruleFailures.push(...result.ruleFailures);
3216
+ scanErrors.push(...result.scanErrors);
3217
+ rulesExecuted += result.rulesExecuted;
3218
+ rulesFailed += result.rulesFailed;
3219
+ if (result.scanned) {
3220
+ filesScanned++;
3130
3221
  }
3131
3222
  }
3132
- const dimensions = this.groupByDimension(allIssues);
3223
+ const issuesWithFingerprints = this.attachFingerprints(allIssues);
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);
3133
3229
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
3134
3230
  const grade = getGrade(overallScore);
3135
3231
  const commitHash = await this.diffParser.getCurrentCommitHash();
3136
3232
  return {
3233
+ schemaVersion: REPORT_SCHEMA_VERSION,
3137
3234
  version: PKG_VERSION,
3138
3235
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3139
3236
  commit: commitHash,
3237
+ scanMode: selection.scanMode,
3140
3238
  overall: {
3141
3239
  score: overallScore,
3142
3240
  grade,
3143
- filesScanned: diffFiles.filter((f) => f.status !== "deleted").length,
3144
- issuesFound: allIssues.length
3241
+ filesScanned,
3242
+ issuesFound: issuesWithLifecycle.length
3243
+ },
3244
+ toolHealth: {
3245
+ rulesExecuted,
3246
+ rulesFailed,
3247
+ filesConsidered: selection.filesConsidered,
3248
+ filesScanned,
3249
+ filesExcluded: selection.filesExcluded,
3250
+ filesSkipped: scanErrors.length,
3251
+ scanErrors,
3252
+ ruleFailures
3145
3253
  },
3146
3254
  dimensions,
3147
- issues: allIssues.sort((a, b) => {
3255
+ issues: issuesWithLifecycle.sort((a, b) => {
3148
3256
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
3149
3257
  return severityOrder[a.severity] - severityOrder[b.severity];
3150
- })
3258
+ }),
3259
+ lifecycle,
3260
+ fixedIssues
3151
3261
  };
3152
3262
  }
3153
- async getDiffFiles(options) {
3263
+ async scanFile(diffFile) {
3264
+ if (diffFile.status === "deleted") {
3265
+ return this.createSkippedResult(diffFile, "deleted-file", `Skipped deleted file: ${diffFile.filePath}`);
3266
+ }
3267
+ if (!this.isTsJsFile(diffFile.filePath)) {
3268
+ return this.createSkippedResult(diffFile, "unsupported-file-type", `Skipped unsupported file type: ${diffFile.filePath}`);
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) {
3314
+ const filePath = resolve2(diffFile.filePath);
3315
+ try {
3316
+ return await readFile(filePath, "utf-8");
3317
+ } catch {
3318
+ const content = await this.diffParser.getFileContent(diffFile.filePath);
3319
+ return content ?? null;
3320
+ }
3321
+ }
3322
+ extractAddedLines(diffFile) {
3323
+ return diffFile.hunks.flatMap((hunk) => {
3324
+ const lines = hunk.content.split("\n");
3325
+ const result = [];
3326
+ let currentLine = hunk.newStart;
3327
+ for (const line of lines) {
3328
+ if (line.startsWith("+")) {
3329
+ result.push({ lineNumber: currentLine, content: line.slice(1) });
3330
+ currentLine++;
3331
+ } else if (line.startsWith("-")) {
3332
+ } else {
3333
+ currentLine++;
3334
+ }
3335
+ }
3336
+ return result;
3337
+ });
3338
+ }
3339
+ runStructureAnalysis(fileContent, filePath) {
3340
+ return analyzeStructure(fileContent, filePath, {
3341
+ maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3342
+ maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3343
+ maxFunctionLength: this.config.thresholds["max-function-length"],
3344
+ maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3345
+ maxParamCount: this.config.thresholds["max-params"]
3346
+ }).issues;
3347
+ }
3348
+ async getScanCandidates(options) {
3349
+ const scanMode = this.getScanMode(options);
3350
+ const candidates = await this.getDiffFiles(options);
3351
+ if (scanMode === "files") {
3352
+ return {
3353
+ scanMode,
3354
+ candidates,
3355
+ filesConsidered: options.files?.length ?? candidates.length,
3356
+ filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
3357
+ };
3358
+ }
3359
+ const filteredCandidates = [];
3360
+ let filesExcluded = 0;
3361
+ for (const candidate of candidates) {
3362
+ if (this.shouldIncludeFile(candidate.filePath)) {
3363
+ filteredCandidates.push(candidate);
3364
+ } else {
3365
+ filesExcluded++;
3366
+ }
3367
+ }
3368
+ return {
3369
+ scanMode,
3370
+ candidates: filteredCandidates,
3371
+ filesConsidered: candidates.length,
3372
+ filesExcluded
3373
+ };
3374
+ }
3375
+ getScanMode(options) {
3376
+ if (options.staged) {
3377
+ return "staged";
3378
+ }
3379
+ if (options.diff) {
3380
+ return "diff";
3381
+ }
3382
+ if (options.files && options.files.length > 0) {
3383
+ return "files";
3384
+ }
3385
+ return "changed";
3386
+ }
3387
+ getDiffFiles(options) {
3154
3388
  if (options.staged) {
3155
3389
  return this.diffParser.getStagedFiles();
3156
3390
  }
@@ -3158,14 +3392,10 @@ var ScanEngine = class {
3158
3392
  return this.diffParser.getDiffFromRef(options.diff);
3159
3393
  }
3160
3394
  if (options.files && options.files.length > 0) {
3395
+ const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
3161
3396
  return Promise.all(
3162
- options.files.map(async (filePath) => {
3163
- let content = "";
3164
- try {
3165
- content = await readFile(resolve2(filePath), "utf-8");
3166
- } catch {
3167
- }
3168
- return {
3397
+ includedFiles.map(
3398
+ (filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
3169
3399
  filePath,
3170
3400
  status: "modified",
3171
3401
  additions: content.split("\n").length,
@@ -3177,18 +3407,154 @@ var ScanEngine = class {
3177
3407
  oldLines: 0,
3178
3408
  newStart: 1,
3179
3409
  newLines: content.split("\n").length,
3180
- content: content.split("\n").map((l) => "+" + l).join("\n")
3410
+ content: content.split("\n").map((line) => "+" + line).join("\n")
3181
3411
  }
3182
3412
  ]
3183
- };
3184
- })
3413
+ }))
3414
+ )
3185
3415
  );
3186
3416
  }
3187
3417
  return this.diffParser.getChangedFiles();
3188
3418
  }
3419
+ shouldIncludeFile(filePath) {
3420
+ const normalizedPath = filePath.split(sep).join("/");
3421
+ const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
3422
+ const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
3423
+ if (!included) {
3424
+ return false;
3425
+ }
3426
+ return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
3427
+ }
3428
+ matchesPattern(filePath, pattern) {
3429
+ let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
3430
+ regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
3431
+ regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
3432
+ regexPattern = regexPattern.replace(/\*/g, "[^/]*");
3433
+ regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
3434
+ regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
3435
+ return new RegExp(`^${regexPattern}$`).test(filePath);
3436
+ }
3189
3437
  isTsJsFile(filePath) {
3190
3438
  return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
3191
3439
  }
3440
+ attachFingerprints(issues) {
3441
+ const occurrenceCounts = /* @__PURE__ */ new Map();
3442
+ return issues.map((issue) => {
3443
+ const normalizedFile = this.normalizeRelativePath(issue.file);
3444
+ const locationComponent = `${issue.startLine}:${issue.endLine}`;
3445
+ const baseKey = [
3446
+ issue.ruleId,
3447
+ normalizedFile,
3448
+ issue.category,
3449
+ issue.severity,
3450
+ locationComponent
3451
+ ].join("|");
3452
+ const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
3453
+ occurrenceCounts.set(baseKey, occurrenceIndex + 1);
3454
+ const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
3455
+ return {
3456
+ ...issue,
3457
+ file: normalizedFile,
3458
+ fingerprint,
3459
+ fingerprintVersion: FINGERPRINT_VERSION
3460
+ };
3461
+ });
3462
+ }
3463
+ normalizeRelativePath(filePath) {
3464
+ const absolutePath = resolve2(filePath);
3465
+ const relativePath = relative(process.cwd(), absolutePath) || filePath;
3466
+ return relativePath.split(sep).join("/");
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
+ }
3192
3558
  groupByDimension(issues) {
3193
3559
  const categories = [
3194
3560
  "security",
@@ -3199,7 +3565,7 @@ var ScanEngine = class {
3199
3565
  ];
3200
3566
  const grouped = {};
3201
3567
  for (const cat of categories) {
3202
- const catIssues = issues.filter((i) => i.category === cat);
3568
+ const catIssues = issues.filter((issue) => issue.category === cat);
3203
3569
  grouped[cat] = calculateDimensionScore(catIssues);
3204
3570
  }
3205
3571
  return grouped;
@@ -3219,7 +3585,13 @@ var en = {
3219
3585
  issuesFound: "{{count}} issue(s) found",
3220
3586
  issuesHeader: "Issues ({{count}}):",
3221
3587
  noIssuesFound: "No issues found! \u{1F389}",
3222
- scanned: "Scanned {{count}} file(s)"
3588
+ scanned: "Scanned {{count}} file(s)",
3589
+ healthHeader: "Tool Health",
3590
+ rulesFailed: "Failed rules: {{count}}",
3591
+ filesSkipped: "Skipped files: {{count}}",
3592
+ filesExcluded: "Excluded files: {{count}}",
3593
+ lifecycleHeader: "Lifecycle",
3594
+ lifecycleSummary: "New: {{new}} Existing: {{existing}} Fixed: {{fixed}}"
3223
3595
  };
3224
3596
  var zh = {
3225
3597
  reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
@@ -3231,7 +3603,13 @@ var zh = {
3231
3603
  issuesFound: "\u53D1\u73B0 {{count}} \u4E2A\u95EE\u9898",
3232
3604
  issuesHeader: "\u95EE\u9898\u5217\u8868 ({{count}}):",
3233
3605
  noIssuesFound: "\u672A\u53D1\u73B0\u95EE\u9898! \u{1F389}",
3234
- scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6"
3606
+ scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6",
3607
+ healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
3608
+ rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
3609
+ filesSkipped: "\u8DF3\u8FC7\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}}"
3235
3613
  };
3236
3614
  function renderTerminalReport(report) {
3237
3615
  const isZh = isZhLocale();
@@ -3273,19 +3651,37 @@ function renderTerminalReport(report) {
3273
3651
  };
3274
3652
  const dims = ["security", "logic", "structure", "style", "coverage"];
3275
3653
  for (const dim of dims) {
3276
- const d = report.dimensions[dim];
3277
- const dimEmoji = d.score >= 80 ? "\u2705" : d.score >= 60 ? "\u26A0\uFE0F" : "\u274C";
3278
- const color = getScoreColor(d.score);
3279
- const issueCount = d.issues.length;
3654
+ const dimension = report.dimensions[dim];
3655
+ const dimEmoji = dimension.score >= 80 ? "\u2705" : dimension.score >= 60 ? "\u26A0\uFE0F" : "\u274C";
3656
+ const color = getScoreColor(dimension.score);
3657
+ const issueCount = dimension.issues.length;
3280
3658
  const detail = issueCount === 0 ? pc.green(t2.noIssues) : t2.issuesFound.replace("{{count}}", String(issueCount));
3281
3659
  table.push([
3282
3660
  `${dimEmoji} ${dimLabels[dim]}`,
3283
- color(String(d.score)),
3661
+ color(String(dimension.score)),
3284
3662
  detail
3285
3663
  ]);
3286
3664
  }
3287
3665
  lines.push(table.toString());
3288
3666
  lines.push("");
3667
+ if (report.toolHealth.rulesFailed > 0 || report.toolHealth.filesSkipped > 0 || report.toolHealth.filesExcluded > 0) {
3668
+ lines.push(pc.bold(t2.healthHeader));
3669
+ if (report.toolHealth.rulesFailed > 0) {
3670
+ lines.push(pc.yellow(` ${t2.rulesFailed.replace("{{count}}", String(report.toolHealth.rulesFailed))}`));
3671
+ }
3672
+ if (report.toolHealth.filesSkipped > 0) {
3673
+ lines.push(pc.yellow(` ${t2.filesSkipped.replace("{{count}}", String(report.toolHealth.filesSkipped))}`));
3674
+ }
3675
+ if (report.toolHealth.filesExcluded > 0) {
3676
+ lines.push(pc.dim(` ${t2.filesExcluded.replace("{{count}}", String(report.toolHealth.filesExcluded))}`));
3677
+ }
3678
+ lines.push("");
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
+ }
3289
3685
  if (report.issues.length > 0) {
3290
3686
  lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
3291
3687
  lines.push("");
@@ -3349,12 +3745,25 @@ function getScoreColor(score) {
3349
3745
 
3350
3746
  // src/cli/output/json.ts
3351
3747
  function renderJsonReport(report) {
3352
- return JSON.stringify(report, null, 2);
3748
+ const payload = {
3749
+ schemaVersion: report.schemaVersion,
3750
+ version: report.version,
3751
+ timestamp: report.timestamp,
3752
+ commit: report.commit,
3753
+ scanMode: report.scanMode,
3754
+ overall: report.overall,
3755
+ toolHealth: report.toolHealth,
3756
+ dimensions: report.dimensions,
3757
+ issues: report.issues,
3758
+ lifecycle: report.lifecycle,
3759
+ fixedIssues: report.fixedIssues
3760
+ };
3761
+ return JSON.stringify(payload, null, 2);
3353
3762
  }
3354
3763
 
3355
3764
  // src/cli/commands/scan.ts
3356
3765
  function createScanCommand() {
3357
- const cmd = new Command("scan").description("Scan code changes for trust issues").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) => {
3358
3767
  try {
3359
3768
  const config = await loadConfig();
3360
3769
  const engine = new ScanEngine(config);
@@ -3363,7 +3772,8 @@ function createScanCommand() {
3363
3772
  diff: opts.diff,
3364
3773
  files: files.length > 0 ? files : void 0,
3365
3774
  format: opts.format,
3366
- minScore: parseInt(opts.minScore, 10)
3775
+ minScore: parseInt(opts.minScore, 10),
3776
+ baseline: opts.baseline
3367
3777
  };
3368
3778
  const report = await engine.scan(scanOptions);
3369
3779
  if (opts.format === "json") {
@@ -3389,7 +3799,7 @@ function createScanCommand() {
3389
3799
  // src/cli/commands/report.ts
3390
3800
  import { Command as Command2 } from "commander";
3391
3801
  function createReportCommand() {
3392
- const cmd = new Command2("report").description("Generate a trust report for recent changes").option("--json", "Output as JSON").option("--diff <ref>", "Diff against a git ref", "HEAD~1").action(async (opts) => {
3802
+ const cmd = new Command2("report").description("Render a report for a diff-based scan (transitional wrapper around scan)").option("--json", "Output as JSON").option("--diff <ref>", "Diff against a git ref for report presentation", "HEAD~1").action(async (opts) => {
3393
3803
  try {
3394
3804
  const config = await loadConfig();
3395
3805
  const engine = new ScanEngine(config);
@@ -3413,14 +3823,14 @@ function createReportCommand() {
3413
3823
 
3414
3824
  // src/cli/commands/init.ts
3415
3825
  import { writeFile } from "fs/promises";
3416
- import { existsSync as existsSync4 } from "fs";
3826
+ import { existsSync as existsSync3 } from "fs";
3417
3827
  import { resolve as resolve3 } from "path";
3418
3828
  import { Command as Command3 } from "commander";
3419
3829
  import pc2 from "picocolors";
3420
3830
  function createInitCommand() {
3421
3831
  const cmd = new Command3("init").description("Initialize CodeTrust configuration file").action(async () => {
3422
3832
  const configPath = resolve3(".codetrust.yml");
3423
- if (existsSync4(configPath)) {
3833
+ if (existsSync3(configPath)) {
3424
3834
  console.log(pc2.yellow("\u26A0\uFE0F .codetrust.yml already exists. Skipping."));
3425
3835
  return;
3426
3836
  }
@@ -3494,7 +3904,7 @@ function formatSeverity2(severity) {
3494
3904
 
3495
3905
  // src/cli/commands/hook.ts
3496
3906
  import { writeFile as writeFile2, chmod, mkdir } from "fs/promises";
3497
- import { existsSync as existsSync5 } from "fs";
3907
+ import { existsSync as existsSync4 } from "fs";
3498
3908
  import { resolve as resolve4, join as join2 } from "path";
3499
3909
  import { Command as Command5 } from "commander";
3500
3910
  import pc4 from "picocolors";
@@ -3511,17 +3921,17 @@ function createHookCommand() {
3511
3921
  const cmd = new Command5("hook").description("Manage git hooks");
3512
3922
  cmd.command("install").description("Install pre-commit hook").action(async () => {
3513
3923
  const gitDir = resolve4(".git");
3514
- if (!existsSync5(gitDir)) {
3924
+ if (!existsSync4(gitDir)) {
3515
3925
  console.error(pc4.red("Error: Not a git repository."));
3516
3926
  process.exit(1);
3517
3927
  }
3518
3928
  const hooksDir = join2(gitDir, "hooks");
3519
3929
  const hookPath = join2(hooksDir, "pre-commit");
3520
3930
  try {
3521
- if (!existsSync5(hooksDir)) {
3931
+ if (!existsSync4(hooksDir)) {
3522
3932
  await mkdir(hooksDir, { recursive: true });
3523
3933
  }
3524
- if (existsSync5(hookPath)) {
3934
+ if (existsSync4(hookPath)) {
3525
3935
  console.log(pc4.yellow("\u26A0\uFE0F pre-commit hook already exists. Skipping."));
3526
3936
  console.log(pc4.dim(" Remove .git/hooks/pre-commit to reinstall."));
3527
3937
  return;