ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг - Этап 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
- 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/.

View File

@ -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

View File

@ -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"));

View File

@ -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")

View File

@ -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;

View File

@ -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("Что пока не доказано:");
});
});
});

View File

@ -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

View File

@ -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("Опора достаточна для первичного вывода.");
});

View File

@ -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);
}