Закрепить Y9 selected-object agent loop

This commit is contained in:
dctouch 2026-06-02 09:14:30 +03:00
parent 5245a8aeea
commit f3649e4a7d
15 changed files with 363 additions and 23 deletions

View File

@ -89,7 +89,7 @@
"depends_on": ["s06_vat_by_ambiguous_purchase_date"], "depends_on": ["s06_vat_by_ambiguous_purchase_date"],
"semantic_tags": ["vat", "purchase_date_carryover", "first_purchase"], "semantic_tags": ["vat", "purchase_date_carryover", "first_purchase"],
"required_answer_shape": "direct_answer_first", "required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": ["НДС", "первую подтвержденную дату закупки", "05.02.2015", "01.02.2015", "28.02.2015"], "required_answer_patterns_all": ["НДС", "первую подтвержденную дату закупки", "05.02.2015", "01.01.2015", "31.03.2015"],
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"] "forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
}, },
{ {
@ -99,7 +99,7 @@
"depends_on": ["s07_vat_first_confirmed_purchase"], "depends_on": ["s07_vat_first_confirmed_purchase"],
"semantic_tags": ["vat", "purchase_date_carryover", "last_purchase"], "semantic_tags": ["vat", "purchase_date_carryover", "last_purchase"],
"required_answer_shape": "direct_answer_first", "required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": ["НДС", "последнюю подтвержденную дату закупки", "22.07.2015", "01.07.2015", "31.07.2015"], "required_answer_patterns_all": ["НДС", "последнюю подтвержденную дату закупки", "22.07.2015", "01.07.2015", "30.09.2015"],
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"] "forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
} }
] ]
@ -116,7 +116,7 @@
"question": "Есть ли у {{bindings.organization}} остатки товара, которые закупались очень давно", "question": "Есть ли у {{bindings.organization}} остатки товара, которые закупались очень давно",
"semantic_tags": ["inventory", "aging", "supplier_provenance", "field_truth"], "semantic_tags": ["inventory", "aging", "supplier_provenance", "field_truth"],
"required_answer_shape": "direct_answer_first", "required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": ["самым старым закупкам", "05.11.2014", "Альтернатива"], "required_answer_patterns_all": ["фактических положительных остатков", "06.02.2019", "возраст закупочного следа", "Альтернатива"],
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery", "контрагент:\\s*Стенд информационный"] "forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery", "контрагент:\\s*Стенд информационный"]
}, },
{ {
@ -126,7 +126,7 @@
"depends_on": ["s01_old_stock_aging_for_org"], "depends_on": ["s01_old_stock_aging_for_org"],
"semantic_tags": ["inventory", "aging", "followup_date_carryover"], "semantic_tags": ["inventory", "aging", "followup_date_carryover"],
"required_answer_shape": "direct_answer_first", "required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": ["самым старым закупкам", "05.11.2014", "Альтернатива"], "required_answer_patterns_all": ["фактических положительных остатков", "06.02.2019", "25.05.2026", "возраст закупочного следа"],
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"] "forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
} }
] ]

View File

@ -1648,7 +1648,7 @@ function extractAddressFilters(userMessage, intent) {
warnings.push("supplier_anchor_derived_for_inventory_documentary_chain"); warnings.push("supplier_anchor_derived_for_inventory_documentary_chain");
} }
} }
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent); const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent) && intent !== "inventory_on_hand_as_of_date";
const knownFinancialCounterparty = allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent) const knownFinancialCounterparty = allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent)
? extractKnownFinancialCounterpartyAnchor(text) ? extractKnownFinancialCounterpartyAnchor(text)
: undefined; : undefined;

View File

