Skip to content

feat: dead role sub-categories (Phase 4.1)#504

Merged
carlos-alm merged 17 commits intomainfrom
docs/roadmap-audit-alignment
Mar 19, 2026
Merged

feat: dead role sub-categories (Phase 4.1)#504
carlos-alm merged 17 commits intomainfrom
docs/roadmap-audit-alignment

Conversation

@carlos-alm
Copy link
Contributor

Summary

  • Sub-classifies the coarse dead role into four categories: dead-leaf (parameters/properties/constants), dead-entry (CLI commands, MCP tools, routes/handlers), dead-ffi (cross-language FFI — .rs, .c, .go, etc.), and dead-unresolved (genuinely unreferenced callables)
  • --role dead still matches all sub-roles for backward compatibility (uses LIKE 'dead%' / prefix matching)
  • Risk weights tuned per sub-role: dead-entry: 0.3, dead-unresolved: 0.15, dead-ffi: 0.05, dead-leaf: 0.0

17 files changed across classifier, DB query layer, CLI, MCP, stats, and tests. Full test suite passes (1922 tests).

Test plan

  • 23 classifier unit tests (15 new for sub-roles, including priority ordering)
  • Unit tests for classifyNodeRoles with in-memory DB
  • Integration tests for rolesData, statsData, whereData, explainData
  • Integration flow test for orphan classification
  • Full suite: 1922 pass, 0 fail
  • Lint clean (biome check)

Add Phase 4 (Resolution Accuracy) addressing the audit's top priorities:
dead role sub-categories, receiver type tracking, interface/trait
implementation edges, precision/recall benchmarks, package.json exports
resolution, and monorepo workspace support.

Reorder phases: TypeScript Migration (now Phase 5) before Native
Acceleration (now Phase 6), per audit recommendation. Remove Web UI
from Phase 12. Add kill list (5.9), confidence annotations (7.10),
shell completion (7.11), and dual-engine architectural note.

Backlog: mark ID 45 done, promote IDs 73/74/80 to roadmap, add
Tier 1j with 10 audit-identified items (IDs 87-96).
The dual-engine strategy is settled — no need for a hedging note.
Document the rationale, trade-offs, and trajectory for maintaining
both Rust native and JS/WASM engines. Covers performance requirements
for large codebases, portability for VS Code extensions and CI, parity
convergence plan, and alternatives considered.
Native binary v3.1.0 predates the type-map feature added in #501. When
the native parser doesn't return typeMap, backfill it by re-extracting
from WASM, convert to Map format, and fall back to the JS edge builder
(which handles typeMap) instead of the native edge builder (which the
old binary ignores). This restores engine parity for the build-parity
test until a new native binary ships.

Impact: 4 functions changed, 3 affected
- Add PROMOTED annotations to backlog IDs 87, 89, 90, 91, 93 that were
  simultaneously added to both backlog and roadmap without cross-references
- Fix Tier 1j header to note exceptions (IDs 92, 94 are not zero-dep)
- Add scope note to Phase 4.6 clarifying it covers resolution layer only,
  deferring full monorepo graph support to Phase 12.2
…ignment

# Conflicts:
#	src/domain/parser.js

Impact: 15 functions changed, 15 affected
Items 71, 73, 74, 80, 83, 87, 89, 90, 91, 93 had strikethrough only on
titles but not on description and benefit columns, inconsistent with all
other done/promoted items in the table.
…ad-unresolved

The coarse "dead" role conflated genuinely different categories. Now the
classifier distinguishes: dead-leaf (parameters/properties/constants),
dead-entry (CLI commands, MCP tools, routes/handlers), dead-ffi
(cross-language FFI like .rs/.c/.go), and dead-unresolved (genuinely
unreferenced callables). --role dead still matches all sub-roles for
backward compatibility. Risk weights tuned per sub-role.

Implements Phase 4.1 of the roadmap.

Impact: 10 functions changed, 5 affected
@claude
Copy link

claude bot commented Mar 18, 2026

