Repository: apigee/registry
Pull request #569 (2022-05-17 17:55:26)

Add support for ScoreCard generation
In this PR:
Support for generating Scorecards

TODOs:
- `dry_run` flag across both the commands
- seeder test pattern for both commands

codecov[bot]: # [Codecov](https://codecov.io/gh/apigee/registry/pull/569?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) Report
> Merging [#569](https://codecov.io/gh/apigee/registry/pull/569?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (7e8b894) into [main](https://codecov.io/gh/apigee/registry/commit/1583969bc4b6ef504d75d89e7cb888f4f458b059?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (1583969) will **increase** coverage by `0.02%`.
> The diff coverage is `43.63%`.

> :exclamation: Current head 7e8b894 differs from pull request most recent head e3ac4ac. Consider uploading reports for the commit e3ac4ac to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #569      +/-   ##
==========================================
+ Coverage   42.05%   42.07%   +0.02%     
==========================================
  Files          73       75       +2     
  Lines        8759     8933     +174     
==========================================
+ Hits         3684     3759      +75     
- Misses       4649     4740      +91     
- Partials      426      434       +8     
```


| [Impacted Files](https://codecov.io/gh/apigee/registry/pull/569?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/compute.go](https://codecov.io/gh/apigee/registry/pull/569/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL2NvbXB1dGUuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/score.go](https://codecov.io/gh/apigee/registry/pull/569/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/569/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/scoring/score.go](https://codecov.io/gh/apigee/registry/pull/569/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3Jpbmcvc2NvcmUuZ28=) | `71.12% <ø> (+2.94%)` | :arrow_up: |
| [cmd/registry/scoring/parser.go](https://codecov.io/gh/apigee/registry/pull/569/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3JpbmcvcGFyc2VyLmdv) | `87.16% <46.66%> (-10.07%)` | :arrow_down: |
| [cmd/registry/scoring/scorecard.go](https://codecov.io/gh/apigee/registry/pull/569/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3Jpbmcvc2NvcmVjYXJkLmdv) | `56.81% <56.81%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/apigee/registry/pull/569?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/apigee/registry/pull/569?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee). Last update [1583969...e3ac4ac](https://codecov.io/gh/apigee/registry/pull/569?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee).

shrutiparabgoogle: > LGTM - not blocking, but it would be useful to see some examples that are outside of the boilerplate of our test infrastructure and closer to the the actual user experience (YAML to YAML instead of test structs to test structs)

Yes, currently the examples that we have are through the unit and functional tests. I can create another e2e test which provide examples and also test the whole flow.

Patch:
diff --git a/cmd/registry/cmd/compute/compute.go b/cmd/registry/cmd/compute/compute.go
index b4ab248..40b1c89 100644
--- a/cmd/registry/cmd/compute/compute.go
+++ b/cmd/registry/cmd/compute/compute.go
@@ -30,6 +30,7 @@ func Command() *cobra.Command {
 	cmd.AddCommand(lintStatsCommand())
 	cmd.AddCommand(referencesCommand())
 	cmd.AddCommand(scoreCommand())
+	cmd.AddCommand(scoreCardCommand())
 	cmd.AddCommand(vocabularyCommand())
 
 	cmd.PersistentFlags().String("filter", "", "Filter selected resources")
diff --git a/cmd/registry/cmd/compute/score.go b/cmd/registry/cmd/compute/score.go
index 60e558e..ce20c6d 100644
--- a/cmd/registry/cmd/compute/score.go
+++ b/cmd/registry/cmd/compute/score.go
@@ -1,3 +1,17 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package compute
 
 import (
@@ -14,7 +28,7 @@ import (
 
 func scoreCommand() *cobra.Command {
 	return &cobra.Command{
-		Use:   "score (wip)",
+		Use:   "score",
 		Short: "Compute scores for APIs and API specs",
 		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
@@ -46,7 +60,6 @@ func scoreCommand() *cobra.Command {
 				}
 				for _, d := range scoreDefinitions {
 					taskQueue <- &computeScoreTask{
-						ctx:         ctx,
 						client:      client,
 						defArtifact: d,
 						resource:    r,
@@ -60,7 +73,6 @@ func scoreCommand() *cobra.Command {
 }
 
 type computeScoreTask struct {
-	ctx         context.Context
 	client      connection.Client
 	defArtifact *rpc.Artifact
 	resource    patterns.ResourceInstance
@@ -71,5 +83,5 @@ func (task *computeScoreTask) String() string {
 }
 
 func (task *computeScoreTask) Run(ctx context.Context) error {
-	return scoring.CalculateScore(task.ctx, task.client, task.defArtifact, task.resource)
+	return scoring.CalculateScore(ctx, task.client, task.defArtifact, task.resource)
 }
diff --git a/cmd/registry/cmd/compute/scorecard.go b/cmd/registry/cmd/compute/scorecard.go
new file mode 100644
index 0000000..ecadefe
--- /dev/null
+++ b/cmd/registry/cmd/compute/scorecard.go
@@ -0,0 +1,87 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compute
+
+import (
+	"context"
+
+	"github.com/apigee/registry/cmd/registry/core"
+	"github.com/apigee/registry/cmd/registry/patterns"
+	"github.com/apigee/registry/cmd/registry/scoring"
+	"github.com/apigee/registry/connection"
+	"github.com/apigee/registry/log"
+	"github.com/apigee/registry/rpc"
+	"github.com/spf13/cobra"
+)
+
+func scoreCardCommand() *cobra.Command {
+	return &cobra.Command{
+		Use:   "scorecard",
+		Short: "Compute score cards for APIs and API specs",
+		Args:  cobra.ExactArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			ctx := cmd.Context()
+			filter, err := cmd.Flags().GetString("filter")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
+			}
+
+			client, err := connection.NewClient(ctx)
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
+			}
+			// Initialize task queue.
+			taskQueue, wait := core.WorkerPool(ctx, 64)
+			defer wait()
+
+			resources, err := patterns.ListResources(ctx, client, args[0], filter)
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to list resources")
+			}
+
+			for _, r := range resources {
+				// Fetch the ScoreCardDefinitions which can be applied to this resource
+				scoreCardDefinitions, err := scoring.FetchScoreCardDefinitions(ctx, client, r.ResourceName())
+				if err != nil {
+					log.FromContext(ctx).WithError(err).Errorf("Skipping resource %q", r.ResourceName())
+					continue
+				}
+				for _, d := range scoreCardDefinitions {
+					taskQueue <- &computeScoreCardTask{
+						client:      client,
+						defArtifact: d,
+						resource:    r,
+					}
+				}
+			}
+
+			return
+		},
+	}
+}
+
+type computeScoreCardTask struct {
+	client      connection.Client
+	defArtifact *rpc.Artifact
+	resource    patterns.ResourceInstance
+}
+
+func (task *computeScoreCardTask) String() string {
+	return "compute score " + task.resource.ResourceName().String()
+}
+
+func (task *computeScoreCardTask) Run(ctx context.Context) error {
+	return scoring.CalculateScoreCard(ctx, task.client, task.defArtifact, task.resource)
+}
diff --git a/cmd/registry/patterns/parser.go b/cmd/registry/patterns/parser.go
index 375672c..5470321 100644
--- a/cmd/registry/patterns/parser.go
+++ b/cmd/registry/patterns/parser.go
@@ -1,3 +1,17 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package patterns
 
 import (
diff --git a/cmd/registry/scoring/parser.go b/cmd/registry/scoring/parser.go
index 75952e2..7dc5046 100644
--- a/cmd/registry/scoring/parser.go
+++ b/cmd/registry/scoring/parser.go
@@ -257,3 +257,60 @@ 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()))
+	if err != nil {
+		return err
+	}
+
+	switch tp := targetPatternName.(type) {
+	case patterns.SpecName:
+		// Check if targetPattern and resourceName match in type
+		r, ok := resourceName.(patterns.SpecName)
+		if !ok {
+			return fmt.Errorf("resource %s doesn't match target pattern %s", r, 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)
+		}
+		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)
+		}
+		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)
+		}
+	case patterns.VersionName:
+		// Check if targetPattern and resourceName match in type
+		r, ok := resourceName.(patterns.VersionName)
+		if !ok {
+			return fmt.Errorf("resource %s doesn't match target pattern %s", r, 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)
+		}
+		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)
+		}
+	case patterns.ApiName:
+		// Check if targetPattern and resourceName match in type
+		r, ok := resourceName.(patterns.ApiName)
+		if !ok {
+			return fmt.Errorf("resource %s doesn't match target pattern %s", r, 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)
+		}
+	default:
+		return fmt.Errorf("unsupported resource type %T", targetPatternName)
+	}
+
+	// TODO: Filter check
+	return nil
+}
diff --git a/cmd/registry/scoring/score.go b/cmd/registry/scoring/score.go
index 08d2448..997ccac 100644
--- a/cmd/registry/scoring/score.go
+++ b/cmd/registry/scoring/score.go
@@ -72,63 +72,6 @@ func FetchScoreDefinitions(
 	return defArtifacts, nil
 }
 
