# shared/analyzers/taxonomy.tree.analyzer.ts

> 133 lines of code and 37 definitions.

Tree: Governance tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/governance#file-governance-shared-analyzers-taxonomy-tree-analyzer-ts
Source text: https://banes-lab.com/source/governance/shared/analyzers/taxonomy.tree.analyzer.ts.txt

Listed in [shared/analyzers](https://banes-lab.com/api/source/governance/shared/analyzers.md), after [shared/analyzers/taxonomy.coverage.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/taxonomy.coverage.analyzer.ts.md).

## Definitions

- `at` (lexical_declaration, line 64)
- `fileFindings` (lexical_declaration, line 68)
- `generatedFolderFindings` (lexical_declaration, line 52, exported)
- `bucketFindings` (lexical_declaration, line 79)
- `textFilesUnder` (lexical_declaration, line 39)
- `carriesMarker` (lexical_declaration, line 47)
- `containerFindings` (lexical_declaration, line 89)
- `PATH_SEPARATOR` (lexical_declaration, line 23)
- `Children` (interface_declaration, line 25)
- `childrenOf` (lexical_declaration, line 30)
- `absDir` (lexical_declaration, line 31)
- `named` (lexical_declaration, line 32)
- `{ dirs, files }` (lexical_declaration, line 40)
- `own` (lexical_declaration, line 41)
- `segments` (lexical_declaration, line 48)
- `vocabulary` (lexical_declaration, line 53, exported)
- `marker` (lexical_declaration, line 54, exported)
- `below` (lexical_declaration, line 69)
- `basename` (lexical_declaration, line 70)
- `folders` (lexical_declaration, line 71)
- `placement` (lexical_declaration, line 75)
- `relContainer` (lexical_declaration, line 80)
- `nested` (lexical_declaration, line 81)
- `data` (lexical_declaration, line 95)
- `direct` (lexical_declaration, line 99)
- `rootReport` (lexical_declaration, line 107)
- `children` (lexical_declaration, line 111)
- `outputs` (lexical_declaration, line 112)
- `dirs` (lexical_declaration, line 113)
- `missing` (lexical_declaration, line 116)
- `loose` (lexical_declaration, line 123)
- `perContainer` (lexical_declaration, line 130)
- `findings` (lexical_declaration, line 131)
- `generated` (lexical_declaration, line 132)
- `assessed` (lexical_declaration, line 133)
- `taxonomyReport` (lexical_declaration, line 137, exported)
- `reports` (lexical_declaration, line 138, exported)

## Uses

- [shared/analyzers/taxonomy.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/taxonomy.analyzer.ts.md)
- [shared/manifests/taxonomy.manifest.ts](https://banes-lab.com/source/governance/shared/manifests/taxonomy.manifest.ts.md)

## Used by

- [shared/analyzers/taxonomy.coverage.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/taxonomy.coverage.analyzer.ts.md)

## Source

```typescript
import type { PlacementFinding, TaxonomyFinding, TaxonomyReport } from "../../types/taxonomy.types.ts";
import {
    containersFor,
    fixtureMarkerOf,
    isDeclaredContainer,
    isForeignContainer,
    isGeneratedFolder,
    isIgnoredName,
    isNameExempt,
    isNestedRoot,
    isSpecialContainer,
    isTestRoot,
    taxonomyRoots,
    testMarkerOf,
    vocabularyFor,
} from "../manifests/taxonomy.manifest.ts";
import { existsSync, readdirSync, statSync } from "node:fs";
import { namingFinding, placementFinding, testFinding } from "./taxonomy.analyzer.ts";
import { WORKSPACE_ROOT } from "../resolvers/anchor.resolver.ts";
import { isTextFile } from "../predicates/text.predicate.ts";
import { join } from "node:path";

const PATH_SEPARATOR = "/";

interface Children {
    dirs: string[];
    files: string[];
}

const childrenOf = function childrenOf(relDir: string, root?: string): Children {
    const absDir = join(WORKSPACE_ROOT, relDir);
    const named = readdirSync(absDir)
        .filter((name) => !isIgnoredName(name, root))
        .toSorted((a, b) => a.localeCompare(b));
    const dirs = named.filter((name) => statSync(join(absDir, name)).isDirectory());
    return { dirs, files: named.filter((name) => !dirs.includes(name)) };
};

const textFilesUnder = function textFilesUnder(relDir: string, root?: string): string[] {
    const { dirs, files } = childrenOf(relDir, root);
    const own = files
        .map((name) => `${relDir}${PATH_SEPARATOR}${name}`)
        .filter((path) => isTextFile(join(WORKSPACE_ROOT, path)));
    return [...own, ...dirs.flatMap((name) => textFilesUnder(`${relDir}${PATH_SEPARATOR}${name}`, root))];
};

const carriesMarker = function carriesMarker(basename: string, marker: string, separator: string): boolean {
    const segments = basename.split(separator);
    return segments.length > 2 && segments.at(-2) === marker;
};

export const generatedFolderFindings = function generatedFolderFindings(relFolder: string, root?: string): TaxonomyFinding[] {
    const vocabulary = vocabularyFor(root);
    const marker = vocabulary.generatedFolder?.marker ?? "";
    return textFilesUnder(relFolder, root)
        .filter((path) => !carriesMarker(path.slice(path.lastIndexOf(PATH_SEPARATOR) + 1), marker, vocabulary.separator))
        .map((path): TaxonomyFinding => ({
            data: { folder: relFolder, marker },
            messageId: "generatedFolderIntruder",
            path,
        }));
};

const at = function at(path: string, finding: PlacementFinding | null): TaxonomyFinding[] {
    return finding === null ? [] : [{ ...finding, path }];
};

const fileFindings = function fileFindings(path: string, root: string, special: boolean): TaxonomyFinding[] {
    const below = path.slice(root.length + 1).split(PATH_SEPARATOR);
    const basename = below.at(-1) ?? "";
    const folders = below.slice(0, -1);
    if (isTestRoot(root) || testMarkerOf(basename) !== undefined || fixtureMarkerOf(basename, root) !== undefined) {
        return at(path, testFinding(basename, folders, root));
    }
    const placement = special ? null : placementFinding(basename, folders, root);
    return [...at(path, placement), ...at(path, namingFinding(basename, folders, root))];
};

const bucketFindings = function bucketFindings(root: string, container: string, files: string[]): TaxonomyFinding[] {
    const relContainer = `${root}${PATH_SEPARATOR}${container}`;
    const nested = childrenOf(relContainer, root).dirs.map((name): TaxonomyFinding => ({
        data: { container, nested: name, root },
        messageId: "nestedInSpecial",
        path: `${relContainer}${PATH_SEPARATOR}${name}`,
    }));
    return [...nested, ...files.flatMap((path) => fileFindings(path, root, true))];
};

const containerFindings = function containerFindings(
    root: string,
    container: string,
    files: string[],
): TaxonomyFinding[] {
    if (!isDeclaredContainer(root, container)) {
        const data = { container, declared: containersFor(root).join(", "), root };
        return [{ data, messageId: "undeclaredContainer", path: `${root}${PATH_SEPARATOR}${container}` }];
    }
    if (isSpecialContainer(root, container)) {
        const direct = files.filter(
            (path) => path.split(PATH_SEPARATOR).length === root.split(PATH_SEPARATOR).length + 2,
        );
        return bucketFindings(root, container, direct);
    }
    return files.flatMap((path) => fileFindings(path, root, false));
};

const rootReport = function rootReport(root: string): TaxonomyReport {
    if (!existsSync(join(WORKSPACE_ROOT, root))) {
        return { assessed: 0, findings: [{ data: { root }, messageId: "missingRoot", path: root }] };
    }
    const children = childrenOf(root, root);
    const outputs = children.dirs.filter((name) => isGeneratedFolder(name, root));
    const dirs = children.dirs.filter(
        (name) => !isNestedRoot(root, name) && !isForeignContainer(root, name) && !outputs.includes(name),
    );
    const missing = containersFor(root)
        .filter((name) => !dirs.includes(name) && !isTestRoot(root))
        .map((container): TaxonomyFinding => ({
            data: { container, root },
            messageId: "missingContainer",
            path: root,
        }));
    const loose = children.files
        .filter((name) => !isNameExempt(name, root) && isTextFile(join(WORKSPACE_ROOT, root, name)))
        .map((name): TaxonomyFinding => ({
            data: { name, root },
            messageId: "looseFileAtRoot",
            path: `${root}${PATH_SEPARATOR}${name}`,
        }));
    const perContainer = dirs.map((name) => textFilesUnder(`${root}${PATH_SEPARATOR}${name}`, root));
    const findings = dirs.flatMap((name, index) => containerFindings(root, name, perContainer[index] ?? []));
    const generated = outputs.flatMap((name) => generatedFolderFindings(`${root}${PATH_SEPARATOR}${name}`, root));
    const assessed = loose.length + perContainer.reduce((sum, files) => sum + files.length, 0);
    return { assessed, findings: [...missing, ...loose, ...findings, ...generated] };
};

export const taxonomyReport = function taxonomyReport(): TaxonomyReport {
    const reports = taxonomyRoots()
        .toSorted((a, b) => a.localeCompare(b))
        .map(rootReport);
    return {
        assessed: reports.reduce((sum, report) => sum + report.assessed, 0),
        findings: reports.flatMap((report) => report.findings),
    };
};
```
