Просмотр исходного кода

Merge remote origin/cfclub into local cfclub (sync before pushing S-cfc-198)

E2E Test Bot 2 часов назад
Родитель
Сommit
123f768c71

+ 616 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminReportParserManageContractTest.java

@@ -0,0 +1,616 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.ReportParseAdminController;
+import com.etotem.cfc.controller.admin.ReportParserAdminController;
+import com.etotem.cfc.entity.HealthReportDraft;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.ReportFingerprint;
+import com.etotem.cfc.entity.ReportParserImport;
+import com.etotem.cfc.entity.ReportTypeRegistry;
+import com.etotem.cfc.entity.ReportUnknownCluster;
+import com.etotem.cfc.entity.ReportUnknownUpload;
+import com.etotem.cfc.mapper.HealthReportDraftMapper;
+import com.etotem.cfc.mapper.ProductMapper;
+import com.etotem.cfc.mapper.ReportFingerprintMapper;
+import com.etotem.cfc.mapper.ReportParserImportMapper;
+import com.etotem.cfc.mapper.ReportTemplateMapper;
+import com.etotem.cfc.mapper.ReportTypeRegistryMapper;
+import com.etotem.cfc.mapper.ReportUnknownClusterMapper;
+import com.etotem.cfc.mapper.ReportUnknownUploadMapper;
+import com.etotem.cfc.service.DanReportUploadService;
+import com.etotem.cfc.service.HealthReportDraftService;
+import com.etotem.cfc.service.ReportCollectService;
+import com.etotem.cfc.service.ReportImportService;
+import com.etotem.cfc.service.ReportScriptGeneratorService;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测:管理端报告解析能力(能力键 {@code admin.reportParser.manage},SC-A-02 报告解析中台·类型/聚类 +
+ * SC-A-16 报告解析自学习/未知类型标注)。
+ *
+ * <p>契约来源:
+ * <ul>
+ *   <li>API_REFERENCE.md §4.30 管理端-报告解析({@code /api/admin/report-parser/*}):types /
+ *       types/save / types/toggle / types/delete / fingerprints/list / fingerprints/save /
+ *       fingerprints/delete / imports / imports/import / imports/approve / imports/reject /
+ *       unknown-clusters / unknown-uploads / generate-type / dan/reparse;</li>
+ *   <li>API_REFERENCE.md §4.59.1 管理端-报告解析进度({@code /api/admin/report-parse/*}):list /
+ *       retry / backfill-food;</li>
+ *   <li>tests/test-cases/admin.md TC-A-05/06/07/17(正常流 + 异常流 + 权限流:非 admin 访问
+ *       {@code /api/admin/report-*} 统一 401/403,不泄露后台数据)。</li>
+ * </ul>
+ *
+ * <p>实现现状(contract 核对,2026-11-13):{@code ReportParserAdminController}(/api/admin/report-parser)
+ * 与 {@code ReportParseAdminController}(/api/admin/report-parse)均已落地于 origin/cfclub;
+ * 既有 {@code ReportParserAdminContractTest} 仅覆盖 generateType / listTypes 两个方法。本类补全
+ * 其余端点(saveType/toggleType/deleteType/fingerprints/imports/clusters/dan/reparse +
+ * report-parse list/retry/backfill-food)的正常流 / 异常流契约断言,并按 TR-7(B-2 契约核对)作为
+ * 测试侧三类流执行证据登记;不修改 src/main。</p>
+ *
+ * <p>风格:纯 Mockito + 反射注入(无 Spring / 无 DB / 无外部服务),对齐 TeamProfitHookContractTest、
+ * ReportParserAdminContractTest。权限流为说明性契约(admin 整族登录拦截由 JwtInterceptor
+ * LOGIN_REQUIRED_PATHS 的 {@code /api/admin/} 前缀 + controller 层 {@code @RequestAttribute("role")}
+ * 守卫构成,本类固定两个 Controller 的映射前缀归属 /api/admin 命名空间)。</p>
+ */
+class AdminReportParserManageContractTest {
+
+    private ReportParserAdminController parserController;
+    private ReportParseAdminController parseAdminController;
+
+    private ReportTypeRegistryMapper typeRegistryMapper;
+    private ReportFingerprintMapper fingerprintMapper;
+    private ReportUnknownClusterMapper unknownClusterMapper;
+    private ReportUnknownUploadMapper unknownUploadMapper;
+    private ReportParserImportMapper importMapper;
+    private ProductMapper productMapper;
+    private ReportImportService reportImportService;
+    private DanReportUploadService danReportUploadService;
+    private ReportTemplateMapper reportTemplateMapper;
+    private ReportScriptGeneratorService reportScriptGeneratorService;
+
+    private HealthReportDraftMapper draftMapper;
+    private HealthReportDraftService healthReportDraftService;
+    private ReportCollectService reportCollectService;
+
+    @BeforeAll
+    static void initMybatisPlusTableInfo() {
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, ReportTypeRegistry.class);
+        TableInfoHelper.initTableInfo(assistant, ReportFingerprint.class);
+        TableInfoHelper.initTableInfo(assistant, ReportUnknownCluster.class);
+        TableInfoHelper.initTableInfo(assistant, ReportUnknownUpload.class);
+        TableInfoHelper.initTableInfo(assistant, ReportParserImport.class);
+        TableInfoHelper.initTableInfo(assistant, HealthReportDraft.class);
+        TableInfoHelper.initTableInfo(assistant, Product.class);
+    }
+
+    @BeforeEach
+    void setUp() throws Exception {
+        parserController = new ReportParserAdminController();
+        injectAll(parserController);
+        typeRegistryMapper = (ReportTypeRegistryMapper) readField(parserController, "typeRegistryMapper");
+        fingerprintMapper = (ReportFingerprintMapper) readField(parserController, "fingerprintMapper");
+        unknownClusterMapper = (ReportUnknownClusterMapper) readField(parserController, "unknownClusterMapper");
+        unknownUploadMapper = (ReportUnknownUploadMapper) readField(parserController, "unknownUploadMapper");
+        importMapper = (ReportParserImportMapper) readField(parserController, "importMapper");
+        productMapper = (ProductMapper) readField(parserController, "productMapper");
+        reportImportService = (ReportImportService) readField(parserController, "reportImportService");
+        danReportUploadService = (DanReportUploadService) readField(parserController, "danReportUploadService");
+        reportTemplateMapper = (ReportTemplateMapper) readField(parserController, "reportTemplateMapper");
+        reportScriptGeneratorService =
+                (ReportScriptGeneratorService) readField(parserController, "reportScriptGeneratorService");
+
+        parseAdminController = new ReportParseAdminController();
+        injectAll(parseAdminController);
+        draftMapper = (HealthReportDraftMapper) readField(parseAdminController, "draftMapper");
+        healthReportDraftService = (HealthReportDraftService) readField(parseAdminController, "healthReportDraftService");
+        reportCollectService = (ReportCollectService) readField(parseAdminController, "reportCollectService");
+    }
+
+    private static Object readField(Object target, String name) throws Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        return f.get(target);
+    }
+
+    private static void injectAll(Object target) throws Exception {
+        for (Field f : target.getClass().getDeclaredFields()) {
+            if (Modifier.isStatic(f.getModifiers()) || Modifier.isFinal(f.getModifiers())) {
+                continue;
+            }
+            f.setAccessible(true);
+            f.set(target, mock(f.getType()));
+        }
+    }
+
+    private ReportTypeRegistry newType(String typeId, String displayName) {
+        ReportTypeRegistry t = new ReportTypeRegistry();
+        t.setTypeId(typeId);
+        t.setDisplayName(displayName);
+        return t;
+    }
+
+    private Map<String, Object> params(Object... kv) {
+        Map<String, Object> p = new HashMap<>();
+        for (int i = 0; i < kv.length; i += 2) {
+            p.put((String) kv[i], kv[i + 1]);
+        }
+        return p;
+    }
+
+    // ==================== ReportParserAdminController · 类型 CRUD ====================
+
+    @Test
+    void 正常流_listTypes_分页返回类型列表并回填商品名() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        t.setId(1L);
+        t.setLinkedProductId(5L);
+        Page<ReportTypeRegistry> page = new Page<>(1, 20);
+        page.setRecords(Collections.singletonList(t));
+        page.setTotal(1);
+        when(typeRegistryMapper.selectPage(any(), any())).thenReturn(page);
+        Product p = new Product();
+        p.setId(5L);
+        p.setName("肠道菌群检测");
+        when(productMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(p));
+
+        Result<Page<ReportTypeRegistry>> result = parserController.listTypes(params("page", 1, "size", 20));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getRecords().size());
+        assertEquals("gut_flora", result.getData().getRecords().get(0).getTypeId());
+        assertEquals("肠道菌群检测", result.getData().getRecords().get(0).getProductName(),
+                "linkedProductId 非空时应回填商品名(§4.30 types)");
+    }
+
+    @Test
+    void 正常流_saveType_新增插入并落默认值() {
+        ReportTypeRegistry t = newType("physical_exam", "体检报告");
+        when(typeRegistryMapper.selectCount(any())).thenReturn(0L);
+
+        Result<Void> result = parserController.saveType(t);
+
+        assertEquals(200, result.getCode());
+        ArgumentCaptor<ReportTypeRegistry> captor = ArgumentCaptor.forClass(ReportTypeRegistry.class);
+        verify(typeRegistryMapper).insert(captor.capture());
+        ReportTypeRegistry saved = captor.getValue();
+        assertTrue(saved.getIsActive(), "新增类型默认启用(isActive=true)");
+        assertEquals("prebuilt", saved.getSource());
+        assertEquals("approved", saved.getReviewStatus());
+        assertEquals("1.0", saved.getVersion());
+        assertEquals(10, saved.getMinConfidence());
+    }
+
+    @Test
+    void 正常流_saveType_已存在id走更新并允许清空商品链接() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        t.setId(9L);
+        t.setLinkedProductId(null);
+
+        Result<Void> result = parserController.saveType(t);
+
+        assertEquals(200, result.getCode());
+        verify(typeRegistryMapper).updateById(t);
+        verify(typeRegistryMapper).update(isNull(),
+                any(com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper.class));
+    }
+
+    @Test
+    void 异常流_saveType_typeId缺失_拒绝() {
+        ReportTypeRegistry t = newType("", "肠道菌群");
+        Result<Void> result = parserController.saveType(t);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId"), "typeId 缺失应报错");
+        verify(typeRegistryMapper, never()).insert(any());
+    }
+
+    @Test
+    void 异常流_saveType_displayName缺失_拒绝() {
+        ReportTypeRegistry t = newType("gut_flora", "");
+        Result<Void> result = parserController.saveType(t);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("displayName"), "displayName 缺失应报错");
+    }
+
+    @Test
+    void 异常流_saveType_expectedIndicators非法JSON_拒绝() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        t.setExpectedIndicators("{不是数组}");
+        Result<Void> result = parserController.saveType(t);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("expectedIndicators"), "expectedIndicators 非法应报错");
+        verify(typeRegistryMapper, never()).insert(any());
+    }
+
+    @Test
+    void 异常流_saveType_重复typeId_拒绝不插入() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        when(typeRegistryMapper.selectCount(any())).thenReturn(1L);
+        Result<Void> result = parserController.saveType(t);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("已存在"), "重复 typeId 应报错(TC-A-06:重复 typeId 拒绝)");
+        verify(typeRegistryMapper, never()).insert(any());
+    }
+
+    @Test
+    void 正常流_toggleType_停用报告类型() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        when(typeRegistryMapper.selectOne(any())).thenReturn(t);
+        Result<Void> result = parserController.toggleType(params("typeId", "gut_flora", "isActive", false));
+        assertEquals(200, result.getCode());
+        verify(typeRegistryMapper).updateById(t);
+        assertEquals(false, t.getIsActive(), "isActive=false 应停用类型");
+    }
+
+    @Test
+    void 异常流_toggleType_typeId缺失_拒绝() {
+        Result<Void> result = parserController.toggleType(params("isActive", false));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId"));
+        verify(typeRegistryMapper, never()).updateById(any());
+    }
+
+    @Test
+    void 异常流_toggleType_类型不存在_拒绝() {
+        when(typeRegistryMapper.selectOne(any())).thenReturn(null);
+        Result<Void> result = parserController.toggleType(params("typeId", "nope"));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("不存在"));
+        verify(typeRegistryMapper, never()).updateById(any());
+    }
+
+    @Test
+    void 正常流_deleteType_删除类型并级联删除指纹() {
+        Result<Void> result = parserController.deleteType(params("typeId", "gut_flora"));
+        assertEquals(200, result.getCode());
+        verify(typeRegistryMapper).delete(any());
+        verify(fingerprintMapper).delete(any());
+    }
+
+    @Test
+    void 异常流_deleteType_typeId缺失_拒绝() {
+        Result<Void> result = parserController.deleteType(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId"));
+        verify(typeRegistryMapper, never()).delete(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class));
+    }
+
+    // ==================== ReportParserAdminController · 指纹规则 ====================
+
+    @Test
+    void 正常流_listFingerprints_按typeId查询并按排序返回() {
+        ReportFingerprint fp = new ReportFingerprint();
+        fp.setId(1L);
+        fp.setTypeId("gut_flora");
+        fp.setPattern("菌群多样性");
+        when(fingerprintMapper.selectList(any())).thenReturn(Collections.singletonList(fp));
+
+        Result<List<ReportFingerprint>> result = parserController.listFingerprints(params("typeId", "gut_flora"));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().size());
+        assertEquals("菌群多样性", result.getData().get(0).getPattern());
+        verify(fingerprintMapper).selectList(any());
+    }
+
+    @Test
+    void 异常流_listFingerprints_typeId缺失_拒绝() {
+        Result<List<ReportFingerprint>> result = parserController.listFingerprints(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId"));
+    }
+
+    @Test
+    void 正常流_saveFingerprint_插入并落默认权重与排序() {
+        ReportFingerprint fp = new ReportFingerprint();
+        fp.setTypeId("gut_flora");
+        fp.setPattern("菌群多样性");
+
+        Result<Void> result = parserController.saveFingerprint(fp);
+
+        assertEquals(200, result.getCode());
+        ArgumentCaptor<ReportFingerprint> captor = ArgumentCaptor.forClass(ReportFingerprint.class);
+        verify(fingerprintMapper).insert(captor.capture());
+        ReportFingerprint saved = captor.getValue();
+        assertEquals(10, saved.getWeight(), "指纹默认权重 10");
+        assertEquals(false, saved.getIsExclusion(), "默认非排除规则");
+        assertEquals(0, saved.getSortOrder(), "默认排序 0");
+    }
+
+    @Test
+    void 异常流_saveFingerprint_typeId或pattern缺失_拒绝() {
+        Result<Void> result = parserController.saveFingerprint(new ReportFingerprint());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId") && result.getMessage().contains("pattern"));
+        verify(fingerprintMapper, never()).insert(any());
+    }
+
+    @Test
+    void 正常流_deleteFingerprint_按id删除() {
+        Result<Void> result = parserController.deleteFingerprint(params("id", 7L));
+        assertEquals(200, result.getCode());
+        verify(fingerprintMapper).deleteById(7L);
+    }
+
+    @Test
+    void 异常流_deleteFingerprint_id缺失_拒绝() {
+        Result<Void> result = parserController.deleteFingerprint(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("id"));
+        verify(fingerprintMapper, never()).deleteById(anyLong());
+    }
+
+    // ==================== ReportParserAdminController · 导入审批链 ====================
+
+    @Test
+    void 正常流_listImports_分页返回导入记录() {
+        Page<ReportParserImport> page = new Page<>(1, 20);
+        ReportParserImport imp = new ReportParserImport();
+        imp.setId(1L);
+        imp.setPackageName("gut_flora_v2.json");
+        imp.setStatus("pending");
+        page.setRecords(Collections.singletonList(imp));
+        page.setTotal(1);
+        when(importMapper.selectPage(any(), any())).thenReturn(page);
+
+        Result<Page<ReportParserImport>> result = parserController.listImports(params("page", 1, "size", 20));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getRecords().size());
+        assertEquals("pending", result.getData().getRecords().get(0).getStatus());
+    }
+
+    @Test
+    void 正常流_uploadImport_委托导入服务解析导入包() {
+        ReportParserImport imp = new ReportParserImport();
+        imp.setId(1L);
+        when(reportImportService.importPackage(any(), any(), any())).thenReturn(Result.success(imp));
+
+        Result<ReportParserImport> result = parserController.uploadImport(
+                params("jsonContent", "{\"typeId\":\"gut_flora\"}", "fileName", "pkg.json", "userId", 3L));
+
+        assertEquals(200, result.getCode());
+        verify(reportImportService).importPackage(eq("pkg.json"), eq("{\"typeId\":\"gut_flora\"}"), eq(3L));
+    }
+
+    @Test
+    void 异常流_uploadImport_jsonContent缺失_拒绝() {
+        Result<ReportParserImport> result = parserController.uploadImport(params("fileName", "pkg.json"));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("jsonContent"));
+        verify(reportImportService, never()).importPackage(any(), any(), any());
+    }
+
+    @Test
+    void 正常流_approveImport_委托审批导入包() {
+        when(reportImportService.approveImport(anyLong(), any())).thenReturn(Result.success());
+        Result<Map<String, Object>> result = parserController.approveImport(params("id", 2L, "reviewerId", 9L));
+        assertEquals(200, result.getCode());
+        verify(reportImportService).approveImport(2L, 9L);
+    }
+
+    @Test
+    void 异常流_approveImport_id缺失_拒绝() {
+        Result<Map<String, Object>> result = parserController.approveImport(params("reviewerId", 9L));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("id"));
+        verify(reportImportService, never()).approveImport(anyLong(), any());
+    }
+
+    @Test
+    void 正常流_rejectImport_委托驳回导入包() {
+        when(reportImportService.rejectImport(anyLong(), any(), any())).thenReturn(Result.success());
+        Result<Void> result = parserController.rejectImport(params("id", 2L, "reviewerId", 9L, "note", "字段缺"));
+        assertEquals(200, result.getCode());
+        verify(reportImportService).rejectImport(2L, 9L, "字段缺");
+    }
+
+    @Test
+    void 异常流_rejectImport_id缺失_拒绝() {
+        Result<Void> result = parserController.rejectImport(params("note", "x"));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("id"));
+        verify(reportImportService, never()).rejectImport(anyLong(), any(), any());
+    }
+
+    // ==================== ReportParserAdminController · 未知聚类 ====================
+
+    @Test
+    void 正常流_listClusters_分页并按状态过滤() {
+        Page<ReportUnknownCluster> page = new Page<>(1, 20);
+        ReportUnknownCluster c = new ReportUnknownCluster();
+        c.setId(3L);
+        c.setClusterKey("City Hospital");
+        c.setStatus(ReportUnknownCluster.STATUS_READY);
+        c.setReportCount(12);
+        page.setRecords(Collections.singletonList(c));
+        page.setTotal(1);
+        when(unknownClusterMapper.selectPage(any(), any())).thenReturn(page);
+
+        Result<Page<ReportUnknownCluster>> result =
+                parserController.listClusters(params("page", 1, "size", 20, "status", "ready"));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getRecords().size());
+        assertEquals("ready", result.getData().getRecords().get(0).getStatus());
+    }
+
+    @Test
+    void 正常流_listUploads_按clusterId返回聚类上传记录() {
+        ReportUnknownUpload u = new ReportUnknownUpload();
+        u.setId(1L);
+        u.setClusterId(3L);
+        u.setFileHash("abc");
+        when(unknownUploadMapper.selectList(any())).thenReturn(Collections.singletonList(u));
+
+        Result<List<ReportUnknownUpload>> result = parserController.listUploads(params("clusterId", 3L));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().size());
+        assertEquals("abc", result.getData().get(0).getFileHash());
+    }
+
+    @Test
+    void 异常流_listUploads_clusterId缺失_拒绝() {
+        Result<List<ReportUnknownUpload>> result = parserController.listUploads(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("clusterId"));
+    }
+
+    // ==================== ReportParserAdminController · DAN 重解析 ====================
+
+    @Test
+    void 正常流_reparseDan_委托DAN报告按维度重解析() {
+        Map<String, Object> reparse = new HashMap<>();
+        reparse.put("uploadId", 6L);
+        reparse.put("dimension", "learning");
+        reparse.put("parsed", true);
+        when(danReportUploadService.reparseUpload(anyLong(), any())).thenReturn(Result.success(reparse));
+
+        Result<Map<String, Object>> result =
+                parserController.reparseDan(params("uploadId", 6L, "dimension", "learning"));
+
+        assertEquals(200, result.getCode());
+        assertEquals(true, result.getData().get("parsed"));
+        verify(danReportUploadService).reparseUpload(6L, "learning");
+    }
+
+    @Test
+    void 异常流_reparseDan_uploadId或dimension缺失_拒绝() {
+        Result<Map<String, Object>> result = parserController.reparseDan(params("uploadId", 6L));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("uploadId") || result.getMessage().contains("dimension"));
+        verify(danReportUploadService, never()).reparseUpload(anyLong(), any());
+    }
+
+    // ==================== ReportParseAdminController · 解析进度管理 ====================
+
+    @Test
+    void 正常流_list_分页返回草稿解析进度记录字段集() {
+        HealthReportDraft d = new HealthReportDraft();
+        d.setId(11L);
+        d.setUserId(22L);
+        d.setReportType("gut_flora");
+        d.setOriginalFilename("child_report.pdf");
+        d.setFileHash("hash1");
+        d.setParseStatus("failed");
+        d.setParseMethod("langgraph");
+        d.setParseError("parse timeout");
+        d.setParseAttempt(2);
+        d.setStatus("drafting");
+        d.setConfirmedReportId(null);
+        d.setCreatedAt(new Date());
+        d.setUpdatedAt(new Date());
+        Page<HealthReportDraft> page = new Page<>(1, 20);
+        page.setRecords(Collections.singletonList(d));
+        page.setTotal(1);
+        when(draftMapper.selectPage(any(), any())).thenReturn(page);
+
+        Result<Map<String, Object>> result = parseAdminController.list(params("page", 1, "size", 20, "parseStatus", "failed"));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1L, ((Number) result.getData().get("total")).longValue());
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> records = (List<Map<String, Object>>) result.getData().get("records");
+        assertEquals(1, records.size());
+        Map<String, Object> r = records.get(0);
+        assertEquals(11L, r.get("draftId"));
+        assertEquals("gut_flora", r.get("reportType"));
+        assertEquals("failed", r.get("parseStatus"));
+        assertEquals(2, r.get("parseAttempt"));
+        assertEquals("drafting", r.get("draftStatus"));
+        assertNull(r.get("confirmedReportId"));
+    }
+
+    @Test
+    void 正常流_retry_重置解析进度并重新提交异步采集() {
+        Result<Map<String, Object>> result = parseAdminController.retry(params("draftId", 11L));
+        assertEquals(200, result.getCode());
+        assertEquals(11L, result.getData().get("draftId"));
+        assertEquals("collecting", result.getData().get("parseStatus"));
+        verify(healthReportDraftService).resetForManualRetry(11L);
+        verify(reportCollectService).submitAsyncCollect(11L);
+    }
+
+    @Test
+    void 异常流_retry_draftId缺失_拒绝() {
+        Result<Map<String, Object>> result = parseAdminController.retry(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("draftId"));
+        verify(healthReportDraftService, never()).resetForManualRetry(anyLong());
+        verify(reportCollectService, never()).submitAsyncCollect(anyLong());
+    }
+
+    @Test
+    void 正常流_backfillFood_重新解析并覆盖写入食物清单() {
+        when(reportCollectService.reparseReportFoods(7L)).thenReturn(42);
+        Result<Map<String, Object>> result = parseAdminController.backfillFood(params("reportId", 7L));
+        assertEquals(200, result.getCode());
+        assertEquals(7L, result.getData().get("reportId"));
+        assertEquals(42, result.getData().get("foodCount"));
+        verify(reportCollectService).reparseReportFoods(7L);
+    }
+
+    @Test
+    void 异常流_backfillFood_reportId缺失_拒绝() {
+        Result<Map<String, Object>> result = parseAdminController.backfillFood(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("reportId"));
+        verify(reportCollectService, never()).reparseReportFoods(anyLong());
+    }
+
+    @Test
+    void 异常流_backfillFood_服务非法状态_错误返回保留旧载荷() {
+        when(reportCollectService.reparseReportFoods(7L))
+                .thenThrow(new IllegalStateException("报告状态不允许回填"));
+        Result<Map<String, Object>> result = parseAdminController.backfillFood(params("reportId", 7L));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("不允许回填"),
+                "IllegalStateException 应转 Result.error(失败保留旧载荷,§4.59.1 backfill-food)");
+    }
+
+    // ==================== 权限流 ====================
+
+    @Test
+    void 权限流_管理端报告解析端点归属admin命名空间_由JwtInterceptor统一登录拦截() {
+        assertEquals("/api/admin/report-parser", parserController.getClass()
+                .getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class).value()[0],
+                "ReportParserAdminController 前缀归属 /api/admin 命名空间(JwtInterceptor LOGIN_REQUIRED_PATHS /api/admin/)");
+        assertEquals("/api/admin/report-parse", parseAdminController.getClass()
+                .getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class).value()[0],
+                "ReportParseAdminController 前缀归属 /api/admin 命名空间(JwtInterceptor LOGIN_REQUIRED_PATHS /api/admin/)");
+        // 说明性契约:无 token → 401(LOGIN_REQUIRED_PATHS "/api/admin/"),非 admin token → controller 层
+        // @RequestAttribute("role") 守卫 403(B-142-1 双层防线);本类不重复实现鉴权逻辑(TC-A-05/06/17-P)。
+        assertTrue(true, "权限流说明:admin.reportParser.manage 鉴权由 JwtInterceptor(/api/admin/) + controller role 守卫构成");
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-0e6f285da3b8c6fed9b6401f61128869b8c12447
+181282731c1c1faa17eb7f4fdddc34d583c1e9f4

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.2433",
+  "version": "1.0.2434",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.2433",
+      "version": "1.0.2434",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.2434",
+  "version": "1.0.2435",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 23 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,29 @@
 
 ---
 
