@safetnsr/vet 1.6.1 → 1.7.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.
@@ -1,5 +1,7 @@
1
1
  import type { CheckResult, CategoryResult, VetResult } from './types.js';
2
2
  export declare function toGrade(score: number): string;
3
+ /** Apply a floor of 20 to non-security checks that have no security-related errors */
4
+ export declare function applyScoreFloor(check: CheckResult): number;
3
5
  export declare function buildCategories(checkMap: {
4
6
  security: CheckResult[];
5
7
  integrity: CheckResult[];
@@ -21,11 +21,20 @@ const WEIGHTS = {
21
21
  debt: 0.25,
22
22
  deps: 0.15,
23
23
  };
24
+ // ── Scoring floor for non-security checks ────────────────────────────────────
25
+ const SECURITY_CHECKS = new Set(['scan', 'secrets', 'permissions', 'owasp']);
26
+ /** Apply a floor of 20 to non-security checks that have no security-related errors */
27
+ export function applyScoreFloor(check) {
28
+ if (SECURITY_CHECKS.has(check.name))
29
+ return check.score;
30
+ // Non-security check: minimum score is 20
31
+ return Math.max(20, check.score);
32
+ }
24
33
  // ── Average scores within a category ────────────────────────────────────────
25
34
  function averageScore(checks) {
26
35
  if (checks.length === 0)
27
36
  return 100;
28
- const total = checks.reduce((sum, c) => sum + c.score, 0);
37
+ const total = checks.reduce((sum, c) => sum + applyScoreFloor(c), 0);
29
38
  return Math.round(total / checks.length);
30
39
  }
31
40
  // ── Group checks into categories ─────────────────────────────────────────────
@@ -1,5 +1,50 @@
1
- import { join } from 'node:path';
1
+ import { join, basename } from 'node:path';
2
2
  import { readFile, walkFiles } from '../util.js';
3
+ // ── AI framework detection ───────────────────────────────────────────────────
4
+ const AI_NAME_KEYWORDS = ['ai', 'llm', 'model', 'openai', 'anthropic', 'langchain', 'provider'];
5
+ const AI_PKG_KEYWORDS = new Set(['ai', 'llm', 'language-model', 'openai', 'anthropic']);
6
+ function isAiFramework(cwd) {
7
+ // Check package.json
8
+ const pkgRaw = readFile(join(cwd, 'package.json'));
9
+ if (pkgRaw) {
10
+ try {
11
+ const pkg = JSON.parse(pkgRaw);
12
+ const name = (pkg.name || '').toLowerCase();
13
+ if (AI_NAME_KEYWORDS.some(k => name.includes(k)))
14
+ return true;
15
+ if (Array.isArray(pkg.keywords) && pkg.keywords.some((k) => AI_PKG_KEYWORDS.has(k.toLowerCase())))
16
+ return true;
17
+ }
18
+ catch { /* skip */ }
19
+ }
20
+ // Check pyproject.toml / setup.py for AI deps
21
+ const pyproject = readFile(join(cwd, 'pyproject.toml'));
22
+ if (pyproject) {
23
+ const aiDeps = ['openai', 'anthropic', 'langchain', 'transformers', 'torch', 'tensorflow'];
24
+ if (aiDeps.some(d => pyproject.includes(d)))
25
+ return true;
26
+ }
27
+ const setupPy = readFile(join(cwd, 'setup.py'));
28
+ if (setupPy) {
29
+ const aiDeps = ['openai', 'anthropic', 'langchain', 'transformers', 'torch', 'tensorflow'];
30
+ if (aiDeps.some(d => setupPy.includes(d)))
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ // ── Test/example/docs path detection ─────────────────────────────────────────
36
+ const TEST_DOCS_PATTERNS = ['test/', 'tests/', '__tests__/', 'examples/', 'docs/'];
37
+ function isTestOrDocsFile(filePath) {
38
+ const normalized = filePath.replace(/\\/g, '/');
39
+ if (TEST_DOCS_PATTERNS.some(p => normalized.includes(p) || normalized.startsWith(p)))
40
+ return true;
41
+ const base = basename(filePath);
42
+ if (/\.(test|spec)\./i.test(base))
43
+ return true;
44
+ if (base.endsWith('.md'))
45
+ return true;
46
+ return false;
47
+ }
3
48
  // Try to use @safetnsr/model-graveyard if installed (248 models, alias matching, YAML registry)
4
49
  async function tryModelGraveyard(cwd) {
5
50
  try {
@@ -8,6 +53,7 @@ async function tryModelGraveyard(cwd) {
8
53
  return null;
9
54
  const report = await mod.scan(cwd);
10
55
  const issues = [];
56
+ const aiFramework = isAiFramework(cwd);
11
57
  // Files that define deprecated model registries should not be flagged
12
58
  const SELF_FILES = ['models.ts', 'models.js', 'model-graveyard', 'model-registry', 'sunset', 'fix/models'];
13
59
  for (const match of report.matches) {
@@ -17,8 +63,10 @@ async function tryModelGraveyard(cwd) {
17
63
  if (match.file && SELF_FILES.some(s => match.file.toLowerCase().includes(s)))
18
64
  continue;
19
65
  if (match.model.status === 'deprecated' || match.model.status === 'eol') {
66
+ const inTestDocs = match.file && isTestOrDocsFile(match.file);
67
+ const severity = (aiFramework || inTestDocs) ? 'info' : 'error';
20
68
  issues.push({
21
- severity: 'error',
69
+ severity,
22
70
  message: `${match.model.status} model "${match.raw}" in ${match.file}:${match.line}${match.model.successor ? ` — use "${match.model.successor}"` : ''}`,
23
71
  file: match.file,
24
72
  line: match.line,
@@ -27,15 +75,22 @@ async function tryModelGraveyard(cwd) {
27
75
  });
28
76
  }
29
77
  }
30
- const score = Math.max(0, 100 - issues.length * 20);
78
+ const errorCount = issues.filter(i => i.severity === 'error').length;
79
+ const score = aiFramework
80
+ ? Math.max(70, 100 - errorCount * 20)
81
+ : Math.max(0, 100 - errorCount * 20);
82
+ let summary = issues.length === 0
83
+ ? `${report.filesScanned} files scanned (via model-graveyard) — all current`
84
+ : `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} (via model-graveyard)`;
85
+ if (aiFramework) {
86
+ summary += ' — AI framework detected — model references are expected';
87
+ }
31
88
  return {
32
89
  name: 'models',
33
90
  score: Math.min(100, score),
34
91
  maxScore: 100,
35
92
  issues,
36
- summary: issues.length === 0
37
- ? `${report.filesScanned} files scanned (via model-graveyard) — all current`
38
- : `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} (via model-graveyard)`,
93
+ summary,
39
94
  };
40
95
  }
41
96
  catch {
@@ -87,6 +142,7 @@ function builtinModels(cwd, ignore) {
87
142
  const issues = [];
88
143
  const files = walkFiles(cwd, ignore);
89
144
  const found = new Map();
145
+ const aiFramework = isAiFramework(cwd);
90
146
  for (const f of files) {
91
147
  if (!SCAN_EXTS.some(ext => f.endsWith(ext)))
92
148
  continue;
@@ -105,24 +161,42 @@ function builtinModels(cwd, ignore) {
105
161
  found.set(model, existing);
106
162
  }
107
163
  }
108
- for (const [model, files] of found) {
164
+ for (const [model, modelFiles] of found) {
109
165
  const info = SUNSET_MODELS[model];
110
- const fileList = files.length <= 2 ? files.join(', ') : `${files[0]} +${files.length - 1} more`;
166
+ const fileList = modelFiles.length <= 2 ? modelFiles.join(', ') : `${modelFiles[0]} +${modelFiles.length - 1} more`;
167
+ // Determine severity: downgrade for AI frameworks or test/docs files
168
+ const allInTestDocs = modelFiles.every(f => isTestOrDocsFile(f));
169
+ const severity = (aiFramework || allInTestDocs) ? 'info' : 'error';
111
170
  issues.push({
112
- severity: 'error',
171
+ severity,
113
172
  message: `deprecated model "${model}" in ${fileList} — use "${info.replacement}"${info.sunset ? ` (sunset ${info.sunset})` : ''}`,
114
- file: files[0],
173
+ file: modelFiles[0],
115
174
  fixable: true,
116
175
  fixHint: `replace "${model}" with "${info.replacement}"`,
117
176
  });
118
177
  }
119
- const score = Math.max(0, 100 - issues.length * 20);
178
+ let score;
179
+ if (aiFramework) {
180
+ // AI framework: models exist for compatibility, score 70+ base
181
+ const errorCount = issues.filter(i => i.severity === 'error').length;
182
+ score = Math.max(70, 100 - errorCount * 20);
183
+ }
184
+ else {
185
+ const errorCount = issues.filter(i => i.severity === 'error').length;
186
+ score = Math.max(0, 100 - errorCount * 20);
187
+ }
188
+ let summary = issues.length === 0
189
+ ? 'all model references current'
190
+ : `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} found`;
191
+ if (aiFramework) {
192
+ summary += ' — AI framework detected — model references are expected';
193
+ }
120
194
  return {
121
195
  name: 'models',
122
196
  score: Math.min(100, score),
123
197
  maxScore: 100,
124
198
  issues,
125
- summary: issues.length === 0 ? 'all model references current' : `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} found`,
199
+ summary,
126
200
  };
127
201
  }
128
202
  export async function checkModels(cwd, ignore) {
@@ -85,9 +85,10 @@ const CONFIG_TARGETS = [
85
85
  '.claude', 'CLAUDE.md', 'AGENTS.md',
86
86
  '.cursorrules', '.cursor',
87
87
  '.github',
88
- '.aider.conf.yml',
88
+ 'copilot-instructions.md',
89
+ '.aider.conf.yml', '.aider',
89
90
  '.continue',
90
- '.mcp',
91
+ '.mcp', 'mcp.json', '.mcp.json',
91
92
  '.roomodes', '.roo',
92
93
  ];
93
94
  // ── File helpers (delegated to util.ts) ──────────────────────────────────────
@@ -1,4 +1,4 @@
1
- import { join, basename } from 'node:path';
1
+ import { join, basename, extname } from 'node:path';
2
2
  import { readFileSync, existsSync, statSync } from 'node:fs';
3
3
  import { execSync } from 'node:child_process';
4
4
  // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -10,9 +10,48 @@ function safeExec(cmd, cwd) {
10
10
  return '';
11
11
  }
12
12
  }
13
+ // ── Config/meta file exclusion for thin-file checks ──────────────────────────
14
+ const CONFIG_DOTFILES = new Set([
15
+ '.gitignore', '.gitattributes', '.nvmrc', '.node-version', '.editorconfig',
16
+ '.prettierrc', '.eslintignore', '.npmrc', '.npmignore',
17
+ ]);
18
+ const CONFIG_EXTENSIONS = new Set([
19
+ '.yml', '.yaml', '.json', '.toml', '.cfg', '.ini', '.lock', '.svg', '.xml',
20
+ ]);
21
+ const CONFIG_DIRS = ['.github/', '.husky/', '.vscode/', '.idea/'];
22
+ const META_FILES = new Set([
23
+ 'FUNDING.yaml', 'CODEOWNERS', 'LICENSE',
24
+ ]);
25
+ function isConfigOrMetaFile(filePath) {
26
+ const base = basename(filePath);
27
+ const ext = extname(filePath).toLowerCase();
28
+ const normalized = filePath.replace(/\\/g, '/');
29
+ // Dotfiles
30
+ if (CONFIG_DOTFILES.has(base))
31
+ return true;
32
+ // Config extensions
33
+ if (CONFIG_EXTENSIONS.has(ext))
34
+ return true;
35
+ // Config directories
36
+ if (CONFIG_DIRS.some(d => normalized.includes(d) || normalized.startsWith(d)))
37
+ return true;
38
+ // Meta files
39
+ if (META_FILES.has(base))
40
+ return true;
41
+ if (base.startsWith('CHANGELOG'))
42
+ return true;
43
+ return false;
44
+ }
45
+ // ── Code file extensions for test detection ──────────────────────────────────
46
+ const CODE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']);
47
+ const TEST_FILE_PATTERN = /\.(test|spec)\.(ts|js|tsx|jsx)$/i;
13
48
  function isTestFile(filePath) {
14
49
  const base = basename(filePath);
15
- if (/\.(test|spec)\.[a-z]+$/i.test(base))
50
+ const ext = extname(filePath).toLowerCase();
51
+ // Only code files can be test files
52
+ if (!CODE_EXTENSIONS.has(ext))
53
+ return false;
54
+ if (TEST_FILE_PATTERN.test(base))
16
55
  return true;
17
56
  const normalized = filePath.replace(/\\/g, '/');
18
57
  // Match __tests__/ anywhere in path (including at root)
@@ -186,6 +225,11 @@ export function checkVerify(cwd, since) {
186
225
  verified++;
187
226
  continue;
188
227
  }
228
+ // Skip thin file check for config/meta files (they're supposed to be small)
229
+ if (isConfigOrMetaFile(relPath)) {
230
+ verified++;
231
+ continue;
232
+ }
189
233
  if (lineCount < 10 && lineCount > 0) {
190
234
  issues.push({
191
235
  severity: 'warning',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safetnsr/vet",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "vet your AI-generated code — one command, one score card, one letter grade",
5
5
  "type": "module",
6
6
  "bin": {