104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
import type { ReportCoverage, ReportDomainSummary } from './report-projection';
|
|
|
|
/**
|
|
* Deterministic key statements of the Management Report (user decision 2026-09-30). They are
|
|
* language-neutral data derived only from already computed aggregates; wording lives in
|
|
* `report-statement-text.ts`. No severity, risk, ranking across sources or free text is derived.
|
|
*/
|
|
export const REPORT_NARRATIVE_VERSION = '1.0.0';
|
|
|
|
export type ReportEvidenceRole =
|
|
| 'passed' | 'deviations' | 'not-run' | 'not-applicable' | 'execution-error' | 'other-unassessed';
|
|
|
|
/** The result IDs behind one number of a statement. */
|
|
export interface ReportStatementEvidence {
|
|
role: ReportEvidenceRole;
|
|
domainId?: string;
|
|
ids: string[];
|
|
}
|
|
|
|
export interface ReportDomainFact {
|
|
domainId: string;
|
|
domainLabel: string;
|
|
deviations: number;
|
|
assessed: number;
|
|
}
|
|
|
|
export type ReportStatement =
|
|
| { id: 'overall'; assessed: number; deviations: number; evidence: ReportStatementEvidence[] }
|
|
| {
|
|
id: 'focus';
|
|
/** Every domain sharing the highest absolute deviation count; ties are never broken arbitrarily. */
|
|
mostDeviations: ReportDomainFact[];
|
|
/** Every domain sharing the highest deviation share among assessed results. */
|
|
highestShare: ReportDomainFact[];
|
|
evidence: ReportStatementEvidence[];
|
|
}
|
|
| {
|
|
id: 'limits';
|
|
notRun: number;
|
|
notApplicable: number;
|
|
executionErrors: number;
|
|
otherUnassessed: number;
|
|
evidence: ReportStatementEvidence[];
|
|
};
|
|
|
|
const fact = (domain: ReportDomainSummary): ReportDomainFact => ({
|
|
domainId: domain.domainId, domainLabel: domain.domainLabel, deviations: domain.deviations, assessed: domain.assessed
|
|
});
|
|
|
|
/** Exact comparison of deviations/assessed without floating-point rounding. */
|
|
function compareShare(left: ReportDomainSummary, right: ReportDomainSummary): number {
|
|
return left.deviations * right.assessed - right.deviations * left.assessed;
|
|
}
|
|
|
|
function allMaxima(domains: ReportDomainSummary[], compare: (a: ReportDomainSummary, b: ReportDomainSummary) => number): ReportDomainSummary[] {
|
|
const best = domains.reduce<ReportDomainSummary | undefined>((current, item) =>
|
|
!current || compare(item, current) > 0 ? item : current, undefined);
|
|
return best ? domains.filter(item => compare(item, best) === 0) : [];
|
|
}
|
|
|
|
export function buildReportStatements(domains: ReportDomainSummary[], coverage: ReportCoverage): ReportStatement[] {
|
|
const statements: ReportStatement[] = [{
|
|
id: 'overall', assessed: coverage.assessed, deviations: coverage.deviations,
|
|
evidence: [
|
|
{ role: 'deviations', ids: coverage.evidence.deviations },
|
|
{ role: 'passed', ids: coverage.evidence.passed }
|
|
]
|
|
}];
|
|
|
|
const withDeviations = domains.filter(domain => domain.assessed > 0 && domain.deviations > 0);
|
|
if (withDeviations.length) {
|
|
const mostDeviations = allMaxima(withDeviations, (a, b) => a.deviations - b.deviations);
|
|
const highestShare = allMaxima(withDeviations, compareShare);
|
|
const named = [...new Map([...mostDeviations, ...highestShare].map(domain => [domain.domainId, domain])).values()];
|
|
statements.push({
|
|
id: 'focus',
|
|
mostDeviations: mostDeviations.map(fact),
|
|
highestShare: highestShare.map(fact),
|
|
evidence: named.flatMap(domain => [
|
|
{ role: 'deviations' as const, domainId: domain.domainId, ids: domain.evidence.deviations },
|
|
{ role: 'passed' as const, domainId: domain.domainId, ids: domain.evidence.passed }
|
|
])
|
|
});
|
|
}
|
|
|
|
const unassessed = coverage.evidence;
|
|
const limits = {
|
|
notRun: unassessed.notRun.length,
|
|
notApplicable: unassessed.notApplicable.length,
|
|
executionErrors: unassessed.executionErrors.length,
|
|
otherUnassessed: unassessed.otherUnassessed.length
|
|
};
|
|
if (limits.notRun + limits.notApplicable + limits.executionErrors + limits.otherUnassessed > 0) {
|
|
statements.push({
|
|
id: 'limits', ...limits,
|
|
evidence: ([
|
|
['not-run', unassessed.notRun], ['not-applicable', unassessed.notApplicable],
|
|
['execution-error', unassessed.executionErrors], ['other-unassessed', unassessed.otherUnassessed]
|
|
] as const).filter(([, ids]) => ids.length).map(([role, ids]) => ({ role, ids }))
|
|
});
|
|
}
|
|
return statements;
|
|
}
|