ночь: мёртвого больше не зовут живым, путь к стенограмме доезжает до утреннего письма, живые состояния названы по-русски
This commit is contained in:
@@ -20,6 +20,14 @@ import { vidPredela } from './budget.mjs';
|
||||
// `resumePlan`. Своей копии здесь нет намеренно: два списка слов на один вопрос разъехались
|
||||
// бы МОЛЧА, и обе стороны остались бы зелёными. Сторож на возврат копии — в проверках.
|
||||
import { PRODOLZHAEMYE } from './resume.mjs';
|
||||
// 🔴 Русские слова состояний берутся ОТТУДА ЖЕ, откуда их берёт утренняя сводка
|
||||
// (`summary.mjs`, задача 9), — своего словаря здесь нет. Английское слово в объяснении
|
||||
// владельцу равно его отсутствию: он не поймёт, погас работник или сломался.
|
||||
// 🪤 Ввозится именно сводка, а не надзиратель: у надзирателя тот же словарь, но ввоз
|
||||
// из него замкнул бы кольцо (`cli-parts` → `supervisor` → `spawn` → `schyot-zamerov`
|
||||
// → `cli-parts`), а кольца ввозов ломаются молча и не там, где заведены. `summary.mjs`
|
||||
// не ввозит ничего вовсе — из него ввозить безопасно.
|
||||
import { statusRu } from './summary.mjs';
|
||||
|
||||
const OBRAZTSY = {
|
||||
run: 'Надо так: node tools/night/cli.mjs run <план> --priyomka <приёмочный лист> --spec <бумага замысла> [--task <номер идущей задачи>] [--files a.php,b.php]',
|
||||
@@ -153,15 +161,34 @@ export function zamerDovody(cfg = {}) {
|
||||
// В затее уже записано, чем кончается общая база на двоих: «ЗЕЛЁНЫЕ ВРУЩИЕ проверки,
|
||||
// а это худший вид беды» (`workerEnv` в spawn.mjs).
|
||||
//
|
||||
// 🔴 СКОЛЬКО РАЗНЫХ СОСТОЯНИЙ У ЭТОГО ВХОДА НА САМОМ ДЕЛЕ — ЗАМЕРЕНО, А НЕ ПРИКИНУТО.
|
||||
// Слов состояния у работника ВОСЕМЬ (посчитано по рабочим файлам 03.08.2026):
|
||||
// продолжаемые — `stalled`, `extinguished`;
|
||||
// живые — `working`, `paused`, `waiting_service`, `waiting_answer_idle`,
|
||||
// `waiting_answer_busy`, `stopped`.
|
||||
// А состояний — ДЕВЯТЬ. Девятое: **слова нет вовсе** (запись оборвалась, карточка неполна,
|
||||
// 🔴 СКОЛЬКО РАЗНЫХ СОСТОЯНИЙ У ЭТОГО ВХОДА НА САМОМ ДЕЛЕ — ПЕРЕСЧИТАНО КОМАНДОЙ 03.08.2026,
|
||||
// А НЕ ГЛАЗАМИ. Прежняя запись здесь говорила «слов ВОСЕМЬ, состояний ДЕВЯТЬ» и была неверна
|
||||
// дважды: `born_dead` в ней не было вовсе (его завели в тот же день), а слово `stopped`
|
||||
// в ней было — хотя такого слова в затее нет ни в одном рабочем файле; настоящее — `stopping`.
|
||||
// Перечень снят зовом самих списков, а не переписыванием их сюда руками:
|
||||
// node -e "…import('./supervisor.mjs')… WORKER_STATUSES + FINAL_STATUSES…"
|
||||
// Слов состояния у работника ДЕВЯТЬ:
|
||||
// продолжаемые (`PRODOLZHAEMYE`, resume.mjs) — `stalled`, `extinguished`;
|
||||
// прочие итоговые (`FINAL_STATUSES`) — `paused`, `born_dead`;
|
||||
// объявляет о себе сам (`WORKER_STATUSES`) — `working`, `stopping`, `waiting_service`,
|
||||
// `waiting_answer_idle`, `waiting_answer_busy`.
|
||||
// А состояний — ДЕСЯТЬ. Десятое: **слова нет вовсе** (запись оборвалась, карточка неполна,
|
||||
// в поле не строка). Слить его с «живым» — соврать владельцу: он прочёл бы «работник жив,
|
||||
// пауза снимается командой `resume`» про работника, которого, может быть, и нет.
|
||||
// Поэтому у «слова нет» свой ответ и свои слова.
|
||||
//
|
||||
// 🪤 ЭТОТ ПЕРЕЧЕНЬ — ПРИМЕЧАНИЕ, А ПРИМЕЧАНИЯ УСТАРЕВАЮТ МОЛЧА: он и устарел, ровно так.
|
||||
// Поэтому ниже по нему НИЧЕГО не считается — счёт идёт по живым спискам, а сторож на само
|
||||
// это примечание стоит в проверках (`slova-vladeltsu.test.mjs`) и краснеет, когда число
|
||||
// слов в затее разойдётся с числом, написанным здесь.
|
||||
// 🔴 Какие слова считаются продолжаемыми — сказать ВЛАДЕЛЬЦУ, а не показать ему список
|
||||
// английских слов. Прежде здесь стояло `PRODOLZHAEMYE.join(' и ')`, то есть в письмо уходило
|
||||
// «(stalled и extinguished)» — ровно та же беда, что и `born_dead` в утренней таблице,
|
||||
// только вторым местом. Список остаётся ОДИН (`resume.mjs`), переводится он на месте печати.
|
||||
function prodolzhaemyeSlovami() {
|
||||
return PRODOLZHAEMYE.map(s => `«${statusRu(s)}»`).join(' и ');
|
||||
}
|
||||
|
||||
export function vyborProdolzhaemogo({ workers = [], workerId = null } = {}) {
|
||||
const spisok = Array.isArray(workers) ? workers.filter(Boolean) : [];
|
||||
if (!spisok.length) {
|
||||
@@ -205,16 +232,47 @@ export function vyborProdolzhaemogo({ workers = [], workerId = null } = {}) {
|
||||
+ 'в том же углу и той же базе оказались бы двое живых разом. Смотрите прогон ' + `${w.resumed_into}.`,
|
||||
};
|
||||
}
|
||||
// Жив — в том числе на паузе.
|
||||
// ━━━ 🔴🔴 НЕ ПРОДОЛЖАЕМ — И ПРИЧИНА НАЗЫВАЕТСЯ ТА, КОТОРАЯ ЕСТЬ НА САМОМ ДЕЛЕ ━━━
|
||||
//
|
||||
// ЧТО БЫЛО, замерено живьём и дословно 03.08.2026:
|
||||
// «Работник w-4 жив (born_dead) — продолжают только вставших и погашенных»
|
||||
// Работник МЁРТВ: он не запустился вовсе. Денежный замок при этом отрабатывал ВЕРНО —
|
||||
// `born_dead` в `PRODOLZHAEMYE` не входит, в платный подъём такого не берут. Врало
|
||||
// только объяснение, которое читает человек.
|
||||
//
|
||||
// 🔴 ПОЧЕМУ ЭТО ХУЖЕ ОБЫЧНОЙ ОШИБКИ. Замок держит, а владелец читает обратное — и
|
||||
// решение принимает по прочитанному. Прочтя «жив», он пошёл бы искать работающего
|
||||
// работника, которого нет ни одного, вместо того чтобы разбираться, почему тот
|
||||
// не запустился, — а причина (не та программа, не то окружение, нет ключа) сама
|
||||
// не проходит и повторится в следующую же ночь. Правильный замок с враньём в объяснении
|
||||
// стоит дороже честного отказа: беду он не пускает, но и чинить её никто не идёт.
|
||||
//
|
||||
// 🔴 «ЖИВ» ТЕПЕРЬ НЕ ДОГАДКА ПО СПИСКУ СЛОВ, А ОТВЕТ ЕГО СОБСТВЕННОЙ КАРТОЧКИ.
|
||||
// Кончившемуся надзиратель пишет `finished_at` и `finish_reason` (`finish` в
|
||||
// supervisor.mjs, одно место на все выходы), живому — не пишет никогда. Второго списка
|
||||
// «живых слов» здесь нет намеренно: он разъехался бы с `FINAL_STATUSES` молча, а ввезти
|
||||
// настоящий отсюда нельзя — вышло бы кольцо ввозов через надзирателя.
|
||||
if (!PRODOLZHAEMYE.includes(w.status)) {
|
||||
const konchilsya = Number.isFinite(w.finished_at) || typeof w.finish_reason === 'string';
|
||||
const proPauzu = w.status === 'paused'
|
||||
? ' Он на паузе, а пауза снимается командой `resume`, а не кнопкой «продолжить».'
|
||||
: '';
|
||||
// 🔴 Слова надзирателя ДОНОСЯТСЯ, а не сочиняются здесь заново: он уже написал
|
||||
// полную русскую причину, и второе правило того же смысла разошлось бы с первым.
|
||||
const zapisal = konchilsya && typeof w.finish_reason === 'string' && w.finish_reason
|
||||
? ` Вот что записал о нём надзиратель: ${w.finish_reason}`
|
||||
: '';
|
||||
// 🪤 Во второй половине сказано «ПО СВОЕЙ КАРТОЧКЕ», а не просто «жив». Карточка —
|
||||
// единственное, что у нас тут есть; работник мог умереть секунду назад, а надзиратель
|
||||
// ещё не успел дописать. Голое «жив» было бы обещанием, которого не на чем держать, —
|
||||
// ровно тем же приёмом, каким сегодня «жив» говорили про мертворождённого.
|
||||
const golova = konchilsya
|
||||
? `Работник ${workerId} кончился и продолжению не подлежит — состояние «${statusRu(w.status)}».`
|
||||
: `Работник ${workerId} по своей карточке ещё жив и работает — состояние «${statusRu(w.status)}».`;
|
||||
return {
|
||||
worker: null,
|
||||
candidates: kandidaty.map(x => x.worker_id),
|
||||
reason: `Работник ${workerId} жив (${w.status}) — продолжают только вставших и погашенных `
|
||||
+ `(${PRODOLZHAEMYE.join(' и ')}).${proPauzu}`,
|
||||
reason: `${golova} Продолжают только вставших и погашенных (${prodolzhaemyeSlovami()}).${proPauzu}${zapisal}`,
|
||||
};
|
||||
}
|
||||
return { worker: w, candidates: kandidaty.map(x => x.worker_id), reason: null };
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// ━━━ СТОРОЖА ОДНОГО КЛАССА: «СКАЗАНО ВЛАДЕЛЬЦУ» ПРОТИВ «СДЕЛАНО НА САМОМ ДЕЛЕ» ━━━
|
||||
//
|
||||
// Все три беды 03.08.2026 — одна и та же беда: работа сделана, а до владельца не дошла
|
||||
// или дошла искажённой.
|
||||
// 1. `born_dead` печатался в утренней таблице английским словом;
|
||||
// 2. `cli-parts.mjs` звал мертворождённого ЖИВЫМ — замок держал верно, врало объяснение;
|
||||
// 3. приговор прибора поиска стенограммы не шёл никуда вовсе.
|
||||
//
|
||||
// 🔴 ПОЧЕМУ ОТДЕЛЬНЫМ ФАЙЛОМ. Он стережёт не какой-то один кусок затеи, а СТЫК между тем,
|
||||
// что затея решила, и тем, что она про это сказала человеку. Стык этот ничей — и оттого
|
||||
// рвётся: каждая из трёх правок сегодня остановилась ровно на пороге чужого файла.
|
||||
//
|
||||
// 🔴 КАЖДЫЙ СТОРОЖ ЗДЕСЬ БЫЛ ПРИНЯТ ВЫРЕЗАНИЕМ: защиту ломали и смотрели, что он краснеет.
|
||||
// Зелёный сторож, которого ни разу не видели красным, не доказывает ничего.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
import { STATUS_WORD, statusRu, buildSummary } from './summary.mjs';
|
||||
import { FINAL_STATUSES, WORKER_STATUSES } from './supervisor.mjs';
|
||||
import { PRODOLZHAEMYE } from './resume.mjs';
|
||||
import { vyborProdolzhaemogo } from './cli-parts.mjs';
|
||||
|
||||
const KATALOG = dirname(fileURLToPath(import.meta.url));
|
||||
const ishodnik = imya => readFileSync(join(KATALOG, imya), 'utf8');
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('слова состояний: ни одно не доходит до владельца по-английски', () => {
|
||||
// Сторож КЛАССА, а не одного забытого слова: список берётся у самой затеи.
|
||||
it('каждое слово, каким затея зовёт работника, переводится на русский', () => {
|
||||
const vse = [...new Set([...FINAL_STATUSES, ...WORKER_STATUSES, ...PRODOLZHAEMYE])];
|
||||
// 🪤 Сам перечень тоже сторожим: опустей он по недосмотру — цикл ниже прошёл бы
|
||||
// вхолостую и остался зелёным, ничего не проверив. Пустой обход — любимый вид
|
||||
// мнимого сторожа.
|
||||
expect(vse.length).toBeGreaterThanOrEqual(9);
|
||||
for (const status of vse) {
|
||||
const slovo = statusRu(status);
|
||||
expect(slovo, `состояние «${status}» завели, а русского слова к нему не дали`).toBeTruthy();
|
||||
expect(slovo, `слово состояния «${status}» осталось латиницей`).not.toMatch(/[A-Za-z]/);
|
||||
}
|
||||
});
|
||||
|
||||
it('словарь в затее ОДИН: у надзирателя своей копии слов больше нет', async () => {
|
||||
// 🔴 Копия слова у надзирателя побеждала общую при слиянии. Разойдись копии на знак —
|
||||
// владелец читал бы про одного работника два разных слова, а обе стороны были бы зелёными.
|
||||
const sup = await import('./supervisor.mjs');
|
||||
expect(Object.keys(sup.SLOVA_NOVYH_SOSTOYANIY)).toEqual([]);
|
||||
for (const [slovo, russkoe] of Object.entries(STATUS_WORD)) {
|
||||
expect(sup.SLOVA_SOSTOYANIY[slovo]).toBe(russkoe);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('отказ кнопки «продолжить»: слово человеку не расходится с решением замка', () => {
|
||||
const kartochka = (o = {}) => ({ worker_id: 'w-1', status: 'stalled', ...o });
|
||||
|
||||
it('🔴 КЛАСС: ни одно НЕпродолжаемое итоговое состояние не зовётся живым', () => {
|
||||
// Замерено живьём: «Работник w-4 жив (born_dead) — продолжают только вставших…».
|
||||
// Работник был мёртв. Сторож обходит ВСЕ итоговые слова, а не одно сегодняшнее:
|
||||
// заведут завтра десятое — он покраснеет сам.
|
||||
const mertvye = FINAL_STATUSES.filter(s => !PRODOLZHAEMYE.includes(s));
|
||||
expect(mertvye.length).toBeGreaterThan(0);
|
||||
for (const status of mertvye) {
|
||||
const r = vyborProdolzhaemogo({
|
||||
workers: [kartochka({ status, finished_at: 1, finish_reason: 'Запустить не удалось.' })],
|
||||
workerId: 'w-1',
|
||||
});
|
||||
expect(r.worker).toBeNull();
|
||||
expect(r.reason, `состояние «${status}» подано владельцу как живое`).not.toMatch(/жив/i);
|
||||
expect(r.reason).toMatch(/кончился/i);
|
||||
}
|
||||
});
|
||||
|
||||
it('в отказе стоит русское слово состояния, а не английское', () => {
|
||||
const r = vyborProdolzhaemogo({
|
||||
workers: [kartochka({ status: 'born_dead', finished_at: 1, finish_reason: 'Запустить не удалось.' })],
|
||||
workerId: 'w-1',
|
||||
});
|
||||
expect(r.reason).toContain(STATUS_WORD.born_dead);
|
||||
expect(r.reason).not.toMatch(/born_dead/);
|
||||
});
|
||||
|
||||
it('🔴 список продолжаемых называется владельцу по-русски, а не слитыми английскими словами', () => {
|
||||
// Прежде здесь печаталось «(stalled и extinguished)» — то же самое английское слово
|
||||
// в лицо владельцу, только вторым местом.
|
||||
const r = vyborProdolzhaemogo({
|
||||
workers: [kartochka({ status: 'born_dead', finished_at: 1, finish_reason: 'Запустить не удалось.' })],
|
||||
workerId: 'w-1',
|
||||
});
|
||||
for (const s of PRODOLZHAEMYE) {
|
||||
expect(r.reason, `слово «${s}» ушло владельцу английским`).not.toContain(s);
|
||||
expect(r.reason).toContain(STATUS_WORD[s]);
|
||||
}
|
||||
});
|
||||
|
||||
it('слова надзирателя доносятся, а не сочиняются заново', () => {
|
||||
const r = vyborProdolzhaemogo({
|
||||
workers: [kartochka({ status: 'born_dead', finished_at: 1, finish_reason: 'Работника не удалось запустить: нет ключа.' })],
|
||||
workerId: 'w-1',
|
||||
});
|
||||
expect(r.reason).toContain('нет ключа');
|
||||
});
|
||||
|
||||
it('работник по карточке ещё не кончился — он и назван живым, но со ссылкой на карточку', () => {
|
||||
// 🪤 «Жив» — не обещание, а пересказ карточки: он мог умереть секунду назад.
|
||||
const r = vyborProdolzhaemogo({ workers: [kartochka({ status: 'working' })], workerId: 'w-1' });
|
||||
expect(r.reason).toMatch(/жив/i);
|
||||
expect(r.reason).toMatch(/карточке/i);
|
||||
});
|
||||
|
||||
it('🪤 примечание о числе состояний сверено с самой затеей, а не с памятью писавшего', () => {
|
||||
// Примечание в `cli-parts.mjs` УЖЕ устаревало молча: говорило «слов восемь» и называло
|
||||
// слово `stopped`, которого в затее нет вовсе. Теперь число в примечании сторожится.
|
||||
const vse = [...new Set([...FINAL_STATUSES, ...WORKER_STATUSES])];
|
||||
expect(vse.length).toBe(9); // измерено командой 03.08.2026
|
||||
const src = ishodnik('cli-parts.mjs');
|
||||
expect(src).toMatch(/Слов состояния у работника ДЕВЯТЬ/);
|
||||
expect(src).toMatch(/состояний — ДЕСЯТЬ/);
|
||||
// 🔴 И каждое слово, перечисленное в самом примечании, обязано быть НАСТОЯЩИМ. Прежнее
|
||||
// примечание называло `stopped`, которого в затее нет ни в одном файле, — выдуманное
|
||||
// слово в примечании страшнее устаревшего числа: по нему свежая смена ищет и не находит.
|
||||
const perechen = src.slice(src.indexOf('Слов состояния у работника ДЕВЯТЬ'), src.indexOf('А состояний — ДЕСЯТЬ'));
|
||||
const nazvannye = [...perechen.matchAll(/`([a-z_]+)`/g)].map(m => m[1]);
|
||||
expect(nazvannye.length).toBeGreaterThanOrEqual(9);
|
||||
for (const slovo of nazvannye) {
|
||||
if (slovo === 'PRODOLZHAEMYE') continue;
|
||||
expect(vse, `в примечании названо слово «${slovo}», которого в затее нет`).toContain(slovo);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('стенограмма работника доезжает до владельца, а не кончается в приборе', () => {
|
||||
// Живое хозяйство прогона: каталог прогона + каталог работника.
|
||||
function hozyaystvo() {
|
||||
const koren = mkdtempSync(join(tmpdir(), 'noch-stenogramma-'));
|
||||
const runDirPath = join(koren, 'run');
|
||||
const workerDirPath = join(runDirPath, 'workers', 'w-1');
|
||||
mkdirSync(workerDirPath, { recursive: true });
|
||||
writeFileSync(join(runDirPath, 'run.json'), JSON.stringify({ started_at: 1, workers: ['w-1'] }), 'utf8');
|
||||
return { koren, runDirPath, workerDirPath };
|
||||
}
|
||||
|
||||
// Круг зовём НЕ целиком — целиком он живёт часами и ходит в git. Зовём `supervise`
|
||||
// с подставками на всё живое, и работник у нас кончается сразу: `spawnWorker` бросает,
|
||||
// то есть попадаем ровно в тот выход, ради которого всё и затевалось (`born_dead`).
|
||||
async function progonPogibshego(naytiStenogrammuFn) {
|
||||
const { runDirPath, workerDirPath } = hozyaystvo();
|
||||
const { supervise } = await import('./supervisor.mjs');
|
||||
const itog = await supervise({
|
||||
runId: 'r-1',
|
||||
workerId: 'w-1',
|
||||
worktreeRoot: workerDirPath,
|
||||
workerDirPath,
|
||||
runDirPath,
|
||||
cfg: { base_branch: 'main', supervisor_tick_seconds: 0 },
|
||||
measuresPath: join(runDirPath, 'measures.jsonl'),
|
||||
commandsDirPath: join(runDirPath, 'commands'),
|
||||
lastRunFile: join(runDirPath, 'last-run.json'),
|
||||
deferredPath: join(runDirPath, 'deferred'),
|
||||
spawnWorker: () => { throw new Error('программа не найдена'); },
|
||||
now: () => 1000,
|
||||
sleep: async () => {},
|
||||
probeDisk: async () => ({ ok: true }),
|
||||
head: () => 'sha-0',
|
||||
changedFiles: () => [],
|
||||
rollbackFn: () => ({ ok: true }),
|
||||
runTests: () => ({ ok: true }),
|
||||
dirty: () => false,
|
||||
naytiStenogrammu: naytiStenogrammuFn,
|
||||
});
|
||||
return { itog, runDirPath, workerDirPath };
|
||||
}
|
||||
|
||||
const nakhodka = {
|
||||
naydeno: 1,
|
||||
put: 'C:/дом/.claude/projects/ugol/abc.jsonl',
|
||||
kandidaty: [],
|
||||
katalog: 'C:/дом/.claude/projects/ugol',
|
||||
katalogi_probovali: [],
|
||||
sposob: 'единственная в каталоге работника',
|
||||
stroka: 'Стенограмма работника: C:/дом/.claude/projects/ugol/abc.jsonl',
|
||||
};
|
||||
|
||||
function strokiSvodki(runDirPath) {
|
||||
const put = join(runDirPath, 'summary-lines.jsonl');
|
||||
if (!existsSync(put)) return [];
|
||||
return readFileSync(put, 'utf8').split('\n').filter(Boolean).map(s => JSON.parse(s).line);
|
||||
}
|
||||
|
||||
it('🔴 путь к стенограмме попадает в СТРОКИ СВОДКИ — то, что владелец читает утром', async () => {
|
||||
const { runDirPath } = await progonPogibshego(() => nakhodka);
|
||||
const stroki = strokiSvodki(runDirPath);
|
||||
expect(stroki.some(l => l.includes(nakhodka.put)), 'путь к улике до сводки не дошёл').toBe(true);
|
||||
});
|
||||
|
||||
it('🔴 и до самой утренней сводки: строка видна в письме владельцу', async () => {
|
||||
// Сквозной сторож: строки сводки → `buildSummary` → письмо. Он краснеет, даже если
|
||||
// строку положить в файл, который письмо не читает.
|
||||
const { runDirPath } = await progonPogibshego(() => nakhodka);
|
||||
const pismo = buildSummary({ run_id: 'r-1', lines: strokiSvodki(runDirPath), disk_free_gb: 10 });
|
||||
expect(pismo.text).toContain(nakhodka.put);
|
||||
});
|
||||
|
||||
it('прибор зовут ГОТОВЫЙ, по готовой карточке — своего второго поиска нет', async () => {
|
||||
let dali = null;
|
||||
await progonPogibshego(put => { dali = put; return nakhodka; });
|
||||
expect(dali, 'прибор поиска стенограммы не позвали вовсе').toBeTruthy();
|
||||
expect(String(dali)).toMatch(/worker\.json$/);
|
||||
// 🪤 И в исходнике надзирателя нет своей копии правила поиска: имя каталога стенограмм
|
||||
// считает один файл на всю затею.
|
||||
const src = ishodnik('supervisor.mjs');
|
||||
expect(src).toMatch(/from '\.\/stenogramma\.mjs'/);
|
||||
expect(src, 'у надзирателя завелась своя копия правила имени каталога').not.toMatch(/\.claude', 'projects'/);
|
||||
});
|
||||
|
||||
it('улика остаётся и в exit.json, а не только в письме', async () => {
|
||||
const { workerDirPath } = await progonPogibshego(() => nakhodka);
|
||||
const exit = JSON.parse(readFileSync(join(workerDirPath, 'exit.json'), 'utf8'));
|
||||
expect(exit.stenogramma?.put).toBe(nakhodka.put);
|
||||
});
|
||||
|
||||
it('🔴 стенограммы НЕ нашлось — владельцу говорят и это, а не молчат', async () => {
|
||||
// Молчание тут неотличимо от «печать опять отвалилась» — ровно та беда, которую чиним.
|
||||
const pusto = { ...nakhodka, naydeno: 0, put: null, stroka: 'Стенограммы работника нет: каталога под неё не заведено вовсе.' };
|
||||
const { runDirPath } = await progonPogibshego(() => pusto);
|
||||
expect(strokiSvodki(runDirPath).some(l => l.includes('Стенограммы работника нет'))).toBe(true);
|
||||
});
|
||||
|
||||
it('🔴 ПАДЕНИЕ ПРИБОРА НЕ УБИВАЕТ НАДЗИРАТЕЛЯ — работник закрыт, а сбой назван вслух', async () => {
|
||||
// Надзиратель в этот миг закрывает работника. Умри он здесь — не будет ни карточки итога,
|
||||
// ни строки сводки, ни освобождения замков, и ночь кончится молча.
|
||||
const { itog, runDirPath, workerDirPath } = await progonPogibshego(() => { throw new Error('диск отвалился'); });
|
||||
expect(itog.status).toBe('born_dead');
|
||||
expect(JSON.parse(readFileSync(join(workerDirPath, 'worker.json'), 'utf8')).status).toBe('born_dead');
|
||||
const stroki = strokiSvodki(runDirPath);
|
||||
expect(stroki.some(l => /поиск упал|диск отвалился/.test(l))).toBe(true);
|
||||
});
|
||||
|
||||
it('🪤 у честно погашенного стенограмму не ищут — сводку не забивают исправностью', async () => {
|
||||
// У него `kak_konchil` нет: он кончился по названной причине, разбирать нечего.
|
||||
// Строка про стенограмму на каждого из семерых утопила бы единственную важную.
|
||||
let zvali = 0;
|
||||
const { runDirPath } = await progonPogibshego(() => { zvali += 1; return nakhodka; });
|
||||
expect(zvali).toBe(1); // погибший — искали
|
||||
const stroki = strokiSvodki(runDirPath);
|
||||
expect(stroki.filter(l => l.includes('Стенограмма работника:')).length).toBe(1);
|
||||
});
|
||||
});
|
||||
+23
-1
@@ -19,10 +19,32 @@ export const STATUS_WORD = {
|
||||
// был чужим (`SLOVA_NOVYH_SOSTOYANIY`): два разных слова про одного работника — та же
|
||||
// ложь, только с двух сторон.
|
||||
born_dead: 'не начал работу вовсе',
|
||||
|
||||
// 🔴 НАЙДЕНО СОБСТВЕННЫМ СТОРОЖЕМ 03.08.2026, СВЕРХ ТРЁХ НАЗВАННЫХ БЕД. Здесь были только
|
||||
// ИТОГОВЫЕ состояния — те, что ставит надзиратель. А есть ещё пять, которые работник
|
||||
// объявляет о себе САМ (`WORKER_STATUSES` в supervisor.mjs), и до владельца они доходят
|
||||
// тоже: отказ кнопки «продолжить» называет ему состояние живого работника. То есть
|
||||
// английских слов в лицо владельцу было не одно, а шесть — просто про пять из них никто
|
||||
// не спрашивал. Сторож обходит теперь ОБА списка, а не один.
|
||||
// 🪤 Слова описательные, а не однословные: «ждёт ответа» без «и стоит без дела» / «и
|
||||
// работу не бросил» — это два РАЗНЫХ состояния, и по ним по-разному идут деньги
|
||||
// (`billableMs` в limits.mjs). Слей их в одно слово — владелец не отличил бы работника,
|
||||
// который жжёт деньги в ожидании, от работника, который просто стоит.
|
||||
working: 'работает',
|
||||
stopping: 'доделывает начатый шаг и останавливается',
|
||||
waiting_answer_idle: 'ждёт ответа владельца и стоит без дела',
|
||||
waiting_answer_busy: 'ждёт ответа владельца, но работу не бросил',
|
||||
waiting_service: 'ждёт чужую службу',
|
||||
};
|
||||
|
||||
// Незнакомое слово печатается как есть: молча съесть его — соврать владельцу.
|
||||
function statusRu(status) {
|
||||
// 🔴 Вывезено наружу 03.08.2026: слова состояний владельцу печатает не только сводка
|
||||
// (`cli-parts.mjs` объясняет ими отказ кнопки «продолжить»). Пусть зовут ЭТО правило,
|
||||
// а не переписывают у себя `STATUS_WORD[x] ?? x`: третья копия одного правила разошлась бы
|
||||
// с первыми двумя молча — ровно тем классом, каким сегодня разъехалась подпись ночи.
|
||||
// 🪤 Этот файл НИЧЕГО не ввозит, и так и должно остаться: его ввозят все, а кольцо ввозов
|
||||
// (`cli-parts` → `supervisor` → `spawn` → `schyot-zamerov` → `cli-parts`) ломается молча.
|
||||
export function statusRu(status) {
|
||||
if (!status) return 'работал';
|
||||
return STATUS_WORD[status] ?? status;
|
||||
}
|
||||
|
||||
+89
-12
@@ -66,6 +66,12 @@ import { markDeferred, deferredDir } from './deferred.mjs';
|
||||
// 🔴 Команда проверок считается ЗДЕСЬ, на каждом судимом пункте. `plan-model.mjs` не ввозит
|
||||
// из затеи ничего, поэтому кольца ввозов не будет.
|
||||
import { komandaProverokPunkta } from './plan-model.mjs';
|
||||
// 🔴 ПОСЛЕДНЯЯ МИЛЯ УЛИКИ (03.08.2026). Прибор поиска стенограммы построен и проверен
|
||||
// (`stenogramma.mjs`), но его приговор никуда не шёл: владелец получал «кончился, не отдав
|
||||
// итога» и перебирал каталоги руками — прошлая смена так потеряла целый день. Прибор зовётся
|
||||
// ГОТОВЫЙ, второго правила поиска здесь нет ни строки: две мысли об одном, написанные
|
||||
// в двух местах, расходятся молча (сегодня это уже случилось с подписью ночи).
|
||||
import { stenogrammaPoKartochke, strokaVladeltsu } from './stenogramma.mjs';
|
||||
|
||||
// Сколько ждать после вежливого «остановись» до безусловного «умри».
|
||||
export const KILL_GRACE_MS = 10_000;
|
||||
@@ -278,19 +284,22 @@ export const WORKER_STATUSES = [
|
||||
// мертворождённого, не трогая ни resume.mjs, ни cli-parts.mjs — чужих файлов, которые правят
|
||||
// прямо сейчас. Признак сбоку так не умеет: его надо было бы ещё научиться читать всем троим.
|
||||
//
|
||||
// 🟡 ЧЕСТНО И ВСЛУХ, ОДНА НЕЗАКРЫТАЯ ПОЛОВИНА: русское слово для `born_dead` живёт в
|
||||
// `summary.mjs` (`STATUS_WORD`) — это чужой файл, и я его не тронул. Пока в него не добавят
|
||||
// строку, утренняя таблица «кто что делал» напечатает по такому работнику английское слово
|
||||
// `born_dead`, а сразу за ним — полную русскую причину, которую пишет этот файл. Смысл до
|
||||
// владельца дойдёт, английское слово в глаза бросится. Это названная дыра, а не молчаливая.
|
||||
// 🟢 ТА САМАЯ НЕЗАКРЫТАЯ ПОЛОВИНА — ЗАКРЫТА 03.08.2026. Русское слово для `born_dead`
|
||||
// дописано в `STATUS_WORD` (`summary.mjs`), и утренняя таблица больше не печатает владельцу
|
||||
// английского слова. Прежде здесь стояло: «пока в него не добавят строку, владелец прочтёт
|
||||
// `born_dead`» — дыра была названа вслух и оттого не потерялась.
|
||||
export const FINAL_STATUSES = ['extinguished', 'paused', 'stalled', 'born_dead'];
|
||||
|
||||
// Русские слова для состояний, которых `summary.mjs` ещё не знает. Заводится СВЕРХУ
|
||||
// единственного списка (`STATUS_WORD`), а не рядом с ним: старые слова остаются в одном
|
||||
// месте и разойтись не могут физически — здесь их попросту нет.
|
||||
export const SLOVA_NOVYH_SOSTOYANIY = {
|
||||
born_dead: 'не начал работу вовсе',
|
||||
};
|
||||
// Русские слова для состояний, которых `summary.mjs` ещё не знает.
|
||||
// 🟢 СЕЙЧАС ЗДЕСЬ ПУСТО, И ЭТО ПРАВИЛЬНОЕ СОСТОЯНИЕ: словарь в затее ОДИН (`STATUS_WORD`),
|
||||
// и `born_dead` переехал туда. Держать копию слова здесь после этого нельзя — при слиянии
|
||||
// (`SLOVA_SOSTOYANIY`) местная копия побеждает общую, и разойдись они хоть на знак, владелец
|
||||
// читал бы про одного работника два разных слова, а обе стороны остались бы зелёными.
|
||||
// 🔴 Пустой список оставлен НАРОЧНО, а не убран: это шов на следующий раз. Заведут новое
|
||||
// состояние в надзирателе раньше, чем слово в сводке, — его временное слово ляжет сюда,
|
||||
// и владелец не увидит английского ни одной ночи. Сторож на «состояние без русского слова» —
|
||||
// в проверках сводки, и он краснеет по КЛАССУ, а не по одному забытому слову.
|
||||
export const SLOVA_NOVYH_SOSTOYANIY = {};
|
||||
export const SLOVA_SOSTOYANIY = { ...STATUS_WORD, ...SLOVA_NOVYH_SOSTOYANIY };
|
||||
|
||||
// 🪤 Незнакомое слово печатается КАК ЕСТЬ, а не проваливается в `undefined`. Прежде строка
|
||||
@@ -1061,6 +1070,13 @@ export async function supervise({
|
||||
// след в БОЕВОЙ `.night/deferred/`, и следующая живая ночь упёрлась бы в отказ,
|
||||
// оставленный проверкой. Умолчание настоящее, то есть живая ночь не изменилась.
|
||||
deferredPath = deferredDir(),
|
||||
// 🪤 Умолчание НАСТОЯЩЕЕ — живой поиск стенограммы по карточке работника, как и у прочих
|
||||
// доводов этого круга. Подставка подаётся ЯВНО и только в проверках: пустышка, отвечающая
|
||||
// «нашлась», за три куска нашлась пять раз, а здесь она была бы особенно вредна —
|
||||
// владелец читал бы путь к улике, которой нет.
|
||||
// 🔴 Довод принят ВМЕСТЕ с зовом (правило хозяйства этого файла: принимать параметр можно
|
||||
// только вместе с зовом в том же куске). Зовётся ниже, в `finish`.
|
||||
naytiStenogrammu = stenogrammaPoKartochke,
|
||||
}) {
|
||||
const startedAt = now();
|
||||
const seen = {};
|
||||
@@ -1191,10 +1207,48 @@ export async function supervise({
|
||||
// видеть, где лежит поток и есть ли в нём хоть что-то, — а не искать по каталогам.
|
||||
const sostoyanieSleda = sledPotoka ? sledPotoka.zakryt() : null;
|
||||
writeJsonAtomic(join(workerDirPath, 'worker.json'), card);
|
||||
|
||||
// ━━━ 🔴🔴 ПОСЛЕДНЯЯ МИЛЯ: ПУТЬ К ПОЛНОЙ ЗАПИСИ РАБОТНИКА ДОХОДИТ ДО ВЛАДЕЛЬЦА ━━━
|
||||
//
|
||||
// Ищется ТОЛЬКО у тех, кто кончился не по-хорошему, — то есть когда есть `kak_konchil`
|
||||
// (не запустился · замолчал · не сказал ни слова · оборвался без итога). У честно
|
||||
// погашенного по слову владельца или по восьми часам разбирать нечего, и строка про
|
||||
// стенограмму была бы шумом: семеро работников за ночь утопили бы в исправности
|
||||
// единственную важную строку — тем же приёмом, каким уже отобран показ следа потока.
|
||||
//
|
||||
// 🔴 Зовётся ГОТОВЫЙ прибор по ГОТОВОЙ карточке — она записана строкой выше и уже несёт
|
||||
// всё нужное (угол, номер сессии, время, поручение). Своего второго поиска здесь нет
|
||||
// ни строки: два правила одного смысла расходятся молча.
|
||||
//
|
||||
// 🔴 ПАДЕНИЕ ПРИБОРА НЕ ИМЕЕТ ПРАВА УБИТЬ НАДЗИРАТЕЛЯ. Он в этот миг закрывает работника;
|
||||
// умри он здесь — не будет ни карточки итога, ни строки сводки, ни освобождения замков,
|
||||
// и ночь кончится молча. Это ровно тот класс беды, из-за которого 334 прогона умирали
|
||||
// три месяца незамеченными. Поэтому сбой поиска — это СТРОКА ВЛАДЕЛЬЦУ, а не обвал.
|
||||
let stenogramma = null;
|
||||
if (kak_konchil) {
|
||||
try {
|
||||
stenogramma = naytiStenogrammu(join(workerDirPath, 'worker.json'));
|
||||
} catch (err) {
|
||||
stenogramma = {
|
||||
naydeno: 0,
|
||||
put: null,
|
||||
kandidaty: [],
|
||||
katalog: null,
|
||||
katalogi_probovali: [],
|
||||
sposob: 'поиск стенограммы упал',
|
||||
stroka: `Стенограмму работника найти не удалось — поиск упал: ${err?.message ?? err}. `
|
||||
+ `Смотреть руками: node tools/night/stenogramma.mjs ${join(workerDirPath, 'worker.json')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
writeJsonAtomic(join(workerDirPath, 'exit.json'), {
|
||||
status: final_status, reason, result, touched, at,
|
||||
kak_konchil,
|
||||
stream_raw: sostoyanieSleda,
|
||||
// 🔴 Кладём и сюда, а не только в письмо: письмо владелец прочтёт утром и закроет,
|
||||
// а `exit.json` останется рядом с уликами навсегда.
|
||||
stenogramma,
|
||||
});
|
||||
if (sostoyanieSleda) {
|
||||
logEvent(runDirPath, {
|
||||
@@ -1225,6 +1279,26 @@ export async function supervise({
|
||||
// а не `summaryLinesPath(runId)`: та функция всегда считает от настоящего NIGHT_ROOT
|
||||
// и подмену каталога в проверках не увидит.
|
||||
appendLine(join(runDirPath, SUMMARY_LINES_FILE), { at, worker_id: workerId, line });
|
||||
|
||||
// 🔴 Строка про стенограмму идёт ОТДЕЛЬНОЙ строкой сразу за приговором, а не приклеена
|
||||
// к нему хвостом: строка приговора и так длинная, а путь к улике владелец копирует
|
||||
// глазами — склеенный с причиной смерти, он потеряется в ней.
|
||||
// 🔴 Печатается ВСЕГДА, когда искали, — в том числе «стенограммы нет» и «их несколько».
|
||||
// Молчи мы при неудаче — владелец не отличил бы «улики нет» от «печать опять отвалилась»,
|
||||
// а неразличимость этих двух и есть та беда, которую здесь чинят.
|
||||
const stenogrammaLine = stenogramma
|
||||
? `${workerId}: ${strokaVladeltsu(stenogramma)}`
|
||||
: null;
|
||||
if (stenogrammaLine) {
|
||||
appendLine(join(runDirPath, SUMMARY_LINES_FILE), { at, worker_id: workerId, line: stenogrammaLine });
|
||||
logEvent(runDirPath, {
|
||||
kind: 'worker_transcript',
|
||||
worker: workerId,
|
||||
naydeno: stenogramma.naydeno,
|
||||
sposob: stenogramma.sposob,
|
||||
put: stenogramma.put,
|
||||
}, at);
|
||||
}
|
||||
logEvent(runDirPath, { kind: 'worker_finish', worker: workerId, status: final_status, kak_konchil, reason }, at);
|
||||
|
||||
// 🔴 Файлы освобождаются здесь, в одном месте на все выходы из круга. Расставь мы
|
||||
@@ -1234,7 +1308,10 @@ export async function supervise({
|
||||
|
||||
closeRunIfLast(at);
|
||||
|
||||
return { status: final_status, reason, touched, lines: [line] };
|
||||
// 🪤 Строка про стенограмму отдаётся и НАВЕРХ, не только в файл сводки: тот, кто зовёт
|
||||
// круг напрямую (проверки, а завтра и кусок 7), читает ответ, а не лезет в файл. Отдай
|
||||
// мы её только в файл — половина зовущих потеряла бы улику молча.
|
||||
return { status: final_status, reason, touched, lines: stenogrammaLine ? [line, stenogrammaLine] : [line] };
|
||||
}
|
||||
|
||||
// 🔴 След сырого потока работника открывается ДО его рождения — иначе первые куски
|
||||
|
||||
Reference in New Issue
Block a user