Explorar o código

Merge branch 'refactor/ddd命名治理二轮-report-type' of jyx/dcjxb.microservice into master

金逸霄 hai 2 semanas
pai
achega
9f4817c563
Modificáronse 20 ficheiros con 1054 adicións e 91 borrados
  1. 10 10
      abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java
  2. 22 0
      abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java
  3. 16 15
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
  4. 40 0
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java
  5. 20 20
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java
  6. 0 5
      abilities/exam-sprint/domain/pom.xml
  7. 2 3
      abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java
  8. 1 2
      abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportRenderer.java
  9. 1 3
      abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportStorage.java
  10. 6 0
      abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportType.java
  11. 3 3
      abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.java
  12. 3 3
      abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java
  13. 2 2
      abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorage.java
  14. 2 2
      abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/InMemoryExamSprintReportStorage.java
  15. 3 3
      abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRendererTest.java
  16. 3 3
      abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java
  17. 2 2
      abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorageTest.java
  18. 1 7
      ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java
  19. 906 0
      docs/superpowers/plans/2026-04-28-ddd-naming-governance-report-type-loop.md
  20. 11 8
      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.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.examsprint.domain.report.ReportType;
 import cn.yunzhixue.ability.center.kernel.BusinessException;
 import cn.yunzhixue.ability.center.kernel.ErrorCode;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -67,28 +67,28 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
     @Override
     public CreateExamSprintReportResponse createOutlookReport(JsonNode payload) {
         validateOutlookPayload(payload);
-        return submitReportGeneration(ExamSprintReportType.OUTLOOK, payload);
+        return submitReportGeneration(ReportType.OUTLOOK, payload);
     }
 
     @Override
     public CreateExamSprintReportResponse createAchievementReport(JsonNode payload) {
         validateAchievementPayload(payload);
-        return submitReportGeneration(ExamSprintReportType.ACHIEVEMENT, payload);
+        return submitReportGeneration(ReportType.ACHIEVEMENT, payload);
     }
 
     @Override
     public CreateExamSprintReportWithUrlResponse createOutlookReportSync(JsonNode payload) {
         validateOutlookPayload(payload);
-        return submitReportGenerationSync(ExamSprintReportType.OUTLOOK, payload);
+        return submitReportGenerationSync(ReportType.OUTLOOK, payload);
     }
 
     @Override
     public CreateExamSprintReportWithUrlResponse createAchievementReportSync(JsonNode payload) {
         validateAchievementPayload(payload);
-        return submitReportGenerationSync(ExamSprintReportType.ACHIEVEMENT, payload);
+        return submitReportGenerationSync(ReportType.ACHIEVEMENT, payload);
     }
 
