Files
brain/tools/secretary-protocol.test.mjs
T
Дмитрий 2b6170313b feat(secretary): нарезка по спанам (реальный промпт владельца) + полное сырьё
Единица разбора — спан: реальный промпт владельца + вся активность ассистента
до следующего реального промпта. Системные ходы (гейт-фидбек, загрузка навыка)
приклеиваются к спану, не считаются отдельными. Разбор отложенный: закрытые
спаны разбираются один раз (курсор в флажке сессии); reconcile и аудитор
получают ПОЛНЫЙ склеенный спан (промпт + все ответы + все действия).

- Слой 1: снят обрез вывода действий (полная картина), защита структурных меток.
- Граница спана — событие UserPromptSubmit (prompt-hook метит realPromptTurns),
  фолбэк по sysLabel; выключение через mode:closing (финальный спан добивает Stop).
- Калибровка скрытых вопросов: страж-ноп (не мутировать при неизменном тексте) +
  кап показа родословной (~~первая~~ → текущая, данные целы).
- Шаги — по спанам («Ход (промпт) N [вобрал ходы X-Y]»); «висит N промптов».
- Новый модуль secretary-span.mjs (computeSpans/spansToDistill/recordRealPrompt/
  parseTurnBlock/assembleSpan).

Свод секретаря зелёный (138 тестов), живой прогон на реальной модели подтвердил:
Шаги по спанам, гейт-шум не плодит скрытые вопросы, находки выживают по одному раз.

Спека/план: docs/superpowers/{specs,plans}/2026-06-23-secretary-span-redesign*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:45:31 +03:00

