Skip to content

Commit 1b1d52c

Browse files
committed
refactor(metrics): recalibrate structural risk and priority
1 parent 8caa23d commit 1b1d52c

25 files changed

Lines changed: 344 additions & 268 deletions

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
# TokenMap
22

3-
TokenMap is a desktop app for quickly finding the parts of a local codebase that are large, complex, hotspot-heavy, or likely to be worth refactoring first.
3+
TokenMap is a desktop app for quickly finding the parts of a local codebase that carry the most structural risk or are most worth refactoring first.
44

55
<img src="docs/readme/screenshot.png" alt="TokenMap main window" width="838">
66

77
## Why TokenMap?
88

99
- See which folders and files actually dominate a repository.
10-
- Find complexity and hotspot signals before refactors, cleanup, or architecture work.
10+
- Find refactor candidates before cleanup, decomposition, or architecture work.
1111
- Estimate which parts of a codebase will cost the most tokens in LLM workflows.
1212

1313
## Metrics
1414

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

1919
## How It Works
2020

2121
- TokenMap scans a local folder into one snapshot.
2222
- `.gitignore`, global excludes, and folder excludes decide what gets in.
2323
- Only included files are measured and shown.
24-
- Metrics are computed locally, and local git history can add extra signals to Refactor Priority.
24+
- Metrics are computed locally, and local git history can add extra change-pressure signals to Refactor Priority.
2525
- Fully offline: no code is uploaded or sent to external services.
2626

2727
> [!NOTE]

docs/architecture.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +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-
- `Refactor Priority` is the product-facing composite score for refactoring urgency; git-derived change pressure stays an internal input, not a separate public metric.
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.
4344
- Token counting stays behind `ITokenCounter`.
4445
- The treemap stays one custom-rendered control.

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

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,13 @@ 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-
["{{complexity}}"] = FormatMetricValue(MetricIds.ComplexityPoints, metrics),
50-
["{{hotspots}}"] = FormatMetricValue(MetricIds.CallableHotspotPoints, metrics),
49+
["{{structural_risk}}"] = FormatMetricValue(MetricIds.ComplexityPoints, metrics),
5150
["{{refactor_priority}}"] = FormatMetricValue(MetricIds.RefactorPriorityPoints, metrics),
52-
["{{complexity_breakdown}}"] = BuildFormulaSection(
53-
title: "Complexity",
54-
ProductMetricFormulas.TryComputeComplexity(metrics, out var complexityBreakdown),
55-
complexityBreakdown,
56-
unavailableReason: "Complexity is unavailable because the required syntax-derived inputs were not produced for this file."),
57-
["{{hotspots_breakdown}}"] = BuildFormulaSection(
58-
title: "Hotspots",
59-
ProductMetricFormulas.TryComputeHotspots(metrics, out var hotspotsBreakdown),
60-
hotspotsBreakdown,
61-
unavailableReason: "Hotspots are unavailable because the required callable-risk inputs were not produced for this file."),
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."),
6256
["{{refactor_priority_breakdown}}"] = BuildRefactorPrioritySection(metrics),
6357
};
6458

@@ -103,7 +97,7 @@ private static string BuildRefactorPrioritySection(MetricSet metrics)
10397
if (!HasGitContext(metrics))
10498
{
10599
builder.AppendLine();
106-
builder.Append("- Refactor Priority currently reflects intrinsic pressure only because git-derived change-pressure inputs are unavailable.");
100+
builder.Append("- Refactor Priority currently matches Structural Risk because git-derived change and co-change inputs are unavailable.");
107101
}
108102

109103
return builder.ToString();

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ 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("{{complexity}}", "Composite complexity score."),
14-
new("{{hotspots}}", "Composite hotspot score."),
13+
new("{{structural_risk}}", "Structural-risk score for the file."),
1514
new("{{refactor_priority}}", "Composite refactor-priority score."),
16-
new("{{complexity_breakdown}}", "Multi-line explanation for complexity drivers."),
17-
new("{{hotspots_breakdown}}", "Multi-line explanation for hotspot drivers."),
15+
new("{{structural_risk_breakdown}}", "Multi-line explanation for structural-risk drivers."),
1816
new("{{refactor_priority_breakdown}}", "Multi-line explanation for refactor-priority drivers."),
1917
];
2018
}

src/Clever.TokenMap.App/ViewModels/FilePreviewExplainabilityViewModel.cs

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,16 @@ private FilePreviewExplainabilityViewModel(IReadOnlyList<MetricExplainabilitySec
2727
var metrics = node.ComputedMetrics;
2828
return new FilePreviewExplainabilityViewModel(
2929
[
30-
CreateComplexitySection(metrics),
31-
CreateHotspotsSection(metrics),
30+
CreateStructuralRiskSection(metrics),
3231
CreateRefactorPrioritySection(metrics),
3332
]);
3433
}
3534

