Files
brain/tools/router-learning-queue.mjs
T

69 lines
3.8 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* router-learning-queue — очередь обучения роутера (3.4 примеры / 3.5 долгая память).
* НЕСУЩИЙ HARD-RULE (решение владельца 2026-06-03): наполнение фонда ТОЛЬКО по явному
* «да» владельца, ИНАЧЕ НИКАК. Система только ПРЕДЛАГАЕТ (enqueue → pending); в фонд
* переводит ТОЛЬКО applyApprovalBatch с явным id в approve. Без авто-обучения.
*/
import fsDefault from 'node:fs';
/** Запись очереди — ВСЕГДА pending (нет авто-одобрения). */
export function makeQueueEntry(candidate) {
return { id: candidate.id, kind: candidate.kind, summary: candidate.summary || '', why_proposed: candidate.why_proposed || '', status: 'pending' };
}
/** Добавить предложение (pending). Никогда не одобряет. */
export function enqueue(queue, candidate) {
return [...(queue || []), makeQueueEntry(candidate)];
}
/** Сколько ждёт одобрения. */
export function pendingCount(queue) {
return (queue || []).filter((e) => e.status === 'pending').length;
}
/**
* Одобрение пачкой — ЕДИНСТВЕННЫЙ путь в фонд. approve → 'approved' + в fund;
* reject → 'rejected' (помечен, не предлагать повторно); defer → остаётся 'pending'.
* Без явного решения id остаётся как был. Возвращает {queue, fund, rejected}.
*
* Приоритет reject > approve: конфликт «и да, и нет» по одному id — двусмысленный
* сигнал → по hard-rule «без явного да НИКАК» в фонд НЕ пускаем (безопасная сторона).
*/
export function applyApprovalBatch(queue, { approve = [], reject = [], defer = [] } = {}) {
const aSet = new Set(approve), rSet = new Set(reject), dSet = new Set(defer);
const fund = [], rejected = [];
const newQueue = (queue || []).map((e) => {
// G (аудит M1-M4): только pending-запись переходит. Повторное approve/reject уже
// решённого id (или reuse id) НЕ перепроводит её снова → нет дублей в фонде.
if (e.status !== 'pending') return e;
if (rSet.has(e.id)) { const ne = { ...e, status: 'rejected' }; rejected.push(ne); return ne; }
if (aSet.has(e.id)) { const ne = { ...e, status: 'approved' }; fund.push(ne); return ne; }
if (dSet.has(e.id)) return { ...e, status: 'pending' };
return e;
});
return { queue: newQueue, fund, rejected };
}
/** Рендер раздела «На одобрение» для /brain-retro (только pending). */
export function renderApprovalSection(queue) {
const pend = (queue || []).filter((e) => e.status === 'pending');
if (pend.length === 0) return '## На одобрение\n\n_нет кандидатов_';
const lines = pend.map((e) => `- [${e.id}] (${e.kind}) ${e.summary} — почему: ${e.why_proposed}`);
return `## На одобрение (${pend.length})\n\n${lines.join('\n')}\n\nОтвет: «одобряю 1,3 / отклоняю 2 / отложить 4».`;
}
/** Сигнал для STATUS.md: «ждут одобрения: N». */
export function statusSignal(queue) {
return `ждут одобрения: ${pendingCount(queue)}`;
}
/** Персист очереди (fs инъектируется). */
export function saveQueue({ queue, path, fsImpl = fsDefault }) {
fsImpl.writeFileSync(path, JSON.stringify(queue));
}
export function loadQueue({ path, fsImpl = fsDefault }) {
try { return JSON.parse(fsImpl.readFileSync(path, 'utf8')); }
catch (e) { if (e && e.code === 'ENOENT') return []; throw e; }
}