Skip to content

Commit e76601f

Browse files
committed
refactor: reorganize CCG into hexagonal architecture
1 parent 58caa72 commit e76601f

307 files changed

Lines changed: 10437 additions & 10564 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -146,52 +146,55 @@ Actual output from CCG parsing its own codebase.
146146

147147
```
148148
$ ccg build .
149-
Build complete: 127 files, 1220 nodes, 12222 edges
149+
Build complete: 145 files, 1654 nodes, 10489 edges
150150
```
151151

152152
### 2. Graph Statistics
153153

154154
```
155155
$ ccg status
156-
Nodes: 1220
157-
Edges: 12222
158-
Files: 127
156+
Nodes: 1654
157+
Edges: 3767
158+
Files: 192
159159
160160
Node kinds:
161-
class: 124
162-
file: 127
163-
function: 405
164-
test: 543
165-
type: 21
161+
class: 253
162+
file: 145
163+
function: 1111
164+
package: 47
165+
type: 98
166166
167167
Edge kinds:
168-
calls: 9245
169-
contains: 1097
170-
imports_from: 1128
171-
inherits: 1
172-
tested_by: 751
173-
174-
Postprocess:
175-
Status: ok
176-
Fail-closed: 0
177-
Recent failures: 0
168+
calls: 1822
169+
contains: 1597
170+
imports_from: 278
171+
implements: 66
172+
inherits: 4
173+
174+
Fallback call analysis:
175+
calls: 1823
176+
fallback_calls: 0
177+
fallback_ratio: 0.00%
178178
```
179179

180180
### 3. Code Search
181181

182182
```
183183
$ ccg search "impact analysis"
184-
internal/analysis/impact/impact_test.go file internal/analysis/impact/impact_test.go:1
185-
internal/analysis/impact/impact.go file internal/analysis/impact/impact.go:1
186-
mcp.ImpactAnalyzer type internal/mcp/server.go:36
187-
impact.EdgeReader type internal/analysis/impact/impact.go:12
188-
impact.Analyzer.ImpactRadius function internal/analysis/impact/impact.go:42
189-
internal/mcp/handler_analysis.go file internal/mcp/handler_analysis.go:1
190-
191-
$ ccg search "dead code"
192-
deadcode.Options class internal/analysis/deadcode/service.go:14
193-
deadcode.Service.Find function internal/analysis/deadcode/service.go:38
194-
mcp.handlers.findDeadCode function internal/mcp/handler_analysis.go:273
184+
internal/app/analyze/impact/impact.go file internal/app/analyze/impact/impact.go:1
185+
mcp.impactRadiusResponse class internal/adapters/inbound/mcp/handler_analysis.go:36
186+
internal/adapters/inbound/mcp/handler_analysis.go file internal/adapters/inbound/mcp/handler_analysis.go:1
187+
mcp.ImpactAnalyzer type internal/adapters/inbound/mcp/deps.go:40
188+
mcp.impactRadiusMetadata class internal/adapters/inbound/mcp/handler_analysis.go:27
189+
mcp.handlers.getImpactRadius function internal/adapters/inbound/mcp/handler_analysis.go:109
190+
191+
$ ccg search "repository sync"
192+
reposync.Service.Sync function internal/app/reposync/service.go:27
193+
remote.buildRepoSyncHTTP function internal/runtime/remote/http.go:88
194+
reposync.syncPayload class internal/app/reposync/queue.go:94
195+
reposync.SyncHandlerFunc type internal/app/reposync/queue.go:21
196+
reposync.SyncQueue class internal/app/reposync/queue.go:118
197+
webhook.SyncFunc type internal/adapters/inbound/webhook/handler.go:22
195198
```
196199

