Files
larksuite-cli/internal/qualitygate/semantic/gatekeeper.go
2026-06-17 16:29:33 +08:00

284 lines
7.4 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package semantic
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/larksuite/cli/internal/qualitygate/facts"
)
var evidencePattern = regexp.MustCompile(`^facts\.(commands|skills|errors|outputs)\[(\d+)\]$`)
func Decide(f facts.Facts, r Review, p Policy) Decision {
return DecideWithWaivers(f, r, p, Waivers{})
}
func DecideWithWaivers(f facts.Facts, r Review, p Policy, waivers Waivers) Decision {
var d Decision
for _, finding := range r.Findings {
if !validFinding(finding) {
addDecisionFinding(&d, decisionFinding(f, finding, ReviewActionObserve))
continue
}
if !categoryBlocks(p, finding.Category) || !reproducible(f, finding) {
addDecisionFinding(&d, decisionFinding(f, finding, ReviewActionObserve))
continue
}
scopes, ok := scopesForFinding(f, finding)
if !ok {
addDecisionFinding(&d, decisionFinding(f, finding, ReviewActionObserve))
continue
}
groups := matchingRolloutGroups(p, finding, scopes)
if len(groups) == 0 {
addDecisionFinding(&d, decisionFinding(f, finding, ReviewActionObserve))
continue
}
finding.RolloutGroups = groups
if waiverID, waiverKeys, ok := waivers.MatchFinding(finding.Category, scopes); ok {
finding.WaiverID = waiverID
finding.WaiverKeys = waiverKeys
addDecisionFinding(&d, decisionFinding(f, finding, ReviewActionConfirm))
continue
}
addDecisionFinding(&d, decisionFinding(f, finding, ReviewActionMustFix))
}
return d
}
func addDecisionFinding(d *Decision, finding Finding) {
if finding.Fingerprint == "" {
return
}
switch finding.ReviewAction {
case ReviewActionMustFix:
if containsFindingFingerprint(d.Blockers, finding.Fingerprint) {
return
}
d.Warnings = removeFindingFingerprint(d.Warnings, finding.Fingerprint)
d.Blockers = append(d.Blockers, finding)
case ReviewActionConfirm:
if containsFindingFingerprint(d.Blockers, finding.Fingerprint) {
return
}
if replaceWarningFinding(d.Warnings, finding, ReviewActionObserve) {
return
}
if containsFindingFingerprint(d.Warnings, finding.Fingerprint) {
return
}
d.Warnings = append(d.Warnings, finding)
default:
if containsFindingFingerprint(d.Blockers, finding.Fingerprint) || containsFindingFingerprint(d.Warnings, finding.Fingerprint) {
return
}
d.Warnings = append(d.Warnings, finding)
}
}
func containsFindingFingerprint(findings []Finding, fingerprint string) bool {
for _, finding := range findings {
if finding.Fingerprint == fingerprint {
return true
}
}
return false
}
func removeFindingFingerprint(findings []Finding, fingerprint string) []Finding {
out := findings[:0]
for _, finding := range findings {
if finding.Fingerprint != fingerprint {
out = append(out, finding)
}
}
return out
}
func replaceWarningFinding(findings []Finding, replacement Finding, replaceAction string) bool {
for i, finding := range findings {
if finding.Fingerprint == replacement.Fingerprint && finding.ReviewAction == replaceAction {
findings[i] = replacement
return true
}
}
return false
}
func decisionFinding(f facts.Facts, finding Finding, action string) Finding {
finding.ReviewAction = action
finding.Fingerprint = findingFingerprint(f, finding)
return finding
}
func findingFingerprint(f facts.Facts, finding Finding) string {
parts := make([]string, 0, len(finding.Evidence)+1)
parts = append(parts, "category:"+finding.Category)
evidence := make([]string, 0, len(finding.Evidence))
for _, ev := range finding.Evidence {
evidence = append(evidence, evidenceFingerprint(f, ev))
}
sort.Strings(evidence)
parts = append(parts, evidence...)
return strings.Join(parts, "|")
}
func evidenceFingerprint(f facts.Facts, ev string) string {
kind, idx, ok := parseEvidence(ev)
if !ok || !evidenceExists(f, kind, idx) {
return "ref:" + ev
}
switch kind {
case "commands":
cmd := f.Commands[idx]
return strings.Join([]string{
"commands",
"path:" + cmd.Path,
"name_conflicts_existing:" + strconv.FormatBool(cmd.NameConflictsExisting),
"flag_alias_conflict:" + strconv.FormatBool(cmd.FlagAliasConflict),
}, ":")
case "skills":
skill := f.Skills[idx]
return strings.Join([]string{
"skills",
"source_file:" + skill.SourceFile,
"line:" + strconv.Itoa(skill.Line),
"command_path:" + skill.CommandPath,
"references_invalid_command:" + strconv.FormatBool(skill.ReferencesInvalidCommand),
}, ":")
case "errors":
errFact := f.Errors[idx]
return strings.Join([]string{
"errors",
"file:" + errFact.File,
"line:" + strconv.Itoa(errFact.Line),
"command_path:" + errFact.CommandPath,
"code:" + errFact.Code,
"boundary:" + strconv.FormatBool(errFact.Boundary),
"required_hint:" + strconv.FormatBool(errFact.RequiredHint),
"hint_action_count:" + strconv.Itoa(errFact.HintActionCount),
}, ":")
case "outputs":
out := f.Outputs[idx]
return strings.Join([]string{
"outputs",
"command:" + out.Command,
"is_list:" + strconv.FormatBool(out.IsList),
"has_default_limit:" + strconv.FormatBool(out.HasDefaultLimit),
"has_decision_field:" + strconv.FormatBool(out.HasDecisionField),
}, ":")
default:
return "ref:" + ev
}
}
func categoryBlocks(p Policy, category string) bool {
return containsString(p.BlockCategories, category)
}
func validFinding(f Finding) bool {
if !allowedCategory(f.Category) {
return false
}
if strings.TrimSpace(f.Severity) == "" ||
strings.TrimSpace(f.Message) == "" ||
strings.TrimSpace(f.SuggestedAction) == "" {
return false
}
if len(f.Message) > 500 || len(f.SuggestedAction) > 500 {
return false
}
if len(f.Evidence) == 0 || len(f.Evidence) > 20 {
return false
}
return true
}
func allowedCategory(category string) bool {
switch category {
case "error_hint", "default_output", "naming", "skill_quality":
return true
default:
return false
}
}
func reproducible(f facts.Facts, finding Finding) bool {
for _, ev := range finding.Evidence {
kind, idx, ok := parseEvidence(ev)
if !ok || !evidenceExists(f, kind, idx) {
return false
}
if !reproducibleEvidence(f, finding.Category, kind, idx) {
return false
}
}
return true
}
func reproducibleEvidence(f facts.Facts, category, kind string, idx int) bool {
switch category {
case "error_hint":
if kind != "errors" {
return false
}
errFact := f.Errors[idx]
return errFact.Boundary && errFact.RequiredHint && errFact.HintActionCount == 0
case "default_output":
if kind != "outputs" {
return false
}
out := f.Outputs[idx]
return out.IsList && (!out.HasDefaultLimit || !out.HasDecisionField)
case "naming":
if kind != "commands" {
return false
}
cmd := f.Commands[idx]
return cmd.NameConflictsExisting || cmd.FlagAliasConflict
case "skill_quality":
if kind != "skills" {
return false
}
skill := f.Skills[idx]
return skill.ReferencesInvalidCommand
default:
return false
}
}
func parseEvidence(ev string) (string, int, bool) {
matches := evidencePattern.FindStringSubmatch(ev)
if len(matches) != 3 {
return "", 0, false
}
idx, err := strconv.Atoi(matches[2])
if err != nil {
return "", 0, false
}
return matches[1], idx, true
}
func evidenceExists(f facts.Facts, kind string, idx int) bool {
if idx < 0 {
return false
}
switch kind {
case "commands":
return idx < len(f.Commands)
case "skills":
return idx < len(f.Skills)
case "errors":
return idx < len(f.Errors)
case "outputs":
return idx < len(f.Outputs)
default:
return false
}
}