# core/validators/coverage.validator.ts

> 159 lines of code and 29 definitions.

Tree: Bane's Lab Content
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/content#file-content-core-validators-coverage-validator-ts
Source text: https://banes-lab.com/source/content/core/validators/coverage.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/content/core/validators.md), after [core/validators/coordination.validator.ts](https://banes-lab.com/source/content/core/validators/coordination.validator.ts.md) and before [core/validators/evidence.validator.ts](https://banes-lab.com/source/content/core/validators/evidence.validator.ts.md).

## Definitions

- `finding` (lexical_declaration, line 29)
- `cyclesIn` (lexical_declaration, line 93)
- `shapeFindings` (lexical_declaration, line 33)
- `requirementFindings` (lexical_declaration, line 73)
- `teachersOf` (lexical_declaration, line 20)
- `cycleFindings` (lexical_declaration, line 118)
- `localRequirements` (lexical_declaration, line 65)
- `visit` (lexical_declaration, line 97)
- `validateUnion` (lexical_declaration, line 134, exported)
- `validateGraph` (lexical_declaration, line 151, exported)
- `conceptsTaughtAcross` (lexical_declaration, line 160, exported)
- `orderedSections` (lexical_declaration, line 13)
- `declared` (lexical_declaration, line 14)
- `built` (lexical_declaration, line 15)
- `requirementMessage` (lexical_declaration, line 53)
- `order` (lexical_declaration, line 81)
- `message` (lexical_declaration, line 84)
- `visiting` (lexical_declaration, line 94)
- `done` (lexical_declaration, line 95)
- `cycles` (lexical_declaration, line 96)
- `taughtAcross` (lexical_declaration, line 130, exported)
- `teachers` (lexical_declaration, line 135, exported)
- `edges` (lexical_declaration, line 136, exported)
- `{ page, section }` (lexical_declaration, line 146, exported)
- `untaughtOf` (lexical_declaration, line 155, exported)
- `traced` (lexical_declaration, line 156, exported)
- `seen` (lexical_declaration, line 161, exported)
- `findings` (lexical_declaration, line 162, exported)
- `other` (lexical_declaration, line 165, exported)

## Contained in

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

## Uses

