Skip to content

Commit 5cd0799

Browse files
committed
refactor(ui): focus explainability on refactor priority
1 parent 1b1d52c commit 5cd0799

20 files changed

Lines changed: 528 additions & 184 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ TokenMap is a desktop app for quickly finding the parts of a local codebase that
1313
## Metrics
1414

1515
- Basic: Tokens, non-empty lines, file size.
16-
- Derived: Structural Risk, Refactor Priority.
16+
- Derived: internal Structural Risk, product-facing Refactor Priority.
1717
- Syntax-aware metrics currently cover C#, TypeScript, JavaScript, Python, Go, Java, PHP, and Rust.
1818

1919
## How It Works

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ This document covers project purpose, canonical ownership, runtime flow, and non
3939
- `Clever.TokenMap.Metrics` stays below app/infrastructure orchestration and above core models; UI consumes metric abstractions through core contracts.
4040
- File metrics flow as raw file metrics, then chained derived file metrics, then directory rollup; raw parser objects stay inside the metrics layer.
4141
- Infrastructure may enrich a snapshot with one optional repo-wide git history pass before per-file metrics so product metrics can consume cached per-file git artifacts without per-file repository walks.
42-
- `Structural Risk` is the product-facing intrinsic quality score; it summarizes file scale, callable burden, and how concentrated that burden is inside the file.
43-
- `Refactor Priority` is the product-facing composite score for refactoring urgency; it layers recent change and co-change pressure on top of Structural Risk instead of exposing git signals as separate public metrics.
42+
- `Structural Risk` is the internal intrinsic quality score; it summarizes file scale, callable burden, and how concentrated that burden is inside the file, and it serves as the base for explainability.
43+
- `Refactor Priority` is the single product-facing quality score for refactoring urgency; it layers recent change and co-change pressure on top of Structural Risk instead of exposing a second public quality metric.
4444
- Token counting stays behind `ITokenCounter`.
4545
- The treemap stays one custom-rendered control.

src/Clever.TokenMap.App/Services/RefactorPromptComposer.cs

Lines changed: 90 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,7 @@ public string Compose(ProjectNode node)
4646
["{{tokens}}"] = FormatMetricValue(MetricIds.Tokens, metrics),
4747
["{{non_empty_lines}}"] = FormatMetricValue(MetricIds.NonEmptyLines, metrics),
4848
["{{file_size}}"] = FormatMetricValue(MetricIds.FileSizeBytes, metrics),
49-
["{{structural_risk}}"] = FormatMetricValue(MetricIds.ComplexityPoints, metrics),
5049
["{{refactor_priority}}"] = FormatMetricValue(MetricIds.RefactorPriorityPoints, metrics),
51-
["{{structural_risk_breakdown}}"] = BuildFormulaSection(
52-
title: "Structural Risk",
53-
ProductMetricFormulas.TryComputeStructuralRisk(metrics, out var structuralRiskBreakdown),
54-
structuralRiskBreakdown,
55-
unavailableReason: "Structural Risk is unavailable because the required syntax-derived inputs were not produced for this file."),
5650
["{{refactor_priority_breakdown}}"] = BuildRefactorPrioritySection(metrics),
5751
};
5852

@@ -71,65 +65,83 @@ private string ResolveTemplate() =>
7165
private static string FormatMetricValue(MetricId metricId, MetricSet metrics) =>
7266
MetricValueFormatter.Format(metricId, metrics.GetOrDefault(metricId), CultureInfo.CurrentCulture);
7367

