# tools/rules/reference.rule.ts

> 237 lines of code and 42 definitions.

Tree: Coordination tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-rules-reference-rule-ts
Source text: https://banes-lab.com/assets/sources/source.d995e51c349c9fbef0f8c1eb91a88ec0ea80767fa04b1066e0cbd5bcfdaf49ea.generated.txt

## Definitions

- `directoriesIn` (lexical_declaration, line 14)
- `unresolvedFinding` (lexical_declaration, line 129)
- `recordEdge` (lexical_declaration, line 78)
- `remediate` (lexical_declaration, line 91)
- `rootsOf` (lexical_declaration, line 20)
- `referenceOutcome` (lexical_declaration, line 153)
- `check` (method_definition, line 212, exported)
- `namesInTree` (lexical_declaration, line 30)
- `slash` (lexical_declaration, line 35)
- `resolvesAgainstAncestor` (lexical_declaration, line 39)
- `directory` (lexical_declaration, line 44)
- `cut` (lexical_declaration, line 51)
- `settledTarget` (lexical_declaration, line 61)
- `prefix` (lexical_declaration, line 73)
- `prefixed` (lexical_declaration, line 74)
- `cited` (lexical_declaration, line 86)
- `ResolveScope` (interface_declaration, line 115)
- `ReferenceOutcome` (interface_declaration, line 122)
- `NO_OUTCOME` (lexical_declaration, line 127)
- `candidate` (lexical_declaration, line 134)
- `settled` (lexical_declaration, line 154)
- `candidates` (lexical_declaration, line 159)
- `ignored` (lexical_declaration, line 160)
- `groupByBasename` (lexical_declaration, line 166)
- `byBasename` (lexical_declaration, line 167)
- `reachableOf` (lexical_declaration, line 174)
- `axes` (lexical_declaration, line 175)
- `board` (lexical_declaration, line 178)
- `cycleFinding` (lexical_declaration, line 182)
- `entry` (lexical_declaration, line 183)
- `byName` (lexical_declaration, line 207)
- `rule` (lexical_declaration, line 211, exported)
- `scope` (lexical_declaration, line 213, exported)
- `reachable` (lexical_declaration, line 220, exported)
- `owned` (lexical_declaration, line 221, exported)
- `skippedAsImmutableContent` (lexical_declaration, line 222, exported)
- `walked` (lexical_declaration, line 223, exported)
- `reached` (lexical_declaration, line 224, exported)
- `outcomes` (lexical_declaration, line 226, exported)
- `edges` (lexical_declaration, line 232, exported)
- `cycles` (lexical_declaration, line 239, exported)
- `findings` (lexical_declaration, line 240, exported)

## Uses

