@safetnsr/vet 0.2.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/dist/checks/models.d.ts +1 -1
- package/dist/checks/models.js +49 -16
- package/dist/checks/ready.d.ts +1 -1
- package/dist/checks/ready.js +48 -9
- package/dist/cli.js +8 -7
- package/package.json +4 -1
package/dist/checks/models.d.ts
CHANGED
|
@@ -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>;
|
package/dist/checks/models.js
CHANGED
|
@@ -1,8 +1,44 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { readFile, walkFiles } from '../util.js';
|
|
3
|
-
//
|
|
3
|
+
// Try to use @safetnsr/model-graveyard if installed (248 models, alias matching, YAML registry)
|
|
4
|
+
async function tryModelGraveyard(cwd) {
|
|
5
|
+
try {
|
|
6
|
+
const mod = await import(/* webpackIgnore: true */ '@safetnsr/model-graveyard');
|
|
7
|
+
if (typeof mod.scan !== 'function')
|
|
8
|
+
return null;
|
|
9
|
+
const report = await mod.scan(cwd);
|
|
10
|
+
const issues = [];
|
|
11
|
+
for (const match of report.matches) {
|
|
12
|
+
if (!match.model)
|
|
13
|
+
continue;
|
|
14
|
+
if (match.model.status === 'deprecated' || match.model.status === 'eol') {
|
|
15
|
+
issues.push({
|
|
16
|
+
severity: 'error',
|
|
17
|
+
message: `${match.model.status} model "${match.raw}" in ${match.file}:${match.line}${match.model.successor ? ` — use "${match.model.successor}"` : ''}`,
|
|
18
|
+
file: match.file,
|
|
19
|
+
line: match.line,
|
|
20
|
+
fixable: !!match.model.successor,
|
|
21
|
+
fixHint: match.model.successor ? `replace "${match.raw}" with "${match.model.successor}"` : undefined,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const score = Math.max(0, 10 - issues.length * 2);
|
|
26
|
+
return {
|
|
27
|
+
name: 'models',
|
|
28
|
+
score: Math.min(10, score),
|
|
29
|
+
maxScore: 10,
|
|
30
|
+
issues,
|
|
31
|
+
summary: issues.length === 0
|
|
32
|
+
? `${report.filesScanned} files scanned (via model-graveyard) — all current`
|
|
33
|
+
: `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} (via model-graveyard)`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Built-in fallback: inline registry, basic string matching
|
|
4
41
|
const SUNSET_MODELS = {
|
|
5
|
-
// OpenAI
|
|
6
42
|
'gpt-3.5-turbo': { replacement: 'gpt-4o-mini', sunset: '2025-06' },
|
|
7
43
|
'gpt-4-turbo': { replacement: 'gpt-4o', sunset: '2025-04' },
|
|
8
44
|
'gpt-4-turbo-preview': { replacement: 'gpt-4o', sunset: '2025-04' },
|
|
@@ -12,7 +48,6 @@ const SUNSET_MODELS = {
|
|
|
12
48
|
'text-davinci-003': { replacement: 'gpt-4o-mini', sunset: '2024-01' },
|
|
13
49
|
'code-davinci-002': { replacement: 'gpt-4o', sunset: '2024-01' },
|
|
14
50
|
'text-embedding-ada-002': { replacement: 'text-embedding-3-small', sunset: '2025-04' },
|
|
15
|
-
// Anthropic
|
|
16
51
|
'claude-instant-1': { replacement: 'claude-sonnet-4-5', sunset: '2024-08' },
|
|
17
52
|
'claude-2': { replacement: 'claude-sonnet-4-5', sunset: '2024-08' },
|
|
18
53
|
'claude-2.0': { replacement: 'claude-sonnet-4-5', sunset: '2024-08' },
|
|
@@ -20,43 +55,36 @@ const SUNSET_MODELS = {
|
|
|
20
55
|
'claude-3-haiku-20240307': { replacement: 'claude-haiku-3-5', sunset: '2025-06' },
|
|
21
56
|
'claude-3-sonnet-20240229': { replacement: 'claude-sonnet-4-5', sunset: '2025-03' },
|
|
22
57
|
'claude-3-opus-20240229': { replacement: 'claude-opus-4-0', sunset: '2025-09' },
|
|
23
|
-
// Google
|
|
24
58
|
'gemini-pro': { replacement: 'gemini-2.0-flash', sunset: '2025-02' },
|
|
25
59
|
'gemini-1.0-pro': { replacement: 'gemini-2.0-flash', sunset: '2025-02' },
|
|
26
60
|
'gemini-1.5-pro': { replacement: 'gemini-2.5-pro', sunset: '2025-09' },
|
|
27
61
|
'gemini-1.5-flash': { replacement: 'gemini-2.0-flash', sunset: '2025-09' },
|
|
28
62
|
'text-bison': { replacement: 'gemini-2.0-flash', sunset: '2024-04' },
|
|
29
63
|
'chat-bison': { replacement: 'gemini-2.0-flash', sunset: '2024-04' },
|
|
30
|
-
// Cohere
|
|
31
|
-
'command': { replacement: 'command-r-plus', sunset: '2025-03' },
|
|
32
64
|
'command-light': { replacement: 'command-r', sunset: '2025-03' },
|
|
33
65
|
'command-nightly': { replacement: 'command-r-plus', sunset: '2025-03' },
|
|
34
66
|
};
|
|
35
67
|
const SCAN_EXTS = ['.ts', '.js', '.tsx', '.jsx', '.py', '.rs', '.go', '.java', '.rb', '.php',
|
|
36
68
|
'.yaml', '.yml', '.json', '.toml', '.env', '.env.example', '.env.local', '.cfg', '.ini', '.conf'];
|
|
37
|
-
// Files that contain model registries should not trigger false positives
|
|
38
69
|
const SELF_IGNORE = ['models.ts', 'models.js', 'model-graveyard', 'model-registry', 'sunset'];
|
|
39
|
-
// Short model names that need context to avoid false positives (e.g. npm "command" field)
|
|
40
70
|
const CONTEXT_REQUIRED = new Set(['command', 'command-light', 'command-nightly']);
|
|
41
71
|
function hasModelContext(content, model) {
|
|
42
|
-
// Require the model name to appear in a string-like context: quotes, assignment, or near "model"/"engine"
|
|
43
72
|
const escaped = model.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
73
|
const contextPatterns = [
|
|
45
|
-
new RegExp(`['"\`]${escaped}['"\`]`),
|
|
46
|
-
new RegExp(`model[_\\s]*[:=].*${escaped}`, 'i'),
|
|
47
|
-
new RegExp(`engine[_\\s]*[:=].*${escaped}`, 'i'),
|
|
48
|
-
new RegExp(`${escaped}.*(?:api|llm|chat|completion)`, 'i'),
|
|
74
|
+
new RegExp(`['"\`]${escaped}['"\`]`),
|
|
75
|
+
new RegExp(`model[_\\s]*[:=].*${escaped}`, 'i'),
|
|
76
|
+
new RegExp(`engine[_\\s]*[:=].*${escaped}`, 'i'),
|
|
77
|
+
new RegExp(`${escaped}.*(?:api|llm|chat|completion)`, 'i'),
|
|
49
78
|
];
|
|
50
79
|
return contextPatterns.some(p => p.test(content));
|
|
51
80
|
}
|
|
52
|
-
|
|
81
|
+
function builtinModels(cwd, ignore) {
|
|
53
82
|
const issues = [];
|
|
54
83
|
const files = walkFiles(cwd, ignore);
|
|
55
84
|
const found = new Map();
|
|
56
85
|
for (const f of files) {
|
|
57
86
|
if (!SCAN_EXTS.some(ext => f.endsWith(ext)))
|
|
58
87
|
continue;
|
|
59
|
-
// Skip files that are model registries themselves
|
|
60
88
|
if (SELF_IGNORE.some(s => f.toLowerCase().includes(s)))
|
|
61
89
|
continue;
|
|
62
90
|
const content = readFile(join(cwd, f));
|
|
@@ -65,7 +93,6 @@ export function checkModels(cwd, ignore) {
|
|
|
65
93
|
for (const [model, info] of Object.entries(SUNSET_MODELS)) {
|
|
66
94
|
if (!content.includes(model))
|
|
67
95
|
continue;
|
|
68
|
-
// For short/ambiguous names, require contextual evidence
|
|
69
96
|
if (CONTEXT_REQUIRED.has(model) && !hasModelContext(content, model))
|
|
70
97
|
continue;
|
|
71
98
|
const existing = found.get(model) || [];
|
|
@@ -93,3 +120,9 @@ export function checkModels(cwd, ignore) {
|
|
|
93
120
|
summary: issues.length === 0 ? 'all model references current' : `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} found`,
|
|
94
121
|
};
|
|
95
122
|
}
|
|
123
|
+
export async function checkModels(cwd, ignore) {
|
|
124
|
+
const rich = await tryModelGraveyard(cwd);
|
|
125
|
+
if (rich)
|
|
126
|
+
return rich;
|
|
127
|
+
return builtinModels(cwd, ignore);
|
|
128
|
+
}
|
package/dist/checks/ready.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CheckResult } from '../types.js';
|
|
2
|
-
export declare function checkReady(cwd: string, ignore: string[]): CheckResult
|
|
2
|
+
export declare function checkReady(cwd: string, ignore: string[]): Promise<CheckResult>;
|
package/dist/checks/ready.js
CHANGED
|
@@ -1,28 +1,64 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { readFile, walkFiles } from '../util.js';
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
// Try to use @safetnsr/ai-ready if installed (richer per-file analysis)
|
|
4
|
+
async function tryAiReady(cwd) {
|
|
5
|
+
try {
|
|
6
|
+
const mod = await import(/* webpackIgnore: true */ '@safetnsr/ai-ready');
|
|
7
|
+
if (typeof mod.main !== 'function')
|
|
8
|
+
return null;
|
|
9
|
+
const result = mod.main(['--json', cwd]);
|
|
10
|
+
if (!result || result.exitCode !== 0)
|
|
11
|
+
return null;
|
|
12
|
+
const data = JSON.parse(result.output);
|
|
13
|
+
const issues = [];
|
|
14
|
+
// Convert ai-ready's per-file results to vet issues
|
|
15
|
+
if (data.files) {
|
|
16
|
+
const lowScoreFiles = data.files.filter((f) => f.score < 5);
|
|
17
|
+
for (const f of lowScoreFiles.slice(0, 5)) {
|
|
18
|
+
issues.push({
|
|
19
|
+
severity: 'warning',
|
|
20
|
+
message: `${f.file}: readiness ${f.score}/10 — ${f.reasons?.join(', ') || 'low score'}`,
|
|
21
|
+
file: f.file,
|
|
22
|
+
fixable: false,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (lowScoreFiles.length > 5) {
|
|
26
|
+
issues.push({ severity: 'info', message: `...and ${lowScoreFiles.length - 5} more low-readiness files`, fixable: false });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Map ai-ready score to vet format
|
|
30
|
+
const score = typeof data.score === 'number' ? data.score : 5;
|
|
31
|
+
return {
|
|
32
|
+
name: 'ready',
|
|
33
|
+
score: Math.round(Math.min(10, score) * 10) / 10,
|
|
34
|
+
maxScore: 10,
|
|
35
|
+
issues,
|
|
36
|
+
summary: `${data.files?.length || 0} files analyzed (via ai-ready) — ${issues.length} issues`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Built-in fallback: simpler project-level checks
|
|
44
|
+
function builtinReady(cwd, ignore) {
|
|
5
45
|
const issues = [];
|
|
6
46
|
const files = walkFiles(cwd, ignore);
|
|
7
|
-
// 1. README exists — critical for AI context
|
|
8
47
|
const hasReadme = files.some(f => /^readme\.(md|txt|rst)$/i.test(f));
|
|
9
48
|
if (!hasReadme) {
|
|
10
49
|
issues.push({ severity: 'error', message: 'no README — AI agents have no project context', fixable: true, fixHint: 'create a README.md' });
|
|
11
50
|
}
|
|
12
|
-
// 2. Project manifest
|
|
13
51
|
const manifests = ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle', 'Gemfile', 'composer.json'];
|
|
14
52
|
const hasManifest = manifests.some(m => files.includes(m));
|
|
15
53
|
if (!hasManifest) {
|
|
16
54
|
issues.push({ severity: 'error', message: 'no package manifest — agents can\'t resolve dependencies', fixable: false });
|
|
17
55
|
}
|
|
18
|
-
// 3. Test coverage
|
|
19
56
|
const codeExts = ['.ts', '.js', '.tsx', '.jsx', '.py', '.rs', '.go', '.java', '.rb', '.php', '.cs', '.swift', '.kt'];
|
|
20
57
|
const testFiles = files.filter(f => /\.(test|spec)\.(ts|js|tsx|jsx|py)$/.test(f) || f.includes('__tests__/') || f.startsWith('tests/') || f.startsWith('test/'));
|
|
21
58
|
const codeFiles = files.filter(f => codeExts.some(ext => f.endsWith(ext)));
|
|
22
59
|
if (codeFiles.length > 5 && testFiles.length === 0) {
|
|
23
60
|
issues.push({ severity: 'error', message: 'no tests — AI agents produce better code when tests exist to validate against', fixable: false });
|
|
24
61
|
}
|
|
25
|
-
// 4. Overly large files (>500 lines)
|
|
26
62
|
let largeFileCount = 0;
|
|
27
63
|
for (const f of files) {
|
|
28
64
|
if (!codeExts.some(ext => f.endsWith(ext)))
|
|
@@ -38,19 +74,16 @@ export function checkReady(cwd, ignore) {
|
|
|
38
74
|
if (largeFileCount > 3) {
|
|
39
75
|
issues.push({ severity: 'warning', message: `...and ${largeFileCount - 3} more large files`, fixable: false });
|
|
40
76
|
}
|
|
41
|
-
// 5. .env without .env.example
|
|
42
77
|
const hasEnv = files.some(f => f === '.env' || f === '.env.local');
|
|
43
78
|
const hasEnvExample = files.some(f => f === '.env.example' || f === '.env.template');
|
|
44
79
|
if (hasEnv && !hasEnvExample) {
|
|
45
80
|
issues.push({ severity: 'warning', message: '.env exists but no .env.example — AI agents can\'t see env structure', fixable: false });
|
|
46
81
|
}
|
|
47
|
-
// 6. No types in JS-heavy project
|
|
48
82
|
const tsFiles = files.filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
49
83
|
const jsFiles = files.filter(f => f.endsWith('.js') || f.endsWith('.jsx'));
|
|
50
84
|
if (jsFiles.length > 10 && tsFiles.length === 0 && files.includes('package.json')) {
|
|
51
85
|
issues.push({ severity: 'info', message: `${jsFiles.length} JS files, no TypeScript — typed code gives agents better context`, fixable: false });
|
|
52
86
|
}
|
|
53
|
-
// Recalibrated scoring: errors = -3, warnings = -1.5, info = -0.3
|
|
54
87
|
const errors = issues.filter(i => i.severity === 'error').length;
|
|
55
88
|
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
56
89
|
const infos = issues.filter(i => i.severity === 'info').length;
|
|
@@ -63,3 +96,9 @@ export function checkReady(cwd, ignore) {
|
|
|
63
96
|
summary: issues.length === 0 ? 'codebase is well-structured for AI' : `${issues.length} readiness issues`,
|
|
64
97
|
};
|
|
65
98
|
}
|
|
99
|
+
export async function checkReady(cwd, ignore) {
|
|
100
|
+
const rich = await tryAiReady(cwd);
|
|
101
|
+
if (rich)
|
|
102
|
+
return rich;
|
|
103
|
+
return builtinReady(cwd, ignore);
|
|
104
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -103,16 +103,17 @@ if (isFix) {
|
|
|
103
103
|
console.log(`\n ${totalFixed > 0 ? c.green : c.dim}fixed ${totalFixed} issue${totalFixed !== 1 ? 's' : ''}${c.reset}\n`);
|
|
104
104
|
process.exit(0);
|
|
105
105
|
}
|
|
106
|
-
function runChecks() {
|
|
106
|
+
async function runChecks() {
|
|
107
107
|
const allChecks = ['ready', 'diff', 'models', 'links', 'config', 'history'];
|
|
108
108
|
const enabledChecks = config.checks || allChecks;
|
|
109
109
|
const results = [];
|
|
110
|
+
// ready and models are async (try rich subpackages first, fallback to built-in)
|
|
110
111
|
if (enabledChecks.includes('ready'))
|
|
111
|
-
results.push(checkReady(cwd, ignore));
|
|
112
|
+
results.push(await checkReady(cwd, ignore));
|
|
112
113
|
if (enabledChecks.includes('diff'))
|
|
113
114
|
results.push(checkDiff(cwd, { since }));
|
|
114
115
|
if (enabledChecks.includes('models'))
|
|
115
|
-
results.push(checkModels(cwd, ignore));
|
|
116
|
+
results.push(await checkModels(cwd, ignore));
|
|
116
117
|
if (enabledChecks.includes('links'))
|
|
117
118
|
results.push(checkLinks(cwd, ignore));
|
|
118
119
|
if (enabledChecks.includes('config'))
|
|
@@ -124,7 +125,7 @@ function runChecks() {
|
|
|
124
125
|
// --watch mode
|
|
125
126
|
if (isWatch) {
|
|
126
127
|
console.clear();
|
|
127
|
-
let result = runChecks();
|
|
128
|
+
let result = await runChecks();
|
|
128
129
|
console.log(reportPretty(result));
|
|
129
130
|
console.log(` ${c.dim}watching for changes... (ctrl+c to stop)${c.reset}\n`);
|
|
130
131
|
let debounce = null;
|
|
@@ -137,9 +138,9 @@ if (isWatch) {
|
|
|
137
138
|
return;
|
|
138
139
|
if (debounce)
|
|
139
140
|
clearTimeout(debounce);
|
|
140
|
-
debounce = setTimeout(() => {
|
|
141
|
+
debounce = setTimeout(async () => {
|
|
141
142
|
console.clear();
|
|
142
|
-
result = runChecks();
|
|
143
|
+
result = await runChecks();
|
|
143
144
|
console.log(reportPretty(result));
|
|
144
145
|
console.log(` ${c.dim}watching for changes... (ctrl+c to stop)${c.reset}\n`);
|
|
145
146
|
}, 500);
|
|
@@ -156,7 +157,7 @@ if (isWatch) {
|
|
|
156
157
|
}
|
|
157
158
|
else {
|
|
158
159
|
// Normal run
|
|
159
|
-
const result = runChecks();
|
|
160
|
+
const result = await runChecks();
|
|
160
161
|
if (isJSON) {
|
|
161
162
|
console.log(reportJSON(result));
|
|
162
163
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@safetnsr/vet",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "vet your AI-generated code — one command, six checks, zero config",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,5 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"optionalDependencies": {
|
|
43
|
+
"@safetnsr/model-graveyard": "^0.2.0"
|
|
41
44
|
}
|
|
42
45
|
}
|