Stage 3 Wave 1: lifecycle-слой встроен в pipeline
This commit is contained in:
@@ -67,6 +67,14 @@ export const FEATURE_ASSISTANT_STAGE2_EVAL_V1 = toBooleanFlag(
|
||||
process.env.FEATURE_ASSISTANT_STAGE2_EVAL_V1,
|
||||
false
|
||||
);
|
||||
export const FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = toBooleanFlag(
|
||||
process.env.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1,
|
||||
false
|
||||
);
|
||||
export const FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = toBooleanFlag(
|
||||
process.env.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1,
|
||||
false
|
||||
);
|
||||
|
||||
export const DATA_DIR = process.env.DATA_DIR ?? path.resolve(MODULE_ROOT, "data");
|
||||
export const TRACES_DIR = path.resolve(DATA_DIR, "traces");
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { RouteHintSummary } from "../types/normalizer";
|
||||
import type { AnswerStructureV11, EvidenceConfidence, EvidenceItem, EvidenceLimitationReasonCode } from "../types/stage1Contracts";
|
||||
import type { ProblemUnit, ProblemUnitSummary, ProblemUnitType } from "../types/stage2ProblemUnits";
|
||||
|
||||
type ProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1";
|
||||
type ProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1" | "stage3_lifecycle_aware_v1";
|
||||
|
||||
interface ComposeAnswerInput {
|
||||
userMessage: string;
|
||||
@@ -21,6 +21,7 @@ interface ComposeAnswerInput {
|
||||
groundingCheck: AnswerGroundingCheck;
|
||||
enableAnswerPolicyV11?: boolean;
|
||||
enableProblemCentricAnswerV1?: boolean;
|
||||
enableLifecycleAnswerV1?: boolean;
|
||||
}
|
||||
|
||||
interface ComposeAnswerOutput {
|
||||
@@ -382,6 +383,61 @@ function formatAffectedScope(unit: ProblemUnit): string {
|
||||
return scopeParts.join("; ");
|
||||
}
|
||||
|
||||
function formatLifecycleScope(unit: ProblemUnit): string | null {
|
||||
if (!unit.lifecycle_domain) {
|
||||
return null;
|
||||
}
|
||||
const parts: string[] = [`domain=${unit.lifecycle_domain}`];
|
||||
if (unit.current_lifecycle_state) {
|
||||
parts.push(`current=${unit.current_lifecycle_state}`);
|
||||
}
|
||||
if (unit.expected_lifecycle_state) {
|
||||
parts.push(`expected=${unit.expected_lifecycle_state}`);
|
||||
}
|
||||
if (unit.lifecycle_defect_type) {
|
||||
parts.push(`defect=${unit.lifecycle_defect_type}`);
|
||||
}
|
||||
if (unit.missing_transition) {
|
||||
parts.push(`missing_transition=${unit.missing_transition}`);
|
||||
}
|
||||
if (unit.invalid_transition) {
|
||||
parts.push(`invalid_transition=${unit.invalid_transition}`);
|
||||
}
|
||||
if (unit.stale_duration) {
|
||||
parts.push(`stale_duration=${unit.stale_duration}`);
|
||||
}
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
function rankProblemUnitsForAnswer(units: ProblemUnit[], lifecycleAnswerEnabled: boolean): ProblemUnit[] {
|
||||
if (!lifecycleAnswerEnabled) {
|
||||
return units.slice().sort((left, right) => {
|
||||
const severityDiff = right.severity.score - left.severity.score;
|
||||
if (severityDiff !== 0) return severityDiff;
|
||||
return right.confidence.score - left.confidence.score;
|
||||
});
|
||||
}
|
||||
return units.slice().sort((left, right) => {
|
||||
const lifecycleRankDiff = (right.lifecycle_ranking_score ?? 0) - (left.lifecycle_ranking_score ?? 0);
|
||||
if (lifecycleRankDiff !== 0) return lifecycleRankDiff;
|
||||
const lifecycleConfidenceDiff = (right.lifecycle_confidence?.score ?? 0) - (left.lifecycle_confidence?.score ?? 0);
|
||||
if (lifecycleConfidenceDiff !== 0) return lifecycleConfidenceDiff;
|
||||
const severityDiff = right.severity.score - left.severity.score;
|
||||
if (severityDiff !== 0) return severityDiff;
|
||||
return right.confidence.score - left.confidence.score;
|
||||
});
|
||||
}
|
||||
|
||||
function hasLifecycleResolution(units: ProblemUnit[]): boolean {
|
||||
return units.some(
|
||||
(unit) =>
|
||||
Boolean(unit.lifecycle_domain) &&
|
||||
Boolean(unit.current_lifecycle_state) &&
|
||||
Boolean(unit.expected_lifecycle_state) &&
|
||||
Boolean(unit.lifecycle_defect_type)
|
||||
);
|
||||
}
|
||||
|
||||
function buildProblemCentricActions(input: {
|
||||
units: ProblemUnit[];
|
||||
mode: PolicyMode;
|
||||
@@ -406,6 +462,17 @@ function buildProblemCentricActions(input: {
|
||||
if (unitTypes.has("lifecycle_anomaly_node")) {
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
}
|
||||
for (const unit of input.units) {
|
||||
if (unit.lifecycle_defect_type === "stale_active_state") {
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "misclosed_state") {
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
}
|
||||
}
|
||||
|
||||
if (input.mode === "clarification_required") {
|
||||
if (input.missingAnchors.period) {
|
||||
@@ -825,7 +892,14 @@ function buildProblemCentricAnswerSummary(input: {
|
||||
mode: PolicyMode;
|
||||
weakUnits: boolean;
|
||||
summary: ProblemUnitSummary | null;
|
||||
lifecycleEnriched: boolean;
|
||||
}): string {
|
||||
if (input.lifecycleEnriched && input.summary?.lifecycle_enriched_units && input.summary.lifecycle_enriched_units > 0) {
|
||||
if (input.mode === "clarification_required") {
|
||||
return "Выявлены lifecycle-дефекты, но для надежного вывода требуется уточнение предметных якорей.";
|
||||
}
|
||||
return `Сформирован lifecycle-aware problem срез: выделено ${input.summary.lifecycle_enriched_units} lifecycle-узлов с приоритетом по дефектам перехода.`;
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
return "Выявлены проблемные кластеры, но для надежного вывода требуется предметное уточнение фокуса.";
|
||||
}
|
||||
@@ -842,17 +916,31 @@ function buildProblemCentricDirectAnswer(input: {
|
||||
mode: PolicyMode;
|
||||
units: ProblemUnit[];
|
||||
weakUnits: boolean;
|
||||
lifecycleAnswerEnabled: boolean;
|
||||
}): string {
|
||||
const lead =
|
||||
input.mode === "clarification_required"
|
||||
? "Обнаружены проблемные зоны, но без уточнения якорей сильный factual-вывод преждевременен."
|
||||
: input.weakUnits
|
||||
? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме."
|
||||
: "Выделены ключевые проблемные зоны и их влияние на учетный контур.";
|
||||
: input.lifecycleAnswerEnabled && hasLifecycleResolution(input.units)
|
||||
? "Выделены lifecycle-проблемы: определены текущие/ожидаемые стадии и тип нарушения перехода."
|
||||
: "Выделены ключевые проблемные зоны и их влияние на учетный контур.";
|
||||
|
||||
const unitLines = input.units.map((unit) => {
|
||||
const scope = formatAffectedScope(unit);
|
||||
return `- ${unit.title}: ${unit.business_defect_class}; ${scope}; severity=${unit.severity.grade}, confidence=${unit.confidence.grade}.`;
|
||||
const lifecycleScope = input.lifecycleAnswerEnabled ? formatLifecycleScope(unit) : null;
|
||||
const lifecycleInterpretation = input.lifecycleAnswerEnabled ? unit.business_lifecycle_interpretation : null;
|
||||
const segments = [
|
||||
`${unit.title}: ${unit.business_defect_class}`,
|
||||
scope,
|
||||
lifecycleScope,
|
||||
lifecycleInterpretation,
|
||||
`severity=${unit.severity.grade}`,
|
||||
`confidence=${unit.confidence.grade}`,
|
||||
unit.lifecycle_confidence ? `lifecycle_confidence=${unit.lifecycle_confidence.grade}` : null
|
||||
].filter((item): item is string => Boolean(item));
|
||||
return `- ${segments.join("; ")}.`;
|
||||
});
|
||||
|
||||
if (unitLines.length === 0) {
|
||||
@@ -873,8 +961,10 @@ function buildProblemCentricAnswerStructure(input: {
|
||||
retrievalResults: UnifiedRetrievalResult[];
|
||||
missingAnchors: MissingAnchors;
|
||||
coverageReport: RequirementCoverageReport;
|
||||
lifecycleAnswerEnabled: boolean;
|
||||
}): AnswerStructureV11 {
|
||||
const weakUnits = input.selectedUnits.every((item) => item.confidence.grade === "low");
|
||||
const lifecycleEnriched = input.lifecycleAnswerEnabled && hasLifecycleResolution(input.selectedUnits);
|
||||
const unitMechanismNotes = uniqueStrings(
|
||||
input.selectedUnits
|
||||
.map((item) => item.mechanism_summary)
|
||||
@@ -932,12 +1022,14 @@ function buildProblemCentricAnswerStructure(input: {
|
||||
answer_summary: buildProblemCentricAnswerSummary({
|
||||
mode: input.mode,
|
||||
weakUnits,
|
||||
summary: input.problemSummary
|
||||
summary: input.problemSummary,
|
||||
lifecycleEnriched
|
||||
}),
|
||||
direct_answer: buildProblemCentricDirectAnswer({
|
||||
mode: input.mode,
|
||||
units: input.selectedUnits,
|
||||
weakUnits
|
||||
weakUnits,
|
||||
lifecycleAnswerEnabled: input.lifecycleAnswerEnabled
|
||||
}),
|
||||
mechanism_block: {
|
||||
status: mechanismStatus,
|
||||
@@ -1059,10 +1151,11 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
.filter((item): item is string => typeof item === "string" && item.trim().length > 0),
|
||||
6
|
||||
);
|
||||
const lifecycleAnswerEnabled = Boolean(input.enableLifecycleAnswerV1);
|
||||
const problemUnits = flattenProblemUnits(input.retrievalResults);
|
||||
const problemUnitSummary = selectProblemUnitSummary(input.retrievalResults);
|
||||
const problemHeavyUnits = problemUnits.filter((item) => PROBLEM_HEAVY_TYPES.has(item.problem_unit_type));
|
||||
const selectedProblemUnits = problemHeavyUnits.slice(0, 4);
|
||||
const selectedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled).slice(0, 4);
|
||||
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
|
||||
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems);
|
||||
const lowConfidenceSignals = evidenceItems.filter((item) => item.confidence === "low").length;
|
||||
@@ -1138,9 +1231,12 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
groundingCheck: input.groundingCheck,
|
||||
retrievalResults: input.retrievalResults,
|
||||
missingAnchors,
|
||||
coverageReport: input.coverageReport
|
||||
coverageReport: input.coverageReport,
|
||||
lifecycleAnswerEnabled
|
||||
});
|
||||
|
||||
const lifecycleModeActive = lifecycleAnswerEnabled && hasLifecycleResolution(selectedProblemUnits);
|
||||
|
||||
return {
|
||||
assistant_reply: renderPolicyReply(problemCentricStructure),
|
||||
fallback_type: decision.fallback_type,
|
||||
@@ -1148,7 +1244,7 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
answer_structure_v11: problemCentricStructure,
|
||||
problem_centric_answer_applied: true,
|
||||
problem_units_used_count: selectedProblemUnits.length,
|
||||
problem_answer_mode: "stage2_problem_centric_v1",
|
||||
problem_answer_mode: lifecycleModeActive ? "stage3_lifecycle_aware_v1" : "stage2_problem_centric_v1",
|
||||
problem_unit_ids_used: selectedProblemUnits.map((item) => item.problem_unit_id)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
FEATURE_ASSISTANT_CONTRACTS_V11,
|
||||
FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1,
|
||||
FEATURE_ASSISTANT_INVESTIGATION_STATE_V1,
|
||||
FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1,
|
||||
FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
||||
FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1,
|
||||
FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1
|
||||
@@ -1203,7 +1204,8 @@ export class AssistantService {
|
||||
coverageReport: coverageEvaluation.coverage,
|
||||
groundingCheck,
|
||||
enableAnswerPolicyV11: FEATURE_ASSISTANT_ANSWER_POLICY_V11,
|
||||
enableProblemCentricAnswerV1: FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1
|
||||
enableProblemCentricAnswerV1: FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
||||
enableLifecycleAnswerV1: FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
||||
});
|
||||
|
||||
const answerStructureV11 = FEATURE_ASSISTANT_CONTRACTS_V11
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
PROBLEM_UNIT_SCHEMA_VERSION,
|
||||
PROBLEM_UNIT_SUMMARY_SCHEMA_VERSION
|
||||
} from "../types/stage2ProblemUnits";
|
||||
import { FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 } from "../config";
|
||||
import { enrichProblemUnitLifecycle, rankLifecycleProblemUnits } from "./lifecycleRuntime";
|
||||
|
||||
type RetrievalResultType = "list" | "summary" | "object" | "chain" | "ranking";
|
||||
|
||||
@@ -486,6 +488,18 @@ function collapseSignature(unit: ProblemUnit): string {
|
||||
return [unit.problem_unit_type, unit.business_defect_class, unit.failed_expected_edge ?? "none", backlink].join("|");
|
||||
}
|
||||
|
||||
function preferredLifecycleSource(left: ProblemUnit, right: ProblemUnit): ProblemUnit {
|
||||
const leftRank = left.lifecycle_ranking_score ?? 0;
|
||||
const rightRank = right.lifecycle_ranking_score ?? 0;
|
||||
if (rightRank > leftRank) {
|
||||
return right;
|
||||
}
|
||||
if (leftRank > rightRank) {
|
||||
return left;
|
||||
}
|
||||
return right.confidence.score > left.confidence.score ? right : left;
|
||||
}
|
||||
|
||||
export function collapseDuplicates(units: ProblemUnit[]): {
|
||||
problem_units: ProblemUnit[];
|
||||
duplicate_collapses: number;
|
||||
@@ -502,6 +516,7 @@ export function collapseDuplicates(units: ProblemUnit[]): {
|
||||
}
|
||||
|
||||
duplicateCollapses += 1;
|
||||
const preferredLifecycle = preferredLifecycleSource(existing, unit);
|
||||
bySignature.set(signature, {
|
||||
...existing,
|
||||
evidence_pack: uniqueStrings([...existing.evidence_pack, ...unit.evidence_pack]),
|
||||
@@ -521,7 +536,72 @@ export function collapseDuplicates(units: ProblemUnit[]): {
|
||||
affected_contracts: uniqueStrings([...existing.affected_contracts, ...unit.affected_contracts]),
|
||||
snapshot_limitations: uniqueStrings([...existing.snapshot_limitations, ...unit.snapshot_limitations]),
|
||||
severity: unit.severity.score > existing.severity.score ? unit.severity : existing.severity,
|
||||
confidence: unit.confidence.score > existing.confidence.score ? unit.confidence : existing.confidence
|
||||
confidence: unit.confidence.score > existing.confidence.score ? unit.confidence : existing.confidence,
|
||||
...(preferredLifecycle.lifecycle_domain
|
||||
? {
|
||||
lifecycle_domain: preferredLifecycle.lifecycle_domain
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.lifecycle_object_id
|
||||
? {
|
||||
lifecycle_object_id: preferredLifecycle.lifecycle_object_id
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.current_lifecycle_state
|
||||
? {
|
||||
current_lifecycle_state: preferredLifecycle.current_lifecycle_state
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.expected_lifecycle_state
|
||||
? {
|
||||
expected_lifecycle_state: preferredLifecycle.expected_lifecycle_state
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.missing_transition
|
||||
? {
|
||||
missing_transition: preferredLifecycle.missing_transition
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.invalid_transition
|
||||
? {
|
||||
invalid_transition: preferredLifecycle.invalid_transition
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.lifecycle_defect_type
|
||||
? {
|
||||
lifecycle_defect_type: preferredLifecycle.lifecycle_defect_type
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.stale_duration
|
||||
? {
|
||||
stale_duration: preferredLifecycle.stale_duration
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.lifecycle_confidence
|
||||
? {
|
||||
lifecycle_confidence: preferredLifecycle.lifecycle_confidence
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.business_lifecycle_interpretation
|
||||
? {
|
||||
business_lifecycle_interpretation: preferredLifecycle.business_lifecycle_interpretation
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.lifecycle_resolution
|
||||
? {
|
||||
lifecycle_resolution: preferredLifecycle.lifecycle_resolution
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.lifecycle_ranking_score !== undefined
|
||||
? {
|
||||
lifecycle_ranking_score: preferredLifecycle.lifecycle_ranking_score
|
||||
}
|
||||
: {}),
|
||||
...(preferredLifecycle.lifecycle_ranking_basis
|
||||
? {
|
||||
lifecycle_ranking_basis: preferredLifecycle.lifecycle_ranking_basis
|
||||
}
|
||||
: {})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -544,11 +624,21 @@ function buildSummary(units: ProblemUnit[], duplicateCollapses: number): Problem
|
||||
medium: 0,
|
||||
high: 0
|
||||
};
|
||||
const lifecycleDomainDistribution: NonNullable<ProblemUnitSummary["lifecycle_domain_distribution"]> = {};
|
||||
const lifecycleDefectDistribution: NonNullable<ProblemUnitSummary["lifecycle_defect_distribution"]> = {};
|
||||
let lifecycleEnrichedUnits = 0;
|
||||
|
||||
for (const unit of units) {
|
||||
typeDistribution[unit.problem_unit_type] = (typeDistribution[unit.problem_unit_type] ?? 0) + 1;
|
||||
severityDistribution[unit.severity.grade] += 1;
|
||||
confidenceDistribution[unit.confidence.grade] += 1;
|
||||
if (unit.lifecycle_domain) {
|
||||
lifecycleEnrichedUnits += 1;
|
||||
lifecycleDomainDistribution[unit.lifecycle_domain] = (lifecycleDomainDistribution[unit.lifecycle_domain] ?? 0) + 1;
|
||||
}
|
||||
if (unit.lifecycle_defect_type) {
|
||||
lifecycleDefectDistribution[unit.lifecycle_defect_type] = (lifecycleDefectDistribution[unit.lifecycle_defect_type] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -559,7 +649,10 @@ function buildSummary(units: ProblemUnit[], duplicateCollapses: number): Problem
|
||||
type_distribution: typeDistribution,
|
||||
severity_distribution: severityDistribution,
|
||||
confidence_distribution: confidenceDistribution,
|
||||
primary_unit_type: units[0]?.problem_unit_type ?? null
|
||||
primary_unit_type: units[0]?.problem_unit_type ?? null,
|
||||
lifecycle_enriched_units: lifecycleEnrichedUnits,
|
||||
lifecycle_domain_distribution: lifecycleDomainDistribution,
|
||||
lifecycle_defect_distribution: lifecycleDefectDistribution
|
||||
};
|
||||
}
|
||||
|
||||
@@ -580,13 +673,24 @@ export function assembleProblemUnits(input: AssembleProblemUnitsInput): {
|
||||
risk_factors: uniqueStrings(input.risk_factors ?? [])
|
||||
});
|
||||
const clusters = clusterCandidateEvidence(candidates);
|
||||
const units = clusters.map((cluster, index) => buildProblemUnit(cluster, index));
|
||||
const units = clusters.map((cluster, index) => {
|
||||
const baseUnit = buildProblemUnit(cluster, index);
|
||||
if (!FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1) {
|
||||
return baseUnit;
|
||||
}
|
||||
return enrichProblemUnitLifecycle({
|
||||
unit: baseUnit,
|
||||
candidates: cluster.candidates
|
||||
});
|
||||
});
|
||||
const collapsed = collapseDuplicates(units);
|
||||
const rankedUnits = FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1
|
||||
? rankLifecycleProblemUnits(collapsed.problem_units)
|
||||
: collapsed.problem_units;
|
||||
|
||||
return {
|
||||
candidate_evidence: candidates,
|
||||
problem_units: collapsed.problem_units,
|
||||
problem_unit_summary: buildSummary(collapsed.problem_units, collapsed.duplicate_collapses)
|
||||
problem_units: rankedUnits,
|
||||
problem_unit_summary: buildSummary(rankedUnits, collapsed.duplicate_collapses)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,9 @@ function mergeSummaryWithProblemUnitMeta(
|
||||
duplicateCollapses: number;
|
||||
severityDistribution: Record<string, number>;
|
||||
confidenceDistribution: Record<string, number>;
|
||||
lifecycleEnrichedUnits: number;
|
||||
lifecycleDomainDistribution: Record<string, number>;
|
||||
lifecycleDefectDistribution: Record<string, number>;
|
||||
}
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
@@ -114,7 +117,10 @@ function mergeSummaryWithProblemUnitMeta(
|
||||
problem_unit_types: input.unitTypes,
|
||||
problem_unit_duplicate_collapses: input.duplicateCollapses,
|
||||
problem_unit_severity_distribution: input.severityDistribution,
|
||||
problem_unit_confidence_distribution: input.confidenceDistribution
|
||||
problem_unit_confidence_distribution: input.confidenceDistribution,
|
||||
lifecycle_enriched_units: input.lifecycleEnrichedUnits,
|
||||
problem_unit_lifecycle_domain_distribution: input.lifecycleDomainDistribution,
|
||||
problem_unit_lifecycle_defect_distribution: input.lifecycleDefectDistribution
|
||||
};
|
||||
}
|
||||
|
||||
@@ -526,7 +532,10 @@ export function normalizeRetrievalResult(
|
||||
unitTypes: assembled.problem_unit_summary.unit_types,
|
||||
duplicateCollapses: assembled.problem_unit_summary.duplicate_collapses,
|
||||
severityDistribution: assembled.problem_unit_summary.severity_distribution,
|
||||
confidenceDistribution: assembled.problem_unit_summary.confidence_distribution
|
||||
confidenceDistribution: assembled.problem_unit_summary.confidence_distribution,
|
||||
lifecycleEnrichedUnits: assembled.problem_unit_summary.lifecycle_enriched_units ?? 0,
|
||||
lifecycleDomainDistribution: (assembled.problem_unit_summary.lifecycle_domain_distribution ?? {}) as Record<string, number>,
|
||||
lifecycleDefectDistribution: (assembled.problem_unit_summary.lifecycle_defect_distribution ?? {}) as Record<string, number>
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,10 @@ export type AssistantReplyType =
|
||||
export type RetrievalResultStatus = "ok" | "empty" | "partial" | "error";
|
||||
export type RetrievalResultType = "list" | "summary" | "object" | "chain" | "ranking";
|
||||
export type RetrievalConfidence = "high" | "medium" | "low";
|
||||
export type AssistantProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1";
|
||||
export type AssistantProblemAnswerMode =
|
||||
| "stage1_policy_v11"
|
||||
| "stage2_problem_centric_v1"
|
||||
| "stage3_lifecycle_aware_v1";
|
||||
|
||||
export interface AssistantRequirement {
|
||||
requirement_id: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { InvestigationState } from "./stage1Contracts";
|
||||
import type { EvidenceSourceRef } from "./stage1Contracts";
|
||||
import type { LifecycleConfidence, LifecycleDefectType, LifecycleDomain, LifecycleResolution } from "./stage3Lifecycle";
|
||||
|
||||
export const CANDIDATE_EVIDENCE_SCHEMA_VERSION = "candidate_evidence_v0_1" as const;
|
||||
export const PROBLEM_UNIT_SCHEMA_VERSION = "problem_unit_v0_1" as const;
|
||||
@@ -75,6 +76,19 @@ export interface ProblemUnit {
|
||||
evidence_pack: string[];
|
||||
entity_backlinks: ProblemUnitEntityBacklink[];
|
||||
snapshot_limitations: string[];
|
||||
lifecycle_domain?: LifecycleDomain;
|
||||
lifecycle_object_id?: string;
|
||||
current_lifecycle_state?: string;
|
||||
expected_lifecycle_state?: string;
|
||||
missing_transition?: string;
|
||||
invalid_transition?: string;
|
||||
lifecycle_defect_type?: LifecycleDefectType;
|
||||
stale_duration?: string;
|
||||
lifecycle_confidence?: LifecycleConfidence;
|
||||
business_lifecycle_interpretation?: string;
|
||||
lifecycle_resolution?: LifecycleResolution;
|
||||
lifecycle_ranking_score?: number;
|
||||
lifecycle_ranking_basis?: string[];
|
||||
}
|
||||
|
||||
export interface ProblemUnitSummary {
|
||||
@@ -86,6 +100,9 @@ export interface ProblemUnitSummary {
|
||||
severity_distribution: Record<ProblemSeverityGrade, number>;
|
||||
confidence_distribution: Record<ProblemConfidenceGrade, number>;
|
||||
primary_unit_type: ProblemUnitType | null;
|
||||
lifecycle_enriched_units?: number;
|
||||
lifecycle_domain_distribution?: Partial<Record<LifecycleDomain, number>>;
|
||||
lifecycle_defect_distribution?: Partial<Record<LifecycleDefectType, number>>;
|
||||
}
|
||||
|
||||
export interface InvestigationProblemUnitState {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
export const LIFECYCLE_MODEL_SCHEMA_VERSION = "lifecycle_model_v0_1" as const;
|
||||
|
||||
export const STAGE3_LIFECYCLE_DOMAINS = [
|
||||
"bank_settlement",
|
||||
"customer_settlement",
|
||||
"deferred_expense",
|
||||
"fixed_asset",
|
||||
"vat_flow",
|
||||
"period_close"
|
||||
] as const;
|
||||
|
||||
export type LifecycleDomain = (typeof STAGE3_LIFECYCLE_DOMAINS)[number];
|
||||
|
||||
export type LifecycleStateClass = "initial" | "active" | "terminal" | "problematic";
|
||||
|
||||
export type LifecycleTransitionType = "expected" | "optional" | "forbidden";
|
||||
|
||||
export const LIFECYCLE_DEFECT_TYPES = [
|
||||
"missing_expected_transition",
|
||||
"invalid_transition",
|
||||
"stale_active_state",
|
||||
"contradictory_state",
|
||||
"premature_terminal_state",
|
||||
"misclosed_state",
|
||||
"orphan_intermediate_state",
|
||||
"cross_branch_state_conflict"
|
||||
] as const;
|
||||
|
||||
export type LifecycleDefectType = (typeof LIFECYCLE_DEFECT_TYPES)[number];
|
||||
|
||||
export interface LifecycleStateDefinition {
|
||||
state_code: string;
|
||||
state_label: string;
|
||||
state_class: LifecycleStateClass;
|
||||
entry_conditions: string[];
|
||||
exit_conditions: string[];
|
||||
is_terminal: boolean;
|
||||
is_problematic: boolean;
|
||||
business_meaning: string;
|
||||
}
|
||||
|
||||
export interface LifecycleTransitionDefinition {
|
||||
from_state: string;
|
||||
to_state: string;
|
||||
transition_type: LifecycleTransitionType;
|
||||
required_evidence: string[];
|
||||
optional_evidence: string[];
|
||||
forbidden_conditions: string[];
|
||||
business_meaning: string;
|
||||
}
|
||||
|
||||
export interface LifecycleDefectDefinition {
|
||||
defect_code: LifecycleDefectType;
|
||||
defect_class: "timing" | "path" | "consistency" | "closure";
|
||||
severity_hint: "low" | "medium" | "high";
|
||||
business_meaning: string;
|
||||
evidence_requirements: string[];
|
||||
period_impact_potential: "none" | "indirect" | "direct";
|
||||
}
|
||||
|
||||
export interface LifecycleDomainModel {
|
||||
schema_version: typeof LIFECYCLE_MODEL_SCHEMA_VERSION;
|
||||
lifecycle_domain: LifecycleDomain;
|
||||
lifecycle_object_types: string[];
|
||||
states: LifecycleStateDefinition[];
|
||||
transitions: LifecycleTransitionDefinition[];
|
||||
defects: LifecycleDefectDefinition[];
|
||||
}
|
||||
|
||||
export type LifecycleConfidenceGrade = "low" | "medium" | "high";
|
||||
|
||||
export interface LifecycleConfidence {
|
||||
score: number;
|
||||
grade: LifecycleConfidenceGrade;
|
||||
}
|
||||
|
||||
export interface LifecycleResolution {
|
||||
lifecycle_object_id: string;
|
||||
lifecycle_domain: LifecycleDomain;
|
||||
resolved_current_state: string;
|
||||
resolved_expected_state: string;
|
||||
resolved_previous_states: string[];
|
||||
missing_transitions: string[];
|
||||
invalid_transitions: string[];
|
||||
detected_defects: LifecycleDefectType[];
|
||||
state_confidence: LifecycleConfidence;
|
||||
resolution_evidence: string[];
|
||||
snapshot_limitations: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user