@gulu9527/code-trust 0.2.1 → 0.3.0

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,9 +120,10 @@ 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
@@ -589,7 +590,7 @@ function detectCodeAfterReturn(context, lines, issues) {
589
590
  }
590
591
  }
591
592
  if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
592
- const endsOpen = /[{(\[,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
593
+ const endsOpen = /[[{(,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
593
594
  if (endsOpen) continue;
594
595
  lastReturnDepth = braceDepth;
595
596
  lastReturnLine = i;
@@ -1024,6 +1025,10 @@ function truncate(s, maxLen) {
1024
1025
  }
1025
1026
 
1026
1027
  // src/rules/builtin/security.ts
1028
+ var isCommentLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("*");
1029
+ var stripQuotedStrings = (line) => line.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, "''").replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, '""').replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, "``");
1030
+ var isPatternDefinitionLine = (line) => /\bpattern\s*:\s*\/.*\/[dgimsuvy]*/.test(line);
1031
+ var hasQueryContext = (line) => /\b(query|sql|statement|stmt)\b/i.test(line) || /\.(query|execute|run|prepare)\s*\(/i.test(line);
1027
1032
  var securityRules = [
1028
1033
  {
1029
1034
  id: "security/hardcoded-secret",
@@ -1036,7 +1041,7 @@ var securityRules = [
1036
1041
  const lines = context.fileContent.split("\n");
1037
1042
  const secretPatterns = [
1038
1043
  // API keys / tokens
1039
- { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_\-]{16,}['"`]/i, label: "API key" },
1044
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_-]{16,}['"`]/i, label: "API key" },
1040
1045
  { pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
1041
1046
  // AWS
1042
1047
  { pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
@@ -1089,15 +1094,17 @@ var securityRules = [
1089
1094
  const issues = [];
1090
1095
  const lines = context.fileContent.split("\n");
1091
1096
  for (let i = 0; i < lines.length; i++) {
1092
- const trimmed = lines[i].trim();
1093
- if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1097
+ const line = lines[i];
1098
+ const trimmed = line.trim();
1099
+ if (isCommentLine(trimmed) || isPatternDefinitionLine(line)) continue;
1100
+ const sanitizedLine = stripQuotedStrings(line);
1094
1101
  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" }
1102
+ { pattern: /\beval\s*\(/, label: "eval()", source: sanitizedLine },
1103
+ { pattern: /new\s+Function\s*\(/, label: "new Function()", source: sanitizedLine },
1104
+ { pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string", source: line }
1098
1105
  ];
1099
- for (const { pattern, label } of evalPatterns) {
1100
- if (pattern.test(lines[i])) {
1106
+ for (const { pattern, label, source } of evalPatterns) {
1107
+ if (pattern.test(source)) {
1101
1108
  issues.push({
1102
1109
  ruleId: "security/eval-usage",
1103
1110
  severity: "high",
@@ -1130,29 +1137,30 @@ var securityRules = [
1130
1137
  check(context) {
1131
1138
  const issues = [];
1132
1139
  const lines = context.fileContent.split("\n");
1140
+ const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
1133
1141
  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
- }
1142
+ const line = lines[i];
1143
+ const trimmed = line.trim();
1144
+ if (isCommentLine(trimmed)) continue;
1145
+ const hasSqlKeyword = sqlKeywords.test(line);
1146
+ const hasInterpolation = /\$\{[^}]+\}/.test(line) || /['"]\s*\+\s*\w+/.test(line);
1147
+ if (hasSqlKeyword && hasInterpolation && hasQueryContext(line)) {
1148
+ issues.push({
1149
+ ruleId: "security/sql-injection",
1150
+ severity: "high",
1151
+ category: "security",
1152
+ file: context.filePath,
1153
+ startLine: i + 1,
1154
+ endLine: i + 1,
1155
+ message: t(
1156
+ "Potential SQL injection \u2014 string interpolation in SQL query.",
1157
+ "\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
1158
+ ),
1159
+ suggestion: t(
1160
+ "Use parameterized queries or prepared statements instead.",
1161
+ "\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
1162
+ )
1163
+ });
1156
1164
  }
1157
1165
  }
1158
1166
  return issues;
@@ -2436,12 +2444,7 @@ var noReassignParamRule = {
2436
2444
  }
2437
2445
  }
2438
2446
  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
- }
2447
+ const body = node.type === AST_NODE_TYPES.MethodDefinition ? node.value : node;
2445
2448
  if (!body || !("body" in body)) return;
2446
2449
  const fnBody = body.body;
2447
2450
  if (!fnBody) return;
@@ -2678,15 +2681,33 @@ var RuleEngine = class {
2678
2681
  );
2679
2682
  }
2680
2683
  run(context) {
2684
+ return this.runWithDiagnostics(context).issues;
2685
+ }
2686
+ runWithDiagnostics(context) {
2681
2687
  const allIssues = [];
2688
+ const ruleFailures = [];
2689
+ let rulesExecuted = 0;
2690
+ let rulesFailed = 0;
2682
2691
  for (const rule of this.rules) {
2692
+ rulesExecuted++;
2683
2693
  try {
2684
2694
  const issues = rule.check(context);
2685
2695
  allIssues.push(...issues);
2686
- } catch (_err) {
2696
+ } catch (err) {
2697
+ rulesFailed++;
2698
+ ruleFailures.push({
2699
+ ruleId: rule.id,
2700
+ file: context.filePath,
2701
+ message: err instanceof Error ? err.message : "Unknown rule execution failure"
2702
+ });
2687
2703
  }
2688
2704
  }
2689
- return allIssues;
2705
+ return {
2706
+ issues: allIssues,
2707
+ rulesExecuted,
2708
+ rulesFailed,
2709
+ ruleFailures
2710
+ };
2690
2711
  }
2691
2712
  getRules() {
2692
2713
  return [...this.rules];
@@ -3066,6 +3087,8 @@ var PKG_VERSION = (() => {
3066
3087
  return "0.1.0";
3067
3088
  }
3068
3089
  })();
3090
+ var REPORT_SCHEMA_VERSION = "1.0.0";
3091
+ var FINGERPRINT_VERSION = "1";
3069
3092
  var ScanEngine = class {
3070
3093
  config;
3071
3094
  diffParser;
@@ -3076,80 +3099,198 @@ var ScanEngine = class {
3076
3099
  this.ruleEngine = new RuleEngine(config);
3077
3100
  }
3078
3101
  async scan(options) {
3079
- const diffFiles = await this.getDiffFiles(options);
3102
+ const selection = await this.getScanCandidates(options);
3080
3103
  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);
3104
+ const scanErrors = [];
3105
+ const ruleFailures = [];
3106
+ let rulesExecuted = 0;
3107
+ let rulesFailed = 0;
3108
+ let filesScanned = 0;
3109
+ const results = await Promise.all(
3110
+ selection.candidates.map((diffFile) => this.scanFile(diffFile))
3111
+ );
3112
+ for (const result of results) {
3113
+ allIssues.push(...result.issues);
3114
+ ruleFailures.push(...result.ruleFailures);
3115
+ scanErrors.push(...result.scanErrors);
3116
+ rulesExecuted += result.rulesExecuted;
3117
+ rulesFailed += result.rulesFailed;
3118
+ if (result.scanned) {
3119
+ filesScanned++;
3130
3120
  }
3131
3121
  }
3132
- const dimensions = this.groupByDimension(allIssues);
3122
+ const issuesWithFingerprints = this.attachFingerprints(allIssues);
3123
+ const dimensions = this.groupByDimension(issuesWithFingerprints);
3133
3124
  const overallScore = calculateOverallScore(dimensions, this.config.weights);
3134
3125
  const grade = getGrade(overallScore);
3135
3126
  const commitHash = await this.diffParser.getCurrentCommitHash();
3136
3127
  return {
3128
+ schemaVersion: REPORT_SCHEMA_VERSION,
3137
3129
  version: PKG_VERSION,
3138
3130
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3139
3131
  commit: commitHash,
3132
+ scanMode: selection.scanMode,
3140
3133
  overall: {
3141
3134
  score: overallScore,
3142
3135
  grade,
3143
- filesScanned: diffFiles.filter((f) => f.status !== "deleted").length,
3144
- issuesFound: allIssues.length
3136
+ filesScanned,
3137
+ issuesFound: issuesWithFingerprints.length
3138
+ },
3139
+ toolHealth: {
3140
+ rulesExecuted,
3141
+ rulesFailed,
3142
+ filesConsidered: selection.filesConsidered,
3143
+ filesScanned,
3144
+ filesExcluded: selection.filesExcluded,
3145
+ filesSkipped: scanErrors.length,
3146
+ scanErrors,
3147
+ ruleFailures
3145
3148
  },
3146
3149
  dimensions,
3147
- issues: allIssues.sort((a, b) => {
3150
+ issues: issuesWithFingerprints.sort((a, b) => {
3148
3151
  const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
3149
3152
  return severityOrder[a.severity] - severityOrder[b.severity];
3150
3153
  })
3151
3154
  };
3152
3155
  }
3156
+ async scanFile(diffFile) {
3157
+ 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
+ };
3172
+ }
3173
+ 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
+ };
3188
+ }
3189
+ const filePath = resolve2(diffFile.filePath);
3190
+ let fileContent;
3191
+ try {
3192
+ fileContent = await readFile(filePath, "utf-8");
3193
+ } catch {
3194
+ 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;
3212
+ }
3213
+ const addedLines = diffFile.hunks.flatMap((hunk) => {
3214
+ const lines = hunk.content.split("\n");
3215
+ const result = [];
3216
+ let currentLine = hunk.newStart;
3217
+ for (const line of lines) {
3218
+ if (line.startsWith("+")) {
3219
+ result.push({ lineNumber: currentLine, content: line.slice(1) });
3220
+ currentLine++;
3221
+ } else if (line.startsWith("-")) {
3222
+ } else {
3223
+ currentLine++;
3224
+ }
3225
+ }
3226
+ return result;
3227
+ });
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, {
3235
+ maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
3236
+ maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
3237
+ maxFunctionLength: this.config.thresholds["max-function-length"],
3238
+ maxNestingDepth: this.config.thresholds["max-nesting-depth"],
3239
+ 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
+ };
3254
+ }
3255
+ async getScanCandidates(options) {
3256
+ const scanMode = this.getScanMode(options);
3257
+ const candidates = await this.getDiffFiles(options);
3258
+ if (scanMode === "files") {
3259
+ return {
3260
+ scanMode,
3261
+ candidates,
3262
+ filesConsidered: options.files?.length ?? candidates.length,
3263
+ filesExcluded: (options.files?.length ?? candidates.length) - candidates.length
3264
+ };
3265
+ }
3266
+ const filteredCandidates = [];
3267
+ let filesExcluded = 0;
3268
+ for (const candidate of candidates) {
3269
+ if (this.shouldIncludeFile(candidate.filePath)) {
3270
+ filteredCandidates.push(candidate);
3271
+ } else {
3272
+ filesExcluded++;
3273
+ }
3274
+ }
3275
+ return {
3276
+ scanMode,
3277
+ candidates: filteredCandidates,
3278
+ filesConsidered: candidates.length,
3279
+ filesExcluded
3280
+ };
3281
+ }
3282
+ getScanMode(options) {
3283
+ if (options.staged) {
3284
+ return "staged";
3285
+ }
3286
+ if (options.diff) {
3287
+ return "diff";
3288
+ }
3289
+ if (options.files && options.files.length > 0) {
3290
+ return "files";
3291
+ }
3292
+ return "changed";
3293
+ }
3153
3294
  async getDiffFiles(options) {
3154
3295
  if (options.staged) {
3155
3296
  return this.diffParser.getStagedFiles();
@@ -3158,14 +3299,10 @@ var ScanEngine = class {
3158
3299
  return this.diffParser.getDiffFromRef(options.diff);
3159
3300
  }
3160
3301
  if (options.files && options.files.length > 0) {
3302
+ const includedFiles = options.files.filter((filePath) => this.shouldIncludeFile(filePath));
3161
3303
  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 {
3304
+ includedFiles.map(
3305
+ (filePath) => readFile(resolve2(filePath), "utf-8").catch(() => "").then((content) => ({
3169
3306
  filePath,
3170
3307
  status: "modified",
3171
3308
  additions: content.split("\n").length,
@@ -3177,18 +3314,64 @@ var ScanEngine = class {
3177
3314
  oldLines: 0,
3178
3315
  newStart: 1,
3179
3316
  newLines: content.split("\n").length,
3180
- content: content.split("\n").map((l) => "+" + l).join("\n")
3317
+ content: content.split("\n").map((line) => "+" + line).join("\n")
3181
3318
  }
3182
3319
  ]
3183
- };
3184
- })
3320
+ }))
3321
+ )
3185
3322
  );
3186
3323
  }
3187
3324
  return this.diffParser.getChangedFiles();
3188
3325
  }
3326
+ shouldIncludeFile(filePath) {
3327
+ const normalizedPath = filePath.split(sep).join("/");
3328
+ const includePatterns = this.config.include.length > 0 ? this.config.include : ["**/*"];
3329
+ const included = includePatterns.some((pattern) => this.matchesPattern(normalizedPath, pattern));
3330
+ if (!included) {
3331
+ return false;
3332
+ }
3333
+ return !this.config.exclude.some((pattern) => this.matchesPattern(normalizedPath, pattern));
3334
+ }
3335
+ matchesPattern(filePath, pattern) {
3336
+ let regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
3337
+ regexPattern = regexPattern.replace(/\*\*\//g, "::DOUBLE_DIR::");
3338
+ regexPattern = regexPattern.replace(/\*\*/g, "::DOUBLE_STAR::");
3339
+ regexPattern = regexPattern.replace(/\*/g, "[^/]*");
3340
+ regexPattern = regexPattern.replace(/::DOUBLE_DIR::/g, "(?:.*/)?");
3341
+ regexPattern = regexPattern.replace(/::DOUBLE_STAR::/g, ".*");
3342
+ return new RegExp(`^${regexPattern}$`).test(filePath);
3343
+ }
3189
3344
  isTsJsFile(filePath) {
3190
3345
  return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
3191
3346
  }
3347
+ attachFingerprints(issues) {
3348
+ const occurrenceCounts = /* @__PURE__ */ new Map();
3349
+ return issues.map((issue) => {
3350
+ const normalizedFile = this.normalizeRelativePath(issue.file);
3351
+ const locationComponent = `${issue.startLine}:${issue.endLine}`;
3352
+ const baseKey = [
3353
+ issue.ruleId,
3354
+ normalizedFile,
3355
+ issue.category,
3356
+ issue.severity,
3357
+ locationComponent
3358
+ ].join("|");
3359
+ const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
3360
+ occurrenceCounts.set(baseKey, occurrenceIndex + 1);
3361
+ const fingerprint = createHash("sha256").update(`${FINGERPRINT_VERSION}|${baseKey}|${occurrenceIndex}`).digest("hex");
3362
+ return {
3363
+ ...issue,
3364
+ file: normalizedFile,
3365
+ fingerprint,
3366
+ fingerprintVersion: FINGERPRINT_VERSION
3367
+ };
3368
+ });
3369
+ }
3370
+ normalizeRelativePath(filePath) {
3371
+ const absolutePath = resolve2(filePath);
3372
+ const relativePath = relative(process.cwd(), absolutePath) || filePath;
3373
+ return relativePath.split(sep).join("/");
3374
+ }
3192
3375
  groupByDimension(issues) {
3193
3376
  const categories = [
3194
3377
  "security",
@@ -3199,7 +3382,7 @@ var ScanEngine = class {
3199
3382
  ];
3200
3383
  const grouped = {};
3201
3384
  for (const cat of categories) {
3202
- const catIssues = issues.filter((i) => i.category === cat);
3385
+ const catIssues = issues.filter((issue) => issue.category === cat);
3203
3386
  grouped[cat] = calculateDimensionScore(catIssues);
3204
3387
  }
3205
3388
  return grouped;
@@ -3219,7 +3402,11 @@ var en = {
3219
3402
  issuesFound: "{{count}} issue(s) found",
3220
3403
  issuesHeader: "Issues ({{count}}):",
3221
3404
  noIssuesFound: "No issues found! \u{1F389}",
3222
- scanned: "Scanned {{count}} file(s)"
3405
+ scanned: "Scanned {{count}} file(s)",
3406
+ healthHeader: "Tool Health",
3407
+ rulesFailed: "Failed rules: {{count}}",
3408
+ filesSkipped: "Skipped files: {{count}}",
3409
+ filesExcluded: "Excluded files: {{count}}"
3223
3410
  };
3224
3411
  var zh = {
3225
3412
  reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
@@ -3231,7 +3418,11 @@ var zh = {
3231
3418
  issuesFound: "\u53D1\u73B0 {{count}} \u4E2A\u95EE\u9898",
3232
3419
  issuesHeader: "\u95EE\u9898\u5217\u8868 ({{count}}):",
3233
3420
  noIssuesFound: "\u672A\u53D1\u73B0\u95EE\u9898! \u{1F389}",
3234
- scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6"
3421
+ scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6",
3422
+ healthHeader: "\u5DE5\u5177\u5065\u5EB7\u5EA6",
3423
+ rulesFailed: "\u5931\u8D25\u89C4\u5219\u6570\uFF1A{{count}}",
3424
+ filesSkipped: "\u8DF3\u8FC7\u6587\u4EF6\u6570\uFF1A{{count}}",
3425
+ filesExcluded: "\u6392\u9664\u6587\u4EF6\u6570\uFF1A{{count}}"
3235
3426
  };
3236
3427
  function renderTerminalReport(report) {
3237
3428
  const isZh = isZhLocale();
@@ -3273,19 +3464,32 @@ function renderTerminalReport(report) {
3273
3464
  };
3274
3465
  const dims = ["security", "logic", "structure", "style", "coverage"];
3275
3466
  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;
3467
+ const dimension = report.dimensions[dim];
3468
+ const dimEmoji = dimension.score >= 80 ? "\u2705" : dimension.score >= 60 ? "\u26A0\uFE0F" : "\u274C";
3469
+ const color = getScoreColor(dimension.score);
3470
+ const issueCount = dimension.issues.length;
3280
3471
  const detail = issueCount === 0 ? pc.green(t2.noIssues) : t2.issuesFound.replace("{{count}}", String(issueCount));
3281
3472
  table.push([
3282
3473
  `${dimEmoji} ${dimLabels[dim]}`,
3283
- color(String(d.score)),
3474
+ color(String(dimension.score)),
3284
3475
  detail
3285
3476
  ]);
3286
3477
  }
3287
3478
  lines.push(table.toString());
3288
3479
  lines.push("");
3480
+ if (report.toolHealth.rulesFailed > 0 || report.toolHealth.filesSkipped > 0 || report.toolHealth.filesExcluded > 0) {
3481
+ lines.push(pc.bold(t2.healthHeader));
3482
+ if (report.toolHealth.rulesFailed > 0) {
3483
+ lines.push(pc.yellow(` ${t2.rulesFailed.replace("{{count}}", String(report.toolHealth.rulesFailed))}`));
3484
+ }
3485
+ if (report.toolHealth.filesSkipped > 0) {
3486
+ lines.push(pc.yellow(` ${t2.filesSkipped.replace("{{count}}", String(report.toolHealth.filesSkipped))}`));
3487
+ }
3488
+ if (report.toolHealth.filesExcluded > 0) {
3489
+ lines.push(pc.dim(` ${t2.filesExcluded.replace("{{count}}", String(report.toolHealth.filesExcluded))}`));
3490
+ }
3491
+ lines.push("");
3492
+ }
3289
3493
  if (report.issues.length > 0) {
3290
3494
  lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
3291
3495
  lines.push("");
@@ -3349,12 +3553,23 @@ function getScoreColor(score) {
3349
3553
 
3350
3554
  // src/cli/output/json.ts
3351
3555
  function renderJsonReport(report) {
3352
- return JSON.stringify(report, null, 2);
3556
+ const payload = {
3557
+ schemaVersion: report.schemaVersion,
3558
+ version: report.version,
3559
+ timestamp: report.timestamp,
3560
+ commit: report.commit,
3561
+ scanMode: report.scanMode,
3562
+ overall: report.overall,
3563
+ toolHealth: report.toolHealth,
3564
+ dimensions: report.dimensions,
3565
+ issues: report.issues
3566
+ };
3567
+ return JSON.stringify(payload, null, 2);
3353
3568
  }
3354
3569
 
3355
3570
  // src/cli/commands/scan.ts
3356
3571
  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) => {
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) => {
3358
3573
  try {
3359
3574
  const config = await loadConfig();
3360
3575
  const engine = new ScanEngine(config);
@@ -3389,7 +3604,7 @@ function createScanCommand() {
3389
3604
  // src/cli/commands/report.ts
3390
3605
  import { Command as Command2 } from "commander";
3391
3606
  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) => {
3607
+ 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
3608
  try {
3394
3609
  const config = await loadConfig();
3395
3610
  const engine = new ScanEngine(config);
@@ -3413,14 +3628,14 @@ function createReportCommand() {
3413
3628
 
3414
3629
  // src/cli/commands/init.ts
3415
3630
  import { writeFile } from "fs/promises";
3416
- import { existsSync as existsSync4 } from "fs";
3631
+ import { existsSync as existsSync3 } from "fs";
3417
3632
  import { resolve as resolve3 } from "path";
3418
3633
  import { Command as Command3 } from "commander";
3419
3634
  import pc2 from "picocolors";
3420
3635
  function createInitCommand() {
3421
3636
  const cmd = new Command3("init").description("Initialize CodeTrust configuration file").action(async () => {
3422
3637
  const configPath = resolve3(".codetrust.yml");
3423
- if (existsSync4(configPath)) {
3638
+ if (existsSync3(configPath)) {
3424
3639
  console.log(pc2.yellow("\u26A0\uFE0F .codetrust.yml already exists. Skipping."));
3425
3640
  return;
3426
3641
  }
@@ -3494,7 +3709,7 @@ function formatSeverity2(severity) {
3494
3709
 
3495
3710
  // src/cli/commands/hook.ts
3496
3711
  import { writeFile as writeFile2, chmod, mkdir } from "fs/promises";
3497
- import { existsSync as existsSync5 } from "fs";
3712
+ import { existsSync as existsSync4 } from "fs";
3498
3713
  import { resolve as resolve4, join as join2 } from "path";
3499
3714
  import { Command as Command5 } from "commander";
3500
3715
  import pc4 from "picocolors";
@@ -3511,17 +3726,17 @@ function createHookCommand() {
3511
3726
  const cmd = new Command5("hook").description("Manage git hooks");
3512
3727
  cmd.command("install").description("Install pre-commit hook").action(async () => {
3513
3728
  const gitDir = resolve4(".git");
3514
- if (!existsSync5(gitDir)) {
3729
+ if (!existsSync4(gitDir)) {
3515
3730
  console.error(pc4.red("Error: Not a git repository."));
3516
3731
  process.exit(1);
3517
3732
  }
3518
3733
  const hooksDir = join2(gitDir, "hooks");
3519
3734
  const hookPath = join2(hooksDir, "pre-commit");
3520
3735
  try {
3521
- if (!existsSync5(hooksDir)) {
3736
+ if (!existsSync4(hooksDir)) {
3522
3737
  await mkdir(hooksDir, { recursive: true });
3523
3738
  }
3524
- if (existsSync5(hookPath)) {
3739
+ if (existsSync4(hookPath)) {
3525
3740
  console.log(pc4.yellow("\u26A0\uFE0F pre-commit hook already exists. Skipping."));
3526
3741
  console.log(pc4.dim(" Remove .git/hooks/pre-commit to reinstall."));
3527
3742
  return;