@safetnsr/vet 0.6.0 → 1.1.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.
@@ -0,0 +1,25 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare const AGENT_CONFIG_FILES: string[];
3
+ export declare function parseAgentConfigs(cwd: string): string[];
4
+ export declare function extractRefs(content: string, cwd: string): string[];
5
+ export type VisibilityTier = 'config' | 'visible' | 'invisible';
6
+ export interface ClassifiedFile {
7
+ path: string;
8
+ tier: VisibilityTier;
9
+ }
10
+ export declare function classifyFiles(cwd: string, configPaths: string[], refs: string[]): ClassifiedFile[];
11
+ export interface MapResult {
12
+ config: string[];
13
+ visible: string[];
14
+ invisible: string[];
15
+ stats: {
16
+ total: number;
17
+ visible_pct: number;
18
+ };
19
+ }
20
+ export declare function checkMap(cwd: string): Promise<CheckResult & {
21
+ mapData: MapResult;
22
+ }>;
23
+ export declare function renderMapReport(result: CheckResult & {
24
+ mapData: MapResult;
25
+ }, asJson: boolean): string;
@@ -0,0 +1,256 @@
1
+ import { join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { walkFiles, readFile, c } from '../util.js';
4
+ // ── Agent config filenames to discover ───────────────────────────────────────
5
+ export const AGENT_CONFIG_FILES = [
6
+ 'CLAUDE.md',
7
+ 'AGENTS.md',
8
+ '.cursorrules',
9
+ 'codex.md',
10
+ '.github/copilot-instructions.md',
11
+ 'cursor.json',
12
+ '.cursor/rules',
13
+ 'copilot-instructions.md',
14
+ ];
15
+ // ── Parse all agent config files present in cwd ──────────────────────────────
16
+ export function parseAgentConfigs(cwd) {
17
+ const found = [];
18
+ for (const name of AGENT_CONFIG_FILES) {
19
+ const full = join(cwd, name);
20
+ if (existsSync(full)) {
21
+ found.push(name);
22
+ }
23
+ }
24
+ return found;
25
+ }
26
+ // ── Extract file/dir references from config file content ─────────────────────
27
+ export function extractRefs(content, cwd) {
28
+ const refs = new Set();
29
+ // Patterns to extract:
30
+ // 1. Backtick paths: `path/to/file.ts` or `./path`
31
+ const backtickPat = /`([^`\s]+)`/g;
32
+ // 2. Inline code in markdown: single-line code with path-like content
33
+ const codePat = /`([./][^`\s]+)`/g;
34
+ // 3. Explicit path patterns in text: word/word or ./word or ~/word
35
+ const pathPat = /(?:^|\s)((?:\.\/|\.\.\/|~\/)?(?:[a-zA-Z0-9_-]+\/)+[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])/gm;
36
+ // 4. Absolute paths starting with /
37
+ const absPat = /(?:^|\s)(\/(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+)/gm;
38
+ const extractFromPattern = (pat) => {
39
+ let match;
40
+ pat.lastIndex = 0;
41
+ while ((match = pat.exec(content)) !== null) {
42
+ const raw = match[1].trim();
43
+ // Skip URLs
44
+ if (raw.startsWith('http://') || raw.startsWith('https://') || raw.includes('://'))
45
+ continue;
46
+ // Skip if looks like a domain
47
+ if (/^[a-z]+\.[a-z]{2,}/.test(raw) && !raw.includes('/'))
48
+ continue;
49
+ refs.add(raw);
50
+ }
51
+ };
52
+ extractFromPattern(backtickPat);
53
+ extractFromPattern(codePat);
54
+ extractFromPattern(pathPat);
55
+ extractFromPattern(absPat);
56
+ // Filter to only refs that actually exist on disk (relative to cwd)
57
+ const resolved = [];
58
+ for (const ref of refs) {
59
+ let resolvedPath;
60
+ if (ref.startsWith('/')) {
61
+ resolvedPath = ref;
62
+ }
63
+ else if (ref.startsWith('~/')) {
64
+ resolvedPath = join(process.env.HOME || '/root', ref.slice(2));
65
+ }
66
+ else {
67
+ resolvedPath = join(cwd, ref);
68
+ }
69
+ if (existsSync(resolvedPath)) {
70
+ // Store as relative to cwd
71
+ const rel = ref.startsWith('/') ? ref : ref;
72
+ resolved.push(rel.replace(/^\.\//, ''));
73
+ }
74
+ }
75
+ return [...new Set(resolved)];
76
+ }
77
+ // ── Classify all codebase files ───────────────────────────────────────────────
78
+ export function classifyFiles(cwd, configPaths, refs) {
79
+ const allFiles = walkFiles(cwd);
80
+ const configSet = new Set(configPaths);
81
+ // Build a set of ref prefixes for directory matching
82
+ const refSet = new Set(refs);
83
+ // Also include files whose parent directory is referenced
84
+ function isReferencedByRef(file) {
85
+ if (refSet.has(file))
86
+ return true;
87
+ // Check if any ref is a directory prefix of this file
88
+ for (const ref of refSet) {
89
+ if (file.startsWith(ref + '/') || file.startsWith(ref.replace(/\/$/, '') + '/')) {
90
+ return true;
91
+ }
92
+ }
93
+ return false;
94
+ }
95
+ const classified = [];
96
+ for (const file of allFiles) {
97
+ let tier;
98
+ if (configSet.has(file)) {
99
+ tier = 'config';
100
+ }
101
+ else if (isReferencedByRef(file)) {
102
+ tier = 'visible';
103
+ }
104
+ else {
105
+ tier = 'invisible';
106
+ }
107
+ classified.push({ path: file, tier });
108
+ }
109
+ // Also add config files that walkFiles might have missed (e.g. .github/copilot-instructions.md)
110
+ for (const cp of configPaths) {
111
+ if (!classified.find(f => f.path === cp)) {
112
+ classified.push({ path: cp, tier: 'config' });
113
+ }
114
+ }
115
+ return classified;
116
+ }
117
+ // ── Main check ───────────────────────────────────────────────────────────────
118
+ export async function checkMap(cwd) {
119
+ const issues = [];
120
+ // Discover agent configs
121
+ const configPaths = parseAgentConfigs(cwd);
122
+ // Extract all refs from all config files
123
+ const allRefs = [];
124
+ for (const cp of configPaths) {
125
+ const content = readFile(join(cwd, cp));
126
+ if (content) {
127
+ const refs = extractRefs(content, cwd);
128
+ allRefs.push(...refs);
129
+ }
130
+ }
131
+ const uniqueRefs = [...new Set(allRefs)];
132
+ // Classify files
133
+ const classified = classifyFiles(cwd, configPaths, uniqueRefs);
134
+ const configFiles = classified.filter(f => f.tier === 'config').map(f => f.path);
135
+ const visibleFiles = classified.filter(f => f.tier === 'visible').map(f => f.path);
136
+ const invisibleFiles = classified.filter(f => f.tier === 'invisible').map(f => f.path);
137
+ const total = classified.length;
138
+ const visible = visibleFiles.length + configFiles.length;
139
+ const visible_pct = total > 0 ? Math.round((visible / total) * 100) : 0;
140
+ // Issues
141
+ if (configPaths.length === 0) {
142
+ issues.push({
143
+ severity: 'warning',
144
+ message: 'no agent config files found (CLAUDE.md, .cursorrules, etc.) — agent has no guided context',
145
+ fixable: true,
146
+ fixHint: 'run: npx @safetnsr/vet init',
147
+ });
148
+ }
149
+ if (visible_pct < 20 && total > 0) {
150
+ issues.push({
151
+ severity: 'warning',
152
+ message: `agent is mostly blind: only ${visible_pct}% of codebase is referenced in agent configs`,
153
+ fixable: false,
154
+ });
155
+ }
156
+ // Surface top invisible dirs as info
157
+ const invisibleDirs = new Set();
158
+ for (const f of invisibleFiles) {
159
+ const parts = f.split('/');
160
+ if (parts.length > 1)
161
+ invisibleDirs.add(parts[0]);
162
+ }
163
+ const topInvisibleDirs = [...invisibleDirs].slice(0, 5);
164
+ if (topInvisibleDirs.length > 0 && invisibleFiles.length > 0) {
165
+ issues.push({
166
+ severity: 'info',
167
+ message: `top invisible directories: ${topInvisibleDirs.join(', ')}`,
168
+ fixable: false,
169
+ });
170
+ }
171
+ const mapData = {
172
+ config: configFiles,
173
+ visible: visibleFiles,
174
+ invisible: invisibleFiles,
175
+ stats: { total, visible_pct },
176
+ };
177
+ const summary = configPaths.length === 0
178
+ ? `no agent config files — all ${total} files invisible`
179
+ : `${visible_pct}% visible to agent (${visible}/${total} files)`;
180
+ return {
181
+ name: 'map',
182
+ score: visible_pct,
183
+ maxScore: 100,
184
+ issues,
185
+ summary,
186
+ mapData,
187
+ };
188
+ }
189
+ // ── Terminal renderer ─────────────────────────────────────────────────────────
190
+ export function renderMapReport(result, asJson) {
191
+ const { mapData } = result;
192
+ if (asJson) {
193
+ return JSON.stringify({
194
+ config: mapData.config,
195
+ visible: mapData.visible,
196
+ invisible: mapData.invisible,
197
+ stats: mapData.stats,
198
+ }, null, 2);
199
+ }
200
+ const lines = [];
201
+ lines.push('');
202
+ lines.push(` ${c.bold}vet map${c.reset} — agent visibility\n`);
203
+ lines.push(` ${c.dim}score:${c.reset} ${c.bold}${mapData.stats.visible_pct}%${c.reset} visible to agent`);
204
+ lines.push(` ${c.dim}total:${c.reset} ${mapData.stats.total} files`);
205
+ lines.push('');
206
+ // Config files tier
207
+ if (mapData.config.length > 0) {
208
+ lines.push(` ${c.yellow}${c.bold}config files${c.reset} ${c.dim}(${mapData.config.length})${c.reset}`);
209
+ for (const f of mapData.config.slice(0, 10)) {
210
+ lines.push(` ${c.yellow}●${c.reset} ${c.dim}${f}${c.reset}`);
211
+ }
212
+ if (mapData.config.length > 10) {
213
+ lines.push(` ${c.dim} ... and ${mapData.config.length - 10} more${c.reset}`);
214
+ }
215
+ lines.push('');
216
+ }
217
+ // Visible files tier
218
+ if (mapData.visible.length > 0) {
219
+ lines.push(` ${c.green}${c.bold}visible to agent${c.reset} ${c.dim}(${mapData.visible.length})${c.reset}`);
220
+ for (const f of mapData.visible.slice(0, 15)) {
221
+ lines.push(` ${c.green}●${c.reset} ${f}`);
222
+ }
223
+ if (mapData.visible.length > 15) {
224
+ lines.push(` ${c.dim} ... and ${mapData.visible.length - 15} more${c.reset}`);
225
+ }
226
+ lines.push('');
227
+ }
228
+ // Invisible dirs summary
229
+ if (mapData.invisible.length > 0) {
230
+ const invisibleDirs = new Map();
231
+ for (const f of mapData.invisible) {
232
+ const parts = f.split('/');
233
+ const dir = parts.length > 1 ? parts[0] : '(root)';
234
+ invisibleDirs.set(dir, (invisibleDirs.get(dir) || 0) + 1);
235
+ }
236
+ const sortedDirs = [...invisibleDirs.entries()].sort((a, b) => b[1] - a[1]);
237
+ lines.push(` ${c.dim}${c.bold}invisible to agent${c.reset} ${c.dim}(${mapData.invisible.length} files)${c.reset}`);
238
+ for (const [dir, count] of sortedDirs.slice(0, 8)) {
239
+ lines.push(` ${c.dim}○ ${dir}/ (${count} files)${c.reset}`);
240
+ }
241
+ if (sortedDirs.length > 8) {
242
+ lines.push(` ${c.dim} ... and ${sortedDirs.length - 8} more directories${c.reset}`);
243
+ }
244
+ lines.push('');
245
+ }
246
+ // Issues
247
+ for (const issue of result.issues) {
248
+ const icon = issue.severity === 'warning' ? c.yellow + '⚠' : c.dim + 'ℹ';
249
+ lines.push(` ${icon}${c.reset} ${issue.message}`);
250
+ if (issue.fixHint)
251
+ lines.push(` ${c.dim}→ ${issue.fixHint}${c.reset}`);
252
+ }
253
+ if (result.issues.length > 0)
254
+ lines.push('');
255
+ return lines.join('\n');
256
+ }
@@ -0,0 +1,2 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare function checkMemory(cwd: string): CheckResult;
@@ -0,0 +1,275 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
3
+ // ── Memory file targets ──────────────────────────────────────────────────────
4
+ const ROOT_FILES = ['CLAUDE.md', 'AGENTS.md', 'SOUL.md', '.cursorrules', 'codex.md'];
5
+ const MEMORY_DIR = 'memory';
6
+ const DAILY_DIR = join(MEMORY_DIR, 'daily');
7
+ const MAX_DAILY_FILES = 30;
8
+ // ── Tool categories for contradiction detection ─────────────────────────────
9
+ const TOOL_CATEGORIES = {
10
+ 'test framework': [/\bvitest\b/i, /\bjest\b/i, /\bmocha\b/i, /\bnode:test\b/i, /\bava\b/i],
11
+ 'package manager': [/\bnpm\b/, /\byarn\b/, /\bpnpm\b/, /\bbun\b/],
12
+ };
13
+ // ── Helpers ──────────────────────────────────────────────────────────────────
14
+ function safeRead(path) {
15
+ try {
16
+ return readFileSync(path, 'utf-8');
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function collectMemoryFiles(cwd) {
23
+ const files = [];
24
+ // Root-level memory files
25
+ for (const name of ROOT_FILES) {
26
+ const full = join(cwd, name);
27
+ if (existsSync(full))
28
+ files.push(full);
29
+ }
30
+ // memory/*.md
31
+ const memDir = join(cwd, MEMORY_DIR);
32
+ if (existsSync(memDir) && statSync(memDir).isDirectory()) {
33
+ try {
34
+ for (const entry of readdirSync(memDir)) {
35
+ if (!entry.endsWith('.md'))
36
+ continue;
37
+ const full = join(memDir, entry);
38
+ try {
39
+ if (statSync(full).isFile())
40
+ files.push(full);
41
+ }
42
+ catch { /* skip */ }
43
+ }
44
+ }
45
+ catch { /* skip */ }
46
+ }
47
+ // memory/daily/*.md (capped)
48
+ const dailyDir = join(cwd, DAILY_DIR);
49
+ if (existsSync(dailyDir) && statSync(dailyDir).isDirectory()) {
50
+ try {
51
+ const entries = readdirSync(dailyDir).filter(e => e.endsWith('.md')).sort().reverse();
52
+ for (const entry of entries.slice(0, MAX_DAILY_FILES)) {
53
+ const full = join(dailyDir, entry);
54
+ try {
55
+ if (statSync(full).isFile())
56
+ files.push(full);
57
+ }
58
+ catch { /* skip */ }
59
+ }
60
+ }
61
+ catch { /* skip */ }
62
+ }
63
+ return files;
64
+ }
65
+ /** Extract @scope/package references */
66
+ function extractScopedPackages(content) {
67
+ const results = [];
68
+ const lines = content.split('\n');
69
+ for (let i = 0; i < lines.length; i++) {
70
+ const matches = lines[i].matchAll(/@[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+/g);
71
+ for (const m of matches) {
72
+ results.push({ pkg: m[0], line: i + 1 });
73
+ }
74
+ }
75
+ return results;
76
+ }
77
+ /** Extract file/dir path references */
78
+ function extractPaths(content) {
79
+ const results = [];
80
+ const lines = content.split('\n');
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const line = lines[i];
83
+ // Skip lines that are purely URLs
84
+ // Match absolute paths
85
+ const absMatches = line.matchAll(/(?:^|\s|["`'(])(\/((?:var|home|usr|etc|opt|tmp|root|srv|mnt)[^\s"'`),;]*))(?=[\s"'`),;]|$)/g);
86
+ for (const m of absMatches) {
87
+ const p = m[1].replace(/[.,:;)]+$/, '');
88
+ if (p.startsWith('//') || p.includes('://'))
89
+ continue;
90
+ if (p.length < 4)
91
+ continue;
92
+ results.push({ path: p, line: i + 1 });
93
+ }
94
+ // Match relative paths starting with ./ or ../
95
+ const relMatches = line.matchAll(/(?:^|\s|["`'(])(\.\.?\/[^\s"'`),;]+)/g);
96
+ for (const m of relMatches) {
97
+ const p = m[1].replace(/[.,:;)]+$/, '');
98
+ if (p.includes('://'))
99
+ continue;
100
+ results.push({ path: p, line: i + 1 });
101
+ }
102
+ }
103
+ return results;
104
+ }
105
+ /** Extract tool mentions per category */
106
+ function extractToolMentions(content, fileName) {
107
+ const mentions = new Map();
108
+ const lines = content.split('\n');
109
+ for (const [category, patterns] of Object.entries(TOOL_CATEGORIES)) {
110
+ for (let i = 0; i < lines.length; i++) {
111
+ for (const regex of patterns) {
112
+ if (regex.test(lines[i])) {
113
+ const toolMatch = lines[i].match(regex);
114
+ if (toolMatch) {
115
+ const existing = mentions.get(category) || [];
116
+ existing.push({ tool: toolMatch[0].toLowerCase(), file: fileName, line: i + 1 });
117
+ mentions.set(category, existing);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return mentions;
124
+ }
125
+ /** Count meaningful fact claims in a file */
126
+ function countFacts(content) {
127
+ let count = 0;
128
+ const lines = content.split('\n');
129
+ for (const line of lines) {
130
+ const trimmed = line.trim();
131
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('---') || trimmed.startsWith('```'))
132
+ continue;
133
+ // A "fact" is a line with at least some substantive content
134
+ if (trimmed.length > 15 && /[a-zA-Z]/.test(trimmed)) {
135
+ // Contains a keyword-like pattern (assignment, instruction, reference)
136
+ if (/[:=→\->]|use |stack|requires?|install|run |npm |config|version|path|file|dir|tool/i.test(trimmed)) {
137
+ count++;
138
+ }
139
+ }
140
+ }
141
+ return count;
142
+ }
143
+ // ── Main check ───────────────────────────────────────────────────────────────
144
+ export function checkMemory(cwd) {
145
+ const memoryFiles = collectMemoryFiles(cwd);
146
+ const issues = [];
147
+ let deductions = 0;
148
+ if (memoryFiles.length === 0) {
149
+ return {
150
+ name: 'memory',
151
+ score: 100,
152
+ maxScore: 100,
153
+ issues: [],
154
+ summary: 'no agent memory files found',
155
+ };
156
+ }
157
+ // Load package.json deps
158
+ const pkgPath = join(cwd, 'package.json');
159
+ const allDeps = new Set();
160
+ if (existsSync(pkgPath)) {
161
+ try {
162
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
163
+ for (const key of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
164
+ if (pkg[key]) {
165
+ for (const name of Object.keys(pkg[key])) {
166
+ allDeps.add(name);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ catch { /* skip */ }
172
+ }
173
+ // Collect all tool mentions across files for contradiction detection
174
+ const globalToolMentions = new Map();
175
+ for (const filePath of memoryFiles) {
176
+ const content = safeRead(filePath);
177
+ if (!content)
178
+ continue;
179
+ const relPath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
180
+ // 1. Stale package references
181
+ if (allDeps.size > 0) {
182
+ const pkgRefs = extractScopedPackages(content);
183
+ for (const { pkg, line } of pkgRefs) {
184
+ if (!allDeps.has(pkg)) {
185
+ issues.push({
186
+ severity: 'warning',
187
+ message: `Stale package: ${pkg} not in package.json`,
188
+ file: relPath,
189
+ line,
190
+ fixable: false,
191
+ fixHint: 'Remove or update this reference',
192
+ });
193
+ deductions += 10;
194
+ }
195
+ }
196
+ }
197
+ // 2. Broken path references
198
+ const pathRefs = extractPaths(content);
199
+ for (const { path: p, line } of pathRefs) {
200
+ const resolved = p.startsWith('/') ? p : resolve(cwd, p);
201
+ if (!existsSync(resolved)) {
202
+ issues.push({
203
+ severity: 'error',
204
+ message: `Broken path reference: ${p}`,
205
+ file: relPath,
206
+ line,
207
+ fixable: false,
208
+ fixHint: 'Remove or update this path reference',
209
+ });
210
+ deductions += 15;
211
+ }
212
+ }
213
+ // 3. Collect tool mentions for contradiction check
214
+ const toolMentions = extractToolMentions(content, relPath);
215
+ for (const [category, mentions] of toolMentions) {
216
+ const existing = globalToolMentions.get(category) || [];
217
+ existing.push(...mentions);
218
+ globalToolMentions.set(category, existing);
219
+ }
220
+ // 4. Bloat check
221
+ if (content.length > 5000) {
222
+ const factCount = countFacts(content);
223
+ if (factCount < 3) {
224
+ issues.push({
225
+ severity: 'info',
226
+ message: `Bloated memory file: ${content.length} chars but only ${factCount} fact claims`,
227
+ file: relPath,
228
+ line: 1,
229
+ fixable: false,
230
+ fixHint: 'Trim this file to only essential facts',
231
+ });
232
+ deductions += 5;
233
+ }
234
+ }
235
+ }
236
+ // 3. Contradiction detection
237
+ for (const [category, mentions] of globalToolMentions) {
238
+ const uniqueTools = new Map();
239
+ for (const m of mentions) {
240
+ if (!uniqueTools.has(m.tool)) {
241
+ uniqueTools.set(m.tool, { file: m.file, line: m.line });
242
+ }
243
+ }
244
+ if (uniqueTools.size > 1) {
245
+ const tools = [...uniqueTools.entries()];
246
+ for (let i = 0; i < tools.length; i++) {
247
+ for (let j = i + 1; j < tools.length; j++) {
248
+ // Only flag if they're in different files
249
+ if (tools[i][1].file !== tools[j][1].file) {
250
+ issues.push({
251
+ severity: 'warning',
252
+ message: `Contradiction in ${category}: "${tools[i][0]}" in ${tools[i][1].file} vs "${tools[j][0]}" in ${tools[j][1].file}`,
253
+ file: tools[i][1].file,
254
+ line: tools[i][1].line,
255
+ fixable: false,
256
+ fixHint: `Standardize on one ${category} across memory files`,
257
+ });
258
+ deductions += 10;
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ const finalScore = Math.max(0, 100 - deductions);
265
+ const issueCount = issues.length;
266
+ return {
267
+ name: 'memory',
268
+ score: finalScore,
269
+ maxScore: 100,
270
+ issues,
271
+ summary: issueCount === 0
272
+ ? `${memoryFiles.length} memory file${memoryFiles.length !== 1 ? 's' : ''} scanned, clean`
273
+ : `${issueCount} stale fact${issueCount !== 1 ? 's' : ''} found in agent memory files`,
274
+ };
275
+ }
@@ -8,9 +8,14 @@ async function tryModelGraveyard(cwd) {
8
8
  return null;
9
9
  const report = await mod.scan(cwd);
10
10
  const issues = [];
11
+ // Files that define deprecated model registries should not be flagged
12
+ const SELF_FILES = ['models.ts', 'models.js', 'model-graveyard', 'model-registry', 'sunset', 'fix/models'];
11
13
  for (const match of report.matches) {
12
14
  if (!match.model)
13
15
  continue;
16
+ // Skip self-referencing files (model definition/fix files)
17
+ if (match.file && SELF_FILES.some(s => match.file.toLowerCase().includes(s)))
18
+ continue;
14
19
  if (match.model.status === 'deprecated' || match.model.status === 'eol') {
15
20
  issues.push({
16
21
  severity: 'error',
@@ -22,11 +27,11 @@ async function tryModelGraveyard(cwd) {
22
27
  });
23
28
  }
24
29
  }
25
- const score = Math.max(0, 10 - issues.length * 2);
30
+ const score = Math.max(0, 100 - issues.length * 20);
26
31
  return {
27
32
  name: 'models',
28
- score: Math.min(10, score),
29
- maxScore: 10,
33
+ score: Math.min(100, score),
34
+ maxScore: 100,
30
35
  issues,
31
36
  summary: issues.length === 0
32
37
  ? `${report.filesScanned} files scanned (via model-graveyard) — all current`
@@ -111,11 +116,11 @@ function builtinModels(cwd, ignore) {
111
116
  fixHint: `replace "${model}" with "${info.replacement}"`,
112
117
  });
113
118
  }
114
- const score = Math.max(0, 10 - issues.length * 2);
119
+ const score = Math.max(0, 100 - issues.length * 20);
115
120
  return {
116
121
  name: 'models',
117
- score: Math.min(10, score),
118
- maxScore: 10,
122
+ score: Math.min(100, score),
123
+ maxScore: 100,
119
124
  issues,
120
125
  summary: issues.length === 0 ? 'all model references current' : `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} found`,
121
126
  };
@@ -0,0 +1,51 @@
1
+ export declare function collectAgentConfigFiles(cwd: string): string[];
2
+ export declare function collectMcpConfigFiles(cwd: string): string[];
3
+ export declare function readTextFile(filePath: string): string | null;
4
+ export interface OwaspFinding {
5
+ asiId: string;
6
+ severity: 'error' | 'warning' | 'info';
7
+ message: string;
8
+ file?: string;
9
+ line?: number;
10
+ fixHint?: string;
11
+ }
12
+ export declare function checkASI01(cwd: string, configFiles: string[]): {
13
+ findings: OwaspFinding[];
14
+ deduction: number;
15
+ };
16
+ export declare function checkASI02(cwd: string, mcpFiles: string[]): {
17
+ findings: OwaspFinding[];
18
+ deduction: number;
19
+ };
20
+ export declare function checkASI03(cwd: string, configFiles: string[]): {
21
+ findings: OwaspFinding[];
22
+ deduction: number;
23
+ };
24
+ export declare function checkASI04(cwd: string, mcpFiles: string[], agentConfigFiles: string[]): {
25
+ findings: OwaspFinding[];
26
+ deduction: number;
27
+ };
28
+ export declare function checkASI05(cwd: string, configFiles: string[]): {
29
+ findings: OwaspFinding[];
30
+ deduction: number;
31
+ };
32
+ export declare function checkASI06(cwd: string): {
33
+ findings: OwaspFinding[];
34
+ deduction: number;
35
+ };
36
+ export declare function checkASI07(cwd: string, configFiles: string[]): {
37
+ findings: OwaspFinding[];
38
+ deduction: number;
39
+ };
40
+ export declare function checkASI08(cwd: string, configFiles: string[]): {
41
+ findings: OwaspFinding[];
42
+ deduction: number;
43
+ };
44
+ export declare function checkASI09(cwd: string, configFiles: string[]): {
45
+ findings: OwaspFinding[];
46
+ deduction: number;
47
+ };
48
+ export declare function checkASI10(cwd: string, configFiles: string[]): {
49
+ findings: OwaspFinding[];
50
+ deduction: number;
51
+ };