АРЧ АП11 - Архитектура после регресса: Архитектура: сделать confirmed payables и receivables snapshots business-first без нумерованного report framing

This commit is contained in:
dctouch 2026-04-18 16:09:55 +03:00
parent 49f291a14b
commit 53e9151d5a
5 changed files with 287 additions and 119 deletions

View File

@ -260,7 +260,16 @@ Latest continuity-authority convergence evidence after the current route pass:
- `list_contracts_by_counterparty`, `list_documents_by_contract`, `bank_operations_by_counterparty`, `bank_operations_by_contract`, and the generic factual-list fallback no longer leak `live address lane / catalog address lane` wording into the user-facing answer;
- these list replies now start with direct business-first leads and keep the selected rows below, which preserves factual usefulness without exposing internal routing labels;
- targeted utf8 header tests now explicitly protect against `lane` leakage in these list families;
- this is still not the end of shaping work: some long evidence-heavy replies and residual catalog-style blocks still need the same cleanup;
- the next human-answer-shaping cleanup pass is now applied to open-contract evidence-heavy replies:
- `list_open_contracts` and `open_contracts_confirmed_as_of_date` no longer open with numbered `Блок 1/2/3...` report framing and now start with direct business-first summaries;
- section headings are still structured, but they now read like user-facing guidance instead of an internal audit report, while keeping the same factual slices and evidence detail below;
- targeted open-contract tests now protect the no-`Блок 1` top-block shape, so future contour work cannot silently bring the report framing back;
- the next human-answer-shaping cleanup pass is now applied to confirmed debt snapshots:
- `payables_confirmed_as_of_date` and `receivables_confirmed_as_of_date` now open with business-first `Коротко: ...` summaries instead of numbered report framing;
- debt snapshot sections now keep the same factual structure, but top-level headings are user-facing (`Что учтено`, `Сводка`, `Категории...`) rather than `Блок 1/2/3...`;
- direct compose tests now protect the no-`Блок 1` top-block shape for both confirmed debt families;
- isolated runtime proof for the `payables_confirmed_as_of_date` `tryHandle` path still needs a wider rerun, because the narrow harness invocation currently returns `undefined` before semantic assertions and therefore is not reliable evidence for this shaping pass by itself;
- this is still not the end of shaping work: heuristic debt shortlists and some residual catalog-style blocks still need the same cleanup;
- this pass does not yet finish full single-owner continuity, but it narrows one of the remaining seams where route arbitration and scope memory could disagree about whether the session was still grounded.
## Next Execution Slice (2026-04-18)

View File

