@safetnsr/vet 0.1.0 → 0.2.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/README.md CHANGED
@@ -13,49 +13,102 @@ works with Claude Code, Cursor, Copilot, Codex, Aider, Windsurf, Cline — anyth
13
13
  | check | what | how |
14
14
  |-------|------|-----|
15
15
  | **ready** | is your codebase AI-friendly? | scans structure, docs, types, tests |
16
- | **diff** | did the AI leave anti-patterns? | checks staged/unstaged changes for secrets, stubs, empty catches |
17
- | **models** | using deprecated AI models? | scans code for sunset model strings |
16
+ | **diff** | did the AI leave anti-patterns? | AI-specific patterns: wholesale rewrites, orphaned imports, catch-alls, over-commenting, plus secrets & stubs |
17
+ | **models** | using deprecated AI models? | scans code for sunset model strings across OpenAI, Anthropic, Google, Cohere |
18
18
  | **links** | broken markdown links? | validates relative links and wikilinks |
19
- | **config** | agent configs in place? | checks for CLAUDE.md, .cursorrules, copilot-instructions, etc. |
19
+ | **config** | agent configs in place? | deep analysis of CLAUDE.md, .cursorrules, copilot-instructions — checks completeness, consistency, and specificity against your actual codebase |
20
20
  | **history** | git patterns healthy? | analyzes commit churn, AI attribution, large changes |
21
21
 
22
22
  ## usage
23
23
 
24
24
  ```bash
25
- # run all checks (default)
25
+ # run all checks
26
26
  npx @safetnsr/vet
27
27
 
28
28
  # check a specific directory
29
29
  npx @safetnsr/vet ./my-project
30
30
 
31
+ # auto-fix: generate CLAUDE.md, .cursorrules, fix deprecated models
32
+ npx @safetnsr/vet --fix
33
+
34
+ # check specific commit range
35
+ npx @safetnsr/vet --since HEAD~5
36
+
37
+ # live monitoring during AI sessions
38
+ npx @safetnsr/vet --watch
39
+
31
40
  # CI mode — exit code 1 if score below threshold
32
41
  npx @safetnsr/vet --ci
33
42
 
34
- # auto-fix what we can
35
- npx @safetnsr/vet --fix
36
-
37
43
  # JSON output
38
44
  npx @safetnsr/vet --json
39
45
 
40
- # set up config + agent files + pre-commit hook
46
+ # generate configs + pre-commit hook
41
47
  npx @safetnsr/vet init
42
48
  ```
43
49
 
44
50
  ## output
45
51
 
46
52
  ```
47
- my-project 8.2/10
53
+ my-project 6.2/10
48
54
 
49
- ready ████████░░ 8 structure + docs look good
50
- diff ██████████ 10 clean diff, no issues
55
+ ready ████░░░░░░ 4 3 readiness issues
56
+ diff ████████░░ 8 3 issues (2 AI-specific) in 5 files
51
57
  models ██████████ 10 all models current
52
58
  links ██████░░░░ 6 3 broken links in docs/
53
- config ████████░░ 8 CLAUDE.md missing react patterns
54
- history ████████░░ 8 2 high-churn files
59
+ config ███░░░░░░░ 3 Cursor needs work (3/10)
60
+ history █████████░ 9 41 commits (~15% AI-attributed)
61
+
62
+ ✗ no README — AI agents have no project context
63
+ ✗ no tests — AI agents produce better code when tests exist
64
+ ! [ai] wholesale rewrite: 40 lines removed, 45 added in utils.ts
65
+ ! [ai] imported "lodash" but never used in new code
55
66
 
56
67
  run --fix to auto-repair 4 issues
57
68
  ```
58
69
 
