fix(secretary): срез впечённой метки «[зачёркнуто]» из текста записей

Модель копировала служебный префикс «[зачёркнуто] » (которым запрос помечает
зачёркнутые строки) в поле text, и он компаундился ([зачёркнуто] [зачёркнуто] …),
плодя марочные клоны. canonicalClauses теперь срезает ведущий маркер перед
сборкой канона: метка не персистится, клоны сливаются с чистой записью (ходы
объединяются). Только ведущий наш маркер — реальный текст с него не начинается.
Дедуп/История/провенанс/Шаги не тронуты. Свод секретаря 113/113.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-23 11:54:39 +03:00
parent c778d10d10
commit 3be6cedd23
2 changed files with 16 additions and 1 deletions
+5 -1
View File
@@ -92,7 +92,11 @@ function allTexts(p) {
/** Куски записи без дословных повторов: split(' — ') от «text — why», dedup по первому вхождению. */
function canonicalClauses(text, why) {
const raw = (why != null && String(why).trim() !== '') ? `${text}${SEP}${why}` : String(text || '');
// Срез служебной метки зачёркивания, впечённой моделью в текст (запрос рисует «[зачёркнуто] X»,
// слабая модель копирует строку в поле text). Только ведущий наш маркер — реальный текст с него
// не начинается; марочные клоны после среза дают тот же канон и сливаются с чистой записью.
const cleanText = String(text || '').replace(/^(?:\[зачёркнуто\]\s*)+/, '');
const raw = (why != null && String(why).trim() !== '') ? `${cleanText}${SEP}${why}` : cleanText;
const seen = new Set();
const out = [];
for (const part of raw.split(SEP).map((s) => s.trim()).filter(Boolean)) {
+11
View File
@@ -231,6 +231,17 @@ describe('collapseProtocol — детерминированное схлопыв
const p = { open: [{ text: 'Хайку или Sonnet? — Хайку или Sonnet?', struck: false, turns: [1] }] };
expect(collapseProtocol(p).open[0].text).toBe('Хайку или Sonnet?');
});
it('срезает впечённую метку «[зачёркнуто]» из текста и сливает с чистой записью', () => {
const p = { decisions: [
{ text: 'Выбрано X', why: 'причина', struck: true, turns: [3] },
{ text: '[зачёркнуто] [зачёркнуто] Выбрано X', why: 'причина', struck: true, turns: [31] },
] };
const out = collapseProtocol(p);
expect(out.decisions).toHaveLength(1);
expect(out.decisions[0].text).toBe('Выбрано X');
expect(out.decisions[0].turns).toEqual([3, 31]);
expect(JSON.stringify(out)).not.toContain('[зачёркнуто]');
});
it('hidden / steps / nextSvId не трогаются', () => {
const p = { decisions: [], hidden: [{ id: 'СВ-1' }], steps: [{ turn: 1 }], nextSvId: 5 };
const out = collapseProtocol(p);