# rules/eslint/strings-terminology.eslint.rule.ts

> 105 lines of code and 21 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-strings-terminology-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/strings-terminology.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/strings-sentence-shape.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/strings-sentence-shape.eslint.rule.ts.md) and before [rules/eslint/strings-vocabulary.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/strings-vocabulary.eslint.rule.ts.md).

## Definitions

- `report` (lexical_declaration, line 53, exported)
- `quasiTexts` (lexical_declaration, line 41)
- `isRecord` (lexical_declaration, line 17)
- `isStringArray` (lexical_declaration, line 21)
- `isTermRecord` (lexical_declaration, line 25)
- `templateLiteral` (method_definition, line 74, exported)
- `create` (method_definition, line 46, exported)
- `literal` (method_definition, line 65, exported)
- `program` (method_definition, line 71, exported)
- `STRINGS_SUFFIX` (lexical_declaration, line 14)
- `ENFORCED` (lexical_declaration, line 15)
- `declaredTerms` (lexical_declaration, line 34)
- `options` (lexical_declaration, line 35)
- `declared` (lexical_declaration, line 36)
- `records` (lexical_declaration, line 37)
- `terms` (lexical_declaration, line 50, exported)
- `synonyms` (lexical_declaration, line 51, exported)
- `code` (lexical_declaration, line 52, exported)
- `synonym` (lexical_declaration, line 54, exported)
- `record` (lexical_declaration, line 55, exported)
- `text` (lexical_declaration, line 66, exported)

## Uses

- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/selectors/literal.selector.ts](https://banes-lab.com/source/governance/shared/selectors/literal.selector.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

- [Semantic Consistency](https://banes-lab.com/records/architecture/semantic-consistency.md)

## Source

```typescript
import { CANONICAL_TERMS, canonicalFor, synonymsOf } from "../../shared/manifests/vocabulary.manifest.ts";
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { STRINGS_CHANNEL, isCheckEnforced } from "../../shared/manifests/writing.canon.manifest.ts";
import { literalString, locOf, nodesAt, recordAt, stringIn } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import type { TermRecord } from "../../types/writing.types.ts";
import { basenameOf } from "../../shared/resolvers/anchor.resolver.ts";
import { codeLiteralsOf } from "../../shared/selectors/literal.selector.ts";
import { concernSuffix } from "../../shared/manifests/taxonomy.manifest.ts";
import { defineCheck } from "@govlab/context/check";
import { firstTermIn } from "../../shared/matchers/vocabulary.matcher.ts";
import { listener } from "../../shared/factories/listener.factory.ts";

const STRINGS_SUFFIX = concernSuffix("strings");
const ENFORCED = isCheckEnforced("one-term-per-concept", STRINGS_CHANNEL);

const isRecord = function isRecord(value: unknown): value is Record<string, unknown> {
    return typeof value === "object" && value !== null;
};

const isStringArray = function isStringArray(value: unknown): value is string[] {
    return Array.isArray(value) && value.every((entry) => typeof entry === "string");
};

const isTermRecord = function isTermRecord(value: unknown): value is TermRecord {
    return (
        isRecord(value) &&
        typeof value["canonical"] === "string" &&
        typeof value["concept"] === "string" &&
        isStringArray(value["synonyms"])
    );
};

const declaredTerms = function declaredTerms(context: RuleContext): readonly TermRecord[] {
    const options: unknown = context.options.at(0);
    const declared = isRecord(options) ? options["terms"] : undefined;
    const records = Array.isArray(declared) ? declared.filter(isTermRecord) : [];
    return records.length > 0 ? records : CANONICAL_TERMS;
};

const quasiTexts = function quasiTexts(node: AstNode): string[] {
    return nodesAt(node, "quasis").map((part) => stringIn(recordAt(part, "value"), "cooked"));
};

export default {
    create(context: RuleContext): RuleListener {
        if (!ENFORCED || !basenameOf(context.filename).endsWith(STRINGS_SUFFIX)) {
            return {};
        }
        const terms = declaredTerms(context);
        const synonyms = synonymsOf(terms);
        let code: ReadonlySet<AstNode> = new Set();
        const report = function report(node: AstNode, text: string): void {
            const synonym = code.has(node) ? null : firstTermIn(text, synonyms);
            const record = synonym === null ? null : canonicalFor(terms, synonym);
            if (synonym !== null && record !== null) {
                context.report({
                    data: { canonical: record.canonical, concept: record.concept, synonym },
                    loc: locOf(node),
                    messageId: "retiredSynonym",
                });
            }
        };
        return listener({
            literal(view) {
                const text = literalString(view);
                if (text !== null) {
                    report(view, text);
                }
            },
            program(view) {
                code = codeLiteralsOf(view);
            },
            templateLiteral(view) {
                report(view, quasiTexts(view).join(" "));
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:semantic-consistency"] }),
            description:
                "A user-visible string names each concept by its one canonical term. The term registry records, per concept, the canonical name and the synonyms it retires, and the rule matches a retired synonym at word boundaries inside every literal and template quasi of a strings module, so a concept never reads as two things. The registry is data; the rule never changes when a term is added. A literal consumed as a code sample is code, not prose, and is not held to the registry.",
        },
        messages: {
            retiredSynonym:
                "The literal names the concept '{{concept}}' as '{{synonym}}'. The term registry retires that word in favor of '{{canonical}}'; restate the sentence with the canonical term.",
        },
        schema: [
            {
                additionalProperties: false,
                properties: {
                    terms: {
                        items: {
                            additionalProperties: false,
                            properties: {
                                canonical: { type: "string" },
                                concept: { type: "string" },
                                synonyms: { items: { type: "string" }, type: "array" },
                            },
                            required: ["canonical", "concept", "synonyms"],
                            type: "object",
                        },
                        type: "array",
                    },
                },
                type: "object",
            },
        ],
        type: "problem",
    },
} satisfies LocalRule;
```
