Коротко. Сервис открывает чужой сайт и считает штраф по КоАП. Внутри двухуровневая проверка по 22 правилам: дешёвые эвристики на cheerio там, где хватает регулярок, и Claude через российский прокси там, где нужно понять смысл текста. На выходе балл от 0 до 100, список нарушений со статьями КоАП и вилкой штрафа. Законы в основе: 152-ФЗ о персональных данных, 242-ФЗ о локализации, закон о рекламе, закон о защите прав потребителей. Ниже стек, архитектура и грабли. Самая обидная грабля в том, что \b в JavaScript не образует границу слова перед кириллицей, и детектор молча слепнет на русском тексте.

Зачем я вообще это делал

У меня небольшой сервисный центр по ремонту техники. В какой‑то момент дошли руки до юридической части собственного сайта, я полез читать 152-ФЗ, и довольно быстро стало ясно: требований к обычному сайту с формой заявки куда больше, чем кажется, и разбросаны они по нескольким законам.

В мае 2025 КоАП по части персональных данных переписали: штрафы выросли в разы, появились оборотные за повторные нарушения, отдельная крупная санкция за утечки. При этом требований к сайту наберётся больше двадцати, и они размазаны по нескольким законам и подзаконным актам. Юрист за разовый аудит просит десятки тысяч. Малый бизнес такое не заказывает, пока не прилетит проверка.

Захотелось собрать что‑то на стыке юриста и линтера: автоматический сканер, который читает сайт и говорит «вот тут нарушение, вот статья, вот порядок суммы». Сначала для себя и своего центра, потом понял, что это нужно не только мне — так появился sitelaw.ru.

Сразу оговорюсь: я не юрист. Балл и вилка штрафа в сервисе это эвристика и оценка порядка риска, а не юридическое заключение. Это важная рамка, к ней ещё вернусь.

Стек

Next.js 16.2.4 + React 19 (App Router)
Playwright (headless Chromium)  рендер JS, перехват сетевых запросов, проверка SSL
cheerio                          статический разбор HTML
@anthropic-ai/sdk                Claude через российский прокси api.proxyapi.ru
@react-pdf/renderer              экспорт отчёта в PDF
PostgreSQL (на VPS)              история проверок, биллинг
shadcn/ui + Tailwind v4          интерфейс
ЮKassa                           платежи
DaData                           автозаполнение реквизитов по ИНН

Хостинг: VPS Beget B, 2 ГБ RAM плюс 4 ГБ swap, nginx ведёт на Next.js через PM2.

Отдельная боль: сборка. Next.js 16 с turbopack на двух гигабайтах падает по памяти. Поэтому билд я делаю локально на Mac, а на сервер уходит rsync собранной .next/. Для маленького сервиса это рабочий компромисс, хотя и нелюбимый.

Первая версия, которая хоть что‑то показывала, собралась за пару часов. А вот калибровка норм, чтобы детектор ссылался на правильную часть статьи и не врал в суммах штрафов, тянется до сих пор. Дальше как раз про то, почему.

Архитектура: эвристики там, где можно, LLM там, где нельзя

Главное решение проекта это разделение проверки на два уровня по стоимости и природе задачи.

Уровень 1: эвристики на cheerio

