Отсечь спам в форме можно тремя способами: поставить капчу и потерять часть живых людей, подключить внешний сервис и зависеть от сети, либо проверять саму заявку по десятку признаков. PHP Form Spam Checker 2.1.2 идёт третьим путём: конвейер проверок спама из двадцати модулей — от скрытой приманки и анализа времени заполнения до репутации IP, SPF/DMARC и байесовского классификатора. Ни капчи, ни зависимостей Composer во время работы: только PHP 8.5 и два расширения. Ниже — как устроен порядок проверок, что именно даёт новая версия, и что получилось при моих собственных запусках на PHP 8.5.0.

PHP Form Spam Checker — обложка обзора
Двадцать проверок по порядку: от приманки до байесовского классификатора

TL;DR: что это и как проверить за десять минут

  1. Что это. PHP-библиотека и CLI: конвейер из 20 проверок, опциональный WAF, вердикты с доказательствами.
  2. Требования. PHP 8.5 и новее, ext-json, ext-mbstring; зависимостей Composer во время работы нет.
  3. Как вызывать. php bin/spam-check с JSON-payload; коды выхода 0 чисто, 1 подозрительно, 2 спам, 3 ошибка.
  4. Что нового в 2.1.2. Опциональная проверка кириллицы в полях имени для русскоязычных форм, по умолчанию выключена.
  5. Важное ограничение. На Packagist пакета нет — composer require из README не сработает, установка только из архива или клона.
php -v                        # нужен PHP 8.5+
php bin/spam-check --help     # список опций, код 0
printf '%s' "$PAYLOAD" | php bin/spam-check --config=/etc/spam.php --stdin --json

Проблема, которую это решает

Форма обратной связи — самый простой вход в приложение и самый частый канал спама. Капча решает проблему ценой конверсии: часть людей уходит, не разобравшись с картинками, а часть не проходит их с телефона. Внешние сервисы добавляют сетевую зависимость: недоступен API — форма либо пропускает всё, либо блокирует живых людей.

Второй эффект — отсутствие объяснений. «Спам» без причины невозможно разобрать: непонятно, что именно сработало, и как ослабить правило, не открыв дверь. Здесь каждое решение несёт доказательства: какая проверка сработала, сколько баллов дала и почему.

Третья проблема — стоимость. Дешёвые проверки должны идти первыми, а дорогие (внешние запросы к репутации IP, DNS) — только после них, иначе каждая заявка тянет сетевой вызов. В конвейере это зафиксировано явно: pipeline.stop_on_spam по умолчанию останавливает обработку на первом жёстком спаме.

Что такое PHP Form Spam Checker и из чего он состоит

Компонент Значение
Тип PHP-библиотека, CLI и HTTP-эндпоинт
Версия в замерах 2.1.2
Требуется PHP 8.5+, ext-json, ext-mbstring
Зависимости Composer при работе ноль (только расширения PHP)
Проверок в конвейере 20
Правил WAF 9
PHP-файлов в src/ 93 (10 295 строк)
Тестовый набор по README 559 тестов, 2 015 проверок
flowchart TB
  REQ["HTTP-запрос"] --> WAF{"WAF включён?"}
  WAF -- да --> RULES["правила WAF: метод, URI, query, поля, cookies, заголовки, тело"]
  RULES -- блок --> BLOCK["403 или запись в журнал в режиме monitor"]
  RULES -- пропуск --> FORM["отправка формы"]
  WAF -- нет --> FORM
  FORM --> BEH["поведение: приманка, time trap, JS-челлендж"]
  BEH --> HEUR["локальные эвристики: UA, частота, длина, SQLi, XSS, ссылки, слова, email, повторы, алфавиты"]
  HEUR --> NET["репутация: AbuseIPDB, DNSBL"]
  NET --> DOMAIN["домен: SPF, DMARC"]
  DOMAIN --> BAYES["байесовский классификатор"]
  BAYES --> AGG["агрегация: жёсткие вердикты и сумма баллов"]
  AGG --> VERDICT{"чисто / подозрительно / спам"}

Лицензия — MIT, одна строка, как и договаривались.

Каталоги устроены прямолинейно: src/ — библиотека (проверки, WAF, конвейер, адаптеры), bin/ — три CLI-моста (spam-check, waf-check, spam-train), integrations/ — готовые точки встраивания (guard.php, waf.php, auto-prepend.php), public/ — HTTP-эндпоинт с JSON, config/ — примеры конфигурации и настройки на сайт.

