import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { CONFIG_DIR } from "./settings.ts"; const TAXONOMY_PATH = join(CONFIG_DIR, "taxonomy.json"); export interface TaxonomyCategory { /** Enum key used in classification and log records. */ key: string; /** Human-readable label shown in the report. */ label: string; /** Excluded from the ranked report while still being logged, e.g. mechanical typos. */ reportable: boolean; } /** * Coarse, mutually disjoint English error categories. Disjointness is the point: a * category an error could plausibly land in twice makes counts incomparable across * time, which is the only reason the enum exists. */ export const DEFAULT_TAXONOMY: TaxonomyCategory[] = [ { key: "subject_verb_agreement", label: "Subject-verb agreement", reportable: true }, { key: "article_determiner", label: "Articles and determiners", reportable: true }, { key: "preposition", label: "Prepositions", reportable: true }, { key: "verb_tense", label: "Verb tense", reportable: true }, { key: "verb_form", label: "Verb form", reportable: true }, { key: "noun_number", label: "Noun number", reportable: true }, { key: "word_form", label: "Word form", reportable: true }, { key: "word_choice", label: "Word choice", reportable: true }, { key: "word_order", label: "Word order", reportable: true }, { key: "pronoun_reference", label: "Pronoun reference", reportable: true }, { key: "sentence_structure", label: "Sentence structure", reportable: true }, { key: "punctuation", label: "Punctuation", reportable: true }, { key: "capitalization", label: "Capitalization", reportable: true }, { key: "spelling", label: "Spelling", reportable: false }, ]; function isTaxonomyCategory(value: unknown): value is TaxonomyCategory { if (!value || typeof value !== "object") return false; const r = value as Record; return typeof r.key === "string" && typeof r.label === "string" && typeof r.reportable === "boolean"; } /** * The override file when it yields at least one valid category, the built-in default * otherwise. Never returns an empty array — that is the invariant that keeps the * extension from being installed but silently inert. */ export function loadTaxonomy(): TaxonomyCategory[] { if (!existsSync(TAXONOMY_PATH)) return DEFAULT_TAXONOMY; try { const raw = JSON.parse(readFileSync(TAXONOMY_PATH, "utf8")); if (!Array.isArray(raw)) return DEFAULT_TAXONOMY; const categories = raw.filter(isTaxonomyCategory); return categories.length > 0 ? categories : DEFAULT_TAXONOMY; } catch { return DEFAULT_TAXONOMY; } }