-func matchResourceWithTarget(targetPattern *rpc.ResourcePattern, resourceName patterns.ResourceName, project string) error {
-	targetPatternName, err := patterns.ParseResourcePattern(fmt.Sprintf("%s/%s", project, targetPattern.GetPattern()))
-	if err != nil {
-		return err
-	}
-
-	switch tp := targetPatternName.(type) {
-	case patterns.SpecName:
-		// Check if targetPattern and resourceName match in type
-		r, ok := resourceName.(patterns.SpecName)
-		if !ok {
-			return fmt.Errorf("resource %s doesn't match target pattern %s", r, 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)
-		}
-		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)
-		}
-		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)
-		}
-	case patterns.VersionName:
-		// Check if targetPattern and resourceName match in type
-		r, ok := resourceName.(patterns.VersionName)
-		if !ok {
-			return fmt.Errorf("resource %s doesn't match target pattern %s", r, 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)
-		}
-		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)
-		}
-	case patterns.ApiName:
-		// Check if targetPattern and resourceName match in type
-		r, ok := resourceName.(patterns.ApiName)
-		if !ok {
-			return fmt.Errorf("resource %s doesn't match target pattern %s", r, 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)
-		}
-	default:
-		return fmt.Errorf("unsupported resource type %T", targetPatternName)
-	}
-
-	// TODO: Filter check
-	return nil
-}
-
 func CalculateScore(
 	ctx context.Context,
 	client connection.Client,
@@ -190,7 +133,7 @@ type scoreResult struct {
 	// Represents the value generated by the expression
 	// Supported types are: int64, float64, bool
 	value interface{}
-	// Represents if the final scoreArtifact needs an updated
+	// Represents if the final scoreArtifact needs an update
 	// This is determined based on the timestamps of the existing scoreArtifact and the dependent artifacts in score_formula
 	needsUpdate bool
 	// Represents the error generated while applying the score_expression.
diff --git a/cmd/registry/scoring/scorecard.go b/cmd/registry/scoring/scorecard.go
new file mode 100644
index 0000000..f78ac5c
--- /dev/null
+++ b/cmd/registry/scoring/scorecard.go
@@ -0,0 +1,221 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package scoring
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/apigee/registry/cmd/registry/core"
+	"github.com/apigee/registry/cmd/registry/patch"
+	"github.com/apigee/registry/cmd/registry/patterns"
+	"github.com/apigee/registry/connection"
+	"github.com/apigee/registry/log"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+	"google.golang.org/protobuf/proto"
+)
+
+func scoreCardID(definitionID string) string {
+	return fmt.Sprintf("scorecard-%s", definitionID)
+}
+
+func FetchScoreCardDefinitions(
+	ctx context.Context,
+	client connection.Client,
+	resource patterns.ResourceName) ([]*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))
+	if err != nil {
+		return nil, err
+	}
+	listFilter := fmt.Sprintf("mime_type == %q", patch.MimeTypeForKind("ScoreCardDefinition"))
+	err = core.ListArtifacts(ctx, client, artifact, listFilter, true,
+		func(artifact *rpc.Artifact) error {
+			definition := &rpc.ScoreCardDefinition{}
+			if err := proto.Unmarshal(artifact.GetContents(), definition); err != nil {
+				return err
+			}
+
+			// Check if ScoreCardDefinition.TargetResource matches with the supplied resource
+			err := matchResourceWithTarget(definition.GetTargetResource(), resource, project)
+			if err != nil {
+				return err
+			}
+
+			defArtifacts = append(defArtifacts, artifact)
+			return nil
+		})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return defArtifacts, nil
+}
+
+func CalculateScoreCard(
+	ctx context.Context,
+	client connection.Client,
+	defArtifact *rpc.Artifact,
+	resource patterns.ResourceInstance) error {
+	project := fmt.Sprintf("%s/locations/global", resource.ResourceName().Project())
+
+	// Extract definition
+	definition := &rpc.ScoreCardDefinition{}
+	if err := proto.Unmarshal(defArtifact.GetContents(), definition); err != nil {
+		return err
+	}
+
+	var takeAction bool
+
+	// Fetch the to be generated ScoreCard artifact (if present)
+	artifactName := fmt.Sprintf("%s/artifacts/%s", resource.ResourceName().String(), scoreCardID(definition.GetId()))
+	scoreCardArtifact, err := getArtifact(ctx, client, artifactName, false)
+	if err != nil {
+		// Generate ScoreCard if the ScoreCard artifact doesn't exist
+		if status.Code(err) == codes.NotFound {
+			takeAction = true
+		} else {
+			return fmt.Errorf("failed to fetch artifact %q: %s", artifactName, err)
+		}
+	}
+
+	// Generate ScoreCard if the definition has been updated
+	if scoreCardArtifact != nil && scoreCardArtifact.GetUpdateTime().AsTime().Before(defArtifact.GetUpdateTime().AsTime()) {
+		takeAction = true
+	}
+
+	result := processScorePatterns(ctx, client, definition, resource, scoreCardArtifact, takeAction, project)
+	if result.err != nil {
+		return err
+	}
+
+	if result.needsUpdate {
+		// TODO: Add dry_run flag
+
+		// upload the scoreCard to registry
+		err = uploadScoreCard(ctx, client, resource, result.scoreCard)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// Response returned after fetching all the scoreArtifacts to form a ScoreCard.
+type scoreCardResult struct {
+	// Represents the ScoreCard generated after fetching all the score artifacts from the score_patterns.
+	scoreCard *rpc.ScoreCard
+	// Represents if the final scoreCardArtifact needs an update
+	// This is determined based on the timestamps of the existing scoreCardArtifact and the dependent scoreArtifacts fetched from score_patterns.
+	needsUpdate bool
+	// Represents the error encountered while generating the ScoreCard.
+	err error
+}
+
+func processScorePatterns(
+	ctx context.Context,
+	client connection.Client,
+	definition *rpc.ScoreCardDefinition,
+	resource patterns.ResourceInstance,
+	scoreCardArtifact *rpc.Artifact,
+	takeAction bool,
+	project string) scoreCardResult {
+	var needsUpdate bool
+	scoreArtifacts := make([]*rpc.Score, 0)
+
+	for _, scorePattern := range definition.GetScorePatterns() {
+		extendedPattern, err := patterns.SubstituteReferenceEntity(scorePattern, resource.ResourceName())
+		if err != nil {
+			return scoreCardResult{
+				scoreCard:   nil,
+				needsUpdate: false,
+				err:         fmt.Errorf("invalid pattern %q in score_patterns: %s", scorePattern, err),
+			}
+		}
+
+		// Fetch scoreArtifact
+		artifact, err := getArtifact(ctx, client, extendedPattern.String(), true)
+		if err != nil {
+			return scoreCardResult{
+				scoreCard:   nil,
+				needsUpdate: false,
+				err:         fmt.Errorf("failed to fetch artifact %s: %s", extendedPattern.String(), err),
+			}
+		}
+
+		// needsUpdate tells the calling function if the ScoreCard artifact needs to be updated
+		needsUpdate = needsUpdate || takeAction || scoreCardArtifact.GetUpdateTime().AsTime().Before(artifact.GetUpdateTime().AsTime())
+		// Extract Score from the fetched artifact
+		score := &rpc.Score{}
+		if err := proto.Unmarshal(artifact.GetContents(), score); err != nil {
+			return scoreCardResult{
+				scoreCard:   nil,
+				needsUpdate: false,
+				err:         fmt.Errorf("failed unmarshaling artifact %q into Score proto: %s", artifact.GetName(), err),
+			}
+		}
+
+		scoreArtifacts = append(scoreArtifacts, score)
+	}
+
+	if needsUpdate {
+		// Build the final ScoreCard proto
+		scoreCard := &rpc.ScoreCard{
+			Id:             scoreCardID(definition.GetId()),
+			Kind:           "ScoreCard",
+			DisplayName:    definition.GetDisplayName(),
+			Description:    definition.GetDescription(),
+			DefinitionName: fmt.Sprintf("%s/artifacts/%s", project, definition.GetId()),
+			Scores:         scoreArtifacts,
+		}
+
+		return scoreCardResult{
+			scoreCard:   scoreCard,
+			needsUpdate: true,
+			err:         nil,
+		}
+	}
+
+	return scoreCardResult{
+		scoreCard:   nil,
+		needsUpdate: false,
+		err:         nil,
+	}
+}
+
+func uploadScoreCard(ctx context.Context, client connection.Client, resource patterns.ResourceInstance, scoreCard *rpc.ScoreCard) error {
+	artifactBytes, err := proto.Marshal(scoreCard)
+	if err != nil {
+		return err
+	}
+	artifact := &rpc.Artifact{
+		Name:     fmt.Sprintf("%s/artifacts/%s", resource.ResourceName().String(), scoreCard.GetId()),
+		Contents: artifactBytes,
+		MimeType: patch.MimeTypeForKind("Score"),
+	}
+	log.Debugf(ctx, "Uploading %s", artifact.GetName())
+	if err = core.SetArtifact(ctx, client, artifact); err != nil {
+		return fmt.Errorf("failed to save artifact %s: %s", artifact.GetName(), err)
+	}
+
+	return nil
+}


Test patch:
diff --git a/cmd/registry/patterns/parser_test.go b/cmd/registry/patterns/parser_test.go
index 89fcb08..23e32ef 100644
--- a/cmd/registry/patterns/parser_test.go
+++ b/cmd/registry/patterns/parser_test.go
@@ -1,3 +1,17 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package patterns
 
 import (
diff --git a/cmd/registry/scoring/parser_test.go b/cmd/registry/scoring/parser_test.go
index dceda35..2f5d682 100644
--- a/cmd/registry/scoring/parser_test.go
+++ b/cmd/registry/scoring/parser_test.go
@@ -1721,3 +1721,95 @@ func TestValidateScoreCardDefinition(t *testing.T) {
 		})
 	}
 }
