Repository: apigee/registry
Pull request #806 (2022-11-02 21:23:34)

Add support for filters in targetPattern
Fixes https://github.com/apigee/registry/issues/799 and https://github.com/apigee/registry/issues/532

codecov[bot]: # [Codecov](https://codecov.io/gh/apigee/registry/pull/806?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) Report
> Merging [#806](https://codecov.io/gh/apigee/registry/pull/806?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (9236361) into [main](https://codecov.io/gh/apigee/registry/commit/1da0f9e6fc9a3dda51b7158492677937c23d0ac9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (1da0f9e) will **increase** coverage by `2.90%`.
> The diff coverage is `28.57%`.

```diff
@@            Coverage Diff             @@
##             main     #806      +/-   ##
==========================================
+ Coverage   52.07%   54.98%   +2.90%     
==========================================
  Files          97       98       +1     
  Lines        8547     8830     +283     
==========================================
+ Hits         4451     4855     +404     
+ Misses       3663     3482     -181     
- Partials      433      493      +60     
```


| [Impacted Files](https://codecov.io/gh/apigee/registry/pull/806?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) | Coverage Δ | |
|---|---|---|
| [cmd/registry/cmd/compute/score.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL3Njb3JlLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/scorecard.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL3Njb3JlY2FyZC5nbw==) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/patterns/resources.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3BhdHRlcm5zL3Jlc291cmNlcy5nbw==) | `18.45% <0.00%> (-1.05%)` | :arrow_down: |
| [cmd/registry/scoring/score.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3Jpbmcvc2NvcmUuZ28=) | `73.57% <0.00%> (-0.74%)` | :arrow_down: |
| [cmd/registry/scoring/scorecard.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3Jpbmcvc2NvcmVjYXJkLmdv) | `52.59% <0.00%> (-1.61%)` | :arrow_down: |
| [server/registry/filtering/filtering.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-c2VydmVyL3JlZ2lzdHJ5L2ZpbHRlcmluZy9maWx0ZXJpbmcuZ28=) | `86.04% <ø> (ø)` | |
| [server/registry/filtering/models.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-c2VydmVyL3JlZ2lzdHJ5L2ZpbHRlcmluZy9tb2RlbHMuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/scoring/parser.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3JpbmcvcGFyc2VyLmdv) | `86.25% <75.63%> (-0.92%)` | :arrow_down: |
| [server/registry/server.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-c2VydmVyL3JlZ2lzdHJ5L3NlcnZlci5nbw==) | `48.61% <100.00%> (+3.02%)` | :arrow_up: |
| [cmd/registry/cmd/apply/apply.go](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9hcHBseS9hcHBseS5nbw==) | `61.11% <0.00%> (-2.05%)` | :arrow_down: |
| ... and [8 more](https://codecov.io/gh/apigee/registry/pull/806/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee)

theganyo: Did you investigate using some kind of reflection against the models instead of repeating the name and type information as manually maintained code?
shrutiparabgoogle: > Did you investigate using some kind of reflection against the models instead of repeating the name and type information as manually maintained code?

The filtering package in this PR is moved from `registry/server/internal/storage/filtering` to `registry/server/filtering`. Most of the code is copy pasted. I wanted to keep the modifications to the filtering package to the minimum since it also affects the filtering capability of the API. I am not sure if reflection was considered as an option while implementing this package initially. @seaneganx might know.

In any case, the use of reflection can be a refactor in a separate PR. I created https://github.com/apigee/registry/issues/809 to investigate this separately.
shrutiparabgoogle: I have rewritten the logic as suggested by Tim here: https://github.com/apigee/registry/issues/532#issuecomment-1304059397

The rewritten code:
- Combines the pattern and filter from the input supplied to the command and from the ScoreDefinitions. It then fetches resources using this combined pattern and calculates scores.
- The validation of filter part (lower priority) is taken out and the filtering module stays in the internal package. 


In a separate PR, we can think of taking out the validation logic from the internal module to the `pkg` level. Once we have that in place, we can implement the validation of filters.

Patch:
diff --git a/cmd/registry/cmd/compute/score.go b/cmd/registry/cmd/compute/score.go
index c9b6ab7..6c72ad7 100644
--- a/cmd/registry/cmd/compute/score.go
+++ b/cmd/registry/cmd/compute/score.go
@@ -24,6 +24,7 @@ import (
 	"github.com/apigee/registry/pkg/connection"
 	"github.com/apigee/registry/rpc"
 	"github.com/spf13/cobra"
+	"google.golang.org/protobuf/proto"
 )
 
 func scoreCommand() *cobra.Command {
@@ -47,6 +48,7 @@ func scoreCommand() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
 			}
+
 			// Initialize task queue.
 			// Use the warnings queue to make sure that failure in one score calculation task doesn't abort the whole queue.
 			jobs, err := cmd.Flags().GetInt("jobs")
@@ -56,21 +58,37 @@ func scoreCommand() *cobra.Command {
 			taskQueue, wait := core.WorkerPoolWithWarnings(ctx, jobs)
 			defer wait()
 
-			resources, err := patterns.ListResources(ctx, client, args[0], filter)
+			inputPattern, err := patterns.ParseResourcePattern(args[0])
 			if err != nil {
-				log.FromContext(ctx).WithError(err).Fatal("Failed to list resources")
+				log.FromContext(ctx).WithError(err).Fatal("invalid pattern supplied in the args")
 			}
-
 			artifactClient := &scoring.RegistryArtifactClient{RegistryClient: client}
 
-			for _, r := range resources {
-				// Fetch the ScoreDefinitions which can be applied to this resource
-				scoreDefinitions, err := scoring.FetchScoreDefinitions(ctx, artifactClient, r.ResourceName())
+			scoreDefinitions, err := scoring.FetchScoreDefinitions(ctx, artifactClient, inputPattern.Project())
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatalf("Failed to get ScoreDefinitions")
+			}
+			// List resources based on the retrieved definitions
+			for _, d := range scoreDefinitions {
+				// Extract definition
+				definition := &rpc.ScoreDefinition{}
+				if err := proto.Unmarshal(d.GetContents(), definition); err != nil {
+					log.FromContext(ctx).WithError(err).Errorf("Failed to unmarshal ScoreDefinition: %q", d.GetName())
+					continue
+				}
+				mergedPattern, mergedFilter, err := scoring.GenerateCombinedPattern(definition.GetTargetResource(), inputPattern, filter)
 				if err != nil {
-					log.FromContext(ctx).WithError(err).Errorf("Skipping resource %q", r.ResourceName())
+					log.FromContext(ctx).WithError(err).Errorf("Skipping definition %q", d.GetName())
 					continue
 				}
-				for _, d := range scoreDefinitions {
+
+				resources, err := patterns.ListResources(ctx, client, mergedPattern, mergedFilter)
+				if err != nil || len(resources) == 0 {
+					log.FromContext(ctx).WithError(err).Errorf("Skipping definition %q", d.GetName())
+					continue
+				}
+
+				for _, r := range resources {
 					taskQueue <- &computeScoreTask{
 						client:      artifactClient,
 						defArtifact: d,
diff --git a/cmd/registry/cmd/compute/scorecard.go b/cmd/registry/cmd/compute/scorecard.go
index d68d333..ba0146f 100644
--- a/cmd/registry/cmd/compute/scorecard.go
+++ b/cmd/registry/cmd/compute/scorecard.go
@@ -24,6 +24,7 @@ import (
 	"github.com/apigee/registry/pkg/connection"
 	"github.com/apigee/registry/rpc"
 	"github.com/spf13/cobra"
+	"google.golang.org/protobuf/proto"
 )
 
 func scoreCardCommand() *cobra.Command {
@@ -46,29 +47,47 @@ func scoreCardCommand() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
 			}
+
 			// Initialize task queue.
+			// Use the warnings queue to make sure that failure in one score calculation task doesn't abort the whole queue.
 			jobs, err := cmd.Flags().GetInt("jobs")
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get jobs from flags")
 			}
-			taskQueue, wait := core.WorkerPool(ctx, jobs)
+			taskQueue, wait := core.WorkerPoolWithWarnings(ctx, jobs)
 			defer wait()
 
-			resources, err := patterns.ListResources(ctx, client, args[0], filter)
+			inputPattern, err := patterns.ParseResourcePattern(args[0])
 			if err != nil {
-				log.FromContext(ctx).WithError(err).Fatal("Failed to list resources")
+				log.FromContext(ctx).WithError(err).Fatal("invalid pattern supplied in the args")
 			}
-
 			artifactClient := &scoring.RegistryArtifactClient{RegistryClient: client}
 
-			for _, r := range resources {
-				// Fetch the ScoreCardDefinitions which can be applied to this resource
-				scoreCardDefinitions, err := scoring.FetchScoreCardDefinitions(ctx, artifactClient, r.ResourceName())
+			scoreCardDefinitions, err := scoring.FetchScoreCardDefinitions(ctx, artifactClient, inputPattern.Project())
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatalf("Failed to get ScoreCardDefinitions")
+			}
+			// List resources based on the retrieved definitions
+			for _, d := range scoreCardDefinitions {
+				// Extract definition
+				definition := &rpc.ScoreCardDefinition{}
+				if err := proto.Unmarshal(d.GetContents(), definition); err != nil {
+					log.FromContext(ctx).WithError(err).Errorf("Failed to unmarshal ScoreCardDefinition: %q", d.GetName())
+					continue
+				}
+				mergedPattern, mergedFilter, err := scoring.GenerateCombinedPattern(definition.GetTargetResource(), inputPattern, filter)
 				if err != nil {
-					log.FromContext(ctx).WithError(err).Errorf("Skipping resource %q", r.ResourceName())
+					log.FromContext(ctx).WithError(err).Errorf("Skipping definition %q", d.GetName())
 					continue
 				}
-				for _, d := range scoreCardDefinitions {
+
+				resources, err := patterns.ListResources(ctx, client, mergedPattern, mergedFilter)
+				if err != nil || len(resources) == 0 {
+					log.FromContext(ctx).WithError(err).Errorf("Skipping definition %q", d.GetName())
+					continue
+				}
+
+				for _, r := range resources {
 					taskQueue <- &computeScoreCardTask{
 						client:      artifactClient,
 						defArtifact: d,
diff --git a/cmd/registry/patterns/resources.go b/cmd/registry/patterns/resources.go
index 20a2e59..48b5b1f 100644
--- a/cmd/registry/patterns/resources.go
+++ b/cmd/registry/patterns/resources.go
@@ -454,7 +454,6 @@ func generateApiHandler(result *[]ResourceInstance) func(*rpc.Api) error {
 			ApiName:   ApiName{Name: name},
 			Timestamp: api.UpdateTime.AsTime(),
 		})
-
 		return nil
 	}
 }
@@ -465,12 +464,10 @@ func generateVersionHandler(result *[]ResourceInstance) func(*rpc.ApiVersion) er
 		if err != nil {
 			return err
 		}
-
 		(*result) = append((*result), VersionResource{
 			VersionName: VersionName{Name: name},
 			Timestamp:   version.UpdateTime.AsTime(),
 		})
-
 		return nil
 	}
 }
@@ -489,7 +486,6 @@ func generateSpecHandler(result *[]ResourceInstance) func(*rpc.ApiSpec) error {
 			},
 			Timestamp: spec.RevisionUpdateTime.AsTime(),
 		})
-
 		return nil
 	}
 }
@@ -500,12 +496,10 @@ func generateArtifactHandler(result *[]ResourceInstance) func(*rpc.Artifact) err
 		if err != nil {
 			return err
 		}
-
 		(*result) = append((*result), ArtifactResource{
 			ArtifactName: ArtifactName{Name: name},
 			Timestamp:    artifact.UpdateTime.AsTime(),
 		})
-
 		return nil
 	}
 }
