Stage 3 Wave 1: lifecycle-слой встроен в pipeline

This commit is contained in:
2026-03-26 15:48:38 +03:00
parent 96353cfd48
commit d0b842adb0
59 changed files with 5132 additions and 154 deletions
@@ -0,0 +1,865 @@
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 defaultExpectedState(domain: LifecycleDomain): string {
if (domain === "bank_settlement") return "settlement_closed";
if (domain === "customer_settlement") return "receivable_closed";
if (domain === "deferred_expense") return "fully_written_off";
if (domain === "fixed_asset") return "depreciation_active";
if (domain === "vat_flow") return "vat_deducted";
return "close_completed";
}
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];
}
}
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();
if (includesAny(unitTokens, [/\bnds\b/, /\bvat\b/, /\btax\b/, /cross[_\s-]?branch/, /\b19\b/, /\b68\b/])) {
return "vat_flow";
}
if (includesAny(unitTokens, [/\bperiod\b/, /\bclose\b/, /закрыт/, /reporting/]) || input.unit.problem_unit_type === "period_risk_cluster") {
return "period_close";
}
if (includesAny(unitTokens, [/deferred/, /writeoff/, /рбп/, /\b97\b/])) {
return "deferred_expense";
}
if (includesAny(unitTokens, [/fixed[_\s-]?asset/, /амортиз/, /ос\b/, /\b01\b/, /\b02\b/, /\b08\b/])) {
return "fixed_asset";
}
if (includesAny(unitTokens, [/buyer/, /customer/, /дебитор/, /\b62\b/])) {
return "customer_settlement";
}
return "bank_settlement";
}
function inferCurrentState(domain: LifecycleDomain, input: LifecycleResolverInput): string {
const explicitActual = input.unit.actual_state?.trim();
if (explicitActual) {
return explicitActual;
}
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|учет/)) 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): string {
const explicitExpected = input.unit.expected_state?.trim();
if (explicitExpected) {
return explicitExpected;
}
return defaultExpectedState(domain);
}
function inferMissingTransition(input: LifecycleResolverInput): 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 "expected_transition_not_observed";
}
return null;
}
function inferInvalidTransition(input: LifecycleResolverInput): string | null {
const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase();
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 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 currentState = inferCurrentState(lifecycle_domain, input);
const expectedState = inferExpectedState(lifecycle_domain, input);
const missingTransition = inferMissingTransition(input);
const invalidTransition = inferInvalidTransition(input);
const defect = classifyLifecycleDefect({
domain: lifecycle_domain,
currentState,
expectedState,
missingTransition,
invalidTransition,
periodCloseSensitive: input.unit.period_impact?.impact_class === "close_risk"
});
const evidenceIds = uniqueStrings(input.unit.evidence_pack, 8);
const limitations = uniqueStrings(
[
...input.unit.snapshot_limitations,
...(input.candidates.some((item) => item.confidence_hint === "low") ? ["low_confidence_candidates_present"] : []),
...(input.unit.actual_state ? [] : ["actual_state_inferred"]),
...(input.unit.expected_state ? [] : ["expected_state_inferred"])
],
8
);
const confidence = resolutionConfidence(input.unit.confidence, {
hasExplicitStates: Boolean(input.unit.actual_state || input.unit.expected_state),
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: [],
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;
});
}