121 lines
10 KiB
JavaScript
121 lines
10 KiB
JavaScript
// След отложенной задачи. Задача не влезла в прогон или не прошла приёмку владельца —
|
|
// она откладывается, и НАЗАВТРА САМА НЕ ПОДХВАТЫВАЕТСЯ (проверки 28 и 29). След — это
|
|
// единственное, чем следующая ночь узнаёт, что такая задача была.
|
|
//
|
|
// 🔴 Где лежит: `dirPath` — каталог следов, файл — `<task_id>.json`. Не под `runs/`:
|
|
// след обязан пережить прогон, которым он рождён, и дожить до следующей ночи. Не
|
|
// в хранилище: корень затеи закрыт от хранилища, и это состояние, а не бумага.
|
|
// Имени в договоре о стыках нет — заведено здесь и названо вслух в каркасе куска 4.
|
|
//
|
|
// 🪤 Здесь НЕЛЬЗЯ пользоваться `readJson` из `state.mjs`: на порванном файле он отдаёт
|
|
// `null`, а «null» тут читается как «следа нет» — то есть ворота пустили бы отложенную
|
|
// задачу в работу САМИ, ровно в ту ночь, когда запись порвалась. Поэтому у чтения два
|
|
// разных ответа: файла нет — следа нет (норма); файл есть и не читается — падаем громко.
|
|
import { existsSync, readFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { NIGHT_ROOT } from './paths.mjs';
|
|
import { writeJsonAtomic } from './state.mjs';
|
|
|
|
// Закрытый список. Незнакомое слово ворота прочли бы как «не rejected», то есть
|
|
// не спросили бы списка брака, и задача вернулась бы в работу без него.
|
|
//
|
|
// 🔴 Третье слово — долг куска 5 (Р14) и куска 7 (блок 6а), внесён 01.08. Пункт, который
|
|
// владелец ответом «нет» объявил невыполнимым, не rejected (приёмки не было) и не
|
|
// not_finished (дело не в нехватке ночи) — без своего слова он молча подпадал бы под одно
|
|
// из двух чужих, и причина отказа во владельческом слове звучала бы неправдой.
|
|
export const DEFER_REASONS = ['not_finished', 'rejected', 'impossible'];
|
|
|
|
// 🪤 Имя задачи становится именем файла. «../беда» увело бы запись из каталога следов
|
|
// вовсе, и след потерялся бы молча — то есть задача назавтра подхватилась бы сама.
|
|
//
|
|
// 🔴 Запрещаем ровно две вещи, и обе по делу: косую черту (любого вида) и имя, в котором
|
|
// нет ничего кроме точек. Две точки ПОСРЕДИ имени — не шаг вверх: черты уже запрещены,
|
|
// а к имени дописывается «.json», и «run-2026-08-01..02» остаётся файлом в том же
|
|
// каталоге. Руби мы и такие имена — владелец получал бы громкий отказ на своём же имени
|
|
// задачи, след не лёг бы вовсе, и отложенная задача назавтра подхватилась бы сама.
|
|
// Строгость шире нужной здесь не безобидна: она ломает ровно то, что стережёт.
|
|
function imyaFajla(task_id) {
|
|
const name = String(task_id ?? '').trim();
|
|
if (!name || /[\\/]/.test(name) || /^\.+$/.test(name)) {
|
|
throw new Error(`Негодное имя задачи для следа отложенной: ${JSON.stringify(task_id)}. Имя задачи становится именем файла, и косая черта или имя из одних точек увели бы след из каталога — он потерялся бы молча, а отложенная задача назавтра подхватилась бы сама.`);
|
|
}
|
|
return `${name}.json`;
|
|
}
|
|
|
|
function put(dirPath, task_id) {
|
|
return join(dirPath, imyaFajla(task_id));
|
|
}
|
|
|
|
export function markDeferred(dirPath, { task_id, plan_path, reason, left_items = [], at }) {
|
|
if (!DEFER_REASONS.includes(reason)) {
|
|
throw new Error(`Вид отложенной задачи должен быть одним из трёх: not_finished (не влезла в прогон), rejected (не прошла приёмку владельца) или impossible (пункт объявлен невыполнимым). Дано: ${JSON.stringify(reason)}. Незнакомое слово ворота возврата прочли бы как «не rejected» и не спросили бы списка брака.`);
|
|
}
|
|
const path = put(dirPath, task_id);
|
|
mkdirSync(dirPath, { recursive: true });
|
|
writeJsonAtomic(path, {
|
|
task_id: String(task_id),
|
|
plan_path: plan_path ?? null,
|
|
reason,
|
|
left_items: [...left_items],
|
|
// 🟡 Пусто нарочно: брак называет приёмка владельца, а её строит кусок 7. До него
|
|
// список подаётся с руки ключом `--defects`. Поле заведено сразу, чтобы кусок 7
|
|
// клал в него брак, не меняя вида файла.
|
|
defect_list: [],
|
|
at: at ?? Date.now(),
|
|
});
|
|
return path;
|
|
}
|
|
|
|
export function readDeferred(dirPath, task_id) {
|
|
const path = put(dirPath, task_id);
|
|
if (!existsSync(path)) return null; // следа нет — норма первой ночи любой задачи
|
|
let raw;
|
|
try {
|
|
raw = readFileSync(path, 'utf8');
|
|
} catch (e) {
|
|
throw new Error(`След отложенной задачи ${task_id} есть, но не читается: ${e.message}. Молчать нельзя: «следа нет» пустило бы отложенную задачу в работу само.`);
|
|
}
|
|
let zapis;
|
|
try {
|
|
zapis = JSON.parse(raw);
|
|
} catch (e) {
|
|
throw new Error(`След отложенной задачи ${task_id} порван и не разбирается: ${e.message}. Посмотрите файл ${path} глазами. Молчать нельзя: «следа нет» пустило бы отложенную задачу в работу само.`);
|
|
}
|
|
// 🔴🔴 НАЙДЕНО ВЫРЕЗАНИЕМ ВТОРОГО РОДА 02.08.2026 — планом не предусмотрено, дыра была
|
|
// настоящая. Защита выше стоит на том, что порванный след НЕ РАЗБИРАЕТСЯ. Но слово `null`
|
|
// разбирается прекрасно и отдаёт `null`, а `null` у этого чтения значит «следа нет» —
|
|
// то есть ворота решили бы, что задача не откладывалась, и пустили бы её в работу САМИ,
|
|
// без слова владельца. Проверки 28 и 29 отменились бы разом и молча.
|
|
// Гнаться за словом `null` бессмысленно: число, список и строка — тоже годный json
|
|
// и тоже не след; прежде они уходили дальше в ворота и падали там чужим словом.
|
|
// Поэтому спрашиваем прямо: это вообще ЗАПИСЬ? Список — не запись (`Array.isArray`).
|
|
if (zapis === null || typeof zapis !== 'object' || Array.isArray(zapis)) {
|
|
throw new Error(`След отложенной задачи ${task_id} разобрался, но это не запись, а ${Array.isArray(zapis) ? 'список' : JSON.stringify(zapis)}. Посмотрите файл ${path} глазами. Молчать нельзя: слово null тут прочлось бы как «следа нет» и пустило бы отложенную задачу в работу само.`);
|
|
}
|
|
return zapis;
|
|
}
|
|
|
|
export function clearDeferred(dirPath, task_id) {
|
|
const path = put(dirPath, task_id);
|
|
// Снимать нечего — молчим и файла на пустом месте не заводим.
|
|
rmSync(path, { force: true });
|
|
}
|
|
|
|
// 🔴🔴 ГДЕ лежат следы. Имя вывезено НАРУЖУ намеренно, и это шов с задачами 5 и 6: путь
|
|
// к следу обязан знать ровно ОДИН файл. Не вывези — надзиратель и командная строка написали
|
|
// бы каждый свой каталог следов, и разойдись они хоть на знак, след клался бы в одно
|
|
// место, а искался в другом: отложенная задача подхватилась бы назавтра САМА — ровно то,
|
|
// что запрещают проверки 28 и 29.
|
|
//
|
|
// 🔴 Корень затеи берётся ИЗ `paths.mjs` — это ПОСТОЯННАЯ `NIGHT_ROOT` (кусок 1),
|
|
// из неё же там сделан `commandsDir()`. Не функция и не из `state.mjs`: в `state.mjs`
|
|
// вывезены только `writeJsonAtomic`, `readJson`, `appendLine`, `readLines`, имени корня
|
|
// там нет вовсе. Второго корня не заводить ни в каком виде — ни своей строкой
|
|
// с этим именем, ни своим `resolve(...)`: разойдись они хоть на знак, след ляжет в одно
|
|
// место, а искаться будет в другом, и отложенная задача подхватится назавтра САМА.
|
|
// 🪤 Имя каталога строкой в этом файле не писать даже в примечании: на него смотрит
|
|
// проверка «корень НЕ заведён здесь заново», и она покраснела бы не по делу.
|
|
export function deferredDir() {
|
|
return join(NIGHT_ROOT, 'deferred');
|
|
}
|