Bladeren bron

refactor(exam-sprint): 完成DDD命名治理首轮状态迁移

金逸霄 2 weken geleden
bovenliggende
commit
15141d242f
13 gewijzigde bestanden met toevoegingen van 176 en 45 verwijderingen
  1. 10 10
      abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java
  2. 14 0
      abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java
  3. 2 2
      abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java
  4. 21 16
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
  5. 22 0
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java
  6. 5 5
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java
  7. 7 8
      abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java
  8. 9 0
      abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportGenerationStatus.java
  9. 6 0
      ability-center-runtime/pom.xml
  10. 68 0
      ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java
  11. 2 2
      ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/examsprint/adapter/http/ExamSprintReportControllerTest.java
  12. 4 0
      docs/superpowers/plans/2026-04-27-ddd-naming-governance-first-loop.md
  13. 6 2
      docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md

+ 10 - 10
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java

@@ -4,12 +4,12 @@ import cn.yunzhixue.ability.center.examsprint.contracts.report.AchievementExamSp
 import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportResponse;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportWithUrlResponse;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportDetailResponse;
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.OutlookExamSprintReportPayload;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
 import cn.yunzhixue.ability.center.kernel.BusinessException;
 import cn.yunzhixue.ability.center.kernel.ErrorCode;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -121,7 +121,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         return new CreateExamSprintReportResponse(
                 report.reportId(),
                 report.reportType(),
-                report.generationStatus(),
+                ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
                 report.createdAt(),
                 report.expiresAt());
     }
@@ -156,7 +156,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         }
 
         ExamSprintReport generatedReport = generatedReportOption.orElseThrow();
