# core/converters/invariant.converter.ts

> 173 lines of code and 45 definitions.

Tree: Build tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build#file-build-core-converters-invariant-converter-ts
Source text: https://banes-lab.com/assets/sources/source.551e78d5d1f22b73eac1e0a32820553a11723a4a3cbd56ea7d7846e8a3ac5629.generated.txt

## Definitions

- `modelStem` (lexical_declaration, line 165)
- `invariantsOf` (lexical_declaration, line 153, exported)
- `unitsOf` (lexical_declaration, line 105)
- `blocksOf` (lexical_declaration, line 134)
- `modelSeeds` (lexical_declaration, line 170, exported)
- `statementsOf` (lexical_declaration, line 119)
- `SOURCE` (lexical_declaration, line 20)
- `NO_LEAD` (lexical_declaration, line 21)
- `STATEMENT_LEADS` (lexical_declaration, line 22)
- `LABEL_STOPS` (lexical_declaration, line 23)
- `MIN_WORD` (lexical_declaration, line 24)
- `isUpperWord` (lexical_declaration, line 26)
- `upperWordsAfterLead` (lexical_declaration, line 38)
- `lead` (lexical_declaration, line 39)
- `close` (lexical_declaration, line 40)
- `paragraphEnd` (lexical_declaration, line 44)
- `body` (lexical_declaration, line 45)
- `objectorWord` (lexical_declaration, line 49)
- `gate` (lexical_declaration, line 50)
- `rest` (lexical_declaration, line 55)
- `slotWordsOf` (lexical_declaration, line 60, exported)
- `[property, set, parties]` (lexical_declaration, line 61, exported)
- `objector` (lexical_declaration, line 62, exported)
- `firstWordOf` (lexical_declaration, line 69)
- `lower` (lexical_declaration, line 70)
- `bare` (lexical_declaration, line 71)
- `end` (lexical_declaration, line 72)
- `stripLeads` (lexical_declaration, line 79)
- `changed` (lexical_declaration, line 81)
- `slotOf` (lexical_declaration, line 94)
- `word` (lexical_declaration, line 95)
- `entries` (lexical_declaration, line 96)
- `units` (lexical_declaration, line 106)
- `previous` (lexical_declaration, line 109)
- `segments` (lexical_declaration, line 122)
- `index` (lexical_declaration, line 123)
- `slot` (lexical_declaration, line 124)
- `statement` (lexical_declaration, line 125)
- `blocks` (lexical_declaration, line 135)
- `at` (lexical_declaration, line 138)
- `last` (lexical_declaration, line 145)
- `out` (lexical_declaration, line 154, exported)
- `found` (lexical_declaration, line 156, exported)
- `{ objector, parties, property, set }` (lexical_declaration, line 157, exported)
- `base` (lexical_declaration, line 166)

## Source

