Answers Salesforce developer "what breaks if I change X / where is M reachable from / what does this method touch / is this dead code" questions. Cascades the v2.3 what-if composer family (`what_if_deactivate_flow`, `what_if_disable_trigger`, `what_if_change_method_signature`, `what_if_change_field_type`, `what_if_remove_picklist_value`, `what_if_make_field_required`, `what_if_merge_profiles`, `what_if_split_profile`) and the v2.7 deep code-understanding tools (`call_graph`, `downstream_effec...
Scanned 9/3/2026
Install to Claude Code
npx -y skills add PranavNagrecha/Salesforce-Intelligence --skill developer-impact-and-reachability --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Developer Impact And Reachability?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pranavnagrecha-developer-impact-and-reachability)More formats (shields.io, HTML) on the badges page.
---
name: developer-impact-and-reachability
description: |
Answers Salesforce developer "what breaks if I change X / where is M
reachable from / what does this method touch / is this dead code"
questions. Cascades the v2.3 what-if composer family
(`what_if_deactivate_flow`, `what_if_disable_trigger`,
`what_if_change_method_signature`, `what_if_change_field_type`,
`what_if_remove_picklist_value`, `what_if_make_field_required`,
`what_if_merge_profiles`, `what_if_split_profile`) and the v2.7 deep
code-understanding tools (`call_graph`, `downstream_effects`,
`test_coverage_for_method`, `method_reachability`,
`meaningful_test_audit`). Discloses the v2.3 / v2.7 boundary axes
verbatim: class-granularity (not method); dynamic-dispatch and
reflective-invocation invisibility; method-name echo (`method_name`
surfaced verbatim, not subsetted); cross-class transitive analysis
not available.
---
# Developer impact and reachability
## Overview
This skill is the developer persona's companion to v2.3's what-if
composer family and v2.7's deep code-understanding tools. The
developer's first-line question is one of five shapes:
1. **"What breaks if I change X?"** — for a specific component +
change kind, the answer is a structured `WhatIfImpactItem[]`
list grouped by category (`metadata-blocker`,
`code-needs-update`, `integration-touch`, `test-class-update`,
`invisible-risk`, `configuration-only`). v2.3 composers.
2. **"Where is method M reachable from?"** — `sfi.call_graph` with
`direction: 'both'` walks `callsApex` edges in both directions
from a root class.
3. **"What does this method touch downstream?"** —
`sfi.downstream_effects` walks downstream `callsApex` and
surfaces side effects (field writes, async dispatches, emails).
4. **"What tests cover this method?"** —
`sfi.test_coverage_for_method` walks upstream `callsApex` from
the target and filters to `isTest === true` classes.
5. **"Is this code dead / where does this Apex actually run from?"**
— `sfi.method_reachability` walks upstream `callsApex` against
the entry-point taxonomy and returns one of three verdicts.
Plus a sixth shape:
6. **"Does this test actually assert anything?"** —
`sfi.meaningful_test_audit` ranks every `@isTest` class by
fake-assertion count and assertion density.
The boundary that matters for developers: **v2.7 ships
CLASS-level granularity.** A call from `ApexClass:A.foo()` to
`ApexClass:B.bar()` produces ONE `A → B` `callsApex` edge with no
method partition. The `methodName` input parameter on
`sfi.test_coverage_for_method` is ACCEPTED and ECHOED verbatim into
the response so callers can pipeline through a future v2.7.1
method-scoped resolution — but v2.7 does NOT subset coverage by
method. Surface this verbatim.
`sfi.method_reachability`, `sfi.call_graph` and
`sfi.test_coverage_for_method` carry a `soundness` envelope
(`complete` / `blindSpots[]` / `staticCoverage`) with two blind-spot
kinds. `dynamic-apex`: the analyzed class uses dynamic Apex, so a
reflective caller can make a reachability verdict wrong and a
test→method mapping incomplete. `unwalked-edge-type`: the walk
traversed a strict SUBSET of the usage edge types, so a component
reachable only through an un-walked type is absent from the result —
it carries `walkedEdgeTypes` and `unwalkedEdgeTypes` by name. This is
why `sfi.call_graph` (a deliberate `callsApex`-only walk) is never
`complete: true`, while `sfi.method_reachability` (which walks the
whole usage set) can be. Treat `complete: false` as "verify by reading
the source", and never present it as a full picture.
The v2.3 composer boundary: **the composers project, not
predict.** Each what-if composer reads the v2.2-vintage vault state,
applies its own per-tool rule set, and returns a
structured impact list. Runtime evaluation, dataflow analysis,
cross-class transitive analysis, and dynamic Apex are all invisible.
Each finding's `confidence` is the worst confidence on the walk path
(`heuristic` < `parsed` < `declared`).
## When to fire
Fire this skill on what-if / reachability / call-graph phrasing.
Concrete triggers:
### What-if shape
- **"What breaks if I deactivate this Flow?"** — Use
`sfi.what_if_deactivate_flow`.
- **"What breaks if I disable this trigger?"** — Use
`sfi.what_if_disable_trigger`.
- **"What breaks if I change `processOrder`'s signature?"** — Use
`sfi.what_if_change_method_signature`.
- **"What breaks if I change `Industry__c` from Text to
Picklist?"** — Use `sfi.what_if_change_field_type`.
- **"What breaks if I remove the `Tier 1` picklist value?"** — Use
`sfi.what_if_remove_picklist_value`.
- **"What breaks if I make `Industry__c` required?"** — Use
`sfi.what_if_make_field_required`.
- **"What if I merge `Sales_Rep` and `Sales_Manager` profiles?"** —
Use `sfi.what_if_merge_profiles`.
- **"How would I split `Sales_Rep` into per-team perm sets?"** —
Use `sfi.what_if_split_profile`.
### Reachability / call-graph shape
- **"Where is `OpportunityService.processOpp` called from?"** /
**"What calls this method?"** — Use `sfi.call_graph` with
`direction: 'upstream'`.
- **"What does `processOpp` call?"** / **"What does this method
invoke downstream?"** — Use `sfi.call_graph` with
`direction: 'downstream'`.
- **"Map the full call chain around `OpportunityService`."** — Use
`sfi.call_graph` with `direction: 'both'`, `maxDepth: 3`.
- **"What side effects does `processOpp` have downstream?"** — Use
`sfi.downstream_effects` (field writes, async dispatches, email
sends).
- **"What tests cover `OpportunityService`?"** — Use
`sfi.test_coverage_for_method`.
- **"Is `OpportunityService` dead code? / Where is this class
actually reached from?"** — Use `sfi.method_reachability`.
### Test-quality shape
- **"Does `OpportunityServiceTest` actually assert anything?"** /
**"What tests are fake?"** — Use `sfi.meaningful_test_audit`.
## When NOT to fire
Defer to another skill when:
- **The user asks "where is `OpportunityService` used in source code?"**
That's a code-reference question. Defer to
`developer-apex-refactor` → `sfi.find_code_usages`.
- **The user asks "what fields does Opportunity have?"** Schema
lookup; defer to `answering-org-questions`.
- **The user asks "audit my Apex for quality issues."** Defer to
`developer-code-quality` → `sfi.code_quality_audit`.
- **The user asks "what's the convention for status fields?"**
Pattern recognition; defer to `recognizing-naming-conventions`.
- **The user wants the full cross-component dependency report**
("what depends on this field?"). That's
`architect-impact-analysis` → `sfi.get_impact` (BFS over every
edge type, not just `callsApex`).
- **The user wants live coverage percentages.** v2.7 surfaces test
REACHABILITY, not Apex Test Run's actual coverage percentage.
v2.7 is offline; tell the user to run `sf apex test run` for the
runtime numbers.
- **The user wants the recognizer to WRITE the refactor.** v2.3 is
read-only; it surfaces impact but never generates the deploy
package.
## The cascade
The 16 tools split by question shape. Pick the right entry point.
### Category A — What-if change projection (v2.3, 11 composers)
Each composer reads the v2.2-vintage vault state, applies its own rule
set, and returns:
- `impacts: WhatIfImpactItem[]` — one entry per affected component.
(The key is `impacts`, NOT `findings`; no what-if tool emits a
`findings` array.)
- `summary` — per-tool aggregations (the profile composers only).
- `disclosure: string` — the tool's verbatim boundary paragraph. It is
a single string, NOT a `boundaries: string[]`. Surface it verbatim.
Each `WhatIfImpactItem` carries `category`, `componentId`,
`componentType`, `apiName`, `confidence` and `explanation`. The
category vocabulary is per tool: the shared six are
`metadata-blocker` | `code-needs-update` | `integration-touch` |
`test-class-update` | `invisible-risk` | `configuration-only`, plus
`input-only` (a dependency the component CONSUMES — deliberately
excluded from the verdict) and, on `what_if_deactivate_flow`,
`broken-caller` (a parent Flow invoking this Flow as a subflow) and
`embedded-caller` (a FlexiPage whose Flow component embeds this Flow).
**Two verdict axes, not one.** `what_if_deactivate_flow` and
`what_if_disable_trigger` return BOTH `structuralVerdict` — what the
dependency structure says, in practice `safe` / `review` / `risky` /
`blocking` — and the headline `verdict`. The shared `Verdict` union is
`safe | review | risky | blocking | unknown | already-inactive`, and
`already-inactive` is the headline whenever the component does not run
today, whatever the structure says. It is never downgraded by
coverage, and it is deliberately NOT folded into `safe`: `safe` means
"no impacts at all", so reusing it would make an inactive-but-heavily-
depended-on component read identically to a genuinely inert one.
`runtimeState`
{`status`, `currentlyRunning`, `note`} carries the runs-today axis on
its own, and `currentlyRunning: null` means UNKNOWN — not inactive.
`entryPoints[]` (where the runtime hands control TO the component) is
reported separately from `impacts` and is not a dependent.
`notProvenHarmless` appears exactly when no verdict-bearing impact was
found; render it with the `safe`.
#### `sfi.what_if_deactivate_flow`
Walks every outgoing edge from the Flow: `triggersOn` (the object
listened to), `readsFrom`/`writesTo` (record lookups + DML),
`callsApex` (Apex action calls), `sendsEmail` (email templates), and
the subflows THIS Flow invokes (`references` / `referenceKind:
'subflow'`). Each becomes a `WhatIfImpactItem`. **R6-02 — the incoming
side:** parent Flows that invoke THIS Flow as a subflow are BROKEN
CALLERS on deactivation, surfaced as a distinct `broken-caller`
category, and FlexiPages whose Flow component EMBEDS this Flow
(`references` / `referenceKind: 'embeddedFlow'`) are EMBEDDED CALLERS,
surfaced as a distinct `embedded-caller` category — the embed is an
active invocation at render time, not passive access (this used to be
excluded on the "access, not usage" theory; it no longer is).
`structuralVerdict`: `safe` (no verdict-bearing impact) / `risky`
(callsApex only, or broken callers that are all inactive
Draft/Obsolete) / `blocking` (record write, trigger, email-send, or
subflow-invocation impact; any broken caller that is an ACTIVE parent
Flow; or ANY embedded-caller — a FlexiPage embed forces `blocking`
UNCONDITIONALLY, because this vault cannot tell whether that page is
assigned to an active app / profile / record page layout).
`triggersOn` / `listensTo` are NOT impacts: they are the Flow's own
`entryPoints[]`. The headline `verdict` is `already-inactive` whenever
the Flow does not run today, whatever the structure says — do not
report "deactivating this changes nothing" from an inactive Flow's
empty impact list; report that it is already off and what still
depends on it. `referenceKind: 'subflow'` and `referenceKind:
'embeddedFlow'` are the only incoming edges that count as dependents;
every other incoming `references` kind, plus `grantedBy` / `parentOf`,
stays excluded as passive access/structure.
```json
{ "flowId": "Flow:Set_Opportunity_Owner" }
```
#### `sfi.what_if_disable_trigger`
Similar walk for `ApexTrigger:`. Handler classes via outgoing
`callsApex`; async dispatches via outgoing `dispatchesAsync`. The
`callsApex` handler edges come from the default-on Apex AST pass, so
most `code-needs-update` findings carry `confidence: parsed`; the
`dispatchesAsync` async edges remain `heuristic` (that async-dispatch
recognizer is still scanner-based). Cite each finding's own
confidence.
#### `sfi.what_if_change_method_signature`
Identifies callers via the default-on Apex AST call-site index (the
recall scanner backfills files the AST could not parse). The tool
reports each finding at its own edge confidence. Caller types:
- **Apex static caller** — `OpportunityService.processOpp(...)`
pattern. `code-needs-update`; `parsed` for AST-resolved call-sites,
`heuristic` for scanner-backfill edges.
- **Apex instance caller** — `obj.processOpp(...)` where `obj` is
typed as `OpportunityService`. Same (the AST resolves the receiver
type through the symbol table).
- **Apex test caller** — an incoming `callsApex` edge from a class
whose `properties.isTest` is true. `test-class-update`; `parsed`
/ `heuristic` as above. The tool ALSO walks a `coversTest` edge,
but that edge has no producer anywhere in the product, so it
contributes nothing on a real vault — see the disclosure below.
- **Flow caller** (when target is `@InvocableMethod`) — `callsApex`
edge + matching `<actionName>`. `code-needs-update`, `declared`.
Surface the verbatim Q105 disclosure:
> Callers are identified via the default-on Apex AST call-site
> index and surface at `parsed` confidence; the recall scanner
> backfills files the AST could not parse at `heuristic`. Cite
> each caller's own confidence. Dynamic dispatch via
> Type.forName + invoke is invisible to both. Test classes are
> identified in ONE way that actually works: an incoming
> `callsApex` edge from a class whose `properties.isTest` is
> true. The `coversTest` edge this tool ALSO walks is declared
> in the contract but is emitted by NO extractor, graph-build
> mint, or enricher in this product, so on a real vault that
> walk ALWAYS returns nothing — Salesforce does not declare
> test-to-class coverage anywhere in the metadata source format
> (coverage is a RUNTIME artifact of a test run, readable only
> from the Tooling API's ApexCodeCoverage). Read an empty
> `testClassesNeedingUpdate` as "test-coverage mapping
> UNAVAILABLE for this class", NEVER as "no tests cover this
> class": a test class that exercises the target only
> indirectly — through a helper, a trigger, or dynamic
> dispatch — has no `callsApex` edge to it and is invisible
> here.
#### `sfi.what_if_change_field_type`
Driven by a fixed field-type compatibility matrix. For each
transition `[c]` (forward-compatible), `[l]` (lossy), or `[b]`
(breaking), emits findings only for references that are type-
sensitive. Categories typically spread across `metadata-blocker`
(formulas, validation rules), `code-needs-update` (Apex, LWC),
`integration-touch` (integration schemas), `configuration-only`
(layouts).
#### `sfi.what_if_remove_picklist_value`
The most dependency-heavy v2.3 tool. Composes over:
- Dependent picklist detection (`properties.controllingField`).
- Formula reference detection (re-tokenize each formula source).
- Flow decision detection (v2.0a `firesWhen` →
`ConditionalContext` → search for `== 'value'` /
`!= 'value'` patterns).
- Apex string-literal detection (conjunction: ApexClass has the
literal AND has an incoming `readsFrom`/`writesTo` edge to the
target field).
The hard dependency on v2.0a: without `firesWhen` edges and
`ConditionalContext` extraction, Flow decisions keyed on the value
would be invisible. The composer surfaces an error if the vault
is missing v2.0a extraction.
#### `sfi.what_if_change_field_value`
The Data-Steward / Identity lens: what breaks if a field's stored
VALUE changes — NOT its schema (that is `change_field_type`).
Returns impact buckets (identity / integration-key / uniqueness /
automation / save-pipeline / display), an overall severity, and
recommended pre-change checks. Identity / key / uniqueness verdicts
come from the field's OWN metadata (`externalId` / `unique` /
`idLookup`, identity catalog), so a value change is flagged even on
a field with ZERO references (e.g. a SAML federation key). Derived
fields (formula / roll-up / auto-number) return `mutable: false`
and re-route to their source. Optional `newValue` adds a targeted
collision/acceptance check.
Surface verbatim: the vault cannot see external upsert systems, the
IdP side of SSO, or dynamic / managed-package code; automation
buckets surface only declarative value-literal couplings — Apex
literal comparisons remain invisible. For the portfolio version
across many fields on an object, use `sfi.value_change_audit`.
#### `sfi.what_if_make_field_required`
Walks the parent object's write paths:
- **Layout coverage** — layouts without the field display →
`configuration-only`.
- **Flow create coverage** — Flow `<recordCreates>` without the
field in `<inputAssignments>` → `metadata-blocker`, `parsed`.
- **Integration write coverage** — `ExternalService` /
`ExternalDataSource` not declaring the field →
`integration-touch`, `declared`.
- **Apex create coverage** — NOT WALKED (deferred to dataflow
analysis).
Surface the verbatim Apex-invisible disclosure:
> The analysis checks layouts (UI input paths), Flow create paths,
> and integration write surfaces. Apex `insert acc;` sites that
> may or may not set the field are invisible — determining whether
> `acc.Industry__c` was assigned before the insert requires
> dataflow analysis. If your org has Apex create paths, verify
> the field is set before making required.
#### `sfi.what_if_merge_profiles`
Takes exactly two profiles; groups grants by `(settingType,
settingKey)` and emits one `WhatIfImpactItem` per conflict.
Conflict shape depends on setting type. Default
`conflictResolution: 'manual-only'` surfaces every conflict with
`suggestedResolution: 'manual'`. Optional `'max'` / `'min'` for
starting-point recommendations.
Multi-profile merge (3+) is not supported in v2.3.
#### `sfi.what_if_split_profile`
Greedy heuristic: each grant goes to the best-keyword-match perm
set (API-name token matching), then domain-cluster fallback, then
default target. No backtracking. Optimal partitioning is deferred
to a future milestone.
Surface verbatim: greedy + fail-conservative; per-org
optimal-partitioning override is not supported.
#### `sfi.what_if_assign_permset`
The NET access a user would GAIN by assigning a target permission
set (or PSG). Give the target (`permissionSetId` — a
`PermissionSet:` / `PermissionSetGroup:` id or bare name) and a
`baseline` container (`{ profileId?, permissionSetIds?[] }` — the
user's current profile + already-assigned sets). It runs the SAME
effective-permissions engine as `sfi.effective_permissions` TWICE
(baseline WITH vs WITHOUT the target) and diffs the max-wins,
muting-applied EFFECTIVE grant sets. NET-CHANGE CORRECTNESS is the
whole value: a permission the baseline already holds via its
profile or another set cancels out and is NOT counted as gained.
Delta classes: `objectPermissions`, `fieldPermissions`,
`systemPermissions`, `customPermissions`, `recordTypeVisibilities`.
Verdict `safe` on a no-op, else `review`.
Surface verbatim: the delta is the NET change under max-wins; this
is a hypothetical READ (nothing is assigned); grants are `declared`
metadata; object permission is not record access (visibility still
depends on OWD + sharing).
#### `sfi.what_if_revoke_permset`
The mirror of `assign_permset`: the NET access a user would LOSE by
revoking a target set whose baseline SHOULD include it. Same twice-
run effective-permissions diff; a permission ALSO granted by the
profile or another assigned set is NOT counted as lost (the user
keeps it). Revoking a set not in the baseline is a disclosed no-op
(`targetInBaseline: false`). Verdict `safe` on a no-op, else
`review`. Same honesty surface as `assign_permset`.
### Category B — Reachability (v2.7, 5 tools)
#### `sfi.call_graph`
BFS over `callsApex` edges from a root ApexClass / ApexTrigger.
Direction: `'upstream'` (incoming — who calls me), `'downstream'`
(outgoing — what do I call), or `'both'`. Default `maxDepth: 3`.
```json
{ "rootId": "ApexClass:OpportunityService", "direction": "both", "maxDepth": 3 }
```
Class-granularity is the v2.7 honesty boundary. State it.
Input key is **`rootId`** (or the `componentId` alias) — it is
REQUIRED. There is no `classApiName` alias on this tool, unlike
`method_reachability` / `explain_apex_method`, so a call naming one is
an `invalid-query`. `maxDepth` is bounded at **5**; `edgeTypes` widens
the walk beyond `callsApex`.
The response carries:
- `nodes[]` — every reached class.
- `edges[]` — every `callsApex` edge walked.
- `cycleDetected: boolean` — whether the BFS observed any back-edge.
- `otherUsageInEdges` {`count`, `byType`} — ALWAYS present. The
incoming usage edges this walk did NOT follow. This is what stops
`edges: []` reading as "no callers": a `count` of 0 is a CHECKED
zero; a non-zero count names exactly what was left unfollowed.
Surface it whenever the call list is empty.
- `soundness` — `complete: false` with an `unwalked-edge-type` blind
spot naming the usage edge types (e.g. `references`,
`dispatchesAsync`) the default `callsApex`-only walk skipped.
- `disclosure` — the verbatim class-granularity disclosure.
#### `sfi.downstream_effects`
Composes `sfi.call_graph` downstream then, for every reachable
class, lists outgoing `writesTo` (field-write effects),
`dispatchesAsync` (async dispatch effects), and `sendsEmail`
(email effects). Returns `DownstreamEffect[]` categorized by
edgeType.
Callouts are NOT a separate v2.7 edge type (no `Apex.Http.send`
extractor yet); the disclosure surfaces this gap explicitly.
#### `sfi.test_coverage_for_method`
BFS upstream over `callsApex` from the target, filtered to nodes
with `properties.isTest === true`. Returns `coveringTestClasses[]`
sorted by id.
The `methodName` parameter is ACCEPTED and echoed verbatim into
the response — but v2.7 does NOT subset coverage by method. The
class-granularity honesty boundary is verbatim. Method-level
granularity is deferred to v2.7.1.
#### `sfi.method_reachability`
Walks upstream **USAGE** edges from the target — every `EdgeType`
EXCEPT `parentOf` (structural containment) and `grantedBy` (an access
grant is not a use). It is a DENY-list, not the `['callsApex']`
allow-list it used to be: an allow-list can never learn about an edge
type added after it (`dispatchesAsync`, the Apex scanner's
`references`), and it is wrong in the direction that calls live code
dead.
ONE walk does both jobs. It classifies each reached node against the
entry-point taxonomy:
- `ApexTrigger:*` (any).
- `ApexClass` with `properties.isRestResource === true`.
- `ApexClass` with `properties.hasAuraEnabledMethod === true`.
- `ApexClass` with `properties.hasInvocableMethod === true`.
- `ApexClass` with any of `properties.isQueueable` /
`isBatchable` / `isSchedulable`.
- the ROOT itself when `properties.isTest === true` — the test
RUNNER is a test class's entry point. Depth 0 ONLY: a test class
UPSTREAM of the root is coverage, not an entry point, so
`test-only-reachable` survives.
- a class reached by a `references` edge from a VisualforcePage /
VisualforceComponent / AuraDefinitionBundle — the `controller=`
binding, derived from the EDGE rather than a node property.
- **unproven dynamic registration** — a class whose `superclass` is
dotted (another namespace's framework instantiates its own
subclasses) or that declares the `Callable` interface. These fire at
ANY depth, are floored at `heuristic` confidence, and establish only
that the class is BUILT to be invoked from outside this vault —
never that the registration is live.
The same walk marks reached `properties.isTest === true` classes as
test coverage. There is no second walk.
Combined verdict:
- `entry-point-reachable` — at least one entry point reaches.
- `test-only-reachable` — no entry point reaches but at least one
test class does.
- `likely-dead-code` — neither reaches within the depth cap.
Surface the response's own `disclosure` verbatim rather than
paraphrasing it. It states the v2.7 class granularity, the depth-3 BFS
cap, and that dynamic dispatch (`Type.forName`), reflective invocation
and framework wiring are invisible. Two conditional suffixes matter:
- on `likely-dead-code` it adds that this is NOT the org's dead-code
verdict — `sfi.find_dead_code` runs an additional whole-word source
re-check and may downgrade the same class to `uncertain`. Run it
before concluding.
- when the ONLY entry points found are unproven registrations it says
so, so a bare `entry-point-reachable` is never read as proof.
#### `sfi.meaningful_test_audit`
For every `isTest === true` ApexClass, computes:
- `assertionCount` — invocations of `System.assert*` per v2.1.
- `fakeAssertionCount` — `qualityIssues[]` entries with
`rule === 'fake-assertion'`.
- `density` — `assertionCount / max(1, sourceBytes / 1000)`.
Sorts by `fakeAssertionCount DESC, density ASC`. Top-of-list test
classes are most likely candidates for a meaningfulness audit.
Verbatim disclosure: the heuristic recognizes direct
`System.assert*` tokens; helper methods
(`MyTestHelper.assertField(record, ...)`) and framework wrappers
are invisible.
## Honesty axes
### v2.7 class-granularity boundary (surface on every reachability response — summarised here; quote the response's own `disclosure`)
> v2.7 ships CLASS-level granularity. A call from `ApexClass:A.foo()`
> to `ApexClass:B.bar()` produces ONE `A → B` `callsApex` edge
> with no method partition. The `methodName` input is accepted and
> echoed into the response so callers can pipeline through future
> method-scoped resolution — but v2.7 does NOT subset coverage,
> reachability, or downstream effects by method. Method-level edge
> resolution is deferred to v2.7.1.
### v2.7 invisible-dispatch boundary (surface on every reachability response — the response's own `disclosure` string is the authoritative wording; this is a summary of it, not a quotation)
> Dynamic dispatch (`Type.forName('...').newInstance().method(...)`),
> reflective invocation, same-namespace framework wiring
> (TriggerHandler / fflib base classes), and managed-package callers
> are INVISIBLE to the graph edges these tools walk. A class genuinely
> invoked at runtime via one of these mechanisms will surface as
> `likely-dead-code` or with an empty `coveringTestClasses[]`. Verify
> before treating as the answer.
Two dynamic-registration shapes are the exception: a class extending a
base class from ANOTHER namespace (a dotted `superclass`) and a class
declaring `Callable` are now recognised as `framework-subclass` /
`callable-dispatch` entry points at `heuristic` confidence. They keep
such a class OFF `likely-dead-code` and map it to `uncertain` in
`sfi.find_dead_code` — but they prove only that the class is BUILT for
outside invocation, never that the registration is live. Do not read
either as "reachable"; read it as "not dead, unproven".
### v2.3 confidence-floor rule
For each finding, `confidence` is the worst confidence on the walk
path from the changing component to the affected component. The
order is: `heuristic` (worst) < `parsed` < `declared` (best).
| Walk path | Confidence |
|---|---|
| Profile FLS, layout placement, XML-declared condition | `declared` |
| Apex AST field reads/writes + cross-class calls (default-on), formula tokenizer, Flow walker, integration schema parser | `parsed` |
| Apex recall scanner (parse-failure/gap backfill), `dispatchesAsync` async-dispatch recognizer, LWC/Aura/VF frontend scanner | `heuristic` |
State the per-finding confidence explicitly; don't paraphrase
`heuristic` as "likely".
### v2.3 per-tool boundaries (selected verbatim phrases)
| Tool | Verbatim phrase |
|---|---|
| `what_if_change_field_type` | "Lookup → Text and MasterDetail → Text are structurally compatible but lose foreign-key semantics. Roll-up summary fields, sharing-by-parent, and cascade-delete behavior change." |
| `what_if_remove_picklist_value` | "Apex variable-based comparisons (`if (account.Industry__c == myVar)`) are invisible. Dynamic SOQL filters by picklist value are invisible. Reports / Dashboards / List Views filtered by this value are NOT extracted." |
| `what_if_make_field_required` | "Apex `insert acc;` sites that may or may not set the field are invisible — dataflow analysis required." |
| `what_if_deactivate_flow` | "Deactivation does NOT delete the Flow; its definition remains and a later reactivation restores every effect listed. Parent Flows invoking this Flow as a declared `<subflows>` call ARE modeled as broken callers (an Active parent forces `blocking`); Lightning pages whose Flow component EMBEDS this Flow (declared `embeddedFlow` references) ARE modeled as embedded callers and always force `blocking`. The STILL-invisible path is Apex that invokes the Flow via `Flow.Interview` or `@InvocableMethod` chains, plus non-metadata launch points (buttons, quick actions)." |
| `what_if_disable_trigger` | "Apex code that conditionally invokes the disabled trigger logic via a static utility wrapping the same handler is invisible. Test classes using `Test.startTest()` / `Test.stopTest()` semantics may depend on the trigger firing — review test setup before disabling." |
| `what_if_change_method_signature` | "Callers come from the default-on Apex AST call-site index (`parsed`), with the recall scanner backfilling parse-failure files (`heuristic`) — cite each caller's own confidence; dynamic dispatch via Type.forName + invoke is invisible to both. Test classes identified by @isTest + naming convention; non-convention test classes may be missed." |
| `what_if_merge_profiles` | "Multi-profile (3+) merge is not supported in v2.3 — exactly two profiles per call. Tie-break defaults to A wins for setting types where comparators are undefined." |
| `what_if_split_profile` | "Greedy heuristic with no backtracking. Optimal partitioning (minimum overlap, maximum coverage) requires graph clustering; deferred to future milestone." |
### v2.7 v2.1 inheritance — meaningful_test_audit
- **`@isTest` recognition tied to v1.4 extraction.** A test class
identified ONLY by naming convention (`MyClassTest` with no
`@isTest` annotation) WILL NOT have `isTest: true`. The audit
scope is `properties.isTest === true` set.
- **Custom assertion helpers invisible.** A test class with all
assertions via a `MyTestHelper.expectField(record, ...)` helper
will surface with `assertionCount: 0` and rank near the top of
the suspicious list — a false positive. Verify before treating
as a real concern.
## Worked example
User: *"What breaks if I deactivate `Flow:Set_Opportunity_Owner`?"*
Claude's flow:
1. **Classify** → what-if shape, deactivate-Flow.
2. **Fire** `sfi.run_analysis` with `{ "name": "sfi.what_if_deactivate_flow", "args": { … } }` with
`{ "flowId": "Flow:Set_Opportunity_Owner" }`.
3. **Receive** (illustrative):
```json
{
"data": {
"appliedScope": { "component": "Flow:Set_Opportunity_Owner", "mode": "component" },
"flowId": "Flow:Set_Opportunity_Owner",
"apiName": "Set_Opportunity_Owner",
"status": "Active",
"runtimeState": { "status": "Active", "currentlyRunning": true, "note": "…" },
"verdict": "blocking",
"structuralVerdict": "blocking",
"entryPoints": [
{ "kind": "triggersOn", "componentId": "CustomObject:Opportunity", "note": "Where the runtime hands control TO this Flow — not a dependent." }
],
"impacts": [
{ "componentId": "CustomField:Opportunity.Owner", "componentType": "CustomField", "apiName": "Opportunity.Owner", "category": "metadata-blocker", "confidence": "parsed", "explanation": "Flow writes to Owner via recordUpdate element 'assign_owner'." },
{ "componentId": "ApexClass:OwnerAssignmentService", "componentType": "ApexClass", "apiName": "OwnerAssignmentService", "category": "code-needs-update", "confidence": "declared", "explanation": "Flow calls OwnerAssignmentService.assign via Apex action." }
],
"firingConditions": [
{ "conditionContextId": "ConditionalContext:Flow:Set_Opportunity_Owner.condition-0", "expression": "RecordType.DeveloperName == 'Standard_Opportunity' AND Owner == null" }
],
"trust": { "…": "…" },
"disclosure": "v2.3 what-if analysis is composition over the v2.2 vault state. …"
}
}
```
Keys to hold onto: it is `impacts`, not `findings`; `disclosure` is
ONE string, not a `boundaries` array; an impact row carries
`componentType` and `explanation` (there is no `type`, `location` or
`suggestedAction`); a firing condition carries `conditionContextId`
and `expression` and nothing else; and `triggersOn` sits in
`entryPoints`, not in `impacts`.
4. **Respond:**
> Deactivating `Flow:Set_Opportunity_Owner` — verdict: **blocking**
> (structural verdict `blocking`; the Flow is Active, so it runs
> today). Two impacts:
>
> **Metadata blockers (1)**
> - `CustomField:Opportunity.Owner` — the Flow writes Owner via
> `recordUpdate` element `assign_owner`. Records saved without
> another owner-assignment surface will be created with no
> Owner. (`parsed`)
>
> **Code needs update (1)**
> - `ApexClass:OwnerAssignmentService` — the Flow calls
> `OwnerAssignmentService.assign` via an Apex action.
> Deactivating the Flow means this Apex action is no longer
> invoked from this path; if the class is used elsewhere it
> continues to function. (`declared`)
>
> **Entry point (not an impact)**
> - `CustomObject:Opportunity` — where the runtime hands control TO
> this Flow. Deactivation silences that trigger, but the object is
> not something the Flow breaks.
>
> **Firing conditions silenced**
> - `ConditionalContext:Flow:Set_Opportunity_Owner.condition-0`
> — `RecordType.DeveloperName == 'Standard_Opportunity' AND
> Owner == null`. Records matching this condition will no longer
> be auto-assigned.
>
> Suggested next checks:
>
> 1. Confirm no other automation (Apex Trigger, Workflow Rule)
> writes Opportunity Owner on save. Run
> `sfi.find_field_anywhere` on
> `CustomField:Opportunity.Owner` filtered to `writesTo`
> edges.
> 2. Verify `OwnerAssignmentService.assign` is not the ONLY
> caller of downstream Apex via incoming `callsApex` edges.
> Run `sfi.run_analysis` with `{ "name": "sfi.call_graph", "args": { … } }` with
> `direction: 'downstream', rootId: 'ApexClass:OwnerAssignmentService'`.
>
> Boundaries — quote the response's `disclosure` string VERBATIM
> (it is one paragraph, not a list). It states that deactivation does
> NOT delete the Flow, that `input-only` rows are dependencies the
> Flow consumes rather than downstream effects, that `entryPoints`
> names where the Flow starts, that an ACTIVE parent Flow's subflow
> call forces `blocking`, and that Apex invoking the Flow via
> `Flow.Interview` / `@InvocableMethod` chains and non-metadata launch
> points (quick actions, buttons, screen-flow entry) remain invisible.
The response leads with the `blocking` verdict, organizes by
category, cites canonical IDs + edge confidence, surfaces the
firing conditions (the actual gating logic the deactivation
silences), suggests concrete next steps, and appends the
verbatim v2.3 disclosure.
## Anti-patterns
| Mistake | Why it's wrong |
|---|---|
| Subsetting `sfi.test_coverage_for_method` results by the user-supplied `methodName`. | The class-granularity boundary is verbatim: v2.7 does NOT subset by method. The `methodName` is echoed for caller-pipelining only. Surface every covering test class for the target CLASS, with the verbatim disclosure. |
| Treating a `likely-dead-code` verdict from `sfi.method_reachability` as "this class is safe to delete." | Dynamic dispatch, reflective invocation, framework wiring, and managed-package callers are invisible. The verdict means "no usage in-edge and no entry-point classifier within depth-3"; verify with `sfi.find_dead_code` (adds a whole-word source re-check that can downgrade the class to `uncertain`) and `sfi.find_code_usages` before deletion. |
| Surfacing a what-if finding without its `confidence`. | The confidence is the developer's verification axis. A `heuristic` finding is a candidate for false positive; a `declared` finding is metadata-sourced. State both. |
| Subsetting `sfi.what_if_change_field_type` results to only `metadata-blocker` (hiding `configuration-only`). | The configuration-only findings (layouts, perm-set assignments) are the developer's "fix this before deploy" surface even though they won't break the deploy. Surface them; group by category, don't drop. |
| Calling `sfi.what_if_remove_picklist_value` against a vault missing v2.0a extraction. | The composer surfaces an error in this case (Flow decisions keyed on the value would be invisible without `firesWhen` edges + `ConditionalContext` nodes). Recover with the v2.0a-not-extracted message; suggest `/sfi-refresh`. |
| Calling `sfi.call_graph` with `maxDepth: 10`. | `maxDepth` is bounded at **5**, so 10 is a rejected `invalid-query`, not a deep walk. The class-granularity boundary collapses methods together anyway; long chains rarely surface useful structure. Default `maxDepth: 3`; raise it to at most 5, and only when the user asks for an exhaustive walk. |
| Treating `cycleDetected: true` in `sfi.call_graph` as "the architecture is broken." | A queueable class enqueueing itself is the textbook chunking pattern, NOT a bug. Mention the cycle but distinguish self-enqueue from genuine call-graph cycles. |
| Skipping the dynamic-dispatch boundary on `sfi.test_coverage_for_method` results. | A class genuinely tested via `Type.forName(...)` will surface with an empty `coveringTestClasses[]`. State the boundary even when results are non-empty; the user may have OTHER tests that aren't surfacing. |
| Recommending a profile split with `sfi.what_if_split_profile` as the deployable answer. | The split tool's greedy heuristic produces a STARTING POINT, not the optimal partition. Surface the per-assignment `reason` so the developer reviews each grant before committing. |
| Treating `meaningful_test_audit`'s `fake-assertion` ranking as "these tests are bad". | Custom assertion helpers and framework wrappers are invisible. A class with all assertions via a `MyTestHelper.expectField(record, ...)` helper ranks at the top of the suspicious list but may be genuinely thorough. Verify by reading the test source. |
## See also
- `developer-apex-refactor` — for code-reference questions ("where
is `OpportunityService` used in source"). The Apex tier is the
default-on parser-grade AST (`parsed`) plus a heuristic recall
scanner; the frontend LWC/Aura/VF tier stays `heuristic`. What-if
tools READ those graph edges as their composition input.
- `developer-code-quality` — for quality / hygiene questions over
the same `qualityIssues[]` mirror. `sfi.meaningful_test_audit`
here and `sfi.test_coverage_gaps` there share the same
`fake-assertion` rule.
- `architect-impact-analysis` — for cross-component impact ("what
depends on this field"). v0.2 `sfi.get_impact` walks every edge
type; v2.3 composers narrow to a specific change kind.
- `architect-async-and-events` — for `sfi.async_chain_depth`
(transitive `dispatchesAsync` walk). Adjacent to
`sfi.call_graph` but specialized to async dispatch.
- `developer-field-deep-dive` — for v3.0 field synthesis
(`sfi.field_360`, `sfi.field_lineage`) which COMPOSES this
skill's per-method walks.
## Verification
Before sending a response, confirm:
- [ ] I classified the question into one of the three categories
(what-if / reachability / test-quality) and fired the right
tool.
- [ ] For reachability tools (`call_graph`, `downstream_effects`,
`test_coverage_for_method`, `method_reachability`,
`meaningful_test_audit`), I surfaced the class-granularity
boundary verbatim and stated the dynamic-dispatch invisibility.
- [ ] For `test_coverage_for_method`, I did NOT subset by the
user-supplied `methodName`; I echoed the methodName for
caller-pipelining and reported coverage at class
granularity.
- [ ] For `method_reachability`, I cited the verdict
(`entry-point-reachable` / `test-only-reachable` /
`likely-dead-code`) and stated which entry points reached the
target (or that NO entry point reaches if the verdict is
`likely-dead-code`).
- [ ] For what-if tools, I grouped findings by `category`
(`metadata-blocker` / `code-needs-update` /
`integration-touch` / `test-class-update` /
`invisible-risk` / `configuration-only`) and cited per-
finding `confidence`.
- [ ] For `what_if_make_field_required`, I surfaced the
Apex-create-coverage invisibility verbatim.
- [ ] For `what_if_change_method_signature`, I surfaced the Q105
dynamic-dispatch + test-class-naming-convention disclosure.
- [ ] For `what_if_remove_picklist_value`, I confirmed v2.0a
extraction ran (the composer errors otherwise).
- [ ] For `meaningful_test_audit`, I cited the custom-assertion-
helper invisibility verbatim.
- [ ] I did NOT recommend a destructive change (delete the class,
remove the picklist value, change the field type) without
naming the verification step.
- [ ] I cited every canonical id in backticks.
---
**Grounding & routing (shared contract).** For a vague or broad ask, call `sfi.route_question` first — in the default hybrid mode it returns a meaning-ranked `toolCandidates` shortlist (which YOU pick from) plus a suggested plane and a `route` hint (and whether to `sfi.resolve` a name first). **Default tool profile is `core`:** only the core spine (including `sfi.live_consent`) is directly invokable. For every other `sfi.*` analysis, call `sfi.run_analysis` with `{ "name": "sfi.<tool>", "args": { … } }` (or follow `route_question.invoke`, which already wraps non-core steps). Optional: `sfi.describe_analysis` first when args are unclear. Every org fact must come from an `sfi.*` tool call, cited by its canonical id — never from memory. Build the answer only from what the tools returned, then pass it through `sfi.synthesize_answer`, which flags any `hallucinatedIds` (canonical ids no tool produced). Full cascade: `using-sf-intelligence`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!