36-
private static MetricExplainabilitySectionViewModel CreateComplexitySection(MetricSet metrics)
35+
private static MetricExplainabilitySectionViewModel CreateStructuralRiskSection(MetricSet metrics)
3736
{
3837
var definition = DefaultMetricCatalog.Instance.GetRequired(MetricIds.ComplexityPoints);
3938
var metricValue = metrics.GetOrDefault(MetricIds.ComplexityPoints);
40-
if (!ProductMetricFormulas.TryComputeComplexity(metrics, out var breakdown))
39+
if (!ProductMetricFormulas.TryComputeStructuralRisk(metrics, out var breakdown))
4140
{
4241
return MetricExplainabilitySectionViewModel.Unavailable(
4342
definition.DisplayName,
@@ -49,29 +48,7 @@ private static MetricExplainabilitySectionViewModel CreateComplexitySection(Metr
4948
definition.DisplayName,
5049
MetricValueFormatter.Format(definition.Id, metricValue, CultureInfo.CurrentCulture),
5150
breakdown.TotalPoints <= 0d
52-
? "Low structural complexity."
53-
: $"Driven mainly by {JoinLabels(GetTopContributors(breakdown, 2))}.",
54-
note: null,
55-
contributors: CreateContributorViewModels(breakdown));
56-
}
57-
58-
private static MetricExplainabilitySectionViewModel CreateHotspotsSection(MetricSet metrics)
59-
{
60-
var definition = DefaultMetricCatalog.Instance.GetRequired(MetricIds.CallableHotspotPoints);
61-
var metricValue = metrics.GetOrDefault(MetricIds.CallableHotspotPoints);
62-
if (!ProductMetricFormulas.TryComputeHotspots(metrics, out var breakdown))
63-
{
64-
return MetricExplainabilitySectionViewModel.Unavailable(
65-
definition.DisplayName,
66-
MetricValueFormatter.Format(definition.Id, metricValue, CultureInfo.CurrentCulture),
67-
"This metric is unavailable for this file.");
68-
}
69-
70-
return MetricExplainabilitySectionViewModel.Available(
71-
definition.DisplayName,
72-
MetricValueFormatter.Format(definition.Id, metricValue, CultureInfo.CurrentCulture),
73-
breakdown.TotalPoints <= 0d
74-
? "No hotspot thresholds are currently triggered."
51+
? "Low structural risk."
7552
: $"Driven mainly by {JoinLabels(GetTopContributors(breakdown, 2))}.",
7653
note: null,
7754
contributors: CreateContributorViewModels(breakdown));
@@ -95,29 +72,26 @@ private static MetricExplainabilitySectionViewModel CreateRefactorPrioritySectio
9572
MetricValueFormatter.Format(definition.Id, metricValue, CultureInfo.CurrentCulture),
9673
hasGitContext
9774
? BuildRefactorPrioritySummary(breakdown)
98-
: "Based on intrinsic pressure only.",
75+
: "Matches structural risk because git change context is unavailable.",
9976
note: hasGitContext ? null : "Git context unavailable.",
10077
contributors: CreateContributorViewModels(breakdown));
10178
}
10279

10380
private static string BuildRefactorPrioritySummary(MetricFormulaBreakdown breakdown)
10481
{
105-
var dominantCategory = breakdown.Components
106-
.GroupBy(component => component.Category, StringComparer.Ordinal)
107-
.Select(group => new
108-
{
109-
Category = group.Key,
110-
ContributionPoints = group.Sum(component => component.ContributionPoints),
111-
})
112-
.OrderByDescending(group => group.ContributionPoints)
113-
.FirstOrDefault()
114-
?.Category;
115-
116-
return dominantCategory switch
82+
var changeContribution = breakdown.Components
83+
.Where(component => string.Equals(component.Category, "Change pressure", StringComparison.Ordinal))
84+
.Sum(component => component.ContributionPoints);
85+
var cochangeContribution = breakdown.Components
86+
.Where(component => string.Equals(component.Category, "Co-change pressure", StringComparison.Ordinal))
87+
.Sum(component => component.ContributionPoints);
88+
89+
return (changeContribution > 0d, cochangeContribution > 0d) switch
11790
{
118-
"Change pressure" => "Driven mainly by recent change pressure.",
119-
"Co-change pressure" => "Driven mainly by co-change pressure.",
120-
_ => "Driven mainly by intrinsic pressure.",
91+
(true, true) => "Driven mainly by structural risk, amplified by recent change and co-change pressure.",
92+
(true, false) => "Driven mainly by structural risk, amplified by recent change pressure.",
93+
(false, true) => "Driven mainly by structural risk, amplified by co-change pressure.",
94+
_ => "Driven mainly by structural risk.",
12195
};
12296
}
12397

src/Clever.TokenMap.Core/Metrics/DefaultMetricCatalog.cs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,13 @@ public sealed class DefaultMetricCatalog : IMetricCatalog
3333
"File size in bytes and summed directory size."),
3434
new(
3535
MetricIds.ComplexityPoints,
36-
"Complexity",
37-
"Complexity",
36+
"Structural Risk",
37+
"Risk",
3838
MetricUnit.Score,
3939
MetricRollupKind.Sum,
4040
VisibleByDefault: true,
4141
SupportsTreemapWeight: true,
42-
"Open-ended composite complexity points for files and summed directory rollups."),
43-
new(
44-
MetricIds.CallableHotspotPoints,
45-
"Hotspots",
46-
"Hotspots",
47-
MetricUnit.Score,
48-
MetricRollupKind.Sum,
49-
VisibleByDefault: true,
50-
SupportsTreemapWeight: true,
51-
"Additive callable hotspot points for files and summed directory rollups."),
42+
"Continuous intrinsic structural-risk score based on file scale, callable burden, and risk distribution."),
5243
new(
5344
MetricIds.RefactorPriorityPoints,
5445
"Refactor Priority",
@@ -57,7 +48,7 @@ public sealed class DefaultMetricCatalog : IMetricCatalog
5748
MetricRollupKind.Sum,
5849
VisibleByDefault: true,
5950
SupportsTreemapWeight: true,
60-
"Open-ended additive refactor priority points for files and summed directory rollups.")
51+
"Refactoring priority score that combines structural risk with recent change and co-change pressure.")
6152
];
6253

6354
private readonly Dictionary<MetricId, MetricDefinition> _definitionsById =

0 commit comments

Comments
 (0)