Repository: apigee/registry
Pull request #679 (2022-07-28 00:45:57)

Update timestamp condition in scoring logic
This PR updates the timestamp check used in the scoring logic to account for the threshold timestamp difference as mentioned in https://github.com/apigee/registry/issues/641.

* A new interface `artifactClient` is defined in scoring package which expects three functionalities:
   - GetArtifact
   - SetArtifact
   - ListArtifacts
* This new interface was deliberately not combined with the one defined in the `controller` package because both of these interfaces expect different methods.
* The non-fake implementation of this interface (`RegistryArtifactClient`) is defined in `scoring` package (similar to `controller` package). This has 3 reasons:
   - the`cmd` package can use these implementations while calling functions defined in the `scoring` package. The `cmd` package doesn't need to worry about how to implement the interface. 
   - By keeping this implementation in `scoring` package, we can use it in real tests (non-timestamp based tests which talk to the real registry).
   - Lastly, it follows the similar pattern from `seeder` package, it defines a real implementation of the interface: `Client`, which we use in other packages to seed the registry.  

I am right now leaning towards keeping the interface definitions and their implementations in respective `scoring` and `controller` package, it keeps the internal details specific to those packages. This was a lingering discussion from the last PR which I deferred to this one.

codecov[bot]: # [Codecov](https://codecov.io/gh/apigee/registry/pull/679?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) Report
> Merging [#679](https://codecov.io/gh/apigee/registry/pull/679?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (c62fbd6) into [main](https://codecov.io/gh/apigee/registry/commit/7935c83fe02fcaaf2351aab7121aeebbc6ac95d3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (7935c83) will **decrease** coverage by `0.01%`.
> The diff coverage is `46.15%`.

> :exclamation: Current head c62fbd6 differs from pull request most recent head 3d2f125. Consider uploading reports for the commit 3d2f125 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #679      +/-   ##
==========================================
- Coverage   54.15%   54.13%   -0.02%     
==========================================
  Files          78       79       +1     
  Lines        7336     7346      +10     
==========================================
+ Hits         3973     3977       +4     
- Misses       2940     2946       +6     
  Partials      423      423              
```


| [Impacted Files](https://codecov.io/gh/apigee/registry/pull/679?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/679/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/679/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/scorecard.go](https://codecov.io/gh/apigee/registry/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3Jpbmcvc2NvcmVjYXJkLmdv) | `54.19% <60.00%> (ø)` | |
| [cmd/registry/scoring/artifact-client.go](https://codecov.io/gh/apigee/registry/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3JpbmcvYXJ0aWZhY3QtY2xpZW50Lmdv) | `66.66% <66.66%> (ø)` | |
| [cmd/registry/scoring/score.go](https://codecov.io/gh/apigee/registry/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3Jpbmcvc2NvcmUuZ28=) | `70.99% <71.42%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee).


Patch:
diff --git a/cmd/registry/cmd/compute/score.go b/cmd/registry/cmd/compute/score.go
index fd674df..10d5bcb 100644
--- a/cmd/registry/cmd/compute/score.go
+++ b/cmd/registry/cmd/compute/score.go
@@ -56,16 +56,18 @@ func scoreCommand() *cobra.Command {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to list resources")
 			}
 
