Filed from LawnStarter adoption ticket PE-80043 (api.lawnstarter.com), graded run at CLI 0.9.40, fork v8 @ 5e9d95c11837aeabbbb5d90cb45544f49dc58ca4.
Summary
graphify affected on an interface method never reaches the implementing method, at any depth. The graph records implements at the class level only; there is no method-level interface → implementation edge. So the single most important answer to "what breaks if I change this interface method?" — the class that implements it — is silently absent.
This is the "silent incompleteness" failure mode: no edge, no warning. The blast radius looks small and correct.
Reproduction
Corpus: api.lawnstarter.com @ 382fe1a2c1 (private LawnStarter repo, PHP/Laravel, 6,224 files).
Minimal shape:
// app/Repositories/ScheduleEvent/ScheduleEventRepository.php:138
interface ScheduleEventRepository
{
public function bulkUpdateDistributionId(Collection $scheduleEvents, int $distributionId): void;
}
// app/Repositories/ScheduleEvent/DbScheduleEventRepository.php:71
class DbScheduleEventRepository extends DbRepository implements ScheduleEventRepository
{
// :2106
public function bulkUpdateDistributionId(Collection $scheduleEvents, int $distributionId): void { /* ... */ }
}
graphify update .
graphify affected "app_repositories_scheduleevent_scheduleeventrepository_scheduleeventrepository_bulkupdatedistributionid" --depth 2
Expected
Everything that breaks if that signature changes:
app/Repositories/ScheduleEvent/DbScheduleEventRepository.php:2106 — the implementing method
app/Actions/Distributions/BaseDistributionCreator.php:73 — the only production caller
app/Actions/Distributions/BaseDistributionCreator.php:47 — transitive, handle() calls bindEventsToDistribution()
Ground truth by grep, all 8 hits adjudicated. The 5 hits in BaseDistributionCreatorTest.php are Mockery shouldReceive('bulkUpdateDistributionId') string literals — correctly excluded as string mentions, and correctly absent from the output.
Actual
Affected nodes for .bulkUpdateDistributionId()
Relations: calls, indirect_call, references, imports, imports_from, re_exports, inherits, extends, implements, uses, mixes_in, embeds, requires
Depth: 2
- .bindEventsToDistribution() [calls] app/Actions/Distributions/BaseDistributionCreator.php:L73
- .handle() [calls] app/Actions/Distributions/BaseDistributionCreator.php:L47
2 of 3. The implementation is missing. --depth 3 returns byte-identical output, so raising depth does not help.
Note that implements is already in the traversed relation list — it just has nothing to traverse at method granularity.
Root cause
Inspecting graph.json directly:
=== iface method inbound ===
calls | .bindEventsToDistribution() | app/Actions/Distributions/BaseDistributionCreator.php L73
method | ScheduleEventRepository | app/Repositories/ScheduleEvent/ScheduleEventRepository.php L138
=== impl method inbound ===
method | DbScheduleEventRepository | app/Repositories/ScheduleEvent/DbScheduleEventRepository.php L2106
=== impl class -> iface class edges ===
implements
The class-level implements edge exists. The two method nodes are connected to nothing but their own owning classes.
Suggestion
When class C implements I, emit an edge between C::m and I::m for each method name declared on I and defined on C — the match is purely by name within an already-established implements pair, so it needs no type inference. The same applies to abstract methods under inherits.
Failing that, having affected walk method -> owning class -> implements -> implementing class -> method of same name would recover it at traversal time.
Filed from LawnStarter adoption ticket PE-80043 (api.lawnstarter.com), graded run at CLI
0.9.40, forkv8@5e9d95c11837aeabbbb5d90cb45544f49dc58ca4.Summary
graphify affectedon an interface method never reaches the implementing method, at any depth. The graph recordsimplementsat the class level only; there is no method-level interface → implementation edge. So the single most important answer to "what breaks if I change this interface method?" — the class that implements it — is silently absent.This is the "silent incompleteness" failure mode: no edge, no warning. The blast radius looks small and correct.
Reproduction
Corpus:
api.lawnstarter.com@382fe1a2c1(private LawnStarter repo, PHP/Laravel, 6,224 files).Minimal shape:
Expected
Everything that breaks if that signature changes:
app/Repositories/ScheduleEvent/DbScheduleEventRepository.php:2106— the implementing methodapp/Actions/Distributions/BaseDistributionCreator.php:73— the only production callerapp/Actions/Distributions/BaseDistributionCreator.php:47— transitive,handle()callsbindEventsToDistribution()Ground truth by grep, all 8 hits adjudicated. The 5 hits in
BaseDistributionCreatorTest.phpare MockeryshouldReceive('bulkUpdateDistributionId')string literals — correctly excluded as string mentions, and correctly absent from the output.Actual
2 of 3. The implementation is missing.
--depth 3returns byte-identical output, so raising depth does not help.Note that
implementsis already in the traversed relation list — it just has nothing to traverse at method granularity.Root cause
Inspecting
graph.jsondirectly:The class-level
implementsedge exists. The two method nodes are connected to nothing but their own owning classes.Suggestion
When class
C implements I, emit an edge betweenC::mandI::mfor each method name declared onIand defined onC— the match is purely by name within an already-establishedimplementspair, so it needs no type inference. The same applies toabstractmethods underinherits.Failing that, having
affectedwalkmethod -> owning class -> implements -> implementing class -> method of same namewould recover it at traversal time.