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
testCompileordering 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-amto 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 testpassed.
abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportGenerationStatus.java
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.java
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapperTest.java
ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/architecture/ExamSprintArchitectureTest.java
abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.java
ReportGenerationStatus instead of contract ExamSprintReportGenerationStatus.abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java
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
ReportGenerationStatus.abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
ExamSprintReport; keep response assertions on contract status.abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java
ability-center-runtime/pom.xml
Files:
abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ReportGenerationStatus.javaModify: 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.
ReportGenerationStatus in domainCreate 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
}
ExamSprintReport to use the domain statusIn 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
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.
Suggested message:
refactor: move report generation status into domain
Files:
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportContractMapper.javaCreate: 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();
}
}
Run:
mvn -q -pl abilities/exam-sprint/application -Dtest=ExamSprintReportContractMapperTest test
Expected: FAIL because ExamSprintReportContractMapper does not exist.
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());
}
}
Run:
mvn -q -pl abilities/exam-sprint/application -Dtest=ExamSprintReportContractMapperTest test
Expected: PASS.
Suggested message:
refactor: map domain generation status to contracts
Files:
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.javaModify: 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.
DefaultExamSprintReportApplicationServiceReplace:
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
with:
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
DefaultExamSprintReportApplicationServiceReplace 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
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());
ExamSprintReportGenerationPipelineReplace:
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
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.
Suggested message:
refactor: use domain generation status in application flow
Files:
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.javaModify: 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.
Use this rule:
ExamSprintReport.generationStatus() use cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;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);
Run:
mvn -q -pl abilities/exam-sprint/application -am test
Expected: PASS for application module tests.
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.
Suggested message:
test: distinguish domain and contract generation status
Files:
ability-center-runtime/pom.xmlCreate: 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>
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());
}
};
}
}
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.
Run:
mvn -q -pl ability-center-runtime -am test
Expected: PASS.
Suggested message:
test: add exam sprint architecture guardrails
Files:
abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/ExamSprintReport.javaabilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/*.javaVerify: 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.
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
domain/pom.xml contracts dependency in place for this loopDo 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.
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.
Suggested message:
refactor: reduce domain dependency on report contracts
Files:
docs/superpowers/specs/2026-04-27-ddd-naming-governance-design.mdModify: 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 |
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.
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.
Run:
mvn -q test
Expected: PASS.
Suggested message:
docs: record ddd governance first loop outcome
Recommended execution mode:
mvn -q test before claiming the loop is complete.Do not proceed to the second governance loop until:
domain -> contracts debt is documented as ReportType migration work.