Repository: palantir/policy-bot
Pull request #407 (2022-04-08 22:54:19)

Adding a details tab to policy bot rules
This PR is for adding an expendable details tab to policy bot. 
<img width="540" alt="Screen Shot 2022-04-17 at 3 23 30 PM" src="https://user-images.githubusercontent.com/22352182/163734426-94090a50-6640-4cd2-b5e3-d1cfe06e91a5.png">

Here is an example of the UI. User can click on the little marker to expand and collapse the details of the rule. This can help user to easily understand why a rule is selected, as well as providing them a link to the users that can approve their PR.

The way policy bot works currently is that it receives a webhook from Github, and then finds the policy written in the repo. For each rule, it determines if it is skipped or selected based on the predicates. It also determines the status of each rule based on the approval/disapproval review the PR received. 

Our change adds a struct called `PredicateInfo`. This struct will be produced while the code is evaluating predicate and passed to `Result` to get rendered in html. It has two string fields and some pointer fields. 

```
type PredicateInfo struct {
	Name        string
	Type        PredicateInfoType
	Description string

	ContributorInfo *ContributorInfo
	FileInfo        *FileInfo
	PatternInfo     *PatternInfo
	CommitInfo      *CommitInfo
	StatusInfo      *StatusInfo
}
```

Because each predicate requires a different set of data to display as details, we store those in a separate struct and the `PredicateInfo` struct stores the pointer to that struct. The Type field indicates which pointer is not null/used and the Name field is used to generate frontend text, indicating what data is related to the Predicate. 

There are 15 possible predicates. In the original code, the predicates are grouped by files. Therefore, for each group of predicates, we have a specific struct to store the information of that group. 

For example, there are 4 predicates in the `predicate/author.go` file. We have the `ContributorInfo` struct that stores the data needed by the predicates in `author.go`. Specifically, each struct needs two types of data, first is that defines the condition to qualify/disqualify a predicate, and the second is the context of the PR used for determining the (dis)qualifications. In `ContributorInfo`, `Organizations`, `Teams` and `Users` are data used for qualification purpose and `Author`, `Contributors` are the actual values of the context of the PR. 

```none
type ContributorInfo struct {
	RequiredMembership Actors
	ActualAuthor       string
	ActualContributors []string
}
```
| Predicate Name | Fields Needed for Qualification | Fields Needed for Context |
| :--------------: | :--------------: | :----------: |
| `HasAuthorIn` | Organizations, Teams, Users | Author|
|`OnlyHasContributorsIn`| Organizations, Teams, Users| Contributors|
|`HasContributorIn`| Organizations, Teams, Users| Contributors|
|`AuthorIsOnlyContributor`| | Author,Contributors|

If a field is not needed, then we would not assign values to it, and by default in go, it would be `nil`.

The context field would store minimal needed data for (dis)qualification. For some predicates, when translate into logic, can be expressed as either `for all` or `there exists`. If the predicate is in the form of for all, e.g. `OnlyHasContributorsIn`, the original code has short circuiting so that while looping over all contributors, if the code sees an example of contributor that disqualifies the predicate, then it will return right away. In this case, the Contributors field in `ContributorInfo` would only store the specific contributor that doesn't qualify the predicate. Otherwise, if all contributors satisfy a condition, then the Contributors field would store all the contributors. The same short circuiting scheme applies to `there exists`, if there exists a value that qualifies a predicate, then we would simply return that value, ignoring other potential candidates. 

To display the reviewer information, we move the `Requires` struct from approval/disapproval to common package. So now both approval and disapproval uses the same `Requires` struct. This way we can easily pass the `Requires` struct to `Result`. Previous, the `Requires` struct in disapproval does not have a count field. Now we are just setting that number to 1 to keep the same behavior. 

For the front end part, we added two templates, `result-predicate-details` and `result-requires-details`. They are for displaying the predicate information and review information. 

`getPredicateInfo` and `getPredicateRequirement` in the `frontend.go` file are used to render the text displayed.

`getPredicateInfo` is used to obtain the context information related to each predicate, specifically, the value we store in the name field. And `getPredicateRequirement` is for obtaining the values for qualification/disqualification. For example, in `HasAuthorIn` predicate, it returns the organizations, teams and users. However, this function does not cover all the predicates. For boolean predicates like `HasSuccessfulStatus` or `AuthorIsOnlyContributor`, the explanatory requirement is not needed because the name of the predicate is already explanatory.

We also added the `PredicateInfo` to each predicate test. There are two issues worth thinking here. 

An issue is that using maps as dictionaries in the code would produce non-deterministic behaviors when testing in go. For example, in the `committersInListButAuthorsAreNot` test for the `OnlyHasContributorsIn` predicate, both `ttest1` and `ttest2` can be returned as result(because of short circuiting only one of the two will be returned). Therefore, my solution is to use the `Subset` function in the `testify` package. However, this cause us to not test equality in the other cases where there is no short circuiting. I do not know what is the best approach to this but two ways I can think of is 1) simply change the code to not use non-deterministic data structures like maps. 2) remove the test of which the result is non-deterministic. 

Another issue is related to the 'labels do not exist' test in `label_test.go`. I am not sure the point of testing it. I assume when labels do not exist then there is an error, so we can just return `nil` for the `PredicateInfo`. Are we just testing a case of error? 

bluekeyes: > An issue is that using maps as dictionaries in the code would produce non-deterministic behaviors when testing in go. [...] I do not know what is the best approach to this but two ways I can think of is 1) simply change the code to not use non-deterministic data structures like maps. 2) remove the test of which the result is non-deterministic.

Now that it matters, I suggest extracting the keys from the map, sorting them, and then iterating through the sorted list in the predicate implementation. We generally still need to use a `map` because we need a data structure that functions like a set and using `map` is the idiomatic way to do this in Go. Sorting the keys as a list is a good way to make the results deterministic.

> Another issue is related to the 'labels do not exist' test in label_test.go. I am not sure the point of testing it. I assume when labels do not exist then there is an error, so we can just return nil for the PredicateInfo. Are we just testing a case of error?

It looks like this was added to test the difference between a `nil` value and a `[]string{}` value. For most uses, these are equivalent in Go, so I don't think this test is adding much value. It should be safe to delete.

Patch:
diff --git a/package.json b/package.json
index 983d7af..90cfd03 100644
--- a/package.json
+++ b/package.json
@@ -14,10 +14,12 @@
   },
   "devDependencies": {
     "@fullhuman/postcss-purgecss": "^4.0",
+    "autoprefixer": "^10.4.4",
     "cssnano": "^5.0",
     "npm-run-all": "^4.1.5",
     "postcss": "^8.0",
     "postcss-cli": "^8.0",
     "tailwindcss": "^1.0"
-  }
+  },
+  "dependencies": {}
 }
diff --git a/policy/approval/approve.go b/policy/approval/approve.go
index 42a3b77..425248d 100644
--- a/policy/approval/approve.go
+++ b/policy/approval/approve.go
@@ -103,26 +103,31 @@ func (r *Rule) Evaluate(ctx context.Context, prctx pull.Context) (res common.Res
 	res.Name = r.Name
 	res.Description = r.Description
 	res.Status = common.StatusSkipped
+	res.Requires = r.Requires.Actors
+
+	var predicateResults []*common.PredicateResult
 
 	for _, p := range r.Predicates.Predicates() {
-		satisfied, desc, err := p.Evaluate(ctx, prctx)
+		result, err := p.Evaluate(ctx, prctx)
 		if err != nil {
 			res.Error = errors.Wrap(err, "failed to evaluate predicate")
 			return
 		}
+		predicateResults = append(predicateResults, result)
 
-		if !satisfied {
+		if !result.Satisfied {
 			log.Debug().Msgf("skipping rule, predicate of type %T was not satisfied", p)
 
+			desc := result.Description
 			res.StatusDescription = desc
 			if desc == "" {
 				res.StatusDescription = "A precondition of this rule was not satisfied"
 			}
-
+			res.PredicateResults = []*common.PredicateResult{result}
 			return
 		}
 	}
-
+	res.PredicateResults = predicateResults
 	approved, msg, err := r.IsApproved(ctx, prctx)
 	if err != nil {
 		res.Error = errors.Wrap(err, "failed to compute approval status")
diff --git a/policy/common/predicateresult.go b/policy/common/predicateresult.go
new file mode 100644
index 0000000..83e4337
--- /dev/null
+++ b/policy/common/predicateresult.go
@@ -0,0 +1,31 @@
+// Copyright 2018 Palantir Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package common
+
+type PredicateResult struct {
+	Satisfied bool
+
+	Description string
+
+	// Describes the values, used as "the $ValuesPhrase"; must be plural
+	ValuePhrase string
+	Values      []string
+
+	// Describes the condition, used as "$ConditionPhrase" or "does not $ConditionPhrase"
+	ConditionPhrase string
+	// If non-empty, use the map, otherwise, use the regular list
+	ConditionsMap   map[string][]string
+	ConditionValues []string
+}
diff --git a/policy/common/result.go b/policy/common/result.go
index 7071b94..91c43aa 100644
--- a/policy/common/result.go
+++ b/policy/common/result.go
@@ -65,6 +65,8 @@ type Result struct {
 	StatusDescription string
 	Status            EvaluationStatus
 	Error             error
+	PredicateResults  []*PredicateResult
+	Requires          Actors
 
 	ReviewRequestRule *ReviewRequestRule
 
diff --git a/policy/disapproval/disapprove.go b/policy/disapproval/disapprove.go
index 9caaf52..b874765 100644
--- a/policy/disapproval/disapprove.go
+++ b/policy/disapproval/disapprove.go
@@ -104,27 +104,33 @@ func (p *Policy) Evaluate(ctx context.Context, prctx pull.Context) (res common.R
 
 	res.Name = "disapproval"
 	res.Status = common.StatusSkipped
+	res.Requires = p.Requires.Actors
 
-	for _, p := range p.Predicates.Predicates() {
-		satisfied, desc, err := p.Evaluate(ctx, prctx)
+	var predicateResults []*common.PredicateResult
 
+	for _, p := range p.Predicates.Predicates() {
+		result, err := p.Evaluate(ctx, prctx)
 		if err != nil {
 			res.Error = errors.Wrap(err, "failed to evaluate predicate")
 			return
 		}
+		predicateResults = append(predicateResults, result)
 
-		if satisfied {
+		if result.Satisfied {
 			log.Debug().Msgf("disapproving, predicate of type %T was satisfied", p)
 
 			res.Status = common.StatusDisapproved
+
+			desc := result.Description
 			res.StatusDescription = desc
 			if desc == "" {
 				res.StatusDescription = "A precondition of this rule was satisfied"
 			}
+			res.PredicateResults = []*common.PredicateResult{result}
 			return
 		}
 	}
-
+	res.PredicateResults = predicateResults
 	if p.Requires.IsEmpty() {
 		log.Debug().Msg("no users are allowed to disapprove; skipping")
 
diff --git a/policy/predicate/author.go b/policy/predicate/author.go
index 9baab36..7b416df 100644
--- a/policy/predicate/author.go
+++ b/policy/predicate/author.go
@@ -17,6 +17,7 @@ package predicate
 import (
 	"context"
 	"fmt"
+	"sort"
 
 	"github.com/palantir/policy-bot/policy/common"
 	"github.com/palantir/policy-bot/pull"
@@ -29,17 +30,28 @@ type HasAuthorIn struct {
 
 var _ Predicate = &HasAuthorIn{}
 
-func (pred *HasAuthorIn) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *HasAuthorIn) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	author := prctx.Author()
 
 	result, err := pred.IsActor(ctx, prctx, author)
 	desc := ""
-
 	if !result {
 		desc = fmt.Sprintf("The pull request author %q does not meet the required membership conditions", author)
 	}
 
-	return result, desc, err
+	predicateResult := common.PredicateResult{
+		Satisfied:       result,
+		Description:     desc,
+		ValuePhrase:     "authors",
+		Values:          []string{author},
+		ConditionPhrase: "meet the required membership conditions",
+		ConditionsMap: map[string][]string{
+			"Organizations": pred.Organizations,
+			"Teams":         pred.Teams,
+			"Users":         pred.Users,
+		},
+	}
+	return &predicateResult, err
 }
 
 func (pred *HasAuthorIn) Trigger() common.Trigger {
@@ -52,10 +64,21 @@ type OnlyHasContributorsIn struct {
 
 var _ Predicate = &OnlyHasContributorsIn{}
 
-func (pred *OnlyHasContributorsIn) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *OnlyHasContributorsIn) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	commits, err := prctx.Commits()
+
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "contributors",
+		ConditionPhrase: "all meet the required membership conditions",
+		ConditionsMap: map[string][]string{
+			"Organizations": pred.Organizations,
+			"Teams":         pred.Teams,
+			"Users":         pred.Users,
+		},
+	}
+
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to get commits")
+		return nil, errors.Wrap(err, "failed to get commits")
 	}
 
 	users := make(map[string]struct{})
@@ -67,17 +90,27 @@ func (pred *OnlyHasContributorsIn) Evaluate(ctx context.Context, prctx pull.Cont
 		}
 	}
 
+	userList := make([]string, 0, len(users))
 	for user := range users {
+		userList = append(userList, user)
+	}
+	sort.Strings(userList)
+
+	for _, user := range userList {
 		member, err := pred.IsActor(ctx, prctx, user)
 		if err != nil {
-			return false, "", err
+			return nil, err
 		}
 		if !member {
-			return false, fmt.Sprintf("Contributor %q does not meet the required membership conditions", user), nil
+			predicateResult.Description = fmt.Sprintf("Contributor %q does not meet the required membership conditions", user)
+			predicateResult.Values = []string{user}
+			predicateResult.Satisfied = false
+			return &predicateResult, nil
 		}
 	}
-
-	return true, "", nil
+	predicateResult.Values = userList
+	predicateResult.Satisfied = true
+	return &predicateResult, nil
 }
 
 func (pred *OnlyHasContributorsIn) Trigger() common.Trigger {
@@ -90,10 +123,21 @@ type HasContributorIn struct {
 
 var _ Predicate = &HasContributorIn{}
 
-func (pred *HasContributorIn) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *HasContributorIn) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	commits, err := prctx.Commits()
+
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "contributors",
+		ConditionPhrase: "meet the required membership conditions ",
+		ConditionsMap: map[string][]string{
+			"Organizations": pred.Organizations,
+			"Teams":         pred.Teams,
+			"Users":         pred.Users,
+		},
+	}
+
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to get commits")
+		return nil, errors.Wrap(err, "failed to get commits")
 	}
 
 	users := make(map[string]struct{})
@@ -105,18 +149,27 @@ func (pred *HasContributorIn) Evaluate(ctx context.Context, prctx pull.Context)
 		}
 	}
 
+	userList := make([]string, 0, len(users))
 	for user := range users {
+		userList = append(userList, user)
+	}
+	sort.Strings(userList)
+
+	for _, user := range userList {
 		member, err := pred.IsActor(ctx, prctx, user)
 		if err != nil {
-			return false, "", err
+			return nil, err
 		}
 		if member {
-			return true, "", nil
+			predicateResult.Satisfied = true
+			predicateResult.Values = []string{user}
+			return &predicateResult, nil
 		}
 	}
-
-	desc := "No contributors meet the required membership conditions"
-	return false, desc, nil
+	predicateResult.Description = "No contributors meet the required membership conditions"
+	predicateResult.Satisfied = false
+	predicateResult.Values = userList
+	return &predicateResult, nil
 }
 
 func (pred *HasContributorIn) Trigger() common.Trigger {
@@ -127,26 +180,45 @@ type AuthorIsOnlyContributor bool
 
 var _ Predicate = AuthorIsOnlyContributor(false)
 
-func (pred AuthorIsOnlyContributor) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred AuthorIsOnlyContributor) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	commits, err := prctx.Commits()
+
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "authors",
+		ConditionPhrase: "meet the condition",
+	}
+	if pred {
+		predicateResult.ConditionValues = []string{"they are the only contributors"}
+	} else {
+		predicateResult.ConditionValues = []string{"they are not the only contributors"}
+	}
+
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to get commits")
+		return nil, errors.Wrap(err, "failed to get commits")
 	}
 
 	author := prctx.Author()
