ARCH: удержать contracts pivot после payments follow-up

This commit is contained in:
dctouch 2026-04-23 20:40:09 +03:00
parent 7e1a2edadb
commit d7ee95286d
5 changed files with 185 additions and 0 deletions

View File

@ -0,0 +1,64 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase76_contracts_to_payments_all_time",
"domain": "address_phase76_contracts_to_payments_all_time",
"title": "Phase 76 contracts to payments all-time continuity",
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to contracts, then pivots again to payments via pronoun follow-up, and finally switches to all-time scope without renaming the counterparty.",
"bindings": {},
"steps": [
{
"step_id": "step_01_documents_by_counterparty",
"title": "Open documents for the counterparty",
"question": "Покажи документы по Жуковке 51.",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)жуковк",
"(?i)документ|сч[её]т|акт|накладн|строк"
],
"criticality": "critical",
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
},
{
"step_id": "step_02_contracts_by_pronoun_followup",
"title": "Pivot to contracts",
"question": "А по нему договоры?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)жуковк|контрагент",
"(?i)договор|контракт|соглаш"
],
"criticality": "critical",
"semantic_tags": ["contracts_followup", "counterparty_pronoun_resolution", "integrity_guard"]
},
{
"step_id": "step_03_payments_after_contracts_pivot",
"title": "Pivot again to payments",
"question": "А по нему платежи?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)жуковк|контрагент",
"(?i)платеж|операц|банк|поступлен|списан"
],
"criticality": "critical",
"semantic_tags": ["payments_followup", "second_pivot", "integrity_guard"]
},
{
"step_id": "step_04_all_time_after_second_pivot",
"title": "Switch to all-time scope after the second pivot",
"question": "А за все время?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)жуковк|контрагент",
"(?i)платеж|операц|банк|поступлен|списан"
],
"forbidden_answer_patterns": [
"(?i)уточните .* контрагент",
"(?i)метадан",
"(?i)схем",
"(?i)объект[а-я]* 1с"
],
"criticality": "critical",
"semantic_tags": ["all_time_after_second_pivot", "payments_followup", "integrity_guard"]
}
]
}

View File

@ -0,0 +1,65 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase77_payments_to_contracts_year_switch",
"domain": "address_phase77_payments_to_contracts_year_switch",
"title": "Phase 77 payments to contracts year-switch continuity",
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to payments, then pivots again to contracts via pronoun follow-up, and finally switches the year without renaming the counterparty.",
"bindings": {},
"steps": [
{
"step_id": "step_01_documents_by_counterparty",
"title": "Open documents for the counterparty",
"question": "Покажи документы по Жуковке 51.",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)жуковк",
"(?i)документ|сч[её]т|акт|накладн|строк"
],
"criticality": "critical",
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
},
{
"step_id": "step_02_payments_by_pronoun_followup",
"title": "Pivot to payments",
"question": "А по нему платежи?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)жуковк|контрагент",
"(?i)платеж|операц|банк|поступлен|списан"
],
"criticality": "critical",
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
},
{
"step_id": "step_03_contracts_after_payments_pivot",
"title": "Pivot again to contracts",
"question": "А по нему договоры?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)жуковк|контрагент",
"(?i)договор|контракт|соглаш"
],
"criticality": "critical",
"semantic_tags": ["contracts_followup", "second_pivot", "integrity_guard"]
},
{
"step_id": "step_04_year_switch_after_second_pivot",
"title": "Switch the year after the second pivot",
"question": "А за 2021?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)жуковк|контрагент",
"(?i)договор|контракт|соглаш"
],
"forbidden_answer_patterns": [
"(?i)уточните .* контрагент",
"(?i)метадан",
"(?i)схем",
"(?i)объект[а-я]* 1с"
],
"criticality": "critical",
"semantic_tags": ["year_switch_after_second_pivot", "contracts_followup", "integrity_guard"]
}
]
}

View File

@ -554,6 +554,7 @@ function createAssistantTransitionPolicy(deps) {
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null;
@ -567,6 +568,7 @@ function createAssistantTransitionPolicy(deps) {
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null;
@ -683,6 +685,7 @@ function createAssistantTransitionPolicy(deps) {
}
hasPrimaryFollowupSignal =
deps.hasAddressFollowupContextSignal(userMessage) ||
hasSuggestedIntentPivotSignal ||
Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary ||
inventoryShortFollowupPrimary ||
@ -690,6 +693,7 @@ function createAssistantTransitionPolicy(deps) {
hasInventoryRootTemporalFollowupPrimary;
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
hasSuggestedIntentPivotSignal ||
Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate ||
inventoryShortFollowupAlternate ||
@ -700,6 +704,7 @@ function createAssistantTransitionPolicy(deps) {
hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
hasSuggestedIntentPivotSignal ||
hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||

View File

@ -751,6 +751,7 @@ export function createAssistantTransitionPolicy(deps) {
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal
) {
@ -766,6 +767,7 @@ export function createAssistantTransitionPolicy(deps) {
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal
) {
@ -944,6 +946,7 @@ export function createAssistantTransitionPolicy(deps) {
}
hasPrimaryFollowupSignal =
deps.hasAddressFollowupContextSignal(userMessage) ||
hasSuggestedIntentPivotSignal ||
Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary ||
inventoryShortFollowupPrimary ||
@ -951,6 +954,7 @@ export function createAssistantTransitionPolicy(deps) {
hasInventoryRootTemporalFollowupPrimary;
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
hasSuggestedIntentPivotSignal ||
Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate ||
inventoryShortFollowupAlternate ||
@ -961,6 +965,7 @@ export function createAssistantTransitionPolicy(deps) {
hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
hasSuggestedIntentPivotSignal ||
hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||

View File

@ -737,6 +737,52 @@ describe("assistantTransitionPolicy", () => {
expect(contract.decision_reasons).toContain("suggested_intent_followup_pivot");
});
it("switches from payments to suggested contracts on a pronoun follow-up even when generic follow-up signal is weak", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Собран список банковских операций по контрагенту ТСЖ \\Жуковка 51\\.",
debug: {
detected_intent: "bank_operations_by_counterparty",
extracted_filters: {
counterparty: "ТСЖ \\Жуковка 51\\"
},
anchor_type: "counterparty",
anchor_value_resolved: "ТСЖ \\Жуковка 51\\"
}
}),
buildAddressFollowupOffer: () => ({
enabled: true,
source_intent: "bank_operations_by_counterparty",
suggested_intents: ["list_documents_by_counterparty", "list_contracts_by_counterparty"]
}),
hasAddressFollowupContextSignal: () => false,
hasReferentialPointer: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext("А по нему договоры?", [], null, null, null);
expect(carryover?.followupContext?.previous_intent).toBe("list_contracts_by_counterparty");
expect(carryover?.followupContext?.target_intent).toBe("list_contracts_by_counterparty");
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(carryover?.hasSuggestedIntentPivotSignal).toBe(true);
expect(carryover?.followupSelectionMode).toBe("switch_to_suggested_intent");
const contract = policy.buildAddressDialogContinuationContractV2(
"А по нему договоры?",
"Покажи договоры, связанные с указанным объектом",
carryover,
{
predecomposeContract: {
intent: "unknown"
}
}
);
expect(contract.decision).toBe("switch_to_suggested");
expect(contract.target_intent).toBe("list_contracts_by_counterparty");
expect(contract.decision_reasons).toContain("suggested_intent_followup_pivot");
});
it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({