Stage 3: улучшена логика жизненного цикла и очищены ответы ассистента
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CandidateEvidenceItem, ProblemUnit } from "../src/types/stage2ProblemUnits";
|
||||
import { LifecycleRegistry, resolveLifecycle } from "../src/services/lifecycleRuntime";
|
||||
|
||||
function buildProblemUnit(input: {
|
||||
id: string;
|
||||
type?: ProblemUnit["problem_unit_type"];
|
||||
mechanismSummary?: string;
|
||||
businessDefectClass?: string;
|
||||
accounts?: string[];
|
||||
actualState?: string;
|
||||
expectedState?: string;
|
||||
periodCloseRisk?: boolean;
|
||||
}): ProblemUnit {
|
||||
return {
|
||||
schema_version: "problem_unit_v0_1",
|
||||
problem_unit_id: input.id,
|
||||
problem_unit_type: input.type ?? "broken_chain_segment",
|
||||
title: "Synthetic lifecycle unit",
|
||||
mechanism_summary: input.mechanismSummary ?? "Synthetic lifecycle mechanism",
|
||||
business_defect_class: input.businessDefectClass ?? "broken_lifecycle",
|
||||
severity: {
|
||||
score: 0.64,
|
||||
grade: "medium"
|
||||
},
|
||||
confidence: {
|
||||
score: 0.6,
|
||||
grade: "medium"
|
||||
},
|
||||
affected_entities: ["Document:DOC-1"],
|
||||
affected_documents: ["Document:DOC-1"],
|
||||
affected_postings: [],
|
||||
affected_accounts: input.accounts ?? ["51"],
|
||||
affected_counterparties: [],
|
||||
affected_contracts: [],
|
||||
...(input.actualState
|
||||
? {
|
||||
actual_state: input.actualState
|
||||
}
|
||||
: {}),
|
||||
...(input.expectedState
|
||||
? {
|
||||
expected_state: input.expectedState
|
||||
}
|
||||
: {}),
|
||||
...(input.periodCloseRisk
|
||||
? {
|
||||
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("stage3 lifecycle registry and resolver wave2", () => {
|
||||
it("exposes all lifecycle domains in the registry", () => {
|
||||
const domains = LifecycleRegistry.listDomains();
|
||||
expect(domains).toEqual([
|
||||
"bank_settlement",
|
||||
"customer_settlement",
|
||||
"deferred_expense",
|
||||
"fixed_asset",
|
||||
"vat_flow",
|
||||
"period_close"
|
||||
]);
|
||||
|
||||
for (const domain of domains) {
|
||||
const model = LifecycleRegistry.getDomain(domain);
|
||||
expect(model.lifecycle_domain).toBe(domain);
|
||||
expect(model.states.length).toBeGreaterThan(0);
|
||||
expect(model.defects.some((definition) => definition.defect_code === "stale_active_state")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("infers lifecycle domain for all covered stage3 domains", () => {
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
unit: ProblemUnit;
|
||||
candidates: CandidateEvidenceItem[];
|
||||
expectedDomain: string;
|
||||
}> = [
|
||||
{
|
||||
name: "bank settlement",
|
||||
unit: buildProblemUnit({ id: "domain-bank", accounts: ["51"], mechanismSummary: "bank settlement reconciliation" }),
|
||||
candidates: [buildCandidate({ id: "cand-bank", relations: ["payment_to_settlement"] })],
|
||||
expectedDomain: "bank_settlement"
|
||||
},
|
||||
{
|
||||
name: "customer settlement",
|
||||
unit: buildProblemUnit({ id: "domain-customer", accounts: ["62"], mechanismSummary: "customer receivable chain" }),
|
||||
candidates: [buildCandidate({ id: "cand-customer", relations: ["settlement_to_invoice"] })],
|
||||
expectedDomain: "customer_settlement"
|
||||
},
|
||||
{
|
||||
name: "deferred expense",
|
||||
unit: buildProblemUnit({ id: "domain-97", accounts: ["97"], mechanismSummary: "deferred writeoff path" }),
|
||||
candidates: [buildCandidate({ id: "cand-97", relations: ["deferred_writeoff"] })],
|
||||
expectedDomain: "deferred_expense"
|
||||
},
|
||||
{
|
||||
name: "fixed asset",
|
||||
unit: buildProblemUnit({ id: "domain-os", accounts: ["01"], mechanismSummary: "fixed asset depreciation" }),
|
||||
candidates: [buildCandidate({ id: "cand-os", relations: ["depreciation_register_movement"] })],
|
||||
expectedDomain: "fixed_asset"
|
||||
},
|
||||
{
|
||||
name: "vat flow",
|
||||
unit: buildProblemUnit({ id: "domain-vat", accounts: ["68"], mechanismSummary: "vat deduction chain" }),
|
||||
candidates: [buildCandidate({ id: "cand-vat", anomalies: ["cross_branch_inconsistency"] })],
|
||||
expectedDomain: "vat_flow"
|
||||
},
|
||||
{
|
||||
name: "period close",
|
||||
unit: buildProblemUnit({
|
||||
id: "domain-close",
|
||||
type: "period_risk_cluster",
|
||||
mechanismSummary: "period close blocker",
|
||||
periodCloseRisk: true
|
||||
}),
|
||||
candidates: [buildCandidate({ id: "cand-close", anomalies: ["period_close_risk"] })],
|
||||
expectedDomain: "period_close"
|
||||
}
|
||||
];
|
||||
|
||||
for (const item of cases) {
|
||||
const resolution = resolveLifecycle({
|
||||
unit: item.unit,
|
||||
candidates: item.candidates
|
||||
});
|
||||
expect(resolution.lifecycle_domain, item.name).toBe(item.expectedDomain);
|
||||
}
|
||||
});
|
||||
|
||||
it("normalizes unknown explicit states against registry and records limitations", () => {
|
||||
const resolution = resolveLifecycle({
|
||||
unit: buildProblemUnit({
|
||||
id: "normalize-invalid-states",
|
||||
accounts: ["01"],
|
||||
mechanismSummary: "fixed asset depreciation",
|
||||
actualState: "legacy_state_unmapped",
|
||||
expectedState: "legacy_target_unmapped"
|
||||
}),
|
||||
candidates: [buildCandidate({ id: "cand-normalize", relations: ["depreciation_register_movement"] })]
|
||||
});
|
||||
|
||||
expect(resolution.lifecycle_domain).toBe("fixed_asset");
|
||||
expect(resolution.resolved_current_state).toBe("depreciation_active");
|
||||
expect(resolution.resolved_expected_state).toBe("disposed");
|
||||
expect(resolution.snapshot_limitations).toContain("actual_state_not_in_registry_normalized");
|
||||
expect(resolution.snapshot_limitations).toContain("expected_state_not_in_registry_normalized");
|
||||
});
|
||||
|
||||
it("infers missing transition from registry transition path", () => {
|
||||
const resolution = resolveLifecycle({
|
||||
unit: buildProblemUnit({
|
||||
id: "missing-transition",
|
||||
accounts: ["51"],
|
||||
actualState: "bank_recorded",
|
||||
expectedState: "settlement_closed"
|
||||
}),
|
||||
candidates: [buildCandidate({ id: "cand-missing", anomalies: ["missing_link", "no_continuation"] })]
|
||||
});
|
||||
|
||||
expect(resolution.missing_transitions[0]).toBe("bank_recorded->settlement_closed");
|
||||
});
|
||||
|
||||
it("builds previous state chain from registry model", () => {
|
||||
const resolution = resolveLifecycle({
|
||||
unit: buildProblemUnit({
|
||||
id: "previous-chain",
|
||||
accounts: ["51"],
|
||||
actualState: "bank_recorded",
|
||||
expectedState: "settlement_closed"
|
||||
}),
|
||||
candidates: [buildCandidate({ id: "cand-prev", relations: ["payment_to_settlement"] })]
|
||||
});
|
||||
|
||||
expect(resolution.resolved_previous_states).toEqual(["initiated_payment"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user