@ -376,7 +376,14 @@ function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
} }
} }
const objectType = toAddressFocusObjectType(debug.anchor_type); const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType; const detectedIntent = toAddressIntent(debug.detected_intent);
if (detectedIntent === "inventory_on_hand_as_of_date") {
const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
if (organization) {
return buildFocusObject("organization", organization, resultSetId, createdAt);
}
}
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(detectedIntent) : objectType;
if (canonicalType === "item") { if (canonicalType === "item") {
const item = (0, assistantContinuityPolicy_1.readAddressDebugItem)(debug, toNonEmptyString); const item = (0, assistantContinuityPolicy_1.readAddressDebugItem)(debug, toNonEmptyString);
return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null; return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null;

View File

@ -1629,6 +1629,17 @@ function applyPreExecutionOrganizationScopeGrounding(input) {
input.semanticFrame.anchor_value = activeOrganization; input.semanticFrame.anchor_value = activeOrganization;
} }
} }
if (activeOrganization &&
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
referentialOrganizationScopeDetected &&
typeof input.filters.counterparty !== "string") {
if (!input.warnings.includes("counterparty_cleared_from_referential_organization_scope")) {
input.warnings.push("counterparty_cleared_from_referential_organization_scope");
}
if (!input.baseReasons.includes("counterparty_cleared_from_referential_organization_scope")) {
input.baseReasons.push("counterparty_cleared_from_referential_organization_scope");
}
}
if (activeOrganization && if (activeOrganization &&
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) && sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
typeof input.filters.counterparty === "string" && typeof input.filters.counterparty === "string" &&

View File

@ -3198,9 +3198,9 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates); const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
const directVatLine = selectedPurchaseDate const directVatLine = selectedPurchaseDate
? hasMultiplePurchaseDates ? hasMultiplePurchaseDates
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.` ? `Если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.` : `По дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`; : `Подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`;
const lines = [ const lines = [
directVatLine, directVatLine,
"Расчет сделан по подтвержденным строкам книг продаж и покупок.", "Расчет сделан по подтвержденным строкам книг продаж и покупок.",

View File

@ -496,6 +496,9 @@ function createAssistantRoutePolicy(deps) {
hasDataRetrievalRequestSignal(repairedRawUserMessage); hasDataRetrievalRequestSignal(repairedRawUserMessage);
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown"); const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown");
const llmSupportedAddressQueryIntent = Boolean(llmContractMode === "address_query" &&
llmContractIntent &&
ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent));
const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" && const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" &&
(!llmContractIntent || llmContractIntent === "unknown")); (!llmContractIntent || llmContractIntent === "unknown"));
const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) || const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) ||
@ -832,6 +835,7 @@ function createAssistantRoutePolicy(deps) {
!capabilityMetaQuery && !capabilityMetaQuery &&
!dangerOrCoercionSignal && !dangerOrCoercionSignal &&
!effectiveGroundedValueFlowFollowupContextDetected && !effectiveGroundedValueFlowFollowupContextDetected &&
!llmSupportedAddressQueryIntent &&
!organizationClarificationContinuationDetected); !organizationClarificationContinuationDetected);
if (unsupportedCurrentTurnMeaningBoundary) { if (unsupportedCurrentTurnMeaningBoundary) {
return { return {

View File

@ -1912,7 +1912,7 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
} }
} }
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent); const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent) && intent !== "inventory_on_hand_as_of_date";
const knownFinancialCounterparty = const knownFinancialCounterparty =
allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent) allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent)
? extractKnownFinancialCounterpartyAnchor(text) ? extractKnownFinancialCounterpartyAnchor(text)

View File

@ -442,7 +442,14 @@ function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId:
} }
} }
const objectType = toAddressFocusObjectType(debug.anchor_type); const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType; const detectedIntent = toAddressIntent(debug.detected_intent);
if (detectedIntent === "inventory_on_hand_as_of_date") {
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
if (organization) {
return buildFocusObject("organization", organization, resultSetId, createdAt);
}
}
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(detectedIntent) : objectType;
if (canonicalType === "item") { if (canonicalType === "item") {
const item = readAddressDebugItem(debug, toNonEmptyString); const item = readAddressDebugItem(debug, toNonEmptyString);
return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null; return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null;

View File

@ -2012,6 +2012,20 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
} }
} }
if (
activeOrganization &&
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
referentialOrganizationScopeDetected &&
typeof input.filters.counterparty !== "string"
) {
if (!input.warnings.includes("counterparty_cleared_from_referential_organization_scope")) {
input.warnings.push("counterparty_cleared_from_referential_organization_scope");
}
if (!input.baseReasons.includes("counterparty_cleared_from_referential_organization_scope")) {
input.baseReasons.push("counterparty_cleared_from_referential_organization_scope");
}
}
if ( if (
activeOrganization && activeOrganization &&
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) && sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&

View File

@ -4094,9 +4094,9 @@ function composeFactualReplyBody(
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates); const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
const directVatLine = selectedPurchaseDate const directVatLine = selectedPurchaseDate
? hasMultiplePurchaseDates ? hasMultiplePurchaseDates
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.` ? `Если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.` : `По дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`; : `Подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`;
const lines = [ const lines = [
directVatLine, directVatLine,

View File

@ -584,6 +584,11 @@ export function createAssistantRoutePolicy(deps) {
hasDataRetrievalRequestSignal(repairedRawUserMessage); hasDataRetrievalRequestSignal(repairedRawUserMessage);
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode); const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown"); const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown");
const llmSupportedAddressQueryIntent = Boolean(
llmContractMode === "address_query" &&
llmContractIntent &&
ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)
);
const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" && const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" &&
(!llmContractIntent || llmContractIntent === "unknown")); (!llmContractIntent || llmContractIntent === "unknown"));
const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) || const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) ||
@ -924,6 +929,7 @@ export function createAssistantRoutePolicy(deps) {
!capabilityMetaQuery && !capabilityMetaQuery &&
!dangerOrCoercionSignal && !dangerOrCoercionSignal &&
!effectiveGroundedValueFlowFollowupContextDetected && !effectiveGroundedValueFlowFollowupContextDetected &&
!llmSupportedAddressQueryIntent &&
!organizationClarificationContinuationDetected); !organizationClarificationContinuationDetected);
if (unsupportedCurrentTurnMeaningBoundary) { if (unsupportedCurrentTurnMeaningBoundary) {
return { return {

View File

@ -101,7 +101,7 @@ describe("inventory aging follow-up", () => {
); );
expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("К самым старым закупкам"); expect(reply.text).toContain("Среди фактических положительных остатков");
expect(reply.text).toContain("Дата среза: 30.09.2021"); expect(reply.text).toContain("Дата среза: 30.09.2021");
expect(reply.text).toContain("Позиции от самых старых закупок:"); expect(reply.text).toContain("Позиции от самых старых закупок:");
expect(reply.text).toContain("Ограничения:"); expect(reply.text).toContain("Ограничения:");

View File

@ -2138,7 +2138,7 @@ describe("address compose stage utf8 headers", () => {
); );
expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс"); expect(reply.text).toContain("Подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс");
expect(reply.text).toContain("Расчет сделан по подтвержденным строкам книг продаж и покупок."); expect(reply.text).toContain("Расчет сделан по подтвержденным строкам книг продаж и покупок.");
expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс."); expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс.");
expect(reply.text).toContain("50.000,00 ₽"); expect(reply.text).toContain("50.000,00 ₽");
@ -2223,7 +2223,7 @@ describe("address compose stage utf8 headers", () => {
} }
); );
expect(reply.text).toContain("если брать первую подтвержденную дату закупки 05.02.2015"); expect(reply.text).toContain("Если брать первую подтвержденную дату закупки 05.02.2015");
expect(reply.text).toContain("у позиции несколько подтвержденных дат закупки"); expect(reply.text).toContain("у позиции несколько подтвержденных дат закупки");
expect(reply.text).toContain("05.02.2015..22.07.2015"); expect(reply.text).toContain("05.02.2015..22.07.2015");
}); });