70
+ ## --fix
71
+
72
+ `vet --fix` doesn't just scaffold — it analyzes your codebase and generates project-specific configs:
73
+
74
+ ```bash
75
+ $ npx @safetnsr/vet --fix
76
+
77
+ vet --fix
78
+
79
+ + CLAUDE.md (generated from codebase: Next.js + React, Vitest, Tailwind CSS, TypeScript)
80
+ + .cursorrules (generated)
81
+ ✓ src/api.ts: "gpt-3.5-turbo" → "gpt-4o-mini"
82
+
83
+ fixed 3 issues
84
+ ```
85
+
86
+ the generated CLAUDE.md includes your actual stack, directory structure, and framework-specific rules — not generic boilerplate.
87
+
88
+ ## AI-specific diff patterns
89
+
90
+ vet catches things that are specific to AI-generated code:
91
+
92
+ | pattern | what it catches |
93
+ |---------|----------------|
94
+ | `[ai] wholesale rewrite` | AI rewrote an entire function when a small edit would suffice |
95
+ | `[ai] orphaned imports` | AI added imports it never uses |
96
+ | `[ai] catch-all handling` | `catch(e) { console.error(e) }` instead of specific error handling |
97
+ | `[ai] comment density` | AI over-commented obvious code |
98
+ | `[ai] empty test body` | AI stubbed a test without implementation |
99
+ | `[ai] trivial assertion` | `expect(true).toBe(true)` — test proves nothing |
100
+
101
+ ## config analysis
102
+
103
+ the config check does deep analysis — not just "does CLAUDE.md exist":
104
+
105
+ ```
106
+ config score breakdown:
107
+ completeness: 4/10 — mentions typescript but not react, vitest
108
+ consistency: 7/10 — "strict TS" but tsconfig.strict is false
109
+ specificity: 3/10 — generic rules, nothing project-specific
110
+ ```
111
+
59
112
  ## config
60
113
 
61
114
  create `.vetrc` in your project root (optional):
@@ -68,14 +121,6 @@ create `.vetrc` in your project root (optional):
68
121
  }
69
122
  ```
70
123
 
71
- ## init
72
-
73
- `npx @safetnsr/vet init` creates:
74
- - `.vetrc` with sensible defaults
75
- - `CLAUDE.md` generated from your codebase
76
- - `.cursorrules` matching your project
77
- - `.git/hooks/pre-commit` that runs vet before every commit
78
-
79
124
  ## ci
80
125
 
81
126
  ```yaml
@@ -11,92 +11,196 @@ const AGENT_CONFIGS = {
11
11
  windsurf: { files: ['.windsurfrules'], name: 'Windsurf' },
12
12
  cline: { files: ['.clinerules', '.cline/settings.json'], name: 'Cline' },
13
13
  };
