feat(ui): the metric wiki — single-source entries, WikiPanel, term chips

Every number Atlas shows gets a wiki entry: one tooltip-sized sentence,
why it matters, how it is computed with named caps, what it deliberately
does not cover, where it appears, and its paired counter-metric. Four
'refused' entries document metrics Atlas will not show (Maintainability
Index, Halstead, coverage gates, quality grades) and why.

Entries live in one typed module (word-budget-enforced by test) and are
rendered by a docked WikiPanel with a navigation stack — terms inside the
panel are chips too, so concepts link without nested tooltips. Dotted-
underline MetricChips wire the Dashboard and Symbol surfaces in; section
headings ship en+zh.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-08-27 12:23:50 +02:00
parent f450f9b13e
commit 3f3f1ac486
10 changed files with 772 additions and 16 deletions
+12
View File
@@ -10,6 +10,7 @@ import { ChangesTab } from "./components/ChangesTab";
import { DashboardTab } from "./components/DashboardTab";
import { CommandK } from "./components/CommandK";
import { ProjectSwitcher } from "./components/ProjectSwitcher";
import { WikiPanel } from "./components/WikiPanel";
import {
PromptBasketProvider,
PromptBasketDrawer,
@@ -74,6 +75,8 @@ export function App() {
const t = useUiMessages();
const [route, setRoute] = useState<RouteState>(readRoute);
const [commandOpen, setCommandOpen] = useState(false);
/* The metric wiki: any dotted-underline term opens its entry here. */
const [wikiSlug, setWikiSlug] = useState<string | null>(null);
/* The galaxy keeps its WebGL context and layout across tab switches:
* mounted once visited, hidden with `invisible` (never display:none, which
* would zero the canvas), frameloop paused while hidden. */
@@ -280,6 +283,7 @@ export function App() {
project={selectedProject}
onOpenSymbol={(ref) => openSymbol(ref)}
onOpenModulesPath={(path) => navigate({ tab: "modules", path })}
onOpenWiki={setWikiSlug}
/>
) : activeTab === "symbol" && selectedProject && symbolRef ? (
<SymbolTab
@@ -287,6 +291,7 @@ export function App() {
symbolRef={symbolRef}
onOpenSymbol={openSymbol}
onOpenRegion={openRegion}
onOpenWiki={setWikiSlug}
/>
) : activeTab === "control" ? (
<ControlTab />
@@ -303,6 +308,13 @@ export function App() {
onClose={() => setCommandOpen(false)}
onOpenSymbol={openSymbol}
/>
{wikiSlug && (
<WikiPanel
slug={wikiSlug}
onClose={() => setWikiSlug(null)}
onNavigate={setWikiSlug}
/>
)}
<PromptBasketDrawer project={selectedProject} />
</div>
</PromptBasketProvider>
+34 -9
View File
@@ -4,9 +4,10 @@
* tones and trends; inventory facts live in the footer; histograms exist
* only with a computed takeaway. No composite score — the cards are the
* score. Everything derives from the graph and one bounded git-log pass. */
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { kneeCount, costSentence, findingKey, isDismissed, dismiss } from "../lib/findings";
import { MetricChip } from "./MetricChip";
interface MetricEntry {
qn?: string;
@@ -54,6 +55,7 @@ interface DashboardTabProps {
project: string;
onOpenSymbol: (ref: { qn: string }) => void;
onOpenModulesPath: (path: string) => void;
onOpenWiki: (slug: string) => void;
}
const CPLX_LABELS = ["1", "2–5", "6–10", "11–20", "21–50", ">50"];
@@ -90,7 +92,7 @@ function QualityCard({
tone,
trend,
}: {
label: string;
label: ReactNode;
value: string;
sub?: string;
tone?: "warn" | "crit";
@@ -119,7 +121,7 @@ function TopList({
total,
onOpenSymbol,
}: {
title: string;
title: ReactNode;
entries: MetricEntry[];
unit: string;
/* Size of the population the list was ranked over — only when the payload
@@ -173,7 +175,12 @@ function TopList({
);
}
export function DashboardTab({ project, onOpenSymbol, onOpenModulesPath }: DashboardTabProps) {
export function DashboardTab({
project,
onOpenSymbol,
onOpenModulesPath,
onOpenWiki,
}: DashboardTabProps) {
const [metrics, setMetrics] = useState<MetricsPayload | null>(null);
const [error, setError] = useState<string | null>(null);
const [showMuted, setShowMuted] = useState(false);
@@ -270,7 +277,9 @@ export function DashboardTab({ project, onOpenSymbol, onOpenModulesPath }: Dashb
<div className="bg-card border border-border/50 rounded-md p-5 mb-4">
<div className="flex items-baseline gap-3 flex-wrap">
<h2 className="text-[17px] font-semibold text-foreground/90">
Where the bugs live — churn × complexity
<MetricChip slug="hotspot" onOpen={onOpenWiki}>
Where the bugs live — churn × complexity
</MetricChip>
</h2>
{metrics.top_churn_complex.length > 0 && (
<span className="text-[12px] uppercase tracking-widest text-foreground/35 tabular-nums">
@@ -351,20 +360,32 @@ export function DashboardTab({ project, onOpenSymbol, onOpenModulesPath }: Dashb
trend={history.map((entry) => 1 - entry.usage_share)}
/>
<QualityCard
label="Dead candidates"
label={
<MetricChip slug="dead-candidate" onOpen={onOpenWiki}>
Dead candidates
</MetricChip>
}
value={totals.dead.toLocaleString("en-US")}
sub="no callers; entry/test/exported excluded"
tone={totals.dead > 0 ? "warn" : undefined}
trend={history.map((entry) => entry.dead)}
/>
<QualityCard
label="Tested symbols"
label={
<MetricChip slug="tested" onOpen={onOpenWiki}>
Tested symbols
</MetricChip>
}
value={`${(testedShare * 100).toFixed(0)}%`}
sub={`${totals.tested_symbols.toLocaleString("en-US")} with TESTS edges`}
trend={history.map((entry) => entry.tested)}
/>
<QualityCard
label="Documented exports"
label={
<MetricChip slug="documented" onOpen={onOpenWiki}>
Documented exports
</MetricChip>
}
value={`${(docShare * 100).toFixed(0)}%`}
sub={`${totals.documented_exported.toLocaleString("en-US")} of ${totals.exported.toLocaleString("en-US")} exported`}
tone={docShare < 0.3 ? "warn" : undefined}
@@ -404,7 +425,11 @@ export function DashboardTab({ project, onOpenSymbol, onOpenModulesPath }: Dashb
{/* ── Drill-down lists ──────────────────────────────────── */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<TopList
title="Most complex"
title={
<MetricChip slug="complexity" onOpen={onOpenWiki}>
Most complex
</MetricChip>
}
entries={metrics.top_complex}
unit="cyclo"
total={totals.callables}
+27
View File
@@ -0,0 +1,27 @@
/* The wiki affordance: any metric name in the UI becomes a dotted-underline
* chip that opens the metric wiki panel — the label itself is the door to
* the definition. Inherits the surrounding typography so wrapping an
* existing label changes nothing but the underline. */
import { type ReactNode } from "react";
import { wikiEntry } from "../wiki/entries";
export function MetricChip({
slug,
onOpen,
children,
}: {
slug: string;
onOpen: (slug: string) => void;
children?: ReactNode;
}) {
return (
<button
type="button"
onClick={() => onOpen(slug)}
className="[font:inherit] [letter-spacing:inherit] text-inherit align-baseline text-left border-b border-dotted border-foreground/40 hover:border-primary hover:text-primary transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary rounded-none"
title="What does this mean? Open the metric wiki"
>
{children ?? wikiEntry(slug)?.term ?? slug}
</button>
);
}
+24 -6
View File
@@ -13,6 +13,7 @@ import {
type SymbolRef,
} from "../lib/atlas";
import { AddToPromptButton } from "./PromptBasket";
import { MetricChip } from "./MetricChip";
import { TriggerTree } from "./TriggerTree";
import type { RepoInfo } from "../lib/types";
@@ -21,6 +22,7 @@ interface SymbolTabProps {
symbolRef: { id?: number; qn?: string };
onOpenSymbol: (ref: { id?: number; qn?: string }) => void;
onOpenRegion: (regionId: number) => void;
onOpenWiki: (slug: string) => void;
}
const CONFIDENCE: Record<string, { label: string; tone: string }> = {
@@ -37,12 +39,14 @@ function ConnectionList({
homeRegion,
onLoadMore,
onOpenSymbol,
onOpenWiki,
}: {
title: string;
page: ConnectionPage;
homeRegion?: number;
onLoadMore: () => void;
onOpenSymbol: (ref: { id?: number; qn?: string }) => void;
onOpenWiki: (slug: string) => void;
}) {
const shown = page.offset + page.items.length;
/* A utility called from everywhere would carry a dot on every row — the
@@ -68,9 +72,11 @@ function ConnectionList({
</span>
)}
<span className="text-[12px] text-foreground/40 ml-auto">
{Object.entries(page.by_type)
.map(([type, count]) => `${CONFIDENCE[type]?.label ?? type.toLowerCase()} ${count}`)
.join(" · ")}
<MetricChip slug="confidence" onOpen={onOpenWiki}>
{Object.entries(page.by_type)
.map(([type, count]) => `${CONFIDENCE[type]?.label ?? type.toLowerCase()} ${count}`)
.join(" · ")}
</MetricChip>
</span>
</div>
<div className="space-y-px">
@@ -133,7 +139,13 @@ function ConnectionList({
);
}
export function SymbolTab({ project, symbolRef, onOpenSymbol, onOpenRegion }: SymbolTabProps) {
export function SymbolTab({
project,
symbolRef,
onOpenSymbol,
onOpenRegion,
onOpenWiki,
}: SymbolTabProps) {
const [bundle, setBundle] = useState<SymbolBundle | null>(null);
const [error, setError] = useState<string | null>(null);
const [limit, setLimit] = useState(50);
@@ -331,6 +343,7 @@ export function SymbolTab({ project, symbolRef, onOpenSymbol, onOpenRegion }: Sy
homeRegion={bundle.region?.id}
onLoadMore={() => setLimit((value) => value + 100)}
onOpenSymbol={onOpenSymbol}
onOpenWiki={onOpenWiki}
/>
<ConnectionList
title="Calls"
@@ -338,6 +351,7 @@ export function SymbolTab({ project, symbolRef, onOpenSymbol, onOpenRegion }: Sy
homeRegion={bundle.region?.id}
onLoadMore={() => setLimit((value) => value + 100)}
onOpenSymbol={onOpenSymbol}
onOpenWiki={onOpenWiki}
/>
</div>
@@ -430,7 +444,9 @@ export function SymbolTab({ project, symbolRef, onOpenSymbol, onOpenRegion }: Sy
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="bg-card border border-border/40 rounded-md p-4">
<p className="text-[12px] uppercase tracking-widest text-foreground/45 mb-2">
Tested by
<MetricChip slug="tested" onOpen={onOpenWiki}>
Tested by
</MetricChip>
</p>
{(bundle.tests ?? []).map((test: SymbolRef) => (
<button
@@ -448,7 +464,9 @@ export function SymbolTab({ project, symbolRef, onOpenSymbol, onOpenRegion }: Sy
<div className="bg-card border border-border/40 rounded-md p-4">
<p className="text-[12px] uppercase tracking-widest text-foreground/45 mb-2">
Changes together with
<MetricChip slug="co-change" onOpen={onOpenWiki}>
Changes together with
</MetricChip>
</p>
{(bundle.co_change ?? []).map((partner) => (
<p key={partner.file_path} className="text-[13px] font-mono text-foreground/55 py-[3px] truncate">
@@ -0,0 +1,58 @@
/* @vitest-environment jsdom */
import "@testing-library/jest-dom/vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { WikiPanel } from "./WikiPanel";
describe("WikiPanel", () => {
it("renders the entry with its sections, caps as warnings included", () => {
render(<WikiPanel slug="hotspot" onClose={() => {}} onNavigate={() => {}} />);
expect(screen.getByRole("heading", { name: "hotspot" })).toBeInTheDocument();
expect(screen.getByText("first-class")).toBeInTheDocument();
expect(screen.getByText(/where fixes cluster/)).toBeInTheDocument();
expect(screen.getByText("Why this matters")).toBeInTheDocument();
expect(screen.getByText("How it's computed")).toBeInTheDocument();
expect(screen.getByText(/files under 100 lines are excluded/)).toBeInTheDocument();
expect(screen.getByText("What this does not cover")).toBeInTheDocument();
expect(screen.getByText("Where it appears")).toBeInTheDocument();
expect(screen.getByText("M-HOTSPOT")).toBeInTheDocument();
});
it("hides absent sections — a refused entry has no notCovered", () => {
render(
<WikiPanel slug="halstead" onClose={() => {}} onNavigate={() => {}} />,
);
expect(screen.getByText("refused")).toBeInTheDocument();
expect(screen.queryByText("What this does not cover")).not.toBeInTheDocument();
expect(screen.queryByText("How it's computed")).not.toBeInTheDocument();
});
it("navigates via the pairedWith chip and closes on Escape", () => {
const onNavigate = vi.fn();
const onClose = vi.fn();
render(<WikiPanel slug="hotspot" onClose={onClose} onNavigate={onNavigate} />);
/* hotspot is paired with tested — the chip pushes that entry. */
fireEvent.click(screen.getByRole("button", { name: "tested" }));
expect(onNavigate).toHaveBeenCalledWith("tested");
fireEvent.keyDown(window, { key: "Escape" });
expect(onClose).toHaveBeenCalled();
});
it("pops the navigation stack with the back arrow", () => {
const onNavigate = vi.fn();
const { rerender } = render(
<WikiPanel slug="hotspot" onClose={() => {}} onNavigate={onNavigate} />,
);
/* The host owns the slug; a chip click comes back as a prop change. */
rerender(<WikiPanel slug="churn" onClose={() => {}} onNavigate={onNavigate} />);
expect(screen.getByRole("heading", { name: "churn" })).toBeInTheDocument();
fireEvent.click(screen.getByTitle("Back"));
expect(onNavigate).toHaveBeenCalledWith("hotspot");
});
});
+202
View File
@@ -0,0 +1,202 @@
/* The metric wiki panel — a docked reader for one WikiEntry: the sentence,
* why it earned its place, how it's computed (with named caps as warnings),
* what it does not cover, and where it appears. Term chips inside the panel
* push onto an internal navigation stack (back arrow pops, breadcrumb
* jumps), so a reader can chase definitions without losing their place.
* "Refused" entries render muted — they document what Atlas won't show. */
import { useEffect, useState } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useUiMessages } from "../lib/i18n";
import { wikiEntry, type WikiTier } from "../wiki/entries";
import { MetricChip } from "./MetricChip";
interface WikiPanelProps {
slug: string;
onClose: () => void;
onNavigate: (slug: string) => void;
}
const TIER_TONE: Record<WikiTier, string> = {
"first-class": "bg-emerald-400/10 text-emerald-300/80",
caveated: "bg-amber-400/10 text-amber-300/80",
refused: "bg-surface-3 text-foreground/40 border border-border/40",
};
function SectionHeading({ children }: { children: string }) {
return (
<p className="text-[12px] uppercase tracking-widest text-foreground/40 mb-1.5">
{children}
</p>
);
}
export function WikiPanel({ slug, onClose, onNavigate }: WikiPanelProps) {
const t = useUiMessages();
/* The navigation stack. The host owns the current slug; every push goes
* through onNavigate and lands here via the prop effect, so chips outside
* and inside the panel behave identically. */
const [stack, setStack] = useState<string[]>([slug]);
useEffect(() => {
setStack((prev) => (prev[prev.length - 1] === slug ? prev : [...prev, slug]));
}, [slug]);
useEffect(() => {
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
const back = () => {
if (stack.length < 2) return;
const next = stack.slice(0, -1);
setStack(next);
onNavigate(next[next.length - 1]);
};
const jumpTo = (index: number) => {
const next = stack.slice(0, index + 1);
setStack(next);
onNavigate(next[next.length - 1]);
};
const entry = wikiEntry(slug);
const refused = entry?.tier === "refused";
return (
<div className="fixed right-0 top-12 bottom-0 z-40 w-full sm:w-[400px] bg-card/95 border-l border-border/60 backdrop-blur-xl flex flex-col shadow-2xl">
{/* Header: back + breadcrumb + close */}
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border/40 shrink-0">
{stack.length > 1 && (
<button
onClick={back}
className="text-foreground/45 hover:text-foreground/70 text-[14px] leading-none transition-colors shrink-0"
title={t.wiki.back}
>
←
</button>
)}
<div className="flex items-baseline gap-1 min-w-0 flex-1 overflow-hidden">
{stack.length > 1 &&
stack.slice(0, -1).map((crumb, index) => (
<span key={`${crumb}-${index}`} className="flex items-baseline gap-1 shrink-0">
<button
onClick={() => jumpTo(index)}
className="text-[12px] text-foreground/40 hover:text-foreground/65 transition-colors"
>
{wikiEntry(crumb)?.term ?? crumb}
</button>
<span className="text-[12px] text-foreground/25">›</span>
</span>
))}
<span className="text-[12px] font-semibold text-foreground/80 truncate">
{entry?.term ?? slug}
</span>
</div>
<button
onClick={onClose}
className="text-foreground/40 hover:text-foreground/60 text-[15px] leading-none transition-colors shrink-0"
title={t.wiki.close}
>
×
</button>
</div>
{!entry ? (
<p className="text-[13px] text-foreground/40 px-4 py-6">
No wiki entry for “{slug}”.
</p>
) : (
<ScrollArea className="flex-1 min-h-0">
<div className="px-4 py-4">
{/* Term + tier */}
<div className="flex items-center gap-2.5 flex-wrap mb-2">
<h3
className={`text-[17px] font-semibold ${
refused ? "text-foreground/50" : "text-foreground/90"
}`}
>
{entry.term}
</h3>
<span
className={`px-2 py-0.5 rounded-md text-[12px] font-medium ${TIER_TONE[entry.tier]}`}
>
{entry.tier}
</span>
</div>
{/* The one-sentence definition — always present. */}
<p
className={`text-[13.5px] leading-relaxed max-w-[60ch] ${
refused ? "text-foreground/50" : "text-foreground/75"
}`}
>
{entry.sentence}
</p>
{entry.why && (
<div className="mt-4">
<SectionHeading>{t.wiki.whyMatters}</SectionHeading>
<p className="text-[13px] text-foreground/60 leading-relaxed max-w-[60ch]">
{entry.why}
</p>
</div>
)}
{(entry.computedParts || entry.caps) && (
<div className="mt-4">
<SectionHeading>{t.wiki.howComputed}</SectionHeading>
{(entry.computedParts ?? []).map((part) => (
<p key={part} className="text-[13px] text-foreground/60 py-[2px]">
· {part}
</p>
))}
{(entry.caps ?? []).map((cap) => (
<p key={cap} className="text-[13px] text-amber-300/70 py-[2px]">
⚠ {cap}
</p>
))}
</div>
)}
{entry.notCovered && (
<div className="mt-4">
<SectionHeading>{t.wiki.notCovered}</SectionHeading>
<p className="text-[13px] text-foreground/60 leading-relaxed max-w-[60ch]">
{entry.notCovered}
</p>
</div>
)}
{entry.appearsIn.length > 0 && (
<div className="mt-4">
<SectionHeading>{t.wiki.whereAppears}</SectionHeading>
{entry.appearsIn.map((place) => (
<p key={place} className="text-[13px] text-foreground/60 py-[2px]">
{place}
</p>
))}
</div>
)}
{/* Footer: id + paired metric + see-also chips */}
<div className="border-t border-border/40 pt-3 mt-5 flex items-baseline gap-x-3 gap-y-1.5 flex-wrap">
<span className="text-[12px] font-mono text-foreground/35">{entry.id}</span>
{entry.pairedWith && (
<span className="text-[13px] text-foreground/45">
{t.wiki.pairedWith}{" "}
<MetricChip slug={entry.pairedWith} onOpen={onNavigate} />
</span>
)}
{(entry.seeAlso ?? []).map((other) => (
<span key={other} className="text-[13px] text-foreground/45">
<MetricChip slug={other} onOpen={onNavigate} />
</span>
))}
</div>
</div>
</ScrollArea>
)}
</div>
);
}
+18
View File
@@ -70,6 +70,15 @@ export const messages = {
title: "Architecture Decision Record",
lastUpdated: "Last updated",
},
wiki: {
whyMatters: "Why this matters",
howComputed: "How it's computed",
notCovered: "What this does not cover",
whereAppears: "Where it appears",
pairedWith: "Paired with",
back: "Back",
close: "Close",
},
control: {
panel: "Control Panel",
totalCpu: "Total CPU",
@@ -151,6 +160,15 @@ export const messages = {
title: "架构决策记录",
lastUpdated: "最后更新",
},
wiki: {
whyMatters: "为什么重要",
howComputed: "如何计算",
notCovered: "不涵盖的内容",
whereAppears: "出现位置",
pairedWith: "配对指标",
back: "返回",
close: "关闭",
},
control: {
panel: "控制面板",
totalCpu: "总 CPU",
+52
View File
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { WIKI_ENTRIES, wikiEntry, wikiEntryById } from "./entries";
describe("wiki entries", () => {
it("every sentence is present and tooltip-sized (≤30 words)", () => {
for (const entry of WIKI_ENTRIES) {
expect(entry.sentence.trim().length, entry.slug).toBeGreaterThan(0);
expect(
entry.sentence.trim().split(/\s+/).length,
`${entry.slug} sentence exceeds 30 words`,
).toBeLessThanOrEqual(30);
}
});
it("ids and slugs are unique", () => {
const ids = WIKI_ENTRIES.map((entry) => entry.id);
const slugs = WIKI_ENTRIES.map((entry) => entry.slug);
expect(new Set(ids).size).toBe(ids.length);
expect(new Set(slugs).size).toBe(slugs.length);
});
it("every non-refused entry names what it does not cover", () => {
for (const entry of WIKI_ENTRIES) {
if (entry.tier === "refused") continue;
expect(entry.notCovered?.trim(), entry.slug).toBeTruthy();
}
});
it("every pairedWith and seeAlso slug resolves to an existing entry", () => {
for (const entry of WIKI_ENTRIES) {
if (entry.pairedWith !== undefined) {
expect(wikiEntry(entry.pairedWith), `${entry.slug} → ${entry.pairedWith}`).toBeDefined();
}
for (const other of entry.seeAlso ?? []) {
expect(wikiEntry(other), `${entry.slug} → ${other}`).toBeDefined();
}
}
});
it("ids follow the M-… register format", () => {
for (const entry of WIKI_ENTRIES) {
expect(entry.id, entry.slug).toMatch(/^M-[A-Z-]+$/);
}
});
it("lookup helpers resolve by slug and by id", () => {
expect(wikiEntry("hotspot")?.id).toBe("M-HOTSPOT");
expect(wikiEntryById("M-HOTSPOT")?.slug).toBe("hotspot");
expect(wikiEntry("no-such-metric")).toBeUndefined();
expect(wikiEntryById("M-NO-SUCH")).toBeUndefined();
});
});
+344
View File
@@ -0,0 +1,344 @@
/* The metric wiki — every number Atlas shows, explained in one place: what
* it means (one tooltip-sized sentence), why it earns its spot, how it is
* computed, the named caps, and what it deliberately does not cover. The
* "refused" tier documents metrics Atlas will NOT show, and why. This file
* is the single source of truth; panels and chips render from it. */
export type WikiTier = "first-class" | "caveated" | "refused";
export interface WikiEntry {
id: string;
slug: string;
term: string;
tier: WikiTier;
/* ≤30 words — this IS the tooltip text. */
sentence: string;
why?: string;
computedParts?: string[];
/* Named caps, each "capped: … because …" style — rendered as warnings. */
caps?: string[];
notCovered?: string;
appearsIn: string[];
pairedWith?: string;
seeAlso?: string[];
}
export const WIKI_ENTRIES: WikiEntry[] = [
{
id: "M-CHURN",
slug: "churn",
term: "churn",
tier: "first-class",
sentence:
"How often this file changed in the last year — the strongest single predictor of where work concentrates.",
why: "Where code changed last year is where it will change next — and every change is a chance to break something. Process history predicts defects better than any measure of the code itself.",
caps: ["vendored and generated paths are excluded"],
notCovered:
"Raw commit counts, not size-adjusted; relative churn (per size) is the variant with peer-reviewed defect-prediction backing and is planned as the default axis.",
appearsIn: ["Dashboard", "Symbol file history", "Handout"],
pairedWith: "complexity",
},
{
id: "M-COCHANGE",
slug: "co-change",
term: "co-change",
tier: "first-class",
sentence:
"Files that tend to change in the same commits as this one — coupling as history actually happened, not as imports suggest.",
why: "If two files always change together, they are one module the directory tree forgot. Editing one without its partner is a classic source of half-done changes.",
notCovered:
"Pairs below the minimum co-commit support are not shown; the observation window is stated on the panel.",
appearsIn: ["Symbol"],
pairedWith: "coupling",
},
{
id: "M-FANIN",
slug: "fan",
term: "fan",
tier: "first-class",
sentence:
"How many symbols call this one (fan-in), and how many it calls (fan-out) — the raw material of blast radius.",
why: "High fan-in means many places notice when this breaks; high fan-out means this breaks when many places change. Both ends deserve tests and careful review.",
notCovered:
"Edges carry resolution confidence; unresolved dynamic calls are not counted.",
appearsIn: ["Symbol", "Galaxy"],
seeAlso: ["confidence", "blast-radius"],
},
{
id: "M-HOTSPOT",
slug: "hotspot",
term: "hotspot",
tier: "first-class",
sentence:
"Files that change often and are hard to follow — where fixes cluster and edits cost the most.",
why: "A small set of hotspots typically carries a large share of defect-fixing work. Relative churn predicts defect density; raw line counts don't. Effort spent here beats effort spread evenly.",
computedParts: [
"churn percentile of this repo",
"complexity percentile of this repo",
],
caps: [
"capped: files under 100 lines are excluded — too small to rank",
"capped: test files are excluded",
],
notCovered:
"Bug density is not measured directly — there is no issue-tracker join yet. Generated or vendored files can inflate churn. A stable-but-tangled file with no recent commits won't appear here; see complexity alone for those.",
appearsIn: ["Dashboard hero", "Changes risk", "Handout"],
pairedWith: "tested",
seeAlso: ["churn", "complexity"],
},
{
id: "M-CPLX",
slug: "complexity",
term: "complexity",
tier: "caveated",
sentence:
"The number of independent paths through a function — a decent proxy for hard-to-test, a weak one for hard-to-read.",
why: "Every independent path is a case a test must cover and a reader must simulate. High-percentile functions in this repository are where reviews slow down and tests multiply.",
notCovered:
"Cyclomatic complexity correlates heavily with size; the percentile shown is of this repository, never an absolute good/bad threshold.",
appearsIn: ["Dashboard", "Symbol"],
pairedWith: "churn",
seeAlso: ["cognitive-complexity"],
},
{
id: "M-COG",
slug: "cognitive-complexity",
term: "cognitive complexity",
tier: "caveated",
sentence:
"How much nesting and control-flow interruption a reader must hold in their head to follow this function.",
why: "Deep nesting forces a reader to keep every enclosing condition in working memory. This measure was designed to match how hard code feels to read, and tracks that better than path counting.",
notCovered:
"Better matched to perceived readability than cyclomatic, but still size-confounded.",
appearsIn: ["Dashboard", "Symbol"],
seeAlso: ["complexity"],
},
{
id: "M-COH",
slug: "cohesion",
term: "cohesion",
tier: "caveated",
sentence:
"How much this region's files actually work together, measured by internal connections.",
why: "A region whose files barely talk to each other is a folder, not a module — changes ripple outward instead of staying local. Read it beside coupling: shredding a module improves one at the cost of the other.",
notCovered:
"Structural cohesion measures failed empirical validation as quality predictors; a history-based co-change cohesion is the planned replacement. Always read beside coupling.",
appearsIn: ["Overview regions", "Modules"],
pairedWith: "coupling",
},
{
id: "M-COUP",
slug: "coupling",
term: "coupling",
tier: "first-class",
sentence:
"How strongly two regions depend on each other, counted in cross-region calls.",
why: "The more two regions call each other, the harder they are to change, test, or understand apart. Heavy coupling between supposedly separate modules is the map's most actionable warning.",
notCovered: "Only statically resolved calls are counted; confidence applies.",
appearsIn: ["Overview", "Modules"],
pairedWith: "cohesion",
},
{
id: "M-BRIDGE",
slug: "boundary-spanner",
term: "boundary spanner",
tier: "first-class",
sentence:
"A symbol that calls into several foreign regions — the joints of the architecture, and the first places a refactor hurts.",
why: "These symbols hold the architecture together — and any boundary refactor lands on them first. They are where an interface change fans out into foreign regions.",
caps: [
"capped: outgoing calls only",
"capped: at least 2 foreign regions required",
"capped: test files are excluded",
],
notCovered:
"Incoming cross-region popularity is a different signal (see fan-in).",
appearsIn: ["Overview"],
seeAlso: ["fan", "coupling"],
},
{
id: "M-CONF",
slug: "confidence",
term: "confidence",
tier: "first-class",
sentence:
"How likely this call edge is real, given how the resolver found it.",
why: "Low-confidence hops can turn a trace into a plausible story that isn't true. The word and the number always appear together: certain ≥95, likely 75–94, uncertain 50–74, speculative <50.",
caps: [
"capped: cross-language boundary → at most likely",
"reduced: target overload set larger than 1",
"speculative: dynamic dispatch unresolved",
],
notCovered:
"Reflection, code generation, and runtime dispatch are not modelled. Coverage is best-effort.",
appearsIn: ["Flows", "Trace", "Symbol connections"],
},
{
id: "M-DEAD",
slug: "dead-candidate",
term: "dead candidate",
tier: "caveated",
sentence:
"Symbols with no recorded callers — candidates for removal, with caveats you must check first.",
why: "The blocker to deleting dead code is fear of unintended breakage, not detection. This list is evidence, not a verdict — verify with a repo-wide search and tests before deleting.",
notCovered:
"Dynamic dispatch, reflection, exported API consumed outside this repository, and config-referenced entry points all look dead here and are not.",
appearsIn: ["Dashboard"],
seeAlso: ["fan", "confidence"],
},
{
id: "M-TESTED",
slug: "tested",
term: "tested",
tier: "caveated",
sentence:
"A TESTS edge reaches this symbol — some test exercises it, directly or through calls.",
why: "A symbol no test reaches fails only in production. Reach is the floor of safety; hotspots without reach are the riskiest code in the repository.",
notCovered:
"Reached is not asserted: a test can execute this code without checking its result. Atlas deliberately shows reach, not a coverage percentage.",
appearsIn: ["Dashboard", "Symbol"],
pairedWith: "hotspot",
seeAlso: ["coverage-gates"],
},
{
id: "M-DOC",
slug: "documented",
term: "documented",
tier: "caveated",
sentence:
"Exported symbols that carry a doc comment — a floor for explainability, not a measure of doc quality.",
why: "An undocumented export forces every caller to read the implementation. A doc comment is a cheap floor that pays off at every call site — and in every agent prompt.",
notCovered:
"Comment presence only; accuracy and usefulness are not assessed.",
appearsIn: ["Dashboard"],
},
{
id: "M-DIVERG",
slug: "divergence",
term: "divergence",
tier: "caveated",
sentence:
"Copies of this code that have started to drift apart — the risk is the drift, not the duplication.",
why: "When copies drift, a bug fixed in one lives on in the other. Divergent near-clones are the copies that have already started disagreeing.",
notCovered:
"Duplication alone is a weak signal; identical, stable copies are mostly harmless. Divergence under change is the hazard this view ranks.",
appearsIn: ["Symbol near-clones"],
},
{
id: "M-HELP",
slug: "who-can-help",
term: "who can help",
tier: "caveated",
sentence:
"People with recent, substantial history in this code — evidence for who to ask, never a performance measure.",
why: "The most-sought information in twenty years of developer studies is what coworkers know. When the graph runs out of answers, the right person is the fastest path — this shows who, with the evidence.",
notCovered:
"Atlas never ranks people against each other and never shows per-person productivity. Names appear only with their evidence: commits, recency, breadth.",
appearsIn: ["Symbol file history"],
},
{
id: "M-REGION",
slug: "region",
term: "region",
tier: "first-class",
sentence:
"A cluster of files that call each other more than they call anything else — Atlas's inferred modules.",
why: "Modules inferred from actual call structure show the architecture as it is, not as the directory tree claims. Where the two disagree is usually worth a look.",
notCovered:
"Inferred from call structure, not from directory names; a region panel must answer why is this here — members, internal density, top external ties.",
appearsIn: ["Galaxy", "Overview", "Modules"],
seeAlso: ["cohesion", "coupling"],
},
{
id: "M-HUB",
slug: "hub",
term: "hub",
tier: "first-class",
sentence:
"The most-connected symbols in the graph — the ones most other code depends on, directly or transitively.",
why: "A change to a hub is felt everywhere; an outage in one is an outage in many. Hubs deserve the strongest tests and the most careful review in the repository.",
caps: [
"capped: top 12 by connectivity are haloed; the cutoff is a display choice, not a cliff",
],
notCovered: "Connectivity is static; a rarely-executed hub still ranks.",
appearsIn: ["Galaxy halos", "Overview"],
seeAlso: ["fan"],
},
{
id: "M-BLAST",
slug: "blast-radius",
term: "blast radius",
tier: "first-class",
sentence:
"Everything that can reach the code you changed — what could notice, grouped by region.",
why: "Most regressions ship because a change was tested where it was made, not where it is felt. The blast list is what to retest before pushing.",
notCovered:
"Computed on the uncommitted diff today; hypothetical per-symbol impact and tests-to-run are planned. Confidence caveats apply along every path.",
appearsIn: ["Changes"],
seeAlso: ["fan", "confidence"],
},
{
id: "M-SCENT",
slug: "scent",
term: "scent",
tier: "first-class",
sentence:
"Where your search term concentrates across the map — follow the smell, not the guess.",
why: "Search results drawn on the map show where a concept lives and how concentrated it is — one bright region means a clean home; scattered hits mean a crosscutting concern.",
notCovered:
"Name and text matches only; File and Folder nodes are excluded from counts.",
appearsIn: ["Galaxy minimap", "Search"],
},
{
id: "M-NEG-MI",
slug: "maintainability-index",
term: "maintainability index",
tier: "refused",
sentence:
"Atlas does not show a Maintainability Index: its weights are folklore, it double-counts size, and it failed empirical validation.",
why: "The MI's constants were fit to a handful of 1990s Hewlett-Packard systems and never revalidated; size dominates every term. What Atlas shows instead: churn, complexity and size separately, each with its own percentile.",
appearsIn: ["Wiki only"],
},
{
id: "M-NEG-HAL",
slug: "halstead",
term: "Halstead",
tier: "refused",
sentence:
"Atlas does not compute Halstead metrics: token-counting science from 1977 that never replicated; size does its job better.",
why: "Operator-and-operand counting promised effort and bug prediction it never delivered under replication; every useful signal it carries, plain size carries better.",
appearsIn: ["Wiki only"],
},
{
id: "M-NEG-COV",
slug: "coverage-gates",
term: "coverage gates",
tier: "refused",
sentence:
"Atlas shows which tests reach a symbol, never a coverage-percentage target: rigid targets produce assertion-free tests.",
why: "The correlation between coverage and post-release defects is weak and disappears above roughly 70–80%. Atlas shows test reach per symbol so you can judge the gap that matters.",
appearsIn: ["Wiki only"],
seeAlso: ["tested"],
},
{
id: "M-NEG-GRADE",
slug: "quality-grades",
term: "quality grades",
tier: "refused",
sentence:
"No A–F, no 0–100: a percentile in this repository carries the same information with no invented scale to misread.",
why: "Published score curves still get read as linear; grades measurably undermine interest and performance relative to explanations; single scores with consequences invite gaming. Atlas ships percentiles, distributions and named caps instead.",
appearsIn: ["Wiki only"],
},
];
const bySlug = new Map(WIKI_ENTRIES.map((entry) => [entry.slug, entry]));
const byId = new Map(WIKI_ENTRIES.map((entry) => [entry.id, entry]));
export function wikiEntry(slug: string): WikiEntry | undefined {
return bySlug.get(slug);
}
export function wikiEntryById(id: string): WikiEntry | undefined {
return byId.get(id);
}
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/rpc.ts","./src/components/changestab.tsx","./src/components/commandk.tsx","./src/components/controltab.tsx","./src/components/dashboardtab.tsx","./src/components/displaysettingsmenu.tsx","./src/components/edgelines.tsx","./src/components/errorboundary.tsx","./src/components/filterpanel.tsx","./src/components/flowstab.tsx","./src/components/graphloader.tsx","./src/components/graphscene.test.ts","./src/components/graphscene.tsx","./src/components/graphtab.deadcode.test.tsx","./src/components/graphtab.filters.test.tsx","./src/components/graphtab.regions.test.tsx","./src/components/graphtab.test.ts","./src/components/graphtab.tsx","./src/components/missedcallout.tsx","./src/components/modulestab.tsx","./src/components/nodecloud.tsx","./src/components/nodedetailpanel.showmore.test.tsx","./src/components/nodedetailpanel.test.tsx","./src/components/nodedetailpanel.tsx","./src/components/nodelabels.tsx","./src/components/nodetooltip.tsx","./src/components/overviewtab.tsx","./src/components/projectcard.tsx","./src/components/promptbasket.tsx","./src/components/regionpanel.tsx","./src/components/resizehandle.tsx","./src/components/sidebar.tsx","./src/components/statstab.test.tsx","./src/components/statstab.tsx","./src/components/symboltab.tsx","./src/components/tabbar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/hooks/usegraphdata.test.ts","./src/hooks/usegraphdata.ts","./src/hooks/useprojects.ts","./src/lib/atlas.test.ts","./src/lib/atlas.ts","./src/lib/colors.ts","./src/lib/composer.ts","./src/lib/density.test.ts","./src/lib/density.ts","./src/lib/findings.test.ts","./src/lib/findings.ts","./src/lib/firstread.ts","./src/lib/i18n.test.ts","./src/lib/i18n.ts","./src/lib/regions.test.ts","./src/lib/regions.ts","./src/lib/treemap.ts","./src/lib/types.ts","./src/lib/utils.ts"],"version":"5.9.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/rpc.ts","./src/components/changestab.tsx","./src/components/commandk.tsx","./src/components/controltab.tsx","./src/components/dashboardtab.tsx","./src/components/displaysettingsmenu.tsx","./src/components/edgelines.tsx","./src/components/errorboundary.tsx","./src/components/filterpanel.tsx","./src/components/flowstab.tsx","./src/components/graphloader.tsx","./src/components/graphscene.test.ts","./src/components/graphscene.tsx","./src/components/graphtab.deadcode.test.tsx","./src/components/graphtab.filters.test.tsx","./src/components/graphtab.regions.test.tsx","./src/components/graphtab.test.ts","./src/components/graphtab.tsx","./src/components/halolayer.tsx","./src/components/metricchip.tsx","./src/components/minimap.tsx","./src/components/missedcallout.tsx","./src/components/modulestab.tsx","./src/components/nodecloud.tsx","./src/components/nodedetailpanel.showmore.test.tsx","./src/components/nodedetailpanel.test.tsx","./src/components/nodedetailpanel.tsx","./src/components/nodelabels.tsx","./src/components/nodetooltip.tsx","./src/components/overviewtab.tsx","./src/components/projectcard.tsx","./src/components/projectswitcher.tsx","./src/components/promptbasket.tsx","./src/components/regionpanel.tsx","./src/components/regiontooltip.tsx","./src/components/resizehandle.tsx","./src/components/sidebar.tsx","./src/components/statstab.test.tsx","./src/components/statstab.tsx","./src/components/symboltab.tsx","./src/components/tabbar.tsx","./src/components/triggertree.tsx","./src/components/wikipanel.test.tsx","./src/components/wikipanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/hooks/usegraphdata.test.ts","./src/hooks/usegraphdata.ts","./src/hooks/useprojects.ts","./src/lib/atlas.test.ts","./src/lib/atlas.ts","./src/lib/colors.ts","./src/lib/composer.ts","./src/lib/density.test.ts","./src/lib/density.ts","./src/lib/findings.test.ts","./src/lib/findings.ts","./src/lib/firstread.ts","./src/lib/flowgroup.test.ts","./src/lib/flowgroup.ts","./src/lib/i18n.test.ts","./src/lib/i18n.ts","./src/lib/regions.test.ts","./src/lib/regions.ts","./src/lib/treemap.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/lib/why.test.ts","./src/lib/why.ts","./src/lib/whyfetch.ts","./src/wiki/entries.test.ts","./src/wiki/entries.ts"],"version":"5.9.3"}