@pilllesss/yorn 1.0.182 โ†’ 1.0.183

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.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PR Analyzer - Analyze PR complexity and suggest review approach.
4
+
5
+ Usage:
6
+ python pr-analyzer.py [--diff-file FILE] [--stats]
7
+
8
+ Or pipe diff directly:
9
+ git diff main...HEAD | python pr-analyzer.py
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import re
15
+ import argparse
16
+ from collections import defaultdict
17
+ from dataclasses import dataclass
18
+ from typing import List, Dict, Optional
19
+
20
+ RISK_NO_TESTS = "NO_TEST_CHANGES"
21
+
22
+
23
+ @dataclass
24
+ class FileStats:
25
+ """Statistics for a single file."""
26
+ filename: str
27
+ additions: int = 0
28
+ deletions: int = 0
29
+ is_test: bool = False
30
+ is_config: bool = False
31
+ language: str = "unknown"
32
+
33
+
34
+ @dataclass
35
+ class PRAnalysis:
36
+ """Complete PR analysis results."""
37
+ total_files: int
38
+ total_additions: int
39
+ total_deletions: int
40
+ files: List[FileStats]
41
+ complexity_score: float
42
+ size_category: str
43
+ estimated_review_time: int
44
+ risk_factors: List[str]
45
+ suggestions: List[str]
46
+
47
+
48
+ def detect_language(filename: str) -> str:
49
+ """Detect programming language from filename."""
50
+ _, ext = os.path.splitext(filename)
51
+ extensions = {
52
+ '.py': 'Python',
53
+ '.js': 'JavaScript',
54
+ '.ts': 'TypeScript',
55
+ '.tsx': 'TypeScript/React',
56
+ '.jsx': 'JavaScript/React',
57
+ '.rs': 'Rust',
58
+ '.go': 'Go',
59
+ '.c': 'C',
60
+ '.h': 'C/C++',
61
+ '.cpp': 'C++',
62
+ '.hpp': 'C++',
63
+ '.cc': 'C++',
64
+ '.cxx': 'C++',
65
+ '.hh': 'C++',
66
+ '.hxx': 'C++',
67
+ '.java': 'Java',
68
+ '.kt': 'Kotlin',
69
+ '.swift': 'Swift',
70
+ '.rb': 'Ruby',
71
+ '.php': 'PHP',
72
+ '.cs': 'C#',
73
+ '.vue': 'Vue',
74
+ '.svelte': 'Svelte',
75
+ '.sql': 'SQL',
76
+ '.md': 'Markdown',
77
+ '.json': 'JSON',
78
+ '.yaml': 'YAML',
79
+ '.yml': 'YAML',
80
+ '.toml': 'TOML',
81
+ '.css': 'CSS',
82
+ '.scss': 'SCSS',
83
+ '.less': 'Less',
84
+ '.html': 'HTML',
85
+ '.zig': 'Zig',
86
+ '.ex': 'Elixir',
87
+ '.exs': 'Elixir',
88
+ '.erl': 'Erlang',
89
+ '.scala': 'Scala',
90
+ '.lua': 'Lua',
91
+ }
92
+ return extensions.get(ext.lower(), 'unknown')
93
+
94
+
95
+ def is_test_file(filename: str) -> bool:
96
+ """Check if file is a test file."""
97
+ test_patterns = [
98
+ r'(?:^|/)test_[^/]+\.py$', # Python: test_handler.py (anchored to path segment)
99
+ r'[^/]+_test\.', # *_test.<ext> โ€” Rust, Go, etc.
100
+ r'[^/]+\.test\.(js|ts|tsx|jsx)$', # JS/TS: handler.test.ts
101
+ r'[^/]+\.spec\.(js|ts|tsx|jsx)$', # JS/TS: handler.spec.ts
102
+ r'(?:^|/)tests?/', # tests/ or test/ directory
103
+ r'(?:^|/)__tests__/', # __tests__/ directory
104
+ r'(?:^|/)spec/', # spec/ directory (Ruby, etc.)
105
+ ]
106
+ return any(re.search(p, filename) for p in test_patterns)
107
+
108
+
109
+ def is_config_file(filename: str) -> bool:
110
+ """Check if file is a configuration file.
111
+
112
+ Uses explicit known-name matching to avoid flagging data files
113
+ like data.json, openapi.yaml, or swagger.json as config.
114
+ """
115
+ basename = os.path.basename(filename)
116
+
117
+ # Env files (.env, .env.local, .env.production, โ€ฆ)
118
+ if basename.startswith('.env'):
119
+ return True
120
+
121
+ # Known config filenames (exact match)
122
+ known_config_names = {
123
+ 'package.json', 'package-lock.json',
124
+ 'tsconfig.json', 'jsconfig.json',
125
+ 'babel.config.json', 'babel.config.js',
126
+ 'webpack.config.js', 'webpack.config.ts',
127
+ 'rollup.config.js', 'vite.config.ts', 'vite.config.js',
128
+ '.eslintrc.json', '.eslintrc.js', '.eslintrc.yml',
129
+ '.prettierrc.json', '.prettierrc.yml', '.prettierrc.js',
130
+ 'jest.config.js', 'jest.config.ts',
131
+ 'vitest.config.ts', 'vitest.config.js',
132
+ 'tailwind.config.js', 'tailwind.config.ts',
133
+ 'postcss.config.js',
134
+ 'docker-compose.yml', 'docker-compose.yaml',
135
+ 'Dockerfile',
136
+ 'Makefile', 'CMakeLists.txt',
137
+ 'pyproject.toml', 'poetry.toml', 'Pipfile',
138
+ 'setup.cfg', 'setup.py', 'tox.ini',
139
+ 'Cargo.toml', 'Cargo.lock',
140
+ 'go.mod', 'go.sum',
141
+ 'Gemfile', 'Gemfile.lock',
142
+ 'composer.json', 'composer.lock',
143
+ 'Podfile', 'Package.swift',
144
+ '.gitignore', '.gitattributes',
145
+ 'gradle.properties', 'build.gradle', 'build.gradle.kts',
146
+ 'settings.gradle', 'settings.gradle.kts',
147
+ }
148
+ if basename in known_config_names:
149
+ return True
150
+
151
+ # Known config path patterns
152
+ config_path_patterns = [
153
+ r'\.github/workflows/[^/]+\.ya?ml$',
154
+ r'\.vscode/',
155
+ r'\.idea/',
156
+ ]
157
+ if any(re.search(p, filename) for p in config_path_patterns):
158
+ return True
159
+
160
+ # Files with "config" in the name (e.g. app.config.ts, database_config.yml)
161
+ if re.search(r'config\.', basename):
162
+ return True
163
+
164
+ # Files under a config/ directory
165
+ if re.search(r'(?:^|/)config/', filename):
166
+ return True
167
+
168
+ return False
169
+
170
+
171
+ def parse_diff(diff_content: str) -> List[FileStats]:
172
+ """Parse git diff output and extract file statistics."""
173
+ files = []
174
+ current_file = None
175
+
176
+ for line in diff_content.split('\n'):
177
+ # New file header
178
+ if line.startswith('diff --git'):
179
+ if current_file:
180
+ files.append(current_file)
181
+ # "diff --git a/<path> b/<path>" โ€” match the b/ side via a
182
+ # backreference so a literal "b/" inside paths like lib/, web/ or
183
+ # db/ can't be mistaken for the prefix. Renames have differing
184
+ # paths, so fall back to the b/ side after the separating space.
185
+ match = re.match(r'diff --git a/(.+?) b/\1', line)
186
+ if not match:
187
+ match = re.search(r' b/(.+)$', line)
188
+ if match:
189
+ filename = match.group(1)
190
+ current_file = FileStats(
191
+ filename=filename,
192
+ language=detect_language(filename),
193
+ is_test=is_test_file(filename),
194
+ is_config=is_config_file(filename),
195
+ )
196
+ else:
197
+ current_file = None
198
+ elif current_file:
199
+ if line.startswith('+') and not line.startswith('+++'):
200
+ current_file.additions += 1
201
+ elif line.startswith('-') and not line.startswith('---'):
202
+ current_file.deletions += 1
203
+
204
+ if current_file:
205
+ files.append(current_file)
206
+
207
+ return files
208
+
209
+
210
+ def calculate_complexity(files: List[FileStats]) -> float:
211
+ """Calculate complexity score (0-1 scale)."""
212
+ if not files:
213
+ return 0.0
214
+
215
+ total_changes = sum(f.additions + f.deletions for f in files)
216
+
217
+ # Base complexity from size
218
+ size_factor = min(total_changes / 1000, 1.0)
219
+
220
+ # Factor for number of files
221
+ file_factor = min(len(files) / 20, 1.0)
222
+
223
+ # Factor for non-test code ratio
224
+ test_lines = sum(f.additions + f.deletions for f in files if f.is_test)
225
+ non_test_ratio = 1 - (test_lines / max(total_changes, 1))
226
+
227
+ # Factor for language diversity
228
+ languages = set(f.language for f in files if f.language != 'unknown')
229
+ lang_factor = min(len(languages) / 5, 1.0)
230
+
231
+ complexity = (
232
+ size_factor * 0.4 +
233
+ file_factor * 0.2 +
234
+ non_test_ratio * 0.2 +
235
+ lang_factor * 0.2
236
+ )
237
+
238
+ return round(complexity, 2)
239
+
240
+
241
+ def categorize_size(total_changes: int) -> str:
242
+ """Categorize PR size."""
243
+ if total_changes < 50:
244
+ return "XS (Extra Small)"
245
+ elif total_changes < 200:
246
+ return "S (Small)"
247
+ elif total_changes < 400:
248
+ return "M (Medium)"
249
+ elif total_changes < 800:
250
+ return "L (Large)"
251
+ else:
252
+ return "XL (Extra Large) - Consider splitting"
253
+
254
+
255
+ def estimate_review_time(files: List[FileStats], complexity: float) -> int:
256
+ """Estimate review time in minutes."""
257
+ total_changes = sum(f.additions + f.deletions for f in files)
258
+
259
+ # Base time: ~1 minute per 20 lines
260
+ base_time = total_changes / 20
261
+
262
+ # Adjust for complexity
263
+ adjusted_time = base_time * (1 + complexity)
264
+
265
+ # Minimum 5 minutes, maximum 120 minutes
266
+ return max(5, min(120, int(adjusted_time)))
267
+
268
+
269
+ def identify_risk_factors(files: List[FileStats]) -> List[str]:
270
+ """Identify potential risk factors in the PR."""
271
+ risks = []
272
+
273
+ total_changes = sum(f.additions + f.deletions for f in files)
274
+ test_changes = sum(f.additions + f.deletions for f in files if f.is_test)
275
+
276
+ if total_changes > 400:
277
+ risks.append("Large PR (>400 lines) - harder to review thoroughly")
278
+
279
+ if test_changes == 0 and total_changes > 50:
280
+ risks.append(f"{RISK_NO_TESTS}: No test changes - verify test coverage")
281
+
282
+ if total_changes > 100 and test_changes / max(total_changes, 1) < 0.2:
283
+ risks.append("Low test ratio (<20%) - consider adding more tests")
284
+
285
+ # Security-sensitive files
286
+ security_patterns = ['.env', 'auth', 'security', 'password', 'token', 'secret']
287
+ for f in files:
288
+ if any(p in f.filename.lower() for p in security_patterns):
289
+ risks.append(f"Security-sensitive file: {f.filename}")
290
+ break
291
+
292
+ # Database changes
293
+ for f in files:
294
+ if 'migration' in f.filename.lower() or f.language == 'SQL':
295
+ risks.append("Database changes detected - review carefully")
296
+ break
297
+
298
+ # Config changes
299
+ config_files = [f for f in files if f.is_config]
300
+ if config_files:
301
+ risks.append(f"Configuration changes in {len(config_files)} file(s)")
302
+
303
+ return risks
304
+
305
+
306
+ def generate_suggestions(files: List[FileStats], complexity: float, risks: List[str]) -> List[str]:
307
+ """Generate review suggestions."""
308
+ suggestions = []
309
+
310
+ total_changes = sum(f.additions + f.deletions for f in files)
311
+
312
+ if total_changes > 800:
313
+ suggestions.append("Consider splitting this PR into smaller, focused changes")
314
+
315
+ if complexity > 0.7:
316
+ suggestions.append("High complexity - allocate extra review time")
317
+ suggestions.append("Consider pair reviewing for critical sections")
318
+
319
+ if any(RISK_NO_TESTS in r for r in risks):
320
+ suggestions.append("Request test additions before approval")
321
+
322
+ # Language-specific suggestions
323
+ languages = set(f.language for f in files)
324
+ if 'TypeScript' in languages or 'TypeScript/React' in languages:
325
+ suggestions.append("Check for proper type usage (avoid 'any')")
326
+ if 'Rust' in languages:
327
+ suggestions.append("Check for unwrap() usage and error handling")
328
+ if 'C' in languages or 'C++' in languages or 'C/C++' in languages:
329
+ suggestions.append("Check for memory safety, bounds checks, and UB risks")
330
+ if 'SQL' in languages:
331
+ suggestions.append("Review for SQL injection and query performance")
332
+
333
+ if not suggestions:
334
+ suggestions.append("Standard review process should suffice")
335
+
336
+ return suggestions
337
+
338
+
339
+ def analyze_pr(diff_content: str) -> PRAnalysis:
340
+ """Perform complete PR analysis."""
341
+ files = parse_diff(diff_content)
342
+
343
+ total_additions = sum(f.additions for f in files)
344
+ total_deletions = sum(f.deletions for f in files)
345
+ total_changes = total_additions + total_deletions
346
+
347
+ complexity = calculate_complexity(files)
348
+ risks = identify_risk_factors(files)
349
+ suggestions = generate_suggestions(files, complexity, risks)
350
+
351
+ return PRAnalysis(
352
+ total_files=len(files),
353
+ total_additions=total_additions,
354
+ total_deletions=total_deletions,
355
+ files=files,
356
+ complexity_score=complexity,
357
+ size_category=categorize_size(total_changes),
358
+ estimated_review_time=estimate_review_time(files, complexity),
359
+ risk_factors=risks,
360
+ suggestions=suggestions,
361
+ )
362
+
363
+
364
+ def print_analysis(analysis: PRAnalysis, show_files: bool = False):
365
+ """Print analysis results."""
366
+ print("\n" + "=" * 60)
367
+ print("PR ANALYSIS REPORT")
368
+ print("=" * 60)
369
+
370
+ print(f"\n๐Ÿ“Š SUMMARY")
371
+ print(f" Files changed: {analysis.total_files}")
372
+ print(f" Additions: +{analysis.total_additions}")
373
+ print(f" Deletions: -{analysis.total_deletions}")
374
+ print(f" Total changes: {analysis.total_additions + analysis.total_deletions}")
375
+
376
+ print(f"\n๐Ÿ“ SIZE: {analysis.size_category}")
377
+ print(f" Complexity score: {analysis.complexity_score}/1.0")
378
+ print(f" Estimated review time: ~{analysis.estimated_review_time} minutes")
379
+
380
+ if analysis.risk_factors:
381
+ print(f"\nโš ๏ธ RISK FACTORS:")
382
+ for risk in analysis.risk_factors:
383
+ print(f" โ€ข {risk}")
384
+
385
+ print(f"\n๐Ÿ’ก SUGGESTIONS:")
386
+ for suggestion in analysis.suggestions:
387
+ print(f" โ€ข {suggestion}")
388
+
389
+ if show_files:
390
+ print(f"\n๐Ÿ“ FILES:")
391
+ # Group by language
392
+ by_lang: Dict[str, List[FileStats]] = defaultdict(list)
393
+ for f in analysis.files:
394
+ by_lang[f.language].append(f)
395
+
396
+ for lang, lang_files in sorted(by_lang.items()):
397
+ print(f"\n [{lang}]")
398
+ for f in lang_files:
399
+ prefix = "๐Ÿงช" if f.is_test else "โš™๏ธ" if f.is_config else "๐Ÿ“„"
400
+ print(f" {prefix} {f.filename} (+{f.additions}/-{f.deletions})")
401
+
402
+ print("\n" + "=" * 60)
403
+
404
+
405
+ def main():
406
+ parser = argparse.ArgumentParser(description='Analyze PR complexity')
407
+ parser.add_argument('--diff-file', '-f', help='Path to diff file')
408
+ parser.add_argument('--stats', '-s', action='store_true', help='Show file details')
409
+ args = parser.parse_args()
410
+
411
+ # Read diff from file or stdin
412
+ try:
413
+ if args.diff_file:
414
+ with open(args.diff_file, 'r', encoding='utf-8', errors='replace') as f:
415
+ diff_content = f.read()
416
+ elif not sys.stdin.isatty():
417
+ diff_content = sys.stdin.buffer.read().decode('utf-8', errors='replace')
418
+ else:
419
+ print("Usage: git diff main...HEAD | python pr-analyzer.py")
420
+ print(" python pr-analyzer.py -f diff.txt")
421
+ sys.exit(1)
422
+ except OSError as e:
423
+ print(f"Error reading diff input: {e}", file=sys.stderr)
424
+ sys.exit(1)
425
+
426
+ if not diff_content.strip():
427
+ print("No diff content provided")
428
+ sys.exit(1)
429
+
430
+ analysis = analyze_pr(diff_content)
431
+ print_analysis(analysis, show_files=args.stats)
432
+
433
+
434
+ if __name__ == '__main__':
435
+ main()