orch-code 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +624 -0
- package/cmd/apply.go +111 -0
- package/cmd/auth.go +393 -0
- package/cmd/auth_test.go +100 -0
- package/cmd/diff.go +57 -0
- package/cmd/doctor.go +149 -0
- package/cmd/explain.go +192 -0
- package/cmd/explain_test.go +62 -0
- package/cmd/init.go +100 -0
- package/cmd/interactive.go +1372 -0
- package/cmd/interactive_input.go +45 -0
- package/cmd/interactive_input_test.go +55 -0
- package/cmd/logs.go +72 -0
- package/cmd/model.go +84 -0
- package/cmd/plan.go +149 -0
- package/cmd/provider.go +189 -0
- package/cmd/provider_model_doctor_test.go +91 -0
- package/cmd/root.go +67 -0
- package/cmd/run.go +123 -0
- package/cmd/run_engine.go +208 -0
- package/cmd/run_engine_test.go +30 -0
- package/cmd/session.go +589 -0
- package/cmd/session_helpers.go +54 -0
- package/cmd/session_integration_test.go +30 -0
- package/cmd/session_list_current_test.go +87 -0
- package/cmd/session_messages_test.go +163 -0
- package/cmd/session_runs_test.go +68 -0
- package/cmd/sprint1_integration_test.go +119 -0
- package/cmd/stats.go +173 -0
- package/cmd/stats_test.go +71 -0
- package/cmd/version.go +4 -0
- package/go.mod +45 -0
- package/go.sum +108 -0
- package/internal/agents/agent.go +31 -0
- package/internal/agents/coder.go +167 -0
- package/internal/agents/planner.go +155 -0
- package/internal/agents/reviewer.go +118 -0
- package/internal/agents/runtime.go +25 -0
- package/internal/agents/runtime_test.go +77 -0
- package/internal/auth/account.go +78 -0
- package/internal/auth/oauth.go +523 -0
- package/internal/auth/store.go +287 -0
- package/internal/confidence/policy.go +174 -0
- package/internal/confidence/policy_test.go +71 -0
- package/internal/confidence/scorer.go +253 -0
- package/internal/confidence/scorer_test.go +83 -0
- package/internal/config/config.go +331 -0
- package/internal/config/config_defaults_test.go +138 -0
- package/internal/execution/contract_builder.go +160 -0
- package/internal/execution/contract_builder_test.go +68 -0
- package/internal/execution/plan_compliance.go +161 -0
- package/internal/execution/plan_compliance_test.go +71 -0
- package/internal/execution/retry_directive.go +132 -0
- package/internal/execution/scope_guard.go +69 -0
- package/internal/logger/logger.go +120 -0
- package/internal/models/contracts_test.go +100 -0
- package/internal/models/models.go +269 -0
- package/internal/orchestrator/orchestrator.go +701 -0
- package/internal/orchestrator/orchestrator_retry_test.go +135 -0
- package/internal/orchestrator/review_engine_test.go +50 -0
- package/internal/orchestrator/state.go +42 -0
- package/internal/orchestrator/test_classifier_test.go +68 -0
- package/internal/patch/applier.go +131 -0
- package/internal/patch/applier_test.go +25 -0
- package/internal/patch/parser.go +89 -0
- package/internal/patch/patch.go +60 -0
- package/internal/patch/summary.go +30 -0
- package/internal/patch/validator.go +104 -0
- package/internal/planning/normalizer.go +416 -0
- package/internal/planning/normalizer_test.go +64 -0
- package/internal/providers/errors.go +35 -0
- package/internal/providers/openai/client.go +498 -0
- package/internal/providers/openai/client_test.go +187 -0
- package/internal/providers/provider.go +47 -0
- package/internal/providers/registry.go +32 -0
- package/internal/providers/registry_test.go +57 -0
- package/internal/providers/router.go +52 -0
- package/internal/providers/state.go +114 -0
- package/internal/providers/state_test.go +64 -0
- package/internal/repo/analyzer.go +188 -0
- package/internal/repo/context.go +83 -0
- package/internal/review/engine.go +267 -0
- package/internal/review/engine_test.go +103 -0
- package/internal/runstore/store.go +137 -0
- package/internal/runstore/store_test.go +59 -0
- package/internal/runtime/lock.go +150 -0
- package/internal/runtime/lock_test.go +57 -0
- package/internal/session/compaction.go +260 -0
- package/internal/session/compaction_test.go +36 -0
- package/internal/session/service.go +117 -0
- package/internal/session/service_test.go +113 -0
- package/internal/storage/storage.go +1498 -0
- package/internal/storage/storage_test.go +413 -0
- package/internal/testing/classifier.go +80 -0
- package/internal/testing/classifier_test.go +36 -0
- package/internal/tools/command.go +160 -0
- package/internal/tools/command_test.go +56 -0
- package/internal/tools/file.go +111 -0
- package/internal/tools/git.go +77 -0
- package/internal/tools/invalid_params_test.go +36 -0
- package/internal/tools/policy.go +98 -0
- package/internal/tools/policy_test.go +36 -0
- package/internal/tools/registry_test.go +52 -0
- package/internal/tools/result.go +30 -0
- package/internal/tools/search.go +86 -0
- package/internal/tools/tool.go +94 -0
- package/main.go +9 -0
- package/npm/orch.js +25 -0
- package/package.json +41 -0
- package/scripts/changelog.js +20 -0
- package/scripts/check-release-version.js +21 -0
- package/scripts/lib/release-utils.js +223 -0
- package/scripts/postinstall.js +157 -0
- package/scripts/release.js +52 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const authFile = "auth.json"
|
|
15
|
+
|
|
16
|
+
type State struct {
|
|
17
|
+
Provider string `json:"provider"`
|
|
18
|
+
Mode string `json:"mode"`
|
|
19
|
+
AccessToken string `json:"accessToken"`
|
|
20
|
+
RefreshToken string `json:"refreshToken,omitempty"`
|
|
21
|
+
ExpiresAt time.Time `json:"expiresAt,omitempty"`
|
|
22
|
+
AccountID string `json:"accountId,omitempty"`
|
|
23
|
+
Email string `json:"email,omitempty"`
|
|
24
|
+
UpdatedAt time.Time `json:"updatedAt"`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Credential struct {
|
|
28
|
+
Type string `json:"type"`
|
|
29
|
+
Key string `json:"key,omitempty"`
|
|
30
|
+
AccessToken string `json:"accessToken,omitempty"`
|
|
31
|
+
RefreshToken string `json:"refreshToken,omitempty"`
|
|
32
|
+
ExpiresAt time.Time `json:"expiresAt,omitempty"`
|
|
33
|
+
AccountID string `json:"accountId,omitempty"`
|
|
34
|
+
Email string `json:"email,omitempty"`
|
|
35
|
+
UpdatedAt time.Time `json:"updatedAt"`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func Load(repoRoot string) (*State, error) {
|
|
39
|
+
cred, err := Get(repoRoot, "openai")
|
|
40
|
+
if err != nil {
|
|
41
|
+
return nil, err
|
|
42
|
+
}
|
|
43
|
+
if cred == nil {
|
|
44
|
+
return nil, nil
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
state := &State{
|
|
48
|
+
Provider: "openai",
|
|
49
|
+
UpdatedAt: cred.UpdatedAt,
|
|
50
|
+
RefreshToken: strings.TrimSpace(cred.RefreshToken),
|
|
51
|
+
ExpiresAt: cred.ExpiresAt,
|
|
52
|
+
AccountID: strings.TrimSpace(cred.AccountID),
|
|
53
|
+
Email: strings.TrimSpace(cred.Email),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
switch strings.TrimSpace(strings.ToLower(cred.Type)) {
|
|
57
|
+
case "oauth", "account":
|
|
58
|
+
state.Mode = "account"
|
|
59
|
+
state.AccessToken = strings.TrimSpace(cred.AccessToken)
|
|
60
|
+
case "api", "api_key":
|
|
61
|
+
state.Mode = "api_key"
|
|
62
|
+
default:
|
|
63
|
+
state.Mode = strings.TrimSpace(cred.Type)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return state, nil
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func LoadAll(repoRoot string) (map[string]Credential, error) {
|
|
70
|
+
path := filepath.Join(repoRoot, config.OrchDir, authFile)
|
|
71
|
+
data, err := os.ReadFile(path)
|
|
72
|
+
if err != nil {
|
|
73
|
+
if os.IsNotExist(err) {
|
|
74
|
+
return map[string]Credential{}, nil
|
|
75
|
+
}
|
|
76
|
+
return nil, fmt.Errorf("read auth state: %w", err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
parsed := map[string]Credential{}
|
|
80
|
+
if err := json.Unmarshal(data, &parsed); err == nil {
|
|
81
|
+
clean := map[string]Credential{}
|
|
82
|
+
for provider, cred := range parsed {
|
|
83
|
+
id := strings.ToLower(strings.TrimSpace(provider))
|
|
84
|
+
if id == "" {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
cred.Type = strings.ToLower(strings.TrimSpace(cred.Type))
|
|
89
|
+
cred.Key = strings.TrimSpace(cred.Key)
|
|
90
|
+
cred.AccessToken = strings.TrimSpace(cred.AccessToken)
|
|
91
|
+
cred.RefreshToken = strings.TrimSpace(cred.RefreshToken)
|
|
92
|
+
cred.AccountID = strings.TrimSpace(cred.AccountID)
|
|
93
|
+
cred.Email = strings.TrimSpace(cred.Email)
|
|
94
|
+
|
|
95
|
+
if cred.Type == "" {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if cred.Type == "api_key" {
|
|
99
|
+
cred.Type = "api"
|
|
100
|
+
}
|
|
101
|
+
if cred.Type == "account" {
|
|
102
|
+
cred.Type = "oauth"
|
|
103
|
+
}
|
|
104
|
+
clean[id] = cred
|
|
105
|
+
}
|
|
106
|
+
if len(clean) > 0 {
|
|
107
|
+
return clean, nil
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
var state State
|
|
112
|
+
if err := json.Unmarshal(data, &state); err != nil {
|
|
113
|
+
return nil, fmt.Errorf("parse auth state: %w", err)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
provider := strings.ToLower(strings.TrimSpace(state.Provider))
|
|
117
|
+
if provider == "" {
|
|
118
|
+
provider = "openai"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
mode := strings.ToLower(strings.TrimSpace(state.Mode))
|
|
122
|
+
cred := Credential{
|
|
123
|
+
RefreshToken: strings.TrimSpace(state.RefreshToken),
|
|
124
|
+
ExpiresAt: state.ExpiresAt,
|
|
125
|
+
AccountID: strings.TrimSpace(state.AccountID),
|
|
126
|
+
Email: strings.TrimSpace(state.Email),
|
|
127
|
+
UpdatedAt: state.UpdatedAt,
|
|
128
|
+
}
|
|
129
|
+
if mode == "account" {
|
|
130
|
+
cred.Type = "oauth"
|
|
131
|
+
cred.AccessToken = strings.TrimSpace(state.AccessToken)
|
|
132
|
+
}
|
|
133
|
+
if mode == "api_key" {
|
|
134
|
+
cred.Type = "api"
|
|
135
|
+
cred.Key = strings.TrimSpace(state.AccessToken)
|
|
136
|
+
}
|
|
137
|
+
if cred.Type == "" {
|
|
138
|
+
cred.Type = mode
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if cred.Type == "" {
|
|
142
|
+
return map[string]Credential{}, nil
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return map[string]Credential{provider: cred}, nil
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func Save(repoRoot string, state *State) error {
|
|
149
|
+
if state == nil {
|
|
150
|
+
return fmt.Errorf("auth state cannot be nil")
|
|
151
|
+
}
|
|
152
|
+
provider := strings.ToLower(strings.TrimSpace(state.Provider))
|
|
153
|
+
if provider == "" {
|
|
154
|
+
provider = "openai"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
mode := strings.ToLower(strings.TrimSpace(state.Mode))
|
|
158
|
+
cred := Credential{
|
|
159
|
+
Email: strings.TrimSpace(state.Email),
|
|
160
|
+
AccessToken: strings.TrimSpace(state.AccessToken),
|
|
161
|
+
RefreshToken: strings.TrimSpace(state.RefreshToken),
|
|
162
|
+
ExpiresAt: state.ExpiresAt,
|
|
163
|
+
AccountID: strings.TrimSpace(state.AccountID),
|
|
164
|
+
}
|
|
165
|
+
if mode == "account" {
|
|
166
|
+
cred.Type = "oauth"
|
|
167
|
+
}
|
|
168
|
+
if mode == "api_key" {
|
|
169
|
+
cred.Type = "api"
|
|
170
|
+
cred.Key = strings.TrimSpace(state.AccessToken)
|
|
171
|
+
cred.AccessToken = ""
|
|
172
|
+
}
|
|
173
|
+
if cred.Type == "" {
|
|
174
|
+
return fmt.Errorf("unsupported auth mode: %s", state.Mode)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Set(repoRoot, provider, cred)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func Clear(repoRoot string) error {
|
|
181
|
+
path := filepath.Join(repoRoot, config.OrchDir, authFile)
|
|
182
|
+
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
183
|
+
return fmt.Errorf("remove auth state: %w", err)
|
|
184
|
+
}
|
|
185
|
+
return nil
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func Get(repoRoot, provider string) (*Credential, error) {
|
|
189
|
+
all, err := LoadAll(repoRoot)
|
|
190
|
+
if err != nil {
|
|
191
|
+
return nil, err
|
|
192
|
+
}
|
|
193
|
+
id := strings.ToLower(strings.TrimSpace(provider))
|
|
194
|
+
if id == "" {
|
|
195
|
+
return nil, nil
|
|
196
|
+
}
|
|
197
|
+
cred, ok := all[id]
|
|
198
|
+
if !ok {
|
|
199
|
+
return nil, nil
|
|
200
|
+
}
|
|
201
|
+
return &cred, nil
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func Set(repoRoot, provider string, cred Credential) error {
|
|
205
|
+
id := strings.ToLower(strings.TrimSpace(provider))
|
|
206
|
+
if id == "" {
|
|
207
|
+
return fmt.Errorf("provider is required")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
kind := strings.ToLower(strings.TrimSpace(cred.Type))
|
|
211
|
+
if kind == "api_key" {
|
|
212
|
+
kind = "api"
|
|
213
|
+
}
|
|
214
|
+
if kind == "account" {
|
|
215
|
+
kind = "oauth"
|
|
216
|
+
}
|
|
217
|
+
if kind != "api" && kind != "oauth" && kind != "wellknown" {
|
|
218
|
+
return fmt.Errorf("unsupported credential type: %s", cred.Type)
|
|
219
|
+
}
|
|
220
|
+
if kind == "api" && strings.TrimSpace(cred.Key) == "" {
|
|
221
|
+
return fmt.Errorf("api credential key cannot be empty")
|
|
222
|
+
}
|
|
223
|
+
if kind == "oauth" && strings.TrimSpace(cred.AccessToken) == "" {
|
|
224
|
+
return fmt.Errorf("oauth access token cannot be empty")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if err := config.EnsureOrchDir(repoRoot); err != nil {
|
|
228
|
+
return err
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
all, err := LoadAll(repoRoot)
|
|
232
|
+
if err != nil {
|
|
233
|
+
return err
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
cred.Type = kind
|
|
237
|
+
cred.Key = strings.TrimSpace(cred.Key)
|
|
238
|
+
cred.AccessToken = strings.TrimSpace(cred.AccessToken)
|
|
239
|
+
cred.RefreshToken = strings.TrimSpace(cred.RefreshToken)
|
|
240
|
+
cred.AccountID = strings.TrimSpace(cred.AccountID)
|
|
241
|
+
cred.Email = strings.TrimSpace(cred.Email)
|
|
242
|
+
cred.UpdatedAt = time.Now().UTC()
|
|
243
|
+
|
|
244
|
+
all[id] = cred
|
|
245
|
+
|
|
246
|
+
data, err := json.MarshalIndent(all, "", " ")
|
|
247
|
+
if err != nil {
|
|
248
|
+
return fmt.Errorf("serialize auth state: %w", err)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
path := filepath.Join(repoRoot, config.OrchDir, authFile)
|
|
252
|
+
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
253
|
+
return fmt.Errorf("write auth state: %w", err)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return nil
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func Remove(repoRoot, provider string) error {
|
|
260
|
+
all, err := LoadAll(repoRoot)
|
|
261
|
+
if err != nil {
|
|
262
|
+
return err
|
|
263
|
+
}
|
|
264
|
+
id := strings.ToLower(strings.TrimSpace(provider))
|
|
265
|
+
if id == "" {
|
|
266
|
+
return fmt.Errorf("provider is required")
|
|
267
|
+
}
|
|
268
|
+
if _, ok := all[id]; !ok {
|
|
269
|
+
return nil
|
|
270
|
+
}
|
|
271
|
+
delete(all, id)
|
|
272
|
+
|
|
273
|
+
if len(all) == 0 {
|
|
274
|
+
return Clear(repoRoot)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
data, err := json.MarshalIndent(all, "", " ")
|
|
278
|
+
if err != nil {
|
|
279
|
+
return fmt.Errorf("serialize auth state: %w", err)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
path := filepath.Join(repoRoot, config.OrchDir, authFile)
|
|
283
|
+
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
284
|
+
return fmt.Errorf("write auth state: %w", err)
|
|
285
|
+
}
|
|
286
|
+
return nil
|
|
287
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
package confidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
8
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type Policy struct {
|
|
12
|
+
enabled bool
|
|
13
|
+
completeMin float64
|
|
14
|
+
failBelow float64
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func NewPolicy(cfg *config.Config) *Policy {
|
|
18
|
+
defaults := config.DefaultConfig()
|
|
19
|
+
policy := &Policy{
|
|
20
|
+
enabled: defaults.Safety.FeatureFlags.ConfidenceEnforcement,
|
|
21
|
+
completeMin: defaults.Safety.Confidence.CompleteMin,
|
|
22
|
+
failBelow: defaults.Safety.Confidence.FailBelow,
|
|
23
|
+
}
|
|
24
|
+
if cfg == nil {
|
|
25
|
+
return policy
|
|
26
|
+
}
|
|
27
|
+
policy.enabled = cfg.Safety.FeatureFlags.ConfidenceEnforcement
|
|
28
|
+
if cfg.Safety.Confidence.CompleteMin > 0 {
|
|
29
|
+
policy.completeMin = cfg.Safety.Confidence.CompleteMin
|
|
30
|
+
}
|
|
31
|
+
if cfg.Safety.Confidence.FailBelow > 0 {
|
|
32
|
+
policy.failBelow = cfg.Safety.Confidence.FailBelow
|
|
33
|
+
}
|
|
34
|
+
return policy
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func (p *Policy) Apply(state *models.RunState) error {
|
|
38
|
+
if state == nil {
|
|
39
|
+
return nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
appendOrReplaceReviewGate(state, models.ValidationResult{
|
|
43
|
+
Name: "review_scorecard_valid",
|
|
44
|
+
Stage: "review",
|
|
45
|
+
Status: models.ValidationPass,
|
|
46
|
+
Severity: models.SeverityLow,
|
|
47
|
+
Summary: "review scorecard produced successfully",
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if state.ReviewScorecard == nil {
|
|
51
|
+
appendOrReplaceReviewGate(state, models.ValidationResult{
|
|
52
|
+
Name: "review_scorecard_valid",
|
|
53
|
+
Stage: "review",
|
|
54
|
+
Status: models.ValidationFail,
|
|
55
|
+
Severity: models.SeverityHigh,
|
|
56
|
+
Summary: "review scorecard is missing",
|
|
57
|
+
})
|
|
58
|
+
return fmt.Errorf("review scorecard is missing")
|
|
59
|
+
}
|
|
60
|
+
if state.Review == nil {
|
|
61
|
+
appendOrReplaceReviewGate(state, models.ValidationResult{
|
|
62
|
+
Name: "review_decision_threshold_met",
|
|
63
|
+
Stage: "review",
|
|
64
|
+
Status: models.ValidationFail,
|
|
65
|
+
Severity: models.SeverityHigh,
|
|
66
|
+
Summary: "review result is missing",
|
|
67
|
+
})
|
|
68
|
+
return fmt.Errorf("review result is missing")
|
|
69
|
+
}
|
|
70
|
+
if state.Confidence == nil {
|
|
71
|
+
appendOrReplaceReviewGate(state, models.ValidationResult{
|
|
72
|
+
Name: "review_decision_threshold_met",
|
|
73
|
+
Stage: "review",
|
|
74
|
+
Status: models.ValidationFail,
|
|
75
|
+
Severity: models.SeverityHigh,
|
|
76
|
+
Summary: "confidence report is missing",
|
|
77
|
+
})
|
|
78
|
+
return fmt.Errorf("confidence report is missing")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if !p.enabled {
|
|
82
|
+
appendOrReplaceReviewGate(state, models.ValidationResult{
|
|
83
|
+
Name: "review_decision_threshold_met",
|
|
84
|
+
Stage: "review",
|
|
85
|
+
Status: models.ValidationPass,
|
|
86
|
+
Severity: models.SeverityLow,
|
|
87
|
+
Summary: "confidence enforcement disabled; review threshold considered satisfied",
|
|
88
|
+
})
|
|
89
|
+
return nil
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
score := state.Confidence.Score
|
|
93
|
+
if score < p.failBelow {
|
|
94
|
+
message := fmt.Sprintf("confidence %.2f is below fail threshold %.2f", score, p.failBelow)
|
|
95
|
+
markReviewRevise(state, message)
|
|
96
|
+
appendOrReplaceReviewGate(state, models.ValidationResult{
|
|
97
|
+
Name: "review_decision_threshold_met",
|
|
98
|
+
Stage: "review",
|
|
99
|
+
Status: models.ValidationFail,
|
|
100
|
+
Severity: models.SeverityHigh,
|
|
101
|
+
Summary: message,
|
|
102
|
+
ActionableItems: []string{"Do not complete the run; inspect the confidence warnings and regenerate the patch with tighter scope and stronger verification."},
|
|
103
|
+
})
|
|
104
|
+
return fmt.Errorf("%s", message)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if score < p.completeMin {
|
|
108
|
+
message := fmt.Sprintf("confidence %.2f is below completion threshold %.2f", score, p.completeMin)
|
|
109
|
+
markReviewRevise(state, message)
|
|
110
|
+
appendOrReplaceReviewGate(state, models.ValidationResult{
|
|
111
|
+
Name: "review_decision_threshold_met",
|
|
112
|
+
Stage: "review",
|
|
113
|
+
Status: models.ValidationFail,
|
|
114
|
+
Severity: models.SeverityMedium,
|
|
115
|
+
Summary: message,
|
|
116
|
+
ActionableItems: []string{"Retry the patch until confidence reaches the completion threshold or reduce uncertainty in validation/test signals."},
|
|
117
|
+
})
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
appendOrReplaceReviewGate(state, models.ValidationResult{
|
|
122
|
+
Name: "review_decision_threshold_met",
|
|
123
|
+
Stage: "review",
|
|
124
|
+
Status: models.ValidationPass,
|
|
125
|
+
Severity: models.SeverityLow,
|
|
126
|
+
Summary: fmt.Sprintf("confidence %.2f meets completion threshold %.2f", score, p.completeMin),
|
|
127
|
+
})
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func appendOrReplaceReviewGate(state *models.RunState, gate models.ValidationResult) {
|
|
132
|
+
if state == nil {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
for i, result := range state.ValidationResults {
|
|
136
|
+
if result.Name == gate.Name && strings.EqualFold(result.Stage, gate.Stage) {
|
|
137
|
+
state.ValidationResults[i] = gate
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
state.ValidationResults = append(state.ValidationResults, gate)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func markReviewRevise(state *models.RunState, message string) {
|
|
145
|
+
if state == nil {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if state.Review != nil {
|
|
149
|
+
state.Review.Decision = models.ReviewRevise
|
|
150
|
+
state.Review.Comments = uniqueNonEmptyPolicy(append(state.Review.Comments, message))
|
|
151
|
+
state.Review.Suggestions = uniqueNonEmptyPolicy(append(state.Review.Suggestions, "Increase confidence by improving validation, tests, or review findings before completion."))
|
|
152
|
+
}
|
|
153
|
+
if state.ReviewScorecard != nil {
|
|
154
|
+
state.ReviewScorecard.Decision = models.ReviewRevise
|
|
155
|
+
state.ReviewScorecard.Findings = uniqueNonEmptyPolicy(append(state.ReviewScorecard.Findings, message))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func uniqueNonEmptyPolicy(values []string) []string {
|
|
160
|
+
result := make([]string, 0, len(values))
|
|
161
|
+
seen := map[string]struct{}{}
|
|
162
|
+
for _, value := range values {
|
|
163
|
+
trimmed := strings.TrimSpace(value)
|
|
164
|
+
if trimmed == "" {
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if _, ok := seen[trimmed]; ok {
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
seen[trimmed] = struct{}{}
|
|
171
|
+
result = append(result, trimmed)
|
|
172
|
+
}
|
|
173
|
+
return result
|
|
174
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package confidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
7
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestPolicyPassesWhenConfidenceMeetsThreshold(t *testing.T) {
|
|
11
|
+
policy := NewPolicy(config.DefaultConfig())
|
|
12
|
+
state := &models.RunState{
|
|
13
|
+
Review: &models.ReviewResult{Decision: models.ReviewAccept},
|
|
14
|
+
ReviewScorecard: &models.ReviewScorecard{Decision: models.ReviewAccept},
|
|
15
|
+
Confidence: &models.ConfidenceReport{Score: 0.82, Band: "medium"},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if err := policy.Apply(state); err != nil {
|
|
19
|
+
t.Fatalf("expected policy pass, got error: %v", err)
|
|
20
|
+
}
|
|
21
|
+
if state.Review.Decision != models.ReviewAccept {
|
|
22
|
+
t.Fatalf("expected accept decision to remain")
|
|
23
|
+
}
|
|
24
|
+
if !hasGate(state.ValidationResults, "review_decision_threshold_met", models.ValidationPass) {
|
|
25
|
+
t.Fatalf("expected passing review threshold gate")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestPolicyRevisesWhenConfidenceBelowCompletionThreshold(t *testing.T) {
|
|
30
|
+
policy := NewPolicy(config.DefaultConfig())
|
|
31
|
+
state := &models.RunState{
|
|
32
|
+
Review: &models.ReviewResult{Decision: models.ReviewAccept},
|
|
33
|
+
ReviewScorecard: &models.ReviewScorecard{Decision: models.ReviewAccept},
|
|
34
|
+
Confidence: &models.ConfidenceReport{Score: 0.61, Band: "low"},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if err := policy.Apply(state); err != nil {
|
|
38
|
+
t.Fatalf("expected revise path without hard error, got: %v", err)
|
|
39
|
+
}
|
|
40
|
+
if state.Review.Decision != models.ReviewRevise {
|
|
41
|
+
t.Fatalf("expected review decision to downgrade to revise")
|
|
42
|
+
}
|
|
43
|
+
if !hasGate(state.ValidationResults, "review_decision_threshold_met", models.ValidationFail) {
|
|
44
|
+
t.Fatalf("expected failing review threshold gate")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func TestPolicyFailsWhenConfidenceBelowFailThreshold(t *testing.T) {
|
|
49
|
+
policy := NewPolicy(config.DefaultConfig())
|
|
50
|
+
state := &models.RunState{
|
|
51
|
+
Review: &models.ReviewResult{Decision: models.ReviewAccept},
|
|
52
|
+
ReviewScorecard: &models.ReviewScorecard{Decision: models.ReviewAccept},
|
|
53
|
+
Confidence: &models.ConfidenceReport{Score: 0.30, Band: "very_low"},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if err := policy.Apply(state); err == nil {
|
|
57
|
+
t.Fatalf("expected hard failure for very low confidence")
|
|
58
|
+
}
|
|
59
|
+
if state.Review.Decision != models.ReviewRevise {
|
|
60
|
+
t.Fatalf("expected decision to downgrade to revise before failure")
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func hasGate(results []models.ValidationResult, name string, status models.ValidationStatus) bool {
|
|
65
|
+
for _, result := range results {
|
|
66
|
+
if result.Name == name && result.Status == status {
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false
|
|
71
|
+
}
|