# tools/core/inspectors/role.inspector.ts

> 121 lines of code and 28 definitions.

Tree: Coordination tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-core-inspectors-role-inspector-ts
Source text: https://banes-lab.com/assets/sources/source.58144f548fdcb7159b30f3a4793148bb52d34d08d25a5e7eb2be907658a5739c.generated.txt

## Definitions

- `roleSubsectionsFrom` (lexical_declaration, line 14, exported)
- `trimmedLines` (lexical_declaration, line 6)
- `isPlaceholder` (lexical_declaration, line 23)
- `roleFieldsFrom` (lexical_declaration, line 59, exported)
- `roleSectionsFrom` (lexical_declaration, line 10, exported)
- `sectionBody` (lexical_declaration, line 27)
- `unfilledMeasuredSections` (lexical_declaration, line 39, exported)
- `checkRoleCoverage` (lexical_declaration, line 83, exported)
- `USAGE_HEADING` (lexical_declaration, line 4)
- `MeasuredSection` (interface_declaration, line 18, exported)
- `start` (lexical_declaration, line 29)
- `end` (lexical_declaration, line 35)
- `body` (lexical_declaration, line 44, exported)
- `fieldLine` (lexical_declaration, line 52)
- `colon` (lexical_declaration, line 53)
- `key` (lexical_declaration, line 54)
- `value` (lexical_declaration, line 55)
- `lines` (lexical_declaration, line 60, exported)
- `rest` (lexical_declaration, line 65, exported)
- `close` (lexical_declaration, line 66, exported)
- `DECIDE` (lexical_declaration, line 70)
- `MEASURED_DECIDE` (lexical_declaration, line 80)
- `checkRoleShape` (lexical_declaration, line 101, exported)
- `sections` (lexical_declaration, line 107, exported)
- `present` (lexical_declaration, line 108, exported)
- `fields` (lexical_declaration, line 110, exported)
- `missing` (lexical_declaration, line 116, exported)
- `unfilled` (lexical_declaration, line 128, exported)

## Uses

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

## Used by

