From 5b0a7da85441fc17a76ea18908765647dd7d31ee 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 22:43:08 +0300 Subject: [PATCH] =?UTF-8?q?fix(=D0=BD=D0=BE=D1=87=D1=8C):=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=B1=D0=BE=D1=80=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=BB=20=D0=BC=D1=91=D1=80=D1=82=D0=B2=D1=8B=D0=BC=20?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=80=D0=BE=D0=B6=D0=B0,=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=D0=B4=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=B0=D0=B5=D1=82=20=D1=81=D0=B0=D0=BC=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B3=D0=BE=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Разбор 33 «точных долгов» глазами (работа 2 из промта смены 26). Настоящих оказалось 20, ложных тревог 13 — почти сорок процентов. Одна ложная тревога была ОПАСНОЙ. `setup` и `teardown` из `storozh-boevogo-hozyaystva.mjs` шли в приговоре с самой твёрдой пометкой прибора — «не зовёт никто вовсе, даже проверки». У этой пометки, в отличие от «зовут только проверки», нет оговорки «посмотри глазами». А зовёт их САМ ПРОГОН: файл назван в его настройке, `app/vitest.config.tools.mjs` строка 56, `globalSetup`. Зова в коде нет и быть не может — соглашение об имени скобками не выглядит никогда. То есть прибор, написанный против класса «написано, но не работает», подталкивал снести работающее. Прочти следующая смена этот приговор буквально — и пошла бы вырезать сторожа боевого хозяйства как мёртвый код. РЕЗ УЗКИЙ НАРОЧНО, и это главное в правке. Наружу из затеи торчат три дороги, зовом имени считается ОДНА: - `globalSetup` — прогон зовёт `setup`/`teardown` сам по соглашению — засчитываем; - `setupFiles` — файл просто исполняется сверху вниз — имён не зовёт; - наряд `package.json` (`node tools/night/utro.mjs`) — имён не зовёт, там рот. Широкий рез тут не ослепил бы прибор, а ЗАТКНУЛ: засчитай мы наряды — все имена `cli.mjs` и `utro.mjs` вышли бы из долга разом, и живая находка `merkaCheloveka` пропала бы из приговора молча. ДОКАЗАНО ВЫРЕЗАНИЕМ, ДВАЖДЫ. Убрал встройку в обход — покраснела ровно одна, сквозная (число красных посчитано ДО прогона, сошлось). Расширил рез на `setupFiles` — покраснел встречный сторож послаблений. Голый шов закрыт нарочно: правило умеет читать настройку, обход умеет судить, и обе половины были бы зелены, даже если обход правила не спрашивает вовсе. Ловится только сквозной проверкой на подложном уголке. Номера новому пятну не даю: нумерация в шапке прибора сама себе противоречит — одно и то же пятно (ввоз модуля целиком) названо там и «шестым», и «№9». Имя вместо номера. Разбор всех 33 поимённо — docs/superpowers/zamer/2026-08-18-razbor-33-dolgov.md. Оттуда же два новых долга, ни один не начат: - механизм вопросов владельцу мёртв С ОБЕИХ СТОРОН: команд `cli.mjs answer` и `cli.mjs ack`, которыми примечание обещает класть ответ, в `cli.mjs` нет вовсе; - живая сверка настроек не спрашивает `kudaOtnesyon` — замерено подложным ключом: выдуманный ключ прошёл молча, а правило при этом честно ответило «не отнесён никуда». Приговор прибора: 33 -> 31 точных долга, 610 вывозится. Проверок 2916 в 106 файлах, ноль красных, код 0. Co-Authored-By: Claude Opus 5 --- .../zamer/2026-08-18-razbor-33-dolgov.md | 154 ++++++++++++++++++ tools/night/obhod-zovushchih-est-rot.test.mjs | 47 ++++++ tools/night/schyot-zovushchih.mjs | 75 +++++++-- tools/night/schyot-zovushchih.test.mjs | 86 +++++++++- 4 files changed, 351 insertions(+), 11 deletions(-) create mode 100644 docs/superpowers/zamer/2026-08-18-razbor-33-dolgov.md diff --git a/docs/superpowers/zamer/2026-08-18-razbor-33-dolgov.md b/docs/superpowers/zamer/2026-08-18-razbor-33-dolgov.md new file mode 100644 index 00000000..782b5b22 --- /dev/null +++ b/docs/superpowers/zamer/2026-08-18-razbor-33-dolgov.md @@ -0,0 +1,154 @@ +# Разбор 33 долгов прибора «кто зовёт это в жизни» — глазами, по одному + +**Смена 26, 06.08.2026.** Работа №2 из промта смены 26: «Прибор их называет за 21 секунду, +но приговор он выносит по устройству, а не по замыслу. Правило чтения: спроси у каждого +„а для кого он писался?“». + +> 🔴 **ГЛАВНОЕ ЧИСЛО.** Из 33 «точных долгов» настоящих оказалось **20**, ложных тревог — +> **13, то есть почти сорок процентов**. И одна ложная тревога была **опасной**: прибор +> объявил мёртвым сторожа, который поднимается на каждом прогоне. + +--- + +## 🔴🔴 Опасная ложная тревога: прибор подталкивал снести живое + +`setup` и `teardown` из `storozh-boevogo-hozyaystva.mjs` шли в приговоре с самой твёрдой +пометкой прибора — **«не зовёт никто вовсе, даже проверки»**. У этой пометки, в отличие +от «зовут только проверки», нет оговорки «посмотри глазами». + +А зовёт их **сам прогон**, потому что файл назван в его настройке: + +``` +app/vitest.config.tools.mjs:56 → globalSetup: ['../tools/night/storozh-boevogo-hozyaystva.mjs'] +``` + +Зова в коде нет и быть не может: соглашение об имени скобками не выглядит никогда. + +🔑 **Прибор против класса «написано, но не работает» сам подталкивал снести работающее.** +Прочти следующая смена этот приговор буквально — и пошла бы вырезать сторожа боевого +хозяйства как мёртвый код. + +**Починено этой же сменой** (см. ниже «Что сделано»). Приговор упал 33 → 31. + +--- + +## Разбор по одному + +### 🟢 Ложные тревоги — 13 + +| имя | файл | почему не долг | +|---|---|---| +| `setup`, `teardown` | `storozh-boevogo-hozyaystva.mjs` | 🔴 поднимает **сам прогон** через `globalSetup` | +| `schyotDolga`, `strokaDolga`, `perebrat`, `svezhiePoddelki`, `otkazPoddelki` | `lezhachie-sledy.mjs` | **тело сторожа есть сама проверка**: вторая половина сторожа течи, заведена 04.08 по прямому решению владельца; краснеет на полном прогоне | +| `PREDEL_ZHIVOGO_ZAPUSKA`, `PREDEL_SAMOY_PROGRAMME` | `predel-zhivogo-zapuska.mjs` | писались **для проверок**, их и должны звать только они (названо ещё в промте) | +| `readEvents` | `heartbeat.mjs` | работа делается **напрямую**: события читают `sudya-nochi.mjs:146` и `zhiv-li-progon.mjs:113` своим путём | +| `vorotaVetkiNochi` | `vetka-nochi-ne-otstayot.mjs` | лишняя обёртка над половинками, которые зовут напрямую (названо в промте) | +| `schyotZovushchih` | `schyot-zovushchih.mjs` | счёт по одному имени; обход зовущих ходит своей дорогой | +| `readProbeAttempts`, `readSpawnAttempts` | `schyot-zamerov.mjs` | читалки журналов для проверок; живой счёт замеров идёт своим путём | + +🪤 **Четыре из них — один и тот же новый род:** «работа делается, но мимо обёртки». +Две дороги к одному файлу могут разъехаться молча — это мелкий долг, но не мёртвый код. + +### 🔴 Настоящие долги — 20 + +**А. Механизм «спросить владельца и принять ответ» — 11 имён, мёртв С ОБЕИХ СТОРОН** + +| имя | файл | +|---|---| +| `askOwner`, `mustAsk`, `answerQuestion`, `applyAnswer`, `assumptionOpen`, `deadlineHit`, `otvetDnyom`, `priznakiPunkta`, `prostoyLiVpravdu` | `questions.mjs` | +| `recordOwnerAck`, `OWNER_ACTION_KINDS` | `mailer.mjs` | + +🔴 **Смена 25 нашла половину.** Вторая половина хуже: примечание в `mailer.mjs` говорит, +что действия владельца «пишет ПОМОЩНИК теми же командами, что кладут ответ: `cli.mjs answer` +и `cli.mjs ack`». **Таких команд в `cli.mjs` нет вовсе** — прибор 4 называет семь, и этих +двух среди них нет (замерено). + +⇒ Владельцу не только нечем задать вопрос — **ему некуда положить ответ**. + +**Б. Долги, названные вслух там, где их надо подключить — 2** + +| имя | где сказано | +|---|---| +| `zadachaKonchena` | `supervisor.mjs:2239`: «ДОЛГ КУСКА 7 (позиция 7): **ИМЕННО ЭТА СТРОКА** заменяется на зов». Вместо правила стоит упрощение `otlozhennyePunkty.size === 0` | +| `reshyonnyeSNevypolnimymi` | `items.mjs`: «правило готово, а живой провод к нему — нет: кто будет его делать, обязан сперва завести ленджер невыполнимых» | + +🪤 **Но примечание в `items.mjs` при этом врёт** — про `zadachaKonchena` там сказано +«**зовут его двое**: блок 5а куска 4 и блок 6а куска 7». Замерено командой: живого зова нет +ни одного. Это правило 235 в чистом виде — задуманное записано в настоящем времени. + +**В. Три находки смены 25 — 4 имени** + +| имя | что это | +|---|---| +| `mayTouchFakt` (`file-claims.mjs`) | 🔴 защита «файл не заявлен — значит ничей»; **ждёт слова владельца** | +| `merkaCheloveka`, `merkaAvariynoy` (`utro.mjs`) | 🟡 мерка человечности; 🆕 **мерок ДВЕ, а не одна** — вторая для аварийной бумаги | + +**Г. Новый долг, найденный этой сменой — 1** + +| имя | что это | +|---|---| +| `kudaOtnesyon` (`sverka-nastroek-s-resheniyami.mjs`) | 🔴 «ключ живой настройки не отнесён никуда — и это беда» | + +🔴 **Замерено, а не предположено.** Подложил в настройку выдуманный ключ и позвал живую сверку: + +``` +Сверка сказала про выдуманный ключ: НЕТ — прошёл молча +А само правило говорит: null +``` + +⇒ Правило готово и верно отвечает «не отнесён никуда». Живая сверка его не спрашивает. +Заведи кто-нибудь новый ключ в настройках ночи — он пройдёт мимо владельца молча. + +**Д. Прочее — 2** + +| имя | что это | +|---|---| +| `mailFailedSince` (`mailer.mjs`) | «не ушла ли сводка с такого-то часа» — не зовёт никто | +| `readResult` (`spawn.mjs`) | разбор последней строки вывода работника — не зовёт никто | + +--- + +## 🔑 Чему научил разбор + +**237. У ПРИБОРА, СУДЯЩЕГО «КТО ЭТО ЗОВЁТ», СПРОСИ СНАЧАЛА — А ВСЕ ЛИ ДОРОГИ ЗОВА ОН ЗНАЕТ.** +Зов приходит не только из кода: из настройки прогона (`globalSetup`), из наряда, по соглашению +об имени. Прибор, знающий одну дорогу, объявляет мёртвым живое — и приговор его тем твёрже, +чем меньше дорог он знает. + +**238. СТОРОЖ, ЧЬЁ ТЕЛО ЕСТЬ САМА ПРОВЕРКА, ЖИВ — ЕСЛИ ПРОВЕРКИ ГОНЯЮТ.** Пять имён +`lezhachie-sledy.mjs` не зовёт ни один рабочий файл, и это правильно: сторож течи работает +тем, что краснеет на полном прогоне. Вопрос «для кого писался» тут отвечается не «для +проверок», а «**чем он доставляется**». + +**239. 🪤 РАБОТА, ИДУЩАЯ МИМО СВОЕЙ ЖЕ ОБЁРТКИ, — НЕ МЁРТВЫЙ КОД, А РАЗЪЕЗД.** Четыре +случая из тринадцати: обёртка есть, а зовут напрямую. Сносить нельзя, оставлять как есть — +значит держать две дороги к одному месту, которые однажды разойдутся молча. + +--- + +## Что сделано этой сменой + +🟢 **Слепое пятно «зов из настройки» закрыто.** Прибор читает `globalSetup` настройки +прогона и не сажает `setup`/`teardown` такого файла в точный долг. Рез **узкий нарочно**: + +| дорога наружу | считается зовом имени? | +|---|---| +| `globalSetup` — прогон зовёт `setup`/`teardown` сам | ✅ да | +| `setupFiles` — файл исполняется сверху вниз | ❌ нет, имён не зовёт | +| наряд `package.json` (`node tools/night/utro.mjs`) | ❌ нет, там рот | + +🔴 **Почему рез узкий — это главное.** Широкий рез здесь не ослепил бы прибор, а **заткнул**: +засчитай мы наряды — все имена `cli.mjs` и `utro.mjs` вышли бы из долга разом, и живая +находка `merkaCheloveka` пропала бы из приговора молча. + +**Доказано вырезанием, дважды:** + +1. убрал встройку в обход → покраснела ровно одна, сквозная (**считал заранее — сошлось**); +2. расширил рез на `setupFiles` → покраснел встречный сторож послаблений. + +**Голый шов закрыт нарочно:** правило умеет читать настройку, обход умеет судить — и обе +половины были бы зелены, даже если обход правила не спрашивает вовсе. Ловится только +сквозной проверкой на подложном уголке. + +**Числа:** проверок 2916 в 106 файлах (было 2911), ноль красных, код 0. +Приговор прибора: **610 вывозится, 31 точный долг** (был 33). diff --git a/tools/night/obhod-zovushchih-est-rot.test.mjs b/tools/night/obhod-zovushchih-est-rot.test.mjs index 7465175e..9a6abb83 100644 --- a/tools/night/obhod-zovushchih-est-rot.test.mjs +++ b/tools/night/obhod-zovushchih-est-rot.test.mjs @@ -110,6 +110,53 @@ describe('обход зовущих: спрашиваем у всего выво } }); + it('3Б. 🔴 кого поднимает САМ ПРОГОН — не долг, хоть зова в коде и нет', () => { + // 🔴 БЕДА ЗАМЕРЕНА НА ЖИВОМ (06.08.2026). Обход объявил `setup`/`teardown` сторожа + // боевого хозяйства «не зовёт никто вовсе, даже проверки» — самым твёрдым своим + // приговором, у которого нет оговорки «посмотри глазами». А их зовёт сам прогон: + // файл назван в его настройке (`globalSetup`). Прочти это следующая смена буквально — + // и пошла бы сносить живого сторожа как мёртвый код. + // + // 🪤 ЗАЧЕМ ЗДЕСЬ ОТДЕЛЬНАЯ ПРОВЕРКА, ЕСЛИ САМО ПРАВИЛО УЖЕ ПОКРЫТО (13–16 у соседа). + // Потому что покрыты были бы ДВЕ ЧЕСТНЫЕ ПОЛОВИНКИ, а дыра живёт МЕЖДУ ними: правило + // умеет читать настройку, обход умеет судить — и обе зелены, даже если обход правила + // не спрашивает вовсе. Голый шов ловится только сквозной проверкой. + const dir = mkdtempSync(join(tmpdir(), 'obhod-zovushchih-nastroyka-')); + try { + writeFileSync(join(dir, 'moy-storozh.mjs'), [ + 'export function setup() { return 1; }', + 'export function teardown() { return 2; }', + 'export function nikomuNeNuzhnoe() { return 3; }', + ].join('\n'), 'utf8'); + const nastroyka = join(dir, 'vitest.config.tools.mjs'); + writeFileSync(nastroyka, [ + 'export default {', + ' test: {', + " globalSetup: ['../tools/night/moy-storozh.mjs'],", + ' },', + '};', + ].join('\n'), 'utf8'); + + const itog = obhodZovushchih(dir, { nastroykaProgona: nastroyka }); + const dolgi = itog.tochnyeDolgi.map((v) => v.imya).sort(); + + // 🔴 Главное: два имени по соглашению вышли из точного долга… + expect(dolgi).not.toContain('setup'); + expect(dolgi).not.toContain('teardown'); + // …и ушли не в тишину, а под глаза — с пометкой, что их поднимает прогон. + const podnyatye = itog.posmotretGlazami.filter((v) => v.podnimaetProgon).map((v) => v.imya).sort(); + expect(podnyatye).toEqual(['setup', 'teardown']); + + // 🔴 ВСТРЕЧНЫЙ СТОРОЖ ПОСЛАБЛЕНИЙ, и он тут главнее первого. Прощение выдано ДВУМ + // именам по соглашению, а не всему файлу: сосед по тому же файлу, которого не зовёт + // никто, обязан остаться в долгу. Иначе один `globalSetup` укрыл бы от прибора целый + // файл — и прибор стал бы выгораживать мёртвое, то есть врать в другую сторону. + expect(dolgi).toContain('nikomuNeNuzhnoe'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it('4. 🔴 пустое место не выдаётся за ноль долгов (правило 233)', () => { // Каталог без единого рабочего файла — это НЕ «долгов нет», это «считать было нечего». const dir = mkdtempSync(join(tmpdir(), 'obhod-zovushchih-pusto-')); diff --git a/tools/night/schyot-zovushchih.mjs b/tools/night/schyot-zovushchih.mjs index fd452653..7d7ba77e 100644 --- a/tools/night/schyot-zovushchih.mjs +++ b/tools/night/schyot-zovushchih.mjs @@ -324,13 +324,58 @@ function vvoziatLiModulCelikom(syroy, rabochie, rodnoyFayl) { return false; } +// ━━━ СЛЕПОЕ ПЯТНО, НАЙДЕНО 06.08.2026: ЗОВ ИДЁТ ИЗ НАСТРОЙКИ, А НЕ ИЗ КОДА ━━━ +// +// 🔴 Прибор смотрит только внутрь затеи — и потому объявил `setup`/`teardown` сторожа +// боевого хозяйства «не зовёт никто вовсе, даже проверки». А зовёт их САМ ПРОГОН, потому +// что файл назван в его настройке: `globalSetup: ['../tools/night/storozh-…mjs']`. Зова +// в коде нет и быть не может — соглашение об имени зовом со скобками не выглядит никогда. +// +// 🔴🔴 Приговор «не зовёт никто вовсе» — самый твёрдый, какой выдаёт прибор: оговорки +// «посмотри глазами» у него нет. Прочти его следующая смена буквально — и пошла бы сносить +// живого сторожа как мёртвый код. Прибор против «написано, но не работает» подталкивал +// снести работающее. +// +// 🔴 РЕЗ УЗКИЙ НАРОЧНО. Наружу из затеи торчат три дороги, зовом имени считается ОДНА: +// · `globalSetup` — прогон зовёт у файла `setup`/`teardown` сам ⇒ засчитываем; +// · `setupFiles` — файл просто исполняется сверху вниз ⇒ имён не зовёт; +// · наряд в `package.json` (`node tools/night/utro.mjs`) ⇒ имён не зовёт, там рот. +// Широкий рез тут не ослепил бы прибор, а ЗАТКНУЛ: засчитай мы наряды — все имена `cli.mjs` +// и `utro.mjs` вышли бы из долга разом, и живая находка `merkaCheloveka` пропала бы молча. +export const IMENA_PO_SOGLASHENIYU = ['setup', 'teardown']; + +/** + * Файлы затеи, которые ПОДНИМАЕТ САМ ПРОГОН по своей настройке (`globalSetup`). + * Ответ — набор имён файлов без пути. Настройки нет — пустой набор, а не падение: + * прибор зовут и на подложных уголках во временном месте машины. + * 🔴 Путь доводом, а не приклеенный: иначе правило нечем показать на подложной настройке, + * а сторож, которого не видели красным, не сторожит ничего. + */ +export function faylyPodnimaemyeNastroykoy(putNastroyki) { + let text; + try { text = readFileSync(putNastroyki, 'utf8'); } catch { return new Set(); } + const kusok = /globalSetup\s*:\s*(\[[^\]]*\]|['"`][^'"`]*['"`])/.exec(text); + if (!kusok) return new Set(); + const out = new Set(); + for (const put of kusok[1].matchAll(/['"`]([^'"`]+)['"`]/g)) { + const imya = put[1].split(/[\\/]/).pop(); + if (imya) out.add(imya); + } + return out; +} + // Спросить у каждого вывозимого: кто зовёт тебя в жизни? // // 🔴 `byloLiChtoSchitat` стоит здесь не для красоты (правило 233): каталог без единого // рабочего файла даёт ноль долгов ровно так же, как безупречное хозяйство, и отличить // их по числу нельзя. Пустое место, посчитанное числом, становится нулём само собой. -export function obhodZovushchih(dir) { +export function obhodZovushchih(dir, { nastroykaProgona = null } = {}) { const vsyo = vsyoVyvozimoe(dir); + // Кого поднимает сам прогон своей настройкой. Умолчание — настоящая настройка затеи; + // доводом её подменяют в проверках, чтобы показать правило на подложке. + const podnimaemye = faylyPodnimaemyeNastroykoy( + nastroykaProgona ?? join(dir, '..', '..', 'app', 'vitest.config.tools.mjs'), + ); const bezZovushchih = []; const sZovushchimi = []; const tochnyeDolgi = []; @@ -376,9 +421,12 @@ export function obhodZovushchih(dir) { const vvezeno = vvezenoLiImya(syroy, rabochie, v.imya, v.fayl); const celikom = vvoziatLiModulCelikom(syroy, rabochie, v.fayl); - const zapis = { ...v, vProverkah, doma, vvezeno, celikom }; + // Поднимает ли этот файл сам прогон, и зовётся ли имя у такого файла по соглашению. + const podnimaetProgon = podnimaemye.has(v.fayl) && IMENA_PO_SOGLASHENIYU.includes(v.imya); + + const zapis = { ...v, vProverkah, doma, vvezeno, celikom, podnimaetProgon }; bezZovushchih.push(zapis); - if (!vvezeno && !celikom && doma === 0) tochnyeDolgi.push(zapis); + if (!vvezeno && !celikom && doma === 0 && !podnimaetProgon) tochnyeDolgi.push(zapis); else posmotretGlazami.push(zapis); } @@ -420,7 +468,7 @@ export function slovaObhoda(itog) { if (itog.tochnyeDolgi.length > 0) { v.push('━━ 🔴 ТОЧНО ДОЛГ: имя никуда не ввезено, дома не зовётся, явных зовов нет ━━'); - v.push('Ни один из четырёх обманов прибора тут невозможен — обманываться нечем.'); + v.push('Ни один из пяти обманов прибора тут невозможен — обманываться нечем.'); v.push(''); for (const b of itog.tochnyeDolgi) { const hvost = b.vProverkah > 0 @@ -434,20 +482,27 @@ export function slovaObhoda(itog) { if (itog.posmotretGlazami.length > 0) { v.push(`━━ 🟡 ПОСМОТРЕТЬ ГЛАЗАМИ (${itog.posmotretGlazami.length}) ━━`); v.push('Явных зовов нет, но имя ввезено, либо его модуль ввозят целиком, либо оно'); - v.push('зовётся дома — значит прибор мог ослепнуть, и ноль тут ничего не доказывает.'); + v.push('зовётся дома, либо файл поднимает сам прогон — значит прибор мог ослепнуть,'); + v.push('и ноль тут ничего не доказывает.'); + const podnyatye = itog.posmotretGlazami.filter((b) => b.podnimaetProgon); + if (podnyatye.length > 0) { + v.push(`🔴 Из них ${podnyatye.length} поднимает САМ ПРОГОН своей настройкой (globalSetup) —`); + v.push(`зова в коде у них нет и быть не может: ${podnyatye.map((b) => b.imya).join(', ')}.`); + } v.push(''); } v.push('━━ 🔴 КАК ЭТО ЧИТАТЬ ━━'); v.push('Ноль зовущих сам по себе — НЕ приговор. Прибор отвечает на вопрос «есть ли явный'); - v.push('зов со скобками в чужом рабочем файле», а не на вопрос «подключено ли». Четыре'); - v.push('обмана, названные в шапке прибора:'); + v.push('зов со скобками в чужом рабочем файле», а не на вопрос «подключено ли». Пять'); + v.push('обманов, названных в шапке прибора:'); v.push(' · это постоянная — скобок она не получает никогда, и ноль у неё почти всегда враньё;'); v.push(' · её передали значением (`имя = имя,` в доводах) — скобок тоже нет;'); v.push(' · её зовут в её же файле — родной файл прибор пропускает нарочно;'); - v.push(' · модуль ввезли целиком, и зов идёт через точку (`m.имя()`).'); - v.push('Сито выше отсеивает все четыре: в «точно долг» попадает лишь то, что никуда'); - v.push('не ввезено и дома не зовётся — обмануться там физически нечем.'); + v.push(' · модуль ввезли целиком, и зов идёт через точку (`m.имя()`);'); + v.push(' · файл поднимает САМ ПРОГОН по своей настройке, и зовёт `setup`/`teardown` без кода.'); + v.push('Сито выше отсеивает все пять: в «точно долг» попадает лишь то, что никуда не ввезено,'); + v.push('дома не зовётся и прогоном не поднимается — обмануться там физически нечем.'); return v.join('\n'); } diff --git a/tools/night/schyot-zovushchih.test.mjs b/tools/night/schyot-zovushchih.test.mjs index f0618ee7..01fd4d4b 100644 --- a/tools/night/schyot-zovushchih.test.mjs +++ b/tools/night/schyot-zovushchih.test.mjs @@ -26,7 +26,10 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { tolkoKod, zovyImeni, schyotZovushchih } from './schyot-zovushchih.mjs'; +import { + tolkoKod, zovyImeni, schyotZovushchih, + faylyPodnimaemyeNastroykoy, IMENA_PO_SOGLASHENIYU, +} from './schyot-zovushchih.mjs'; // Короткий заход: очистить образец от примечаний и строк, посчитать зовы имени. // Ровно то, что делает `schyotZovushchih` над содержимым каждого файла, только без ФС. @@ -203,3 +206,84 @@ describe('счёт зовущих: семь слепых пятен, назва expect(skolko("const m = await import('./foo.mjs');\nm.zovi();", 'foo')).toBe(0); }); }); + +// ────────────────────────────────────────────────────────────────────────────── +// СЛЕПОЕ ПЯТНО, НАЙДЕНО 06.08.2026 СМЕНОЙ 26: ЗОВ ИДЁТ ИЗ НАСТРОЙКИ, А НЕ ИЗ КОДА +// ────────────────────────────────────────────────────────────────────────────── +// 🔴 БЕДА, ЗАМЕРЕННАЯ ЖИВЬЁМ. Обход объявил `setup` и `teardown` из +// `storozh-boevogo-hozyaystva.mjs` точным долгом со словами «не зовёт никто вовсе, даже +// проверки». А это САМЫЙ ЖИВОЙ сторож затеи: он поднимается на КАЖДОМ полном прогоне, +// потому что назван в настройке прогона: +// app/vitest.config.tools.mjs → globalSetup: ['../tools/night/storozh-boevogo-hozyaystva.mjs'] +// Прогон зовёт у такого файла `setup` и `teardown` САМ, ПО СОГЛАШЕНИЮ ОБ ИМЕНИ, — зова +// в коде нет и быть не может. Прибор смотрит только внутрь затеи и настройки не видит вовсе. +// +// 🔴🔴 ЧЕМ ЭТО ОПАСНО, И ЭТО ГЛАВНОЕ. Приговор «не зовёт никто вовсе» — самый твёрдый +// из всех, какие прибор выдаёт: у него нет оговорки «посмотри глазами». Прочитай его +// следующая смена буквально — и пошла бы сносить сторожа боевого хозяйства как мёртвый код. +// То есть прибор против класса «написано, но не работает» сам подталкивал снести работающее. +// +// 🪤 НОМЕРА ЭТОМУ ПЯТНУ НЕ ДАЮ НАРОЧНО. Нумерация в шапке прибора сама себе противоречит: +// одно и то же пятно (ввоз модуля целиком) названо там и «шестым», и «№9». Плодить девятое +// поверх этого значило бы закрепить путаницу. Имя вместо номера: «зов из настройки». +// +// 🔴 РЕЗ УЗКИЙ НАРОЧНО — правило 234 («не режет ли датчик вместе с шелухой зерно»), только +// с другой стороны: слишком широкий рез здесь не ослепит прибор, а ЗАТКНЁТ ЕГО. Из затеи +// наружу торчат ТРИ дороги, и зовом имени считается лишь одна: +// · `globalSetup` — прогон зовёт `setup`/`teardown` по соглашению ⇒ ЗОВ, засчитываем; +// · `setupFiles` — файл просто исполняется сверху вниз ⇒ имён не зовёт; +// · наряд `package.json` (`node tools/night/utro.mjs`) ⇒ имён не зовёт, там рот. +// Засчитай мы вторую и третью — все имена `cli.mjs` и `utro.mjs` разом вышли бы из долга, +// и `merkaCheloveka` (живая находка смены 25) исчезла бы из приговора молча. +describe('счёт зовущих: зов из НАСТРОЙКИ прогона, а не из кода (найдено 06.08.2026)', () => { + function podlozhnayaNastroyka(telo) { + const dir = mkdtempSync(join(tmpdir(), 'nastroyka-progona-')); + const put = join(dir, 'vitest.config.tools.mjs'); + writeFileSync(put, telo, 'utf8'); + return { dir, put }; + } + + it('13. файл из globalSetup назван — прогон поднимает его сам', () => { + const { dir, put } = podlozhnayaNastroyka([ + 'export default {', + ' test: {', + " globalSetup: ['../tools/night/storozh-boevogo-hozyaystva.mjs'],", + ' },', + '};', + ].join('\n')); + try { + const podnimaet = faylyPodnimaemyeNastroykoy(put); + expect(podnimaet.has('storozh-boevogo-hozyaystva.mjs')).toBe(true); + } finally { rmSync(dir, { recursive: true, force: true }); } + }); + + it('14. ВСТРЕЧНЫЙ СТОРОЖ: setupFiles зовом имён НЕ считается — рез узкий', () => { + // 🔴 Тут и живёт цена ошибки: `setupFiles` исполняет файл целиком, но ни `setup`, + // ни любого другого имени по соглашению не зовёт. Засчитай мы его — прибор начал бы + // выгораживать мёртвое, то есть врать в ту самую сторону, от которой написан. + const { dir, put } = podlozhnayaNastroyka([ + 'export default {', + ' test: {', + " setupFiles: ['../tools/night/nastroyka-storozha-zhivogo-dereva.mjs'],", + ' },', + '};', + ].join('\n')); + try { + const podnimaet = faylyPodnimaemyeNastroykoy(put); + expect(podnimaet.has('nastroyka-storozha-zhivogo-dereva.mjs')).toBe(false); + expect(podnimaet.size).toBe(0); + } finally { rmSync(dir, { recursive: true, force: true }); } + }); + + it('15. настройки нет вовсе — пусто, а не падение', () => { + // Прибор обязан работать и там, где настройки прогона нет: его зовут на подложных + // уголках во временном месте машины, и никакой настройки рядом с ними не лежит. + expect(faylyPodnimaemyeNastroykoy(join(tmpdir(), 'takoy-nastroyki-net-12345.mjs')).size).toBe(0); + }); + + it('16. имена по соглашению названы поимённо и список ЗАКРЫТ', () => { + // 🪤 Список закрыт нарочно: прогон зовёт у globalSetup-файла ровно эти два имени. + // Допиши сюда третье «на всякий случай» — и оно начнёт выгораживать мёртвое. + expect(IMENA_PO_SOGLASHENIYU).toEqual(['setup', 'teardown']); + }); +});