14
+ function analyzeConfig(cwd, configFile, agentName, files) {
15
+ const content = readFile(join(cwd, configFile)) || '';
16
+ const contentLower = content.toLowerCase();
17
+ const suggestions = [];
18
+ // Existence: it exists, so 10
19
+ const existence = 10;
20
+ // Completeness: does it cover the project's actual stack?
21
+ let completenessScore = 5; // base
22
+ let completenessChecks = 0;
23
+ let completenessHits = 0;
24
+ const pkgJson = readFile(join(cwd, 'package.json'));
25
+ const deps = {};
26
+ let projectName = '';
27
+ if (pkgJson) {
28
+ try {
29
+ const pkg = JSON.parse(pkgJson);
30
+ projectName = pkg.name || '';
31
+ Object.assign(deps, pkg.dependencies, pkg.devDependencies);
32
+ }
33
+ catch { /* */ }
34
+ }
35
+ // Framework detection + config coverage
36
+ const frameworkMap = {
37
+ react: { keywords: ['react', 'jsx', 'tsx', 'component', 'hook', 'usestate', 'useeffect'], category: 'UI framework' },
38
+ next: { keywords: ['next', 'nextjs', 'app router', 'pages router', 'server component'], category: 'framework' },
39
+ vue: { keywords: ['vue', 'composition api', 'options api', 'ref(', 'reactive'], category: 'UI framework' },
40
+ svelte: { keywords: ['svelte', 'sveltekit', '$:'], category: 'UI framework' },
41
+ express: { keywords: ['express', 'middleware', 'router', 'req, res'], category: 'backend' },
42
+ hono: { keywords: ['hono', 'c.json', 'c.text'], category: 'backend' },
43
+ fastify: { keywords: ['fastify', 'schema', 'route'], category: 'backend' },
44
+ vitest: { keywords: ['vitest', 'describe', 'it(', 'expect', 'test('], category: 'testing' },
45
+ jest: { keywords: ['jest', 'describe', 'it(', 'expect', 'test('], category: 'testing' },
46
+ tailwind: { keywords: ['tailwind', 'className', 'tw-'], category: 'styling' },
47
+ prisma: { keywords: ['prisma', 'schema.prisma', 'prismaClient'], category: 'database' },
48
+ drizzle: { keywords: ['drizzle', 'drizzle-orm'], category: 'database' },
49
+ };
50
+ for (const [dep, info] of Object.entries(frameworkMap)) {
51
+ if (deps[dep] || deps[`@${dep}/core`] || deps[`${dep}-dom`]) {
52
+ completenessChecks++;
53
+ if (info.keywords.some(k => contentLower.includes(k))) {
54
+ completenessHits++;
55
+ }
56
+ else {
57
+ suggestions.push(`add ${dep} conventions (${info.category} detected in dependencies)`);
58
+ }
59
+ }
60
+ }
61
+ if (completenessChecks > 0) {
62
+ completenessScore = Math.round((completenessHits / completenessChecks) * 10);
63
+ }
64
+ // Consistency: cross-reference with actual project config
65
+ let consistencyScore = 10;
66
+ const tsconfig = readFile(join(cwd, 'tsconfig.json'));
67
+ if (tsconfig) {
68
+ try {
69
+ const tc = JSON.parse(tsconfig);
70
+ const strict = tc.compilerOptions?.strict;
71
+ if (contentLower.includes('strict') && strict === false) {
72
+ consistencyScore -= 4;
73
+ suggestions.push('config says "strict" but tsconfig.strict is false — resolve contradiction');
74
+ }
75
+ if (contentLower.includes('esm') && tc.compilerOptions?.module?.toLowerCase()?.includes('commonjs')) {
76
+ consistencyScore -= 3;
77
+ suggestions.push('config mentions ESM but tsconfig uses CommonJS');
78
+ }
79
+ }
80
+ catch { /* */ }
81
+ }
82
+ // Check if config mentions testing but no test framework installed
83
+ if ((contentLower.includes('test') || contentLower.includes('spec')) && !deps.vitest && !deps.jest && !deps.mocha && !deps.ava) {
84
+ consistencyScore -= 2;
85
+ suggestions.push('config mentions tests but no test framework in dependencies');
86
+ }
87
+ // Specificity: generic platitudes vs project-specific rules
88
+ let specificityScore = 5;
89
+ const genericPhrases = [
90
+ 'keep functions small', 'write clean code', 'follow best practices',
91
+ 'use meaningful names', 'handle errors', 'write tests', 'be consistent',
92
+ 'follow conventions', 'keep it simple',
93
+ ];
94
+ let genericCount = 0;
95
+ for (const phrase of genericPhrases) {
96
+ if (contentLower.includes(phrase))
97
+ genericCount++;
98
+ }
99
+ // Specific indicators: file paths, function names, patterns, architecture
100
+ const specificIndicators = [
101
+ /\.(ts|js|py|rs|go)\b/, // mentions specific file types with context
102
+ /src\/|lib\/|app\/|pages\/|components\//, // directory structure
103
+ /import .+ from/, // code examples
104
+ /```/, // code blocks
105
+ /\bapi\/|route|endpoint/i, // API patterns
106
+ /\bmigration|schema|model\b/i, // data patterns
107
+ ];
108
+ let specificCount = 0;
109
+ for (const pattern of specificIndicators) {
110
+ if (pattern.test(content))
111
+ specificCount++;
112
+ }
113
+ if (genericCount > 3 && specificCount < 2) {
114
+ specificityScore = 2;
115
+ suggestions.push('mostly generic rules — add project-specific conventions, file paths, architecture patterns');
116
+ }
117
+ else if (specificCount >= 4) {
118
+ specificityScore = 9;
119
+ }
120
+ else if (specificCount >= 2) {
121
+ specificityScore = 6;
122
+ }
123
+ // Length-based adjustments
124
+ if (content.length < 100) {
125
+ specificityScore = Math.min(specificityScore, 2);
126
+ completenessScore = Math.min(completenessScore, 2);
127
+ suggestions.push('config is very sparse — add project context, conventions, and constraints');
128
+ }
129
+ else if (content.length < 300) {
130
+ specificityScore = Math.min(specificityScore, 5);
131
+ suggestions.push('config could be richer — consider adding architecture decisions and code patterns');
132
+ }
133
+ return {
134
+ file: configFile,
135
+ agent: agentName,
136
+ length: content.length,
137
+ existence,
138
+ completeness: Math.max(0, completenessScore),
139
+ consistency: Math.max(0, consistencyScore),
140
+ specificity: Math.max(0, specificityScore),
141
+ suggestions,
142
+ };
143
+ }
14
144
  export function checkConfig(cwd, ignore) {
15
145
  const issues = [];
16
146
  const files = walkFiles(cwd, ignore);
17
147
  // Detect which agents have config
18
- const detected = [];
19
- const missing = [];
148
+ const analyses = [];
20
149
  for (const [agent, info] of Object.entries(AGENT_CONFIGS)) {
21
- const found = info.files.some(f => fileExists(join(cwd, f)));
22
- if (found)
23
- detected.push(info.name);
150
+ for (const configFile of info.files) {
151
+ if (fileExists(join(cwd, configFile))) {
152
+ analyses.push(analyzeConfig(cwd, configFile, info.name, files));
153
+ }
154
+ }
24
155
  }
25
- if (detected.length === 0) {
156
+ if (analyses.length === 0) {
26
157
  issues.push({
27
- severity: 'warning',
28
- message: 'no AI agent config found — add CLAUDE.md, .cursorrules, or similar to guide AI behavior',
158
+ severity: 'error',
159
+ message: 'no AI agent config found — add CLAUDE.md, .cursorrules, or similar',
29
160
  fixable: true,
30
161
  fixHint: 'run vet init to generate agent config',
31
162
  });
163
+ return {
164
+ name: 'config',
165
+ score: 1,
166
+ maxScore: 10,
167
+ issues,
168
+ summary: 'no agent configs — critically under-configured',
169
+ };
32
170
  }
33
- // Check quality of found configs
34
- for (const [agent, info] of Object.entries(AGENT_CONFIGS)) {
35
- for (const configFile of info.files) {
36
- const content = readFile(join(cwd, configFile));
37
- if (!content)
38
- continue;
39
- // Too short to be useful
40
- if (content.length < 50) {
41
- issues.push({
42
- severity: 'warning',
43
- message: `${configFile} is only ${content.length} chars probably too sparse to guide AI effectively`,
44
- file: configFile,
45
- fixable: false,
46
- });
47
- }
48
- // Check if config mentions key project patterns
49
- const pkgJson = readFile(join(cwd, 'package.json'));
50
- if (pkgJson) {
51
- try {
52
- const pkg = JSON.parse(pkgJson);
53
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
54
- // Major frameworks that should be mentioned
55
- const frameworks = {
56
- react: ['react', 'jsx', 'tsx', 'component'],
57
- next: ['next', 'nextjs', 'app router', 'pages router'],
58
- vue: ['vue', 'composition api', 'options api'],
59
- svelte: ['svelte', 'sveltekit'],
60
- express: ['express', 'middleware', 'router'],
61
- hono: ['hono'],
62
- fastify: ['fastify'],
63
- django: ['django'],
64
- flask: ['flask'],
65
- };
66
- const contentLower = content.toLowerCase();
67
- for (const [framework, keywords] of Object.entries(frameworks)) {
68
- if (deps[framework] && !keywords.some(k => contentLower.includes(k))) {
69
- issues.push({
70
- severity: 'info',
71
- message: `${configFile} doesn't mention ${framework} — but it's in your dependencies`,
72
- file: configFile,
73
- fixable: false,
74
- });
75
- }
76
- }
77
- }
78
- catch { /* invalid package.json, skip */ }
79
- }
80
- }
171
+ // Aggregate scores from best config
172
+ const best = analyses.reduce((a, b) => (a.completeness + a.consistency + a.specificity) > (b.completeness + b.consistency + b.specificity) ? a : b);
173
+ // Generate issues from analysis
174
+ if (best.completeness < 5) {
175
+ issues.push({ severity: 'warning', message: `${best.file}: low completeness (${best.completeness}/10) — doesn't mention key dependencies`, fixable: true, fixHint: 'run vet --fix to enrich' });
176
+ }
177
+ if (best.consistency < 7) {
178
+ issues.push({ severity: 'warning', message: `${best.file}: consistency issues (${best.consistency}/10) contradicts project config`, fixable: false });
179
+ }
180
+ if (best.specificity < 5) {
181
+ issues.push({ severity: 'warning', message: `${best.file}: too generic (${best.specificity}/10)add project-specific rules`, fixable: true, fixHint: 'run vet --fix to add specifics' });
81
182
  }
82
- // Check for .gitignore (agents need to know what to ignore)
183
+ if (best.length < 100) {
184
+ issues.push({ severity: 'warning', message: `${best.file}: only ${best.length} chars — too sparse to guide AI`, fixable: true });
185
+ }
186
+ for (const suggestion of best.suggestions.slice(0, 5)) {
187
+ issues.push({ severity: 'info', message: suggestion, file: best.file, fixable: false });
188
+ }
189
+ // Check .gitignore
83
190
  if (!fileExists(join(cwd, '.gitignore'))) {
84
- issues.push({
85
- severity: 'info',
86
- message: 'no .gitignore — agents may create files in wrong directories',
87
- fixable: false,
88
- });
191
+ issues.push({ severity: 'warning', message: 'no .gitignore — agents may write to wrong directories', fixable: false });
89
192
  }
90
- const errors = issues.filter(i => i.severity === 'error').length;
91
- const warnings = issues.filter(i => i.severity === 'warning').length;
92
- const infos = issues.filter(i => i.severity === 'info').length;
93
- const score = Math.max(0, Math.min(10, 10 - errors * 2 - warnings * 1.5 - infos * 0.3));
94
- const configSummary = detected.length > 0 ? `configs: ${detected.join(', ')}` : 'no agent configs';
193
+ // Score: weighted average of sub-scores
194
+ const subScore = (best.existence * 0.2 + best.completeness * 0.3 + best.consistency * 0.25 + best.specificity * 0.25);
195
+ const gitignorePenalty = fileExists(join(cwd, '.gitignore')) ? 0 : 1;
196
+ const finalScore = Math.max(0, Math.min(10, subScore - gitignorePenalty));
197
+ const agents = analyses.map(a => a.agent);
198
+ const uniqueAgents = [...new Set(agents)];
95
199
  return {
96
200
  name: 'config',
97
- score: Math.round(score * 10) / 10,
201
+ score: Math.round(finalScore * 10) / 10,
98
202
  maxScore: 10,
99
203
  issues,
100
- summary: issues.length === 0 ? `${configSummary} well configured` : `${configSummary} ${issues.length} suggestions`,
204
+ summary: `${uniqueAgents.join(', ')} ${best.completeness >= 7 && best.specificity >= 7 ? 'well configured' : `needs work (${Math.round(finalScore)}/10)`}`,
101
205
  };
102
206
  }
