From cf967d9cc9317ae99b58767148fcab022e05b664 Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 13:42:33 +0200 Subject: [PATCH 01/14] refactor(types): delete dead model files (issue/pattern/session) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes three fully-unused modules (~457 LOC) — verified no importers outside their own barrel re-exports. Also removes the mismatched legacy severity vocabulary (critical/error/warning/info) and category enum they carried, which conflicted with the live critical/high/medium/low set. No behavior change: build clean, 2619/2619 tests pass (unchanged). Co-Authored-By: Claude Opus 4.8 --- src/shared/types/index.ts | 4 - src/shared/types/issue.model.ts | 136 ------------------------ src/shared/types/pattern.model.ts | 166 ------------------------------ src/shared/types/session.model.ts | 152 --------------------------- 4 files changed, 458 deletions(-) delete mode 100644 src/shared/types/issue.model.ts delete mode 100644 src/shared/types/pattern.model.ts delete mode 100644 src/shared/types/session.model.ts diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index e8ba4daf..5b2ad7a5 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,7 +1,3 @@ -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'; 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/pattern.model.ts b/src/shared/types/pattern.model.ts deleted file mode 100644 index 38bcfe53..00000000 --- a/src/shared/types/pattern.model.ts +++ /dev/null @@ -1,166 +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, - 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/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; - } -} From 61bce355f14b88ba5d93761aaa3d9cbf135ea809 Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 13:49:33 +0200 Subject: [PATCH 02/14] refactor(types): unify duplicated root-cause & extract-log types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One definition per concept, forward-layered (shared/types is the home; stages import from it — no low→high dependency introduced): - RootCauseTaxonomy: delete the byte-identical copy in taxonomy.ts; re-export from shared/types. - RootCauseClassification: delete the identical ClassificationResult interface in root-cause-extract; use the shared type. - ExtractLog: single definition in shared/types (kept the index signature superset); extract-log.ts re-exports it. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/shared/types/index.ts | 1 + src/stages/analyze/utils/extract-log.ts | 16 ++-------------- src/stages/root-cause-extract/index.ts | 14 ++++---------- src/stages/root-cause-extract/taxonomy.ts | 9 +++------ 4 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 5b2ad7a5..ef60cb0c 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -173,6 +173,7 @@ export interface ExtractLog { processed: boolean; } >; + [key: string]: unknown; } // Review stage types diff --git a/src/stages/analyze/utils/extract-log.ts b/src/stages/analyze/utils/extract-log.ts index 90754a69..da5095aa 100644 --- a/src/stages/analyze/utils/extract-log.ts +++ b/src/stages/analyze/utils/extract-log.ts @@ -2,23 +2,11 @@ import { readFile } from 'node:fs/promises'; import { calculateFileHash } from './hash-calculator'; 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 type { ExtractLog } from '../../../shared/types'; export { calculateFileHash } from './hash-calculator'; export async function loadExtractLog(): Promise { 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[] = [ { From 7916d78565b3f6e41bdd212fba699168e09f1aca Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 13:53:40 +0200 Subject: [PATCH 03/14] refactor(forges): extract duplicated forge-integration contracts to forges/core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QualOpsResult, the QUALOPS_COMMENT_MARKER constant, and the loosely-typed forge issue shape were duplicated byte-for-byte in github-integration.ts and gitlab-integration.ts. Centralized into a new src/forges/core module; both integrations import from it. The forge issue DTO is renamed ForgeReviewIssue to disambiguate it from the canonical ReviewIssue (it is a forge-boundary type read from unvalidated JSON, deliberately kept distinct — not widened). No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/forges/core/index.ts | 44 ++++++++++++++++++++++++++++++++ src/github/github-integration.ts | 32 ++--------------------- src/gitlab/gitlab-integration.ts | 32 ++--------------------- 3 files changed, 48 insertions(+), 60 deletions(-) create mode 100644 src/forges/core/index.ts diff --git a/src/forges/core/index.ts b/src/forges/core/index.ts new file mode 100644 index 00000000..26032df9 --- /dev/null +++ b/src/forges/core/index.ts @@ -0,0 +1,44 @@ +/** + * Shared forge-integration contracts and constants. + * + * Home for logic common to the GitHub and GitLab integrations, previously + * duplicated byte-for-byte in each. Behaviour is unchanged — these are the + * exact shapes/values both integrations already used. + */ + +/** Hidden marker embedded in QualOps-authored PR/MR comments for re-identification. */ +export const QUALOPS_COMMENT_MARKER = ''; + +/** The pipeline result payload both forge integrations read from disk. */ +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; + }>; +} + +/** + * The loosely-typed issue shape the forge integrations parse from review + * output (all-string, read from unvalidated JSON). Intentionally distinct from + * the canonical `ReviewIssue` in `shared/types` — it is a forge-boundary DTO, + * not the pipeline's finding type. + */ +export interface ForgeReviewIssue { + file: string; + location: string; + severity: string; + description: string; + type: string; +} diff --git a/src/github/github-integration.ts b/src/github/github-integration.ts index 45185939..10b9588b 100644 --- a/src/github/github-integration.ts +++ b/src/github/github-integration.ts @@ -3,10 +3,9 @@ import { join } from 'path'; import { GitHubAPIClient } from './github-api-client'; import { GitHubChecksService } from './github-checks'; +import { QUALOPS_COMMENT_MARKER, type ForgeReviewIssue, type QualOpsResult } from '../forges/core'; import { logger } from '../shared/utils/logger'; -const QUALOPS_COMMENT_MARKER = ''; - interface GitHubConfig { enabled?: boolean; postComments?: boolean; @@ -31,33 +30,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; @@ -240,7 +212,7 @@ export class GitHubIntegration { try { const reviewSummary = JSON.parse(readFileSync(reviewSummaryPath, 'utf8')); const summary = reviewSummary.summary || {}; - const issues: ReviewIssue[] = reviewSummary.issues || []; + const issues: ForgeReviewIssue[] = reviewSummary.issues || []; return { summary: { diff --git a/src/gitlab/gitlab-integration.ts b/src/gitlab/gitlab-integration.ts index ab51f401..98aa3628 100644 --- a/src/gitlab/gitlab-integration.ts +++ b/src/gitlab/gitlab-integration.ts @@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync } from 'fs'; import { join } from 'path'; +import { QUALOPS_COMMENT_MARKER, type ForgeReviewIssue, type QualOpsResult } from '../forges/core'; import { logger } from '../shared/utils/logger'; import { hasValidUrlScheme, isValidGitSha } from '../shared/utils/security'; @@ -13,8 +14,6 @@ const SEVERITY_EMOJI = { low: '🟢', } as const; -const QUALOPS_COMMENT_MARKER = ''; - interface GitLabConfig { enabled?: boolean; postComments?: boolean; @@ -43,25 +42,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 +55,6 @@ interface GitLabNote { }; } -interface ReviewIssue { - file: string; - location: string; - severity: string; - description: string; - type: string; -} - class GitLabIntegration { private env: GitLabEnv; private apiUrl: string; @@ -597,7 +569,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: ForgeReviewIssue) => { // Parse location field - handle both "42" and "line:42" formats const locationStr = issue.location.replace(/^line:?/i, '').trim(); const lineNumber = parseInt(locationStr, 10) || 0; From d3747647796cc8c18a930899a23a285a7162bc5d Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 13:57:44 +0200 Subject: [PATCH 04/14] refactor(kernel): introduce kernel/ layer; relocate pure hash utility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establishes src/kernel/ as the stdlib-only shared-utility layer (concept architecture §2). Moves the content-hashing helper (calculateFileHash/ calculateFileHashes — only node:crypto + node:fs, genuinely pure) to kernel/hash.ts; extract-log.ts imports it via the @/kernel alias. Test mirror relocated to tests/unit/kernel/hash.spec.ts and repointed. concurrency.ts was intentionally NOT moved: it depends on the logger, so it is not stdlib-pure; relocating it would either seed a kernel→shared dependency or require removing logging (a behavior change). Deferred. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- .../analyze/utils/hash-calculator.ts => kernel/hash.ts} | 0 src/stages/analyze/utils/extract-log.ts | 5 +++-- .../utils/hash-calculator.spec.ts => kernel/hash.spec.ts} | 2 +- tests/unit/stages/analyze/utils/extract-log.spec.ts | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) rename src/{stages/analyze/utils/hash-calculator.ts => kernel/hash.ts} (100%) rename tests/unit/{stages/analyze/utils/hash-calculator.spec.ts => kernel/hash.spec.ts} (98%) 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/stages/analyze/utils/extract-log.ts b/src/stages/analyze/utils/extract-log.ts index da5095aa..a1e076e2 100644 --- a/src/stages/analyze/utils/extract-log.ts +++ b/src/stages/analyze/utils/extract-log.ts @@ -1,13 +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 type { ExtractLog } from '../../../shared/types'; -export { calculateFileHash } from './hash-calculator'; +export { calculateFileHash } from '@/kernel/hash'; export async function loadExtractLog(): Promise { try { 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; From 9efc753cc2716a4656e71b60a7835e39c71fdb8e Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 14:10:37 +0200 Subject: [PATCH 05/14] refactor(types): split shared/types/index.ts into domain-focused files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 323-line barrel mixed generic primitives, AI/provider config, framework context, the core finding shapes, pipeline identity, and per-stage metadata for five different stages. Split by domain/purpose with speaking names: primitives.ts · ai-config.ts · framework-context.ts · finding.ts · pipeline.ts · analysis-metadata.ts · review-metadata.ts · root-cause-metadata.ts · fix-metadata.ts · report-metadata.ts · judge-metadata.ts index.ts is now a pure barrel re-exporting them, so every existing `from '.../shared/types'` import keeps resolving — zero consumer churn. Type definitions are copied verbatim (no field renames — those would be a behavior change on serialized shapes). The drifted second FrameworkContext in framework-detector.ts is noted for a separate reconciliation. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/shared/types/ai-config.ts | 65 +++++ src/shared/types/analysis-metadata.ts | 27 ++ src/shared/types/finding.ts | 36 +++ src/shared/types/fix-metadata.ts | 17 ++ src/shared/types/framework-context.ts | 14 + src/shared/types/index.ts | 341 ++---------------------- src/shared/types/judge-metadata.ts | 32 +++ src/shared/types/pipeline.ts | 11 + src/shared/types/primitives.ts | 34 +++ src/shared/types/report-metadata.ts | 30 +++ src/shared/types/review-metadata.ts | 46 ++++ src/shared/types/root-cause-metadata.ts | 22 ++ 12 files changed, 353 insertions(+), 322 deletions(-) create mode 100644 src/shared/types/ai-config.ts create mode 100644 src/shared/types/analysis-metadata.ts create mode 100644 src/shared/types/finding.ts create mode 100644 src/shared/types/fix-metadata.ts create mode 100644 src/shared/types/framework-context.ts create mode 100644 src/shared/types/judge-metadata.ts create mode 100644 src/shared/types/pipeline.ts create mode 100644 src/shared/types/primitives.ts create mode 100644 src/shared/types/report-metadata.ts create mode 100644 src/shared/types/review-metadata.ts create mode 100644 src/shared/types/root-cause-metadata.ts diff --git a/src/shared/types/ai-config.ts b/src/shared/types/ai-config.ts new file mode 100644 index 00000000..518182f2 --- /dev/null +++ b/src/shared/types/ai-config.ts @@ -0,0 +1,65 @@ +/** AI provider, model, and per-stage/overall configuration shapes. */ + +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..42123149 --- /dev/null +++ b/src/shared/types/analysis-metadata.ts @@ -0,0 +1,27 @@ +/** Persisted metadata written by the analyze stage. */ + +export interface AnalysisMetadata { + timestamp: string; + filePaths: string[]; + dependencies?: Record; + executionTime?: number; + gitRefs?: { + base: string; + head: string; + }; +} + +/** File-hash log tracking which files have been processed. */ +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..d52c98e0 --- /dev/null +++ b/src/shared/types/finding.ts @@ -0,0 +1,36 @@ +/** The core review finding and fix-suggestion shapes used across stages. */ + +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..2402a97f --- /dev/null +++ b/src/shared/types/fix-metadata.ts @@ -0,0 +1,17 @@ +/** Persisted metadata written by the fix stage. */ + +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..3d46fe19 --- /dev/null +++ b/src/shared/types/framework-context.ts @@ -0,0 +1,14 @@ +/** + * Framework context for AI analysis. + * + * NOTE: a second, drifted definition of this concept lives in + * `shared/utils/framework-detector.ts`. Reconciling the two is a + * behaviour-touching change and is intentionally deferred (not part of the + * structure-only refactor). + */ +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 ef60cb0c..96e23155 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,322 +1,19 @@ -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; - } - >; - [key: string]: unknown; -} - -// 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; -} +/** + * Barrel for the shared type vocabulary. + * + * Definitions live in domain-focused sibling files; this file only re-exports + * them so existing `from '.../shared/types'` imports keep resolving. Add new + * shared types to the appropriate domain file, not here. + */ + +export * from './primitives'; +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/judge-metadata.ts b/src/shared/types/judge-metadata.ts new file mode 100644 index 00000000..61586b7f --- /dev/null +++ b/src/shared/types/judge-metadata.ts @@ -0,0 +1,32 @@ +/** Quality thresholds and the verdict metadata written by the judge stage. */ + +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/pipeline.ts b/src/shared/types/pipeline.ts new file mode 100644 index 00000000..e29f7ae5 --- /dev/null +++ b/src/shared/types/pipeline.ts @@ -0,0 +1,11 @@ +/** Pipeline stage identity and per-stage execution result. */ + +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/primitives.ts b/src/shared/types/primitives.ts new file mode 100644 index 00000000..69b927d9 --- /dev/null +++ b/src/shared/types/primitives.ts @@ -0,0 +1,34 @@ +/** Generic, domain-agnostic value shapes shared across the pipeline. */ + +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; +}; diff --git a/src/shared/types/report-metadata.ts b/src/shared/types/report-metadata.ts new file mode 100644 index 00000000..6d78960c --- /dev/null +++ b/src/shared/types/report-metadata.ts @@ -0,0 +1,30 @@ +/** Persisted metadata and section shapes written by the report stage. */ + +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..082a3b86 --- /dev/null +++ b/src/shared/types/review-metadata.ts @@ -0,0 +1,46 @@ +/** Persisted metadata written by the review and filter stages. */ + +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..aa913548 --- /dev/null +++ b/src/shared/types/root-cause-metadata.ts @@ -0,0 +1,22 @@ +/** Taxonomy and classification shapes for the root-cause-extract stage. */ + +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; +} From 345aec140bc25d7381b6d0c49dddfbe337a11a18 Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 14:19:29 +0200 Subject: [PATCH 06/14] refactor(types): replace generic primitives.ts with purpose-named files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit primitives.ts was a generic bucket holding three unrelated concerns. Split into speaking names: metrics.ts (Metrics), source-code.ts (FilePath, FileContent, CodeLocation, CodeSnippet), session.ts (SessionId). Barrel re-exports them, so the package's public type API is unchanged (these types are exported via src/index.ts for external consumers — they have no internal callers, which is expected for public-API vocabulary). No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/shared/types/index.ts | 4 +++- src/shared/types/metrics.ts | 10 ++++++++++ src/shared/types/session.ts | 2 ++ src/shared/types/{primitives.ts => source-code.ts} | 13 +------------ 4 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 src/shared/types/metrics.ts create mode 100644 src/shared/types/session.ts rename src/shared/types/{primitives.ts => source-code.ts} (54%) diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 96e23155..2eaacce0 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -6,7 +6,9 @@ * shared types to the appropriate domain file, not here. */ -export * from './primitives'; +export * from './metrics'; +export * from './source-code'; +export * from './session'; export * from './ai-config'; export * from './framework-context'; export * from './finding'; diff --git a/src/shared/types/metrics.ts b/src/shared/types/metrics.ts new file mode 100644 index 00000000..87c98c1d --- /dev/null +++ b/src/shared/types/metrics.ts @@ -0,0 +1,10 @@ +/** Aggregate metrics for a single pipeline run. */ +export type Metrics = { + duration: number; + filesAnalyzed: number; + issuesFound: number; + fixesApplied: number; + tokensUsed?: number; + cacheHits?: number; + cacheMisses?: number; +}; diff --git a/src/shared/types/session.ts b/src/shared/types/session.ts new file mode 100644 index 00000000..78a60866 --- /dev/null +++ b/src/shared/types/session.ts @@ -0,0 +1,2 @@ +/** Identifier for a single pipeline run/session. */ +export type SessionId = string; diff --git a/src/shared/types/primitives.ts b/src/shared/types/source-code.ts similarity index 54% rename from src/shared/types/primitives.ts rename to src/shared/types/source-code.ts index 69b927d9..6ed6124a 100644 --- a/src/shared/types/primitives.ts +++ b/src/shared/types/source-code.ts @@ -1,7 +1,6 @@ -/** Generic, domain-agnostic value shapes shared across the pipeline. */ +/** File paths, file content, and positions/snippets within source code. */ export type FilePath = string; -export type SessionId = string; export type FileContent = { path: FilePath; @@ -22,13 +21,3 @@ 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; -}; From 4d7adc7e9f552cf99a0d33e1a80bdb33fb220ba4 Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 14:24:00 +0200 Subject: [PATCH 07/14] refactor(report): extract duplicated confidence-badge thresholds to a constant The 1-10 confidence display cutoffs (>=8 High, >=6 Medium) were hardcoded identically in report/utils/formatters.ts and report/templates/components.ts. Extracted to CONFIDENCE_DISPLAY_THRESHOLDS in a new report/constants.ts, with a doc comment clarifying it is display-only bucketing, not a gating threshold. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/stages/report/constants.ts | 10 ++++++++++ src/stages/report/templates/components.ts | 5 +++-- src/stages/report/utils/formatters.ts | 5 +++-- 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/stages/report/constants.ts diff --git a/src/stages/report/constants.ts b/src/stages/report/constants.ts new file mode 100644 index 00000000..27e311dd --- /dev/null +++ b/src/stages/report/constants.ts @@ -0,0 +1,10 @@ +/** Report-stage display constants. */ + +/** + * Confidence-score (1–10 scale) cutoffs for the report's High/Medium/Low + * confidence badges. Display-only bucketing — not a gating threshold. + */ +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 { From c642343968a84008e21f016165c9bffb2415fe5f Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 14:31:41 +0200 Subject: [PATCH 08/14] refactor(types): remove dead public-API-only types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FilePath, SessionId, FileContent, CodeLocation, CodeSnippet, and Metrics were exported via src/index.ts (`export * from './shared/types'`) but used NOWHERE in src, tests, or evals — verified by whole-repo search. They were public API by accident, not by design. Removed (deletes the metrics.ts/source-code.ts/ session.ts files added in the previous commit, now that a full-repo usage check confirms they are unreferenced). Reduces the published package's exported type surface — acceptable for a 0.x package with no evidence of external use; flagged for release notes. Types that ARE used (FrameworkContext, ReviewIssue, the metadata shapes, Config…) are untouched. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/shared/types/index.ts | 3 --- src/shared/types/metrics.ts | 10 ---------- src/shared/types/session.ts | 2 -- src/shared/types/source-code.ts | 23 ----------------------- 4 files changed, 38 deletions(-) delete mode 100644 src/shared/types/metrics.ts delete mode 100644 src/shared/types/session.ts delete mode 100644 src/shared/types/source-code.ts diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 2eaacce0..713ec930 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -6,9 +6,6 @@ * shared types to the appropriate domain file, not here. */ -export * from './metrics'; -export * from './source-code'; -export * from './session'; export * from './ai-config'; export * from './framework-context'; export * from './finding'; diff --git a/src/shared/types/metrics.ts b/src/shared/types/metrics.ts deleted file mode 100644 index 87c98c1d..00000000 --- a/src/shared/types/metrics.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** Aggregate metrics for a single pipeline run. */ -export type Metrics = { - duration: number; - filesAnalyzed: number; - issuesFound: number; - fixesApplied: number; - tokensUsed?: number; - cacheHits?: number; - cacheMisses?: number; -}; diff --git a/src/shared/types/session.ts b/src/shared/types/session.ts deleted file mode 100644 index 78a60866..00000000 --- a/src/shared/types/session.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** Identifier for a single pipeline run/session. */ -export type SessionId = string; diff --git a/src/shared/types/source-code.ts b/src/shared/types/source-code.ts deleted file mode 100644 index 6ed6124a..00000000 --- a/src/shared/types/source-code.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** File paths, file content, and positions/snippets within source code. */ - -export type FilePath = 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; -}; From 4ddc3041c44543da777d45e008ef7b29be91ded1 Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 14:43:02 +0200 Subject: [PATCH 09/14] refactor(pricing): extract TOKENS_PER_MILLION constant The per-million token-pricing divisor (1_000_000) was hardcoded in 6 cost-math sites across base.ts, bedrock.ts, and all-command.ts. Extracted to a named constant in a dedicated src/ai/providers/pricing-constants.ts. Homed in its own module (not config.ts) deliberately: several provider unit tests mock '@/config/config' wholesale, which would make a constant defined there `undefined` under mock (NaN cost). A dedicated, never-mocked file keeps the real value everywhere. Distinct from the un-underscored 1000000 maxTokensPerFile cap, which is a different concept and left untouched. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/ai/providers/base.ts | 10 ++++++---- src/ai/providers/bedrock.ts | 3 ++- src/ai/providers/pricing-constants.ts | 2 ++ src/cli/commands/all-command.ts | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 src/ai/providers/pricing-constants.ts 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..f7a2f040 --- /dev/null +++ b/src/ai/providers/pricing-constants.ts @@ -0,0 +1,2 @@ +/** Token-pricing unit: model prices are quoted per one million tokens. */ +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:'); From 0dd28239bfc3a2b98b19b798bd0f375ebdf7f9ce Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 14:51:59 +0200 Subject: [PATCH 10/14] refactor: strip narration/redundant comments added during the refactor Remove file-header and rationale comments that restated the obvious or tracked history ("previously duplicated", "behaviour unchanged", etc.). Code stays lean; names carry the meaning. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/ai/providers/pricing-constants.ts | 1 - src/forges/core/index.ts | 16 ---------------- src/shared/types/ai-config.ts | 2 -- src/shared/types/analysis-metadata.ts | 3 --- src/shared/types/finding.ts | 2 -- src/shared/types/fix-metadata.ts | 2 -- src/shared/types/framework-context.ts | 8 -------- src/shared/types/index.ts | 8 -------- src/shared/types/judge-metadata.ts | 2 -- src/shared/types/pipeline.ts | 2 -- src/shared/types/report-metadata.ts | 2 -- src/shared/types/review-metadata.ts | 2 -- src/shared/types/root-cause-metadata.ts | 2 -- src/stages/report/constants.ts | 6 ------ 14 files changed, 58 deletions(-) diff --git a/src/ai/providers/pricing-constants.ts b/src/ai/providers/pricing-constants.ts index f7a2f040..75d5b8bf 100644 --- a/src/ai/providers/pricing-constants.ts +++ b/src/ai/providers/pricing-constants.ts @@ -1,2 +1 @@ -/** Token-pricing unit: model prices are quoted per one million tokens. */ export const TOKENS_PER_MILLION = 1_000_000; diff --git a/src/forges/core/index.ts b/src/forges/core/index.ts index 26032df9..803006cf 100644 --- a/src/forges/core/index.ts +++ b/src/forges/core/index.ts @@ -1,15 +1,5 @@ -/** - * Shared forge-integration contracts and constants. - * - * Home for logic common to the GitHub and GitLab integrations, previously - * duplicated byte-for-byte in each. Behaviour is unchanged — these are the - * exact shapes/values both integrations already used. - */ - -/** Hidden marker embedded in QualOps-authored PR/MR comments for re-identification. */ export const QUALOPS_COMMENT_MARKER = ''; -/** The pipeline result payload both forge integrations read from disk. */ export interface QualOpsResult { summary: { totalIssues: number; @@ -29,12 +19,6 @@ export interface QualOpsResult { }>; } -/** - * The loosely-typed issue shape the forge integrations parse from review - * output (all-string, read from unvalidated JSON). Intentionally distinct from - * the canonical `ReviewIssue` in `shared/types` — it is a forge-boundary DTO, - * not the pipeline's finding type. - */ export interface ForgeReviewIssue { file: string; location: string; diff --git a/src/shared/types/ai-config.ts b/src/shared/types/ai-config.ts index 518182f2..fca08c10 100644 --- a/src/shared/types/ai-config.ts +++ b/src/shared/types/ai-config.ts @@ -1,5 +1,3 @@ -/** AI provider, model, and per-stage/overall configuration shapes. */ - import type { z } from 'zod'; import type { aiProvider, modelConfigSchema } from '@/config/config-schema'; diff --git a/src/shared/types/analysis-metadata.ts b/src/shared/types/analysis-metadata.ts index 42123149..e4a89394 100644 --- a/src/shared/types/analysis-metadata.ts +++ b/src/shared/types/analysis-metadata.ts @@ -1,5 +1,3 @@ -/** Persisted metadata written by the analyze stage. */ - export interface AnalysisMetadata { timestamp: string; filePaths: string[]; @@ -11,7 +9,6 @@ export interface AnalysisMetadata { }; } -/** File-hash log tracking which files have been processed. */ export interface ExtractLog { timestamp: string; files: Record< diff --git a/src/shared/types/finding.ts b/src/shared/types/finding.ts index d52c98e0..54e4152e 100644 --- a/src/shared/types/finding.ts +++ b/src/shared/types/finding.ts @@ -1,5 +1,3 @@ -/** The core review finding and fix-suggestion shapes used across stages. */ - export interface ReviewIssue { id: string; file: string; diff --git a/src/shared/types/fix-metadata.ts b/src/shared/types/fix-metadata.ts index 2402a97f..6be0984d 100644 --- a/src/shared/types/fix-metadata.ts +++ b/src/shared/types/fix-metadata.ts @@ -1,5 +1,3 @@ -/** Persisted metadata written by the fix stage. */ - import type { FixSuggestion } from './finding'; export interface FixMetadata { diff --git a/src/shared/types/framework-context.ts b/src/shared/types/framework-context.ts index 3d46fe19..31c6261f 100644 --- a/src/shared/types/framework-context.ts +++ b/src/shared/types/framework-context.ts @@ -1,11 +1,3 @@ -/** - * Framework context for AI analysis. - * - * NOTE: a second, drifted definition of this concept lives in - * `shared/utils/framework-detector.ts`. Reconciling the two is a - * behaviour-touching change and is intentionally deferred (not part of the - * structure-only refactor). - */ export interface FrameworkContext { framework: 'angular' | 'react' | 'nodejs' | 'typescript'; version?: string; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 713ec930..80b7e280 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,11 +1,3 @@ -/** - * Barrel for the shared type vocabulary. - * - * Definitions live in domain-focused sibling files; this file only re-exports - * them so existing `from '.../shared/types'` imports keep resolving. Add new - * shared types to the appropriate domain file, not here. - */ - export * from './ai-config'; export * from './framework-context'; export * from './finding'; diff --git a/src/shared/types/judge-metadata.ts b/src/shared/types/judge-metadata.ts index 61586b7f..1293461a 100644 --- a/src/shared/types/judge-metadata.ts +++ b/src/shared/types/judge-metadata.ts @@ -1,5 +1,3 @@ -/** Quality thresholds and the verdict metadata written by the judge stage. */ - export interface QualityThresholds { minQualityScore?: number; maxCriticalIssues: number; diff --git a/src/shared/types/pipeline.ts b/src/shared/types/pipeline.ts index e29f7ae5..3941ab1c 100644 --- a/src/shared/types/pipeline.ts +++ b/src/shared/types/pipeline.ts @@ -1,5 +1,3 @@ -/** Pipeline stage identity and per-stage execution result. */ - export type Stage = 'analyze' | 'review' | 'fix' | 'judge' | 'report'; export type StageResult = { diff --git a/src/shared/types/report-metadata.ts b/src/shared/types/report-metadata.ts index 6d78960c..8faad357 100644 --- a/src/shared/types/report-metadata.ts +++ b/src/shared/types/report-metadata.ts @@ -1,5 +1,3 @@ -/** Persisted metadata and section shapes written by the report stage. */ - export interface ReportSection { title: string; content: string; diff --git a/src/shared/types/review-metadata.ts b/src/shared/types/review-metadata.ts index 082a3b86..92c0525d 100644 --- a/src/shared/types/review-metadata.ts +++ b/src/shared/types/review-metadata.ts @@ -1,5 +1,3 @@ -/** Persisted metadata written by the review and filter stages. */ - import type { ReviewIssue } from './finding'; export interface ReviewMetadata { diff --git a/src/shared/types/root-cause-metadata.ts b/src/shared/types/root-cause-metadata.ts index aa913548..f277371f 100644 --- a/src/shared/types/root-cause-metadata.ts +++ b/src/shared/types/root-cause-metadata.ts @@ -1,5 +1,3 @@ -/** Taxonomy and classification shapes for the root-cause-extract stage. */ - export interface RootCauseTaxonomy { key: string; label: string; diff --git a/src/stages/report/constants.ts b/src/stages/report/constants.ts index 27e311dd..1017ca33 100644 --- a/src/stages/report/constants.ts +++ b/src/stages/report/constants.ts @@ -1,9 +1,3 @@ -/** Report-stage display constants. */ - -/** - * Confidence-score (1–10 scale) cutoffs for the report's High/Medium/Low - * confidence badges. Display-only bucketing — not a gating threshold. - */ export const CONFIDENCE_DISPLAY_THRESHOLDS = { HIGH: 8, MEDIUM: 6, From 8025c1be533087351fe12108e9a7a3d623a1372d Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 15:02:21 +0200 Subject: [PATCH 11/14] refactor: rename forges/ to integrations/ (clearer than the "forge" jargon) src/forges/core/index.ts -> src/integrations/shared.ts; ForgeReviewIssue -> IntegrationReviewIssue. Matches the term the specs already use for the GitHub/GitLab integrations. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/github/github-integration.ts | 8 ++++++-- src/gitlab/gitlab-integration.ts | 8 ++++++-- src/{forges/core/index.ts => integrations/shared.ts} | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) rename src/{forges/core/index.ts => integrations/shared.ts} (92%) diff --git a/src/github/github-integration.ts b/src/github/github-integration.ts index 10b9588b..b6b3ccff 100644 --- a/src/github/github-integration.ts +++ b/src/github/github-integration.ts @@ -3,7 +3,11 @@ import { join } from 'path'; import { GitHubAPIClient } from './github-api-client'; import { GitHubChecksService } from './github-checks'; -import { QUALOPS_COMMENT_MARKER, type ForgeReviewIssue, type QualOpsResult } from '../forges/core'; +import { + QUALOPS_COMMENT_MARKER, + type IntegrationReviewIssue, + type QualOpsResult, +} from '../integrations/shared'; import { logger } from '../shared/utils/logger'; interface GitHubConfig { @@ -212,7 +216,7 @@ export class GitHubIntegration { try { const reviewSummary = JSON.parse(readFileSync(reviewSummaryPath, 'utf8')); const summary = reviewSummary.summary || {}; - const issues: ForgeReviewIssue[] = reviewSummary.issues || []; + const issues: IntegrationReviewIssue[] = reviewSummary.issues || []; return { summary: { diff --git a/src/gitlab/gitlab-integration.ts b/src/gitlab/gitlab-integration.ts index 98aa3628..70609f54 100644 --- a/src/gitlab/gitlab-integration.ts +++ b/src/gitlab/gitlab-integration.ts @@ -3,7 +3,11 @@ import { existsSync, readdirSync, readFileSync } from 'fs'; import { join } from 'path'; -import { QUALOPS_COMMENT_MARKER, type ForgeReviewIssue, type QualOpsResult } from '../forges/core'; +import { + QUALOPS_COMMENT_MARKER, + type IntegrationReviewIssue, + type QualOpsResult, +} from '../integrations/shared'; import { logger } from '../shared/utils/logger'; import { hasValidUrlScheme, isValidGitSha } from '../shared/utils/security'; @@ -569,7 +573,7 @@ class GitLabIntegration { if (reviewReport.issues && Array.isArray(reviewReport.issues)) { // Transform issues to match expected format - const transformedIssues = reviewReport.issues.map((issue: ForgeReviewIssue) => { + 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/forges/core/index.ts b/src/integrations/shared.ts similarity index 92% rename from src/forges/core/index.ts rename to src/integrations/shared.ts index 803006cf..f9a80ce6 100644 --- a/src/forges/core/index.ts +++ b/src/integrations/shared.ts @@ -19,7 +19,7 @@ export interface QualOpsResult { }>; } -export interface ForgeReviewIssue { +export interface IntegrationReviewIssue { file: string; location: string; severity: string; From 34cc19e9afc4401c7220111e14f9c5a4ff23798a Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 15:08:51 +0200 Subject: [PATCH 12/14] refactor: move shared integration contracts to src/shared/integrations.ts Shared code belongs under shared/; the top-level integrations/ name is reserved for the later migration of the github/ and gitlab/ folders. No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/github/github-integration.ts | 2 +- src/gitlab/gitlab-integration.ts | 2 +- src/{integrations/shared.ts => shared/integrations.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{integrations/shared.ts => shared/integrations.ts} (100%) diff --git a/src/github/github-integration.ts b/src/github/github-integration.ts index b6b3ccff..ccb9bbd6 100644 --- a/src/github/github-integration.ts +++ b/src/github/github-integration.ts @@ -7,7 +7,7 @@ import { QUALOPS_COMMENT_MARKER, type IntegrationReviewIssue, type QualOpsResult, -} from '../integrations/shared'; +} from '../shared/integrations'; import { logger } from '../shared/utils/logger'; interface GitHubConfig { diff --git a/src/gitlab/gitlab-integration.ts b/src/gitlab/gitlab-integration.ts index 70609f54..5e7f8271 100644 --- a/src/gitlab/gitlab-integration.ts +++ b/src/gitlab/gitlab-integration.ts @@ -7,7 +7,7 @@ import { QUALOPS_COMMENT_MARKER, type IntegrationReviewIssue, type QualOpsResult, -} from '../integrations/shared'; +} from '../shared/integrations'; import { logger } from '../shared/utils/logger'; import { hasValidUrlScheme, isValidGitSha } from '../shared/utils/security'; diff --git a/src/integrations/shared.ts b/src/shared/integrations.ts similarity index 100% rename from src/integrations/shared.ts rename to src/shared/integrations.ts From 41f83a42d63dfdda3d0d8d66fc57df402d41a6c2 Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 15:10:19 +0200 Subject: [PATCH 13/14] refactor: move shared integration contracts to src/shared/types/integrations.ts No behavior change: build clean, 2619/2619 tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 --- src/github/github-integration.ts | 2 +- src/gitlab/gitlab-integration.ts | 2 +- src/shared/{ => types}/integrations.ts | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/shared/{ => types}/integrations.ts (100%) diff --git a/src/github/github-integration.ts b/src/github/github-integration.ts index ccb9bbd6..46aeb46c 100644 --- a/src/github/github-integration.ts +++ b/src/github/github-integration.ts @@ -7,7 +7,7 @@ import { QUALOPS_COMMENT_MARKER, type IntegrationReviewIssue, type QualOpsResult, -} from '../shared/integrations'; +} from '../shared/types/integrations'; import { logger } from '../shared/utils/logger'; interface GitHubConfig { diff --git a/src/gitlab/gitlab-integration.ts b/src/gitlab/gitlab-integration.ts index 5e7f8271..edefdb75 100644 --- a/src/gitlab/gitlab-integration.ts +++ b/src/gitlab/gitlab-integration.ts @@ -7,7 +7,7 @@ import { QUALOPS_COMMENT_MARKER, type IntegrationReviewIssue, type QualOpsResult, -} from '../shared/integrations'; +} from '../shared/types/integrations'; import { logger } from '../shared/utils/logger'; import { hasValidUrlScheme, isValidGitSha } from '../shared/utils/security'; diff --git a/src/shared/integrations.ts b/src/shared/types/integrations.ts similarity index 100% rename from src/shared/integrations.ts rename to src/shared/types/integrations.ts From ec69732cd4ef33abca28b6055b1dc25a5160881e Mon Sep 17 00:00:00 2001 From: Sebastian Wessel Date: Thu, 9 Jul 2026 15:12:19 +0200 Subject: [PATCH 14/14] docs(changelog): note the structure/dedup refactor + trimmed public types Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b296bd83..9cfa9ba3 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.