119 lines
5.6 KiB
JavaScript
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);
|
|
}
|