- [core/converters/section.converter.ts](https://banes-lab.com/source/content/core/converters/section.converter.ts.md)

## Linked from

- [core/converters](https://banes-lab.com/anatomy/content/folder-content-core-converters.md)
- [core/validators](https://banes-lab.com/anatomy/content/folder-content-core-validators.md)

## Source

```typescript
import {
    CONCEPT_TAUGHT_TWICE,
    CYCLE_FOUND,
    REQUIRES_FORWARD,
    REQUIRES_UNKNOWN,
    SECTION_UNDECLARED,
    SECTION_UNTAUGHT,
    TRACE_UNKNOWN,
} from "#configuration/strings/coverage.strings";
import type { ContentGraph, GraphContext, GraphFinding, GraphSection } from "#types/coverage.types";
import { conceptTeachers, nodeOf, nodePartsOf } from "#core/converters/section.converter";

const orderedSections = function orderedSections(graph: ContentGraph, context: GraphContext): string[] {
    const declared = Object.keys(graph.sections);
    const built = context.payloadSections.get(graph.page) ?? [];
    const order = built.filter((section) => declared.includes(section));
    return [...order, ...declared.filter((section) => !order.includes(section))];
};

const teachersOf = function teachersOf(graph: ContentGraph): ReadonlyMap<string, readonly string[]> {
    return new Map(
        [...conceptTeachers([graph])].map(([concept, nodes]) => [
            concept,
            nodes.map((node) => nodePartsOf(node).section),
        ]),
    );
};

const finding = function finding(page: string, section: string, message: string): GraphFinding {
    return { message, page, section };
};

const shapeFindings = function shapeFindings(graph: ContentGraph, context: GraphContext): GraphFinding[] {
    const findings: GraphFinding[] = [];
    for (const section of context.payloadSections.get(graph.page) ?? []) {
        if (!(section in graph.sections)) {
            findings.push(finding(graph.page, section, SECTION_UNDECLARED));
        }
    }
    for (const [section, held] of Object.entries(graph.sections)) {
        if (held.teaches.length === 0 && held.narrative !== true) {
            findings.push(finding(graph.page, section, SECTION_UNTAUGHT));
        }
        for (const trace of held.traces) {
            if (!context.known.has(trace)) {
                findings.push(finding(graph.page, section, `${TRACE_UNKNOWN}${trace}`));
            }
        }
    }
    return findings;
};

const requirementMessage = function requirementMessage(
    teacher: string | undefined,
    required: string,
    order: readonly string[],
    index: number,
): string | null {
    if (teacher === undefined) {
        return `${REQUIRES_UNKNOWN}${required}`;
    }
    return order.indexOf(teacher) >= index ? `${REQUIRES_FORWARD}${required}` : null;
};

const localRequirements = function localRequirements(
    held: GraphSection | undefined,
    teachers: ReadonlyMap<string, readonly string[]>,
    context: GraphContext,
): readonly string[] {
    return (held?.requires ?? []).filter((required) => teachers.has(required) || !context.taught.has(required));
};

const requirementFindings = function requirementFindings(graph: ContentGraph, context: GraphContext): GraphFinding[] {
    const findings: GraphFinding[] = [];
    const teachers = teachersOf(graph);
    for (const [concept, sections] of teachers) {
        if (sections.length > 1) {
            findings.push(finding(graph.page, sections.join(", "), `${CONCEPT_TAUGHT_TWICE}${concept}`));
        }
    }
    const order = orderedSections(graph, context);
    for (const [index, section] of order.entries()) {
        for (const required of localRequirements(graph.sections[section], teachers, context)) {
            const message = requirementMessage(teachers.get(required)?.[0], required, order, index);
            if (message !== null) {
                findings.push(finding(graph.page, section, message));
            }
        }
    }
    return findings;
};

const cyclesIn = function cyclesIn(edges: ReadonlyMap<string, readonly string[]>): string[] {
    const visiting = new Set<string>();
    const done = new Set<string>();
    const cycles: string[] = [];
    const visit = function visit(node: string): void {
        if (done.has(node)) {
            return;
        }
        if (visiting.has(node)) {
            cycles.push(node);
            return;
        }
        visiting.add(node);
        for (const next of edges.get(node) ?? []) {
            visit(next);
        }
        visiting.delete(node);
        done.add(node);
    };
    for (const node of edges.keys()) {
        visit(node);
    }
    return cycles;
};

const cycleFindings = function cycleFindings(graph: ContentGraph): GraphFinding[] {
    const teachers = teachersOf(graph);
    const edges = new Map<string, string[]>();
    for (const [section, held] of Object.entries(graph.sections)) {
        edges.set(
            section,
            held.requires.flatMap((concept) => teachers.get(concept) ?? []),
        );
    }
    return cyclesIn(edges).map((section) => finding(graph.page, section, CYCLE_FOUND));
};

export const taughtAcross = function taughtAcross(graphs: readonly ContentGraph[]): ReadonlySet<string> {
    return new Set(graphs.flatMap((graph) => Object.values(graph.sections).flatMap((held) => held.teaches)));
};

export const validateUnion = function validateUnion(graphs: readonly ContentGraph[]): GraphFinding[] {
    const teachers = conceptTeachers(graphs);
    const edges = new Map<string, string[]>();
    for (const graph of graphs) {
        for (const [section, held] of Object.entries(graph.sections)) {
            edges.set(
                nodeOf(graph.page, section),
                held.requires.flatMap((concept) => teachers.get(concept) ?? []),
            );
        }
    }
    return cyclesIn(edges).map((node) => {
        const { page, section } = nodePartsOf(node);
        return finding(page, section, CYCLE_FOUND);
    });
};

export const validateGraph = function validateGraph(graph: ContentGraph, context: GraphContext): GraphFinding[] {
    return [...shapeFindings(graph, context), ...requirementFindings(graph, context), ...cycleFindings(graph)];
};

export const untaughtOf = function untaughtOf(graphs: readonly ContentGraph[], slugs: readonly string[]): string[] {
    const traced = new Set(graphs.flatMap((graph) => Object.values(graph.sections).flatMap((held) => held.traces)));
    return [...new Set(slugs)].filter((slug) => !traced.has(slug));
};

export const conceptsTaughtAcross = function conceptsTaughtAcross(graphs: readonly ContentGraph[]): GraphFinding[] {
    const seen = new Map<string, string>();
    const findings: GraphFinding[] = [];
    for (const graph of graphs) {
        for (const concept of teachersOf(graph).keys()) {
            const other = seen.get(concept);
            if (other !== undefined && other !== graph.page) {
                findings.push(finding(graph.page, other, `${CONCEPT_TAUGHT_TWICE}${concept}`));
            }
            seen.set(concept, graph.page);
        }
    }
    return findings;
};
```