Возможности: что проверяется

Поведение и идентификация

Проверка По умолчанию Эффект
honeypot включена заполненное скрытое поле — жёсткий спам, 100
dynamic_honeypot включена при секрете ≥ 16 символов заполнено — спам 100; нет ожидаемого поля — 10
time_trap выключена слишком быстро или подделка — спам 80; нет метки — 30
js_challenge выключена нет или неверный ответ — 15, никогда жёстко
user_agent включена пустой — 40; строка инструмента (curl/) — жёсткий спам 100
rate_limit включена, 8 запросов за 900 с превышение — жёсткий спам 100
autoban выключена забаненный IP — спам 100 до всех проверок

Полезная нагрузка, инъекции и контент

Проверка По умолчанию Эффект
length включена, максимум 5000 длинно — 30; больше 20 000 или управляющие символы — спам 100
sql_injection включена сильная сигнатура — спам 100; слабая — 40
xss включена скрипт или обработчик — спам 100; закодированное — 45
links включена, максимум 2 лишняя ссылка 15; сокращатель 35; обфускация 25
keywords включена 12 за попадание, максимум 90
email включена битый адрес 40; одноразовый домен 70
repetition включена повторы 25, максимум 60
language выключена смешанные алфавиты 20 (максимум 40); нет кириллицы в имени — missing_score

Сеть, домен и модель

Проверка По умолчанию Эффект
abuseipdb выключена уверенность ≥ 90 — жёсткий спам
dnsbl выключена попадание в список — спам 100
spf выключена нет пригодной политики — 10
dmarc выключена нет записи — 10; p=none — 5
bayes выключена P(spam) ≥ 0,92 — балл равен вероятности × 100

Секция dkim зарезервирована под будущую проверку и в конвейер не входит — это сказано прямо в документации, а не оставлено на догадки.

Из таблиц видна логика порогов. Баллы складываются, но не любые: часть проверок ставит «жёсткий спам» и обрывает дальнейшую работу — это приманка, строка инструмента в UA, превышение частоты, сильная сигнатура инъекции. Остальные дают очки в общую сумму, и решение принимается по порогам «чисто / подозрительно / спам». Поэтому одна слабая проверка не приводит к блокировке, а сочетание нескольких — приводит. И поэтому же настраивать фильтр надо по тому, какие проверки включены, а не по одному числу порога.

Отдельно про выключенные по умолчанию проверки — сеть (AbuseIPDB, DNSBL, SPF, DMARC) и модель. Причины две: внешние запросы стоят времени и квоты, а байесовскому классификатору нужна обучающая выборка конкретного сайта, иначе он приносит больше шума, чем пользы. Включать их стоит после того, как базовый конвейер показал себя на живом трафике.

Опция версии 2.1.2: требование кириллицы в имени

Релиз маленький по объёму и важный по смыслу: появилась опциональная проверка кириллицы в полях имени.

Что Как работает По умолчанию
language.require_cyrillic поля вида name, fio, first_name должны содержать хотя бы одну кириллическую букву выключено
language.missing_score сколько баллов даёт имя без кириллицы 40
language.max_score потолок баллов этой проверки 40
language.fields список проверяемых полей набор имён по умолчанию

Есть и менее очевидный слой настроек — пороги вердикта. Именно они решают, где заканчивается «подозрительно» и начинается «спам»: одно и то же количество баллов при разных порогах даёт разный код выхода и, соответственно, разное поведение сайта. Менять пороги без контрольных заявок нельзя: слишком низкий порог начнёт блокировать людей, слишком высокий — пропускать спам, и оба эффекта видны только по журналу за несколько дней.

Важная деталь, которую легко не заметить: письма, компания и свободный текст не проверяются никогда — только поля-имена. Иначе правило ловило бы нормальные заявки от людей, которые пишут латиницей.

Автор отдельно, прямо в заметках к релизу, описал исправление: ранее опубликованный архив 2.1.2 собирался из коммита aea672a, в его текстах правило было описано как включённое в эталонных конфигурациях, хотя фактически оно выключено. Архив пересобран из коммита 6a38c45, старый SHA-256 (dce99231…) помечен как заменённый. Это редкий случай, когда исправление не спрятано: «документировано, а не молча».

Как это устроено внутри

