core/converters/graph.ontology.converter.ts

core/converters/graph.ontology.converter.ts is a file in Bane's Lab Build Scripts. 159 lines of code and 27 definitions.

import type { FieldRule, OntologyGraph, Undeclared, Vocabulary } from "#types/graph.types";
import type { GraphEdge } from "@banes-lab/web/types/graph.types.js";
import { ONTOLOGY_LAYER } from "#configuration/constants/graph.constants";
import type { Population } from "#types/catalog.types";
import type { ReferenceFaces } from "#types/ontology.types";
import type { ReferenceRelation } from "@banes-lab/web/types/reference.types.js";
import { graphNode } from "#core/factories/graph.factory";

interface Source {
    readonly face: string;
    readonly kind: string;
    readonly ref: string;
}

type FieldOutcome = "ambiguous" | "emitted" | "inverse" | "undeclared";

interface Ruling {
    readonly outcome: FieldOutcome;
    readonly rule: FieldRule | null;
}

const UNDECLARED: Ruling = { outcome: "undeclared", rule: null };
const AMBIGUOUS: Ruling = { outcome: "ambiguous", rule: null };

type TargetOutcome = "emitted" | "unresolved" | "unruled";

interface Ruled {
    readonly key: string;
    readonly outcome: FieldOutcome;
    readonly relation: ReferenceRelation;
    readonly rule: FieldRule | null;
}

interface Rules {
    readonly overrides: ReadonlyMap<string, FieldRule>;
    readonly vocabulary: Vocabulary;
}

const rulingOf = function rulingOf(rule: FieldRule): Ruling {
    return { outcome: rule.relation === null ? "inverse" : "emitted", rule };
};

const overrideFor = function overrideFor(override: FieldRule, vocabulary: Vocabulary): Ruling {
    const declared = override.relation === null || vocabulary.pairs.some((pair) => pair.forward === override.relation);
    return declared ? rulingOf(override) : UNDECLARED;
};

const ruleFor = function ruleFor(source: Source, relation: string, rules: Rules): Ruling {
    const override =
        rules.overrides.get(`${source.face}:${source.kind}:${relation}`) ??
        rules.overrides.get(`${source.face}:${relation}`);
    if (override !== undefined) {
        return overrideFor(override, rules.vocabulary);
    }
    if (rules.vocabulary.pairs.some((pair) => pair.forward === relation)) {
        return rulingOf({ flip: false, relation });
    }
    const reversed = rules.vocabulary.pairs.filter((pair) => pair.reverse === relation);
    if (reversed.length === 1) {
        return rulingOf({ flip: true, relation: reversed[0]?.forward ?? null });
    }
    return reversed.length > 1 ? AMBIGUOUS : UNDECLARED;
};

const ruledFields = function ruledFields(
    source: Source,
    relations: readonly ReferenceRelation[],
    rules: Rules,
): readonly Ruled[] {
    return relations.map((relation) => {
        const ruling = ruleFor(source, relation.relation, rules);
        return {
            key: `${source.face}:${source.kind}:${relation.relation}`,
            outcome: ruling.outcome,
            relation,
            rule: ruling.rule,
        };
    });
};

const targetOutcome = function targetOutcome(held: Ruled, target: string | null): TargetOutcome {
    if (held.outcome !== "emitted") {
        return "unruled";
    }
    return target === null ? "unresolved" : "emitted";
};

const edgeOf = function edgeOf(ref: string, held: Ruled, target: string): GraphEdge {
    const forward = held.rule?.relation ?? "";
    return held.rule?.flip === true
        ? { from: target, relation: forward, to: ref }
        : { from: ref, relation: forward, to: target };
};

const tally = function tally(name: string, outcomes: readonly string[], parts: readonly string[]): Population {
    const counts = Object.fromEntries(parts.map((part) => [part, outcomes.filter((held) => held === part).length]));
    return { name, parts: counts, whole: outcomes.length };
};

const uniqueBy = function uniqueBy<T>(entries: readonly (readonly [string, T])[]): T[] {
    return [...new Map(entries).values()];
};

export const ontologyGraph = function ontologyGraph(
    faces: ReferenceFaces,
    vocabulary: Vocabulary,
    overrides: ReadonlyMap<string, FieldRule>,
): OntologyGraph {
    const records = [...faces].flatMap(([face, index]) =>
        Object.entries(index).map(([ref, record]) => ({ face, record, ref })),
    );
    const fields = records.flatMap((held) =>
        ruledFields({ face: held.face, kind: held.record.kind, ref: held.ref }, held.record.relations, {
            overrides,
            vocabulary,
        }).map((ruled) => ({ held, ruled })),
    );
    const targets = fields.flatMap(({ held, ruled }) =>
        ruled.relation.edges.map((edge) => ({ edge, held, outcome: targetOutcome(ruled, edge.ref), ruled })),
    );
    const named = function named(outcome: FieldOutcome): (readonly [string, Undeclared])[] {
        return fields
            .filter(({ ruled }) => ruled.outcome === outcome)
            .map(({ held, ruled }) => [
                ruled.key,
                { face: held.face, kind: held.record.kind, relation: ruled.relation.relation },
            ]);
    };
    return {
        ambiguous: uniqueBy(named("ambiguous")),
        edges: targets.flatMap(({ edge, held, outcome, ruled }) =>
            outcome === "emitted" && edge.ref !== null ? [edgeOf(held.ref, ruled, edge.ref)] : [],
        ),
        fields: uniqueBy(
            fields.flatMap(({ held, ruled }) =>
                ruled.rule === null
                    ? []
                    : [
                          [
                              ruled.key,
                              {
                                  face: held.face,
                                  flip: ruled.rule.flip,
                                  kind: held.record.kind,
                                  relation: ruled.relation.relation,
                                  stored: ruled.rule.relation,
                              },
                          ] as const,
                      ],
            ),
        ),
        nodes: records.map((held) => graphNode(held.ref, held.record.kind, ONTOLOGY_LAYER, held.record.name)),
        populations: [
            tally(
                "records",
                records.map(() => "read"),
                ["read"],
            ),
            tally(
                "relation fields",
                fields.map(({ ruled }) => ruled.outcome),
                ["emitted", "inverse", "ambiguous", "undeclared"],
            ),
            tally(
                "relation targets",
                targets.map(({ outcome }) => outcome),
                ["emitted", "unresolved", "unruled"],
            ),
        ],
        undeclared: uniqueBy(named("undeclared")),
        unresolved: targets
            .filter(({ outcome }) => outcome === "unresolved")
            .map(({ edge, held, ruled }) => ({ from: held.ref, label: edge.label, relation: ruled.relation.relation })),
    };
};