mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-10 09:13:53 +08:00
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:
@@ -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">
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 到达它们——已排除入口点与测试");
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 @@
|
||||
{"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"}
|
||||
Reference in New Issue
Block a user