feat(ui): dead code and islands — what nothing reaches, and everything that could still reach it (CG-59)

A Dead code screen and a mark on the Map, both drawn from one derivation in
src/graph/dead-code.ts so a second surface can never disagree with the first.

The SQL half is four lines — no incoming edge but `contains`. It returns ~2 500
candidates on this repository and the shipped list is 20; everything in between
is the feature. A candidate is dropped the moment there is any reason to believe
something outside the graph reaches it: exported symbols and header
declarations, test and generated files, abstract and interface members, anything
carrying a `decorates` edge, overrides of an ancestor's member, names the
language calls by itself, vendored directories, files nothing in the index
reaches (those are islands, and the Map says so instead), names the resolver
failed to resolve somewhere, and names shared with a symbol that IS referenced —
the mis-resolution that leaves a used method with a self-edge and its twin with
nothing. The last rule is the only one that is not a graph query: before a claim
is made, the declaring file and every file that reaches it are read and the
identifier counted, which is what catches the references the extractor never
recorded (`this.handleMessage.bind(this)`, a call inside an object literal, a
shorthand property).

Every subtraction is counted and printed under the list with the scale it came
from, and the caveat line above it never collapses: the claim is "no static
reference in the index", not "unused".

On the Map a module nothing depends on keeps its stroke and says so in its count
line, and tool-generated files and modules recede to ink-4 there, in the map's
file list, in search results and on the file screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-27 07:57:00 -05:00
co-authored by Claude Opus 5
parent 2a0c6dc58f
commit 56dfdb0655
32 changed files with 2800 additions and 39 deletions
+55
View File
@@ -34,6 +34,12 @@ export interface WireNodeRef {
exported?: boolean;
/** Lives in a file that looks like test or fixture code. */
test: boolean;
/**
* Lives in a tool-generated file, so the row draws in ink-4. Optional: only
* the endpoints that show it pay for the lookup, so `undefined` means "not
* asked", never "no".
*/
generated?: boolean;
}
export interface WireNodeDetail extends WireNodeRef {
@@ -594,6 +600,10 @@ export interface WireMapModule {
languages: Array<{ language: string; files: number }>;
/** More than half its files are tests. */
test: boolean;
/** How many of its files are tool-generated. All of them → drawn in ink-4. */
generated: number;
/** Which of `fileList.items` are generated, so a row in the panel can dim too. */
generatedFiles: string[];
/** A single file kept out of the root bucket because it is the façade. */
facade: boolean;
/** Its files, capped — the side panel's list when the module is selected. */
@@ -631,3 +641,48 @@ export interface WireMapPayload {
index: { lastIndexedAt: number | null; edges: number; files: number };
timing: { elapsedMs: number; cached: boolean };
}
/* -------------------------------------------------------------- dead code -- */
/** One symbol nothing in the index reaches. */
export interface WireDeadCodeRow extends WireNodeRef {
/** Source lines it spans — the rank, and what deleting it would remove. */
lines: number;
/** Unreferenced members inside it: a dead class takes its methods with it. */
members: WireList<WireNodeRef>;
}
/** The rows of one file, in source order. */
export interface WireDeadCodeGroup {
file: string;
/** Tool-generated — drawn dimmed wherever it appears. */
generated: boolean;
test: boolean;
lines: number;
rows: WireDeadCodeRow[];
}
/** One reason candidates were dropped, already worded for the screen. */
export interface WireDeadCodeExclusion {
reason: string;
count: number;
label: string;
}
export interface WireDeadCode {
rows: WireList<WireDeadCodeRow>;
/** The SHOWN rows, grouped by file — group order follows the best row. */
groups: WireDeadCodeGroup[];
/** Symbols with no incoming reference at all, before any exclusion ran. */
candidates: number;
excluded: WireDeadCodeExclusion[];
excludedTotal: number;
kinds: string[];
includeExported: boolean;
includeTests: boolean;
includeGenerated: boolean;
bounded: boolean;
/** Every row was checked against the text of the files that can reach it. */
corroborated: boolean;
timing: { elapsedMs: number };
}