refactor: turn the repo into a two-plugin marketplace
tea and tdl were two repositories, each carrying its own
.claude-plugin/marketplace.json — two marketplaces to register for what
is one collection. Fold them into one.
The repo root is now the marketplace and nothing else: a single
.claude-plugin/marketplace.json whose entries point at ./plugins/tea and
./plugins/tdl. A plugin's root is its own directory under plugins/, so
${CLAUDE_PLUGIN_ROOT} still resolves inside it and every path a plugin
uses stays relative to itself — the hooks and the test roots needed no
adjustment beyond the move.
tea's files move with git mv, so its history and blame follow. tdl
arrives as a plain copy; its history stays in claude-skills/threedotslab.
test_payload_root asserted `tmp/` was ignored by REPO/.gitignore. The
rule is that tmp/ is ignored, not which file says so, and git reads every
.gitignore on the way up — so the test now walks up to the repo root the
same way git does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
# Repository Rules (REPO-01..07)
|
||||
|
||||
## REPO-01: Interface Defined in Domain (CRITICAL)
|
||||
|
||||
Repository interfaces MUST be defined in the domain package, next to the entity they persist. This follows the Dependency Inversion Principle — the domain defines what it needs, adapters implement it.
|
||||
|
||||
**Correct:**
|
||||
```go
|
||||
// domain/hour/repository.go
|
||||
package hour
|
||||
|
||||
type Repository interface {
|
||||
GetHour(ctx context.Context, hourTime time.Time) (*Hour, error)
|
||||
UpdateHour(ctx context.Context, hourTime time.Time,
|
||||
updateFn func(h *Hour) (*Hour, error)) error
|
||||
}
|
||||
```
|
||||
|
||||
**Wrong:**
|
||||
```go
|
||||
// adapters/repository.go ← VIOLATION: interface in adapter layer
|
||||
package adapters
|
||||
|
||||
type HourRepository interface { ... }
|
||||
```
|
||||
|
||||
**Check:** Grep `domain/` for `type.*Repository interface`. Grep `adapters/` for the same — if found in adapters, it's a violation.
|
||||
|
||||
---
|
||||
|
||||
## REPO-02: Update Callback Pattern (WARNING)
|
||||
|
||||
Repository update methods SHOULD use a callback/closure pattern. The repository handles transaction lifecycle; the callback handles domain logic.
|
||||
|
||||
```go
|
||||
// Interface
|
||||
UpdateHour(ctx context.Context, hourTime time.Time,
|
||||
updateFn func(h *Hour) (*Hour, error)) error
|
||||
|
||||
// Usage in handler
|
||||
err := h.hourRepo.UpdateHour(ctx, cmd.Hour, func(h *hour.Hour) (*hour.Hour, error) {
|
||||
if err := h.CancelTraining(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
})
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Transaction scope is clear
|
||||
- Domain logic is isolated from persistence details
|
||||
- Enables optimistic locking, retries, etc. transparently
|
||||
|
||||
---
|
||||
|
||||
## REPO-03: Separate DB Model Structs (WARNING)
|
||||
|
||||
Adapter implementations MUST use separate structs for database representation. Domain entities should NOT have serialization tags.
|
||||
|
||||
**Correct:**
|
||||
```go
|
||||
// adapters/ — DB model
|
||||
type mysqlHour struct {
|
||||
ID int `db:"id"`
|
||||
Hour time.Time `db:"hour"`
|
||||
Availability string `db:"availability"`
|
||||
}
|
||||
|
||||
// or for Firestore (needs exported fields for tags)
|
||||
type TrainingModel struct {
|
||||
UUID string `firestore:"Uuid"`
|
||||
UserUUID string `firestore:"UserUuid"`
|
||||
Time time.Time `firestore:"Time"`
|
||||
}
|
||||
|
||||
// Conversion in adapter
|
||||
func (r *MySQLHourRepository) toHour(m mysqlHour) (*hour.Hour, error) {
|
||||
availability, err := hour.NewAvailabilityFromString(m.Availability)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hour.UnmarshalHourFromDatabase(m.Hour, availability), nil
|
||||
}
|
||||
```
|
||||
|
||||
**Wrong:**
|
||||
```go
|
||||
// domain/hour/hour.go
|
||||
type Hour struct {
|
||||
Hour time.Time `json:"hour" db:"hour"` // VIOLATION: DB tags on domain entity
|
||||
Availability string `json:"availability"` // VIOLATION: serialization concern in domain
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## REPO-04: Adapter Constructor Naming (INFO)
|
||||
|
||||
Repository adapter constructors follow: `New{Technology}{Entity}Repository`
|
||||
|
||||
```go
|
||||
func NewFirestoreHourRepository(client *firestore.Client, factory hour.Factory) *FirestoreHourRepository
|
||||
func NewMySQLHourRepository(db *sqlx.DB) *MySQLHourRepository
|
||||
func NewMemoryHourRepository(factory hour.Factory) *MemoryHourRepository
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## REPO-05: Technology Suffix Naming (INFO)
|
||||
|
||||
Adapter types use technology as a suffix/prefix to distinguish implementations.
|
||||
|
||||
```go
|
||||
type FirestoreHourRepository struct { ... }
|
||||
type MySQLHourRepository struct { ... }
|
||||
type MemoryHourRepository struct { ... }
|
||||
|
||||
// For external service clients
|
||||
type TrainerGrpc struct { ... }
|
||||
type UsersGrpc struct { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## REPO-06: Shared Test Suite (WARNING)
|
||||
|
||||
Repository tests SHOULD run the same test logic against ALL implementations (memory, MySQL, Firestore, etc.). This ensures behavioral consistency.
|
||||
|
||||
**Pattern:**
|
||||
```go
|
||||
func createRepositories(t *testing.T) []Repository {
|
||||
return []Repository{
|
||||
{Name: "Firebase", Repository: newFirebaseRepository(t)},
|
||||
{Name: "MySQL", Repository: newMySQLRepository(t)},
|
||||
{Name: "memory", Repository: adapters.NewMemoryHourRepository(testFactory)},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository(t *testing.T) {
|
||||
repositories := createRepositories(t)
|
||||
for i := range repositories {
|
||||
r := repositories[i] // capture loop variable
|
||||
t.Run(r.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testUpdateHour(t, r.Repository)
|
||||
testUpdateHour_parallel(t, r.Repository)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Check:** Look for test files in `adapters/` that test repository implementations. Verify they use a shared test function or table-driven approach.
|
||||
|
||||
---
|
||||
|
||||
## REPO-07: UnmarshalFromDatabase Usage (WARNING)
|
||||
|
||||
Adapter implementations MUST use the entity's `UnmarshalFromDatabase` function to reconstruct domain objects from persistence, not the regular constructor.
|
||||
|
||||
**Correct:**
|
||||
```go
|
||||
func (r *FirestoreHourRepository) toHour(doc *firestore.DocumentSnapshot) (*hour.Hour, error) {
|
||||
var m HourModel
|
||||
if err := doc.DataTo(&m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
availability, err := hour.NewAvailabilityFromString(m.Availability)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hour.UnmarshalHourFromDatabase(m.Hour, availability), nil
|
||||
}
|
||||
```
|
||||
|
||||
**Wrong:**
|
||||
```go
|
||||
func (r *FirestoreHourRepository) toHour(doc *firestore.DocumentSnapshot) (*hour.Hour, error) {
|
||||
// VIOLATION: using business constructor for DB reconstruction
|
||||
return hour.NewAvailableHour(m.Hour) // This re-validates and may reject valid stored data
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user