fix(fn-impact): add direct and transitive summary fields to JSON output#1603
Conversation
… cross-file edges The role classifier inferred "is exported" purely from cross-file calls/imports-type edges and barrel reexports. This missed symbols that are declared with the `export` keyword but are only used as type annotations within the same file — no edge is produced for same-file type usage, so the symbol's fan-in stayed 0 and isExported=false, yielding a spurious dead-unresolved classification. Fix: after the existing cross-file-edge and reexport-barrel checks, query `WHERE exported = 1` and add those IDs to exportedIds. Applied to all four classification paths: classifyNodeRolesFull and classifyNodeRolesIncremental in TypeScript, and do_classify_full and do_classify_incremental in the Rust native engine. docs check acknowledged Closes #1583
…ables Rust struct/enum/trait definitions (and equivalent type-definition kinds in other languages) have fan_in=0 by design — they are consumed via type annotations and struct literals, neither of which produces a call edge. The classifier previously sent these through `classifyDeadSubRole`, which returned `dead-ffi` for any `.rs` file (FFI extension check). Fix: extend the existing `constant` heuristic to cover all annotation-only kinds (`struct`, `enum`, `trait`, `type`, `interface`, `record`). When the same file has at least one active callable (function/method with fanIn > 0 or fanOut > 0), these type definitions are almost certainly live — classify them as `leaf` instead of dead. Changes: - `src/graph/classifiers/roles.ts`: add TYPE_DEF_KINDS set; check it in classifyUnreferencedNode alongside the existing constant check - `src/features/structure.ts`: extract ANNOTATION_ONLY_KINDS constant; use it in buildActiveFilesSet (exclude type defs from "active" count) and buildClassifierInput (pass hasActiveFileSiblings for type defs) - `crates/codegraph-core/src/graph/classifiers/roles.rs`: mirror both changes in the Rust native classifier (TYPE_DEF_KINDS constant, classify_node guard, compute_active_files exclusion, classify_rows is_annotation_only check) - `tests/unit/roles.test.ts`: two new tests for #1584 — one verifying struct/enum/trait classify as leaf with active siblings, one verifying they remain dead with no active siblings docs check acknowledged Closes #1584
…not dead-entry Methods named `execute` and `validate` in files matching ENTRY_PATH_PATTERNS (cli/commands/, routes/, handlers/, mcp/, middleware/) are Commander.js dispatch entry points confirmed at the framework level. Classifying them as `dead-entry` caused them to appear in `--role dead` output, producing ~12,000+ false positives and making dead-code analysis unactionable on Commander-based CLIs. The fix adds COMMANDER_DISPATCH_NAMES (execute, validate) and promotes matching methods directly to `entry` when their file path confirms they are in a framework dispatch directory. An `execute` in a non-framework directory (src/utils/) is still classified as dead-unresolved. Both the TypeScript classifier and the mirrored Rust classifier are updated. Closes #1585
Three call-site patterns cause false-positive dead-unresolved classification
when codegraph cannot trace the call relationship:
1. Interface dispatch via conditional property access:
`if (v.enterFunction) v.enterFunction(fn, ctx)` — the call resolves to
the property accessor, not the concrete method implementation, so the
method gets fanIn=0. Affects enterFunction/exitFunction/enterNode/exitNode/
finish methods in cfg-visitor.ts, complexity-visitor.ts, dataflow-visitor.ts.
2. Logical-or function defaults:
`const fn = options._fetchLatest || fetchLatestVersion` — the function
reference is a value reference, not a call site, so no call edge is produced.
Requires fanOut>0 as evidence the function is non-trivial. Affects
fetchLatestVersion in update-check.ts.
3. Handler-table object property callbacks:
`{ handle: handleReturn }` — the function stored as a property value
generates no call edge when dispatched via `h.handle(...)`.
Fix: Extend the existing hasActiveFileSiblings heuristic (previously only
applied to constant kind) to also cover method and function kinds. When the
same file contains at least one callable connected to the graph:
- method kinds: always classified as leaf (interface implementations)
- function kinds with fanOut>0: classified as leaf (non-trivial helpers
used as value references)
The fanOut>0 guard for function kinds prevents genuinely inert dead functions
(fanOut=0) from being promoted to leaf, preserving dead-code detection for
the common case.
Both the TypeScript classifier and the mirrored Rust classifier are updated.
Also updates a stale unit test that expected dead-entry for execute() in CLI
files, which was corrected to entry by #1585.
Closes #1586
-- docs check acknowledged
fn-impact --json was missing direct/transitive shorthand counts, making it difficult for tools (GAUNTLET, manifesto rules, scripts) to assess blast radius without parsing the full levels object. Adds two computed fields to each fnImpactData result: - direct: count of level-1 callers (functions that directly call the target) - transitive: totalDependents - direct (callers at depth 2+) These are derived from the existing BFS levels data and add no extra DB queries. Fully backward-compatible: existing fields are unchanged. Closes #1587
Greptile SummaryThis PR adds
Confidence Score: 5/5Safe to merge; the fn-impact output change is additive and the classifier fixes are well-tested with positive and boundary cases. The core change (adding src/graph/classifiers/roles.ts — the new function leaf-promotion heuristic (fanOut > 0 in active file) is intentionally broad and may reduce visibility of some genuinely dead functions in Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[bfsTransitiveCallers] --> B[levels + totalDependents]
B --> C[direct = level-1 count]
B --> D[transitive = total - direct]
C --> E[fnImpactData result]
D --> E
F[classifyNodeRole fanIn=0, not exported] --> G{Commander name + entry path?}
G -- yes --> H[entry]
G -- no --> I[classifyUnreferencedNode]
I --> J{hasActiveFileSiblings?}
J -- no --> K[classifyDeadSubRole]
J -- yes --> L{kind}
L -- constant --> M[leaf]
L -- TYPE_DEF_KINDS --> M
L -- method + fanOut gt 0 --> M
L -- function + fanOut gt 0 --> M
L -- other --> K
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[bfsTransitiveCallers] --> B[levels + totalDependents]
B --> C[direct = level-1 count]
B --> D[transitive = total - direct]
C --> E[fnImpactData result]
D --> E
F[classifyNodeRole fanIn=0, not exported] --> G{Commander name + entry path?}
G -- yes --> H[entry]
G -- no --> I[classifyUnreferencedNode]
I --> J{hasActiveFileSiblings?}
J -- no --> K[classifyDeadSubRole]
J -- yes --> L{kind}
L -- constant --> M[leaf]
L -- TYPE_DEF_KINDS --> M
L -- method + fanOut gt 0 --> M
L -- function + fanOut gt 0 --> M
L -- other --> K
Reviews (3): Last reviewed commit: "fix(roles): add fanOut > 0 guard to meth..." | Re-trigger Greptile |
| if (node.kind === 'method') { | ||
| // Methods implementing interfaces are dispatched via conditional property | ||
| // access e.g. `if (v.enterFunction) v.enterFunction(...)`. Codegraph | ||
| // resolves the call to the property accessor rather than to the concrete | ||
| // method implementation, so the method has no inbound call edge. All | ||
| // methods with active file siblings are almost certainly interface | ||
| // implementations — classify as leaf. | ||
| return 'leaf'; | ||
| } |
There was a problem hiding this comment.
Method leaf-promotion is asymmetric with the function case
Functions require fanOut > 0 as evidence of non-triviality before being promoted to leaf when hasActiveFileSiblings is true. Methods have no such guard — any method with fanIn === 0 in a file with active callables is immediately classified leaf. A genuinely dead helper method (e.g., a private method with no callers and no outgoing calls that was accidentally left behind) in a busy file will be silently excluded from --role dead output. Adding the same node.fanOut > 0 guard as the function case would preserve the intent (interface dispatch methods do call out) while keeping trivially-inert dead methods visible.
There was a problem hiding this comment.
Fixed. Added fanOut > 0 guard to the method case in both roles.ts and roles.rs, making it symmetric with the function case. A trivially-inert method (fanOut === 0, no callers) in a busy file now surfaces as dead-unresolved. Added a new test case to verify this behavior.
Codegraph Impact Analysis3 functions changed → 9 callers affected across 5 files
|
|
Addressed both Greptile P2 findings:
|
Summary
fn-impact --jsonwas missingdirectandtransitiveshorthand counts in each result, making it difficult for tools (GAUNTLET, manifesto rules, external scripts) to assess blast radius without parsing the fulllevelsobjectfnImpactDataresult:direct: count of level-1 callers (functions that call the target directly)transitive:totalDependents - direct(callers at depth 2+)levelsdata with no additional DB queries; existing fields (levels,totalDependents) are unchanged — fully backward-compatibleTest plan
result includes direct and transitive summary fieldsverifiesdirect == level[1].length,transitive == totalDependents - direct, anddirect + transitive == totalDependentsqueries.test.tsstill pass (87 total with 2 new)buildFileCallEdgesnow reportsdirect: 1, transitive: 5, totalDependents: 6matching the actual call graphcodegraph fn-impact) unchangedCloses #1587