# shared/matchers/filename.matcher.ts

> 185 lines of code and 48 definitions.

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

Listed in [shared/matchers](https://banes-lab.com/api/source/governance/shared/matchers.md), before [shared/matchers/folder.matcher.ts](https://banes-lab.com/source/governance/shared/matchers/folder.matcher.ts.md).

## Definitions

- `resolveFile` (lexical_declaration, line 192, exported)
- `isParsed` (lexical_declaration, line 179, exported)
- `hasConcern` (lexical_declaration, line 183, exported)
- `slotError` (lexical_declaration, line 72)
- `isKebab` (lexical_declaration, line 31, exported)
- `parseDialect` (lexical_declaration, line 117, exported)
- `isKebabChar` (lexical_declaration, line 18)
- `hasKebabEdges` (lexical_declaration, line 24)
- `headSlotsOf` (lexical_declaration, line 99)
- `concernRunOf` (lexical_declaration, line 90)
- `parseFilename` (lexical_declaration, line 150, exported)
- `shapeError` (lexical_declaration, line 52)
- `filesWithConcern` (lexical_declaration, line 188, exported)
- `KEBAB_EXTRA` (lexical_declaration, line 14)
- `KEBAB_CASE` (lexical_declaration, line 16)
- `isLower` (lexical_declaration, line 19)
- `isDigit` (lexical_declaration, line 20)
- `at` (lexical_declaration, line 38, exported)
- `slotBounds` (lexical_declaration, line 46)
- `vocabulary` (lexical_declaration, line 47)
- `counts` (lexical_declaration, line 48)
- `shapes` (lexical_declaration, line 58)
- `{ max, min }` (lexical_declaration, line 59)
- `{ concern, subject, variant }` (lexical_declaration, line 76)
- `offending` (lexical_declaration, line 86)
- `whole` (lexical_declaration, line 103)
- `take` (lexical_declaration, line 107)
- `{ separator, splitters }` (lexical_declaration, line 118, exported)
- `cut` (lexical_declaration, line 123, exported)
- `stem` (lexical_declaration, line 124, exported)
- `words` (lexical_declaration, line 126, exported)
- `marker` (lexical_declaration, line 130, exported)
- `taken` (lexical_declaration, line 134, exported)
- `slots` (lexical_declaration, line 139, exported)
- `segments` (lexical_declaration, line 151, exported)
- `ext` (lexical_declaration, line 152, exported)
- `splitter` (lexical_declaration, line 153, exported)
- `concern` (lexical_declaration, line 157, exported)
- `shape` (lexical_declaration, line 162, exported)
- `head` (lexical_declaration, line 167, exported)
- `subject` (lexical_declaration, line 168, exported)
- `variant` (lexical_declaration, line 169, exported)
- `slot` (lexical_declaration, line 171, exported)
- `isExempt` (lexical_declaration, line 175, exported)
- `parsed` (lexical_declaration, line 184, exported)
- `matches` (lexical_declaration, line 193, exported)
- `basename` (lexical_declaration, line 194, exported)
- `[first]` (lexical_declaration, line 197, exported)

## Uses

- [shared/manifests/taxonomy.manifest.ts](https://banes-lab.com/source/governance/shared/manifests/taxonomy.manifest.ts.md)
- [shared/matchers/segment.matcher.ts](https://banes-lab.com/source/governance/shared/matchers/segment.matcher.ts.md)
- [shared/strings/taxonomy.strings.ts](https://banes-lab.com/source/governance/shared/strings/taxonomy.strings.ts.md)

## Used by

- [rules/eslint/closure-event-system-has-trajectory.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-event-system-has-trajectory.eslint.rule.ts.md)
- [rules/eslint/closure-no-undeclared-type-layer.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-no-undeclared-type-layer.eslint.rule.ts.md)
- [rules/eslint/require-self-registration.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/require-self-registration.eslint.rule.ts.md)
- [shared/analyzers/taxonomy.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/taxonomy.analyzer.ts.md)
- [shared/matchers/folder.matcher.ts](https://banes-lab.com/source/governance/shared/matchers/folder.matcher.ts.md)
- [shared/predicates/location.predicate.ts](https://banes-lab.com/source/governance/shared/predicates/location.predicate.ts.md)

## Records this file grounds

- [Dialect Resolution](https://banes-lab.com/records/algorithms/dialect-resolution.md)
- [Case Dialect](https://banes-lab.com/records/architecture/case-dialect.md)

## Source

```typescript
import type { ExemptName, ParseFailure, ParseResult, ParsedName } from "../../types/taxonomy.types.ts";
import { ambiguousCoordinate, noSplitter, unresolvedCoordinate } from "../strings/taxonomy.strings.ts";
import {
    isCompoundMarker,
    isConcern,
    isDeclaredSubject,
    isDeclaredVariant,
    isLegalSubject,
    splitterFor,
    vocabularyFor,
} from "../manifests/taxonomy.manifest.ts";
import { slotWordOf, wordsOf } from "./segment.matcher.ts";

const KEBAB_EXTRA = "-";

const KEBAB_CASE = "kebab";

const isKebabChar = function isKebabChar(char: string): boolean {
    const isLower = char >= "a" && char <= "z";
    const isDigit = char >= "0" && char <= "9";
    return isLower || isDigit || char === KEBAB_EXTRA;
};

const hasKebabEdges = function hasKebabEdges(word: string): boolean {
    if (word === "" || word.startsWith(KEBAB_EXTRA) || word.endsWith(KEBAB_EXTRA)) {
        return false;
    }
    return !word.includes(`${KEBAB_EXTRA}${KEBAB_EXTRA}`);
};

export const isKebab = function isKebab(word: string, root?: string): boolean {
    if (vocabularyFor(root).case !== KEBAB_CASE) {
        return true;
    }
    if (!hasKebabEdges(word)) {
        return false;
    }
    for (let at = 0; at < word.length; at += 1) {
        if (!isKebabChar(word.charAt(at))) {
            return false;
        }
    }
    return true;
};

const slotBounds = function slotBounds(root: string | undefined): { min: number; max: number } {
    const vocabulary = vocabularyFor(root);
    const counts = vocabulary.fileShapes.map((shape) => shape.split(vocabulary.separator).length);
    return { max: Math.max(...counts), min: Math.min(...counts) };
};

const shapeError = function shapeError(
    basename: string,
    segments: readonly string[],
    concern: string,
    root: string | undefined,
): ParseFailure | null {
    const shapes = vocabularyFor(root).fileShapes;
    const { max, min } = slotBounds(root);
    if (segments.length < min) {
        return { reason: `a governed file needs at least ${shapes[0] ?? ""}`, word: basename };
    }
    if (segments.length > max) {
        return { reason: `more slots than the grammar allows (${shapes.join(" | ")})`, word: basename };
    }
    if (!isConcern(concern, root)) {
        return { reason: "last segment before the extension is not a declared concern tag", word: concern };
    }
    return null;
};

const slotError = function slotError(
    slots: { subject: string; variant: string | null; concern: string },
    root: string | undefined,
): ParseFailure | null {
    const { concern, subject, variant } = slots;
    if (!isLegalSubject(subject, root)) {
        return { reason: "subject is neither a declared subject nor a concern tag", word: subject };
    }
    if (subject === concern) {
        return { reason: "subject must not equal concern; the subject is carrying no information", word: subject };
    }
    if (variant !== null && !isDeclaredVariant(variant, root) && !isDeclaredSubject(variant, root)) {
        return { reason: "variant is neither a declared variant nor a declared subject", word: variant };
    }
    const offending = (variant === null ? [subject] : [subject, variant]).find((slot) => !isKebab(slot, root));
    return offending === undefined ? null : { reason: `slot is not ${vocabularyFor(root).case}-case`, word: offending };
};

const concernRunOf = function concernRunOf(words: readonly string[], root: string | undefined): number {
    for (let take = words.length - 1; take >= 1; take -= 1) {
        if (isConcern(slotWordOf(words.slice(-take)), root)) {
            return take;
        }
    }
    return 0;
};

const headSlotsOf = function headSlotsOf(
    head: readonly string[],
    root: string | undefined,
): { subject: string; variant: string | null } | null {
    const whole = slotWordOf(head);
    if (isLegalSubject(whole, root)) {
        return { subject: whole, variant: null };
    }
    for (let take = head.length - 1; take >= 1; take -= 1) {
        const subject = slotWordOf(head.slice(0, take));
        const variant = slotWordOf(head.slice(take));
        if (isLegalSubject(subject, root) && (isDeclaredVariant(variant, root) || isDeclaredSubject(variant, root))) {
            return { subject, variant };
        }
    }
    return null;
};

export const parseDialect = function parseDialect(basename: string, splitterName: string, root?: string): ParseResult {
    const { separator, splitters } = vocabularyFor(root);
    const splitter = splitters.get(splitterName);
    if (splitter === undefined) {
        throw new Error(noSplitter(splitterName));
    }
    const cut = basename.lastIndexOf(separator);
    const stem = cut === -1 ? basename : basename.slice(0, cut);
    const ext = cut === -1 ? "" : basename.slice(cut + 1);
    const words = wordsOf(stem, splitter);
    if (words === null) {
        return { reason: `a ${splitterName} dialect file is one ${splitterName} word run`, word: basename };
    }
    const marker = words.at(-1) ?? "";
    if (isCompoundMarker(marker, root)) {
        return { exempt: true, marker };
    }
    const taken = concernRunOf(words, root);
    if (taken === 0) {
        return { reason: "the trailing words are not a declared concern tag", word: marker };
    }
    const concern = slotWordOf(words.slice(-taken));
    const slots = headSlotsOf(words.slice(0, -taken), root);
    if (slots === null) {
        const head = slotWordOf(words.slice(0, -taken));
        return {
            reason: "the leading words are neither a legal subject nor a subject and a declared variant",
            word: head,
        };
    }
    return slotError({ concern, ...slots }, root) ?? { concern, ext, ...slots };
};

export const parseFilename = function parseFilename(basename: string, root?: string): ParseResult {
    const segments = basename.split(vocabularyFor(root).separator);
    const ext = segments.at(-1) ?? "";
    const splitter = splitterFor(ext, root);
    if (splitter !== null) {
        return parseDialect(basename, splitter.name, root);
    }
    const concern = segments.at(-2) ?? "";

    if (isCompoundMarker(concern, root)) {
        return { exempt: true, marker: concern };
    }
    const shape = shapeError(basename, segments, concern, root);
    if (shape !== null) {
        return shape;
    }

    const head = segments.slice(0, -2);
    const subject = head[0] ?? "";
    const variant = head.length === 2 ? (head[1] ?? "") : null;

    const slot = slotError({ concern, subject, variant }, root);
    return slot ?? { concern, ext, subject, variant };
};

export const isExempt = function isExempt(result: ParseResult): result is ExemptName {
    return Object.hasOwn(result, "exempt");
};

export const isParsed = function isParsed(result: ParseResult): result is ParsedName {
    return Object.hasOwn(result, "concern");
};

export const hasConcern = function hasConcern(basename: string, tag: string): boolean {
    const parsed = parseFilename(basename);
    return isParsed(parsed) && parsed.concern === tag;
};

export const filesWithConcern = function filesWithConcern(tag: string, files: readonly string[]): string[] {
    return files.filter((file) => hasConcern(file.slice(file.lastIndexOf("/") + 1), tag));
};

export const resolveFile = function resolveFile(subject: string, tag: string, files: readonly string[]): string {
    const matches = filesWithConcern(tag, files).filter((file) => {
        const basename = file.slice(file.lastIndexOf("/") + 1);
        return basename.slice(0, basename.indexOf(".")) === subject;
    });
    const [first] = matches;
    if (first === undefined) {
        throw new Error(unresolvedCoordinate(subject, tag));
    }
    if (matches.length > 1) {
        throw new Error(ambiguousCoordinate(subject, tag, matches));
    }
    return first;
};
```