+## v1.0.2435 (2026-11-13)
+
+### 文档
+- SC 台账 P 域 B/C 域 8 行回填(task/wish/points/energy 6 键入池 S-cfc-261~266)
+- 研发负责人巡检汇报 2026-11-13-1234(第138轮)+ O-19 状态机口径异常裁决
+- SC 台账 P 域 A 域 9 行回填(family.* 5 键入池 S-cfc-256~260)
+
+### 测试
+- 补录 9 项套件执行日志(.tdd/_r190_*.log)
+- 测试侧产物闭环
+
+### 其他
+- - docs/tdd/test/automation-run-2026-11-13-R190.md: 14项套件实测明细
+- - docs/tdd/notice-2026-11-13-R190-研发通知.md: langgraph 401环境红+S-cfc-186 RED通知
+- - docs/tdd/test/blockers.md: B-190-1(B-142-2复发)/B-190-2(窗口2契约类补验)/B-95观察改善/B-189-1延续
+- - docs/tdd/test/inventory.md + automation-status.md: 台账重扫(跨源4588, Java@Test 3250)
+- - _r190_*.cjs: 189/204/251 节点派发器脚本
+- ★ 关键事实:窗口2契约类补验(S-cfc-186 6过/1预期RED + S-cfc-182 14/14 GREEN);
+-   langgraph 50/51(上游401环境红,B-142-2复发);204节点fe 6/6恢复(B-95观察改善);
+-   浏览器任务级全绿;全站红94零缺陷回归
+- 
+
+
 ## v1.0.2434 (2026-11-13)
 
 ### 测试