flowchart LR
  IN["payload: form, fields, context"] --> CFG{"конфиг задан?"}
  CFG -- нет --> DEF["умолчания: часть проверок выключена"]
  CFG -- да --> RUN["конвейер проверок"]
  DEF --> RUN
  RUN --> HARD{"жёсткий вердикт?"}
  HARD -- да --> SPAM["спам, код 2, stop_on_spam"]
  HARD -- нет --> SUM["сумма баллов против порогов"]
  SUM --> CLEAN["чисто, код 0"]
  SUM --> SUSP["подозрительно, код 1"]
  RUN --> ERR["ошибка проверки: error и пропуск, форма работает, код 3 только у оболочки"]

Каждый шаг возвращает не «да/нет», а объект результата: статус (ham, spam, error, uncertain), баллы и доказательства — какие поля сработали и почему. Именно из этих объектов собирается итоговый вердикт и журнальная строка. Практическая польза видна при разборе ложного срабатывания: не надо воспроизводить заявку вручную — достаточно открыть журнал и прочитать, какая проверка дала баллы.

Порядок проверок — не список «что есть», а именно последовательность: дешёвые шаги впереди, дорогие сзади. Упавшая проверка записывается как error и не блокирует форму: лучше пропустить заявку, чем потерять клиента из-за недоступного API репутации.

Шаг Что происходит Почему в этом месте
WAF если включён — проверка метода, URI, query, полей, cookies, заголовков, тела отсекает явные атаки до разбора формы
Поведение приманка, анализ времени, JS-челлендж не требует внешних данных
Эвристики UA, частота, длина, SQLi, XSS, ссылки, слова, email, повторы локальные, быстрые, детерминированные
Репутация AbuseIPDB, DNSBL сеть, дорого — только если раньше не поймали
Домен SPF, DMARC DNS-запросы
Модель байесовский классификатор по обучающей выборке сайта
flowchart TD
  LANG["language.enabled и require_cyrillic"] --> L1["по умолчанию выключено: правила алфавитов не считаются"]
  SCORE["language.missing_score"] --> CAP["обрезается до language.max_score (40)"]
  NETC["abuseipdb.enabled, dnsbl"] --> NET1["по умолчанию выключено: нет сети в конвейере"]
  WAFC["waf.mode"] --> W1["по умолчанию off; начинать с monitor"]
  LOGPII["log.pii"] --> P1["по умолчанию false: журнал псевдонимизирован"]

Замеры и цифры

Всё ниже получено мной на этой машине: PHP 8.5.0 (CLI, NTS), архив релиза скачан из GitHub и проверен по контрольной сумме.

Что измерено Значение Как измерено
SHA-256 архива 9295006…23eaa, совпал с SHA256SUMS.txt Get-FileHash против файла из релиза
Архив 360 743 байта загрузка gh release download v2.1.2
Версия PHP локально 8.5.0 php -v
Проверок в одном прогоне 12 вывод bin/spam-check на тестовой заявке
Чистая заявка, правило кириллицы выключено clean, балл 0, код 0 запуск с конфигом по умолчанию
Та же заявка, require_cyrillic включён suspicious, балл 40, код 1 запуск с language.enabled=true
missing_score = 60 балл остался 40 обрезка по language.max_score = 40 (Config.php)
Заявка с именем на кириллице clean, балл 0, код 0 тот же конфиг
Явный спам-текст spam, балл 100, код 2 превышение порогов и признак автоматизации
Пакет на Packagist 404 repo.packagist.org/p2/…json и поиск: 0 результатов
vendor/ в архиве отсутствует распаковка архива
composer на машине отсутствует Get-Command composer
PHP Form Spam Checker: ключевые числа
PHP Form Spam Checker 2.1.2: проверки, коды выхода и результаты прогонов

Три вывода из таблицы. Первый: опция 2.1.2 работает ровно как обещано — включили, и латинское имя сдвигает вердикт с clean в suspicious. Второй: missing_score = 60, указанный в заметках как «достаточно для спама», на деле даёт 40, потому что language.max_score по умолчанию 40; чтобы получить жёсткий спам, нужно поднимать именно потолок, а не очко. Третий: composer require из README и AGENTS.md сейчас не сработает — на Packagist пакета нет, а vendor/ в архиве отсутствует, поэтому тесты (php vendor/bin/phpunit) в этой поставке тоже не запустить без установки PHPUnit.

