返回 DeepSeek-Reasonix
risk.go
根目录 / internal / evidence / risk.go
1 package evidence
2
3 import (
4 "path/filepath"
5 "strings"
6 )
7
8 // RiskLevel classifies the latest post-mutation change set for adaptive review.
9 type RiskLevel string
10
11 const (
12 RiskLow RiskLevel = "low"
13 RiskMedium RiskLevel = "medium"
14 RiskHigh RiskLevel = "high"
15 )
16
17 // highRiskPathHints elevate ordinary production edits to High when the path
18 // touches auth, crypto, networking, providers, plugins, sandbox, config,
19 // migrations, persistence, or concurrency.
20 var highRiskPathHints = []string{
21 "auth", "permission", "secret", "credential", "token", "password", "oauth",
22 "crypto", "encrypt", "decrypt", "tls", "ssl", "keyring",
23 "network", "proxy", "http", "websocket", "provider",
24 "plugin", "mcp", "tool", "schema", "sandbox",
25 "config", "migrate", "migration", "persist", "store", "database", "db",
26 "concurrent", "mutex", "race", "lock", "atomic",
27 }
28
29 // highRiskToolHints elevate opaque or privileged mutation surfaces.
30 var highRiskToolHints = []string{
31 "mcp__", "install_source", "install_skill", "plugin",
32 }
33
34 // ClassifyMutationRisk scores the change set after the latest mutation.
35 // Low: docs/tests/i18n/pure presentation only, with no opaque writes.
36 // Medium: ordinary production code or limited multi-file edits.
37 // High: security-sensitive surfaces, opaque mutations, or 10+ paths.
38 func ClassifyMutationRisk(receipts []Receipt, after int) RiskLevel {
39 start := after + 1
40 if start < 0 {
41 start = 0
42 }
43 var paths []string
44 seen := map[string]bool{}
45 opaque := false
46 hasProd := false
47 onlyLow := true
48
49 // Include the mutation receipt itself.
50 if after >= 0 && after < len(receipts) {
51 r := receipts[after]
52 if r.Success && r.Mutation {
53 if len(r.Paths) == 0 {
54 opaque = true
55 }
56 for _, p := range r.Paths {
57 if !seen[p] {
58 seen[p] = true
59 paths = append(paths, p)
60 }
61 }
62 if toolLooksHighRisk(r.ToolName) {
63 return RiskHigh
64 }
65 }
66 }
67 for i := start; i < len(receipts); i++ {
68 r := receipts[i]
69 if !r.Success || !r.Mutation {
70 continue
71 }
72 if len(r.Paths) == 0 {
73 opaque = true
74 }
75 if toolLooksHighRisk(r.ToolName) {
76 return RiskHigh
77 }
78 for _, p := range r.Paths {
79 if !seen[p] {
80 seen[p] = true
81 paths = append(paths, p)
82 }
83 }
84 }
85 if opaque {
86 return RiskHigh
87 }
88 if len(paths) == 0 {
89 return RiskLow
90 }
91 if len(paths) >= 10 {
92 return RiskHigh
93 }
94 for _, p := range paths {
95 if pathLooksHighRisk(p) {
96 return RiskHigh
97 }
98 if !pathLooksLowRisk(p) {
99 onlyLow = false
100 hasProd = true
101 }
102 }
103 if onlyLow && !hasProd {
104 return RiskLow
105 }
106 return RiskMedium
107 }
108
109 // MutationRiskAfter classifies risk from the ledger using the latest mutation.
110 func (l *Ledger) MutationRiskAfter(after int) RiskLevel {
111 if l == nil {
112 return RiskLow
113 }
114 l.mu.Lock()
115 receipts := append([]Receipt(nil), l.receipts...)
116 l.mu.Unlock()
117 return ClassifyMutationRisk(receipts, after)
118 }
119
120 // PathsSince returns distinct paths from successful mutation/write receipts at
121 // or after the given index (inclusive of the mutation itself when after >= 0).
122 func (l *Ledger) PathsSince(after int) []string {
123 if l == nil {
124 return nil
125 }
126 start := after
127 if start < 0 {
128 start = 0
129 }
130 l.mu.Lock()
131 defer l.mu.Unlock()
132 seen := map[string]bool{}
133 var out []string
134 for i := start; i < len(l.receipts); i++ {
135 r := l.receipts[i]
136 if !r.Success || (!r.Mutation && !r.Write) {
137 continue
138 }
139 for _, p := range r.Paths {
140 if p == "" || seen[p] {
141 continue
142 }
143 seen[p] = true
144 out = append(out, p)
145 }
146 }
147 return out
148 }
149
150 func pathLooksHighRisk(path string) bool {
151 lower := strings.ToLower(filepath.ToSlash(path))
152 base := strings.ToLower(filepath.Base(path))
153 for _, hint := range highRiskPathHints {
154 if strings.Contains(lower, hint) || strings.Contains(base, hint) {
155 return true
156 }
157 }
158 return false
159 }
160
161 func pathLooksLowRisk(path string) bool {
162 lower := strings.ToLower(filepath.ToSlash(path))
163 base := filepath.Base(lower)
164 if strings.HasSuffix(lower, "_test.go") || strings.HasSuffix(lower, "_test.ts") ||
165 strings.HasSuffix(lower, ".test.ts") || strings.HasSuffix(lower, ".test.tsx") ||
166 strings.HasSuffix(lower, "_spec.ts") || strings.HasSuffix(lower, ".spec.ts") {
167 return true
168 }
169 if strings.Contains(lower, "/testdata/") || strings.Contains(lower, "/__tests__/") ||
170 strings.Contains(lower, "/fixtures/") {
171 return true
172 }
173 switch {
174 case strings.HasSuffix(base, ".md"), strings.HasSuffix(base, ".mdx"),
175 strings.HasSuffix(base, ".txt"), strings.HasSuffix(base, ".rst"):
176 return true
177 case strings.Contains(lower, "/docs/"), strings.Contains(lower, "/locales/"),
178 strings.Contains(lower, "/i18n/"), strings.HasPrefix(base, "readme"):
179 return true
180 case strings.HasSuffix(base, ".css") && !strings.Contains(lower, "sandbox"):
181 // Pure presentation styles are low risk unless mixed with other paths.
182 return true
183 }
184 return false
185 }
186
187 func toolLooksHighRisk(name string) bool {
188 lower := strings.ToLower(strings.TrimSpace(name))
189 for _, hint := range highRiskToolHints {
190 if strings.Contains(lower, hint) {
191 return true
192 }
193 }
194 return false
195 }
196
196 lines GO