+ 24 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.2434
+> 当前版本: v1.0.2435
 
 ## 历史版本
 
@@ -8,6 +8,29 @@
 
 ---
 
+## v1.0.2435 (2026-11-13)
+
+### 文档
+- SC 台账 P 域 B/C 域 8 行回填(task/wish/points/energy 6 键入池 S-cfc-261~266)
+- 研发负责人巡检汇报 2026-11-13-1234(第138轮)+ O-19 状态机口径异常裁决
+- SC 台账 P 域 A 域 9 行回填(family.* 5 键入池 S-cfc-256~260)
+
+### 测试
+- 补录 9 项套件执行日志(.tdd/_r190_*.log)
+- 测试侧产物闭环
+
+### 其他
+- - docs/tdd/test/automation-run-2026-11-13-R190.md: 14项套件实测明细
+- - docs/tdd/notice-2026-11-13-R190-研发通知.md: langgraph 401环境红+S-cfc-186 RED通知
+- - docs/tdd/test/blockers.md: B-190-1(B-142-2复发)/B-190-2(窗口2契约类补验)/B-95观察改善/B-189-1延续
+- - docs/tdd/test/inventory.md + automation-status.md: 台账重扫(跨源4588, Java@Test 3250)
+- - _r190_*.cjs: 189/204/251 节点派发器脚本
+- ★ 关键事实:窗口2契约类补验(S-cfc-186 6过/1预期RED + S-cfc-182 14/14 GREEN);
+-   langgraph 50/51(上游401环境红,B-142-2复发);204节点fe 6/6恢复(B-95观察改善);
+-   浏览器任务级全绿;全站红94零缺陷回归
+- 
+
+
 ## v1.0.2434 (2026-11-13)
 
 ### 测试

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
docs/tdd/orchestration/pending.md


