Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/fonts/Geist-Variable.woff2
Binary file not shown.
Binary file added public/fonts/GeistMono-Variable.woff2
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package de.tum.cit.memo.controller;

import de.tum.cit.memo.dto.CreateCompetencyRelationshipRequest;
import de.tum.cit.memo.dto.VoteRequest;
import de.tum.cit.memo.entity.CompetencyRelationship;
import de.tum.cit.memo.service.CompetencyRelationshipService;
import de.tum.cit.memo.service.SchedulingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
Expand All @@ -19,18 +21,37 @@

import java.util.List;

/**
* Controller for competency relationship management.
* The POST endpoint creates a relationship and records a vote.
*/
@RestController
@RequestMapping("/api/competency-relationships")
@RequiredArgsConstructor
@Tag(name = "Competency Relationships", description = "Competency relationship management endpoints")
public class CompetencyRelationshipController {

private final CompetencyRelationshipService relationshipService;
private final SchedulingService schedulingService;

@PostMapping
@Operation(summary = "Create a new competency relationship")
public ResponseEntity<CompetencyRelationship> createRelationship(@Valid @RequestBody CreateCompetencyRelationshipRequest request) {
CompetencyRelationship relationship = relationshipService.createRelationship(request);
@Operation(summary = "Create a competency relationship and record a vote")
public ResponseEntity<CompetencyRelationship> createRelationship(
@Valid @RequestBody CreateCompetencyRelationshipRequest request) {
// Create the relationship if it doesn't exist
CompetencyRelationship relationship = relationshipService.findOrCreateRelationship(
request.getOriginId(), request.getDestinationId());

// Record the vote via scheduling service
VoteRequest voteRequest = VoteRequest.builder()
.relationshipId(relationship.getId())
.relationshipType(request.getRelationshipType())
.build();
schedulingService.submitVote(request.getUserId(), voteRequest);

// Refresh to get updated counters
relationship = relationshipService.getRelationshipById(relationship.getId());

return ResponseEntity.status(HttpStatus.CREATED).body(relationship);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package de.tum.cit.memo.controller;

import de.tum.cit.memo.dto.RelationshipTaskResponse;
import de.tum.cit.memo.dto.VoteRequest;
import de.tum.cit.memo.dto.VoteResponse;
import de.tum.cit.memo.service.SchedulingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/scheduling")
@RequiredArgsConstructor
@Tag(name = "Scheduling", description = "Endpoints for competency mapping scheduling")
public class SchedulingController {

private final SchedulingService schedulingService;

@GetMapping("/next-relationship")
@Operation(summary = "Get next relationship to vote on", description = "Returns a competency pair for the user to map. Uses coverage (70%) and consensus (30%) pipelines.")
public ResponseEntity<RelationshipTaskResponse> getNextRelationship(
@RequestHeader(value = "X-User-Id", required = false, defaultValue = "anonymous") String userId) {
RelationshipTaskResponse task = schedulingService.getNextTask(userId);
Comment thread
markstockhausen marked this conversation as resolved.
Outdated
return ResponseEntity.ok(task);
}

@PostMapping("/vote")
@Operation(summary = "Submit a vote on a relationship", description = "Records the user's vote on a competency relationship. MATCHES votes are bidirectional.")
Comment thread
markstockhausen marked this conversation as resolved.
Outdated
public ResponseEntity<VoteResponse> submitVote(
@RequestHeader(value = "X-User-Id", required = false, defaultValue = "anonymous") String userId,
@Valid @RequestBody VoteRequest request) {
VoteResponse response = schedulingService.submitVote(userId, request);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.tum.cit.memo.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RelationshipTaskResponse {

private String relationshipId;
private CompetencyInfo origin;
private CompetencyInfo destination;
private String pipeline; // "COVERAGE" or "CONSENSUS"
private VoteCounts currentVotes;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CompetencyInfo {
private String id;
private String title;
private String description;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class VoteCounts {
private int assumes;
private int extendsRelation; // 'extends' is a Java reserved keyword
private int matches;
private int unrelated;
}
}
22 changes: 22 additions & 0 deletions server/src/main/java/de/tum/cit/memo/dto/VoteRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package de.tum.cit.memo.dto;

import de.tum.cit.memo.enums.RelationshipType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VoteRequest {

@NotBlank(message = "Relationship ID is required")
private String relationshipId;

@NotNull(message = "Relationship type is required")
private RelationshipType relationshipType;
}
28 changes: 28 additions & 0 deletions server/src/main/java/de/tum/cit/memo/dto/VoteResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package de.tum.cit.memo.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VoteResponse {

private boolean success;
private VoteCounts updatedVotes;
private double newEntropy;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class VoteCounts {
private int assumes;
private int extendsRelation; // 'extends' is a Java reserved keyword
private int matches;
private int unrelated;
}
}
3 changes: 3 additions & 0 deletions server/src/main/java/de/tum/cit/memo/entity/Competency.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ public class Competency {
@CreationTimestamp
@Column(nullable = false, updatable = false)
private Instant createdAt;

@Column(nullable = false)
private int degree;
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
package de.tum.cit.memo.entity;

import de.tum.cit.memo.enums.RelationshipType;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.Instant;

Expand All @@ -34,11 +32,6 @@ public class CompetencyRelationship {
@Column(length = 30)
private String id;

@Enumerated(EnumType.STRING)
@Column(name = "relationship_type", nullable = false)
@NotNull
private RelationshipType relationshipType;

@NotBlank
@Column(name = "origin_id", nullable = false, length = 30)
private String originId;
Expand All @@ -47,25 +40,79 @@ public class CompetencyRelationship {
@Column(name = "destination_id", nullable = false, length = 30)
private String destinationId;

@NotBlank
@Column(name = "user_id", nullable = false, length = 30)
private String userId;
// Aggregated vote counters
@Column(name = "vote_assumes", nullable = false)
@Builder.Default
private Integer voteAssumes = 0;

@Column(name = "vote_extends", nullable = false)
@Builder.Default
private Integer voteExtends = 0;

@Column(name = "vote_matches", nullable = false)
@Builder.Default
private Integer voteMatches = 0;

@Column(name = "vote_unrelated", nullable = false)
@Builder.Default
private Integer voteUnrelated = 0;

// Precomputed entropy for consensus scheduling
@Column(name = "entropy", nullable = false)
@Builder.Default
private Double entropy = 0.0;

// Total votes (denormalized for convenience)
@Column(name = "total_votes", nullable = false)
@Builder.Default
private Integer totalVotes = 0;

// Relationships (ignored in JSON to avoid proxy serialization issues)
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "origin_id", insertable = false, updatable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Competency origin;

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "destination_id", insertable = false, updatable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Competency destination;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;

@CreationTimestamp
@Column(nullable = false, updatable = false)
private Instant createdAt;

@UpdateTimestamp
@Column(nullable = false)
private Instant updatedAt;

/**
* Recalculates entropy based on current vote distribution.
* Entropy ranges from 0 (unanimous) to ~2.0 (max disagreement with 4 types).
*/
public void recalculateEntropy() {
int total = voteAssumes + voteExtends + voteMatches + voteUnrelated;
this.totalVotes = total;

if (total == 0) {
this.entropy = 0.0;
return;
}

double ent = 0.0;
int[] counts = new int[4];
counts[0] = voteAssumes;
counts[1] = voteExtends;
counts[2] = voteMatches;
counts[3] = voteUnrelated;
for (int count : counts) {
if (count > 0) {
double p = (double) count / total;
ent -= p * (Math.log(p) / Math.log(2));
}
}
this.entropy = ent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package de.tum.cit.memo.entity;

import de.tum.cit.memo.enums.RelationshipType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import java.time.Instant;

@Entity
@Table(name = "competency_relationships_votes")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CompetencyRelationshipVote {

@Id
@Column(length = 30)
private String id;

@NotBlank
@Column(name = "relationship_id", nullable = false, length = 30)
private String relationshipId;

@NotBlank
@Column(name = "user_id", nullable = false, length = 30)
private String userId;

@Enumerated(EnumType.STRING)
@Column(name = "relationship_type", nullable = false)
@NotNull
private RelationshipType relationshipType;

// Relationships
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "relationship_id", insertable = false, updatable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private CompetencyRelationship relationship;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;

@CreationTimestamp
@Column(nullable = false, updatable = false)
private Instant createdAt;
}
Loading
Loading