# 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 `<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.

---

## 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 `<fieldName>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.
