|
@@ -1,6 +1,7 @@
|
|
|
package com.etotem.cfc.unit;
|
|
package com.etotem.cfc.unit;
|
|
|
|
|
|
|
|
import com.etotem.cfc.constant.AttributionRedlines;
|
|
import com.etotem.cfc.constant.AttributionRedlines;
|
|
|
|
|
+import com.etotem.cfc.constant.AttributionLowSideRules;
|
|
|
import com.etotem.cfc.dto.AttributionInput;
|
|
import com.etotem.cfc.dto.AttributionInput;
|
|
|
import com.etotem.cfc.dto.AttributionReportVO;
|
|
import com.etotem.cfc.dto.AttributionReportVO;
|
|
|
import com.etotem.cfc.entity.AttributionReport;
|
|
import com.etotem.cfc.entity.AttributionReport;
|
|
@@ -19,6 +20,7 @@ import com.etotem.cfc.mapper.HealthNormReferenceMapper;
|
|
|
import com.etotem.cfc.mapper.HealthReportMapper;
|
|
import com.etotem.cfc.mapper.HealthReportMapper;
|
|
|
import com.etotem.cfc.mapper.SurveyRecordMapper;
|
|
import com.etotem.cfc.mapper.SurveyRecordMapper;
|
|
|
import com.etotem.cfc.service.AttributionAnalysisService;
|
|
import com.etotem.cfc.service.AttributionAnalysisService;
|
|
|
|
|
+import com.etotem.cfc.service.attribution.AttributionComplianceGate;
|
|
|
import com.etotem.cfc.service.attribution.AttributionNarrativeService;
|
|
import com.etotem.cfc.service.attribution.AttributionNarrativeService;
|
|
|
import com.etotem.cfc.service.attribution.AttributionReportBuilder;
|
|
import com.etotem.cfc.service.attribution.AttributionReportBuilder;
|
|
|
import com.etotem.cfc.service.attribution.FalsificationEngine;
|
|
import com.etotem.cfc.service.attribution.FalsificationEngine;
|
|
@@ -129,6 +131,7 @@ class AttributionPipelineMockTest {
|
|
|
setField(svc, "falsificationEngine", falsificationEngine);
|
|
setField(svc, "falsificationEngine", falsificationEngine);
|
|
|
setField(svc, "attributionNarrativeService", narrativeService);
|
|
setField(svc, "attributionNarrativeService", narrativeService);
|
|
|
setField(svc, "attributionReportBuilder", reportBuilder);
|
|
setField(svc, "attributionReportBuilder", reportBuilder);
|
|
|
|
|
+ setField(svc, "attributionComplianceGate", new AttributionComplianceGate());
|
|
|
service = svc;
|
|
service = svc;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -204,12 +207,19 @@ class AttributionPipelineMockTest {
|
|
|
|
|
|
|
|
// --- 5) 被证伪项 ---
|
|
// --- 5) 被证伪项 ---
|
|
|
List<FalsifiedItem> falsified = falsificationEngine.falsify(five.attributions, compare.getReadings(), flags);
|
|
List<FalsifiedItem> falsified = falsificationEngine.falsify(five.attributions, compare.getReadings(), flags);
|
|
|
- // 应含“补充剂印记类异常”
|
|
|
|
|
|
|
+ // 应含"补充剂印记类异常"
|
|
|
assertTrue(falsified.stream().anyMatch(f -> f.getItem().contains("补充剂印记")), "有补充剂印记被证伪项");
|
|
assertTrue(falsified.stream().anyMatch(f -> f.getItem().contains("补充剂印记")), "有补充剂印记被证伪项");
|
|
|
- // 应含“常模缺失项归因无效”
|
|
|
|
|
|
|
+ // 应含"常模缺失项归因无效"
|
|
|
assertTrue(falsified.stream().anyMatch(f -> f.getItem().contains("常模缺失")), "有常模缺失被证伪项");
|
|
assertTrue(falsified.stream().anyMatch(f -> f.getItem().contains("常模缺失")), "有常模缺失被证伪项");
|
|
|
|
|
|
|
|
- // --- 6) AI 叙述(规则兜底,AI 关闭)---
|
|
|
|
|
|
|
+ // --- 6) G-1 低侧红线组(L4 出结论前 enforce):任意「偏低」归因节点 association_only=true
|
|
|
|
|
+ AttributionComplianceGate.GateReport gate = new AttributionComplianceGate().enforceLowSide(five.attributions);
|
|
|
|
|
+ assertFalse(gate.getNodes().isEmpty(), "至少应有 1 个偏低节点 associationOnly=true");
|
|
|
|
|
+ for (AttributionComplianceGate.LowSideNode n : gate.getNodes()) {
|
|
|
|
|
+ assertTrue(n.isAssociationOnly(), n.getIndicator() + " associationOnly 应强制为 true");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- 7) L5 出文档前 scan(对 AI 叙述反向回扫)
|
|
|
Map<String, Object> narrativeCtx = new LinkedHashMap<>();
|
|
Map<String, Object> narrativeCtx = new LinkedHashMap<>();
|
|
|
narrativeCtx.put("attributions", toAttributionView(five.attributions));
|
|
narrativeCtx.put("attributions", toAttributionView(five.attributions));
|
|
|
narrativeCtx.put("familyMap", five.familyMap);
|
|
narrativeCtx.put("familyMap", five.familyMap);
|
|
@@ -221,15 +231,21 @@ class AttributionPipelineMockTest {
|
|
|
assertNotNull(narrative.get("conclusionHeadline"));
|
|
assertNotNull(narrative.get("conclusionHeadline"));
|
|
|
assertNotNull(narrative.get("coachQuestions"));
|
|
assertNotNull(narrative.get("coachQuestions"));
|
|
|
assertNotNull(narrative.get("closingLine"));
|
|
assertNotNull(narrative.get("closingLine"));
|
|
|
|
|
+ // L5 scan
|
|
|
|
|
+ String narText = narrative.get("conclusionHeadline") + "\n" + narrative.get("closingLine");
|
|
|
|
|
+ List<AttributionComplianceGate.Violation> narHits = new AttributionComplianceGate().scan(narText);
|
|
|
|
|
+ AttributionComplianceGate.GateReport gateReport = AttributionComplianceGate.merge(gate, narHits);
|
|
|
|
|
|
|
|
- // --- 7) 报告 JSON 构建(十节)---
|
|
|
|
|
|
|
+ // --- 8) 报告 JSON 构建(十节 + G-1 complianceGate 段)
|
|
|
AttributionInput input = new AttributionInput();
|
|
AttributionInput input = new AttributionInput();
|
|
|
input.setFamilyId(1L);
|
|
input.setFamilyId(1L);
|
|
|
input.setMemberIds(Arrays.asList(1L, 2L, 3L, 4L));
|
|
input.setMemberIds(Arrays.asList(1L, 2L, 3L, 4L));
|
|
|
|
|
+ List<String> redlineHits = reportBuilder.computeRedlineHits(five, compare.getIntermediateState(), members);
|
|
|
|
|
+ if (!gateReport.getNodes().isEmpty() || gateReport.hasViolations()) {
|
|
|
|
|
+ redlineHits.add(AttributionLowSideRules.GROUP_CODE);
|
|
|
|
|
+ }
|
|
|
String reportJson = reportBuilder.buildReportJson(
|
|
String reportJson = reportBuilder.buildReportJson(
|
|
|
- five, links, falsified, compare, narrative,
|
|
|
|
|
- reportBuilder.computeRedlineHits(five, compare.getIntermediateState(), members),
|
|
|
|
|
- input);
|
|
|
|
|
|
|
+ five, links, falsified, compare, narrative, redlineHits, input, gateReport);
|
|
|
|
|
|
|
|
// 验收:十节结构
|
|
// 验收:十节结构
|
|
|
JsonNode root = objectMapper.readTree(reportJson);
|
|
JsonNode root = objectMapper.readTree(reportJson);
|
|
@@ -245,8 +261,16 @@ class AttributionPipelineMockTest {
|
|
|
// 1 结论先行
|
|
// 1 结论先行
|
|
|
assertNotNull(sections.get("conclusion"));
|
|
assertNotNull(sections.get("conclusion"));
|
|
|
assertNotNull(sections.get("conclusion").path("headline").asText());
|
|
assertNotNull(sections.get("conclusion").path("headline").asText());
|
|
|
- // 2 数据速览
|
|
|
|
|
|
|
+ // 2 数据速览(含 associationOnly 字段)
|
|
|
assertTrue(sections.get("overview").isArray());
|
|
assertTrue(sections.get("overview").isArray());
|
|
|
|
|
+ boolean hasAssocField = false;
|
|
|
|
|
+ for (JsonNode row : sections.get("overview")) {
|
|
|
|
|
+ if (row.has("associationOnly")) {
|
|
|
|
|
+ hasAssocField = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assertTrue(hasAssocField, "overview 行须携带 associationOnly 字段");
|
|
|
// 3 逐人归因
|
|
// 3 逐人归因
|
|
|
JsonNode perMember = sections.get("perMember");
|
|
JsonNode perMember = sections.get("perMember");
|
|
|
assertEquals(4, perMember.size());
|
|
assertEquals(4, perMember.size());
|
|
@@ -263,10 +287,9 @@ class AttributionPipelineMockTest {
|
|
|
assertTrue(sections.get("coachQuestions").isArray());
|
|
assertTrue(sections.get("coachQuestions").isArray());
|
|
|
// 8 问诊清单
|
|
// 8 问诊清单
|
|
|
assertNotNull(sections.get("questionnaire"));
|
|
assertNotNull(sections.get("questionnaire"));
|
|
|
- // 9 红线与转介
|
|
|
|
|
|
|
+ // 9 红线与转介(RL01-RL08 + G-1 可能追加)
|
|
|
JsonNode redlines = sections.get("redlines");
|
|
JsonNode redlines = sections.get("redlines");
|
|
|
assertTrue(redlines.isArray());
|
|
assertTrue(redlines.isArray());
|
|
|
- // RL01-RL08 共 8 条
|
|
|
|
|
Set<String> codes = new LinkedHashSet<>();
|
|
Set<String> codes = new LinkedHashSet<>();
|
|
|
for (JsonNode r : redlines) {
|
|
for (JsonNode r : redlines) {
|
|
|
codes.add(r.path("code").asText());
|
|
codes.add(r.path("code").asText());
|
|
@@ -274,6 +297,7 @@ class AttributionPipelineMockTest {
|
|
|
for (String code : new String[]{"RL01","RL02","RL03","RL04","RL05","RL06","RL07","RL08"}) {
|
|
for (String code : new String[]{"RL01","RL02","RL03","RL04","RL05","RL06","RL07","RL08"}) {
|
|
|
assertTrue(codes.contains(code), "红线缺失: " + code);
|
|
assertTrue(codes.contains(code), "红线缺失: " + code);
|
|
|
}
|
|
}
|
|
|
|
|
+ assertTrue(codes.contains(AttributionLowSideRules.GROUP_CODE), "命中节点时应追加 G-1 红线");
|
|
|
// RL02 hit(女儿 心 短板)→ 命中
|
|
// RL02 hit(女儿 心 短板)→ 命中
|
|
|
boolean rl02hit = false;
|
|
boolean rl02hit = false;
|
|
|
for (JsonNode r : redlines) {
|
|
for (JsonNode r : redlines) {
|
|
@@ -299,7 +323,18 @@ class AttributionPipelineMockTest {
|
|
|
// 状态 READY
|
|
// 状态 READY
|
|
|
assertEquals(AttributionReport.STATUS_READY, root.path("status").asText());
|
|
assertEquals(AttributionReport.STATUS_READY, root.path("status").asText());
|
|
|
|
|
|
|
|
- System.out.println("✅ B1 验收通过:十节结构 + 中间态 5 键 + RL01/RL02 命中 + status=READY");
|
|
|
|
|
|
|
+ // ---- G-1 合规门禁段落地校验 ----
|
|
|
|
|
+ JsonNode cg = sections.get("complianceGate");
|
|
|
|
|
+ assertNotNull(cg, "sections.complianceGate 缺失");
|
|
|
|
|
+ assertEquals(AttributionLowSideRules.GROUP_CODE, cg.path("group").asText());
|
|
|
|
|
+ assertEquals(true, cg.path("active").asBoolean());
|
|
|
|
|
+ assertEquals(true, cg.path("associationOnly").asBoolean());
|
|
|
|
|
+ assertEquals(gate.getNodes().size(), cg.get("lowSideNodes").size(), "G-1 节点数应与 enforce 一致");
|
|
|
|
|
+ for (JsonNode node : cg.get("lowSideNodes")) {
|
|
|
|
|
+ assertTrue(node.path("associationOnly").asBoolean(), "G-1 节点必须关联非因果");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("✅ B1 验收通过:十节结构 + 中间态 5 键 + RL01/RL02/G-1 命中 + status=READY");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ============================================================
|
|
// ============================================================
|