1111import de .tum .cit .aet .ai .service .ContributionFairnessService ;
1212import de .tum .cit .aet .analysis .domain .AnalyzedChunk ;
1313import de .tum .cit .aet .analysis .domain .ExerciseEmailMapping ;
14- import de .tum .cit .aet .analysis .domain .ExerciseTemplateAuthor ;
1514import de .tum .cit .aet .analysis .dto .AuthorContributionDTO ;
1615import de .tum .cit .aet .analysis .dto .OrphanCommitDTO ;
1716import de .tum .cit .aet .analysis .dto .RepositoryAnalysisResultDTO ;
3332import de .tum .cit .aet .repositoryProcessing .repository .StudentRepository ;
3433import de .tum .cit .aet .repositoryProcessing .repository .TeamParticipationRepository ;
3534import de .tum .cit .aet .repositoryProcessing .repository .TeamRepositoryRepository ;
36- import de .tum .cit .aet .repositoryProcessing .repository .TutorRepository ;
3735import lombok .extern .slf4j .Slf4j ;
3836import org .springframework .stereotype .Service ;
3937import org .springframework .transaction .annotation .Transactional ;
4038import org .springframework .transaction .support .TransactionTemplate ;
4139
4240import java .nio .file .Files ;
4341import java .nio .file .Path ;
44- import java .util .ArrayList ;
45- import java .util .HashMap ;
46- import java .util .List ;
47- import java .util .Map ;
48- import java .util .Optional ;
42+ import java .util .*;
43+ import java .util .stream .Collectors ;
4944
5045/**
5146 * Persists Phase-2 (git analysis) and Phase-3 (AI analysis) results.
@@ -68,7 +63,6 @@ public class AnalysisResultPersistenceService {
6863
6964 private final TeamRepositoryRepository teamRepositoryRepository ;
7065 private final TeamParticipationRepository teamParticipationRepository ;
71- private final TutorRepository tutorRepository ;
7266 private final StudentRepository studentRepository ;
7367 private final AnalyzedChunkRepository analyzedChunkRepository ;
7468 private final ExerciseTemplateAuthorRepository templateAuthorRepository ;
@@ -94,7 +88,6 @@ public AnalysisResultPersistenceService(
9488 PairProgrammingService pairProgrammingService ,
9589 TeamRepositoryRepository teamRepositoryRepository ,
9690 TeamParticipationRepository teamParticipationRepository ,
97- TutorRepository tutorRepository ,
9891 StudentRepository studentRepository ,
9992 AnalyzedChunkRepository analyzedChunkRepository ,
10093 ExerciseTemplateAuthorRepository templateAuthorRepository ,
@@ -113,7 +106,6 @@ public AnalysisResultPersistenceService(
113106 this .pairProgrammingService = pairProgrammingService ;
114107 this .teamRepositoryRepository = teamRepositoryRepository ;
115108 this .teamParticipationRepository = teamParticipationRepository ;
116- this .tutorRepository = tutorRepository ;
117109 this .studentRepository = studentRepository ;
118110 this .analyzedChunkRepository = analyzedChunkRepository ;
119111 this .templateAuthorRepository = templateAuthorRepository ;
@@ -256,9 +248,10 @@ public ClientResponseWithUsage saveAIAnalysisResultWithUsage(TeamRepositoryDTO r
256248 teamParticipationRepository .save (teamParticipation );
257249
258250 List <Student > students = studentRepository .findAllByTeam (teamParticipation );
259- String templateAuthorEmail = templateAuthorRepository .findByExerciseId (exerciseId )
260- .map (ExerciseTemplateAuthor ::getTemplateEmail )
261- .orElse (null );
251+ Set <String > templateAuthorEmails = templateAuthorRepository .findByExerciseId (exerciseId )
252+ .stream ()
253+ .map (ta -> ta .getTemplateEmail ().toLowerCase (Locale .ROOT ))
254+ .collect (Collectors .toSet ());
262255
263256 Double cqi = null ;
264257 boolean isSuspicious = false ;
@@ -270,7 +263,7 @@ public ClientResponseWithUsage saveAIAnalysisResultWithUsage(TeamRepositoryDTO r
270263 // 1) Detect orphan commits
271264 try {
272265 RepositoryAnalysisResultDTO analysisResult = gitContributionAnalysisService
273- .analyzeRepositoryWithOrphans (repo , templateAuthorEmail );
266+ .analyzeRepositoryWithOrphans (repo , templateAuthorEmails );
274267 orphanCommits = analysisResult .orphanCommits ();
275268 } catch (Exception e ) {
276269 log .warn ("Failed to detect orphan commits for team {}: {}" , team .name (), e .getMessage ());
@@ -280,7 +273,7 @@ public ClientResponseWithUsage saveAIAnalysisResultWithUsage(TeamRepositoryDTO r
280273 boolean fairnessSucceeded = false ;
281274 try {
282275 FairnessReportWithUsageDTO fairnessResult = fairnessService .analyzeFairnessWithUsage (
283- repo , templateAuthorEmail );
276+ repo , templateAuthorEmails , null );
284277 FairnessReportDTO report = fairnessResult .report ();
285278 teamTokenTotals = fairnessResult .tokenTotals ();
286279
@@ -541,11 +534,11 @@ public void applyExistingEmailMappings(TeamParticipation participation, Long exe
541534 Map <String , List <AnalyzedChunk >> remappedByStudent = new HashMap <>();
542535
543536 for (ExerciseEmailMapping mapping : mappings ) {
544- String emailLower = mapping .getGitEmail ().toLowerCase (java . util . Locale .ROOT );
537+ String emailLower = mapping .getGitEmail ().toLowerCase (Locale .ROOT );
545538 for (AnalyzedChunk chunk : chunks ) {
546539 if (Boolean .TRUE .equals (chunk .getIsExternalContributor ())
547540 && emailLower .equals (chunk .getAuthorEmail () != null
548- ? chunk .getAuthorEmail ().toLowerCase (java . util . Locale .ROOT ) : null )) {
541+ ? chunk .getAuthorEmail ().toLowerCase (Locale .ROOT ) : null )) {
549542 chunk .setIsExternalContributor (false );
550543 chunk .setAuthorName (mapping .getStudentName ());
551544 remappedByStudent .computeIfAbsent (mapping .getStudentName (), k -> new ArrayList <>())
@@ -587,6 +580,27 @@ public void applyExistingEmailMappings(TeamParticipation participation, Long exe
587580 }
588581 }
589582 cqiRecalculationService .recalculateFromChunks (participation , chunks );
583+
584+ // Recompute orphan commit count from remaining external-contributor chunks,
585+ // but exclude template authors (they should not be considered "unmatched")
586+ Set <String > templateAuthorEmails = templateAuthorRepository .findByExerciseId (exerciseId )
587+ .stream ().map (ta -> ta .getTemplateEmail ().toLowerCase (Locale .ROOT )).collect (Collectors .toSet ());
588+ int remainingOrphanCommits = 0 ;
589+ for (AnalyzedChunk c : chunks ) {
590+ if (Boolean .TRUE .equals (c .getIsExternalContributor ())) {
591+ String chunkEmail = c .getAuthorEmail () != null ? c .getAuthorEmail ().toLowerCase (Locale .ROOT ) : null ;
592+ if (chunkEmail != null && templateAuthorEmails .contains (chunkEmail )) {
593+ // skip template authors
594+ continue ;
595+ }
596+ if (c .getCommitShas () != null && !c .getCommitShas ().isEmpty ()) {
597+ remainingOrphanCommits += c .getCommitShas ().split ("," ).length ;
598+ }
599+ }
600+ }
601+ participation .setOrphanCommitCount (remainingOrphanCommits );
602+ // persist updated participation so UI queries see new value
603+ teamParticipationRepository .save (participation );
590604 }
591605 } catch (Exception e ) {
592606 log .warn ("Failed to apply existing email mappings for team {}: {}" ,
@@ -630,24 +644,6 @@ public String serializeCommitMessages(List<String> messages) {
630644 }
631645 }
632646
633- /**
634- * Serializes a weekly effort distribution to a JSON string.
635- *
636- * @param weeklyDistribution the weekly effort values
637- * @return JSON array string, or {@code null} if empty or on error
638- */
639- public String serializeWeeklyDistribution (List <Double > weeklyDistribution ) {
640- try {
641- if (weeklyDistribution == null || weeklyDistribution .isEmpty ()) {
642- return null ;
643- }
644- return objectMapper .writeValueAsString (weeklyDistribution );
645- } catch (Exception e ) {
646- log .warn ("Failed to serialize weekly distribution: {}" , e .getMessage ());
647- return null ;
648- }
649- }
650-
651647 // =====================================================================
652648 // Internal helpers
653649 // =====================================================================
0 commit comments