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.
Files changed (116) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +624 -0
  4. package/cmd/apply.go +111 -0
  5. package/cmd/auth.go +393 -0
  6. package/cmd/auth_test.go +100 -0
  7. package/cmd/diff.go +57 -0
  8. package/cmd/doctor.go +149 -0
  9. package/cmd/explain.go +192 -0
  10. package/cmd/explain_test.go +62 -0
  11. package/cmd/init.go +100 -0
  12. package/cmd/interactive.go +1372 -0
  13. package/cmd/interactive_input.go +45 -0
  14. package/cmd/interactive_input_test.go +55 -0
  15. package/cmd/logs.go +72 -0
  16. package/cmd/model.go +84 -0
  17. package/cmd/plan.go +149 -0
  18. package/cmd/provider.go +189 -0
  19. package/cmd/provider_model_doctor_test.go +91 -0
  20. package/cmd/root.go +67 -0
  21. package/cmd/run.go +123 -0
  22. package/cmd/run_engine.go +208 -0
  23. package/cmd/run_engine_test.go +30 -0
  24. package/cmd/session.go +589 -0
  25. package/cmd/session_helpers.go +54 -0
  26. package/cmd/session_integration_test.go +30 -0
  27. package/cmd/session_list_current_test.go +87 -0
  28. package/cmd/session_messages_test.go +163 -0
  29. package/cmd/session_runs_test.go +68 -0
  30. package/cmd/sprint1_integration_test.go +119 -0
  31. package/cmd/stats.go +173 -0
  32. package/cmd/stats_test.go +71 -0
  33. package/cmd/version.go +4 -0
  34. package/go.mod +45 -0
  35. package/go.sum +108 -0
  36. package/internal/agents/agent.go +31 -0
  37. package/internal/agents/coder.go +167 -0
  38. package/internal/agents/planner.go +155 -0
  39. package/internal/agents/reviewer.go +118 -0
  40. package/internal/agents/runtime.go +25 -0
  41. package/internal/agents/runtime_test.go +77 -0
  42. package/internal/auth/account.go +78 -0
  43. package/internal/auth/oauth.go +523 -0
  44. package/internal/auth/store.go +287 -0
  45. package/internal/confidence/policy.go +174 -0
  46. package/internal/confidence/policy_test.go +71 -0
  47. package/internal/confidence/scorer.go +253 -0
  48. package/internal/confidence/scorer_test.go +83 -0
  49. package/internal/config/config.go +331 -0
  50. package/internal/config/config_defaults_test.go +138 -0
  51. package/internal/execution/contract_builder.go +160 -0
  52. package/internal/execution/contract_builder_test.go +68 -0
  53. package/internal/execution/plan_compliance.go +161 -0
  54. package/internal/execution/plan_compliance_test.go +71 -0
  55. package/internal/execution/retry_directive.go +132 -0
  56. package/internal/execution/scope_guard.go +69 -0
  57. package/internal/logger/logger.go +120 -0
  58. package/internal/models/contracts_test.go +100 -0
  59. package/internal/models/models.go +269 -0
  60. package/internal/orchestrator/orchestrator.go +701 -0
  61. package/internal/orchestrator/orchestrator_retry_test.go +135 -0
  62. package/internal/orchestrator/review_engine_test.go +50 -0
  63. package/internal/orchestrator/state.go +42 -0
  64. package/internal/orchestrator/test_classifier_test.go +68 -0
  65. package/internal/patch/applier.go +131 -0
  66. package/internal/patch/applier_test.go +25 -0
  67. package/internal/patch/parser.go +89 -0
  68. package/internal/patch/patch.go +60 -0
  69. package/internal/patch/summary.go +30 -0
  70. package/internal/patch/validator.go +104 -0
  71. package/internal/planning/normalizer.go +416 -0
  72. package/internal/planning/normalizer_test.go +64 -0
  73. package/internal/providers/errors.go +35 -0
  74. package/internal/providers/openai/client.go +498 -0
  75. package/internal/providers/openai/client_test.go +187 -0
  76. package/internal/providers/provider.go +47 -0
  77. package/internal/providers/registry.go +32 -0
  78. package/internal/providers/registry_test.go +57 -0
  79. package/internal/providers/router.go +52 -0
  80. package/internal/providers/state.go +114 -0
  81. package/internal/providers/state_test.go +64 -0
  82. package/internal/repo/analyzer.go +188 -0
  83. package/internal/repo/context.go +83 -0
  84. package/internal/review/engine.go +267 -0
  85. package/internal/review/engine_test.go +103 -0
  86. package/internal/runstore/store.go +137 -0
  87. package/internal/runstore/store_test.go +59 -0
  88. package/internal/runtime/lock.go +150 -0
  89. package/internal/runtime/lock_test.go +57 -0
  90. package/internal/session/compaction.go +260 -0
  91. package/internal/session/compaction_test.go +36 -0
  92. package/internal/session/service.go +117 -0
  93. package/internal/session/service_test.go +113 -0
  94. package/internal/storage/storage.go +1498 -0
  95. package/internal/storage/storage_test.go +413 -0
  96. package/internal/testing/classifier.go +80 -0
  97. package/internal/testing/classifier_test.go +36 -0
  98. package/internal/tools/command.go +160 -0
  99. package/internal/tools/command_test.go +56 -0
  100. package/internal/tools/file.go +111 -0
  101. package/internal/tools/git.go +77 -0
  102. package/internal/tools/invalid_params_test.go +36 -0
  103. package/internal/tools/policy.go +98 -0
  104. package/internal/tools/policy_test.go +36 -0
  105. package/internal/tools/registry_test.go +52 -0
  106. package/internal/tools/result.go +30 -0
  107. package/internal/tools/search.go +86 -0
  108. package/internal/tools/tool.go +94 -0
  109. package/main.go +9 -0
  110. package/npm/orch.js +25 -0
  111. package/package.json +41 -0
  112. package/scripts/changelog.js +20 -0
  113. package/scripts/check-release-version.js +21 -0
  114. package/scripts/lib/release-utils.js +223 -0
  115. package/scripts/postinstall.js +157 -0
  116. package/scripts/release.js +52 -0
