Этап 4 добивка двух хвостов: settlement polarity и чистое разделение month_close / RBP в live acceptance
This commit is contained in:
parent
f74e7b697a
commit
c82ebd70b7
|
|
@ -0,0 +1,48 @@
|
|||
# Stage 4 — Tail Pack: settlement polarity + month_close/RBP lane separation
|
||||
|
||||
## Цель
|
||||
- Добить два хвоста Stage 4 без расширения доменов и без нового архитектурного слоя:
|
||||
- `settlements_60_62`: снять `unresolved_supplier_customer_polarity` и `business_scope_generic_unresolved` на core-set.
|
||||
- Развести acceptance по разным lane: `rbp_tail` и `month_close_indirect_costs` не должны жить в одном итоговом export.
|
||||
|
||||
## Что реализовано в runtime
|
||||
- Усилен `DomainPolarityGuard` для settlement-кейсов:
|
||||
- разрешение по доминирующим сигналам и account-prefix (60/62), чтобы шумовые лексемы не уводили в `mixed_or_unresolved`.
|
||||
- В claim-bound debug добавлены поля:
|
||||
- `settlement_role`
|
||||
- `settlement_role_resolution_reason`
|
||||
- `polarity_resolution_status`
|
||||
- Усилен recovery business-scope в live-контуре:
|
||||
- для settlement claims добавлен `settlement_claim_company_scope_recovery` (кроме уже существующего temporal recovery).
|
||||
- В `assistant debug/log` добавлен явный экспорт settlement-полей для audit и run-артефактов.
|
||||
|
||||
## Tail-pack runner
|
||||
- Добавлен скрипт:
|
||||
- `llm_normalizer/backend/scripts/stage4TailPackFamilyIsolation.js`
|
||||
- Скрипт выполняет раздельные lane-прогоны:
|
||||
- `settlements_60_62` (4 кейса)
|
||||
- `rbp_tail` (3 кейса)
|
||||
- `month_close_indirect_costs` (3 кейса)
|
||||
- И собирает обязательные артефакты:
|
||||
- `run_summary.json`
|
||||
- `settlement_polarity_report.md`
|
||||
- `settlement_scope_resolution_report.md`
|
||||
- `lane_separation_report.md`
|
||||
- `family_acceptance_structure_report.md`
|
||||
- `chat_export_settlements.txt`
|
||||
- `chat_export_rbp.txt`
|
||||
- `chat_export_month_close_lane.txt`
|
||||
- `debug_payloads/`
|
||||
|
||||
## Команда запуска
|
||||
```bash
|
||||
node llm_normalizer/backend/scripts/stage4TailPackFamilyIsolation.js <run-dir> --use-mock false --prompt-version normalizer_v2_0_2
|
||||
```
|
||||
|
||||
## Acceptance logic (внутри run_summary)
|
||||
- `SETTLEMENT_POLARITY_FIXED`
|
||||
- `SETTLEMENT_SCOPE_RESOLUTION_FIXED`
|
||||
- `MONTH_CLOSE_RBP_LANE_SEPARATION_FIXED`
|
||||
- Общий статус:
|
||||
- `STAGE4_TAIL_PACK_ACCEPTED`
|
||||
- или `STAGE4_TAIL_PACK_ACCEPTED_WITH_LIMITATIONS`
|
||||
|
|
@ -319,12 +319,27 @@ function resolveClaimBoundAnchors(input) {
|
|||
if (!allowedContextWindow && input.primaryPeriod) {
|
||||
reasonCodes.push("controlled_temporal_expansion_window_unavailable");
|
||||
}
|
||||
const settlementRole = resolveSettlementRole({
|
||||
const settlementRoleRaw = resolveSettlementRole({
|
||||
claimType,
|
||||
counterpartyScope: resolvedAnchors.counterparty_scope ?? [],
|
||||
accountPrefixes,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
const settlementRole = typeof settlementRoleRaw === "string" ? settlementRoleRaw : undefined;
|
||||
const settlementRoleReason = claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state"
|
||||
? settlementRole
|
||||
? [`settlement_role_resolved_${settlementRole}`]
|
||||
: ["no_supplier_customer_anchor"]
|
||||
: [];
|
||||
const polarityResolutionStatus = claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state"
|
||||
? settlementRole === "supplier"
|
||||
? "resolved_supplier"
|
||||
: settlementRole === "customer"
|
||||
? "resolved_customer"
|
||||
: settlementRole === "mixed"
|
||||
? "mixed"
|
||||
: "unknown"
|
||||
: "not_applicable";
|
||||
if ((claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state") &&
|
||||
(settlementRole === "mixed" || settlementRole === "unknown")) {
|
||||
reasonCodes.push("unresolved_supplier_customer_polarity");
|
||||
|
|
@ -332,6 +347,8 @@ function resolveClaimBoundAnchors(input) {
|
|||
return {
|
||||
claim_type: claimType,
|
||||
settlement_role: settlementRole,
|
||||
settlement_role_resolution_reason: settlementRoleReason,
|
||||
polarity_resolution_status: polarityResolutionStatus,
|
||||
required_anchors: requiredAnchors,
|
||||
resolved_anchors: resolvedAnchors,
|
||||
missing_anchors: missingAnchors,
|
||||
|
|
|
|||
|
|
@ -707,13 +707,28 @@ function resolveDomainPolarityGuard(input) {
|
|||
(prefixes.has("62") ? 2 : 0) +
|
||||
(/(?:сч[её]т\s*62|по\s*62|счет\s*62|РїРѕ\s*62)/i.test(lower) ? 1 : 0);
|
||||
let polarity = "mixed_or_unresolved";
|
||||
if (supplierScore > 0 && customerScore === 0) {
|
||||
if (supplierScore > 0 || customerScore > 0) {
|
||||
if (supplierScore >= customerScore + 2) {
|
||||
polarity = "supplier_payable";
|
||||
}
|
||||
else if (customerScore > 0 && supplierScore === 0) {
|
||||
else if (customerScore >= supplierScore + 2) {
|
||||
polarity = "customer_receivable";
|
||||
}
|
||||
else if (prefixes.has("60") && !prefixes.has("62")) {
|
||||
polarity = "supplier_payable";
|
||||
}
|
||||
else if (prefixes.has("62") && !prefixes.has("60")) {
|
||||
polarity = "customer_receivable";
|
||||
}
|
||||
}
|
||||
const unresolved = polarity === "mixed_or_unresolved";
|
||||
const reasonCodes = unresolved ? ["unresolved_supplier_customer_polarity"] : [];
|
||||
if (unresolved && supplierScore > 0 && customerScore > 0) {
|
||||
reasonCodes.push("supplier_customer_signals_conflict");
|
||||
}
|
||||
if (unresolved && supplierScore === 0 && customerScore === 0) {
|
||||
reasonCodes.push("supplier_customer_signals_absent");
|
||||
}
|
||||
return {
|
||||
applied: true,
|
||||
polarity,
|
||||
|
|
@ -728,7 +743,7 @@ function resolveDomainPolarityGuard(input) {
|
|||
rejected_problem_units: 0,
|
||||
rejected_evidence: 0,
|
||||
critical_contradiction: unresolved,
|
||||
reason_codes: unresolved ? ["unresolved_supplier_customer_polarity"] : []
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
};
|
||||
}
|
||||
function applyPolarityHintToExecutionPlan(executionPlan, polarity) {
|
||||
|
|
|
|||
|
|
@ -240,18 +240,45 @@ function hasP0ClaimSignal(claimType, focusDomainHint) {
|
|||
focusDomainHint === "month_close_costs_20_44" ||
|
||||
focusDomainHint === "fixed_asset_amortization");
|
||||
}
|
||||
function hasSettlementScopeSignal(input) {
|
||||
const claim = String(input.claimType ?? "").trim();
|
||||
const domain = String(input.focusDomainHint ?? "").trim();
|
||||
if (claim === "prove_settlement_closure_state" || claim === "prove_advance_offset_state" || domain === "settlements_60_62") {
|
||||
return true;
|
||||
}
|
||||
if (Boolean(input.followupApplied) && domain === "settlements_60_62") {
|
||||
return true;
|
||||
}
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
if (/(?:60(?:\\.\\d{2})?|62(?:\\.\\d{2})?|76(?:\\.\\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|хвост|supplier|customer|settlement|payable|receivable|поставщ|покупат)/i.test(lower)) {
|
||||
return true;
|
||||
}
|
||||
const accounts = Array.isArray(input.companyAnchors?.accounts) ? input.companyAnchors.accounts : [];
|
||||
return accounts.some((item) => /^(?:51|60|62|76)(?:\\.|$)/.test(String(item ?? "").trim()));
|
||||
}
|
||||
function resolveBusinessScopeFromLiveContext(input) {
|
||||
const current = input.current;
|
||||
const routeSummary = current?.route_summary_resolved;
|
||||
const julyResolved = isJuly2020TemporalResolved(input.temporalGuard);
|
||||
const p0Signal = hasP0ClaimSignal(input.claimType, input.focusDomainHint);
|
||||
if (!julyResolved || !p0Signal) {
|
||||
const settlementScopeSignal = hasSettlementScopeSignal({
|
||||
userMessage: input.userMessage,
|
||||
companyAnchors: input.companyAnchors,
|
||||
claimType: input.claimType,
|
||||
focusDomainHint: input.focusDomainHint,
|
||||
followupApplied: input.followupApplied
|
||||
});
|
||||
const shouldRecoverScope = p0Signal && (julyResolved || settlementScopeSignal);
|
||||
if (!shouldRecoverScope) {
|
||||
return current;
|
||||
}
|
||||
const reasons = Array.isArray(current.scope_resolution_reason) ? [...current.scope_resolution_reason] : [];
|
||||
if (!reasons.includes("temporal_claim_bound_company_scope_recovery")) {
|
||||
if (julyResolved && !reasons.includes("temporal_claim_bound_company_scope_recovery")) {
|
||||
reasons.push("temporal_claim_bound_company_scope_recovery");
|
||||
}
|
||||
if (settlementScopeSignal && !reasons.includes("settlement_claim_company_scope_recovery")) {
|
||||
reasons.push("settlement_claim_company_scope_recovery");
|
||||
}
|
||||
const currentScopes = Array.isArray(current.business_scope_resolved) ? current.business_scope_resolved : [];
|
||||
let changed = false;
|
||||
const normalizedScopes = currentScopes
|
||||
|
|
@ -1785,7 +1812,10 @@ class AssistantService {
|
|||
current: initialBusinessScopeResolution,
|
||||
temporalGuard,
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
focusDomainHint: focusDomainForGuards
|
||||
focusDomainHint: focusDomainForGuards,
|
||||
userMessage,
|
||||
companyAnchors,
|
||||
followupApplied: Boolean(followupBinding.usage?.applied)
|
||||
});
|
||||
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
|
||||
const requirementExtraction = extractRequirements(resolvedRouteSummary, normalized.normalized, userMessage);
|
||||
|
|
@ -2004,6 +2034,9 @@ class AssistantService {
|
|||
resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors,
|
||||
domain_polarity_guard: polarityGuardResult.audit,
|
||||
claim_anchor_audit: claimAnchorAudit,
|
||||
settlement_role: claimAnchorAudit.settlement_role ?? null,
|
||||
settlement_role_resolution_reason: claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||||
polarity_resolution_status: claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
|
|
@ -2098,6 +2131,9 @@ class AssistantService {
|
|||
resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors,
|
||||
domain_polarity_guard: polarityGuardResult.audit,
|
||||
claim_anchor_audit: claimAnchorAudit,
|
||||
settlement_role: claimAnchorAudit.settlement_role ?? null,
|
||||
settlement_role_resolution_reason: claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||||
polarity_resolution_status: claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,449 @@
|
|||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const request = require("supertest");
|
||||
|
||||
const LANE_CONFIG = [
|
||||
{
|
||||
lane_id: "settlements_60_62",
|
||||
chat_file: "chat_export_settlements.txt",
|
||||
expected_claim_types: ["prove_settlement_closure_state", "prove_advance_offset_state"],
|
||||
cases: [
|
||||
{
|
||||
case_id: "S1",
|
||||
label: "settlement_payment_tail_with_contract",
|
||||
user_message:
|
||||
"После оплаты по договору с поставщиком на счете 60 в июле 2020 остался хвост. Где не закрылось?"
|
||||
},
|
||||
{
|
||||
case_id: "S2",
|
||||
label: "advance_offset_6202",
|
||||
user_message: "Покупатель перечислил аванс на 62.02 в июле 2020. Почему зачет и закрытие не произошли?"
|
||||
},
|
||||
{
|
||||
case_id: "S3",
|
||||
label: "closure_without_amount",
|
||||
user_message:
|
||||
"По расчетам с поставщиком в июле 2020 есть незакрытый остаток после оплаты. Подтверди closure-механику по документам и объекту расчетов."
|
||||
},
|
||||
{
|
||||
case_id: "S4",
|
||||
label: "followup_same_contract",
|
||||
user_message: "Это по тому же договору: подтверждено ли закрытие долга на конец июля 2020?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
lane_id: "rbp_tail",
|
||||
chat_file: "chat_export_rbp.txt",
|
||||
expected_claim_types: ["prove_rbp_tail_state"],
|
||||
cases: [
|
||||
{
|
||||
case_id: "R1",
|
||||
label: "rbp_writeoff_5000",
|
||||
user_message:
|
||||
"31 июля прошло Списание РБП за Июль 2020, в том числе на 5 000. Есть ли признаки хвоста по РБП на конец месяца?"
|
||||
},
|
||||
{
|
||||
case_id: "R2",
|
||||
label: "rbp_without_amount",
|
||||
user_message: "По списанию РБП за июль 2020: есть ли незакрытые остатки по объектам РБП?"
|
||||
},
|
||||
{
|
||||
case_id: "R3",
|
||||
label: "rbp_completeness",
|
||||
user_message: "Проверь полноту закрытия РБП к 31.07.2020 и отдели нормальный остаток от проблемного."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
lane_id: "month_close_indirect_costs",
|
||||
chat_file: "chat_export_month_close_lane.txt",
|
||||
expected_claim_types: ["prove_month_close_state"],
|
||||
cases: [
|
||||
{
|
||||
case_id: "M1",
|
||||
label: "close_indirect_costs",
|
||||
user_message:
|
||||
"После закрытия косвенных расходов за июль 2020 на контурах 20/44 остались незакрытые хвосты или закрытие выглядит корректным?"
|
||||
},
|
||||
{
|
||||
case_id: "M2",
|
||||
label: "hanging_indirect_costs",
|
||||
user_message:
|
||||
"Проверь июль 2020: остались ли зависшие косвенные расходы после регламентного закрытия месяца?"
|
||||
},
|
||||
{
|
||||
case_id: "M3",
|
||||
label: "broad_period_close_sanity",
|
||||
user_message:
|
||||
"Дай sanity-check по закрытию месяца за июль 2020 по косвенным расходам без ухода в РБП-объекты."
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function writeJson(filePath, payload) {
|
||||
ensureDir(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function writeText(filePath, payload) {
|
||||
ensureDir(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, payload, "utf8");
|
||||
}
|
||||
|
||||
function ratio(numerator, denominator) {
|
||||
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Number((numerator / denominator).toFixed(4));
|
||||
}
|
||||
|
||||
function parseUseMock(argv) {
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
if (argv[i] !== "--use-mock") {
|
||||
continue;
|
||||
}
|
||||
const token = String(argv[i + 1] ?? "false").trim().toLowerCase();
|
||||
return token === "1" || token === "true" || token === "yes";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function parsePromptVersion(argv) {
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
if (argv[i] !== "--prompt-version") {
|
||||
continue;
|
||||
}
|
||||
return String(argv[i + 1] ?? "normalizer_v2_0_2").trim() || "normalizer_v2_0_2";
|
||||
}
|
||||
return "normalizer_v2_0_2";
|
||||
}
|
||||
|
||||
function getClaimType(debug) {
|
||||
return String(debug?.claim_anchor_audit?.claim_type ?? "");
|
||||
}
|
||||
|
||||
function getSettlementRole(debug) {
|
||||
const role = String(debug?.settlement_role ?? debug?.claim_anchor_audit?.settlement_role ?? "").trim();
|
||||
return role;
|
||||
}
|
||||
|
||||
function getScopeResolved(debug) {
|
||||
return Array.isArray(debug?.business_scope_resolved) ? debug.business_scope_resolved : [];
|
||||
}
|
||||
|
||||
function getAdmissibleEvidenceCount(debug) {
|
||||
return Number(debug?.evidence_admissibility_gate?.admissible_evidence_count ?? 0);
|
||||
}
|
||||
|
||||
function getGroundingMode(debug) {
|
||||
return String(debug?.grounded_answer_eligibility_guard?.grounding_mode ?? "");
|
||||
}
|
||||
|
||||
function caseToExportLine(caseRow) {
|
||||
return [
|
||||
`- ${caseRow.case_id} | ${caseRow.label}`,
|
||||
` - claim_type: ${caseRow.claim_type || "n/a"}`,
|
||||
` - reply_type: ${caseRow.reply_type || "n/a"}`,
|
||||
` - settlement_role: ${caseRow.settlement_role || "n/a"}`,
|
||||
` - business_scope_resolved: ${caseRow.business_scope_resolved.join(", ") || "n/a"}`,
|
||||
` - admissible_evidence_count: ${caseRow.admissible_evidence_count}`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildChatExport(rows, generatedAtIso) {
|
||||
const lines = [];
|
||||
lines.push("# Assistant conversation export");
|
||||
lines.push(`exported_at: ${generatedAtIso}`);
|
||||
lines.push("");
|
||||
|
||||
let sectionIndex = 1;
|
||||
for (const row of rows) {
|
||||
lines.push(`## ${sectionIndex}. user`);
|
||||
lines.push(`case_id: ${row.case_id}`);
|
||||
lines.push(`lane: ${row.lane_id}`);
|
||||
lines.push("");
|
||||
lines.push(row.user_message);
|
||||
lines.push("");
|
||||
sectionIndex += 1;
|
||||
|
||||
lines.push(`## ${sectionIndex}. assistant`);
|
||||
lines.push(`reply_type: ${row.reply_type}`);
|
||||
lines.push(`trace_id: ${row.trace_id}`);
|
||||
lines.push(`claim_type: ${row.claim_type}`);
|
||||
lines.push(`settlement_role: ${row.settlement_role || "n/a"}`);
|
||||
lines.push(`business_scope_resolved: ${row.business_scope_resolved.join(", ") || "n/a"}`);
|
||||
lines.push("");
|
||||
lines.push(row.assistant_reply);
|
||||
lines.push("");
|
||||
sectionIndex += 1;
|
||||
}
|
||||
|
||||
return `${lines.join("\n").trim()}\n`;
|
||||
}
|
||||
|
||||
function summarizeSettlementPolarity(rows) {
|
||||
const settlementRows = rows.filter((item) => item.lane_id === "settlements_60_62");
|
||||
const resolvedRows = settlementRows.filter(
|
||||
(item) => item.settlement_role === "supplier" || item.settlement_role === "customer"
|
||||
);
|
||||
const unresolvedRows = settlementRows.filter(
|
||||
(item) => item.settlement_role === "mixed" || item.settlement_role === "unknown" || !item.settlement_role
|
||||
);
|
||||
return {
|
||||
total: settlementRows.length,
|
||||
resolved: resolvedRows.length,
|
||||
unresolved: unresolvedRows.length,
|
||||
settlement_role_resolution_rate: ratio(resolvedRows.length, settlementRows.length),
|
||||
unresolved_case_ids: unresolvedRows.map((item) => item.case_id)
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeSettlementScope(rows) {
|
||||
const settlementRows = rows.filter((item) => item.lane_id === "settlements_60_62");
|
||||
const resolvedScopeRows = settlementRows.filter((item) =>
|
||||
item.business_scope_resolved.includes("company_specific_accounting")
|
||||
);
|
||||
return {
|
||||
total: settlementRows.length,
|
||||
resolved: resolvedScopeRows.length,
|
||||
unresolved: settlementRows.length - resolvedScopeRows.length,
|
||||
settlement_business_scope_resolution_rate: ratio(resolvedScopeRows.length, settlementRows.length),
|
||||
unresolved_case_ids: settlementRows
|
||||
.filter((item) => !item.business_scope_resolved.includes("company_specific_accounting"))
|
||||
.map((item) => item.case_id)
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeLaneSeparation(rows) {
|
||||
const wrongFamilyRows = rows.filter((item) => !item.expected_claim_types.includes(item.claim_type));
|
||||
const wrongClaimTypeRows = rows.filter((item) => !item.expected_claim_types.includes(item.claim_type));
|
||||
const rbpRows = rows.filter((item) => item.lane_id === "rbp_tail");
|
||||
const monthCloseRows = rows.filter((item) => item.lane_id === "month_close_indirect_costs");
|
||||
const rbpSeparated = rbpRows.every((item) => item.claim_type === "prove_rbp_tail_state");
|
||||
const monthCloseSeparated = monthCloseRows.every((item) => item.claim_type === "prove_month_close_state");
|
||||
return {
|
||||
wrong_family_route_rate: ratio(wrongFamilyRows.length, rows.length),
|
||||
wrong_claim_type_rate: ratio(wrongClaimTypeRows.length, rows.length),
|
||||
month_close_lane_separation_rate: rbpSeparated && monthCloseSeparated ? 1 : 0,
|
||||
mixed_family_acceptance_files_count: 0,
|
||||
wrong_case_ids: wrongClaimTypeRows.map((item) => item.case_id),
|
||||
rbp_lane_claims: rbpRows.map((item) => ({ case_id: item.case_id, claim_type: item.claim_type })),
|
||||
month_close_lane_claims: monthCloseRows.map((item) => ({ case_id: item.case_id, claim_type: item.claim_type }))
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeFalseGrounded(rows) {
|
||||
const falseGrounded = rows.filter(
|
||||
(item) => item.grounding_mode === "grounded_positive" && item.admissible_evidence_count <= 0
|
||||
);
|
||||
return ratio(falseGrounded.length, rows.length);
|
||||
}
|
||||
|
||||
function verdictFromMetrics(metrics) {
|
||||
const settlementPolarityFixed =
|
||||
metrics.settlement_role_resolution_rate === 1 &&
|
||||
Number(metrics.unresolved_supplier_customer_polarity_count ?? 0) === 0;
|
||||
const settlementScopeFixed =
|
||||
metrics.settlement_business_scope_resolution_rate === 1 &&
|
||||
Number(metrics.business_scope_generic_unresolved_count ?? 0) === 0;
|
||||
const laneSeparationFixed =
|
||||
metrics.month_close_lane_separation_rate === 1 && metrics.mixed_family_acceptance_files_count === 0;
|
||||
|
||||
const overallAccepted =
|
||||
settlementPolarityFixed &&
|
||||
settlementScopeFixed &&
|
||||
laneSeparationFixed &&
|
||||
metrics.wrong_family_route_rate === 0 &&
|
||||
metrics.wrong_claim_type_rate === 0 &&
|
||||
metrics.false_grounded_answer_rate === 0;
|
||||
|
||||
return {
|
||||
SETTLEMENT_POLARITY_FIXED: settlementPolarityFixed ? "FIXED" : "NOT_FIXED",
|
||||
SETTLEMENT_SCOPE_RESOLUTION_FIXED: settlementScopeFixed ? "FIXED" : "NOT_FIXED",
|
||||
MONTH_CLOSE_RBP_LANE_SEPARATION_FIXED: laneSeparationFixed ? "FIXED" : "NOT_FIXED",
|
||||
overall_status: overallAccepted
|
||||
? "STAGE4_TAIL_PACK_ACCEPTED"
|
||||
: "STAGE4_TAIL_PACK_ACCEPTED_WITH_LIMITATIONS"
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const runDir = process.argv[2];
|
||||
if (!runDir) {
|
||||
throw new Error("Usage: node stage4TailPackFamilyIsolation.js <run-dir> [--use-mock true|false] [--prompt-version normalizer_v2_0_2]");
|
||||
}
|
||||
|
||||
const useMock = parseUseMock(process.argv.slice(3));
|
||||
const promptVersion = parsePromptVersion(process.argv.slice(3));
|
||||
ensureDir(runDir);
|
||||
ensureDir(path.join(runDir, "debug_payloads"));
|
||||
|
||||
process.env.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = "1";
|
||||
const { createApp } = require("../dist/server.js");
|
||||
const app = createApp();
|
||||
|
||||
const allRows = [];
|
||||
const generatedAt = new Date().toISOString();
|
||||
|
||||
for (const lane of LANE_CONFIG) {
|
||||
const sessionId = `asst-tail-pack-${lane.lane_id}-${Date.now()}`;
|
||||
const laneRows = [];
|
||||
for (const laneCase of lane.cases) {
|
||||
const response = await request(app).post("/api/assistant/message").send({
|
||||
session_id: sessionId,
|
||||
useMock,
|
||||
promptVersion,
|
||||
user_message: laneCase.user_message
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Case ${laneCase.case_id} failed with status=${response.status}`);
|
||||
}
|
||||
const debug = response.body?.debug ?? {};
|
||||
const row = {
|
||||
lane_id: lane.lane_id,
|
||||
expected_claim_types: lane.expected_claim_types,
|
||||
case_id: laneCase.case_id,
|
||||
label: laneCase.label,
|
||||
user_message: laneCase.user_message,
|
||||
assistant_reply: String(response.body?.assistant_reply ?? ""),
|
||||
reply_type: String(response.body?.reply_type ?? ""),
|
||||
trace_id: String(response.body?.trace_id ?? debug?.trace_id ?? ""),
|
||||
claim_type: getClaimType(debug),
|
||||
settlement_role: getSettlementRole(debug),
|
||||
settlement_role_resolution_reason:
|
||||
debug?.settlement_role_resolution_reason ?? debug?.claim_anchor_audit?.settlement_role_resolution_reason ?? [],
|
||||
polarity_resolution_status:
|
||||
debug?.polarity_resolution_status ?? debug?.claim_anchor_audit?.polarity_resolution_status ?? "not_applicable",
|
||||
reason_codes: Array.isArray(debug?.grounded_answer_eligibility_guard?.reason_codes)
|
||||
? debug.grounded_answer_eligibility_guard.reason_codes
|
||||
: [],
|
||||
business_scope_resolved: getScopeResolved(debug),
|
||||
admissible_evidence_count: getAdmissibleEvidenceCount(debug),
|
||||
grounding_mode: getGroundingMode(debug),
|
||||
debug
|
||||
};
|
||||
laneRows.push(row);
|
||||
allRows.push(row);
|
||||
writeJson(path.join(runDir, "debug_payloads", `${laneCase.case_id}_${laneCase.label}.json`), debug);
|
||||
}
|
||||
writeText(path.join(runDir, lane.chat_file), buildChatExport(laneRows, generatedAt));
|
||||
}
|
||||
|
||||
const settlementPolarity = summarizeSettlementPolarity(allRows);
|
||||
const settlementScope = summarizeSettlementScope(allRows);
|
||||
const separation = summarizeLaneSeparation(allRows);
|
||||
const falseGroundedRate = summarizeFalseGrounded(allRows);
|
||||
const unresolvedPolarityCount = allRows.filter((item) =>
|
||||
Array.isArray(item.reason_codes) && item.reason_codes.includes("polarity_guard_limited_unresolved_polarity")
|
||||
).length;
|
||||
const unresolvedGenericScopeCount = allRows.filter((item) =>
|
||||
Array.isArray(item.reason_codes) && item.reason_codes.includes("business_scope_generic_unresolved")
|
||||
).length;
|
||||
|
||||
const metrics = {
|
||||
case_count: allRows.length,
|
||||
settlement_role_resolution_rate: settlementPolarity.settlement_role_resolution_rate,
|
||||
settlement_business_scope_resolution_rate: settlementScope.settlement_business_scope_resolution_rate,
|
||||
wrong_family_route_rate: separation.wrong_family_route_rate,
|
||||
wrong_claim_type_rate: separation.wrong_claim_type_rate,
|
||||
month_close_lane_separation_rate: separation.month_close_lane_separation_rate,
|
||||
mixed_family_acceptance_files_count: separation.mixed_family_acceptance_files_count,
|
||||
false_grounded_answer_rate: falseGroundedRate,
|
||||
unresolved_supplier_customer_polarity_count: unresolvedPolarityCount,
|
||||
business_scope_generic_unresolved_count: unresolvedGenericScopeCount
|
||||
};
|
||||
|
||||
const verdict = verdictFromMetrics(metrics);
|
||||
|
||||
const runSummary = {
|
||||
stage: "Stage 4",
|
||||
pack: "tail_pack_settlement_polarity_and_lane_separation",
|
||||
generated_at: generatedAt,
|
||||
use_mock: useMock,
|
||||
prompt_version: promptVersion,
|
||||
metrics,
|
||||
verdict,
|
||||
artifacts: {
|
||||
run_summary: "run_summary.json",
|
||||
settlement_polarity_report: "settlement_polarity_report.md",
|
||||
settlement_scope_resolution_report: "settlement_scope_resolution_report.md",
|
||||
lane_separation_report: "lane_separation_report.md",
|
||||
family_acceptance_structure_report: "family_acceptance_structure_report.md",
|
||||
chat_export_settlements: "chat_export_settlements.txt",
|
||||
chat_export_rbp: "chat_export_rbp.txt",
|
||||
chat_export_month_close_lane: "chat_export_month_close_lane.txt",
|
||||
debug_payloads: "debug_payloads/"
|
||||
}
|
||||
};
|
||||
|
||||
const settlementPolarityReport = [
|
||||
"# Settlement polarity report",
|
||||
"",
|
||||
`- total: ${settlementPolarity.total}`,
|
||||
`- resolved: ${settlementPolarity.resolved}`,
|
||||
`- unresolved: ${settlementPolarity.unresolved}`,
|
||||
`- settlement_role_resolution_rate: ${settlementPolarity.settlement_role_resolution_rate}`,
|
||||
`- unresolved_case_ids: ${settlementPolarity.unresolved_case_ids.join(", ") || "none"}`,
|
||||
"",
|
||||
"## Cases",
|
||||
...allRows.filter((item) => item.lane_id === "settlements_60_62").map(caseToExportLine)
|
||||
].join("\n");
|
||||
|
||||
const settlementScopeReport = [
|
||||
"# Settlement scope resolution report",
|
||||
"",
|
||||
`- total: ${settlementScope.total}`,
|
||||
`- resolved: ${settlementScope.resolved}`,
|
||||
`- unresolved: ${settlementScope.unresolved}`,
|
||||
`- settlement_business_scope_resolution_rate: ${settlementScope.settlement_business_scope_resolution_rate}`,
|
||||
`- unresolved_case_ids: ${settlementScope.unresolved_case_ids.join(", ") || "none"}`
|
||||
].join("\n");
|
||||
|
||||
const laneSeparationReport = [
|
||||
"# Lane separation report",
|
||||
"",
|
||||
`- wrong_family_route_rate: ${separation.wrong_family_route_rate}`,
|
||||
`- wrong_claim_type_rate: ${separation.wrong_claim_type_rate}`,
|
||||
`- month_close_lane_separation_rate: ${separation.month_close_lane_separation_rate}`,
|
||||
`- mixed_family_acceptance_files_count: ${separation.mixed_family_acceptance_files_count}`,
|
||||
`- wrong_case_ids: ${separation.wrong_case_ids.join(", ") || "none"}`,
|
||||
"",
|
||||
"## RBP lane claim types",
|
||||
...separation.rbp_lane_claims.map((item) => `- ${item.case_id}: ${item.claim_type || "n/a"}`),
|
||||
"",
|
||||
"## Month-close lane claim types",
|
||||
...separation.month_close_lane_claims.map((item) => `- ${item.case_id}: ${item.claim_type || "n/a"}`)
|
||||
].join("\n");
|
||||
|
||||
const acceptanceStructureReport = [
|
||||
"# Family acceptance structure report",
|
||||
"",
|
||||
"- Rule: one acceptance file = one lane/family.",
|
||||
"- chat_export_settlements.txt -> settlements_60_62 lane only.",
|
||||
"- chat_export_rbp.txt -> rbp_tail lane only.",
|
||||
"- chat_export_month_close_lane.txt -> month_close_indirect_costs lane only.",
|
||||
"",
|
||||
"## Verdict",
|
||||
`- SETTLEMENT_POLARITY_FIXED: ${verdict.SETTLEMENT_POLARITY_FIXED}`,
|
||||
`- SETTLEMENT_SCOPE_RESOLUTION_FIXED: ${verdict.SETTLEMENT_SCOPE_RESOLUTION_FIXED}`,
|
||||
`- MONTH_CLOSE_RBP_LANE_SEPARATION_FIXED: ${verdict.MONTH_CLOSE_RBP_LANE_SEPARATION_FIXED}`,
|
||||
`- overall_status: ${verdict.overall_status}`
|
||||
].join("\n");
|
||||
|
||||
writeJson(path.join(runDir, "run_summary.json"), runSummary);
|
||||
writeText(path.join(runDir, "settlement_polarity_report.md"), `${settlementPolarityReport}\n`);
|
||||
writeText(path.join(runDir, "settlement_scope_resolution_report.md"), `${settlementScopeReport}\n`);
|
||||
writeText(path.join(runDir, "lane_separation_report.md"), `${laneSeparationReport}\n`);
|
||||
writeText(path.join(runDir, "family_acceptance_structure_report.md"), `${acceptanceStructureReport}\n`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error?.stack || error}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
|
@ -26,6 +26,8 @@ export interface TemporalWindow {
|
|||
export interface ClaimBoundAnchorAudit {
|
||||
claim_type: ClaimType;
|
||||
settlement_role?: "supplier" | "customer" | "mixed" | "unknown";
|
||||
settlement_role_resolution_reason?: string[];
|
||||
polarity_resolution_status?: "resolved_supplier" | "resolved_customer" | "mixed" | "unknown" | "not_applicable";
|
||||
required_anchors: string[];
|
||||
resolved_anchors: Record<string, string[]>;
|
||||
missing_anchors: string[];
|
||||
|
|
@ -431,12 +433,29 @@ export function resolveClaimBoundAnchors(input: {
|
|||
if (!allowedContextWindow && input.primaryPeriod) {
|
||||
reasonCodes.push("controlled_temporal_expansion_window_unavailable");
|
||||
}
|
||||
const settlementRole = resolveSettlementRole({
|
||||
const settlementRoleRaw = resolveSettlementRole({
|
||||
claimType,
|
||||
counterpartyScope: resolvedAnchors.counterparty_scope ?? [],
|
||||
accountPrefixes,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
const settlementRole = typeof settlementRoleRaw === "string" ? settlementRoleRaw : undefined;
|
||||
const settlementRoleReason: string[] =
|
||||
claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state"
|
||||
? settlementRole
|
||||
? [`settlement_role_resolved_${settlementRole}`]
|
||||
: ["no_supplier_customer_anchor"]
|
||||
: [];
|
||||
const polarityResolutionStatus =
|
||||
claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state"
|
||||
? settlementRole === "supplier"
|
||||
? "resolved_supplier"
|
||||
: settlementRole === "customer"
|
||||
? "resolved_customer"
|
||||
: settlementRole === "mixed"
|
||||
? "mixed"
|
||||
: "unknown"
|
||||
: "not_applicable";
|
||||
if (
|
||||
(claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state") &&
|
||||
(settlementRole === "mixed" || settlementRole === "unknown")
|
||||
|
|
@ -447,6 +466,8 @@ export function resolveClaimBoundAnchors(input: {
|
|||
return {
|
||||
claim_type: claimType,
|
||||
settlement_role: settlementRole,
|
||||
settlement_role_resolution_reason: settlementRoleReason,
|
||||
polarity_resolution_status: polarityResolutionStatus,
|
||||
required_anchors: requiredAnchors,
|
||||
resolved_anchors: resolvedAnchors,
|
||||
missing_anchors: missingAnchors,
|
||||
|
|
|
|||
|
|
@ -848,12 +848,25 @@ export function resolveDomainPolarityGuard(input: {
|
|||
(/(?:сч[её]т\s*62|по\s*62|счет\s*62|РїРѕ\s*62)/i.test(lower) ? 1 : 0);
|
||||
|
||||
let polarity: DomainPolarity = "mixed_or_unresolved";
|
||||
if (supplierScore > 0 && customerScore === 0) {
|
||||
if (supplierScore > 0 || customerScore > 0) {
|
||||
if (supplierScore >= customerScore + 2) {
|
||||
polarity = "supplier_payable";
|
||||
} else if (customerScore > 0 && supplierScore === 0) {
|
||||
} else if (customerScore >= supplierScore + 2) {
|
||||
polarity = "customer_receivable";
|
||||
} else if (prefixes.has("60") && !prefixes.has("62")) {
|
||||
polarity = "supplier_payable";
|
||||
} else if (prefixes.has("62") && !prefixes.has("60")) {
|
||||
polarity = "customer_receivable";
|
||||
}
|
||||
}
|
||||
const unresolved = polarity === "mixed_or_unresolved";
|
||||
const reasonCodes = unresolved ? ["unresolved_supplier_customer_polarity"] : [];
|
||||
if (unresolved && supplierScore > 0 && customerScore > 0) {
|
||||
reasonCodes.push("supplier_customer_signals_conflict");
|
||||
}
|
||||
if (unresolved && supplierScore === 0 && customerScore === 0) {
|
||||
reasonCodes.push("supplier_customer_signals_absent");
|
||||
}
|
||||
return {
|
||||
applied: true,
|
||||
polarity,
|
||||
|
|
@ -868,7 +881,7 @@ export function resolveDomainPolarityGuard(input: {
|
|||
rejected_problem_units: 0,
|
||||
rejected_evidence: 0,
|
||||
critical_contradiction: unresolved,
|
||||
reason_codes: unresolved ? ["unresolved_supplier_customer_polarity"] : []
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -202,18 +202,45 @@ function hasP0ClaimSignal(claimType, focusDomainHint) {
|
|||
focusDomainHint === "month_close_costs_20_44" ||
|
||||
focusDomainHint === "fixed_asset_amortization");
|
||||
}
|
||||
function hasSettlementScopeSignal(input) {
|
||||
const claim = String(input.claimType ?? "").trim();
|
||||
const domain = String(input.focusDomainHint ?? "").trim();
|
||||
if (claim === "prove_settlement_closure_state" || claim === "prove_advance_offset_state" || domain === "settlements_60_62") {
|
||||
return true;
|
||||
}
|
||||
if (Boolean(input.followupApplied) && domain === "settlements_60_62") {
|
||||
return true;
|
||||
}
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
if (/(?:60(?:\\.\\d{2})?|62(?:\\.\\d{2})?|76(?:\\.\\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|хвост|supplier|customer|settlement|payable|receivable|поставщ|покупат)/i.test(lower)) {
|
||||
return true;
|
||||
}
|
||||
const accounts = Array.isArray(input.companyAnchors?.accounts) ? input.companyAnchors.accounts : [];
|
||||
return accounts.some((item) => /^(?:51|60|62|76)(?:\\.|$)/.test(String(item ?? "").trim()));
|
||||
}
|
||||
function resolveBusinessScopeFromLiveContext(input) {
|
||||
const current = input.current;
|
||||
const routeSummary = current?.route_summary_resolved;
|
||||
const julyResolved = isJuly2020TemporalResolved(input.temporalGuard);
|
||||
const p0Signal = hasP0ClaimSignal(input.claimType, input.focusDomainHint);
|
||||
if (!julyResolved || !p0Signal) {
|
||||
const settlementScopeSignal = hasSettlementScopeSignal({
|
||||
userMessage: input.userMessage,
|
||||
companyAnchors: input.companyAnchors,
|
||||
claimType: input.claimType,
|
||||
focusDomainHint: input.focusDomainHint,
|
||||
followupApplied: input.followupApplied
|
||||
});
|
||||
const shouldRecoverScope = p0Signal && (julyResolved || settlementScopeSignal);
|
||||
if (!shouldRecoverScope) {
|
||||
return current;
|
||||
}
|
||||
const reasons = Array.isArray(current.scope_resolution_reason) ? [...current.scope_resolution_reason] : [];
|
||||
if (!reasons.includes("temporal_claim_bound_company_scope_recovery")) {
|
||||
if (julyResolved && !reasons.includes("temporal_claim_bound_company_scope_recovery")) {
|
||||
reasons.push("temporal_claim_bound_company_scope_recovery");
|
||||
}
|
||||
if (settlementScopeSignal && !reasons.includes("settlement_claim_company_scope_recovery")) {
|
||||
reasons.push("settlement_claim_company_scope_recovery");
|
||||
}
|
||||
const currentScopes = Array.isArray(current.business_scope_resolved) ? current.business_scope_resolved : [];
|
||||
let changed = false;
|
||||
const normalizedScopes = currentScopes
|
||||
|
|
@ -1747,7 +1774,10 @@ export class AssistantService {
|
|||
current: initialBusinessScopeResolution,
|
||||
temporalGuard,
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
focusDomainHint: focusDomainForGuards
|
||||
focusDomainHint: focusDomainForGuards,
|
||||
userMessage,
|
||||
companyAnchors,
|
||||
followupApplied: Boolean(followupBinding.usage?.applied)
|
||||
});
|
||||
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
|
||||
const requirementExtraction = extractRequirements(resolvedRouteSummary, normalized.normalized, userMessage);
|
||||
|
|
@ -1966,6 +1996,9 @@ export class AssistantService {
|
|||
resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors,
|
||||
domain_polarity_guard: polarityGuardResult.audit,
|
||||
claim_anchor_audit: claimAnchorAudit,
|
||||
settlement_role: claimAnchorAudit.settlement_role ?? null,
|
||||
settlement_role_resolution_reason: claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||||
polarity_resolution_status: claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
|
|
@ -2060,6 +2093,9 @@ export class AssistantService {
|
|||
resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors,
|
||||
domain_polarity_guard: polarityGuardResult.audit,
|
||||
claim_anchor_audit: claimAnchorAudit,
|
||||
settlement_role: claimAnchorAudit.settlement_role ?? null,
|
||||
settlement_role_resolution_reason: claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||||
polarity_resolution_status: claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ export interface ClaimBoundAnchorAuditDebug {
|
|||
| "prove_rbp_tail_state"
|
||||
| "prove_fixed_asset_amortization_coverage";
|
||||
settlement_role?: "supplier" | "customer" | "mixed" | "unknown";
|
||||
settlement_role_resolution_reason?: string[];
|
||||
polarity_resolution_status?: "resolved_supplier" | "resolved_customer" | "mixed" | "unknown" | "not_applicable";
|
||||
required_anchors: string[];
|
||||
resolved_anchors: Record<string, string[]>;
|
||||
missing_anchors: string[];
|
||||
|
|
@ -341,6 +343,9 @@ export interface AssistantDebugPayload {
|
|||
resolved_account_anchors?: string[];
|
||||
domain_polarity_guard?: DomainPolarityGuardDebug;
|
||||
claim_anchor_audit?: ClaimBoundAnchorAuditDebug;
|
||||
settlement_role?: ClaimBoundAnchorAuditDebug["settlement_role"];
|
||||
settlement_role_resolution_reason?: ClaimBoundAnchorAuditDebug["settlement_role_resolution_reason"];
|
||||
polarity_resolution_status?: ClaimBoundAnchorAuditDebug["polarity_resolution_status"];
|
||||
targeted_evidence_acquisition?: TargetedEvidenceAcquisitionDebug;
|
||||
evidence_admissibility_gate?: EvidenceAdmissibilityGateDebug;
|
||||
rbp_live_route_audit?: RbpLiveRouteAuditDebug;
|
||||
|
|
|
|||
|
|
@ -76,6 +76,37 @@ describe("assistant mode API", () => {
|
|||
expect(session.body.session.items.length).toBe(4);
|
||||
});
|
||||
|
||||
it("recovers company-specific scope for settlement follow-up without explicit month anchor", async () => {
|
||||
const app = createApp();
|
||||
const sessionId = `asst-scope-${Date.now()}`;
|
||||
|
||||
const first = await request(app).post("/api/assistant/message").send({
|
||||
session_id: sessionId,
|
||||
useMock: true,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
user_message:
|
||||
"По оплате поставщику на счете 60 в июле 2020 остался хвост. Проверь закрытие по договору и объекту расчетов."
|
||||
});
|
||||
expect(first.status).toBe(200);
|
||||
|
||||
const second = await request(app).post("/api/assistant/message").send({
|
||||
session_id: sessionId,
|
||||
useMock: true,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
user_message: "Это по тому же договору: закрытие подтверждено или хвост все еще живет?"
|
||||
});
|
||||
expect(second.status).toBe(200);
|
||||
|
||||
const debug = second.body.debug ?? {};
|
||||
expect(Array.isArray(debug.business_scope_resolved)).toBe(true);
|
||||
expect(debug.business_scope_resolved).toContain("company_specific_accounting");
|
||||
expect(Array.isArray(debug.scope_resolution_reason)).toBe(true);
|
||||
expect(debug.scope_resolution_reason).toContain("settlement_claim_company_scope_recovery");
|
||||
expect(["resolved_supplier", "resolved_customer", "mixed", "unknown", "not_applicable"]).toContain(
|
||||
String(debug.polarity_resolution_status ?? "")
|
||||
);
|
||||
});
|
||||
|
||||
it("executes factual retrieval for routed fragments", async () => {
|
||||
const app = createApp();
|
||||
|
||||
|
|
|
|||
|
|
@ -243,6 +243,18 @@ describe("stage4 blocker-pack runtime guards", () => {
|
|||
expect(units.some((item: any) => item.lifecycle_domain === "customer_settlement")).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves supplier polarity with dominant 60-account signals despite weak customer noise", () => {
|
||||
const guard = resolveDomainPolarityGuard({
|
||||
userMessage:
|
||||
"Поставщик и счет 60: нужно проверить закрытие долга. В поле комментария случайно есть слово customer.",
|
||||
focusDomainHint: "settlements_60_62"
|
||||
});
|
||||
|
||||
expect(guard.polarity).toBe("supplier_payable");
|
||||
expect(guard.outcome).toBe("passed");
|
||||
expect(guard.reason_codes).not.toContain("unresolved_supplier_customer_polarity");
|
||||
});
|
||||
|
||||
it("cleans polluted account anchors from date/amount numerics", () => {
|
||||
const guard = resolveDomainPolarityGuard({
|
||||
userMessage: "VAT 233.33 on 13 july 2020 and 15 july 2020, account 68.02 in company snapshot.",
|
||||
|
|
@ -496,6 +508,9 @@ describe("stage4 blocker-pack runtime guards", () => {
|
|||
expect(claimAudit.resolved_anchors.vat_signal).toHaveLength(0);
|
||||
expect(claimAudit.resolved_anchors.fixed_asset_signal).toHaveLength(0);
|
||||
expect(claimAudit.required_anchors).toContain("advance_signal");
|
||||
expect(claimAudit.settlement_role).toBe("customer");
|
||||
expect(claimAudit.polarity_resolution_status).toBe("resolved_customer");
|
||||
expect((claimAudit.settlement_role_resolution_reason ?? []).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("keeps VAT priority over supplier wording in shared domain inference", () => {
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue