# tools/rules/vocabulary.rule.ts

> 237 lines of code and 48 definitions.

Tree: Coordination tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-rules-vocabulary-rule-ts
Source text: https://banes-lab.com/assets/sources/source.3207a2c9ed8373197bbebec453ed27841f2b37ddc852e5281a76ad607083214a.generated.txt

## Definitions

- `declaredRow` (lexical_declaration, line 178)
- `definesTheAxis` (lexical_declaration, line 158)
- `words` (lexical_declaration, line 64)
- `columnsOf` (lexical_declaration, line 103)
- `statesTheSet` (lexical_declaration, line 120)
- `phrases` (lexical_declaration, line 137)
- `declaredIn` (lexical_declaration, line 187)
- `axisNamedBy` (lexical_declaration, line 89)
- `MARKDOWN` (lexical_declaration, line 5)
- `CELL` (lexical_declaration, line 7)
- `CLOSED` (lexical_declaration, line 9)
- `Declared` (interface_declaration, line 15)
- `MARKUP` (lexical_declaration, line 21)
- `bare` (lexical_declaration, line 23)
- `start` (lexical_declaration, line 24)
- `end` (lexical_declaration, line 25)
- `SEPARATORS` (lexical_declaration, line 37)
- `tokens` (lexical_declaration, line 39)
- `token` (lexical_declaration, line 48)
- `last` (lexical_declaration, line 55)
- `WORD_BREAK` (lexical_declaration, line 62)
- `AXIS_COLUMN` (lexical_declaration, line 85)
- `VALUE_COLUMN` (lexical_declaration, line 87)
- `Columns` (interface_declaration, line 98)
- `at` (lexical_declaration, line 107)
- `label` (lexical_declaration, line 108)
- `closed` (lexical_declaration, line 121)
- `parts` (lexical_declaration, line 122)
- `SPACING` (lexical_declaration, line 135)
- `held` (lexical_declaration, line 139)
- `cells` (lexical_declaration, line 162)
- `trimmed` (lexical_declaration, line 163)
- `out` (lexical_declaration, line 168)
- `cell` (lexical_declaration, line 170)
- `axis` (lexical_declaration, line 179)
- `value` (lexical_declaration, line 180)
- `found` (lexical_declaration, line 188)
- `columns` (lexical_declaration, line 189)
- `row` (lexical_declaration, line 192)
- `OffVocabulary` (interface_declaration, line 205)
- `offVocabularyFinding` (lexical_declaration, line 211)
- `rule` (lexical_declaration, line 237, exported)
- `check` (method_definition, line 238, exported)
- `declaring` (lexical_declaration, line 239, exported)
- `walked` (lexical_declaration, line 243, exported)
- `off` (lexical_declaration, line 245, exported)
- `offVocabulary` (lexical_declaration, line 250, exported)
- `findings` (lexical_declaration, line 251, exported)

## Source

```typescript
import type { RuleContext, RuleDeclaration, RuleResult } from "../core/types/rule.types.ts";
import type { Finding } from "../core/types/segment.types.ts";
import { lifetime } from "../../config/surface.config.ts";

const MARKDOWN = ".md";

const CELL = "|";

const CLOSED: Readonly<Record<string, readonly string[]>> = {
    mutability: lifetime.values.mutability,
    removal: lifetime.values.removal,
    retention: lifetime.values.retention,
};

interface Declared {
    readonly axis: string;
    readonly value: string;
    readonly line: number;
}

const MARKUP = new Set(["`", "*", "_", " "]);

const bare = function bare(cell: string): string {
    let start = 0;
    let end = cell.length;

    while (start < end && MARKUP.has(cell[start] ?? "")) {
        start += 1;
    }
    while (end > start && MARKUP.has(cell[end - 1] ?? "")) {
        end -= 1;
    }

    return cell.slice(start, end).toLowerCase();
};

const SEPARATORS = new Set(["·", ",", "/"]);

const tokens = function tokens(value: string): string[] {
    const out: string[] = [];
    let held = "";

    for (const char of value) {
        if (!SEPARATORS.has(char)) {
            held += char;
            continue;
        }
        const token = bare(held);
        if (token.length > 0) {
            out.push(token);
        }
        held = "";
    }

    const last = bare(held);
    if (last.length > 0) {
        out.push(last);
    }
    return out;
};

const WORD_BREAK = new Set([" ", "\t", "-"]);

const words = function words(text: string): string[] {
    const out: string[] = [];
    let held = "";

    for (const char of text) {
        if (!WORD_BREAK.has(char)) {
            held += char;
            continue;
        }
        if (held.length > 0) {
            out.push(held);
        }
        held = "";
    }

    if (held.length > 0) {
        out.push(held);
    }
    return out;
};

const AXIS_COLUMN = lifetime.columns.axis;

const VALUE_COLUMN = lifetime.columns.value;

const axisNamedBy = function axisNamedBy(cell: string): string | null {
    for (const word of words(cell)) {
        if (CLOSED[word] !== undefined) {
            return word;
        }
    }
    return null;
};

interface Columns {
    readonly axis: number;
    readonly value: number;
}

const columnsOf = function columnsOf(row: readonly string[]): Columns | null {
    let axis = -1;
    let value = -1;

    for (let at = 0; at < row.length; at += 1) {
        const label = bare(row[at] ?? "");
        if (label === AXIS_COLUMN) {
            axis = at;
        }
        if (label === VALUE_COLUMN) {
            value = at;
        }
    }

    return axis === -1 || value === -1 ? null : { axis, value };
};

const statesTheSet = function statesTheSet(axis: string, value: string): boolean {
    const closed = CLOSED[axis] ?? [];
    const parts = tokens(value);
    if (parts.length < 2) {
        return false;
    }

    for (const part of parts) {
        if (!closed.includes(part)) {
            return false;
        }
    }
    return true;
};

const SPACING = new Set([" ", "\t"]);

const phrases = function phrases(text: string): string[] {
    const out: string[] = [];
    let held = "";

    for (const char of text) {
        if (!SPACING.has(char)) {
            held += char;
            continue;
        }
        if (held.length > 0) {
            out.push(held);
        }
        held = "";
    }

    if (held.length > 0) {
        out.push(held);
    }
    return out;
};

const definesTheAxis = function definesTheAxis(value: string): boolean {
    return phrases(value).length > 1;
};

const cells = function cells(line: string): string[] {
    const trimmed = line.trim();
    if (!trimmed.startsWith(CELL)) {
        return [];
    }

    const out: string[] = [];
    for (const part of trimmed.split(CELL)) {
        const cell = part.trim();
        if (cell.length > 0) {
            out.push(cell);
        }
    }
    return out;
};

const declaredRow = function declaredRow(row: readonly string[], columns: Columns, line: number): Declared[] {
    const axis = axisNamedBy(bare(row[columns.axis] ?? ""));
    const value = bare(row[columns.value] ?? "");
    if (axis === null || value.length === 0 || statesTheSet(axis, value) || definesTheAxis(value)) {
        return [];
    }
    return [{ axis, line, value }];
};

const declaredIn = function declaredIn(source: string): Declared[] {
    const found: Declared[] = [];
    let columns: Columns | null = null;

    for (const [index, line] of source.split("\n").entries()) {
        const row = cells(line);
        if (row.length === 0) {
            columns = null;
        } else if (columns === null) {
            columns = columnsOf(row);
        } else {
            found.push(...declaredRow(row, columns, index + 1));
        }
    }

    return found;
};

interface OffVocabulary {
    readonly target: string;
    readonly declared: Declared;
    readonly closed: readonly string[];
}

const offVocabularyFinding = function offVocabularyFinding({ closed, declared, target }: OffVocabulary): Finding {
    return {
        actual: `${target} declares a ${declared.axis} value the closed set for that axis does not contain`,
        expected: closed.join(" "),
        healed: false,
        line: declared.line,
        locus: declared.axis,
        path: target,
        remediation: {
            action: "declare",
            decide: "state a value the closed set carries, or raise the missing one as an approved extension against the DECLARED set rather than writing it locally — a vocabulary that grows by one word per surface is not closed, and a value invented where it is USED is a second declaration of the set that disagrees with the first the moment either moves. A SET CLOSED IN ITS STATEMENT AND OPEN IN ITS CONTENTS IS THE FAILURE THIS OBSERVES: the statement reads as governed, every author can satisfy it, and nothing joins the declaration to the members, so a value outside it survives every reading by every party. THIS CHECK RESOLVES THE SET FROM THE PARAMETER SURFACE RATHER THAN CARRYING A COPY OF IT, so an extension is one edit to the declaration and reaches the check and every other consumer at once — the class surface states what the axes MEAN and the declaration states what their values ARE, which is one fact with one home and no transcription between them",
            deterministic: false,
            from: declared.value,
            target,
            to: null,
        },
        rule: "vocabulary/undeclaredValue",
        stack: [
            { check: "axis", resolved: declared.axis },
            { check: "declaredSet", resolved: closed.join(" ") },
            { check: "carried", resolved: declared.value },
            { check: "member", resolved: "no" },
        ],
    };
};

