posimai-root/posimai-guard-ext/src/commands/scanWorkspace.ts

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);
}
},
);
}