+			artifactClient := &scoring.RegistryArtifactClient{RegistryClient: client}
+
 			for _, r := range resources {
 				// Fetch the ScoreDefinitions which can be applied to this resource
-				scoreDefinitions, err := scoring.FetchScoreDefinitions(ctx, client, r.ResourceName())
+				scoreDefinitions, err := scoring.FetchScoreDefinitions(ctx, artifactClient, r.ResourceName())
 				if err != nil {
 					log.FromContext(ctx).WithError(err).Errorf("Skipping resource %q", r.ResourceName())
 					continue
 				}
 				for _, d := range scoreDefinitions {
 					taskQueue <- &computeScoreTask{
-						client:      client,
+						client:      artifactClient,
 						defArtifact: d,
 						resource:    r,
 						dryRun:      dryRun,
@@ -79,7 +81,7 @@ func scoreCommand() *cobra.Command {
 }
 
 type computeScoreTask struct {
-	client      connection.RegistryClient
+	client      *scoring.RegistryArtifactClient
 	defArtifact *rpc.Artifact
 	resource    patterns.ResourceInstance
 	dryRun      bool
diff --git a/cmd/registry/cmd/compute/scorecard.go b/cmd/registry/cmd/compute/scorecard.go
index 7607579..41b367a 100644
--- a/cmd/registry/cmd/compute/scorecard.go
+++ b/cmd/registry/cmd/compute/scorecard.go
@@ -55,16 +55,18 @@ func scoreCardCommand() *cobra.Command {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to list resources")
 			}
 
+			artifactClient := &scoring.RegistryArtifactClient{RegistryClient: client}
+
 			for _, r := range resources {
 				// Fetch the ScoreCardDefinitions which can be applied to this resource
-				scoreCardDefinitions, err := scoring.FetchScoreCardDefinitions(ctx, client, r.ResourceName())
+				scoreCardDefinitions, err := scoring.FetchScoreCardDefinitions(ctx, artifactClient, 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,
+						client:      artifactClient,
 						defArtifact: d,
 						resource:    r,
 						dryRun:      dryRun,
@@ -78,7 +80,7 @@ func scoreCardCommand() *cobra.Command {
 }
 
 type computeScoreCardTask struct {
-	client      connection.RegistryClient
+	client      *scoring.RegistryArtifactClient
 	defArtifact *rpc.Artifact
 	resource    patterns.ResourceInstance
 	dryRun      bool
diff --git a/cmd/registry/scoring/artifact-client.go b/cmd/registry/scoring/artifact-client.go
new file mode 100644
index 0000000..59f4485
--- /dev/null
+++ b/cmd/registry/scoring/artifact-client.go
@@ -0,0 +1,32 @@
+package scoring
+
+import (
+	"context"
+
+	"github.com/apigee/registry/cmd/registry/core"
+	"github.com/apigee/registry/pkg/connection"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
+)
+
+type artifactClient interface {
+	GetArtifact(context.Context, names.Artifact, bool, core.ArtifactHandler) error
+	SetArtifact(context.Context, *rpc.Artifact) error
+	ListArtifacts(context.Context, names.Artifact, string, bool, core.ArtifactHandler) error
+}
+
+type RegistryArtifactClient struct {
+	RegistryClient connection.RegistryClient
+}
+
+func (r *RegistryArtifactClient) GetArtifact(ctx context.Context, artifact names.Artifact, getContents bool, handler core.ArtifactHandler) error {
+	return core.GetArtifact(ctx, r.RegistryClient, artifact, getContents, handler)
+}
+
+func (r *RegistryArtifactClient) SetArtifact(ctx context.Context, artifact *rpc.Artifact) error {
+	return core.SetArtifact(ctx, r.RegistryClient, artifact)
+}
+
+func (r *RegistryArtifactClient) ListArtifacts(ctx context.Context, artifact names.Artifact, filter string, contents bool, handler core.ArtifactHandler) error {
+	return core.ListArtifacts(ctx, r.RegistryClient, artifact, filter, contents, handler)
+}
diff --git a/cmd/registry/scoring/score.go b/cmd/registry/scoring/score.go
index 510ed9d..28d712a 100644
--- a/cmd/registry/scoring/score.go
+++ b/cmd/registry/scoring/score.go
@@ -24,7 +24,6 @@ import (
 	"github.com/apigee/registry/cmd/registry/patch"
 	"github.com/apigee/registry/cmd/registry/patterns"
 	"github.com/apigee/registry/log"
-	"github.com/apigee/registry/pkg/connection"
 	"github.com/apigee/registry/rpc"
 	"github.com/apigee/registry/server/registry/names"
 	"google.golang.org/grpc/codes"
@@ -38,7 +37,7 @@ func scoreID(definitionID string) string {
 
 func FetchScoreDefinitions(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client artifactClient,
 	resource patterns.ResourceName) ([]*rpc.Artifact, error) {
 	defArtifacts := make([]*rpc.Artifact, 0)
 
@@ -48,7 +47,7 @@ func FetchScoreDefinitions(
 		return nil, err
 	}
 	listFilter := fmt.Sprintf("mime_type == %q", patch.MimeTypeForKind("ScoreDefinition"))
-	err = core.ListArtifacts(ctx, client, artifact, listFilter, true,
+	err = client.ListArtifacts(ctx, artifact, listFilter, true,
 		func(artifact *rpc.Artifact) error {
 			definition := &rpc.ScoreDefinition{}
 			if err := proto.Unmarshal(artifact.GetContents(), definition); err != nil {
@@ -74,7 +73,7 @@ func FetchScoreDefinitions(
 
 func CalculateScore(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client artifactClient,
 	defArtifact *rpc.Artifact,
 	resource patterns.ResourceInstance,
 	dryRun bool) error {
@@ -101,7 +100,8 @@ func CalculateScore(
 	}
 
 	// Calculate score if the definition has been updated
-	if scoreArtifact != nil && scoreArtifact.GetUpdateTime().AsTime().Before(defArtifact.GetUpdateTime().AsTime()) {
+	// This condition is required to avoid the scenario mentioned here: https://github.com/apigee/registry/issues/641
+	if scoreArtifact != nil && defArtifact.GetUpdateTime().AsTime().Add(patterns.ResourceUpdateThresholdSeconds).After(scoreArtifact.GetUpdateTime().AsTime()) {
 		takeAction = true
 	}
 
@@ -143,7 +143,7 @@ type scoreResult struct {
 
 func processFormula(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client artifactClient,
 	definition *rpc.ScoreDefinition,
 	resource patterns.ResourceInstance,
 	scoreArtifact *rpc.Artifact,
@@ -165,7 +165,7 @@ func processFormula(
 
 func processScoreFormula(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client artifactClient,
 	formula *rpc.ScoreFormula,
 	resource patterns.ResourceInstance,
 	scoreArtifact *rpc.Artifact,
@@ -197,7 +197,8 @@ func processScoreFormula(
 	}
 
 	// Update required tells the calling function if the score artifact needs to be updated
-	updateRequired := takeAction || scoreArtifact.GetUpdateTime().AsTime().Before(artifact.GetUpdateTime().AsTime())
+	// This condition is required to avoid the scenario mentioned here: https://github.com/apigee/registry/issues/641
+	updateRequired := takeAction || artifact.GetUpdateTime().AsTime().Add(patterns.ResourceUpdateThresholdSeconds).After(scoreArtifact.GetUpdateTime().AsTime())
 
 	// Apply the scoreExpression by default. This value will be required by the rollup_formula in the case where
 	// another formula from rollup_formula.score_formulas makes the score outdated.
@@ -230,7 +231,7 @@ func processScoreFormula(
 
 func processRollUpFormula(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client artifactClient,
 	formula *rpc.RollUpFormula,
 	resource patterns.ResourceInstance,
 	scoreArtifact *rpc.Artifact,
@@ -439,7 +440,7 @@ func processScoreType(definition *rpc.ScoreDefinition, scoreValue interface{}, p
 	return score, nil
 }
 
-func uploadScore(ctx context.Context, client connection.RegistryClient, resource patterns.ResourceInstance, score *rpc.Score) error {
+func uploadScore(ctx context.Context, client artifactClient, resource patterns.ResourceInstance, score *rpc.Score) error {
 	artifactBytes, err := proto.Marshal(score)
 	if err != nil {
 		return err
@@ -450,21 +451,21 @@ func uploadScore(ctx context.Context, client connection.RegistryClient, resource
 		MimeType: patch.MimeTypeForKind("Score"),
 	}
 	log.Debugf(ctx, "Uploading %s", artifact.GetName())
-	if err = core.SetArtifact(ctx, client, artifact); err != nil {
+	if err = client.SetArtifact(ctx, artifact); err != nil {
 		return fmt.Errorf("failed to save artifact %s: %s", artifact.GetName(), err)
 	}
 
 	return nil
 }
 
-func getArtifact(ctx context.Context, client connection.RegistryClient, artifactPattern string, getContents bool) (*rpc.Artifact, error) {
+func getArtifact(ctx context.Context, client artifactClient, artifactPattern string, getContents bool) (*rpc.Artifact, error) {
 	artifactName, err := names.ParseArtifact(artifactPattern)
 	if err != nil {
 		return nil, fmt.Errorf("invalid artifact pattern %q: %s", artifactPattern, err)
 	}
 
 	gotArtifact := &rpc.Artifact{}
-	err = core.GetArtifact(ctx, client, artifactName, true, func(artifact *rpc.Artifact) error {
+	err = client.GetArtifact(ctx, artifactName, true, func(artifact *rpc.Artifact) error {
 		gotArtifact = artifact
 		return nil
 	})
diff --git a/cmd/registry/scoring/scorecard.go b/cmd/registry/scoring/scorecard.go
index 29b1a02..a98e835 100644
--- a/cmd/registry/scoring/scorecard.go
+++ b/cmd/registry/scoring/scorecard.go
@@ -22,7 +22,6 @@ import (
 	"github.com/apigee/registry/cmd/registry/patch"
 	"github.com/apigee/registry/cmd/registry/patterns"
 	"github.com/apigee/registry/log"
-	"github.com/apigee/registry/pkg/connection"
 	"github.com/apigee/registry/rpc"
 	"github.com/apigee/registry/server/registry/names"
 	"google.golang.org/grpc/codes"
@@ -36,7 +35,7 @@ func scoreCardID(definitionID string) string {
 
 func FetchScoreCardDefinitions(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client artifactClient,
 	resource patterns.ResourceName) ([]*rpc.Artifact, error) {
 	defArtifacts := make([]*rpc.Artifact, 0)
 
@@ -46,7 +45,7 @@ func FetchScoreCardDefinitions(
 		return nil, err
 	}
 	listFilter := fmt.Sprintf("mime_type == %q", patch.MimeTypeForKind("ScoreCardDefinition"))
-	err = core.ListArtifacts(ctx, client, artifact, listFilter, true,
+	err = client.ListArtifacts(ctx, artifact, listFilter, true,
 		func(artifact *rpc.Artifact) error {
 			definition := &rpc.ScoreCardDefinition{}
 			if err := proto.Unmarshal(artifact.GetContents(), definition); err != nil {
@@ -72,7 +71,7 @@ func FetchScoreCardDefinitions(
 
 func CalculateScoreCard(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client artifactClient,
 	defArtifact *rpc.Artifact,
 	resource patterns.ResourceInstance,
 	dryRun bool) error {
@@ -99,7 +98,8 @@ func CalculateScoreCard(
 	}
 
 	// Generate ScoreCard if the definition has been updated
-	if scoreCardArtifact != nil && scoreCardArtifact.GetUpdateTime().AsTime().Before(defArtifact.GetUpdateTime().AsTime()) {
+	// This condition is required to avoid the scenario mentioned here: https://github.com/apigee/registry/issues/641
+	if scoreCardArtifact != nil && defArtifact.GetUpdateTime().AsTime().Add(patterns.ResourceUpdateThresholdSeconds).After(scoreCardArtifact.GetUpdateTime().AsTime()) {
 		takeAction = true
 	}
 
@@ -134,7 +134,7 @@ type scoreCardResult struct {
 
 func processScorePatterns(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client artifactClient,
 	definition *rpc.ScoreCardDefinition,
 	resource patterns.ResourceInstance,
 	scoreCardArtifact *rpc.Artifact,
@@ -164,7 +164,8 @@ func processScorePatterns(
 		}
 
 		// needsUpdate tells the calling function if the ScoreCard artifact needs to be updated
-		needsUpdate = needsUpdate || takeAction || scoreCardArtifact.GetUpdateTime().AsTime().Before(artifact.GetUpdateTime().AsTime())
+		// This condition is required to avoid the scenario mentioned here: https://github.com/apigee/registry/issues/641
+		needsUpdate = needsUpdate || takeAction || artifact.GetUpdateTime().AsTime().Add(patterns.ResourceUpdateThresholdSeconds).After(scoreCardArtifact.GetUpdateTime().AsTime())
 		// Extract Score from the fetched artifact
 		score := &rpc.Score{}
 		if err := proto.Unmarshal(artifact.GetContents(), score); err != nil {
@@ -203,7 +204,7 @@ func processScorePatterns(
 	}
 }
 
-func uploadScoreCard(ctx context.Context, client connection.RegistryClient, resource patterns.ResourceInstance, scoreCard *rpc.ScoreCard) error {
+func uploadScoreCard(ctx context.Context, client artifactClient, resource patterns.ResourceInstance, scoreCard *rpc.ScoreCard) error {
 	artifactBytes, err := proto.Marshal(scoreCard)
 	if err != nil {
 		return err
@@ -214,7 +215,7 @@ func uploadScoreCard(ctx context.Context, client connection.RegistryClient, reso
 		MimeType: patch.MimeTypeForKind("ScoreCard"),
 	}
 	log.Debugf(ctx, "Uploading %s", artifact.GetName())
-	if err = core.SetArtifact(ctx, client, artifact); err != nil {
+	if err = client.SetArtifact(ctx, artifact); err != nil {
 		return fmt.Errorf("failed to save artifact %s: %s", artifact.GetName(), err)
 	}
 


Test patch:
diff --git a/cmd/registry/scoring/score_test.go b/cmd/registry/scoring/score_test.go
index c85b3a0..b343ddb 100644
--- a/cmd/registry/scoring/score_test.go
+++ b/cmd/registry/scoring/score_test.go
@@ -196,61 +196,6 @@ func TestCalculateScore(t *testing.T) {
 				},
 			},
 		},
-		{
-			desc: "existing up-to-date score",
-			seed: []seeder.RegistryResource{
-				// score formula artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				// definition artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/artifacts/lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreDefinition",
-					Contents: protoMarshal(&rpc.ScoreDefinition{
-						Id: "lint-error",
-						TargetResource: &rpc.ResourcePattern{
-							Pattern: "apis/-/versions/-/specs/-",
-						},
-						Formula: &rpc.ScoreDefinition_ScoreFormula{
-							ScoreFormula: &rpc.ScoreFormula{
-								Artifact: &rpc.ResourcePattern{
-									Pattern: "$resource.spec/artifacts/lint-spectral",
-								},
-								ScoreExpression: "size(files[0].problems)",
-							},
-						},
-						Type: &rpc.ScoreDefinition_Integer{
-							Integer: &rpc.IntegerType{
-								MinValue: 0,
-								MaxValue: 10,
-							},
-						},
-					}),
-				},
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-					Contents: []byte{},
-				},
-			},
-			wantScore: &rpc.Score{},
-		},
 		{
 			desc: "existing score updated definition",
 			seed: []seeder.RegistryResource{
@@ -492,20 +437,22 @@ func TestCalculateScore(t *testing.T) {
 				},
 			}
 
+			artifactClient := &RegistryArtifactClient{RegistryClient: registryClient}
+
 			//fetch definition artifact
-			defArtifact, err := getArtifact(ctx, registryClient, "projects/score-formula-test/locations/global/artifacts/lint-error", true)
+			defArtifact, err := getArtifact(ctx, artifactClient, "projects/score-formula-test/locations/global/artifacts/lint-error", true)
 			if err != nil {
 				t.Errorf("failed to fetch the definition Artifact from setup: %s", err)
 			}
 
-			gotErr := CalculateScore(ctx, registryClient, defArtifact, resource, false)
+			gotErr := CalculateScore(ctx, artifactClient, defArtifact, resource, false)
 			if gotErr != nil {
 				t.Errorf("CalculateScore(ctx, client, %v, %v) returned unexpected error: %s", defArtifact, resource, gotErr)
 			}
 
 			//fetch score artifact and check the value
 			scoreArtifact, err := getArtifact(
-				ctx, registryClient,
+				ctx, artifactClient,
 				"projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error", true)
 			if err != nil {
 				t.Errorf("failed to get the result scoreArtifact from registry")
@@ -595,7 +542,9 @@ func TestProcessScoreFormula(t *testing.T) {
 		err:         nil,
 	}
 
-	gotResult := processScoreFormula(ctx, registryClient, formula, resource, &rpc.Artifact{}, true)
+	artifactClient := &RegistryArtifactClient{RegistryClient: registryClient}
+
+	gotResult := processScoreFormula(ctx, artifactClient, formula, resource, &rpc.Artifact{}, true)
 
 	opts := cmp.AllowUnexported(scoreResult{})
 	if !cmp.Equal(wantResult, gotResult, opts) {
@@ -813,7 +762,9 @@ func TestProcessScoreFormulaError(t *testing.T) {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
 			}
 
-			gotResult := processScoreFormula(ctx, registryClient, test.formula, test.resource, &rpc.Artifact{}, true)
+			artifactClient := &RegistryArtifactClient{RegistryClient: registryClient}
+
+			gotResult := processScoreFormula(ctx, artifactClient, test.formula, test.resource, &rpc.Artifact{}, true)
 			if gotResult.err == nil {
 				t.Errorf("processScoreFormula(ctx, client, %v, %v) did not return an error", test.formula, test.resource)
 			}
@@ -821,246 +772,6 @@ func TestProcessScoreFormulaError(t *testing.T) {
 	}
 }
 
-func TestProcessScoreFormulaTimestamp(t *testing.T) {
-	tests := []struct {
-		desc       string
-		seed       []seeder.RegistryResource
-		resource   patterns.ResourceInstance
-		takeAction bool
-		wantResult scoreResult
-	}{
-		// When takeAction is true, the score value should be always updated
-		{
-			desc: "takeAction is true and score is outdated",
-			seed: []seeder.RegistryResource{
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-					Contents: []byte{},
-				},
-				// score  formula artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "score-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: true,
-			wantResult: scoreResult{
-				value:       int64(1),
-				needsUpdate: true,
-				err:         nil,
-			},
-		},
-		// When takeAction is true, the score value should be always updated
-		{
-			desc: "takeAction and score is up-to-date",
-			seed: []seeder.RegistryResource{
-				// score formula artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-					Contents: []byte{},
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "score-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: true,
-			wantResult: scoreResult{
-				value:       int64(1),
-				needsUpdate: true,
-				err:         nil,
-			},
-		},
-		{
-			desc: "!takeAction and score is outdated",
-			seed: []seeder.RegistryResource{
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-				},
-				// score formula artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "score-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: false,
-			wantResult: scoreResult{
-				value:       int64(1),
-				needsUpdate: true,
-				err:         nil,
-			},
-		},
-		{
-			desc: "!takeAction and score is up-to-date",
-			seed: []seeder.RegistryResource{
-				// score formula artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "score-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: false,
-			wantResult: scoreResult{
-				value:       int64(1),
-				needsUpdate: false,
-				err:         nil,
-			},
-		},
-	}
-
-	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-formula-test")
-			t.Cleanup(func() { deleteProject(ctx, adminClient, t, "score-formula-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)
-			}
-
-			//fetch score artifact
-			scoreArtifact, err := getArtifact(ctx, registryClient, "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error", false)
-			if err != nil {
-				t.Errorf("failed to fetch the scoreArtifact from setup: %s", err)
-			}
-
-			formula := &rpc.ScoreFormula{
-				Artifact: &rpc.ResourcePattern{
-					Pattern: "$resource.spec/artifacts/lint-spectral",
-				},
-				ScoreExpression: "size(files[0].problems)",
-			}
-
-			gotResult := processScoreFormula(ctx, registryClient, formula, test.resource, scoreArtifact, test.takeAction)
-
-			opts := cmp.AllowUnexported(scoreResult{})
-			if !cmp.Equal(test.wantResult, gotResult, opts) {
-				t.Errorf("processScoreFormula() returned unexpected response, (-want, + got):\n%s", cmp.Diff(test.wantResult, gotResult, opts))
-			}
-		})
-	}
-}
-
 func TestProcessRollUpFormula(t *testing.T) {
 	ctx := context.Background()
 	registryClient, err := connection.NewRegistryClient(ctx)
@@ -1159,7 +870,9 @@ func TestProcessRollUpFormula(t *testing.T) {
 		err:         nil,
 	}
 
-	gotResult := processRollUpFormula(ctx, registryClient, formula, resource, &rpc.Artifact{}, true)
+	artifactClient := &RegistryArtifactClient{RegistryClient: registryClient}
+
+	gotResult := processRollUpFormula(ctx, artifactClient, formula, resource, &rpc.Artifact{}, true)
 
 	opts := cmp.AllowUnexported(scoreResult{})
 	if !cmp.Equal(wantResult, gotResult, opts) {
@@ -1429,7 +1142,9 @@ func TestProcessRollUpFormulaError(t *testing.T) {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
 			}
 
-			gotResult := processRollUpFormula(ctx, registryClient, test.formula, test.resource, &rpc.Artifact{}, true)
+			artifactClient := &RegistryArtifactClient{RegistryClient: registryClient}
+
+			gotResult := processRollUpFormula(ctx, artifactClient, test.formula, test.resource, &rpc.Artifact{}, true)
 			if gotResult.err == nil {
 				t.Errorf("processRollUpFormula(ctx, client, %v, %v) did not return an error", test.formula, test.resource)
 			}
@@ -1437,427 +1152,6 @@ func TestProcessRollUpFormulaError(t *testing.T) {
 	}
 }
 
-func TestProcessRollUpFormulaTimestamp(t *testing.T) {
-	tests := []struct {
-		desc       string
-		seed       []seeder.RegistryResource
-		resource   patterns.ResourceInstance
-		takeAction bool
-		wantResult scoreResult
-	}{
-		{
-			desc: "takeAction and score completely outdated",
-			seed: []seeder.RegistryResource{
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-				},
-				// lint artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				//  complexity artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
-					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
-					Contents: protoMarshal(&metrics.Complexity{
-						GetCount:    1,
-						PostCount:   1,
-						PutCount:    1,
-						DeleteCount: 1,
-					}),
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "rollup-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: true,
-			wantResult: scoreResult{
-				value:       float64(0.5),
-				needsUpdate: true,
-				err:         nil,
-			},
-		},
-		{
-			desc: "takeAction and score partially outdated",
-			seed: []seeder.RegistryResource{
-				// lint artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-				},
-				// complexity artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
-					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
-					Contents: protoMarshal(&metrics.Complexity{
-						GetCount:    1,
-						PostCount:   1,
-						PutCount:    1,
-						DeleteCount: 1,
-					}),
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "rollup-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: true,
-			wantResult: scoreResult{
-				value:       float64(0.5),
-				needsUpdate: true,
-				err:         nil,
-			},
-		},
-		{
-			desc: "takeAction and score up-to-date",
-			seed: []seeder.RegistryResource{
-				// lint artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				// complexity artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
-					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
-					Contents: protoMarshal(&metrics.Complexity{
-						GetCount:    1,
-						PostCount:   1,
-						PutCount:    1,
-						DeleteCount: 1,
-					}),
-				},
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "rollup-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: true,
-			wantResult: scoreResult{
-				value:       float64(0.5),
-				needsUpdate: true,
-				err:         nil,
-			},
-		},
-		{
-			desc: "!takeAction and score completely outdated",
-			seed: []seeder.RegistryResource{
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-				},
-				// lint artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				// complexity artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
-					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
-					Contents: protoMarshal(&metrics.Complexity{
-						GetCount:    1,
-						PostCount:   1,
-						PutCount:    1,
-						DeleteCount: 1,
-					}),
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "rollup-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: false,
-			wantResult: scoreResult{
-				value:       float64(0.5),
-				needsUpdate: true,
-				err:         nil,
-			},
-		},
-		{
-			desc: "!takeAction and score partially outdated",
-			seed: []seeder.RegistryResource{
-				// lint artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-				},
-				// complexity artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
-					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
-					Contents: protoMarshal(&metrics.Complexity{
-						GetCount:    1,
-						PostCount:   1,
-						PutCount:    1,
-						DeleteCount: 1,
-					}),
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "rollup-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: false,
-			wantResult: scoreResult{
-				value:       float64(0.5),
-				needsUpdate: true,
-				err:         nil,
-			},
-		},
-		{
-			desc: "!takeAction and score up-to-date",
-			seed: []seeder.RegistryResource{
-				// lint artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
-					Contents: protoMarshal(&rpc.Lint{
-						Name: "openapi.yaml",
-						Files: []*rpc.LintFile{
-							{
-								FilePath: "openapi.yaml",
-								Problems: []*rpc.LintProblem{
-									{
-										Message: "lint-error",
-									},
-									{
-										Message: "lint-error",
-									},
-								},
-							},
-						},
-					}),
-				},
-				// complexity artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
-					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
-					Contents: protoMarshal(&metrics.Complexity{
-						GetCount:    1,
-						PostCount:   1,
-						PutCount:    1,
-						DeleteCount: 1,
-					}),
-				},
-				// score artifact
-				&rpc.Artifact{
-					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-				},
-			},
-			resource: patterns.SpecResource{
-				SpecName: patterns.SpecName{
-					Name: names.Spec{
-						ProjectID: "rollup-formula-test",
-						ApiID:     "petstore",
-						VersionID: "1.0.0",
-						SpecID:    "openapi.yaml",
-					},
-				},
-			},
-			takeAction: false,
-			wantResult: scoreResult{
-				value:       nil,
-				needsUpdate: false,
-				err:         nil,
-			},
-		},
-	}
-
-	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, "rollup-formula-test")
-			t.Cleanup(func() { deleteProject(ctx, adminClient, t, "rollup-formula-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)
-			}
-
-			//fetch score artifact
-			scoreArtifact, err := getArtifact(ctx, registryClient, "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error", false)
-			if err != nil {
-				t.Errorf("failed to fetch the scoreArtifact from setup: %s", err)
-			}
-
-			formula := &rpc.RollUpFormula{
-				ScoreFormulas: []*rpc.ScoreFormula{
-					{
-						Artifact: &rpc.ResourcePattern{
-							Pattern: "$resource.spec/artifacts/lint-spectral",
-						},
-						ScoreExpression: "size(files[0].problems)",
-						ReferenceId:     "numErrors",
-					},
-					{
-						Artifact: &rpc.ResourcePattern{
-							Pattern: "$resource.spec/artifacts/complexity",
-						},
-						ScoreExpression: "getCount + postCount + putCount + deleteCount",
-						ReferenceId:     "numOperations",
-					},
-				},
-				RollupExpression: "double(numErrors)/numOperations",
-			}
-
-			gotResult := processRollUpFormula(ctx, registryClient, formula, test.resource, scoreArtifact, test.takeAction)
-
-			opts := cmp.AllowUnexported(scoreResult{})
-			if !cmp.Equal(test.wantResult, gotResult, opts) {
-				t.Errorf("processScoreFormula() returned unexpected response, (-want, +got):\n%s", cmp.Diff(test.wantResult, gotResult, opts))
-			}
-		})
-	}
-}
-
 func TestProcessScoreType(t *testing.T) {
 	tests := []struct {
 		desc       string
diff --git a/cmd/registry/scoring/scorecard_test.go b/cmd/registry/scoring/scorecard_test.go
index e6c1910..724b381 100644
--- a/cmd/registry/scoring/scorecard_test.go
+++ b/cmd/registry/scoring/scorecard_test.go
@@ -527,20 +527,22 @@ func TestCalculateScoreCard(t *testing.T) {
 				},
 			}
 
+			artifactClient := &RegistryArtifactClient{RegistryClient: registryClient}
+
 			//fetch definition artifact
-			defArtifact, err := getArtifact(ctx, registryClient, "projects/score-card-test/locations/global/artifacts/quality", true)
+			defArtifact, err := getArtifact(ctx, artifactClient, "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, false)
+			gotErr := CalculateScoreCard(ctx, artifactClient, defArtifact, resource, false)
 			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,
+				ctx, artifactClient,
 				"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")
@@ -809,95 +811,6 @@ func TestProcessScorePatterns(t *testing.T) {
 				err:         nil,
 			},
 		},
-		{
-			desc: "!takeAction and scoreCard is up-to-date",
-			seed: []seeder.RegistryResource{
-				// Score lint-error
-				&rpc.Artifact{
-					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-					Contents: protoMarshal(&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,
-							},
-						},
-					}),
-				},
-				// Score lang-reuse
-				&rpc.Artifact{
-					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
-					Contents: protoMarshal(&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,
-							},
-						},
-					}),
-				},
-				// ScoreCard artifact
-				&rpc.Artifact{
-					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
-					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard",
-					Contents: protoMarshal(&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: 50,
-									},
-								},
-							},
-							{
-								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: 60,
-									},
-								},
-							},
-						},
-					}),
-				},
-			},
-			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",
 			seed: []seeder.RegistryResource{
@@ -1179,13 +1092,15 @@ func TestProcessScorePatterns(t *testing.T) {
 				},
 			}
 
+			artifactClient := &RegistryArtifactClient{RegistryClient: registryClient}
+
 			//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)
+			scoreCardArtifact, err := getArtifact(ctx, artifactClient, "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")
+			gotResult := processScorePatterns(ctx, artifactClient, definition, test.resource, scoreCardArtifact, test.takeAction, "projects/score-patterns-test/locations/global")
 
 			opts := cmp.Options{
 				cmp.AllowUnexported(scoreCardResult{}),
@@ -1335,7 +1250,9 @@ func TestProcessScorePatternsError(t *testing.T) {
 				},
 			}
 
-			gotResult := processScorePatterns(ctx, registryClient, test.definition, resource, &rpc.Artifact{}, test.takeAction, "projects/score-patterns-test/locations/global")
+			artifactClient := &RegistryArtifactClient{RegistryClient: registryClient}
+
+			gotResult := processScorePatterns(ctx, artifactClient, 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)
diff --git a/cmd/registry/scoring/scoring-timestamp_test.go b/cmd/registry/scoring/scoring-timestamp_test.go
new file mode 100644
index 0000000..ff69ade
--- /dev/null
+++ b/cmd/registry/scoring/scoring-timestamp_test.go
@@ -0,0 +1,1380 @@
+package scoring
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/apigee/registry/cmd/registry/core"
+	"github.com/apigee/registry/cmd/registry/patterns"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
+	"github.com/apigee/registry/server/registry/test/seeder"
+	metrics "github.com/google/gnostic/metrics"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/testing/protocmp"
+	"google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type fakeArtifactClient struct {
+	artifacts []*rpc.Artifact
+}
+
+func (f *fakeArtifactClient) GetArtifact(ctx context.Context, artifact names.Artifact, getContents bool, handler core.ArtifactHandler) error {
+	for _, a := range f.artifacts {
+		if a.GetName() == artifact.String() {
+			err := handler(a)
+			if err != nil {
+				return err
+			}
+			return nil
+		}
+	}
+
+	return nil
+}
+
+func (f *fakeArtifactClient) SetArtifact(ctx context.Context, artifact *rpc.Artifact) error {
+	if artifact.UpdateTime == nil {
+		artifact.UpdateTime = timestamppb.Now()
+	}
+	for i, a := range f.artifacts {
+		if a.GetName() == artifact.GetName() {
+			// remove the old copy
+			f.artifacts = append(f.artifacts[:i], f.artifacts[i+1:]...)
+			break
+		}
+	}
+	f.artifacts = append(f.artifacts, artifact)
+	return nil
+}
+
+// This implementation doesn't support filter functionality
+func (f *fakeArtifactClient) ListArtifacts(ctx context.Context, artifact names.Artifact, filter string, contents bool, handler core.ArtifactHandler) error {
+	for _, a := range f.artifacts {
+		name, _ := names.ParseArtifact(a.GetName())
+		if strings.Contains(filter, name.Parent()) || (artifact.ArtifactID() != "-" && name.ArtifactID() != artifact.ArtifactID()) {
+			continue
+		}
+
+		if err := handler(a); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// These functions are needed to use the fakeLister with the seeder package.
+func (f *fakeArtifactClient) CreateProject(ctx context.Context, req *rpc.CreateProjectRequest) (*rpc.Project, error) {
+	project := &rpc.Project{
+		Name: fmt.Sprintf("projects/%s", req.GetProjectId()),
+	}
+	return project, nil
+}
+
+func (f *fakeArtifactClient) UpdateApi(ctx context.Context, req *rpc.UpdateApiRequest) (*rpc.Api, error) {
+	api := req.Api
+	if api.UpdateTime == nil {
+		api.UpdateTime = timestamppb.Now()
+	}
+	return api, nil
+}
+
+func (f *fakeArtifactClient) UpdateApiVersion(ctx context.Context, req *rpc.UpdateApiVersionRequest) (*rpc.ApiVersion, error) {
+	version := req.ApiVersion
+	if version.UpdateTime == nil {
+		version.UpdateTime = timestamppb.Now()
+	}
+	return version, nil
+}
+
+func (f *fakeArtifactClient) UpdateApiSpec(ctx context.Context, req *rpc.UpdateApiSpecRequest) (*rpc.ApiSpec, error) {
+	spec := req.ApiSpec
+	if spec.RevisionUpdateTime == nil {
+		spec.RevisionUpdateTime = timestamppb.Now()
+	}
+	return spec, nil
+}
+
+func (f *fakeArtifactClient) UpdateApiDeployment(ctx context.Context, req *rpc.UpdateApiDeploymentRequest) (*rpc.ApiDeployment, error) {
+	deployment := req.ApiDeployment
+	if deployment.RevisionUpdateTime == nil {
+		deployment.RevisionUpdateTime = timestamppb.Now()
+	}
+	return deployment, nil
+}
+
+func (f *fakeArtifactClient) CreateArtifact(ctx context.Context, req *rpc.CreateArtifactRequest) (*rpc.Artifact, error) {
+	artifact := req.Artifact
+	if artifact.UpdateTime == nil {
+		artifact.UpdateTime = timestamppb.Now()
+	}
+	f.artifacts = append(f.artifacts, artifact)
+	return artifact, nil
+}
+
+func (f *fakeArtifactClient) ReplaceArtifact(ctx context.Context, req *rpc.ReplaceArtifactRequest) (*rpc.Artifact, error) {
+	artifact := req.Artifact
+	if artifact.UpdateTime == nil {
+		artifact.UpdateTime = timestamppb.Now()
+	}
+	for i, a := range f.artifacts {
+		if a.GetName() == artifact.GetName() {
+			// remove the old copy
+			f.artifacts = append(f.artifacts[:i], f.artifacts[i+1:]...)
+			break
+		}
+	}
+	f.artifacts = append(f.artifacts, artifact)
+	return artifact, nil
+}
+
+func TestTimestampCalculateScore(t *testing.T) {
+	tests := []struct {
+		desc            string
+		seed            []seeder.RegistryResource
+		definitionProto *rpc.ScoreDefinition
+		wantScore       *rpc.Score
+	}{
+		{
+			desc: "existing up-to-date score",
+			seed: []seeder.RegistryResource{
+				// score formula artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// definition artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/artifacts/lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreDefinition",
+					Contents: protoMarshal(&rpc.ScoreDefinition{
+						Id: "lint-error",
+						TargetResource: &rpc.ResourcePattern{
+							Pattern: "apis/-/versions/-/specs/-",
+						},
+						Formula: &rpc.ScoreDefinition_ScoreFormula{
+							ScoreFormula: &rpc.ScoreFormula{
+								Artifact: &rpc.ResourcePattern{
+									Pattern: "$resource.spec/artifacts/lint-spectral",
+								},
+								ScoreExpression: "size(files[0].problems)",
+							},
+						},
+						Type: &rpc.ScoreDefinition_Integer{
+							Integer: &rpc.IntegerType{
+								MinValue: 0,
+								MaxValue: 10,
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					Contents:   []byte{},
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+			},
+			wantScore: &rpc.Score{},
+		},
+		{
+			desc: "not recent enough score",
+			seed: []seeder.RegistryResource{
+				// score formula artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// definition artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/artifacts/lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreDefinition",
+					Contents: protoMarshal(&rpc.ScoreDefinition{
+						Id: "lint-error",
+						TargetResource: &rpc.ResourcePattern{
+							Pattern: "apis/-/versions/-/specs/-",
+						},
+						Formula: &rpc.ScoreDefinition_ScoreFormula{
+							ScoreFormula: &rpc.ScoreFormula{
+								Artifact: &rpc.ResourcePattern{
+									Pattern: "$resource.spec/artifacts/lint-spectral",
+								},
+								ScoreExpression: "size(files[0].problems)",
+							},
+						},
+						Type: &rpc.ScoreDefinition_Integer{
+							Integer: &rpc.IntegerType{
+								MinValue: 0,
+								MaxValue: 10,
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					Contents:   []byte{},
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+			},
+			wantScore: &rpc.Score{
+				Id:             "score-lint-error",
+				Kind:           "Score",
+				DefinitionName: "projects/score-formula-test/locations/global/artifacts/lint-error",
+				Severity:       rpc.Severity_SEVERITY_UNSPECIFIED,
+				Value: &rpc.Score_IntegerValue{
+					IntegerValue: &rpc.IntegerValue{
+						Value:    1,
+						MinValue: 0,
+						MaxValue: 10,
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			client := &fakeArtifactClient{}
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			resource := patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			}
+
+			//fetch definition artifact
+			defArtifact, err := getArtifact(ctx, client, "projects/score-formula-test/locations/global/artifacts/lint-error", true)
+			if err != nil {
+				t.Errorf("failed to fetch the definition Artifact from setup: %s", err)
+			}
+
+			gotErr := CalculateScore(ctx, client, defArtifact, resource, false)
+			if gotErr != nil {
+				t.Errorf("CalculateScore(ctx, client, %v, %v) returned unexpected error: %s", defArtifact, resource, gotErr)
+			}
+
+			//fetch score artifact and check the value
+			scoreArtifact, err := getArtifact(
+				ctx, client,
+				"projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error", true)
+			if err != nil {
+				t.Errorf("failed to get the result scoreArtifact from registry")
+			}
+
+			gotScore := &rpc.Score{}
+			err = proto.Unmarshal(scoreArtifact.GetContents(), gotScore)
+			if err != nil {
+				t.Errorf("failed unmarshalling score artifact from registry: %s", err)
+			}
+
+			opts := cmp.Options{protocmp.Transform()}
+			if !cmp.Equal(test.wantScore, gotScore, opts) {
+				t.Errorf("CalculateScore() returned unexpected response (-want +got):\n%s", cmp.Diff(test.wantScore, gotScore, opts))
+			}
+		})
+	}
+}
+
+func TestProcessScoreFormulaTimestamp(t *testing.T) {
+	tests := []struct {
+		desc       string
+		seed       []seeder.RegistryResource
+		resource   patterns.ResourceInstance
+		takeAction bool
+		wantResult scoreResult
+	}{
+		// When takeAction is true, the score value should be always updated
+		{
+			desc: "takeAction is true and score is outdated",
+			seed: []seeder.RegistryResource{
+				// score artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					Contents: []byte{},
+				},
+				// score  formula artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: true,
+			wantResult: scoreResult{
+				value:       int64(1),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		// When takeAction is true, the score value should be always updated
+		{
+			desc: "takeAction and score is up-to-date",
+			seed: []seeder.RegistryResource{
+				// score formula artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					Contents:   []byte{},
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: true,
+			wantResult: scoreResult{
+				value:       int64(1),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and score is outdated",
+			seed: []seeder.RegistryResource{
+				// score artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+				},
+				// score formula artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreResult{
+				value:       int64(1),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and score is up-to-date",
+			seed: []seeder.RegistryResource{
+				// score formula artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreResult{
+				value:       int64(1),
+				needsUpdate: false,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and score not recent enough",
+			seed: []seeder.RegistryResource{
+				// score formula artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreResult{
+				value:       int64(1),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "takeAction and score not recent enough",
+			seed: []seeder.RegistryResource{
+				// score formula artifact
+				&rpc.Artifact{
+					Name:     "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "score-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: true,
+			wantResult: scoreResult{
+				value:       int64(1),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			client := &fakeArtifactClient{}
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			//fetch score artifact
+			scoreArtifact, err := getArtifact(ctx, client, "projects/score-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error", false)
+			if err != nil {
+				t.Errorf("failed to fetch the scoreArtifact from setup: %s", err)
+			}
+
+			formula := &rpc.ScoreFormula{
+				Artifact: &rpc.ResourcePattern{
+					Pattern: "$resource.spec/artifacts/lint-spectral",
+				},
+				ScoreExpression: "size(files[0].problems)",
+			}
+
+			gotResult := processScoreFormula(ctx, client, formula, test.resource, scoreArtifact, test.takeAction)
+
+			opts := cmp.AllowUnexported(scoreResult{})
+			if !cmp.Equal(test.wantResult, gotResult, opts) {
+				t.Errorf("processScoreFormula() returned unexpected response, (-want, + got):\n%s", cmp.Diff(test.wantResult, gotResult, opts))
+			}
+		})
+	}
+}
+
+func TestProcessRollUpFormulaTimestamp(t *testing.T) {
+	tests := []struct {
+		desc       string
+		seed       []seeder.RegistryResource
+		resource   patterns.ResourceInstance
+		takeAction bool
+		wantResult scoreResult
+	}{
+		{
+			desc: "takeAction and score completely outdated",
+			seed: []seeder.RegistryResource{
+				// score artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+				},
+				// lint artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+				},
+				//  complexity artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
+					Contents: protoMarshal(&metrics.Complexity{
+						GetCount:    1,
+						PostCount:   1,
+						PutCount:    1,
+						DeleteCount: 1,
+					}),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "rollup-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: true,
+			wantResult: scoreResult{
+				value:       float64(0.5),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "takeAction and score partially outdated",
+			seed: []seeder.RegistryResource{
+				// lint artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+				},
+				// complexity artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
+					Contents: protoMarshal(&metrics.Complexity{
+						GetCount:    1,
+						PostCount:   1,
+						PutCount:    1,
+						DeleteCount: 1,
+					}),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "rollup-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: true,
+			wantResult: scoreResult{
+				value:       float64(0.5),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "takeAction and score up-to-date",
+			seed: []seeder.RegistryResource{
+				// lint artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// complexity artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
+					Contents: protoMarshal(&metrics.Complexity{
+						GetCount:    1,
+						PostCount:   1,
+						PutCount:    1,
+						DeleteCount: 1,
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "rollup-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: true,
+			wantResult: scoreResult{
+				value:       float64(0.5),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and score completely outdated",
+			seed: []seeder.RegistryResource{
+				// score artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+				},
+				// lint artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+				},
+				// complexity artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
+					Contents: protoMarshal(&metrics.Complexity{
+						GetCount:    1,
+						PostCount:   1,
+						PutCount:    1,
+						DeleteCount: 1,
+					}),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "rollup-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreResult{
+				value:       float64(0.5),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and score partially outdated",
+			seed: []seeder.RegistryResource{
+				// lint artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+				},
+				// complexity artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
+					Contents: protoMarshal(&metrics.Complexity{
+						GetCount:    1,
+						PostCount:   1,
+						PutCount:    1,
+						DeleteCount: 1,
+					}),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "rollup-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreResult{
+				value:       float64(0.5),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and score up-to-date",
+			seed: []seeder.RegistryResource{
+				// lint artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// complexity artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
+					Contents: protoMarshal(&metrics.Complexity{
+						GetCount:    1,
+						PostCount:   1,
+						PutCount:    1,
+						DeleteCount: 1,
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "rollup-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreResult{
+				value:       nil,
+				needsUpdate: false,
+				err:         nil,
+			},
+		},
+		{
+			desc: "!takeAction and score not recent enough",
+			seed: []seeder.RegistryResource{
+				// lint artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-spectral",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.style.Lint",
+					Contents: protoMarshal(&rpc.Lint{
+						Name: "openapi.yaml",
+						Files: []*rpc.LintFile{
+							{
+								FilePath: "openapi.yaml",
+								Problems: []*rpc.LintProblem{
+									{
+										Message: "lint-error",
+									},
+									{
+										Message: "lint-error",
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// complexity artifact
+				&rpc.Artifact{
+					Name:     "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					MimeType: "application/octet-stream;type=gnostic.metrics.Complexity",
+					Contents: protoMarshal(&metrics.Complexity{
+						GetCount:    1,
+						PostCount:   1,
+						PutCount:    1,
+						DeleteCount: 1,
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// score artifact
+				&rpc.Artifact{
+					Name:       "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType:   "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+			},
+			resource: patterns.SpecResource{
+				SpecName: patterns.SpecName{
+					Name: names.Spec{
+						ProjectID: "rollup-formula-test",
+						ApiID:     "petstore",
+						VersionID: "1.0.0",
+						SpecID:    "openapi.yaml",
+					},
+				},
+			},
+			takeAction: false,
+			wantResult: scoreResult{
+				value:       float64(0.5),
+				needsUpdate: true,
+				err:         nil,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			client := &fakeArtifactClient{}
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			//fetch score artifact
+			scoreArtifact, err := getArtifact(ctx, client, "projects/rollup-formula-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error", false)
+			if err != nil {
+				t.Errorf("failed to fetch the scoreArtifact from setup: %s", err)
+			}
+
+			formula := &rpc.RollUpFormula{
+				ScoreFormulas: []*rpc.ScoreFormula{
+					{
+						Artifact: &rpc.ResourcePattern{
+							Pattern: "$resource.spec/artifacts/lint-spectral",
+						},
+						ScoreExpression: "size(files[0].problems)",
+						ReferenceId:     "numErrors",
+					},
+					{
+						Artifact: &rpc.ResourcePattern{
+							Pattern: "$resource.spec/artifacts/complexity",
+						},
+						ScoreExpression: "getCount + postCount + putCount + deleteCount",
+						ReferenceId:     "numOperations",
+					},
+				},
+				RollupExpression: "double(numErrors)/numOperations",
+			}
+
+			gotResult := processRollUpFormula(ctx, client, formula, test.resource, scoreArtifact, test.takeAction)
+
+			opts := cmp.AllowUnexported(scoreResult{})
+			if !cmp.Equal(test.wantResult, gotResult, opts) {
+				t.Errorf("processScoreFormula() returned unexpected response, (-want, +got):\n%s", cmp.Diff(test.wantResult, gotResult, opts))
+			}
+		})
+	}
+}
+
+func TestProcessScorePatternsTimestamp(t *testing.T) {
+	tests := []struct {
+		desc       string
+		seed       []seeder.RegistryResource
+		resource   patterns.ResourceInstance
+		takeAction bool
+		wantResult scoreCardResult
+	}{
+		{
+			desc: "!takeAction and scoreCard is up-to-date",
+			seed: []seeder.RegistryResource{
+				// Score lint-error
+				&rpc.Artifact{
+					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					Contents: protoMarshal(&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,
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// Score lang-reuse
+				&rpc.Artifact{
+					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					Contents: protoMarshal(&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,
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// ScoreCard artifact
+				&rpc.Artifact{
+					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard",
+					Contents: protoMarshal(&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: 50,
+									},
+								},
+							},
+							{
+								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: 60,
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+			},
+			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 not recent enough",
+			seed: []seeder.RegistryResource{
+				// Score lint-error
+				&rpc.Artifact{
+					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lint-error",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					Contents: protoMarshal(&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,
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// Score lang-reuse
+				&rpc.Artifact{
+					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-lang-reuse",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.Score",
+					Contents: protoMarshal(&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,
+							},
+						},
+					}),
+					UpdateTime: timestamppb.Now(),
+				},
+				// ScoreCard artifact
+				&rpc.Artifact{
+					Name:     "projects/score-patterns-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/scorecard-quality",
+					MimeType: "application/octet-stream;type=google.cloud.apigeeregistry.v1.ScoreCard",
+					Contents: protoMarshal(&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: 50,
+									},
+								},
+							},
+							{
+								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: 60,
+									},
+								},
+							},
+						},
+					}),
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+			},
+			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()
+			client := &fakeArtifactClient{}
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			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, client, "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, client, 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))
+			}
+		})
+	}
+}