11package de .tum .cit .aet .artemis .atlas .service ;
22
3+ import java .util .Collections ;
4+ import java .util .Set ;
5+
36import org .slf4j .Logger ;
47import org .slf4j .LoggerFactory ;
58import org .springframework .context .annotation .Conditional ;
912import org .springframework .stereotype .Component ;
1013
1114import de .tum .cit .aet .artemis .atlas .config .AtlasEnabled ;
15+ import de .tum .cit .aet .artemis .atlas .dto .CourseAutoOrchestrationConfigDTO ;
1216import de .tum .cit .aet .artemis .core .security .SecurityUtils ;
1317import de .tum .cit .aet .artemis .core .service .feature .Feature ;
1418import de .tum .cit .aet .artemis .core .service .feature .FeatureToggleService ;
1519import de .tum .cit .aet .artemis .course .domain .Course ;
20+ import de .tum .cit .aet .artemis .course .repository .CourseConfigurationRepository ;
1621import de .tum .cit .aet .artemis .exercise .domain .Exercise ;
1722import de .tum .cit .aet .artemis .exercise .domain .event .ExerciseVersionCreatedEvent ;
23+ import de .tum .cit .aet .artemis .exercise .service .ExerciseVersionService ;
1824
1925/**
2026 * Feeds the automatic competency pipeline whenever an exercise version is created. Hooks into the
2329 * resource.
2430 * <p>
2531 * Everything is gated behind the {@link Feature#AtlasAgent} toggle, which is
26- * disabled by default — instructors can opt in per-instance via the feature-toggle admin UI. Exam
27- * exercises are skipped because competency management is scoped to course content only.
32+ * disabled by default — instructors can opt in per-instance via the feature-toggle admin UI — and,
33+ * additionally, behind the per-course {@code autoOrchestratorEnabled} kill switch: a course only
34+ * participates in the pipeline when the instructor has explicitly enabled it. Exam exercises are
35+ * skipped because competency management is scoped to course content only.
36+ * <p>
37+ * The recording is further filtered by the changed-field set carried on the event
38+ * ({@link ExerciseVersionCreatedEvent#changedFields()}): only versions that touched a
39+ * {@link ExerciseVersionService#COMPETENCY_RELEVANT_FIELDS content-bearing field} record into the
40+ * accumulator, so purely administrative edits (dates, points, grading config, …) never burn the
41+ * per-course daily cap on an orchestration that could not change competency mapping.
2842 */
2943@ Conditional (AtlasEnabled .class )
3044@ Lazy
@@ -37,19 +51,28 @@ public class AutonomousCompetencyExerciseEventListener {
3751
3852 private final FeatureToggleService featureToggleService ;
3953
40- public AutonomousCompetencyExerciseEventListener (ContentChangeAccumulatorService accumulator , FeatureToggleService featureToggleService ) {
54+ private final CourseConfigurationRepository courseConfigurationRepository ;
55+
56+ public AutonomousCompetencyExerciseEventListener (ContentChangeAccumulatorService accumulator , FeatureToggleService featureToggleService ,
57+ CourseConfigurationRepository courseConfigurationRepository ) {
4158 this .accumulator = accumulator ;
4259 this .featureToggleService = featureToggleService ;
60+ this .courseConfigurationRepository = courseConfigurationRepository ;
4361 }
4462
4563 /**
4664 * Fires on every {@link ExerciseVersionCreatedEvent} — publishers live in the exercise module,
4765 * so one listener covers every authoring path (programming / text / modeling / quiz / file
48- * upload). The method is a no-op when the toggle is off, when the exercise is an exam exercise,
49- * or when any null guard trips; in the success path it merges the exercise id into the
50- * per-course accumulator for the scheduler to pick up.
66+ * upload). The method is a no-op when the global toggle is off, when the exercise is an exam
67+ * exercise, when any null guard trips, or when the change touched no content-bearing field; in
68+ * the success path it merges the exercise id into the per-course accumulator for the scheduler to
69+ * pick up.
70+ * <p>
71+ * When the owning course has auto-orchestration disabled the method flushes the course's
72+ * accumulator bucket (dropping any ids buffered while it was enabled) and returns without
73+ * recording, so disabling acts as an immediate per-course kill switch.
5174 *
52- * @param event the just-published event carrying the newly versioned exercise
75+ * @param event the just-published event carrying the newly versioned exercise and its changed fields
5376 */
5477 @ EventListener
5578 @ Async
@@ -66,7 +89,25 @@ public void onExerciseVersionCreated(ExerciseVersionCreatedEvent event) {
6689 if (course == null || course .getId () == null ) {
6790 return ;
6891 }
69- log .debug ("atlas.automatic recorded exercise change courseId={} exerciseId={}" , course .getId (), exercise .getId ());
70- accumulator .record (course .getId (), exercise .getId ());
92+ long courseId = course .getId ();
93+ boolean autoOrchestratorEnabled = courseConfigurationRepository .findAutoOrchestrationConfigByCourseId (courseId )
94+ .map (CourseAutoOrchestrationConfigDTO ::autoOrchestratorEnabled ).orElse (false );
95+ if (!autoOrchestratorEnabled ) {
96+ // Per-course kill switch is off: drop anything buffered while it was on so a later
97+ // re-enable or scheduler tick cannot resurrect stale changes for a disabled course.
98+ accumulator .flush (courseId );
99+ return ;
100+ }
101+ // Filter on the changed-field set: only record when the version touched a content-bearing
102+ // field that could affect competency mapping. An empty set (e.g. legacy events) is treated
103+ // as not relevant.
104+ Set <String > changedFields = event .changedFields () == null ? Collections .emptySet () : event .changedFields ();
105+ if (Collections .disjoint (changedFields , ExerciseVersionService .COMPETENCY_RELEVANT_FIELDS )) {
106+ log .debug ("atlas.automatic skipping exercise change courseId={} exerciseId={}: no competency-relevant field changed (changed={})" , courseId , exercise .getId (),
107+ changedFields );
108+ return ;
109+ }
110+ log .debug ("atlas.automatic recorded exercise change courseId={} exerciseId={}" , courseId , exercise .getId ());
111+ accumulator .record (courseId , exercise .getId ());
71112 }
72113}
0 commit comments