feat(round-memory): память кругов в промптах judge/mentor + split M/J (SP2b)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
import { canonicalJson } from './receipt-sign.mjs';
|
||||
import { validateVerdictSlots } from './judge-verdict-slots.mjs';
|
||||
import { partitionObjections } from './judge-anchor.mjs';
|
||||
import { renderRoundMemory } from './round-memory-render.mjs';
|
||||
import { requiredSubRunsPresent } from './judge-subrun-journal.mjs';
|
||||
|
||||
export const VOTE_LAYOUTS = Object.freeze({
|
||||
@@ -41,10 +42,10 @@ export function requiredLensesFor(functionName, { risk = false } = {}) {
|
||||
}
|
||||
|
||||
/** Чистый построитель промпта судьи: {system, user}. Детерминирован. */
|
||||
export function buildJudgePrompt({ functionName, requiredLenses = [], product = {}, goal = '', cards = [] }) {
|
||||
export function buildJudgePrompt({ functionName, requiredLenses = [], product = {}, goal = '', cards = [], roundMemory = {} }) {
|
||||
const system = [
|
||||
'Ты — судья (критик, не исполнитель). Выноси МОТИВИРОВАННЫЕ возражения с якорем.',
|
||||
'Ты СЛЕП к переписке и рассуждениям сторон — судишь только поданный продукт против цели.',
|
||||
'Ты СЛЕП к переписке наставника со сторонами — судишь продукт против цели; учитываешь лишь СВОИ прошлые замечания и доводы контроллера лично тебе.',
|
||||
'План — это БУДУЩЕЕ: слово «проверено»/«сделано» за ФАКТ не принимай (K5).',
|
||||
`Функция: ${functionName}.`,
|
||||
`Заполни слот на КАЖДУЮ линзу (пустой слот → вердикт невалиден): ${requiredLenses.join(', ')}.`,
|
||||
@@ -55,7 +56,8 @@ export function buildJudgePrompt({ functionName, requiredLenses = [], product =
|
||||
`ЦЕЛЬ (неизменный якорь): ${goal}`,
|
||||
`КАРТОЧКИ НАВЫКОВ: ${canonicalJson(cards)}`,
|
||||
`ПРОДУКТ НА СУД: ${canonicalJson(product)}`,
|
||||
].join('\n');
|
||||
renderRoundMemory(roundMemory),
|
||||
].filter(Boolean).join('\n');
|
||||
return { system, user };
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ describe('buildJudgePrompt (чистая, детерминированная; с
|
||||
for (const c of PREMORTEM_CLASSES) expect(system).toContain(c);
|
||||
expect(user).toContain('лендинг');
|
||||
});
|
||||
it('круг 1 слеп: пустой roundMemory → нет блока; непустой → блок памяти в user', () => {
|
||||
expect(buildJudgePrompt(args).user).not.toContain('ПАМЯТЬ КРУГОВ');
|
||||
const withMem = buildJudgePrompt({ ...args, roundMemory: { objections: ['моё прошлое замечание'] } });
|
||||
expect(withMem.user).toContain('ПАМЯТЬ КРУГОВ');
|
||||
expect(withMem.user).toContain('моё прошлое замечание');
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyByReversibility (G1: фатальное=тяжёлое И необратимое → блок)', () => {
|
||||
|
||||
@@ -39,6 +39,7 @@ export function isMentorVerdictSubstantive(verdict, { wired = false } = {}) {
|
||||
// mentor-seam (дубль-литералы с дрейфом сняты; циклов нет — seam verdict не импортирует).
|
||||
import { parseRouterResponse } from './router-engine.mjs';
|
||||
import { DR1_LINE, renderVerifiedContext, renderNegotiation } from './mentor-seam.mjs';
|
||||
import { renderRoundMemory } from './round-memory-render.mjs';
|
||||
|
||||
/**
|
||||
* Промпт-производитель вердикта (§6.1): зеркало идиомы buildRouterPrompt {system,user},
|
||||
@@ -47,10 +48,11 @@ import { DR1_LINE, renderVerifiedContext, renderNegotiation } from './mentor-sea
|
||||
* контекст + журнал переговоров (единые рендеры seam, VA-1; пустой контекст НЕ молчит,
|
||||
* VA-2) + граф-секция (опора наставника). A8 (нах.F6): system несёт ДР-1 гранулярность.
|
||||
*/
|
||||
export function buildMentorVerdictPrompt({ plan = null, verifiedContext = [], negotiationLog = [], graphSection = null, skillContext = null } = {}) {
|
||||
export function buildMentorVerdictPrompt({ plan = null, verifiedContext = [], negotiationLog = [], graphSection = null, skillContext = null, roundMemory = {} } = {}) {
|
||||
const system = [
|
||||
'Ты — НАСТАВНИК. Разбери ПЛАН ПО ПУНКТАМ (не выбирай скил — это другой вызов).',
|
||||
DR1_LINE,
|
||||
'Переоцени ТЕКУЩУЮ версию заново по памяти кругов ниже; что уже снято — НЕ повторяй.',
|
||||
// Smoke 2026-06-12: «статус+замечание» без типа элемента провоцировал массив объектов —
|
||||
// валидатор (F-C3/М1-М4: слот = строки) браковал содержательный вердикт. Тип — явно.
|
||||
'Вынеси РЕШЕНИЕ: decision="GO" если можно реализовывать, decision="NO-GO" если нужна переделка (тогда в recommendation — ЧТО править).',
|
||||
@@ -60,6 +62,7 @@ export function buildMentorVerdictPrompt({ plan = null, verifiedContext = [], ne
|
||||
`ПЛАН: ${plan ? JSON.stringify(plan) : '(нет)'}`,
|
||||
renderVerifiedContext(verifiedContext),
|
||||
renderNegotiation(negotiationLog),
|
||||
renderRoundMemory(roundMemory),
|
||||
graphSection ? `--- ГРАФ (карта районов) ---\n${JSON.stringify(graphSection.layer0 || [])}` : '',
|
||||
skillContext ? skillContext : '',
|
||||
].filter(Boolean).join('\n');
|
||||
@@ -95,10 +98,11 @@ export async function runMentorVerdict({ plan = null, verifiedContext = [], nego
|
||||
* Зеркало buildMentorVerdictPrompt, но просит разбор СПЕКИ по разделам (не плана по пунктам).
|
||||
* Наставник ВИДИТ контекст (verified-context + переговоры — Р6), судья — нет. Те же слоты.
|
||||
*/
|
||||
export function buildMentorSpecVerdictPrompt({ specContent = '', verifiedContext = [], negotiationLog = [], graphSection = null } = {}) {
|
||||
export function buildMentorSpecVerdictPrompt({ specContent = '', verifiedContext = [], negotiationLog = [], graphSection = null, roundMemory = {} } = {}) {
|
||||
const system = [
|
||||
'Ты — НАСТАВНИК. Разбери СПЕКУ ПО РАЗДЕЛАМ (это спецификация решения, не план; скил не выбираешь).',
|
||||
DR1_LINE,
|
||||
'Переоцени ТЕКУЩУЮ версию заново по памяти кругов ниже; что уже снято — НЕ повторяй.',
|
||||
'Вынеси РЕШЕНИЕ: decision="GO" если спеку можно принять, decision="NO-GO" если нужна переделка (тогда в recommendation — ЧТО править).',
|
||||
'ВЫВОД — строго JSON-вердикт со слотами: plan_points_addressed (массив СТРОК — по КАЖДОМУ разделу/решению спеки ровно одна строка вида «раздел X: статус — замечание»; элемент-объект недопустим, не объект а строка), reasoning (строка-разбор), recommendation (строка — что править), confidence (число 0..1), decision ("GO"|"NO-GO"). Пустой слот недопустим.',
|
||||
].join('\n');
|
||||
@@ -106,6 +110,7 @@ export function buildMentorSpecVerdictPrompt({ specContent = '', verifiedContext
|
||||
`СПЕКА:\n${specContent || '(нет)'}`,
|
||||
renderVerifiedContext(verifiedContext),
|
||||
renderNegotiation(negotiationLog),
|
||||
renderRoundMemory(roundMemory),
|
||||
graphSection ? `--- ГРАФ (карта районов) ---\n${JSON.stringify(graphSection.layer0 || [])}` : '',
|
||||
].filter(Boolean).join('\n');
|
||||
return { system, user };
|
||||
|
||||
@@ -137,6 +137,11 @@ describe('buildMentorVerdictPrompt (§6.1 verdict-слоты, НЕ router)', ()
|
||||
expect(p.system).toMatch(/массив СТРОК/);
|
||||
expect(p.system).toMatch(/не объект/i);
|
||||
});
|
||||
it('roundMemory: пусто → нет блока; непусто → блок памяти (оба построителя)', () => {
|
||||
expect(buildMentorVerdictPrompt({ plan: { steps: [] } }).user).not.toContain('ПАМЯТЬ КРУГОВ');
|
||||
expect(buildMentorVerdictPrompt({ plan: { steps: [] }, roundMemory: { objections: ['замечание M'] } }).user).toContain('замечание M');
|
||||
expect(buildMentorSpecVerdictPrompt({ specContent: '# с', roundMemory: { objections: ['замечание M2'] } }).user).toContain('замечание M2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runMentorVerdict (§6.1 производитель + binding нах.F4)', () => {
|
||||
|
||||
@@ -15,7 +15,21 @@ export function parseNegotiationSection(md) {
|
||||
const re = /(?:^|\n)###\s*Круг\s*(\d+)[^\n]*\n([\s\S]*?)(?=\n###\s|$)/gi;
|
||||
let m;
|
||||
while ((m = re.exec(body)) !== null) {
|
||||
out.push({ round: Number(m[1]), position: m[2].trim() });
|
||||
const position = m[2].trim();
|
||||
out.push({
|
||||
round: Number(m[1]),
|
||||
position,
|
||||
mentor: extractAddressee(position, 'Наставнику'),
|
||||
judge: extractAddressee(position, 'Судье'),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Достаёт текст, адресованный стороне (`**Наставнику:**` / `**Судье:**`), до следующего
|
||||
* адресата или конца. Нет адресата → пустая строка (обратная совместимость с position). */
|
||||
function extractAddressee(body, label) {
|
||||
const re = new RegExp(`\\*\\*${label}:\\*\\*\\s*([\\s\\S]*?)(?=\\n?\\*\\*(?:Наставнику|Судье):\\*\\*|$)`, 'i');
|
||||
const mm = String(body || '').match(re);
|
||||
return mm ? mm[1].trim() : '';
|
||||
}
|
||||
|
||||
@@ -16,10 +16,20 @@ describe('parseNegotiationSection', () => {
|
||||
it('достаёт круги с дословным текстом позиции', () => {
|
||||
const r = parseNegotiationSection(PLAN);
|
||||
expect(r).toEqual([
|
||||
{ round: 1, position: 'Не согласен с замечанием про шаг 2: он атомарен.' },
|
||||
{ round: 2, position: 'Принял про шаг 4, переписал.' },
|
||||
{ round: 1, position: 'Не согласен с замечанием про шаг 2: он атомарен.', mentor: '', judge: '' },
|
||||
{ round: 2, position: 'Принял про шаг 4, переписал.', mentor: '', judge: '' },
|
||||
]);
|
||||
});
|
||||
it('делит доводы по дорожкам M/J внутри круга', () => {
|
||||
const plan = `## Переговоры
|
||||
### Круг 1
|
||||
**Наставнику:** не согласен с пунктом 2, он атомарен
|
||||
**Судье:** критерий в разделе K
|
||||
## Шаги`;
|
||||
const r = parseNegotiationSection(plan);
|
||||
expect(r[0].mentor).toBe('не согласен с пунктом 2, он атомарен');
|
||||
expect(r[0].judge).toBe('критерий в разделе K');
|
||||
});
|
||||
it('нет раздела → пустой массив', () => {
|
||||
expect(parseNegotiationSection('# План\n## Цель\nY')).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
/** round-memory-render — чистый рендер блока «ПАМЯТЬ КРУГОВ» для промптов судьи/наставника (SP2b).
|
||||
* Пустой вход → пустая строка (круг 1 слеп). Свои замечания и доводы контроллера — дословно.
|
||||
* Что класть (свои J/M-замечания, J/M-доводы, какой diff, замечание судьи при возврате) — решает
|
||||
* вызыватель (оркестрация, SP2c); этот модуль лишь рендерит то, что дали. */
|
||||
|
||||
export function renderRoundMemory(mem = {}) {
|
||||
const {
|
||||
versionDiff = '',
|
||||
objections = [],
|
||||
args = [],
|
||||
judgeObjectionOnReturn = '',
|
||||
} = mem || {};
|
||||
const parts = [];
|
||||
if (Array.isArray(objections) && objections.length) {
|
||||
parts.push('--- ТВОИ ПРОШЛЫЕ ЗАМЕЧАНИЯ (дословно) ---');
|
||||
objections.forEach((o, i) => parts.push(`${i + 1}. ${o}`));
|
||||
}
|
||||
if (Array.isArray(args) && args.length) {
|
||||
parts.push('--- ДОВОДЫ КОНТРОЛЛЕРА (дословно) ---');
|
||||
args.forEach((a, i) => parts.push(`${i + 1}. ${a}`));
|
||||
}
|
||||
if (versionDiff) {
|
||||
parts.push('--- ИЗМЕНЕНИЯ С ПРОШЛОЙ ВЕРСИИ (diff) ---');
|
||||
parts.push(versionDiff);
|
||||
}
|
||||
if (judgeObjectionOnReturn) {
|
||||
parts.push('--- ЗАМЕЧАНИЕ СУДЬИ (учесть при доработке) ---');
|
||||
parts.push(judgeObjectionOnReturn);
|
||||
}
|
||||
if (parts.length === 0) return '';
|
||||
return ['=== ПАМЯТЬ КРУГОВ ===', ...parts].join('\n');
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderRoundMemory } from './round-memory-render.mjs';
|
||||
|
||||
describe('renderRoundMemory', () => {
|
||||
it('пустой вход → пустая строка (круг 1 слеп)', () => {
|
||||
expect(renderRoundMemory()).toBe('');
|
||||
expect(renderRoundMemory({})).toBe('');
|
||||
expect(renderRoundMemory({ objections: [], args: [] })).toBe('');
|
||||
});
|
||||
it('замечания дословно', () => {
|
||||
const r = renderRoundMemory({ objections: ['правь раздел X'] });
|
||||
expect(r).toContain('ТВОИ ПРОШЛЫЕ ЗАМЕЧАНИЯ');
|
||||
expect(r).toContain('правь раздел X');
|
||||
});
|
||||
it('доводы контроллера дословно', () => {
|
||||
const r = renderRoundMemory({ args: ['не согласен, потому что Y'] });
|
||||
expect(r).toContain('ДОВОДЫ КОНТРОЛЛЕРА');
|
||||
expect(r).toContain('не согласен, потому что Y');
|
||||
});
|
||||
it('diff версий и замечание судьи при возврате', () => {
|
||||
const r = renderRoundMemory({ versionDiff: '+ новая строка', judgeObjectionOnReturn: 'судья: нет критерия' });
|
||||
expect(r).toContain('+ новая строка');
|
||||
expect(r).toContain('судья: нет критерия');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user