Почему это круто

  • Без капчи. Форму проходит живой человек, а не тот, кто умеет разбирать картинки.
  • Двадцать проверок по порядку. Дешёвые шаги впереди, сетевые — только после них, квота внешнего API не тратится на очевидный спам.
  • Доказательства в вердикте. В ответе видно, какая проверка сработала, сколько дала баллов и на каком поле.
  • Жёсткие вердикты отдельно от суммы. Заполненная приманка или строка curl/ в UA не «складываются» с остальным — это спам сразу.
  • Ноль зависимостей. Библиотека грузится через собственный autoload.php и работает без vendor/.
  • Упавшая проверка не блокирует форму. error фиксируется, а не превращается в отказ живому человеку.
  • Опции по умолчанию выключены. Сеть, WAF и проверка алфавитов включаются осознанно, на своём сайте.
  • Есть CLI-мост. Тот же конвейер вызывается из не-PHP бэкенда через bin/spam-check, коды выхода описаны контрактом.

Установка

Путь Что даёт Работает ли сейчас
composer require paulmann/php-form-spam-checker:2.1.2 библиотека в проект нет: на Packagist пакета нет (404)
архив релиза php-form-spam-checker-2.1.2.tar.gz всё дерево, включая тесты и документацию да, SHA-256 сверяется с SHA256SUMS.txt
клон по тегу v2.1.2 то же плюс git-история да
# проверка требований
php -v                                  # 8.5 или новее
php -m | grep -E 'json|mbstring'        # оба расширения
php bin/spam-check --help               # список опций, код 0

Дальше два способа встроить: скопировать дерево и подключить autoload.php из своего кода, либо использовать готовую точку из integrations/ — guard.php для формы, waf.php для всего запроса, auto-prepend.php для раннего перехвата.

Настройка

Настройка — это файл конфигурации (PHP-массив или JSON) и флаг --config, либо переменная SPAM_CHECKER_CONFIG. Без конфига конвейер тихо работает на умолчаниях, и это первая ловушка эксплуатации.

Ключ По умолчанию Когда менять
language.enabled false русскоязычная форма, где имена всегда кириллицей
language.require_cyrillic false то же, вместе с предыдущим
language.missing_score 40 ослабить или усилить правило
language.max_score 40 поднять, если нужно «спам» одним правилом
pipeline.stop_on_spam true выключить, если нужно собирать все доказательства
abuseipdb.enabled, dnsbl false когда есть ключ и готовность к сетевым запросам
waf.mode off начинать с monitor, не с block
log.pii false оставить выключенным: журнал псевдонимизирован намеренно

Пример конфигурации для сайта, где нужна проверка кириллицы, выглядит так: секция language с enabled и require_cyrillic в true, missing_score равным 40, и путь к хранилищу в домашнем каталоге приложения. Всё остальное можно оставить по умолчанию: базовые проверки уже дают большую часть эффекта, а сеть и модель добавляются позже, когда есть чем их проверить.

Проверка после установки

Перед проверкой полезно понять, что именно вы тестируете. Формулировка «форма не пропускает спам» ничего не значит без контрольных заявок: нужны три эталона (нормальная заявка, заявка с подозрительным признаком и явный спам) и ожидаемые вердикты для них. Тогда любая настройка проверяется сравнением с эталоном, а не на глаз по живому трафику.

Всё это можно сделать без сети и без боевого сайта: CLI-мост принимает JSON и работает на копии конфигурации. Мой прогон занял одну команду на каждый случай, и именно он показал расхождение с примером из заметок к релизу — про missing_score 60 против фактических 40.

Проверка Ожидание Что означает отклонение
php bin/spam-check --help код 0 и список опций код 3 — ошибка окружения или синтаксиса
чистая заявка с конфигом clean, код 0 suspicious на нормальной заявке — порог слишком низкий
заявка с именем латиницей при включённом правиле suspicious, балл 40, код 1 балл 0 — правило не включилось
заявка с явным спамом spam, код 2 код 0 — проверки отключены конфигом
журнал вердиктов одна JSON-строка на отправку, без персональных данных персональные данные в журнале — log.pii включён напрасно

