core/analyzers/document.analyzer.ts

core/analyzers/document.analyzer.ts is a file in GovLab Docs. 138 lines of code and 28 definitions.

import type { Categories, Finding, NameFinding } from "#types/finding.types";
import type { DocMeta, DocNode } from "#types/document.types";
import { GENERATED_MARKER, README_FILE } from "#configuration/constants/document.constants";
import { brokenPaths, frontmatterFileRefs } from "#core/analyzers/reference.analyzer";
import { docBasename, docStem, spineProfileFor, stemParts } from "#core/selectors/metadata.selector";
import { isBoundaryFilename, isHarnessOwned, isHostBoundary } from "#core/predicates/document.predicate";
import { AGENT_FRONTMATTER_SCHEMA } from "#configuration/schemas/agent.schema";
import { DOC_ARCH_FRONTMATTER_SCHEMA } from "#configuration/schemas/document.schema";
import { DOC_TYPE_SCHEMAS } from "#configuration/schemas/readme.schema";
import type { DocEntry } from "#types/location.types";
import { REF_CLAIMS } from "#configuration/constants/verb.constants";
import type { RefScan } from "#types/reference.types";
import type { ValidateCtx } from "#types/environment.types";
import { bannedLanguage } from "#core/analyzers/vocabulary.analyzer";
import { conventionHits } from "#core/analyzers/markdown.analyzer";
import { docLocation } from "#core/validators/location.validator";
import { docName } from "#core/validators/filename.validator";
import { docNodeOf } from "#core/converters/document.converter";
import { frontmatterSchema } from "#core/validators/metadata.validator";
import { mermaidHardening } from "#core/analyzers/diagram.analyzer";
import { offSchema } from "#core/validators/section.validator";
import { parseFrontmatter } from "#core/parsers/metadata.parser";
import { refConstructs } from "#core/validators/reference.validator";
import { validateSpine } from "#core/validators/document.validator";

const README_TYPE = "readme";
const NAME_FIELD = "name";
const TYPE_FIELD = "type";
const CONCERN_FIELD = "concern";
const MEMBER_DOT = ".";
const POSIX_SEPARATOR = "/";
const GENERATED_SEGMENT = `${MEMBER_DOT}${GENERATED_MARKER}${MEMBER_DOT}`;
const README_SUFFIX = `${POSIX_SEPARATOR}${README_FILE}`;

export const docMeta = function docMeta(context: ValidateCtx, doc: string, source: string): DocMeta {
    const relDoc = context.relative(doc);
    const { fields } = parseFrontmatter(source);
    const docFilename = docBasename(relDoc);
    return {
        delegated: context.delegatedRoots.some((prefix) => relDoc.startsWith(prefix)),
        doc,
        docFilename,
        fields,
        fmConcern: fields[CONCERN_FIELD] ?? "",
        fmType: fields[TYPE_FIELD],
        harnessOwned: isHarnessOwned(relDoc, context.harnessRoot),
        hostBoundary: isHostBoundary(docFilename, context.boundaryDocs),
        relDoc,
        source,
    };
};

const locationFindings = function locationFindings(context: ValidateCtx, meta: DocMeta): Finding[] {
    if (meta.harnessOwned || meta.hostBoundary || meta.relDoc.includes(GENERATED_SEGMENT)) {
        return [];
    }
    return docLocation({
        concern: meta.fmConcern,
        form: meta.fmType ?? "",
        kind: "authored",
        options: context.locationOptions,
        registries: context.registries,
        relPath: meta.relDoc,
    });
};

const sectionFindings = function sectionFindings(meta: DocMeta): Finding[] {
    const schema = meta.fmType === undefined ? undefined : DOC_TYPE_SCHEMAS[meta.fmType];
    if (schema === undefined || (meta.fmType === README_TYPE && !meta.relDoc.endsWith(README_SUFFIX))) {
        return [];
    }
    return offSchema(meta.source, schema);
};

export const refScanOf = function refScanOf(context: ValidateCtx, meta: DocMeta): RefScan {
    return refConstructs(meta.source, { claims: REF_CLAIMS, verbs: context.userReg.refVerbs });
};

const agentSchemaFindings = function agentSchemaFindings(context: ValidateCtx, meta: DocMeta): Finding[] {
    const agentDir = context.harnessAgentDir;
    return agentDir !== null && meta.relDoc.startsWith(agentDir)
        ? frontmatterSchema(meta.source, AGENT_FRONTMATTER_SCHEMA)
        : [];
};

const spineFindings = function spineFindings(context: ValidateCtx, meta: DocMeta): Finding[] {
    const profile = spineProfileFor(meta.relDoc, context);
    return validateSpine(meta.source, profile.keys, {
        requireFrontmatter: profile.requireFrontmatter,
        requireTitle: profile.requireTitle,
    });
};

const structuralFindings = function structuralFindings(context: ValidateCtx, meta: DocMeta): Categories {
    if (meta.delegated) {
        return { agentSchema: [], docStatus: [], location: [], sections: [], spine: [] };
    }
    return {
        agentSchema: agentSchemaFindings(context, meta),
        docStatus: meta.relDoc.startsWith(context.rootPrefix)
            ? frontmatterSchema(meta.source, DOC_ARCH_FRONTMATTER_SCHEMA)
            : [],
        location: locationFindings(context, meta),
        sections: sectionFindings(meta),
        spine: spineFindings(context, meta),
    };
};

export const analyzeSync = function analyzeSync(context: ValidateCtx, meta: DocMeta): Categories {
    return {
        ...structuralFindings(context, meta),
        broken: [
            ...brokenPaths(context.paths, meta.doc, meta.source),
            ...frontmatterFileRefs(context.paths, meta.doc, meta.source),
        ],
        conventions: conventionHits(meta.source),
        mermaid: mermaidHardening(meta.source),
        refs: refScanOf(context, meta).defects,
        smell: meta.harnessOwned ? [] : bannedLanguage(meta.source),
    };
};

export const locationEntryOf = function locationEntryOf(context: ValidateCtx, meta: DocMeta): DocEntry | null {
    if (meta.delegated || meta.harnessOwned || meta.hostBoundary || isBoundaryFilename(meta.docFilename)) {
        return null;
    }
    const form = meta.fmType ?? "";
    return {
        concern: meta.fmConcern,
        form,
        ...stemParts(docStem(meta.relDoc), context.registries.forms[form]?.tag),
        source: meta.relDoc,
    };
};

export const docNodeFor = function docNodeFor(context: ValidateCtx, meta: DocMeta): DocNode | null {
    return meta.relDoc.startsWith(context.rootPrefix) && typeof meta.fields[NAME_FIELD] === "string"
        ? docNodeOf(meta.relDoc, meta.fields)
        : null;
};

export const nameFindingFor = function nameFindingFor(context: ValidateCtx, meta: DocMeta): NameFinding | null {
    const form = meta.fmType === undefined ? undefined : context.userReg.forms[meta.fmType];
    const name = meta.fields[NAME_FIELD];
    if (!meta.relDoc.startsWith(context.rootPrefix) || name === undefined || form === undefined) {
        return null;
    }
    const names = docName({ activityVerbs: context.userReg.activityVerbs, concern: meta.fmConcern, form, name });
    return names.length > 0 ? { names, relDoc: meta.relDoc } : null;
};