Coverage audit of the v2 registry / invariant prophets against a real 885-file codebase
I ran the new root-cause/registry/invariant family (v2, feat/root-cause-precedence @ adfbd09, all 7
phases) over a snapshot of a real app (the jessegall/workflows package, 885 PHP files) plus minimal
isolated repros, to check the thesis "the prophets flag what opposes the invariant/registry architecture."
Overall: the family is in great shape — precedence/deferral works (the invariant root cause leads its
deferred symptom), and the exemptions are genuinely well-considered (finder prefixes, <thing>For<Other>
directional lookups, the own-?T-contract tail). Most of what looked like misses on first glance turned
out to be correct, intentional exemptions. Credit where due; this is a tight ruleset.
Two real issues remain — one false positive and one coverage gap — plus two minor notes.
1. False positive — RegistryReturnContractProphet flags first(callable): Option (a predicate scan) — SIN / Structural / commit-blocking
A registry base with a predicate finder:
abstract class Registry // marked: extends/named Registry
{
/** @return Option<TType> */
public function first(callable $predicate): Option
{
return Option::first($this->all(), $predicate);
}
}
is flagged as a registry-contract sin ("return T or throw, with a has() companion"). But first(callable $predicate) is not a key lookup — it's a predicate scan, inherently value-or-nothing, exactly like
search*. Returning Option is the correct design; throwing would be wrong. The allowlist
(FINDER_PREFIXES = ['find','search','try','lookup'] + *OrNull/*OrDefault/<thing>For<Other>) simply
doesn't anticipate first / a callable-predicate getter.
- Real evidence:
Support/Registry.php:87 (public function first(callable $predicate): Option) — a
blocking SIN on the canonical, correct base registry.
- Note: the package's own scaffolded
Registry.stub happens to avoid this only because it ships
without a first(); any registry base that adds first/firstWhere/a callable-predicate finder hits it.
Suggested fix: treat a method that takes a callable/predicate and returns Option as a scan (exempt),
and/or add first to the finder allowlist.
2. Coverage gap — a subclass that extends Registry but defeats the base store is flagged by nothing — architecture opposition, no detector
This is the case that started the audit. ResourceRegistry extends Registry<AssistantResource> but:
class ResourceRegistry extends Registry
{
private array|null $resources = null;
// overrides all() to read a PRIVATE store and never touches the base $items
public function all(): array { return $this->resources ??= $this->build(); }
// ...
}
Because all() is overridden to read $this->resources and the base $items is bypassed, the inherited
register() / registerMany() are dead — calling them writes to $items, which all()/find()/get()
never read. It "is a Registry" by inheritance but cannot be used as one via registration; it's really a
discovered catalog wearing the base. That directly opposes the registry architecture (the base's whole
mechanism is silently neutered), yet 0 prophets flag it (verified across RegistryReturnContract,
RegistryNamingHonesty, RegistryPattern, NoOptionToNull, PreferOptionOverNull).
Minimal repro (judge it — only nameFor gets flagged, for an unrelated reason; the override/bypass is invisible):
final class ThingRegistry extends Registry // App\Support\Registry (scaffolded)
{
private array $things = [];
public function all(): array { return $this->things; } // ← bypasses base $items; inherited register() is now dead
public function keyForName(string $name): Option { /* ... */ }
}
Suggested detector: flag a subclass of a Registry (or any base) that overrides a method the base
implements in terms of a protected store and stops using that store, leaving inherited mutators
(register*) unreachable — i.e. "inherited base mechanism defeated / dead inherited mutator." (Generalizes
beyond registries.)
3. Discussion (not asserting a bug) — the exemption seam lets a built Option be collapsed to null
ResourceRegistry::findModelClass():
public function findModelClass(string $type): string | null
{
return $this->find($type)->transform(fn ($r) => $r->modelClass())->getOr(null);
}
builds a real Option (via find()) and collapses it with getOr(null). This is exempt by every
relevant prophet, each defensibly:
NoOptionToNullProphet — exempts the return …->getOr(null) tail as "the method's own ?T contract"
(its own WHAT DOES NOT list, ~line 109);
RegistryReturnContract / PreferOptionOverNull — exempt find* as a finder.
So a registry method that constructs an Option and immediately throws it away slips through. Per the strict
architecture ("don't unwrap a real Option back to null — map it or throw"), this opposes the model; per the
current exemptions it's intentional. Worth a decision: should the own-contract exemption still apply when
the collapsed value came from a constructed Option (a find()/transform() chain), vs. a bare nullable
passthrough? Flagging here as a judgment call, not a defect.
4. Minor UX — single-file judging under-reports (index-dependent prophets go silent)
judge --file=ResourceRegistry.php → "Righteous: No sins found." The same file inside a full-tree
judge surfaces findings, because the NeedsCodebaseIndex prophets (e.g. NoOptionToNull resolving that
find() returns Option) have no call graph to consult on a lone file. Devs (and the pre-commit
--staged/--git paths) judging a small file set can get false comfort. Worth either building a minimal
index for the file's referenced types, or a one-line "N index-dependent prophets limited in single-file
mode" notice.
Repro
# point a scroll at the target tree with the registry/invariant prophets enabled, then:
vendor/bin/commandments judge --no-cache
# (1) -> SIN: RegistryReturnContractProphet on a base with first(callable): Option
# (2) -> ThingRegistry / ResourceRegistry override+bypass: no finding
Happy to provide the full minimal repro project (scaffolded Registry/Option + the two repro classes) or
a PR for the first/predicate-scan allowlist fix.
Coverage audit of the v2 registry / invariant prophets against a real 885-file codebase
I ran the new root-cause/registry/invariant family (v2,
feat/root-cause-precedence@adfbd09, all 7phases) over a snapshot of a real app (the
jessegall/workflowspackage, 885 PHP files) plus minimalisolated repros, to check the thesis "the prophets flag what opposes the invariant/registry architecture."
Overall: the family is in great shape — precedence/deferral works (the invariant root cause leads its
deferred symptom), and the exemptions are genuinely well-considered (finder prefixes,
<thing>For<Other>directional lookups, the own-
?T-contract tail). Most of what looked like misses on first glance turnedout to be correct, intentional exemptions. Credit where due; this is a tight ruleset.
Two real issues remain — one false positive and one coverage gap — plus two minor notes.
1. False positive —
RegistryReturnContractProphetflagsfirst(callable): Option(a predicate scan) — SIN / Structural / commit-blockingA registry base with a predicate finder:
is flagged as a registry-contract sin ("return T or throw, with a
has()companion"). Butfirst(callable $predicate)is not a key lookup — it's a predicate scan, inherently value-or-nothing, exactly likesearch*. ReturningOptionis the correct design; throwing would be wrong. The allowlist(
FINDER_PREFIXES = ['find','search','try','lookup']+*OrNull/*OrDefault/<thing>For<Other>) simplydoesn't anticipate
first/ acallable-predicate getter.Support/Registry.php:87(public function first(callable $predicate): Option) — ablocking SIN on the canonical, correct base registry.
Registry.stubhappens to avoid this only because it shipswithout a
first(); any registry base that addsfirst/firstWhere/a callable-predicate finder hits it.Suggested fix: treat a method that takes a
callable/predicate and returnsOptionas a scan (exempt),and/or add
firstto the finder allowlist.2. Coverage gap — a subclass that
extends Registrybut defeats the base store is flagged by nothing — architecture opposition, no detectorThis is the case that started the audit.
ResourceRegistry extends Registry<AssistantResource>but:Because
all()is overridden to read$this->resourcesand the base$itemsis bypassed, the inheritedregister()/registerMany()are dead — calling them writes to$items, whichall()/find()/get()never read. It "is a Registry" by inheritance but cannot be used as one via registration; it's really a
discovered catalog wearing the base. That directly opposes the registry architecture (the base's whole
mechanism is silently neutered), yet 0 prophets flag it (verified across
RegistryReturnContract,RegistryNamingHonesty,RegistryPattern,NoOptionToNull,PreferOptionOverNull).Minimal repro (judge it — only
nameForgets flagged, for an unrelated reason; the override/bypass is invisible):Suggested detector: flag a subclass of a
Registry(or any base) that overrides a method the baseimplements in terms of a protected store and stops using that store, leaving inherited mutators
(
register*) unreachable — i.e. "inherited base mechanism defeated / dead inherited mutator." (Generalizesbeyond registries.)
3. Discussion (not asserting a bug) — the exemption seam lets a built Option be collapsed to null
ResourceRegistry::findModelClass():builds a real
Option(viafind()) and collapses it withgetOr(null). This is exempt by everyrelevant prophet, each defensibly:
NoOptionToNullProphet— exempts thereturn …->getOr(null)tail as "the method's own?Tcontract"(its own
WHAT DOES NOTlist, ~line 109);RegistryReturnContract/PreferOptionOverNull— exemptfind*as a finder.So a registry method that constructs an
Optionand immediately throws it away slips through. Per the strictarchitecture ("don't unwrap a real Option back to null — map it or throw"), this opposes the model; per the
current exemptions it's intentional. Worth a decision: should the own-contract exemption still apply when
the collapsed value came from a constructed
Option(afind()/transform()chain), vs. a bare nullablepassthrough? Flagging here as a judgment call, not a defect.
4. Minor UX — single-file judging under-reports (index-dependent prophets go silent)
judge --file=ResourceRegistry.php→ "Righteous: No sins found." The same file inside a full-treejudgesurfaces findings, because theNeedsCodebaseIndexprophets (e.g.NoOptionToNullresolving thatfind()returnsOption) have no call graph to consult on a lone file. Devs (and the pre-commit--staged/--gitpaths) judging a small file set can get false comfort. Worth either building a minimalindex for the file's referenced types, or a one-line "N index-dependent prophets limited in single-file
mode" notice.
Repro
Happy to provide the full minimal repro project (scaffolded
Registry/Option+ the two repro classes) ora PR for the
first/predicate-scan allowlist fix.