99edc75a61
Находка М-410, правило 254. Смена 31 исполнила задание смены 30 — задала правилу 253 вопрос «нет ли близнеца с тем же ремеслом» остальному хозяйству. Больных нашлось ДВА. ━━ 1. СТОРОЖ ПЛАНОВ И ЖИВЫЕ ВОРОТА СЧИТАЛИ ЗАДАЧИ РАЗНЫМИ ТЕЛАМИ ━━ Правило «план размечен для ночи НАПОЛОВИНУ» — самое дорогое в своей семье: беда выглядит РОВНО как успех (ночь делает три задачи из девяти и отчитывается успехом). Оно родилось в стороже (`polumechen`) и позже переехало на дорогу живого запуска (`validatePlan`). Переезд состоялся — а старая копия осталась жить на прежнем месте, со своей приметой. Замерено по ВСЕМ 594 живым планам обоими настоящими телами. Приговор «кричать или молчать» сошёлся везде. А число, которое видит владелец, разошлось на живой бумаге: `2026-08-01-kusok2-progon-ne-nachnyotsya.md` — сторож 12, ворота 11. Двенадцатая «задача» — строка `## Задача 1: оставить след` на 6122-й строке плана. Она лежит ВНУТРИ рамки с подписью powershell: это текст, который команда записывает в пустышку, а не работа плана. Ворота правы, сторож ошибался. 🔴 И своё неверное число сторож держал утверждением `expect(schyot).toEqual({… 12 …})`. Пока оно стояло, оно ошибку ОХРАНЯЛО: почини кто-нибудь тело — покраснел бы верный ответ. Разъехались тремя разрезами, все три замерены подложками: рамка-образец (ворота пропускают, сторож считал), «####» (у ворот подзаголовок, у сторожа задача), буква «ё» в номере (у ворот `[а-яё]`, у сторожа `[а-я]` — теряла). Лечение по правилу 249, а не подгонкой приметы: `skolkoRusskihZadach` вывезено наружу, своей приметы у сторожа больше НЕТ вовсе. Разъехаться им теперь физически нечем. Доказано вырезанием: вернул прежнее тело — покраснели ровно 5 проверок, посчитанные ДО прогона (четыре новых сторожа + закреплённое неверное число). ━━ 2. ИМЯ, ИЗ КОТОРОГО СОБИРАЮТ ОБРАЗЕЦ, ОБЕЗВРЕЖИВАЛИ ТРИ МЕСТА ИЗ ПЯТИ ━━ В `schyot-zovushchih.mjs` пять мест делают одно ремесло — берут имя из чужого кода и собирают из него образец. Три обезвреживали знак `$`, `zovyImeni` — нет, и `vvoziatLiModulCelikom` не обезвреживал имя файла. `$` — законный знак имени, а в образце значит «конец строки». Имя `ceny$vRublyah` в образце совпасть не может НИКОГДА: прибор ответит «зовущих нет», сито увидит «нигде не ввезено, дома не зовётся» и вынесет «ТОЧНО ДОЛГ» — самый твёрдый приговор, у которого оговорки «посмотри глазами» нет вовсе. Живой код пошли бы сносить как мёртвый. 🟢 Честная граница: живого вреда на день находки НЕТ. Имён со знаком `$` в хозяйстве ноль из 613 вывозимых, расхождений по живому — ноль. Правится не потому, что врёт сегодня, а потому что соврёт молча и в приговоре, который никто не перепроверяет. Доказано вырезанием: ровно 2 красных, посчитанные до прогона; второй сказал последствие словами — живое имя оказалось в списке «точно долг». ━━ ЧИСЛА ━━ Проверок 2954 в 109 файлах (было 2948/109), ноль красных, код 0. Вывозится наружу 614 (было 613 — прибавилось общее тело `skolkoRusskihZadach`). Точных долгов 29, состав сверен поимённо командой `sort | diff` до и после лечения — те же 29 имён. Живых рождений ноль, денег не потрачено. 🔴 Свои ошибки, названные честно, — их две. Первая: мерил число ворот своей приметой по их же словам, и примета цепляла ПРИМЕР из самих слов отказа (правило 246 с новой стороны) — поймал сам, перемерил. Вторая: взял `git stash` в углу, у которого заначка ОБЩАЯ со всеми параллельными сессиями; толкнись сосед между моими push и pop — pop без адреса вытащил бы чужую работу в мой угол молча. Обошлось по удаче, приём вычеркнут. 🟢 Правку «только Edit/Write» смена 31 НЕ нарушила: оба вырезания сделаны через Edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
446 lines
39 KiB
JavaScript
446 lines
39 KiB
JavaScript
// Разбор плана задачи. Пункт — заголовок «### Task N: Название».
|
||
//
|
||
// 🔴 Помету ставит ТОТ, КТО ПИШЕТ ПЛАН, а не работник на ходу — так дословно требует
|
||
// проверка 41. Отсюда и вид: помета — строка-примечание под заголовком, и ищется она
|
||
// в ближайших ТРЁХ строках. Слово «long=yes», написанное в тексте пункта, пометкой
|
||
// не считается: иначе датчик топтания выключался бы изнутри, теми же руками, за которыми
|
||
// он следит.
|
||
//
|
||
// 🔴 `tests=` — то, чем меряется закрытие пункта (проверки 38 и 39). Пусто здесь — и пункт
|
||
// закрывается по одному слову работника. `files=` держит три замка куска 3: ворота
|
||
// «задачи не пересекаются по файлам», разрез по файлам внутри задачи и список чужих файлов
|
||
// в поручении работнику.
|
||
//
|
||
// 🪤 Ни одного «наверное»: подали не текст или не список — падаем ГРОМКО. Пустышка, тихо
|
||
// отвечающая «всё хорошо», за три куска ловилась пять раз, и здесь она стоила бы дороже
|
||
// всего: ворота прочли бы пустой разбор как «в плане нет пунктов» и написали бы владельцу
|
||
// неправду о его целом плане.
|
||
|
||
// 🔴🔴 СПИСОК МЕРНЫХ ПРИБОРОВ ОДИН НА ВСЮ ЗАТЕЮ и живёт в `claims-audit.mjs`. Ввозится,
|
||
// а не переписывается: два списка разъезжаются молча — ворота пускали бы то, что сверка
|
||
// тронутых файлов потом откатывает, и ночь сгорала бы ровно так, как описано у самих ворот.
|
||
// 🔴 Кольца ЗДЕСЬ нет: `claims-audit.mjs` ввозит `file-claims.mjs` и `paths.mjs`, и ни один
|
||
// из троих не ввозит этот файл. Кольцевой ввоз есть у замка занятий — там он назван вслух
|
||
// в его же шапке и сторожится живой проверкой.
|
||
// 🪤 Долг, названный вслух: раньше `plan-model.mjs` не ввозил из затеи НИЧЕГО, и на это
|
||
// прямо ссылается примечание к ввозу в `item-audit.mjs` («кольца тут нет: plan-model
|
||
// не ввозит из затеи ничего»). Теперь ввозит — кольца по-прежнему нет, но то примечание
|
||
// устарело. Чинить его отсюда нельзя: чужой файл.
|
||
import { mernyyPribor } from './claims-audit.mjs';
|
||
|
||
// Заголовок пункта. Ровно три решётки: «####» — это уже подзаголовок внутри пункта.
|
||
const HEAD = /^###\s+Task\s+([\w.]+)\s*:\s*(.+?)\s*$/;
|
||
// Помета. Только строка-примечание целиком — кусок такого вида посреди прозы не годится.
|
||
const MARK = /^<!--\s*night:\s*(.+?)\s*-->$/;
|
||
// Сколько строк под заголовком просматриваем. Число названо в договоре о стыках дословно.
|
||
const OKNO_STROK = 3;
|
||
// Рамка-образец. До трёх пробелов отступа, три и больше кавычек ИЛИ тильд, дальше подпись.
|
||
const RAMKA = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
||
|
||
// 🔴🔴 ВТОРОЙ СЛОВАРЬ СЛОВА «ЗАДАЧА» — тот, которым пишут ЛЮДИ.
|
||
//
|
||
// 🔴 Это НЕ пункт ночи и пунктом не станет. Разбор выше не тронут ни на знак: ночь
|
||
// по-прежнему берёт только «### Task N: Название» с пометой. Здесь считается ДРУГОЕ —
|
||
// сколько работы описано в бумаге по-человечески, — и нужно это ровно для одного:
|
||
// чтобы отказ ворот не соврал владельцу.
|
||
//
|
||
// 🔴 ЗАЧЕМ, ЗАМЕРЕНО 05.08.2026. Позови разбор на любой из семи кусков затеи — ответ
|
||
// «0 пунктов», и ворота говорят: «В плане нет ни одного пункта». Про бумагу, где описано
|
||
// шестнадцать задач, это звучит как «план пустой». Три смены подряд читали этот отказ
|
||
// и делали один и тот же вывод: «надо научить ночь русским заголовкам» либо «надо
|
||
// переписать заголовки в семи планах». Оба вывода неверны и оба дороги: куски 1–7 —
|
||
// это план стройки самого хозяйства, его исполняют дневные смены (решение владельца 26
|
||
// от 06.08: «это не поломка разметки — это недостающая работа»), работа по ним уже
|
||
// сделана, а отмечать сделанное в плане нечем — ночь помнит закрытые пункты только
|
||
// в состоянии своего прогона. Оживи чтение этих планов — и ночь пойдёт делать заново
|
||
// то, что готово. Владелец про это сказал дословно: «жечь впустую недопустимо».
|
||
//
|
||
// 🪤 Примета — строго разметка, и границы названы вслух: две или три решётки (одна —
|
||
// это заголовок всей бумаги, четыре — подзаголовок внутри задачи), слово с БОЛЬШОЙ буквы
|
||
// и в единственном числе, номер, и сразу за ним двоеточие либо тире. «## Задача 1:»
|
||
// куска 4 и «### Задача 1а —» куска 7 — да; «## Задачи», «## задача», строка в прозе — нет.
|
||
const RUSSKAYA_ZADACHA = /^#{2,3}\s+Задача\s+\d+[а-яё]?\s*[—:-]/u;
|
||
|
||
// Сколько задач описано в бумаге по-человечески. Рамки-образцы пропускаются той же
|
||
// разметкой, что и у пунктов: план показывает, КАК писать задачу, куском внутри рамки —
|
||
// посчитай эти картинки, и здоровый план объявится полумеченым на ровном месте.
|
||
// 🪤 Текст не подан или разметка рамок не читается — отвечаем 0, то есть «разговора
|
||
// про словари не будет». Это не тихая подмена: отказ в таком случае остаётся прежним,
|
||
// полным и честным, просто без объяснения про дневную бумагу.
|
||
// 🔴 ВЫВОЗИТСЯ НАРУЧУ НАРОЧНО (правило 249, смена 31). Это же дело делал сторож папки
|
||
// планов (`plany-ne-uhodyat-v-noch-pustymi.test.mjs`) — своей, ДРУГОЙ приметой, и они
|
||
// разошлись молча: на живом куске 2 сторож насчитывал 12 задач, ворота — 11 (двенадцатая
|
||
// лежала картинкой внутри рамки ```powershell). Теперь тело одно и разъехаться нечем.
|
||
export function skolkoRusskihZadach(text) {
|
||
if (typeof text !== 'string') return 0;
|
||
const lines = text.split(/\r?\n/);
|
||
let vRamke;
|
||
try {
|
||
vRamke = razmetkaRamok(lines);
|
||
} catch {
|
||
return 0;
|
||
}
|
||
let n = 0;
|
||
for (let i = 0; i < lines.length; i += 1) {
|
||
if (!vRamke[i] && RUSSKAYA_ZADACHA.test(lines[i])) n += 1;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
function spisok(value) {
|
||
return value.split(',').map(s => s.trim()).filter(Boolean);
|
||
}
|
||
|
||
// 🔴🔴 Где в плане КАРТИНКА пункта, а где сам пункт.
|
||
//
|
||
// Планы показывают, как выглядит пункт, — куском-образцом внутри рамки ``` … ```.
|
||
// Разбор брал эти картинки за работу: в живом плане `2026-08-01-kusok4-...md`
|
||
// он видел СЕМНАДЦАТЬ пунктов при нуле настоящих, в `...kusok3-...md` — четыре.
|
||
// Ворота при этом отвечали «план годен», а помощник соседней задачи вправду получил
|
||
// куски примеров как задание и чуть не записал ложный вывод о самих планах.
|
||
//
|
||
// 🔴 Примета — РАЗМЕТКА, а не слово: никакого «рамка», «пример», «образец» в тексте
|
||
// искать нельзя, этих слов в живых планах нет. Считаем ровно так, как размётку читает
|
||
// сам markdown, и два места читаются по-разному:
|
||
//
|
||
// 🪤 подпись рамки на кавычках не может содержать кавычку. Строка
|
||
// «```` ```verified-context-json ````» — это вставка кода В СТРОКУ, а не начало рамки.
|
||
// Простая примета «начинается с трёх кавычек» открывает на ней рамку, рамка не
|
||
// закрывается никогда — и ТРИ настоящих пункта ниже пропадают МОЛЧА. Не выдумка:
|
||
// эта строка лежит в `docs/superpowers/plans/2026-06-12-router-mentor-activation-wrapper.md`.
|
||
//
|
||
// 🪤 закрывает рамку только та же кавычка (или тильда) и не короче открывающей, и после
|
||
// неё на строке ничего нет. Иначе рамка на четырёх кавычках закрывалась бы тройкой
|
||
// изнутри себя же, и половина образца читалась бы как настоящие пункты.
|
||
//
|
||
// 🔴 Рамка, которую забыли закрыть, глотает всё до конца плана. Съела заголовки — падаем
|
||
// ГРОМКО: съесть план ЦЕЛИКОМ не страшно (ворота скажут «нет пунктов»), а вот съесть
|
||
// ПОЛОВИНУ — это ночь по урезанному плану, о которой владелец не узнает никогда.
|
||
// Не съела ничего — молчим: так кончаются семь живых планов, и ронять их не за что.
|
||
function razmetkaRamok(lines) {
|
||
const vRamke = new Array(lines.length).fill(false);
|
||
let ramka = null;
|
||
let sedeno = 0;
|
||
for (let i = 0; i < lines.length; i += 1) {
|
||
const m = RAMKA.exec(lines[i]);
|
||
if (m) {
|
||
const znak = m[1][0];
|
||
const dlina = m[1].length;
|
||
const podpis = m[2];
|
||
if (ramka) {
|
||
if (znak === ramka.znak && dlina >= ramka.dlina && podpis.trim() === '') {
|
||
ramka = null;
|
||
sedeno = 0;
|
||
} else {
|
||
vRamke[i] = true;
|
||
}
|
||
continue;
|
||
}
|
||
if (znak === '`' && podpis.includes('`')) continue; // вставка в строку, а не рамка
|
||
ramka = { znak, dlina, stroka: i + 1 };
|
||
sedeno = 0;
|
||
vRamke[i] = true;
|
||
continue;
|
||
}
|
||
if (!ramka) continue;
|
||
vRamke[i] = true;
|
||
if (HEAD.test(lines[i])) sedeno += 1;
|
||
}
|
||
if (ramka && sedeno > 0) {
|
||
throw new Error(`parsePlan: рамка-образец открыта на строке ${ramka.stroka} и не закрыта до конца плана. Внутри неё осталось пунктов: ${sedeno} — и молча взять их за примеры нельзя: с тем же успехом это настоящая работа, которую ночь просто не увидит. Закройте рамку («${ramka.znak.repeat(ramka.dlina)}» отдельной строкой) и повторите.`);
|
||
}
|
||
return vRamke;
|
||
}
|
||
|
||
export function parsePlan(text) {
|
||
if (typeof text !== 'string') {
|
||
throw new Error('parsePlan: подан не текст плана. Разбирать нечего, и молчать об этом нельзя: пустой список пунктов ворота прочли бы как «в плане нет ни одного пункта» и написали бы владельцу неправду о его плане.');
|
||
}
|
||
const lines = text.split(/\r?\n/);
|
||
const vRamke = razmetkaRamok(lines);
|
||
const items = [];
|
||
for (let i = 0; i < lines.length; i += 1) {
|
||
if (vRamke[i]) continue; // это картинка пункта в образце, а не пункт
|
||
const h = HEAD.exec(lines[i]);
|
||
if (!h) continue;
|
||
// Умолчания честные и пустые: не написано — значит пусто, а не «наверное всё».
|
||
const item = { id: h[1], title: h[2], tests: [], files: [], long: false }; const doStroki = Math.min(i + 1 + OKNO_STROK, lines.length);
|
||
for (let j = i + 1; j < doStroki; j += 1) {
|
||
// 🔴 Окно обрывается на заголовке СЛЕДУЮЩЕГО пункта. Без этого пункт без пометы
|
||
// дотягивается до пометы соседа и забирает ЕГО проверки: проверка 39 обходится
|
||
// молча, а закрытие пункта меряется чужой зеленью. Ровно от этой беды и заведено
|
||
// окно в три строки — но одного числа мало, когда пункты идут вплотную.
|
||
// 🪤 Помета ИЗ ОБРАЗЦА настоящему пункту не достаётся: мало не считать заголовок
|
||
// из рамки пунктом — отдай ему чужие `tests`, и закрытие пункта померяется
|
||
// зеленью, которой он не касался (проверка 38). Строки рамки просто пропускаем,
|
||
// окно на них не обрывая: настоящая помета может стоять и ниже образца.
|
||
if (vRamke[j]) continue;
|
||
if (HEAD.test(lines[j])) break;
|
||
const m = MARK.exec(lines[j].trim());
|
||
if (!m) continue;
|
||
for (const part of m[1].split(';')) {
|
||
const [rawKey, ...rest] = part.split('=');
|
||
const key = rawKey.trim();
|
||
// Склеиваем обратно: в значении может стоять свой знак «=».
|
||
const value = rest.join('=').trim();
|
||
if (key === 'tests') item.tests = spisok(value);
|
||
if (key === 'files') item.files = spisok(value);
|
||
if (key === 'long') item.long = value.toLowerCase() === 'yes';
|
||
}
|
||
break; // первая помета под заголовком — она и есть помета этого пункта
|
||
}
|
||
items.push(item);
|
||
}
|
||
return items;
|
||
}
|
||
|
||
// 🔴 ВТОРОЙ ДОВОД — САМ ТЕКСТ ПЛАНА — необязателен НАРОЧНО.
|
||
// Подан — ворота вдобавок сверяют два словаря слова «задача» (см. `RUSSKAYA_ZADACHA`)
|
||
// и ловят полумеченый план. Не подан — ответ ровно тот же, что был до 05.08.2026,
|
||
// и ни один зовущий не сломан: `validatePlan(parsePlan(text))` работает как работал.
|
||
// 🪤 Умолчание здесь — НЕ «наверное всё хорошо»: без текста просто нет сведений, чтобы
|
||
// судить о втором словаре, и ворота об этом молчат, а не выдают догадку за проверку.
|
||
export function validatePlan(items, text = null) {
|
||
if (!Array.isArray(items)) {
|
||
throw new Error('validatePlan: подан не список пунктов. Это сбой зова, а не свойство плана: промахнись зовущий мимо разбора — и «ok» пустило бы в работу план, которого никто не смотрел.');
|
||
}
|
||
const items_count = items.length;
|
||
const long_count = items.filter(i => i?.long === true).length;
|
||
const russkih = skolkoRusskihZadach(text);
|
||
|
||
if (items_count === 0) {
|
||
// 🔴 Отказ разный не по вкусу, а по тому, ЧТО владельцу делать дальше. Бумага без
|
||
// единой задачи — её надо написать. Бумага с задачами по-русски — её писать не надо,
|
||
// её надо НЕ ТРОГАТЬ: это дневной план, и переписывать его заголовки под ночь значит
|
||
// послать ночь переделывать готовое.
|
||
if (russkih > 0) {
|
||
return {
|
||
ok: false,
|
||
reason: `Ночных пунктов в этом плане нет, но задач, описанных по-русски («## Задача 1: …»), в нём ${russkih}. Значит это дневная бумага, а не задание ночи: по таким планам работа уже идёт руками, и отметок «сделано» в них нет — ночь про сделанное не узнает и пойдёт делать заново. 🔴 Переписывать эти заголовки под ночной вид НЕЛЬЗЯ. Ночное задание пишется отдельной бумагой и оно маленькое: заголовок «### Task 1: Название» и строкой ниже помета «<!-- night: tests=…; files=… -->» — чем пункт меряется и какие файлы правит.`,
|
||
items_count,
|
||
long_count,
|
||
};
|
||
}
|
||
return { ok: false, reason: 'В плане нет ни одного пункта — запускать нечего. Пункт плана — это заголовок вида «### Task 1: Название».', items_count, long_count };
|
||
}
|
||
|
||
// 🔴🔴 Номер пункта — это КЛЮЧ, а не подпись. По нему ведут очередь, по нему запирают
|
||
// файлы, им работник заявляет «закрыл». Два пункта под одним номером — двое работников
|
||
// закроют друг друга, и оба отчитаются ПРАВДОЙ: каждый закрыл «Task 13».
|
||
//
|
||
// 🔴 Стоит РАНЬШЕ всех остальных отказов намеренно. Ниже виновных зовут по имени
|
||
// («Task 1»), и пока ключ не уникален, любое такое имя двусмысленно: владелец не
|
||
// поймёт, который из двух чинить. Сперва уникальность ключа — потом всё, что на нём
|
||
// держится.
|
||
const schyot = new Map();
|
||
for (const i of items) {
|
||
const id = String(i?.id ?? '?');
|
||
schyot.set(id, (schyot.get(id) ?? 0) + 1);
|
||
}
|
||
const povtory = [...schyot].filter(([, n]) => n > 1);
|
||
if (povtory.length) {
|
||
const imena = povtory.map(([id, n]) => `Task ${id} (${n} раза)`).join(', ');
|
||
return {
|
||
ok: false,
|
||
reason: `В плане повторяются номера пунктов: ${imena}. Номер — ключ, по которому ведут очередь, запирают файлы и заявляют «пункт закрыт»; двое работников под одним номером закроют друг друга, и оба отчитаются правдой. Перенумеруйте так, чтобы каждый номер встречался ровно один раз.`,
|
||
items_count,
|
||
long_count,
|
||
};
|
||
}
|
||
|
||
// 🔴🔴 ПЛАН, РАЗМЕЧЕННЫЙ ДЛЯ НОЧИ НАПОЛОВИНУ. Самая дорогая беда этой семьи.
|
||
//
|
||
// 🔴 ЧЕМ ОНА ХУЖЕ ОТКАЗА. Пометь автор ночными пунктами три задачи из девяти — ворота
|
||
// до сего дня отвечали «план годен» (пункты есть, у каждого свои проверки), ночь честно
|
||
// делала три и гасла, а владелец читал утром «план отработан» и считал, что сделаны
|
||
// девять. Ни отказа, ни красноты, ни строки в сводке: беда выглядит РОВНО как успех.
|
||
// Тот же разрез, что у мерного прибора выше, — и потому она стоит рядом с ним, раньше
|
||
// отказа «пункт без проверок»: тот пункт владелец и так не запустит, а этот выглядит
|
||
// отработавшим.
|
||
//
|
||
// 🟡 ГРАНИЦА, НАЗВАННАЯ ВСЛУХ. Правило судит только тогда, когда ночные пункты ЕСТЬ.
|
||
// Ноль ночных пунктов при девяти задачах по-русски — это не полумеченый план, а дневная
|
||
// бумага, и отвечено про неё выше, своими словами. Правило это старше моего круга: оно
|
||
// жило в проверке соседа (`plany-ne-uhodyat-v-noch-pustymi.test.mjs`, `polumechen`)
|
||
// и ходило по папке планов во время прогона проверок — то есть на ЖИВОМ запуске
|
||
// не стояло вовсе. Здесь оно встаёт на дорогу самого запуска.
|
||
if (russkih > items_count) {
|
||
return {
|
||
ok: false,
|
||
reason: `План размечен для ночи НАПОЛОВИНУ: задач, описанных по-русски, в нём ${russkih}, а ночных пунктов размечено ${items_count}. Ночь сделала бы ${items_count} и отчиталась успехом, а ${russkih - items_count} задач вы не досчитались бы молча — ни отказа, ни красноты в сводке. Разметьте пометой «<!-- night: tests=…; files=… -->» либо все задачи, либо вынесите в ночное задание только те, что вправду отдаёте ночи, — отдельной бумагой.`,
|
||
items_count,
|
||
long_count,
|
||
};
|
||
}
|
||
|
||
// 🔴🔴 МЕРНЫЙ ПРИБОР В `files=` ПУНКТА — ПЛАНА НЕТ.
|
||
//
|
||
// 🔴 ЗАЧЕМ ЭТО ЗДЕСЬ, А НЕ ТОЛЬКО В ЗАМКЕ. Беда ловится в трёх местах, и это самое
|
||
// дешёвое из них: владелец правит план утром за минуту. Замерено 02.08.2026, вот путь
|
||
// без этих ворот: план с `package.json` в `files=` признаётся годным (про приборы ворота
|
||
// не знали) → работник честно занимает файл (занятия отвечают «можно»: у прибора нет
|
||
// хозяина) → работник правит и коммитит → сверка тронутых файлов видит прибор, ОТКАТЫВАЕТ
|
||
// правку и ОТБИРАЕТ пункт → работник берёт тот же пункт снова, и так каждые полминуты
|
||
// до утра. Владелец читает «работал всю ночь» и видит ноль сделанного. Это ХУЖЕ
|
||
// отказа: отказ виден сразу, а такая ночь сгорает МОЛЧА и выглядит как работа.
|
||
//
|
||
// 🔴 СТОИТ РАНЬШЕ отказа «пункт без проверок» намеренно: тот пункт владелец и так
|
||
// не запустит, а этот — единственный, который выглядит запущенным и работающим.
|
||
//
|
||
// 🟡 ГРАНИЦА, названная вслух: ворота видят только `files=`. Напиши автор плана «правь
|
||
// package.json» СЛОВАМИ в тексте пункта, а в `files=` поставь другой файл — ворота
|
||
// не увидят ничего, и беду поймает только сверка тронутых файлов, то есть уже ночью.
|
||
// Текст пункта здесь не разбирается вовсе, и заводить его разбор ради этого дороже,
|
||
// чем беда: `files=` — это то, чем пункт запирается, и правит работник ровно его.
|
||
const spribolom = [];
|
||
for (const i of items) {
|
||
for (const f of Array.isArray(i?.files) ? i.files : []) {
|
||
const pribor = mernyyPribor(f);
|
||
if (pribor) spribolom.push(`Task ${i?.id ?? '?'} → ${f} (${pribor.chem})`);
|
||
}
|
||
}
|
||
if (spribolom.length) {
|
||
return {
|
||
ok: false,
|
||
reason: `В плане есть пункты, которые правят мерный прибор — файл, которым хозяйство меряет само себя: ${spribolom.join('; ')}. Такой пункт не «упадёт», он сгорит молча: работник займёт файл, будет править его всю ночь, а сверка каждый раз откатит правку и отберёт пункт — утром в сводке будет «работал всю ночь» и ноль сделанного. Уберите эти файлы из списка файлов пункта (помета «files=»); если работа вправду в них — такую ночь ведут при вас, а не без вас.`,
|
||
items_count,
|
||
long_count,
|
||
};
|
||
}
|
||
|
||
// 🔴🔴 ПУНКТ, ПРОВЕРКИ КОТОРОГО РАЗНОГО РОДА. Довод и живой замер — в шапке
|
||
// `razdvoenieProverok`; здесь только отказ словами владельца.
|
||
//
|
||
// 🔴 ЗАЧЕМ ЭТО ЗДЕСЬ, А НЕ ТОЛЬКО У ЗАМКА — довод тот же, что и у мерного прибора выше:
|
||
// это самое дешёвое из мест. Владелец правит план утром за минуту, и правка простая —
|
||
// разбить пункт надвое. Оставь мы одну защиту у замка — план прошёл бы ворота, работник
|
||
// взял бы пункт, сделал и закрыл, а замок каждый раз отвечал бы «замерить нечем»: пункт
|
||
// не засчитан, работник берёт его снова, и так до утра. Ночь сгорает молча и выглядит
|
||
// как работа — та самая беда, ради которой заведены ворота на приборы.
|
||
//
|
||
// 🔴 СТОИТ РАНЬШЕ отказа «пункт без проверок» намеренно. Обе беды из одной семьи —
|
||
// «пункт нечем померить», — но пункт без проверок видно глазами, а этот выглядит
|
||
// безупречно: проверки названы, имена настоящие, и до сего дня он вправду зеленел.
|
||
// Первым называем менее очевидного.
|
||
const smeshannye = [];
|
||
for (const i of items) {
|
||
const r = razdvoenieProverok(i);
|
||
if (r) smeshannye.push(`Task ${i?.id ?? '?'} (продукт: ${r.php.join(', ')}; затея: ${r.nashi.join(', ')})`);
|
||
}
|
||
if (smeshannye.length) {
|
||
return {
|
||
ok: false,
|
||
reason: `В плане есть пункты, проверки которых разного рода, а одной командой два рода не гоняются: ${smeshannye.join('; ')}. Проверки продукта («.php») запускает одна программа, проверки самой затеи — другая, и чужую каждая из них молча пропускает: замерено — из двух названных проверок отрабатывает одна, ответ выходит без единой поломки, и про вторую не говорится ни слова. Такой пункт засчитывался бы наполовину и молча. Разбейте его на два пункта: в один соберите проверки продукта, в другой — проверки затеи.`,
|
||
items_count,
|
||
long_count,
|
||
};
|
||
}
|
||
|
||
// Проверка 39б: пункт, для которого не записано ни одной проверки, закрыть нельзя,
|
||
// значит и запускать по такому плану нечего. Виновные названы ВСЕ разом: владелец
|
||
// чинит план по этому списку, и получать отказ по одному пункту за ночь ему незачем.
|
||
const golye = items.filter(i => !Array.isArray(i?.tests) || i.tests.length === 0);
|
||
if (golye.length) {
|
||
const imena = golye.map(i => `Task ${i?.id ?? '?'}`).join(', ');
|
||
return {
|
||
ok: false,
|
||
reason: `В плане есть пункты, для которых не записано ни одной проверки: ${imena}. Закрытие пункта меряется зелёными проверками именно этого пункта, а их не названо — закрыть такой пункт нечем. План не годится.`,
|
||
items_count,
|
||
long_count,
|
||
};
|
||
}
|
||
|
||
// Проверка 41: длинными помечено больше половины — плана к запуску нет.
|
||
if (long_count * 2 > items_count) {
|
||
return {
|
||
ok: false,
|
||
reason: `Длинными помечено ${long_count} пунктов из ${items_count} — больше половины. Так выключается единственный датчик топтания, и запуска по такому плану нет. Пометку ставит тот, кто пишет план: перечитайте, вправду ли все эти пункты длинные.`,
|
||
items_count,
|
||
long_count,
|
||
};
|
||
}
|
||
|
||
return { ok: true, reason: null, items_count, long_count };
|
||
}
|
||
|
||
// 🔴🔴 РОД ПРОВЕРКИ — ЧЕМ ЕЁ ВПРАВДУ ГОНЯЮТ. Родов ровно два, и это не деление ради
|
||
// деления: php-шную проверку запускает прогон продукта (`php artisan test`), нашу —
|
||
// прогон затеи (`npm run test:tools`), и ЧУЖУЮ каждый из них не запускает вовсе.
|
||
// Всё, что не кончается на `.php`, считается нашим: наш прогон принимает имя как строку
|
||
// отбора, и «наше» тут — это «то, что ищется среди наших файлов проверок».
|
||
// 🪤 Регистр не важен намеренно: строгое сравнение отправило бы «A.PHP» в наш прогон,
|
||
// то есть ровно в ту беду, от которой всё это и заведено.
|
||
export function rodProverki(t) {
|
||
return /\.php$/i.test(String(t)) ? 'php' : 'nash';
|
||
}
|
||
|
||
// 🔴🔴 ПРОВЕРКИ ПУНКТА РАЗНОГО РОДА — ОДНОЙ КОМАНДОЙ ИХ НЕ ПОМЕРИТЬ.
|
||
//
|
||
// 🔴 ЗАМЕРЕНО ЖИВЬЁМ 02.08.2026, а не выведено рассуждением. Прежнее правило смешанный
|
||
// набор отдавало нашему прогону целиком, с доводом «`php artisan test` наш `.mjs` не
|
||
// запустит вовсе». Довод верен ровно наполовину — и этой половиной опасен. Выполнено руками:
|
||
// npm run test:tools -- app/tests/Feature/ProjectSyncStatusFromPivotTest.php \
|
||
// tools/night/plan-fingerprint.test.mjs
|
||
// Ответ: «Test Files 1 passed (1) · Tests 15 passed (15)», код 0. Из ДВУХ названных
|
||
// проверок отработала ОДНА. Имя php-шного файла наш прогон берёт за строку отбора,
|
||
// ни одного файла по ней не находит и молча идёт дальше — потому что второе имя что-то нашло.
|
||
//
|
||
// 🔴 ПОЧЕМУ ЭТО ХУЖЕ ПАДЕНИЯ. Прогон вышел с нулём И что-то напечатал — значит и заслон
|
||
// «программа ничего не сказала» (`item-audit.mjs`) его не ловит. Замок объявляет проверки
|
||
// зелёными и засчитывает пункт, у которого половина проверок не запускалась. Владелец
|
||
// наутро читает «проверки прогнаны надзирателем и зелены» — про бэкенд, которого не мерил
|
||
// никто. 🪤 Тихо это ТОЛЬКО у смешанного: набор из одних php-шных имён, поданный тому же
|
||
// прогону, отвечает кодом 1 («No test files found») — замерено отдельно.
|
||
//
|
||
// 🟡 ГРАНИЦА, НАЗВАННАЯ ВСЛУХ. Родов здесь два, и второй — «всё остальное». Напиши автор
|
||
// плана в `tests=` имя, которое не находит НИ ОДНОГО нашего файла (опечатка, `.py`, просто
|
||
// слово), — рядом с настоящей нашей проверкой оно так же молча пропадёт, и здесь это не
|
||
// ловится. Ловить это значит требовать, чтобы каждое имя вправду что-то находило, а такого
|
||
// прибора у нас нет: наш прогон отвечает одним кодом на весь набор. Долг записан, не закрыт.
|
||
//
|
||
// 🔴🔴 «ТРЕТЬЕ», КОТОРОЕ СЮДА ПРИХОДИТ. Пункт попадает к замку не только из разбора плана,
|
||
// но и с диска — из карточки работника. Порченый или правленный рукой JSON даёт `tests`
|
||
// строкой, а не списком, и первый вид этой защиты падал на такой строке `TypeError`-ом
|
||
// ВЫШЕ того места, где замок ловит сбои прогона: вместо «пункт не засчитан» умирал бы
|
||
// надзиратель, и не на один пункт, а на весь остаток ночи. Замерено 02.08.2026.
|
||
// Ответ здесь — «не мой вопрос»: родов не видно, судить нечего, молчим. Негодный `tests`
|
||
// ловят двое других — ворота плана (там `Array.isArray` прописан прямо) и сам замок,
|
||
// у которого этот сбой попадает в разбор «замерить нечем».
|
||
export function razdvoenieProverok(item) {
|
||
if (!Array.isArray(item?.tests)) return null;
|
||
const tests = item.tests.filter(Boolean).map(String);
|
||
const php = tests.filter(t => rodProverki(t) === 'php');
|
||
const nashi = tests.filter(t => rodProverki(t) !== 'php');
|
||
if (!php.length || !nashi.length) return null;
|
||
return { php, nashi };
|
||
}
|
||
|
||
// Чем гонять проверки ИМЕННО ЭТОГО пункта.
|
||
//
|
||
// 🔴🔴 Строится ИЗ его `tests`, и это сердце проверки 38. Возьми правило общий набор —
|
||
// «зелень по этому пункту» стала бы «зеленью вообще», и пункт засчитывался бы за чужую
|
||
// работу при полностью зелёных проверках. Ломается ТИХО.
|
||
//
|
||
// 🔴 Правило лежит здесь, а не рядом с командной строкой, потому что нужно оно ДВОИМ:
|
||
// `cli.mjs` — чтобы сказать работнику, чем меряется его первый пункт (задача 6), и
|
||
// надзирателю — чтобы гонять проверки КАЖДОГО судимого пункта (задача 5). Второй дороги
|
||
// к этому правилу не заводить.
|
||
//
|
||
// 🪤 Здесь мы только СТРОИМ строку. Программу по полному пути ищет замок
|
||
// (`progonProverokPunkta`, задача 4) — голым именем отсюда ничего не зовётся.
|
||
export function komandaProverokPunkta(item) {
|
||
const tests = (item?.tests ?? []).filter(Boolean).map(String);
|
||
if (!tests.length) return null;
|
||
// 🔴 Разного рода — команды НЕТ. Довод — в шапке `razdvoenieProverok`. Пустой ответ здесь
|
||
// читается замком как «замерить нечем» и пункт НЕ засчитывается: это единственный честный
|
||
// ответ, пока команда одна, а родов два.
|
||
if (razdvoenieProverok(item)) return null;
|
||
const phpshnye = tests.filter(t => rodProverki(t) === 'php');
|
||
// Все php-шные — гоним прогоном продукта по именам.
|
||
if (phpshnye.length === tests.length) {
|
||
const imena = phpshnye.map(t => t.split(/[\\/]/).pop().replace(/\.php$/i, ''));
|
||
return `php artisan test --filter=${imena.join('|')}`;
|
||
}
|
||
return `npm run test:tools -- ${tests.join(' ')}`;
|
||
}
|