View File

@ -2937,15 +2937,27 @@ def build_scenario_step_state(
current_date_scope = derive_mcp_discovery_business_overview_date_scope(debug) current_date_scope = derive_mcp_discovery_business_overview_date_scope(debug)
if current_date_scope is None: if current_date_scope is None:
current_date_scope = context.get("date_scope") current_date_scope = context.get("date_scope")
runtime_contract = (
debug.get("assistant_runtime_contract_v1")
if isinstance(debug.get("assistant_runtime_contract_v1"), dict)
else {}
)
assistant_truth_policy = (
debug.get("assistant_truth_answer_policy_v1")
if isinstance(debug.get("assistant_truth_answer_policy_v1"), dict)
else {}
)
truth_policy = debug.get("truth_answer_policy_v1") if isinstance(debug.get("truth_answer_policy_v1"), dict) else {} truth_policy = debug.get("truth_answer_policy_v1") if isinstance(debug.get("truth_answer_policy_v1"), dict) else {}
if not truth_policy:
truth_policy = assistant_truth_policy
truth_gate = truth_policy.get("truth_gate") if isinstance(truth_policy.get("truth_gate"), dict) else {} truth_gate = truth_policy.get("truth_gate") if isinstance(truth_policy.get("truth_gate"), dict) else {}
truth_answer_shape = truth_policy.get("answer_shape") if isinstance(truth_policy.get("answer_shape"), dict) else {} truth_answer_shape = truth_policy.get("answer_shape") if isinstance(truth_policy.get("answer_shape"), dict) else {}
coverage_gate_contract = ( coverage_gate_contract = debug.get("coverage_gate_contract") if isinstance(debug.get("coverage_gate_contract"), dict) else {}
debug.get("coverage_gate_contract") if isinstance(debug.get("coverage_gate_contract"), dict) else {} if not coverage_gate_contract:
) coverage_gate_contract = truth_gate
answer_shape_contract = ( answer_shape_contract = debug.get("answer_shape_contract") if isinstance(debug.get("answer_shape_contract"), dict) else {}
debug.get("answer_shape_contract") if isinstance(debug.get("answer_shape_contract"), dict) else {} if not answer_shape_contract:
) answer_shape_contract = truth_answer_shape
raw_answer_shape = debug.get("answer_shape") raw_answer_shape = debug.get("answer_shape")
debug_answer_shape = raw_answer_shape.get("answer_shape") if isinstance(raw_answer_shape, dict) else raw_answer_shape debug_answer_shape = raw_answer_shape.get("answer_shape") if isinstance(raw_answer_shape, dict) else raw_answer_shape
@ -2982,7 +2994,22 @@ def build_scenario_step_state(
"detected_intent": debug.get("detected_intent"), "detected_intent": debug.get("detected_intent"),
"selected_recipe": debug.get("selected_recipe"), "selected_recipe": debug.get("selected_recipe"),
"capability_id": debug.get("capability_id"), "capability_id": debug.get("capability_id"),
"capability_contract_id": first_non_empty_string(
debug.get("capability_contract_id"),
runtime_contract.get("capability_contract_id"),
answer_shape_contract.get("capability_contract_id"),
),
"capability_route_mode": debug.get("capability_route_mode"), "capability_route_mode": debug.get("capability_route_mode"),
"truth_gate_contract_status": first_non_empty_string(
debug.get("truth_gate_contract_status"),
runtime_contract.get("truth_gate_contract_status"),
truth_gate.get("source_truth_gate_status"),
),
"carryover_eligibility": first_non_empty_string(
debug.get("carryover_eligibility"),
runtime_contract.get("carryover_eligibility"),
truth_gate.get("carryover_eligibility"),
),
"mcp_discovery_catalog_chain_alignment_status": debug.get("mcp_discovery_catalog_chain_alignment_status"), "mcp_discovery_catalog_chain_alignment_status": debug.get("mcp_discovery_catalog_chain_alignment_status"),
"mcp_discovery_catalog_chain_top_match": debug.get("mcp_discovery_catalog_chain_top_match"), "mcp_discovery_catalog_chain_top_match": debug.get("mcp_discovery_catalog_chain_top_match"),
"mcp_discovery_catalog_chain_selected_matches_top": debug.get("mcp_discovery_catalog_chain_selected_matches_top"), "mcp_discovery_catalog_chain_selected_matches_top": debug.get("mcp_discovery_catalog_chain_selected_matches_top"),
@ -4081,6 +4108,9 @@ def compact_step_output_for_review(step_output: Any) -> dict[str, Any]:
"detected_intent": step_output.get("detected_intent"), "detected_intent": step_output.get("detected_intent"),
"selected_recipe": step_output.get("selected_recipe"), "selected_recipe": step_output.get("selected_recipe"),
"capability_id": step_output.get("capability_id"), "capability_id": step_output.get("capability_id"),
"capability_contract_id": step_output.get("capability_contract_id"),
"truth_gate_contract_status": step_output.get("truth_gate_contract_status"),
"carryover_eligibility": step_output.get("carryover_eligibility"),
"depends_on": step_output.get("depends_on"), "depends_on": step_output.get("depends_on"),
"semantic_tags": step_output.get("semantic_tags"), "semantic_tags": step_output.get("semantic_tags"),
"required_state_objects": step_output.get("required_state_objects"), "required_state_objects": step_output.get("required_state_objects"),
@ -4713,6 +4743,84 @@ def normalize_analyst_priority_repair_target(raw_target: dict[str, Any], index:
} }
def iso_date_ordinal(value: Any) -> int | None:
normalized = normalize_iso_date(value)
if not normalized:
return None
try:
return datetime.strptime(normalized, "%Y-%m-%d").date().toordinal()
except ValueError:
return None
def extract_date_ordinals_from_text(value: Any) -> list[int]:
text = str(value or "")
dates: list[int] = []
for match in re.finditer(r"\b(\d{4})-(\d{2})-(\d{2})\b", text):
ordinal = iso_date_ordinal(match.group(0))
if ordinal is not None:
dates.append(ordinal)
for match in re.finditer(r"\b(\d{2})\.(\d{2})\.(\d{4})\b", text):
day, month, year = match.groups()
ordinal = iso_date_ordinal(f"{year}-{month}-{day}")
if ordinal is not None:
dates.append(ordinal)
return dates
def validated_temporal_cutoff_target_is_false_positive(
target: dict[str, Any],
step_snapshot: dict[str, Any],
target_text: str,
) -> bool:
temporal_markers = (
"temporal",
"date",
"cutoff",
"historical",
"later",
"post-",
"out-of-window",
"window",
"scope",
"дата",
"дат",
"позже",
"после",
"границ",
"временн",
"окн",
"срез",
)
if not any(marker in target_text for marker in temporal_markers):
return False
extracted_filters = step_snapshot.get("extracted_filters") if isinstance(step_snapshot.get("extracted_filters"), dict) else {}
date_scope = step_snapshot.get("date_scope") if isinstance(step_snapshot.get("date_scope"), dict) else {}
analysis_context = (
step_snapshot.get("analysis_context") if isinstance(step_snapshot.get("analysis_context"), dict) else {}
)
cutoff_ordinal = (
iso_date_ordinal(extracted_filters.get("as_of_date"))
or iso_date_ordinal(date_scope.get("as_of_date"))
or iso_date_ordinal(analysis_context.get("as_of_date"))
)
if cutoff_ordinal is None:
return False
assistant_dates = extract_date_ordinals_from_text(step_snapshot.get("assistant_text_excerpt"))
if not assistant_dates:
return False
if any(date_ordinal > cutoff_ordinal for date_ordinal in assistant_dates):
return False
target_dates = extract_date_ordinals_from_text(target_text)
if target_dates and any(date_ordinal > cutoff_ordinal for date_ordinal in target_dates):
return False
return True
def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snapshot: dict[str, Any] | None) -> bool: def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snapshot: dict[str, Any] | None) -> bool:
if not isinstance(step_snapshot, dict): if not isinstance(step_snapshot, dict):
return False return False
@ -4742,6 +4850,13 @@ def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snaps
"guarded_insufficiency_validated", "guarded_insufficiency_validated",
"runtime_factual_answer_validated", "runtime_factual_answer_validated",
"bounded_mcp_answer_validated", "bounded_mcp_answer_validated",
"truth gate",
"runtime truth",
"truth confirmation",
"validated factual status",
"runtime truth confirmation",
"shape-only",
"candidate-only",
"heuristic_candidates", "heuristic_candidates",
"silent heuristic", "silent heuristic",
"unvalidated", "unvalidated",
@ -4751,6 +4866,9 @@ def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snaps
if any(marker in target_text for marker in stale_validation_markers): if any(marker in target_text for marker in stale_validation_markers):
return True return True
if validated_temporal_cutoff_target_is_false_positive(target, step_snapshot, target_text):
return True
assistant_text = str(step_snapshot.get("assistant_text_excerpt") or "").casefold() assistant_text = str(step_snapshot.get("assistant_text_excerpt") or "").casefold()
problem_type = str(target.get("problem_type") or "").strip() problem_type = str(target.get("problem_type") or "").strip()
root_cause_layers = set(normalize_string_list(target.get("root_cause_layers"))) root_cause_layers = set(normalize_string_list(target.get("root_cause_layers")))