Claude finished @carlos-alm's task —— View job


Reviewing PR #504: Dead role sub-categories implementation

Review Checklist

  • Phase 1: Root cause analysis
  • Phase 2: Configuration change detection
  • Phase 3: Test integrity verification
  • Phase 4: Code quality review
  • Phase 5: Backlog compliance assessment
  • Final structured assessment

Phase 1 Analysis: Root Cause ✅

Problem Being Solved: The coarse "dead" role classification was too broad and not useful for triage. Dead code spans from truly unused functions (high priority) to unused parameters (normal/expected). This creates noise in audit workflows.

Why This Approach: Sub-categorizing dead code allows users to focus on genuinely problematic dead code (dead-unresolved) while deprioritizing expected dead nodes like parameters (dead-leaf) or FFI boundaries (dead-ffi).

Risk Assessment: This is architectural improvement, not symptom hiding. The logic is sound - parameters can't have callers by definition, making them "dead" by fan-in but not problematic.

Phase 2: Configuration Changes ✅

No risky configuration changes detected:

  • No package.json, biome.json, tsconfig.json, or other config files modified
  • No linting rules disabled or relaxed
  • No test timeouts increased or coverage thresholds lowered

Phase 3: Test Integrity ✅

Strong test coverage:

  • 23 new/updated classifier tests with comprehensive sub-role coverage
  • Tests verify priority ordering (dead-leaf > dead-ffi for parameters in .rs files)
  • Integration tests updated for new role names
  • No weakened assertions or conditional test logic
  • All 1922 tests pass according to PR description

Phase 4: Code Quality ✅

Clean implementation:

  • src/graph/classifiers/roles.js:36-50: Well-structured classification logic with clear priority hierarchy
  • src/db/query-builder.js:246-256: Proper backward compatibility via LIKE 'dead%' for legacy --role dead
  • src/shared/kinds.js:50-62: Constants properly organized and exported
  • src/graph/classifiers/risk.js:29-32: Risk weights are thoughtfully tuned per sub-role
  • No security concerns, SQL injection risks mitigated with parameterized queries
  • Performance impact minimal - classification logic is O(1) per node
  • Only 3 existing eslint-disable|TODO|FIXME comments found across entire codebase

Phase 5: Backlog Compliance ✅

Zero-dep: ✓ — No new runtime dependencies added to package.json
Foundation-aligned: ✓ — Fully compliant with all 8 principles:

  • P1 (always-current): No impact on rebuild speed
  • P2 (native speed): No parsing changes
  • P3 (confidence over noise): Improves result quality by reducing dead-code noise
  • P4 (zero-cost core): Purely local classification logic
  • P5 (embeddable): API contract unchanged, backward compatible
  • P6 (one registry): Uses existing kind constants and classification framework
  • P7 (security): No security implications
  • P8 (honest scope): Pure structural analysis, no code generation

Problem-fit: 4/5 — Directly improves agent accuracy by reducing false-positive dead code alerts. High-priority dead code (dead-unresolved) is now clearly separated from noise (dead-leaf).

Breaking: No — --role dead maintains backward compatibility via prefix matching

Tier: 1 (zero-dep ✓ + foundation-aligned ✓)

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Coarse "dead" role created noise in triage workflows by mixing genuinely problematic unused code with expected dead nodes (parameters, FFI boundaries)
**Why This Approach**: Sub-classification with tuned risk weights allows users to focus on real dead code while deprioritizing expected cases
**Risk Assessment**: Architectural improvement that reduces false positives without hiding real issues

## Backlog Compliance
- Zero-dep: ✓ — No new runtime dependencies
- Foundation-aligned: ✓ — Complies with all 8 core principles
- Problem-fit: 4/5 — Directly reduces agent confusion from dead code noise
- Breaking: No — Backward compatible via prefix matching
- Tier: 1

## Critical Concerns
None. This is a well-engineered improvement that enhances the utility of role-based analysis.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Clean implementation, comprehensive tests, meaningful improvement to dead code analysis, full backward compatibility. Minor deduction only because it's an incremental enhancement rather than a breakthrough feature.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 18, 2026

