Problem
BaseExpressionAPI.__getattr__ (line 91 of api_base.py) uses hasattr(ns_cls, name) to check if a builder handles an attribute. Because builders inherit from their Protocol classes, protocol method stubs are visible to hasattr even if the concrete builder doesn't override them.
for ns_cls in self._FLAT_NAMESPACES:
ns = ns_cls(self)
if hasattr(ns_cls, name): # ← matches inherited Protocol stubs too
return getattr(ns, name)
If a builder inherits a protocol but forgets to implement one of its methods, the dispatch would silently bind the protocol stub (returning None or ...) instead of raising AttributeError.
Why it hasn't bitten yet
The test_protocol_alignment.py tests enforce that every protocol method has a concrete implementation on the composed ExpressionSystem. But no test validates the dispatch mechanism itself — specifically that a dispatched call hits a real implementation rather than an inherited stub.
Fix
Change dispatch to check name in ns_cls.__dict__ instead of hasattr(ns_cls, name). This only routes to methods defined directly on the concrete builder class.
Secondary concern: dir() coverage
dir(ExpressionAPI_instance) reflects __getattr__-dispatched methods only if __dir__ is overridden. Currently no __dir__ exists. This isn't a bug (no test relies on dir() for expressions), but becomes relevant when relations adopt the same pattern.
Discovered during
Codex adversarial review of the Relation API builder decomposition spec (2026-05-06).
Problem
BaseExpressionAPI.__getattr__(line 91 ofapi_base.py) useshasattr(ns_cls, name)to check if a builder handles an attribute. Because builders inherit from their Protocol classes, protocol method stubs are visible tohasattreven if the concrete builder doesn't override them.If a builder inherits a protocol but forgets to implement one of its methods, the dispatch would silently bind the protocol stub (returning
Noneor...) instead of raisingAttributeError.Why it hasn't bitten yet
The
test_protocol_alignment.pytests enforce that every protocol method has a concrete implementation on the composed ExpressionSystem. But no test validates the dispatch mechanism itself — specifically that a dispatched call hits a real implementation rather than an inherited stub.Fix
Change dispatch to check
name in ns_cls.__dict__instead ofhasattr(ns_cls, name). This only routes to methods defined directly on the concrete builder class.Secondary concern:
dir()coveragedir(ExpressionAPI_instance)reflects__getattr__-dispatched methods only if__dir__is overridden. Currently no__dir__exists. This isn't a bug (no test relies ondir()for expressions), but becomes relevant when relations adopt the same pattern.Discovered during
Codex adversarial review of the Relation API builder decomposition spec (2026-05-06).