- [tools/core/runners/role.runner.ts](https://banes-lab.com/source/coordination/tools/core/runners/role.runner.ts.md)

## Source

```typescript
import type { Finding, FindingBuilder } from "../types/segment.types.ts";
import { FRONTMATTER_FENCE } from "../constants/template.constants.ts";

const USAGE_HEADING = "# HOW THIS TEMPLATE IS USED";

const trimmedLines = function trimmedLines(source: string): string[] {
    return source.split("\n").map((line) => line.trim());
};

export const roleSectionsFrom = function roleSectionsFrom(template: string): readonly string[] {
    return trimmedLines(template).filter((text) => text.startsWith("# ") && text !== USAGE_HEADING);
};

export const roleSubsectionsFrom = function roleSubsectionsFrom(template: string): readonly string[] {
    return trimmedLines(template).filter((text) => text.startsWith("## "));
};

export interface MeasuredSection {
    readonly section: string;
    readonly state: "absent" | "unfilled";
}

const isPlaceholder = function isPlaceholder(text: string): boolean {
    return text.startsWith("<") && text.endsWith(">");
};

const sectionBody = function sectionBody(source: string, heading: string): string[] | null {
    const lines = trimmedLines(source);
    const start = lines.findIndex((text) => text.startsWith(heading));
    if (start === -1) {
        return null;
    }

    const rest = lines.slice(start + 1);
    const end = rest.findIndex((text) => text.startsWith("#"));
    return (end === -1 ? rest : rest.slice(0, end)).filter((text) => text.length > 0 && !isPlaceholder(text));
};

export const unfilledMeasuredSections = function unfilledMeasuredSections(
    source: string,
    template: string,
): MeasuredSection[] {
    return roleSubsectionsFrom(template).flatMap((heading): MeasuredSection[] => {
        const body = sectionBody(source, heading);
        if (body === null) {
            return [{ section: heading, state: "absent" }];
        }
        return body.length === 0 ? [{ section: heading, state: "unfilled" }] : [];
    });
};

const fieldLine = function fieldLine(text: string): string {
    const colon = text.indexOf(":");
    const key = text.slice(0, colon);
    const value = text.slice(colon + 1).trim();
    return value.startsWith("<") ? `${key}:` : `${key}: ${value}`;
};

export const roleFieldsFrom = function roleFieldsFrom(template: string): readonly string[] {
    const lines = trimmedLines(template);
    if (lines[0] !== FRONTMATTER_FENCE) {
        return [];
    }

    const rest = lines.slice(1);
    const close = rest.indexOf(FRONTMATTER_FENCE);
    return (close === -1 ? rest : rest.slice(0, close)).filter((text) => text.indexOf(":") > 0).map(fieldLine);
};

const DECIDE =
    "a role document states who a seat is, what it owns, what it refuses and the principles that decide " +
    "its calls — and it is read by every other seat to know what may be routed where. A uniform shape is " +
    "what makes the set comparable: a reader looking for what a seat REFUSES must find it in the " +
    "same place in each, or the document is prose that happens to be filed together. The section set is " +
    "DERIVED from the role template rather than transcribed here, so the check cannot drift from the " +
    "document a seat is raised from: it is " +
    "the contract and the frontmatter carries the operands a tool reads — the letter the citations " +
    "resolve through, and the concern the filename is derived from";

const MEASURED_DECIDE =
    "A SEAT'S MEASURED ERROR DISTRIBUTION IS THE ONLY EVIDENCE IT HOLDS ABOUT ITSELF, AND NOTHING ELSE IN THE TREE KEEPS A COPY. A rule carries the shape it enforces, a gate carries a verdict, the accumulator carries what a discussion concluded — none of them records what one seat has repeatedly got wrong, which is the input that makes a role document more than a job description. A DECLARED-AND-EMPTY SECTION AND AN ABSENT ONE ARE DIFFERENT STATES AND ONLY ONE OF THEM IS AN ANSWER: a seat that has measured nothing yet writes what it has, and a template seed left in place states nothing while reading as a section that exists. The section set is DERIVED from the role template on every run, so this cannot drift from the document a seat is raised from, and a document listing only virtues is decoration — a seat reading its own document is looking for the trap it fell into last time";

export const checkRoleCoverage = function checkRoleCoverage(
    letters: readonly string[],
    files: readonly string[],
    build: FindingBuilder,
): Finding[] {
    return letters
        .filter((letter) => !files.some((file) => file.endsWith(`.${letter.toLowerCase()}.role.md`)))
        .map((letter) =>
            build(
                "roleMissing",
                letter,
                `${letter} is ACTIVE in the agent index and owns no role document`,
                `a <subject>.${letter.toLowerCase()}.role.md under the roles directory`,
                "a seat's role document is what every other seat reads to know what may be routed to it, and what a resuming session reads to know what it owns and refuses. The index ALLOCATES the letter and this check binds the obligation to it, so a new seat cannot become active without one — otherwise the shape is gated and the existence is not, and a uniform document nobody is required to write is a folder that stays empty while the gate reports green",
            ),
        );
};

export const checkRoleShape = function checkRoleShape(
    path: string,
    source: string,
    template: string,
    build: FindingBuilder,
): Finding[] {
    const sections = roleSectionsFrom(template);
    const present = new Set(trimmedLines(source));

    const fields = roleFieldsFrom(template)
        .filter((field) => !source.includes(field))
        .map((field) =>
            build("roleFieldMissing", field, `${path} declares no ${field}`, `${field} in the frontmatter`, DECIDE),
        );

    const missing = sections
        .filter((section) => !present.has(section))
        .map((section) =>
            build(
                "roleSectionMissing",
                section,
                `${path} carries no "${section}" section`,
                sections.join(", "),
                DECIDE,
            ),
        );

    const unfilled = unfilledMeasuredSections(source, template).map((gap) =>
        build(
            "measuredSectionUnfilled",
            gap.section,
            gap.state === "absent"
                ? `${path} carries no "${gap.section}" subsection`
                : `${path} declares "${gap.section}" and states nothing under it`,
            `${gap.section} carrying at least one measured entry, or absent from the template`,
            MEASURED_DECIDE,
        ),
    );

    return [...fields, ...missing, ...unfilled];
};
```
