Skip to content

Commit fb06cc6

Browse files
az108claude
andcommitted
Merge origin/main into fix-pair-programming
Resolve conflicts in Teams.tsx: keep PP merge logic in onGitDone and adopt main's templateAuthors accumulation pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents 38b8333 + 221ae2a commit fb06cc6

21 files changed

Lines changed: 997 additions & 790 deletions

openapi/openapi.yaml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ paths:
292292
/api/exercises/{exerciseId}/email-mappings/template-author:
293293
get:
294294
tags: [email-mapping-resource]
295-
operationId: getTemplateAuthor
295+
operationId: getTemplateAuthors
296296
parameters:
297297
- name: exerciseId
298298
in: path
@@ -303,10 +303,12 @@ paths:
303303
description: OK
304304
content:
305305
application/json:
306-
schema: {$ref: '#/components/schemas/TemplateAuthorDTO'}
306+
schema:
307+
type: array
308+
items: {$ref: '#/components/schemas/TemplateAuthorDTO'}
307309
put:
308310
tags: [email-mapping-resource]
309-
operationId: setTemplateAuthor
311+
operationId: setTemplateAuthors
310312
parameters:
311313
- name: exerciseId
312314
in: path
@@ -315,7 +317,9 @@ paths:
315317
requestBody:
316318
content:
317319
application/json:
318-
schema: {$ref: '#/components/schemas/TemplateAuthorDTO'}
320+
schema:
321+
type: array
322+
items: {$ref: '#/components/schemas/TemplateAuthorDTO'}
319323
required: true
320324
responses:
321325
'200':
@@ -327,7 +331,7 @@ paths:
327331
items: {$ref: '#/components/schemas/ClientResponseDTO'}
328332
delete:
329333
tags: [email-mapping-resource]
330-
operationId: deleteTemplateAuthor
334+
operationId: deleteTemplateAuthors
331335
parameters:
332336
- name: exerciseId
333337
in: path

