import { CITE_NODE, CITE_TREE_ONLY, CITE_UNQUALIFIED, GROUNDING_CONTRADICTED, GROUNDING_UNASSESSED, GROUNDING_UNDECLARED, citeUnresolved, } from "#configuration/strings/coverage.strings"; import type { ContentGraph, DeclaredAbsence, Evidence, GraphFinding, GroundingReport } from "#types/coverage.types"; import { PATH_SEPARATOR, TREE_MARK } from "@banes-lab/web/configuration/constants/anatomy.constants.ts"; import { nodeOf, nodePartsOf, teachingNodes } from "#core/converters/section.converter"; import { ANATOMY_TREE_DECLARATIONS } from "@banes-lab/web/core/registries/anatomy.tree.registry.ts"; import type { EvidenceNode } from "@banes-lab/web/types/evidence.types.js"; import { TREE_TAB } from "@banes-lab/web/core/ids/anatomy.ids.ts"; import { treeLabelOf } from "@banes-lab/web/domain/converters/source.converter.ts"; import { treeOf } from "@banes-lab/web/core/converters/folder.converter.ts"; const finding = function finding(node: string, message: string): GraphFinding { return { message, ...nodePartsOf(node) }; }; 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.flatMap((entry) => entry.subject.kind === "chapter" ? [nodeOf(entry.subject.page, entry.subject.section)] : [], ), ); const unassessed = teaching.filter((node) => !grounded.has(node) && !absent.has(node)); 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)), ...unassessed.map((node) => finding(node, GROUNDING_UNASSESSED)), ]; return { absent: teaching.filter((node) => absent.has(node)), findings, grounded: teaching.filter((node) => grounded.has(node)), unassessed, }; }; const writtenOf = function writtenOf(node: EvidenceNode): string { if (node.kind === "folder") { return node.words.join(PATH_SEPARATOR); } return node.kind === "definition" && node.file !== null ? node.file : node.name; }; const labelOf = function labelOf(node: EvidenceNode): string { return node.kind === "folder" ? writtenOf(node) : node.name; }; const citeFinding = function citeFinding(node: EvidenceNode, path: string | null): string | null { if (path === null) { return CITE_NODE + labelOf(node) + citeUnresolved(ANATOMY_TREE_DECLARATIONS.map((tree) => tree.label)); } const tree = treeOf(path); return tree === TREE_TAB || writtenOf(node).includes(TREE_MARK) ? null : `${CITE_NODE}${labelOf(node)}${CITE_TREE_ONLY}${treeLabelOf(tree)}${CITE_UNQUALIFIED}${path}.`; }; export const citeFindings = function citeFindings( evidence: readonly Evidence[], locate: (node: EvidenceNode) => string | null, ): readonly GraphFinding[] { return evidence.flatMap((entry) => entry.nodes.flatMap((node) => { const message = citeFinding(node, locate(node)); if (message === null) { return []; } const { subject } = entry; return [ subject.kind === "chapter" ? { message, page: subject.page, section: subject.section } : { message, page: subject.ref, section: labelOf(node) }, ]; }), ); };