128 lines
9.1 KiB
JavaScript
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(' ')}`;
|
|
}
|