Ver código fonte

feat(guhe.gate.evidenceIsolate): S-cfc-91 G-3 证据码隔离(T11/P1)— AttributionLowSideRules 增 RULE_EVIDENCE_ISOLATE + PUB/KB/COM/未核实 证据码常量与 G-3 分组码;AttributionComplianceGate 增 tagEvidence / canEnterRecommendationBasis / isExistenceRecordOnly / isolateForRecommendation(PUB/KB 进推荐依据、COM 仅存在性记录、未核实编号剔除);GuheEvidenceIsolateContractTest 12/12 绿(含 call() 变长 null 实参归一化修复)

Sisyphus 3 dias atrás
pai
commit
14f238ac6

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/constant/AttributionLowSideRules.java

@@ -30,6 +30,35 @@ public final class AttributionLowSideRules {
     public static final String RULE_NO_CAUSAL_BRIDGE = "no_causal_bridge";
     public static final String RULE_NO_PRESCRIPTION = "no_prescription";
 
+    // ==================== G-3 证据码隔离(合规门禁·横切) ====================
+    //
+    // 对应平台修改计划 §三任务分解 T11(P1):证据码隔离——低侧机制证据标 PUB/KB;
+    // COM 只作存在性记录;未核实编号标「未核实」。§一 合规门禁(横切)③:COM 不进推荐依据。
+    //
+    // 证据码口径(决定证据能否进推荐依据):
+    //   PUB / KB → 可进推荐依据(低侧机制证据必须标 PUB/KB);
+    //   COM      → 仅作存在性记录,绝不进推荐依据;
+    //   未核实   → 未核实的编号(空/null/未知编号如 COM-870)统一标此值,不进推荐依据。
+
+    /** G-3 门禁分组码(证据码隔离组,与 G-1 低侧红线组并列) */
+    public static final String GROUP_CODE_G3 = "G-3";
+    public static final String GROUP_TITLE_G3 = "证据码隔离组(证据标 PUB/KB · COM 仅存在性记录 · 未核实标「未核实」)";
+
+    /** G-3 规则码:证据码隔离 */
+    public static final String RULE_EVIDENCE_ISOLATE = "evidence_isolate";
+
+    /** 证据码:公开文献(可进推荐依据) */
+    public static final String EVIDENCE_PUB = "PUB";
+
+    /** 证据码:知识库(可进推荐依据) */
+    public static final String EVIDENCE_KB = "KB";
+
+    /** 证据码:存在性记录(只作存在性记录,不进推荐依据) */
+    public static final String EVIDENCE_COM = "COM";
+
+    /** 未核实标注(未核实的编号统一标此值,不进推荐依据) */
+    public static final String EVIDENCE_UNVERIFIED = "未核实";
+
     /**
      * 措辞白名单(关联非因果口径)。描述菌↔问题关联时,仅允许使用下列措辞;
      * 白名单外的因果表述(如「缺大肠杆菌→致病/需补充」)由门禁拦截并给出建议改写。

+ 60 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/attribution/AttributionComplianceGate.java

@@ -19,6 +19,11 @@ import java.util.regex.Pattern;
  * (剂量/用法用量/疗程),命中即收集为合规违规(violation),不阻断流程
  * (E4 弱提示口径),由报告侧 {@code sections.complianceGate} 暴露。
  *
+ * <p><b>G-3 证据码隔离</b>:另含 T11(G-3)证据码隔离执行器——证据标 PUB/KB
+ * (可进推荐依据)、COM 仅作存在性记录(不进推荐依据)、未核实编号标「未核实」,
+ * 见 {@link #tagEvidence} / {@link #canEnterRecommendationBasis} /
+ * {@link #isExistenceRecordOnly} / {@link #isolateForRecommendation}。
+ *
  * <p>纯规则、无 DB 依赖、无副作用(除设置 {@link IndicatorReading#setAssociationOnly(boolean)}),
  * 可单测。挂载点:{@code AttributionAnalysisServiceImpl.analyze()}(L4 出结论前 enforce +
  * L5 出文档前 scan)。
@@ -186,6 +191,61 @@ public class AttributionComplianceGate {
         return new GateReport(base == null ? Collections.emptyList() : base.getNodes(), all);
     }
 
+    // ==================== G-3 证据码隔离(T11,P1) ====================
+    //
+    // 证据码隔离口径:低侧机制证据标 PUB/KB(可进推荐依据);COM 只作存在性记录
+    // (绝不进推荐依据);未核实的编号(空/null/未知编号)统一标「未核实」,
+    // 不得充当推荐依据。纯规则、无 DB 依赖、无副作用,可单测。
+    // 挂载点:与 G-1 同处横切合规门禁(L4 出结论前 / L5 出文档前),推荐依据组装前调用。
+
+    /**
+     * 证据码规范化:PUB/KB/COM 原样保留;空/null/未知编号 → {@link AttributionLowSideRules#EVIDENCE_UNVERIFIED}「未核实」。
+     */
+    public String tagEvidence(String evidenceCode) {
+        if (evidenceCode == null || evidenceCode.trim().isEmpty()) {
+            return AttributionLowSideRules.EVIDENCE_UNVERIFIED;
+        }
+        String code = evidenceCode.trim();
+        if (AttributionLowSideRules.EVIDENCE_PUB.equals(code)
+                || AttributionLowSideRules.EVIDENCE_KB.equals(code)
+                || AttributionLowSideRules.EVIDENCE_COM.equals(code)) {
+            return code;
+        }
+        return AttributionLowSideRules.EVIDENCE_UNVERIFIED;
+    }
+
+    /**
+     * 证据码是否可进推荐依据:仅 PUB/KB 为 true;
+     * COM(存在性记录)与「未核实」(空/null/未知编号)一律 false。
+     */
+    public boolean canEnterRecommendationBasis(String evidenceCode) {
+        String code = tagEvidence(evidenceCode);
+        return AttributionLowSideRules.EVIDENCE_PUB.equals(code)
+                || AttributionLowSideRules.EVIDENCE_KB.equals(code);
+    }
+
+    /** 证据码是否为「仅存在性记录」:仅 COM 为 true,PUB/KB/未核实 均为 false。 */
+    public boolean isExistenceRecordOnly(String evidenceCode) {
+        return AttributionLowSideRules.EVIDENCE_COM.equals(tagEvidence(evidenceCode));
+    }
+
+    /**
+     * 推荐依据隔离:从证据码清单中过滤仅保留 PUB/KB(保序),
+     * 剔除 COM(存在性记录)与「未核实」(空/null/未知编号)。
+     */
+    public List<String> isolateForRecommendation(List<String> evidenceCodes) {
+        if (evidenceCodes == null) {
+            return Collections.emptyList();
+        }
+        List<String> out = new ArrayList<>();
+        for (String code : evidenceCodes) {
+            if (canEnterRecommendationBasis(code)) {
+                out.add(code);
+            }
+        }
+        return out;
+    }
+
     // ==================== 内部 ====================
 
     private List<Violation> matchAll(String text, List<Pattern> patterns, String rule,

+ 5 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheEvidenceIsolateContractTest.java

@@ -263,6 +263,11 @@ class GuheEvidenceIsolateContractTest {
 
     /** 按方法名 + 参数个数匹配并调用(宽松适配基本/包装类型),契约方法缺失即断言失败。 */
     private static Object call(Object host, String method, Object... args) {
+        // 显式传 null 参数时(如 tagEvidence(null)),Java 会把 null 解析成整个变长数组(args==null),
+        // 此处归一化为「单个 null 实参」,保证 null 用例可正常进入契约方法(方法内统一按「未核实」处理)。
+        if (args == null) {
+            args = new Object[]{null};
+        }
         for (Method m : host.getClass().getMethods()) {
             if (m.getName().equals(method) && m.getParameterCount() == args.length) {
                 try {