Этап 4 / Волна 10: корректировка settlement-кейса — доменная фиксация синтеза, честное покрытие, удержание фокуса / Этап 4 / Волна 11: бизнес-якоря, доменное заземление и устранение утечки дебага
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
DeterministicRouteHint,
|
||||
NoRouteReason,
|
||||
NormalizedPayload,
|
||||
NormalizedQueryV1,
|
||||
@@ -42,6 +43,244 @@ function toRouteHintSummaryV1(normalized: NormalizedQueryV1): RouteHintSummaryV1
|
||||
type V2Family = NormalizedQueryV2 | NormalizedQueryV2_0_1 | NormalizedQueryV2_0_2;
|
||||
type V2FamilyFragment = V2Family["fragments"][number];
|
||||
|
||||
type RouteQueryClass =
|
||||
| "exact_object_trace"
|
||||
| "ranking_or_period_summary"
|
||||
| "symptom_first"
|
||||
| "lifecycle_first"
|
||||
| "chain_break"
|
||||
| "period_impact"
|
||||
| "causal_query"
|
||||
| "mixed_ambiguity"
|
||||
| "rule_check_without_symptom"
|
||||
| "canonical_fact_lookup";
|
||||
|
||||
interface RouteDisciplineRule {
|
||||
query_class: RouteQueryClass;
|
||||
required_route: Exclude<DeterministicRouteHint, "no_route">;
|
||||
allowed_fallback: DeterministicRouteHint[];
|
||||
forbidden_fallback: DeterministicRouteHint[];
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ACCOUNT_HINT_PATTERN =
|
||||
/(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
||||
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
||||
const SYMPTOM_MARKER_PATTERN =
|
||||
/(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
||||
const LIFECYCLE_MARKER_PATTERN =
|
||||
/(?:\blifecycle\b|\bchain\b|\btransition\b|\bstep\b|\btrace\b|цепоч|этап|переход|связк|где\s+разрыв)/i;
|
||||
const CHAIN_BREAK_PATTERN =
|
||||
/(?:\bbreak\b|\bbroken\b|\bgap\b|missing\s+(?:transition|step|link)|chain\s+break|разрыв|обрыв|нет\s+переход|не\s+дошл|не\s+наблюд)/i;
|
||||
const PERIOD_IMPACT_PATTERN =
|
||||
/(?:period\s*close|month\s*close|month-end|residual|allocation|20[/-]44|закрыти|остатк|распредел|конец\s+месяц)/i;
|
||||
const CAUSAL_PATTERN = /(?:\bwhy\b|\bbecause\b|\breason\b|explain\s+mechanism|почему|объясни|механизм|причин)/i;
|
||||
const AMBIGUITY_PATTERN =
|
||||
/(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i;
|
||||
const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i;
|
||||
const DOMAIN_LEXICAL_ANCHOR_PATTERN =
|
||||
/(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|РЅРґСЃ|СЃС‡[её]С‚.?фактур|РєРЅРёРі[аи]|затрат|закрыт|остатк)/i;
|
||||
|
||||
export const ROUTE_DISCIPLINE_RULE_TABLE: RouteDisciplineRule[] = [
|
||||
{
|
||||
query_class: "exact_object_trace",
|
||||
required_route: "live_mcp_drilldown",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live", "store_feature_risk", "batch_refresh_then_store"],
|
||||
description: "Exact object trace queries always run via live drilldown."
|
||||
},
|
||||
{
|
||||
query_class: "ranking_or_period_summary",
|
||||
required_route: "batch_refresh_then_store",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
||||
description: "Ranking and period summary queries require analytical batch path."
|
||||
},
|
||||
{
|
||||
query_class: "symptom_first",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Symptom-first intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "lifecycle_first",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Lifecycle-first intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "chain_break",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Chain-break intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "period_impact",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Period-impact problem intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "causal_query",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Causal/mechanism intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "mixed_ambiguity",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Mixed ambiguity keeps hybrid as primary route, with explicit no-route fallback."
|
||||
},
|
||||
{
|
||||
query_class: "rule_check_without_symptom",
|
||||
required_route: "store_feature_risk",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Rule checks without symptom/lifecycle signals run via risk profile path."
|
||||
},
|
||||
{
|
||||
query_class: "canonical_fact_lookup",
|
||||
required_route: "store_canonical",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["hybrid_store_plus_live"],
|
||||
description: "Only plain factual lookups are allowed to stay on canonical path."
|
||||
}
|
||||
];
|
||||
|
||||
const ROUTE_DISCIPLINE_RULE_MAP = new Map<RouteQueryClass, RouteDisciplineRule>(
|
||||
ROUTE_DISCIPLINE_RULE_TABLE.map((item) => [item.query_class, item])
|
||||
);
|
||||
|
||||
function mergedFragmentText(fragment: V2FamilyFragment): string {
|
||||
return `${fragment.raw_fragment_text ?? ""} ${fragment.normalized_fragment_text ?? ""}`.toLowerCase();
|
||||
}
|
||||
|
||||
function hasLifecycleDomainHint(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||
const accountHints = Array.isArray(fragment.account_hints) ? fragment.account_hints.map((item) => String(item)) : [];
|
||||
if (accountHints.some((item) => /^(97|01|02|08|19|20|21|23|25|26|28|29|44|68(?:\.\d+)?|51|60|62)$/.test(item))) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
fragment.candidate_labels.includes("anomaly_probe") ||
|
||||
fragment.candidate_labels.includes("period_close_risk") ||
|
||||
PERIOD_IMPACT_PATTERN.test(lowerText)
|
||||
);
|
||||
}
|
||||
|
||||
function hasSymptomSignal(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||
return (
|
||||
fragment.flags.asks_for_anomaly_scan ||
|
||||
fragment.candidate_labels.includes("anomaly_probe") ||
|
||||
fragment.candidate_labels.includes("period_close_risk") ||
|
||||
SYMPTOM_MARKER_PATTERN.test(lowerText) ||
|
||||
TRANSLIT_PROBLEM_PATTERN.test(lowerText)
|
||||
);
|
||||
}
|
||||
|
||||
function hasLifecycleSignal(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||
return (
|
||||
fragment.flags.asks_for_chain_explanation ||
|
||||
fragment.flags.mentions_period_close_context ||
|
||||
LIFECYCLE_MARKER_PATTERN.test(lowerText) ||
|
||||
hasLifecycleDomainHint(fragment, lowerText)
|
||||
);
|
||||
}
|
||||
|
||||
function hasChainBreakSignal(lowerText: string): boolean {
|
||||
return CHAIN_BREAK_PATTERN.test(lowerText);
|
||||
}
|
||||
|
||||
function hasPeriodImpactSignal(lowerText: string): boolean {
|
||||
return PERIOD_IMPACT_PATTERN.test(lowerText);
|
||||
}
|
||||
|
||||
function hasCausalSignal(lowerText: string): boolean {
|
||||
return CAUSAL_PATTERN.test(lowerText);
|
||||
}
|
||||
|
||||
function hasAmbiguitySignal(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||
return (
|
||||
AMBIGUITY_PATTERN.test(lowerText) ||
|
||||
fragment.confidence === "low" ||
|
||||
fragment.domain_relevance === "unclear" ||
|
||||
fragment.business_scope === "unclear"
|
||||
);
|
||||
}
|
||||
|
||||
function hasAccountOrPeriodAnchor(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
||||
}
|
||||
|
||||
function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
||||
const lowerText = mergedFragmentText(fragment);
|
||||
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
||||
const lifecycleSignal = hasLifecycleSignal(fragment, lowerText);
|
||||
const chainBreakSignal = hasChainBreakSignal(lowerText);
|
||||
const periodImpactSignal = hasPeriodImpactSignal(lowerText);
|
||||
const causalSignal = hasCausalSignal(lowerText);
|
||||
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
||||
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
||||
|
||||
if (fragment.flags.asks_for_exact_object_trace) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace")!;
|
||||
}
|
||||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary")!;
|
||||
}
|
||||
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity")!;
|
||||
}
|
||||
if (chainBreakSignal) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("chain_break")!;
|
||||
}
|
||||
if (periodImpactSignal && accountOrPeriodAnchor) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("period_impact")!;
|
||||
}
|
||||
if (lifecycleSignal) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("lifecycle_first")!;
|
||||
}
|
||||
if (symptomSignal) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("symptom_first")!;
|
||||
}
|
||||
if (causalSignal && accountOrPeriodAnchor) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("causal_query")!;
|
||||
}
|
||||
if (fragment.flags.asks_for_rule_check) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("rule_check_without_symptom")!;
|
||||
}
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("canonical_fact_lookup")!;
|
||||
}
|
||||
|
||||
function shouldPromoteFromNoRoute(fragment: V2FamilyFragment, rule: RouteDisciplineRule): boolean {
|
||||
if (rule.required_route === "store_canonical") {
|
||||
return false;
|
||||
}
|
||||
if (explicitNoRouteReason(fragment) === "out_of_scope") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lowerText = mergedFragmentText(fragment);
|
||||
const hasProblemSignal =
|
||||
hasSymptomSignal(fragment, lowerText) ||
|
||||
hasLifecycleSignal(fragment, lowerText) ||
|
||||
hasChainBreakSignal(lowerText) ||
|
||||
hasPeriodImpactSignal(lowerText) ||
|
||||
hasCausalSignal(lowerText);
|
||||
|
||||
const hasAnchor =
|
||||
hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
||||
fragment.candidate_labels.includes("cross_entity") ||
|
||||
DOMAIN_LEXICAL_ANCHOR_PATTERN.test(lowerText);
|
||||
return hasProblemSignal && hasAnchor;
|
||||
}
|
||||
|
||||
function reasonForNoRoute(noRouteReason: NoRouteReason | null | undefined): string {
|
||||
if (noRouteReason === "out_of_scope") {
|
||||
return "Fragment is out-of-scope for company-specific accounting contour.";
|
||||
@@ -98,37 +337,33 @@ function decideRouteForFragment(fragment: V2FamilyFragment): RouteDecisionV2 {
|
||||
const readiness = executionReadiness(fragment);
|
||||
const clarification = clarificationReason(fragment);
|
||||
const soft = softAssumptions(fragment);
|
||||
const routeRule = resolveRouteClass(fragment);
|
||||
|
||||
if (status === "no_route") {
|
||||
return buildNoRouteDecision(fragment, noRouteReason);
|
||||
}
|
||||
|
||||
if (readiness === "needs_clarification" || readiness === "no_route") {
|
||||
return buildNoRouteDecision(fragment, noRouteReason ?? "insufficient_specificity");
|
||||
}
|
||||
|
||||
if (fragment.domain_relevance !== "in_scope") {
|
||||
if (fragment.domain_relevance === "out_of_scope") {
|
||||
return buildNoRouteDecision(fragment, "out_of_scope");
|
||||
}
|
||||
|
||||
if (fragment.flags.asks_for_exact_object_trace) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "live_mcp_drilldown",
|
||||
reason: "Exact object trace requested."
|
||||
};
|
||||
if (status === "no_route" || readiness === "needs_clarification" || readiness === "no_route") {
|
||||
if (shouldPromoteFromNoRoute(fragment, routeRule)) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: routeRule.required_route,
|
||||
reason: `${routeRule.description} Query class: ${routeRule.query_class}. Promoted from no-route by anchor/symptom guardrail.`
|
||||
};
|
||||
}
|
||||
return buildNoRouteDecision(fragment, noRouteReason);
|
||||
}
|
||||
|
||||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||
if (status === "routed" || status === null) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
@@ -140,80 +375,8 @@ function decideRouteForFragment(fragment: V2FamilyFragment): RouteDecisionV2 {
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "batch_refresh_then_store",
|
||||
reason: "Ranking/summary semantics require batch analytical route."
|
||||
};
|
||||
}
|
||||
|
||||
if (fragment.flags.has_multi_entity_scope && fragment.flags.asks_for_chain_explanation) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "hybrid_store_plus_live",
|
||||
reason: "Multi-entity causal chain requested."
|
||||
};
|
||||
}
|
||||
|
||||
if (fragment.flags.asks_for_rule_check && !fragment.flags.asks_for_chain_explanation) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "store_feature_risk",
|
||||
reason: "Rule-control check without causal decomposition."
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
fragment.flags.asks_for_anomaly_scan &&
|
||||
!fragment.flags.asks_for_ranking_or_top &&
|
||||
!(fragment.flags.has_multi_entity_scope && fragment.flags.asks_for_chain_explanation)
|
||||
) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "store_feature_risk",
|
||||
reason: "Anomaly scan without heavy ranking or causal chain."
|
||||
};
|
||||
}
|
||||
|
||||
if (status === "routed") {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "store_canonical",
|
||||
reason: "Routed fragment without deep analytical or causal signals."
|
||||
route: routeRule.required_route,
|
||||
reason: `${routeRule.description} Query class: ${routeRule.query_class}. Allowed fallback: ${routeRule.allowed_fallback.join(", ")}. Forbidden fallback: ${routeRule.forbidden_fallback.join(", ")}.`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user