ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг - Этап 4: обновлён формат ответов и добавлено правило о русском названии коммита

This commit is contained in:
dctouch 2026-04-12 01:10:08 +03:00
parent 8167bd228d
commit 5a8edfb4f3
9 changed files with 119 additions and 37 deletions

View File

@ -1,6 +1,9 @@
## encoding_rule ## 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. - 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 ## graphify
This project has a graphify knowledge graph at graphify-out/. This project has a graphify knowledge graph at graphify-out/.

View File

@ -2629,7 +2629,29 @@ Stage 4 kickoff preconditions:
2. No route/MCP interface changes are required to start Stage 4. 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. 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 ## Stage 5 (P3): Quality Loop Driven By GUI Markup

View File

@ -3649,13 +3649,22 @@ function renderPolicyReply(structure, context) {
limitationLines limitationLines
}; };
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context); 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([ return sanitizeUserFacingReply([
`Коротко: ${enriched.shortLine}`, `Коротко: ${enriched.shortLine}`,
`Что сломано:\n${formatList(enriched.brokenLines)}`, `Что именно проверено:\n${formatList(checkedLines.length > 0
`Почему это похоже на проблему:\n${formatList(enriched.whyLines)}`, ? checkedLines
`На чем это основано:\n${formatList(enriched.evidenceLines)}`, : ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."])}`,
`Что проверить первым:\n${formatList(enriched.checkLines)}`, `Что найдено:\n${formatList(foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."])}`,
`Ограничения:\n${formatList(enriched.limitationLines)}` `Что пока не доказано:\n${formatList(unresolvedLines.length > 0
? unresolvedLines
: ["Существенных ограничений в текущем срезе не выявлено."])}`,
`Что проверить первым:\n${formatList(nextStepLines.length > 0
? nextStepLines
: ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."])}`
] ]
.filter(Boolean) .filter(Boolean)
.join("\n\n")); .join("\n\n"));
@ -3687,6 +3696,7 @@ function shouldUseSoftPolicyReply(input) {
function renderSoftPolicyReply(input) { function renderSoftPolicyReply(input) {
const questionType = input.context?.questionType ?? "unknown"; const questionType = input.context?.questionType ?? "unknown";
const shortLine = ensureSentence(buildShortSectionLine(input.structure)); 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 evidenceLines = dedupeNarrativeLines(buildEvidenceSectionLines(input.structure, questionType, input.context), 3);
const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3); const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3);
const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3); const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3);
@ -3700,9 +3710,16 @@ function renderSoftPolicyReply(input) {
return sanitizeUserFacingReply([ return sanitizeUserFacingReply([
`Коротко: ${shortLine}`, `Коротко: ${shortLine}`,
modeLine, modeLine,
evidenceLines.length > 0 ? `Что уже проверено: ${evidenceLines.join("; ")}` : "", `Что именно проверено:\n${formatList(evidenceLines.length > 0
limitationLines.length > 0 ? `Что пока не доказано: ${limitationLines.join("; ")}` : "", ? evidenceLines
actionLines.length > 0 ? `Что могу сделать сейчас: ${actionLines.join("; ")}` : "" : ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."])}`,
`Что найдено:\n${formatList(foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."])}`,
`Что пока не доказано:\n${formatList(limitationLines.length > 0
? limitationLines
: ["Для полного вывода не хватает деталей по части требований."])}`,
`Что могу сделать сейчас:\n${formatList(actionLines.length > 0
? actionLines
: ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."])}`
] ]
.filter(Boolean) .filter(Boolean)
.join("\n\n")); .join("\n\n"));

View File

@ -4338,15 +4338,32 @@ function renderPolicyReply(structure: AnswerStructureV11, context?: AnswerRender
limitationLines limitationLines
}; };
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context); 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( return sanitizeUserFacingReply(
[ [
`Коротко: ${enriched.shortLine}`, `Коротко: ${enriched.shortLine}`,
`Что сломано:\n${formatList(enriched.brokenLines)}`, `Что именно проверено:\n${formatList(
`Почему это похоже на проблему:\n${formatList(enriched.whyLines)}`, checkedLines.length > 0
`На чем это основано:\n${formatList(enriched.evidenceLines)}`, ? checkedLines
`Что проверить первым:\n${formatList(enriched.checkLines)}`, : ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."]
`Ограничения:\n${formatList(enriched.limitationLines)}` )}`,
`Что найдено:\n${formatList(
foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."]
)}`,
`Что пока не доказано:\n${formatList(
unresolvedLines.length > 0
? unresolvedLines
: ["Существенных ограничений в текущем срезе не выявлено."]
)}`,
`Что проверить первым:\n${formatList(
nextStepLines.length > 0
? nextStepLines
: ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."]
)}`
] ]
.filter(Boolean) .filter(Boolean)
.join("\n\n") .join("\n\n")
@ -4394,6 +4411,10 @@ function renderSoftPolicyReply(input: {
}): string { }): string {
const questionType = input.context?.questionType ?? "unknown"; const questionType = input.context?.questionType ?? "unknown";
const shortLine = ensureSentence(buildShortSectionLine(input.structure)); 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 evidenceLines = dedupeNarrativeLines(buildEvidenceSectionLines(input.structure, questionType, input.context), 3);
const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3); const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3);
const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3); const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3);
@ -4412,9 +4433,24 @@ function renderSoftPolicyReply(input: {
[ [
`Коротко: ${shortLine}`, `Коротко: ${shortLine}`,
modeLine, modeLine,
evidenceLines.length > 0 ? `Что уже проверено: ${evidenceLines.join("; ")}` : "", `Что именно проверено:\n${formatList(
limitationLines.length > 0 ? `Что пока не доказано: ${limitationLines.join("; ")}` : "", evidenceLines.length > 0
actionLines.length > 0 ? `Что могу сделать сейчас: ${actionLines.join("; ")}` : "" ? evidenceLines
: ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."]
)}`,
`Что найдено:\n${formatList(
foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."]
)}`,
`Что пока не доказано:\n${formatList(
limitationLines.length > 0
? limitationLines
: ["Для полного вывода не хватает деталей по части требований."]
)}`,
`Что могу сделать сейчас:\n${formatList(
actionLines.length > 0
? actionLines
: ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."]
)}`
] ]
.filter(Boolean) .filter(Boolean)
.join("\n\n") .join("\n\n")

View File

@ -99,7 +99,7 @@ describe.sequential("assistant answer policy v1.1", () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(["partial_coverage", "factual_with_explanation", "factual"]).toContain(response.body.reply_type); 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); expect(String(response.body.assistant_reply)).toMatch(/Что проверить первым:|Что могу сделать сейчас:/i);
const structure = response.body.debug?.answer_structure_v11; const structure = response.body.debug?.answer_structure_v11;

View File

@ -174,7 +174,7 @@ describe("assistant soft policy reply", () => {
}); });
expect(output.reply_type).toBe("factual_with_explanation"); 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("Что пока не доказано:");
}); });
}); });

