tools/rules/reference.rule.ts
tools/rules/reference.rule.ts is a file in Coordination Surface. 174 lines of code and 37 definitions.
import { AXIS_DOCUMENTS, UPSTREAM_ROOTS } from "../core/constants/path.constants.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 { cycleFinding, unresolvedFinding } from "../core/factories/reference.factory.ts";
import { existsSync, readdirSync } from "node:fs";
import { BOARD_PATH } from "../core/constants/board.constants.ts";
import type { Finding } from "../core/types/segment.types.ts";
import type { Reference } from "../core/types/reference.types.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";
import { referencesIn } from "../core/matchers/reference.matcher.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);
};
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 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 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",
};