fix(mentor): резолв ID узлов в имена в скил-контексте наставника
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) <noreply@anthropic.com>
This commit is contained in:
+14
-3
@@ -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" + что добавить/убрать.';
|
||||
}
|
||||
|
||||
@@ -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' }];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 без имени нет
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user