```typescript
import {
    BOLD_MARK,
    HEADING_MARK,
    LEAD_IN_SEPARATOR,
    LINE_END,
    LIST_MARKER,
} from "#configuration/constants/inventory.constants";
import {
    GATE_HEADING,
    ITS_PREFIX,
    MODEL_EXTENSION,
    SEED_ID_SEPARATOR,
    SEED_LEVELS,
    SLOT_SENTENCE_LEAD,
} from "#configuration/constants/seed.constants";
import type { InvariantStatement, LessonSeed, SlotWords } from "#types/lesson.types";
import type { SourceText } from "#types/inventory.types";
import { slugify } from "@govlab/context/algo";

const SOURCE = "model";
const NO_LEAD = "with no ";
const STATEMENT_LEADS: readonly string[] = [LEAD_IN_SEPARATOR, ".", ":", ",", " "];
const LABEL_STOPS: ReadonlySet<string> = new Set([".", ",", ":", " "]);
const MIN_WORD = 3;

const isUpperWord = function isUpperWord(word: string): boolean {
    if (word.length < MIN_WORD) {
        return false;
    }
    for (const char of word) {
        if (char < "A" || char > "Z") {
            return false;
        }
    }
    return true;
};

const upperWordsAfterLead = function upperWordsAfterLead(text: string): string[] {
    const lead = text.indexOf(SLOT_SENTENCE_LEAD);
    const close = lead === -1 ? -1 : text.indexOf(BOLD_MARK, lead);
    if (close === -1) {
        return [];
    }
    const paragraphEnd = text.indexOf(LINE_END + LINE_END, close);
    const body = text.slice(close, paragraphEnd === -1 ? text.length : paragraphEnd);
    return body.split(" ").filter(isUpperWord);
};

const objectorWord = function objectorWord(text: string): string | null {
    const gate = text.indexOf(GATE_HEADING);
    const at = gate === -1 ? -1 : text.indexOf(NO_LEAD, gate);
    if (at === -1) {
        return null;
    }
    const rest = text.slice(at + NO_LEAD.length);
    const end = rest.indexOf(" ");
    return end === -1 ? rest : rest.slice(0, end);
};

export const slotWordsOf = function slotWordsOf(template: string): SlotWords | null {
    const [property, set, parties] = upperWordsAfterLead(template).map((word) => word.toLowerCase());
    const objector = objectorWord(template);
    if (property === undefined || set === undefined || parties === undefined || objector === null) {
        return null;
    }
    return { objector, parties, property, set };
};

const firstWordOf = function firstWordOf(label: string): string {
    const lower = label.trim().toLowerCase();
    const bare = lower.startsWith(ITS_PREFIX.toLowerCase()) ? lower.slice(ITS_PREFIX.length) : lower;
    let end = 0;
    while (end < bare.length && !LABEL_STOPS.has(bare.charAt(end))) {
        end += 1;
    }
    return bare.slice(0, end);
};

const stripLeads = function stripLeads(text: string): string {
    let out = text;
    let changed = true;
    while (changed) {
        changed = false;
        for (const lead of STATEMENT_LEADS) {
            if (out.startsWith(lead)) {
                out = out.slice(lead.length);
                changed = true;
            }
        }
    }
    return out.trim();
};

const slotOf = function slotOf(label: string, words: SlotWords): keyof SlotWords | null {
    const word = firstWordOf(label);
    const entries: [keyof SlotWords, string][] = [
        ["property", words.property],
        ["set", words.set],
        ["parties", words.parties],
        ["objector", words.objector],
    ];
    return entries.find(([, held]) => held === word)?.[0] ?? null;
};

const unitsOf = function unitsOf(block: string): string[] {
    const units: string[] = [];
    for (const line of block.split(LINE_END)) {
        const last = units.length - 1;
        const previous = units[last];
        if (line.trim().length === 0 || line.startsWith(LIST_MARKER) || previous === undefined) {
            units.push(line);
            continue;
        }
        units[last] = `${previous} ${line.trim()}`;
    }
    return units;
};

const statementsOf = function statementsOf(block: string, words: SlotWords): Partial<Record<keyof SlotWords, string>> {
    const found: Partial<Record<keyof SlotWords, string>> = {};
    for (const line of unitsOf(block)) {
        const segments = line.split(BOLD_MARK);
        for (let index = 1; index < segments.length; index += 2) {
            const slot = slotOf(segments[index] ?? "", words);
            const statement = stripLeads(segments[index + 1] ?? "");
            if (slot !== null && found[slot] === undefined && statement.length > 0) {
                found[slot] = statement;
            }
        }
    }
    return found;
};

const blocksOf = function blocksOf(text: string): { heading: string; body: string }[] {
    const blocks: { heading: string; body: string }[] = [];
    for (const line of text.split(LINE_END)) {
        if (line.startsWith(HEADING_MARK + HEADING_MARK)) {
            let at = 0;
            while (line.charAt(at) === HEADING_MARK) {
                at += 1;
            }
            blocks.push({ body: "", heading: line.slice(at).trim() });
            continue;
        }
        const last = blocks.at(-1);
        if (last !== undefined) {
            last.body += line + LINE_END;
        }
    }
    return blocks;
};

export const invariantsOf = function invariantsOf(text: string, words: SlotWords): InvariantStatement[] {
    const out: InvariantStatement[] = [];
    for (const block of blocksOf(text)) {
        const found = statementsOf(block.body, words);
        const { objector, parties, property, set } = found;
        if (objector !== undefined && parties !== undefined && property !== undefined && set !== undefined) {
            out.push({ heading: block.heading, objector, parties, property, set });
        }
    }
    return out;
};

const modelStem = function modelStem(path: string): string {
    const base = path.slice(path.lastIndexOf("/") + 1);
    return base.endsWith(MODEL_EXTENSION) ? base.slice(0, -MODEL_EXTENSION.length) : base;
};

export const modelSeeds = function modelSeeds(models: readonly SourceText[], words: SlotWords): LessonSeed[] {
    return models.flatMap((model) =>
        invariantsOf(model.text, words).map((held) => ({
            application: [held.set],
            boundary: null,
            cause: [],
            decision: held.parties,
            domain: modelStem(model.path),
            failureMode: held.objector,
            id: modelStem(model.path) + SEED_ID_SEPARATOR + slugify(held.heading),
            level: SEED_LEVELS[SOURCE],
            principle: held.property,
            problem: null,
            source: SOURCE,
            validation: held.objector,
        })),
    );
};
```