Подводные камни

  • composer require не работает. Пакета на Packagist нет: страница пакета и метаданные отдают 404, поиск — 0 результатов. Установка только из архива релиза или клона.
  • vendor/ в архиве отсутствует. Значит php vendor/bin/phpunit из инструкции требует composer install, а Composer нужен отдельно: в этой поставке тесты «из коробки» не запускаются.
  • missing_score обрезается потолком. Значение 60 в заметках к релизу даёт на практике 40: language.max_score по умолчанию 40, и поднимать нужно его.
  • Без --config конвейер молчит о важном. Отсутствующий конфиг означает умолчания, где сеть, WAF и алфавиты выключены; ничего не сломается, но и защиты будет меньше, чем ожидалось.
  • uncertain — это не «чисто». Пустой результат сетевой проверки означает «не проверялось»: сеть выключена или запрос не удался.
  • WAF по умолчанию off. Если ждали блокировку, её не будет; включать надо осознанно и начинать с monitor.
  • Расхождение в числе тестов. В заметках к релизу — 560 тестов, в README — 559 тестов и 2 015 проверок на конкретном коммите. Разница в один тест не критична, но показывает, что цифры в двух документах считались в разных состояниях дерева.
  • Жёсткие вердикты перебивают сумму. Заполненная приманка или маркер автоматизации в UA — сразу спам, независимо от остальных баллов; при разборе ложных срабатываний смотреть надо на них первыми.
  • Правило про кириллицу ломает формы с иностранной аудиторией. Если форма общая и на ней пишут и по-русски, и по-английски, включённое require_cyrillic будет давать баллы половине нормальных заявок; в такой ситуации правило не включают или делят формы по языку.

Чек-лист внедрения

  1. Проверить окружение: PHP 8.5+, json, mbstring, затем php bin/spam-check --help.
  2. Скачать архив релиза и сверить SHA-256 с SHA256SUMS.txt.
  3. Написать конфиг на сайт: путь к хранилищу и журналу, нужные проверки, сети — по готовности.
  4. Прогнать три заявки: чистую, с латинским именем при включённом правиле, с явным спамом.
  5. Сверить коды выхода с ожиданием: 0, 1, 2; код 3 — ошибка окружения.
  6. Включить журнал вердиктов и убедиться, что персональные данные в него не попадают.
  7. Только потом включать WAF — сначала в режиме monitor, и смотреть, что он поймал бы.

Критерий «работает»: чистая заявка даёт clean и код 0, спам — код 2, спорная — код 1, а в журнале на каждую отправку есть строка с перечнем сработавших проверок.

FAQ

Нужна ли капча вместе с этим конвейером?

Нет, капчи здесь нет вовсе: вместо неё приманка, анализ времени, эвристики и модель. Если спам всё же проходит, ослаблять надо конкретную проверку, а не добавлять капчу сверху.

Почему composer require не находит пакет?

Потому что он не опубликован на Packagist: страница и метаданные отдают 404, поиск по имени даёт ноль результатов. Ставьте из архива релиза или клона по тегу.

Почему балл 40, если я задал missing_score 60?

Потому что у проверки есть потолок language.max_score, по умолчанию 40. Чтобы правило давало больше, поднимайте потолок, иначе значение обрежется.

Что будет, если внешняя проверка недоступна?

Она запишется как error или uncertain и не заблокирует форму. Это осознанный выбор: лучше пропустить заявку, чем потерять живого человека из-за недоступного API.

Можно ли вызывать конвейер не из PHP?

Да: bin/spam-check принимает JSON или base64 в аргументе либо --stdin и возвращает JSON, а коды выхода — часть контракта (0, 1, 2, 3).

Чем language отличается от проверки языка как таковой?

Она смотрит алфавиты: смешение латиницы с кириллицей в одном тексте даёт 20 баллов (максимум 40), а включённое require_cyrillic требует хотя бы одной кириллической буквы в поле имени.

Стоит ли включать проверку байесовским классификатором сразу?

Нет: без обучающей выборки конкретного сайта модель даёт случайные баллы. Сначала работают базовые проверки, затем на живом журнале оценивается доля спорных заявок, и только после этого модель включается на выборке, где есть и спам, и нормальные письма.

Что будет, если в форму пишут люди латиницей?

При включённом require_cyrillic их имена будут получать баллы и могут давать «подозрительно». Поэтому правило выключено по умолчанию и включается только там, где аудитория действительно пишет кириллицей; в остальных случаях работает лишь проверка смешанных алфавитов.

Итог

PHP Form Spam Checker 2.1.2 — зрелая вещь для узкой задачи: он не пытается быть WAF и антивирусом, а честно раскладывает проверку заявки на двадцать шагов с баллами и доказательствами. В версии 2.1.2 появилась ровно одна новая опция — требование кириллицы в поле имени, и она по умолчанию выключена, что для русскоязычных форм правильно: включать её нужно там, где имена действительно пишут кириллицей. Два практических замечания на выходе: пакета нет на Packagist (установка только из архива, а vendor/ в архиве отсутствует), и missing_score в примерах стоит читать вместе с потолком max_score.

