106 lines
4.1 KiB
TypeScript
106 lines
4.1 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import { readWorkspaceFiles } from '../scanner/fileReader';
|
|
import { scanWithGemini } from '../scanner/geminiClient';
|
|
import { scanWithClaude } from '../scanner/claudeClient';
|
|
import { issuesToDiagnostics } from '../ui/diagnostics';
|
|
import { ResultsPanel } from '../ui/resultsPanel';
|
|
import { ScanIssue } from '../scanner/prompt';
|
|
// Rule engine: deterministic scan — no API key required
|
|
import { runRuleEngine } from '../../../posimai-guard/src/lib/ruleEngine';
|
|
|
|
export async function scanWorkspace(
|
|
context: vscode.ExtensionContext,
|
|
collection: vscode.DiagnosticCollection,
|
|
): Promise<void> {
|
|
const folders = vscode.workspace.workspaceFolders;
|
|
if (!folders || folders.length === 0) {
|
|
vscode.window.showWarningMessage('Guard: ワークスペースが開かれていません');
|
|
return;
|
|
}
|
|
|
|
const config = vscode.workspace.getConfiguration('guard');
|
|
const maxFiles: number = config.get('maxFiles') ?? 80;
|
|
const model: string = config.get('model') ?? 'gemini';
|
|
|
|
await vscode.window.withProgress(
|
|
{
|
|
location: vscode.ProgressLocation.Notification,
|
|
title: 'Guard: スキャン中...',
|
|
cancellable: false,
|
|
},
|
|
async progress => {
|
|
progress.report({ message: 'ファイルを読み込んでいます...' });
|
|
const files = readWorkspaceFiles(maxFiles);
|
|
|
|
if (files.length === 0) {
|
|
vscode.window.showInformationMessage('Guard: スキャン対象のファイルが見つかりませんでした');
|
|
return;
|
|
}
|
|
|
|
const sourceName = folders[0].name;
|
|
|
|
// ── Step 1: Rule engine (instant, no API key) ──────────────
|
|
progress.report({ message: `静的ルール解析中... (${files.length} ファイル)` });
|
|
const ruleIssues = runRuleEngine(files) as unknown as ScanIssue[];
|
|
|
|
// Show rule engine results immediately
|
|
const seen = new Set(ruleIssues.map(i => `${i.file}::${i.title}`));
|
|
let issues: ScanIssue[] = [...ruleIssues];
|
|
|
|
// ── Step 2: Gemini (optional — deeper semantic analysis) ───
|
|
const geminiKey = await context.secrets.get('guard.geminiKey');
|
|
if (geminiKey && (model === 'gemini' || model === 'both')) {
|
|
try {
|
|
progress.report({ message: 'Gemini でセマンティック解析中...' });
|
|
const geminiIssues = await scanWithGemini(files, geminiKey);
|
|
for (const gi of geminiIssues) {
|
|
if (!seen.has(`${gi.file}::${gi.title}`)) {
|
|
issues.push(gi);
|
|
seen.add(`${gi.file}::${gi.title}`);
|
|
}
|
|
}
|
|
} catch {
|
|
// Gemini is optional — silent fail
|
|
}
|
|
}
|
|
|
|
// ── Step 3: Claude (optional) ─────────────────────────────
|
|
if (model === 'claude' || model === 'both') {
|
|
const claudeKey = await context.secrets.get('guard.claudeKey');
|
|
if (claudeKey) {
|
|
try {
|
|
progress.report({ message: 'Claude でセマンティック解析中...' });
|
|
const claudeIssues = await scanWithClaude(files, claudeKey);
|
|
for (const ci of claudeIssues) {
|
|
if (!seen.has(`${ci.file}::${ci.title}`)) {
|
|
issues.push(ci);
|
|
seen.add(`${ci.file}::${ci.title}`);
|
|
}
|
|
}
|
|
} catch {
|
|
// Claude is optional — silent fail
|
|
}
|
|
}
|
|
}
|
|
|
|
issuesToDiagnostics(issues, collection);
|
|
ResultsPanel.show(context, issues, sourceName);
|
|
|
|
const danger = issues.filter(i => i.severity === 'danger').length;
|
|
const warning = issues.filter(i => i.severity === 'warning').length;
|
|
const ruleCount = ruleIssues.length;
|
|
const aiCount = issues.length - ruleCount;
|
|
const aiNote = aiCount > 0 ? ` (+AI ${aiCount})` : '';
|
|
const msg = `Guard: ${issues.length} 件検出 (危険 ${danger} / 警告 ${warning})${aiNote}`;
|
|
|
|
if (danger > 0) {
|
|
vscode.window.showErrorMessage(msg);
|
|
} else if (warning > 0) {
|
|
vscode.window.showWarningMessage(msg);
|
|
} else {
|
|
vscode.window.showInformationMessage(msg);
|
|
}
|
|
},
|
|
);
|
|
}
|