09b2c1dce5
build / build (push) Successful in 1m17s
修正筛选入口位置:五项条件在表头就地选择,保持完整集筛选与无感续载。 统一固定规则:未覆盖一律待确认,原问答/人工模型证据不改写,已覆盖仍保留点踩优先。 验证:行为红绿、前端/Go全套及隔离PostgreSQL全mirror竞态通过;完整本地门禁与原生备份真实恢复通过。
129 lines
4.1 KiB
Go
129 lines
4.1 KiB
Go
package qaopsquality
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"knowledge-governance/internal/dobotaiops"
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var hashPattern = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
|
var noncePattern = regexp.MustCompile(`^[A-Za-z0-9_-]{16,128}$`)
|
|
|
|
func validText(s string, max int) bool {
|
|
return utf8.ValidString(s) && utf8.RuneCountInString(s) <= max && !strings.ContainsRune(s, 0)
|
|
}
|
|
func validID(s string) bool { return strings.TrimSpace(s) != "" && validText(s, 256) }
|
|
func canonical(raw json.RawMessage) (json.RawMessage, error) {
|
|
if len(raw) == 0 {
|
|
return json.RawMessage("null"), nil
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.UseNumber()
|
|
var v any
|
|
if decoder.Decode(&v) != nil {
|
|
return nil, ErrInvalid
|
|
}
|
|
var extra any
|
|
if decoder.Decode(&extra) != io.EOF {
|
|
return nil, ErrInvalid
|
|
}
|
|
out, err := json.Marshal(v)
|
|
return out, err
|
|
}
|
|
func EvidenceFor(record dobotaiops.Record) (Evidence, error) {
|
|
if !validID(record.ID) {
|
|
return Evidence{}, ErrInvalid
|
|
}
|
|
refs, err := canonical(record.References)
|
|
if err != nil {
|
|
return Evidence{}, ErrInvalid
|
|
}
|
|
media, err := canonical(record.Media)
|
|
if err != nil {
|
|
return Evidence{}, ErrInvalid
|
|
}
|
|
// Keep only assessment inputs, never personnel or mutable feedback/organisation.
|
|
payload, err := json.Marshal(struct {
|
|
Version string `json:"version"`
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
Question string `json:"question"`
|
|
Answer string `json:"answer"`
|
|
References json.RawMessage `json:"references"`
|
|
Media json.RawMessage `json:"media"`
|
|
}{"dobotai-2.0-dingtalk-quality-v1", record.ID, record.Status, record.Question, record.Answer, refs, media})
|
|
if err != nil || len(payload) > 8<<20 {
|
|
return Evidence{}, ErrInvalid
|
|
}
|
|
sum := sha256.Sum256(payload)
|
|
hash := hex.EncodeToString(sum[:])
|
|
if record.QualityInputHash != "" {
|
|
if !hashPattern.MatchString(record.QualityInputHash) {
|
|
return Evidence{}, ErrInvalid
|
|
}
|
|
hash = record.QualityInputHash
|
|
}
|
|
return Evidence{Hash: hash, Payload: payload, Record: record, Result: dobotaiops.CheckAnswerCoverage(record.Answer)}, nil
|
|
}
|
|
func Validate(r SaveRequest) error {
|
|
if !hashPattern.MatchString(r.InputHash) || r.InputEpoch < 1 || r.BaseRevision < 0 || !noncePattern.MatchString(r.Nonce) || strings.TrimSpace(r.Reason) == "" || !validText(r.Reason, 4000) {
|
|
return ErrInvalid
|
|
}
|
|
f := r.Fields
|
|
validCoverage := false
|
|
for _, s := range []string{"covered", "uncovered", "partial", "pending", "excluded"} {
|
|
validCoverage = validCoverage || f.Coverage == s
|
|
}
|
|
if !validCoverage || (f.Correctness != "correct" && f.Correctness != "incorrect" && f.Correctness != "pending") {
|
|
return ErrInvalid
|
|
}
|
|
if (f.Correctness == "pending") != (f.Coverage == "uncovered") {
|
|
return ErrInvalid
|
|
}
|
|
if !validText(f.ProductCategory, 128) || !validText(f.ProblemType, 128) || !validText(f.RootCause, 2000) || !validText(f.ExclusionReason, 2000) {
|
|
return ErrInvalid
|
|
}
|
|
if f.Correctness == "correct" && f.RootCause != "" || f.Coverage == "excluded" && strings.TrimSpace(f.ExclusionReason) == "" || f.Coverage != "excluded" && f.ExclusionReason != "" {
|
|
return ErrInvalid
|
|
}
|
|
return nil
|
|
}
|
|
func Project(hash string, epoch, revision int64, automatic Check, review *Review) View {
|
|
view := View{InputHash: hash, InputEpoch: epoch, Revision: revision, State: "unreviewed", Automatic: automatic, LatestReview: review, Effective: Fields{Coverage: automatic.Result.Verdict}}
|
|
if review == nil {
|
|
return view
|
|
}
|
|
view.State = "stale"
|
|
if review.InputEpoch == epoch && review.InputHash == hash {
|
|
view.State = "reviewed"
|
|
view.Effective = review.Fields
|
|
}
|
|
return view
|
|
}
|
|
func Summarize(views map[string]View) Summary {
|
|
result := Summary{Total: len(views), Rates: ComputeRates(views)}
|
|
for _, v := range views {
|
|
switch v.State {
|
|
case "reviewed":
|
|
result.Reviewed++
|
|
if v.Effective.Coverage == "pending" || v.Effective.Correctness == "pending" {
|
|
result.PendingConfirmation++
|
|
}
|
|
case "stale":
|
|
result.Stale++
|
|
default:
|
|
result.Unreviewed++
|
|
}
|
|
if v.Automatic.Result.Verdict == "uncovered" {
|
|
result.RuleUncovered++
|
|
}
|
|
}
|
|
return result
|
|
}
|