+
+func TestMatchResourceWithTarget(t *testing.T) {
+	tests := []struct {
+		desc          string
+		targetPattern *rpc.ResourcePattern
+		resourceName  string
+		wantErr       bool
+	}{
+		{
+			desc: "spec pattern",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/-",
+			},
+			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+		},
+		{
+			desc: "specific api match",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/petstore/versions/-/specs/-",
+			},
+			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+		},
+		{
+			desc: "specific api no match",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/test/versions/-/specs/-",
+			},
+			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+			wantErr:      true,
+		},
+		{
+			desc: "specific version match",
+			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",
+		},
+		{
+			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",
+			wantErr:      true,
+		},
+		{
+			desc: "specific spec match",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/openapi.yaml",
+			},
+			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/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",
+			wantErr:      true,
+		},
+		{
+			desc: "artifact pattern error",
+			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",
+			wantErr:      true,
+		},
+		{
+			desc: "target and resource mismatch",
+			targetPattern: &rpc.ResourcePattern{
+				Pattern: "apis/-/versions/-/specs/-",
+			},
+			resourceName: "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")
+			if test.wantErr && gotErr == nil {
+				t.Errorf("matchResourceWithTarget(%s, %v, %s) did not return an error", test.targetPattern, resourceName, "projects/pattern-test/locations/global")
+			}
+
+			if !test.wantErr && gotErr != nil {
+				t.Errorf("matchResourceWithTarget() returned unexpected error: %s", gotErr)
+			}
+		})
+	}
+}
diff --git a/cmd/registry/scoring/score_test.go b/cmd/registry/scoring/score_test.go
index bc1d9e8..aed5ebf 100644
--- a/cmd/registry/scoring/score_test.go
+++ b/cmd/registry/scoring/score_test.go
@@ -504,7 +504,7 @@ func TestCalculateScore(t *testing.T) {
 			//fetch definition artifact
 			defArtifact, err := getArtifact(ctx, registryClient, "projects/score-formula-test/locations/global/artifacts/lint-error", true)
 			if err != nil {
-				t.Errorf("failed to fetch the scoreArtifact from setup: %s", err)
+				t.Errorf("failed to fetch the definition Artifact from setup: %s", err)
 			}
 
 			gotErr := CalculateScore(ctx, registryClient, defArtifact, resource)
@@ -534,98 +534,6 @@ func TestCalculateScore(t *testing.T) {
 	}
 }
 
