397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
3.8 KiB
JavaScript
69 lines
3.8 KiB
JavaScript
#!/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; }
|
||
}
|