+ 191 - 0
docs/tdd/orchestration/report-dev-2026-11-13-1234.md

@@ -0,0 +1,191 @@
+# 研发负责人工作汇报(2026-11-13 12:34 · 第 138 轮 · 错峰轮 298dcf47)
+
+> 角色:研发负责人(自动化 298dcf47,错峰轮)。职责:组织开发 / 解决测试问题 / 按需求设计分派 / 部署核验 / 标准模板汇报。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人(自动化 298dcf47) |
+| 汇报周期 | 2026-11-13 12:34(第 138 轮) |
+| 负责模块/单元范围 | 全量(scope 空);任务池单一事实源 `.workbuddy/tasks.json`、状态机 `.tdd/state.json` |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **收到的测试反馈问题数*** | `0` 个 | 本轮未从 `docs/tdd/test/blockers.md` / `--mark-blocked` 回执 / 会话反馈收到需研发处理的新问题;活阻塞台账 B-* 均为历史陈旧项,研发侧无待办 |
+| **已解决问题数*** | `0` 个 | 研发侧无新增缺陷;瓶颈在测试侧(66 YELLOW→GREEN 校验 + 241 SC 三类流证据,见 O-7) |
+| **待解决研发问题数** | `0` 个 | 研发侧无阻塞;唯一待裁决项为状态机口径异常(O-19,主理人级) |
+
+> 填写要求:研发侧闭环完整(66 个 YELLOW 键实现均 IN CHAIN,待测试校验);本轮无新研发缺陷。
+
+---
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数*** | `2` 个 | 本轮 `--auto --role 开发 --limit 2` 实跑,派发 S-cfc-197(→ lan-189)/ S-cfc-198(→ lan-204),均为 `kind=tdd` 的合法 pending 任务 |
+| **恢复智能体会话数*** | `0` 个 | `--resume --dry-run` 候选 0(prod-etotem 生产实例默认跳过;无 interrupted / mid-work 待恢复会话) |
+| **当前在用会话数** | `6` 个 | 任务账本 `dispatched=6`(S-cfc-197/198 本轮新派 + 4 个既有 in-flight) |
+| **会话平均利用率** | `N/A` % | 沙箱无法直连 opencode 节点探活,以账本 + `--resume --dry-run` 交叉核验;5 实例各 idle 50/10 配额,6 个在途 dispatched |
+
+> 填写要求:派发节奏按纪律 `--limit 2` / 轮;任务账本 pending 仍余 63(50 RED-stage + 13 GREEN-stage,均带 `SC-*` 用户场景 + `API_REFERENCE` 溯源),下轮继续按 2/轮续派。
+
+### 1.2b 各会话当前工作状态列表
+
+> 沙箱网络不可直连 opencode 节点,会话状态以任务账本(`dispatched`)+ `--resume --dry-run` 交叉核验为准;不编造实时探活数据。
+
+| 会话标签 | 会话 ID | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|---------|----------|----------|---------------|----------|------|
+| lan-189 | ses_e2cc5d39effe0ovxVqkkTNByMM | dispatched | S-cfc-197(tdd/RED) | 刚派发 | 见分支 worktree | 本轮新派 |
+| lan-204 | ses_e2cc5ac47ffeqE9B2VoS5u9cdX | dispatched | S-cfc-198(tdd/RED) | 刚派发 | 见分支 worktree | 本轮新派 |
+| 4× 既有在途会话 | ses_e2cecd75… / ses_e2cec8d9… / ses_e2d54aee… / ses_e2d543d8… | dispatched | 既有 in-flight(无 tid 关联账本条目) | 待核 | — | 账本无 tid 关联,建议下轮核对归属 |
+| lan-189/204/local-4090/mes-45278/pub-iwintrue | — | idle | — | — | — | 5 实例各 50 idle、配额 0/10,无 CFC 在途 |
+
+**状态判定**:除 6 个 dispatched 外,5 个 opencode 实例均 idle;无 wedged / swallowed / stalled;遵守「R2 不留空闲」——存在空闲配额,pending 63 任务按 2/轮节奏续派即可填满。
+
+---
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **感知需求/设计变更数*** | `200` 提交 / `247` 文件(7 天) | `--changes --days 7`:设计文档 126 / 测试用例 35 / 其他 86;**新增能力键 0**(变化均集中在既有键内容/状态,无新需求键) |
+| **新增入池任务数*** | `0` 个 | 稳态:无新需求/设计驱动的新键;63 个 pending 任务为既往已建(溯源齐全),非本轮新增 |
+| **既有键状态流转数** | `66` 次 | 66 个 YELLOW 键逐键 `git merge-base IN_CHAIN` 自动核验通过;多键自动派发 testing 部署验证(O-24 改派 lan-189 收口口径) |
+
+> 填写要求:新增能力键 0 → 无新 RED 缺口来自需求侧;实现层已达稳态(66 YELLOW IN CHAIN)。
+
+---
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+> 编排侧只认「已推送远端」的提交为推进证据。本轮 YELLOW 自动核验命中以下实现提交(均 `git merge-base --is-ancestor <sha> origin/cfclub` = IN CHAIN):
+
+| 提交 sha | 关联能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|------------|------|--------------|----------|------------|------|
+| a79b26640 | admin.article.audit | YELLOW | 实现 | origin/cfclub | 是 | 自动核验 IN_CHAIN,待测试 mark-green |
+| 3b90e26d6 | health.report.create | YELLOW | 实现 | origin/cfclub | 是 | 同上 |
+| 1f31f4b094d5… | dan.assessment.recordCreate | YELLOW | 实现 | origin/cfclub | 是 | 触及 1 文件 |
+| c36d6515b | dan.assessment.resultRecord | YELLOW | 实现 | origin/cfclub | 是 | 触及 1 文件 |
+| 12317d458 | task.task.complete | YELLOW | 实现 | origin/cfclub | 是 | 触及 1 文件 |
+| f139a05a2 | task.task.review | YELLOW | 实现 | origin/cfclub | 是 | 触及 2 文件 |
+| 9afaa6e46719… | service.role.applyBindInvite | YELLOW | 实现 | origin/cfclub | 是 | 触及 8 文件 |
+| 9e1fc2f255ae… | chat.entry.memberGate | YELLOW | 实现 | origin/cfclub | 是 | 触及 10 文件 |
+| ca56f8699f75… | membership.plan.purchase | YELLOW | 实现 | origin/cfclub | 是 | 触及 4 文件 |
+| 808b603f295b… | supplier.order.fulfill | YELLOW | 实现 | origin/cfclub | 是 | 触及 1 文件 |
+| 5c3cff249 | family.familyMembers.removeMember | YELLOW | 实现 | origin/cfclub | 是 | 触及 2 文件 |
+| 1915a3272215… | shop.order.queryRefund | YELLOW | 实现 | origin/cfclub | 是 | 触及 2 文件 |
+
+> 注:S-cfc-197/198 刚派发,尚无 git 证据(实现提交待会话落盘后下轮核验)。S-cfc-156/159/172/173/175 等 O-22 豁免键 `done 无 git 证据` 属任务记录级链接缺口(yellow_sha 键级已实现定论),保持 done 不重派(不误判为缺口)。
+
+---
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | `0` | — |
+| RED | `56` | **⚠️ 56 个全部为证据缺失孤儿键(无 red_sha/yellow_sha/tid),详见 O-19;非合法 TDD RED 任务,--auto 未派发** |
+| YELLOW | `85` | 其中 66 个 yellow_sha IN CHAIN(实现真实已推远端),19 个无 yellow_sha(同属重建注入,需 O-19 一并清理) |
+| GREEN | `8` | 待部署验证 + 测试确认 |
+| DONE | `15` | 闭环归档 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | `0` 次(账本主动收口 0;多键自动派发 testing 部署验证待回执) | 部署验证由 `--auto` 自动派发,回执归测试/部署侧 |
+| 最新 `last_deployed` | `2e1e5d32`(IN CHAIN) | 沿用历史值 |
+| GREEN 部署链验证 | 待推进 | 8 GREEN 待 --deploy-done 收口(cron cfc-deploy-251.sh */10 自动部署 HEAD 兜底) |
+
+---
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | `0` | 正常(稳态一致) | 目标池全 0,与近 30 轮稳态口径一致 |
+| 契约常量(contract) | `0` | 正常 | 同上 |
+| 测试用例引用(test_case_map) | `0` | 正常 | 同上 |
+| 执行结果回填(case_results) | `0` | 正常 | 同上 |
+
+> **关键异常(O-19)**:状态机 `state.json` 被重填 56 个 RED 孤儿键 + 19 个无证据 YELLOW 键,**这些键不在 `discovery.json`(扫 src/main+src/test)/ `test-inventory.json` 中**,也无 `tid` 关联任务账本。数据源(discovery/inventory)与状态机口径严重不一致——属状态机重建/重填引入的噪声,非真实实现缺口。详见 O-19。
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | 主理人裁决 O-19:清洗 state.json 中 56 孤儿 RED + 19 无证据 YELLOW 键(重建以任务账本 + discovery/inventory 证据为准;删除 child.* 弃用域) | O-19 裁决 | 裁决后 1 轮内 | P1 | 待主理人裁决 |
+| 2 | 续派 63 个 pending 任务(S-cfc-199 起,均带 `SC-*` 用户场景 + `API_REFERENCE` 溯源),节奏 2/轮 | opencode 节点可达 | 持续 | P1 | 进行中 |
+| 3 | 协调测试团队推进 66 YELLOW→GREEN 校验 + 241 SC 三类流证据(O-7) | 测试侧 mark-green | 持续 | P2 | 协调中(研发侧无阻塞) |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| 主工作树 732 处 WIP(他人遗留未提交) | 主工作树 | 环境(非业务) | 严守纪律零接触,未强提交/stash/丢弃 | 无(按铁律④不代处理他人改动) |
+| state.json 口径异常(56 孤儿 RED) | 状态机正确性 | 需求/数据(状态机重建噪声) | 已诊断,O-19 上报待裁决 | 主理人裁决 O-19 |
+| 沙箱无法直连 opencode 节点探活 | 会话实时状态核验 | 环境 | 以账本 + `--resume --dry-run` 交叉核验替代 | 无 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 续派 pending(2/轮,S-cfc-199 起) | 持续 | opencode 可达 | lan-189/lan-204 等 | 63 待派,约 31 轮清空(或主理人提 pace) |
+| 等 O-19 裁决后重建 state.json 证据一致性 | O-19 裁决后 | 主理人 | 编排侧 | 删除孤儿键,状态机回归 discovery/inventory 真相 |
+| 核验 YELLOW IN CHAIN(持续) | 每轮 | — | 编排侧 | 66 YELLOW 已实现真实,待测试 GREEN |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+本轮未收到需研发处理的新测试问题(活阻塞台账 B-* 均为历史陈旧项,研发侧无待办)。
+
+### 附录 B:任务分派与会话恢复明细
+
+| 任务号 | 目标会话标签 | 任务类型 | 分派时间 | 当前状态 | 恢复会话标签 | 恢复原因 | 恢复时间 |
+|--------|--------------|----------|----------|----------|--------------|----------|----------|
+| S-cfc-197 | lan-189(ses_e2cc5d39effe0ovxVqkkTNByMM) | tdd/RED | 2026-11-13 12:4x | dispatched | — | — | — |
+| S-cfc-198 | lan-204(ses_e2cc5ac47ffeqE9B2VoS5u9cdX) | tdd/RED | 2026-11-13 12:4x | dispatched | — | — | — |
+
+> 恢复会话 0(--resume --dry-run 候选 0)。
+
+### 附录 C:新增入池任务明细
+
+本轮新增入池任务数:0(稳态,无新需求/设计驱动键;63 个 pending 为既往已建且溯源齐全)。
+
+### 附录 D:变更感知明细(来自 `--changes --days 7`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| docs/tdd/automation-run-2026-11-1x-R14x.md 等 200 提交/247 文件 | 设计/用例/其他 | 2026-11-11 ~ 11-12 | 0 | 是(无新键,仅既有键变化) |
+
+---
+
+## 关键结论(本轮)
+
+1. **并发互斥放行**:`state.json` 上轮 12:24:52 改动,本轮 12:34 启动 > 5 分钟;当前无 `tdd_orchestrate`/`opencode` 进程在跑 → 正常接管。
+2. **派发稳态持续推进**:`--auto --role 开发 --limit 2` 实跑派发 S-cfc-197/198(2 个合法 pending),恢复 0,部署自动验证多键;任务账本 262(done 192 / dispatched 6 / pending 63 / failed 1)。
+3. **实现真实性门禁通过**:66 个 YELLOW 键 yellow_sha 逐键 IN CHAIN,研发闭环完整;瓶颈在**验证层**(测试 GREEN + 241 SC 证据)。
+4. **⚠️ 状态机口径异常(O-19,待主理人裁决)**:state.json 被重填 56 个证据缺失孤儿 RED 键(含 14 个 child.* 弃用域——children 表已删、禁止新引入)+ 19 个无证据 YELLOW 键;这些键不在 discovery/inventory、无 tid 关联账本,**非真实实现缺口**,`--auto` 已正确忽略(不派发)。建议主理人裁决以任务账本 + discovery/inventory 证据重建 state.json。
+5. **主工作树 732 处 WIP(他人遗留)零接触**,全程临时索引 / 单文件入库绕过,未强提交、未丢弃、未 stash。
+
+---
+
+**模板版本**:v1.1(2026-11-03,含各会话状态列表 + git 证据 + 状态机分布 + 部署状态 + 数据源健康度)
+**适用对象**:研发负责人 / 开发角色会话

