2026-05-06-exam-sprint-report-payload-summary-and-efficiency.md 13 KB

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:

    /** 覆盖展望报告原始报文日志场景,当 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:

    /** 覆盖成果报告原始报文日志场景,当 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:

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:

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:

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

    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:

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:

        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.

  • Step 2: Add zero-denominator assertion

In createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero, add:

        assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("0");
  • Step 3: Run tests and verify they fail before implementation

Run:

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:

        String learningEfficiencyText = learningEfficiencyText(payload.testPaperImprovedWordCount(), payload.studentImproveWordCount());

Then replace both occurrences of:

format(payload.improveStudyEfficiency())

with:

learningEfficiencyText
  • Step 2: Add the calculation helper

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);
    }
  • Step 3: Run learning-efficiency tests and verify pass

Run:

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:

mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest test

Expected: PASS.

  • Step 2: Run application module tests

Run:

mvn -pl abilities/exam-sprint/application test

Expected: PASS.

  • Step 3: Inspect working tree

Run:

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.