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: Log incoming OUTLOOK and ACHIEVEMENT report payloads while replacing every array field with <fieldName>Size, and derive achievement learning efficiency from mastery hit rate divided by 0.04.
Architecture: Keep changes inside the existing report application boundary. DefaultExamSprintReportApplicationService owns payload logging because it receives raw JsonNode request payloads; AchievementReportContentMapper owns presentation-field derivation for achievement reports.
Tech Stack: Java 17, Spring Boot, Jackson JsonNode/ObjectNode, SLF4J, JUnit 5, AssertJ, Spring OutputCaptureExtension, Maven.
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java
<fieldName>Size numeric fields.abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.java
(testPaperImprovedWordCount / studentImproveWordCount) / 0.04.0 when studentImproveWordCount is zero.abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
OUTLOOK and ACHIEVEMENT payload summaries.Files:
Test: abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
[ ] Step 1: Add an OUTLOOK payload logging test
Insert this test near existing OUTLOOK creation tests after createOutlookReportAcceptsCallerVocabularyPayloadAndDispatches:
/** 覆盖展望报告原始报文日志场景,当 payload 包含数组时,应只记录数组大小而不记录数组内容。 */
@Test
void createOutlookReportLogsPayloadWithArraySizesOnly(CapturedOutput output) throws Exception {
TestRepository repository = new TestRepository();
DefaultExamSprintReportApplicationService service = service(repository, reportId -> { }, new TestStorage());
service.createOutlookReport(callerVocabularyPayload());
assertThat(output.getAll())
.contains("exam_sprint_report_payload_received")
.contains("reportType=OUTLOOK")
.contains("mode=async")
.contains("StudentName")
.contains("20260318测试")
.contains("TestPaperWordIdArraySize")
.contains("StudentWordsLatestSize")
.contains("TestPaperUnMasterWordsSize")
.contains("TestPaperMastedWordsSize")
.doesNotContain("TestPaperWordIdArray\":")
.doesNotContain("WordSpell\":\"w1")
.doesNotContain("lot")
.doesNotContain("father")
.doesNotContain("catch");
}
Insert this test near existing ACHIEVEMENT creation tests after createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent:
/** 覆盖成果报告原始报文日志场景,当 payload 包含数组时,应只记录数组大小而不记录数组内容。 */
@Test
void createAchievementReportLogsPayloadWithArraySizesOnly(CapturedOutput output) {
TestRepository repository = new TestRepository();
DefaultExamSprintReportApplicationService service = service(repository, reportId -> { }, new TestStorage());
service.createAchievementReport(validAchievementPayload());
assertThat(output.getAll())
.contains("exam_sprint_report_payload_received")
.contains("reportType=ACHIEVEMENT")
.contains("mode=async")
.contains("StudentName")
.contains("吴泓妤")
.contains("TestPaperImprovedWordsSize")
.contains("StudentWordsLatestSize")
.contains("StudentWordsFirstPreExamAssaultAfterSize")
.doesNotContain("TestPaperImprovedWords\":")
.doesNotContain("number")
.doesNotContain("ignored-large-array");
}
Run:
mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createOutlookReportLogsPayloadWithArraySizesOnly,ExamSprintReportApplicationServiceTest#createAchievementReportLogsPayloadWithArraySizesOnly test
Expected: FAIL because exam_sprint_report_payload_received, TestPaperWordIdArraySize, and TestPaperImprovedWordsSize are not logged yet.
Files:
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.javaTest: abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
[ ] Step 1: Add Jackson node imports
Update imports in DefaultExamSprintReportApplicationService.java:
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
Keep the existing ObjectNode import and add ArrayNode plus JsonNodeFactory beside it.
Replace the four public creation methods with:
@Override
public CreateExamSprintReportResponse createOutlookReport(JsonNode payload) {
logReceivedPayload(ReportType.OUTLOOK, "async", payload);
validateOutlookPayload(payload);
JsonNode normalizedPayload = normalizeOutlookPayload(payload);
return submitReportGeneration(ReportType.OUTLOOK, new UnmodeledReportContent(ReportType.OUTLOOK, normalizedPayload));
}
@Override
public CreateExamSprintReportResponse createAchievementReport(JsonNode payload) {
logReceivedPayload(ReportType.ACHIEVEMENT, "async", payload);
AchievementReportContent content = validateAchievementPayload(payload);
return submitReportGeneration(ReportType.ACHIEVEMENT, content);
}
@Override
public CreateExamSprintReportWithUrlResponse createOutlookReportSync(JsonNode payload) {
logReceivedPayload(ReportType.OUTLOOK, "sync", payload);
validateOutlookPayload(payload);
JsonNode normalizedPayload = normalizeOutlookPayload(payload);
return submitReportGenerationSync(ReportType.OUTLOOK, new UnmodeledReportContent(ReportType.OUTLOOK, normalizedPayload));
}
@Override
public CreateExamSprintReportWithUrlResponse createAchievementReportSync(JsonNode payload) {
logReceivedPayload(ReportType.ACHIEVEMENT, "sync", payload);
AchievementReportContent content = validateAchievementPayload(payload);
return submitReportGenerationSync(ReportType.ACHIEVEMENT, content);
}
Add these private methods before submitReportGeneration:
private void logReceivedPayload(ReportType reportType, String mode, JsonNode payload) {
log.info(
"exam_sprint_report_payload_received reportType={} mode={} payload={}",
reportType,
mode,
summarizePayloadForLog(payload));
}
private JsonNode summarizePayloadForLog(JsonNode payload) {
if (payload == null || payload.isNull() || payload.isMissingNode()) {
return payload;
}
if (payload.isObject()) {
ObjectNode summary = objectMapper.createObjectNode();
payload.fields().forEachRemaining(entry -> appendSummarizedField(summary, entry.getKey(), entry.getValue()));
return summary;
}
if (payload.isArray()) {
return JsonNodeFactory.instance.numberNode(payload.size());
}
return payload.deepCopy();
}
private void appendSummarizedField(ObjectNode summary, String fieldName, JsonNode value) {
if (value != null && value.isArray()) {
summary.put(fieldName + "Size", value.size());
return;
}
summary.set(fieldName, summarizePayloadForLog(value));
}
Run:
mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createOutlookReportLogsPayloadWithArraySizesOnly,ExamSprintReportApplicationServiceTest#createAchievementReportLogsPayloadWithArraySizesOnly test
Expected: PASS.
Files:
Test: abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
[ ] Step 1: Update existing learning-efficiency assertion
In createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent, replace:
assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("0.48");
with:
assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("5.263158");
This expectation comes from (4 / 19) / 0.04, rounded to 6 decimal places.
In createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero, add:
assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("0");
Run:
mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent,ExamSprintReportApplicationServiceTest#createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero test
Expected: FAIL because implementation still uses upstream ImproveStudyEfficiency.
Files:
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.javaTest: abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
[ ] Step 1: Introduce derived learning efficiency text
In toDomainContent, after masteryHitRateText add:
String learningEfficiencyText = learningEfficiencyText(payload.testPaperImprovedWordCount(), payload.studentImproveWordCount());
Then replace both occurrences of:
format(payload.improveStudyEfficiency())
with:
learningEfficiencyText
Add this method after masteryHitRateText:
private static String learningEfficiencyText(BigDecimal improvedWordCount, BigDecimal studentImproveWordCount) {
if (studentImproveWordCount.signum() == 0) {
return "0";
}
BigDecimal efficiency = improvedWordCount
.divide(studentImproveWordCount, 8, RoundingMode.HALF_UP)
.divide(new BigDecimal("0.04"), 6, RoundingMode.HALF_UP);
return format(efficiency);
}
Run:
mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent,ExamSprintReportApplicationServiceTest#createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero test
Expected: PASS.
Files:
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.javaVerify: pom.xml, abilities/exam-sprint/application/pom.xml
[ ] Step 1: Run full application-module test class
Run:
mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest test
Expected: PASS.
Run:
mvn -pl abilities/exam-sprint/application test
Expected: PASS.
Run:
git status --short
Expected: modified Java source/test files and this plan file only, unless local build outputs already existed.
OUTLOOK and ACHIEVEMENT, array-size-only behavior, and derived achievement learning efficiency are each covered by tests and implementation tasks.TBD, TODO, or unspecified implementation steps remain.JsonNode, ObjectNode, ObjectMapper, ReportType, BigDecimal, and RoundingMode types already present in the target classes.