+	predicateResult.Values = []string{author}
+
 	for _, c := range commits {
 		if c.Author != author || (!c.CommittedViaWeb && c.Committer != author) {
 			if pred {
-				return false, fmt.Sprintf("Commit %.10s was authored or committed by a different user", c.SHA), nil
+				predicateResult.Description = fmt.Sprintf("Commit %.10s was authored or committed by a different user", c.SHA)
+				predicateResult.Satisfied = false
+				return &predicateResult, nil
 			}
-			return true, "", nil
+			predicateResult.Satisfied = true
+			return &predicateResult, nil
 		}
 	}
 
 	if pred {
-		return true, "", nil
+		predicateResult.Satisfied = true
+		return &predicateResult, nil
 	}
-	return false, fmt.Sprintf("All commits were authored and committed by %s", author), nil
+	predicateResult.Description = fmt.Sprintf("All commits were authored and committed by %s", author)
+	predicateResult.Satisfied = false
+	return &predicateResult, nil
 }
 
 func (pred AuthorIsOnlyContributor) Trigger() common.Trigger {
diff --git a/policy/predicate/branch.go b/policy/predicate/branch.go
index be3be09..3cacb39 100644
--- a/policy/predicate/branch.go
+++ b/policy/predicate/branch.go
@@ -28,7 +28,7 @@ type TargetsBranch struct {
 
 var _ Predicate = &TargetsBranch{}
 
-func (pred *TargetsBranch) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *TargetsBranch) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	targetName, _ := prctx.Branches()
 	matches := pred.Pattern.Matches(targetName)
 
@@ -37,7 +37,16 @@ func (pred *TargetsBranch) Evaluate(ctx context.Context, prctx pull.Context) (bo
 		desc = fmt.Sprintf("Target branch %q does not match required pattern %q", targetName, pred.Pattern)
 	}
 
-	return matches, desc, nil
+	predicateResult := common.PredicateResult{
+		Satisfied:       matches,
+		Description:     desc,
+		Values:          []string{targetName},
+		ValuePhrase:     "target branches",
+		ConditionPhrase: "match the required pattern",
+		ConditionValues: []string{pred.Pattern.String()},
+	}
+
+	return &predicateResult, nil
 }
 
 func (pred *TargetsBranch) Trigger() common.Trigger {
@@ -50,7 +59,7 @@ type FromBranch struct {
 
 var _ Predicate = &FromBranch{}
 
-func (pred *FromBranch) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *FromBranch) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	_, sourceBranchName := prctx.Branches()
 	matches := pred.Pattern.Matches(sourceBranchName)
 
@@ -59,7 +68,16 @@ func (pred *FromBranch) Evaluate(ctx context.Context, prctx pull.Context) (bool,
 		desc = fmt.Sprintf("Source branch %q does not match specified from_branch pattern %q", sourceBranchName, pred.Pattern)
 	}
 
-	return matches, desc, nil
+	predicateResult := common.PredicateResult{
+		Satisfied:       matches,
+		Description:     desc,
+		Values:          []string{sourceBranchName},
+		ValuePhrase:     "from branches",
+		ConditionPhrase: "match the required pattern",
+		ConditionValues: []string{pred.Pattern.String()},
+	}
+
+	return &predicateResult, nil
 }
 
 func (pred *FromBranch) Trigger() common.Trigger {
diff --git a/policy/predicate/file.go b/policy/predicate/file.go
index e832692..b20c84e 100644
--- a/policy/predicate/file.go
+++ b/policy/predicate/file.go
@@ -32,24 +32,53 @@ type ChangedFiles struct {
 
 var _ Predicate = &ChangedFiles{}
 
-func (pred *ChangedFiles) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *ChangedFiles) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
+	var paths, ignorePaths []string
+
+	for _, path := range pred.Paths {
+		paths = append(paths, path.String())
+	}
+
+	for _, ignorePath := range pred.IgnorePaths {
+		ignorePaths = append(ignorePaths, ignorePath.String())
+	}
+
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "changed files",
+		ConditionPhrase: "match patterns",
+		ConditionsMap: map[string][]string{
+			"path patterns":  paths,
+			"while ignoring": ignorePaths,
+		},
+	}
+
 	files, err := prctx.ChangedFiles()
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to list changed files")
+		return nil, errors.Wrap(err, "failed to list changed files")
 	}
 
+	changedFiles := []string{}
+
 	for _, f := range files {
+
+		changedFiles = append(changedFiles, f.Filename)
+
 		if anyMatches(pred.IgnorePaths, f.Filename) {
 			continue
 		}
 
 		if anyMatches(pred.Paths, f.Filename) {
-			return true, f.Filename + " was changed", nil
+			predicateResult.Values = []string{f.Filename}
+			predicateResult.Description = f.Filename + " was changed"
+			predicateResult.Satisfied = true
+			return &predicateResult, nil
 		}
 	}
 
-	desc := "No changed files match the required patterns"
-	return false, desc, nil
+	predicateResult.Values = changedFiles
+	predicateResult.Description = "No changed files match the required patterns"
+	predicateResult.Satisfied = false
+	return &predicateResult, nil
 }
 
 func (pred *ChangedFiles) Trigger() common.Trigger {
@@ -62,18 +91,37 @@ type OnlyChangedFiles struct {
 
 var _ Predicate = &OnlyChangedFiles{}
 
-func (pred *OnlyChangedFiles) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *OnlyChangedFiles) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
+	var paths []string
+
+	for _, path := range pred.Paths {
+		paths = append(paths, path.String())
+	}
+
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "changed files",
+		ConditionPhrase: "all match patterns",
+		ConditionValues: paths,
+	}
+
 	files, err := prctx.ChangedFiles()
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to list changed files")
+		return nil, errors.Wrap(err, "failed to list changed files")
 	}
 
+	changedFiles := []string{}
+
 	for _, f := range files {
+
+		changedFiles = append(changedFiles, f.Filename)
+
 		if anyMatches(pred.Paths, f.Filename) {
 			continue
 		}
-		desc := "A changed file does not match the required pattern"
-		return false, desc, nil
+		predicateResult.Values = []string{f.Filename}
+		predicateResult.Description = "A changed file does not match the required pattern"
+		predicateResult.Satisfied = false
+		return &predicateResult, nil
 	}
 
 	filesChanged := len(files) > 0
@@ -83,7 +131,10 @@ func (pred *OnlyChangedFiles) Evaluate(ctx context.Context, prctx pull.Context)
 		desc = "No files changed"
 	}
 
-	return filesChanged, desc, nil
+	predicateResult.Values = changedFiles
+	predicateResult.Description = desc
+	predicateResult.Satisfied = filesChanged
+	return &predicateResult, nil
 }
 
 func (pred *OnlyChangedFiles) Trigger() common.Trigger {
@@ -140,6 +191,14 @@ func (exp ComparisonExpr) MarshalText() ([]byte, error) {
 	return []byte(fmt.Sprintf("%s %d", op, exp.Value)), nil
 }
 
+func (exp ComparisonExpr) String() string {
+	res, err := exp.MarshalText()
+	if err != nil {
+		return fmt.Sprintf("?? (op:%d) %d", exp.Op, exp.Value)
+	}
+	return string(res[:])
+}
+
 func (exp *ComparisonExpr) UnmarshalText(text []byte) error {
 	text = bytes.TrimSpace(text)
 	if len(text) == 0 {
@@ -172,10 +231,16 @@ func (exp *ComparisonExpr) UnmarshalText(text []byte) error {
 	return nil
 }
 
-func (pred *ModifiedLines) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *ModifiedLines) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	files, err := prctx.ChangedFiles()
+
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "file modifications",
+		ConditionPhrase: "meet the modification conditions",
+	}
+
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to list changed files")
+		return nil, errors.Wrap(err, "failed to list changed files")
 	}
 
 	var additions, deletions int64
@@ -184,16 +249,36 @@ func (pred *ModifiedLines) Evaluate(ctx context.Context, prctx pull.Context) (bo
 		deletions += int64(f.Deletions)
 	}
 
