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

69 lines
3.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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; }
}