diffprism 0.34.1 → 0.36.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.
@@ -0,0 +1,501 @@
1
+ // packages/analysis/src/deterministic.ts
2
+ var MECHANICAL_CONFIG_PATTERNS = [
3
+ /\.config\./,
4
+ /\.eslintrc/,
5
+ /\.prettierrc/,
6
+ /tsconfig.*\.json$/,
7
+ /\.gitignore$/,
8
+ /\.lock$/
9
+ ];
10
+ var API_SURFACE_PATTERNS = [
11
+ /\/api\//,
12
+ /\/routes\//
13
+ ];
14
+ function isFormattingOnly(file) {
15
+ if (file.hunks.length === 0) return false;
16
+ for (const hunk of file.hunks) {
17
+ const adds = hunk.changes.filter((c) => c.type === "add").map((c) => c.content.replace(/\s/g, ""));
18
+ const deletes = hunk.changes.filter((c) => c.type === "delete").map((c) => c.content.replace(/\s/g, ""));
19
+ if (adds.length === 0 || deletes.length === 0) return false;
20
+ const deleteBag = [...deletes];
21
+ for (const add of adds) {
22
+ const idx = deleteBag.indexOf(add);
23
+ if (idx === -1) return false;
24
+ deleteBag.splice(idx, 1);
25
+ }
26
+ if (deleteBag.length > 0) return false;
27
+ }
28
+ return true;
29
+ }
30
+ function isImportOnly(file) {
31
+ if (file.hunks.length === 0) return false;
32
+ const importPattern = /^\s*(import\s|export\s.*from\s|const\s+\w+\s*=\s*require\(|require\()/;
33
+ for (const hunk of file.hunks) {
34
+ for (const change of hunk.changes) {
35
+ if (change.type === "context") continue;
36
+ const trimmed = change.content.trim();
37
+ if (trimmed === "") continue;
38
+ if (!importPattern.test(trimmed)) return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+ function isMechanicalConfigFile(path) {
44
+ return MECHANICAL_CONFIG_PATTERNS.some((re) => re.test(path));
45
+ }
46
+ function isApiSurface(file) {
47
+ if (API_SURFACE_PATTERNS.some((re) => re.test(file.path))) return true;
48
+ const basename = file.path.slice(file.path.lastIndexOf("/") + 1);
49
+ if ((basename === "index.ts" || basename === "index.js") && file.additions >= 10) {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ function categorizeFiles(files) {
55
+ const critical = [];
56
+ const notable = [];
57
+ const mechanical = [];
58
+ const securityFlags = detectSecurityPatterns(files);
59
+ const complexityScores = computeComplexityScores(files);
60
+ const securityByFile = /* @__PURE__ */ new Map();
61
+ for (const flag of securityFlags) {
62
+ const existing = securityByFile.get(flag.file) || [];
63
+ existing.push(flag);
64
+ securityByFile.set(flag.file, existing);
65
+ }
66
+ const complexityByFile = /* @__PURE__ */ new Map();
67
+ for (const score of complexityScores) {
68
+ complexityByFile.set(score.path, score);
69
+ }
70
+ for (const file of files) {
71
+ const description = `${file.status} (${file.language || "unknown"}) +${file.additions} -${file.deletions}`;
72
+ const fileSecurityFlags = securityByFile.get(file.path);
73
+ const fileComplexity = complexityByFile.get(file.path);
74
+ const criticalReasons = [];
75
+ if (fileSecurityFlags && fileSecurityFlags.length > 0) {
76
+ const patterns = fileSecurityFlags.map((f) => f.pattern);
77
+ const unique = [...new Set(patterns)];
78
+ criticalReasons.push(`security patterns detected: ${unique.join(", ")}`);
79
+ }
80
+ if (fileComplexity && fileComplexity.score >= 8) {
81
+ criticalReasons.push(`high complexity score (${fileComplexity.score}/10)`);
82
+ }
83
+ if (isApiSurface(file)) {
84
+ criticalReasons.push("modifies public API surface");
85
+ }
86
+ if (criticalReasons.length > 0) {
87
+ critical.push({
88
+ file: file.path,
89
+ description,
90
+ reason: `Critical: ${criticalReasons.join("; ")}`
91
+ });
92
+ continue;
93
+ }
94
+ const isPureRename = file.status === "renamed" && file.additions === 0 && file.deletions === 0;
95
+ if (isPureRename) {
96
+ mechanical.push({
97
+ file: file.path,
98
+ description,
99
+ reason: "Mechanical: pure rename with no content changes"
100
+ });
101
+ continue;
102
+ }
103
+ if (isFormattingOnly(file)) {
104
+ mechanical.push({
105
+ file: file.path,
106
+ description,
107
+ reason: "Mechanical: formatting/whitespace-only changes"
108
+ });
109
+ continue;
110
+ }
111
+ if (isMechanicalConfigFile(file.path)) {
112
+ mechanical.push({
113
+ file: file.path,
114
+ description,
115
+ reason: "Mechanical: config file change"
116
+ });
117
+ continue;
118
+ }
119
+ if (file.hunks.length > 0 && isImportOnly(file)) {
120
+ mechanical.push({
121
+ file: file.path,
122
+ description,
123
+ reason: "Mechanical: import/require-only changes"
124
+ });
125
+ continue;
126
+ }
127
+ notable.push({
128
+ file: file.path,
129
+ description,
130
+ reason: "Notable: requires review"
131
+ });
132
+ }
133
+ return { critical, notable, mechanical };
134
+ }
135
+ function computeFileStats(files) {
136
+ return files.map((f) => ({
137
+ path: f.path,
138
+ language: f.language,
139
+ status: f.status,
140
+ additions: f.additions,
141
+ deletions: f.deletions
142
+ }));
143
+ }
144
+ function detectAffectedModules(files) {
145
+ const dirs = /* @__PURE__ */ new Set();
146
+ for (const f of files) {
147
+ const lastSlash = f.path.lastIndexOf("/");
148
+ if (lastSlash > 0) {
149
+ dirs.add(f.path.slice(0, lastSlash));
150
+ }
151
+ }
152
+ return [...dirs].sort();
153
+ }
154
+ var TEST_PATTERNS = [
155
+ /\.test\./,
156
+ /\.spec\./,
157
+ /\/__tests__\//,
158
+ /\/test\//
159
+ ];
160
+ function detectAffectedTests(files) {
161
+ return files.filter((f) => TEST_PATTERNS.some((re) => re.test(f.path))).map((f) => f.path);
162
+ }
163
+ var DEPENDENCY_FIELDS = [
164
+ '"dependencies"',
165
+ '"devDependencies"',
166
+ '"peerDependencies"',
167
+ '"optionalDependencies"'
168
+ ];
169
+ function detectNewDependencies(files) {
170
+ const deps = /* @__PURE__ */ new Set();
171
+ const packageFiles = files.filter(
172
+ (f) => f.path.endsWith("package.json") && f.hunks.length > 0
173
+ );
174
+ for (const file of packageFiles) {
175
+ for (const hunk of file.hunks) {
176
+ let inDependencyBlock = false;
177
+ for (const change of hunk.changes) {
178
+ const line = change.content;
179
+ if (DEPENDENCY_FIELDS.some((field) => line.includes(field))) {
180
+ inDependencyBlock = true;
181
+ continue;
182
+ }
183
+ if (inDependencyBlock && line.trim().startsWith("}")) {
184
+ inDependencyBlock = false;
185
+ continue;
186
+ }
187
+ if (change.type === "add" && inDependencyBlock) {
188
+ const match = line.match(/"([^"]+)"\s*:/);
189
+ if (match) {
190
+ deps.add(match[1]);
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ return [...deps].sort();
197
+ }
198
+ function generateSummary(files) {
199
+ const totalFiles = files.length;
200
+ const counts = {
201
+ added: 0,
202
+ modified: 0,
203
+ deleted: 0,
204
+ renamed: 0
205
+ };
206
+ let totalAdditions = 0;
207
+ let totalDeletions = 0;
208
+ for (const f of files) {
209
+ counts[f.status]++;
210
+ totalAdditions += f.additions;
211
+ totalDeletions += f.deletions;
212
+ }
213
+ const parts = [];
214
+ if (counts.modified > 0) parts.push(`${counts.modified} modified`);
215
+ if (counts.added > 0) parts.push(`${counts.added} added`);
216
+ if (counts.deleted > 0) parts.push(`${counts.deleted} deleted`);
217
+ if (counts.renamed > 0) parts.push(`${counts.renamed} renamed`);
218
+ const breakdown = parts.length > 0 ? `: ${parts.join(", ")}` : "";
219
+ return `${totalFiles} files changed${breakdown} (+${totalAdditions} -${totalDeletions})`;
220
+ }
221
+ var BRANCH_PATTERN = /\b(if|else|switch|case|catch)\b|\?\s|&&|\|\|/;
222
+ function computeComplexityScores(files) {
223
+ const results = [];
224
+ for (const file of files) {
225
+ let score = 0;
226
+ const factors = [];
227
+ const totalChanges = file.additions + file.deletions;
228
+ if (totalChanges > 100) {
229
+ score += 3;
230
+ factors.push(`large diff (+${file.additions} -${file.deletions})`);
231
+ } else if (totalChanges > 50) {
232
+ score += 2;
233
+ factors.push(`medium diff (+${file.additions} -${file.deletions})`);
234
+ } else if (totalChanges > 20) {
235
+ score += 1;
236
+ factors.push(`moderate diff (+${file.additions} -${file.deletions})`);
237
+ }
238
+ const hunkCount = file.hunks.length;
239
+ if (hunkCount > 4) {
240
+ score += 2;
241
+ factors.push(`many hunks (${hunkCount})`);
242
+ } else if (hunkCount > 2) {
243
+ score += 1;
244
+ factors.push(`multiple hunks (${hunkCount})`);
245
+ }
246
+ let branchCount = 0;
247
+ let deepNestCount = 0;
248
+ for (const hunk of file.hunks) {
249
+ for (const change of hunk.changes) {
250
+ if (change.type !== "add") continue;
251
+ const line = change.content;
252
+ if (BRANCH_PATTERN.test(line)) {
253
+ branchCount++;
254
+ }
255
+ const leadingSpaces = line.match(/^(\s*)/);
256
+ if (leadingSpaces) {
257
+ const ws = leadingSpaces[1];
258
+ const tabCount = (ws.match(/\t/g) || []).length;
259
+ const spaceCount = ws.replace(/\t/g, "").length;
260
+ if (tabCount >= 4 || spaceCount >= 16) {
261
+ deepNestCount++;
262
+ }
263
+ }
264
+ }
265
+ }
266
+ const branchScore = Math.floor(branchCount / 5);
267
+ if (branchScore > 0) {
268
+ score += branchScore;
269
+ factors.push(`${branchCount} logic branches`);
270
+ }
271
+ const nestScore = Math.floor(deepNestCount / 5);
272
+ if (nestScore > 0) {
273
+ score += nestScore;
274
+ factors.push(`${deepNestCount} deeply nested lines`);
275
+ }
276
+ score = Math.max(1, Math.min(10, score));
277
+ results.push({ path: file.path, score, factors });
278
+ }
279
+ results.sort((a, b) => b.score - a.score);
280
+ return results;
281
+ }
282
+ var NON_CODE_EXTENSIONS = /* @__PURE__ */ new Set([
283
+ ".json",
284
+ ".md",
285
+ ".css",
286
+ ".scss",
287
+ ".less",
288
+ ".svg",
289
+ ".png",
290
+ ".jpg",
291
+ ".gif",
292
+ ".ico",
293
+ ".yaml",
294
+ ".yml",
295
+ ".toml",
296
+ ".lock",
297
+ ".html"
298
+ ]);
299
+ var CONFIG_PATTERNS = [
300
+ /\.config\./,
301
+ /\.rc\./,
302
+ /eslint/,
303
+ /prettier/,
304
+ /tsconfig/,
305
+ /tailwind/,
306
+ /vite\.config/,
307
+ /vitest\.config/
308
+ ];
309
+ function isTestFile(path) {
310
+ return TEST_PATTERNS.some((re) => re.test(path));
311
+ }
312
+ function isNonCodeFile(path) {
313
+ const ext = path.slice(path.lastIndexOf("."));
314
+ return NON_CODE_EXTENSIONS.has(ext);
315
+ }
316
+ function isConfigFile(path) {
317
+ return CONFIG_PATTERNS.some((re) => re.test(path));
318
+ }
319
+ function detectTestCoverageGaps(files) {
320
+ const filePaths = new Set(files.map((f) => f.path));
321
+ const results = [];
322
+ for (const file of files) {
323
+ if (file.status !== "added" && file.status !== "modified") continue;
324
+ if (isTestFile(file.path)) continue;
325
+ if (isNonCodeFile(file.path)) continue;
326
+ if (isConfigFile(file.path)) continue;
327
+ const dir = file.path.slice(0, file.path.lastIndexOf("/") + 1);
328
+ const basename = file.path.slice(file.path.lastIndexOf("/") + 1);
329
+ const extDot = basename.lastIndexOf(".");
330
+ const name = extDot > 0 ? basename.slice(0, extDot) : basename;
331
+ const ext = extDot > 0 ? basename.slice(extDot) : "";
332
+ const candidates = [
333
+ `${dir}${name}.test${ext}`,
334
+ `${dir}${name}.spec${ext}`,
335
+ `${dir}__tests__/${name}${ext}`,
336
+ `${dir}__tests__/${name}.test${ext}`,
337
+ `${dir}__tests__/${name}.spec${ext}`
338
+ ];
339
+ const matchedTest = candidates.find((c) => filePaths.has(c));
340
+ results.push({
341
+ sourceFile: file.path,
342
+ testFile: matchedTest ?? null
343
+ });
344
+ }
345
+ return results;
346
+ }
347
+ var PATTERN_MATCHERS = [
348
+ { pattern: "todo", test: (l) => /\btodo\b/i.test(l) },
349
+ { pattern: "fixme", test: (l) => /\bfixme\b/i.test(l) },
350
+ { pattern: "hack", test: (l) => /\bhack\b/i.test(l) },
351
+ {
352
+ pattern: "console",
353
+ test: (l) => /\bconsole\.(log|debug|warn|error)\b/.test(l)
354
+ },
355
+ { pattern: "debug", test: (l) => /\bdebugger\b/.test(l) },
356
+ {
357
+ pattern: "disabled_test",
358
+ test: (l) => /\.(skip)\(|(\bxit|xdescribe|xtest)\(/.test(l)
359
+ }
360
+ ];
361
+ function detectPatterns(files) {
362
+ const results = [];
363
+ for (const file of files) {
364
+ if (file.status === "added" && file.additions > 500) {
365
+ results.push({
366
+ file: file.path,
367
+ line: 0,
368
+ pattern: "large_file",
369
+ content: `Large added file: ${file.additions} lines`
370
+ });
371
+ }
372
+ for (const hunk of file.hunks) {
373
+ for (const change of hunk.changes) {
374
+ if (change.type !== "add") continue;
375
+ for (const matcher of PATTERN_MATCHERS) {
376
+ if (matcher.test(change.content)) {
377
+ results.push({
378
+ file: file.path,
379
+ line: change.lineNumber,
380
+ pattern: matcher.pattern,
381
+ content: change.content.trim()
382
+ });
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ results.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
389
+ return results;
390
+ }
391
+ var SECURITY_MATCHERS = [
392
+ {
393
+ pattern: "eval",
394
+ severity: "critical",
395
+ test: (l) => /\beval\s*\(/.test(l)
396
+ },
397
+ {
398
+ pattern: "inner_html",
399
+ severity: "warning",
400
+ test: (l) => /\.innerHTML\b|dangerouslySetInnerHTML/.test(l)
401
+ },
402
+ {
403
+ pattern: "sql_injection",
404
+ severity: "critical",
405
+ test: (l) => /`[^`]*\b(SELECT|INSERT|UPDATE|DELETE)\b/i.test(l) || /\b(SELECT|INSERT|UPDATE|DELETE)\b[^`]*\$\{/i.test(l)
406
+ },
407
+ {
408
+ pattern: "exec",
409
+ severity: "critical",
410
+ test: (l) => /child_process/.test(l) || /\bexec\s*\(/.test(l) || /\bexecSync\s*\(/.test(l)
411
+ },
412
+ {
413
+ pattern: "hardcoded_secret",
414
+ severity: "critical",
415
+ test: (l) => /\b(token|secret|api_key|apikey|password|passwd|credential)\s*=\s*["']/i.test(l)
416
+ },
417
+ {
418
+ pattern: "insecure_url",
419
+ severity: "warning",
420
+ test: (l) => /http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(l)
421
+ }
422
+ ];
423
+ function detectSecurityPatterns(files) {
424
+ const results = [];
425
+ for (const file of files) {
426
+ for (const hunk of file.hunks) {
427
+ for (const change of hunk.changes) {
428
+ if (change.type !== "add") continue;
429
+ for (const matcher of SECURITY_MATCHERS) {
430
+ if (matcher.test(change.content)) {
431
+ results.push({
432
+ file: file.path,
433
+ line: change.lineNumber,
434
+ pattern: matcher.pattern,
435
+ content: change.content.trim(),
436
+ severity: matcher.severity
437
+ });
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ results.sort((a, b) => {
444
+ const severityOrder = { critical: 0, warning: 1 };
445
+ const aSev = severityOrder[a.severity];
446
+ const bSev = severityOrder[b.severity];
447
+ if (aSev !== bSev) return aSev - bSev;
448
+ return a.file.localeCompare(b.file) || a.line - b.line;
449
+ });
450
+ return results;
451
+ }
452
+
453
+ // packages/analysis/src/index.ts
454
+ function analyze(diffSet) {
455
+ const { files } = diffSet;
456
+ const triage = categorizeFiles(files);
457
+ const fileStats = computeFileStats(files);
458
+ const affectedModules = detectAffectedModules(files);
459
+ const affectedTests = detectAffectedTests(files);
460
+ const newDependencies = detectNewDependencies(files);
461
+ const summary = generateSummary(files);
462
+ const complexity = computeComplexityScores(files);
463
+ const testCoverage = detectTestCoverageGaps(files);
464
+ const codePatterns = detectPatterns(files);
465
+ const securityPatterns = detectSecurityPatterns(files);
466
+ const patterns = [...securityPatterns, ...codePatterns];
467
+ return {
468
+ summary,
469
+ triage,
470
+ impact: {
471
+ affectedModules,
472
+ affectedTests,
473
+ publicApiChanges: false,
474
+ breakingChanges: [],
475
+ newDependencies
476
+ },
477
+ verification: {
478
+ testsPass: null,
479
+ typeCheck: null,
480
+ lintClean: null
481
+ },
482
+ fileStats,
483
+ complexity,
484
+ testCoverage,
485
+ patterns
486
+ };
487
+ }
488
+
489
+ export {
490
+ categorizeFiles,
491
+ computeFileStats,
492
+ detectAffectedModules,
493
+ detectAffectedTests,
494
+ detectNewDependencies,
495
+ generateSummary,
496
+ computeComplexityScores,
497
+ detectTestCoverageGaps,
498
+ detectPatterns,
499
+ detectSecurityPatterns,
500
+ analyze
501
+ };