Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/domain/analysis/module-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,13 @@ function countRoles(db: BetterSqlite3Database, noTests: boolean) {
`SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL ${testFilter} GROUP BY role`,
)
.all() as Array<{ role: string; c: number }>;
const roles: Record<string, number> & { dead?: number } = {};
const roles: Record<string, number> = {};
let deadTotal = 0;
for (const r of roleRows) {
roles[r.role] = r.c;
if (r.role.startsWith(DEAD_ROLE_PREFIX)) deadTotal += r.c;
}
if (deadTotal > 0) roles.dead = deadTotal;
return roles;
return { roles, deadTotal };
}

function getComplexitySummary(db: BetterSqlite3Database, testFilter: string) {
Expand Down Expand Up @@ -424,14 +423,13 @@ function computeQualityScore(

/** Aggregate role counts and derive the `dead` total. */
function aggregateRolesFromNative(roleCounts: Array<{ role: string; count: number }>) {
const roles: Record<string, number> & { dead?: number } = {};
const roles: Record<string, number> = {};
let deadTotal = 0;
for (const r of roleCounts) {
roles[r.role] = r.count;
if (r.role.startsWith(DEAD_ROLE_PREFIX)) deadTotal += r.count;
}
if (deadTotal > 0) roles.dead = deadTotal;
return roles;
return { roles, deadTotal };
}

type NativeGraphStatsFn = NonNullable<NativeDatabase['getGraphStats']>;
Expand All @@ -454,7 +452,7 @@ function buildStatsFromNative(
for (const k of s.nodesByKind) nodesByKind[k.kind] = k.count;
const edgesByKind: Record<string, number> = {};
for (const k of s.edgesByKind) edgesByKind[k.kind] = k.count;
const roles = aggregateRolesFromNative(s.roleCounts);
const { roles, deadTotal } = aggregateRolesFromNative(s.roleCounts);

const callerCoverage =
s.quality.callableTotal > 0 ? s.quality.callableWithCallers / s.quality.callableTotal : 0;
Expand Down Expand Up @@ -508,6 +506,7 @@ function buildStatsFromNative(
falsePositiveWarnings,
},
roles,
deadTotal: deadTotal > 0 ? deadTotal : undefined,
complexity: s.complexity
? {
analyzed: s.complexity.analyzed,
Expand Down Expand Up @@ -542,7 +541,7 @@ function buildStatsFromJs(
const embeddings = getEmbeddingsInfo(db);
const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
const quality = computeQualityMetrics(db, testFilter, fpThreshold);
const roles = countRoles(db, noTests);
const { roles, deadTotal } = countRoles(db, noTests);
const complexity = getComplexitySummary(db, testFilter);

return {
Expand All @@ -554,6 +553,7 @@ function buildStatsFromJs(
embeddings,
quality,
roles,
deadTotal: deadTotal > 0 ? deadTotal : undefined,
complexity,
};
}
Expand Down
28 changes: 22 additions & 6 deletions src/presentation/queries-cli/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { debug } from '../../infrastructure/logger.js';
import { outputResult } from '../../infrastructure/result-formatter.js';
import { toErrorMessage } from '../../shared/errors.js';
import { DEAD_ROLE_PREFIX } from '../../shared/kinds.js';

interface OutputOpts {
json?: boolean;
Expand Down Expand Up @@ -95,6 +96,7 @@ interface StatsData {
embeddings?: EmbeddingsInfo;
quality?: QualityInfo;
roles?: Record<string, number>;
deadTotal?: number;
complexity?: ComplexityInfo;
communities?: CommunityInfo;
}
Expand Down Expand Up @@ -130,7 +132,10 @@ function printCountGrid(entries: [string, number][], padWidth: number): void {
for (let i = 0; i < parts.length; i += 3) {
const row = parts
.slice(i, i + 3)
.map((p) => p.padEnd(padWidth))
// A part at or beyond padWidth would otherwise run straight into the next
// column with no separator, since padEnd is a no-op once the string already
// meets the target width.
.map((p) => (p.length >= padWidth ? `${p} ` : p.padEnd(padWidth)))
.join('');
console.log(` ${row}`);
}
Expand Down Expand Up @@ -225,11 +230,22 @@ function printRoles(data: StatsData): void {
if (data.roles && Object.keys(data.roles).length > 0) {
const total = Object.values(data.roles).reduce((a, b) => a + b, 0);
console.log(`\nRoles: ${total} classified symbols`);
const roleEntries = Object.entries(data.roles).sort((a, b) => b[1] - a[1]) as [
string,
number,
][];
printCountGrid(roleEntries, 18);
const liveEntries = Object.entries(data.roles).filter(
([role]) => !role.startsWith(DEAD_ROLE_PREFIX),
) as [string, number][];
printCountGrid(
liveEntries.sort((a, b) => b[1] - a[1]),
18,
);
if (data.deadTotal) {
console.log(` dead ${data.deadTotal}`);
const deadEntries = Object.entries(data.roles).filter(([role]) =>
role.startsWith(DEAD_ROLE_PREFIX),
) as [string, number][];
for (const [role, count] of deadEntries.sort((a, b) => b[1] - a[1])) {
console.log(` ${role} ${count}`);
}
}
}
}

Expand Down
16 changes: 14 additions & 2 deletions tests/integration/roles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,8 @@ describe('statsData with roles', () => {
const data = statsData(dbPath);
expect(data.roles).toBeDefined();
expect(Object.keys(data.roles).length).toBeGreaterThan(0);
// Should have dead for the unused function
expect(data.roles.dead).toBeGreaterThanOrEqual(1);
// Should have a dead sub-role for the unused function
expect(data.deadTotal).toBeGreaterThanOrEqual(1);
});

test('roles distribution respects noTests filter', () => {
Expand All @@ -466,6 +466,18 @@ describe('statsData with roles', () => {
const totalWithout = Object.values(withoutTests.roles).reduce((a, b) => a + b, 0);
expect(totalWithout).toBeLessThanOrEqual(totalWith);
});

test('roles map does not carry an aggregate "dead" peer key', () => {
// Regression test for #2383: `dead` used to be injected into the flat
// roles map as the sum of its own dead-* sub-roles, double-counting
// every dead symbol in any total over the map.
const data = statsData(dbPath);
expect(data.roles.dead).toBeUndefined();
const deadSubRoleTotal = Object.entries(data.roles)
.filter(([role]) => role.startsWith('dead-'))
.reduce((sum, [, count]) => sum + count, 0);
expect(data.deadTotal).toBe(deadSubRoleTotal);
});
});

// ─── whereData includes role ────────────────────────────────────────────
Expand Down
Loading