@arela/uploader 1.1.3 → 1.2.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 (99) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/document-type-shared.js +1 -1
  92. package/src/document-types/pedimento-completo.js +8 -1
  93. package/src/file-detection.js +9 -0
  94. package/src/index.js +55 -0
  95. package/src/scoring/scoring-engine.js +1 -1
  96. package/src/services/LoggingService.js +35 -0
  97. package/src/services/PipelineApiService.js +7 -1
  98. package/src/services/PipelineJobRunner.js +389 -0
  99. package/src/services/ProfileManager.js +164 -0
@@ -0,0 +1,164 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ export const DEFAULT_PROFILES_PATH = '~/.arela/profiles.json';
6
+ export const DEFAULT_AGENT_LOG_PATH = '~/.arela/arela-agent.log';
7
+ const DEFAULT_POLL_INTERVAL_MS = 5000;
8
+
9
+ /**
10
+ * Expand a leading '~' to the user's home directory
11
+ * @param {string} inputPath
12
+ * @returns {string}
13
+ */
14
+ export function expandHome(inputPath) {
15
+ if (inputPath && inputPath.startsWith('~')) {
16
+ return path.join(os.homedir(), inputPath.slice(1));
17
+ }
18
+ return inputPath;
19
+ }
20
+
21
+ /**
22
+ * Profile Manager
23
+ * Loads and validates the agent's profiles.json — the only local configuration
24
+ * the multi-profile agent needs: one machine serverId plus {name, url, token}
25
+ * per tenant API. Everything else (scan directories, tuning, schedule) lives
26
+ * server-side in pipeline_config.
27
+ *
28
+ * Minimal schema:
29
+ * {
30
+ * "version": 1,
31
+ * "serverId": "PALCO-01",
32
+ * "pollIntervalMs": 5000, // optional
33
+ * "logFile": "~/.arela/arela-agent.log", // optional
34
+ * "profiles": [
35
+ * { "name": "ktj", "url": "https://…", "token": "ak_…" },
36
+ * {
37
+ * "name": "cem", "url": "https://…", "token": "ak_…",
38
+ * "targets": { "agencia": { "baseUrl": "https://…", "token": "ak_…" } },
39
+ * "env": { "SCAN_DIRECTORY_LEVEL": "2" }
40
+ * }
41
+ * ]
42
+ * }
43
+ */
44
+ export class ProfileManager {
45
+ /**
46
+ * Load, validate and normalize the profiles file.
47
+ * @param {string} [configPath] - Path to profiles.json (supports '~')
48
+ * @returns {{serverId: string, pollIntervalMs: number, logFile: string, profiles: Object[]}}
49
+ */
50
+ static loadProfiles(configPath = DEFAULT_PROFILES_PATH) {
51
+ const resolvedPath = path.resolve(expandHome(configPath));
52
+
53
+ if (!fs.existsSync(resolvedPath)) {
54
+ throw new Error(
55
+ `Profiles file not found: ${resolvedPath}. ` +
56
+ `Run 'arela agent init --from <dir...>' to generate one from existing .env folders.`,
57
+ );
58
+ }
59
+
60
+ let raw;
61
+ try {
62
+ raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
63
+ } catch (error) {
64
+ throw new Error(`Invalid JSON in ${resolvedPath}: ${error.message}`);
65
+ }
66
+
67
+ if (!raw.serverId || typeof raw.serverId !== 'string') {
68
+ throw new Error(
69
+ `${resolvedPath}: 'serverId' is required at the root level (one per machine, e.g. "PALCO-01")`,
70
+ );
71
+ }
72
+
73
+ if (!Array.isArray(raw.profiles) || raw.profiles.length === 0) {
74
+ throw new Error(`${resolvedPath}: 'profiles' must be a non-empty array`);
75
+ }
76
+
77
+ const seenNames = new Set();
78
+ const profiles = [];
79
+
80
+ raw.profiles.forEach((profile, index) => {
81
+ const label = `Profile ${profile?.name ? `'${profile.name}'` : `#${index + 1}`}`;
82
+
83
+ if (!profile.name || typeof profile.name !== 'string') {
84
+ throw new Error(`Profile #${index + 1}: 'name' is required`);
85
+ }
86
+ const name = profile.name.toLowerCase();
87
+ if (seenNames.has(name)) {
88
+ throw new Error(`${label}: duplicate profile name`);
89
+ }
90
+ seenNames.add(name);
91
+
92
+ if (profile.enabled === false) return;
93
+
94
+ this.#validateEndpoint(label, profile.url, profile.token);
95
+
96
+ const targets = {
97
+ // Same tenant answers under every symbolic name jobs may reference
98
+ agencia: { baseUrl: profile.url, token: profile.token },
99
+ cliente: { baseUrl: profile.url, token: profile.token },
100
+ [name]: { baseUrl: profile.url, token: profile.token },
101
+ };
102
+
103
+ // Explicit targets override symbolic names (real cross-tenant cases)
104
+ for (const [targetName, targetConfig] of Object.entries(
105
+ profile.targets || {},
106
+ )) {
107
+ this.#validateEndpoint(
108
+ `${label}: targets.${targetName}`,
109
+ targetConfig?.baseUrl,
110
+ targetConfig?.token,
111
+ );
112
+ targets[targetName.toLowerCase()] = {
113
+ baseUrl: targetConfig.baseUrl,
114
+ token: targetConfig.token,
115
+ };
116
+ }
117
+
118
+ if (profile.env && typeof profile.env !== 'object') {
119
+ throw new Error(`${label}: 'env' must be an object of key/value pairs`);
120
+ }
121
+
122
+ profiles.push({
123
+ name,
124
+ url: profile.url,
125
+ token: profile.token,
126
+ serverId: raw.serverId,
127
+ targets,
128
+ env: profile.env || {},
129
+ });
130
+ });
131
+
132
+ if (profiles.length === 0) {
133
+ throw new Error(`${resolvedPath}: all profiles are disabled`);
134
+ }
135
+
136
+ return {
137
+ serverId: raw.serverId,
138
+ pollIntervalMs:
139
+ parseInt(raw.pollIntervalMs) > 0
140
+ ? parseInt(raw.pollIntervalMs)
141
+ : DEFAULT_POLL_INTERVAL_MS,
142
+ logFile: path.resolve(expandHome(raw.logFile || DEFAULT_AGENT_LOG_PATH)),
143
+ profiles,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Validate a {url, token} pair with a per-field error message
149
+ * @private
150
+ */
151
+ static #validateEndpoint(label, url, token) {
152
+ if (!url || typeof url !== 'string') {
153
+ throw new Error(`${label}: 'url' is required`);
154
+ }
155
+ if (!/^https?:\/\//.test(url)) {
156
+ throw new Error(`${label}: 'url' must start with http:// or https://`);
157
+ }
158
+ if (!token || typeof token !== 'string') {
159
+ throw new Error(`${label}: 'token' is required`);
160
+ }
161
+ }
162
+ }
163
+
164
+ export default ProfileManager;
@@ -0,0 +1,511 @@
1
+ /**
2
+ * ReportingService.js
3
+ * Phase 7 - Task 2: Reporting & Analytics
4
+ *
5
+ * Provides comprehensive reporting and analytics including:
6
+ * - Session reports
7
+ * - Batch reports
8
+ * - Error analysis reports
9
+ * - Export functionality (JSON, CSV, PDF-ready)
10
+ * - Statistics aggregation
11
+ */
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+
15
+ class ReportingService {
16
+ constructor(logger) {
17
+ this.logger = logger;
18
+ this.reports = {};
19
+ this.sessions = [];
20
+
21
+ this.reportConfig = {
22
+ includeTimestamps: true,
23
+ includeMetrics: true,
24
+ includeErrors: true,
25
+ includeSummary: true,
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Generate session report
31
+ */
32
+ generateSessionReport(session) {
33
+ const report = {
34
+ type: 'session_report',
35
+ timestamp: new Date().toISOString(),
36
+ sessionId: session.sessionId,
37
+ duration: session.endTime
38
+ ? session.endTime - session.startTime
39
+ : Date.now() - session.startTime,
40
+ startTime: new Date(session.startTime).toISOString(),
41
+ endTime: session.endTime ? new Date(session.endTime).toISOString() : null,
42
+
43
+ summary: {
44
+ totalFiles: session.fileCount || 0,
45
+ processedFiles: session.processedCount || 0,
46
+ successCount: session.successCount || 0,
47
+ errorCount: session.errorCount || 0,
48
+ skippedCount: session.skippedCount || 0,
49
+ successRate: this._calculateSuccessRate(session),
50
+ },
51
+
52
+ metrics: {
53
+ averageProcessingTime: this._calculateAvgTime(session),
54
+ filesPerSecond: this._calculateFilesPerSecond(session),
55
+ successRate: this._calculateSuccessRate(session),
56
+ errorRate: this._calculateErrorRate(session),
57
+ },
58
+
59
+ details: {
60
+ uploadedFiles: session.uploadedFiles || [],
61
+ failedFiles: session.failedFiles || [],
62
+ skippedFiles: session.skippedFiles || [],
63
+ },
64
+
65
+ errors: this._extractErrors(session),
66
+
67
+ tags: session.tags || [],
68
+ notes: session.notes || '',
69
+ };
70
+
71
+ this.reports[session.sessionId] = report;
72
+ this.sessions.push(report);
73
+
74
+ return report;
75
+ }
76
+
77
+ /**
78
+ * Generate batch report
79
+ */
80
+ generateBatchReport(sessions) {
81
+ const report = {
82
+ type: 'batch_report',
83
+ timestamp: new Date().toISOString(),
84
+ batchId: `batch_${Date.now()}`,
85
+
86
+ summary: {
87
+ totalSessions: sessions.length,
88
+ totalFiles: 0,
89
+ totalProcessed: 0,
90
+ totalSuccess: 0,
91
+ totalErrors: 0,
92
+ totalSkipped: 0,
93
+ },
94
+
95
+ sessionMetrics: [],
96
+ aggregatedMetrics: {},
97
+ timelineData: [],
98
+ errorSummary: {},
99
+ };
100
+
101
+ // Aggregate data from all sessions
102
+ for (const session of sessions) {
103
+ const sessionReport = this.generateSessionReport(session);
104
+
105
+ report.summary.totalFiles += sessionReport.summary.totalFiles;
106
+ report.summary.totalProcessed += sessionReport.summary.processedFiles;
107
+ report.summary.totalSuccess += sessionReport.summary.successCount;
108
+ report.summary.totalErrors += sessionReport.summary.errorCount;
109
+ report.summary.totalSkipped += sessionReport.summary.skippedCount;
110
+
111
+ report.sessionMetrics.push({
112
+ sessionId: session.sessionId,
113
+ duration: sessionReport.duration,
114
+ fileCount: sessionReport.summary.totalFiles,
115
+ successRate: sessionReport.metrics.successRate,
116
+ });
117
+
118
+ report.timelineData.push({
119
+ timestamp: sessionReport.startTime,
120
+ filesProcessed: sessionReport.summary.processedFiles,
121
+ duration: sessionReport.duration,
122
+ });
123
+
124
+ // Aggregate errors by type
125
+ for (const error of sessionReport.errors) {
126
+ const errorType = error.type || 'unknown';
127
+ if (!report.errorSummary[errorType]) {
128
+ report.errorSummary[errorType] = { count: 0, examples: [] };
129
+ }
130
+ report.errorSummary[errorType].count++;
131
+ if (report.errorSummary[errorType].examples.length < 3) {
132
+ report.errorSummary[errorType].examples.push(error.message);
133
+ }
134
+ }
135
+ }
136
+
137
+ // Calculate aggregated metrics
138
+ report.aggregatedMetrics = {
139
+ totalDuration: report.sessionMetrics.reduce(
140
+ (sum, s) => sum + s.duration,
141
+ 0,
142
+ ),
143
+ averageSessionDuration:
144
+ report.sessionMetrics.length > 0
145
+ ? report.sessionMetrics.reduce((sum, s) => sum + s.duration, 0) /
146
+ report.sessionMetrics.length
147
+ : 0,
148
+ overallSuccessRate:
149
+ report.summary.totalProcessed > 0
150
+ ? (
151
+ (report.summary.totalSuccess / report.summary.totalProcessed) *
152
+ 100
153
+ ).toFixed(2) + '%'
154
+ : 'N/A',
155
+ overallErrorRate:
156
+ report.summary.totalProcessed > 0
157
+ ? (
158
+ (report.summary.totalErrors / report.summary.totalProcessed) *
159
+ 100
160
+ ).toFixed(2) + '%'
161
+ : 'N/A',
162
+ averageFilesPerSession:
163
+ report.summary.totalFiles / Math.max(report.summary.totalSessions, 1),
164
+ };
165
+
166
+ return report;
167
+ }
168
+
169
+ /**
170
+ * Generate error analysis report
171
+ */
172
+ generateErrorAnalysisReport(sessions) {
173
+ const report = {
174
+ type: 'error_analysis_report',
175
+ timestamp: new Date().toISOString(),
176
+
177
+ summary: {
178
+ totalErrors: 0,
179
+ uniqueErrorTypes: new Set(),
180
+ affectedSessions: new Set(),
181
+ },
182
+
183
+ errorBreakdown: {},
184
+ errorPatterns: [],
185
+ recommendations: [],
186
+ };
187
+
188
+ for (const session of sessions) {
189
+ const sessionReport = this.generateSessionReport(session);
190
+
191
+ for (const error of sessionReport.errors) {
192
+ report.summary.totalErrors++;
193
+ report.summary.uniqueErrorTypes.add(error.type);
194
+ report.summary.affectedSessions.add(session.sessionId);
195
+
196
+ // Breakdown by type
197
+ const errorType = error.type || 'unknown';
198
+ if (!report.errorBreakdown[errorType]) {
199
+ report.errorBreakdown[errorType] = {
200
+ count: 0,
201
+ severity: error.severity || 'unknown',
202
+ examples: [],
203
+ };
204
+ }
205
+ report.errorBreakdown[errorType].count++;
206
+
207
+ if (report.errorBreakdown[errorType].examples.length < 5) {
208
+ report.errorBreakdown[errorType].examples.push({
209
+ message: error.message,
210
+ file: error.file,
211
+ timestamp: error.timestamp,
212
+ });
213
+ }
214
+ }
215
+ }
216
+
217
+ // Convert sets to arrays for JSON serialization
218
+ report.summary.uniqueErrorTypes = Array.from(
219
+ report.summary.uniqueErrorTypes,
220
+ );
221
+ report.summary.affectedSessions = Array.from(
222
+ report.summary.affectedSessions,
223
+ );
224
+
225
+ // Identify patterns
226
+ const errorTypes = Object.keys(report.errorBreakdown);
227
+ if (errorTypes.length > 0) {
228
+ const topErrors = errorTypes
229
+ .sort(
230
+ (a, b) =>
231
+ report.errorBreakdown[b].count - report.errorBreakdown[a].count,
232
+ )
233
+ .slice(0, 3);
234
+
235
+ for (const errorType of topErrors) {
236
+ report.errorPatterns.push({
237
+ type: errorType,
238
+ frequency: report.errorBreakdown[errorType].count,
239
+ severity: report.errorBreakdown[errorType].severity,
240
+ occurrenceRate:
241
+ (
242
+ (report.errorBreakdown[errorType].count /
243
+ report.summary.totalErrors) *
244
+ 100
245
+ ).toFixed(2) + '%',
246
+ });
247
+ }
248
+ }
249
+
250
+ // Generate recommendations
251
+ if (report.summary.totalErrors > 0) {
252
+ for (const pattern of report.errorPatterns) {
253
+ report.recommendations.push(
254
+ `${pattern.type}: Occurs in ${pattern.occurrenceRate} of errors. ` +
255
+ `Consider implementing retry logic or improving validation.`,
256
+ );
257
+ }
258
+ }
259
+
260
+ return report;
261
+ }
262
+
263
+ /**
264
+ * Export report to JSON
265
+ */
266
+ async exportReportJson(report, outputPath) {
267
+ try {
268
+ const jsonContent = JSON.stringify(report, null, 2);
269
+ await fs.writeFile(outputPath, jsonContent, 'utf-8');
270
+
271
+ this.logger.info(`Report exported to JSON: ${outputPath}`);
272
+ return { success: true, path: outputPath };
273
+ } catch (error) {
274
+ this.logger.error(`Failed to export report to JSON:`, error);
275
+ return { success: false, error: error.message };
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Export report to CSV
281
+ */
282
+ async exportReportCsv(report, outputPath) {
283
+ try {
284
+ let csvContent = '';
285
+
286
+ if (report.type === 'session_report') {
287
+ csvContent = this._generateSessionCsv(report);
288
+ } else if (report.type === 'batch_report') {
289
+ csvContent = this._generateBatchCsv(report);
290
+ } else if (report.type === 'error_analysis_report') {
291
+ csvContent = this._generateErrorAnalysisCsv(report);
292
+ }
293
+
294
+ await fs.writeFile(outputPath, csvContent, 'utf-8');
295
+
296
+ this.logger.info(`Report exported to CSV: ${outputPath}`);
297
+ return { success: true, path: outputPath };
298
+ } catch (error) {
299
+ this.logger.error(`Failed to export report to CSV:`, error);
300
+ return { success: false, error: error.message };
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Generate summary statistics
306
+ */
307
+ generateSummaryStatistics(sessions) {
308
+ const stats = {
309
+ timestamp: new Date().toISOString(),
310
+ periodAnalyzed: {
311
+ start: sessions.length > 0 ? sessions[0].startTime : null,
312
+ end: sessions.length > 0 ? sessions[sessions.length - 1].endTime : null,
313
+ },
314
+
315
+ totals: {
316
+ sessions: sessions.length,
317
+ files: 0,
318
+ processed: 0,
319
+ successful: 0,
320
+ failed: 0,
321
+ skipped: 0,
322
+ },
323
+
324
+ rates: {},
325
+ performance: {},
326
+ trending: {},
327
+ };
328
+
329
+ for (const session of sessions) {
330
+ stats.totals.files += session.fileCount || 0;
331
+ stats.totals.processed += session.processedCount || 0;
332
+ stats.totals.successful += session.successCount || 0;
333
+ stats.totals.failed += session.errorCount || 0;
334
+ stats.totals.skipped += session.skippedCount || 0;
335
+ }
336
+
337
+ // Calculate rates
338
+ if (stats.totals.processed > 0) {
339
+ stats.rates.successRate =
340
+ ((stats.totals.successful / stats.totals.processed) * 100).toFixed(2) +
341
+ '%';
342
+ stats.rates.failureRate =
343
+ ((stats.totals.failed / stats.totals.processed) * 100).toFixed(2) + '%';
344
+ stats.rates.skipRate =
345
+ ((stats.totals.skipped / stats.totals.processed) * 100).toFixed(2) +
346
+ '%';
347
+ }
348
+
349
+ // Performance metrics
350
+ if (sessions.length > 0) {
351
+ const totalDuration = sessions.reduce(
352
+ (sum, s) => sum + ((s.endTime || Date.now()) - s.startTime),
353
+ 0,
354
+ );
355
+ stats.performance.averageSessionDuration =
356
+ (totalDuration / sessions.length).toFixed(0) + 'ms';
357
+ stats.performance.totalDuration = totalDuration + 'ms';
358
+ stats.performance.filesPerSecond = (
359
+ stats.totals.processed /
360
+ (totalDuration / 1000)
361
+ ).toFixed(2);
362
+ }
363
+
364
+ return stats;
365
+ }
366
+
367
+ /**
368
+ * Get report history
369
+ */
370
+ getReportHistory(limit = 10) {
371
+ return this.sessions
372
+ .slice(-limit)
373
+ .reverse()
374
+ .map((report) => ({
375
+ reportId: report.sessionId,
376
+ type: report.type,
377
+ timestamp: report.timestamp,
378
+ summary: report.summary,
379
+ successRate: report.metrics?.successRate,
380
+ }));
381
+ }
382
+
383
+ /**
384
+ * Clear old reports
385
+ */
386
+ clearOldReports(daysOld = 30) {
387
+ const cutoffTime = Date.now() - daysOld * 24 * 60 * 60 * 1000;
388
+ const beforeCount = this.sessions.length;
389
+
390
+ this.sessions = this.sessions.filter(
391
+ (report) => new Date(report.timestamp).getTime() > cutoffTime,
392
+ );
393
+
394
+ const removed = beforeCount - this.sessions.length;
395
+ this.logger.info(`Cleared ${removed} reports older than ${daysOld} days`);
396
+
397
+ return { removed, remaining: this.sessions.length };
398
+ }
399
+
400
+ /**
401
+ * Private helper methods
402
+ */
403
+
404
+ _calculateSuccessRate(session) {
405
+ if (!session.processedCount || session.processedCount === 0) return '0%';
406
+ return (
407
+ (((session.successCount || 0) / session.processedCount) * 100).toFixed(
408
+ 2,
409
+ ) + '%'
410
+ );
411
+ }
412
+
413
+ _calculateErrorRate(session) {
414
+ if (!session.processedCount || session.processedCount === 0) return '0%';
415
+ return (
416
+ (((session.errorCount || 0) / session.processedCount) * 100).toFixed(2) +
417
+ '%'
418
+ );
419
+ }
420
+
421
+ _calculateAvgTime(session) {
422
+ if (!session.processedCount || session.processedCount === 0) return 0;
423
+ const duration = session.endTime
424
+ ? session.endTime - session.startTime
425
+ : Date.now() - session.startTime;
426
+ return Math.round(duration / session.processedCount) + 'ms';
427
+ }
428
+
429
+ _calculateFilesPerSecond(session) {
430
+ if (!session.processedCount) return 0;
431
+ const duration = session.endTime
432
+ ? session.endTime - session.startTime
433
+ : Date.now() - session.startTime;
434
+ return (session.processedCount / (duration / 1000)).toFixed(2);
435
+ }
436
+
437
+ _extractErrors(session) {
438
+ return (session.errors || []).map((error) => ({
439
+ type: error.type || 'unknown',
440
+ message: error.message,
441
+ file: error.file,
442
+ severity: error.severity || 'error',
443
+ timestamp: error.timestamp || new Date().toISOString(),
444
+ context: error.context,
445
+ }));
446
+ }
447
+
448
+ _generateSessionCsv(report) {
449
+ const lines = [
450
+ 'Session Report',
451
+ `Generated: ${report.timestamp}`,
452
+ `Session ID: ${report.sessionId}`,
453
+ '',
454
+ 'Summary',
455
+ 'Total Files,Processed,Success,Errors,Skipped',
456
+ `${report.summary.totalFiles},${report.summary.processedFiles},${report.summary.successCount},${report.summary.errorCount},${report.summary.skippedCount}`,
457
+ '',
458
+ 'Metrics',
459
+ 'Metric,Value',
460
+ `Success Rate,${report.metrics.successRate}`,
461
+ `Error Rate,${report.metrics.errorRate}`,
462
+ `Files Per Second,${report.metrics.filesPerSecond}`,
463
+ `Avg Processing Time,${report.metrics.averageProcessingTime}`,
464
+ ];
465
+
466
+ return lines.join('\n');
467
+ }
468
+
469
+ _generateBatchCsv(report) {
470
+ const lines = [
471
+ 'Batch Report',
472
+ `Generated: ${report.timestamp}`,
473
+ '',
474
+ 'Session Metrics',
475
+ 'Session ID,Duration (ms),File Count,Success Rate',
476
+ ];
477
+
478
+ for (const metric of report.sessionMetrics) {
479
+ lines.push(
480
+ `${metric.sessionId},${metric.duration},${metric.fileCount},${metric.successRate}`,
481
+ );
482
+ }
483
+
484
+ lines.push('', 'Summary', 'Metric,Value');
485
+ for (const [key, value] of Object.entries(report.aggregatedMetrics)) {
486
+ lines.push(`${key},${value}`);
487
+ }
488
+
489
+ return lines.join('\n');
490
+ }
491
+
492
+ _generateErrorAnalysisCsv(report) {
493
+ const lines = [
494
+ 'Error Analysis Report',
495
+ `Generated: ${report.timestamp}`,
496
+ '',
497
+ 'Error Summary',
498
+ 'Error Type,Count,Severity,Occurrence Rate',
499
+ ];
500
+
501
+ for (const pattern of report.errorPatterns) {
502
+ lines.push(
503
+ `${pattern.type},${pattern.frequency},${pattern.severity},${pattern.occurrenceRate}`,
504
+ );
505
+ }
506
+
507
+ return lines.join('\n');
508
+ }
509
+ }
510
+
511
+ export default ReportingService;