import type { ContentGraph, DeclaredAbsence, Evidence, GraphFinding, GroundingReport } from "#types/coverage.types"; import { GROUNDING_CONTRADICTED, GROUNDING_UNDECLARED } from "#configuration/strings/coverage.strings"; const NODE_MARK = "#"; const nodeOf = function nodeOf(page: string, section: string): string { return page + NODE_MARK + section; }; const finding = function finding(node: string, message: string): GraphFinding { const cut = node.indexOf(NODE_MARK); return { message, page: node.slice(0, cut), section: node.slice(cut + NODE_MARK.length) }; }; const teachingNodes = function teachingNodes(graphs: readonly ContentGraph[]): readonly string[] { return graphs.flatMap((graph) => Object.entries(graph.sections).flatMap(([section, held]) => held.teaches.length > 0 ? [nodeOf(graph.page, section)] : [], ), ); }; const chapterNodes = function chapterNodes(evidence: readonly Evidence[]): ReadonlySet { return new Set( evidence.flatMap((entry) => entry.subject.kind === "chapter" ? [nodeOf(entry.subject.page, entry.subject.section)] : [], ), ); }; export const groundingOf = function groundingOf( graphs: readonly ContentGraph[], evidence: readonly Evidence[], absences: readonly DeclaredAbsence[], ): GroundingReport { const teaching = teachingNodes(graphs); const declared = new Set( graphs.flatMap((graph) => Object.keys(graph.sections).map((key) => nodeOf(graph.page, key))), ); const grounded = chapterNodes(evidence); const absent = new Set(absences.map((entry) => nodeOf(entry.subject.page, entry.subject.section))); const findings = [ ...[...grounded].filter((node) => absent.has(node)).map((node) => finding(node, GROUNDING_CONTRADICTED)), ...[...grounded, ...absent] .filter((node) => !declared.has(node)) .map((node) => finding(node, GROUNDING_UNDECLARED)), ]; return { absent: teaching.filter((node) => absent.has(node)), findings, grounded: teaching.filter((node) => grounded.has(node)), unassessed: teaching.filter((node) => !grounded.has(node) && !absent.has(node)), }; };