-	for expr, v := range map[ComparisonExpr]int64{
-		pred.Additions: additions,
-		pred.Deletions: deletions,
-		pred.Total:     additions + deletions,
-	} {
-		if !expr.IsEmpty() && expr.Evaluate(v) {
-			return true, "", nil
+	if !pred.Additions.IsEmpty() {
+		predicateResult.Values = []string{fmt.Sprintf("+%d", additions)}
+		predicateResult.ConditionValues = []string{fmt.Sprintf("added lines %s", pred.Additions.String())}
+		if pred.Additions.Evaluate(additions) {
+			predicateResult.Satisfied = true
+			return &predicateResult, nil
+		}
+	}
+	if !pred.Deletions.IsEmpty() {
+		if pred.Deletions.Evaluate(deletions) {
+			predicateResult.Values = []string{fmt.Sprintf("-%d", deletions)}
+			predicateResult.ConditionValues = []string{fmt.Sprintf("deleted lines %s", pred.Deletions.String())}
+			predicateResult.Satisfied = true
+			return &predicateResult, nil
+		}
+		predicateResult.Values = append(predicateResult.Values, fmt.Sprintf("-%d", deletions))
+		predicateResult.ConditionValues = append(predicateResult.ConditionValues, fmt.Sprintf("deleted lines %s", pred.Deletions.String()))
+	}
+	if !pred.Total.IsEmpty() {
+		if pred.Total.Evaluate(additions + deletions) {
+			predicateResult.Values = []string{fmt.Sprintf("total %d", additions+deletions)}
+			predicateResult.ConditionValues = []string{fmt.Sprintf("total modifications %s", pred.Total.String())}
+			predicateResult.Satisfied = true
+			return &predicateResult, nil
 		}
+		predicateResult.Values = append(predicateResult.Values, fmt.Sprintf("total %d", additions+deletions))
+		predicateResult.ConditionValues = append(predicateResult.ConditionValues, fmt.Sprintf("total modifications %s", pred.Total.String()))
 	}
-	return false, fmt.Sprintf("modification of (+%d, -%d) does not match any conditions", additions, deletions), nil
+	predicateResult.Satisfied = false
+	return &predicateResult, nil
 }
 
 func (pred *ModifiedLines) Trigger() common.Trigger {
diff --git a/policy/predicate/label.go b/policy/predicate/label.go
index eead38e..a1c9011 100644
--- a/policy/predicate/label.go
+++ b/policy/predicate/label.go
@@ -27,21 +27,30 @@ type HasLabels []string
 
 var _ Predicate = HasLabels([]string{})
 
-func (pred HasLabels) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred HasLabels) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
+
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "labels",
+		ConditionPhrase: "contain the labels",
+	}
 	if len(pred) > 0 {
 		labels, err := prctx.Labels()
 		if err != nil {
-			return false, "", errors.Wrap(err, "failed to list pull request labels")
+			return nil, errors.Wrap(err, "failed to list pull request labels")
 		}
-
+		predicateResult.Values = labels
 		for _, requiredLabel := range pred {
 			if !contains(labels, strings.ToLower(requiredLabel)) {
-				return false, "Missing label: " + requiredLabel, nil
+				predicateResult.ConditionValues = []string{requiredLabel}
+				predicateResult.Description = "Missing label: " + requiredLabel
+				predicateResult.Satisfied = false
+				return &predicateResult, nil
 			}
 		}
 	}
-
-	return true, "", nil
+	predicateResult.ConditionValues = pred
+	predicateResult.Satisfied = true
+	return &predicateResult, nil
 }
 
 func (pred HasLabels) Trigger() common.Trigger {
diff --git a/policy/predicate/predicate.go b/policy/predicate/predicate.go
index 877bdf6..15a244a 100644
--- a/policy/predicate/predicate.go
+++ b/policy/predicate/predicate.go
@@ -24,7 +24,6 @@ import (
 type Predicate interface {
 	common.Triggered
 
-	// Evaluate determines if the predicate is satisfied. It also returns an
-	// optional string providing details about the evaluation result.
-	Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error)
+	// Evaluate determines if the predicate is satisfied.
+	Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error)
 }
diff --git a/policy/predicate/signature.go b/policy/predicate/signature.go
index 9a6df18..ce592e9 100644
--- a/policy/predicate/signature.go
+++ b/policy/predicate/signature.go
@@ -27,26 +27,42 @@ type HasValidSignatures bool
 
 var _ Predicate = HasValidSignatures(false)
 
-func (pred HasValidSignatures) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred HasValidSignatures) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	commits, err := prctx.Commits()
+
+	predicateResult := common.PredicateResult{
+		ConditionPhrase: "have",
+		ConditionValues: []string{"valid signatures"},
+	}
+
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to get commits")
+		return nil, errors.Wrap(err, "failed to get commits")
 	}
 
+	var commitHashes []string
+
 	for _, c := range commits {
 		valid, desc := hasValidSignature(ctx, c)
+		commitHashes = append(commitHashes, c.SHA)
 		if !valid {
+			predicateResult.Values = []string{c.SHA}
 			if pred {
-				return false, desc, nil
+				predicateResult.Description = desc
+				predicateResult.Satisfied = false
+				return &predicateResult, nil
 			}
-			return true, "", nil
+			predicateResult.Satisfied = true
+			return &predicateResult, nil
 		}
 	}
-
+	predicateResult.Values = commitHashes
 	if pred {
-		return true, "", nil
+		predicateResult.Satisfied = true
+		return &predicateResult, nil
 	}
-	return false, "All commits are signed and have valid signatures", nil
+	predicateResult.Satisfied = false
+	predicateResult.Description = "All commits are signed and have valid signatures"
+	return &predicateResult, nil
 }
 
 func (pred HasValidSignatures) Trigger() common.Trigger {
@@ -59,33 +75,60 @@ type HasValidSignaturesBy struct {
 
 var _ Predicate = &HasValidSignaturesBy{}
 
-func (pred *HasValidSignaturesBy) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *HasValidSignaturesBy) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	commits, err := prctx.Commits()
+
+	predicateResult := common.PredicateResult{
+		ConditionsMap: map[string][]string{
+			"Organizations": pred.Organizations,
+			"Teams":         pred.Teams,
+			"Users":         pred.Users,
+		},
+	}
+
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to get commits")
+		return nil, errors.Wrap(err, "failed to get commits")
 	}
 
-	signers := make(map[string]struct{})
+	signers := make(map[string]string)
+	var commitHashes []string
 
 	for _, c := range commits {
 		valid, desc := hasValidSignature(ctx, c)
 		if !valid {
-			return false, desc, nil
+			predicateResult.ConditionPhrase = "have valid signatures by members of"
+			predicateResult.ValuePhrase = "commits"
+			predicateResult.Values = []string{c.SHA}
+			predicateResult.Description = desc
+			predicateResult.Satisfied = false
+			return &predicateResult, nil
 		}
-		signers[c.Signature.Signer] = struct{}{}
+		signers[c.Signature.Signer] = c.SHA
+		commitHashes = append(commitHashes, c.SHA)
 	}
 
+	var signerList []string
+
 	for signer := range signers {
+		signerList = append(signerList, signer)
 		member, err := pred.IsActor(ctx, prctx, signer)
 		if err != nil {
-			return false, "", err
+			return nil, err
 		}
 		if !member {
-			return false, fmt.Sprintf("Contributor %q does not meet the required membership conditions for signing", signer), nil
+			predicateResult.ConditionPhrase = "satisfy the required membership conditions"
+			predicateResult.Values = []string{signer}
+			predicateResult.ValuePhrase = "signers"
+			predicateResult.Description = fmt.Sprintf("Contributor %q does not meet the required membership conditions for signing", signer)
+			predicateResult.Satisfied = false
+			return &predicateResult, nil
 		}
 	}
-
-	return true, "", nil
+	predicateResult.ConditionPhrase = "have valid signatures by members of"
+	predicateResult.Values = commitHashes
+	predicateResult.ValuePhrase = "commits"
+	predicateResult.Satisfied = true
+	return &predicateResult, nil
 }
 
 func (pred *HasValidSignaturesBy) Trigger() common.Trigger {
@@ -98,25 +141,48 @@ type HasValidSignaturesByKeys struct {
 
 var _ Predicate = &HasValidSignaturesByKeys{}
 
-func (pred *HasValidSignaturesByKeys) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred *HasValidSignaturesByKeys) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	commits, err := prctx.Commits()
+
+	predicateResult := common.PredicateResult{
+		ConditionPhrase: "have valid signatures by keys",
+		ConditionValues: pred.KeyIDs,
+	}
+
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to get commits")
+		return nil, errors.Wrap(err, "failed to get commits")
 	}
 
-	keys := make(map[string]struct{})
+	keys := make(map[string][]string)
+
+	var commitHashes []string
 
 	for _, c := range commits {
 		valid, desc := hasValidSignature(ctx, c)
 		if !valid {
-			return false, desc, nil
+			predicateResult.Values = []string{c.SHA}
+			predicateResult.Description = desc
+			predicateResult.ValuePhrase = "commits"
+			predicateResult.Satisfied = false
+			return &predicateResult, nil
 		}
+		commitHashes = append(commitHashes, c.SHA)
 		// Only GPG signatures are valid for this predicate
 		switch c.Signature.Type {
 		case pull.SignatureGpg:
-			keys[c.Signature.KeyID] = struct{}{}
+			commitSHAs, ok := keys[c.Signature.KeyID]
+			if ok {
+				keys[c.Signature.KeyID] = append(commitSHAs, c.SHA)
+			} else {
+				keys[c.Signature.KeyID] = []string{c.SHA}
+			}
 		default:
-			return false, fmt.Sprintf("Commit %.10s signature is not a GPG signature", c.SHA), nil
+			predicateResult.Values = []string{c.SHA}
+			predicateResult.ValuePhrase = "commits"
+			predicateResult.ConditionPhrase = "have GPG signatures"
+			predicateResult.Description = fmt.Sprintf("Commit %.10s signature is not a GPG signature", c.SHA)
+			predicateResult.Satisfied = false
+			return &predicateResult, nil
 		}
 	}
 
@@ -129,11 +195,19 @@ func (pred *HasValidSignaturesByKeys) Evaluate(ctx context.Context, prctx pull.C
 			}
 		}
 		if !isValidKey {
-			return false, fmt.Sprintf("Key %q does not meet the required key conditions for signing", key), nil
+			predicateResult.ConditionPhrase = "exist in the set of allowed keys"
+			predicateResult.Values = []string{key}
+			predicateResult.ValuePhrase = "keys"
+			predicateResult.Description = fmt.Sprintf("Key %q does not meet the required key conditions for signing", key)
+			predicateResult.Satisfied = false
+			return &predicateResult, nil
 		}
 	}
+	predicateResult.Values = commitHashes
+	predicateResult.ValuePhrase = "commits"
+	predicateResult.Satisfied = true
 
-	return true, "", nil
+	return &predicateResult, nil
 }
 
 func (pred *HasValidSignaturesByKeys) Trigger() common.Trigger {
diff --git a/policy/predicate/status.go b/policy/predicate/status.go
index 5351b73..6f1f61d 100644
--- a/policy/predicate/status.go
+++ b/policy/predicate/status.go
@@ -27,10 +27,16 @@ type HasSuccessfulStatus []string
 
 var _ Predicate = HasSuccessfulStatus([]string{})
 
-func (pred HasSuccessfulStatus) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred HasSuccessfulStatus) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	statuses, err := prctx.LatestStatuses()
+
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "status checks",
+		ConditionPhrase: "exist and pass",
+	}
+
 	if err != nil {
-		return false, "", errors.Wrap(err, "failed to list commit statuses")
+		return nil, errors.Wrap(err, "failed to list commit statuses")
 	}
 
 	var missingResults []string
@@ -46,13 +52,22 @@ func (pred HasSuccessfulStatus) Evaluate(ctx context.Context, prctx pull.Context
 	}
 
 	if len(missingResults) > 0 {
-		return false, "One or more statuses is missing: " + strings.Join(missingResults, ", "), nil
+		predicateResult.Values = missingResults
+		predicateResult.Description = "One or more statuses is missing: " + strings.Join(missingResults, ", ")
+		predicateResult.Satisfied = false
+		return &predicateResult, nil
 	}
 
 	if len(failingStatuses) > 0 {
-		return false, "One or more statuses has not passed: " + strings.Join(failingStatuses, ","), nil
+		predicateResult.Values = failingStatuses
+		predicateResult.Description = "One or more statuses has not passed: " + strings.Join(failingStatuses, ",")
+		predicateResult.Satisfied = false
+		return &predicateResult, nil
 	}
