Project Structure¶
go-crap-tracker follows Hexagonal Architecture (Ports & Adapters) — it was scaffolded with HexaGo. Business logic in internal/core depends only on port interfaces; adapters in internal/adapters implement them, so infrastructure stays swappable.
go-crap-tracker/
├── main.go # entry point → cmd.Execute()
├── cmd/ # Cobra CLI commands
│ ├── root.go # root command, config init
│ ├── serve.go # control plane startup
│ ├── worker.go # worker startup
│ ├── publish.go # Hugo report generation
│ ├── repo.go # repo cache + per-repo config management
│ ├── version.go # version
│ └── helpers.go # shared DB/migration helpers
├── internal/
│ ├── core/
│ │ ├── domain/ # pure business entities & value objects
│ │ │ ├── scantasks/ # ScanTask + status state machine
│ │ │ ├── repoanalyses/# RepoAnalysis result entity
│ │ │ ├── repos/ # canonical repo registry / ID helpers
│ │ │ ├── repocfg/ # per-repo overrides
│ │ │ ├── repospec/ # repository spec (url + ref)
│ │ │ ├── jobspecs/ # NATS job spec
│ │ │ ├── repoinfo/ # cloned repo info
│ │ │ ├── repometadata/# GitHub metadata
│ │ │ ├── crapreport/ # go-crap report parser
│ │ │ ├── crapsummary/ # CRAP score summary
│ │ │ └── crapentry/ # per-function entry
│ │ ├── ports/ # port interfaces
│ │ │ ├── inbound/ # Scheduler, ScanTaskService, WorkerService, Reconciler
│ │ │ └── outbound/ # NATSClient, ScanTaskStore, ResultStore, RepoLock, RepoConfigStore, EventRelay, ...
│ │ └── usecases/ # application logic
│ │ ├── schedule/ # schedule a scan task
│ │ ├── scantaskservice/ # task CRUD, seen/hidden, result lookup
│ │ ├── reconciler/ # stale/retry/reschedule loop
│ │ ├── scancoordinator/ # lock → clone → test → scan → save
│ │ └── publishreport/ # build the Hugo report
│ ├── adapters/
│ │ ├── primary/ # inbound (drives the app)
│ │ │ └── http/ # HTTP server (Chi)
│ │ │ ├── http.go # wiring: health + UI + API
│ │ │ ├── health/ # /livez, /readyz, /health
│ │ │ ├── api/ # REST + SSE handlers
│ │ │ └── ui/ # embedded SPA (static/)
│ │ └── secondary/ # outbound (driven by the app)
│ │ ├── nats/ # JetStream client (streams, consumers, events, heartbeats)
│ │ ├── postgres/ # task/result/repo/repo-config stores, repo locks
│ │ ├── repocache/ # shared clone + coverage cache, lease refresh
│ │ ├── coveragegen/ # go test -coverprofile executor
│ │ ├── runner/ # go-crap runner (local)
│ │ ├── githubmeta/ # GitHub metadata fetcher
│ │ ├── eventrelay/ # SSE event hub
│ │ ├── markdown/ # Hugo/PaperMod report publisher
│ │ └── workerregistry/# worker heartbeat registry
│ ├── config/ # Viper config loader + defaults
│ └── workers/ # scan worker loop (queue, heartbeat, commands)
├── pkg/
│ ├── httpserver/ # reusable HTTP server wrapper (timeouts, graceful stop)
│ └── logger/ # structured logger
├── migrations/ # golang-migrate SQL (embedded)
├── infra/ # Kubernetes manifests
├── pages/ # Hugo (PaperMod) blog the publish command writes into
├── main.go
├── Makefile
├── Dockerfile
└── go.mod
Dependency rule¶
adapters ──▶ ports ◀── core (domain + usecases)
│
└── implement the ports; core never imports adapters
internal/core has no external dependencies beyond the standard library and the domain. Adapters import the ports they implement. This is what lets you swap Postgres for something else, or add a gRPC inbound adapter, without touching the domain.