tools/rules/tense.rule.ts

tools/rules/tense.rule.ts is a file in Coordination Surface. 189 lines of code and 46 definitions.

import {
    ACCUMULATOR_MARKER,
    HISTORY_HEADINGS,
    PAST_MARKERS,
    STATUS_MARKERS,
    TENSE_EXEMPT,
    TENSE_EXEMPT_ROOTS,
} from "../core/constants/tense.constants.ts";
import type { RuleContext, RuleDeclaration, RuleResult } from "../core/types/rule.types.ts";
import { contentDivides, contentIsImmutable } from "../../config/surface.config.ts";
import { enclosingRecord, itemSpanFlags } from "../core/analyzers/fence.analyzer.ts";
import type { Finding } from "../core/types/segment.types.ts";

const FENCE = "```";

const isAccumulator = function isAccumulator(path: string): boolean {
    const slash = path.lastIndexOf("/");
    const name = slash === -1 ? path : path.slice(slash + 1);
    return name.startsWith(ACCUMULATOR_MARKER);
};

interface Hit {
    readonly marker: string;
    readonly line: number;
    readonly text: string;
    readonly kind: string;
}

const CODE_MARK = "`";

const HEADING_MARK = "#";

const EXCERPT = 200;

const prose = function prose(line: string): string {
    let out = "";
    let inCode = false;

    for (const char of line) {
        if (char === CODE_MARK) {
            inCode = !inCode;
        } else {
            out += inCode ? "" : char;
        }
    }

    return out.toLowerCase();
};

const isLetter = function isLetter(char: string): boolean {
    return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
};

const containsPhrase = function containsPhrase(text: string, phrase: string): boolean {
    let from = text.indexOf(phrase);

    while (from !== -1) {
        const before = from === 0 ? "" : (text[from - 1] ?? "");
        const afterIndex = from + phrase.length;
        const after = afterIndex >= text.length ? "" : (text[afterIndex] ?? "");

        if (!isLetter(before) && !isLetter(after)) {
            return true;
        }
        from = text.indexOf(phrase, from + 1);
    }

    return false;
};

const hitsIn = function hitsIn(raw: string, line: number): Hit[] {
    const text = prose(raw);
    if (text.length === 0) {
        return [];
    }

    const trimmed = raw.trim();
    const headings = raw.startsWith(HEADING_MARK) ? HISTORY_HEADINGS.filter((heading) => text.includes(heading)) : [];
    return [
        ...STATUS_MARKERS.filter((marker) => text.includes(marker)).map((marker) => ({
            kind: "retiredStatus",
            line,
            marker,
            text: trimmed,
        })),
        ...headings.map((marker) => ({ kind: "historySection", line, marker, text: trimmed })),
        ...PAST_MARKERS.filter((marker) => containsPhrase(text, marker)).map((marker) => ({
            kind: "pastTense",
            line,
            marker,
            text: trimmed,
        })),
    ];
};

const scan = function scan(source: string): Hit[] {
    const out: Hit[] = [];
    let fenced = false;

    for (const [index, raw] of source.split("\n").entries()) {
        const fence = raw.trimStart().startsWith(FENCE);
        if (!fence && !fenced) {
            out.push(...hitsIn(raw, index + 1));
        }
        fenced = fence ? !fenced : fenced;
    }

    return out;
};

const PAST_TENSE_DECIDE =
    "rewrite the sentence so it describes the current state, or move it to _changelogs.txt; a fact about third-party platform behavior is written in present tense and is not history";

const DECIDE_BY_KIND = new Map([
    [
        "retiredStatus",
        "delete the record rather than marking it — a retired record is history, and history has two homes",
    ],
    ["historySection", "move the section to _changelogs.txt — a governing document states what is true now"],
]);