-        if (generatedReport.generationStatus() != ExamSprintReportGenerationStatus.SUCCESS
+        if (generatedReport.generationStatus() != ReportGenerationStatus.SUCCESS
                 || generatedReport.storageObjectKey() == null) {
             log.warn(
                     "exam_sprint_report_sync_generation_unavailable reportId={} reportType={} generationStatus={} storageObjectKeyPresent={} durationMs={}",
@@ -193,7 +193,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         return new CreateExamSprintReportWithUrlResponse(
                 generatedReport.reportId(),
                 generatedReport.reportType(),
-                generatedReport.generationStatus(),
+                ExamSprintReportContractMapper.toContractStatus(generatedReport.generationStatus()),
                 generatedReport.createdAt(),
                 generatedReport.updatedAt(),
                 generatedReport.expiresAt(),
@@ -204,7 +204,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
     public ExamSprintReportDetailResponse getReport(String reportId) {
         Instant now = clock.instant();
         ExamSprintReport report = requireReport(reportId);
-        if (report.isExpiredAt(now) && report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED) {
+        if (report.isExpiredAt(now) && report.generationStatus() != ReportGenerationStatus.EXPIRED) {
             report = repository.save(report.expired(now));
             log.info(
                     "exam_sprint_report_marked_expired_on_query reportId={} reportType={} generationStatus={} expiresAt={}",
@@ -215,7 +215,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         }
 
         String downloadUrl = null;
-        if (report.generationStatus() == ExamSprintReportGenerationStatus.SUCCESS
+        if (report.generationStatus() == ReportGenerationStatus.SUCCESS
                 && !report.isExpiredAt(now)
                 && report.storageObjectKey() != null) {
             try {
@@ -243,7 +243,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         return new ExamSprintReportDetailResponse(
                 report.reportId(),
                 report.reportType(),
-                report.generationStatus(),
+                ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
                 report.createdAt(),
                 report.updatedAt(),
                 report.expiresAt(),
@@ -261,7 +261,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                 report.reportType(),
                 report.generationStatus());
         if (report.isExpiredAt(now)) {
-            if (report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED) {
+            if (report.generationStatus() != ReportGenerationStatus.EXPIRED) {
                 report = repository.save(report.expired(now));
             }
             log.warn(
@@ -271,7 +271,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                     report.generationStatus());
             throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
         }
-        if (report.generationStatus() != ExamSprintReportGenerationStatus.SUCCESS) {
+        if (report.generationStatus() != ReportGenerationStatus.SUCCESS) {
             log.warn(
                     "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=not_success",
                     report.reportId(),
@@ -337,7 +337,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                     storage.delete(report.storageObjectKey());
                     repository.save(report.expiredWithStorageCleared(now));
                     storageClearedCount++;
-                } else if (report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED) {
+                } else if (report.generationStatus() != ReportGenerationStatus.EXPIRED) {
                     repository.save(report.expired(now));
                     markedExpiredCount++;
                 }

+ 14 - 0
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java

@@ -0,0 +1,14 @@
+package cn.yunzhixue.ability.center.examsprint.application.report;
+
+import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
+
+final class ExamSprintReportContractMapper {
+
+    private ExamSprintReportContractMapper() {
+    }
+
+    static ExamSprintReportGenerationStatus toContractStatus(ReportGenerationStatus status) {
+        return status == null ? null : ExamSprintReportGenerationStatus.valueOf(status.name());
+    }
+}

+ 2 - 2
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java

@@ -1,11 +1,11 @@
 package cn.yunzhixue.ability.center.examsprint.application.report;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportPdfGenerator;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRenderer;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
@@ -49,7 +49,7 @@ public class ExamSprintReportGenerationPipeline {
             return Optional.empty();
         }
 
-        if (report.generationStatus() != ExamSprintReportGenerationStatus.PENDING) {
+        if (report.generationStatus() != ReportGenerationStatus.PENDING) {
             log.info(
                     "exam_sprint_report_generation_skipped reportId={} reportType={} generationStatus={} reason=not_pending",
                     reportId,

+ 21 - 16
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java

@@ -3,7 +3,6 @@ package cn.yunzhixue.ability.center.examsprint.application.report;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.AchievementExamSprintReportPayload;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportWithUrlResponse;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportDetailResponse;
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.OutlookExamSprintReportPayload;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
@@ -11,6 +10,7 @@ import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportPdfG
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRenderer;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
 import cn.yunzhixue.ability.center.kernel.BusinessException;
 import cn.yunzhixue.ability.center.kernel.ErrorCode;
 import com.fasterxml.jackson.databind.JsonNode;
@@ -77,7 +77,7 @@ class ExamSprintReportApplicationServiceTest {
         assertThat(response.reportId()).isNotBlank();
         ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
         assertThat(saved.reportType()).isEqualTo(ExamSprintReportType.OUTLOOK);
-        assertThat(saved.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.PENDING);
+        assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.PENDING);
     }
 
     @Test
@@ -91,7 +91,7 @@ class ExamSprintReportApplicationServiceTest {
         assertThat(response.reportId()).isNotBlank();
         ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
         assertThat(saved.reportType()).isEqualTo(ExamSprintReportType.ACHIEVEMENT);
-        assertThat(saved.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.PENDING);
+        assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.PENDING);
         assertThat(saved.payload().path("reportTitle").asText()).isEqualTo("高考英语临考突击学习成果报告");
     }
 
@@ -110,10 +110,11 @@ class ExamSprintReportApplicationServiceTest {
 
         assertThat(response.reportId()).isNotBlank();
         assertThat(response.reportType()).isEqualTo(ExamSprintReportType.OUTLOOK);
-        assertThat(response.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
+        assertThat(response.generationStatus())
+                .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus.SUCCESS);
         assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
         ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
-        assertThat(saved.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
+        assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
         assertThat(saved.storageObjectKey()).isEqualTo("exam-sprint-outlook-report-" + response.reportId() + ".pdf");
         assertThat(storage.generatedKeys).containsExactly(saved.storageObjectKey());
     }
@@ -133,10 +134,11 @@ class ExamSprintReportApplicationServiceTest {
 
         assertThat(response.reportId()).isNotBlank();
         assertThat(response.reportType()).isEqualTo(ExamSprintReportType.ACHIEVEMENT);
-        assertThat(response.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
+        assertThat(response.generationStatus())
+                .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus.SUCCESS);
         assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
         ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
-        assertThat(saved.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
+        assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
         assertThat(saved.storageObjectKey()).isEqualTo("exam-sprint-achievement-report-" + response.reportId() + ".pdf");
         assertThat(storage.generatedKeys).containsExactly(saved.storageObjectKey());
     }
@@ -187,7 +189,7 @@ class ExamSprintReportApplicationServiceTest {
         assertThat(repository.storage.values())
                 .singleElement()
                 .extracting(ExamSprintReport::generationStatus)
-                .isEqualTo(ExamSprintReportGenerationStatus.FAILED);
+                .isEqualTo(ReportGenerationStatus.FAILED);
     }
 
     @Test
@@ -238,13 +240,14 @@ class ExamSprintReportApplicationServiceTest {
         var response = service.createOutlookReport(validOutlookPayload());
 
         assertThat(response.reportId()).isNotBlank();
-        assertThat(response.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.FAILED);
+        assertThat(response.generationStatus())
+                .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus.FAILED);
         ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
-        assertThat(saved.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.FAILED);
+        assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.FAILED);
         assertThat(saved.failureReason()).isEqualTo("report_generation_dispatch_failed");
         assertThat(saved.failureReason()).doesNotContain("task-executor-7");
         assertThat(saved.failureReason()).doesNotContain("dispatcher unavailable");
-        assertThat(repository.countByStatus(ExamSprintReportGenerationStatus.PENDING)).isZero();
+        assertThat(repository.countByStatus(ReportGenerationStatus.PENDING)).isZero();
         assertThat(output.getAll())
                 .contains("exam_sprint_report_submitted")
                 .contains("reportType=OUTLOOK")
@@ -289,7 +292,8 @@ class ExamSprintReportApplicationServiceTest {
 
         var response = service.getReport("report-success");
 
-        assertThat(response.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
+        assertThat(response.generationStatus())
+                .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus.SUCCESS);
         assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/report-success/download");
         assertThat(storage.generatedKeys)
                 .containsExactly("exam-sprint-achievement-report-report-success.pdf");
@@ -321,7 +325,8 @@ class ExamSprintReportApplicationServiceTest {
 
         var response = service.getReport("report-query-url-failure");
 
-        assertThat(response.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
+        assertThat(response.generationStatus())
+                .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus.SUCCESS);
         assertThat(response.downloadUrl()).isNull();
         assertThat(output.getAll())
                 .contains("exam_sprint_report_download_url_generation_failed")
@@ -428,7 +433,7 @@ class ExamSprintReportApplicationServiceTest {
 
         ExamSprintReport saved = repository.findById("report-expired-at-boundary").orElseThrow();
         assertThat(saved.isExpiredAt(FIXED_CLOCK.instant())).isTrue();
-        assertThat(saved.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.EXPIRED);
+        assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.EXPIRED);
     }
 
     @Test
@@ -462,7 +467,7 @@ class ExamSprintReportApplicationServiceTest {
         ExamSprintReport successfulDeleteReport = repository.findById("report-delete-succeeds").orElseThrow();
         assertThat(failedDeleteReport.storageObjectKey())
                 .isEqualTo("first.pdf");
-        assertThat(successfulDeleteReport.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.EXPIRED);
+        assertThat(successfulDeleteReport.generationStatus()).isEqualTo(ReportGenerationStatus.EXPIRED);
         assertThat(successfulDeleteReport.storageObjectKey()).isNull();
         assertThat(storage.deletedKeys)
                 .contains("first.pdf")
@@ -708,7 +713,7 @@ class ExamSprintReportApplicationServiceTest {
             return storage.values().stream().filter(report -> report.isExpiredAt(instant)).toList();
         }
 
-        long countByStatus(ExamSprintReportGenerationStatus status) {
+        long countByStatus(ReportGenerationStatus status) {
             return storage.values().stream().filter(report -> report.generationStatus() == status).count();
         }
     }

+ 22 - 0
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java

@@ -0,0 +1,22 @@
+package cn.yunzhixue.ability.center.examsprint.application.report;
+
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ExamSprintReportContractMapperTest {
+
+    @Test
+    void mapsEveryDomainGenerationStatusToContractStatusWithTheSamePublicName() {
+        for (ReportGenerationStatus domainStatus : ReportGenerationStatus.values()) {
+            assertThat(ExamSprintReportContractMapper.toContractStatus(domainStatus).name())
+                    .isEqualTo(domainStatus.name());
+        }
+    }
+
+    @Test
+    void mapsNullGenerationStatusToNull() {
+        assertThat(ExamSprintReportContractMapper.toContractStatus(null)).isNull();
+    }
+}

+ 5 - 5
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java

@@ -1,11 +1,11 @@
 package cn.yunzhixue.ability.center.examsprint.application.report;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRenderer;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -46,7 +46,7 @@ class ExamSprintReportGenerationWorkerTest {
         worker.process("report-success");
 
         ExamSprintReport report = repository.findById("report-success").orElseThrow();
-        assertThat(report.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
+        assertThat(report.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
         assertThat(report.storageObjectKey()).isEqualTo("exam-sprint-outlook-report-report-success.pdf");
         assertThat(report.fileName()).isEqualTo("exam-sprint-outlook-report-report-success.pdf");
     }
@@ -97,7 +97,7 @@ class ExamSprintReportGenerationWorkerTest {
         worker.process("report-achievement");
 
         ExamSprintReport report = repository.findById("report-achievement").orElseThrow();
-        assertThat(report.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
+        assertThat(report.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
         assertThat(report.fileName()).isEqualTo("exam-sprint-achievement-report-report-achievement.pdf");
         assertThat(report.storageObjectKey()).isEqualTo("exam-sprint-achievement-report-report-achievement.pdf");
     }
@@ -120,7 +120,7 @@ class ExamSprintReportGenerationWorkerTest {
         worker.process("report-failed");
 
         ExamSprintReport report = repository.findById("report-failed").orElseThrow();
-        assertThat(report.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.FAILED);
+        assertThat(report.generationStatus()).isEqualTo(ReportGenerationStatus.FAILED);
         assertThat(report.failureReason()).isEqualTo("renderer exploded");
     }
 
@@ -169,7 +169,7 @@ class ExamSprintReportGenerationWorkerTest {
         worker.process("report-sensitive-failed");
 
         ExamSprintReport report = repository.findById("report-sensitive-failed").orElseThrow();
-        assertThat(report.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.FAILED);
+        assertThat(report.generationStatus()).isEqualTo(ReportGenerationStatus.FAILED);
         assertThat(report.failureReason()).isEqualTo("<html>SENSITIVE_FAILURE_DO_NOT_LOG</html>");
         assertThat(output.getAll())
                 .contains("exam_sprint_report_generation_failed")

+ 7 - 8
abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java

@@ -1,6 +1,5 @@
 package cn.yunzhixue.ability.center.examsprint.domain.report;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
 import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import com.fasterxml.jackson.databind.JsonNode;
 
@@ -10,7 +9,7 @@ public record ExamSprintReport(
         String reportId,
         ExamSprintReportType reportType,
         JsonNode payload,
-        ExamSprintReportGenerationStatus generationStatus,
+        ReportGenerationStatus generationStatus,
         Instant createdAt,
         Instant updatedAt,
         Instant expiresAt,
@@ -32,7 +31,7 @@ public record ExamSprintReport(
                 reportId,
                 reportType,
                 payload,
-                ExamSprintReportGenerationStatus.PENDING,
+                ReportGenerationStatus.PENDING,
                 createdAt,
                 createdAt,
                 expiresAt,
@@ -46,7 +45,7 @@ public record ExamSprintReport(
                 reportId,
                 reportType,
                 payload,
-                ExamSprintReportGenerationStatus.PROCESSING,
+                ReportGenerationStatus.PROCESSING,
                 createdAt,
                 updatedAt,
                 expiresAt,
@@ -60,7 +59,7 @@ public record ExamSprintReport(
                 reportId,
                 reportType,
                 payload,
-                ExamSprintReportGenerationStatus.SUCCESS,
+                ReportGenerationStatus.SUCCESS,
                 createdAt,
                 updatedAt,
                 expiresAt,
@@ -74,7 +73,7 @@ public record ExamSprintReport(
                 reportId,
                 reportType,
                 payload,
-                ExamSprintReportGenerationStatus.FAILED,
+                ReportGenerationStatus.FAILED,
                 createdAt,
                 updatedAt,
                 expiresAt,
@@ -88,7 +87,7 @@ public record ExamSprintReport(
                 reportId,
                 reportType,
                 payload,
-                ExamSprintReportGenerationStatus.EXPIRED,
+                ReportGenerationStatus.EXPIRED,
                 createdAt,
                 updatedAt,
                 expiresAt,
@@ -102,7 +101,7 @@ public record ExamSprintReport(
                 reportId,
                 reportType,
                 payload,
-                ExamSprintReportGenerationStatus.EXPIRED,
+                ReportGenerationStatus.EXPIRED,
                 createdAt,
                 updatedAt,
                 expiresAt,

+ 9 - 0
abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportGenerationStatus.java

@@ -0,0 +1,9 @@
+package cn.yunzhixue.ability.center.examsprint.domain.report;
+
+public enum ReportGenerationStatus {
+    PENDING,
+    PROCESSING,
+    SUCCESS,
+    FAILED,
+    EXPIRED
+}

+ 6 - 0
ability-center-runtime/pom.xml

@@ -63,5 +63,11 @@
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.tngtech.archunit</groupId>
+            <artifactId>archunit-junit5</artifactId>
+            <version>1.3.0</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>

+ 68 - 0
ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java

@@ -0,0 +1,68 @@
+package cn.yunzhixue.ability.center.architecture;
+
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.importer.ImportOption;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.lang.ArchRule;
+import com.tngtech.archunit.base.DescribedPredicate;
+
+import java.util.Set;
+
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+
+@AnalyzeClasses(
+        packages = "cn.yunzhixue.ability.center.examsprint",
+        importOptions = ImportOption.DoNotIncludeTests.class)
+class ExamSprintArchitectureTest {
+
+    private static final Set<String> CURRENT_DOMAIN_CONTRACT_DEBT = Set.of(
+            "ExamSprintReport",
+            "ExamSprintReportRenderer",
+            "ExamSprintReportStorage");
+
+    private static final Set<String> CURRENT_DOMAIN_JACKSON_DEBT = Set.of(
+            "ExamSprintReport",
+            "ExamSprintReportRenderer");
+
+    @ArchTest
+    static final ArchRule contracts_should_not_depend_on_inner_layers = noClasses()
+            .that().resideInAPackage("..contracts..")
+            .should().dependOnClassesThat().resideInAnyPackage(
+                    "..domain..",
+                    "..application..",
+                    "..infrastructure..",
+                    "..adapter.."
+            );
+
+    @ArchTest
+    static final ArchRule infrastructure_should_not_depend_on_runtime_adapters = noClasses()
+            .that().resideInAPackage("..infrastructure..")
+            .should().dependOnClassesThat().resideInAnyPackage(
+                    "..adapter..",
+                    "..configuration.."
+            );
+
+    @ArchTest
+    static final ArchRule new_domain_classes_should_not_depend_on_contracts = noClasses()
+            .that().resideInAPackage("..domain..")
+            .and(areNotNamed(CURRENT_DOMAIN_CONTRACT_DEBT))
+            .should().dependOnClassesThat().resideInAPackage("..contracts..");
+
+    @ArchTest
+    static final ArchRule new_domain_classes_should_not_depend_on_jackson = noClasses()
+            .that().resideInAPackage("..domain..")
+            .and(areNotNamed(CURRENT_DOMAIN_JACKSON_DEBT))
+            .should().dependOnClassesThat().resideInAnyPackage(
+                    "com.fasterxml.jackson.."
+            );
+
+    private static DescribedPredicate<JavaClass> areNotNamed(Set<String> simpleNames) {
+        return new DescribedPredicate<>("are not named " + simpleNames) {
+            @Override
+            public boolean test(JavaClass input) {
+                return !simpleNames.contains(input.getSimpleName());
+            }
+        };
+    }
+}

+ 2 - 2
ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/examsprint/adapter/http/ExamSprintReportControllerTest.java

@@ -1,9 +1,9 @@
 package cn.yunzhixue.ability.center.examsprint.adapter.http;
 
 import cn.yunzhixue.ability.center.AbilityCenterRuntimeApplication;
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.junit.jupiter.api.Test;
@@ -172,7 +172,7 @@ class ExamSprintReportControllerTest {
                 .andExpect(jsonPath("$.code").value("EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE"));
 
         assertThat(reportRepository.findById(reportId).orElseThrow().generationStatus())
-                .isEqualTo(ExamSprintReportGenerationStatus.EXPIRED);
+                .isEqualTo(ReportGenerationStatus.EXPIRED);
     }
 
     private String validRequestJson() {

+ 4 - 0
docs/superpowers/plans/2026-04-27-ddd-naming-governance-first-loop.md

@@ -12,6 +12,10 @@
 
 > **Execution note:** Do not create git commits unless the user explicitly asks for commits. Treat each “commit checkpoint” as a local verification checkpoint until commit permission is given.
 
+> **Execution note after first-loop implementation:** Task 2/3 verification was handled as a continuous loop because application `testCompile` ordering and local artifact issues could leave stale module artifacts when commands were run without `-am`. A runtime test also had a stale domain status assertion and was updated minimally. In submodule verification scenarios, prefer commands with `-am` to rebuild required modules and avoid stale artifacts.
+
+> **Execution status:** First loop implemented in this worktree. The checklist remains the original plan for traceability and is intentionally left unchecked. Final verification after the Task 7 docs update: `mvn -q test` passed.
+
 ## Target File Structure
 
 ### Create

+ 6 - 2
docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md

@@ -1,6 +1,6 @@
 # DDD Naming and Architecture Governance Design
 
-> Status: Draft written from conversation approval, pending user review before implementation.
+> Status: First governance loop implemented; remaining `domain -> contracts` debt is `ReportType`. The next `ReportType` loop is recommended but not yet implemented.
 
 ## Goal
 
@@ -322,7 +322,7 @@ Acceptance criteria:
 
 | Debt | Current reason | Exit condition |
 | --- | --- | --- |
-| `domain -> contracts` | domain reuses contract enums | domain owns equivalent concepts and application maps to contracts |
+| `domain -> contracts` | `ReportType` still reuses API enum after `ReportGenerationStatus` migration | domain-owned `ReportType` plus application mapper |
 | `domain -> jackson` | `ExamSprintReport` and renderer use `JsonNode` payload | domain uses strongly named content/value objects |
 | Payload records contain business behavior | request payloads currently host consistency methods | invariants move to domain value objects |
 | `ExamSprintReport` has mixed semantics | current model stores generation state and file references | model is renamed or split around generation semantics |
@@ -395,6 +395,10 @@ Why this loop first:
 - it creates a repeatable mapper pattern for future `ReportType` and payload migrations;
 - it gives the architecture tests a concrete baseline and a concrete debt reduction.
 
+First loop result: `ReportGenerationStatus` is now domain-owned; the application boundary maps it back to public contracts status, architecture tests baseline the remaining debt, and the next `ReportType` loop has not yet been implemented.
+
+Next recommended loop: migrate `ReportType` into domain, remove `exam-sprint-domain` dependency on `exam-sprint-contracts`, and tighten the architecture allowlist accordingly.
+
 The detailed implementation plan is in:
 
 ```text