NODEDC_1C/llm_normalizer/backend/src/services/lifecycleRuntime.ts

1100 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { CandidateEvidenceItem, ProblemConfidence, ProblemUnit, ProblemUnitType } from "../types/stage2ProblemUnits";
import {
LIFECYCLE_MODEL_SCHEMA_VERSION,
STAGE3_LIFECYCLE_DOMAINS,
type LifecycleConfidence,
type LifecycleDefectDefinition,
type LifecycleDefectType,
type LifecycleDomain,
type LifecycleDomainModel,
type LifecycleResolution
} from "../types/stage3Lifecycle";
interface LifecycleResolverInput {
unit: ProblemUnit;
candidates: CandidateEvidenceItem[];
}
interface LifecycleRankingResult {
lifecycle_ranking_score: number;
lifecycle_ranking_basis: string[];
}
function clampUnitScore(value: number): number {
if (!Number.isFinite(value)) {
return 0;
}
if (value <= 0) return 0;
if (value >= 1) return 1;
return Number(value.toFixed(2));
}
function lifecycleConfidenceGrade(score: number): LifecycleConfidence["grade"] {
if (score >= 0.75) return "high";
if (score >= 0.45) return "medium";
return "low";
}
function uniqueStrings(values: string[], limit = 16): string[] {
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
}
function includesAny(source: string, patterns: RegExp[]): boolean {
return patterns.some((pattern) => pattern.test(source));
}
function hasToken(values: string[], pattern: RegExp): boolean {
return values.some((value) => pattern.test(value));
}
function normalizeStateToken(value: string): string {
return value.trim().toLowerCase();
}
function resolveStateCode(model: LifecycleDomainModel, stateCode: string | null | undefined): string | null {
if (!stateCode || typeof stateCode !== "string") {
return null;
}
const normalized = normalizeStateToken(stateCode);
const matched = model.states.find((state) => normalizeStateToken(state.state_code) === normalized);
return matched?.state_code ?? null;
}
function defaultInitialState(model: LifecycleDomainModel): string {
const initial = model.states.find((state) => state.state_class === "initial");
if (initial) {
return initial.state_code;
}
return model.states[0]?.state_code ?? "unknown_state";
}
function defaultExpectedState(model: LifecycleDomainModel): string {
const terminal = model.states.find((state) => state.is_terminal || state.state_class === "terminal");
if (terminal) {
return terminal.state_code;
}
const active = model.states.find((state) => state.state_class === "active");
if (active) {
return active.state_code;
}
return defaultInitialState(model);
}
function expectedTransitionAdjacency(model: LifecycleDomainModel): Map<string, string[]> {
const graph = new Map<string, string[]>();
for (const transition of model.transitions) {
if (transition.transition_type !== "expected") {
continue;
}
const from = transition.from_state;
const to = transition.to_state;
const current = graph.get(from) ?? [];
if (!current.includes(to)) {
current.push(to);
}
graph.set(from, current);
}
return graph;
}
function shortestExpectedPath(model: LifecycleDomainModel, fromState: string, toState: string): string[] | null {
if (fromState === toState) {
return [fromState];
}
const graph = expectedTransitionAdjacency(model);
const queue: string[][] = [[fromState]];
const visited = new Set<string>([fromState]);
while (queue.length > 0) {
const path = queue.shift();
if (!path) {
continue;
}
const tail = path[path.length - 1];
const nextStates = graph.get(tail) ?? [];
for (const nextState of nextStates) {
if (visited.has(nextState)) {
continue;
}
const nextPath = [...path, nextState];
if (nextState === toState) {
return nextPath;
}
visited.add(nextState);
queue.push(nextPath);
}
}
return null;
}
function transitionEdgeLabel(fromState: string, toState: string): string {
return `${fromState}->${toState}`;
}
function resolvePreviousStates(model: LifecycleDomainModel, currentState: string): string[] {
const initialState = defaultInitialState(model);
if (initialState === currentState) {
return [];
}
const path = shortestExpectedPath(model, initialState, currentState);
if (!path || path.length <= 1) {
return [];
}
return path.slice(0, -1);
}
const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
bank_settlement: {
schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION,
lifecycle_domain: "bank_settlement",
lifecycle_object_types: ["payment_settlement_link"],
states: [
{
state_code: "initiated_payment",
state_label: "Платеж инициирован",
state_class: "initial",
entry_conditions: ["payment_order_created"],
exit_conditions: ["bank_recorded"],
is_terminal: false,
is_problematic: false,
business_meaning: "Есть инициирование платежа."
},
{
state_code: "bank_recorded",
state_label: "Платеж отражен банком",
state_class: "active",
entry_conditions: ["bank_statement_recorded"],
exit_conditions: ["settlement_linked"],
is_terminal: false,
is_problematic: false,
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
},
{
state_code: "settlement_closed",
state_label: "Расчет закрыт",
state_class: "terminal",
entry_conditions: ["payment_to_settlement_linked"],
exit_conditions: [],
is_terminal: true,
is_problematic: false,
business_meaning: "Платеж доведен до расчетного результата."
},
{
state_code: "stale_unlinked_payment",
state_label: "Платеж завис без закрытия",
state_class: "problematic",
entry_conditions: ["bank_recorded", "missing_link"],
exit_conditions: ["settlement_closed"],
is_terminal: false,
is_problematic: true,
business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
},
{
state_code: "misclosed_payment",
state_label: "Платеж закрыт некорректно",
state_class: "problematic",
entry_conditions: ["wrong_document_type_or_posting_mismatch"],
exit_conditions: ["settlement_closed"],
is_terminal: false,
is_problematic: true,
business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
}
],
transitions: [
{
from_state: "initiated_payment",
to_state: "bank_recorded",
transition_type: "expected",
required_evidence: ["bank_statement_recorded"],
optional_evidence: ["payment_order"],
forbidden_conditions: [],
business_meaning: "Платеж должен появиться во выписке."
},
{
from_state: "bank_recorded",
to_state: "settlement_closed",
transition_type: "expected",
required_evidence: ["payment_to_settlement_link"],
optional_evidence: ["document_to_posting"],
forbidden_conditions: ["wrong_document_type"],
business_meaning: "После выписки должен закрываться расчет."
}
],
defects: []
},
customer_settlement: {
schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION,
lifecycle_domain: "customer_settlement",
lifecycle_object_types: ["receivable_chain"],
states: [
{
state_code: "invoice_issued",
state_label: "Реализация отражена",
state_class: "initial",
entry_conditions: ["realization_document_exists"],
exit_conditions: ["payment_recorded"],
is_terminal: false,
is_problematic: false,
business_meaning: "Возникла дебиторская позиция."
},
{
state_code: "payment_recorded",
state_label: "Оплата отражена",
state_class: "active",
entry_conditions: ["payment_document_exists"],
exit_conditions: ["receivable_closed"],
is_terminal: false,
is_problematic: false,
business_meaning: "Оплата есть, ожидается корректное закрытие."
},
{
state_code: "receivable_closed",
state_label: "Дебиторка закрыта",
state_class: "terminal",
entry_conditions: ["closing_document_linked"],
exit_conditions: [],
is_terminal: true,
is_problematic: false,
business_meaning: "Дебиторская позиция закрыта корректно."
},
{
state_code: "stale_receivable",
state_label: "Дебиторка зависла",
state_class: "problematic",
entry_conditions: ["unresolved_settlement"],
exit_conditions: ["receivable_closed"],
is_terminal: false,
is_problematic: true,
business_meaning: "Позиция остается незавершенной дольше ожидаемого."
}
],
transitions: [
{
from_state: "invoice_issued",
to_state: "payment_recorded",
transition_type: "expected",
required_evidence: ["payment_document_exists"],
optional_evidence: [],
forbidden_conditions: [],
business_meaning: "После реализации ожидается оплата/зачет."
},
{
from_state: "payment_recorded",
to_state: "receivable_closed",
transition_type: "expected",
required_evidence: ["closing_document_linked"],
optional_evidence: ["register_movement_exists"],
forbidden_conditions: ["cross_branch_inconsistency"],
business_meaning: "Оплата должна завершаться корректным закрытием расчета."
}
],
defects: []
},
deferred_expense: {
schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION,
lifecycle_domain: "deferred_expense",
lifecycle_object_types: ["deferred_expense_item"],
states: [
{
state_code: "recognized",
state_label: "РБП признан",
state_class: "initial",
entry_conditions: ["deferred_expense_created"],
exit_conditions: ["writeoff_started"],
is_terminal: false,
is_problematic: false,
business_meaning: "РБП поставлен на учет."
},
{
state_code: "partially_written_off",
state_label: "Частичное списание",
state_class: "active",
entry_conditions: ["partial_writeoff_exists"],
exit_conditions: ["fully_written_off"],
is_terminal: false,
is_problematic: false,
business_meaning: "Списание идет по графику."
},
{
state_code: "fully_written_off",
state_label: "РБП полностью списан",
state_class: "terminal",
entry_conditions: ["full_writeoff_exists"],
exit_conditions: [],
is_terminal: true,
is_problematic: false,
business_meaning: "РБП завершил lifecycle."
},
{
state_code: "overdue_writeoff",
state_label: "Просроченное списание",
state_class: "problematic",
entry_conditions: ["period_boundary", "missing_link"],
exit_conditions: ["fully_written_off"],
is_terminal: false,
is_problematic: true,
business_meaning: "РБП живет дольше допустимого окна."
}
],
transitions: [],
defects: []
},
fixed_asset: {
schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION,
lifecycle_domain: "fixed_asset",
lifecycle_object_types: ["fixed_asset_card"],
states: [
{
state_code: "capitalized",
state_label: "Капвложения отражены",
state_class: "initial",
entry_conditions: ["capitalization_document_exists"],
exit_conditions: ["accepted_for_accounting"],
is_terminal: false,
is_problematic: false,
business_meaning: "Объект зафиксирован как вложение."
},
{
state_code: "accepted_for_accounting",
state_label: "Принят к учету",
state_class: "active",
entry_conditions: ["acceptance_document_exists"],
exit_conditions: ["depreciation_active"],
is_terminal: false,
is_problematic: false,
business_meaning: "Объект переведен в основной контур учета."
},
{
state_code: "depreciation_active",
state_label: "Амортизация активна",
state_class: "active",
entry_conditions: ["depreciation_register_movement"],
exit_conditions: ["disposed"],
is_terminal: false,
is_problematic: false,
business_meaning: "Жизненный цикл ОС идет штатно."
},
{
state_code: "contradictory_asset_state",
state_label: "Противоречивый статус ОС",
state_class: "problematic",
entry_conditions: ["posting_mismatch_or_wrong_path"],
exit_conditions: ["depreciation_active"],
is_terminal: false,
is_problematic: true,
business_meaning: "Статус ОС формально есть, но смыслово противоречив."
},
{
state_code: "disposed",
state_label: "Выбыл",
state_class: "terminal",
entry_conditions: ["disposal_document_exists"],
exit_conditions: [],
is_terminal: true,
is_problematic: false,
business_meaning: "Жизненный цикл ОС завершен."
}
],
transitions: [],
defects: []
},
vat_flow: {
schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION,
lifecycle_domain: "vat_flow",
lifecycle_object_types: ["vat_document_chain"],
states: [
{
state_code: "vat_registered",
state_label: "НДС отражен документно",
state_class: "initial",
entry_conditions: ["invoice_registered"],
exit_conditions: ["vat_reflected"],
is_terminal: false,
is_problematic: false,
business_meaning: "Сформирован первичный документный слой НДС."
},
{
state_code: "vat_reflected",
state_label: "НДС отражен в учете",
state_class: "active",
entry_conditions: ["vat_register_movement"],
exit_conditions: ["vat_deducted"],
is_terminal: false,
is_problematic: false,
business_meaning: "НДС проходит штатную стадию отражения."
},
{
state_code: "vat_deducted",
state_label: "НДС принят к вычету",
state_class: "terminal",
entry_conditions: ["deduction_confirmed"],
exit_conditions: [],
is_terminal: true,
is_problematic: false,
business_meaning: "НДС-цепочка завершена корректно."
},
{
state_code: "vat_conflict",
state_label: "Конфликт НДС-цепочки",
state_class: "problematic",
entry_conditions: ["cross_branch_inconsistency"],
exit_conditions: ["vat_reflected"],
is_terminal: false,
is_problematic: true,
business_meaning: "Бухгалтерская и налоговая ветки расходятся."
}
],
transitions: [],
defects: []
},
period_close: {
schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION,
lifecycle_domain: "period_close",
lifecycle_object_types: ["period_close_blocker"],
states: [
{
state_code: "preclose_checks",
state_label: "Предзакрытие",
state_class: "active",
entry_conditions: ["period_scope_detected"],
exit_conditions: ["close_ready"],
is_terminal: false,
is_problematic: false,
business_meaning: "Идет проверка готовности периода."
},
{
state_code: "close_ready",
state_label: "Готов к закрытию",
state_class: "active",
entry_conditions: ["no_blockers_detected"],
exit_conditions: ["close_completed"],
is_terminal: false,
is_problematic: false,
business_meaning: "Период может быть закрыт."
},
{
state_code: "close_completed",
state_label: "Закрытие завершено",
state_class: "terminal",
entry_conditions: ["close_operation_done"],
exit_conditions: [],
is_terminal: true,
is_problematic: false,
business_meaning: "Период закрыт."
},
{
state_code: "close_blocked",
state_label: "Закрытие заблокировано",
state_class: "problematic",
entry_conditions: ["period_close_risk_or_stale_state"],
exit_conditions: ["close_ready"],
is_terminal: false,
is_problematic: true,
business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
},
{
state_code: "close_contradicted",
state_label: "Закрыт формально, но с противоречием",
state_class: "problematic",
entry_conditions: ["misclosed_or_cross_branch_conflict"],
exit_conditions: ["close_completed"],
is_terminal: false,
is_problematic: true,
business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
}
],
transitions: [],
defects: []
}
};
const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
{
defect_code: "missing_expected_transition",
defect_class: "path",
severity_hint: "medium",
business_meaning: "Ожидаемый переход не произошел.",
evidence_requirements: ["expected_state", "missing_transition_signal"],
period_impact_potential: "indirect"
},
{
defect_code: "invalid_transition",
defect_class: "path",
severity_hint: "high",
business_meaning: "Переход произошел по некорректному пути.",
evidence_requirements: ["invalid_transition_signal"],
period_impact_potential: "indirect"
},
{
defect_code: "stale_active_state",
defect_class: "timing",
severity_hint: "high",
business_meaning: "Объект завис в активном состоянии.",
evidence_requirements: ["stale_marker", "missing_transition_signal"],
period_impact_potential: "direct"
},
{
defect_code: "contradictory_state",
defect_class: "consistency",
severity_hint: "high",
business_meaning: "Статусы объекта противоречат друг другу.",
evidence_requirements: ["contradiction_signal"],
period_impact_potential: "direct"
},
{
defect_code: "premature_terminal_state",
defect_class: "closure",
severity_hint: "medium",
business_meaning: "Терминальное состояние наступило преждевременно.",
evidence_requirements: ["terminal_state", "missing_required_previous_state"],
period_impact_potential: "indirect"
},
{
defect_code: "misclosed_state",
defect_class: "closure",
severity_hint: "high",
business_meaning: "Контур формально закрыт, но закрыт неверно.",
evidence_requirements: ["wrong_closure_path"],
period_impact_potential: "direct"
},
{
defect_code: "orphan_intermediate_state",
defect_class: "path",
severity_hint: "medium",
business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
evidence_requirements: ["intermediate_state_without_next"],
period_impact_potential: "indirect"
},
{
defect_code: "cross_branch_state_conflict",
defect_class: "consistency",
severity_hint: "high",
business_meaning: "Состояния соседних веток учета противоречат друг другу.",
evidence_requirements: ["cross_branch_conflict_signal"],
period_impact_potential: "direct"
}
];
for (const domain of STAGE3_LIFECYCLE_DOMAINS) {
LIFECYCLE_DOMAIN_MODELS[domain].defects = SHARED_DEFECTS;
}
class LifecycleRegistryImpl {
constructor(private readonly models: Record<LifecycleDomain, LifecycleDomainModel>) {}
public listDomains(): LifecycleDomain[] {
return STAGE3_LIFECYCLE_DOMAINS.slice();
}
public getDomain(domain: LifecycleDomain): LifecycleDomainModel {
return this.models[domain];
}
public hasState(domain: LifecycleDomain, stateCode: string | null | undefined): boolean {
const model = this.getDomain(domain);
return Boolean(resolveStateCode(model, stateCode));
}
public resolveDefaultExpectedState(domain: LifecycleDomain): string {
return defaultExpectedState(this.getDomain(domain));
}
public resolveInitialState(domain: LifecycleDomain): string {
return defaultInitialState(this.getDomain(domain));
}
public findExpectedPath(domain: LifecycleDomain, fromState: string, toState: string): string[] | null {
return shortestExpectedPath(this.getDomain(domain), fromState, toState);
}
}
export const LifecycleRegistry = new LifecycleRegistryImpl(LIFECYCLE_DOMAIN_MODELS);
function inferLifecycleDomain(input: LifecycleResolverInput): LifecycleDomain {
const unitTokens = [
input.unit.problem_unit_type,
input.unit.business_defect_class,
input.unit.mechanism_summary,
input.unit.failed_expected_edge ?? "",
input.unit.expected_state ?? "",
input.unit.actual_state ?? "",
...input.unit.affected_accounts,
...input.unit.affected_entities,
...input.unit.affected_documents,
...input.unit.affected_counterparties,
...input.candidates.flatMap((item) => item.anomaly_patterns),
...input.candidates.flatMap((item) => item.relation_pattern_hits)
]
.join(" ")
.toLowerCase();
const hasExplicitVatHint = includesAny(unitTokens, [/domain_hint:vat_flow/]);
const hasExplicitDeferredHint = includesAny(unitTokens, [/domain_hint:deferred_expense/]);
const hasExplicitFixedAssetHint = includesAny(unitTokens, [/domain_hint:fixed_asset/]);
const hasExplicitPeriodCloseHint = includesAny(unitTokens, [/domain_hint:period_close/]);
const hasCustomerSettlementHint = includesAny(unitTokens, [/domain_hint:customer_settlement/]);
const hasBankSettlementHint = includesAny(unitTokens, [/domain_hint:bank_settlement/]);
const hasVatMarkers = includesAny(unitTokens, [
/\binvoice_to_vat\b/,
/\bvat_chain_conflict\b/,
/(^|[^a-z0-9])nds([^a-z0-9]|$)/,
/(^|[^a-z0-9])vat([^a-z0-9]|$)/,
/(^|[^a-z0-9])tax(?:es)?([^a-z0-9]|$)/,
/\baccount[_:\s-]?(19|68)\b/
]);
const hasDeferredMarkers = includesAny(unitTokens, [
/\bdeferred(?:_expense)?\b/,
/\bdeferred_expense_to_writeoff\b/,
/\bwriteoff\b/,
/\bpartially_written_off\b/,
/\bfully_written_off\b/,
/\baccount[_:\s-]?97\b/
]);
const hasFixedAssetMarkers = includesAny(unitTokens, [
/\bfixed[_\s-]?asset(?:s)?\b/,
/\basset_card_to_depreciation\b/,
/\bdepreciation(?:_active)?\b/,
/\baccepted_for_accounting\b/,
/\bcapitalized\b/,
/\baccount[_:\s-]?(01|02|08)\b/
]);
const hasPeriodCloseMarkers = includesAny(unitTokens, [
/\bperiod[_\s-]?close\b/,
/\bperiod_close_risk\b/,
/\bclose[_\s-]?risk\b/,
/\bclosure[_\s-]?risk\b/,
/\bpreclose\b/,
/\bmonth[_\s-]?close\b/,
/\bperiod_risk\b/
]);
if (hasExplicitDeferredHint) {
return "deferred_expense";
}
if (hasExplicitFixedAssetHint) {
return "fixed_asset";
}
if (hasExplicitVatHint) {
return "vat_flow";
}
if (hasExplicitPeriodCloseHint) {
return "period_close";
}
if (hasCustomerSettlementHint) {
return "customer_settlement";
}
if (hasBankSettlementHint) {
return "bank_settlement";
}
if (hasDeferredMarkers) {
return "deferred_expense";
}
if (hasFixedAssetMarkers) {
return "fixed_asset";
}
if (hasVatMarkers) {
return "vat_flow";
}
if (
hasPeriodCloseMarkers ||
input.unit.problem_unit_type === "period_risk_cluster" ||
input.unit.period_impact?.impact_class === "close_risk"
) {
return "period_close";
}
if (includesAny(unitTokens, [/buyer/, /customer/, /\b62\b/])) {
return "customer_settlement";
}
if (
includesAny(unitTokens, [
/domain_hint:bank_settlement/,
/\bpayment_to_settlement\b/,
/\bstatement_to_document\b/,
/\bbank_recorded\b/,
/\binitiated_payment\b/,
/\bsettlement(?:_closed)?\b/
]) ||
input.unit.problem_unit_type === "unresolved_settlement_cluster" ||
input.unit.problem_unit_type === "broken_chain_segment"
) {
return "bank_settlement";
}
if (input.unit.problem_unit_type === "cross_branch_inconsistency_cluster") {
return "vat_flow";
}
if (input.unit.problem_unit_type === "lifecycle_anomaly_node") {
return "deferred_expense";
}
return "bank_settlement";
}
function inferCurrentState(domain: LifecycleDomain, input: LifecycleResolverInput): string {
const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).map((item) => item.toLowerCase());
const relations = input.candidates.flatMap((item) => item.relation_pattern_hits).map((item) => item.toLowerCase());
const hasStale = hasToken(anomalies, /(no_continuation|stale|tail|missing_link|broken_lifecycle|partially_linked)/);
const hasInvalid = hasToken(anomalies, /(posting_mismatch|wrong_document_type|cross_domain_inconsistency|misclose|cross_branch)/);
if (domain === "bank_settlement") {
if (hasInvalid) return "misclosed_payment";
if (hasStale) return "stale_unlinked_payment";
if (hasToken(relations, /payment_to_settlement/)) return "bank_recorded";
return "initiated_payment";
}
if (domain === "customer_settlement") {
if (hasStale) return "stale_receivable";
if (hasToken(relations, /payment|settlement/)) return "payment_recorded";
return "invoice_issued";
}
if (domain === "deferred_expense") {
if (hasStale) return "overdue_writeoff";
if (hasToken(relations, /writeoff|partial/)) return "partially_written_off";
return "recognized";
}
if (domain === "fixed_asset") {
if (hasInvalid) return "contradictory_asset_state";
if (hasToken(relations, /depreciation|amort/)) return "depreciation_active";
if (hasToken(relations, /accept|account/)) return "accepted_for_accounting";
return "capitalized";
}
if (domain === "vat_flow") {
if (hasInvalid || hasToken(anomalies, /cross_branch|inconsistency/)) return "vat_conflict";
if (hasToken(relations, /invoice_to_vat|vat/)) return "vat_reflected";
return "vat_registered";
}
if (hasInvalid) return "close_contradicted";
if (hasStale || input.unit.period_impact?.impact_class === "close_risk") return "close_blocked";
return "preclose_checks";
}
function inferExpectedState(domain: LifecycleDomain, input: LifecycleResolverInput, model: LifecycleDomainModel): string {
const explicitExpected = input.unit.expected_state?.trim();
if (explicitExpected) {
return explicitExpected;
}
return defaultExpectedState(model);
}
function inferMissingTransition(
input: LifecycleResolverInput,
model: LifecycleDomainModel,
currentState: string,
expectedState: string
): string | null {
if (typeof input.unit.failed_expected_edge === "string" && input.unit.failed_expected_edge.trim().length > 0) {
return input.unit.failed_expected_edge.trim();
}
const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase();
if (!/(missing_link|no_continuation|broken_lifecycle|tail|unresolved)/.test(anomalies)) {
return null;
}
if (currentState !== expectedState) {
const path = shortestExpectedPath(model, currentState, expectedState);
if (path && path.length >= 2) {
return transitionEdgeLabel(path[0], path[1]);
}
}
const directExpected = model.transitions.find(
(transition) => transition.transition_type === "expected" && transition.from_state === currentState
);
if (directExpected) {
return transitionEdgeLabel(directExpected.from_state, directExpected.to_state);
}
return "expected_transition_not_observed";
}
function inferInvalidTransition(input: LifecycleResolverInput, model: LifecycleDomainModel): string | null {
const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase();
for (const transition of model.transitions) {
for (const forbiddenCondition of transition.forbidden_conditions) {
if (anomalies.includes(forbiddenCondition.toLowerCase())) {
return `${transitionEdgeLabel(transition.from_state, transition.to_state)}:forbidden:${forbiddenCondition}`;
}
}
}
if (/(cross_branch|cross_domain_inconsistency)/.test(anomalies)) {
return "cross_branch_conflict_transition";
}
if (/(wrong_document_type|posting_mismatch|misclose)/.test(anomalies)) {
return "invalid_document_or_posting_transition";
}
return null;
}
export function classifyLifecycleDefect(input: {
domain: LifecycleDomain;
currentState: string;
expectedState: string;
missingTransition: string | null;
invalidTransition: string | null;
periodCloseSensitive: boolean;
}): LifecycleDefectType | null {
const current = input.currentState.toLowerCase();
if (input.invalidTransition?.includes("cross_branch")) {
return "cross_branch_state_conflict";
}
if (input.invalidTransition) {
if (current.includes("misclosed") || input.domain === "period_close") {
return "misclosed_state";
}
return "invalid_transition";
}
if (input.missingTransition) {
if (current.includes("stale") || current.includes("overdue") || input.periodCloseSensitive) {
return "stale_active_state";
}
return "missing_expected_transition";
}
if (current.includes("contradict")) {
return "contradictory_state";
}
if (current.includes("closed") && !input.expectedState.toLowerCase().includes("closed")) {
return "premature_terminal_state";
}
if (input.currentState !== input.expectedState && !input.currentState.toLowerCase().includes("closed")) {
return "orphan_intermediate_state";
}
return null;
}
function registryBackedDefect(domain: LifecycleDomain, defect: LifecycleDefectType | null): LifecycleDefectType | null {
if (!defect) {
return null;
}
const model = LifecycleRegistry.getDomain(domain);
return model.defects.some((definition) => definition.defect_code === defect) ? defect : null;
}
function resolutionConfidence(unitConfidence: ProblemConfidence, input: {
hasExplicitStates: boolean;
hasDefectSignal: boolean;
candidateCount: number;
hasSnapshotLimitations: boolean;
}): LifecycleConfidence {
let score = unitConfidence.score;
if (input.hasExplicitStates) score += 0.1;
if (input.hasDefectSignal) score += 0.08;
if (input.candidateCount >= 2) score += 0.05;
if (input.hasSnapshotLimitations) score -= 0.12;
const normalized = clampUnitScore(score);
return {
score: normalized,
grade: lifecycleConfidenceGrade(normalized)
};
}
function staleDurationHint(domain: LifecycleDomain, defect: LifecycleDefectType | null, input: LifecycleResolverInput): string | undefined {
const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase();
if (defect !== "stale_active_state") {
return undefined;
}
if (/(period_boundary|period|close_risk)/.test(anomalies) || domain === "period_close") {
return "period_boundary_exceeded";
}
return "unknown_snapshot_window";
}
function lifecycleInterpretation(input: {
domain: LifecycleDomain;
currentState: string;
expectedState: string;
defect: LifecycleDefectType | null;
missingTransition: string | null;
invalidTransition: string | null;
}): string {
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
if (input.defect === "stale_active_state") {
return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
}
if (input.defect === "misclosed_state") {
return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
}
if (input.defect === "cross_branch_state_conflict") {
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
}
if (input.defect === "missing_expected_transition") {
return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
}
if (input.defect === "invalid_transition") {
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
}
return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
}
export function resolveLifecycle(input: LifecycleResolverInput): LifecycleResolution {
const lifecycle_domain = inferLifecycleDomain(input);
const model = LifecycleRegistry.getDomain(lifecycle_domain);
const inferredCurrentState = inferCurrentState(lifecycle_domain, input);
const inferredExpectedState = inferExpectedState(lifecycle_domain, input, model);
const explicitActualState = input.unit.actual_state?.trim() ?? null;
const explicitExpectedState = input.unit.expected_state?.trim() ?? null;
const explicitCurrentState = resolveStateCode(model, explicitActualState);
const explicitExpectedResolved = resolveStateCode(model, explicitExpectedState);
const inferredCurrentResolved = resolveStateCode(model, inferredCurrentState);
const inferredExpectedResolved = resolveStateCode(model, inferredExpectedState);
const currentState = explicitCurrentState ?? inferredCurrentResolved ?? defaultInitialState(model);
const expectedState = explicitExpectedResolved ?? inferredExpectedResolved ?? defaultExpectedState(model);
const missingTransition = inferMissingTransition(input, model, currentState, expectedState);
const invalidTransition = inferInvalidTransition(input, model);
const detectedDefect = classifyLifecycleDefect({
domain: lifecycle_domain,
currentState,
expectedState,
missingTransition,
invalidTransition,
periodCloseSensitive: input.unit.period_impact?.impact_class === "close_risk"
});
const defect = registryBackedDefect(lifecycle_domain, detectedDefect);
const evidenceIds = uniqueStrings(input.unit.evidence_pack, 8);
const previousStates = resolvePreviousStates(model, currentState);
const limitations = uniqueStrings(
[
...input.unit.snapshot_limitations,
...(input.candidates.some((item) => item.confidence_hint === "low") ? ["low_confidence_candidates_present"] : []),
...(explicitActualState && !explicitCurrentState ? ["actual_state_not_in_registry_normalized"] : []),
...(explicitExpectedState && !explicitExpectedResolved ? ["expected_state_not_in_registry_normalized"] : []),
...(explicitCurrentState ? [] : ["actual_state_inferred"]),
...(explicitExpectedResolved ? [] : ["expected_state_inferred"])
],
8
);
const confidence = resolutionConfidence(input.unit.confidence, {
hasExplicitStates: Boolean(explicitCurrentState || explicitExpectedResolved),
hasDefectSignal: Boolean(defect || missingTransition || invalidTransition),
candidateCount: input.candidates.length,
hasSnapshotLimitations: limitations.length > 0
});
return {
lifecycle_object_id: `lcobj-${input.unit.problem_unit_id}`,
lifecycle_domain,
resolved_current_state: currentState,
resolved_expected_state: expectedState,
resolved_previous_states: previousStates,
missing_transitions: missingTransition ? [missingTransition] : [],
invalid_transitions: invalidTransition ? [invalidTransition] : [],
detected_defects: defect ? [defect] : [],
state_confidence: confidence,
resolution_evidence: evidenceIds,
snapshot_limitations: limitations
};
}
function lifecycleRanking(defect: LifecycleDefectType | null, input: {
unit: ProblemUnit;
resolution: LifecycleResolution;
staleDuration?: string;
}): LifecycleRankingResult {
let score = input.unit.severity.score;
const basis: string[] = ["base_problem_severity"];
if (defect === "cross_branch_state_conflict") {
score += 0.55;
basis.push("cross_branch_conflict_weight");
} else if (defect === "misclosed_state") {
score += 0.45;
basis.push("misclosed_state_weight");
} else if (defect === "stale_active_state") {
score += 0.35;
basis.push("stale_duration_weight");
} else if (defect === "invalid_transition") {
score += 0.3;
basis.push("invalid_transition_weight");
} else if (defect === "missing_expected_transition") {
score += 0.25;
basis.push("missing_transition_weight");
}
if (input.staleDuration) {
score += 0.15;
basis.push("stale_duration_present");
}
if (input.unit.period_impact?.impact_class === "close_risk") {
score += 0.22;
basis.push("period_close_impact");
}
if (input.resolution.state_confidence.grade === "high") {
score += 0.08;
basis.push("state_confidence_weight");
}
return {
lifecycle_ranking_score: Number(score.toFixed(2)),
lifecycle_ranking_basis: basis
};
}
export function enrichProblemUnitLifecycle(input: LifecycleResolverInput): ProblemUnit {
const resolution = resolveLifecycle(input);
const defect = resolution.detected_defects[0] ?? null;
const staleDuration = staleDurationHint(resolution.lifecycle_domain, defect, input);
const ranking = lifecycleRanking(defect, {
unit: input.unit,
resolution,
staleDuration
});
return {
...input.unit,
lifecycle_domain: resolution.lifecycle_domain,
lifecycle_object_id: resolution.lifecycle_object_id,
current_lifecycle_state: resolution.resolved_current_state,
expected_lifecycle_state: resolution.resolved_expected_state,
...(resolution.missing_transitions.length > 0
? {
missing_transition: resolution.missing_transitions[0]
}
: {}),
...(resolution.invalid_transitions.length > 0
? {
invalid_transition: resolution.invalid_transitions[0]
}
: {}),
...(defect
? {
lifecycle_defect_type: defect
}
: {}),
...(staleDuration
? {
stale_duration: staleDuration
}
: {}),
lifecycle_confidence: resolution.state_confidence,
business_lifecycle_interpretation: lifecycleInterpretation({
domain: resolution.lifecycle_domain,
currentState: resolution.resolved_current_state,
expectedState: resolution.resolved_expected_state,
defect,
missingTransition: resolution.missing_transitions[0] ?? null,
invalidTransition: resolution.invalid_transitions[0] ?? null
}),
lifecycle_resolution: resolution,
lifecycle_ranking_score: ranking.lifecycle_ranking_score,
lifecycle_ranking_basis: ranking.lifecycle_ranking_basis
};
}
export function rankLifecycleProblemUnits(units: ProblemUnit[]): ProblemUnit[] {
return units
.slice()
.sort((left, right) => {
const rankDiff = (right.lifecycle_ranking_score ?? 0) - (left.lifecycle_ranking_score ?? 0);
if (rankDiff !== 0) return rankDiff;
const severityDiff = right.severity.score - left.severity.score;
if (severityDiff !== 0) return severityDiff;
return right.confidence.score - left.confidence.score;
});
}