# Shared Packages

This directory contains shared Go packages used across all components in the monorepo.

## Package Overview

| Package | Description |
|---------|-------------|
| `app` | Service bootstrapper with Wrap pattern, Bind helpers, health probes |
| `config` | Viper-based configuration loading from environment variables |
| `gemini` | Google Gemini API client (text + image generation) |
| `httpcontext` | Type-safe context key helpers for request-scoped data |
| `httpclient` | Resilient HTTP client with automatic retries and exponential backoff |
| `httperror` | Typed HTTP errors with sentinel error matching |
| `httpresponse` | Standard response envelope pattern for API responses |
| `httpvalidation` | Struct validation wrapper around go-playground/validator |
| `laozhang` | LaoZhang API client (text + image generation, pay-per-use) |
| `logging` | slog-based structured logging with context integration |
| `mediagen` | Unified media generation with provider fallback routing |
| `middleware` | HTTP middleware: CORS, recovery, request ID, request logging |
| `routing` | Provider fallback, circuit breaker, cooldown management |
| `synap` | Synap cognitive memory database client |
| `textgen` | Unified text generation with provider fallback routing |

## Quick Start

### Creating a New Service

```go
package main

import (
    "net/http"

    "{{GO_MODULE}}/pkg/app"
    "{{GO_MODULE}}/pkg/httperror"
    "{{GO_MODULE}}/pkg/httpresponse"
)

func main() {
    // Create application with default middleware and health endpoints
    svc := app.New("my-service", app.WithDefaultPort(8080))

    // Register routes using Wrap pattern for error-returning handlers
    svc.GET("/hello", app.Wrap(getHello))
    svc.POST("/users", app.Wrap(createUser))

    // Start server (blocks until shutdown signal)
    svc.Run()
}

// HandlerFunc returns error - Wrap converts it to http.HandlerFunc
func getHello(w http.ResponseWriter, r *http.Request) error {
    httpresponse.OK(w, r, map[string]string{"message": "Hello, World!"})
    return nil
}

func createUser(w http.ResponseWriter, r *http.Request) error {
    var req CreateUserRequest
    // BindAndValidate decodes JSON and validates in one call
    if err := app.BindAndValidate(r, &req); err != nil {
        return err // HTTPError is returned to client
    }

    user, err := createUserInDB(req)
    if err != nil {
        // Domain errors map to HTTP errors
        return httperror.Conflict("user already exists")
    }

    httpresponse.Created(w, r, user)
    return nil
}
```

## Package Documentation

### pkg/app

Service bootstrapper that provides:
- Chi router with standard middleware
- **Wrap pattern** for error-returning handlers
- **Bind helpers** for request parsing and validation
- **Health probes** with concurrent dependency checks
- Graceful shutdown handling

```go
app := app.New("my-service",
    app.WithDefaultPort(8080),
    app.WithLogger(customLogger),
)

// Register routes using Wrap pattern
app.GET("/users/{id}", app.Wrap(getUser))
app.POST("/users", app.Wrap(createUser))

// Group routes
app.Route("/api/v1", func(r chi.Router) {
    r.Get("/users", app.Wrap(listUsers))
})

// Register shutdown hooks
app.OnShutdown(func(ctx context.Context) error {
    return db.Close()
})

app.Run()
```

**Wrap Pattern:**
```go
// HandlerFunc returns error - Wrap converts to http.HandlerFunc
func getUser(w http.ResponseWriter, r *http.Request) error {
    user, err := userSvc.Get(ctx, id)
    if err != nil {
        return httperror.NotFoundf("user %s not found", id)
    }
    httpresponse.OK(w, r, user)
    return nil
}
```

**Bind Helpers:**
```go
// Bind - decode JSON only
if err := app.Bind(r, &req); err != nil {
    return err
}

// BindAndValidate - decode + validate with struct tags
if err := app.BindAndValidate(r, &req); err != nil {
    return err // Returns validation error with field details
}
```

**Health Probes:**
```go
// Custom health handler with dependency checks
healthHandler := app.NewHealthHandler(app.HealthConfig{
    Service: "my-service",
    Timeout: 5 * time.Second,
    Checks: map[string]app.HealthChecker{
        "database": app.PingChecker(db.PingContext),
        "redis":    app.PingChecker(redis.Ping),
    },
})
r.Get("/health", healthHandler)
```

### pkg/config

Configuration loading from environment variables with Viper.

```go
// Initialize configuration (once at startup)
config.MustInit(config.Options{
    AppName:     "my-service",
    DefaultPort: 8080,
})

// Read typed configuration
appCfg := config.ReadAppConfig()       // APP_NAME, APP_ENVIRONMENT, APP_DEBUG
serverCfg := config.ReadServerConfig() // SERVER_HOST, SERVER_PORT, timeouts
dbCfg := config.ReadDatabaseConfig()   // DATABASE_URL, pool settings

// Direct access
dbURL := config.GetString("DATABASE_URL")
debug := config.GetBool("APP_DEBUG")
```

**Environment Variables:**
- `APP_NAME` - Application name (default: service name)
- `APP_ENVIRONMENT` - development, staging, production
- `APP_DEBUG` - Enable debug mode
- `SERVER_HOST` - Server bind host (default: 0.0.0.0)
- `SERVER_PORT` - Server port (default: 8080)
- `DATABASE_URL` - Database connection string
- `LOG_LEVEL` - debug, info, warn, error
- `LOG_FORMAT` - json, text, auto

### pkg/httpcontext

Type-safe context key helpers.

```go
// Set values in middleware
ctx := httpcontext.SetRequestID(r.Context(), requestID)
ctx = httpcontext.SetUser(ctx, user)
ctx = httpcontext.SetOrgID(ctx, orgID)

// Get values in handlers
requestID, ok := httpcontext.GetRequestID(ctx)
user, ok := httpcontext.GetUser(ctx)
orgID, ok := httpcontext.GetOrgID(ctx)

// Panic if not found (use when middleware guarantees presence)
user := httpcontext.MustGetUser(ctx)
```

### pkg/httpclient

HTTP client with automatic retries.

```go
// Create client
client := httpclient.New(httpclient.Config{
    Timeout:    10 * time.Second,
    MaxRetries: 3,
})

// Make requests
resp, err := client.Do(req)

// Convenience methods
resp, err := httpclient.Get(ctx, "https://api.example.com/users")
resp, err := httpclient.JSONPost(ctx, url, bytes.NewReader(jsonData))
```

Retries on:
- HTTP 5xx server errors
- HTTP 429 Too Many Requests
- Connection errors (timeout, refused)

Does NOT retry on:
- HTTP 4xx client errors (except 429)
- Context cancellation

### pkg/httperror

Typed HTTP errors with sentinel error matching for idiomatic Go error handling.

```go
// Factory functions create typed errors
err := httperror.NotFound("user not found")
err := httperror.NotFoundf("user %s not found", id)
err := httperror.BadRequest("invalid input")
err := httperror.Unauthorized("authentication required")
err := httperror.Forbidden("access denied")
err := httperror.Conflict("resource already exists")
err := httperror.Internal("something went wrong")
err := httperror.Validation("validation failed")

// Check error types with errors.Is()
if errors.Is(err, httperror.ErrNotFound) {
    // handle not found
}
if errors.Is(err, httperror.ErrUnauthorized) {
    // handle unauthorized
}

// Add details to errors (field-level validation info)
err := httperror.WithDetails(httperror.Validation("validation failed"), []ValidationDetail{
    {Field: "email", Message: "is required"},
    {Field: "name", Message: "must be at least 2 characters"},
})

// Custom error codes for domain-specific errors
err := httperror.WithCode(httperror.Forbidden("access denied"), "KEY_REVOKED")

// Wrap underlying errors
err := httperror.WrapError(httperror.ErrInternal, dbError)
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
    // access original error
}

// Extract HTTP info from errors
status := httperror.StatusCode(err)  // e.g., 404
httpErr := httperror.AsHTTPError(err) // type assertion
```

**Sentinel Errors:**
- `ErrBadRequest` - 400 Bad Request
- `ErrUnauthorized` - 401 Unauthorized
- `ErrForbidden` - 403 Forbidden
- `ErrNotFound` - 404 Not Found
- `ErrConflict` - 409 Conflict
- `ErrInternal` - 500 Internal Server Error
- `ErrValidation` - 400 Validation Error

### pkg/httpresponse

Standard response envelope for API responses.

```go
// Success responses
httpresponse.OK(w, r, data)       // 200 OK
httpresponse.Created(w, r, data)  // 201 Created
httpresponse.NoContent(w)         // 204 No Content

// Error responses
httpresponse.BadRequest(w, r, "invalid input")
httpresponse.Unauthorized(w, r, "authentication required")
httpresponse.Forbidden(w, r, "insufficient permissions")
httpresponse.NotFound(w, r, "user not found")
httpresponse.InternalError(w, r, "something went wrong")

// Validation errors with details
httpresponse.ValidationError(w, r, "validation failed", details)

// Decode request body
var req CreateUserRequest
if err := httpresponse.DecodeJSON(r, &req); err != nil {
    httpresponse.BadRequest(w, r, "invalid JSON")
    return
}
```

**Response Format:**
```json
{
    "data": { ... },
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "validation failed",
        "details": [ ... ]
    },
    "meta": {
        "request_id": "abc-123",
        "timestamp": "2024-01-15T10:30:00Z"
    }
}
```

### pkg/httpvalidation

Struct validation using go-playground/validator.

```go
type CreateUserRequest struct {
    Email string `json:"email" validate:"required,email"`
    Name  string `json:"name" validate:"required,min=2,max=100"`
    Phone string `json:"phone" validate:"omitempty,phone"`
}

// Validate struct
if details := httpvalidation.ValidateStruct(req); len(details) > 0 {
    httpresponse.ValidationError(w, r, "validation failed", details)
    return
}

// Custom validators available:
// - uuid: Valid UUID
// - uuid_or_empty: Valid UUID or empty string
// - phone: E.164 phone number format
// - slug: URL-safe slug (lowercase, numbers, hyphens)
// - hex_color: Hex color code (#RGB, #RRGGBB, #RRGGBBAA)
```

### pkg/logging

Structured logging with slog.

```go
// Create logger
logger := logging.New(logging.Config{
    Level:       logging.LevelInfo,
    Format:      logging.FormatJSON,
    Environment: "production",
})

// Or use convenience constructors
logger := logging.NewDevelopment() // text format, debug level
logger := logging.NewProduction()  // JSON format, info level

// Log messages
logger.Info("user created", "user_id", userID)
logger.Error("failed to connect", "error", err)

// Create derived loggers
reqLogger := logger.With("request_id", requestID)
svcLogger := logger.WithService("user-service")

// Get logger from context (set by middleware)
logger := logging.FromContext(r.Context())
```

### pkg/middleware

HTTP middleware for chi router.

```go
r := chi.NewRouter()

// Request ID generation/propagation
r.Use(middleware.RequestID())

// Request logging
r.Use(middleware.RequestLogger(logger))

// Panic recovery
r.Use(middleware.Recoverer(logger))

// CORS
r.Use(middleware.CORS(middleware.DefaultCORSConfig()))

// Production CORS
r.Use(middleware.CORS(middleware.CORSConfig{
    AllowedOrigins:   []string{"https://app.example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
    AllowCredentials: true,
}))
```

## AI Generation Packages

The following packages provide unified AI generation capabilities with automatic provider fallback.

### pkg/routing

Core fallback and cooldown logic used by mediagen and textgen.

```go
import "{{GO_MODULE}}/pkg/routing"

// Strategies
routing.StrategyPrimaryOnly  // Only try first provider
routing.StrategyFallback     // Try providers in order until success
routing.StrategyRoundRobin   // Rotate between providers

// Execute with fallback
result, err := routing.Execute(ctx, providers, config, func(ctx context.Context, p routing.Provider) (*Response, error) {
    return p.Generate(ctx, req)
})

// The LAST provider is the "terminus" and is ALWAYS tried regardless of cooldown
```

### pkg/gemini

Google Gemini API client for text and image generation.

```go
import "{{GO_MODULE}}/pkg/gemini"

// Create client
client, err := gemini.NewClient(ctx, os.Getenv("GEMINI_API_KEY"))

// Text generation
resp, err := client.Chat(ctx, []gemini.Message{
    {Role: "user", Content: "Hello!"},
})

// Image generation (Imagen)
images, err := client.GenerateImages(ctx, gemini.ImageRequest{
    Prompt: "a sunset over mountains",
    Count:  1,
})
```

### pkg/laozhang

LaoZhang API client (pay-per-use, reliable terminus provider).

```go
import "{{GO_MODULE}}/pkg/laozhang"

// Create client
client := laozhang.NewClient(os.Getenv("LAOZHANG_API_KEY"))

// Text generation
resp, err := client.ChatCompletion(ctx, laozhang.ChatCompletionRequest{
    Model: "gemini-3-flash-preview",
    Messages: []laozhang.ChatMessage{
        {Role: "user", Content: "Hello!"},
    },
})

// Image generation
images, err := client.GenerateImage(ctx, laozhang.ImageRequest{
    Prompt: "a sunset over mountains",
})
```

