aggregateData function

AggregatedData aggregateData(
  1. List<SessionMeta> sessions,
  2. Map<String, SessionFacets> facets
)

Aggregate data from all sessions and facets into a single report.

Implementation

AggregatedData aggregateData(
  List<SessionMeta> sessions,
  Map<String, SessionFacets> facets,
) {
  final result = AggregatedData(totalSessions: sessions.length);
  result.sessionsWithFacets = facets.length;

  final dates = <String>[];
  final allResponseTimes = <double>[];
  final allMessageHours = <int>[];

  for (final session in sessions) {
    dates.add(session.startTime);
    result.totalMessages += session.userMessageCount;
    result.totalDurationHours += session.durationMinutes / 60.0;
    result.totalInputTokens += session.inputTokens;
    result.totalOutputTokens += session.outputTokens;
    result.gitCommits += session.gitCommits;
    result.gitPushes += session.gitPushes;

    // Aggregate new stats.
    result.totalInterruptions += session.userInterruptions;
    result.totalToolErrors += session.toolErrors;
    for (final entry in session.toolErrorCategories.entries) {
      result.toolErrorCategories[entry.key] =
          (result.toolErrorCategories[entry.key] ?? 0) + entry.value;
    }
    allResponseTimes.addAll(session.userResponseTimes);
    if (session.usesTaskAgent) result.sessionsUsingTaskAgent++;
    if (session.usesMcp) result.sessionsUsingMcp++;
    if (session.usesWebSearch) result.sessionsUsingWebSearch++;
    if (session.usesWebFetch) result.sessionsUsingWebFetch++;

    // Additional stats.
    result.totalLinesAdded += session.linesAdded;
    result.totalLinesRemoved += session.linesRemoved;
    result.totalFilesModified += session.filesModified;
    allMessageHours.addAll(session.messageHours);

    // Merge tool and language counts.
    for (final entry in session.toolCounts.entries) {
      result.toolCounts[entry.key] =
          (result.toolCounts[entry.key] ?? 0) + entry.value;
    }
    for (final entry in session.languages.entries) {
      result.languages[entry.key] =
          (result.languages[entry.key] ?? 0) + entry.value;
    }

    if (session.projectPath.isNotEmpty) {
      result.projects[session.projectPath] =
          (result.projects[session.projectPath] ?? 0) + 1;
    }

    // Merge facets.
    final sessionFacets = facets[session.sessionId];
    if (sessionFacets != null) {
      for (final entry in sessionFacets.goalCategories.entries) {
        if (entry.value > 0) {
          result.goalCategories[entry.key] =
              (result.goalCategories[entry.key] ?? 0) + entry.value;
        }
      }

      result.outcomes[sessionFacets.outcome] =
          (result.outcomes[sessionFacets.outcome] ?? 0) + 1;

      for (final entry in sessionFacets.userSatisfactionCounts.entries) {
        if (entry.value > 0) {
          result.satisfaction[entry.key] =
              (result.satisfaction[entry.key] ?? 0) + entry.value;
        }
      }

      result.helpfulness[sessionFacets.neomageHelpfulness] =
          (result.helpfulness[sessionFacets.neomageHelpfulness] ?? 0) + 1;

      result.sessionTypes[sessionFacets.sessionType] =
          (result.sessionTypes[sessionFacets.sessionType] ?? 0) + 1;

      for (final entry in sessionFacets.frictionCounts.entries) {
        if (entry.value > 0) {
          result.friction[entry.key] =
              (result.friction[entry.key] ?? 0) + entry.value;
        }
      }

      if (sessionFacets.primarySuccess != 'none') {
        result.success[sessionFacets.primarySuccess] =
            (result.success[sessionFacets.primarySuccess] ?? 0) + 1;
      }
    }

    // Collect session summaries (max 50).
    if (result.sessionSummaries.length < 50) {
      result.sessionSummaries.add({
        'id': session.sessionId.length >= 8
            ? session.sessionId.substring(0, 8)
            : session.sessionId,
        'date': session.startTime.split('T').first,
        'summary':
            session.summary ??
            (session.firstPrompt.length > 100
                ? session.firstPrompt.substring(0, 100)
                : session.firstPrompt),
        if (sessionFacets?.underlyingGoal != null)
          'goal': sessionFacets!.underlyingGoal,
      });
    }
  }

  // Date range.
  dates.sort();
  if (dates.isNotEmpty) {
    result.dateRange = (
      start: dates.first.split('T').first,
      end: dates.last.split('T').first,
    );
  }

  // Response time statistics.
  result.userResponseTimes = allResponseTimes;
  if (allResponseTimes.isNotEmpty) {
    final sorted = [...allResponseTimes]..sort();
    result.medianResponseTime = sorted[sorted.length ~/ 2];
    result.avgResponseTime =
        allResponseTimes.reduce((a, b) => a + b) / allResponseTimes.length;
  }

  // Days active and messages per day.
  final uniqueDays = dates.map((d) => d.split('T').first).toSet();
  result.daysActive = uniqueDays.length;
  result.messagesPerDay = result.daysActive > 0
      ? (result.totalMessages / result.daysActive * 10).round() / 10.0
      : 0;

  // Store message hours for time-of-day chart.
  result.messageHours = allMessageHours;

  // Multi-clauding detection.
  result.multiClauding = detectMultiClauding(
    sessions
        .map(
          (s) => (sessionId: s.sessionId, timestamps: s.userMessageTimestamps),
        )
        .toList(),
  );

  return result;
}