core/analyzers/taxonomy.analyzer.ts

core/analyzers/taxonomy.analyzer.ts is a file in GovLab Stats. 151 lines of code and 28 definitions.

import type {
    AssessedFile,
    ContainerStat,
    RootScan,
    TaxonomyRootStats,
    TaxonomyStats,
    UngovernedArea,
} from "#types/taxonomy.types";
import {
    containersFor,
    governedRoots,
    isIgnoredName,
    isSpecialContainer,
    layerFor,
    rootFor,
    vocabularyFor,
} from "@ssot/govlab/shared/manifests/taxonomy.manifest.ts";
import { extensionOf, posixOf } from "#core/selectors/source.selector";
import { isAuthoredRole, roleOf } from "#core/classifiers/source.classifier";
import { isExempt, isParsed, parseFilename } from "@ssot/govlab/shared/matchers/filename.matcher.ts";
import { memberIndex, ownerOf } from "#core/resolvers/package.resolver";
import type { PathExclusion } from "@govlab/quality/config";
import { isGeneratedPath } from "#core/predicates/source.predicate";
import path from "node:path";
import { vocabularyRows } from "#core/converters/taxonomy.converter";
import { walkFiles } from "#core/loaders/folder.loader";

const isAssessable = function isAssessable(abs: string, root?: string): boolean {
    const name = path.basename(abs);
    const role = roleOf(extensionOf(name));
    return !isIgnoredName(name, root) && isAuthoredRole(role) && !isGeneratedPath(abs, abs);
};

const bump = function bump<K>(counts: Map<K, number>, key: K): void {
    counts.set(key, (counts.get(key) ?? 0) + 1);
};

const assessedUnder = function assessedUnder(
    workspaceRoot: string,
    root: string,
    ignore: PathExclusion,
): AssessedFile[] {
    const base = path.join(workspaceRoot, root);
    return walkFiles(base, ignore)
        .filter((abs) => rootFor(posixOf(path.relative(workspaceRoot, abs))) === root && isAssessable(abs, root))
        .map((abs) => {
            const segments = path.relative(base, abs).split(path.sep);
            return { abs, folders: segments.slice(0, -1), name: segments.at(-1) ?? "" };
        });
};

const scanRoot = function scanRoot(workspaceRoot: string, root: string, ignore: PathExclusion): RootScan {
    const vocabulary = vocabularyFor(root);
    const scan: RootScan = {
        layerHits: new Map(),
        stats: {
            assessed: 0,
            conformant: 0,
            containers: [],
            declared: containersFor(root).length,
            depth: new Map(),
            overCap: 0,
            present: 0,
            root,
        },
        usedConcerns: new Set(),
        usedSubjects: new Set(),
        usedVariants: new Set(),
    };
    const perContainer = new Map<string, number>();
    for (const file of assessedUnder(workspaceRoot, root, ignore)) {
        scan.stats.assessed += 1;
        bump(scan.stats.depth, file.folders.length);
        scan.stats.overCap += file.folders.length > vocabulary.maxDepth ? 1 : 0;
        const [container] = file.folders;
        if (container !== undefined) {
            bump(perContainer, container);
        }
        const parsed = parseFilename(file.name, root);
        scan.stats.conformant += isParsed(parsed) || isExempt(parsed) ? 1 : 0;
        if (isParsed(parsed)) {
            scan.usedConcerns.add(parsed.concern);
            scan.usedSubjects.add(parsed.subject);
            if (parsed.variant !== null) {
                scan.usedVariants.add(parsed.variant);
            }
            bump(scan.layerHits, layerFor(parsed.concern, root) ?? "");
        }
    }
    scan.stats.containers = [...perContainer.entries()]
        .map(([name, files]): ContainerStat => ({
            files,
            kind: isSpecialContainer(root, name) ? "bucket" : "declared",
            name,
        }))
        .toSorted((a, b) => b.files - a.files);
    scan.stats.present = scan.stats.containers.length;
    return scan;
};

const isClaimed = function isClaimed(rel: string, claims: readonly string[]): boolean {
    return claims.some((claim) => rel === claim || rel.startsWith(`${claim}/`));
};

const collectUngoverned = function collectUngoverned(
    workspaceRoot: string,
    ignore: PathExclusion,
    claims: readonly string[],
): UngovernedArea[] {
    const members = memberIndex(workspaceRoot);
    const counts = new Map<string, number>();
    for (const abs of walkFiles(workspaceRoot, ignore)) {
        const rel = posixOf(path.relative(workspaceRoot, abs));
        if (rootFor(rel) === undefined && !isClaimed(rel, claims) && isAssessable(abs)) {
            bump(counts, ownerOf(members, rel) ?? rel.split("/").at(0) ?? "");
        }
    }
    return [...counts.entries()].map(([area, files]) => ({ area, files })).toSorted((a, b) => b.files - a.files);
};

const sumOf = function sumOf(roots: readonly TaxonomyRootStats[], pick: (root: TaxonomyRootStats) => number): number {
    return roots.reduce((total, root) => total + pick(root), 0);
};

export const collectTaxonomy = function collectTaxonomy(
    workspaceRoot: string,
    ignore: PathExclusion,
    claims: readonly string[],
): TaxonomyStats {
    const host = vocabularyFor();
    const scans = governedRoots().map((root) => scanRoot(workspaceRoot, root, ignore));
    const roots = scans.map((scan) => scan.stats).toSorted((a, b) => b.assessed - a.assessed);
    const layerTotals = new Map<string, number>();
    for (const [layer, hits] of scans.flatMap((scan) => [...scan.layerHits])) {
        layerTotals.set(layer, (layerTotals.get(layer) ?? 0) + hits);
    }
    const layers = [...new Set([...host.byTag.values()].map((concern) => concern.layer))];
    const ungoverned = collectUngoverned(workspaceRoot, ignore, claims);
    const declared = sumOf(roots, (root) => root.declared);
    const present = sumOf(roots, (root) => root.present);
    return {
        layers: layers
            .map((layer) => ({ files: layerTotals.get(layer) ?? 0, layer }))
            .toSorted((a, b) => b.files - a.files),
        maxDepth: host.maxDepth,
        roots,
        totals: {
            assessed: sumOf(roots, (root) => root.assessed),
            atRoot: sumOf(roots, (root) => root.depth.get(0) ?? 0),
            conformant: sumOf(roots, (root) => root.conformant),
            declared,
            overCap: sumOf(roots, (root) => root.overCap),
            present,
        },
        ungoverned,
        ungovernedFiles: ungoverned.reduce((sum, area) => sum + area.files, 0),
        vocabulary: vocabularyRows(host, scans, { declared, layerTotals, layers: layers.length, present }),
    };
};