Assessor_2.0/src/reporting/report-narrative.ts

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;
}