@@ -1,2 +1,2 @@
1
- import type { CheckResult } from '../types.js';
2
- export declare function checkDiff(cwd: string): CheckResult;
1
+ import type { CheckResult, DiffOptions } from '../types.js';
2
+ export declare function checkDiff(cwd: string, opts?: DiffOptions): CheckResult;
@@ -1,49 +1,52 @@
1
1
  import { git } from '../util.js';
2
- const PATTERNS = [
2
+ // Generic patterns (still useful but not the star)
3
+ const GENERIC_PATTERNS = [
3
4
  // Secrets
4
5
  { regex: /(?:api[_-]?key|secret|token|password|credential)\s*[:=]\s*['"][^'"]{8,}['"]/i, message: 'possible hardcoded secret', severity: 'error' },
5
6
  { regex: /sk-[a-zA-Z0-9]{20,}/, message: 'possible OpenAI API key', severity: 'error' },
6
7
  { regex: /AKIA[0-9A-Z]{16}/, message: 'possible AWS access key', severity: 'error' },
7
8
  { regex: /AIza[0-9A-Za-z_-]{35}/, message: 'possible Google API key', severity: 'error' },
8
- // AI anti-patterns
9
- { regex: /\/\/\s*TODO[:\s]/i, message: 'TODO comment left in code', severity: 'warning' },
10
- { regex: /\/\/\s*FIXME[:\s]/i, message: 'FIXME comment left in code', severity: 'warning' },
11
- { regex: /\/\/\s*HACK[:\s]/i, message: 'HACK comment left in code', severity: 'warning' },
12
- { regex: /console\.log\(/, message: 'console.log left in code', severity: 'warning' },
13
- { regex: /catch\s*\([^)]*\)\s*\{\s*\}/, message: 'empty catch block — error silently swallowed', severity: 'error' },
14
- { regex: /catch\s*\([^)]*\)\s*\{\s*\/\//, message: 'catch block with only a comment — errors need handling', severity: 'warning' },
15
- { regex: /it\(\s*['"].*['"]\s*,\s*(?:async\s*)?\(\)\s*=>\s*\{\s*\}\s*\)/, message: 'empty test body — stubbed test', severity: 'error' },
16
- { regex: /test\(\s*['"].*['"]\s*,\s*(?:async\s*)?\(\)\s*=>\s*\{\s*\}\s*\)/, message: 'empty test body — stubbed test', severity: 'error' },
17
- { regex: /expect\(true\)\.toBe\(true\)/, message: 'trivial assertion — test proves nothing', severity: 'error' },
18
- { regex: /assert\s+True\s*$/, message: 'trivial assertion — test proves nothing', severity: 'error' },
19
- { regex: /\.only\(/, message: '.only() left in test — other tests will be skipped', severity: 'error' },
20
- { regex: /debugger;/, message: 'debugger statement left in code', severity: 'error' },
9
+ { regex: /debugger;/, message: 'debugger statement', severity: 'error' },
10
+ { regex: /\.only\(/, message: '.only() left in test — other tests skipped', severity: 'error' },
21
11
  ];
22
- export function checkDiff(cwd) {
23
- const issues = [];
24
- // Get staged + unstaged diff
12
+ // AI-specific patterns — things AI agents do that humans typically don't
13
+ const AI_PATTERNS = [
14
+ // Empty/trivial tests
15
+ { regex: /(?:it|test)\(\s*['"].*['"]\s*,\s*(?:async\s*)?\(\)\s*=>\s*\{\s*\}\s*\)/, message: '[ai] empty test body — stubbed test', severity: 'error' },
16
+ { regex: /expect\(true\)\.toBe\(true\)/, message: '[ai] trivial assertion — test proves nothing', severity: 'error' },
17
+ { regex: /assert\s+True\s*$/, message: '[ai] trivial assertion', severity: 'error' },
18
+ // Catch-all error handling (AI defaults to generic catches)
19
+ { regex: /catch\s*\([^)]*\)\s*\{\s*\}/, message: '[ai] empty catch block — error silently swallowed', severity: 'error' },
20
+ { regex: /catch\s*\(\w+\)\s*\{\s*console\.(log|error)\(\w+\)\s*;?\s*\}/, message: '[ai] catch-all with just console.log — handle errors specifically', severity: 'warning' },
21
+ // Over-commenting (AI tends to add obvious comments)
22
+ { regex: /\/\/\s*(set|get|return|create|initialize|import|export|define)\s+(the|a)\s+/i, message: '[ai] obvious comment — "// get the value" adds no information', severity: 'warning' },
23
+ ];
24
+ function getDiff(cwd, opts) {
25
+ if (opts.since) {
26
+ return git(`diff ${opts.since}`, cwd);
27
+ }
28
+ // Default: last commit + working changes
25
29
  let diff = git('diff HEAD', cwd);
26
30
  if (!diff)
27
31
  diff = git('diff --cached', cwd);
28
32
  if (!diff)
29
33
  diff = git('diff', cwd);
30
- if (!diff) {
31
- return {
32
- name: 'diff',
33
- score: 10,
34
- maxScore: 10,
35
- issues: [],
36
- summary: 'no uncommitted changes to check',
37
- };
38
- }
39
- // Parse diff: only check added lines
40
- let currentFile = '';
34
+ // If still nothing, diff last commit against its parent
35
+ if (!diff)
36
+ diff = git('diff HEAD~1..HEAD', cwd);
37
+ return diff;
38
+ }
39
+ function parseDiff(diff) {
40
+ const files = [];
41
+ let current = null;
41
42
  let lineNum = 0;
42
43
  for (const line of diff.split('\n')) {
43
44
  if (line.startsWith('diff --git')) {
44
45
  const match = line.match(/b\/(.+)$/);
45
- if (match)
46
- currentFile = match[1];
46
+ if (match) {
47
+ current = { path: match[1], addedLines: [], removedLines: [], addedCount: 0, removedCount: 0 };
48
+ files.push(current);
49
+ }
47
50
  lineNum = 0;
48
51
  continue;
49
52
  }
@@ -53,46 +56,122 @@ export function checkDiff(cwd) {
53
56
  lineNum = parseInt(match[1]) - 1;
54
57
  continue;
55
58
  }
59
+ if (!current)
60
+ continue;
56
61
  if (line.startsWith('+') && !line.startsWith('+++')) {
57
62
  lineNum++;
58
- const added = line.slice(1);
59
- for (const pattern of PATTERNS) {
60
- if (pattern.regex.test(added)) {
61
- issues.push({
62
- severity: pattern.severity,
63
- message: pattern.message,
64
- file: currentFile,
65
- line: lineNum,
66
- fixable: false,
67
- });
68
- break; // one issue per line
63
+ current.addedLines.push({ num: lineNum, text: line.slice(1) });
64
+ current.addedCount++;
65
+ }
66
+ else if (line.startsWith('-') && !line.startsWith('---')) {
67
+ current.removedLines.push(line.slice(1));
68
+ current.removedCount++;
69
+ }
70
+ else {
71
+ lineNum++;
72
+ }
73
+ }
74
+ return files;
75
+ }
76
+ export function checkDiff(cwd, opts = {}) {
77
+ const issues = [];
78
+ const diff = getDiff(cwd, opts);
79
+ if (!diff) {
80
+ return { name: 'diff', score: 10, maxScore: 10, issues: [], summary: 'no changes to check' };
81
+ }
82
+ const files = parseDiff(diff);
83
+ const allPatterns = [...GENERIC_PATTERNS, ...AI_PATTERNS];
84
+ // Pattern matching on added lines
85
+ for (const file of files) {
86
+ for (const { num, text } of file.addedLines) {
87
+ for (const pattern of allPatterns) {
88
+ if (pattern.regex.test(text)) {
89
+ issues.push({ severity: pattern.severity, message: pattern.message, file: file.path, line: num, fixable: false });
90
+ break;
69
91
  }
70
92
  }
71
93
  }
72
- else if (!line.startsWith('-')) {
73
- lineNum++;
94
+ }
95
+ // AI-specific: wholesale function rewrite detection
96
+ for (const file of files) {
97
+ if (file.removedCount > 10 && file.addedCount > 10) {
98
+ const ratio = Math.min(file.removedCount, file.addedCount) / Math.max(file.removedCount, file.addedCount);
99
+ if (ratio > 0.7 && file.removedCount > 20) {
100
+ issues.push({
101
+ severity: 'warning',
102
+ message: `[ai] ${file.path}: ${file.removedCount} lines removed, ${file.addedCount} added — looks like a wholesale rewrite, verify intent`,
103
+ file: file.path,
104
+ fixable: false,
105
+ });
106
+ }
74
107
  }
75
108
  }
76
- // Check for deleted error handling
109
+ // AI-specific: orphaned imports (added import lines without corresponding usage)
110
+ for (const file of files) {
111
+ const addedImports = file.addedLines.filter(l => /^import\s/.test(l.text) || /^from\s/.test(l.text) || /require\(/.test(l.text));
112
+ for (const imp of addedImports) {
113
+ // Extract imported name
114
+ const nameMatch = imp.text.match(/import\s+(?:\{([^}]+)\}|(\w+))/);
115
+ if (nameMatch) {
116
+ const names = (nameMatch[1] || nameMatch[2] || '').split(',').map(n => n.trim().split(' as ').pop()?.trim()).filter(Boolean);
117
+ for (const name of names) {
118
+ if (!name || name.length < 2)
119
+ continue;
120
+ // Check if name is used in any other added line
121
+ const usedElsewhere = file.addedLines.some(l => l !== imp && l.text.includes(name));
122
+ if (!usedElsewhere && file.addedLines.length > 3) {
123
+ issues.push({
124
+ severity: 'warning',
125
+ message: `[ai] imported "${name}" but never used in new code`,
126
+ file: file.path,
127
+ line: imp.num,
128
+ fixable: false,
129
+ });
130
+ break; // one per import line
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ // AI-specific: comment density spike
137
+ for (const file of files) {
138
+ if (file.addedCount < 10)
139
+ continue;
140
+ const commentLines = file.addedLines.filter(l => /^\s*(\/\/|#|\/\*|\*)/.test(l.text)).length;
141
+ const ratio = commentLines / file.addedCount;
142
+ if (ratio > 0.4 && commentLines > 5) {
143
+ issues.push({
144
+ severity: 'info',
145
+ message: `[ai] ${file.path}: ${Math.round(ratio * 100)}% of new lines are comments — AI tends to over-comment`,
146
+ file: file.path,
147
+ fixable: false,
148
+ });
149
+ }
150
+ }
151
+ // Deleted error handling
77
152
  let deletedErrorHandling = 0;
78
- for (const line of diff.split('\n')) {
79
- if (line.startsWith('-') && !line.startsWith('---')) {
80
- if (/catch|throw|Error|reject|finally/.test(line)) {
153
+ for (const file of files) {
154
+ for (const line of file.removedLines) {
155
+ if (/catch|throw new|\.reject\(|finally\s*\{/.test(line))
81
156
  deletedErrorHandling++;
82
- }
83
157
  }
84
158
  }
85
159
  if (deletedErrorHandling > 3) {
86
- issues.push({ severity: 'warning', message: `${deletedErrorHandling} lines of error handling removed — verify this was intentional`, fixable: false });
160
+ issues.push({ severity: 'warning', message: `${deletedErrorHandling} lines of error handling removed — verify intentional`, fixable: false });
87
161
  }
162
+ // Recalibrated scoring
88
163
  const errors = issues.filter(i => i.severity === 'error').length;
89
164
  const warnings = issues.filter(i => i.severity === 'warning').length;
90
- const score = Math.max(0, Math.min(10, 10 - errors * 1.5 - warnings * 0.5));
165
+ const score = Math.max(0, Math.min(10, 10 - errors * 2 - warnings * 0.75));
166
+ const aiIssues = issues.filter(i => i.message.startsWith('[ai]')).length;
167
+ const totalFiles = files.length;
91
168
  return {
92
169
  name: 'diff',
93
170
  score: Math.round(score * 10) / 10,
94
171
  maxScore: 10,
95
172
  issues,
96
- summary: issues.length === 0 ? 'clean diff, no issues' : `${issues.length} issues in uncommitted changes`,
173
+ summary: issues.length === 0
174
+ ? `${totalFiles} file${totalFiles !== 1 ? 's' : ''} changed, clean`
175
+ : `${issues.length} issues (${aiIssues} AI-specific) in ${totalFiles} files`,
97
176
  };
98
177
  }
@@ -1,2 +1,2 @@
1
1
  import type { CheckResult } from '../types.js';
2
- export declare function checkModels(cwd: string, ignore: string[]): CheckResult;
2
+ export declare function checkModels(cwd: string, ignore: string[]): Promise<CheckResult>;