View File

@ -461,7 +461,7 @@ describe("wave10 settlement corrective regression", () => {
buildRetrieval({ requirementId: "R2", status: "empty" }) 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] ?? ""; const checksSection = checksSectionMatch?.[1] ?? "";
expect(checksSection).toMatch(/договор|регистр|зачет|зачёт|60\/62/i); expect(checksSection).toMatch(/договор|регистр|зачет|зачёт|60\/62/i);
const firstLine = checksSection const firstLine = checksSection

View File

@ -310,7 +310,7 @@ describe("wave12 vat/month-close consistency + confidence reconciliation", () =>
}); });
expect(output.reply_type).toBe("clarification_required"); expect(output.reply_type).toBe("clarification_required");
expect(output.assistant_reply).toContain("Ограничения:"); expect(output.assistant_reply).toContain("Что пока не доказано:");
expect(output.assistant_reply).not.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.reply_type).toBe("clarification_required");
expect(output.assistant_reply).toContain("Ограничения:"); expect(output.assistant_reply).toContain("Что пока не доказано:");
expect(output.assistant_reply).not.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.answer_structure_v11?.mechanism_block?.status).toBe("limited");
expect(output.assistant_reply).toContain("Ограничения:"); expect(output.assistant_reply).toContain("Что пока не доказано:");
expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода."); expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода.");
}); });