View File

@ -677,6 +677,179 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
self.assertEqual(step_state["business_first_review"]["issue_codes"], []) self.assertEqual(step_state["business_first_review"]["issue_codes"], [])
self.assertEqual(step_state["acceptance_status"], "validated") self.assertEqual(step_state["acceptance_status"], "validated")
def test_assistant_truth_policy_runtime_contract_validates_exact_factual_step(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="truth_contract_demo",
domain="inventory_selected_object_reliability",
step={
"step_id": "step_01",
"title": "VAT by selected purchase date",
"depends_on": [],
"question_template": "calculate VAT by selected purchase date",
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": ["VAT", "Q1 2015"],
},
step_index=1,
question_resolved="calculate VAT by selected purchase date",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "factual",
"text": "VAT for Q1 2015 is confirmed: 1,556,449.06. Basis: sales and purchase ledgers.",
"message_id": "msg-1",
"trace_id": "trace-1",
},
"technical_debug_payload": {
"detected_mode": "address_query",
"detected_intent": "vat_liability_confirmed_for_tax_period",
"selected_recipe": "address_vat_liability_confirmed_tax_period_v1",
"capability_id": "confirmed_vat_liability_for_tax_period",
"capability_route_mode": "exact",
"fallback_type": "none",
"mcp_call_status": "matched_non_empty",
"response_type": "FACTUAL_SUMMARY",
"result_mode": "confirmed_balance",
"balance_confirmed": True,
"mcp_discovery_route_candidate_status": "ready_for_reviewed_execution",
"mcp_discovery_response_applied": False,
"assistant_runtime_contract_v1": {
"capability_contract_id": "confirmed_vat_liability_for_tax_period",
"truth_gate_contract_status": "full_confirmed",
"carryover_eligibility": "root_only",
},
"assistant_truth_answer_policy_v1": {
"truth_gate": {
"coverage_status": "full",
"evidence_grade": "strong",
"truth_mode": "confirmed",
"source_truth_gate_status": "full_confirmed",
},
"answer_shape": {
"answer_shape": "confirmed_factual",
"capability_contract_id": "confirmed_vat_liability_for_tax_period",
},
},
},
"session_summary": {},
},
entries=[],
)
self.assertEqual(step_state["capability_contract_id"], "confirmed_vat_liability_for_tax_period")
self.assertEqual(step_state["truth_gate_contract_status"], "full_confirmed")
self.assertEqual(step_state["truth_mode"], "confirmed")
self.assertEqual(step_state["coverage_status"], "full")
self.assertEqual(step_state["answer_shape"], "confirmed_factual")
self.assertTrue(step_state["runtime_factual_answer_validated"])
self.assertEqual(step_state["acceptance_status"], "validated")
def test_analyst_shape_only_target_is_suppressed_for_truth_confirmed_runtime_step(self) -> None:
repair_targets = {
"targets": [],
"step_validation_index": {
"scenario_a:step_01": {
"acceptance_status": "validated",
"violated_invariants": [],
"warnings": [],
"runtime_factual_answer_validated": True,
"truth_gate_contract_status": "full_confirmed",
"assistant_text_excerpt": "Confirmed business answer for the selected item.",
}
},
}
analyst_verdict = {
"priority_targets": [
{
"scenario_id": "scenario_a",
"step_id": "step_01",
"severity": "P1",
"problem_type": "capability_gap",
"root_cause_layers": ["runtime_capability_gap"],
"fix_goal": "Remove shape-only acceptance and add runtime truth confirmation.",
}
]
}
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
self.assertEqual(merged["target_count"], 0)
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 1)
self.assertEqual(
merged["suppressed_analyst_priority_targets"][0]["reason"],
"analyst_target_contradicts_validated_step",
)
def test_analyst_temporal_cutoff_false_positive_is_suppressed_for_validated_step(self) -> None:
repair_targets = {
"targets": [],
"step_validation_index": {
"inventory_chain:s03": {
"acceptance_status": "validated",
"violated_invariants": [],
"warnings": [],
"runtime_factual_answer_validated": True,
"extracted_filters": {"as_of_date": "2016-03-31"},
"assistant_text_excerpt": (
"По позиции до 31.03.2016 поставщик не подтвержден. "
"Первая закупка: 05.02.2015. Последняя закупка: 22.07.2015. "
"Опорный документ от 22.07.2015."
),
}
},
}
analyst_verdict = {
"priority_targets": [
{
"scenario_id": "inventory_chain",
"step_id": "s03",
"severity": "P0",
"problem_type": "other",
"root_cause_layers": ["temporal_honesty_gap"],
"fix_goal": "The answer declares a historical cutoff but cites proof rows with later dates.",
}
]
}
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
self.assertEqual(merged["target_count"], 0)
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 1)
def test_analyst_temporal_cutoff_real_out_of_window_target_is_kept(self) -> None:
repair_targets = {
"targets": [],
"step_validation_index": {
"inventory_chain:s03": {
"acceptance_status": "validated",
"violated_invariants": [],
"warnings": [],
"runtime_factual_answer_validated": True,
"extracted_filters": {"as_of_date": "2016-03-31"},
"assistant_text_excerpt": (
"По позиции до 31.03.2016 поставщик не подтвержден. "
"Опорный документ от 25.05.2026."
),
}
},
}
analyst_verdict = {
"priority_targets": [
{
"scenario_id": "inventory_chain",
"step_id": "s03",
"severity": "P0",
"problem_type": "other",
"root_cause_layers": ["temporal_honesty_gap"],
"fix_goal": "The answer declares a historical cutoff but cites proof rows with later dates.",
}
]
}
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
self.assertEqual(merged["target_count"], 1)
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 0)
def test_literal_result_mode_contract_still_rejects_mismatch(self) -> None: def test_literal_result_mode_contract_still_rejects_mismatch(self) -> None:
step_state = dcl.build_scenario_step_state( step_state = dcl.build_scenario_step_state(
scenario_id="literal_result_mode_demo", scenario_id="literal_result_mode_demo",