From db1eb8e33722bc0949280cbcd9cf8eda1488ac8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 13 Jun 2026 17:15:14 +0300 Subject: [PATCH] =?UTF-8?q?fix(mentor):=20=D1=80=D0=B5=D0=B7=D0=BE=D0=BB?= =?UTF-8?q?=D0=B2=20ID=20=D1=83=D0=B7=D0=BB=D0=BE=D0=B2=20=D0=B2=20=D0=B8?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=B2=20=D1=81=D0=BA=D0=B8=D0=BB-?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D0=B2=D0=BD=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderSkillContext резолвит #N -> '#N - имя' через registry.indexById (resolveNodeName, fail-safe -> голый #N); onPlanWrite прокидывает registry. Наставник видит рекомендацию роутера именами. TDD +4 теста, регрессия tools-only 3910 GREEN. Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/mentor-seam.mjs | 17 ++++++++++++++--- tools/mentor-seam.test.mjs | 17 +++++++++++++++++ tools/on-plan-write.mjs | 2 +- tools/on-plan-write.test.mjs | 15 +++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tools/mentor-seam.mjs b/tools/mentor-seam.mjs index 67fba3fc..5f7afb77 100644 --- a/tools/mentor-seam.mjs +++ b/tools/mentor-seam.mjs @@ -53,13 +53,24 @@ export function renderNegotiation(log) { return ['--- ПЕРЕГОВОРЫ ЗАДАЧИ (история кругов) ---', ...lines].join('\n'); } +/** Резолв ID узла → «#N — имя» через registry.indexById; нет узла/реестра → голый #N (fail-safe). */ +function resolveNodeName(id, registry) { + const node = registry && registry.indexById && typeof registry.indexById.get === 'function' + ? registry.indexById.get(id) : null; + const label = node && (node.name || node.slug); + return label ? `${id} — ${label}` : String(id); +} + /** Рендер скил-контекста для промпта наставника (мерж): объявленные в плане скилы + - * рекомендация роутера. recommendedChain=null → классификатор недоступен (не сверять). */ -export function renderSkillContext({ declared = [], recommendedChain = null } = {}) { + * рекомендация роутера (ID резолвятся в имена через registry). recommendedChain=null → + * классификатор недоступен (не сверять). */ +export function renderSkillContext({ declared = [], recommendedChain = null, registry = null } = {}) { const dec = declared.length ? declared.join(', ') : '(не объявлены)'; const rec = recommendedChain === null ? '(рекомендация роутера недоступна — НЕ заворачивай за скилы)' - : (Array.isArray(recommendedChain) && recommendedChain.length ? recommendedChain.join(', ') : '(роутер ничего не порекомендовал)'); + : (Array.isArray(recommendedChain) && recommendedChain.length + ? recommendedChain.map((id) => resolveNodeName(id, registry)).join(', ') + : '(роутер ничего не порекомендовал)'); return `--- СКИЛЫ ---\nОбъявлены в плане: ${dec}\nРекомендация роутера: ${rec}\n` + 'Оцени уместность выбора скилов; неуместный/неполный выбор → decision="NO-GO" + что добавить/убрать.'; } diff --git a/tools/mentor-seam.test.mjs b/tools/mentor-seam.test.mjs index 93d46143..2a500378 100644 --- a/tools/mentor-seam.test.mjs +++ b/tools/mentor-seam.test.mjs @@ -15,6 +15,23 @@ describe('renderSkillContext (мерж роутер↔наставник)', () = }); }); +describe('renderSkillContext — резолв ID→имя (фикс)', () => { + const registry = { indexById: new Map([['#4', { id: '#4', name: 'markdownlint-cli2', slug: 'markdownlint' }]]) }; + it('с реестром резолвит #4 → имя', () => { + const s = renderSkillContext({ declared: ['executing-plans'], recommendedChain: ['#4'], registry }); + expect(s).toMatch(/#4 — markdownlint-cli2/); + }); + it('без реестра → голый ID (fail-safe)', () => { + const s = renderSkillContext({ declared: ['x'], recommendedChain: ['#4'] }); + expect(s).toMatch(/#4/); + expect(s).not.toMatch(/markdownlint/); + }); + it('неизвестный ID → голый ID (не падает)', () => { + const s = renderSkillContext({ declared: ['x'], recommendedChain: ['#999'], registry }); + expect(s).toMatch(/#999/); + }); +}); + describe('buildMentorPrompt (§6.2)', () => { const graphSection = { kind: 'project-graph', districtCount: 2, layer0: [{ district: 'tools', nodeCount: 3 }, { district: 'app-backend', nodeCount: 7 }], staleness: { stale: true, commits_behind: 12, uncommitted: 1 } }; const verifiedContext = [{ id: '1', claim: 'есть runRouter', ref: 'router-engine.mjs:140', kind: 'EXTRACTED' }]; diff --git a/tools/on-plan-write.mjs b/tools/on-plan-write.mjs index 2d64f665..e6ba3be9 100644 --- a/tools/on-plan-write.mjs +++ b/tools/on-plan-write.mjs @@ -52,7 +52,7 @@ export async function onPlanWrite({ recommendedChain = (c && c.recommended_chain) || (c && c.recommended_node ? [c.recommended_node] : []); } catch { recommendedChain = null; } } - const skillContext = renderSkillContext({ declared: declaredSkills, recommendedChain }); + const skillContext = renderSkillContext({ declared: declaredSkills, recommendedChain, registry }); // Производитель вердикта (C T5b): сбой → ok:false/wired:false (SE-R6-6, не суд). const r = await runMentorVerdictImpl({ plan: { steps: planSteps }, planHash, verifiedContext, negotiationLog, graphSection, skillContext, llmCall, diff --git a/tools/on-plan-write.test.mjs b/tools/on-plan-write.test.mjs index b828c838..7122a444 100644 --- a/tools/on-plan-write.test.mjs +++ b/tools/on-plan-write.test.mjs @@ -84,4 +84,19 @@ describe('onPlanWrite — скил-сверка через classify (мерж)', expect(r.ok).toBe(true); expect(capturedUser).toMatch(/недоступн/i); }); + + it('резолвит ID рекомендации в имя через registry (фикс)', async () => { + let capturedUser = null; + const registry = { indexById: new Map([['#4', { id: '#4', name: 'markdownlint-cli2' }]]) }; + await onPlanWrite({ + planSteps: [{ n: 1, op: 'Edit', object: 'x', ref: 'D1' }], + declaredSkills: ['executing-plans'], + classifyImpl: async () => ({ recommended_chain: ['#4'] }), + registry, + llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return GO; }, + planGoal: 'правка .md', + }); + expect(capturedUser).toMatch(/markdownlint-cli2/); + expect(capturedUser).not.toMatch(/#4(?! — )/); // голого #4 без имени нет + }); });