From 72522dd815ede6118927402ea25fa80262cc1983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Engebr=C3=A5ten?= Date: Sat, 14 Feb 2026 13:01:48 +0100 Subject: [PATCH] =?UTF-8?q?refactor(issues):=20remove=20dead=20trust=20lev?= =?UTF-8?q?el=20system=20=E2=80=94=20frontend=20progressive=20trust=20UI,?= =?UTF-8?q?=20backend=20L0-L4=20escalation,=20and=20deprecated=20ai-triage?= =?UTF-8?q?=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two unconnected trust systems were fully implemented but never wired up: the frontend crawl/walk/run progressive trust settings wrote to config that no backend code read, and the backend L0-L4 trust escalation module was never imported anywhere. Removes ~2200 lines of dead code across components, stores, IPC handlers, types, constants, i18n, and tests. Co-Authored-By: Claude Opus 4.6 --- apps/backend/runners/github/multi_repo.py | 4 - apps/backend/runners/github/trust.py | 543 ------------------ .../__tests__/ai-triage-handlers.test.ts | 43 -- .../ipc-handlers/github/ai-triage-handlers.ts | 59 -- .../src/preload/api/modules/github-api.ts | 10 - .../__tests__/ai-triage-store.test.ts | 177 ------ .../__tests__/ai-triage-integration.test.ts | 252 -------- .../__tests__/ai-triage-integration.test.tsx | 6 +- .../__tests__/phase5-exports.test.ts | 2 - .../components/ProgressiveTrustSettings.tsx | 187 ------ .../ProgressiveTrustSettingsConnected.tsx | 57 -- .../ProgressiveTrustSettings.test.tsx | 141 ----- ...ProgressiveTrustSettingsConnected.test.tsx | 61 -- .../github-issues/components/index.ts | 1 - .../settings/sections/SectionRouter.tsx | 2 - .../frontend/src/renderer/lib/browser-mock.ts | 3 - .../github/__tests__/ai-triage-store.test.ts | 369 ------------ .../renderer/stores/github/ai-triage-store.ts | 159 ----- .../__tests__/ai-triage-constants.test.ts | 12 - .../shared/__tests__/ai-triage-types.test.ts | 37 -- .../src/shared/constants/ai-triage.ts | 12 - apps/frontend/src/shared/constants/ipc.ts | 2 - .../src/shared/i18n/locales/en/common.json | 17 - .../src/shared/i18n/locales/fr/common.json | 17 - apps/frontend/src/shared/types/ai-triage.ts | 32 -- 25 files changed, 4 insertions(+), 2201 deletions(-) delete mode 100644 apps/backend/runners/github/trust.py delete mode 100644 apps/frontend/src/renderer/__tests__/ai-triage-store.test.ts delete mode 100644 apps/frontend/src/renderer/components/github-issues/__tests__/ai-triage-integration.test.ts delete mode 100644 apps/frontend/src/renderer/components/github-issues/components/ProgressiveTrustSettings.tsx delete mode 100644 apps/frontend/src/renderer/components/github-issues/components/ProgressiveTrustSettingsConnected.tsx delete mode 100644 apps/frontend/src/renderer/components/github-issues/components/__tests__/ProgressiveTrustSettings.test.tsx delete mode 100644 apps/frontend/src/renderer/components/github-issues/components/__tests__/ProgressiveTrustSettingsConnected.test.tsx delete mode 100644 apps/frontend/src/renderer/stores/github/__tests__/ai-triage-store.test.ts delete mode 100644 apps/frontend/src/renderer/stores/github/ai-triage-store.ts diff --git a/apps/backend/runners/github/multi_repo.py b/apps/backend/runners/github/multi_repo.py index 314841fa..309289cc 100644 --- a/apps/backend/runners/github/multi_repo.py +++ b/apps/backend/runners/github/multi_repo.py @@ -53,7 +53,6 @@ class RepoConfig: relationship: Relationship to other repos upstream_repo: Upstream repo if this is a fork labels: Label configuration overrides - trust_level: Trust level for this repo """ repo: str # owner/repo format @@ -64,7 +63,6 @@ class RepoConfig: labels: dict[str, list[str]] = field( default_factory=dict ) # e.g., {"auto_fix": ["fix-me"]} - trust_level: int = 0 # 0-4 trust level display_name: str | None = None # Human-readable name # Feature toggles per repo @@ -125,7 +123,6 @@ class RepoConfig: "relationship": self.relationship.value, "upstream_repo": self.upstream_repo, "labels": self.labels, - "trust_level": self.trust_level, "display_name": self.display_name, "auto_fix_enabled": self.auto_fix_enabled, "pr_review_enabled": self.pr_review_enabled, @@ -141,7 +138,6 @@ class RepoConfig: relationship=RepoRelationship(data.get("relationship", "standalone")), upstream_repo=data.get("upstream_repo"), labels=data.get("labels", {}), - trust_level=data.get("trust_level", 0), display_name=data.get("display_name"), auto_fix_enabled=data.get("auto_fix_enabled", True), pr_review_enabled=data.get("pr_review_enabled", True), diff --git a/apps/backend/runners/github/trust.py b/apps/backend/runners/github/trust.py deleted file mode 100644 index c5230d20..00000000 --- a/apps/backend/runners/github/trust.py +++ /dev/null @@ -1,543 +0,0 @@ -""" -Trust Escalation Model -====================== - -Progressive trust system that unlocks more autonomous actions as accuracy improves: - -- L0: Review-only (comment, no actions) -- L1: Auto-apply labels based on triage -- L2: Auto-close duplicates and spam -- L3: Auto-merge trivial fixes (docs, typos) -- L4: Full auto-fix with merge - -Trust increases with accuracy, decreases with overrides. -""" - -from __future__ import annotations - -import json -from dataclasses import dataclass, field -from datetime import datetime, timezone -from enum import IntEnum -from pathlib import Path -from typing import Any - - -class TrustLevel(IntEnum): - """Trust levels with increasing autonomy.""" - - L0_REVIEW_ONLY = 0 # Comment only, no actions - L1_LABEL = 1 # Auto-apply labels - L2_CLOSE = 2 # Auto-close duplicates/spam - L3_MERGE_TRIVIAL = 3 # Auto-merge trivial fixes - L4_FULL_AUTO = 4 # Full autonomous operation - - @property - def display_name(self) -> str: - names = { - 0: "Review Only", - 1: "Auto-Label", - 2: "Auto-Close", - 3: "Auto-Merge Trivial", - 4: "Full Autonomous", - } - return names.get(self.value, "Unknown") - - @property - def description(self) -> str: - descriptions = { - 0: "AI can comment with suggestions but takes no actions", - 1: "AI can automatically apply labels based on triage", - 2: "AI can auto-close clear duplicates and spam", - 3: "AI can auto-merge trivial changes (docs, typos, formatting)", - 4: "AI can auto-fix issues and merge PRs autonomously", - } - return descriptions.get(self.value, "") - - @property - def allowed_actions(self) -> set[str]: - """Actions allowed at this trust level.""" - actions = { - 0: {"comment", "review"}, - 1: {"comment", "review", "label", "triage"}, - 2: { - "comment", - "review", - "label", - "triage", - "close_duplicate", - "close_spam", - }, - 3: { - "comment", - "review", - "label", - "triage", - "close_duplicate", - "close_spam", - "merge_trivial", - }, - 4: { - "comment", - "review", - "label", - "triage", - "close_duplicate", - "close_spam", - "merge_trivial", - "auto_fix", - "merge", - }, - } - return actions.get(self.value, set()) - - def can_perform(self, action: str) -> bool: - """Check if this trust level allows an action.""" - return action in self.allowed_actions - - -# Thresholds for trust level upgrades -TRUST_THRESHOLDS = { - TrustLevel.L1_LABEL: { - "min_actions": 20, - "min_accuracy": 0.90, - "min_days": 3, - }, - TrustLevel.L2_CLOSE: { - "min_actions": 50, - "min_accuracy": 0.92, - "min_days": 7, - }, - TrustLevel.L3_MERGE_TRIVIAL: { - "min_actions": 100, - "min_accuracy": 0.95, - "min_days": 14, - }, - TrustLevel.L4_FULL_AUTO: { - "min_actions": 200, - "min_accuracy": 0.97, - "min_days": 30, - }, -} - - -@dataclass -class AccuracyMetrics: - """Tracks accuracy metrics for trust calculation.""" - - total_actions: int = 0 - correct_actions: int = 0 - overridden_actions: int = 0 - last_action_at: str | None = None - first_action_at: str | None = None - - # Per-action type metrics - review_total: int = 0 - review_correct: int = 0 - label_total: int = 0 - label_correct: int = 0 - triage_total: int = 0 - triage_correct: int = 0 - close_total: int = 0 - close_correct: int = 0 - merge_total: int = 0 - merge_correct: int = 0 - fix_total: int = 0 - fix_correct: int = 0 - - @property - def accuracy(self) -> float: - """Overall accuracy rate.""" - if self.total_actions == 0: - return 0.0 - return self.correct_actions / self.total_actions - - @property - def override_rate(self) -> float: - """Rate of overridden actions.""" - if self.total_actions == 0: - return 0.0 - return self.overridden_actions / self.total_actions - - @property - def days_active(self) -> int: - """Days since first action.""" - if not self.first_action_at: - return 0 - first = datetime.fromisoformat(self.first_action_at) - now = datetime.now(timezone.utc) - return (now - first).days - - def record_action( - self, - action_type: str, - correct: bool, - overridden: bool = False, - ) -> None: - """Record an action outcome.""" - now = datetime.now(timezone.utc).isoformat() - - self.total_actions += 1 - if correct: - self.correct_actions += 1 - if overridden: - self.overridden_actions += 1 - - self.last_action_at = now - if not self.first_action_at: - self.first_action_at = now - - # Update per-type metrics - type_map = { - "review": ("review_total", "review_correct"), - "label": ("label_total", "label_correct"), - "triage": ("triage_total", "triage_correct"), - "close": ("close_total", "close_correct"), - "merge": ("merge_total", "merge_correct"), - "fix": ("fix_total", "fix_correct"), - } - - if action_type in type_map: - total_attr, correct_attr = type_map[action_type] - setattr(self, total_attr, getattr(self, total_attr) + 1) - if correct: - setattr(self, correct_attr, getattr(self, correct_attr) + 1) - - def to_dict(self) -> dict[str, Any]: - return { - "total_actions": self.total_actions, - "correct_actions": self.correct_actions, - "overridden_actions": self.overridden_actions, - "last_action_at": self.last_action_at, - "first_action_at": self.first_action_at, - "review_total": self.review_total, - "review_correct": self.review_correct, - "label_total": self.label_total, - "label_correct": self.label_correct, - "triage_total": self.triage_total, - "triage_correct": self.triage_correct, - "close_total": self.close_total, - "close_correct": self.close_correct, - "merge_total": self.merge_total, - "merge_correct": self.merge_correct, - "fix_total": self.fix_total, - "fix_correct": self.fix_correct, - } - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> AccuracyMetrics: - return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__}) - - -@dataclass -class TrustState: - """Trust state for a repository.""" - - repo: str - current_level: TrustLevel = TrustLevel.L0_REVIEW_ONLY - metrics: AccuracyMetrics = field(default_factory=AccuracyMetrics) - manual_override: TrustLevel | None = None # User-set override - last_level_change: str | None = None - level_history: list[dict[str, Any]] = field(default_factory=list) - - @property - def effective_level(self) -> TrustLevel: - """Get effective trust level (considers manual override).""" - if self.manual_override is not None: - return self.manual_override - return self.current_level - - def can_perform(self, action: str) -> bool: - """Check if current trust level allows an action.""" - return self.effective_level.can_perform(action) - - def get_progress_to_next_level(self) -> dict[str, Any]: - """Get progress toward next trust level.""" - current = self.current_level - if current >= TrustLevel.L4_FULL_AUTO: - return { - "next_level": None, - "at_max": True, - } - - next_level = TrustLevel(current + 1) - thresholds = TRUST_THRESHOLDS.get(next_level, {}) - - min_actions = thresholds.get("min_actions", 0) - min_accuracy = thresholds.get("min_accuracy", 0) - min_days = thresholds.get("min_days", 0) - - return { - "next_level": next_level.value, - "next_level_name": next_level.display_name, - "at_max": False, - "actions": { - "current": self.metrics.total_actions, - "required": min_actions, - "progress": min(1.0, self.metrics.total_actions / max(1, min_actions)), - }, - "accuracy": { - "current": self.metrics.accuracy, - "required": min_accuracy, - "progress": min(1.0, self.metrics.accuracy / max(0.01, min_accuracy)), - }, - "days": { - "current": self.metrics.days_active, - "required": min_days, - "progress": min(1.0, self.metrics.days_active / max(1, min_days)), - }, - } - - def check_upgrade(self) -> TrustLevel | None: - """Check if eligible for trust level upgrade.""" - current = self.current_level - if current >= TrustLevel.L4_FULL_AUTO: - return None - - next_level = TrustLevel(current + 1) - thresholds = TRUST_THRESHOLDS.get(next_level) - if not thresholds: - return None - - if ( - self.metrics.total_actions >= thresholds["min_actions"] - and self.metrics.accuracy >= thresholds["min_accuracy"] - and self.metrics.days_active >= thresholds["min_days"] - ): - return next_level - - return None - - def upgrade_level(self, new_level: TrustLevel, reason: str = "auto") -> None: - """Upgrade to a new trust level.""" - if new_level <= self.current_level: - return - - now = datetime.now(timezone.utc).isoformat() - self.level_history.append( - { - "from_level": self.current_level.value, - "to_level": new_level.value, - "reason": reason, - "timestamp": now, - "metrics_snapshot": self.metrics.to_dict(), - } - ) - self.current_level = new_level - self.last_level_change = now - - def downgrade_level(self, reason: str = "override") -> None: - """Downgrade trust level due to override or errors.""" - if self.current_level <= TrustLevel.L0_REVIEW_ONLY: - return - - new_level = TrustLevel(self.current_level - 1) - now = datetime.now(timezone.utc).isoformat() - self.level_history.append( - { - "from_level": self.current_level.value, - "to_level": new_level.value, - "reason": reason, - "timestamp": now, - } - ) - self.current_level = new_level - self.last_level_change = now - - def set_manual_override(self, level: TrustLevel | None) -> None: - """Set or clear manual trust level override.""" - self.manual_override = level - if level is not None: - now = datetime.now(timezone.utc).isoformat() - self.level_history.append( - { - "from_level": self.current_level.value, - "to_level": level.value, - "reason": "manual_override", - "timestamp": now, - } - ) - - def to_dict(self) -> dict[str, Any]: - return { - "repo": self.repo, - "current_level": self.current_level.value, - "metrics": self.metrics.to_dict(), - "manual_override": self.manual_override.value - if self.manual_override - else None, - "last_level_change": self.last_level_change, - "level_history": self.level_history[-20:], # Keep last 20 changes - } - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> TrustState: - return cls( - repo=data["repo"], - current_level=TrustLevel(data.get("current_level", 0)), - metrics=AccuracyMetrics.from_dict(data.get("metrics", {})), - manual_override=TrustLevel(data["manual_override"]) - if data.get("manual_override") is not None - else None, - last_level_change=data.get("last_level_change"), - level_history=data.get("level_history", []), - ) - - -class TrustManager: - """ - Manages trust levels across repositories. - - Usage: - trust = TrustManager(state_dir=Path(".auto-claude/github")) - - # Check if action is allowed - if trust.can_perform("owner/repo", "auto_fix"): - perform_auto_fix() - - # Record action outcome - trust.record_action("owner/repo", "review", correct=True) - - # Check for upgrade - if trust.check_and_upgrade("owner/repo"): - print("Trust level upgraded!") - """ - - def __init__(self, state_dir: Path): - self.state_dir = state_dir - self.trust_dir = state_dir / "trust" - self.trust_dir.mkdir(parents=True, exist_ok=True) - self._states: dict[str, TrustState] = {} - - def _get_state_file(self, repo: str) -> Path: - safe_name = repo.replace("/", "_") - return self.trust_dir / f"{safe_name}.json" - - def get_state(self, repo: str) -> TrustState: - """Get trust state for a repository.""" - if repo in self._states: - return self._states[repo] - - state_file = self._get_state_file(repo) - if state_file.exists(): - try: - with open(state_file, encoding="utf-8") as f: - data = json.load(f) - state = TrustState.from_dict(data) - except (json.JSONDecodeError, UnicodeDecodeError): - # Return default state if file is corrupted - state = TrustState(repo=repo) - else: - state = TrustState(repo=repo) - - self._states[repo] = state - return state - - def save_state(self, repo: str) -> None: - """Save trust state for a repository with secure file permissions.""" - import os - - state = self.get_state(repo) - state_file = self._get_state_file(repo) - - # Write with restrictive permissions (0o600 = owner read/write only) - fd = os.open(str(state_file), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) - # os.fdopen takes ownership of fd and will close it when the with block exits - with os.fdopen(fd, "w", encoding="utf-8") as f: - json.dump(state.to_dict(), f, indent=2) - - def get_trust_level(self, repo: str) -> TrustLevel: - """Get current trust level for a repository.""" - return self.get_state(repo).effective_level - - def can_perform(self, repo: str, action: str) -> bool: - """Check if an action is allowed for a repository.""" - return self.get_state(repo).can_perform(action) - - def record_action( - self, - repo: str, - action_type: str, - correct: bool, - overridden: bool = False, - ) -> None: - """Record an action outcome.""" - state = self.get_state(repo) - state.metrics.record_action(action_type, correct, overridden) - - # Check for downgrade on override - if overridden: - # Downgrade if override rate exceeds 10% - if state.metrics.override_rate > 0.10 and state.metrics.total_actions >= 10: - state.downgrade_level(reason="high_override_rate") - - self.save_state(repo) - - def check_and_upgrade(self, repo: str) -> bool: - """Check for and apply trust level upgrade.""" - state = self.get_state(repo) - new_level = state.check_upgrade() - - if new_level: - state.upgrade_level(new_level, reason="threshold_met") - self.save_state(repo) - return True - - return False - - def set_manual_level(self, repo: str, level: TrustLevel) -> None: - """Manually set trust level for a repository.""" - state = self.get_state(repo) - state.set_manual_override(level) - self.save_state(repo) - - def clear_manual_override(self, repo: str) -> None: - """Clear manual trust level override.""" - state = self.get_state(repo) - state.set_manual_override(None) - self.save_state(repo) - - def get_progress(self, repo: str) -> dict[str, Any]: - """Get progress toward next trust level.""" - state = self.get_state(repo) - return { - "current_level": state.effective_level.value, - "current_level_name": state.effective_level.display_name, - "is_manual_override": state.manual_override is not None, - "accuracy": state.metrics.accuracy, - "total_actions": state.metrics.total_actions, - "override_rate": state.metrics.override_rate, - "days_active": state.metrics.days_active, - "progress_to_next": state.get_progress_to_next_level(), - } - - def get_all_states(self) -> list[TrustState]: - """Get trust states for all repos.""" - states = [] - for file in self.trust_dir.glob("*.json"): - try: - with open(file, encoding="utf-8") as f: - data = json.load(f) - states.append(TrustState.from_dict(data)) - except (json.JSONDecodeError, UnicodeDecodeError): - # Skip corrupted state files - continue - return states - - def get_summary(self) -> dict[str, Any]: - """Get summary of trust across all repos.""" - states = self.get_all_states() - by_level = {} - for state in states: - level = state.effective_level.value - by_level[level] = by_level.get(level, 0) + 1 - - total_actions = sum(s.metrics.total_actions for s in states) - total_correct = sum(s.metrics.correct_actions for s in states) - - return { - "total_repos": len(states), - "by_level": by_level, - "total_actions": total_actions, - "overall_accuracy": total_correct / max(1, total_actions), - } diff --git a/apps/frontend/src/main/ipc-handlers/github/__tests__/ai-triage-handlers.test.ts b/apps/frontend/src/main/ipc-handlers/github/__tests__/ai-triage-handlers.test.ts index 3baa9d1d..14f1c3f8 100644 --- a/apps/frontend/src/main/ipc-handlers/github/__tests__/ai-triage-handlers.test.ts +++ b/apps/frontend/src/main/ipc-handlers/github/__tests__/ai-triage-handlers.test.ts @@ -499,46 +499,3 @@ describe('applyTriageResults handler', () => { }); }); -// ============================================ -// saveProgressiveTrust -// ============================================ - -describe('saveProgressiveTrust handler', () => { - const trustConfig = { - autoApply: { - type: { enabled: true, threshold: 0.85 }, - priority: { enabled: false, threshold: 0.9 }, - labels: { enabled: true, threshold: 0.8 }, - duplicate: { enabled: false, threshold: 0.9 }, - }, - batchSize: 25, - confirmAbove: 5, - }; - - const call = (projectId: string, config: typeof trustConfig) => - handleHandlers['github:triage:saveTrust']({}, projectId, config); - - it('saves config successfully', async () => { - const { writeJsonWithRetry } = await import('../../../utils/atomic-file'); - const result = await call('test-project', trustConfig); - - expect(writeJsonWithRetry).toHaveBeenCalled(); - expect(result).toBe(true); - }); -}); - -// ============================================ -// getProgressiveTrust -// ============================================ - -describe('getProgressiveTrust handler', () => { - const call = (projectId: string) => - handleHandlers['github:triage:getTrust']({}, projectId); - - it('returns default config when no file exists', async () => { - const result = await call('test-project') as { batchSize: number }; - - expect(result).toBeDefined(); - expect(result.batchSize).toBe(50); - }); -}); diff --git a/apps/frontend/src/main/ipc-handlers/github/ai-triage-handlers.ts b/apps/frontend/src/main/ipc-handlers/github/ai-triage-handlers.ts index 294a2b82..30e1bcf6 100644 --- a/apps/frontend/src/main/ipc-handlers/github/ai-triage-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/github/ai-triage-handlers.ts @@ -33,7 +33,6 @@ import { import { getToolPath } from '../../cli-tool-manager'; import { killProcessGracefully } from '../../platform'; import { MAX_SPLIT_SUB_ISSUES } from '../../../shared/constants/ai-triage'; -import { createDefaultProgressiveTrust } from '../../../shared/types/ai-triage'; import { readEnrichmentFile, writeEnrichmentFile, withEnrichmentFileLock, appendTransition } from './enrichment-persistence'; import { createDefaultEnrichment } from '../../../shared/types/enrichment'; import type { TriageCategory } from '../../../shared/types/enrichment'; @@ -44,7 +43,6 @@ import type { EnrichmentProgress, SplitProgress, ApplyResultsProgress, - ProgressiveTrustConfig, } from '../../../shared/types/ai-triage'; const { debug: debugLog } = createContextLogger('AI Triage'); @@ -442,63 +440,6 @@ export function registerAITriageHandlers( }, ); - // ============================================ - // Save progressive trust config - // ============================================ - ipcMain.handle( - IPC_CHANNELS.GITHUB_TRIAGE_SAVE_TRUST, - async (_, projectId: string, config: ProgressiveTrustConfig): Promise => { - debugLog('saveProgressiveTrust handler called', { projectId }); - const result = await withProjectOrNull(projectId, async (project) => { - const githubDir = getGitHubDir(project.path); - fs.mkdirSync(githubDir, { recursive: true }); - - const configPath = path.join(githubDir, 'config.json'); - let existingConfig: Record = {}; - - try { - existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - } catch { - // Use empty config if file doesn't exist - } - - const updatedConfig = { - ...existingConfig, - progressive_trust: config, - }; - - await writeJsonWithRetry(configPath, updatedConfig, { indent: 2 }); - return true; - }); - return result ?? false; - }, - ); - - // ============================================ - // Get progressive trust config - // ============================================ - ipcMain.handle( - IPC_CHANNELS.GITHUB_TRIAGE_GET_TRUST, - async (_, projectId: string): Promise => { - debugLog('getProgressiveTrust handler called', { projectId }); - const result = await withProjectOrNull(projectId, async (project) => { - const configPath = path.join(getGitHubDir(project.path), 'config.json'); - - try { - const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - if (data.progressive_trust) { - return data.progressive_trust as ProgressiveTrustConfig; - } - } catch { - // Return default - } - - return createDefaultProgressiveTrust(); - }); - return result ?? createDefaultProgressiveTrust(); - }, - ); - // ============================================ // Save pending review queue // ============================================ diff --git a/apps/frontend/src/preload/api/modules/github-api.ts b/apps/frontend/src/preload/api/modules/github-api.ts index 3db53d2c..cb006f8f 100644 --- a/apps/frontend/src/preload/api/modules/github-api.ts +++ b/apps/frontend/src/preload/api/modules/github-api.ts @@ -32,7 +32,6 @@ import type { EnrichmentProgress, SplitProgress, ApplyResultsProgress, - ProgressiveTrustConfig, } from '../../../shared/types/ai-triage'; import { createIpcListener, invokeIpc, sendIpc, IpcListenerCleanup } from './ipc-utils'; @@ -429,9 +428,6 @@ export interface GitHubAPI { savePendingReview: (projectId: string, items: TriageReviewItem[]) => Promise; loadPendingReview: (projectId: string) => Promise; - saveProgressiveTrust: (projectId: string, config: ProgressiveTrustConfig) => Promise; - getProgressiveTrust: (projectId: string) => Promise; - // Label Sync (Phase 4) enableLabelSync: (projectId: string) => Promise; disableLabelSync: (projectId: string, cleanup: boolean) => Promise<{ success: boolean }>; @@ -1089,12 +1085,6 @@ export const createGitHubAPI = (): GitHubAPI => ({ loadPendingReview: (projectId: string): Promise => invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_LOAD_PENDING_REVIEW, projectId), - saveProgressiveTrust: (projectId: string, config: ProgressiveTrustConfig): Promise => - invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_SAVE_TRUST, projectId, config), - - getProgressiveTrust: (projectId: string): Promise => - invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_GET_TRUST, projectId), - // Label Sync (Phase 4) enableLabelSync: (projectId: string): Promise => invokeIpc(IPC_CHANNELS.GITHUB_LABEL_SYNC_ENABLE, projectId), diff --git a/apps/frontend/src/renderer/__tests__/ai-triage-store.test.ts b/apps/frontend/src/renderer/__tests__/ai-triage-store.test.ts deleted file mode 100644 index 135868db..00000000 --- a/apps/frontend/src/renderer/__tests__/ai-triage-store.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { useAITriageStore } from '../stores/github/ai-triage-store'; -import type { TriageReviewItem } from '../../shared/types/ai-triage'; - -function createReviewItem(overrides: Partial = {}): TriageReviewItem { - return { - issueNumber: 1, - issueTitle: 'Test Issue', - result: { - category: 'bug', - confidence: 0.9, - labelsToAdd: ['bug'], - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'high', - triagedAt: '2026-01-01T00:00:00Z', - }, - status: 'pending', - ...overrides, - }; -} - -beforeEach(() => { - useAITriageStore.setState({ - isTriaging: false, - triageProgress: null, - reviewItems: [], - enrichmentProgress: null, - splitSuggestion: null, - splitProgress: null, - }); -}); - -describe('initial state', () => { - it('has no results, not triaging, no review items', () => { - const state = useAITriageStore.getState(); - expect(state.isTriaging).toBe(false); - expect(state.triageProgress).toBeNull(); - expect(state.reviewItems).toEqual([]); - expect(state.enrichmentProgress).toBeNull(); - expect(state.splitSuggestion).toBeNull(); - expect(state.splitProgress).toBeNull(); - }); -}); - -describe('startTriage / endTriage', () => { - it('startTriage sets isTriaging to true', () => { - useAITriageStore.getState().startTriage(); - expect(useAITriageStore.getState().isTriaging).toBe(true); - }); - - it('endTriage sets isTriaging to false', () => { - useAITriageStore.getState().startTriage(); - useAITriageStore.getState().endTriage(); - expect(useAITriageStore.getState().isTriaging).toBe(false); - }); -}); - -describe('setTriageProgress', () => { - it('updates progress', () => { - const progress = { phase: 'analyzing' as const, progress: 50, message: 'Processing...' }; - useAITriageStore.getState().setTriageProgress(progress); - expect(useAITriageStore.getState().triageProgress).toEqual(progress); - }); -}); - -describe('review items', () => { - it('addReviewItems adds items to review queue', () => { - const items = [createReviewItem({ issueNumber: 1 }), createReviewItem({ issueNumber: 2 })]; - useAITriageStore.getState().addReviewItems(items); - expect(useAITriageStore.getState().reviewItems).toHaveLength(2); - }); - - it('acceptReviewItem changes status to accepted', () => { - useAITriageStore.getState().addReviewItems([createReviewItem({ issueNumber: 1 })]); - useAITriageStore.getState().acceptReviewItem(1); - expect(useAITriageStore.getState().reviewItems[0].status).toBe('accepted'); - }); - - it('rejectReviewItem changes status to rejected', () => { - useAITriageStore.getState().addReviewItems([createReviewItem({ issueNumber: 1 })]); - useAITriageStore.getState().rejectReviewItem(1); - expect(useAITriageStore.getState().reviewItems[0].status).toBe('rejected'); - }); - - it('acceptAllRemaining accepts all pending items', () => { - useAITriageStore.getState().addReviewItems([ - createReviewItem({ issueNumber: 1 }), - createReviewItem({ issueNumber: 2, status: 'rejected' }), - createReviewItem({ issueNumber: 3 }), - ]); - useAITriageStore.getState().acceptAllRemaining(); - - const items = useAITriageStore.getState().reviewItems; - expect(items[0].status).toBe('accepted'); - expect(items[1].status).toBe('rejected'); // Unchanged — already decided - expect(items[2].status).toBe('accepted'); - }); - - it('dismissReview clears review queue', () => { - useAITriageStore.getState().addReviewItems([createReviewItem()]); - useAITriageStore.getState().dismissReview(); - expect(useAITriageStore.getState().reviewItems).toEqual([]); - }); - - it('getUnreviewedCount returns count of pending items', () => { - useAITriageStore.getState().addReviewItems([ - createReviewItem({ issueNumber: 1 }), - createReviewItem({ issueNumber: 2, status: 'accepted' }), - createReviewItem({ issueNumber: 3 }), - ]); - expect(useAITriageStore.getState().getUnreviewedCount()).toBe(2); - }); -}); - -describe('enrichment progress', () => { - it('setEnrichmentProgress sets progress', () => { - const progress = { phase: 'analyzing' as const, progress: 30, message: 'Analyzing...' }; - useAITriageStore.getState().setEnrichmentProgress(progress); - expect(useAITriageStore.getState().enrichmentProgress).toEqual(progress); - }); - - it('clearEnrichmentProgress clears progress', () => { - useAITriageStore.getState().setEnrichmentProgress({ - phase: 'analyzing', - progress: 30, - message: 'X', - }); - useAITriageStore.getState().clearEnrichmentProgress(); - expect(useAITriageStore.getState().enrichmentProgress).toBeNull(); - }); -}); - -describe('split suggestion', () => { - it('setSplitSuggestion sets suggestion', () => { - const suggestion = { - issueNumber: 42, - subIssues: [{ title: 'Sub 1', body: 'Body', labels: [] }], - rationale: 'Too broad', - confidence: 0.9, - }; - useAITriageStore.getState().setSplitSuggestion(suggestion); - expect(useAITriageStore.getState().splitSuggestion).toEqual(suggestion); - }); - - it('clearSplitSuggestion clears suggestion', () => { - useAITriageStore.getState().setSplitSuggestion({ - issueNumber: 42, - subIssues: [], - rationale: 'X', - confidence: 0.5, - }); - useAITriageStore.getState().clearSplitSuggestion(); - expect(useAITriageStore.getState().splitSuggestion).toBeNull(); - }); -}); - -describe('split progress', () => { - it('setSplitProgress sets progress', () => { - const progress = { phase: 'creating' as const, progress: 60, message: 'Creating sub-issues...' }; - useAITriageStore.getState().setSplitProgress(progress); - expect(useAITriageStore.getState().splitProgress).toEqual(progress); - }); - - it('clearSplitProgress clears progress', () => { - useAITriageStore.getState().setSplitProgress({ - phase: 'creating', - progress: 60, - message: 'X', - }); - useAITriageStore.getState().clearSplitProgress(); - expect(useAITriageStore.getState().splitProgress).toBeNull(); - }); -}); diff --git a/apps/frontend/src/renderer/components/github-issues/__tests__/ai-triage-integration.test.ts b/apps/frontend/src/renderer/components/github-issues/__tests__/ai-triage-integration.test.ts deleted file mode 100644 index 0c7094c9..00000000 --- a/apps/frontend/src/renderer/components/github-issues/__tests__/ai-triage-integration.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * Integration test verifying the AI triage flow from store → constants → types. - */ -import { describe, it, expect, beforeEach } from 'vitest'; -import { useAITriageStore } from '../../../stores/github/ai-triage-store'; -import { - CONFIDENCE_HIGH, - CONFIDENCE_MEDIUM, - DEFAULT_BATCH_SIZE, - DEFAULT_CONFIRM_ABOVE, - MAX_SPLIT_SUB_ISSUES, - THRESHOLD_MIN, - THRESHOLD_MAX, - getConfidenceLevel, - isValidThreshold, - clampThreshold, - estimateBatchCost, -} from '@shared/constants/ai-triage'; -import { - createDefaultProgressiveTrust, - mapTriageCategory, -} from '@shared/types/ai-triage'; -import type { TriageReviewItem } from '@shared/types/ai-triage'; - -function createItem( - issueNumber: number, - status: TriageReviewItem['status'] = 'pending', -): TriageReviewItem { - return { - issueNumber, - issueTitle: `Issue #${issueNumber}`, - result: { - category: 'bug', - confidence: 0.85, - labelsToAdd: ['bug'], - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'high', - triagedAt: '2026-01-01T00:00:00Z', - }, - status, - }; -} - -beforeEach(() => { - useAITriageStore.setState({ - isTriaging: false, - triageProgress: null, - reviewItems: [], - enrichmentProgress: null, - splitSuggestion: null, - splitProgress: null, - }); -}); - -// ============================================ -// Store + constants consistency -// ============================================ - -describe('store + constants consistency', () => { - it('default progressive trust uses expected constant values', () => { - const defaults = createDefaultProgressiveTrust(); - expect(defaults.batchSize).toBe(DEFAULT_BATCH_SIZE); - expect(defaults.confirmAbove).toBe(DEFAULT_CONFIRM_ABOVE); - }); - - it('all trust categories start disabled at 0.9 threshold', () => { - const defaults = createDefaultProgressiveTrust(); - for (const category of ['type', 'priority', 'labels', 'duplicate'] as const) { - expect(defaults.autoApply[category].enabled).toBe(false); - expect(defaults.autoApply[category].threshold).toBe(0.9); - expect(isValidThreshold(defaults.autoApply[category].threshold)).toBe(true); - } - }); - - it('category mapping covers all enrichment categories', () => { - const pythonCategories = [ - 'bug', 'feature', 'documentation', 'question', - 'enhancement', 'chore', 'security', 'performance', - ]; - for (const cat of pythonCategories) { - expect(mapTriageCategory(cat)).toBe(cat); - } - }); - - it('category mapping maps special Python categories', () => { - expect(mapTriageCategory('duplicate')).toBe('bug'); - expect(mapTriageCategory('spam')).toBe('chore'); - expect(mapTriageCategory('feature_creep')).toBe('enhancement'); - }); - - it('unknown categories map to chore', () => { - expect(mapTriageCategory('unknown_type')).toBe('chore'); - }); -}); - -// ============================================ -// Progressive trust defaults -// ============================================ - -describe('progressive trust defaults', () => { - it('threshold bounds are correct', () => { - expect(THRESHOLD_MIN).toBe(0.5); - expect(THRESHOLD_MAX).toBe(1.0); - expect(THRESHOLD_MIN).toBeLessThan(THRESHOLD_MAX); - }); - - it('default thresholds are within valid range', () => { - const defaults = createDefaultProgressiveTrust(); - for (const category of ['type', 'priority', 'labels', 'duplicate'] as const) { - const threshold = defaults.autoApply[category].threshold; - expect(threshold).toBeGreaterThanOrEqual(THRESHOLD_MIN); - expect(threshold).toBeLessThanOrEqual(THRESHOLD_MAX); - } - }); - - it('clampThreshold enforces bounds', () => { - expect(clampThreshold(0.3)).toBe(THRESHOLD_MIN); - expect(clampThreshold(1.5)).toBe(THRESHOLD_MAX); - expect(clampThreshold(0.75)).toBe(0.75); - }); -}); - -// ============================================ -// Review item lifecycle -// ============================================ - -describe('review item lifecycle', () => { - it('pending → accepted → applied flow', () => { - const store = useAITriageStore.getState(); - - // Add pending items - store.addReviewItems([createItem(1), createItem(2), createItem(3)]); - expect(useAITriageStore.getState().reviewItems).toHaveLength(3); - expect(useAITriageStore.getState().getUnreviewedCount()).toBe(3); - - // Accept one - useAITriageStore.getState().acceptReviewItem(1); - const item1 = useAITriageStore.getState().reviewItems.find((i) => i.issueNumber === 1); - expect(item1?.status).toBe('accepted'); - expect(useAITriageStore.getState().getUnreviewedCount()).toBe(2); - - // Reject one - useAITriageStore.getState().rejectReviewItem(2); - const item2 = useAITriageStore.getState().reviewItems.find((i) => i.issueNumber === 2); - expect(item2?.status).toBe('rejected'); - expect(useAITriageStore.getState().getUnreviewedCount()).toBe(1); - }); - - it('acceptAllRemaining accepts only pending items', () => { - const store = useAITriageStore.getState(); - store.addReviewItems([ - createItem(1, 'rejected'), - createItem(2, 'pending'), - createItem(3, 'pending'), - ]); - - useAITriageStore.getState().acceptAllRemaining(); - - const items = useAITriageStore.getState().reviewItems; - expect(items.find((i) => i.issueNumber === 1)?.status).toBe('rejected'); - expect(items.find((i) => i.issueNumber === 2)?.status).toBe('accepted'); - expect(items.find((i) => i.issueNumber === 3)?.status).toBe('accepted'); - }); - - it('dismissReview clears all items', () => { - const store = useAITriageStore.getState(); - store.addReviewItems([createItem(1), createItem(2)]); - expect(useAITriageStore.getState().reviewItems).toHaveLength(2); - - useAITriageStore.getState().dismissReview(); - expect(useAITriageStore.getState().reviewItems).toHaveLength(0); - }); -}); - -// ============================================ -// Split suggestion capping -// ============================================ - -describe('split suggestion capping', () => { - it('MAX_SPLIT_SUB_ISSUES is 5', () => { - expect(MAX_SPLIT_SUB_ISSUES).toBe(5); - }); - - it('store accepts split suggestion with sub-issues at max', () => { - const subIssues = Array.from({ length: MAX_SPLIT_SUB_ISSUES }, (_, i) => ({ - title: `Sub ${i + 1}`, - body: `Body ${i + 1}`, - labels: [], - })); - - useAITriageStore.getState().setSplitSuggestion({ - issueNumber: 42, - subIssues, - rationale: 'Too complex', - confidence: 0.9, - }); - - const suggestion = useAITriageStore.getState().splitSuggestion; - expect(suggestion?.subIssues).toHaveLength(MAX_SPLIT_SUB_ISSUES); - }); - - it('clearSplitSuggestion removes the suggestion', () => { - useAITriageStore.getState().setSplitSuggestion({ - issueNumber: 42, - subIssues: [{ title: 'Sub', body: 'Body', labels: [] }], - rationale: 'Test', - confidence: 0.8, - }); - - useAITriageStore.getState().clearSplitSuggestion(); - expect(useAITriageStore.getState().splitSuggestion).toBeNull(); - }); -}); - -// ============================================ -// Confidence level helper at boundaries -// ============================================ - -describe('confidence level helper at boundaries', () => { - it('exactly at CONFIDENCE_HIGH returns high', () => { - expect(getConfidenceLevel(CONFIDENCE_HIGH)).toBe('high'); - }); - - it('just below CONFIDENCE_HIGH returns medium', () => { - expect(getConfidenceLevel(CONFIDENCE_HIGH - 0.001)).toBe('medium'); - }); - - it('exactly at CONFIDENCE_MEDIUM returns medium', () => { - expect(getConfidenceLevel(CONFIDENCE_MEDIUM)).toBe('medium'); - }); - - it('just below CONFIDENCE_MEDIUM returns low', () => { - expect(getConfidenceLevel(CONFIDENCE_MEDIUM - 0.001)).toBe('low'); - }); - - it('0 returns low', () => { - expect(getConfidenceLevel(0)).toBe('low'); - }); - - it('1 returns high', () => { - expect(getConfidenceLevel(1)).toBe('high'); - }); - - it('estimateBatchCost returns formatted string', () => { - const cost = estimateBatchCost(10, 'sonnet'); - expect(cost).toMatch(/~\$\d+\.\d{2}/); - }); -}); diff --git a/apps/frontend/src/renderer/components/github-issues/__tests__/ai-triage-integration.test.tsx b/apps/frontend/src/renderer/components/github-issues/__tests__/ai-triage-integration.test.tsx index 17926df5..554d4dbe 100644 --- a/apps/frontend/src/renderer/components/github-issues/__tests__/ai-triage-integration.test.tsx +++ b/apps/frontend/src/renderer/components/github-issues/__tests__/ai-triage-integration.test.tsx @@ -14,7 +14,7 @@ vi.mock('react-i18next', () => ({ describe('AI Triage integration', () => { it('TriageProgressOverlay renders progress bar and message', () => { - const progress = { progress: 60, message: 'Enriching issue...' }; + const progress = { phase: 'generating' as const, progress: 60, message: 'Enriching issue...' }; render(); expect(screen.getByText('Enriching issue...')).toBeDefined(); expect(screen.getByRole('progressbar')).toBeDefined(); @@ -22,7 +22,7 @@ describe('AI Triage integration', () => { it('TriageProgressOverlay cancel button calls onCancel', () => { const onCancel = vi.fn(); - const progress = { progress: 30, message: 'Working...' }; + const progress = { phase: 'analyzing' as const, progress: 30, message: 'Working...' }; render(); fireEvent.click(screen.getByRole('button')); expect(onCancel).toHaveBeenCalledOnce(); @@ -32,6 +32,7 @@ describe('AI Triage integration', () => { const suggestion = { issueNumber: 42, rationale: 'This issue should be split', + confidence: 0.9, subIssues: [ { title: 'Sub-issue A', body: 'Body A', labels: ['bug'] }, { title: 'Sub-issue B', body: 'Body B', labels: [] }, @@ -55,6 +56,7 @@ describe('AI Triage integration', () => { const suggestion = { issueNumber: 42, rationale: 'Split needed', + confidence: 0.85, subIssues: [{ title: 'Sub A', body: 'Body A', labels: [] }], }; render( diff --git a/apps/frontend/src/renderer/components/github-issues/__tests__/phase5-exports.test.ts b/apps/frontend/src/renderer/components/github-issues/__tests__/phase5-exports.test.ts index 489813a4..15e9b384 100644 --- a/apps/frontend/src/renderer/components/github-issues/__tests__/phase5-exports.test.ts +++ b/apps/frontend/src/renderer/components/github-issues/__tests__/phase5-exports.test.ts @@ -41,8 +41,6 @@ describe('Phase 5 Barrel Exports', () => { expect(exportNames).toContain('BatchTriageReview'); expect(exportNames).toContain('IssueSplitDialog'); expect(exportNames).toContain('TriageProgressOverlay'); - expect(exportNames).toContain('ProgressiveTrustSettings'); - // Phase 4 expect(exportNames).toContain('LabelSyncSettings'); expect(exportNames).toContain('DependencyList'); diff --git a/apps/frontend/src/renderer/components/github-issues/components/ProgressiveTrustSettings.tsx b/apps/frontend/src/renderer/components/github-issues/components/ProgressiveTrustSettings.tsx deleted file mode 100644 index 7cfde42c..00000000 --- a/apps/frontend/src/renderer/components/github-issues/components/ProgressiveTrustSettings.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Progressive Trust Settings — configure auto-apply thresholds per category. - */ - -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import type { ProgressiveTrustConfig } from '@shared/types/ai-triage'; -import { THRESHOLD_MIN, THRESHOLD_MAX, THRESHOLD_STEP } from '@shared/constants/ai-triage'; -import type { TrustLevel } from '@shared/constants/ai-triage'; - -const CATEGORIES = ['type', 'priority', 'labels', 'duplicate'] as const; -type Category = (typeof CATEGORIES)[number]; - -const TRUST_LEVELS: TrustLevel[] = ['crawl', 'walk', 'run']; - -const TRUST_LEVEL_PRESETS: Record> = { - crawl: { type: false, priority: false, labels: false, duplicate: false }, - walk: { type: false, priority: false, labels: true, duplicate: true }, - run: { type: true, priority: true, labels: true, duplicate: true }, -}; - -function deriveTrustLevel(config: ProgressiveTrustConfig): TrustLevel { - const allEnabled = CATEGORIES.every((c) => config.autoApply[c].enabled); - const noneEnabled = CATEGORIES.every((c) => !config.autoApply[c].enabled); - if (allEnabled) return 'run'; - if (noneEnabled) return 'crawl'; - return 'walk'; -} - -interface ProgressiveTrustSettingsProps { - config: ProgressiveTrustConfig; - onSave: (config: ProgressiveTrustConfig) => void; - onCancel: () => void; -} - -export function ProgressiveTrustSettings({ config: initialConfig, onSave, onCancel }: ProgressiveTrustSettingsProps) { - const { t } = useTranslation(['common']); - const [config, setConfig] = useState(() => - JSON.parse(JSON.stringify(initialConfig)), - ); - - const toggleCategory = (category: Category) => { - setConfig((prev) => ({ - ...prev, - autoApply: { - ...prev.autoApply, - [category]: { - ...prev.autoApply[category], - enabled: !prev.autoApply[category].enabled, - }, - }, - })); - }; - - const setTrustLevel = (level: TrustLevel) => { - const preset = TRUST_LEVEL_PRESETS[level]; - setConfig((prev) => ({ - ...prev, - autoApply: { - type: { ...prev.autoApply.type, enabled: preset.type }, - priority: { ...prev.autoApply.priority, enabled: preset.priority }, - labels: { ...prev.autoApply.labels, enabled: preset.labels }, - duplicate: { ...prev.autoApply.duplicate, enabled: preset.duplicate }, - }, - })); - }; - - const currentLevel = deriveTrustLevel(config); - - const setThreshold = (category: Category, threshold: number) => { - setConfig((prev) => ({ - ...prev, - autoApply: { - ...prev.autoApply, - [category]: { - ...prev.autoApply[category], - threshold, - }, - }, - })); - }; - - return ( -
- {/* Trust level radio group */} -
- {t('common:progressiveTrust.trustLevel')} -
- {TRUST_LEVELS.map((level) => ( - - ))} -
- {currentLevel === 'run' && ( -

- {t('common:progressiveTrust.runWarning')} -

- )} -
- - {/* Category rows */} - {CATEGORIES.map((category) => ( -
- - setThreshold(category, Number.parseFloat(e.target.value))} - disabled={!config.autoApply[category].enabled} - className="flex-1" - aria-label={t('common:progressiveTrust.threshold', { category })} - /> - - {Math.round(config.autoApply[category].threshold * 100)}% - -
- ))} - - {/* Batch size */} -
- - setConfig((prev) => ({ ...prev, batchSize: Number.parseInt(e.target.value, 10) || 0 }))} - className="w-20 bg-transparent text-sm border border-border/50 rounded px-2 py-1 outline-none" - min={1} - max={200} - /> -
- - {/* Confirm above */} -
- - setConfig((prev) => ({ ...prev, confirmAbove: Number.parseInt(e.target.value, 10) || 0 }))} - className="w-20 bg-transparent text-sm border border-border/50 rounded px-2 py-1 outline-none" - min={0} - max={100} - /> -
- - {/* Actions */} -
- - -
-
- ); -} diff --git a/apps/frontend/src/renderer/components/github-issues/components/ProgressiveTrustSettingsConnected.tsx b/apps/frontend/src/renderer/components/github-issues/components/ProgressiveTrustSettingsConnected.tsx deleted file mode 100644 index 502b8f5e..00000000 --- a/apps/frontend/src/renderer/components/github-issues/components/ProgressiveTrustSettingsConnected.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Connected wrapper for ProgressiveTrustSettings that loads config via IPC. - * Used in SectionRouter where hooks can't be called conditionally. - */ - -import { useState, useEffect, useCallback } from 'react'; -import { ProgressiveTrustSettings } from './ProgressiveTrustSettings'; -import { useProjectStore } from '../../../stores/project-store'; -import { createDefaultProgressiveTrust } from '@shared/types/ai-triage'; -import type { ProgressiveTrustConfig } from '@shared/types/ai-triage'; - -export function ProgressiveTrustSettingsConnected() { - const projectId = useProjectStore((s) => s.activeProjectId); - const [config, setConfig] = useState(createDefaultProgressiveTrust); - const [visible, setVisible] = useState(false); - - useEffect(() => { - if (!projectId) return; - window.electronAPI.github - .getProgressiveTrust(projectId) - .then((c) => { - setConfig(c); - setVisible(true); - }) - .catch(() => { - setVisible(true); - }); - }, [projectId]); - - const handleSave = useCallback( - async (updated: ProgressiveTrustConfig) => { - if (!projectId) return; - await window.electronAPI.github.saveProgressiveTrust(projectId, updated); - setConfig(updated); - }, - [projectId], - ); - - const handleCancel = useCallback(() => { - // Reset to last saved config by re-fetching - if (!projectId) return; - window.electronAPI.github - .getProgressiveTrust(projectId) - .then(setConfig) - .catch(() => { /* config not available yet — use defaults */ }); - }, [projectId]); - - if (!visible) return null; - - return ( - - ); -} diff --git a/apps/frontend/src/renderer/components/github-issues/components/__tests__/ProgressiveTrustSettings.test.tsx b/apps/frontend/src/renderer/components/github-issues/components/__tests__/ProgressiveTrustSettings.test.tsx deleted file mode 100644 index 6b76b16f..00000000 --- a/apps/frontend/src/renderer/components/github-issues/components/__tests__/ProgressiveTrustSettings.test.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { ProgressiveTrustSettings } from '../ProgressiveTrustSettings'; -import { createDefaultProgressiveTrust } from '@shared/types/ai-triage'; - -// Mock i18n -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - -const defaultProps = { - config: createDefaultProgressiveTrust(), - onSave: vi.fn(), - onCancel: vi.fn(), -}; - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe('ProgressiveTrustSettings', () => { - it('renders four category rows', () => { - render(); - expect(screen.getByText(/type/i)).toBeDefined(); - expect(screen.getByText(/priority/i)).toBeDefined(); - expect(screen.getByText(/labels/i)).toBeDefined(); - expect(screen.getByText(/duplicate/i)).toBeDefined(); - }); - - it('renders toggle for each category', () => { - render(); - const checkboxes = screen.getAllByRole('checkbox'); - expect(checkboxes.length).toBeGreaterThanOrEqual(4); - }); - - it('calls onSave when save button clicked', () => { - render(); - fireEvent.click(screen.getByRole('button', { name: /save/i })); - expect(defaultProps.onSave).toHaveBeenCalled(); - }); - - it('calls onCancel when cancel button clicked', () => { - render(); - fireEvent.click(screen.getByRole('button', { name: /cancel/i })); - expect(defaultProps.onCancel).toHaveBeenCalled(); - }); - - it('shows batch size input', () => { - render(); - const batchInput = screen.getByDisplayValue('50'); - expect(batchInput).toBeDefined(); - }); - - it('shows confirm-above input', () => { - render(); - const confirmInput = screen.getByDisplayValue('10'); - expect(confirmInput).toBeDefined(); - }); - - it('toggles category enabled state', () => { - render(); - const checkboxes = screen.getAllByRole('checkbox'); - fireEvent.click(checkboxes[0]); - // After save, the config should reflect the change - fireEvent.click(screen.getByRole('button', { name: /save/i })); - const savedConfig = defaultProps.onSave.mock.calls[0][0]; - expect(savedConfig.autoApply.type.enabled).toBe(true); - }); - - it('renders trust level radio group with Crawl/Walk/Run', () => { - render(); - const radios = screen.getAllByRole('radio'); - expect(radios).toHaveLength(3); - expect(screen.getByText('common:progressiveTrust.crawl')).toBeDefined(); - expect(screen.getByText('common:progressiveTrust.walk')).toBeDefined(); - expect(screen.getByText('common:progressiveTrust.run')).toBeDefined(); - }); - - it('defaults to Crawl when all categories disabled', () => { - render(); - const radios = screen.getAllByRole('radio'); - // Default config has all disabled → crawl selected - expect((radios[0] as HTMLInputElement).checked).toBe(true); - }); - - it('selecting Run enables all categories and shows warning', () => { - render(); - const radios = screen.getAllByRole('radio'); - // Click Run (third radio) - fireEvent.click(radios[2]); - // All checkboxes should be checked - const checkboxes = screen.getAllByRole('checkbox'); - for (const checkbox of checkboxes) { - expect((checkbox as HTMLInputElement).checked).toBe(true); - } - // Warning should be shown - expect(screen.getByRole('alert')).toBeDefined(); - expect(screen.getByText('common:progressiveTrust.runWarning')).toBeDefined(); - }); - - it('selecting Crawl disables all categories', () => { - // Start with all enabled (run-like config) - const runConfig = createDefaultProgressiveTrust(); - runConfig.autoApply.type.enabled = true; - runConfig.autoApply.priority.enabled = true; - runConfig.autoApply.labels.enabled = true; - runConfig.autoApply.duplicate.enabled = true; - - render(); - const radios = screen.getAllByRole('radio'); - // Click Crawl (first radio) - fireEvent.click(radios[0]); - // All checkboxes should be unchecked - const checkboxes = screen.getAllByRole('checkbox'); - for (const checkbox of checkboxes) { - expect((checkbox as HTMLInputElement).checked).toBe(false); - } - // No warning - expect(screen.queryByRole('alert')).toBeNull(); - }); - - it('selecting Walk enables labels and duplicate only', () => { - render(); - const radios = screen.getAllByRole('radio'); - // Click Walk (second radio) - fireEvent.click(radios[1]); - - // Save and check config - fireEvent.click(screen.getByRole('button', { name: /save/i })); - const savedConfig = defaultProps.onSave.mock.calls[0][0]; - expect(savedConfig.autoApply.type.enabled).toBe(false); - expect(savedConfig.autoApply.priority.enabled).toBe(false); - expect(savedConfig.autoApply.labels.enabled).toBe(true); - expect(savedConfig.autoApply.duplicate.enabled).toBe(true); - }); -}); diff --git a/apps/frontend/src/renderer/components/github-issues/components/__tests__/ProgressiveTrustSettingsConnected.test.tsx b/apps/frontend/src/renderer/components/github-issues/components/__tests__/ProgressiveTrustSettingsConnected.test.tsx deleted file mode 100644 index afcd5d22..00000000 --- a/apps/frontend/src/renderer/components/github-issues/components/__tests__/ProgressiveTrustSettingsConnected.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor, fireEvent } from '@testing-library/react'; -import { ProgressiveTrustSettingsConnected } from '../ProgressiveTrustSettingsConnected'; -import { createDefaultProgressiveTrust } from '@shared/types/ai-triage'; - -// Mock i18n -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - -// Mock project store -vi.mock('../../../../stores/project-store', () => ({ - useProjectStore: vi.fn((selector: (s: { activeProjectId: string | null }) => unknown) => - selector({ activeProjectId: 'proj-1' }), - ), -})); - -// Mock electronAPI -const mockGithub = { - getProgressiveTrust: vi.fn(), - saveProgressiveTrust: vi.fn(), -}; - -beforeEach(() => { - vi.clearAllMocks(); - (window as unknown as Record).electronAPI = { github: mockGithub }; - mockGithub.getProgressiveTrust.mockResolvedValue(createDefaultProgressiveTrust()); - mockGithub.saveProgressiveTrust.mockResolvedValue(true); -}); - -describe('ProgressiveTrustSettingsConnected', () => { - it('renders ProgressiveTrustSettings after config loaded', async () => { - render(); - await waitFor(() => { - expect(screen.getByText('common:progressiveTrust.crawl')).toBeDefined(); - }); - }); - - it('loads config from IPC on mount', async () => { - render(); - await waitFor(() => { - expect(mockGithub.getProgressiveTrust).toHaveBeenCalledWith('proj-1'); - }); - }); - - it('saves config via IPC on save', async () => { - render(); - await waitFor(() => { - expect(screen.getByRole('button', { name: /save/i })).toBeDefined(); - }); - fireEvent.click(screen.getByRole('button', { name: /save/i })); - await waitFor(() => { - expect(mockGithub.saveProgressiveTrust).toHaveBeenCalledWith('proj-1', expect.any(Object)); - }); - }); -}); diff --git a/apps/frontend/src/renderer/components/github-issues/components/index.ts b/apps/frontend/src/renderer/components/github-issues/components/index.ts index 435f9146..7c38682b 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/index.ts +++ b/apps/frontend/src/renderer/components/github-issues/components/index.ts @@ -24,7 +24,6 @@ export { TriageResultCard } from './TriageResultCard'; export { BatchTriageReview } from './BatchTriageReview'; export { IssueSplitDialog } from './IssueSplitDialog'; export { TriageProgressOverlay } from './TriageProgressOverlay'; -export { ProgressiveTrustSettings } from './ProgressiveTrustSettings'; // Phase 4 — Polish export { LabelSyncSettings } from './LabelSyncSettings'; diff --git a/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx b/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx index 60ba4bdf..c4118abe 100644 --- a/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx +++ b/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx @@ -8,7 +8,6 @@ import { GitHubIntegration } from '../integrations/GitHubIntegration'; import { GitLabIntegration } from '../integrations/GitLabIntegration'; import { InitializationGuard } from '../common/InitializationGuard'; import { LabelSyncSettingsConnected } from '../../github-issues/components/LabelSyncSettingsConnected'; -import { ProgressiveTrustSettingsConnected } from '../../github-issues/components/ProgressiveTrustSettingsConnected'; import { InvestigationSettings } from '../../github-issues/components/InvestigationSettings'; import type { ProjectSettingsSection } from '../ProjectSettingsContent'; @@ -143,7 +142,6 @@ export function SectionRouter({ setSettings={setSettings} /> - diff --git a/apps/frontend/src/renderer/lib/browser-mock.ts b/apps/frontend/src/renderer/lib/browser-mock.ts index 86210c34..73c9b9ae 100644 --- a/apps/frontend/src/renderer/lib/browser-mock.ts +++ b/apps/frontend/src/renderer/lib/browser-mock.ts @@ -312,9 +312,6 @@ const browserMockAPI: ElectronAPI = { savePendingReview: async () => true, loadPendingReview: async () => [], - saveProgressiveTrust: async () => true, - getProgressiveTrust: async () => ({ autoApply: { type: { enabled: false, threshold: 0.9 }, priority: { enabled: false, threshold: 0.9 }, labels: { enabled: false, threshold: 0.9 }, duplicate: { enabled: false, threshold: 0.9 } }, batchSize: 50, confirmAbove: 10 }), - // Label Sync (Phase 4) enableLabelSync: async () => ({ created: 0, updated: 0, removed: 0, errors: [] }), disableLabelSync: async () => ({ success: true }), diff --git a/apps/frontend/src/renderer/stores/github/__tests__/ai-triage-store.test.ts b/apps/frontend/src/renderer/stores/github/__tests__/ai-triage-store.test.ts deleted file mode 100644 index 83baaab2..00000000 --- a/apps/frontend/src/renderer/stores/github/__tests__/ai-triage-store.test.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { useAITriageStore } from '../ai-triage-store'; -import type { TriageReviewItem, ProgressiveTrustConfig } from '@shared/types/ai-triage'; - -function makeReviewItem( - overrides: Partial & { issueNumber: number }, -): TriageReviewItem { - return { - issueTitle: `Issue #${overrides.issueNumber}`, - status: 'pending', - result: { - category: 'bug', - confidence: 0.8, - labelsToAdd: [], - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'medium', - triagedAt: '2026-01-01T00:00:00Z', - }, - ...overrides, - }; -} - -function makeTrustConfig( - overrides?: Partial, -): ProgressiveTrustConfig { - return { - autoApply: { - type: { enabled: false, threshold: 0.9 }, - priority: { enabled: false, threshold: 0.9 }, - labels: { enabled: false, threshold: 0.9 }, - duplicate: { enabled: false, threshold: 0.9 }, - }, - batchSize: 50, - confirmAbove: 10, - ...overrides, - }; -} - -describe('useAITriageStore', () => { - beforeEach(() => { - useAITriageStore.setState({ - isTriaging: false, - triageProgress: null, - reviewItems: [], - enrichmentProgress: null, - enrichmentResult: null, - splitSuggestion: null, - splitProgress: null, - lastError: null, - lastBatchSnapshot: null, - }); - }); - - describe('autoApplyByTrust', () => { - it('auto-applies items above labels threshold when labels enabled', () => { - useAITriageStore.getState().addReviewItems([ - makeReviewItem({ - issueNumber: 1, - result: { - category: 'bug', - confidence: 0.95, - labelsToAdd: ['bug'], - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'high', - triagedAt: '2026-01-01T00:00:00Z', - }, - }), - ]); - - const config = makeTrustConfig({ - autoApply: { - type: { enabled: false, threshold: 0.9 }, - priority: { enabled: false, threshold: 0.9 }, - labels: { enabled: true, threshold: 0.9 }, - duplicate: { enabled: false, threshold: 0.9 }, - }, - }); - - useAITriageStore.getState().autoApplyByTrust(config); - - expect(useAITriageStore.getState().reviewItems[0].status).toBe('auto-applied'); - }); - - it('leaves items below threshold as pending', () => { - useAITriageStore.getState().addReviewItems([ - makeReviewItem({ - issueNumber: 1, - result: { - category: 'bug', - confidence: 0.7, - labelsToAdd: ['bug'], - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'medium', - triagedAt: '2026-01-01T00:00:00Z', - }, - }), - ]); - - const config = makeTrustConfig({ - autoApply: { - type: { enabled: false, threshold: 0.9 }, - priority: { enabled: false, threshold: 0.9 }, - labels: { enabled: true, threshold: 0.9 }, - duplicate: { enabled: false, threshold: 0.9 }, - }, - }); - - useAITriageStore.getState().autoApplyByTrust(config); - - expect(useAITriageStore.getState().reviewItems[0].status).toBe('pending'); - }); - - it('auto-applies duplicate items above threshold when duplicate enabled', () => { - useAITriageStore.getState().addReviewItems([ - makeReviewItem({ - issueNumber: 2, - result: { - category: 'bug', - confidence: 0.95, - labelsToAdd: [], - labelsToRemove: [], - isDuplicate: true, - duplicateOf: 1, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'medium', - triagedAt: '2026-01-01T00:00:00Z', - }, - }), - ]); - - const config = makeTrustConfig({ - autoApply: { - type: { enabled: false, threshold: 0.9 }, - priority: { enabled: false, threshold: 0.9 }, - labels: { enabled: false, threshold: 0.9 }, - duplicate: { enabled: true, threshold: 0.9 }, - }, - }); - - useAITriageStore.getState().autoApplyByTrust(config); - - expect(useAITriageStore.getState().reviewItems[0].status).toBe('auto-applied'); - }); - - it('skips disabled categories even when confidence is high', () => { - useAITriageStore.getState().addReviewItems([ - makeReviewItem({ - issueNumber: 1, - result: { - category: 'bug', - confidence: 0.99, - labelsToAdd: ['bug'], - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'high', - triagedAt: '2026-01-01T00:00:00Z', - }, - }), - ]); - - // All categories disabled - const config = makeTrustConfig(); - - useAITriageStore.getState().autoApplyByTrust(config); - - expect(useAITriageStore.getState().reviewItems[0].status).toBe('pending'); - }); - - it('does not touch already-accepted or rejected items', () => { - useAITriageStore.getState().addReviewItems([ - makeReviewItem({ - issueNumber: 1, - status: 'accepted', - result: { - category: 'bug', - confidence: 0.95, - labelsToAdd: ['bug'], - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'high', - triagedAt: '2026-01-01T00:00:00Z', - }, - }), - makeReviewItem({ - issueNumber: 2, - status: 'rejected', - result: { - category: 'bug', - confidence: 0.95, - labelsToAdd: ['bug'], - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'high', - triagedAt: '2026-01-01T00:00:00Z', - }, - }), - ]); - - const config = makeTrustConfig({ - autoApply: { - type: { enabled: false, threshold: 0.9 }, - priority: { enabled: false, threshold: 0.9 }, - labels: { enabled: true, threshold: 0.9 }, - duplicate: { enabled: false, threshold: 0.9 }, - }, - }); - - useAITriageStore.getState().autoApplyByTrust(config); - - const items = useAITriageStore.getState().reviewItems; - expect(items[0].status).toBe('accepted'); - expect(items[1].status).toBe('rejected'); - }); - - it('labels category requires labelsToAdd to be non-empty', () => { - useAITriageStore.getState().addReviewItems([ - makeReviewItem({ - issueNumber: 1, - result: { - category: 'bug', - confidence: 0.95, - labelsToAdd: [], // empty — no labels to apply - labelsToRemove: [], - isDuplicate: false, - isSpam: false, - isFeatureCreep: false, - suggestedBreakdown: [], - priority: 'high', - triagedAt: '2026-01-01T00:00:00Z', - }, - }), - ]); - - const config = makeTrustConfig({ - autoApply: { - type: { enabled: false, threshold: 0.9 }, - priority: { enabled: false, threshold: 0.9 }, - labels: { enabled: true, threshold: 0.9 }, - duplicate: { enabled: false, threshold: 0.9 }, - }, - }); - - useAITriageStore.getState().autoApplyByTrust(config); - - // Should remain pending since no labels to add - expect(useAITriageStore.getState().reviewItems[0].status).toBe('pending'); - }); - }); - - describe('enrichmentResult', () => { - it('setEnrichmentResult stores the result', () => { - const result = { - issueNumber: 42, - problem: 'Login fails', - goal: 'Fix login', - scopeIn: ['auth'], - scopeOut: ['signup'], - acceptanceCriteria: ['User can log in'], - technicalContext: 'React app', - risksEdgeCases: ['Token expiry'], - confidence: 0.9, - }; - useAITriageStore.getState().setEnrichmentResult(result); - expect(useAITriageStore.getState().enrichmentResult).toEqual(result); - }); - - it('clearEnrichmentResult resets to null', () => { - useAITriageStore.getState().setEnrichmentResult({ - issueNumber: 1, - problem: 'test', - goal: 'test', - scopeIn: [], - scopeOut: [], - acceptanceCriteria: [], - technicalContext: '', - risksEdgeCases: [], - confidence: 0.5, - }); - useAITriageStore.getState().clearEnrichmentResult(); - expect(useAITriageStore.getState().enrichmentResult).toBeNull(); - }); - }); - - describe('undoBatch', () => { - it('snapshotBeforeApply saves current review items', () => { - const items = [ - makeReviewItem({ issueNumber: 1, status: 'accepted' }), - makeReviewItem({ issueNumber: 2, status: 'rejected' }), - ]; - useAITriageStore.getState().addReviewItems(items); - useAITriageStore.getState().snapshotBeforeApply(); - - const snapshot = useAITriageStore.getState().lastBatchSnapshot; - expect(snapshot).toHaveLength(2); - expect(snapshot?.[0].status).toBe('accepted'); - }); - - it('undoLastBatch restores snapshot and clears it', () => { - const items = [ - makeReviewItem({ issueNumber: 1 }), - makeReviewItem({ issueNumber: 2 }), - ]; - useAITriageStore.getState().addReviewItems(items); - useAITriageStore.getState().snapshotBeforeApply(); - - // Simulate apply — accept all - useAITriageStore.getState().acceptAllRemaining(); - expect(useAITriageStore.getState().reviewItems[0].status).toBe('accepted'); - - // Undo - useAITriageStore.getState().undoLastBatch(); - const restored = useAITriageStore.getState().reviewItems; - expect(restored[0].status).toBe('pending'); - expect(restored[1].status).toBe('pending'); - expect(useAITriageStore.getState().lastBatchSnapshot).toBeNull(); - }); - - it('undoLastBatch does nothing when no snapshot', () => { - useAITriageStore.getState().addReviewItems([ - makeReviewItem({ issueNumber: 1, status: 'accepted' }), - ]); - useAITriageStore.getState().undoLastBatch(); - // Items unchanged - expect(useAITriageStore.getState().reviewItems[0].status).toBe('accepted'); - }); - }); - - describe('lastError', () => { - it('setLastError stores error message', () => { - useAITriageStore.getState().setLastError('API timeout'); - expect(useAITriageStore.getState().lastError).toBe('API timeout'); - }); - - it('clearLastError resets error to null', () => { - useAITriageStore.getState().setLastError('Some error'); - useAITriageStore.getState().clearLastError(); - expect(useAITriageStore.getState().lastError).toBeNull(); - }); - - it('startTriage clears lastError', () => { - useAITriageStore.getState().setLastError('Previous error'); - useAITriageStore.getState().startTriage(); - expect(useAITriageStore.getState().lastError).toBeNull(); - }); - }); -}); diff --git a/apps/frontend/src/renderer/stores/github/ai-triage-store.ts b/apps/frontend/src/renderer/stores/github/ai-triage-store.ts deleted file mode 100644 index d20394ea..00000000 --- a/apps/frontend/src/renderer/stores/github/ai-triage-store.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * @deprecated Legacy AI triage store — replaced by investigation-store.ts. - * Kept for backwards compatibility. Will be removed in a future cleanup pass. - * - * AI Triage Zustand store (Phase 3). - * - * Manages triage operation state, review queue, enrichment progress, - * and split suggestion state. - */ - -import { create } from 'zustand'; -import type { - TriageReviewItem, - EnrichmentProgress, - SplitSuggestion, - SplitProgress, - ProgressiveTrustConfig, - AIEnrichmentResult, -} from '@shared/types/ai-triage'; - -// Re-use the enrichment progress type for triage progress display -type TriageProgress = EnrichmentProgress; - -interface AITriageState { - // Triage operation - isTriaging: boolean; - triageProgress: TriageProgress | null; - - // Review queue - reviewItems: TriageReviewItem[]; - - // Enrichment operation - enrichmentProgress: EnrichmentProgress | null; - enrichmentResult: AIEnrichmentResult | null; - - // Split operation - splitSuggestion: SplitSuggestion | null; - splitProgress: SplitProgress | null; - - // Error state - lastError: string | null; - - // Undo state - lastBatchSnapshot: TriageReviewItem[] | null; - - // Actions - startTriage: () => void; - endTriage: () => void; - setTriageProgress: (progress: TriageProgress) => void; - addReviewItems: (items: TriageReviewItem[]) => void; - acceptReviewItem: (issueNumber: number) => void; - rejectReviewItem: (issueNumber: number) => void; - acceptAllRemaining: () => void; - dismissReview: () => void; - getUnreviewedCount: () => number; - setEnrichmentProgress: (progress: EnrichmentProgress) => void; - clearEnrichmentProgress: () => void; - setEnrichmentResult: (result: AIEnrichmentResult) => void; - clearEnrichmentResult: () => void; - setSplitSuggestion: (suggestion: SplitSuggestion) => void; - clearSplitSuggestion: () => void; - setSplitProgress: (progress: SplitProgress) => void; - clearSplitProgress: () => void; - autoApplyByTrust: (config: ProgressiveTrustConfig) => void; - snapshotBeforeApply: () => void; - undoLastBatch: () => void; - setLastError: (error: string) => void; - clearLastError: () => void; -} - -export const useAITriageStore = create((set, get) => ({ - // Initial state - isTriaging: false, - triageProgress: null, - reviewItems: [], - enrichmentProgress: null, - enrichmentResult: null, - splitSuggestion: null, - splitProgress: null, - lastError: null, - lastBatchSnapshot: null, - - // Triage operation - startTriage: () => set({ isTriaging: true, lastError: null }), - endTriage: () => set({ isTriaging: false, triageProgress: null }), - setTriageProgress: (progress) => set({ triageProgress: progress }), - - // Review queue - addReviewItems: (items) => - set((state) => ({ reviewItems: [...state.reviewItems, ...items] })), - - acceptReviewItem: (issueNumber) => - set((state) => ({ - reviewItems: state.reviewItems.map((item) => - item.issueNumber === issueNumber ? { ...item, status: 'accepted' as const } : item, - ), - })), - - rejectReviewItem: (issueNumber) => - set((state) => ({ - reviewItems: state.reviewItems.map((item) => - item.issueNumber === issueNumber ? { ...item, status: 'rejected' as const } : item, - ), - })), - - acceptAllRemaining: () => - set((state) => ({ - reviewItems: state.reviewItems.map((item) => - item.status === 'pending' ? { ...item, status: 'accepted' as const } : item, - ), - })), - - dismissReview: () => set({ reviewItems: [] }), - - getUnreviewedCount: () => - get().reviewItems.filter((item) => item.status === 'pending').length, - - // Enrichment - setEnrichmentProgress: (progress) => set({ enrichmentProgress: progress }), - clearEnrichmentProgress: () => set({ enrichmentProgress: null }), - setEnrichmentResult: (result) => set({ enrichmentResult: result }), - clearEnrichmentResult: () => set({ enrichmentResult: null }), - - // Split - setSplitSuggestion: (suggestion) => set({ splitSuggestion: suggestion }), - clearSplitSuggestion: () => set({ splitSuggestion: null }), - setSplitProgress: (progress) => set({ splitProgress: progress }), - clearSplitProgress: () => set({ splitProgress: null }), - - snapshotBeforeApply: () => - set((state) => ({ - lastBatchSnapshot: state.reviewItems.map((item) => ({ ...item })), - })), - - undoLastBatch: () => - set((state) => { - if (!state.lastBatchSnapshot) return state; - return { reviewItems: state.lastBatchSnapshot, lastBatchSnapshot: null }; - }), - - setLastError: (error) => set({ lastError: error }), - clearLastError: () => set({ lastError: null }), - - autoApplyByTrust: (config) => - set((state) => ({ - reviewItems: state.reviewItems.map((item) => { - if (item.status !== 'pending') return item; - const conf = item.result.confidence; - // Check each enabled trust category - if (config.autoApply.labels.enabled && conf >= config.autoApply.labels.threshold && item.result.labelsToAdd.length > 0) { - return { ...item, status: 'auto-applied' as const }; - } - if (config.autoApply.duplicate.enabled && conf >= config.autoApply.duplicate.threshold && item.result.isDuplicate) { - return { ...item, status: 'auto-applied' as const }; - } - return item; - }), - })), -})); diff --git a/apps/frontend/src/shared/__tests__/ai-triage-constants.test.ts b/apps/frontend/src/shared/__tests__/ai-triage-constants.test.ts index b521b2ab..3a350a5d 100644 --- a/apps/frontend/src/shared/__tests__/ai-triage-constants.test.ts +++ b/apps/frontend/src/shared/__tests__/ai-triage-constants.test.ts @@ -12,14 +12,12 @@ import { THRESHOLD_MIN, THRESHOLD_MAX, THRESHOLD_STEP, - TRUST_LEVEL_LABELS, ENRICHMENT_COMMENT_FOOTER, getConfidenceLevel, isValidThreshold, clampThreshold, estimateBatchCost, } from '../constants/ai-triage'; -import type { TrustLevel } from '../constants/ai-triage'; describe('confidence constants', () => { it('CONFIDENCE_HIGH is 0.8', () => { @@ -63,16 +61,6 @@ describe('threshold constants', () => { }); }); -describe('TRUST_LEVEL_LABELS', () => { - it('has entry for every trust level', () => { - const allLevels: TrustLevel[] = ['crawl', 'walk', 'run']; - for (const level of allLevels) { - expect(TRUST_LEVEL_LABELS).toHaveProperty(level); - expect(typeof TRUST_LEVEL_LABELS[level]).toBe('string'); - } - }); -}); - describe('ENRICHMENT_COMMENT_FOOTER', () => { it('contains Auto-Claude marker', () => { expect(ENRICHMENT_COMMENT_FOOTER).toContain('Auto-Claude'); diff --git a/apps/frontend/src/shared/__tests__/ai-triage-types.test.ts b/apps/frontend/src/shared/__tests__/ai-triage-types.test.ts index b6c947a5..11e6e268 100644 --- a/apps/frontend/src/shared/__tests__/ai-triage-types.test.ts +++ b/apps/frontend/src/shared/__tests__/ai-triage-types.test.ts @@ -3,45 +3,8 @@ */ import { describe, it, expect } from 'vitest'; import { - createDefaultProgressiveTrust, mapTriageCategory, } from '../types/ai-triage'; -import type { ProgressiveTrustConfig } from '../types/ai-triage'; - -describe('createDefaultProgressiveTrust', () => { - it('returns all categories disabled', () => { - const config = createDefaultProgressiveTrust(); - expect(config.autoApply.type.enabled).toBe(false); - expect(config.autoApply.priority.enabled).toBe(false); - expect(config.autoApply.labels.enabled).toBe(false); - expect(config.autoApply.duplicate.enabled).toBe(false); - }); - - it('returns 0.9 threshold for all categories', () => { - const config = createDefaultProgressiveTrust(); - expect(config.autoApply.type.threshold).toBe(0.9); - expect(config.autoApply.priority.threshold).toBe(0.9); - expect(config.autoApply.labels.threshold).toBe(0.9); - expect(config.autoApply.duplicate.threshold).toBe(0.9); - }); - - it('returns default batch size of 50', () => { - const config = createDefaultProgressiveTrust(); - expect(config.batchSize).toBe(50); - }); - - it('returns default confirmAbove of 10', () => { - const config = createDefaultProgressiveTrust(); - expect(config.confirmAbove).toBe(10); - }); - - it('returns a fresh object each time (no shared references)', () => { - const a = createDefaultProgressiveTrust(); - const b = createDefaultProgressiveTrust(); - expect(a).not.toBe(b); - expect(a.autoApply).not.toBe(b.autoApply); - }); -}); describe('mapTriageCategory', () => { it('maps bug to bug', () => { diff --git a/apps/frontend/src/shared/constants/ai-triage.ts b/apps/frontend/src/shared/constants/ai-triage.ts index 9ad10ef8..e6af6d77 100644 --- a/apps/frontend/src/shared/constants/ai-triage.ts +++ b/apps/frontend/src/shared/constants/ai-triage.ts @@ -26,18 +26,6 @@ export const THRESHOLD_MIN = 0.5; export const THRESHOLD_MAX = 1.0; export const THRESHOLD_STEP = 0.05; -// ============================================ -// Trust Levels -// ============================================ - -export type TrustLevel = 'crawl' | 'walk' | 'run'; - -export const TRUST_LEVEL_LABELS: Record = { - crawl: 'Suggestions Only', - walk: 'Auto-Apply Above Threshold', - run: 'Fully Automated', -}; - // ============================================ // Enrichment Comment // ============================================ diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index 938578f2..24703056 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -475,8 +475,6 @@ export const IPC_CHANNELS = { GITHUB_TRIAGE_CANCEL: 'github:triage:cancel', GITHUB_TRIAGE_SAVE_PENDING_REVIEW: 'github:triage:savePendingReview', GITHUB_TRIAGE_LOAD_PENDING_REVIEW: 'github:triage:loadPendingReview', - GITHUB_TRIAGE_SAVE_TRUST: 'github:triage:saveTrust', - GITHUB_TRIAGE_GET_TRUST: 'github:triage:getTrust', // GitHub Enrichment operations GITHUB_ENRICHMENT_GET_ALL: 'github:enrichment:getAll', diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index 4e812cad..347d7741 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -643,23 +643,6 @@ "cancel": "Cancel", "duplicateWarning": "An AI enrichment comment already exists on this issue. Posting will create a duplicate." }, - "progressiveTrust": { - "title": "Progressive Trust Settings", - "type": "Type", - "priority": "Priority", - "labels": "Labels", - "duplicate": "Duplicate", - "threshold": "{{category}} threshold", - "batchSize": "Batch size", - "confirmAbove": "Confirm above", - "save": "Save", - "cancel": "Cancel", - "trustLevel": "Trust Level", - "crawl": "Suggestions Only", - "walk": "Auto-Apply Above Threshold", - "run": "Fully Automated", - "runWarning": "All triage results will be applied automatically without review." - }, "labelSync": { "title": "Label Synchronization", "description": "Sync workflow states as ac: labels on GitHub", diff --git a/apps/frontend/src/shared/i18n/locales/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index e24b1aa5..d07af4de 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -643,23 +643,6 @@ "cancel": "Annuler", "duplicateWarning": "Un commentaire d'enrichissement IA existe déjà sur cette issue. La publication créera un doublon." }, - "progressiveTrust": { - "title": "Paramètres de confiance progressive", - "type": "Type", - "priority": "Priorité", - "labels": "Labels", - "duplicate": "Doublon", - "threshold": "Seuil de {{category}}", - "batchSize": "Taille du lot", - "confirmAbove": "Confirmer au-dessus de", - "save": "Enregistrer", - "cancel": "Annuler", - "trustLevel": "Niveau de confiance", - "crawl": "Suggestions uniquement", - "walk": "Auto-application au-dessus du seuil", - "run": "Entièrement automatisé", - "runWarning": "Tous les résultats de triage seront appliqués automatiquement sans révision." - }, "labelSync": { "title": "Synchronisation des labels", "description": "Synchroniser les états du workflow en tant que labels ac: sur GitHub", diff --git a/apps/frontend/src/shared/types/ai-triage.ts b/apps/frontend/src/shared/types/ai-triage.ts index a0f8862e..bd5950e3 100644 --- a/apps/frontend/src/shared/types/ai-triage.ts +++ b/apps/frontend/src/shared/types/ai-triage.ts @@ -8,21 +8,6 @@ import type { TriageCategory as EnrichmentTriageCategory } from './enrichment'; // (cannot import directly due to main/renderer boundary — consumers // in renderer use IPC; only main-process code imports triage-handlers) -// ============================================ -// Progressive Trust -// ============================================ - -export interface ProgressiveTrustConfig { - autoApply: { - type: { enabled: boolean; threshold: number }; - priority: { enabled: boolean; threshold: number }; - labels: { enabled: boolean; threshold: number }; - duplicate: { enabled: boolean; threshold: number }; - }; - batchSize: number; - confirmAbove: number; -} - // ============================================ // AI Enrichment // ============================================ @@ -116,23 +101,6 @@ export interface ApplyResultsProgress { currentIssueNumber?: number; } -// ============================================ -// Factory Functions -// ============================================ - -export function createDefaultProgressiveTrust(): ProgressiveTrustConfig { - return { - autoApply: { - type: { enabled: false, threshold: 0.9 }, - priority: { enabled: false, threshold: 0.9 }, - labels: { enabled: false, threshold: 0.9 }, - duplicate: { enabled: false, threshold: 0.9 }, - }, - batchSize: 50, - confirmAbove: 10, - }; -} - // ============================================ // Category Mapping // ============================================