diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index 22bdc3f..5b49063 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -2651,7 +2651,23 @@ Implemented in current pass (Stage 4.1 answer-layer contract rollout, 2026-04-12 - focused Stage 4 answer pack: `5 files / 30 tests passed`; - `npm --prefix llm_normalizer/backend run build` passed. -Status: In progress (Stage 4.1 completed; continue with quality loop on real runs/manual comments) +Implemented in current pass (Stage 4.2 boundary fallback contract alignment, 2026-04-12): +1. Reworked boundary fallback replies to the same Stage 4 human-centric contract shape: + - `Коротко` + - `Что именно проверено` + - `Что найдено` + - `Что пока не доказано` + - `Что могу сделать сейчас` +2. Preserved fallback semantics and safety behavior: + - soft refusal and clarification flow remain deterministic; + - no route/runtime contract changes. +3. Updated boundary regression assertions to the unified Stage 4 answer shape: + - `assistantBoundaryFallbackReply`. +4. Validation snapshot: + - focused Stage 4 pack passed: `4 files / 16 tests` (`assistantBoundaryFallbackReply`, `assistantSoftPolicyReply`, `assistantAnswerPolicyV11`, `assistantWave17RunRegression20260411`); + - `npm --prefix llm_normalizer/backend run build` passed. + +Status: In progress (Stage 4.1-4.2 completed; continue with quality loop on real runs/manual comments) ## Stage 5 (P3): Quality Loop Driven By GUI Markup diff --git a/llm_normalizer/backend/dist/services/answerComposer.js b/llm_normalizer/backend/dist/services/answerComposer.js index 7ca23b3..edb8caf 100644 --- a/llm_normalizer/backend/dist/services/answerComposer.js +++ b/llm_normalizer/backend/dist/services/answerComposer.js @@ -1635,6 +1635,13 @@ function buildBoundaryQuickActionLine(capabilities) { } return `Что могу сделать сейчас: ${actions.join("; ")}.`; } +function buildBoundaryQuickActionItems(capabilities) { + const actions = capabilities + .slice(0, 3) + .map((item) => item.replace(/:\s*/u, " — ").trim()) + .filter((item) => item.length > 0); + return uniqueStrings(actions, 3); +} function buildNaturalClarificationHints(input) { const hints = []; if (input.missingAnchors.period) { @@ -1686,16 +1693,27 @@ function shouldUseBoundaryFallbackReply(input) { function buildBoundaryFallbackReply(input) { const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3); const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities); + const quickActionItems = buildBoundaryQuickActionItems(nearbyCapabilities); if (input.focusDomain === null) { const heading = pickDeterministicBoundaryVariant(input.userMessage, [ "По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.", "Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички." ]); return sanitizeUserFacingReply([ - heading, - nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "", - quickActionLine ?? "", - "Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С." + `Коротко: ${ensureSentence(heading)}`, + `Что именно проверено:\n${formatList([ + "Проверен доступный контур 1С и возможность маршрутизации запроса.", + "Надежный доменный путь для текущей формулировки не подтвержден." + ])}`, + `Что найдено:\n${formatList(nearbyCapabilities.length > 0 + ? nearbyCapabilities + : ["Близких поддерживаемых сценариев по текущей формулировке не найдено."])}`, + `Что пока не доказано:\n${formatList([ + "Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий." + ])}`, + `Что могу сделать сейчас:\n${formatList(quickActionItems.length > 0 + ? quickActionItems + : [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."])}` ] .filter(Boolean) .join("\n\n")); @@ -1709,10 +1727,20 @@ function buildBoundaryFallbackReply(input) { `По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.` ]); return sanitizeUserFacingReply([ - domainHeading, - clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "", - nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : "", - quickActionLine ?? "" + `Коротко: ${ensureSentence(domainHeading)}`, + `Что именно проверено:\n${formatList([ + `Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`, + "Оценена достаточность подтвержденной опоры для прямого вывода." + ])}`, + `Что найдено:\n${formatList(nearbyCapabilities.length > 0 + ? nearbyCapabilities.slice(0, 2) + : ["Подтвержденных близких сценариев в текущем проходе не найдено."])}`, + `Что пока не доказано:\n${formatList(clarificationHints.length > 0 + ? clarificationHints + : ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."])}`, + `Что могу сделать сейчас:\n${formatList(quickActionItems.length > 0 + ? quickActionItems + : [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."])}` ] .filter(Boolean) .join("\n\n")); diff --git a/llm_normalizer/backend/src/services/answerComposer.ts b/llm_normalizer/backend/src/services/answerComposer.ts index 658990f..ffe94d6 100644 --- a/llm_normalizer/backend/src/services/answerComposer.ts +++ b/llm_normalizer/backend/src/services/answerComposer.ts @@ -1932,6 +1932,14 @@ function buildBoundaryQuickActionLine(capabilities: string[]): string | null { return `Что могу сделать сейчас: ${actions.join("; ")}.`; } +function buildBoundaryQuickActionItems(capabilities: string[]): string[] { + const actions = capabilities + .slice(0, 3) + .map((item) => item.replace(/:\s*/u, " — ").trim()) + .filter((item) => item.length > 0); + return uniqueStrings(actions, 3); +} + function buildNaturalClarificationHints(input: { missingAnchors: MissingAnchors; coverageReport: RequirementCoverageReport; @@ -2006,6 +2014,7 @@ function buildBoundaryFallbackReply(input: { }): string { const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3); const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities); + const quickActionItems = buildBoundaryQuickActionItems(nearbyCapabilities); if (input.focusDomain === null) { const heading = pickDeterministicBoundaryVariant( input.userMessage, @@ -2016,10 +2025,24 @@ function buildBoundaryFallbackReply(input: { ); return sanitizeUserFacingReply( [ - heading, - nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "", - quickActionLine ?? "", - "Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С." + `Коротко: ${ensureSentence(heading)}`, + `Что именно проверено:\n${formatList([ + "Проверен доступный контур 1С и возможность маршрутизации запроса.", + "Надежный доменный путь для текущей формулировки не подтвержден." + ])}`, + `Что найдено:\n${formatList( + nearbyCapabilities.length > 0 + ? nearbyCapabilities + : ["Близких поддерживаемых сценариев по текущей формулировке не найдено."] + )}`, + `Что пока не доказано:\n${formatList([ + "Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий." + ])}`, + `Что могу сделать сейчас:\n${formatList( + quickActionItems.length > 0 + ? quickActionItems + : [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."] + )}` ] .filter(Boolean) .join("\n\n") @@ -2039,10 +2062,26 @@ function buildBoundaryFallbackReply(input: { ); return sanitizeUserFacingReply( [ - domainHeading, - clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "", - nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : "", - quickActionLine ?? "" + `Коротко: ${ensureSentence(domainHeading)}`, + `Что именно проверено:\n${formatList([ + `Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`, + "Оценена достаточность подтвержденной опоры для прямого вывода." + ])}`, + `Что найдено:\n${formatList( + nearbyCapabilities.length > 0 + ? nearbyCapabilities.slice(0, 2) + : ["Подтвержденных близких сценариев в текущем проходе не найдено."] + )}`, + `Что пока не доказано:\n${formatList( + clarificationHints.length > 0 + ? clarificationHints + : ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."] + )}`, + `Что могу сделать сейчас:\n${formatList( + quickActionItems.length > 0 + ? quickActionItems + : [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."] + )}` ] .filter(Boolean) .join("\n\n") diff --git a/llm_normalizer/backend/tests/assistantBoundaryFallbackReply.test.ts b/llm_normalizer/backend/tests/assistantBoundaryFallbackReply.test.ts index f380ee3..3f46f08 100644 --- a/llm_normalizer/backend/tests/assistantBoundaryFallbackReply.test.ts +++ b/llm_normalizer/backend/tests/assistantBoundaryFallbackReply.test.ts @@ -53,7 +53,9 @@ describe("assistant boundary fallback reply", () => { expect(output.reply_type).toBe("clarification_required"); expect(output.assistant_reply).toMatch(/мягкий отказ/i); - expect(output.assistant_reply).toContain("Что могу сделать рядом по смыслу:"); + expect(output.assistant_reply).toContain("Что именно проверено:"); + expect(output.assistant_reply).toContain("Что найдено:"); + expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).toContain("Что могу сделать сейчас:"); expect(output.assistant_reply).not.toContain("Что сломано:"); }); @@ -78,8 +80,9 @@ describe("assistant boundary fallback reply", () => { expect(output.reply_type).toBe("clarification_required"); expect(output.assistant_reply).toMatch(/не могу надежно ответить по сценарию|не хватает подтвержденной опоры/i); - expect(output.assistant_reply).toContain("Чтобы сразу перейти к проверке, уточни:"); - expect(output.assistant_reply).toContain("Если удобнее, могу начать с близкого сценария:"); + expect(output.assistant_reply).toContain("Что именно проверено:"); + expect(output.assistant_reply).toContain("Что найдено:"); + expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).toContain("Что могу сделать сейчас:"); expect(output.assistant_reply).not.toContain("Что сломано:"); }); @@ -161,8 +164,9 @@ describe("assistant boundary fallback reply", () => { }); expect(output.reply_type).toBe("partial_coverage"); - expect(output.assistant_reply).toContain("Что могу сделать рядом по смыслу:"); + expect(output.assistant_reply).toContain("Что найдено:"); + expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).toContain("Что могу сделать сейчас:"); expect(output.assistant_reply).not.toContain("Что сломано:"); }); -}); \ No newline at end of file +});