core/validators/coverage.validator.ts
core/validators/coverage.validator.ts is a file in Bane's Lab Content. 159 lines of code and 29 definitions.
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;
};