- [config/surface.config.ts](https://banes-lab.com/source/coordination/config/surface.config.ts.md)

## Source

```typescript
import { AXIS_DOCUMENTS, UPSTREAM_ROOTS } from "../core/constants/path.constants.ts";
import type { Finding, Remediation } from "../core/types/segment.types.ts";

import { type Reference, referencesIn } from "../core/matchers/reference.matcher.ts";
import type { RuleContext, RuleDeclaration, RuleResult } from "../core/types/rule.types.ts";
import { basename, dirname, resolve } from "node:path";
import { contentIsImmutable, surfacePrefix, surfaceRoot } from "../../config/surface.config.ts";
import { existsSync, readdirSync } from "node:fs";
import { BOARD_PATH } from "../core/constants/board.constants.ts";
import type { TaxonomyData } from "../core/types/taxonomy.types.ts";
import { cyclesIn } from "../core/analyzers/graph.analyzer.ts";
import { readDocument } from "../core/readers/document.reader.ts";

const directoriesIn = function directoriesIn(root: string): string[] {
    return readdirSync(root, { withFileTypes: true })
        .filter((entry) => entry.isDirectory())
        .map((entry) => entry.name);
};

const rootsOf = function rootsOf(repoRoot: string, taxonomy: TaxonomyData): ReadonlySet<string> {
    return new Set([
        ...directoriesIn(repoRoot),
        ...directoriesIn(surfaceRoot()),
        ...taxonomy.concernFolders,
        ...Object.values(taxonomy.containers).flat(),
        ...Object.values(taxonomy.specialContainers).flat(),
    ]);
};

const namesInTree = function namesInTree(target: string, roots: ReadonlySet<string>, candidates: number): boolean {
    if (candidates > 0) {
        return true;
    }

    const slash = target.indexOf("/");
    return slash !== -1 && roots.has(target.slice(0, slash));
};

const resolvesAgainstAncestor = function resolvesAgainstAncestor(
    path: string,
    target: string,
    existing: ReadonlySet<string>,
): boolean {
    let directory = dirname(path);

    while (directory.length > 0 && directory !== ".") {
        if (existing.has(`${directory}/${target}`)) {
            return true;
        }

        const cut = directory.lastIndexOf("/");
        if (cut === -1) {
            break;
        }
        directory = directory.slice(0, cut);
    }

    return existing.has(`${directory}/${target}`);
};

const settledTarget = function settledTarget(
    context: RuleContext,
    target: string,
    existing: ReadonlySet<string>,
): string | null {
    if (existing.has(target)) {
        return target;
    }
    if (existsSync(resolve(context.repoRoot, target))) {
        return target;
    }

    const prefix = surfacePrefix();
    const prefixed = prefix.length === 0 ? target : `${prefix}/${target}`;
    return existsSync(resolve(context.repoRoot, prefixed)) ? prefixed : null;
};

const recordEdge = function recordEdge(edges: Map<string, Set<string>>, from: string, to: string): void {
    if (from === to) {
        return;
    }
    if (!to.endsWith(".md")) {
        return;
    }

    const cited = edges.get(from) ?? new Set<string>();
    cited.add(to);
    edges.set(from, cited);
};

const remediate = function remediate(reference: Reference, path: string, candidate: string | null): Remediation {
    if (candidate !== null) {
        return {
            action: "rename",
            decide: null,
            deterministic: true,
            from: reference.target,
            target: path,
            to: candidate,
        };
    }

    return {
        action: "rename",
        decide:
            "the referenced file does not exist and no single file in the tree carries its basename — " +
            "locate the intended target, or drop the reference if what it pointed at is gone",
        deterministic: false,
        from: reference.target,
        target: path,
        to: null,
    };
};

interface ResolveScope {
    readonly context: RuleContext;
    readonly existing: ReadonlySet<string>;
    readonly roots: ReadonlySet<string>;
    readonly byBasename: ReadonlyMap<string, readonly string[]>;
}

interface ReferenceOutcome {
    readonly edge: string | null;
    readonly finding: Finding | null;
}

const NO_OUTCOME: ReferenceOutcome = { edge: null, finding: null };

const unresolvedFinding = function unresolvedFinding(
    path: string,
    reference: Reference,
    candidates: readonly string[],
): Finding {
    const candidate = candidates.length === 1 ? (candidates[0] ?? null) : null;
    return {
        actual: reference.target,
        expected: candidate,
        healed: false,
        line: reference.line,
        locus: reference.locus,
        path,
        remediation: remediate(reference, path, candidate),
        rule: "reference/unresolved",
        stack: [
            { check: "scheme", resolved: "in-tree" },
            { check: "rootRelative", resolved: "absent" },
            { check: "documentRelative", resolved: "absent" },
            { check: "basenameCandidates", resolved: String(candidates.length) },
        ],
    };
};

const referenceOutcome = function referenceOutcome(path: string, reference: Reference, scope: ResolveScope): ReferenceOutcome {
    const settled = settledTarget(scope.context, reference.target, scope.existing);
    if (settled !== null) {
        return { edge: settled, finding: null };
    }

    const candidates = scope.byBasename.get(basename(reference.target)) ?? [];
    const ignored =
        resolvesAgainstAncestor(path, reference.target, scope.existing) ||
        !namesInTree(reference.target, scope.roots, candidates.length);
    return ignored ? NO_OUTCOME : { edge: null, finding: unresolvedFinding(path, reference, candidates) };
};

const groupByBasename = function groupByBasename(paths: readonly string[]): Map<string, string[]> {
    const byBasename = new Map<string, string[]>();
    for (const path of paths) {
        byBasename.set(basename(path), [...(byBasename.get(basename(path)) ?? []), path]);
    }
    return byBasename;
};

const reachableOf = function reachableOf(context: RuleContext): string[] {
    const axes = AXIS_DOCUMENTS.filter(
        (axis) => context.paths.includes(axis) || existsSync(resolve(context.repoRoot, axis)),
    );
    const board = existsSync(resolve(context.repoRoot, BOARD_PATH)) ? [BOARD_PATH] : [];
    return [...new Set([...context.paths, ...axes, ...board])];
};

const cycleFinding = function cycleFinding(cycle: readonly string[], graphSize: number): Finding {
    const entry = cycle[0] ?? "";
    return {
        actual: `${cycle.join(" > ")} > ${entry}`,
        expected: null,
        healed: false,
        line: 1,
        locus: cycle.join(" > "),
        path: entry,
        remediation: {
            action: "none",
            decide: "a closed citation loop has no entry point, so a reader following the graph to understand the subject is returned to where they started and no document in the loop is the one that states the thing. Break it by deciding which document OWNS the subject and making the others cite it one-way — never by deleting a citation to satisfy the count, because the loop is a statement that ownership was never settled",
            deterministic: false,
            from: entry,
            target: entry,
            to: null,
        },
        rule: "reference/citationCycle",
        stack: [
            { check: "graph", resolved: String(graphSize) },
            { check: "cycleLength", resolved: String(cycle.length) },
        ],
    };
};

const byName = function byName(left: string, right: string): number {
    return left.localeCompare(right, "en");
};

export const rule: RuleDeclaration = {
    check(context: RuleContext): RuleResult {
        const scope: ResolveScope = {
            byBasename: groupByBasename(context.paths),
            context,
            existing: new Set(context.paths),
            roots: rootsOf(context.repoRoot, context.taxonomy),
        };

        const reachable = reachableOf(context);
        const owned = reachable.filter((path) => !UPSTREAM_ROOTS.some((root) => path.startsWith(root)));
        const skippedAsImmutableContent = owned.filter((path) => contentIsImmutable(path));
        const walked = owned.filter((path) => !contentIsImmutable(path));
        const reached = reachable.filter((path) => !skippedAsImmutableContent.includes(path));

        const outcomes = walked.flatMap((path) =>
            readDocument(path, context.read(path))
                .segments.flatMap((segment) => referencesIn(segment))
                .map((reference) => ({ path, ...referenceOutcome(path, reference, scope) })),
        );

        const edges = new Map<string, Set<string>>();
        for (const { edge, path } of outcomes) {
            if (edge !== null) {
                recordEdge(edges, path, edge);
            }
        }

        const cycles = cyclesIn(edges);
        const findings = [
            ...outcomes.flatMap((outcome) => (outcome.finding === null ? [] : [outcome.finding])),
            ...cycles.map((cycle) => cycleFinding(cycle, edges.size)),
        ];

        return {
            derivations: {
                citing: [...edges.keys()].toSorted(byName),
                cycles: cycles.map((cycle) => cycle.join(" > ")),
                reached: reached.toSorted(byName),
                skippedAsImmutableContent: skippedAsImmutableContent.toSorted(byName),
            },
            findings,
            healed: [],
        };
    },
    extensions: [".md"],
    heals: false,
    invariant:
        "every in-tree reference in a governed document and on the coordination board resolves to a file that exists, and no set of documents cites itself in a closed loop",
    jurisdiction: "taxonomy",
    kinds: ["unresolved", "citationCycle"],
    reads: [...AXIS_DOCUMENTS, BOARD_PATH],
    readsTree:
        "a reference resolves against the whole tree rather than against the readable path set — it may " +
        "point at a binary, a generated artifact or a directory none of which the run hands to a rule — so " +
        "this rule reaches past the context by construction",

    stage: "content",
};
```
