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

feat(attribution.t2): 个人层双断言(L2-4)_PERSONAL_KEYS 键名级 + 页码残段/ND 参考 内容级

任务:T2 个人层双断言(L2-4,写库前门禁),规格见
docs/superpowers/plans/2026-11-08-浠艾福平台程序修改计划.md。

改动:
- 新增 PersonalLayerDualAssertion:写库前对个人层条目执行
  1) 键名级断言(_PERSONAL_KEYS 白名单:丰度/正常范围/人群水平/检出率/状态/
     说明/改善方式/评估值/参考范围,支持主体+键后缀匹配);
  2) 字符串内容级断言(扫全文命中 分页残段 \d{1,3}/16\d、ND 参考、
     缺X补X 知识层泄漏伪键 即判违规)。
- 新增 PersonalLayerDualAssertionTest:正样本「大肠杆菌改善方式以『苦丁茶』开头」
  必须通过;分页残段/ND 参考/知识层键/缺X补X 负样本全部命中;合法含%内容不误杀。

C 档平台现状(自主探测,已记录于报告):
- Adapter 形态:已有指纹路由 ReportParserDispatchService(新增 guhe_v2 分支即可)
- KB 加载:UnifiedKnowledgeBaseService(DB 主源 + classpath 种子兜底)
- L4 引擎:service/attribution 五阶段 pipeline 已存在(补规则即可)
- 合规门禁:AttributionComplianceGate 已挂 AttributionAnalysisServiceImpl(G-1 部分落地)

验证:
- mvn -f cfc-backend/pom.xml compile → BUILD SUCCESS(2112 源文件)
- 隔离 harness:正样本 ALL_PASS + 负样本全拦截
- cfc-web vue-cli-service build → Build complete

不新增 DB 迁移。
Agnes Agent 4 дней назад
Родитель
Сommit
8983f9c735

+ 224 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/attribution/PersonalLayerDualAssertion.java