diff --git a/cmd/registry/scoring/parser.go b/cmd/registry/scoring/parser.go
index e76523c..a5d541b 100644
--- a/cmd/registry/scoring/parser.go
+++ b/cmd/registry/scoring/parser.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 
 	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
 
 	"github.com/apigee/registry/cmd/registry/patterns"
 )
@@ -33,9 +34,7 @@ func ValidateScoreDefinition(parent string, scoreDefinition *rpc.ScoreDefinition
 		totalErrs = append(totalErrs, err)
 	}
 
-	// TODO: Check for valid filter in target_resource
-
-	// Validate formula if there were no errors in target_resource.pattern
+	// Validate formula if there were no errors in target_resource
 	if len(totalErrs) == 0 {
 		switch formula := scoreDefinition.GetFormula().(type) {
 		case *rpc.ScoreDefinition_ScoreFormula:
@@ -132,7 +131,7 @@ func validateReferencesInPattern(targetName patterns.ResourceName, pattern strin
 		errs = append(errs, fmt.Errorf("invalid pattern: %q, must always start with '$resource.(api|version|spec|artifact)'", pattern))
 	} else if _, err = patterns.GetReferenceEntityValue(pattern, targetName); err != nil {
 		// $resource should have valid entity reference wrt target_resource
-		errs = append(errs, fmt.Errorf("invalid pattern %q, invalid $resource reference in pattern: %s", pattern, err))
+		errs = append(errs, fmt.Errorf("invalid pattern: %q, invalid $resource reference in pattern: %s", pattern, err))
 	}
 
 	return errs
@@ -258,59 +257,131 @@ func validateBooleanThresholds(thresholds []*rpc.BooleanThreshold) []error {
 	return errs
 }
 
-func matchResourceWithTarget(targetPattern *rpc.ResourcePattern, resourceName patterns.ResourceName, project string) error {
-	targetPatternName, err := patterns.ParseResourcePattern(fmt.Sprintf("%s/%s", project, targetPattern.GetPattern()))
+func GenerateCombinedPattern(targetPattern *rpc.ResourcePattern, inputPatternName patterns.ResourceName, inputFilter string) (string, string, error) {
+	projectID := strings.Split(inputPatternName.Project(), "/")[1]
+	// Generate a common list pattern based on the supplied input and the targetPattern in the definition
+	targetPatternName, err := patterns.ParseResourcePattern(fmt.Sprintf("projects/%s/locations/global/%s", projectID, targetPattern.GetPattern()))
 	if err != nil {
-		return err
+		return "", "", fmt.Errorf("invalid targetPattern in ScoreDefinition: %s", err)
 	}
+	fmt.Printf("%s", targetPatternName.String())
 
+	// Merge the two patterns into one
 	switch tp := targetPatternName.(type) {
 	case patterns.SpecName:
-		// Check if targetPattern and resourceName match in type
-		r, ok := resourceName.(patterns.SpecName)
+		// Check if targetPattern and inputPattern match in type
+		ip, ok := inputPatternName.(patterns.SpecName)
 		if !ok {
-			return fmt.Errorf("resource %s doesn't match target pattern %s", r, tp)
+			return "", "", fmt.Errorf("input pattern %q does not match with target pattern %q", ip, tp)
 		}
 
-		// Check if the individual entities match
-		if tp.Name.ApiID != "-" && tp.Name.ApiID != r.Name.ApiID {
-			return fmt.Errorf("api mismatch in resource %s and target pattern %v", resourceName.String(), targetPattern)
+		// Merge the patters together
+		mergedPatternName := patterns.SpecName{
+			Name: names.Spec{
+				ProjectID: projectID,
+			},
+		}
+		mergedApi, err := findCommonPattern(tp.Name.ApiID, ip.Name.ApiID)
+		if err != nil {
+			return "", "", fmt.Errorf("cannot find common pattern between %q and %q", tp.String(), ip.String())
 		}
-		if tp.Name.VersionID != "-" && tp.Name.VersionID != r.Name.VersionID {
-			return fmt.Errorf("version mismatch in resource %s and target pattern %v", resourceName.String(), targetPattern)
+		mergedPatternName.Name.ApiID = mergedApi
+		mergedVersion, err := findCommonPattern(tp.Name.VersionID, ip.Name.VersionID)
+		if err != nil {
+			return "", "", fmt.Errorf("cannot find common pattern between %q and %q", tp.String(), ip.String())
 		}
-		if tp.Name.SpecID != "-" && tp.Name.SpecID != r.Name.SpecID {
-			return fmt.Errorf("spec mismatch in resource %s and target pattern %v", resourceName.String(), targetPattern)
+		mergedPatternName.Name.VersionID = mergedVersion
+		mergedSpec, err := findCommonPattern(tp.Name.SpecID, ip.Name.SpecID)
+		if err != nil {
+			return "", "", fmt.Errorf("cannot find common pattern between %q and %q", tp.String(), ip.String())
 		}
+		mergedPatternName.Name.SpecID = mergedSpec
+
+		// Merge the filters together
+		mergedFilter := generateCommonFilter(targetPattern.GetFilter(), inputFilter)
+
+		return mergedPatternName.String(), mergedFilter, nil
 	case patterns.VersionName:
-		// Check if targetPattern and resourceName match in type
-		r, ok := resourceName.(patterns.VersionName)
+		// Check if targetPattern and inputPattern match in type
+		ip, ok := inputPatternName.(patterns.VersionName)
 		if !ok {
-			return fmt.Errorf("resource %s doesn't match target pattern %s", r, tp)
+			return "", "", fmt.Errorf("input pattern %q does not match with target pattern %q", ip, tp)
 		}
 
-		// Check if the individual entities match
-		if tp.Name.ApiID != "-" && tp.Name.ApiID != r.Name.ApiID {
-			return fmt.Errorf("api mismatch in resource %s and target pattern %v", resourceName.String(), targetPattern)
+		// Merge the patters together
+		mergedPatternName := patterns.VersionName{
+			Name: names.Version{
+				ProjectID: projectID,
+			},
+		}
+		mergedApi, err := findCommonPattern(tp.Name.ApiID, ip.Name.ApiID)
+		if err != nil {
+			return "", "", fmt.Errorf("cannot find common pattern between %q and %q", tp.String(), ip.String())
 		}
-		if tp.Name.VersionID != "-" && tp.Name.VersionID != r.Name.VersionID {
-			return fmt.Errorf("version mismatch in resource %s and target pattern %v", resourceName.String(), targetPattern)
+		mergedPatternName.Name.ApiID = mergedApi
+		mergedVersion, err := findCommonPattern(tp.Name.VersionID, ip.Name.VersionID)
+		if err != nil {
+			return "", "", fmt.Errorf("cannot find common pattern between %q and %q", tp.String(), ip.String())
 		}
+		mergedPatternName.Name.VersionID = mergedVersion
+
+		// Merge the filters together
+		mergedFilter := generateCommonFilter(targetPattern.GetFilter(), inputFilter)
+
+		return mergedPatternName.String(), mergedFilter, nil
 	case patterns.ApiName:
-		// Check if targetPattern and resourceName match in type
-		r, ok := resourceName.(patterns.ApiName)
+		// Check if targetPattern and inputPattern match in type
+		ip, ok := inputPatternName.(patterns.ApiName)
 		if !ok {
-			return fmt.Errorf("resource %s doesn't match target pattern %s", r, tp)
+			return "", "", fmt.Errorf("input pattern %q does not match with target pattern %q", ip, tp)
 		}
 
-		// Check if the individual entities match
-		if tp.Name.ApiID != "-" && tp.Name.ApiID != r.Name.ApiID {
-			return fmt.Errorf("api mismatch in resource %s and target pattern %v", resourceName.String(), targetPattern)
+		// Merge the patters together
+		mergedPatternName := patterns.ApiName{
+			Name: names.Api{
+				ProjectID: projectID,
+			},
+		}
+		mergedApi, err := findCommonPattern(tp.Name.ApiID, ip.Name.ApiID)
+		if err != nil {
+			return "", "", fmt.Errorf("cannot find common pattern between %q and %q", tp.String(), ip.String())
 		}
+		mergedPatternName.Name.ApiID = mergedApi
+
+		// Merge the filters together
+		mergedFilter := generateCommonFilter(targetPattern.GetFilter(), inputFilter)
+
+		return mergedPatternName.String(), mergedFilter, nil
 	default:
-		return fmt.Errorf("unsupported resource type %T", targetPatternName)
+		return "", "", fmt.Errorf("Unsupported pattern in either targetPattern %q or inputPattern %q", targetPatternName.String(), inputPatternName.String())
 	}
+}
+
+func generateCommonFilter(a, b string) string {
+	if a == b {
+		return a
+	}
+	if a != "" && b != "" {
+		return fmt.Sprintf("(%s) && (%s)", a, b)
+	}
+	if a != "" && b == "" {
+		return a
+	}
+	return b
+}
 
-	// TODO: Filter check
-	return nil
+func findCommonPattern(a, b string) (string, error) {
+	if a == "" || b == "" {
+		return "", fmt.Errorf("cannot have empty name")
+	}
+	if a == b {
+		return a, nil
+	}
+	if a == "-" && b != "-" {
+		return b, nil
+	}
+	if a != "-" && b == "-" {
+		return a, nil
+	}
+	return "", fmt.Errorf("Cannot find common pattern")
 }
diff --git a/cmd/registry/scoring/score.go b/cmd/registry/scoring/score.go
index e42082c..30c1f13 100644
--- a/cmd/registry/scoring/score.go
+++ b/cmd/registry/scoring/score.go
@@ -38,11 +38,10 @@ func scoreID(definitionID string) string {
 func FetchScoreDefinitions(
 	ctx context.Context,
 	client artifactClient,
-	resource patterns.ResourceName) ([]*rpc.Artifact, error) {
+	project string) ([]*rpc.Artifact, error) {
 	defArtifacts := make([]*rpc.Artifact, 0)
 
-	project := fmt.Sprintf("%s/locations/global", resource.Project())
-	artifact, err := names.ParseArtifact(fmt.Sprintf("%s/artifacts/-", project))
+	artifact, err := names.ParseArtifact(fmt.Sprintf("%s/locations/global/artifacts/-", project))
 	if err != nil {
 		return nil, err
 	}
@@ -56,14 +55,6 @@ func FetchScoreDefinitions(
 				return nil
 			}
 
-			// Check if ScoreDefinition.TargetResource matches with the supplied resource
-			err1 := matchResourceWithTarget(definition.GetTargetResource(), resource, project)
-			if err1 != nil {
-				// don't return err, to proccess the rest of the artifacts from the list.
-				log.Debugf(ctx, "Skipping definition %q: %s", artifact.GetName(), err1)
-				return nil
-			}
-
 			defArtifacts = append(defArtifacts, artifact)
 			return nil
 		})
diff --git a/cmd/registry/scoring/scorecard.go b/cmd/registry/scoring/scorecard.go
index 10da9c9..62c5a20 100644
--- a/cmd/registry/scoring/scorecard.go
+++ b/cmd/registry/scoring/scorecard.go
@@ -36,11 +36,10 @@ func scoreCardID(definitionID string) string {
 func FetchScoreCardDefinitions(
 	ctx context.Context,
 	client artifactClient,
-	resource patterns.ResourceName) ([]*rpc.Artifact, error) {
+	project string) ([]*rpc.Artifact, error) {
 	defArtifacts := make([]*rpc.Artifact, 0)
 
-	project := fmt.Sprintf("%s/locations/global", resource.Project())
-	artifact, err := names.ParseArtifact(fmt.Sprintf("%s/artifacts/-", project))
+	artifact, err := names.ParseArtifact(fmt.Sprintf("%s/locations/global/artifacts/-", project))
 	if err != nil {
 		return nil, err
 	}
@@ -54,14 +53,6 @@ func FetchScoreCardDefinitions(
 				return nil
 			}
 
-			// Check if ScoreCardDefinition.TargetResource matches with the supplied resource
-			err1 := matchResourceWithTarget(definition.GetTargetResource(), resource, project)
-			if err1 != nil {
-				// don't return err, to proccess the rest of the artifacts from the list.
-				log.Debugf(ctx, "Skipping definition %q: %s", artifact.GetName(), err1)
-				return nil
-			}
-
 			defArtifacts = append(defArtifacts, artifact)
 			return nil
 		})


Test patch:
diff --git a/cmd/registry/cmd/compute/score_test.go b/cmd/registry/cmd/compute/score_test.go
new file mode 100644
index 0000000..520cd35
--- /dev/null
+++ b/cmd/registry/cmd/compute/score_test.go
@@ -0,0 +1,336 @@
+package compute
+
+import (
+	"context"
+	"regexp"
+	"testing"
+
+	"github.com/apigee/registry/pkg/connection"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/test/seeder"
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/api/iterator"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+	"google.golang.org/protobuf/proto"
+)
+
+const gzipOpenAPIv3 = "application/x.openapi+gzip;version=3.0.0"
+const gzipProtobuf = "application/x.protobuf+gzip"
+const scoreDefinitionType = "application/octet-stream;type=google.cloud.apigeeregistry.v1.scoring.ScoreDefinition"
+const conformanceReportType = "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.ConformanceReport"
+
+var (
+	conformanceReport = &rpc.ConformanceReport{
+		Id:         "conformance-report",
+		Kind:       "ConformanceReport",
+		Styleguide: "projects/score-test/locations/global/artifacts/styleguide",
+		GuidelineReportGroups: []*rpc.GuidelineReportGroup{
+			{State: rpc.Guideline_STATE_UNSPECIFIED},
+			{State: rpc.Guideline_PROPOSED},
+			{
+				State: rpc.Guideline_ACTIVE,
+				GuidelineReports: []*rpc.GuidelineReport{
+					{
+						GuidelineId: "sample-guideline",
+						RuleReportGroups: []*rpc.RuleReportGroup{
+							{Severity: rpc.Rule_SEVERITY_UNSPECIFIED},
+							{
+								Severity: rpc.Rule_ERROR,
+								RuleReports: []*rpc.RuleReport{
+									{
+										RuleId: "openapi-tags",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	scoreAll = &rpc.ScoreDefinition{
+		Id:   "lint-error",
+		Kind: "ScoreDefinition",
+		TargetResource: &rpc.ResourcePattern{
+			Pattern: "apis/-/versions/-/specs/-",
+		},
+		Formula: &rpc.ScoreDefinition_ScoreFormula{
+			ScoreFormula: &rpc.ScoreFormula{
+				Artifact:        &rpc.ResourcePattern{Pattern: "$resource.spec/artifacts/conformance-report"},
+				ScoreExpression: "sum(guidelineReportGroups[2].guidelineReports.map(r, size(r.ruleReportGroups[1].ruleReports)))",
+			},
+		},
+		Type: &rpc.ScoreDefinition_Integer{
+			Integer: &rpc.IntegerType{
+				MinValue: 0,
+				MaxValue: 10,
+			},
+		},
+	}
+
+	scoreOpenAPI = &rpc.ScoreDefinition{
+		Id:   "lint-error-openapi",
+		Kind: "ScoreDefinition",
+		TargetResource: &rpc.ResourcePattern{
+			Pattern: "apis/-/versions/-/specs/-",
+			Filter:  "mime_type.contains('openapi')",
+		},
+		Formula: &rpc.ScoreDefinition_ScoreFormula{
+			ScoreFormula: &rpc.ScoreFormula{
+				Artifact:        &rpc.ResourcePattern{Pattern: "$resource.spec/artifacts/conformance-report"},
+				ScoreExpression: "sum(guidelineReportGroups[2].guidelineReports.map(r, size(r.ruleReportGroups[1].ruleReports)))",
+			},
+		},
+		Type: &rpc.ScoreDefinition_Integer{
+			Integer: &rpc.IntegerType{
+				MinValue: 0,
+				MaxValue: 10,
+			},
+		},
+	}
+
+	scoreProto = &rpc.ScoreDefinition{
+		Id:   "lint-error-proto",
+		Kind: "ScoreDefinition",
+		TargetResource: &rpc.ResourcePattern{
+			Pattern: "apis/-/versions/-/specs/-",
+			Filter:  "mime_type.contains('protobuf')",
+		},
+		Formula: &rpc.ScoreDefinition_ScoreFormula{
+			ScoreFormula: &rpc.ScoreFormula{
+				Artifact:        &rpc.ResourcePattern{Pattern: "$resource.spec/artifacts/conformance-report"},
+				ScoreExpression: "sum(guidelineReportGroups[2].guidelineReports.map(r, size(r.ruleReportGroups[1].ruleReports)))",
+			},
+		},
+		Type: &rpc.ScoreDefinition_Integer{
+			Integer: &rpc.IntegerType{
+				MinValue: 0,
+				MaxValue: 10,
+			},
+		},
+	}
+)
+
+func protoMarshal(m proto.Message) []byte {
+	b, _ := proto.Marshal(m)
+	return b
+}
+
+func deleteProject(
+	ctx context.Context,
+	client connection.AdminClient,
+	t *testing.T,
+	projectID string) {
+	t.Helper()
+	req := &rpc.DeleteProjectRequest{
+		Name:  "projects/" + projectID,
+		Force: true,
+	}
+	err := client.DeleteProject(ctx, req)
+	if err != nil && status.Code(err) != codes.NotFound {
+		t.Fatalf("Failed DeleteProject(%v): %s", req, err.Error())
+	}
+}
+
+func TestScore(t *testing.T) {
+	tests := []struct {
+		desc string
+		seed []seeder.RegistryResource
+		want []string
+	}{
+		{
+			desc: "all spec scores",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/conformance-report",
+					MimeType: conformanceReportType,
+					Contents: protoMarshal(conformanceReport),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/conformance-report",
+					MimeType: conformanceReportType,
+					Contents: protoMarshal(conformanceReport),
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/artifacts/lint-error",
+					MimeType: scoreDefinitionType,
+					Contents: protoMarshal(scoreAll),
+				},
+			},
+			want: []string{
+				"projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml@([a-z0-9-]+)/artifacts/score-lint-error",
+				"projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml@([a-z0-9-]+)/artifacts/score-lint-error",
+			},
+		},
+		{
+			desc: "only openapi scores with single definition",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/conformance-report",
+					MimeType: conformanceReportType,
+					Contents: protoMarshal(conformanceReport),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml",
+					MimeType: gzipProtobuf,
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml/artifacts/conformance-report",
+					MimeType: conformanceReportType,
+					Contents: protoMarshal(conformanceReport),
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/artifacts/lint-error-openapi",
+					MimeType: scoreDefinitionType,
+					Contents: protoMarshal(scoreOpenAPI),
+				},
+			},
+			want: []string{
+				"projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml@([a-z0-9-]+)/artifacts/score-lint-error-openapi",
+			},
+		},
+		{
+			desc: "only proto scores with single definition",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/conformance-report",
+					MimeType: conformanceReportType,
+					Contents: protoMarshal(conformanceReport),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml",
+					MimeType: gzipProtobuf,
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml/artifacts/conformance-report",
+					MimeType: conformanceReportType,
+					Contents: protoMarshal(conformanceReport),
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/artifacts/lint-error-proto",
+					MimeType: scoreDefinitionType,
+					Contents: protoMarshal(scoreProto),
+				},
+			},
+			want: []string{
+				"projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml@([a-z0-9-]+)/artifacts/score-lint-error-proto",
+			},
+		},
+		{
+			desc: "proto and openapi scores with both definitions",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/conformance-report",
+					MimeType: conformanceReportType,
+					Contents: protoMarshal(conformanceReport),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml",
+					MimeType: gzipProtobuf,
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml/artifacts/conformance-report",
+					MimeType: conformanceReportType,
+					Contents: protoMarshal(conformanceReport),
+				},
+
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/artifacts/lint-error-openapi",
+					MimeType: scoreDefinitionType,
+					Contents: protoMarshal(scoreOpenAPI),
+				},
+				&rpc.Artifact{
+					Name:     "projects/score-test/locations/global/artifacts/lint-error-proto",
+					MimeType: scoreDefinitionType,
+					Contents: protoMarshal(scoreProto),
+				},
+			},
+			want: []string{
+				"projects/score-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml@([a-z0-9-]+)/artifacts/score-lint-error-openapi",
+				"projects/score-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml@([a-z0-9-]+)/artifacts/score-lint-error-proto",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			registryClient, err := connection.NewRegistryClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			t.Cleanup(func() { registryClient.Close() })
+
+			adminClient, err := connection.NewAdminClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			t.Cleanup(func() { adminClient.Close() })
+
+			deleteProject(ctx, adminClient, t, "score-test")
+			t.Cleanup(func() { deleteProject(ctx, adminClient, t, "score-test") })
+
+			client := seeder.Client{
+				RegistryClient: registryClient,
+				AdminClient:    adminClient,
+			}
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			// setup the score command
+			scoreCmd := Command()
+			args := []string{"score", "projects/score-test/locations/global/apis/-/versions/-/specs/-"}
+			scoreCmd.SetArgs(args)
+
+			if err = scoreCmd.Execute(); err != nil {
+				t.Fatalf("Execute() with args %v returned error: %s", args, err)
+			}
+
+			// list score artifacts
+			it := registryClient.ListArtifacts(ctx, &rpc.ListArtifactsRequest{
+				Parent: "projects/score-test/locations/global/apis/-/versions/-/specs/-",
+				Filter: "mime_type.contains('Score')",
+			})
+
+			got := []string{}
+			for a, err := it.Next(); err != iterator.Done; a, err = it.Next() {
+				if err != nil {
+					break
+				}
+				got = append(got, a.GetName())
+			}
+
+			regexComparer := cmp.Comparer(func(a, b string) bool {
+				return regexp.MustCompile(a).MatchString(b) || regexp.MustCompile(b).MatchString(a)
+			})
+
+			if diff := cmp.Diff(test.want, got, regexComparer); diff != "" {
+				t.Errorf("compute score command returned unexpected diff (-want +got): %s", diff)
+			}
+		})
+	}
+}
diff --git a/cmd/registry/cmd/compute/scorecard_test.go b/cmd/registry/cmd/compute/scorecard_test.go
new file mode 100644
index 0000000..3f87f8c
--- /dev/null
+++ b/cmd/registry/cmd/compute/scorecard_test.go
@@ -0,0 +1,266 @@
+package compute
+
+import (
+	"context"
+	"regexp"
+	"testing"
+
+	"github.com/apigee/registry/pkg/connection"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/test/seeder"
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/api/iterator"
+)
+
+const scoreCardDefinitionType = "application/octet-stream;type=google.cloud.apigeeregistry.v1.scoring.ScoreCardDefinition"
+const scoreType = "application/octet-stream;type=google.cloud.apigeeregistry.v1.scoring.Score"
+
+var (
+	scoreCardAll = &rpc.ScoreCardDefinition{
+		Id:   "lint-summary",
+		Kind: "ScoreCardDefinition",
+		TargetResource: &rpc.ResourcePattern{
+			Pattern: "apis/-/versions/-/specs/-",
+		},
+		ScorePatterns: []string{
+			"$resource.spec/artifacts/score-lint-error",
+		},
+	}
+
+	scoreCardOpenAPI = &rpc.ScoreCardDefinition{
+		Id:   "lint-summary-openapi",
+		Kind: "ScoreCardDefinition",
+		TargetResource: &rpc.ResourcePattern{
+			Pattern: "apis/-/versions/-/specs/-",
+			Filter:  "mime_type.contains('openapi')",
+		},
+		ScorePatterns: []string{
+			"$resource.spec/artifacts/score-lint-error",
+		},
+	}
+
+	scoreCardProto = &rpc.ScoreCardDefinition{
+		Id:   "lint-summary-proto",
+		Kind: "ScoreCardDefinition",
+		TargetResource: &rpc.ResourcePattern{
+			Pattern: "apis/-/versions/-/specs/-",
+			Filter:  "mime_type.contains('protobuf')",
+		},
+		ScorePatterns: []string{
+			"$resource.spec/artifacts/score-lint-error",
+		},
+	}
+
+	scoreLintError = &rpc.Score{
+		Id:   "score-lint-error",
+		Kind: "Score",
+		Value: &rpc.Score_IntegerValue{
+			IntegerValue: &rpc.IntegerValue{
+				Value:    1,
+				MinValue: 0,
+				MaxValue: 10,
+			},
+		},
+	}
+)
+
+func TestScoreCard(t *testing.T) {
+	tests := []struct {
+		desc string
+		seed []seeder.RegistryResource
+		want []string
+	}{
+		{
+			desc: "all spec scores",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: scoreType,
+					Contents: protoMarshal(scoreLintError),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: scoreType,
+					Contents: protoMarshal(scoreLintError),
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/artifacts/lint-summary",
+					MimeType: scoreCardDefinitionType,
+					Contents: protoMarshal(scoreCardAll),
+				},
+			},
+			want: []string{
+				"projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml@([a-z0-9-]+)/artifacts/scorecard-lint-summary",
+				"projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml@([a-z0-9-]+)/artifacts/scorecard-lint-summary",
+			},
+		},
+		{
+			desc: "only openapi scores with single definition",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: scoreType,
+					Contents: protoMarshal(scoreLintError),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml",
+					MimeType: gzipProtobuf,
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml/artifacts/score-lint-error",
+					MimeType: scoreType,
+					Contents: protoMarshal(scoreLintError),
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/artifacts/lint-summary-openapi",
+					MimeType: scoreCardDefinitionType,
+					Contents: protoMarshal(scoreCardOpenAPI),
+				},
+			},
+			want: []string{
+				"projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml@([a-z0-9-]+)/artifacts/scorecard-lint-summary-openapi",
+			},
+		},
+		{
+			desc: "only proto scores with single definition",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: scoreType,
+					Contents: protoMarshal(scoreLintError),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml",
+					MimeType: gzipProtobuf,
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml/artifacts/score-lint-error",
+					MimeType: scoreType,
+					Contents: protoMarshal(scoreLintError),
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/artifacts/lint-summary-proto",
+					MimeType: scoreCardDefinitionType,
+					Contents: protoMarshal(scoreCardProto),
+				},
+			},
+			want: []string{
+				"projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml@([a-z0-9-]+)/artifacts/scorecard-lint-summary-proto",
+			},
+		},
+		{
+			desc: "proto and openapi scores with both definitions",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: scoreType,
+					Contents: protoMarshal(scoreLintError),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml",
+					MimeType: gzipProtobuf,
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml/artifacts/score-lint-error",
+					MimeType: scoreType,
+					Contents: protoMarshal(scoreLintError),
+				},
+
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/artifacts/lint-summary-openapi",
+					MimeType: scoreCardDefinitionType,
+					Contents: protoMarshal(scoreCardOpenAPI),
+				},
+				&rpc.Artifact{
+					Name:     "projects/scorecard-test/locations/global/artifacts/lint-summary-proto",
+					MimeType: scoreCardDefinitionType,
+					Contents: protoMarshal(scoreCardProto),
+				},
+			},
+			want: []string{
+				"projects/scorecard-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml@([a-z0-9-]+)/artifacts/scorecard-lint-summary-openapi",
+				"projects/scorecard-test/locations/global/apis/petstore/versions/1.0.1/specs/proto.yaml@([a-z0-9-]+)/artifacts/scorecard-lint-summary-proto",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			registryClient, err := connection.NewRegistryClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			t.Cleanup(func() { registryClient.Close() })
+
+			adminClient, err := connection.NewAdminClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			t.Cleanup(func() { adminClient.Close() })
+
+			deleteProject(ctx, adminClient, t, "scorecard-test")
+			t.Cleanup(func() { deleteProject(ctx, adminClient, t, "scorecard-test") })
+
+			client := seeder.Client{
+				RegistryClient: registryClient,
+				AdminClient:    adminClient,
+			}
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			// setup the score command
+			scoreCardCmd := Command()
+			args := []string{"scorecard", "projects/scorecard-test/locations/global/apis/-/versions/-/specs/-"}
+			scoreCardCmd.SetArgs(args)
+
+			if err = scoreCardCmd.Execute(); err != nil {
+				t.Fatalf("Execute() with args %v returned error: %s", args, err)
+			}
+
+			// list scorecard artifacts
+			it := registryClient.ListArtifacts(ctx, &rpc.ListArtifactsRequest{
+				Parent: "projects/scorecard-test/locations/global/apis/-/versions/-/specs/-",
+				Filter: "mime_type.contains('ScoreCard')",
+			})
+
+			got := []string{}
+			for a, err := it.Next(); err != iterator.Done; a, err = it.Next() {
+				if err != nil {
+					break
+				}
+				got = append(got, a.GetName())
+			}
+
+			regexComparer := cmp.Comparer(func(a, b string) bool {
+				return regexp.MustCompile(a).MatchString(b) || regexp.MustCompile(b).MatchString(a)
+			})
+
+			if diff := cmp.Diff(test.want, got, regexComparer); diff != "" {
+				t.Errorf("compute scorecard command returned unexpected diff (-want +got): %s", diff)
+			}
+		})
+	}
+}
diff --git a/cmd/registry/scoring/parser_test.go b/cmd/registry/scoring/parser_test.go
index 2f5d682..84ebf3f 100644
--- a/cmd/registry/scoring/parser_test.go
+++ b/cmd/registry/scoring/parser_test.go
@@ -1605,6 +1605,7 @@ func TestValidateScoreCardDefinition(t *testing.T) {
 				Kind: "ScoreCardDefinition",
 				TargetResource: &rpc.ResourcePattern{
 					Pattern: "apis/-/versions/-/specs/-",
+					Filter:  "name.contains('openapi.yaml')",
 				},
 				ScorePatterns: []string{
 					"$resource.spec/artifacts/score-lint-error",
@@ -1616,7 +1617,7 @@ func TestValidateScoreCardDefinition(t *testing.T) {
 		},
 		// errors
 		{
-			desc:   "invalid target_resource",
+			desc:   "invalid target_resource pattern",
 			parent: "projects/demo/locations/global",
 			scoreCardDefinition: &rpc.ScoreCardDefinition{
 				Id:   "test-scorecard-definition",
@@ -1722,63 +1723,100 @@ func TestValidateScoreCardDefinition(t *testing.T) {
 	}
 }
 
-func TestMatchResourceWithTarget(t *testing.T) {
+func TestGenerateCombinedPattern(t *testing.T) {
 	tests := []struct {
 		desc          string
 		targetPattern *rpc.ResourcePattern
-		resourceName  string
+		inputPattern  string
+		wantPattern   string
 		wantErr       bool
 	}{
 		{
-			desc: "spec pattern",
+			desc: "spec pattern spec input",
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/-/versions/-/specs/-",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			wantPattern:  "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
 		},
 		{
-			desc: "specific api match",
+			desc: "spec pattern collection input",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/-",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			wantPattern:  "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+		},
+		{
+			desc: "specific api match spec input",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/petstore/versions/-/specs/-",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			wantPattern:  "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+		},
+		{
+			desc: "specific api match collection input",
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/petstore/versions/-/specs/-",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			wantPattern:  "projects/pattern-test/locations/global/apis/petstore/versions/-/specs/-",
 		},
 		{
-			desc: "specific api no match",
+			desc: "specific api no match spec input",
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/test/versions/-/specs/-",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
 			wantErr:      true,
 		},
 		{
-			desc: "specific version match",
+			desc: "specific version match spec input",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/1.0.0/specs/-",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			wantPattern:  "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+		},
+		{
+			desc: "specific version match collection input",
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/-/versions/1.0.0/specs/-",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			wantPattern:  "projects/pattern-test/locations/global/apis/-/versions/1.0.0/specs/-",
 		},
 		{
 			desc: "specific version no match",
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/-/versions/2.0.0/specs/-",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
 			wantErr:      true,
 		},
 		{
-			desc: "specific spec match",
+			desc: "specific spec match spec input",
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/-/versions/-/specs/openapi.yaml",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			wantPattern:  "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+		},
+		{
+			desc: "specific spec match collection input",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/openapi.yaml",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			wantPattern:  "projects/pattern-test/locations/global/apis/-/versions/-/specs/openapi.yaml",
 		},
 		{
 			desc: "specific spec no match",
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/-/versions/-/specs/swagger.yaml",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
 			wantErr:      true,
 		},
 		{
@@ -1786,7 +1824,7 @@ func TestMatchResourceWithTarget(t *testing.T) {
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/-/versions/-/specs/-/artifacts/lint-spectral",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
 			wantErr:      true,
 		},
 		{
@@ -1794,21 +1832,101 @@ func TestMatchResourceWithTarget(t *testing.T) {
 			targetPattern: &rpc.ResourcePattern{
 				Pattern: "apis/-/versions/-/specs/-",
 			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0",
+			inputPattern: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0",
 			wantErr:      true,
 		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.desc, func(t *testing.T) {
-			resourceName, _ := patterns.ParseResourcePattern(test.resourceName)
-			gotErr := matchResourceWithTarget(test.targetPattern, resourceName, "projects/pattern-test/locations/global")
+			inputPatternName, err := patterns.ParseResourcePattern(test.inputPattern)
+			if err != nil {
+				t.Errorf("invalid test.inputPattern %q", test.inputPattern)
+			}
+			gotPattern, _, gotErr := GenerateCombinedPattern(test.targetPattern, inputPatternName, "")
+			if gotPattern != test.wantPattern {
+				t.Errorf("GenerateCombinedPattern(%s, %s,\"\") returned unexpected output, want: %q, got: %q ", test.targetPattern, inputPatternName.String(), test.wantPattern, gotPattern)
+			}
+
 			if test.wantErr && gotErr == nil {
-				t.Errorf("matchResourceWithTarget(%s, %v, %s) did not return an error", test.targetPattern, resourceName, "projects/pattern-test/locations/global")
+				t.Errorf("GenerateCombinedPattern(%s, %s, \"\") did not return an error", test.targetPattern, inputPatternName.String())
 			}
 
 			if !test.wantErr && gotErr != nil {
-				t.Errorf("matchResourceWithTarget() returned unexpected error: %s", gotErr)
+				t.Errorf("GenerateCombinedPattern() returned unexpected error: %s", gotErr)
+			}
+		})
+	}
+}
+
+func TestGenerateCombinedPatternFilters(t *testing.T) {
+	tests := []struct {
+		desc          string
+		targetPattern *rpc.ResourcePattern
+		inputPattern  string
+		inputFilter   string
+		wantFilter    string
+	}{
+		{
+			desc: "input filter only",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/-",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			inputFilter:  "mime_type.contains('openapi')",
+			wantFilter:   "mime_type.contains('openapi')",
+		},
+		{
+			desc: "target filter only",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/-",
+				Filter:  "mime_type.contains('openapi')",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			inputFilter:  "",
+			wantFilter:   "mime_type.contains('openapi')",
+		},
+		{
+			desc: "target and input filter identical",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/-",
+				Filter:  "mime_type.contains('openapi')",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			inputFilter:  "mime_type.contains('openapi')",
+			wantFilter:   "mime_type.contains('openapi')",
+		},
+		{
+			desc: "target and input filter empty",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/-",
+				Filter:  "",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			inputFilter:  "",
+			wantFilter:   "",
+		},
+		{
+			desc: "target and input filter different",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/-",
+				Filter:  "mime_type.contains('openapi')",
+			},
+			inputPattern: "projects/pattern-test/locations/global/apis/-/versions/-/specs/-",
+			inputFilter:  "mime_type.contains('protobuf')",
+			wantFilter:   "(mime_type.contains('openapi')) && (mime_type.contains('protobuf'))",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			inputPatternName, err := patterns.ParseResourcePattern(test.inputPattern)
+			if err != nil {
+				t.Errorf("invalid test.inputPattern %q", test.inputPattern)
+			}
+			_, gotFilter, _ := GenerateCombinedPattern(test.targetPattern, inputPatternName, test.inputFilter)
+			if gotFilter != test.wantFilter {
+				t.Errorf("GenerateCombinedPattern(%s, %s,\"\") returned unexpected output, want: %q, got: %q ", test.targetPattern, inputPatternName.String(), test.wantFilter, gotFilter)
 			}
 		})
 	}