# core/validators/grammar.validator.ts

> 89 lines of code and 17 definitions.

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

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

## Definitions

- `grammarIssuesOf` (lexical_declaration, line 76, exported)
- `duplicateKeywords` (lexical_declaration, line 15)
- `grammarSymbolsOf` (lexical_declaration, line 41)
- `recognitionGaps` (lexical_declaration, line 60)
- `keywordValuesOf` (lexical_declaration, line 56)
- `byName` (lexical_declaration, line 11)
- `seen` (lexical_declaration, line 16)
- `duplicates` (lexical_declaration, line 17)
- `nonterminalRefs` (lexical_declaration, line 27)
- `open` (lexical_declaration, line 29)
- `close` (lexical_declaration, line 31)
- `defined` (lexical_declaration, line 44)
- `terminals` (lexical_declaration, line 45)
- `refs` (lexical_declaration, line 46)
- `referenced` (lexical_declaration, line 49)
- `typeKeywords` (lexical_declaration, line 63)
- `verbKeywords` (lexical_declaration, line 64)

## Contained in

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

## Uses

- [core/resolvers/template.resolver.ts](https://banes-lab.com/source/context/core/resolvers/template.resolver.ts.md)
- [core/selectors/grammar.selector.ts](https://banes-lab.com/source/context/core/selectors/grammar.selector.ts.md)

## Used by

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

## Linked from

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

## Source

```typescript
import {
    DOCUMENT_TYPE_CATEGORY,
    DOCUMENT_VERB_CATEGORY,
    NONTERMINAL_CLOSE,
} from "#configuration/constants/grammar.constants";
import type { DocumentTypeRecord, GrammarIssues, PagData } from "#types/grammar.types";
import { hasRoleIn, keywordIdOf } from "#core/selectors/grammar.selector";
import { NONTERMINAL_OPEN } from "#configuration/constants/algorithm.constants";
import { slotIsDangling } from "#core/resolvers/template.resolver";

const byName = function byName(a: string, b: string): number {
    return a.localeCompare(b);
};

const duplicateKeywords = function duplicateKeywords(data: PagData): string[] {
    const seen = new Set<string>();
    const duplicates: string[] = [];
    for (const id of data.keywords.map(keywordIdOf)) {
        if (seen.has(id)) {
            duplicates.push(id);
        }
        seen.add(id);
    }
    return duplicates;
};

const nonterminalRefs = function nonterminalRefs(rhs: string): string[] {
    const refs: string[] = [];
    let open = rhs.indexOf(NONTERMINAL_OPEN);
    while (open !== -1) {
        const close = rhs.indexOf(NONTERMINAL_CLOSE, open);
        if (close === -1) {
            break;
        }
        refs.push(rhs.slice(open + 1, close));
        open = rhs.indexOf(NONTERMINAL_OPEN, close + 1);
    }
    return refs;
};

const grammarSymbolsOf = function grammarSymbolsOf(
    data: PagData,
): Pick<GrammarIssues, "danglingNonterminals" | "unusedTerminals"> {
    const defined = new Set(data.productions.map((record) => record.lhs));
    const terminals = new Set(data.terminals);
    const refs = data.productions.flatMap((production) =>
        nonterminalRefs(production.rhs).map((ref) => ({ production: production.lhs, ref })),
    );
    const referenced = new Set(refs.map((entry) => entry.ref));
    return {
        danglingNonterminals: refs.filter((entry) => !defined.has(entry.ref) && !terminals.has(entry.ref)),
        unusedTerminals: [...terminals].filter((terminal) => !referenced.has(terminal)).toSorted(byName),
    };
};

const keywordValuesOf = function keywordValuesOf(data: PagData, category: string): Set<string> {
    return new Set(data.keywords.filter((record) => hasRoleIn(record, category)).map((record) => record.keyword));
};

const recognitionGaps = function recognitionGaps(
    data: PagData,
): Pick<GrammarIssues, "unrecognizedDocumentTypes" | "unrecognizedDocumentVerbs"> {
    const typeKeywords = keywordValuesOf(data, DOCUMENT_TYPE_CATEGORY);
    const verbKeywords = keywordValuesOf(data, DOCUMENT_VERB_CATEGORY);
    return {
        unrecognizedDocumentTypes: data.documentTypes
            .filter((record) => !typeKeywords.has(record.type))
            .map((record) => record.type)
            .toSorted(byName),
        unrecognizedDocumentVerbs: data.documentTypes
            .flatMap((record) => record.verbs.map((verb) => ({ type: record.type, verb })))
            .filter((entry) => !verbKeywords.has(entry.verb)),
    };
};

export const grammarIssuesOf = function grammarIssuesOf(
    data: PagData,
    typeByName: ReadonlyMap<string, DocumentTypeRecord>,
): GrammarIssues {
    return {
        ...recognitionGaps(data),
        ...grammarSymbolsOf(data),
        danglingTemplateSlots: data.templates.flatMap((template) =>
            template.slots
                .filter((slot) => slotIsDangling(template, slot))
                .map((slot) => ({ slot: slot.name, type: template.type })),
        ),
        docTypesWithoutVerb: data.documentTypes
            .filter((record) => record.defaultVerb === "" || record.verbs.length === 0)
            .map((record) => record.type),
        duplicateKeywordIds: duplicateKeywords(data),
        unknownTemplateTypes: data.templates
            .filter((template) => !typeByName.has(template.type))
            .map((template) => template.type),
    };
};
```
