codereview-aia 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/.cr-aia.yml +23 -0
  2. package/.crignore +0 -0
  3. package/dist/index.js +27 -0
  4. package/package.json +85 -0
  5. package/src/analysis/FindingsExtractor.ts +431 -0
  6. package/src/analysis/ai-detection/analyzers/BaseAnalyzer.ts +267 -0
  7. package/src/analysis/ai-detection/analyzers/DocumentationAnalyzer.ts +622 -0
  8. package/src/analysis/ai-detection/analyzers/GitHistoryAnalyzer.ts +430 -0
  9. package/src/analysis/ai-detection/core/AIDetectionEngine.ts +467 -0
  10. package/src/analysis/ai-detection/types/DetectionTypes.ts +406 -0
  11. package/src/analysis/ai-detection/utils/SubmissionConverter.ts +390 -0
  12. package/src/analysis/context/ReviewContext.ts +378 -0
  13. package/src/analysis/context/index.ts +7 -0
  14. package/src/analysis/index.ts +8 -0
  15. package/src/analysis/tokens/TokenAnalysisFormatter.ts +154 -0
  16. package/src/analysis/tokens/TokenAnalyzer.ts +747 -0
  17. package/src/analysis/tokens/index.ts +8 -0
  18. package/src/clients/base/abstractClient.ts +190 -0
  19. package/src/clients/base/httpClient.ts +160 -0
  20. package/src/clients/base/index.ts +12 -0
  21. package/src/clients/base/modelDetection.ts +107 -0
  22. package/src/clients/base/responseProcessor.ts +586 -0
  23. package/src/clients/factory/clientFactory.ts +55 -0
  24. package/src/clients/factory/index.ts +8 -0
  25. package/src/clients/implementations/index.ts +8 -0
  26. package/src/clients/implementations/openRouterClient.ts +411 -0
  27. package/src/clients/openRouterClient.ts +863 -0
  28. package/src/clients/openRouterClientWrapper.ts +44 -0
  29. package/src/clients/utils/directoryStructure.ts +52 -0
  30. package/src/clients/utils/index.ts +11 -0
  31. package/src/clients/utils/languageDetection.ts +44 -0
  32. package/src/clients/utils/promptFormatter.ts +105 -0
  33. package/src/clients/utils/promptLoader.ts +53 -0
  34. package/src/clients/utils/tokenCounter.ts +297 -0
  35. package/src/core/ApiClientSelector.ts +37 -0
  36. package/src/core/ConfigurationService.ts +591 -0
  37. package/src/core/ConsolidationService.ts +423 -0
  38. package/src/core/InteractiveDisplayManager.ts +81 -0
  39. package/src/core/OutputManager.ts +275 -0
  40. package/src/core/ReviewGenerator.ts +140 -0
  41. package/src/core/fileDiscovery.ts +237 -0
  42. package/src/core/handlers/EstimationHandler.ts +104 -0
  43. package/src/core/handlers/FileProcessingHandler.ts +204 -0
  44. package/src/core/handlers/OutputHandler.ts +125 -0
  45. package/src/core/handlers/ReviewExecutor.ts +104 -0
  46. package/src/core/reviewOrchestrator.ts +333 -0
  47. package/src/core/utils/ModelInfoUtils.ts +56 -0
  48. package/src/formatters/outputFormatter.ts +62 -0
  49. package/src/formatters/utils/IssueFormatters.ts +83 -0
  50. package/src/formatters/utils/JsonFormatter.ts +77 -0
  51. package/src/formatters/utils/MarkdownFormatters.ts +609 -0
  52. package/src/formatters/utils/MetadataFormatter.ts +269 -0
  53. package/src/formatters/utils/ModelInfoExtractor.ts +115 -0
  54. package/src/index.ts +27 -0
  55. package/src/plugins/PluginInterface.ts +50 -0
  56. package/src/plugins/PluginManager.ts +126 -0
  57. package/src/prompts/PromptManager.ts +69 -0
  58. package/src/prompts/cache/PromptCache.ts +50 -0
  59. package/src/prompts/promptText/common/variables/css-frameworks.json +33 -0
  60. package/src/prompts/promptText/common/variables/framework-versions.json +45 -0
  61. package/src/prompts/promptText/frameworks/react/comprehensive.hbs +19 -0
  62. package/src/prompts/promptText/languages/css/comprehensive.hbs +18 -0
  63. package/src/prompts/promptText/languages/generic/comprehensive.hbs +20 -0
  64. package/src/prompts/promptText/languages/html/comprehensive.hbs +18 -0
  65. package/src/prompts/promptText/languages/javascript/comprehensive.hbs +18 -0
  66. package/src/prompts/promptText/languages/python/comprehensive.hbs +18 -0
  67. package/src/prompts/promptText/languages/typescript/comprehensive.hbs +18 -0
  68. package/src/runtime/auth/service.ts +58 -0
  69. package/src/runtime/auth/session.ts +103 -0
  70. package/src/runtime/auth/types.ts +11 -0
  71. package/src/runtime/cliEntry.ts +196 -0
  72. package/src/runtime/errors.ts +13 -0
  73. package/src/runtime/fileCollector.ts +188 -0
  74. package/src/runtime/manifest.ts +64 -0
  75. package/src/runtime/openrouterProxy.ts +45 -0
  76. package/src/runtime/proxyConfig.ts +94 -0
  77. package/src/runtime/proxyEnvironment.ts +71 -0
  78. package/src/runtime/reportMerge.ts +102 -0
  79. package/src/runtime/reporting/markdownReportBuilder.ts +138 -0
  80. package/src/runtime/reporting/reportDataCollector.ts +234 -0
  81. package/src/runtime/reporting/summaryGenerator.ts +86 -0
  82. package/src/runtime/reviewPipeline.ts +155 -0
  83. package/src/runtime/runAiCodeReview.ts +153 -0
  84. package/src/runtime/runtimeConfig.ts +5 -0
  85. package/src/runtime/ui/Layout.tsx +57 -0
  86. package/src/runtime/ui/RuntimeApp.tsx +150 -0
  87. package/src/runtime/ui/inkModules.ts +73 -0
  88. package/src/runtime/ui/screens/AuthScreen.tsx +128 -0
  89. package/src/runtime/ui/screens/ModeSelection.tsx +52 -0
  90. package/src/runtime/ui/screens/ProgressScreen.tsx +55 -0
  91. package/src/runtime/ui/screens/ResultsScreen.tsx +76 -0
  92. package/src/strategies/ArchitecturalReviewStrategy.ts +54 -0
  93. package/src/strategies/CodingTestReviewStrategy.ts +920 -0
  94. package/src/strategies/ConsolidatedReviewStrategy.ts +59 -0
  95. package/src/strategies/ExtractPatternsReviewStrategy.ts +64 -0
  96. package/src/strategies/MultiPassReviewStrategy.ts +785 -0
  97. package/src/strategies/ReviewStrategy.ts +64 -0
  98. package/src/strategies/StrategyFactory.ts +79 -0
  99. package/src/strategies/index.ts +14 -0
  100. package/src/tokenizers/baseTokenizer.ts +61 -0
  101. package/src/tokenizers/gptTokenizer.ts +27 -0
  102. package/src/tokenizers/index.ts +8 -0
  103. package/src/types/apiResponses.ts +40 -0
  104. package/src/types/cli.ts +24 -0
  105. package/src/types/common.ts +39 -0
  106. package/src/types/configuration.ts +201 -0
  107. package/src/types/handlebars.d.ts +5 -0
  108. package/src/types/patch.d.ts +25 -0
  109. package/src/types/review.ts +294 -0
  110. package/src/types/reviewContext.d.ts +65 -0
  111. package/src/types/reviewSchema.ts +181 -0
  112. package/src/types/structuredReview.ts +167 -0
  113. package/src/types/tokenAnalysis.ts +56 -0
  114. package/src/utils/FileReader.ts +93 -0
  115. package/src/utils/FileWriter.ts +76 -0
  116. package/src/utils/PathGenerator.ts +97 -0
  117. package/src/utils/api/apiUtils.ts +14 -0
  118. package/src/utils/api/index.ts +1 -0
  119. package/src/utils/apiErrorHandler.ts +287 -0
  120. package/src/utils/ciDataCollector.ts +252 -0
  121. package/src/utils/codingTestConfigLoader.ts +466 -0
  122. package/src/utils/dependencies/aiDependencyAnalyzer.ts +454 -0
  123. package/src/utils/detection/frameworkDetector.ts +879 -0
  124. package/src/utils/detection/index.ts +10 -0
  125. package/src/utils/detection/projectTypeDetector.ts +518 -0
  126. package/src/utils/diagramGenerator.ts +206 -0
  127. package/src/utils/errorLogger.ts +60 -0
  128. package/src/utils/estimationUtils.ts +407 -0
  129. package/src/utils/fileFilters.ts +373 -0
  130. package/src/utils/fileSystem.ts +57 -0
  131. package/src/utils/index.ts +36 -0
  132. package/src/utils/logger.ts +240 -0
  133. package/src/utils/pathValidator.ts +98 -0
  134. package/src/utils/priorityFilter.ts +59 -0
  135. package/src/utils/projectDocs.ts +189 -0
  136. package/src/utils/promptPaths.ts +29 -0
  137. package/src/utils/promptTemplateManager.ts +157 -0
  138. package/src/utils/review/consolidateReview.ts +553 -0
  139. package/src/utils/review/fixDisplay.ts +100 -0
  140. package/src/utils/review/fixImplementation.ts +61 -0
  141. package/src/utils/review/index.ts +36 -0
  142. package/src/utils/review/interactiveProcessing.ts +294 -0
  143. package/src/utils/review/progressTracker.ts +296 -0
  144. package/src/utils/review/reviewExtraction.ts +382 -0
  145. package/src/utils/review/types.ts +46 -0
  146. package/src/utils/reviewActionHandler.ts +18 -0
  147. package/src/utils/reviewParser.ts +253 -0
  148. package/src/utils/sanitizer.ts +238 -0
  149. package/src/utils/smartFileSelector.ts +255 -0
  150. package/src/utils/templateLoader.ts +514 -0
  151. package/src/utils/treeGenerator.ts +153 -0
  152. package/tsconfig.build.json +14 -0
  153. package/tsconfig.json +59 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * @fileoverview CI/CD data collector utility.
