From 5a8edfb4f3ea7536e6ec0a6283ee21d856f5c323 Mon Sep 17 00:00:00 2001 From: dctouch Date: Sun, 12 Apr 2026 01:10:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=9B=D0=9E=D0=91=D0=90=D0=9B=D0=AC?= =?UTF-8?q?=D0=9D=D0=AB=D0=99=20=D0=A0=D0=95=D0=A4=D0=90=D0=9A=D0=A2=D0=9E?= =?UTF-8?q?=D0=A0=D0=98=D0=9D=D0=93=20=D0=90=D0=A0=D0=A5=D0=98=D0=A2=D0=95?= =?UTF-8?q?=D0=9A=D0=A2=D0=A3=D0=A0=D0=AB=20-=20=D0=A0=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20-=20=D0=AD=D1=82?= =?UTF-8?q?=D0=B0=D0=BF=204:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D1=91?= =?UTF-8?q?=D0=BD=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=D0=BE=20=D0=BE=20=D1=80=D1=83=D1=81=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 3 ++ docs/TECH/1CLLMARCH-FACT.md | 24 ++++++++- .../backend/dist/services/answerComposer.js | 33 +++++++++--- .../backend/src/services/answerComposer.ts | 52 ++++++++++++++++--- .../tests/assistantAnswerPolicyV11.test.ts | 2 +- .../tests/assistantSoftPolicyReply.test.ts | 6 +-- ...ve10SettlementCorrectiveRegression.test.ts | 2 +- ...VatMonthCloseConsistencyRegression.test.ts | 6 +-- ...antWave6ProblemFirstAnswerContract.test.ts | 28 +++++----- 9 files changed, 119 insertions(+), 37 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 58decb8..bb79ee1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,9 @@ ## encoding_rule - All source/code/config/docs files must be saved and edited in UTF-8 without BOM; never write mojibake placeholders or replacement characters. +## commit_message_rule +- After applying fixes, always provide the user with a ready commit title in Russian. + ## graphify This project has a graphify knowledge graph at graphify-out/. diff --git a/docs/TECH/1CLLMARCH-FACT.md b/docs/TECH/1CLLMARCH-FACT.md index 531318f..22bdc3f 100644 --- a/docs/TECH/1CLLMARCH-FACT.md +++ b/docs/TECH/1CLLMARCH-FACT.md @@ -2629,7 +2629,29 @@ Stage 4 kickoff preconditions: 2. No route/MCP interface changes are required to start Stage 4. 3. Remaining acknowledged risk: domain coverage is still limited and will be expanded iteratively. -Status: Ready to start (kickoff approved on 2026-04-11) +Implemented in current pass (Stage 4.1 answer-layer contract rollout, 2026-04-12): +1. Reworked Stage 1.1 policy renderer to Stage 4 human-centric 5-block response contract: + - `Коротко` + - `Что именно проверено` + - `Что найдено` + - `Что пока не доказано` + - `Что проверить первым` +2. Reworked weak-envelope soft policy renderer to the same contract shape with strict uncertainty and actionable next-step blocks. +3. Preserved route/runtime behavior: + - no MCP route changes; + - no transport/schema refactor; + - claim/evidence contract (`answer_structure_v11`) remains source of truth. +4. Updated regression expectations to Stage 4 response shape: + - `assistantSoftPolicyReply` + - `assistantWave6ProblemFirstAnswerContract` + - `assistantWave10SettlementCorrectiveRegression` + - `assistantWave12VatMonthCloseConsistencyRegression` + - `assistantAnswerPolicyV11` +5. Validation snapshot: + - 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) ## 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 a0a55aa..7ca23b3 100644 --- a/llm_normalizer/backend/dist/services/answerComposer.js +++ b/llm_normalizer/backend/dist/services/answerComposer.js @@ -3649,13 +3649,22 @@ function renderPolicyReply(structure, context) { limitationLines }; const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context); + const checkedLines = dedupeNarrativeLines(enriched.evidenceLines, 6); + const foundLines = dedupeNarrativeLines([...enriched.brokenLines, ...enriched.whyLines], 6); + const unresolvedLines = dedupeNarrativeLines(enriched.limitationLines, 6); + const nextStepLines = dedupeNarrativeLines(enriched.checkLines, 5); return sanitizeUserFacingReply([ `Коротко: ${enriched.shortLine}`, - `Что сломано:\n${formatList(enriched.brokenLines)}`, - `Почему это похоже на проблему:\n${formatList(enriched.whyLines)}`, - `На чем это основано:\n${formatList(enriched.evidenceLines)}`, - `Что проверить первым:\n${formatList(enriched.checkLines)}`, - `Ограничения:\n${formatList(enriched.limitationLines)}` + `Что именно проверено:\n${formatList(checkedLines.length > 0 + ? checkedLines + : ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."])}`, + `Что найдено:\n${formatList(foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."])}`, + `Что пока не доказано:\n${formatList(unresolvedLines.length > 0 + ? unresolvedLines + : ["Существенных ограничений в текущем срезе не выявлено."])}`, + `Что проверить первым:\n${formatList(nextStepLines.length > 0 + ? nextStepLines + : ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."])}` ] .filter(Boolean) .join("\n\n")); @@ -3687,6 +3696,7 @@ function shouldUseSoftPolicyReply(input) { function renderSoftPolicyReply(input) { const questionType = input.context?.questionType ?? "unknown"; const shortLine = ensureSentence(buildShortSectionLine(input.structure)); + const foundLines = dedupeNarrativeLines([...buildBrokenSectionLines(input.structure), ...buildWhySectionLines(input.structure, input.context)], 3); const evidenceLines = dedupeNarrativeLines(buildEvidenceSectionLines(input.structure, questionType, input.context), 3); const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3); const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3); @@ -3700,9 +3710,16 @@ function renderSoftPolicyReply(input) { return sanitizeUserFacingReply([ `Коротко: ${shortLine}`, modeLine, - evidenceLines.length > 0 ? `Что уже проверено: ${evidenceLines.join("; ")}` : "", - limitationLines.length > 0 ? `Что пока не доказано: ${limitationLines.join("; ")}` : "", - actionLines.length > 0 ? `Что могу сделать сейчас: ${actionLines.join("; ")}` : "" + `Что именно проверено:\n${formatList(evidenceLines.length > 0 + ? evidenceLines + : ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."])}`, + `Что найдено:\n${formatList(foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."])}`, + `Что пока не доказано:\n${formatList(limitationLines.length > 0 + ? limitationLines + : ["Для полного вывода не хватает деталей по части требований."])}`, + `Что могу сделать сейчас:\n${formatList(actionLines.length > 0 + ? actionLines + : ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."])}` ] .filter(Boolean) .join("\n\n")); diff --git a/llm_normalizer/backend/src/services/answerComposer.ts b/llm_normalizer/backend/src/services/answerComposer.ts index 17f16a1..658990f 100644 --- a/llm_normalizer/backend/src/services/answerComposer.ts +++ b/llm_normalizer/backend/src/services/answerComposer.ts @@ -4338,15 +4338,32 @@ function renderPolicyReply(structure: AnswerStructureV11, context?: AnswerRender limitationLines }; const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context); + const checkedLines = dedupeNarrativeLines(enriched.evidenceLines, 6); + const foundLines = dedupeNarrativeLines([...enriched.brokenLines, ...enriched.whyLines], 6); + const unresolvedLines = dedupeNarrativeLines(enriched.limitationLines, 6); + const nextStepLines = dedupeNarrativeLines(enriched.checkLines, 5); return sanitizeUserFacingReply( [ `Коротко: ${enriched.shortLine}`, - `Что сломано:\n${formatList(enriched.brokenLines)}`, - `Почему это похоже на проблему:\n${formatList(enriched.whyLines)}`, - `На чем это основано:\n${formatList(enriched.evidenceLines)}`, - `Что проверить первым:\n${formatList(enriched.checkLines)}`, - `Ограничения:\n${formatList(enriched.limitationLines)}` + `Что именно проверено:\n${formatList( + checkedLines.length > 0 + ? checkedLines + : ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."] + )}`, + `Что найдено:\n${formatList( + foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."] + )}`, + `Что пока не доказано:\n${formatList( + unresolvedLines.length > 0 + ? unresolvedLines + : ["Существенных ограничений в текущем срезе не выявлено."] + )}`, + `Что проверить первым:\n${formatList( + nextStepLines.length > 0 + ? nextStepLines + : ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."] + )}` ] .filter(Boolean) .join("\n\n") @@ -4394,6 +4411,10 @@ function renderSoftPolicyReply(input: { }): string { const questionType = input.context?.questionType ?? "unknown"; const shortLine = ensureSentence(buildShortSectionLine(input.structure)); + const foundLines = dedupeNarrativeLines( + [...buildBrokenSectionLines(input.structure), ...buildWhySectionLines(input.structure, input.context)], + 3 + ); const evidenceLines = dedupeNarrativeLines(buildEvidenceSectionLines(input.structure, questionType, input.context), 3); const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3); const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3); @@ -4412,9 +4433,24 @@ function renderSoftPolicyReply(input: { [ `Коротко: ${shortLine}`, modeLine, - evidenceLines.length > 0 ? `Что уже проверено: ${evidenceLines.join("; ")}` : "", - limitationLines.length > 0 ? `Что пока не доказано: ${limitationLines.join("; ")}` : "", - actionLines.length > 0 ? `Что могу сделать сейчас: ${actionLines.join("; ")}` : "" + `Что именно проверено:\n${formatList( + evidenceLines.length > 0 + ? evidenceLines + : ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."] + )}`, + `Что найдено:\n${formatList( + foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."] + )}`, + `Что пока не доказано:\n${formatList( + limitationLines.length > 0 + ? limitationLines + : ["Для полного вывода не хватает деталей по части требований."] + )}`, + `Что могу сделать сейчас:\n${formatList( + actionLines.length > 0 + ? actionLines + : ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."] + )}` ] .filter(Boolean) .join("\n\n") diff --git a/llm_normalizer/backend/tests/assistantAnswerPolicyV11.test.ts b/llm_normalizer/backend/tests/assistantAnswerPolicyV11.test.ts index 7e91764..779b48f 100644 --- a/llm_normalizer/backend/tests/assistantAnswerPolicyV11.test.ts +++ b/llm_normalizer/backend/tests/assistantAnswerPolicyV11.test.ts @@ -99,7 +99,7 @@ describe.sequential("assistant answer policy v1.1", () => { expect(response.status).toBe(200); expect(["partial_coverage", "factual_with_explanation", "factual"]).toContain(response.body.reply_type); - expect(String(response.body.assistant_reply)).toMatch(/не хватает|уточните|опорного ориентира|Ограничения:/i); + expect(String(response.body.assistant_reply)).toMatch(/не хватает|уточните|опорного ориентира|Что пока не доказано:/i); expect(String(response.body.assistant_reply)).toMatch(/Что проверить первым:|Что могу сделать сейчас:/i); const structure = response.body.debug?.answer_structure_v11; diff --git a/llm_normalizer/backend/tests/assistantSoftPolicyReply.test.ts b/llm_normalizer/backend/tests/assistantSoftPolicyReply.test.ts index 4bf707a..e496eeb 100644 --- a/llm_normalizer/backend/tests/assistantSoftPolicyReply.test.ts +++ b/llm_normalizer/backend/tests/assistantSoftPolicyReply.test.ts @@ -174,7 +174,7 @@ describe("assistant soft policy reply", () => { }); expect(output.reply_type).toBe("factual_with_explanation"); - expect(output.assistant_reply).toContain("Что сломано:"); - expect(output.assistant_reply).toContain("Ограничения:"); + expect(output.assistant_reply).toContain("Что найдено:"); + expect(output.assistant_reply).toContain("Что пока не доказано:"); }); -}); \ No newline at end of file +}); diff --git a/llm_normalizer/backend/tests/assistantWave10SettlementCorrectiveRegression.test.ts b/llm_normalizer/backend/tests/assistantWave10SettlementCorrectiveRegression.test.ts index 3ad8a05..440b56e 100644 --- a/llm_normalizer/backend/tests/assistantWave10SettlementCorrectiveRegression.test.ts +++ b/llm_normalizer/backend/tests/assistantWave10SettlementCorrectiveRegression.test.ts @@ -461,7 +461,7 @@ describe("wave10 settlement corrective regression", () => { buildRetrieval({ requirementId: "R2", status: "empty" }) ]); - const checksSectionMatch = output.assistant_reply.match(/Что проверить первым:\s*([\s\S]*?)\s*Ограничения:/i); + const checksSectionMatch = output.assistant_reply.match(/Что проверить первым:\s*([\s\S]*)$/i); const checksSection = checksSectionMatch?.[1] ?? ""; expect(checksSection).toMatch(/договор|регистр|зачет|зачёт|60\/62/i); const firstLine = checksSection diff --git a/llm_normalizer/backend/tests/assistantWave12VatMonthCloseConsistencyRegression.test.ts b/llm_normalizer/backend/tests/assistantWave12VatMonthCloseConsistencyRegression.test.ts index db19104..d94cc63 100644 --- a/llm_normalizer/backend/tests/assistantWave12VatMonthCloseConsistencyRegression.test.ts +++ b/llm_normalizer/backend/tests/assistantWave12VatMonthCloseConsistencyRegression.test.ts @@ -310,7 +310,7 @@ describe("wave12 vat/month-close consistency + confidence reconciliation", () => }); expect(output.reply_type).toBe("clarification_required"); - expect(output.assistant_reply).toContain("Ограничения:"); + expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода."); }); @@ -331,7 +331,7 @@ describe("wave12 vat/month-close consistency + confidence reconciliation", () => }); expect(output.reply_type).toBe("clarification_required"); - expect(output.assistant_reply).toContain("Ограничения:"); + expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода."); }); @@ -364,7 +364,7 @@ describe("wave12 vat/month-close consistency + confidence reconciliation", () => }); expect(output.answer_structure_v11?.mechanism_block?.status).toBe("limited"); - expect(output.assistant_reply).toContain("Ограничения:"); + expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода."); }); diff --git a/llm_normalizer/backend/tests/assistantWave6ProblemFirstAnswerContract.test.ts b/llm_normalizer/backend/tests/assistantWave6ProblemFirstAnswerContract.test.ts index f0195e2..728da98 100644 --- a/llm_normalizer/backend/tests/assistantWave6ProblemFirstAnswerContract.test.ts +++ b/llm_normalizer/backend/tests/assistantWave6ProblemFirstAnswerContract.test.ts @@ -213,11 +213,11 @@ function extractSection(text: string, title: string): string { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const stopTitles = [ "Коротко", - "Что сломано", - "Почему это похоже на проблему", - "На чем это основано", + "Что именно проверено", + "Что найдено", + "Что пока не доказано", "Что проверить первым", - "Ограничения" + "Что могу сделать сейчас" ]; const stopPattern = stopTitles.map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); const re = new RegExp(`${escaped}:([\\s\\S]*?)(?=(?:${stopPattern}):|$)`, "i"); @@ -238,7 +238,7 @@ describe("assistant wave6 problem-first answer contract", () => { it("keeps narrative mechanism-first and avoids entity-list direct answer", () => { const units = [buildProblemUnit({ id: "pu-1", type: "broken_chain_segment", defect: "failed_edge:payment_to_settlement", account: "60" })]; const output = composeCase("Проверь по 60 счету, где разрыв.", buildRetrieval(units)); - const brokenSection = extractSection(output.assistant_reply, "Что сломано"); + const brokenSection = extractSection(output.assistant_reply, "Что найдено"); expect(brokenSection).toMatch(/не подтвержден|разрыв|зависл|закрыти/i); expect(brokenSection).not.toMatch(/^\s*-\s*(Document|Record|Entity)\b/i); @@ -257,19 +257,22 @@ describe("assistant wave6 problem-first answer contract", () => { buildProblemUnit({ id: "pu-2", type: "unresolved_settlement_cluster", defect: "payment_to_settlement", account: "60" }) ]; const output = composeCase("Проверь хвост по расчетам.", buildRetrieval(units)); - const brokenSection = extractSection(output.assistant_reply, "Что сломано"); + const brokenSection = extractSection(output.assistant_reply, "Что найдено"); const bulletLines = brokenSection .split(/\r?\n/g) .map((line) => line.trim()) .filter((line) => line.startsWith("- ")); + const normalized = bulletLines.map((line) => line.replace(/\s+/g, " ").trim().toLowerCase()); + const dedupedCount = new Set(normalized).size; - expect(bulletLines.length).toBe(1); + expect(bulletLines.length).toBeGreaterThan(0); + expect(dedupedCount).toBe(bulletLines.length); }); it("shows explicit limitation when period is missing", () => { const units = [buildProblemUnit({ id: "pu-1", type: "lifecycle_anomaly_node", defect: "missing_expected_transition", account: "97", lifecycleDomain: "deferred_expense" })]; const output = composeCase("Проверь по 97 счету зависание списания.", buildRetrieval(units)); - const limitationsSection = extractSection(output.assistant_reply, "Ограничения"); + const limitationsSection = extractSection(output.assistant_reply, "Что пока не доказано"); expect(limitationsSection).toMatch(/период/i); }); @@ -307,11 +310,12 @@ describe("assistant wave6 problem-first answer contract", () => { const output = composeCase(testCase.message, testCase.retrieval); expect(output.assistant_reply).toMatch(testCase.domainHint); 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("Почему это похоже на проблему:"); + expect(output.assistant_reply).not.toContain("На чем это основано:"); + expect(output.assistant_reply).toContain("Что пока не доказано:"); expect(output.assistant_reply).toContain("Что проверить первым:"); - expect(output.assistant_reply).toContain("Ограничения:"); expect(output.assistant_reply.length).toBeLessThan(1800); expect(output.assistant_reply).not.toMatch(/graph_|domain_scope|relation_patterns|semantic_profile|route|profile/i); }