fix(bot): сторож резал ПРАВДУ про минимальный заказ — «от 1 номера» считал выдумкой
Живая проверка на боевом сразу после выката: «Можно купить всего 5 номеров?» — бот ответил обрубком «Платите только за то, что реально получите». Две мои же ошибки: 1. Правило про минимальный заказ было слишком грубым и резало ЧЕСТНУЮ формулировку «минимальный заказ — от 1 номера». Теперь режем только выдуманный минимум БОЛЬШЕ одного («минимальный заказ — 100 заявок», «пакет от 500 штук»). 2. Сторож собирал разрешённые числа ТОЛЬКО цифрами. В статье написано словом («продаём любой объём, хоть ОДНУ штуку»), бот ответил цифрой («от 1 номера») — и число 1 сочлось выдуманным. Теперь слово-число в материалах разрешает цифру в ответе; добавлены падежные формы (одну, одного, двух, трёх). Живьём: «Можно купить всего 5 номеров?» → «Минимального заказа нет — можно купить хоть одну заявку, хоть пять, хоть сколько угодно». Тесты: бот 259/259, весь бэкенд 2924/2928 (0 падений). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -108,10 +108,12 @@ class AnswerGuard
|
||||
// статьях этого нет вовсе) и делил одно на два — клиент слышит отказ там, где отказа нет.
|
||||
'/не\s+прода[её]м[^.!?]{0,40}(поштучн\w*|по\s+одн\w+|отдельн\w+\s+номер\w*)/iu',
|
||||
'/(номер\w*|лид\w*)[^.!?]{0,30}(поштучн\w*|по\s+штук\w*)[^.!?]{0,20}не\s+прода/iu',
|
||||
// NB: «минимального заказа НЕТ» — правда, её резать нельзя. Поэтому ловим только
|
||||
// УТВЕРЖДЕНИЕ о минимуме: «минимальный заказ — 100 заявок», «пакет от 500 штук».
|
||||
'/минимальн\w+\s+(объ[её]м|заказ|партия|пакет)\w*[^.!?]{0,20}(—|-|:|от|составля\w+|\d)/iu',
|
||||
'/(пакет\w*|партия\w*)\s+от\s+\d+/iu',
|
||||
// NB: «минимального заказа НЕТ» и «минимальный заказ — одна заявка» — ПРАВДА, резать
|
||||
// нельзя (живая проверка на боевом 14.07.2026: сторож съел верный ответ, остался
|
||||
// обрубок «Платите только за то, что получите»). Ловим только выдуманный минимум
|
||||
// БОЛЬШЕ ОДНОГО: «минимальный заказ — 100 заявок», «пакет от 500 штук».
|
||||
'/минимальн\w+\s+(объ[её]м|заказ|партия|пакет)\w*[^.!?]{0,20}(?!1\b)([2-9]|\d{2,})/iu',
|
||||
'/(пакет\w*|партия\w*)\s+от\s+(?!1\b)([2-9]|\d{2,})/iu',
|
||||
// 🔴 Владелец 14.07.2026: сколько заявок выйдет на подарочную 1000 ₽ — НЕ ОБЕЩАЕМ и
|
||||
// числа не называем (besplatnyy-test.md). Сколько придёт, зависит от источника, а на
|
||||
// тесте по своему сайту их обычно единицы. «Несколько десятков» — враньё в лицо.
|
||||
@@ -129,11 +131,14 @@ class AnswerGuard
|
||||
* Числа словами. Модель их не всегда переводит в цифры («у вас два проекта» вместо
|
||||
* «2 проекта»), а клиенту от формы всё равно — считаем так же, как цифру.
|
||||
*/
|
||||
// NB: падежные формы обязательны. Живая проверка 14.07.2026: в статье написано «продаём
|
||||
// любой объём, хоть ОДНУ штуку», а бот отвечал «от 1 номера» — сторож не знал формы
|
||||
// «одну», числа 1 в разрешённых не увидел и вырезал ПРАВДУ, оставив обрубок.
|
||||
private const WORD_NUMBERS = [
|
||||
'ноль' => '0',
|
||||
'один' => '1', 'одна' => '1', 'одно' => '1',
|
||||
'два' => '2', 'две' => '2',
|
||||
'три' => '3',
|
||||
'один' => '1', 'одна' => '1', 'одно' => '1', 'одну' => '1', 'одного' => '1', 'одной' => '1',
|
||||
'два' => '2', 'две' => '2', 'двух' => '2',
|
||||
'три' => '3', 'трёх' => '3', 'трех' => '3',
|
||||
'четыре' => '4',
|
||||
'пять' => '5',
|
||||
'шесть' => '6',
|
||||
@@ -770,9 +775,21 @@ class AnswerGuard
|
||||
{
|
||||
preg_match_all('/\d[\d\s]{0,9}\d|\d/u', $context, $matches);
|
||||
|
||||
return array_values(array_unique(array_map(
|
||||
$numbers = array_map(
|
||||
fn (string $n) => (string) preg_replace('/\s+/u', '', $n),
|
||||
$matches[0] ?? []
|
||||
)));
|
||||
);
|
||||
|
||||
// 🔴 Живая проверка на боевом 14.07.2026: в статье написано словом («продаём любой
|
||||
// объём, хоть ОДНУ штуку»), бот ответил цифрой («от 1 номера») — и сторож вырезал
|
||||
// ПРАВДУ, потому что видел в материалах только цифры. Слово-число в материалах —
|
||||
// такое же разрешение, как цифра.
|
||||
$wordAlt = implode('|', array_keys(self::WORD_NUMBERS));
|
||||
preg_match_all('/\b('.$wordAlt.')\b/iu', $context, $words);
|
||||
foreach ($words[1] ?? [] as $word) {
|
||||
$numbers[] = $this->normalizeNumberToken($word);
|
||||
}
|
||||
|
||||
return array_values(array_unique($numbers));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,19 @@ it('число, названное самим клиентом, сторож н
|
||||
expect($guard->clean($text, $context, personalFactsKnown: false))->toContain('5 номеров');
|
||||
});
|
||||
|
||||
it('правду «минимальный заказ — одна заявка» сторож не режет, а выдуманный минимум режет', function () {
|
||||
// Живая проверка на боевом 14.07.2026: сторож съел верный ответ про 5 номеров, остался
|
||||
// обрубок «Платите только за то, что получите». Виновата была моя же грубая регулярка.
|
||||
$guard = app(AnswerGuard::class);
|
||||
$ctx = 'Минимального заказа нет: продаём любой объём, хоть одну штуку.';
|
||||
|
||||
$true = 'Да, конечно. Минимальный заказ — от 1 номера, берите сколько нужно.';
|
||||
expect($guard->clean($true, $ctx, personalFactsKnown: false))->toContain('от 1 номера');
|
||||
|
||||
$lie = 'Минимальный заказ — 100 заявок в месяц.';
|
||||
expect($guard->clean($lie, $ctx, personalFactsKnown: false))->not->toContain('100 заявок');
|
||||
});
|
||||
|
||||
it('сколько заявок выйдет на подарочную тысячу — бот не обещает', function () {
|
||||
$guard = app(AnswerGuard::class);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user