# Repo v2 map — ground truth (with `file:line`)

The one source of truth for where the "v2" architecture actually lives. Verified against source.
Lessons cite this; if code moves, fix it here first.

## The pattern matrix (the headline)

| Pattern | Lives in | Does NOT live in |
|---|---|---|
| **Hexagonal / ports & adapters** | eureka `v2/` (documented canonical), **spike**, **notification** (`modules/`), **conversationmgmt** (`modules/conversation`) | — but adoption is uneven; see leaks below |
| **CQRS** (folders + handler types, **no bus**) | **spike** (`commands/`+`queries/`), **notification** `system_notification` (canonical) | eureka uses `usecase/` + Reader/Writer split, not `commands/`/`queries/` |
| **sqlc** | **ONLY `internal/eureka`** (v1 legacy + v2 + dwh) | **NOT** spike, notification, conversationmgmt — they hand-roll dynamic SQL |

**The key fact for this course:** your three services do hexagonal + CQRS but **never sqlc**. sqlc
is eureka-only — so Part 3 borrows eureka (the `assessment` module) for the purest example.

## Naming — the vocabulary is split
- **eureka** calls it "**v2**" (literal dir `internal/eureka/v2/`), framed as "**clean architecture +
  DDD**", layers = transport / usecase / domain / repository.
- **spike / notification** call it the "**modules/ layer**", framed as "**hexagonal + CQRS**",
  layers = controller / application(commands,queries) / core-or-domain / infrastructure.
- Same idea, two dialects. This course uses **hexagonal + CQRS + sqlc** as the umbrella.

---

## 1. Hexagonal — ports & adapters

### spike (cleanest of your services) — `internal/spike/modules/email/`
```
domain/{model,dto}        application/{commands,queries,consumers,media_manager}
infrastructure/repo_port.go (PORTS)  infrastructure/repositories/ (ADAPTERS)
controller/{grpc,http,kafka}
```
- **Repository port (interface)** — `internal/spike/modules/email/infrastructure/repo_port.go:11`
  `type EmailRepo interface { UpsertEmail(...) }`
- **Adapter (impl)** — `internal/spike/modules/email/infrastructure/repositories/email.go:19`
  `UpsertEmail`; builds dynamic SQL at `:32-50`, `db.Exec` at `:52`. **No sqlc.**
- Layering rules documented in `internal/spike/CLAUDE.md:29-88`.

### conversationmgmt — `internal/conversationmgmt/modules/conversation/`
Explicit `core/` + `infrastructure/` split:
```
core/domain  core/port/{repository,service,publisher,consumer}  core/service  core/dto
infrastructure/{postgres,grpc,kafka,nats}   controller/{grpc,http,kafka,nats}
```
- **Repository port** — `.../core/port/repository/conversation.go:10` `type ConversationRepo interface`
  (methods self-annotated `// Command` at :11, `// Query` at :17 — CQRS naming *inside* one interface).
- **Service port** — `.../core/port/service/conversation_modifier.go:9` `type ConversationModifierService interface`
- **Service impl** — `.../core/service/conversation_modifier_create_conversation.go:19` `CreateConversation`
- **Repository adapter** — `.../infrastructure/postgres/conversation.go:23` `type ConversationRepo struct{}`;
  dynamic SQL `:33-50`. **No sqlc.**
- **Transport** — `.../controller/grpc/create_conversation.go:14`.

### eureka v2 (documented canonical) — `internal/eureka/v2/modules/{module}/`
```
domain/ (pure Go, no pgtype)   repository/storage.go (PORTS) + repository/postgres/ (ADAPTERS)
usecase/abstraction.go (interfaces) + one-file-per-operation   transport/{grpc,kafka}
```
- **Port** — `internal/eureka/v2/modules/learning_time/repository/storage.go:11`
  `type LearningTimeWriterRepo interface` (role-split: Writer/Reader/... at :11,18,30,37,45; reference
  only `domain.*` + `database.Ext`, never pgx).
- **Adapter** — `.../repository/postgres/learning_time_postgres.go:22` `type LearningTimePostgresRepo struct{ querier lt_db.Querier }`.
- **Usecase interfaces** — `.../usecase/abstraction.go:62-101`.
- Docs: `internal/eureka/CLAUDE.md` "v2 Architecture"; `internal/eureka/v2/readme.md:28-61`.

