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

184 lines
5.4 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package semantic
import (
"encoding/json"
"fmt"
"io"
)
const maxModelResponseBytes = 1 << 20
type Review struct {
Verdict string `json:"verdict"`
Findings []Finding `json:"findings"`
}
type Finding struct {
Category string `json:"category"`
Severity string `json:"severity"`
Evidence []string `json:"evidence"`
Message string `json:"message"`
SuggestedAction string `json:"suggested_action"`
ReviewAction string `json:"review_action,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
RolloutGroups []string `json:"rollout_groups,omitempty"`
WaiverID string `json:"waiver_id,omitempty"`
WaiverKeys []string `json:"waiver_keys,omitempty"`
}
const (
ReviewActionMustFix = "must_fix"
ReviewActionConfirm = "confirm"
ReviewActionObserve = "observe"
)
type SystemWarning struct {
Severity string `json:"severity"`
Message string `json:"message"`
SuggestedAction string `json:"suggested_action,omitempty"`
}
type Policy struct {
SchemaVersion int `json:"schema_version"`
DefaultEnforcement string `json:"default_enforcement"`
BlockCategories []string `json:"block_categories"`
RolloutGroups []RolloutGroup `json:"rollout_groups,omitempty"`
}
type RolloutGroup struct {
ID string `json:"id"`
Enforcement string `json:"enforcement"`
Scope ScopeSelector `json:"scope,omitempty"`
Categories []string `json:"categories"`
Owner string `json:"owner"`
Reason string `json:"reason"`
}
type ScopeSelector struct {
ChangedOnly bool `json:"changed_only,omitempty"`
Domains []string `json:"domains,omitempty"`
FactKinds []string `json:"fact_kinds,omitempty"`
Sources []string `json:"sources,omitempty"`
}
type Decision struct {
BlockMode bool `json:"block_mode"`
Skipped bool `json:"skipped,omitempty"`
Degraded bool `json:"degraded,omitempty"`
InfrastructureFailure bool `json:"infrastructure_failure,omitempty"`
Blockers []Finding `json:"blockers,omitempty"`
Warnings []Finding `json:"warnings,omitempty"`
SystemWarnings []SystemWarning `json:"system_warnings,omitempty"`
}
func DefaultPolicy() Policy {
return Policy{
SchemaVersion: 1,
DefaultEnforcement: "observe",
BlockCategories: []string{"error_hint", "default_output", "naming", "skill_quality"},
RolloutGroups: []RolloutGroup{{
ID: "all",
Enforcement: "blocking",
Categories: []string{"error_hint", "default_output", "naming", "skill_quality"},
Owner: "test",
Reason: "default in-memory policy",
}},
}
}
func DecodeReview(r io.Reader) (Review, error) {
return decodeReview(r, true)
}
// Model responses are normalized through modelReview for compatibility; the
// local gatekeeper still recomputes blocking evidence from facts.
func DecodeModelReview(r io.Reader) (Review, error) {
dec := json.NewDecoder(io.LimitReader(r, maxModelResponseBytes))
var raw modelReview
if err := dec.Decode(&raw); err != nil {
return Review{}, err
}
review := Review{
Verdict: raw.Verdict,
Findings: make([]Finding, 0, len(raw.Findings)),
}
for _, finding := range raw.Findings {
review.Findings = append(review.Findings, Finding{
Category: finding.Category,
Severity: finding.Severity,
Evidence: []string(finding.Evidence),
Message: finding.Message,
SuggestedAction: finding.SuggestedAction,
RolloutGroups: finding.RolloutGroups,
WaiverID: finding.WaiverID,
WaiverKeys: finding.WaiverKeys,
})
}
if err := validateReview(review); err != nil {
return Review{}, err
}
return review, nil
}
func decodeReview(r io.Reader, strict bool) (Review, error) {
dec := json.NewDecoder(io.LimitReader(r, maxModelResponseBytes))
if strict {
dec.DisallowUnknownFields()
}
var review Review
if err := dec.Decode(&review); err != nil {
return Review{}, err
}
if err := validateReview(review); err != nil {
return Review{}, err
}
return review, nil
}
type modelReview struct {
Verdict string `json:"verdict"`
Findings []modelFinding `json:"findings"`
}
type modelFinding struct {
Category string `json:"category"`
Severity string `json:"severity"`
Evidence modelEvidence `json:"evidence"`
Message string `json:"message"`
SuggestedAction string `json:"suggested_action"`
RolloutGroups []string `json:"rollout_groups,omitempty"`
WaiverID string `json:"waiver_id,omitempty"`
WaiverKeys []string `json:"waiver_keys,omitempty"`
}
type modelEvidence []string
func (e *modelEvidence) UnmarshalJSON(data []byte) error {
var list []string
if err := json.Unmarshal(data, &list); err == nil {
*e = list
return nil
}
var one string
if err := json.Unmarshal(data, &one); err == nil {
*e = []string{one}
return nil
}
return fmt.Errorf("evidence must be a string or string array")
}
func validateReview(review Review) error {
if len(review.Findings) > 20 {
return fmt.Errorf("too many findings: %d", len(review.Findings))
}
for _, finding := range review.Findings {
if len(finding.Message) > 500 || len(finding.SuggestedAction) > 500 {
return fmt.Errorf("finding text exceeds limit")
}
}
return nil
}