core/converters/invariant.converter.ts
core/converters/invariant.converter.ts is a file in Bane's Lab Content. 173 lines of code and 45 definitions.
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";
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,
})),
);
};