### ⚠️ The dependency-inversion LEAK (flagship, verified)
`internal/conversationmgmt/modules/conversation/core/service/conversation_modifier.go`:
- `:6` — the **core** imports `.../infrastructure/postgres`
- `:38-45` — `NewConversationModifierService` news up concrete adapters *inside the core*:
  `ConversationRepo: &postgres.ConversationRepo{}`, `...MemberRepo: &postgres.ConversationMemberRepo{}`, …

So `core` depends on `infrastructure` — the composition root lives *in the core*, not in `cmd`.
Textbook hexagonal forbids this (the whole point is the core knows only ports). Contrast eureka,
whose interface files never import postgres and wire in `cmd/server/eureka/gserver.go`, and spike,
which wires in the controller constructor. **This is a real smell in code you own — Lesson 4.**

### Purity rule (verified — `.claude/rules/eureka-v2-conventions.md`)
`repository/storage.go` interfaces must reference only domain types + `database.Ext`, never
pgx/postgres. `database.Ext` accepts both a real pool AND a tx handle → that's how transactions are
injected. **`pgtype` leak smell:** notification `announcement` leaks `pgtype` into *usecase*
signatures (`internal/notification/CLAUDE.md:69-75`) — recognise it as an anti-pattern.

---

## 2. CQRS — commands & queries (NO bus)

CQRS here = **folder + handler-type separation, called directly**. There is **no command
bus / mediator / dispatcher** anywhere. Controllers call handlers directly.

### spike — explicit `commands/` vs `queries/`
- **Command handler** — `internal/spike/modules/email/application/commands/create_email_handler.go:19`
  `type CreateEmailHandler struct { DB database.Ext; EmailRepo ...; EmailRecipientRepo ... }`;
  `CreateEmail` at `:27`; `database.ExecInTx` at `:42`; two repo writes at `:43,48`.
- **Query handler** — `internal/spike/modules/email/application/queries/get_email_event_handler.go:18`
  `type GetEmailEventHandler struct`; `GetEmailDeliveryStatuses` at `:24`.
- **No bus / inline port** — the controller declares the handler as an *inline interface*:
  `internal/spike/modules/email/controller/grpc/email_modifier_service.go:25-30`
  (`EmailCommandHandler interface { CreateEmail(...) }`), wired to the concrete handler at `:52-57`.
- Documented: `internal/spike/CLAUDE.md:31`.

### notification — canonical CQRS module `system_notification`
- **Command handler** — `.../modules/system_notification/application/commands/upsert_system_notification_command_handler.go:14`
  (`db database.QueryExecer` passed **per call** → cross-handler transactions).
- **Query handler** — `.../application/queries/retrieve_system_notification_query_handler.go:17`.
- `internal/notification/CLAUDE.md:71` — "✅ Canonical — copy this"; handlers **shared by gRPC and Kafka**.

---

## 3. sqlc — typed SQL (eureka only)

### Configs (all under `internal/eureka`)
- `internal/eureka/sqlc.yaml` — **legacy** (stock `gen: go:` blocks + `genTraced.go` post-processing).
- `internal/eureka/v2/sqlc.yaml` — **active v2** — custom **Manabie WASM plugin**
  (`sqlc-gen-go.wasm`, `v1.1.0-manabie`), per-module `queries/`. Modules: `learning_time, book,
  assessment, statistic, exporter, ai_tutor`.
- `internal/eureka/v2/sqlc.dwh.yaml` — data-warehouse reads.

### v2 config options (verified — `v2/sqlc.yaml`)
- `emit_interface: true` → generates a `Querier` interface (mockable).
- `emit_dynamic_filter: true` → optional WHERE clauses (v2-only feature).
- `emit_tracing:` → injects an OTel span per method: `interceptors.StartSpan(ctx, "{Module}Queries.{{.MethodName}}")`.
- `go_generate_mock:` → mockery auto-generates the Querier mock into `mock/eureka/v2/.../db`.
  (assessment adds `--with-expecter`.)
