Skip to content

#4120: Selection CredentialSelector — filter candidates by PD field ID values#4121

Open
stevenvegt wants to merge 2 commits intofeature/4088-credential-selectorfrom
feature/4120-selection-selector
Open

#4120: Selection CredentialSelector — filter candidates by PD field ID values#4121
stevenvegt wants to merge 2 commits intofeature/4088-credential-selectorfrom
feature/4120-selection-selector

Conversation

@stevenvegt
Copy link
Member

Summary

  • Introduces NewSelectionSelector factory in vcr/pe/selector.go that creates a CredentialSelector filtering candidates by PD field ID values
  • Selection keys are validated against PD field IDs at construction time — unknown keys return an error
  • For input descriptors with matching selection keys: resolves field values via existing matchConstraint logic, filters by equality
  • For input descriptors without matching selection keys: falls back to FirstMatchSelector
  • Zero matches → ErrNoCredentials (soft failure via Refactor PD matching to return all candidates per input descriptor #4088), multiple matches → ErrMultipleCredentials

Closes #4120
Part of #4067

Test plan

  • Selection picks the correct credential by field value
  • Zero matches returns ErrNoCredentials
  • Multiple matches returns ErrMultipleCredentials
  • Unknown selection key returns construction error
  • No selection keys for descriptor → fallback to first match
  • Multiple selection keys on same descriptor use AND semantics
  • Multiple descriptors with independent selection keys

🤖 Generated with Claude Code

…D values

NewSelectionSelector creates a CredentialSelector that filters candidates
using named field ID values from the credential_selection API parameter.
Selection keys are validated against PD field IDs at construction time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@qltysh
Copy link

qltysh bot commented Mar 25, 2026

0 new issues

Tool Category Rule Count

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@stevenvegt stevenvegt linked an issue Mar 25, 2026 that may be closed by this pull request
16 tasks
@qltysh
Copy link

qltysh bot commented Mar 25, 2026

Qlty

Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (1)

RatingFile% DiffUncovered Line #s
New file Coverage rating: B
vcr/pe/selector.go84.2%40, 44, 72, 76...
Total84.2%
🤖 Increase coverage with AI coding...

In the `feature/4120-selection-selector` branch, add test coverage for this new code:

- `vcr/pe/selector.go` -- Lines 40, 44, 72, 76, 97-98, and 103-105

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Presentation Exchange CredentialSelector factory (NewSelectionSelector) that narrows already-matching credential candidates using credential_selection key/value pairs that reference PD constraint field IDs. This supports deterministic credential selection when multiple VCs satisfy the same input descriptor.

Changes:

  • Introduce NewSelectionSelector in vcr/pe/selector.go, including construction-time validation of selection keys against PD field IDs and per-descriptor fallback behavior.
  • Add comprehensive unit tests for selection behavior, including AND semantics, fallback behavior, and expected error cases.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
vcr/pe/selector.go Implements NewSelectionSelector and the field-value filtering logic for candidates.
vcr/pe/selector_test.go Adds tests covering single match, zero/multiple matches, unknown key validation, fallback, AND semantics, and multi-descriptor behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +79
var matched []vc.VerifiableCredential
for _, candidate := range candidates {
if descriptor.Constraints == nil {
continue
}
isMatch, values, err := matchConstraint(descriptor.Constraints, candidate)
if err != nil || !isMatch {
continue
}
if matchesSelections(values, selections) {
matched = append(matched, candidate)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewSelectionSelector re-runs matchConstraint for each candidate even though the selector contract says all candidates already match the input descriptor (they’re produced by PresentationDefinition.matchConstraints). This duplicates JSON remarshal + full constraint evaluation and can be a noticeable overhead when wallets contain many VCs. Consider resolving only the selected field values (e.g., build a fieldID→Field map at construction and use matchField/getValueAtPath on a single remarshal), without re-checking the entire constraint set.

Copilot uses AI. Check for mistakes.
continue
}
isMatch, values, err := matchConstraint(descriptor.Constraints, candidate)
if err != nil || !isMatch {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors from matchConstraint are currently swallowed (if err != nil || !isMatch { continue }). If matchConstraint ever fails (e.g., due to unexpected credential encoding/path issues), this will silently drop candidates and surface as ErrNoCredentials/ErrMultipleCredentials, masking the real problem. Return the error immediately when err != nil so the caller gets a hard failure with the root cause.

Suggested change
if err != nil || !isMatch {
if err != nil {
return nil, fmt.Errorf("input descriptor '%s': %w", descriptor.Id, err)
}
if !isMatch {

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +67
return func(descriptor InputDescriptor, candidates []vc.VerifiableCredential) (*vc.VerifiableCredential, error) {
selections, ok := descriptorSelections[descriptor.Id]
if !ok {
return fallback(descriptor, candidates)
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewSelectionSelector may call fallback(descriptor, candidates) without guarding against a nil fallback. Since this is a public factory, passing a nil fallback would panic at runtime. Consider defaulting fallback to FirstMatchSelector when nil (or returning an error at construction time).

Copilot uses AI. Check for mistakes.

// NewSelectionSelector creates a CredentialSelector that filters candidates using
// named field ID values from the credential_selection parameter.
func NewSelectionSelector(selection map[string]string, pd PresentationDefinition, fallback CredentialSelector) (CredentialSelector, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like it selects a selection, but it just selects credentials? So why not NewCredentialSelector?

continue
}
isMatch, values, err := matchConstraint(descriptor.Constraints, candidate)
if err != nil || !isMatch {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth logging the error, if we're not returning it? In which cases cant his occur?

if descriptor.Constraints == nil {
continue
}
isMatch, values, err := matchConstraint(descriptor.Constraints, candidate)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have multiple constraint types in Presentation Definitions. We generally use constant (value must be exactly X) or pattern (value must comply to regex pattern Y). Do we want to support both?

Does that work properly with matchesSelections() which compares the given value to the matched value? Because it sounds like we only want to support constant.

if str != sel.expected {
return false
}
} else if fmt.Sprintf("%v", resolved) != sel.expected {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we doing this? Support for bools and numbers? Does that actually work?

Why not just comparison through ==?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Selection CredentialSelector: filter candidates by PD field ID values

3 participants