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