Цитата: Антиспам без объяснений — это лотерея: ценно не то, что форма заблокировала заявку, а то, что она может сказать, какая проверка и почему это сделала.

Связанные материалы

LLM Summary

  • Тема: PHP Form Spam Checker 2.1.2 — PHP-библиотека и CLI с конвейером из 20 проверок для форм, без капчи и без зависимостей Composer во время работы.
  • Требования: PHP 8.5+, ext-json, ext-mbstring; лицензия MIT.
  • Коды выхода: bin/spam-check — 0 чисто, 1 подозрительно, 2 спам, 3 ошибка; bin/waf-check — 0 пропуск, 2 блок, 3 ошибка.
  • Новое в 2.1.2: опциональная проверка language.require_cyrillic — поля имени должны содержать кириллицу; language.enabled и require_cyrillic по умолчанию false.
  • Мои замеры (PHP 8.5.0): SHA-256 архива совпал с SHA256SUMS.txt; чистая заявка — clean, 0, код 0; та же заявка с включённым правилом — suspicious, 40, код 1; missing_score = 60 дал 40 из-за потолка language.max_score; спам-заявка — spam, 100, код 2; на Packagist 404 и поиск даёт 0 результатов; vendor/ в архиве отсутствует.
  • Грабли: composer require из README не работает; тесты требуют composer install; отсутствие --config означает умолчания с выключенными сетью, WAF и алфавитами; uncertain значит «не проверялось», а не «чисто»; WAF по умолчанию off.
  • Расхождение в документации: заметки к релизу говорят о 560 тестах, README — о 559 тестах и 2 015 проверках на конкретном коммите.
  • Ссылки: https://github.com/paulmann/PHP-Form-Spam-Checker, https://github.com/paulmann/PHP-Form-Spam-Checker/releases/tag/v2.1.2

Промт для агента (установка и настройка)

Готовый промт для AI-агента: скопируйте блок целиком кнопкой и отдайте своему ассистенту. Шаги, коды выхода и ловушки взяты из AGENTS.md репозитория — это инструкция автора для агентов, а не пересказ.

Задача: подключить PHP Form Spam Checker к форме на сайте и доказать, что конвейер различает чистую заявку, спорную и спам.

Материалы:
- репозиторий: https://github.com/paulmann/PHP-Form-Spam-Checker
- релиз: https://github.com/paulmann/PHP-Form-Spam-Checker/releases/tag/v2.1.2
- контракт исполнителя: https://github.com/paulmann/PHP-Form-Spam-Checker/blob/main/AGENTS.md

Установка (Packagist не содержит пакет — это проверено: 404):
gh release download v2.1.2 -R paulmann/PHP-Form-Spam-Checker -D ./rel
# сверить SHA-256 архива с SHA256SUMS.txt
tar -xzf rel/php-form-spam-checker-2.1.2.tar.gz -C /opt

Проверка перед использованием:
php -v                        # 8.5 или новее
php -m | grep -E 'json|mbstring'
php bin/spam-check --help     # код 0 и список опций

Вызов (неинтерактивно, секреты только из окружения):
printf '%s' "$PAYLOAD" | php bin/spam-check --config=/etc/spam.php --form=contact --stdin --json

Коды выхода — контракт: spam-check 0 чисто, 1 подозрительно, 2 спам, 3 ошибка; waf-check 0 пропуск, 2 блок, 3 ошибка. Изменение кода — повод остановиться и сообщить, а не подстраиваться.

Настройка для русскоязычной формы (по умолчанию правило выключено):
'language' => ['enabled' => true, 'require_cyrillic' => true, 'missing_score' => 40, 'max_score' => 40]
# missing_score обрезается до max_score: чтобы правило давало «спам», поднимать нужно max_score

Запрещено:
- включать log.pii и публиковать сырые заявки: журнал псевдонимизирован намеренно;
- направлять examples/ или CLI-пробу на боевой сайт или реальный ящик;
- включать WAF сразу в режиме block: начинать с monitor;
- добавлять зависимость Composer времени выполнения;
- считать uncertain доказательством чистоты: это «не проверялось»;
- запускать без --config или SPAM_CHECKER_CONFIG: умолчания выключают сеть, WAF и алфавиты.

Что считать успехом: чистая заявка даёт clean и код 0, заявка с латинским именем при включённом правиле — suspicious и код 1, явный спам — код 2, в журнале на каждую отправку одна строка с перечнем сработавших проверок и без персональных данных.