2026-04-27-ddd-naming-governance-first-loop.md 22 KB

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.

Execution note after first-loop implementation: Task 2/3 verification was handled as a continuous loop because application testCompile ordering and local artifact issues could leave stale module artifacts when commands were run without -am. A runtime test also had a stale domain status assertion and was updated minimally. In submodule verification scenarios, prefer commands with -am to rebuild required modules and avoid stale artifacts.

Execution status: First loop implemented in this worktree. The checklist remains the original plan for traceability and is intentionally left unchecked. Final verification after the Task 7 docs update: mvn -q test passed.

Target File Structure

Create

  • 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:

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:

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:

import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;

Change the record field from:

ExamSprintReportGenerationStatus generationStatus,

to:

ReportGenerationStatus generationStatus,

Change all status references in the same file:

ExamSprintReportGenerationStatus.PENDING
ExamSprintReportGenerationStatus.PROCESSING
ExamSprintReportGenerationStatus.SUCCESS
ExamSprintReportGenerationStatus.FAILED
ExamSprintReportGenerationStatus.EXPIRED

to:

ReportGenerationStatus.PENDING
ReportGenerationStatus.PROCESSING
ReportGenerationStatus.SUCCESS
ReportGenerationStatus.FAILED
ReportGenerationStatus.EXPIRED
  • Step 4: Run domain tests to verify the domain module compiles

Run:

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:

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:

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:

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:

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:

mvn -q -pl abilities/exam-sprint/application -Dtest=ExamSprintReportContractMapperTest test

Expected: PASS.

  • Commit checkpoint, only if explicitly requested

Suggested message:

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:

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:

import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;

with:

import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
  • Step 3: Update status comparisons in DefaultExamSprintReportApplicationService

Replace comparisons such as:

generatedReport.generationStatus() != ExamSprintReportGenerationStatus.SUCCESS
report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED
report.generationStatus() == ExamSprintReportGenerationStatus.SUCCESS

with:

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:

ExamSprintReportContractMapper.toContractStatus(report.generationStatus())

or the equivalent variable-specific expression instead of passing report.generationStatus() directly.

For example, change:

return new CreateExamSprintReportResponse(
        report.reportId(),
        report.reportType(),
        report.generationStatus(),
        report.createdAt(),
        report.expiresAt());

to:

return new CreateExamSprintReportResponse(
        report.reportId(),
        report.reportType(),
        ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
        report.createdAt(),
        report.expiresAt());
  • Step 5: Update imports and comparisons in ExamSprintReportGenerationPipeline

Replace:

import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;

with:

import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;

Replace:

report.generationStatus() != ExamSprintReportGenerationStatus.PENDING

with:

report.generationStatus() != ReportGenerationStatus.PENDING
  • Step 6: Compile the application module

Run:

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:

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:

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:

assertThat(savedReport.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);

Example contract response assertion:

assertThat(response.generationStatus())
        .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus.SUCCESS);
  • Step 3: Run application tests

Run:

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:

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:

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>:

<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:

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:

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:

mvn -q -pl ability-center-runtime -am test

Expected: PASS.

  • Commit checkpoint, only if explicitly requested

Suggested message:

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:

rg "ExamSprintReportGenerationStatus" "abilities/exam-sprint/domain/src/main/java"

Expected: no matches.

  • Step 2: Search for remaining domain contract imports

Run:

rg "contracts\.report" "abilities/exam-sprint/domain/src/main/java"

Expected: matches remain only for the current known debt around ExamSprintReportType, likely in:

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:

<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:

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:

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:

| `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:

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:

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:

mvn -q test

Expected: PASS.

  • Commit checkpoint, only if explicitly requested

Suggested message:

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.