core/validators/graph.validator.ts
core/validators/graph.validator.ts is a file in Bane's Lab Build Scripts. 168 lines of code and 32 definitions.
import type { GraphEdge, GraphNode } from "@banes-lab/web/types/graph.types.js";
import {
NO_GRAPH_REPORT,
POPULATION_NOUNS,
ambiguousRelation,
danglingEdge,
duplicateNode,
emptyPopulation,
malformedReportEntries,
missingReportList,
sharedNumber,
tooltipGap,
unbalancedPopulation,
uncoveredSection,
undeclaredAbsence,
undeclaredRelation,
unresolvedTarget,
} from "#configuration/strings/graph.strings";
import type { Undeclared, UnresolvedTarget } from "#types/graph.types";
import { existsSync, readFileSync } from "node:fs";
import { EXPECTED_ABSENCES } from "#configuration/constants/graph.constants";
import type { Finding } from "#types/validation.types";
import type { Population } from "#types/catalog.types";
import { isRecord } from "#core/selectors/base.selector";
const ABSENT_MARK = "absent";
const PART_SEPARATOR = " ";
interface Numbered {
readonly number: string;
readonly ref: string;
}
interface Duplicate {
readonly dropped: GraphNode;
readonly kept: GraphNode;
}
const isText = function isText(value: unknown): value is string {
return typeof value === "string";
};
const isEdge = function isEdge(value: unknown): value is GraphEdge {
return (
isRecord(value) &&
typeof value["from"] === "string" &&
typeof value["relation"] === "string" &&
typeof value["to"] === "string"
);
};
const isUndeclared = function isUndeclared(value: unknown): value is Undeclared {
return isRecord(value) && typeof value["face"] === "string" && typeof value["relation"] === "string";
};
const isPopulation = function isPopulation(value: unknown): value is Population {
return (
isRecord(value) &&
typeof value["name"] === "string" &&
typeof value["whole"] === "number" &&
isRecord(value["parts"])
);
};
const isTarget = function isTarget(value: unknown): value is UnresolvedTarget {
return (
isRecord(value) &&
typeof value["from"] === "string" &&
typeof value["label"] === "string" &&
typeof value["relation"] === "string"
);
};
const isNode = function isNode(value: unknown): value is GraphNode {
return (
isRecord(value) &&
typeof value["ref"] === "string" &&
typeof value["kind"] === "string" &&
typeof value["layer"] === "string"
);
};
const isDuplicate = function isDuplicate(value: unknown): value is Duplicate {
return isRecord(value) && isNode(value["dropped"]) && isNode(value["kept"]);
};
type ReportCheck = (report: string, parsed: unknown) => Finding[];
const reportCheck = function reportCheck<T>(
path: readonly string[],
guard: (value: unknown) => value is T,
findingsOf: (items: readonly T[], report: string) => Finding[],
): ReportCheck {
const key = path.join(".");
return (report, parsed) => {
const list = path.reduce<unknown>((value, segment) => (isRecord(value) ? value[segment] : null), parsed);
if (!Array.isArray(list)) {
return [{ file: report, message: missingReportList(key) }];
}
const kept = list.filter(guard);
const dropped = list.length - kept.length;
return [
...findingsOf(kept, report),
...(dropped === 0 ? [] : [{ file: report, message: malformedReportEntries(key, dropped) }]),
];
};
};
const duplicateNumbers = function duplicateNumbers(nodes: readonly GraphNode[]): Finding[] {
const first = new Map<string, string>();
const findings: Finding[] = [];
const numbered = nodes.filter((node): node is GraphNode & Numbered => node.number !== null);
for (const node of numbered) {
const held = first.get(node.number);
if (held === undefined) {
first.set(node.number, node.ref);
} else {
findings.push({ file: held, message: sharedNumber(node.number, held, node.ref) });
}
}
return findings;
};
const countsOf = function countsOf(population: Population): readonly (readonly [string, number])[] {
return Object.entries(population.parts).filter((entry): entry is [string, number] => typeof entry[1] === "number");
};
const absenceFindings = function absenceFindings(report: string, population: Population): Finding[] {
const noun = POPULATION_NOUNS.get(population.name);
if (noun === undefined) {
return [];
}
const declared = EXPECTED_ABSENCES.get(population.name);
return countsOf(population).flatMap(([part, count]) => {
const [state = "", layer = "", kind = ""] = part.split(PART_SEPARATOR);
return state !== ABSENT_MARK || count === 0 || declared?.has(part) === true
? []
: [{ file: report, message: undeclaredAbsence(count, kind, layer, noun) }];
});
};
const populationFindings = function populationFindings(report: string, population: Population): Finding[] {
const sum = countsOf(population).reduce((total, [, count]) => total + count, 0);
return [
...(population.whole === 0 ? [{ file: report, message: emptyPopulation(population.name) }] : []),
...(sum === population.whole
? []
: [{ file: report, message: unbalancedPopulation(population.name, sum, population.whole) }]),
...absenceFindings(report, population),
];
};
const REPORT_CHECKS: readonly ReportCheck[] = [
reportCheck(["undeclared"], isUndeclared, (entries) =>
entries.map((entry) => ({ file: entry.face, message: undeclaredRelation(entry.face, entry.relation) })),
),
reportCheck(["ambiguous"], isUndeclared, (entries) =>
entries.map((entry) => ({ file: entry.face, message: ambiguousRelation(entry.face, entry.relation) })),
),
reportCheck(["unresolved"], isTarget, (targets) =>
targets.map((target) => ({
file: target.from,
message: unresolvedTarget(target.from, target.label, target.relation),
})),
),
reportCheck(["duplicates"], isDuplicate, (duplicates) =>
duplicates.map(({ dropped, kept }) => ({ file: kept.ref, message: duplicateNode(kept, dropped) })),
),
reportCheck(["uncovered"], isText, (refs) => refs.map((ref) => ({ file: ref, message: uncoveredSection(ref) }))),
reportCheck(["tooltipGaps"], isEdge, (gaps) => gaps.map((gap) => ({ file: gap.from, message: tooltipGap(gap) }))),
reportCheck(["populations"], isPopulation, (populations, report) =>
populations.flatMap((population) => populationFindings(report, population)),
),
reportCheck(["dangling"], isEdge, (edges) =>
edges.map((edge) => ({ file: edge.from, message: danglingEdge(edge) })),
),
reportCheck(["graph", "nodes"], isNode, duplicateNumbers),
];
export const graphFindings = function graphFindings(report: string): Finding[] {
if (!existsSync(report)) {
return [{ file: report, message: NO_GRAPH_REPORT }];
}
const parsed: unknown = JSON.parse(readFileSync(report, "utf8"));
return REPORT_CHECKS.flatMap((check) => check(report, parsed));
};