- `emit_methods_with_db_argument: true` → `db.New()` takes no args; DB is passed per method.
- `emit_err_nil_if_no_rows: true` → returns `(nil,nil)`, not `pgx.ErrNoRows`.
- `sql_package: pgx/v4`, `omit_sqlc_version: true`.
- Makefile: `Makefile:165 eureka-gen-sqlc` (legacy), `Makefile:171 eureka-gen-sqlc-v2` (v2 + dwh + mocks).

### Generated artifacts (learning_time)
`internal/eureka/v2/modules/learning_time/repository/postgres/db/`:
`db.go:12 DBTX`, `db.go:18 New()`, `db.go:22 Queries struct{}`; `querier.go:11 Querier interface`,
`querier.go:15 var _ Querier = (*Queries)(nil)`; `learning_time.sql.go` (const SQL + Params + method
+ injected span); `models.go`.
Source: `.../queries/learning_time.sql:1` (`-- name: BulkRemoveLearningTimesBySessionIDs :exec`).

### The adapter wraps the Querier (verified)
`internal/eureka/v2/modules/learning_time/repository/postgres/learning_time_postgres.go`:
- `:22-24` `type LearningTimePostgresRepo struct { querier lt_db.Querier }`
- `:26-28` `NewLearningTimePostgresRepo()` → `&LearningTimePostgresRepo{querier: lt_db.New()}`
- `:83-87` `BulkRemoveLearningTimesBySessionIDs` delegates to `l.querier.BulkRemove...`
- **Constructor, not literal:** the repo holds a `querier` field the constructor fills; a bare
  `&LearningTimePostgresRepo{}` leaves it nil → panics (`.claude/rules/eureka-v2-conventions.md`).
  (Contrast: spike/convo repos are stateless `struct{}` → bare `&Repo{}` is fine.)

### ⚠️ sqlc is a HYBRID even in eureka v2 (verified)
In `learning_time_postgres.go`, **only 1 of ~17 methods** (`BulkRemoveLearningTimesBySessionIDs`
:83) calls the sqlc `querier`. `InsertNewLearningTime` :30, `UpdateLearningTimeByID` :46,
`RemoveLearningTimesBySessionID` :69 all hand-roll SQL via `database.Insert`/`UpdateFields`/`fmt.Sprintf`.
**Use the `assessment` module as the "real sqlc" exemplar.**

### The read/write asymmetry (flagship, verified — `internal/eureka/CLAUDE.md`)
- **Read:** any module may read any table via the sqlc `Querier` **directly** from a usecase.
- **Write:** all writes MUST go through a repository interface — "**Never call sqlc mutating
  queries (INSERT/UPDATE/DELETE) directly from a usecase.**" (So domain invariants, event emission,
  observability are never bypassed.)
- Live example — `internal/eureka/v2/modules/assessment/usecase/reset_student_study_plan.go`:
  `:25` reads via `uc.AssessmentQuerier.ListRichSubmissionsByCourseAndStudents(ctx, uc.DB, …)`;
  `:60` opens `database.ExecInTxV2`; `:61,65,69,73,77` write via repo ports
  (`SubmissionRemoverRepo.BulkRemoveSubmissions`, `LearningTimeWriterRepo.BulkRemove…`, …).
  Adapter delegate: `.../assessment/repository/postgres/submission_postgres.go:24` (querier) → `:1198`.

---

## Myths to pre-empt (all verified)
1. **sqlc ≠ your services.** conversationmgmt / notification / spike use zero sqlc (all dynamic SQL).
2. **sqlc is partial even in eureka v2** (`learning_time` hybrid) — teach with `assessment`.
3. **No CQRS bus/mediator.** "CQRS" = folders + handler types, invoked directly.
4. **conversationmgmt leaks DI** (`conversation_modifier.go:6,38-45`) — core imports infrastructure.
5. **conversationmgmt isn't uniform** — only `modules/conversation` uses `core/port`; `chatthread`,
   `agora_usermgmt` differ.
6. **notification is two-generation** (legacy flat + `modules/`) and its modules are inconsistent.
7. **Read/write asymmetry is a RULE, not sloppiness** — read via sqlc; write via repo port.
8. **Two sqlc mechanisms** — legacy stock `gen:` + genTraced vs v2 WASM plugin. Don't conflate.
9. **`.claude/rules/*.md` are the real spec** — more prescriptive than the readmes.
