951b15fb85
Находка М-406, правила 244-246. Смена 27 передала список «пять доступных настоящих долгов». Правило 243 велит проверять такой список замером, каждую строку отдельно. Проверил — ДОСТУПНЫМ ОКАЗАЛСЯ ОДИН. Третья смена подряд с той же долей: 14 ложных из 33 долгов, 3 из 4 разъездов, 4 из 5. - `zadachaKonchena` — ✅ настоящий, закрыт этим коммитом; - `reshyonnyeSNevypolnimymi` — 🔒 заперт словом владельца: единственный источник невыполнимых пунктов — ответ владельца «нет» в `questions.mjs`, а он мёртв целиком; - `mailFailedSince` — 🔒 заперт им же: замысел строки 750/1073 называет его дословно («затея видит, что сводка не ушла») частью правила ворот «отклик владельца»; - `readResult` — ❌ не долг, а МЁРТВЫЙ ОСТАТОК. Дороги «запусти, дождись конца, возьми итог» в затее нет: `startWorker` отдаёт живого ребёнка. Четверо читающих потоки делают ДРУГОЕ (надзиратель — живьём по строке; `budget` — ищет замеры предела; судья — берёт последнее событие `result` и текст приговора; `dengi-bez-zapisi` — первую цену), а расход затея нарочно не считает, а спрашивает. Не снесён намеренно: это решение владельца. ЧТО ПОЧИНЕНО. В блоке 5а надзирателя стояло упрощение `otlozhennyePunkty.size === 0` — «отложенных нет, значит всё засчитано». Сюда приходят по ДВУМ причинам, и вторая — не «пунктов не осталось», а «пункт ЕСТЬ, но его файлы держит живой сосед и занять их из-под руки не вышло». Отложенных при этом нет — и на НЕДОДЕЛАННОЙ задаче поднимался флаг приёмщику, а владельцу уходило «Пункты задачи кончились и все засчитаны». Доказано вырезанием: сторож написан первым и покраснел (`items_all_verified` = true на недоделанной задаче), после починки зелёный, при возврате упрощения — снова красный, и ровно один, посчитанный ДО прогона. Прибор №6 подтвердил независимо: имя ушло из приговора. 🔴 ГЛАВНОЕ, ЧТО КУПИЛА СМЕНА, И КУПЛЕНО ЧУЖИМ КРАСНЫМ. Полный прогон после починки дал красное у чужого сторожа `primechanie-ne-rashoditsya-s-pravdoy.test.mjs`. Правило 242 велит сперва спросить, что он бережёт. Он бережёт согласие примечания с правдой и по замыслу ПРАВ. Шапку я поправил — он всё равно краснел. Причина: сторож брал `items.mjs` СЫРЫМ файлом и искал фразу-маркер «НЕТ НИ ОДНОГО», а она стоит там в ЖИВОМ КОДЕ — в тексте ошибки «в плане задачи нет ни одного пункта». Пока шапка врала тем же словом, обе половины совпадали и сторож молчал: он был зелёным ПО ДВУМ ПРИЧИНАМ РАЗОМ, и одна из них случайная. ⇒ Заведён третий рез `tolkoPrimechaniya`, обратный `bezPrimechaniy`, в тот же дом и к той же развилке «какой брать». Обратность стережётся отдельной проверкой (что вырезал один — то оставил второй). Все три места сторожа переведены на него, хотя слепота задевала одно: у двух других фразы-маркера в коде сегодня нет — но это удача, а не защита. 244. У СТОРОЖА, ЧИТАЮЩЕГО ФАЙЛ СЫРЫМ, СПРОСИ: НЕ ОТВЕЧАЕТ ЛИ ЗА ПРИМЕЧАНИЕ ТО, ЧТО ПРИМЕЧАНИЕМ НЕ ЯВЛЯЕТСЯ. Обратная сторона 234: тот про «рез срезал зерно», этот про «отсутствие реза подсунуло шелуху». 245. У КРАСНОГО ЧУЖОГО СТОРОЖА ТРИ ИСХОДА, А НЕ ДВА. Третий: прав он по замыслу, но слеп по способу — тогда чинят САМОГО СТОРОЖА. Спроси, что он измерил, и то ли это, что он берётся охранять. 246. `grep -i` НА РУССКИХ БУКВАХ РЕГИСТР НЕ ПРИВОДИТ: «НЕТ НИ ОДНОГО» → 0, «нет ни одного» → 1, node тем же выражением → нашёл. Мерь тем же телом, каким меряет проверяемый. ПОПУТНО три примечания приведены к правде (правило 235): у `zadachaKonchena` живой зовущий теперь ЕСТЬ И ОН ОДИН (второго не будет, пока нет куска 7 — писать «зовут двое» значило бы чинить враньё половиной правды); `readResult` назван мёртвым остатком; `reshyonnyeSNevypolnimymi` помечен запертым, а не «некому сделать». Проверок 2929 в 108 файлах (было 2927), ноль красных, код 0. Прибор №6: вывозится 613, точных долгов 29 — число то же, состав другой: `zadachaKonchena` ушёл, на его место встал новый рез (класс правила 231, прибор говорит это сам). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
435 lines
36 KiB
JavaScript
435 lines
36 KiB
JavaScript
// Команды снаружи. Панель, бот или сама рука владельца кладут файл в `.night/commands/`,
|
||
// а читает его НАДЗИРАТЕЛЬ — работник этого модуля не видит вовсе: он другой процесс
|
||
// в другом каталоге и разговаривает только файлами своего почтового ящика.
|
||
//
|
||
// 🔴 Файл заведён ЧАСТЬЮ. Здесь только приём команд; `closeItem`, `afterItemDone`,
|
||
// `afterTaskDone` и `deferredTaskGate` дописывает в этот же файл кусок 4 — заводить
|
||
// его заново ему не надо.
|
||
import { existsSync, readdirSync, unlinkSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { readJson, writeJsonAtomic } from './state.mjs';
|
||
|
||
// Закрытый список, и порядок в нём значимый: «стоп всё» главнее «стопа», «стоп» главнее
|
||
// «паузы». Слово не из списка не читается вовсе — выдумывать, что оно значит, нельзя.
|
||
// 🔴 Список вывезен наружу нарочно: надзиратель называет его владельцу в сводке, когда
|
||
// команду не понял. Второй такой же список рядом разошёлся бы с этим в первый же месяц.
|
||
export const COMMAND_WORDS = ['stopall', 'stop', 'pause', 'resume'];
|
||
const CMDS = COMMAND_WORDS;
|
||
|
||
export function readCommand(commandsDirPath, workerId) {
|
||
// 🪤 Каталог команд заводит рука владельца, и в первую ночь его может не быть.
|
||
// Брошенная тут ошибка убила бы надзирателя молча — тот самый класс беды, из-за
|
||
// которого 334 прогона умирали три месяца незамеченными.
|
||
if (!commandsDirPath || !existsSync(commandsDirPath)) return null;
|
||
let files;
|
||
try {
|
||
files = readdirSync(commandsDirPath);
|
||
} catch {
|
||
return null; // каталог недоступен — это отсутствие команды, а не повод падать
|
||
}
|
||
for (const cmd of CMDS) {
|
||
const all = `all.${cmd}.json`;
|
||
const mine = `${workerId}.${cmd}.json`;
|
||
// Общая идёт первой: она касается и того, кому лично ничего не давали.
|
||
for (const name of [all, mine]) {
|
||
if (!files.includes(name)) continue;
|
||
// Порванный файл — отсутствие сведений, а не мусор: слово команды стоит
|
||
// в самом имени файла, и его достаточно.
|
||
const body = readJson(join(commandsDirPath, name)) ?? {};
|
||
// 🪤 Общую команду, которую ЭТОТ работник уже исполнил, второй раз не отдаём:
|
||
// иначе он гасился бы по ней снова и снова, пока файл ждёт остальных.
|
||
if (name === all && (body.done ?? []).includes(workerId)) continue;
|
||
// 🔴🔴 `cmd` стоит ПОСЛЕ тела, а не до него, и это не вкус. Слово команды берётся
|
||
// из ИМЕНИ файла — оно и есть закрытый список. Поставь мы `cmd` перед `...body`,
|
||
// и тело файла перебило бы его: положи кто-нибудь в `w-1.pause.json` тело
|
||
// {"cmd":"stopall"} — работник был бы погашен, хотя имя файла говорит «пауза».
|
||
// Тем же способом обходился бы и весь закрытый список `CMDS`. Тело команды несёт
|
||
// сведения (кто, когда, почему), а не слово; слово — только из имени.
|
||
return { ...body, cmd, file: name, common: name === all };
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 🔴🔴 Владелец узнаёт, что его команду НЕ ПОНЯЛИ (решение диспетчера). Слово не из
|
||
// закрытого списка исполнять нельзя — выдумывать его смысл мы не станем. Но и молчать
|
||
// нельзя: владелец положил файл, ушёл спать и думает, что «пауза» нажата, а её никто
|
||
// не понял. Молчание в ответ на команду хозяина недопустимо.
|
||
// 🪤 Эта работа НИЧЕГО не удаляет и ничего не исполняет — только называет. Файл владельца
|
||
// остаётся лежать: он и есть доказательство, и утром хозяин увидит его на месте, а не
|
||
// будет гадать, куда тот делся.
|
||
export function unknownCommands(commandsDirPath, workerId) {
|
||
if (!commandsDirPath || !existsSync(commandsDirPath)) return [];
|
||
let files;
|
||
try {
|
||
files = readdirSync(commandsDirPath);
|
||
} catch {
|
||
return []; // каталог недоступен — сказать нечего, и падать не о чем
|
||
}
|
||
const out = [];
|
||
for (const name of files) {
|
||
// Команда — это ровно `<кому>.<слово>.json`. Всё прочее в каталоге не наше дело:
|
||
// объявлять чужой файл «непонятой командой» — значит пугать владельца на пустом месте.
|
||
const razbor = /^(.+)\.([^.]+)\.json$/.exec(name);
|
||
if (!razbor) continue;
|
||
const [, komu, slovo] = razbor;
|
||
// Чужая личная команда — не наша забота: о ней скажет надзиратель того работника,
|
||
// иначе одну и ту же строку написали бы семеро разом.
|
||
if (komu !== 'all' && komu !== workerId) continue;
|
||
if (CMDS.includes(slovo)) continue; // понятную здесь не называем
|
||
out.push({ file: name, word: slovo, common: komu === 'all' });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// 🪤 Прежде эта работа стирала И общий, И личный файл разом. Из-за этого «стоп всё»
|
||
// съедал первый же прочитавший, а остальные шестеро продолжали работать при отданной
|
||
// команде — и владелец видел «остановлено», когда ночь шла дальше. Теперь личная команда
|
||
// убирается сразу, а общая — только когда её исполнили ВСЕ, кого она касается; до тех пор
|
||
// в её файле копится список исполнивших.
|
||
export function consumeCommand(commandsDirPath, workerId, cmd, { concerns = [] } = {}) {
|
||
const result = { personal_removed: false, common_removed: false, done: [] };
|
||
if (!commandsDirPath || !existsSync(commandsDirPath)) return result;
|
||
|
||
const mine = join(commandsDirPath, `${workerId}.${cmd}.json`);
|
||
if (existsSync(mine)) {
|
||
unlinkSync(mine);
|
||
result.personal_removed = true;
|
||
}
|
||
|
||
const all = join(commandsDirPath, `all.${cmd}.json`);
|
||
if (!existsSync(all)) return result;
|
||
|
||
// 🔴 Список исполнивших живёт В ФАЙЛЕ, а не в памяти надзирателя: надзиратели — разные
|
||
// процессы, и каждый видел бы свой собственный список. Перечитываем прямо перед записью,
|
||
// чтобы не затереть отметку соседа, поставленную секунду назад.
|
||
const body = readJson(all) ?? {};
|
||
const done = Array.from(new Set([...(body.done ?? []), workerId]));
|
||
result.done = done;
|
||
|
||
// Кого команда касается, знает зовущий: надзиратель берёт состав прогона из `run.json`.
|
||
// Списка нет — файл НЕ удаляем: лучше лишний раз промолчать, чем снять команду с тех,
|
||
// до кого она не дошла.
|
||
if (concerns.length && concerns.every(w => done.includes(w))) {
|
||
unlinkSync(all);
|
||
result.common_removed = true;
|
||
return result;
|
||
}
|
||
writeJsonAtomic(all, { ...body, done });
|
||
return result;
|
||
}
|
||
|
||
// ── Кусок 4: закрытие пункта, очередь пунктов, конец задачи ────────────────────
|
||
//
|
||
// 🔴 «Пункт закрыт» — не слово работника, а доказательство: сохранённый коммит ПЛЮС
|
||
// зелёные проверки ИМЕННО ЭТОГО пункта. Зелень меряет надзиратель сам (`item-audit.mjs`,
|
||
// задача 4), сюда она приходит уже замеренной; слово работника доказательством не считается.
|
||
//
|
||
// 🔴 Порядок отказов значим. Владелец и работник читают ПЕРВУЮ названную причину и чинят
|
||
// ЕЁ. Поэтому сперва «в плане нет проверок», потом «в расчёте на ответ», потом «нет
|
||
// коммита», и только потом «проверки красные»: скажи мы про красный код там, где на деле
|
||
// не пришёл ответ хозяина, — работник полночи чинил бы верное.
|
||
|
||
// Номера пунктов владелец пишет то `2`, то `"2"`. Сравнение знак в знак вернуло бы
|
||
// работнику пункт, который он только что закрыл, и он крутился бы на нём до утра.
|
||
function odinNomer(id) {
|
||
return String(id ?? '').trim();
|
||
}
|
||
|
||
export function closeItem({ item, commit_sha, tests_green, on_assumption = false, answer_received = false }) {
|
||
if (!item || typeof item !== 'object') {
|
||
throw new Error('closeItem: пункт не передан. Это сбой зова, а не свойство пункта: тихий отказ «не засчитан» выглядел бы как честная работа замка, хотя замок ничего не смотрел.');
|
||
}
|
||
|
||
// Проверка 39б: у пункта в плане не записано ни одной проверки — закрывать нечем.
|
||
if (!Array.isArray(item.tests) || item.tests.length === 0) {
|
||
return { closed: false, reason: `Для пункта ${odinNomer(item.id) || '?'} в плане не записано ни одной проверки — закрыть его нечем. Проверки пункта ставит тот, кто пишет план, строкой-пометой под заголовком.` };
|
||
}
|
||
|
||
// Проверка 40: сделанное «в расчёте на ответ» не закрывается, пока ответа нет,
|
||
// сколько бы зелёных проверок по нему ни прошло.
|
||
if (on_assumption && !answer_received) {
|
||
return { closed: false, reason: `Пункт ${odinNomer(item.id)} сделан «в расчёте на ответ» владельца, а ответа ещё нет. Закрывать такой пункт нельзя, сколько бы зелёных проверок по нему ни прошло: ответ может отменить сделанное.` };
|
||
}
|
||
|
||
if (!commit_sha) {
|
||
return { closed: false, reason: `Сделанное по пункту ${odinNomer(item.id)} не сохранено — коммита в ветке задачи нет. Несохранённое не переживёт ночь.` };
|
||
}
|
||
|
||
// Проверка 38: нет зелени по ЭТОМУ пункту — не засчитан. `tests_green` приходит
|
||
// от надзирателя замеренным (задача 4); не переданный вовсе — это «замерить
|
||
// не удалось», а не «наверное зелено».
|
||
if (tests_green !== true) {
|
||
return { closed: false, reason: `Проверки пункта ${odinNomer(item.id)} (${item.tests.join(', ')}) не прогнаны или не зелёные — пункт не засчитан.` };
|
||
}
|
||
|
||
return { closed: true, reason: null };
|
||
}
|
||
|
||
// 🔴🔴 ГРАНИЦА ЭТОЙ РАБОТЫ — ЧИТАТЬ ДО ТОГО, КАК ВЗЯТЬ ЕЁ ОТВЕТ ЗА «ВОТ ТВОЙ ПУНКТ».
|
||
//
|
||
// На вопрос «какой пункт работнику брать следующим» в затее отвечают ДВЕ работы, и они
|
||
// отвечают РАЗНОЕ на одном и том же хозяйстве. Это не поломка, это разделение ролей:
|
||
//
|
||
// · `nextFreeItem` (`file-claims.mjs`) — ВЫДАЁТ пункт. Ей передают каталог прогона,
|
||
// она читает список занятых файлов и пропускает пункты, чьи файлы держит чужой.
|
||
// · `afterItemDone` (здесь) — говорит, ЕСТЬ ЛИ ВООБЩЕ ЧТО БРАТЬ и не пора ли гаснуть.
|
||
//
|
||
// ЧЕГО ЭТА РАБОТА НЕ ЗНАЕТ: занятых файлов. Ей не передают ни каталога прогона, ни списка
|
||
// занятий — и передать нечем, в её подписи такого входа нет. Она смотрит только на план
|
||
// и на список закрытых пунктов.
|
||
//
|
||
// ОТ ЧЕГО ОНА НЕ ЗАЩИЩАЕТ: от того, что названный ею пункт прямо сейчас правит сосед.
|
||
// Замер 02.08.2026: w-1 занял `app/a.php` (пункт 1) и сидит на нём, w-2 спрашивает —
|
||
// `nextFreeItem` отвечает «пункт 2», `afterItemDone` отвечает «пункт 1», то есть тот,
|
||
// на котором сидит сосед. Возьми кто-нибудь этот ответ как выдачу пункта — двое молча
|
||
// правили бы одни файлы всю ночь при зелёных проверках, а замок занятий не сработал бы
|
||
// ни разу: его просто не спросили.
|
||
//
|
||
// 🔴 КАК ЗВАТЬ ПРАВИЛЬНО (так зовёт план задачи 5, проверено 02.08.2026): у этой работы
|
||
// спрашивают «пусто или нет» и «гаснуть ли», а САМ ПУНКТ берут через `nextFreeItem`.
|
||
// Ответ `next_item` тут — это «работа ещё есть», а не «вот твой пункт».
|
||
//
|
||
// 🪤 Собрался передать сюда каталог прогона или занятия — ОСТАНОВИСЬ и иди к владельцу.
|
||
// Подписи обеих работ ждёт задача 5, и менять разделение ролей — его решение, не твоё.
|
||
// Границу сторожит проверка «две двери на один вопрос» в `items.test.mjs`: она покраснеет
|
||
// и на попытку научить эту работу занятиям, и на попытку отучить от них `nextFreeItem`.
|
||
export function afterItemDone({ items, done, answered_during_day = false }) {
|
||
if (!Array.isArray(items)) {
|
||
throw new Error('afterItemDone: подан не список пунктов. Тихий ответ «пункты кончились» погасил бы всех работников задачи на ровном месте, и владелец прочёл бы утром «работа сделана», когда её никто не делал.');
|
||
}
|
||
if (!Array.isArray(done)) {
|
||
throw new Error('afterItemDone: не подан список закрытых пунктов. Тихое «ничего не закрыто» вернуло бы работнику тот самый пункт, который он только что закрыл.');
|
||
}
|
||
|
||
if (answered_during_day) {
|
||
return {
|
||
next_item: null,
|
||
extinguish_reason: 'Работник дождался ответа владельца днём — доделал свой пункт и гаснет, нового не берёт: иначе он жёг бы недельный запас вместе с владельцем.',
|
||
task_note: 'Остаток плана сам не подхватывается — задача продолжится только по слову владельца «продолжай».',
|
||
};
|
||
}
|
||
|
||
const zakryto = done.map(odinNomer);
|
||
const next = items.find(i => !zakryto.includes(odinNomer(i?.id))) ?? null;
|
||
return {
|
||
next_item: next,
|
||
extinguish_reason: next ? null : 'Пункты плана кончились — брать больше нечего.',
|
||
task_note: null,
|
||
};
|
||
}
|
||
|
||
export function afterTaskDone({ task_id, workers }) {
|
||
if (!Array.isArray(workers) || workers.length === 0) {
|
||
throw new Error('afterTaskDone: не сказано, каких работников гасить. Тихий пустой список прочёлся бы как «все погашены», и задача считалась бы законченной при живых работниках.');
|
||
}
|
||
return {
|
||
task_id,
|
||
// Список копируется: правка снаружи не должна менять то, что уже решено.
|
||
extinguish_workers: [...workers],
|
||
// Проверка 25: следующая задача в том же прогоне не начинается — задача упирается
|
||
// в приёмку владельца.
|
||
start_next_task: false,
|
||
// 🟡 Флаг поднят и ляжет в `run.json` (задача 5). САМОГО приёмщика по нему заводит
|
||
// кусок 7 (`startReviewer`) — здесь заводить его нечем и незачем. Пока по флагу
|
||
// никто ничего не запускает, и это названо вслух, чтобы правило не сочли работающим.
|
||
reviewer_starts: true,
|
||
note: 'Задача закончена: все её работники погашены, следующая задача в этом прогоне не начинается. Дальше — приёмка владельца, приёмщика по этому флагу заводит кусок 7.',
|
||
};
|
||
}
|
||
|
||
// ── Кусок 4, задача 3: ворота возврата отложенной задачи ───────────────────────
|
||
//
|
||
// 🔴 Проверка 28: задача не влезла в прогон — следующим прогоном САМА не подхватывается.
|
||
// 🔴 Проверка 29: не прошла приёмку — возвращается только по слову владельца, с тем же
|
||
// планом и со списком брака, и делает её НОВЫЙ работник.
|
||
// 🔴 Проверка 30: план отложенной задачи сначала заново проходит проверку — и в случае
|
||
// «не влезла», и в случае «не прошла приёмку». Пока задача лежала, продукт менялся.
|
||
//
|
||
// 🪤 Слово `reason` тут значит две разные вещи, и договор о стыках их не развёл: НА ВХОДЕ
|
||
// это вид случая (закрытый список), НА ВЫХОДЕ — текст отказа владельцу. Имена заданы
|
||
// договором дословно и менять их нельзя; столкновение названо вслух.
|
||
import { DEFER_REASONS, spisokPrichinSlovami } from './deferred.mjs';
|
||
// 🔴🔴 СРАВНИВАТЕЛЬ ПУТИ ЗДЕСЬ НЕ ЗАВОДИТСЯ СВОЙ, и это отступление от плана задачи 3,
|
||
// названное вслух. План (01.08) велел завести в этом файле свою мелочь `odinVidPuti`,
|
||
// сводящую только косые черты. К 02.08 в `paths.mjs` уже живут ДВЕ работы с этим же
|
||
// вопросом — `samePath` («это один и тот же путь?») и `odinVidPuti` (свой вид имени
|
||
// файла), и на второй копии сравнивателя там стоят два сторожа: «сравниватель пути —
|
||
// ОДИН на всю затею». Третья копия под тем же именем `odinVidPuti`, но с другим
|
||
// поведением — ровно тот класс, на котором уже обжигались.
|
||
//
|
||
// 🔴 И дело не только в чистоте: своя мелочь плана ВРАЛА БЫ ЖИВЬЁМ. Замерено 02.08.2026
|
||
// (`node` на трёх сравнивателях): владелец запускает прогон ПОЛНЫМ путём
|
||
// (`night run C:\…\план.md` — так сказано в примечании к `samePath`), а в след ложится
|
||
// путь ОТ КОРНЯ хранилища. Сведение одних косых даёт «план не тот» на ОДНОМ И ТОМ ЖЕ
|
||
// плане: ворота отказали бы владельцу на его же работе, отложенная задача не вернулась
|
||
// бы никогда, и починить это он не смог бы ничем. То же и с `./план.md`.
|
||
// `samePath` оба случая проходит верно (замерено), а разные планы по-прежнему разводит.
|
||
import { samePath } from './paths.mjs';
|
||
|
||
export function deferredTaskGate({
|
||
reason, owner_said_continue, plan_revalidated, defect_list = [],
|
||
// 🔴 «ТОТ ЖЕ ПЛАН» (проверка 29 дословно). `plan_path` — путь плана ИЗ СЛЕДА, тот,
|
||
// по которому задачу откладывали. `plan_path_now` — путь плана в ЭТОМ запуске.
|
||
// Не сравни их — задачу можно было бы «доделать» по совсем другому плану, и доделано
|
||
// было бы НЕ то, что не доделали. Оба умолчания пустые: не дали — не сравниваем
|
||
// и молчим, а не выдумываем совпадение.
|
||
plan_path = null,
|
||
plan_path_now = null,
|
||
}) {
|
||
if (!DEFER_REASONS.includes(reason)) {
|
||
// 🟡 Отступление от плана, названное вслух: план велел ЭТУ строку не трогать, потому
|
||
// что её проверяют только словами `not_finished|rejected`. Но после третьего слова
|
||
// (`impossible`, шаг 9ж) она стала ВРАТЬ владельцу: говорила «одним из двух», а список
|
||
// принимает три. Врущее сообщение об отказе — ровно тот класс, от которого вся затея
|
||
// и строится, поэтому счёт исправлен. Сами слова в строке не тронуты.
|
||
// 🔴 04.08.2026: строка была ГОЛОЙ тройкой без единого русского слова — рядом
|
||
// (`deferred.mjs`) те же три слова уже объяснены по-русски, а тут владелец видел бы
|
||
// латиницу без перевода. Список слов теперь один на оба файла — `spisokPrichinSlovami()`.
|
||
throw new Error(`Ворота возврата: вид отложенной задачи должен быть одним из трёх — ${spisokPrichinSlovami()}. Дано: ${JSON.stringify(reason)}. Молчаливое «это не rejected» отменило бы требование списка брака, и задача вернулась бы в работу без него.`);
|
||
}
|
||
|
||
// 🔴 ТОТ ЖЕ ПЛАН (проверка 29). Стоит ПЕРВЫМ из отказов, и это нарочно: слово владельца
|
||
// «продолжай» относится к ТОЙ задаче, которую отложили. Подмени план — и «продолжай»
|
||
// сказано уже про другую работу, а владелец об этом не знает.
|
||
if (plan_path && plan_path_now && !samePath(plan_path, plan_path_now)) {
|
||
return {
|
||
may_run: false,
|
||
reason: `План не тот. Задачу откладывали по плану ${plan_path}, а запускают её по плану ${plan_path_now} — это разные планы, и доделано будет не то, что не доделали. Запустите с тем же планом; если план вправду сменился, это новая задача, а старый след снимите руками.`,
|
||
fresh_worker_required: false,
|
||
defect_list: [...defect_list],
|
||
};
|
||
}
|
||
|
||
// Проверка 28/29а: без слова владельца — никуда. Молчание не согласие.
|
||
if (!owner_said_continue) {
|
||
return {
|
||
may_run: false,
|
||
reason: 'Отложенная задача сама не подхватывается. Нужно слово владельца «продолжай» — ключ --continue в строке запуска.',
|
||
fresh_worker_required: false,
|
||
defect_list: [...defect_list],
|
||
};
|
||
}
|
||
|
||
// Проверка 30: план перепроверяется заново — в ОБОИХ случаях, и «не влезла»,
|
||
// и «не прошла приёмку».
|
||
if (!plan_revalidated) {
|
||
return {
|
||
may_run: false,
|
||
reason: 'План отложенной задачи должен заново пройти проверку: пока задача лежала, продукт менялся, и вчера годный план сегодня может не годиться вовсе.',
|
||
fresh_worker_required: false,
|
||
defect_list: [...defect_list],
|
||
};
|
||
}
|
||
|
||
// Проверка 29в: у вернувшейся с приёмки список брака обязателен. У той, что просто
|
||
// не успела, брака не было — и спрашивать его не с кого.
|
||
if (reason === 'rejected' && defect_list.length === 0) {
|
||
return {
|
||
may_run: false,
|
||
reason: 'Задача возвращена с приёмки владельца, но список брака пуст — работать не с чем. Подайте его ключом --defects <файл>. (До куска 7 список подаётся с руки: настоящую приёмку, откуда он возьмётся сам, строит кусок 7.)',
|
||
fresh_worker_required: false,
|
||
defect_list: [],
|
||
};
|
||
}
|
||
|
||
return {
|
||
may_run: true,
|
||
reason: null,
|
||
// Проверка 29г: делает её НОВЫЙ работник — прежний погас вместе с концом задачи.
|
||
fresh_worker_required: true,
|
||
// Список брака идёт ДАЛЬШЕ тем же составом: иначе ворота его прочли, кивнули
|
||
// и потеряли, а работник получил бы тот же план без единого слова о том, что в нём
|
||
// не так.
|
||
defect_list: [...defect_list],
|
||
};
|
||
}
|
||
|
||
// ── Долг куска 5 (Р14) и куска 7 (блок 6а), внесён 01.08 ───────────────────────
|
||
//
|
||
// 🔴 Пункт, который владелец ответом «нет» велел признать НЕВЫПОЛНИМЫМ (кусок 5, Р14),
|
||
// не проходит через `closeItem`: у него никогда не будет ни коммита, ни зелёных проверок —
|
||
// он и не должен их иметь. Без этого правила такой пункт НАВСЕГДА остаётся «не среди
|
||
// засчитанных», задача не закрывается никогда, и приёмщик (кусок 7, блок 6а) не заводится
|
||
// вовсе — молча (находка куска 7 СВ-6).
|
||
//
|
||
// 🪤 Списки `verified` и `impossible` по смыслу не пересекаются (пункт либо закрыт зелёными
|
||
// проверками, либо признан невыполнимым — не оба сразу), но эта функция того не проверяет:
|
||
// она не судья, а чистое сложение множеств. Судят в разных местах: замок (эта же задача,
|
||
// `closeItem`) — про «закрыт»; надзиратель круга (кусок 5, блок 2в) — про «невыполним».
|
||
//
|
||
// 🔴🔴 Откуда взять живой список невыполнимых — здесь НЕ решено. `item-verdict.json` кусок 5
|
||
// кладёт в ящик РАБОТНИКА одним файлом, и следующий вердикт затирает предыдущий; копящего
|
||
// ленджера невыполнимых пунктов затея на 02.08 не заводит нигде. Значит правило готово,
|
||
// а живой провод к нему — нет: кто будет его делать, обязан сперва завести такой ленджер,
|
||
// иначе звать это правило будет нечем.
|
||
//
|
||
// 🔒 ЗАМЕРЕНО 07.08.2026, СМЕНА 28 — И ЭТО НЕ «НЕКОМУ СДЕЛАТЬ», А «НЕЛЬЗЯ НАЧАТЬ».
|
||
// Единственное место затеи, откуда пункт вообще становится невыполнимым, — ответ владельца
|
||
// «нет» в `questions.mjs` (`item_marked_impossible`). Механизм вопросов владельцу мёртв
|
||
// С ОБЕИХ СТОРОН и ждёт его слова: подключение меняет ЕГО ночь. Значит и это правило
|
||
// заперто тем же решением — заводить ленджер невыполнимых, пока их неоткуда взять,
|
||
// значит строить провод к источнику, которого нет.
|
||
export function reshyonnyeSNevypolnimymi(verified = [], impossible = []) {
|
||
return [...new Set([...verified, ...impossible].map(String))];
|
||
}
|
||
|
||
// ── кусок 7, задача 6 ────────────────────────────────────────────────────────────────
|
||
// 🟢 ЖИВОЙ ЗОВУЩИЙ ЕСТЬ, И ОН ОДИН. ЗАМЕРЕНО 07.08.2026, СМЕНА 28.
|
||
//
|
||
// 🔴 ЧИСЛО ЗДЕСЬ НАЗВАНО ТОЧНО, И ЭТО НЕ ПРИДИРКА. Замысел велит звать правило ДВОИМ:
|
||
// блок 5а куска 4 (вслед за засчитанной заявкой) и блок 6а куска 7 (каждый круг).
|
||
// Подключён ПЕРВЫЙ — `supervisor.mjs`, блок 5а. Второго нет: куска 7 в затее нет вовсе.
|
||
// 🪤 Напиши здесь «зовут его двое» — и это снова стало бы правилом 235: задуманное
|
||
// в настоящем времени читается как доказательство подключения, будучи его заменой.
|
||
// Прежняя смена честно пометила ту же строку как враньё; чинить враньё половиной правды
|
||
// нельзя. Пока куска 7 нет — здесь стоит ОДИН.
|
||
//
|
||
// 🔴 ЕДИНСТВЕННОЕ место затеи, где записано, что значит «задача кончена». Второй дороги
|
||
// к этому правилу не заводить: разъедутся молча, и приёмщик пойдёт снимать недоделанную
|
||
// задачу — а владелец утром прочтёт «сделано». Ровно эту беду и держал прежний зов:
|
||
// вместо правила там стояло упрощение `otlozhennyePunkty.size === 0`, и на пункте,
|
||
// чьи файлы держит живой сосед, оно отвечало «всё засчитано» (замерено вырезанием).
|
||
//
|
||
// 🔴 Третий довод `otlozhennye` обязателен по существу (находка Н8). Отложенный пункт лежит
|
||
// в списке `done` очереди — чтобы она не выдала его снова, — но в `items-verified.jsonl`
|
||
// НЕ попадает. Значит по паре `{ items, verified }` его не видно вовсе, и правило объявило бы
|
||
// задачу кончённой там, где кусок 4 её кончённой не считает.
|
||
export function zadachaKonchena({ items, verified, otlozhennye }) {
|
||
const plan = (Array.isArray(items) ? items : []).map(String);
|
||
// 🔴 Сравниваем ИМЕНАМИ, а не числами: пункты зовутся «4.11», «10», «2»,
|
||
// и числовое сравнение здесь врёт молча.
|
||
const zaschitany = new Set((Array.isArray(verified) ? verified : []).map(String));
|
||
|
||
// 🔴 «Про отложенные не спросили» — это ТРЕТЬЕ состояние, и слипаться с «отложенных нет»
|
||
// ему нельзя: старый зовущий, не знающий про новый довод, молча получал бы «кончена»,
|
||
// то есть правка тихо вернула бы ровно ту беду, ради которой довод и заведён.
|
||
if (otlozhennye === null || otlozhennye === undefined) {
|
||
return {
|
||
konchena: false,
|
||
left: plan.filter(p => !zaschitany.has(p)),
|
||
reason: 'про отложенные пункты не спросили — пока не спросят, задача кончённой не считается',
|
||
};
|
||
}
|
||
const otlozheny = new Set([...otlozhennye].map(String));
|
||
|
||
// Пустой план — это не победа, а сбой разбора: пунктов не нашлось вовсе.
|
||
if (plan.length === 0) {
|
||
return { konchena: false, left: [], reason: 'в плане задачи нет ни одного пункта — считать нечего' };
|
||
}
|
||
|
||
const left = plan.filter(p => !zaschitany.has(p));
|
||
if (left.length) {
|
||
return { konchena: false, left, reason: `не засчитаны пункты: ${left.join(', ')}` };
|
||
}
|
||
const otlozhennyeIzPlana = plan.filter(p => otlozheny.has(p));
|
||
if (otlozhennyeIzPlana.length) {
|
||
return {
|
||
konchena: false,
|
||
left: otlozhennyeIzPlana,
|
||
reason: `отложены пункты: ${otlozhennyeIzPlana.join(', ')} — задача не кончена, пока они висят`,
|
||
};
|
||
}
|
||
return { konchena: true, left: [], reason: 'все пункты плана засчитаны, отложенных нет' };
|
||
}
|