Files
brain/tools/secretary-render-fluffy.mjs
T

119 lines
5.6 KiB
JavaScript

// tools/secretary-render-fluffy.mjs
// Детерминированный рендер пушистого дерева (без LLM): протокол → markdown по макету.
import { turnFileRef } from './secretary-layer1.mjs';
import { renderProtocol } from './secretary-protocol.mjs';
import { fluffyPipelineOn } from './secretary-flag.mjs';
// Сквозные ссылки на источник: [ход N](ходы/turn-N.log).
function srcLinks(turns) {
const ts = (Array.isArray(turns) ? turns : (turns != null ? [turns] : [])).filter((t) => t != null);
if (!ts.length) return '';
return ' — ' + ts.map((t) => `[ход ${t}](${turnFileRef(t)})`).join(' → ');
}
function branchSrc(h) {
const ts = [h.born];
if (h.lastTouch != null && h.lastTouch !== h.born) ts.push(h.lastTouch);
return srcLinks(ts.filter((t) => t != null));
}
const GLYPH = { открыт: '🌿', сужен: '✂️', мутировал: '🔁', закрыт: '✅' };
export function renderFluffy(protocol, opts = {}) {
const p = protocol || {};
const L = [];
L.push(`# 📋 Протокол: ${p.subject || '(без темы)'}`);
L.push(`*статус: ${p.status || 'открыто'}${opts.date ? ' · ' + opts.date : ''} · каждая строка тянется до ходы/turn-N.log*`, '');
// 🌳 СТВОЛ — живое наверху, зачёркнутое собираем в свёрнутый блок
L.push('## 🌳 Ствол', '');
const struckLines = [];
const trunkSec = (title, arr, { why = false, done = false } = {}) => {
const list = arr || [];
L.push(`**${title}**`);
const live = list.filter((e) => !e.struck);
if (!live.length) L.push('- (пусто)');
for (const e of live) {
const w = why && e.why ? `${e.why}` : '';
const box = done ? `[${e.done ? 'x' : ' '}] ` : '';
L.push(`- ${box}${e.text}${w}${srcLinks(e.turns)}`);
}
L.push('');
for (const e of list.filter((x) => x.struck)) {
const w = why && e.why ? `${e.why}` : '';
struckLines.push(`- ~~${e.text}~~${w}${srcLinks(e.turns)}`);
}
};
trunkSec('Решения', p.decisions, { why: true });
trunkSec('Воля владельца', p.will);
trunkSec('Открытые вопросы', p.open);
trunkSec('Последствия / цена', p.consequences);
trunkSec('Сделано / дальше', p.doneNext, { done: true });
if (struckLines.length) {
L.push('<details><summary>▸ решённое в стволе (свёрнуто)</summary>', '');
L.push(...struckLines);
L.push('', '</details>', '');
}
// 🌿 ЖИВЫЕ ВЕТКИ
const hidden = p.hidden || [];
const liveB = hidden.filter((h) => h.status !== 'закрыт');
const closedB = hidden.filter((h) => h.status === 'закрыт');
L.push('## 🌿 Живые ветки', '');
if (!liveB.length) L.push('(нет)', '');
else {
L.push('| Ветка | Линза | Состояние | Паспорт | Источник |', '|---|---|---|---|---|');
for (const h of liveB) {
const glyph = GLYPH[h.status] || '';
const pass = h.опора ? `${h.опора}${h.тяжесть ? ' · ' + h.тяжесть : ''}` : '—';
const src = branchSrc(h).replace(/^ — /, '');
L.push(`| ${h.text} | ${h.lens || ''} | ${glyph} ${h.status} | ${pass} | ${src} |`);
}
L.push('');
}
// 🔥 ГОРИТ
const acc = (p.acceptance || []).filter((e) => !e.done);
const tails = (p.tails || []).filter((e) => !e.done);
if (acc.length || tails.length) {
L.push('## 🔥 Горит', '');
for (const e of acc) L.push(`- **Приёмка (Л8):** ${e.text}${srcLinks(e.born)}`);
for (const e of tails) L.push(`- **Хвост (Л9):** ${e.text}${srcLinks(e.born)}`);
L.push('');
}
// 💡 КАНДИДАТЫ — релевантные наверху, low свёрнуты
const cands = p.candidates || [];
if (cands.length) {
const strong = cands.filter((c) => c.релевантность !== 'low');
const weak = cands.filter((c) => c.релевантность === 'low');
L.push('## 💡 Кандидаты (брейншторм — живут до разбора)', '');
for (const c of strong) L.push(`- ${c.branch} \`${c.опора || 'догадка'} · ${c.релевантность || '?'}\`${srcLinks(c.born)}`);
if (weak.length) {
L.push('', `<details><summary>▸ свёрнуто ${weak.length} слабых (low)</summary>`, '');
for (const c of weak) L.push(`- ${c.branch} — зацепка: ${c.trigger || '—'} \`${c.опора || 'догадка'}\`${srcLinks(c.born)}`);
L.push('', '</details>');
}
L.push('');
}
// ✅ РЕШЁННЫЕ ВЕТКИ (свёрнуто, с пруфом)
if (closedB.length) {
L.push(`<details><summary>▸ ✅ решённые ветки (${closedB.length})</summary>`, '');
for (const h of closedB) L.push(`- ~~${h.text}~~${h.proof ? ` · пруф: ${h.proof}` : ''}${branchSrc(h)}`);
L.push('', '</details>', '');
}
// 🧭 ШАГИ
L.push('## 🧭 Шаги', '');
const steps = (p.steps || []).slice().sort((a, b) => (a.turn || 0) - (b.turn || 0));
for (const s of steps) {
const link = s.turn != null ? `[Ход ${s.turn}](${turnFileRef(s.turn)})` : 'Ход';
L.push(`- ${link}${s.text}`);
}
return L.join('\n');
}
/** Развилка вида по флагу: ON → пушистый, OFF → старый renderProtocol. env инъектируем для теста. */
export function renderDoc(protocol, opts = {}, env = process.env) {
return fluffyPipelineOn(env) ? renderFluffy(protocol, opts) : renderProtocol(protocol, opts);
}