@ -2774,20 +2774,19 @@ function composeFactualReplyBody(intent, rows, options = {}) {
return `${index + 1}. ${item.contract} | контрагент: ${counterpartyLabel} | подтвержденный открытый остаток: ${formatMoneyRub(item.confirmedAmount)} | тип остатка: ${openContractSettlementKindLabel(item.settlementKind)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${accountsLabel}${evidenceLabel}${refsLabel}${specialReasonLabel}`;
});
const lines = [
`Собран подтвержденный срез открытых договоров на ${formatDateRu(asOfDate)}.`,
`Чистый коммерческий остаток: ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
`Брутто коммерческих компонентов: ${formatMoneyRub(commercialGrossTotal)}.`,
`Специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
`Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
`Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
`Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`,
`Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
`Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
"- База ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.",
"- Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов.",
"- Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка."
"Что это значит",
"- Это подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
"- Основа ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.",
"- По каждому договору показаны чистый остаток и состав по типам открытых расчетов.",
"- Одна строка = один договор, один контрагент и один тип открытого остатка."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push("Что учтено");
lines.push(`- Дата среза: ${formatDateRu(asOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
@ -2796,7 +2795,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push("- Контур: остатки по счетам 60/62/76.");
lines.push("- Смешанные экономические смыслы не склеиваются: дебиторка, кредиторка, авансы и прочие остатки показаны раздельно.");
lines.push("");
lines.push("Блок 3. Сводка");
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Уникальных договоров: ${formatNumberWithDots(uniqueContracts.length)}.`);
lines.push(`- Подтвержденных договор-контрагент профилей: ${formatNumberWithDots(contractProfiles.length)}.`);
@ -2811,42 +2810,42 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push(`- Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`);
if (commercialProfiles.length > 0) {
lines.push("");
lines.push("Блок 4. Чистый открытый остаток по договорам");
lines.push("Чистый открытый остаток по договорам");
lines.push(...renderContractProfileLines(commercialProfiles, false));
}
if (commercialReceivables.length > 0) {
lines.push("");
lines.push("Блок 5. Коммерческие дебиторские компоненты");
lines.push("Коммерческие дебиторские компоненты");
lines.push(...renderConfirmedContractLines(commercialReceivables, false));
}
if (commercialPayables.length > 0) {
lines.push("");
lines.push("Блок 6. Коммерческие кредиторские компоненты");
lines.push("Коммерческие кредиторские компоненты");
lines.push(...renderConfirmedContractLines(commercialPayables, false));
}
if (commercialAdvances.length > 0) {
lines.push("");
lines.push("Блок 7. Коммерческие авансовые компоненты");
lines.push("Коммерческие авансовые компоненты");
lines.push(...renderConfirmedContractLines(commercialAdvances, false));
}
if (commercialOther.length > 0) {
lines.push("");
lines.push("Блок 8. Прочие компоненты по 76");
lines.push("Прочие компоненты по 76");
lines.push(...renderConfirmedContractLines(commercialOther, false));
}
if (specialProfiles.length > 0) {
lines.push("");
lines.push("Блок 9. Финансовые/специальные позиции");
lines.push("Финансовые и специальные позиции");
lines.push(...renderContractProfileLines(specialProfiles, true));
}
if (dirtyProfiles.length > 0) {
lines.push("");
lines.push("Блок 10. Спорные/некачественно нормализованные позиции");
lines.push("Спорные и некачественно нормализованные позиции");
lines.push(...renderContractProfileLines(dirtyProfiles, true));
}
if (confirmedContracts.length === 0) {
lines.push("");
lines.push("Блок 4. Подтвержденные позиции");
lines.push("Подтвержденные позиции");
lines.push("- На дату среза подтвержденные договоры с открытыми взаиморасчетами не найдены.");
}
return {
@ -2869,13 +2868,10 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const specialContracts = contracts.filter((item) => item.category !== "commercial");
const commercialTotal = commercialContracts.reduce((sum, item) => sum + item.totalAmount, 0);
const lines = [
`Итого по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""}: ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`,
`Коротко: по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""} найдено ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`,
"Это shortlist на проверку, а не финальный подтвержденный реестр.",
"",
"Блок 1. Статус результата",
"- Результат: предварительный список договоров с возможными незакрытыми расчетами.",
"- Перед финансовым решением нужна сверка карточек договоров и взаиморасчетов в 1С.",
"",
"Блок 2. Что учтено",
"Что учтено",
...(asOfDate
? [`- Дата среза: ${formatDateRu(asOfDate)}.`]
: periodFrom || periodTo
@ -2883,7 +2879,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
: []),
"- Контур: движения по счетам 60/62/76 и договорная аналитика.",
"",
"Блок 3. Сводка",
"Сводка",
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Договоров-кандидатов всего: ${formatNumberWithDots(contracts.length)}.`,
`- Основной список (коммерческие): ${formatNumberWithDots(commercialContracts.length)}.`,
@ -2891,7 +2887,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
];
if (commercialContracts.length > 0) {
lines.push("");
lines.push("Блок 4. Основной список (коммерческие договоры)");
lines.push("Основной список (коммерческие договоры)");
lines.push(...commercialContracts.slice(0, 10).map((item, index) => {
const counterpartiesLabel = item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен";
const sourceRefsSuffix = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
@ -2899,7 +2895,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
}));
if (specialContracts.length > 0) {
lines.push("");
lines.push("Блок 5. Финансовые/спорные позиции (вынесены отдельно)");
lines.push("Финансовые и спорные позиции (вынесены отдельно)");
lines.push(...specialContracts.slice(0, 8).map((item, index) => {
const counterpartiesLabel = item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен";
const sourceRefsSuffix = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
@ -2909,7 +2905,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
}
else if (counterparties.length > 0) {
lines.push("");
lines.push("Блок 4. Контрагенты с сигналом незакрытых расчетов");
lines.push("Контрагенты с сигналом незакрытых расчетов");
lines.push(`- Контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`);
lines.push(...counterparties
.slice(0, 8)
@ -2918,9 +2914,9 @@ function composeFactualReplyBody(intent, rows, options = {}) {
}
else {
lines.push("");
lines.push("Блок 4. Позиции не выделены");
lines.push("- По текущему live-срезу не удалось выделить договоры с достаточным качеством идентификации.");
lines.push("Блок 5. Примеры исходных строк");
lines.push("Позиции не выделены");
lines.push("- По текущему срезу не удалось выделить договоры с достаточным качеством идентификации.");
lines.push("Примеры исходных строк");
lines.push(...formatTopRows(rows, 6));
}
return {
@ -2951,13 +2947,11 @@ function composeFactualReplyBody(intent, rows, options = {}) {
return acc;
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
const lines = [
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез обязательств к оплате."
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез обязательств к оплате, а не эвристический shortlist."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push("Что учтено");
lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
@ -2967,17 +2961,17 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push(carryoverLine);
}
lines.push("");
lines.push("Блок 3. Сводка");
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории обязательств");
lines.push("Категории обязательств");
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к оплате");
lines.push("Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) {
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
@ -3018,13 +3012,11 @@ function composeFactualReplyBody(intent, rows, options = {}) {
return acc;
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
const lines = [
`Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез дебиторской задолженности."
`Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push("Что учтено");
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
@ -3034,17 +3026,17 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push(carryoverLine);
}
lines.push("");
lines.push("Блок 3. Сводка");
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории дебиторской задолженности");
lines.push("Категории дебиторской задолженности");
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к получению");
lines.push("Подтвержденные позиции к получению");
if (confirmedBalances.length > 0) {
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,

View File

@ -3588,21 +3588,20 @@ function composeFactualReplyBody(
});
const lines: string[] = [
`Собран подтвержденный срез открытых договоров на ${formatDateRu(asOfDate)}.`,
`Чистый коммерческий остаток: ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
`Брутто коммерческих компонентов: ${formatMoneyRub(commercialGrossTotal)}.`,
`Специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
`Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
`Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
`Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`,
`Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
`Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
"- База ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.",
"- Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов.",
"- Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка."
"Что это значит",
"- Это подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
"- Основа ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.",
"- По каждому договору показаны чистый остаток и состав по типам открытых расчетов.",
"- Одна строка = один договор, один контрагент и один тип открытого остатка."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push("Что учтено");
lines.push(`- Дата среза: ${formatDateRu(asOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
@ -3612,7 +3611,7 @@ function composeFactualReplyBody(
lines.push("- Смешанные экономические смыслы не склеиваются: дебиторка, кредиторка, авансы и прочие остатки показаны раздельно.");
lines.push("");
lines.push("Блок 3. Сводка");
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Уникальных договоров: ${formatNumberWithDots(uniqueContracts.length)}.`);
lines.push(`- Подтвержденных договор-контрагент профилей: ${formatNumberWithDots(contractProfiles.length)}.`);
@ -3638,49 +3637,49 @@ function composeFactualReplyBody(
if (commercialProfiles.length > 0) {
lines.push("");
lines.push("Блок 4. Чистый открытый остаток по договорам");
lines.push("Чистый открытый остаток по договорам");
lines.push(...renderContractProfileLines(commercialProfiles, false));
}
if (commercialReceivables.length > 0) {
lines.push("");
lines.push("Блок 5. Коммерческие дебиторские компоненты");
lines.push("Коммерческие дебиторские компоненты");
lines.push(...renderConfirmedContractLines(commercialReceivables, false));
}
if (commercialPayables.length > 0) {
lines.push("");
lines.push("Блок 6. Коммерческие кредиторские компоненты");
lines.push("Коммерческие кредиторские компоненты");
lines.push(...renderConfirmedContractLines(commercialPayables, false));
}
if (commercialAdvances.length > 0) {
lines.push("");
lines.push("Блок 7. Коммерческие авансовые компоненты");
lines.push("Коммерческие авансовые компоненты");
lines.push(...renderConfirmedContractLines(commercialAdvances, false));
}
if (commercialOther.length > 0) {
lines.push("");
lines.push("Блок 8. Прочие компоненты по 76");
lines.push("Прочие компоненты по 76");
lines.push(...renderConfirmedContractLines(commercialOther, false));
}
if (specialProfiles.length > 0) {
lines.push("");
lines.push("Блок 9. Финансовые/специальные позиции");
lines.push("Финансовые и специальные позиции");
lines.push(...renderContractProfileLines(specialProfiles, true));
}
if (dirtyProfiles.length > 0) {
lines.push("");
lines.push("Блок 10. Спорные/некачественно нормализованные позиции");
lines.push("Спорные и некачественно нормализованные позиции");
lines.push(...renderContractProfileLines(dirtyProfiles, true));
}
if (confirmedContracts.length === 0) {
lines.push("");
lines.push("Блок 4. Подтвержденные позиции");
lines.push("Подтвержденные позиции");
lines.push("- На дату среза подтвержденные договоры с открытыми взаиморасчетами не найдены.");
}
@ -3705,13 +3704,10 @@ function composeFactualReplyBody(
const specialContracts = contracts.filter((item) => item.category !== "commercial");
const commercialTotal = commercialContracts.reduce((sum, item) => sum + item.totalAmount, 0);
const lines: string[] = [
`Итого по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""}: ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`,
`Коротко: по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""} найдено ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`,
"Это shortlist на проверку, а не финальный подтвержденный реестр.",
"",
"Блок 1. Статус результата",
"- Результат: предварительный список договоров с возможными незакрытыми расчетами.",
"- Перед финансовым решением нужна сверка карточек договоров и взаиморасчетов в 1С.",
"",
"Блок 2. Что учтено",
"Что учтено",
...(asOfDate
? [`- Дата среза: ${formatDateRu(asOfDate)}.`]
: periodFrom || periodTo
@ -3719,7 +3715,7 @@ function composeFactualReplyBody(
: []),
"- Контур: движения по счетам 60/62/76 и договорная аналитика.",
"",
"Блок 3. Сводка",
"Сводка",
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Договоров-кандидатов всего: ${formatNumberWithDots(contracts.length)}.`,
`- Основной список (коммерческие): ${formatNumberWithDots(commercialContracts.length)}.`,
@ -3727,7 +3723,7 @@ function composeFactualReplyBody(
];
if (commercialContracts.length > 0) {
lines.push("");
lines.push("Блок 4. Основной список (коммерческие договоры)");
lines.push("Основной список (коммерческие договоры)");
lines.push(
...commercialContracts.slice(0, 10).map((item, index) => {
const counterpartiesLabel =
@ -3739,7 +3735,7 @@ function composeFactualReplyBody(
);
if (specialContracts.length > 0) {
lines.push("");
lines.push("Блок 5. Финансовые/спорные позиции (вынесены отдельно)");
lines.push("Финансовые и спорные позиции (вынесены отдельно)");
lines.push(
...specialContracts.slice(0, 8).map((item, index) => {
const counterpartiesLabel =
@ -3752,7 +3748,7 @@ function composeFactualReplyBody(
}
} else if (counterparties.length > 0) {
lines.push("");
lines.push("Блок 4. Контрагенты с сигналом незакрытых расчетов");
lines.push("Контрагенты с сигналом незакрытых расчетов");
lines.push(`- Контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`);
lines.push(
...counterparties
@ -3765,9 +3761,9 @@ function composeFactualReplyBody(
lines.push("- Договорные реквизиты выделены недостаточно надежно, поэтому показан контрагентный список для проверки.");
} else {
lines.push("");
lines.push("Блок 4. Позиции не выделены");
lines.push("- По текущему live-срезу не удалось выделить договоры с достаточным качеством идентификации.");
lines.push("Блок 5. Примеры исходных строк");
lines.push("Позиции не выделены");
lines.push("- По текущему срезу не удалось выделить договоры с достаточным качеством идентификации.");
lines.push("Примеры исходных строк");
lines.push(...formatTopRows(rows, 6));
}
return {
@ -3805,14 +3801,12 @@ function composeFactualReplyBody(
);
const lines: string[] = [
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез обязательств к оплате."
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез обязательств к оплате, а не эвристический shortlist."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push("Что учтено");
lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
@ -3823,19 +3817,19 @@ function composeFactualReplyBody(
}
lines.push("");
lines.push("Блок 3. Сводка");
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории обязательств");
lines.push("Категории обязательств");
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к оплате");
lines.push("Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) {
lines.push(
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
@ -3885,14 +3879,12 @@ function composeFactualReplyBody(
);
const lines: string[] = [
`Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез дебиторской задолженности."
`Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push("Что учтено");
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
@ -3903,19 +3895,19 @@ function composeFactualReplyBody(
}
lines.push("");
lines.push("Блок 3. Сводка");
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории дебиторской задолженности");
lines.push("Категории дебиторской задолженности");
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к получению");
lines.push("Подтвержденные позиции к получению");
if (confirmedBalances.length > 0) {
lines.push(
...confirmedBalances.slice(0, 10).flatMap((item, index) => [

View File

@ -509,10 +509,10 @@ describe("address compose stage utf8 headers", () => {
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Итого по предварительному срезу открытых договоров");
expect(reply.text).toContain("Блок 1. Статус результата");
expect(reply.text).toContain("Результат: предварительный список договоров с возможными незакрытыми расчетами.");
expect(reply.text).toContain("Блок 4. Основной список (коммерческие договоры)");
expect(reply.text).toContain("Коротко: по предварительному срезу открытых договоров");
expect(reply.text).toContain("Это shortlist на проверку");
expect(reply.text).toContain("Основной список (коммерческие договоры)");
expect(reply.text).not.toContain("Блок 1");
expect(reply.semantics?.result_mode).toBe("heuristic_candidates");
expect(reply.semantics?.balance_confirmed).toBe(false);
});
@ -539,12 +539,13 @@ describe("address compose stage utf8 headers", () => {
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Собран подтвержденный срез открытых договоров");
expect(reply.text).toContain("Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.");
expect(reply.text).toContain("Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов.");
expect(reply.text).toContain("Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка.");
expect(reply.text).toContain("Блок 4. Чистый открытый остаток по договорам");
expect(reply.text).toContain("Блок 6. Коммерческие кредиторские компоненты");
expect(reply.text).toContain("Коротко: на 31.03.2020 подтверждено открытых договоров");
expect(reply.text).toContain("Это подтвержденный срез договоров с открытыми взаиморасчетами на дату.");
expect(reply.text).toContain("По каждому договору показаны чистый остаток и состав по типам открытых расчетов.");
expect(reply.text).toContain("Одна строка = один договор, один контрагент и один тип открытого остатка.");
expect(reply.text).toContain("Чистый открытый остаток по договорам");
expect(reply.text).toContain("Коммерческие кредиторские компоненты");
expect(reply.text).not.toContain("Блок 1");
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
expect(reply.semantics?.balance_confirmed).toBe(true);
});
@ -586,15 +587,74 @@ describe("address compose stage utf8 headers", () => {
}
);
expect(reply.text).toContain("Блок 4. Чистый открытый остаток по договорам");
expect(reply.text).toContain("Блок 5. Коммерческие дебиторские компоненты");
expect(reply.text).toContain("Блок 6. Коммерческие кредиторские компоненты");
expect(reply.text).toContain("Блок 10. Спорные/некачественно нормализованные позиции");
expect(reply.text).toContain("Чистый открытый остаток по договорам");
expect(reply.text).toContain("Коммерческие дебиторские компоненты");
expect(reply.text).toContain("Коммерческие кредиторские компоненты");
expect(reply.text).toContain("Спорные и некачественно нормализованные позиции");
expect(reply.text).not.toContain("Блок 1");
expect(reply.text).toContain("брутто компонентов");
expect(reply.text).not.toContain("счета: 62.01; 0");
expect(reply.text).toContain("договор не похож на устойчивый договорный реквизит");
});
it("renders confirmed payables snapshot business-first without numbered report framing", () => {
const reply = composeFactualReply(
"payables_confirmed_as_of_date",
[
{
period: "2020-05-31T23:59:59Z",
registrator: "Остатки на дату",
account_dt: "",
account_kt: "60.01",
amount: 125000,
analytics: ["ООО Ромашка", "Договор №19/15"]
}
],
{
asOfDate: "2020-05-31",
useRubCurrency: true
}
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Коротко: подтвержденный долг к оплате на 31.05.2020");
expect(reply.text).toContain("Это подтвержденный срез обязательств к оплате");
expect(reply.text).toContain("Что учтено");
expect(reply.text).toContain("Сводка");
expect(reply.text).toContain("Категории обязательств");
expect(reply.text).toContain("Подтвержденные позиции к оплате");
expect(reply.text).not.toContain("Блок 1");
});
it("renders confirmed receivables snapshot business-first without numbered report framing", () => {
const reply = composeFactualReply(
"receivables_confirmed_as_of_date",
[
{
period: "2020-05-31T23:59:59Z",
registrator: "Остатки на дату",
account_dt: "62.01",
account_kt: "",
amount: 84000,
analytics: ["ООО Ромашка", "Договор №19/15"]
}
],
{
asOfDate: "2020-05-31",
useRubCurrency: true
}
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Коротко: подтвержденная дебиторская задолженность на 31.05.2020");
expect(reply.text).toContain("Это подтвержденный срез дебиторской задолженности");
expect(reply.text).toContain("Что учтено");
expect(reply.text).toContain("Сводка");
expect(reply.text).toContain("Категории дебиторской задолженности");
expect(reply.text).toContain("Подтвержденные позиции к получению");
expect(reply.text).not.toContain("Блок 1");
});
it("renders period coverage summary for management profile intent", () => {
const reply = composeFactualReply("period_coverage_profile", [
{
@ -3284,12 +3344,13 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(reply.toLowerCase()).toContain("shortlist");
} else {
expect(result?.debug.balance_confirmed).toBe(true);
expect(reply).toContain("Итого подтвержденный долг");
expect(reply).toMatch(/Блок\s+(?:\*\*1\*\*|1)\.\s+Статус результата/u);
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*2\*\*|2)\.\s+Что учтено/u);
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*3\*\*|3)\.\s+Сводка/u);
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*4\*\*|4)\.\s+Категории обязательств/u);
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*5\*\*|5)\.\s+Подтвержденные позиции к оплате/u);
expect(reply).toContain("Коротко: подтвержденный долг к оплате");
expect(reply).toContain("Это подтвержденный срез обязательств к оплате");
expect(reply).toMatch(/\n\nЧто учтено/u);
expect(reply).toMatch(/\n\nСводка/u);
expect(reply).toMatch(/\n\nКатегории обязательств/u);
expect(reply).toMatch(/\n\nПодтвержденные позиции к оплате/u);
expect(reply).not.toContain("Блок 1");
expect(reply).not.toContain("эвристический");
}
});

View File

@ -0,0 +1,114 @@
{
"suite_id": "assistant_saved_session_runtime_job-mmTvku3mtK",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
}
]
}
]
}