A number you can argue with.
Every finding carries a deterministic confidence in [0, 1] and the named signals that produced it. No model is involved. Same repository, same score — which means it can be reviewed, diffed, and trusted in CI.
Severity comes from confidence
| Confidence | Severity | Meaning |
|---|---|---|
| ≥ 0.85 | block | High confidence. Exit code 1. Goes in the plan unless the judge says keep. Counsel reviews it as a veto, not a skip. |
| ≥ 0.55 | warn | Probably dead, but a signal hints at dynamic or framework use. Triaged once, then verified. |
| < 0.55 | note | Lead only. Never fails CI, never sent to a model. |
| < 0.25 | dropped | Default --min-confidence. Raise it for stricter output. |
Base score by kind
orphan_file — no importer edgesunused_export — never imported by nameunused_dep — declared, never importedunreachable — private, unreferencedSignals
Signals are added to the base and clamped to [0.02, 0.99]. Negative signals are the false-positive defenses; they are the reason a Django signal handler or a Spring bean does not fail your build.
| Signal | Δ | Applies to | Why |
|---|---|---|---|
framework_registration_decorator | −0.60 | export | Routes, tasks, fixtures, commands, components are called by the framework, not imported. |
framework_entry_role | −0.55 | orphan | Django urls.py, Next.js page.tsx, Rails app/**, Spring controller/… (files with this role are skipped entirely unless they also carry a __main__ guard). |
main_guard | −0.45 | orphan | if __name__ == "__main__" means it is a script, not an orphan. |
module_getattr_lazy_export | −0.40 | export | PEP 562 lazy modules resolve names at runtime. |
*_named_in_string_literal | −0.35 | both | Another file names the module/symbol in a string: plugin loaders, settings, templates. |
framework_base_class | −0.35 | export | Models, admin classes, widgets, test cases are discovered via inheritance. |
*_named_in_config | −0.30 | both | Referenced from TOML/YAML/JSON/INI/XML/Gradle/Dockerfile/Makefile/CI. |
referenced_locally | −0.30 | export | Used inside its own module; removing the export is a smaller change than deletion. |
module_imported_whole_attribute_access_possible | −0.30 | export | import pkg.mod hides some attribute access from the graph. (Direct mod.name access is tracked and clears the export.) |
package_init | −0.30 | orphan | __init__.py is loaded implicitly. |
<lang>_name_reference_graph | −0.30 | orphan | Java, Kotlin, Scala, C#, PHP, Swift, Elixir: references are type/module tokens, so the graph is capped at warn. |
<lang>_token_match_heuristic | −0.30 | export | Non-Python export detection is token-based; never reaches block. |
name_token_seen_elsewhere | −0.20 | orphan | The file's stem appears as a token in another file — possibly a string reference or reflection. |
decorated_unknown | −0.20 | export | Any decorator may register the symbol somewhere. |
listed_in_dunder_all_public_api | −0.15 | export | Declared public API; consumers may live outside the repo. |
repo_uses_dynamic_import | −0.08 / −0.10 | both | Any importlib/__import__/getattr in the repo lowers global certainty. |
ts_regex_graph* | −0.05 / −0.30 | TS/JS | The TS graph is regex-based; exports never reach block. |
<lang>_path_resolved_graph | −0.03 | orphan | Go, Rust, Ruby, Dart, C, Lua, Perl: imports resolve to files; small discount for regex extraction. |
stable_across_runs | up to +0.03 | all | From memory: findings that survive several runs are slightly more credible. |
Worked example
{
"id": "unused_export:svc/routes.py:health",
"kind": "unused_export",
"severity": "note",
"confidence": 0.28,
"signals": {
"not_imported_by_name": 0.0,
"framework_registration_decorator": -0.6
},
"why": "Exported symbol `health` in svc/routes.py is never imported."
}
The FastAPI route is decorated with @app.get. Base 0.88 − 0.60 = 0.28 → note. It is still reported (an agent may want to know), but it can never fail CI, never appear as a deletion in the plan, and is never sent to a model.
Compare pkg/hooks.py:maybe_dead in the fixture: base 0.88 − 0.30 (module imported whole) = 0.58 → warn. That is exactly the ambiguous case the judge and triage step exist for. The fixture's pkg/nightly.py is the other class: the graph says unused, the devil's advocate finds ops/crontab:2, and the verdict is keep.