Greptile Summary

This PR sub-classifies the coarse dead role into four semantically distinct sub-roles (dead-leaf, dead-entry, dead-ffi, dead-unresolved), each carrying its own risk weight, while preserving full backward compatibility via --role dead prefix matching. The change touches the classifier, DB query layer, in-memory repository, CLI/MCP descriptions, stats aggregation, and the test suite (1922 passing).

  • The core classifier logic in src/graph/classifiers/roles.js is clean and well-structured, with priority ordering documented and thoroughly tested (15 new unit tests).
  • DEAD_ROLE_PREFIX and DEAD_SUB_ROLES are correctly extracted to shared/kinds.js as a single source of truth, and all query sites use parameterized bindings.
  • Visual regression: src/presentation/colors.js was not updated — all four dead sub-roles are missing from DEFAULT_ROLE_COLORS. In role-coloring mode, these nodes fall back to their kind color in the graph but render as #ccc in the legend, making dead sub-role nodes visually indistinguishable from live nodes and creating an inconsistency between the graph and its legend.
  • src/features/graph-enrichment.js uses a hardcoded 'dead' string literal for the startsWith check instead of the DEAD_ROLE_PREFIX constant used at every other call site in this PR.

Confidence Score: 3/5

  • Safe to merge for data/analysis correctness; blocks on a visual regression in the graph viewer's role-coloring mode.
  • The classifier, DB, CLI, and test layers are all correct and well-tested. The one concrete regression is in src/presentation/colors.js: the four new dead sub-roles have no color entries, so the graph viewer silently degrades when switching to role-coloring mode — nodes appear in their kind color while the legend shows gray, which is both a visual bug and an inconsistency. Until the color palette is updated, the graph visualization feature is broken for dead sub-role nodes.
  • src/presentation/colors.js — missing color entries for all four dead sub-roles causes a visual regression in the role-colored graph view.

Important Files Changed

Filename Overview
src/graph/classifiers/roles.js Core classifier addition: introduces classifyDeadSubRole() with clear priority ordering (dead-leafdead-ffidead-entrydead-unresolved) and a deliberate comment explaining the ffi-over-entry priority decision. Logic is clean and well-tested.
src/presentation/colors.js Missing color entries for all four dead sub-roles. Causes a visual regression in role-colored graph view: nodes render in kind color while the legend shows gray, making them visually indistinguishable from live nodes.
src/shared/kinds.js Clean addition of DEAD_ROLE_PREFIX and DEAD_SUB_ROLES exports. VALID_ROLES is correctly extended with ...DEAD_SUB_ROLES. Good single-source-of-truth design.
src/db/query-builder.js roleFilter() correctly uses a parameterized LIKE ? binding with DEAD_ROLE_PREFIX + '%' when the caller passes 'dead', addressing the previously flagged non-parameterized pattern. All other filters remain exact-match.
src/db/repository/in-memory-repository.js Mirrors the SQL LIKE 'dead%' behaviour with n.role?.startsWith(DEAD_ROLE_PREFIX) in the in-memory filter. Consistent with the DB layer.
src/features/graph-enrichment.js Dead-role risk check correctly widened from === 'dead' to ?.startsWith('dead'), but uses a raw string literal instead of the DEAD_ROLE_PREFIX constant used everywhere else in the PR.
src/features/structure.js classifyNodeRoles now passes kind and file to the classifier, correctly initialises the summary with all dead sub-role keys, and maintains a summary.dead aggregate alongside individual sub-role counts.
src/domain/analysis/module-map.js countRoles now aggregates all dead-* sub-roles into a roles.dead total for backward-compatible stats display, using DEAD_ROLE_PREFIX. Logic is correct.
src/graph/classifiers/risk.js Four new role weight entries added. The tuning (dead-entry: 0.3, dead-unresolved: 0.15, dead-ffi: 0.05, dead-leaf: 0.0) is consistent with the PR description and adds meaningful risk signal differentiation.
tests/graph/classifiers/roles.test.js Comprehensive test coverage: 15 new unit tests covering all sub-roles, priority ordering (dead-leaf > dead-ffi, dead-leaf > dead-entry), fallback to dead-unresolved, and edge cases (no kind/file info).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Node: fanIn=0 AND not exported] --> B{testOnlyFanIn > 0?}
    B -- Yes --> C[role = test-only]
    B -- No --> D[classifyDeadSubRole]

    D --> E{kind in LEAF_KINDS?}
    E -- Yes\nparam/prop/const --> F[dead-leaf\nriskWeight=0.0]

    E -- No --> G{file exists?}
    G -- No --> H[dead-unresolved\nriskWeight=0.15]

    G -- Yes --> I{extension in FFI_EXTENSIONS?}
    I -- Yes\n.rs/.c/.go/... --> J[dead-ffi\nriskWeight=0.05]

    I -- No --> K{path matches ENTRY_PATH_PATTERNS?}
    K -- Yes\ncli/mcp/routes/... --> L[dead-entry\nriskWeight=0.3]
    K -- No --> H

    style F fill:#fffde7
    style J fill:#fce4ec
    style L fill:#ffcdd2
    style H fill:#ef9a9a