74-
private static string BuildFormulaSection(
75-
string title,
76-
bool isAvailable,
77-
MetricFormulaBreakdown breakdown,
78-
string unavailableReason)
79-
{
80-
if (!isAvailable)
81-
{
82-
return "- " + unavailableReason;
83-
}
84-
85-
return BuildBreakdownContributors(title, breakdown);
86-
}
87-
8868
private static string BuildRefactorPrioritySection(MetricSet metrics)
8969
{
9070
if (!ProductMetricFormulas.TryComputeRefactorPriority(metrics, out var refactorBreakdown))
9171
{
9272
return "- Refactor Priority is unavailable because one or more prerequisite product metrics are unavailable.";
9373
}
9474

75+
var hasStructuralBreakdown = ProductMetricFormulas.TryComputeStructuralRisk(metrics, out var structuralBreakdown);
76+
var structuralBasePoints = metrics.TryGetNumber(MetricIds.ComplexityPoints) ?? structuralBreakdown.TotalPoints;
9577
var builder = new StringBuilder();
96-
builder.Append(BuildBreakdownContributors("Refactor Priority", refactorBreakdown));
97-
if (!HasGitContext(metrics))
78+
var hasGitContext = HasGitContext(metrics);
79+
var gitUpliftPoints = Math.Max(0d, refactorBreakdown.TotalPoints - structuralBasePoints);
80+
81+
builder.Append("- Refactor Priority is built from a structural base of ");
82+
builder.Append(FormatPoints(structuralBasePoints));
83+
builder.AppendLine(".");
84+
85+
if (hasStructuralBreakdown)
9886
{
99-
builder.AppendLine();
100-
builder.Append("- Refactor Priority currently matches Structural Risk because git-derived change and co-change inputs are unavailable.");
87+
builder.AppendLine("- Structural base drivers:");
88+
foreach (var contributor in structuralBreakdown.Components)
89+
{
90+
AppendContributorLine(
91+
builder,
92+
contributor.Label,
93+
contributor.RawValue,
94+
contributor.ContributionPoints,
95+
GetStructuralDescription(contributor.Key));
96+
}
10197
}
10298

103-
return builder.ToString();
104-
}
99+
if (!hasGitContext)
100+
{
101+
builder.Append("- Git uplift: unavailable because git-derived change and co-change inputs were not produced for this file.");
102+
return builder.ToString().TrimEnd();
103+
}
105104

106-
private static string BuildBreakdownContributors(
107-
string title,
108-
MetricFormulaBreakdown breakdown)
109-
{
110-
var builder = new StringBuilder();
111-
var contributors = breakdown.Components.Any(component => component.ContributionPoints > 0d)
112-
? breakdown.Components
113-
.Where(component => component.ContributionPoints > 0d)
114-
.OrderByDescending(component => component.ContributionPoints)
115-
.Take(4)
116-
: breakdown.Components.Take(4);
117-
118-
builder.Append("- ");
119-
builder.Append(title);
120-
builder.AppendLine(" is driven by:");
121-
foreach (var contributor in contributors)
105+
if (gitUpliftPoints <= 0d)
122106
{
123-
builder.Append(" - ");
124-
builder.Append(contributor.Label);
125-
builder.Append(": ");
126-
builder.Append(FormatRawValue(contributor.RawValue));
127-
builder.Append(" (");
128-
builder.Append(FormatContribution(contributor.ContributionPoints));
129-
builder.AppendLine(")");
107+
builder.Append("- Git uplift: +0 pts because all git signals stayed below the uplift thresholds.");
108+
return builder.ToString().TrimEnd();
130109
}
131110

132-
return builder.ToString().TrimEnd();
111+
builder.Append("- Git uplift: ");
112+
builder.Append(FormatContribution(gitUpliftPoints));
113+
builder.AppendLine(".");
114+
builder.AppendLine("- Git drivers:");
115+
foreach (var contributor in refactorBreakdown.Components
116+
.Where(component => !string.Equals(component.Category, "Structural", StringComparison.Ordinal))
117+
.Where(component => component.ContributionPoints > 0d))
118+
{
119+
AppendContributorLine(
120+
builder,
121+
contributor.Label,
122+
contributor.RawValue,
123+
contributor.ContributionPoints,
124+
GetGitDescription(contributor.Key));
125+
}
126+
127+
return builder.ToString();
128+
}
129+
130+
private static void AppendContributorLine(
131+
StringBuilder builder,
132+
string label,
133+
double rawValue,
134+
double contributionPoints,
135+
string description)
136+
{
137+
builder.Append(" - ");
138+
builder.Append(label);
139+
builder.Append(": ");
140+
builder.Append(FormatRawValue(rawValue));
141+
builder.Append(" (");
142+
builder.Append(FormatContribution(contributionPoints));
143+
builder.Append(") ");
144+
builder.AppendLine(description);
133145
}
134146