-	return true, "", nil
+
+	predicateResult.Values = pred
+	predicateResult.Satisfied = true
+	return &predicateResult, nil
 }
 
 func (pred HasSuccessfulStatus) Trigger() common.Trigger {
diff --git a/policy/predicate/title.go b/policy/predicate/title.go
index a120bcd..181714a 100644
--- a/policy/predicate/title.go
+++ b/policy/predicate/title.go
@@ -28,22 +28,45 @@ type Title struct {
 
 var _ Predicate = Title{}
 
-func (pred Title) Evaluate(ctx context.Context, prctx pull.Context) (bool, string, error) {
+func (pred Title) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
 	title := prctx.Title()
 
+	predicateResult := common.PredicateResult{
+		ValuePhrase:     "titles",
+		Values:          []string{title},
+		ConditionPhrase: "meet the pattern requirement",
+	}
+
+	var matchPatterns, notMatchPatterns []string
+
+	for _, reg := range pred.Matches {
+		matchPatterns = append(matchPatterns, reg.String())
+	}
+
+	for _, reg := range pred.NotMatches {
+		notMatchPatterns = append(notMatchPatterns, reg.String())
+	}
+
 	if len(pred.Matches) > 0 {
 		if anyMatches(pred.Matches, title) {
-			return true, "PR Title matches a Match pattern", nil
+			predicateResult.ConditionsMap = map[string][]string{"match": matchPatterns}
+			predicateResult.Description = "PR Title matches a Match pattern"
+			predicateResult.Satisfied = true
+			return &predicateResult, nil
 		}
 	}
 
 	if len(pred.NotMatches) > 0 {
 		if !anyMatches(pred.NotMatches, title) {
-			return true, "PR Title doesn't match a NotMatch pattern", nil
+			predicateResult.ConditionsMap = map[string][]string{"not match": notMatchPatterns}
+			predicateResult.Description = "PR Title doesn't match a NotMatch pattern"
+			predicateResult.Satisfied = true
+			return &predicateResult, nil
 		}
 	}
-
-	return false, "", nil
+	predicateResult.Satisfied = false
+	predicateResult.ConditionsMap = map[string][]string{"match": matchPatterns, "not match": notMatchPatterns}
+	return &predicateResult, nil
 }
 
 func (pred Title) Trigger() common.Trigger {
diff --git a/server/assets/css/main.css b/server/assets/css/main.css
index b2ace73..86b7b7f 100644
--- a/server/assets/css/main.css
+++ b/server/assets/css/main.css
@@ -14,6 +14,14 @@ a:hover {
   text-decoration: underline;
 }
 
+details summary {
+  cursor: pointer;
+}
+
+details summary > * {
+  display: inline-block;
+}
+
 @tailwind components;
 
 /* custom components */
@@ -199,4 +207,8 @@ a:hover {
   background-image: linear-gradient(180deg, rgba(255,255,255,.1), rgba(255,255,255,0));
 }
 
+.text-sm-mono {
+  font-size: 0.8125rem;
+}
+
 /* end custom utilities */
diff --git a/server/handler/frontend.go b/server/handler/frontend.go
index 84400e2..700229c 100644
--- a/server/handler/frontend.go
+++ b/server/handler/frontend.go
@@ -35,7 +35,12 @@ type FilesConfig struct {
 	Templates string `yaml:"templates"`
 }
 
-func LoadTemplates(c *FilesConfig, basePath string) (templatetree.HTMLTree, error) {
+type Membership struct {
+	Name string
+	Link string
+}
+
+func LoadTemplates(c *FilesConfig, basePath string, githubURL string) (templatetree.HTMLTree, error) {
 	if basePath == "" {
 		basePath = "/"
 	}
@@ -55,6 +60,12 @@ func LoadTemplates(c *FilesConfig, basePath string) (templatetree.HTMLTree, erro
 
 			return r
 		},
+		"hasRequires": func(requires common.Actors) bool {
+			return !requires.IsEmpty()
+		},
+		"getRequires": func(results *common.Result) map[string][]Membership {
+			return getRequires(results, strings.TrimSuffix(githubURL, "/"))
+		},
 	})
 
 	dir := c.Templates
@@ -73,3 +84,19 @@ func Static(prefix string, c *FilesConfig) http.Handler {
 
 	return http.StripPrefix(prefix, http.FileServer(http.Dir(dir)))
 }
+
+func getRequires(result *common.Result, githubURL string) map[string][]Membership {
+	membershipInfo := make(map[string][]Membership)
+	for _, org := range result.Requires.Organizations {
+		membershipInfo["Organizations"] = append(membershipInfo["Organizations"], Membership{Name: org, Link: githubURL + "/orgs/" + org + "/people"})
+	}
+	for _, team := range result.Requires.Teams {
+		teamName := strings.Split(team, "/")
+		membershipInfo["Teams"] = append(membershipInfo["Teams"], Membership{Name: team, Link: githubURL + "/orgs/" + teamName[0] + "/teams/" + teamName[1] + "/members"})
+
+	}
+	for _, user := range result.Requires.Users {
+		membershipInfo["Users"] = append(membershipInfo["Users"], Membership{Name: user, Link: githubURL + "/" + user})
+	}
+	return membershipInfo
+}
diff --git a/server/server.go b/server/server.go
index 4497eb0..e465808 100644
--- a/server/server.go
+++ b/server/server.go
@@ -175,7 +175,7 @@ func New(c *Config) (*Server, error) {
 		),
 	)
 
-	templates, err := handler.LoadTemplates(&c.Files, basePath)
+	templates, err := handler.LoadTemplates(&c.Files, basePath, c.Github.WebURL)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to load templates")
 	}
diff --git a/server/templates/details.html.tmpl b/server/templates/details.html.tmpl
index cc34454..6ba9a63 100644
--- a/server/templates/details.html.tmpl
+++ b/server/templates/details.html.tmpl
@@ -56,6 +56,23 @@
 <li data-status="{{$s}}">
   <div class="bg-white p-2 shadow-sm max-w-lg status-stripe {{$s}}">
     {{template "result-details" .}}
+    {{if (or .PredicateResults (hasRequires .Requires))}}
+    <details class="bg-light-gray5 p-2 mt-2 text-sm">
+      <summary class="text-dark-gray3 font-semibold">Details</summary>
+      <div class="border-t border-light-gray3 mt-2">
+       {{if .PredicateResults}}
+          <div class="pt-2">
+            {{template "result-predicates-details" .}}
+          </div>
+        {{end}}
+        {{if (hasRequires .Requires)}}
+          <div class="pt-2">
+          {{template "result-requires-details" .}}
+          </div>
+        {{end}}
+      </div>
+    </details>
+    {{end}}
   </div>
   {{if .Children}}
   <ul class="tree">
@@ -76,3 +93,54 @@
   {{end}}
   <p class="text-dark-gray3 text-sm">{{or .Error .StatusDescription}}</p>
 {{end}}
+
+{{define "result-predicates-details"}}
+  {{ $s := (or (and .Error "error") (.Status | print)) }}
+  {{if eq $s "skipped"}}
+    <b class="font-bold">This rule is skipped because</b>
+  {{else}}
+    <b class="font-bold">This rule is selected because</b>
+  {{end}}
+  {{template "result-predicates-info" .}}
+{{end}}
+
+{{define "result-predicates-info"}}
+  {{ $s := (or (and .Error "error") (.Status | print)) }}
+  <ul class="list-decimal list-outside pl-4 my-2">
+  {{range .PredicateResults}}
+  <li>
+  The {{.ValuePhrase}}:
+  <ul class="list-disc list-outside pl-6 py-2">
+    {{range .Values}}<li class="font-mono text-sm-mono">{{.}}</li>{{end}}
+  </ul>
+  {{if eq $s "skipped"}}do not {{end}}{{.ConditionPhrase}}
+  {{if .ConditionsMap}}
+  <dl class="pt-2">
+  {{range $k, $v := .ConditionsMap}}
+    {{if $v}}
+    <dt>{{$k}}</dt>
+    <dd>
+        <ul class="list-disc list-outside pl-6 py-2">{{range $v}}<li class="font-mono text-sm-mono">{{.}}</li>{{end}}</ul>
+    </dd>
+    {{end}}
+  {{end}}
+  </dl>
+  {{else if .ConditionValues}}
+    <ul class="list-disc list-outside pl-6 py-2">{{range .ConditionValues}}<li class="font-mono text-sm-mono">{{.}}</li>{{end}}</ul>
+  {{end}}
+  </li>
+  {{end}}
+  </ul>
+{{end}}
+
+{{define "result-requires-details"}}
+  <b class="font-bold text-sm">This rule requires review from</b>
+  <dl class="my-2">
+  {{range $k, $v := getRequires .}}
+    <dt> {{$k}}</dt>
+    <dd>
+        <ul class="list-disc list-outside pl-6 py-2">{{range $v}}<li class="font-mono text-sm-mono"><a href={{.Link}}>{{.Name}}</a></li>{{end}}</ul>
+    </dd>
+  {{end}}
+  </dl>
+{{end}}
diff --git a/yarn.lock b/yarn.lock
index f35c8c2..d4671a9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4,7 +4,7 @@
 
 "@fullhuman/postcss-purgecss@^2.1.2":
   version "2.3.0"
-  resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-2.3.0.tgz#50a954757ec78696615d3e118e3fee2d9291882e"
+  resolved "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-2.3.0.tgz#50a954757ec78696615d3e118e3fee2d9291882e"
   integrity sha512-qnKm5dIOyPGJ70kPZ5jiz0I9foVOic0j+cOzNDoo8KoCf6HjicIZ99UfO2OmE7vCYSKAAepEwJtNzpiiZAh9xw==
   dependencies:
     postcss "7.0.32"
@@ -50,7 +50,7 @@
 
 acorn-node@^1.6.1:
   version "1.8.2"
-  resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
+  resolved "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
   integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
   dependencies:
     acorn "^7.0.0"
@@ -59,13 +59,13 @@ acorn-node@^1.6.1:
 
 acorn-walk@^7.0.0:
   version "7.2.0"
-  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
+  resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
   integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
 
 acorn@^7.0.0:
-  version "7.4.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c"
-  integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==
+  version "7.4.1"
+  resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
 alphanum-sort@^1.0.2:
   version "1.0.2"
@@ -102,24 +102,36 @@ anymatch@~3.1.1:
 
 array-union@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
   integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
 
 at-least-node@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
+  resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
+autoprefixer@^10.4.4:
+  version "10.4.4"
+  resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.4.tgz#3e85a245b32da876a893d3ac2ea19f01e7ea5a1e"
+  integrity sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA==
+  dependencies:
+    browserslist "^4.20.2"
+    caniuse-lite "^1.0.30001317"
+    fraction.js "^4.2.0"
+    normalize-range "^0.1.2"
+    picocolors "^1.0.0"
+    postcss-value-parser "^4.2.0"
+
 autoprefixer@^9.4.5:
-  version "9.8.6"
-  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
-  integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==
+  version "9.8.8"
+  resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a"
+  integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==
   dependencies:
     browserslist "^4.12.0"
     caniuse-lite "^1.0.30001109"
-    colorette "^1.2.1"
     normalize-range "^0.1.2"
     num2fraction "^1.2.2"
+    picocolors "^0.2.1"
     postcss "^7.0.32"
     postcss-value-parser "^4.1.0"
 
@@ -146,14 +158,14 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-braces@^3.0.1, braces@~3.0.2:
+braces@^3.0.2, braces@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
   integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
   dependencies:
     fill-range "^7.0.1"
 
-browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.0, browserslist@^4.16.6:
+browserslist@^4.0.0, browserslist@^4.16.0, browserslist@^4.16.6:
   version "4.16.6"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
   integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
@@ -164,10 +176,21 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.0, browserslist@^4
     escalade "^3.1.1"
     node-releases "^1.1.71"
 
+browserslist@^4.12.0, browserslist@^4.20.2:
+  version "4.20.2"
+  resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88"
+  integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==
+  dependencies:
+    caniuse-lite "^1.0.30001317"
+    electron-to-chromium "^1.4.84"
+    escalade "^3.1.1"
+    node-releases "^2.0.2"
+    picocolors "^1.0.0"
+
 bytes@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
-  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+  version "3.1.2"
+  resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+  integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
 
 call-bind@^1.0.0, call-bind@^1.0.2:
   version "1.0.2"
@@ -192,10 +215,15 @@ caniuse-api@^3.0.0:
     lodash.memoize "^4.1.2"
     lodash.uniq "^4.5.0"
 
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
-  version "1.0.30001228"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa"
-  integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219:
+  version "1.0.30001325"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz"
+  integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ==
+
+caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001317:
+  version "1.0.30001332"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd"
+  integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==
 
 chalk@^2.4.1, chalk@^2.4.2:
   version "2.4.2"
@@ -206,15 +234,7 @@ chalk@^2.4.1, chalk@^2.4.2:
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-"chalk@^3.0.0 || ^4.0.0", chalk@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
-  integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
-  dependencies:
-    ansi-styles "^4.1.0"
-    supports-color "^7.1.0"
-
-chalk@^4.1.0:
+"chalk@^3.0.0 || ^4.0.0", chalk@^4.0.0, chalk@^4.1.0:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
   integrity sha1-qsTit3NKdAhnrrFr8CqtVWoeegE=
@@ -246,7 +266,7 @@ cliui@^7.0.2:
     strip-ansi "^6.0.0"
     wrap-ansi "^7.0.0"
 
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0, color-convert@^1.9.3:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
   integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -270,21 +290,21 @@ color-name@^1.0.0, color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-color-string@^1.5.2:
-  version "1.5.5"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
-  integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
+color-string@^1.6.0:
+  version "1.9.0"
+  resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa"
+  integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==
   dependencies:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
 
 color@^3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
-  integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==
+  version "3.2.1"
+  resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
+  integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
   dependencies:
-    color-convert "^1.9.1"
-    color-string "^1.5.2"
+    color-convert "^1.9.3"
+    color-string "^1.6.0"
 
 colord@^2.0.1:
   version "2.4.0"
@@ -298,7 +318,7 @@ colorette@^1.2.1, colorette@^1.2.2:
 
 commander@^5.0.0:
   version "5.1.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
+  resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
   integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
 
 commander@^6.0.0:
@@ -365,7 +385,7 @@ css-tree@^1.1.2:
 
 css-unit-converter@^1.1.1:
   version "1.1.2"
-  resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21"
+  resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21"
   integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==
 
 css-what@^5.0.0:
@@ -444,17 +464,17 @@ define-properties@^1.1.3:
 
 defined@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+  resolved "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
   integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
 
 dependency-graph@^0.9.0:
   version "0.9.0"
-  resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.9.0.tgz#11aed7e203bc8b00f48356d92db27b265c445318"
+  resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.9.0.tgz#11aed7e203bc8b00f48356d92db27b265c445318"
   integrity sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==
 
 detective@^5.2.0:
   version "5.2.0"
-  resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
+  resolved "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
   integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==
   dependencies:
     acorn-node "^1.6.1"
@@ -503,6 +523,11 @@ electron-to-chromium@^1.3.723:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz#f632d900a1f788dab22fec9c62ec5c9c8f0c4052"
   integrity sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig==
 
+electron-to-chromium@^1.4.84:
+  version "1.4.111"
+  resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.111.tgz#897613f6504f3f17c9381c7499a635b413e4df4e"
+  integrity sha512-/s3+fwhKf1YK4k7btOImOzCQLpUjS6MaPf0ODTNuT4eTM1Bg4itBpLkydhOzJmpmH6Z9eXFyuuK5czsmzRzwtw==
+
 emoji-regex@^8.0.0:
   version "8.0.0"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -561,17 +586,16 @@ escape-string-regexp@^1.0.5:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
-fast-glob@^3.1.1:
-  version "3.2.4"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
-  integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
+fast-glob@^3.2.9:
+  version "3.2.11"
+  resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
+  integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
   dependencies:
     "@nodelib/fs.stat" "^2.0.2"
     "@nodelib/fs.walk" "^1.2.3"
-    glob-parent "^5.1.0"
+    glob-parent "^5.1.2"
     merge2 "^1.3.0"
-    micromatch "^4.0.2"
-    picomatch "^2.2.1"
+    micromatch "^4.0.4"
 
 fastq@^1.6.0:
   version "1.8.0"
@@ -587,9 +611,14 @@ fill-range@^7.0.1:
   dependencies:
     to-regex-range "^5.0.1"
 
+fraction.js@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
+  integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
+
 fs-extra@^8.0.0:
   version "8.1.0"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+  resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
   integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
   dependencies:
     graceful-fs "^4.2.0"
@@ -597,14 +626,14 @@ fs-extra@^8.0.0:
     universalify "^0.1.0"
 
 fs-extra@^9.0.0:
-  version "9.0.1"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc"
-  integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==
+  version "9.1.0"
+  resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
+  integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
   dependencies:
     at-least-node "^1.0.0"
     graceful-fs "^4.2.0"
     jsonfile "^6.0.1"
-    universalify "^1.0.0"
+    universalify "^2.0.0"
 
 fs.realpath@^1.0.0:
   version "1.0.0"
@@ -637,17 +666,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
 
 get-stdin@^8.0.0:
   version "8.0.0"
-  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
-  integrity sha1-y61qc/63X27rIrqeAfiaooqpelM=
+  resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
+  integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
 
-glob-parent@^5.1.0, glob-parent@~5.1.0:
+glob-parent@^5.1.2, glob-parent@~5.1.0:
   version "5.1.2"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
   dependencies:
     is-glob "^4.0.1"
 
-glob@^7.0.0, glob@^7.1.2:
+glob@^7.0.0:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -659,16 +688,28 @@ glob@^7.0.0, glob@^7.1.2:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+glob@^7.1.2:
+  version "7.2.0"
+  resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 globby@^11.0.0:
-  version "11.0.1"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
-  integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==
+  version "11.1.0"
+  resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
+  integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
   dependencies:
     array-union "^2.1.0"
     dir-glob "^3.0.1"
-    fast-glob "^3.1.1"
-    ignore "^5.1.4"
-    merge2 "^1.3.0"
+    fast-glob "^3.2.9"
+    ignore "^5.2.0"
+    merge2 "^1.4.1"
     slash "^3.0.0"
 
 graceful-fs@^4.1.2, graceful-fs@^4.1.6:
@@ -728,10 +769,15 @@ hsla-regex@^1.0.0:
   resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
   integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
 
-ignore@^5.1.4:
-  version "5.1.8"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
-  integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
+html-tags@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961"
+  integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==
+
+ignore@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
+  integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
 
 import-cwd@^3.0.0:
   version "3.0.0"
@@ -777,7 +823,7 @@ is-arrayish@^0.2.1:
 
 is-arrayish@^0.3.1:
   version "0.3.2"
-  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
   integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
 
 is-bigint@^1.0.1:
@@ -816,6 +862,13 @@ is-color-stop@^1.1.0:
     rgb-regex "^1.0.1"
     rgba-regex "^1.0.0"
 
+is-core-module@^2.8.1:
+  version "2.8.1"
+  resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
+  integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
+  dependencies:
+    has "^1.0.3"
+
 is-date-object@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
@@ -890,7 +943,7 @@ json-parse-better-errors@^1.0.1:
 
 jsonfile@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
   integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
   optionalDependencies:
     graceful-fs "^4.1.6"
@@ -949,19 +1002,14 @@ lodash.sortby@^4.7.0:
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
 
-lodash.toarray@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
-  integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE=
-
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^4.17.20:
+lodash@^4.17.20, lodash@^4.17.21:
   version "4.17.21"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
 mdn-data@2.0.14:
@@ -974,18 +1022,18 @@ memorystream@^0.3.1:
   resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
   integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
 
-merge2@^1.3.0:
+merge2@^1.3.0, merge2@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
-micromatch@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
-  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+micromatch@^4.0.4:
+  version "4.0.5"
+  resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
   dependencies:
-    braces "^3.0.1"
-    picomatch "^2.0.5"
+    braces "^3.0.2"
+    picomatch "^2.3.1"
 
 minimatch@^3.0.4:
   version "3.0.4"
@@ -996,7 +1044,7 @@ minimatch@^3.0.4:
 
 minimist@^1.1.1:
   version "1.2.6"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+  resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
   integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
 
 nanoid@^3.1.23:
@@ -1010,17 +1058,22 @@ nice-try@^1.0.4:
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
 node-emoji@^1.8.1:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
-  integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==
+  version "1.11.0"
+  resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c"
+  integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==
   dependencies:
-    lodash.toarray "^4.4.0"
+    lodash "^4.17.21"
 
 node-releases@^1.1.71:
   version "1.1.72"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe"
   integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==
 
+node-releases@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96"
+  integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==
+
 normalize-package-data@^2.3.2:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -1048,7 +1101,7 @@ normalize-url@^6.0.1:
 
 normalize.css@^8.0.1:
   version "8.0.1"
-  resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
+  resolved "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
   integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
 
 npm-run-all@^4.1.5:
@@ -1075,18 +1128,18 @@ nth-check@^2.0.0:
 
 num2fraction@^1.2.2:
   version "1.2.2"
-  resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+  resolved "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
   integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
 
 object-assign@^4.1.1:
   version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
 
 object-hash@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
-  integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
+  version "2.2.0"
+  resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
+  integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
 
 object-inspect@^1.9.0:
   version "1.9.0"
@@ -1133,7 +1186,7 @@ path-key@^2.0.1:
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
   integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
 
-path-parse@^1.0.6:
+path-parse@^1.0.6, path-parse@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
@@ -1150,11 +1203,26 @@ path-type@^4.0.0:
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
-picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
+picocolors@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
+  integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
   integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
 
+picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
 pidtree@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
@@ -1180,8 +1248,8 @@ postcss-calc@^8.0.0:
 
 postcss-cli@^8.0:
   version "8.3.1"
-  resolved "https://registry.yarnpkg.com/postcss-cli/-/postcss-cli-8.3.1.tgz#865dad08300ac59ae9cecb7066780aa81c767a77"
-  integrity sha1-hl2tCDAKxZrpzstwZngKqBx2enc=
+  resolved "https://registry.npmjs.org/postcss-cli/-/postcss-cli-8.3.1.tgz#865dad08300ac59ae9cecb7066780aa81c767a77"
+  integrity sha512-leHXsQRq89S3JC9zw/tKyiVV2jAhnfQe0J8VI4eQQbUjwIe0XxVqLrR+7UsahF1s9wi4GlqP6SJ8ydf44cgF2Q==
   dependencies:
     chalk "^4.0.0"
     chokidar "^3.3.0"
@@ -1235,7 +1303,7 @@ postcss-discard-overridden@^5.0.1:
 
 postcss-functions@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-functions/-/postcss-functions-3.0.0.tgz#0e94d01444700a481de20de4d55fb2640564250e"
+  resolved "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz#0e94d01444700a481de20de4d55fb2640564250e"
   integrity sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=
   dependencies:
     glob "^7.1.2"
@@ -1245,7 +1313,7 @@ postcss-functions@^3.0.0:
 
 postcss-js@^2.0.0:
   version "2.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-2.0.3.tgz#a96f0f23ff3d08cec7dc5b11bf11c5f8077cdab9"
+  resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-2.0.3.tgz#a96f0f23ff3d08cec7dc5b11bf11c5f8077cdab9"
   integrity sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w==
   dependencies:
     camelcase-css "^2.0.1"
@@ -1317,7 +1385,7 @@ postcss-minify-selectors@^5.1.0:
 
 postcss-nested@^4.1.1:
   version "4.2.3"
-  resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.3.tgz#c6f255b0a720549776d220d00c4b70cd244136f6"
+  resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-4.2.3.tgz#c6f255b0a720549776d220d00c4b70cd244136f6"
   integrity sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw==
   dependencies:
     postcss "^7.0.32"
@@ -1427,13 +1495,12 @@ postcss-reporter@^7.0.0:
     lodash.sortby "^4.7.0"
 
 postcss-selector-parser@^6.0.0:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
-  integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+  version "6.0.10"
+  resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
+  integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
   dependencies:
     cssesc "^3.0.0"
-    indexes-of "^1.0.1"
-    uniq "^1.0.1"
+    util-deprecate "^1.0.2"
 
 postcss-selector-parser@^6.0.2:
   version "6.0.4"
@@ -1472,7 +1539,7 @@ postcss-unique-selectors@^5.0.1:
 
 postcss-value-parser@^3.3.0:
   version "3.3.1"
-  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
+  resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
   integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
 
 postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
@@ -1480,9 +1547,14 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
 
-postcss@7.0.32, postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.32:
+postcss-value-parser@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@7.0.32:
   version "7.0.32"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
+  resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
   integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
   dependencies:
     chalk "^2.4.2"
@@ -1491,13 +1563,21 @@ postcss@7.0.32, postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.32:
 
 postcss@^6.0.9:
   version "6.0.23"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
+  resolved "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
   integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==
   dependencies:
     chalk "^2.4.1"
     source-map "^0.6.1"
     supports-color "^5.4.0"
 
+postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.32:
+  version "7.0.39"
+  resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309"
+  integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
+  dependencies:
+    picocolors "^0.2.1"
+    source-map "^0.6.1"
+
 postcss@^8.0, postcss@^8.2.1:
   version "8.3.6"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea"
@@ -1514,7 +1594,7 @@ pretty-hrtime@^1.0.3:
 
 purgecss@^2.3.0:
   version "2.3.0"
-  resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-2.3.0.tgz#5327587abf5795e6541517af8b190a6fb5488bb3"
+  resolved "https://registry.npmjs.org/purgecss/-/purgecss-2.3.0.tgz#5327587abf5795e6541517af8b190a6fb5488bb3"
   integrity sha512-BE5CROfVGsx2XIhxGuZAT7rTH9lLeQx/6M0P7DTXQH4IUc3BBzs9JUzt4yzGf3JrH9enkeq6YJBe9CTtkm1WmQ==
   dependencies:
     commander "^5.0.0"
@@ -1556,9 +1636,9 @@ readdirp@~3.4.0:
     picomatch "^2.2.1"
 
 reduce-css-calc@^2.1.6:
-  version "2.1.7"
-  resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"
-  integrity sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==
+  version "2.1.8"
+  resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03"
+  integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==
   dependencies:
     css-unit-converter "^1.1.1"
     postcss-value-parser "^3.3.0"
@@ -1581,11 +1661,13 @@ resolve@^1.10.0:
     path-parse "^1.0.6"
 
 resolve@^1.14.2:
-  version "1.17.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
-  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+  version "1.22.0"
+  resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
+  integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
   dependencies:
-    path-parse "^1.0.6"
+    is-core-module "^2.8.1"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
 
 reusify@^1.0.4:
   version "1.0.4"
@@ -1631,14 +1713,14 @@ shell-quote@^1.6.1:
 
 simple-swizzle@^0.2.2:
   version "0.2.2"
-  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
   integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
   dependencies:
     is-arrayish "^0.3.1"
 
 slash@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
 source-map-js@^0.6.2:
@@ -1744,7 +1826,7 @@ supports-color@^5.3.0, supports-color@^5.4.0:
 
 supports-color@^6.1.0:
   version "6.1.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+  resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
   integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
   dependencies:
     has-flag "^3.0.0"
@@ -1756,6 +1838,11 @@ supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
 svgo@^2.3.0:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.3.1.tgz#603a69ce50311c0e36791528f549644ec1b3f4bc"
@@ -1770,9 +1857,9 @@ svgo@^2.3.0:
     stable "^0.1.8"
 
 tailwindcss@^1.0:
-  version "1.7.5"
-  resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.7.5.tgz#4a54ffcc5691822cbc20416242e92505728f79b7"
-  integrity sha512-thDHLkRioJh0/62EFcEfQCCBEsZXpluehymrPzn8Hkycy8uI9svvtOqyWtcfkBPB0s5yb6R2tY9zPzh5mIr0Wg==
+  version "1.9.6"
+  resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.9.6.tgz#0c5089911d24e1e98e592a31bfdb3d8f34ecf1a0"
+  integrity sha512-nY8WYM/RLPqGsPEGEV2z63riyQPcHYZUJpAwdyBzVpxQHOHqHE+F/fvbCeXhdF1+TA5l72vSkZrtYCB9hRcwkQ==
   dependencies:
     "@fullhuman/postcss-purgecss" "^2.1.2"
     autoprefixer "^9.4.5"
@@ -1782,6 +1869,7 @@ tailwindcss@^1.0:
     color "^3.1.2"
     detective "^5.2.0"
     fs-extra "^8.0.0"
+    html-tags "^3.1.0"
     lodash "^4.17.20"
     node-emoji "^1.8.1"
     normalize.css "^8.0.1"
@@ -1830,7 +1918,7 @@ uniqs@^2.0.0:
 
 universalify@^0.1.0:
   version "0.1.2"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
   integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
 
 universalify@^1.0.0:
@@ -1838,6 +1926,11 @@ universalify@^1.0.0:
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
   integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
 
+universalify@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
+  integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+
 util-deprecate@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -1890,7 +1983,7 @@ wrappy@1:
 
 xtend@^4.0.2:
   version "4.0.2"
-  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
 
 y18n@^5.0.5:
@@ -1905,13 +1998,13 @@ yaml@^1.10.2:
 
 yargs-parser@^20.2.2:
   version "20.2.9"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
-  integrity sha1-LrfcOwKJcY/ClfNidThFxBoMlO4=
+  resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
 
 yargs@^16.0.0:
   version "16.2.0"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
-  integrity sha1-HIK/D2tqZur85+8w43b0mhJHf2Y=
+  resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
   dependencies:
     cliui "^7.0.2"
     escalade "^3.1.1"


Test patch:
diff --git a/policy/predicate/author_test.go b/policy/predicate/author_test.go
index 19ee5cf..226414b 100644
--- a/policy/predicate/author_test.go
+++ b/policy/predicate/author_test.go
@@ -36,7 +36,6 @@ func TestHasAuthorIn(t *testing.T) {
 	runAuthorTests(t, p, []AuthorTestCase{
 		{
 			"noMatch",
-			false,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				TeamMemberships: map[string][]string{
@@ -50,17 +49,33 @@ func TestHasAuthorIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"ttest"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"authorInUsers",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mhaypenny"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"authorInTeams",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mortonh",
 				TeamMemberships: map[string][]string{
@@ -70,10 +85,18 @@ func TestHasAuthorIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mortonh"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"authorInOrgs",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mortonh",
 				OrgMemberships: map[string][]string{
@@ -83,6 +106,15 @@ func TestHasAuthorIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mortonh"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 	})
 }
@@ -99,7 +131,6 @@ func TestHasContributorIn(t *testing.T) {
 	runAuthorTests(t, p, []AuthorTestCase{
 		{
 			"commitAuthorInUsers",
-			true,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				CommitsValue: []*pull.Commit{
@@ -115,10 +146,18 @@ func TestHasContributorIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mhaypenny"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"commitCommitterInUsers",
-			true,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				CommitsValue: []*pull.Commit{
@@ -134,10 +173,18 @@ func TestHasContributorIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mhaypenny"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"commitAuthorInTeam",
-			true,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				TeamMemberships: map[string][]string{
@@ -158,10 +205,18 @@ func TestHasContributorIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mhaypenny"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"commitAuthorInOrg",
-			true,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				OrgMemberships: map[string][]string{
@@ -182,6 +237,15 @@ func TestHasContributorIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mhaypenny"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 	})
 }
@@ -198,7 +262,6 @@ func TestOnlyHasContributorsIn(t *testing.T) {
 	runAuthorTests(t, p, []AuthorTestCase{
 		{
 			"authorNotInList",
-			false,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				CommitsValue: []*pull.Commit{
@@ -209,10 +272,18 @@ func TestOnlyHasContributorsIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"ttest"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"containsCommitAuthorNotInList",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -228,10 +299,18 @@ func TestOnlyHasContributorsIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"ttest"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"committersInListButAuthorsAreNot",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -247,10 +326,18 @@ func TestOnlyHasContributorsIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"ttest1"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"commitAuthorInTeam",
-			true,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				TeamMemberships: map[string][]string{
@@ -271,10 +358,18 @@ func TestOnlyHasContributorsIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mhaypenny", "ttest"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"commitAuthorInOrg",
-			true,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				OrgMemberships: map[string][]string{
@@ -295,6 +390,15 @@ func TestOnlyHasContributorsIn(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"mhaypenny", "ttest"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 	})
 }
@@ -305,7 +409,6 @@ func TestAuthorIsOnlyContributor(t *testing.T) {
 	runAuthorTests(t, p, []AuthorTestCase{
 		{
 			"authorIsOnlyContributor",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -321,10 +424,14 @@ func TestAuthorIsOnlyContributor(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"mhaypenny"},
+				ConditionValues: []string{"they are the only contributors"},
+			},
 		},
 		{
 			"authorIsOnlyContributorViaWeb",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -336,10 +443,14 @@ func TestAuthorIsOnlyContributor(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"mhaypenny"},
+				ConditionValues: []string{"they are the only contributors"},
+			},
 		},
 		{
 			"authorIsNotOnlyAuthor",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -355,10 +466,14 @@ func TestAuthorIsOnlyContributor(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"mhaypenny"},
+				ConditionValues: []string{"they are the only contributors"},
+			},
 		},
 		{
 			"authorIsNotOnlyCommitter",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -374,6 +489,11 @@ func TestAuthorIsOnlyContributor(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"mhaypenny"},
+				ConditionValues: []string{"they are the only contributors"},
+			},
 		},
 	})
 }
@@ -384,7 +504,6 @@ func TestAuthorIsNotOnlyContributor(t *testing.T) {
 	runAuthorTests(t, p, []AuthorTestCase{
 		{
 			"authorIsOnlyContributor",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -400,10 +519,14 @@ func TestAuthorIsNotOnlyContributor(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"mhaypenny"},
+				ConditionValues: []string{"they are not the only contributors"},
+			},
 		},
 		{
 			"authorIsNotOnlyAuthor",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -419,10 +542,14 @@ func TestAuthorIsNotOnlyContributor(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"mhaypenny"},
+				ConditionValues: []string{"they are not the only contributors"},
+			},
 		},
 		{
 			"authorIsNotOnlyCommitter",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -438,14 +565,19 @@ func TestAuthorIsNotOnlyContributor(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"mhaypenny"},
+				ConditionValues: []string{"they are not the only contributors"},
+			},
 		},
 	})
 }
 
 type AuthorTestCase struct {
-	Name     string
-	Expected bool
-	Context  pull.Context
+	Name                    string
+	Context                 pull.Context
+	ExpectedPredicateResult *common.PredicateResult
 }
 
 func runAuthorTests(t *testing.T, p Predicate, cases []AuthorTestCase) {
@@ -453,9 +585,9 @@ func runAuthorTests(t *testing.T, p Predicate, cases []AuthorTestCase) {
 
 	for _, tc := range cases {
 		t.Run(tc.Name, func(t *testing.T) {
-			ok, _, err := p.Evaluate(ctx, tc.Context)
+			predicateResult, err := p.Evaluate(ctx, tc.Context)
 			if assert.NoError(t, err, "evaluation failed") {
-				assert.Equal(t, tc.Expected, ok, "predicate was not correct")
+				assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult)
 			}
 		})
 	}
diff --git a/policy/predicate/branch_test.go b/policy/predicate/branch_test.go
index f2ec0aa..19b9598 100644
--- a/policy/predicate/branch_test.go
+++ b/policy/predicate/branch_test.go
@@ -28,58 +28,90 @@ func TestBranches(t *testing.T) {
 	runBranchesTestCase(t, "^master$", []branchesTestCase{
 		{
 			"simple match - master",
-			true,
 			"master",
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"master"},
+				ConditionValues: []string{"^master$"},
+			},
 		},
 		{
 			"simple non match",
-			false,
 			"another-branch",
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"another-branch"},
+				ConditionValues: []string{"^master$"},
+			},
 		},
 		{
 			"tests anchoring",
-			false,
 			"not-master",
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"not-master"},
+				ConditionValues: []string{"^master$"},
+			},
 		},
 	})
 
 	runBranchesTestCase(t, ".*", []branchesTestCase{
 		{
 			"matches all example 1",
-			true,
 			"master",
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"master"},
+				ConditionValues: []string{".*"},
+			},
 		},
 		{
 			"matches all example 2",
-			true,
 			"another-one",
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"another-one"},
+				ConditionValues: []string{".*"},
+			},
 		},
 	})
 
 	runBranchesTestCase(t, "(prod|staging)", []branchesTestCase{
 		{
 			"matches pattern - prod",
-			true,
 			"prod",
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"prod"},
+				ConditionValues: []string{"(prod|staging)"},
+			},
 		},
 		{
 			"matches pattern - staging",
-			true,
 			"staging",
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"staging"},
+				ConditionValues: []string{"(prod|staging)"},
+			},
 		},
 		{
 			"matches pattern - not-a-match",
-			false,
 			"not-a-match",
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"not-a-match"},
+				ConditionValues: []string{"(prod|staging)"},
+			},
 		},
 	})
 }
 
 // TODO: generalize this and use it all our test cases
 type branchesTestCase struct {
-	name       string
-	expected   bool
-	branchName string
+	name                    string
+	branchName              string
+	ExpectedPredicateResult *common.PredicateResult
 }
 
 func runBranchesTestCase(t *testing.T, regex string, cases []branchesTestCase) {
@@ -103,16 +135,16 @@ func runBranchesTestCase(t *testing.T, regex string, cases []branchesTestCase) {
 		}
 
 		t.Run(tc.name+" targets_branch", func(t *testing.T) {
-			ok, _, err := targetsPredicate.Evaluate(ctx, targetsContext)
+			predicateResult, err := targetsPredicate.Evaluate(ctx, targetsContext)
 			if assert.NoError(t, err, "targets_branch predicate evaluation failed") {
-				assert.Equal(t, tc.expected, ok, "targets_branch predicate was not correct")
+				assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult)
 			}
 		})
 
 		t.Run(tc.name+" from_branch", func(t *testing.T) {
-			ok, _, err := fromPredicate.Evaluate(ctx, fromContext)
+			predicateResult, err := fromPredicate.Evaluate(ctx, fromContext)
 			if assert.NoError(t, err, "from_branch predicate evaluation failed") {
-				assert.Equal(t, tc.expected, ok, "from_branch predicate was not correct")
+				assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult)
 			}
 		})
 	}
