# core/validators/coverage.validator.ts

> 170 lines of code and 31 definitions.

Tree: Build tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build#file-build-core-validators-coverage-validator-ts
Source text: https://banes-lab.com/assets/sources/source.89fdf50b0f1d0854a7f6913bdacddb393272c87911cbb4b76ccfb6e5e18b2c0b.generated.txt

## Definitions

- `finding` (lexical_declaration, line 29)
- `cyclesIn` (lexical_declaration, line 93)
- `nodeOf` (lexical_declaration, line 132)
- `shapeFindings` (lexical_declaration, line 33)
- `requirementFindings` (lexical_declaration, line 73)
- `cycleFindings` (lexical_declaration, line 118)
- `teachersOf` (lexical_declaration, line 19)
- `localRequirements` (lexical_declaration, line 65)
- `visit` (lexical_declaration, line 97)
- `validateUnion` (lexical_declaration, line 140, exported)
- `validateGraph` (lexical_declaration, line 164, exported)
- `conceptsTaughtAcross` (lexical_declaration, line 173, exported)
- `orderedSections` (lexical_declaration, line 12)
- `declared` (lexical_declaration, line 13)
- `built` (lexical_declaration, line 14)
- `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)
- `NODE_MARK` (lexical_declaration, line 130)
- `taughtAcross` (lexical_declaration, line 136, exported)
- `teachers` (lexical_declaration, line 141, exported)
- `edges` (lexical_declaration, line 149, exported)
- `cut` (lexical_declaration, line 159, exported)
- `untaughtOf` (lexical_declaration, line 168, exported)
- `traced` (lexical_declaration, line 169, exported)
- `seen` (lexical_declaration, line 174, exported)
- `findings` (lexical_declaration, line 175, exported)
- `other` (lexical_declaration, line 178, exported)

## 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";

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): Map<string, string[]> {
    const teachers = new Map<string, string[]>();
    for (const [section, held] of Object.entries(graph.sections)) {
        for (const concept of held.teaches) {
            teachers.set(concept, [...(teachers.get(concept) ?? []), section]);
        }
    }
    return teachers;
};

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

const NODE_MARK = "#";

const nodeOf = function nodeOf(page: string, section: string): string {
    return page + NODE_MARK + section;
};

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 = new Map<string, string[]>();
    for (const graph of graphs) {
        for (const [section, held] of Object.entries(graph.sections)) {
            for (const concept of held.teaches) {
                teachers.set(concept, [...(teachers.get(concept) ?? []), nodeOf(graph.page, section)]);
            }
        }
    }
    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 cut = node.indexOf(NODE_MARK);
        return finding(node.slice(0, cut), node.slice(cut + NODE_MARK.length), 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;
};
```
