# core/validators/document.validator.ts

> 104 lines of code and 22 definitions.

Tree: GovLab Context
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/context#file-context-core-validators-document-validator-ts
Source text: https://banes-lab.com/source/context/core/validators/document.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/context/core/validators.md), after [core/validators/concept.validator.ts](https://banes-lab.com/source/context/core/validators/concept.validator.ts.md) and before [core/validators/exemplar.validator.ts](https://banes-lab.com/source/context/core/validators/exemplar.validator.ts.md).

## Definitions

- `validate` (lexical_declaration, line 78, exported)
- `verdictOf` (lexical_declaration, line 95, exported)
- `tokenDefects` (lexical_declaration, line 31)
- `validateDocuments` (lexical_declaration, line 112, exported)
- `COLON` (lexical_declaration, line 16)
- `DECLARATION_TOKEN` (lexical_declaration, line 17)
- `lineDefect` (lexical_declaration, line 19)
- `lower` (lexical_declaration, line 23)
- `needsColon` (lexical_declaration, line 27)
- `trimmed` (lexical_declaration, line 33)
- `defect` (lexical_declaration, line 34)
- `retiredDefects` (lexical_declaration, line 39)
- `invariantDefects` (lexical_declaration, line 47)
- `duplicateNodeDefects` (lexical_declaration, line 61)
- `seen` (lexical_declaration, line 62)
- `number` (lexical_declaration, line 65)
- `byLine` (lexical_declaration, line 74)
- `doc` (lexical_declaration, line 79, exported)
- `declType` (lexical_declaration, line 80, exported)
- `defects` (lexical_declaration, line 81, exported)
- `pag` (lexical_declaration, line 96, exported)
- `offset` (lexical_declaration, line 104, exported)

## Contained in

- [core/validators](https://banes-lab.com/anatomy/context/folder-context-core-validators.md)

## Uses

- [core/converters/text.converter.ts](https://banes-lab.com/source/context/core/converters/text.converter.ts.md)

## Used by

- [core/factories/grammar.factory.ts](https://banes-lab.com/source/context/core/factories/grammar.factory.ts.md)

## Linked from

- [core/converters](https://banes-lab.com/anatomy/context/folder-context-core-converters.md)
- [core/factories](https://banes-lab.com/anatomy/context/folder-context-core-factories.md)
- [core/validators](https://banes-lab.com/anatomy/context/folder-context-core-validators.md)

## Source

```typescript
import type {
    DocumentSource,
    DocumentVerdict,
    PagDefect,
    PagDocument,
    ValidateResult,
} from "#types/grammar.document.types";
import { FIRST_LINE, PAG_FENCE_INFO } from "#configuration/constants/document.constants";
import { FOR_EACH_HEAD, FOR_HEAD, LOWERCASE_KEYWORDS, NEEDS_COLON } from "#configuration/constants/grammar.constants";
import { gateDefects, nodeDefects } from "#core/validators/node.validator";
import { pagTextOf } from "#core/selectors/document.selector";
import { parse } from "#core/parsers/document.parser";
import { readFileSync } from "node:fs";
import { splitLines } from "#core/converters/text.converter";

const COLON = ":";
const DECLARATION_TOKEN = "THIS";

const lineDefect = function lineDefect(trimmed: string, lineNo: number): PagDefect | null {
    if (trimmed.startsWith(FOR_HEAD) && !trimmed.startsWith(FOR_EACH_HEAD)) {
        return { code: "for_without_each", line: lineNo, token: FOR_HEAD.trim() };
    }
    const lower = LOWERCASE_KEYWORDS.find((keyword) => trimmed.startsWith(keyword));
    if (lower !== undefined) {
        return { code: "lowercase_keyword", line: lineNo, token: lower.trim() };
    }
    const needsColon = NEEDS_COLON.find((head) => trimmed.startsWith(head) && !trimmed.includes(COLON));
    return needsColon === undefined ? null : { code: "missing_colon", line: lineNo, token: needsColon.trim() };
};

const tokenDefects = function tokenDefects(text: string): PagDefect[] {
    return splitLines(text).flatMap((line, index) => {
        const trimmed = line.trim();
        const defect = trimmed === "" ? null : lineDefect(trimmed, index + 1);
        return defect === null ? [] : [defect];
    });
};

const retiredDefects = function retiredDefects(doc: PagDocument): PagDefect[] {
    return doc.retired.map((entry) => ({
        code: entry.kind === "invariant" ? "bare_invariant_block" : "retired_unit_head",
        line: entry.line,
        token: entry.token,
    }));
};

const invariantDefects = function invariantDefects(doc: PagDocument): PagDefect[] {
    return doc.invariants.flatMap((invariant): PagDefect[] => [
        ...(invariant.set === null || invariant.set === ""
            ? [{ code: "invariant_without_set" as const, line: invariant.line, token: invariant.name }]
            : []),
        ...(invariant.parties === null || invariant.parties === ""
            ? [{ code: "invariant_without_parties" as const, line: invariant.line, token: invariant.name }]
            : []),
        ...(invariant.objector === null || invariant.objector === ""
            ? [{ code: "invariant_without_objector" as const, line: invariant.line, token: invariant.name }]
            : []),
    ]);
};

const duplicateNodeDefects = function duplicateNodeDefects(doc: PagDocument): PagDefect[] {
    const seen = new Set<string>();
    const defects: PagDefect[] = [];
    for (const node of doc.nodes) {
        const number = node.number ?? "";
        if (number !== "" && seen.has(number)) {
            defects.push({ code: "node_declared_twice", line: node.line, token: number });
        }
        seen.add(number);
    }
    return defects;
};

const byLine = function byLine(a: PagDefect, b: PagDefect): number {
    return a.line - b.line || a.code.localeCompare(b.code);
};

export const validate = function validate(text: string): ValidateResult {
    const doc = parse(text);
    const declType = doc.declaration?.type;
    const defects: PagDefect[] = [
        ...(typeof declType !== "string" || declType.length === 0
            ? [{ code: "no_declaration" as const, line: 1, token: DECLARATION_TOKEN }]
            : []),
        ...retiredDefects(doc),
        ...doc.nodes.flatMap((node) => nodeDefects(node, doc)),
        ...doc.nodes.flatMap(gateDefects),
        ...duplicateNodeDefects(doc),
        ...invariantDefects(doc),
        ...tokenDefects(text),
    ].toSorted(byLine);
    return { defects, wellFormed: defects.length === 0 };
};

export const verdictOf = function verdictOf(source: DocumentSource, text: string): DocumentVerdict {
    const pag = pagTextOf(source, text);
    if (pag === null) {
        return {
            defects: [{ code: "template_without_block", line: FIRST_LINE, token: PAG_FENCE_INFO }],
            extracted: false,
            source,
        };
    }
    const offset = pag.firstLine - FIRST_LINE;
    return {
        defects: validate(pag.text).defects.map((defect) => ({ ...defect, line: defect.line + offset })),
        extracted: true,
        source,
    };
};

export const validateDocuments = function validateDocuments(sources: readonly DocumentSource[]): DocumentVerdict[] {
    return sources.map((source) => verdictOf(source, readFileSync(source.path, "utf8")));
};
```