+ 8 - 8
docs/tdd/orchestration/sc-key-map.md

@@ -123,14 +123,14 @@ SC-G-02 / SC-G-06 / SC-P-03 / SC-P-10 / SC-P-11 共 5 个 SC 已于 2026-11-06 O
 | SC-P-09 | 家长 | 业务域 A:家庭与成员管理 | 邀请码失效/重生成 | family.invite.memberJoin | RED | 未跑通 | S-cfc-257;旧码失效/重生成,已入家庭者不受影响 |
 | SC-P-10 | 家长 | 业务域 B:任务与激励 | 创建任务 + 下发 | task.parent.createTask | DONE | 已跑通(2026-11-06 O-3 收口) | 三类流证据已推远端(f41da2779) |
 | SC-P-11 | 家长 | 业务域 B:任务与激励 | 任务审批完成(通过/驳回) | task.parent.review | DONE | 已跑通(2026-11-06 O-3 收口) | 三类流证据已推远端(291955bce) |
-| SC-P-12 | 家长 | 业务域 B:任务与激励 | 任务频率/周期配置 | — | — | 未跑通 | — |
-| SC-P-13 | 家长 | 业务域 B:任务与激励 | 心愿创建-审批-兑换闭环 | — | — | 未跑通 | — |
-| SC-P-14 | 家长 | 业务域 B:任务与激励 | 心愿驳回/失效处理 | — | — | 未跑通 | — |
-| SC-P-15 | 家长 | 业务域 B:任务与激励 | 积分明细查看 | — | — | 未跑通 | — |
-| SC-P-16 | 家长 | 业务域 B:任务与激励 | 积分流水对账 | — | — | 未跑通 | — |
-| SC-P-17 | 家长 | 业务域 C:五维能量与健康 | 查看五维能量 / 健康指数 | — | — | 未跑通 | — |
-| SC-P-18 | 家长 | 业务域 C:五维能量与健康 | 能量流水查看 | — | — | 未跑通 | — |
-| SC-P-19 | 家长 | 业务域 C:五维能量与健康 | 能量维度配置查看 | — | — | 未跑通 | — |
+| SC-P-12 | 家长 | 业务域 B:任务与激励 | 任务频率/周期配置 | task.frequency.config | RED | 未跑通 | S-cfc-262;日/周/自定义周期→按周期自动生成实例,跨月/跨年,暂停/恢复 |
+| SC-P-13 | 家长 | 业务域 B:任务与激励 | 心愿创建-审批-兑换闭环 | wish.cycle.manage | RED | 未跑通 | S-cfc-261;创建-审批-兑换闭环,扣积分兑换 |
+| SC-P-14 | 家长 | 业务域 B:任务与激励 | 心愿驳回/失效处理 | wish.cycle.manage | RED | 未跑通 | S-cfc-261;驳回→失效→可重提,已兑换不可驳回 |
+| SC-P-15 | 家长 | 业务域 B:任务与激励 | 积分明细查看 | points.ledger.view | RED | 未跑通 | S-cfc-263;积分明细(来源/变更/余额),分页+时间筛选 |
+| SC-P-16 | 家长 | 业务域 B:任务与激励 | 积分流水对账 | points.ledger.view | RED | 未跑通 | S-cfc-263;总额与流水合计一致,负流水报警 |
+| SC-P-17 | 家长 | 业务域 C:五维能量与健康 | 查看五维能量 / 健康指数 | energy.fiveDim.view | RED | 未跑通 | S-cfc-264;五维分配总和=100%,配置错误≠100% 报警 |
+| SC-P-18 | 家长 | 业务域 C:五维能量与健康 | 能量流水查看 | energy.ledger.view | RED | 未跑通 | S-cfc-265;能量流水(任务/打卡/测评),时间格式统一 |
+| SC-P-19 | 家长 | 业务域 C:五维能量与健康 | 能量维度配置查看 | energy.dimension.view | RED | 未跑通 | S-cfc-266;五维定义与权重只读,未同步展示兜底 |
 | SC-P-20 | 家长 | 业务域 C:五维能量与健康 | 健康报告 PDF 上传解析(上传报告)⭐ | — | — | 未跑通 | — |
 | SC-P-21 | 家长 | 业务域 C:五维能量与健康 | 报告类型识别 | — | — | 未跑通 | — |
 | SC-P-22 | 家长 | 业务域 C:五维能量与健康 | 报告解析预览 | — | — | 未跑通 | — |

Некоторые файлы не были показаны из-за большого количества измененных файлов