refactor(issues): remove dead trust level system — frontend progressive trust UI, backend L0-L4 escalation, and deprecated ai-triage store

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 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-14 13:01:48 +01:00
co-authored by Claude Opus 4.6
parent 1b34dec107
commit 72522dd815
25 changed files with 4 additions and 2201 deletions
@@ -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),
-543
View File
@@ -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),
}
@@ -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);
});
});
@@ -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<boolean> => {
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<string, unknown> = {};
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<ProgressiveTrustConfig> => {
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
// ============================================
@@ -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<boolean>;
loadPendingReview: (projectId: string) => Promise<TriageReviewItem[]>;
saveProgressiveTrust: (projectId: string, config: ProgressiveTrustConfig) => Promise<boolean>;
getProgressiveTrust: (projectId: string) => Promise<ProgressiveTrustConfig>;
// Label Sync (Phase 4)
enableLabelSync: (projectId: string) => Promise<LabelSyncResult>;
disableLabelSync: (projectId: string, cleanup: boolean) => Promise<{ success: boolean }>;
@@ -1089,12 +1085,6 @@ export const createGitHubAPI = (): GitHubAPI => ({
loadPendingReview: (projectId: string): Promise<TriageReviewItem[]> =>
invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_LOAD_PENDING_REVIEW, projectId),
saveProgressiveTrust: (projectId: string, config: ProgressiveTrustConfig): Promise<boolean> =>
invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_SAVE_TRUST, projectId, config),
getProgressiveTrust: (projectId: string): Promise<ProgressiveTrustConfig> =>
invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_GET_TRUST, projectId),
// Label Sync (Phase 4)
enableLabelSync: (projectId: string): Promise<LabelSyncResult> =>
invokeIpc(IPC_CHANNELS.GITHUB_LABEL_SYNC_ENABLE, projectId),
@@ -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> = {}): 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();
});
});
@@ -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}/);
});
});
@@ -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(<TriageProgressOverlay progress={progress} onCancel={vi.fn()} />);
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(<TriageProgressOverlay progress={progress} onCancel={onCancel} />);
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(
@@ -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');
@@ -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<TrustLevel, Record<Category, boolean>> = {
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<ProgressiveTrustConfig>(() =>
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 (
<section className="space-y-4" aria-label={t('common:progressiveTrust.title')}>
{/* Trust level radio group */}
<fieldset className="space-y-2">
<legend className="text-sm font-medium text-foreground">{t('common:progressiveTrust.trustLevel')}</legend>
<div className="flex gap-4">
{TRUST_LEVELS.map((level) => (
<label key={level} className="flex items-center gap-1.5 cursor-pointer">
<input
type="radio"
name="trust-level"
value={level}
checked={currentLevel === level}
onChange={() => setTrustLevel(level)}
/>
<span className="text-sm">{t(`common:progressiveTrust.${level}`)}</span>
</label>
))}
</div>
{currentLevel === 'run' && (
<p role="alert" className="text-xs text-destructive">
{t('common:progressiveTrust.runWarning')}
</p>
)}
</fieldset>
{/* Category rows */}
{CATEGORIES.map((category) => (
<div key={category} className="flex items-center gap-3">
<label className="flex items-center gap-2 min-w-[120px]">
<input
type="checkbox"
checked={config.autoApply[category].enabled}
onChange={() => toggleCategory(category)}
className="rounded"
/>
<span className="text-sm capitalize">{t(`common:progressiveTrust.${category}`)}</span>
</label>
<input
type="range"
min={THRESHOLD_MIN}
max={THRESHOLD_MAX}
step={THRESHOLD_STEP}
value={config.autoApply[category].threshold}
onChange={(e) => setThreshold(category, Number.parseFloat(e.target.value))}
disabled={!config.autoApply[category].enabled}
className="flex-1"
aria-label={t('common:progressiveTrust.threshold', { category })}
/>
<span className="text-xs text-foreground/50 w-10 text-right">
{Math.round(config.autoApply[category].threshold * 100)}%
</span>
</div>
))}
{/* Batch size */}
<div className="flex items-center gap-3">
<label htmlFor="trust-batch-size" className="text-sm min-w-[120px]">{t('common:progressiveTrust.batchSize')}</label>
<input
id="trust-batch-size"
type="number"
value={config.batchSize}
onChange={(e) => 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}
/>
</div>
{/* Confirm above */}
<div className="flex items-center gap-3">
<label htmlFor="trust-confirm-above" className="text-sm min-w-[120px]">{t('common:progressiveTrust.confirmAbove')}</label>
<input
id="trust-confirm-above"
type="number"
value={config.confirmAbove}
onChange={(e) => 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}
/>
</div>
{/* Actions */}
<div className="flex gap-2 justify-end pt-2">
<button
type="button"
aria-label={t('common:progressiveTrust.cancel')}
className="text-xs px-3 py-1.5 rounded bg-foreground/10 hover:bg-foreground/20 text-foreground/70 transition-colors"
onClick={onCancel}
>
{t('common:progressiveTrust.cancel')}
</button>
<button
type="button"
aria-label={t('common:progressiveTrust.save')}
className="text-xs px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-500 text-white transition-colors"
onClick={() => onSave(config)}
>
{t('common:progressiveTrust.save')}
</button>
</div>
</section>
);
}
@@ -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<ProgressiveTrustConfig>(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 (
<ProgressiveTrustSettings
config={config}
onSave={handleSave}
onCancel={handleCancel}
/>
);
}
@@ -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(<ProgressiveTrustSettings {...defaultProps} />);
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(<ProgressiveTrustSettings {...defaultProps} />);
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes.length).toBeGreaterThanOrEqual(4);
});
it('calls onSave when save button clicked', () => {
render(<ProgressiveTrustSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /save/i }));
expect(defaultProps.onSave).toHaveBeenCalled();
});
it('calls onCancel when cancel button clicked', () => {
render(<ProgressiveTrustSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
expect(defaultProps.onCancel).toHaveBeenCalled();
});
it('shows batch size input', () => {
render(<ProgressiveTrustSettings {...defaultProps} />);
const batchInput = screen.getByDisplayValue('50');
expect(batchInput).toBeDefined();
});
it('shows confirm-above input', () => {
render(<ProgressiveTrustSettings {...defaultProps} />);
const confirmInput = screen.getByDisplayValue('10');
expect(confirmInput).toBeDefined();
});
it('toggles category enabled state', () => {
render(<ProgressiveTrustSettings {...defaultProps} />);
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(<ProgressiveTrustSettings {...defaultProps} />);
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(<ProgressiveTrustSettings {...defaultProps} />);
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(<ProgressiveTrustSettings {...defaultProps} />);
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(<ProgressiveTrustSettings {...defaultProps} config={runConfig} />);
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(<ProgressiveTrustSettings {...defaultProps} />);
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);
});
});
@@ -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<string, unknown>).electronAPI = { github: mockGithub };
mockGithub.getProgressiveTrust.mockResolvedValue(createDefaultProgressiveTrust());
mockGithub.saveProgressiveTrust.mockResolvedValue(true);
});
describe('ProgressiveTrustSettingsConnected', () => {
it('renders ProgressiveTrustSettings after config loaded', async () => {
render(<ProgressiveTrustSettingsConnected />);
await waitFor(() => {
expect(screen.getByText('common:progressiveTrust.crawl')).toBeDefined();
});
});
it('loads config from IPC on mount', async () => {
render(<ProgressiveTrustSettingsConnected />);
await waitFor(() => {
expect(mockGithub.getProgressiveTrust).toHaveBeenCalledWith('proj-1');
});
});
it('saves config via IPC on save', async () => {
render(<ProgressiveTrustSettingsConnected />);
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));
});
});
});
@@ -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';
@@ -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}
/>
<LabelSyncSettingsConnected />
<ProgressiveTrustSettingsConnected />
<InvestigationSettings projectId={project.id} />
</InitializationGuard>
</SettingsSection>
@@ -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 }),
@@ -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<TriageReviewItem> & { 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>,
): 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();
});
});
});
@@ -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<AITriageState>((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;
}),
})),
}));
@@ -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');
@@ -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', () => {
@@ -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<TrustLevel, string> = {
crawl: 'Suggestions Only',
walk: 'Auto-Apply Above Threshold',
run: 'Fully Automated',
};
// ============================================
// Enrichment Comment
// ============================================
@@ -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',
@@ -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",
@@ -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",
@@ -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
// ============================================