Этап 4 / Волна 19.1 выравнивание живого контура для слоя адресного сбора доказательной базы

This commit is contained in:
2026-03-29 08:33:54 +03:00
parent 60d8b96a14
commit d461cedf35
19 changed files with 2389 additions and 66 deletions
@@ -82,6 +82,83 @@ function extractExecutionState(normalized) {
};
});
}
function collectBusinessScopesFromNormalized(normalized) {
const scopes = [];
for (const item of extractFragments(normalized)) {
if (!item || typeof item !== "object") {
continue;
}
const scope = String(item.business_scope ?? "").trim();
if (scope) {
scopes.push(scope);
}
}
return Array.from(new Set(scopes));
}
function hasJuly2020SnapshotSignal(userMessage, companyAnchors) {
const lower = String(userMessage ?? "").toLowerCase();
if (/(?:\b2020[-/.]0?7\b|\bиюл[ьяе]?\b(?:\s+20\d{2})?|\bjuly\b(?:\s+20\d{2})?)/i.test(lower)) {
return true;
}
const periods = Array.isArray(companyAnchors?.periods) ? companyAnchors.periods : [];
const dates = Array.isArray(companyAnchors?.dates) ? companyAnchors.dates : [];
return [...periods, ...dates].some((item) => /2020[-/.]0?7|июл|july/i.test(String(item ?? "").toLowerCase()));
}
function hasP0DomainSignal(userMessage, companyAnchors) {
if (inferP0DomainFromMessage(userMessage)) {
return true;
}
const accounts = Array.isArray(companyAnchors?.accounts) ? companyAnchors.accounts : [];
if (accounts.some((item) => /^(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.|$)/.test(String(item ?? "").trim()))) {
return true;
}
return /(?:ндс|vat|рбп|deferred|амортиз|supplier|customer|settlement|month\s*close|закрыти[ея]\s+месяц|поставщ|покупат)/i.test(String(userMessage ?? "").toLowerCase());
}
function resolveBusinessScopeAlignment(input) {
const rawScopes = collectBusinessScopesFromNormalized(input.normalized);
const needsCompanyGrounding = hasJuly2020SnapshotSignal(input.userMessage, input.companyAnchors) && hasP0DomainSignal(input.userMessage, input.companyAnchors);
const reasons = [];
if (needsCompanyGrounding) {
reasons.push("july_2020_snapshot_p0_signal");
}
if (!input.routeSummary || input.routeSummary.mode !== "deterministic_v2" || !needsCompanyGrounding) {
return {
business_scope_raw: rawScopes,
business_scope_resolved: rawScopes,
company_grounding_applied: false,
scope_resolution_reason: reasons,
route_summary_resolved: input.routeSummary
};
}
let changed = false;
const decisions = input.routeSummary.decisions.map((decision) => {
const scopeValue = String(decision.business_scope ?? "").trim();
if (scopeValue !== "generic_accounting" && scopeValue !== "unclear") {
return decision;
}
changed = true;
return {
...decision,
business_scope: "company_specific_accounting"
};
});
const resolvedSummary = changed
? {
...input.routeSummary,
decisions
}
: input.routeSummary;
const resolvedScopes = changed
? Array.from(new Set(decisions.map((decision) => String(decision.business_scope ?? "").trim()).filter(Boolean)))
: rawScopes;
return {
business_scope_raw: rawScopes,
business_scope_resolved: resolvedScopes,
company_grounding_applied: changed,
scope_resolution_reason: changed ? [...reasons, "generic_or_unclear_to_company_specific_override"] : reasons,
route_summary_resolved: resolvedSummary
};
}
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -141,8 +218,9 @@ function extractDiscardedIntentSegments(normalized) {
function collectDateSpans(text) {
const spans = [];
const datePatterns = [
/\b20\d{2}[-/.](?:0[1-9]|1[0-2])(?:[-/.](?:0[1-9]|[12]\d|3[01]))?\b/g,
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g
/\b20\d{2}(?:[-/.](?:0?[1-9]|1[0-2]))(?:[-/.](?:0?[1-9]|[12]\d|3[01]))?\b/g,
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g,
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]?|июл[ьяе]?|август[ае]?|сентябр[ьяе]?|октябр[ьяе]?|ноябр[ьяе]?|декабр[ьяе]?|january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+20\d{2})?\b/giu
];
for (const datePattern of datePatterns) {
let match = null;
@@ -155,6 +233,32 @@ function collectDateSpans(text) {
}
return spans;
}
function collectAmountSpans(text) {
const spans = [];
const amountPatterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
for (const amountPattern of amountPatterns) {
let match = null;
while ((match = amountPattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectPercentSpans(text) {
const spans = [];
const percentPattern = /\b\d{1,3}(?:[.,]\d+)?\s*%/g;
let match = null;
while ((match = percentPattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
return spans;
}
function intersectsAnySpan(start, end, spans) {
return spans.some((span) => start < span.end && end > span.start);
}
@@ -230,7 +334,7 @@ function extractAccountTokens(text) {
if (explicitAccounts.size > 0) {
return Array.from(explicitAccounts);
}
const spans = collectDateSpans(lower);
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment)/iu.test(lower);
if (!hasAccountingLexeme) {
return [];
@@ -846,7 +950,7 @@ function extractNormalizedPeriodLiteral(text) {
}
function extractFollowupAccountAnchorsLoose(text) {
const lower = String(text ?? "").toLowerCase();
const spans = collectDateSpans(lower);
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
const anchors = [];
const followupAccountPattern = /\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.\d{2})?\b/g;
let match = null;
@@ -1192,6 +1296,13 @@ export class AssistantService {
};
const normalized = await this.normalizerService.normalize(normalizePayload);
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
const businessScopeResolution = resolveBusinessScopeAlignment({
userMessage,
companyAnchors,
normalized: normalized.normalized,
routeSummary: normalized.route_hint_summary
});
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
inferredDomainByMessage === "vat_document_register_book" ||
@@ -1214,8 +1325,8 @@ export class AssistantService {
focusDomainHint: focusDomainForGuards,
primaryPeriod: temporalGuard.primary_period_window
});
const requirementExtraction = extractRequirements(normalized.route_hint_summary, normalized.normalized, userMessage);
let executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
const requirementExtraction = extractRequirements(resolvedRouteSummary, normalized.normalized, userMessage);
let executionPlan = toExecutionPlan(resolvedRouteSummary, normalized.normalized, userMessage, requirementExtraction.byFragment);
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
const retrievalCalls = [];
@@ -1305,7 +1416,8 @@ export class AssistantService {
polarity: polarityGuardResult.audit,
evidence: evidenceGateResult.audit,
claimAnchors: claimAnchorAudit,
targetedEvidenceHitRate: targetedEvidenceResult.audit.targeted_evidence_hit_rate
targetedEvidenceHitRate: targetedEvidenceResult.audit.targeted_evidence_hit_rate,
businessScopeResolved: businessScopeResolution.business_scope_resolved
});
const groundingCheck = (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheckBase, groundedAnswerEligibilityGuard);
const focusDomainHint = followupBinding.usage?.applied
@@ -1317,7 +1429,7 @@ export class AssistantService {
const normalizationPeriodExplicit = hasExplicitPeriodAnchorFromNormalized(normalized.normalized) || hasPeriodInCompanyAnchors;
const composition = (0, answerComposer_1.composeAssistantAnswer)({
userMessage,
routeSummary: normalized.route_hint_summary,
routeSummary: resolvedRouteSummary,
retrievalResults,
requirements: coverageEvaluation.requirements,
coverageReport: coverageEvaluation.coverage,
@@ -1351,7 +1463,7 @@ export class AssistantService {
timestamp: new Date().toISOString(),
questionId: userItem.message_id,
userMessage,
routeSummary: normalized.route_hint_summary,
routeSummary: resolvedRouteSummary,
requirements: coverageEvaluation.requirements,
coverageReport: coverageEvaluation.coverage,
retrievalResults,
@@ -1367,11 +1479,11 @@ export class AssistantService {
prompt_version: normalized.prompt_version,
schema_version: normalized.schema_version,
fallback_type: composition.fallback_type,
route_summary: normalized.route_hint_summary,
route_summary: resolvedRouteSummary,
fragments: extractFragments(normalized.normalized),
requirements_extracted: coverageEvaluation.requirements,
coverage_report: coverageEvaluation.coverage,
routes: toDebugRoutes(normalized.route_hint_summary),
routes: toDebugRoutes(resolvedRouteSummary),
retrieval_status: retrievalResults.map((item) => ({
fragment_id: item.fragment_id,
requirement_ids: item.requirement_ids,
@@ -1384,16 +1496,29 @@ export class AssistantService {
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
question_type_class: questionTypeClass,
company_anchors: companyAnchors,
business_scope_raw: businessScopeResolution.business_scope_raw,
business_scope_resolved: businessScopeResolution.business_scope_resolved,
company_grounding_applied: businessScopeResolution.company_grounding_applied,
scope_resolution_reason: businessScopeResolution.scope_resolution_reason,
raw_time_anchor: temporalGuard.raw_time_anchor,
raw_time_scope: temporalGuard.raw_time_scope,
resolved_time_anchor: temporalGuard.resolved_time_anchor,
resolved_primary_period: temporalGuard.resolved_primary_period,
temporal_alignment_status: temporalGuard.temporal_alignment_status,
temporal_resolution_source: temporalGuard.temporal_resolution_source,
temporal_guard_basis: temporalGuard.temporal_guard_basis,
temporal_guard_applied: temporalGuard.temporal_guard_applied,
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
temporal_guard: temporalGuard,
raw_numeric_tokens: polarityGuardResult.audit.raw_numeric_tokens,
classified_numeric_tokens: polarityGuardResult.audit.classified_numeric_tokens,
rejected_as_non_accounts: polarityGuardResult.audit.rejected_as_non_accounts,
resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors,
domain_polarity_guard: polarityGuardResult.audit,
claim_anchor_audit: claimAnchorAudit,
targeted_evidence_acquisition: targetedEvidenceResult.audit,
evidence_admissibility_gate: evidenceGateResult.audit,
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
@@ -1438,7 +1563,7 @@ export class AssistantService {
normalizer_output: normalized.normalized,
execution_plan: executionPlan,
resolved_execution_state: extractExecutionState(normalized.normalized),
routes: toDebugRoutes(normalized.route_hint_summary),
routes: toDebugRoutes(resolvedRouteSummary),
retrieval_calls: retrievalCalls,
retrieval_results_raw: retrievalResultsRaw,
retrieval_results_normalized: retrievalResults,
@@ -1460,16 +1585,29 @@ export class AssistantService {
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
question_type_class: questionTypeClass,
company_anchors: companyAnchors,
business_scope_raw: businessScopeResolution.business_scope_raw,
business_scope_resolved: businessScopeResolution.business_scope_resolved,
company_grounding_applied: businessScopeResolution.company_grounding_applied,
scope_resolution_reason: businessScopeResolution.scope_resolution_reason,
raw_time_anchor: temporalGuard.raw_time_anchor,
raw_time_scope: temporalGuard.raw_time_scope,
resolved_time_anchor: temporalGuard.resolved_time_anchor,
resolved_primary_period: temporalGuard.resolved_primary_period,
temporal_alignment_status: temporalGuard.temporal_alignment_status,
temporal_resolution_source: temporalGuard.temporal_resolution_source,
temporal_guard_basis: temporalGuard.temporal_guard_basis,
temporal_guard_applied: temporalGuard.temporal_guard_applied,
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
temporal_guard: temporalGuard,
raw_numeric_tokens: polarityGuardResult.audit.raw_numeric_tokens,
classified_numeric_tokens: polarityGuardResult.audit.classified_numeric_tokens,
rejected_as_non_accounts: polarityGuardResult.audit.rejected_as_non_accounts,
resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors,
domain_polarity_guard: polarityGuardResult.audit,
claim_anchor_audit: claimAnchorAudit,
targeted_evidence_acquisition: targetedEvidenceResult.audit,
evidence_admissibility_gate: evidenceGateResult.audit,
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,