@@ -0,0 +1,224 @@
+package com.etotem.cfc.service.attribution;
+
+import org.springframework.stereotype.Component;
+
+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.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 个人层双断言(L2-4,写库前门禁)。
+ *
+ * <p><b>职责</b>:在把菌群报告「个人层」数据写入知识库(写库)之前,执行两级确定性断言,
+ * 防止个人层丰度/干预文本混入知识层、以及分页表拼接残段污染:
+ * <ol>
+ *   <li><b>键名级</b>:个人层条目键必须命中 {@link #PERSONAL_KEYS} 白名单
+ *       (键名级 {@code _PERSONAL_KEYS});否则判为「知识层键泄漏 / 分页残段伪键」。</li>
+ *   <li><b>字符串内容级</b>:对条目内容扫全文,命中
+ *       {@link #PAGE_TAIL_PATTERN 分页残段}(如 {@code 34/167 0.0033 参考 0-0.1746% ND})
+ *       或 {@link #ND_TOKEN_PATTERN ND 参考} 即判违规。</li>
+ * </ol>
+ *
+ * <p><b>正样本</b>「大肠杆菌改善方式以『苦丁茶』开头」必须通过——纯干预文本不含分页残段 /
+ * ND 参考,键名在白名单内,断言不得误杀合法个人层干预内容。
+ *
+ * <p>纯规则、无 DB 依赖、无副作用,可单测。挂载点:写库前(个人层数据入库前),与
+ * {@link AttributionComplianceGate}(G-1 低侧红线,L4/L5 出结论/出文档前)互补。
+ *
+ * @author 浠艾福平台程序修改(guhe_v2 兼容 · T2 个人层双断言 L2-4)
+ */
+@Component
+public class PersonalLayerDualAssertion {
+
+    // ==================== 规则码 ====================
+
+    /** 键名级断言:个人层键不在白名单 */
+    public static final String RULE_KEY_NOT_PERSONAL = "key_not_personal";
+    /** 字符串内容级断言:分页残段(如 34/167 …) */
+    public static final String RULE_PAGE_TAIL_REMNANT = "page_tail_remnant";
+    /** 字符串内容级断言:ND 参考(低于检出丰度标注泄漏) */
+    public static final String RULE_ND_TOKEN = "nd_reference";
+
+    // ==================== 键名级白名单(_PERSONAL_KEYS) ====================
+
+    /**
+     * 个人层键名白名单 {@code _PERSONAL_KEYS}。
+     *
+     * <p>仅这些键可作为个人层条目写入;超出即视为知识层键泄漏或分页残段伪键。
+     * 涵盖谷禾/北京菌群报告个人层字段:丰度、正常范围、人群水平、检出率、状态、
+     * 菌种说明、改善方式、评估值、参考范围。
+     */
+    public static final List<String> PERSONAL_KEYS = Collections.unmodifiableList(Arrays.asList(
+            "丰度", "正常范围", "人群水平", "检出率", "状态", "说明", "改善方式",
+            "评估值", "参考范围"));
+
+    // ==================== 字符串内容级:分页残段 / ND 参考 ====================
+
+    /**
+     * 分页残段(「与健康相关菌评估」分页表拼接):
+     * 形如 {@code 34/167 0.0033 参考 0-0.1746% ND} —— 页码 {@code \d{1,3}/16\d} 为强特征。
+     */
+    public static final Pattern PAGE_TAIL_PATTERN =
+            Pattern.compile("\\d{1,3}/16\\d");
+
+    /** ND 参考:低于检出丰度标注(ND = not detected)泄漏到内容层。 */
+    public static final Pattern ND_TOKEN_PATTERN =
+            Pattern.compile("ND\\s*参考");
+
+    /** 知识层键泄漏(「缺 X → 补 X」桥接伪键名,个人层不应出现)。 */
+    public static final Pattern KNOWLEDGE_LEAK_KEY_PATTERN =
+            Pattern.compile("缺[\\u4e00-\\u9fff]{1,10}补[\\u4e00-\\u9fff]{1,10}");
+
+    private static final Pattern[] CONTENT_PATTERNS = {
+            PAGE_TAIL_PATTERN, ND_TOKEN_PATTERN, KNOWLEDGE_LEAK_KEY_PATTERN
+    };
+
+    // ==================== 单条断言 ====================
+
+    /**
+     * 写库前对单条个人层条目执行双断言(键名级 + 内容级)。
+     *
+     * @param key     个人层条目键(如「大肠杆菌改善方式」)
+     * @param content 条目内容(如「苦丁茶…」)
+     * @return 违规列表;空表示该条目通过(正样本应返回空)
+     */
+    public List<Violation> assertEntry(String key, String content) {
+        List<Violation> out = new ArrayList<>();
+        if (key != null && !isPersonalKey(key)) {
+            out.add(new Violation(RULE_KEY_NOT_PERSONAL,
+                    "个人层键「" + key + "」不在 _PERSONAL_KEYS 白名单(知识层键泄漏或分页残段伪键)",
+                    key, "个人层仅允许键:" + PERSONAL_KEYS));
+        }
+        out.addAll(scanContent(content));
+        return out;
+    }
+
+    /**
+     * 字符串内容级断言:扫全文,命中分页残段 / ND 参考 / 知识层泄漏即收集违规。
+     *
+     * @param content 条目内容(可空)
+     * @return 违规列表;空表示内容干净
+     */
+    public List<Violation> scanContent(String content) {
+        if (content == null || content.trim().isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Violation> out = new ArrayList<>();
+        collect(out, content, PAGE_TAIL_PATTERN, RULE_PAGE_TAIL_REMNANT,
+                "内容含分页残段(页码 \\d{1,3}/16\\d),个人层不得混入知识层分页表拼接",
+                "写库前须 _strip_table_tail() 清洗分页表拼接残段");
+        collect(out, content, ND_TOKEN_PATTERN, RULE_ND_TOKEN,
+                "内容含「ND 参考」(低于检出丰度标注),不应写入知识层",
+                "ND 仅作个人层「低于检出丰度」记录,参考下限记 0");
+        collect(out, content, KNOWLEDGE_LEAK_KEY_PATTERN, RULE_KEY_NOT_PERSONAL,
+                "内容含「缺 X → 补 X」桥接伪键(知识层键泄漏)",
+                "低侧机制仅关联非因果,禁缺 X→补 X(叠加 16S 粒度声明)");
+        return out;
+    }
+
+    // ==================== 批量断言(个人层 Map) ====================
+
+    /**
+     * 写库前对个人层条目集合执行双断言。
+     *
+     * @param entries 键 → 内容(如「大肠杆菌改善方式」→「苦丁茶…」)
+     * @return 全部违规;空表示整批通过
+     */
+    public List<Violation> assertEntries(Map<String, String> entries) {
+        List<Violation> out = new ArrayList<>();
+        if (entries == null || entries.isEmpty()) {
+            return out;
+        }
+        for (Map.Entry<String, String> e : entries.entrySet()) {
+            out.addAll(assertEntry(e.getKey(), e.getValue()));
+        }
+        return out;
+    }
+
+    // ==================== 便捷判断 ====================
+
+    /** 键是否为合法个人层键(键名级断言通过)。 */
+    public static boolean isPersonalKey(String key) {
+        if (key == null) {
+            return false;
+        }
+        for (String pk : PERSONAL_KEYS) {
+            if (pk.equals(key) || key.endsWith(pk)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** 内容是否干净(无分页残段 / ND 参考 / 知识层泄漏)。 */
+    public static boolean isCleanContent(String content) {
+        if (content == null || content.trim().isEmpty()) {
+            return true;
+        }
+        for (Pattern p : CONTENT_PATTERNS) {
+            if (p.matcher(content).find()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // ==================== 违规载体 ====================
+
+    /** 单条断言违规 */
+    public static final class Violation {
+        private final String rule;
+        private final String message;
+        private final String snippet;
+        private final String suggestion;
+
+        public Violation(String rule, String message, String snippet, String suggestion) {
+            this.rule = rule;
+            this.message = message;
+            this.snippet = snippet;
+            this.suggestion = suggestion;
+        }
+
+        public String getRule() {
+            return rule;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public String getSnippet() {
+            return snippet;
+        }
+
+        public String getSuggestion() {
+            return suggestion;
+        }
+
+        @Override
+        public String toString() {
+            return "Violation{rule='" + rule + "', snippet='" + snippet + "'}";
+        }
+    }
+
+    // ==================== 内部 ====================
+
+    private void collect(List<Violation> out, String text, Pattern pattern,
+                         String rule, String message, String suggestion) {
+        Matcher m = pattern.matcher(text);
+        while (m.find()) {
+            out.add(new Violation(rule, message, snippet(text, m.start(), m.end()), suggestion));
+        }
+    }
+
+    private static String snippet(String text, int start, int end) {
+        int s = Math.max(0, start - 8);
+        int e = Math.min(text.length(), end + 8);
+        return text.substring(s, e).replace('\n', ' ').trim();
+    }
+}

+ 165 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/PersonalLayerDualAssertionTest.java

@@ -0,0 +1,165 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.service.attribution.PersonalLayerDualAssertion;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * T2 个人层双断言(L2-4,写库前门禁)单元测试。
+ *
+ * <p>覆盖:
+ * <ul>
+ *   <li>正样本「大肠杆菌改善方式以『苦丁茶』开头」必须通过(键名级 + 内容级双通过);</li>
+ *   <li>键名级:非 _PERSONAL_KEYS 白名单键判违规(知识层键泄漏 / 分页残段伪键);</li>
+ *   <li>内容级:分页残段(34/167 …ND)、ND 参考 扫全文命中判违规;</li>
+ *   <li>内容级不误杀:合法干预文本(苦丁茶等)通过、带百分号的内容通过。</li>
+ * </ul>
+ */
+class PersonalLayerDualAssertionTest {
+
+    private final PersonalLayerDualAssertion assertion = new PersonalLayerDualAssertion();
+
+    // ============================================================
+    // 正样本:大肠杆菌改善方式以『苦丁茶』开头 —— 必须通过
+    // ============================================================
+    @Test
+    void 正样本大肠杆菌改善方式以苦丁茶开头必须通过() {
+        Map<String, String> positives = new LinkedHashMap<>();
+        positives.put("大肠杆菌改善方式",
+                "苦丁茶泡水代茶饮,日常饮用;以中医体质调理为主,配合饮食清淡、少油腻辛辣。");
+        positives.put("双歧杆菌改善方式", "补充益生元与发酵乳制品,按产品说明服用。");
+
+        List<PersonalLayerDualAssertion.Violation> v = assertion.assertEntries(positives);
+        assertTrue(v.isEmpty(),
+                "正样本必须通过(键名级 + 内容级双断言均无违规):" + v);
+    }
+
+    @Test
+    void 正样本单条断言返回空违规() {
+        List<PersonalLayerDualAssertion.Violation> v =
+                assertion.assertEntry("大肠杆菌改善方式", "苦丁茶泡水代茶饮");
+        assertTrue(v.isEmpty(), "单条正样本断言应为空违规:" + v);
+    }
+
+    @Test
+    void 正样本键名级通过_endsWith白名单() {
+        assertTrue(PersonalLayerDualAssertion.isPersonalKey("大肠杆菌改善方式"),
+                "「大肠杆菌改善方式」应以「改善方式」结尾即视为个人层键");
+        assertTrue(PersonalLayerDualAssertion.isPersonalKey("丰度"));
+        assertTrue(PersonalLayerDualAssertion.isPersonalKey("大肠杆菌正常范围"));
+        assertTrue(PersonalLayerDualAssertion.isPersonalKey("检出率"));
+    }
+
+    // ============================================================
+    // 字符串内容级:分页残段 / ND 参考 扫全文(页码残段)
+    // ============================================================
+    @Test
+    void 内容含分页残段_命中页码残段违规() {
+        String remnant = "34/167 0.0033 参考 0-0.1746% ND";
+        List<PersonalLayerDualAssertion.Violation> v = assertion.scanContent(remnant);
+        assertFalse(v.isEmpty(), "应命中分页残段");
+        assertTrue(v.stream().anyMatch(x -> PersonalLayerDualAssertion.RULE_PAGE_TAIL_REMNANT.equals(x.getRule())),
+                "应命中 RULE_PAGE_TAIL_REMNANT:" + v);
+    }
+
+    @Test
+    void 内容含ND参考_命中ND违规() {
+        String ndText = "该菌丰度低于检出限,ND 参考即参考下限记 0,不代表菌缺失。";
+        List<PersonalLayerDualAssertion.Violation> v = assertion.scanContent(ndText);
+        assertFalse(v.isEmpty(), "应命中 ND 参考");
+        assertTrue(v.stream().anyMatch(x -> PersonalLayerDualAssertion.RULE_ND_TOKEN.equals(x.getRule())),
+                "应命中 RULE_ND_TOKEN:" + v);
+    }
+
+    @Test
+    void 批量断言_混合批次只报违规条目() {
+        Map<String, String> entries = new LinkedHashMap<>();
+        entries.put("大肠杆菌改善方式", "苦丁茶泡水代茶饮");           // 通过
+        entries.put("大肠杆菌丰度", "34/167 0.0033 参考 0-0.1746% ND"); // 分页残段
+        entries.put("报告总结", "营养均衡,菌群多样");                  // 知识层键
+
+        List<PersonalLayerDualAssertion.Violation> v = assertion.assertEntries(entries);
+        assertTrue(v.size() >= 2, "应有 2 条违规(分页残段 + 知识层键):" + v);
+        boolean pageTailHit = v.stream().anyMatch(x ->
+                PersonalLayerDualAssertion.RULE_PAGE_TAIL_REMNANT.equals(x.getRule()));
+        boolean keyHit = v.stream().anyMatch(x ->
+                PersonalLayerDualAssertion.RULE_KEY_NOT_PERSONAL.equals(x.getRule()));
+        assertTrue(pageTailHit, "应命中分页残段:" + v);
+        assertTrue(keyHit, "应命中非个人层键:" + v);
+    }
+
+    // ============================================================
+    // 键名级:_PERSONAL_KEYS 白名单
+    // ============================================================
+    @Test
+    void 知识层键不属于个人层_判违规() {
+        List<PersonalLayerDualAssertion.Violation> v = assertion.assertEntry("报告总结", "营养均衡");
+        assertFalse(v.isEmpty(), "「报告总结」为知识层键,个人层断言应拒绝");
+        assertEquals(PersonalLayerDualAssertion.RULE_KEY_NOT_PERSONAL, v.get(0).getRule());
+    }
+
+    @Test
+    void 分页残段伪键_判违规() {
+        assertFalse(PersonalLayerDualAssertion.isPersonalKey("34/167"));
+        List<PersonalLayerDualAssertion.Violation> v = assertion.assertEntry("34/167", "0.0033");
+        assertFalse(v.isEmpty());
+        assertEquals(PersonalLayerDualAssertion.RULE_KEY_NOT_PERSONAL, v.get(0).getRule());
+    }
+
+    @Test
+    void 缺X补X桥接伪键_判违规() {
+        List<PersonalLayerDualAssertion.Violation> v =
+                assertion.assertEntry("缺大肠杆菌补菌种", "需补充该菌");
+        assertFalse(v.isEmpty(), "「缺大肠杆菌补菌种」为知识层/桥接伪键,应拒绝");
+        assertEquals(PersonalLayerDualAssertion.RULE_KEY_NOT_PERSONAL, v.get(0).getRule());
+    }
+
+    // ============================================================
+    // 内容级不误杀:合法个人层内容
+    // ============================================================
+    @Test
+    void 合法干预内容不误报() {
+        for (String safe : new String[]{
+                "苦丁茶泡水代茶饮,日常饮用。",
+                "说明:肠道重要有益菌,维持代谢平衡。",
+                "正常范围为 0-0.1746%,处于人群 35% 水平,检出率 89%。", // 带 % 合法内容
+                "参考范围为 5%-15%"
+        }) {
+            List<PersonalLayerDualAssertion.Violation> v = assertion.scanContent(safe);
+            assertTrue(v.isEmpty(), "误报: " + safe);
+        }
+    }
+
+    @Test
+    void 空内容与空集合不报违规() {
+        assertTrue(assertion.scanContent(null).isEmpty());
+        assertTrue(assertion.scanContent("").isEmpty());
+        assertTrue(assertion.assertEntries(null).isEmpty());
+        assertTrue(assertion.assertEntries(new LinkedHashMap<>()).isEmpty());
+    }
+
+    // ============================================================
+    // 白名单与规则码常量
+    // ============================================================
+    @Test
+    void 个人层键白名单非空且含规定键() {
+        List<String> keys = PersonalLayerDualAssertion.PERSONAL_KEYS;
+        assertFalse(keys.isEmpty());
+        for (String required : new String[]{"丰度", "正常范围", "人群水平", "检出率", "状态", "改善方式"}) {
+            assertTrue(keys.contains(required), "白名单须含「" + required + "」");
+        }
+    }
+
+    @Test
+    void 规则码常量存在() {
+        assertEquals("key_not_personal", PersonalLayerDualAssertion.RULE_KEY_NOT_PERSONAL);
+        assertEquals("page_tail_remnant", PersonalLayerDualAssertion.RULE_PAGE_TAIL_REMNANT);
+        assertEquals("nd_reference", PersonalLayerDualAssertion.RULE_ND_TOKEN);
+    }
+}

+ 94 - 0
docs/tdd/orchestration/report-t2-personal-layer-dual-assertion-2026-11-09.md

@@ -0,0 +1,94 @@
+# T2 个人层双断言(L2-4)—— 平台现状探测与实现报告
+
+**日期**: 2026-11-09
+**任务**: T2 个人层双断言(`_PERSONAL_KEYS` 键名级 + `页码残段 / ND 参考` 字符串内容级扫全文),正样本「大肠杆菌改善方式以『苦丁茶』开头」须通过。
+**状态**: ✅ 已实现并编译通过,待 push
+
+---
+
+## 一、C 档 4 项平台现状探测(自主探测,不阻塞派发)
+
+| # | 探测点 | 现状 | 对接方式 |
+|---|--------|------|----------|
+| **1** | Adapter 形态 | 已有统一指纹路由 `ReportParserDispatchService`(`detect()` → `typeId` → `parserPath` → `runParser`),L1-1 guhe_v2 分支为**新增类型**而非重写路由 | 在 `RunParserService.runParser(parserPath)` 内新增 guhe_v2 解析器入口(参考 beijing/guhe 现有分支) |
+| **2** | KB 服务加载方式 | `UnifiedKnowledgeBaseService` 已封装——**DB 为主源**(`knowledge_base_section` 表)+ classpath `/knowledge-base/统一知识库.json` 种子兜底;启动时 DB 空则导入 | L2-1 改数据文件升级路径;L2-3 新字段写入 DB;无代码重构需求 |
+| **3** | L4 归因引擎存在性 | **强存在**:`service/attribution/` 包已实现五阶段 pipeline:`AttributionAnalysisServiceImpl`(主入口)→ `FiveDimAttributionEngine` / `MainCauseChainEngine` / `NormCompareEngine` / `IndicatorInterpretEngine` / `FalsificationEngine` | L4-1~L4-3 应在 `AttributionAnalysisServiceImpl.analyze()` 管线内补规则,或扩展对应 Engine |
+| **4** | 合规门禁挂载点 | **已存在**:`AttributionComplianceGate`(G-1 低侧红线组)挂载于 `AttributionAnalysisServiceImpl.analyze()` 内(L4 出结论前 enforce + L5 出文档前 scan),已拦截「缺 X→致病」桥接和处方化措辞 | G-1 部分落地完成;T2 的「写库前」挂载点应在新 Parser 调用链内(见下文 T2 落点) |
+
+**小结**:平台已在渐进落地该计划,P0/T5/G-1 基本就位(T5 实测已 PASS,见 `AttributionComplianceGateTest`)。T2/L1-2/P0 待实施件。
+
+---
+
+## 二、T2 个人层双断言实现
+
+### 2.1 新增类
+
+`cfc-backend/src/main/java/com/etotem/cfc/service/attribution/PersonalLayerDualAssertion.java`
+
+两条确定性断言,纯规则无副作用:
+
+1. **键名级断言** `_PERSONAL_KEYS`(白名单):
+   - 合法键含:`丰度`、`正常范围`、`人群水平`、`检出率`、`状态`、`说明`、`改善方式`、`评估值`、`参考范围`
+   - 键不在白名单即视为「知识层键泄漏 / 分页残段伪键」
+   - 容错:支持「主体 + 键」后缀匹配(如 `大肠杆菌改善方式` 命中)
+
+2. **字符串内容级断言**:
+   - 分页残段:`\d{1,3}/16\d`(如 `34/167 0.0033 参考 0-0.1746% ND`)
+   - ND 参考:`ND\s*参考`
+   - 知识层泄漏伪键:`缺[\\u4e00-\\u9fff]{1,10}补[\\u4e00-\\u9fff]{1,10}`(叠加 §5.2 16S 粒度声明)
+
+### 2.2 挂载点建议
+
+当前 Parser 链路:`PdfParseService.parse()` → `ParsedReportResult` → `HealthReportDraftService` → `saveDraft() / saveToDb()`。个人层(菌群指标 `gutFlora[]`)在**保存健康报告草稿前**插入双断言:
+
+```java
+// HealthReportDraftService.saveDraft() 内,gutter 入库前
+for (ParsedGutFlora f : parsed.getGutFlora()) {
+    List<Violation> v = assertion.assertEntries(buildPersonalEntries(f));
+    if (!v.isEmpty()) {
+        // 打 tag / 拦截入库 / 上报 UnknownCluster
+    }
+}
+```
+
+### 2.3 正样本验证
+
+正样本「大肠杆菌改善方式以『苦丁茶』开头」必须通过(两条均放行):
+
+```
+assertEntry("大肠杆菌改善方式", "苦丁茶泡水代茶饮,日常饮用;以中医体质调理为主...") → []
+assertEntries({"大肠杆菌改善方式": "苦丁茶...", "双歧杆菌改善方式": "补充益生元..."}) → []
+```
+
+实测:**PASS**(隔离 harness 输出 `ALL_PASS`)。
+
+---
+
+## 三、验证矩阵
+
+| 检查项 | 结果 |
+|--------|------|
+| `mvn compile`(`cfc-backend` 整工程) | ✅ BUILD SUCCESS(2112 source files,4m29s) |
+| `javac PersonalLayerDualAssertionTest` + junit-api | ✅ 编译通过 |
+| 正样本 harness(苦丁茶 / 合法干预文本) | ✅ ALL_PASS |
+| 负样本:分页残段 / ND 参考 / 知识层键 / 缺X补X | ✅ 全命中 |
+| 不误杀:含 `%` 的正常范围 / `说明:...` / `参考范围...` | ✅ PASS |
+| `cfc-web` npm build | ✅ Build complete (57.5s) |
+| cfc-langgraph pytest | ⏭ 本次未触发(非本次域内) |
+
+---
+
+## 四、提交物清单
+
+```
+cfc-backend/src/main/java/com/etotem/cfc/service/attribution/PersonalLayerDualAssertion.java
+cfc-backend/src/test/java/com/etotem/cfc/unit/PersonalLayerDualAssertionTest.java
+```
+
+---
+
+## 五、后续建议
+
+1. **T2 挂载**:在 `HealthReportDraftService.saveDraft()` 的个人层字段落库前插入 `assertEntries()`,违规条目不落 DB、记日志。
+2. **T1/T3/T4/T5/T6**:平台已部分就绪(见 §2.4 C 档报告),可复用 `AttributionComplianceGate` 模式补其余红线条。
+3. **guhe_v2 路由**(T7/T8/T9/T10/T11/T12/T13):需引入 A 档资产 `extract_guhe_v2.py`(本仓库外),由采集链路(LangGraph adapter)提供 32 键 + 清洗后 JSON,再挂入现有 Parser dispatch。