# core/validators/reason.validator.ts

> 146 lines of code and 30 definitions.

Tree: GovLab Context
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/context#file-context-core-validators-reason-validator-ts
Source text: https://banes-lab.com/source/context/core/validators/reason.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/context/core/validators.md), after [core/validators/reason.reference.validator.ts](https://banes-lab.com/source/context/core/validators/reason.reference.validator.ts.md) and before [core/validators/record.validator.ts](https://banes-lab.com/source/context/core/validators/record.validator.ts.md).

## Definitions

- `resolves` (lexical_declaration, line 66)
- `shapesOf` (lexical_declaration, line 111)
- `validateReason` (lexical_declaration, line 146, exported)
- `danglingFieldsOf` (lexical_declaration, line 38)
- `emptyFieldsIn` (lexical_declaration, line 48)
- `conceptDangling` (lexical_declaration, line 59)
- `answerShapeMismatches` (lexical_declaration, line 120)
- `Issues` (type_alias_declaration, line 15)
- `TARGET_PREFIX` (lexical_declaration, line 17)
- `byName` (lexical_declaration, line 19)
- `idSet` (lexical_declaration, line 23)
- `duplicateIdsOf` (lexical_declaration, line 27)
- `seen` (lexical_declaration, line 29)
- `duplicate` (lexical_declaration, line 31)
- `membersOf` (lexical_declaration, line 40)
- `schema` (lexical_declaration, line 50)
- `concepts` (lexical_declaration, line 60)
- `transitionDangling` (lexical_declaration, line 75)
- `edgeSourceDangling` (lexical_declaration, line 89)
- `collidingSurfaceCells` (lexical_declaration, line 100)
- `byCell` (lexical_declaration, line 101)
- `cell` (lexical_declaration, line 103)
- `yields` (lexical_declaration, line 121)
- `allowed` (lexical_declaration, line 123)
- `unresolvedModelSteps` (lexical_declaration, line 130)
- `members` (lexical_declaration, line 131)
- `kind` (lexical_declaration, line 136)
- `nodeIds` (lexical_declaration, line 147, exported)
- `stageIds` (lexical_declaration, line 148, exported)
- `parts` (lexical_declaration, line 149, exported)

## Contained in

- [core/validators](https://banes-lab.com/anatomy/context/folder-context-core-validators.md)

## Uses

- [core/validators/field.validator.ts](https://banes-lab.com/source/context/core/validators/field.validator.ts.md)

## Used by

- [core/factories/reason.factory.ts](https://banes-lab.com/source/context/core/factories/reason.factory.ts.md)

## Linked from

- [core/factories](https://banes-lab.com/anatomy/context/folder-context-core-factories.md)
- [core/validators](https://banes-lab.com/anatomy/context/folder-context-core-validators.md)

## Source

```typescript
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) };
};
```
