// Структура и сверка короткого протокола (§D5/§D7). Отменённое зачёркивается, не удаляется. export function EMPTY_PROTOCOL() { return { subject: '', status: 'открыто', decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [], history: [], steps: [], hidden: [], acceptance: [], tails: [], nextSvId: 1, }; } // Провенанс: первый ход — где внесено [→N]; последующие — где снова касались [M]. function prov(turns) { if (!Array.isArray(turns) || !turns.length) return ''; const [first, ...rest] = turns; return ` [→${first}]${rest.map((t) => `, [${t}]`).join('')}`; } // Провенанс в обычных строках — только [→N]; имя файла/сессии живёт в разделе «Шаги». const line = (e) => `${e.struck ? `~~${e.text}~~` : e.text}${prov(e.turns)}`; // Шаги (Слой 1): человекочитаемая строка на КАЖДЫЙ ход («спросил → ответил»), в конце — // ссылка(и) на сырьё для подробностей. Шаги ведёт хук (по ходу), не модель. function stepsSection(p) { const steps = (p.steps || []).slice().sort((a, b) => (a.turn || 0) - (b.turn || 0)); // Ссылка на отдельный файл хода (file=«ходы/turn-N.log»), проставленный при остановке; // до нарезки — запасной вариант на общий лог сессии. Между ходами разных сессий — // строка-разделитель «—— сессия X ——» (граница рабочего захода в сквозной тетради). const out = []; let prevSession = null; for (const s of steps) { if (s.session && prevSession !== null && s.session !== prevSession) { out.push(`—— сессия ${s.session} ——`); } const ref = s.file || (s.session ? `${s.session}.log` : ''); out.push(`- ${s.text}${ref ? ` · ${ref}` : ''}`); if (s.session) prevSession = s.session; } return out; } // Полная форма протокола (§D7): шапка «Дело» + 8 корзин (2–9) + навигация Шаги→Слой 1. export function renderProtocol(protocol, opts = {}) { const L = []; if (opts.work) { L.push(`**Дело:** ${opts.work} · **Статус:** ${protocol.status || 'открыто'} · ` + `**Дата:** ${opts.date || ''} · **Хозяин:** владелец · **Цель:** ${protocol.subject || ''}`, ''); } // «висит N» — число реальных промптов (спанов), прошедших с born, а не сырых ходов. const spanDist = (born) => { const rp = Array.isArray(opts.realPromptTurns) ? opts.realPromptTurns : null; if (rp && opts.turn) return rp.filter((t) => t > born && t <= opts.turn).length; return opts.turn && born != null ? opts.turn - born : 0; // фолбэк (сырые ходы) }; const burn = (title, arr) => { const live = (arr || []).filter((e) => !e.done); if (!live.length) return; L.push(title); for (const e of live) { const n = e.lastTouch != null && opts.turn && opts.turn > e.lastTouch ? spanDist(e.born) : 0; const stale = n > 0 ? ` · висит ${n} промптов` : ''; L.push(`- ${e.text}${e.born ? ` [→${e.born}]` : ''}${stale}`); } L.push(''); }; burn('## ⚠️ ЗАЯВЛЕНО ГОТОВО — НЕ ПРОВЕРЕНО (Л8)', protocol.acceptance); burn('## 🧹 ХВОСТЫ — НЕ ПРИБРАНО (Л9)', protocol.tails); L.push('## Решения'); for (const d of protocol.decisions || []) { const body = d.struck ? `~~${d.text}~~` : d.text; const why = d.why ? ` — ${d.why}` : ''; L.push(`- ${body}${why}${prov(d.turns)}`); } L.push('', '## Альтернативы'); for (const a of protocol.alternatives || []) L.push(`- ${line(a)}`); L.push('', '## Последствия / цена'); for (const c of protocol.consequences || []) L.push(`- ${line(c)}`); L.push('', '## Твоя воля / запреты'); for (const w of protocol.will || []) L.push(`- ${line(w)}`); L.push('', '## Открытые вопросы'); for (const o of protocol.open || []) L.push(`- ${line(o)}`); L.push('', '## Сделано / дальше'); for (const s of protocol.doneNext || []) L.push(`- [${s.done ? 'x' : ' '}] ${s.struck ? `~~${s.text}~~` : s.text}${prov(s.turns)}`); L.push('', '## История (заменено, не стёрто)'); for (const hh of protocol.history || []) { if (Array.isArray(hh.events) && hh.events.length) { // Тайм-линия: внёс [→N], вынес [←N]; может повторяться (вернул/снова вынес). const seq = hh.events.map((ev) => (ev.dir === 'out' ? `[←${ev.turn}]` : `[→${ev.turn}]`)).join(' '); const removed = hh.events[hh.events.length - 1].dir === 'out'; L.push(`- ${removed ? `~~${hh.text}~~` : hh.text} ${seq}`); } else { L.push(`- ~~${hh.oldText}~~ → ${hh.newText}${prov(hh.turns)}`); // legacy-формат } } L.push('', '## Шаги (Слой 1)'); for (const s of stepsSection(protocol)) L.push(s); L.push('', '## Скрытые вопросы (фон)'); for (const h of (protocol.hidden || [])) { const head = h.lineage && h.lineage.length ? `~~${h.lineage[0].text}~~ → ${h.text}` // кап показа: только первая → текущая (данные в JSON целы) : h.text; const prov2 = ` [→${h.born}]` + (h.lastTouch && h.lastTouch !== h.born ? ` [${h.lastTouch}]` : ''); L.push(`- ${h.id} [${h.lens} · ${h.status}]: ${head}${prov2}`); } return L.join('\n'); }