diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f9a7d3..76cf50bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `BASH_TOOL_DESCRIPTION` constant removed; callers use `buildBashToolDescription(root)` directly so the description always reflects the actual workspace root rather than a hardcoded `/workspace/pr`. - Updated all dependencies to their latest stable versions (OpenTelemetry, `openai`, `@openai/agents`, `@types/node`, ESLint + `typescript-eslint`, `jest`/`ts-jest`, `prettier`, `zod`; website: Astro 6→7 + Starlight). Dropped the redundant `@types/diff` stub (diff@9 ships its own types) and pinned transitive `uuid` via `overrides`. Build, lint, type-check, unit tests, and evals all pass. +- Internal structure & de-duplication refactor (no runtime behavior change): centralized duplicated utilities/types, split the `shared/types` barrel into domain-focused files, introduced `src/kernel/` for pure helpers, and extracted shared constants. As part of this, unused types were trimmed from the package's exported type surface — the dead `Issue`/`Pattern`/`Session` model classes and the unreferenced `FilePath`/`SessionId`/`FileContent`/`CodeLocation`/`CodeSnippet`/`Metrics` aliases (no internal or known external use). ### Security - Reduced dependency vulnerabilities: website advisories fully resolved (8→0); remaining root advisories trace to `@eggai/configurable-agent@0.2.1` (no upstream fix; slated for retirement). Note: the majority of GitHub's Dependabot alerts are frozen third-party eval fixtures under `evals/datasets/`, not shipped dependencies. diff --git a/src/ai/providers/base.ts b/src/ai/providers/base.ts index 0fff17a5..23b58937 100644 --- a/src/ai/providers/base.ts +++ b/src/ai/providers/base.ts @@ -6,6 +6,7 @@ import { logger } from '@/shared/utils/logger'; import type { ProviderCapabilities } from './capabilities'; import { detectCapabilities } from './capabilities'; +import { TOKENS_PER_MILLION } from './pricing-constants'; import type { AICompletionOptions, AICompletionOptionsWithSchema, @@ -139,11 +140,12 @@ export abstract class BaseAIProvider implements AIProvider { const { inputPerMillion, outputPerMillion } = this.stageConfig; const { cacheWriteMultiplier, cacheReadMultiplier } = this.pricingMultipliers(use1HourCache); - const inputCost = (fullPriceInput / 1_000_000) * inputPerMillion; - const cacheReadCost = (cachedReadTokens / 1_000_000) * (inputPerMillion * cacheReadMultiplier); + const inputCost = (fullPriceInput / TOKENS_PER_MILLION) * inputPerMillion; + const cacheReadCost = + (cachedReadTokens / TOKENS_PER_MILLION) * (inputPerMillion * cacheReadMultiplier); const cacheWriteCost = - (cacheCreationTokens / 1_000_000) * (inputPerMillion * cacheWriteMultiplier); - const outputCost = (output / 1_000_000) * outputPerMillion; + (cacheCreationTokens / TOKENS_PER_MILLION) * (inputPerMillion * cacheWriteMultiplier); + const outputCost = (output / TOKENS_PER_MILLION) * outputPerMillion; this.tokenStats.estimatedCost += inputCost + cacheReadCost + cacheWriteCost + outputCost; this.logUsageIfDue(); diff --git a/src/ai/providers/bedrock.ts b/src/ai/providers/bedrock.ts index 18efa066..6c1de02a 100644 --- a/src/ai/providers/bedrock.ts +++ b/src/ai/providers/bedrock.ts @@ -14,6 +14,7 @@ import { logger } from '@/shared/utils/logger'; import { BaseAIProvider, type PricingMultipliers } from './base'; import { AIProviderType } from './factory'; +import { TOKENS_PER_MILLION } from './pricing-constants'; import type { AICompletionOptions, AICompletionOptionsWithSchema, @@ -135,7 +136,7 @@ export class BedrockProvider extends BaseAIProvider { if (counters.reads > 0 || counters.writes > 0) { const hitRate = ((counters.hits / stats.invocationCount) * 100).toFixed(1); const cacheReadSavings = - (counters.reads / 1_000_000) * (this.stageConfig.inputPerMillion * 0.9); + (counters.reads / TOKENS_PER_MILLION) * (this.stageConfig.inputPerMillion * 0.9); msg += `, CacheWrite=${counters.writes.toLocaleString()}`; msg += `, CacheRead=${counters.reads.toLocaleString()} (${hitRate}% hit rate)`; msg += `, CacheSavings=$${cacheReadSavings.toFixed(4)}`; diff --git a/src/ai/providers/pricing-constants.ts b/src/ai/providers/pricing-constants.ts new file mode 100644 index 00000000..75d5b8bf --- /dev/null +++ b/src/ai/providers/pricing-constants.ts @@ -0,0 +1 @@ +export const TOKENS_PER_MILLION = 1_000_000; diff --git a/src/cli/commands/all-command.ts b/src/cli/commands/all-command.ts index 90fc65f2..924771ef 100644 --- a/src/cli/commands/all-command.ts +++ b/src/cli/commands/all-command.ts @@ -4,6 +4,7 @@ import type { Span, Tracer } from '@opentelemetry/api'; import { executeAnalyzeStage } from './analyze-command'; import { executeFixStage } from './fix-command'; +import { TOKENS_PER_MILLION } from '../../ai/providers/pricing-constants'; import { ConfigService, DEFAULT_CONFIG_PATH } from '../../config/config'; import { setupTracing, @@ -71,7 +72,7 @@ export async function executeAllStages(options: QualOpsOptions): Promise { logger.info(`Output Tokens: ${totalStats.totalOutputTokens.toLocaleString()}`); if (totalStats.totalCachedTokens > 0) { logger.info(`Cached Tokens: ${totalStats.totalCachedTokens.toLocaleString()}`); - const savings = (totalStats.totalCachedTokens / 1_000_000) * 1.25; + const savings = (totalStats.totalCachedTokens / TOKENS_PER_MILLION) * 1.25; logger.info(`Cache Savings: $${savings.toFixed(4)}`); } logger.info('\nBy Stage:'); diff --git a/src/github/github-integration.ts b/src/github/github-integration.ts index d44d1f71..ceda7c6f 100644 --- a/src/github/github-integration.ts +++ b/src/github/github-integration.ts @@ -3,10 +3,13 @@ import { join } from 'path'; import { GitHubAPIClient } from './github-api-client'; import { GitHubChecksService } from './github-checks'; +import { + QUALOPS_COMMENT_MARKER, + type IntegrationReviewIssue, + type QualOpsResult, +} from '../shared/types/integrations'; import { logger } from '../shared/utils/logger'; -const QUALOPS_COMMENT_MARKER = ''; - interface GitHubConfig { enabled?: boolean; postComments?: boolean; @@ -31,33 +34,6 @@ interface GitHubEnv { GITHUB_SERVER_URL?: string; } -interface QualOpsResult { - summary: { - totalIssues: number; - criticalSeverity: number; - highSeverity: number; - mediumSeverity: number; - lowSeverity: number; - filesAnalyzed: number; - }; - reportPath: string; - issues: Array<{ - file: string; - line: number; - severity: string; - message: string; - category: string; - }>; -} - -interface ReviewIssue { - file: string; - location: string; - severity: string; - description: string; - type: string; -} - interface PullRequestEvent { pull_request: { number: number; @@ -241,7 +217,7 @@ export class GitHubIntegration { try { const reviewSummary = JSON.parse(readFileSync(reviewSummaryPath, 'utf8')); const summary = reviewSummary.summary || {}; - const issues: ReviewIssue[] = reviewSummary.issues || []; + const issues: IntegrationReviewIssue[] = reviewSummary.issues || []; return { summary: { diff --git a/src/gitlab/gitlab-integration.ts b/src/gitlab/gitlab-integration.ts index cf792009..fe65a808 100644 --- a/src/gitlab/gitlab-integration.ts +++ b/src/gitlab/gitlab-integration.ts @@ -3,6 +3,11 @@ import { existsSync, readdirSync, readFileSync } from 'fs'; import { join } from 'path'; +import { + QUALOPS_COMMENT_MARKER, + type IntegrationReviewIssue, + type QualOpsResult, +} from '../shared/types/integrations'; import { logger } from '../shared/utils/logger'; import { hasValidUrlScheme, isValidGitSha } from '../shared/utils/security'; @@ -13,8 +18,6 @@ const SEVERITY_EMOJI = { low: '🟢', } as const; -const QUALOPS_COMMENT_MARKER = ''; - interface GitLabConfig { enabled?: boolean; postComments?: boolean; @@ -43,25 +46,6 @@ interface GitLabEnv { CI_PROJECT_URL?: string; } -interface QualOpsResult { - summary: { - totalIssues: number; - criticalSeverity: number; - highSeverity: number; - mediumSeverity: number; - lowSeverity: number; - filesAnalyzed: number; - }; - reportPath: string; - issues: Array<{ - file: string; - line: number; - severity: string; - message: string; - category: string; - }>; -} - interface GitLabJob { id: number; name: string; @@ -75,14 +59,6 @@ interface GitLabNote { }; } -interface ReviewIssue { - file: string; - location: string; - severity: string; - description: string; - type: string; -} - class GitLabIntegration { private env: GitLabEnv; private apiUrl: string; @@ -602,7 +578,7 @@ class GitLabIntegration { if (reviewReport.issues && Array.isArray(reviewReport.issues)) { // Transform issues to match expected format - const transformedIssues = reviewReport.issues.map((issue: ReviewIssue) => { + const transformedIssues = reviewReport.issues.map((issue: IntegrationReviewIssue) => { // Parse location field - handle both "42" and "line:42" formats const locationStr = issue.location.replace(/^line:?/i, '').trim(); const lineNumber = parseInt(locationStr, 10) || 0; diff --git a/src/stages/analyze/utils/hash-calculator.ts b/src/kernel/hash.ts similarity index 100% rename from src/stages/analyze/utils/hash-calculator.ts rename to src/kernel/hash.ts diff --git a/src/shared/types/ai-config.ts b/src/shared/types/ai-config.ts new file mode 100644 index 00000000..fca08c10 --- /dev/null +++ b/src/shared/types/ai-config.ts @@ -0,0 +1,63 @@ +import type { z } from 'zod'; + +import type { aiProvider, modelConfigSchema } from '@/config/config-schema'; + +import type { ReviewConfig } from './config'; + +export type AIProviderName = z.infer; + +export type ModelConfig = z.infer; + +export type AIStageConfig = { + provider?: AIProviderName; // deprecated fallback; prefer model: { provider, name } + model: ModelConfig; + inputPerMillion: number; + outputPerMillion: number; + temperature?: number; + maxTokens?: number; + baseUrl?: string; + [key: string]: unknown; +}; + +export type ResolvedStageConfig = { + provider: AIProviderName; + model: string; + inputPerMillion: number; + outputPerMillion: number; + temperature?: number; + maxTokens?: number; + baseUrl?: string; + [key: string]: unknown; +}; + +export type Config = { + ai?: { + reviewStage?: AIStageConfig; + fixStage?: AIStageConfig; + judgeStage?: AIStageConfig; + }; + review?: ReviewConfig; + fix?: { + maxConcurrentFixes: number; + }; + report?: { + generateIssueMarkdown?: boolean; + enableRootCauseExtraction?: boolean; + }; + maxFilesPerBatch?: number; + maxConcurrency?: number; + cacheEnabled?: boolean; + cacheTTL?: number; + skipPatterns?: string[]; + includePatterns?: string[]; + outputFormat?: 'json' | 'html' | 'markdown'; + outputPath?: string; + verbose?: boolean; + debug?: boolean; + maxFileSizeKB?: number; + maxTokensPerFile?: number; + maxReactSteps?: number; + throttling?: { + apiCallsPerMinute: number; + }; +}; diff --git a/src/shared/types/analysis-metadata.ts b/src/shared/types/analysis-metadata.ts new file mode 100644 index 00000000..e4a89394 --- /dev/null +++ b/src/shared/types/analysis-metadata.ts @@ -0,0 +1,24 @@ +export interface AnalysisMetadata { + timestamp: string; + filePaths: string[]; + dependencies?: Record; + executionTime?: number; + gitRefs?: { + base: string; + head: string; + }; +} + +export interface ExtractLog { + timestamp: string; + files: Record< + string, + { + hash: string; + size: number; + lastModified: string; + processed: boolean; + } + >; + [key: string]: unknown; +} diff --git a/src/shared/types/finding.ts b/src/shared/types/finding.ts new file mode 100644 index 00000000..54e4152e --- /dev/null +++ b/src/shared/types/finding.ts @@ -0,0 +1,34 @@ +export interface ReviewIssue { + id: string; + file: string; + line?: number; + type: 'bug' | 'security' | 'performance' | 'maintainability'; + severity: 'critical' | 'high' | 'medium' | 'low'; + category?: string; + description: string; + location: string; + reasoning: string; + suggestion: string; + context: string; + confidence: number; + tags?: string[]; + priority?: number; + estimatedEffort?: 'low' | 'medium' | 'high'; + knowledge_source?: string; + impact?: string; + cwe?: string; + threat_model?: string; + validation_reasoning?: string; +} + +export interface FixSuggestion { + issueId: string; + file: string; + line: number; + originalCode: string; + suggestedCode: string; + explanation: string; + confidence: 'high' | 'medium' | 'low'; + breaking: boolean; + applied: boolean; +} diff --git a/src/shared/types/fix-metadata.ts b/src/shared/types/fix-metadata.ts new file mode 100644 index 00000000..6be0984d --- /dev/null +++ b/src/shared/types/fix-metadata.ts @@ -0,0 +1,15 @@ +import type { FixSuggestion } from './finding'; + +export interface FixMetadata { + timestamp: string; + issuesProcessed: number; + suggestions: FixSuggestion[]; + summary: { + totalSuggestions: number; + highConfidence: number; + mediumConfidence: number; + lowConfidence: number; + breaking: number; + applied: number; + }; +} diff --git a/src/shared/types/framework-context.ts b/src/shared/types/framework-context.ts new file mode 100644 index 00000000..31c6261f --- /dev/null +++ b/src/shared/types/framework-context.ts @@ -0,0 +1,6 @@ +export interface FrameworkContext { + framework: 'angular' | 'react' | 'nodejs' | 'typescript'; + version?: string; + dependencies: string[]; + fileType?: 'service' | 'component' | 'guard' | 'pipe' | 'directive'; +} diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index e8ba4daf..80b7e280 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,325 +1,10 @@ -export * from './issue.model'; -export * from './pattern.model'; -export * from './session.model'; - -import type { z } from 'zod'; - -import type { aiProvider, modelConfigSchema } from '@/config/config-schema'; - -import type { ReviewConfig } from './config'; - -export type FilePath = string; -export type SessionId = string; - -export type FileContent = { - path: FilePath; - content: string; - encoding?: string; - size?: number; -}; - -export type CodeLocation = { - file: FilePath; - line: number; - column?: number; - endLine?: number; - endColumn?: number; -}; - -export type CodeSnippet = CodeLocation & { - code: string; - language?: string; -}; - -export type Metrics = { - duration: number; - filesAnalyzed: number; - issuesFound: number; - fixesApplied: number; - tokensUsed?: number; - cacheHits?: number; - cacheMisses?: number; -}; - -export type AIProviderName = z.infer; - -export type ModelConfig = z.infer; - -export type AIStageConfig = { - provider?: AIProviderName; // deprecated fallback; prefer model: { provider, name } - model: ModelConfig; - inputPerMillion: number; - outputPerMillion: number; - temperature?: number; - maxTokens?: number; - baseUrl?: string; - [key: string]: unknown; -}; - -export type ResolvedStageConfig = { - provider: AIProviderName; - model: string; - inputPerMillion: number; - outputPerMillion: number; - temperature?: number; - maxTokens?: number; - baseUrl?: string; - [key: string]: unknown; -}; - -export type Config = { - ai?: { - reviewStage?: AIStageConfig; - fixStage?: AIStageConfig; - judgeStage?: AIStageConfig; - }; - review?: ReviewConfig; - fix?: { - maxConcurrentFixes: number; - }; - report?: { - generateIssueMarkdown?: boolean; - enableRootCauseExtraction?: boolean; - }; - maxFilesPerBatch?: number; - maxConcurrency?: number; - cacheEnabled?: boolean; - cacheTTL?: number; - skipPatterns?: string[]; - includePatterns?: string[]; - outputFormat?: 'json' | 'html' | 'markdown'; - outputPath?: string; - verbose?: boolean; - debug?: boolean; - maxFileSizeKB?: number; - maxTokensPerFile?: number; - maxReactSteps?: number; - throttling?: { - apiCallsPerMinute: number; - }; -}; - -// Framework context for AI analysis -export interface FrameworkContext { - framework: 'angular' | 'react' | 'nodejs' | 'typescript'; - version?: string; - dependencies: string[]; - fileType?: 'service' | 'component' | 'guard' | 'pipe' | 'directive'; -} - -// Types used by AI review system -export interface ReviewIssue { - id: string; - file: string; - line?: number; - type: 'bug' | 'security' | 'performance' | 'maintainability'; - severity: 'critical' | 'high' | 'medium' | 'low'; - category?: string; - description: string; - location: string; - reasoning: string; - suggestion: string; - context: string; - confidence: number; - tags?: string[]; - priority?: number; - estimatedEffort?: 'low' | 'medium' | 'high'; - knowledge_source?: string; - impact?: string; - cwe?: string; - threat_model?: string; - validation_reasoning?: string; -} - -export interface FixSuggestion { - issueId: string; - file: string; - line: number; - originalCode: string; - suggestedCode: string; - explanation: string; - confidence: 'high' | 'medium' | 'low'; - breaking: boolean; - applied: boolean; -} - -export type Stage = 'analyze' | 'review' | 'fix' | 'judge' | 'report'; - -export type StageResult = { - stage: Stage; - status: 'success' | 'failure' | 'skipped'; - duration: number; - data?: unknown; - error?: Error; -}; - -// Analysis stage types -export interface AnalysisMetadata { - timestamp: string; - filePaths: string[]; - dependencies?: Record; - executionTime?: number; - gitRefs?: { - base: string; - head: string; - }; -} - -// Extract log types -export interface ExtractLog { - timestamp: string; - files: Record< - string, - { - hash: string; - size: number; - lastModified: string; - processed: boolean; - } - >; -} - -// Review stage types -export interface ReviewMetadata { - timestamp: string; - filesReviewed: number; - projectsReviewed?: number; - passed?: number; - failed?: number; - issues: ReviewIssue[]; - summary: { - totalIssues: number; - critical: number; - high: number; - medium: number; - low: number; - byType: { - bug: number; - security: number; - performance: number; - maintainability: number; - }; - }; - tokenUsage?: { - input: number; - output: number; - total: number; - }; -} - -// Root Cause Extract stage types -export interface RootCauseTaxonomy { - key: string; - label: string; - description: string; - patterns: string[]; -} - -export interface RootCauseClassification { - issueId: string; - rootCause: string; - confidence: number; -} - -export interface RootCauseMetadata { - timestamp: string; - totalIssues: number; - classifications: Record; - taxonomy: RootCauseTaxonomy[]; - distribution?: Record; -} - -// Filter stage types -export interface FilterMetadata { - timestamp: string; - originalIssues: number; - filteredIssues: number; - keptIssues: ReviewIssue[]; - excludedIssues: Array<{ - issue: ReviewIssue; - reason: string; - }>; - filterStats: { - byConfidence: Record; - byExclusionType: Record; - falsePositiveRate: number; - }; -} - -// Fix stage types -export interface FixMetadata { - timestamp: string; - issuesProcessed: number; - suggestions: FixSuggestion[]; - summary: { - totalSuggestions: number; - highConfidence: number; - mediumConfidence: number; - lowConfidence: number; - breaking: number; - applied: number; - }; -} - -// Report stage types -export interface ReportSection { - title: string; - content: string; -} - -export interface ReportMetadata { - timestamp: string; - summary: { - filesAnalyzed: number; - totalIssues: number; - critical: number; - high: number; - medium: number; - low: number; - fixSuggestions: number; - qualityStatus: 'FAILED' | 'WARNING' | 'PASSED'; - }; - sections: ReportSection[]; - executionTime: number; - stageResults: { - analyze: boolean; - review: boolean; - filter?: boolean; - fix: boolean; - }; - markdownReport?: string; - htmlReport?: string; -} - -// Quality thresholds for judge stage -export interface QualityThresholds { - minQualityScore?: number; - maxCriticalIssues: number; - maxHighIssues: number; - maxMediumIssues?: number; - maxLowIssues?: number; - requireAllStages?: boolean; - failOnMedium?: boolean; - failOnLow?: boolean; -} - -// Judge stage types -export interface JudgeMetadata { - timestamp: string; - passed: boolean; - qualityScore?: number; - qualityStatus: 'PASSED' | 'FAILED'; - summary: { - totalIssues: number; - critical: number; - high: number; - medium: number; - low: number; - }; - thresholds: QualityThresholds; - reasons: string[]; - warnings?: string[]; - details?: string; - detailedReport?: string; - executionTime?: number; -} +export * from './ai-config'; +export * from './framework-context'; +export * from './finding'; +export * from './pipeline'; +export * from './analysis-metadata'; +export * from './review-metadata'; +export * from './root-cause-metadata'; +export * from './fix-metadata'; +export * from './report-metadata'; +export * from './judge-metadata'; diff --git a/src/shared/types/integrations.ts b/src/shared/types/integrations.ts new file mode 100644 index 00000000..f9a80ce6 --- /dev/null +++ b/src/shared/types/integrations.ts @@ -0,0 +1,28 @@ +export const QUALOPS_COMMENT_MARKER = ''; + +export interface QualOpsResult { + summary: { + totalIssues: number; + criticalSeverity: number; + highSeverity: number; + mediumSeverity: number; + lowSeverity: number; + filesAnalyzed: number; + }; + reportPath: string; + issues: Array<{ + file: string; + line: number; + severity: string; + message: string; + category: string; + }>; +} + +export interface IntegrationReviewIssue { + file: string; + location: string; + severity: string; + description: string; + type: string; +} diff --git a/src/shared/types/issue.model.ts b/src/shared/types/issue.model.ts deleted file mode 100644 index 0f76f8fe..00000000 --- a/src/shared/types/issue.model.ts +++ /dev/null @@ -1,136 +0,0 @@ -export const IssueSeverity = { - Critical: 'critical', - Error: 'error', - Warning: 'warning', - Info: 'info', -} as const; - -export type IssueSeverity = (typeof IssueSeverity)[keyof typeof IssueSeverity]; - -export const IssueCategory = { - MemoryLeak: 'memory-leak', - Performance: 'performance', - Security: 'security', - BestPractice: 'best-practice', - CodeQuality: 'code-quality', - Accessibility: 'accessibility', - TypeSafety: 'type-safety', - StyleGuide: 'style-guide', -} as const; - -export type IssueCategory = (typeof IssueCategory)[keyof typeof IssueCategory]; - -export class Issue { - public readonly file: string; - public readonly line: number; - public readonly message: string; - public readonly severity: IssueSeverity; - public readonly category: IssueCategory; - public readonly column?: number; - public readonly endLine?: number; - public readonly endColumn?: number; - public readonly suggestion?: string; - public readonly pattern?: string; - public readonly evidence?: string; - public readonly confidence?: number; - public readonly ruleId?: string; - public readonly metadata?: Record; - - constructor(data: { - file: string; - line: number; - message: string; - severity: IssueSeverity; - category: IssueCategory; - column?: number; - endLine?: number; - endColumn?: number; - suggestion?: string; - pattern?: string; - evidence?: string; - confidence?: number; - ruleId?: string; - metadata?: Record; - }) { - this.file = data.file; - this.line = data.line; - this.message = data.message; - this.severity = data.severity; - this.category = data.category; - this.column = data.column; - this.endLine = data.endLine; - this.endColumn = data.endColumn; - this.suggestion = data.suggestion; - this.pattern = data.pattern; - this.evidence = data.evidence; - this.confidence = data.confidence; - this.ruleId = data.ruleId; - this.metadata = data.metadata; - } - - get id(): string { - return `${this.file}:${this.line}:${this.column || 0}:${this.ruleId || this.category}`; - } - - get isCritical(): boolean { - return this.severity === IssueSeverity.Critical; - } - - get isError(): boolean { - return this.severity === IssueSeverity.Error; - } - - get isWarning(): boolean { - return this.severity === IssueSeverity.Warning; - } - - get isInfo(): boolean { - return this.severity === IssueSeverity.Info; - } - - get hasAutoFix(): boolean { - return !!this.suggestion; - } - - get location(): string { - return `${this.file}:${this.line}${this.column ? ':' + this.column : ''}`; - } - - toJSON(): Record { - return { - file: this.file, - line: this.line, - column: this.column, - endLine: this.endLine, - endColumn: this.endColumn, - message: this.message, - severity: this.severity, - category: this.category, - suggestion: this.suggestion, - pattern: this.pattern, - evidence: this.evidence, - confidence: this.confidence, - ruleId: this.ruleId, - metadata: this.metadata, - }; - } - - static fromJSON(data: Record): Issue { - return new Issue({ - file: data.file as string, - line: data.line as number, - message: data.message as string, - severity: data.severity as IssueSeverity, - category: data.category as IssueCategory, - column: data.column as number | undefined, - endLine: data.endLine as number | undefined, - endColumn: data.endColumn as number | undefined, - suggestion: data.suggestion as string | undefined, - pattern: data.pattern as string | undefined, - evidence: data.evidence as string | undefined, - confidence: data.confidence as number | undefined, - ruleId: data.ruleId as string | undefined, - metadata: data.metadata as Record | undefined, - }); - } -} diff --git a/src/shared/types/judge-metadata.ts b/src/shared/types/judge-metadata.ts new file mode 100644 index 00000000..1293461a --- /dev/null +++ b/src/shared/types/judge-metadata.ts @@ -0,0 +1,30 @@ +export interface QualityThresholds { + minQualityScore?: number; + maxCriticalIssues: number; + maxHighIssues: number; + maxMediumIssues?: number; + maxLowIssues?: number; + requireAllStages?: boolean; + failOnMedium?: boolean; + failOnLow?: boolean; +} + +export interface JudgeMetadata { + timestamp: string; + passed: boolean; + qualityScore?: number; + qualityStatus: 'PASSED' | 'FAILED'; + summary: { + totalIssues: number; + critical: number; + high: number; + medium: number; + low: number; + }; + thresholds: QualityThresholds; + reasons: string[]; + warnings?: string[]; + details?: string; + detailedReport?: string; + executionTime?: number; +} diff --git a/src/shared/types/pattern.model.ts b/src/shared/types/pattern.model.ts deleted file mode 100644 index 407fd9dd..00000000 --- a/src/shared/types/pattern.model.ts +++ /dev/null @@ -1,167 +0,0 @@ -export const PatternType = { - Observable: 'observable', - EventListener: 'event-listener', - Timer: 'timer', - Promise: 'promise', - Subscription: 'subscription', - Resource: 'resource', -} as const; - -export type PatternType = (typeof PatternType)[keyof typeof PatternType]; - -export const CleanupRequirement = { - Required: 'required', - NotRequired: 'not-required', - Conditional: 'conditional', - Unknown: 'unknown', -} as const; - -export type CleanupRequirement = (typeof CleanupRequirement)[keyof typeof CleanupRequirement]; - -export class Pattern { - public readonly name: string; - public readonly type: PatternType; - public readonly cleanupRequirement: CleanupRequirement; - public readonly regex?: RegExp; - public readonly evidence?: string; - public readonly confidence?: number; - public readonly framework?: string; - public readonly documentation?: string; - public readonly examples?: string[]; - public readonly metadata?: Record; - - constructor(data: { - name: string; - type: PatternType; - cleanupRequirement: CleanupRequirement; - regex?: RegExp; - evidence?: string; - confidence?: number; - framework?: string; - documentation?: string; - examples?: string[]; - metadata?: Record; - }) { - this.name = data.name; - this.type = data.type; - this.cleanupRequirement = data.cleanupRequirement; - this.regex = data.regex; - this.evidence = data.evidence; - this.confidence = data.confidence; - this.framework = data.framework; - this.documentation = data.documentation; - this.examples = data.examples; - this.metadata = data.metadata; - } - - get requiresCleanup(): boolean { - return this.cleanupRequirement === CleanupRequirement.Required; - } - - get autoCompletes(): boolean { - return this.cleanupRequirement === CleanupRequirement.NotRequired; - } - - get isConditional(): boolean { - return this.cleanupRequirement === CleanupRequirement.Conditional; - } - - matches(code: string): boolean { - if (!this.regex) return false; - return this.regex.test(code); - } - - extractMatches(code: string): string[] { - if (!this.regex) return []; - const matches: string[] = []; - let match; - const regex = new RegExp(this.regex, 'g'); - while ((match = regex.exec(code)) !== null) { - matches.push(match[0]); - } - return matches; - } - - toJSON(): Record { - return { - name: this.name, - type: this.type, - cleanupRequirement: this.cleanupRequirement, - regex: this.regex?.source, - evidence: this.evidence, - confidence: this.confidence, - framework: this.framework, - documentation: this.documentation, - examples: this.examples, - metadata: this.metadata, - }; - } - - static fromJSON(data: Record): Pattern { - return new Pattern({ - name: data.name as string, - type: data.type as PatternType, - cleanupRequirement: data.cleanupRequirement as CleanupRequirement, - // nosemgrep: detect-non-literal-regexp -- pattern from operator's own stored config model, not attacker input - regex: data.regex ? new RegExp(data.regex as string) : undefined, - evidence: data.evidence as string | undefined, - confidence: data.confidence as number | undefined, - framework: data.framework as string | undefined, - documentation: data.documentation as string | undefined, - examples: data.examples as string[] | undefined, - metadata: data.metadata as Record | undefined, - }); - } -} - -export class PatternLibrary { - private patterns: Map = new Map(); - - add(pattern: Pattern): void { - this.patterns.set(pattern.name, pattern); - } - - get(name: string): Pattern | undefined { - return this.patterns.get(name); - } - - findMatches(code: string): Pattern[] { - return Array.from(this.patterns.values()).filter((p) => p.matches(code)); - } - - getByType(type: PatternType): Pattern[] { - return Array.from(this.patterns.values()).filter((p) => p.type === type); - } - - getByFramework(framework: string): Pattern[] { - return Array.from(this.patterns.values()).filter((p) => p.framework === framework); - } - - getRequiringCleanup(): Pattern[] { - return Array.from(this.patterns.values()).filter((p) => p.requiresCleanup); - } - - getAutoCompleting(): Pattern[] { - return Array.from(this.patterns.values()).filter((p) => p.autoCompletes); - } - - size(): number { - return this.patterns.size; - } - - clear(): void { - this.patterns.clear(); - } - - toJSON(): Record[] { - return Array.from(this.patterns.values()).map((p) => p.toJSON()); - } - - static fromJSON(data: unknown[]): PatternLibrary { - const library = new PatternLibrary(); - for (const item of data) { - library.add(Pattern.fromJSON(item as Record)); - } - return library; - } -} diff --git a/src/shared/types/pipeline.ts b/src/shared/types/pipeline.ts new file mode 100644 index 00000000..3941ab1c --- /dev/null +++ b/src/shared/types/pipeline.ts @@ -0,0 +1,9 @@ +export type Stage = 'analyze' | 'review' | 'fix' | 'judge' | 'report'; + +export type StageResult = { + stage: Stage; + status: 'success' | 'failure' | 'skipped'; + duration: number; + data?: unknown; + error?: Error; +}; diff --git a/src/shared/types/report-metadata.ts b/src/shared/types/report-metadata.ts new file mode 100644 index 00000000..8faad357 --- /dev/null +++ b/src/shared/types/report-metadata.ts @@ -0,0 +1,28 @@ +export interface ReportSection { + title: string; + content: string; +} + +export interface ReportMetadata { + timestamp: string; + summary: { + filesAnalyzed: number; + totalIssues: number; + critical: number; + high: number; + medium: number; + low: number; + fixSuggestions: number; + qualityStatus: 'FAILED' | 'WARNING' | 'PASSED'; + }; + sections: ReportSection[]; + executionTime: number; + stageResults: { + analyze: boolean; + review: boolean; + filter?: boolean; + fix: boolean; + }; + markdownReport?: string; + htmlReport?: string; +} diff --git a/src/shared/types/review-metadata.ts b/src/shared/types/review-metadata.ts new file mode 100644 index 00000000..92c0525d --- /dev/null +++ b/src/shared/types/review-metadata.ts @@ -0,0 +1,44 @@ +import type { ReviewIssue } from './finding'; + +export interface ReviewMetadata { + timestamp: string; + filesReviewed: number; + projectsReviewed?: number; + passed?: number; + failed?: number; + issues: ReviewIssue[]; + summary: { + totalIssues: number; + critical: number; + high: number; + medium: number; + low: number; + byType: { + bug: number; + security: number; + performance: number; + maintainability: number; + }; + }; + tokenUsage?: { + input: number; + output: number; + total: number; + }; +} + +export interface FilterMetadata { + timestamp: string; + originalIssues: number; + filteredIssues: number; + keptIssues: ReviewIssue[]; + excludedIssues: Array<{ + issue: ReviewIssue; + reason: string; + }>; + filterStats: { + byConfidence: Record; + byExclusionType: Record; + falsePositiveRate: number; + }; +} diff --git a/src/shared/types/root-cause-metadata.ts b/src/shared/types/root-cause-metadata.ts new file mode 100644 index 00000000..f277371f --- /dev/null +++ b/src/shared/types/root-cause-metadata.ts @@ -0,0 +1,20 @@ +export interface RootCauseTaxonomy { + key: string; + label: string; + description: string; + patterns: string[]; +} + +export interface RootCauseClassification { + issueId: string; + rootCause: string; + confidence: number; +} + +export interface RootCauseMetadata { + timestamp: string; + totalIssues: number; + classifications: Record; + taxonomy: RootCauseTaxonomy[]; + distribution?: Record; +} diff --git a/src/shared/types/session.model.ts b/src/shared/types/session.model.ts deleted file mode 100644 index e53f01a3..00000000 --- a/src/shared/types/session.model.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Issue } from './issue.model'; - -export const SessionStage = { - Analyze: 'analyze', - Review: 'review', - Fix: 'fix', - Judge: 'judge', - Report: 'report', -} as const; - -export type SessionStage = (typeof SessionStage)[keyof typeof SessionStage]; - -export const SessionStatus = { - Pending: 'pending', - Running: 'running', - Completed: 'completed', - Failed: 'failed', - Cancelled: 'cancelled', -} as const; - -export type SessionStatus = (typeof SessionStatus)[keyof typeof SessionStatus]; - -export class Session { - public readonly id: string; - public readonly startTime: Date; - public endTime?: Date; - public status: SessionStatus; - public currentStage?: SessionStage; - public stages: SessionStage[]; - public files: string[]; - public issues: Issue[]; - public fixes: unknown[]; - public metadata: Record; - - constructor(id: string, stages: SessionStage[] = [], metadata: Record = {}) { - this.id = id; - this.startTime = new Date(); - this.status = SessionStatus.Pending; - this.stages = stages; - this.files = []; - this.issues = []; - this.fixes = []; - this.metadata = metadata; - } - - get duration(): number | null { - if (!this.endTime) return null; - return this.endTime.getTime() - this.startTime.getTime(); - } - - get isCompleted(): boolean { - return this.status === SessionStatus.Completed; - } - - get isFailed(): boolean { - return this.status === SessionStatus.Failed; - } - - get isRunning(): boolean { - return this.status === SessionStatus.Running; - } - - get summary(): Record { - return { - id: this.id, - status: this.status, - startTime: this.startTime, - endTime: this.endTime, - duration: this.duration, - stages: this.stages, - currentStage: this.currentStage, - filesAnalyzed: this.files.length, - issuesFound: this.issues.length, - fixesApplied: this.fixes.length, - criticalIssues: this.issues.filter((i) => i.isCritical).length, - errorIssues: this.issues.filter((i) => i.isError).length, - warningIssues: this.issues.filter((i) => i.isWarning).length, - infoIssues: this.issues.filter((i) => i.isInfo).length, - }; - } - - start(): void { - this.status = SessionStatus.Running; - } - - complete(): void { - this.status = SessionStatus.Completed; - this.endTime = new Date(); - } - - fail(error?: Error): void { - this.status = SessionStatus.Failed; - this.endTime = new Date(); - if (error) { - this.metadata.error = { - message: error.message, - stack: error.stack, - }; - } - } - - cancel(): void { - this.status = SessionStatus.Cancelled; - this.endTime = new Date(); - } - - setStage(stage: SessionStage): void { - this.currentStage = stage; - } - - addFile(file: string): void { - if (!this.files.includes(file)) { - this.files.push(file); - } - } - - addIssue(issue: Issue): void { - this.issues.push(issue); - } - - addFix(fix: unknown): void { - this.fixes.push(fix); - } - - toJSON(): Record { - return { - ...this.summary, - metadata: this.metadata, - issues: this.issues.map((i) => i.toJSON()), - fixes: this.fixes, - }; - } - - static fromJSON(data: Record): Session { - const session = new Session( - data.id as string, - data.stages as SessionStage[], - data.metadata as Record, - ); - session.status = data.status as SessionStatus; - session.currentStage = data.currentStage as SessionStage | undefined; - session.files = (data.files as string[]) || []; - session.issues = Array.isArray(data.issues) - ? data.issues.map((i) => Issue.fromJSON(i as Record)) - : []; - session.fixes = (data.fixes as unknown[]) || []; - if (data.endTime) { - session.endTime = new Date(data.endTime as string | number); - } - return session; - } -} diff --git a/src/stages/analyze/utils/extract-log.ts b/src/stages/analyze/utils/extract-log.ts index 90754a69..a1e076e2 100644 --- a/src/stages/analyze/utils/extract-log.ts +++ b/src/stages/analyze/utils/extract-log.ts @@ -1,25 +1,14 @@ import { readFile } from 'node:fs/promises'; -import { calculateFileHash } from './hash-calculator'; +import { calculateFileHash } from '@/kernel/hash'; + import { getCurrentSessionPaths } from '../../../shared/runtime/session-context'; +import type { ExtractLog } from '../../../shared/types'; import { writeMetadataFile } from '../../../shared/utils/file-utils'; import { logger } from '../../../shared/utils/logger'; -export interface ExtractLog { - timestamp: string; - files: Record< - string, - { - hash: string; - size: number; - lastModified: string; - processed: boolean; - } - >; - [key: string]: unknown; -} - -export { calculateFileHash } from './hash-calculator'; +export type { ExtractLog } from '../../../shared/types'; +export { calculateFileHash } from '@/kernel/hash'; export async function loadExtractLog(): Promise { try { diff --git a/src/stages/report/constants.ts b/src/stages/report/constants.ts new file mode 100644 index 00000000..1017ca33 --- /dev/null +++ b/src/stages/report/constants.ts @@ -0,0 +1,4 @@ +export const CONFIDENCE_DISPLAY_THRESHOLDS = { + HIGH: 8, + MEDIUM: 6, +} as const; diff --git a/src/stages/report/templates/components.ts b/src/stages/report/templates/components.ts index 02e531da..614a8a3e 100644 --- a/src/stages/report/templates/components.ts +++ b/src/stages/report/templates/components.ts @@ -1,4 +1,5 @@ import type { ReviewIssue } from '../../../shared/types'; +import { CONFIDENCE_DISPLAY_THRESHOLDS } from '../constants'; import { calculateIssueCounts, generateSafeId } from '../utils/data-transformer'; import { getConfidenceBadge } from '../utils/formatters'; @@ -241,11 +242,11 @@ function generateConfidenceSection(issue: ReviewIssue): string { let confidenceColor: string; let explanation: string; - if (confidence >= 8) { + if (confidence >= CONFIDENCE_DISPLAY_THRESHOLDS.HIGH) { confidenceLevel = 'High'; confidenceColor = 'var(--color-success)'; explanation = 'This finding is highly reliable with strong evidence and clear reasoning.'; - } else if (confidence >= 6) { + } else if (confidence >= CONFIDENCE_DISPLAY_THRESHOLDS.MEDIUM) { confidenceLevel = 'Medium'; confidenceColor = 'var(--color-warning)'; explanation = 'This finding has reasonable evidence but may need additional verification.'; diff --git a/src/stages/report/utils/formatters.ts b/src/stages/report/utils/formatters.ts index 19426e47..082e2ef7 100644 --- a/src/stages/report/utils/formatters.ts +++ b/src/stages/report/utils/formatters.ts @@ -1,4 +1,5 @@ import type { ReviewIssue } from '../../../shared/types'; +import { CONFIDENCE_DISPLAY_THRESHOLDS } from '../constants'; export function getConfidenceBadge(confidence: number | undefined): string { if (confidence === undefined) return ''; @@ -6,10 +7,10 @@ export function getConfidenceBadge(confidence: number | undefined): string { let _level: string; let className: string; - if (confidence >= 8) { + if (confidence >= CONFIDENCE_DISPLAY_THRESHOLDS.HIGH) { _level = 'high'; className = 'confidence-high'; - } else if (confidence >= 6) { + } else if (confidence >= CONFIDENCE_DISPLAY_THRESHOLDS.MEDIUM) { _level = 'medium'; className = 'confidence-medium'; } else { diff --git a/src/stages/root-cause-extract/index.ts b/src/stages/root-cause-extract/index.ts index 40e6d5f8..9df8315a 100644 --- a/src/stages/root-cause-extract/index.ts +++ b/src/stages/root-cause-extract/index.ts @@ -6,7 +6,7 @@ import { AIFactory } from '../../ai/providers'; import { RootCauseClassificationsSchema } from '../../ai/shared/schemas/root-cause-classification'; import { StructuredOutputError } from '../../ai/shared/structured'; import { addStageTokenStats, getCurrentSessionPaths } from '../../shared/runtime/session-context'; -import type { RootCauseMetadata } from '../../shared/types'; +import type { RootCauseClassification, RootCauseMetadata } from '../../shared/types'; import { logger } from '../../shared/utils/logger'; import { isPathTraversalSafe } from '../../shared/utils/security'; @@ -19,12 +19,6 @@ interface IssueForClassification { filePath: string; } -interface ClassificationResult { - issueId: string; - rootCause: string; - confidence: number; -} - async function readIssuesFromFolder(issuesPath: string): Promise { const issues: IssueForClassification[] = []; @@ -66,7 +60,7 @@ async function readIssuesFromFolder(issuesPath: string): Promise extends Promise ? T : never, -): Promise { +): Promise { const taxonomyDescription = ROOT_CAUSE_TAXONOMY.map((rc) => { const patterns = rc.patterns.length > 0 ? `\n Patterns: ${rc.patterns.join(', ')}` : ''; return `- ${rc.key}: ${rc.label}${patterns}`; @@ -126,7 +120,7 @@ ${issuesForPrompt}`; } async function moveIssuesToRootCauseFolders( - classifications: Map, + classifications: Map, issuesBasePath: string, ): Promise { // First, create all root cause folders @@ -194,7 +188,7 @@ export async function extractRootCauses(): Promise { logger.ai(`Using AI provider: ${aiProvider.name}`); const BATCH_SIZE = 10; - const classifications = new Map(); + const classifications = new Map(); for (let i = 0; i < issues.length; i += BATCH_SIZE) { const batch = issues.slice(i, i + BATCH_SIZE); diff --git a/src/stages/root-cause-extract/taxonomy.ts b/src/stages/root-cause-extract/taxonomy.ts index 04832272..7e57e02f 100644 --- a/src/stages/root-cause-extract/taxonomy.ts +++ b/src/stages/root-cause-extract/taxonomy.ts @@ -1,9 +1,6 @@ -export interface RootCauseTaxonomy { - key: string; - label: string; - description: string; - patterns: string[]; -} +import type { RootCauseTaxonomy } from '../../shared/types'; + +export type { RootCauseTaxonomy } from '../../shared/types'; export const ROOT_CAUSE_TAXONOMY: RootCauseTaxonomy[] = [ { diff --git a/tests/unit/stages/analyze/utils/hash-calculator.spec.ts b/tests/unit/kernel/hash.spec.ts similarity index 98% rename from tests/unit/stages/analyze/utils/hash-calculator.spec.ts rename to tests/unit/kernel/hash.spec.ts index 2d75c7e7..a0170ff7 100644 --- a/tests/unit/stages/analyze/utils/hash-calculator.spec.ts +++ b/tests/unit/kernel/hash.spec.ts @@ -1,7 +1,7 @@ import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; -import { calculateFileHash, calculateFileHashes } from '@/stages/analyze/utils/hash-calculator'; +import { calculateFileHash, calculateFileHashes } from '@/kernel/hash'; jest.mock('node:crypto'); jest.mock('node:fs/promises'); diff --git a/tests/unit/stages/analyze/utils/extract-log.spec.ts b/tests/unit/stages/analyze/utils/extract-log.spec.ts index 735874d6..addc822b 100644 --- a/tests/unit/stages/analyze/utils/extract-log.spec.ts +++ b/tests/unit/stages/analyze/utils/extract-log.spec.ts @@ -1,5 +1,6 @@ import { readFile, stat } from 'node:fs/promises'; +import { calculateFileHash } from '@/kernel/hash'; import { getCurrentSessionPaths } from '@/shared/runtime/session-context'; import { writeMetadataFile } from '@/shared/utils/file-utils'; import { logger } from '@/shared/utils/logger'; @@ -12,13 +13,12 @@ import { updateExtractLog, updateFileInExtractLog, } from '@/stages/analyze/utils/extract-log'; -import { calculateFileHash } from '@/stages/analyze/utils/hash-calculator'; jest.mock('node:fs/promises'); jest.mock('@/shared/runtime/session-context'); jest.mock('@/shared/utils/file-utils'); jest.mock('@/shared/utils/logger'); -jest.mock('@/stages/analyze/utils/hash-calculator'); +jest.mock('@/kernel/hash'); const mockReadFile = readFile as jest.MockedFunction; const mockStat = stat as jest.MockedFunction;