Files
portal/tools/night/plan-model.mjs
T

128 lines
9.1 KiB
JavaScript

// Разбор плана задачи. Пункт — заголовок «### Task N: Название».
//
// 🔴 Помету ставит ТОТ, КТО ПИШЕТ ПЛАН, а не работник на ходу — так дословно требует
// проверка 41. Отсюда и вид: помета — строка-примечание под заголовком, и ищется она
// в ближайших ТРЁХ строках. Слово «long=yes», написанное в тексте пункта, пометкой
// не считается: иначе датчик топтания выключался бы изнутри, теми же руками, за которыми
// он следит.
//
// 🔴 `tests=` — то, чем меряется закрытие пункта (проверки 38 и 39). Пусто здесь — и пункт
// закрывается по одному слову работника. `files=` держит три замка куска 3: ворота
// «задачи не пересекаются по файлам», разрез по файлам внутри задачи и список чужих файлов
// в поручении работнику.
//
// 🪤 Ни одного «наверное»: подали не текст или не список — падаем ГРОМКО. Пустышка, тихо
// отвечающая «всё хорошо», за три куска ловилась пять раз, и здесь она стоила бы дороже
// всего: ворота прочли бы пустой разбор как «в плане нет пунктов» и написали бы владельцу
// неправду о его целом плане.
// Заголовок пункта. Ровно три решётки: «####» — это уже подзаголовок внутри пункта.
const HEAD = /^###\s+Task\s+([\w.]+)\s*:\s*(.+?)\s*$/;
// Помета. Только строка-примечание целиком — кусок такого вида посреди прозы не годится.
const MARK = /^<!--\s*night:\s*(.+?)\s*-->$/;
// Сколько строк под заголовком просматриваем. Число названо в договоре о стыках дословно.
const OKNO_STROK = 3;
function spisok(value) {
return value.split(',').map(s => s.trim()).filter(Boolean);
}
export function parsePlan(text) {
if (typeof text !== 'string') {
throw new Error('parsePlan: подан не текст плана. Разбирать нечего, и молчать об этом нельзя: пустой список пунктов ворота прочли бы как «в плане нет ни одного пункта» и написали бы владельцу неправду о его плане.');
}
const lines = text.split(/\r?\n/);
const items = [];
for (let i = 0; i < lines.length; i += 1) {
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 обходится
// молча, а закрытие пункта меряется чужой зеленью. Ровно от этой беды и заведено
// окно в три строки — но одного числа мало, когда пункты идут вплотную.
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;
}
export function validatePlan(items) {
if (!Array.isArray(items)) {
throw new Error('validatePlan: подан не список пунктов. Это сбой зова, а не свойство плана: промахнись зовущий мимо разбора — и «ok» пустило бы в работу план, которого никто не смотрел.');
}
const items_count = items.length;
const long_count = items.filter(i => i?.long === true).length;
if (items_count === 0) {
return { ok: false, reason: 'В плане нет ни одного пункта — запускать нечего. Пункт плана — это заголовок вида «### Task 1: Название».', 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 };
}
// Чем гонять проверки ИМЕННО ЭТОГО пункта.
//
// 🔴🔴 Строится ИЗ его `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;
const phpshnye = tests.filter(t => /\.php$/i.test(t));
// Только php-шные — гоним прогоном продукта по именам. Смешанный набор идёт нашим
// прогоном: `php artisan test` наш `.mjs` не запустит вовсе.
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(' ')}`;
}