@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.
@@ -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>;
@@ -1,8 +1,44 @@
1
1
  import { join } from 'node:path';
2
2
  import { readFile, walkFiles } from '../util.js';
3
- // Model sunset/deprecation registry kept inline for zero deps
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}['"\`]`), // quoted
46
- new RegExp(`model[_\\s]*[:=].*${escaped}`, 'i'), // model assignment
47
- new RegExp(`engine[_\\s]*[:=].*${escaped}`, 'i'), // engine assignment
48
- new RegExp(`${escaped}.*(?:api|llm|chat|completion)`, 'i'), // near API terms
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
- export function checkModels(cwd, ignore) {
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
+ }
@@ -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>;
@@ -1,28 +1,64 @@
1
1
  import { join } from 'node:path';
2
2
  import { readFile, walkFiles } from '../util.js';
3
- // Codebase AI-readiness: structure, complexity, documentation
4
- export function checkReady(cwd, ignore) {
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.0",
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
  }