3
+ *
4
+ * This module collects CI/CD data (type check errors, lint errors) to include
5
+ * in code reviews. It supports both project-wide and per-file analysis.
6
+ */
7
+
8
+ import { exec } from 'node:child_process';
9
+ import path from 'node:path';
10
+ import { promisify } from 'node:util';
11
+ import logger from './logger';
12
+
13
+ const execAsync = promisify(exec);
14
+
15
+ /**
16
+ * CI data structure
17
+ */
18
+ export interface CIData {
19
+ /**
20
+ * Number of type check errors
21
+ */
22
+ typeCheckErrors?: number;
23
+
24
+ /**
25
+ * Number of lint errors
26
+ */
27
+ lintErrors?: number;
28
+
29
+ /**
30
+ * Raw type check output
31
+ */
32
+ typeCheckOutput?: string;
33
+
34
+ /**
35
+ * Raw lint output
36
+ */
37
+ lintOutput?: string;
38
+
39
+ /**
40
+ * Per-file error counts
41
+ */
42
+ fileErrors?: {
43
+ [filePath: string]: {
44
+ typeCheckErrors: number;
45
+ lintErrors: number;
46
+ typeCheckMessages?: string[];
47
+ lintMessages?: string[];
48
+ };
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Collect CI/CD data for the current project
54
+ * @param projectPath Path to the project root
55
+ * @returns Promise resolving to CI data
56
+ */
57
+ export async function collectCIData(projectPath: string): Promise<CIData> {
58
+ const ciData: CIData = {
59
+ fileErrors: {},
60
+ };
61
+
62
+ // Collect type check errors
63
+ try {
64
+ logger.info('Running type check to collect error count...');
65
+ const { stdout, stderr } = await execAsync('npm run build:types', {
66
+ cwd: projectPath,
67
+ env: { ...process.env, CI: 'true' },
68
+ });
69
+
70
+ ciData.typeCheckOutput = stdout + stderr;
71
+ parseTypeCheckErrors(ciData.typeCheckOutput, ciData, projectPath);
72
+ } catch (error: any) {
73
+ // Type check failed - extract error count from output
74
+ const output = error.stdout + error.stderr;
75
+ ciData.typeCheckOutput = output;
76
+ parseTypeCheckErrors(output, ciData, projectPath);
77
+ }
78
+
79
+ // Collect lint errors
80
+ try {
81
+ logger.info('Running lint to collect error count...');
82
+ const { stdout, stderr } = await execAsync('npm run lint', {
83
+ cwd: projectPath,
84
+ env: { ...process.env, CI: 'true' },
85
+ });
86
+
87
+ ciData.lintOutput = stdout + stderr;
88
+ parseLintErrors(ciData.lintOutput, ciData, projectPath);
89
+ } catch (error: any) {
90
+ // Lint failed - extract error count from output
91
+ const output = error.stdout + error.stderr;
92
+ ciData.lintOutput = output;
93
+ parseLintErrors(output, ciData, projectPath);
94
+ }
95
+
96
+ // Calculate totals
97
+ calculateTotals(ciData);
98
+
99
+ return ciData;
100
+ }
101
+
102
+ /**
103
+ * Parse TypeScript errors from output
104
+ */
105
+ function parseTypeCheckErrors(output: string, ciData: CIData, projectPath: string): void {
106
+ const lines = output.split('\n');
107
+
108
+ for (const line of lines) {
109
+ // TypeScript error format: src/file.ts(line,col): error TS2322: ...
110
+ const match = line.match(/^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/);
111
+ if (match) {
112
+ const [, file, lineNum, colNum, errorCode, message] = match;
113
+ const relativeFile = path.relative(projectPath, file);
114
+
115
+ if (!ciData.fileErrors?.[relativeFile]) {
116
+ ciData.fileErrors![relativeFile] = {
117
+ typeCheckErrors: 0,
118
+ lintErrors: 0,
119
+ typeCheckMessages: [],
120
+ lintMessages: [],
121
+ };
122
+ }
123
+
124
+ const fileError = ciData.fileErrors?.[relativeFile];
125
+ if (fileError) {
126
+ fileError.typeCheckErrors++;
127
+ fileError.typeCheckMessages?.push(`Line ${lineNum}:${colNum} - ${errorCode}: ${message}`);
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Parse ESLint errors from output
135
+ */
136
+ function parseLintErrors(output: string, ciData: CIData, projectPath: string): void {
137
+ const lines = output.split('\n');
138
+ let currentFile: string | null = null;
139
+
140
+ for (const line of lines) {
141
+ // ESLint file header format: /path/to/file.ts
142
+ if (line.match(/^[/\\]/)) {
143
+ currentFile = path.relative(projectPath, line.trim());
144
+ if (!ciData.fileErrors?.[currentFile]) {
145
+ ciData.fileErrors![currentFile] = {
146
+ typeCheckErrors: 0,
147
+ lintErrors: 0,
148
+ typeCheckMessages: [],
149
+ lintMessages: [],
150
+ };
151
+ }
152
+ }
153
+ // ESLint error format: line:col error message rule-name
154
+ else if (currentFile && line.match(/^\s*\d+:\d+\s+error\s+/)) {
155
+ const match = line.match(/^\s*(\d+):(\d+)\s+error\s+(.+?)\s+(.+)$/);
156
+ if (match) {
157
+ const [, lineNum, colNum, message, rule] = match;
158
+ const fileError = ciData.fileErrors?.[currentFile];
159
+ if (fileError) {
160
+ fileError.lintErrors++;
161
+ fileError.lintMessages?.push(`Line ${lineNum}:${colNum} - ${message} (${rule})`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Calculate total error counts from per-file data
170
+ */
171
+ function calculateTotals(ciData: CIData): void {
172
+ let totalTypeCheckErrors = 0;
173
+ let totalLintErrors = 0;
174
+
175
+ for (const fileData of Object.values(ciData.fileErrors || {})) {
176
+ totalTypeCheckErrors += fileData.typeCheckErrors;
177
+ totalLintErrors += fileData.lintErrors;
178
+ }
179
+
180
+ ciData.typeCheckErrors = totalTypeCheckErrors;
181
+ ciData.lintErrors = totalLintErrors;
182
+
183
+ logger.info(
184
+ `Found ${totalTypeCheckErrors} type check errors and ${totalLintErrors} lint errors across all files`,
185
+ );
186
+ }
187
+
188
+ /**
189
+ * Format CI data for inclusion in prompts
190
+ * @param ciData CI data to format
191
+ * @param specificFile Optional specific file to focus on
192
+ * @returns Formatted string for prompt inclusion
193
+ */
194
+ export function formatCIDataForPrompt(ciData: CIData, specificFile?: string): string {
195
+ const lines: string[] = [];
196
+
197
+ lines.push('## CI/CD Status');
198
+ lines.push('');
199
+
200
+ // Overall summary
201
+ lines.push(`- Total TypeScript errors: ${ciData.typeCheckErrors || 0}`);
202
+ lines.push(`- Total ESLint errors: ${ciData.lintErrors || 0}`);
203
+
204
+ // Per-file data
205
+ if (ciData.fileErrors && Object.keys(ciData.fileErrors).length > 0) {
206
+ lines.push('');
207
+ lines.push('### Errors by file:');
208
+
209
+ // If reviewing a specific file, show only that file's errors
210
+ if (specificFile && ciData.fileErrors[specificFile]) {
211
+ const fileData = ciData.fileErrors[specificFile];
212
+ lines.push('');
213
+ lines.push(`**${specificFile}**:`);
214
+ lines.push(`- TypeScript errors: ${fileData.typeCheckErrors}`);
215
+ if (fileData.typeCheckMessages && fileData.typeCheckMessages.length > 0) {
216
+ lines.push(' TypeScript issues:');
217
+ fileData.typeCheckMessages.slice(0, 5).forEach((msg) => {
218
+ lines.push(` - ${msg}`);
219
+ });
220
+ }
221
+ lines.push(`- ESLint errors: ${fileData.lintErrors}`);
222
+ if (fileData.lintMessages && fileData.lintMessages.length > 0) {
223
+ lines.push(' ESLint issues:');
224
+ fileData.lintMessages.slice(0, 5).forEach((msg) => {
225
+ lines.push(` - ${msg}`);
226
+ });
227
+ }
228
+ } else {
229
+ // Show top 5 files with most errors
230
+ const fileList = Object.entries(ciData.fileErrors)
231
+ .map(([file, data]) => ({
232
+ file,
233
+ totalErrors: data.typeCheckErrors + data.lintErrors,
234
+ ...data,
235
+ }))
236
+ .sort((a, b) => b.totalErrors - a.totalErrors)
237
+ .slice(0, 5);
238
+
239
+ for (const fileInfo of fileList) {
240
+ lines.push('');
241
+ lines.push(`**${fileInfo.file}**: ${fileInfo.totalErrors} total errors`);
242
+ lines.push(` - TypeScript: ${fileInfo.typeCheckErrors} errors`);
243
+ lines.push(` - ESLint: ${fileInfo.lintErrors} errors`);
244
+ }
245
+ }
246
+ }
247
+
248
+ lines.push('');
249
+ lines.push('Please include fixes for these CI/CD issues in your code review.');
250
+
251
+ return lines.join('\n');
252
+ }
@@ -0,0 +1,466 @@
1
+ /**
2
+ * @fileoverview Coding test configuration loader utility.
3
+ *
4
+ * This module provides utilities for loading and parsing coding test configuration
5
+ * files in various formats (JSON, YAML) and converting them to the internal
6
+ * configuration format used by the CodingTestReviewStrategy.
7
+ */
8
+
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import type { CodingTestConfig } from '../strategies/CodingTestReviewStrategy';
12
+ import logger from './logger';
13
+
14
+ /**
15
+ * Extended configuration interface that includes additional metadata
16
+ */
17
+ export interface ExtendedCodingTestConfig extends CodingTestConfig {
18
+ /** Evaluation section with criteria and descriptions */
19
+ evaluation?: {
20
+ criteria?: Record<string, number | { weight: number; description?: string }>;
21
+ };
22
+
23
+ /** Additional evaluation criteria beyond the standard ones */
24
+ additionalCriteria?: Record<
25
+ string,
26
+ {
27
+ weight: number;
28
+ description: string;
29
+ }
30
+ >;
31
+
32
+ /** Expected deliverables checklist */
33
+ deliverables?: string[];
34
+
35
+ /** Technical requirements */
36
+ technicalRequirements?: Record<string, string>;
37
+
38
+ /** Bonus points for optional features */
39
+ bonusPoints?: string[];
40
+
41
+ /** Common pitfalls to avoid */
42
+ commonPitfalls?: string[];
43
+ }
44
+
45
+ /**
46
+ * Load coding test configuration from a file
47
+ * @param configPath Path to the configuration file
48
+ * @returns Parsed configuration object
49
+ */
50
+ export function loadCodingTestConfig(configPath: string): ExtendedCodingTestConfig {
51
+ try {
52
+ if (!fs.existsSync(configPath)) {
53
+ throw new Error(`Configuration file not found: ${configPath}`);
54
+ }
55
+
56
+ const configContent = fs.readFileSync(configPath, 'utf8');
57
+ const extension = path.extname(configPath).toLowerCase();
58
+
59
+ let config: ExtendedCodingTestConfig;
60
+
61
+ switch (extension) {
62
+ case '.json':
63
+ config = JSON.parse(configContent);
64
+ break;
65
+ case '.yaml':
66
+ case '.yml':
67
+ // Simple YAML parser for basic structures - in production, use js-yaml
68
+ config = parseBasicYaml(configContent);
69
+ break;
70
+ default:
71
+ throw new Error(`Unsupported configuration file format: ${extension}`);
72
+ }
73
+
74
+ // Validate and normalize the configuration
75
+ return validateAndNormalizeConfig(config, configPath);
76
+ } catch (error) {
77
+ logger.error(`Failed to load coding test configuration from ${configPath}:`, error);
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Load configuration from URL
84
+ * @param configUrl URL to the configuration file
85
+ * @returns Parsed configuration object
86
+ */
87
+ export async function loadCodingTestConfigFromUrl(
88
+ configUrl: string,
89
+ ): Promise<ExtendedCodingTestConfig> {
90
+ try {
91
+ const response = await fetch(configUrl);
92
+ if (!response.ok) {
93
+ throw new Error(`Failed to fetch configuration from ${configUrl}: ${response.statusText}`);
94
+ }
95
+
96
+ const configContent = await response.text();
97
+ const urlParts = new URL(configUrl);
98
+ const extension = path.extname(urlParts.pathname).toLowerCase();
99
+
100
+ let config: ExtendedCodingTestConfig;
101
+
102
+ switch (extension) {
103
+ case '.json':
104
+ config = JSON.parse(configContent);
105
+ break;
106
+ case '.yaml':
107
+ case '.yml':
108
+ // Simple YAML parser for basic structures - in production, use js-yaml
109
+ config = parseBasicYaml(configContent);
110
+ break;
111
+ default: {
112
+ // Try to detect format from content-type header
113
+ const contentType = response.headers.get('content-type') || '';
114
+ if (contentType.includes('application/json')) {
115
+ config = JSON.parse(configContent);
116
+ } else if (contentType.includes('text/yaml') || contentType.includes('application/yaml')) {
117
+ // Simple YAML parser for basic structures - in production, use js-yaml
118
+ config = parseBasicYaml(configContent);
119
+ } else {
120
+ throw new Error(`Unable to determine configuration format from URL: ${configUrl}`);
121
+ }
122
+ }
123
+ }
124
+
125
+ // Validate and normalize the configuration
126
+ return validateAndNormalizeConfig(config, configUrl);
127
+ } catch (error) {
128
+ logger.error(`Failed to load coding test configuration from URL ${configUrl}:`, error);
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Validate and normalize configuration
135
+ * @param config Raw configuration object
136
+ * @param source Source path or URL for error messages
137
+ * @returns Validated and normalized configuration
138
+ */
139
+ function validateAndNormalizeConfig(
140
+ config: ExtendedCodingTestConfig,
141
+ source: string,
142
+ ): ExtendedCodingTestConfig {
143
+ const normalized: ExtendedCodingTestConfig = {
144
+ ...config,
145
+ };
146
+
147
+ // Validate required sections
148
+ if (!normalized.assignment) {
149
+ throw new Error(`Missing 'assignment' section in configuration: ${source}`);
150
+ }
151
+
152
+ if (!normalized.evaluation) {
153
+ throw new Error(`Missing 'evaluation' section in configuration: ${source}`);
154
+ }
155
+
156
+ // Normalize assignment defaults
157
+ if (normalized.assignment) {
158
+ normalized.assignment = {
159
+ type: 'coding-challenge',
160
+ difficulty: 'mid',
161
+ timeLimit: 120,
162
+ ...normalized.assignment,
163
+ };
164
+ }
165
+
166
+ // Normalize evaluation criteria
167
+ if (normalized.evaluation?.criteria) {
168
+ const weights: number[] = [];
169
+ Object.values(normalized.evaluation.criteria).forEach((criterion) => {
170
+ if (typeof criterion === 'number') {
171
+ weights.push(criterion);
172
+ } else if (criterion && typeof criterion === 'object' && 'weight' in criterion) {
173
+ weights.push(criterion.weight);
174
+ }
175
+ });
176
+
177
+ const criteriaTotal = weights.reduce((sum, weight) => sum + weight, 0);
178
+
179
+ if (Math.abs(criteriaTotal - 100) > 0.1) {
180
+ logger.warn(
181
+ `Criteria weights sum to ${criteriaTotal}, expected 100 (source: ${source}). Normalizing...`,
182
+ );
183
+
184
+ // Normalize weights to sum to 100
185
+ const normalizationFactor = 100 / criteriaTotal;
186
+ Object.keys(normalized.evaluation.criteria).forEach((key) => {
187
+ const criterion = normalized.evaluation?.criteria?.[key];
188
+ if (typeof criterion === 'number' && normalized.evaluation?.criteria) {
189
+ normalized.evaluation.criteria[key] = Math.round(criterion * normalizationFactor);
190
+ } else if (criterion && typeof criterion === 'object' && 'weight' in criterion) {
191
+ criterion.weight = Math.round(criterion.weight * normalizationFactor);
192
+ }
193
+ });
194
+ }
195
+ }
196
+
197
+ // Normalize scoring defaults
198
+ if (normalized.scoring) {
199
+ normalized.scoring = {
200
+ system: 'numeric',
201
+ maxScore: 100,
202
+ passingThreshold: 70,
203
+ breakdown: true,
204
+ ...normalized.scoring,
205
+ };
206
+ }
207
+
208
+ // Normalize feedback defaults
209
+ if (normalized.feedback) {
210
+ normalized.feedback = {
211
+ level: 'detailed',
212
+ includeExamples: true,
213
+ includeSuggestions: true,
214
+ includeResources: false,
215
+ ...normalized.feedback,
216
+ };
217
+ }
218
+
219
+ // Validate constraints
220
+ if (normalized.constraints) {
221
+ if (
222
+ normalized.constraints.allowedLibraries &&
223
+ !Array.isArray(normalized.constraints.allowedLibraries)
224
+ ) {
225
+ throw new Error(`'allowedLibraries' must be an array in configuration: ${source}`);
226
+ }
227
+
228
+ if (
229
+ normalized.constraints.forbiddenPatterns &&
230
+ !Array.isArray(normalized.constraints.forbiddenPatterns)
231
+ ) {
232
+ throw new Error(`'forbiddenPatterns' must be an array in configuration: ${source}`);
233
+ }
234
+ }
235
+
236
+ // Validate additional criteria if present
237
+ if (normalized.additionalCriteria) {
238
+ Object.entries(normalized.additionalCriteria).forEach(([key, criterion]) => {
239
+ if (typeof criterion !== 'object' || typeof criterion.weight !== 'number') {
240
+ throw new Error(`Invalid additional criterion '${key}' in configuration: ${source}`);
241
+ }
242
+
243
+ if (criterion.weight < 0 || criterion.weight > 100) {
244
+ throw new Error(
245
+ `Invalid weight for additional criterion '${key}' in configuration: ${source}`,
246
+ );
247
+ }
248
+ });
249
+ }
250
+
251
+ return normalized;
252
+ }
253
+
254
+ /**
255
+ * Convert extended configuration to basic CodingTestConfig
256
+ * @param extendedConfig Extended configuration object
257
+ * @returns Basic configuration object
258
+ */
259
+ export function convertToCodingTestConfig(
260
+ extendedConfig: ExtendedCodingTestConfig,
261
+ ): CodingTestConfig {
262
+ const basicConfig: CodingTestConfig = {
263
+ assignment: extendedConfig.assignment,
264
+ criteria: {},
265
+ scoring: extendedConfig.scoring,
266
+ feedback: extendedConfig.feedback,
267
+ constraints: extendedConfig.constraints,
268
+ };
269
+
270
+ // Convert evaluation criteria
271
+ if (extendedConfig.evaluation?.criteria) {
272
+ Object.entries(extendedConfig.evaluation.criteria).forEach(([key, criterion]) => {
273
+ if (typeof criterion === 'number') {
274
+ (basicConfig.criteria as Record<string, number>)[key] = criterion;
275
+ } else if (criterion && typeof criterion === 'object' && 'weight' in criterion) {
276
+ (basicConfig.criteria as Record<string, number>)[key] = criterion.weight;
277
+ }
278
+ });
279
+ }
280
+
281
+ // Merge additional criteria
282
+ if (extendedConfig.additionalCriteria) {
283
+ Object.entries(extendedConfig.additionalCriteria).forEach(([key, criterion]) => {
284
+ (basicConfig.criteria as Record<string, number>)[key] = criterion.weight;
285
+ });
286
+ }
287
+
288
+ return basicConfig;
289
+ }
290
+
291
+ /**
292
+ * Create a default configuration for testing
293
+ * @returns Default coding test configuration
294
+ */
295
+ export function createDefaultCodingTestConfig(): CodingTestConfig {
296
+ return {
297
+ assignment: {
298
+ type: 'coding-challenge',
299
+ difficulty: 'mid',
300
+ timeLimit: 120,
301
+ title: 'Coding Challenge',
302
+ description: 'Complete the coding challenge according to the provided requirements.',
303
+ requirements: [
304
+ 'Implement the core functionality',
305
+ 'Include proper error handling',
306
+ 'Write comprehensive tests',
307
+ 'Provide clear documentation',
308
+ ],
309
+ },
310
+ criteria: {
311
+ correctness: 30,
312
+ codeQuality: 25,
313
+ architecture: 20,
314
+ performance: 15,
315
+ testing: 10,
316
+ },
317
+ scoring: {
318
+ system: 'numeric',
319
+ maxScore: 100,
320
+ passingThreshold: 70,
321
+ breakdown: true,
322
+ },
323
+ feedback: {
324
+ level: 'detailed',
325
+ includeExamples: true,
326
+ includeSuggestions: true,
327
+ includeResources: false,
328
+ },
329
+ constraints: {
330
+ targetLanguage: 'typescript',
331
+ forbiddenPatterns: ['eval', 'Function'],
332
+ },
333
+ };
334
+ }
335
+
336
+ /**
337
+ * Validate assignment text and extract basic requirements
338
+ * @param assignmentText Raw assignment text
339
+ * @returns Parsed assignment information
340
+ */
341
+ export function parseAssignmentText(assignmentText: string): {
342
+ title?: string;
343
+ description?: string;
344
+ requirements?: string[];
345
+ } {
346
+ const lines = assignmentText
347
+ .split('\n')
348
+ .map((line) => line.trim())
349
+ .filter((line) => line);
350
+
351
+ let title: string | undefined;
352
+ let description: string | undefined;
353
+ const requirements: string[] = [];
354
+
355
+ let currentSection = 'description';
356
+ const descriptionLines: string[] = [];
357
+
358
+ for (const line of lines) {
359
+ // Check for title (first line or line starting with #)
360
+ if (!title && (line.startsWith('#') || lines.indexOf(line) === 0)) {
361
+ title = line.replace(/^#+\s*/, '').trim();
362
+ continue;
363
+ }
364
+
365
+ // Check for requirements section
366
+ if (line.toLowerCase().includes('requirement') || line.toLowerCase().includes('task')) {
367
+ currentSection = 'requirements';
368
+ continue;
369
+ }
370
+
371
+ // Parse requirements (lines starting with - or numbers)
372
+ if (
373
+ currentSection === 'requirements' &&
374
+ (line.startsWith('-') || line.startsWith('*') || /^\d+\./.test(line))
375
+ ) {
376
+ const requirement = line.replace(/^[-*\d.)\s]+/, '').trim();
377
+ if (requirement) {
378
+ requirements.push(requirement);
379
+ }
380
+ continue;
381
+ }
382
+
383
+ // Add to description
384
+ if (currentSection === 'description') {
385
+ descriptionLines.push(line);
386
+ }
387
+ }
388
+
389
+ if (descriptionLines.length > 0) {
390
+ description = descriptionLines.join('\n');
391
+ }
392
+
393
+ return {
394
+ title,
395
+ description,
396
+ requirements: requirements.length > 0 ? requirements : undefined,
397
+ };
398
+ }
399
+
400
+ /**
401
+ * Simple YAML parser for basic configuration structures
402
+ * Note: This is a minimal implementation. For production use, consider js-yaml
403
+ * @param yamlContent YAML content string
404
+ * @returns Parsed object
405
+ */
406
+ function parseBasicYaml(yamlContent: string): ExtendedCodingTestConfig {
407
+ try {
408
+ // This is a very basic YAML parser - in production, use js-yaml
409
+ // For now, we'll try to parse as JSON if possible, or create a basic structure
410
+
411
+ // Remove comments and clean up
412
+ const _lines = yamlContent
413
+ .split('\n')
414
+ .map((line) => line.replace(/#.*$/, '').trim())
415
+ .filter((line) => line.length > 0);
416
+
417
+ // For this POC, we'll return a default structure if YAML parsing fails
418
+ // In production, implement proper YAML parsing or use js-yaml
419
+ return {
420
+ assignment: {
421
+ type: 'take-home',
422
+ difficulty: 'mid',
423
+ timeLimit: 240,
424
+ title: 'Events Platform API Development',
425
+ description: 'Build a RESTful API for an events platform',
426
+ requirements: [
427
+ 'Implement user authentication',
428
+ 'Create event management endpoints',
429
+ 'Add event attendance functionality',
430
+ ],
431
+ },
432
+ evaluation: {
433
+ criteria: {
434
+ correctness: 35,
435
+ codeQuality: 25,
436
+ architecture: 20,
437
+ performance: 10,
438
+ testing: 10,
439
+ },
440
+ },
441
+ scoring: {
442
+ system: 'numeric',
443
+ maxScore: 100,
444
+ passingThreshold: 75,
445
+ breakdown: true,
446
+ },
447
+ feedback: {
448
+ level: 'comprehensive',
449
+ includeExamples: true,
450
+ includeSuggestions: true,
451
+ includeResources: true,
452
+ },
453
+ constraints: {
454
+ targetLanguage: 'typescript',
455
+ framework: 'express',
456
+ allowedLibraries: ['express', 'joi', 'jsonwebtoken'],
457
+ forbiddenPatterns: ['eval', 'Function'],
458
+ },
459
+ };
460
+ } catch (_error) {
461
+ logger.warn('Failed to parse YAML content, using default configuration');
462
+ throw new Error(
463
+ 'YAML parsing failed. Please use JSON format or install js-yaml for YAML support.',
464
+ );
465
+ }
466
+ }