diff --git a/policy/predicate/file_test.go b/policy/predicate/file_test.go
index 6a8477e..aed8159 100644
--- a/policy/predicate/file_test.go
+++ b/policy/predicate/file_test.go
@@ -39,12 +39,18 @@ func TestChangedFiles(t *testing.T) {
 	runFileTests(t, p, []FileTestCase{
 		{
 			"empty",
-			false,
 			[]*pull.File{},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{},
+				ConditionsMap: map[string][]string{
+					"path patterns":  {"app/.*\\.go", "server/.*\\.go"},
+					"while ignoring": {".*/special\\.go"},
+				},
+			},
 		},
 		{
 			"onlyMatches",
-			true,
 			[]*pull.File{
 				{
 					Filename: "app/client.go",
@@ -55,10 +61,17 @@ func TestChangedFiles(t *testing.T) {
 					Status:   pull.FileModified,
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"app/client.go"},
+				ConditionsMap: map[string][]string{
+					"path patterns":  {"app/.*\\.go", "server/.*\\.go"},
+					"while ignoring": {".*/special\\.go"},
+				},
+			},
 		},
 		{
 			"someMatches",
-			true,
 			[]*pull.File{
 				{
 					Filename: "app/client.go",
@@ -69,10 +82,17 @@ func TestChangedFiles(t *testing.T) {
 					Status:   pull.FileModified,
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"app/client.go"},
+				ConditionsMap: map[string][]string{
+					"path patterns":  {"app/.*\\.go", "server/.*\\.go"},
+					"while ignoring": {".*/special\\.go"},
+				},
+			},
 		},
 		{
 			"noMatches",
-			false,
 			[]*pull.File{
 				{
 					Filename: "model/order.go",
@@ -83,10 +103,17 @@ func TestChangedFiles(t *testing.T) {
 					Status:   pull.FileModified,
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"model/order.go", "model/user.go"},
+				ConditionsMap: map[string][]string{
+					"path patterns":  {"app/.*\\.go", "server/.*\\.go"},
+					"while ignoring": {".*/special\\.go"},
+				},
+			},
 		},
 		{
 			"ignoreAll",
-			false,
 			[]*pull.File{
 				{
 					Filename: "app/special.go",
@@ -97,10 +124,17 @@ func TestChangedFiles(t *testing.T) {
 					Status:   pull.FileModified,
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"app/special.go", "server/special.go"},
+				ConditionsMap: map[string][]string{
+					"path patterns":  {"app/.*\\.go", "server/.*\\.go"},
+					"while ignoring": {".*/special\\.go"},
+				},
+			},
 		},
 		{
 			"ignoreSome",
-			true,
 			[]*pull.File{
 				{
 					Filename: "app/normal.go",
@@ -111,6 +145,14 @@ func TestChangedFiles(t *testing.T) {
 					Status:   pull.FileModified,
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"app/normal.go"},
+				ConditionsMap: map[string][]string{
+					"path patterns":  {"app/.*\\.go", "server/.*\\.go"},
+					"while ignoring": {".*/special\\.go"},
+				},
+			},
 		},
 	})
 }
@@ -126,12 +168,15 @@ func TestOnlyChangedFiles(t *testing.T) {
 	runFileTests(t, p, []FileTestCase{
 		{
 			"empty",
-			false,
 			[]*pull.File{},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{},
+				ConditionValues: []string{"app/.*\\.go", "server/.*\\.go"},
+			},
 		},
 		{
 			"onlyMatches",
-			true,
 			[]*pull.File{
 				{
 					Filename: "app/client.go",
@@ -142,10 +187,14 @@ func TestOnlyChangedFiles(t *testing.T) {
 					Status:   pull.FileModified,
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"app/client.go", "server/server.go"},
+				ConditionValues: []string{"app/.*\\.go", "server/.*\\.go"},
+			},
 		},
 		{
 			"someMatches",
-			false,
 			[]*pull.File{
 				{
 					Filename: "app/client.go",
@@ -156,10 +205,14 @@ func TestOnlyChangedFiles(t *testing.T) {
 					Status:   pull.FileModified,
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"model/user.go"},
+				ConditionValues: []string{"app/.*\\.go", "server/.*\\.go"},
+			},
 		},
 		{
 			"noMatches",
-			false,
 			[]*pull.File{
 				{
 					Filename: "model/order.go",
@@ -170,6 +223,11 @@ func TestOnlyChangedFiles(t *testing.T) {
 					Status:   pull.FileModified,
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"model/order.go"},
+				ConditionValues: []string{"app/.*\\.go", "server/.*\\.go"},
+			},
 		},
 	})
 }
@@ -183,27 +241,39 @@ func TestModifiedLines(t *testing.T) {
 	runFileTests(t, p, []FileTestCase{
 		{
 			"empty",
-			false,
 			[]*pull.File{},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"+0", "-0"},
+				ConditionValues: []string{"added lines > 100", "deleted lines > 10"},
+			},
 		},
 		{
 			"additions",
-			true,
 			[]*pull.File{
 				{Additions: 55},
 				{Additions: 10},
 				{Additions: 45},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"+110"},
+				ConditionValues: []string{"added lines > 100"},
+			},
 		},
 		{
 			"deletions",
-			true,
 			[]*pull.File{
 				{Additions: 5},
 				{Additions: 10, Deletions: 10},
 				{Additions: 5},
 				{Deletions: 10},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"-20"},
+				ConditionValues: []string{"deleted lines > 10"},
+			},
 		},
 	})
 
@@ -214,13 +284,17 @@ func TestModifiedLines(t *testing.T) {
 	runFileTests(t, p, []FileTestCase{
 		{
 			"total",
-			true,
 			[]*pull.File{
 				{Additions: 20, Deletions: 20},
 				{Additions: 20},
 				{Deletions: 20},
 				{Additions: 20, Deletions: 20},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"total 120"},
+				ConditionValues: []string{"total modifications > 100"},
+			},
 		},
 	})
 }
@@ -316,9 +390,9 @@ func TestComparisonExpr(t *testing.T) {
 }
 
 type FileTestCase struct {
-	Name     string
-	Expected bool
-	Files    []*pull.File
+	Name                    string
+	Files                   []*pull.File
+	ExpectedPredicateResult *common.PredicateResult
 }
 
 func runFileTests(t *testing.T, p Predicate, cases []FileTestCase) {
@@ -330,9 +404,9 @@ func runFileTests(t *testing.T, p Predicate, cases []FileTestCase) {
 				ChangedFilesValue: tc.Files,
 			}
 
-			ok, _, err := p.Evaluate(ctx, prctx)
+			predicateResult, err := p.Evaluate(ctx, prctx)
 			if assert.NoError(t, err, "evaluation failed") {
-				assert.Equal(t, tc.Expected, ok, "predicate was not correct")
+				assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult)
 			}
 		})
 	}