197200
### 4. Agent Integration via MCP
@@ -205,7 +208,7 @@ The agent calls CCG MCP tools and answers directly from the graph:
205208
```
206209
trace_flow(qualified_name: "webhook.WebhookHandler.ServeHTTP")
207210
→ WebhookHandler.ServeHTTP
208-
→ SyncQueue.Enqueue
211+
→ SyncQueue.Add
209212
→ safeHandle (retry loop: max 3 attempts, exponential backoff 1s→30s)
210213
→ clone (git clone, 15min timeout)
211214
→ build (ccg build, same context)
@@ -215,7 +218,7 @@ trace_flow(qualified_name: "webhook.WebhookHandler.ServeHTTP")
215218
216219
```
217220
search(query: "authentication")
218-
→ internal/webhook/handler.go (HMAC signature validation)
221+
→ internal/adapters/inbound/webhook/handler.go (HMAC signature validation)
219222
→ cmd/ccg-server/main.go (--webhook-secret flag)
220223
```
221224

cmd/ccg-server/main.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
"github.com/spf13/cobra"
1111
"github.com/spf13/viper"
1212

13+
ccgserver "github.com/tae2089/code-context-graph/internal/adapters/inbound/http"
1314
ccgconfig "github.com/tae2089/code-context-graph/internal/config"
14-
"github.com/tae2089/code-context-graph/internal/core"
15-
ccgserver "github.com/tae2089/code-context-graph/internal/server"
15+
ccgruntime "github.com/tae2089/code-context-graph/internal/runtime"
16+
remote "github.com/tae2089/code-context-graph/internal/runtime/remote"
1617
"github.com/tae2089/trace"
1718
)
1819

@@ -25,7 +26,7 @@ var (
2526
// @intent run the self-hosted HTTP MCP/webhook server as a dedicated binary.
2627
func main() {
2728
logger := slog.Default()
28-
rt := core.NewRuntime(logger)
29+
rt := ccgruntime.NewRuntime(logger)
2930
cmd := newRootCmd(rt, version)
3031
if err := cmd.Execute(); err != nil {
3132
slog.Error("server command failed", trace.SlogError(err))
@@ -38,7 +39,7 @@ func main() {
3839
// newRootCmd builds the ccg-server command with HTTP and webhook options.
3940
// @intent keep self-hosted server flags separate from the local ccg CLI.
4041
// @sideEffect reads config/env, opens the DB, and starts a long-running HTTP server.
41-
func newRootCmd(rt *core.Runtime, serviceVersion string) *cobra.Command {
42+
func newRootCmd(rt *ccgruntime.Runtime, serviceVersion string) *cobra.Command {
4243
cfg := ccgserver.DefaultConfig()
4344
cfg.Transport = "streamable-http"
4445

@@ -107,7 +108,7 @@ func newRootCmd(rt *core.Runtime, serviceVersion string) *cobra.Command {
107108
return nil
108109
},
109110
RunE: func(cmd *cobra.Command, args []string) error {
110-
return ccgserver.Run(rt, cfg, serviceVersion, ccgconfig.RagIndexDir(), ccgconfig.RagDescription())
111+
return remote.RunHTTP(rt, cfg, serviceVersion, ccgconfig.RagIndexDir(), ccgconfig.RagDescription())
111112
},
112113
}
113114

cmd/ccg/health_test.go

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
"testing"
1111
"time"
1212

13+
ccgserver "github.com/tae2089/code-context-graph/internal/adapters/inbound/http"
14+
"github.com/tae2089/code-context-graph/internal/app/reposync"
1315
ccgdb "github.com/tae2089/code-context-graph/internal/db"
14-
ccgserver "github.com/tae2089/code-context-graph/internal/server"
15-
"github.com/tae2089/code-context-graph/internal/webhook"
1616
)
1717

1818
func TestHandleHealth_OK(t *testing.T) {
@@ -98,10 +98,10 @@ func TestReadyHandler_MethodNotAllowed(t *testing.T) {
9898
}
9999

100100
func TestStatusHandler_ReportsWebhookQueueFull(t *testing.T) {
101-
q := webhook.NewSyncQueueWithConfig(nil, 0, func(context.Context, string, string, string) error {
101+
q := reposync.NewSyncQueueWithConfig(nil, 0, func(context.Context, string, string, string) error {
102102
return nil
103-
}, webhook.QueueConfig{
104-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
103+
}, reposync.QueueConfig{
104+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
105105
MaxTrackedRepos: 1,
106106
})
107107
defer q.Shutdown()
@@ -112,18 +112,18 @@ func TestStatusHandler_ReportsWebhookQueueFull(t *testing.T) {
112112
req := httptest.NewRequest(http.MethodGet, "/status", nil)
113113
rec := httptest.NewRecorder()
114114

115-
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req)
115+
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *reposync.SyncQueue { return q }).ServeHTTP(rec, req)
116116

117117
if rec.Code != http.StatusServiceUnavailable {
118118
t.Fatalf("expected 503, got %d body=%s", rec.Code, rec.Body.String())
119119
}
120120
}
121121

122122
func TestStatusHandler_ReportsWebhookQueueAges(t *testing.T) {
123-
q := webhook.NewSyncQueueWithConfig(nil, 0, func(context.Context, string, string, string) error {
123+
q := reposync.NewSyncQueueWithConfig(nil, 0, func(context.Context, string, string, string) error {
124124
return nil
125-
}, webhook.QueueConfig{
126-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
125+
}, reposync.QueueConfig{
126+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
127127
MaxTrackedRepos: 2,
128128
})
129129
defer q.Shutdown()
@@ -135,7 +135,7 @@ func TestStatusHandler_ReportsWebhookQueueAges(t *testing.T) {
135135
req := httptest.NewRequest(http.MethodGet, "/status", nil)
136136
rec := httptest.NewRecorder()
137137

138-
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req)
138+
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *reposync.SyncQueue { return q }).ServeHTTP(rec, req)
139139

140140
if rec.Code != http.StatusOK {
141141
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
@@ -154,10 +154,10 @@ func TestStatusHandler_ReportsWebhookQueueAges(t *testing.T) {
154154
}
155155

156156
func TestReadyHandler_ReportsWebhookQueueDelay(t *testing.T) {
157-
q := webhook.NewSyncQueueWithConfig(nil, 0, func(context.Context, string, string, string) error {
157+
q := reposync.NewSyncQueueWithConfig(nil, 0, func(context.Context, string, string, string) error {
158158
return nil
159-
}, webhook.QueueConfig{
160-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
159+
}, reposync.QueueConfig{
160+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
161161
MaxTrackedRepos: 2,
162162
})
163163
defer q.Shutdown()
@@ -180,11 +180,11 @@ func TestReadyHandler_ReportsWebhookQueueDelay(t *testing.T) {
180180

181181
func TestStatusHandler_ReportsUnresolvedWebhookFailure(t *testing.T) {
182182
done := make(chan struct{})
183-
q := webhook.NewSyncQueueWithConfig(nil, 1, func(context.Context, string, string, string) error {
183+
q := reposync.NewSyncQueueWithConfig(nil, 1, func(context.Context, string, string, string) error {
184184
close(done)
185185
return errors.New("boom")
186-
}, webhook.QueueConfig{
187-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
186+
}, reposync.QueueConfig{
187+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
188188
MaxTrackedRepos: 2,
189189
})
190190
defer q.Shutdown()
@@ -206,7 +206,7 @@ func TestStatusHandler_ReportsUnresolvedWebhookFailure(t *testing.T) {
206206
req := httptest.NewRequest(http.MethodGet, "/status", nil)
207207
rec := httptest.NewRecorder()
208208

209-
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req)
209+
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *reposync.SyncQueue { return q }).ServeHTTP(rec, req)
210210

211211
if rec.Code != http.StatusOK {
212212
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
@@ -224,19 +224,19 @@ func TestWebhookStatsDegraded_PerRepoUnresolvedFailure(t *testing.T) {
224224
errTime := time.Now()
225225
successTime := errTime.Add(2 * time.Minute)
226226

227-
if !ccgserver.WebhookStatsDegraded(webhook.SyncQueueStats{
227+
if !ccgserver.WebhookStatsDegraded(reposync.SyncQueueStats{
228228
LastSuccessTime: successTime,
229-
RecentRepos: []webhook.RepoStats{{
229+
RecentRepos: []reposync.RepoStats{{
230230
Repo: "org/failrepo",
231231
LastErrorTime: errTime,
232232
}},
233233
}) {
234234
t.Fatal("expected unresolved repo failure to degrade status")
235235
}
236236

237-
if ccgserver.WebhookStatsDegraded(webhook.SyncQueueStats{
237+
if ccgserver.WebhookStatsDegraded(reposync.SyncQueueStats{
238238
LastSuccessTime: successTime,
239-
RecentRepos: []webhook.RepoStats{{
239+
RecentRepos: []reposync.RepoStats{{
240240
Repo: "org/recovered",
241241
LastErrorTime: errTime,
242242
LastSuccessTime: successTime,
@@ -249,16 +249,16 @@ func TestWebhookStatsDegraded_PerRepoUnresolvedFailure(t *testing.T) {
249249
func TestStatusHandler_DegradedWhenRecentRepoFailureUnresolved(t *testing.T) {
250250
var mu sync.Mutex
251251
completed := make([]string, 0, 2)
252-
q := webhook.NewSyncQueueWithConfig(nil, 1, func(_ context.Context, repoFullName, cloneURL, branch string) error {
252+
q := reposync.NewSyncQueueWithConfig(nil, 1, func(_ context.Context, repoFullName, cloneURL, branch string) error {
253253
mu.Lock()
254254
completed = append(completed, repoFullName)
255255
mu.Unlock()
256256
if repoFullName == "org/failrepo" {
257257
return errors.New("boom")
258258
}
259259
return nil
260-
}, webhook.QueueConfig{
261-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
260+
}, reposync.QueueConfig{
261+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
262262
MaxTrackedRepos: 10,
263263
})
264264
defer q.Shutdown()
@@ -282,7 +282,7 @@ func TestStatusHandler_DegradedWhenRecentRepoFailureUnresolved(t *testing.T) {
282282

283283
req := httptest.NewRequest(http.MethodGet, "/status", nil)
284284
rec := httptest.NewRecorder()
285-
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req)
285+
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *reposync.SyncQueue { return q }).ServeHTTP(rec, req)
286286

287287
if rec.Code != http.StatusOK {
288288
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
@@ -298,11 +298,11 @@ func TestStatusHandler_DegradedWhenRecentRepoFailureUnresolved(t *testing.T) {
298298

299299
func TestStatusHandler_DBNotReadyTakesPrecedenceOverWebhookDegraded(t *testing.T) {
300300
done := make(chan struct{})
301-
q := webhook.NewSyncQueueWithConfig(nil, 1, func(_ context.Context, repoFullName, cloneURL, branch string) error {
301+
q := reposync.NewSyncQueueWithConfig(nil, 1, func(_ context.Context, repoFullName, cloneURL, branch string) error {
302302
close(done)
303303
return errors.New("boom")
304-
}, webhook.QueueConfig{
305-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
304+
}, reposync.QueueConfig{
305+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
306306
MaxTrackedRepos: 10,
307307
})
308308
defer q.Shutdown()
@@ -325,7 +325,7 @@ func TestStatusHandler_DBNotReadyTakesPrecedenceOverWebhookDegraded(t *testing.T
325325

326326
req := httptest.NewRequest(http.MethodGet, "/status", nil)
327327
rec := httptest.NewRecorder()
328-
ccgserver.StatusHandler(func(r *http.Request) error { return errors.New("db down") }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req)
328+
ccgserver.StatusHandler(func(r *http.Request) error { return errors.New("db down") }, time.Minute, func() *reposync.SyncQueue { return q }).ServeHTTP(rec, req)
329329

330330
if rec.Code != http.StatusServiceUnavailable {
331331
t.Fatalf("expected 503, got %d body=%s", rec.Code, rec.Body.String())
@@ -340,7 +340,7 @@ func TestStatusHandler_DBNotReadyTakesPrecedenceOverWebhookDegraded(t *testing.T
340340
}
341341

342342
func TestReadyHandler_IgnoresUnresolvedWebhookFailure(t *testing.T) {
343-
stats := webhook.SyncQueueStats{LastErrorTime: time.Now()}
343+
stats := reposync.SyncQueueStats{LastErrorTime: time.Now()}
344344
req := httptest.NewRequest(http.MethodGet, "/ready", nil)
345345
rec := httptest.NewRecorder()
346346

@@ -355,11 +355,11 @@ func TestReadyHandler_IgnoresUnresolvedWebhookFailure(t *testing.T) {
355355

356356
func TestStatusHandler_ReportsPerRepoRecentRepos(t *testing.T) {
357357
done := make(chan struct{})
358-
q := webhook.NewSyncQueueWithConfig(nil, 1, func(_ context.Context, repoFullName, cloneURL, branch string) error {
358+
q := reposync.NewSyncQueueWithConfig(nil, 1, func(_ context.Context, repoFullName, cloneURL, branch string) error {
359359
close(done)
360360
return nil
361-
}, webhook.QueueConfig{
362-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
361+
}, reposync.QueueConfig{
362+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
363363
MaxTrackedRepos: 10,
364364
})
365365
defer q.Shutdown()
@@ -385,7 +385,7 @@ func TestStatusHandler_ReportsPerRepoRecentRepos(t *testing.T) {
385385

386386
req := httptest.NewRequest(http.MethodGet, "/status", nil)
387387
rec := httptest.NewRecorder()
388-
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req)
388+
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *reposync.SyncQueue { return q }).ServeHTTP(rec, req)
389389

390390
if rec.Code != http.StatusOK {
391391
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
@@ -419,11 +419,11 @@ func TestStatusHandler_ReportsPerRepoRecentRepos(t *testing.T) {
419419

420420
func TestStatusHandler_RecentRepos_FailureHasErrorFields(t *testing.T) {
421421
done := make(chan struct{})
422-
q := webhook.NewSyncQueueWithConfig(nil, 1, func(_ context.Context, repoFullName, cloneURL, branch string) error {
422+
q := reposync.NewSyncQueueWithConfig(nil, 1, func(_ context.Context, repoFullName, cloneURL, branch string) error {
423423
close(done)
424424
return errors.New("auth failed")
425-
}, webhook.QueueConfig{
426-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
425+
}, reposync.QueueConfig{
426+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
427427
MaxTrackedRepos: 10,
428428
})
429429
defer q.Shutdown()
@@ -449,7 +449,7 @@ func TestStatusHandler_RecentRepos_FailureHasErrorFields(t *testing.T) {
449449

450450
req := httptest.NewRequest(http.MethodGet, "/status", nil)
451451
rec := httptest.NewRecorder()
452-
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req)
452+
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *reposync.SyncQueue { return q }).ServeHTTP(rec, req)
453453

454454
var body map[string]any
455455
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
@@ -471,17 +471,17 @@ func TestStatusHandler_RecentRepos_FailureHasErrorFields(t *testing.T) {
471471
}
472472

473473
func TestStatusHandler_ExistingAggregateFieldsPreserved(t *testing.T) {
474-
q := webhook.NewSyncQueueWithConfig(nil, 0, func(context.Context, string, string, string) error {
474+
q := reposync.NewSyncQueueWithConfig(nil, 0, func(context.Context, string, string, string) error {
475475
return nil
476-
}, webhook.QueueConfig{
477-
RetryConfig: webhook.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
476+
}, reposync.QueueConfig{
477+
RetryConfig: reposync.RetryConfig{MaxAttempts: 1, BaseDelay: time.Millisecond, MaxDelay: time.Millisecond},
478478
MaxTrackedRepos: 5,
479479
})
480480
defer q.Shutdown()
481481

482482
req := httptest.NewRequest(http.MethodGet, "/status", nil)
483483
rec := httptest.NewRecorder()
484-
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req)
484+
ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *reposync.SyncQueue { return q }).ServeHTTP(rec, req)
485485

486486
var body map[string]any
487487
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {

0 commit comments

Comments
 (0)