外包风险评估系统:领域引擎+前端+服务端持久化与生产部署

- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解
- 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护)
- 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理
- 专业图标体系替换全部emoji、看板美化
- 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC
- 444单测+204前端测试+51 e2e
This commit is contained in:
freedakgmail
2026-06-13 01:06:39 +08:00
commit c670b9e454
404 changed files with 61820 additions and 0 deletions
@@ -0,0 +1,300 @@
/**
* Property 36: 报告章节完备 的属性化测试(Report_GeneratorReq 10.1)。
*
* 属性陈述:对任意风险评分、红线校验、费用测算与可接受性结论均已完成的评估,
* `generate` 产出的报告*必*包含全部规定章节(项目概要与业务类型判定、风险总分与分级、
* 风险热力图、各维度风险明细、Top 关键风险与红线校验结果、可接受性结论、应对方案、
* 假设与信息缺口说明);且未命中任一红线时,红线校验结果*必*明确标注为"无红线命中"。
*
* 本测试在生成器中构造"完整 Assessment"(评分/红线/费用/可接受性结论均已产生),
* 覆盖启用/停用维度与指标、混合数据来源(含"智能体假设")、有/无红线命中等形态,
* 对 `generate` 施加上述章节完备性与"无红线命中"标注约束。
*
* Feature: outsourcing-risk-assessment, Property 36: 报告章节完备
* Validates: Requirements 10.1
*/
import { describe, expect, it } from 'vitest';
import fc from 'fast-check';
import {
ACCEPTABILITY_VALUES,
REDLINE_STATUS_VALUES,
type Acceptability,
type Assessment,
type RedlineResult,
type RedlineStatus,
type ScoringItem,
} from '../../domain/assessment.js';
import {
DATA_PROVENANCE_VALUES,
RISK_GRADE_VALUES,
RISK_LEVEL_VALUES,
type DataProvenance,
type RiskGrade,
type RiskLevel,
} from '../../domain/common.js';
import type { CostEstimate } from '../../domain/cost.js';
import type { Dimension, Indicator, Redline, RiskModel, ScoringRule } from '../../domain/model.js';
import { REGION_CN } from '../../domain/region.js';
import { generate, NO_REDLINE_HIT_LABEL } from '../report.js';
// ----------------------------------------------------------------------------
// 生成器:构造"评分/红线/费用/可接受性结论均已完成"的 Assessment
// ----------------------------------------------------------------------------
/** 合法 Indicator 须覆盖 Risk_Level 1-5Req 11.3);报告不读取规则文本。 */
const scoringRules: ScoringRule[] = (RISK_LEVEL_VALUES as readonly RiskLevel[]).map(
(level) => ({ level, label: `L${level}`, description: `规则${level}` }),
);
const riskLevelArb = fc.constantFrom<RiskLevel>(...(RISK_LEVEL_VALUES as readonly RiskLevel[]));
const provenanceArb = fc.constantFrom<DataProvenance>(
...(DATA_PROVENANCE_VALUES as readonly DataProvenance[]),
);
const riskGradeArb = fc.constantFrom<RiskGrade>(...(RISK_GRADE_VALUES as readonly RiskGrade[]));
const acceptabilityArb = fc.constantFrom<Acceptability>(
...(ACCEPTABILITY_VALUES as readonly Acceptability[]),
);
const redlineStatusArb = fc.constantFrom<RedlineStatus>(
...(REDLINE_STATUS_VALUES as readonly RedlineStatus[]),
);
/** 置信度:[0,1] 两位小数(Req 4.6, 18.4)。 */
const confidenceArb = fc.integer({ min: 0, max: 100 }).map((n) => n / 100);
const weightArb = fc.double({ min: 0, max: 100, noNaN: true });
const moneyArb = fc.double({ min: 0, max: 1_000_000, noNaN: true });
interface RawIndicator {
enabled: boolean;
weight: number;
riskLevel: RiskLevel;
provenance: DataProvenance;
confidence: number;
}
interface RawDimension {
enabled: boolean;
weight: number;
indicators: RawIndicator[];
}
interface RawRedline {
enabled: boolean;
status: RedlineStatus;
}
const rawIndicatorArb: fc.Arbitrary<RawIndicator> = fc.record({
enabled: fc.boolean(),
weight: weightArb,
riskLevel: riskLevelArb,
provenance: provenanceArb,
confidence: confidenceArb,
});
const rawDimensionArb: fc.Arbitrary<RawDimension> = fc.record({
enabled: fc.boolean(),
weight: weightArb,
indicators: fc.array(rawIndicatorArb, { minLength: 0, maxLength: 4 }),
});
const rawRedlineArb: fc.Arbitrary<RawRedline> = fc.record({
enabled: fc.boolean(),
status: redlineStatusArb,
});
const costEstimateArb: fc.Arbitrary<CostEstimate> = fc
.record({
baseline: moneyArb,
premiumLower: fc.double({ min: 0, max: 50, noNaN: true }),
premiumDelta: fc.double({ min: 0, max: 50, noNaN: true }),
})
.map(({ baseline, premiumLower, premiumDelta }) => ({
riskPremiumRange: { lower: premiumLower, upper: premiumLower + premiumDelta, unit: '百分比' },
advanceInterest: 0,
insuranceCost: 0,
compensationReserve: 0,
badDebtReserve: 0,
baselineQuote: baseline,
riskAdjustedQuote: baseline,
breakdown: [],
}));
interface AssessmentSpec {
dimensions: RawDimension[];
redlines: RawRedline[];
riskScore: number;
riskGrade: RiskGrade;
acceptability: Acceptability;
costEstimate: CostEstimate;
/** 强制构造"无红线命中"情形,确保该分支被充分覆盖。 */
forceNoHit: boolean;
}
const assessmentSpecArb: fc.Arbitrary<AssessmentSpec> = fc.record({
dimensions: fc.array(rawDimensionArb, { minLength: 0, maxLength: 4 }),
redlines: fc.array(rawRedlineArb, { minLength: 0, maxLength: 5 }),
riskScore: fc.integer({ min: 0, max: 100 }),
riskGrade: riskGradeArb,
acceptability: acceptabilityArb,
costEstimate: costEstimateArb,
forceNoHit: fc.boolean(),
});
/**
* 由随机规格构造一个"已完成"的 Assessment
* - 风险模型保留全部随机形态(启用/停用维度与指标、空指标维度、权重为 0 等);
* - 每个 Indicator 均有对应 ScoringItem(含来源/置信/三要素);
* - 每个**启用**红线均有对应的红线校验结果(满足红线校验完成条件);
* - riskScore/riskGrade/costEstimate/acceptability 均已产生。
*/
function buildAssessment(spec: AssessmentSpec): Assessment {
const dimensions: Dimension[] = spec.dimensions.map((d, i) => ({
id: `d${i}`,
name: `维度${i}`,
weight: d.weight,
enabled: d.enabled,
indicators: d.indicators.map(
(ind, j): Indicator => ({
id: `i${j}`,
name: `指标${i}-${j}`,
weight: ind.weight,
enabled: ind.enabled,
scoringRules,
evidenceRequired: '',
askPrompt: '',
}),
),
}));
const redlines: Redline[] = spec.redlines.map((r, k) => ({
id: `r${k}`,
triggerCondition: `触发条件${k}`,
consequence: `后果${k}`,
enabled: r.enabled,
}));
const riskModel: RiskModel = {
id: 'm1',
name: '风险模型',
businessType: '岗位外包',
dimensions,
redlines,
};
// 为每个指标构造评分项(含停用项;报告以对象引用映射,停用项不进入热力图/TopN)。
const scoringItems: ScoringItem[] = spec.dimensions.flatMap((d, i) =>
d.indicators.map((ind, j): ScoringItem => ({
dimensionId: `d${i}`,
indicatorId: `i${j}`,
riskLevel: ind.riskLevel,
score: ind.riskLevel * ind.weight,
provenance: ind.provenance,
confidence: ind.confidence,
rationale: `判定依据${i}-${j}`,
riskImpact: `风险影响${i}-${j}`,
recommendation: `建议${i}-${j}`,
})),
);
// 每个启用红线产生一个红线校验结果(满足红线校验完成条件)。
const redlineResults: RedlineResult[] = spec.redlines
.map((r, k): { enabled: boolean; result: RedlineResult } => {
const status: RedlineStatus =
spec.forceNoHit && r.status === '命中' ? '未命中' : r.status;
const base: RedlineResult = { redlineId: `r${k}`, status };
const result: RedlineResult =
status === '命中'
? { ...base, triggeredCondition: `触发条件${k}`, evidenceData: `判定依据数据${k}` }
: base;
return { enabled: r.enabled, result };
})
.filter((entry) => entry.enabled)
.map((entry) => entry.result);
return {
id: 'a1',
projectDescription: '某外包项目风险评估',
businessType: '岗位外包',
industry: '制造业',
region: REGION_CN,
riskModel,
scoringItems,
riskScore: spec.riskScore,
riskGrade: spec.riskGrade,
redlineResults,
costEstimate: spec.costEstimate,
acceptability: spec.acceptability,
metadata: {
businessType: '岗位外包',
industry: '制造业',
region: REGION_CN,
riskScore: spec.riskScore,
riskGrade: spec.riskGrade,
createdAt: '2024-01-01T00:00:00.000Z',
assessorId: 'assessor-1',
},
createdAt: '2024-01-01T00:00:00.000Z',
assessorId: 'assessor-1',
};
}
// ----------------------------------------------------------------------------
// 属性测试
// ----------------------------------------------------------------------------
describe('Property 36: 报告章节完备 (Req 10.1)', () => {
it('对任意已完成评估,报告含全部规定章节;未命中任一红线时明确标注"无红线命中"', () => {
fc.assert(
fc.property(assessmentSpecArb, (spec) => {
const assessment = buildAssessment(spec);
const report = generate(assessment);
// 1) 全部八个规定章节均存在,且各章节标题非空。
expect(report.projectOverview).toBeDefined();
expect(report.projectOverview.title.length).toBeGreaterThan(0);
expect(report.riskScoreGrade).toBeDefined();
expect(report.riskScoreGrade.title.length).toBeGreaterThan(0);
expect(report.heatmap).toBeDefined();
expect(report.heatmap.title.length).toBeGreaterThan(0);
expect(report.dimensionDetails).toBeDefined();
expect(report.dimensionDetails.title.length).toBeGreaterThan(0);
expect(report.keyRisksAndRedlines).toBeDefined();
expect(report.keyRisksAndRedlines.title.length).toBeGreaterThan(0);
expect(report.acceptabilityConclusion).toBeDefined();
expect(report.acceptabilityConclusion.title.length).toBeGreaterThan(0);
expect(report.responsePlan).toBeDefined();
expect(report.responsePlan.title.length).toBeGreaterThan(0);
expect(report.assumptionsAndGaps).toBeDefined();
expect(report.assumptionsAndGaps.title.length).toBeGreaterThan(0);
// 2) 章节内容与已完成评估一致的基本结构。
expect(report.riskScoreGrade.riskScore).toBe(assessment.riskScore);
expect(report.riskScoreGrade.riskGrade).toBe(assessment.riskGrade);
expect(report.acceptabilityConclusion.acceptability).toBe(assessment.acceptability);
expect(Array.isArray(report.heatmap.cells)).toBe(true);
expect(report.dimensionDetails.dimensions.length).toBe(
assessment.riskModel.dimensions.length,
);
expect(report.keyRisksAndRedlines.redlineResults.length).toBe(
assessment.redlineResults.length,
);
expect(Array.isArray(report.assumptionsAndGaps.items)).toBe(true);
// 3) 红线命中与"无红线命中"标注。
const anyHit = assessment.redlineResults.some((r) => r.status === '命中');
if (anyHit) {
expect(report.keyRisksAndRedlines.hasRedlineHit).toBe(true);
expect(report.keyRisksAndRedlines.hitRedlines.length).toBe(
assessment.redlineResults.filter((r) => r.status === '命中').length,
);
expect(report.keyRisksAndRedlines.redlineSummary).not.toBe(NO_REDLINE_HIT_LABEL);
} else {
expect(report.keyRisksAndRedlines.hasRedlineHit).toBe(false);
expect(report.keyRisksAndRedlines.hitRedlines.length).toBe(0);
expect(report.keyRisksAndRedlines.redlineSummary).toBe(NO_REDLINE_HIT_LABEL);
}
}),
{ numRuns: 100 },
);
});
});