Files
naudachu 83f73c5cea 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>
2026-08-11 00:25:28 +05:00

3.5 KiB

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

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

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)

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