142 lines
8.6 KiB
JavaScript
Raw 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.
import { describe, it, expect } from 'vitest';
import { renderProtocol, EMPTY_PROTOCOL } from './secretary-protocol.mjs';
describe('EMPTY_PROTOCOL', () => {
it('пустой протокол со всеми 9 разделами', () => {
expect(EMPTY_PROTOCOL()).toEqual({
subject: '', status: 'открыто',
decisions: [], alternatives: [], consequences: [],
will: [], open: [], doneNext: [], history: [], steps: [],
hidden: [], acceptance: [], tails: [], nextSvId: 1,
});
});
it('EMPTY_PROTOCOL содержит поля аудитора скрытых вопросов', () => {
const p = EMPTY_PROTOCOL();
expect(p.hidden).toEqual([]);
expect(p.acceptance).toEqual([]);
expect(p.tails).toEqual([]);
expect(p.nextSvId).toBe(1);
});
});
describe('renderProtocol — 9 категорий + шаги', () => {
const proto = {
subject: 'фоновый секретарь', status: 'открыто',
decisions: [{ text: 'D', why: 'w', turns: [7], session: '69992620-x' }],
alternatives: [{ text: 'ALT', turns: [8], session: '69992620-x' }],
consequences: [{ text: 'CONS', turns: [9], session: '69992620-x' }],
will: [{ text: 'W', turns: [10], session: '69992620-x' }],
open: [{ text: 'Q', turns: [11], session: '69992620-x' }],
doneNext: [{ text: 'N', done: false, turns: [12], session: '69992620-x' }],
history: [],
};
it('шапка «Дело» со статусом/хозяином/целью (по opts)', () => {
const md = renderProtocol(proto, { work: 'создание-секретаря', date: '2026-06-22 11:00' });
expect(md).toContain('**Дело:** создание-секретаря');
expect(md).toContain('**Статус:** открыто');
expect(md).toContain('**Хозяин:** владелец');
expect(md).toContain('**Цель:** фоновый секретарь');
});
it('разделы Альтернативы и Последствия / цена', () => {
const md = renderProtocol(proto);
expect(md).toContain('## Альтернативы');
expect(md).toContain('- ALT');
expect(md).toContain('## Последствия / цена');
expect(md).toContain('- CONS');
});
it('провенанс [→N] без метки файла @ (имя файла — только в Шагах)', () => {
const md = renderProtocol(proto);
expect(md).toContain('- D — w [→7]');
expect(md).not.toContain('@69992620');
expect(md).toContain('## Твоя воля / запреты');
expect(md).toContain('## Открытые вопросы');
});
it('история: тайм-линия toggle (внёс →, вынес ←, вернул →, снова вынес ←)', () => {
const md = renderProtocol({
subject: '', status: 'открыто', steps: [],
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
history: [{ text: 'пункт X', events: [{ turn: 41, dir: 'in' }, { turn: 43, dir: 'out' }, { turn: 55, dir: 'in' }, { turn: 70, dir: 'out' }] }],
});
expect(md).toContain('~~пункт X~~ [→41] [←43] [→55] [←70]');
});
it('провенанс с несколькими ходами: [→33], [50]', () => {
const md = renderProtocol({
subject: '', status: 'открыто', steps: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [], history: [],
decisions: [{ text: 'Y', why: null, turns: [33, 50] }],
});
expect(md).toContain('- Y [→33], [50]');
});
it('зачёркивание во всех корзинах', () => {
const md = renderProtocol({
subject: '', status: 'открыто', history: [],
decisions: [{ text: 'D', struck: true }], alternatives: [{ text: 'A', struck: true }],
consequences: [{ text: 'C', struck: true }], will: [{ text: 'W', struck: true }],
open: [{ text: 'Q', struck: true }], doneNext: [{ text: 'N', struck: true, done: false }],
});
for (const t of ['~~D~~', '~~A~~', '~~C~~', '~~W~~', '~~Q~~', '~~N~~']) expect(md).toContain(t);
});
it('раздел Шаги: ссылка на отдельный файл хода (s.file) вместо общего лога', () => {
const md = renderProtocol({
subject: '', status: 'открыто', history: [],
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
steps: [{ turn: 1, session: 'sess', file: 'ходы/turn-1.log', text: 'Ход 1 — я: x · ты: y · делал: —' }],
});
expect(md).toContain('Ход 1 — я: x · ты: y · делал: — · ходы/turn-1.log');
expect(md).not.toContain('· sess.log');
});
it('раздел Шаги (Слой 1): строка на ход + название файла полного хода в конце строки', () => {
const md = renderProtocol({
subject: '', status: 'открыто', history: [],
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
steps: [{ turn: 1, session: '69992620-x', text: 'Ход 1 — я: про оглавление · ты: тема+время · делал: читал хук' }],
});
expect(md).toContain('## Шаги (Слой 1)');
expect(md).toContain('Ход 1 — я: про оглавление · ты: тема+время · делал: читал хук · 69992620-x.log');
});
it('рендерит горящие блоки и скрытые вопросы с мутацией', () => {
const p = { ...EMPTY_PROTOCOL(),
acceptance: [{ text: 'заявлено работает', born: 14, lastTouch: 14, done: false },
{ text: 'старое', born: 1, lastTouch: 1, done: true }],
tails: [{ text: 'не запушено', born: 23, lastTouch: 23, done: false }],
hidden: [{ id: 'СВ-2', lens: 'Л1', status: 'мутировал', text: 'новая', born: 1, lastTouch: 11,
lineage: [{ turn: 1, text: 'старая' }] }] };
const md = renderProtocol(p, { work: 'x', date: '2026-06-22 14:00' });
expect(md).toContain('⚠️ ЗАЯВЛЕНО ГОТОВО');
expect(md).toContain('заявлено работает');
expect(md).not.toContain('старое'); // done — не показываем
expect(md).toContain('🧹 ХВОСТЫ');
expect(md).toContain('Скрытые вопросы');
expect(md).toContain('~~старая~~ → новая'); // мутация зачёркиванием
});
it('кап родословной: ~~первая~~ → текущая (середина скрыта, данные в JSON целы)', () => {
const p = { ...EMPTY_PROTOCOL(),
hidden: [{ id: 'СВ-1', lens: 'Л3', status: 'мутировал', text: 'нынешняя', born: 3, lastTouch: 15,
lineage: [{ turn: 3, text: 'первая' }, { turn: 9, text: 'средняя-1' }, { turn: 12, text: 'средняя-2' }] }] };
const md = renderProtocol(p, { work: 'x', date: 'd' });
expect(md).toContain('~~первая~~ → нынешняя');
expect(md).not.toContain('средняя-1');
expect(md).not.toContain('средняя-2');
});
it('«висит N промптов» считает спаны, прошедшие с born (не сырые ходы)', () => {
const p = { ...EMPTY_PROTOCOL(),
acceptance: [{ text: 'заявлено готово', born: 3, lastTouch: 3, done: false }] };
// реальные промпты на ходах 3,12,15,22; текущий ход 31 → с born=3 прошло 3 промпта (12,15,22)
const md = renderProtocol(p, { work: 'x', date: 'd', turn: 31, realPromptTurns: [3, 12, 15, 22] });
expect(md).toContain('висит 3 промптов');
});
it('Шаги: разделитель «—— сессия X ——» при смене сессии (не перед первой)', () => {
const md = renderProtocol({
subject: '', status: 'открыто', history: [],
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
steps: [
{ turn: 1, session: 'sА', text: 'Ход 1 — я: a · ты: b · делал: —' },
{ turn: 2, session: 'sА', text: 'Ход 2 — я: c · ты: d · делал: —' },
{ turn: 3, session: 'sБ', text: 'Ход 3 — я: e · ты: f · делал: —' },
],
});
expect(md).toContain('—— сессия sБ ——');
expect(md).not.toContain('—— сессия sА ——'); // перед первой сессией разделителя нет
expect(md.indexOf('—— сессия sБ ——')).toBeLessThan(md.indexOf('Ход 3'));
});
});