@@ -0,0 +1,47 @@
1
+ package providers
2
+
3
+ import "context"
4
+
5
+ type Role string
6
+
7
+ const (
8
+ RolePlanner Role = "planner"
9
+ RoleCoder Role = "coder"
10
+ RoleReviewer Role = "reviewer"
11
+ )
12
+
13
+ type ChatRequest struct {
14
+ Role Role
15
+ Model string
16
+ SystemPrompt string
17
+ UserPrompt string
18
+ MaxTokens int
19
+ Temperature float64
20
+ ReasoningEffort string
21
+ }
22
+
23
+ type Usage struct {
24
+ InputTokens int
25
+ OutputTokens int
26
+ TotalTokens int
27
+ }
28
+
29
+ type ChatResponse struct {
30
+ Text string
31
+ FinishReason string
32
+ Usage Usage
33
+ ProviderMetadata map[string]string
34
+ }
35
+
36
+ type StreamEvent struct {
37
+ Type string
38
+ Text string
39
+ Metadata map[string]string
40
+ }
41
+
42
+ type Provider interface {
43
+ Name() string
44
+ Validate(ctx context.Context) error
45
+ Chat(ctx context.Context, req ChatRequest) (ChatResponse, error)
46
+ Stream(ctx context.Context, req ChatRequest) (<-chan StreamEvent, <-chan error)
47
+ }
@@ -0,0 +1,32 @@
1
+ package providers
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ )
7
+
8
+ type Registry struct {
9
+ providers map[string]Provider
10
+ }
11
+
12
+ func NewRegistry() *Registry {
13
+ return &Registry{providers: make(map[string]Provider)}
14
+ }
15
+
16
+ func (r *Registry) Register(p Provider) {
17
+ if r == nil || p == nil {
18
+ return
19
+ }
20
+ r.providers[strings.ToLower(p.Name())] = p
21
+ }
22
+
23
+ func (r *Registry) Get(name string) (Provider, error) {
24
+ if r == nil {
25
+ return nil, fmt.Errorf("provider registry is nil")
26
+ }
27
+ p, ok := r.providers[strings.ToLower(strings.TrimSpace(name))]
28
+ if !ok {
29
+ return nil, fmt.Errorf("provider not found: %s", name)
30
+ }
31
+ return p, nil
32
+ }
@@ -0,0 +1,57 @@
1
+ package providers
2
+
3
+ import (
4
+ "context"
5
+ "testing"
6
+
7
+ "github.com/furkanbeydemir/orch/internal/config"
8
+ )
9
+
10
+ type fakeProvider struct{ name string }
11
+
12
+ func (f fakeProvider) Name() string { return f.name }
13
+
14
+ func (f fakeProvider) Validate(ctx context.Context) error { return nil }
15
+
16
+ func (f fakeProvider) Chat(ctx context.Context, req ChatRequest) (ChatResponse, error) {
17
+ return ChatResponse{Text: "ok"}, nil
18
+ }
19
+
20
+ func (f fakeProvider) Stream(ctx context.Context, req ChatRequest) (<-chan StreamEvent, <-chan error) {
21
+ ev := make(chan StreamEvent)
22
+ err := make(chan error)
23
+ close(ev)
24
+ close(err)
25
+ return ev, err
26
+ }
27
+
28
+ func TestRegistryGetProvider(t *testing.T) {
29
+ reg := NewRegistry()
30
+ reg.Register(fakeProvider{name: "openai"})
31
+
32
+ got, err := reg.Get("openai")
33
+ if err != nil {
34
+ t.Fatalf("get provider: %v", err)
35
+ }
36
+ if got.Name() != "openai" {
37
+ t.Fatalf("unexpected provider: %s", got.Name())
38
+ }
39
+ }
40
+
41
+ func TestRouterResolveRoleModel(t *testing.T) {
42
+ cfg := config.DefaultConfig()
43
+ reg := NewRegistry()
44
+ reg.Register(fakeProvider{name: "openai"})
45
+
46
+ router := NewRouter(cfg, reg)
47
+ provider, model, err := router.Resolve(RoleCoder)
48
+ if err != nil {
49
+ t.Fatalf("resolve route: %v", err)
50
+ }
51
+ if provider.Name() != "openai" {
52
+ t.Fatalf("unexpected provider: %s", provider.Name())
53
+ }
54
+ if model != cfg.Provider.OpenAI.Models.Coder {
55
+ t.Fatalf("unexpected model: got=%s want=%s", model, cfg.Provider.OpenAI.Models.Coder)
56
+ }
57
+ }
@@ -0,0 +1,52 @@
1
+ package providers
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/furkanbeydemir/orch/internal/config"
7
+ )
8
+
9
+ type Router struct {
10
+ cfg *config.Config
11
+ registry *Registry
12
+ }
13
+
14
+ func NewRouter(cfg *config.Config, registry *Registry) *Router {
15
+ return &Router{cfg: cfg, registry: registry}
16
+ }
17
+
18
+ func (r *Router) Resolve(role Role) (Provider, string, error) {
19
+ if r == nil || r.cfg == nil || r.registry == nil {
20
+ return nil, "", fmt.Errorf("provider router is not initialized")
21
+ }
22
+
23
+ providerName := r.cfg.Provider.Default
24
+ model := modelForRole(r.cfg, role)
25
+
26
+ provider, err := r.registry.Get(providerName)
27
+ if err != nil {
28
+ return nil, "", err
29
+ }
30
+
31
+ if model == "" {
32
+ return nil, "", fmt.Errorf("model not configured for role %s", role)
33
+ }
34
+
35
+ return provider, model, nil
36
+ }
37
+
38
+ func modelForRole(cfg *config.Config, role Role) string {
39
+ if cfg == nil {
40
+ return ""
41
+ }
42
+ switch role {
43
+ case RolePlanner:
44
+ return cfg.Provider.OpenAI.Models.Planner
45
+ case RoleCoder:
46
+ return cfg.Provider.OpenAI.Models.Coder
47
+ case RoleReviewer:
48
+ return cfg.Provider.OpenAI.Models.Reviewer
49
+ default:
50
+ return ""
51
+ }
52
+ }
@@ -0,0 +1,114 @@
1
+ package providers
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "sort"
7
+ "strings"
8
+ "time"
9
+
10
+ "github.com/furkanbeydemir/orch/internal/auth"
11
+ "github.com/furkanbeydemir/orch/internal/config"
12
+ )
13
+
14
+ type OpenAIState struct {
15
+ Enabled bool
16
+ Connected bool
17
+ Mode string
18
+ Source string
19
+ Reason string
20
+ }
21
+
22
+ type ProviderState struct {
23
+ All []string
24
+ Default map[string]string
25
+ Connected []string
26
+ OpenAI OpenAIState
27
+ }
28
+
29
+ func ReadState(repoRoot string) (ProviderState, error) {
30
+ cfg, err := config.Load(repoRoot)
31
+ if err != nil {
32
+ return ProviderState{}, fmt.Errorf("failed to load config: %w", err)
33
+ }
34
+
35
+ state := ProviderState{
36
+ All: []string{},
37
+ Default: map[string]string{},
38
+ Connected: []string{},
39
+ OpenAI: OpenAIState{
40
+ Enabled: cfg.Provider.Flags.OpenAIEnabled,
41
+ Mode: strings.ToLower(strings.TrimSpace(cfg.Provider.OpenAI.AuthMode)),
42
+ },
43
+ }
44
+
45
+ if state.OpenAI.Mode == "" {
46
+ state.OpenAI.Mode = "api_key"
47
+ }
48
+
49
+ if cfg.Provider.Flags.OpenAIEnabled {
50
+ state.All = append(state.All, "openai")
51
+ state.Default["openai"] = strings.TrimSpace(cfg.Provider.OpenAI.Models.Coder)
52
+ }
53
+
54
+ if !cfg.Provider.Flags.OpenAIEnabled {
55
+ state.OpenAI.Reason = "provider disabled"
56
+ return state, nil
57
+ }
58
+
59
+ var connected bool
60
+ var source string
61
+
62
+ switch state.OpenAI.Mode {
63
+ case "api_key":
64
+ if strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.APIKeyEnv)) != "" {
65
+ connected = true
66
+ source = "env"
67
+ } else {
68
+ cred, credErr := auth.Get(repoRoot, "openai")
69
+ if credErr == nil && cred != nil && strings.TrimSpace(strings.ToLower(cred.Type)) == "api" && strings.TrimSpace(cred.Key) != "" {
70
+ connected = true
71
+ source = "local"
72
+ }
73
+ }
74
+ if !connected {
75
+ state.OpenAI.Reason = fmt.Sprintf("missing API key (%s or local auth)", cfg.Provider.OpenAI.APIKeyEnv)
76
+ }
77
+ case "account":
78
+ if strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.AccountTokenEnv)) != "" {
79
+ connected = true
80
+ source = "env"
81
+ } else {
82
+ cred, credErr := auth.Get(repoRoot, "openai")
83
+ if credErr == nil && cred != nil && strings.TrimSpace(strings.ToLower(cred.Type)) == "oauth" && strings.TrimSpace(cred.AccessToken) != "" {
84
+ expired := !cred.ExpiresAt.IsZero() && time.Now().UTC().After(cred.ExpiresAt)
85
+ if expired && strings.TrimSpace(cred.RefreshToken) == "" {
86
+ connected = false
87
+ state.OpenAI.Reason = "stored oauth token expired and missing refresh token"
88
+ } else {
89
+ connected = true
90
+ source = "local"
91
+ }
92
+ }
93
+ }
94
+ if !connected {
95
+ if state.OpenAI.Reason == "" {
96
+ state.OpenAI.Reason = fmt.Sprintf("missing account token (%s or local auth)", cfg.Provider.OpenAI.AccountTokenEnv)
97
+ }
98
+ }
99
+ default:
100
+ state.OpenAI.Reason = fmt.Sprintf("invalid auth mode (%s)", state.OpenAI.Mode)
101
+ }
102
+
103
+ state.OpenAI.Connected = connected
104
+ state.OpenAI.Source = source
105
+ if connected {
106
+ state.Connected = append(state.Connected, "openai")
107
+ state.OpenAI.Reason = ""
108
+ }
109
+
110
+ sort.Strings(state.All)
111
+ sort.Strings(state.Connected)
112
+
113
+ return state, nil
114
+ }
@@ -0,0 +1,64 @@
1
+ package providers
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/furkanbeydemir/orch/internal/auth"
7
+ "github.com/furkanbeydemir/orch/internal/config"
8
+ )
9
+
10
+ func TestReadStateDisconnectedWithoutCredentials(t *testing.T) {
11
+ repoRoot := t.TempDir()
12
+ cfg := config.DefaultConfig()
13
+ cfg.Provider.Default = "openai"
14
+ cfg.Provider.OpenAI.AuthMode = "api_key"
15
+ if err := config.Save(repoRoot, cfg); err != nil {
16
+ t.Fatalf("save config: %v", err)
17
+ }
18
+
19
+ t.Setenv(cfg.Provider.OpenAI.APIKeyEnv, "")
20
+
21
+ state, err := ReadState(repoRoot)
22
+ if err != nil {
23
+ t.Fatalf("read state: %v", err)
24
+ }
25
+ if len(state.All) != 1 || state.All[0] != "openai" {
26
+ t.Fatalf("unexpected all providers: %+v", state.All)
27
+ }
28
+ if state.OpenAI.Connected {
29
+ t.Fatalf("expected disconnected openai state")
30
+ }
31
+ if len(state.Connected) != 0 {
32
+ t.Fatalf("expected no connected providers, got: %+v", state.Connected)
33
+ }
34
+ }
35
+
36
+ func TestReadStateConnectedWithStoredAPIKey(t *testing.T) {
37
+ repoRoot := t.TempDir()
38
+ cfg := config.DefaultConfig()
39
+ cfg.Provider.Default = "openai"
40
+ cfg.Provider.OpenAI.AuthMode = "api_key"
41
+ if err := config.Save(repoRoot, cfg); err != nil {
42
+ t.Fatalf("save config: %v", err)
43
+ }
44
+
45
+ if err := auth.Set(repoRoot, "openai", auth.Credential{Type: "api", Key: "sk-test"}); err != nil {
46
+ t.Fatalf("save auth: %v", err)
47
+ }
48
+
49
+ t.Setenv(cfg.Provider.OpenAI.APIKeyEnv, "")
50
+
51
+ state, err := ReadState(repoRoot)
52
+ if err != nil {
53
+ t.Fatalf("read state: %v", err)
54
+ }
55
+ if !state.OpenAI.Connected {
56
+ t.Fatalf("expected connected openai state")
57
+ }
58
+ if state.OpenAI.Source != "local" {
59
+ t.Fatalf("expected local source, got: %s", state.OpenAI.Source)
60
+ }
61
+ if len(state.Connected) != 1 || state.Connected[0] != "openai" {
62
+ t.Fatalf("unexpected connected providers: %+v", state.Connected)
63
+ }
64
+ }
@@ -0,0 +1,188 @@
1
+ // - Primary programming language
2
+ // - File inventory
3
+ package repo
4
+
5
+ import (
6
+ "encoding/json"
7
+ "fmt"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+
12
+ "github.com/furkanbeydemir/orch/internal/models"
13
+ )
14
+
15
+ type Analyzer struct {
16
+ rootPath string
17
+ }
18
+
19
+ func NewAnalyzer(rootPath string) *Analyzer {
20
+ return &Analyzer{
21
+ rootPath: rootPath,
22
+ }
23
+ }
24
+
25
+ func (a *Analyzer) Analyze() (*models.RepoMap, error) {
26
+ repoMap := &models.RepoMap{
27
+ RootPath: a.rootPath,
28
+ Files: make([]models.FileInfo, 0),
29
+ }
30
+
31
+ err := filepath.Walk(a.rootPath, func(path string, info os.FileInfo, err error) error {
32
+ if err != nil {
33
+ }
34
+
35
+ // Skip hidden directories and .orch.
36
+ if info.IsDir() {
37
+ name := info.Name()
38
+ if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" {
39
+ return filepath.SkipDir
40
+ }
41
+ return nil
42
+ }
43
+
44
+ // Resolve relative path.
45
+ relPath, err := filepath.Rel(a.rootPath, path)
46
+ if err != nil {
47
+ return nil
48
+ }
49
+
50
+ fileInfo := models.FileInfo{
51
+ Path: relPath,
52
+ Language: detectLanguage(path),
53
+ Size: info.Size(),
54
+ }
55
+
56
+ repoMap.Files = append(repoMap.Files, fileInfo)
57
+ return nil
58
+ })
59
+
60
+ if err != nil {
61
+ return nil, fmt.Errorf("repository scan failed: %w", err)
62
+ }
63
+
64
+ repoMap.Language = a.detectMainLanguage(repoMap.Files)
65
+ repoMap.PackageManager = a.detectPackageManager()
66
+ repoMap.TestFramework = a.detectTestFramework()
67
+
68
+ // Persist repo map.
69
+ if err := a.saveRepoMap(repoMap); err != nil {
70
+ return nil, err
71
+ }
72
+
73
+ return repoMap, nil
74
+ }
75
+
76
+ func detectLanguage(path string) string {
77
+ ext := strings.ToLower(filepath.Ext(path))
78
+ languages := map[string]string{
79
+ ".go": "go",
80
+ ".js": "javascript",
81
+ ".ts": "typescript",
82
+ ".py": "python",
83
+ ".rs": "rust",
84
+ ".java": "java",
85
+ ".rb": "ruby",
86
+ ".cpp": "cpp",
87
+ ".c": "c",
88
+ ".cs": "csharp",
89
+ ".php": "php",
90
+ ".swift": "swift",
91
+ ".kt": "kotlin",
92
+ ".md": "markdown",
93
+ ".json": "json",
94
+ ".yaml": "yaml",
95
+ ".yml": "yaml",
96
+ ".toml": "toml",
97
+ ".xml": "xml",
98
+ ".html": "html",
99
+ ".css": "css",
100
+ ".sql": "sql",
101
+ ".sh": "shell",
102
+ }
103
+
104
+ if lang, ok := languages[ext]; ok {
105
+ return lang
106
+ }
107
+ return "unknown"
108
+ }
109
+
110
+ // detectMainLanguage infers the primary language from the file inventory.
111
+ func (a *Analyzer) detectMainLanguage(files []models.FileInfo) string {
112
+ counts := make(map[string]int)
113
+ for _, f := range files {
114
+ if f.Language != "unknown" && f.Language != "markdown" && f.Language != "json" && f.Language != "yaml" {
115
+ counts[f.Language]++
116
+ }
117
+ }
118
+
119
+ maxLang := "unknown"
120
+ maxCount := 0
121
+ for lang, count := range counts {
122
+ if count > maxCount {
123
+ maxLang = lang
124
+ maxCount = count
125
+ }
126
+ }
127
+
128
+ return maxLang
129
+ }
130
+
131
+ func (a *Analyzer) detectPackageManager() string {
132
+ indicators := map[string]string{
133
+ "go.mod": "go modules",
134
+ "package.json": "npm",
135
+ "requirements.txt": "pip",
136
+ "Cargo.toml": "cargo",
137
+ "pom.xml": "maven",
138
+ "build.gradle": "gradle",
139
+ "Gemfile": "bundler",
140
+ "composer.json": "composer",
141
+ }
142
+
143
+ for file, manager := range indicators {
144
+ if _, err := os.Stat(filepath.Join(a.rootPath, file)); err == nil {
145
+ return manager
146
+ }
147
+ }
148
+
149
+ return "unknown"
150
+ }
151
+
152
+ func (a *Analyzer) detectTestFramework() string {
153
+ indicators := map[string]string{
154
+ "go.mod": "go test",
155
+ "jest.config.js": "jest",
156
+ "jest.config.ts": "jest",
157
+ "pytest.ini": "pytest",
158
+ "setup.cfg": "pytest",
159
+ ".rspec": "rspec",
160
+ }
161
+
162
+ for file, framework := range indicators {
163
+ if _, err := os.Stat(filepath.Join(a.rootPath, file)); err == nil {
164
+ return framework
165
+ }
166
+ }
167
+
168
+ return "unknown"
169
+ }
170
+
171
+ func (a *Analyzer) saveRepoMap(repoMap *models.RepoMap) error {
172
+ orchDir := filepath.Join(a.rootPath, ".orch")
173
+ if err := os.MkdirAll(orchDir, 0o755); err != nil {
174
+ return fmt.Errorf("failed to create .orch directory: %w", err)
175
+ }
176
+
177
+ data, err := json.MarshalIndent(repoMap, "", " ")
178
+ if err != nil {
179
+ return fmt.Errorf("failed to serialize repo map: %w", err)
180
+ }
181
+
182
+ mapPath := filepath.Join(orchDir, "repo-map.json")
183
+ if err := os.WriteFile(mapPath, data, 0o644); err != nil {
184
+ return fmt.Errorf("failed to write repo map file: %w", err)
185
+ }
186
+
187
+ return nil
188
+ }
@@ -0,0 +1,83 @@
1
+ // Package repo - Context Builder implementasyonu.
2
+ //
3
+ // Girdi:
4
+ // - Planner hints (opsiyonel)
5
+ package repo
6
+
7
+ import (
8
+ "path/filepath"
9
+ "strings"
10
+
11
+ "github.com/furkanbeydemir/orch/internal/models"
12
+ )
13
+
14
+ type ContextBuilder struct {
15
+ rootPath string
16
+ }
17
+
18
+ func NewContextBuilder(rootPath string) *ContextBuilder {
19
+ return &ContextBuilder{
20
+ rootPath: rootPath,
21
+ }
22
+ }
23
+
24
+ func (cb *ContextBuilder) Build(task *models.Task, repoMap *models.RepoMap, plan *models.Plan) *models.ContextResult {
25
+ result := &models.ContextResult{
26
+ SelectedFiles: make([]string, 0),
27
+ RelatedTests: make([]string, 0),
28
+ RelevantConfigs: make([]string, 0),
29
+ }
30
+
31
+ if plan != nil {
32
+ result.SelectedFiles = append(result.SelectedFiles, plan.FilesToModify...)
33
+ result.SelectedFiles = append(result.SelectedFiles, plan.FilesToInspect...)
34
+ }
35
+
36
+ for _, file := range repoMap.Files {
37
+ if isTestFile(file.Path) {
38
+ result.RelatedTests = append(result.RelatedTests, file.Path)
39
+ } else if isConfigFile(file.Path) {
40
+ result.RelevantConfigs = append(result.RelevantConfigs, file.Path)
41
+ }
42
+ }
43
+
44
+ return result
45
+ }
46
+
47
+ func isTestFile(path string) bool {
48
+ base := strings.ToLower(filepath.Base(path))
49
+ patterns := []string{
50
+ "_test.go",
51
+ ".test.js", ".test.ts",
52
+ ".spec.js", ".spec.ts",
53
+ "test_", "_test.py",
54
+ }
55
+
56
+ for _, pattern := range patterns {
57
+ if strings.Contains(base, pattern) {
58
+ return true
59
+ }
60
+ }
61
+
62
+ dir := strings.ToLower(filepath.Dir(path))
63
+ return strings.Contains(dir, "test") || strings.Contains(dir, "__tests__")
64
+ }
65
+
66
+ func isConfigFile(path string) bool {
67
+ base := strings.ToLower(filepath.Base(path))
68
+ configs := []string{
69
+ "config", "conf", ".env", ".rc",
70
+ "tsconfig", "jest.config", "webpack",
71
+ "package.json", "go.mod", "cargo.toml",
72
+ "dockerfile", "docker-compose", "makefile",
73
+ }
74
+
75
+ for _, cfg := range configs {
76
+ if strings.Contains(base, cfg) {
77
+ return true
78
+ }
79
+ }
80
+
81
+ ext := strings.ToLower(filepath.Ext(path))
82
+ return ext == ".yaml" || ext == ".yml" || ext == ".toml" || ext == ".ini"
83
+ }