src/main/java/de/tum/cit/aet/ai/service/ContributionFairnessService.java

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -51,41 +51,19 @@ public class ContributionFairnessService {
5151
* @return fairness report
5252
*/
5353
public FairnessReportDTO analyzeFairness(TeamRepositoryDTO repositoryDTO) {
54-
return analyzeFairnessWithUsage(repositoryDTO).report();
55-
}
56-
57-
/**
58-
* Analyses a repository and returns both fairness report and LLM token totals.
59-
*
60-
* @param repositoryDTO the repository to analyse
61-
* @return fairness report plus token usage
62-
*/
63-
public FairnessReportWithUsageDTO analyzeFairnessWithUsage(TeamRepositoryDTO repositoryDTO) {
64-
return analyzeFairnessWithUsage(repositoryDTO, null, null);
65-
}
66-
67-
/**
68-
* Analyses a repository with optional template author exclusion (default weights).
69-
*
70-
* @param repositoryDTO the repository to analyse
71-
* @param templateAuthorEmail email of the template author to exclude (lowercase), or {@code null}
72-
* @return fairness report plus token usage
73-
*/
74-
public FairnessReportWithUsageDTO analyzeFairnessWithUsage(TeamRepositoryDTO repositoryDTO,
75-
String templateAuthorEmail) {
76-
return analyzeFairnessWithUsage(repositoryDTO, templateAuthorEmail, null);
54+
return analyzeFairnessWithUsage(repositoryDTO, null, null).report();
7755
}
7856

7957
/**
8058
* Analyses a repository with optional template author exclusion.
8159
*
82-
* @param repositoryDTO the repository to analyse
83-
* @param templateAuthorEmail email of the template author to exclude (lowercase), or {@code null}
84-
* @param exerciseId the exercise ID for per-exercise CQI weight resolution
60+
* @param repositoryDTO the repository to analyse
61+
* @param templateAuthorEmails emails of the template authors to exclude (lowercase), or {@code null}
62+
* @param exerciseId the exercise ID for per-exercise CQI weight resolution
8563
* @return fairness report plus token usage
8664
*/
8765
public FairnessReportWithUsageDTO analyzeFairnessWithUsage(TeamRepositoryDTO repositoryDTO,
88-
String templateAuthorEmail, Long exerciseId) {
66+
Set<String> templateAuthorEmails, Long exerciseId) {
8967
String repoPath = repositoryDTO.localPath();
9068
String teamName = repositoryDTO.participation().team().name();
9169
String shortName = repositoryDTO.participation().team().shortName();
@@ -103,7 +81,7 @@ public FairnessReportWithUsageDTO analyzeFairnessWithUsage(TeamRepositoryDTO rep
10381

10482
try {
10583
// 1) Map commits to authors using full git history walk
106-
AuthorMappingResult authorMapping = mapCommitsToAuthors(repositoryDTO, templateAuthorEmail);
84+
AuthorMappingResult authorMapping = mapCommitsToAuthors(repositoryDTO, templateAuthorEmails);
10785

10886
if (authorMapping.teamMemberCommits.isEmpty()) {
10987
return new FairnessReportWithUsageDTO(
@@ -207,10 +185,10 @@ private record AuthorMappingResult(
207185
* and assigns synthetic negative IDs for external contributors (grouped by email).
208186
*/
209187
private AuthorMappingResult mapCommitsToAuthors(TeamRepositoryDTO repositoryDTO,
210-
String templateAuthorEmail) {
188+
Set<String> templateAuthorEmails) {
211189
// 1) Delegate commit mapping to analysis service (single git walk)
212190
CommitMappingResultDTO mapping = gitContributionAnalysisService.mapCommitToAuthor(
213-
repositoryDTO, templateAuthorEmail);
191+
repositoryDTO, templateAuthorEmails);
214192

215193
// 2) Filter assigned commits to team members only
216194
Set<Long> teamMemberIds = new HashSet<>();

src/main/java/de/tum/cit/aet/analysis/domain/ExerciseTemplateAuthor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class ExerciseTemplateAuthor {
2222
@GeneratedValue(strategy = GenerationType.UUID)
2323
private UUID id;
2424

25-
@Column(name = "exercise_id", nullable = false, unique = true)
25+
@Column(name = "exercise_id", nullable = false)
2626
private Long exerciseId;
2727

2828
@Column(name = "template_email", nullable = false)

src/main/java/de/tum/cit/aet/analysis/repository/ExerciseTemplateAuthorRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
import org.springframework.stereotype.Repository;
77
import org.springframework.transaction.annotation.Transactional;
88

9-
import java.util.Optional;
9+
import java.util.List;
1010
import java.util.UUID;
1111

1212
@Repository
1313
public interface ExerciseTemplateAuthorRepository extends JpaRepository<ExerciseTemplateAuthor, UUID> {
1414

15-
Optional<ExerciseTemplateAuthor> findByExerciseId(Long exerciseId);
15+
List<ExerciseTemplateAuthor> findByExerciseId(Long exerciseId);
1616

1717
@Modifying
1818
@Transactional

src/main/java/de/tum/cit/aet/analysis/service/AnalysisResultPersistenceService.java

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import de.tum.cit.aet.ai.service.ContributionFairnessService;
1212
import de.tum.cit.aet.analysis.domain.AnalyzedChunk;
1313
import de.tum.cit.aet.analysis.domain.ExerciseEmailMapping;
14-
import de.tum.cit.aet.analysis.domain.ExerciseTemplateAuthor;
1514
import de.tum.cit.aet.analysis.dto.AuthorContributionDTO;
1615
import de.tum.cit.aet.analysis.dto.OrphanCommitDTO;
1716
import de.tum.cit.aet.analysis.dto.RepositoryAnalysisResultDTO;
@@ -33,19 +32,15 @@
3332
import de.tum.cit.aet.repositoryProcessing.repository.StudentRepository;
3433
import de.tum.cit.aet.repositoryProcessing.repository.TeamParticipationRepository;
3534
import de.tum.cit.aet.repositoryProcessing.repository.TeamRepositoryRepository;
36-
import de.tum.cit.aet.repositoryProcessing.repository.TutorRepository;
3735
import lombok.extern.slf4j.Slf4j;
3836
import org.springframework.stereotype.Service;
3937
import org.springframework.transaction.annotation.Transactional;
4038
import org.springframework.transaction.support.TransactionTemplate;
4139

4240
import java.nio.file.Files;
4341
import 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

Comments
 (0)