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,156 @@
|
||||
# Domain Entity Scaffold Template
|
||||
|
||||
Generate a domain entity with factory constructor, value objects, and errors.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{Name}}` — PascalCase entity name (e.g., `Training`, `Hour`, `Order`)
|
||||
- `{{name}}` — camelCase (e.g., `training`)
|
||||
- `{{name_lower}}` — all lowercase package name (e.g., `training`)
|
||||
- `{{name_snake}}` — snake_case (e.g., `training`)
|
||||
|
||||
## File: `domain/{{name_lower}}/{{name_snake}}.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// {{Name}} is the aggregate root for the {{name_lower}} domain.
|
||||
type {{Name}} struct {
|
||||
uuid string
|
||||
createdAt time.Time
|
||||
// TODO: Add domain fields (all private)
|
||||
// status Status // value object, not raw string
|
||||
}
|
||||
|
||||
// New{{Name}} creates a new {{Name}} with validated invariants.
|
||||
func New{{Name}}(uuid string) (*{{Name}}, error) {
|
||||
if uuid == "" {
|
||||
return nil, errors.New("empty {{name_lower}} uuid")
|
||||
}
|
||||
|
||||
return &{{Name}}{
|
||||
uuid: uuid,
|
||||
createdAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Unmarshal{{Name}}FromDatabase reconstructs a {{Name}} from persistence.
|
||||
// Bypasses validation — data was valid when stored.
|
||||
func Unmarshal{{Name}}FromDatabase(
|
||||
uuid string,
|
||||
createdAt time.Time,
|
||||
// TODO: Add all persisted fields
|
||||
) *{{Name}} {
|
||||
return &{{Name}}{
|
||||
uuid: uuid,
|
||||
createdAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Accessor methods — expose state without allowing mutation.
|
||||
|
||||
func (t {{Name}}) UUID() string {
|
||||
return t.uuid
|
||||
}
|
||||
|
||||
func (t {{Name}}) CreatedAt() time.Time {
|
||||
return t.createdAt
|
||||
}
|
||||
|
||||
// TODO: Add behavior methods using domain language.
|
||||
// Examples:
|
||||
//
|
||||
// func (t *{{Name}}) Approve() error {
|
||||
// if t.status != Pending {
|
||||
// return ErrNotPending
|
||||
// }
|
||||
// t.status = Approved
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// func (t *{{Name}}) Cancel() error { ... }
|
||||
// func (t *{{Name}}) Submit(details string) error { ... }
|
||||
```
|
||||
|
||||
## File: `domain/{{name_lower}}/errors.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "errors"
|
||||
|
||||
// Sentinel errors — simple, no context needed.
|
||||
var (
|
||||
ErrNotFound = errors.New("{{name_lower}} not found")
|
||||
// TODO: Add domain-specific errors
|
||||
// ErrAlreadyCanceled = errors.New("{{name_lower}} already canceled")
|
||||
// ErrNotPending = errors.New("{{name_lower}} is not in pending state")
|
||||
)
|
||||
|
||||
// Typed errors — carry context for logging/display.
|
||||
// Example:
|
||||
//
|
||||
// type ForbiddenError struct {
|
||||
// RequestingUserUUID string
|
||||
// OwnerUUID string
|
||||
// }
|
||||
//
|
||||
// func (e ForbiddenError) Error() string {
|
||||
// return fmt.Sprintf("user %s cannot access {{name_lower}} owned by %s",
|
||||
// e.RequestingUserUUID, e.OwnerUUID)
|
||||
// }
|
||||
```
|
||||
|
||||
## File: `domain/{{name_lower}}/status.go` (Optional Value Object)
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Status is a value object — cannot be constructed with arbitrary values.
|
||||
type Status struct {
|
||||
s string
|
||||
}
|
||||
|
||||
var (
|
||||
Pending = Status{"pending"}
|
||||
Approved = Status{"approved"}
|
||||
Canceled = Status{"canceled"}
|
||||
)
|
||||
|
||||
func NewStatusFromString(s string) (Status, error) {
|
||||
switch s {
|
||||
case "pending":
|
||||
return Pending, nil
|
||||
case "approved":
|
||||
return Approved, nil
|
||||
case "canceled":
|
||||
return Canceled, nil
|
||||
default:
|
||||
return Status{}, fmt.Errorf("unknown {{name_lower}} status: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
return s.s
|
||||
}
|
||||
|
||||
func (s Status) IsZero() bool {
|
||||
return s == Status{}
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Creation Checklist
|
||||
|
||||
- [ ] All struct fields are private (unexported)
|
||||
- [ ] Factory constructor validates all invariants
|
||||
- [ ] UnmarshalFromDatabase accepts all persisted fields
|
||||
- [ ] Value objects are struct wrappers, not type aliases
|
||||
- [ ] Behavior methods use domain language, not CRUD
|
||||
- [ ] Errors are sentinel vars or typed structs
|
||||
Reference in New Issue
Block a user