core/validators/reason.validator.ts

core/validators/reason.validator.ts is a file in GovLab Context. 146 lines of code and 30 definitions.

import {
    CELL_SEPARATOR,
    CONCEPT_COLLECTION_BY_AXIS,
    FREE_STEP_KIND,
    REASON_COLLECTION,
    SHAPE_SEPARATOR,
} from "#configuration/constants/reason.constants";
import { EVERY_STEP, TRANSITION_FROM, TRANSITION_GATE, TRANSITION_TO } from "#configuration/strings/reason.strings";
import { REASON_KINDS, kindMembersOf } from "#core/selectors/reason.selector";
import type { ReasonData, ReasonOntologyIssues } from "#types/reason.types";
import { danglingFields, emptyFields } from "#core/validators/field.validator";
import { REASON_SCHEMA } from "#configuration/schemas/reason.schema";
import type { ReasonNode } from "#types/reason.node.types";

type Issues = Omit<ReasonOntologyIssues, "total">;

const TARGET_PREFIX = `${REASON_COLLECTION}:`;

const byName = function byName(a: string, b: string): number {
    return a.localeCompare(b);
};

const idSet = function idSet(records: readonly { id: string }[]): Set<string> {
    return new Set(records.map((record) => record.id));
};

const duplicateIdsOf = function duplicateIdsOf(data: ReasonData): string[] {
    return [...REASON_KINDS].flatMap(([kind, recordsOf]) => {
        const seen = new Set<string>();
        return recordsOf(data).flatMap((record) => {
            const duplicate = seen.has(record.id);
            seen.add(record.id);
            return duplicate ? [`${kind}:${record.id}`] : [];
        });
    });
};

const danglingFieldsOf = function danglingFieldsOf(data: ReasonData): Issues["danglingFields"] {
    const members = kindMembersOf(data);
    const membersOf = (target: string): ReadonlySet<string> | undefined =>
        target.startsWith(TARGET_PREFIX) ? members.get(target.slice(TARGET_PREFIX.length)) : undefined;
    return [...REASON_KINDS].flatMap(([kind, recordsOf]) => {
        const schema = REASON_SCHEMA.get(kind);
        return schema === undefined ? [] : danglingFields(kind, schema, recordsOf(data), membersOf);
    });
};

const emptyFieldsIn = function emptyFieldsIn(data: ReasonData): Issues["emptyFields"] {
    return [...REASON_KINDS].flatMap(([kind, recordsOf]) => {
        const schema = REASON_SCHEMA.get(kind);
        return schema === undefined
            ? []
            : recordsOf(data).flatMap((record) =>
                  emptyFields(schema, record).map((field) => ({ field, id: record.id, kind })),
              );
    });
};

const conceptDangling = function conceptDangling(data: ReasonData): Issues["danglingConcepts"] {
    const concepts = new Map<string, Set<string>>([
        ["dimension", idSet(data.dimensions)],
        ["lens", idSet(data.lenses)],
        ["mode", idSet(data.modes)],
        ["representation", idSet(data.representations)],
    ]);
    const resolves = (node: ReasonNode): boolean => {
        const kind = CONCEPT_COLLECTION_BY_AXIS.get(node.axis);
        return kind !== undefined && (concepts.get(kind)?.has(node.concept ?? "") ?? false);
    };
    return data.nodes
        .filter((node) => typeof node.concept === "string" && !resolves(node))
        .map((node) => ({ concept: node.concept ?? "", node: node.id }));
};

const transitionDangling = function transitionDangling(
    data: ReasonData,
    stageIds: Set<string>,
    nodeIds: Set<string>,
): Issues["danglingTransitions"] {
    return data.derivationLoop.transitions.flatMap((transition) =>
        [
            stageIds.has(transition.from) ? null : TRANSITION_FROM,
            stageIds.has(transition.to) ? null : TRANSITION_TO,
            typeof transition.gate === "string" && !nodeIds.has(transition.gate) ? TRANSITION_GATE : null,
        ].flatMap((reason) => (reason === null ? [] : [{ from: transition.from, reason, to: transition.to }])),
    );
};

const edgeSourceDangling = function edgeSourceDangling(
    data: ReasonData,
    nodeIds: Set<string>,
    stageIds: Set<string>,
): string[] {
    return data.edges
        .filter((edge) => !nodeIds.has(edge.from) && !stageIds.has(edge.from) && edge.from !== data.derivationLoop.id)
        .map((edge) => edge.from)
        .toSorted(byName);
};

const collidingSurfaceCells = function collidingSurfaceCells(data: ReasonData): Issues["collidingSurfaceCells"] {
    const byCell = new Map<string, string[]>();
    for (const surface of data.testSurfaces) {
        const cell = `${surface.dimension}${CELL_SEPARATOR}${surface.lens}`;
        byCell.set(cell, [...(byCell.get(cell) ?? []), surface.id]);
    }
    return [...byCell]
        .filter(([, surfaces]) => surfaces.length > 1)
        .map(([cell, surfaces]) => ({ cell, surfaces: surfaces.toSorted(byName) }));
};

const shapesOf = function shapesOf(yieldsShape: string): Set<string> {
    return new Set(
        yieldsShape
            .split(SHAPE_SEPARATOR)
            .map((shape) => shape.trim())
            .filter((shape) => shape.length > 0),
    );
};

const answerShapeMismatches = function answerShapeMismatches(data: ReasonData): Issues["answerShapeMismatches"] {
    const yields = new Map(data.mathTypes.map((mathType) => [mathType.id, mathType.yieldsShape]));
    return data.nodes.flatMap((node) => {
        const allowed = yields.get(node.mathType) ?? "";
        return node.answerShape === undefined || shapesOf(allowed).has(node.answerShape)
            ? []
            : [{ allowed, answerShape: node.answerShape, node: node.id }];
    });
};

const unresolvedModelSteps = function unresolvedModelSteps(data: ReasonData): Issues["unresolvedModelSteps"] {
    const members = kindMembersOf(data);
    return data.models.flatMap((model) => {
        if (model.stepKind === FREE_STEP_KIND) {
            return [];
        }
        const kind = members.get(model.stepKind);
        if (kind === undefined) {
            return [{ model: model.id, step: EVERY_STEP, stepKind: model.stepKind }];
        }
        return model.sequence
            .filter((step) => !kind.has(step))
            .map((step) => ({ model: model.id, step, stepKind: model.stepKind }));
    });
};

export const validateReason = function validateReason(data: ReasonData): ReasonOntologyIssues {
    const nodeIds = idSet(data.nodes);
    const stageIds = new Set(data.derivationLoop.stages.map((stage) => stage.id));
    const parts: Issues = {
        answerShapeMismatches: answerShapeMismatches(data),
        collidingSurfaceCells: collidingSurfaceCells(data),
        danglingConcepts: conceptDangling(data),
        danglingEdgeSources: edgeSourceDangling(data, nodeIds, stageIds),
        danglingFields: danglingFieldsOf(data),
        danglingTransitions: transitionDangling(data, stageIds, nodeIds),
        duplicateIds: duplicateIdsOf(data),
        emptyFields: emptyFieldsIn(data),
        unresolvedModelSteps: unresolvedModelSteps(data),
    };
    return { ...parts, total: Object.values(parts).reduce((count, findings) => count + findings.length, 0) };
};