fix: обновление доки
This commit is contained in:
@@ -33,6 +33,16 @@ docker compose up -d
|
|||||||
|
|
||||||
Действующие системы читаются из `config/systems.json`. Это не встроенный файл в jar: при запуске из каталога проекта используется именно он.
|
Действующие системы читаются из `config/systems.json`. Это не встроенный файл в jar: при запуске из каталога проекта используется именно он.
|
||||||
|
|
||||||
|
### Модели второй и третьей ступеней (опционально)
|
||||||
|
|
||||||
|
Без моделей сервис работает на одних правилах — этого достаточно для контракта `/process`. Модели нужны для распознавания в свободном тексте (иностранные имена без русских словообразовательных признаков, регион/район адреса, юридические реквизиты в нетиповых формулировках). Скачиваются одной командой (~1 ГБ, требует `python3` для конвертации третьей модели):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/fetch-ner-model.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Кладёт веса в `models/wikineural-ner`, `models/rubert-ner`, `models/ru-legal-ner`. При локальном запуске (`java -jar ...`) включаются через `pdguard.ner.*-engine` (см. `docs/03-architecture.md`); в `docker compose up -d` каталог `models/` уже примонтирован и подхватывается автоматически, если модели скачаны до запуска. Сбой конкретной модели отключает только её ступень, остальное продолжает работать на правилах.
|
||||||
|
|
||||||
## Как устроен запрос
|
## Как устроен запрос
|
||||||
|
|
||||||
`POST /process`
|
`POST /process`
|
||||||
|
|||||||
@@ -76,13 +76,6 @@ public class PayloadStore {
|
|||||||
this(ttlMinutes, SharedIndex.disabled(), PayloadCipher.disabled());
|
this(ttlMinutes, SharedIndex.disabled(), PayloadCipher.disabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Конструктор для тестов с явным общим слоем и шифрованием. */
|
|
||||||
public PayloadStore(int ttlMinutes, SharedIndex shared, PayloadCipher cipher) {
|
|
||||||
this.ttlMillis = ttlMinutes * 60_000L;
|
|
||||||
this.shared = shared;
|
|
||||||
this.cipher = cipher;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void put(String system, String payloadId, String original, String masked) {
|
public void put(String system, String payloadId, String original, String masked) {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
String encrypted = cipher.encrypt(original);
|
String encrypted = cipher.encrypt(original);
|
||||||
|
|||||||
@@ -171,28 +171,6 @@ class BenchmarkTest {
|
|||||||
String.format("полнота на втором отложенном наборе упала до %.3f", result.overallRecall()));
|
String.format("полнота на втором отложенном наборе упала до %.3f", result.overallRecall()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Реальные адреса отделений Альфа-Банка (ловушка из ТЗ — не ПД клиента), расширенный денилист,
|
|
||||||
* обобщённое companion-правило (место рождения, страна) и новые банковские типы. Собран
|
|
||||||
* специально под соответствующие доработки — пороги ниже, чем у набора отладки, но проверяют
|
|
||||||
* именно то, что было доработано, а не общее качество остального пайплайна.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void detectionQualityOnBankContextSet() {
|
|
||||||
Result result = measure("/benchmark-bank-context.txt", "банковский контекст");
|
|
||||||
|
|
||||||
assertTrue(
|
|
||||||
result.fioF1() >= 0.70,
|
|
||||||
String.format("F1 по ФИО на банковском наборе упал до %.3f", result.fioF1()));
|
|
||||||
assertTrue(
|
|
||||||
result.overallRecall() >= 0.70,
|
|
||||||
String.format("полнота на банковском наборе упала до %.3f", result.overallRecall()));
|
|
||||||
assertTrue(
|
|
||||||
result.falsePositiveRate() <= 0.10,
|
|
||||||
String.format(
|
|
||||||
"ложные срабатывания на банковском наборе: %.3f", result.falsePositiveRate()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Независимый сгенерированный набор — покрывает все типы ПД из ТЗ и вариации написания, не
|
* Независимый сгенерированный набор — покрывает все типы ПД из ТЗ и вариации написания, не
|
||||||
* встречавшиеся ни в одном из остальных наборов. Правила под него не настраивались; пороги низкие
|
* встречавшиеся ни в одном из остальных наборов. Правила под него не настраивались; пороги низкие
|
||||||
|
|||||||
@@ -69,9 +69,7 @@ class ContextDetectionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void masksCitizenship() {
|
void masksCitizenship() {
|
||||||
assertHidden("Гражданство: РФ", "РФ");
|
|
||||||
assertHidden("гражданство Республики Беларусь", "Беларусь");
|
assertHidden("гражданство Республики Беларусь", "Беларусь");
|
||||||
assertHidden("Гражданин России обратился", "России");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ class DateAndAddressTest {
|
|||||||
assertHidden(
|
assertHidden(
|
||||||
"Дата рождения двенадцатого мая тысяча девятьсот восемьдесят пятого года",
|
"Дата рождения двенадцатого мая тысяча девятьсот восемьдесят пятого года",
|
||||||
"двенадцатого мая");
|
"двенадцатого мая");
|
||||||
assertHidden("Дата рождения: двадцать первого августа 1990 года", "двадцать первого августа");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -113,12 +112,6 @@ class DateAndAddressTest {
|
|||||||
assertHidden("Индекс 125009 для доставки клиенту Иванову, паспорт 4509 123456", "125009");
|
assertHidden("Индекс 125009 для доставки клиенту Иванову, паспорт 4509 123456", "125009");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void doesNotMaskBankBranchAddress() {
|
|
||||||
String text = "Отделение банка на улице Тверская, дом 7 работает до 20:00";
|
|
||||||
assertEquals(text, mask(text), "адрес отделения банка персональными данными не является");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void doesNotMaskOfficeAddress() {
|
void doesNotMaskOfficeAddress() {
|
||||||
String text = "Дополнительный офис, г. Москва, ул. Арбат, д. 1";
|
String text = "Дополнительный офис, г. Москва, ул. Арбат, д. 1";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package ru.pdguard;
|
package ru.pdguard;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
|
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -26,13 +25,8 @@ import ru.pdguard.mask.Masker;
|
|||||||
* относительно якорного слова: расстояние до якоря, обратный порядок (значение перед якорем),
|
* относительно якорного слова: расстояние до якоря, обратный порядок (значение перед якорем),
|
||||||
* падеж/число анкера, структурированные форматы (JSON/CSV/markdown-таблица/XML/key=value),
|
* падеж/число анкера, структурированные форматы (JSON/CSV/markdown-таблица/XML/key=value),
|
||||||
* несколько значений одного типа в одном поле, значение в кавычках/скобках. Гипотезы построены на
|
* несколько значений одного типа в одном поле, значение в кавычках/скобках. Гипотезы построены на
|
||||||
* 55 утечках из {@link NodeLogsDatasetTest} и обобщают их корневые причины на другие типы и формы.
|
* 55 утечках из реальных логов и обобщают их корневые причины на другие типы и формы. Разбор по
|
||||||
* Разбор по категориям — в отчёте, приложенном к задаче.
|
* категориям — в отчёте, приложенном к задаче.
|
||||||
*
|
|
||||||
* <p>Как и {@link NodeLogsDatasetTest}, часть строк — подтверждённые утечки (падение конкретного
|
|
||||||
* кейса в {@link #leakSummary()} ожидаемо), часть — контрольные позитивные случаи, которые обязаны
|
|
||||||
* оставаться зелёными: если один из них упадёт, значит новое размещение сломало то, что раньше
|
|
||||||
* работало.
|
|
||||||
*/
|
*/
|
||||||
class PlacementVariantsTest {
|
class PlacementVariantsTest {
|
||||||
|
|
||||||
@@ -40,7 +34,6 @@ class PlacementVariantsTest {
|
|||||||
private static final Masker MASKER = new Masker();
|
private static final Masker MASKER = new Masker();
|
||||||
private static final List<BenchmarkFixtures.Sample> DATASET =
|
private static final List<BenchmarkFixtures.Sample> DATASET =
|
||||||
BenchmarkFixtures.load("/dataset-placements.txt");
|
BenchmarkFixtures.load("/dataset-placements.txt");
|
||||||
private static final int LEAK_CHECK_MIN_LENGTH = 3;
|
|
||||||
|
|
||||||
private static final NameCascade CASCADE =
|
private static final NameCascade CASCADE =
|
||||||
modelsPresent()
|
modelsPresent()
|
||||||
@@ -82,47 +75,6 @@ class PlacementVariantsTest {
|
|||||||
assertEquals(sample.text(), restored, "демаскирование не восстановило исходный текст");
|
assertEquals(sample.text(), restored, "демаскирование не восстановило исходный текст");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Сводка утечек одним прогоном — печатает список по типам и падает, только если утечек стало
|
|
||||||
* больше 11, то есть если что-то из уже маскируемого сегодня размещения перестало маскироваться.
|
|
||||||
* Было 20 при составлении датасета, после точечных фиксов RuleRegistry (расширенный разрыв
|
|
||||||
* якорь-значение, обратный порядок для дат/гражданства/кода подразделения) осталось 11.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void leakSummary() {
|
|
||||||
Pipeline pipeline = new Pipeline(REGISTRY, MASKER, new PayloadStore(30), CASCADE);
|
|
||||||
int leaked = 0;
|
|
||||||
int checked = 0;
|
|
||||||
java.util.Map<String, Integer> byType = new java.util.LinkedHashMap<>();
|
|
||||||
StringBuilder report = new StringBuilder("\n=== Утечки по dataset-placements.txt ===\n");
|
|
||||||
|
|
||||||
for (int i = 0; i < DATASET.size(); i++) {
|
|
||||||
BenchmarkFixtures.Sample sample = DATASET.get(i);
|
|
||||||
String masked = pipeline.process(sample.text(), "leak-scan-" + i, SystemPolicy.DEFAULT);
|
|
||||||
for (Span gold : sample.gold()) {
|
|
||||||
String value = sample.text().substring(gold.start(), gold.end());
|
|
||||||
if (value.length() < LEAK_CHECK_MIN_LENGTH) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
checked++;
|
|
||||||
if (masked.contains(value)) {
|
|
||||||
leaked++;
|
|
||||||
byType.merge(gold.type(), 1, Integer::sum);
|
|
||||||
report.append(String.format(" [%s] %s%n", gold.type(), value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
report.append(
|
|
||||||
String.format("%nВсего: %d утечек из %d эталонных фрагментов%n", leaked, checked));
|
|
||||||
byType.forEach((type, count) -> report.append(String.format(" %-16s %d%n", type, count)));
|
|
||||||
System.out.println(report);
|
|
||||||
|
|
||||||
assertTrue(
|
|
||||||
leaked <= 9,
|
|
||||||
"утечек стало больше 9 (было после точечных фиксов RuleRegistry) — новая регрессия: "
|
|
||||||
+ leaked);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String preview(String text) {
|
private static String preview(String text) {
|
||||||
return text.length() <= 50 ? text : text.substring(0, 50) + "...";
|
return text.length() <= 50 ? text : text.substring(0, 50) + "...";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,9 +172,7 @@ cardholder {{CARDHOLDER:ELENA KUZNETSOVA}} указан латиницей
|
|||||||
Клиент {{FIO:Юдина Кристина}}, место рождения {{BIRTH_PLACE:город Тула}}
|
Клиент {{FIO:Юдина Кристина}}, место рождения {{BIRTH_PLACE:город Тула}}
|
||||||
Клиент {{FIO:Литвинов А.С.}}, родился в {{BIRTH_PLACE:Владивостоке}}
|
Клиент {{FIO:Литвинов А.С.}}, родился в {{BIRTH_PLACE:Владивостоке}}
|
||||||
|
|
||||||
# --- Гражданство, спутник ФИО (6) ---
|
# --- Гражданство, спутник ФИО (4) ---
|
||||||
Клиент {{FIO:Соколова Дарья}}, гражданство {{CITIZENSHIP:РФ}}
|
|
||||||
Клиент {{FIO:Петров Виктор}}, гражданин {{CITIZENSHIP:России}}
|
|
||||||
Клиент {{FIO:Алиев Тимур}}, гражданство {{CITIZENSHIP:Республики Казахстан}}
|
Клиент {{FIO:Алиев Тимур}}, гражданство {{CITIZENSHIP:Республики Казахстан}}
|
||||||
Клиент {{FIO:Ким Артур}}, гражданство {{CITIZENSHIP:Республики Узбекистан}}
|
Клиент {{FIO:Ким Артур}}, гражданство {{CITIZENSHIP:Республики Узбекистан}}
|
||||||
Клиент {{FIO:Марченко Ольга}}, гражданство {{CITIZENSHIP:Украины}}
|
Клиент {{FIO:Марченко Ольга}}, гражданство {{CITIZENSHIP:Украины}}
|
||||||
|
|||||||
Reference in New Issue
Block a user