-func TestMatchResourceWithTarget(t *testing.T) {
-	tests := []struct {
-		desc          string
-		targetPattern *rpc.ResourcePattern
-		resourceName  string
-		wantErr       bool
-	}{
-		{
-			desc: "spec pattern",
-			targetPattern: &rpc.ResourcePattern{
-				Pattern: "apis/-/versions/-/specs/-",
-			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
-		},
-		{
-			desc: "specific api match",
-			targetPattern: &rpc.ResourcePattern{
-				Pattern: "apis/petstore/versions/-/specs/-",
-			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
-		},
-		{
-			desc: "specific api no match",
-			targetPattern: &rpc.ResourcePattern{
-				Pattern: "apis/test/versions/-/specs/-",
-			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
-			wantErr:      true,
-		},
-		{
-			desc: "specific version match",
-			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",
-		},
-		{
-			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",
-			wantErr:      true,
-		},
-		{
-			desc: "specific spec match",
-			targetPattern: &rpc.ResourcePattern{
-				Pattern: "apis/-/versions/-/specs/openapi.yaml",
-			},
-			resourceName: "projects/pattern-test/locations/global/apis/petstore/versions/1.0.0/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",
-			wantErr:      true,
-		},
-		{
-			desc: "artifact pattern error",
-			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",
-			wantErr:      true,
-		},
-		{
-			desc: "target and resource mismatch",
-			targetPattern: &rpc.ResourcePattern{
-				Pattern: "apis/-/versions/-/specs/-",
-			},
-			resourceName: "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")
-			if test.wantErr && gotErr == nil {
-				t.Errorf("matchResourceWithTarget(%s, %v, %s) did not return an error", test.targetPattern, resourceName, "projects/pattern-test/locations/global")
-			}
-
-			if !test.wantErr && gotErr != nil {
-				t.Errorf("matchResourceWithTarget() returned unexpected error: %s", gotErr)
-			}
-		})
-	}
-}
-
 func TestProcessScoreFormula(t *testing.T) {
 	ctx := context.Background()
 	registryClient, err := connection.NewClient(ctx)
diff --git a/cmd/registry/scoring/scorecard_test.go b/cmd/registry/scoring/scorecard_test.go
new file mode 100644
index 0000000..c571b45
--- /dev/null
+++ b/cmd/registry/scoring/scorecard_test.go
@@ -0,0 +1,1379 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package scoring
+
+import (
+	"context"
+	"testing"
+
+	"github.com/apigee/registry/cmd/registry/patterns"
+	"github.com/apigee/registry/connection"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/testing/protocmp"
+)
+
+func TestCalculateScoreCard(t *testing.T) {
+	tests := []struct {
+		desc          string
+		setup         func(context.Context, connection.Client, connection.AdminClient)
+		wantScoreCard *rpc.ScoreCard
+	}{
+		{
+			desc: "non existent ScoreCard artifact",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-card-test")
+				createProject(ctx, adminClient, t, "score-card-test")
+				createApi(ctx, client, t, "projects/score-card-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-card-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-card-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// ScoreCard definition
+				artifactBytes, _ := proto.Marshal(&rpc.ScoreCardDefinition{
+					Id:          "quality",
+					Kind:        "ScoreCardDefinition",
+					DisplayName: "Quality",
+					Description: "Quality ScoreCard",
+					TargetResource: &rpc.ResourcePattern{
+						Pattern: "apis/-/versions/-/specs/-",
+					},
+					ScorePatterns: []string{
+						"$resource.spec/artifacts/score-lint-error",
+						"$resource.spec/artifacts/score-lang-reuse",
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/artifacts/quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCardDefinition")
+				// score lint-error
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+			},
+			wantScoreCard: &rpc.ScoreCard{
+				Id:             "scorecard-quality",
+				Kind:           "ScoreCard",
+				DisplayName:    "Quality",
+				Description:    "Quality ScoreCard",
+				DefinitionName: "projects/score-card-test/locations/global/artifacts/quality",
+				Scores: []*rpc.Score{
+					{
+						Id:             "score-lint-error",
+						Kind:           "Score",
+						DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+						Severity:       rpc.Severity_ALERT,
+						Value: &rpc.Score_PercentValue{
+							PercentValue: &rpc.PercentValue{
+								Value: 60,
+							},
+						},
+					},
+					{
+						Id:             "score-lang-reuse",
+						Kind:           "Score",
+						DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+						Severity:       rpc.Severity_OK,
+						Value: &rpc.Score_PercentValue{
+							PercentValue: &rpc.PercentValue{
+								Value: 70,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "updated definition",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-card-test")
+				createProject(ctx, adminClient, t, "score-card-test")
+				createApi(ctx, client, t, "projects/score-card-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-card-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-card-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// score lint-error
+				artifactBytes, _ := proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// scorecard quality
+				artifactBytes, _ = proto.Marshal(&rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 50,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard")
+				// ScoreCard definition
+				artifactBytes, _ = proto.Marshal(&rpc.ScoreCardDefinition{
+					Id:          "quality",
+					Kind:        "ScoreCardDefinition",
+					DisplayName: "Quality",
+					Description: "Quality ScoreCard",
+					TargetResource: &rpc.ResourcePattern{
+						Pattern: "apis/-/versions/-/specs/-",
+					},
+					ScorePatterns: []string{
+						"$resource.spec/artifacts/score-lint-error",
+						"$resource.spec/artifacts/score-lang-reuse",
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/artifacts/quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCardDefinition")
+			},
+			wantScoreCard: &rpc.ScoreCard{
+				Id:             "scorecard-quality",
+				Kind:           "ScoreCard",
+				DisplayName:    "Quality",
+				Description:    "Quality ScoreCard",
+				DefinitionName: "projects/score-card-test/locations/global/artifacts/quality",
+				Scores: []*rpc.Score{
+					{
+						Id:             "score-lint-error",
+						Kind:           "Score",
+						DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+						Severity:       rpc.Severity_ALERT,
+						Value: &rpc.Score_PercentValue{
+							PercentValue: &rpc.PercentValue{
+								Value: 60,
+							},
+						},
+					},
+					{
+						Id:             "score-lang-reuse",
+						Kind:           "Score",
+						DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+						Severity:       rpc.Severity_OK,
+						Value: &rpc.Score_PercentValue{
+							PercentValue: &rpc.PercentValue{
+								Value: 70,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "updated score artifacts",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-card-test")
+				createProject(ctx, adminClient, t, "score-card-test")
+				createApi(ctx, client, t, "projects/score-card-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-card-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-card-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// ScoreCard definition
+				artifactBytes, _ := proto.Marshal(&rpc.ScoreCardDefinition{
+					Id:          "quality",
+					Kind:        "ScoreCardDefinition",
+					DisplayName: "Quality",
+					Description: "Quality ScoreCard",
+					TargetResource: &rpc.ResourcePattern{
+						Pattern: "apis/-/versions/-/specs/-",
+					},
+					ScorePatterns: []string{
+						"$resource.spec/artifacts/score-lint-error",
+						"$resource.spec/artifacts/score-lang-reuse",
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/artifacts/quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCardDefinition")
+				// scorecard quality
+				artifactBytes, _ = proto.Marshal(&rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 50,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard")
+				// score lint-error
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+			},
+			wantScoreCard: &rpc.ScoreCard{
+				Id:             "scorecard-quality",
+				Kind:           "ScoreCard",
+				DisplayName:    "Quality",
+				Description:    "Quality ScoreCard",
+				DefinitionName: "projects/score-card-test/locations/global/artifacts/quality",
+				Scores: []*rpc.Score{
+					{
+						Id:             "score-lint-error",
+						Kind:           "Score",
+						DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+						Severity:       rpc.Severity_ALERT,
+						Value: &rpc.Score_PercentValue{
+							PercentValue: &rpc.PercentValue{
+								Value: 60,
+							},
+						},
+					},
+					{
+						Id:             "score-lang-reuse",
+						Kind:           "Score",
+						DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+						Severity:       rpc.Severity_OK,
+						Value: &rpc.Score_PercentValue{
+							PercentValue: &rpc.PercentValue{
+								Value: 70,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "updated definition and score artifacts",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-card-test")
+				createProject(ctx, adminClient, t, "score-card-test")
+				createApi(ctx, client, t, "projects/score-card-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-card-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-card-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// scorecard quality
+				artifactBytes, _ := proto.Marshal(&rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 50,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard")
+				// score lint-error
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// ScoreCard definition
+				artifactBytes, _ = proto.Marshal(&rpc.ScoreCardDefinition{
+					Id:          "quality",
+					Kind:        "ScoreCardDefinition",
+					DisplayName: "Quality",
+					Description: "Quality ScoreCard",
+					TargetResource: &rpc.ResourcePattern{
+						Pattern: "apis/-/versions/-/specs/-",
+					},
+					ScorePatterns: []string{
+						"$resource.spec/artifacts/score-lint-error",
+						"$resource.spec/artifacts/score-lang-reuse",
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-card-test/locations/global/artifacts/quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCardDefinition")
+			},
+			wantScoreCard: &rpc.ScoreCard{
+				Id:             "scorecard-quality",
+				Kind:           "ScoreCard",
+				DisplayName:    "Quality",
+				Description:    "Quality ScoreCard",
+				DefinitionName: "projects/score-card-test/locations/global/artifacts/quality",
+				Scores: []*rpc.Score{
+					{
+						Id:             "score-lint-error",
+						Kind:           "Score",
+						DefinitionName: "projects/score-card-test/locations/global/artifacts/lint-error",
+						Severity:       rpc.Severity_ALERT,
+						Value: &rpc.Score_PercentValue{
+							PercentValue: &rpc.PercentValue{
+								Value: 60,
+							},
+						},
+					},
+					{
+						Id:             "score-lang-reuse",
+						Kind:           "Score",
+						DefinitionName: "projects/score-card-test/locations/global/artifacts/lang-reuse",
+						Severity:       rpc.Severity_OK,
+						Value: &rpc.Score_PercentValue{
+							PercentValue: &rpc.PercentValue{
+								Value: 70,
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			registryClient, err := connection.NewClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			defer registryClient.Close()
+			adminClient, err := connection.NewAdminClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			defer adminClient.Close()
+
+			test.setup(ctx, registryClient, adminClient)
+
+			resource := patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-card-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			}
+
+			//fetch definition artifact
+			defArtifact, err := getArtifact(ctx, registryClient, "projects/score-card-test/locations/global/artifacts/quality", true)
+			if err != nil {
+				t.Errorf("failed to fetch the definition Artifact from setup: %s", err)
+			}
+
+			gotErr := CalculateScoreCard(ctx, registryClient, defArtifact, resource)
+			if gotErr != nil {
+				t.Errorf("CalculateScore(ctx, client, %v, %v) returned unexpected error: %s", defArtifact, resource, gotErr)
+			}
+
+			//fetch score artifact and check the value
+			scoreCardArtifact, err := getArtifact(
+				ctx, registryClient,
+				"projects/score-card-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality", true)
+			if err != nil {
+				t.Errorf("failed to get the result scoreCardArtifact from registry")
+			}
+
+			gotScoreCard := &rpc.ScoreCard{}
+			err = proto.Unmarshal(scoreCardArtifact.GetContents(), gotScoreCard)
+			if err != nil {
+				t.Errorf("failed unmarshalling ScoreCard artifact from registry: %s", err)
+			}
+
+			opts := cmp.Options{
+				protocmp.Transform(),
+				cmpopts.SortSlices(func(a, b string) bool { return a < b }),
+			}
+			if !cmp.Equal(test.wantScoreCard, gotScoreCard, opts) {
+				t.Errorf("CalculateScoreCard() returned unexpected response (-want +got):\n%s", cmp.Diff(test.wantScoreCard, gotScoreCard, opts))
+			}
+		})
+	}
+}
+
+func TestProcessScorePatterns(t *testing.T) {
+	tests := []struct {
+		desc       string
+		setup      func(context.Context, connection.Client, connection.AdminClient)
+		resource   patterns.ResourceInstance
+		takeAction bool
+		wantResult scoreCardResult
+	}{
+		{
+			desc: "takeAction and scoreCard is up-to-date",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-patterns-test")
+				createProject(ctx, adminClient, t, "score-patterns-test")
+				createApi(ctx, client, t, "projects/score-patterns-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// score lint-error
+				artifactBytes, _ := proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// ScoreCard quality
+				artifactBytes, _ = proto.Marshal(&rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard")
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-patterns-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: true,
+			wantResult: scoreCardResult{
+				scoreCard: &rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				},
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "takeAction and scoreCard is outdated",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-patterns-test")
+				createProject(ctx, adminClient, t, "score-patterns-test")
+				createApi(ctx, client, t, "projects/score-patterns-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// ScoreCard quality
+				artifactBytes, _ := proto.Marshal(&rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard")
+				// score lint-error
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-patterns-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: true,
+			wantResult: scoreCardResult{
+				scoreCard: &rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				},
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and scoreCard is up-to-date",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-patterns-test")
+				createProject(ctx, adminClient, t, "score-patterns-test")
+				createApi(ctx, client, t, "projects/score-patterns-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// score lint-error
+				artifactBytes, _ := proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// ScoreCard quality
+				artifactBytes, _ = proto.Marshal(&rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard")
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-patterns-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreCardResult{
+				scoreCard:   nil,
+				needsUpdate: false,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and scoreCard is outdated",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-patterns-test")
+				createProject(ctx, adminClient, t, "score-patterns-test")
+				createApi(ctx, client, t, "projects/score-patterns-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// ScoreCard quality
+				artifactBytes, _ := proto.Marshal(&rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard")
+				// score lint-error
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-patterns-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreCardResult{
+				scoreCard: &rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				},
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and scoreCard is partially outdated",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-patterns-test")
+				createProject(ctx, adminClient, t, "score-patterns-test")
+				createApi(ctx, client, t, "projects/score-patterns-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// score lint-error
+				artifactBytes, _ := proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+				// ScoreCard quality
+				artifactBytes, _ = proto.Marshal(&rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard")
+				// score lang-reuse
+				artifactBytes, _ = proto.Marshal(&rpc.Score{
+					Id:             "score-lang-reuse",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+					Severity:       rpc.Severity_OK,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 70,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-patterns-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreCardResult{
+				scoreCard: &rpc.ScoreCard{
+					Id:             "scorecard-quality",
+					Kind:           "ScoreCard",
+					DisplayName:    "Quality",
+					Description:    "Quality ScoreCard",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/quality",
+					Scores: []*rpc.Score{
+						{
+							Id:             "score-lint-error",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+							Severity:       rpc.Severity_ALERT,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 60,
+								},
+							},
+						},
+						{
+							Id:             "score-lang-reuse",
+							Kind:           "Score",
+							DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lang-reuse",
+							Severity:       rpc.Severity_OK,
+							Value: &rpc.Score_PercentValue{
+								PercentValue: &rpc.PercentValue{
+									Value: 70,
+								},
+							},
+						},
+					},
+				},
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			registryClient, err := connection.NewClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			defer registryClient.Close()
+			adminClient, err := connection.NewAdminClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			defer adminClient.Close()
+
+			test.setup(ctx, registryClient, adminClient)
+
+			definition := &rpc.ScoreCardDefinition{
+				Id:          "quality",
+				Kind:        "ScoreCardDefinition",
+				DisplayName: "Quality",
+				Description: "Quality ScoreCard",
+				TargetResource: &rpc.ResourcePattern{
+					Pattern: "apis/-/versions/-/specs/-",
+				},
+				ScorePatterns: []string{
+					"$resource.spec/artifacts/score-lint-error",
+					"$resource.spec/artifacts/score-lang-reuse",
+				},
+			}
+
+			//fetch the ScoreCard artifact
+			scoreCardArtifact, err := getArtifact(ctx, registryClient, "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality", false)
+			if err != nil {
+				t.Errorf("failed to fetch the scoreCardArtifact from setup: %s", err)
+			}
+
+			gotResult := processScorePatterns(ctx, registryClient, definition, test.resource, scoreCardArtifact, test.takeAction, "projects/score-patterns-test/locations/global")
+
+			opts := cmp.Options{
+				cmp.AllowUnexported(scoreCardResult{}),
+				protocmp.Transform(),
+				cmpopts.SortSlices(func(a, b string) bool { return a < b }),
+			}
+
+			if !cmp.Equal(test.wantResult, gotResult, opts) {
+				t.Errorf("processScorePatterns() returned unexpected response, (-want +got):\n%s", cmp.Diff(test.wantResult, gotResult, opts))
+			}
+		})
+	}
+}
+
+func TestProcessScorePatternsError(t *testing.T) {
+	tests := []struct {
+		desc       string
+		setup      func(context.Context, connection.Client, connection.AdminClient)
+		takeAction bool
+		definition *rpc.ScoreCardDefinition
+	}{
+		{
+			desc:       "Invalid reference pattern in ScoreCardDefinition",
+			setup:      func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {},
+			takeAction: true,
+			definition: &rpc.ScoreCardDefinition{
+				Id:          "quality",
+				Kind:        "ScoreCardDefinition",
+				DisplayName: "Quality",
+				Description: "Quality ScoreCard",
+				TargetResource: &rpc.ResourcePattern{
+					Pattern: "apis/-/versions/-/specs/-",
+				},
+				ScorePatterns: []string{
+					"$resource.spec/artifact/score-lint-error",
+					"$resource.spec/artifact/score-lang-reuse",
+				},
+			},
+		},
+		{
+			desc: "Missing score artifact",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-patterns-test")
+				createProject(ctx, adminClient, t, "score-patterns-test")
+				createApi(ctx, client, t, "projects/score-patterns-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// score lint-error
+				artifactBytes, _ := proto.Marshal(&rpc.Score{
+					Id:             "score-lint-error",
+					Kind:           "Score",
+					DefinitionName: "projects/score-patterns-test/locations/global/artifacts/lint-error",
+					Severity:       rpc.Severity_ALERT,
+					Value: &rpc.Score_PercentValue{
+						PercentValue: &rpc.PercentValue{
+							Value: 60,
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+			},
+			takeAction: true,
+			definition: &rpc.ScoreCardDefinition{
+				Id:          "quality",
+				Kind:        "ScoreCardDefinition",
+				DisplayName: "Quality",
+				Description: "Quality ScoreCard",
+				TargetResource: &rpc.ResourcePattern{
+					Pattern: "apis/-/versions/-/specs/-",
+				},
+				ScorePatterns: []string{
+					"$resource.spec/artifact/score-lint-error",
+					"$resource.spec/artifact/score-lang-reuse",
+				},
+			},
+		},
+		{
+			desc: "Invalid score artifact",
+			setup: func(ctx context.Context, client connection.Client, adminClient connection.AdminClient) {
+				//setup
+				deleteProject(ctx, adminClient, t, "score-patterns-test")
+				createProject(ctx, adminClient, t, "score-patterns-test")
+				createApi(ctx, client, t, "projects/score-patterns-test/locations/global", "petstore")
+				createVersion(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore", "1.0.0")
+				createSpec(ctx, client, t, "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0", "openapi.yaml", gzipOpenAPIv3)
+				// score lint-error
+				artifactBytes, _ := proto.Marshal(&rpc.Lint{
+					Name: "openapi.yaml",
+					Files: []*rpc.LintFile{
+						{
+							FilePath: "openapi.yaml",
+							Problems: []*rpc.LintProblem{
+								{
+									Message: "lint-error",
+								},
+							},
+						},
+					},
+				})
+				createUpdateArtifact(
+					ctx, client, t,
+					"projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					artifactBytes, "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score")
+			},
+			takeAction: true,
+			definition: &rpc.ScoreCardDefinition{
+				Id:          "quality",
+				Kind:        "ScoreCardDefinition",
+				DisplayName: "Quality",
+				Description: "Quality ScoreCard",
+				TargetResource: &rpc.ResourcePattern{
+					Pattern: "apis/-/versions/-/specs/-",
+				},
+				ScorePatterns: []string{
+					"$resource.spec/artifact/score-lint-error",
+					"$resource.spec/artifact/score-lang-reuse",
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			registryClient, err := connection.NewClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			defer registryClient.Close()
+			adminClient, err := connection.NewAdminClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			defer adminClient.Close()
+
+			test.setup(ctx, registryClient, adminClient)
+
+			resource := patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-patterns-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			}
+
+			gotResult := processScorePatterns(ctx, registryClient, test.definition, resource, &rpc.Artifact{}, test.takeAction, "projects/score-patterns-test/locations/global")
+
+			if gotResult.err == nil {
+				t.Errorf("processScorePatterns(ctx, client, %v, %v) did not return an error", test.definition, resource)
+			}
+		})
+	}
+}