@luckychen1993/pm-mcp 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +304 -0
- package/dist/acceptance-evaluator.js +330 -0
- package/dist/acceptance-model.js +440 -0
- package/dist/acceptance-report.js +335 -0
- package/dist/acceptance-tools.js +318 -0
- package/dist/audit-tools.js +61 -0
- package/dist/audit.js +364 -0
- package/dist/budget.js +71 -0
- package/dist/dashboard.js +241 -0
- package/dist/governance-audit.js +126 -0
- package/dist/governance-model.js +306 -0
- package/dist/governance-tools.js +240 -0
- package/dist/health.js +58 -0
- package/dist/index-db.js +97 -0
- package/dist/index-store.js +381 -0
- package/dist/index.js +210 -0
- package/dist/init.js +40 -0
- package/dist/knowledge-tools.js +204 -0
- package/dist/language-adapters.js +450 -0
- package/dist/language-dependencies.js +351 -0
- package/dist/license.js +261 -0
- package/dist/paths.js +51 -0
- package/dist/polyglot-ast.js +329 -0
- package/dist/portfolio.js +349 -0
- package/dist/project-fingerprint.js +83 -0
- package/dist/project-tools.js +217 -0
- package/dist/quality-store.js +77 -0
- package/dist/registry.js +38 -0
- package/dist/roadmap.js +96 -0
- package/dist/scan.js +271 -0
- package/dist/search.js +312 -0
- package/dist/security.js +269 -0
- package/dist/semantic-evidence-store.js +133 -0
- package/dist/semantic-evidence.js +100 -0
- package/dist/semantic-graph-algorithms.js +86 -0
- package/dist/semantic-graph.js +390 -0
- package/dist/semantic-parsers.js +311 -0
- package/dist/store.js +185 -0
- package/dist/task-tools.js +198 -0
- package/dist/tool-base.js +32 -0
- package/dist/tools.js +21 -0
- package/dist/types.js +208 -0
- package/dist/typescript-semantic.js +282 -0
- package/package.json +70 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ACCEPTANCE_SCHEMA_VERSION, AcceptanceBaselineSchema, AcceptanceEvaluationSchema, ISO_25010_CHARACTERISTICS, ISO_25040_STAGES, RISK_LEVELS, } from "./acceptance-model.js";
|
|
3
|
+
import { ACCEPTANCE_STANDARD_BASIS, AcceptanceReportSchema, } from "./acceptance-report.js";
|
|
4
|
+
function compareText(left, right) {
|
|
5
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
6
|
+
}
|
|
7
|
+
function sortedUnique(values) {
|
|
8
|
+
return [...new Set(values)].sort(compareText);
|
|
9
|
+
}
|
|
10
|
+
function canonicalize(value) {
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.map(canonicalize);
|
|
13
|
+
if (value !== null && typeof value === "object") {
|
|
14
|
+
const record = value;
|
|
15
|
+
return Object.fromEntries(Object.keys(record).sort(compareText).map((key) => [key, canonicalize(record[key])]));
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function acceptanceBaselineFingerprint(input) {
|
|
20
|
+
const baseline = AcceptanceBaselineSchema.parse(input);
|
|
21
|
+
return createHash("sha256").update(JSON.stringify(canonicalize(baseline)), "utf8").digest("hex");
|
|
22
|
+
}
|
|
23
|
+
function metricPassed(observed, metric) {
|
|
24
|
+
if (metric.direction === "at_least")
|
|
25
|
+
return observed >= metric.threshold;
|
|
26
|
+
if (metric.direction === "at_most")
|
|
27
|
+
return observed <= metric.threshold;
|
|
28
|
+
return Math.abs(observed - metric.threshold) <= metric.tolerance;
|
|
29
|
+
}
|
|
30
|
+
export function evaluateAcceptance(baselineInput, evaluationInput) {
|
|
31
|
+
// Parsing is deliberately first: malformed acceptance data cannot be turned
|
|
32
|
+
// into a plausible-looking report.
|
|
33
|
+
const baseline = AcceptanceBaselineSchema.parse(baselineInput);
|
|
34
|
+
const evaluation = AcceptanceEvaluationSchema.parse(evaluationInput);
|
|
35
|
+
const findings = [];
|
|
36
|
+
const addError = (code, subject, message) => {
|
|
37
|
+
findings.push({ code, severity: "error", subject, message });
|
|
38
|
+
};
|
|
39
|
+
const addWarning = (code, subject, message) => {
|
|
40
|
+
findings.push({ code, severity: "warning", subject, message });
|
|
41
|
+
};
|
|
42
|
+
if (baseline.approval.status !== "approved") {
|
|
43
|
+
addError("BASELINE_NOT_APPROVED", `${baseline.baseline_id}@${baseline.baseline_version}`, `质量基线状态为 ${baseline.approval.status};只有预先批准的基线可以通过验收。`);
|
|
44
|
+
}
|
|
45
|
+
else if (baseline.approval.approved_at) {
|
|
46
|
+
const executeStartedAt = evaluation.stage_results.find((item) => item.stage === "execute")?.started_at;
|
|
47
|
+
if (executeStartedAt === null || executeStartedAt === undefined) {
|
|
48
|
+
addError("EXECUTE_START_TIME_MISSING", "execute", "execute 阶段缺少开始时间,无法证明基线在执行前获批。");
|
|
49
|
+
}
|
|
50
|
+
else if (Date.parse(baseline.approval.approved_at) > Date.parse(executeStartedAt)) {
|
|
51
|
+
addError("BASELINE_APPROVED_AFTER_EXECUTION_STARTED", baseline.baseline_id, "基线批准时间晚于 execute 阶段开始时间,不能作为预先批准的验收条件。");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (evaluation.baseline_id !== baseline.baseline_id) {
|
|
55
|
+
addError("BASELINE_ID_MISMATCH", evaluation.baseline_id, `评价引用 ${evaluation.baseline_id},实际基线为 ${baseline.baseline_id}。`);
|
|
56
|
+
}
|
|
57
|
+
if (evaluation.baseline_version !== baseline.baseline_version) {
|
|
58
|
+
addError("BASELINE_VERSION_MISMATCH", evaluation.baseline_version, `评价引用 ${evaluation.baseline_version},实际基线版本为 ${baseline.baseline_version}。`);
|
|
59
|
+
}
|
|
60
|
+
if (baseline.acceptance_policy.require_independent_evaluator && !evaluation.evaluator.independent) {
|
|
61
|
+
addError("EVALUATOR_NOT_INDEPENDENT", evaluation.evaluator.name, "基线要求独立评价,但评价人被标记为非独立。");
|
|
62
|
+
}
|
|
63
|
+
const evidenceById = new Map(evaluation.evidence.map((item) => [item.id, item]));
|
|
64
|
+
const referencedEvidence = new Set();
|
|
65
|
+
const validEvidence = (ids, subject) => {
|
|
66
|
+
let ok = true;
|
|
67
|
+
for (const id of ids) {
|
|
68
|
+
referencedEvidence.add(id);
|
|
69
|
+
const evidence = evidenceById.get(id);
|
|
70
|
+
if (!evidence) {
|
|
71
|
+
addError("EVIDENCE_NOT_FOUND", subject, `引用的证据 "${id}" 不存在。`);
|
|
72
|
+
ok = false;
|
|
73
|
+
}
|
|
74
|
+
else if (Date.parse(evidence.captured_at) > Date.parse(evaluation.report_generated_at)) {
|
|
75
|
+
ok = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return ok;
|
|
79
|
+
};
|
|
80
|
+
for (const evidence of evaluation.evidence) {
|
|
81
|
+
if (Date.parse(evidence.captured_at) > Date.parse(evaluation.report_generated_at)) {
|
|
82
|
+
addError("EVIDENCE_FROM_FUTURE", evidence.id, "证据采集时间晚于报告生成时间。");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const stageByName = new Map(evaluation.stage_results.map((item) => [item.stage, item]));
|
|
86
|
+
const stageResults = ISO_25040_STAGES.map((stage) => {
|
|
87
|
+
const result = stageByName.get(stage);
|
|
88
|
+
const evidenceOk = validEvidence(result.artifact_evidence_ids, `stage:${stage}`);
|
|
89
|
+
const timeOk = result.completed_at === null || Date.parse(result.completed_at) <= Date.parse(evaluation.evaluated_at);
|
|
90
|
+
const passed = result.status === "completed" && evidenceOk && timeOk;
|
|
91
|
+
if (result.status !== "completed") {
|
|
92
|
+
addError("EVALUATION_STAGE_INCOMPLETE", stage, `ISO/IEC 25040 阶段 ${stage} 状态为 ${result.status}。`);
|
|
93
|
+
}
|
|
94
|
+
if (result.completed_at && Date.parse(result.completed_at) > Date.parse(evaluation.evaluated_at)) {
|
|
95
|
+
addError("STAGE_COMPLETED_AFTER_EVALUATION", stage, "阶段完成时间晚于评价时间。");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
stage,
|
|
99
|
+
status: result.status,
|
|
100
|
+
passed,
|
|
101
|
+
evidence_ids: sortedUnique(result.artifact_evidence_ids),
|
|
102
|
+
result_note: result.result_note,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
const testDefinitionById = new Map(baseline.tests.map((item) => [item.id, item]));
|
|
106
|
+
const testResultById = new Map(evaluation.test_results.map((item) => [item.test_id, item]));
|
|
107
|
+
for (const result of evaluation.test_results) {
|
|
108
|
+
if (!testDefinitionById.has(result.test_id))
|
|
109
|
+
addError("UNKNOWN_TEST_RESULT", result.test_id, "评价结果引用了基线未定义的测试。");
|
|
110
|
+
if (Date.parse(result.executed_at) > Date.parse(evaluation.evaluated_at))
|
|
111
|
+
addError("TEST_EXECUTED_AFTER_EVALUATION", result.test_id, "测试执行时间晚于评价时间。");
|
|
112
|
+
}
|
|
113
|
+
const testResults = [...baseline.tests].sort((a, b) => compareText(a.id, b.id)).map((definition) => {
|
|
114
|
+
const result = testResultById.get(definition.id);
|
|
115
|
+
if (!result) {
|
|
116
|
+
addError("TEST_RESULT_MISSING", definition.id, "缺少基线要求的测试结果。");
|
|
117
|
+
return { id: definition.id, status: "missing", verification_mode: definition.verification_mode, assertion: definition.assertion, passed: false, evidence_ids: [] };
|
|
118
|
+
}
|
|
119
|
+
const evidenceOk = validEvidence(result.evidence_ids, `test:${definition.id}`);
|
|
120
|
+
const matchingKind = result.evidence_ids.some((id) => evidenceById.get(id)?.kind === definition.expected_evidence_kind);
|
|
121
|
+
const assertionEvidenceOk = definition.verification_mode === "manual"
|
|
122
|
+
|| Boolean(definition.assertion && result.evidence_ids.includes(definition.assertion.evidence_id));
|
|
123
|
+
const timeOk = Date.parse(result.executed_at) <= Date.parse(evaluation.evaluated_at);
|
|
124
|
+
if (!matchingKind) {
|
|
125
|
+
addError("TEST_EVIDENCE_KIND_MISMATCH", definition.id, `测试至少需要一项 ${definition.expected_evidence_kind} 类型证据。`);
|
|
126
|
+
}
|
|
127
|
+
if (!assertionEvidenceOk) {
|
|
128
|
+
addError("TEST_ASSERTION_EVIDENCE_MISSING", definition.id, `测试结果未引用基线冻结的断言证据 ${definition.assertion?.evidence_id}。`);
|
|
129
|
+
}
|
|
130
|
+
if (result.status !== "passed")
|
|
131
|
+
addError("TEST_NOT_PASSED", definition.id, `测试状态为 ${result.status}。`);
|
|
132
|
+
return {
|
|
133
|
+
id: definition.id,
|
|
134
|
+
status: result.status,
|
|
135
|
+
verification_mode: definition.verification_mode,
|
|
136
|
+
assertion: definition.assertion,
|
|
137
|
+
passed: result.status === "passed" && evidenceOk && matchingKind && assertionEvidenceOk && timeOk,
|
|
138
|
+
evidence_ids: sortedUnique(result.evidence_ids),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
const evaluatedTestById = new Map(testResults.map((item) => [item.id, item]));
|
|
142
|
+
const measurementByRequirement = new Map(evaluation.measurements.map((item) => [item.requirement_id, item]));
|
|
143
|
+
const requirementDefinitionById = new Map(baseline.requirements.map((item) => [item.id, item]));
|
|
144
|
+
for (const measurement of evaluation.measurements) {
|
|
145
|
+
if (!requirementDefinitionById.has(measurement.requirement_id))
|
|
146
|
+
addError("UNKNOWN_REQUIREMENT_MEASUREMENT", measurement.requirement_id, "评价测量引用了基线未定义的需求。");
|
|
147
|
+
if (Date.parse(measurement.measured_at) > Date.parse(evaluation.evaluated_at))
|
|
148
|
+
addError("MEASUREMENT_AFTER_EVALUATION", measurement.requirement_id, "测量时间晚于评价时间。");
|
|
149
|
+
}
|
|
150
|
+
const requirementResults = [...baseline.requirements].sort((a, b) => compareText(a.id, b.id)).map((requirement) => {
|
|
151
|
+
const measurement = measurementByRequirement.get(requirement.id);
|
|
152
|
+
if (!measurement) {
|
|
153
|
+
addError("REQUIREMENT_MEASUREMENT_MISSING", requirement.id, "缺少量化需求实测值和证据。");
|
|
154
|
+
}
|
|
155
|
+
const evidenceOk = measurement ? validEvidence(measurement.evidence_ids, `requirement:${requirement.id}`) : false;
|
|
156
|
+
const sourceEvidenceOk = Boolean(measurement?.evidence_ids.includes(requirement.measurement_source.evidence_id));
|
|
157
|
+
if (measurement && !sourceEvidenceOk) {
|
|
158
|
+
addError("MEASUREMENT_SOURCE_EVIDENCE_MISSING", requirement.id, `测量结果未引用基线冻结的数据源证据 ${requirement.measurement_source.evidence_id}。`);
|
|
159
|
+
}
|
|
160
|
+
const thresholdOk = measurement ? metricPassed(measurement.observed_value, requirement.metric) : false;
|
|
161
|
+
const timeOk = measurement ? Date.parse(measurement.measured_at) <= Date.parse(evaluation.evaluated_at) : false;
|
|
162
|
+
if (measurement && !thresholdOk) {
|
|
163
|
+
addError("QUALITY_THRESHOLD_NOT_MET", requirement.id, `实测值 ${measurement.observed_value} 未达到 ${requirement.metric.direction} ${requirement.metric.threshold} ${requirement.metric.unit}。`);
|
|
164
|
+
}
|
|
165
|
+
const linkedTestsOk = requirement.test_ids.every((id) => evaluatedTestById.get(id)?.passed === true);
|
|
166
|
+
if (!linkedTestsOk)
|
|
167
|
+
addError("REQUIREMENT_TESTS_NOT_PASSED", requirement.id, "一个或多个关联验收测试未通过。");
|
|
168
|
+
return {
|
|
169
|
+
id: requirement.id,
|
|
170
|
+
characteristic: requirement.characteristic,
|
|
171
|
+
metric_name: requirement.metric.name,
|
|
172
|
+
unit: requirement.metric.unit,
|
|
173
|
+
direction: requirement.metric.direction,
|
|
174
|
+
threshold: requirement.metric.threshold,
|
|
175
|
+
tolerance: requirement.metric.tolerance,
|
|
176
|
+
observed_value: measurement?.observed_value ?? null,
|
|
177
|
+
measurement_source: requirement.measurement_source,
|
|
178
|
+
passed: Boolean(measurement && evidenceOk && sourceEvidenceOk && thresholdOk && linkedTestsOk && timeOk),
|
|
179
|
+
risk_ids: sortedUnique(requirement.risk_ids),
|
|
180
|
+
test_ids: sortedUnique(requirement.test_ids),
|
|
181
|
+
evidence_ids: sortedUnique(measurement?.evidence_ids ?? []),
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
const evaluatedRequirementById = new Map(requirementResults.map((item) => [item.id, item]));
|
|
185
|
+
const riskDefinitionById = new Map(baseline.risks.map((item) => [item.id, item]));
|
|
186
|
+
const residualById = new Map(evaluation.residual_risks.map((item) => [item.risk_id, item]));
|
|
187
|
+
for (const residual of evaluation.residual_risks) {
|
|
188
|
+
if (!riskDefinitionById.has(residual.risk_id))
|
|
189
|
+
addError("UNKNOWN_RESIDUAL_RISK", residual.risk_id, "评价结果引用了基线未定义的风险。");
|
|
190
|
+
}
|
|
191
|
+
const maximumRisk = RISK_LEVELS.indexOf(baseline.acceptance_policy.maximum_residual_risk_level);
|
|
192
|
+
const riskResults = [...baseline.risks].sort((a, b) => compareText(a.id, b.id)).map((risk) => {
|
|
193
|
+
const residual = residualById.get(risk.id);
|
|
194
|
+
if (!residual) {
|
|
195
|
+
addError("RESIDUAL_RISK_MISSING", risk.id, "缺少残余风险评价。");
|
|
196
|
+
return {
|
|
197
|
+
id: risk.id,
|
|
198
|
+
owner: risk.owner,
|
|
199
|
+
likelihood: risk.likelihood,
|
|
200
|
+
impact: risk.impact,
|
|
201
|
+
inherent_level: risk.inherent_level,
|
|
202
|
+
compensating_controls: risk.compensating_controls,
|
|
203
|
+
residual_level: null,
|
|
204
|
+
disposition: "missing",
|
|
205
|
+
accepted_by: null,
|
|
206
|
+
evidence_ids: [],
|
|
207
|
+
controlled: false,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const evidenceOk = validEvidence(residual.evidence_ids, `risk:${risk.id}`);
|
|
211
|
+
const levelOk = RISK_LEVELS.indexOf(residual.residual_level) <= maximumRisk;
|
|
212
|
+
if (!levelOk)
|
|
213
|
+
addError("RESIDUAL_RISK_ABOVE_LIMIT", risk.id, `残余风险 ${residual.residual_level} 超过基线允许上限 ${baseline.acceptance_policy.maximum_residual_risk_level}。`);
|
|
214
|
+
if (residual.disposition === "open")
|
|
215
|
+
addError("RESIDUAL_RISK_OPEN", risk.id, "残余风险仍处于 open 状态。");
|
|
216
|
+
let acceptanceOk = true;
|
|
217
|
+
if (residual.disposition === "accepted") {
|
|
218
|
+
const acceptance = residual.acceptance;
|
|
219
|
+
acceptanceOk = baseline.acceptance_policy.authorized_risk_acceptors.includes(acceptance.accepted_by);
|
|
220
|
+
if (!acceptanceOk)
|
|
221
|
+
addError("RISK_ACCEPTOR_UNAUTHORIZED", risk.id, `接受人 "${acceptance.accepted_by}" 不在基线授权名单。`);
|
|
222
|
+
if (Date.parse(acceptance.accepted_at) > Date.parse(evaluation.evaluated_at)) {
|
|
223
|
+
acceptanceOk = false;
|
|
224
|
+
addError("RISK_ACCEPTED_AFTER_EVALUATION", risk.id, "风险接受时间晚于评价时间。");
|
|
225
|
+
}
|
|
226
|
+
if (Date.parse(acceptance.review_due) < Date.parse(evaluation.report_generated_at)) {
|
|
227
|
+
acceptanceOk = false;
|
|
228
|
+
addError("RISK_ACCEPTANCE_EXPIRED", risk.id, "风险接受记录已过复审期限。");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const linkedTestsOk = risk.test_ids.every((id) => evaluatedTestById.get(id)?.passed === true);
|
|
232
|
+
const linkedRequirementsOk = risk.requirement_ids.every((id) => evaluatedRequirementById.get(id)?.passed === true);
|
|
233
|
+
if (!linkedTestsOk || !linkedRequirementsOk)
|
|
234
|
+
addError("RISK_CONTROLS_NOT_VERIFIED", risk.id, "风险关联的需求或测试尚未全部通过。");
|
|
235
|
+
const controlled = residual.disposition !== "open" && evidenceOk && levelOk && acceptanceOk && linkedTestsOk && linkedRequirementsOk;
|
|
236
|
+
return {
|
|
237
|
+
id: risk.id,
|
|
238
|
+
owner: risk.owner,
|
|
239
|
+
likelihood: risk.likelihood,
|
|
240
|
+
impact: risk.impact,
|
|
241
|
+
inherent_level: risk.inherent_level,
|
|
242
|
+
compensating_controls: risk.compensating_controls,
|
|
243
|
+
residual_level: residual.residual_level,
|
|
244
|
+
disposition: residual.disposition,
|
|
245
|
+
accepted_by: residual.acceptance?.accepted_by ?? null,
|
|
246
|
+
evidence_ids: sortedUnique(residual.evidence_ids),
|
|
247
|
+
controlled,
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
const traceabilityMatrix = [...baseline.requirements].sort((a, b) => compareText(a.id, b.id)).map((requirement) => {
|
|
251
|
+
const measurement = measurementByRequirement.get(requirement.id);
|
|
252
|
+
const testEvidence = requirement.test_ids.flatMap((id) => testResultById.get(id)?.evidence_ids ?? []);
|
|
253
|
+
const riskEvidence = requirement.risk_ids.flatMap((id) => residualById.get(id)?.evidence_ids ?? []);
|
|
254
|
+
const evidenceIds = sortedUnique([...(measurement?.evidence_ids ?? []), ...testEvidence, ...riskEvidence]);
|
|
255
|
+
const complete = Boolean(measurement
|
|
256
|
+
&& requirement.test_ids.every((id) => evaluatedTestById.has(id))
|
|
257
|
+
&& requirement.risk_ids.every((id) => riskResults.some((risk) => risk.id === id && risk.controlled))
|
|
258
|
+
&& evidenceIds.length > 0
|
|
259
|
+
&& evidenceIds.every((id) => evidenceById.has(id)));
|
|
260
|
+
if (!complete)
|
|
261
|
+
addError("TRACEABILITY_INCOMPLETE", requirement.id, "需求—风险—测试—证据追踪链未完整闭环。");
|
|
262
|
+
return {
|
|
263
|
+
requirement_id: requirement.id,
|
|
264
|
+
risk_ids: sortedUnique(requirement.risk_ids),
|
|
265
|
+
test_ids: sortedUnique(requirement.test_ids),
|
|
266
|
+
evidence_ids: evidenceIds,
|
|
267
|
+
complete,
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
const characteristicResults = ISO_25010_CHARACTERISTICS.map((id) => {
|
|
271
|
+
const characteristic = baseline.characteristics.find((item) => item.id === id);
|
|
272
|
+
const requirementIds = baseline.requirements.filter((item) => item.characteristic === id).map((item) => item.id).sort(compareText);
|
|
273
|
+
if (!characteristic.applicable) {
|
|
274
|
+
return { id, status: "tailored_out", requirement_ids: requirementIds, tailoring_reason: characteristic.tailoring_reason };
|
|
275
|
+
}
|
|
276
|
+
const accepted = requirementIds.every((requirementId) => evaluatedRequirementById.get(requirementId)?.passed === true);
|
|
277
|
+
return { id, status: accepted ? "accepted" : "rejected", requirement_ids: requirementIds, tailoring_reason: null };
|
|
278
|
+
});
|
|
279
|
+
for (const evidence of evaluation.evidence) {
|
|
280
|
+
if (!referencedEvidence.has(evidence.id))
|
|
281
|
+
addWarning("UNREFERENCED_EVIDENCE", evidence.id, "证据未被阶段、需求、测试或风险结果引用。");
|
|
282
|
+
}
|
|
283
|
+
findings.sort((left, right) => compareText(`${left.severity}:${left.code}:${left.subject}:${left.message}`, `${right.severity}:${right.code}:${right.subject}:${right.message}`));
|
|
284
|
+
const errors = findings.filter((finding) => finding.severity === "error").length;
|
|
285
|
+
const warnings = findings.length - errors;
|
|
286
|
+
const verdict = errors === 0 ? "accepted" : "rejected";
|
|
287
|
+
const report = {
|
|
288
|
+
schema_version: ACCEPTANCE_SCHEMA_VERSION,
|
|
289
|
+
report_id: evaluation.report_id,
|
|
290
|
+
evaluation_id: evaluation.evaluation_id,
|
|
291
|
+
report_generated_at: evaluation.report_generated_at,
|
|
292
|
+
product: baseline.product,
|
|
293
|
+
scope: baseline.scope,
|
|
294
|
+
baseline: {
|
|
295
|
+
id: baseline.baseline_id,
|
|
296
|
+
version: baseline.baseline_version,
|
|
297
|
+
title: baseline.title,
|
|
298
|
+
fingerprint_sha256: acceptanceBaselineFingerprint(baseline),
|
|
299
|
+
approval: baseline.approval,
|
|
300
|
+
},
|
|
301
|
+
evaluator: evaluation.evaluator,
|
|
302
|
+
standard_basis: [...ACCEPTANCE_STANDARD_BASIS],
|
|
303
|
+
verdict,
|
|
304
|
+
summary: {
|
|
305
|
+
applicable_characteristics: characteristicResults.filter((item) => item.status !== "tailored_out").length,
|
|
306
|
+
accepted_characteristics: characteristicResults.filter((item) => item.status === "accepted").length,
|
|
307
|
+
requirements_total: requirementResults.length,
|
|
308
|
+
requirements_passed: requirementResults.filter((item) => item.passed).length,
|
|
309
|
+
tests_total: testResults.length,
|
|
310
|
+
tests_passed: testResults.filter((item) => item.passed).length,
|
|
311
|
+
risks_total: riskResults.length,
|
|
312
|
+
risks_controlled: riskResults.filter((item) => item.controlled).length,
|
|
313
|
+
stages_total: stageResults.length,
|
|
314
|
+
stages_completed: stageResults.filter((item) => item.passed).length,
|
|
315
|
+
errors,
|
|
316
|
+
warnings,
|
|
317
|
+
},
|
|
318
|
+
stage_results: stageResults,
|
|
319
|
+
characteristic_results: characteristicResults,
|
|
320
|
+
requirement_results: requirementResults,
|
|
321
|
+
test_results: testResults,
|
|
322
|
+
risk_results: riskResults,
|
|
323
|
+
traceability_matrix: traceabilityMatrix,
|
|
324
|
+
findings,
|
|
325
|
+
conclusion: verdict === "accepted"
|
|
326
|
+
? "本次评价所引用的预批准质量基线、量化需求、测试证据、双向追踪、残余风险处置及 ISO/IEC 25040 五阶段记录均满足既定准则,产品在本报告限定范围内通过验收。"
|
|
327
|
+
: `本次评价存在 ${errors} 个阻断项,产品在本报告限定范围内不通过验收;关闭全部阻断项并重新执行评价后方可批准。`,
|
|
328
|
+
};
|
|
329
|
+
return AcceptanceReportSchema.parse(report);
|
|
330
|
+
}
|