Commit ea02d50
## Summary
Closes #47. `bridge triage`'s phantom-classified findings were uniformly
recommending **\"Remove unused dependency `<pkg>` from Cargo.toml\"**,
but the 2026-05-26 estate sweep showed every sampled phantom (28/28 in a
6-repo sample, 157 phantoms total) was a *transitive* dep pulled in by
an upstream crate — never declared in any `Cargo.toml`. The action was
unactionable.
## Fix
Parse the project's `Cargo.toml` dependency tables once per triage run,
then route phantom findings to one of two action strings based on
whether the package appears as a direct dependency.
| Case | Old action | New action |
|---|---|---|
| Phantom + direct dep | \"Remove unused dependency `<pkg>` from
Cargo.toml\" | (unchanged) |
| Phantom + transitive | \"Remove unused dependency `<pkg>` from
Cargo.toml\" ❌ | \"Transitive — run `cargo update -p <pkg>` to pull a
non-vulnerable version if one is published, or upgrade the upstream
crate that pulls it in. Otherwise informational: code unreachable from
this project.\" |
| Unreachable / Reachable | (unchanged) | (unchanged) |
## Implementation
| File | Change |
|---|---|
| `src/bridge/lockfile.rs` | New `collect_direct_cargo_dependencies()`
walks the root `Cargo.toml` + each `workspace.members` manifest. Indexes
`[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`,
`[workspace.dependencies]`, and target-prefixed variants
(`[target.cfg(...).dependencies]` etc.). Crate names normalised to
hyphen + lowercase so a CVE feed reporting `serde_json` matches a
manifest line `serde-json`. |
| `src/bridge/classify.rs` | `classify()` signature gains `is_direct:
bool`. Phantom arm splits on the flag; reachable arms unchanged. |
| `src/bridge/mod.rs` | `triage()` builds the direct-deps set **once**
(outside the per-CVE loop) and passes the lookup result into classify. |
## Regression coverage
New tests:
- `direct_deps_skips_transitive_only_crates` — direct repro of the `lru`
/ `ratatui` case from the issue.
- `direct_deps_collects_dev_and_build_sections` — all three direct
sections.
- `direct_deps_handles_target_sections` —
`[target.cfg(unix).dependencies]` etc.
- `direct_deps_handles_workspace_members` — root manifest declares
members; member deps are reachable.
- `direct_deps_normalises_underscore_to_hyphen` — `serde_json` ↔
`serde-json`.
- `direct_deps_ignores_commented_lines_and_strings_with_hash` — `# foo =
\"1.0\"` does not count.
- `direct_deps_empty_when_no_manifest` — graceful degradation (returns
empty set, all phantoms treated as transitive).
- `test_phantom_transitive_recommends_cargo_update` — the bug behaviour
from #47 is gone.
- `test_phantom_direct_recommends_removal` — direct case unchanged.
- `test_reachable_classification_unaffected_by_is_direct` — invariant.
## Test plan
- [x] `cargo test --bin panic-attack --features signing,http bridge::` —
28 passed (10 new + 18 existing)
- [x] Full binary test suite: 236 passed, 0 failed
- [x] `cargo clippy --all-targets --features signing,http -- -D
warnings` — clean
- [x] `cargo fmt --check` — clean
- [x] GPG-signed commit
## Out of scope (per the issue)
> For unmitigable + reachable: actionable warning (this is the real-risk
category — 8 in the 2026-05-26 sweep, all advisory-issued without fixed
versions).
The current unmitigable rationale already describes import sites +
lack-of-fix. Whether the action wording for that category needs further
sharpening is a separate concern from the
wrong-action-on-phantom-transitive bug.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent db0e1b7 commit ea02d50
3 files changed
Lines changed: 396 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
19 | 26 | | |
20 | 27 | | |
21 | 28 | | |
22 | 29 | | |
| 30 | + | |
23 | 31 | | |
24 | 32 | | |
25 | 33 | | |
26 | | - | |
| 34 | + | |
27 | 35 | | |
28 | 36 | | |
29 | 37 | | |
| |||
37 | 45 | | |
38 | 46 | | |
39 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
40 | 66 | | |
41 | 67 | | |
42 | 68 | | |
| |||
178 | 204 | | |
179 | 205 | | |
180 | 206 | | |
181 | | - | |
182 | | - | |
| 207 | + | |
| 208 | + | |
183 | 209 | | |
184 | | - | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
185 | 239 | | |
186 | 240 | | |
187 | 241 | | |
188 | 242 | | |
189 | | - | |
| 243 | + | |
190 | 244 | | |
191 | 245 | | |
192 | 246 | | |
193 | 247 | | |
194 | 248 | | |
195 | | - | |
| 249 | + | |
196 | 250 | | |
197 | 251 | | |
198 | 252 | | |
199 | 253 | | |
200 | 254 | | |
201 | 255 | | |
202 | | - | |
| 256 | + | |
203 | 257 | | |
204 | 258 | | |
205 | 259 | | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
206 | 269 | | |
0 commit comments