فهرست منبع

merge(cfclub): re-sync after concurrent pushes before push

Sisyphus Agent 4 روز پیش
والد
کامیت
b94414d177

+ 10 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/FoodRecommendController.java

@@ -92,7 +92,16 @@ public class FoodRecommendController {
         if (reportId != null) {
         if (reportId != null) {
             return Result.success(foodRecommendService.getCautionFoodsByReport(reportId));
             return Result.success(foodRecommendService.getCautionFoodsByReport(reportId));
         }
         }
-        List<FoodRecommendIndex> foods = foodRecommendService.getCautionFoods(memberId != null ? memberId : userId);
+        // 成员视角(memberId 语义 = family_members.id):先用 resolveMemberViewToUserId 归一为 users.id,
+        // 规避 users 与 family_members.id 数值重叠时把成员误判为无关用户(家庭72 书宁宁案例)。
+        Long queryUid;
+        if (memberId != null) {
+            Long resolved = foodRecommendService.resolveMemberViewToUserId(memberId);
+            queryUid = resolved != null ? resolved : userId;
+        } else {
+            queryUid = userId;
+        }
+        List<FoodRecommendIndex> foods = foodRecommendService.getCautionFoods(queryUid);
         return Result.success(foods);
         return Result.success(foods);
     }
     }
 
 

+ 10 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -1972,9 +1972,17 @@ public class HealthReportController {
             foods = foodService.listByReportId(reportId);
             foods = foodService.listByReportId(reportId);
         } else {
         } else {
             // 未指定报告:优先用 memberId(成员视角),其次 JWT userId(本人视角)
             // 未指定报告:优先用 memberId(成员视角),其次 JWT userId(本人视角)
-            // food_recommend_idx 以 subjectId 维度写入,家庭主账号 JWT userId 查不到家庭成员的推荐
+            // food_recommend_idx 以 subjectId 维度写入,家庭主账号 JWT userId 查不到家庭成员的推荐。
+            // 成员视角须用 resolveMemberViewToUserId 归一(family_members.id → users.id),
+            // 规避 users 与 family_members.id 数值重叠时把成员误判为无关用户(家庭72 书宁宁案例)。
             Long memberId = params != null ? ParamUtils.getLong(params.get("memberId")) : null;
             Long memberId = params != null ? ParamUtils.getLong(params.get("memberId")) : null;
-            Long uid = memberId != null ? memberId : (userId != null ? userId : -1L);
+            Long uid;
+            if (memberId != null) {
+                Long resolved = foodRecommendService.resolveMemberViewToUserId(memberId);
+                uid = resolved != null ? resolved : -1L;
+            } else {
+                uid = userId != null ? userId : -1L;
+            }
             foods = foodRecommendService.getCurrentFoodsAsFoodList(uid);
             foods = foodRecommendService.getCurrentFoodsAsFoodList(uid);
         }
         }
         return Result.success(foods);
         return Result.success(foods);

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FoodRecommendService.java

@@ -94,6 +94,34 @@ public class FoodRecommendService {
         return subjectId;
         return subjectId;
     }
     }
 
 
+    /**
+     * 成员视角解析:把 {@code family_members.id}(成员档案ID)解析为 food_recommend_idx 统一键 users.id。
+     *
+     * <p>与 {@link #resolveFoodIndexUserId(Long)} 的差异:该方法语义明确是「成员视角」,持续优先按
+     * family_members 反查(即使该数值在 users 表也有同名 id——users 与 family_members 独立自增,数值高度
+     * 重叠,成长页/食材页以 family_members.id 切换成员)。旧逻辑(resolveFoodIndexUserId)为防
+     * user→member 误归一而优先查 users,导致 memberId 与 users.id 数值重叠时把成员误判为无关用户:
+     * 如家庭72 书宁宁 family_members.id=62(user_id=34)与 users.id=62("用户2712")重叠,
+     * 成员视角查询 62 被误归一为 62 → food_recommend_idx 查 user_id=62 无数据,成长页看不到食材。</p>
+     *
+     * @param memberId family_members.id 或已是 users.id(非成员卡时原样返回,兼容前端直接传 userId)
+     * @return 该成员档案对应登录用户 users.id;memberId 为 null 返回 null;member 无登录账号返回 memberId
+     */
+    public Long resolveMemberViewToUserId(Long memberId) {
+        if (memberId == null) return null;
+        try {
+            // 成员视角:始终优先按 family_members.id 反查登录用户(与 resolveFoodIndexUserId 的 users 优先互补)
+            FamilyMember m = familyMemberMapper.selectById(memberId);
+            if (m != null) {
+                return m.getUserId() != null ? m.getUserId() : memberId;
+            }
+        } catch (Exception e) {
+            log.warn("成员视角解析 food_recommend_idx 键失败 memberId={}: {}", memberId, e.getMessage());
+        }
+        // 非成员卡(前端直接传 users.id 等)→ 原样返回
+        return memberId;
+    }
+
     @Transactional
     @Transactional
     public void recalculateForUser(Long userId) {
     public void recalculateForUser(Long userId) {
         // T28 方向1:入参 userId 可能是 family_members.id(报告确认链路 subjectId,经 resolveSubjectToMemberId 归一)
         // T28 方向1:入参 userId 可能是 family_members.id(报告确认链路 subjectId,经 resolveSubjectToMemberId 归一)
@@ -110,6 +138,17 @@ public class FoodRecommendService {
             refreshFamilyOverviews(writeKey);
             refreshFamilyOverviews(writeKey);
             return;
             return;
         }
         }
+        // 写键权威化:报告已绑定成员(subject_id 非空 = family_members.id 口径)时,以成员视角解析写入键,
+        // 规避 users 与 family_members.id 数值重叠时 resolveFoodIndexUserId 的 users 优先启发式误判。
+        // 如家庭72 报告63:subject_id=84(CF 成员卡, user_id=27),而 users.id=84 是无关用户"用户1905",
+        // resolveFoodIndexUserId(84) 会把食材错写到 user_id=84;成员视角解析得真实登录用户 27。
+        // subject_id 为空(历史数据/无绑定)时保持原 writeKey 不变,兼容既有行为与单测。
+        if (report.getSubjectId() != null) {
+            Long memberViewKey = resolveMemberViewToUserId(report.getSubjectId());
+            if (memberViewKey != null) {
+                writeKey = memberViewKey;
+            }
+        }
 
 
         // 优先从 foods 表读取(正常确认流程写入 foods 表);
         // 优先从 foods 表读取(正常确认流程写入 foods 表);
         // 若 foods 表无数据(审核路径只写 report_food_suitability),回退到 report_food_suitability
         // 若 foods 表无数据(审核路径只写 report_food_suitability),回退到 report_food_suitability

+ 29 - 9
cfc-backend/src/main/java/com/etotem/cfc/service/ReportCollectService.java

@@ -672,6 +672,12 @@ public class ReportCollectService {
             subjectId = healthReportService.matchFamilyMemberByName(
             subjectId = healthReportService.matchFamilyMemberByName(
                     payload.getSummary().getPersonName(), draft.getFamilyId());
                     payload.getSummary().getPersonName(), draft.getFamilyId());
         }
         }
+        // 兜底:PDF 未提取到姓名或姓名无法匹配到成员时,回退用户上传时选择的成员(draft.member_id)。
+        // 旧逻辑忽略 draft.memberId 导致 subject_id 落空(报告无成员名、成员视角列表不可见、
+        // 食材推荐无法重算)——家庭72 报告63 案例(PDF 无姓名,draft.member_id=27)。
+        if (subjectId == null && draft.getMemberId() != null) {
+            subjectId = draft.getMemberId();
+        }
 
 
         HealthReport report = buildReportFromPayload(draft, payload, subjectId);
         HealthReport report = buildReportFromPayload(draft, payload, subjectId);
         List<HealthIndicator> indicators = buildIndicatorsFromPayload(payload);
         List<HealthIndicator> indicators = buildIndicatorsFromPayload(payload);
@@ -696,23 +702,28 @@ public class ReportCollectService {
             }
             }
         }
         }
 
 
-        if (subjectId != null) {
-            healthReportService.bindReportToMember(created.getId(), subjectId);
+        // 下游统一使用归一化后的成员卡 id(created.subject_id 已由 resolveSubjectToMemberId 归一为
+        // family_members.id 口径),保证 bindReportToMember / 维度刷新 / 食材重算与报告归属一致。
+        // 旧逻辑传原始 subjectId(可能仍是 users.id 语义,如 draft.member_id=27),导致 getLatestReport
+        // 查不到报告(subject_id 已归一为成员卡 84)而误清空食材推荐——家庭72 报告63 案例。
+        Long memberCardId = created.getSubjectId() != null ? created.getSubjectId() : subjectId;
+        if (memberCardId != null) {
+            healthReportService.bindReportToMember(created.getId(), memberCardId);
             try {
             try {
-                dimensionScoreService.refreshFromGutReport(subjectId, created.getId());
+                dimensionScoreService.refreshFromGutReport(memberCardId, created.getId());
             } catch (Exception e) {
             } catch (Exception e) {
-                log.warn("刷新7维健康评分失败 memberId={}", subjectId, e);
+                log.warn("刷新7维健康评分失败 memberId={}", memberCardId, e);
             }
             }
             try {
             try {
-                healthReportService.refreshMemberIndicatorCurrent(subjectId, created.getId());
-                log.info("已刷新成员{}的指标当前值diff", subjectId);
+                healthReportService.refreshMemberIndicatorCurrent(memberCardId, created.getId());
+                log.info("已刷新成员{}的指标当前值diff", memberCardId);
             } catch (Exception e) {
             } catch (Exception e) {
-                log.warn("刷新指标当前值diff失败 memberId={}", subjectId, e);
+                log.warn("刷新指标当前值diff失败 memberId={}", memberCardId, e);
             }
             }
             try {
             try {
-                foodRecommendService.recalculateForUser(subjectId);
+                foodRecommendService.recalculateForUser(memberCardId);
             } catch (Exception e) {
             } catch (Exception e) {
-                log.warn("刷新食材推荐指数失败 memberId={}", subjectId, e);
+                log.warn("刷新食材推荐指数失败 memberId={}", memberCardId, e);
             }
             }
         }
         }
 
 
