1002 lines
46 KiB
JavaScript
1002 lines
46 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.LifecycleRegistry = void 0;
|
||
exports.classifyLifecycleDefect = classifyLifecycleDefect;
|
||
exports.resolveLifecycle = resolveLifecycle;
|
||
exports.enrichProblemUnitLifecycle = enrichProblemUnitLifecycle;
|
||
exports.rankLifecycleProblemUnits = rankLifecycleProblemUnits;
|
||
const stage3Lifecycle_1 = require("../types/stage3Lifecycle");
|
||
function clampUnitScore(value) {
|
||
if (!Number.isFinite(value)) {
|
||
return 0;
|
||
}
|
||
if (value <= 0)
|
||
return 0;
|
||
if (value >= 1)
|
||
return 1;
|
||
return Number(value.toFixed(2));
|
||
}
|
||
function lifecycleConfidenceGrade(score) {
|
||
if (score >= 0.75)
|
||
return "high";
|
||
if (score >= 0.45)
|
||
return "medium";
|
||
return "low";
|
||
}
|
||
function uniqueStrings(values, limit = 16) {
|
||
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
|
||
}
|
||
function includesAny(source, patterns) {
|
||
return patterns.some((pattern) => pattern.test(source));
|
||
}
|
||
function hasToken(values, pattern) {
|
||
return values.some((value) => pattern.test(value));
|
||
}
|
||
function normalizeStateToken(value) {
|
||
return value.trim().toLowerCase();
|
||
}
|
||
function resolveStateCode(model, stateCode) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
const graph = new Map();
|
||
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, fromState, toState) {
|
||
if (fromState === toState) {
|
||
return [fromState];
|
||
}
|
||
const graph = expectedTransitionAdjacency(model);
|
||
const queue = [[fromState]];
|
||
const visited = new Set([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, toState) {
|
||
return `${fromState}->${toState}`;
|
||
}
|
||
function resolvePreviousStates(model, currentState) {
|
||
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 = {
|
||
bank_settlement: {
|
||
schema_version: stage3Lifecycle_1.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: stage3Lifecycle_1.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: stage3Lifecycle_1.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: stage3Lifecycle_1.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: stage3Lifecycle_1.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: stage3Lifecycle_1.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 = [
|
||
{
|
||
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 stage3Lifecycle_1.STAGE3_LIFECYCLE_DOMAINS) {
|
||
LIFECYCLE_DOMAIN_MODELS[domain].defects = SHARED_DEFECTS;
|
||
}
|
||
class LifecycleRegistryImpl {
|
||
models;
|
||
constructor(models) {
|
||
this.models = models;
|
||
}
|
||
listDomains() {
|
||
return stage3Lifecycle_1.STAGE3_LIFECYCLE_DOMAINS.slice();
|
||
}
|
||
getDomain(domain) {
|
||
return this.models[domain];
|
||
}
|
||
hasState(domain, stateCode) {
|
||
const model = this.getDomain(domain);
|
||
return Boolean(resolveStateCode(model, stateCode));
|
||
}
|
||
resolveDefaultExpectedState(domain) {
|
||
return defaultExpectedState(this.getDomain(domain));
|
||
}
|
||
resolveInitialState(domain) {
|
||
return defaultInitialState(this.getDomain(domain));
|
||
}
|
||
findExpectedPath(domain, fromState, toState) {
|
||
return shortestExpectedPath(this.getDomain(domain), fromState, toState);
|
||
}
|
||
}
|
||
exports.LifecycleRegistry = new LifecycleRegistryImpl(LIFECYCLE_DOMAIN_MODELS);
|
||
function inferLifecycleDomain(input) {
|
||
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 hasVatMarkers = includesAny(unitTokens, [
|
||
/domain_hint:vat_flow/,
|
||
/\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, [
|
||
/domain_hint:deferred_expense/,
|
||
/\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, [
|
||
/domain_hint:fixed_asset/,
|
||
/\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, [
|
||
/domain_hint:period_close/,
|
||
/\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 (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, input) {
|
||
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, input, model) {
|
||
const explicitExpected = input.unit.expected_state?.trim();
|
||
if (explicitExpected) {
|
||
return explicitExpected;
|
||
}
|
||
return defaultExpectedState(model);
|
||
}
|
||
function inferMissingTransition(input, model, currentState, expectedState) {
|
||
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, model) {
|
||
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;
|
||
}
|
||
function classifyLifecycleDefect(input) {
|
||
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, defect) {
|
||
if (!defect) {
|
||
return null;
|
||
}
|
||
const model = exports.LifecycleRegistry.getDomain(domain);
|
||
return model.defects.some((definition) => definition.defect_code === defect) ? defect : null;
|
||
}
|
||
function resolutionConfidence(unitConfidence, input) {
|
||
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, defect, input) {
|
||
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) {
|
||
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-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
|
||
}
|
||
function resolveLifecycle(input) {
|
||
const lifecycle_domain = inferLifecycleDomain(input);
|
||
const model = exports.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, input) {
|
||
let score = input.unit.severity.score;
|
||
const basis = ["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
|
||
};
|
||
}
|
||
function enrichProblemUnitLifecycle(input) {
|
||
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
|
||
};
|
||
}
|
||
function rankLifecycleProblemUnits(units) {
|
||
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;
|
||
});
|
||
}
|