core/validators/node.validator.ts
core/validators/node.validator.ts is a file in GovLab Context. 131 lines of code and 28 definitions.
import {
ARTIFACT_SHAPE,
DECLARING_PREFIXES,
GATE_MAX_CONDITIONS,
GATE_MIN_CONDITIONS,
JURISDICTION_KEY,
NAME_TERMINATORS,
RESULT_BLOCKED,
SOURCE_TOKENS,
VAGUE_TERMS,
WRITING_TOKENS,
ZERO,
} from "#configuration/constants/grammar.constants";
import type { PagDefect, PagDocument, PagGate, PagNode } from "#types/grammar.document.types";
const hasVagueCondition = function hasVagueCondition(condition: string): boolean {
const lower = condition.toLowerCase();
return VAGUE_TERMS.some((term) => lower.includes(term));
};
const declaredNameOf = function declaredNameOf(directive: string): string | null {
const prefix = DECLARING_PREFIXES.find((candidate) => directive.startsWith(candidate));
if (prefix === undefined) {
return null;
}
const rest = directive.slice(prefix.length);
let end = 0;
while (end < rest.length && !NAME_TERMINATORS.has(rest.charAt(end))) {
end += 1;
}
const name = rest.slice(0, end);
return name === "" ? null : name;
};
const declaredNamesOf = function declaredNamesOf(doc: PagDocument): string[] {
return doc.nodes.flatMap((node) => node.directives.flatMap((directive) => declaredNameOf(directive) ?? []));
};
const inJurisdiction = function inJurisdiction(node: PagNode, doc: PagDocument, input: string): boolean {
const jurisdiction = doc.meta[JURISDICTION_KEY] ?? "";
return doc.nodes[0] === node && jurisdiction !== "" && jurisdiction.includes(input);
};
const inputHasSource = function inputHasSource(node: PagNode, doc: PagDocument, names: string[]): boolean {
const input = node.input ?? "";
return (
SOURCE_TOKENS.some((token) => input.includes(token)) ||
names.some((name) => input.includes(name)) ||
inJurisdiction(node, doc, input)
);
};
const isWordChar = function isWordChar(ch: string): boolean {
return ch !== "" && ch.toLowerCase() !== ch.toUpperCase();
};
const hasWord = function hasWord(line: string, token: string): boolean {
let at = line.indexOf(token);
while (at !== -1) {
if (!isWordChar(line.charAt(at - 1))) {
return true;
}
at = line.indexOf(token, at + 1);
}
return false;
};
const writes = function writes(node: PagNode): boolean {
const lines = [node.transform ?? "", ...node.directives];
return lines.some((line) => WRITING_TOKENS.some((token) => hasWord(line, token)));
};
const tokenOf = function tokenOf(node: PagNode): string {
return node.number ?? node.title;
};
export const nodeDefects = function nodeDefects(node: PagNode, doc: PagDocument): PagDefect[] {
const token = tokenOf(node);
const defects: PagDefect[] = [];
if (node.tag === null) {
defects.push({ code: "node_tag_malformed", line: node.line, token });
}
if (node.input !== null && !inputHasSource(node, doc, declaredNamesOf(doc))) {
defects.push({ code: "input_without_source", line: node.line, token });
}
if (writes(node) && node.gate?.refusal === null) {
defects.push({ code: "write_without_refusal", line: node.gate.line, token });
}
if (node.tag?.yields.includes(ARTIFACT_SHAPE) === true && node.freshness === null) {
defects.push({ code: "artifact_without_freshness", line: node.line, token });
}
if (node.gate === null) {
defects.push({ code: "node_without_gate", line: node.line, token });
}
return defects;
};
const countDefect = function countDefect(gate: PagGate): PagDefect | null {
const count = gate.checks.length;
if (count < GATE_MIN_CONDITIONS) {
return { code: "gate_too_few_conditions", line: gate.line, token: String(count) };
}
return count > GATE_MAX_CONDITIONS
? { code: "gate_too_many_conditions", line: gate.line, token: String(count) }
: null;
};
const checkDefects = function checkDefects(gate: PagGate): PagDefect[] {
return gate.checks.flatMap((check): PagDefect[] => [
...(hasVagueCondition(check.condition)
? [{ code: "vague_condition" as const, line: check.line, token: check.marker }]
: []),
...(check.evidence === null || check.evidence === ""
? [{ code: "check_without_evidence" as const, line: check.line, token: check.marker }]
: []),
...(check.population?.measured === ZERO && check.population.whole === ZERO
? [{ code: "empty_population" as const, line: check.line, token: check.population.set }]
: []),
]);
};
const resultDefects = function resultDefects(gate: PagGate, nodeToken: string): PagDefect[] {
if (gate.result === null) {
return [{ code: "result_missing", line: gate.line, token: nodeToken }];
}
return gate.result.unknown === RESULT_BLOCKED
? []
: [{ code: "unknown_unrouted", line: gate.result.line, token: nodeToken }];
};
export const gateDefects = function gateDefects(node: PagNode): PagDefect[] {
const { gate } = node;
if (gate === null) {
return [];
}
const countIssue = countDefect(gate);
return [
...(countIssue ? [countIssue] : []),
...(gate.checks.some((check) => check.population !== null)
? []
: [{ code: "gate_without_population" as const, line: gate.line, token: tokenOf(node) }]),
...checkDefects(gate),
...resultDefects(gate, tokenOf(node)),
];
};