feat: round-memory загрузка roundMemory M-side в хуке наставника SP2c-2 инкремент 2b

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-16 17:11:21 +03:00
parent 2ba6d3c405
commit 1289e68524
2 changed files with 38 additions and 1 deletions
+15 -1
View File
@@ -35,6 +35,9 @@ import { bumpMentorNoGo, MENTOR_ESCALATE_AFTER } from './mentor-nogo-counter.mjs
import { buildMentorGo, persistMentorGo } from './mentor-go-store.mjs';
import { classifyJudgeOutcome } from './verdict-outcome-line.mjs';
import { pushVerdict } from './verdict-surface-store.mjs';
// SP2c-2: загрузчик памяти кругов M-side (свои замечания + M-доводы + diff + замечание судьи
// при возврате) — инъектируется в runMentorOnPlanWrite, протягивается до построителя вердикта.
import { buildRoundMemory } from './round-memory-store.mjs';
/**
* Волна 7 (§6): сообщение арбитража при 3 NO-GO наставника — дословное замечание +
@@ -101,11 +104,13 @@ export function buildLlmCall({ apiKey, model = CLASSIFIER_MODEL, transport = cal
* Чистый производитель: inert → {ran:false}; не план-Write → {ran:false}; план без
* steps-json → {ran:false, reason} (вердикт НЕ фабрикуется — печать всё равно fail-CLOSE
* у судьи [enforce-judge-gate.mjs:79]); иначе onPlanWrite + персист. ВСЕ deps инъектируются.
* roundMemoryImpl (SP2c-2): загрузчик памяти кругов M-side ({stage,content,taskId}→roundMemory);
* null → круг слеп (память пуста, backward-compat).
*/
export async function runMentorOnPlanWrite(event, {
mentorActiveImpl, llmCall, loadJournalImpl, persistJournalImpl, persistVerdictImpl,
loadTaskIdImpl, persistTaskIdImpl, journalKey, graphSectionImpl, nowMs = null,
classifyImpl = null, registryImpl = null,
classifyImpl = null, registryImpl = null, roundMemoryImpl = null,
} = {}) {
if (!mentorActiveImpl()) return { ran: false, reason: 'mentor inert ($0)' };
const tool = event && event.tool_name;
@@ -126,6 +131,8 @@ export async function runMentorOnPlanWrite(event, {
let graphSectionS = null;
try { graphSectionS = graphSectionImpl(); } catch { graphSectionS = null; }
const verifiedContextS = parseVerifiedContext(content);
// SP2c-2: память кругов M-side спеки (свои замечания + M-доводы + diff + замечание судьи).
const roundMemoryS = roundMemoryImpl ? roundMemoryImpl({ stage: 'spec', content, taskId: taskIdForPromptS }) : {};
const rs = await onSpecWrite({
specContent: content,
specHash,
@@ -138,6 +145,7 @@ export async function runMentorOnPlanWrite(event, {
verifiedContext: verifiedContextS,
negotiationLog: negotiationLogS,
graphSection: graphSectionS,
roundMemory: roundMemoryS,
});
try { persistVerdictImpl({ ok: rs.ok, wired: rs.wired, reason: rs.reason ?? null, planHash: specHash, verdict: rs.verdict }); } catch { /* best-effort */ }
if (rs.journalOk && rs.journal) { try { persistJournalImpl(rs.journal); } catch { /* best-effort (SE10) */ } }
@@ -165,6 +173,8 @@ export async function runMentorOnPlanWrite(event, {
const planGoal = extractPlanGoal(content);
let registry = null;
try { registry = typeof registryImpl === 'function' ? registryImpl() : null; } catch { registry = null; }
// SP2c-2: память кругов M-side плана (свои замечания + M-доводы + diff + замечание судьи).
const roundMemoryP = roundMemoryImpl ? roundMemoryImpl({ stage: 'plan', content, taskId: taskIdForPrompt }) : {};
const r = await onPlanWrite({
planSteps: steps,
existingTaskId: loadTaskIdImpl(),
@@ -180,6 +190,7 @@ export async function runMentorOnPlanWrite(event, {
registry,
declaredSkills,
planGoal,
roundMemory: roundMemoryP,
});
const planHash = planId(steps);
try { persistVerdictImpl({ ok: r.ok, wired: r.wired, reason: r.reason ?? null, planHash, verdict: r.verdict }); } catch { /* best-effort */ }
@@ -211,6 +222,9 @@ async function main() {
// ключ/транспорт; сбой ловит onPlanWrite → fail-safe (план без скил-сверки, §5).
classifyImpl: async (goal, registry) => classify(goal, registry, { skipPrefilter: true }),
registryImpl: () => { try { return loadRegistry({ useCache: false }); } catch { return null; } },
// SP2c-2: реальный загрузчик памяти кругов M-side из стора (fail-quiet внутри
// buildRoundMemory). baseDir = runtime; side='mentor' → M-дорожка + замечание судьи при возврате.
roundMemoryImpl: ({ stage, content, taskId }) => buildRoundMemory({ taskId, stage, side: 'mentor', currentContent: content, baseDir: dir }),
});
if (res && res.ran) {
// SP1: громкая видимость вердикта наставника (best-effort, fail-quiet).
@@ -116,6 +116,29 @@ describe('runMentorOnPlanWrite (обёртка-производитель W7)',
expect(r.ran).toBe(true);
expect(classifyCalled).toBe(true);
});
// SP2c-2: хук грузит память кругов M-side через roundMemoryImpl и протягивает её до
// построителя вердикта наставника (план и спека). stage прокидывается верно.
it('SP2c-2: roundMemoryImpl (план) → память доходит до промпта наставника', async () => {
let capturedUser = null;
const d = deps({
roundMemoryImpl: ({ stage }) => ({ objections: [`память дорожки ${stage}`] }),
llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return GOOD_VERDICT; },
});
const r = await runMentorOnPlanWrite(planEvent, d);
expect(r.ran).toBe(true);
expect(capturedUser).toContain('память дорожки plan');
});
it('SP2c-2: roundMemoryImpl (спека) → stage=spec в памяти промпта', async () => {
let capturedUser = null;
const d = deps({
roundMemoryImpl: ({ stage }) => ({ objections: [`память дорожки ${stage}`] }),
llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return GOOD_VERDICT; },
});
const r = await runMentorOnPlanWrite(specEvent, d);
expect(r.ran).toBe(true);
expect(capturedUser).toContain('память дорожки spec');
});
});
describe('decideMentorObjection (Фаза 1 — канал замечаний наставника контроллеру)', () => {