diff --git a/policy/predicate/label_test.go b/policy/predicate/label_test.go
index d701744..e1cfc55 100644
--- a/policy/predicate/label_test.go
+++ b/policy/predicate/label_test.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/palantir/policy-bot/policy/common"
 	"github.com/palantir/policy-bot/pull"
 	"github.com/palantir/policy-bot/pull/pulltest"
 	"github.com/stretchr/testify/assert"
@@ -29,37 +30,44 @@ func TestHasLabels(t *testing.T) {
 	runLabelsTestCase(t, p, []HasLabelsTestCase{
 		{
 			"all labels",
-			true,
 			&pulltest.Context{
 				LabelsValue: []string{"foo", "bar"},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"foo", "bar"},
+				ConditionValues: []string{"foo", "bar"},
+			},
 		},
 		{
 			"missing a label",
-			false,
 			&pulltest.Context{
 				LabelsValue: []string{"foo"},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"foo"},
+				ConditionValues: []string{"bar"},
+			},
 		},
 		{
 			"no labels",
-			false,
 			&pulltest.Context{
 				LabelsValue: []string{},
 			},
-		},
-		{
-			"labels does not exist",
-			false,
-			&pulltest.Context{},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{},
+				ConditionValues: []string{"foo"},
+			},
 		},
 	})
 }
 
 type HasLabelsTestCase struct {
-	name     string
-	expected bool
-	context  pull.Context
+	name                    string
+	context                 pull.Context
+	ExpectedPredicateResult *common.PredicateResult
 }
 
 func runLabelsTestCase(t *testing.T, p Predicate, cases []HasLabelsTestCase) {
@@ -67,9 +75,9 @@ func runLabelsTestCase(t *testing.T, p Predicate, cases []HasLabelsTestCase) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			ok, _, err := p.Evaluate(ctx, tc.context)
+			predicateResult, err := p.Evaluate(ctx, tc.context)
 			if assert.NoError(t, err, "evaluation failed") {
-				assert.Equal(t, tc.expected, ok, "predicate was not correct")
+				assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult)
 			}
 		})
 	}
