From c351b0b6f5b2f1eacaa43901ed10b028b97d8f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 6 Aug 2026 23:53:05 +0300 Subject: [PATCH] =?UTF-8?q?fix=20=D0=BE=D0=B1=D0=B7=D0=B2=D0=BE=D0=BD:=20?= =?UTF-8?q?=D0=B4=D0=B2=D0=B5=D1=80=D1=8C=20=D0=B8=D1=82=D0=BE=D0=B3=D0=B0?= =?UTF-8?q?=20=D1=81=D1=82=D0=BE=D1=80=D0=BE=D0=B6=D0=B5=D0=B9=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82=20=D1=81=D0=B2?= =?UTF-8?q?=D1=8F=D0=B7=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=82=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE,=20=D1=87=D1=82=D0=BE=20=D0=BF=D1=80=D0=B8=D0=BD=D1=8F?= =?UTF-8?q?=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дверь принимала итог, который сам себе противоречил, и записывала «всё хорошо». Годный по форме случай: тело говорит «сторожей 3, красных 2», а в списке лежит одна зелёная строка — двое сторожей исчезали молча. Дыры было две: - длина списка storozha ни с чем не сверялась; - поле storozhey_krasnyh не читалось вовсе — машина присылала своё показание, а дверь его выбрасывала. Теперь сверяются три показания о числе сторожей — настройка, объявленное число и длина списка — плюс объявленное число красных против нашего подсчёта по строкам. Красных считаем ровно по машинному правилу: код не ноль или провалов не ноль. Иначе зелёный сторож с нулём проверок давал бы ложную тревогу, потому что портал ругается на такого, а машина его красным не считает. Сверх заказанного закрыты два места того же рода. Поля, которых дверь не знает, ищутся вычитанием, а не перечислением — и в итоге, и в строке сторожа: тогда забытое поле оказывается под защитой, а не мимо неё. И ловится список нужной длины, набитый повторами одного имени: числа при этом сходятся все до одного, а гоняли одного сторожа трижды. Каждая новая проверка показана красной вырезанием своего куска по одному за раз. Без чтения storozhey_krasnyh случай с объявленной краснотой при трёх зелёных строках не ловит ничто — список бед выходит пустым. Машину не трогал, схему не трогал, остальную дверь не переписывал. Полный прогон: всего 5207, зелёных 5203, красных 0, ошибок 0, пропущено 4. --- .../Api/LenaStorozhaWebhookController.php | 102 +++++++ .../Feature/Lena/StorozhaMashinyTest.php | 104 +++++++ bots/lena-golos/README.md | 33 +++ ...-pomoshchnika-z-0-10-dodelka-2026-08-06.md | 266 ++++++++++++++++++ 4 files changed, 505 insertions(+) create mode 100644 docs/superpowers/priyomka/stroyka-6/otchyot-pomoshchnika-z-0-10-dodelka-2026-08-06.md diff --git a/app/app/Http/Controllers/Api/LenaStorozhaWebhookController.php b/app/app/Http/Controllers/Api/LenaStorozhaWebhookController.php index f7ac9fe1..74737560 100644 --- a/app/app/Http/Controllers/Api/LenaStorozhaWebhookController.php +++ b/app/app/Http/Controllers/Api/LenaStorozhaWebhookController.php @@ -50,6 +50,34 @@ class LenaStorozhaWebhookController extends Controller /** Насколько дата итога может опережать наши часы, прежде чем мы ей не поверим. */ private const ZAPAS_VPERYOD_MINUT = 10; + /** + * Поля итога, которые дверь знает ПОИМЁННО. + * + * 🔴 Список нужен, чтобы считать проверенное ВЫЧИТАНИЕМ, а не перечислением. + * Перечисление («проверяем вот эти поля») оставляет всё остальное мимо + * защиты: добавят на машине новое поле, забудут добавить сюда — и оно + * молча проедет. Вычитание («всё, чего нет в этом списке, — новость») + * ставит забытое поле ПОД защиту: дверь заругается, пока человек не + * посмотрит на него и не впишет сюда осознанно. + * + * Цена решения названа честно: безобидная прибавка поля на машине красит + * итог до правки этого списка. Так же устроено и число ожидаемых сторожей + * в настройке — прибавление подтверждает человек, а не догадка портала. + */ + private const ZNAKOMYE_POLYA_ITOGA = [ + 'shema', // разновидность итога — проверена отдельно + 'sobrano_v', // дата сборки — разбирается в receive() + 'mashina', // кто прислал — знаем, нарочно не проверяем + 'dlilos_ms', // сколько длился обход — ложится в отметку + 'storozhey_vsego', // объявленное число сторожей + 'storozhey_krasnyh', // объявленное число красных + 'slepok_etalonov', // слепок папки — сверить не с чем, лежит для истории + 'storozha', // сами строки сторожей + ]; + + /** Поля строки сторожа, которые дверь знает поимённо. Остальное — новость. */ + private const ZNAKOMYE_POLYA_STOROZHA = ['imya', 'kod', 'proverok', 'provalov']; + public function receive(Request $request): JsonResponse { if (! $this->podpisVerna($request)) { @@ -93,6 +121,25 @@ class LenaStorozhaWebhookController extends Controller /** * Что не так с итогом. Пустой список = всё зелено. * + * 🔴 ЧТО ЗДЕСЬ ЛЕГКО ПОТЕРЯТЬ ПРИ ПРАВКЕ — СВЯЗНОСТЬ ТОГО, ЧТО ПРИНЯЛИ. + * Первый заход этой двери стоял на допущении «список storozha ПОЛОН, + * поэтому красноту можно пересчитать по нему, а собственное число красных + * из итога читать незачем». Допущение оказалось ложным дважды: + * + * 1. Длина списка ни с чем не сверялась. Тело «сторожей 3», а в списке + * одна зелёная строка — двое сторожей исчезали, и портал записывал + * «всё хорошо». + * 2. Поле storozhey_krasnyh не читалось вовсе. Машина присылала своё + * показание, а дверь его выбрасывала: тело «красных 3» при трёх + * зелёных строках проезжало насквозь. + * + * Поэтому здесь сверяются ТРИ независимых показания об одном и том же: + * сколько сторожей ждёт настройка, сколько объявил итог и сколько строк + * в списке на самом деле. Расхождение любых двух — само по себе беда, + * даже когда каждая строка по отдельности зелёная. Это не дублирование: + * согласие трёх показаний и есть единственное доказательство, что список + * не обрезан и не подменён. + * * @param array $itog * @return list */ @@ -106,6 +153,13 @@ class LenaStorozhaWebhookController extends Controller return $bedy; // Дальше разбирать нечего — устройство файла нам неизвестно. } + // Вычитанием, а не перечислением: всё, чего дверь не знает поимённо, — + // новость, о которой надо сказать вслух. См. ZNAKOMYE_POLYA_ITOGA. + $novyePolya = array_values(array_diff(array_keys($itog), self::ZNAKOMYE_POLYA_ITOGA)); + if ($novyePolya !== []) { + $bedy[] = 'в итоге поля, которых дверь не знает: '.implode(', ', $novyePolya); + } + $zhdyom = (int) config('storozha_mashiny.ozhidaem_storozhey'); $vsego = (int) ($itog['storozhey_vsego'] ?? -1); @@ -122,25 +176,73 @@ class LenaStorozhaWebhookController extends Controller return $bedy; } + // Второе показание против первого: список обязан быть той длины, + // какую итог сам же и объявил. Короткий список = сторожа исчезли. + if (count($spisok) !== $vsego) { + $bedy[] = 'строк в списке сторожей '.count($spisok).", а объявлено {$vsego}"; + } + + $krasnyhPoSpisku = 0; + $imena = []; + $novyePolyaStrok = []; + foreach ($spisok as $s) { if (! is_array($s)) { $bedy[] = 'строка сторожа не разобрана'; + // Не разобрали — считаем красной. Звать её зелёной не за что, + // и наш счёт красных обязан остаться сравнимым с машинным. + $krasnyhPoSpisku++; continue; } + + $novyePolyaStrok = array_merge( + $novyePolyaStrok, + array_diff(array_keys($s), self::ZNAKOMYE_POLYA_STOROZHA), + ); + $imya = (string) ($s['imya'] ?? '(без имени)'); + $imena[] = $imya; $kod = (int) ($s['kod'] ?? -1); $proverok = (int) ($s['proverok'] ?? -1); $provalov = (int) ($s['provalov'] ?? -1); if ($kod !== 0 || $provalov !== 0) { + // 🔴 Красным считаем РОВНО ПО ТОМУ ЖЕ правилу, по какому считает + // обход на машине (код не ноль ИЛИ провалов не ноль). Иначе + // сверка двух чисел ниже ругалась бы на ровном месте. + $krasnyhPoSpisku++; $bedy[] = "{$imya}: код {$kod}, проверок {$proverok}, красных {$provalov}"; } elseif ($proverok <= 0) { // Зелёный сторож, не сделавший ни одной проверки, — не сторож. + // 🔴 В счёт красных НЕ идёт: машина такого красным не считает, + // а мы сверяемся именно с её счётом. $bedy[] = "{$imya}: код 0, но проверок {$proverok} — сторож ничего не проверил"; } } + if ($novyePolyaStrok !== []) { + $bedy[] = 'в строках сторожей поля, которых дверь не знает: ' + .implode(', ', array_values(array_unique($novyePolyaStrok))); + } + + // Длину списка можно добрать повторами, и тогда все числа сойдутся, + // а гоняли одного сторожа трижды. Имена сторожей — имена файлов на + // машине, повторяться они не могут. + $povtory = array_keys(array_filter(array_count_values($imena), static fn (int $skolko): bool => $skolko > 1)); + if ($povtory !== []) { + $bedy[] = 'в списке сторожей повторяются имена: '.implode(', ', $povtory); + } + + // Третье показание: машина сама сказала, скольких считает красными. + // Не сошлось с нашим счётом — беда, даже если каждая строка зелена. + if (! array_key_exists('storozhey_krasnyh', $itog)) { + $bedy[] = 'в итоге нет числа красных — наш счёт сверить не с чем'; + } elseif ((int) $itog['storozhey_krasnyh'] !== $krasnyhPoSpisku) { + $objavleno = (int) $itog['storozhey_krasnyh']; + $bedy[] = "красных по списку {$krasnyhPoSpisku}, а объявлено {$objavleno}"; + } + return $bedy; } diff --git a/app/tests/Feature/Lena/StorozhaMashinyTest.php b/app/tests/Feature/Lena/StorozhaMashinyTest.php index a74f1cdb..46ce007b 100644 --- a/app/tests/Feature/Lena/StorozhaMashinyTest.php +++ b/app/tests/Feature/Lena/StorozhaMashinyTest.php @@ -208,6 +208,110 @@ it('краснеет на неизвестной разновидности ит expect(otmetka()->last_error)->toContain('непонятная разновидность'); }); +// ======================================= СВЯЗНОСТЬ ТОГО, ЧТО ПРИНЯЛИ ======== +// +// 🔴 Нож владельца по допущению, на котором стояла защита: «список storozha +// ПОЛОН, поэтому красноту можно пересчитать по нему, а собственное число +// красных из итога читать незачем». Оба случая ниже годны по форме — +// разновидность 1, дата свежая, подпись верна, адрес разрешён — и оба +// проходили насквозь: портал записывал «всё хорошо», хотя машина в том же +// теле прямо сказала обратное. + +it('🔴 краснеет, когда в списке сторожей МЕНЬШЕ строк, чем объявлено', function (): void { + // Случай Б ножа владельца: объявлено трое, в списке ОДИН зелёный. + // Двое сторожей просто исчезли, и до починки портал молчал. + $telo = (string) json_encode([ + 'shema' => 1, + 'sobrano_v' => now()->toIso8601String(), + 'mashina' => 'fhmg5qpo4jconna4s9jo', + 'dlilos_ms' => 41000, + 'storozhey_vsego' => 3, + 'storozhey_krasnyh' => 2, + 'slepok_etalonov' => str_repeat('c', 64), + 'storozha' => [ + ['imya' => 'proverka-raspisaniya', 'kod' => 0, 'proverok' => 26, 'provalov' => 0], + ], + ], JSON_UNESCAPED_UNICODE); + + postuchat($telo)->assertStatus(202); + + // 🪤 Приводим к строке нарочно. На голом toContain() непочиненная дверь даёт + // last_error = NULL, и Pest падает «Expected [iterable]» — по такому + // красному не видно, дыра это или сторож сам сломан. Со строкой красный + // честный: «'' не содержит ...». + $o = otmetka(); + expect((string) $o->last_error)->toContain('строк в списке сторожей 1, а объявлено 3'); + expect($o->last_success_at)->toBeNull() + ->and((int) $o->consecutive_failures)->toBe(1); +}); + +it('🔴 краснеет, когда объявленное число красных не сходится со списком', function (): void { + // Случай В ножа владельца: машина объявила «красных 3», а все три строки + // зелёные. Портал обязан заметить, что итог сам себе противоречит, а не + // выбрасывать показание, которое ему дали. + $telo = (string) json_encode([ + 'shema' => 1, + 'sobrano_v' => now()->toIso8601String(), + 'mashina' => 'fhmg5qpo4jconna4s9jo', + 'dlilos_ms' => 41000, + 'storozhey_vsego' => 3, + 'storozhey_krasnyh' => 3, + 'slepok_etalonov' => str_repeat('d', 64), + 'storozha' => [ + ['imya' => 'proverka-raspisaniya', 'kod' => 0, 'proverok' => 26, 'provalov' => 0], + ['imya' => 'proverka-tekstovogo-sleda', 'kod' => 0, 'proverok' => 46, 'provalov' => 0], + ['imya' => 'proverka-chistki', 'kod' => 0, 'proverok' => 27, 'provalov' => 0], + ], + ], JSON_UNESCAPED_UNICODE); + + postuchat($telo)->assertStatus(202); + + expect((string) otmetka()->last_error)->toContain('красных по списку 0, а объявлено 3'); +}); + +it('🔴 контроль ножа: связный зелёный итог остаётся зелёным', function (): void { + // Случай А ножа владельца. Без него две проверки выше доказывали бы только + // то, что дверь научилась ругаться, — а не то, что она ругается по делу. + $otvet = postuchat(itogSTremyaZelenymi()); + + $otvet->assertStatus(202)->assertJson(['status' => 'принят', 'bedy' => []]); + expect(otmetka()->last_error)->toBeNull(); +}); + +it('🔴 краснеет на поле, о котором дверь не знает', function (): void { + // Список «под защитой» считается ВЫЧИТАНИЕМ: всё, чего дверь не знает + // поимённо, — новость, о которой надо сказать вслух. Иначе поле, добавленное + // на машине и забытое здесь, окажется мимо защиты, а не под ней. + $itog = json_decode(itogSTremyaZelenymi(), true); + $itog['storozhey_propushcheno'] = 2; + + postuchat((string) json_encode($itog, JSON_UNESCAPED_UNICODE))->assertStatus(202); + + expect((string) otmetka()->last_error)->toContain('storozhey_propushcheno'); +}); + +it('🔴 краснеет на поле строки сторожа, о котором дверь не знает', function (): void { + $itog = json_decode(itogSTremyaZelenymi(), true); + $itog['storozha'][1]['propushcheno'] = 4; + + postuchat((string) json_encode($itog, JSON_UNESCAPED_UNICODE))->assertStatus(202); + + expect((string) otmetka()->last_error)->toContain('propushcheno'); +}); + +it('🔴 краснеет на список нужной длины, набитый одним и тем же сторожем', function (): void { + // Второе место того же рода, о котором нож владельца не говорил: длину + // списка можно добрать повторами. Тогда числа сходятся все до одного, а + // на деле гоняли одного сторожа трижды. + $itog = json_decode(itogSTremyaZelenymi(), true); + $itog['storozha'][1]['imya'] = 'proverka-raspisaniya'; + $itog['storozha'][2]['imya'] = 'proverka-raspisaniya'; + + postuchat((string) json_encode($itog, JSON_UNESCAPED_UNICODE))->assertStatus(202); + + expect((string) otmetka()->last_error)->toContain('повторяются имена'); +}); + it('🔴 разбирает НАСТОЯЩЕЕ тело, снятое с машины, а не только своё выдуманное', function (): void { // 🪤 Шов между двумя честными половинками. Все проверки выше кормят портал // телом, которое сами же и составили. Так можно доказать что угодно и diff --git a/bots/lena-golos/README.md b/bots/lena-golos/README.md index 0634d655..e1c0c8dd 100644 --- a/bots/lena-golos/README.md +++ b/bots/lena-golos/README.md @@ -805,6 +805,39 @@ bash bots/lena-golos/proverka-sutochnogo-obhoda.sh 3. дописать его в список эталонов сторожа обхода ``` +### Прибавили поле в итог — тоже скажите порталу + +С 06.08.2026 портал проверяет не только числа в итоге, но и **связность** того, +что принял. Три вещи, о которых стоит знать, прежде чем править вывод обхода: + +```text +1. Длина списка обязана сойтись с объявленным числом. + Сказали "storozhey_vsego": 3, а строк в списке две — + портал заругается. Раньше двое сторожей исчезали молча. + +2. Объявленное число красных обязано сойтись со списком. + Поле "storozhey_krasnyh" портал теперь ЧИТАЕТ и сверяет + со своим подсчётом по строкам. Считает он красным ровно + по тому же правилу, что и обход: код не ноль ИЛИ провалов + не ноль. Зелёного сторожа с нулём проверок обход красным + не считает — и портал в этом счёте тоже не считает. + +3. Поля, которых портал не знает, — беда, а не мелочь. + Портал знает восемь полей итога: + shema, sobrano_v, mashina, dlilos_ms, + storozhey_vsego, storozhey_krasnyh, + slepok_etalonov, storozha + и четыре поля строки сторожа: + imya, kod, proverok, provalov + Всё остальное он назовёт новостью и покраснеет. +``` + +🔴 **Цена этого названа честно:** прибавите в итог безобидное поле — портал +станет красным, пока человек не впишет его в список знакомых полей на +портальной стороне. Это сделано нарочно, той же рукой, что и число ожидаемых +сторожей: забытое поле должно оказаться **под** защитой, а не мимо неё. Правится +одной строкой в списке знакомых полей в двери приёма итога. + ## Как сверить копию настройки с сервером `extensions-lena.conf` — копия `/etc/asterisk/extensions.conf` с сервера diff --git a/docs/superpowers/priyomka/stroyka-6/otchyot-pomoshchnika-z-0-10-dodelka-2026-08-06.md b/docs/superpowers/priyomka/stroyka-6/otchyot-pomoshchnika-z-0-10-dodelka-2026-08-06.md new file mode 100644 index 00000000..79d76f57 --- /dev/null +++ b/docs/superpowers/priyomka/stroyka-6/otchyot-pomoshchnika-z-0-10-dodelka-2026-08-06.md @@ -0,0 +1,266 @@ +# Отчёт помощника — З-0.10, доделка по ножу владельца + +Дата: 06.08.2026 +Ветка: `feat/prospects-manual-testing-kp` +Чинится: дверь `app/app/Http/Controllers/Api/LenaStorozhaWebhookController.php`, метод `chtoNeTak()` +Что чинилось до этого: коммит `67195f0bf` — «машина робота проверяет себя сама, портал следит за свежестью» + +Пишется ПО ХОДУ работы, не задним числом. + +## Задача + +Владелец принял вчерашнюю работу и нашёл в ней дыру своим ножом. Дыр на самом деле две: + +1. Длина списка `storozha` нигде не сверяется с объявленным `storozhey_vsego`. + Список может быть короче — сторожа тихо исчезают, портал молчит. +2. Поле `storozhey_krasnyh` не читается вообще. Машина прислала своё показание — + дверь его выбросила. + +## Ход работы + +### 1. Разведка — что уже есть + +Прочитано: + +- дверь `LenaStorozhaWebhookController.php` (260 строк); +- сторож `app/tests/Feature/Lena/StorozhaMashinyTest.php` (338 строк, 18 проверок); +- настройка `app/config/storozha_mashiny.php`; +- обход на машине `bots/lena-golos/sutochnyy-obhod.sh` — только чтением, машину не трогал; +- потребитель отметки `app/app/Console/Commands/LenaProverkaObhodaStorozhey.php`. + +### 2. Замер ДО починки + +Заведено 6 новых проверок, из них 3 — дословно ножом владельца +(случаи Б, В и контроль А). Прогон по непочиненной двери: + +``` +всего 27, зелёных 22, красных 5 +``` + +Красные — ровно пять новых, и все с одним и тем же словом: + +``` +Failed asserting that '' [ASCII](length: 0) contains "строк в списке сторожей 1, а объявлено 3" +Failed asserting that '' [ASCII](length: 0) contains "красных по списку 0, а объявлено 3" +Failed asserting that '' [ASCII](length: 0) contains "storozhey_propushcheno" +Failed asserting that '' [ASCII](length: 0) contains "propushcheno" +Failed asserting that '' [ASCII](length: 0) contains "повторяются имена" +``` + +Пустая строка слева — это и есть дыра: `last_error` в отметке **пустой**, +портал записал «всё хорошо». Контроль А (случай владельца «3 зелёных») +прошёл зелёным и до починки — значит красные краснеют по делу, а не потому, +что сторож сломан. + +🪤 Первый заход этих проверок падал не так: на голом `toContain()` Pest +выдавал «Invalid expectation value type. Expected [iterable]», потому что +`last_error` был `NULL`. По такому красному не различить дыру и сломанного +сторожа. Приведение к строке сделало красный честным. + +### 3. Починка + +Правлен один метод — `chtoNeTak()` — и добавлены два списка знакомых полей. +Остальная дверь не тронута. + +Теперь сверяются **три независимых показания** об одном и том же: + +1. сколько сторожей ждёт настройка (`ozhidaem_storozhey`); +2. сколько объявил сам итог (`storozhey_vsego`); +3. сколько строк в списке `storozha` на самом деле. + +Плюс четвёртое, отдельное: объявленное `storozhey_krasnyh` против нашего +подсчёта красных по строкам. + +🔴 **Самое тонкое место починки, и оно же — ответ на вопрос владельца о ложных +тревогах.** Красных считаем **ровно по тому же правилу, что и обход на машине**: +код не ноль ИЛИ провалов не ноль. У портала правило строже машинного — он +ругается ещё и на зелёного сторожа с нулём проверок, а машина такого красным +НЕ считает. Сравни я объявленное число со счётом жалоб, а не со счётом красных +по машинному правилу — портал ругался бы «числа разошлись» на каждом таком +случае. Это и была бы та ложная тревога, которой владелец опасался. + +Сверх заказанного добавлены два места того же рода — обоснование в разборе: + +- список знакомых полей **строки сторожа**, а не только верхнего уровня; +- повторы имён в списке (длину можно добрать одним и тем же сторожем). + +### 4. Замер ПОСЛЕ починки + +Проверки одного файла сторожей: + +``` +result=passed всего 27 зелёных 27 красных 0 +``` + +🔑 Важнее числа то, что **21 прежняя проверка осталась зелёной**, и среди них — +та, что кормит дверь НАСТОЯЩИМ телом, снятым с машины 06.08.2026. То есть +новая строгость не забраковала то, что машина шлёт сегодня. + +### 5. Порча собственной починки + +Каждая проверка показана красной вырезанием своего куска — по одному за раз, +чтобы видеть, что сторож охраняет именно своё, а не соседнее. + +| что вырезал | стало красных | что сказал сторож | +|---|---|---| +| сверку длины списка | 1 | `'красных по списку 0, а объявлено 2' не содержит 'строк в списке сторожей 1, а объявлено 3'` | +| сверку числа красных | 1 | `'' не содержит 'красных по списку 0, а объявлено 3'` | +| вычитание полей итога + повторы имён | 2 | `'' не содержит 'storozhey_propushcheno'`, `'' не содержит 'повторяются имена'` | +| вычитание полей строки сторожа | 1 | `'' не содержит 'propushcheno'` | + +Два наблюдения из этой таблицы: + +- при вырезанной сверке длины случай Б всё равно краснеет — но **другой** + проверкой, сверкой числа красных. Две новые защиты ловят случай Б независимо + друг от друга, и это видно глазом в тексте красного. +- при вырезанной сверке числа красных случай В даёт **пустую** строку бед. + То есть без чтения `storozhey_krasnyh` этот случай не ловит НИЧЕГО. Это + прямое доказательство, что показание не лишнее. + +**Возврат доказан слепком со снятием невидимых знаков конца строки:** + +``` +до порчи: 7473756072af309d0b203853fc8c208ca75464fe +после возврата: 7473756072af309d0b203853fc8c208ca75464fe +``` + +Плюс `grep "false &&"` по двери — порчи не осталось. + +### 6. Полный прогон + +Своя база `liderra_testing_z010fix`, заведена и промигрирована отдельно — +чужих баз не занимал. + +``` +всего 5207, зелёных 5203, красных 0, ошибок 0, пропущено 4 +утверждений 16348, длилось 19 минут +``` + +Арифметика сходится: `5203 + 0 + 0 + 4 = 5207`. + +🔴 Датчиком брал **число проверок**, а не код возврата: обёртка печатает +«выход 0» даже когда прогон умер от нехватки памяти. `php artisan test` +не звал вовсе, `composer test` тоже — он режет по 300 секунд, а прогон идёт 19 +минут. Вывод складывал в файл перенаправлением, не трубой. + +### 7. Соседи + +- **моего имени нет ни в одном выводе ни одного сторожа** — проверено поиском + по всем моим правкам и по отчёту: ни метки смены, ни имени помощника, ни + имени моей базы в коде и в текстах проверок нет. +- **чужие файлы в дереве не тронуты** — я правил ровно четыре пути: дверь, + её проверки, README машины и этот отчёт. Соседи в это же время держат + правки в `app/.env.example`, `app/config/services.php`, + `docs/observer/STATUS.md`, `package.json`, `package-lock.json` и в чужом + приёмочном листе `stroyka-5` — ни одного из них я не открывал на запись и + ни один не попадёт в мой коммит: кладу поимённо. + +### 8. Зовущие поимённо + +Кто зовёт то, что я правил — смотрено глазами, не числом: + +| что | кто зовёт | +|---|---| +| `LenaStorozhaWebhookController@receive` | одна строка `routes/web.php:736`, `POST /api/webhook/lena-storozha`. Больше ниоткуда: поиск по всему коду дал только саму дверь и эту строку | +| эту дорогу снаружи | обход на машине 51.250.1.97, шаг 5 в `sutochnyy-obhod.sh` (`curl` с подписью), по расписанию `/etc/cron.d/lena-sutochnyy-obhod` | +| `chtoNeTak()` | только `receive()` в той же двери, один вызов | +| отметку, которую кладёт дверь | команда `lena:proverka-obhoda`, а её зовёт `routes/console.php:471` ежедневно | +| текст бед, который я дописал | та же команда, полем `last_error` → в письмо тревоги. Она читает поле целиком, поимённо тексты не разбирает — значит новые беды доедут до письма сами | + +### 9. Разметка, стиль, статанализ + +- `markdownlint` по этому отчёту и по README машины — `0 error(s)`. +- `pint --test` по обоим моим файлам PHP — `passed`. +- `composer stan` — 6 ошибок, **все шесть в файлах соседа** + (`app/Services/Obzvon/ObzvonClient.php`, `tests/Feature/Obzvon/ObzvonClientShovTest.php`), + которые мне трогать запрещено. В моих файлах — ноль. + +## Разбор задания + +### Моя ошибка — первой + +Первый заход новых сторожей падал негодным красным: на голом `toContain()` +Pest выдавал «Invalid expectation value type. Expected [iterable]», потому что +`last_error` был `NULL`. Такой красный не различает «дыра есть» и «сторож сам +сломан» — то есть сторож, показанный красным, ничего этим не доказывал. Поймал +сам, приведением к строке. Но написал я его сперва неправильно. + +### Картина беды владельца — верна, обе дыры настоящие + +Замерил своим прогоном: до починки оба случая давали `last_error` **пустым** — +портал записывал «всё хорошо». Подтверждаю оба пункта. + +### Ответ на прямой вопрос: читать ли `storozhey_krasnyh` + +**Да, читать. Но не так, как напрашивается — иначе ложные тревоги были бы.** + +Показание не лишнее: вырезав его, я получил случай В с **пустым** списком бед. +Больше его не ловит ничто. + +А вот опасение владельца про ложные тревоги — обоснованное, и оно бы сбылось +при прямолинейной починке. У портала и у машины **разные определения красноты**: +портал ругается ещё и на зелёного сторожа с нулём проверок, машина такого +красным не считает. Сравнивать объявленное число со счётом жалоб нельзя — +разошлись бы на ровном месте. Сравнивать надо со счётом, посчитанным +машинным правилом. Так и сделано, и это записано в коде комментарием, потому +что при следующей правке это первое, что потеряется. + +### 🔴 Где картина владельца ЗАВЫШЕНА — про подделку + +В задании сказано: автор той работы оставил незакрытым, что «подделать свежий +зелёный итог с утёкшим словом односторонний канал не мешает», и что мой нож +показывает — «подделывать связный итог даже не нужно, хватит обрезанного +списка». + +**Как довод про злоумышленника это неверно.** Чтобы стук вообще дошёл до +разбора тела, его надо подписать тайным словом и прийти с разрешённого адреса. +У того, кто уже владеет словом, нет никакой причины слать обрезанный список — +он пришлёт связный зелёный итог, и моя починка его не поймает. Порог для +подделки задаёт подпись, и он этой работой **не поднят ни на сколько**. + +Настоящая цена починки — другая, и она честная: защита от **случайной** порчи +и от будущих правок. Обход на машине правится (сегодня же и правился), вывод +собирается склейкой строк в оболочке, где потерять кусок легко и незаметно. +Вот от этого защита появилась. Предлагаю так и записать в решении, а довод +про подделку из обоснования убрать — иначе он даст ложное чувство, что канал +защищён лучше, чем он защищён. + +### Второе место того же рода, о котором задание не говорило + +Длину списка можно добрать **повторами**. Пришли три строки с одним и тем же +именем — и сходится ВСЁ: и с настройкой, и с объявленным числом, и с числом +красных. А гоняли одного сторожа трижды, двое молчат. Проверка на повтор имён +добавлена; ложных тревог она дать не может — имена сторожей это имена файлов +на машине, повторяться они не умеют. + +Так же добавлено вычитание по полям **строки сторожа**, а не только верхнего +уровня: правило владельца про «новое поле должно оказаться под защитой» +относится к строкам ровно так же. + +### Что в этой починке стоит денег и от чего можно отказаться + +Вычитание по полям — единственная часть с будущей ценой: прибавят на машине +безобидное поле, и портал станет красным, пока человек не впишет его в список. +Это сделано нарочно и той же рукой, что число ожидаемых сторожей. Но если +владелец решит, что цена не по нему, отказываться надо **именно от вычитания**, +а две сверки чисел оставить — они не стоят ничего и ловят настоящее. + +## Что осталось незакрытым + +1. **Живой машины я не касался** — запрещено заданием, и не касался. Значит + доказано только то, что дверь стала строже к телу, снятому 06.08.2026. + Что обход на машине сегодня шлёт ровно это тело — взято из чужой проверки, + своими руками не перемерено. +2. **Портальная половина не выкачена.** README машины прямо говорит: файла + настроек отправки на машине нет, обход пишет итог файлом и в портал не + стучится. То есть вся эта цепочка на бою пока не работает — ни до моей + починки, ни после. Моя работа готовит выкат, но выкатом не является. +3. **Список знакомых полей сверен глазами, а не программой.** Он списан с + `sutochnyy-obhod.sh` и с настоящего тела в проверке. Сторожа, который + ронял бы сборку при расхождении списка в PHP и вывода в оболочке, нет. +4. **Статанализ по репозиторию не зелёный** — 6 ошибок в файлах соседней + смены. Мои файлы чисты, но общий прогон `composer stan` сейчас красный не + по моей вине и не мною чинится. +5. **Числа проверок 26/46/27** в проверках взяты из вчерашней работы как есть. + Изменятся сторожа на машине — проверки начнут врать, и заметит это только + человек.