135147
private static string FormatRelativePath(string relativePath) =>
@@ -147,6 +159,34 @@ private static string FormatContribution(double contributionPoints) =>
147159
? $"+{contributionPoints.ToString("N0", CultureInfo.CurrentCulture)} pts"
148160
: $"+{contributionPoints.ToString("N1", CultureInfo.CurrentCulture)} pts";
149161

162+
private static string FormatPoints(double value) =>
163+
IsWholeNumber(value)
164+
? $"{value.ToString("N0", CultureInfo.CurrentCulture)} pts"
165+
: $"{value.ToString("N1", CultureInfo.CurrentCulture)} pts";
166+
167+
private static string GetStructuralDescription(string key) =>
168+
key switch
169+
{
170+
"code_lines" => "Code volume in the file. Larger files tend to accumulate more moving parts before method-level risk is considered.",
171+
"total_callable_burden_points" => "Sum of per-callable burden after soft thresholds for method length, cyclomatic complexity, nesting depth, and parameter count.",
172+
"top_callable_burden_points" => "Burden of the single heaviest callable. This catches one dominant method even when the rest of the file looks moderate.",
173+
"affected_callable_ratio" => "Share of callables that exceed the soft thresholds. Higher ratios mean the problem is spread across the file rather than isolated.",
174+
"top_three_callable_burden_share" => "Share of callable burden concentrated in the top three callables. High concentration means a small number of methods dominate the risk.",
175+
_ => "Structural input that feeds the intrinsic refactor-risk base score.",
176+
};
177+
178+
private static string GetGitDescription(string key) =>
179+
key switch
180+
{
181+
"churn_lines_90d" => "Recently rewritten line volume. Frequent rewrites raise urgency, but only as a bounded uplift on top of the structural base.",
182+
"touch_count_90d" => "Number of recent commits that touched this file. Repeated touches suggest ongoing friction around the code.",
183+
"author_count_90d" => "Number of recent contributors touching the file. More contributors usually increase coordination pressure around risky code.",
184+
"strong_cochanged_file_count_90d" => "Files that repeatedly change together with this one. This indicates a tighter blast radius when the file moves.",
185+
"unique_cochanged_file_count_90d" => "Breadth of different files that changed alongside this one across the recent history window.",
186+
"avg_cochange_set_size_90d" => "Typical width of change sets that include this file. Wider sets suggest changes tend to propagate.",
187+
_ => "Git-derived pressure that can amplify urgency without dominating the structural base.",
188+
};
189+
150190
private static bool HasGitContext(MetricSet metrics) =>
151191
metrics.TryGetNumber(MetricIds.ChurnLines90d).HasValue &&
152192
metrics.TryGetNumber(MetricIds.TouchCount90d).HasValue &&

src/Clever.TokenMap.App/Services/RefactorPromptTemplateCatalog.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ public static class RefactorPromptTemplateCatalog
1010
new("{{tokens}}", "Token count for the file."),
1111
new("{{non_empty_lines}}", "Non-empty line count."),
1212
new("{{file_size}}", "File size, formatted for display."),
13-
new("{{structural_risk}}", "Structural-risk score for the file."),
1413
new("{{refactor_priority}}", "Composite refactor-priority score."),
15-
new("{{structural_risk_breakdown}}", "Multi-line explanation for structural-risk drivers."),
16-
new("{{refactor_priority_breakdown}}", "Multi-line explanation for refactor-priority drivers."),
14+
new("{{refactor_priority_breakdown}}", "Multi-line explanation for the structural base and any git-derived uplift behind refactor priority."),
1715
];
1816
}
1917

src/Clever.TokenMap.App/State/SettingsState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public void ResetVisibleMetricIdsToDefault() =>
148148
ReplaceVisibleMetricIdsCore(DefaultMetricCatalog.GetDefaultVisibleMetricIds());
149149

150150
public void ShowAllMetricIds() =>
151-
ReplaceVisibleMetricIdsCore(DefaultMetricCatalog.GetAllMetricIds());
151+
ReplaceVisibleMetricIdsCore(DefaultMetricCatalog.GetAllUserVisibleMetricIds());
152152

153153
internal void ReplaceRecentFolderPaths(IEnumerable<string> folderPaths)
154154
{

0 commit comments

Comments
 (0)