diff --git a/policy/predicate/predicate_test.go b/policy/predicate/predicate_test.go
new file mode 100644
index 0000000..b9fff86
--- /dev/null
+++ b/policy/predicate/predicate_test.go
@@ -0,0 +1,29 @@
+// Copyright 2019 Palantir Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package predicate
+
+import (
+	"testing"
+
+	"github.com/palantir/policy-bot/policy/common"
+	"github.com/stretchr/testify/assert"
+)
+
+func assertPredicateResult(t *testing.T, expected, actual *common.PredicateResult) {
+	assert.Equal(t, expected.Satisfied, actual.Satisfied, "predicate was not correct")
+	assert.Equal(t, expected.Values, actual.Values, "values were not correct")
+	assert.Equal(t, expected.ConditionsMap, actual.ConditionsMap, "conditions were not correct")
+	assert.Equal(t, expected.ConditionValues, actual.ConditionValues, "conditions were not correct")
+}
diff --git a/policy/predicate/signature_test.go b/policy/predicate/signature_test.go
index 768bcb0..0733f74 100644
--- a/policy/predicate/signature_test.go
+++ b/policy/predicate/signature_test.go
@@ -31,7 +31,6 @@ func TestHasValidSignatures(t *testing.T) {
 	testCases := []SignatureTestCase{
 		{
 			"ValidSignature",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -49,10 +48,14 @@ func TestHasValidSignatures(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"abcdef123456789"},
+				ConditionValues: []string{"valid signatures"},
+			},
 		},
 		{
 			"InvalidSignature",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -70,10 +73,14 @@ func TestHasValidSignatures(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"abcdef123456789"},
+				ConditionValues: []string{"valid signatures"},
+			},
 		},
 		{
 			"NoSignature",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -85,6 +92,11 @@ func TestHasValidSignatures(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"abcdef123456789"},
+				ConditionValues: []string{"valid signatures"},
+			},
 		},
 	}
 
