feat(ui): computed sentences compose natively per locale

The takeaway/cost sentences, the trigger tree's guard grammar, and the
first-read content each build their own full sentence in the viewer's
language from shared parameters — never word substitution into an
English frame (zh restructures where zh reads better: 只要 X 运行,它就
一定运行——调用点上没有任何条件). The load-bearing discipline holds
bilingually: the complexity takeaway's simple/exceed claims are parsed
back out of BOTH languages and asserted against the same CPLX_BINS
metadata, so a bin edit moves every locale or fails loudly. The
server's region-why templates get parsed and recomposed client-side
with byte-identical English reproduction and verbatim pass-through for
unknown shapes. English output is byte-identical everywhere, guarded
by the untouched en expectations.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-08-28 09:17:02 +02:00
parent 32e910aab5
commit bda4608651
17 changed files with 431 additions and 76 deletions
+7 -5
View File
@@ -7,6 +7,7 @@
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 { useUiMessages } from "../lib/i18n";
import {
CPLX_BINS,
CPLX_TAIL_MIN,
@@ -239,6 +240,7 @@ export function DashboardTab({
onOpenModulesPath,
onOpenWiki,
}: DashboardTabProps) {
const t = useUiMessages();
const [metrics, setMetrics] = useState<MetricsPayload | null>(null);
const [error, setError] = useState<string | null>(null);
const [showMuted, setShowMuted] = useState(false);
@@ -285,16 +287,14 @@ export function DashboardTab({
};
}, [metrics, showMuted, mutedTick]);
/* TODO(content-i18n): the computed takeaway and cost sentences interpolate
* many numbers and stay English this pass; route them through the i18n
* messages mechanism in a follow-up. */
const takeaway = useMemo(() => {
if (!metrics) return null;
return complexityTakeaway(
metrics.complexity_hist,
metrics.top_complex.slice(0, 3).map((entry) => entry.name ?? ""),
t.dashboard,
);
}, [metrics]);
}, [metrics, t]);
if (error) {
return (
@@ -322,6 +322,7 @@ export function DashboardTab({
hero.headCommits,
totals.files,
metrics.churn_total_commits,
t.dashboard,
);
return (
@@ -353,7 +354,8 @@ export function DashboardTab({
</div>
{cost ? (
<p className="text-[13px] text-foreground/55 mt-1">
{cost} — change here is where defects concentrate.
{cost}
{t.dashboard.costConcentrate}
</p>
) : (
<p className="text-[13px] text-foreground/45 mt-1">
+2 -2
View File
@@ -330,7 +330,7 @@ export function FlowsTab({
}`}
title={t.flows.guardTitleHop}
>
{formatGuard(guard)}
{formatGuard(guard, t.why)}
</span>
))}
{step.observed && (
@@ -511,7 +511,7 @@ export function FlowsTab({
}`}
title={t.flows.guardTitleStep}
>
{formatGuard(guard)}
{formatGuard(guard, t.why)}
</span>
))}
{step.observed && <ObservedChip observed={step.observed} />}
+10 -4
View File
@@ -17,6 +17,7 @@ import {
disambiguateRegionNames,
lensRegionsPayload,
isBuiltinQn,
localizeRegionWhy,
} from "../lib/regions";
import { surprisingCouplings, suggestedQuestions } from "../lib/firstread";
import { kneeCount, findingKey, isDismissed, dismiss } from "../lib/findings";
@@ -371,11 +372,16 @@ export function OverviewTab({
const hiddenTestRegions =
regions && lensed ? regions.regions.length - lensed.regions.length : 0;
const couplings = useMemo(() => (lensed ? surprisingCouplings(lensed, 5) : []), [lensed]);
const couplings = useMemo(
() => (lensed ? surprisingCouplings(lensed, 5, t.firstread) : []),
[lensed, t],
);
const questions = useMemo(
() =>
lensed ? suggestedQuestions(lensed, { deadCount: metrics?.totals.dead }, 4) : [],
[lensed, metrics],
lensed
? suggestedQuestions(lensed, { deadCount: metrics?.totals.dead }, 4, t.firstread)
: [],
[lensed, metrics, t],
);
/* Findings with dismissal (magnitude-bucketed keys re-alert on jumps). */
@@ -635,7 +641,7 @@ export function OverviewTab({
</div>
{region.why && (
<p className="text-[12px] text-foreground/35 mt-1 line-clamp-2">
{region.why}
{localizeRegionWhy(region.why, t.regions)}
</p>
)}
</button>
+4 -1
View File
@@ -1,4 +1,6 @@
import { ScrollArea } from "@/components/ui/scroll-area";
import { useUiMessages } from "../lib/i18n";
import { localizeRegionWhy } from "../lib/regions";
import type { Region } from "../lib/types";
interface RegionPanelProps {
@@ -10,6 +12,7 @@ interface RegionPanelProps {
/* Detail panel for a selected region body: what it is, why it got its name,
* its representative symbols, and the door into its full-detail layout. */
export function RegionPanel({ region, onOpen, onClose }: RegionPanelProps) {
const t = useUiMessages();
return (
<div className="w-full bg-card/95 backdrop-blur-xl flex flex-col h-full min-h-0 overflow-hidden">
<div className="px-4 pt-4 pb-3 border-b border-border/30">
@@ -38,7 +41,7 @@ export function RegionPanel({ region, onOpen, onClose }: RegionPanelProps) {
{region.why && (
<p className="text-[13px] text-foreground/40 mt-2 leading-relaxed">
{region.why}
{localizeRegionWhy(region.why, t.regions)}
</p>
)}
+18 -17
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useUiMessages } from "../lib/i18n";
import {
fetchWhy,
formatGuard,
@@ -12,27 +13,29 @@ import {
* Thin by default; elision declared; guards honest about being syntactic. */
function DispatchChip({ entry }: { entry: WhyEntry }) {
const t = useUiMessages();
if (!entry.candidates || entry.candidates <= 1) return null;
return (
<span
className="text-[11px] px-1.5 rounded-full border border-border/60 text-foreground/55"
title="dynamic dispatch: the resolver saw several possible targets for this call site — this is one of them"
title={t.why.dispatchTitle}
>
◇ 1 of {entry.candidates} targets
{t.why.dispatchChip(entry.candidates)}
</span>
);
}
function GuardChips({ entry }: { entry: WhyEntry }) {
const t = useUiMessages();
if (entry.guards_unavailable)
return <span className="text-[12px] text-foreground/35">guards unavailable (source not readable)</span>;
return <span className="text-[12px] text-foreground/35">{t.why.guardsUnavailable}</span>;
if (entry.guards.length === 0 && !entry.loop)
return <span className="text-[12px] text-foreground/35">unguarded — always on this path</span>;
return <span className="text-[12px] text-foreground/35">{t.why.unguarded}</span>;
return (
<span className="inline-flex flex-wrap gap-1 items-center">
{entry.loop && (
<span className="text-[11px] px-1.5 rounded-full border border-border/60 text-foreground/50" title="the call site sits inside a loop">
⟳ loop
<span className="text-[11px] px-1.5 rounded-full border border-border/60 text-foreground/50" title={t.why.loopChipTitle}>
{t.why.loopChip}
</span>
)}
{entry.guards.map((guard, index) => (
@@ -43,9 +46,9 @@ function GuardChips({ entry }: { entry: WhyEntry }) {
? "border-warn/50 text-warn/90 bg-warn/10"
: "border-primary/40 text-primary/90 bg-primary/10"
}`}
title={`syntactic ${guard.kind} guard around the call site — not a proven path condition`}
title={t.why.guardChipTitle(guard.kind)}
>
? {formatGuard(guard)}
? {formatGuard(guard, t.why)}
</span>
))}
</span>
@@ -242,6 +245,7 @@ export function TriggerTree({
symbolId: number;
onOpenSymbol: (ref: { id?: number; qn?: string }) => void;
}) {
const t = useUiMessages();
const [direction, setDirection] = useState<"up" | "down">("up");
const [root, setRoot] = useState<WhyPayload | null>(null);
const [asTable, setAsTable] = useState(false);
@@ -264,7 +268,7 @@ export function TriggerTree({
<div className="bg-card border border-border/50 rounded-md p-4">
<div className="flex items-baseline gap-2 mb-2">
<p className="text-[12px] uppercase tracking-widest text-foreground/40">
{direction === "up" ? "When does this run?" : "What does this trigger?"}
{direction === "up" ? t.why.whenRuns : t.why.whatTriggers}
</p>
<div className="ml-auto flex items-center gap-3">
{guardedCount >= 3 && (
@@ -280,16 +284,16 @@ export function TriggerTree({
onClick={() => setDirection((d) => (d === "up" ? "down" : "up"))}
className="text-[12px] text-foreground/45 hover:text-primary transition-colors"
>
{direction === "up" ? "→ what it triggers" : "← when it runs"}
{direction === "up" ? t.why.toWhatTriggers : t.why.toWhenRuns}
</button>
</div>
</div>
{root && allUnguarded(root.entries) && root.entries.length > 0 && root.entries.length <= 2 ? (
<p className="text-[13px] text-foreground/60">
{direction === "up" ? "Always runs when " : "Always triggers "}
{direction === "up" ? t.why.alwaysRunsPrefix : t.why.alwaysTriggersPrefix}
{root.entries.map((entry, index) => (
<span key={entry.id}>
{index > 0 && " and "}
{index > 0 && t.why.alwaysJoiner}
<button
onClick={() => onOpenSymbol({ id: entry.id })}
className="font-mono text-foreground/80 hover:text-primary transition-colors"
@@ -298,7 +302,7 @@ export function TriggerTree({
</button>
</span>
))}
{direction === "up" ? " runs — no conditions at the call sites." : " — unconditionally."}
{direction === "up" ? t.why.alwaysRunsSuffix : t.why.alwaysTriggersSuffix}
</p>
) : asTable && root ? (
<GuardTable
@@ -314,10 +318,7 @@ export function TriggerTree({
onOpenSymbol={onOpenSymbol}
/>
)}
<p className="text-[11px] text-foreground/30 mt-2">
Guards are the syntactic conditions around each call site — not proven path
conditions. Dispatch, events and reflection are invisible here.
</p>
<p className="text-[11px] text-foreground/30 mt-2">{t.why.honestyFooter}</p>
</div>
);
}
+35 -12
View File
@@ -6,6 +6,7 @@ import {
CPLX_TAIL_START,
complexityTakeaway,
} from "./complexity";
import { messages } from "./i18n";
/* Parse a bin label back into its bounds so the text the user reads under
* the bars cannot drift from the numeric metadata the sentence derives
@@ -37,25 +38,47 @@ describe("complexity bins", () => {
});
describe("complexityTakeaway", () => {
/* Expectations recounted from the label-parsed bounds, independent of the
* metadata. Both languages' sentences derive from the same recount, so a
* bin edit fails both or neither. */
const hist = [4, 6, 3, 2, 1, 1];
const total = hist.reduce((a, b) => a + b, 0);
const simple = hist
.filter((_, index) => parseLabel(CPLX_BINS[index].label).hi <= CPLX_SIMPLE_MAX)
.reduce((a, b) => a + b, 0);
const tail = hist
.filter((_, index) => parseLabel(CPLX_BINS[index].label).lo > CPLX_TAIL_MIN)
.reduce((a, b) => a + b, 0);
const simpleMax = parseLabel(CPLX_BINS[1].label).hi;
const tailMin = parseLabel(CPLX_BINS[CPLX_TAIL_START].label).lo - 1;
const pct = ((simple / total) * 100).toFixed(0);
it("claims exactly what the labels claim", () => {
const hist = [4, 6, 3, 2, 1, 1];
const sentence = complexityTakeaway(hist, [])!;
/* Recount from the label-parsed bounds, independent of the metadata. */
const simple = hist
.filter((_, index) => parseLabel(CPLX_BINS[index].label).hi <= CPLX_SIMPLE_MAX)
.reduce((a, b) => a + b, 0);
const tail = hist
.filter((_, index) => parseLabel(CPLX_BINS[index].label).lo > CPLX_TAIL_MIN)
.reduce((a, b) => a + b, 0);
const total = hist.reduce((a, b) => a + b, 0);
expect(sentence).toBe(
`${((simple / total) * 100).toFixed(0)}% of functions are simple (≤${CPLX_SIMPLE_MAX}); ${tail} exceed ${CPLX_TAIL_MIN}.`,
expect(complexityTakeaway(hist, [])).toBe(
`${pct}% of functions are simple (≤${simpleMax}); ${tail} exceed ${tailMin}.`,
);
});
it("composes the zh sentence from the same label-parsed bounds", () => {
expect(complexityTakeaway(hist, [], messages.zh.dashboard)).toBe(
`${pct}% 的函数是简单的(≤${simpleMax});${tail} 个超过 ${tailMin}。`,
);
});
it("parses each language's threshold claims back to the bin metadata", () => {
for (const m of [messages.en.dashboard, messages.zh.dashboard]) {
const sentence = complexityTakeaway(hist, [], m)!;
expect(Number(/≤(\d+)/.exec(sentence)![1])).toBe(CPLX_SIMPLE_MAX);
const over = /exceed (\d+)|超过 (\d+)/.exec(sentence)!;
expect(Number(over[1] ?? over[2])).toBe(CPLX_TAIL_MIN);
}
});
it("appends the leading names when given", () => {
expect(complexityTakeaway([10, 0, 0, 0, 0, 2], ["a", "b"])).toContain("— led by a, b");
expect(
complexityTakeaway([10, 0, 0, 0, 0, 2], ["a", "b"], messages.zh.dashboard),
).toContain("——以 a、b 为首");
});
it("returns null on an empty population", () => {
expect(complexityTakeaway([0, 0, 0, 0, 0, 0], [])).toBeNull();
expect(complexityTakeaway([0, 0, 0, 0, 0, 0], [], messages.zh.dashboard)).toBeNull();
});
});
+19 -4
View File
@@ -1,7 +1,9 @@
/* Complexity bins — the single source of truth for the dashboard's
* histogram labels and the takeaway sentence. The sentence's bin claims
* ("simple (≤5)", "exceed 20") derive from this table, and a test binds
* label text to the numeric bounds, so a label edit cannot silently lie. */
* label text to the numeric bounds — in every catalog language — so a
* label edit cannot silently lie. */
import { messages, type UiMessages } from "./i18n";
export interface CplxBin {
label: string;
@@ -24,7 +26,15 @@ export const CPLX_SIMPLE_MAX = CPLX_BINS[1].hi;
export const CPLX_TAIL_START = CPLX_BINS.length - 2;
export const CPLX_TAIL_MIN = CPLX_BINS[CPLX_TAIL_START - 1].hi;
export function complexityTakeaway(hist: number[], names: string[]): string | null {
/* The takeaway sentence, composed by the active locale's catalog message.
* Every locale receives the same bin-derived parameters — no locale may
* hardcode a threshold. Pass the active locale's dashboard messages to
* localize. */
export function complexityTakeaway(
hist: number[],
names: string[],
m: UiMessages["dashboard"] = messages.en.dashboard,
): string | null {
const total = hist.reduce((a, b) => a + b, 0);
if (total === 0) return null;
const simple = hist
@@ -33,6 +43,11 @@ export function complexityTakeaway(hist: number[], names: string[]): string | nu
const tail = hist
.filter((_, index) => index >= CPLX_TAIL_START)
.reduce((a, b) => a + b, 0);
const led = names.filter(Boolean).join(", ");
return `${((simple / total) * 100).toFixed(0)}% of functions are simple (≤${CPLX_SIMPLE_MAX}); ${tail.toLocaleString("en-US")} exceed ${CPLX_TAIL_MIN}${led ? ` — led by ${led}` : ""}.`;
return m.takeaway(
((simple / total) * 100).toFixed(0),
CPLX_SIMPLE_MAX,
tail,
CPLX_TAIL_MIN,
names.filter(Boolean),
);
}
+7
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { kneeCount, costSentence, findingKey } from "./findings";
import { messages } from "./i18n";
import { isTestRegion, lensRegionsPayload, isBuiltinQn } from "./regions";
import type { RegionsPayload } from "./types";
@@ -41,9 +42,15 @@ describe("costSentence", () => {
"9 files — 1.1% of the codebase, 34% of all commits this year",
);
});
it("composes the zh sentence from the same numbers", () => {
expect(costSentence(9, 340, 800, 1000, messages.zh.dashboard)).toBe(
"9 个文件——占代码库的 1.1%,占今年全部提交的 34%",
);
});
it("returns null without denominators", () => {
expect(costSentence(9, 340, 0, 1000)).toBeNull();
expect(costSentence(0, 0, 800, 1000)).toBeNull();
expect(costSentence(9, 340, 0, 1000, messages.zh.dashboard)).toBeNull();
});
});
+6 -2
View File
@@ -1,5 +1,6 @@
/* Findings machinery: knee-point cutoffs, the cost sentence, and finding
* dismissals — the difference between a dashboard and a number museum. */
import { messages, type UiMessages } from "./i18n";
/* Knee of a descending score list — how many head items deserve emphasis:
* "the head that matters" (CodeScene's refusal to show 5,000 items).
@@ -37,17 +38,20 @@ export function kneeCount(values: number[]): number {
return Math.min(Math.max(knee, 3), Math.min(15, Math.ceil(n / 2)));
}
/* "9 files — 1.1% of the codebase, 34% of all commits this year." */
/* "9 files — 1.1% of the codebase, 34% of all commits this year." The
* shares are computed here so every locale interpolates identical numbers.
* Pass the active locale's dashboard messages to localize. */
export function costSentence(
headFiles: number,
headCommits: number,
totalFiles: number,
totalCommits: number,
m: UiMessages["dashboard"] = messages.en.dashboard,
): string | null {
if (headFiles <= 0 || totalFiles <= 0 || totalCommits <= 0) return null;
const filesShare = ((headFiles / totalFiles) * 100).toFixed(1);
const commitShare = ((headCommits / totalCommits) * 100).toFixed(0);
return `${headFiles} file${headFiles > 1 ? "s" : ""} — ${filesShare}% of the codebase, ${commitShare}% of all commits this year`;
return m.costSentence(headFiles, filesShare, commitShare);
}
/* Dismissals: a finding stays dismissed until its magnitude changes bucket
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { surprisingCouplings, suggestedQuestions } from "./firstread";
import { messages } from "./i18n";
import type { RegionsPayload } from "./types";
/* One coupling that trips all three reasons: heavy, cross-top-level, and
* touching a low-cohesion region big enough to also earn the split
* question. */
const payload: RegionsPayload = {
level: "regions",
method: "leiden+folders",
total_nodes: 100,
unmapped_nodes: 0,
regions: [
{ id: 0, name: "src/core", files: 4, members: 60, cohesion: 0.8, top_nodes: [], x: 0, y: 0, z: 0, size: 1, color: "#111111" },
{ id: 1, name: "web/api", files: 3, members: 50, cohesion: 0.2, top_nodes: [], x: 0, y: 0, z: 0, size: 1, color: "#222222" },
],
edges: [{ source: 0, target: 1, weight: 1200 }],
};
describe("surprisingCouplings", () => {
it("explains the ranking in English by default", () => {
const [coupling] = surprisingCouplings(payload);
expect(coupling.reasons).toEqual([
"1,200 edges cross the boundary",
"links src/ to web/ — different top-level areas",
"web/api holds together loosely (cohesion 0.20)",
]);
});
it("composes the reasons natively in zh from the same parameters", () => {
const [coupling] = surprisingCouplings(payload, 5, messages.zh.firstread);
expect(coupling.reasons).toEqual([
"1,200 条边跨越这条边界",
"连接 src/ 与 web/——分属不同的顶层目录",
"web/api 内聚松散(cohesion 0.20)",
]);
});
});
describe("suggestedQuestions", () => {
it("asks in English by default, evidence joined with a semicolon", () => {
const questions = suggestedQuestions(payload, { deadCount: 1234 });
expect(questions[0].question).toBe("Why does src/core depend on web/api?");
expect(questions[0].why).toContain("; ");
expect(questions.map((q) => q.question)).toContain(
"Are the 1,234 functions with no callers safe to delete?",
);
});
it("asks the zh questions with zh evidence, seeds untouched", () => {
const questions = suggestedQuestions(
payload,
{ deadCount: 1234 },
5,
messages.zh.firstread,
);
expect(questions[0].question).toBe("为什么 src/core 依赖 web/api?");
expect(questions[0].why).toContain(";");
expect(questions.map((q) => q.question)).toContain("web/api 应该拆分成更小的模块吗?");
const dead = questions.find((q) => q.about.includes("dead code"))!;
expect(dead.question).toBe("这 1,234 个没有调用者的函数可以安全删除吗?");
expect(dead.why).toBe("没有任何 CALLS 或 USAGE 到达它们——已排除入口点与测试");
});
});
+20 -18
View File
@@ -2,6 +2,7 @@
* graph already holds — every insight carries the reason it is interesting.
* No model, no scores without provenance. */
import type { Region, RegionsPayload } from "./types";
import { messages, type UiMessages } from "./i18n";
import { disambiguateRegionNames } from "./regions";
export interface SurprisingCoupling {
@@ -13,10 +14,12 @@ export interface SurprisingCoupling {
/* Cross-region couplings ranked by an explained score: heavier links, links
* that cross top-level directories, and links touching a low-cohesion region
* rank higher. `misc` never surprises anyone. */
* rank higher. `misc` never surprises anyone. Pass the active locale's
* firstread messages to localize the reasons. */
export function surprisingCouplings(
payload: RegionsPayload,
limit = 5,
m: UiMessages["firstread"] = messages.en.firstread,
): SurprisingCoupling[] {
const regions = disambiguateRegionNames(payload.regions);
const byId = new Map(regions.map((region) => [region.id, region]));
@@ -29,19 +32,15 @@ export function surprisingCouplings(
if (source.name === "misc" || target.name === "misc") continue;
const reasons: string[] = [];
let score = Math.log2(1 + edge.weight);
reasons.push(`${edge.weight.toLocaleString("en-US")} edges cross the boundary`);
reasons.push(m.edgesCross(edge.weight));
if (top(source.name) !== top(target.name)) {
score += 2;
reasons.push(
`links ${top(source.name)}/ to ${top(target.name)}/ — different top-level areas`,
);
reasons.push(m.linksAreas(top(source.name), top(target.name)));
}
if (source.cohesion < 0.3 || target.cohesion < 0.3) {
score += 1;
const loose = source.cohesion < target.cohesion ? source : target;
reasons.push(
`${loose.name} holds together loosely (cohesion ${loose.cohesion.toFixed(2)})`,
);
reasons.push(m.holdsLoosely(loose.name, loose.cohesion));
}
scored.push({ source, target, weight: edge.weight, reasons, score });
}
@@ -64,22 +63,25 @@ export interface FirstReadStats {
}
/* Question templates keyed to structural signals — each one is a prompt
* starter the human can hand to their agent. */
* starter the human can hand to their agent. Pass the active locale's
* firstread messages to localize question and evidence; the `about` seeds
* stay English (they feed the composer, not the page). */
export function suggestedQuestions(
payload: RegionsPayload,
stats: FirstReadStats = {},
limit = 5,
m: UiMessages["firstread"] = messages.en.firstread,
): SuggestedQuestion[] {
const regions = disambiguateRegionNames(payload.regions).filter(
(region) => region.name !== "misc",
);
const questions: SuggestedQuestion[] = [];
const couplings = surprisingCouplings(payload, 2);
const couplings = surprisingCouplings(payload, 2, m);
for (const coupling of couplings) {
questions.push({
question: `Why does ${coupling.source.name} depend on ${coupling.target.name}?`,
why: coupling.reasons.join("; "),
question: m.questionWhyDepend(coupling.source.name, coupling.target.name),
why: coupling.reasons.join(m.reasonSeparator),
about: [coupling.source.name, coupling.target.name],
});
}
@@ -89,24 +91,24 @@ export function suggestedQuestions(
.sort((a, b) => a.cohesion - b.cohesion)[0];
if (loose) {
questions.push({
question: `Should ${loose.name} be split into smaller modules?`,
why: `only ${(loose.cohesion * 100).toFixed(0)}% of its edges stay inside the region (${loose.members.toLocaleString("en-US")} symbols)`,
question: m.questionSplit(loose.name),
why: m.whySplit((loose.cohesion * 100).toFixed(0), loose.members),
about: [loose.name],
});
}
if (stats.deadCount && stats.deadCount > 0) {
questions.push({
question: `Are the ${stats.deadCount.toLocaleString("en-US")} functions with no callers safe to delete?`,
why: "zero CALLS and zero USAGE reach them, excluding entry points and tests",
question: m.questionDeadSafe(stats.deadCount),
why: m.whyDeadSafe,
about: ["dead code"],
});
}
if (stats.unresolvedShare !== undefined && stats.unresolvedShare > 0.3) {
questions.push({
question: "Which call sites does the graph fail to resolve, and why?",
why: `${(stats.unresolvedShare * 100).toFixed(0)}% of call-ish edges are USAGE (no proven single target)`,
question: m.questionUnresolved,
why: m.whyUnresolved((stats.unresolvedShare * 100).toFixed(0)),
about: ["resolution certainty"],
});
}
+151
View File
@@ -277,6 +277,84 @@ export const messages = {
inventory: (symbols: string, edges: string, regions: string) =>
`${symbols} symbols · ${edges} edges · ${regions} regions`,
},
dashboard: {
/* Both thresholds arrive from CPLX_BINS via complexityTakeaway — the
* binding test in complexity.test.ts parses each language's claims
* back against the bin labels, so a bin edit fails both or neither. */
takeaway: (
simplePct: string,
simpleMax: number,
tail: number,
tailMin: number,
led: string[],
) =>
`${simplePct}% of functions are simple (≤${simpleMax}); ${tail.toLocaleString("en-US")} exceed ${tailMin}${led.length > 0 ? ` — led by ${led.join(", ")}` : ""}.`,
costSentence: (files: number, filesShare: string, commitShare: string) =>
`${files} file${files > 1 ? "s" : ""} — ${filesShare}% of the codebase, ${commitShare}% of all commits this year`,
costConcentrate: " — change here is where defects concentrate.",
},
why: {
whenRuns: "When does this run?",
whatTriggers: "What does this trigger?",
toWhatTriggers: "→ what it triggers",
toWhenRuns: "← when it runs",
dispatchChip: (n: number) => `◇ 1 of ${n} targets`,
dispatchTitle:
"dynamic dispatch: the resolver saw several possible targets for this call site — this is one of them",
guardsUnavailable: "guards unavailable (source not readable)",
unguarded: "unguarded — always on this path",
loopChip: "⟳ loop",
loopChipTitle: "the call site sits inside a loop",
guardChipTitle: (kind: string) =>
`syntactic ${kind} guard around the call site — not a proven path condition`,
alwaysRunsPrefix: "Always runs when ",
alwaysRunsSuffix: " runs — no conditions at the call sites.",
alwaysTriggersPrefix: "Always triggers ",
alwaysTriggersSuffix: " — unconditionally.",
alwaysJoiner: " and ",
honestyFooter:
"Guards are the syntactic conditions around each call site — not proven path conditions. Dispatch, events and reflection are invisible here.",
/* Guard fragments — the condition expression stays verbatim code. */
guardWhen: (cond: string) => `when ${cond}`,
guardUnless: (cond: string) => `unless ${cond}`,
guardConditionally: "conditionally",
guardElseArm: "in an else arm",
guardCase: (cond: string) => `case ${cond}`,
guardSwitchCase: "in a switch case",
guardSwitchOn: (cond: string) => `switch on ${cond}`,
guardSwitch: "in a switch",
guardLoopWhile: (cond: string) => `looping while ${cond}`,
guardLoop: "in a loop",
guardCatch: "on error handling",
chainJoiner: " → ",
},
firstread: {
edgesCross: (weight: number) =>
`${weight.toLocaleString("en-US")} edges cross the boundary`,
linksAreas: (a: string, b: string) =>
`links ${a}/ to ${b}/ — different top-level areas`,
holdsLoosely: (name: string, cohesion: number) =>
`${name} holds together loosely (cohesion ${cohesion.toFixed(2)})`,
reasonSeparator: "; ",
questionWhyDepend: (a: string, b: string) => `Why does ${a} depend on ${b}?`,
questionSplit: (name: string) => `Should ${name} be split into smaller modules?`,
whySplit: (pct: string, members: number) =>
`only ${pct}% of its edges stay inside the region (${members.toLocaleString("en-US")} symbols)`,
questionDeadSafe: (n: number) =>
`Are the ${n.toLocaleString("en-US")} functions with no callers safe to delete?`,
whyDeadSafe: "zero CALLS and zero USAGE reach them, excluding entry points and tests",
questionUnresolved: "Which call sites does the graph fail to resolve, and why?",
whyUnresolved: (pct: string) =>
`${pct}% of call-ish edges are USAGE (no proven single target)`,
},
regions: {
/* The en messages reproduce layout_regions.c's templates byte-for-byte
* — localizeRegionWhy round-trips English through them unchanged. */
whyCallCommunity: (files: number, pct: number, dir: string) =>
`call community: ${files} files, ${pct}% under ${dir}`,
whyFolderGroup: "folder group (not explained by a kept call community)",
whyMisc: "files outside every kept community and folder group",
},
control: {
panel: "Control Panel",
totalCpu: "Total CPU",
@@ -559,6 +637,79 @@ export const messages = {
inventory: (symbols: string, edges: string, regions: string) =>
`${symbols} 个符号 · ${edges} 条边 · ${regions} 个区域`,
},
dashboard: {
takeaway: (
simplePct: string,
simpleMax: number,
tail: number,
tailMin: number,
led: string[],
) =>
`${simplePct}% 的函数是简单的(≤${simpleMax});${tail.toLocaleString("en-US")} 个超过 ${tailMin}${led.length > 0 ? `——以 ${led.join("、")} 为首` : ""}。`,
costSentence: (files: number, filesShare: string, commitShare: string) =>
`${files} 个文件——占代码库的 ${filesShare}%,占今年全部提交的 ${commitShare}%`,
costConcentrate: "——缺陷就集中在这里的变更中。",
},
why: {
whenRuns: "它何时运行?",
whatTriggers: "它会触发什么?",
toWhatTriggers: "→ 它触发什么",
toWhenRuns: "← 它何时运行",
dispatchChip: (n: number) => `◇ ${n} 个候选目标之一`,
dispatchTitle:
"动态分发:解析器在这个调用点看到多个可能的目标——这是其中之一",
guardsUnavailable: "守卫不可用(源码不可读)",
unguarded: "无守卫——这条路径上必然执行",
loopChip: "⟳ 循环",
loopChipTitle: "调用点位于循环内部",
guardChipTitle: (kind: string) =>
`调用点周围的语法 ${kind} 守卫——不是经过证明的路径条件`,
alwaysRunsPrefix: "只要 ",
alwaysRunsSuffix: " 运行,它就一定运行——调用点上没有任何条件。",
alwaysTriggersPrefix: "总是触发 ",
alwaysTriggersSuffix: "——无条件。",
alwaysJoiner: " 与 ",
honestyFooter:
"守卫只是各调用点周围的语法条件——不是经过证明的路径条件。分发、事件与反射在这里不可见。",
/* 守卫片段——条件表达式保留代码原文。 */
guardWhen: (cond: string) => `当 ${cond} 时`,
guardUnless: (cond: string) => `除非 ${cond}`,
guardConditionally: "视条件而定",
guardElseArm: "在 else 分支中",
guardCase: (cond: string) => `匹配 ${cond} 分支`,
guardSwitchCase: "在 switch 分支中",
guardSwitchOn: (cond: string) => `按 ${cond} 分支`,
guardSwitch: "在 switch 中",
guardLoopWhile: (cond: string) => `当 ${cond} 时循环`,
guardLoop: "在循环中",
guardCatch: "在错误处理中",
chainJoiner: " → ",
},
firstread: {
edgesCross: (weight: number) =>
`${weight.toLocaleString("en-US")} 条边跨越这条边界`,
linksAreas: (a: string, b: string) =>
`连接 ${a}/ 与 ${b}/——分属不同的顶层目录`,
holdsLoosely: (name: string, cohesion: number) =>
`${name} 内聚松散(cohesion ${cohesion.toFixed(2)})`,
reasonSeparator: ";",
questionWhyDepend: (a: string, b: string) => `为什么 ${a} 依赖 ${b}?`,
questionSplit: (name: string) => `${name} 应该拆分成更小的模块吗?`,
whySplit: (pct: string, members: number) =>
`只有 ${pct}% 的边留在区域内部(${members.toLocaleString("en-US")} 个符号)`,
questionDeadSafe: (n: number) =>
`这 ${n.toLocaleString("en-US")} 个没有调用者的函数可以安全删除吗?`,
whyDeadSafe: "没有任何 CALLS 或 USAGE 到达它们——已排除入口点与测试",
questionUnresolved: "图谱未能解析哪些调用点?为什么?",
whyUnresolved: (pct: string) =>
`${pct}% 的调用类边是 USAGE(没有可证明的唯一目标)`,
},
regions: {
whyCallCommunity: (files: number, pct: number, dir: string) =>
`调用社区:${files} 个文件,${pct}% 位于 ${dir} 之下`,
whyFolderGroup: "目录分组(未被任何保留的调用社区解释)",
whyMisc: "游离于所有保留社区与目录分组之外的文件",
},
control: {
panel: "控制面板",
totalCpu: "总 CPU",
+27
View File
@@ -5,7 +5,9 @@ import {
regionsViewWorthwhile,
REGIONS_MIN_TOTAL_NODES,
cohesionWord,
localizeRegionWhy,
} from "./regions";
import { messages } from "./i18n";
import { minimapLayout, minimapProject } from "../components/Minimap";
import type { Region, RegionsPayload } from "./types";
@@ -69,6 +71,31 @@ describe("region adapters", () => {
});
});
describe("localizeRegionWhy", () => {
const templates = [
"call community: 23 files, 87% under src/parser",
"folder group (not explained by a kept call community)",
"files outside every kept community and folder group",
"some future server template",
];
it("passes the server's English through byte-identically", () => {
for (const why of templates) expect(localizeRegionWhy(why)).toBe(why);
});
it("recomposes the known templates in zh, unknown ones verbatim", () => {
const zh = messages.zh.regions;
expect(localizeRegionWhy(templates[0], zh)).toBe(
"调用社区:23 个文件,87% 位于 src/parser 之下",
);
expect(localizeRegionWhy(templates[1], zh)).toBe(
"目录分组(未被任何保留的调用社区解释)",
);
expect(localizeRegionWhy(templates[2], zh)).toBe(
"游离于所有保留社区与目录分组之外的文件",
);
expect(localizeRegionWhy(templates[3], zh)).toBe(templates[3]);
});
});
describe("cohesionWord", () => {
it("maps scores to the 0.7/0.4 word ladder", () => {
expect(cohesionWord(0.84)).toBe("tightly connected");
+20
View File
@@ -1,5 +1,6 @@
/* Region level of detail — adapters between the level=regions payload
* (layout_regions.c) and the node/edge shapes the 3D scene renders. */
import { messages, type UiMessages } from "./i18n";
import type { GraphData, GraphNode, Region, RegionsPayload } from "./types";
/* The scene renders GraphNodes; a region becomes one body. Region ids live in
@@ -80,6 +81,25 @@ export function lensRegionsPayload(
};
}
/* region.why arrives from layout_regions.c as one of three English
* templates; recompose it in the viewer's locale. The en messages
* reproduce the server strings byte-for-byte, and an unrecognized string
* (a future server template) passes through verbatim — honest English
* beats a wrong translation. */
const REGION_WHY_CALL_COMMUNITY = /^call community: (\d+) files, (\d+)% under (.+)$/;
export function localizeRegionWhy(
why: string,
m: UiMessages["regions"] = messages.en.regions,
): string {
const match = REGION_WHY_CALL_COMMUNITY.exec(why);
if (match) return m.whyCallCommunity(Number(match[1]), Number(match[2]), match[3]);
if (why === "folder group (not explained by a kept call community)")
return m.whyFolderGroup;
if (why === "files outside every kept community and folder group") return m.whyMisc;
return why;
}
/* Hub lists must never headline builtins — a `len` hub reads as a bug and
* costs trust in everything else on the page. */
export function isBuiltinQn(qn: string): boolean {
+25
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { messages } from "./i18n";
import { formatGuard, formatGuardChain, allUnguarded, type WhyEntry } from "./why";
describe("formatGuard", () => {
@@ -20,6 +21,30 @@ describe("formatGuard", () => {
]),
).toBe("when mode > 2 → when strict");
});
it("composes zh guards with native grammar, conditions verbatim", () => {
const zh = messages.zh.why;
expect(formatGuard({ kind: "if", cond: "mode > 2" }, zh)).toBe("当 mode > 2 时");
expect(formatGuard({ kind: "if", cond: "strict", negated: true }, zh)).toBe(
"除非 strict",
);
expect(formatGuard({ kind: "case", cond: "CBM_LANG_C" }, zh)).toBe(
"匹配 CBM_LANG_C 分支",
);
expect(formatGuard({ kind: "loop", cond: "i < n" }, zh)).toBe("当 i < n 时循环");
expect(formatGuard({ kind: "loop" }, zh)).toBe("在循环中");
expect(formatGuard({ kind: "catch" }, zh)).toBe("在错误处理中");
});
it("chains a zh guard sentence outermost-first", () => {
expect(
formatGuardChain(
[
{ kind: "if", cond: "mode > 2" },
{ kind: "if", cond: "strict", negated: true },
],
messages.zh.why,
),
).toBe("当 mode > 2 时 → 除非 strict");
});
});
describe("allUnguarded", () => {
+16 -10
View File
@@ -1,3 +1,4 @@
import { messages, type UiMessages } from "./i18n";
import { fetchJsonFrom } from "./whyfetch";
/* The Why view's data layer. Guards are SYNTACTIC: the conditions the code
@@ -44,30 +45,35 @@ export function fetchWhy(
}
/* One guard as human text: `when mode > 2` / `unless strict` /
* `case CBM_LANG_C` / `in a loop` / `in catch`. */
export function formatGuard(guard: Guard): string {
* `case CBM_LANG_C` / `in a loop` / `in catch`. Pass the active locale's
* why messages to localize the frame — the condition expression stays
* verbatim code in every language. */
export function formatGuard(guard: Guard, m: UiMessages["why"] = messages.en.why): string {
const cond = guard.cond ?? "";
switch (guard.kind) {
case "if":
case "ternary":
if (!cond) return guard.negated ? "in an else arm" : "conditionally";
return `${guard.negated ? "unless" : "when"} ${cond}`;
if (!cond) return guard.negated ? m.guardElseArm : m.guardConditionally;
return guard.negated ? m.guardUnless(cond) : m.guardWhen(cond);
case "case":
return cond ? `case ${cond}` : "in a switch case";
return cond ? m.guardCase(cond) : m.guardSwitchCase;
case "switch":
return cond ? `switch on ${cond}` : "in a switch";
return cond ? m.guardSwitchOn(cond) : m.guardSwitch;
case "loop":
return cond ? `looping while ${cond}` : "in a loop";
return cond ? m.guardLoopWhile(cond) : m.guardLoop;
case "catch":
return "on error handling";
return m.guardCatch;
default:
return cond || guard.kind;
}
}
/* A whole chain as one sentence fragment, outermost first. */
export function formatGuardChain(guards: Guard[]): string {
return guards.map(formatGuard).join(" → ");
export function formatGuardChain(
guards: Guard[],
m: UiMessages["why"] = messages.en.why,
): string {
return guards.map((guard) => formatGuard(guard, m)).join(m.chainJoiner);
}
/* Trivial-trigger check: the complexity gate. A symbol whose callers are all
+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.observed.test.tsx","./src/components/flowstab.tsx","./src/components/flowstab.zh.test.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/wikihovercard.test.tsx","./src/components/wikihovercard.tsx","./src/components/wikipanel.test.tsx","./src/components/wikipanel.tsx","./src/components/wikipanel.zh.test.tsx","./src/components/wikitier.ts","./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/complexity.test.ts","./src/lib/complexity.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/impact.test.ts","./src/lib/impact.ts","./src/lib/observed.test.ts","./src/lib/observed.ts","./src/lib/questions.test.ts","./src/lib/questions.ts","./src/lib/rationale.test.ts","./src/lib/rationale.ts","./src/lib/regions.test.ts","./src/lib/regions.ts","./src/lib/treemap.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/lib/who.test.ts","./src/lib/who.ts","./src/lib/why.test.ts","./src/lib/why.ts","./src/lib/whyfetch.ts","./src/test/setup.ts","./src/wiki/entries.test.ts","./src/wiki/entries.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.observed.test.tsx","./src/components/flowstab.tsx","./src/components/flowstab.zh.test.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/wikihovercard.test.tsx","./src/components/wikihovercard.tsx","./src/components/wikipanel.test.tsx","./src/components/wikipanel.tsx","./src/components/wikipanel.zh.test.tsx","./src/components/wikitier.ts","./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/complexity.test.ts","./src/lib/complexity.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.test.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/impact.test.ts","./src/lib/impact.ts","./src/lib/observed.test.ts","./src/lib/observed.ts","./src/lib/questions.test.ts","./src/lib/questions.ts","./src/lib/rationale.test.ts","./src/lib/rationale.ts","./src/lib/regions.test.ts","./src/lib/regions.ts","./src/lib/treemap.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/lib/who.test.ts","./src/lib/who.ts","./src/lib/why.test.ts","./src/lib/why.ts","./src/lib/whyfetch.ts","./src/test/setup.ts","./src/wiki/entries.test.ts","./src/wiki/entries.ts"],"version":"5.9.3"}