feat(secretary): раздел «Шаги (Слой 1)» — все ходы человекочитаемо + ссылка на сырьё в конце
- buildStepLine: кратко «спросил -> ответил» (служебные строки экономия/coverage/вердикт отброшены) - protocol.steps: хук ведёт по строке на КАЖДЫЙ ход; рендер — список + одна ссылка raw в конце - reconcile (stampProvenance) сохраняет steps (модель их не трогает) - stop-хук добавляет шаг текущего хода 41 тест green, exit=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,3 +11,17 @@ export function buildRawRecord({ turn, time, session, user, assistant, actions =
|
||||
lines.push('=== КОНЕЦ ХОДА ===', '');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Человекочитаемая строка шага для раздела «Шаги (Слой 1)»: кратко «спросил → ответил».
|
||||
// Служебные строки (экономия/coverage/вердикт) из ответа отбрасываются; длинное усекается.
|
||||
export function buildStepLine({ turn, user, assistant } = {}) {
|
||||
const gist = (s) => {
|
||||
const t = String(s ?? '').replace(/\s+/g, ' ').trim();
|
||||
return t.length > 140 ? `${t.slice(0, 140)}…` : t;
|
||||
};
|
||||
const cleanA = String(assistant ?? '').split('\n')
|
||||
.filter((l) => !/^\s*(экономия:|coverage:|вердикт:)/i.test(l)).join(' ');
|
||||
const u = gist(user) || '(без вопроса)';
|
||||
const a = gist(cleanA) || '(без ответа)';
|
||||
return `Ход ${turn}: ${u} → ${a}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildRawRecord } from './secretary-layer1.mjs';
|
||||
import { buildRawRecord, buildStepLine } from './secretary-layer1.mjs';
|
||||
|
||||
describe('buildRawRecord', () => {
|
||||
it('содержит заголовок с turn, реплики и действие', () => {
|
||||
@@ -18,3 +18,16 @@ describe('buildRawRecord', () => {
|
||||
expect(rec).not.toContain('[ДЕЙСТВИЕ]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildStepLine', () => {
|
||||
it('кратко: спросил → ответил, без служебных строк', () => {
|
||||
const s = buildStepLine({ turn: 5, user: 'сделай флажок по сессии', assistant: 'экономия: 100%\nГотово, сделал флажок' });
|
||||
expect(s).toContain('Ход 5');
|
||||
expect(s).toContain('сделай флажок по сессии');
|
||||
expect(s).toContain('Готово, сделал флажок');
|
||||
expect(s).not.toContain('экономия');
|
||||
});
|
||||
it('пустой вопрос → (без вопроса)', () => {
|
||||
expect(buildStepLine({ turn: 2, user: '', assistant: 'a' })).toContain('(без вопроса)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ export function EMPTY_PROTOCOL() {
|
||||
return {
|
||||
subject: '', status: 'открыто',
|
||||
decisions: [], alternatives: [], consequences: [],
|
||||
will: [], open: [], doneNext: [], history: [],
|
||||
will: [], open: [], doneNext: [], history: [], steps: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,20 +18,14 @@ function src(entry) {
|
||||
|
||||
const line = (e) => `${e.struck ? `~~${e.text}~~` : e.text}${prov(e.turns)}${src(e)}`;
|
||||
|
||||
// Шаги (Слой 1): уникальные (сессия, ход) из всех корзин → ссылка в raw/<session>.log.
|
||||
function stepsIndex(p) {
|
||||
const seen = new Map();
|
||||
for (const sec of ['decisions', 'alternatives', 'consequences', 'will', 'open', 'doneNext']) {
|
||||
for (const e of (p[sec] || [])) {
|
||||
const sess = e.session || '';
|
||||
for (const t of (e.turns || [])) {
|
||||
const key = `${sess}#${t}`;
|
||||
if (!seen.has(key)) seen.set(key, { turn: t, session: sess });
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...seen.values()].sort((a, b) => a.turn - b.turn)
|
||||
.map(({ turn, session }) => `- [→${turn}]${session ? ` raw/${session}.log` : ''}`);
|
||||
// Шаги (Слой 1): человекочитаемая строка на КАЖДЫЙ ход («спросил → ответил»), в конце —
|
||||
// ссылка(и) на сырьё для подробностей. Шаги ведёт хук (по ходу), не модель.
|
||||
function stepsSection(p) {
|
||||
const steps = (p.steps || []).slice().sort((a, b) => (a.turn || 0) - (b.turn || 0));
|
||||
const L = steps.map((s) => `- ${s.text}`);
|
||||
const sessions = [...new Set(steps.map((s) => s.session).filter(Boolean))];
|
||||
if (sessions.length) L.push('', ...sessions.map((s) => `Подробно (дословно): raw/${s}.log`));
|
||||
return L;
|
||||
}
|
||||
|
||||
// Полная форма протокола (§D7): шапка «Дело» + 8 корзин (2–9) + навигация Шаги→Слой 1.
|
||||
@@ -60,6 +54,6 @@ export function renderProtocol(protocol, opts = {}) {
|
||||
L.push('', '## История (заменено, не стёрто)');
|
||||
for (const h of protocol.history || []) L.push(`- ~~${h.oldText}~~ → ${h.newText}${prov(h.turns)}`);
|
||||
L.push('', '## Шаги (Слой 1)');
|
||||
for (const s of stepsIndex(protocol)) L.push(s);
|
||||
for (const s of stepsSection(protocol)) L.push(s);
|
||||
return L.join('\n');
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('EMPTY_PROTOCOL', () => {
|
||||
expect(EMPTY_PROTOCOL()).toEqual({
|
||||
subject: '', status: 'открыто',
|
||||
decisions: [], alternatives: [], consequences: [],
|
||||
will: [], open: [], doneNext: [], history: [],
|
||||
will: [], open: [], doneNext: [], history: [], steps: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -51,10 +51,19 @@ describe('renderProtocol — 9 категорий + шаги', () => {
|
||||
});
|
||||
for (const t of ['~~D~~', '~~A~~', '~~C~~', '~~W~~', '~~Q~~', '~~N~~']) expect(md).toContain(t);
|
||||
});
|
||||
it('раздел Шаги (Слой 1) со ссылками в raw по ходам', () => {
|
||||
const md = renderProtocol(proto);
|
||||
it('раздел Шаги (Слой 1): человекочитаемые строки на КАЖДЫЙ ход + ссылка на сырьё в конце', () => {
|
||||
const md = renderProtocol({
|
||||
subject: '', status: 'открыто', history: [],
|
||||
decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
|
||||
steps: [
|
||||
{ turn: 1, session: '69992620-x', text: 'Спросил про оглавление → ответил: тема + время' },
|
||||
{ turn: 2, session: '69992620-x', text: 'Попросил флажок по сессии → сделал' },
|
||||
],
|
||||
});
|
||||
expect(md).toContain('## Шаги (Слой 1)');
|
||||
expect(md).toContain('raw/69992620-x.log');
|
||||
expect(md).toContain('→7');
|
||||
expect(md).toContain('Спросил про оглавление → ответил: тема + время');
|
||||
expect(md).toContain('Попросил флажок по сессии → сделал');
|
||||
expect(md).toContain('Подробно (дословно): raw/69992620-x.log');
|
||||
expect(md).not.toContain('[→1] raw/'); // не ссылка в каждой строке
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,6 +107,7 @@ export function stampProvenance(oldProtocol, returned, turn, session) {
|
||||
open: (returned.open || []).map(stamp),
|
||||
doneNext: (returned.doneNext || []).map(stamp),
|
||||
history: Array.isArray(oldProtocol.history) ? oldProtocol.history : [],
|
||||
steps: Array.isArray(oldProtocol.steps) ? oldProtocol.steps : [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { parseLastExchange } from './secretary-transcript.mjs';
|
||||
import { secretaryModeFileName } from './secretary-flag.mjs';
|
||||
import { buildRawRecord } from './secretary-layer1.mjs';
|
||||
import { buildRawRecord, buildStepLine } from './secretary-layer1.mjs';
|
||||
import { reconcileTurn } from './secretary-reconcile.mjs';
|
||||
import { renderProtocol, EMPTY_PROTOCOL } from './secretary-protocol.mjs';
|
||||
import { upsertIndexEntry } from './secretary-index.mjs';
|
||||
@@ -69,6 +69,9 @@ async function main() {
|
||||
const updated = await reconcileTurn({ proto, ex, turn, session, callModel });
|
||||
if (updated) {
|
||||
const stamp = new Date().toISOString().slice(0, 16).replace('T', ' ');
|
||||
// Шаги (Слой 1) ведёт хук: по строке на ход «спросил → ответил» (модель их не трогает).
|
||||
updated.steps = [...(Array.isArray(updated.steps) ? updated.steps : []),
|
||||
{ turn, session, text: buildStepLine({ turn, user: ex.user, assistant: ex.assistant }) }];
|
||||
mkdirSync(workDir, { recursive: true });
|
||||
writeFileSync(protoJson, JSON.stringify(updated, null, 2), 'utf-8');
|
||||
writeFileSync(join(workDir, 'protocol.md'), renderProtocol(updated, { work, date: stamp }), 'utf-8');
|
||||
|
||||
Reference in New Issue
Block a user