Code Coverage Orphan Class Cleanup
Activate when an org's overall Apex coverage has crept toward the 75% deploy threshold and the source of the drag is uncovered, unreferenced classes accumulated over years of half-finished features. The skill produces a ranked list of orphan classes, separates "delete-safe" from "test-required-to-keep," and emits a destructive-changes manifest.
Before Starting
Gather this context before working on anything in this domain:
- Current org-wide coverage and the gap to 75%. The Tooling API query against
ApexOrgWideCoverage gives a single number; per-class data lives in ApexCodeCoverageAggregate.
- Whether managed-package classes are inflating the denominator. Salesforce excludes managed-package code from the org-wide calculation, but unmanaged code from a previously-installed-then-uninstalled package may still be present.
- Any classes flagged as
@deprecated already, since those telegraph the team's intent.
- Whether any of the candidate classes are referenced from non-Apex artifacts: Flow Apex action invocations, LWC
@AuraEnabled callers, REST endpoints (URL mappings), Process Builder Apex actions, scheduled jobs, custom buttons. Coverage-only signals miss these references.
Core Concepts
Coverage denominator
The 75% threshold is computed across all unmanaged Apex in the org (classes + triggers, excluding test classes). Removing a 200-line class with 0% coverage subtracts 200 from both the numerator and the denominator's "uncovered" portion — net positive on the percentage.
"Orphan" definition
A class is an orphan candidate when all of the following hold:
- 0% coverage in
ApexCodeCoverageAggregate over the last full test run.
- No source-code reference: no
extends, implements, new <Class>, static call, or type usage in any other Apex.
- No metadata reference: not invoked from a Flow, Process Builder, scheduled job, REST mapping, custom button/link, validation rule formula (
$Apex), or LWC's @AuraEnabled calls.
- Not annotated
@RestResource, @HttpGet/Post/... (otherwise it's an external entry point even if no Apex calls it).
If all four hold, the class is delete-safe. If only the first three hold and the class has, e.g., a custom button reference, it must keep its test.
Coverage queries that lie
ApexCodeCoverage (per-test) and ApexCodeCoverageAggregate (latest-run) only reflect the last test run. If the run was incomplete or skipped a class because a dependency failed, coverage shows zero for that class for reasons other than missing tests. Always run a clean --test-level RunLocalTests before treating coverage as authoritative.
Common Patterns
Pattern: pre-deploy coverage rescue
When to use: Friday's deploy fails at 74.3%. You need to ship by Monday.
How it works: Query ApexCodeCoverageAggregate for 0%-coverage classes ordered by NumLinesCovered DESC + NumLinesUncovered DESC. Static-reference-check the top 20. Of those that pass all four orphan criteria, build a destructive-changes manifest and deploy with --purge-on-delete=true (sandbox first).
Why not the alternative: Writing 4,000 lines of stub tests under deadline pressure produces brittle tests that obscure real coverage signal.
Pattern: scheduled tech-debt cleanup
When to use: Quarterly engineering tax.
How it works: Same query, but instead of destructive deploy, file a per-class issue with the orphan evidence. Engineers triage: delete, test, or document why kept.
Pattern: package-deprecation cleanup
When to use: A managed package was uninstalled but unmanaged "ghost" classes remain (e.g., from an early-stage 1GP install).
How it works: Filter classes by namespace prefix in name, age, and zero-reference. Most are bulk-deletable.
Decision Guidance
| Class state |
Action |
Reason |
0% coverage, 0 source refs, 0 metadata refs, no @RestResource |
Delete |
Pure dead weight on the denominator |
| 0% coverage, used by a Flow/button/REST mapping |
Write test or document waiver |
Active code; test debt is real debt |
| Low (<75%) coverage, actively referenced |
Out of scope — see apex/test-class-standards |
Different problem (improve tests) |
| 0% coverage, looks unused, but team uncertain |
Mark @deprecated, deploy, watch one cycle |
Cheap reversibility before destructive change |
Recommended Workflow
- Run a clean
sf apex run test --test-level RunLocalTests --code-coverage --result-format json to refresh ApexCodeCoverageAggregate. Without a clean run, coverage data is unreliable.
- Query orphan candidates:
SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered FROM ApexCodeCoverageAggregate WHERE NumLinesCovered = 0.
- For each candidate, search the source tree for references: name token in
.cls, .trigger, .flow-meta.xml, .weblink-meta.xml, .flexipage-meta.xml, REST URL mappings.
- Check metadata-only references:
aura:component, LWC @AuraEnabled callers via connectedCallback patterns, Process Builder bindings.
- Categorize each candidate: delete-safe, needs-test-to-stay, deprecated-but-kept-for-back-compat.
- Build destructive-changes manifest for delete-safe classes. Deploy in sandbox first; verify no tests start failing because of compile-time references the search missed.
- Promote to higher environments. Track org-wide coverage delta — should jump cleanly upward.
Review Checklist
Salesforce-Specific Gotchas
- Coverage queries reflect only the last run — Skipping the clean test run before querying produces phantom orphans (and phantom keeps).
- REST endpoint classes have no Apex callers but are not orphans —
@RestResource(urlMapping='/api/v1/things') is invoked by an HTTP request, never by Apex. Filter by annotation before declaring orphan.
- Scheduled-job classes can be invoked from
System.schedule calls in other classes or from Setup-only schedule entries — Setup → Apex Jobs → Scheduled Jobs lists schedules that have no Apex caller record. Check before deleting any Schedulable class.
- Deleting a class fails if it's referenced by a Flow — The destructive deploy returns a clear error, but only after the apparently-clean static check. Flow XML must be searched for
<actionName>ClassName</actionName>.
Output Artifacts
| Artifact |
Description |
| orphan-candidates.csv |
Class name, lines, last-modified date, reference-search summary |
| delete-safe.xml |
destructiveChanges.xml for verified-orphan classes |
| keep-with-test.md |
Classes that must stay, with the test gap they need |
| coverage-before-after.txt |
Org-wide coverage delta proof |
Related Skills
- apex/test-class-standards — for the actively-referenced classes that need a real test, not a deletion
- devops/destructive-changes-deployment — for safely shipping the delete manifest
- architect/technical-debt-assessment — when the orphan list is large enough to be a strategic conversation, not a tactical sweep
1---2name: code-coverage-orphan-class-cleanup3description: Use when an org's overall Apex coverage is sliding toward 75% because of orphaned (uncovered, unreferenced) classes inflating the denominator. Triggers: 'production deploy blocked at 74% coverage', 'find apex classes with 0% coverage', 'orphan apex class report', 'org coverage dropping despite green test runs'. NOT for writing tests on actively used classes (use apex/test-class-standards) or for raising coverage on partially-covered classes.4---56# Code Coverage Orphan Class Cleanup78Activate when an org's overall Apex coverage has crept toward the 75% deploy threshold and the source of the drag is *uncovered, unreferenced* classes accumulated over years of half-finished features. The skill produces a ranked list of orphan classes, separates "delete-safe" from "test-required-to-keep," and emits a destructive-changes manifest.910---1112## Before Starting1314Gather this context before working on anything in this domain:1516- Current org-wide coverage and the gap to 75%. The Tooling API query against `ApexOrgWideCoverage` gives a single number; per-class data lives in `ApexCodeCoverageAggregate`.17- Whether managed-package classes are inflating the denominator. Salesforce excludes managed-package code from the org-wide calculation, but unmanaged code from a previously-installed-then-uninstalled package may still be present.18- Any classes flagged as `@deprecated` already, since those telegraph the team's intent.19- Whether any of the candidate classes are referenced from non-Apex artifacts: Flow Apex action invocations, LWC `@AuraEnabled` callers, REST endpoints (URL mappings), Process Builder Apex actions, scheduled jobs, custom buttons. Coverage-only signals miss these references.2021---2223## Core Concepts2425### Coverage denominator2627The 75% threshold is computed across **all unmanaged Apex** in the org (classes + triggers, excluding test classes). Removing a 200-line class with 0% coverage subtracts 200 from both the numerator and the denominator's "uncovered" portion — net positive on the percentage.2829### "Orphan" definition3031A class is an *orphan* candidate when **all** of the following hold:3233- 0% coverage in `ApexCodeCoverageAggregate` over the last full test run.34- No source-code reference: no `extends`, `implements`, `new <Class>`, static call, or type usage in any other Apex.35- No metadata reference: not invoked from a Flow, Process Builder, scheduled job, REST mapping, custom button/link, validation rule formula (`$Apex`), or LWC's `@AuraEnabled` calls.36- Not annotated `@RestResource`, `@HttpGet/Post/...` (otherwise it's an external entry point even if no Apex calls it).3738If all four hold, the class is delete-safe. If only the first three hold and the class has, e.g., a custom button reference, it must keep its test.3940### Coverage queries that lie4142`ApexCodeCoverage` (per-test) and `ApexCodeCoverageAggregate` (latest-run) only reflect the **last test run**. If the run was incomplete or skipped a class because a dependency failed, coverage shows zero for that class for reasons other than missing tests. Always run a clean `--test-level RunLocalTests` before treating coverage as authoritative.4344---4546## Common Patterns4748### Pattern: pre-deploy coverage rescue4950**When to use:** Friday's deploy fails at 74.3%. You need to ship by Monday.5152**How it works:** Query `ApexCodeCoverageAggregate` for 0%-coverage classes ordered by NumLinesCovered DESC + NumLinesUncovered DESC. Static-reference-check the top 20. Of those that pass all four orphan criteria, build a destructive-changes manifest and deploy with `--purge-on-delete=true` (sandbox first).5354**Why not the alternative:** Writing 4,000 lines of stub tests under deadline pressure produces brittle tests that obscure real coverage signal.5556### Pattern: scheduled tech-debt cleanup5758**When to use:** Quarterly engineering tax.5960**How it works:** Same query, but instead of destructive deploy, file a per-class issue with the orphan evidence. Engineers triage: delete, test, or document why kept.6162### Pattern: package-deprecation cleanup6364**When to use:** A managed package was uninstalled but unmanaged "ghost" classes remain (e.g., from an early-stage 1GP install).6566**How it works:** Filter classes by namespace prefix in name, age, and zero-reference. Most are bulk-deletable.6768---6970## Decision Guidance7172| Class state | Action | Reason |73|---|---|---|74| 0% coverage, 0 source refs, 0 metadata refs, no `@RestResource` | Delete | Pure dead weight on the denominator |75| 0% coverage, used by a Flow/button/REST mapping | Write test or document waiver | Active code; test debt is real debt |76| Low (<75%) coverage, actively referenced | Out of scope — see apex/test-class-standards | Different problem (improve tests) |77| 0% coverage, looks unused, but team uncertain | Mark `@deprecated`, deploy, watch one cycle | Cheap reversibility before destructive change |7879---8081## Recommended Workflow82831. Run a clean `sf apex run test --test-level RunLocalTests --code-coverage --result-format json` to refresh `ApexCodeCoverageAggregate`. Without a clean run, coverage data is unreliable.842. Query orphan candidates: `SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered FROM ApexCodeCoverageAggregate WHERE NumLinesCovered = 0`.853. For each candidate, search the source tree for references: name token in `.cls`, `.trigger`, `.flow-meta.xml`, `.weblink-meta.xml`, `.flexipage-meta.xml`, REST URL mappings.864. Check metadata-only references: `aura:component`, LWC `@AuraEnabled` callers via `connectedCallback` patterns, Process Builder bindings.875. Categorize each candidate: delete-safe, needs-test-to-stay, deprecated-but-kept-for-back-compat.886. Build destructive-changes manifest for delete-safe classes. Deploy in sandbox first; verify no tests start failing because of compile-time references the search missed.897. Promote to higher environments. Track org-wide coverage delta — should jump cleanly upward.9091---9293## Review Checklist9495- [ ] Coverage data refreshed via clean local-tests run, not a stale snapshot96- [ ] Each candidate verified against four orphan criteria (coverage, source ref, metadata ref, REST entry)97- [ ] Sandbox deploy of the destructive manifest passes all tests98- [ ] Org-wide coverage measured before and after; delta documented99- [ ] Classes kept-but-untested have a tracked issue or `@deprecated` annotation100101---102103## Salesforce-Specific Gotchas1041051. **Coverage queries reflect only the last run** — Skipping the clean test run before querying produces phantom orphans (and phantom keeps).1062. **REST endpoint classes have no Apex callers but are not orphans** — `@RestResource(urlMapping='/api/v1/things')` is invoked by an HTTP request, never by Apex. Filter by annotation before declaring orphan.1073. **Scheduled-job classes can be invoked from `System.schedule` calls in *other* classes or from Setup-only schedule entries** — Setup → Apex Jobs → Scheduled Jobs lists schedules that have no Apex caller record. Check before deleting any `Schedulable` class.1084. **Deleting a class fails if it's referenced by a Flow** — The destructive deploy returns a clear error, but only after the apparently-clean static check. Flow XML must be searched for `<actionName>ClassName</actionName>`.109110---111112## Output Artifacts113114| Artifact | Description |115|---|---|116| orphan-candidates.csv | Class name, lines, last-modified date, reference-search summary |117| delete-safe.xml | destructiveChanges.xml for verified-orphan classes |118| keep-with-test.md | Classes that must stay, with the test gap they need |119| coverage-before-after.txt | Org-wide coverage delta proof |120121---122123## Related Skills124125- apex/test-class-standards — for the actively-referenced classes that need a real test, not a deletion126- devops/destructive-changes-deployment — for safely shipping the delete manifest127- architect/technical-debt-assessment — when the orphan list is large enough to be a strategic conversation, not a tactical sweep