tools/rules/placement.rule.ts

tools/rules/placement.rule.ts is a file in Coordination Surface. 140 lines of code and 21 definitions.

import type { Finding, Remediation } from "../core/types/segment.types.ts";

import type { RuleContext, RuleDeclaration, RuleResult } from "../core/types/rule.types.ts";
import { basename, dirname } from "node:path";
import { checkPlacement, parseFilename } from "../core/validators/taxonomy.validator.ts";
import { BLOCKING_SUFFIX } from "../core/constants/blocking.constants.ts";
import { PLACEMENT_CODES } from "../core/types/taxonomy.types.ts";
import type { TaxonomyData } from "../core/types/taxonomy.types.ts";
import { UPSTREAM_ROOTS } from "../core/constants/path.constants.ts";
import { governedPath } from "../core/resolvers/taxonomy.resolver.ts";
import { isImmutable } from "../core/predicates/placement.predicate.ts";
import { surfacePath } from "../../config/surface.config.ts";

const folderForTag = function folderForTag(tag: string, data: TaxonomyData): string | null {
    return data.concernFolders.find((folder) => data.folderToTag[folder] === tag) ?? null;
};

const remediate = function remediate(code: string, path: string, name: string, data: TaxonomyData): Remediation {
    if (code === "looseFileAtRoot" || code === "parentNotConcern") {
        const { parsed } = parseFilename(name, data);
        if (parsed !== null) {
            const folder = folderForTag(parsed.concern, data);
            if (folder !== null) {
                return {
                    action: "move",
                    decide: "both operands are computed and this still does NOT heal, which is a property of the OPERATION rather than of the derivation: moving a file changes what every importer of it resolves, so the repair is not confined to the artifact the remediation names and applying it alone breaks the tree. A remediation heals where its effect is bounded by its own operands; this one is deterministic and unbounded, and the missing half is a reference rewrite this package does not perform. Take the move and the importers together, by hand, in one change",
                    deterministic: true,
                    from: path,
                    target: `${dirname(path)}/${folder}`,
                    to: `${dirname(path)}/${folder}/${name}`,
                };
            }
        }
        return {
            action: "classify",
            decide: "read the file, name the narrowest accurate concern, then place it in that concern folder",
            deterministic: false,
            from: path,
            target: path,
            to: null,
        };
    }

    const decideBy: Record<string, string> = {
        dottedFolder: "a folder carries one word and never a dot — choose the single word it is",
        nestedInSpecial: "a bucket holds files and never folders — move the folder out or stop declaring the bucket",
        roleOutOfOrder: "roles resolve container < subject < concern and are never revisited — reorder or drop a level",
        tooDeep: "relieve sideways: a collision takes the variant slot, breadth takes a sibling subject folder",
        undeclaredContainer:
            "work the container ladder: subject folder, concern folder, already grouped, or not source at all — only a new grouping axis earns a container",
    };

    return {
        action: "move",
        decide: decideBy[code] ?? `resolve placement code ${code}`,
        deterministic: false,
        from: path,
        target: path,
        to: null,
    };
};

export const PLACEMENT_ROUTES = ["boundary", "immutable", "outside", "reached", "upstream"] as const;

type PlacementRoute = (typeof PLACEMENT_ROUTES)[number];

interface PlacementOutcome {
    readonly path: string;
    readonly route: PlacementRoute;
    readonly finding: Finding | null;
}

const earlyRoute = function earlyRoute(path: string, board: string): PlacementRoute | null {
    if (UPSTREAM_ROOTS.some((root) => path.startsWith(root))) {
        return "upstream";
    }
    if (path === board) {
        return "boundary";
    }
    return isImmutable(path) ? "immutable" : null;
};

const placementFinding = function placementFinding(
    path: string,
    root: string,
    segments: readonly string[],
    data: TaxonomyData,
): Finding | null {
    const name = basename(path);
    const verdict = checkPlacement(root, segments, data, name);
    if (verdict.ok) {
        return null;
    }

    const remediation = remediate(verdict.code ?? "", path, name, data);
    return {
        actual: path,
        expected: remediation.to,
        healed: false,
        line: 0,
        locus: verdict.evidence,
        path,
        remediation,
        rule: `placement/${verdict.code ?? ""}`,
        stack: [
            { check: "jurisdiction", resolved: `root=${root}` },
            { check: "segments", resolved: segments.length === 0 ? "(none)" : segments.join("/") },
            { check: "placement", resolved: verdict.code ?? "fail" },
        ],
    };
};

const placementOutcome = function placementOutcome(path: string, data: TaxonomyData, board: string): PlacementOutcome {
    const early = earlyRoute(path, board);
    if (early !== null) {
        return { finding: null, path, route: early };
    }

    const governed = governedPath(path, data);
    if (governed === null) {
        return { finding: null, path, route: "outside" };
    }

    const name = basename(path);
    if (data.boundaryDocuments.includes(name) || name.endsWith(BLOCKING_SUFFIX)) {
        return { finding: null, path, route: "boundary" };
    }

    return { finding: placementFinding(path, governed.root, governed.segments, data), path, route: "reached" };
};

export const rule: RuleDeclaration = {
    check(context: RuleContext): RuleResult {
        const board = surfacePath("board");
        const outcomes = context.paths.map((path) => placementOutcome(path, context.taxonomy, board));
        const pathsOn = (route: PlacementRoute): string[] =>
            outcomes.filter((outcome) => outcome.route === route).map((outcome) => outcome.path);

        return {
            derivations: {
                reached: pathsOn("reached"),
                skippedAsBoundaryDocument: pathsOn("boundary"),
                skippedAsImmutable: pathsOn("immutable"),
                skippedAsOutsideJurisdiction: pathsOn("outside"),
                skippedAsUpstream: pathsOn("upstream"),
            },
            findings: outcomes.flatMap((outcome) => (outcome.finding === null ? [] : [outcome.finding])),
            healed: [],
        };
    },
    extensions: [],
    heals: false,
    invariant: "every governed file resolves through the ordered roles to a concern folder within the depth cap",
    jurisdiction: "taxonomy",
    kinds: [...PLACEMENT_CODES],

    stage: "structure",
};