-    private CreateExamSprintReportResponse submitReportGeneration(ExamSprintReportType reportType, JsonNode payload) {
+    private CreateExamSprintReportResponse submitReportGeneration(ReportType reportType, JsonNode payload) {
         Instant now = clock.instant();
         ExamSprintReport report = ExamSprintReport.pending(
                 UUID.randomUUID().toString(),
@@ -120,14 +120,14 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         }
         return new CreateExamSprintReportResponse(
                 report.reportId(),
-                report.reportType(),
+                ExamSprintReportContractMapper.toContractReportType(report.reportType()),
                 ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
                 report.createdAt(),
                 report.expiresAt());
     }
 
     private CreateExamSprintReportWithUrlResponse submitReportGenerationSync(
-            ExamSprintReportType reportType,
+            ReportType reportType,
             JsonNode payload) {
         long startedNanos = System.nanoTime();
         Instant now = clock.instant();
@@ -192,7 +192,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                 generatedReport.storageObjectKey());
         return new CreateExamSprintReportWithUrlResponse(
                 generatedReport.reportId(),
-                generatedReport.reportType(),
+                ExamSprintReportContractMapper.toContractReportType(generatedReport.reportType()),
                 ExamSprintReportContractMapper.toContractStatus(generatedReport.generationStatus()),
                 generatedReport.createdAt(),
                 generatedReport.updatedAt(),
@@ -242,7 +242,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
 
         return new ExamSprintReportDetailResponse(
                 report.reportId(),
-                report.reportType(),
+                ExamSprintReportContractMapper.toContractReportType(report.reportType()),
                 ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
                 report.createdAt(),
                 report.updatedAt(),

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

@@ -1,7 +1,9 @@
 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.ReportGenerationStatus;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 
 final class ExamSprintReportContractMapper {
 
@@ -11,4 +13,24 @@ final class ExamSprintReportContractMapper {
     static ExamSprintReportGenerationStatus toContractStatus(ReportGenerationStatus status) {
         return status == null ? null : ExamSprintReportGenerationStatus.valueOf(status.name());
     }
+
+    static ExamSprintReportType toContractReportType(ReportType reportType) {
+        if (reportType == null) {
+            return null;
+        }
+        return switch (reportType) {
+            case OUTLOOK -> ExamSprintReportType.OUTLOOK;
+            case ACHIEVEMENT -> ExamSprintReportType.ACHIEVEMENT;
+        };
+    }
+
+    static ReportType toDomainReportType(ExamSprintReportType reportType) {
+        if (reportType == null) {
+            return null;
+        }
+        return switch (reportType) {
+            case OUTLOOK -> ReportType.OUTLOOK;
+            case ACHIEVEMENT -> ReportType.ACHIEVEMENT;
+        };
+    }
 }

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

@@ -11,6 +11,7 @@ import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRend
 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.examsprint.domain.report.ReportType;
 import cn.yunzhixue.ability.center.kernel.BusinessException;
 import cn.yunzhixue.ability.center.kernel.ErrorCode;
 import com.fasterxml.jackson.databind.JsonNode;
@@ -76,7 +77,7 @@ class ExamSprintReportApplicationServiceTest {
 
         assertThat(response.reportId()).isNotBlank();
         ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
-        assertThat(saved.reportType()).isEqualTo(ExamSprintReportType.OUTLOOK);
+        assertThat(saved.reportType()).isEqualTo(ReportType.OUTLOOK);
         assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.PENDING);
     }
 
@@ -90,7 +91,7 @@ class ExamSprintReportApplicationServiceTest {
 
         assertThat(response.reportId()).isNotBlank();
         ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
-        assertThat(saved.reportType()).isEqualTo(ExamSprintReportType.ACHIEVEMENT);
+        assertThat(saved.reportType()).isEqualTo(ReportType.ACHIEVEMENT);
         assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.PENDING);
         assertThat(saved.payload().path("reportTitle").asText()).isEqualTo("高考英语临考突击学习成果报告");
     }
@@ -279,7 +280,7 @@ class ExamSprintReportApplicationServiceTest {
         TestStorage storage = new TestStorage();
         ExamSprintReport report = ExamSprintReport.pending(
                         "report-success",
-                        ExamSprintReportType.ACHIEVEMENT,
+                        ReportType.ACHIEVEMENT,
                         validAchievementPayload(),
                         FIXED_CLOCK.instant().minusSeconds(120),
                         FIXED_CLOCK.instant().plusSeconds(3600))
@@ -311,7 +312,7 @@ class ExamSprintReportApplicationServiceTest {
         TestStorage storage = new TestStorage();
         ExamSprintReport report = ExamSprintReport.pending(
                         "report-query-url-failure",
-                        ExamSprintReportType.ACHIEVEMENT,
+                        ReportType.ACHIEVEMENT,
                         validAchievementPayload(),
                         FIXED_CLOCK.instant().minusSeconds(120),
                         FIXED_CLOCK.instant().plusSeconds(3600))
@@ -341,7 +342,7 @@ class ExamSprintReportApplicationServiceTest {
         TestStorage storage = new TestStorage();
         repository.save(ExamSprintReport.pending(
                 "report-expired",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant().minusSeconds(600),
                 FIXED_CLOCK.instant().minusSeconds(1)).success(
@@ -368,7 +369,7 @@ class ExamSprintReportApplicationServiceTest {
         TestStorage storage = new TestStorage();
         repository.save(ExamSprintReport.pending(
                 "report-missing-content",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant().minusSeconds(600),
                 FIXED_CLOCK.instant().plusSeconds(3600)).success(
@@ -396,7 +397,7 @@ class ExamSprintReportApplicationServiceTest {
         TestStorage storage = new TestStorage();
         repository.save(ExamSprintReport.pending(
                 "report-storage-download-failure",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant().minusSeconds(600),
                 FIXED_CLOCK.instant().plusSeconds(3600)).success(
@@ -423,7 +424,7 @@ class ExamSprintReportApplicationServiceTest {
         TestRepository repository = new TestRepository();
         repository.save(ExamSprintReport.pending(
                 "report-expired-at-boundary",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant().minusSeconds(600),
                 FIXED_CLOCK.instant()));
@@ -442,7 +443,7 @@ class ExamSprintReportApplicationServiceTest {
         TestStorage storage = new TestStorage();
         repository.save(ExamSprintReport.pending(
                 "report-delete-fails",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant().minusSeconds(600),
                 FIXED_CLOCK.instant().minusSeconds(1)).success(
@@ -451,7 +452,7 @@ class ExamSprintReportApplicationServiceTest {
                 "first.pdf"));
         repository.save(ExamSprintReport.pending(
                 "report-delete-succeeds",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant().minusSeconds(600),
                 FIXED_CLOCK.instant().minusSeconds(1)).success(
@@ -728,7 +729,7 @@ class ExamSprintReportApplicationServiceTest {
         @Override
         public StoredExamSprintReportFile upload(
                 String reportId,
-                ExamSprintReportType reportType,
+                ReportType reportType,
                 String fileName,
                 byte[] pdfBytes,
                 Instant expiresAt) {
@@ -789,8 +790,8 @@ class ExamSprintReportApplicationServiceTest {
     private static class PreviewTestRenderer implements ExamSprintReportRenderer {
 
         @Override
-        public boolean supports(ExamSprintReportType reportType) {
-            return reportType == ExamSprintReportType.OUTLOOK || reportType == ExamSprintReportType.ACHIEVEMENT;
+        public boolean supports(ReportType reportType) {
+            return reportType == ReportType.OUTLOOK || reportType == ReportType.ACHIEVEMENT;
         }
 
         @Override
@@ -802,8 +803,8 @@ class ExamSprintReportApplicationServiceTest {
     private static class FailingRenderer implements ExamSprintReportRenderer {
 
         @Override
-        public boolean supports(ExamSprintReportType reportType) {
-            return reportType == ExamSprintReportType.OUTLOOK;
+        public boolean supports(ReportType reportType) {
+            return reportType == ReportType.OUTLOOK;
         }
 
         @Override

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

@@ -1,6 +1,8 @@
 package cn.yunzhixue.ability.center.examsprint.application.report;
 
+import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -19,4 +21,42 @@ class ExamSprintReportContractMapperTest {
     void mapsNullGenerationStatusToNull() {
         assertThat(ExamSprintReportContractMapper.toContractStatus(null)).isNull();
     }
+
+    @Test
+    void mapsKnownDomainReportTypesToExplicitContractReportTypes() {
+        assertThat(ExamSprintReportContractMapper.toContractReportType(ReportType.OUTLOOK))
+                .isEqualTo(ExamSprintReportType.OUTLOOK);
+        assertThat(ExamSprintReportContractMapper.toContractReportType(ReportType.ACHIEVEMENT))
+                .isEqualTo(ExamSprintReportType.ACHIEVEMENT);
+    }
+
+    @Test
+    void mapsKnownContractReportTypesToExplicitDomainReportTypes() {
+        assertThat(ExamSprintReportContractMapper.toDomainReportType(ExamSprintReportType.OUTLOOK))
+                .isEqualTo(ReportType.OUTLOOK);
+        assertThat(ExamSprintReportContractMapper.toDomainReportType(ExamSprintReportType.ACHIEVEMENT))
+                .isEqualTo(ReportType.ACHIEVEMENT);
+    }
+
+    @Test
+    void mapsEveryDomainReportTypeToContractReportTypeWithTheSamePublicName() {
+        for (ReportType domainReportType : ReportType.values()) {
+            assertThat(ExamSprintReportContractMapper.toContractReportType(domainReportType).name())
+                    .isEqualTo(domainReportType.name());
+        }
+    }
+
+    @Test
+    void mapsEveryContractReportTypeToDomainReportTypeWithTheSamePublicName() {
+        for (ExamSprintReportType contractReportType : ExamSprintReportType.values()) {
+            assertThat(ExamSprintReportContractMapper.toDomainReportType(contractReportType).name())
+                    .isEqualTo(contractReportType.name());
+        }
+    }
+
+    @Test
+    void mapsNullReportTypeToNullInBothDirections() {
+        assertThat(ExamSprintReportContractMapper.toContractReportType(null)).isNull();
+        assertThat(ExamSprintReportContractMapper.toDomainReportType(null)).isNull();
+    }
 }

+ 20 - 20
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.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 cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -36,7 +36,7 @@ class ExamSprintReportGenerationWorkerTest {
         TestRepository repository = new TestRepository();
         repository.save(ExamSprintReport.pending(
                 "report-success",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant(),
                 FIXED_CLOCK.instant().plusSeconds(86400)));
@@ -56,7 +56,7 @@ class ExamSprintReportGenerationWorkerTest {
         TestRepository repository = new TestRepository();
         repository.save(ExamSprintReport.pending(
                 "report-log-success",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant(),
                 FIXED_CLOCK.instant().plusSeconds(86400)));
@@ -84,14 +84,14 @@ class ExamSprintReportGenerationWorkerTest {
         TestRepository repository = new TestRepository();
         repository.save(ExamSprintReport.pending(
                 "report-achievement",
-                ExamSprintReportType.ACHIEVEMENT,
+                ReportType.ACHIEVEMENT,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant(),
                 FIXED_CLOCK.instant().plusSeconds(86400)));
         TestStorage storage = new TestStorage();
         ExamSprintReportGenerationWorker worker = createWorker(
                 repository,
-                List.of(new TestRenderer(ExamSprintReportType.ACHIEVEMENT)),
+                List.of(new TestRenderer(ReportType.ACHIEVEMENT)),
                 storage);
 
         worker.process("report-achievement");
@@ -107,7 +107,7 @@ class ExamSprintReportGenerationWorkerTest {
         TestRepository repository = new TestRepository();
         repository.save(ExamSprintReport.pending(
                 "report-failed",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant(),
                 FIXED_CLOCK.instant().plusSeconds(86400)));
@@ -129,7 +129,7 @@ class ExamSprintReportGenerationWorkerTest {
         TestRepository repository = new TestRepository();
         repository.save(ExamSprintReport.pending(
                 "report-log-failed",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant(),
                 FIXED_CLOCK.instant().plusSeconds(86400)));
@@ -156,7 +156,7 @@ class ExamSprintReportGenerationWorkerTest {
         TestRepository repository = new TestRepository();
         repository.save(ExamSprintReport.pending(
                 "report-sensitive-failed",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant(),
                 FIXED_CLOCK.instant().plusSeconds(86400)));
@@ -183,7 +183,7 @@ class ExamSprintReportGenerationWorkerTest {
         TestRepository repository = new TestRepository();
         repository.save(ExamSprintReport.pending(
                 "report-deleted",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 OBJECT_MAPPER.createObjectNode(),
                 FIXED_CLOCK.instant(),
                 FIXED_CLOCK.instant().plusSeconds(86400)));
@@ -213,18 +213,18 @@ class ExamSprintReportGenerationWorkerTest {
     }
 
     private static class TestRenderer implements ExamSprintReportRenderer {
-        private final ExamSprintReportType supportedReportType;
+        private final ReportType supportedReportType;
 
         TestRenderer() {
-            this(ExamSprintReportType.OUTLOOK);
+            this(ReportType.OUTLOOK);
         }
 
-        TestRenderer(ExamSprintReportType supportedReportType) {
+        TestRenderer(ReportType supportedReportType) {
             this.supportedReportType = supportedReportType;
         }
 
         @Override
-        public boolean supports(ExamSprintReportType reportType) {
+        public boolean supports(ReportType reportType) {
             return reportType == supportedReportType;
         }
 
@@ -236,8 +236,8 @@ class ExamSprintReportGenerationWorkerTest {
 
     private static class FailingRenderer implements ExamSprintReportRenderer {
         @Override
-        public boolean supports(ExamSprintReportType reportType) {
-            return reportType == ExamSprintReportType.OUTLOOK;
+        public boolean supports(ReportType reportType) {
+            return reportType == ReportType.OUTLOOK;
         }
 
         @Override
@@ -248,8 +248,8 @@ class ExamSprintReportGenerationWorkerTest {
 
     private static class SensitiveHtmlRenderer implements ExamSprintReportRenderer {
         @Override
-        public boolean supports(ExamSprintReportType reportType) {
-            return reportType == ExamSprintReportType.OUTLOOK;
+        public boolean supports(ReportType reportType) {
+            return reportType == ReportType.OUTLOOK;
         }
 
         @Override
@@ -260,8 +260,8 @@ class ExamSprintReportGenerationWorkerTest {
 
     private static class SensitiveFailureRenderer implements ExamSprintReportRenderer {
         @Override
-        public boolean supports(ExamSprintReportType reportType) {
-            return reportType == ExamSprintReportType.OUTLOOK;
+        public boolean supports(ReportType reportType) {
+            return reportType == ReportType.OUTLOOK;
         }
 
         @Override
@@ -300,7 +300,7 @@ class ExamSprintReportGenerationWorkerTest {
         @Override
         public StoredExamSprintReportFile upload(
                 String reportId,
-                ExamSprintReportType reportType,
+                ReportType reportType,
                 String fileName,
                 byte[] pdfBytes,
                 Instant expiresAt) {

+ 0 - 5
abilities/exam-sprint/domain/pom.xml

@@ -16,11 +16,6 @@
             <artifactId>ability-center-kernel</artifactId>
             <version>${project.version}</version>
         </dependency>
-        <dependency>
-            <groupId>cn.yunzhixue</groupId>
-            <artifactId>exam-sprint-contracts</artifactId>
-            <version>${project.version}</version>
-        </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>

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

@@ -1,13 +1,12 @@
 package cn.yunzhixue.ability.center.examsprint.domain.report;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import com.fasterxml.jackson.databind.JsonNode;
 
 import java.time.Instant;
 
 public record ExamSprintReport(
         String reportId,
-        ExamSprintReportType reportType,
+        ReportType reportType,
         JsonNode payload,
         ReportGenerationStatus generationStatus,
         Instant createdAt,
@@ -23,7 +22,7 @@ public record ExamSprintReport(
 
     public static ExamSprintReport pending(
             String reportId,
-            ExamSprintReportType reportType,
+            ReportType reportType,
             JsonNode payload,
             Instant createdAt,
             Instant expiresAt) {

+ 1 - 2
abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportRenderer.java

@@ -1,13 +1,12 @@
 package cn.yunzhixue.ability.center.examsprint.domain.report;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import com.fasterxml.jackson.databind.JsonNode;
 
 import java.time.Instant;
 
 public interface ExamSprintReportRenderer {
 
-    boolean supports(ExamSprintReportType reportType);
+    boolean supports(ReportType reportType);
 
     String render(JsonNode payload, Instant generatedAt);
 }

+ 1 - 3
abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportStorage.java

@@ -1,7 +1,5 @@
 package cn.yunzhixue.ability.center.examsprint.domain.report;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
-
 import java.net.URI;
 import java.time.Duration;
 import java.time.Instant;
@@ -11,7 +9,7 @@ public interface ExamSprintReportStorage {
 
     StoredExamSprintReportFile upload(
             String reportId,
-            ExamSprintReportType reportType,
+            ReportType reportType,
             String fileName,
             byte[] pdfBytes,
             Instant expiresAt);

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

@@ -0,0 +1,6 @@
+package cn.yunzhixue.ability.center.examsprint.domain.report;
+
+public enum ReportType {
+    OUTLOOK,
+    ACHIEVEMENT
+}

+ 3 - 3
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.java

@@ -1,8 +1,8 @@
 package cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.achievement;
 
 import cn.yunzhixue.ability.center.examsprint.contracts.report.AchievementExamSprintReportPayload;
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRenderer;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -41,8 +41,8 @@ public class ClasspathAchievementExamSprintReportRenderer implements ExamSprintR
     }
 
     @Override
-    public boolean supports(ExamSprintReportType reportType) {
-        return reportType == ExamSprintReportType.ACHIEVEMENT;
+    public boolean supports(ReportType reportType) {
+        return reportType == ReportType.ACHIEVEMENT;
     }
 
     @Override

+ 3 - 3
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java

@@ -1,8 +1,8 @@
 package cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.outlook;
 
-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.ExamSprintReportRenderer;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.core.io.ClassPathResource;
@@ -37,8 +37,8 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
     }
 
     @Override
-    public boolean supports(ExamSprintReportType reportType) {
-        return reportType == ExamSprintReportType.OUTLOOK;
+    public boolean supports(ReportType reportType) {
+        return reportType == ReportType.OUTLOOK;
     }
 
     @Override

+ 2 - 2
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorage.java

@@ -1,7 +1,7 @@
 package cn.yunzhixue.ability.center.examsprint.infrastructure.report.storage;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import com.azure.storage.blob.BlobClient;
 import com.azure.storage.blob.BlobContainerClient;
 import com.azure.storage.blob.BlobContainerClientBuilder;
@@ -56,7 +56,7 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
     @Override
     public StoredExamSprintReportFile upload(
             String reportId,
-            ExamSprintReportType reportType,
+            ReportType reportType,
             String fileName,
             byte[] pdfBytes,
             Instant expiresAt) {

+ 2 - 2
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/InMemoryExamSprintReportStorage.java

@@ -1,7 +1,7 @@
 package cn.yunzhixue.ability.center.examsprint.infrastructure.report.storage;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
 import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 
@@ -21,7 +21,7 @@ public class InMemoryExamSprintReportStorage implements ExamSprintReportStorage
     @Override
     public StoredExamSprintReportFile upload(
             String reportId,
-            ExamSprintReportType reportType,
+            ReportType reportType,
             String fileName,
             byte[] pdfBytes,
             Instant expiresAt) {

+ 3 - 3
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRendererTest.java

@@ -1,6 +1,6 @@
 package cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.achievement;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -100,8 +100,8 @@ class ClasspathAchievementExamSprintReportRendererTest {
     void supportsOnlyAchievementReportType() {
         ClasspathAchievementExamSprintReportRenderer renderer = new ClasspathAchievementExamSprintReportRenderer(OBJECT_MAPPER);
 
-        assertThat(renderer.supports(ExamSprintReportType.ACHIEVEMENT)).isTrue();
-        assertThat(renderer.supports(ExamSprintReportType.OUTLOOK)).isFalse();
+        assertThat(renderer.supports(ReportType.ACHIEVEMENT)).isTrue();
+        assertThat(renderer.supports(ReportType.OUTLOOK)).isFalse();
     }
 
     @Test

+ 3 - 3
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java

@@ -1,6 +1,6 @@
 package cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.outlook;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -112,8 +112,8 @@ class ClasspathOutlookExamSprintReportRendererTest {
     void supportsOnlyOutlookReportType() {
         ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
 
-        assertThat(renderer.supports(ExamSprintReportType.OUTLOOK)).isTrue();
-        assertThat(renderer.supports(ExamSprintReportType.ACHIEVEMENT)).isFalse();
+        assertThat(renderer.supports(ReportType.OUTLOOK)).isTrue();
+        assertThat(renderer.supports(ReportType.ACHIEVEMENT)).isFalse();
     }
 
     @Test

+ 2 - 2
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorageTest.java

@@ -1,6 +1,6 @@
 package cn.yunzhixue.ability.center.examsprint.infrastructure.report.storage;
 
-import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import com.azure.storage.blob.BlobClient;
 import com.azure.storage.blob.BlobContainerClient;
 import org.junit.jupiter.api.Test;
@@ -148,7 +148,7 @@ class AzureBlobExamSprintReportStorageTest {
 
         storage.upload(
                 "report-123",
-                ExamSprintReportType.OUTLOOK,
+                ReportType.OUTLOOK,
                 "file.pdf",
                 new byte[]{1, 2, 3},
                 Instant.parse("2026-01-10T00:00:00Z"));

+ 1 - 7
ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java

@@ -16,11 +16,6 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
         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");
@@ -44,9 +39,8 @@ class ExamSprintArchitectureTest {
             );
 
     @ArchTest
-    static final ArchRule new_domain_classes_should_not_depend_on_contracts = noClasses()
+    static final ArchRule domain_should_not_depend_on_contracts = noClasses()
             .that().resideInAPackage("..domain..")
-            .and(areNotNamed(CURRENT_DOMAIN_CONTRACT_DEBT))
             .should().dependOnClassesThat().resideInAPackage("..contracts..");
 
     @ArchTest

+ 906 - 0
docs/superpowers/plans/2026-04-28-ddd-naming-governance-report-type-loop.md

@@ -0,0 +1,906 @@
+# DDD Naming Governance ReportType Loop Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Complete the second DDD naming governance loop by moving report type ownership into `domain`, keeping the public contracts enum stable, and removing the `exam-sprint-domain -> exam-sprint-contracts` dependency.
+
+**Architecture:** `domain` owns the business category enum `ReportType`; `contracts` keeps the public API enum `ExamSprintReportType`; `application` maps explicitly between the two at API boundaries. `domain` model and domain ports use the domain enum, while response DTOs and runtime HTTP JSON continue to expose the existing contract enum names.
+
+**Tech Stack:** Java 17, Maven multi-module build, Spring Boot 3.3.5, JUnit 5, AssertJ, ArchUnit, existing exam-sprint application/infrastructure/runtime tests.
+
+---
+
+> **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:** This plan is intentionally limited to `ReportType`. Do not migrate `JsonNode` payloads, do not move `Storage` / `Renderer` / `PdfGenerator`, do not split `DefaultExamSprintReportApplicationService`, and do not change HTTP endpoint paths, JSON field names, status codes, or public enum literals.
+
+> **Initial worktree baseline:** The isolated worktree is `/Users/exiao/Codes/dcjxb.microservice/.worktrees/refactor-ddd-report-type-loop` on branch `refactor/ddd命名治理二轮-report-type`. Before writing this plan, `mvn -q test` was run in this worktree and completed without a tool-reported failure.
+
+## Target File Structure
+
+### Create
+
+- `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportType.java`
+  - Domain-owned report category enum with the same current literals as the public contract enum.
+
+### Modify
+
+- `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java`
+  - Change `reportType` field and `pending(...)` factory parameter from contract `ExamSprintReportType` to domain `ReportType`.
+- `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportRenderer.java`
+  - Change `supports(...)` to accept domain `ReportType`; keep `JsonNode` payload unchanged.
+- `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportStorage.java`
+  - Change `upload(...)` report type parameter to domain `ReportType`.
+- `abilities/exam-sprint/domain/pom.xml`
+  - Remove the `exam-sprint-contracts` dependency; keep `jackson-databind` because payload migration is out of scope.
+- `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java`
+  - Add explicit `ReportType -> ExamSprintReportType` and `ExamSprintReportType -> ReportType` mapping methods.
+- `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java`
+  - Use domain `ReportType` internally and map report type back to contract enum when constructing response DTOs.
+- `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java`
+  - Cover all report type mappings and null strategy.
+- `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
+  - Assert domain entity type with domain `ReportType`; keep response assertions on contract `ExamSprintReportType`.
+- `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java`
+  - Update domain report construction and renderer/storage test doubles to use domain `ReportType`.
+- `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/InMemoryExamSprintReportStorage.java`
+  - Implement the updated domain storage port using domain `ReportType`.
+- `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorage.java`
+  - Implement the updated domain storage port using domain `ReportType`; keep metadata value `reportType.name()`.
+- `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java`
+  - Use domain `ReportType.OUTLOOK` in `supports(...)`; keep contract payload DTO usage unchanged.
+- `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.java`
+  - Use domain `ReportType.ACHIEVEMENT` in `supports(...)`; keep contract payload DTO usage unchanged.
+- `abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorageTest.java`
+  - Update storage upload call to domain `ReportType` while preserving metadata assertions.
+- `abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java`
+  - Update renderer support assertions to domain `ReportType`.
+- `abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRendererTest.java`
+  - Update renderer support assertions to domain `ReportType`.
+- `ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java`
+  - Remove the domain-to-contract allowlist and make `domain -> contracts` a hard rule; keep Jackson allowlist unchanged.
+- `docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md`
+  - Record that `ReportType` migration cleared `domain -> contracts`; identify Jackson / payload governance as the next loop.
+
+### Keep unchanged
+
+- `abilities/exam-sprint/contracts/src/main/java/cn/yunzhixue/ability/center/examsprint/contracts/report/ExamSprintReportType.java`
+  - Must remain public enum `ExamSprintReportType { OUTLOOK, ACHIEVEMENT }`.
+- `abilities/exam-sprint/contracts/src/main/java/cn/yunzhixue/ability/center/examsprint/contracts/report/*Response.java`
+  - Response DTO report type fields stay `ExamSprintReportType`.
+- `abilities/exam-sprint/infrastructure/pom.xml`
+  - Keep `exam-sprint-contracts` because renderers still use contract payload DTOs; payload migration is out of scope.
+- `ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/examsprint/adapter/http/ExamSprintReportControllerWebMvcTest.java`
+  - Should still use contract `ExamSprintReportType` and verify public JSON strings.
+
+## Task 1: Establish current baseline and identify remaining debt
+
+**Files:**
+- Verify: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/*.java`
+- Verify: `abilities/exam-sprint/domain/pom.xml`
+- Verify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/*.java`
+- Verify: `ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java`
+
+- [ ] **Step 1: Confirm the branch and clean worktree**
+
+Run:
+
+```bash
+git status -sb
+git status --short
+```
+
+Expected: branch is `refactor/ddd命名治理二轮-report-type`; before implementation edits, `git status --short` should show only this new plan document as untracked.
+
+- [ ] **Step 2: Search current domain-to-contract report references**
+
+Run:
+
+```bash
+rg "contracts\.report" "abilities/exam-sprint/domain/src/main/java"
+```
+
+Expected current debt before migration:
+
+```text
+abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java:import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
+abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportRenderer.java:import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
+abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportStorage.java:import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
+```
+
+- [ ] **Step 3: Search all current `ExamSprintReportType` usages**
+
+Run:
+
+```bash
+rg "ExamSprintReportType" "abilities/exam-sprint"
+```
+
+Expected: matches in domain, application, infrastructure, contracts, and tests. Domain matches should be limited to `ExamSprintReport`, `ExamSprintReportRenderer`, and `ExamSprintReportStorage`.
+
+- [ ] **Step 4: Run domain baseline tests**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/domain -am test
+```
+
+Expected: PASS. If it fails before editing, stop and record the exact failing module/test so the migration is not blamed for a pre-existing failure.
+
+- [ ] **Step 5: Run application baseline tests with `-am`**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/application -am test
+```
+
+Expected: PASS. If it fails before editing, stop and record the exact failing module/test.
+
+- [ ] **Checkpoint**
+
+Record in the session summary: current remaining `domain -> contracts` debt is `ExamSprintReportType` only, and the initial full-worktree `mvn -q test` baseline already completed without a tool-reported failure.
+
+## Task 2: Add domain-owned `ReportType` and failing mapper tests
+
+**Files:**
+- Create: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportType.java`
+- Modify: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java`
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java`
+
+- [ ] **Step 1: Add failing report type mapper tests first**
+
+Modify `ExamSprintReportContractMapperTest.java` so it imports both the domain and contract report type concepts and contains these tests in addition to the existing status tests:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+```java
+    @Test
+    void mapsEveryDomainReportTypeToContractReportTypeWithTheSamePublicName() {
+        for (ReportType domainReportType : ReportType.values()) {
+            assertThat(ExamSprintReportContractMapper.toContractReportType(domainReportType).name())
+                    .isEqualTo(domainReportType.name());
+        }
+    }
+
+    @Test
+    void mapsEveryContractReportTypeToDomainReportTypeWithTheSamePublicName() {
+        for (ExamSprintReportType contractReportType : ExamSprintReportType.values()) {
+            assertThat(ExamSprintReportContractMapper.toDomainReportType(contractReportType).name())
+                    .isEqualTo(contractReportType.name());
+        }
+    }
+
+    @Test
+    void mapsNullReportTypeToNullInBothDirections() {
+        assertThat(ExamSprintReportContractMapper.toContractReportType(null)).isNull();
+        assertThat(ExamSprintReportContractMapper.toDomainReportType(null)).isNull();
+    }
+```
+
+- [ ] **Step 2: Run mapper test and verify it fails before implementation**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/application -am -Dtest=ExamSprintReportContractMapperTest test
+```
+
+Expected: FAIL because `ReportType`, `toContractReportType(...)`, or `toDomainReportType(...)` is not implemented yet.
+
+- [ ] **Step 3: Create domain enum with public-compatible literals**
+
+Create `ReportType.java`:
+
+```java
+package cn.yunzhixue.ability.center.examsprint.domain.report;
+
+public enum ReportType {
+    OUTLOOK,
+    ACHIEVEMENT
+}
+```
+
+- [ ] **Step 4: Implement explicit mapper methods**
+
+Modify `ExamSprintReportContractMapper.java` imports:
+
+```java
+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.ReportGenerationStatus;
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+Add these package-private methods after `toContractStatus(...)`:
+
+```java
+    static ExamSprintReportType toContractReportType(ReportType reportType) {
+        return reportType == null ? null : ExamSprintReportType.valueOf(reportType.name());
+    }
+
+    static ReportType toDomainReportType(ExamSprintReportType reportType) {
+        return reportType == null ? null : ReportType.valueOf(reportType.name());
+    }
+```
+
+- [ ] **Step 5: Run mapper test again**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/application -am -Dtest=ExamSprintReportContractMapperTest test
+```
+
+Expected: PASS.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+test(exam-sprint): 覆盖报告类型边界映射
+```
+
+## Task 3: Migrate domain model and domain ports to domain `ReportType`
+
+**Files:**
+- Modify: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java`
+- Modify: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportRenderer.java`
+- Modify: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReportStorage.java`
+
+- [ ] **Step 1: Run domain tests before editing this task**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/domain -am test
+```
+
+Expected: PASS from the previous task’s state.
+
+- [ ] **Step 2: Update `ExamSprintReport` type ownership**
+
+In `ExamSprintReport.java`, remove:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
+```
+
+Change the record field and factory parameter:
+
+```java
+public record ExamSprintReport(
+        String reportId,
+        ReportType reportType,
+        JsonNode payload,
+        ReportGenerationStatus generationStatus,
+        Instant createdAt,
+        Instant updatedAt,
+        Instant expiresAt,
+        String storageObjectKey,
+        String fileName,
+        String failureReason) {
+```
+
+```java
+    public static ExamSprintReport pending(
+            String reportId,
+            ReportType reportType,
+            JsonNode payload,
+            Instant createdAt,
+            Instant expiresAt) {
+```
+
+- [ ] **Step 3: Update renderer domain port signature**
+
+In `ExamSprintReportRenderer.java`, remove the contract enum import and use the same-package domain enum:
+
+```java
+public interface ExamSprintReportRenderer {
+
+    boolean supports(ReportType reportType);
+
+    String render(JsonNode payload, Instant generatedAt);
+}
+```
+
+- [ ] **Step 4: Update storage domain port signature**
+
+In `ExamSprintReportStorage.java`, remove the contract enum import and change the `upload(...)` parameter:
+
+```java
+    StoredExamSprintReportFile upload(
+            String reportId,
+            ReportType reportType,
+            String fileName,
+            byte[] pdfBytes,
+            Instant expiresAt);
+```
+
+- [ ] **Step 5: Run domain tests and source grep**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/domain -am test
+rg "contracts\.report" "abilities/exam-sprint/domain/src/main/java"
+rg "ExamSprintReportType" "abilities/exam-sprint/domain/src/main/java"
+```
+
+Expected: domain tests PASS; both `rg` commands return no matches in domain main source.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+refactor(exam-sprint): 迁移领域报告类型
+```
+
+## Task 4: Update application boundary and application tests
+
+**Files:**
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java`
+- Modify: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
+- Modify: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java`
+
+- [ ] **Step 1: Run application tests to expose compile failures after domain port changes**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/application -am test
+```
+
+Expected: FAIL until application code and tests stop passing contract `ExamSprintReportType` into domain APIs. Record the first compile/test failure as the expected red state.
+
+- [ ] **Step 2: Change application service to use domain `ReportType` internally**
+
+In `DefaultExamSprintReportApplicationService.java`, replace the report type import:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+Change entry methods and private method signatures:
+
+```java
+    public CreateExamSprintReportResponse createOutlookReport(JsonNode payload) {
+        validateOutlookPayload(payload);
+        return submitReportGeneration(ReportType.OUTLOOK, payload);
+    }
+
+    public CreateExamSprintReportResponse createAchievementReport(JsonNode payload) {
+        validateAchievementPayload(payload);
+        return submitReportGeneration(ReportType.ACHIEVEMENT, payload);
+    }
+
+    public CreateExamSprintReportWithUrlResponse createOutlookReportSync(JsonNode payload) {
+        validateOutlookPayload(payload);
+        return submitReportGenerationSync(ReportType.OUTLOOK, payload);
+    }
+
+    public CreateExamSprintReportWithUrlResponse createAchievementReportSync(JsonNode payload) {
+        validateAchievementPayload(payload);
+        return submitReportGenerationSync(ReportType.ACHIEVEMENT, payload);
+    }
+
+    private CreateExamSprintReportResponse submitReportGeneration(ReportType reportType, JsonNode payload) {
+```
+
+```java
+    private CreateExamSprintReportWithUrlResponse submitReportGenerationSync(
+            ReportType reportType,
+            JsonNode payload) {
+```
+
+- [ ] **Step 3: Map domain report type when constructing response DTOs**
+
+In `DefaultExamSprintReportApplicationService.java`, replace every response constructor argument that currently passes `report.reportType()` or `generatedReport.reportType()` directly.
+
+For `CreateExamSprintReportResponse`:
+
+```java
+        return new CreateExamSprintReportResponse(
+                report.reportId(),
+                ExamSprintReportContractMapper.toContractReportType(report.reportType()),
+                ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
+                report.createdAt(),
+                report.expiresAt());
+```
+
+For `CreateExamSprintReportWithUrlResponse`:
+
+```java
+        return new CreateExamSprintReportWithUrlResponse(
+                generatedReport.reportId(),
+                ExamSprintReportContractMapper.toContractReportType(generatedReport.reportType()),
+                ExamSprintReportContractMapper.toContractStatus(generatedReport.generationStatus()),
+                generatedReport.createdAt(),
+                generatedReport.updatedAt(),
+                generatedReport.expiresAt(),
+                downloadUrl);
+```
+
+For `ExamSprintReportDetailResponse`:
+
+```java
+        return new ExamSprintReportDetailResponse(
+                report.reportId(),
+                ExamSprintReportContractMapper.toContractReportType(report.reportType()),
+                ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
+                report.createdAt(),
+                report.updatedAt(),
+                report.expiresAt(),
+                downloadUrl,
+                report.failureReason());
+```
+
+- [ ] **Step 4: Update application service tests by assertion target**
+
+In `ExamSprintReportApplicationServiceTest.java`, use this rule:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+Domain entity assertions:
+
+```java
+assertThat(saved.reportType()).isEqualTo(ReportType.OUTLOOK);
+assertThat(saved.reportType()).isEqualTo(ReportType.ACHIEVEMENT);
+```
+
+Contract response assertions remain on the public enum, using either the existing import or a fully qualified name:
+
+```java
+assertThat(response.reportType())
+        .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType.OUTLOOK);
+```
+
+Test data that constructs `ExamSprintReport.pending(...)` or `new ExamSprintReport(...)` must use `ReportType.OUTLOOK` or `ReportType.ACHIEVEMENT`.
+
+Test doubles implementing `ExamSprintReportRenderer` or `ExamSprintReportStorage` must update method parameters to domain `ReportType`:
+
+```java
+public boolean supports(ReportType reportType) {
+    return reportType == ReportType.OUTLOOK || reportType == ReportType.ACHIEVEMENT;
+}
+```
+
+```java
+public StoredExamSprintReportFile upload(
+        String reportId,
+        ReportType reportType,
+        String fileName,
+        byte[] pdfBytes,
+        Instant expiresAt) {
+```
+
+- [ ] **Step 5: Update generation worker tests**
+
+In `ExamSprintReportGenerationWorkerTest.java`, replace contract enum use in domain construction and test doubles with:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+Examples:
+
+```java
+ExamSprintReport.pending("report-success", ReportType.OUTLOOK, payload, NOW, EXPIRES_AT)
+```
+
+```java
+private final ReportType supportedReportType;
+
+TestRenderer(ReportType supportedReportType) {
+    this.supportedReportType = supportedReportType;
+}
+
+public boolean supports(ReportType reportType) {
+    return reportType == supportedReportType;
+}
+```
+
+- [ ] **Step 6: Run application verification**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/application -am test
+```
+
+Expected: PASS. Tests should clearly distinguish domain `ReportType` assertions from contract `ExamSprintReportType` response assertions.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+refactor(exam-sprint): 在应用边界映射报告类型
+```
+
+## Task 5: Update infrastructure adapters and tests
+
+**Files:**
+- Modify: `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/InMemoryExamSprintReportStorage.java`
+- Modify: `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorage.java`
+- Modify: `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java`
+- Modify: `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.java`
+- Modify: `abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorageTest.java`
+- Modify: `abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java`
+- Modify: `abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRendererTest.java`
+
+- [ ] **Step 1: Run infrastructure tests to expose compile failures after domain port changes**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/infrastructure -am test
+```
+
+Expected: FAIL until infrastructure implementations use domain `ReportType` in overridden port methods. Record the first compile/test failure as the expected red state.
+
+- [ ] **Step 2: Update storage implementations**
+
+In `InMemoryExamSprintReportStorage.java` and `AzureBlobExamSprintReportStorage.java`, replace the contract enum import with:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+Change the `upload(...)` signature in both classes:
+
+```java
+    public StoredExamSprintReportFile upload(
+            String reportId,
+            ReportType reportType,
+            String fileName,
+            byte[] pdfBytes,
+            Instant expiresAt) {
+```
+
+Keep Azure metadata unchanged in behavior:
+
+```java
+"reportType", reportType.name(),
+```
+
+- [ ] **Step 3: Update renderer implementations without changing payload DTOs**
+
+In `ClasspathOutlookExamSprintReportRenderer.java`, keep `OutlookExamSprintReportPayload` and replace only the enum import/support logic:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+```java
+    public boolean supports(ReportType reportType) {
+        return reportType == ReportType.OUTLOOK;
+    }
+```
+
+In `ClasspathAchievementExamSprintReportRenderer.java`, keep `AchievementExamSprintReportPayload` and replace only the enum import/support logic:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+```java
+    public boolean supports(ReportType reportType) {
+        return reportType == ReportType.ACHIEVEMENT;
+    }
+```
+
+- [ ] **Step 4: Update infrastructure tests**
+
+Replace contract enum imports in these tests with domain `ReportType`:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
+```
+
+Update assertions/calls:
+
+```java
+assertThat(renderer.supports(ReportType.OUTLOOK)).isTrue();
+assertThat(renderer.supports(ReportType.ACHIEVEMENT)).isFalse();
+```
+
+```java
+assertThat(renderer.supports(ReportType.ACHIEVEMENT)).isTrue();
+assertThat(renderer.supports(ReportType.OUTLOOK)).isFalse();
+```
+
+```java
+storage.upload(
+        "report-1",
+        ReportType.OUTLOOK,
+        "exam-sprint-outlook-report-report-1.pdf",
+        pdfBytes,
+        expiresAt);
+```
+
+Keep any metadata assertions expecting the public string value:
+
+```java
+assertThat(metadata).containsEntry("reportType", "OUTLOOK");
+```
+
+- [ ] **Step 5: Run infrastructure verification**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/infrastructure -am test
+```
+
+Expected: PASS. Do not remove `exam-sprint-contracts` from `abilities/exam-sprint/infrastructure/pom.xml` because contract payload DTO usage remains in scope for infrastructure renderers.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+refactor(exam-sprint): 适配基础设施报告类型
+```
+
+## Task 6: Remove domain contracts dependency and tighten architecture guardrail
+
+**Files:**
+- Modify: `abilities/exam-sprint/domain/pom.xml`
+- Modify: `ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java`
+
+- [ ] **Step 1: Remove `exam-sprint-contracts` from domain POM**
+
+In `abilities/exam-sprint/domain/pom.xml`, delete only this dependency block:
+
+```xml
+<dependency>
+    <groupId>cn.yunzhixue</groupId>
+    <artifactId>exam-sprint-contracts</artifactId>
+    <version>${project.version}</version>
+</dependency>
+```
+
+Keep:
+
+```xml
+<dependency>
+    <groupId>com.fasterxml.jackson.core</groupId>
+    <artifactId>jackson-databind</artifactId>
+</dependency>
+```
+
+- [ ] **Step 2: Verify domain compiles without contracts dependency**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/domain -am test
+rg "exam-sprint-contracts" "abilities/exam-sprint/domain/pom.xml"
+rg "contracts\.report" "abilities/exam-sprint/domain/src/main/java"
+```
+
+Expected: domain tests PASS; both `rg` commands return no matches for domain contracts dependency/source imports.
+
+- [ ] **Step 3: Tighten ArchUnit domain-to-contract rule**
+
+In `ExamSprintArchitectureTest.java`, remove this field:
+
+```java
+private static final Set<String> CURRENT_DOMAIN_CONTRACT_DEBT = Set.of(
+        "ExamSprintReport",
+        "ExamSprintReportRenderer",
+        "ExamSprintReportStorage");
+```
+
+Replace the current rule:
+
+```java
+@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..");
+```
+
+with the hard rule:
+
+```java
+@ArchTest
+static final ArchRule domain_should_not_depend_on_contracts = noClasses()
+        .that().resideInAPackage("..domain..")
+        .should().dependOnClassesThat().resideInAPackage("..contracts..");
+```
+
+Keep `CURRENT_DOMAIN_JACKSON_DEBT` and `areNotNamed(...)` because the Jackson allowlist remains intentionally out of scope.
+
+- [ ] **Step 4: Run architecture and runtime verification**
+
+Run:
+
+```bash
+mvn -q -pl ability-center-runtime -Dtest=ExamSprintArchitectureTest test
+mvn -q -pl ability-center-runtime -am test
+```
+
+Expected: PASS. Runtime controller tests should continue to verify public response JSON still uses `"OUTLOOK"` / `"ACHIEVEMENT"` strings via contract response DTOs.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+test(exam-sprint): 收紧领域依赖架构守护
+```
+
+## Task 7: Update governance documentation
+
+**Files:**
+- Modify: `docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md`
+- Verify: `docs/superpowers/plans/2026-04-28-ddd-naming-governance-report-type-loop.md`
+
+- [ ] **Step 1: Update design document status line**
+
+Change the status near the top from “remaining `domain -> contracts` debt is `ReportType`” to:
+
+```markdown
+> Status: Second governance loop implemented for `ReportType`; `exam-sprint-domain` no longer depends on `exam-sprint-contracts`. Remaining near-term debt is Jackson / `JsonNode` payload governance.
+```
+
+- [ ] **Step 2: Update the technical debt register**
+
+Replace the `domain -> contracts` row with an implementation result row or remove it from active debts. Use this wording to preserve history without listing it as active debt:
+
+```markdown
+| `domain -> contracts` | Cleared in the `ReportType` loop after `ReportGenerationStatus` and `ReportType` became domain-owned | keep ArchUnit hard rule; map future contract/domain duplicates in application |
+```
+
+Keep the `domain -> jackson`, payload records, mixed model semantics, `Storage`/`Renderer`/`PdfGenerator`, and application orchestration rows unchanged except for any wording needed to identify them as next-loop candidates.
+
+- [ ] **Step 3: Update first-loop / next-loop summary**
+
+Replace the existing next-loop sentence:
+
+```markdown
+Next recommended loop: migrate `ReportType` into domain, remove `exam-sprint-domain` dependency on `exam-sprint-contracts`, and tighten the architecture allowlist accordingly.
+```
+
+with:
+
+```markdown
+Second loop result: `ReportType` is now domain-owned; application maps it to and from the public `ExamSprintReportType`; `exam-sprint-domain` no longer depends on `exam-sprint-contracts`; the domain-to-contract architecture rule is now a hard guardrail.
+
+Next recommended loop: address Jackson / `JsonNode` payload leakage by introducing strongly named domain report-content concepts for one report type while keeping external payload DTOs stable.
+```
+
+- [ ] **Step 4: Run documentation grep checks**
+
+Run:
+
+```bash
+rg "ReportType|domain -> contracts|JsonNode" "docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md" "docs/superpowers/plans/2026-04-28-ddd-naming-governance-report-type-loop.md"
+```
+
+Expected: matches describe `ReportType` as implemented after this loop and `JsonNode` as next-loop debt; no text should say `ReportType` is still active remaining `domain -> contracts` debt.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+docs(exam-sprint): 记录DDD命名治理二轮计划与结果
+```
+
+## Task 8: Final full verification and review preparation
+
+**Files:**
+- Verify: all modified files
+
+- [ ] **Step 1: Verify no domain main source depends on contracts report package**
+
+Run:
+
+```bash
+rg "contracts\.report" "abilities/exam-sprint/domain/src/main/java"
+```
+
+Expected: no matches.
+
+- [ ] **Step 2: Verify domain no longer uses the public API report type enum**
+
+Run:
+
+```bash
+rg "ExamSprintReportType" "abilities/exam-sprint/domain/src/main/java"
+```
+
+Expected: no matches.
+
+- [ ] **Step 3: Verify domain POM no longer depends on contracts**
+
+Run:
+
+```bash
+rg "exam-sprint-contracts" "abilities/exam-sprint/domain/pom.xml"
+```
+
+Expected: no matches.
+
+- [ ] **Step 4: Run targeted module suites with `-am`**
+
+Run:
+
+```bash
+mvn -q -pl abilities/exam-sprint/application -am test
+mvn -q -pl abilities/exam-sprint/infrastructure -am test
+mvn -q -pl ability-center-runtime -am test
+```
+
+Expected: all PASS.
+
+- [ ] **Step 5: Run full reactor tests**
+
+Run:
+
+```bash
+mvn -q test
+```
+
+Expected: PASS. If unrelated failures occur, record exact module and test names before deciding whether to fix or defer.
+
+- [ ] **Step 6: Check git status**
+
+Run:
+
+```bash
+git status --short
+```
+
+Expected: only files modified by this plan are listed.
+
+- [ ] **Step 7: Prepare code review checklist**
+
+Review these points before claiming completion:
+
+```text
+- domain main source has no `contracts.report` imports and no `ExamSprintReportType` usage.
+- `abilities/exam-sprint/domain/pom.xml` has no `exam-sprint-contracts` dependency.
+- contracts `ExamSprintReportType` remains named the same and still exposes `OUTLOOK`, `ACHIEVEMENT`.
+- application mapper covers all current enum values in both directions and null strategy.
+- response DTOs still use contracts `ExamSprintReportType`.
+- runtime/controller JSON shape remains unchanged.
+- ArchUnit domain-to-contract allowlist is removed or reduced to a hard rule.
+- Jackson / `JsonNode` allowlist remains intentionally unchanged.
+- no Storage/Renderer/PdfGenerator move, no payload DTO migration, no application service split.
+```
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+refactor(exam-sprint): 完成DDD命名治理二轮类型迁移
+```
+
+## Implementation Handoff
+
+Recommended execution mode:
+
+1. Use subagent-driven development, with one task per implementation subagent after Task 1/2 establishes the red-green mapping baseline.
+2. Review after each task for dependency direction, API compatibility, and scope control.
+3. Run `mvn -q test`, `git status --short`, and `rg "contracts\.report" "abilities/exam-sprint/domain/src/main/java"` before claiming the loop is complete.
+
+Do not proceed to a Jackson / payload governance loop until:
+
+- full tests have passed for this loop;
+- the architecture rule has been tightened;
+- the external response enum literals remain unchanged;
+- this plan and the long-term governance document record the `ReportType` loop outcome.

+ 11 - 8
docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md

@@ -1,6 +1,6 @@
 # DDD Naming and Architecture Governance Design
 
-> Status: First governance loop implemented; remaining `domain -> contracts` debt is `ReportType`. The next `ReportType` loop is recommended but not yet implemented.
+> Status: Second governance loop implemented for `ReportType`; `exam-sprint-domain` no longer depends on `exam-sprint-contracts`. Remaining near-term debt is Jackson / `JsonNode` payload governance.
 
 ## Goal
 
@@ -27,9 +27,9 @@ The project already has a useful DDD/Clean Architecture foundation:
 - `ExamSprintReport` contains lifecycle behavior instead of being a pure data object;
 - HTTP adapters live under `adapter.http` rather than being mixed into domain code.
 
-The main governance gaps are:
+The main governance items are:
 
-- `exam-sprint-domain` depends on `exam-sprint-contracts`;
+- historical `exam-sprint-domain -> exam-sprint-contracts` coupling has been cleared and should remain guarded by architecture tests;
 - domain classes use `JsonNode` as report payload representation;
 - `contracts` payload classes contain many business concepts and value-object candidates;
 - `ExamSprintReport` mixes report, generation task, generated file, and status-record semantics;
@@ -320,9 +320,9 @@ Acceptance criteria:
 
 ## Technical Debt Register
 
-| Debt | Current reason | Exit condition |
+| Debt | Status / current reason | Exit condition or guardrail |
 | --- | --- | --- |
-| `domain -> contracts` | `ReportType` still reuses API enum after `ReportGenerationStatus` migration | domain-owned `ReportType` plus application mapper |
+| `domain -> contracts` | Cleared in the `ReportType` loop after `ReportGenerationStatus` and `ReportType` became domain-owned | keep ArchUnit hard rule; map future contract/domain duplicates in application |
 | `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 |
@@ -392,15 +392,18 @@ Why this loop first:
 
 - generation status is a lifecycle concept and belongs to domain language;
 - it reduces `domain -> contracts` coupling without changing the external API;
-- it creates a repeatable mapper pattern for future `ReportType` and payload migrations;
+- it created a repeatable mapper pattern later used for the `ReportType` migration and still applicable to 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.
+First loop result: `ReportGenerationStatus` is now domain-owned; the application boundary maps it back to public contracts status, and architecture tests established the initial dependency baseline.
 
-Next recommended loop: migrate `ReportType` into domain, remove `exam-sprint-domain` dependency on `exam-sprint-contracts`, and tighten the architecture allowlist accordingly.
+Second loop result: `ReportType` is now domain-owned; the application boundary maps it to and from public `ExamSprintReportType`; `exam-sprint-domain` no longer depends on `exam-sprint-contracts`; and the domain-to-contract architecture rule is a hard guardrail.
+
+Next recommended loop: govern Jackson / `JsonNode` payload leakage by introducing strongly named domain report-content concepts for one report type while keeping external payload DTOs stable.
 
 The detailed implementation plan is in:
 
 ```text
 docs/superpowers/plans/2026-04-27-ddd-naming-governance-first-loop.md
+docs/superpowers/plans/2026-04-28-ddd-naming-governance-report-type-loop.md
 ```