# Exam Sprint Report Payload Summary And Efficiency 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:** Log incoming `OUTLOOK` and `ACHIEVEMENT` report payloads while replacing every array field with `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. --- ## File Structure - Modify `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java` - Add payload-received logs in the four report creation entry points. - Add recursive Jackson helper methods that preserve scalar/object fields and replace arrays with `Size` numeric fields. - Modify `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.java` - Compute learning efficiency as `(testPaperImprovedWordCount / studentImproveWordCount) / 0.04`. - Return `0` when `studentImproveWordCount` is zero. - Modify `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java` - Add log-capture tests for `OUTLOOK` and `ACHIEVEMENT` payload summaries. - Update achievement learning-efficiency assertions. ## Task 1: Payload Summary Logging Tests **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`: ```java /** 覆盖展望报告原始报文日志场景,当 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"); } ``` - [ ] **Step 2: Add an ACHIEVEMENT payload logging test** Insert this test near existing ACHIEVEMENT creation tests after `createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent`: ```java /** 覆盖成果报告原始报文日志场景,当 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"); } ``` - [ ] **Step 3: Run tests and verify the new tests fail before implementation** Run: ```bash 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. ## Task 2: Payload Summary Logging Implementation **Files:** - Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java` - Test: `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`: ```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. - [ ] **Step 2: Log payload summaries in all report creation entry points** Replace the four public creation methods with: ```java @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); } ``` - [ ] **Step 3: Add recursive payload summary helpers** Add these private methods before `submitReportGeneration`: ```java 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)); } ``` - [ ] **Step 4: Run payload logging tests and verify pass** Run: ```bash mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createOutlookReportLogsPayloadWithArraySizesOnly,ExamSprintReportApplicationServiceTest#createAchievementReportLogsPayloadWithArraySizesOnly test ``` Expected: PASS. ## Task 3: Achievement Learning Efficiency Tests **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: ```java assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("0.48"); ``` with: ```java assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("5.263158"); ``` This expectation comes from `(4 / 19) / 0.04`, rounded to 6 decimal places. - [ ] **Step 2: Add zero-denominator assertion** In `createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero`, add: ```java assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("0"); ``` - [ ] **Step 3: Run tests and verify they fail before implementation** Run: ```bash mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent,ExamSprintReportApplicationServiceTest#createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero test ``` Expected: FAIL because implementation still uses upstream `ImproveStudyEfficiency`. ## Task 4: Achievement Learning Efficiency Implementation **Files:** - Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.java` - Test: `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: ```java String learningEfficiencyText = learningEfficiencyText(payload.testPaperImprovedWordCount(), payload.studentImproveWordCount()); ``` Then replace both occurrences of: ```java format(payload.improveStudyEfficiency()) ``` with: ```java learningEfficiencyText ``` - [ ] **Step 2: Add the calculation helper** Add this method after `masteryHitRateText`: ```java 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); } ``` - [ ] **Step 3: Run learning-efficiency tests and verify pass** Run: ```bash mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent,ExamSprintReportApplicationServiceTest#createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero test ``` Expected: PASS. ## Task 5: Regression Verification **Files:** - Test: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java` - Verify: `pom.xml`, `abilities/exam-sprint/application/pom.xml` - [ ] **Step 1: Run full application-module test class** Run: ```bash mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest test ``` Expected: PASS. - [ ] **Step 2: Run application module tests** Run: ```bash mvn -pl abilities/exam-sprint/application test ``` Expected: PASS. - [ ] **Step 3: Inspect working tree** Run: ```bash git status --short ``` Expected: modified Java source/test files and this plan file only, unless local build outputs already existed. ## Self-Review - Spec coverage: payload logging for `OUTLOOK` and `ACHIEVEMENT`, array-size-only behavior, and derived achievement learning efficiency are each covered by tests and implementation tasks. - Placeholder scan: no `TBD`, `TODO`, or unspecified implementation steps remain. - Type consistency: helper methods use existing `JsonNode`, `ObjectNode`, `ObjectMapper`, `ReportType`, `BigDecimal`, and `RoundingMode` types already present in the target classes.