# core/formatters/report.formatter.ts

> 52 lines of code and 14 definitions.

Tree: GovLab Patterns
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/patterns#file-patterns-core-formatters-report-formatter-ts
Source text: https://banes-lab.com/source/patterns/core/formatters/report.formatter.ts.txt

Listed in [core/formatters](https://banes-lab.com/api/source/patterns/core/formatters.md), after [core/formatters/markup.formatter.ts](https://banes-lab.com/source/patterns/core/formatters/markup.formatter.ts.md).

## Definitions

- `section` (lexical_declaration, line 10)
- `buildMasterFindings` (lexical_declaration, line 28, exported)
- `buildFindings` (lexical_declaration, line 23, exported)
- `buildAnalysis` (lexical_declaration, line 44, exported)
- `byKind` (lexical_declaration, line 15)
- `ROW_JOIN` (lexical_declaration, line 8)
- `rows` (lexical_declaration, line 11)
- `byModuleName` (lexical_declaration, line 19)
- `sorted` (lexical_declaration, line 29, exported)
- `all` (lexical_declaration, line 30, exported)
- `perModule` (lexical_declaration, line 31, exported)
- `{ insight, graph, findingsCount, title }` (lexical_declaration, line 45, exported)
- `summary` (lexical_declaration, line 46, exported)
- `body` (lexical_declaration, line 53, exported)

## Contained in

- [core/formatters](https://banes-lab.com/anatomy/patterns/folder-patterns-core-formatters.md)

## Uses

- [core/counters/base.counter.ts](https://banes-lab.com/source/patterns/core/counters/base.counter.ts.md)

## Used by

- [core/coordinators/package.coordinator.ts](https://banes-lab.com/source/patterns/core/coordinators/package.coordinator.ts.md)
- [core/persistence/report.persistence.ts](https://banes-lab.com/source/patterns/core/persistence/report.persistence.ts.md)
- [core/validators/report.validator.ts](https://banes-lab.com/source/patterns/core/validators/report.validator.ts.md)

## Linked from

- [core/coordinators](https://banes-lab.com/anatomy/patterns/folder-patterns-core-coordinators.md)
- [core/counters](https://banes-lab.com/anatomy/patterns/folder-patterns-core-counters.md)
- [core/formatters](https://banes-lab.com/anatomy/patterns/folder-patterns-core-formatters.md)
- [core/persistence](https://banes-lab.com/anatomy/patterns/folder-patterns-core-persistence.md)
- [core/validators](https://banes-lab.com/anatomy/patterns/folder-patterns-core-validators.md)

## Source

```typescript
import type { AnalysisModel, ModuleFindings } from "#types/report.types";
import { sortedEdges, unresolvedNames } from "#core/selectors/code.selector";
import type { CodeFinding } from "#types/code.types";
import { definitionRecords } from "#core/converters/definition.converter";
import { resolutionRate } from "#core/counters/report.counter";
import { tally } from "#core/counters/base.counter";

const ROW_JOIN = ",\n";

const section = function section(key: string, items: readonly unknown[]): string {
    const rows = items.map((item) => JSON.stringify(item)).join(ROW_JOIN);
    return items.length === 0 ? `"${key}": []` : `"${key}": [\n${rows}\n]`;
};

const byKind = function byKind(findings: readonly CodeFinding[]): Record<string, number> {
    return tally(findings, (finding) => finding.kind);
};

const byModuleName = function byModuleName(left: { module: string }, right: { module: string }): number {
    return left.module.localeCompare(right.module);
};

export const buildFindings = function buildFindings(title: string, findings: readonly CodeFinding[]): string {
    const summary = { byKind: byKind(findings), total: findings.length };
    return `{\n"module": ${JSON.stringify(title)},\n"summary": ${JSON.stringify(summary)},\n${section("findings", findings)}\n}\n`;
};

export const buildMasterFindings = function buildMasterFindings(modules: readonly ModuleFindings[]): string {
    const sorted = modules.toSorted(byModuleName);
    const all = sorted.flatMap((entry) => entry.findings.map((finding) => ({ module: entry.module, ...finding })));
    const perModule = sorted
        .map((entry) => ({ byKind: byKind(entry.findings), module: entry.module, total: entry.findings.length }))
        .filter((entry) => entry.total > 0)
        .sort((left, right) => right.total - left.total || byModuleName(left, right));
    const summary = {
        byKind: tally(all, (finding) => finding.kind),
        bySeverity: tally(all, (finding) => finding.severity),
        findings: all.length,
        modules: modules.length,
    };
    return `{\n"summary": ${JSON.stringify(summary)},\n${section("modules", perModule)},\n${section("findings", all)}\n}\n`;
};

export const buildAnalysis = function buildAnalysis(model: AnalysisModel): string {
    const { insight, graph, findingsCount, title } = model;
    const summary = {
        definitions: insight.definitions,
        edges: graph.edges.length,
        findings: findingsCount,
        resolutionRate: resolutionRate(graph.edges.length, graph.external.length),
        unresolvedCalls: graph.external.length,
    };
    const body = [
        section("unresolvedCalls", unresolvedNames(graph)),
        section("definitions", definitionRecords(insight, graph)),
        section("edges", sortedEdges(graph)),
    ].join(",\n");
    return `{\n"module": ${JSON.stringify(title)},\n"summary": ${JSON.stringify(summary)},\n${body}\n}\n`;
};
```
