skannr 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/cli.js +86 -0
- package/dist/cli.js.map +1 -1
- package/dist/guard/call-context.d.ts +14 -0
- package/dist/guard/call-context.d.ts.map +1 -0
- package/dist/guard/call-context.js +120 -0
- package/dist/guard/call-context.js.map +1 -0
- package/dist/guard/fix-applier.d.ts +38 -0
- package/dist/guard/fix-applier.d.ts.map +1 -0
- package/dist/guard/fix-applier.js +181 -0
- package/dist/guard/fix-applier.js.map +1 -0
- package/dist/guard/hook-installer.d.ts +19 -0
- package/dist/guard/hook-installer.d.ts.map +1 -0
- package/dist/guard/hook-installer.js +119 -0
- package/dist/guard/hook-installer.js.map +1 -0
- package/dist/guard/index.d.ts +34 -0
- package/dist/guard/index.d.ts.map +1 -0
- package/dist/guard/index.js +152 -0
- package/dist/guard/index.js.map +1 -0
- package/dist/guard/provider.d.ts +12 -0
- package/dist/guard/provider.d.ts.map +1 -0
- package/dist/guard/provider.js +211 -0
- package/dist/guard/provider.js.map +1 -0
- package/dist/guard/review-runner.d.ts +21 -0
- package/dist/guard/review-runner.d.ts.map +1 -0
- package/dist/guard/review-runner.js +85 -0
- package/dist/guard/review-runner.js.map +1 -0
- package/dist/guard/rules-loader.d.ts +10 -0
- package/dist/guard/rules-loader.d.ts.map +1 -0
- package/dist/guard/rules-loader.js +121 -0
- package/dist/guard/rules-loader.js.map +1 -0
- package/dist/guard/schema.d.ts +182 -0
- package/dist/guard/schema.d.ts.map +1 -0
- package/dist/guard/schema.js +53 -0
- package/dist/guard/schema.js.map +1 -0
- package/dist/guard/symbol-diff.d.ts +18 -0
- package/dist/guard/symbol-diff.d.ts.map +1 -0
- package/dist/guard/symbol-diff.js +243 -0
- package/dist/guard/symbol-diff.js.map +1 -0
- package/dist/guard/types.d.ts +62 -0
- package/dist/guard/types.d.ts.map +1 -0
- package/dist/guard/types.js +6 -0
- package/dist/guard/types.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +44 -0
- package/dist/mcp-server.js.map +1 -1
- package/package.json +3 -2
- package/src/cli.ts +101 -0
- package/src/guard/call-context.ts +101 -0
- package/src/guard/fix-applier.ts +172 -0
- package/src/guard/hook-installer.ts +96 -0
- package/src/guard/index.ts +134 -0
- package/src/guard/provider.ts +209 -0
- package/src/guard/review-runner.ts +110 -0
- package/src/guard/rules-loader.ts +97 -0
- package/src/guard/schema.ts +59 -0
- package/src/guard/symbol-diff.ts +227 -0
- package/src/guard/types.ts +68 -0
- package/src/mcp-server.ts +49 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Build symbol-level diff units from staged git changes.
|
|
4
|
+
* Uses Skannr's existing parser to identify which symbols changed,
|
|
5
|
+
* instead of working with raw file diffs.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.getStagedFiles = getStagedFiles;
|
|
42
|
+
exports.buildSymbolDiffs = buildSymbolDiffs;
|
|
43
|
+
exports.buildSymbolDiffsFromDiff = buildSymbolDiffsFromDiff;
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
const registry_1 = require("../languages/registry");
|
|
48
|
+
const scanner_1 = require("../scanner");
|
|
49
|
+
/** Get the list of staged files (paths relative to root). */
|
|
50
|
+
function getStagedFiles(root) {
|
|
51
|
+
try {
|
|
52
|
+
const output = (0, child_process_1.execSync)('git diff --cached --name-only --diff-filter=ACMR', {
|
|
53
|
+
cwd: root,
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
});
|
|
56
|
+
return output.trim().split('\n').filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Get the diff text for a specific staged file. */
|
|
63
|
+
function getStagedDiff(root, file) {
|
|
64
|
+
try {
|
|
65
|
+
return (0, child_process_1.execSync)(`git diff --cached -- "${file}"`, {
|
|
66
|
+
cwd: root,
|
|
67
|
+
encoding: 'utf-8',
|
|
68
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Get the old (HEAD) version of a file, or null if newly added. */
|
|
76
|
+
function getHeadContent(root, file) {
|
|
77
|
+
try {
|
|
78
|
+
return (0, child_process_1.execSync)(`git show HEAD:"${file}"`, {
|
|
79
|
+
cwd: root,
|
|
80
|
+
encoding: 'utf-8',
|
|
81
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Extract changed line numbers from a unified diff. */
|
|
89
|
+
function extractChangedLines(diff) {
|
|
90
|
+
const lines = new Set();
|
|
91
|
+
let currentLine = 0;
|
|
92
|
+
for (const line of diff.split('\n')) {
|
|
93
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
|
|
94
|
+
if (hunkMatch) {
|
|
95
|
+
currentLine = parseInt(hunkMatch[1], 10);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
99
|
+
lines.add(currentLine);
|
|
100
|
+
currentLine++;
|
|
101
|
+
}
|
|
102
|
+
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
103
|
+
// Deleted lines don't advance the new-file line counter
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
currentLine++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return lines;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Determine which symbols in a file were affected by the staged changes.
|
|
113
|
+
* Matches symbols whose line range overlaps any changed line.
|
|
114
|
+
*/
|
|
115
|
+
function findAffectedSymbols(symbols, changedLines, allSymbols) {
|
|
116
|
+
if (changedLines.size === 0)
|
|
117
|
+
return [];
|
|
118
|
+
// Build approximate end lines: each symbol extends until the next symbol starts
|
|
119
|
+
const sorted = [...allSymbols].sort((a, b) => a.line - b.line);
|
|
120
|
+
const affected = [];
|
|
121
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
122
|
+
const sym = sorted[i];
|
|
123
|
+
const startLine = sym.line;
|
|
124
|
+
const endLine = i + 1 < sorted.length ? sorted[i + 1].line - 1 : startLine + 50;
|
|
125
|
+
for (let line = startLine; line <= endLine; line++) {
|
|
126
|
+
if (changedLines.has(line)) {
|
|
127
|
+
affected.push(sym);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return affected;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build symbol-level diff units for all staged files in the project.
|
|
136
|
+
* This is the primary input to the review runner.
|
|
137
|
+
*/
|
|
138
|
+
function buildSymbolDiffs(root) {
|
|
139
|
+
const stagedFiles = getStagedFiles(root);
|
|
140
|
+
const units = [];
|
|
141
|
+
for (const relFile of stagedFiles) {
|
|
142
|
+
const absPath = path.join(root, relFile);
|
|
143
|
+
if (!fs.existsSync(absPath))
|
|
144
|
+
continue;
|
|
145
|
+
const adapter = (0, registry_1.getAdapter)(absPath);
|
|
146
|
+
const newContent = (0, scanner_1.readFileContent)(absPath);
|
|
147
|
+
if (!newContent)
|
|
148
|
+
continue;
|
|
149
|
+
const newSymbols = adapter.extractSymbols(newContent);
|
|
150
|
+
const oldContent = getHeadContent(root, relFile);
|
|
151
|
+
const oldSymbols = oldContent ? adapter.extractSymbols(oldContent) : [];
|
|
152
|
+
const diff = getStagedDiff(root, relFile);
|
|
153
|
+
const changedLines = extractChangedLines(diff);
|
|
154
|
+
// Find symbols that overlap changed lines in the new version
|
|
155
|
+
const affected = findAffectedSymbols(newSymbols, changedLines, newSymbols);
|
|
156
|
+
// Determine change type for each affected symbol
|
|
157
|
+
const oldSymbolNames = new Set(oldSymbols.map((s) => s.name));
|
|
158
|
+
for (const sym of affected) {
|
|
159
|
+
if (sym.kind === 'export')
|
|
160
|
+
continue; // skip synthetic export markers
|
|
161
|
+
const changeType = oldSymbolNames.has(sym.name) ? 'modified' : 'added';
|
|
162
|
+
const oldSym = oldSymbols.find((s) => s.name === sym.name);
|
|
163
|
+
units.push({
|
|
164
|
+
file: relFile,
|
|
165
|
+
symbol: sym.name,
|
|
166
|
+
changeType,
|
|
167
|
+
oldSignature: oldSym ? `${oldSym.kind} ${oldSym.name}` : undefined,
|
|
168
|
+
newSignature: `${sym.kind} ${sym.name}`,
|
|
169
|
+
callers: [], // populated by call-context module
|
|
170
|
+
callees: [], // populated by call-context module
|
|
171
|
+
diffText: diff,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Handle deleted symbols (in old but not in new)
|
|
175
|
+
if (oldContent) {
|
|
176
|
+
const newSymbolNames = new Set(newSymbols.map((s) => s.name));
|
|
177
|
+
for (const oldSym of oldSymbols) {
|
|
178
|
+
if (oldSym.kind === 'export')
|
|
179
|
+
continue;
|
|
180
|
+
if (!newSymbolNames.has(oldSym.name)) {
|
|
181
|
+
units.push({
|
|
182
|
+
file: relFile,
|
|
183
|
+
symbol: oldSym.name,
|
|
184
|
+
changeType: 'deleted',
|
|
185
|
+
oldSignature: `${oldSym.kind} ${oldSym.name}`,
|
|
186
|
+
callers: [],
|
|
187
|
+
callees: [],
|
|
188
|
+
diffText: diff,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return units;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Build symbol diffs from a PR diff string (for --pr-mode).
|
|
198
|
+
*/
|
|
199
|
+
function buildSymbolDiffsFromDiff(root, diffContent) {
|
|
200
|
+
const parseDiff = require('parse-diff');
|
|
201
|
+
const parsedFiles = parseDiff(diffContent);
|
|
202
|
+
const units = [];
|
|
203
|
+
for (const file of parsedFiles) {
|
|
204
|
+
const raw = file.to ?? file.from;
|
|
205
|
+
if (!raw || raw === '/dev/null')
|
|
206
|
+
continue;
|
|
207
|
+
const relFile = raw.replace(/^[ab]\//, '');
|
|
208
|
+
const absPath = path.join(root, relFile);
|
|
209
|
+
if (!fs.existsSync(absPath))
|
|
210
|
+
continue;
|
|
211
|
+
const adapter = (0, registry_1.getAdapter)(absPath);
|
|
212
|
+
const content = (0, scanner_1.readFileContent)(absPath);
|
|
213
|
+
if (!content)
|
|
214
|
+
continue;
|
|
215
|
+
const symbols = adapter.extractSymbols(content);
|
|
216
|
+
const changedLines = new Set();
|
|
217
|
+
for (const chunk of file.chunks) {
|
|
218
|
+
for (const change of chunk.changes) {
|
|
219
|
+
if (change.type === 'add' || change.type === 'del') {
|
|
220
|
+
const ln = change.ln;
|
|
221
|
+
if (typeof ln === 'number')
|
|
222
|
+
changedLines.add(ln);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const affected = findAffectedSymbols(symbols, changedLines, symbols);
|
|
227
|
+
for (const sym of affected) {
|
|
228
|
+
if (sym.kind === 'export')
|
|
229
|
+
continue;
|
|
230
|
+
units.push({
|
|
231
|
+
file: relFile,
|
|
232
|
+
symbol: sym.name,
|
|
233
|
+
changeType: 'modified',
|
|
234
|
+
newSignature: `${sym.kind} ${sym.name}`,
|
|
235
|
+
callers: [],
|
|
236
|
+
callees: [],
|
|
237
|
+
diffText: file.chunks.map((c) => c.content).join('\n'),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return units;
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=symbol-diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"symbol-diff.js","sourceRoot":"","sources":["../../src/guard/symbol-diff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for the guard module.
|
|
3
|
+
*/
|
|
4
|
+
/** Severity levels for rules and violations. */
|
|
5
|
+
export type Severity = 'low' | 'medium' | 'high' | 'critical';
|
|
6
|
+
/** A team-defined review rule. */
|
|
7
|
+
export interface GuardRule {
|
|
8
|
+
id: string;
|
|
9
|
+
description: string;
|
|
10
|
+
severity: Severity;
|
|
11
|
+
fixable: boolean;
|
|
12
|
+
category: string;
|
|
13
|
+
}
|
|
14
|
+
/** A single symbol-level diff unit sent to the LLM for review. */
|
|
15
|
+
export interface SymbolDiffUnit {
|
|
16
|
+
file: string;
|
|
17
|
+
symbol: string;
|
|
18
|
+
changeType: 'added' | 'modified' | 'deleted';
|
|
19
|
+
oldSignature?: string;
|
|
20
|
+
newSignature?: string;
|
|
21
|
+
callers: string[];
|
|
22
|
+
callees: string[];
|
|
23
|
+
diffText: string;
|
|
24
|
+
}
|
|
25
|
+
/** A violation found by the review. */
|
|
26
|
+
export interface Violation {
|
|
27
|
+
rule_id: string;
|
|
28
|
+
file: string;
|
|
29
|
+
symbol: string;
|
|
30
|
+
line_start: number;
|
|
31
|
+
line_end: number;
|
|
32
|
+
severity: Severity;
|
|
33
|
+
confidence: number;
|
|
34
|
+
fixable: boolean;
|
|
35
|
+
message: string;
|
|
36
|
+
suggested_fix?: string;
|
|
37
|
+
}
|
|
38
|
+
/** The structured response contract from the LLM. */
|
|
39
|
+
export interface ReviewResponse {
|
|
40
|
+
violations: Violation[];
|
|
41
|
+
status: 'pass' | 'fail';
|
|
42
|
+
summary: string;
|
|
43
|
+
}
|
|
44
|
+
/** Result of a full guard run. */
|
|
45
|
+
export interface GuardResult {
|
|
46
|
+
response: ReviewResponse;
|
|
47
|
+
rulesUsed: GuardRule[];
|
|
48
|
+
symbolsReviewed: number;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
}
|
|
51
|
+
/** Guard configuration (provider, model, etc.). */
|
|
52
|
+
export interface GuardConfig {
|
|
53
|
+
/** LLM provider: 'gemini' | 'openai' */
|
|
54
|
+
provider: 'gemini' | 'openai';
|
|
55
|
+
/** Model name (e.g. 'gemini-2.0-flash-exp', 'gpt-4o'). */
|
|
56
|
+
model: string;
|
|
57
|
+
/** API key (resolved from env if not set). */
|
|
58
|
+
apiKey?: string;
|
|
59
|
+
/** OpenAI-compatible base URL (for openai provider). */
|
|
60
|
+
baseUrl?: string;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/guard/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,gDAAgD;AAChD,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAE9D,kCAAkC;AAClC,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kCAAkC;AAClC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC9B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/guard/types.ts"],"names":[],"mappings":";AAAA;;GAEG"}
|
package/dist/mcp-server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAsBH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAsBH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CA+KpD"}
|
package/dist/mcp-server.js
CHANGED
|
@@ -110,6 +110,32 @@ async function startMcpServer() {
|
|
|
110
110
|
required: ['root'],
|
|
111
111
|
},
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: 'guard_review',
|
|
115
|
+
description: 'Review staged or diff changes against team-defined rules. Returns structured violations with severity, confidence, and fixability.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
root: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'Absolute path to the repo root',
|
|
122
|
+
},
|
|
123
|
+
diff: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Unified diff content (if omitted, reviews staged files)',
|
|
126
|
+
},
|
|
127
|
+
diff_only: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
description: 'Skip cross-file context for faster review (default: false)',
|
|
130
|
+
},
|
|
131
|
+
fix: {
|
|
132
|
+
type: 'boolean',
|
|
133
|
+
description: 'Apply auto-fixes for fixable violations (default: false)',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
required: ['root'],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
113
139
|
],
|
|
114
140
|
}));
|
|
115
141
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
@@ -156,6 +182,24 @@ async function startMcpServer() {
|
|
|
156
182
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
157
183
|
};
|
|
158
184
|
}
|
|
185
|
+
if (toolName === 'guard_review') {
|
|
186
|
+
const { runGuard, formatGuardJson } = await Promise.resolve().then(() => __importStar(require('./guard/index')));
|
|
187
|
+
const root = String(args.root ?? '');
|
|
188
|
+
const diffContent = typeof args.diff === 'string' && args.diff.length > 0
|
|
189
|
+
? args.diff
|
|
190
|
+
: undefined;
|
|
191
|
+
const diffOnly = args.diff_only === true;
|
|
192
|
+
const fix = args.fix === true;
|
|
193
|
+
const { result } = await runGuard({
|
|
194
|
+
root,
|
|
195
|
+
diffOnly,
|
|
196
|
+
fix,
|
|
197
|
+
...(diffContent ? { prMode: false } : {}),
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
content: [{ type: 'text', text: formatGuardJson(result) }],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
159
203
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
160
204
|
});
|
|
161
205
|
const transport = new stdio_js_1.StdioServerTransport();
|
package/dist/mcp-server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.js","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":";AAAA;;GAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsBH,
|
|
1
|
+
{"version":3,"file":"mcp-server.js","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skannr",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A universal tool to analyze any codebase and generate semantic skeletons for AI agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"ignore": "^7.0.5",
|
|
36
36
|
"parse-diff": "0.12.0",
|
|
37
37
|
"ts-morph": "^21.0.1",
|
|
38
|
-
"web-tree-sitter": "0.26.10"
|
|
38
|
+
"web-tree-sitter": "0.26.10",
|
|
39
|
+
"zod": "3.23.8"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/jest": "^29.5.11",
|
package/src/cli.ts
CHANGED
|
@@ -483,6 +483,102 @@ cacheCommand
|
|
|
483
483
|
console.log(` Dir: ${cacheManager.getCacheDir()}\n`);
|
|
484
484
|
});
|
|
485
485
|
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Subcommand: guard
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
const guardCommand = program
|
|
490
|
+
.command('guard')
|
|
491
|
+
.description('Review staged changes against team-defined rules')
|
|
492
|
+
.option('--root <path>', 'project root directory', process.cwd())
|
|
493
|
+
.option('--fix', 'apply auto-fixes for fixable violations')
|
|
494
|
+
.option('--dry-run', 'show what --fix would change without writing')
|
|
495
|
+
.option('--pr-mode', 'review full PR diff vs base branch')
|
|
496
|
+
.option('--diff-only', 'skip cross-file context (faster, cheaper)')
|
|
497
|
+
.option('--no-cache', 'ignore symbol cache')
|
|
498
|
+
.option('--json', 'output as JSON')
|
|
499
|
+
.action(async (cmdOpts: {
|
|
500
|
+
root: string;
|
|
501
|
+
fix?: boolean;
|
|
502
|
+
dryRun?: boolean;
|
|
503
|
+
prMode?: boolean;
|
|
504
|
+
diffOnly?: boolean;
|
|
505
|
+
noCache?: boolean;
|
|
506
|
+
json?: boolean;
|
|
507
|
+
}) => {
|
|
508
|
+
try {
|
|
509
|
+
const { runGuard, formatGuardText, formatGuardJson } = await import('./guard/index');
|
|
510
|
+
const { result, exitCode } = await runGuard({
|
|
511
|
+
root: cmdOpts.root,
|
|
512
|
+
fix: cmdOpts.fix,
|
|
513
|
+
dryRun: cmdOpts.dryRun,
|
|
514
|
+
prMode: cmdOpts.prMode,
|
|
515
|
+
diffOnly: cmdOpts.diffOnly,
|
|
516
|
+
noCache: cmdOpts.noCache,
|
|
517
|
+
json: cmdOpts.json,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const output = cmdOpts.json
|
|
521
|
+
? formatGuardJson(result)
|
|
522
|
+
: formatGuardText(result);
|
|
523
|
+
|
|
524
|
+
process.stdout.write(output + (output.endsWith('\n') ? '' : '\n'));
|
|
525
|
+
process.exit(exitCode);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
console.error(
|
|
528
|
+
'Error running guard:',
|
|
529
|
+
error instanceof Error ? error.message : error,
|
|
530
|
+
);
|
|
531
|
+
process.exit(2);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
guardCommand
|
|
536
|
+
.command('install')
|
|
537
|
+
.description('Install git pre-commit hook')
|
|
538
|
+
.option('--root <path>', 'project root directory', process.cwd())
|
|
539
|
+
.action(async (cmdOpts: { root: string }) => {
|
|
540
|
+
const { installHook } = await import('./guard/index');
|
|
541
|
+
const { installed, message } = installHook(path.resolve(cmdOpts.root));
|
|
542
|
+
console.log(message);
|
|
543
|
+
process.exit(installed ? 0 : 1);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
guardCommand
|
|
547
|
+
.command('uninstall')
|
|
548
|
+
.description('Remove git pre-commit hook')
|
|
549
|
+
.option('--root <path>', 'project root directory', process.cwd())
|
|
550
|
+
.action(async (cmdOpts: { root: string }) => {
|
|
551
|
+
const { uninstallHook } = await import('./guard/index');
|
|
552
|
+
const { removed, message } = uninstallHook(path.resolve(cmdOpts.root));
|
|
553
|
+
console.log(message);
|
|
554
|
+
process.exit(removed ? 0 : 1);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
guardCommand
|
|
558
|
+
.command('config')
|
|
559
|
+
.description('Show loaded rules and provider config')
|
|
560
|
+
.option('--root <path>', 'project root directory', process.cwd())
|
|
561
|
+
.action(async (cmdOpts: { root: string }) => {
|
|
562
|
+
const { loadRules, loadGuardConfig } = await import('./guard/index');
|
|
563
|
+
const root = path.resolve(cmdOpts.root);
|
|
564
|
+
try {
|
|
565
|
+
const rules = loadRules(root);
|
|
566
|
+
const config = loadGuardConfig(root);
|
|
567
|
+
console.log('\n Guard Config:');
|
|
568
|
+
console.log(` Provider: ${config.provider}`);
|
|
569
|
+
console.log(` Model: ${config.model}`);
|
|
570
|
+
console.log(` API Key: ${config.apiKey ? '***' + config.apiKey.slice(-4) : 'not set'}`);
|
|
571
|
+
console.log(`\n Rules (${rules.length}):`);
|
|
572
|
+
for (const r of rules) {
|
|
573
|
+
console.log(` [${r.id}] ${r.severity} | fixable=${r.fixable} | ${r.description}`);
|
|
574
|
+
}
|
|
575
|
+
console.log('');
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error(error instanceof Error ? error.message : error);
|
|
578
|
+
process.exit(2);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
486
582
|
// ---------------------------------------------------------------------------
|
|
487
583
|
// Help
|
|
488
584
|
// ---------------------------------------------------------------------------
|
|
@@ -499,6 +595,11 @@ Examples:
|
|
|
499
595
|
skannr risk --diff feature.patch Impact of a patch file
|
|
500
596
|
skannr risk -n 3 --json 3 hops, JSON for CI
|
|
501
597
|
|
|
598
|
+
skannr guard Review staged changes against rules
|
|
599
|
+
skannr guard --fix Auto-fix fixable violations
|
|
600
|
+
skannr guard --pr-mode --json Review PR diff, JSON output
|
|
601
|
+
skannr guard install Install pre-commit hook
|
|
602
|
+
|
|
502
603
|
skannr report Repo health summary
|
|
503
604
|
skannr agent Interactive mode
|
|
504
605
|
skannr cache stats Cache hit rate
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull caller/callee context from the existing dependency graph.
|
|
3
|
+
* Enriches SymbolDiffUnit[] with cross-file relationship information
|
|
4
|
+
* so the LLM can detect breakages beyond the changed file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { scanFiles, readFileContent } from '../scanner';
|
|
9
|
+
import { analyzeDependencyGraph } from '../ranker-enhanced';
|
|
10
|
+
import { getAdapter } from '../languages/registry';
|
|
11
|
+
import { buildReverseGraph } from '../blast-radius';
|
|
12
|
+
import { loadConfig } from '../config';
|
|
13
|
+
import type { SymbolDiffUnit } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enrich symbol diff units with caller and callee context.
|
|
17
|
+
* Uses the project's dependency graph to find:
|
|
18
|
+
* - callers: files that import the changed file (reverse deps)
|
|
19
|
+
* - callees: files that the changed file imports (forward deps)
|
|
20
|
+
*/
|
|
21
|
+
export function enrichWithCallContext(
|
|
22
|
+
units: SymbolDiffUnit[],
|
|
23
|
+
root: string,
|
|
24
|
+
): SymbolDiffUnit[] {
|
|
25
|
+
if (units.length === 0) return units;
|
|
26
|
+
|
|
27
|
+
// Scan project files and build dependency graph
|
|
28
|
+
const config = loadConfig(root);
|
|
29
|
+
const allFiles = scanFiles(root, {
|
|
30
|
+
extensions: config.extensions,
|
|
31
|
+
exclude: config.exclude,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const forwardGraph = analyzeDependencyGraph(allFiles, root);
|
|
35
|
+
const reverseGraph = buildReverseGraph(forwardGraph);
|
|
36
|
+
|
|
37
|
+
// Group units by file
|
|
38
|
+
const fileSet = new Set(units.map((u) => u.file));
|
|
39
|
+
|
|
40
|
+
for (const unit of units) {
|
|
41
|
+
const fileRel = unit.file;
|
|
42
|
+
const fileNoExt = fileRel.replace(/\.[^.]+$/, '');
|
|
43
|
+
const fileBase = path.basename(fileRel, path.extname(fileRel));
|
|
44
|
+
|
|
45
|
+
// Find callers (files that import this file)
|
|
46
|
+
const callers: string[] = [];
|
|
47
|
+
for (const [key, importers] of reverseGraph.entries()) {
|
|
48
|
+
const normalizedKey = key.split(path.sep).join('/');
|
|
49
|
+
if (
|
|
50
|
+
normalizedKey === fileRel ||
|
|
51
|
+
normalizedKey === fileNoExt ||
|
|
52
|
+
normalizedKey.endsWith('/' + fileBase)
|
|
53
|
+
) {
|
|
54
|
+
for (const importer of importers) {
|
|
55
|
+
callers.push(importer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find callees (files this file imports)
|
|
61
|
+
const callees: string[] = [];
|
|
62
|
+
const normalizedFileRel = fileRel.split(path.sep).join('/');
|
|
63
|
+
for (const [key, imports] of forwardGraph.entries()) {
|
|
64
|
+
const normalizedKey = key.split(path.sep).join('/');
|
|
65
|
+
if (normalizedKey === normalizedFileRel) {
|
|
66
|
+
callees.push(...imports);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Format as "file:symbol" where possible
|
|
72
|
+
unit.callers = [...new Set(callers)].slice(0, 10).map((c) => formatCallerRef(c, root));
|
|
73
|
+
unit.callees = [...new Set(callees)].slice(0, 10);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return units;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format a caller file reference, attempting to identify which symbols
|
|
81
|
+
* in that file actually reference the changed file.
|
|
82
|
+
*/
|
|
83
|
+
function formatCallerRef(callerFile: string, root: string): string {
|
|
84
|
+
const absPath = path.join(root, callerFile);
|
|
85
|
+
const content = readFileContent(absPath);
|
|
86
|
+
if (!content) return callerFile;
|
|
87
|
+
|
|
88
|
+
const adapter = getAdapter(absPath);
|
|
89
|
+
const symbols = adapter.extractSymbols(content);
|
|
90
|
+
|
|
91
|
+
// Return the file with its top exported symbols for context
|
|
92
|
+
const topSymbols = symbols
|
|
93
|
+
.filter((s) => s.kind === 'function' || s.kind === 'class')
|
|
94
|
+
.slice(0, 3)
|
|
95
|
+
.map((s) => s.name);
|
|
96
|
+
|
|
97
|
+
if (topSymbols.length > 0) {
|
|
98
|
+
return `${callerFile}:${topSymbols.join(',')}`;
|
|
99
|
+
}
|
|
100
|
+
return callerFile;
|
|
101
|
+
}
|