Loading

Last reviewed commit: "fix: use DEAD_ROLE_P..."

Comment on lines +40 to +46
if (node.file) {
// Cross-language FFI: compiled-language files in a JS/TS project
const dotIdx = node.file.lastIndexOf('.');
if (dotIdx !== -1 && FFI_EXTENSIONS.has(node.file.slice(dotIdx))) return 'dead-ffi';

// Framework-dispatched entry points (CLI commands, MCP tools, routes)
if (ENTRY_PATH_PATTERNS.some((p) => p.test(node.file))) return 'dead-entry';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 dead-ffi priority blinds dead-entry for FFI files in entry-point paths

Because the file-extension check (→ dead-ffi) runs before the path-pattern check (→ dead-entry), a function in, say, src/cli/commands/analyze.rs will always be labelled dead-ffi — even though it lives in a CLI command directory. The existing priority tests cover dead-leaf > dead-ffi and dead-leaf > dead-entry, but there is no test for the dead-ffi vs dead-entry case. This is presumably intentional (the FFI boundary is considered more fundamental than the path hint), but a comment noting the deliberate ordering would prevent future readers from accidentally swapping the two blocks.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 20881fa. Added a comment above the dead-ffi check explaining the deliberate priority: FFI boundary is a more fundamental classification than path-based entry-point hints (e.g. a .so/.dll in a routes/ directory is still FFI, not an entry point).

Comment on lines +249 to +251
if (role === 'dead') {
this.#conditions.push("n.role LIKE 'dead%'");
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 LIKE 'dead%' pattern duplicated across three sites

Identical LIKE 'dead%' logic (hardcoded literal) appears in three places:

  • src/db/query-builder.js (line 250)
  • src/domain/analysis/roles.js (line 17)
  • src/db/repository/in-memory-repository.js (line 265, as startsWith('dead'))

If the prefix ever changes (e.g., dead roles are renamed to unused-*), three callsites must be updated in sync. A shared constant such as export const DEAD_ROLE_PREFIX = 'dead' in kinds.js (alongside DEAD_SUB_ROLES) would make this a single-point change and clarify the intent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3e90e19. Added DEAD_ROLE_PREFIX = 'dead' to src/shared/kinds.js alongside DEAD_SUB_ROLES, and updated all three consumption sites to use it:

  • src/db/query-builder.js: roleFilter() now uses DEAD_ROLE_PREFIX for the comparison and LIKE pattern
  • src/domain/analysis/roles.js: rolesData() uses the constant instead of hardcoded string
  • src/db/repository/in-memory-repository.js: startsWith() check uses the constant

@carlos-alm
Copy link
Contributor Author

All three review comments addressed:

  1. dead-ffi vs dead-entry priority (20881fa): Added comment in classifyDeadSubRole documenting the deliberate ordering -- FFI boundary is more fundamental than path-based entry-point hints.

  2. DEAD_ROLE_PREFIX constant (3e90e19): Extracted DEAD_ROLE_PREFIX = 'dead' into shared/kinds.js and updated all three consumption sites (query-builder.js, domain/analysis/roles.js, in-memory-repository.js) to use it instead of duplicated string literals.

  3. Stale JSDoc in test (aab2ba9): Updated tests/unit/roles.test.js line 12 from dead to dead-unresolved to match actual Phase 4.1 classification.

All 1922 tests pass, lint clean on changed files.

@greptileai

this.#conditions.push('n.role = ?');
this.#params.push(role);
if (role === DEAD_ROLE_PREFIX) {
this.#conditions.push(`n.role LIKE '${DEAD_ROLE_PREFIX}%'`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Non-parameterized LIKE pattern bypasses prepared statement convention

Every other value in roleFilter (and across the query builder) is pushed to #params and bound via ?. Here the prefix is interpolated directly into the SQL string as a string literal. While DEAD_ROLE_PREFIX = 'dead' is a safe constant today, this deviates from the codebase's consistent parameterized-query pattern. If the constant ever acquired a SQL-special character (e.g., a quote), this would silently break. Consider:

Suggested change
this.#conditions.push(`n.role LIKE '${DEAD_ROLE_PREFIX}%'`);
this.#conditions.push('n.role LIKE ?');
this.#params.push(`${DEAD_ROLE_PREFIX}%`);

The same pattern applies to src/domain/analysis/roles.js line 18.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 057b6ec. Both query-builder.js and roles.js now use parameterized \ bindings for the LIKE pattern instead of template-literal interpolation. Also fixed module-map.js (55bb468) which was the last site hardcoding \ instead of using .

Add comment explaining why FFI boundary classification takes precedence
over entry-point path hints — a .so/.dll in a routes/ directory is
fundamentally an FFI boundary, not an entry point.

Impact: 1 functions changed, 2 affected
…attern

Add shared DEAD_ROLE_PREFIX = 'dead' in shared/kinds.js and use it in
query-builder.js, domain/analysis/roles.js, and in-memory-repository.js
instead of duplicating the 'dead' string and LIKE 'dead%' pattern.

Impact: 5 functions changed, 0 affected
The test graph comment still said deadFn -> dead but the actual
classification since Phase 4.1 produces dead-unresolved for nodes
with no special sub-category match.
Replace template-literal interpolation of DEAD_ROLE_PREFIX in SQL LIKE
patterns with proper parameterized ? bindings, consistent with the rest
of the query builder's convention.

Impact: 3 functions changed, 0 affected
Replace hardcoded 'dead' string with the shared DEAD_ROLE_PREFIX
constant, making it the last consumption site to use the single-source
constant from kinds.js.

Impact: 1 functions changed, 1 affected
@carlos-alm carlos-alm force-pushed the docs/roadmap-audit-alignment branch from aab2ba9 to 55bb468 Compare March 19, 2026 00:35
@carlos-alm
Copy link
Contributor Author

Addressed the remaining Greptile findings:

  1. Parameterized LIKE binding (057b6ec): query-builder.js and roles.js now use ? bindings instead of template-literal interpolation for DEAD_ROLE_PREFIX LIKE patterns.
  2. module-map.js constant (55bb468): Replaced hardcoded 'dead' with DEAD_ROLE_PREFIX import — the last site using the raw string.
  3. Commitlint fix: Removed 4 net-zero cross-session contamination commits (WIP + revert, centralize-config + revert) that were causing the commitlint failure.

All 1922 tests pass, lint clean.

@greptileai

@carlos-alm carlos-alm merged commit f17ce96 into main Mar 19, 2026
18 checks passed
@carlos-alm carlos-alm deleted the docs/roadmap-audit-alignment branch March 19, 2026 04:05
@github-actions github-actions bot locked and limited conversation to collaborators Mar 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant