plugins/dingo/skills/dingo-developer/SKILL.md
Comprehensive Dingo meta-language skill covering all features: error propagation (?), guard let, Result/Option types, enums, pattern matching, lambdas, safe navigation (?.), null coalescing (??), generics, tuples, expression sugar, feature combinations, code review rules, anti-patterns, and Go boundary guidance.
npx skillsauth add madappgang/magus dingo-developerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Dingo is a meta-language for Go that transpiles .dingo files to idiomatic .go files. It
provides modern language features — Result/Option types, sum types (enums), pattern matching,
error propagation, safe navigation, null coalescing, lambdas, generics, and tuples — while
maintaining 100% Go ecosystem compatibility.
Repository: https://github.com/MadAppGang/dingo
Key philosophy: Dingo is syntax sugar over Go, not a new type system. The transpiled Go
code is clean and idiomatic. All Go syntax remains valid in .dingo files.
When to use Dingo:
? operatorOption[T] and Result[T, E] types?.) and null coalescing (??)CLI commands:
dingo build # Transpile + compile binary
dingo run main.dingo # Transpile + run directly
dingo go # Generate .go files only (CI/CD)
? Operator — Three VariantsDecision tree — pick the first that applies:
?? "message"? |e| exprguard (see below)Option[T] + guard// BEFORE (Go)
user, err := getUser(id)
if err != nil {
return nil, err
}
// AFTER (Dingo)
user := getUser(id)?
Generates fmt.Errorf("message: %w", err).
// BEFORE (Go)
config, err := loadConfig(path)
if err != nil {
return nil, fmt.Errorf("load config: %w", err)
}
// AFTER (Dingo)
config := loadConfig(path) ? "load config"
Captures local variables and builds custom error types. Both Rust-style (|e|) and
TypeScript-style (e =>) are valid.
// BEFORE (Go)
resp, err := httpClient.Get(url)
if err != nil {
return nil, &NetworkError{URL: url, Err: err}
}
// AFTER — Rust-style
resp := httpClient.Get(url) ? |e| &NetworkError{URL: url, Err: e}
// AFTER — TypeScript-style (identical semantics)
resp := httpClient.Get(url) ? e => &NetworkError{URL: url, Err: e}
| Return type | ? works? | Notes |
|---|---|---|
| (T, error) | YES | Most common |
| (T1, T2, error) | YES | Extracts last value |
| Result[T, E] | YES | Unwraps to T |
| error only (void success) | YES | Propagates error directly |
| (T, bool) | NO | Use Option[T] + guard |
| T only (no error) | NO | Nothing to propagate |
? in void functions is a compile error. If you need to propagate from a
void-returning call, acknowledge explicitly:
// WRONG — compile error when function returns void
validateInput(data)?
// CORRECT
_ = validateInput(data)?
guard Statementguard provides early-return logic for Result[T, E] and Option[T] values. It is the
idiomatic way to flatten a "pyramid of doom" into a sequential happy path.
CRITICAL CONSTRAINT: guard ONLY works with Result[T, E] or Option[T] return types.
It does NOT work on plain booleans, integers, or other types.
// WRONG — guard on boolean generates invalid Go (compile error)
guard user.Active else {
return "inactive"
}
// CORRECT — guard on Option[T]
guard profile := getProfile(userID) else {
return "no profile"
}
// CORRECT — guard on Result[T, E]
guard user := findUser(id) else |err| {
return fmt.Errorf("find user: %w", err)
}
// Result: bind error value with |err|
guard user := findUser(id) else |err| {
return Err(fmt.Errorf("find user: %w", err))
}
// Option: no |err| binding (None has no error value)
guard theme := getTheme(id) else {
return "default"
}
// Reassignment: use = not := when variable already declared
guard user = refreshUser(ctx, user.ID) else |err| {
return Err(err)
}
// Tuple destructuring
guard (lat, lng) := getCoords(address) else |err| {
return Err(fmt.Errorf("coords: %w", err))
}
// BEFORE (Go) — nested error ladder
user, err := findUser(id)
if err != nil {
return "", fmt.Errorf("find user: %w", err)
}
profile, err := loadProfile(user.ID)
if err != nil {
return "", fmt.Errorf("load profile: %w", err)
}
settings, err := loadSettings(profile.ID)
if err != nil {
return "", fmt.Errorf("load settings: %w", err)
}
// AFTER (Dingo) — flat guard chain
guard user := findUser(id) else |e| { return "", fmt.Errorf("find user: %w", e) }
guard profile := loadProfile(user.ID) else |e| { return "", fmt.Errorf("load profile: %w", e) }
guard settings := loadSettings(profile.ID) else |e| { return "", fmt.Errorf("load settings: %w", e) }
? in GoroutinesThe ? operator cannot propagate errors from inside goroutines (there is no enclosing
function to return to). Use channels or sync.WaitGroup + error collection instead.
// WRONG — ? inside goroutine has no propagation target
go func() {
result := fetch()? // compile error or logic error
}()
// CORRECT — explicit error handling inside goroutines
errCh := make(chan error, 1)
go func() {
result, err := fetch()
if err != nil {
errCh <- err
return
}
process(result)
errCh <- nil
}()
if err := <-errCh; err != nil {
return nil, err
}
Use for operations that can fail when callers need to act on the error type.
Constructors: Ok[T, E](value) and Err[T, E](err)
Methods (complete list):
| Method | Description |
|---|---|
| .IsOk() | Returns true if Ok variant |
| .IsErr() | Returns true if Err variant |
| .MustOk() | Returns value; panics if Err |
| .MustErr() | Returns error; panics if Ok |
| .OkOr(default) | Returns value or default if Err |
NOT available: .Unwrap(), .Map(), .FlatMap(), .UnwrapErr() — these do not exist
in the dgo package. Do not use them.
func divide(a, b int) Result[int, string] {
if b == 0 {
return Err[int, string]("division by zero")
}
return Ok[int, string](a / b)
}
result := divide(10, 2)
if result.IsOk() {
fmt.Println(result.MustOk()) // 5
}
// Safe extraction with default
value := divide(10, 0).OkOr(0) // 0
func (s *UserService) FindByID(ctx context.Context, id string) Result[User, error] {
guard user := s.repo.FindByID(ctx, id) else |err| {
if errors.Is(err, sql.ErrNoRows) {
return Err[User, error](NotFoundError("user"))
}
return Err[User, error](InternalError(err))
}
return Ok[User, error](user)
}
Use when absence is expected and normal — not an error condition.
Constructors: Some(value) and None[T]()
Methods (complete list):
| Method | Description |
|---|---|
| .IsSome() | Returns true if Some variant |
| .IsNone() | Returns true if None variant |
| .MustSome() | Returns value; panics if None |
| .SomeOr(default) | Returns value or default |
| .SomeOrElse(fn) | Returns value or calls fn() for default |
NOT available: .Unwrap(), .Map(), .FlatMap() — these do not exist in the dgo
package. Do not use them.
func findUser(id string) Option[User] {
user, err := db.FindByID(id)
if err != nil {
return None[User]()
}
return Some(user)
}
user := findUser("123")
if user.IsSome() {
fmt.Println(user.MustSome().Name)
}
// Safe extraction with default
name := findUser("123").SomeOr(User{Name: "Anonymous"}).Name
// Lazy default (only computed when None)
name := findUser("123").SomeOrElse(func() User { return buildDefaultUser() }).Name
| Situation | Use |
|---|---|
| Absence is expected and normal ("not found") | Option[T] |
| Absence needs explanation to the caller | Result[T, E] |
| Infrastructure/stdlib calls | (T, error) + ? |
| Domain API with typed errors | Result[T, DomainError] |
| "Find" queries on collections | Option[T] |
| "Create/Update" domain operations | Result[T, error] |
Rule: If absence is the expected outcome (e.g., cache miss, optional config), use
Option[T]. If absence would surprise the caller and they need to know why, use Result[T, E].
Do NOT mix: a function should return either Result[T, E] or (T, error) — not both.
func f() (Result[User, string], error) is always wrong.
// Simple variants (no data)
enum Status {
Pending
Active
Done
}
// Variants with data
enum Event {
UserCreated { userID: int, email: string }
UserDeleted { userID: int }
EmailChanged { userID: int, oldEmail: string, newEmail: string }
}
// Mixed
enum APIResponse {
Success { transactionID: string, amount: float64 }
NotFound
Error { code: int, reason: string }
}
Constructors are auto-generated as New{EnumName}{VariantName}(fieldValues...).
// Simple variants (no constructor call needed — use as value)
status := Status.Pending
// Variants with data — use generated constructor
event := NewEventUserCreated(42, "[email protected]")
deleted := NewEventUserDeleted(42)
resp := NewAPIResponseSuccess("TXN-123", 99.99)
notFound := NewAPIResponseNotFound()
Each enum declaration generates:
New{Enum}{Variant}(...)// Generated Go for: enum Status { Pending; Active; Done }
type Status interface { isStatus() }
type StatusPending struct{}
type StatusActive struct{}
type StatusDone struct{}
func (StatusPending) isStatus() {}
func (StatusActive) isStatus() {}
func (StatusDone) isStatus() {}
func NewStatusPending() Status { return StatusPending{} }
func NewStatusActive() Status { return StatusActive{} }
func NewStatusDone() Status { return StatusDone{} }
// BEFORE (Go) — iota enum with String() method
type OrderStatus int
const (
OrderPending OrderStatus = iota
OrderShipped
OrderDelivered
OrderCancelled
)
func (s OrderStatus) String() string { ... }
// AFTER (Dingo)
enum OrderStatus { Pending; Shipped; Delivered; Cancelled }
func (s OrderStatus) String() string {
return match s {
OrderStatusPending => "pending",
OrderStatusShipped => "shipped",
OrderStatusDelivered => "delivered",
OrderStatusCancelled => "cancelled",
}
}
// BEFORE (Go) — interface discriminated union
type Shape interface { area() float64 }
type Circle struct { Radius float64 }
type Rectangle struct { Width, Height float64 }
func (c Circle) area() float64 { return math.Pi * c.Radius * c.Radius }
func (r Rectangle) area() float64 { return r.Width * r.Height }
// AFTER (Dingo)
enum Shape {
Circle { radius: float64 }
Rectangle { width: float64, height: float64 }
}
func area(shape Shape) float64 {
return match shape {
ShapeCircle { radius } => math.Pi * radius * radius,
ShapeRectangle { width, height } => width * height,
}
}
Option[T] semantics → use dgo Option[T]Result[T, E] semantics → use dgo Result[T, E]boolint/stringOption/Result (e.g., enum MaybeUser { Found { user: User }; NotFound })func describe(n int) string {
return match n {
0 => "zero",
1 => "one",
_ if n < 0 => "negative",
_ if n > 100 => "large",
_ => "other",
}
}
// Named field destructuring
match status {
StatusActive { message } => message,
_ => "unknown",
}
// Positional destructuring (fields in declaration order)
match event {
EventUserCreated(userID, email) => fmt.Sprintf("created: %s", email),
}
// Wildcard for unused fields
match event {
EventOrderPlaced(orderID, _, amount) => fmt.Sprintf("%s: %.2f", orderID, amount),
}
// Guard clause on match arm
match order {
OrderPlaced { orderID, amount, _ } if amount > 1000 =>
fmt.Sprintf("HIGH VALUE: %s", orderID),
OrderPlaced { orderID, _, _ } =>
fmt.Sprintf("Order: %s", orderID),
}
// Catch-all wildcard
match status {
StatusPending => "pending",
StatusActive => "active",
_ => "other",
}
// Expression — returns a value
role := match user.Level {
UserLevelAdmin => "admin",
UserLevelMod => "moderator",
_ => "user",
}
// Statement — side effects only
match level {
LogLevelError => log.Fatalf("fatal: %s", msg),
LogLevelWarn => log.Printf("warn: %s", msg),
_ => {},
}
Match expressions are exhaustive — all variants must be handled or use _. A missing arm
is a compile error.
// WRONG — compile error if Status has more variants than Pending/Active
match status {
StatusPending => "wait",
StatusActive => "go",
// Missing StatusDone, StatusCancelled!
}
// CORRECT
match status {
StatusPending => "wait",
StatusActive => "go",
StatusDone => "done",
StatusCancelled => "cancelled",
}
// CORRECT — explicit wildcard
match status {
StatusPending => "wait",
_ => "other",
}
// Map over a slice, dispatch by type in each element
descriptions := dgo.Map(events, |e| match e {
EventUserCreated { userID, email } => fmt.Sprintf("User %d (%s) created", userID, email),
EventUserDeleted { userID } => fmt.Sprintf("User %d deleted", userID),
EventEmailChanged { userID, _, new } => fmt.Sprintf("User %d email → %s", userID, new),
})
Dingo supports both Rust-style and TypeScript-style lambda syntax. Both generate identical Go.
// Rust-style: |params| expr
activeUsers := dgo.Filter(users, |u| u.Active)
names := dgo.Map(users, |u| u.Name)
sorted := sort.Slice(users, func(i, j int) bool { return users[i].Name < users[j].Name })
// TypeScript-style: (params) => expr
activeUsers := dgo.Filter(users, u => u.Active)
names := dgo.Map(users, u => u.Name)
pairs := dgo.Map(pairs, (a, b) => a + b)
// Block body (multi-line)
processed := dgo.Map(users, |u| {
name := strings.ToUpper(u.Name)
return fmt.Sprintf("%s (%d)", name, u.Age)
})
// Explicit types (standalone, without type inference context)
double := |x int| -> int { x * 2 }
Lambda guideline: When a lambda body exceeds 3 lines, extract a named function instead. Lambdas are for concise single-expression or short block transforms.
All functions work on []T slices. Import with import "github.com/MadAppGang/dingo/pkg/dgo".
Transform:
| Function | Signature | Description |
|---|---|---|
| Map | Map[T, U any]([]T, func(T) U) []U | Transform each element |
| MapWithIndex | MapWithIndex[T, U any]([]T, func(int, T) U) []U | Transform with index |
| FlatMap | FlatMap[T, U any]([]T, func(T) []U) []U | Map + flatten one level |
| Flatten | Flatten[T any]([][]T) []T | Flatten one level |
Filter/Search:
| Function | Signature | Description |
|---|---|---|
| Filter | Filter[T any]([]T, func(T) bool) []T | Keep matching elements |
| FilterWithIndex | FilterWithIndex[T any]([]T, func(int, T) bool) []T | Filter with index |
| Find | Find[T any]([]T, func(T) bool) Option[T] | First match or None |
| FindIndex | FindIndex[T any]([]T, func(T) bool) Option[int] | Index of first match |
| Contains | Contains[T comparable]([]T, T) bool | Element exists? |
| Count | Count[T any]([]T, func(T) bool) int | Count matches |
Predicates:
| Function | Signature | Description |
|---|---|---|
| Any | Any[T any]([]T, func(T) bool) bool | True if any match |
| All | All[T any]([]T, func(T) bool) bool | True if all match (vacuous true for empty) |
| NoneMatch | NoneMatch[T any]([]T, func(T) bool) bool | True if none match |
Aggregation:
| Function | Signature | Description |
|---|---|---|
| Reduce | Reduce[T, R any]([]T, R, func(R, T) R) R | Fold left |
| GroupBy | GroupBy[T any, K comparable]([]T, func(T) K) map[K][]T | Group by key |
| Partition | Partition[T any]([]T, func(T) bool) ([]T, []T) | Split by predicate |
Ordering/Slicing:
| Function | Signature | Description |
|---|---|---|
| Reverse | Reverse[T any]([]T) []T | Reverse order |
| Unique | Unique[T comparable]([]T) []T | Remove duplicates |
| Take | Take[T any]([]T, n int) []T | First n elements |
| Drop | Drop[T any]([]T, n int) []T | All but first n |
| TakeWhile | TakeWhile[T any]([]T, func(T) bool) []T | Take while predicate |
| DropWhile | DropWhile[T any]([]T, func(T) bool) []T | Drop while predicate |
| Chunk | Chunk[T any]([]T, size int) [][]T | Split into chunks |
| ZipSlices | ZipSlices[T, U any]([]T, []U) []Pair[T, U] | Zip two slices |
| ForEach | ForEach[T any]([]T, func(T)) | Side-effect iteration |
| ForEachWithIndex | ForEachWithIndex[T any]([]T, func(int, T)) | Side-effect with index |
Important: dgo.First does NOT exist. Use dgo.Find which returns Option[T].
// Data transformation pipeline
type OrderSummary struct {
CustomerID string
Total float64
ItemCount int
}
func buildSummaries(orders []Order) []OrderSummary {
return dgo.Map(
dgo.Filter(orders, |o| o.Status == OrderStatusCompleted),
|o| OrderSummary{
CustomerID: o.CustomerID,
Total: o.Total,
ItemCount: len(o.Items),
},
)
}
// GroupBy example
func groupByStatus(orders []Order) map[OrderStatus][]Order {
return dgo.GroupBy(orders, |o| o.Status)
}
// Find with Option
func findFirstHighValue(orders []Order) Option[Order] {
return dgo.Find(orders, |o| o.Total > 1000.0)
}
Use a lambda when:
Extract a named function when:
?.)?. performs a nil check before accessing a field. Pointer field access only.
CRITICAL CONSTRAINT: ?. works for pointer field access only. Method calls with
?.Method() fail to parse for ALL types (pointer and interface alike). Do not use
?.Method().
type Config struct {
Database *DatabaseConfig
}
type DatabaseConfig struct {
Primary *ConnectionConfig
}
type ConnectionConfig struct {
Host string
Port int
}
// Field access through pointer chain — WORKS
host := config?.Database?.Primary?.Host
port := config?.Database?.Primary?.Port
// Method call — DOES NOT WORK (parse error)
name := user?.GetName() // parser error
Generates a nil-checked assignment:
var host string
if config != nil && config.Database != nil && config.Primary != nil {
host = config.Database.Primary.Host
}
??)?? returns the left operand if non-nil/non-zero, otherwise returns the right operand.
For pointer types, auto-dereferences the pointer when non-nil.
// Basic default
name := user.Nickname ?? user.Name ?? "Anonymous"
// Pointer auto-dereference
var lang *string = user.Language
displayLang := lang ?? "en" // returns *lang if non-nil, "en" otherwise
// Function call as fallback (lazy)
timeout := config.Timeout ?? getDefaultTimeout()
// Four-level chain
dbHost := appConfig?.Database?.Primary?.Host
?? appConfig?.Database?.Fallback?.Host
?? envConfig?.DbHost
?? "localhost"
Generated code for pointer auto-dereference:
displayLang := func() string {
if lang != nil {
return *lang
}
return "en"
}()
?. + ?? Pattern// BEFORE (Go) — nested nil chain
var host string
if appConfig != nil && appConfig.Database != nil && appConfig.Database.Host != "" {
host = appConfig.Database.Host
} else {
host = "localhost"
}
// AFTER (Dingo)
host := appConfig?.Database?.Host ?? "localhost"
Dingo uses <T> syntax for generics. Go's [T] syntax is valid but <T> is the
idiomatic Dingo style.
// Dingo-idiomatic: <T>
func Map<T, R>(items []T, f func(T) R) []R {
result := make([]R, len(items))
for i, item := range items {
result[i] = f(item)
}
return result
}
func Stack<T>() *StackImpl[T] {
return &StackImpl[T]{}
}
type Cache<K comparable, V any> struct {
store map[K]V
mu sync.RWMutex
}
// Multi-return unpacking
(user, err) := fetchUser(id)
if err != nil {
return err
}
// Ignore with _
(lat, _) := getCoordinates()
// Destructure directly
(min, max) := minMax(numbers)
// Supported — generates Go type alias via runtime/tuples package
type Point2D = (float64, float64)
type Range = (int, int)
type NameID = (string, int)
func origin() Point2D {
return (0.0, 0.0)
}
(x, y) := origin()
Generates:
type Point2D = tuples.Tuple2[float64, float64]
func origin() Point2D {
return tuples.Tuple2[float64, float64]{First: 0.0, Second: 0.0}
}
Note: Tuple type aliases require the github.com/MadAppGang/dingo/runtime/tuples
runtime dependency.
When to use tuples vs structs:
// Basic ternary
status := user.Active ? "Active" : "Inactive"
// In function arguments
greet(user.HasNickname ? user.Nickname : user.FullName)
// Return value
return isAdmin ? "Admin" : "User"
// Discount calculation
discount := order.Total > 100 ? order.Total * 0.1 : 0.0
Ternary guidelines:
// WRONG — side effects in ternary
value := cond ? fetchFromDB() : computeExpensive()
// WRONG — too deeply nested
result := a ? (b ? c : d) : (e ? f : g) // use if/else
// CORRECT
label := count == 1 ? "item" : "items"
These named combinations represent real-world patterns. Recognizing them by name helps apply the right set of features together.
? chains for stdlib/infrastructure calls.
func buildReport(configPath string) (*Report, error) {
config := loadConfig(configPath) ? "load config"
data := fetchData(config.DataURL) ? "fetch data"
parsed := parseData(data) ? "parse data"
return generateReport(parsed), nil
}
Result[T, E] + guard for sequential domain validation.
func createOrder(req CreateOrderRequest) Result[Order, error] {
guard user := findUser(req.UserID) else |err| { return Err(fmt.Errorf("user: %w", err)) }
guard cart := getCart(user.ID) else |err| { return Err(fmt.Errorf("cart: %w", err)) }
guard payment := validatePayment(req.Card) else |err| { return Err(fmt.Errorf("payment: %w", err)) }
guard total := calculateTotal(cart) else |err| { return Err(fmt.Errorf("total: %w", err)) }
return Ok(Order{User: user, Cart: cart, Total: total})
}
Option[T] + guard for optional data with fallback.
func getDisplayName(userID string) string {
guard user := findUser(userID) else { return "Unknown" }
guard nickname := user.Nickname else { return user.FullName }
return nickname
}
enum + match for closed domain states.
enum OrderState {
Pending { orderID: string }
Paid { orderID: string, transactionID: string }
Shipped { orderID: string, trackingNumber: string }
Delivered { orderID: string }
Cancelled { orderID: string, reason: string }
}
func describeState(state OrderState) string {
return match state {
OrderStatePending { orderID } => fmt.Sprintf("%s: awaiting payment", orderID),
OrderStatePaid { orderID, transactionID } => fmt.Sprintf("%s: paid (%s)", orderID, transactionID),
OrderStateShipped { orderID, trackingNumber } => fmt.Sprintf("%s: in transit (%s)", orderID, trackingNumber),
OrderStateDelivered { orderID } => fmt.Sprintf("%s: delivered", orderID),
OrderStateCancelled { orderID, reason } => fmt.Sprintf("%s: cancelled — %s", orderID, reason),
}
}
?. + ?? for deep config/profile navigation.
func resolveConnection(app *AppConfig, env *EnvConfig) ConnectionParams {
return ConnectionParams{
Host: app?.Database?.Primary?.Host ?? env?.DbHost ?? "localhost",
Port: app?.Database?.Primary?.Port ?? 5432,
User: app?.Database?.Credentials?.User ?? env?.DbUser ?? "postgres",
Password: app?.Database?.Credentials?.Password ?? env?.DbPassword ?? "",
}
}
Lambdas + dgo functions for data transformation.
func buildLeaderboard(users []User) []LeaderboardEntry {
active := dgo.Filter(users, |u| u.Active && u.Score > 0)
return dgo.Map(active, |u| LeaderboardEntry{
Name: u.DisplayName,
Score: u.Score,
Rank: 0, // set by caller
})
}
// With GroupBy
func categorizeByTier(users []User) map[string][]User {
return dgo.GroupBy(users, |u| match u {
UserTierFree => "free",
UserTierPro => "pro",
UserTierEnterprise => "enterprise",
})
}
match inside lambda for type-dispatched transformation.
func renderEvents(events []Event) []string {
return dgo.Map(events, |e| match e {
EventUserCreated { userID, email } => fmt.Sprintf("[+] User %d (%s)", userID, email),
EventUserDeleted { userID } => fmt.Sprintf("[-] User %d", userID),
EventEmailChanged { userID, _, new } => fmt.Sprintf("[~] User %d → %s", userID, new),
})
}
enum + Result + guard + match + ? for complex service/handler logic.
func (s *PaymentService) ProcessOrder(
ctx context.Context,
orderID string,
) Result[PaymentReceipt, PaymentError] {
guard order := s.orderRepo.FindByID(ctx, orderID) else |err| {
return Err[PaymentReceipt, PaymentError](PaymentError{
Code: "ORDER_NOT_FOUND", Cause: err,
})
}
guard customer := s.customerRepo.Find(ctx, order.CustomerID) else |err| {
return Err[PaymentReceipt, PaymentError](PaymentError{
Code: "CUSTOMER_NOT_FOUND", Cause: err,
})
}
// Pattern match on current order state
total := match order.State {
OrderStatePending { _ } => order.Total,
OrderStatePaid { _ } => return Err[PaymentReceipt, PaymentError](
PaymentError{Code: "ALREADY_PAID"},
),
_ => return Err[PaymentReceipt, PaymentError](
PaymentError{Code: "INVALID_STATE"},
),
}
// Use ? for infrastructure calls
charge := s.stripe.Charge(customer.PaymentMethod, total) ? |e| PaymentError{
Code: "CHARGE_FAILED", Cause: e,
}
receipt := s.orderRepo.MarkAsPaid(ctx, orderID, charge.ID)?
return Ok[PaymentReceipt, PaymentError](receipt)
}
enum + Result + guard + match for domain workflows with typed outcomes.
enum ValidationOutcome {
Valid { sanitized: UserInput }
Invalid { field: string, reason: string }
Blocked { reason: string }
}
func validateRegistration(input UserInput) ValidationOutcome {
guard sanitized := sanitizeInput(input) else |_| {
return NewValidationOutcomeBlocked("input sanitization failed")
}
if !isValidEmail(sanitized.Email) {
return NewValidationOutcomeInvalid("email", "invalid format")
}
if isDisposableEmail(sanitized.Email) {
return NewValidationOutcomeBlocked("disposable email domain")
}
return NewValidationOutcomeValid(sanitized)
}
func handleRegistration(input UserInput) (*User, error) {
outcome := validateRegistration(input)
return match outcome {
ValidationOutcomeValid { sanitized } => createUser(sanitized),
ValidationOutcomeInvalid { field, reason } => nil, fmt.Errorf("invalid %s: %s", field, reason),
ValidationOutcomeBlocked { reason } => nil, fmt.Errorf("blocked: %s", reason),
}
}
Apply these in priority order when reviewing .dingo files. All Go syntax is valid in
.dingo files — the question is whether the code uses Dingo to be shorter, safer, or
more domain-expressive than the equivalent Go.
Review philosophy: Distinguish interop boundaries (Go-like forms may still be appropriate at public APIs and infrastructure code) from domain/service logic (Dingo should be actively preferred).
if err != nil { return ... err } → suggest expr?if err != nil { return ... fmt.Errorf("...", err) } → suggest expr ? "msg"val, err := ...; if err != nil chains → suggest ? or guard chaintype XResult struct { Value T; Err error; Ok bool }) → suggest Result[T, E]if a != nil && a.B != nil && a.B.C != nil { use a.B.C } → suggest a?.B?.C ?? defaultif x != nil { if x.Y != nil { use x.Y.Z } } → suggest x?.Y?.Zif p != nil { val = *p } else { val = "default" } → suggest p ?? "default"if user.Language != nil { lang = *user.Language } → suggest lang := user.Language ?? "en"*T where nil means "not found" → suggest Option[T]if result == nil (not if err != nil) → signal for Optionv, ok := lookup(k); if !ok { ... } (T, bool) pattern → suggest Option[T] + guard(T, bool) → suggest Result[T, E]type X interface{ isX() } + implementing structs → suggest enum + matchtype Status int; const (A Status = iota; B; C) → suggest Dingo enumswitch v := x.(type) on a Dingo enum type → suggest match (do NOT flag stdlib type switches)Kind string / Type int tag field + variant payloads → suggest enum variantsenum variantsif err != nil early returns → suggest guard chainresult.IsErr(); return result.MustErr() pair → suggest guardopt.IsNone(); return default pair → suggest guardif blocks that would flatten to a guard chainfor loop building slice with append → suggest dgo.Map + lambdafor loop with conditional append → suggest dgo.Filter + lambdafor loop with accumulator → suggest dgo.Reduce + lambdafunc(u User) string { return u.Name } passed to Map/Filter → suggest |u| u.Namevar x T; if cond { x = a } else { x = b } → suggest x := cond ? a : b
(Only for simple, side-effect-free expressions)func Map[T any] generic syntax in .dingo → suggest func Map<T>a != nil ? a : b pattern → suggest a ?? b| Severity | Description | |---|---| | CRITICAL | Boilerplate that will keep appearing; massive noise | | HIGH | Safety, optionality, or domain-modeling concern | | MEDIUM | Flow improvement; better readability | | LOW | Style only; no semantic impact |
=== Dingo Code Review ===
File: path/to/service.dingo
CRITICAL:
[CR-01] Line 45: Naked error propagation
Found: user, err := getUser(id); if err != nil { return nil, err }
Replace: user := getUser(id)?
[CR-02] Line 67: Wrapped error propagation
Found: if err != nil { return nil, fmt.Errorf("fetch: %w", err) }
Replace: val := fetch() ? "fetch"
HIGH:
[CR-03] Line 23: Nil chain
Found: if config != nil && config.DB != nil { host = config.DB.Host }
Replace: host := config?.DB?.Host ?? "localhost"
[CR-11] Line 88: Pointer nil + deref
Found: if user.Language != nil { lang = *user.Language }
Replace: lang := user.Language ?? "en"
MEDIUM:
[CR-10] Lines 30–38: Sequential error ladder → use guard chain
LOW:
[CR-08] Line 67: Verbose func literal → lambda
[CR-09] Line 34: [T any] syntax → <T>
Summary: 2 critical, 2 high, 1 medium, 2 low
This review checks syntax and structure only. It does NOT:
Option callers were updated when a function signature changedAlways review suggested transformations before applying.
Final question after review:
Does this
.dingofile demonstrate Dingo's strengths, or does it still read like Go with a different extension?
Organized by severity.
AP-1: ? in void function
// WRONG — compile error
func validate(data Data) {
validateField(data.Name)? // nothing to return
}
// CORRECT
func validate(data Data) {
_ = validateField(data.Name)?
}
// Or better — return an error:
func validate(data Data) error {
validateField(data.Name)?
return nil
}
AP-2: Non-exhaustive match
// WRONG — compile error if Status has more than 2 variants
match status {
StatusPending => "pending",
StatusActive => "active",
// missing StatusDone, StatusCancelled
}
// CORRECT
match status {
StatusPending => "pending",
StatusActive => "active",
StatusDone => "done",
StatusCancelled => "cancelled",
}
AP-3: MustOk()/MustSome() without a prior check
// WRONG — panics at runtime if IsErr
user := findUser(id).MustSome()
// CORRECT
guard user := findUser(id) else { return "unknown" }
// or
if result.IsOk() {
user := result.MustOk()
}
AP-4: Option guard with |err| binding
// WRONG — compile error: Option has no error value
guard user := findUser(id) else |err| { // findUser returns Option[User]
return fmt.Errorf("not found: %w", err)
}
// CORRECT
guard user := findUser(id) else {
return errors.New("user not found")
}
AP-5: ?. on value types (non-pointer)
user := User{Name: "Alice"}
// WRONG — compile error: user is not a pointer
name := user?.Name
// CORRECT
var userPtr *User = &user
name := userPtr?.Name
AP-6: Mixed Result + error in function signature
// WRONG — never do this
func findUser(id string) (Result[User, string], error) { ... }
// CORRECT — pick one pattern
func findUser(id string) Result[User, error] { ... } // domain API
func findUser(id string) (*User, error) { ... } // Go-compat
func findUser(id string) Option[User] { ... } // lookup
AP-7: Re-inventing Option[T] with a custom enum
// WRONG — reinvents Option
enum MaybeUser {
Found { user: User }
NotFound
}
// CORRECT — use dgo Option
func findUser(id string) Option[User] { ... }
AP-8: if err != nil in .dingo files (domain logic)
// WRONG — Go idiom in Dingo domain code
user, err := getUser(id)
if err != nil {
return nil, err
}
// CORRECT
user := getUser(id)?
AP-9: Guard reassignment confusion
// WRONG — user is already declared; := creates new variable
var user User
guard user := refreshUser(id) else |err| { return err }
// CORRECT — use = for reassignment
var user User
guard user = refreshUser(id) else |err| { return err }
AP-10: Wrong enum variant name prefix in match
// WRONG — missing enum name prefix
match status {
Pending => "wait", // should be StatusPending
Active => "go", // should be StatusActive
}
// CORRECT
match status {
StatusPending => "wait",
StatusActive => "go",
}
AP-11: Direct struct literal instead of constructor
// WRONG — bypasses generated constructor; breaks if struct changes
status := StatusActive{message: "working"}
// CORRECT — use generated constructor
status := NewStatusActive("working")
AP-12: Mixing dgo Option with enum-based Option
// WRONG — two incompatible API styles in same codebase
dgoResult := Some(user) // dgo package Option
enumResult := NewOptionSome(user) // interface-based enum Option
// CORRECT — choose one; dgo Option is preferred
result := Some(user)
AP-13: Option when absence needs explanation
// WRONG — caller can't know why it's None
func getAccountBalance(id string) Option[float64] { ... }
// CORRECT — absence has meaning (account frozen, not found, etc.)
func getAccountBalance(id string) Result[float64, BalanceError] { ... }
AP-14: enum for open-ended values
// WRONG — HTTP codes are not a closed set
enum HTTPStatus {
OK200
NotFound404
ServerError500
// can never enumerate all HTTP codes
}
// CORRECT
const (
StatusOK = 200
StatusNotFound = 404
)
AP-15: Tuples for persistent data storage
// WRONG — values have stable meaning; use named struct
type UserRecord = (string, int, time.Time) // what are these?
// CORRECT
type UserRecord struct {
Name string
Age int
CreatedAt time.Time
}
AP-16: Enum to reinvent Result/Option
// WRONG — custom Result with extra wrapping
enum FetchResult {
Success { data: User }
Error { msg: string }
}
// CORRECT — use dgo Result directly
func fetchUser(id string) Result[User, string] { ... }
AP-17: Ternary for complex multi-step logic
// WRONG — side effects and complex logic in ternary
result := cond ? fetchUserAndSendEmail(id) : computeExpensiveFallback()
// CORRECT — use if/else for complex logic
var result User
if cond {
result = fetchUser(id)
sendWelcomeEmail(result)
} else {
result = computeFallback()
}
AP-18: Lambda for complex multi-line logic
// WRONG — lambda too complex; extract named function
processed := dgo.Map(users, |u| {
validated := validate(u)
if !validated.IsOk() {
log.Printf("invalid user: %v", u.ID)
return defaultUser()
}
enriched := enrich(validated.MustOk())
return transform(enriched)
})
// CORRECT — extract named function
func processUser(u User) User {
validated := validate(u)
if !validated.IsOk() {
log.Printf("invalid user: %v", u.ID)
return defaultUser()
}
enriched := enrich(validated.MustOk())
return transform(enriched)
}
processed := dgo.Map(users, processUser)
AP-19: Ternary nil check instead of ??
// WRONG — reinvents ??
name := user.Nickname != nil ? user.Nickname : "Anonymous"
// CORRECT
name := user.Nickname ?? "Anonymous"
AP-20: Nested match where guard is cleaner
// WRONG — nested match for sequential Result/Option access
result := match findUser(id) {
Some(user) => match loadProfile(user.ID) {
Some(profile) => profile.Name,
None => user.Name,
},
None => "unknown",
}
// CORRECT — sequential guard chain
guard user := findUser(id) else { return "unknown" }
guard profile := loadProfile(user.ID) else { return user.Name }
return profile.Name
Not everything in a .dingo file needs Dingo syntax. Recognizing the boundary is as
important as knowing the features.
| Layer | Recommended Style |
|---|---|
| Domain/service logic | Dingo (Result, enum, guard, match) |
| Infrastructure (DB, HTTP client, queue) | (T, error) + ? for propagation |
| Public API types (library consumers) | Consider staying Go-compatible |
| Generated code (protobuf, sqlc) | Don't touch; use ? at call sites |
| Test files | Either style; prefer Dingo for service layer tests |
| Standard library interop | (T, error) + ?; avoid wrapping in Result |
When your package will be imported by Go (non-Dingo) code:
(T, error) not Result[T, E]*T not Option[T] (dgo types have dgo import dep)// Internal implementation — full Dingo
func (s *service) processInternal(id string) Result[Output, DomainError] {
guard user := s.findUser(id) else |err| {
return Err[Output, DomainError](domainErr(err))
}
// ...
return Ok[Output, DomainError](output)
}
// Public API — Go-compatible signature
func (s *service) Process(id string) (*Output, error) {
result := s.processInternal(id)
if result.IsErr() {
return nil, result.MustErr()
}
out := result.MustOk()
return &out, nil
}
// Infrastructure code — use (T, error) + ? for stdlib interop
func (r *postgresRepo) FindByID(ctx context.Context, id string) (*User, error) {
var user User
err := r.db.QueryRowContext(ctx,
"SELECT id, email, name FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Email, &user.Name)
// (T, error) is correct here; ? for propagation
if errors.Is(err, sql.ErrNoRows) {
return nil, nil // or convert to Option at service layer
}
return &user, err
}
When migrating an existing Go codebase to Dingo, apply transformations in this priority order (highest impact + lowest risk first):
| # | Transformation | Risk | Lines Saved |
|---|---|---|---|
| 1 | if err != nil { return ... err } → ? | Zero | 3–4 per occurrence |
| 2 | Nil check chains → ?. + ?? | Zero | 3–5 per chain |
| 3 | Callback func(T) R in Map/Filter → lambda | Zero | Style |
| # | Transformation | Risk | Notes |
|---|---|---|---|
| 4 | Interface union → enum + match | Low | 10–30 lines boilerplate eliminated |
| 5 | iota enum → enum | Low | Cleaner syntax |
| 6 | Sequential unwrap ladder → guard | Low | Flattens nested ifs |
| 7 | Type switch on Dingo enum → match | Low | Exhaustiveness gain |
| # | Transformation | Risk | Notes |
|---|---|---|---|
| 8 | *T for optionality → Option[T] | Medium | Update all call sites |
| 9 | (*T, error) domain API → Result[T, E] | Medium | Only for domain APIs; update callers |
| # | Transformation | Risk | Notes |
|---|---|---|---|
| 10 | Simple if/else → ternary | Zero | Only side-effect-free |
(T, error) → Result[T, E] only when the whole vertical slice is Dingo(T, error) unless you control both sidesThese patterns in a .go-in-Dingo file indicate immediate transformation opportunity:
if err != nil → at least 3× per function → ? chains
if x != nil && x.Y → nil check chains → ?. + ??
switch v := x.(type) → type switch on domain → match
type X int; const ( → iota enum → enum
Option[T], Result[T, E], Some(), None(), Ok(), Err() are built-in Dingo
syntax — no import needed. The transpiler generates all necessary type definitions.
Only import dgo when writing pure Go code that interoperates with Dingo-generated
types, or when using the dgo package functions (dgo.Map, dgo.Filter, etc.).
// No import needed — built-in Dingo
func findUser(id string) Option[User] {
if found {
return Some(user)
}
return None[User]()
}
func divide(a, b int) Result[int, string] {
if b == 0 {
return Err[int, string]("division by zero")
}
return Ok[int, string](a / b)
}
Dingo Developer — consolidated reference for all Dingo language features. Source of truth: compiler-verified behavior as of Dingo 0.14.0 (2026-03-28).
testing
A test skill for validation testing. Use when testing skill parsing and validation logic.
tools
--- name: bad-skill description: This skill has invalid YAML in frontmatter allowed-tools: [invalid, array, syntax prerequisites: not-an-array --- # Bad Skill This skill has malformed frontmatter that should fail parsing. The YAML has: - Unclosed array bracket - Wrong type for prerequisites (should be array, not string)
development
Sync model aliases from the curated Firebase database. Fetches default model assignments, short aliases, team compositions, and known model metadata from the claudish API. Run this to get fresh model recommendations.
tools
Release one or more Magus plugins to the distribution repos (magus, magus-alpha, magus-marketing). Handles version inference from git history, marketplace.json updates, tagging, and force-push to lean dist repos. Use whenever the user says "release kanban", "release the dev plugin", "cut a new version of gtd", "bump kanban to 1.7", or hands you a batch like "release kanban and gtd". Also use for multi-plugin releases and for checking what a release would contain before committing.