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:
co-authored by
Claude Opus 4.6
parent
f99c771b39
commit
aa52c5ed5f
@@ -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)}`;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user