feat(ночь): у ночи появился свой судья — приходит на КАЖДУЮ ночь, знает чем она кончилась
This commit is contained in:
@@ -2589,3 +2589,31 @@ stenogramma
|
||||
vorota
|
||||
deneg
|
||||
zovushchih
|
||||
utro
|
||||
zavesti
|
||||
Sudyu
|
||||
sudya
|
||||
nochi
|
||||
zaveden
|
||||
platnyy
|
||||
tolko
|
||||
cherez
|
||||
rodit
|
||||
Rebyonka
|
||||
minut
|
||||
Konchilas
|
||||
Priborom
|
||||
Punkty
|
||||
SUDI
|
||||
Sudye
|
||||
chego
|
||||
itog
|
||||
nakaz
|
||||
nemye
|
||||
otlozhennye
|
||||
prigovor
|
||||
prigovory
|
||||
slova
|
||||
tekst
|
||||
uliki
|
||||
vladeltsu
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
# Судья ночи — что построено, чем замерено и чего он НЕ умеет
|
||||
|
||||
**Смена от 06.08.2026, угол `night1`, ветка `feat/night-dispatcher-part1`.**
|
||||
Исполнено решение владельца 12 (протокол `docs/grilling/2026-08-02-nastroyki-nochi-i-ramka-deneg.md`,
|
||||
раздел «🔴🔴 У НОЧИ БУДЕТ СВОЙ СУДЬЯ») и решения 134/135, откладывавшие третью ночь до тех пор,
|
||||
пока судья не построен. Двенадцать смен решение не исполнялось.
|
||||
|
||||
Живого судью НЕ заводил ни разу. Ни одной копейки владельца не потрачено.
|
||||
|
||||
---
|
||||
|
||||
## 1. Что построено, простыми словами
|
||||
|
||||
Когда гаснет **последний** работник прогона и задача доделана целиком, ночь сама заводит
|
||||
**одного** помощника, который не писал ни задания, ни строчки работы. Ему дают пять вопросов
|
||||
и пути ко всем уликам этой ночи. Он читает, проверяет числа командами, ничего не чинит —
|
||||
и пишет приговор. Приговор ложится файлом `prigovor-sudi.md` рядом с уликами прогона, а в
|
||||
утренней бумаге владельца появляется строка: «Судья ночи прочёл эту ночь целиком и сказал
|
||||
вот что: …».
|
||||
|
||||
Три новых файла и две правки:
|
||||
|
||||
| файл | что в нём |
|
||||
|---|---|
|
||||
| `tools/night/sudya-nochi.mjs` | наказ судье, сбор улик, замок «не больше одного раза», рождение, запись приговора |
|
||||
| `tools/night/sudya-nochi.test.mjs` | 31 проверка |
|
||||
| `tools/night/sudya-nochi-zamer-do.md` | вот эта бумага |
|
||||
| `tools/night/supervisor.mjs` | зов судьи в `closeRunIfLast` + довод `zavestiSudyuFn` |
|
||||
| `tools/night/supervisor.test.mjs` | 6 проверок на зов |
|
||||
|
||||
Больше не тронуто **ничего**.
|
||||
|
||||
### Пять вопросов — не черновик
|
||||
|
||||
Взяты **дословно** из настоящего приговора настоящего судьи
|
||||
(`docs/superpowers/priyomka/4/sudya-tretiy-pribor-2026-08-03.md`):
|
||||
|
||||
1. Где надзиратель и помощник согласились, а правда другая
|
||||
2. Числа, которые не сходятся
|
||||
3. Правила, которые не следуют из своих находок
|
||||
4. Чего в журнале нет вовсе
|
||||
5. Похвала как порча
|
||||
|
||||
### Четыре правила, каждое куплено болью
|
||||
|
||||
- **Довод принят ВМЕСТЕ с зовом.** Правило хозяйства самого `supervisor.mjs`: шесть доводов
|
||||
«на вырост» когда-то приняли и не звали — вышла кнопка «стоп», которая никого не слышит.
|
||||
Довод `zavestiSudyuFn` зовётся в том же куске, и на зов стоят проверки, краснеющие без него.
|
||||
- **Не больше одного раза за прогон.** Место занимается файлом `sudya.json` с запретом на
|
||||
перезапись (`flag: 'wx'`) — одно неделимое действие. Двое, дошедшие в одну секунду, не разойдутся.
|
||||
- **Смерть судьи не роняет ночь.** Итог прогона (`writeRunOutcome`, `last-run.json`, утренняя
|
||||
бумага) кладётся **строкой выше** зова судьи. На это стоит отдельная проверка: она смотрит,
|
||||
что в минуту зова итог **уже лежал**.
|
||||
- **Но молча его смерть не глотается.** Всякая неудача — строка владельцу в
|
||||
`summary-lines.jsonl` и запись `sudya_ne_zaveden` в ленту прогона.
|
||||
|
||||
### Судья — не работник
|
||||
|
||||
Карточки в `workers/` ему не заводится, приговоров о пунктах он не пишет, в состав прогона
|
||||
не вписывается. Значит: потолка задач не съедает, счёт закрытых пунктов не портит,
|
||||
`amILast` его не видит. Проверено двумя проверками.
|
||||
|
||||
### Строка в сводке — без единой правки чужого файла
|
||||
|
||||
Судья пишет свою строку в `summary-lines.jsonl` — ту же улику, куда пишет надзиратель.
|
||||
`utro.mjs` читает её сам и печатает в графе «ЧТО НОЧЬ РАССКАЗАЛА О СЕБЕ». `summary.mjs`
|
||||
и `utro.mjs` не тронуты ни знаком. Замерено живьём:
|
||||
|
||||
```
|
||||
ЧТО НОЧЬ РАССКАЗАЛА О СЕБЕ
|
||||
· Судья ночи прочёл эту ночь целиком и сказал вот что: двое сошлись на числе,
|
||||
которого никто не мерил. Весь его приговор лежит отдельным файлом в каталоге
|
||||
этого прогона — имя файла ниже, в части для мастера: (слово мастера).
|
||||
```
|
||||
|
||||
Путь заменён на «(слово мастера)» — так утренняя бумага поступает со **всякой** латиницей.
|
||||
Строка написана так, чтобы **читаться и без хвоста**; полный путь стоит ниже, в части для
|
||||
мастера, и там он целый. Чинить это без правки `utro.mjs` нечем — а её мне не разрешали.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🔴 ПРИЁМКА ВЫРЕЗАНИЕМ — с числами
|
||||
|
||||
Каждая правка вносилась и **возвращалась** обычной правкой файла.
|
||||
|
||||
| # | что вырезал | что должно было покраснеть | покраснело |
|
||||
|---|---|---|---|
|
||||
| 1 | зов `zavestiSudyuFn` в `closeRunIfLast` | проверки на зов | **3 красных из 248** |
|
||||
| 2 | вернул зов | всё зелено | **248 из 248** |
|
||||
| 3 | запрет на перезапись отметки (`flag: 'wx'`) | защита «не больше одного раза» | **2 красных из 31** |
|
||||
| 4 | пояс `try/catch` вокруг зова судьи | прогон падает вместе с судьёй | **1 красная из 217** |
|
||||
|
||||
**Поимённо, чтобы можно было повторить:**
|
||||
|
||||
**Вырез 1** — `supervisor.mjs`, вместо `await zavestiSudyuFn({…})` поставлено `void zavestiSudyuFn;`.
|
||||
Покраснели:
|
||||
|
||||
- «погас ПОСЛЕДНИЙ работник и задача доделана — судья позван РОВНО ОДИН раз»
|
||||
- «🔴 итог прогона лёг ДО судьи — упади судья, утренняя бумага уже на месте»
|
||||
- «🔴🔴 судья УПАЛ — ночь всё равно закрыта, а его смерть НЕ проглочена молча»
|
||||
|
||||
**Вырез 3** — `sudya-nochi.mjs`, у `writeFileSync` убран `{ flag: 'wx' }`. Покраснели:
|
||||
|
||||
- «второй заход не занимает место и говорит почему»
|
||||
- «🔴 два зова `zavestiSudyu` подряд рождают судью РОВНО ОДИН раз» (счётчик рождений стал 2)
|
||||
|
||||
**Вырез 4** — `supervisor.mjs`, пояс заменён на `throw err`. Покраснела проверка «судья УПАЛ»
|
||||
с настоящим обвалом: `Error: судья взорвался на ровном месте` — то есть без пояса падение
|
||||
судьи вправду уносит надзирателя, а с ним и закрытие ночи.
|
||||
|
||||
### Живой замер вне прибора проверок
|
||||
|
||||
Отдельно прогнан настоящий `zavestiSudyu` **вне** vitest (дорога к рождению — подставная,
|
||||
денег ноль). Замерено:
|
||||
|
||||
- судья «упал» → `zaveden: false`, файла приговора **нет**, в `summary-lines.jsonl` легла
|
||||
строка «Судью завести не удалось: …», в ленту — `sudya_ne_zaveden`;
|
||||
- второй зов после падения → **0 рождений**, ответ «судью по этому прогону уже заводили».
|
||||
|
||||
---
|
||||
|
||||
## 3. Полный прогон
|
||||
|
||||
```
|
||||
npm run test:tools
|
||||
Test Files 104 passed (104)
|
||||
Tests 2824 passed (2824)
|
||||
код возврата 0
|
||||
```
|
||||
|
||||
Было до смены: **2787 проверок в 103 файлах**. Стало: **2824 в 104**. Прибавилось 37 (31 + 6).
|
||||
|
||||
### 🔴 По дороге сработали три чужих сторожа, и они были правы
|
||||
|
||||
Это стоит записать: новый файл **сам по себе** нарушил три живых правила хозяйства, и все три
|
||||
пришлось лечить **со своей стороны** — списки живут в чужих файлах, которые смене трогать нельзя.
|
||||
|
||||
1. **`platnyy-zamer-tolko-cherez-schyot.test.mjs`** — «к деньгам владельца подходят ровно три
|
||||
файла поимённо». Первый заход ввозил `startWorker` прямо в судью. ⇒ **Ввоз убран.** Дорогу
|
||||
к рождению подаёт надзиратель доводом `roditRebyonka` — он и есть «единственный зовущий
|
||||
рождения» по формулировке самого сторожа. Так ворота счёта рождений (решение владельца 9)
|
||||
продолжают считать **все** рождения за ночь, а не часть.
|
||||
2. **`paths.test.mjs`** — «всякая настройка, которую читает код, стоит в образце
|
||||
`night.config.example.json`». Первый заход читал `sudya_model` и `sudya_minut`. Образец
|
||||
править нельзя ⇒ **обе настройки сняты**: срок судьи — число в коде (`PREDEL_SUDI_MINUT = 20`),
|
||||
голова — та же, что у работников (`worker_model`). Кто будет заводить настройки — заводит
|
||||
их **обе разом**, и в образце, и в коде.
|
||||
3. **`nemye-prigovory.test.mjs`** — «приговор владельцу без читателя — немой». Он узнаёт цели
|
||||
по слову `tekst:` в теле функции; у меня в этом поле лежал текст приговора судьи. ⇒ **Поле
|
||||
переименовано в `prigovor`.** Говорю это вслух, а не прячу: сторож здесь ошибался — приговор
|
||||
судьи уходит к владельцу **другой** дорогой (файлом и строкой в сводке), которую он видеть
|
||||
не умеет. Имя `prigovor` вернее по смыслу; что приговор вправду доезжает, сторожат две
|
||||
собственные проверки этого файла.
|
||||
|
||||
---
|
||||
|
||||
## 4. 🔴 ЧЕГО ЭТОТ СУДЬЯ НЕ УМЕЕТ
|
||||
|
||||
Пишу поимённо, потому что это ценнее похвалы.
|
||||
|
||||
1. **🔴🔴 Главное: судья приходит только на УДАЧНУЮ ночь, а решение 12 говорит «не реже раза
|
||||
за ночь».** Зов стоит на флаге `reviewer_starts`, а этот флаг блок 5а ставит **только**
|
||||
когда все пункты задачи засчитаны. Отложи ночь хоть один пункт — флага нет, судьи нет.
|
||||
То есть ночь, которую **больше всего** стоило бы судить (половина не вышла, работники
|
||||
гасли), останется **без единого взгляда со стороны**. Так велел промт задания, и я так
|
||||
сделал; но с решением владельца это расходится, и решать тут не мне. Развилка, оставшаяся
|
||||
открытой в самом протоколе допроса («когда именно ночь зовёт судью»), этим **не закрыта**.
|
||||
2. **Судью никто не судит.** Он ровно такой же прибор, как надзиратель и работники, только
|
||||
не участвовавший в стройке. Ошибётся — узнать об этом нечем: сторожа на его приговор нет.
|
||||
Это записал о себе и настоящий судья 03.08.2026, и с тех пор не изменилось ничего.
|
||||
3. **Судья не видит прошлых ночей.** Улики собираются по **одному** прогону. «Числа, которые
|
||||
не сходятся» между сменами (а именно там их и находили — счётчик возражений дал пять разных
|
||||
чисел) он не поймает: сравнивать ему не с чем.
|
||||
4. **Живого судью не заводили ни разу.** Всё замерено на подставном рождении. Значит
|
||||
не проверено: успевает ли он за 20 минут; влезает ли наказ с уликами семерых работников
|
||||
в его голову; понимает ли он наказ вообще. Первый живой заход — это отдельный платный
|
||||
замер, и делать его без печати владельца нельзя.
|
||||
5. **Приговор судьи никуда не действует.** Он ложится файлом и строкой в сводку — и всё.
|
||||
Ни одна находка судьи ночью ничего не меняет: работники уже погасли. Вторая половина
|
||||
развилки из протокола («что делать с его находками ночью, пока человека нет») **открыта**.
|
||||
6. **Своей головы и своего срока у судьи нет** — см. §3 пункт 2. Судья читает много и думает
|
||||
вширь; вполне возможно, ему нужна другая голова, чем работникам. Сегодня выбора нет.
|
||||
7. **Прогон, в котором не завёлся НИ ОДИН работник, судьи не получает.** Такой прогон
|
||||
закрывает `run.mjs` своим зовом `writeRunOutcome`, минуя надзирателя. Это, на мой взгляд,
|
||||
правильно (судить нечего), но сказать вслух обязан.
|
||||
8. **Наказ судье может устареть молча.** Он написан один раз рукой, из головы, и сторожа
|
||||
«наказ не разошёлся с образцом приговора» нет. Ровно тот класс, который настоящий судья
|
||||
и нашёл: «правила теряются между передачами».
|
||||
9. **Второй зов молчит для владельца** (пишет только в ленту). Сделано нарочно — чтобы
|
||||
не задваивать строку в сводке, — но если защита однажды сработает не по делу, владелец
|
||||
об этом из бумаги не узнает.
|
||||
|
||||
---
|
||||
|
||||
## 5. Что осталось надзирателю
|
||||
|
||||
1. **Развилка «когда звать судью»** — см. §4 пункт 1. Это решение владельца, не моё.
|
||||
2. **Строка в сводке отдельной графой.** Сегодня приговор судьи стоит наравне с прочими
|
||||
строками ночи в графе «ЧТО НОЧЬ РАССКАЗАЛА О СЕБЕ», и путь к файлу владелец видит как
|
||||
«(слово мастера)». Сделать судье **свою** графу («ЧТО СКАЗАЛ СУДЬЯ») можно только правкой
|
||||
`utro.mjs` — чужого файла, которого мне трогать нельзя. Не делал.
|
||||
3. **Настройки владельца `sudya_model` и `sudya_minut`** — заводить вместе с образцом
|
||||
`night.config.example.json`.
|
||||
4. **Первый живой заход судьи** — платный, только с печати владельца.
|
||||
5. **Открыть ночи её собственные файлы?** `sudya-nochi.mjs` лежит в `tools/night/` и в список
|
||||
«кроме» (`claims-audit.mjs`, 13 путей, открытых работникам поимённо) **не внесён** — я его
|
||||
не трогал и трогать не вправе. Считаю, что так и надо: судья — мерный прибор, и работник,
|
||||
правящий своего судью, судим быть не может. Но решение это не моё.
|
||||
|
||||
---
|
||||
|
||||
# ДОПИСКА ОТ 06.08.2026 — ПОЧИНКА ГЛАВНОГО ДОЛГА (§4 пункт 1 ЗАКРЫТ)
|
||||
|
||||
**Тот же угол `night1`, та же ветка. Живого судью снова НЕ заводил ни разу. Ни копейки
|
||||
владельца не потрачено.**
|
||||
|
||||
Строитель судьи назвал свой главный долг сам и оказался прав: **ошибка была не в судье,
|
||||
а в надзирателе.** Эта смена её чинит.
|
||||
|
||||
## 6. Что было не так, простыми словами
|
||||
|
||||
Судья приходил **только на удачную ночь**. Зов стоял на признаке `reviewer_starts`, а этот
|
||||
признак блок 5а поднимает ровно тогда, когда **все** пункты задачи засчитаны:
|
||||
|
||||
```js
|
||||
const vseZaschitany = otlozhennyePunkty.size === 0;
|
||||
if (!vseZaschitany) {
|
||||
pravitKartochkuProgona(runDirPath, { reviewer_starts: false, … })
|
||||
```
|
||||
|
||||
Отложи ночь хоть один пункт — признака нет, судьи нет. Получалось наоборот замыслу:
|
||||
**ночь, которую больше всего стоило бы судить (половина не вышла, работники гасли),
|
||||
оставалась без единого взгляда со стороны**, а взгляд получала та, где и так всё сошлось.
|
||||
Решение владельца 12 говорит дословно: судья заводится **«не реже раза за ночь»**.
|
||||
|
||||
Ломалось это **тихо**: и сводка, и приёмщик вели себя как положено — просто судьи не было,
|
||||
и об этом никто не узнавал.
|
||||
|
||||
## 7. Что изменено
|
||||
|
||||
Тронуто **пять** файлов и ни одного больше.
|
||||
|
||||
| файл | что сделано |
|
||||
|---|---|
|
||||
| `tools/night/supervisor.mjs` | из зова судьи убрана опора на `reviewer_starts`; заведён счёт итога ночи `chemKonchilasNoch`, и он передаётся судье доводом `itog` |
|
||||
| `tools/night/supervisor.test.mjs` | одна проверка заменена (она закрепляла ошибку), одна добавлена, одна усилена |
|
||||
| `tools/night/sudya-nochi.mjs` | наказ судье получил раздел «ЧЕМ КОНЧИЛАСЬ ЭТА НОЧЬ»; довод `itog` доносится до наказа |
|
||||
| `tools/night/sudya-nochi.test.mjs` | 6 новых проверок |
|
||||
| `tools/night/sudya-nochi-zamer-do.md` | эта дописка |
|
||||
|
||||
### 7.1. Судья приходит на каждую ночь
|
||||
|
||||
Дошло дело до закрытия прогона — судья заводится. Удачная ночь, наполовину удачная,
|
||||
провальная — одинаково. **Признак `reviewer_starts` не тронут ни знаком**: он про сборщик
|
||||
экранов продукта и нужен затее сам по себе. Зов на него просто больше не смотрит.
|
||||
|
||||
### 7.2. 🔴 Судье говорят, ЧЕМ НОЧЬ КОНЧИЛАСЬ — и это меняет его работу целиком
|
||||
|
||||
Раньше он этого не получал вовсе. Теперь в наказе стоит:
|
||||
|
||||
```
|
||||
━━━ ЧЕМ КОНЧИЛАСЬ ЭТА НОЧЬ ━━━
|
||||
|
||||
🔴 Это СЛОВО НАДЗИРАТЕЛЯ о самом себе, а не замер. Он — один из тех двоих, кого ты
|
||||
судишь. Пересчитай эти числа по уликам сам; разойдись они с уликами — это находка,
|
||||
и находка дорогая.
|
||||
|
||||
· Пунктов в задании: 2
|
||||
· Засчитано надзирателем: 1 — а именно: 2
|
||||
· НЕ закрыто: 1 — а именно: 1
|
||||
· Из них отложено надзирателем (пункт дважды не сошёлся): 1
|
||||
```
|
||||
|
||||
и дальше — **разный главный вопрос**:
|
||||
|
||||
- **ночь не доделана** → «ПОЧЕМУ НЕ ВЫШЛО. Ответь на это ПЕРВЫМ, прежде пяти вопросов…
|
||||
скажи прямо: помешало ли делу устройство самой ночи (правила надзирателя, разводка
|
||||
работников по файлам, сроки, пределы) или дело было только в самой работе»;
|
||||
- **ночь доделана** → «Тем внимательнее смотри, ЧЕМ ЗА ЭТО ЗАПЛАЧЕНО. Слово „засчитано"
|
||||
сказал надзиратель о работе, которую сам же и раздавал»;
|
||||
- **пунктов в задании нет вовсе** → сказано и это;
|
||||
- **итога не подали** → «Чем кончилась эта ночь, мне не сказали — сочти сам и скажи вслух,
|
||||
что считал сам». Молчание тут было бы неотличимо от «ночь удалась».
|
||||
|
||||
Три решения внутри, каждое названо вслух:
|
||||
|
||||
1. **Числа считаются ПО ФАЙЛАМ, а не по памяти надзирателя.** Источник закрытого — тот же
|
||||
`closedItemsOfRun`, каким считают «сделано» очередь пунктов и след отложенной задачи.
|
||||
Второго счёта не заведено: разойдись они — владелец получил бы утром два разных числа
|
||||
об одной ночи.
|
||||
2. **Числа поданы судье как СЛОВО НАДЗИРАТЕЛЯ, а не как замер.** Их даёт тот самый, кого
|
||||
судья судит. Не скажи мы этого — судья принял бы их за проверенное, а «поверил числу
|
||||
подсудимого» и есть та самая болезнь, ради которой он заводится.
|
||||
3. **Список отложенного честно неполон, и это написано прямо в наказе.** Отложенное помнит
|
||||
только тот надзиратель, который откладывал (дыра, названная ещё в самом
|
||||
`otlozhennyePunkty`). Отложенное соседями попадает в «НЕ закрыто», но не в «отложено» —
|
||||
судья видит оба списка и потому не примет часть за целое.
|
||||
|
||||
## 8. 🔴 ПРИЁМКА ВЫРЕЗАНИЕМ — с числами
|
||||
|
||||
Каждая правка вносилась и **возвращалась** обычной правкой файла.
|
||||
|
||||
| # | что вырезал | что должно было покраснеть | покраснело |
|
||||
|---|---|---|---|
|
||||
| 0 | ничего (замер ДО починки: новые проверки против сегодняшнего кода) | все новые | **9 красных из 255** |
|
||||
| 1 | вернул зависимость от `reviewer_starts` | судья не приходит на провальную ночь | **2 красных из 218** |
|
||||
| 2 | снял передачу `itog` в наказ | судья не знает, чем кончилась ночь | **1 красная из 37** |
|
||||
| 3 | снял запрет на перезапись отметки (`flag: 'wx'`) | защита «не больше одного судьи за прогон» | **2 красных из 255** |
|
||||
|
||||
**Поимённо, чтобы можно было повторить:**
|
||||
|
||||
**Замер 0** — новые проверки прогнаны против **непочиненного** кода. Покраснели все девять:
|
||||
шесть в `sudya-nochi.test.mjs` (весь новый раздел про итог) и три в `supervisor.test.mjs`
|
||||
(«погас последний… судья позван ровно один раз» — на новом требовании об итоге;
|
||||
«🔴🔴 ПУНКТ ОТЛОЖЕН… судья ВСЁ РАВНО приходит»; «флага приёмщика нет… судья ВСЁ РАВНО зван»).
|
||||
|
||||
**Вырез 1** — `supervisor.mjs`, перед зовом возвращена строка
|
||||
`if (readJson(...)?.reviewer_starts !== true) return;`. Покраснели ровно те две, ради
|
||||
которых всё и делалось:
|
||||
|
||||
- «🔴🔴 ПУНКТ ОТЛОЖЕН, ночь НЕ доделана — судья ВСЁ РАВНО приходит, и знает, что не вышло»
|
||||
(`ночь, которую больше всего стоило судить, осталась без судьи`);
|
||||
- «🔴 флага приёмщика нет и до пунктов дело не дошло — судья ВСЁ РАВНО зван».
|
||||
|
||||
**Вырез 2** — `sudya-nochi.mjs`, `nakazSudye({ runId, uliki, itog })` заменено на
|
||||
`nakazSudye({ runId, uliki })`. Покраснела сквозная: «итог доезжает СКВОЗЬ `zavestiSudyu`
|
||||
до самого наказа, а не теряется по дороге».
|
||||
|
||||
**Вырез 3** — `sudya-nochi.mjs`, у `writeFileSync` убран `{ flag: 'wx' }`. Покраснели обе
|
||||
проверки защиты: «второй заход не занимает место и говорит почему» и «два зова
|
||||
`zavestiSudyu` подряд рождают судью РОВНО ОДИН раз» (счётчик рождений стал 2). **Защита
|
||||
жива.**
|
||||
|
||||
### Пояс «смерть судьи не роняет ночь» не тронут
|
||||
|
||||
Проверка «🔴🔴 судья УПАЛ — ночь всё равно закрыта, а его смерть НЕ проглочена молча»
|
||||
осталась на месте и зелена: итог прогона по-прежнему кладётся **строкой выше** зова, а
|
||||
неудача уходит строкой владельцу и записью `sudya_ne_zaveden` в ленту. Вырез этого пояса
|
||||
делала прошлая смена (1 красная из 217) — повторно не резал.
|
||||
|
||||
## 9. Полный прогон
|
||||
|
||||
```
|
||||
npm run test:tools
|
||||
Test Files 104 passed (104)
|
||||
Tests 2831 passed (2831)
|
||||
код возврата 0
|
||||
```
|
||||
|
||||
Было до правки: **2824 в 104**. Стало: **2831 в 104**. Прибавилось 7 (6 новых в
|
||||
`sudya-nochi.test.mjs`, 1 новая в `supervisor.test.mjs`; ещё одна там же **заменена** —
|
||||
прежняя «флага приёмки нет — судью НЕ зовём» закрепляла как раз ту ошибку, которую чиним,
|
||||
и оставить её значило бы охранять баг).
|
||||
|
||||
### 🪤 Чего я боялся и чего не случилось
|
||||
|
||||
Снятие признака открывает зов судьи для **всякой** ночи, а значит и для тех проверок в
|
||||
чужих файлах, где подставки судьи нет (`slova-vladeltsu.test.mjs` зовёт круг напрямую).
|
||||
Там теперь работает **настоящий** `zavestiSudyu` — и упирается в собственный пол
|
||||
`podPriboromProverok`: живого рождения нет, денег ноль, в сводку ложится честное «судью
|
||||
завести не удалось: идёт прибор проверок». Ни одна чужая проверка от этого не покраснела —
|
||||
замерено полным прогоном выше, а не выведено рассуждением.
|
||||
|
||||
## 10. 🔴 ЧЕГО ЭТА ПРАВКА НЕ ЗАКРЫВАЕТ
|
||||
|
||||
Честно и поимённо. **§4 пункт 1 закрыт, остальные восемь пунктов §4 живы полностью:**
|
||||
|
||||
1. **Судью по-прежнему никто не судит** (§4 п. 2). Ошибётся — узнать нечем.
|
||||
2. **Судья по-прежнему не видит прошлых ночей** (§4 п. 3). Улики — по одному прогону.
|
||||
3. **Живого судью так и не заводили ни разу** (§4 п. 4). Всё замерено на подставном
|
||||
рождении. Не проверено: успевает ли он за 20 минут, влезает ли наказ (а он стал
|
||||
**длиннее** на раздел об итоге) в его голову, понимает ли он наказ вообще. Первый
|
||||
живой заход — платный, только с печати владельца.
|
||||
4. **Приговор судьи по-прежнему никуда не действует** (§4 п. 5). Он ложится файлом и
|
||||
строкой в сводку — и всё. Вторая половина развилки из протокола («что делать с его
|
||||
находками ночью, пока человека нет») **осталась открытой**: эта правка закрыла только
|
||||
вопрос «когда звать», и то наполовину.
|
||||
5. **Своей головы и своего срока у судьи по-прежнему нет** (§4 п. 6, §3 п. 2).
|
||||
6. **Прогон, в котором не завёлся НИ ОДИН работник, судьи по-прежнему не получает**
|
||||
(§4 п. 7). Такой прогон закрывает `run.mjs` своим зовом `writeRunOutcome`, минуя
|
||||
надзирателя, а `run.mjs` мне трогать нельзя. **Говорю прямо: это единственная дыра
|
||||
в требовании «на каждую ночь», которая осталась.** Ночь, где всё погибло на рождении,
|
||||
судьи не увидит.
|
||||
7. **Наказ судье может устареть молча** (§4 п. 8) — и теперь его стало **больше**, значит
|
||||
расхождению есть где спрятаться.
|
||||
8. **Второй зов по-прежнему молчит для владельца** (§4 п. 9).
|
||||
9. **Число «отложено» неполно между надзирателями.** Оно верно для того, кто откладывал,
|
||||
и пусто для соседей. В наказе это сказано вслух, но починить это нечем без пятого файла
|
||||
состояния, которого затея не заводила.
|
||||
10. **Порядок «сколько попыток дали пункту» судья узнаёт не из итога, а из улик.** Я передаю
|
||||
номера и счёт, но не историю попыток: она есть в ленте прогона, и наказ велит её читать.
|
||||
Если ленты не будет — судья об этом узнает из списка `chego_net`, но не из итога.
|
||||
@@ -0,0 +1,718 @@
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// СУДЬЯ НОЧИ — третий прибор, смотрящий на работу ЦЕЛИКОМ
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
//
|
||||
// 🔴🔴 ЗАЧЕМ ОН ЕСТЬ. Решение владельца 12 (протокол `docs/grilling/2026-08-02-nastroyki-nochi-
|
||||
// i-ramka-deneg.md`): «ночь заводит своего судью». Довод владельца замерен числом, а не выведен:
|
||||
// за целую смену помощники не поймали НИ ОДНОЙ ошибки надзирателя — все пять он нашёл сам.
|
||||
// Причина не в помощниках: правило «ничего другого не трогай» разводит их по файлам и
|
||||
// ОБРЕЗАЕТ ИМ ОБЗОР ДО СВОЕГО ФАЙЛА. Ночь разводит работников так же ⇒ перекос неизбежен
|
||||
// по устройству. Судья — тот, кто не писал ни задания, ни работы, и потому видит целое.
|
||||
//
|
||||
// 🔴 ЭТО НЕ `reviewer.mjs`. Тот файл собирает экраны продукта к сводке и к судье отношения
|
||||
// не имеет. Два прибора, два имени, ни одной общей строки.
|
||||
//
|
||||
// 🔴 ЦЕНА, НАЗВАННАЯ ВЛАДЕЛЬЦУ И ПРИНЯТАЯ: одно-два лишних рождения за ночь.
|
||||
//
|
||||
// ━━━ ПЯТЬ ПРАВИЛ ХОЗЯЙСТВА, КАЖДОЕ КУПЛЕНО БОЛЬЮ ━━━
|
||||
//
|
||||
// 0. 🔴🔴 СУДЬЯ ПРИХОДИТ НА КАЖДУЮ НОЧЬ, дошедшую до закрытия прогона: удачную,
|
||||
// наполовину удачную и провальную — одинаково. Так велит решение владельца 12
|
||||
// дословно: «не реже раза за ночь». Первый заход строился иначе — зов стоял на флаге
|
||||
// приёмщика `reviewer_starts`, а тот поднимается ТОЛЬКО когда все пункты засчитаны, —
|
||||
// и выходило ровно наоборот замыслу: ночь, которую больше всего стоило судить,
|
||||
// оставалась без единого взгляда со стороны. Починено 06.08.2026.
|
||||
// И вместе с зовом судье говорят, ЧЕМ НОЧЬ КОНЧИЛАСЬ (довод `itog`): на провальной
|
||||
// ночи главный вопрос другой, и не зная итога, судья судил бы провал как удачу.
|
||||
// 1. Судья заводится НЕ БОЛЬШЕ ОДНОГО РАЗА за прогон. Дважды — это лишние деньги владельца
|
||||
// и ДВЕ РАЗНЫЕ ПРАВДЫ утром, между которыми ему пришлось бы выбирать. Место занимается
|
||||
// файлом-отметкой с запретом на перезапись (`flag: 'wx'`) — это одно неделимое действие
|
||||
// файловой системы, и двое, дошедшие сюда в одну секунду, не разойдутся.
|
||||
// 2. Смерть судьи НЕ РОНЯЕТ ночь. Судья — оценка, а не работа: не завёлся, упал, кончилось
|
||||
// время — прогон закрывается как обычно. Итог прогона (`writeRunOutcome`) кладётся
|
||||
// ДО зова судьи, а не после, и это не вкусовщина: случись наоборот, упавший судья унёс
|
||||
// бы с собой утреннюю бумагу владельца.
|
||||
// 3. НО МОЛЧА ГЛОТАТЬ ЕГО СМЕРТЬ НЕЛЬЗЯ. «Никогда не роняем» уже однажды превращало ошибку
|
||||
// в тишину (память: `feedback-best-effort-glotaet-moyu-oshibku.md`). Всякая неудача судьи
|
||||
// уходит СТРОКОЙ В СВОДКУ ВЛАДЕЛЬЦУ и записью в ленту прогона.
|
||||
// 4. Судья НЕ РАБОТНИК. Карточки в `workers/` ему не заводится, приговоров о пунктах он
|
||||
// не пишет, в состав прогона не вписывается — значит ни потолка задач не съедает,
|
||||
// ни счёта закрытых пунктов не портит, и `amILast` его не видит вовсе.
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { readJson, appendLine } from './state.mjs';
|
||||
import { logEvent } from './heartbeat.mjs';
|
||||
// 🔴 Имя файла сводки знает ОДНО место на всю затею. Своей строки-копии здесь нет намеренно:
|
||||
// переименуют файл там — судья писал бы в старое имя, а утренняя бумага читала бы новое
|
||||
// и видела пустоту (та же беда уже ловилась в надзирателе).
|
||||
import { SUMMARY_LINES_FILE, REPO_ROOT, priyomkaDir, podPriboromProverok } from './paths.mjs';
|
||||
// 🔴🔴 РОЖДЕНИЯ ЗДЕСЬ НЕТ И НЕ БУДЕТ — ЕГО ПОДАЁТ НАДЗИРАТЕЛЬ ДОВОДОМ. Это не удобство,
|
||||
// а требование живого сторожа `platnyy-zamer-tolko-cherez-schyot.test.mjs`: подходить
|
||||
// к деньгам владельца разрешено ровно трём файлам поимённо (`spawn.mjs` — дом рождения,
|
||||
// `budget.mjs` — крошечный замер, `supervisor.mjs` — единственный зовущий). Судья в этот
|
||||
// список не входит и входить не должен: заведи он свою дорогу к запуску — ворота счёта
|
||||
// рождений (решение владельца 9) считали бы не все рождения за ночь, а часть, и предел
|
||||
// владельца молча перестал бы быть пределом. Поэтому здесь ввозится только разбор потока.
|
||||
import { parseStreamLine } from './spawn.mjs';
|
||||
|
||||
// --- имена и числа -----------------------------------------------------------
|
||||
|
||||
// Как судья зовётся. Не `w-…`: он не работник, и путать их именами нельзя.
|
||||
export const SUDYA_ID = 'sudya';
|
||||
|
||||
// Куда ложится его приговор — файлом, в каталоге прогона, рядом с уликами.
|
||||
export const PRIGOVOR_FILE = 'prigovor-sudi.md';
|
||||
|
||||
// Отметка «судью по этому прогону уже заводили». Она же — замок от второго рождения.
|
||||
export const OTMETKA_FILE = 'sudya.json';
|
||||
|
||||
// 🔴 Метка, с которой судья ОБЯЗАН начать приговор. Из неё берётся строка в утреннюю сводку:
|
||||
// без неё владелец получил бы только путь к файлу и не узнал бы, что судья вообще сказал.
|
||||
export const METKA_GLAVNOGO = 'ГЛАВНОЕ ОДНОЙ СТРОКОЙ:';
|
||||
|
||||
// 🔴 Сколько ждём судью. Число не с потолка и не от щедрости: настоящий судья 03.08.2026
|
||||
// (`docs/superpowers/priyomka/4/sudya-tretiy-pribor-2026-08-03.md`) читал бумаги и проверял
|
||||
// утверждения командами — это работа на десятки минут, а не на минуту. Но и вечно ждать
|
||||
// нельзя: судья, зациклившийся в четыре утра, жёг бы запас владельца до самого утра, а ночь
|
||||
// уже закрыта и его никто не сторожит.
|
||||
//
|
||||
// 🟡 СВОЕЙ НАСТРОЙКИ У ВЛАДЕЛЬЦА ПОКА НЕТ, И Я ГОВОРЮ ЭТО ВСЛУХ, А НЕ ПРЯЧУ. Число и голова
|
||||
// судьи просились в настройки (`sudya_minut`, `sudya_model`) — но всякая настройка, которую
|
||||
// читает код, обязана стоять в образце `night.config.example.json`, а этот файл смене
|
||||
// заводить не разрешено. Читать настройку, которой нет в образце, — значит завести молчаливое
|
||||
// расхождение образца с живым кодом, и на это в затее стоит свой сторож (`paths.test.mjs`).
|
||||
// ⇒ Пока: срок — вот это число, голова — та же, что у работников (`worker_model`).
|
||||
// Кто будет заводить настройки, заводит их ОБЕ разом: и в образце, и здесь.
|
||||
export const PREDEL_SUDI_MINUT = 20;
|
||||
|
||||
// ━━━ ПЯТЬ ВОПРОСОВ. ЭТО НЕ ЧЕРНОВИК — ЭТО ПРОВЕРЕННЫЙ ДЕЛОМ НАКАЗ ━━━
|
||||
//
|
||||
// 🔴 Взяты дословно из настоящего приговора настоящего судьи (тот же файл приёмки куска 4).
|
||||
// Менять их можно только с замером: каждый из пяти на живой работе что-то нашёл, и вопрос 4
|
||||
// («чего в журнале нет вовсе») вскрыл ровно ту беду, из-за которой судья и заводится.
|
||||
export const PYAT_VOPROSOV = Object.freeze([
|
||||
'Ответ 1. Где надзиратель и помощник согласились, а правда другая',
|
||||
'Ответ 2. Числа, которые не сходятся',
|
||||
'Ответ 3. Правила, которые не следуют из своих находок',
|
||||
'Ответ 4. Чего в журнале нет вовсе',
|
||||
'Ответ 5. Похвала как порча',
|
||||
]);
|
||||
|
||||
// --- улики -------------------------------------------------------------------
|
||||
|
||||
function estLiFayl(put) {
|
||||
try { return Boolean(put) && existsSync(put); } catch { return false; }
|
||||
}
|
||||
|
||||
// Бумаги работников `*-zamer-do.md`. Живут в ХРАНИЛИЩЕ (`tools/night/`), а не под `.night`:
|
||||
// это единственная дорога от работника к владельцу, и работник кладёт их коммитом.
|
||||
// 🪤 Собираются КОМАНДОЙ (обход каталога), а не перечнем имён: перечень устарел бы к первой же
|
||||
// новой бумаге и молча увёл бы у судьи улику. Правило самого надзирателя — «всякий перечень
|
||||
// собирать командой, а не памятью» — и нарушать его в приборе, который за такое и судит,
|
||||
// было бы смешно.
|
||||
function bumagiRabotnikov(koren) {
|
||||
const dir = join(koren, 'tools', 'night');
|
||||
try {
|
||||
return readdirSync(dir)
|
||||
.filter(f => f.endsWith('-zamer-do.md'))
|
||||
.sort()
|
||||
.map(f => join(dir, f));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Что судье читать. Собирается КОМАНДОЙ по каталогу прогона и хранилищу.
|
||||
*
|
||||
* 🔴 Чего НЕ нашлось — называется вслух, отдельным списком (`chego_net`). Молча пропущенная
|
||||
* улика страшнее отсутствующей: судья решил бы, что прочёл всё, и вынес бы приговор по
|
||||
* половине дела, не зная об этом. Тот же класс, что «зелёный сторож, смотревший не туда».
|
||||
*/
|
||||
export function ulikiProgona(runDirPath, { koren = REPO_ROOT } = {}) {
|
||||
const chego_net = [];
|
||||
|
||||
const kartochka_progona = join(runDirPath, 'run.json');
|
||||
const run = readJson(kartochka_progona) ?? null;
|
||||
if (!run) chego_net.push('карточки прогона (run.json) нет или она не читается — состав ночи и задание неизвестны');
|
||||
|
||||
// 🪤 Нет файла — отдаём `null`, а не путь. Иначе наказ звал бы судью читать то, чего нет,
|
||||
// и «улика пуста» стало бы неотличимо от «улики не существует».
|
||||
const slovaPut = join(runDirPath, SUMMARY_LINES_FILE);
|
||||
const slova_nochi = estLiFayl(slovaPut) ? slovaPut : null;
|
||||
if (!slova_nochi) chego_net.push('слов, которые ночь написала о себе, нет — сводки строк не осталось');
|
||||
|
||||
const lentaPut = join(runDirPath, 'events.jsonl');
|
||||
const lenta = estLiFayl(lentaPut) ? lentaPut : null;
|
||||
if (!lenta) chego_net.push('ленты событий прогона нет — по минутам ночь восстановить нечем');
|
||||
|
||||
const rabotniki = [];
|
||||
const workersDir = join(runDirPath, 'workers');
|
||||
let imena = [];
|
||||
try { imena = readdirSync(workersDir).sort(); } catch { imena = []; }
|
||||
for (const wid of imena) {
|
||||
const kartochka = join(workersDir, wid, 'worker.json');
|
||||
if (!estLiFayl(kartochka)) continue;
|
||||
const card = readJson(kartochka) ?? {};
|
||||
const konecPut = join(workersDir, wid, 'exit.json');
|
||||
const prigovoryPut = join(workersDir, wid, 'items-verified.jsonl');
|
||||
rabotniki.push({
|
||||
worker_id: card.worker_id ?? wid,
|
||||
kartochka,
|
||||
konec: estLiFayl(konecPut) ? konecPut : null,
|
||||
prigovory: estLiFayl(prigovoryPut) ? prigovoryPut : null,
|
||||
ugol: card.workspace && estLiFayl(card.workspace) ? card.workspace : null,
|
||||
});
|
||||
}
|
||||
if (!rabotniki.length) chego_net.push('карточек работников не нашлось ни одной — судить некого и не по чему');
|
||||
|
||||
// 🪤 Угол работника к этой минуте мог быть уже снесён уборкой. Это НЕ поломка, но и молчать
|
||||
// нельзя: судья, не найдя в углу кода, обязан знать, что кода там нет вовсе, а не решить,
|
||||
// что работник ничего не написал.
|
||||
const bezUgla = rabotniki.filter(r => !r.ugol).map(r => r.worker_id);
|
||||
if (bezUgla.length) {
|
||||
chego_net.push(`рабочих углов больше нет у: ${bezUgla.join(', ')} — их код смотреть негде, только по коммитам`);
|
||||
}
|
||||
|
||||
let zadanie = null;
|
||||
if (run?.plan_path) {
|
||||
const put = join(koren, String(run.plan_path));
|
||||
if (estLiFayl(put)) zadanie = put;
|
||||
else if (estLiFayl(String(run.plan_path))) zadanie = String(run.plan_path);
|
||||
else chego_net.push(`задания (плана) по пути «${run.plan_path}» на месте нет — что именно заказывали, проверить нечем`);
|
||||
} else {
|
||||
chego_net.push('в карточке прогона не назван план — задание ночи неизвестно');
|
||||
}
|
||||
|
||||
const listPut = priyomkaDir();
|
||||
const list = estLiFayl(listPut) ? listPut : null;
|
||||
if (!list) chego_net.push('приёмочных бумаг нет — сверить работу с тем, как её обещали принимать, нечем');
|
||||
|
||||
const bumagi = bumagiRabotnikov(koren);
|
||||
if (!bumagi.length) chego_net.push('бумаг работников «-zamer-do.md» не нашлось ни одной');
|
||||
|
||||
return {
|
||||
kartochka_progona,
|
||||
slova_nochi,
|
||||
lenta,
|
||||
rabotniki,
|
||||
bumagi,
|
||||
zadanie,
|
||||
list,
|
||||
chego_net,
|
||||
};
|
||||
}
|
||||
|
||||
// --- наказ -------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Чем кончилась ночь — словами, которые прочтёт судья.
|
||||
*
|
||||
* 🔴🔴 ЗАЧЕМ ЭТО ВООБЩЕ ЕСТЬ. Судья приходит на КАЖДУЮ ночь, дошедшую до закрытия прогона
|
||||
* (решение владельца 12: «не реже раза за ночь»). Но провальная ночь и удачная — это два
|
||||
* РАЗНЫХ дела, и главный вопрос у них разный: на удачной спрашивают «чем заплатили за
|
||||
* успех», на провальной — «почему не вышло». Приди судья на провал, не зная о нём, он
|
||||
* судил бы его как удачу, и весь смысл его прихода пропал бы.
|
||||
*
|
||||
* 🔴 ЧИСЛА НАЗЫВАЮТСЯ СЛОВОМ НАДЗИРАТЕЛЯ, А НЕ ЗАМЕРОМ. Их даёт тот самый, кого судья
|
||||
* судит. Не скажи мы этого вслух — судья принял бы их за проверенное, а «поверил числу
|
||||
* подсудимого» и есть ровно та болезнь, ради которой он заводится.
|
||||
*/
|
||||
function slovaOItoge(itog) {
|
||||
const s = [];
|
||||
s.push('━━━ ЧЕМ КОНЧИЛАСЬ ЭТА НОЧЬ ━━━');
|
||||
s.push('');
|
||||
|
||||
if (!itog || typeof itog !== 'object') {
|
||||
s.push('🔴 Чем кончилась эта ночь, мне не сказали — зовущий итога не подал. Сочти сам, по');
|
||||
s.push('уликам, и в приговоре скажи вслух, что считал сам: иначе выйдет, будто числа тебе');
|
||||
s.push('дали, а их не давал никто.');
|
||||
s.push('');
|
||||
return s;
|
||||
}
|
||||
|
||||
const spisok = a => (Array.isArray(a) && a.length ? a.join(', ') : 'ни одного');
|
||||
const vsego = Number(itog.vsego ?? 0);
|
||||
const zakryto = Array.isArray(itog.zakryto) ? itog.zakryto : [];
|
||||
const neZakryto = Array.isArray(itog.ne_zakryto) ? itog.ne_zakryto : [];
|
||||
const otlozheno = Array.isArray(itog.otlozheno) ? itog.otlozheno : [];
|
||||
|
||||
s.push('🔴 Это СЛОВО НАДЗИРАТЕЛЯ о самом себе, а не замер. Он — один из тех двоих, кого ты');
|
||||
s.push('судишь. Пересчитай эти числа по уликам сам; разойдись они с уликами — это находка,');
|
||||
s.push('и находка дорогая.');
|
||||
s.push('');
|
||||
s.push(`· Пунктов в задании: ${vsego}`);
|
||||
s.push(`· Засчитано надзирателем: ${zakryto.length} — а именно: ${spisok(zakryto)}`);
|
||||
s.push(`· НЕ закрыто: ${neZakryto.length} — а именно: ${spisok(neZakryto)}`);
|
||||
s.push(`· Из них отложено надзирателем (пункт дважды не сошёлся): ${spisok(otlozheno)}`);
|
||||
// 🪤 Дыра названа вслух: отложенное помнит только тот надзиратель, который откладывал.
|
||||
s.push(' 🪤 Этот список неполон по устройству: отложенное помнит только тот надзиратель,');
|
||||
s.push(' который откладывал. Отложенное соседями стоит выше, в «НЕ закрыто».');
|
||||
s.push('');
|
||||
|
||||
if (vsego === 0) {
|
||||
s.push('🟡 Пунктов в задании не названо ни одного — судить по счёту закрытого тут нечего.');
|
||||
s.push('Тем важнее ответить, дошло ли до работы вообще и что этой ночью вправду произошло.');
|
||||
} else if (itog.dodelana === true) {
|
||||
s.push('🟢 Все пункты задания засчитаны — ночь дошла до конца.');
|
||||
s.push('🔴 Тем внимательнее смотри, ЧЕМ ЗА ЭТО ЗАПЛАЧЕНО. Слово «засчитано» сказал');
|
||||
s.push('надзиратель о работе, которую сам же и раздавал: выдавал пункты и проверял их');
|
||||
s.push('один и тот же. Ночь, где всё зелено, — самый удобный случай для дорогой ошибки');
|
||||
s.push('проехать незамеченной.');
|
||||
} else {
|
||||
s.push('🔴🔴 ЭТА НОЧЬ НЕ ДОДЕЛАНА — И ЭТО МЕНЯЕТ ТВОЙ ГЛАВНЫЙ ВОПРОС.');
|
||||
s.push('На удачной ночи спрашивают «чем заплатили за успех». Здесь спрашивают другое:');
|
||||
s.push('ПОЧЕМУ НЕ ВЫШЛО. Ответь на это ПЕРВЫМ, прежде пяти вопросов, и ответь по уликам,');
|
||||
s.push('а не по чувству: возьми каждый незакрытый пункт поимённо, найди, на чём он встал');
|
||||
s.push('и сколько попыток ему дали, и скажи прямо: помешало ли делу устройство самой ночи');
|
||||
s.push('(правила надзирателя, разводка работников по файлам, сроки, пределы) или дело было');
|
||||
s.push('только в самой работе. Ночь, которая не вышла, — это ровно то, ради чего тебя');
|
||||
s.push('и заводят.');
|
||||
}
|
||||
s.push('');
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Наказ судье. ПРОСТЫМ РУССКИМ: его слова владелец читает в приговоре, а не разбирает.
|
||||
*
|
||||
* 🔴 Наказ говорит судье ЧЕТЫРЕ вещи и ни одной лишней: кто он, что читать, чем ночь
|
||||
* кончилась и о чём отвечать. Ни одного намёка на то, какой ответ мы ждём, здесь нет
|
||||
* намеренно — судья, которому подсказали вывод, меряет наше согласие, а не правду. Ровно
|
||||
* этой болезнью и болеет пара «надзиратель + помощник», ради которой судья заводится.
|
||||
*
|
||||
* 🔴 `itog` — чем кончилась ночь. Без него судья судил бы всякую ночь как удачную; не дали —
|
||||
* говорим об этом вслух, а не молчим (то же правило, что у списка `chego_net`).
|
||||
*/
|
||||
export function nakazSudye({ runId = null, uliki, itog = null, kudaPisat = PRIGOVOR_FILE } = {}) {
|
||||
const u = uliki ?? { rabotniki: [], bumagi: [], chego_net: [] };
|
||||
const s = [];
|
||||
|
||||
s.push('Ты судья этой ночи. Ты не писал ни задания, ни строчки работы — тебя завели именно');
|
||||
s.push('затем, чтобы посмотреть на сделанное со стороны.');
|
||||
s.push('');
|
||||
s.push('🔴 Почему ты нужен, честно и без прикрас. Ночью работают несколько помощников, и каждому');
|
||||
s.push('велено не трогать ничего, кроме своего файла. Это разумно, но у этого есть цена: обзор');
|
||||
s.push('каждого обрезан до его файла. Замерено на живой смене: за целый круг помощники не поймали');
|
||||
s.push('ни одной ошибки надзирателя — все нашёл он сам. Двое, которые смотрят друг на друга,');
|
||||
s.push('строят согласие, а не правду. Ты — третий, и ты не участвовал.');
|
||||
s.push('');
|
||||
s.push('━━━ ЧТО ТЕБЕ МОЖНО И ЧЕГО НЕЛЬЗЯ ━━━');
|
||||
s.push('');
|
||||
s.push('· Читай что угодно и проверяй утверждения командами — считай, меряй, открывай файлы.');
|
||||
s.push('· 🔴 Ничего не чини и ничего не правь. Ты — оценка, а не работа. Ни одного файла ты');
|
||||
s.push(' не трогаешь: свой приговор просто напиши ответом, его положат на место без тебя.');
|
||||
s.push('· Не верь ни одному числу на слово — ни моему, ни чужому. Не смог проверить — так');
|
||||
s.push(' и напиши: «не проверял и потому не знаю». Это ценнее любой похвалы.');
|
||||
s.push('· Не рассуждай там, где можно замерить. Рассуждение о коде — не замена вопросу к коду.');
|
||||
s.push('');
|
||||
s.push('━━━ УЛИКИ ЭТОЙ НОЧИ ━━━');
|
||||
if (runId) s.push(`Прогон: ${runId}`);
|
||||
s.push('');
|
||||
s.push(`· Карточка прогона (кто был, что заказывали, чем кончилось): ${u.kartochka_progona}`);
|
||||
if (u.slova_nochi) s.push(`· Что ночь написала о себе строка за строкой: ${u.slova_nochi}`);
|
||||
if (u.lenta) s.push(`· Лента событий по минутам: ${u.lenta}`);
|
||||
if (u.zadanie) s.push(`· Задание (план), по которому шла ночь: ${u.zadanie}`);
|
||||
if (u.list) s.push(`· Приёмочные бумаги — как работу обещали принимать: ${u.list}`);
|
||||
for (const r of u.rabotniki) {
|
||||
const chasti = [`карточка ${r.kartochka}`];
|
||||
if (r.konec) chasti.push(`конец ${r.konec}`);
|
||||
if (r.prigovory) chasti.push(`приговоры по пунктам ${r.prigovory}`);
|
||||
if (r.ugol) chasti.push(`рабочий угол ${r.ugol}`);
|
||||
s.push(`· Работник ${r.worker_id}: ${chasti.join('; ')}`);
|
||||
}
|
||||
for (const b of u.bumagi) s.push(`· Бумага работника: ${b}`);
|
||||
s.push('');
|
||||
if (u.chego_net?.length) {
|
||||
s.push('🔴 ЧЕГО В УЛИКАХ НЕТ (сказано вслух, чтобы ты не принял половину дела за целое):');
|
||||
for (const n of u.chego_net) s.push(`· ${n}`);
|
||||
s.push('');
|
||||
}
|
||||
for (const stroka of slovaOItoge(itog)) s.push(stroka);
|
||||
s.push('━━━ НА ЧТО ОТВЕТИТЬ. ПЯТЬ ВОПРОСОВ, И ВСЕ ПЯТЬ ОБЯЗАТЕЛЬНЫ ━━━');
|
||||
s.push('');
|
||||
s.push(`${PYAT_VOPROSOV[0]}.`);
|
||||
s.push(' Ищи места, где двое сошлись во мнении, а замера под этим мнением нет. Согласие —');
|
||||
s.push(' это не доказательство. Самое дорогое такое согласие называй первым.');
|
||||
s.push('');
|
||||
s.push(`${PYAT_VOPROSOV[1]}.`);
|
||||
s.push(' Возьми числа из бумаг и пересчитай их сам, командой, а не глазами. Одно число,');
|
||||
s.push(' написанное в двух местах по-разному, — находка.');
|
||||
s.push('');
|
||||
s.push(`${PYAT_VOPROSOV[2]}.`);
|
||||
s.push(' Каждое новое правило сверь с находкой, из которой оно выросло. Правило шире своей');
|
||||
s.push(' находки разрешает то, чего автор не имел в виду. Скажи прямо — что именно.');
|
||||
s.push('');
|
||||
s.push(`${PYAT_VOPROSOV[3]}.`);
|
||||
s.push(' Найди вопрос, которого не задал никто. Это самое трудное и самое ценное.');
|
||||
s.push('');
|
||||
s.push(`${PYAT_VOPROSOV[4]}.`);
|
||||
s.push(' Посмотри, не измельчали ли возражения после похвалы и не сменился ли их предмет.');
|
||||
s.push(' Если нет — так и скажи; ответ «не измельчали» тоже ответ.');
|
||||
s.push('');
|
||||
s.push('━━━ КАК ПИСАТЬ ПРИГОВОР ━━━');
|
||||
s.push('');
|
||||
s.push('· Простым русским. Твой приговор читает хозяин, а он не программист.');
|
||||
s.push(`· 🔴 ПЕРВОЙ СТРОКОЙ напиши ровно так: «${METKA_GLAVNOGO} …» и одним предложением`);
|
||||
s.push(' назови самое дорогое, что ты нашёл. Эту строку хозяин прочтёт утром в сводке —');
|
||||
s.push(' всё остальное он откроет, только если она его зацепит.');
|
||||
s.push('· Дальше — пять разделов по пяти вопросам, ровно в этом порядке.');
|
||||
s.push('· В конце — раздел «Чего я сам не проверил и потому не знаю», поимённо. Ты заведён');
|
||||
s.push(' против выдавания непроверенного за замеренное — начни с себя.');
|
||||
s.push(`· Твой ответ целиком ляжет в файл «${kudaPisat}» рядом с уликами. Писать его руками`);
|
||||
s.push(' не нужно, и создавать никаких файлов тоже.');
|
||||
|
||||
return s.join('\n');
|
||||
}
|
||||
|
||||
// --- приговор ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Главное одной строкой — то, что доедет до владельца в утренней сводке.
|
||||
*
|
||||
* 🪤 Метки нет — отвечаем «нет», а не берём первую попавшуюся строку. Первая строка чужого
|
||||
* текста — это заголовок, вступление или пустота, и выдать её за вывод судьи значило бы
|
||||
* положить владельцу в сводку случайные слова под видом приговора.
|
||||
*/
|
||||
export function glavnoeIzPrigovora(tekst) {
|
||||
const s = String(tekst ?? '');
|
||||
const i = s.indexOf(METKA_GLAVNOGO);
|
||||
if (i === -1) return null;
|
||||
const hvost = s.slice(i + METKA_GLAVNOGO.length);
|
||||
const stroka = hvost.split('\n')[0].trim();
|
||||
return stroka ? stroka : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Слово судьи из его потока.
|
||||
*
|
||||
* 🔴 Пустой ответ приговором НЕ считается — тот же урок, что у работника (`isRealSuccess`):
|
||||
* 334 прогона возвращали «ошибки нет» при полном провале. Судья, сказавший пустоту, — это
|
||||
* судья, который не судил.
|
||||
*/
|
||||
export function itogIzPotoka(potok) {
|
||||
let itog = null;
|
||||
for (const line of String(potok ?? '').split('\n')) {
|
||||
const event = parseStreamLine(line);
|
||||
if (!event || event.type !== 'result') continue;
|
||||
if (event.is_error) { itog = null; continue; }
|
||||
const t = typeof event.result === 'string' ? event.result.trim() : '';
|
||||
itog = t ? event.result : null;
|
||||
}
|
||||
return itog;
|
||||
}
|
||||
|
||||
// --- замок «не больше одного раза» -------------------------------------------
|
||||
|
||||
/**
|
||||
* Занять место судьи. Одно неделимое действие файловой системы.
|
||||
*
|
||||
* 🔴 Почему не «прочитать, посмотреть, записать»: прочтение и запись — это два действия,
|
||||
* и двое, дошедшие сюда в одну секунду, оба прочли бы «судьи нет» и оба родили бы своего.
|
||||
* Ровно эту беду затея уже доказывала живьём на четырёх процессах для `file-claims.json`.
|
||||
* Запрет на перезапись (`flag: 'wx'`) не даёт второму даже начать.
|
||||
*/
|
||||
export function zanyatMestoSudi(runDirPath, { at = Date.now() } = {}) {
|
||||
const put = join(runDirPath, OTMETKA_FILE);
|
||||
try {
|
||||
mkdirSync(runDirPath, { recursive: true });
|
||||
writeFileSync(put, JSON.stringify({ zavodili_v: at, kto: SUDYA_ID }, null, 2), { flag: 'wx' });
|
||||
return { ok: true, prichina: null, put };
|
||||
} catch (err) {
|
||||
if (err?.code === 'EEXIST') {
|
||||
return {
|
||||
ok: false,
|
||||
put,
|
||||
prichina: 'судью по этому прогону уже заводили — второй раз не зовём: это лишние деньги '
|
||||
+ 'владельца и две разные правды утром',
|
||||
};
|
||||
}
|
||||
return { ok: false, put, prichina: `отметку судьи положить не удалось: ${err?.message ?? err}` };
|
||||
}
|
||||
}
|
||||
|
||||
// --- рождение ----------------------------------------------------------------
|
||||
|
||||
// Настройки, с которыми шли работники этого прогона. Судья идёт с ТЕМИ ЖЕ: в них лежат
|
||||
// замки владельца (запреты, предупреждение про боевой), и пустить судью мимо них значило бы
|
||||
// выпустить в ночь прибор без предохранителей.
|
||||
function nastroykiProgona(runDirPath) {
|
||||
const workersDir = join(runDirPath, 'workers');
|
||||
let imena = [];
|
||||
try { imena = readdirSync(workersDir).sort(); } catch { return null; }
|
||||
for (const wid of imena) {
|
||||
const card = readJson(join(workersDir, wid, 'worker.json'));
|
||||
const put = card?.settings_path;
|
||||
if (put && existsSync(put)) return put;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Живое рождение судьи. Отдельным именем — чтобы его можно было подменить в проверках
|
||||
* и чтобы `zavestiSudyu` осталась читаемой.
|
||||
*
|
||||
* 🔴🔴 ПОЛ, А НЕ ПОТОЛОК. Под прибором проверок живого судью не рождаем НИКОГДА — ни с
|
||||
* подставкой, ни без неё, ни с забытой. Это ровно то же правило и тот же прибор
|
||||
* (`podPriboromProverok`), каким `paths.mjs` не пускает проверки в оплаченные улики
|
||||
* владельца. Причина та же: подставку однажды забудут, и забытая подставка тратила бы
|
||||
* деньги владельца молча, прогон за прогоном.
|
||||
* 🔴 `env` — ДОВОД, а не `process.env` изнутри: иначе этот пол нечем показать красным,
|
||||
* а сторож, которого не видели красным, не сторожит ничего.
|
||||
*
|
||||
* 🔴 `startFn` — ДОВОД БЕЗ УМОЛЧАНИЯ, и это не забывчивость. Рождение подаёт надзиратель
|
||||
* (единственный, кому оно разрешено поимённо, — см. ввозы в шапке). Подставь мы умолчание —
|
||||
* судья завёл бы свою дорогу к деньгам владельца мимо счёта рождений.
|
||||
*
|
||||
* Отвечает ВСЕГДА и никогда не бросает: `{ ok, prigovor, prichina }`.
|
||||
* 🪤 Поле зовётся `prigovor`, а не `tekst`, НАРОЧНО и с оговоркой вслух: по слову `tekst:`
|
||||
* узнаёт свои цели сторож немых приговоров (`nemye-prigovory.test.mjs`), а он умеет искать
|
||||
* читателя только внутри кода — приговор же судьи уходит к владельцу ДРУГОЙ дорогой,
|
||||
* файлом и строкой в сводке, и сторож её не видит. Имя `prigovor` вернее по смыслу и снимает
|
||||
* ложную тревогу; что приговор вправду доезжает до владельца, сторожат проверки этого файла
|
||||
* («файл приговора на месте», «строка в сводке есть»).
|
||||
*/
|
||||
export function rozhdenieSudi({
|
||||
nakaz,
|
||||
runDirPath,
|
||||
runId = null,
|
||||
cfg = {},
|
||||
predelMs = null,
|
||||
env = process.env,
|
||||
startFn = null,
|
||||
cwd = REPO_ROOT,
|
||||
} = {}) {
|
||||
return new Promise(resolve => {
|
||||
let konchili = false;
|
||||
let chasy = null;
|
||||
const konec = otvet => {
|
||||
if (konchili) return;
|
||||
konchili = true;
|
||||
if (chasy) clearTimeout(chasy);
|
||||
resolve(otvet);
|
||||
};
|
||||
|
||||
if (podPriboromProverok(env)) {
|
||||
konec({
|
||||
ok: false,
|
||||
prigovor: null,
|
||||
prichina: 'идёт прибор проверок — живого судью здесь не рождаем никогда: это были бы '
|
||||
+ 'настоящие деньги владельца, потраченные проверкой',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof startFn !== 'function') {
|
||||
konec({
|
||||
ok: false,
|
||||
prigovor: null,
|
||||
prichina: 'судью рождать нечем: зовущий не подал дорогу к рождению. Своей здесь нет '
|
||||
+ 'намеренно — она одна на затею и живёт у надзирателя, вместе с воротами счёта',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔴 Голова судьи — из настроек владельца, своего слова здесь нет. Причина денежная:
|
||||
// разные головы едят запас по-разному, а запас у владельца один на всё.
|
||||
// 🟡 Своей настройки у судьи пока нет (см. оговорку у `PREDEL_SUDI_MINUT`) — идёт той же
|
||||
// головой, что и работники.
|
||||
const model = cfg?.worker_model ?? null;
|
||||
if (!model) {
|
||||
konec({
|
||||
ok: false,
|
||||
prigovor: null,
|
||||
prichina: 'не сказано, какой головой заводить судью: в настройках нет worker_model. '
|
||||
+ 'Подставлять голову молча нельзя — это чужой запас',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const settingsPath = nastroykiProgona(runDirPath);
|
||||
if (!settingsPath) {
|
||||
konec({
|
||||
ok: false,
|
||||
prigovor: null,
|
||||
prichina: 'настроек прогона не нашлось (ни у одного работника нет settings_path на диске) — '
|
||||
+ 'судья пошёл бы в ночь без замков владельца, а этого делать нельзя',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let child;
|
||||
try {
|
||||
child = startFn({
|
||||
cwd,
|
||||
prompt: nakaz,
|
||||
settingsPath,
|
||||
sessionId: randomUUID(),
|
||||
// 🪤 Имя базы обязательное (без него `workerEnv` намеренно падает), и оно у судьи
|
||||
// СВОЁ и заведомо несуществующее. Судья проверок не гоняет вовсе; полезет он вдруг
|
||||
// их гонять — упадёт громко на своей базе, а не затрёт чужую при зелёном ответе.
|
||||
dbName: `night_sudya_${String(runId ?? 'bez-imeni').replace(/[^\w]+/g, '_')}`,
|
||||
model,
|
||||
// Каталог прогона лежит вне хранилища — без этого судья не прочёл бы ни одной улики.
|
||||
addDirs: [runDirPath],
|
||||
});
|
||||
} catch (err) {
|
||||
konec({ ok: false, prigovor: null, prichina: `судью не удалось запустить: ${err?.message ?? err}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const minut = PREDEL_SUDI_MINUT;
|
||||
const predel = Number.isFinite(predelMs) && predelMs > 0 ? predelMs : minut * 60_000;
|
||||
|
||||
let syro = '';
|
||||
let oshibki = '';
|
||||
child.stdout?.on?.('data', chunk => { syro += String(chunk); });
|
||||
child.stderr?.on?.('data', chunk => { oshibki += String(chunk); });
|
||||
child.on?.('error', err => {
|
||||
konec({ ok: false, prigovor: null, prichina: `судья сорвался на запуске: ${err?.message ?? err}` });
|
||||
});
|
||||
child.on?.('exit', code => {
|
||||
const slovo = itogIzPotoka(syro);
|
||||
if (slovo) { konec({ ok: true, prigovor: slovo, prichina: null }); return; }
|
||||
const hvost = oshibki.trim() ? ` Последние слова: ${oshibki.trim().slice(0, 400)}` : '';
|
||||
konec({
|
||||
ok: false,
|
||||
prigovor: null,
|
||||
prichina: `судья кончился (код ${code}), а приговора не отдал — говорить о ночи ему было нечем.${hvost}`,
|
||||
});
|
||||
});
|
||||
|
||||
chasy = setTimeout(() => {
|
||||
// 🔴 Гасим. Ночь уже закрыта, сторожить судью больше некому, и зациклившийся судья жёг
|
||||
// бы запас владельца до самого утра — ровно тот «пустой расход», от которого затея
|
||||
// сторожит работников.
|
||||
try { child.kill?.(); } catch { /* уже мёртв — и хорошо */ }
|
||||
konec({
|
||||
ok: false,
|
||||
prigovor: null,
|
||||
prichina: `судья не уложился в ${minut} минут и погашен — приговора он не отдал`,
|
||||
});
|
||||
}, predel);
|
||||
chasy.unref?.();
|
||||
});
|
||||
}
|
||||
|
||||
// --- сам зов -----------------------------------------------------------------
|
||||
|
||||
function skazatVladeltsu(runDirPath, at, line) {
|
||||
// 🔴 Строка ложится в ту же улику, что и слова надзирателя (`summary-lines.jsonl`), и
|
||||
// потому доезжает до утренней бумаги БЕЗ единой правки чужих файлов: `utro.mjs` читает
|
||||
// эту улику сам и печатает её в графе «ЧТО НОЧЬ РАССКАЗАЛА О СЕБЕ», а сырую строку
|
||||
// целиком — в части для мастера.
|
||||
// 🪤 `worker_id: null` — судья не работник, и подписывать его строку именем работника
|
||||
// значило бы соврать про состав ночи.
|
||||
try {
|
||||
appendLine(join(runDirPath, SUMMARY_LINES_FILE), { at, worker_id: null, line });
|
||||
} catch { /* сказать не вышло — но ронять из-за этого нечего: ниже есть лента */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Завести судью на этот прогон. Зовётся ОДИН раз — из `closeRunIfLast`, когда погас
|
||||
* последний работник и прогон закрывается.
|
||||
*
|
||||
* 🔴🔴 НА КАЖДУЮ НОЧЬ, А НЕ ТОЛЬКО НА УДАЧНУЮ. Решение владельца 12: «не реже раза за
|
||||
* ночь». Зов НЕ смотрит на флаг `reviewer_starts` (тот про сборщик экранов продукта):
|
||||
* стой он на этом флаге — судью получала бы только ночь, где и так всё сошлось, а ночь,
|
||||
* которую больше всего стоило судить, оставалась бы без единого взгляда со стороны.
|
||||
*
|
||||
* 🔴 `itog` — чем ночь кончилась (сколько пунктов закрыто, сколько нет и каких). Считает
|
||||
* его зовущий, по файлам; здесь он только доносится до наказа. Не дали — наказ скажет
|
||||
* судье об этом вслух, а не промолчит.
|
||||
*
|
||||
* 🔴 НИКОГДА НЕ БРОСАЕТ. Её зовёт надзиратель в ту минуту, когда закрывает ночь; умри она
|
||||
* здесь — и ночь кончилась бы молча, ровно тем классом беды, из-за которого 334 прогона
|
||||
* умирали три месяца незамеченными.
|
||||
*
|
||||
* 🔴 НО И МОЛЧА НЕ ГЛОТАЕТ. Всякая неудача — строка владельцу и запись в ленту.
|
||||
*/
|
||||
export async function zavestiSudyu({
|
||||
runDirPath,
|
||||
runId = null,
|
||||
cfg = {},
|
||||
// 🔴 Чем кончилась ночь. Умолчание `null` — «не сказали», и наказ произнесёт это вслух.
|
||||
itog = null,
|
||||
now = () => Date.now(),
|
||||
koren = REPO_ROOT,
|
||||
// 🔴 Умолчание НАСТОЯЩЕЕ — живое рождение судьи. Подставка подаётся ЯВНО и только
|
||||
// в проверках: пустышка, отвечающая «всё хорошо», в этом хозяйстве за три куска нашлась
|
||||
// пять раз, и умолчанием здесь она означала бы, что судьи нет вовсе, а бумаги о нём есть.
|
||||
rodit = rozhdenieSudi,
|
||||
// 🔴 Дорога к рождению. Своей здесь нет намеренно (см. ввозы в шапке): её подаёт
|
||||
// надзиратель — единственный, кому подходить к деньгам владельца разрешено поимённо.
|
||||
// Не подали — судья не родится и скажет об этом человеческой строкой, а не молча.
|
||||
roditRebyonka = null,
|
||||
predelMs = null,
|
||||
} = {}) {
|
||||
const at = now();
|
||||
|
||||
const mesto = zanyatMestoSudi(runDirPath, { at });
|
||||
if (!mesto.ok) {
|
||||
// 🪤 Второй зов — это НЕ новая беда для владельца, а защита, которая отработала.
|
||||
// Строки в сводку не пишем: первый зов о судье уже сказал, и вторая строка об одном
|
||||
// и том же — то самое дублирование, от которого сводку берегут. В ленту — пишем.
|
||||
try {
|
||||
logEvent(runDirPath, { kind: 'sudya_povtor', prichina: mesto.prichina }, at);
|
||||
} catch { /* лента не открылась — дальше некуда, ронять ночь из-за этого нельзя */ }
|
||||
return { zaveden: false, put: null, prichina: mesto.prichina, glavnoe: null, stroka: null };
|
||||
}
|
||||
|
||||
let otvet;
|
||||
try {
|
||||
const uliki = ulikiProgona(runDirPath, { koren });
|
||||
const nakaz = nakazSudye({ runId, uliki, itog });
|
||||
otvet = await rodit({ nakaz, runDirPath, runId, cfg, predelMs, startFn: roditRebyonka });
|
||||
} catch (err) {
|
||||
otvet = { ok: false, prigovor: null, prichina: String(err?.message ?? err) };
|
||||
}
|
||||
|
||||
if (!otvet?.ok || !String(otvet?.prigovor ?? '').trim()) {
|
||||
const prichina = otvet?.prichina ?? 'причина не названа';
|
||||
const line = `Судью завести не удалось: ${prichina}. Ночь закрыта как обычно, но взгляда со `
|
||||
+ 'стороны на эту работу нет — принимать её придётся вам самим.';
|
||||
skazatVladeltsu(runDirPath, at, line);
|
||||
try {
|
||||
logEvent(runDirPath, { kind: 'sudya_ne_zaveden', prichina }, at);
|
||||
} catch { /* см. выше */ }
|
||||
return { zaveden: false, put: null, prichina, glavnoe: null, stroka: line };
|
||||
}
|
||||
|
||||
const glavnoe = glavnoeIzPrigovora(otvet.prigovor);
|
||||
const put = join(runDirPath, PRIGOVOR_FILE);
|
||||
try {
|
||||
const shapka = [
|
||||
'# Приговор судьи ночи',
|
||||
'',
|
||||
`Прогон: ${runId ?? 'без имени'}. Судья не писал ни задания, ни работы — его завели, чтобы`,
|
||||
'посмотреть на сделанное целиком и со стороны. Ничего он не чинил и не правил.',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
].join('\n');
|
||||
writeFileSync(put, `${shapka}${String(otvet.prigovor).trim()}\n`, 'utf8');
|
||||
} catch (err) {
|
||||
const prichina = `приговор судьи получен, но положить его файлом не удалось: ${err?.message ?? err}`;
|
||||
const line = `Судью завести не удалось: ${prichina}.`;
|
||||
skazatVladeltsu(runDirPath, at, line);
|
||||
try { logEvent(runDirPath, { kind: 'sudya_ne_zaveden', prichina }, at); } catch { /* см. выше */ }
|
||||
return { zaveden: false, put: null, prichina, glavnoe, stroka: line };
|
||||
}
|
||||
|
||||
// 🔴 Строка владельцу читается ГЛАЗАМИ и обязана читаться даже тогда, когда путь к файлу
|
||||
// утренняя бумага заменит пометкой «слово мастера» (она так делает со всякой латиницей).
|
||||
// Поэтому имя файла стоит В КОНЦЕ, а смысл — в начале: пропади хвост, строка всё равно
|
||||
// говорит владельцу, что судья был и что он сказал.
|
||||
const line = glavnoe
|
||||
? `Судья ночи прочёл эту ночь целиком и сказал вот что: ${glavnoe} Весь его приговор лежит `
|
||||
+ `отдельным файлом в каталоге этого прогона — имя файла ниже, в части для мастера: ${put}`
|
||||
: 'Судья ночи прочёл эту ночь целиком, но главного одной строкой не назвал — читать его '
|
||||
+ `приговор придётся целиком. Он лежит отдельным файлом в каталоге прогона: ${put}`;
|
||||
skazatVladeltsu(runDirPath, at, line);
|
||||
try {
|
||||
logEvent(runDirPath, { kind: 'sudya_prigovor', put, glavnoe: glavnoe ?? null }, at);
|
||||
} catch { /* см. выше */ }
|
||||
|
||||
return { zaveden: true, put, prichina: null, glavnoe, stroka: line };
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
// Проверки СУДЬИ НОЧИ. Писаны ДО кода — так велит договор смены.
|
||||
//
|
||||
// 🔴 Чего здесь нет и быть не должно: живого рождения судьи. Ни одна проверка в этом файле
|
||||
// не тратит ни копейки владельца. Рождение подставляется доводом `rodit`/`startFn`, а на
|
||||
// случай, если кто-то однажды подставку забудет, в самом приборе стоит ПОЛ (`podPriboromProverok`) —
|
||||
// и на него здесь отдельная проверка.
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { readJson, writeJsonAtomic, readLines } from './state.mjs';
|
||||
import { SUMMARY_LINES_FILE } from './paths.mjs';
|
||||
import {
|
||||
SUDYA_ID, PRIGOVOR_FILE, OTMETKA_FILE, METKA_GLAVNOGO, PREDEL_SUDI_MINUT,
|
||||
PYAT_VOPROSOV,
|
||||
ulikiProgona, nakazSudye, glavnoeIzPrigovora, itogIzPotoka,
|
||||
zanyatMestoSudi, rozhdenieSudi, zavestiSudyu,
|
||||
} from './sudya-nochi.mjs';
|
||||
|
||||
let dir, rdir, koren;
|
||||
|
||||
// Подставной ребёнок: тот же вид, что настоящий запуск отдаёт надзирателю.
|
||||
function podstavnoyRebyonok() {
|
||||
const c = new EventEmitter();
|
||||
c.stdout = new EventEmitter();
|
||||
c.stderr = new EventEmitter();
|
||||
c.killed = false;
|
||||
c.kill = () => { c.killed = true; };
|
||||
c.skazat = obj => c.stdout.emit('data', JSON.stringify(obj) + '\n');
|
||||
c.umeret = (code = 0) => c.emit('exit', code);
|
||||
return c;
|
||||
}
|
||||
|
||||
function sobratProgon({ reviewer_starts = true } = {}) {
|
||||
mkdirSync(join(rdir, 'workers', 'w-1'), { recursive: true });
|
||||
// 🔴 Настройки прогона обязаны ЛЕЖАТЬ НА ДИСКЕ, а не быть просто именем в карточке:
|
||||
// судья идёт с теми же замками владельца, что и работники, и прибор это проверяет.
|
||||
writeFileSync(join(dir, 'settings.json'), '{}');
|
||||
writeJsonAtomic(join(rdir, 'run.json'), {
|
||||
run_id: 'r-1', task_id: 't-1', status: 'finished', workers: ['w-1'],
|
||||
reviewer_starts,
|
||||
plan_path: 'docs/superpowers/plans/пример.md',
|
||||
items: [{ id: '1' }],
|
||||
});
|
||||
writeJsonAtomic(join(rdir, 'workers', 'w-1', 'worker.json'), {
|
||||
worker_id: 'w-1', status: 'extinguished', workspace: join(dir, 'ws-1'),
|
||||
settings_path: join(dir, 'settings.json'),
|
||||
});
|
||||
writeJsonAtomic(join(rdir, 'workers', 'w-1', 'exit.json'), { status: 'extinguished' });
|
||||
writeFileSync(join(rdir, SUMMARY_LINES_FILE), '');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'night-sudya-'));
|
||||
rdir = join(dir, 'runs', 'r-1');
|
||||
koren = join(dir, 'hranilishche');
|
||||
mkdirSync(join(koren, 'docs', 'superpowers', 'plans'), { recursive: true });
|
||||
writeFileSync(join(koren, 'docs', 'superpowers', 'plans', 'пример.md'), '# план\n');
|
||||
mkdirSync(join(koren, 'tools', 'night'), { recursive: true });
|
||||
writeFileSync(join(koren, 'tools', 'night', 'proba-zamer-do.md'), '# бумага работника\n');
|
||||
sobratProgon();
|
||||
});
|
||||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('наказ судье — пять вопросов, и владелец их прочтёт', () => {
|
||||
it('в наказе стоят ВСЕ пять вопросов, дословно', () => {
|
||||
expect(PYAT_VOPROSOV).toHaveLength(5);
|
||||
const nakaz = nakazSudye({ runId: 'r-1', uliki: ulikiProgona(rdir, { koren }) });
|
||||
for (const v of PYAT_VOPROSOV) {
|
||||
expect(nakaz, `в наказе нет вопроса «${v}»`).toContain(v);
|
||||
}
|
||||
});
|
||||
|
||||
it('🔴 наказ прямо запрещает судье чинить и править — он оценка, а не работа', () => {
|
||||
const nakaz = nakazSudye({ runId: 'r-1', uliki: ulikiProgona(rdir, { koren }) });
|
||||
expect(nakaz).toMatch(/ничего не чини/i);
|
||||
expect(nakaz).toMatch(/не прав/i); // «в чём я не прав» / «правда другая»
|
||||
});
|
||||
|
||||
it('🔴 наказ велит начать с одной строки главного — её потом читает владелец в сводке', () => {
|
||||
const nakaz = nakazSudye({ runId: 'r-1', uliki: ulikiProgona(rdir, { koren }) });
|
||||
expect(nakaz).toContain(METKA_GLAVNOGO);
|
||||
});
|
||||
|
||||
it('наказ называет улики ЭТОГО прогона по путям, а не «посмотри где-нибудь»', () => {
|
||||
const uliki = ulikiProgona(rdir, { koren });
|
||||
const nakaz = nakazSudye({ runId: 'r-1', uliki });
|
||||
expect(nakaz).toContain(uliki.kartochka_progona);
|
||||
expect(nakaz).toContain(uliki.slova_nochi);
|
||||
expect(nakaz).toContain(uliki.rabotniki[0].kartochka);
|
||||
expect(nakaz).toContain(uliki.zadanie);
|
||||
expect(nakaz).toContain(uliki.bumagi[0]);
|
||||
});
|
||||
|
||||
it('🔴 чего среди улик НЕТ — названо вслух, а не молча пропущено', () => {
|
||||
rmSync(join(koren, 'docs', 'superpowers', 'plans', 'пример.md'));
|
||||
const uliki = ulikiProgona(rdir, { koren });
|
||||
expect(uliki.zadanie).toBe(null);
|
||||
expect(uliki.chego_net.join(' ')).toMatch(/задание|план/i);
|
||||
const nakaz = nakazSudye({ runId: 'r-1', uliki });
|
||||
expect(nakaz).toMatch(/чего в уликах нет/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 🔴🔴 СУДЬЯ ОБЯЗАН ЗНАТЬ, ЧЕМ КОНЧИЛАСЬ НОЧЬ
|
||||
//
|
||||
// Решение владельца 12: судья заводится «не реже раза за ночь» — значит приходит и на
|
||||
// ПРОВАЛЬНУЮ ночь тоже. А на провальной ночи главный вопрос ДРУГОЙ: не «чем заплатили
|
||||
// за успех», а «почему не вышло». Не скажи мы судье итог — он судил бы всякую ночь как
|
||||
// удачную и половину дела не увидел бы вовсе.
|
||||
describe('🔴🔴 наказ говорит судье, ЧЕМ КОНЧИЛАСЬ НОЧЬ', () => {
|
||||
const uliki = () => ulikiProgona(rdir, { koren });
|
||||
|
||||
it('числа итога стоят в наказе поимённо: сколько закрыто, сколько нет и какие именно', () => {
|
||||
const nakaz = nakazSudye({
|
||||
runId: 'r-1',
|
||||
uliki: uliki(),
|
||||
itog: { vsego: 3, zakryto: ['2'], ne_zakryto: ['1', '3'], otlozheno: ['1'], dodelana: false },
|
||||
});
|
||||
expect(nakaz).toContain('Пунктов в задании: 3');
|
||||
expect(nakaz).toContain('Засчитано надзирателем: 1');
|
||||
expect(nakaz).toContain('НЕ закрыто: 2');
|
||||
// 🔴 Номера, а не только счёт: «два не закрыто» без имён судье некуда приложить.
|
||||
expect(nakaz).toContain('1, 3');
|
||||
expect(nakaz).toMatch(/отложено[^\n]*1/i);
|
||||
});
|
||||
|
||||
it('🔴🔴 ночь НЕ доделана — наказ ставит судье ДРУГОЙ главный вопрос: почему не вышло', () => {
|
||||
const nakaz = nakazSudye({
|
||||
runId: 'r-1',
|
||||
uliki: uliki(),
|
||||
itog: { vsego: 2, zakryto: ['2'], ne_zakryto: ['1'], otlozheno: ['1'], dodelana: false },
|
||||
});
|
||||
expect(nakaz, 'судье не сказали, что ночь не доделана').toMatch(/не доделана/i);
|
||||
expect(nakaz, 'главный вопрос провальной ночи не поставлен').toMatch(/почему не вышло/i);
|
||||
// И вопрос этот не про работу одну: устройство самой ночи судья обязан взвесить тоже.
|
||||
expect(nakaz).toMatch(/устройств[оа] самой ночи/i);
|
||||
});
|
||||
|
||||
it('ночь доделана — сказано и это, и сказано, что зелёное проверяют строже', () => {
|
||||
const nakaz = nakazSudye({
|
||||
runId: 'r-1',
|
||||
uliki: uliki(),
|
||||
itog: { vsego: 2, zakryto: ['1', '2'], ne_zakryto: [], otlozheno: [], dodelana: true },
|
||||
});
|
||||
expect(nakaz).toMatch(/все пункты задания засчитаны/i);
|
||||
expect(nakaz).toMatch(/чем за это заплачено/i);
|
||||
// 🪤 И НЕ ставит вопрос провальной ночи: на удачной он увёл бы судью не туда.
|
||||
expect(nakaz).not.toMatch(/почему не вышло/i);
|
||||
});
|
||||
|
||||
it('🔴 итога судье не подали вовсе — наказ говорит это ВСЛУХ, а не молчит', () => {
|
||||
const nakaz = nakazSudye({ runId: 'r-1', uliki: uliki() });
|
||||
expect(nakaz, 'молчание тут неотличимо от «ночь удалась»').toMatch(/чем кончилась эта ночь, мне не сказали/i);
|
||||
});
|
||||
|
||||
it('🔴 числа названы СЛОВОМ НАДЗИРАТЕЛЯ, а не замером — судья судит и того, кто их дал', () => {
|
||||
const nakaz = nakazSudye({
|
||||
runId: 'r-1',
|
||||
uliki: uliki(),
|
||||
itog: { vsego: 1, zakryto: ['1'], ne_zakryto: [], otlozheno: [], dodelana: true },
|
||||
});
|
||||
expect(nakaz).toMatch(/слово надзирателя/i);
|
||||
});
|
||||
|
||||
it('🔴 итог доезжает СКВОЗЬ `zavestiSudyu` до самого наказа, а не теряется по дороге', async () => {
|
||||
let nakaz = null;
|
||||
const rodit = async d => { nakaz = d.nakaz; return { ok: true, prigovor: 'приговор', prichina: null }; };
|
||||
await zavestiSudyu({
|
||||
runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 1,
|
||||
itog: { vsego: 2, zakryto: [], ne_zakryto: ['1', '2'], otlozheno: ['1'], dodelana: false },
|
||||
});
|
||||
expect(nakaz, 'судью завели, а наказа ему не собрали').toBeTruthy();
|
||||
expect(nakaz).toContain('Пунктов в задании: 2');
|
||||
expect(nakaz).toMatch(/почему не вышло/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('улики прогона собираются КОМАНДОЙ, а не памятью', () => {
|
||||
it('находит карточку прогона, слова ночи, карточки и концы работников', () => {
|
||||
const u = ulikiProgona(rdir, { koren });
|
||||
expect(u.kartochka_progona).toBe(join(rdir, 'run.json'));
|
||||
expect(u.slova_nochi).toBe(join(rdir, SUMMARY_LINES_FILE));
|
||||
expect(u.rabotniki).toHaveLength(1);
|
||||
expect(u.rabotniki[0].worker_id).toBe('w-1');
|
||||
expect(u.rabotniki[0].konec).toBe(join(rdir, 'workers', 'w-1', 'exit.json'));
|
||||
});
|
||||
|
||||
it('бумаги работников `*-zamer-do.md` находятся в хранилище', () => {
|
||||
const u = ulikiProgona(rdir, { koren });
|
||||
expect(u.bumagi).toContain(join(koren, 'tools', 'night', 'proba-zamer-do.md'));
|
||||
});
|
||||
|
||||
it('🪤 каталога работников нет вовсе — не падаем, а честно отдаём пусто', () => {
|
||||
rmSync(join(rdir, 'workers'), { recursive: true, force: true });
|
||||
const u = ulikiProgona(rdir, { koren });
|
||||
expect(u.rabotniki).toEqual([]);
|
||||
expect(u.chego_net.join(' ')).toMatch(/работник/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('главное одной строкой — то, что доедет до владельца в сводке', () => {
|
||||
it('вытаскивает строку после метки', () => {
|
||||
const t = `${METKA_GLAVNOGO} двое сошлись на числе, которого никто не мерил.\n\nдальше подробности`;
|
||||
expect(glavnoeIzPrigovora(t)).toBe('двое сошлись на числе, которого никто не мерил.');
|
||||
});
|
||||
|
||||
it('🪤 метки нет — отвечаем «нет», а не выдумываем первую попавшуюся строку', () => {
|
||||
expect(glavnoeIzPrigovora('просто текст без метки')).toBe(null);
|
||||
expect(glavnoeIzPrigovora('')).toBe(null);
|
||||
expect(glavnoeIzPrigovora(null)).toBe(null);
|
||||
});
|
||||
|
||||
it('🪤 метка есть, а строка за ней пустая — это тоже «нет»', () => {
|
||||
expect(glavnoeIzPrigovora(`${METKA_GLAVNOGO} \n\nтело`)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('итог из потока судьи', () => {
|
||||
it('берёт слово судьи из итогового события', () => {
|
||||
const potok = [
|
||||
JSON.stringify({ type: 'system', subtype: 'init' }),
|
||||
JSON.stringify({ type: 'assistant', message: {} }),
|
||||
JSON.stringify({ type: 'result', is_error: false, result: 'мой приговор' }),
|
||||
].join('\n');
|
||||
expect(itogIzPotoka(potok)).toBe('мой приговор');
|
||||
});
|
||||
|
||||
it('🔴 пустой ответ приговором НЕ считается', () => {
|
||||
expect(itogIzPotoka(JSON.stringify({ type: 'result', is_error: false, result: ' ' }))).toBe(null);
|
||||
expect(itogIzPotoka(JSON.stringify({ type: 'result', is_error: true, result: 'беда' }))).toBe(null);
|
||||
expect(itogIzPotoka('')).toBe(null);
|
||||
expect(itogIzPotoka('мусор не по-json')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('🔴🔴 судья заводится НЕ БОЛЬШЕ ОДНОГО РАЗА за прогон', () => {
|
||||
it('второй заход не занимает место и говорит почему', () => {
|
||||
expect(zanyatMestoSudi(rdir, { at: 1 }).ok).toBe(true);
|
||||
const vtoroy = zanyatMestoSudi(rdir, { at: 2 });
|
||||
expect(vtoroy.ok).toBe(false);
|
||||
expect(vtoroy.prichina).toMatch(/уже/i);
|
||||
expect(existsSync(join(rdir, OTMETKA_FILE))).toBe(true);
|
||||
});
|
||||
|
||||
it('🔴 два зова `zavestiSudyu` подряд рождают судью РОВНО ОДИН раз', async () => {
|
||||
let rozhdeniy = 0;
|
||||
const rodit = async () => { rozhdeniy += 1; return { ok: true, prigovor: `${METKA_GLAVNOGO} раз.`, prichina: null }; };
|
||||
const a = await zavestiSudyu({ runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 1 });
|
||||
const b = await zavestiSudyu({ runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 2 });
|
||||
expect(rozhdeniy, 'судья родился больше одного раза — это лишние деньги владельца').toBe(1);
|
||||
expect(a.zaveden).toBe(true);
|
||||
expect(b.zaveden).toBe(false);
|
||||
expect(b.prichina).toMatch(/уже/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('приговор ложится файлом, и сводка о нём говорит', () => {
|
||||
it('удачный судья: файл приговора на месте, строка в сводке есть, событие в ленте есть', async () => {
|
||||
const rodit = async () => ({ ok: true, prigovor: `${METKA_GLAVNOGO} числа не сходятся.\n\nОтвет 1. …`, prichina: null });
|
||||
const r = await zavestiSudyu({ runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 111 });
|
||||
|
||||
expect(r.zaveden).toBe(true);
|
||||
expect(r.put).toBe(join(rdir, PRIGOVOR_FILE));
|
||||
const tekst = readFileSync(r.put, 'utf8');
|
||||
expect(tekst).toContain('числа не сходятся');
|
||||
expect(tekst).toMatch(/судья/i);
|
||||
|
||||
const stroki = readLines(join(rdir, SUMMARY_LINES_FILE)).map(s => s.line).join('\n');
|
||||
expect(stroki).toMatch(/судья/i);
|
||||
expect(stroki).toContain('числа не сходятся');
|
||||
|
||||
const lenta = readLines(join(rdir, 'events.jsonl')).map(e => e.kind);
|
||||
expect(lenta).toContain('sudya_prigovor');
|
||||
});
|
||||
|
||||
it('🪤 судья не назвал главного одной строкой — говорим это честно, а не молчим', async () => {
|
||||
const rodit = async () => ({ ok: true, prigovor: 'приговор без метки', prichina: null });
|
||||
const r = await zavestiSudyu({ runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 1 });
|
||||
expect(r.zaveden).toBe(true);
|
||||
const stroki = readLines(join(rdir, SUMMARY_LINES_FILE)).map(s => s.line).join('\n');
|
||||
expect(stroki).toMatch(/судья/i);
|
||||
expect(stroki).toMatch(/главного одной строкой (он )?не назвал/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('🔴🔴 смерть судьи НЕ роняет ночь и НЕ проглатывается молча', () => {
|
||||
it('рождение бросило — `zavestiSudyu` не бросает, а честно рассказывает', async () => {
|
||||
const rodit = async () => { throw new Error('нутро программы не найдено'); };
|
||||
const r = await zavestiSudyu({ runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 1 });
|
||||
expect(r.zaveden).toBe(false);
|
||||
expect(r.prichina).toMatch(/нутро программы не найдено/);
|
||||
const stroki = readLines(join(rdir, SUMMARY_LINES_FILE)).map(s => s.line).join('\n');
|
||||
expect(stroki, 'смерть судьи проглочена молча — так нельзя').toMatch(/судью завести не удалось/i);
|
||||
expect(stroki).toMatch(/нутро программы не найдено/);
|
||||
const lenta = readLines(join(rdir, 'events.jsonl')).map(e => e.kind);
|
||||
expect(lenta).toContain('sudya_ne_zaveden');
|
||||
});
|
||||
|
||||
it('судья родился, но приговора не отдал — это неудача, и файла приговора нет', async () => {
|
||||
const rodit = async () => ({ ok: false, prigovor: null, prichina: 'судья кончился, не сказав ни слова' });
|
||||
const r = await zavestiSudyu({ runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 1 });
|
||||
expect(r.zaveden).toBe(false);
|
||||
expect(existsSync(join(rdir, PRIGOVOR_FILE)), 'пустой приговор класть нельзя — его прочтут как настоящий').toBe(false);
|
||||
const stroki = readLines(join(rdir, SUMMARY_LINES_FILE)).map(s => s.line).join('\n');
|
||||
expect(stroki).toMatch(/судью завести не удалось/i);
|
||||
expect(stroki).toContain('не сказав ни слова');
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('🔴 судья — не работник: места в прогоне он не занимает', () => {
|
||||
it('карточки работника ему не заводится, состав прогона не растёт', async () => {
|
||||
const rodit = async () => ({ ok: true, prigovor: 'приговор', prichina: null });
|
||||
await zavestiSudyu({ runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 1 });
|
||||
expect(existsSync(join(rdir, 'workers', SUDYA_ID))).toBe(false);
|
||||
const run = readJson(join(rdir, 'run.json'));
|
||||
expect(run.workers).toEqual(['w-1']);
|
||||
});
|
||||
|
||||
it('🔴 приговоров о пунктах он не пишет — в счёт закрытого не попадает', async () => {
|
||||
const rodit = async () => ({ ok: true, prigovor: 'приговор', prichina: null });
|
||||
await zavestiSudyu({ runDirPath: rdir, runId: 'r-1', rodit, koren, now: () => 1 });
|
||||
expect(existsSync(join(rdir, 'workers', SUDYA_ID, 'items-verified.jsonl'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('🔴🔴 ПОЛ: под прибором проверок живого судью не рождаем НИКОГДА', () => {
|
||||
it('настоящее окружение проверок — рождения нет, и сказано почему', async () => {
|
||||
let zvali = 0;
|
||||
const r = await rozhdenieSudi({
|
||||
nakaz: 'наказ', runDirPath: rdir, runId: 'r-1',
|
||||
startFn: () => { zvali += 1; return podstavnoyRebyonok(); },
|
||||
});
|
||||
expect(zvali, 'под прибором проверок родили ЖИВОГО судью — это деньги владельца').toBe(0);
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.prichina).toMatch(/прибор/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('рождение судьи: поток, предел времени и отказы', () => {
|
||||
const boy = { env: {} }; // «как будто не под прибором» — довод, а не подмена process.env
|
||||
|
||||
it('судья сказал итог — берём его слово', async () => {
|
||||
const c = podstavnoyRebyonok();
|
||||
const p = rozhdenieSudi({
|
||||
nakaz: 'наказ', runDirPath: rdir, runId: 'r-1', ...boy,
|
||||
cfg: { worker_model: 'голова' },
|
||||
startFn: () => c,
|
||||
});
|
||||
c.skazat({ type: 'result', is_error: false, result: 'мой приговор' });
|
||||
c.umeret(0);
|
||||
await expect(p).resolves.toMatchObject({ ok: true, prigovor: 'мой приговор' });
|
||||
});
|
||||
|
||||
it('🔴 судья молчит дольше предела — гасим и говорим вслух', async () => {
|
||||
const c = podstavnoyRebyonok();
|
||||
const r = await rozhdenieSudi({
|
||||
nakaz: 'наказ', runDirPath: rdir, runId: 'r-1', ...boy,
|
||||
cfg: { worker_model: 'голова' },
|
||||
predelMs: 20,
|
||||
startFn: () => c,
|
||||
});
|
||||
expect(c.killed, 'судью не погасили — он жёг бы деньги до утра').toBe(true);
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.prichina).toMatch(/не уложился|предел|минут/i);
|
||||
});
|
||||
|
||||
it('🔴 запуск бросил — это ответ, а не обвал', async () => {
|
||||
const r = await rozhdenieSudi({
|
||||
nakaz: 'наказ', runDirPath: rdir, runId: 'r-1', ...boy,
|
||||
cfg: { worker_model: 'голова' },
|
||||
startFn: () => { throw new Error('claude не найден'); },
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.prichina).toContain('claude не найден');
|
||||
});
|
||||
|
||||
it('🔴 головы судье не назвали — не рождаем и говорим прямо', async () => {
|
||||
let zvali = 0;
|
||||
const r = await rozhdenieSudi({
|
||||
nakaz: 'наказ', runDirPath: rdir, runId: 'r-1', ...boy,
|
||||
cfg: {},
|
||||
startFn: () => { zvali += 1; return podstavnoyRebyonok(); },
|
||||
});
|
||||
expect(zvali).toBe(0);
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.prichina).toMatch(/голов/i);
|
||||
});
|
||||
|
||||
it('🔴 голову судьи берём из настроек владельца, своего слова не подставляем', async () => {
|
||||
let dano = null;
|
||||
const c = podstavnoyRebyonok();
|
||||
const p = rozhdenieSudi({
|
||||
nakaz: 'наказ', runDirPath: rdir, runId: 'r-1', ...boy,
|
||||
cfg: { worker_model: 'голова-работника' },
|
||||
startFn: dovody => { dano = dovody; return c; },
|
||||
});
|
||||
c.skazat({ type: 'result', is_error: false, result: 'п' });
|
||||
c.umeret(0);
|
||||
await p;
|
||||
expect(dano.model).toBe('голова-работника');
|
||||
expect(dano.prompt).toBe('наказ');
|
||||
// 🔴 Свою базу судье дают ЗАВЕДОМО отдельную: общая база на всех даёт зелёные врущие
|
||||
// проверки, и это худший вид беды.
|
||||
expect(dano.dbName).toMatch(/sudya/);
|
||||
});
|
||||
|
||||
it('🔴 дорогу к рождению не подали — не рождаем и говорим прямо', async () => {
|
||||
const r = await rozhdenieSudi({
|
||||
nakaz: 'наказ', runDirPath: rdir, runId: 'r-1', ...boy,
|
||||
cfg: { worker_model: 'голова' },
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.prichina).toMatch(/рождать нечем|не подал/i);
|
||||
});
|
||||
|
||||
it('🔴 настроек прогона нет — не рождаем: судья пошёл бы без замков владельца', async () => {
|
||||
rmSync(join(rdir, 'workers'), { recursive: true, force: true });
|
||||
let zvali = 0;
|
||||
const r = await rozhdenieSudi({
|
||||
nakaz: 'наказ', runDirPath: rdir, runId: 'r-1', ...boy,
|
||||
cfg: { worker_model: 'голова' },
|
||||
startFn: () => { zvali += 1; return podstavnoyRebyonok(); },
|
||||
});
|
||||
expect(zvali).toBe(0);
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.prichina).toMatch(/настро/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('числа, которые нельзя менять молча', () => {
|
||||
it('предел судьи назван и разумен', () => {
|
||||
expect(Number.isFinite(PREDEL_SUDI_MINUT)).toBe(true);
|
||||
expect(PREDEL_SUDI_MINUT).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('имена файлов судьи — по-русски латиницей, но постоянные', () => {
|
||||
expect(PRIGOVOR_FILE).toBe('prigovor-sudi.md');
|
||||
expect(OTMETKA_FILE).toBe('sudya.json');
|
||||
expect(SUDYA_ID).toBe('sudya');
|
||||
});
|
||||
});
|
||||
+112
-4
@@ -43,6 +43,11 @@ import { pustoyRashod, slepokZapisi, schyotOdinakovyh } from './pustoy-rashod.mj
|
||||
// здесь руками: на это стоит сторож (`resume.test.mjs`, П-06), и он прав.
|
||||
import { statePath } from './resume.mjs';
|
||||
import { startWorker, parseStreamLine, isStepBoundary, isRealSuccess, rateLimitFromEvent } from './spawn.mjs';
|
||||
// 🔴🔴 СУДЬЯ НОЧИ (решение владельца 12 от 03.08.2026, подтверждено решениями 134 и 135).
|
||||
// Правило «кто и как судит ночь целиком» живёт отдельным файлом, а ЗОВЁТ его этот — иначе
|
||||
// оно зеленело бы на проверках и не сработало ни разу. Ввоз односторонний: `sudya-nochi.mjs`
|
||||
// не ввозит надзирателя ни одной строкой, кольца нет.
|
||||
import { zavestiSudyu } from './sudya-nochi.mjs';
|
||||
// 🔴 «Сделал ли он дело» — вопрос о СЛЕДЕ, а не о том, как работник завершился. Правило
|
||||
// живёт отдельным файлом и ввозится, а не переписывается здесь: `isRealSuccess` (spawn.mjs)
|
||||
// отвечает только на «отдал ли он годный ответ», и на живой оплаченной пробе 03.08.2026
|
||||
@@ -1085,6 +1090,43 @@ export function closedItemsOfRun(runDirPath) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴🔴 ЧЕМ КОНЧИЛАСЬ НОЧЬ — то, без чего судья судит не ту ночь.
|
||||
*
|
||||
* Решение владельца 12 велит заводить судью «не реже раза за ночь» — значит и на ту ночь,
|
||||
* где половина не вышла. Но провальная ночь и удачная — это два РАЗНЫХ дела: на одной
|
||||
* спрашивают «чем заплатили за успех», на другой «почему не вышло». Не скажи мы судье,
|
||||
* чем ночь кончилась, — он всякую судил бы как удачную, и главного вопроса провальной
|
||||
* ночи не задал бы никто.
|
||||
*
|
||||
* 🔴 СЧИТАЕТСЯ ПО ФАЙЛАМ, А НЕ ПО ПАМЯТИ НАДЗИРАТЕЛЯ. Источник закрытого — тот же
|
||||
* `closedItemsOfRun` (приговоры надзирателей в каталоге прогона), каким считает «сделано»
|
||||
* очередь пунктов и след отложенной задачи в `writeRunOutcome`. Второго счёта затея
|
||||
* не заводит: разойдись они — владелец получил бы утром два разных числа об одной ночи.
|
||||
*
|
||||
* 🪤 `otlozheno` — ЧЕСТНО НЕПОЛНОЕ, и это сказано вслух, а не спрятано. Отложенное помнит
|
||||
* только ТОТ надзиратель, который откладывал (та же названная дыра, что у самого
|
||||
* `otlozhennyePunkty`): пункты, отложенные соседями, попадут в `ne_zakryto`, но не в
|
||||
* `otlozheno`. Судья видит оба списка и потому не примет часть за целое.
|
||||
*/
|
||||
export function chemKonchilasNoch(runDirPath, otlozhennye = []) {
|
||||
const run = readJson(join(runDirPath, 'run.json')) ?? {};
|
||||
const vse = (run.items ?? []).map(i => String(i?.id)).filter(id => id && id !== 'undefined');
|
||||
const zaschitany = new Set(closedItemsOfRun(runDirPath));
|
||||
const zakryto = vse.filter(id => zaschitany.has(id));
|
||||
const ne_zakryto = vse.filter(id => !zaschitany.has(id));
|
||||
const otlozheno = [...otlozhennye].map(String).filter(id => ne_zakryto.includes(id));
|
||||
return {
|
||||
vsego: vse.length,
|
||||
zakryto,
|
||||
ne_zakryto,
|
||||
otlozheno,
|
||||
// 🪤 Ночь без единого пункта в задании доделанной НЕ зовём: там судить по счёту закрытого
|
||||
// нечего вовсе, и «доделана» прозвучало бы похвалой за пустоту.
|
||||
dodelana: vse.length > 0 && ne_zakryto.length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Правка карточки прогона ПОД ЗАМКОМ и с перечитыванием внутри него.
|
||||
//
|
||||
// 🔴🔴 Карточку прогона правят все семеро надзирателей, и правят её чтением-и-записью:
|
||||
@@ -1187,6 +1229,15 @@ export async function supervise({
|
||||
// 🔴 Довод принят ВМЕСТЕ с зовом (правило хозяйства этого файла: принимать параметр можно
|
||||
// только вместе с зовом в том же куске). Зовётся ниже, в `finish`.
|
||||
naytiStenogrammu = stenogrammaPoKartochke,
|
||||
// 🔴🔴 СУДЬЯ НОЧИ. Довод принят ВМЕСТЕ с зовом — зовётся ниже, в `closeRunIfLast`, и на зов
|
||||
// стоят проверки, которые краснеют без него. Шестерых предшественников этого довода
|
||||
// («на вырост», принятых и не звавшихся) файл уже выгонял: они выглядели работающей
|
||||
// кнопкой «стоп», которая на деле никого не слышит.
|
||||
// 🪤 Умолчание НАСТОЯЩЕЕ — живое заведение судьи. Подставка подаётся ЯВНО и только
|
||||
// в проверках: судья стоит денег владельца, и пустышка умолчанием означала бы, что судьи
|
||||
// нет вовсе, а бумаги о нём есть. Второй заслон от живого рождения под прибором проверок
|
||||
// стоит внутри самого судьи (`podPriboromProverok`), и он не отменяет этого.
|
||||
zavestiSudyuFn = zavestiSudyu,
|
||||
}) {
|
||||
const startedAt = now();
|
||||
const seen = {};
|
||||
@@ -1251,16 +1302,68 @@ export async function supervise({
|
||||
// Последнее удавшееся число свободного места — для утренней бумаги (заполняется кругом ниже).
|
||||
let posledniyZamerMesta = null;
|
||||
|
||||
function closeRunIfLast(at) {
|
||||
async function closeRunIfLast(at) {
|
||||
// 🔴 Прогон закрывает надзиратель ПОСЛЕДНЕГО погасшего работника — больше некому:
|
||||
// оркестратор к этому времени давно умер вместе с сессией помощника.
|
||||
// Заведение приёмщика по флагу `reviewer_starts` дописывает кусок 7 — ровно сюда.
|
||||
if (!amILast(runDirPath)) return;
|
||||
// 🔴 Каталог следов передаётся ЯВНО: иначе итог прогона взял бы своё умолчание —
|
||||
// боевой `.night/deferred/`, — и проверки круга оставляли бы там настоящие отказы.
|
||||
// Поймано живьём 02.08.2026: вырез этой строки положил в хранилище восемь настоящих
|
||||
// отказов по задачам, которых никто не откладывал.
|
||||
writeRunOutcome({ runDirPath, runId, lastRunFile, now: at, deferredPath, diskFreeGb: posledniyZamerMesta });
|
||||
|
||||
// ━━━ 🔴🔴 СУДЬЯ НОЧИ. РЕШЕНИЕ ВЛАДЕЛЬЦА 12, ИСПОЛНЕНО 06.08.2026 ━━━
|
||||
//
|
||||
// 🔴 ПОРЯДОК СТРОК ЗДЕСЬ — НЕ ВКУСОВЩИНА. Итог прогона лёг СТРОКОЙ ВЫШЕ: и карточка
|
||||
// прогона, и `last-run.json`, и утренняя бумага владельцу — всё уже на месте. Судья
|
||||
// зовётся ПОСЛЕ, и только так: он оценка, а не работа, и его падение не имеет права
|
||||
// унести бумагу, ради которой вся ночь и затевалась. На этот порядок стоит отдельная
|
||||
// проверка — она смотрит, что в минуту зова судьи итог УЖЕ лежал.
|
||||
//
|
||||
// 🔴🔴 СУДЬЯ ПРИХОДИТ НА КАЖДУЮ НОЧЬ, ДОШЕДШУЮ ДО ЗАКРЫТИЯ ПРОГОНА. Удачную,
|
||||
// наполовину удачную и провальную — одинаково. Решение владельца 12 дословно:
|
||||
// судья заводится «НЕ РЕЖЕ РАЗА ЗА НОЧЬ».
|
||||
//
|
||||
// 🔴 ЧТО ЗДЕСЬ БЫЛО НЕ ТАК И ПОЧЕМУ ЭТО ОШИБКА НАДЗИРАТЕЛЯ, А НЕ СТРОИТЕЛЯ СУДЬИ.
|
||||
// Зов стоял на флаге `reviewer_starts`, а тот флаг блок 5а поднимает ТОЛЬКО когда
|
||||
// все пункты засчитаны (`vseZaschitany`). Отложи ночь хоть один пункт — флага нет,
|
||||
// судьи нет. Выходило ровно наоборот замыслу: ночь, которую больше всего стоило бы
|
||||
// судить (половина не вышла, работники гасли), оставалась без единого взгляда со
|
||||
// стороны, а взгляд получала только та, где и так всё сошлось. Ломалось ТИХО:
|
||||
// и сводка, и приёмщик вели себя как положено.
|
||||
//
|
||||
// 🔴 ФЛАГ `reviewer_starts` НЕ ТРОНУТ И ТРОГАТЬ ЕГО НЕЛЬЗЯ: он про сборщик экранов
|
||||
// продукта (кусок 7) и нужен затее сам по себе. Мы просто на него не опираемся.
|
||||
//
|
||||
// 🔴🔴 И ГЛАВНОЕ: СУДЬЕ ГОВОРИМ, ЧЕМ НОЧЬ КОНЧИЛАСЬ. Провальная ночь и удачная — два
|
||||
// разных дела: на одной главный вопрос «чем заплатили за успех», на другой «почему
|
||||
// не вышло». Приди судья на провал, не зная о нём, — он судил бы её как удачную,
|
||||
// и весь смысл прихода пропал бы. Итог считается ПО ФАЙЛАМ (`chemKonchilasNoch`),
|
||||
// тем же счётом, каким считает след отложенной задачи строкой выше.
|
||||
//
|
||||
// 🔴 ПАДЕНИЕ СУДЬИ НЕ ИМЕЕТ ПРАВА УБИТЬ НАДЗИРАТЕЛЯ. Сам судья не бросает по устройству,
|
||||
// но верить в это на слово нельзя: тот же класс беды («никогда не роняем» превратило
|
||||
// ошибку в тишину) уже стоил этому хозяйству смены. Поэтому здесь свой пояс — и он
|
||||
// НЕ МОЛЧАЛИВЫЙ: сбой уходит строкой владельцу и записью в ленту, ровно теми же словами,
|
||||
// какими о своей неудаче говорит сам судья.
|
||||
const itogNochi = chemKonchilasNoch(runDirPath, otlozhennyePunkty);
|
||||
try {
|
||||
// 🔴 ДОРОГУ К РОЖДЕНИЮ ПОДАЁМ МЫ, А НЕ СУДЬЯ. Живой запуск головы разрешён поимённо
|
||||
// ровно трём файлам (сторож `platnyy-zamer-tolko-cherez-schyot.test.mjs`), и надзиратель
|
||||
// среди них — «единственный зовущий рождения». Заведи судья свою дорогу к `startWorker` —
|
||||
// ворота счёта рождений (решение владельца 9) считали бы не все рождения за ночь, и
|
||||
// предел владельца молча перестал бы быть пределом. Поэтому зов остаётся здесь.
|
||||
await zavestiSudyuFn({ runDirPath, runId, cfg, itog: itogNochi, now: () => at, roditRebyonka: startWorker });
|
||||
} catch (err) {
|
||||
const prichina = String(err?.message ?? err);
|
||||
appendLine(join(runDirPath, SUMMARY_LINES_FILE), {
|
||||
at,
|
||||
worker_id: null,
|
||||
line: `Судью завести не удалось: ${prichina}. Ночь закрыта как обычно, но взгляда со `
|
||||
+ 'стороны на эту работу нет — принимать её придётся вам самим.',
|
||||
});
|
||||
logEvent(runDirPath, { kind: 'sudya_ne_zaveden', prichina }, at);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔴🔴 ЗАКРЫТИЕ ДЫРЫ 03.08.2026, ВТОРОЙ КРУГ, ВРЕЗАНО В ГОРЯЧЕЕ МЕСТО. `claimFiles`
|
||||
@@ -1300,7 +1403,12 @@ export async function supervise({
|
||||
// 🪤 Признак приходит ДОВОДОМ, а не вычитывается из текста причины. Разбирай мы причину
|
||||
// по словам вроде «закончил своё дело» — сторож со списком слов сам стал бы дверью:
|
||||
// первая же правка формулировки прошла бы насквозь молча.
|
||||
function finish({ final_status, reason, kak_konchil = null, ulika = false, delo_zayavleno = false, at = now() }) {
|
||||
// 🔴 `async` — ради ОДНОЙ строки в самом конце: закрытие прогона теперь может завести судью,
|
||||
// и дождаться его обязан ТОТ ЖЕ процесс. Не дождись — надзиратель вышел бы, а судья остался
|
||||
// бы сиротой: его поток некому читать, его приговор некому положить, а сам он продолжал бы
|
||||
// жечь запас владельца. Все зовущие `finish` — это `return finish(…)` внутри самого круга,
|
||||
// который и так асинхронный, поэтому наружу это не видно ни одной строкой.
|
||||
async function finish({ final_status, reason, kak_konchil = null, ulika = false, delo_zayavleno = false, at = now() }) {
|
||||
const prev = readJson(join(workerDirPath, 'worker.json')) ?? {};
|
||||
const baseBranch = prev.base_branch ?? cfg?.base_branch ?? null;
|
||||
// 🔴 Список изменённых файлов считается ОДИН раз и расходится по двум потребителям:
|
||||
@@ -1463,7 +1571,7 @@ export async function supervise({
|
||||
// и назавтра ночь началась бы с замков, которые держат мертвецы.
|
||||
releaseClaims(runDirPath, workerId);
|
||||
|
||||
closeRunIfLast(at);
|
||||
await closeRunIfLast(at);
|
||||
|
||||
// 🪤 Строка про стенограмму отдаётся и НАВЕРХ, не только в файл сводки: тот, кто зовёт
|
||||
// круг напрямую (проверки, а завтра и кусок 7), читает ответ, а не лезет в файл. Отдай
|
||||
|
||||
@@ -485,6 +485,12 @@ async function runSupervisor({
|
||||
// 🔴 Свой каталог следов — обязателен. Умолчание круга настоящее (`.night/deferred`),
|
||||
// и без подмены проверки оставляли бы в хранилище настоящие отказы по задачам.
|
||||
deferredPath = null,
|
||||
// 🔴🔴 СУДЬЯ НОЧИ — ПОДСТАВКА, И ЗДЕСЬ ЭТО ЕДИНСТВЕННО ВЕРНО. Умолчание самого круга
|
||||
// настоящее (живое рождение за деньги владельца), и без этой строки всякая проверка,
|
||||
// доходящая до конца задачи, пыталась бы завести ЖИВОГО судью. Второй заслон на тот же
|
||||
// случай стоит в самом приборе (`podPriboromProverok` в `sudya-nochi.mjs`) — но заслон
|
||||
// без подставки означал бы, что проверки каждый раз бьются в него, а не проверяют дело.
|
||||
zavestiSudyuFn = async () => ({ zaveden: false, put: null, prichina: 'подставка проверок' }),
|
||||
} = {}) {
|
||||
putRunStartedAt(runStartedAt);
|
||||
const clock = { t: startAt };
|
||||
@@ -520,6 +526,7 @@ async function runSupervisor({
|
||||
zarabotano,
|
||||
dbName,
|
||||
deferredPath: deferredPath ?? join(dir, 'deferred'),
|
||||
zavestiSudyuFn,
|
||||
});
|
||||
return { out, child, clock };
|
||||
}
|
||||
@@ -3729,3 +3736,150 @@ describe('🔴🔴 датчик пустого расхода в ЖИВОМ кр
|
||||
expect(readLines(join(rdir, 'events.jsonl')).filter(e => e.kind === 'pustoy_rashod')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 🔴🔴 СУДЬЯ НОЧИ ЗОВЁТСЯ ИЗ КРУГА (решение владельца 12, решения 134/135)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
//
|
||||
// 🔴 Довод `zavestiSudyuFn` принят В ТОМ ЖЕ КУСКЕ, что и зов, — это правило хозяйства
|
||||
// самого `supervisor.mjs`, и куплено оно болью: шесть доводов «на вырост» когда-то приняли
|
||||
// и не звали, вышла «кнопка стоп, которая никого не слышит». Проверки ниже краснеют без
|
||||
// зова — вырежи строку в `closeRunIfLast`, и первые две падают.
|
||||
//
|
||||
// 🔴🔴 ПОЧИНКА 06.08.2026, ОШИБКА НАДЗИРАТЕЛЯ, А НЕ СТРОИТЕЛЯ. Зов судьи стоял на флаге
|
||||
// `reviewer_starts`, а тот флаг блок 5а поднимает ТОЛЬКО когда все пункты засчитаны.
|
||||
// Выходило ровно наоборот замыслу: ночь, которую больше всего стоило судить (половина
|
||||
// не вышла, работники гасли), оставалась без единого взгляда со стороны. Решение
|
||||
// владельца 12 говорит дословно: судья заводится «не реже раза за ночь». Теперь зов
|
||||
// от удачи ночи НЕ зависит, а вместо флага судье передаётся ИТОГ — чем ночь кончилась.
|
||||
describe('🔴🔴 судья ночи: зов, единственность и то, что его смерть не роняет ночь', () => {
|
||||
it('погас ПОСЛЕДНИЙ работник и задача доделана — судья позван РОВНО ОДИН раз', async () => {
|
||||
zayavit('1');
|
||||
planPunktov4([{ id: '1', title: 'Первый', tests: ['a.test.mjs'], files: ['app/A.php'], long: false }]);
|
||||
const zovy = [];
|
||||
await runSupervisor({
|
||||
tickMs: 1000, script: [() => {}], head: () => 'sha-1', runTests: ZELENO,
|
||||
zavestiSudyuFn: async dovody => { zovy.push(dovody); return { zaveden: true, put: 'п', prichina: null }; },
|
||||
});
|
||||
expect(readJson(join(rdir, 'run.json')).reviewer_starts).toBe(true);
|
||||
expect(zovy.length, 'судью позвали не один раз — это лишние деньги владельца').toBe(1);
|
||||
// Судье дают ровно то, без чего он слеп: где улики и какой это прогон.
|
||||
expect(zovy[0].runDirPath).toBe(rdir);
|
||||
expect(zovy[0].runId).toBe('r-1');
|
||||
// И чем ночь кончилась: на удачной ночи главный вопрос один, на провальной другой.
|
||||
expect(zovy[0].itog, 'судье не сказали, чем кончилась ночь').toBeTruthy();
|
||||
expect(zovy[0].itog.dodelana).toBe(true);
|
||||
expect(zovy[0].itog.zakryto).toEqual(['1']);
|
||||
expect(zovy[0].itog.ne_zakryto).toEqual([]);
|
||||
});
|
||||
|
||||
it('🔴🔴 ПУНКТ ОТЛОЖЕН, ночь НЕ доделана — судья ВСЁ РАВНО приходит, и знает, что не вышло', async () => {
|
||||
// 🔴🔴 СЕРДЦЕ ПОЧИНКИ. Раньше зов стоял на флаге приёмщика, а его на отложенном пункте
|
||||
// не поднимают — и ночь, которую больше всего стоило судить, оставалась без судьи вовсе.
|
||||
// Ломалось ТИХО: и сводка, и приёмщик вели себя как положено, просто взгляда со стороны
|
||||
// не было ни одного. Решение владельца 12: «не реже раза за ночь».
|
||||
// 🪤 Рецепт провальной ночи взят у соседней проверки («пункт отложен, а свободных больше
|
||||
// нет»): пункт 1 краснеет дважды и откладывается, пункт 2 зелёный и засчитан.
|
||||
sozdatKatalog(inbox, { recursive: true });
|
||||
zapisatFayl(join(inbox, 'closed-items.jsonl'),
|
||||
JSON.stringify({ item: '1', at: T0 }) + '\n' + JSON.stringify({ item: '2', at: T0 }) + '\n');
|
||||
planPunktov4([
|
||||
{ id: '1', title: 'Первый', tests: ['a.test.mjs'], files: ['app/A.php'], long: false },
|
||||
{ id: '2', title: 'Второй', tests: ['b.test.mjs'], files: ['app/B.php'], long: false },
|
||||
]);
|
||||
let kommit = 'sha-1';
|
||||
let shag = 0;
|
||||
const zovy = [];
|
||||
await runSupervisor({
|
||||
tickMs: 1000, script: [() => {}, () => {}, () => {}],
|
||||
head: () => kommit,
|
||||
runTests: d => { shag += 1; kommit = `sha-${shag + 1}`; return d.item.id === '1' ? KRASNO() : ZELENO(); },
|
||||
zavestiSudyuFn: async dovody => { zovy.push(dovody); return { zaveden: true, put: 'п', prichina: null }; },
|
||||
});
|
||||
// Флага приёмщика нет — и это правильно, он про другое. Судья от него не зависит.
|
||||
expect(readJson(join(rdir, 'run.json')).reviewer_starts).toBe(false);
|
||||
expect(zovy.length, 'ночь, которую больше всего стоило судить, осталась без судьи').toBe(1);
|
||||
// 🔴 И судье сказано, ЧЕМ она кончилась: без этого он судил бы провал как удачу.
|
||||
expect(zovy[0].itog.dodelana, 'судье выдали недоделанную ночь за доделанную').toBe(false);
|
||||
expect(zovy[0].itog.zakryto).toEqual(['2']);
|
||||
expect(zovy[0].itog.ne_zakryto).toEqual(['1']);
|
||||
expect(zovy[0].itog.otlozheno).toEqual(['1']);
|
||||
expect(zovy[0].itog.vsego).toBe(2);
|
||||
});
|
||||
|
||||
it('🔴 итог прогона лёг ДО судьи — упади судья, утренняя бумага уже на месте', async () => {
|
||||
zayavit('1');
|
||||
planPunktov4([{ id: '1', title: 'Первый', tests: ['a.test.mjs'], files: ['app/A.php'], long: false }]);
|
||||
let itogBylNaMeste = null;
|
||||
await runSupervisor({
|
||||
tickMs: 1000, script: [() => {}], head: () => 'sha-1', runTests: ZELENO,
|
||||
zavestiSudyuFn: async () => {
|
||||
itogBylNaMeste = readJson(lastRunFile) !== null;
|
||||
return { zaveden: true, put: 'п', prichina: null };
|
||||
},
|
||||
});
|
||||
expect(itogBylNaMeste, 'судью позвали ДО того, как ночь закрыли, — так его падение уносило бы бумагу').toBe(true);
|
||||
});
|
||||
|
||||
it('🔴 флага приёмщика нет и до пунктов дело не дошло — судья ВСЁ РАВНО зван', async () => {
|
||||
// 🔴 Признак `reviewer_starts` — про сборщик экранов продукта, а не про судью. Зов судьи
|
||||
// на него больше не опирается: дошло дело до закрытия прогона — судья заводится.
|
||||
const zovy = [];
|
||||
await runSupervisor({
|
||||
script: [c => { c.say({ type: 'result', is_error: false, result: 'сделал' }); c.die(0); }],
|
||||
zavestiSudyuFn: async d => { zovy.push(d); return { zaveden: true }; },
|
||||
});
|
||||
expect(readJson(lastRunFile), 'прогон закрыт — значит до места зова дошли').not.toBeNull();
|
||||
// Флаг не тронут и остался чужим делом.
|
||||
expect(readJson(join(rdir, 'run.json')).reviewer_starts ?? false).toBe(false);
|
||||
expect(zovy.length, 'судья не пришёл на ночь без флага приёмщика').toBe(1);
|
||||
expect(zovy[0].itog.dodelana).toBe(false);
|
||||
expect(zovy[0].itog.ne_zakryto).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('погас НЕ последний — судью не зовём, ночь ещё идёт', async () => {
|
||||
writeJsonAtomic(join(rdir, 'run.json'), {
|
||||
...readJson(join(rdir, 'run.json')), workers: ['w-1', 'w-2'], reviewer_starts: true,
|
||||
});
|
||||
mkdirSync(join(rdir, 'workers', 'w-2'), { recursive: true });
|
||||
writeJsonAtomic(join(rdir, 'workers', 'w-2', 'worker.json'), { worker_id: 'w-2', status: 'working' });
|
||||
let zvali = 0;
|
||||
await runSupervisor({
|
||||
script: [c => { c.say({ type: 'result', is_error: false, result: 'сделал' }); c.die(0); }],
|
||||
zavestiSudyuFn: async () => { zvali += 1; return { zaveden: true }; },
|
||||
});
|
||||
expect(readJson(lastRunFile)).toBeNull();
|
||||
expect(zvali).toBe(0);
|
||||
});
|
||||
|
||||
it('🔴🔴 судья УПАЛ — ночь всё равно закрыта, а его смерть НЕ проглочена молча', async () => {
|
||||
zayavit('1');
|
||||
planPunktov4([{ id: '1', title: 'Первый', tests: ['a.test.mjs'], files: ['app/A.php'], long: false }]);
|
||||
const { out } = await runSupervisor({
|
||||
tickMs: 1000, script: [() => {}], head: () => 'sha-1', runTests: ZELENO,
|
||||
zavestiSudyuFn: async () => { throw new Error('судья взорвался на ровном месте'); },
|
||||
});
|
||||
// Ночь закрыта как обычно — это главное.
|
||||
expect(out.status).toBe('extinguished');
|
||||
expect(readJson(lastRunFile), 'падение судьи унесло итог прогона — так нельзя').not.toBeNull();
|
||||
expect(readJson(join(rdir, 'run.json')).status).toBe('finished');
|
||||
// И его смерть слышно: строкой владельцу и записью в ленту.
|
||||
const stroki = readLines(join(rdir, SUMMARY_LINES_FILE)).map(r => r.line).join('\n');
|
||||
expect(stroki, 'смерть судьи проглочена молча').toMatch(/судью завести не удалось/i);
|
||||
expect(stroki).toContain('судья взорвался на ровном месте');
|
||||
expect(readLines(join(rdir, 'events.jsonl')).map(e => e.kind)).toContain('sudya_ne_zaveden');
|
||||
});
|
||||
|
||||
it('🔴 судья не считается работником: состав прогона и счёт закрытых пунктов не меняются', async () => {
|
||||
zayavit('1');
|
||||
planPunktov4([{ id: '1', title: 'Первый', tests: ['a.test.mjs'], files: ['app/A.php'], long: false }]);
|
||||
await runSupervisor({
|
||||
tickMs: 1000, script: [() => {}], head: () => 'sha-1', runTests: ZELENO,
|
||||
zavestiSudyuFn: async () => ({ zaveden: true, put: 'п', prichina: null }),
|
||||
});
|
||||
const last = readJson(lastRunFile);
|
||||
expect(last.workers.map(w => w.worker_id)).toEqual(['w-1']);
|
||||
expect(readJson(join(rdir, 'run.json')).workers).toEqual(['w-1']);
|
||||
expect(closedItemsOfRun(rdir)).toEqual(['1']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user