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,150 @@
|
||||
# Error Rules (ERR-01..05)
|
||||
|
||||
## Three-Tier Error Architecture
|
||||
|
||||
The error system has three tiers:
|
||||
|
||||
1. **Domain errors** — sentinel variables and typed structs in `domain/`
|
||||
2. **Application errors** — `SlugError` with machine-readable slugs in `app/`
|
||||
3. **Port errors** — protocol-specific error mapping in `ports/`
|
||||
|
||||
---
|
||||
|
||||
## ERR-01: Sentinel Error Variables (WARNING)
|
||||
|
||||
Simple domain errors without context SHOULD use sentinel `var` declarations.
|
||||
|
||||
```go
|
||||
// domain/hour/errors.go
|
||||
var (
|
||||
ErrNotFullHour = errors.New("hour should be a full hour")
|
||||
ErrPastHour = errors.New("cannot create hour in the past")
|
||||
ErrTrainingScheduled = errors.New("unable to modify hour, because scheduled training")
|
||||
ErrHourNotAvailable = errors.New("hour is not available")
|
||||
ErrNoTrainingScheduled = errors.New("no training scheduled")
|
||||
)
|
||||
```
|
||||
|
||||
**Naming:** `Err{DescriptiveName}` — always starts with `Err`.
|
||||
|
||||
**Usage in domain methods:**
|
||||
```go
|
||||
func (h *Hour) ScheduleTraining() error {
|
||||
if !h.IsAvailable() {
|
||||
return ErrHourNotAvailable
|
||||
}
|
||||
h.availability = TrainingScheduled
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ERR-02: Typed Error Structs (WARNING)
|
||||
|
||||
Errors that carry context (values for logging/display) SHOULD be typed structs implementing the `error` interface.
|
||||
|
||||
```go
|
||||
type TooDistantDateError struct {
|
||||
MaxWeeksInTheFutureToSet int
|
||||
ProvidedDate time.Time
|
||||
}
|
||||
|
||||
func (e TooDistantDateError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"schedule can be only set for next %d weeks, provided date: %s",
|
||||
e.MaxWeeksInTheFutureToSet, e.ProvidedDate,
|
||||
)
|
||||
}
|
||||
|
||||
type TooEarlyHourError struct {
|
||||
MinUtcHour int
|
||||
ProvidedTime time.Time
|
||||
}
|
||||
|
||||
type ForbiddenToSeeTrainingError struct {
|
||||
RequestingUserUUID string
|
||||
TrainingOwnerUUID string
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
TrainingUUID string
|
||||
}
|
||||
```
|
||||
|
||||
**Naming:** `{Condition}Error` — describes the error condition.
|
||||
|
||||
---
|
||||
|
||||
## ERR-03: SlugError for Application Layer (WARNING)
|
||||
|
||||
Application-layer errors (command/query handlers) SHOULD use `SlugError` from the common errors package. SlugErrors carry:
|
||||
- Human-readable error message
|
||||
- Machine-readable slug (used by API clients)
|
||||
- Error type (authorization, incorrect-input, unknown)
|
||||
|
||||
```go
|
||||
// common/errors/errors.go
|
||||
type ErrorType struct {
|
||||
t string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrorTypeUnknown = ErrorType{"unknown"}
|
||||
ErrorTypeAuthorization = ErrorType{"authorization"}
|
||||
ErrorTypeIncorrectInput = ErrorType{"incorrect-input"}
|
||||
)
|
||||
|
||||
type SlugError struct {
|
||||
error string
|
||||
slug string
|
||||
errorType ErrorType
|
||||
}
|
||||
|
||||
func NewSlugError(error string, slug string) SlugError
|
||||
func NewAuthorizationError(error string, slug string) SlugError
|
||||
func NewIncorrectInputError(error string, slug string) SlugError
|
||||
```
|
||||
|
||||
**Usage in handlers:**
|
||||
```go
|
||||
func (h cancelTrainingHandler) Handle(ctx context.Context, cmd CancelTraining) error {
|
||||
if 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
|
||||
}); err != nil {
|
||||
return errors.NewSlugError(err.Error(), "unable-to-update-availability")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ERR-04: Error Wrapping with Context (INFO)
|
||||
|
||||
When re-raising errors, wrap them with context using `fmt.Errorf("context: %w", err)` or a wrapping library.
|
||||
|
||||
```go
|
||||
// In adapters
|
||||
if err := doc.DataTo(&model); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling hour from firestore: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ERR-05: No Bare fmt.Errorf in Domain (CRITICAL)
|
||||
|
||||
The domain package MUST NOT use `fmt.Errorf` for error creation. Domain errors must be either:
|
||||
- Sentinel variables (`var ErrX = errors.New(...)`)
|
||||
- Typed error structs
|
||||
- Standard `errors.New(...)` for simple cases
|
||||
|
||||
**Check:** Grep `domain/` for `fmt.Errorf`. Any match in non-test files is a violation.
|
||||
|
||||
**Rationale:** `fmt.Errorf` creates untyped errors that cannot be checked with `errors.Is` or `errors.As`. Domain errors should be programmatically handleable.
|
||||
|
||||
**Exception:** `fmt.Errorf` with `%w` for wrapping IS acceptable in domain validation helpers that combine multiple checks, but prefer typed errors or sentinel variables.
|
||||
Reference in New Issue
Block a user