@@ -765,6 +776,15 @@ public class ReportCollectService {
             report.setPersonName(s.getPersonName());
             report.setPersonName(s.getPersonName());
             report.setReportDate(parseDate(s.getReportDate()));
             report.setReportDate(parseDate(s.getReportDate()));
         }
         }
+        // personName 回填:PDF 未提取到姓名(空)但已解析出 subject(draft.memberId 兜底)时,
+        // 从成员档案回填昵称,避免报告无成员名(家庭72 报告63 案例)。
+        if ((report.getPersonName() == null || report.getPersonName().trim().isEmpty())
+                && report.getSubjectId() != null) {
+            String subjectName = healthReportService.resolveSubjectName(report.getSubjectId(), draft.getFamilyId());
+            if (subjectName != null && !subjectName.isEmpty()) {
+                report.setPersonName(subjectName);
+            }
+        }
         if (report.getReportDate() == null) {
         if (report.getReportDate() == null) {
             report.setReportDate(new Date());
             report.setReportDate(new Date());
         }
         }

+ 191 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FoodRecommendMemberViewResolutionTest.java

@@ -0,0 +1,191 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * 回归测试:resolveMemberViewToUserId — 成员视角(family_members.id 语义)解析为 users.id。
+ *
+ * 背景(家庭72 报告问题3):成长页「饮食推荐」以 family_members.id=62 查食材(/api/health/foods?memberId=62)。
+ * users.id=62("用户2712")与 family_members.id=62(书宁宁, user_id=34)数值重叠。
+ * 旧逻辑(resolveFoodIndexUserId)为防 user→member 误归一,优先查 users 表 → 误判 62 为 users.id
+ * → food_recommend_idx 查 user_id=62 无数据(书宁宁食材实际挂 user_id=34)→ 成长页看不到食材。
+ *
+ * 修复:成员视角新增 resolveMemberViewToUserId(语义明确为 family_members.id),始终优先按
+ * family_members 反查其登录用户 users.id,规避 user 视角的 users 优先启发式。
+ */
+class FoodRecommendMemberViewResolutionTest {
+
+    @Mock private FoodRecommendIndexMapper foodRecommendIndexMapper;
+    @Mock private FoodRecommendIdxLogMapper foodRecommendIdxLogMapper;
+    @Mock private ReportFoodSuitabilityMapper reportFoodSuitabilityMapper;
+    @Mock private FamilyMemberMapper familyMemberMapper;
+    @Mock private UserMapper userMapper;
+    @Mock private HealthReportService healthReportService;
+    @Mock private FoodService foodService;
+    @Mock private FamilyFoodOverviewService familyFoodOverviewService;
+
+    private FoodRecommendService service;
+
+    @BeforeEach
+    void setup() throws Exception {
+        MockitoAnnotations.openMocks(this);
+        service = new FoodRecommendService();
+        setField(service, "foodRecommendIndexMapper", foodRecommendIndexMapper);
+        setField(service, "foodRecommendIdxLogMapper", foodRecommendIdxLogMapper);
+        setField(service, "reportFoodSuitabilityMapper", reportFoodSuitabilityMapper);
+        setField(service, "familyMemberMapper", familyMemberMapper);
+        setField(service, "userMapper", userMapper);
+        setField(service, "healthReportService", healthReportService);
+        setField(service, "foodService", foodService);
+        setField(service, "familyFoodOverviewService", familyFoodOverviewService);
+        installLambdaColumnCache();
+    }
+
+    private static void setField(Object target, String fieldName, Object value) throws Exception {
+        java.lang.reflect.Field f = FoodRecommendService.class.getDeclaredField(fieldName);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+
+    private FamilyMember member(Long id, Long userId, Long familyId) {
+        FamilyMember m = new FamilyMember();
+        m.setId(id);
+        m.setUserId(userId);
+        m.setFamilyId(familyId);
+        return m;
+    }
+
+    private User memberUser(Long id) {
+        User u = new User();
+        u.setId(id);
+        u.setNickname("用户" + id);
+        return u;
+    }
+
+    @Test
+    void memberView_familyMember62_resolvesToLoginUserId34() {
+        // 家庭72 书宁宁:family_members.id=62, user_id=34;users.id=62 是无关用户"用户2712"
+        when(familyMemberMapper.selectById(62L))
+                .thenReturn(member(62L, 34L, 72L));
+
+        Long resolved = service.resolveMemberViewToUserId(62L);
+
+        assertEquals(34L, resolved, "成员视角 family_members.id=62 应解析到其登录用户 users.id=34,"
+                + "而非数值重叠的无关用户 users.id=62");
+    }
+
+    @Test
+    void memberView_cfMember84_resolvesToLoginUserId27() {
+        // 家庭72 CF:family_members.id=84, user_id=27
+        when(familyMemberMapper.selectById(84L))
+                .thenReturn(member(84L, 27L, 72L));
+
+        Long resolved = service.resolveMemberViewToUserId(84L);
+
+        assertEquals(27L, resolved);
+    }
+
+    @Test
+    void memberView_notAMemberId_returnsAsIs() {
+        // 非成员卡(例如前端直接传 users.id)→ 原样返回
+        when(familyMemberMapper.selectById(999L)).thenReturn(null);
+
+        Long resolved = service.resolveMemberViewToUserId(999L);
+
+        assertEquals(999L, resolved);
+    }
+
+    @Test
+    void memberView_memberWithoutLoginUser_keepsMemberId() {
+        // 无登录账号的档案成员(user_id=null)→ 保留 member id(与 resolveFoodIndexUserId 语义一致)
+        when(familyMemberMapper.selectById(23L))
+                .thenReturn(member(23L, null, 72L));
+
+        Long resolved = service.resolveMemberViewToUserId(23L);
+
+        assertEquals(23L, resolved);
+    }
+
+    @Test
+    void memberView_null_returnsNull() {
+        assertNull(service.resolveMemberViewToUserId(null));
+    }
+
+    // === recalculateForUser 写侧 writeKey 权威化(家庭72 报告63 案例) ===
+
+    @Test
+    void recalculateForUser_reportSubjectId_authoritativeWriteKeyOverridesCollision() {
+        // 报告63:subject_id=84(CF 成员卡),user_id=27;users.id=84 是无关用户"用户1905"
+        // 旧逻辑 writeKey=resolveFoodIndexUserId(84)=84(users 优先碰撞误判);
+        // 权威化后 writeKey=resolveMemberViewToUserId(84)=27(CF 真实登录用户),食材应写入 user_id=27
+        HealthReport report = new HealthReport();
+        report.setId(63L);
+        report.setUserId(27L);
+        report.setSubjectId(84L);
+        report.setStatus("active");
+
+        when(familyMemberMapper.selectById(84L)).thenReturn(member(84L, 27L, 72L));
+        when(userMapper.selectById(84L)).thenReturn(memberUser(84L));
+        when(healthReportService.getLatestReport(84L)).thenReturn(report);
+
+        // 报告有食材:food id=10 score=5
+        Food food = new Food();
+        food.setId(10L);
+        food.setName("黄豆");
+        food.setScore(5);
+        when(foodService.listByReportId(63L)).thenReturn(java.util.Collections.singletonList(food));
+
+        // 已有索引为空 → 走 insert 分支
+        when(foodRecommendIndexMapper.selectList(any())).thenReturn(new java.util.ArrayList<>());
+        // refreshFamilyOverviews 内部反查成员家庭:返回空列表即可
+        when(familyMemberMapper.selectList(any())).thenReturn(new java.util.ArrayList<>());
+
+        service.recalculateForUser(84L);
+
+        // 验证写入的 FoodRecommendIndex.userId = 27(CF 登录用户),而非碰撞误判的 84
+        // (1个食材且无已有索引 → 恰好 1 次 index insert;另有 1 次 idx-log insert,属不同 mapper)
+        org.mockito.ArgumentCaptor<FoodRecommendIndex> insCap =
+                org.mockito.ArgumentCaptor.forClass(FoodRecommendIndex.class);
+        verify(foodRecommendIndexMapper).insert(insCap.capture());
+        assertEquals(27L, insCap.getValue().getUserId(),
+                "食材写入键应为成员84的登录用户 users.id=27,而非数值重叠的无关用户 users.id=84");
+    }
+
+    private static void installLambdaColumnCache() {
+        try {
+            java.lang.reflect.Field f = com.baomidou.mybatisplus.core.toolkit.LambdaUtils.class.getDeclaredField("COLUMN_CACHE_MAP");
+            f.setAccessible(true);
+            @SuppressWarnings("unchecked")
+            Map<String, Map<String, com.baomidou.mybatisplus.core.toolkit.support.ColumnCache>> cache =
+                    (Map<String, Map<String, com.baomidou.mybatisplus.core.toolkit.support.ColumnCache>>) f.get(null);
+            if (!cache.containsKey(FoodRecommendIndex.class.getName())) {
+                Map<String, com.baomidou.mybatisplus.core.toolkit.support.ColumnCache> cols = new HashMap<>();
+                cols.put("USERID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("user_id", "user_id", null));
+                cols.put("FOODID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("food_id", "food_id", null));
+                cols.put("FOODNAME", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("food_name", "food_name", null));
+                cols.put("INDEXSCORE", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("index_score", "index_score", null));
+                cols.put("HEALTHREPORTID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("health_report_id", "health_report_id", null));
+                cols.put("CALCULATEDAT", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("calculated_at", "calculated_at", null));
+                cols.put("CREATEDAT", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("created_at", "created_at", null));
+                cache.put(FoodRecommendIndex.class.getName(), cols);
+            }
+            if (!cache.containsKey(com.etotem.cfc.entity.FamilyMember.class.getName())) {
+                Map<String, com.baomidou.mybatisplus.core.toolkit.support.ColumnCache> cols = new HashMap<>();
+                cols.put("USERID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("user_id", "user_id", null));
+                cols.put("FAMILYID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("family_id", "family_id", null));
+                cache.put(com.etotem.cfc.entity.FamilyMember.class.getName(), cols);
+            }
+        } catch (Exception ignored) {
+        }
+    }
+}

+ 281 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheGateStrainBlockContractTest.java

@@ -0,0 +1,281 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.constant.AttributionLowSideRules;
+import com.etotem.cfc.service.attribution.AttributionComplianceGate;
+import com.etotem.cfc.service.attribution.model.IndicatorReading;
+import com.etotem.cfc.service.attribution.model.MemberAttribution;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * 契约级单测:合规门禁「16S 粒度拦截 / 菌株级推荐阻断」(能力键 {@code guhe.gate.strainBlock})。
+ *
+ * <p>契约来源(2026-11-08《浠艾福平台程序修改计划(guhe_v2 兼容 · 知识库低侧口径 · 专家同步)》§三任务分解 T6,P0,
+ * 合规门禁·横切 · G-2 菌株级拦截组):
+ * <blockquote>
+ *   T6 16S 粒度拦截(G-2):<b>{@code granularity != strain} 时禁生成含菌株号的商品推荐</b>
+ *   (叠加 §5.2 16S 粒度声明 —— 16S 只到菌属/种,不确知具体菌株号)。
+ * </blockquote></p>
+ *
+ * <p><b>契约语义</b>:本能力键是 <b>合规门禁 G-2(菌株级拦截组)</b> —— 在「非菌株级」粒度
+ * (16S/菌属层,默认粒度)下,归因/推荐产物不得出现「菌株名 + 菌株号」(如 长双歧杆菌 BB536、
+ * 乳双歧杆菌 HN019、鼠李糖乳杆菌 LGG、BB-12 等);一旦出现,门禁应识别为合规违规
+ * (G-2 菌株级拦截)并暴露,而非放行到商品推荐。菌株级粒度(granularity==strain)不受限。
+ * 与已落地的 T5 G-1 低侧红线组({@code association_only} / {@code no_causal_bridge} /
+ * {@code no_prescription})同构,但针对「16S 粒度 × 菌株号商品推荐」这一独立红线。</p>
+ *
+ * <p><b>实现现状(src/main 探测,2026-11-08)</b>:合规门禁框架与 G-1 低侧红线组已落地
+ * ({@code AttributionComplianceGate.enforceLowSide/scan/merge} + {@code AttributionLowSideRules}
+ * 三规则码 + 措辞白名单 + 禁写模式)。但 <b>T6 G-2 菌株级拦截未落地</b>:
+ * {@code AttributionLowSideRules} 无独立「菌株级拦截」规则码、{@code AttributionComplianceGate}
+ * 无按粒度拦菌株号推荐的门禁入口 {@code enforceStrainBlock}、L5 {@code scan} 只回扫
+ * 「缺X桥接 + 处方化」不识别菌株号、归因模型({@code IndicatorReading}/{@code MemberAttribution})
+ * 无粒度(granularity)维度。故本测试以「反射结构 + 纯规则行为」双通道探测实现缺失:能编译、
+ * 能运行、断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 {@code AttributionLowSideRules} 增加
+ * {@code RULE_NO_STRAIN_RECOMMENDATION="no_strain_recommendation"} 与「非菌株级粒度禁菌株号推荐」
+ * 口径;在归因模型暴露粒度维度({@code IndicatorReading.granularity} / {@code isStrainGranularity()},
+ * 默认菌属级 = 非菌株级);在 {@code AttributionComplianceGate} 增加
+ * {@code enforceStrainBlock(List<MemberAttribution>)} 门禁入口并把菌株号推荐识别并入 {@code scan};
+ * 届时本测试自动转绿。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为后端内部 L4/L5 合规门禁规则,无用户权限维度,权限流不适用,
+ * 以说明性用例标注。</p>
+ */
+class GuheGateStrainBlockContractTest {
+
+    /** T6 期望的独立规则码(区别于 T5 的 association_only / no_causal_bridge / no_prescription) */
+    private static final String RULE_NO_STRAIN_RECOMMENDATION = "no_strain_recommendation";
+
+    /** T6 期望的 G-2 门禁入口方法名(L4 出结论前按粒度拦菌株号推荐) */
+    private static final String GATE_METHOD = "enforceStrainBlock";
+
+    /** T6 期望的粒度维度:字段名 / 常量值 / 节点标记方法名 */
+    private static final String GRANULARITY_FIELD = "granularity";
+    private static final String GRANULARITY_STRAIN_VALUE = "strain";
+    private static final String GRANULARITY_FLAG_METHOD = "isStrainGranularity";
+
+    /**
+     * 菌株号探测(16S 粒度下不应出现在推荐中的「菌株名 + 菌株号」模式):
+     * 菌名(双歧杆菌/乳杆菌/LGG 等)紧随字母数字菌株号(BB536/HN019/BB-12)。
+     */
+    private static final java.util.regex.Pattern STRAIN_NUMBER_PATTERN = java.util.regex.Pattern.compile(
+            "(双歧杆菌|乳杆菌|乳酸菌|益生菌|布拉氏酵母菌|鼠李糖乳杆菌|罗伊氏乳杆菌)"
+                    + "[^。;,,\\n]{0,6}[A-Z0-9]{2,}[A-Z0-9\\-]{0,8}");
+
+    // ------------------------------------------------------------------
+    // 正常流:G-2 菌株级拦截规则码 + 粒度维度 + 门禁入口
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:门禁规则层应暴露 T6「菌株级拦截」独立规则码")
+    void strainBlockRules_exposesStrainBlockRuleCode() {
+        String value = findConstantValue(AttributionLowSideRules.class, RULE_NO_STRAIN_RECOMMENDATION);
+        if (value == null) {
+            value = findConstantValue(AttributionComplianceGate.class, RULE_NO_STRAIN_RECOMMENDATION);
+        }
+        assertNotNull(value,
+                "[guhe.gate.strainBlock] 规则层(AttributionLowSideRules / AttributionComplianceGate)缺少 \""
+                        + RULE_NO_STRAIN_RECOMMENDATION + "\" 规则码常量:T6(G-2)要求独立「16S 粒度拦截 / "
+                        + "菌株级推荐阻断」规则码,区别于 T5 的 association_only/no_causal_bridge/no_prescription。"
+                        + "当前仅落地 G-1 三规则码,未落地 T6 菌株级拦截。");
+        assertTrue(RULE_NO_STRAIN_RECOMMENDATION.equals(value),
+                "[guhe.gate.strainBlock] 菌株级拦截规则码应取值为 \"" + RULE_NO_STRAIN_RECOMMENDATION
+                        + "\",实际为 \"" + value + "\"。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:归因模型应暴露粒度(granularity)维度且默认非菌株级")
+    void attributionModel_exposesGranularityDimension() {
+        boolean hasGranularityField = hasField(IndicatorReading.class, GRANULARITY_FIELD)
+                || hasField(MemberAttribution.class, GRANULARITY_FIELD);
+        boolean hasStrainConstant =
+                !allConstantValues(IndicatorReading.class, GRANULARITY_STRAIN_VALUE).isEmpty()
+                        || !allConstantValues(MemberAttribution.class, GRANULARITY_STRAIN_VALUE).isEmpty();
+        boolean hasStrainFlagMethod = hasMethod(IndicatorReading.class, GRANULARITY_FLAG_METHOD)
+                || hasMethod(MemberAttribution.class, GRANULARITY_FLAG_METHOD);
+
+        assertTrue(hasGranularityField || hasStrainConstant || hasStrainFlagMethod,
+                "[guhe.gate.strainBlock] 归因模型(IndicatorReading/MemberAttribution)未暴露粒度(granularity)"
+                        + "维度:T6(G-2)按「granularity != strain」拦菌株号推荐,需在读数/归因结论上区分"
+                        + "「菌株级(strain)」与「菌属级/16S(非 strain,默认)」粒度。当前模型无 granularity 字段、"
+                        + "无 strain 粒度常量、无 isStrainGranularity() 标记 —— 粒度维度未落地。");
+    }
+
+    @Test
+    @DisplayName("正常流-3:合规门禁应暴露 enforceStrainBlock 入口(L4 出结论前按粒度拦菌株号推荐)")
+    void complianceGate_exposesEnforceStrainBlockEntry() {
+        assertTrue(hasMethod(AttributionComplianceGate.class, GATE_METHOD),
+                "[guhe.gate.strainBlock] AttributionComplianceGate 缺少 " + GATE_METHOD
+                        + "(...) 门禁入口:T6(G-2)要求在 L4 出结论前按粒度拦「含菌株号的商品推荐」,"
+                        + "与已落地的 enforceLowSide(G-1)同构。当前门禁仅 enforceLowSide + scan,"
+                        + "未落地 T6 菌株级拦截入口。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:16S 粒度下应拦「含菌株号的商品推荐」;scan 应回扫菌株号
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:16S 粒度下含菌株号的商品推荐应被 G-2 拦为违规")
+    void enforceStrainBlock_16SGranularityWithStrainNumberProduct_flagsViolation() {
+        // 菌属级(16S,非 strain)粒度 + 含菌株号的商品推荐 → 应产 G-2 违规
+        MemberAttribution attribution = genusLevelAttributionWithStrainProduct();
+        List<MemberAttribution> attributions = Collections.singletonList(attribution);
+
+        boolean entryAvailable = hasMethod(AttributionComplianceGate.class, GATE_METHOD);
+        if (!entryAvailable) {
+            fail("[guhe.gate.strainBlock] 16S 粒度下含菌株号的商品推荐未被拦(AttributionComplianceGate 缺少 "
+                    + GATE_METHOD + " 入口):T6(G-2)要求 granularity != strain 时禁生成含菌株号的商品推荐,"
+                    + "「推荐含长双歧杆菌 BB536 的产品」应在门禁被识别为 G-2 菌株级拦截违规。当前未落地。");
+        }
+        // 入口已落地:反射调用,取回 GateReport,断言违规清单非空且规则码为菌株级拦截
+        try {
+            AttributionComplianceGate gate = new AttributionComplianceGate();
+            Method m = AttributionComplianceGate.class.getDeclaredMethod(GATE_METHOD, List.class);
+            Object report = m.invoke(gate, attributions);
+            assertNotNull(report, "[guhe.gate.strainBlock] enforceStrainBlock 不应返回 null。");
+            Method violationsM = report.getClass().getDeclaredMethod("getViolations");
+            List<?> violations = (List<?>) violationsM.invoke(report);
+            assertNotNull(violations, "[guhe.gate.strainBlock] 违规清单不应为 null。");
+            boolean hit = false;
+            for (Object v : violations) {
+                Method ruleM = v.getClass().getDeclaredMethod("getRule");
+                if (RULE_NO_STRAIN_RECOMMENDATION.equals(String.valueOf(ruleM.invoke(v)))) {
+                    hit = true;
+                    break;
+                }
+            }
+            assertTrue(hit,
+                    "[guhe.gate.strainBlock] 16S 粒度下「含菌株号的商品推荐」未被标记为 " + RULE_NO_STRAIN_RECOMMENDATION
+                            + " 违规:T6(G-2)粒度拦截未对菌株号推荐生效。");
+        } catch (ReflectiveOperationException e) {
+            fail("[guhe.gate.strainBlock] 调用 enforceStrainBlock 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("边界流-2:L5 scan 应回扫「含菌株号的商品推荐」为 G-2 违规(当前仅 G-1 两类)")
+    void scan_strainNumberProductRecommendation_flagsStrainBlockViolation() {
+        AttributionComplianceGate gate = new AttributionComplianceGate();
+        // 16S 粒度下含具体菌株号的商品推荐文案(不应出现 BB536 等菌株号)
+        String recommendation =
+                "应对方向:推荐含长双歧杆菌 BB536 的益生元产品,帮助改善肠道环境。";
+        assertTrue(STRAIN_NUMBER_PATTERN.matcher(recommendation).find(),
+                "[guhe.gate.strainBlock] 测试夹具自检:推荐文案应含菌株号(长双歧杆菌 BB536),否则夹具失效。");
+
+        List<AttributionComplianceGate.Violation> violations = gate.scan(recommendation);
+        boolean hit = false;
+        for (AttributionComplianceGate.Violation v : violations) {
+            if (RULE_NO_STRAIN_RECOMMENDATION.equals(v.getRule())) {
+                hit = true;
+                break;
+            }
+        }
+        assertTrue(hit,
+                "[guhe.gate.strainBlock] L5 scan 未将「含菌株号的商品推荐」回扫为 " + RULE_NO_STRAIN_RECOMMENDATION
+                        + " 违规:T6(G-2)要求 granularity != strain 时禁菌株号推荐,scan 应识别「长双歧杆菌 BB536」"
+                        + "等菌株号。当前 scan 仅回扫 G-1 的「缺X桥接 + 处方化」,未落地菌株号识别(实际命中="
+                        + violations.size() + " 条)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):后端内部 L4/L5 合规门禁规则,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为后端内部 L4/L5 合规门禁规则,无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「L4/L5 门禁 → 菌株号商品推荐拦截」内部规则,无权限校验维度。
+        assertTrue(true, "权限流不适用:guhe.gate.strainBlock 为后端内部合规门禁规则,无用户权限维度。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射与数据构造辅助
+    // ------------------------------------------------------------------
+
+    /** 在类(含指定候选类)静态字段中查找名称==name 的 String 常量值,找不到返回 null */
+    private static String findConstantValue(Class<?> clazz, String name) {
+        for (Field f : safeDeclaredFields(clazz)) {
+            if (f.getName().equals(name)) {
+                try {
+                    Object v = f.get(null);
+                    return v == null ? null : String.valueOf(v);
+                } catch (IllegalAccessException ignored) {
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** 取类中所有值==value 的静态 String 常量名 */
+    private static List<String> allConstantValues(Class<?> clazz, String value) {
+        List<String> out = new ArrayList<>();
+        for (Field f : safeDeclaredFields(clazz)) {
+            if (f.getType() == String.class && java.lang.reflect.Modifier.isStatic(f.getModifiers())) {
+                try {
+                    if (value.equals(String.valueOf(f.get(null)))) {
+                        out.add(f.getName());
+                    }
+                } catch (IllegalAccessException ignored) {
+                    // 忽略
+                }
+            }
+        }
+        return out;
+    }
+
+    private static boolean hasField(Class<?> clazz, String name) {
+        for (Field f : safeDeclaredFields(clazz)) {
+            if (f.getName().equals(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasMethod(Class<?> clazz, String name) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static Field[] safeDeclaredFields(Class<?> clazz) {
+        try {
+            return clazz.getDeclaredFields();
+        } catch (Throwable t) {
+            return new Field[0];
+        }
+    }
+
+    /**
+     * 菌属级(16S,非 strain)粒度的归因结论,携带一条「含菌株号的商品推荐」应对方向。
+     * 16S 只到菌属/种,不应出现 BB536 等具体菌株号(§5.2 16S 粒度声明)。
+     */
+    private static MemberAttribution genusLevelAttributionWithStrainProduct() {
+        MemberAttribution a = new MemberAttribution();
+        a.setMemberId(1L);
+        a.setName("妈妈");
+        a.setRole("mom");
+        a.setRoleLabel("妈妈");
+        a.getActions().add("推荐含长双歧杆菌 BB536 的益生元产品");
+        a.getActions().add("建议补充膳食纤维,调整饮食结构");
+        return a;
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-b6f95c1f5926e0786318c85f4f52cff33e2abd51
+a80a6115a99b308970adb49624ee4c8f80878ca5

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

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

+ 1 - 1
cfc-web/package.json

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

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

@@ -4,6 +4,69 @@
 
 
 ---
 ---
 
 
+## v1.0.2225 (2026-11-08)
+
+### 文档
+- 研发负责人工作汇报 2026-11-09 0130 — 68键状态机(RED19/YELLOW27/GREEN8/DONE14) + 27 YELLOW实现全在链复核 + 19 RED测试侧契约批次在途 + 新缺陷修复9e6825ef8跟踪
+- 食材推荐按健康问卷个性化调整 需求变更+设计
+- 研发负责人工作汇报 2026-11-09 0054 — 68键状态机(RED19/YELLOW27/GREEN8/DONE14) + SC 15/241 + 11 dispatched RED 任务 git 实证完成 + 会话全 idle 待 hub 拾取 pending
+- 错峰03第70轮 - O-18 裁决(自动巡检链注册/diet走hub入池/8僵尸重派)+ 研发汇报 0015
+- 迁移方案追加 T8 巡检并入 WorkBuddy automation 落地状态
+- v2 实现方式重写任务书+需求变更(竖屏直签+canvas type=2d,取代 11-08 横屏适配 a7edf5698)
+
+### Bug 修复
+- 报告采集兜底成员绑定与食材成员视角归一
+
+### 其他
+- 1. 报告无成员名:PDF 未提取到姓名 → person_name 空
+- 2. 报告列表选成员看不到:subject_id 落空(NULL),getMemberReports 按 subject_id 过滤查不到
+- 3. 成长页饮食推荐选成员看不到食材:users.id 与 family_members.id 数值重叠时
+-    resolveFoodIndexUserId 的 users 优先启发式把成员卡误判为无关用户
+- 修复:
+- - ReportCollectService.insertReport:姓名匹配失败回退 draft.member_id;下游统一使用归一化
+-   成员卡 id(created.subject_id);buildReportFromPayload 对空 person_name 回填成员昵称
+- - FoodRecommendService:新增 resolveMemberViewToUserId(成员视角 family_members.id → users.id);
+-   recalculateForUser 写键基于报告 subject_id 权威化,规避数值碰撞
+- - HealthReportController /api/health/foods、FoodRecommendController /api/food/recommendation/caution:
+-   成员视角入参先经 resolveMemberViewToUserId 归一
+- 测试:FoodRecommendMemberViewResolutionTest 6 例全绿(成员视角解析 + 写键权威化)
+- 
+- granularity != strain 时禁生成含菌株号的商品推荐 (叠加 §5.2 16S 粒度声明)。
+- RED 探测: AttributionLowSideRules/AttributionComplianceGate 无 no_strain_recommendation
+- 规则码; 归因模型无 granularity 维度; 门禁无 enforceStrainBlock 入口; L5 scan 不回扫菌株号。
+- 预期红: 6 run / 5 fail (结构+行为双通道), 权限流说明性用例通过。
+- 
+- TDD 入池 diet.food.healthAdjust(UI) 能力键 RED 登记(S-cfc-96/97)。
+- 溯源:用户 2026-11-09 拍板。
+- 
+- - guhe_v2 解析后校验增量 11 键齐全(21 旧键 + 11 增量键 = 32 键)
+- - 空壳(曾误用艾德范思提取器产 26 键)→ 换正确提取器 extract_guhe_v2
+- RED 预期:实现未落地,5 用例红(守卫入口/11 键基准/32 键边界/26 键空壳签名/提取器寻址),
+- 2 用例说明性绿(parseReport 挂载点 + 权限流不适用)。纯反射结构契约,无 Spring/DB/外部服务。
+- GREEN 指引:ReportParserDispatchService 提供空壳守卫入口(方法名含 Shell/KeyComplete/ValidateKeys),
+- L1 层声明 11/32 键边界与 26 键空壳签名、增量 11 键集合,并寻址 extract_guhe_v2。
+- 
+- 
+- guhe2026-new-extension module (11 subkeys incl low-side patch subkey),
+- with idempotent upgrade script (importFromClasspath: overwrite, no accumulation).
+- RED: 9/11 fail - seed version is 2.0, extension module not registered/loaded.
+- 
+
+### 测试
+- S-cfc-81 add RED contract tests for 16S granularity strain-block gate (T6/G-2)
+- S-cfc-91 guhe.gate.evidenceIsolate 证据码隔离契约测试(TDD-RED,11 红 1 绿权限流)
+- guhe.l1.pageTailClean 分页残段清洗下沉 RED 契约单测 (T12/L1-2)
+- S-cfc-89 add RED contract tests for empty-shell rate guard (T13/L1-4)
+- S-cfc-90 add RED contract tests for guhe_v2 fingerprint route (T7 chain aidefansi->beijing->guhe_v2->standard + trigger words + 32-key self-check)
+- S-cfc-88 add RED contract tests for P3 cross-case aggregate (T15 low-side reading aggregate & same-strategy retrieval)
+- S-cfc-87 add RED contract tests for existing-key new field exposure (T10 L2-3: genus-function-library [40]/[72]/[73] high/low impact, species-info Escherichia low-side impact, aidefansi E.coli colonization note)
+- S-cfc-86 add RED contract tests for low-side no-supplement redline (T4/L4-3, contract not landed)
+- S-cfc-85 add RED contract tests for five-dim no_path mapping (gut_flora no_path to wealth/action, mind/wisdom mediation chain locked wording, low-side body battlefield)
+- S-cfc-84 add RED contract tests for report format version split (T8/L1-3 old 21-key vs guhe_v2 32-key)
+- S-cfc-83 add RED contract tests for KB version sensing (T9 L2-1)
+- S-cfc-80 add RED contract tests for co-diner participant select (family opens multi-select with default all, personal passes currentMemberId)
+
+
 ## v1.0.2224 (2026-11-08)
 ## v1.0.2224 (2026-11-08)
 
 
 ### 测试
 ### 测试

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

@@ -1,6 +1,6 @@
 # 更新日志
 # 更新日志
 
 
-> 当前版本: v1.0.2224
+> 当前版本: v1.0.2225
 
 
 ## 历史版本
 ## 历史版本
 
 
@@ -8,6 +8,69 @@
 
 
 ---
 ---
 
 
+## v1.0.2225 (2026-11-08)
+
+### 文档
+- 研发负责人工作汇报 2026-11-09 0130 — 68键状态机(RED19/YELLOW27/GREEN8/DONE14) + 27 YELLOW实现全在链复核 + 19 RED测试侧契约批次在途 + 新缺陷修复9e6825ef8跟踪
+- 食材推荐按健康问卷个性化调整 需求变更+设计
+- 研发负责人工作汇报 2026-11-09 0054 — 68键状态机(RED19/YELLOW27/GREEN8/DONE14) + SC 15/241 + 11 dispatched RED 任务 git 实证完成 + 会话全 idle 待 hub 拾取 pending
+- 错峰03第70轮 - O-18 裁决(自动巡检链注册/diet走hub入池/8僵尸重派)+ 研发汇报 0015
+- 迁移方案追加 T8 巡检并入 WorkBuddy automation 落地状态
+- v2 实现方式重写任务书+需求变更(竖屏直签+canvas type=2d,取代 11-08 横屏适配 a7edf5698)
+
+### Bug 修复
+- 报告采集兜底成员绑定与食材成员视角归一
+
+### 其他
+- 1. 报告无成员名:PDF 未提取到姓名 → person_name 空
+- 2. 报告列表选成员看不到:subject_id 落空(NULL),getMemberReports 按 subject_id 过滤查不到
+- 3. 成长页饮食推荐选成员看不到食材:users.id 与 family_members.id 数值重叠时
+-    resolveFoodIndexUserId 的 users 优先启发式把成员卡误判为无关用户
+- 修复:
+- - ReportCollectService.insertReport:姓名匹配失败回退 draft.member_id;下游统一使用归一化
+-   成员卡 id(created.subject_id);buildReportFromPayload 对空 person_name 回填成员昵称
+- - FoodRecommendService:新增 resolveMemberViewToUserId(成员视角 family_members.id → users.id);
+-   recalculateForUser 写键基于报告 subject_id 权威化,规避数值碰撞
+- - HealthReportController /api/health/foods、FoodRecommendController /api/food/recommendation/caution:
+-   成员视角入参先经 resolveMemberViewToUserId 归一
+- 测试:FoodRecommendMemberViewResolutionTest 6 例全绿(成员视角解析 + 写键权威化)
+- 
+- granularity != strain 时禁生成含菌株号的商品推荐 (叠加 §5.2 16S 粒度声明)。
+- RED 探测: AttributionLowSideRules/AttributionComplianceGate 无 no_strain_recommendation
+- 规则码; 归因模型无 granularity 维度; 门禁无 enforceStrainBlock 入口; L5 scan 不回扫菌株号。
+- 预期红: 6 run / 5 fail (结构+行为双通道), 权限流说明性用例通过。
+- 
+- TDD 入池 diet.food.healthAdjust(UI) 能力键 RED 登记(S-cfc-96/97)。
+- 溯源:用户 2026-11-09 拍板。
+- 
+- - guhe_v2 解析后校验增量 11 键齐全(21 旧键 + 11 增量键 = 32 键)
+- - 空壳(曾误用艾德范思提取器产 26 键)→ 换正确提取器 extract_guhe_v2
+- RED 预期:实现未落地,5 用例红(守卫入口/11 键基准/32 键边界/26 键空壳签名/提取器寻址),
+- 2 用例说明性绿(parseReport 挂载点 + 权限流不适用)。纯反射结构契约,无 Spring/DB/外部服务。
+- GREEN 指引:ReportParserDispatchService 提供空壳守卫入口(方法名含 Shell/KeyComplete/ValidateKeys),
+- L1 层声明 11/32 键边界与 26 键空壳签名、增量 11 键集合,并寻址 extract_guhe_v2。
+- 
+- 
+- guhe2026-new-extension module (11 subkeys incl low-side patch subkey),
+- with idempotent upgrade script (importFromClasspath: overwrite, no accumulation).
+- RED: 9/11 fail - seed version is 2.0, extension module not registered/loaded.
+- 
+
+### 测试
+- S-cfc-81 add RED contract tests for 16S granularity strain-block gate (T6/G-2)
+- S-cfc-91 guhe.gate.evidenceIsolate 证据码隔离契约测试(TDD-RED,11 红 1 绿权限流)
+- guhe.l1.pageTailClean 分页残段清洗下沉 RED 契约单测 (T12/L1-2)
+- S-cfc-89 add RED contract tests for empty-shell rate guard (T13/L1-4)
+- S-cfc-90 add RED contract tests for guhe_v2 fingerprint route (T7 chain aidefansi->beijing->guhe_v2->standard + trigger words + 32-key self-check)
+- S-cfc-88 add RED contract tests for P3 cross-case aggregate (T15 low-side reading aggregate & same-strategy retrieval)
+- S-cfc-87 add RED contract tests for existing-key new field exposure (T10 L2-3: genus-function-library [40]/[72]/[73] high/low impact, species-info Escherichia low-side impact, aidefansi E.coli colonization note)
+- S-cfc-86 add RED contract tests for low-side no-supplement redline (T4/L4-3, contract not landed)
+- S-cfc-85 add RED contract tests for five-dim no_path mapping (gut_flora no_path to wealth/action, mind/wisdom mediation chain locked wording, low-side body battlefield)
+- S-cfc-84 add RED contract tests for report format version split (T8/L1-3 old 21-key vs guhe_v2 32-key)
+- S-cfc-83 add RED contract tests for KB version sensing (T9 L2-1)
+- S-cfc-80 add RED contract tests for co-diner participant select (family opens multi-select with default all, personal passes currentMemberId)
+
+
 ## v1.0.2224 (2026-11-08)
 ## v1.0.2224 (2026-11-08)
 
 
 ### 测试
 ### 测试

+ 6 - 0
docs/tdd/dev/test-requests.md

@@ -194,3 +194,9 @@
 | TR-17 | D/X 域运动健康接入(华为 D1-D5 + 小米 X1-X5,O-14 主理人拍板) | 派发 | **O-14 主理人拍板(17:5x,commit 78277897e)**:D/X 域作 O-7 独立契约核对批次,**不纳入 241 基线**;实现已合入(`4344688d2`/`978d1bc95`/`4815aae95` + B-50 鉴权 `cc7b3e00b`)无需研发重做,仅需设计契约登记 + 测试证据补建。开工包 `docs/tdd/orchestration/dx-health-kickoff-20261107.md` 已推 e1fe1f7bc(20:2x);契约核对已派 lan-204 `ses_e4a07f4b6ffeKN3Z2d7spU4CSk`(axonhub/deepseek,--confirm 确认会话真实执行 ✓:正文 183 字 + 工具调用 1 次)。**10 端点初核全部 ✅**(华为 /api/huawei-health/{auth-url,callback,sync,revoke,status} + 小米 /api/xiaomi-health/ 同名 5 端点,JwtInterceptor L115-116 均入 LOGIN_REQUIRED_PATHS)。产出契约文档后转测试侧补契约 JUnit + 三类流证据(重点:D-4/X-4 撤销授权合规流、D-3/X-3 同步幂等与去重);⚠ 三项待会话实测回填(前端消费链路 / 儿童手动导入双轨 / health_data_sources 去重覆盖语义)。注意 pom.xml:28 `<skipTests>true</skipTests>`,`mvn test` 须显式 `-DskipTests=false` | 研发负责人(闭环巡检轮) | 2026-11-07 20:26 | 待测试处理 |
 | TR-17 | D/X 域运动健康接入(华为 D1-D5 + 小米 X1-X5,O-14 主理人拍板) | 派发 | **O-14 主理人拍板(17:5x,commit 78277897e)**:D/X 域作 O-7 独立契约核对批次,**不纳入 241 基线**;实现已合入(`4344688d2`/`978d1bc95`/`4815aae95` + B-50 鉴权 `cc7b3e00b`)无需研发重做,仅需设计契约登记 + 测试证据补建。开工包 `docs/tdd/orchestration/dx-health-kickoff-20261107.md` 已推 e1fe1f7bc(20:2x);契约核对已派 lan-204 `ses_e4a07f4b6ffeKN3Z2d7spU4CSk`(axonhub/deepseek,--confirm 确认会话真实执行 ✓:正文 183 字 + 工具调用 1 次)。**10 端点初核全部 ✅**(华为 /api/huawei-health/{auth-url,callback,sync,revoke,status} + 小米 /api/xiaomi-health/ 同名 5 端点,JwtInterceptor L115-116 均入 LOGIN_REQUIRED_PATHS)。产出契约文档后转测试侧补契约 JUnit + 三类流证据(重点:D-4/X-4 撤销授权合规流、D-3/X-3 同步幂等与去重);⚠ 三项待会话实测回填(前端消费链路 / 儿童手动导入双轨 / health_data_sources 去重覆盖语义)。注意 pom.xml:28 `<skipTests>true</skipTests>`,`mvn test` 须显式 `-DskipTests=false` | 研发负责人(闭环巡检轮) | 2026-11-07 20:26 | 待测试处理 |
 | TR-18 | family.signature.* 5 键(openPage / landscape / healthPlan / consignee / routeIntegrate,S-cfc-50~54) | 假 RED 纠偏 + 证据补建 | **发现(2026-11-08 00:5x 复核)**:签名横屏契约 `docs/superpowers/specs/2026-11-07-signature-page-landscape.md` 的 **WI-1~WI-5 实现已合入 origin/cfclub**——`e35ca7186`(2026-11-08 00:30,`feat(signature)`,6 文件 +495:新增 `pages/signature/signature.vue` 312 行 + `utils/signature-bus.js` + `pages.json` 路由 + 改造 `health-plan-summary.vue`/`consignee-edit.vue`/`checkout.vue`),`git merge-base --is-ancestor` ✓。但 S-cfc-50~54(00:40 建)仍标 RED、red_sha 空 → **账本滞后于实现(假 RED)**,按 O-2 方案 C 不派研发实现型任务,转测试侧补三类流证据。请测试侧:① 对 5 键补正常/异常/权限三类流执行证据(正常:问卷+水机走「去签名→横屏签名→确认→回传→预览」;异常:空白签名拦截、返回不回传;权限:未登录/非成员访问签名页);② 证据 + 契约/用例推 origin/cfclub + 提交干净;③ 完成后按 sc-key-map.md 对应 SC(健康方案/水机购买相关)登记跑通。注意 pom.xml:28 `<skipTests>true</skipTests>`,`mvn test` 须显式 `-DskipTests=false` | 研发负责人(本自动化第58轮) | 2026-11-08 01:1x | 待测试处理 |
 | TR-18 | family.signature.* 5 键(openPage / landscape / healthPlan / consignee / routeIntegrate,S-cfc-50~54) | 假 RED 纠偏 + 证据补建 | **发现(2026-11-08 00:5x 复核)**:签名横屏契约 `docs/superpowers/specs/2026-11-07-signature-page-landscape.md` 的 **WI-1~WI-5 实现已合入 origin/cfclub**——`e35ca7186`(2026-11-08 00:30,`feat(signature)`,6 文件 +495:新增 `pages/signature/signature.vue` 312 行 + `utils/signature-bus.js` + `pages.json` 路由 + 改造 `health-plan-summary.vue`/`consignee-edit.vue`/`checkout.vue`),`git merge-base --is-ancestor` ✓。但 S-cfc-50~54(00:40 建)仍标 RED、red_sha 空 → **账本滞后于实现(假 RED)**,按 O-2 方案 C 不派研发实现型任务,转测试侧补三类流证据。请测试侧:① 对 5 键补正常/异常/权限三类流执行证据(正常:问卷+水机走「去签名→横屏签名→确认→回传→预览」;异常:空白签名拦截、返回不回传;权限:未登录/非成员访问签名页);② 证据 + 契约/用例推 origin/cfclub + 提交干净;③ 完成后按 sc-key-map.md 对应 SC(健康方案/水机购买相关)登记跑通。注意 pom.xml:28 `<skipTests>true</skipTests>`,`mvn test` 须显式 `-DskipTests=false` | 研发负责人(本自动化第58轮) | 2026-11-08 01:1x | 待测试处理 |
 | TR-19 | team.profit.settle(S-cfc-41/61)+ team.profit.hook(S-cfc-42/62)@ 分润体系 | 实现落地送测 | **第 63 轮回收(2026-11-08 07:0x~07:15)**:两键实现已落地并推 origin/cfclub,均 IN CHAIN + kind=main 复核 ✓——① `team.profit.settle` → **`649dd2be7`**(07:03 推,Agnes Agent:ServiceCostConfig 实体/Mapper + TeamProfitService 8 份分佣/团队奖方法并入,3 src/main 文件 +157,5 契约类 32 例全绿);② `team.profit.hook` → **`b037fe71b`**(07:12 推,rebase 至 649dd2be7 之上:支付成功钩子接入 5 订单服务 6 调用点 + service_cost_config 表 + DatabaseInitializer 迁移,7 文件 +108,67/67 GREEN)。两键 yellow_sha 已按「实现真相优先」人工回填(O-16 脚本缺陷下)。请测试侧:① 复跑 TeamProfitSettle/Hook 契约测试核验 GREEN 后 `--mark-green` 两键;② 分润闭环 SC(SC-*E7/T9/T10 等)按 sc-key-map.md 补三类流证据;③ 部署链验证(testing 已部署 S-cfc-64 b6d5b0391/65 20309e50e/66 2c65d98f7/67 4f40a8973 + 61/62 待部署)。注意 pom.xml:28 `<skipTests>true</skipTests>`,`mvn test` 须显式 `-DskipTests=false` | 研发负责人(第63轮) | 2026-11-08 07:15 | 待测试处理 |
 | TR-19 | team.profit.settle(S-cfc-41/61)+ team.profit.hook(S-cfc-42/62)@ 分润体系 | 实现落地送测 | **第 63 轮回收(2026-11-08 07:0x~07:15)**:两键实现已落地并推 origin/cfclub,均 IN CHAIN + kind=main 复核 ✓——① `team.profit.settle` → **`649dd2be7`**(07:03 推,Agnes Agent:ServiceCostConfig 实体/Mapper + TeamProfitService 8 份分佣/团队奖方法并入,3 src/main 文件 +157,5 契约类 32 例全绿);② `team.profit.hook` → **`b037fe71b`**(07:12 推,rebase 至 649dd2be7 之上:支付成功钩子接入 5 订单服务 6 调用点 + service_cost_config 表 + DatabaseInitializer 迁移,7 文件 +108,67/67 GREEN)。两键 yellow_sha 已按「实现真相优先」人工回填(O-16 脚本缺陷下)。请测试侧:① 复跑 TeamProfitSettle/Hook 契约测试核验 GREEN 后 `--mark-green` 两键;② 分润闭环 SC(SC-*E7/T9/T10 等)按 sc-key-map.md 补三类流证据;③ 部署链验证(testing 已部署 S-cfc-64 b6d5b0391/65 20309e50e/66 2c65d98f7/67 4f40a8973 + 61/62 待部署)。注意 pom.xml:28 `<skipTests>true</skipTests>`,`mvn test` 须显式 `-DskipTests=false` | 研发负责人(第63轮) | 2026-11-08 07:15 | 待测试处理 |
+
+## TR-20:27 个 YELLOW 键实现全部在链复核,送测试侧复跑契约测试 → --mark-green(研发 → 测试,2026-11-09 01:40)
+
+| 编号 | 关联键/对象 | 类型 | 描述 | 经办 | 登记时间 | 状态 |
+|---|---|---|---|---|---|---|
+| TR-20 | 27 个 YELLOW 键(见描述分组) | 复验 + mark-green | **第 0130 轮复核**:27/27 YELLOW 键实现 sha 全部在 origin/cfclub 链上(`git merge-base --is-ancestor` 逐键复核通过,无实现缺失、无账本滞后)。请测试侧复跑契约测试/前端测试后 `--mark-green` 收口。分组明细:**① admin.family.* 5 键**(actionMenu/createdAtDisplay/planEntry/reportEntry→`ef3e81fec`;familyCoupon.backToList→`e777224fc`,对应 G1 前端 spec 已合入:Families.actionMenu/createdAtDisplay/planEntry/reportEntry + FamilyCouponManage.backToList);**② family.invite.* 5 键**(joinRequestCardInfo→`4f40a8973` / joinRequiredInfoFrontend→`2c65d98f7` / joinRequiredInfoTest→`f4081f011` / joinValidationGate→`f1d3e663e` / joinValidationRules→`dc1b83a65`,S-cfc-63~67 批次);**③ team.* 9 键**(admin.manage→`17b6d6bfb` / invitation.create→`19c7439b2` / user.portal→`b6d5b0391` / star.myView→`2b73e20f6` / star.digest→`786c88e02` / star.multiplier→`20309e50e` / profit.settle→`649dd2be7` / profit.hook→`b037fe71b`(TR-19 已单独送测,本条目并轨)/ profit.cron→`0bf1815cf` / profit.pipeline→`3d28ba94d`);**④ family.signature.* 5 键**(openPage/landscape/healthPlan/consignee/routeIntegrate→`e35ca7186`,TR-18 假 RED 纠偏项,实现已在链,请补三类流证据后 mark-green);**⑤ family.earnings.* 2 键**(adminRole/poolToggle→`239710c47`)。注意 pom.xml:28 `<skipTests>true</skipTests>`,`mvn test` 须显式 `-DskipTests=false` | 研发负责人(本自动化第 R-0130 轮) | 2026-11-09 01:40 | 待测试处理 |

+ 180 - 0
docs/tdd/orchestration/report-dev-2026-11-09-0130.md

@@ -0,0 +1,180 @@
+# 研发负责人工作汇报(2026-11-09 0130 轮 · TDD 闭环巡检)
+
+> 汇报人:研发负责人(本自动化 05193ba0,经 tdd-orchestrator + git-workflow 技能)|汇报周期:2026-11-09 0130 轮(时间戳唯一标识)|负责模块:TDD 编排全域(研发负责人职责:检查 / 调度 / 汇总,不代写实现)
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人(自动化 05193ba0) |
+| 汇报周期 | 2026-11-09 0130 轮(时间戳唯一,非全局第 N 轮) |
+| 负责模块/单元范围 | TDD 编排全域(状态机 / 任务账本 / 会话调度 / SC 覆盖矩阵 / 主理人协调清单) |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **收到的测试反馈问题数*** | 0 个(本实例) | 本环境只读核查,未直连测试台账逐条核验;远端测试团队 RED 契约测试持续推进(guhe/diet 域 S-cfc-80~97),详见 §1.2b |
+| **已解决问题数*** | 0 个(本实例) | 本实例不代写实现;RED→YELLOW→GREEN 收口由 hub 派发会话 + 测试团队执行,证据以 git 真理为准 |
+| **待解决研发问题数** | 2 项(均非本实例可独立闭环) | ① 27 个 YELLOW 键实现已在链、待测试侧复跑契约测试后 `--mark-green`(P0 流程衔接);② 受 9e6825ef8 修复触达的报告-成员绑定/食材成员视角归一 SC(家庭72 报告63),待测试侧补三类流执行证据转正(P1) |
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **分派任务数*** | 0 个(本实例) | 本环境派发通道由独立 hub 自动化(xaf_agent_hub)执行,本实例仅检查 / 调度 / 汇报,不直接派发(避免触动主工作树 314 处他人 WIP) |
+| **恢复智能体会话数*** | 0 个(本实例) | 会话恢复属 hub 职责;本轮 hub 快照 6 实例全部 running/idle 正常,无 wedged/swallowed/upstream_err/stalled |
+| **当前在用会话数** | 13 个 running(hub 管理) | lan-189×8 running(RED 契约测试批次)+ lan-204×1 running(手签名实现方式重写)+ prod-etotem×1 running(生产库 connection_type 补字段)+ 其余 idle |
+| **会话平均利用率** | 高(满载运行) | 上一轮(0054)「11 RED 任务会话已回落空闲」后,hub 已拾取 7 个 pending 并续派 S-cfc-91~97 批次,当前 lan-189 达 8 running(超节点 4 并发上限,详见 §1.2b 备注) |
+
+### 1.2b 各会话当前工作状态列表
+
+> 数据来源:`hub.py sessions`(只读,2026-11-09 01:23 快照)。状态取值见模板 §1.2b 速查表。
+
+| 会话标签 | 会话 ID | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|---------|----------|----------|---------------|----------|------|
+| lan-189 | ses_e43d6adf… | `running` | TDD RED S-cfc-97 diet.food.healthAdjustUi | 1min | 不干净(远端 WIP) | hub 调度中 |
+| lan-189 | ses_e43d6ecb… | `running` | TDD RED S-cfc-96 diet.food.healthAdjust | 1min | 不干净 | hub 调度中 |
+| lan-189 | ses_e43d792b… | `running` | TDD RED S-cfc-93 guhe.report.personalAssert | 1min | 不干净 | hub 调度中 |
+| lan-189 | ses_e43d75ab… | `running` | TDD RED S-cfc-94 guhe.kb.queryLowSidePatch | 2min | 不干净 | hub 调度中 |
+| lan-189 | ses_e43d721f… | `running` | TDD RED S-cfc-95 guhe.lowSide.ruleLoad | 1min | 不干净 | hub 调度中 |
+| lan-189 | ses_e43e4046… | `running` | TDD RED S-cfc-92 guhe.gate.lowSideRedline | 12min | 不干净 | hub 调度中 |
+| lan-189 | ses_e43e970b… | `running` | TDD RED S-cfc-81 guhe.gate.strainBlock | 12min | 不干净 | hub 调度中 |
+| lan-189 | ses_e43df669… | `running` | TDD RED S-cfc-81 guhe.gate.strainBlock(同任务双会话,hub 冗余防护) | 12min | 不干净 | 与上行为同一任务,需观察冗余是否合并 |
+| lan-204 | ses_e440a19a… | `running` | hub: 【CFC 开发任务派发:手签名实现方式重写】 | 61min | 不干净(D:/cfc) | 运行 61min,接近 wedged 阈值(45min)需下轮确认产出 |
+| prod-etotem | ses_f70aa87c… | `running` | 补生产库 connection_type 字段并同步初始化代码 | 11min | 不干净(/root/cfc) | 生产库迁移,属部署/运维链 |
+| lain-189 其余 / lan-204 其余 | 多会话 | `idle` | 已完成 RED 契约测试待回收 | — | 不干净 | 属测试侧登记职责 |
+| pub-iwintrue | — | `idle` | testing 部署链(pending 9 在途) | — | 不干净 | deploy_sessions.testing |
+| local-45278 / lan-248 | — | `idle` | 待 hub 拾取 | — | — | 可调度实例,配额富余 |
+
+> **R2「不留空闲」核验结论**:13 个会话 running 满载,无「空闲且无任务」浪费;lan-189 已 8 running 超并发上限(配置 4),属测试侧 RED 批次加急派发,由 hub 管控。无 wedged(lan-204 手签名会话 61min 需下轮确认产出)/swallowed/stalled。
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **感知需求/设计变更数*** | 1 个新需求变更 | `404f9f74d`(01:07):食材推荐按健康问卷(饮食习惯/过敏/用药/主要健康体症)个性化调整,新增 specs 文档 + diet.food.healthAdjust / diet.food.healthAdjustUi 两键 RED 登记(S-cfc-96/97)——已入状态机(RED 19 内)且已派发会话 running |
+| **新增能力键数** | 0 个(本实例新增) | 需求变更键已由上游借 S-cfc-96/97 入池;`--changes --days 2` 检出来源键 0 → 0,无未入池新键 |
+| **新增入池任务数*** | 0 个(本实例) | 0 新能力键缺口;避免重复建键 |
+| **既有键状态流转数** | 0 次(本实例) | 流转由 hub 派发会话 + 测试团队推进,本实例只读跟踪 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+> 本实例一轮只读核验,产出 1 份汇报文档(本文件,commit 见文末推送 sha)。以下为核验结论(非本实例提交):
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| 9e6825ef8 | 报告-成员绑定 / 食材成员视角归一(家庭72 报告63) | 实现(fix) | src/main(ReportCollectService/FoodRecommendService/两 Controller)+ src/test(FoodRecommendMemberViewResolutionTest 6 例全绿) | origin/cfclub | 是(01:20 推) | 新落地缺陷修复,自带 6 例测试;需测试侧补 SC 三类流证据后转正 |
+| 68a890d59 | S-cfc-91 guhe.gate.evidenceIsolate | RED | src/test(11 红 1 绿权限流契约测试) | origin/cfclub | 是 | 测试侧产出,契约测试已推 |
+| 15c7ce949 等一批 | S-cfc-80~90 guhe/diet RED | RED | src/test 契约单测 | origin/cfclub | 是 | 测试侧 RED 批次,已推送远端 |
+| 27 个 YELLOW 键对应实现 sha | family.invite.* / team.* / family.signature.* / family.earnings.* / admin.family.* 等 | YELLOW | src/main/契约 | origin/cfclub | **全部是**(`merge-base --is-ancestor` 27/27 复核通过) | 本轮逐一复核,无实现缺失 |
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | 0 | 目标池 ⏳=0 |
+| RED | 19 | 全为测试侧契约测试批次(guhe 域 13 + diet 域 4 + 其他),会话已派发在途 |
+| YELLOW | 27 | 实现全部在 origin/cfclub(27/27 复核),待测试 `--mark-green` |
+| GREEN | 8 | health.report.createReport / dan.assessment.getActiveMaterial / admin.auth.login / health.report.upload / task.history.view / growth.history.view / sleep.config.tags / achievement.grid.fiveCol |
+| DONE | 14 | auth.guest.wechatLogin / jwtGuard / family.parent.switchRole / task.parent.createTask / review / reward.cfEstimate.expectedCf / langgraph.questionnaire.generate / dan.report.parseFormat 等 |
+| **合计** | **68** | RED 19 + YELLOW 27 + GREEN 8 + DONE 14 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | 0 次(本实例) | 部署链由部署会话执行,本实例只读 |
+| 最新 `last_deployed` | ef3e81fec(01:13 在途)/ confirmed 最新 c22382b3 | 部署在途 pending 9 / confirmed 37,0 个「done 但未 confirmed」滞留 |
+| GREEN 部署链验证 | 通过(既有 GREEN 8 键无新增部署缺口) | pending 9 全为 YELLOW 键在途部署,无死锁 |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | 0 | 正常 | 目标池已清空,241 SC 全部有开工包覆盖(EVD-9~15) |
+| 契约常量(contract) | 0 | 正常 | 契约常量仅作「已契约化」标注,非池门槛 |
+| 测试用例引用(test_case_map) | 0 | 正常 | 当前无「用例引用 ∩ ⏳」新键 |
+| 执行结果回填(case_results) | 0 | 正常 | 无已部署键新执行结果待回填 |
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | 27 个 YELLOW 键送测试复跑契约测试 → `--mark-green` 收口(family.invite.* / team.* / family.signature.* / family.earnings.* / admin.family.*) | 测试侧复验;部署链 pending 9 收口 | 2026-11-09(今日) | P0 | 待测试处理 |
+| 2 | 9e6825ef8 报告-成员绑定/食材成员视角归一补 SC 三类流执行证据转正(家庭72 报告63 场景) | 测试侧补 e2e 证据;实现已在链 | 2026-11-09(今日) | P1 | 待测试处理 |
+| 3 | guhe K1 域 13 RED 契约测试(S-cfc-81~95)完成后,按契约实现使绿(guhe.gate.* / guhe.l1.* / guhe.kb.* 等) | RED 契约测试完成 + 测试侧 mark-red | 待 RED 落地后 | P0 | 待契约测试完成后启动 |
+| 4 | 观察 lan-204 手签名实现会话(running 61min 超 wedged 阈值)是否产出;未产出则 nudge/重派 | 会话产出核验 | 下轮(01:45) | P1 | 观察中 |
+
+> **优先级定义**:P0 = 阻断本轮测试或阶段推进;P1 = 影响本迭代交付。
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| 19 个 RED 键 red_sha 全空(契约测试已推但未 mark-red 登记) | 状态机 RED→YELLOW 无法推进 | 测试(登记时序) | 测试团队批次在途(lan-189 8 running),预计随批收口 | 无(测试侧职责,保留观察) |
+| lan-189 超并发上限(8 running / 配置 4) | 单节点过载风险 | 测试(调度资源) | hub 管控中,S-cfc-81 同任务双会话需确认合并 | 主理人知悉即可 |
+| 主工作树 314 处他人 WIP | 本实例所有写操作受限 | 环境 | 本实例全程单文件 hash-object 入库,不触碰 index | 无(既有状态) |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 回收 lan-189 RED 批次产出,核验 red_sha 登记 | 01:45 轮 | 会话完成 + mark-red | lan-189 | 若已登记,派发 guhe 实现使绿 |
+| 确认 lan-204 手签名会话产出(61min 超窗) | 01:45 轮 | 会话产出 | lan-204 | 未产出 → nudge/重派 |
+| 27 YELLOW mark-green 进度复核 | 随测试侧节奏 | 测试侧复跑 | 测试会话 | P0 衔接 |
+| guhe/diet RED 契约完成后按实现批次派发 | 待 RED 落地 | mark-red 登记 | lan-248/lan-204/prod | 防假 YELLOW |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+| 问题 ID | 能力键/任务号 | 问题类型 | 来源 | 接收时间 | 处置状态 | 关联 sha |
+|---------|---------------|----------|------|----------|----------|----------|
+| —(本实例 0 条) | — | — | — | — | — | — |
+
+### 附录 B:任务分派与会话恢复明细
+
+| 任务号 | 目标会话标签 | 任务类型 | 分派时间 | 当前状态 | 恢复会话标签 | 恢复原因 | 恢复时间 |
+|--------|--------------|----------|----------|----------|--------------|----------|----------|
+| S-cfc-96/97 diet.food.* | lan-189×2 | RED(测试侧) | 01:2x | running | — | — | — |
+| S-cfc-81/92/93/94/95 guhe.* | lan-189×5 | RED(测试侧) | 01:1x | running | — | — | — |
+| 手签名实现方式重写 | lan-204 | YELLOW(实现) | 00:22 | running 61min | — | — | — |
+| 补生产库 connection_type | prod-etotem | 部署/迁移 | 01:12 | running | — | — | — |
+
+> 本实例 0 分派、0 恢复;上表为 hub 侧在途会话实录(只读观测)。
+
+### 附录 C:新增入池任务明细
+
+| 任务号 | 能力键 | 需求溯源 | 设计溯源 | 创建时间 | 状态 |
+|--------|--------|----------|----------|----------|------|
+| S-cfc-96 | diet.food.healthAdjust | 404f9f74d(2026-11-09 01:07,用户拍板:食材推荐按健康问卷个性化调整) | specs/…health-questionnaire-adjust.md | 2026-11-09 | RED(在途) |
+| S-cfc-97 | diet.food.healthAdjustUi | 同上 | 同上 | 2026-11-09 | RED(在途) |
+
+### 附录 D:变更感知明细(来自 `--changes`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| specs/…-health-questionnaire-adjust.md(404f9f74d) | 需求+设计 | 2026-11-09 | diet.food.healthAdjust / diet.food.healthAdjustUi | 是(S-cfc-96/97,在途) |
+| guard 红线域 guhe 契约测试批次(S-cfc-80~95) | 测试用例 | 2026-11-08 | 0(既有键变化) | 既有键,--status 复核 |
+
+---
+
+## 汇总结论
+
+- **研发实现侧无缺口**:27/27 YELLOW 实现全部在 origin/cfclub 链上(`merge-base --is-ancestor` 逐键复核);19 RED 全部为测试侧契约测试批次(在途 13 会话 running);本轮 2 个新需求键(diet.food.*)已入池并派发。
+- **新缺陷修复跟踪**:9e6825ef8(01:20)报告-成员绑定/食材成员视角归一修复自带 6 例测试,转测试侧补 SC 三类流证据(家庭72 报告63)。
+- **R2 不留空闲**:13 会话 running 满载;唯一超窗观察项 = lan-204 手签名会话(61min)。
+- **约束遵守**:本实例只读核查 + git 真理 + 单文件 hash-object 入库;未改业务代码、未 add -A/checkout/stash/force-push、未代越测试 mark-red/mark-green。