Sfoglia il codice sorgente

Merge branch 'docs/ddd命名治理方案' of jyx/dcjxb.microservice into master

金逸霄 2 settimane fa
parent
commit
240dfd321f

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

@@ -0,0 +1,659 @@
+# DDD Naming Governance First 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:** Establish the first DDD governance loop by moving report generation lifecycle status into `domain`, mapping it back to public contract responses, and adding an architecture-test baseline that prevents new domain dependency violations.
+
+**Architecture:** Keep the public API stable while shifting ownership of lifecycle language from `contracts` to `domain`. `domain` owns `ReportGenerationStatus`; `application` maps domain status to `contracts.ExamSprintReportGenerationStatus`; `runtime` continues returning the same JSON values. Architecture tests live in `ability-center-runtime` because that module sees the complete assembled classpath.
+
+**Tech Stack:** Java 17, Maven multi-module build, Spring Boot 3.3.5, JUnit 5, ArchUnit, existing Spring Boot test setup.
+
+---
+
+> **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.
+
+## Target File Structure
+
+### Create
+
+- `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportGenerationStatus.java`
+  - Domain-owned lifecycle status for report generation.
+- `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java`
+  - Package-private application mapper from domain concepts to public contract types.
+- `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java`
+  - Verifies every domain status maps to the existing public contract enum value.
+- `ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java`
+  - Baseline architecture tests with current debt allowlisted.
+
+### Modify
+
+- `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java`
+  - Use domain `ReportGenerationStatus` instead of contract `ExamSprintReportGenerationStatus`.
+- `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java`
+  - Compare against domain `ReportGenerationStatus` and map response status back to contract status.
+- `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java`
+  - Compare against domain `ReportGenerationStatus`.
+- `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
+  - Update domain status assertions where tests inspect `ExamSprintReport`; keep response assertions on contract status.
+- `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java`
+  - Update domain status assertions.
+- `ability-center-runtime/pom.xml`
+  - Add ArchUnit test dependency.
+
+## Task 1: Add domain-owned generation status
+
+**Files:**
+- Create: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportGenerationStatus.java`
+- Modify: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java`
+
+- [ ] **Step 1: Run the current domain tests as a baseline**
+
+Run:
+
+```powershell
+mvn -q -pl abilities/exam-sprint/domain -am test
+```
+
+Expected: PASS in a clean workspace. If it fails before editing, stop and record the exact failing module/test before continuing so the governance migration is not blamed for a pre-existing failure.
+
+- [ ] **Step 2: Create `ReportGenerationStatus` in domain**
+
+Create `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportGenerationStatus.java`:
+
+```java
+package cn.yunzhixue.ability.center.examsprint.domain.report;
+
+public enum ReportGenerationStatus {
+    PENDING,
+    PROCESSING,
+    SUCCESS,
+    FAILED,
+    EXPIRED
+}
+```
+
+- [ ] **Step 3: Update `ExamSprintReport` to use the domain status**
+
+In `ExamSprintReport.java`, remove this import:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
+```
+
+Change the record field from:
+
+```java
+ExamSprintReportGenerationStatus generationStatus,
+```
+
+to:
+
+```java
+ReportGenerationStatus generationStatus,
+```
+
+Change all status references in the same file:
+
+```java
+ExamSprintReportGenerationStatus.PENDING
+ExamSprintReportGenerationStatus.PROCESSING
+ExamSprintReportGenerationStatus.SUCCESS
+ExamSprintReportGenerationStatus.FAILED
+ExamSprintReportGenerationStatus.EXPIRED
+```
+
+to:
+
+```java
+ReportGenerationStatus.PENDING
+ReportGenerationStatus.PROCESSING
+ReportGenerationStatus.SUCCESS
+ReportGenerationStatus.FAILED
+ReportGenerationStatus.EXPIRED
+```
+
+- [ ] **Step 4: Run domain tests to verify the domain module compiles**
+
+Run:
+
+```powershell
+mvn -q -pl abilities/exam-sprint/domain -am test
+```
+
+Expected: domain module compiles. Remaining domain-to-contract dependency still exists through `ExamSprintReportType`, `ExamSprintReportRenderer`, and `ExamSprintReportStorage`; this task only migrates generation status.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+refactor: move report generation status into domain
+```
+
+## Task 2: Add application contract mapping
+
+**Files:**
+- Create: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java`
+- Create: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java`
+
+- [ ] **Step 1: Write the mapper test first**
+
+Create `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java`:
+
+```java
+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();
+    }
+}
+```
+
+- [ ] **Step 2: Run the mapper test and verify it fails before implementation**
+
+Run:
+
+```powershell
+mvn -q -pl abilities/exam-sprint/application -Dtest=ExamSprintReportContractMapperTest test
+```
+
+Expected: FAIL because `ExamSprintReportContractMapper` does not exist.
+
+- [ ] **Step 3: Implement the mapper**
+
+Create `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java`:
+
+```java
+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());
+    }
+}
+```
+
+- [ ] **Step 4: Run the mapper test again**
+
+Run:
+
+```powershell
+mvn -q -pl abilities/exam-sprint/application -Dtest=ExamSprintReportContractMapperTest test
+```
+
+Expected: PASS.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+refactor: map domain generation status to contracts
+```
+
+## Task 3: Update application code to use domain status internally
+
+**Files:**
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java`
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java`
+
+- [ ] **Step 1: Run application tests before editing**
+
+Run:
+
+```powershell
+mvn -q -pl abilities/exam-sprint/application -am test
+```
+
+Expected: FAIL after Task 1 if application still imports contract `ExamSprintReportGenerationStatus` for domain status comparisons.
+
+- [ ] **Step 2: Update imports in `DefaultExamSprintReportApplicationService`**
+
+Replace:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
+```
+
+with:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
+```
+
+- [ ] **Step 3: Update status comparisons in `DefaultExamSprintReportApplicationService`**
+
+Replace comparisons such as:
+
+```java
+generatedReport.generationStatus() != ExamSprintReportGenerationStatus.SUCCESS
+report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED
+report.generationStatus() == ExamSprintReportGenerationStatus.SUCCESS
+```
+
+with:
+
+```java
+generatedReport.generationStatus() != ReportGenerationStatus.SUCCESS
+report.generationStatus() != ReportGenerationStatus.EXPIRED
+report.generationStatus() == ReportGenerationStatus.SUCCESS
+```
+
+- [ ] **Step 4: Map status when constructing contract responses**
+
+In `DefaultExamSprintReportApplicationService`, every `new CreateExamSprintReportResponse`, `new CreateExamSprintReportWithUrlResponse`, and `new ExamSprintReportDetailResponse` call must pass:
+
+```java
+ExamSprintReportContractMapper.toContractStatus(report.generationStatus())
+```
+
+or the equivalent variable-specific expression instead of passing `report.generationStatus()` directly.
+
+For example, change:
+
+```java
+return new CreateExamSprintReportResponse(
+        report.reportId(),
+        report.reportType(),
+        report.generationStatus(),
+        report.createdAt(),
+        report.expiresAt());
+```
+
+to:
+
+```java
+return new CreateExamSprintReportResponse(
+        report.reportId(),
+        report.reportType(),
+        ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
+        report.createdAt(),
+        report.expiresAt());
+```
+
+- [ ] **Step 5: Update imports and comparisons in `ExamSprintReportGenerationPipeline`**
+
+Replace:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
+```
+
+with:
+
+```java
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
+```
+
+Replace:
+
+```java
+report.generationStatus() != ExamSprintReportGenerationStatus.PENDING
+```
+
+with:
+
+```java
+report.generationStatus() != ReportGenerationStatus.PENDING
+```
+
+- [ ] **Step 6: Compile the application module**
+
+Run:
+
+```powershell
+mvn -q -pl abilities/exam-sprint/application -am -DskipTests compile
+```
+
+Expected: PASS. Full application tests run in Task 4 after test imports distinguish domain status from contract response status.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+refactor: use domain generation status in application flow
+```
+
+## Task 4: Update tests for domain-vs-contract status ownership
+
+**Files:**
+- 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: Locate contract status usages in application tests**
+
+Run:
+
+```powershell
+rg "ExamSprintReportGenerationStatus" "abilities/exam-sprint/application/src/test/java"
+```
+
+Expected: matches in application tests that currently import the contract enum.
+
+- [ ] **Step 2: Split status imports by assertion target**
+
+Use this rule:
+
+- assertions against `ExamSprintReport.generationStatus()` use `cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus`;
+- assertions against `CreateExamSprintReportResponse`, `CreateExamSprintReportWithUrlResponse`, or `ExamSprintReportDetailResponse` use `cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus`.
+
+When both are needed in the same test file, import the domain enum and refer to the contract enum with a fully qualified name to avoid ambiguous names.
+
+Example domain assertion:
+
+```java
+assertThat(savedReport.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
+```
+
+Example contract response assertion:
+
+```java
+assertThat(response.generationStatus())
+        .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus.SUCCESS);
+```
+
+- [ ] **Step 3: Run application tests**
+
+Run:
+
+```powershell
+mvn -q -pl abilities/exam-sprint/application -am test
+```
+
+Expected: PASS for application module tests.
+
+- [ ] **Step 4: Run runtime tests to verify response compatibility**
+
+Run:
+
+```powershell
+mvn -q -pl ability-center-runtime -am test
+```
+
+Expected: runtime tests pass and API responses still expose the original public enum names: `PENDING`, `PROCESSING`, `SUCCESS`, `FAILED`, `EXPIRED`.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+test: distinguish domain and contract generation status
+```
+
+## Task 5: Add architecture baseline tests
+
+**Files:**
+- Modify: `ability-center-runtime/pom.xml`
+- Create: `ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java`
+
+- [ ] **Step 1: Add ArchUnit dependency to runtime test scope**
+
+In `ability-center-runtime/pom.xml`, add this dependency inside `<dependencies>`:
+
+```xml
+<dependency>
+    <groupId>com.tngtech.archunit</groupId>
+    <artifactId>archunit-junit5</artifactId>
+    <version>1.3.0</version>
+    <scope>test</scope>
+</dependency>
+```
+
+- [ ] **Step 2: Write architecture tests with current domain debt allowlisted**
+
+Create `ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java`:
+
+```java
+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());
+            }
+        };
+    }
+}
+```
+
+- [ ] **Step 3: Run architecture tests**
+
+Run:
+
+```powershell
+mvn -q -pl ability-center-runtime -Dtest=ExamSprintArchitectureTest test
+```
+
+Expected: PASS. If this fails because additional historical debt exists, add only the exact existing class names to the relevant `CURRENT_DOMAIN_*_DEBT` set and document why in the PR description.
+
+- [ ] **Step 4: Run all runtime tests**
+
+Run:
+
+```powershell
+mvn -q -pl ability-center-runtime -am test
+```
+
+Expected: PASS.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+test: add exam sprint architecture guardrails
+```
+
+## Task 6: Remove the migrated contract status dependency from domain imports
+
+**Files:**
+- Verify: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java`
+- Verify: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/*.java`
+- Verify: `abilities/exam-sprint/domain/pom.xml`
+
+- [ ] **Step 1: Search for remaining contract status imports in domain**
+
+Run:
+
+```powershell
+rg "ExamSprintReportGenerationStatus" "abilities/exam-sprint/domain/src/main/java"
+```
+
+Expected: no matches.
+
+- [ ] **Step 2: Search for remaining domain contract imports**
+
+Run:
+
+```powershell
+rg "contracts\.report" "abilities/exam-sprint/domain/src/main/java"
+```
+
+Expected: matches remain only for the current known debt around `ExamSprintReportType`, likely in:
+
+```text
+ExamSprintReport.java
+ExamSprintReportRenderer.java
+ExamSprintReportStorage.java
+```
+
+- [ ] **Step 3: Leave `domain/pom.xml` contracts dependency in place for this loop**
+
+Do not remove this dependency yet:
+
+```xml
+<dependency>
+    <groupId>cn.yunzhixue</groupId>
+    <artifactId>exam-sprint-contracts</artifactId>
+    <version>${project.version}</version>
+</dependency>
+```
+
+Reason: `ExamSprintReportType` still lives in contracts. Removing the dependency belongs to the next governance loop that migrates `ReportType`.
+
+- [ ] **Step 4: Run full reactor tests**
+
+Run:
+
+```powershell
+mvn -q test
+```
+
+Expected: PASS. If unrelated failures occur, record the exact failing module and test names before deciding whether to fix them in this loop or defer.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+refactor: reduce domain dependency on report contracts
+```
+
+## Task 7: Update governance tracking after implementation
+
+**Files:**
+- Modify: `docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md`
+- Modify: `docs/superpowers/plans/2026-04-27-ddd-naming-governance-first-loop.md` only if execution notes differ from this plan.
+
+- [ ] **Step 1: Update the debt register result**
+
+In `docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md`, update the debt register entry for `domain -> contracts` to mention that `ReportGenerationStatus` has been migrated and `ReportType` remains.
+
+Use wording like:
+
+```markdown
+| `domain -> contracts` | `ReportType` still reuses API enum after `ReportGenerationStatus` migration | domain-owned `ReportType` plus application mapper |
+```
+
+- [ ] **Step 2: Add next-loop recommendation**
+
+In the same design document, under “First Governance Loop”, add the next recommended loop:
+
+```markdown
+Next recommended loop: migrate `ReportType` into domain, remove `exam-sprint-domain` dependency on `exam-sprint-contracts`, and tighten the architecture allowlist accordingly.
+```
+
+- [ ] **Step 3: Run documentation grep checks**
+
+Run:
+
+```powershell
+rg "ReportGenerationStatus" "docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md" "docs/superpowers/plans/2026-04-27-ddd-naming-governance-first-loop.md"
+```
+
+Expected: matches describe both the completed first loop and next governance direction clearly.
+
+- [ ] **Step 4: Final verification**
+
+Run:
+
+```powershell
+mvn -q test
+```
+
+Expected: PASS.
+
+- [ ] **Commit checkpoint, only if explicitly requested**
+
+Suggested message:
+
+```text
+docs: record ddd governance first loop outcome
+```
+
+## Implementation Handoff
+
+Recommended execution mode:
+
+1. Use subagent-driven development, one task per subagent, because tasks are mostly independent after Task 1 establishes the enum.
+2. Review after each task with emphasis on API compatibility and dependency direction.
+3. Run `mvn -q test` before claiming the loop is complete.
+
+Do not proceed to the second governance loop until:
+
+- full tests have passed;
+- architecture tests are passing;
+- external response enum values remain unchanged;
+- remaining `domain -> contracts` debt is documented as `ReportType` migration work.

+ 402 - 0
docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.md

@@ -0,0 +1,402 @@
+# DDD Naming and Architecture Governance Design
+
+> Status: Draft written from conversation approval, pending user review before implementation.
+
+## Goal
+
+Establish a long-term governance model for DDD-aligned naming, module dependencies, and architectural boundaries in `dcjxb.microservice`, starting with the existing `exam-sprint` ability and then turning the resulting rules into a repeatable template for future abilities.
+
+## Current Context
+
+The repository is a Java 17, Spring Boot 3.3.5, Maven multi-module project with this ability layout:
+
+```text
+ability-center-runtime
+ability-center-kernel
+abilities/exam-sprint/contracts
+abilities/exam-sprint/application
+abilities/exam-sprint/domain
+abilities/exam-sprint/infrastructure
+```
+
+The project already has a useful DDD/Clean Architecture foundation:
+
+- code is grouped by the `exam-sprint` business ability;
+- `contracts`, `application`, `domain`, and `infrastructure` are separate Maven modules;
+- `ExamSprintReportRepository` is defined in `domain` and implemented in `infrastructure`;
+- `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:
+
+- `exam-sprint-domain` depends on `exam-sprint-contracts`;
+- 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;
+- `Pipeline`, `Worker`, `Dispatcher`, and `Scheduler` naming emphasizes execution mechanics over use cases;
+- `Storage`, `Renderer`, and `PdfGenerator` currently sit in `domain` even though they mostly describe technical delivery mechanisms.
+
+## Governance Principles
+
+### 1. Domain owns the ubiquitous language
+
+Domain classes should answer: “what is this in business terms?” Examples:
+
+```text
+ExamSprintReportGeneration
+ReportGenerationStatus
+GeneratedReportFile
+ReadinessAssessment
+SyllabusMastery
+FrequencyPlan
+ScoreImprovementCase
+```
+
+Domain classes should not be named after transport or implementation concepts such as `Payload`, `JsonNode`, `Response`, `storageObjectKey`, `pdfBytes`, or `htmlContent`.
+
+### 2. Contracts describe external protocol only
+
+The `contracts` module owns external request/response shapes and API-facing enums. Contracts may contain validation annotations and JSON-friendly structure, but domain code must not depend on contract DTOs.
+
+### 3. Application names use cases and ports
+
+The `application` module owns use-case orchestration and outbound port definitions. Its primary names should be `UseCase`, `Command`, `Query`, `Result`, `ApplicationService`, and business-named ports.
+
+### 4. Infrastructure names implementation technology
+
+The `infrastructure` module may use technical names because its job is to adapt concrete technology. Prefixes such as `AzureBlob`, `OpenHtmlToPdf`, `Classpath`, and `InMemory` are appropriate there.
+
+### 5. Governance is incremental
+
+Existing debt should be baselined and reduced in small vertical slices. New code must follow the rules immediately. Historical exceptions need owners, expiry expectations, and clear exit criteria.
+
+## Target Module Dependencies
+
+Long-term target:
+
+```text
+runtime -> contracts
+runtime -> application
+runtime -> infrastructure
+
+infrastructure -> application
+infrastructure -> domain
+
+application -> domain
+
+contracts -> kernel / validation api
+domain -> kernel, only for stable domain-agnostic primitives
+```
+
+Forbidden long-term dependencies:
+
+```text
+domain -> contracts
+domain -> application
+domain -> infrastructure
+domain -> runtime
+domain -> jackson
+domain -> spring
+domain -> jakarta.validation
+
+application -> infrastructure implementation classes
+contracts -> domain/application/infrastructure/runtime
+infrastructure -> runtime/adapter.http/controller
+```
+
+## Naming Rules
+
+### Domain
+
+Recommended names:
+
+```text
+ExamSprintReportGeneration
+ReportGenerationStatus
+ReportType
+GeneratedReportFile
+ReportRetentionPolicy
+ReportFailureReason
+OutlookReportContent
+AchievementReportContent
+ReadinessAssessment
+SyllabusMastery
+FrequencyPlan
+ScoreImprovementCase
+ExamSprintReportRepository
+```
+
+Allowed suffixes:
+
+| Concept | Preferred naming |
+| --- | --- |
+| Repository | `Repository` |
+| Domain service | `DomainService`, only when behavior does not belong naturally on an entity/value object |
+| Domain event | `Event` |
+| Business rule | `Policy` or `Specification` |
+| Identifier | `Id` |
+| Lifecycle state | `Status` |
+| Business category | `Type` |
+
+Avoid in domain:
+
+```text
+DTO
+Request
+Response
+Payload
+Controller
+Manager
+Processor
+Handler
+Pipeline
+Worker
+Dispatcher
+Scheduler
+JsonNode
+Storage
+Renderer
+PdfGenerator
+```
+
+`Storage`, `Renderer`, and `PdfGenerator` may only remain in domain if the team explicitly decides they are domain capabilities rather than technical delivery mechanisms.
+
+### Application
+
+Recommended names:
+
+```text
+CreateExamSprintReportUseCase
+GenerateExamSprintReportUseCase
+GetExamSprintReportUseCase
+DownloadExamSprintReportUseCase
+ExpireExamSprintReportsUseCase
+CreateExamSprintReportCommand
+ExamSprintReportDetailResult
+ReportFileStorage
+ReportDocumentRenderer
+PdfDocumentGenerator
+ReportGenerationRequestPublisher
+```
+
+Preferred suffixes:
+
+```text
+UseCase
+ApplicationService
+Command
+Query
+Result
+Port
+Executor
+Publisher
+Mapper
+Job
+ScheduledJob
+```
+
+Avoid using `Pipeline`, `Worker`, `Dispatcher`, and `Scheduler` for new business-flow classes. If execution mechanics are the actual concept, place the class in a technical package and document the reason in review.
+
+### Contracts
+
+Recommended names:
+
+```text
+CreateExamSprintReportRequest
+CreateExamSprintReportResponse
+CreateExamSprintReportWithUrlResponse
+ExamSprintReportDetailResponse
+OutlookExamSprintReportPayload
+AchievementExamSprintReportPayload
+ExamSprintReportType
+ExamSprintReportGenerationStatus
+```
+
+Allowed suffixes:
+
+```text
+Request
+Response
+Payload
+Type
+Status
+```
+
+Rules:
+
+- contracts DTOs must not be used as domain models;
+- contracts enums that duplicate domain concepts must be mapped explicitly;
+- nested payload records that contain business behavior must be registered as domain value-object candidates.
+
+### Infrastructure
+
+Recommended names:
+
+```text
+AzureBlobReportFileStorage
+InMemoryExamSprintReportRepository
+ClasspathOutlookReportDocumentRenderer
+ClasspathAchievementReportDocumentRenderer
+OpenHtmlToPdfDocumentGenerator
+ExamSprintReportInfrastructureConfiguration
+```
+
+Allowed technology prefixes:
+
+```text
+AzureBlob
+OpenHtmlToPdf
+Classpath
+InMemory
+Jdbc
+Jpa
+Redis
+Kafka
+Rabbit
+```
+
+Infrastructure names should make the chosen technology visible.
+
+## Phased Roadmap
+
+### Short term: 1-2 weeks
+
+Goal: establish rules and stop new violations.
+
+Actions:
+
+1. Add this governance document to the repository.
+2. Add a first architecture test baseline with allowlisted existing debt.
+3. Start PR checklist enforcement for naming and dependency direction.
+4. Move one lifecycle concept from contracts ownership into domain ownership; recommended first candidate: `ReportGenerationStatus`.
+5. Keep external API responses unchanged by mapping domain status back to contract status in application code.
+
+Acceptance criteria:
+
+- new domain code cannot introduce new dependencies on `contracts` or `JsonNode` without a reviewed exception;
+- at least one existing domain-to-contract dependency is removed;
+- architecture tests run in CI or in the normal Maven test command;
+- external HTTP contract remains compatible.
+
+### Mid term: 1-2 months
+
+Goal: make domain independent from contracts and reduce JSON leakage.
+
+Actions:
+
+1. Move or duplicate domain-owned enums into `domain` and map at boundaries.
+2. Replace domain `JsonNode payload` usage with strongly named content concepts for at least one report type.
+3. Move business invariants from payload records into domain value objects.
+4. Reclassify `ExamSprintReportStorage`, `ExamSprintReportRenderer`, and `ExamSprintReportPdfGenerator` as application ports unless the team explicitly documents domain ownership.
+5. Start thinning `DefaultExamSprintReportApplicationService` by extracting use cases behind the existing facade.
+
+Acceptance criteria:
+
+- `exam-sprint-domain` no longer depends on `exam-sprint-contracts`;
+- new domain code does not use Jackson types;
+- at least one report content payload has a domain model;
+- application ports own rendering, PDF, and file-storage abstractions if they are technical delivery mechanisms.
+
+### Long term: 1-2 quarters
+
+Goal: establish a stable DDD/Clean Architecture template for future abilities.
+
+Actions:
+
+1. Decide whether the current `ExamSprintReport` should become `ExamSprintReportGeneration`.
+2. Extract `GeneratedReportFile`, `ReportGenerationFailure`, and report-content value objects when their behavior becomes stable.
+3. Split application orchestration into use cases: create, generate, get, download, and expire.
+4. Convert architecture rules from allowlist-based soft governance to hard CI checks.
+5. Publish the `exam-sprint` structure as the standard template for new `abilities/{ability-name}` modules.
+
+Acceptance criteria:
+
+- domain contains business language and business rules only;
+- application contains use cases and ports;
+- contracts contains external API protocol only;
+- infrastructure contains concrete adapters only;
+- architectural allowlist trends toward zero;
+- new ability modules follow the same pattern from creation.
+
+## Technical Debt Register
+
+| Debt | Current reason | Exit condition |
+| --- | --- | --- |
+| `domain -> contracts` | domain reuses contract enums | domain owns equivalent concepts and application maps to contracts |
+| `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 |
+| domain hosts `Storage`/`Renderer`/`PdfGenerator` | ports were initially placed near report aggregate | ports move to application or are explicitly documented as domain capabilities |
+| application contains technical-flow names | existing pipeline/worker/dispatcher naming | new use-case names introduced and old names retired gradually |
+
+## Review Checklist
+
+Use this list for every PR that touches `abilities/exam-sprint`.
+
+### Dependencies
+
+- [ ] Did domain add a dependency on contracts?
+- [ ] Did domain add Jackson, Spring, validation, Azure, PDF, or persistence dependencies?
+- [ ] Did application depend on infrastructure implementation classes?
+- [ ] Did contracts depend on domain, application, infrastructure, or runtime?
+- [ ] Did infrastructure depend on runtime, controller, or HTTP adapter classes?
+
+### Naming
+
+- [ ] Does each new domain name describe a business concept?
+- [ ] Does each new application name describe a use case, command, result, or port?
+- [ ] Does each new contract name clearly describe request/response/payload protocol?
+- [ ] Does each new infrastructure name describe concrete technology or adapter behavior?
+- [ ] If a new class uses `Manager`, `Processor`, `Handler`, `Pipeline`, `Worker`, `Dispatcher`, or `Scheduler`, is the reason documented?
+
+### Model boundaries
+
+- [ ] Is an API DTO being used as a domain model?
+- [ ] Is `JsonNode`, `Map<String,Object>`, or raw JSON entering domain logic?
+- [ ] Is a new string field actually a value-object candidate?
+- [ ] Is a business invariant implemented outside domain without a reason?
+- [ ] Are lifecycle transitions controlled by domain methods?
+
+### Compatibility and tests
+
+- [ ] Does the public API remain compatible?
+- [ ] Are contracts mapped explicitly when domain concepts differ?
+- [ ] Are domain/application tests updated for success and failure paths?
+- [ ] Are architecture tests updated when dependency rules change?
+- [ ] Is any allowlist entry reduced, updated, or justified?
+
+## Metrics
+
+Track these metrics once per iteration:
+
+| Metric | Target |
+| --- | --- |
+| `domain -> contracts` references | 0 |
+| `domain -> jackson` references | 0 |
+| `JsonNode` usages in domain | 0 |
+| new domain classes ending in `DTO`, `Request`, `Response`, or `Payload` | 0 |
+| architecture allowlist entries | decreasing |
+| new architectural violations | 0 |
+| application classes with broad orchestration responsibilities | decreasing |
+| payload nested records with business behavior | decreasing or explicitly accepted |
+
+## First Governance Loop
+
+The recommended first loop is:
+
+```text
+domain dependency baseline + ReportGenerationStatus migration
+```
+
+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 gives the architecture tests a concrete baseline and a concrete debt reduction.
+
+The detailed implementation plan is in:
+
+```text
+docs/superpowers/plans/2026-04-27-ddd-naming-governance-first-loop.md
+```