Przeglądaj źródła

test(S-cfc-95): guhe.lowSide.ruleLoad 低侧判读规则入库 RED 契约单测 (T3/L4-1,L4-2)

契约源:2026-11-08 浠艾福平台程序修改计划 §三 T3(P0)
- 正常流:AttributionLowSideRules 应入库 四机制/RULE_U_SHAPE/RULE_REF_LOWER_ZERO 规则码 +
  判读铁律 INTERPRET_LAW_LOW_SIDE(关联非因果)+ U 型口径 U_SHAPE_CALIBER_HINT +
  规则加载入口 lowSideInterpretRules()
- 边界流:IndicatorInterpretEngine 对偏低大肠杆菌(参考下限 0)应按四机制判读、
  全部标关联非因果、套 U 型双向口径(越低越好不成立、参考下限,不标偏低超标)
RED 验证:10 run / 9 fail / 0 error(仅权限流说明性用例通过)
Sisyphus Agent 4 dni temu
rodzic
commit
1480a8ca60

+ 336 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheLowSideRuleLoadContractTest.java

@@ -0,0 +1,336 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.constant.AttributionLowSideRules;
+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;
+
+/**
+ * 契约级单测:低侧判读规则入库(能力键 {@code guhe.lowSide.ruleLoad},单元 guhe.lowSide)。
+ *
+ * <p>契约来源(2026-11-08《浠艾福平台程序修改计划(guhe_v2 兼容 · 知识库低侧口径 · 专家同步)》§三任务分解 T3,P0):
+ * <blockquote>
+ *   T3 低侧判读规则入库(L4-1/L4-2):<b>条件致病菌偏低侧按四机制解释,全部标「关联非因果」</b>;
+ *   <b>U 型双向口径</b>;<b>参考下限 0 的菌不标「偏低超标」</b>。
+ *   <br>验收:任意「偏低」归因节点 {@code association_only==true};存量无「缺大肠杆菌→补菌」措辞;
+ *   低侧机制证据全部标 PUB/KB。
+ * </blockquote></p>
+ *
+ * <p><b>契约语义</b>:本能力键不是「合规门禁事后回扫」(那是 T5 G-1 红线组),而是 <b>低侧判读规则
+ * 在 L4 规则层「入库」并为 L4 归因引擎所加载应用</b> —— 依据知识库新增子键
+ * {@code 条件致病菌低侧判读补充(人工)}(§2.2:判读铁律 U 型双向 / 参考下限 0 / 关联非因果 +
+ * 大肠杆菌偏低侧四机制),L4 引擎对偏低条件致病菌读数:
+ * <ul>
+ *   <li>按<b>四机制</b>解释,全部标「关联非因果」(L4-1);</li>
+ *   <li>套用 <b>U 型双向口径</b>:越低越好<b>不成立</b>(L4-2);</li>
+ *   <li>参考下限 <b>0</b> 的菌<b>不标「偏低超标」</b>(L4-2,{@code ND} 只是低于检出丰度)。</li>
+ * </ul></p>
+ *
+ * <p><b>实现现状(C 档探测,src/main 现状核实,2026-11-08)</b>:L4 引擎已存在且为纯 Java 确定性规则
+ * ({@code IndicatorInterpretEngine} 解读 + {@code AttributionComplianceGate} 门禁);已落地的是
+ * <b>T5 G-1 低侧红线组</b>({@code association_only} / {@code no_causal_bridge} /
+ * {@code no_prescription} 三规则码 + 措辞白名单,全部在 {@code AttributionLowSideRules})。
+ * <b>T3 低侧判读规则(L4-1/L4-2)未落地</b>:{@code AttributionLowSideRules} 无「四机制判读」/「U 型
+ * 双向口径」/「参考下限 0」三规则码、无「判读铁律」与「U 型口径」常量、无规则加载入口
+ * {@code lowSideInterpretRules()};{@code IndicatorInterpretEngine} 对偏低条件致病菌仅产通用
+ * 「先医后补」含义({@code buildMeaning} 身维兜底),未按四机制判读、未标「关联非因果」、未套
+ * U 型双向口径。故本测试以「反射结构 + 纯规则行为」双通道探测实现缺失:能编译、能运行、断言
+ * 失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 {@code AttributionLowSideRules} 增加
+ * {@code RULE_FOUR_MECHANISM="four_mechanism"} / {@code RULE_U_SHAPE="u_shape"} /
+ * {@code RULE_REF_LOWER_ZERO="ref_lower_zero"} 三规则码、{@code INTERPRET_LAW_LOW_SIDE}(含
+ * 「关联非因果」)与 {@code U_SHAPE_CALIBER_HINT}(含「越低越好不成立」与「参考下限」)常量、
+ * 规则加载入口 {@code lowSideInterpretRules()}(返回上述规则码),并在
+ * {@code IndicatorInterpretEngine} 的低侧(偏低条件致病菌)解读路径注入四机制 + 关联非因果 +
+ * U 型双向口径后,本测试自动转绿。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为后端内部 L4 低侧判读规则,无用户权限维度,权限流不适用,以说明性用例标注。</p>
+ */
+class GuheLowSideRuleLoadContractTest {
+
+    /** T3(L4-1)期望的四机制判读规则码 */
+    private static final String RULE_FOUR_MECHANISM = "four_mechanism";
+
+    /** T3(L4-2)期望的 U 型双向口径规则码 */
+    private static final String RULE_U_SHAPE = "u_shape";
+
+    /** T3(L4-2)期望的参考下限 0 规则码 */
+    private static final String RULE_REF_LOWER_ZERO = "ref_lower_zero";
+
+    /** T3 期望的「判读铁律」常量名(含关联非因果) */
+    private static final String INTERPRET_LAW_FIELD = "INTERPRET_LAW_LOW_SIDE";
+
+    /** T3 期望的「U 型双向口径」常量名(含越低越好不成立 / 参考下限) */
+    private static final String U_SHAPE_HINT_FIELD = "U_SHAPE_CALIBER_HINT";
+
+    /** T3 期望的规则加载入口方法名(低侧判读规则入库后供 L4 引擎加载) */
+    private static final String RULE_LOAD_METHOD = "lowSideInterpretRules";
+
+    // ------------------------------------------------------------------
+    // 正常流:规则层应暴露并入库低侧判读规则(T3,L4-1/L4-2)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:AttributionLowSideRules 应暴露「四机制判读」规则码")
+    void lowSideRules_exposesFourMechanismRuleCode() {
+        Field f = declaredField(AttributionLowSideRules.class, "RULE_FOUR_MECHANISM");
+        assertNotNull(f,
+                "[guhe.lowSide.ruleLoad] AttributionLowSideRules 缺少 RULE_FOUR_MECHANISM 规则码:"
+                        + "T3(L4-1)要求条件致病菌偏低侧按「四机制」解释(§2.2 大肠杆菌偏低侧四机制),"
+                        + "当前只剩 T5 门禁规则,未落地低侧判读规则入库。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.ruleLoad] RULE_FOUR_MECHANISM 常量值不应为 null。");
+            assertTrue(RULE_FOUR_MECHANISM.equals(String.valueOf(v)),
+                    "[guhe.lowSide.ruleLoad] RULE_FOUR_MECHANISM 应取值为 \"" + RULE_FOUR_MECHANISM
+                            + "\",实际为 \"" + v + "\"。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.ruleLoad] 读取 RULE_FOUR_MECHANISM 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-2:AttributionLowSideRules 应暴露「U 型双向口径」规则码")
+    void lowSideRules_exposesUShapeRuleCode() {
+        Field f = declaredField(AttributionLowSideRules.class, "RULE_U_SHAPE");
+        assertNotNull(f,
+                "[guhe.lowSide.ruleLoad] AttributionLowSideRules 缺少 RULE_U_SHAPE 规则码:"
+                        + "T3(L4-2)要求 U 型双向口径——条件致病菌「越低越好」不成立,"
+                        + "当前仅剩 T5 门禁规则的 association_only,未落地 U 型判读规则。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.ruleLoad] RULE_U_SHAPE 常量值不应为 null。");
+            assertTrue(RULE_U_SHAPE.equals(String.valueOf(v)),
+                    "[guhe.lowSide.ruleLoad] RULE_U_SHAPE 应取值为 \"" + RULE_U_SHAPE
+                            + "\",实际为 \"" + v + "\"。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.ruleLoad] 读取 RULE_U_SHAPE 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-3:AttributionLowSideRules 应暴露「参考下限 0」规则码")
+    void lowSideRules_exposesRefLowerZeroRuleCode() {
+        Field f = declaredField(AttributionLowSideRules.class, "RULE_REF_LOWER_ZERO");
+        assertNotNull(f,
+                "[guhe.lowSide.ruleLoad] AttributionLowSideRules 缺少 RULE_REF_LOWER_ZERO 规则码:"
+                        + "T3(L4-2)要求参考下限 0 的菌(ND 只是低于检出丰度)不标「偏低超标」,"
+                        + "当前无此判读规则。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.ruleLoad] RULE_REF_LOWER_ZERO 常量值不应为 null。");
+            assertTrue(RULE_REF_LOWER_ZERO.equals(String.valueOf(v)),
+                    "[guhe.lowSide.ruleLoad] RULE_REF_LOWER_ZERO 应取值为 \"" + RULE_REF_LOWER_ZERO
+                            + "\",实际为 \"" + v + "\"。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.ruleLoad] 读取 RULE_REF_LOWER_ZERO 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-4:规则层应提供低侧判读规则加载入口 lowSideInterpretRules()")
+    void lowSideRules_ruleLoadEntry_registeredRuleCodes() {
+        Method m = declaredMethod(AttributionLowSideRules.class, RULE_LOAD_METHOD, 0);
+        assertNotNull(m,
+                "[guhe.lowSide.ruleLoad] AttributionLowSideRules 缺少规则加载入口 " + RULE_LOAD_METHOD
+                        + "():T3「低侧判读规则入库」要求规则集可被 L4 引擎加载(四机制 / U 型 / 参考下限 0),"
+                        + "当前仅 T5 门禁规则,无低侧判读规则加载入口。");
+        try {
+            Object rules = m.invoke(null);
+            assertTrue(rules instanceof Iterable,
+                    "[guhe.lowSide.ruleLoad] " + RULE_LOAD_METHOD + "() 应返回规则码集合,实际 "
+                            + (rules == null ? "null" : rules.getClass().getName()));
+            assertTrue(containsAny((Iterable<?>) rules, RULE_FOUR_MECHANISM),
+                    "[guhe.lowSide.ruleLoad] 低侧判读规则集应含四机制规则码 " + RULE_FOUR_MECHANISM);
+            assertTrue(containsAny((Iterable<?>) rules, RULE_U_SHAPE),
+                    "[guhe.lowSide.ruleLoad] 低侧判读规则集应含 U 型双向口径规则码 " + RULE_U_SHAPE);
+            assertTrue(containsAny((Iterable<?>) rules, RULE_REF_LOWER_ZERO),
+                    "[guhe.lowSide.ruleLoad] 低侧判读规则集应含参考下限 0 规则码 " + RULE_REF_LOWER_ZERO);
+        } catch (Exception e) {
+            assertTrue(false, "[guhe.lowSide.ruleLoad] 调用 " + RULE_LOAD_METHOD + "() 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-5:AttributionLowSideRules 应暴露「判读铁律」口径常量(含关联非因果)")
+    void lowSideRules_exposesInterpretLawCaliber() {
+        Field f = declaredField(AttributionLowSideRules.class, INTERPRET_LAW_FIELD);
+        assertNotNull(f,
+                "[guhe.lowSide.ruleLoad] AttributionLowSideRules 缺少 " + INTERPRET_LAW_FIELD
+                        + " 判读铁律常量:T3(L4-1)要求低侧判读规则全部标「关联非因果」(§2.2 判读铁律),"
+                        + "供 L4 引擎取用,而非仅门禁可回扫出违规。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.ruleLoad] " + INTERPRET_LAW_FIELD + " 常量值不应为 null。");
+            String text = String.valueOf(v);
+            assertTrue(text.contains("关联非因果"),
+                    "[guhe.lowSide.ruleLoad] " + INTERPRET_LAW_FIELD
+                            + " 应含「关联非因果」低侧判读铁律(L4-1 全部按关联非因果解释),实际为:「"
+                            + text + "」。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.ruleLoad] 读取 " + INTERPRET_LAW_FIELD + " 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-6:AttributionLowSideRules 应暴露「U 型双向口径」常量(越低越好不成立 / 参考下限 0)")
+    void lowSideRules_exposesUShapeCaliberHint() {
+        Field f = declaredField(AttributionLowSideRules.class, U_SHAPE_HINT_FIELD);
+        assertNotNull(f,
+                "[guhe.lowSide.ruleLoad] AttributionLowSideRules 缺少 " + U_SHAPE_HINT_FIELD
+                        + " 口径常量:T3(L4-2)要求 U 型双向口径——越低越好不成立、参考下限 0 的菌不标"
+                        + "「偏低超标」(ND 只是低于检出丰度)。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.ruleLoad] " + U_SHAPE_HINT_FIELD + " 常量值不应为 null。");
+            String text = String.valueOf(v);
+            assertTrue(text.contains("越低越好不成立"),
+                    "[guhe.lowSide.ruleLoad] " + U_SHAPE_HINT_FIELD
+                            + " 应含「越低越好不成立」U 型口径(§2.3 专家口径①),实际为:「" + text + "」。");
+            assertTrue(text.contains("参考下限"),
+                    "[guhe.lowSide.ruleLoad] " + U_SHAPE_HINT_FIELD
+                            + " 应含「参考下限 0」口径(§2.3 专家口径②),实际为:「" + text + "」。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.ruleLoad] 读取 " + U_SHAPE_HINT_FIELD + " 失败:" + e.getMessage());
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:L4 引擎应加载并应用已入库的低侧判读规则(T3 L4-1/L4-2)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:偏低大肠杆菌解读含义应携带「关联非因果」低侧判读口径")
+    void interpret_lowSideEColi_meaningCarriesAssociationNonCausal() {
+        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.ruleLoad] interpret 不应返回 null。");
+        assertFalse(attrs.isEmpty(), "[guhe.lowSide.ruleLoad] interpret 应产出至少 1 人归因。");
+        String meaning = attrs.get(0).getReadings().get(0).getMeaning();
+        assertNotNull(meaning, "[guhe.lowSide.ruleLoad] 偏低大肠杆菌节点应产出干预含义。");
+        assertTrue(meaning.contains("关联非因果"),
+                "[guhe.lowSide.ruleLoad] 偏低大肠杆菌解读含义应按四机制判读并标「关联非因果」(L4-1),"
+                        + "实际为:「" + meaning + "」。" + "当前引擎仅产通用「先医后补」兜底,未落地 T3 低侧判读规则。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:偏低大肠杆菌解读含义应携带「越低越好不成立」U 型双向口径")
+    void interpret_lowSideEColi_meaningCarriesUShapeCaliber() {
+        IndicatorReading reading = lowSideEColi();
+        MemberProfile member = profile();
+        IndicatorInterpretEngine engine = new IndicatorInterpretEngine();
+
+        List<MemberAttribution> attrs = engine.interpret(Collections.singletonList(reading),
+                Collections.singletonList(member));
+        String meaning = attrs.get(0).getReadings().get(0).getMeaning();
+        assertNotNull(meaning, "[guhe.lowSide.ruleLoad] 偏低大肠杆菌节点应产出干预含义。");
+        assertTrue(meaning.contains("越低越好不成立"),
+                "[guhe.lowSide.ruleLoad] 偏低大肠杆菌解读含义应套用 U 型双向口径「越低越好不成立」"
+                        + "(L4-2,条件致病菌并非越少越好),实际为:「" + meaning + "」。"
+                        + "当前引擎仅产通用「先医后补」兜底,未落地 T3 低侧判读规则。");
+    }
+
+    @Test
+    @DisplayName("边界流-3:参考下限 0 的菌不应标「偏低超标」——解读应携带参考下限口径")
+    void interpret_lowSideEColi_refLowerZero_notFlaggedLowOverThreshold() {
+        IndicatorReading reading = lowSideEColi();
+        MemberProfile member = profile();
+        IndicatorInterpretEngine engine = new IndicatorInterpretEngine();
+
+        List<MemberAttribution> attrs = engine.interpret(Collections.singletonList(reading),
+                Collections.singletonList(member));
+        String meaning = attrs.get(0).getReadings().get(0).getMeaning();
+        assertNotNull(meaning, "[guhe.lowSide.ruleLoad] 偏低大肠杆菌节点应产出干预含义。");
+        assertTrue(meaning.contains("参考下限"),
+                "[guhe.lowSide.ruleLoad] 参考下限 0 的菌解读应携带「参考下限 0」口径(L4-2:ND 只是低于"
+                        + "检出丰度,不标「偏低超标」、不强化越低越好),实际为:「" + meaning + "」。"
+                        + "当前引擎仅产通用「先医后补」兜底,未落地 T3 低侧判读规则。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):后端内部 L4 低侧判读规则,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为后端内部 L4 低侧判读规则,无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「L4 归因引擎 → 低侧判读规则入库/加载」内部规则,无权限校验维度。
+        assertTrue(true, "权限流不适用:guhe.lowSide.ruleLoad 为后端内部 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;
+    }
+
+    private static Method declaredMethod(Class<?> clazz, String name, int paramCount) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name) && m.getParameterCount() == paramCount) {
+                m.setAccessible(true);
+                return m;
+            }
+        }
+        return null;
+    }
+
+    private static boolean containsAny(Iterable<?> iterable, String value) {
+        if (iterable == null) {
+            return false;
+        }
+        for (Object o : iterable) {
+            if (o != null && value.equals(String.valueOf(o))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** 偏低条件致病菌读数(大肠杆菌,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;
+    }
+}