# tools/core/validators/channel.validator.ts

> 81 lines of code and 19 definitions.

Tree: Coordination tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-core-validators-channel-validator-ts
Source text: https://banes-lab.com/assets/sources/source.81fc03b8d943441167ec1298357751d52685fa204a4d8d0d3ccb641f8564e2e1.generated.txt

## Definitions

- `channelsIn` (lexical_declaration, line 36, exported)
- `channelOf` (lexical_declaration, line 30)
- `misnamedChannels` (lexical_declaration, line 61, exported)
- `collidingScopes` (lexical_declaration, line 74, exported)
- `isChannelBody` (lexical_declaration, line 18)
- `declaredScope` (lexical_declaration, line 25)
- `removeChannel` (lexical_declaration, line 98, exported)
- `staleChannels` (lexical_declaration, line 41, exported)
- `Channel` (interface_declaration, line 8, exported)
- `StaleChannel` (interface_declaration, line 14, exported)
- `value` (lexical_declaration, line 26)
- `scope` (lexical_declaration, line 31)
- `parsed` (lexical_declaration, line 32)
- `dir` (lexical_declaration, line 37, exported)
- `NamingBreach` (interface_declaration, line 50, exported)
- `ScopeCollision` (interface_declaration, line 56, exported)
- `byScope` (lexical_declaration, line 75, exported)
- `held` (lexical_declaration, line 82, exported)
- `out` (lexical_declaration, line 87, exported)

## Uses

- [tools/core/resolvers/checklist.resolver.ts](https://banes-lab.com/source/coordination/tools/core/resolvers/checklist.resolver.ts.md)

## Used by

- [tools/rules/channel.rule.ts](https://banes-lab.com/source/coordination/tools/rules/channel.rule.ts.md)
- [tools/rules/governance.rule.ts](https://banes-lab.com/source/coordination/tools/rules/governance.rule.ts.md)

## Source

```typescript
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
import { fieldOf, tryParse } from "../readers/json.reader.ts";
import { GENERATED_DIR } from "../constants/path.constants.ts";

import { channelScopeOf } from "../reporters/rule.reporter.ts";
import { resolve } from "node:path";

export interface Channel {
    readonly name: string;
    readonly scope: string;
    readonly declared: string | null;
}

export interface StaleChannel extends Channel {
    readonly removable: boolean;
}

const isChannelBody = function isChannelBody(parsed: unknown): boolean {
    if (typeof parsed !== "object" || parsed === null) {
        return false;
    }
    return fieldOf(parsed, "tool") === "govern" && fieldOf(parsed, "authoritative") === false;
};

const declaredScope = function declaredScope(parsed: unknown): string | null {
    const value = typeof parsed === "object" && parsed !== null ? fieldOf(parsed, "scope") : null;
    return typeof value === "string" && value.length > 0 ? value : null;
};

const channelOf = function channelOf(dir: string, entry: string): Channel[] {
    const scope = channelScopeOf(entry);
    const parsed = scope === null ? null : tryParse(readFileSync(resolve(dir, entry), "utf8"))?.value;
    return scope !== null && isChannelBody(parsed) ? [{ declared: declaredScope(parsed), name: entry, scope }] : [];
};

export const channelsIn = function channelsIn(repoRoot: string): Channel[] {
    const dir = resolve(repoRoot, GENERATED_DIR);
    return existsSync(dir) ? readdirSync(dir).flatMap((entry) => channelOf(dir, entry)) : [];
};

export const staleChannels = function staleChannels(
    repoRoot: string,
    resolves: (scope: string) => boolean,
): StaleChannel[] {
    return channelsIn(repoRoot)
        .filter((channel) => !resolves(channel.scope))
        .map((channel) => ({ ...channel, removable: true }));
};

export interface NamingBreach {
    readonly name: string;
    readonly named: string;
    readonly declared: string;
}

export interface ScopeCollision {
    readonly scope: string;
    readonly names: readonly string[];
}

export const misnamedChannels = function misnamedChannels(repoRoot: string): NamingBreach[] {
    const out: NamingBreach[] = [];

    for (const channel of channelsIn(repoRoot)) {
        if (channel.declared === null || channel.declared === channel.scope) {
            continue;
        }
        out.push({ declared: channel.declared, name: channel.name, named: channel.scope });
    }

    return out;
};

export const collidingScopes = function collidingScopes(repoRoot: string): ScopeCollision[] {
    const byScope = new Map<string, string[]>();

    for (const channel of channelsIn(repoRoot)) {
        if (channel.declared === null) {
            continue;
        }

        const held = byScope.get(channel.declared) ?? [];
        held.push(channel.name);
        byScope.set(channel.declared, held);
    }

    const out: ScopeCollision[] = [];
    for (const [scope, names] of byScope) {
        if (names.length < 2) {
            continue;
        }
        out.push({ names: names.toSorted((left, right) => left.localeCompare(right, "en")), scope });
    }

    return out;
};

export const removeChannel = function removeChannel(repoRoot: string, name: string): void {
    rmSync(resolve(repoRoot, GENERATED_DIR, name), { force: true });
};
```
