feat(ai-triage): WP-1 types, constants, and validation utils

Progressive trust config, enrichment result, split suggestion types.
Category mapping, confidence helpers, threshold validation, cost estimation.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-12 18:29:36 +01:00
co-authored by Claude Opus 4.6
parent f99c771b39
commit aa52c5ed5f
4 changed files with 488 additions and 0 deletions
@@ -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');
});
});
@@ -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');
});
});
@@ -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<TrustLevel, string> = {
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)}`;
}
+160
View File
@@ -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<string, EnrichmentTriageCategory> = {
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';
}