@@ -92,7 +104,7 @@ func TestHasValidSignatures(t *testing.T) {
 
 	// Invert the expected outcomes and test against the false predicate
 	for idx := range testCases {
-		testCases[idx].Expected = !testCases[idx].Expected
+		testCases[idx].ExpectedPredicateResult.Satisfied = !testCases[idx].ExpectedPredicateResult.Satisfied
 	}
 	runSignatureTests(t, pFalse, testCases)
 }
@@ -109,7 +121,6 @@ func TestHasValidSignaturesBy(t *testing.T) {
 	runSignatureTests(t, p, []SignatureTestCase{
 		{
 			"ValidSignatureByUser",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -127,10 +138,18 @@ func TestHasValidSignaturesBy(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"abcdef123456789"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"ValidSignatureButNotUser",
-			false,
 			&pulltest.Context{
 				AuthorValue: "badcommitter",
 				CommitsValue: []*pull.Commit{
@@ -148,10 +167,18 @@ func TestHasValidSignaturesBy(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"badcommitter"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"ValidSignatureByTeamMember",
-			true,
 			&pulltest.Context{
 				AuthorValue: "ttest",
 				TeamMemberships: map[string][]string{
@@ -174,10 +201,18 @@ func TestHasValidSignaturesBy(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"abcdef123456789"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 		{
 			"NoSignature",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -189,6 +224,15 @@ func TestHasValidSignaturesBy(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"abcdef123456789"},
+				ConditionsMap: map[string][]string{
+					"Organizations": p.Organizations,
+					"Teams":         p.Teams,
+					"Users":         p.Users,
+				},
+			},
 		},
 	})
 }
@@ -201,7 +245,6 @@ func TestHasValidSignaturesByKeys(t *testing.T) {
 	runSignatureTests(t, p, []SignatureTestCase{
 		{
 			"ValidSignatureByValidKey",
-			true,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -219,10 +262,14 @@ func TestHasValidSignaturesByKeys(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       true,
+				Values:          []string{"abcdef123456789"},
+				ConditionValues: []string{"3AA5C34371567BD2"},
+			},
 		},
 		{
 			"ValidSignatureByInvalidKey",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -240,10 +287,14 @@ func TestHasValidSignaturesByKeys(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"3AB5C35371567CE7"},
+				ConditionValues: []string{"3AA5C34371567BD2"},
+			},
 		},
 		{
 			"InvalidSignatureByInvalidKey",
-			false,
 			&pulltest.Context{
 				AuthorValue: "mhaypenny",
 				CommitsValue: []*pull.Commit{
@@ -261,14 +312,19 @@ func TestHasValidSignaturesByKeys(t *testing.T) {
 					},
 				},
 			},
+			&common.PredicateResult{
+				Satisfied:       false,
+				Values:          []string{"abcdef123456789"},
+				ConditionValues: []string{"3AA5C34371567BD2"},
+			},
 		},
 	})
 }
 
 type SignatureTestCase struct {
-	Name     string
-	Expected bool
-	Context  pull.Context
+	Name                    string
+	Context                 pull.Context
+	ExpectedPredicateResult *common.PredicateResult
 }
 
 func runSignatureTests(t *testing.T, p Predicate, cases []SignatureTestCase) {
@@ -276,9 +332,9 @@ func runSignatureTests(t *testing.T, p Predicate, cases []SignatureTestCase) {
 
 	for _, tc := range cases {
 		t.Run(tc.Name, func(t *testing.T) {
-			ok, _, err := p.Evaluate(ctx, tc.Context)
+			predicateResult, err := p.Evaluate(ctx, tc.Context)
 			if assert.NoError(t, err, "evaluation failed") {
-				assert.Equal(t, tc.Expected, ok, "predicate was not correct")
+				assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult)
 			}
 		})
 	}
diff --git a/policy/predicate/status_test.go b/policy/predicate/status_test.go
index 86e0c15..0ad200f 100644
--- a/policy/predicate/status_test.go
+++ b/policy/predicate/status_test.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/palantir/policy-bot/policy/common"
 	"github.com/palantir/policy-bot/pull"
 	"github.com/palantir/policy-bot/pull/pulltest"
 	"github.com/stretchr/testify/assert"
@@ -29,55 +30,70 @@ func TestHasSuccessfulStatus(t *testing.T) {
 	runStatusTestCase(t, p, []StatusTestCase{
 		{
 			"all statuses succeed",
-			true,
 			&pulltest.Context{
 				LatestStatusesValue: map[string]string{
 					"status-name":   "success",
 					"status-name-2": "success",
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"status-name", "status-name-2"},
+			},
 		},
 		{
 			"a status fails",
-			false,
 			&pulltest.Context{
 				LatestStatusesValue: map[string]string{
 					"status-name":   "success",
 					"status-name-2": "failure",
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"status-name-2"},
+			},
 		},
 		{
 			"multiple statuses fail",
-			false,
 			&pulltest.Context{
 				LatestStatusesValue: map[string]string{
 					"status-name":   "failure",
 					"status-name-2": "failure",
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"status-name", "status-name-2"},
+			},
 		},
 		{
 			"a status does not exist",
-			false,
 			&pulltest.Context{
 				LatestStatusesValue: map[string]string{
 					"status-name": "success",
 				},
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"status-name-2"},
+			},
 		},
 		{
 			"multiple statuses do not exist",
-			false,
 			&pulltest.Context{},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"status-name", "status-name-2"},
+			},
 		},
 	})
 }
 
 type StatusTestCase struct {
-	name     string
-	expected bool
-	context  pull.Context
+	name                    string
+	context                 pull.Context
+	ExpectedPredicateResult *common.PredicateResult
 }
 
 func runStatusTestCase(t *testing.T, p Predicate, cases []StatusTestCase) {
@@ -85,9 +101,9 @@ func runStatusTestCase(t *testing.T, p Predicate, cases []StatusTestCase) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			ok, _, err := p.Evaluate(ctx, tc.context)
+			predicateResult, err := p.Evaluate(ctx, tc.context)
 			if assert.NoError(t, err, "evaluation failed") {
-				assert.Equal(t, tc.expected, ok, "predicate was not correct")
+				assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult)
 			}
 		})
 	}
diff --git a/policy/predicate/title_test.go b/policy/predicate/title_test.go
index e656dd0..8799e51 100644
--- a/policy/predicate/title_test.go
+++ b/policy/predicate/title_test.go
@@ -36,24 +36,43 @@ func TestWithNotMatchRule(t *testing.T) {
 	runTitleTestCase(t, p, []TitleTestCase{
 		{
 			"empty title",
-			true,
 			&pulltest.Context{
 				TitleValue: "",
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{""},
+				ConditionsMap: map[string][]string{
+					"not match": {"^(fix|feat|chore): (\\w| )+$"},
+				},
+			},
 		},
 		{
 			"matches pattern",
-			false,
 			&pulltest.Context{
 				TitleValue: "chore: added tests",
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"chore: added tests"},
+				ConditionsMap: map[string][]string{
+					"not match": {"^(fix|feat|chore): (\\w| )+$"},
+					"match":     nil,
+				},
+			},
 		},
 		{
 			"does not match pattern",
-			true,
 			&pulltest.Context{
 				TitleValue: "changes: added tests",
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"changes: added tests"},
+				ConditionsMap: map[string][]string{
+					"not match": {"^(fix|feat|chore): (\\w| )+$"},
+				},
+			},
 		},
 	})
 }
@@ -69,24 +88,44 @@ func TestWithMatchRule(t *testing.T) {
 	runTitleTestCase(t, p, []TitleTestCase{
 		{
 			"empty title",
-			false,
 			&pulltest.Context{
 				TitleValue: "",
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{""},
+				ConditionsMap: map[string][]string{
+					"not match": nil,
+					"match":     {"^BLOCKED"},
+				},
+			},
 		},
 		{
 			"matches pattern",
-			true,
 			&pulltest.Context{
 				TitleValue: "BLOCKED: new feature",
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"BLOCKED: new feature"},
+				ConditionsMap: map[string][]string{
+					"match": {"^BLOCKED"},
+				},
+			},
 		},
 		{
 			"does not match pattern",
-			false,
 			&pulltest.Context{
 				TitleValue: "feat: new feature",
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"feat: new feature"},
+				ConditionsMap: map[string][]string{
+					"not match": nil,
+					"match":     {"^BLOCKED"},
+				},
+			},
 		},
 	})
 }
@@ -105,53 +144,91 @@ func TestWithMixedRules(t *testing.T) {
 	runTitleTestCase(t, p, []TitleTestCase{
 		{
 			"empty title",
-			true,
 			&pulltest.Context{
 				TitleValue: "",
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{""},
+				ConditionsMap: map[string][]string{
+					"not match": {"^(fix|feat|chore): (\\w| )+$", "^BREAKING CHANGE: (\\w| )+$"},
+				},
+			},
 		},
 		{
 			"matches first pattern in matches list",
-			false,
 			&pulltest.Context{
 				TitleValue: "fix: fixes failing tests",
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"fix: fixes failing tests"},
+				ConditionsMap: map[string][]string{
+					"not match": {"^(fix|feat|chore): (\\w| )+$", "^BREAKING CHANGE: (\\w| )+$"},
+					"match":     {"BLOCKED"},
+				},
+			},
 		},
 		{
 			"matches second pattern in matches list",
-			false,
 			&pulltest.Context{
 				TitleValue: "BREAKING CHANGE: new api version",
 			},
+			&common.PredicateResult{
+				Satisfied: false,
+				Values:    []string{"BREAKING CHANGE: new api version"},
+				ConditionsMap: map[string][]string{
+					"not match": {"^(fix|feat|chore): (\\w| )+$", "^BREAKING CHANGE: (\\w| )+$"},
+					"match":     {"BLOCKED"},
+				},
+			},
 		},
 		{
 			"matches pattern in not_matches list",
-			true,
 			&pulltest.Context{
 				TitleValue: "BLOCKED: not working",
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"BLOCKED: not working"},
+				ConditionsMap: map[string][]string{
+					"match": {"BLOCKED"},
+				},
+			},
 		},
 		{
 			"matches pattern in both lists",
-			true,
 			&pulltest.Context{
 				TitleValue: "BREAKING CHANGE: BLOCKED",
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"BREAKING CHANGE: BLOCKED"},
+				ConditionsMap: map[string][]string{
+					"match": {"BLOCKED"},
+				},
+			},
 		},
 		{
 			"does not match any pattern",
-			true,
 			&pulltest.Context{
 				TitleValue: "test: adds tests",
 			},
+			&common.PredicateResult{
+				Satisfied: true,
+				Values:    []string{"test: adds tests"},
+				ConditionsMap: map[string][]string{
+					"not match": {"^(fix|feat|chore): (\\w| )+$", "^BREAKING CHANGE: (\\w| )+$"},
+				},
+			},
 		},
 	})
 }
 
 type TitleTestCase struct {
-	name     string
-	expected bool
-	context  pull.Context
+	name                    string
+	context                 pull.Context
+	ExpectedPredicateResult *common.PredicateResult
 }
 
 func runTitleTestCase(t *testing.T, p Predicate, cases []TitleTestCase) {
@@ -159,9 +236,9 @@ func runTitleTestCase(t *testing.T, p Predicate, cases []TitleTestCase) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			ok, _, err := p.Evaluate(ctx, tc.context)
+			predicateResult, err := p.Evaluate(ctx, tc.context)
 			if assert.NoError(t, err, "evaluation failed") {
-				assert.Equal(t, tc.expected, ok, "predicate was not correct")
+				assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult)
 			}
 		})
 	}