diff --git a/apps/frontend/src/shared/__tests__/ai-triage-constants.test.ts b/apps/frontend/src/shared/__tests__/ai-triage-constants.test.ts new file mode 100644 index 00000000..b521b2ab --- /dev/null +++ b/apps/frontend/src/shared/__tests__/ai-triage-constants.test.ts @@ -0,0 +1,161 @@ +/** + * Tests for AI triage constants and utility functions. + */ +import { describe, it, expect } from 'vitest'; +import { + CONFIDENCE_HIGH, + CONFIDENCE_MEDIUM, + DEFAULT_BATCH_SIZE, + DEFAULT_CONFIRM_ABOVE, + MAX_SPLIT_SUB_ISSUES, + APPLY_INTER_ITEM_DELAY, + 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', () => { + expect(CONFIDENCE_HIGH).toBe(0.8); + }); + + it('CONFIDENCE_MEDIUM is 0.5', () => { + expect(CONFIDENCE_MEDIUM).toBe(0.5); + }); +}); + +describe('batch constants', () => { + it('DEFAULT_BATCH_SIZE is 50', () => { + expect(DEFAULT_BATCH_SIZE).toBe(50); + }); + + it('DEFAULT_CONFIRM_ABOVE is 10', () => { + expect(DEFAULT_CONFIRM_ABOVE).toBe(10); + }); + + it('MAX_SPLIT_SUB_ISSUES is 5', () => { + expect(MAX_SPLIT_SUB_ISSUES).toBe(5); + }); + + it('APPLY_INTER_ITEM_DELAY is 100', () => { + expect(APPLY_INTER_ITEM_DELAY).toBe(100); + }); +}); + +describe('threshold constants', () => { + it('THRESHOLD_MIN is 0.5', () => { + expect(THRESHOLD_MIN).toBe(0.5); + }); + + it('THRESHOLD_MAX is 1.0', () => { + expect(THRESHOLD_MAX).toBe(1.0); + }); + + it('THRESHOLD_STEP is 0.05', () => { + expect(THRESHOLD_STEP).toBe(0.05); + }); +}); + +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'); + }); + + it('starts with horizontal rule', () => { + expect(ENRICHMENT_COMMENT_FOOTER).toMatch(/^---/); + }); +}); + +describe('getConfidenceLevel', () => { + it('returns high for >= 0.8', () => { + expect(getConfidenceLevel(0.8)).toBe('high'); + expect(getConfidenceLevel(0.95)).toBe('high'); + expect(getConfidenceLevel(1.0)).toBe('high'); + }); + + it('returns medium for >= 0.5 and < 0.8', () => { + expect(getConfidenceLevel(0.5)).toBe('medium'); + expect(getConfidenceLevel(0.79)).toBe('medium'); + }); + + it('returns low for < 0.5', () => { + expect(getConfidenceLevel(0.49)).toBe('low'); + expect(getConfidenceLevel(0.1)).toBe('low'); + expect(getConfidenceLevel(0)).toBe('low'); + }); +}); + +describe('isValidThreshold', () => { + it('returns true for values in [0.5, 1.0]', () => { + expect(isValidThreshold(0.5)).toBe(true); + expect(isValidThreshold(0.75)).toBe(true); + expect(isValidThreshold(1.0)).toBe(true); + }); + + it('returns false for values below 0.5', () => { + expect(isValidThreshold(0.49)).toBe(false); + expect(isValidThreshold(0)).toBe(false); + expect(isValidThreshold(-1)).toBe(false); + }); + + it('returns false for values above 1.0', () => { + expect(isValidThreshold(1.01)).toBe(false); + expect(isValidThreshold(2)).toBe(false); + }); + + it('returns false for non-numbers', () => { + expect(isValidThreshold(Number.NaN)).toBe(false); + }); +}); + +describe('clampThreshold', () => { + it('returns value when in range', () => { + expect(clampThreshold(0.75)).toBe(0.75); + }); + + it('clamps to min when below', () => { + expect(clampThreshold(0.1)).toBe(0.5); + expect(clampThreshold(-1)).toBe(0.5); + }); + + it('clamps to max when above', () => { + expect(clampThreshold(1.5)).toBe(1.0); + expect(clampThreshold(99)).toBe(1.0); + }); +}); + +describe('estimateBatchCost', () => { + it('estimates haiku cost correctly', () => { + expect(estimateBatchCost(100, 'haiku')).toBe('~$0.08'); + }); + + it('estimates non-haiku cost correctly', () => { + expect(estimateBatchCost(100, 'sonnet')).toBe('~$0.35'); + }); + + it('handles small batches', () => { + expect(estimateBatchCost(1, 'haiku')).toBe('~$0.00'); + }); + + it('handles large batches', () => { + const cost = estimateBatchCost(1000, 'haiku'); + expect(cost).toBe('~$0.80'); + }); +}); diff --git a/apps/frontend/src/shared/__tests__/ai-triage-types.test.ts b/apps/frontend/src/shared/__tests__/ai-triage-types.test.ts new file mode 100644 index 00000000..b6c947a5 --- /dev/null +++ b/apps/frontend/src/shared/__tests__/ai-triage-types.test.ts @@ -0,0 +1,94 @@ +/** + * Tests for AI triage type factories and utilities. + */ +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', () => { + expect(mapTriageCategory('bug')).toBe('bug'); + }); + + it('maps feature to feature', () => { + expect(mapTriageCategory('feature')).toBe('feature'); + }); + + it('maps documentation to documentation', () => { + expect(mapTriageCategory('documentation')).toBe('documentation'); + }); + + it('maps question to question', () => { + expect(mapTriageCategory('question')).toBe('question'); + }); + + it('maps duplicate to bug (flagged separately via isDuplicate)', () => { + expect(mapTriageCategory('duplicate')).toBe('bug'); + }); + + it('maps spam to chore', () => { + expect(mapTriageCategory('spam')).toBe('chore'); + }); + + it('maps feature_creep to enhancement', () => { + expect(mapTriageCategory('feature_creep')).toBe('enhancement'); + }); + + it('maps unknown categories to chore', () => { + expect(mapTriageCategory('unknown_xyz')).toBe('chore'); + }); + + it('maps enhancement to enhancement', () => { + expect(mapTriageCategory('enhancement')).toBe('enhancement'); + }); + + it('maps chore to chore', () => { + expect(mapTriageCategory('chore')).toBe('chore'); + }); + + it('maps security to security', () => { + expect(mapTriageCategory('security')).toBe('security'); + }); + + it('maps performance to performance', () => { + expect(mapTriageCategory('performance')).toBe('performance'); + }); +}); diff --git a/apps/frontend/src/shared/constants/ai-triage.ts b/apps/frontend/src/shared/constants/ai-triage.ts new file mode 100644 index 00000000..7ae605d6 --- /dev/null +++ b/apps/frontend/src/shared/constants/ai-triage.ts @@ -0,0 +1,73 @@ +/** + * AI triage constants and utility functions for Phase 3 (AI Power). + */ + +// ============================================ +// Confidence Thresholds +// ============================================ + +export const CONFIDENCE_HIGH = 0.8; +export const CONFIDENCE_MEDIUM = 0.5; + +// ============================================ +// Batch Limits +// ============================================ + +export const DEFAULT_BATCH_SIZE = 50; +export const DEFAULT_CONFIRM_ABOVE = 10; +export const MAX_SPLIT_SUB_ISSUES = 5; +export const APPLY_INTER_ITEM_DELAY = 100; + +// ============================================ +// Threshold Bounds +// ============================================ + +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 +// ============================================ + +export const ENRICHMENT_COMMENT_FOOTER = '---\n*Generated by Auto-Claude AI Triage*'; + +// ============================================ +// Utility Functions +// ============================================ + +export function getConfidenceLevel(confidence: number): 'high' | 'medium' | 'low' { + if (confidence >= CONFIDENCE_HIGH) return 'high'; + if (confidence >= CONFIDENCE_MEDIUM) return 'medium'; + return 'low'; +} + +export function isValidThreshold(value: number): boolean { + if (Number.isNaN(value)) return false; + return value >= THRESHOLD_MIN && value <= THRESHOLD_MAX; +} + +export function clampThreshold(value: number): number { + return Math.max(THRESHOLD_MIN, Math.min(THRESHOLD_MAX, value)); +} + +/** + * Estimates the cost of a batch triage operation. + * Uses rough per-issue token cost estimates. + */ +export function estimateBatchCost(issueCount: number, model: string): string { + const costPerIssue = model.includes('haiku') ? 0.0008 : 0.0035; + return `~$${(issueCount * costPerIssue).toFixed(2)}`; +} diff --git a/apps/frontend/src/shared/types/ai-triage.ts b/apps/frontend/src/shared/types/ai-triage.ts new file mode 100644 index 00000000..a0f8862e --- /dev/null +++ b/apps/frontend/src/shared/types/ai-triage.ts @@ -0,0 +1,160 @@ +/** + * AI triage types for Phase 3 (AI Power). + * Progressive trust, enrichment, splitting, and review queue types. + */ +import type { TriageCategory as EnrichmentTriageCategory } from './enrichment'; + +// Re-export TriageResult from triage-handlers for convenience +// (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 +// ============================================ + +export interface AIEnrichmentResult { + issueNumber: number; + problem: string; + goal: string; + scopeIn: string[]; + scopeOut: string[]; + acceptanceCriteria: string[]; + technicalContext: string; + risksEdgeCases: string[]; + confidence: number; +} + +// ============================================ +// Issue Splitting +// ============================================ + +export interface SplitSuggestion { + issueNumber: number; + subIssues: Array<{ + title: string; + body: string; + labels: string[]; + }>; + rationale: string; + confidence: number; +} + +export interface CreateIssueParams { + title: string; + body: string; + labels?: string[]; + assignees?: string[]; +} + +export interface CreateIssueResult { + number: number; + url: string; +} + +// ============================================ +// Triage Review Queue +// ============================================ + +export type TriageReviewStatus = 'pending' | 'accepted' | 'rejected' | 'auto-applied'; + +export interface TriageReviewItem { + issueNumber: number; + issueTitle: string; + result: { + category: string; + confidence: number; + labelsToAdd: string[]; + labelsToRemove: string[]; + isDuplicate: boolean; + duplicateOf?: number; + isSpam: boolean; + isFeatureCreep: boolean; + suggestedBreakdown: string[]; + priority: 'high' | 'medium' | 'low'; + comment?: string; + triagedAt: string; + }; + status: TriageReviewStatus; +} + +// ============================================ +// Progress Types +// ============================================ + +export interface EnrichmentProgress { + phase: 'analyzing' | 'generating' | 'complete'; + progress: number; + message: string; +} + +export interface SplitProgress { + phase: 'analyzing' | 'suggesting' | 'creating' | 'closing' | 'complete'; + progress: number; + message: string; + createdCount?: number; + totalCount?: number; +} + +export interface ApplyResultsProgress { + totalItems: number; + processedItems: number; + 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 +// ============================================ + +/** + * Maps Python triage runner categories to enrichment TriageCategory. + * The runner may return 'duplicate', 'spam', or 'feature_creep' which + * don't exist in the enrichment type system — this function bridges them. + */ +export function mapTriageCategory(category: string): EnrichmentTriageCategory { + const mapping: Record = { + bug: 'bug', + feature: 'feature', + documentation: 'documentation', + question: 'question', + enhancement: 'enhancement', + chore: 'chore', + security: 'security', + performance: 'performance', + duplicate: 'bug', + spam: 'chore', + feature_creep: 'enhancement', + }; + return mapping[category] ?? 'chore'; +}