You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #134: a role-inference catalog — detect what a class is from shape + usage, not just its base class
#134 proposes flagging a class doing work out of its role. That needs a way to infer the role even
when there's no marker (no *Registry name, no base class, no attribute). The base class is the sure
signal — but most classes don't announce themselves, and the interesting bugs hide in the unmarked ones.
The idea: infer role from three stacked evidence tiers, and treat agreement across tiers as confidence.
The confidence model
Tier A — declared (certain): base class, implemented interface, attribute, name suffix.
Tier B — structural fingerprint (probable, AST-local): property shapes + method shapes. Example
(yours): one private array + public add/get whose only writers funnel through a private writer, methods
are array ops → this is a store/bag, no marker needed.
Tier C — usage shape (confirming, needs the call graph / NeedsCodebaseIndex): fan-in (referenced in
N files), read/write ratio, who writes it (only a provider/boot path vs. runtime), and mutation
provenance (below).
A role fires when Tier A, or Tier B + Tier C agree. Two independent tiers ⇒ high confidence, low FP.
Mutation provenance (your core idea, generalised)
Trace where a class's own state is written — it's the single most discriminating signal:
Provenance
Implies
never written after construction
immutable — Value Object / DTO
written only in the constructor
immutable
written by a public setter directly
mutable bag / config
written only via a public method → private writer
encapsulated mutable store (registry / cache / aggregator)
a public array assigned by other classes
leaky / anemic — flag
$this->x[$k] ??= compute() only
memo / cache
Combine with read/write ratio + fan-in: encapsulated store + populated once (boot) + read across many
files ⇒ registry; encapsulated store + written every call ⇒ accumulator; etc.
Pipe / Step / Handler — one real public method (handle($ctx)/__invoke), rest private helpers.
Resolver / Strategy selector — resolve($x) iterating candidates / predicates; first-match wins;
no store. (Flag inverse: a "Resolver" that owns a register() store → it's a registry.)
Validator — validate/assert*/check*; returns violations or throws; no state.
Visitor — many visit*/enter*/leave* methods dispatching on node type.
Middleware — handle($x, $next) with a $next continuation it calls.
State machine — closed state set + transitionTo/guards; mutates a single state prop under rules.
Decorator — ctor takes the same interface it implements; most methods delegate to $this->inner->m(),
a few add behaviour. (Detectable: implements I + holds I + delegating method bodies.)
Adapter — implements interface X by wrapping an unrelated type Y and translating.
Facade / static proxy — all-static methods delegating to a container-resolved instance.
Manual-enum → real enum (archetype 6), Value-Object-should-be-readonly (4), mutable shared bag
(2 + leaky provenance), Null-Object opportunity (an interface with one impl everyone null-checks), Decorator-forgot-a-method (20: implements I but doesn't delegate every I method), Singleton →
inject it (23) — each is a targeted advisory once the role is inferred.
Tier B alone is suggestive, never conclusive — require Tier A, or B+C agreement to fire; size/length
is only ever a tie-breaker (mirror the FP discipline that keeps RegistryReturnContract marker-driven).
Ship a default archetype catalog; let consumers add/disable archetypes and tune thresholds in config.
All advisory, none auto-fixable (role re-shaping is a design call).
Open questions
Express each archetype as a composable predicate set (property-shape + method-shape + usage-shape) over a
shared ClassFingerprint the call-graph index builds once — agree that's the right substrate?
Confidence scoring: boolean (A | B&C) vs. a weighted score with a configurable threshold?
Proving ground — mine two real codebases, don't theorise the catalog
The ~26 archetypes above are a starting hypothesis, not the spec. The team should treat two real,
independent consumer codebases — workflows and smart-farmers — as both the discovery corpus
and the proof-of-concept test suite:
Mine them for patterns. Scan each codebase, build a ClassFingerprint per class, and cluster —
let the archetypes that actually recur drive which inferers to write. Bottom-up from real code, not
top-down from this list; the codebases are the ground truth and will surface idioms (and counter-examples)
the catalog missed.
Write a comprehensive inferer suite from what the corpora reveal.
Use the two codebases as the labeled regression suite. Hand-label a sample of classes per archetype
in each, then assert each inferer (a) classifies the positives correctly and (b) does not misclassify
the negatives (precision matters more than recall here). Report precision/recall per inferer per codebase.
Two independent codebases is the point: an inferer that nails workflows but misfires on smart-farmers is overfit to one project's idioms — exactly the failure mode to catch before shipping. workflows already has clean known instances of registry / bag / value-object / manual-enum / DTO-with-logic
(see #119, #134); smart-farmers is the independent cross-check.
I'm happy to prototype the ClassFingerprint + the first 3–4 inferers (registry, bag, value-object,
manual-enum) against the workflows snapshot I already have, as the first half of that proving ground.
Follow-up to #134: a role-inference catalog — detect what a class is from shape + usage, not just its base class
#134 proposes flagging a class doing work out of its role. That needs a way to infer the role even
when there's no marker (no
*Registryname, no base class, no attribute). The base class is the suresignal — but most classes don't announce themselves, and the interesting bugs hide in the unmarked ones.
The idea: infer role from three stacked evidence tiers, and treat agreement across tiers as confidence.
The confidence model
(yours): one private array + public
add/getwhose only writers funnel through a private writer, methodsare array ops → this is a store/bag, no marker needed.
NeedsCodebaseIndex): fan-in (referenced inN files), read/write ratio, who writes it (only a provider/boot path vs. runtime), and mutation
provenance (below).
A role fires when Tier A, or Tier B + Tier C agree. Two independent tiers ⇒ high confidence, low FP.
Mutation provenance (your core idea, generalised)
Trace where a class's own state is written — it's the single most discriminating signal:
public arrayassigned by other classes$this->x[$k] ??= compute()onlyCombine with read/write ratio + fan-in: encapsulated store + populated once (boot) + read across many
files ⇒ registry; encapsulated store + written every call ⇒ accumulator; etc.
The catalog (~26 archetypes + fingerprints)
Storage / data
register/add/put(key,item)→ private writer;get/has/find/alllookups;write-once (boot) / read-many fan-in. (Feeds RegistryNamingHonesty/ReturnContract/BaseBypass.)
add/all/map/filter/count/first);often
IteratorAggregate/Countable/ArrayAccess.[$k] ??= …then read; no external writers.from*/make), equality (equals/CompareSelf),pure methods, no injected deps, no I/O, never mutates.
from/toArray/serialization; SHOULD have no logic → flag if ithas loops /
ReflectionClass/ injected services (this is theTestRunOutcomeDatasmell from Prophet idea: OutOfPurposeProphet — flag a class doing work out of its role (role-vs-behaviour incoherence), generalising RegistryNamingHonesty #134).public staticself-returning consts/factories →should be a real
enum.config(...)/env; no domain behaviour.$this,[], or empty body); nameNull*/No*/Empty*.Construction
make/create/build/from*returningnew X(a family); no state, no store.return $this/static; accumulate; terminalbuild()/get().to*/from*/map; name*Mapper/*Transformer.Behaviour
handle/run/process)coordinating them; no array store. (Watch: too many injected deps ⇒ god-service, ties to Prophet idea: OutOfPurposeProphet — flag a class doing work out of its role (role-vs-behaviour incoherence), generalising RegistryNamingHonesty #134.)
handle($ctx)/__invoke), rest private helpers.resolve($x)iterating candidates / predicates; first-match wins;no store. (Flag inverse: a "Resolver" that owns a
register()store → it's a registry.)validate/assert*/check*; returns violations or throws; no state.visit*/enter*/leave*methods dispatching on node type.handle($x, $next)with a$nextcontinuation it calls.transitionTo/guards; mutates a singlestateprop under rules.isSatisfiedBy($x): bool/matches()/ builds a query; composable.Structural / wrapping
$this->inner->m(),a few add behaviour. (Detectable: implements I + holds I + delegating method bodies.)
private static self $instance+getInstance()+ private ctor.Framework roles (sure via base, but pattern-detectable too)
find/save/delete/allover one entity.(
handle(Event); staticfor*factories on a Throwable;bind()calls in a provider; etc.).What this unlocks (prophets it feeds)
*Registrythatfingerprints as a reflection engine," "a
*Datathat fingerprints as an assembler."(2 + leaky provenance), Null-Object opportunity (an interface with one impl everyone null-checks),
Decorator-forgot-a-method (20: implements I but doesn't delegate every I method), Singleton →
inject it (23) — each is a targeted advisory once the role is inferred.
registry (B+C) can be nudged to adopt the base — closing the exact gap that let
ResourceRegistry(v2 coverage audit: false positive on first(callable): Option, and no detector for a subclass that extends Registry but bypasses the base store #119) slip through.
Feasibility / FP discipline
is only ever a tie-breaker (mirror the FP discipline that keeps
RegistryReturnContractmarker-driven).Open questions
shared
ClassFingerprintthe call-graph index builds once — agree that's the right substrate?Proving ground — mine two real codebases, don't theorise the catalog
The ~26 archetypes above are a starting hypothesis, not the spec. The team should treat two real,
independent consumer codebases —
workflowsandsmart-farmers— as both the discovery corpusand the proof-of-concept test suite:
ClassFingerprintper class, and cluster —let the archetypes that actually recur drive which inferers to write. Bottom-up from real code, not
top-down from this list; the codebases are the ground truth and will surface idioms (and counter-examples)
the catalog missed.
in each, then assert each inferer (a) classifies the positives correctly and (b) does not misclassify
the negatives (precision matters more than recall here). Report precision/recall per inferer per codebase.
Two independent codebases is the point: an inferer that nails
workflowsbut misfires onsmart-farmersis overfit to one project's idioms — exactly the failure mode to catch before shipping.workflowsalready has clean known instances of registry / bag / value-object / manual-enum / DTO-with-logic(see #119, #134);
smart-farmersis the independent cross-check.I'm happy to prototype the
ClassFingerprint+ the first 3–4 inferers (registry, bag, value-object,manual-enum) against the
workflowssnapshot I already have, as the first half of that proving ground.