mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-09 09:11:00 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 @@
|
||||
{"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"}
|
||||
Reference in New Issue
Block a user