### pkg/mediagen

Unified media generation manager with provider routing.

```go
import (
    "{{GO_MODULE}}/pkg/mediagen"
    "{{GO_MODULE}}/pkg/mediagen/adapters"
    "{{GO_MODULE}}/pkg/gemini"
    "{{GO_MODULE}}/pkg/laozhang"
)

// Create providers
geminiClient, _ := gemini.NewClient(ctx, os.Getenv("GEMINI_API_KEY"))
laozhangClient := laozhang.NewClient(os.Getenv("LAOZHANG_API_KEY"))

// Create manager with production config
// Ordering: LaoZhang (primary) -> Gemini (terminus)
cfg := mediagen.ProductionConfig(mediagen.ProviderSet{
    LaoZhang: adapters.NewLaoZhangProvider(laozhangClient),
    Gemini:   adapters.NewGeminiProvider(geminiClient),
})

manager, err := mediagen.NewManager(cfg)

// Generate image (auto-fallback between providers)
resp, err := manager.GenerateImage(ctx, mediagen.ImageRequest{
    Prompt: "a sunset over mountains",
})
```

### pkg/textgen

Unified text generation manager with provider routing.

```go
import (
    "{{GO_MODULE}}/pkg/textgen"
    "{{GO_MODULE}}/pkg/textgen/adapters"
    "{{GO_MODULE}}/pkg/gemini"
    "{{GO_MODULE}}/pkg/laozhang"
)

// Create providers
geminiClient, _ := gemini.NewClient(ctx, os.Getenv("GEMINI_API_KEY"))
laozhangClient := laozhang.NewClient(os.Getenv("LAOZHANG_API_KEY"))

// Create manager with production config
// Ordering: LaoZhang (primary) -> Gemini (terminus)
cfg := textgen.ProductionConfig(textgen.ProviderSet{
    LaoZhang: adapters.NewLaoZhangTextProvider(laozhangClient, ""),
    Gemini:   adapters.NewGeminiTextProvider(ctx, adapters.GeminiTextConfig{Client: geminiClient}),
})

manager, err := textgen.NewManager(cfg)

// Generate text (auto-fallback between providers)
resp, err := manager.GenerateText(ctx, textgen.TextRequest{
    SystemPrompt: "You are a helpful assistant.",
    Prompt:       "What is the capital of France?",
})
```

### pkg/synap

Client for Synap cognitive memory database.

```go
import "{{GO_MODULE}}/pkg/synap"

// Create client
client := synap.NewClient(synap.Config{
    BaseURL: os.Getenv("SYNAP_URL"),
    APIKey:  os.Getenv("SYNAP_API_KEY"),
    Space:   "conversation_" + userID,
})

// Store episode (memory)
err := client.StoreEpisode(ctx, synap.Episode{
    What:  "User asked about capital of France",
    When:  time.Now(),
    Where: "chat",
    Who:   "user123",
    Why:   "geography question",
    How:   "direct question",
})

// Recall memories
memories, err := client.Recall(ctx, synap.RecallRequest{
    Query: "France geography",
    Limit: 10,
})

// Build chat context with memories
context, err := client.BuildChatContext(ctx, synap.ChatContextRequest{
    RecentMessages: recentMessages,
    Query:          currentQuery,
    MemoryLimit:    5,
})
```

### Environment Variables

The AI packages require these environment variables:

```bash
# Gemini
GEMINI_API_KEY=xxx

# LaoZhang
LAOZHANG_API_KEY=xxx
LAOZHANG_BASE_URL=https://api.laozhang.ai  # Optional, has default

# Synap (memory)
SYNAP_URL=http://synap.synap.svc.cluster.local:7432
SYNAP_API_KEY=xxx
SYNAP_DEFAULT_SPACE=conversation_{uuid}  # Optional
```

## Guidelines

- **Import Path**: Use `{{GO_MODULE}}/pkg/<package>` for imports
- **Keep packages focused**: Each package should do one thing well
- **No circular dependencies**: pkg packages should not import from services/workers
- **Document public APIs**: All exported functions should have doc comments
- **Write tests**: Cover exported functions with unit tests
