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,232 @@
import { describe, expect, it } from "vitest";
import { composeAssistantAnswer } from "../src/services/answerComposer";
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant";
import type { ProblemUnit, ProblemUnitSummary } from "../src/types/stage2ProblemUnits";
function buildRouteSummary() {
return {
mode: "deterministic_v2" as const,
message_in_scope: true,
scope_confidence: "high" as const,
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none" as const,
message: null
}
};
}
function buildCoverage(partial = false): RequirementCoverageReport {
return {
requirements_total: 1,
requirements_covered: partial ? 0 : 1,
requirements_uncovered: partial ? ["R1"] : [],
requirements_partially_covered: partial ? ["R1"] : [],
clarification_needed_for: [],
out_of_scope_requirements: []
};
}
function buildGrounding(status: AnswerGroundingCheck["status"]): AnswerGroundingCheck {
return {
status,
route_subject_match: true,
missing_requirements: status === "partial" ? ["R1"] : [],
reasons: status === "partial" ? ["Coverage is partial for lifecycle-focused analysis."] : [],
why_included_summary: ["synthetic-test"],
selection_reason_summary: ["synthetic-test"]
};
}
function buildLifecycleProblemUnit(): ProblemUnit {
return {
schema_version: "problem_unit_v0_1",
problem_unit_id: "pu-lc-1",
problem_unit_type: "lifecycle_anomaly_node",
title: "Lifecycle anomaly node detected",
mechanism_summary: "Mechanism candidate: expected transition is missing.",
business_defect_class: "missing_expected_transition",
severity: {
score: 0.84,
grade: "high"
},
confidence: {
score: 0.68,
grade: "medium"
},
affected_entities: ["Document:DOC-1"],
affected_documents: ["Document:DOC-1"],
affected_postings: [],
affected_accounts: ["51", "60"],
affected_counterparties: ["Counterparty:CP-1"],
affected_contracts: ["Contract:CTR-1"],
expected_state: "settlement_closed",
actual_state: "stale_unlinked_payment",
failed_expected_edge: "payment_to_settlement",
period_impact: {
is_period_sensitive: true,
impact_class: "close_risk"
},
evidence_pack: ["cand-1"],
entity_backlinks: [{ entity: "Document", id: "DOC-1" }],
snapshot_limitations: [],
lifecycle_domain: "bank_settlement",
lifecycle_object_id: "lcobj-pu-lc-1",
current_lifecycle_state: "stale_unlinked_payment",
expected_lifecycle_state: "settlement_closed",
missing_transition: "payment_to_settlement",
lifecycle_defect_type: "stale_active_state",
stale_duration: "period_boundary_exceeded",
lifecycle_confidence: {
score: 0.79,
grade: "high"
},
business_lifecycle_interpretation:
"Текущая стадия: stale_unlinked_payment; ожидаемая стадия: settlement_closed. Объект завис во времени и не дошел до ожидаемого перехода.",
lifecycle_ranking_score: 1.41,
lifecycle_ranking_basis: ["base_problem_severity", "stale_duration_weight", "period_close_impact"]
};
}
function buildSummary(units: ProblemUnit[]): ProblemUnitSummary {
const unitTypes = Array.from(new Set(units.map((item) => item.problem_unit_type)));
return {
schema_version: "problem_unit_summary_v0_1",
units_total: units.length,
duplicate_collapses: 0,
unit_types: unitTypes,
type_distribution: {
lifecycle_anomaly_node: units.length
},
severity_distribution: {
low: 0,
medium: 0,
high: units.length
},
confidence_distribution: {
low: 0,
medium: units.length,
high: 0
},
primary_unit_type: unitTypes[0] ?? null,
lifecycle_enriched_units: units.length,
lifecycle_domain_distribution: {
bank_settlement: units.length
},
lifecycle_defect_distribution: {
stale_active_state: units.length
}
};
}
function buildRetrievalResult(problemUnits: ProblemUnit[]): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "chain",
items: [
{
counterparty_id: "CP-1",
operations_count: 5,
document_refs_count: 3
}
],
raw_entities: [],
candidate_evidence: [],
problem_units: problemUnits,
problem_unit_summary: buildSummary(problemUnits),
summary: {
broad_query_detected: true,
broad_result_flag: true,
minimum_evidence_failed: false,
degraded_to: "partial",
narrowing_strength: "weak"
},
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "Document",
id: "DOC-1",
period: "2020-06",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-06"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "Document",
id: "DOC-1",
period: "2020-06"
},
locator: {
field_path: "risk_score",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "failed_edge=payment_to_settlement",
confidence: "medium",
limitation: null,
payload: {
risk_score: 5
}
}
],
why_included: ["synthetic-test"],
selection_reason: ["synthetic-test"],
risk_factors: ["broken_chain"],
business_interpretation: ["synthetic-test"],
confidence: "medium",
limitations: [],
errors: []
};
}
describe("assistant lifecycle-aware answer mode v1", () => {
it("promotes stage3 lifecycle mode when lifecycle answer flag is enabled", () => {
const units = [buildLifecycleProblemUnit()];
const output = composeAssistantAnswer({
userMessage: "Проверь, где зависли платежи по 51/60 и какой переход не завершился.",
routeSummary: buildRouteSummary(),
retrievalResults: [buildRetrievalResult(units)],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверить lifecycle-переход",
subject_tokens: ["chain", "account_51", "account_60"],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: buildCoverage(true),
groundingCheck: buildGrounding("partial"),
enableAnswerPolicyV11: true,
enableProblemCentricAnswerV1: true,
enableLifecycleAnswerV1: true
});
expect(output.problem_centric_answer_applied).toBe(true);
expect(output.problem_answer_mode).toBe("stage3_lifecycle_aware_v1");
expect(output.answer_structure_v11?.answer_summary).toMatch(/lifecycle|Lifecycle/i);
expect(output.answer_structure_v11?.direct_answer).toContain("current=stale_unlinked_payment");
expect(output.answer_structure_v11?.direct_answer).toContain("expected=settlement_closed");
expect(output.answer_structure_v11?.direct_answer).toContain("defect=stale_active_state");
});
});
@@ -0,0 +1,162 @@
import { describe, expect, it } from "vitest";
import type { CandidateEvidenceItem, ProblemUnit } from "../src/types/stage2ProblemUnits";
import { enrichProblemUnitLifecycle, rankLifecycleProblemUnits, resolveLifecycle } from "../src/services/lifecycleRuntime";
function buildBaseProblemUnit(input: {
id: string;
type: ProblemUnit["problem_unit_type"];
expectedState?: string;
actualState?: string;
failedEdge?: string;
impactCloseRisk?: boolean;
severity?: number;
confidence?: number;
}): ProblemUnit {
const severityScore = input.severity ?? 0.62;
const confidenceScore = input.confidence ?? 0.58;
const severityGrade: ProblemUnit["severity"]["grade"] = severityScore >= 0.7 ? "high" : severityScore >= 0.4 ? "medium" : "low";
const confidenceGrade: ProblemUnit["confidence"]["grade"] =
confidenceScore >= 0.75 ? "high" : confidenceScore >= 0.45 ? "medium" : "low";
return {
schema_version: "problem_unit_v0_1",
problem_unit_id: input.id,
problem_unit_type: input.type,
title: "Synthetic unit",
mechanism_summary: "Synthetic mechanism",
business_defect_class: "broken_lifecycle",
severity: {
score: Number(severityScore.toFixed(2)),
grade: severityGrade
},
confidence: {
score: Number(confidenceScore.toFixed(2)),
grade: confidenceGrade
},
affected_entities: ["Document:DOC-1"],
affected_documents: ["Document:DOC-1"],
affected_postings: [],
affected_accounts: ["51", "60"],
affected_counterparties: [],
affected_contracts: [],
...(input.expectedState
? {
expected_state: input.expectedState
}
: {}),
...(input.actualState
? {
actual_state: input.actualState
}
: {}),
...(input.failedEdge
? {
failed_expected_edge: input.failedEdge
}
: {}),
...(input.impactCloseRisk
? {
period_impact: {
is_period_sensitive: true,
impact_class: "close_risk" as const
}
}
: {}),
evidence_pack: ["cand-1"],
entity_backlinks: [{ entity: "Document", id: "DOC-1" }],
snapshot_limitations: []
};
}
function buildCandidate(input: {
id: string;
anomalies?: string[];
relations?: string[];
confidence?: "high" | "medium" | "low";
}): CandidateEvidenceItem {
return {
schema_version: "candidate_evidence_v0_1",
candidate_id: input.id,
route: "hybrid_store_plus_live",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "Document",
id: "DOC-1",
period: "2020-06",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-06"
},
relation_pattern_hits: input.relations ?? [],
anomaly_patterns: input.anomalies ?? [],
entity_backlinks: [{ entity: "Document", id: "DOC-1" }],
confidence_hint: input.confidence ?? "medium"
};
}
describe("lifecycle runtime stage3", () => {
it("resolves stale missing transition for bank settlement chains", () => {
const unit = buildBaseProblemUnit({
id: "pu-bank-1",
type: "broken_chain_segment",
expectedState: "settlement_closed",
failedEdge: "payment_to_settlement"
});
const candidates = [
buildCandidate({
id: "cand-1",
relations: ["payment_to_settlement", "statement_to_document"],
anomalies: ["broken_lifecycle", "missing_link", "no_continuation"]
})
];
const resolution = resolveLifecycle({ unit, candidates });
expect(resolution.lifecycle_domain).toBe("bank_settlement");
expect(resolution.missing_transitions.length).toBeGreaterThan(0);
expect(resolution.detected_defects[0]).toBe("stale_active_state");
});
it("classifies cross-branch conflicts for VAT flow", () => {
const unit = buildBaseProblemUnit({
id: "pu-vat-1",
type: "cross_branch_inconsistency_cluster"
});
const candidates = [
buildCandidate({
id: "cand-1",
anomalies: ["cross_branch_inconsistency", "vat_chain_conflict"],
relations: ["invoice_to_vat"]
})
];
const enriched = enrichProblemUnitLifecycle({ unit, candidates });
expect(enriched.lifecycle_domain).toBe("vat_flow");
expect(enriched.lifecycle_defect_type).toBe("cross_branch_state_conflict");
expect(typeof enriched.business_lifecycle_interpretation).toBe("string");
});
it("sorts lifecycle-ranked units by lifecycle severity and ranking score", () => {
const stale = enrichProblemUnitLifecycle({
unit: buildBaseProblemUnit({
id: "pu-stale",
type: "period_risk_cluster",
impactCloseRisk: true,
expectedState: "close_completed"
}),
candidates: [buildCandidate({ id: "cand-stale", anomalies: ["period_close_risk", "missing_link", "no_continuation"] })]
});
const generic = enrichProblemUnitLifecycle({
unit: buildBaseProblemUnit({
id: "pu-generic",
type: "document_conflict",
severity: 0.4,
confidence: 0.45
}),
candidates: [buildCandidate({ id: "cand-generic", anomalies: ["anomaly_signal"] })]
});
const ranked = rankLifecycleProblemUnits([generic, stale]);
expect(ranked[0].problem_unit_id).toBe("pu-stale");
expect((ranked[0].lifecycle_ranking_score ?? 0) >= (ranked[1].lifecycle_ranking_score ?? 0)).toBe(true);
});
});
@@ -0,0 +1,116 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const PROBLEM_UNITS_FLAG = "FEATURE_ASSISTANT_PROBLEM_UNITS_V1";
const LIFECYCLE_RUNTIME_FLAG = "FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1";
const ORIGINAL_PROBLEM_UNITS_FLAG = process.env[PROBLEM_UNITS_FLAG];
const ORIGINAL_LIFECYCLE_RUNTIME_FLAG = process.env[LIFECYCLE_RUNTIME_FLAG];
function restoreFlags(): void {
if (ORIGINAL_PROBLEM_UNITS_FLAG === undefined) {
delete process.env[PROBLEM_UNITS_FLAG];
} else {
process.env[PROBLEM_UNITS_FLAG] = ORIGINAL_PROBLEM_UNITS_FLAG;
}
if (ORIGINAL_LIFECYCLE_RUNTIME_FLAG === undefined) {
delete process.env[LIFECYCLE_RUNTIME_FLAG];
} else {
process.env[LIFECYCLE_RUNTIME_FLAG] = ORIGINAL_LIFECYCLE_RUNTIME_FLAG;
}
}
async function normalizeWithFlags(input: {
problemUnitsFlag: "0" | "1";
lifecycleRuntimeFlag: "0" | "1";
}) {
process.env[PROBLEM_UNITS_FLAG] = input.problemUnitsFlag;
process.env[LIFECYCLE_RUNTIME_FLAG] = input.lifecycleRuntimeFlag;
vi.resetModules();
const { normalizeRetrievalResult } = await import("../src/services/retrievalResultNormalizer");
return normalizeRetrievalResult("F1", ["R1"], "hybrid_store_plus_live", {
status: "ok",
result_type: "list",
items: [
{
source_entity: "Document",
source_id: "DOC-1",
risk_score: 4
}
],
summary: {
broad_query_detected: false
},
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "Document",
id: "DOC-1",
period: "2020-06"
},
locator: {
field_path: "risk_score",
item_index: 0
}
},
failed_expected_edge: "payment_to_settlement",
anomaly_patterns: ["period_close_risk", "broken_lifecycle", "missing_link", "no_continuation"],
confidence: "medium"
}
],
why_included: ["test"],
selection_reason: ["test"],
risk_factors: ["test"],
business_interpretation: ["test"],
confidence: "medium",
limitations: [],
errors: []
});
}
describe.sequential("retrieval lifecycle runtime rollout", () => {
afterEach(() => {
restoreFlags();
vi.resetModules();
});
it("keeps Stage 2 payload when lifecycle runtime flag is OFF", async () => {
const result = await normalizeWithFlags({
problemUnitsFlag: "1",
lifecycleRuntimeFlag: "0"
});
expect(Array.isArray(result.problem_units)).toBe(true);
const first = result.problem_units?.[0];
expect(first?.lifecycle_domain).toBeUndefined();
expect(first?.lifecycle_defect_type).toBeUndefined();
expect(result.summary.lifecycle_enriched_units).toBe(0);
});
it("adds lifecycle fields to problem units when lifecycle runtime flag is ON", async () => {
const result = await normalizeWithFlags({
problemUnitsFlag: "1",
lifecycleRuntimeFlag: "1"
});
expect(Array.isArray(result.problem_units)).toBe(true);
expect(result.problem_units?.length).toBeGreaterThan(0);
const first = result.problem_units?.[0];
expect(first?.lifecycle_domain).toBeTruthy();
expect(first?.current_lifecycle_state).toBeTruthy();
expect(first?.expected_lifecycle_state).toBeTruthy();
expect(first?.lifecycle_defect_type).toBeTruthy();
expect(typeof first?.business_lifecycle_interpretation).toBe("string");
expect(result.problem_unit_summary?.lifecycle_enriched_units).toBeGreaterThan(0);
expect(result.summary.lifecycle_enriched_units).toBeGreaterThan(0);
expect(result.summary.problem_unit_lifecycle_domain_distribution).toBeTruthy();
expect(result.summary.problem_unit_lifecycle_defect_distribution).toBeTruthy();
});
});