cycleWarnings
What to say of the cycles a save makes or grows: definitions that refer to each other are hashed as one, so a change to any re-hashes and re-checks all of them, and editing one brings the rest along. One that's a cycle only because types name each other (a function taking the class that calls it) says which, since an interface of what's needed breaks it; a big one of code calling itself says so too. Cycles that were there already, as big, aren't mentioned again.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { BIG_CYCLE } from "#codebase/tcmts/cli/main"; import type { Row } from "#codebase/tcmts/cli/main"; import { tarjan } from "#codebase/tcmts/core/analyze"; export function cycleWarnings(cb: Codebase, rows: Row[]): string[] { const out: string[] = []; const byComponent = new Map<string, Row[]>(); for (const r of rows) byComponent.set(r.def.componentHash, [...(byComponent.get(r.def.componentHash) ?? []), r]); for (const members of byComponent.values()) { const size = members[0]!.def.componentSize; if (size < 2 || members.length < size) continue; // as big as before: nothing new to say const before = Math.max( 0, ...members.flatMap((m) => cb.store.hashesAt(m.path).map((h) => cb.store.getDef(h)?.componentSize ?? 0), ), ); if (size <= before) continue; const index = new Map(members.map((m, i) => [m.def.hash, i])); const edges = (pos: "value" | "type") => members.map((m) => [ ...new Set( m.def.deps .filter((d) => d.pos === pos) .map((d) => index.get(d.ref.slice(1))) .filter((i): i is number => i !== undefined), ), ]); const value = edges("value"); const type = edges("type"); const parts = tarjan(size, (v) => value[v]!); const names = members.map((m) => m.path); const listed = `${names.slice(0, 3).join(", ")}${names.length > 3 ? ` and ${names.length - 3} more` : ""}`; const head = `${listed} ${before ? "grew into" : "became"} one cycle (${size} definitions): they're hashed as one, so a change to any re-hashes and re-checks them all.`; if (parts.length > 1) { // the type references that join what calling alone doesn't const partOf = new Map<number, number>(); parts.forEach((p, k) => p.forEach((v) => partOf.set(v, k))); const joining = type .flatMap((ts, from) => ts .filter((to) => partOf.get(to) !== partOf.get(from)) .map((to) => `${names[from]}'s types name ${names[to]}`), ) .slice(0, 3); out.push( `${head} It's a cycle only because types name each other (${joining.join("; ")}): typing those with an interface of what they need, not the definition itself, breaks it.`, ); } else if (size >= BIG_CYCLE) out.push(`${head} They call each other: splitting them where you can keeps changes small.`); } return out; }