View File

@ -213,11 +213,11 @@ function extractSection(text: string, title: string): string {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const stopTitles = [ const stopTitles = [
"Коротко", "Коротко",
"Что сломано", "Что именно проверено",
"Почему это похоже на проблему", "Что найдено",
"На чем это основано", "Что пока не доказано",
"Что проверить первым", "Что проверить первым",
"Ограничения" "Что могу сделать сейчас"
]; ];
const stopPattern = stopTitles.map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); const stopPattern = stopTitles.map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
const re = new RegExp(`${escaped}:([\\s\\S]*?)(?=(?:${stopPattern}):|$)`, "i"); 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", () => { 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 units = [buildProblemUnit({ id: "pu-1", type: "broken_chain_segment", defect: "failed_edge:payment_to_settlement", account: "60" })];
const output = composeCase("Проверь по 60 счету, где разрыв.", buildRetrieval(units)); const output = composeCase("Проверь по 60 счету, где разрыв.", buildRetrieval(units));
const brokenSection = extractSection(output.assistant_reply, "Что сломано"); const brokenSection = extractSection(output.assistant_reply, "Что найдено");
expect(brokenSection).toMatch(/не подтвержден|разрыв|зависл|закрыти/i); expect(brokenSection).toMatch(/не подтвержден|разрыв|зависл|закрыти/i);
expect(brokenSection).not.toMatch(/^\s*-\s*(Document|Record|Entity)\b/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" }) buildProblemUnit({ id: "pu-2", type: "unresolved_settlement_cluster", defect: "payment_to_settlement", account: "60" })
]; ];
const output = composeCase("Проверь хвост по расчетам.", buildRetrieval(units)); const output = composeCase("Проверь хвост по расчетам.", buildRetrieval(units));
const brokenSection = extractSection(output.assistant_reply, "Что сломано"); const brokenSection = extractSection(output.assistant_reply, "Что найдено");
const bulletLines = brokenSection const bulletLines = brokenSection
.split(/\r?\n/g) .split(/\r?\n/g)
.map((line) => line.trim()) .map((line) => line.trim())
.filter((line) => line.startsWith("- ")); .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", () => { 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 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 output = composeCase("Проверь по 97 счету зависание списания.", buildRetrieval(units));
const limitationsSection = extractSection(output.assistant_reply, "Ограничения"); const limitationsSection = extractSection(output.assistant_reply, "Что пока не доказано");
expect(limitationsSection).toMatch(/период/i); expect(limitationsSection).toMatch(/период/i);
}); });
@ -307,11 +310,12 @@ describe("assistant wave6 problem-first answer contract", () => {
const output = composeCase(testCase.message, testCase.retrieval); const output = composeCase(testCase.message, testCase.retrieval);
expect(output.assistant_reply).toMatch(testCase.domainHint); 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).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).toContain("Ограничения:");
expect(output.assistant_reply.length).toBeLessThan(1800); expect(output.assistant_reply.length).toBeLessThan(1800);
expect(output.assistant_reply).not.toMatch(/graph_|domain_scope|relation_patterns|semantic_profile|route|profile/i); expect(output.assistant_reply).not.toMatch(/graph_|domain_scope|relation_patterns|semantic_profile|route|profile/i);
} }