|
|
@@ -12,18 +12,22 @@ import java.util.regex.Matcher;
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
/**
|
|
|
- * 合规门禁(横切 L4 出结论前 + L5 出文档前)——低侧红线组 G-1 确定性执行器。
|
|
|
+ * 合规门禁(横切 L4 出结论前 + L5 出文档前)——低侧红线组 G-1 + 16S 粒度拦截组 G-2 确定性执行器。
|
|
|
*
|
|
|
* <p><b>职责</b>:对任意「偏低」归因节点强制 {@code association_only == true}
|
|
|
* (低侧机制禁因果),并反向回扫「缺 X → 致病/需补充」桥接文案与处方化建议
|
|
|
* (剂量/用法用量/疗程),命中即收集为合规违规(violation),不阻断流程
|
|
|
* (E4 弱提示口径),由报告侧 {@code sections.complianceGate} 暴露。
|
|
|
*
|
|
|
+ * <p><b>G-2 16S 粒度拦截(T6,P0)</b>:在非菌株级粒度(granularity != strain,
|
|
|
+ * 16S 只到菌属/种,§5.2 16S 粒度声明)下禁生成「含菌株号的商品推荐」
|
|
|
+ * (菌名 + 菌株号,如 长双歧杆菌 BB536);菌株级粒度(granularity == strain,宏基因组)放行。
|
|
|
+ *
|
|
|
* <p>纯规则、无 DB 依赖、无副作用(除设置 {@link IndicatorReading#setAssociationOnly(boolean)}),
|
|
|
* 可单测。挂载点:{@code AttributionAnalysisServiceImpl.analyze()}(L4 出结论前 enforce +
|
|
|
* L5 出文档前 scan)。
|
|
|
*
|
|
|
- * @author 家庭归因阶段二(ATT-S2-B1,G-1 低侧红线组)
|
|
|
+ * @author 家庭归因阶段二(ATT-S2-B1,G-1 低侧红线组 + G-2 16S 粒度拦截)
|
|
|
*/
|
|
|
@Component
|
|
|
public class AttributionComplianceGate {
|
|
|
@@ -128,8 +132,8 @@ public class AttributionComplianceGate {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * L4 出结论前 enforce:任意「偏低」归因节点强制 association_only=true,
|
|
|
- * 并反向回扫该节点的干预含义文本(低侧机制禁因果)。
|
|
|
+ * L4 出结论前 enforce(G-1):任意「偏低」归因节点强制 association_only=true,
|
|
|
+ * 并反向回扫该节点的干预含义文本(低侧机制禁因果 + 非菌株级粒度禁菌株号)。
|
|
|
*/
|
|
|
public GateReport enforceLowSide(List<MemberAttribution> attributions) {
|
|
|
List<LowSideNode> nodes = new ArrayList<>();
|
|
|
@@ -148,14 +152,55 @@ public class AttributionComplianceGate {
|
|
|
r.setAssociationOnly(true); // G-1 规则 1:偏低节点禁因果
|
|
|
nodes.add(new LowSideNode(a.getMemberId(), a.getName(),
|
|
|
label(r), r.getValue(), r.getPercentile()));
|
|
|
- violations.addAll(scan(r.getMeaning()));
|
|
|
+ violations.addAll(scan(r.getMeaning(), r.isStrainGranularity()));
|
|
|
}
|
|
|
}
|
|
|
return new GateReport(nodes, violations);
|
|
|
}
|
|
|
|
|
|
- /** L5 出文档前 scan:对任意叙述/结论文本反向回扫(缺X桥接 + 处方化) */
|
|
|
+ /**
|
|
|
+ * L4 出结论前 enforce(G-2,T6 16S 粒度拦截):granularity != strain(16S/菌属层,默认)
|
|
|
+ * 时,禁生成含菌株号的商品推荐。对成员应对方向(actions)与读数干预含义(meaning)
|
|
|
+ * 反向回扫「菌名 + 菌株号」;任一读数达到菌株级粒度(strain,宏基因组)则整成员放行。
|
|
|
+ */
|
|
|
+ public GateReport enforceStrainBlock(List<MemberAttribution> attributions) {
|
|
|
+ List<Violation> violations = new ArrayList<>();
|
|
|
+ if (attributions == null) {
|
|
|
+ return new GateReport(Collections.emptyList(), violations);
|
|
|
+ }
|
|
|
+ for (MemberAttribution a : attributions) {
|
|
|
+ if (a == null || isStrainGranularity(a)) {
|
|
|
+ continue; // 菌株级粒度(strain)不受限
|
|
|
+ }
|
|
|
+ List<String> actions = a.getActions();
|
|
|
+ if (actions != null) {
|
|
|
+ for (String action : actions) {
|
|
|
+ violations.addAll(matchStrainRecommendation(action));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (IndicatorReading r : a.getReadings()) {
|
|
|
+ if (r == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ violations.addAll(matchStrainRecommendation(r.getMeaning()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return new GateReport(Collections.emptyList(), violations);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** L5 出文档前 scan:对任意叙述/结论文本反向回扫(缺X桥接 + 处方化),非菌株级粒度默认 */
|
|
|
public List<Violation> scan(String text) {
|
|
|
+ return scan(text, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * L5 出文档前 scan(粒度感知):对任意叙述/结论文本反向回扫
|
|
|
+ * (缺X桥接 + 处方化 + 非菌株级粒度下的菌株号推荐)。
|
|
|
+ *
|
|
|
+ * @param text 待回扫文本
|
|
|
+ * @param strainGranularity 是否菌株级粒度(true 时跳过菌株号推荐拦截,16S 粒度禁)
|
|
|
+ */
|
|
|
+ public List<Violation> scan(String text, boolean strainGranularity) {
|
|
|
if (text == null || text.trim().isEmpty()) {
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
@@ -168,6 +213,9 @@ public class AttributionComplianceGate {
|
|
|
AttributionLowSideRules.RULE_NO_PRESCRIPTION,
|
|
|
"禁把调整建议写成处方(剂量/用法用量/疗程)",
|
|
|
"剂量与用法用量由医生/专业定,方案只写「医生定」或「按产品说明」"));
|
|
|
+ if (!strainGranularity) {
|
|
|
+ out.addAll(matchStrainRecommendation(text));
|
|
|
+ }
|
|
|
return out;
|
|
|
}
|
|
|
|
|
|
@@ -188,6 +236,27 @@ public class AttributionComplianceGate {
|
|
|
|
|
|
// ==================== 内部 ====================
|
|
|
|
|
|
+ /** 非菌株级粒度下「菌名 + 菌株号」商品推荐拦截(T6 G-2) */
|
|
|
+ private List<Violation> matchStrainRecommendation(String text) {
|
|
|
+ if (text == null || text.trim().isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ return matchAll(text, AttributionLowSideRules.strainRecommendationPatterns(),
|
|
|
+ AttributionLowSideRules.RULE_NO_STRAIN_RECOMMENDATION,
|
|
|
+ "16S/菌属级粒度禁生成含菌株号的商品推荐(16S 只到菌属/种,§5.2 粒度声明)",
|
|
|
+ "菌株级专属推荐需宏基因组菌株级检测(granularity=strain)支撑;16S 粒度请改为属/种级应对方向(如「补充双歧杆菌属」)");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 成员任一读数达到菌株级粒度(strain)则判定为菌株级(放行菌株号推荐) */
|
|
|
+ private static boolean isStrainGranularity(MemberAttribution a) {
|
|
|
+ for (IndicatorReading r : a.getReadings()) {
|
|
|
+ if (r != null && r.isStrainGranularity()) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
private List<Violation> matchAll(String text, List<Pattern> patterns, String rule,
|
|
|
String message, String suggestion) {
|
|
|
List<Violation> out = new ArrayList<>();
|