export const rule: RuleDeclaration = {
    check(context: RuleContext): RuleResult {
        const declaring = context.paths
            .filter((target) => target.endsWith(MARKDOWN))
            .map((target) => ({ declarations: declaredIn(context.read(target)), target }))
            .filter((entry) => entry.declarations.length > 0);
        const walked = declaring.map((entry) => entry.target);

        const off: OffVocabulary[] = declaring.flatMap((entry) =>
            entry.declarations
                .map((declared) => ({ closed: CLOSED[declared.axis] ?? [], declared, target: entry.target }))
                .filter((candidate) => !candidate.closed.includes(candidate.declared.value)),
        );
        const offVocabulary = off.map((entry) => `${entry.target}:${String(entry.declared.line)} ${entry.declared.axis}`);
        const findings = off.map(offVocabularyFinding);

        return {
            derivations: {
                axes: Object.keys(CLOSED),
                columnSource:
                    "THE TWO COLUMN LABELS THAT IDENTIFY A DECLARING TABLE ARE READ FROM THE PARAMETER SURFACE, THE WAY THE MEMBER SETS ALREADY ARE, so this check transcribes no part of the schema it enforces. A declaring table is recognized by its own header naming an axis column and a value column; holding those two words as literals here was the same construct this check removed from its axis matching — a schema copied into a mechanism, which fails identically the moment a surface relabels its columns, dropping that surface out of the population silently while the walk stays green over it. The smallest possible registry is still a registry, and what made the earlier form deficient was position rather than size: it sat INSIDE the mechanism rather than beside the vocabulary it belongs to. One declaration now names the axes, their members and the columns that carry them, so a relabelling is one edit that reaches this check and every other consumer at once",
                offVocabulary,
                population:
                    "every markdown surface in this run's path set carrying a table whose OWN HEADER declares an axis column and a value column, which is the surface stating where its declarations live rather than this check matching an axis by an English spelling it carries. THE SPELLING FORM WAS THE DEFECT AND IT FAILED IN BOTH DIRECTIONS: a hardcoded set of axis names is a name list inside a check about closed vocabularies, so a surface reworded its rows and fell silently out of the population while the walk stayed green, and widening the match to any word made every prose cell mentioning an axis word into a declaration. The header is the surface's own identity for its columns, so a declaring table is recognized however its cells are worded and a prose table is outside by construction. A surface that DEFINES the axes without declaring a lifetime carries no value column and is therefore outside — which is correct rather than an exclusion, since it declares nothing for this check to test",
                range: "the check decides MEMBERSHIP and claims nothing more: whether the value the surface carries is one the set contains, never whether it is the RIGHT value for that surface, which is a reading of the surface and stays with its author. An axis the set does not cover contributes no comparison rather than a passing one, so a new axis is unmeasured here until its set arrives — an unmeasured axis and a satisfied one are different states and collapsing them is the defect this whole venue named",
                surfacesCarryingADeclaration: walked,
                valueKinds:
                    "a value cell resolves to one of three kinds and only the last is a finding. It STATES THE SET where every one of its separated tokens is a member and there is more than one — the surface exhibiting the vocabulary rather than using it. It DEFINES the axis where the cell is more than one word, which is prose about what the axis separates and carries no claim a member could satisfy. It USES a value where the cell is a single word, and that is the only kind compared against the set. The definition kind is the one this walk lacked, and its absence made a surface that DESCRIBES an axis indistinguishable from one declaring an off-vocabulary value for it",
                vocabularySource: lifetime.vocabulary,
            },
            findings,
            healed: [],
        };
    },
    extensions: [],
    heals: false,
    invariant:
        "a surface declaring a lifetime axis carries a value the closed set for that axis contains, so a vocabulary closed in its statement and open in its contents fails rather than reading as governed",
    jurisdiction: "all",
    kinds: ["undeclaredValue"],

    stage: "content",
};
```
