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

merge(S-cfc-89): sync cfclub (S-cfc-85/86/87/88 guhe RED tests) into task/S-cfc-89 before push

Sisyphus Agent 4 дней назад
Родитель
Сommit
197d3e1288

+ 293 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheKbFieldExposeContractTest.java

@@ -0,0 +1,293 @@
+package com.etotem.cfc.unit;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 契约级单测:谷禾知识库既有键新字段暴露(能力键 {@code guhe.kb.fieldExpose},L2-3/T10)。
+ *
+ * <p><b>需求/设计锚点</b>({@code docs/superpowers/plans/2026-11-08-浠艾福平台程序修改计划.md}):
+ * <ul>
+ *   <li>§三 任务分解 · P1 · <b>T10 既有键新字段暴露(L2-3)</b>:菌属功能库 [40]/[72]/[73]
+ *       {@code 偏高影响}/{@code 偏低影响}、菌种综合信息「埃希氏菌属」{@code 偏低侧影响}、
+ *       艾德范思大肠杆菌 {@code 健康定植说明} <b>入 L2 查询返回</b>。</li>
+ *   <li>§2.2 既有键补齐(只填空、不覆盖,幂等):菌属功能库 [40] 埃希氏菌属 / [72] 大肠杆菌 /
+ *       [73] 脆弱拟杆菌(8 字段)、菌种综合信息「埃希氏菌属」{@code 偏低侧影响}、
+ *       艾德范思大肠杆菌 {@code 健康定植说明}。</li>
+ * </ul></p>
+ *
+ * <p><b>C 档平台探测(2026-11-08 自主核实,不假定包路径)</b>:
+ * <ul>
+ *   <li><b>#2 KB 服务加载方式</b>:平台已封装服务层 ——
+ *       {@code com.etotem.cfc.service.UnifiedKnowledgeBaseService}(12 个顶层模块,classpath
+ *       种子 {@code /knowledge-base/统一知识库.json} 兜底;查询入口 {@code getSection} /
+ *       {@code lookupBacteria}(菌属功能库,学名主键 + 属级回退)/ {@code lookupSpeciesInfo}
+ *       (菌种综合信息)/ {@code lookupIndicator} / {@code lookupNutrient})。</li>
+ *   <li><b>#1 Adapter 形态</b>、<b>#3 L4 引擎</b>、<b>#4 合规门禁挂载点</b>:与本键(L2-3 字段暴露)
+ *       无直接耦合,不进入本契约。</li>
+ * </ul></p>
+ *
+ * <p><b>实现现状(RED 依据)</b>:宿主 {@code UnifiedKnowledgeBaseService} 类已存在,但<b>契约要求
+ * 的新字段数据未落地</b>——种子 {@code 统一知识库.json} 中:
+ * <ul>
+ *   <li>菌属功能库 {@code Escherichia}(埃希氏菌属)/ {@code Escherichia coli}(大肠杆菌)/
+ *       {@code Bacteroides fragilis}(脆弱拟杆菌)的 {@code 偏高影响}/{@code 偏低影响} 键<b>存在但为空字符串</b>;</li>
+ *   <li>菌种综合信息「埃希氏菌属 Escherichia」<b>缺少</b> {@code 偏低侧影响} 键;</li>
+ *   <li>「艾德范思」来源知识及 {@code 健康定植说明} 键<b>全库缺失</b>。</li>
+ * </ul>
+ * 因此本测试以「L2 查询返回必须暴露上述新字段(非空)」为契约断言,当前实现未满足 → 断言失败(红);
+ * GREEN 阶段由实现方回填种子数据(只填空、不覆盖),服务查询即自动暴露。</p>
+ *
+ * <p><b>测试范围</b>:纯契约级单测 —— 无 Spring 容器 / 无 DB / 无真实微信,仅读 classpath 种子
+ * JSON(与宿主服务加载同源),反射锚定宿主类(避免 import 编译耦合)。</p>
+ *
+ * <p><b>权限流</b>:本能力为后端内部知识库查询(L2 字段暴露),无用户权限维度,权限流不适用,
+ * 以说明性用例标注。</p>
+ *
+ * <p><b>RED 预期</b>:正常流 3 用例在「字段非空」断言处失败(红)——{@code 偏高影响}/{@code 偏低影响}
+ * 为空、{@code 偏低侧影响} 与 {@code 健康定植说明} 缺失;边界流「只填空、不覆盖」守卫生效、
+ * 宿主结构锚点存在(二者当前通过,作为契约不变式/锚点)。</p>
+ */
+class GuheKbFieldExposeContractTest {
+
+    /** L2 宿主(统一知识库服务,12 模块)——契约查询返回承载者。 */
+    private static final String HOST_CLASS = "com.etotem.cfc.service.UnifiedKnowledgeBaseService";
+
+    /** classpath 种子文件(宿主加载源,与 docs 同源)。 */
+    private static final String KB_SEED_CLASSPATH = "knowledge-base/统一知识库.json";
+
+    /** 菌属功能库目标:{学名, 中文名} —— [40]埃希氏菌属 / [72]大肠杆菌 / [73]脆弱拟杆菌(§2.2)。 */
+    private static final String[][] TARGET_BACTERIA = {
+            {"Escherichia", "埃希氏菌属"},
+            {"Escherichia coli", "大肠杆菌"},
+            {"Bacteroides fragilis", "脆弱拟杆菌"}
+    };
+
+    /** 菌种综合信息目标菌种键(§2.2)。 */
+    private static final String SPECIES_INFO_KEY = "埃希氏菌属 Escherichia";
+
+    /** 艾德范思品牌来源标记(§2.2 艾德范思大肠杆菌 健康定植说明)。 */
+    private static final String AIDEFANSI_BRAND = "艾德范思";
+
+    // ==================== 正常流:L2 查询返回必须暴露新字段(T10 L2-3) ====================
+
+    @Test
+    @DisplayName("正常流-1:菌属功能库 [40]/[72]/[73] 应暴露非空 偏高影响/偏低影响")
+    void bacteriaGenus_highLowImpact_exposed() {
+        Map<String, Object> flora = section("菌属功能库");
+        assertNotNull(flora, "[guhe.kb.fieldExpose] 菌属功能库 模块缺失(L2 查询返回承载者)。");
+        for (String[] target : TARGET_BACTERIA) {
+            Map<String, Object> entry = findBacteria(flora, target[0], target[1]);
+            assertNotNull(entry,
+                    "[guhe.kb.fieldExpose] 菌属功能库缺少目标菌属: " + target[1]
+                            + "(学名 " + target[0] + ",契约 [40]/[72]/[73])。");
+            String high = text(entry.get("偏高影响"));
+            String low = text(entry.get("偏低影响"));
+            assertTrue(high != null && !high.trim().isEmpty(),
+                    "[guhe.kb.fieldExpose] 菌属功能库[" + target[1] + "] 偏高影响 应为非空"
+                            + "(契约 §2.2 既有键补齐,T10 L2-3 入 L2 查询返回);当前=" + high);
+            assertTrue(low != null && !low.trim().isEmpty(),
+                    "[guhe.kb.fieldExpose] 菌属功能库[" + target[1] + "] 偏低影响 应为非空"
+                            + "(契约 §2.2 既有键补齐,T10 L2-3 入 L2 查询返回);当前=" + low);
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-2:菌种综合信息「埃希氏菌属」应暴露非空 偏低侧影响")
+    void speciesInfo_escherichia_lowSideImpact_exposed() {
+        Map<String, Object> speciesInfo = section("菌种综合信息");
+        assertNotNull(speciesInfo, "[guhe.kb.fieldExpose] 菌种综合信息 模块缺失。");
+        Object raw = speciesInfo.get("菌种");
+        assertNotNull(raw, "[guhe.kb.fieldExpose] 菌种综合信息 缺少 菌种 节点。");
+        Map<?, ?> species = (Map<?, ?>) raw;
+        Object entryObj = species.get(SPECIES_INFO_KEY);
+        assertNotNull(entryObj,
+                "[guhe.kb.fieldExpose] 菌种综合信息缺少目标菌种: " + SPECIES_INFO_KEY + "。");
+        Map<?, ?> entry = (Map<?, ?>) entryObj;
+        String lowSide = text(entry.get("偏低侧影响"));
+        assertTrue(lowSide != null && !lowSide.trim().isEmpty(),
+                "[guhe.kb.fieldExpose] 菌种综合信息「埃希氏菌属」偏低侧影响 应为非空"
+                        + "(契约 §2.2,T10 L2-3 入 L2 查询返回);当前=" + lowSide);
+    }
+
+    @Test
+    @DisplayName("正常流-3:艾德范思大肠杆菌应暴露非空 健康定植说明")
+    void aidefansiEcoli_colonizationNote_exposed() {
+        Map<String, Object> root = kbRoot();
+        assertTrue(containsBrand(root, AIDEFANSI_BRAND),
+                "[guhe.kb.fieldExpose] 平台知识库缺少「艾德范思」来源知识"
+                        + "(契约 §2.2 艾德范思大肠杆菌 健康定植说明);艾德范思品牌未落地。");
+        String note = findNonEmptyStringValue(root, "健康定植说明");
+        assertTrue(note != null && !note.trim().isEmpty(),
+                "[guhe.kb.fieldExpose] 艾德范思大肠杆菌 健康定植说明 应为非空"
+                        + "(契约 §2.2,T10 L2-3 入 L2 查询返回);当前缺失。");
+    }
+
+    // ==================== 边界流:只填空、不覆盖(幂等) ====================
+
+    @Test
+    @DisplayName("边界流-1:既有键补齐只填空、不覆盖(既有 功能说明 不得被回填清空)")
+    void backfill_additive_doesNotClobberExisting() {
+        Map<String, Object> flora = section("菌属功能库");
+        Map<String, Object> escherichia = findBacteria(flora, "Escherichia", "埃希氏菌属");
+        assertNotNull(escherichia, "菌属功能库缺少 Escherichia。");
+        String func = text(escherichia.get("功能说明"));
+        assertTrue(func != null && !func.trim().isEmpty(),
+                "[guhe.kb.fieldExpose] 既有 功能说明 不得被回填覆盖"
+                        + "(契约 §2.2 只填空、不覆盖,幂等);功能说明=" + func);
+    }
+
+    // ==================== 宿主契约锚点:L2 查询承载者存在 ====================
+
+    @Test
+    @DisplayName("宿主锚点:UnifiedKnowledgeBaseService 应存在并提供 L2 查询入口")
+    void l2Host_serviceAndQuery_exist() {
+        Class<?> host = loadClass(HOST_CLASS);
+        assertNotNull(host,
+                "[guhe.kb.fieldExpose] L2 宿主 UnifiedKnowledgeBaseService 不存在(T10 L2-3 查询返回承载者)。");
+        assertTrue(hasMethod(host, "getSection"), "应提供 getSection(模块级查询)。");
+        assertTrue(hasMethod(host, "lookupBacteria"), "应提供 lookupBacteria(菌属功能库查询)。");
+        assertTrue(hasMethod(host, "lookupSpeciesInfo"), "应提供 lookupSpeciesInfo(菌种综合信息查询)。");
+    }
+
+    // ==================== 权限流(不适用):内部知识库查询,无用户权限维度 ====================
+
+    @Test
+    @DisplayName("权限流:本能力为后端内部知识库查询(L2 字段暴露),权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「L2 查询返回暴露新字段」内部链路,无用户权限校验维度。
+        assertTrue(true, "权限流不适用:guhe.kb.fieldExpose 为内部知识库查询,无用户权限维度。");
+    }
+
+    // ==================== 数据读取与解析(无 Spring / 无 DB) ====================
+
+    /** 读 classpath 种子 KB 根(与宿主加载同源)。 */
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> kbRoot() {
+        try (InputStream is = getClass().getClassLoader().getResourceAsStream(KB_SEED_CLASSPATH)) {
+            assertNotNull(is, "classpath 种子不存在: " + KB_SEED_CLASSPATH);
+            return new ObjectMapper().readValue(is, new TypeReference<Map<String, Object>>() {});
+        } catch (Exception e) {
+            throw new IllegalStateException("解析种子KB失败: " + e.getMessage(), e);
+        }
+    }
+
+    /** 取指定顶层模块(sectionMap.get(moduleKey))。 */
+    private Map<String, Object> section(String moduleKey) {
+        Map<String, Object> root = kbRoot();
+        Object obj = root.get(moduleKey);
+        if (obj instanceof Map) {
+            return (Map<String, Object>) obj;
+        }
+        return null;
+    }
+
+    /** 在 菌属功能库.菌属 数组中按 学名/中文名 定位条目。 */
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> findBacteria(Map<String, Object> flora, String latin, String cnName) {
+        Object raw = flora.get("菌属");
+        if (!(raw instanceof List)) {
+            return null;
+        }
+        for (Object item : (List<Object>) raw) {
+            if (!(item instanceof Map)) {
+                continue;
+            }
+            Map<String, Object> entry = (Map<String, Object>) item;
+            if (latin.equals(entry.get("学名")) || cnName.equals(entry.get("中文名"))) {
+                return entry;
+            }
+        }
+        return null;
+    }
+
+    /** 深度遍历:任一 字符串值/键 包含品牌标记。 */
+    @SuppressWarnings("unchecked")
+    private boolean containsBrand(Object node, String brand) {
+        if (node == null) {
+            return false;
+        }
+        if (node instanceof Map) {
+            for (Map.Entry<?, ?> e : ((Map<?, ?>) node).entrySet()) {
+                if (String.valueOf(e.getKey()).contains(brand)) {
+                    return true;
+                }
+                if (containsBrand(e.getValue(), brand)) {
+                    return true;
+                }
+            }
+        } else if (node instanceof List) {
+            for (Object item : (List<Object>) node) {
+                if (containsBrand(item, brand)) {
+                    return true;
+                }
+            }
+        } else if (node instanceof String) {
+            return ((String) node).contains(brand);
+        }
+        return false;
+    }
+
+    /** 深度遍历:返回首个指定键的非空字符串值。 */
+    @SuppressWarnings("unchecked")
+    private String findNonEmptyStringValue(Object node, String fieldKey) {
+        if (node == null) {
+            return null;
+        }
+        if (node instanceof Map) {
+            for (Map.Entry<?, ?> e : ((Map<?, ?>) node).entrySet()) {
+                if (fieldKey.equals(String.valueOf(e.getKey()))) {
+                    String v = text(e.getValue());
+                    if (v != null && !v.trim().isEmpty()) {
+                        return v;
+                    }
+                }
+                String found = findNonEmptyStringValue(e.getValue(), fieldKey);
+                if (found != null) {
+                    return found;
+                }
+            }
+        } else if (node instanceof List) {
+            for (Object item : (List<Object>) node) {
+                String found = findNonEmptyStringValue(item, fieldKey);
+                if (found != null) {
+                    return found;
+                }
+            }
+        }
+        return null;
+    }
+
+    // ==================== 反射辅助 ====================
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private boolean hasMethod(Class<?> clazz, String methodName) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(methodName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String text(Object v) {
+        return v == null ? null : String.valueOf(v);
+    }
+}

+ 363 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheLowSideAggregateContractTest.java

@@ -0,0 +1,363 @@
+package com.etotem.cfc.unit;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.jar.JarFile;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * 契约级单测:P3 跨案例聚合(能力键 {@code guhe.p3.lowSideAggregate},T15)。
+ *
+ * <p>契约来源(2026-11-08-浠艾福平台程序修改计划.md §三-P3 / §三-T15 / §五 边界声明):</p>
+ * <ul>
+ *   <li><b>T15 跨案例聚合</b>:低侧口径并入既有 P3 聚合口径,可检索「同条件致病菌低侧」应对方案;
+ *       验收:跨案例可按低侧判读口径聚合同策略。</li>
+ *   <li><b>§三-L4 归因引擎 ③</b>:条件致病菌「偏低」按四机制解释、全部标「关联非因果」;
+ *       参考下限 0 的菌不标「偏低超标」;⛔ 禁「缺 X → 买含 X 菌株产品」桥接。</li>
+ *   <li><b>§2.2 知识库</b>:新增子键「条件致病菌低侧判读补充(人工)」为聚合口径的事实锚点。</li>
+ *   <li><b>§五</b>:所有低侧机制表述为「关联非因果」,不构成健康评估/诊断/干预建议。</li>
+ * </ul>
+ *
+ * <p><b>实现现状(C 档探测,src/main 现状核实)</b>:L4 归因引擎({@code FiveDimAttributionEngine})、
+ * 低侧红线组({@code AttributionLowSideRules} + {@code AttributionComplianceGate})已合入主链;
+ * 但 <b>P3 跨案例聚合未落地</b>——{@code com.etotem.cfc.service} 及其
+ * {@code attribution} 子包无类名含 {@code CaseAggregate} / {@code CrossCase} / {@code CaseStrategy} /
+ * {@code LowSideAggregate} 的服务类,{@code schema.sql} 无跨案例聚合承载表,
+ * {@code constant} 包无「条件致病菌低侧判读」聚合口径常量。
+ * 因此本测试以「包内类名扫描 + 方法签名探测 + classpath 资源探测 + 反射常量探测」验证契约缺失:
+ * 能编译通过、能运行、断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>RED 预期</b>:正常流/边界流用例均应失败(红)——聚合服务类、聚合检索入口、聚合承载表、
+ * 判读口径常量、同策略聚类语义均未落地。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者落地跨案例聚合后本测试自动转绿——<ol>
+ * <li>在 {@code com.etotem.cfc.service}(或 {@code attribution} 子包)新增聚合服务类
+ *     (类名含 {@code CaseAggregate} / {@code CrossCase} / {@code CaseStrategy} / {@code LowSideAggregate}
+ *     任一即可);</li>
+ * <li>该类提供按「条件致病菌低侧判读」口径聚合跨案例、检索应对方案的入口方法
+ *     (方法名含 {@code aggregate} 且 {@code lowSide},或含 {@code aggregate} 且 {@code strategy},
+ *     或含 {@code aggregate} 且 {@code case});聚合结果按判读口径聚类为「同策略」;</li>
+ * <li>落地聚合承载表({@code schema.sql} 表名含 {@code attribution_case})与
+ *     「条件致病菌低侧判读补充(人工)」口径常量({@code constant} 包 String 常量含
+ *     低侧判读口径键名);</li>
+ * </ol>
+ * 届时可将结构探测升级为行为级验证(mock 聚合服务断言低侧案例聚合同策略)。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为 P3 沉淀与协同层的跨案例聚合(后端内部归因资产层),
+ * 无用户权限维度,「权限(适用时)」三流中的权限流不适用,以说明性用例标注。</p>
+ */
+class GuheLowSideAggregateContractTest {
+
+    /** 能力键 */
+    private static final String CAP_KEY = "guhe.p3.lowSideAggregate";
+
+    /** 聚合服务类名关键词(实现未落地 → 类名由实现者定,任一台词即满足契约) */
+    private static final List<String> AGGREGATE_CLASS_KEYWORDS = Arrays.asList(
+            "CaseAggregate", "CrossCase", "CaseStrategy", "LowSideAggregate");
+
+    /** 聚合检索入口方法名关键词(聚合 + 低侧/策略/案例) */
+    private static final List<String> AGGREGATE_METHOD_KEYWORDS = Arrays.asList(
+            "aggregate", "Aggregate");
+
+    /** 低侧判读口径关键词(方法名/参数/常量层) */
+    private static final List<String> LOW_SIDE_KEYWORDS = Arrays.asList(
+            "lowSide", "LowSide", "低侧");
+
+    /** 同策略聚类关键词(T15:聚合同策略) */
+    private static final List<String> STRATEGY_KEYWORDS = Arrays.asList(
+            "strategy", "Strategy", "策略");
+
+    /** 判读口径键名(§2.2 KB 新增子键,作为聚合口径的事实锚点) */
+    private static final String LOW_SIDE_READING_KEY = "条件致病菌低侧判读补充(人工)";
+
+    /** 需扫描的候选包(不得假定实现包路径,扫全部候选包) */
+    private static final List<String> SCAN_PACKAGES = Arrays.asList(
+            "com.etotem.cfc.service",
+            "com.etotem.cfc.service.attribution",
+            "com.etotem.cfc.constant",
+            "com.etotem.cfc.entity");
+
+    // ------------------------------------------------------------------
+    // 正常流:跨案例聚合服务类应存在(T15 聚合入口)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:attribution 服务层应存在跨案例聚合服务类")
+    void aggregateService_classExists() {
+        String fqcn = findClassInPackages(AGGREGATE_CLASS_KEYWORDS);
+        assertTrue(fqcn != null,
+                "[" + CAP_KEY + "] service/attribution 等候选包无跨案例聚合服务类"
+                        + "(类名应含 CaseAggregate/CrossCase/CaseStrategy/LowSideAggregate 任一)。"
+                        + "T15:低侧口径并入既有 P3 聚合口径,可检索「同条件致病菌低侧」应对方案。"
+                        + "当前跨案例聚合实现未落地。");
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流:按低侧判读口径聚合 + 检索应对方案(T15 核心行为)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-2:聚合服务应提供按低侧判读口径聚合检索应对方案的入口方法")
+    void aggregateService_hasAggregateEntryMethod() {
+        Class<?> clazz = loadAggregateServiceClass();
+        if (clazz == null) {
+            fail("[" + CAP_KEY + "] 跨案例聚合服务类不存在,无法验证聚合入口方法(T15)。"
+                    + "当前实现未落地。");
+        }
+        boolean hasAggregate = false;
+        for (Method m : clazz.getDeclaredMethods()) {
+            String name = m.getName();
+            boolean agg = containsAny(name, AGGREGATE_METHOD_KEYWORDS);
+            boolean lowSide = containsAny(name, LOW_SIDE_KEYWORDS);
+            boolean strategy = containsAny(name, STRATEGY_KEYWORDS);
+            boolean caseKw = name.toLowerCase().contains("case");
+            if (agg && (lowSide || strategy || caseKw)) {
+                hasAggregate = true;
+                break;
+            }
+        }
+        assertTrue(hasAggregate,
+                "[" + CAP_KEY + "] 跨案例聚合服务类 " + clazz.getSimpleName()
+                        + " 应提供按「条件致病菌低侧判读」口径聚合跨案例、检索应对方案的入口方法"
+                        + "(方法名含 aggregate 且 lowSide/strategy/case)。"
+                        + "T15:可检索「同条件致病菌低侧」应对方案;跨案例可按低侧判读口径聚合同策略。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:聚合承载表(T15 数据层)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:schema.sql 应有跨案例聚合承载表(表名含 attribution_case)")
+    void schemaSql_hasCrossCaseAggregateTable() {
+        String schema = readSchemaSql();
+        assertTrue(schema.contains("attribution_case"),
+                "[" + CAP_KEY + "] schema.sql 缺少跨案例聚合承载表(表名应含 attribution_case,"
+                        + "如 attribution_case_strategies),用于沉淀跨案例「同条件致病菌低侧」应对方案。"
+                        + "T15:低侧口径并入既有 P3 聚合口径。当前聚合承载表未落地。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:判读口径常量(聚合口径的事实锚点,§2.2)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-2:constant 包应暴露「条件致病菌低侧判读补充(人工)」口径常量")
+    void constantPackage_exposesLowSideReadingKey() {
+        boolean found = false;
+        for (String pkg : SCAN_PACKAGES) {
+            for (String fqcn : scanClassNames(pkg)) {
+                Class<?> clazz = loadClass(fqcn);
+                if (clazz == null) {
+                    continue;
+                }
+                if (hasStringConstantContaining(clazz, "条件致病菌低侧")
+                        || hasStringConstantContaining(clazz, LOW_SIDE_READING_KEY)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (found) {
+                break;
+            }
+        }
+        assertTrue(found,
+                "[" + CAP_KEY + "] constant 等候选包应暴露「条件致病菌低侧判读补充(人工)」"
+                        + "判读口径常量(§2.2 KB 新增子键),作为跨案例聚合口径的事实锚点。"
+                        + "T15:低侧口径并入既有 P3 聚合口径。当前判读口径常量未落地。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:聚合同策略语义(T15 验收)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-3:聚合结果应支持「同条件致病菌低侧→同策略」聚类(验收)")
+    void aggregateResult_supportsSameReadingSameStrategy() {
+        Class<?> clazz = loadAggregateServiceClass();
+        if (clazz == null) {
+            fail("[" + CAP_KEY + "] 跨案例聚合服务类不存在,无法验证同策略聚类语义(T15)。"
+                    + "当前实现未落地。");
+        }
+        boolean hasStrategySemantics = false;
+        for (Method m : clazz.getDeclaredMethods()) {
+            String name = m.getName();
+            boolean agg = containsAny(name, AGGREGATE_METHOD_KEYWORDS);
+            boolean strategy = containsAny(name, STRATEGY_KEYWORDS);
+            if (agg && strategy) {
+                hasStrategySemantics = true;
+                break;
+            }
+        }
+        // 兜底:方法返回类型名含 strategy(聚合结果按判读口径聚类为同策略)
+        if (!hasStrategySemantics) {
+            for (Method m : clazz.getDeclaredMethods()) {
+                String ret = m.getReturnType().getSimpleName();
+                if (containsAny(ret, STRATEGY_KEYWORDS) || ret.toLowerCase().contains("aggregate")) {
+                    Class<?>[] params = m.getParameterTypes();
+                    for (Class<?> p : params) {
+                        if (containsAny(p.getSimpleName(), LOW_SIDE_KEYWORDS)
+                                || containsAny(p.getSimpleName(), STRATEGY_KEYWORDS)) {
+                            return; // 返回/参数已表达聚合口径或策略 → 契约满足(说明性绿)
+                        }
+                    }
+                    hasStrategySemantics = true;
+                    break;
+                }
+            }
+        }
+        assertTrue(hasStrategySemantics,
+                "[" + CAP_KEY + "] 跨案例聚合服务类 " + clazz.getSimpleName()
+                        + " 应表达「同条件致病菌低侧 → 同策略」聚类语义"
+                        + "(方法名含 aggregate+strategy,或返回/参数类型表达聚合口径与策略)。"
+                        + "T15 验收:跨案例可按低侧判读口径聚合同策略。当前未落地。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):P3 沉淀与协同层,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为 P3 沉淀与协同层的跨案例聚合,无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「低侧口径并入既有 P3 聚合口径 → 跨案例可检索同低侧应对方案」,
+        // 属后端内部归因资产层,不产生用户可见权限校验维度。
+        // 不产生断言失败;仅为三流覆盖标注「不适用」。
+        assertTrue(true, "权限流不适用:guhe.p3.lowSideAggregate 为 P3 沉淀与协同层跨案例聚合,无权权限维度。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射/资源辅助(纯 JUnit5 + 反射,无 Spring / DB / Mockito)
+    // ------------------------------------------------------------------
+
+    /** 在候选包内查找类名含任一关键词的类(返回 FQCN;找不到返回 null) */
+    private String findClassInPackages(List<String> keywords) {
+        for (String pkg : SCAN_PACKAGES) {
+            for (String fqcn : scanClassNames(pkg)) {
+                String simple = fqcn.substring(fqcn.lastIndexOf('.') + 1);
+                for (String kw : keywords) {
+                    if (simple.contains(kw)) {
+                        return fqcn;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /** 加载聚合服务类(找不到返回 null) */
+    private Class<?> loadAggregateServiceClass() {
+        String fqcn = findClassInPackages(AGGREGATE_CLASS_KEYWORDS);
+        return fqcn == null ? null : loadClass(fqcn);
+    }
+
+    private boolean containsAny(String text, List<String> keywords) {
+        if (text == null) return false;
+        for (String kw : keywords) {
+            if (text.contains(kw)) return true;
+        }
+        return false;
+    }
+
+    /** 是否存在静态 String 常量值包含 target(口径键探针) */
+    private boolean hasStringConstantContaining(Class<?> clazz, String target) {
+        try {
+            for (Field f : clazz.getDeclaredFields()) {
+                if (f.getType() != String.class) continue;
+                if (!Modifier.isStatic(f.getModifiers()) || !Modifier.isFinal(f.getModifiers())) continue;
+                try {
+                    f.setAccessible(true);
+                    Object v = f.get(null);
+                    if (v instanceof String && ((String) v).contains(target)) return true;
+                } catch (Exception ignored) {
+                    // 常量读取失败 → 跳过
+                }
+            }
+        } catch (Throwable ignored) {
+            // 类初始化异常 → 跳过该类的常量扫描
+        }
+        return false;
+    }
+
+    /** 扫描包内所有类全限定名(classpath 资源;支持 file: 目录与 jar: 两种形态) */
+    private List<String> scanClassNames(String pkg) {
+        List<String> names = new ArrayList<String>();
+        try {
+            String path = pkg.replace('.', '/');
+            ClassLoader cl = Thread.currentThread().getContextClassLoader();
+            Enumeration<URL> resources = cl.getResources(path);
+            while (resources.hasMoreElements()) {
+                URL url = resources.nextElement();
+                String protocol = url.getProtocol();
+                if ("file".equals(protocol)) {
+                    File dir = new File(url.toURI());
+                    File[] files = dir.listFiles();
+                    if (files == null) continue;
+                    for (File f : files) {
+                        if (f.isFile() && f.getName().endsWith(".class")) {
+                            names.add(pkg + "." + f.getName().substring(0, f.getName().length() - 6));
+                        }
+                    }
+                } else if ("jar".equals(protocol)) {
+                    JarURLConnection conn = (JarURLConnection) url.openConnection();
+                    try (JarFile jar = conn.getJarFile()) {
+                        java.util.Enumeration<java.util.jar.JarEntry> entries = jar.entries();
+                        while (entries.hasMoreElements()) {
+                            java.util.jar.JarEntry e = entries.nextElement();
+                            String n = e.getName();
+                            if (n.startsWith(path + "/") && n.endsWith(".class") && n.indexOf('/', path.length() + 1) < 0) {
+                                names.add(n.substring(0, n.length() - 6).replace('/', '.'));
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception ignored) {
+            // 包扫描失败 → 返回已收集列表(空),结构探测按缺失处理
+        }
+        Set<String> unique = new HashSet<String>(names);
+        return new ArrayList<String>(unique);
+    }
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private String readSchemaSql() {
+        try (InputStream is = getClass().getClassLoader().getResourceAsStream("schema.sql")) {
+            if (is == null) {
+                return "";
+            }
+            java.util.Scanner scanner = new java.util.Scanner(is, StandardCharsets.UTF_8.name());
+            try {
+                scanner.useDelimiter("\\A");
+                return scanner.hasNext() ? scanner.next() : "";
+            } finally {
+                scanner.close();
+            }
+        } catch (Exception e) {
+            return "";
+        }
+    }
+}

+ 225 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheLowSideNoSupplementContractTest.java

@@ -0,0 +1,225 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.constant.AttributionLowSideRules;
+import com.etotem.cfc.service.attribution.AttributionComplianceGate;
+import com.etotem.cfc.service.attribution.IndicatorInterpretEngine;
+import com.etotem.cfc.service.attribution.model.IndicatorReading;
+import com.etotem.cfc.service.attribution.model.MemberAttribution;
+import com.etotem.cfc.service.attribution.model.MemberProfile;
+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.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;
+
+/**
+ * 契约级单测:谷禾 v2 低侧口径「低位补菌红线」(能力键 {@code guhe.lowSide.noSupplement})。
+ *
+ * <p>契约来源(2026-11-08《浠艾福平台程序修改计划(guhe_v2 兼容 · 知识库低侧口径 · 专家同步)》§三任务分解 T4,P0):
+ * <blockquote>
+ *   T4 低位补菌红线(L4-3):<b>无成熟证据支持「缺大肠杆菌→补大肠杆菌」</b>;
+ *   ⛔ 禁「报告缺 X → 买含 X 菌株产品」桥接(叠加 §5.2 16S 粒度声明)。
+ *   <br>验收:任意「偏低」归因节点 {@code association_only==true};存量无「缺大肠杆菌→补菌」措辞;
+ *   低侧机制证据全部标 PUB/KB。
+ * </blockquote></p>
+ *
+ * <p><b>契约语义</b>:本能力键不是「事后文本扫描」而是 <b>L4 归因引擎层面的禁补菌红线</b> ——
+ * 归因引擎对任意「偏低」归因节点(含条件致病菌如大肠杆菌)<b>不生成补菌建议</b>,
+ * 并在解读产出中显式携带「偏低不推荐补菌」口径(无成熟证据支持缺X→补X),
+ * 使「报告缺 X → 买含 X 菌株产品」桥接在 <b>源头</b>被拦截,而非仅靠门禁事后回扫。</p>
+ *
+ * <p><b>实现现状(src/main 探测,2026-11-08)</b>:L4 归因引擎与 G-1 门禁已落地
+ * ({@code IndicatorInterpretEngine} / {@code AttributionComplianceGate} / {@code AttributionLowSideRules}),
+ * 但已落地的是 <b>T5 G-1 低侧红线组</b>({@code association_only} / {@code no_causal_bridge} /
+ * {@code no_prescription} 三规则码 + 措辞白名单 + 门禁文本回扫 {@code scan})。
+ * <b>T4 低位补菌红线(L4-3)未落地</b>:{@code AttributionLowSideRules} 无独立
+ * {@code no_supplement} 规则码、无「偏低不推荐补菌」口径常量;{@code LowSideNode} 无节点级
+ * 禁补菌标记;{@code IndicatorInterpretEngine} 对偏低节点仅产通用「先医后补」含义,未产
+ * 「不推荐补菌」口径。故本测试以「反射结构 + 纯规则行为」双通道探测实现缺失:能编译、
+ * 能运行、断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 {@code AttributionLowSideRules} 增加
+ * {@code RULE_NO_SUPPLEMENT="no_supplement"} 与 {@code NO_SUPPLEMENT_HINT}(含「不推荐补菌」),
+ * 在 {@code AttributionComplianceGate.LowSideNode} 暴露 {@code isNoSupplement()}(低侧节点恒 true),
+ * 并在 {@code IndicatorInterpretEngine} 的偏低含义生成路径注入「偏低不推荐补菌」口径后,本测试自动转绿。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为后端内部 L4 归因规则,无用户权限维度,权限流不适用,以说明性用例标注。</p>
+ */
+class GuheLowSideNoSupplementContractTest {
+
+    /** T4 期望的独立规则码(区别于 T5 的 no_causal_bridge) */
+    private static final String RULE_NO_SUPPLEMENT = "no_supplement";
+
+    /** T4 期望的「偏低不推荐补菌」口径常量名 */
+    private static final String HINT_FIELD = "NO_SUPPLEMENT_HINT";
+
+    /** T4 期望的节点级禁补菌标记方法名 */
+    private static final String NODE_FLAG_METHOD = "isNoSupplement";
+
+    // ------------------------------------------------------------------
+    // 正常流:规则层应暴露独立的 low-side 禁补菌红线(T4 规则码 + 口径)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:AttributionLowSideRules 应暴露 RULE_NO_SUPPLEMENT 规则码")
+    void lowSideRules_exposesNoSupplementRuleCode() {
+        Field f = declaredField(AttributionLowSideRules.class, "RULE_NO_SUPPLEMENT");
+        assertNotNull(f,
+                "[guhe.lowSide.noSupplement] AttributionLowSideRules 缺少 RULE_NO_SUPPLEMENT 规则码:"
+                        + "T4(L4-3)要求独立「低位补菌红线」——无成熟证据支持「缺大肠杆菌→补大肠杆菌」。"
+                        + "当前仅落地 T5 的 no_causal_bridge,未区分「引擎不产出补菌建议」与「文本回扫」。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.noSupplement] RULE_NO_SUPPLEMENT 常量值不应为 null。");
+            assertTrue(RULE_NO_SUPPLEMENT.equals(String.valueOf(v)),
+                    "[guhe.lowSide.noSupplement] RULE_NO_SUPPLEMENT 应取值为 \"" + RULE_NO_SUPPLEMENT
+                            + "\",实际为 \"" + v + "\"。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.noSupplement] 读取 RULE_NO_SUPPLEMENT 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-2:AttributionLowSideRules 应暴露「偏低不推荐补菌」口径常量")
+    void lowSideRules_exposesNoSupplementHintWording() {
+        Field f = declaredField(AttributionLowSideRules.class, HINT_FIELD);
+        assertNotNull(f,
+                "[guhe.lowSide.noSupplement] AttributionLowSideRules 缺少 " + HINT_FIELD
+                        + " 口径常量:T4 要求「偏低不推荐补菌」显式口径(无成熟证据支持缺X→补X),"
+                        + "供 L4 引擎与报告侧引用,而非仅通用「先医后补」。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.noSupplement] " + HINT_FIELD + " 常量值不应为 null。");
+            String text = String.valueOf(v);
+            assertTrue(text.contains("不推荐补菌"),
+                    "[guhe.lowSide.noSupplement] " + HINT_FIELD + " 应含「不推荐补菌」口径(§5.1.1 专家口径第四条),"
+                            + "实际为:「" + text + "」。");
+            assertTrue(text.contains("缺"),
+                    "[guhe.lowSide.noSupplement] " + HINT_FIELD + " 应含「无成熟证据支持缺X→补X」表述,"
+                            + "实际为:「" + text + "」。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.noSupplement] 读取 " + HINT_FIELD + " 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-3:低侧节点应暴露节点级禁补菌标记 isNoSupplement()")
+    void lowSideNode_exposesNoSupplementFlag() {
+        boolean hasFlag = false;
+        for (Method m : AttributionComplianceGate.LowSideNode.class.getDeclaredMethods()) {
+            if (NODE_FLAG_METHOD.equals(m.getName())) {
+                hasFlag = true;
+                break;
+            }
+        }
+        assertTrue(hasFlag,
+                "[guhe.lowSide.noSupplement] AttributionComplianceGate.LowSideNode 缺少 " + NODE_FLAG_METHOD
+                        + "() 节点级禁补菌标记:T4 要求「偏低」归因节点在源头即标注不推荐补菌,"
+                        + "而非仅门禁事后回扫。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:L4 解读引擎对「偏低」条件致病菌节点不产补菌建议(源头拦截)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:偏低大肠杆菌节点的干预含义应携带「不推荐补菌」口径")
+    void interpret_lowSideEColi_meaningCarriesNoSupplementRedline() {
+        IndicatorReading reading = lowSideEColi();
+        MemberProfile member = profile();
+        IndicatorInterpretEngine engine = new IndicatorInterpretEngine();
+
+        List<MemberAttribution> attrs = engine.interpret(Collections.singletonList(reading),
+                Collections.singletonList(member));
+        assertNotNull(attrs, "[guhe.lowSide.noSupplement] interpret 不应返回 null。");
+        assertFalse(attrs.isEmpty(), "[guhe.lowSide.noSupplement] interpret 应产出至少 1 人归因。");
+        IndicatorReading out = attrs.get(0).getReadings().get(0);
+        String meaning = out.getMeaning() == null ? "" : out.getMeaning();
+        assertTrue(meaning.contains("不推荐补菌"),
+                "[guhe.lowSide.noSupplement] 偏低大肠杆菌节点干预含义应携带「偏低不推荐补菌」口径"
+                        + "(T4 L4-3,无成熟证据支持缺X→补X),实际为:「" + meaning + "」。"
+                        + "当前仅产通用「先医后补」,未落地 T4 源头禁补菌。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:偏低大肠杆菌节点的应对方向不得含补菌产品桥接措辞")
+    void interpret_lowSideEColi_actionsContainNoSupplementBridge() {
+        IndicatorReading reading = lowSideEColi();
+        MemberProfile member = profile();
+        IndicatorInterpretEngine engine = new IndicatorInterpretEngine();
+
+        List<MemberAttribution> attrs = engine.interpret(Collections.singletonList(reading),
+                Collections.singletonList(member));
+        for (String action : attrs.get(0).getActions()) {
+            assertFalse(containsSupplementBridge(action),
+                    "[guhe.lowSide.noSupplement] 偏低大肠杆菌节点应对方向不得含「补菌产品」桥接措辞"
+                            + "(⛔ 禁「报告缺 X → 买含 X 菌株产品」),实际为:「" + action + "」。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):后端内部 L4 归因规则,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为后端内部 L4 归因规则,无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「L4 归因引擎 → 低侧禁补菌」内部规则,无权限校验维度。
+        assertTrue(true, "权限流不适用:guhe.lowSide.noSupplement 为后端内部 L4 归因规则,无用户权限维度。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射与数据构造辅助
+    // ------------------------------------------------------------------
+
+    private static Field declaredField(Class<?> clazz, String name) {
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getName().equals(name)) {
+                f.setAccessible(true);
+                return f;
+            }
+        }
+        return null;
+    }
+
+    /** 偏低条件致病菌读数(大肠杆菌,P3,参考下限 0 / ND 口径) */
+    private static IndicatorReading lowSideEColi() {
+        IndicatorReading r = new IndicatorReading();
+        r.setMemberId(1L);
+        r.setMemberName("妈妈");
+        r.setCode("Escherichia_coli");
+        r.setDisplayName("大肠杆菌");
+        r.setValue("0.5%");
+        r.setPercentile("P3");
+        r.setStatus(IndicatorReading.STATUS_LOW);
+        r.setBand(IndicatorReading.BAND_LOW);
+        return r;
+    }
+
+    private static MemberProfile profile() {
+        MemberProfile p = new MemberProfile();
+        p.setMemberId(1L);
+        p.setName("妈妈");
+        p.setRole("mom");
+        p.setRoleLabel("妈妈");
+        return p;
+    }
+
+    /** 补菌产品桥接措辞(「报告缺 X → 买含 X 菌株产品」) */
+    private static boolean containsSupplementBridge(String text) {
+        if (text == null) {
+            return false;
+        }
+        String t = text;
+        return t.contains("补大肠杆菌") || t.contains("补充大肠杆菌")
+                || t.contains("补菌") || t.contains("需要补充") || t.contains("需补充")
+                || t.contains("买含") || t.contains("购买") || t.contains("选购")
+                || t.contains("下单") || t.contains("菌株产品");
+    }
+}

+ 322 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheNoPathMapContractTest.java

@@ -0,0 +1,322 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.constant.AttributionLowSideRules;
+import com.etotem.cfc.constant.AttributionWeights;
+import com.etotem.cfc.service.attribution.FiveDimAttributionEngine;
+import com.etotem.cfc.service.attribution.model.IndicatorReading;
+import com.etotem.cfc.service.attribution.model.MemberAttribution;
+import com.etotem.cfc.service.attribution.model.MemberProfile;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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;
+
+/**
+ * 契约级单测:五维 no_path 映射(能力键 {@code guhe.l4.noPathMap},单元 l4)。
+ *
+ * <p><b>需求/设计锚点</b>:
+ * {@code docs/superpowers/plans/2026-11-08-浠艾福平台程序修改计划.md} §三任务分解
+ * §T14(P1,L4-4):菌群观测对「富/行」自动标 {@code no_path};心/智介导链自动阶梯并锁
+ * 关联措辞;低侧口径归身维主战场。(对齐 v1.1 验收 7/9/10)</p>
+ *
+ * <p><b>平台现状探测(C 档 4 项,会话实现前自主探测)</b></p>
+ * <ul>
+ *   <li><b>Adapter 形态(#1)</b>:报告解析侧存在 {@code ReportParserDispatchService} /
+ *       {@code LangGraphReportParseService};guhe_v2 采集为 Python 侧 {@code extract_guhe_v2.py},
+ *       Java 侧以 {@code HealthGutFlora} + {@code AttributionInput.gutReportIds} 进入归因,
+ *       L4 段消费与采集 Adapter 解耦。</li>
+ *   <li><b>KB 加载方式(#2)</b>:{@code 统一知识库.json}(Python/LangGraph 侧,{@code +guhe2026.2}),
+ *       Java 侧 L4 归因不直接读 KB,L2 查询走 LangGraph——T14 不涉及 KB 装载。</li>
+ *   <li><b>L4 引擎存在性(#3)</b>:{@link FiveDimAttributionEngine} 已存在(纯 Java 确定性规则引擎,
+ *       {@code attribute()} 计算五维 0-100 相对刻度)→ T14 为「补规则」而非「新建引擎」。</li>
+ *   <li><b>合规门禁挂载点(#4)</b>:{@code AttributionComplianceGate} + {@code AttributionLowSideRules}
+ *       (G-1 association_only,措辞白名单)已存在,L4 结论前 enforce → 心/智介导链措辞锁定复用白名单。</li>
+ * </ul>
+ *
+ * <p><b>被校验的契约行为(T14:L4-4 五维 no_path 映射)</b></p>
+ * <ul>
+ *   <li>菌群观测({@code source=gut_flora})对「富/行」两维自动标 {@code no_path}:菌群为生物观测,
+ *       与「富(经济)/行(行为习惯)」维度无直接归因路径,异常不应对该两维产生扣分/弱维。</li>
+ *   <li>菌群观测对「心/智」走介导链(mediation chain,如肠脑轴):自动生成阶梯式表述,
+ *       且每级措辞锁定为关联措辞(命中 {@link AttributionLowSideRules#WORDING_WHITELIST} 白名单,
+ *       禁因果/禁补菌桥接)。</li>
+ *   <li>菌群低侧(偏低)判读口径归「身」维主战场:其五维弱维不应出现「富/行」。</li>
+ * </ul>
+ *
+ * <p><b>宿主契约(TDD-RED 锚点)</b>:{@code com.etotem.cfc.service.attribution.FiveDimAttributionEngine}
+ * —— 引擎类已存在(ATT-S2-B1 合入),但 T14 的 no_path 映射 <b>尚未实现</b>(cfclub 全仓
+ * {@code no_path}/{@code noPath} 零命中)。本测试通过反射锚定契约方法(避免直接 import 不存在方法
+ * 导致编译失败),运行时契约方法缺失即断言失败(红);GREEN 阶段实现方按
+ * 「契约方法签名」落地后转为验证真实行为(绿)。</p>
+ *
+ * <p><b>契约方法签名(实现方按此落地,落在 {@code FiveDimAttributionEngine} 上)</b></p>
+ * <ul>
+ *   <li>{@code Map<String, Set<String>> noPathMap()} —— 数据源 → no_path 维度集合;
+ *       契约期望含 {@code "gut_flora" → {"富","行"}}</li>
+ *   <li>{@code boolean isNoPath(String source, String dimension)} —— 指定数据源/维度是否 no_path;
+ *       期望 {@code isNoPath("gut_flora","富")==true}、{@code isNoPath("gut_flora","行")==true},
+ *       {@code isNoPath("gut_flora","身")==false}(身维为主战场)、
+ *       {@code isNoPath("gut_flora","心")==false}(心走介导链非 no_path)</li>
+ *   <li>{@code List<String> mediationChain(String source, String dimension)} —— 心/智介导链阶梯表述;
+ *       期望非空、≥2 级,且每级措辞命中关联措辞白名单</li>
+ * </ul>
+ *
+ * <p><b>测试范围</b>:纯契约级单测,无 Spring / 无 DB / 无外部服务;直接实例化纯规则引擎
+ * 或反射调用契约方法(与同批 TeamProfitHookContractTest / TeamStarMultiplierContractTest 风格一致)。</p>
+ *
+ * <p><b>RED 预期</b>:契约方法 {@code noPathMap()}/{@code isNoPath()}/{@code mediationChain()}
+ * 未实现 → 用例在反射探测处失败(红);另 3 个真实引擎用例断言现有 {@code attribute()} 对
+ * 菌群读数的富/行扣分行为违反 no_path 契约 → 断言失败(红)。</p>
+ */
+class GuheNoPathMapContractTest {
+
+    /** 宿主契约类(已存在,ATT-S2-B1 合入;no_path 映射规则未落地)。 */
+    private static final String HOST_CLASS = "com.etotem.cfc.service.attribution.FiveDimAttributionEngine";
+
+    // ==================== 契约行为 1:菌群观测对「富/行」自动标 no_path ====================
+
+    @Test
+    void noPathMap_gutFlora_wealthAndActionMarked() {
+        Object host = contractHost();
+        Map<?, ?> map = callMap(host, "noPathMap");
+        assertNotNull(map, "noPathMap() 应返回数据源 → no_path 维度集合");
+        assertTrue(map.containsKey("gut_flora"), "noPathMap 应含 gut_flora 菌群数据源");
+        Object dims = map.get("gut_flora");
+        assertTrue(dims instanceof Set, "gut_flora 的 no_path 维度应为 Set");
+        Set<?> set = (Set<?>) dims;
+        assertTrue(set.contains("富"), "菌群观测对「富」应自动标 no_path(无直接归因路径)");
+        assertTrue(set.contains("行"), "菌群观测对「行」应自动标 no_path(无直接归因路径)");
+    }
+
+    @Test
+    void isNoPath_gutFloraWealth_true() {
+        Object host = contractHost();
+        assertTrue(callBoolean(host, "isNoPath", "gut_flora", "富"),
+                "菌群观测 → 富维应判定为 no_path");
+    }
+
+    @Test
+    void isNoPath_gutFloraAction_true() {
+        Object host = contractHost();
+        assertTrue(callBoolean(host, "isNoPath", "gut_flora", "行"),
+                "菌群观测 → 行维应判定为 no_path");
+    }
+
+    @Test
+    void isNoPath_gutFloraBody_false() {
+        Object host = contractHost();
+        assertFalse(callBoolean(host, "isNoPath", "gut_flora", "身"),
+                "身维是菌群(低侧口径)主战场,不应标 no_path");
+    }
+
+    @Test
+    void isNoPath_gutFloraMind_false() {
+        Object host = contractHost();
+        assertFalse(callBoolean(host, "isNoPath", "gut_flora", "心"),
+                "心维走介导链(非 no_path,措辞锁关联)");
+    }
+
+    // ==================== 契约行为 2:心/智介导链自动阶梯 + 锁关联措辞 ====================
+
+    @Test
+    void mediationChain_gutFloraMind_ladderLockedWording() {
+        Object host = contractHost();
+        List<String> ladder = callListString(host, "mediationChain", "gut_flora", "心");
+        assertNotNull(ladder, "介导链应返回阶梯表述列表");
+        assertTrue(ladder.size() >= 2, "心介导链应为阶梯式自动生成(≥2 级),实际 " + ladder.size());
+        for (String step : ladder) {
+            assertTrue(usesWhitelistWording(step),
+                    "介导链每级措辞必须锁定为关联措辞(白名单),实际: " + step);
+        }
+    }
+
+    @Test
+    void mediationChain_gutFloraWisdom_ladderLockedWording() {
+        Object host = contractHost();
+        List<String> ladder = callListString(host, "mediationChain", "gut_flora", "智");
+        assertNotNull(ladder, "介导链应返回阶梯表述列表");
+        assertTrue(ladder.size() >= 2, "智介导链应为阶梯式自动生成(≥2 级),实际 " + ladder.size());
+        for (String step : ladder) {
+            assertTrue(usesWhitelistWording(step),
+                    "介导链每级措辞必须锁定为关联措辞(白名单),实际: " + step);
+        }
+    }
+
+    // ==================== 契约行为 3:低侧口径归身维主战场(真实引擎违约断言) ====================
+
+    @Test
+    void attribute_gutFloraReading_wealthNoPenalty_red() {
+        // 菌群源读数(显示名命中「富」关键词「饮食」;注意读数必须不命中更靠前的身/心/智/行关键词);
+        // 契约要求按 source=gut_flora 对「富」标 no_path → 不按关键词归因扣分(保持基数)
+        MemberAttribution attr = new MemberAttribution();
+        attr.setMemberId(1L);
+        attr.setReadings(Collections.singletonList(reading("gut_flora", "外食饮食习惯观测",
+                IndicatorReading.STATUS_LOW, IndicatorReading.BAND_VERY_LOW)));
+        Map<String, List<String>> evidence = new LinkedHashMap<>();
+        evidence.put("富", Collections.singletonList("外食饮食习惯观测"));
+        attr.setDimensionEvidence(evidence);
+
+        FiveDimAttributionEngine.FiveDimResult result =
+                new FiveDimAttributionEngine().attribute(
+                        Collections.singletonList(attr), Collections.singletonList(profile(1L)));
+
+        Double wealth = result.attributions.get(0).getFiveDim().get("富");
+        double baseWealth = AttributionWeights.BASE_SCORE.getOrDefault("富", 78.0);
+        assertEquals(baseWealth, wealth, 1e-9,
+                "菌群观测对「富」维 no_path:异常菌群读数不应使富维扣分(保持基数 " + baseWealth + "),实际 " + wealth);
+    }
+
+    @Test
+    void attribute_gutFloraReading_actionNoPenalty_red() {
+        // 菌群源读数(显示名命中「行」关键词「运动」;不命中更靠前的身/心/智关键词);
+        // 契约要求按 source=gut_flora 对「行」标 no_path → 不按关键词归因扣分(保持基数)
+        MemberAttribution attr = new MemberAttribution();
+        attr.setMemberId(2L);
+        attr.setReadings(Collections.singletonList(reading("gut_flora", "运动作息习惯观测",
+                IndicatorReading.STATUS_LOW, IndicatorReading.BAND_VERY_LOW)));
+        Map<String, List<String>> evidence = new LinkedHashMap<>();
+        evidence.put("行", Collections.singletonList("运动作息习惯观测"));
+        attr.setDimensionEvidence(evidence);
+
+        FiveDimAttributionEngine.FiveDimResult result =
+                new FiveDimAttributionEngine().attribute(
+                        Collections.singletonList(attr), Collections.singletonList(profile(2L)));
+
+        Double action = result.attributions.get(0).getFiveDim().get("行");
+        double baseAction = AttributionWeights.BASE_SCORE.getOrDefault("行", 70.0);
+        assertEquals(baseAction, action, 1e-9,
+                "菌群观测对「行」维 no_path:异常菌群读数不应使行维扣分(保持基数 " + baseAction + "),实际 " + action);
+    }
+
+    @Test
+    void attribute_lowSideGutFlora_weakExcludesWealthAndAction_red() {
+        // 低侧口径归身维主战场:多条菌群低侧读数(身/行/富三路关键词各一)→
+        // no_path 契约下弱维(<阈值)只应在身维;现有关键词引擎会把行/富误打成弱维
+        MemberAttribution attr = new MemberAttribution();
+        attr.setMemberId(3L);
+        attr.setReadings(Arrays.asList(
+                reading("gut_flora", "大肠杆菌偏低", IndicatorReading.STATUS_LOW, IndicatorReading.BAND_VERY_LOW),
+                reading("gut_flora", "运动作息习惯观测", IndicatorReading.STATUS_LOW, IndicatorReading.BAND_VERY_LOW),
+                reading("gut_flora", "外食饮食习惯观测", IndicatorReading.STATUS_LOW, IndicatorReading.BAND_VERY_LOW)));
+        Map<String, List<String>> evidence = new LinkedHashMap<>();
+        evidence.put("身", Collections.singletonList("大肠杆菌偏低"));
+        evidence.put("行", Collections.singletonList("运动作息习惯观测"));
+        evidence.put("富", Collections.singletonList("外食饮食习惯观测"));
+        attr.setDimensionEvidence(evidence);
+
+        FiveDimAttributionEngine.FiveDimResult result =
+                new FiveDimAttributionEngine().attribute(
+                        Collections.singletonList(attr), Collections.singletonList(profile(3L)));
+
+        List<String> weak = result.attributions.get(0).getWeakDimensions();
+        assertNotNull(weak, "应输出五维短板标签");
+        assertTrue(weak.contains("身"), "低侧菌群判读主战场应为身维,weak=" + weak);
+        assertFalse(weak.contains("富"), "非身维遮挡:菌群低侧读数不应使「富」成弱维(no_path),weak=" + weak);
+        assertFalse(weak.contains("行"), "非身维遮挡:菌群低侧读数不应使「行」成弱维(no_path),weak=" + weak);
+    }
+
+    // ==================== 反射辅助(锚定宿主契约方法,避免编译期依赖不存在方法) ====================
+
+    /** 措辞是否命中关联措辞白名单(G-1 关联非因果口径)。 */
+    private static boolean usesWhitelistWording(String text) {
+        if (text == null) {
+            return false;
+        }
+        for (String wl : AttributionLowSideRules.WORDING_WHITELIST) {
+            if (text.contains(wl)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static IndicatorReading reading(String source, String displayName, String status, String band) {
+        IndicatorReading r = new IndicatorReading();
+        r.setSource(source);
+        r.setCode(displayName);
+        r.setDisplayName(displayName);
+        r.setStatus(status);
+        r.setBand(band);
+        return r;
+    }
+
+    private static MemberProfile profile(long memberId) {
+        MemberProfile p = new MemberProfile();
+        p.setMemberId(memberId);
+        p.setName("成员" + memberId);
+        p.setRole("mom");
+        return p;
+    }
+
+    private Object contractHost() {
+        try {
+            Class<?> clazz = Class.forName(HOST_CLASS);
+            return clazz.getDeclaredConstructor().newInstance();
+        } catch (ClassNotFoundException e) {
+            fail("RED[guhe.l4.noPathMap] 契约宿主 FiveDimAttributionEngine 未实现(实现未落地):"
+                    + e.getMessage());
+            throw new IllegalStateException("unreachable");
+        } catch (Exception e) {
+            fail("契约宿主 FiveDimAttributionEngine 实例化失败:" + e.getMessage());
+            throw new IllegalStateException("unreachable");
+        }
+    }
+
+    /** 按方法名 + 参数个数匹配并调用,契约方法缺失即断言失败(红)。 */
+    private Object call(Object host, String method, Object... args) {
+        for (Method m : host.getClass().getMethods()) {
+            if (m.getName().equals(method) && m.getParameterCount() == args.length) {
+                try {
+                    return m.invoke(host, args);
+                } catch (Exception e) {
+                    fail("契约方法 " + HOST_CLASS + "#" + method + " 调用失败:" + e.getMessage());
+                    throw new IllegalStateException("unreachable");
+                }
+            }
+        }
+        fail("RED[guhe.l4.noPathMap] 契约方法 " + HOST_CLASS + "#"
+                + method + " 未实现(T14 五维 no_path 映射未落地)");
+        throw new IllegalStateException("unreachable");
+    }
+
+    private boolean callBoolean(Object host, String method, Object... args) {
+        Object r = call(host, method, args);
+        if (r instanceof Boolean) {
+            return (Boolean) r;
+        }
+        fail("契约方法 " + method + " 应返回布尔,实际 " + r);
+        throw new IllegalStateException("unreachable");
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<?, ?> callMap(Object host, String method) {
+        Object r = call(host, method);
+        if (r instanceof Map) {
+            return (Map<?, ?>) r;
+        }
+        fail("契约方法 " + method + " 应返回 Map,实际 " + r);
+        throw new IllegalStateException("unreachable");
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<String> callListString(Object host, String method, Object... args) {
+        Object r = call(host, method, args);
+        if (r instanceof List) {
+            return (List<String>) r;
+        }
+        fail("契约方法 " + method + " 应返回 List<String>,实际 " + r);
+        throw new IllegalStateException("unreachable");
+    }
+}

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


+ 140 - 0
docs/tdd/orchestration/report-dev-2026-11-09-0015.md

@@ -0,0 +1,140 @@
+# 研发负责人工作汇报(2026-11-09 0015 轮 · TDD 闭环巡检 · 错峰03)
+
+> 汇报人:研发负责人(本自动化 错峰03,经 tdd-orchestrator + git-workflow 技能)|汇报周期:2026-11-09 0015 轮(时间戳唯一标识)|负责模块:TDD 编排全域(研发负责人职责:检查 / 调度 / 汇总 / 裁决,不代写实现)
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **收到的测试反馈问题数*** | 0 个(本实例) | 本环境只读核查;远端测试团队 R94(00:30)已闭环 B-79(D/X 桥接假绿自处置)/ B-61(健康鉴权 14 轮绿),新增 B-77/B-78/B-80,详见 §三 |
+| **已解决问题数*** | 0 个(本实例) | 本实例不代写实现;RED→YELLOW→GREEN 收口由 hub 派发会话 + 测试团队执行,证据以 git 真理为准 |
+| **待解决研发问题数** | 4 项 | ① 17 RED(guhe_v2×15 + diet×2)待 hub 派发产出(P0);② B-77 251 chat 挂起(P0 新增,研发修复);③ B-65 连续多轮未修(P1);④ B-75 签名横屏 SRS 实现已落地 adb032a61(R94 首验,待测试复验) |
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **分派任务数*** | 0 个(本实例) | 本实例派发通道断(B-42/B-56),派发由独立 hub 自动化执行;hub status 显示 lan-189 已达上限(4/4)、prod-etotem 1 在跑 |
+| **恢复智能体会话数*** | 0 个(本实例) | 会话恢复属 hub 职责(fix_free_limit 自愈) |
+| **当前在用会话数** | 5(hub 管理) | hub 实测运行中:lan-189 4 / prod-etotem 1;其余节点(local-45278/lan-248/lan-204/pub-iwintrue)当前 0 在跑 |
+| **会话平均利用率** | 中 | 20:27 fill_slots 手动补派 15 个 guhe RED(全部派发成功)后,guhe-v2 会话 4h 无产出提交,疑 flash-free 黑窗家族空转,已列 O-18 裁决重派 |
+
+### 1.2b 各会话当前工作状态列表(必填)
+
+| 会话标签 | 角色隔离 | 当前状态 | 当前任务 | 工作目录 | 备注 |
+|----------|----------|----------|----------|----------|------|
+| local-45278 | 本机开发首选 | idle(0 在跑) | — | 不干净(主工作树 WIP,不代提交) | 主工作树 312 处他人 WIP,纪律④只读 |
+| lan-248 | 远程测试 | idle(0 在跑) | — | 不干净 | 测试侧契约单测职责;20:27 guhe 补派后无在跑 |
+| lan-204 | 远程开发 | idle(0 在跑) | — | 不干净(D:/cfc) | 20:27 guhe T9/T12/T13/T14 派发后 4h 无产出 |
+| lan-189 | 远程开发 | running 4/4(已达上限) | guhe_v2 补派批次 | 不干净 | 上限饱和;B-56 通道根因已定位(缺 LLM 凭证)+ 修复 736d7f9a1 |
+| pub-iwintrue | 云端 testing 部署 | idle(0 在跑) | — | 不干净(/sc-data/cfc) | deploy_sessions.testing 节点 |
+| prod-etotem | 生产部署(仅研发负责人) | running 1 | — | 不干净(/root/cfc) | confirm_required,本实例未触发 |
+
+> 结论:**lan-189 饱和、prod-etotem 有 1 在跑,其余节点当前 0 在跑**——20:27 补派的 guhe-v2 会话已超 4h 无产出(疑空转),属 O-18 ③ 范畴,下轮按裁决重派;无「会话空闲且无任务可派」的研发浪费(idle 为角色隔离或补派后无产出待回收)。
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **感知需求/设计变更数*** | 200 提交 / 273+ 文件(7d 窗口) | `--changes --days 7` 检出,全部为既有键内容变化 |
+| **新增入池任务数*** | 0 个(本轮) | **0 新增能力键**;guhe_v2×15 + diet×2 RED 已于 18:16 轮入池(`ac7116d4f`,含溯源锚点),本轮无需重复入池 |
+| **既有键状态流转数** | 0 次(本实例) | 流转由 hub 派发会话 + 测试团队推进,本实例只读跟踪 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+> 本实例不提交业务代码;本轮入库仅 docs 裁决 + 汇报(authentic 临时索引法,绕过主工作树脏索引)。
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| `ac7116d4f` | guhe_v2 T1–T15 + diet×2 入池 | 契约登记 | 文档/backlog | cfclub | 是 | 17 RED 带溯源锚点 |
+| `adb032a61` | 签名横屏 SRS 实现(B-75) | 实现 | src/main | cfclub | 是 | R94 运行窗口内合入,7 例用例落地 |
+| `b6f95c1f5` | 测试负责人 R94 汇报 | 测试证据 | 文档 | cfclub | 是 | 后端 2664 例 98.01% 首测 |
+| `4faa8b2b1` | origin/cfclub HEAD(最新 bump) | — | — | cfclub | 是 | 本轮基准父提交 |
+
+---
+
+## 二、下一步事项(研发侧)
+
+| 序号 | 事项 | 依赖 | 预估完成 | 优先级 | 状态 |
+|------|------|------|----------|--------|------|
+| 1 | 17 RED(guhe_v2×15 + diet×2)实现经 hub 派发产出回填 | 189 通道恢复 + 模型 fallback 链(三档) | hub 滚动派发 | P0 | 进行中(O-18 裁决重派) |
+| 2 | B-77 251 chat 挂起:定位 chat graph 加 timeout + 熔断 + 降级文案 | 研发定位 + 运维复核 LLM 凭证连通 | 研发排期 | P0 | 待研发(R94 新增) |
+| 3 | 27 YELLOW 键待测试侧 mark-green 复验收口 | 测试侧复验(yellow_sha 全部 IN CHAIN) | 测试侧排期 | P1 | 待测试侧(非研发职责) |
+| 4 | B-65 连续多轮未修(精确拆分后待研发) | 测试侧精确拆分 B-65 | 研发排期 | P1 | 待研发 |
+| 5 | B-75 签名横屏 SRS 实现复验(adb032a61) | 测试侧复验 | 下轮 | P1 | 待测试侧 |
+| 6 | G11 CI 未接后端 `mvn -o test` | 研发/运维 | 排期 | P1 | 待协调 |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| B-77 251 chat 挂起(R94 新增 P0) | AI 管家线上核心链路不可用 | 研发 | chat 无响应 0 字节;待定位 LLM/检索未设超时 | 研发修复 + 运维复核 LLM 凭证 |
+| B-56 / B-42 派发通道(本环境直连断) | 本实例无法直接派发 opencode | 测试/运维 | 189 根因定位(缺 LLM 凭证)+ 客户端修复 736d7f9a1;hub 仍经独立通道派发 | 运维确认 189/204/251 节点凭据 |
+| B-78 251 无 cfc-langgraph 源码 | G10 派发永不达成 | 运维 | 251 仅 memory/ 无 tests/ | 运维落盘 repo checkout + venv |
+| B-65 连续多轮未修 | 对应能力键 YELLOW→GREEN 延迟 | 研发 | 测试侧已精确拆分;待研发排期 | 研发排期 |
+| B-75 签名横屏 | 签名横屏 SC 跑通 | 研发 | 实现已落地 adb032a61;待测试复验 | 测试复验 |
+| B-80 reset-db 默认凭据对 204 鉴权失败 | 默认 E2E 配置必然中断 | 运维/装置 | 测试侧已定位 MYSQL_PASS 默认值 | 运维授库权限 + 加 SKIP_DB_RESET 开关 |
+| 部署账本 pending 18(commit=None 旧格式)+ last_deployed 顶层字段丢失 | 部署验证口径 | 编排侧 | 69 轮已记录;待部署侧回填 | 部署侧 |
+| 主工作树 312 处他人 WIP | 工作目录门禁(--clean-check) | 研发 | 纪律④只读列示,归产生会话收口 | 产生会话/主理人协调 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 核验 guhe_v2×15 + diet×2 派发产出并回填 yellow_sha | 下轮 | O-18 重派后 hub 生成实现提交 | hub / cfc-test | 产出已推远端则 backfill;零产出按 O-18③ 回收重派 |
+| 监控 B-77 251 chat 修复 + B-75 签名横屏复验 | 滚动 | 研发/测试 | — | 只读跟踪 |
+| 监控 27 YELLOW 测试侧 mark-green 进度 | 滚动 | 测试侧复验 | — | 只读跟踪 |
+| 部署 pending 18 收口 + last_deployed 回填 | 下轮 | 部署侧 | pub-iwintrue | 归部署侧 |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+本轮本实例 0 个(不直接读取测试台账);远端 R94(00:30)闭环 B-79(D/X 桥接假绿,测试侧自处置)+ B-61(14 轮绿),新增 B-77(P0 chat 挂起)/B-78(P1 无源码)/B-80(P2 凭据),详见 §三。
+
+### 附录 B:任务分派与会话恢复明细
+本轮本实例 0 派发 / 0 恢复(通道断,派发归 hub)。hub 实测运行中 5(lan-189 4 达上限 / prod-etotem 1);20:27 fill_slots 手动补派 15 guhe(全部成功),任务池当前「待派发 0 / 执行中 23+ / 已完成 114」。
+
+### 附录 C:新增入池任务明细
+本轮 0 个(0 新增能力键)。既有 RED 17 = guhe_v2×15(溯源 `docs/superpowers/plans/2026-11-08-浠艾福平台程序修改计划.md`)+ diet×2(溯源 `docs/superpowers/specs/2026-11-08-diet-recommendation-polish`),均带 `--requirement`+`--design`。
+
+### 附录 D:变更感知明细(来自 `--changes --days 7`)
+200 提交 / 273+ 文件(设计 / 用例 / 其他);0 新增能力键;变化集中在既有键内容演进(含签名横屏 SRS 实现 adb032a61、D/X 桥接契约拍板版)。
+
+---
+
+## ★ 主理人裁决(本轮唯一待裁决项 O-18 已裁决)
+
+### O-18 · 派发自动链断裂与任务滞留 → 研发负责人裁决意见(待主理人最终确认)
+
+**核实结论**(本轮三重取证):
+- **① 自动巡检链断裂确认**:`schtasks` 查询无 XAFHubCheck/fill_slots 计划任务(`setup_scheduled_tasks.py` 未注册/失效);20:27 手动 `fill_slots.py` 补位,15 个 guhe RED 全部派发成功(fill_slots.log 20:28:23,任务池待派发 0)。**当前无积压,但持续性风险**——新任务入池后无人自动派发。
+- **② diet 2 键(uiRework/participantSelect)**:RED 已入 TDD 状态机(S-cfc-79 契约 2438bb283/fb1e335e9),hub 池无对应 DOC 任务;TDD 直连派发通道 B-42/B-56 断。
+- **③ 8 僵尸 dispatched**:EVD-8(11-07 07:00,41h+)/ referral-T1/T2/T3/T10(11-08 04:54-55,19h+)/ T5/T6/T8(11-08 10:46,13h+),会话均不在运行(hub 运行中仅 5);guhe-v2 15 个补派会话 4h 无产出提交(疑 flash-free 黑窗家族空转)。
+
+**裁决意见**:
+1. **诉求①(自动巡检链)**:由运维侧注册 XAFHubCheck 计划任务(每 30 分钟 `fill_slots.py` 兜底)或恢复常驻 watchdog;过渡期各链路人工 fill_slots 兜底。属运维/基础设施动作,非 TDD 池内键。
+2. **诉求②(diet 2 键)**:走 **hub 建 DOC 任务入池**(正常派发链),B-42/B-56 直连通道修复归脚本/运维通道,不阻塞入池。
+3. **诉求③(8 僵尸)**:**全部重派**——清脏会话后按 hub 正常通道重派(模型链三档:opencode flash-free 首,黑窗时自动落 axonhub/deepseek);重派后 1 轮仍零产出 → 标记 failed 回收,对应实现走 TDD GREEN 任务通道(S-cfc-60/68/70/71 已在 TDD 账本),EVD-8(测试侧 43 SC 三类流证据)转测试侧 TR 重派。
+
+> 本裁决为研发负责人裁决意见,待主理人最终确认后生效。
+
+---
+
+## 顶层目标进度(DEV_GOAL:241 SC 全跑通)
+
+- **SC 跑通**:15/241(权威台账 `sc-key-map.md`,O-3/O-8 收口),本轮无新增。
+- **实现真相**:66 跟踪键 = DONE14 + GREEN8 + YELLOW27 + RED17(guhe_v2/diet 新功能,设计驱动、已溯源入池)。
+- **剩余 226 SC**:O-7 契约补建判定——端点已 100% 实现(66 SC 实测 0 缺失),缺口为测试侧三类流执行证据;由测试团队按批次补证。
+- **结论**:研发侧「实现」主体已完成,闭环瓶颈在 ① 17 RED 新功能派发产出(hub 进行中,O-18 裁决重派)② 27 YELLOW 测试复验 ③ 226 SC 测试证据补建(O-7);本实例按职责确保任务入池 + 会话由 hub 持续派发 + 阻塞 relay + 裁决 O-18,未改业务代码、未动他人 WIP、未用 --auto/--apply。
+
+**模板版本**:v1.1 | **适用对象**:研发负责人 | **记录轮次**:2026-11-09 0015(时间戳唯一)

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