const finding = function finding(path: string, hit: Hit, anchor: string | null): Finding {
    const decide = DECIDE_BY_KIND.get(hit.kind) ?? PAST_TENSE_DECIDE;

    return {
        actual: hit.text.slice(0, EXCERPT),
        expected: null,
        healed: false,
        line: hit.line,
        locus: anchor ?? hit.marker,
        path,
        remediation: { action: "none", decide, deterministic: false, from: hit.marker, target: path, to: null },
        rule: `tense/${hit.kind}`,
        stack: [
            { check: "exempt", resolved: "no" },
            { check: "mention", resolved: "no" },
            { check: "marker", resolved: hit.marker },
            { check: "anchor", resolved: anchor ?? "line" },
        ],
    };
};

export const TENSE_ROUTES = ["accumulator", "declared", "reached", "transient", "upstream"] as const;

type TenseRoute = (typeof TENSE_ROUTES)[number];

const tenseRoute = function tenseRoute(path: string): TenseRoute {
    if (TENSE_EXEMPT.includes(path)) {
        return "declared";
    }
    if (TENSE_EXEMPT_ROOTS.some((root) => path.startsWith(root))) {
        return "upstream";
    }
    if (isAccumulator(path)) {
        return "accumulator";
    }
    return contentIsImmutable(path) ? "transient" : "reached";
};

const surfaceFindings = function surfaceFindings(path: string, source: string): Finding[] {
    const hits = scan(source);
    if (!contentDivides(path)) {
        return hits.map((hit) => finding(path, hit, null));
    }

    const appendOnly = itemSpanFlags(source);
    const anchorOf = enclosingRecord(source);
    return hits.filter((hit) => appendOnly[hit.line - 1] !== true).map((hit) => finding(path, hit, anchorOf(hit.line)));
};

export const rule: RuleDeclaration = {
    check(context: RuleContext): RuleResult {
        const routed = context.paths.map((path) => ({ path, route: tenseRoute(path) }));
        const pathsOn = (route: TenseRoute): string[] =>
            routed.filter((entry) => entry.route === route).map((entry) => entry.path);

        const reached = pathsOn("reached");
        const skippedByDeclaredPath = pathsOn("declared");
        const skippedByUpstreamRoot = pathsOn("upstream");
        const skippedAsAccumulator = pathsOn("accumulator");
        const skippedAsTransientVenue = pathsOn("transient");
        const dividedSurfaces = reached.filter((path) => contentDivides(path));
        const findings = reached.flatMap((path) => surfaceFindings(path, context.read(path)));

        return {
            derivations: {
                anchorReason:
                    "on a surface whose declared lifetime DIVIDES, a finding's locus is the enclosing RECORD rather " +
                    "than a line, because such a surface only ever grows: every position any party appends moves every " +
                    "line below it, so a reported line points further above its subject at a constant rate while the " +
                    "report still reads as current, and a reader joining that line against the live file lands the " +
                    "finding in whichever span happened to grow. The record letter is the anchor an append cannot " +
                    "move. The line is kept beside it because it is exact at the moment of the run, and the two are " +
                    "told apart by the DECLARED lifetime rather than by the file's extension",
                dividedReason:
                    "a surface whose declared lifetime DIVIDES is scanned in its owner-rewritable region and skipped in " +
                    "its append-only one, because a finding on a span no party may rewrite is a report nobody can drain " +
                    "— the unreachable-remediation shape, whose cost lands on the findings beside it rather than on " +
                    "itself. The declaration decides THAT a surface splits and the span mechanism decides WHERE, so no " +
                    "prose is parsed and the one config stays the only truth",
                dividedSurfaces,
                reached,
                scanned: reached.length,
                skippedAsAccumulator,
                skippedAsTransientVenue,
                skippedByDeclaredPath,
                skippedByUpstreamRoot,
            },
            findings,
            healed: [],
        };
    },
    extensions: [".md"],
    heals: false,
    invariant:
        "every governed document that OUTLIVES its writing states what is true now and carries no history of this project; a transient surface is governed on its FORMAT and never on its content",
    jurisdiction: "taxonomy",
    kinds: ["retiredStatus", "historySection", "pastTense"],

    stage: "content",
};