Большая часть правил написана обычным кодом. Каждое правило это отдельный файл в lib/rules/*.ts с функцией check(ctx), которая возвращает результат. Примеры:

  • https-ssl.ts смотрит наличие HTTPS, валидность сертификата, версию TLS;

  • cookie-consent.ts ищет баннер согласия и проверяет его структуру: информационная плашка без кнопок это не согласие, одна кнопка «Принять» без альтернативы тоже;

  • legal-requisites.ts ищет на странице ИНН, ОГРН, название юрлица;

  • foreign-services.ts ловит запрещённые и ограниченные сервисы по списку из 46 штук: Google Analytics, Meta Pixel, разные зарубежные шрифты, капчи, виджеты.

Эвристики дешёвые: миллисекунды на сайт, ноль токенов, детерминированный результат. Проблема у них одна, зато большая: ложные срабатывания на регулярках. Про две самые показательные расскажу в разделе про грабли, там интереснее.

Уровень 2: LLM на том, что регуляркой не возьмёшь

Часть вопросов регуляркой решить нельзя в принципе:

  • соответствует ли политика конфиденциальности 152-ФЗ по структуре, а не просто «есть файл по ссылке». Это надо понимать смысл текста на нескольких страницах;

  • к какому типу относится сайт и не попадает ли он под особые требования (медицина, интернет‑магазин, площадка с пользовательским контентом);

  • какой общий риск с учётом всех нарушений вместе.

Здесь подключается Claude. Я использую две модели на три задачи: дешёвую haiku на классификацию и sonnet на оба смысловых разбора, чтобы не платить за классификацию столько же, сколько за глубокий анализ:

Задача

Модель

Классификация типа сайта

claude haiku 4.5

Разбор политики ПДн

claude sonnet 4.6

Сводный риск‑профиль

claude sonnet 4.6

Free‑тариф работает только на эвристиках, без LLM вообще. Это сознательное решение: анонимная проверка должна быть почти бесплатной для меня, иначе любой всплеск трафика вынесет кассу. Полный пайплайн с двумя вызовами sonnet включается только на платных проверках.

Почему российский прокси, а не Anthropic напрямую

Тут техническое решение упирается прямо в предмет сервиса. Я делаю инструмент, который ругает чужие сайты за трансграничную передачу персональных данных без оснований. Если бы я сам слал HTML проверяемых сайтов напрямую в Anthropic в США, я бы делал ровно то, за что мой же сканер снижает балл.

Поэтому запросы к Claude идут через api.proxyapi.ru, российский прокси с оплатой в рублях. Прокси берёт свою наценку, в итоге одна платная проверка обходится примерно в 12 ₽ против сырой токенной цены около двух центов. Дороже, но это согласуется и с позиционированием, и с поданным уведомлением в Роскомнадзор. Бесплатная проверка на одних эвристиках стоит около 0,1 ₽, фактически только классификатор.

prompt caching: главная оптимизация по деньгам

Системный промпт с правилами, шаблонами политик и примерами это несколько тысяч токенов. Без кеша каждая проверка платит за них заново.

Кешируется именно системный префикс с правилами, он передаётся отдельным параметром system. Уникальный HTML проверяемой страницы идёт в messages и не кешируется:

const system = [
  { type: "text", text: SYSTEM_RULES, cache_control: { type: "ephemeral" } },
];
const messages = [
  { role: "user", content: SITE_HTML }, // уникальный для каждой проверки
];

Проверки идут пачкой: пользователь запустил, правила пошли почти одновременно. Поэтому попаданий в кеш много, и стоимость промптовой части падает в несколько раз. Важная деталь, на которой я сначала споткнулся: кешируемый блок должен быть длинным. Минимальный размер для Sonnet 4.6 это 1024 токена. Поставишь cache_control на короткий префикс, и кеш не сработает, причём без ошибки: cache_creation_input_tokens и cache_read_input_tokens в ответе просто придут нулями.

Грабли, ради которых стоит читать дальше

1. \b в JavaScript не образует границу перед кириллицей

Детектор реквизитов искал ИНН и ОГРН по очевидной на вид регулярке вроде /\bИНН\b/. И тихо не находил их вообще ни на одном русском сайте.

Причина в том, что \b в JavaScript опирается на ASCII‑определение «словесного символа». Кириллическая буква для него не словесный символ, поэтому граница \b перед «И» в слове «ИНН» не возникает никогда. Регулярка не падает, не кидает ошибку, она просто молча не матчит. Самый неприятный класс багов: всё зелёное, тесты на латинице проходят, а на проде детектор слепой.

Чинил через negative lookbehind на русскую букву. Заодно это отсекает ложные хвосты вроде «длинный», «старинный», «финн», где «инн» сидит внутри слова:

const m = text.match(/(?<![А-Яа-яЁё])ИНН[\s:№-]*\d{10,12}(?!\d)/i);

Штатное решение тут это флаг u и Unicode property escapes (\p{L}, ES2018). Я выбрал negative lookbehind на класс [А-Яа-яЁё], потому что мне нужно было заодно отсечь хвосты вроде «длинный» и «финн». Мораль простая и злая: если парсите русский текст регулярками, \b и \w по умолчанию вас обманут на кириллице, об этом надо помнить отдельно.

2. Case‑insensitive G- поймал --bg-card

Детектор Google Analytics 4 искал идентификатор формата G-XXXXXXXXXX. В первой версии регулярка была регистронезависимой. И она радостно сработала на CSS‑переменной --bg-card: внутри bg-card сидит подстрока g-c, а с флагом i это совпадает с G-.

В итоге сервис обвинял в трансграничной передаче данных сайты, у которых из «аналитики» был только аккуратный дизайн на CSS‑переменных.

Чинил тремя ограничениями сразу: убрал регистронезависимость, добавил границу слова и оперся на длину идентификатора (в подавляющем большинстве GA4-ID после G- идёт 10 алфанумериков). Это эвристика, а не гарантированная спецификация Google, поэтому я страхуюсь сильным сигналом рядом:

idPatterns: [/\bG-[A-Z0-9]{10}\b/, /\bUA-\d{4,10}-\d+\b/]

Плюс ввёл уровень «сильных» сигналов: вызов gtag(, домен googletagmanager.com. ID‑паттерн засчитывается только когда рядом на странице есть сильный сигнал. Регулярка без контекста это лотерея.

3. Я.Метрика как ложное «нарушение» на собственном сайте

Метрика хостится в России, поэтому по локализации (242-ФЗ) в трансграничный детектор она попадать не должна. Но мой же детектор однажды сработал на ней как на трансграничной аналитике, потому что она местами инициализируется похожим на gtag способом. Это был ложный матч именно трансграничного правила, а не индульгенция Метрике вообще. Лечилось белым списком российских доменов аналитики, который проверяется до того, как сработает общий детектор.

4. Символ рубля, которого нет в шрифте

Первая версия PDF‑отчёта вместо «₽» рисовала «½». Roboto в варианте cyrillic‑ext не содержит символ U+20BD. Пришлось добавить санитайзер, который перед рендером PDF заменяет «₽» на “ руб.«, бесконечность на „без ограничений“, эмодзи вырезает. Мелочь, которая выглядела как издевательство ровно в том месте отчёта, где написана сумма штрафа.»

5. Самая дорогая грабля оказалась не технической, а юридической

Правило может быть идеально написано как код и при этом ссылаться не на ту часть статьи. У меня детектор cookie‑согласия одно время был привязан к части про «совмещение согласий», хотя ловил он отсутствие согласия как такового, а это другой состав и другая вилка штрафа. Код отрабатывал верно, дефект был именно в привязке к норме: её нужно сверять руками, а не доверять первой похожей статье.

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

6. react‑pdf и длинные отчёты

Открытая до сих пор бяка: при девятнадцати и более нарушениях один из блоков react‑pdf перестаёт разрываться по страницам и уезжает за край. На обычном сайте нарушений меньше, но самые «богатые» экземпляры в отчёт не влезают красиво. Лежит в бэклоге с пометкой починить до того, как пойдут платные продажи в товарных количествах.

Что показал прогон, или зачем вообще брать чужие сайты

Детектор без проверки на реальных данных это гипотеза. Чтобы убедиться, что правила ловят осмысленные вещи, а не раздают случайные баллы, я взял одну B2B‑выборку примерно из восьмидесяти сайтов и прогнал её одним заходом по единой формуле. Картина вышла депрессивная и довольно однородная: средний балл около 38 из 100, выше семидесяти набрал ровно один сайт из восьмидесяти. Самые частые срабатывания везде одни и те же: отсутствие или фрагментарность обязательных реквизитов, форма с одним общим чекбоксом вместо раздельных согласий, cookie‑баннер для галочки, сторонние шрифты и аналитика без оснований.

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

Чего ещё нет

Чтобы не создавать впечатление законченного продукта, честный список дыр:

  • Регулярный мониторинг. Подписочные тарифы обещают периодическую перепроверку с алертами. Этого функционала пока нет, он первый в очереди. Сейчас сервис умеет разовую проверку.

  • Тот самый react‑pdf на больших отчётах из пункта 6 грабель.

  • GEO и llms.txt. Поисковики сайт уже видят, а вот citability для ИИ‑ответов я ещё не настроил.

  • Парсер свежих штрафов. Хочется показывать пользователю реальные недавние кейсы «вот за это уже штрафуют», пока руки не дошли.

Итог

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

Разбор устройства правил и критику стека читаю в комментариях, на граблях я явно ещё не закончил.

Павел