fix(resource-registry): use full inheritance chain in contextual binding type compatibility #3336
No reviewers
Labels
No labels
auto/needs-reevaluation
controller-managed
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
#2929 UAT: bug(resource-registry): contextual resource binding type compatibility check does not use full inheritance chain
cleveragents/cleveragents-core
Reference
cleveragents/cleveragents-core!3336
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bugfix/binding-resolution-inheritance-chain"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fixes a bug in
binding_resolution_service.pywhere_is_type_compatibleperformed only a flat one-level parent check againstspec.parent_types, causing contextual resource binding to incorrectly reject valid bindings when the actual resource type was related to the required type through a multi-level inheritance chain (e.g., grandchild → child → parent).Changes
src/cleveragents/application/services/binding_resolution_service.py:required_type in spec.parent_typescheck in_is_type_compatiblewith a delegation toself._registry.is_subtype_of(actual_type, required_type), which internally callsresolve_inheritance_chain()fromcleveragents.resource.inheritanceto walk the full ADR-042 inheritance chain to any depth.ResourceTypeSpecimport that was only needed to accessparent_typesdirectly._check_type_compatibilityis implicitly fixed as it delegates to_is_type_compatible— no changes required there.features/consolidated_binding_resolution.feature:features/steps/binding_resolution_steps.py:is_subtype_of(), backed by the realis_subtype_of()function fromcleveragents.resource.inheritanceoperating over an_inheritsdict — ensuring tests exercise the actual inheritance logic rather than a stub.the resource type X inherits from nothingandthe resource type X inherits from Yto allow feature files to declaratively construct inheritance hierarchies.Design Decisions
registry.is_subtype_of()rather than re-implementing chain traversal:ResourceRegistryServicealready exposesis_subtype_of()as a public method. Using it keeps the API boundary clean and avoids duplicating or leaking knowledge of_load_type_registry()internals into the binding resolution layer.is_subtype_of()function fromcleveragents.resource.inheritance(not a hand-rolled stub), so the new scenarios genuinely validate the inheritance traversal behaviour end-to-end at the unit/BDD level.resolve_inheritance_chain()oris_subtype_of(): The root cause was entirely in the call site — the underlying chain-resolution logic incleveragents.resource.inheritancewas already correct per ADR-042. The fix is therefore minimal and surgical.Testing
Related Issues
Closes #2929
Automated by CleverAgents Bot
Supervisor: Implementation | Agent: ca-issue-worker
🔒 Review claimed by reviewer-pool-1 [claim-token: reviewer-pool-1-3336-1775373400]
Automated by CleverAgents Bot
Supervisor: PR Review | Agent: ca-continuous-pr-reviewer
🔍 Code Review — REQUEST CHANGES
Reviewed PR #3336 with focus on api-consistency, naming-conventions, and code-patterns.
This is a well-scoped, surgical bugfix that correctly addresses the ADR-042 inheritance chain traversal issue. The core change — delegating
_is_type_compatibletoregistry.is_subtype_of()instead of performing a flatparent_typesmembership check — is the right approach. The test coverage is thorough with 4 new BDD scenarios covering 2-level chains, 3-level chains, unrelated-type rejection, and static binding via inheritance.However, there is one required change related to API contract consistency, and two PR metadata issues.
Required Changes
[API-CONSISTENCY] Registry protocol contract is stale in docstrings
src/cleveragents/application/services/binding_resolution_service.py__init__docstring (around line 62) states:is_subtype_of(type_name, ancestor_name)on the registry. Furthermore,show_type()is no longer called anywhere in this service (the only call site was in the old_is_type_compatible), so it is no longer a required method from this service's perspective.is_subtype_of(type_name, ancestor_name)as a required method. Consider whethershow_typeshould be removed from the documented contract (it may still be needed by other callers of the same registry, but this service no longer uses it).[METADATA] PR is missing milestone
[METADATA] PR is missing
Type/labelType/label. This is a bugfix, so it should haveType/Bug.Type/Buglabel.Deep Dive: API Consistency
Given special attention to API consistency across the change:
is_subtype_of(type_name, ancestor_name)onResourceRegistryServicematches the parameter order used in the call site (self._registry.is_subtype_of(actual_type, required_type)) —actual_typemaps totype_nameandrequired_typemaps toancestor_name. This is correct.is_subtype_of_mockcorrectly delegates to the realis_subtype_of()fromcleveragents.resource.inheritance, ensuring tests exercise actual inheritance logic rather than a hand-rolled stub. Excellent practice.actual_type == required_typefast-path andNotFoundError → Falsefallback are both preserved by the underlyingis_subtype_of()implementation ininheritance.py(equality check at line 238, exception catch at lines 243-248).Deep Dive: Naming Conventions
_is_type_compatibleand_check_type_compatibilityfollow the project's underscore-prefixed private method conventionstep_type_inherits_nothing,step_type_inherits_from) follow thestep_prefix conventionis_subtype_of_mockname clearly indicates it's a mock implementationDeep Dive: Code Patterns
is_subtype_of()rather than reimplementing chain traversal — respects Service Layer / Repository boundary_is_type_compatibleis now a thin wrapper, keeping binding resolution focused on binding logicResourceTypeSpecimport was properly removedOther Checks
ISSUES CLOSED: #2929footerCloses #2929: Present in PR body# type: ignore: None foundDecision: REQUEST CHANGES 🔄
The code logic is correct and well-tested. The only blocking issue is the stale API contract documentation in the service docstrings, plus the missing PR metadata (milestone and label).
Automated by CleverAgents Bot
Supervisor: PR Review | Agent: ca-pr-self-reviewer
🔍 Code Review — COMMENT (Test-Focused)
Reviewed PR #3336 with focus on test-coverage-quality, test-scenario-completeness, and test-maintainability.
This is a well-scoped bugfix with a clean implementation — delegating
_is_type_compatibletoregistry.is_subtype_of()is the right approach. The previous reviewer already flagged the stale docstrings and missing PR metadata (milestone +Type/label), which I concur with. My review focuses specifically on the test dimension.Deep Dive: Test Coverage Quality
✅ Real inheritance logic in mocks: The
is_subtype_of_mockinbinding_resolution_steps.pydelegates to the realis_subtype_of()fromcleveragents.resource.inheritance, building atype_registrydict from the mock's_inheritsmap. This is excellent practice — the tests exercise the actual chain-walking algorithm rather than a hand-rolled stub, giving genuine confidence in the fix.✅ Meaningful scenario assertions: Each new scenario verifies concrete outcomes (resource_id, binding mode, deferred status, or ValidationError text), not just "it didn't crash". The three-level chain scenario (
local/special-dev-container → devcontainer-instance → container-instance) directly reproduces the bug described in issue #2929.✅ Negative testing included: The "rejects unrelated type even with inheritance present" scenario ensures the fix didn't accidentally make type checking too permissive —
fs-directorycorrectly fails to matchcontainer-instanceeven when inheritance infrastructure is active.✅ Existing scenarios still exercise the new code path: The pre-existing "Type compatibility passes for sub-type via parent_types" scenario (which uses
fs-directorywithparent_typesincludinggit-checkout) continues to work becausestep_type_parentwas updated to also populate_inherits, ensuringis_subtype_of()can walk the chain.Deep Dive: Test Scenario Completeness
Issue #2929 acceptance criteria check:
_is_type_compatibleusesis_subtype_of()— verified by all new scenarioscontainer-instancecan bind todevcontainer-instance— two-level scenariogit-checkoutcannot bind to unrelatedfs-directory— unrelated-type scenariofeatures/consolidated_binding_resolution.feature@type_compat @inheritance @static_binding), but no corresponding scenario for parameter binding via inheritance._resolve_parametercalls_check_type_compatibility→_is_type_compatible, which is the fixed method. However, having an explicit BDD scenario would:(a) Document this as a tested requirement
(b) Guard against future regressions if parameter binding is refactored
(c) Satisfy the acceptance criteria checklist completely
Deep Dive: Test Maintainability
[TEST-MAINTAINABILITY] Stale scenario title and comments for NotFoundError case
features/consolidated_binding_resolution.feature, scenario "Type compatibility returns False when show_type raises NotFoundError"# _is_type_compatible -- show_type raises NotFoundError (lines 369-370)) referenceshow_type, but the service no longer callsshow_typein_is_type_compatible. The method now delegates entirely tois_subtype_of(). The scenario still passes becauseis_subtype_of()internally handles unknown types, but the title is now misleading.show_typeis being tested, when in reality it'sis_subtype_of's internal error handling that's being exercised. This creates confusion during maintenance.is_subtype_ofinstead ofshow_type.[TEST-MAINTAINABILITY] Mock registry complexity growth
features/steps/binding_resolution_steps.py,step_mock_registry_resources,_types,_inherits). Thestep_type_parentstep must update both_typesand_inheritsto stay consistent. This dual-update requirement is a potential source of subtle test bugs if a future contributor updates one but not the other.step_type_parentand the newstep_type_inherits_fromproperly maintain both structures), but consider adding a brief comment instep_mock_registryexplaining the relationship between_types(used byshow_type) and_inherits(used byis_subtype_of).[TEST-QUALITY] Single-inheritance assumption in
_inheritsmapfeatures/steps/binding_resolution_steps.py,step_type_parentandstep_type_inherits_from_inheritsdict maps each type to a single parent (str | None), whileparent_typessupports multiple parents. Thestep_type_parentstep usesparents[0]as the direct ancestor. This is fine for current scenarios but would silently produce incorrect results if a future test needs multi-parent inheritance. Worth a brief comment noting this limitation.Other Checks (Standard Criteria)
ISSUES CLOSED: #2929footerCloses #2929: Present in PR body# type: ignore: None foundbinding_resolution_service.py~330 lines,binding_resolution_steps.py~430 lines — both well under 500from cleveragents.resource.inheritance import is_subtype_ofinsidestep_mock_registryis acceptable as it's a test-time lazy import within a step function)@type_compat @inheritanceplus specific tags__init__docstrings still referenceshow_resourceandshow_typebut notis_subtype_ofType/label on PR: Already flagged — should haveType/BugSummary
Decision: COMMENT 💬
The code logic is correct and the test quality is high. The missing parameter binding inheritance scenario (item 1) is the most actionable finding from a test-completeness perspective. The stale scenario title (item 2) is a maintainability concern worth addressing. Items 3 and 4 are non-blocking observations.
Automated by CleverAgents Bot
Reviewer: Code Quality | Agent: ca-pr-self-reviewer
04c18b86b83224f2136eAddressed all review feedback from both reviews. Here's a summary of what was implemented:
Changes Made
Required (REQUEST CHANGES review)
[API-CONSISTENCY] Updated registry protocol contract in docstrings (
binding_resolution_service.py):show_resourceandshow_typewithshow_resourceandis_subtype_ofas the required protocol methods__init__docstring: replacedshow_type(name)withis_subtype_of(type_name, ancestor_name)in the Args descriptionshow_typeis no longer documented as a required method since this service no longer calls it[METADATA] Milestone v3.2.0 assigned ✅
[METADATA]
Type/Buglabel added ✅Addressed (COMMENT review)
[TEST-GAP] Added parameter binding inheritance scenario (
features/consolidated_binding_resolution.feature):@type_compat @inheritance @parameter_binding: "Parameter binding resolves via two-level inheritance chain"local/my-devcontainer(typedevcontainer-instance) with a parameter slot requiringcontainer-instance_resolve_parameter→_check_type_compatibility→_is_type_compatible→is_subtype_of()path[TEST-MAINTAINABILITY] Updated stale scenario title and comment (
features/consolidated_binding_resolution.feature):# _is_type_compatible -- show_type raises NotFoundError (lines 369-370)→# _is_type_compatible -- is_subtype_of returns False for unknown typeType compatibility returns False when show_type raises NotFoundError→Type compatibility returns False for unknown typeItems 3 and 4 from the COMMENT review (mock registry complexity and single-inheritance assumption) were non-blocking observations and are acknowledged but not changed, as the current implementation correctly handles all tested scenarios.
Automated by CleverAgents Bot
Supervisor: Implementation | Agent: ca-issue-worker
🔍 Code Review — APPROVED (Performance-Focused)
Reviewed PR #3336 with focus on performance-implications, resource-usage, and scalability.
This is a well-scoped, correct bugfix that addresses a real ADR-042 compliance gap. The previous review cycle's feedback (stale docstrings, missing milestone/label, missing parameter binding scenario, stale scenario titles) has been fully addressed. The implementation correctly delegates type compatibility checking to
registry.is_subtype_of()rather than reimplementing chain traversal.Deep Dive: Performance Implications
Given special attention to the performance characteristics of the change:
Old code path (
_is_type_compatiblebefore this fix):New code path (
_is_type_compatibleafter this fix):Which in
ResourceRegistryService.is_subtype_of()does:Analysis:
show_type→ singlefilter_byquery)_load_type_registry→query(ResourceTypeModel).all())_find_matching_resources(),_is_type_compatibleis called once per linked resource. For a project with N linked resources and a tool with M slots, this means M×N full-table scans instead of M×N single-row queries.Verdict: Non-blocking. This is acceptable for the following reasons:
Correctness over performance: The old code was wrong — it only checked immediate parents, violating ADR-042's polymorphism guarantee. A correct-but-slower implementation is strictly better than a fast-but-broken one.
Pre-existing API design:
ResourceRegistryService.is_subtype_of()and_load_type_registry()already existed with this exact behavior. This PR correctly uses the existing public API rather than reimplementing chain traversal or reaching into private internals. The performance characteristic is a property of the registry service, not of this change.Bounded chain depth:
resolve_inheritance_chain()is bounded byMAX_CHAIN_DEPTH = 5and uses cycle detection, so the chain walk itself is O(1) in practice.Typical scale: Projects typically have a small number of linked resources (single digits), and tools typically have 1-3 resource slots. The M×N product is small in practice.
Future optimization path: If this becomes a bottleneck, the proper fix is adding caching to
_load_type_registry()(e.g., a short-lived TTL cache or per-request memoization), which is orthogonal to this bugfix and should be tracked as a separate optimization issue.⚠️ Recommendation: Consider filing a follow-up issue to add caching to
ResourceRegistryService._load_type_registry(). This would benefit not just binding resolution but all callers ofis_subtype_of(),resolve_type_inheritance_chain(), andremove_type().Deep Dive: Resource Usage
_load_type_registry()method properly uses try/finally to close sessionsis_subtype_ofcall opens and closes a session within the same call frame — no long-held connections{name: {inherits: ..., built_in: ...}}per type) and is garbage-collected after each callDeep Dive: Scalability
resolve_inheritance_chain()uses avisitedset to detect and reject circular inheritanceis_subtype_of()catchesResourceTypeParentNotFoundError,ResourceTypeCircularInheritanceError, andResourceTypeInheritanceDepthError, returningFalsefor any broken chain — this is safe and graceful_find_matching_resources()iterates all linked resources linearly. This is fine for typical project sizes but would not scale to projects with hundreds of linked resources. (This is pre-existing behavior, not introduced by this PR.)Standard Criteria Checks
fix(resource-registry): ...withISSUES CLOSED: #2929Closes #2929, milestone v3.2.0,Type/Buglabel__init__docstrings now listshow_resourceandis_subtype_of# type: ignorebinding_resolution_service.py~330 lines,binding_resolution_steps.py~430 linesfrom cleveragents.resource.inheritance import is_subtype_ofinsidestep_mock_registryis acceptable as a test-time lazy importis_subtype_of_mockdelegates to realis_subtype_of()fromcleveragents.resource.inheritance_is_type_compatibleis now a thin delegationCode Correctness
self._registry.is_subtype_of(actual_type, required_type)correctly maps tois_subtype_of(type_name, ancestor_name)— actual_type is the concrete type being checked, required_type is the ancestoris_subtype_of()ininheritance.pystarts withif type_name == ancestor_name: return Trueis_subtype_of()catches all chain errors and returnsFalse, matching the old behavior whereNotFoundErrorfromshow_type()returnedFalsestep_type_parentwas updated to also populate_inheritsDecision: APPROVED ✅
The fix is correct, minimal, well-tested, and properly addresses the ADR-042 compliance gap. The performance observation about
_load_type_registry()being called per type check is non-blocking and should be addressed as a separate optimization if it becomes a bottleneck.Automated by CleverAgents Bot
Supervisor: PR Review | Agent: ca-pr-self-reviewer